diff --git a/.claude/agents/code-architect.md b/.claude/agents/code-architect.md index 624316d14..fd46bd41c 100644 --- a/.claude/agents/code-architect.md +++ b/.claude/agents/code-architect.md @@ -16,7 +16,7 @@ architectural principles: 3. **Thin adapters**: each adapter translates platform types to/from core types. Business logic never lives in adapters. The adapter file structure is: - `context.rs`, `request.rs`, `response.rs`, `proxy.rs`, `logger.rs`, `cli.rs`. + `context.rs`, `request.rs`, `response.rs`, `outbound.rs`, `logger.rs`, `cli.rs`. 4. **Contract testing**: every adapter has `tests/contract.rs` that validates request/response mapping. New adapters must follow this pattern. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..d0ad3270c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Unified patches contain single-space context lines; check whitespace in the applied source. +patches/viceroy/*.patch -whitespace diff --git a/.github/actions/deploy-core/tests/run.sh b/.github/actions/deploy-core/tests/run.sh index 36267f247..8cf5e9526 100755 --- a/.github/actions/deploy-core/tests/run.sh +++ b/.github/actions/deploy-core/tests/run.sh @@ -2795,8 +2795,8 @@ test_fastly_logical_link_documentation() { grep -Fq -- '--service-id "$FASTLY_SERVICE_ID"' "$fastly_deployment" assert_fails "Fastly managed deployment does not recommend direct provider deployment" \ grep -Fq 'fastly compute deploy' "$fastly_deployment" - assert_succeeds "bare Fastly deploy is explicitly store-free production compatibility" \ - grep -Fq 'store-free production compatibility' "$fastly_deployment_flat" + assert_succeeds "bare Fastly deploy is explicitly limited to store-free production" \ + grep -Fq 'supports store-free production deployments using a manifest command' "$fastly_deployment_flat" # shellcheck disable=SC2016 # GitHub expression is the literal documentation contract. assert_fails "lifecycle examples do not reference a cross-job archive path output" \ diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index e6ebfe251..a3c63bb10 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -31,13 +31,14 @@ jobs: ~/.cargo/git/db/ target/ tests/fixtures/reusable-app/target/ + crates/edgezero-test-harness/target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo- - name: Retrieve Rust version id: rust-version - run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" shell: bash - name: Set up rust toolchain @@ -47,7 +48,10 @@ jobs: toolchain: ${{ steps.rust-version.outputs.rust-version }} - name: Fetch dependencies (locked) - run: cargo fetch --locked + run: | + cargo fetch --locked + cargo fetch --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml + cargo fetch --locked --manifest-path crates/edgezero-test-harness/Cargo.toml - name: Run cargo fmt run: cargo fmt --all -- --check @@ -55,8 +59,13 @@ jobs: - name: Run cargo fmt (lifecycle fixtures) run: cargo fmt --manifest-path tests/fixtures/reusable-app/Cargo.toml --all -- --check + - name: Run cargo fmt (test harness) + run: cargo fmt --manifest-path crates/edgezero-test-harness/Cargo.toml --all -- --check + - name: Run cargo clippy (native lifecycle fixtures) - run: cargo clippy --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -p fixture-core -p fixture-axum --all-targets -- -D warnings + run: | + cargo clippy --locked --manifest-path crates/edgezero-test-harness/Cargo.toml --all-targets -- -D warnings + cargo clippy --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-core -p fixture-axum --all-targets -- -D warnings - name: Run cargo clippy run: cargo clippy --workspace --all-targets --all-features -- -D warnings @@ -84,8 +93,8 @@ jobs: working-directory: examples/app-demo run: cargo clippy --workspace --all-targets --all-features -- -D warnings - adapter-wasm-clippy: - name: ${{ matrix.adapter }} wasm clippy + adapter-clippy: + name: ${{ matrix.check_name }} runs-on: ubuntu-latest strategy: fail-fast: false @@ -93,19 +102,32 @@ jobs: include: - adapter: cloudflare target: wasm32-unknown-unknown - features: cloudflare - - adapter: fastly - target: wasm32-wasip1 - features: fastly - # The `cli` feature is a valid wasip1 build combination (the CLI host - # code compiled for the guest target); gate it so a dead-code or - # portability regression there is caught, not only in the fastly-only build. + check_name: cloudflare wasm clippy + feature_sets: |- + cloudflare + cloudflare,test-utils - adapter: fastly target: wasm32-wasip1 - features: fastly cli + check_name: fastly wasm clippy + # The `cli` feature is valid on wasip1. Lint it independently so + # feature unification cannot mask target-specific cfg mistakes. + feature_sets: |- + fastly + fastly,cli + fastly,test-utils + fastly,cli,test-utils - adapter: spin target: wasm32-wasip2 - features: spin + check_name: spin wasm clippy + feature_sets: |- + spin + spin,test-utils + - adapter: axum + target: native + check_name: axum native clippy + feature_sets: |- + axum + axum,test-utils steps: - uses: actions/checkout@v6 with: @@ -126,7 +148,7 @@ jobs: - name: Retrieve Rust version id: rust-version - run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" shell: bash - name: Set up Rust tool chain @@ -136,17 +158,46 @@ jobs: components: clippy - name: Add wasm target + if: matrix.target != 'native' run: rustup target add ${{ matrix.target }} - name: Fetch dependencies (locked) run: cargo fetch --locked - - name: Clippy ${{ matrix.adapter }} (${{ matrix.features }}) on ${{ matrix.target }} - run: cargo clippy -p edgezero-adapter-${{ matrix.adapter }} --features "${{ matrix.features }}" --target ${{ matrix.target }} --all-targets -- -D warnings + - name: Clippy ${{ matrix.adapter }} feature combinations on ${{ matrix.target }} + env: + ADAPTER: ${{ matrix.adapter }} + FEATURE_SETS: ${{ matrix.feature_sets }} + TARGET: ${{ matrix.target }} + run: | + target_args=() + if [[ "${TARGET}" != "native" ]]; then + target_args=(--target "${TARGET}") + fi + + while IFS= read -r features; do + echo "==> edgezero-adapter-${ADAPTER} ${TARGET} features=[${features}]" + cargo clippy \ + --package "edgezero-adapter-${ADAPTER}" \ + --no-default-features \ + --features "${features}" \ + "${target_args[@]}" \ + --all-targets \ + -- \ + -D warnings + done <<<"${FEATURE_SETS}" - name: Clippy ${{ matrix.adapter }} lifecycle fixture - if: matrix.features != 'fastly cli' - run: cargo clippy --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-${{ matrix.adapter }} --target ${{ matrix.target }} --all-targets -- -D warnings + env: + ADAPTER: ${{ matrix.adapter }} + TARGET: ${{ matrix.target }} + run: | + target_args=() + if [[ "${TARGET}" != "native" ]]; then + target_args=(--target "${TARGET}") + fi + cargo clippy --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml \ + -p "fixture-${ADAPTER}" "${target_args[@]}" --all-targets -- -D warnings format-docs: runs-on: ubuntu-latest @@ -162,7 +213,7 @@ jobs: - name: Retrieve Node.js version id: node-version working-directory: . - run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" shell: bash - name: Use Node.js @@ -180,3 +231,10 @@ jobs: - name: Run Prettier (check) run: npm run format + + - name: Check outbound documentation contract + working-directory: . + run: node scripts/check_outbound_docs_contract.mjs + + - name: Build documentation + run: npm run build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eff6eb8ee..74659beb6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,6 +36,7 @@ jobs: ~/.cargo/git/db/ target/ tests/fixtures/reusable-app/target/ + crates/edgezero-test-harness/target/ key: ${{ runner.os }}-cargo-${{ matrix.suite }}-tests-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-${{ matrix.suite }}-tests- @@ -48,14 +49,28 @@ jobs: - name: Set up Rust tool chain uses: actions-rust-lang/setup-rust-toolchain@v1 with: + components: "clippy, rustfmt" toolchain: ${{ steps.rust-version.outputs.rust-version }} - name: Install wasm targets for generated-project checks - if: matrix.suite == 'cli' + if: matrix.suite == 'cli' || matrix.suite == 'workspace' || matrix.suite == 'app-demo' run: rustup target add wasm32-wasip1 wasm32-wasip2 wasm32-unknown-unknown + - name: Retrieve Node version + id: node-version + run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" + + - name: Set up Node for test and contract guards + uses: actions/setup-node@v6 + with: + node-version: ${{ steps.node-version.outputs.node-version }} + - name: Fetch dependencies (locked) - run: cargo fetch --locked + run: | + cargo fetch --locked + cargo fetch --locked --manifest-path examples/app-demo/Cargo.toml + cargo fetch --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml + cargo fetch --locked --manifest-path crates/edgezero-test-harness/Cargo.toml - name: No placeholder pins if: matrix.suite == 'workspace' @@ -65,6 +80,40 @@ jobs: if: matrix.suite == 'workspace' run: ./scripts/check_no_legacy_typed_reads.sh + # These roots are the active API surface. Historical design and plan + # documents under docs/superpowers intentionally remain immutable. + - name: No legacy outbound API + run: bash scripts/check_outbound_legacy_api.sh + + - name: Outbound documentation contract + run: node scripts/check_outbound_docs_contract.mjs + + - name: Compile mandatory forwarding example + if: matrix.suite == 'workspace' + run: | + node --test scripts/check_forwarding_example.test.mjs + node scripts/check_forwarding_example.mjs + + - name: Outbound evidence tool contracts + if: matrix.suite == 'workspace' + run: | + node --test scripts/check_outbound_cutover_evidence.test.mjs + node scripts/check_outbound_cutover_evidence.mjs --manifest + + - name: Build strict public API documentation + env: + RUSTDOCFLAGS: -D warnings + run: cargo doc --offline --locked --workspace --all-features --no-deps + + - name: Brotli dependency contract + run: node scripts/check_brotli_dependency_contract.mjs + + - name: Flate dependency contract + run: node scripts/check_flate_dependency_contract.mjs + + - name: Serde JSON map contract + run: node scripts/check_serde_json_map_contract.mjs + # `validator` is pinned in four places cargo cannot reconcile (this # workspace, the excluded demo workspace, the scaffold seeds, and the # deploy fixture). A mismatch only surfaces as a confusing @@ -82,11 +131,15 @@ jobs: - name: Lifecycle fixture host tests if: matrix.suite == 'workspace' - run: >- - cargo test --locked - --manifest-path tests/fixtures/reusable-app/Cargo.toml - -p fixture-harness - -p fixture-core + run: | + scripts/run_test_nonzero.sh outbound_evidence_assembly_retains_case_counts_and_hashes cargo test --offline --locked --manifest-path crates/edgezero-test-harness/Cargo.toml outbound_evidence_assembly_retains_case_counts_and_hashes + cargo test --offline --locked --manifest-path crates/edgezero-test-harness/Cargo.toml + cargo test --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-core --features qualification + node --test scripts/qualify_response_egress.test.mjs + + - name: Shared redirect policy contracts + if: matrix.suite == 'workspace' + run: scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests - name: Check Axum lifecycle fixture if: matrix.suite == 'axum' @@ -95,10 +148,63 @@ jobs: --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-axum + - name: Native redirect wire acceptance + if: matrix.suite == 'axum' + run: | + run="$GITHUB_WORKSPACE/tests/fixtures/reusable-app/.runs/outbound/redirect/axum" + CARGO_NET_OFFLINE=true ./scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-wire --output "$run" + jq --slurp --exit-status --arg adapter axum ' + ([.[] | select(.event == ($adapter + "-wire-inventory"))] + | length == 1 and .[0].status == "pass" and .[0].exchanges == 68 + and .[0].origin_requests == 84 and .[0].qualification == false) + and ([.[] | select(.event == ($adapter + "-wire-case"))] + | length == 68 and ([.[].origin_requests | length] | add) == 84) + and all(.[]; .status != "fail") + ' "$run/events.jsonl" + + - name: Preserve native lifecycle evidence + if: always() && matrix.suite == 'axum' + uses: actions/upload-artifact@v4 + with: + name: axum-lifecycle-evidence + path: tests/fixtures/reusable-app/.runs/ + include-hidden-files: true + if-no-files-found: ignore + + - name: Check opt-in lifecycle qualification fixtures + if: matrix.suite == 'workspace' + # These are compile-only identities, never deployed qualification evidence. + env: + EDGEZERO_QUALIFICATION_REVISION: "0000000000000000000000000000000000000000" + EDGEZERO_QUALIFICATION_ARTIFACT: "0000000000000000000000000000000000000000000000000000000000000000" + run: | + cargo check --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-axum --features qualification + cargo check --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-fastly --features qualification --bin fixture-fastly-qualification --target wasm32-wasip1 + cargo check --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-cloudflare --features qualification --target wasm32-unknown-unknown + cargo check --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-spin --features qualification --target wasm32-wasip2 + - name: Run workspace tests if: matrix.suite == 'workspace' run: cargo test --workspace --all-targets + - name: Bundled demo logging and fallback TCP contracts + if: matrix.suite == 'cli' + run: | + node --test scripts/run_test_nonzero.test.mjs + scripts/run_test_nonzero.sh --include-ignored demo_fallbacks_enforce_body_cap_over_tcp cargo test --offline --locked -p edgezero-cli --features demo-example --test demo_logging + + - name: Run native outbound adapter contracts + if: matrix.suite == 'workspace' + run: | + scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --test contract + scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils --test contract + scripts/run_test_nonzero.sh batch_preflight_rejects_streamed_slots_without_poisoning_siblings cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --test contract + scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils --test contract + + # The adapter CLI dispatch (build/deploy/stage/healthcheck/rollback) and + # its tests live behind the `cli` feature, which the unfeatured + # `cargo test --workspace` step above does not enable — so none of those + # tests compile or run there. Exercise them explicitly. - name: Run EdgeZero CLI tests if: matrix.suite == 'cli' run: cargo test -p edgezero-cli --all-targets @@ -115,23 +221,45 @@ jobs: --no-default-features --features cli + - name: Verify exact outbound capability matrices + if: matrix.suite == 'workspace' + run: | + scripts/run_test_nonzero.sh adapter_capability_matrix_matches_contracts cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features cli --lib adapter_capability_matrix_matches_contracts + scripts/run_test_nonzero.sh adapter_capability_matrix_matches_contracts cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cli --lib adapter_capability_matrix_matches_contracts + scripts/run_test_nonzero.sh adapter_capability_matrix_matches_contracts cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features cli --lib adapter_capability_matrix_matches_contracts + scripts/run_test_nonzero.sh adapter_capability_matrix_matches_contracts cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features cli --lib adapter_capability_matrix_matches_contracts + - name: Check feature compilation if: matrix.suite == 'workspace' run: cargo check --workspace --all-targets --features "fastly cloudflare spin" - - name: Verify a generated project compiles + - name: Check native adapter feature matrices + if: matrix.suite == 'workspace' + run: | + for adapter in axum cloudflare fastly spin; do + bash scripts/check_adapter_feature_matrix.sh "${adapter}" native + done + + - name: Verify generated project builds and fallback TCP policy if: matrix.suite == 'cli' - run: cargo test -p edgezero-cli --test generated_project_builds -- --ignored + run: scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds - name: Run Axum host tests if: matrix.suite == 'axum' - run: cargo test -p edgezero-adapter-axum --all-targets --features axum + run: scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets - name: Run app-demo workspace tests if: matrix.suite == 'app-demo' working-directory: examples/app-demo run: cargo test --locked --workspace --all-targets + - name: Check app-demo wasm targets + if: matrix.suite == 'app-demo' + working-directory: examples/app-demo + run: | + cargo check --locked -p app-demo-adapter-cloudflare --target wasm32-unknown-unknown --no-default-features --features cloudflare + cargo check --locked -p app-demo-adapter-fastly --target wasm32-wasip1 --no-default-features --features fastly + cargo check --locked -p app-demo-adapter-spin --target wasm32-wasip2 --no-default-features --features spin cargo-test: name: cargo test needs: host-tests @@ -154,14 +282,17 @@ jobs: target: wasm32-unknown-unknown runner_env: CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER runner_value: wasm-bindgen-test-runner + sentinel: dispatch_runs_router_and_returns_response - adapter: fastly target: wasm32-wasip1 runner_env: CARGO_TARGET_WASM32_WASIP1_RUNNER runner_value: viceroy run + sentinel: dispatch_runs_router_and_returns_response - adapter: spin target: wasm32-wasip2 runner_env: CARGO_TARGET_WASM32_WASIP2_RUNNER - runner_value: wasmtime run + runner_value: wasmtime run -W component-model-async=y -S p3=y -S http=y + sentinel: router_dispatches_get_and_returns_response steps: - uses: actions/checkout@v6 with: @@ -178,7 +309,8 @@ jobs: ~/.wasmtime/bin/ target/ tests/fixtures/reusable-app/target/ - key: ${{ runner.os }}-cargo-${{ matrix.adapter }}-${{ hashFiles('**/Cargo.lock', '.tool-versions') }} + crates/edgezero-test-harness/target/ + key: ${{ runner.os }}-cargo-${{ matrix.adapter }}-${{ hashFiles('**/Cargo.lock', '.tool-versions', 'patches/viceroy/0.21.0-body-readiness.patch') }} restore-keys: | ${{ runner.os }}-cargo-${{ matrix.adapter }}- @@ -195,6 +327,21 @@ jobs: - name: Add wasm target run: rustup target add ${{ matrix.target }} + - name: Retrieve Node version for Cloudflare guest contracts + if: matrix.adapter == 'cloudflare' + id: guest-node-version + run: echo "version=$(awk '$1 == "nodejs" {print $2}' .tool-versions)" >> "$GITHUB_OUTPUT" + + - name: Set up Node for Cloudflare guest contracts + if: matrix.adapter == 'cloudflare' + uses: actions/setup-node@v6 + with: + node-version: ${{ steps.guest-node-version.outputs.version }} + + - name: Verify Cloudflare guest stream fixture + if: matrix.adapter == 'cloudflare' + run: node --test crates/edgezero-adapter-cloudflare/tests/worker_runtime.test.mjs + - name: Resolve wasm-bindgen CLI version if: matrix.adapter == 'cloudflare' id: wasm-bindgen-version @@ -236,6 +383,48 @@ jobs: VICEROY_VERSION: ${{ steps.viceroy-version.outputs.version }} run: cargo install viceroy --version "$VICEROY_VERSION" --locked --force + - name: Build isolated Viceroy readiness repair + if: matrix.adapter == 'fastly' + id: viceroy-repair + shell: bash + env: + VICEROY_VERSION: ${{ steps.viceroy-version.outputs.version }} + VICEROY_REVISION: a69d982d1d95e13c0fc9f92683b9118639bb4b04 + VICEROY_LOCK_SHA256: d642d177081f42aec75d25c0d1124c013d3abcb081886251420a90e9c53c1a2a + VICEROY_PATCH_SHA256: 8693ce34f82cc98dc272bf39379007b17a40dfc19138c55b445378fd9a163759 + RUSTUP_TOOLCHAIN: 1.98.1 + ASDF_RUST_VERSION: 1.98.1 + CARGO_TARGET_DIR: ${{ github.workspace }}/target/viceroy-readiness + CARGO_PROFILE_DEV_DEBUG: 0 + CARGO_INCREMENTAL: 0 + run: | + test "$VICEROY_VERSION" = 0.21.0 + stock_identity="$(sha256sum "$(command -v viceroy)")" + source="$RUNNER_TEMP/viceroy-readiness-source" + patch="$GITHUB_WORKSPACE/patches/viceroy/0.21.0-body-readiness.patch" + evidence="$GITHUB_WORKSPACE/tests/fixtures/reusable-app/.runs/viceroy-repair" + test ! -e "$source" + git clone --depth 1 --branch "v$VICEROY_VERSION" https://github.com/fastly/Viceroy.git "$source" + test "$(git -C "$source" rev-parse --verify HEAD)" = "$VICEROY_REVISION" + printf '%s %s\n' "$VICEROY_LOCK_SHA256" "$source/Cargo.lock" | sha256sum --check --strict + printf '%s %s\n' "$VICEROY_PATCH_SHA256" "$patch" | sha256sum --check --strict + git -C "$source" apply --check "$patch" + git -C "$source" apply "$patch" + cargo fetch --locked --manifest-path "$source/Cargo.toml" + "$GITHUB_WORKSPACE/scripts/run_test_nonzero.sh" readiness_preserves_bytes_without_polling_the_source_again cargo test --offline --locked --manifest-path "$source/Cargo.toml" -p viceroy-lib --lib body::tests::readiness_ + "$GITHUB_WORKSPACE/scripts/run_test_nonzero.sh" readiness_preserves_between_bytes_timeout_without_repolling_source cargo test --offline --locked --manifest-path "$source/Cargo.toml" -p viceroy-lib --lib body::tests:: + cargo build --offline --locked --manifest-path "$source/Cargo.toml" -p viceroy + executable="$CARGO_TARGET_DIR/debug/viceroy" + test -x "$executable" + printf '%s\n' "$stock_identity" | sha256sum --check --strict + mkdir -p "$evidence" + { + printf 'upstream=%s\nprofile=dev debug=0 incremental=0\n' "$VICEROY_REVISION" + rustc --version + sha256sum "$source/Cargo.lock" "$patch" "$source/src/body.rs" "$executable" + } | tee "$evidence/build-identity.txt" + echo "executable=$executable" >> "$GITHUB_OUTPUT" + - name: Resolve Wasmtime version if: matrix.adapter == 'spin' id: wasmtime-version @@ -264,8 +453,8 @@ jobs: echo "$install_dir" >> "$GITHUB_PATH" export PATH="$install_dir:$PATH" version="$WASMTIME_VERSION" - # Compare exact versions so a pin like `44.0.1` does not silently - # accept `wasmtime 44.0.10` from a stale cache or runner-provided + # Compare exact versions so a pin like `48.0.2` does not silently + # accept `wasmtime 48.0.20` from a stale cache or runner-provided # binary. actual="$(wasmtime --version 2>/dev/null | awk '{print $2}' || true)" if [ "$actual" != "$version" ]; then @@ -281,12 +470,67 @@ jobs: wasmtime --version - name: Fetch dependencies (locked) - run: cargo fetch --locked + run: | + cargo fetch --locked + if [ "${{ matrix.adapter }}" = spin ] || [ "${{ matrix.adapter }}" = fastly ]; then + cargo fetch --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml + cargo fetch --locked --manifest-path crates/edgezero-test-harness/Cargo.toml + fi - name: Run ${{ matrix.adapter }} wasm tests + env: + ${{ matrix.runner_env }}: ${{ matrix.adapter == 'fastly' && format('{0} run', steps.viceroy-repair.outputs.executable) || matrix.runner_value }} + run: scripts/run_test_nonzero.sh ${{ matrix.sentinel }} cargo test --offline --locked -p edgezero-adapter-${{ matrix.adapter }} --no-default-features --features ${{ matrix.adapter }},test-utils --target ${{ matrix.target }} --test contract + + - name: Run Fastly closed lifecycle wasm sentinel + if: matrix.adapter == 'fastly' + env: + CARGO_TARGET_WASM32_WASIP1_RUNNER: ${{ steps.viceroy-repair.outputs.executable }} run + run: scripts/run_test_nonzero.sh closed_lifecycle_orders_hooks_and_returns_state_after_delivery cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --test contract + + - name: Run Spin WASI HTTP SDK resource tests + if: matrix.adapter == 'spin' + env: + ${{ matrix.runner_env }}: ${{ matrix.runner_value }} + run: scripts/run_test_nonzero.sh spin_error_code_table_is_exhaustive cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources + + - name: Run Spin eager exchange component + if: matrix.adapter == 'spin' + run: | + cargo build --offline --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --example outbound-eager + wasmtime run -W component-model-async=y -S p3=y -S http=y target/wasm32-wasip2/debug/examples/outbound_eager.wasm + + - name: Run Spin wire contracts + if: matrix.adapter == 'spin' + shell: bash + run: | + version="$(awk '$1 == "spin" {print $2}' .tool-versions)" + test "$version" = 4.1.0 + install_dir="$RUNNER_TEMP/spin-wire-$version" + mkdir -p "$install_dir" + archive="spin-v${version}-linux-amd64.tar.gz" + curl -fL "https://github.com/spinframework/spin/releases/download/v${version}/${archive}" -o "$install_dir/$archive" + printf '%s %s\n' aeeb892f94cf861f5302546faa0bdaa82cc4be30539621299746a6dc011f3b5a "$install_dir/$archive" | sha256sum --check --strict + tar -xzf "$install_dir/$archive" -C "$install_dir" + export PATH="$install_dir:$PATH" + test "$(spin --version | awk '{print $2}')" = "$version" + run="$GITHUB_WORKSPACE/tests/fixtures/reusable-app/.runs/outbound/redirect/spin" + CARGO_NET_OFFLINE=true ./scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-wire --output "$run" + jq --slurp --exit-status ' + ([.[] | select(.event == "spin-wire-inventory")] + | length == 1 and .[0].status == "pass" + and .[0].exchanges == 68 and .[0].origin_requests == 84 + and .[0].qualification == false) + and ([.[] | select(.event == "spin-wire-case")] + | length == 68 and ([.[].origin_requests | length] | add) == 84) + and all(.[]; .status != "fail") + ' "$run/events.jsonl" + + - name: Run Cloudflare outbound runtime unit tests + if: matrix.adapter == 'cloudflare' env: ${{ matrix.runner_env }}: ${{ matrix.runner_value }} - run: cargo test -p edgezero-adapter-${{ matrix.adapter }} --features ${{ matrix.adapter }} --target ${{ matrix.target }} --test contract + run: scripts/run_test_nonzero.sh streamed_upload_eof_yields_before_final_precedence cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --lib # The colocated runtime unit tests (config-store resolver, chunk # remediation) need the `fastly` feature and only RUN under Viceroy — the @@ -295,26 +539,68 @@ jobs: - name: Run fastly wasm runtime unit tests if: matrix.adapter == 'fastly' env: - ${{ matrix.runner_env }}: ${{ matrix.runner_value }} - run: cargo test -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 --lib + CARGO_TARGET_WASM32_WASIP1_RUNNER: ${{ steps.viceroy-repair.outputs.executable }} run + working-directory: crates/edgezero-adapter-fastly + run: ../../scripts/run_test_nonzero.sh backend_creation_error_table_is_exhaustive cargo test --offline --locked --no-default-features --features fastly,test-utils --lib + - name: Check ${{ matrix.adapter }} wasm feature matrix + run: bash scripts/check_adapter_feature_matrix.sh ${{ matrix.adapter }} ${{ matrix.target }} - name: Fastly reusable HTTP smoke if: matrix.adapter == 'fastly' + env: + VICEROY_BIN: ${{ steps.viceroy-repair.outputs.executable }} run: ./scripts/smoke_test_reusable_app.sh --adapter fastly --suite smoke - - name: Preserve Fastly lifecycle evidence - if: always() && matrix.adapter == 'fastly' + - name: Fastly production client progress + if: matrix.adapter == 'fastly' + shell: bash + env: + VICEROY_BIN: ${{ steps.viceroy-repair.outputs.executable }} + run: | + run="$GITHUB_WORKSPACE/tests/fixtures/reusable-app/.runs/ci-outbound-client-progress" + status=0 + ./scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-client-progress --output "$run" || status=$? + # Exit 2 retains the unqualified disposition; exit 1 and missing evidence fail CI. + test "$status" -eq 2 + jq --slurp --exit-status ' + ([.[] | select(.event == "fastly-client-progress-inventory")] + | length == 1 and .[0].status == "pass" + and .[0].probes == 12 and .[0].expected_probes == 12 + and .[0].origin_requests == 18 and .[0].qualification == false) + and ([.[] | select(.event == "fastly-p5-inventory")] + | length == 1 and .[0].status == "pass" + and .[0].probes == 23 and .[0].expected_probes == 23 + and .[0].origin_requests == 43 and .[0].qualification == false) + and any(.[]; .status == "unverified" and .qualification == false) + and all(.[]; .status != "fail") + ' "$run/events.jsonl" + + - name: Fastly redirect wire acceptance + if: matrix.adapter == 'fastly' + env: + VICEROY_BIN: ${{ steps.viceroy-repair.outputs.executable }} + run: | + run="$GITHUB_WORKSPACE/tests/fixtures/reusable-app/.runs/outbound/redirect/viceroy" + CARGO_NET_OFFLINE=true ./scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-wire --output "$run" + jq --slurp --exit-status --arg adapter fastly ' + ([.[] | select(.event == ($adapter + "-wire-inventory"))] + | length == 1 and .[0].status == "pass" and .[0].exchanges == 68 + and .[0].origin_requests == 84 and .[0].qualification == false) + and ([.[] | select(.event == ($adapter + "-wire-case"))] + | length == 68 and ([.[].origin_requests | length] | add) == 84) + and all(.[]; .status != "fail") + ' "$run/events.jsonl" + + - name: Preserve provider lifecycle evidence + if: always() && (matrix.adapter == 'fastly' || matrix.adapter == 'spin') uses: actions/upload-artifact@v4 with: - name: fastly-lifecycle-evidence + name: ${{ matrix.adapter }}-lifecycle-evidence path: tests/fixtures/reusable-app/.runs/ include-hidden-files: true if-no-files-found: ignore retention-days: 14 - - name: Check ${{ matrix.adapter }} wasm target - run: cargo check -p edgezero-adapter-${{ matrix.adapter }} --features ${{ matrix.adapter }} --target ${{ matrix.target }} - - name: Check ${{ matrix.adapter }} lifecycle fixture run: >- cargo check --locked diff --git a/.tool-versions b/.tool-versions index 9eda0b09a..5cfd52066 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,12 +1,6 @@ fastly 16.0.0 nodejs 24.20.0 rust 1.98.1 -# spin-cli is not pinned by asdf-style version managers (no -# asdf-spin plugin in widespread use). spin-sdk ~7.0 in the -# workspace imports wasi:http/types@0.3.0, which the Spin 4.0.x -# runtime does not provide; Spin 4.1+ is required for the demo's -# Spin adapter to boot. Install matching CLI manually -# (https://spinframework.dev/install) before running -# smoke_test_kv.sh spin; otherwise set SKIP_SPIN=1. +spin 4.1.0 viceroy 0.21.0 -wasmtime 48.0.1 +wasmtime 48.0.2 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..0f0304a63 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1053 @@ +# Changelog + +## Unreleased + +### Portable HTTP And Request Lifecycles (PR #275) + +The changes below cover [PR #275](https://github.com/IABTechLab/edgezero/pull/275), including its +implementation and review follow-ups. This is a hard API migration: retired APIs and enum variants +are removed rather than retained as compatibility aliases. Provider limitations are published +separately from the portable API and must be considered when declaring required capabilities. +The current outbound migration is summarized first. Earlier checkpoint entries below describe +their scope when introduced; the current summary supersedes their then-pending API work. + +### Head-First Outbound Hard Cut + +- Replace implementer `send` and raw-request batch hooks with paired `time`, explicit `progress`, synchronous + support validation, prepared `dispatch_head`, eager `begin` and prepared batching. Core + captures invocation time, performs validation/settlement once and finalizes provider heads. + Remove duplicate adapter preparation, header normalization, decoding and response collection. +- Return owned local futures from facade `send`, `begin` and `send_all_until`; no caller stack + frame is borrowed. Preserve original budgets, typed failures, independent caps, coding and + provenance, direct native Image Optimizer dispatch and provider capability limitations. +- Settle unknown GET/HEAD bodies to EOF or the first nonempty byte before provider dispatch, + racing paired timers with outbound and admitted inbound deadlines. Preserve certified empty + bodies without reading, source errors and the inbound 408/outbound 504 distinction. +- Replace public batch receipt/collection fields and Option slots with identity-bound receipts, + accessors, consuming decomposition and explicit AlreadyDelivered/Resolved/Unresolved states. + Add nonblocking observation and lossless accumulator recovery. Producers emit raw indexed + terminal observations from eagerly scheduled work; remove `from_futures`, `finish_batch_item` + and public receipt construction rather than retaining compatibility helpers. +- Preserve malformed/trailing driver validation, partial results, logical own-deadline timing, + strict cutoff equality and exactly-once local teardown. Migrate supplied clients, fixtures, + demo, generator and documentation together. Explicit redirect policy uses the same pipeline; + hosted qualification remains separate and this migration does not promote provider guarantees. +- Use one core scope implementation for callback/body ownership and revocable operation roots. + Capture retained Send futures, begun exchanges and batches at invocation, before first polling. + Retained clients resolve fresh request ownership, and later polling cannot reparent the work. + Require the typed provider-progress hook; Axum, Cloudflare and Spin share the same independent + implementation, while Fastly reuses its cooperative native multiplexer. Remove its private + lifetime registry rather than maintaining an alternate ownership path. +- Retain response-owned work through terminal egress, not source EOF; revoke retained source + handles before releasing completion resources. Transfer successful streamed outbound responses + independently of their original callback. Keep recorded batch partials and typed scope failures. +- Start early response-owned work with `OutboundScopeOwner::response_body()` and attach the exact + owner to its response. An unaccepted owner remains callback-bound; canonical egress accepts it + before policy, framing or suppression. Share provider progress across the handoff without + sharing callback cancellation. Failed application bodies close before bounded fallback delivery. + Run source disposal under its captured identity, preserve terminal notifications after supported + cleanup unwinding, and keep lifecycle rejection distinct from HTTP framing diagnostics. +- Replace public `Body::Once`/`Stream` construction and patterns with a private body carrier, + `Body::from_bytes`/`from_stream` and `BodyPayload` at transport boundaries. Carry local body + ownership without HTTP extensions, streamifying buffered bytes or extra payload copies. +- Share the revocable local task runner across Axum, Cloudflare and Spin. Fail scope registration + before spawning and release work on executor drop even while its cancellation owner is retained. + Use one source-binding implementation for outbound and downstream bodies, one core earlier-own- + deadline cutoff predicate, and observation-only nonblocking readiness. Remove copied task states + and the second Workers response-deadline wrapper without removing host yielding or abort ownership. +- Remove Fastly's elapsed-preparation slack rejection, its public constant and test-injection hook. + Dispatch and cold registration still reject expired budgets and backwards clocks. Preserve B31 + backend identity/timers and original absolute deadlines instead of restarting time after settlement. +- Preserve earlier own-deadline timeout results when Fastly or Spin preparation crosses the + observation cutoff, without starting expired provider work. Make supplied fixtures return + cutoff rather than premature EOF for exact-cutoff requests. Race Fastly's pending response + source against its write deadline without cancelling callback-owned siblings. + +### Explicit Redirect Following + +- Add `HttpClient::follow` with a required synchronous target policy and validated 1..=20 limit. + Plain send, begin and batches remain unfollowed. Every hop uses the same supplied client and + shared preparation/head finalization; no alternate transport or compatibility wrapper is added. +- Apply the specified status/method/body decision order, relative Location resolution and original + duplicate/nontext evidence. Replay buffered `Bytes` without copying; never buffer a streamed + upload for replay. Discard followed heads unread, including pending, erroneous, compressed and + oversized bodies. Stopped/final responses retain ordinary response policies and typed failures. +- Strip only Authorization, Cookie and authority overrides from automatically prepared cross-origin + requests. Preserve deliberate policy replacements, unchanged caps/options and arbitrary local + policy errors with their redirect number. Applications retain initial and per-hop target policy. +- Keep the initial absolute deadline through policy replacements and final streamed consumption, + while renewing per-hop timeouts and retaining a fresh request's normal default budget. + Capture invocation ownership and cancel through the same scoped operation machinery as send. +- Publish `outbound-redirect-following` for the portable policy, separate from provider deadline, + cancellation, allocation and hosted qualification guarantees. Preserve the native optimizer call. + Add real-TCP Axum parity and two-origin tests, deterministic supplied-client tests, documentation + guards, a mandatory core sentinel and execution of the existing Spin eager SDK component in CI. +- Stop reentrant scope cancellation before another dispatch, upload chunk or response-source + read, including cancellation from terminal source disposal instead of exposing clean EOF. + Reuse the shared source binder and retain clock history through stopped responses and + transport cleanup. Borrow the existing collection engine for buffered finalization instead of + allocating another payload box; keep public collector signatures and typed error precedence. +- Exercise the actual provider clients at their existing dispatch boundaries: Cloudflare guest + fetch fixtures, local Spin HTTP wire controls and repaired-Viceroy Fastly progress probes. + Compare plain Send with Follow for coding, caps and streamed final responses; verify unread + intermediate heads, stopped-body failures, hop metadata and native optimizer unavailability. + Run the Spin wire harness in its existing CI job and require the separate Fastly redirect + inventory without weakening the original progress inventory. These tests do not qualify + hosted cancellation, successful optimizer transformation or resource guarantees. +- Run the Spin host wire fixture in the existing Rust harness and remove the Python runner. + Preserve all 68 redirect exchanges, 84 correlated origin requests and existing raw-framing + and lifecycle controls. Reuse bounded HTTP capture, file-backed runtime logs and process-group + teardown; reject late worker failures and unexpected runtime exits before publishing success. + Keep one runner and no compatibility wrapper or additional host-language dependency. +- Reuse the Rust wire matrix for local Cloudflare workerd/origin redirect checks through the + standard request-context client. Preserve the Spin inventory and require exact provider-specific + source errors. Cover cleanup before runtime startup and distinguish confirmed requested SIGTERM + wrapper exits from premature runtime exits. These local checks do not qualify hosted behavior. +- Add fixed Begin/Batch lifetime probes to the same Rust harness and production-client guests. + Preserve actual host writes, guest observations, original cutoffs, typed failures and mixed + collection states. Reject late or incomplete evidence; keep timeline misses and provider + progress failures visible rather than widening tolerances or promoting qualification. +- Pin the coordinated Spin SDK executor upgrade to immutable Stackpop fork revision + `e65ff90dadf4173b08b14194e878ad3acd555337`; propose it upstream in + [spin-rust-sdk#169](https://github.com/spinframework/spin-rust-sdk/pull/169). + Hard-migrate SDK spawn calls to `spawn_local`, using WASIp3 0.9.0 / wit-bindgen 0.62.0 + through one SDK executor. Align the root, demo, reusable fixture and generated manifests; + compare Git source and revision as well as package version in dependency guards. + Preserve absolute deadlines, cutoffs and all limits; scheduler-latency and hosted qualification + remain separate gates. See the [dependency audit](docs/audits/2026-10-27-spin-sdk-eager-progress.md). + +### Main Integration + +- Move the shared native HTTP test runner to the unpublished, standalone + `crates/edgezero-test-harness` workspace. Keep fixture applications separate, preserve guest + build/output paths, record both lockfiles and remove the old runner package without aliases. + Update active CI and commands; preserve all four redirect inventories and bind exact core/runtime + acceptance tests without promoting hosted qualification or optional idle-body policy. + +- Integrate main's IABTechLab repository transfer; align outbound guide links, + generated READMEs and capability diagnostics with the canonical repository + and GitHub Pages site. +- Merge main's Rust 1.98.1 toolchain and dependency upgrades without restoring + retired outbound or lifecycle APIs. Keep audited decoder and SDK requirements + exact across the workspace, demo and generated applications. Adapt SHA-256 + digest formatting without reintroducing full canonical-output staging. +- Validate shared scaffold, demo and fixture dependency requirements with TOML + parsing, including exact SDK pins; regenerate all three workspace lockfiles. +- Retain the fallible application assembly, pinned runtime targets and adapter-owned + ingress/egress while integrating immutable release deployment from main. Deployment + capability checks run before preflight or provider work; fallback dispatch retains + the selected target instead of rediscovering source from the working directory. + Remove the Fastly direct-deploy wrapper; callers supply `AdapterDeployContext`. +- Retire Fastly runtime selector-store setup. Applications open stable logical store + aliases; deployment selects and reconciles physical resource links. Logging policy + comes from baked `Hooks::logging_for("fastly")`, including custom entrypoints. +- Align bounded Axum secret reads with the prefixed uppercase environment mapping + used by unbounded reads, preserving independent byte caps and deadlines. + +### Guest Validation + +- Honor cooperative source wakes in Fastly's existing progress loop instead of entering a + blocking host selection after a ready-to-repoll body yield. Preserve expiry publication, + nested progress, retained wakers and normal waits on later pending polls. Keep the caller's + waker out of persistent progress state, without changing deadlines, read sizes or runtime pins. +- Compile Spin's test-only helpers only with the feature that calls them. Verify both + `spin` and `spin,test-utils` WASM all-targets builds, without suppressing unused-code + diagnostics or changing runtime behavior. +- Keep Fastly's declaration ordering valid in CLI-only tests, where the runtime-feature + ordering exception does not apply. Extend the existing conflicting module-file lint + exception to native test builds without changing scheduling or transport behavior. +- Run Cloudflare guest contracts on pinned Node, matching the library guest suite. Assert + native request-body presence before conversion, reuse scoped numeric-timer guards, and + model only the fixed-length byte-stream surface missing from that host in a test fixture. +- Test exact bytes, length violations, backpressure, cancellation, abort and large declared + lengths. Preserve exact canonical response-header assertions and exercise the default + configuration-error renderer rather than a bodyless workaround. Guest tests are not + Workers-host qualification; production transport and capability declarations are unchanged. + +### Forwarding Documentation Gate + +- Compile the forwarding example directly from the guide using offline/locked Cargo artifacts + and rustdoc. Require one non-ignored passing test without invoking the network handler. +- Add real compiler regressions and workspace CI activation with pinned Node. This compile-only + prerequisite does not replace runtime forwarding tests or provider qualification. + +### Fastly Cooperative Receive Progress + +- Replace blocking pending-request waits and selected-body drains with one private progress + engine shared by the existing Send and Batch receive paths. Select borrowed handle identities, + validate every result, rotate slot service, and perform at most one 16 KiB raw read per fresh + matching readiness observation. Remove the superseded ownership-reassociation implementation. +- Preserve cached SDK Send behavior with nonblocking opaque-head polling and one shared positive + 1 ms pulse. Keep B31, original deadlines/cutoffs, typed source errors and response policies; + leave direct native Image Optimizer dispatch unchanged. Ready results backpressure only their + own slot, with exactly-once local lease cleanup on cancellation/terminal paths. +- Release batch drivers on terminal or poisoned observations even when callers retain the batch. + Preserve provider details through the outer Fastly deadline guard and release stream sources + before publishing terminal errors. Neither local cleanup nor a dropped pending request proves + provider-side cancellation. +- Validate local/CI Fastly receive paths using the explicitly selected, hash-verified Viceroy + source repair without replacing the installed runtime. Do not upgrade capabilities or claim + hosted qualification. Callback-wide ownership, eager public modes, full pipeline migration + and dispatch-slack retirement remain open. +- Gate the actual-client functional inventory through the existing harness's explicit + `outbound-client-progress` suite. Preserve the broader `outbound-readiness` SDK diagnostic, + strict timing assertions and failed observations separately. Functional CI requires complete + client evidence and exactly the unqualified exit-2 disposition, not a qualification pass. + +### Fastly Batch Lifetimes + +- Retain the fixture callback's effective injected HTTP client for post-delivery work, including + routed handler errors. Remove its manual Fastly client construction and prepare-time override; + preserve state precedence, request isolation, streaming and fallback/no-attempt gates. + +- Give each custom callback a fresh private batch owner; standalone send helpers establish a + fallback owner, while nested helpers reuse the callback. Resolve ownership per operation so + retained clients work in the next callback without retaining the previous request's scope. +- Release complete drivers, processing state and queued outcomes at scope exit even when batch + handles are retained. Wake pending consumers and report typed internal scope closure once; + do not misclassify it as a cutoff or discard results collected before failure. +- Revoke all roots before reentrant cleanup, keep the closing owner active until teardown ends, + and reject new sends/batches before preparation or provider work. Clone/drop wakers outside + driver borrows and observe closure before returning Pending. +- Independently own batches created during response-body production through transmission cleanup. + Normal completion and source/write failure release them without cancelling callback work or + post-delivery operations. Keep driver EOF validation and exactly-once guest cleanup intact. +- Preserve current transport, clocks, limits, B31, direct native image optimization and public + APIs. Full Send-root cancellation, eager/head-first migration, callback-wide progress and + provider cancellation/hosted qualification remain open; capabilities are unchanged. + +### Fastly Mixed-Phase Readiness Diagnostic + +- Extend the existing local SDK/ABI suite with pending-head/ready-body and pending-body/delayed-head + cases, plus a multi-buffer truncated response. Require full 16 KiB reads, exact payload evidence, + fresh per-read readiness and preserved terminal errors for buffered and streamed consumers. +- Append separately validated observations without changing the original 135-probe inventory or + replaying earlier failures under a weaker rule. Retain stock and explicitly repaired runtime + outcomes, including timing failures. This does not migrate production dispatch, approve a read + maximum, change installed runtime pins, upgrade capabilities or qualify hosted behavior. +- Remove the Axum partial-result contract test's wall-clock race: gate the slow origin response + and advance injected time only after collection stores the successful fast slot. Keep the + 25 ms logical cutoff, exact result assertions and an independent finite containment watchdog. + +### Shared Outbound Header Policy + +- Route all four adapter response paths and `OutboundHead::new` through + `OutboundHeaderInput::normalize`. Charge original fields before filtering, retain Content-Type + and Location evidence, then charge the optional synthetic marker once. Preserve unknown + provider fidelity, bodyless/205 disposition and marker-off upstream fields. Reuse static + adapter-marker bytes without another allocation or retained raw-header copy. +- Remove the separate public `normalize_response_headers` and `insert_proxy_header` helpers + and their root exports. Adapter implementations use the shared operation; defensive final + response normalization remains private and does not repeat accounting or marker insertion. +- Document hard-cut adoption without a second application transport, while retaining current + positional/partial batch results, cancellation, cutoff, timing, typed errors, limits, encoding, + provenance and ingress/egress ownership. Demo and template request APIs are unchanged here. + This checkpoint does not complete eager/head-first/redirect dispatch or change SDK ownership, + provider deadlines, backend policy, capability levels or hosted qualification. +- Correct existing Spin fallback contract expectations to require the exact `Content-Length` + added by explicit buffered framing. Preserve status/body checks and all resource-release assertions. + +### Spin Supported-Option Precedence + +- Reject unsupported Host authority overrides before portable method/body/policy validation on + both existing single-send and batch paths. Keep Image Optimizer rejection first and preserve + the authority diagnostic. Pending GET/HEAD bodies, nonportable methods, invalid Brotli settings + and zero timeouts cannot mask the unsupported setting or cause body polling. +- Add native and actual WASI SDK preflight regressions. This changes no public signature, + transport, timer, capability, runtime pin or batch-cutoff semantics; full shared preparation + and the head-first/eager migration remain pending. + +### Shared Dispatch Validation + +- Hard-cut the public adapter helper to `validate_for_dispatch(&request, OutboundSendMode)`. + Migrate every in-tree caller without a one-argument wrapper. Core owns buffered Batch shape + checks instead of four adapter copies; supported-option checks remain adapter-owned and first. +- Preserve method/upload-cap precedence, existing batch diagnostics, zero source polling and + observation-cutoff behavior. Begin requires buffered uploads but permits streamed responses; + Image Optimizer remains unavailable in Begin/Batch. This does not implement eager dispatch + or redirect following, change handler/demo/template APIs, or upgrade provider capabilities. + +### Spin Connect-Setting Fallback + +- Treat every host connect-timeout setter refusal as advisory under the existing aggregate + deadline race. Do not turn `Immutable` or `Other` into a request-fatal 500 or expose provider + text. Keep first-byte/between-bytes typed setup failures unchanged. +- Add SDK outcome coverage and a local asynchronous Spin service probe for actual timer wakeup, + ready success, expired-entry zero polling, timeout provenance and guest-owner release. This + does not add independently configured connect timeouts or upgrade provider capabilities. + +### Outbound Response Handoff Deadlines + +- Check the selected absolute budget immediately before returning any successful outbound + response on all four adapters. Fix Axum, Cloudflare and Spin bodyless/streamed head handoffs + that could return late success after normalization; retain Fastly's existing enforcement. +- Replace duplicated early-success constructors with one checked return per adapter. Keep + header/source/cap errors, body suppression, 205 handling, application clocks and lazy streams + unchanged. A rejected handoff releases its native source; Cloudflare's already-disarmed + known-empty 205 guard stays disarmed. No public API, capability or runtime pin changes. + +### Axum Host-Known-Empty Ingress + +- Preserve transport-certified `is_end_stream()` bodies as buffered empty after admission on + Axum, for every method, without polling the native body. Keep request-context state Initial, + not Cached, and ingress framing HostManaged. At that checkpoint, unknown streams and all + other adapters were unchanged. +- Forward actual HTTP/1 GET/HEAD requests with absent framing or `Content-Length: 0` through + the existing client API, even after the inbound read deadline. Chunked-empty and nonempty + streamed GET controls still return 400 without contacting the origin and preserve connection + boundaries. Demo and generated proxy handlers benefit from the same converter without API changes. +- Implement only P2's existing known-empty conversion subtask as an engineering sequencing + clarification, not a new approval or full P2 completion. Core first-`body_bytes` deadline + precedence stays unchanged. General outbound settlement and support/time/head hooks remain + with atomic P1; no capability row, promotion, backend policy, timer or slack changes. + +### Fastly SDK Body Absence + +- Preserve only actual SDK body absence (`try_take_body() == None`) as core `Body::empty()` + in direct conversion and admitted dispatch, without creating an SDK body. Every present + SDK body remains lazy, even with known host length zero. Preserve prefetched read bytes + and pending writes after the prepare hook, with no header/hint inference, repacking, read + or flush during conversion. Request-context state remains Initial, not Cached; core's first + `body_bytes` read-deadline check can still return 408. +- Add actual WASI SDK regressions showing payload remains in prefetched read and pending-write + buffers despite zero host length. Host length is not EOF even beyond SDK buffering: + Viceroy uses Hyper exact size hints, not a terminal-body signal. +- G3's actual `from_client()` requests always supplied a body, including absent framing and + CL0. This checkpoint does not fix ordinary Fastly bodiless forwarding or general settlement. + Demo/generated proxy APIs remain unchanged and share the converter; this does not establish + raw Fastly GET proxy success. This is scope selection, not new user approval or a Native + claim; no capability rows/promotions, B/backend policy, slack, timers or SDK/runtime pins change. + +### Cloudflare Native Body Absence + +- Preserve native Fetch `Request.body == null` as buffered empty in direct conversion and + admitted dispatch, without a read. Remove the unsupported ArrayBuffer-only fallback and unused + attachment helper. Every present body remains streamed, including an empty ArrayBuffer or + Uint8Array; headers and SDK bookkeeping do not imply absence. Admission refusal/abort still + precedes source inspection, and consumed present bodies keep their construction error. +- Keep context state Initial, HostManaged framing and the first-read deadline/poisoning contract. + Demo/generated entrypoints inherit the converter unchanged. Add native WASM and actual local + workerd source-presence regressions; this does not complete general settlement, eager/head-first + migration, provider qualification or change SDK pins, capabilities or backend policy. + +### Native Image Optimizer + +- Record the requester's scoped 2026-10-24 timing/native-integration approval: original logical + deadlines and late-result rejection remain; +5 ms stays a measured qualification target/test, + not a hard scheduler guarantee. The synchronous optimizer blocking horizon is explicitly UNKNOWN + until provider evidence. Preserve historical failures; hosted/performance qualification stays blocked. +- Add a Send-first optimizer checkpoint through existing `execute`/`process_response`, with typed + request/parts attachment, support-first checks on all four adapters, direct SDK single-Send mapping, + `BadGatewayReason::ImageOptimizerUnavailable` on the non-exhaustive core reason enum and post-return + original attributed 504. Map SDK `SendErrorCause::ImageOptimizerUnsupported` to a 502 while preserving + the existing message `Fastly outbound request failed` exactly; only the typed reason/provider detail + distinguish this cause. Keep the ordinary SDK send branch + and current trait unchanged; add no separate transport, default, alias or compatibility helper. + This is an implementation sequencing decision, not approval of different signatures or a claim + of full migration completion. Full EZ-1 settlement, Follow, B/backend policy, engine and atomic head-first + migration stay open. Backend B/resource constraints, policy, timers and slack are not approved or + changed; typed body-error/readiness safety remains required. No deployment or performance claim. +- Clarify the checkpoint's timing anchor: the current async `send` selects its budget at adapter + method entry on first poll, before SDK preparation/registration, and preserves it across the native + call. Facade-invocation capture and full P1 remain pending with the later atomic head-first/trait + cut. Static inspection confirms native SDK `.send` selection, not runtime host entry. The + `image_optimizer_unavailable_is_typed_on_viceroy` test proves option retention and typed unavailable + errors only; SDK `.send_async` can return the same unsupported error without host entry. Synthetic + successful-response cap/laziness checks do not establish hosted image transformation or qualification. +- Publish `outbound-image-optimizer` as BestEffort for Fastly single sends and Unsupported on + other adapters. Add a Fastly-only `/image-demo` example to the demo and matching generated + projects, retaining response limits, native default caching and explicit typed settings. +- Remove the redundant conditional source-error drop from Fastly's no-attempt constructor; + retain original private source ownership and check the native `cli,test-utils` build too. + +### Fastly B31 Backend Policy + +- Adopt the approved Fastly B31 aggregate backend policy: stable versioned names, a shared + verified registration registry, ceiling-rounded coupled timers and unchanged logical deadlines. + Remove client namespaces and the exact-budget/quarter-split policy. Readiness, dispatch guards, + runtime pins and hosted/resource qualification remain separate and unchanged. + +### Qualification Evidence Preparation + +- Exercise the B31 proposal's 62 timer vectors across coupled/explicit Connect scopes through + the pinned SDK and stock local Viceroy, with typed overflow and setter-omission controls. + This is local registration evidence only; production policy and qualification remain open. +- Prepare a pending Fastly G2/G4 backend-policy/readiness review packet. Compare literal B31/B1, + execute full-domain boundary/timeout-anchor arithmetic, and retain separate SDK/provider, + runtime, resource and qualification gates. No production policy or runtime changes. +- Reject empty measurement lists for comparison-required legs and mark hosted Image Optimizer + acceptance as comparison-required. This checks evidence presence, not metric-policy sufficiency. + Retain a fresh stock/repaired local recheck and replay its negative cleanup/timing/error results; + runtime integration, hosted performance and Fastly policy approval remain open. No deployment. +- Record native Image Optimizer parity requirements and exact integration/test steps. Require + direct typed SDK mapping, lazy bodies, unchanged backend reuse and no extra fetch, plus paired + hosted application/SDK-only performance evidence. Runtime integration, numerical horizon/policy + approval and performance qualification remain open; no optimizer execution is enabled. +- Add bounded native/WASI/empty-select timer controls and raw ABI timing inside the opt-in Fastly + diagnostic. Retain lateness and matched stock/repaired artifacts without inferring a timer fix, + changing +5 ms acceptance or replacing previous failed evidence. Production APIs/pins are unchanged. +- Expand the opt-in Fastly readiness diagnostic to eleven controlled origin cases, buffered and + pull-streamed consumption, two read capacities and three repetitions: 135 probes / 147 requests. + Require complete inventories, fresh per-slot readiness, unchanged absolute deadlines, typed + source errors and joined bounded origins. Persist partial traces before failures. Retain negative + timing evidence without widening +5 ms or promoting production/hosted guarantees. +- Carry a Viceroy 0.21.0 source patch with co-located readiness regressions and an explicit build/ + runtime-override recipe. Preserve typed errors and yield on empty/no-output frames. Retain + stock-versus-repaired guest evidence separately; version pins, installed runtime, production + transport and full G2/hosted qualification remain unchanged. +- Add an unaccepted TS qualification runbook source template with explicit freeze/build inputs, + command/count ownership and separate approval/authorization gates. Mark full-outbound execution + and frozen export as unimplemented; existing egress probes do not qualify the full package. +- Add `--assemble` and `--report` to the existing outbound evidence checker. Preserve per-leg + outcomes, counts and raw SHA-256 hashes; drafts remain explicitly pre-freeze and unqualified. +- Require explicit clean/frozen provenance and linked SHA-256 build identities for release + records. Aggregate independent blockers using the same rules as fail-closed release validation. +- Bound artifact hashing to regular files of at most 256 MiB with 64 KiB scratch space; refuse + unsafe paths and evidence overwrites. Exercise the actual Node CLI from the fixture's Rust + sentinel and existing workspace CI. No transport contract, runtime pin or capability changes. + +### Policy-Aware Axum Servers + +- **Breaking:** `AxumDevServer::{new, with_config}` consume an assembled `App`, not a router. + Preserve configured ingress/admission, renderers, extraction limits, clocks, state, platform + metadata and response-egress policies. `run_app` uses the same construction/serving path. +- Add `AxumDevServer::serve(listener, shutdown)` and `test-utils::AxumTestServer`, with a real + bounded HTTP/1 listener and local executor. Explicit shutdown cancels connections and awaits + worker cleanup and the serving result; drop only signals cancellation. Document skipped + startup orchestration and absent foreign provider contexts in host-parity tests. +- Prefer the router's supplied `HttpClient` without constructing an unused default transport; + otherwise construct the default once with the app clock. Remove the obsolete transport-only + request-conversion helper. Warn once when normalized head limits exceed the raw parser budget. +- Add TCP policy, upload, streaming, HEAD, raw-head, local-work, store/state and cleanup controls. + These do not promote capabilities or replace consumer/hosted qualification. + +### Bounded Synthesized Response Headers + +- Add `App::set_synthesized_response_headers` and immutable `SynthesizedResponseHeaders` for + detached ingress errors, admitted source errors, canonical 404/405 errors and adapter-built + fallbacks. Routed responses, routed handler errors, application admission refusals and + unconfigured assembly failures remain undecorated. +- Validate at most 32 field values and 4096 serialized field-section bytes before installation; + reject framing/coding fields and nonportable values with payload-free typed errors. Compact + retained maps and field backing storage. Invalid replacements leave the valid policy intact. +- Preserve response-owned names and ordered repeated configured values. Canonical `Allow`, + effective config retry policy and typed generated-overflow `Retry-After` suppression take + precedence. Fallback decoration performs no application callback or repeat validation. +- Let Fastly `prepare` install a validated per-request replacement through its extension bag, + including an explicitly empty set. Retain snapshots through initial preparation failures and + Axum's emergency fallback without changing completion ownership or fallback deadlines. +- Align demo/scaffold configuration and tests. Local contract checks are separate from consumer, + deployed runtime and frozen-candidate qualification; no capability is promoted. +- Add an independent four-runtime HTTP/1 wire suite for maximum-size repeated fields, normalized + Fastly rejections, GET/HEAD fallbacks, request replacements and undecorated application paths. + Preserve failed captures and verify actual EOF and final correlated terminal reports. +- Verify partial-configuration header exclusion on canonical assembly failures and generation-two + policy after retained recovery. Cover Axum's startup-failure precedence without an HTTP response; + correlate Fastly delivery reports per request and observe failed-state release before retry. + +### Buffered Response Framing + +- Derive exact buffered response lengths, including empty and framework fallback bodies, on + Axum, Cloudflare and Spin. Preserve explicit source-length validation and lazy stream framing; + suppressed HEAD/204/205/304 bodies gain no core-derived length. +- Stage Fastly buffers before a single SDK-equivalent Automatic handoff. Check deadlines around + each staging write; never pump, finish or abandon a body consumed by a successful bulk send. + Preserve strict body suppression and zero payload accounting. Do not restore HEAD bytes for + size inference: pinned local SDK controls expose HTTP/2 failures and forbidden 205 payloads; + exact HEAD/205 SDK parity remains unqualified pending hosted evidence. Host compression owns + final wire lengths; hint-bearing declared streams use + Automatic framing while retaining source-length enforcement. +- Hard-cut framework egress preparation and `ResponseEgressEnvelope::begin` to require an explicit + `ResponseEgressFramingMode`; migrate demos, scaffolds and fixtures without compatibility aliases. +- Add `buffered-response-content-length`: Native on Axum, BestEffort on serverless adapters pending + qualifying runtime/hosted evidence. Local probes do not complete hosted compression or consumer + parity gates, and do not alter the failed Fastly outbound body-read readiness gate. + +### Bounded Local Config Snapshots + +- Hard-cut `ConfigStore::{get, get_bounded}` and matching handle APIs from owned `String` + to shared `ConfigValue` UTF-8 payloads across all adapters and fixtures. Borrow via + `as_str()`/`as_ref()`; ownership copies are explicit. Debug output is redacted. +- Load Axum immutable local snapshots before listener binding, with validated finite + `EDGEZERO__ADAPTER__CONFIG_STORE__*` file/entry/key/value/resident/startup budgets. + Enforce actual reads and decoder/index/payload staging charges, reject duplicate keys, + fail startup on allocation-limit violations, and retain typed failed declared bindings. +- Make Axum `from_path`, `from_local_file` and `from_map` require `ConfigStoreLimits`; + `from_map` is now fallible. Publish `resident_allocation_bytes`; push updates require + server restart. Shared reads preserve independent byte caps and the application clock. +- Pin the decoder allocation audit to `serde_json 1.0.150`; stream canonical hashing + without full-output staging and release the extractor's raw handle before secret reads. +- Add native TCP proofs for suppressed-body completion ownership, stalled producer and + non-reading client deadlines, disconnect cleanup, exactly-once release and capacity + recovery. These are local evidence, not hosted provider qualification; broad config + allocation remains Unsupported and response egress remains BestEffort. + +### Bounded Provider Config Reads + +- Replace Cloudflare bounded KV text materialization with capped BYOB byte-stream reads, + reusable 16 KiB buffers and acquired-reader cancellation on overflow, timeout or drop. + Require valid UTF-8 without lossy expansion; raw `get` remains explicitly unbounded. + Cancel late-arriving streams after an abandoned acquisition, consume rejected cancellation + promises, and yield cooperatively between bounded groups of clock-checked reads. + Require runtime GC cleanup support before KV dispatch; demo/generated Workers enable + `enable_weak_ref` explicitly. GC retention is still excluded from allocation certification. +- Replace Fastly SDK auto-growing config reads with one caller-sized host-ABI buffer. + Reject over-cap chunk declarations before fetching or allocating reassembly; borrow + discriminator tokens rather than building a JSON tree. Bounded storage resolution checks + transport integrity; typed envelope verification remains behind the extractor's structural scan. +- Hard-cut `FastlyConfigStore::try_open` to return `ConfigStoreError` rather than the SDK's + error type. Reject duplicate pointer discriminators and preserve future-format overwrite guards. +- Publish narrow guest allocation charges and the remaining Spin/hosted evidence gaps. + Broad config allocation stays Unsupported and config deadlines stay BestEffort. +- Add opt-in, revision-pinned lifecycle qualification fixtures for all four standard entrypoints, + a bounded HTTPS probe runner and a fail-closed correlated ownership-evidence validator. + Compilation and local tests are not hosted qualification; no capability is promoted. + +### Declared KV Failure Bindings + +- Preserve all declared KV bindings on Fastly, Cloudflare and Spin without opening stores + before admission or routing. First use returns payload-free `KvError::OpenFailed` with + `KvStoreOpenFailure::{MissingBinding, InvalidConfiguration, Unavailable}` (500/500/503). +- Share settled opening results within one request, retry on subsequent requests, and + deduplicate boot warnings by compiled logical ID without caching provider availability. + Spin cancellation leaves unfinished initialization retryable; operations release the + initialization lock before provider I/O. Validate Cloudflare namespace method shapes. +- Retain explicit fail-before-dispatch required-store behavior and Axum startup semantics. + Add SDK/source-harness tests and real local runtime cases for unused declarations, + sanitized errors, warning deduplication, completion ownership and linked-store CRUD. + Extend local wire checks to failed writes, deletes and lists, verify deleted state, + correlate actual typed operation results, and recheck every completion after shutdown. + These do not qualify hosted outage recovery, KV allocation, or stronger capabilities. + +### Fastly Custom Delivery Reports + +- Hard-cut `send_request_with_registries_and_hooks`, `send_with_registries_and_hooks` + and `FastlyService::send_request_with_hooks` from `Result, _>` to + owned `FastlyRequestDelivery`. Distinguish no attempt, policy abort, detached + and routed delivery; preserve routed finalizer state after failure. +- Retain exact terminal completion reports, last-body commitment and separate delivery + errors. Permit native replacement only for no attempt or confirmed non-commitment, + never unknown commitment, policy abort or committed failure. Public pre-attempt + diagnostics are typed and payload-free; standard serving preserves original errors. +- Add consuming `ResponseEgressAttempt::into_terminal_report` with the existing live + Drop semantics and no second terminal clock sample or notification. Standard Fastly + run/serve/send policy is unchanged, including successful fallback handling. +- Migrate fixture callers and publish synthetic `delivery::testing` utilities. Demo + and generated applications retain the unchanged standard entrypoints. Local runtime + characterization does not qualify hosted delivery or promote capabilities. + +### Fallible Application Assembly + +- Hard-cut `Hooks::routes` and macro-generated `build_router` to + `Result`. Route/state assembly failures return unchanged, + skip configuration and never produce a partially configured application. +- Add `App::from_router::` for externally built routes, applying the same name, + configuration and complete platform-metadata check once. Raw constructors remain + low-level tools, not compatibility assembly wrappers. +- Add borrowed `App::state::` and `RouterService::state::` access to the existing + registry. Configuration, entrypoints and request extractors share registered interiors. +- Migrate adapter contracts, demo, generator and reusable fixtures. Verify real + successful-only `Sandbox` retention after route and configuration failures. +- Adopt one assembly-failure delivery contract across all serverless entrypoints and + fallible macro state expressions (`state = load_state()?`). Add app-free + `App::assembly_error_egress` and adapter `request::send_assembly_error` helpers; + use the existing default renderer and owned converters, preserving typed status, + internal-error redaction, HEAD suppression and one full boot diagnostic per attempt. +- Hard-remove host-generic assembly-error mappings and their test-only wrappers. + Failed apps supply no configured callbacks or headers. The default finite write + deadline uses the captured pre-assembly start and supplied clock; it does not + interrupt synchronous construction. Axum logs the original failure on the boot target + and still fails before listener binding, without constructing an HTTP response. +- Standard retained Fastly serving answers assembly failures and retries on the next + request after successful delivery; custom callbacks receive the detached terminal + report without building a temporary app. Preserve commitment-safe no-resend rules. + Demo and generated entrypoints inherit this behavior without extra application plumbing. + Local runtime characterization remains separate from consumer and hosted qualification. + +### Outbound HTTP And Batches + +- Add standalone validated Image Optimizer values with typed format/filter/focus selectors, + a public seven-variant region enum with exact pinned-SDK token conversions, private settings + and complete builder readback. Remove the string-backed region wrapper. Validate + quality, nonzero dimensions and paired percentage focus without provider dependencies. + Request attachment, support-first preflight and Fastly execution remain unimplemented/gated; + these values do not enable optimization or add ignored request options. +- Add the portable head/finalizer foundation: original Location evidence and field fidelity, + pre-normalization header caps, policy-controlled markers and exactly-once exchange ownership. + Discard never reads payloads; finalization preserves coding, independent caps, bodyless/205 + semantics, paired-clock deadlines and typed failures. Yield between encoded reads before + decoding so ready empty/no-output input cannot starve the deadline race. Contexts remain opaque; + the live facade/adapter trait migration, progress scopes and redirect API are still pending. + No compatibility path, new transport, provider pin or capability promotion is introduced. +- Add a separate bounded local overflow characterization suite through all four standard + entrypoints. Preserve exact cap boundaries, typed 413/no-retry rendering and ordinary upstream + 413 metadata. Axum, Viceroy and Spin pass; retain Cloudflare's post-overflow proxy failure, + reproduced independently by a JavaScript-only control. No hosted capability is promoted. +- Add the exact 2 KiB chunked-ingress proof on all four local runtimes, preserve the original + overflow/recovery sequence, and exercise built-in JSON-extractor overflow with custom rendering + over Axum TCP. Prove collector drop waits for worker cleanup. Narrow Cloudflare's retained + recovery failure to the local front-proxy path without substituting a diagnostic entrypoint. +- Exercise freshly generated Axum applications and the bundled demo over real TCP, including + fixed/chunked fallback cap boundaries, exact 413/no-retry responses, canonical 405 Allow, + stalled-read 408 responses and deferred unread-body/pipeline handling. Reuse the existing + generated-project CI gate and demo contract tests; require the fallback sentinel and execute + ignored demo tests explicitly. No application policies are replaced. + +- Return typed `EdgeError::PayloadTooLarge` (413) for inbound reader/extractor and outbound + upload overflow, with non-wire `RequestBodyOverflowOrigin`. Reject oversized buffered uploads + before every adapter dispatch; retain existing prebuffered or aborting stream behavior. + Reject oversized normalized declared lengths without polling or poisoning reusable bodies; + expired inbound deadlines still win with 408 and consumed overflow stays sticky. + Suppress `Retry-After` on generated overflow after rendering and final egress preparation, + while preserving upstream 413 and config-503 retry policies. Align demo/scaffold fallback status. +- Verify actual downstream encoded relay through all four local entrypoints: 24 cases per + adapter across gzip/deflate/Brotli, buffered/streamed modes, Original/206 preservation and + Decode controls. Capture binary wire payloads without decoding, retain representation metadata + and origin receipts, and reject conflicting framing. Keep hosted qualification separate. +- Retain local Cloudflare workerd 2026-09-10 characterization: 24 coding, 46 marker, five + scheduling and five HTTP/1 framing checks. Require the original Workers body signal in framing + assertions and continue independent scheduling checks after policy-probe failures. Record exact + tool, source, JS/WASM and compatibility-setting identities without claiming hosted qualification. +- Add the portable `OutboundExchangeOwner` / `OutboundPending` ownership foundation. Teardown + is local and exactly once; readiness caches success or the original error without polling body + bytes, and cancellation releases uncollected work. Successful collection disarms pending + cancellation before handing off a live streamed response. Producers must supply already-scheduled + completion receivers and separate body cleanup, not lazy transport futures or duplicate SDK + release guards. This does not yet add `HttpClient::begin` or migrate the dispatch trait. +- Add client-wide proxy-marker defaults and per-request `OutboundProxyMarker::{Inherit, On, Off}`, + with effective-policy inspection and resolution before supplied-client delegation. Default stays + on; off skips synthetic header insertion/accounting while preserving ordinary upstream fields. + Relay demo/templates configure their client handle off. Carry the policy through single sends, + batches, bodyless responses and parts round-trips on all four adapters. + Adapter implementers must pass the request policy to `insert_proxy_header`; its prior + unconditional signature is removed rather than retained as a compatibility overload. +- Correct Spin `outbound-header-fidelity` to BestEffort: a local direct-WASI control proves the + host hides `Connection` before normalization while retaining a nominated extension field. + Preserve visible-header normalization and upstream marker opt-out; do not claim raw fidelity. +- Add `ResponseDecoding::{Decode, Original}` with per-request readback and parts round-tripping + across all four adapters. Original delivers recognized encoded bytes without decoder allocation; + every 206 response now retains original coding, even with Decode selected. Keep normalization + precedence, identity decoded caps, encoded/final limits, bodyless handling and provenance. + Relay demo/templates select Original; JSON fanout keeps Decode. Local Axum/Viceroy/Spin/workerd + probes do not constitute hosted qualification; hosted relay evidence remains pending. +- Add the portable `OutboundFuture` / `OutboundTime` foundation for pairing the application + clock with an adapter-owned wakeup factory. Preserve absolute deadlines across early wakes, + yield cooperatively, propagate timer errors unchanged, reject observed clock regression and + release timer/factory state on termination or cancellation. All four adapters now use the pair + for live upload, fetch and response deadline handling. Hard-migrate clock-only constructors to + Axum `try_with_time` / internal `with_transport_and_time` and serverless `with_time`, with no + compatibility aliases. Standard request injectors pair the application clock with the existing + runtime timer. Timer setup/failure precedes source polling; Fastly timer-only waits share its + progress multiplexer, Workers validate scheduling/cancellation together, and Axum has no + independent reqwest total-timeout clock. Dispatch-trait migration and hosted qualification + remain separate; provider capability levels are unchanged. +- Replace the proxy-client/service API with `HttpClient`, `OutboundHttpClient`, `OutboundRequest`, + and `OutboundResponse`. Standard adapters inject the client into request extensions; handlers + access it through `RequestContext::http_client()`. Missing manual wiring remains explicit. +- Add buffered and streamed uploads/responses, absolute deadlines, request-body bounds, explicit + cache policy, and HTTP authority override independent of URI routing and TLS identity where + supported. Automatic redirects and implicit provider response decoding are disabled. +- Request construction is fallible and validates absolute HTTP(S) targets. Dispatch accepts GET, + HEAD, POST, PUT, PATCH, DELETE, and OPTIONS; GET/HEAD reject nonempty bodies. Ordinary `Host` + headers are normalized away; use the explicit authority override instead. +- Default to a 1 MiB buffered response, an 8 MiB upload cap, a 32 MiB decoder budget, a 24-bit Brotli + window limit, and a 30-second exchange budget when no timeout/deadline is supplied. Response-mode + setters are last-write-wins; `stream_response()` opts into streamed delivery. Encoded, decoded, + header, and chunk limits are separate opt-in controls. +- Accept streamed upload sources under the request cap and deadline, but collect them before + provider dispatch on Axum and Cloudflare. Cloudflare no longer forwards upload chunks directly + to Fetch as the old proxy implementation did; this API does not promise wire-streamed uploads. +- Expose `OutboundCachePolicy::{PlatformDefault, Bypass}`. Bypass disables provider caching on + Cloudflare/Fastly and is a no-op on Axum/Spin, which do not install an outbound response cache. +- Add completion-driven fanout through `start_batch_until`, indexed `next`, explicit `cancel`, + ordered `collect`, and `send_all_until`. Batch slots require buffered requests and responses; + streaming exchanges use `send`. +- Return per-slot elapsed time and outcomes, preserving input indices while yielding observed + completion order. Timing uses one batch-entry snapshot and includes preflight, provider setup, + complete body collection, and guest-observation delays rather than claiming pure transport RTT. +- Define `observation_cutoff` as the point where batch observation stops, even if individual slots + retain request budget. Earlier cutoffs are valid partial-result operations, not request timeouts. +- Distinguish `Completed`, `Cutoff`, and typed batch failure. Only cutoff permits unresolved slots + in successful collection; `OutboundBatchFailure` retains both the precise error and previously + collected, index-aligned slots. Cancellation releases the driver and returns unresolved indices. +- Require clean driver EOF before `Completed`, including zero-slot drivers. Trailing failures, + duplicate/out-of-range indices, premature EOF, and cutoff after all slots resolve are rejected; + poisoned batches cannot resume driver polling. External adapters have a documented, public + `OutboundBatchDriverEvent` / `OutboundBatch::from_driver` construction protocol. +- Axum, Cloudflare, and Spin drive complete eligible exchanges concurrently. Fastly dispatches + eligible inputs before harvesting ready pending handles, preserves handle-to-slot identity, and + uses deterministic dynamic-backend identities that include exact timer budgets. An early + `BatchCutoff`-attributed provider phase timeout terminates only its slot unless the actual + observation cutoff has expired. +- Share the future-based batch driver through `OutboundBatch::from_futures` on Axum, Cloudflare, + and Spin. Preserve recorded terminal timestamps, ready-result retention, index validation, + and clean-EOF completion; Fastly retains its provider-handle driver. +- Add `ContentTypeProvenance::{Ineligible(ContentTypeIneligibility), SingleTextUnchanged, Unknown}` + from original response-header multiplicity and UTF-8 evidence before filtering. Reasons are + `Missing`, `Modified`, `MultipleValues`, `NonText`, and `Removed`. Axum/Fastly/Spin retain + evidence on every response path; Cloudflare always reports `Unknown` because Fetch strings + lose original bytes and field boundaries. No MIME validation, trimming, comma splitting, + raw-header/body copies, or new capability is added. A known textual singleton containing a + comma can remain eligible; application MIME validation, acceptance, and redaction still apply. +- Expose `content_type_provenance()`. `headers_mut()` conservatively invalidates an originally + eligible result as `Ineligible(Modified)` without rewriting private original evidence; + original ineligible/unknown evidence never upgrades. `into_response()` carries the effective + enum as a response extension describing the historical outbound boundary only, not certifying + later ordinary `http::Response` edits. +- Hard-cut response construction to + `OutboundResponse::new(method, status, headers, body, clock, content_type_provenance)`, + decomposition to `(method, status, headers, body, provenance)`, and normalization to + `Result` containing body disposition and provenance. + Metadata is mandatory, never inferred from normalized headers; no old signatures remain. + +| Retired outbound API | Required replacement | +| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| four- or five-argument `OutboundResponse::new` | six-argument `new(method, status, headers, body, clock, content_type_provenance)` requiring the application clock and original evidence | +| four-element `OutboundResponse::into_parts()` | five-tuple `(method, status, headers, body, provenance)` with effective `ContentTypeProvenance` | +| `normalize_response_headers(..) -> Result` | `Result`; use `.body_disposition` and retain original `.content_type_provenance` | + +### Encoding, Limits, And Error Classification + +- Enforce independent encoded-transport, decoded-output, and final buffered-response caps. + Decoded caps apply to identity and EdgeZero-decoded bodies, not raw passthrough encodings. + Checks occur incrementally before appending excess output; response collection retains the + configured bounds and original absolute deadline. +- Share the response-body policy through `ResponseBodyPolicy` and `apply_response_body_policy` + across all four adapters. Declared lengths, encoded limits, decoding/header rewriting, decoded + limits, and rechunking retain their order; transport lifetimes remain adapter-owned. +- Add `into_bytes_bounded`, `into_bytes_bounded_until`, `json_bounded`, and `json_bounded_until` + for explicit response consumption. The `_until` helpers check the retained application clock + cooperatively; pending-stream cancellation still depends on the adapter's deadline wrapper. +- Add response-header byte/count caps with typed overflow origins. Accounting includes every + adapter-visible upstream field before normalization and the synthetic `x-edgezero-proxy` field. + Preserve repeated headers, including `Set-Cookie`, and strip hop-by-hop fields consistently. +- Decode single bare `gzip`, zlib-wrapped `deflate`, and `br` codings. Validate multi-member gzip + through transport EOF, reject trailing data after deflate/Brotli completion, and remove compressed + representation headers only when EdgeZero decodes. Request bodies retain their declared encoding + without implicit recompression. +- Replace bare `ContentEncoding::Passthrough` with `Passthrough(PassthroughReason)`, distinguishing + `Stacked`, `Unsupported(token)`, and `Malformed` while retaining encoded bodies and headers. +- Reject excessive Brotli window bits before decoder allocation. Publish pinned Brotli and flate + decoder memory-charge contracts and enforce the codec-independent `max_decoder_bytes` budget. + Add opt-in `max_chunk_bytes` rechunking as an emitted-item bound, not a receive-allocation bound. +- Add `Deadline`, `DispatchBudget`, the EdgeZero-owned `MonotonicInstant` alias, and injectable + `MonotonicClock`. Ingress, outbound clients, deferred response drains, config extraction, and + egress use one application clock domain. Equality expiry wins; far-future deadlines are clamped + and remaining dispatch budgets cannot grow beyond their entry duration. Backwards batch terminal + observations and Fastly dispatch samples fail closed. +- Add typed timeout provenance (`BudgetSource`, including `BatchCutoff`), gateway transport/decode + reasons, response-limit reasons, and store-extraction reasons. Distinguish initial unreachable + failures, midstream transport failures, protocol failures, and content-coding layers without + requiring message parsing where the provider exposes those distinctions. Opaque Cloudflare Fetch + failures retain `BadGatewayReason::Unspecified`. Preserve timeout causes through decoder error + conversion; upstream/response-limit failures use 502 and exhausted exchange budgets use 504. +- Use category-only public diagnostics for provider-backed failures so URLs, credentials, and + provider error details cannot escape in framework HTTP error bodies. +- Add fixed-size, text-free `ProviderFailureDetail` catalogs for the pinned adapters and + `EdgeError::provider_failure_detail()`. Explicit gateway/request/timeout/internal constructors attach + detail without changing Display, messages, statuses, typed reasons or wire bytes. Retain it + through response-body policies, buffered collection, pending handles, batch failures and sticky + request-body replay. Existing internal sources and `From` remain intact. + Populate existing send/body SDK boundaries on all four adapters, Fastly backend/upload I/O, + and Spin request completion. Preserve observed detail when existing deadline checks select a + timeout, including bounded response collection. Record Cloudflare constructor/header/setup + failures and Spin's five header errors, typed timeout-option rejections and unit-returning + target rejections as `RequestPreparation`. Keep SDK-erased causes Unknown, advisory timeout + options successful, and shared response conversion unmodified. Retain the first observed + body-source failure in `ResponseEgressReport::source_failure` on all four adapters. Completion + and observer receive the same bounded category/provider snapshot and original body tag, even + if a deadline, cancellation or fallback wins. Recording changes no polling, clocks, terminal + precedence or wire bytes. Rendered application errors and transport-only failures are not + source failures. Exact report literals must add the new optional field; no compatibility + shape remains. Hosted qualification remains follow-up work. +- Publish `outbound-failure-diagnostics`: Native for Fastly/Spin's existing typed SDK-cause + projection and BestEffort for Axum/Cloudflare's reduced cause visibility. Manifest parsing, + required/optional gates, adapter support and published matrices use the same capability. + This adds no transport guarantee, reconstructs no host-hidden cause and does not complete + hosted qualification or the remaining outbound migration. +- Retain precommit response framing rules in `ResponseEgressReport::framing_failures`, with + independent first-observed application/fallback slots using the existing `ResponseEgressFramingError`. + Use shared attempt-owned normalization on all four adapters, including fallback and Axum + revalidation, and log only fixed rule text at error level. Add `EdgeError::response_framing_error()` + for an already-owned internal cause. Preserve Display, statuses, fallback bytes, terminal + precedence, source categories, clocks and ownership. Exact report literals must add the new + carrier; no compatibility report shape is retained. Consumer and hosted qualification remain open. +- Correct Fastly lifecycle test-helper gating so plain `fastly` WASM unit builds retain the + required-store failure regression without enabling `test-utils` or changing production behavior. +- Hard-cut exact field construction/matching of `EdgeError::{BadGateway, BadRequest, GatewayTimeout, Internal}` + to include `provider_detail: Option`; prefer semantic constructors and + `..` in patterns. No wrapper variant or compatibility error type is introduced. + +### Ingress Admission And Routing + +- Resolve routes before admission and body polling on all four standard adapters. Publish stable + `RouteId`, `RouteMetadata`, and `RouteResolution::{Matched, MethodNotAllowed, NotFound}`, including + optional manifest-sourced route `class` metadata. +- Stamp request start with the application clock, expose absolute read deadlines, and retain an + opaque `IngressGrant` through admitted processing. Request contexts can consume the grant once. +- Replace eager unbounded inbound reads with bounded body consumption and deadline-aware wrappers. + Cloudflare and Spin race pending asynchronous reads with timers; Axum owns its connection-local + read future. Fastly retains its synchronous-hostcall limitation. +- Add cached, fallible `RequestContext::body_bytes(max)` and bounded JSON/form consumption. + Failed or cancelled drains poison the body cell so later readers cannot retry a partially consumed + source. Default extractors are bounded; `ValidatedJsonWithin` and + `ValidatedFormWithin` let handlers select their own compile-time caps. +- Add `ReadBodyBeforeFallback` for bounded discard before canonical 404/405. It carries the + application grant, byte cap, read deadline, and buffered `on_exceeded` / `on_timeout` responses. + Overflow or timeout takes precedence without invoking a route handler or middleware; refusal + remains the zero-read overload path. +- Add non-response `AdmissionDecision::Abort`. It starts no response lifecycle, invokes no handler, + middleware, or completion callback, and polls no body. Axum closes the owned HTTP/1 connection; + other adapters delegate the final transport effect to their providers. +- Add defensive normalized request-target/header/framing checks and typed accounting summaries. + These do not claim access to raw parser-boundary memory or ambiguous raw framing. +- Emit a sorted, deduplicated `Allow` header for canonical 405 responses, including successful + fallback drains. Add an application-controlled framework-error renderer while preserving status, + `Allow`, and the effective config `Retry-After` policy after custom rendering. Applications remain + responsible for bounding their renderer's output. + +### Bounded Axum HTTP/1 Ingress + +- Enforce finite pre-admission limits through the existing Axum server entrypoints: 256 live + connections, 64 KiB combined raw request heads, 100 raw fields, and a non-resetting 10-second + idle-plus-header deadline by default. Explicit invalid settings fail startup before application + configuration or listener binding. `AxumIngressConfig` publishes validated effective limits and + parser receive-allocation assumptions. +- Replace per-connection spawned tasks with one bounded connection-future owner. At capacity, + close the single transient accepted socket without a parser, HTTP refusal response, or waiter + queue. Keep-alive retains its slot; disconnect, failure, timeout, cancellation, and shutdown + release connection state. Preserve existing application body-read and response-write deadlines. +- Pin Hyper 1.12.0, bytes 1.12.1, and http 1.4.2 for the audited HTTP/1 resource boundary. Raw head and duplicate + field-count limits precede admission, but independent raw-target/header accounting and strict + ambiguous-framing rejection remain unsupported. HTTP/2 and socket upgrade ownership are not + enabled. Kernel buffers, allocator overhead, and application allocations are not RSS-certified. +- Bound and compact response headers before Hyper reuses their map for request parsing, preserving + duplicates and sensitive flags. Response fields share the configured head-byte budget and a + derived `max(100, request_field_limit)` count cap; overflow closes the connection. Custom Hyper + reason phrases are removed. Oversized spare capacity/backing buffers cannot persist in the + parser cache. +- **Breaking:** `AxumDevServerConfig` now requires its validated `ingress` field in complete struct + literals. Use `AxumIngressConfig::default()`, `from_env`, or `new`; generated entrypoints still + delegate to `run_app`. Axum CLI Cargo children receive selected manifest environment defaults, + respecting parent environment precedence and declaration order. The demo manifest and scaffold + document the ingress settings without duplicating runtime enforcement. +- Recognize case-insensitive `EDGEZERO__` prefixes on Windows, where child environment-key aliases + retain their first spelling. Unix key identity stays unchanged, and explicit invalid limits + cannot be bypassed through a Windows case alias. + +### Response Egress And Application Assembly + +- Carry every response-producing admission outcome through `ResponseEgressEnvelope` and one + `ResponseEgressAttempt`, retaining the application clock, policy, and response-scoped completion + resource until the exactly-once terminal transition. +- Add non-clone `ResponseEgressCompletion`, generic `late_bound` resource installation, and + left-to-right `join` composition. Completion runs before the global observer; abandonment and + competing terminal paths release resources without duplicate reports. +- Refusal/fallback responses retain their admission-decision-owned completion resources. Detached + normalized errors use `DetachedResponseEgressDecision::{Send { completion, deadline }, Abort}`; + the application factory can reserve resources or abort without creating a response. Raw parser + failures remain outside the application lifecycle. +- Support per-response absolute and egress-start-relative deadlines through + `ResponseEgressDeadline`. An absolute `at(deadline)` can include preceding application queue time; + `after(Duration)` resolves in `ResponseEgressEnvelope::begin` and excludes time before egress + starts. Preserve the selected deadline through policy, framing, conversion, and delivery. +- Replace response-object handoff and whole-stream buffering with adapter-owned response pumps: + Axum's Hyper connection, Cloudflare's writer/abort lifetime, Fastly's manual `stream_to_client`, + and Spin's owned WASI response/body/result writers. Account accepted payload prefixes and release + sources on timeout, disconnect, source failure, and terminal transport errors. +- Fix Spin streamed egress when its host response-result future succeeds before body production + finishes. Retire that observation once, continue the same pending source/write under its deadline, + and finish only after stream closure and successful body-result publication. Keep late success, + disconnects and source/write failures distinct from end-client delivery guarantees. +- Share response framing rules for HEAD, body-forbidden statuses, declared-length validation, + hop-by-hop stripping, unsupported upgrades, and bounded pre-commit error fallbacks. Fastly manual + framing is enabled only when a body is actually transmitted. +- Validate Axum exact-length stream EOF before handing Hyper the final frame; preceding frames + remain incremental. Validate transmitted zero-length streams before handoff under the original + deadline. Excess/late-error tails fail, pending/empty tails stay deadline-bounded, detached finite + fallbacks retain GET payloads, and HEAD remains bodyless. Registration expiry selects 504 rather + than a conversion 500. +- Make assembly core-owned and fallible through `App::build::(platform)` and + `Hooks::configure(&mut App) -> Result<(), EdgeError>`. Install target metadata before configuration, + propagate hook errors unchanged, and reject successful configuration that changes any platform + fact, provenance, or unknown reason. Add `app!(..., configure = ...)` with a fallible callback + whose errors propagate through `App::build`. Axum completes assembly before listener binding. +- Add Fastly's closed `run_app_with_hooks` lifecycle for raw-request preparation and routed-response + finalization while retaining adapter-owned delivery and returning routed hook state only after + terminal delivery. +- Integrate opt-in reusable application lifecycles with fallible assembly and canonical admission/ + egress. Cloudflare and Spin expose prebuilt `dispatch_app`; Fastly exposes `serve_app`, + `serve_app_with_hooks`, and successful-only `lifecycle::Sandbox` retention. Fastly custom serving + callbacks own delivery and return `Result<(), E>` without an SDK-generated second response. + Reuse no request handles, bodies, extensions, registries, or pending transport work. + +### Bounded Config And Secret Extraction + +- Add `ConfigExtractionLimits` and typed bounded config/secret reads with per-value, backend-byte, + and cumulative extraction caps under one extraction-start-relative timeout and application clock. + Fastly chunk reconstruction and secret hydration share the same cumulative accounting/deadline. +- Bound JSON depth and node count before materializing a config envelope as `serde_json::Value`, + so nested or oversized structural shapes are rejected before allocating the retained tree. +- Distinguish missing binding/value/secret, malformed envelope, unsupported envelope version, + integrity failure, deserialization, validation, overflow, deadline, and provider-read failures + through `StoreExtractionReason` and typed store errors rather than the previous coarse extraction + classifications. +- Require provider implementations of `ConfigStore::get_bounded` and + `SecretStore::get_bytes_bounded`; there is no default unbounded-read-then-check implementation. + Return `BoundedStoreRead` with explicit backend bytes and an optional value. +- Preserve declared Cloudflare/Fastly config bindings when opening fails by installing typed failing + handles. Optional missing-config policy cannot reinterpret a broken declared binding as absence. + Defer Spin config-store opening until bounded extraction, after admission, so unrelated routes + can still dispatch and opening participates in extraction timing. +- Race asynchronous Cloudflare/Spin reads with platform timers and recheck injected-clock expiry + around ready results. Synchronous Axum/Fastly provider calls remain non-preemptible; byte and + timeout controls do not certify provider allocation or finite cancellation latency. + +### Platform resource metadata + +- Add `Adapter::platform_metadata()` and application-visible `PlatformMetadata` with typed + `PlatformFact::{Known, Unknown}` values, provenance, and unknown reasons for memory ceilings, + concurrent live inbound population, and host ingress/framing memory accounting. +- Distinguish per-instance from per-execution ceilings and separate stack allowances. Publish + canonical Axum, Cloudflare, Fastly, generic Spin, and opt-in Akamai Functions profiles; generic + Spin remains runtime-configured rather than inheriting a hosted quota. +- Add checked, scope-aware `validate_memory_envelope` with `Fits`, `Exceeds`, and typed + `Indeterminate` results. Reject scope mismatch and arithmetic overflow; unknown platform facts + remain indeterminate rather than being replaced with guessed population or parser-memory bounds. + +Applications and adapter implementations must migrate to the current platform-fact and response +construction APIs. The migration record includes APIs from earlier PR revisions as well as the +pre-PR release surface, so consumers pinned to intermediate commits can update directly. No +compatibility aliases are retained. + +The focused platform-design table below preserves that earlier migration record. Its outbound +constructor row is historical and superseded by the six-argument outbound migration above. + +| Removed or changed API | Replacement | +| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `MemoryCeilingSource` | `PlatformResourceSource` inside `PlatformFact::Known` | +| `MemoryCeiling::new(total, scope, stack, source)` | `MemoryCeiling::new(total, scope, stack)` wrapped in `PlatformFact::known(value, source)` | +| `MemoryCeiling::source()` | Match or inspect the enclosing `PlatformFact` | +| `PlatformMetadata::new(Option)` | `PlatformMetadata::new(memory, inbound_population, host_ingress_accounting)` | +| `PlatformMetadata::memory_ceiling() -> Option<_>` | `PlatformMetadata::memory_ceiling() -> PlatformFact<_>` | +| `Adapter::memory_ceiling()` | `Adapter::platform_metadata()` | +| overridable `Hooks::build_app()` and infallible `configure` | `App::build::(platform)` plus fallible `Hooks::configure` | +| four-argument `OutboundResponse::new` | five-argument constructor requiring the application `MonotonicClock` | + +### Logging Ownership And Boot Diagnostics + +`owns_logging = true` now explicitly owns backend installation, backend filters, and the facade +maximum before entering an adapter runner. `resolve_logging_level(&EnvConfig)` shares runtime +level resolution. Managed Axum/Fastly loggers retain `edgezero::boot` warnings and errors at +`Warn` even when ordinary logging is `Error` or `Off`. Cloudflare/Spin initializers remain no-ops. + +Fastly `runtime_env_config` now returns `FastlyRuntimeConfig` with an `env` field and bounded +deferred diagnostics. Custom callers must initialize logging and call `emit_boot_diagnostics()`; +the former direct `EnvConfig` return is removed, with no compatibility alias. Managed runners +initialize logging before application configuration. Axum now returns logger-setup errors instead +of ignoring them; preinstalled backends must use `owns_logging = true`. + +Axum's `run_app_with_preflight` supports checks after logger setup and before configuration or +listener binding. The bundled demo uses it without first installing the CLI logger. Terminal CLI +errors remain visible on stderr regardless of runtime logging level. + +### CLI, Capabilities, Demo, And Generator + +- Add typed manifest capabilities and `Adapter::capability` with `Native`, `BoundedCooperative`, + `BestEffort`, and `Unsupported`. Runtime-producing CLI operations validate required support + before side effects; optional support emits boot diagnostics without blocking execution. + Unknown names, duplicates, overlap, and malformed baked contracts fail closed. +- Add canonical outbound-host declarations, with an omitted declaration defaulting to HTTPS only + and explicit `"*"` enabling HTTP and HTTPS. These configure platform plumbing, not destination + authorization. Spin's selected component must match the canonical host contract. +- Pin CLI execution to the resolved application root, platform manifest, and component through + `AdapterExecutionTarget` / `execute_target`, including manifest-defined shell commands. Reject + ambiguous or escaping manifest selection instead of checking one project and executing another. + Fastly deployment honors the selected manifest path. +- Migrate checked-in demos and generated applications to the new client, buffered fanout, + passthrough classification, response limits, clocks, ingress policy, and fallible assembly. + Include explicit missing-client 501 behavior and partial-result/failure examples. +- Keep runtime setup in reusable adapter runners rather than copying transport or logging code + into templates. The demo illustrates completion composition and late-bound ownership; generated + scaffolds retain finite admission/egress policies with core-owned empty lifecycle tokens. +- Gate `handlebars` behind the existing `edgezero-cli/cli` feature so runtime-only library consumers + do not pull scaffolding or implicitly enable its `serde_json/preserve_order` dependency feature. + +### Additional Hard Migrations + +| Removed or changed API | Replacement | +| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `ProxyClient`, `ProxyHandle`, `ProxyRequest`, `ProxyResponse`, `ProxyService`, adapter `*ProxyClient` | `OutboundHttpClient`, `HttpClient`, `OutboundRequest`, `OutboundResponse`, client forwarding, adapter `*OutboundClient` | +| Infallible proxy request constructors and tuple `into_parts()` | Fallible `OutboundRequest` constructors / `from_parts` and structured `OutboundRequestParts` | +| Proxy request extension access and `body_mut()` | Application-owned state and outbound request builders / structured parts | +| `Body::from_stream` with arbitrary external errors | Typed `EdgeError` items, or `Body::from_external_stream` for external errors | +| Synchronous/unbounded request-context body access | Async `body_bytes(max)`, `json_within(max)`, `form_within(max)`; fallible `take_body()` / `into_request()` | +| `EdgeError::MethodNotAllowed.allowed: String` | Structured `Vec` used to render the mandatory `Allow` header | +| `config_out_of_date_from_serde` | `store_deserialization_from_serde` and `EdgeError::StoreExtraction { reason, .. }` | +| Store implementations with only unbounded reads | Required `ConfigStore::get_bounded` / `SecretStore::get_bytes_bounded` implementations | +| Decoder helpers without a memory policy | Codec helpers accepting decoder budgets and Brotli window policy | +| Response-returning Fastly entrypoints and `#[fastly::main]` | An undecorated `main` calling a send-owning adapter runner | +| Transport-blind response converters and stream-buffer fallback constants | Adapter-owned response-egress coordinators | +| Fastly `runtime_env_config(...) -> EnvConfig` | `FastlyRuntimeConfig`, using `.env` and `emit_boot_diagnostics()` after logger setup | + +Earlier PR revisions additionally require these migrations: + +| Superseded intermediate API | Current API | +| ---------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| `send_all` returning bare positional outcomes | `start_batch_until` / `next` / `cancel` / `collect`, or fallible `send_all_until` | +| Batch `cutoff` parameter described as a batch deadline | Explicit `observation_cutoff` that stops observation regardless of slot budgets | +| Bare `ContentEncoding::Passthrough` | `ContentEncoding::Passthrough(PassthroughReason)` | +| Codec-specific `max_brotli_decoder_bytes` | Codec-independent `max_decoder_bytes` | +| Admission refusal/fallback without response completion ownership | Decision-owned `ResponseEgressCompletion` transferred through ingress and egress | +| Detached completion-only factory | `DetachedResponseEgressDecision::Send { completion, deadline }` or `Abort` | + +### Dependencies And Toolchain + +- Hard-cut platform SDKs to Fastly `0.13.1` (SDK/shared/sys/logger), Cloudflare `worker 0.8.5`, and + `spin-sdk 7.0.0`, adapting Spin to its WASI HTTP 0.3 response/body/result resource protocol. +- Pin native outbound dependencies (`reqwest 0.13.4`, `url 2.5.8`) and audited decoder graphs, + including `async-compression 0.4.43`, `compression-codecs 0.4.38`, `brotli-decompressor 5.0.1`, + and `flate2 1.1.9`. Decoder charge changes require explicit pin/audit updates, not lockfile-only + assumptions. Spin's SQLite config writer is reverified against Spin 4.1.0. +- Update runtime/tool pins to Spin CLI `4.1.0`, Viceroy `0.21.0`, and Wasmtime `48.0.2`; generated + projects select corresponding SDK generations and applicable platform CLI/Viceroy pins. Root + workspace dependencies use exact SDK pins. Rust `1.95.0`, Node `24.12.0`, and Fastly CLI `15.1.0` + remain the workspace toolchain. + +### Tests, Documentation, And Maintenance + +- Expand native and provider contract coverage for deadlines, decoding, limits, batch ordering, + driver failure/cancellation, admission/fallback precedence, clocks, framing, and resource release. + Add Hyper raw-socket, Spin SDK-resource, Fastly closed-lifecycle, and demo logging regressions. +- Gate native/WASM feature combinations independently, include `test-utils` runtime paths, and + require named nonzero test sentinels. Consolidate adapter clippy into one workflow matrix while + retaining distinct per-target check names, including native Axum. +- Verify generated all-adapter projects and the excluded demo workspace. Fetch both locked + dependency graphs before offline audits; enforce decoder dependency, JSON-map, documentation, + public-rustdoc, and retired-API contracts in CI. +- Update outbound, ingress, egress, and configuration specs/plans, capability matrices, migration + guides, and examples. Replace ordinary hand-built JSON values/fixtures with `serde_json::json!`, + retaining raw input only for syntax, exact-byte, static-input, and serialization-free fallback + requirements. + +### Capability And Evidence Boundaries + +- Axum alone reports native outbound and inbound-read deadline enforcement. Cloudflare outbound + and streamed-upload deadlines remain `BestEffort` pending deployed host-observed cancellation + evidence; Fastly and Spin retain their documented phase-timer/teardown limitations. +- Fastly fanout is dispatch-before-harvest, not guaranteed simultaneous overlap. Synchronous + registration/body drain and bounded handle-selection groups limit completion order, cancellation, + and slot isolation; dynamic backends require provider enablement. A 25 ms dispatch-slack guard can + reject a still-live slot before synchronous cold registration, including heterogeneous hosts and + budgets. Cloudflare header fidelity and authority override remain `BestEffort`; Spin authority + override is rejected because routing and authority cannot be separated through its provider API. +- Response-egress abort, backpressure, completion, and write deadlines remain `BestEffort` on all + adapters. An exactly-once guest lifecycle report does not prove end-client receipt or a finite + provider teardown interval. Configuration-read deadlines also remain `BestEffort` everywhere. +- Raw ingress head limits/framing guarantees, complete outbound resource accounting, and complete + config-read allocation bounds remain `Unsupported`. Provider parsing, materialization, SDK copies, + hidden informational fields/trailers, allocator overhead, and host buffering are not certified by + guest-visible byte, header, chunk, or decoder-policy caps. + +See [the capability guide](docs/guide/capabilities.md) for the exact per-target matrix, accounting +boundaries, canonical platform facts, and evidence required before promoting support. diff --git a/CLAUDE.md b/CLAUDE.md index cd07b08c3..1c7d20807 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,15 +4,15 @@ EdgeZero is a portable HTTP workload toolkit in Rust. Write once, deploy to Fastly Compute, Cloudflare Workers, Fermyon Spin, or native Axum servers. The -codebase is a Cargo workspace with 8 crates under `crates/`, an example app -under `examples/app-demo/`, a VitePress documentation site under `docs/`, and -CI workflows under `.github/workflows/`. +codebase has an eight-crate production Cargo workspace and an isolated test +runner under `crates/`, an example app under `examples/app-demo/`, a VitePress +documentation site under `docs/`, and CI workflows under `.github/workflows/`. ## Workspace Layout ``` crates/ - edgezero-core/ # Core: routing, extractors, middleware, proxy, body, errors + edgezero-core/ # Core: routing, extractors, middleware, outbound HTTP, body, errors edgezero-macros/ # Proc macros: #[action], #[app] edgezero-adapter/ # Adapter registry and traits edgezero-adapter-fastly/ # Fastly Compute bridge (wasm32-wasip1) @@ -20,6 +20,7 @@ crates/ edgezero-adapter-spin/ # Fermyon Spin bridge (wasm32-wasip2) edgezero-adapter-axum/ # Axum/Tokio bridge (native, dev server) edgezero-cli/ # CLI lib + bin: new, build, deploy, serve, auth, provision, config (validate|push), demo + edgezero-test-harness/ # Standalone native integration-test runner (own workspace/lockfile) examples/app-demo/ # Reference app with all 4 adapters (excluded from workspace) docs/ # VitePress documentation site (Node.js) scripts/ # Build/deploy/test helper scripts @@ -134,11 +135,13 @@ impl Middleware for MyMiddleware { } ``` -### Proxy +### Outbound HTTP -Use `ProxyService` with adapter-specific clients (`FastlyProxyClient`, -`CloudflareProxyClient`, `SpinProxyClient`). Keep proxy logic provider-agnostic -in core. +Handlers obtain `HttpClient` from `RequestContext::http_client()`, construct +`OutboundRequest` values, and consume `OutboundResponse` or per-slot +`OutboundSlotResult` outcomes. Keep provider transport logic in adapter-specific +`*OutboundClient` implementations and use the capability matrix for behavioral +differences. ### Logging @@ -157,6 +160,9 @@ in core. - **Async tests** use `futures::executor::block_on` (not Tokio) for WASM compat. - **Minimal changes**: every change should impact as little code as possible. Avoid unnecessary refactoring, docstrings on untouched code, or premature abstractions. +- **JSON construction**: use `serde_json::json!` for ordinary JSON values and fixtures; + serialize typed structs directly. Keep raw JSON for syntax or exact-byte tests, + static-input APIs, and last-resort fallbacks that must avoid serialization. - **Feature gates**: platform-specific code goes behind `fastly`, `cloudflare`, `spin`, or `axum` features. Core stays `default-features = false` for WASM targets. - **No direct `http` crate imports** in application code — use `edgezero_core` re-exports. @@ -168,7 +174,7 @@ Each adapter follows the same structure: - `context.rs` — platform-specific request context - `request.rs` — platform request → core request conversion - `response.rs` — core response → platform response conversion -- `proxy.rs` — platform-specific proxy client +- `outbound.rs` — platform-specific outbound HTTP client - `logger.rs` — platform-specific logging init - `cli.rs` — adapter dispatch behind the `cli` feature: `build` / `deploy` / `serve` (legacy) plus `Adapter::execute` for `auth` (login/logout/status) and dedicated trait methods `provision` (Stage 6 — platform-resource creation) and `push_config_entries` (Stage 7 — `config push` writeback). Self-registers via `#[ctor]` into the `edgezero-adapter` registry. diff --git a/Cargo.lock b/Cargo.lock index 4991d92bd..2cd7ef219 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "android_system_properties" -version = "0.1.6" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] @@ -93,9 +93,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.104" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "app-demo-core" @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.44" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "515a1f282e33d55983c499d7e9e87082e81cbc32974825bf9032f928392d5844" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -145,13 +145,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.92" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] @@ -168,9 +168,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.1" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" dependencies = [ "aws-lc-sys", "zeroize", @@ -178,9 +178,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" dependencies = [ "cc", "cmake", @@ -273,9 +273,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -286,6 +286,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.12.1" @@ -308,9 +317,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.3" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -346,9 +355,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.4.5" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", @@ -364,18 +373,18 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", - "cpufeatures 0.3.1", + "cpufeatures 0.3.0", "rand_core", ] @@ -394,9 +403,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.6" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -404,9 +413,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -416,14 +425,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] @@ -458,9 +467,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.8" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", @@ -468,9 +477,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.39" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe67f2944eef52fc7b106b8c9450d243a88701a0c065f7f57235e76abaed7df" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "brotli", "compression-core", @@ -480,9 +489,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.33" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "const-oid" @@ -517,22 +526,32 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.5.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-common" version = "0.2.2" @@ -558,18 +577,8 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core", + "darling_macro", ] [[package]] @@ -586,37 +595,13 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.119", -] - [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.119", ] @@ -627,6 +612,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ + "powerfmt", "serde_core", ] @@ -645,7 +631,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling 0.20.11", + "darling", "proc-macro2", "quote", "syn 2.0.119", @@ -670,6 +656,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + [[package]] name = "digest" version = "0.11.3" @@ -678,18 +674,18 @@ checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", "const-oid", - "crypto-common", + "crypto-common 0.2.2", ] [[package]] name = "displaydoc" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] @@ -709,9 +705,10 @@ name = "edgezero-adapter" version = "0.1.0" dependencies = [ "base16ct", + "edgezero-core", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "tempfile", "toml", "walkdir", @@ -722,15 +719,22 @@ name = "edgezero-adapter-axum" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "async-trait", "axum", "bytes", "ctor", "edgezero-adapter", + "edgezero-adapter-cloudflare", "edgezero-core", + "flate2", "futures", "futures-util", "http", + "http-body", + "hyper", + "hyper-util", + "libc", "log", "redb", "reqwest", @@ -751,6 +755,7 @@ name = "edgezero-adapter-cloudflare" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "async-trait", "brotli", "bytes", @@ -766,6 +771,7 @@ dependencies = [ "toml_edit", "walkdir", "wasm-bindgen-test", + "wasm-streams", "web-sys", "worker", ] @@ -785,6 +791,8 @@ dependencies = [ "edgezero-adapter", "edgezero-core", "fastly", + "fastly-shared", + "fastly-sys", "fern", "flate2", "futures", @@ -794,7 +802,7 @@ dependencies = [ "log-fastly", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "tempfile", "thiserror 2.0.21", "toml_edit", @@ -806,13 +814,12 @@ name = "edgezero-adapter-spin" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "async-trait", - "brotli", "bytes", "ctor", "edgezero-adapter", "edgezero-core", - "flate2", "futures", "futures-util", "http-body-util", @@ -844,6 +851,7 @@ dependencies = [ "edgezero-core", "futures", "handlebars", + "httparse", "log", "proc-macro2", "serde", @@ -862,13 +870,20 @@ dependencies = [ name = "edgezero-core" version = "0.1.0" dependencies = [ + "adler2", + "alloc-no-stdlib", + "alloc-stdlib", "anyhow", "async-compression", "async-stream", "async-trait", "base16ct", "brotli", + "brotli-decompressor", "bytes", + "compression-codecs", + "compression-core", + "crc32fast", "edgezero-macros", "flate2", "futures", @@ -877,17 +892,20 @@ dependencies = [ "http-body", "log", "matchit 0.9.2", + "miniz_oxide", "ryu", "serde", "serde_json", "serde_path_to_error", "serde_urlencoded", - "sha2 0.11.0", + "sha2 0.11.1", + "simd-adler32", "tempfile", "thiserror 2.0.21", "toml", "tower-service", "tracing", + "url", "validator", "web-time", ] @@ -913,9 +931,9 @@ dependencies = [ [[package]] name = "either" -version = "1.18.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elsa" @@ -1018,9 +1036,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fern" @@ -1033,19 +1051,18 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.12" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.10" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", - "zlib-rs", ] [[package]] @@ -1054,6 +1071,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -1077,9 +1100,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1092,9 +1115,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1102,15 +1125,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1119,38 +1142,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1188,29 +1211,43 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core", + "wasip2", + "wasip3 0.4.0+wasi-0.3.0-rc-2026-01-06", "wasm-bindgen", ] [[package]] name = "glob" -version = "0.3.4" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "handlebars" -version = "6.4.4" +version = "6.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c54236f9045c8004a77942bebc52145b4844639db934a5c70fe08617fbe61a" +checksum = "4633d16a2350341713c379d6d06a4b9e1845329386026a49ce4fd09c2f3b16f6" dependencies = [ "derive_builder", "log", @@ -1222,13 +1259,22 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1239,9 +1285,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.5.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1249,9 +1295,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", @@ -1284,22 +1330,21 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" dependencies = [ "atomic-waker", "bytes", - "futures-channel", "futures-core", "http", "http-body", @@ -1376,9 +1421,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", @@ -1390,9 +1435,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1403,9 +1448,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1417,17 +1462,16 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -1438,15 +1482,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.3.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1492,21 +1536,21 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.2" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "ipnet" -version = "2.12.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -1580,11 +1624,11 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.35" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.4.3", + "getrandom 0.3.4", "libc", ] @@ -1607,9 +1651,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -1648,15 +1692,15 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "log" -version = "0.4.34" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "log-fastly" @@ -1664,6 +1708,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "433713b6191ad3e1d5248db9536f3e2d99a12d7ff998afd272a98d96e6dbc2ed" dependencies = [ + "anyhow", "fastly", "log", "regex", @@ -1695,9 +1740,9 @@ checksum = "8863b587001c1b9a8a4e36008cebc6b3612cb1226fe2de94858e06092687b608" [[package]] name = "memchr" -version = "2.8.3" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mime" @@ -1717,9 +1762,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.9.1" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -1727,9 +1772,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.3" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -1753,9 +1798,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-modular" -version = "0.6.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8e500409e6cd603b03e477c26a6caecdc27ac58979a53e881c75eafc079f44" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" [[package]] name = "num-order" @@ -1823,9 +1868,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.9.1" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -1833,9 +1878,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.9.1" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -1843,9 +1888,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.9.1" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", @@ -1856,11 +1901,12 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.9.1" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", + "sha2 0.10.9", ] [[package]] @@ -1891,15 +1937,15 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.34" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "potential_utf" -version = "0.1.6" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -1921,41 +1967,51 @@ dependencies = [ ] [[package]] -name = "proc-macro-error-attr3" -version = "3.1.1" +name = "prettyplease" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn 3.0.9", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ "proc-macro2", "quote", ] [[package]] -name = "proc-macro-error3" -version = "3.1.1" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ - "proc-macro-error-attr3", + "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] name = "proc-macro2" -version = "1.0.107" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quinn" -version = "0.11.11" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -1979,7 +2035,7 @@ checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.4.3", + "getrandom 0.4.2", "lru-slab", "rand", "rand_pcg", @@ -1996,27 +2052,33 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.15" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.47" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -2030,7 +2092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.3", + "getrandom 0.4.2", "rand_core", ] @@ -2060,9 +2122,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.13.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2072,9 +2134,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.18" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2083,9 +2145,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.11" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -2146,7 +2208,7 @@ version = "0.40.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.11.1", "fallible-iterator", "fallible-streaming-iterator", "libsqlite3-sys", @@ -2155,9 +2217,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.3" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -2174,7 +2236,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -2209,9 +2271,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.1" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -2246,9 +2308,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring", @@ -2258,9 +2320,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2292,7 +2354,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.11.1", "core-foundation", "core-foundation-sys", "libc", @@ -2317,9 +2379,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.229" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -2338,30 +2400,31 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.229" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.229" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ + "indexmap", "itoa", "memchr", "serde", @@ -2382,13 +2445,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.21" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] @@ -2427,12 +2490,23 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", - "cpufeatures 0.3.1", + "cpufeatures 0.3.0", "digest 0.11.3", ] @@ -2454,15 +2528,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.10" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simd_cesu8" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" dependencies = [ "rustc_version", "simdutf8", @@ -2503,15 +2577,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.5" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2520,8 +2594,7 @@ dependencies = [ [[package]] name = "spin-macro" version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8a641da857d4e7cada31a853c40180eeb10485a0f2f0282d0a7e5a2a4105d4" +source = "git+https://github.com/stackpop/spin-rust-sdk.git?rev=e65ff90dadf4173b08b14194e878ad3acd555337#e65ff90dadf4173b08b14194e878ad3acd555337" dependencies = [ "proc-macro2", "quote", @@ -2531,8 +2604,7 @@ dependencies = [ [[package]] name = "spin-sdk" version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7a3d9a6805ccdb4d404e91ccf7198ac5e486b6199dc9387aeebaa368272112" +source = "git+https://github.com/stackpop/spin-rust-sdk.git?rev=e65ff90dadf4173b08b14194e878ad3acd555337#e65ff90dadf4173b08b14194e878ad3acd555337" dependencies = [ "anyhow", "bytes", @@ -2542,7 +2614,7 @@ dependencies = [ "http-body-util", "spin-macro", "thiserror 2.0.21", - "wasip3", + "wasip3 0.9.0+wasi-0.3.0", ] [[package]] @@ -2639,9 +2711,9 @@ dependencies = [ [[package]] name = "target-triple" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" [[package]] name = "tempfile" @@ -2650,7 +2722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2707,11 +2779,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.55" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", "libc", "num-conv", "num_threads", @@ -2723,15 +2796,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.9" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.32" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -2739,9 +2812,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -2749,9 +2822,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.13.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -2764,9 +2837,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -2780,20 +2853,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] name = "tokio-rustls" -version = "0.26.5" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2801,9 +2874,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.5+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -2838,18 +2911,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.3+spec-1.1.0" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" [[package]] name = "tower" @@ -2873,7 +2946,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.11.1", "bytes", "futures-util", "http", @@ -2937,9 +3010,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.120" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" +checksum = "47c635f0191bd3a2941013e5062667100969f8c4e9cd787c14f977265d73616e" dependencies = [ "glob", "serde", @@ -3021,12 +3094,13 @@ dependencies = [ [[package]] name = "validator_derive" -version = "0.20.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240e4b81c20a1d6d50d1d7265c658dfbd204e8b9ac4d80f3c931f39462196335" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" dependencies = [ - "darling 0.23.0", - "proc-macro-error3", + "darling", + "once_cell", + "proc-macro-error2", "proc-macro2", "quote", "syn 2.0.119", @@ -3056,9 +3130,9 @@ dependencies = [ [[package]] name = "want" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" dependencies = [ "try-lock", ] @@ -3071,24 +3145,33 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.4+wasi-0.2.12" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen 0.57.1", ] [[package]] name = "wasip3" -version = "0.7.1+wasi-0.3.0" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasip3" +version = "0.9.0+wasi-0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e9ed91ee5a995b6fe16c89d4511a8c12eb76967be20b57557618e02e781bcc" +checksum = "f1d5749fdf69bb9400562eba50f8f25f5cd800ef4b74300038b6f99ae7409674" dependencies = [ "bytes", "http", "http-body", "thiserror 2.0.21", - "wit-bindgen 0.57.1", + "wit-bindgen 0.62.0", ] [[package]] @@ -3187,24 +3270,46 @@ checksum = "f37b4f992cebe528ef34964ae69681ac0fe7080071e7298e46008f9d380302af" [[package]] name = "wasm-encoder" -version = "0.247.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" +dependencies = [ + "leb128fmt", + "wasmparser 0.259.0", ] [[package]] name = "wasm-metadata" -version = "0.247.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7bf119eb01f4a246864c10bd87c8f26c566350abb25aa4d5273c415bbab338" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.259.0", + "wasmparser 0.259.0", ] [[package]] @@ -3222,12 +3327,24 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.247.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.13.1", - "hashbrown", + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.17.1", "indexmap", "semver", ] @@ -3335,7 +3452,16 @@ 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]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -3353,14 +3479,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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3369,53 +3512,101 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -3426,7 +3617,8 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.11.1", + "wit-bindgen-rust-macro 0.51.0", ] [[package]] @@ -3435,80 +3627,149 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.11.1", +] + +[[package]] +name = "wit-bindgen" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cb4b5556c3a791e86838ea287782bdafa704d55b0e68b5b81a3a16b9ea5f4b" +dependencies = [ + "bitflags 2.11.1", "futures", - "wit-bindgen-rust-macro", + "wit-bindgen-rust-macro 0.62.0", ] [[package]] name = "wit-bindgen-core" -version = "0.57.1" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" +checksum = "72ad22a37ecbc0e1fdca34d2b670ba41368cb0920735643e00fdf39a16ee5431" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.259.0", ] [[package]] name = "wit-bindgen-rust" -version = "0.57.1" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", "indexmap", - "prettyplease", + "prettyplease 0.2.37", "syn 2.0.119", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "wasm-metadata 0.244.0", + "wit-bindgen-core 0.51.0", + "wit-component 0.244.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ba5213d4332c260a0d4e69e34b0f7e4b85ddae10b6ebfbac00d9c185ba628a" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease 0.3.0", + "syn 3.0.9", + "wasm-metadata 0.259.0", + "wit-bindgen-core 0.62.0", + "wit-component 0.259.0", ] [[package]] name = "wit-bindgen-rust-macro" -version = "0.57.1" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9237d678e3513ad24e96fe98beacdc0db6405284ba2a2400418cf0d42caa89" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ "anyhow", - "prettyplease", + "prettyplease 0.2.37", "proc-macro2", "quote", "syn 2.0.119", - "wit-bindgen-core", - "wit-bindgen-rust", + "wit-bindgen-core 0.51.0", + "wit-bindgen-rust 0.51.0", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f19d63da8e478a370ef8192d90ccb517965b7747d6b46cd2d831dd0ef4d2541" +dependencies = [ + "anyhow", + "prettyplease 0.3.0", + "proc-macro2", + "quote", + "syn 3.0.9", + "wit-bindgen-core 0.62.0", + "wit-bindgen-rust 0.62.0", ] [[package]] name = "wit-component" -version = "0.247.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.13.1", + "bitflags 2.11.1", "indexmap", "log", "serde", "serde_derive", "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "wasm-encoder 0.244.0", + "wasm-metadata 0.244.0", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-component" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092f783dee3253265fcde131475055f8c275ac0176a9c0396db1dc7b5544bfb4" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.259.0", + "wasm-metadata 0.259.0", + "wasmparser 0.259.0", + "wit-parser 0.259.0", ] [[package]] name = "wit-parser" -version = "0.247.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", - "hashbrown", "id-arena", "indexmap", "log", @@ -3517,7 +3778,26 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6390f4f02493ce678d509c859cf1698e38929fb75c8012a6ee2f7d6b6cb66cd7" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser 0.259.0", ] [[package]] @@ -3581,15 +3861,15 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.4" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3631,15 +3911,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.9.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3648,9 +3928,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.8" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3659,23 +3939,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.6" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] -[[package]] -name = "zlib-rs" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" - [[package]] name = "zmij" -version = "1.0.23" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index cde6c0c69..223baf956 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ "crates/edgezero-core", "crates/edgezero-macros", ] -exclude = ["examples/app-demo"] +exclude = ["examples/app-demo", "crates/edgezero-test-harness"] resolver = "2" [workspace.package] @@ -27,24 +27,27 @@ publish = false version = "0.1.0" [workspace.dependencies] +alloc-no-stdlib = "=2.0.4" +alloc-stdlib = "=0.2.4" +adler2 = "=2.0.1" anyhow = "1" -async-compression = { version = "0.4", features = [ +async-compression = { version = "=0.4.43", default-features = false, features = [ "futures-io", "gzip", "brotli", + "zlib", ] } async-stream = "0.3" async-trait = "0.1" axum = { version = "0.8", default-features = true } base16ct = { version = "1", features = ["alloc"] } -# Held at 8 while `async-compression` still resolves brotli 8: taking 9 -# directly does not replace that copy, it adds a second complete -# allocator/decompressor stack to every adapter graph (~40 KB of extra -# wasm in the demo Spin build). We only use `Decompressor` and -# `CompressorWriter`, unchanged across the two majors. -brotli = "8" -bytes = "1" +brotli = "=8.0.4" +brotli-decompressor = "=5.0.1" +bytes = "=1.12.1" chrono = "0.4" +compression-codecs = { version = "=0.4.38", default-features = false, features = ["brotli", "gzip", "zlib"] } +compression-core = "=0.4.32" +crc32fast = "=1.5.0" ctor = "1.0" edgezero-adapter = { path = "crates/edgezero-adapter" } edgezero-adapter-axum = { path = "crates/edgezero-adapter-axum", default-features = false } @@ -53,17 +56,24 @@ edgezero-adapter-fastly = { path = "crates/edgezero-adapter-fastly", default-fea edgezero-adapter-spin = { path = "crates/edgezero-adapter-spin", default-features = false } edgezero-core = { path = "crates/edgezero-core", default-features = false } edgezero-cli = { path = "crates/edgezero-cli", default-features = false } -fastly = "0.13" +fastly = "=0.13.1" +fastly-shared = "=0.13.1" +fastly-sys = "=0.13.1" fern = "0.7" -flate2 = { version = "1", features = ["rust_backend"] } +flate2 = { version = "=1.1.9", default-features = false, features = ["rust_backend"] } futures = { version = "0.3", features = ["std", "executor"] } futures-util = { version = "0.3", features = ["alloc", "io"] } handlebars = "6" -http = "1" +http = "=1.4.2" http-body = "1" http-body-util = "0.1" +httparse = "=1.10.1" +hyper = { version = "=1.12.0", features = ["http1", "server"] } +hyper-util = { version = "0.1", features = ["tokio"] } log = "0.4" -log-fastly = "0.13" +libc = "0.2" +miniz_oxide = { version = "=0.8.9", default-features = false, features = ["simd", "with-alloc"] } +log-fastly = "=0.13.1" matchit = "0.9" once_cell = "1" # 4.2 asserts that no read reference outlives its transaction. That check @@ -71,7 +81,8 @@ once_cell = "1" # floor stays at 4.2 rather than 4.1 -- resolving an older patch would # silently drop the guard against reintroducing it. redb = "4.2.0" -reqwest = { version = "0.13", default-features = false, features = ["rustls", "blocking", "json"] } +reqwest = { version = "=0.13.4", default-features = false, features = ["rustls", "blocking", "json"] } +simd-adler32 = { version = "=0.3.9", default-features = false } # `bundled` ships SQLite source so operators don't need a system # `libsqlite3-sys` install. Used by `edgezero-adapter-spin`'s CLI-only # `config push --adapter spin` writer to write into Spin's local KV @@ -80,7 +91,7 @@ reqwest = { version = "0.13", default-features = false, features = ["rustls", "b rusqlite = { version = "0.40", default-features = false, features = ["bundled"] } ryu = "1" serde = { version = "1", features = ["derive"] } -serde_json = "1" +serde_json = "=1.0.150" serde_path_to_error = "0.1" serde_urlencoded = "0.7" sha2 = "0.11" @@ -90,27 +101,25 @@ proc-macro2 = { version = "1", features = ["span-locations"] } quote = "1" syn = { version = "3", features = ["full", "extra-traits", "visit"] } subtle = "2" -# Pinned to the `~7.0` range (allows 7.0.x, blocks 7.1+) so a minor -# bump that touches `key_value::Store::open`'s async signature or the -# wasi-http import surface fails at build time rather than at `spin -# up` (where a runtime mismatch surfaces as opaque WIT linker errors). -# 7.0 imports `wasi:http/types@0.3.0`, which Spin 4.1+ provides and -# Spin 4.0.x does not -- see the spin entry in `.tool-versions`. -spin-sdk = { version = "~7.0", default-features = false, features = ["http", "key-value", "variables"] } +# The Spin outbound classifier exhaustively matches this SDK's WASI HTTP +# snapshot, so both dependencies move only through an explicit review. +spin-sdk = { version = "=7.0.0", git = "https://github.com/stackpop/spin-rust-sdk.git", rev = "e65ff90dadf4173b08b14194e878ad3acd555337", default-features = false, features = ["http", "key-value", "variables"] } tempfile = "3" toml_edit = "0.25" thiserror = "2" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } +tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] } trybuild = "1" toml = { version = "1.1" } tower = { version = "0.5", features = ["util"] } tower-layer = "0.3" tower-service = "0.3" tracing = "0.1" +url = "=2.5.8" validator = { version = "0.21", features = ["derive"] } walkdir = { version = "2" } web-time = "1" -worker = { version = "0.8", features = ["http"] } +worker = { version = "=0.8.5", default-features = false, features = ["http"] } +wasm-streams = "=0.6.0" [workspace.lints.clippy] # Same strict gate as the demo workspace. Allow-list mirrors the demo's @@ -147,11 +156,10 @@ separated_literal_suffix = "allow" # decompress_body, and the rest). pub_with_shorthand = "allow" # `module_name_repetitions` was attempted: 39 sites in edgezero-core, -# centred on three concrete blockers that surfaced during the rename: -# 1. `proxy::Request`/`proxy::Response` would collide with the -# `http::Request`/`http::Response` already imported by every -# consumer; the only viable alternative names (`OutboundRequest`, -# `Outbound`) are strictly more verbose than `ProxyRequest`. +# centred on three concrete blockers that surfaced during the API review: +# 1. Outbound request/response types intentionally retain the `Outbound*` +# prefix because they are re-exported beside inbound `http::Request` and +# `http::Response`; shorter names would collide in normal consumers. # 2. `manifest.rs` has 17 `Manifest*` types; consumers in adapters, # cli, demos, scaffold templates, and the macro-generated app # code use these names directly. Stripping the prefix would force @@ -194,4 +202,5 @@ std_instead_of_alloc = "allow" std_instead_of_core = "allow" [workspace.lints.rust] -unsafe_code = "deny" \ No newline at end of file +unsafe_code = "deny" +unexpected_cfgs = "deny" diff --git a/README.md b/README.md index 4c01d1d8b..1b2c0413b 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Full documentation is available at **[iabtechlab.github.io/edgezero](https://iab - [Getting Started](https://iabtechlab.github.io/edgezero/guide/getting-started) - Project setup and first steps - [Architecture](https://iabtechlab.github.io/edgezero/guide/architecture) - How EdgeZero works - [Configuration](https://iabtechlab.github.io/edgezero/guide/configuration) - `edgezero.toml` reference +- [Outbound HTTP](https://iabtechlab.github.io/edgezero/guide/proxying) - Portable requests, limits, deadlines, and batching +- [Capabilities](https://iabtechlab.github.io/edgezero/guide/capabilities) - Exact adapter support matrix and deployment caveats - [CLI Reference](https://iabtechlab.github.io/edgezero/guide/cli-reference) - All CLI commands - [Blob App-Config Migration](https://iabtechlab.github.io/edgezero/guide/blob-app-config-migration) - Typed `AppConfig` extractor + `config push` / `config diff` workflow diff --git a/TODO.md b/TODO.md index 19bf99e07..83c6cb327 100644 --- a/TODO.md +++ b/TODO.md @@ -7,9 +7,9 @@ High-level backlog and decisions to drive the next milestones. ### High Priority - [ ] Core: `Response::json` behind `serde` feature -- [ ] Fastly proxy: add tests for backend send + header/body mapping +- [x] Fastly outbound HTTP: add tests for backend send + header/body mapping - [ ] Cloudflare streaming: map `Response::with_chunks` to `ReadableStream` with backpressure -- [ ] Core proxy: add async fetch facade (feature `async-client`) and implement Cloudflare proxy async fetch facade (feature `async-client`) and implement Cloudflare proxy +- [x] Core outbound HTTP: add the portable async client and Cloudflare fetch implementation ### Medium Priority @@ -31,13 +31,13 @@ High-level backlog and decisions to drive the next milestones. ## Test Coverage Plan (2025-09-18) -- [ ] Adapters: introduce Fastly/Cloudflare mapping tests (headers, streaming, proxy failure) to catch glue regressions. +- [x] Adapters: introduce Fastly/Cloudflare outbound mapping tests (headers, streaming, transport failure) to catch glue regressions. - [ ] Adapters: assert error-path mapping for Fastly/Cloudflare request conversion and re-enable the ignored Cloudflare response header test. - [ ] CLI: add integration tests for `edgezero new` scaffolding, feature-flag builds, and `dev` fallback app. - [ ] CLI: cover `dev_server`, generator, and template scaffolding flows with tempdir-based integration tests to guard manual HTTP parsing and shell commands. - [ ] CI: verify feature combinations (without `demo-example`, `json`, `form`) compile and run basic smoke tests. - [ ] Macros: add trybuild coverage for `app!` manifest expansion (route/middleware generation and error surfacing). -- [x] Core: unit-test `App::build_app`/`Hooks` wiring and `PathParams::deserialize` edge cases beyond indirect coverage. _(Added targeted unit tests in `crates/edgezero-core/src/app.rs` and `crates/edgezero-core/src/params.rs`.)_ +- [x] Core: unit-test `App::build::`/`Hooks` wiring and `PathParams::deserialize` edge cases beyond indirect coverage. _(Added targeted unit tests in `crates/edgezero-core/src/app.rs` and `crates/edgezero-core/src/params.rs`.)_ - [x] Coverage hygiene: consolidate duplicate router/extractor request-parsing tests and share adapter contract fixtures to reduce redundant maintenance. _(Router duplicates trimmed; extractor suite now owns request parsing checks.)_ - [x] Router: add regression cases for overlapping routes, prefix/nest behaviour, and BodyMode error handling (`Streaming` vs `Buffered`). - [x] Controller: cover negative paths (`State` missing, `ValidatedJson` errors) and assert returned status/body. @@ -49,14 +49,14 @@ High-level backlog and decisions to drive the next milestones. - [x] Map `edgezero-core::Response` -> Fastly Response (headers/body) - [x] Handle binary/text bodies and content-length - [x] Example: runnable Fastly demo (`examples/app-demo/crates/app-demo-adapter-fastly`) with wasm32-wasip1 target and explicit logger init - - [x] Streaming: native streaming on Fastly via `stream_to_client` (wasm32-wasip1), buffered fallback elsewhere -- [ ] Cloudflare Workers adapter MVP + - [x] Streaming: adapter-owned lazy response delivery on Axum, Cloudflare, Fastly, and Spin +- [x] Cloudflare Workers adapter MVP - [x] Basic mapping: Workers Request -> EdgeZero Request - - [x] Basic mapping: EdgeZero Response -> Workers Response (buffered bodies) - - [ ] Streaming behavior (ReadableStream) and backpressure + - [x] Basic mapping: EdgeZero Response -> Workers Response + - [x] Adapter-owned streaming writer and backpressure propagation - [x] Example: deploy sample with `wrangler` (`examples/app-demo/crates/app-demo-adapter-cloudflare`) - [ ] CLI - - [x] `edgezero new `: scaffold an app (lib with `build_app()`) + - [x] `edgezero new `: scaffold an app (lib consumed by `App::build::`) - [x] `edgezero build --adapter fastly|cloudflare` - [x] `edgezero deploy --adapter fastly|cloudflare` - [ ] `edgezero dev` improvements: better HTTP parsing, hot-reload @@ -84,7 +84,7 @@ High-level backlog and decisions to drive the next milestones. ## Roadmap (2025-09-24) -- [x] Adapter stability: formalise the provider adapter contract (request/response mapping, streaming guarantees, proxy hooks) and capture it in shared docs + integration tests so new targets plug in safely. (`docs/adapter-contract.md`, Fastly contract tests under `crates/edgezero-adapter-fastly/tests/contract.rs`, Cloudflare contract tests under `crates/edgezero-adapter-cloudflare/tests/contract.rs`, manifest schema in `docs/manifest.md`) +- [x] Adapter stability: formalise the provider adapter contract (request/response mapping, streaming guarantees, outbound hooks) and capture it in shared docs + integration tests so new targets plug in safely. (`docs/adapter-contract.md`, Fastly contract tests under `crates/edgezero-adapter-fastly/tests/contract.rs`, Cloudflare contract tests under `crates/edgezero-adapter-cloudflare/tests/contract.rs`, manifest schema in `docs/manifest.md`) - [ ] Provider additions: prototype a third adapter (e.g. AWS Lambda@Edge or Vercel Edge Functions) using the stabilized adapter API to validate cross-provider abstractions. - [x] Manifest ergonomics: design an `edgezero.toml` schema that mirrors Spin’s manifest convenience (route triggers, env/secrets, build targets) while remaining provider-agnostic; update CLI scaffolding accordingly. (`crates/edgezero-cli/src/manifest.rs`, templates in `crates/edgezero-cli/src/templates/root/edgezero.toml.hbs`, doc `docs/manifest.md`, app-demo manifest `examples/app-demo/edgezero.toml`) - [ ] Tooling parity: extend `edgezero-cli` with template/plugin style commands (similar to Spin templates) to streamline new app scaffolds and provider-specific wiring. @@ -112,9 +112,9 @@ High-level backlog and decisions to drive the next milestones. ### Familiarization Summary -- EdgeZero centres around `edgezero-core`, which provides provider-neutral HTTP primitives, routing, middleware, logging, and proxy abstractions; adapters reuse these types to stay DRY. +- EdgeZero centres around `edgezero-core`, which provides provider-neutral HTTP primitives, routing, middleware, logging, and outbound abstractions; adapters reuse these types to stay DRY. - Controller ergonomics live in `edgezero-controller` plus `edgezero-macros`, offering `#[action]` functions that extract typed inputs and return `Responder`s. -- Provider adapters (`edgezero-adapter-fastly`, `edgezero-adapter-cloudflare`) are feature-gated; each exposes `handle` plus logging/proxy helpers while delegating behaviour to the core crate. +- Provider adapters (`edgezero-adapter-fastly`, `edgezero-adapter-cloudflare`) are feature-gated; each exposes `handle` plus logging/outbound helpers while delegating behavior to the core crate. - Supporting crates include `edgezero-std` for stdout logging, `edgezero-cli` for dev server + scaffolding, and demo workspaces under `examples/app-demo` to validate provider flows. - Workspace `Cargo.toml` keeps default members lean (core only) to support offline builds; additional crates are opt-in via features when targeting specific adapters. @@ -267,7 +267,7 @@ High-level backlog and decisions to drive the next milestones. - [ ] Update the Fastly adapter to compile against `fastly` 0.11 APIs (request building, async streaming, response conversion). - [ ] Adjust logging helper to the new log-fastly builder API. -- [ ] Ensure proxy tests/builds pass for streaming + compression paths. +- [x] Ensure outbound tests/builds pass for streaming + compression paths. - [ ] Verify the app demos compile for `wasm32-wasip1` with the updated SDK. ## Review (2025-09-19 01:28 UTC) @@ -311,7 +311,7 @@ High-level backlog and decisions to drive the next milestones. ## Review (2025-09-19 02:35 UTC) - Temporary stopgap: adapter builds against Fastly 0.11 by buffering request/response bodies and wiring a new logging helper; wasm demo (`cargo build -p app-demo-adapter-fastly --target wasm32-wasip1`) and `cargo test` now succeed. -- Regression: streaming proxy behaviour (and streaming decompression) is currently disabled because bodies are buffered; follow-up work is required to restore async streaming under the new SDK. +- Superseded by the 2026 response-egress hard cut: every adapter now preserves lazy downstream response production without a 16 MiB conversion fallback. ## Review (2025-09-19 07:35 UTC) @@ -505,14 +505,14 @@ High-level backlog and decisions to drive the next milestones. ## Review (2026-01-27 00:50:51 UTC) -- Summary: Aligned guide content with current EdgeZero APIs (App::build_app, adapter entrypoints, middleware signature, proxy clients), corrected routing/streaming/handlers/CLI details, and refreshed manifest/logging docs; added the missing CLI/dev features list to the roadmap. +- Summary: Aligned guide content with the then-current EdgeZero APIs, corrected routing/streaming/handlers/CLI details, and refreshed manifest/logging docs; this was superseded by the outbound HTTP migration. - Assumptions: Docs now reflect current behavior for Fastly/Cloudflare/Axum adapters and the CLI; future features are captured explicitly in the roadmap rather than implied in guides. - Outstanding: None (docs-only updates). ## Review (2026-01-27 01:02:05 UTC) -- Summary: Condensed the proxying guide into a single end-to-end example that uses adapter proxy handles, and added short logging-status callouts to the Fastly, Cloudflare, and Axum adapter docs. -- Assumptions: The proxy handle approach is the preferred public pattern; adapter logging notes should stay concise and match current defaults. +- Summary: Condensed the upstream-forwarding guide into one example and added logging-status callouts; the client example was superseded by `HttpClient`. +- Assumptions: Adapter logging notes should stay concise and match current defaults. - Outstanding: None (docs-only updates). ## Review (2026-01-27 01:05:18 UTC) @@ -589,14 +589,14 @@ High-level backlog and decisions to drive the next milestones. - [x] Add the key doc/CLI gaps found during the review to the roadmap page. - [x] Add an explicit roadmap item for Spin support (define scope at the doc level). -## Codex Plan (2026-01-27 - Proxying Snippet + Adapter Logging Callout) +## Codex Plan (2026-01-27 - Upstream Snippet + Adapter Logging Callout) -- [x] Condense proxying guide into a single end-to-end example using adapter proxy handles. +- [x] Condense the upstream guide into a single end-to-end example (later migrated to `HttpClient`). - [x] Add a short logging status callout to the adapter docs (Axum/Cloudflare/Fastly). ## Codex Plan (2026-01-27 - Docs Alignment + Roadmap Additions) -- [x] Update guides to reflect current APIs (App::build_app, adapter entrypoints, middleware signature, proxy client usage). +- [x] Update guides to reflect current APIs (`App::build::`, adapter entrypoints, middleware signature, outbound client usage). - [x] Correct routing, streaming, handlers, and CLI reference docs to match current behavior. - [x] Refresh configuration docs to align with manifest schema and loader APIs. - [x] Add missing-feature backlog (list-adapters, exit codes, manifest search-up, RUST_LOG, hot reload) to the roadmap section. diff --git a/crates/edgezero-adapter-axum/Cargo.toml b/crates/edgezero-adapter-axum/Cargo.toml index 9979e39a5..0e3e110a4 100644 --- a/crates/edgezero-adapter-axum/Cargo.toml +++ b/crates/edgezero-adapter-axum/Cargo.toml @@ -13,10 +13,14 @@ workspace = true [features] default = ["axum"] axum = [ + "dep:async-stream", "dep:axum", "dep:tokio", "dep:tower", "dep:futures-util", + "dep:http-body", + "dep:hyper", + "dep:hyper-util", "dep:reqwest", "dep:redb", ] @@ -27,6 +31,7 @@ cli = [ "dep:toml", "dep:walkdir", ] +test-utils = ["axum"] [dependencies] edgezero-adapter = { path = "../edgezero-adapter", optional = true, features = [ @@ -35,15 +40,20 @@ edgezero-adapter = { path = "../edgezero-adapter", optional = true, features = [ edgezero-core = { path = "../edgezero-core" } anyhow = { workspace = true } async-trait = { workspace = true } +async-stream = { workspace = true, optional = true } axum = { workspace = true, optional = true } bytes = { workspace = true } ctor = { workspace = true, optional = true } futures = { workspace = true } futures-util = { workspace = true, optional = true } http = { workspace = true } +http-body = { workspace = true, optional = true } +hyper = { workspace = true, optional = true } +hyper-util = { workspace = true, optional = true } log = { workspace = true } redb = { workspace = true, optional = true } reqwest = { workspace = true, optional = true } +serde = { workspace = true } serde_json = { workspace = true } simple_logger = { workspace = true } thiserror = { workspace = true } @@ -53,10 +63,18 @@ tower = { workspace = true, optional = true } tracing = { workspace = true } walkdir = { workspace = true, optional = true } +[target.'cfg(unix)'.dependencies] +libc = { workspace = true } + [dev-dependencies] async-trait = { workspace = true } axum = { workspace = true, features = ["macros"] } +edgezero-adapter-cloudflare = { path = "../edgezero-adapter-cloudflare", default-features = false } edgezero-core = { path = "../edgezero-core", features = ["test-utils"] } +flate2 = { workspace = true } +http-body = { workspace = true } +hyper = { workspace = true } +hyper-util = { workspace = true } serde = { workspace = true } tempfile = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] } +tokio = { workspace = true, features = ["io-util", "macros", "net", "rt", "rt-multi-thread", "test-util"] } diff --git a/crates/edgezero-adapter-axum/src/cli.rs b/crates/edgezero-adapter-axum/src/cli.rs index cc7b910fb..c410ab0a8 100644 --- a/crates/edgezero-adapter-axum/src/cli.rs +++ b/crates/edgezero-adapter-axum/src/cli.rs @@ -11,8 +11,8 @@ use edgezero_adapter::cli_support::{ find_manifest_upwards, find_workspace_root, path_distance, read_package_name, }; use edgezero_adapter::registry::{ - Adapter, AdapterAction, AdapterPushContext, ProvisionStores, ReadConfigEntry, ResolvedStoreId, - TypedSecretEntry, register_adapter, + Adapter, AdapterAction, AdapterExecutionTarget, AdapterPushContext, ProvisionStores, + ReadConfigEntry, ResolvedStoreId, TypedSecretEntry, register_adapter, }; use edgezero_adapter::scaffold::{ AdapterBlueprint, AdapterFileSpec, CommandTemplates, DependencySpec, LoggingDefaults, @@ -20,6 +20,7 @@ use edgezero_adapter::scaffold::{ }; use edgezero_core::addr; use edgezero_core::manifest::ManifestLoader; +use edgezero_core::{Capability, CapabilitySupport, PlatformMetadata}; use toml::Value; use walkdir::WalkDir; @@ -135,6 +136,38 @@ struct EdgezeroAxumConfig { reason = "axum has no validate_app_config_keys / validate_adapter_manifest requirements; those two trait defaults are intentionally inherited. `validate_typed_secrets` IS overridden below (each `#[secret]` value must map to one `EDGEZERO__SECRETS__` env var). `read_config_entry` delegates to `read_config_entry_local` (axum is local-only). `single_store_kinds` IS overridden below (returns `&[\"secrets\"]`)." )] impl Adapter for AxumCliAdapter { + fn capability(&self, capability: Capability) -> CapabilitySupport { + match capability { + Capability::ConfigReadDeadlines + | Capability::OutboundFailureDiagnostics + | Capability::ResponseEgressAbort + | Capability::ResponseEgressBackpressure + | Capability::ResponseEgressCompletion + | Capability::ResponseWriteDeadlines => CapabilitySupport::BestEffort, + Capability::BufferedResponseContentLength + | Capability::InboundReadDeadlines + | Capability::IngressAdmission + | Capability::IngressAdmissionAbort + | Capability::OutboundAuthorityOverride + | Capability::OutboundBatchCancellation + | Capability::OutboundBatchCompletionOrder + | Capability::OutboundBatchSlotIsolation + | Capability::OutboundCacheBypass + | Capability::OutboundDeadlines + | Capability::OutboundFlexiblePhaseBudget + | Capability::OutboundHeaderFidelity + | Capability::OutboundHttp + | Capability::OutboundRedirectFollowing + | Capability::LazyStreamedResponsePassthrough + | Capability::StreamedUploadDeadlines => CapabilitySupport::Native, + Capability::ConfigReadAllocationBounds + | Capability::OutboundCompleteResourceAccounting + | Capability::RawIngressFramingValidation + | Capability::RawIngressHeadLimits + | _ => CapabilitySupport::Unsupported, + } + } + fn execute(&self, action: AdapterAction, args: &[String]) -> Result<(), String> { match action { // The axum adapter is the in-process native dev server — @@ -160,10 +193,37 @@ impl Adapter for AxumCliAdapter { } } + fn execute_target( + &self, + action: AdapterAction, + target: &AdapterExecutionTarget, + args: &[String], + ) -> Result<(), String> { + match action { + AdapterAction::Build => build_target(target, args), + AdapterAction::Deploy => deploy(args), + AdapterAction::Serve => serve_target(target, args), + AdapterAction::AuthLogin + | AdapterAction::AuthLogout + | AdapterAction::AuthStatus + | AdapterAction::DeployStaging + | AdapterAction::EmitVersion + | AdapterAction::Healthcheck + | AdapterAction::Rollback + | _ => Err(format!( + "axum adapter does not support pinned target action {action:?}" + )), + } + } + fn name(&self) -> &'static str { "axum" } + fn platform_metadata(&self) -> PlatformMetadata { + crate::AXUM_PLATFORM + } + fn provision( &self, _manifest_root: &Path, @@ -395,12 +455,35 @@ fn register_ctor() { fn build(extra_args: &[String]) -> Result<(), String> { let project = locate_project()?; - run_cargo(&project, "build", extra_args) + run_cargo(&project, "build", extra_args, &[]) +} + +fn build_target(target: &AdapterExecutionTarget, extra_args: &[String]) -> Result<(), String> { + let project = read_axum_project(&target_manifest(target, "axum.toml")?)?; + run_cargo(&project, "build", extra_args, target.environment_defaults()) } fn serve(extra_args: &[String]) -> Result<(), String> { let project = locate_project()?; - run_cargo(&project, "run", extra_args) + run_cargo(&project, "run", extra_args, &[]) +} + +fn serve_target(target: &AdapterExecutionTarget, extra_args: &[String]) -> Result<(), String> { + let project = read_axum_project(&target_manifest(target, "axum.toml")?)?; + run_cargo(&project, "run", extra_args, target.environment_defaults()) +} + +fn target_manifest(target: &AdapterExecutionTarget, name: &str) -> Result { + let manifest = target + .platform_manifest() + .map_or_else(|| target.app_root().join(name), Path::to_path_buf); + if !manifest.is_file() { + return Err(format!( + "pinned axum manifest {} is not a regular file", + manifest.display() + )); + } + Ok(manifest) } fn deploy(_extra_args: &[String]) -> Result<(), String> { @@ -413,7 +496,12 @@ fn locate_project() -> Result { read_axum_project(&manifest) } -fn run_cargo(project: &AxumProject, subcommand: &str, extra_args: &[String]) -> Result<(), String> { +fn cargo_command( + project: &AxumProject, + subcommand: &str, + extra_args: &[String], + defaults: &[(String, String)], +) -> Result { let resolution = resolve_subprocess_addr(project)?; for warning in &resolution.warnings { log::warn!("[edgezero] {warning}"); @@ -442,6 +530,17 @@ fn run_cargo(project: &AxumProject, subcommand: &str, extra_args: &[String]) -> // no-op for the child process. command.env("EDGEZERO__ADAPTER__HOST", bind_addr.ip().to_string()); command.env("EDGEZERO__ADAPTER__PORT", bind_addr.port().to_string()); + command.envs(defaults.iter().map(|(key, value)| (key, value))); + Ok(command) +} + +fn run_cargo( + project: &AxumProject, + subcommand: &str, + extra_args: &[String], + defaults: &[(String, String)], +) -> Result<(), String> { + let mut command = cargo_command(project, subcommand, extra_args, defaults)?; let status = command .status() .map_err(|err| format!("failed to run cargo {subcommand}: {err}"))?; @@ -719,6 +818,168 @@ mod tests { use std::net::Ipv6Addr; use tempfile::tempdir; + #[test] + fn cargo_commands_preserve_ingress_defaults_and_bind_precedence() { + let dir = tempdir().unwrap(); + let project = AxumProject { + addr: SocketAddr::from(([127, 0, 0, 1], 8787)), + axum_host: None, + axum_manifest: dir.path().join("axum.toml"), + axum_port: None, + cargo_manifest: dir.path().join("Cargo.toml"), + crate_dir: dir.path().to_path_buf(), + crate_name: "fixture".to_owned(), + env_host: None, + env_port: None, + }; + let defaults = vec![ + ( + "EDGEZERO__ADAPTER__INGRESS__MAX_CONNECTIONS".to_owned(), + "3".to_owned(), + ), + ("EDGEZERO__ADAPTER__PORT".to_owned(), "9090".to_owned()), + ]; + for subcommand in ["build", "run"] { + let command = cargo_command(&project, subcommand, &[], &defaults).unwrap(); + assert!(command.get_envs().any(|(key, value)| key + == "EDGEZERO__ADAPTER__INGRESS__MAX_CONNECTIONS" + && value.is_some_and(|configured| configured == "3"))); + assert!( + command + .get_envs() + .any(|(key, value)| key == "EDGEZERO__ADAPTER__PORT" + && value.is_some_and(|configured| configured == "9090")) + ); + } + } + + #[test] + fn adapter_capability_matrix_matches_contracts() { + for (capability, support) in [ + ( + Capability::ConfigReadAllocationBounds, + CapabilitySupport::Unsupported, + ), + ( + Capability::ConfigReadDeadlines, + CapabilitySupport::BestEffort, + ), + (Capability::InboundReadDeadlines, CapabilitySupport::Native), + (Capability::IngressAdmission, CapabilitySupport::Native), + (Capability::IngressAdmissionAbort, CapabilitySupport::Native), + ( + Capability::RawIngressFramingValidation, + CapabilitySupport::Unsupported, + ), + ( + Capability::RawIngressHeadLimits, + CapabilitySupport::Unsupported, + ), + ( + Capability::ResponseEgressAbort, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseEgressBackpressure, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseEgressCompletion, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseWriteDeadlines, + CapabilitySupport::BestEffort, + ), + ( + Capability::BufferedResponseContentLength, + CapabilitySupport::Native, + ), + (Capability::OutboundHttp, CapabilitySupport::Native), + ( + Capability::OutboundRedirectFollowing, + CapabilitySupport::Native, + ), + ( + Capability::OutboundCompleteResourceAccounting, + CapabilitySupport::Unsupported, + ), + ( + Capability::OutboundHeaderFidelity, + CapabilitySupport::Native, + ), + (Capability::OutboundDeadlines, CapabilitySupport::Native), + ( + Capability::OutboundFailureDiagnostics, + CapabilitySupport::BestEffort, + ), + ( + Capability::OutboundFlexiblePhaseBudget, + CapabilitySupport::Native, + ), + ( + Capability::OutboundAuthorityOverride, + CapabilitySupport::Native, + ), + ( + Capability::OutboundBatchCancellation, + CapabilitySupport::Native, + ), + ( + Capability::OutboundBatchCompletionOrder, + CapabilitySupport::Native, + ), + ( + Capability::OutboundBatchSlotIsolation, + CapabilitySupport::Native, + ), + (Capability::OutboundCacheBypass, CapabilitySupport::Native), + ( + Capability::OutboundImageOptimizer, + CapabilitySupport::Unsupported, + ), + ( + Capability::StreamedUploadDeadlines, + CapabilitySupport::Native, + ), + ( + Capability::LazyStreamedResponsePassthrough, + CapabilitySupport::Native, + ), + ] { + assert_eq!( + AXUM_ADAPTER.capability(capability), + support, + "{capability:?}" + ); + } + } + + #[test] + fn adapter_platform_metadata_matches_runtime_metadata() { + let metadata = AXUM_ADAPTER.platform_metadata(); + + assert_eq!(metadata, crate::AXUM_PLATFORM); + assert_eq!( + metadata.memory_ceiling(), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::OperatorConfigured + ) + ); + assert_eq!( + metadata.inbound_request_population_bound(), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::OperatorConfigured + ) + ); + assert_eq!( + metadata.host_ingress_memory_accounting(), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::OperatorConfigured + ) + ); + } + #[test] fn read_axum_project_loads_defaults() { let dir = tempdir().unwrap(); @@ -1338,7 +1599,7 @@ mod tests { fs::create_dir_all(&local_dir).expect("create dir"); fs::write( local_dir.join("local-config-app_config.json"), - r#"{"other_key": "value"}"#, + serde_json::json!({"other_key": "value"}).to_string(), ) .expect("write"); let result = AxumCliAdapter @@ -1364,7 +1625,7 @@ mod tests { fs::create_dir_all(&local_dir).expect("create dir"); fs::write( local_dir.join("local-config-app_config.json"), - r#"{"greeting": "hello-axum"}"#, + serde_json::json!({"greeting": "hello-axum"}).to_string(), ) .expect("write"); let result = AxumCliAdapter @@ -1392,7 +1653,7 @@ mod tests { fs::create_dir_all(&local_dir).expect("create dir"); fs::write( local_dir.join("local-config-app_config.json"), - r#"{"greeting": "hello-axum"}"#, + serde_json::json!({"greeting": "hello-axum"}).to_string(), ) .expect("write"); let store = ResolvedStoreId::from_logical("app_config"); diff --git a/crates/edgezero-adapter-axum/src/config_snapshot.rs b/crates/edgezero-adapter-axum/src/config_snapshot.rs new file mode 100644 index 000000000..dbd91e800 --- /dev/null +++ b/crates/edgezero-adapter-axum/src/config_snapshot.rs @@ -0,0 +1,363 @@ +//! Flat-string snapshots: no intermediate JSON tree or per-read payload copies. + +use std::fmt; +use std::io::Read; +use std::mem::size_of; + +use edgezero_core::ConfigValue; +use edgezero_core::config_store::ConfigStoreError; +use serde::Deserializer; +use serde::de::{self, DeserializeSeed, Error as _, MapAccess, Visitor}; + +use crate::config_store_limits::ConfigStoreLimits; + +pub(crate) struct Snapshot { + entries: Vec<(String, ConfigValue)>, + resident_bytes: usize, +} + +struct SnapshotSeed<'snapshot> { + existing: usize, + failure: &'snapshot mut Option, + limits: ConfigStoreLimits, + snapshot: &'snapshot mut Snapshot, +} + +struct StringSeed<'failure> { + failure: &'failure mut Option, + max_bytes: usize, +} + +impl Snapshot { + pub(crate) fn empty( + limits: ConfigStoreLimits, + existing: usize, + ) -> Result { + let index_bytes = limits + .max_entries() + .checked_mul(size_of::<(String, ConfigValue)>()) + .ok_or(ConfigStoreError::ValueTooLarge)?; + check_charge(limits, existing, index_bytes)?; + let mut entries = Vec::new(); + entries + .try_reserve_exact(limits.max_entries()) + .map_err(|_allocation| { + ConfigStoreError::internal(anyhow::anyhow!("config snapshot allocation failed")) + })?; + Ok(Self { + resident_bytes: entries + .capacity() + .checked_mul(size_of::<(String, ConfigValue)>()) + .ok_or(ConfigStoreError::ValueTooLarge)?, + entries, + }) + } + + fn finish(&mut self) -> Result<(), ConfigStoreError> { + self.entries + .sort_unstable_by(|(left, _value), (right, _other)| left.cmp(right)); + if self.entries.windows(2).any(|pair| { + pair.first() + .zip(pair.get(1)) + .is_some_and(|(left, right)| left.0 == right.0) + }) { + return Err(ConfigStoreError::unavailable( + "duplicate config snapshot key", + )); + } + Ok(()) + } + + pub(crate) fn from_entries( + entries: E, + limits: ConfigStoreLimits, + ) -> Result + where + E: IntoIterator, + { + let mut snapshot = Self::empty(limits, 0)?; + for (key, value) in entries { + snapshot.insert(&key, &value, limits, 0)?; + } + snapshot.finish()?; + Ok(snapshot) + } + + pub(crate) fn get(&self, key: &str) -> Option<&ConfigValue> { + self.entries + .binary_search_by(|(stored, _value)| stored.as_str().cmp(key)) + .ok() + .and_then(|index| self.entries.get(index)) + .map(|(_key, value)| value) + } + + fn insert( + &mut self, + key: &str, + value: &str, + limits: ConfigStoreLimits, + existing: usize, + ) -> Result<(), ConfigStoreError> { + if self.entries.len() >= limits.max_entries() + || key.len() > limits.max_key_bytes() + || value.len() > limits.max_value_bytes() + { + return Err(ConfigStoreError::ValueTooLarge); + } + // The Vec index was reserved already. Strings have exact requested size; + // Arc has two reference counts in addition to the UTF-8 payload. + let charge = self + .resident_bytes + .checked_add(key.len()) + .and_then(|bytes| bytes.checked_add(value.len())) + .and_then(|bytes| { + bytes.checked_add(size_of::().saturating_mul(3).saturating_sub(1)) + }) + .ok_or(ConfigStoreError::ValueTooLarge)?; + check_charge(limits, existing, charge)?; + self.entries + .push((key.to_owned(), ConfigValue::from(value))); + self.resident_bytes = charge; + Ok(()) + } + + pub(crate) fn load( + reader: &mut impl Read, + limits: ConfigStoreLimits, + existing: usize, + ) -> Result { + let mut snapshot = Self::empty(limits, existing)?; + let raw = read_capped(reader, limits.max_file_bytes())?; + // Reject non-object roots before Serde can format their contents into + // an expanded type-error diagnostic outside the decoder scratch budget. + if raw + .iter() + .copied() + .find(|byte| !matches!(byte, b' ' | b'\n' | b'\r' | b'\t')) + != Some(b'{') + { + return Err(ConfigStoreError::unavailable( + "config snapshot requires an object", + )); + } + let mut decoder = serde_json::Deserializer::from_slice(&raw); + let mut failure = None; + let result = SnapshotSeed { + snapshot: &mut snapshot, + limits, + existing, + failure: &mut failure, + } + .deserialize(&mut decoder) + .and_then(|()| decoder.end()); + if result.is_err() { + return Err( + failure.unwrap_or_else(|| ConfigStoreError::unavailable("invalid config snapshot")) + ); + } + snapshot.finish()?; + Ok(snapshot) + } + + pub(crate) const fn resident_bytes(&self) -> usize { + self.resident_bytes + } +} + +impl<'de> DeserializeSeed<'de> for SnapshotSeed<'_> { + type Value = (); + fn deserialize(self, deserializer: D) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(self) + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "all non-map visitor methods intentionally retain Serde's type rejection" +)] +impl<'de> Visitor<'de> for SnapshotSeed<'_> { + type Value = (); + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a flat string map") + } + fn visit_map(self, mut map: A) -> Result<(), A::Error> + where + A: MapAccess<'de>, + { + while let Some(key) = map.next_key_seed(StringSeed { + max_bytes: self.limits.max_key_bytes(), + failure: self.failure, + })? { + if self.snapshot.entries.len() >= self.limits.max_entries() { + *self.failure = Some(ConfigStoreError::ValueTooLarge); + return Err(A::Error::custom("snapshot limit")); + } + let value = map.next_value_seed(StringSeed { + max_bytes: self.limits.max_value_bytes(), + failure: self.failure, + })?; + if let Err(error) = self + .snapshot + .insert(&key, &value, self.limits, self.existing) + { + *self.failure = Some(error); + return Err(A::Error::custom("snapshot limit")); + } + } + Ok(()) + } +} + +impl<'de> DeserializeSeed<'de> for StringSeed<'_> { + type Value = String; + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(self) + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "Serde's borrowed string method forwards to the bounded visit_str; non-strings are rejected by default" +)] +impl Visitor<'_> for StringSeed<'_> { + type Value = String; + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a bounded UTF-8 string") + } + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if v.len() > self.max_bytes { + *self.failure = Some(ConfigStoreError::ValueTooLarge); + return Err(E::custom("snapshot string limit")); + } + Ok(v.to_owned()) + } +} + +fn check_charge( + limits: ConfigStoreLimits, + existing: usize, + additional: usize, +) -> Result<(), ConfigStoreError> { + let resident = existing + .checked_add(additional) + .ok_or(ConfigStoreError::ValueTooLarge)?; + if resident > limits.max_resident_bytes() + || resident.saturating_add(limits.staging_bytes()) > limits.max_startup_bytes() + { + return Err(ConfigStoreError::ValueTooLarge); + } + Ok(()) +} + +fn read_capped(reader: &mut impl Read, max_bytes: usize) -> Result, ConfigStoreError> { + let mut raw = Vec::new(); + let mut chunk = [0_u8; 4096]; + loop { + let remaining = max_bytes.saturating_sub(raw.len()); + let read_len = chunk.len().min(remaining.saturating_add(1)); + let read = reader + .read( + chunk + .get_mut(..read_len) + .ok_or(ConfigStoreError::ValueTooLarge)?, + ) + .map_err(|_io| ConfigStoreError::unavailable("config snapshot read failed"))?; + if read == 0 { + return Ok(raw); + } + if read > remaining { + return Err(ConfigStoreError::ValueTooLarge); + } + raw.try_reserve_exact(read).map_err(|_allocation| { + ConfigStoreError::internal(anyhow::anyhow!("config snapshot allocation failed")) + })?; + raw.extend_from_slice(chunk.get(..read).ok_or(ConfigStoreError::ValueTooLarge)?); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + fn limits(file: usize, entries: usize, key: usize, value: usize) -> ConfigStoreLimits { + ConfigStoreLimits::new(file, entries, key, value, 8192, 0x8000).expect("limits") + } + + #[test] + fn file_growth_is_capped_by_reads_not_metadata() { + let mut reader = Cursor::new(vec![b' '; 9]); + assert!(matches!( + read_capped(&mut reader, 8), + Err(ConfigStoreError::ValueTooLarge) + )); + assert_eq!(reader.position(), 9); + assert_eq!( + read_capped(&mut Cursor::new(vec![b' '; 8]), 8).expect("exact"), + vec![b' '; 8] + ); + } + + #[test] + fn malformed_root_strings_are_rejected_before_diagnostic_expansion() { + let raw = serde_json::to_vec(&serde_json::json!("\u{0001}".repeat(8192))).expect("fixture"); + let policy = ConfigStoreLimits::new( + raw.len(), + 1, + 1, + 1, + 4096, + raw.len().saturating_mul(5).saturating_add(4096), + ) + .expect("policy"); + let error = Snapshot::load(&mut raw.as_slice(), policy, 0) + .err() + .expect("non-object"); + assert_eq!( + error.to_string(), + ConfigStoreError::unavailable("config snapshot requires an object").to_string() + ); + } + + #[test] + fn escaped_values_and_entry_limits_are_enforced() { + let raw = serde_json::to_vec(&serde_json::json!({"a": "\n\n\n"})).expect("fixture"); + let _snapshot = Snapshot::load(&mut raw.as_slice(), limits(128, 1, 1, 3), 0) + .expect("exact decoded cap"); + assert!(matches!( + Snapshot::load(&mut raw.as_slice(), limits(128, 1, 1, 2), 0), + Err(ConfigStoreError::ValueTooLarge) + )); + let many = serde_json::to_vec(&serde_json::json!({"a": "a", "b": "b"})).expect("fixture"); + assert!(matches!( + Snapshot::load(&mut many.as_slice(), limits(128, 1, 1, 3), 0), + Err(ConfigStoreError::ValueTooLarge) + )); + } + + #[test] + fn all_store_residency_is_reserved_before_loading() { + let limits = limits(128, 1, 1, 3); + let raw = serde_json::to_vec(&serde_json::json!({"a": "abc"})).expect("fixture"); + let first = Snapshot::load(&mut raw.as_slice(), limits, 0).expect("snapshot"); + assert!(matches!( + Snapshot::load( + &mut raw.as_slice(), + limits, + limits.max_resident_bytes() - first.resident_bytes() + 1 + ), + Err(ConfigStoreError::ValueTooLarge) + )); + let _recovered = Snapshot::load(&mut raw.as_slice(), limits, 0) + .expect("failed load has no retained charge"); + } +} diff --git a/crates/edgezero-adapter-axum/src/config_store.rs b/crates/edgezero-adapter-axum/src/config_store.rs index 19b7ddfd7..41e7e9d08 100644 --- a/crates/edgezero-adapter-axum/src/config_store.rs +++ b/crates/edgezero-adapter-axum/src/config_store.rs @@ -12,31 +12,33 @@ //! every key). This keeps `edgezero serve --adapter axum` permissive when //! the project hasn't seeded any local config yet. -use std::collections::HashMap; use std::env; use std::fs; use std::io::ErrorKind; +#[cfg(unix)] +use std::os::unix::fs::OpenOptionsExt as _; use std::path::{Path, PathBuf}; +#[cfg(test)] +use std::sync::atomic::{AtomicUsize, Ordering}; use async_trait::async_trait; use edgezero_core::config_store::{ConfigStore, ConfigStoreError}; +use edgezero_core::{BoundedStoreRead, Deadline, MonotonicClock}; + +use crate::config_snapshot::Snapshot; +use crate::config_store_limits::ConfigStoreLimits; /// Local-file config store used by the Axum dev server. /// -/// Construction is fallible only when the backing file is present but -/// malformed JSON — a missing file is a documented "no values seeded yet" -/// state, not an error. +/// Construction validates finite allocation budgets and the flat-string shape. +/// A missing file is an empty declared snapshot, not an absent binding. pub struct AxumConfigStore { - data: HashMap, + data: Snapshot, + #[cfg(test)] + unbounded_get_calls: AtomicUsize, } impl AxumConfigStore { - fn empty() -> Self { - Self { - data: HashMap::new(), - } - } - /// Open the local-file config store for a given logical id. /// /// Reads `.edgezero/local-config-.json` if present and parses it @@ -47,22 +49,29 @@ impl AxumConfigStore { /// /// # Errors /// Returns [`ConfigStoreError::Unavailable`] when the backing file - /// exists but cannot be read or parsed. + /// exists but cannot be read or parsed, and [`ConfigStoreError::ValueTooLarge`] + /// for file, entry, string or allocation-budget violations. #[inline] - pub fn from_local_file(id: &str) -> Result { - Self::from_path(&Self::local_path(id)) + pub fn from_local_file(id: &str, limits: ConfigStoreLimits) -> Result { + Self::from_path(&Self::local_path(id), limits) } /// Build a store from an explicit `{key -> value}` map. Intended for /// tests and for callers that already have parsed config in memory. + /// Caller-owned input allocations are outside the snapshot's allocation charge. + /// + /// # Errors + /// Returns a typed size error for allocation limits, or unavailable for duplicates. #[inline] - pub fn from_map(entries: E) -> Self + pub fn from_map(entries: E, limits: ConfigStoreLimits) -> Result where E: IntoIterator, { - Self { - data: entries.into_iter().collect(), - } + Ok(Self { + data: Snapshot::from_entries(entries, limits)?, + #[cfg(test)] + unbounded_get_calls: AtomicUsize::new(0), + }) } /// Open the local-file config store at an explicit path @@ -94,28 +103,50 @@ impl AxumConfigStore { /// /// # Errors /// Returns [`ConfigStoreError::Unavailable`] when the file - /// exists but cannot be read or parsed. + /// exists but cannot be read or parsed, and [`ConfigStoreError::ValueTooLarge`] + /// for file, entry, string or allocation-budget violations. #[inline] - pub fn from_path(path: &Path) -> Result { - let raw = match fs::read_to_string(path) { - Ok(raw) => raw, - Err(err) if err.kind() == ErrorKind::NotFound => { - return Ok(Self::empty()); + pub fn from_path(path: &Path, limits: ConfigStoreLimits) -> Result { + Self::load_at_startup(path, limits, 0) + } + + pub(crate) fn load_at_startup( + path: &Path, + limits: ConfigStoreLimits, + existing: usize, + ) -> Result { + let mut options = fs::OpenOptions::new(); + options.read(true); + #[cfg(unix)] + options.custom_flags(libc::O_NONBLOCK); + let data = match options.open(path) { + Ok(mut file) => { + let metadata = file.metadata().map_err(|_io| { + ConfigStoreError::unavailable("config snapshot metadata unavailable") + })?; + if !metadata.is_file() { + return Err(ConfigStoreError::unavailable( + "config snapshot requires a regular file", + )); + } + if metadata.len() + > u64::try_from(limits.max_file_bytes()) + .map_err(|_size| ConfigStoreError::ValueTooLarge)? + { + return Err(ConfigStoreError::ValueTooLarge); + } + Snapshot::load(&mut file, limits, existing)? } - Err(err) => { - return Err(ConfigStoreError::unavailable(format!( - "failed to read {}: {err}", - path.display() - ))); + Err(err) if err.kind() == ErrorKind::NotFound => Snapshot::empty(limits, existing)?, + Err(_io) => { + return Err(ConfigStoreError::unavailable("config snapshot open failed")); } }; - let data: HashMap = serde_json::from_str(&raw).map_err(|err| { - ConfigStoreError::unavailable(format!( - "{} is not a flat string -> string JSON object: {err}", - path.display() - )) - })?; - Ok(Self { data }) + Ok(Self { + data, + #[cfg(test)] + unbounded_get_calls: AtomicUsize::new(0), + }) } /// Resolve the on-disk path for the given logical config id. @@ -152,14 +183,60 @@ impl AxumConfigStore { } suffix } + + /// Charged snapshot index, key and shared-payload requested allocation bytes. + #[inline] + #[must_use] + pub const fn resident_allocation_bytes(&self) -> usize { + self.data.resident_bytes() + } } #[async_trait(?Send)] impl ConfigStore for AxumConfigStore { #[inline] - async fn get(&self, key: &str) -> Result, ConfigStoreError> { + async fn get(&self, key: &str) -> Result, ConfigStoreError> { + #[cfg(test)] + self.unbounded_get_calls.fetch_add(1, Ordering::Relaxed); Ok(self.data.get(key).cloned()) } + + #[inline] + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + + // Payloads are already bounded resident snapshots; reads clone only Arc. + let stored_value = self.data.get(key); + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + + let backend_bytes = stored_value.map_or(Ok(0_u64), |value| { + u64::try_from(value.len()).map_err(|_length_error| ConfigStoreError::ValueTooLarge) + })?; + if backend_bytes > max_backend_bytes || backend_bytes > max_value_bytes { + return Err(ConfigStoreError::ValueTooLarge); + } + + let value = stored_value.cloned(); + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + + Ok(BoundedStoreRead { + backend_bytes, + value, + }) + } } /// Walk up from the process cwd looking for an ancestor that @@ -192,22 +269,206 @@ fn find_project_root_dir_from(start: &Path) -> Option { mod tests { // Run the shared contract tests against AxumConfigStore. edgezero_core::config_store_contract_tests!(axum_config_store_contract, { - AxumConfigStore::from_map([ - ("contract.key.a".to_owned(), "value_a".to_owned()), - ("contract.key.b".to_owned(), "value_b".to_owned()), - ]) + AxumConfigStore::from_map( + [ + ("contract.key.a".to_owned(), "value_a".to_owned()), + ("contract.key.b".to_owned(), "value_b".to_owned()), + ], + ConfigStoreLimits::default(), + ) + .expect("snapshot") }); use super::*; + use edgezero_core::{Deadline, MonotonicClock, MonotonicInstant}; use futures::executor::block_on; + use std::sync::atomic::Ordering; + use std::time::Duration; use tempfile::tempdir; + #[test] + fn bounded_get_shares_payload_across_reads() { + let store = AxumConfigStore::from_map( + [("key".to_owned(), "payload".to_owned())], + ConfigStoreLimits::default(), + ) + .expect("snapshot"); + let clock = MonotonicClock::default(); + let deadline = Deadline::after(Duration::from_secs(1)); + let first = block_on(store.get_bounded("key", &clock, deadline, 7, 7)) + .expect("first read") + .value + .expect("first value"); + let second = block_on(store.get_bounded("key", &clock, deadline, 7, 7)) + .expect("second read") + .value + .expect("second value"); + assert_eq!( + first.as_ptr(), + second.as_ptr(), + "reads must share the snapshot payload" + ); + } + + #[test] + fn startup_rejects_keys_over_the_default_allocation_limit() { + let temp = tempdir().expect("tempdir"); + let path = temp.path().join("config.json"); + let key = "k".repeat(1025); + fs::write(&path, serde_json::json!({key: "value"}).to_string()).expect("write"); + assert!(matches!( + AxumConfigStore::from_path(&path, ConfigStoreLimits::default()), + Err(ConfigStoreError::ValueTooLarge) + )); + } + + #[test] + fn startup_rejects_duplicate_keys_instead_of_overwriting() { + let temp = tempdir().expect("tempdir"); + let path = temp.path().join("config.json"); + fs::write(&path, r#"{"key":"first","key":"second"}"#).expect("write duplicate-key fixture"); + assert!(matches!( + AxumConfigStore::from_path(&path, ConfigStoreLimits::default()), + Err(ConfigStoreError::Unavailable { .. }) + )); + } + + #[cfg(unix)] + #[test] + fn named_pipe_is_rejected_without_waiting_for_a_writer() { + use std::process::Command; + use std::sync::mpsc::{self, RecvTimeoutError}; + use std::thread; + let directory = tempdir().expect("directory"); + let path = directory.path().join("config.fifo"); + assert!( + Command::new("mkfifo") + .arg(&path) + .status() + .expect("mkfifo") + .success() + ); + let reader_path = path.clone(); + let (sender, receiver) = mpsc::channel(); + let loader = thread::spawn(move || { + sender + .send(AxumConfigStore::from_path(&reader_path, ConfigStoreLimits::default()).err()) + .expect("send"); + }); + let result = receiver.recv_timeout(Duration::from_secs(2)); + if matches!(result, Err(RecvTimeoutError::Timeout)) { + // Unblock a regressed blocking open before failing, so the test leaks no thread. + drop( + fs::OpenOptions::new() + .write(true) + .open(&path) + .expect("unblock reader"), + ); + } + loader.join().expect("loader"); + assert!(matches!( + result.expect("nonblocking rejection"), + Some(ConfigStoreError::Unavailable { .. }) + )); + } + + #[test] + fn bounded_get_accepts_exact_caps_and_reports_exact_backend_bytes() { + let cs = AxumConfigStore::from_map( + [("greeting".to_owned(), "hello".to_owned())], + ConfigStoreLimits::default(), + ) + .expect("snapshot"); + let clock = MonotonicClock::default(); + + let read = block_on(cs.get_bounded( + "greeting", + &clock, + Deadline::after(Duration::from_secs(1)), + 5, + 5, + )) + .expect("exact cap must succeed"); + + assert_eq!(read.backend_bytes, 5); + assert_eq!(read.value.as_deref(), Some("hello")); + } + + #[test] + fn bounded_get_rejects_either_cap_without_using_cloning_get() { + for (max_backend_bytes, max_value_bytes) in [(4, 5), (5, 4)] { + let cs = AxumConfigStore::from_map( + [("greeting".to_owned(), "hello".to_owned())], + ConfigStoreLimits::default(), + ) + .expect("snapshot"); + let clock = MonotonicClock::default(); + + let error = block_on(cs.get_bounded( + "greeting", + &clock, + Deadline::after(Duration::from_secs(1)), + max_backend_bytes, + max_value_bytes, + )) + .expect_err("over-cap value must fail"); + + assert!(matches!(error, ConfigStoreError::ValueTooLarge)); + assert_eq!(cs.unbounded_get_calls.load(Ordering::Relaxed), 0); + } + } + + #[test] + fn bounded_get_rejects_an_expired_deadline_before_reading() { + let cs = AxumConfigStore::from_map( + [("greeting".to_owned(), "hello".to_owned())], + ConfigStoreLimits::default(), + ) + .expect("snapshot"); + let expired = Deadline::at_instant(MonotonicInstant::now()); + let clock = MonotonicClock::default(); + + let error = block_on(cs.get_bounded("greeting", &clock, expired, 5, 5)) + .expect_err("expired deadline must fail"); + + assert!(matches!(error, ConfigStoreError::DeadlineExceeded)); + assert_eq!(cs.unbounded_get_calls.load(Ordering::Relaxed), 0); + } + + #[test] + fn bounded_config_read_uses_injected_clock() { + let cs = AxumConfigStore::from_map( + [("greeting".to_owned(), "hello".to_owned())], + ConfigStoreLimits::default(), + ) + .expect("snapshot"); + let process_now = MonotonicInstant::now(); + let injected_now = process_now + .checked_sub(Duration::from_mins(1)) + .expect("injected instant"); + let clock = MonotonicClock::new(move || injected_now); + let deadline = Deadline::at_instant( + injected_now + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), + ); + + let read = block_on(cs.get_bounded("greeting", &clock, deadline, 5, 5)) + .expect("the application clock is still before its deadline"); + + assert_eq!(read.value.as_deref(), Some("hello")); + } + #[test] fn axum_config_store_from_map_returns_values() { - let cs = AxumConfigStore::from_map([("greeting".to_owned(), "hello".to_owned())]); + let cs = AxumConfigStore::from_map( + [("greeting".to_owned(), "hello".to_owned())], + ConfigStoreLimits::default(), + ) + .expect("snapshot"); assert_eq!( block_on(cs.get("greeting")).expect("config value"), - Some("hello".to_owned()) + Some(edgezero_core::ConfigValue::from("hello")) ); assert_eq!(block_on(cs.get("missing")).expect("missing config"), None); } @@ -274,8 +535,11 @@ mod tests { #[test] fn axum_config_store_from_path_returns_empty_for_missing_file() { let temp = tempdir().expect("tempdir"); - let cs = AxumConfigStore::from_path(&temp.path().join("nope.json")) - .expect("missing file is permissive"); + let cs = AxumConfigStore::from_path( + &temp.path().join("nope.json"), + ConfigStoreLimits::default(), + ) + .expect("missing file is permissive"); assert_eq!(block_on(cs.get("anything")).expect("empty store"), None); } @@ -285,18 +549,20 @@ mod tests { let path = temp.path().join("local-config-app_config.json"); fs::write( &path, - r#"{"greeting":"hello from file","feature.new_checkout":"false"}"#, + serde_json::json!({"greeting": "hello from file", "feature.new_checkout": "false"}) + .to_string(), ) .expect("write json"); - let cs = AxumConfigStore::from_path(&path).expect("parse json"); + let cs = + AxumConfigStore::from_path(&path, ConfigStoreLimits::default()).expect("parse json"); assert_eq!( block_on(cs.get("greeting")).expect("value"), - Some("hello from file".to_owned()) + Some(edgezero_core::ConfigValue::from("hello from file")) ); assert_eq!( block_on(cs.get("feature.new_checkout")).expect("dotted value"), - Some("false".to_owned()) + Some(edgezero_core::ConfigValue::from("false")) ); assert_eq!(block_on(cs.get("missing")).expect("missing"), None); } @@ -307,7 +573,7 @@ mod tests { let path = temp.path().join("local-config-bad.json"); fs::write(&path, "{not json}").expect("write"); - match AxumConfigStore::from_path(&path) { + match AxumConfigStore::from_path(&path, ConfigStoreLimits::default()) { Err(ConfigStoreError::Unavailable { .. }) => {} Err(other) => panic!("expected Unavailable, got {other:?}"), Ok(_) => panic!("malformed JSON must surface as error"), @@ -318,9 +584,9 @@ mod tests { fn axum_config_store_from_path_rejects_non_string_values() { let temp = tempdir().expect("tempdir"); let path = temp.path().join("local-config-numeric.json"); - fs::write(&path, r#"{"greeting":42}"#).expect("write"); + fs::write(&path, serde_json::json!({"greeting": 42_u32}).to_string()).expect("write"); - match AxumConfigStore::from_path(&path) { + match AxumConfigStore::from_path(&path, ConfigStoreLimits::default()) { Err(ConfigStoreError::Unavailable { .. }) => {} Err(other) => panic!("expected Unavailable, got {other:?}"), Ok(_) => panic!("non-string values must surface as error"), diff --git a/crates/edgezero-adapter-axum/src/config_store_limits.rs b/crates/edgezero-adapter-axum/src/config_store_limits.rs new file mode 100644 index 000000000..4aa1840f7 --- /dev/null +++ b/crates/edgezero-adapter-axum/src/config_store_limits.rs @@ -0,0 +1,230 @@ +//! Validated allocation budgets for immutable local configuration snapshots. + +use edgezero_core::env_config::EnvConfig; + +/// Limits shared by every local config store loaded during one server startup. +/// +/// Resident and startup allowances are requested allocation charges, not RSS. +/// Startup reserves five file budgets for input, decoder growth and temporary strings. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ConfigStoreLimits { + entries: usize, + file_bytes: usize, + key_bytes: usize, + resident_bytes: usize, + startup_bytes: usize, + value_bytes: usize, +} + +/// A rejected setting. The supplied value is never included in diagnostics. +#[derive(Debug, thiserror::Error)] +#[error("invalid Axum config-store setting `{field}`")] +pub struct ConfigStoreLimitsError { + field: &'static str, +} + +impl Default for ConfigStoreLimits { + #[inline] + fn default() -> Self { + Self { + entries: 1024, + file_bytes: 0x0100_0000, + key_bytes: 1024, + resident_bytes: 0x0200_0000, + startup_bytes: 0x0700_0000, + value_bytes: 0x0080_0000, + } + } +} + +impl ConfigStoreLimits { + /// Resolve `EDGEZERO__ADAPTER__CONFIG_STORE__*`. Missing settings alone use defaults. + /// + /// # Errors + /// Rejects malformed, zero, out-of-range or inconsistent explicit settings. + #[inline] + pub fn from_env(env: &EnvConfig) -> Result { + let defaults = Self::default(); + Self::new( + setting(env, "max_file_bytes", defaults.file_bytes)?, + setting(env, "max_entries", defaults.entries)?, + setting(env, "max_key_bytes", defaults.key_bytes)?, + setting(env, "max_value_bytes", defaults.value_bytes)?, + setting(env, "max_resident_bytes", defaults.resident_bytes)?, + setting(env, "max_startup_bytes", defaults.startup_bytes)?, + ) + } + + /// Maximum entries in any one snapshot, including duplicate-key attempts. + #[inline] + #[must_use] + pub const fn max_entries(self) -> usize { + self.entries + } + + /// Maximum wire bytes read from a single opened local file. + #[inline] + #[must_use] + pub const fn max_file_bytes(self) -> usize { + self.file_bytes + } + + /// Maximum decoded UTF-8 bytes per key. + #[inline] + #[must_use] + pub const fn max_key_bytes(self) -> usize { + self.key_bytes + } + + /// Maximum charged residency across the entire startup registry. + #[inline] + #[must_use] + pub const fn max_resident_bytes(self) -> usize { + self.resident_bytes + } + + /// Maximum charged residency plus staging during sequential startup loading. + #[inline] + #[must_use] + pub const fn max_startup_bytes(self) -> usize { + self.startup_bytes + } + + /// Maximum decoded UTF-8 bytes per value, before creating its shared allocation. + #[inline] + #[must_use] + pub const fn max_value_bytes(self) -> usize { + self.value_bytes + } + + /// Construct finite limits. File/value/resident limits support up to 256 MiB; + /// startup up to 1 GiB, keys up to 64 KiB and entries up to 65536. + /// + /// # Errors + /// Rejects invalid ranges, keys/values larger than the wire budget, or a startup + /// budget smaller than the worst-case input/decoder staging reservation. + #[inline] + pub fn new( + max_file_bytes: usize, + max_entries: usize, + max_key_bytes: usize, + max_value_bytes: usize, + max_resident_bytes: usize, + max_startup_bytes: usize, + ) -> Result { + for (valid, field) in [ + ( + (1..=0x1000_0000).contains(&max_file_bytes), + "max_file_bytes", + ), + ((1..=0x0001_0000).contains(&max_entries), "max_entries"), + ( + (1..=0x0001_0000).contains(&max_key_bytes) && max_key_bytes <= max_file_bytes, + "max_key_bytes", + ), + ( + (1..=0x1000_0000).contains(&max_value_bytes) && max_value_bytes <= max_file_bytes, + "max_value_bytes", + ), + ( + (1..=0x1000_0000).contains(&max_resident_bytes), + "max_resident_bytes", + ), + ( + (1..=0x4000_0000).contains(&max_startup_bytes) + && max_startup_bytes >= max_file_bytes.max(32).saturating_mul(5), + "max_startup_bytes", + ), + ] { + if !valid { + return Err(ConfigStoreLimitsError { field }); + } + } + Ok(Self { + entries: max_entries, + file_bytes: max_file_bytes, + key_bytes: max_key_bytes, + resident_bytes: max_resident_bytes, + startup_bytes: max_startup_bytes, + value_bytes: max_value_bytes, + }) + } + + /// Reserved requested input/decoder capacity while one file is loaded. + #[inline] + #[must_use] + pub const fn staging_bytes(self) -> usize { + let base = if self.file_bytes < 32 { + 32 + } else { + self.file_bytes + }; + base.saturating_mul(5) + } +} + +fn setting( + env: &EnvConfig, + field: &'static str, + default: usize, +) -> Result { + let Some(value) = env.get(&["adapter", "config_store", field]) else { + return Ok(default); + }; + if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(ConfigStoreLimitsError { field }); + } + value + .parse() + .map_err(|_invalid_integer| ConfigStoreLimitsError { field }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn defaults_and_explicit_limits_are_validated() { + let defaults = ConfigStoreLimits::from_env(&EnvConfig::default()).expect("defaults"); + assert_eq!(defaults, ConfigStoreLimits::default()); + assert_eq!(defaults.staging_bytes(), 5 * defaults.max_file_bytes()); + let env = EnvConfig::from_vars([("EDGEZERO__ADAPTER__CONFIG_STORE__MAX_ENTRIES", "2")]); + assert_eq!( + ConfigStoreLimits::from_env(&env) + .expect("explicit") + .max_entries(), + 2 + ); + } + + #[test] + fn invalid_explicit_limits_never_fall_back_or_echo_values() { + for field in [ + "MAX_FILE_BYTES", + "MAX_ENTRIES", + "MAX_KEY_BYTES", + "MAX_VALUE_BYTES", + "MAX_RESIDENT_BYTES", + "MAX_STARTUP_BYTES", + ] { + for value in [ + "", + "0", + "-1", + "+2", + " 2", + "credential=private", + "18446744073709551616", + ] { + let key = format!("EDGEZERO__ADAPTER__CONFIG_STORE__{field}"); + let env = EnvConfig::from_vars([(key.as_str(), value)]); + let error = + ConfigStoreLimits::from_env(&env).expect_err("invalid explicit setting"); + assert!(!error.to_string().contains("credential=private")); + } + } + ConfigStoreLimits::new(10, 1, 1, 1, 10, 29).expect_err("invalid policy"); + ConfigStoreLimits::new(10, 1, 11, 1, 10, 30).expect_err("invalid policy"); + ConfigStoreLimits::new(10, 1, 1, 11, 10, 30).expect_err("invalid policy"); + } +} diff --git a/crates/edgezero-adapter-axum/src/connection.rs b/crates/edgezero-adapter-axum/src/connection.rs new file mode 100644 index 000000000..b729592ab --- /dev/null +++ b/crates/edgezero-adapter-axum/src/connection.rs @@ -0,0 +1,1290 @@ +use std::error::Error as StdError; +use std::future::{self, Future}; +use std::io; + +use http_body::Body as HttpBody; +use hyper::Request; +use hyper::body::Incoming; +use hyper::server::conn::http1::Builder as Http1Builder; +use hyper::service::Service; +use hyper_util::rt::TokioIo; +use tokio::net::TcpStream; +use tokio::time::{Instant as TokioInstant, sleep_until}; + +use crate::header_deadline::{HeaderDeadline, HeaderDeadlineExceeded, HeaderIo, HeaderService}; +use crate::ingress_config::AxumIngressConfig; +use crate::response::AxumBodyError; +use crate::response::EgressConnection; +use crate::service::AxumIngressAbort; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ConnectionExit { + AdmissionAborted, + Completed, + DeadlineExceeded, +} + +/// Owns HTTP/1 parsing, idle/header expiry and the existing response deadline race. +#[expect( + clippy::integer_division_remainder_used, + reason = "tokio::select! expands to internal randomized branch selection arithmetic" +)] +pub(crate) fn serve_http1( + stream: TcpStream, + service: ServiceType, + responses: EgressConnection, + limits: AxumIngressConfig, +) -> impl Future> +where + ServiceType: Service, Response = hyper::Response>, + ServiceType::Error: Into>, + ServiceType::Future: 'static, + ResponseBody: HttpBody + 'static, + ResponseBody::Error: Into>, +{ + // Stamp before scheduling the connection future, not on its first poll. + let head = HeaderDeadline::new(limits.head_read_timeout()); + async move { + let connection_future = Http1Builder::new() + .keep_alive(true) + .max_headers(limits.max_header_count()) + .max_header_size(limits.max_raw_head_bytes()) + .max_buf_size(limits.parser_buffer_bytes()) + .header_read_timeout(limits.head_read_timeout()) + .timer(head.clone()) + .serve_connection( + TokioIo::new(HeaderIo::new(stream, head.clone())), + HeaderService::new(service, head.clone(), limits, responses.clone()), + ); + let mut connection = Box::pin(connection_future); + + loop { + if head + .deadline() + .is_some_and(|deadline| TokioInstant::now() >= deadline) + { + responses.signal_elapsed_deadline(TokioInstant::now()); + responses.signal_transport_error(); + drop(connection); + return Ok(ConnectionExit::DeadlineExceeded); + } + let next_deadline = head + .deadline() + .into_iter() + .chain(responses.next_deadline()) + .min(); + let wait = async move { + if let Some(deadline) = next_deadline { + sleep_until(deadline).await; + } else { + future::pending::<()>().await; + } + }; + tokio::select! { + biased; + () = wait => { + if responses.signal_elapsed_deadline(TokioInstant::now()) { + drop(connection); + return Ok(ConnectionExit::DeadlineExceeded); + } + } + result = &mut connection => { + responses.signal_transport_error(); + drop(connection); + return classify_connection_result(result); + } + () = head.changed() => {} + () = responses.changed() => {} + } + } + } +} + +fn classify_connection_result( + result: Result<(), hyper::Error>, +) -> Result { + match result { + Ok(()) => Ok(ConnectionExit::Completed), + Err(error) if error_chain_has_admission_abort(&error) => { + Ok(ConnectionExit::AdmissionAborted) + } + Err(error) if error_chain_has_deadline(&error) => Ok(ConnectionExit::DeadlineExceeded), + Err(error) => Err(error), + } +} + +fn error_chain_has_admission_abort(error: &hyper::Error) -> bool { + let mut source = error.source(); + while let Some(current) = source { + if current.downcast_ref::().is_some() { + return true; + } + source = current.source(); + } + false +} + +fn error_chain_has_deadline(error: &hyper::Error) -> bool { + if error.is_timeout() { + return true; + } + let mut source = error.source(); + while let Some(current) = source { + if current + .downcast_ref::() + .is_some_and(|body_error| body_error.is_deadline()) + { + return true; + } + if current.downcast_ref::().is_some() { + return true; + } + if current + .downcast_ref::() + .and_then(io::Error::get_ref) + .is_some_and(::is::) + { + return true; + } + source = current.source(); + } + false +} + +#[cfg(test)] +mod bounded_tests { + use std::convert::Infallible; + use std::time::Duration; + + use axum::body::Body; + use futures::future::join; + use hyper::Response; + use hyper::service::service_fn; + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + use tokio::net::{TcpListener, TcpStream}; + use tokio::time::timeout; + + use super::serve_http1; + use crate::ingress_config::AxumIngressConfig; + use crate::response::EgressConnection; + + async fn raw_exchange(request: &[u8]) -> Vec { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let server = async move { + let (socket, _) = listener.accept().await.unwrap(); + let service = service_fn(|_request| async { + Ok::<_, Infallible>(Response::new(Body::from("ok"))) + }); + let _result = serve_http1( + socket, + service, + EgressConnection::default(), + AxumIngressConfig::default(), + ) + .await; + }; + let client = async { + let mut stream = TcpStream::connect(address).await.unwrap(); + stream.write_all(request).await.unwrap(); + let mut response = Vec::new(); + stream.read_to_end(&mut response).await.unwrap(); + response + }; + let ((), response) = timeout(Duration::from_secs(2), join(server, client)) + .await + .unwrap(); + response + } + + #[tokio::test] + async fn accepted_deadline_precedes_first_poll_but_not_admitted_handler_work() { + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use tokio::time::sleep; + + for delayed_poll in [true, false] { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let calls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&calls); + let server = async move { + let (socket, _) = listener.accept().await.unwrap(); + let service = service_fn(move |_request| { + let counted = Arc::clone(&observed); + async move { + counted.fetch_add(1, Ordering::SeqCst); + sleep(Duration::from_millis(200)).await; + Ok::<_, Infallible>(Response::new(Body::from("ok"))) + } + }); + let limits = + AxumIngressConfig::new(1, 8192, 100, Duration::from_millis(100)).unwrap(); + let connection = serve_http1(socket, service, EgressConnection::default(), limits); + if delayed_poll { + sleep(Duration::from_millis(150)).await; + } + let _result = connection.await; + }; + let client = async { + let mut stream = TcpStream::connect(address).await.unwrap(); + stream + .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .unwrap(); + let mut response = Vec::new(); + let _read = stream.read_to_end(&mut response).await; + response + }; + let ((), response) = timeout(Duration::from_secs(2), join(server, client)) + .await + .unwrap(); + if delayed_poll { + assert!(response.is_empty()); + assert_eq!(calls.load(Ordering::SeqCst), 0); + } else { + assert!(response.starts_with(b"HTTP/1.1 200")); + assert_eq!(calls.load(Ordering::SeqCst), 1); + } + } + } + + #[tokio::test] + async fn default_raw_head_limit_rejects_first_byte_over_before_service() { + let prefix = b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nX-Pad: "; + let mut request = prefix.to_vec(); + request.extend(vec![b'a'; 0x0001_0000 - prefix.len() - 4]); + request.extend_from_slice(b"\r\n\r\n"); + let response = raw_exchange(&request).await; + assert!(response.starts_with(b"HTTP/1.1 200")); + request.insert(prefix.len(), b'a'); + let rejected = raw_exchange(&request).await; + assert!(rejected.starts_with(b"HTTP/1.1 431"), "unexpected status"); + } +} + +#[cfg(test)] +mod tests { + use std::convert::Infallible; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::Poll; + use std::time::Duration; + + use axum::body::{Body as AxumBody, to_bytes}; + use bytes::Bytes; + use edgezero_core::app::App; + use edgezero_core::body::Body; + use edgezero_core::context::BodyKind; + use edgezero_core::error::EdgeError; + use edgezero_core::extractor::State; + use edgezero_core::http::Response as CoreResponse; + use edgezero_core::http::{HeaderMap, Method, StatusCode, Uri, Version, response_builder}; + use edgezero_core::ingress::{AdmissionDecision, IngressGrant}; + use edgezero_core::response_egress::{ + ResponseEgressAttempt, ResponseEgressCompletion, ResponseEgressHead, + ResponseEgressObserver, ResponseEgressObserverHandle, ResponseEgressOutcome, + ResponseEgressPolicy, ResponseEgressReport, + }; + use edgezero_core::router::{RouteMetadata, RouterService}; + use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; + use futures::future::join; + use futures_util::stream::{pending, poll_fn, repeat_with}; + use hyper::Request; + use hyper::Response; + use hyper::body::Incoming; + use hyper::server::conn::http1::Builder as Http1Builder; + use hyper::service::service_fn; + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + use tokio::net::{TcpListener, TcpStream}; + use tokio::task::{LocalSet, spawn_local}; + use tokio::time::timeout; + + use super::{ConnectionExit, serve_http1}; + use crate::ingress_config::AxumIngressConfig; + use crate::response::{AxumEgressBody, EgressConnection}; + use crate::service::AxumServiceState; + + use edgezero_core::context::RequestContext; + + #[derive(Clone, Copy)] + enum DeclaredTail { + Clean, + Empty, + EmptyForever, + Error, + Excess, + Pending, + } + + #[derive(Clone, Default)] + struct RecordingObserver(Arc>>); + + struct SourceDropProbe(Arc); + + impl ResponseEgressObserver for RecordingObserver { + fn complete(&self, report: &ResponseEgressReport) { + self.0.lock().expect("reports lock").push(report.clone()); + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for SourceDropProbe { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + fn declared_stream_app( + length: &'static str, + tail: DeclaredTail, + observer: &RecordingObserver, + polls: &Arc, + drops: &Arc, + ) -> App { + let source_polls = Arc::clone(polls); + let source_drops = Arc::clone(drops); + let router = RouterService::builder() + .get("/", move |_ctx| { + let observed_polls = Arc::clone(&source_polls); + let drop_probe = SourceDropProbe(Arc::clone(&source_drops)); + async move { + let stream = poll_fn(move |_context| { + let _keep_source_alive = &drop_probe; + let index = observed_polls.fetch_add(1, Ordering::SeqCst); + if index == 0 && length != "0" { + return Poll::Ready(Some(Ok(Bytes::from_static(b"abc")))); + } + match tail { + DeclaredTail::Empty if index <= 1 => { + Poll::Ready(Some(Ok(Bytes::new()))) + } + DeclaredTail::Clean | DeclaredTail::Empty => Poll::Ready(None), + DeclaredTail::EmptyForever => Poll::Ready(Some(Ok(Bytes::new()))), + DeclaredTail::Error => Poll::Ready(Some(Err(EdgeError::internal( + anyhow::anyhow!("late source error"), + )))), + DeclaredTail::Excess => { + Poll::Ready(Some(Ok(Bytes::from_static(b"extra")))) + } + DeclaredTail::Pending => Poll::Pending, + } + }); + let response = response_builder() + .header("content-length", length) + .body(Body::from_stream(stream)) + .expect("declared response"); + Ok::<_, EdgeError>(response) + } + }) + .build(); + let mut app = App::new(router); + app.set_response_egress_observer(observer.clone()); + let budget = if matches!(tail, DeclaredTail::Pending | DeclaredTail::EmptyForever) { + Duration::from_millis(100) + } else { + Duration::from_secs(5) + }; + app.set_response_egress_policy(move |_, started_at| ResponseEgressPolicy { + write_deadline: Deadline::at_instant(started_at.checked_add(budget).expect("deadline")), + }); + app + } + + async fn exchange_with_app(app: App) -> (String, Result) { + exchange_with_method(app, "GET").await + } + + async fn exchange_with_method( + app: App, + method: &str, + ) -> (String, Result) { + exchange_with_wire( + app, + format!("{method} / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"), + ) + .await + } + + async fn exchange_with_wire( + app: App, + request: String, + ) -> (String, Result) { + LocalSet::new() + .run_until(async { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind listener"); + let address = listener.local_addr().expect("listener address"); + let state = AxumServiceState::from_app(app); + let server = spawn_local(async move { + let (socket, peer) = listener.accept().await.expect("accept client"); + let responses = EgressConnection::default(); + let service = state.for_connection(peer, responses.clone()); + serve_http1(socket, service, responses, AxumIngressConfig::default()).await + }); + let mut client = TcpStream::connect(address).await.expect("connect client"); + client + .write_all(request.as_bytes()) + .await + .expect("write request"); + let mut wire = Vec::new(); + timeout(Duration::from_secs(2), client.read_to_end(&mut wire)) + .await + .expect("bounded response lifetime") + .expect("read response"); + let exit = server.await.expect("server task"); + (String::from_utf8(wire).expect("ASCII response"), exit) + }) + .await + } + + #[edgezero_core::action] + async fn forward_inbound( + ctx: RequestContext, + State(target): State, + ) -> Result { + assert_eq!(ctx.body_kind(), BodyKind::Initial); + let client = ctx.http_client().expect("injected outbound client"); + let request = edgezero_core::OutboundRequest::from_request(ctx.into_request()?, target)? + .timeout(Duration::from_secs(1)); + client.send(request).await?.into_response() + } + + async fn record_forwarded_request( + listener: TcpListener, + ) -> (bool, Option<(Method, HeaderMap, Bytes)>) { + use hyper_util::rt::TokioIo; + + let Ok(Ok((socket, _peer))) = timeout(Duration::from_millis(500), listener.accept()).await + else { + return (false, None); + }; + let receipt = Arc::new(Mutex::new(None)); + let recorded = Arc::clone(&receipt); + let service = service_fn(move |request: Request| { + let request_receipt = Arc::clone(&recorded); + async move { + let (parts, body) = request.into_parts(); + let bytes = to_bytes(AxumBody::new(body), 16) + .await + .expect("bounded upstream body"); + *request_receipt.lock().expect("receipt") = + Some((parts.method, parts.headers, bytes)); + Ok::<_, Infallible>( + Response::builder() + .status(StatusCode::NO_CONTENT) + .header("connection", "close") + .body(AxumBody::empty()) + .expect("origin response"), + ) + } + }); + Http1Builder::new() + .serve_connection(TokioIo::new(socket), service) + .await + .expect("origin connection"); + let request = receipt.lock().expect("receipt").take(); + (true, request) + } + + #[tokio::test(flavor = "current_thread")] + async fn host_known_empty_get_and_head_forward_over_tcp_after_read_deadline() { + for method in [Method::GET, Method::HEAD] { + for framing in ["", "Content-Length: 0\r\n"] { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("origin"); + let target: Uri = + format!("http://{}/asset", listener.local_addr().expect("address")) + .parse() + .expect("target"); + let router = RouterService::builder() + .with_state(target) + .route("/", method.clone(), forward_inbound) + .build(); + let mut app = App::new(router); + app.set_ingress_admission_policy(|head| { + assert_eq!(head.framing(), edgezero_core::IngressFraming::HostManaged); + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: Deadline::at_instant(head.request_start()), + } + }); + let request = format!( + "{method} / HTTP/1.1\r\nHost: localhost\r\n{framing}Connection: close\r\n\r\n" + ); + let ((response, exit), (accepted, receipt)) = timeout( + Duration::from_secs(2), + join( + exchange_with_wire(app, request), + record_forwarded_request(listener), + ), + ) + .await + .expect("bounded forwarding"); + assert!(response.starts_with("HTTP/1.1 204"), "{method}: {response}"); + assert_eq!(exit.expect("connection"), ConnectionExit::Completed); + assert!(accepted, "origin accepted the forward"); + let (received_method, headers, body) = receipt.expect("origin received request"); + assert_eq!(received_method, method); + assert!(body.is_empty()); + assert!(!headers.contains_key("transfer-encoding")); + assert!(!headers.contains_key("content-length")); + } + } + } + + #[tokio::test(flavor = "current_thread")] + async fn unknown_get_bodies_still_reject_before_dispatch_and_preserve_pipeline_boundaries() { + #[edgezero_core::action] + async fn healthy() -> &'static str { + "healthy" + } + + for (framing, body) in [ + ("Content-Length: 4", "data"), + ("Transfer-Encoding: chunked", "4\r\ndata\r\n0\r\n\r\n"), + ("Transfer-Encoding: chunked", "0\r\n\r\n"), + ] { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("origin"); + let target: Uri = format!("http://{}/asset", listener.local_addr().expect("address")) + .parse() + .expect("target"); + let router = RouterService::builder() + .with_state(target) + .get("/", forward_inbound) + .get("/healthy", healthy) + .build(); + let request = format!( + "GET / HTTP/1.1\r\nHost: localhost\r\n{framing}\r\n\r\n{body}\ + GET /healthy HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" + ); + let ((response, exit), (accepted, receipt)) = timeout( + Duration::from_secs(2), + join( + exchange_with_wire(App::new(router), request), + record_forwarded_request(listener), + ), + ) + .await + .expect("bounded rejection and connection cleanup"); + let empty = body == "0\r\n\r\n"; + assert!( + response.starts_with(if empty { + "HTTP/1.1 204" + } else { + "HTTP/1.1 400" + }), + "{response}" + ); + assert_eq!( + exit.expect("closed or correctly reused"), + ConnectionExit::Completed + ); + if empty { + let (method, headers, forwarded_body) = + receipt.expect("settled empty GET reaches origin"); + assert_eq!(method, "GET"); + assert!(forwarded_body.is_empty()); + assert!(!headers.contains_key("transfer-encoding")); + assert!(accepted); + let mut responses = response.split("HTTP/1.1 ").skip(1); + assert!( + responses + .next() + .expect("forwarded response") + .starts_with("204 ") + ); + let healthy = responses.next().expect("pipelined healthy response"); + assert!(healthy.starts_with("200 "), "{response}"); + assert!(healthy.ends_with("healthy"), "{response}"); + assert!( + responses.next().is_none(), + "unexpected extra response: {response}" + ); + } else { + assert!( + receipt.is_none(), + "a rejected body must never reach an origin" + ); + assert!( + !accepted, + "rejection must not even open an origin connection" + ); + assert!( + rejection_pipeline_is_safe(&response), + "unread body bytes must not become a new request: {response}" + ); + } + } + } + + fn rejection_pipeline_is_safe(response: &str) -> bool { + let mut responses = response.split("HTTP/1.1 ").skip(1); + match (responses.next(), responses.next(), responses.next()) { + (Some(first), None, None) => first.starts_with("400 "), + (Some(first), Some(second), None) => { + first.starts_with("400 ") + && second.starts_with("200 ") + && second.ends_with("healthy") + } + _ => false, + } + } + + #[test] + fn rejection_pipeline_guard_rejects_corrupted_second_response() { + let rejected = "HTTP/1.1 400 Bad Request\r\ncontent-length: 0\r\n\r\n"; + let corrupt = + "HTTP/1.1 405 Method Not Allowed\r\nconnection: close\r\ncontent-length: 0\r\n\r\n"; + let healthy = "HTTP/1.1 200 OK\r\nconnection: close\r\ncontent-length: 7\r\n\r\nhealthy"; + assert!(rejection_pipeline_is_safe(rejected)); + assert!(rejection_pipeline_is_safe(&format!("{rejected}{healthy}"))); + assert!(!rejection_pipeline_is_safe(&format!("{rejected}{corrupt}"))); + assert!(!rejection_pipeline_is_safe(&format!( + "{rejected}{corrupt}{healthy}" + ))); + } + + #[tokio::test] + async fn origin_recorder_distinguishes_connection_from_request() { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("origin"); + let address = listener.local_addr().expect("address"); + let ((accepted, request), ()) = timeout( + Duration::from_secs(2), + join(record_forwarded_request(listener), async move { + let socket = TcpStream::connect(address) + .await + .expect("connect without sending"); + drop(socket); + }), + ) + .await + .expect("bounded empty connection observation"); + assert!(accepted); + assert!(request.is_none()); + } + + #[tokio::test(flavor = "current_thread")] + async fn chunked_overflow_keeps_pipelined_request_boundaries_and_error_policy() { + use edgezero_core::extractor::State; + + #[edgezero_core::action] + async fn capped( + ctx: RequestContext, + State(cap): State, + ) -> Result<&'static str, EdgeError> { + ctx.body_bytes(cap).await?; + Ok("accepted") + } + + for (cap, len) in [(8_usize, 9_usize), (1024, 2048)] { + let router = RouterService::builder() + .with_state(cap) + .post("/", capped) + .get("/healthy", |_ctx: RequestContext| async { + Ok::<_, EdgeError>("healthy") + }) + .build(); + let mut app = App::new(router); + app.set_error_response_renderer(|_| { + response_builder() + .header("retry-after", "120") + .body(Body::from("overflow")) + .expect("error response") + }); + let request = format!( + "POST / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n\ + {len:x}\r\n{}\r\n0\r\n\r\n\ + GET /healthy HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + "x".repeat(len), + ); + let (wire, _exit) = exchange_with_wire(app, request).await; + assert!(wire.starts_with("HTTP/1.1 413"), "{wire}"); + assert!( + !wire.to_ascii_lowercase().contains("retry-after:"), + "{wire}" + ); + // Closing on unread bytes is valid; if reused, the following request must be intact. + if wire.matches("HTTP/1.1 ").count() > 1 { + assert!(wire.contains("HTTP/1.1 200"), "{wire}"); + assert!(wire.ends_with("healthy"), "{wire}"); + } + } + } + + #[tokio::test(flavor = "current_thread")] + async fn json_extractor_overflow_reaches_custom_renderer_without_retry_after() { + use edgezero_core::context::DEFAULT_INBOUND_JSON_BYTES; + use edgezero_core::extractor::Json; + + #[edgezero_core::action] + async fn extracted(_value: Json) -> Result<&'static str, EdgeError> { + Ok("accepted") + } + + let mut app = App::new(RouterService::builder().post("/", extracted).build()); + app.set_error_response_renderer(|error| { + assert_eq!( + error.request_body_overflow_origin(), + Some(edgezero_core::RequestBodyOverflowOrigin::Inbound) + ); + response_builder() + .header("retry-after", "120") + .body(Body::from("extractor overflow")) + .expect("custom error response") + }); + let cap = DEFAULT_INBOUND_JSON_BYTES; + let body = serde_json::to_string(&serde_json::json!("x".repeat(cap - 1))) + .expect("valid oversized JSON string"); + assert_eq!(body.len(), cap + 1); + let request = format!( + "POST / HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\n\ + Transfer-Encoding: chunked\r\nConnection: close\r\n\r\n\ + {:x}\r\n{body}\r\n0\r\n\r\n", + body.len(), + ); + let (wire, _exit) = exchange_with_wire(app, request).await; + assert!(wire.starts_with("HTTP/1.1 413"), "{wire}"); + assert!(wire.ends_with("extractor overflow"), "{wire}"); + assert!( + !wire.to_ascii_lowercase().contains("retry-after:"), + "{wire}" + ); + } + + #[tokio::test(flavor = "current_thread")] + async fn bodyless_tcp_responses_release_the_response_resource_exactly_once() { + for (method, status) in [ + ("HEAD", StatusCode::OK), + ("GET", StatusCode::NO_CONTENT), + ("GET", StatusCode::NOT_MODIFIED), + ] { + let polls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let completed = Arc::new(AtomicUsize::new(0)); + let resources = Arc::new(AtomicUsize::new(0)); + let source_polls = Arc::clone(&polls); + let source_drops = Arc::clone(&drops); + let router = RouterService::builder() + .route( + "/", + Method::from_bytes(method.as_bytes()).expect("method"), + move |_| { + let observed = Arc::clone(&source_polls); + let guard = SourceDropProbe(Arc::clone(&source_drops)); + async move { + let body = poll_fn(move |_cx| { + let _keep_alive = &guard; + observed.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + }); + Ok::<_, EdgeError>( + response_builder() + .status(status) + .body(Body::from_stream(body)) + .expect("response"), + ) + } + }, + ) + .build(); + let mut app = App::new(router); + let observer = RecordingObserver::default(); + app.set_response_egress_observer(observer.clone()); + let terminals = Arc::clone(&completed); + let released = Arc::clone(&resources); + app.set_ingress_admission_policy(move |head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::new({ + let callback = Arc::clone(&terminals); + let resource = SourceDropProbe(Arc::clone(&released)); + move |report| { + assert_eq!(report.outcome, ResponseEgressOutcome::HostHandoff); + callback.fetch_add(1, Ordering::SeqCst); + drop(resource); + } + }), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + }); + let (wire, exit) = exchange_with_method(app, method).await; + assert_eq!(exit.expect("complete"), ConnectionExit::Completed); + assert_eq!(wire.split_once("\r\n\r\n").expect("wire head").1, ""); + assert_eq!(polls.load(Ordering::SeqCst), 0); + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert_eq!(completed.load(Ordering::SeqCst), 1); + assert_eq!(resources.load(Ordering::SeqCst), 1); + let reports = observer.0.lock().expect("reports"); + assert_eq!(reports.len(), 1); + assert_eq!(reports.first().expect("report").bytes_written, 0); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn oversized_response_head_closes_and_completes_once_without_polling_body() { + let observer = RecordingObserver::default(); + let polls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let resource_drops = Arc::new(AtomicUsize::new(0)); + let observed_polls = Arc::clone(&polls); + let observed_drops = Arc::clone(&drops); + let router = RouterService::builder() + .get("/", move |_ctx| { + let source_polls = Arc::clone(&observed_polls); + let probe = SourceDropProbe(Arc::clone(&observed_drops)); + async move { + let source = poll_fn(move |_cx| { + let _keep_alive = &probe; + source_polls.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + }); + Ok::<_, EdgeError>( + response_builder() + .header( + "x-oversized", + "a".repeat(AxumIngressConfig::default().max_raw_head_bytes()), + ) + .body(Body::from_stream(source)) + .unwrap(), + ) + } + }) + .build(); + let mut app = App::new(router); + app.set_response_egress_observer(observer.clone()); + let observed_resources = Arc::clone(&resource_drops); + app.set_ingress_admission_policy(move |head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::new({ + let resource = SourceDropProbe(Arc::clone(&observed_resources)); + move |_report| drop(resource) + }), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + }); + let (wire, exit) = exchange_with_app(app).await; + assert!(wire.is_empty()); + let _error = exit.expect_err("oversized response head must close the connection"); + assert_eq!(polls.load(Ordering::SeqCst), 0); + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert_eq!(resource_drops.load(Ordering::SeqCst), 1); + let reports = observer.0.lock().unwrap(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::TransportError); + assert_eq!(reports[0].bytes_written, 0); + } + + #[tokio::test(flavor = "current_thread")] + async fn hyper_declared_stream_checks_clean_eof_including_zero() { + for (length, tail, expected_body, expected_polls) in [ + ("3", DeclaredTail::Clean, "abc", 2), + ("0", DeclaredTail::Clean, "", 1), + ("3", DeclaredTail::Empty, "abc", 3), + ("0", DeclaredTail::Empty, "", 3), + ] { + let observer = RecordingObserver::default(); + let polls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let app = declared_stream_app(length, tail, &observer, &polls, &drops); + + let (wire, exit) = exchange_with_app(app).await; + assert_eq!(exit.expect("clean connection"), ConnectionExit::Completed); + let (head, body) = wire.split_once("\r\n\r\n").expect("response head"); + assert!(head.starts_with("HTTP/1.1 200")); + assert_eq!(body, expected_body); + assert_eq!(polls.load(Ordering::SeqCst), expected_polls); + assert_eq!(drops.load(Ordering::SeqCst), 1); + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::HostHandoff); + assert_eq!( + reports[0].bytes_written, + u64::try_from(expected_body.len()).expect("body length") + ); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn hyper_declared_stream_rejects_excess_and_error_tails() { + for tail in [DeclaredTail::Excess, DeclaredTail::Error] { + let observer = RecordingObserver::default(); + let polls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let app = declared_stream_app("3", tail, &observer, &polls, &drops); + + let (wire, exit) = exchange_with_app(app).await; + assert!(exit.is_err(), "bad tail must close the connection"); + assert!(!wire.ends_with("abc"), "unvalidated final frame was sent"); + assert_eq!(polls.load(Ordering::SeqCst), 2); + assert_eq!(drops.load(Ordering::SeqCst), 1); + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::SourceError); + assert_eq!(reports[0].bytes_written, 0); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn hyper_declared_stream_pending_eof_is_deadline_bounded() { + let observer = RecordingObserver::default(); + let polls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let app = declared_stream_app("3", DeclaredTail::Pending, &observer, &polls, &drops); + + let (wire, exit) = exchange_with_app(app).await; + assert_eq!( + exit.expect("deadline closes connection"), + ConnectionExit::DeadlineExceeded + ); + assert!(!wire.ends_with("abc")); + assert!(polls.load(Ordering::SeqCst) >= 2); + assert_eq!(drops.load(Ordering::SeqCst), 1); + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert_eq!(reports[0].bytes_written, 0); + } + + #[tokio::test(flavor = "current_thread")] + async fn hyper_zero_declared_stream_validation_is_bounded_before_handoff() { + for tail in [ + DeclaredTail::Excess, + DeclaredTail::Error, + DeclaredTail::Pending, + ] { + let observer = RecordingObserver::default(); + let polls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let app = declared_stream_app("0", tail, &observer, &polls, &drops); + + let (wire, exit) = exchange_with_app(app).await; + assert_eq!(exit.expect("bounded fallback"), ConnectionExit::Completed); + let expected = if matches!(tail, DeclaredTail::Pending) { + ("HTTP/1.1 504", ResponseEgressOutcome::DeadlineExceeded) + } else { + ("HTTP/1.1 500", ResponseEgressOutcome::ConversionError) + }; + assert!(wire.starts_with(expected.0)); + assert!(polls.load(Ordering::SeqCst) > 0); + assert_eq!(drops.load(Ordering::SeqCst), 1); + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, expected.1); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn hyper_eof_lookahead_yields_to_host_deadlines_with_a_frozen_clock() { + for length in ["3", "0"] { + let observer = RecordingObserver::default(); + let polls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let mut app = declared_stream_app( + length, + DeclaredTail::EmptyForever, + &observer, + &polls, + &drops, + ); + let frozen_at = MonotonicInstant::now(); + app.set_monotonic_clock(MonotonicClock::new(move || frozen_at)); + + let (wire, exit) = exchange_with_app(app).await; + if length == "3" { + assert_eq!( + exit.expect("host deadline closes connection"), + ConnectionExit::DeadlineExceeded + ); + assert!(!wire.ends_with("abc")); + } else { + assert_eq!(exit.expect("bounded fallback"), ConnectionExit::Completed); + assert!(wire.starts_with("HTTP/1.1 504")); + } + assert!(polls.load(Ordering::SeqCst) > 1); + assert_eq!(drops.load(Ordering::SeqCst), 1); + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn hyper_zero_length_registration_expiry_uses_timeout_fallback() { + let observer = RecordingObserver::default(); + let polls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let mut app = declared_stream_app("0", DeclaredTail::Clean, &observer, &polls, &drops); + let started_at = MonotonicInstant::now(); + let deadline = started_at + .checked_add(Duration::from_secs(5)) + .expect("deadline"); + let observed_polls = Arc::clone(&polls); + let after_eof_samples = AtomicUsize::new(0); + app.set_monotonic_clock(MonotonicClock::new(move || { + if observed_polls.load(Ordering::SeqCst) == 0 + || after_eof_samples.fetch_add(1, Ordering::SeqCst) == 0 + { + started_at + } else { + deadline + } + })); + + let (wire, exit) = exchange_with_app(app).await; + assert_eq!(exit.expect("bounded fallback"), ConnectionExit::Completed); + assert!( + wire.starts_with("HTTP/1.1 504"), + "expiry at registration must remain a timeout" + ); + assert_eq!(polls.load(Ordering::SeqCst), 1); + assert_eq!(drops.load(Ordering::SeqCst), 1); + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + } + + fn attempt( + observer: &RecordingObserver, + started_at: MonotonicInstant, + ) -> ResponseEgressAttempt { + let headers = HeaderMap::new(); + let route = RouteMetadata::new(Method::GET, "/pending"); + let head = ResponseEgressHead::new( + StatusCode::OK, + Version::HTTP_11, + &headers, + None, + started_at, + Some(&route), + ); + ResponseEgressAttempt::new( + &head, + started_at, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + ) + } + + #[tokio::test(flavor = "current_thread")] + async fn independent_deadline_closes_connection_with_pending_non_send_body() { + let local = LocalSet::new(); + local + .run_until(async { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind listener"); + let address = listener.local_addr().expect("listener address"); + let observer = RecordingObserver::default(); + let server_observer = observer.clone(); + + let server = spawn_local(async move { + let (stream, _) = listener.accept().await.expect("accept client"); + let responses = EgressConnection::default(); + let service_responses = responses.clone(); + let service = service_fn(move |_request: Request| { + let started_at = MonotonicInstant::now(); + let deadline = Deadline::at_instant( + started_at + .checked_add(Duration::from_millis(25)) + .expect("deadline"), + ); + let body_result = AxumEgressBody::application( + Body::from_stream(pending()), + deadline, + MonotonicClock::default(), + attempt(&server_observer, started_at), + &service_responses, + ); + async move { + let Ok(registered_body) = body_result else { + panic!("register body"); + }; + Ok::<_, Infallible>(Response::new(registered_body)) + } + }); + + serve_http1(stream, service, responses, AxumIngressConfig::default()).await + }); + + let mut client = TcpStream::connect(address).await.expect("connect client"); + client + .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .expect("write request"); + + let exit = server + .await + .expect("server task") + .expect("serve connection"); + assert_eq!(exit, ConnectionExit::DeadlineExceeded); + + let mut response = Vec::new(); + timeout( + Duration::from_millis(100), + client.read_to_end(&mut response), + ) + .await + .expect("connection closes promptly") + .expect("read response"); + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + }) + .await; + } + + #[tokio::test(flavor = "current_thread")] + async fn zero_read_client_is_closed_when_socket_backpressure_reaches_deadline() { + let local = LocalSet::new(); + local + .run_until(async { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind listener"); + let address = listener.local_addr().expect("listener address"); + let observer = RecordingObserver::default(); + let server_observer = observer.clone(); + + let server = spawn_local(async move { + let (stream, _) = listener.accept().await.expect("accept client"); + let responses = EgressConnection::default(); + let service_responses = responses.clone(); + let service = service_fn(move |_request: Request| { + let started_at = MonotonicInstant::now(); + let deadline = Deadline::at_instant( + started_at + .checked_add(Duration::from_millis(50)) + .expect("deadline"), + ); + let source = + repeat_with(|| Ok(bytes::Bytes::from(vec![0_u8; 256 * 1_024]))); + let body_result = AxumEgressBody::application( + Body::from_stream(source), + deadline, + MonotonicClock::default(), + attempt(&server_observer, started_at), + &service_responses, + ); + async move { + let Ok(registered_body) = body_result else { + panic!("register body"); + }; + Ok::<_, Infallible>(Response::new(registered_body)) + } + }); + serve_http1(stream, service, responses, AxumIngressConfig::default()).await + }); + + let mut client = TcpStream::connect(address).await.expect("connect client"); + client + .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .expect("write request"); + + let exit = timeout(Duration::from_secs(2), server) + .await + .expect("deadline closes blocked connection") + .expect("server task") + .expect("serve connection"); + assert_eq!(exit, ConnectionExit::DeadlineExceeded); + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert!(reports[0].bytes_written > 0); + }) + .await; + } + + #[test] + fn deadline_attributes_winner_and_ordered_pipeline_collateral() { + let observer = RecordingObserver::default(); + let responses = EgressConnection::default(); + let started_at = MonotonicInstant::now(); + let later = Deadline::at_instant( + started_at + .checked_add(Duration::from_secs(2)) + .expect("later deadline"), + ); + let earlier = Deadline::at_instant( + started_at + .checked_add(Duration::from_secs(1)) + .expect("earlier deadline"), + ); + let first_result = AxumEgressBody::application( + Body::from_stream(pending()), + later, + MonotonicClock::default(), + attempt(&observer, started_at), + &responses, + ); + let second_result = AxumEgressBody::application( + Body::from_stream(pending()), + earlier, + MonotonicClock::default(), + attempt(&observer, started_at), + &responses, + ); + let (Ok(first_body), Ok(second_body)) = (first_result, second_result) else { + panic!("register pipelined bodies"); + }; + + let elapsed = responses.next_deadline().expect("next deadline"); + assert!(responses.signal_elapsed_deadline(elapsed)); + drop(first_body); + drop(second_body); + + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 2); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::TransportError); + assert_eq!(reports[1].outcome, ResponseEgressOutcome::DeadlineExceeded); + } + + #[test] + fn host_timer_uses_deadline_as_the_minimum_terminal_timestamp() { + let observer = RecordingObserver::default(); + let responses = EgressConnection::default(); + let started_at = MonotonicInstant::now(); + let deadline_at = started_at + .checked_add(Duration::from_secs(1)) + .expect("deadline"); + let frozen_clock = MonotonicClock::new(move || started_at); + let headers = HeaderMap::new(); + let head = ResponseEgressHead::new( + StatusCode::OK, + Version::HTTP_11, + &headers, + None, + started_at, + None, + ); + let attempt = ResponseEgressAttempt::new( + &head, + started_at, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + frozen_clock.clone(), + ); + let body_result = AxumEgressBody::application( + Body::from_stream(pending()), + Deadline::at_instant(deadline_at), + frozen_clock, + attempt, + &responses, + ); + let Ok(body) = body_result else { + panic!("register body"); + }; + + let elapsed = responses.next_deadline().expect("host deadline"); + assert!(responses.signal_elapsed_deadline(elapsed)); + drop(body); + + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert_eq!(reports[0].elapsed, Duration::from_secs(1)); + } +} diff --git a/crates/edgezero-adapter-axum/src/dev_server.rs b/crates/edgezero-adapter-axum/src/dev_server.rs index d8d1adc4c..5408f61eb 100644 --- a/crates/edgezero-adapter-axum/src/dev_server.rs +++ b/crates/edgezero-adapter-axum/src/dev_server.rs @@ -1,36 +1,47 @@ +use std::any::Any; use std::fs; +use std::future; #[cfg(test)] use std::iter; use std::net::{SocketAddr, TcpListener as StdTcpListener}; +use std::panic::AssertUnwindSafe; use std::path::{Path, PathBuf}; -use std::str::FromStr as _; use std::sync::Arc; use anyhow::Context as _; -use axum::Router; +use futures_util::FutureExt as _; +use futures_util::StreamExt as _; +use futures_util::future::{Either, select}; +use futures_util::stream::FuturesUnordered; use tokio::net::TcpListener as TokioTcpListener; use tokio::runtime::Builder as RuntimeBuilder; use tokio::signal; -use tower::{Service as _, service_fn}; +use tokio::sync::oneshot::{Receiver as ShutdownReceiver, Sender as ShutdownSender, channel}; +use tokio::task::{LocalSet, spawn_blocking}; use edgezero_core::addr; -use edgezero_core::app::{Hooks, StoreMetadata, StoresMetadata}; -use edgezero_core::config_store::ConfigStoreHandle; +use edgezero_core::app::{App, Hooks, StoreMetadata, StoresMetadata}; +use edgezero_core::config_store::{ConfigStoreError, ConfigStoreHandle, ConfigStoreOpenFailure}; use edgezero_core::env_config::EnvConfig; use edgezero_core::key_value_store::KvHandle; +use edgezero_core::logging::{BOOT_LOG_LEVEL, BOOT_LOG_TARGET, resolve_logging_level}; +#[cfg(test)] use edgezero_core::router::RouterService; use edgezero_core::secret_store::SecretHandle; use edgezero_core::store_registry::{ BoundSecretStore, ConfigRegistry, ConfigStoreBinding, KvRegistry, SecretRegistry, StoreRegistry, }; -use log::LevelFilter; use simple_logger::SimpleLogger; use std::collections::BTreeMap; use crate::config_store::AxumConfigStore; +use crate::config_store_limits::ConfigStoreLimits; +use crate::connection::{ConnectionExit, serve_http1}; +use crate::ingress_config::AxumIngressConfig; use crate::key_value_store::PersistentKvStore; +use crate::response::EgressConnection; use crate::secret_store::EnvSecretStore; -use crate::service::EdgeZeroAxumService; +use crate::service::AxumServiceState; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum KvInitRequirement { @@ -38,11 +49,15 @@ enum KvInitRequirement { Required, } +type ConnectionResult = Result, Box>; + /// Configuration used when running the dev server embedding `EdgeZero` into Axum. #[derive(Clone)] pub struct AxumDevServerConfig { pub addr: SocketAddr, pub enable_ctrl_c: bool, + /// Validated listener, parser and idle-plus-header limits. + pub ingress: AxumIngressConfig, } impl Default for AxumDevServerConfig { @@ -51,6 +66,7 @@ impl Default for AxumDevServerConfig { Self { addr: SocketAddr::from((addr::DEFAULT_HOST, addr::DEFAULT_PORT)), enable_ctrl_c: true, + ingress: AxumIngressConfig::default(), } } } @@ -71,18 +87,24 @@ struct Stores { /// Blocking dev server runner used by the `EdgeZero` CLI. pub struct AxumDevServer { + app: App, config: AxumDevServerConfig, - router: RouterService, stores: Stores, } impl AxumDevServer { + pub(crate) async fn bind_listener(&self) -> anyhow::Result { + TokioTcpListener::bind(self.config.addr) + .await + .with_context(|| format!("failed to bind dev server to {}", self.config.addr)) + } + #[must_use] #[inline] - pub fn new(router: RouterService) -> Self { + pub fn new(app: App) -> Self { Self { + app, config: AxumDevServerConfig::default(), - router, stores: Stores::default(), } } @@ -100,41 +122,50 @@ impl AxumDevServer { } async fn run_async(self) -> anyhow::Result<()> { - let AxumDevServer { - router, - config, - stores, - } = self; - - // Allow binding to already-open listener if caller created one to surface errors early. - let std_listener = StdTcpListener::bind(config.addr) - .with_context(|| format!("failed to bind dev server to {}", config.addr))?; - std_listener - .set_nonblocking(true) - .context("failed to set listener to non-blocking")?; - - let listener = TokioTcpListener::from_std(std_listener) - .context("failed to adopt std listener into tokio")?; - - serve_with_stores(router, listener, config.enable_ctrl_c, stores).await + let listener = self.bind_listener().await?; + self.serve(listener, future::pending()).await } - #[cfg(test)] - async fn run_with_listener(self, listener: TokioTcpListener) -> anyhow::Result<()> { + /// Serves this assembled application through the bounded HTTP/1 listener and local executor. + /// + /// The supplied listener's address wins over `config.addr`. Resolving `shutdown` cancels + /// active connections instead of draining their responses, then waits for worker cleanup. + /// Dropping this future also signals cancellation but cannot await its completion. + /// Platform metadata and policies are retained; foreign provider request contexts are absent. + /// + /// # Errors + /// Returns listener adoption, runtime, worker or serving failures. + #[inline] + pub async fn serve( + self, + listener: TokioTcpListener, + shutdown: Shutdown, + ) -> anyhow::Result<()> + where + Shutdown: future::Future, + { let AxumDevServer { - router, + app, config, stores, } = self; - serve_with_stores(router, listener, config.enable_ctrl_c, stores).await + serve_with_stores( + app, + listener, + config.enable_ctrl_c, + config.ingress, + stores, + shutdown, + ) + .await } #[must_use] #[inline] - pub fn with_config(router: RouterService, config: AxumDevServerConfig) -> Self { + pub fn with_config(app: App, config: AxumDevServerConfig) -> Self { Self { + app, config, - router, stores: Stores::default(), } } @@ -272,52 +303,148 @@ fn kv_handle_from_path(kv_path: &Path) -> anyhow::Result { } async fn serve_with_stores( - router: RouterService, + app: App, listener: TokioTcpListener, enable_ctrl_c: bool, + ingress: AxumIngressConfig, stores: Stores, + shutdown: impl future::Future, ) -> anyhow::Result<()> { - let service = { - let mut service = EdgeZeroAxumService::new(router); - if let Some(registry) = stores.config_registry { - service = service.with_config_registry(registry); - } - if let Some(handle) = stores.config_store { - service = service.with_config_store_handle(handle); - } - if let Some(registry) = stores.kv_registry { - service = service.with_kv_registry(registry); - } - if let Some(handle) = stores.kv { - service = service.with_kv_handle(handle); - } - if let Some(registry) = stores.secret_registry { - service = service.with_secret_registry(registry); + struct ShutdownOnDrop(Option>); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for ShutdownOnDrop { + fn drop(&mut self) { + if let Some(sender) = self.0.take() { + let _sent = sender.send(()); + } } - if let Some(handle) = stores.secrets { - service = service.with_secret_handle(handle); + } + + let native_listener = listener + .into_std() + .context("failed to release tokio listener")?; + let (shutdown_sender, shutdown_receiver) = channel(); + let shutdown_guard = ShutdownOnDrop(Some(shutdown_sender)); + let worker = spawn_blocking(move || { + serve_local( + app, + native_listener, + enable_ctrl_c, + ingress, + stores, + shutdown_receiver, + ) + }); + let result = match select(worker, Box::pin(shutdown)).await { + Either::Left((result, _shutdown)) => result, + Either::Right(((), pending_worker)) => { + drop(shutdown_guard); + return pending_worker.await.context("axum server thread failed")?; } - service }; - let axum_router = Router::new().fallback_service(service_fn(move |req| { - let mut svc = service.clone(); - async move { svc.call(req).await } - })); - let make_service = axum_router.into_make_service_with_connect_info::(); - - let shutdown = enable_ctrl_c.then_some(async { - let _ctrl_c = signal::ctrl_c().await; - }); + drop(shutdown_guard); + result.context("axum server thread failed")? +} - let server = axum::serve(listener, make_service); - if let Some(shutdown_signal) = shutdown { - let graceful_server = server.with_graceful_shutdown(shutdown_signal); - graceful_server.await.context("axum server error")?; - } else { - server.await.context("axum server error")?; +fn serve_local( + app: App, + std_listener: StdTcpListener, + enable_ctrl_c: bool, + ingress: AxumIngressConfig, + stores: Stores, + shutdown_receiver: ShutdownReceiver<()>, +) -> anyhow::Result<()> { + if app.ingress_head_limits().max_request_target_bytes() + > u64::try_from(ingress.max_raw_head_bytes()).unwrap_or(u64::MAX) + || app.ingress_head_limits().max_request_header_bytes() + > u64::try_from(ingress.max_raw_head_bytes()).unwrap_or(u64::MAX) + || app.ingress_head_limits().max_request_header_count() + > u64::try_from(ingress.max_header_count()).unwrap_or(u64::MAX) + { + log::warn!(target: BOOT_LOG_TARGET, "application head limits exceed Axum raw ingress limits; the parser rejects first"); + } + let mut service = AxumServiceState::from_app(app); + if let Some(registry) = stores.config_registry { + service = service.with_config_registry(registry); + } + if let Some(handle) = stores.config_store { + service = service.with_config_store_handle(handle); + } + if let Some(registry) = stores.kv_registry { + service = service.with_kv_registry(registry); } + if let Some(handle) = stores.kv { + service = service.with_kv_handle(handle); + } + if let Some(registry) = stores.secret_registry { + service = service.with_secret_registry(registry); + } + if let Some(handle) = stores.secrets { + service = service.with_secret_handle(handle); + } + + let runtime = RuntimeBuilder::new_current_thread() + .enable_all() + .build() + .context("failed to build axum connection runtime")?; + let local = LocalSet::new(); + #[expect( + clippy::integer_division_remainder_used, + reason = "tokio::select! expands to internal randomized branch selection arithmetic" + )] + let serve = async move { + let listener = TokioTcpListener::from_std(std_listener) + .context("failed to adopt listener on axum connection runtime")?; + let shutdown = async move { + let _closed = shutdown_receiver.await; + }; + let ctrl_c = async move { + if enable_ctrl_c { + let _signal = signal::ctrl_c().await; + } else { + future::pending::<()>().await; + } + }; + tokio::pin!(shutdown); + tokio::pin!(ctrl_c); + let mut connections = FuturesUnordered::new(); - Ok(()) + loop { + tokio::select! { + biased; + () = &mut shutdown => break, + () = &mut ctrl_c => break, + Some(result) = connections.next(), if !connections.is_empty() => { + report_connection_exit(&result); + } + accepted = listener.accept() => { + let (stream, remote_addr) = accepted.context("axum listener accept failed")?; + // One transient accepted socket; no parser, task or waiter on refusal. + if connections.len() >= ingress.max_connections() { + drop(stream); + continue; + } + let responses = EgressConnection::default(); + let connection_service = service.for_connection(remote_addr, responses.clone()); + connections.push(AssertUnwindSafe(serve_http1(stream, connection_service, responses, ingress)).catch_unwind()); + } + } + } + // Dropping the bounded owner releases sockets, parser buffers and egress attempts. + drop(connections); + Ok(()) + }; + runtime.block_on(local.run_until(serve)) +} + +fn report_connection_exit(result: &ConnectionResult) { + if !matches!(result, Ok(Ok(_))) { + log::debug!("axum HTTP/1 connection rejected or disconnected"); + } } /// Entry point for an Axum dev-server application. @@ -329,60 +456,90 @@ async fn serve_with_stores( /// /// # Errors /// Returns an error if the dev server fails to bind or any required store handle cannot be initialised. +fn build_app_for_dispatch() -> anyhow::Result +where + A: Hooks, +{ + App::build::(crate::AXUM_PLATFORM) + .inspect_err(|error| { + log::error!(target: BOOT_LOG_TARGET, "application assembly failed: {error:?}"); + }) + .context("application configuration failed") +} + +fn app_logger(env: &EnvConfig) -> SimpleLogger { + SimpleLogger::new() + .with_level(resolve_logging_level(env)) + .with_module_level(BOOT_LOG_TARGET, BOOT_LOG_LEVEL) +} + +/// Runs an application with the Axum development server. +/// +/// # Errors +/// Returns an error if logger setup, application configuration, runtime setup, store initialization, listener +/// binding, or connection serving fails. #[inline] pub fn run_app() -> anyhow::Result<()> where A: Hooks, { - let env = EnvConfig::from_env(); - let stores = A::stores(); - let kv_init_requirement = kv_init_requirement(stores); - - let level = env - .logging_level() - .and_then(|raw| LevelFilter::from_str(raw).ok()) - .unwrap_or(LevelFilter::Info); + run_app_with_preflight::(|| Ok(())) +} +/// Run a startup check after logger initialization and before application configuration. +/// +/// Useful for capability validation in embedded runners. Applications that own logging +/// must install their backend and filters before calling this function. +/// +/// # Errors +/// Returns an error if logger setup, `preflight`, application configuration, runtime setup, +/// store initialization, listener binding, or connection serving fails. +#[inline] +pub fn run_app_with_preflight(preflight: F) -> anyhow::Result<()> +where + A: Hooks, + F: FnOnce() -> anyhow::Result<()>, +{ + let env = EnvConfig::from_env(); if !A::owns_logging() { - let _logger_init = SimpleLogger::new().with_level(level).init(); + app_logger(&env) + .init() + .context("failed to initialize application logger")?; } + let ingress = AxumIngressConfig::from_env(&env)?; + let config_limits = ConfigStoreLimits::from_env(&env)?; + log::info!("[edgezero] axum HTTP/1 ingress limits: {ingress:?}"); + log::info!("[edgezero] axum config snapshot limits: {config_limits:?}"); + preflight()?; + let app = build_app_for_dispatch::()?; + let stores = A::stores(); + let kv_init_requirement = kv_init_requirement(stores); + let kv_registry = build_kv_registry(stores.kv, &env, kv_init_requirement)?; + let config_registry = build_config_registry(stores.config, &env, config_limits)?; + let secret_registry = build_secret_registry(stores.secrets, &env); let resolution = resolve_addr(&env); for warning in &resolution.warnings { - log::warn!("{warning}"); + log::warn!(target: BOOT_LOG_TARGET, "{warning}"); } let addr = resolution.addr; - let app = A::build_app(); - let router = app.router().clone(); - log::info!("[edgezero] starting axum server on http://{addr}"); - let runtime = RuntimeBuilder::new_multi_thread() - .enable_all() - .build() - .context("failed to build tokio runtime")?; - - runtime.block_on(async move { - let std_listener = StdTcpListener::bind(addr) - .with_context(|| format!("failed to bind dev server to {addr}"))?; - std_listener - .set_nonblocking(true) - .context("failed to set listener to non-blocking")?; - let listener = TokioTcpListener::from_std(std_listener) - .context("failed to adopt std listener into tokio")?; - - let kv_registry = build_kv_registry(stores.kv, &env, kv_init_requirement)?; - let config_registry = build_config_registry(stores.config, &env); - let secret_registry = build_secret_registry(stores.secrets, &env); - - let request_stores = Stores { + AxumDevServer { + app, + config: AxumDevServerConfig { + addr, + enable_ctrl_c: true, + ingress, + }, + stores: Stores { config_registry, kv_registry, secret_registry, ..Stores::default() - }; - serve_with_stores(router, listener, true, request_stores).await - }) + }, + } + .run() } /// Build the per-request KV registry from baked store metadata. @@ -408,6 +565,7 @@ fn build_kv_registry( Err(err) => match init { KvInitRequirement::Optional => { log::warn!( + target: BOOT_LOG_TARGET, "KV store '{}' (id `{}`) could not be initialized at {}: {}", store_name, id, @@ -432,6 +590,7 @@ fn build_kv_registry( let default_id = meta.default.to_owned(); if !by_id.contains_key(&default_id) { log::warn!( + target: BOOT_LOG_TARGET, "KV registry default id `{default_id}` failed to initialize; dropping the KV registry — \ handlers will see no KV store" ); @@ -444,44 +603,69 @@ fn build_kv_registry( /// Each declared id reads `.edgezero/local-config-.json`. A missing /// file yields an empty store for that id — the dev server stays usable /// before any `config push` has populated the file. A malformed file logs a -/// warning and the id is dropped from the registry rather than failing -/// startup, matching the cloudflare config-binding behaviour. +/// warning and preserves a typed failing binding. Allocation violations fail +/// startup before any listener is bound. Snapshots load sequentially. fn build_config_registry( config_meta: Option, env: &EnvConfig, -) -> Option { - let meta = config_meta?; + limits: ConfigStoreLimits, +) -> Result, ConfigStoreError> { + build_config_registry_at(config_meta, env, limits, AxumConfigStore::local_path) +} + +fn build_config_registry_at( + config_meta: Option, + env: &EnvConfig, + limits: ConfigStoreLimits, + path_for: impl Fn(&str) -> PathBuf, +) -> Result, ConfigStoreError> { + let Some(meta) = config_meta else { + return Ok(None); + }; + if meta.ids.is_empty() + || !meta.ids.contains(&meta.default) + || meta.ids.iter().enumerate().any(|(index, id)| { + id.is_empty() + || !id + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_') + || meta.ids.iter().take(index).any(|previous| previous == id) + }) + { + return Err(ConfigStoreError::internal(anyhow::anyhow!( + "invalid declared config-store metadata" + ))); + } let mut by_id: BTreeMap = BTreeMap::new(); + let mut resident = 0_usize; for id in meta.ids { - let store = match AxumConfigStore::from_local_file(id) { - Ok(store) => store, - Err(err) => { + let handle = match AxumConfigStore::load_at_startup(&path_for(id), limits, resident) { + Ok(store) => { + resident = resident + .checked_add(store.resident_allocation_bytes()) + .ok_or(ConfigStoreError::ValueTooLarge)?; + ConfigStoreHandle::new(Arc::new(store)) + } + Err(ConfigStoreError::ValueTooLarge) => return Err(ConfigStoreError::ValueTooLarge), + Err(_error) => { log::warn!( - "config store for id `{}` could not be loaded from {}: {}; \ - dropping this id from the registry", - id, - AxumConfigStore::local_path(id).display(), - err + target: BOOT_LOG_TARGET, + "config snapshot unavailable; retaining a failed declared binding" ); - continue; + ConfigStoreHandle::failed_open(ConfigStoreOpenFailure::Unavailable) } }; by_id.insert( (*id).to_owned(), ConfigStoreBinding { - handle: ConfigStoreHandle::new(Arc::new(store)), + handle, default_key: env.store_key("config", id), }, ); } let default_id = meta.default.to_owned(); - if !by_id.contains_key(&default_id) { - log::warn!( - "config registry default id `{default_id}` failed to load; dropping the config registry — \ - handlers will see no config store" - ); - } - StoreRegistry::from_parts(by_id, default_id) + log::info!("[edgezero] axum config snapshot resident charge: {resident} bytes"); + Ok(StoreRegistry::from_parts(by_id, default_id)) } /// Build the per-request secret registry. Axum is `Single` for secrets — every @@ -523,7 +707,275 @@ pub(crate) fn resolve_addr(env: &EnvConfig) -> addr::BindAddrResolution { #[cfg(test)] mod tests { use super::*; + use edgezero_core::error::EdgeError; + use futures::executor::block_on; + use log::LevelFilter; + use std::env; use std::net::{IpAddr, Ipv4Addr}; + use std::process::Command; + use std::sync::atomic::{AtomicUsize, Ordering}; + + struct FailingConfiguration; + + struct LoggingConfiguration; + + struct OwnedLoggingConfiguration; + + struct PlatformAwareConfiguration; + + #[expect( + clippy::missing_trait_methods, + reason = "test hook exercises only adapter startup failure" + )] + impl Hooks for FailingConfiguration { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Err(EdgeError::service_unavailable("configuration unavailable")) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "test hook probes startup logging" + )] + impl Hooks for LoggingConfiguration { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + log::warn!(target: edgezero_core::BOOT_LOG_TARGET, "boot-warning-probe"); + log::info!(target: edgezero_core::BOOT_LOG_TARGET, "boot-info-probe"); + log::warn!("runtime-warning-probe"); + log::debug!("runtime-debug-probe"); + Err(EdgeError::service_unavailable("stop before listening")) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "test hook probes application logger ownership" + )] + impl Hooks for OwnedLoggingConfiguration { + fn configure(app: &mut App) -> Result<(), EdgeError> { + LoggingConfiguration::configure(app) + } + + fn owns_logging() -> bool { + true + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "test hook exercises only target metadata propagation" + )] + impl Hooks for PlatformAwareConfiguration { + fn configure(app: &mut App) -> Result<(), EdgeError> { + if app.platform() == crate::AXUM_PLATFORM { + Ok(()) + } else { + Err(EdgeError::service_unavailable("wrong application platform")) + } + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + fn logging_probe(scenario: &str, level: &str) -> String { + let output = Command::new(env::current_exe().expect("test executable")) + .args([ + "--exact", + "dev_server::tests::startup_logging_child", + "--nocapture", + ]) + .env("EDGEZERO_TEST_LOGGING_SCENARIO", scenario) + .env("EDGEZERO__LOGGING__LEVEL", level) + .env_remove("EDGEZERO__ADAPTER__INGRESS__MAX_CONNECTIONS") + .env_remove("EDGEZERO__ADAPTER__INGRESS__MAX_RAW_HEAD_BYTES") + .env_remove("EDGEZERO__ADAPTER__INGRESS__MAX_HEADER_COUNT") + .env_remove("EDGEZERO__ADAPTER__INGRESS__HEAD_READ_TIMEOUT_MS") + .envs( + (scenario == "invalid-ingress") + .then_some(("EDGEZERO__ADAPTER__INGRESS__MAX_CONNECTIONS", "0")), + ) + .output() + .expect("isolated logging probe"); + let stdout = String::from_utf8(output.stdout).expect("probe stdout"); + let stderr = String::from_utf8(output.stderr).expect("probe stderr"); + assert!(output.status.success(), "{stdout}\n{stderr}"); + assert!( + stdout.contains("1 passed"), + "child probe must execute: {stdout}" + ); + format!("{stdout}\n{stderr}") + } + + #[test] + fn startup_logging_child() { + let Ok(scenario) = env::var("EDGEZERO_TEST_LOGGING_SCENARIO") else { + return; + }; + let result = match scenario.as_str() { + "invalid-ingress" => { + let result = run_app_with_preflight::(|| { + panic!( + "invalid ingress must fail before preflight or application configuration" + ) + }); + assert_eq!( + result + .expect_err("invalid ingress must stop startup") + .to_string(), + "invalid Axum ingress setting `max_connections`" + ); + return; + } + "installed" => { + SimpleLogger::new() + .with_level(LevelFilter::Trace) + .init() + .expect("existing logger"); + let result = run_app_with_preflight::(|| { + log::warn!(target: BOOT_LOG_TARGET, "unexpected-preflight-probe"); + Ok(()) + }); + assert_eq!( + result + .expect_err("logger conflict must stop startup") + .to_string(), + "failed to initialize application logger" + ); + return; + } + "owned" => { + SimpleLogger::new() + .with_level(LevelFilter::Trace) + .init() + .expect("owned logger"); + log::set_max_level(LevelFilter::Debug); + let result = run_app::(); + assert_eq!(log::max_level(), LevelFilter::Debug); + result + } + "preflight" => run_app_with_preflight::(|| { + log::warn!(target: edgezero_core::BOOT_LOG_TARGET, "preflight-warning-probe"); + anyhow::bail!("preflight stopped startup"); + }), + _ => run_app::(), + }; + assert!(result.is_err(), "probe must stop before listener setup"); + } + + #[test] + fn invalid_ingress_stops_before_preflight_configuration_and_binding() { + let output = logging_probe("invalid-ingress", "info"); + assert!(!output.contains("boot-warning-probe"), "{output}"); + } + + #[test] + fn logging_is_ready_before_configure_with_separate_boot_filter() { + for level in ["off", "error", "trace"] { + let output = logging_probe("managed", level); + assert_eq!( + output.matches("application assembly failed:").count(), + 1, + "{output}" + ); + assert!(output.contains("stop before listening"), "{output}"); + assert!(output.contains("boot-warning-probe"), "{output}"); + assert!(!output.contains("boot-info-probe"), "{output}"); + assert_eq!( + output.contains("runtime-warning-probe"), + level == "trace", + "{output}" + ); + assert_eq!( + output.contains("runtime-debug-probe"), + level == "trace", + "{output}" + ); + } + } + + #[test] + fn owned_logging_preserves_backend_and_facade_filter() { + let output = logging_probe("owned", "off"); + assert!(output.contains("runtime-debug-probe"), "{output}"); + assert!( + output.contains("boot-info-probe"), + "application owns boot filtering: {output}" + ); + } + + #[test] + fn preflight_runs_after_logger_and_before_configuration() { + let output = logging_probe("preflight", "off"); + assert!(output.contains("preflight-warning-probe"), "{output}"); + assert!(!output.contains("boot-warning-probe"), "{output}"); + } + + #[test] + fn logger_conflict_prevents_preflight_and_configuration() { + let output = logging_probe("installed", "trace"); + assert!(!output.contains("unexpected-preflight-probe"), "{output}"); + assert!(!output.contains("boot-warning-probe"), "{output}"); + } + + #[test] + fn failing_configuration_prevents_listener_boundary() { + let bind_calls = AtomicUsize::new(0); + let result = build_app_for_dispatch::().map(|_app| { + bind_calls.fetch_add(1, Ordering::SeqCst); + }); + + let error = result.expect_err("configuration must fail before listener bind"); + assert_eq!(error.to_string(), "application configuration failed"); + assert_eq!(bind_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn failing_routes_prevent_configuration_and_listener_boundary() { + struct FailingRoutes; + #[expect( + clippy::missing_trait_methods, + reason = "tests route failure at adapter assembly boundary" + )] + impl Hooks for FailingRoutes { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Err(EdgeError::bad_request("configure must not run")) + } + fn routes() -> Result { + Err(EdgeError::service_unavailable("route state unavailable")) + } + } + let bind_calls = AtomicUsize::new(0); + let result = build_app_for_dispatch::().map(|_app| { + bind_calls.fetch_add(1, Ordering::SeqCst); + }); + let error = result.expect_err("route failure must precede the listener boundary"); + assert_eq!( + error.downcast_ref::().unwrap().status().as_u16(), + 503 + ); + assert_eq!(bind_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn application_configuration_receives_axum_platform_metadata() { + let app = build_app_for_dispatch::().expect("configured app"); + + assert_eq!(app.platform(), crate::AXUM_PLATFORM); + } #[test] fn default_config_uses_expected_address() { @@ -552,6 +1004,7 @@ mod tests { let config = AxumDevServerConfig { addr, enable_ctrl_c: false, + ingress: AxumIngressConfig::default(), }; assert_eq!(config.addr.ip(), IpAddr::V4(Ipv4Addr::UNSPECIFIED)); assert_eq!(config.addr.port(), 3000); @@ -563,7 +1016,7 @@ mod tests { use edgezero_core::router::RouterService; let router = RouterService::builder().build(); - let server = AxumDevServer::new(router); + let server = AxumDevServer::new(App::new(router)); assert_eq!(server.config.addr.port(), 8787); assert!(server.config.enable_ctrl_c); } @@ -576,8 +1029,9 @@ mod tests { let config = AxumDevServerConfig { addr: SocketAddr::from(([127, 0, 0, 1], 9000)), enable_ctrl_c: false, + ingress: AxumIngressConfig::default(), }; - let server = AxumDevServer::with_config(router, config); + let server = AxumDevServer::with_config(App::new(router), config); assert_eq!(server.config.addr.port(), 9000); assert!(!server.config.enable_ctrl_c); } @@ -716,7 +1170,9 @@ mod tests { default: "app_config", ids: &["app_config"], }; - let registry = build_config_registry(Some(meta), &env).expect("registry built"); + let registry = build_config_registry(Some(meta), &env, ConfigStoreLimits::default()) + .expect("load") + .expect("registry built"); let binding = registry.named("app_config").expect("binding registered"); assert_eq!(binding.default_key, "app_config_staging"); } @@ -729,10 +1185,654 @@ mod tests { default: "app_config", ids: &["app_config"], }; - let registry = build_config_registry(Some(meta), &env).expect("registry built"); + let registry = build_config_registry(Some(meta), &env, ConfigStoreLimits::default()) + .expect("load") + .expect("registry built"); let binding = registry.named("app_config").expect("binding registered"); assert_eq!(binding.default_key, "app_config"); } + + #[test] + fn malformed_declared_snapshot_is_not_an_absent_binding() { + let directory = tempfile::tempdir().expect("directory"); + let path = directory.path().join("config.json"); + fs::write(&path, "credential=private invalid JSON").expect("fixture"); + let meta = StoreMetadata { + default: "config", + ids: &["config"], + }; + let registry = build_config_registry_at( + Some(meta), + &EnvConfig::default(), + ConfigStoreLimits::default(), + |_id| path.clone(), + ) + .expect("failed binding") + .expect("registry"); + let binding = registry.named("config").expect("declared binding survives"); + let error = block_on(binding.handle.get("key")).expect_err("unavailable, not absent"); + assert!(matches!(error, ConfigStoreError::Unavailable { .. })); + assert!(!error.to_string().contains("credential=private")); + } + + #[test] + fn aggregate_snapshot_overflow_fails_startup_and_retry_recovers() { + let directory = tempfile::tempdir().expect("directory"); + let path = directory.path().join("config.json"); + fs::write(&path, serde_json::json!({"k": "v"}).to_string()).expect("fixture"); + let limits = ConfigStoreLimits::new(128, 1, 1, 1, 4096, 8192).expect("limits"); + let charge = AxumConfigStore::from_path(&path, limits) + .expect("snapshot") + .resident_allocation_bytes(); + let capped = ConfigStoreLimits::new( + 128, + 1, + 1, + 1, + charge.saturating_mul(2).saturating_sub(1), + 8192, + ) + .expect("limits"); + let meta = StoreMetadata { + default: "first", + ids: &["first", "second"], + }; + assert!(matches!( + build_config_registry_at(Some(meta), &EnvConfig::default(), capped, |_id| path + .clone()), + Err(ConfigStoreError::ValueTooLarge) + )); + let registry = build_config_registry_at(Some(meta), &EnvConfig::default(), limits, |_id| { + path.clone() + }) + .expect("retry") + .expect("registry"); + assert!(registry.named("first").is_some()); + assert!(registry.named("second").is_some()); + } + + #[test] + fn invalid_config_registry_metadata_fails_without_opening_stores() { + for meta in [ + StoreMetadata { + default: "first", + ids: &[], + }, + StoreMetadata { + default: "absent", + ids: &["first"], + }, + StoreMetadata { + default: "first", + ids: &["first", "first"], + }, + StoreMetadata { + default: "bad/id", + ids: &["bad/id"], + }, + ] { + let error = build_config_registry_at( + Some(meta), + &EnvConfig::default(), + ConfigStoreLimits::default(), + |_id| panic!("must not open invalid metadata"), + ) + .expect_err("invalid metadata"); + assert!(matches!(error, ConfigStoreError::Internal { .. })); + } + } +} + +#[cfg(test)] +mod bounded_ingress_tests { + use std::future::pending; + use std::net::SocketAddr; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::time::Duration; + + use bytes::Bytes; + use edgezero_core::app::App; + use edgezero_core::body::Body; + use edgezero_core::error::EdgeError; + use edgezero_core::http::response_builder; + use edgezero_core::ingress::{AdmissionDecision, IngressGrant}; + use edgezero_core::response_egress::{ResponseEgressCompletion, ResponseEgressOutcome}; + use edgezero_core::router::RouterService; + use edgezero_core::{Deadline, MonotonicClock, MonotonicInstant, ResponseEgressPolicy}; + use futures_util::stream::poll_fn; + use std::task::Poll; + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + use tokio::net::{TcpListener, TcpStream}; + use tokio::task::{JoinHandle, yield_now}; + use tokio::time::{Instant, sleep, timeout}; + + use super::{AxumDevServer, AxumDevServerConfig}; + use crate::ingress_config::AxumIngressConfig; + + struct Server { + address: SocketAddr, + calls: Arc, + panics: Arc, + task: JoinHandle>, + } + + struct DropProbe(Arc); + + #[derive(Clone, Default)] + struct EgressProbes { + completions: Arc, + polls: Arc, + resources: Arc, + sources: Arc, + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropProbe { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for Server { + fn drop(&mut self) { + self.task.abort(); + } + } + + async fn closed(stream: &mut TcpStream) { + let mut byte = [0]; + let read = timeout(Duration::from_secs(2), stream.read(&mut byte)) + .await + .expect("connection must close"); + assert!( + matches!(read, Ok(0) | Err(_)), + "refusal must not send a response" + ); + } + + async fn healthy(stream: &mut TcpStream) { + stream + .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .unwrap(); + let read = async { + let mut head = Vec::new(); + while !head.ends_with(b"\r\n\r\n") { + head.push(stream.read_u8().await.unwrap()); + assert!(head.len() < 4096); + } + assert!(head.starts_with(b"HTTP/1.1 200")); + let mut body = [0; 2]; + stream.read_exact(&mut body).await.unwrap(); + assert_eq!(&body, b"ok"); + }; + timeout(Duration::from_secs(2), read).await.unwrap(); + } + + fn stalled_egress_app( + ready: bool, + expected: ResponseEgressOutcome, + probes: &EgressProbes, + ) -> App { + let sources = Arc::clone(&probes.sources); + let polls = Arc::clone(&probes.polls); + let chunk = Bytes::from(vec![b'a'; 0x0004_0000]); + let router = RouterService::builder() + .get("/", |_| async { Ok::<_, EdgeError>("ok") }) + .get("/slow", move |_| { + let source = DropProbe(Arc::clone(&sources)); + let observed_polls = Arc::clone(&polls); + let payload = chunk.clone(); + async move { + let body = poll_fn(move |_cx| { + let _keep_alive = &source; + let polled = observed_polls.fetch_add(1, Ordering::SeqCst); + if ready || polled == 0 { + Poll::Ready(Some(Ok(payload.clone()))) + } else { + Poll::Pending + } + }); + Ok::<_, EdgeError>( + response_builder() + .body(Body::from_stream(body)) + .expect("response"), + ) + } + }) + .build(); + let mut app = App::new(router); + let frozen = MonotonicInstant::now(); + app.set_monotonic_clock(MonotonicClock::new(move || frozen)); + let budget = if expected == ResponseEgressOutcome::TransportError { + Duration::from_secs(1) + } else { + Duration::from_millis(100) + }; + app.set_response_egress_policy(move |_, started| ResponseEgressPolicy { + write_deadline: Deadline::at_instant(started.checked_add(budget).expect("deadline")), + }); + let completions = Arc::clone(&probes.completions); + let resources = Arc::clone(&probes.resources); + app.set_ingress_admission_policy(move |head| { + let completion = if head.target().path() == "/slow" { + ResponseEgressCompletion::new({ + let callback = Arc::clone(&completions); + let resource = DropProbe(Arc::clone(&resources)); + move |report| { + assert_eq!(report.outcome, expected); + callback.fetch_add(1, Ordering::SeqCst); + drop(resource); + } + }) + } else { + ResponseEgressCompletion::empty() + }; + AdmissionDecision::Admit { + completion, + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + } + }); + app + } + + #[tokio::test] + async fn stalled_egress_termination_releases_capacity_and_recovers() { + for (ready, disconnect) in [(false, true), (false, false), (true, false)] { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("listener"); + let address = listener.local_addr().expect("address"); + let probes = EgressProbes::default(); + let expected = if disconnect { + ResponseEgressOutcome::TransportError + } else { + ResponseEgressOutcome::DeadlineExceeded + }; + let app = stalled_egress_app(ready, expected, &probes); + let task = tokio::spawn(super::serve_with_stores( + app, + listener, + false, + AxumIngressConfig::new(1, 8192, 100, Duration::from_secs(5)).expect("ingress"), + super::Stores::default(), + pending(), + )); + let mut client = TcpStream::connect(address).await.expect("client"); + client + .write_all(b"GET /slow HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .expect("request"); + timeout(Duration::from_secs(2), async { + while probes.polls.load(Ordering::SeqCst) < 2 { + yield_now().await; + } + }) + .await + .expect("producer is live"); + if disconnect { + drop(client); + } + timeout(Duration::from_secs(2), async { + while probes.completions.load(Ordering::SeqCst) != 1 { + yield_now().await; + } + }) + .await + .expect("terminal callback"); + assert_eq!(probes.resources.load(Ordering::SeqCst), 1); + assert_eq!(probes.sources.load(Ordering::SeqCst), 1); + let stopped = probes.polls.load(Ordering::SeqCst); + timeout(Duration::from_secs(2), async { + loop { + let mut recovered = TcpStream::connect(address) + .await + .expect("recovery connection"); + if recovered + .write_all( + b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ) + .await + .is_ok() + { + let mut wire = Vec::new(); + if recovered.read_to_end(&mut wire).await.is_ok() && wire.ends_with(b"ok") { + break; + } + } + yield_now().await; + } + }) + .await + .expect("capacity recovery"); + assert_eq!( + probes.polls.load(Ordering::SeqCst), + stopped, + "terminated sources must stop" + ); + assert_eq!(probes.completions.load(Ordering::SeqCst), 1); + task.abort(); + let _shutdown = task.await; + } + } + + async fn start(connections: usize, milliseconds: u64, count: usize) -> Server { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let calls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&calls); + let panics = Arc::new(AtomicUsize::new(0)); + let observed_panics = Arc::clone(&panics); + let router = RouterService::builder() + .get("/panic", move |_| { + let counted = Arc::clone(&observed_panics); + async move { + counted.fetch_add(1, Ordering::SeqCst); + panic!("isolated connection failure"); + #[expect( + unreachable_code, + reason = "sets the handler response type after the intentional panic" + )] + Ok::<_, EdgeError>("unreachable") + } + }) + .get("/", move |_| { + let counted = Arc::clone(&observed); + async move { + counted.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("ok") + } + }) + .build(); + let server = AxumDevServer::with_config( + App::new(router), + AxumDevServerConfig { + addr: address, + enable_ctrl_c: false, + ingress: AxumIngressConfig::new( + connections, + 8192, + count, + Duration::from_millis(milliseconds), + ) + .unwrap(), + }, + ); + let task = tokio::spawn(server.serve(listener, pending())); + Server { + address, + calls, + panics, + task, + } + } + + #[tokio::test] + async fn parser_and_unwinding_failures_release_capacity_and_leave_server_healthy() { + let server = start(1, 10_000, 100).await; + for request in [ + &b"invalid\r\n\r\n"[..], + &b"GET /panic HTTP/1.1\r\nHost: localhost\r\n\r\n"[..], + ] { + let mut failed = TcpStream::connect(server.address).await.unwrap(); + failed.write_all(request).await.unwrap(); + let mut response = Vec::new(); + let _read = timeout(Duration::from_secs(2), failed.read_to_end(&mut response)) + .await + .unwrap(); + assert!(!response.starts_with(b"HTTP/1.1 200")); + let mut recovered = TcpStream::connect(server.address).await.unwrap(); + healthy(&mut recovered).await; + drop(recovered); + } + assert_eq!(server.calls.load(Ordering::SeqCst), 2); + assert_eq!(server.panics.load(Ordering::SeqCst), 1); + assert!(!server.task.is_finished()); + } + + #[tokio::test] + async fn shutdown_releases_pending_handler_stream_and_completion_resources() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let started = Arc::new(AtomicUsize::new(0)); + let released = Arc::new(AtomicUsize::new(0)); + let resources = Arc::new(AtomicUsize::new(0)); + let terminals = Arc::new(AtomicUsize::new(0)); + let handler_started = Arc::clone(&started); + let handler_released = Arc::clone(&released); + let stream_started = Arc::clone(&started); + let stream_released = Arc::clone(&released); + let router = RouterService::builder() + .get("/handler", move |_| { + let entered = Arc::clone(&handler_started); + let guard = DropProbe(Arc::clone(&handler_released)); + async move { + entered.fetch_add(1, Ordering::SeqCst); + pending::<()>().await; + drop(guard); + Ok::<_, EdgeError>("unreachable") + } + }) + .get("/stream", move |_| { + let entered = Arc::clone(&stream_started); + let guard = DropProbe(Arc::clone(&stream_released)); + async move { + let body = poll_fn(move |_cx| { + let _keep_alive = &guard; + entered.store(2, Ordering::SeqCst); + Poll::>>::Pending + }); + Ok::<_, EdgeError>(response_builder().body(Body::from_stream(body)).unwrap()) + } + }) + .build(); + let mut app = App::new(router); + let completion_resources = Arc::clone(&resources); + let completion_terminals = Arc::clone(&terminals); + app.set_ingress_admission_policy(move |head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::new({ + let resource = DropProbe(Arc::clone(&completion_resources)); + let observed = Arc::clone(&completion_terminals); + move |report| { + assert_eq!(report.outcome, ResponseEgressOutcome::TransportError); + observed.fetch_add(1, Ordering::SeqCst); + drop(resource); + } + }), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(10)), + }); + let task = tokio::spawn(super::serve_with_stores( + app, + listener, + false, + AxumIngressConfig::new(2, 8192, 100, Duration::from_secs(10)).unwrap(), + super::Stores::default(), + pending(), + )); + let mut handler = TcpStream::connect(address).await.unwrap(); + handler + .write_all(b"GET /handler HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .unwrap(); + timeout(Duration::from_secs(2), async { + while started.load(Ordering::SeqCst) != 1 { + yield_now().await; + } + }) + .await + .unwrap(); + let mut stream = TcpStream::connect(address).await.unwrap(); + stream + .write_all(b"GET /stream HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .unwrap(); + let mut head = Vec::new(); + timeout(Duration::from_secs(2), async { + while !head.ends_with(b"\r\n\r\n") { + head.push(stream.read_u8().await.unwrap()); + assert!(head.len() < 4096); + } + }) + .await + .unwrap(); + assert!(head.starts_with(b"HTTP/1.1 200")); + assert_eq!(started.load(Ordering::SeqCst), 2); + assert_eq!(released.load(Ordering::SeqCst), 0); + assert_eq!(resources.load(Ordering::SeqCst), 0); + task.abort(); + closed(&mut handler).await; + closed(&mut stream).await; + timeout(Duration::from_secs(2), async { + while released.load(Ordering::SeqCst) != 2 || resources.load(Ordering::SeqCst) != 2 { + yield_now().await; + } + }) + .await + .unwrap(); + assert_eq!(terminals.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn saturation_refuses_without_queueing_and_disconnect_recovers() { + let server = start(2, 10_000, 100).await; + let mut first = TcpStream::connect(server.address).await.unwrap(); + healthy(&mut first).await; + let mut second = TcpStream::connect(server.address).await.unwrap(); + healthy(&mut second).await; + for _ in 0_usize..64 { + let mut refused = TcpStream::connect(server.address).await.unwrap(); + closed(&mut refused).await; + } + assert_eq!(server.calls.load(Ordering::SeqCst), 2); + drop(first); + // Wait for disconnect processing without depending on an arbitrary sleep. + let recover = async { + loop { + let mut stream = TcpStream::connect(server.address).await.unwrap(); + let written = stream + .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") + .await; + let mut response = Vec::new(); + let read = stream.read_to_end(&mut response).await; + if written.is_ok() && read.is_ok() && response.starts_with(b"HTTP/1.1 200") { + break; + } + yield_now().await; + } + }; + timeout(Duration::from_secs(2), recover).await.unwrap(); + healthy(&mut second).await; + assert_eq!(server.calls.load(Ordering::SeqCst), 4); + } + + #[tokio::test] + async fn silence_and_trickling_initial_and_keepalive_heads_expire() { + let server = start(1, 150, 100).await; + let mut silent = TcpStream::connect(server.address).await.unwrap(); + closed(&mut silent).await; + for keepalive in [false, true] { + let mut stream = TcpStream::connect(server.address).await.unwrap(); + if keepalive { + healthy(&mut stream).await; + } + stream + .write_all(b"GET / HTTP/1.1\r\nX-Trickle: ") + .await + .unwrap(); + let started = Instant::now(); + let trickle = async { + loop { + sleep(Duration::from_millis(10)).await; + if stream.write_all(b"a").await.is_err() { + break; + } + let mut byte = [0]; + if let Ok(read) = + timeout(Duration::from_millis(1), stream.peek(&mut byte)).await + && !matches!(read, Ok(1..)) + { + break; + } + } + }; + timeout(Duration::from_secs(2), trickle) + .await + .expect("trickle must not extend deadline"); + assert!(started.elapsed() < Duration::from_secs(2)); + closed(&mut stream).await; + } + let mut recovered = TcpStream::connect(server.address).await.unwrap(); + healthy(&mut recovered).await; + assert_eq!(server.calls.load(Ordering::SeqCst), 2); + } + + #[tokio::test] + async fn idle_keepalive_timeout_releases_capacity() { + let server = start(1, 100, 100).await; + let mut idle = TcpStream::connect(server.address).await.unwrap(); + healthy(&mut idle).await; + closed(&mut idle).await; + let mut recovered = TcpStream::connect(server.address).await.unwrap(); + healthy(&mut recovered).await; + } + + #[tokio::test] + async fn raw_count_duplicates_and_byte_boundaries_precede_dispatch() { + let server = start(1, 10_000, 4).await; + let prefix = b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nX-Pad: "; + let mut exact = prefix.to_vec(); + exact.extend(vec![b'a'; 8192 - prefix.len() - 4]); + exact.extend_from_slice(b"\r\n\r\n"); + let mut over = exact.clone(); + over.insert(prefix.len(), b'a'); + let count_exact = b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nX-Dup: a\r\nX-Dup: b\r\n\r\n".to_vec(); + let count_over = b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nX-Dup: a\r\nX-Dup: b\r\nX-Dup: c\r\n\r\n".to_vec(); + let mut long_line = b"GET /".to_vec(); + long_line.extend(vec![b'a'; 8192]); + long_line.extend_from_slice(b" HTTP/1.1\r\nHost: localhost\r\n\r\n"); + for (request, expected) in [ + (exact, b"HTTP/1.1 200"), + (over, b"HTTP/1.1 431"), + (count_exact, b"HTTP/1.1 200"), + (count_over, b"HTTP/1.1 431"), + (long_line, b"HTTP/1.1 431"), + ] { + let mut stream = TcpStream::connect(server.address).await.unwrap(); + stream.write_all(&request).await.unwrap(); + let mut response = Vec::new(); + let _read = timeout(Duration::from_secs(2), stream.read_to_end(&mut response)) + .await + .unwrap(); + assert!(response.starts_with(expected), "unexpected response status"); + } + assert_eq!(server.calls.load(Ordering::SeqCst), 2); + let mut recovered = TcpStream::connect(server.address).await.unwrap(); + healthy(&mut recovered).await; + } + + #[tokio::test] + async fn cancellation_closes_idle_and_partial_connections() { + let server = start(2, 10_000, 100).await; + let mut idle = TcpStream::connect(server.address).await.unwrap(); + healthy(&mut idle).await; + let mut partial = TcpStream::connect(server.address).await.unwrap(); + healthy(&mut partial).await; + partial + .write_all(b"GET / HTTP/1.1\r\nX-Pending:") + .await + .unwrap(); + server.task.abort(); + closed(&mut idle).await; + closed(&mut partial).await; + } } #[cfg(test)] @@ -744,6 +1844,7 @@ mod integration_tests { use edgezero_core::extractor::Secrets; use edgezero_core::router::RouterService; use edgezero_core::secret_store::SecretHandle as CoreSecretHandle; + use reqwest::header::ALLOW; use std::time::{Duration, Instant}; use tokio::task::{JoinHandle, spawn_blocking}; use tokio::time::sleep; @@ -767,15 +1868,16 @@ mod integration_tests { let config = AxumDevServerConfig { addr, enable_ctrl_c: false, + ingress: AxumIngressConfig::default(), }; // Use a unique temp directory for each test server let temp_dir = tempfile::tempdir().expect("create temp dir"); let kv_path = temp_dir.path().join("kv.redb"); let kv_handle = kv_handle_from_path(&kv_path).expect("create kv store"); - let server = AxumDevServer::with_config(router, config).with_kv_handle(kv_handle); + let server = AxumDevServer::with_config(App::new(router), config).with_kv_handle(kv_handle); let handle = tokio::spawn(async move { - let _result = server.run_with_listener(listener).await; + let _result = server.serve(listener, future::pending()).await; }); TestServer { @@ -854,6 +1956,7 @@ mod integration_tests { let response = send_with_retry(&client, |http_client| http_client.get(url.as_str())).await; assert_eq!(response.status(), reqwest::StatusCode::METHOD_NOT_ALLOWED); + assert_eq!(response.headers().get(ALLOW).expect("Allow header"), "POST"); server.handle.abort(); } @@ -862,7 +1965,6 @@ mod integration_tests { async fn server_forwards_headers() { async fn handler(ctx: RequestContext) -> Result { let value = ctx - .request() .headers() .get("x-custom") .and_then(|val| val.to_str().ok()) @@ -897,8 +1999,9 @@ mod integration_tests { let config = AxumDevServerConfig { addr, enable_ctrl_c: false, + ingress: AxumIngressConfig::default(), }; - let server = AxumDevServer::with_config(router, config); + let server = AxumDevServer::with_config(App::new(router), config); // Run in blocking mode to capture the error let result = spawn_blocking(move || server.run()).await; @@ -1124,13 +2227,14 @@ mod integration_tests { let config = super::AxumDevServerConfig { addr, enable_ctrl_c: false, + ingress: AxumIngressConfig::default(), }; - let mut server = super::AxumDevServer::with_config(router, config); + let mut server = super::AxumDevServer::with_config(App::new(router), config); if let Some(handle) = secret_handle { server = server.with_secret_handle(handle); } let handle = tokio::spawn(async move { - let _result = server.run_with_listener(listener).await; + let _result = server.serve(listener, future::pending()).await; }); TestServerWithStore { base_url: format!("http://{addr}"), @@ -1197,7 +2301,8 @@ mod integration_tests { ); let body = response.text().await.unwrap(); assert!(!body.contains("API_KEY")); - assert!(body.contains("required secret is not configured")); + assert!(body.contains("internal server error")); + assert!(!body.contains("required secret is not configured")); server.handle.abort(); } @@ -1218,7 +2323,8 @@ mod integration_tests { reqwest::StatusCode::INTERNAL_SERVER_ERROR ); let body = response.text().await.unwrap(); - assert!(body.contains( + assert!(body.contains("internal server error")); + assert!(!body.contains( "no secret store configured -- check [stores.secrets] in edgezero.toml and platform bindings" )); diff --git a/crates/edgezero-adapter-axum/src/header_deadline.rs b/crates/edgezero-adapter-axum/src/header_deadline.rs new file mode 100644 index 000000000..a2b307464 --- /dev/null +++ b/crates/edgezero-adapter-axum/src/header_deadline.rs @@ -0,0 +1,333 @@ +use std::error::Error as StdError; +use std::future::Future; +use std::io::{self, IoSlice}; +use std::pin::Pin; +use std::sync::{Arc, Mutex, PoisonError}; +use std::task::{Context, Poll}; +use std::time::{Duration, Instant}; + +use edgezero_core::http::{HeaderMap, HeaderName, HeaderValue}; +use hyper::Request; +use hyper::body::Incoming; +use hyper::ext::ReasonPhrase; +use hyper::rt::{Sleep, Timer}; +use hyper::service::Service; +use hyper_util::rt::TokioTimer; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::net::TcpStream; +use tokio::sync::Notify; +use tokio::time::Instant as TokioInstant; + +use crate::ingress_config::AxumIngressConfig; +use crate::response::EgressConnection; +/// Shares Hyper's head-read phase with the connection's independent timer race. +/// A successful service call disarms it; parser-error writes retain the deadline. +#[derive(Clone)] +pub(crate) struct HeaderDeadline { + changed: Arc, + deadline: Arc>>, +} + +#[derive(Debug, thiserror::Error)] +#[error("HTTP/1 request head deadline exceeded")] +pub(crate) struct HeaderDeadlineExceeded; + +pub(crate) struct HeaderIo { + deadline: HeaderDeadline, + stream: TcpStream, +} + +pub(crate) struct HeaderService { + deadline: HeaderDeadline, + limits: AxumIngressConfig, + responses: EgressConnection, + service: S, +} + +#[derive(Debug, thiserror::Error)] +#[error("HTTP/1 response head exceeds adapter parser storage limits")] +struct ResponseHeadExceeded; + +impl HeaderDeadline { + pub(crate) async fn changed(&self) { + self.changed.notified().await; + } + + fn check_io(&self) -> io::Result<()> { + if self + .deadline() + .is_some_and(|limit| TokioInstant::now() >= limit) + { + return Err(io::Error::new( + io::ErrorKind::TimedOut, + HeaderDeadlineExceeded, + )); + } + Ok(()) + } + + pub(crate) fn deadline(&self) -> Option { + *self.deadline.lock().unwrap_or_else(PoisonError::into_inner) + } + + pub(crate) fn finish_head(&self, now: TokioInstant) -> Result<(), HeaderDeadlineExceeded> { + let mut deadline = self.deadline.lock().unwrap_or_else(PoisonError::into_inner); + if deadline.is_some_and(|limit| now >= limit) { + return Err(HeaderDeadlineExceeded); + } + *deadline = None; + self.changed.notify_one(); + Ok(()) + } + + pub(crate) fn new(timeout: Duration) -> Self { + let now = TokioInstant::now(); + Self { + changed: Arc::new(Notify::new()), + deadline: Arc::new(Mutex::new(Some(now.checked_add(timeout).unwrap_or(now)))), + } + } +} + +impl Timer for HeaderDeadline { + fn now(&self) -> Instant { + TokioInstant::now().into_std() + } + + fn reset(&self, sleep: &mut Pin>, new_deadline: Instant) { + *sleep = self.sleep_until(new_deadline); + } + + fn sleep(&self, duration: Duration) -> Pin> { + let now = self.now(); + self.sleep_until(now.checked_add(duration).unwrap_or(now)) + } + + fn sleep_until(&self, deadline: Instant) -> Pin> { + let requested = TokioInstant::from_std(deadline); + let mut state = self.deadline.lock().unwrap_or_else(PoisonError::into_inner); + let effective = state.map_or(requested, |existing| existing.min(requested)); + *state = Some(effective); + self.changed.notify_one(); + TokioTimer::new().sleep_until(effective.into_std()) + } +} + +impl HeaderIo { + pub(crate) fn new(stream: TcpStream, deadline: HeaderDeadline) -> Self { + Self { deadline, stream } + } +} + +impl AsyncRead for HeaderIo { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.get_mut(); + this.deadline.check_io()?; + Pin::new(&mut this.stream).poll_read(cx, buf) + } +} + +impl AsyncWrite for HeaderIo { + fn is_write_vectored(&self) -> bool { + self.stream.is_write_vectored() + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + this.deadline.check_io()?; + Pin::new(&mut this.stream).poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.get_mut().stream).poll_shutdown(cx) + } + + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.get_mut(); + this.deadline.check_io()?; + Pin::new(&mut this.stream).poll_write(cx, buf) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[IoSlice<'_>], + ) -> Poll> { + let this = self.get_mut(); + this.deadline.check_io()?; + Pin::new(&mut this.stream).poll_write_vectored(cx, bufs) + } +} + +impl HeaderService { + pub(crate) fn new( + service: S, + deadline: HeaderDeadline, + limits: AxumIngressConfig, + responses: EgressConnection, + ) -> Self { + Self { + deadline, + limits, + responses, + service, + } + } +} + +impl Service> for HeaderService +where + S: Service, Response = hyper::Response>, + S::Error: Into>, + S::Future: 'static, + B: 'static, +{ + type Error = Box; + type Future = Pin>>>; + type Response = hyper::Response; + + fn call(&self, req: Request) -> Self::Future { + if let Err(error) = self.deadline.finish_head(TokioInstant::now()) { + let boxed_error: Self::Error = Box::new(error); + return Box::pin(async move { Err(boxed_error) }); + } + let future = self.service.call(req); + let limits = self.limits; + let responses = self.responses.clone(); + Box::pin(async move { + let mut response = future.await.map_err(Into::into)?; + if let Err(error) = bound_response_head(&mut response, limits) { + responses.signal_transport_error(); + let boxed_error: Self::Error = Box::new(error); + return Err(boxed_error); + } + Ok(response) + }) + } +} + +fn bound_response_head( + response: &mut hyper::Response, + limits: AxumIngressConfig, +) -> Result<(), ResponseHeadExceeded> { + let count = response.headers().len(); + let bytes = response + .headers() + .iter() + .try_fold(0_usize, |charged, (name, value)| { + charged + .checked_add(name.as_str().len())? + .checked_add(value.as_bytes().len())? + .checked_add(4) + }); + if count > limits.max_response_header_count() + || bytes.is_none_or(|total| total > limits.max_response_header_bytes()) + { + return Err(ResponseHeadExceeded); + } + // Discard application-supplied spare capacity before Hyper caches this map. + let mut compact = HeaderMap::with_capacity(count); + for (name, value) in response.headers() { + let copied_name = HeaderName::from_bytes(name.as_str().as_bytes()) + .map_err(|_invalid_name| ResponseHeadExceeded)?; + let mut copied_value = HeaderValue::from_bytes(value.as_bytes()) + .map_err(|_invalid_value| ResponseHeadExceeded)?; + copied_value.set_sensitive(value.is_sensitive()); + compact.append(copied_name, copied_value); + } + *response.headers_mut() = compact; + // Only canonical status phrases participate in the bounded encoded head. + response.extensions_mut().remove::(); + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use edgezero_core::http::{HeaderMap, HeaderValue}; + use hyper::ext::ReasonPhrase; + use hyper::rt::Timer as _; + use tokio::time::{Instant, advance}; + + use super::{HeaderDeadline, bound_response_head}; + use crate::ingress_config::AxumIngressConfig; + + #[test] + fn response_head_compacts_capacity_preserves_duplicates_and_rejects_overflow() { + let limits = AxumIngressConfig::default(); + let mut response = hyper::Response::new(()); + *response.headers_mut() = HeaderMap::with_capacity(0x4000); + response + .headers_mut() + .append("set-cookie", HeaderValue::from_static("a=1")); + let mut sensitive = HeaderValue::from_static("b=2"); + sensitive.set_sensitive(true); + response.headers_mut().append("set-cookie", sensitive); + response + .extensions_mut() + .insert(ReasonPhrase::from_static(b"custom")); + bound_response_head(&mut response, limits).unwrap(); + assert_eq!(response.headers().get_all("set-cookie").iter().count(), 2); + assert!(response.headers().capacity() < 100); + assert!( + response + .headers() + .get_all("set-cookie") + .iter() + .nth(1) + .unwrap() + .is_sensitive() + ); + assert!(response.extensions().get::().is_none()); + let oversized = HeaderValue::from_bytes(&vec![b'a'; limits.max_raw_head_bytes()]).unwrap(); + response.headers_mut().insert("x-large", oversized); + bound_response_head(&mut response, limits).unwrap_err(); + response.headers_mut().clear(); + for _ in 0..=limits.max_response_header_count() { + response + .headers_mut() + .append("x-dup", HeaderValue::from_static("a")); + } + bound_response_head(&mut response, limits).unwrap_err(); + } + + #[tokio::test(start_paused = true)] + async fn rearmed_header_timer_remains_in_tokio_clock_domain() { + let deadline = HeaderDeadline::new(Duration::from_secs(10)); + deadline.finish_head(Instant::now()).unwrap(); + advance(Duration::from_secs(20)).await; + drop(deadline.sleep(Duration::from_secs(10))); + assert_eq!( + deadline + .deadline() + .unwrap() + .checked_duration_since(Instant::now()), + Some(Duration::from_secs(10)) + ); + } + + #[tokio::test] + async fn first_arm_cannot_extend_accept_deadline_and_next_head_gets_new_budget() { + let timer = HeaderDeadline::new(Duration::from_millis(10)); + let accepted_deadline = timer.deadline().unwrap(); + let _sleep = timer.sleep_until((Instant::now() + Duration::from_secs(1)).into_std()); + assert_eq!(timer.deadline(), Some(accepted_deadline)); + timer + .finish_head(accepted_deadline - Duration::from_millis(1)) + .unwrap(); + assert_eq!(timer.deadline(), None); + let next = Instant::now() + Duration::from_secs(1); + let _next_sleep = timer.sleep_until(next.into_std()); + assert_eq!(timer.deadline(), Some(next)); + assert!(timer.finish_head(next).is_err()); + } +} diff --git a/crates/edgezero-adapter-axum/src/ingress_config.rs b/crates/edgezero-adapter-axum/src/ingress_config.rs new file mode 100644 index 000000000..08cccf72f --- /dev/null +++ b/crates/edgezero-adapter-axum/src/ingress_config.rs @@ -0,0 +1,280 @@ +//! Validated limits for Axum's adapter-owned HTTP/1 listener and parser. + +use std::time::Duration; + +use edgezero_core::env_config::EnvConfig; +use edgezero_core::http::{HeaderName, HeaderValue}; + +/// Effective HTTP/1 ingress limits, frozen before the listener starts. +/// +/// The raw-head budget includes the request line, field syntax, CRLFs and final +/// blank line. It bounds both the request line and headers jointly, not with +/// independent byte allowances. The head timeout also bounds keep-alive idle. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AxumIngressConfig { + head_read_timeout: Duration, + max_connections: usize, + max_header_count: usize, + max_raw_head_bytes: usize, +} + +/// A rejected setting. Diagnostics never include the supplied value. +#[derive(Debug, thiserror::Error)] +#[error("invalid Axum ingress setting `{field}`")] +pub struct IngressConfigError { + field: &'static str, +} + +impl Default for AxumIngressConfig { + #[inline] + fn default() -> Self { + Self { + head_read_timeout: Duration::from_secs(10), + max_connections: 256, + max_header_count: 100, + max_raw_head_bytes: 0x0001_0000, + } + } +} + +impl AxumIngressConfig { + /// Resolve `EDGEZERO__ADAPTER__INGRESS__*`; only missing settings use defaults. + /// + /// # Errors + /// Rejects malformed, zero, overflowing or out-of-range explicit settings. + #[inline] + pub fn from_env(env: &EnvConfig) -> Result { + let defaults = Self::default(); + let milliseconds = setting(env, "head_read_timeout_ms", 10_000)?; + let timeout_millis = + u64::try_from(milliseconds).map_err(|_overflow| IngressConfigError { + field: "head_read_timeout_ms", + })?; + Self::new( + setting(env, "max_connections", defaults.max_connections)?, + setting(env, "max_raw_head_bytes", defaults.max_raw_head_bytes)?, + setting(env, "max_header_count", defaults.max_header_count)?, + Duration::from_millis(timeout_millis), + ) + } + + /// Absolute idle-plus-header budget for each request head. + #[must_use] + #[inline] + pub const fn head_read_timeout(self) -> Duration { + self.head_read_timeout + } + + /// Maximum owned live HTTP/1 connections, including keep-alive. + #[must_use] + #[inline] + pub const fn max_connections(self) -> usize { + self.max_connections + } + + /// Maximum raw fields in a request head, counting duplicates separately. + #[must_use] + #[inline] + pub const fn max_header_count(self) -> usize { + self.max_header_count + } + + /// Combined raw request-head byte limit, inclusive for complete heads. + #[must_use] + #[inline] + pub const fn max_raw_head_bytes(self) -> usize { + self.max_raw_head_bytes + } + + /// Maximum application response field bytes before Hyper encodes/caches them. + #[must_use] + #[inline] + pub const fn max_response_header_bytes(self) -> usize { + self.max_raw_head_bytes + } + + /// Response field cap bounds the header map Hyper reuses for request parsing. + #[must_use] + #[inline] + pub fn max_response_header_count(self) -> usize { + self.max_header_count.max(100) + } + + /// Construct validated settings. + /// + /// Ranges: 1..=4096 connections, 8192..=1048576 raw-head bytes, + /// 1..=1024 fields, and 1 ms..=120 s idle-plus-header timeout. + /// + /// # Errors + /// Rejects values outside the supported finite ranges. + #[inline] + pub fn new( + max_connections: usize, + max_raw_head_bytes: usize, + max_header_count: usize, + head_read_timeout: Duration, + ) -> Result { + for (valid, field) in [ + ((1..=4096).contains(&max_connections), "max_connections"), + ( + (8192..=0x0010_0000).contains(&max_raw_head_bytes), + "max_raw_head_bytes", + ), + ((1..=1024).contains(&max_header_count), "max_header_count"), + ( + (Duration::from_millis(1)..=Duration::from_mins(2)).contains(&head_read_timeout), + "head_read_timeout_ms", + ), + ] { + if !valid { + return Err(IngressConfigError { field }); + } + } + Ok(Self { + head_read_timeout, + max_connections, + max_header_count, + max_raw_head_bytes, + }) + } + + /// Hyper's logical buffer threshold; allocation overhead is accounted separately. + #[must_use] + #[inline] + pub const fn parser_buffer_bytes(self) -> usize { + self.max_raw_head_bytes + } + + /// Conservative requested metadata capacity per parser/cache header map. + /// Excludes name/value bytes, the fixed map object, allocator overhead and RSS. + /// Audited against http 1.4.2 and Rust 1.95.0, including collision-driven growth. + #[must_use] + #[inline] + pub fn parser_header_map_allocation_bytes(self) -> usize { + let fields = self.max_response_header_count().saturating_add(1); + let raw_slots = 1_usize << fields.saturating_mul(10).ilog2(); + let buckets = raw_slots.saturating_mul(3) >> 2_usize; + let extras = fields.saturating_sub(1).max(4).next_power_of_two(); + let word = size_of::(); + let bucket_bytes = size_of::() + .saturating_add(size_of::()) + .saturating_add(word.saturating_mul(8)); + let extra_bytes = size_of::().saturating_add(word.saturating_mul(6)); + raw_slots + .saturating_mul(4) + .saturating_add(buckets.saturating_mul(bucket_bytes)) + .saturating_add(extras.saturating_mul(extra_bytes)) + } + + /// Conservative capacity bound per receive backing allocation, not RSS. + /// Audited against Hyper 1.12.0, bytes 1.12.1 and Rust 1.95.0. + #[must_use] + #[inline] + pub const fn parser_read_allocation_bytes(self) -> usize { + self.max_raw_head_bytes.saturating_mul(4) + } + + /// Conservative capacity bound for the separate raw chunked-trailer buffer. + #[must_use] + #[inline] + pub const fn parser_trailer_allocation_bytes(self) -> usize { + self.max_raw_head_bytes.next_power_of_two() + } +} + +fn setting( + env: &EnvConfig, + field: &'static str, + default: usize, +) -> Result { + let Some(value) = env.get(&["adapter", "ingress", field]) else { + return Ok(default); + }; + if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(IngressConfigError { field }); + } + value + .parse() + .map_err(|_invalid_integer| IngressConfigError { field }) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use edgezero_core::env_config::EnvConfig; + use edgezero_core::http::HeaderValue; + + use super::AxumIngressConfig; + + #[test] + fn defaults_are_finite_and_effective() { + let config = AxumIngressConfig::default(); + assert_eq!(config.max_connections(), 256); + assert_eq!(config.max_raw_head_bytes(), 0x0001_0000); + assert_eq!(config.max_header_count(), 100); + assert_eq!(config.parser_buffer_bytes(), 0x0001_0000); + assert_eq!(config.parser_read_allocation_bytes(), 0x0004_0000); + assert_eq!(config.parser_trailer_allocation_bytes(), 0x0001_0000); + assert_eq!(config.head_read_timeout(), Duration::from_secs(10)); + assert!(config.parser_header_map_allocation_bytes() > 100 * size_of::()); + assert_eq!( + AxumIngressConfig::from_env(&EnvConfig::default()).unwrap(), + config + ); + } + + #[test] + fn explicit_environment_values_are_validated() { + let env = EnvConfig::from_vars([ + ("EDGEZERO__ADAPTER__INGRESS__MAX_CONNECTIONS", "2"), + ("EDGEZERO__ADAPTER__INGRESS__MAX_RAW_HEAD_BYTES", "8192"), + ("EDGEZERO__ADAPTER__INGRESS__MAX_HEADER_COUNT", "4"), + ("EDGEZERO__ADAPTER__INGRESS__HEAD_READ_TIMEOUT_MS", "25"), + ]); + let config = AxumIngressConfig::from_env(&env).unwrap(); + assert_eq!(config.max_connections(), 2); + assert_eq!(config.max_raw_head_bytes(), 8192); + assert_eq!(config.max_header_count(), 4); + assert_eq!(config.head_read_timeout(), Duration::from_millis(25)); + let non_power_of_two = AxumIngressConfig::new(2, 8193, 4, Duration::from_secs(1)).unwrap(); + assert_eq!(non_power_of_two.parser_buffer_bytes(), 8193); + assert_eq!(non_power_of_two.parser_read_allocation_bytes(), 0x8004); + assert_eq!(non_power_of_two.parser_trailer_allocation_bytes(), 0x4000); + } + + #[test] + fn invalid_explicit_values_never_fall_back() { + for field in [ + "MAX_CONNECTIONS", + "MAX_RAW_HEAD_BYTES", + "MAX_HEADER_COUNT", + "HEAD_READ_TIMEOUT_MS", + ] { + for value in ["", " ", "0", "-1", "+1", "abc", "184467440737095516160"] { + let env = + EnvConfig::from_vars([(format!("EDGEZERO__ADAPTER__INGRESS__{field}"), value)]); + let error = AxumIngressConfig::from_env(&env).unwrap_err(); + assert!(!error.to_string().contains(value) || value.is_empty() || value == " "); + } + } + } + + #[test] + fn programmatic_limits_accept_boundaries_and_reject_overflow() { + AxumIngressConfig::new(1, 8192, 1, Duration::from_millis(1)).unwrap(); + AxumIngressConfig::new(4096, 0x0010_0000, 1024, Duration::from_mins(2)).unwrap(); + for (connections, bytes, count, timeout) in [ + (0, 8192, 1, Duration::from_secs(1)), + (4097, 8192, 1, Duration::from_secs(1)), + (1, 8191, 1, Duration::from_secs(1)), + (1, 0x0010_0001, 1, Duration::from_secs(1)), + (1, 8192, 0, Duration::from_secs(1)), + (1, 8192, 1025, Duration::from_secs(1)), + (1, 8192, 1, Duration::ZERO), + (1, 8192, 1, Duration::from_secs(121)), + ] { + AxumIngressConfig::new(connections, bytes, count, timeout).unwrap_err(); + } + } +} diff --git a/crates/edgezero-adapter-axum/src/lib.rs b/crates/edgezero-adapter-axum/src/lib.rs index f6bca2ce8..ef2004a18 100644 --- a/crates/edgezero-adapter-axum/src/lib.rs +++ b/crates/edgezero-adapter-axum/src/lib.rs @@ -1,28 +1,43 @@ //! Axum adapter for `EdgeZero` routers and applications. +#[cfg(feature = "axum")] +mod config_snapshot; #[cfg(feature = "axum")] pub mod config_store; +pub mod config_store_limits; +#[cfg(feature = "axum")] +mod connection; #[cfg(feature = "axum")] pub mod context; #[cfg(feature = "axum")] pub mod dev_server; #[cfg(feature = "axum")] +mod header_deadline; +pub mod ingress_config; +#[cfg(feature = "axum")] pub mod key_value_store; #[cfg(feature = "axum")] -pub mod proxy; +pub mod outbound; #[cfg(feature = "axum")] pub mod request; #[cfg(feature = "axum")] -pub mod response; +mod response; #[cfg(any(feature = "axum", feature = "cli"))] pub mod secret_env; #[cfg(feature = "axum")] pub mod secret_store; #[cfg(feature = "axum")] -pub mod service; +mod service; -#[cfg(feature = "cli")] +#[cfg(all(feature = "cli", not(target_arch = "wasm32")))] pub mod cli; -#[cfg(test)] +#[cfg(all(feature = "axum", any(test, feature = "test-utils")))] pub mod test_utils; + +/// Native Axum resource limits and accounting are operator-configured. +pub const AXUM_PLATFORM: edgezero_core::PlatformMetadata = edgezero_core::PlatformMetadata::new( + edgezero_core::PlatformFact::unknown(edgezero_core::PlatformUnknownReason::OperatorConfigured), + edgezero_core::PlatformFact::unknown(edgezero_core::PlatformUnknownReason::OperatorConfigured), + edgezero_core::PlatformFact::unknown(edgezero_core::PlatformUnknownReason::OperatorConfigured), +); diff --git a/crates/edgezero-adapter-axum/src/outbound.rs b/crates/edgezero-adapter-axum/src/outbound.rs new file mode 100644 index 000000000..584647a81 --- /dev/null +++ b/crates/edgezero-adapter-axum/src/outbound.rs @@ -0,0 +1,1941 @@ +#![cfg_attr( + test, + expect( + clippy::self_named_module_files, + reason = "the established outbound module keeps private response fixtures in a submodule" + ) +)] + +use async_stream::stream; +use bytes::Bytes; +use core::time::Duration; +use edgezero_core::body::{Body, BodyPayload, BodyStream}; +use edgezero_core::error::{BadGatewayReason, BudgetSource, EdgeError}; +use edgezero_core::error_detail::{ + AxumFailureCause, AxumFailureKind, ProviderFailureDetail, ProviderFailurePhase, +}; +use edgezero_core::http::header::{ACCEPT_ENCODING, HOST}; +use edgezero_core::http::{HeaderMap, HeaderValue, Method, StatusCode}; +use edgezero_core::outbound::{ + OutboundBatch, OutboundBatchDriverEvent, OutboundDispatchRequest, OutboundExchangeOwner, + OutboundFuture, OutboundHead, OutboundHeaderFidelity, OutboundHeaderInput, OutboundHttpClient, + OutboundPending, OutboundPreparedBatch, OutboundRequest, OutboundRequestParts, + OutboundResponse, OutboundResponseContext, OutboundSendMode, OutboundTask, OutboundTime, +}; +use edgezero_core::time::{DispatchBudget, MonotonicClock, MonotonicInstant}; +use edgezero_core::{IndependentOutboundProgress, OutboundProgressRuntime}; +use futures::channel::{mpsc, oneshot}; +use futures_util::StreamExt as _; +use futures_util::future::{FutureExt as _, LocalBoxFuture, poll_fn}; +use futures_util::stream::{FuturesUnordered, once}; +use reqwest::header::HeaderMap as ReqwestHeaderMap; +use reqwest::redirect::Policy; +use std::error::Error as _; +use std::future::Future; +use std::io::Error as IoError; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::rc::Rc; +use std::sync::Arc; +use std::task::Poll; +use tokio::runtime::Handle; +use tokio::task::spawn_local; +use tokio::time::sleep; +#[cfg(test)] +use tokio::time::timeout; + +#[cfg(test)] +mod testing; + +/// Native outbound HTTP implementation used by the Axum adapter. +#[derive(Clone)] +pub struct AxumOutboundClient { + client: Arc, + time: OutboundTime, +} + +impl AxumOutboundClient { + async fn execute(&self, prepared: OutboundDispatchRequest) -> Result { + let (parts, context) = prepared.into_parts(); + let budget = context.budget(); + let time = context.time().clone(); + let OutboundRequestParts { + body, + connect_timeout, + mut headers, + host_authority_override, + max_request_body_bytes, + method, + uri, + .. + } = parts; + + if !headers.contains_key(ACCEPT_ENCODING) { + headers.insert(ACCEPT_ENCODING, HeaderValue::from_static("identity")); + } + if let Some(authority) = host_authority_override { + let value = HeaderValue::from_str(authority.as_str()).map_err(EdgeError::internal)?; + headers.insert(HOST, value); + } + let request_body = + collect_request_body(body, max_request_body_bytes, budget, &time).await?; + let request_builder = self + .client + .request(reqwest_method(&method)?, uri.to_string()) + .headers(headers) + .body(request_body); + let mut previous = time.clock().now(); + let mut timer = time.wait_until(budget.deadline); + let response = race_with_timer( + request_builder.send(), + budget, + &time, + &mut timer, + &mut previous, + None, + |result| { + result + .as_ref() + .err() + .map(|error| reqwest_failure_detail(error, ProviderFailurePhase::Send)) + }, + ) + .await? + .map_err(|error| { + classify_send_error(&error, budget, time.clock(), connect_timeout.is_some()) + })?; + + response_head(response, context) + } + + /// Builds a client with redirects and automatic content decoding disabled. + /// + /// # Errors + /// Returns the underlying client-construction error when TLS initialization fails. + #[inline] + pub fn try_new() -> Result { + Self::try_with_time(native_time(MonotonicClock::default())) + } + + pub(crate) fn try_transport() -> Result, reqwest::Error> { + reqwest::Client::builder() + .redirect(Policy::none()) + .no_brotli() + .no_deflate() + .no_gzip() + .no_zstd() + .build() + .map(Arc::new) + } + + /// Builds a client using the supplied monotonic clock and cancellable timer factory. + /// + /// # Errors + /// Returns the underlying client-construction error when TLS initialization fails. + #[inline] + pub fn try_with_time(time: OutboundTime) -> Result { + Self::try_transport().map(|client| Self::with_transport_and_time(client, time)) + } + + fn validate_options(request: &OutboundRequest) -> Result<(), EdgeError> { + if request.image_optimizer_options().is_some() { + return Err(EdgeError::bad_request( + "image_optimizer is not supported by axum", + )); + } + Ok(()) + } + + pub(crate) fn with_transport_and_time( + client: Arc, + time: OutboundTime, + ) -> Self { + Self { client, time } + } +} + +impl OutboundHttpClient for AxumOutboundClient { + #[inline] + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let scope = request.scope().clone(); + let time = request.time().clone(); + let entered_at = request.entered_at(); + let head = self.dispatch_head(request); + schedule_response(scope.run( + time, + entered_at, + Box::pin(async move { head.await?.finish().await }), + )) + } + + #[inline] + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + let client = self.clone(); + let scope = request.scope().clone(); + scope.run( + request.time().clone(), + request.entered_at(), + Box::pin(async move { client.execute(request).await }), + ) + } + + #[inline] + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + #[inline] + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + schedule_batch(self, batch) + } + + #[inline] + fn time(&self) -> OutboundTime { + self.time.clone() + } + + #[inline] + fn validate_supported_options( + &self, + request: &OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + Self::validate_options(request) + } +} + +fn schedule_batch(client: &dyn OutboundHttpClient, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, slots) = batch.into_slots(); + if slots.is_empty() { + return OutboundBatch::cutoff(context); + } + let cutoff = context.observation_cutoff(); + let (mut sender, receiver) = mpsc::channel(slots.len().saturating_add(1)); + let mut work = FuturesUnordered::new(); + for (index, request) in slots { + let budget = request.budget(); + let clock = request.clock().clone(); + let scope = request.scope().clone(); + let time = request.time().clone(); + let entered_at = request.entered_at(); + let head = client.dispatch_head(request); + let pipeline = scope.run( + time, + entered_at, + Box::pin(async move { head.await?.finish().await }), + ); + work.push( + async move { + let outcome = pipeline.await; + (index, clock.now(), budget, outcome) + } + .boxed_local(), + ); + } + let task = Box::pin(async move { + let mut cutoff_unresolved = false; + while let Some((index, completed_at, budget, outcome)) = work.next().await { + cutoff_unresolved |= cutoff.is_expired_at(completed_at) + && !budget.own_deadline_expired_before(cutoff, completed_at); + if sender + .try_send(OutboundBatchDriverEvent::Item { + index, + completed_at, + outcome, + }) + .is_err() + { + return; + } + } + if cutoff_unresolved { + let _closed = sender.try_send(OutboundBatchDriverEvent::Cutoff); + } + }); + match spawn_owned(task) { + Ok(owner) => OutboundBatch::from_driver(context, receiver, owner), + Err(error) => OutboundBatch::from_driver( + context, + once(async move { OutboundBatchDriverEvent::Failed(error) }), + OutboundExchangeOwner::new(|| {}), + ), + } +} + +fn spawn_owned(work: LocalBoxFuture<'static, ()>) -> Result { + Handle::try_current().map_err(|_missing| { + EdgeError::internal(anyhow::anyhow!( + "outbound work requires a Tokio local scheduler" + )) + })?; + let (runner, owner) = OutboundTask::new(work)?; + let task = catch_unwind(AssertUnwindSafe(|| spawn_local(runner))).map_err(|_missing| { + EdgeError::internal(anyhow::anyhow!("outbound work requires a Tokio LocalSet")) + })?; + let abort = task.abort_handle(); + Ok(OutboundExchangeOwner::new(move || { + drop(owner); + abort.abort(); + })) +} + +fn schedule_response(work: OutboundFuture) -> Result { + let (sender, receiver) = oneshot::channel(); + let owner = spawn_owned(Box::pin(async move { + let _closed = sender.send(work.await); + }))?; + Ok(OutboundPending::new( + Box::pin(async move { + receiver.await.map_err(|_closed| { + EdgeError::internal(anyhow::anyhow!("outbound completion task stopped")) + })? + }), + owner, + )) +} + +async fn collect_request_body( + body: Body, + maximum: u64, + budget: DispatchBudget, + time: &OutboundTime, +) -> Result { + let clock = time.clock(); + let (payload, _scope) = body.into_parts(); + match payload { + BodyPayload::Once(bytes) => { + let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX); + if length > maximum { + return Err(EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + )); + } + budget_remaining(budget, clock)?; + Ok(bytes) + } + BodyPayload::Stream(mut source) => { + let mut collected = Vec::new(); + let mut total = 0_u64; + let mut previous = clock.now(); + let mut timer = time.wait_until(budget.deadline); + loop { + let next_item = race_with_timer( + source.next(), + budget, + time, + &mut timer, + &mut previous, + None, + |item| { + item.as_ref() + .and_then(|outcome| outcome.as_ref().err()) + .and_then(EdgeError::provider_failure_detail) + .copied() + }, + ) + .await?; + budget_remaining(budget, clock).map_err(|selected| { + let detail = next_item + .as_ref() + .and_then(|item| item.as_ref().err()) + .and_then(EdgeError::provider_failure_detail) + .copied(); + preserve_timeout_detail(selected, detail) + })?; + let Some(item) = next_item else { + return Ok(Bytes::from(collected)); + }; + let bytes = item?; + let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX); + let Some(next_total) = total.checked_add(length) else { + return Err(EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + )); + }; + if next_total > maximum { + return Err(EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + )); + } + collected.extend_from_slice(&bytes); + total = next_total; + } + } + } +} + +fn response_head( + response: reqwest::Response, + context: OutboundResponseContext, +) -> Result { + let budget = context.budget(); + let clock = context.clock().clone(); + budget_remaining(budget, &clock)?; + let status = StatusCode::from_u16(response.status().as_u16()).map_err(EdgeError::internal)?; + let headers = OutboundHeaderInput::new( + "axum", + copy_headers(response.headers()), + OutboundHeaderFidelity::AdapterVisibleFields, + ); + let encoded = response_stream(response, budget, clock); + OutboundHead::new( + status, + headers, + encoded, + context, + OutboundExchangeOwner::new(|| {}), + ) +} + +fn budget_remaining(budget: DispatchBudget, clock: &MonotonicClock) -> Result { + budget + .deadline + .remaining_at(clock.now()) + .map(|remaining| remaining.min(budget.duration)) + .ok_or_else(|| timeout_error(budget.cause)) +} + +fn classify_send_error( + error: &reqwest::Error, + budget: DispatchBudget, + clock: &MonotonicClock, + connect_timeout_configured: bool, +) -> EdgeError { + let classified = if budget.deadline.is_expired_at(clock.now()) { + timeout_error(budget.cause) + } else if error.is_timeout() && error.is_connect() && connect_timeout_configured { + timeout_error(BudgetSource::ConnectTimeout) + } else if error.is_timeout() { + timeout_error(BudgetSource::Unspecified) + } else { + let reason = if error.is_connect() { + BadGatewayReason::Unreachable + } else if error.is_builder() { + BadGatewayReason::Protocol + } else { + BadGatewayReason::Transport + }; + EdgeError::bad_gateway_with_reason("upstream request failed", reason) + }; + diagnose_reqwest_error(classified, error, ProviderFailurePhase::Send) +} + +fn diagnose_reqwest_error( + mut classified: EdgeError, + source: &reqwest::Error, + phase: ProviderFailurePhase, +) -> EdgeError { + if let EdgeError::BadGateway { + provider_detail, .. + } + | EdgeError::GatewayTimeout { + provider_detail, .. + } = &mut classified + { + *provider_detail = Some(reqwest_failure_detail(source, phase)); + } + classified +} + +fn reqwest_failure_detail( + error: &reqwest::Error, + phase: ProviderFailurePhase, +) -> ProviderFailureDetail { + let kind = if error.is_builder() { + AxumFailureKind::Builder + } else if error.is_redirect() { + AxumFailureKind::Redirect + } else if error.is_status() { + AxumFailureKind::Status + } else if error.is_request() { + AxumFailureKind::Request + } else if error.is_body() { + AxumFailureKind::Body + } else if error.is_decode() { + AxumFailureKind::Decode + } else if error.is_upgrade() { + AxumFailureKind::Upgrade + } else { + AxumFailureKind::Unknown + }; + let mut io_kind = None; + let mut source = error.source(); + for _ in 0_u8..32 { + let Some(current) = source else { + break; + }; + if let Some(io) = current.downcast_ref::() { + io_kind = Some(io.kind()); + break; + } + source = current.source(); + } + ProviderFailureDetail::Axum { + phase, + cause: AxumFailureCause { + io_kind, + is_connect: error.is_connect(), + is_timeout: error.is_timeout(), + kind, + }, + } +} + +fn copy_headers(headers: &ReqwestHeaderMap) -> HeaderMap { + let mut copied = HeaderMap::with_capacity(headers.len()); + for (name, value) in headers { + copied.append(name.clone(), value.clone()); + } + copied +} + +fn reqwest_method(method: &Method) -> Result { + reqwest::Method::from_bytes(method.as_str().as_bytes()).map_err(EdgeError::internal) +} + +fn response_stream( + mut response: reqwest::Response, + budget: DispatchBudget, + clock: MonotonicClock, +) -> BodyStream { + stream! { + loop { + match response.chunk().await { + Ok(Some(bytes)) => yield Ok(bytes), + Ok(None) => return, + Err(error) => { + let classified = if budget.deadline.is_expired_at(clock.now()) || error.is_timeout() { + timeout_error(budget.cause) + } else { + EdgeError::bad_gateway_with_reason( + "upstream response body failed", + BadGatewayReason::Transport, + ) + }; + yield Err(diagnose_reqwest_error(classified, &error, ProviderFailurePhase::ResponseBodyRead)); + return; + } + } + } + } + .boxed_local() +} + +fn timeout_error(cause: BudgetSource) -> EdgeError { + EdgeError::gateway_timeout_caused("outbound request deadline expired", cause) +} + +fn preserve_timeout_detail( + mut selected: EdgeError, + detail: Option, +) -> EdgeError { + if let EdgeError::GatewayTimeout { + provider_detail, .. + } = &mut selected + { + *provider_detail = detail; + } + selected +} + +pub(crate) fn native_time(clock: MonotonicClock) -> OutboundTime { + OutboundTime::new(clock, |deadline, paired_clock| { + Box::pin(async move { + if Handle::try_current().is_err() { + return Err(EdgeError::internal(anyhow::anyhow!( + "outbound timer requires a Tokio runtime" + ))); + } + let Some(remaining) = deadline.remaining_at(paired_clock.now()) else { + return Ok(()); + }; + AssertUnwindSafe(async move { + sleep(remaining).await; + }) + .catch_unwind() + .await + .map_err(|_panic| EdgeError::internal(anyhow::anyhow!("outbound timer is unavailable"))) + }) + }) +} + +fn checked_now( + time: &OutboundTime, + previous: &mut MonotonicInstant, +) -> Result { + let now = time.clock().now(); + if now < *previous { + return Err(EdgeError::internal(anyhow::anyhow!( + "outbound clock moved backwards during I/O" + ))); + } + *previous = now; + Ok(now) +} + +async fn race_with_timer( + future: impl Future, + budget: DispatchBudget, + time: &OutboundTime, + timer: &mut OutboundFuture<()>, + previous: &mut MonotonicInstant, + expiry_detail: Option, + detail: impl FnOnce(&Output) -> Option, +) -> Result { + let mut extract_detail = Some(detail); + futures_util::pin_mut!(future); + poll_fn(|cx| { + checked_now(time, previous)?; + if let Poll::Ready(result) = timer.as_mut().poll(cx) { + result?; + checked_now(time, previous)?; + return Poll::Ready(Err(preserve_timeout_detail( + timeout_error(budget.cause), + expiry_detail, + ))); + } + if budget.deadline.is_expired_at(checked_now(time, previous)?) { + return Poll::Ready(Err(preserve_timeout_detail( + timeout_error(budget.cause), + expiry_detail, + ))); + } + let output = future.as_mut().poll(cx); + if budget.deadline.is_expired_at(checked_now(time, previous)?) { + let observed_detail = if let Poll::Ready(value) = &output { + extract_detail.take().and_then(|extract| extract(value)) + } else { + None + }; + return Poll::Ready(Err(preserve_timeout_detail( + timeout_error(budget.cause), + observed_detail.or(expiry_detail), + ))); + } + output.map(Ok) + }) + .await +} + +#[cfg(test)] +mod clock_tests { + + use super::testing::{finish_fixture_response, finish_fixture_stream}; + use edgezero_core::outbound::{ + HttpClient, OutboundBatchTermination, OutboundCollectedSlot, OutboundProxyMarker, + ResponseDecoding, ResponseMode, + }; + use std::cell::{Cell, RefCell}; + use std::collections::VecDeque; + use std::io::{Error as IoError, ErrorKind}; + use std::iter::repeat_with; + use std::pin::Pin; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll}; + + use edgezero_core::http::response_builder; + use edgezero_core::response::IntoResponse as _; + use edgezero_core::time::DEFAULT_NO_DEADLINE_BUDGET; + use edgezero_core::time::Deadline; + use edgezero_core::{ + AxumFailureCause, AxumFailureKind, OutboundScopeOwner, ProviderFailureDetail, + ProviderFailurePhase, + }; + use futures::executor::block_on; + use futures_util::future::pending; + use futures_util::stream; + use http_body::Frame; + use tokio::net::TcpListener; + use tokio::task::{LocalSet, yield_now}; + + use super::*; + + struct NeverReadyBody(Arc); + + struct FailingBody(Option); + + #[expect( + clippy::missing_trait_methods, + reason = "default hints describe this failing test body" + )] + impl http_body::Body for FailingBody { + type Data = Bytes; + type Error = IoError; + + fn poll_frame( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + Poll::Ready(self.get_mut().0.take().map(Err)) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "default size and EOF hints describe this never-ready test source" + )] + impl http_body::Body for NeverReadyBody { + type Data = Bytes; + type Error = IoError; + + fn poll_frame( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + self.0.fetch_add(1, Ordering::Relaxed); + Poll::Pending + } + } + + #[tokio::test(flavor = "current_thread")] + async fn streamed_send_keeps_original_scope_under_foreign_request_polling() { + LocalSet::new() + .run_until(async { + let original = OutboundScopeOwner::callback(); + let foreign = OutboundScopeOwner::callback(); + let client = + HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let nested_client = client.clone(); + let nested = Rc::new(RefCell::new(None::>)); + let retained = Rc::clone(&nested); + let upload = Rc::new(()); + let released_upload = Rc::downgrade(&upload); + let child = Rc::new(()); + let released_child = Rc::downgrade(&child); + let mut pending_child = Some(child); + let body = Body::from_stream(stream::poll_fn(move |_| { + assert_eq!(Rc::strong_count(&upload), 1); + if let Some(owner) = pending_child.take() { + let source = Body::from_stream(stream::poll_fn(move |_| { + assert_eq!(Rc::strong_count(&owner), 1); + Poll::>>::Pending + })); + let send = nested_client.send( + OutboundRequest::post("http://127.0.0.1:9/") + .expect("child request") + .body(source), + ); + *retained.borrow_mut() = Some(send); + } + Poll::>>::Pending + })); + let mut send = original.with(|| { + client.send( + OutboundRequest::post("http://127.0.0.1:9/") + .expect("request") + .body(body), + ) + }); + foreign + .run(poll_fn(|cx| { + assert!(send.as_mut().poll(cx).is_pending()); + Poll::Ready(()) + })) + .await; + assert!(nested.borrow().is_some(), "upload poll creates child send"); + drop(foreign); + assert!( + released_child.upgrade().is_some(), + "foreign scope cannot cancel child" + ); + drop(original); + assert!( + released_upload.upgrade().is_none(), + "send upload is revoked" + ); + assert!( + released_child.upgrade().is_none(), + "child belongs to original scope" + ); + assert!(nested.borrow().is_some(), "child future is still retained"); + drop(send); + drop(nested.borrow_mut().take()); + }) + .await; + } + + #[tokio::test] + async fn missing_local_scheduler_never_polls_owned_upload() { + let polls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&polls); + let source = stream::poll_fn(move |_| { + observed.fetch_add(1, Ordering::SeqCst); + Poll::Pending + }) + .boxed_local(); + let pair = native_time(MonotonicClock::default()); + let budget = test_budget(pair.clock().now(), Duration::from_secs(1)); + let pending = schedule_response(Box::pin(async move { + collect_request_body(Body::from_stream(source), 8, budget, &pair).await?; + Err(EdgeError::internal(anyhow::anyhow!( + "fixture upload unexpectedly completed" + ))) + })); + assert!(matches!(pending, Err(EdgeError::Internal { .. }))); + assert_eq!(polls.load(Ordering::SeqCst), 0); + } + + #[test] + fn supplied_timer_setup_error_precedes_upload_polling() { + let reads = Arc::new(AtomicUsize::new(0)); + let captured = Arc::clone(&reads); + let source = stream::poll_fn(move |_context| { + captured.fetch_add(1, Ordering::SeqCst); + Poll::Pending + }); + let pair = OutboundTime::new(MonotonicClock::default(), |_deadline, _clock| { + Box::pin(async { + Err(EdgeError::internal(anyhow::anyhow!( + "fixture timer setup failed" + ))) + }) + }); + let client = HttpClient::with_client(AxumOutboundClient::try_with_time(pair).unwrap()); + let request = OutboundRequest::post("http://127.0.0.1:9/upload") + .unwrap() + .body(Body::from_stream(source)); + let error = block_on(client.send(request)).expect_err("timer setup"); + assert!(matches!(error, EdgeError::Internal { .. })); + assert_eq!(reads.load(Ordering::SeqCst), 0); + } + + #[tokio::test(flavor = "current_thread")] + async fn closed_scope_rejects_scheduling_before_work_is_polled() { + LocalSet::new() + .run_until(async { + let callback = OutboundScopeOwner::callback(); + callback.close(); + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let resource = Rc::new(()); + let released = Rc::downgrade(&resource); + let refused = callback.with(|| { + schedule_response(Box::pin(poll_fn(move |_| { + assert_eq!(Rc::strong_count(&resource), 1); + observed.set(observed.get().saturating_add(1)); + Poll::Pending + }))) + }); + assert!(matches!(refused, Err(EdgeError::Internal { .. }))); + assert_eq!(polls.get(), 0); + assert!(released.upgrade().is_none(), "refused work is released"); + }) + .await; + } + + #[tokio::test(flavor = "current_thread")] + async fn local_set_drop_releases_work_and_fails_retained_manual_pending() { + let local = LocalSet::new(); + let resource = Rc::new(()); + let released = Rc::downgrade(&resource); + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let mut pending = local + .run_until(async move { + let work = poll_fn(move |_| { + assert_eq!(Rc::strong_count(&resource), 1); + observed.set(observed.get().saturating_add(1)); + Poll::Pending + }); + let retained = schedule_response(Box::pin(work)).expect("manual pending"); + yield_now().await; + retained + }) + .await; + assert_eq!(polls.get(), 1, "scheduled work entered pending"); + assert!(released.upgrade().is_some()); + drop(local); + assert!( + released.upgrade().is_none(), + "executor drop releases owned work" + ); + assert!(pending.is_ready(), "destroyed producer closes its receiver"); + let error = timeout(Duration::from_secs(1), pending.collect()) + .await + .expect("completion failure is bounded") + .expect_err("stopped task cannot complete successfully"); + assert!(matches!(error, EdgeError::Internal { .. })); + assert!( + error + .to_string() + .contains("outbound completion task stopped") + ); + assert_eq!(polls.get(), 1, "executor destruction does not repoll work"); + } + + #[tokio::test(flavor = "current_thread")] + async fn scheduled_work_allows_reentrant_cancellation_while_polling() { + LocalSet::new() + .run_until(async { + let owner = Rc::new(RefCell::new(None::)); + let cancelled = Rc::clone(&owner); + let panicked = Rc::new(Cell::new(false)); + let observed = Rc::clone(&panicked); + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let resource = Rc::new(()); + let released = Rc::downgrade(&resource); + let work = poll_fn(move |_| { + assert_eq!(Rc::strong_count(&resource), 1); + counted.set(counted.get().saturating_add(1)); + let cancel_owner = cancelled.borrow_mut().take(); + observed.set(catch_unwind(AssertUnwindSafe(|| drop(cancel_owner))).is_err()); + Poll::Pending + }) + .boxed_local(); + *owner.borrow_mut() = Some(spawn_owned(work).expect("local scheduler")); + yield_now().await; + assert_eq!(polls.get(), 1, "cancelled work must not be polled again"); + assert!( + !panicked.get(), + "cancellation must not borrow the future being polled" + ); + assert!( + released.upgrade().is_none(), + "cancelled pipeline releases its source" + ); + }) + .await; + } + + #[test] + fn native_timer_without_runtime_returns_internal_error() { + let pair = native_time(MonotonicClock::default()); + let deadline = pair.clock().deadline_after(Duration::from_secs(1)); + let error = block_on(pair.wait_until(deadline)).expect_err("no runtime"); + assert!(matches!(error, EdgeError::Internal { .. })); + assert!(error.to_string().contains("Tokio runtime")); + } + + #[test] + fn native_timer_without_timer_driver_returns_internal_error() { + use tokio::runtime::Builder; + + let runtime = Builder::new_current_thread().build().expect("runtime"); + let pair = native_time(MonotonicClock::default()); + let deadline = pair.clock().deadline_after(Duration::from_secs(1)); + let error = runtime + .block_on(pair.wait_until(deadline)) + .expect_err("no timer driver"); + assert!(matches!(error, EdgeError::Internal { .. })); + assert!(error.to_string().contains("timer is unavailable")); + } + + #[tokio::test] + async fn review_cross_chunk_regression_releases_retained_source() { + use std::future::pending; + + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(10)); + let advanced = start.checked_add(Duration::from_millis(2)).unwrap(); + let regressed = start.checked_add(Duration::from_millis(1)).unwrap(); + let observed = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&observed); + let pair = native_time(MonotonicClock::new(move || *sampled.lock().unwrap())); + let owner = Arc::new(()); + let released = Arc::downgrade(&owner); + let source_clock = Arc::clone(&observed); + let source = stream! { + *source_clock.lock().unwrap() = advanced; + yield Ok(Bytes::from_static(b"first")); + yield Ok(Bytes::from_static(b"second")); + pending::<()>().await; + drop(owner); + } + .boxed_local(); + let mut guarded = finish_fixture_stream(source, budget, pair) + .await + .expect("shared response stream"); + assert_eq!( + guarded.next().await.unwrap().unwrap(), + Bytes::from_static(b"first") + ); + *observed.lock().unwrap() = regressed; + let error = guarded + .next() + .await + .unwrap() + .expect_err("cross-chunk clock regression"); + assert!(matches!(error, EdgeError::Internal { .. })); + assert!( + released.upgrade().is_none(), + "release before terminal wrapper drop" + ); + } + + #[test] + fn image_optimizer_rejection_precedes_pending_body_and_expired_budget() { + use edgezero_core::OutboundImageOptimizerOptions; + use futures::executor::block_on; + use std::cell::Cell; + use std::rc::Rc; + + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let body = Body::from_stream(stream::poll_fn(move |_| { + counted.set(counted.get().saturating_add(1)); + Poll::Pending + })); + let request = OutboundRequest::get("http://127.0.0.1:9/image") + .unwrap() + .image_optimizer(OutboundImageOptimizerOptions::new()) + .body(body) + .timeout(Duration::ZERO); + let client = HttpClient::with_client(AxumOutboundClient::try_new().unwrap()); + let error = block_on(client.send(request)).expect_err("unsupported"); + assert!(matches!(error, EdgeError::BadRequest { .. })); + assert!(error.message().contains("image_optimizer")); + assert!(error.message().contains("axum")); + assert_eq!(polls.get(), 0); + } + + #[tokio::test(flavor = "current_thread")] + async fn image_optimizer_batch_rejection_keeps_live_plain_sibling() { + LocalSet::new().run_until(async { + use edgezero_core::OutboundImageOptimizerOptions; + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let target = format!("http://{}/plain", listener.local_addr().unwrap()); + let server = tokio::spawn(async move { + let (mut socket, _) = timeout(Duration::from_secs(2), listener.accept()) + .await + .unwrap() + .unwrap(); + let mut head = Vec::new(); + while !head.ends_with(b"\r\n\r\n") { + assert!(head.len() < 1024, "bounded fixture request head"); + head.push(socket.read_u8().await.unwrap()); + } + assert!(head.starts_with(b"GET /plain HTTP/1.1\r\n")); + socket + .write_all(b"HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n") + .await + .unwrap(); + }); + let client = HttpClient::with_client(AxumOutboundClient::try_new().unwrap()); + let image = OutboundRequest::get(&target) + .unwrap() + .image_optimizer(OutboundImageOptimizerOptions::new()) + .body(Body::from_stream(stream::pending())) + .timeout(Duration::ZERO); + let plain = OutboundRequest::get(&target).unwrap(); + let results = client + .start_batch_until(vec![image, plain], Deadline::after(Duration::from_secs(2))) + .collect() + .await + .unwrap(); + assert_eq!( + results.termination(), + edgezero_core::OutboundBatchTermination::Completed + ); + assert!(matches!( + results.slots().first(), + Some(OutboundCollectedSlot::Resolved(edgezero_core::OutboundSlotResult { + outcome: Err(EdgeError::BadRequest { .. }), + .. + })) + )); + assert!( + matches!(results.slots().get(1), Some(OutboundCollectedSlot::Resolved(edgezero_core::OutboundSlotResult { + outcome: Ok(response), .. + })) if response.status() == StatusCode::NO_CONTENT) + ); + server.await.unwrap(); + + }).await; + } + + fn scripted_clock(script: Vec) -> MonotonicClock { + let observations = Arc::new(Mutex::new(VecDeque::from(script))); + MonotonicClock::new(move || { + let mut samples = observations.lock().expect("clock observations"); + let now = *samples.front().expect("clock observation"); + if samples.len() > 1 { + samples.pop_front(); + } + now + }) + } + + fn test_budget(start: MonotonicInstant, duration: Duration) -> DispatchBudget { + DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant(start.checked_add(duration).expect("deadline")), + duration, + } + } + + #[tokio::test(flavor = "current_thread")] + async fn provider_diagnostics_survive_post_ready_upload_and_response_expiry() { + let detail = ProviderFailureDetail::Axum { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: AxumFailureCause { + kind: AxumFailureKind::Decode, + is_connect: false, + is_timeout: false, + io_kind: Some(ErrorKind::UnexpectedEof), + }, + }; + for upload in [false, true] { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + let observed = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&observed); + let clock = MonotonicClock::new(move || *sampled.lock().unwrap()); + let source = stream::once(async move { + *observed.lock().unwrap() = budget.deadline.instant(); + Err(EdgeError::bad_gateway_with_detail( + "source failed", + BadGatewayReason::Transport, + detail, + )) + }) + .boxed_local(); + let error = if upload { + collect_request_body(Body::from_stream(source), 32, budget, &native_time(clock)) + .await + .expect_err("late upload error") + } else { + finish_fixture_stream(source, budget, native_time(clock)) + .await + .expect("shared response stream") + .next() + .await + .expect("terminal") + .expect_err("late response error") + }; + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); + assert_eq!(error.provider_failure_detail(), Some(&detail)); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn reported_connect_timeout_requires_setting_and_preserves_governing_deadline() { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("origin"); + let target = format!("https://{}/", listener.local_addr().expect("address")); + let server = tokio::spawn(async move { + let (_socket, _) = listener.accept().await.expect("connection"); + pending::<()>().await; + }); + let error = reqwest::Client::builder() + .no_proxy() + .connect_timeout(Duration::from_millis(20)) + .build() + .expect("native client") + .get(target) + .send() + .await + .expect_err("stalled TLS"); + server.abort(); + let _cancelled = server.await; + assert!(error.is_timeout() && error.is_connect()); + let start = MonotonicInstant::now(); + for configured in [false, true] { + for cause in [ + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::BatchCutoff, + BudgetSource::Default, + ] { + let mut budget = test_budget(start, Duration::from_secs(1)); + budget.cause = cause; + for offset in [999_u64, 1000, 1001] { + let observed = start + .checked_add(Duration::from_millis(offset)) + .expect("observation"); + let clock = MonotonicClock::new(move || observed); + let mapped = classify_send_error(&error, budget, &clock, configured); + let expected = if offset >= 1000 { + cause + } else if configured { + BudgetSource::ConnectTimeout + } else { + BudgetSource::Unspecified + }; + assert!( + matches!(mapped, EdgeError::GatewayTimeout { cause: actual, .. } if actual == expected), + "configured={configured}, offset={offset}, {mapped:?}" + ); + assert_eq!( + mapped.provider_failure_detail(), + Some(&reqwest_failure_detail(&error, ProviderFailurePhase::Send)) + ); + } + } + } + } + + #[tokio::test(flavor = "current_thread")] + async fn clamped_batch_cutoff_uses_actual_observation_cutoff() { + LocalSet::new() + .run_until(async { + for (elapsed_hours, unresolved) in [(168_u64, false), (192, true)] { + let start = MonotonicInstant::now(); + let now = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&now); + let clock = MonotonicClock::new(move || *sampled.lock().expect("clock")); + let client = HttpClient::with_client( + AxumOutboundClient::try_with_time(native_time(clock)).expect("client"), + ); + let cutoff = Deadline::at_instant( + start + .checked_add(Duration::from_hours(192)) + .expect("eight-day cutoff"), + ); + let requests = repeat_with(|| { + OutboundRequest::get("http://127.0.0.1:9/") + .expect("request") + .timeout(Duration::from_hours(192)) + }) + .take(2) + .collect(); + let batch = client.start_batch_until(requests, cutoff); + *now.lock().expect("clock") = start + .checked_add(Duration::from_hours(elapsed_hours)) + .expect("terminal observation"); + let results = batch + .collect() + .await + .expect("clamped observation remains a valid driver"); + let expected = if unresolved { + OutboundBatchTermination::Cutoff + } else { + OutboundBatchTermination::Completed + }; + assert_eq!(results.termination(), expected); + assert_eq!(results.slots().len(), 2); + for slot in results.slots() { + if unresolved { + assert!(matches!(slot, OutboundCollectedSlot::Unresolved)); + } else { + assert!(matches!(slot, OutboundCollectedSlot::Resolved(result) if matches!(result.outcome, Err(EdgeError::GatewayTimeout { cause: BudgetSource::BatchCutoff, .. })))); + } + } + } + }) + .await; + } + + #[tokio::test(flavor = "current_thread")] + async fn provider_diagnostics_preserve_refused_connection_without_url_text() { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("listener"); + let address = listener.local_addr().expect("address"); + drop(listener); + let error = reqwest::Client::builder() + .no_proxy() + .build() + .expect("client") + .get(format!("http://{address}/?token=provider-secret-token")) + .timeout(Duration::from_secs(2)) + .send() + .await + .expect_err("closed local port"); + assert!(error.to_string().contains("provider-secret-token")); + assert!(error.is_connect()); + let start = MonotonicInstant::now(); + for expired in [false, true] { + let clock = MonotonicClock::new(move || start); + let mut budget = test_budget(start, Duration::from_secs(1)); + if expired { + budget.deadline = Deadline::at_instant(start); + } + let mapped = classify_send_error(&error, budget, &clock, false); + assert_eq!( + mapped.provider_failure_detail(), + Some(&ProviderFailureDetail::Axum { + phase: ProviderFailurePhase::Send, + cause: AxumFailureCause { + kind: AxumFailureKind::Request, + is_connect: true, + is_timeout: false, + io_kind: Some(ErrorKind::ConnectionRefused), + }, + }) + ); + let plain = if expired { + timeout_error(budget.cause) + } else { + EdgeError::bad_gateway_with_reason( + "upstream request failed", + BadGatewayReason::Unreachable, + ) + }; + assert_same_public_error(plain, mapped); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn provider_diagnostics_preserve_reqwest_body_source_kind() { + let source = FailingBody(Some(IoError::new( + ErrorKind::UnexpectedEof, + "provider-secret-token", + ))); + let response = reqwest::Response::from( + response_builder() + .body(reqwest::Body::wrap(source)) + .expect("response"), + ); + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + let mut stream = response_stream(response, budget, MonotonicClock::new(move || start)); + let mapped = stream + .next() + .await + .expect("source failure") + .expect_err("body failure"); + assert_eq!( + mapped.provider_failure_detail(), + Some(&ProviderFailureDetail::Axum { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: AxumFailureCause { + kind: AxumFailureKind::Decode, + is_connect: false, + is_timeout: false, + io_kind: Some(ErrorKind::UnexpectedEof), + }, + }) + ); + assert!(stream.next().await.is_none()); + assert_same_public_error( + EdgeError::bad_gateway_with_reason( + "upstream response body failed", + BadGatewayReason::Transport, + ), + mapped, + ); + } + + fn assert_same_public_error(plain: EdgeError, diagnosed: EdgeError) { + assert_eq!(plain.to_string(), diagnosed.to_string()); + assert_eq!(plain.message(), diagnosed.message()); + assert_eq!(plain.kind(), diagnosed.kind()); + assert_eq!(plain.status(), diagnosed.status()); + assert!( + !format!("{:?}", diagnosed.provider_failure_detail()).contains("provider-secret-token") + ); + let before = plain.into_response().expect("plain wire"); + let after = diagnosed.into_response().expect("diagnosed wire"); + assert_eq!(before.status(), after.status()); + assert_eq!(before.headers(), after.headers()); + assert_eq!(before.body().as_bytes(), after.body().as_bytes()); + } + + #[tokio::test(flavor = "current_thread")] + async fn upload_cap_boundaries_preserve_typed_origins() { + let start = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || start); + let budget = test_budget(start, Duration::from_secs(1)); + for len in [7_usize, 8, 9] { + for streamed in [false, true] { + let bytes = Bytes::from(vec![b'x'; len]); + let body = if streamed { + Body::stream(stream::iter([bytes])) + } else { + Body::from(bytes) + }; + let result = + collect_request_body(body, 8, budget, &native_time(clock.clone())).await; + if len > 8 { + let error = result.expect_err("upload overflow"); + assert_eq!(error.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!( + error.request_body_overflow_origin(), + Some(edgezero_core::RequestBodyOverflowOrigin::OutboundUpload) + ); + } else { + assert_eq!(result.expect("body").len(), len); + } + } + } + let source_error = + EdgeError::payload_too_large(edgezero_core::RequestBodyOverflowOrigin::Inbound); + let body = Body::from_stream(stream::iter([Err(source_error)])); + let error = collect_request_body(body, 8, budget, &native_time(clock)) + .await + .expect_err("source overflow"); + assert_eq!( + error.request_body_overflow_origin(), + Some(edgezero_core::RequestBodyOverflowOrigin::Inbound) + ); + } + + #[tokio::test] + async fn marker_off_preserves_exact_upstream_header_budgets() { + let bytes = u64::try_from(edgezero_core::PROXY_HEADER.len() + "upstream".len() + 2) + .expect("header bytes"); + for (max_bytes, max_count, reason) in [ + ( + Some(bytes), + None, + edgezero_core::ResponseLimitReason::HeaderBytes, + ), + ( + None, + Some(2), + edgezero_core::ResponseLimitReason::HeaderCount, + ), + ] { + for setting in [ + OutboundProxyMarker::Off, + OutboundProxyMarker::On, + OutboundProxyMarker::Inherit, + ] { + let native = http::Response::builder() + .status(StatusCode::NO_CONTENT) + .header(edgezero_core::PROXY_HEADER, "upstream") + .header("x", "y") + .body(reqwest::Body::from(Bytes::new())) + .expect("native response"); + let start = MonotonicInstant::now(); + let result = finish_fixture_response( + reqwest::Response::from(native), + Method::GET, + setting, + ResponseDecoding::Decode, + ResponseMode::Buffered { max_bytes: 0 }, + test_budget(start, Duration::from_secs(1)), + 0, + 24, + None, + Some(0), + Some(0), + max_bytes, + max_count, + native_time(MonotonicClock::new(move || start)), + ) + .await; + if setting == OutboundProxyMarker::Off { + assert_eq!( + result + .expect("exact upstream caps") + .headers() + .get(edgezero_core::PROXY_HEADER) + .expect("upstream marker"), + "upstream" + ); + } else { + assert!( + matches!(result, Err(EdgeError::ResponseTooLarge { reason: actual, .. }) if actual == reason) + ); + } + } + } + } + + #[tokio::test] + async fn response_conversion_checks_deadline_before_handoff() { + for (method, status) in [ + (Method::HEAD, StatusCode::OK), + (Method::GET, StatusCode::NOT_MODIFIED), + (Method::GET, StatusCode::NO_CONTENT), + (Method::GET, StatusCode::RESET_CONTENT), + (Method::GET, StatusCode::OK), + ] { + let modes: &[ResponseMode] = if method == Method::GET && status == StatusCode::OK { + &[ResponseMode::Streamed] + } else { + &[ + ResponseMode::Buffered { max_bytes: 1 }, + ResponseMode::Streamed, + ] + }; + for &response_mode in modes { + for cause in [ + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::BatchCutoff, + BudgetSource::Default, + ] { + for offset in [9_u64, 10, 11] { + let start = MonotonicInstant::now(); + let duration = if cause == BudgetSource::Default { + DEFAULT_NO_DEADLINE_BUDGET + } else { + Duration::from_millis(10) + }; + let mut budget = test_budget(start, duration); + budget.cause = cause; + let offset_duration = duration + .checked_sub(Duration::from_millis(10)) + .and_then(|lead| lead.checked_add(Duration::from_millis(offset))) + .expect("fixture duration bound"); + let observed = start.checked_add(offset_duration).expect("handoff time"); + let polls = Arc::new(AtomicUsize::new(0)); + let native = response_builder() + .status(status) + .header("content-length", "1") + .body(reqwest::Body::wrap(NeverReadyBody(Arc::clone(&polls)))) + .expect("native response"); + let result = finish_fixture_response( + reqwest::Response::from(native), + method.clone(), + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + response_mode, + budget, + 1024, + 24, + None, + None, + None, + None, + None, + native_time(scripted_clock(vec![start, observed])), + ) + .await; + assert_eq!( + polls.load(Ordering::Relaxed), + 0, + "handoff never reads payload" + ); + if cause == BudgetSource::BatchCutoff + && response_mode == ResponseMode::Streamed + { + assert!( + matches!(result, Err(EdgeError::BadRequest { .. })), + "streamed batches reject before response processing" + ); + } else if offset < 10 { + drop(result.expect("on-time handoff")); + } else { + assert!( + matches!(result, Err(EdgeError::GatewayTimeout {cause: actual, ..}) if actual == cause), + "expired handoff must retain its selected budget" + ); + } + assert_eq!( + Arc::strong_count(&polls), + 1, + "source released on return or response drop" + ); + } + } + } + } + } + + #[tokio::test] + async fn original_coding_bodyless_disposition_precedes_caps_and_polling() { + for (method, status) in [ + (Method::HEAD, StatusCode::OK), + (Method::GET, StatusCode::NOT_MODIFIED), + (Method::GET, StatusCode::NO_CONTENT), + (Method::GET, StatusCode::RESET_CONTENT), + ] { + for choice in [ResponseDecoding::Decode, ResponseDecoding::Original] { + for mode in [ + ResponseMode::Buffered { max_bytes: 1 }, + ResponseMode::Streamed, + ] { + let polls = Arc::new(AtomicUsize::new(0)); + let native = http::Response::builder() + .status(status) + .header("content-encoding", "gzip") + .header("content-length", "1073741824") + .body(reqwest::Body::wrap(NeverReadyBody(Arc::clone(&polls)))) + .expect("native response"); + let start = MonotonicInstant::now(); + let response = finish_fixture_response( + reqwest::Response::from(native), + method.clone(), + OutboundProxyMarker::Inherit, + choice, + mode, + test_budget(start, Duration::from_secs(1)), + 0, + 24, + None, + Some(1), + Some(1), + None, + None, + native_time(MonotonicClock::new(move || start)), + ) + .await + .expect("bodyless response"); + assert_eq!(polls.load(Ordering::Relaxed), 0); + assert_eq!(response.body().as_bytes(), Some(b"".as_slice())); + assert_eq!( + response + .headers() + .get("content-encoding") + .expect("baseline coding metadata"), + "gzip" + ); + if method == Method::HEAD || status == StatusCode::NOT_MODIFIED { + assert_eq!( + response + .headers() + .get("content-length") + .expect("representation length"), + "1073741824" + ); + } else if status == StatusCode::RESET_CONTENT { + assert_eq!( + response + .headers() + .get("content-length") + .expect("reset length"), + "0" + ); + } else { + assert!(!response.headers().contains_key("content-length")); + } + } + } + } + } + + #[tokio::test] + async fn method_entry_and_preflight_elapsed_use_the_injected_clock() { + LocalSet::new() + .run_until(async { + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(9)) + .expect("completed instant"); + let client = HttpClient::with_client( + AxumOutboundClient::try_with_time(native_time(scripted_clock(vec![ + start, completed, + ]))) + .expect("client"), + ); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + + let results = client + .start_batch_until(vec![request], Deadline::after(Duration::from_secs(1))) + .collect() + .await + .expect("valid batch driver"); + + let slot = resolved_slot(&results.slots()[0]); + assert_eq!(slot.elapsed, Duration::from_millis(9)); + assert!(matches!(slot.outcome, Err(EdgeError::BadRequest { .. }))); + }) + .await; + } + + #[tokio::test] + async fn backwards_clock_fails_slot_without_invalid_elapsed() { + LocalSet::new() + .run_until(async { + let start = MonotonicInstant::now(); + let earlier = start + .checked_sub(Duration::from_millis(1)) + .expect("earlier instant"); + let client = HttpClient::with_client( + AxumOutboundClient::try_with_time(native_time(scripted_clock(vec![ + start, earlier, + ]))) + .expect("client"), + ); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + + let results = client + .start_batch_until(vec![request], Deadline::after(Duration::from_secs(1))) + .collect() + .await + .expect("valid batch driver"); + + let slot = resolved_slot(&results.slots()[0]); + assert_eq!(slot.elapsed, Duration::ZERO); + assert!(matches!(slot.outcome, Err(EdgeError::Internal { .. }))); + }) + .await; + } + + #[test] + fn backwards_clock_cannot_expand_the_selected_budget() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let earlier = start + .checked_sub(Duration::from_millis(5)) + .expect("earlier instant"); + let clock = scripted_clock(vec![earlier]); + + assert_eq!( + budget_remaining(budget, &clock).expect("remaining budget"), + budget.duration + ); + } + + #[test] + fn paired_timer_rejects_final_expiry_before_transport_poll() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let clock = MonotonicClock::new(move || budget.deadline.instant()); + let time = native_time(clock); + let mut previous = start; + let mut timer = time.wait_until(budget.deadline); + let error = block_on(race_with_timer( + async {}, + budget, + &time, + &mut timer, + &mut previous, + None, + |()| None, + )) + .expect_err("dispatch sample must reject equality expiry without a runtime"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[tokio::test] + async fn buffered_request_preparation_reduces_the_remaining_injected_budget() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let observed = start + .checked_add(Duration::from_millis(3)) + .expect("observed instant"); + let clock = scripted_clock(vec![observed, observed]); + + let body = + collect_request_body(Body::from("body"), 16, budget, &native_time(clock.clone())) + .await + .expect("prepared body"); + let remaining = budget_remaining(budget, &clock).expect("remaining budget"); + + assert_eq!(body, Bytes::from_static(b"body")); + assert_eq!(remaining, Duration::from_millis(7)); + } + + #[tokio::test] + async fn streamed_upload_checks_injected_clock_after_ready_item() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let observed = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&observed); + let clock = MonotonicClock::new(move || *sampled.lock().unwrap()); + let body = Body::from_stream(stream::once(async move { + *observed.lock().unwrap() = budget.deadline.instant(); + Ok(Bytes::from_static(b"body")) + })); + + let error = collect_request_body(body, 16, budget, &native_time(clock)) + .await + .expect_err("post-ready expiry"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[tokio::test] + async fn converted_response_retains_injected_clock() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + let observed = Arc::new(Mutex::new(start)); + let clock_observed = Arc::clone(&observed); + let clock = MonotonicClock::new(move || *clock_observed.lock().expect("clock observation")); + let native = reqwest::Response::from( + http::Response::builder() + .status(StatusCode::NO_CONTENT) + .body(reqwest::Body::from(Bytes::new())) + .expect("native response"), + ); + let response = finish_fixture_response( + native, + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Streamed, + budget, + 32 * 1024 * 1024, + 24, + None, + None, + None, + None, + None, + native_time(clock), + ) + .await + .expect("converted response"); + *observed.lock().expect("clock observation") = budget.deadline.instant(); + + let error = response + .into_bytes_bounded_until(1, budget.deadline) + .await + .expect_err("retained clock reached deadline"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[tokio::test] + async fn response_content_type_provenance_observes_raw_tcp_duplicates() { + use edgezero_core::{ContentTypeIneligibility, ContentTypeProvenance}; + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + use tokio::net::TcpListener; + + for opaque_sibling in [false, true] { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("loopback listener"); + let address = listener.local_addr().expect("local address"); + let origin = tokio::spawn(async move { + let (mut socket, _) = listener.accept().await.expect("accepted connection"); + let mut request = [0_u8; 4096]; + let read = timeout(Duration::from_secs(5), socket.read(&mut request)) + .await + .expect("request deadline") + .expect("request head"); + assert!(read > 0); + let mut head = + b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Type: ".to_vec(); + head.extend_from_slice(if opaque_sibling { + b"\xff" + } else { + b"application/json" + }); + head.extend_from_slice(b"\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}"); + socket.write_all(&head).await.expect("upstream response"); + }); + let client = + HttpClient::with_client(AxumOutboundClient::try_new().expect("outbound client")); + let request = OutboundRequest::get(format!("http://{address}/")) + .expect("request") + .timeout(Duration::from_secs(5)); + let response = client.send(request).await.expect("outbound response"); + assert_eq!( + response.content_type_provenance(), + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::MultipleValues) + ); + assert_eq!( + response.headers().get_all("content-type").iter().count(), + if opaque_sibling { 1 } else { 2 } + ); + assert_eq!(response.body().as_bytes(), Some(b"{}".as_slice())); + origin.await.expect("origin task"); + } + } + + #[tokio::test] + async fn response_content_type_provenance_survives_native_conversion() { + use ContentTypeIneligibility::{Missing, MultipleValues, NonText, Removed}; + use ContentTypeProvenance::{Ineligible, SingleTextUnchanged}; + use edgezero_core::{ContentTypeIneligibility, ContentTypeProvenance}; + + let cases: &[(&[&[u8]], bool, ContentTypeProvenance)] = &[ + (&[], false, Ineligible(Missing)), + (&[b"application/json"], false, SingleTextUnchanged), + (&[b"caf\xc3\xa9"], false, SingleTextUnchanged), + ( + &[b"application/json, text/plain"], + false, + SingleTextUnchanged, + ), + ( + &[b"application/json", b"application/json"], + false, + Ineligible(MultipleValues), + ), + ( + &[b"application/json", b"text/plain"], + false, + Ineligible(MultipleValues), + ), + ( + &[b"application/json", b"\xff"], + false, + Ineligible(MultipleValues), + ), + (&[b"\xff"], false, Ineligible(NonText)), + (&[b"application/json"], true, Ineligible(Removed)), + ]; + let buffered = ResponseMode::Buffered { max_bytes: 32 }; + for (method, status, response_mode) in [ + (Method::GET, StatusCode::OK, buffered), + (Method::GET, StatusCode::OK, ResponseMode::Streamed), + (Method::HEAD, StatusCode::OK, buffered), + (Method::GET, StatusCode::NO_CONTENT, buffered), + (Method::GET, StatusCode::RESET_CONTENT, buffered), + (Method::GET, StatusCode::NOT_MODIFIED, buffered), + ] { + for (values, nominated, expected) in cases { + let mut native_headers = HeaderMap::new(); + for value in *values { + native_headers.append( + "content-type", + HeaderValue::from_bytes(value).expect("header bytes"), + ); + } + if *nominated { + native_headers.insert("connection", HeaderValue::from_static("content-type")); + } + let mut native = http::Response::builder() + .status(status) + .body(reqwest::Body::from(Bytes::new())) + .expect("native response"); + *native.headers_mut() = native_headers; + let start = MonotonicInstant::now(); + let response = finish_fixture_response( + reqwest::Response::from(native), + method.clone(), + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + response_mode, + test_budget(start, Duration::from_secs(1)), + 32 * 1024 * 1024, + 24, + None, + None, + None, + None, + None, + native_time(MonotonicClock::new(move || start)), + ) + .await + .expect("converted response"); + assert_eq!(response.content_type_provenance(), *expected); + let converted = response.into_response().expect("application response"); + assert_eq!( + converted.extensions().get::(), + Some(expected) + ); + } + } + } + + #[tokio::test] + async fn response_stream_retains_clock_for_post_ready_expiry() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let observed = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&observed); + let clock = MonotonicClock::new(move || *sampled.lock().unwrap()); + let source = stream::once(async move { + *observed.lock().unwrap() = budget.deadline.instant(); + Ok(Bytes::from_static(b"body")) + }) + .boxed_local(); + let mut body = finish_fixture_stream(source, budget, native_time(clock)) + .await + .expect("shared response stream"); + + let error = body + .next() + .await + .expect("terminal item") + .expect_err("post-ready expiry"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + fn resolved_slot(slot: &OutboundCollectedSlot) -> &edgezero_core::OutboundSlotResult { + match slot { + OutboundCollectedSlot::Resolved(result) => result, + other @ (OutboundCollectedSlot::AlreadyDelivered + | OutboundCollectedSlot::Unresolved) => panic!("expected resolved slot, got {other:?}"), + } + } +} diff --git a/crates/edgezero-adapter-axum/src/outbound/testing.rs b/crates/edgezero-adapter-axum/src/outbound/testing.rs new file mode 100644 index 000000000..a5ac8945c --- /dev/null +++ b/crates/edgezero-adapter-axum/src/outbound/testing.rs @@ -0,0 +1,201 @@ +use core::num::NonZeroU64; +use std::cell::{Cell, RefCell}; +use std::collections::BTreeMap; +use tokio::task::LocalSet; + +use edgezero_core::outbound::{ + HttpClient, OutboundCollectedSlot, OutboundProxyMarker, ResponseDecoding, ResponseMode, +}; + +use super::*; + +type FixtureHead = Box Result>; + +thread_local! { + static RESPONSES: RefCell> = const { RefCell::new(BTreeMap::new()) }; + static NEXT_RESPONSE: Cell = const { Cell::new(0) }; +} + +struct ResponseClient { + id: u64, + time: OutboundTime, +} + +#[expect( + clippy::missing_trait_methods, + reason = "Drop::pin_drop is unstable and conflicts with drop" +)] +impl Drop for ResponseClient { + fn drop(&mut self) { + let response = RESPONSES.with(|responses| responses.borrow_mut().remove(&self.id)); + drop(response); + } +} + +impl OutboundHttpClient for ResponseClient { + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let head = self.dispatch_head(request); + schedule_response(Box::pin(async move { head.await?.finish().await })) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + let response = RESPONSES.with(|responses| responses.borrow_mut().remove(&self.id)); + let (_, context) = request.into_parts(); + Box::pin(async move { + response + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("fixture response missing")))?( + context, + ) + }) + } + + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + schedule_batch(self, batch) + } + + fn time(&self) -> OutboundTime { + self.time.clone() + } + + fn validate_supported_options( + &self, + request: &OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + AxumOutboundClient::validate_options(request) + } +} + +fn fixture_client(time: OutboundTime, head: Head) -> Result +where + Head: FnOnce(OutboundResponseContext) -> Result + 'static, +{ + let id = NEXT_RESPONSE + .with(|next| { + let id = next.get(); + id.checked_add(1).map(|next_id| { + next.set(next_id); + id + }) + }) + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("fixture identity overflow")))?; + RESPONSES.with(|responses| responses.borrow_mut().insert(id, Box::new(head))); + Ok(HttpClient::with_client(ResponseClient { id, time })) +} + +#[expect( + clippy::too_many_arguments, + reason = "existing response fixtures exercise independent policy boundaries" +)] +pub(super) async fn finish_fixture_response( + response: reqwest::Response, + request_method: Method, + proxy_marker: OutboundProxyMarker, + response_decoding: ResponseDecoding, + response_mode: ResponseMode, + budget: DispatchBudget, + max_decoder_bytes: u64, + max_brotli_window_bits: u8, + max_chunk_bytes: Option, + max_decoded_response_bytes: Option, + max_encoded_response_bytes: Option, + max_response_header_bytes: Option, + max_response_header_count: Option, + time: OutboundTime, +) -> Result { + let mut parts = OutboundRequest::get("http://fixture.invalid/")?.into_parts(); + parts.method = request_method; + parts.proxy_marker = proxy_marker; + parts.response_decoding = response_decoding; + parts.response_mode = response_mode; + parts.max_decoder_bytes = max_decoder_bytes; + parts.max_brotli_window_bits = max_brotli_window_bits; + parts.max_chunk_bytes = max_chunk_bytes; + parts.max_decoded_response_bytes = max_decoded_response_bytes; + parts.max_encoded_response_bytes = max_encoded_response_bytes; + parts.max_response_header_bytes = max_response_header_bytes; + parts.max_response_header_count = max_response_header_count; + match budget.cause { + BudgetSource::PerCallTimeout => parts.timeout = Some(budget.duration), + BudgetSource::RequestDeadline => parts.deadline = Some(budget.deadline), + BudgetSource::BatchCutoff | BudgetSource::Default => {} + BudgetSource::ConnectTimeout + | BudgetSource::IdleTimeout + | BudgetSource::Unspecified + | _ => panic!("unsupported fixture governing source"), + } + let request = OutboundRequest::from_parts(parts)?; + let client = fixture_client(time, move |context| response_head(response, context))?; + if budget.cause == BudgetSource::BatchCutoff { + LocalSet::new() + .run_until(async { + let results = client + .start_batch_until(vec![request], budget.deadline) + .collect() + .await + .map_err(|failure| failure.into_parts().0)?; + let (mut slots, _) = results.into_parts(); + match slots.pop().expect("fixture slot") { + OutboundCollectedSlot::Resolved(slot) => slot.outcome, + OutboundCollectedSlot::Unresolved => { + Err(timeout_error(BudgetSource::BatchCutoff)) + } + OutboundCollectedSlot::AlreadyDelivered => { + panic!("fixture result was not delivered") + } + } + }) + .await + } else { + client.send(request).await + } +} + +pub(super) async fn finish_fixture_stream( + source: BodyStream, + budget: DispatchBudget, + time: OutboundTime, +) -> Result { + let mut parts = OutboundRequest::get("http://fixture.invalid/")? + .stream_response() + .into_parts(); + match budget.cause { + BudgetSource::PerCallTimeout => parts.timeout = Some(budget.duration), + BudgetSource::RequestDeadline => parts.deadline = Some(budget.deadline), + BudgetSource::Default => {} + BudgetSource::BatchCutoff + | BudgetSource::ConnectTimeout + | BudgetSource::IdleTimeout + | BudgetSource::Unspecified + | _ => { + return Err(EdgeError::internal(anyhow::anyhow!( + "unsupported streamed fixture budget" + ))); + } + } + let client = fixture_client(time, move |context| { + OutboundHead::new( + StatusCode::OK, + OutboundHeaderInput::new( + "axum", + HeaderMap::new(), + OutboundHeaderFidelity::AdapterVisibleFields, + ), + source, + context, + OutboundExchangeOwner::new(|| {}), + ) + })?; + client + .send(OutboundRequest::from_parts(parts)?) + .await? + .into_body() + .into_stream() + .map_err(|_buffered| { + EdgeError::internal(anyhow::anyhow!("streamed fixture returned a buffered body")) + }) +} diff --git a/crates/edgezero-adapter-axum/src/proxy.rs b/crates/edgezero-adapter-axum/src/proxy.rs deleted file mode 100644 index 1dd3e504c..000000000 --- a/crates/edgezero-adapter-axum/src/proxy.rs +++ /dev/null @@ -1,334 +0,0 @@ -use std::time::Duration; - -use async_trait::async_trait; -use edgezero_core::body::Body; -use edgezero_core::error::EdgeError; -use edgezero_core::http::{HeaderName, HeaderValue, Method, StatusCode}; -use edgezero_core::proxy::{ProxyClient, ProxyRequest, ProxyResponse}; -use futures_util::StreamExt as _; -use reqwest::{Client, header}; - -pub struct AxumProxyClient { - client: Client, -} - -impl AxumProxyClient { - /// Construct a proxy client with the workspace-default 30-second timeout. - /// - /// **Breaking change (pre-1.0):** previously `AxumProxyClient` implemented - /// `Default` and panicked if reqwest's TLS backend could not be initialised. - /// Construction is now fallible so callers can decide how to handle a - /// missing or misconfigured TLS backend. - /// - /// # Errors - /// Returns the underlying [`reqwest::Error`] if `reqwest::Client::builder().build()` - /// fails — typically because the TLS backend cannot be initialised on this target. - #[inline] - pub fn try_new() -> Result { - let client = Client::builder().timeout(Duration::from_secs(30)).build()?; - Ok(Self { client }) - } -} - -#[async_trait(?Send)] -impl ProxyClient for AxumProxyClient { - #[inline] - async fn send(&self, request: ProxyRequest) -> Result { - let (method, uri, headers, body, _extensions) = request.into_parts(); - let reqwest_method = reqwest_method(&method)?; - let mut builder = self.client.request(reqwest_method, uri.to_string()); - - for (name, value) in &headers { - let header_name = header::HeaderName::from_bytes(name.as_str().as_bytes()) - .map_err(EdgeError::internal)?; - let header_value = - header::HeaderValue::from_bytes(value.as_bytes()).map_err(EdgeError::internal)?; - builder = builder.header(header_name, header_value); - } - - builder = match body { - Body::Once(bytes) => builder.body(bytes.to_vec()), - Body::Stream(mut stream) => { - let mut buf = Vec::new(); - while let Some(result) = stream.next().await { - let chunk = result.map_err(EdgeError::internal)?; - buf.extend_from_slice(&chunk); - } - builder.body(buf) - } - }; - - let response = builder.send().await.map_err(EdgeError::internal)?; - let status = - StatusCode::from_u16(response.status().as_u16()).map_err(EdgeError::internal)?; - let mut proxy_response = ProxyResponse::new(status, Body::empty()); - - for (name, value) in response.headers() { - let header_name = - HeaderName::from_bytes(name.as_str().as_bytes()).map_err(EdgeError::internal)?; - let header_value = - HeaderValue::from_bytes(value.as_bytes()).map_err(EdgeError::internal)?; - proxy_response - .headers_mut() - .insert(header_name, header_value); - } - - let bytes = response.bytes().await.map_err(EdgeError::internal)?; - *proxy_response.body_mut() = Body::from(bytes.to_vec()); - - Ok(proxy_response) - } -} - -fn reqwest_method(method: &Method) -> Result { - reqwest::Method::from_bytes(method.as_str().as_bytes()).map_err(EdgeError::internal) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::mem; - - #[test] - fn converts_method_to_reqwest() { - let method = Method::POST; - let req = reqwest_method(&method).expect("reqwest method"); - assert_eq!(req, reqwest::Method::POST); - } - - #[test] - fn converts_all_methods_to_reqwest() { - let cases = [ - (Method::GET, reqwest::Method::GET), - (Method::POST, reqwest::Method::POST), - (Method::PUT, reqwest::Method::PUT), - (Method::DELETE, reqwest::Method::DELETE), - (Method::PATCH, reqwest::Method::PATCH), - (Method::HEAD, reqwest::Method::HEAD), - (Method::OPTIONS, reqwest::Method::OPTIONS), - ]; - for (input, expected) in cases { - let result = reqwest_method(&input).expect("method conversion"); - assert_eq!(result, expected); - } - } - - #[test] - fn default_client_creates_successfully() { - let client = AxumProxyClient::try_new().expect("reqwest client init"); - // Just verify it builds without panicking - assert!(mem::size_of_val(&client) > 0); - } -} - -#[cfg(test)] -mod integration_tests { - use super::*; - use axum::Router; - use axum::body::Bytes as AxumBytes; - use axum::http::header::CONTENT_TYPE; - use axum::http::{HeaderMap as AxumHeaderMap, StatusCode as AxumStatusCode}; - use axum::routing::{delete, get, patch, post, put}; - use edgezero_core::http::Uri; - use tokio::net::TcpListener; - - async fn start_test_server(router: Router) -> String { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, router).await.unwrap(); - }); - format!("http://{addr}") - } - - #[tokio::test] - async fn proxy_client_sends_get_request() { - let app = Router::new().route("/test", get(|| async { "hello from server" })); - let base_url = start_test_server(app).await; - - let client = AxumProxyClient::try_new().expect("reqwest client init"); - let uri: Uri = format!("{base_url}/test").parse().unwrap(); - let request = ProxyRequest::new(Method::GET, uri); - - let response = client.send(request).await.expect("response"); - assert_eq!(response.status(), StatusCode::OK); - - match response.body() { - Body::Once(bytes) => assert_eq!(bytes.as_ref(), b"hello from server"), - Body::Stream(_) => panic!("expected buffered body"), - } - } - - #[tokio::test] - async fn proxy_client_sends_post_with_body() { - let app = Router::new().route("/echo", post(|body: AxumBytes| async move { body })); - let base_url = start_test_server(app).await; - - let client = AxumProxyClient::try_new().expect("reqwest client init"); - let uri: Uri = format!("{base_url}/echo").parse().unwrap(); - let mut request = ProxyRequest::new(Method::POST, uri); - *request.body_mut() = Body::from("request body data"); - - let response = client.send(request).await.expect("response"); - assert_eq!(response.status(), StatusCode::OK); - - match response.body() { - Body::Once(bytes) => assert_eq!(bytes.as_ref(), b"request body data"), - Body::Stream(_) => panic!("expected buffered body"), - } - } - - #[tokio::test] - async fn proxy_client_forwards_request_headers() { - let app = Router::new().route( - "/headers", - get(|headers: AxumHeaderMap| async move { - headers - .get("x-custom-header") - .and_then(|val| val.to_str().ok()) - .unwrap_or("missing") - .to_owned() - }), - ); - let base_url = start_test_server(app).await; - - let client = AxumProxyClient::try_new().expect("reqwest client init"); - let uri: Uri = format!("{base_url}/headers").parse().unwrap(); - let mut request = ProxyRequest::new(Method::GET, uri); - request - .headers_mut() - .insert("x-custom-header", HeaderValue::from_static("custom-value")); - - let response = client.send(request).await.expect("response"); - assert_eq!(response.status(), StatusCode::OK); - - match response.body() { - Body::Once(bytes) => assert_eq!(bytes.as_ref(), b"custom-value"), - Body::Stream(_) => panic!("expected buffered body"), - } - } - - #[tokio::test] - async fn proxy_client_receives_response_headers() { - let app = Router::new().route( - "/with-headers", - get(|| async { ([(CONTENT_TYPE, "application/json")], "{}") }), - ); - let base_url = start_test_server(app).await; - - let client = AxumProxyClient::try_new().expect("reqwest client init"); - let uri: Uri = format!("{base_url}/with-headers").parse().unwrap(); - let request = ProxyRequest::new(Method::GET, uri); - - let response = client.send(request).await.expect("response"); - assert_eq!(response.status(), StatusCode::OK); - - let content_type = response - .headers() - .get("content-type") - .and_then(|val| val.to_str().ok()); - assert_eq!(content_type, Some("application/json")); - } - - #[tokio::test] - async fn proxy_client_handles_404() { - let app = Router::new(); - let base_url = start_test_server(app).await; - - let client = AxumProxyClient::try_new().expect("reqwest client init"); - let uri: Uri = format!("{base_url}/nonexistent").parse().unwrap(); - let request = ProxyRequest::new(Method::GET, uri); - - let response = client.send(request).await.expect("response"); - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn proxy_client_handles_500() { - let app = Router::new().route( - "/error", - get(|| async { (AxumStatusCode::INTERNAL_SERVER_ERROR, "error") }), - ); - let base_url = start_test_server(app).await; - - let client = AxumProxyClient::try_new().expect("reqwest client init"); - let uri: Uri = format!("{base_url}/error").parse().unwrap(); - let request = ProxyRequest::new(Method::GET, uri); - - let response = client.send(request).await.expect("response"); - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[tokio::test] - async fn proxy_client_handles_various_methods() { - let app = Router::new() - .route("/method", get(|| async { "GET" })) - .route("/method", post(|| async { "POST" })) - .route("/method", put(|| async { "PUT" })) - .route("/method", delete(|| async { "DELETE" })) - .route("/method", patch(|| async { "PATCH" })); - let base_url = start_test_server(app).await; - - let client = AxumProxyClient::try_new().expect("reqwest client init"); - - for (method, expected_body) in [ - (Method::GET, "GET"), - (Method::POST, "POST"), - (Method::PUT, "PUT"), - (Method::DELETE, "DELETE"), - (Method::PATCH, "PATCH"), - ] { - let uri: Uri = format!("{base_url}/method").parse().unwrap(); - let request = ProxyRequest::new(method, uri); - let response = client.send(request).await.expect("response"); - assert_eq!(response.status(), StatusCode::OK); - match response.body() { - Body::Once(bytes) => assert_eq!(bytes.as_ref(), expected_body.as_bytes()), - Body::Stream(_) => panic!("expected buffered body"), - } - } - } - - #[tokio::test] - async fn proxy_client_handles_connection_refused() { - let client = AxumProxyClient::try_new().expect("reqwest client init"); - // Use a port that's unlikely to have anything running - let uri: Uri = "http://127.0.0.1:1".parse().unwrap(); - let request = ProxyRequest::new(Method::GET, uri); - - client - .send(request) - .await - .expect_err("expected connection refused"); - } - - #[tokio::test] - async fn proxy_client_sends_streaming_body() { - use bytes::Bytes; - use futures::stream; - - let app = Router::new().route("/stream-echo", post(|body: AxumBytes| async move { body })); - let base_url = start_test_server(app).await; - - let client = AxumProxyClient::try_new().expect("reqwest client init"); - let uri: Uri = format!("{base_url}/stream-echo").parse().unwrap(); - let mut request = ProxyRequest::new(Method::POST, uri); - - // Create a streaming body - Body::stream expects Stream - let chunks = vec![ - Bytes::from("chunk1"), - Bytes::from("chunk2"), - Bytes::from("chunk3"), - ]; - let stream = stream::iter(chunks); - *request.body_mut() = Body::stream(stream); - - let response = client.send(request).await.expect("response"); - assert_eq!(response.status(), StatusCode::OK); - - match response.body() { - Body::Once(bytes) => assert_eq!(bytes.as_ref(), b"chunk1chunk2chunk3"), - Body::Stream(_) => panic!("expected buffered body"), - } - } -} diff --git a/crates/edgezero-adapter-axum/src/request.rs b/crates/edgezero-adapter-axum/src/request.rs index 9e3f5976d..020b031f2 100644 --- a/crates/edgezero-adapter-axum/src/request.rs +++ b/crates/edgezero-adapter-axum/src/request.rs @@ -1,36 +1,58 @@ use std::net::SocketAddr; +use std::pin::Pin; -use axum::body::{Body as AxumBody, to_bytes}; +use axum::body::{Body as AxumBody, BodyDataStream}; use axum::extract::connect_info::ConnectInfo; -use axum::http::Request; +use axum::http::{Request, request::Parts}; use edgezero_core::body::Body; -use edgezero_core::http::HeaderValue; +use edgezero_core::error::EdgeError; use edgezero_core::http::Request as CoreRequest; -use edgezero_core::http::header::CONTENT_TYPE; -use edgezero_core::proxy::ProxyHandle; +use edgezero_core::outbound::HttpClient; +use edgezero_core::time::{Deadline, MonotonicClock}; +use futures_util::{StreamExt as _, stream}; +use http_body::Body as _; +use tokio::time::timeout; use crate::context::AxumRequestContext; -use crate::proxy::AxumProxyClient; +use crate::outbound::AxumOutboundClient; /// Convert an Axum/Hyper request into an `EdgeZero` core request while preserving streaming bodies /// and exposing connection metadata through `AxumRequestContext`. /// /// # Errors -/// Returns an error if a buffered (`application/json`) body cannot be read into memory. +/// Returns an error if the outbound client cannot be initialized. #[inline] +#[expect( + clippy::unused_async, + reason = "the public converter retains its established async API while request bodies remain lazy" +)] pub async fn into_core_request(request: Request) -> Result { let (parts, axum_body) = request.into_parts(); + let client = AxumOutboundClient::try_new() + .map_err(|_error| "failed to build outbound HTTP client".to_owned())?; + Ok(into_core_request_parts( + parts, + axum_body, + None, + HttpClient::with_client(client), + )) +} - let body = match parts.headers.get(CONTENT_TYPE) { - Some(value) if is_json_content_type(value) => { - let bytes = to_bytes(axum_body, usize::MAX) - .await - .map_err(|err| format!("Failed to convert body into bytes: {err}"))?; - Body::from_bytes(bytes) - } - _ => { - let stream = axum_body.into_data_stream(); - Body::from_stream(stream) +pub(crate) fn into_core_request_parts( + parts: Parts, + axum_body: AxumBody, + read_lifetime: Option<(Deadline, MonotonicClock)>, + outbound_client: HttpClient, +) -> CoreRequest { + // A zero size hint can still leave trailers; only the transport's terminal signal is enough. + let body = if axum_body.is_end_stream() { + Body::empty() + } else { + match read_lifetime { + Some((deadline, monotonic_clock)) => { + deadline_body(axum_body.into_data_stream(), deadline, monotonic_clock) + } + None => Body::from_external_stream(axum_body.into_data_stream()), } }; @@ -52,47 +74,239 @@ pub async fn into_core_request(request: Request) -> Result Body { + let deadline_stream = stream::unfold( + Some(Box::pin(body_stream)), + move |stream_state: Option>>| { + let clock = monotonic_clock.clone(); + async move { + let mut state_stream = stream_state?; + let Some(remaining) = deadline.remaining_at(clock.now()) else { + return Some(( + Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )), + None, + )); + }; + let item = match timeout(remaining, state_stream.next()).await { + Ok(item) => item, + Err(_elapsed) => { + return Some(( + Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )), + None, + )); + } + }; + if deadline.is_expired_at(clock.now()) { + return Some(( + Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )), + None, + )); + } + match item { + Some(Ok(bytes)) => Some((Ok(bytes), Some(state_stream))), + Some(Err(error)) => Some((Err(EdgeError::internal(error)), None)), + None => None, + } + } + }, + ); + Body::from_stream(deadline_stream) } -fn is_json_content_type(value: &HeaderValue) -> bool { - let Ok(raw) = value.to_str() else { - return false; - }; +#[cfg(test)] +mod tests { + use super::*; + use bytes::Bytes; + use edgezero_core::http::{Method, StatusCode}; + use edgezero_core::time::MonotonicInstant; + use std::io; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::{Context, Poll}; - let media_type = raw.split(';').next().map_or("", str::trim); - if media_type.eq_ignore_ascii_case("application/json") { - return true; + struct DropSignal(Arc); + + struct EndSignalBody { + ended: bool, + polls: Arc, + release: DropSignal, } - let Some((ty, raw_subtype)) = media_type.split_once('/') else { - return false; - }; + impl http_body::Body for EndSignalBody { + type Data = Bytes; + type Error = io::Error; + + fn is_end_stream(&self) -> bool { + self.ended + } + + fn poll_frame( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + self.polls.fetch_add(1, Ordering::SeqCst); + assert_eq!(self.release.0.load(Ordering::SeqCst), 0); + Poll::Ready(None) + } - if !ty.eq_ignore_ascii_case("application") { - return false; + fn size_hint(&self) -> http_body::SizeHint { + http_body::SizeHint::with_exact(0) + } } - let subtype = raw_subtype.trim(); - let Some(suffix_start) = subtype.len().checked_sub(5) else { - return false; - }; - subtype - .get(suffix_start..) - .is_some_and(|suffix| suffix.eq_ignore_ascii_case("+json")) -} + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } -#[cfg(test)] -mod tests { - use super::*; - use edgezero_core::body::Body; - use edgezero_core::http::Method; + #[tokio::test] + async fn deadline_body_releases_source_when_timeout_is_emitted() { + let dropped = Arc::new(AtomicUsize::new(0)); + let signal = DropSignal(Arc::clone(&dropped)); + let source = stream::poll_fn(move |_cx| { + let _keep_alive = &signal; + Poll::>>::Pending + }); + let start = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || start); + let body = deadline_body( + AxumBody::from_stream(source).into_data_stream(), + Deadline::at_instant(start), + clock, + ); + let mut body_stream = body.into_stream().expect("stream"); + + let error = body_stream + .next() + .await + .expect("terminal timeout item") + .expect_err("timeout"); + assert_eq!(error.status(), StatusCode::REQUEST_TIMEOUT); + assert_eq!(dropped.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn host_known_empty_body_is_preserved_without_reading() { + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + for (method, admitted) in [ + Method::GET, + Method::HEAD, + Method::POST, + Method::OPTIONS, + Method::DELETE, + ] + .into_iter() + .flat_map(|method| [false, true].map(|admitted| (method.clone(), admitted))) + { + let polls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let request = Request::builder() + .method(method) + .uri("/demo") + .body(AxumBody::new(EndSignalBody { + ended: true, + polls: Arc::clone(&polls), + release: DropSignal(Arc::clone(&drops)), + })) + .expect("request"); + let now = MonotonicInstant::now(); + let lifetime = + admitted.then(|| (Deadline::at_instant(now), MonotonicClock::new(move || now))); + let (parts, native) = request.into_parts(); + let core = into_core_request_parts(parts, native, lifetime, client.clone()); + assert_eq!(core.body().as_bytes(), Some(b"".as_slice())); + assert_eq!(polls.load(Ordering::SeqCst), 0); + assert_eq!(drops.load(Ordering::SeqCst), 1); + drop(core); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + } + + #[tokio::test] + async fn zero_size_hint_and_zero_content_length_do_not_certify_empty_body() { + let polls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let request = Request::builder() + .method(Method::GET) + .uri("/demo") + .header("content-length", "0") + .body(AxumBody::new(EndSignalBody { + ended: false, + polls: Arc::clone(&polls), + release: DropSignal(Arc::clone(&drops)), + })) + .expect("request"); + let core = into_core_request(request).await.expect("conversion"); + assert!(core.body().is_stream()); + assert_eq!(polls.load(Ordering::SeqCst), 0); + assert_eq!(drops.load(Ordering::SeqCst), 0); + drop(core); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn unknown_body_preserves_empty_chunks_then_data_and_source_errors() { + for source_error in [false, true] { + let polls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&polls); + let drops = Arc::new(AtomicUsize::new(0)); + let release = DropSignal(Arc::clone(&drops)); + let mut chunks = vec![ + Ok(Bytes::new()), + if source_error { + Err(io::Error::other("inbound fixture source error")) + } else { + Ok(Bytes::from_static(b"data")) + }, + ] + .into_iter(); + let source = stream::poll_fn(move |_cx| { + let _retain_release = &release; + observed.fetch_add(1, Ordering::SeqCst); + Poll::Ready(chunks.next()) + }); + let request = Request::builder() + .method(Method::GET) + .uri("/demo") + .header("content-length", "0") + .body(AxumBody::from_stream(source)) + .expect("request"); + let core = into_core_request(request).await.expect("conversion"); + assert!(core.body().is_stream()); + assert_eq!(polls.load(Ordering::SeqCst), 0); + let outcome = core.into_body().into_bytes_bounded(4).await; + if source_error { + assert_eq!( + outcome.expect_err("original source failure").status(), + StatusCode::INTERNAL_SERVER_ERROR + ); + } else { + assert_eq!(outcome.expect("data survives"), Bytes::from_static(b"data")); + } + assert!(polls.load(Ordering::SeqCst) >= 2); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + } #[tokio::test] async fn converts_request_and_records_connect_info() { @@ -112,10 +326,10 @@ mod tests { assert_eq!(core_request.method(), &Method::POST); assert_eq!(core_request.uri().path(), "/demo"); assert_eq!(core_request.headers()["x-test"], "1"); - match core_request.body() { - Body::Stream(_) => {} // streaming bodies stay streaming - Body::Once(_) => panic!("body should remain streaming"), - } + assert!( + core_request.body().is_stream(), + "body should remain streaming" + ); let context = AxumRequestContext::get(&core_request).expect("context"); assert_eq!(context.remote_addr, Some("127.0.0.1:4000".parse().unwrap())); @@ -125,6 +339,28 @@ mod tests { .get::>() .is_none() ); + assert!(core_request.extensions().get::().is_some()); + } + + #[tokio::test] + async fn supplied_outbound_client_policy_is_retained_by_the_core_request() { + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")) + .with_proxy_marker(false); + let request = Request::builder() + .method(Method::GET) + .uri("/demo") + .body(AxumBody::empty()) + .expect("request"); + let (parts, body) = request.into_parts(); + + let core_request = into_core_request_parts(parts, body, None, client); + assert!( + !core_request + .extensions() + .get::() + .expect("supplied client") + .proxy_marker_enabled() + ); } #[tokio::test] @@ -142,8 +378,8 @@ mod tests { } #[tokio::test] - async fn json_content_type_buffers_body() { - let json_payload = r#"{"name":"test"}"#; + async fn json_content_type_stays_streaming() { + let json_payload = serde_json::json!({"name": "test"}).to_string(); let request = Request::builder() .method(Method::POST) .uri("/api/test") @@ -156,12 +392,7 @@ mod tests { .expect("request conversion"); assert_eq!(core_request.method(), &Method::POST); - match core_request.body() { - Body::Once(bytes) => { - assert_eq!(bytes.as_ref(), json_payload.as_bytes()); - } - Body::Stream(_) => panic!("JSON body should be buffered, not streaming"), - } + assert!(core_request.body().is_stream()); } #[tokio::test] @@ -177,29 +408,6 @@ mod tests { .await .expect("request conversion"); - assert!(matches!(core_request.body(), Body::Stream(_))); - } - - #[test] - fn json_content_type_detection() { - assert!(is_json_content_type(&HeaderValue::from_static( - "application/json" - ))); - assert!(is_json_content_type(&HeaderValue::from_static( - "application/json; charset=utf-8" - ))); - assert!(is_json_content_type(&HeaderValue::from_static( - "application/vnd.api+json" - ))); - assert!(is_json_content_type(&HeaderValue::from_static( - "APPLICATION/VND.CUSTOM+JSON; CHARSET=UTF-8" - ))); - - assert!(!is_json_content_type(&HeaderValue::from_static( - "text/json" - ))); - assert!(!is_json_content_type(&HeaderValue::from_static( - "application/json+xml" - ))); + assert!(core_request.body().is_stream()); } } diff --git a/crates/edgezero-adapter-axum/src/response.rs b/crates/edgezero-adapter-axum/src/response.rs index cf3bb6c69..834bd7691 100644 --- a/crates/edgezero-adapter-axum/src/response.rs +++ b/crates/edgezero-adapter-axum/src/response.rs @@ -1,101 +1,1558 @@ -use axum::body::Body as AxumBody; -use axum::http::header::CONTENT_TYPE; -use axum::http::{HeaderValue, Response, StatusCode}; -use futures::executor::block_on; -use futures_util::{StreamExt as _, pin_mut}; -use tracing::error; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::error::Error as StdError; +use std::fmt; +use std::mem; +use std::pin::Pin; +use std::ptr; +use std::rc::{Rc, Weak}; +use std::task::{Context, Poll}; -use edgezero_core::body::Body; -use edgezero_core::http::Response as CoreResponse; +use axum::http::Response; +use bytes::Bytes; +use edgezero_core::body::{Body, BodyPayload, BodyStream}; +use edgezero_core::error::EdgeError; +use edgezero_core::http::{Method, Response as CoreResponse, StatusCode}; +use edgezero_core::response_egress::{ + RESPONSE_EGRESS_FALLBACK_SAFETY_BUDGET, ResponseEgressAttempt, ResponseEgressEnvelope, + ResponseEgressFallbackDisposition, ResponseEgressOutcome, +}; +use edgezero_core::response_egress_framing::{ + PreparedResponseEgress, ResponseEgressBodyLengthError, ResponseEgressFramingMode, + response_egress_source_error_category, +}; +use edgezero_core::time::{Deadline, MonotonicClock}; +use futures_util::StreamExt as _; +use http_body::{Body as HttpBody, Frame, SizeHint}; +use tokio::sync::Notify; +use tokio::task::yield_now; +use tokio::time::Instant as TokioInstant; +use tokio::time::timeout; -/// Convert an `EdgeZero` response into one consumable by Axum/Hyper. -/// -/// Streaming responses are collected into an in-memory buffer. While this sacrifices -/// incremental flushing, it keeps the adapter compatible with the non-`Send` streaming type used by -/// `edgezero_core::Body` and works well for local development. +const INTERNAL_FALLBACK_BODY: &[u8] = b"internal server error"; +const TIMEOUT_FALLBACK_BODY: &[u8] = b"response write deadline exceeded"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum EgressKind { + Application, + Fallback, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct AxumBodyError(ResponseEgressOutcome); + +impl fmt::Display for AxumBodyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self.0 { + ResponseEgressOutcome::DeadlineExceeded => "response write deadline exceeded", + ResponseEgressOutcome::SourceError => "response body source failed", + ResponseEgressOutcome::ClientDisconnected + | ResponseEgressOutcome::Completed + | ResponseEgressOutcome::ConversionError + | ResponseEgressOutcome::HostHandoff + | ResponseEgressOutcome::RequestCancelled + | ResponseEgressOutcome::TransportError + | ResponseEgressOutcome::Unspecified + | _ => "response transport failed", + }) + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "the category-only body error has no nested diagnostic source" +)] +impl StdError for AxumBodyError {} + +impl AxumBodyError { + pub(crate) const fn is_deadline(self) -> bool { + matches!(self.0, ResponseEgressOutcome::DeadlineExceeded) + } +} + +#[derive(Default)] +struct EgressConnectionInner { + changed: Notify, + responses: RefCell>>, +} + +struct ConnectionResponseState { + active: Cell, + connection: Weak, + host_deadline: TokioInstant, + requested_terminal: Cell>, +} + +impl ConnectionResponseState { + fn request_terminal(&self, outcome: ResponseEgressOutcome) { + if self.active.get() && self.requested_terminal.get().is_none() { + self.requested_terminal.set(Some(outcome)); + } + } + + fn unregister(&self) { + if !self.active.replace(false) { + return; + } + let Some(connection) = self.connection.upgrade() else { + return; + }; + connection + .responses + .borrow_mut() + .retain(|candidate| !ptr::eq(Rc::as_ptr(candidate), self)); + connection.changed.notify_one(); + } +} + +/// Per-HTTP/1-connection response registry used by the connection supervisor. /// -#[inline] -pub fn into_axum_response(response: CoreResponse) -> Response { - let (parts, core_body) = response.into_parts(); - let body = match core_body { - Body::Once(bytes) => AxumBody::from(bytes), - Body::Stream(stream) => { - let result = block_on(async { - let mut buf = Vec::new(); - pin_mut!(stream); - while let Some(chunk) = stream.next().await { - let bytes = chunk?; - buf.extend_from_slice(&bytes); +/// The registry stores only deadline events. The non-clone response attempt remains owned by the +/// body coordinator and observes the event when Hyper next polls or drops that body. +#[derive(Clone, Default)] +pub(crate) struct EgressConnection { + inner: Rc, +} + +#[expect( + clippy::arbitrary_source_item_ordering, + reason = "registry operations are grouped by registration, deadline, transport, and notification lifecycle" +)] +impl EgressConnection { + fn register( + &self, + deadline: Deadline, + clock: &MonotonicClock, + ) -> Option> { + let remaining = deadline.remaining_at(clock.now())?; + let host_deadline = TokioInstant::now().checked_add(remaining)?; + let state = Rc::new(ConnectionResponseState { + active: Cell::new(true), + connection: Rc::downgrade(&self.inner), + host_deadline, + requested_terminal: Cell::new(None), + }); + self.inner + .responses + .borrow_mut() + .push_back(Rc::clone(&state)); + self.inner.changed.notify_one(); + Some(state) + } + + pub(crate) fn next_deadline(&self) -> Option { + self.inner + .responses + .borrow() + .iter() + .filter(|state| state.active.get()) + .map(|state| state.host_deadline) + .min() + } + + /// Marks the first ordered response at the earliest elapsed deadline as the winner and every + /// other unsettled response as HTTP/1 connection collateral. + pub(crate) fn signal_elapsed_deadline(&self, observed_at: TokioInstant) -> bool { + let responses = self.inner.responses.borrow(); + let Some(winner) = responses + .iter() + .filter(|state| state.active.get() && state.host_deadline <= observed_at) + .min_by_key(|state| state.host_deadline) + .cloned() + else { + return false; + }; + for state in responses.iter().filter(|state| state.active.get()) { + if Rc::ptr_eq(state, &winner) { + state.request_terminal(ResponseEgressOutcome::DeadlineExceeded); + } else { + state.request_terminal(ResponseEgressOutcome::TransportError); + } + } + true + } + + pub(crate) fn signal_transport_error(&self) { + for state in self + .inner + .responses + .borrow() + .iter() + .filter(|state| state.active.get()) + { + state.request_terminal(ResponseEgressOutcome::TransportError); + } + } + + pub(crate) async fn changed(&self) { + self.inner.changed.notified().await; + } + + #[cfg(test)] + fn is_empty(&self) -> bool { + self.inner.responses.borrow().is_empty() + } +} + +enum ResponseSource { + Done, + Once(Option), + Stream(BodyStream), +} + +struct ActiveEgress { + attempt: ResponseEgressAttempt, + clock: MonotonicClock, + deadline: Deadline, + kind: EgressKind, + state: Rc, +} + +/// Private Hyper body that owns one response-egress attempt through source completion or drop. +pub(crate) struct AxumEgressBody { + active: Option, + remaining_hint: Option, + source: ResponseSource, + staged_final: Option, +} + +#[expect( + clippy::arbitrary_source_item_ordering, + reason = "constructors precede coordinator transitions and query helpers" +)] +impl AxumEgressBody { + #[cfg(test)] + pub(crate) fn application( + body: Body, + deadline: Deadline, + clock: MonotonicClock, + attempt: ResponseEgressAttempt, + connection: &EgressConnection, + ) -> Result> { + Self::owned( + body, + None, + true, + deadline, + clock, + attempt, + EgressKind::Application, + connection, + ) + } + + fn detached(bytes: Bytes) -> Self { + Self { + active: None, + remaining_hint: u64::try_from(bytes.len()).ok(), + source: if bytes.is_empty() { + ResponseSource::Done + } else { + ResponseSource::Once(Some(bytes)) + }, + staged_final: None, + } + } + + #[expect( + clippy::too_many_arguments, + reason = "the private constructor keeps the complete response-lifetime contract explicit" + )] + fn owned( + body: Body, + declared_length: Option, + transmits_body: bool, + deadline: Deadline, + clock: MonotonicClock, + mut attempt: ResponseEgressAttempt, + kind: EgressKind, + connection: &EgressConnection, + ) -> Result> { + let Some(state) = connection.register(deadline, &clock) else { + return Err(Box::new(attempt)); + }; + if !attempt.begin_writing() { + state.unregister(); + return Err(Box::new(attempt)); + } + let (payload, _scope) = body.into_parts(); + let (source, inferred_length) = if transmits_body { + match payload { + BodyPayload::Once(bytes) if bytes.is_empty() => (ResponseSource::Done, Some(0)), + BodyPayload::Once(bytes) => { + let length = u64::try_from(bytes.len()).ok(); + (ResponseSource::Once(Some(bytes)), length) } - Ok::, anyhow::Error>(buf) - }); - match result { - Ok(buf) => AxumBody::from(buf), - Err(err) => { - error!("streaming response error: {err}"); - return error_response_500("streaming response error"); + BodyPayload::Stream(stream) => (ResponseSource::Stream(stream), None), + } + } else { + drop(payload); + (ResponseSource::Done, Some(0)) + }; + Ok(Self { + active: Some(ActiveEgress { + attempt, + clock, + deadline, + kind, + state, + }), + remaining_hint: declared_length.or(inferred_length), + source, + staged_final: None, + }) + } + + fn account(&mut self, length: usize) -> Result<(), AxumBodyError> { + let Ok(accepted_length) = u64::try_from(length) else { + self.fail(ResponseEgressOutcome::TransportError); + return Err(AxumBodyError(ResponseEgressOutcome::TransportError)); + }; + if self.active.as_mut().is_some_and(|active| { + !active + .attempt + .account_bytes(accepted_length, active.clock.now()) + }) { + self.fail(ResponseEgressOutcome::TransportError); + return Err(AxumBodyError(ResponseEgressOutcome::TransportError)); + } + if let Some(remaining) = self.remaining_hint.as_mut() { + *remaining = remaining.saturating_sub(accepted_length); + } + Ok(()) + } + + fn fail(&mut self, outcome: ResponseEgressOutcome) { + self.source = ResponseSource::Done; + self.staged_final = None; + self.remaining_hint = Some(0); + let Some(mut active) = self.active.take() else { + return; + }; + let observed_at = if outcome == ResponseEgressOutcome::DeadlineExceeded { + active.clock.now().max(active.deadline.instant()) + } else { + active.clock.now() + }; + match active.kind { + EgressKind::Application => { + active.attempt.terminate(outcome, observed_at); + } + EgressKind::Fallback => { + active + .attempt + .finish_fallback(ResponseEgressFallbackDisposition::Aborted, observed_at); + } + } + active.state.unregister(); + } + + fn poll_chunk(&mut self, cx: &mut Context<'_>) -> Poll>> { + if self.staged_final.is_some() { + return self.poll_final_eof(cx); + } + let next = match &mut self.source { + ResponseSource::Done => Poll::Ready(None), + ResponseSource::Once(bytes) => { + let final_bytes = bytes.take(); + self.source = ResponseSource::Done; + Poll::Ready(final_bytes.map(Ok)) + } + ResponseSource::Stream(stream) => stream.as_mut().poll_next(cx), + }; + if matches!(self.source, ResponseSource::Stream(_)) + && let Some(remaining) = self.remaining_hint + && let Poll::Ready(Some(Ok(bytes))) = &next + && u64::try_from(bytes.len()) == Ok(remaining) + { + self.staged_final = Some(bytes.clone()); + return self.poll_final_eof(cx); + } + next + } + + // Hyper stops polling once Content-Length is satisfied, so validate EOF before handing it + // the final frame. At most that one source-controlled frame is retained during lookahead. + fn poll_final_eof(&mut self, cx: &mut Context<'_>) -> Poll>> { + let ResponseSource::Stream(stream) = &mut self.source else { + return Poll::Ready(Some(Err(EdgeError::internal(anyhow::anyhow!( + "response EOF lookahead lost its source" + ))))); + }; + match stream.as_mut().poll_next(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(None) => { + self.source = ResponseSource::Done; + Poll::Ready(self.staged_final.take().map(Ok)) + } + Poll::Ready(Some(Ok(bytes))) if bytes.is_empty() => { + cx.waker().wake_by_ref(); + Poll::Pending + } + Poll::Ready(Some(Ok(_bytes))) => Poll::Ready(Some(Err(EdgeError::internal( + ResponseEgressBodyLengthError::Exceeded, + )))), + Poll::Ready(Some(Err(error))) => Poll::Ready(Some(Err(error))), + } + } + + fn finish(&mut self) { + let Some(mut active) = self.active.take() else { + return; + }; + let observed_at = active.clock.now(); + match active.kind { + EgressKind::Application => { + active + .attempt + .terminate(ResponseEgressOutcome::HostHandoff, observed_at); + } + EgressKind::Fallback => { + active + .attempt + .finish_fallback(ResponseEgressFallbackDisposition::Completed, observed_at); + } + } + active.state.unregister(); + } + + fn requested_failure(&self) -> Option { + let active = self.active.as_ref()?; + active.state.requested_terminal.get() + } + + fn deadline_expired(&self) -> bool { + self.active + .as_ref() + .is_some_and(|active| active.deadline.is_expired_at(active.clock.now())) + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for AxumEgressBody { + fn drop(&mut self) { + if self.active.is_none() { + return; + } + if let Some(outcome) = self.requested_failure() { + self.fail(outcome); + return; + } + if matches!( + self.source, + ResponseSource::Done | ResponseSource::Once(None) + ) { + self.finish(); + return; + } + self.fail(ResponseEgressOutcome::TransportError); + } +} + +impl HttpBody for AxumEgressBody { + type Data = Bytes; + type Error = AxumBodyError; + + fn is_end_stream(&self) -> bool { + matches!( + self.source, + ResponseSource::Done | ResponseSource::Once(None) + ) && self.staged_final.is_none() + } + + fn poll_frame( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + let this = self.get_mut(); + if this.active.is_none() && matches!(this.source, ResponseSource::Done) { + return Poll::Ready(None); + } + if let Some(outcome) = this.requested_failure() { + this.fail(outcome); + return Poll::Ready(Some(Err(AxumBodyError(outcome)))); + } + if this.deadline_expired() { + this.fail(ResponseEgressOutcome::DeadlineExceeded); + return Poll::Ready(Some(Err(AxumBodyError( + ResponseEgressOutcome::DeadlineExceeded, + )))); + } + + let next = this.poll_chunk(cx); + if let (Some(active), Poll::Ready(Some(Err(error)))) = (&mut this.active, &next) { + active.attempt.record_source_error(error); + } + if !matches!(next, Poll::Pending) { + if let Some(outcome) = this.requested_failure() { + this.fail(outcome); + return Poll::Ready(Some(Err(AxumBodyError(outcome)))); + } + if this.deadline_expired() { + this.source = ResponseSource::Done; + this.fail(ResponseEgressOutcome::DeadlineExceeded); + return Poll::Ready(Some(Err(AxumBodyError( + ResponseEgressOutcome::DeadlineExceeded, + )))); + } + } + match next { + Poll::Pending => Poll::Pending, + Poll::Ready(Some(Ok(bytes))) => { + if let Err(error) = this.account(bytes.len()) { + return Poll::Ready(Some(Err(error))); + } + Poll::Ready(Some(Ok(Frame::data(bytes)))) + } + Poll::Ready(Some(Err(error))) => { + log::warn!( + "response-egress Axum body source failed after commit: {}", + response_egress_source_error_category(&error) + ); + this.source = ResponseSource::Done; + this.fail(ResponseEgressOutcome::SourceError); + Poll::Ready(Some(Err(AxumBodyError(ResponseEgressOutcome::SourceError)))) + } + Poll::Ready(None) => { + this.source = ResponseSource::Done; + this.finish(); + Poll::Ready(None) + } + } + } + + fn size_hint(&self) -> SizeHint { + let mut hint = SizeHint::new(); + if let Some(remaining) = self.remaining_hint { + hint.set_exact(remaining); + } + hint + } +} + +/// Converts one owned core envelope into the private body used by `EdgeZero`'s Hyper server. +pub(crate) async fn prepare_egress_response( + egress: ResponseEgressEnvelope, + connection: &EgressConnection, +) -> Response { + let request_method = egress.request_method().clone(); + match egress.begin(ResponseEgressFramingMode::ExplicitBufferedLength) { + Ok((prepared, policy, mut attempt, clock)) => { + if policy.write_deadline.is_expired_at(clock.now()) { + return prepare_fallback( + &request_method, + ResponseEgressOutcome::DeadlineExceeded, + attempt, + &clock, + connection, + ); + } + let validated = match validate_empty_stream( + prepared, + &request_method, + policy.write_deadline, + &clock, + &mut attempt, + ) + .await + { + Ok(validated) => validated, + Err(cause) => { + return prepare_fallback(&request_method, cause, attempt, &clock, connection); + } + }; + match start_prepared( + validated, + policy.write_deadline, + attempt, + clock.clone(), + EgressKind::Application, + connection, + ) { + Ok(response) => response, + Err(returned_attempt) => { + let cause = if policy.write_deadline.is_expired_at(clock.now()) { + ResponseEgressOutcome::DeadlineExceeded + } else { + ResponseEgressOutcome::ConversionError + }; + prepare_fallback( + &request_method, + cause, + *returned_attempt, + &clock, + connection, + ) } } } + Err(failure) => { + let (attempt, clock, cause) = failure.into_parts(); + prepare_fallback(&request_method, cause, attempt, &clock, connection) + } + } +} + +async fn validate_empty_stream( + prepared: PreparedResponseEgress, + request_method: &Method, + deadline: Deadline, + clock: &MonotonicClock, + attempt: &mut ResponseEgressAttempt, +) -> Result { + if !prepared.transmits_body() || prepared.declared_length() != Some(0) { + return Ok(prepared); + } + let mut response = prepared.into_response(); + let (payload, _scope) = mem::take(response.body_mut()).into_parts(); + if let BodyPayload::Stream(mut source) = payload { + let remaining = deadline + .remaining_at(clock.now()) + .ok_or(ResponseEgressOutcome::DeadlineExceeded)?; + let validation = async { + loop { + if deadline.is_expired_at(clock.now()) { + return Err(ResponseEgressOutcome::DeadlineExceeded); + } + let next = source.next().await; + if let Some(Err(error)) = &next { + attempt.record_source_error(error); + } + if deadline.is_expired_at(clock.now()) { + return Err(ResponseEgressOutcome::DeadlineExceeded); + } + match next { + None => return Ok(()), + Some(Ok(bytes)) if bytes.is_empty() => yield_now().await, + Some(Ok(_bytes)) => return Err(ResponseEgressOutcome::ConversionError), + Some(Err(error)) => { + log::warn!( + "response-egress Axum zero-length source failed before commit: {}", + response_egress_source_error_category(&error) + ); + return Err(ResponseEgressOutcome::ConversionError); + } + } + } + }; + timeout(remaining, validation) + .await + .map_err(|_elapsed| ResponseEgressOutcome::DeadlineExceeded)??; + } + attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + request_method, + response, + ) + .map_err(|_error| ResponseEgressOutcome::ConversionError) +} + +fn start_prepared( + prepared: PreparedResponseEgress, + deadline: Deadline, + attempt: ResponseEgressAttempt, + clock: MonotonicClock, + kind: EgressKind, + connection: &EgressConnection, +) -> Result, Box> { + let declared_length = prepared.declared_length(); + let transmits_body = prepared.transmits_body(); + let (parts, core_body) = prepared.into_response().into_parts(); + match AxumEgressBody::owned( + core_body, + declared_length, + transmits_body, + deadline, + clock, + attempt, + kind, + connection, + ) { + Ok(egress_body) => Ok(Response::from_parts(parts, egress_body)), + Err(returned_attempt) => Err(returned_attempt), + } +} + +fn prepare_fallback( + request_method: &Method, + cause: ResponseEgressOutcome, + mut attempt: ResponseEgressAttempt, + clock: &MonotonicClock, + connection: &EgressConnection, +) -> Response { + let headers = attempt.synthesized_response_headers(); + if !attempt.begin_fallback(cause) { + attempt.terminate(ResponseEgressOutcome::ConversionError, clock.now()); + return detached_fallback( + request_method, + ResponseEgressOutcome::ConversionError, + &headers, + ); + } + let started_at = clock.now(); + let Some(fallback_deadline) = started_at.checked_add(RESPONSE_EGRESS_FALLBACK_SAFETY_BUDGET) + else { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, clock.now()); + return detached_fallback(request_method, cause, &headers); + }; + let fallback = fallback_response(cause); + let Ok(prepared) = attempt.prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + request_method, + fallback, + ) else { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, clock.now()); + return detached_fallback(request_method, cause, &headers); }; + match start_prepared( + prepared, + Deadline::at_instant(fallback_deadline), + attempt, + clock.clone(), + EgressKind::Fallback, + connection, + ) { + Ok(response) => response, + Err(mut returned_attempt) => { + returned_attempt + .finish_fallback(ResponseEgressFallbackDisposition::Aborted, clock.now()); + detached_fallback(request_method, cause, &headers) + } + } +} - Response::from_parts(parts, body) +fn fallback_response(cause: ResponseEgressOutcome) -> CoreResponse { + let (status, body) = fallback_parts(cause); + let mut response = CoreResponse::new(Body::from(body)); + *response.status_mut() = status; + response } -/// Build a minimal 500 response without any builder steps that could fail. -/// Used as a fallback on the request path so we never panic on synthesis. -fn error_response_500(message: &'static str) -> Response { - let mut response = Response::new(AxumBody::from(message)); - *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - response.headers_mut().insert( - CONTENT_TYPE, - HeaderValue::from_static("text/plain; charset=utf-8"), - ); +fn detached_fallback( + request_method: &Method, + cause: ResponseEgressOutcome, + headers: &edgezero_core::SynthesizedResponseHeaders, +) -> Response { + let (status, body) = fallback_parts(cause); + let payload = if *request_method == Method::HEAD { + Bytes::new() + } else { + Bytes::from_static(body) + }; + let mut response = Response::new(AxumEgressBody::detached(payload)); + *response.status_mut() = status; + headers.apply(response.headers_mut()); response } +const fn fallback_parts(cause: ResponseEgressOutcome) -> (StatusCode, &'static [u8]) { + if matches!(cause, ResponseEgressOutcome::DeadlineExceeded) { + (StatusCode::GATEWAY_TIMEOUT, TIMEOUT_FALLBACK_BODY) + } else { + (StatusCode::INTERNAL_SERVER_ERROR, INTERNAL_FALLBACK_BODY) + } +} + #[cfg(test)] mod tests { - use super::*; + use std::collections::VecDeque; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::Poll; + use std::time::Duration; + + use bytes::Bytes; + use edgezero_core::action; + use edgezero_core::app::App; use edgezero_core::body::Body; - use edgezero_core::http::{StatusCode, response_builder}; - use futures::stream; - - #[test] - fn converts_core_response_stream_into_axum_body() { - let stream = stream::iter(vec![ - Ok::<_, anyhow::Error>(bytes::Bytes::from_static(b"hel")), - Ok(bytes::Bytes::from_static(b"lo")), - ]); - let body = Body::from_stream(stream); - let response = response_builder() - .status(StatusCode::OK) - .header("content-type", "text/plain") - .body(body) - .expect("response"); + use edgezero_core::error::{BadGatewayReason, EdgeError}; + use edgezero_core::error_detail::{ + AxumFailureCause, AxumFailureKind, ProviderFailureDetail, ProviderFailurePhase, + }; + use edgezero_core::extractor::State; + use edgezero_core::http::{ + HeaderMap, Method, Response as CoreResponse, StatusCode, Version, request_builder, + response_builder, + }; + use edgezero_core::ingress::{IngressDispatchOutcome, IngressFraming, IngressHeadAccounting}; + use edgezero_core::response_egress::{ + ResponseEgressAttempt, ResponseEgressBodyKind, ResponseEgressCompletion, + ResponseEgressHead, ResponseEgressObserver, ResponseEgressObserverHandle, + ResponseEgressOutcome, ResponseEgressPolicy, ResponseEgressReport, + }; + use edgezero_core::response_egress_framing::{ + ResponseEgressFramingMode, prepare_response_egress, + }; + use edgezero_core::router::{RouteMetadata, RouterService}; + use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; + use futures_util::future::poll_fn as poll_future; + use futures_util::stream::{pending, poll_fn as poll_stream}; + use futures_util::{FutureExt as _, StreamExt as _, stream}; + use http_body::Body as _; - let axum_response = into_axum_response(response); - assert_eq!(axum_response.status(), StatusCode::OK); + use super::{ + AxumEgressBody, EgressConnection, EgressKind, INTERNAL_FALLBACK_BODY, + TIMEOUT_FALLBACK_BODY, prepare_fallback, start_prepared, + }; + + #[derive(Clone, Default)] + struct RecordingObserver(Arc>>); + + #[derive(Clone)] + struct SourceCase { + declared: u64, + expired: Arc, + late: bool, + } + + #[action] + async fn failing_response(State(case): State) -> Result { + let source = stream::once(async move { + case.expired.store(case.late, Ordering::SeqCst); + Err(EdgeError::bad_gateway_with_detail( + "provider-secret-token", + BadGatewayReason::Transport, + ProviderFailureDetail::Axum { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: AxumFailureCause { + kind: AxumFailureKind::Decode, + is_connect: false, + is_timeout: false, + io_kind: None, + }, + }, + )) + }); + let prefix = stream::iter((case.declared != 0).then(|| Ok(Bytes::from_static(b"x")))); + response_builder() + .header("content-length", case.declared) + .body(Body::from_stream(prefix.chain(source))) + .map_err(EdgeError::internal) + } + + #[action] + async fn rendered_failure() -> Result { + Err(EdgeError::bad_gateway_with_detail( + "provider-secret-token", + BadGatewayReason::Transport, + ProviderFailureDetail::Axum { + phase: ProviderFailurePhase::Send, + cause: AxumFailureCause { + kind: AxumFailureKind::Request, + is_connect: false, + is_timeout: false, + io_kind: None, + }, + }, + )) + } + + impl ResponseEgressObserver for RecordingObserver { + fn complete(&self, report: &ResponseEgressReport) { + self.0.lock().expect("reports lock").push(report.clone()); + } + } + + fn scripted_clock(script: Vec) -> MonotonicClock { + let observations = Arc::new(Mutex::new(VecDeque::from(script))); + MonotonicClock::new(move || { + observations + .lock() + .expect("clock observations") + .pop_front() + .expect("clock observation") + }) + } + + fn attempt(observer: &RecordingObserver, now: MonotonicInstant) -> ResponseEgressAttempt { + let headers = HeaderMap::new(); + let route = RouteMetadata::new(Method::GET, "/test"); + let head = ResponseEgressHead::new( + StatusCode::OK, + Version::HTTP_11, + &headers, + None, + now, + Some(&route), + ); + ResponseEgressAttempt::new( + &head, + now, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + ) + } + + #[tokio::test(flavor = "current_thread")] + async fn generated_overflow_retry_after_precedes_synthesized_headers() { + use edgezero_core::RequestBodyOverflowOrigin; + use edgezero_core::response_egress_framing::test_support::synthesized_error_fixture; + for (error, retry) in [ + ( + EdgeError::payload_too_large(RequestBodyOverflowOrigin::Inbound), + None, + ), + ( + EdgeError::payload_too_large(RequestBodyOverflowOrigin::OutboundUpload), + None, + ), + ( + EdgeError::config_out_of_date("private", "field"), + Some("60"), + ), + ] { + let envelope = synthesized_error_fixture(error).expect("fixture"); + let connection = EgressConnection::default(); + let response = super::prepare_egress_response(envelope, &connection).await; + assert_eq!(response.headers()["x-synthesized-fixture"], "configured"); + assert_eq!( + response + .headers() + .get("retry-after") + .map(|value| value.to_str().expect("text")), + retry + ); + let mut body = Box::pin(response.into_body()); + while let Some(frame) = poll_future(|cx| body.as_mut().poll_frame(cx)).await { + frame.expect("body frame"); + } + drop(body); + assert!(connection.is_empty()); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn framing_observer_preserves_each_typed_reason() { + use edgezero_core::response_egress_framing::test_support::{ + FRAMING_RULES, framing_fixture, has_full_synthesized_fixture_headers, + }; + for reason in FRAMING_RULES { + for detached in [false, true] { + let fixture = framing_fixture(reason, detached).await.expect("fixture"); + let connection = EgressConnection::default(); + let response = super::prepare_egress_response(fixture.envelope, &connection).await; + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(response.headers()["x-synthesized-fixture"], "configured"); + assert!(has_full_synthesized_fixture_headers(response.headers())); + let mut pinned = Box::pin(response.into_body()); + let mut bytes = Vec::new(); + while let Some(frame) = poll_future(|cx| pinned.as_mut().poll_frame(cx)).await { + bytes.extend_from_slice( + frame.expect("fallback frame").data_ref().expect("data"), + ); + } + drop(pinned); + assert_eq!(bytes, INTERNAL_FALLBACK_BODY); + let completed = fixture.completed.lock().expect("completion"); + assert_eq!(completed.len(), 1); + assert_eq!(completed[0].framing_failures.application(), Some(reason)); + assert_eq!(completed[0].framing_failures.fallback(), None); + assert_eq!(completed[0].outcome, ResponseEgressOutcome::ConversionError); + let observed = fixture.observed.lock().expect("observer"); + if detached { + assert!(observed.is_empty()); + } else { + assert_eq!(*observed, *completed); + } + assert!(connection.is_empty()); + } + } + } + + #[tokio::test(flavor = "current_thread")] + async fn provider_diagnostics_reach_response_observer_before_deadline_arbitration() { + let detail = ProviderFailureDetail::Axum { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: AxumFailureCause { + kind: AxumFailureKind::Decode, + is_connect: false, + is_timeout: false, + io_kind: None, + }, + }; + for with_detail in [false, true] { + for late in [false, true] { + let now = MonotonicInstant::now(); + let deadline = now.checked_add(Duration::from_secs(10)).expect("deadline"); + let expired = Arc::new(AtomicBool::new(false)); + let clock_expired = Arc::clone(&expired); + let clock = MonotonicClock::new(move || { + if clock_expired.load(Ordering::SeqCst) { + deadline + } else { + now + } + }); + let error = if with_detail { + EdgeError::bad_gateway_with_detail( + "provider-secret-token", + BadGatewayReason::Transport, + detail, + ) + } else { + EdgeError::bad_gateway_with_reason( + "provider-secret-token", + BadGatewayReason::Transport, + ) + }; + let source = stream::once(async move { + expired.store(late, Ordering::SeqCst); + Err(error) + }); + let observer = RecordingObserver::default(); + let connection = EgressConnection::default(); + let Ok(body) = AxumEgressBody::application( + Body::from_stream(source), + Deadline::at_instant(deadline), + clock, + attempt(&observer, now), + &connection, + ) else { + panic!("body registration") + }; + let mut pinned = Box::pin(body); + poll_future(|cx| pinned.as_mut().poll_frame(cx)) + .await + .expect("failure frame") + .expect_err("source failure"); + drop(pinned); + let reports = observer.0.lock().expect("reports"); + assert_eq!(reports.len(), 1); + assert_eq!( + reports[0].outcome, + if late { + ResponseEgressOutcome::DeadlineExceeded + } else { + ResponseEgressOutcome::SourceError + } + ); + let failure = reports[0] + .source_failure + .as_ref() + .expect("source observation"); + assert_eq!(reports[0].framing_failures.application(), None); + assert_eq!(reports[0].framing_failures.fallback(), None); + assert_eq!(failure.category(), "bad_gateway_transport"); + assert_eq!( + failure.provider_failure_detail(), + with_detail.then_some(&detail) + ); + assert!(!format!("{failure:?}").contains("secret")); + assert!(connection.is_empty()); + } + } + } + + #[tokio::test(flavor = "current_thread")] + async fn provider_diagnostics_survive_zero_length_fallback_and_final_eof_lookahead() { + for declared in [0, 1] { + for late in [false, true] { + let now = MonotonicInstant::now(); + let deadline = now.checked_add(Duration::from_secs(10)).expect("deadline"); + let expired = Arc::new(AtomicBool::new(false)); + let clock_expired = Arc::clone(&expired); + let mut app = App::new( + RouterService::builder() + .with_state(SourceCase { + declared, + expired, + late, + }) + .get("/", failing_response) + .build(), + ); + app.set_monotonic_clock(MonotonicClock::new(move || { + if clock_expired.load(Ordering::SeqCst) { + deadline + } else { + now + } + })); + app.set_response_egress_policy(move |_, _| ResponseEgressPolicy { + write_deadline: Deadline::at_instant(deadline), + }); + let observer = RecordingObserver::default(); + app.set_response_egress_observer(observer.clone()); + let request = request_builder() + .uri("/") + .body(Body::empty()) + .expect("request"); + let outcome = app + .dispatch_ingress( + request, + now, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ) + .await + .expect("dispatch"); + let IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("response envelope") + }; + let connection = EgressConnection::default(); + let response = super::prepare_egress_response(*envelope, &connection).await; + let mut pinned = Box::pin(response.into_body()); + while let Some(frame) = poll_future(|cx| pinned.as_mut().poll_frame(cx)).await { + if frame.is_err() { + break; + } + } + drop(pinned); + let reports = observer.0.lock().expect("reports"); + assert_eq!(reports.len(), 1); + assert_eq!( + reports[0].outcome, + if late { + ResponseEgressOutcome::DeadlineExceeded + } else if declared == 0 { + ResponseEgressOutcome::ConversionError + } else { + ResponseEgressOutcome::SourceError + } + ); + assert_eq!( + reports[0].body_kind, + if declared == 0 { + ResponseEgressBodyKind::Fallback + } else { + ResponseEgressBodyKind::Application + } + ); + let failure = reports[0].source_failure.as_ref().expect("observed source"); + assert_eq!(failure.body_kind(), ResponseEgressBodyKind::Application); + assert_eq!(failure.category(), "bad_gateway_transport"); + assert!(matches!( + failure.provider_failure_detail(), + Some(ProviderFailureDetail::Axum { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: AxumFailureCause { + kind: AxumFailureKind::Decode, + is_connect: false, + is_timeout: false, + io_kind: None + }, + }) + )); + assert!(connection.is_empty()); + } + } + } + + #[tokio::test(flavor = "current_thread")] + async fn rendered_application_error_is_not_a_body_source_failure() { + let observer = RecordingObserver::default(); + let mut app = App::new(RouterService::builder().get("/", rendered_failure).build()); + app.set_response_egress_observer(observer.clone()); + let request = request_builder() + .uri("/") + .body(Body::empty()) + .expect("request"); + let outcome = app + .dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ) + .await + .expect("dispatch"); + let IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("response envelope") + }; + let connection = EgressConnection::default(); + let response = super::prepare_egress_response(*envelope, &connection).await; + assert_eq!(response.status(), StatusCode::BAD_GATEWAY); + let mut pinned = Box::pin(response.into_body()); + let mut bytes = Vec::new(); + while let Some(frame) = poll_future(|cx| pinned.as_mut().poll_frame(cx)).await { + bytes.extend_from_slice(frame.expect("rendered frame").data_ref().expect("data")); + assert!(bytes.len() <= 1024); + } + drop(pinned); + assert!(!String::from_utf8_lossy(&bytes).contains("secret")); + let reports = observer.0.lock().expect("reports"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::HostHandoff); + assert!(reports[0].source_failure.is_none()); + assert!(connection.is_empty()); + } + + #[tokio::test(flavor = "current_thread")] + async fn body_is_lazy_and_accounts_each_frame_before_clean_handoff() { + let observer = RecordingObserver::default(); + let polls = Arc::new(AtomicUsize::new(0)); + let source_polls = Arc::clone(&polls); + let source = poll_stream(move |_cx| { + let poll_index = source_polls.fetch_add(1, Ordering::SeqCst); + Poll::Ready(match poll_index { + 0 => Some(Ok(Bytes::from_static(b"abc"))), + 1 => Some(Ok(Bytes::from_static(b"de"))), + _ => None, + }) + }); + let now = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || now); + let deadline = + Deadline::at_instant(now.checked_add(Duration::from_secs(1)).expect("deadline")); + let connection = EgressConnection::default(); + let body_result = AxumEgressBody::application( + Body::from_stream(source), + deadline, + clock, + attempt(&observer, now), + &connection, + ); + let Ok(registered_body) = body_result else { + panic!("register body"); + }; + let mut pinned_body = Box::pin(registered_body); + + assert_eq!(polls.load(Ordering::SeqCst), 0); + let first = poll_future(|cx| pinned_body.as_mut().poll_frame(cx)) + .await + .expect("first frame") + .expect("first frame result") + .into_data() + .expect("first data"); + assert_eq!(first, Bytes::from_static(b"abc")); + assert!(observer.0.lock().expect("reports lock").is_empty()); + + let second = poll_future(|cx| pinned_body.as_mut().poll_frame(cx)) + .await + .expect("second frame") + .expect("second frame result") + .into_data() + .expect("second data"); + assert_eq!(second, Bytes::from_static(b"de")); + assert!( + poll_future(|cx| pinned_body.as_mut().poll_frame(cx)) + .await + .is_none() + ); + + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].bytes_written, 5); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::HostHandoff); + assert!(reports[0].source_failure.is_none()); + assert!(connection.is_empty()); + } + + #[tokio::test(flavor = "current_thread")] + async fn dropping_unfinished_body_reports_transport_error_once() { + let observer = RecordingObserver::default(); + let now = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || now); + let deadline = + Deadline::at_instant(now.checked_add(Duration::from_secs(1)).expect("deadline")); + let connection = EgressConnection::default(); + let body_result = AxumEgressBody::application( + Body::from_stream(pending()), + deadline, + clock, + attempt(&observer, now), + &connection, + ); + let Ok(registered_body) = body_result else { + panic!("register body"); + }; + + drop(registered_body); + + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].bytes_written, 0); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::TransportError); + assert!(connection.is_empty()); + } + + #[tokio::test(flavor = "current_thread")] + async fn dropping_handed_off_frame_after_connection_abort_reports_transport_error() { + let observer = RecordingObserver::default(); + let now = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || now); + let deadline = + Deadline::at_instant(now.checked_add(Duration::from_secs(1)).expect("deadline")); + let connection = EgressConnection::default(); + let body_result = AxumEgressBody::application( + Body::from(Bytes::from_static(b"sent to Hyper")), + deadline, + clock, + attempt(&observer, now), + &connection, + ); + let Ok(body) = body_result else { + panic!("register body"); + }; + let mut pinned_body = Box::pin(body); + + let frame = poll_future(|cx| pinned_body.as_mut().poll_frame(cx)) + .await + .expect("one frame") + .expect("data frame"); assert_eq!( - axum_response - .headers() - .get("content-type") - .expect("header") - .to_str() - .unwrap(), - "text/plain" + frame.into_data().expect("data"), + Bytes::from_static(b"sent to Hyper") + ); + assert!(observer.0.lock().expect("reports lock").is_empty()); + + connection.signal_transport_error(); + drop(pinned_body); + + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::TransportError); + assert!(connection.is_empty()); + } + + #[tokio::test(flavor = "current_thread")] + async fn response_egress_post_ready_deadline_precedence() { + let observer = RecordingObserver::default(); + let now = MonotonicInstant::now(); + let deadline = now.checked_add(Duration::from_secs(1)).expect("deadline"); + let clock = scripted_clock(vec![now, now, deadline, deadline]); + let connection = EgressConnection::default(); + let body_result = AxumEgressBody::application( + Body::from_stream(poll_stream(|_context| { + Poll::Ready(Some(Ok(Bytes::from_static(b"late")))) + })), + Deadline::at_instant(deadline), + clock, + attempt(&observer, now), + &connection, ); + let Ok(body) = body_result else { + panic!("register body"); + }; + let mut pinned_body = Box::pin(body); - let collected = block_on(async { - let mut data = Vec::new(); - let mut body_stream = axum_response.into_body().into_data_stream(); - while let Some(result) = body_stream.next().await { - let chunk = result.expect("chunk"); - data.extend_from_slice(&chunk); + let error = poll_future(|context| pinned_body.as_mut().poll_frame(context)) + .await + .expect("terminal frame") + .expect_err("deadline must win over ready source data"); + + assert!(error.is_deadline()); + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].bytes_written, 0); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert!(connection.is_empty()); + } + + #[tokio::test(flavor = "current_thread")] + async fn emergency_fallback_retains_synthesized_headers_and_single_completion() { + use edgezero_core::ResponseEgressFramingError; + use edgezero_core::response_egress_framing::test_support::framing_fixture; + for method in [Method::GET, Method::HEAD] { + let fixture = framing_fixture(ResponseEgressFramingError::UnsupportedTrailers, false) + .await + .expect("fixture"); + let Err(failure) = fixture + .envelope + .begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("framing failure"); + }; + let (attempt, _, cause) = failure.into_parts(); + let started = MonotonicInstant::now(); + let ticks = Arc::new(AtomicUsize::new(0)); + let clock = MonotonicClock::new(move || { + let tick = u64::try_from(ticks.fetch_add(1, Ordering::SeqCst)).expect("tick"); + started + Duration::from_secs(2 * tick) + }); + let connection = EgressConnection::default(); + let response = prepare_fallback(&method, cause, attempt, &clock, &connection); + assert_eq!(response.headers()["x-synthesized-fixture"], "configured"); + let mut body = Box::pin(response.into_body()); + let mut bytes = Vec::new(); + while let Some(frame) = poll_future(|cx| body.as_mut().poll_frame(cx)).await { + bytes.extend_from_slice(frame.expect("frame").data_ref().expect("data")); + } + assert_eq!( + bytes.as_slice(), + if method == Method::HEAD { + b"" + } else { + INTERNAL_FALLBACK_BODY + } + ); + drop(body); + assert_eq!(fixture.completed.lock().expect("completion").len(), 1); + assert!(connection.is_empty()); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn expired_fallback_registration_preserves_get_payload_and_head_suppression() { + for (method, cause, payload) in [ + ( + Method::GET, + ResponseEgressOutcome::DeadlineExceeded, + TIMEOUT_FALLBACK_BODY, + ), + ( + Method::HEAD, + ResponseEgressOutcome::DeadlineExceeded, + TIMEOUT_FALLBACK_BODY, + ), + ( + Method::GET, + ResponseEgressOutcome::ConversionError, + INTERNAL_FALLBACK_BODY, + ), + ( + Method::HEAD, + ResponseEgressOutcome::ConversionError, + INTERNAL_FALLBACK_BODY, + ), + ] { + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let clock_polls = Arc::new(AtomicUsize::new(0)); + let clock = MonotonicClock::new(move || { + let tick = + u64::try_from(clock_polls.fetch_add(1, Ordering::SeqCst)).expect("clock tick"); + started_at + .checked_add(Duration::from_secs(tick * 2)) + .expect("clock sample") + }); + let connection = EgressConnection::default(); + let response = prepare_fallback( + &method, + cause, + attempt(&observer, started_at), + &clock, + &connection, + ); + let status = if cause == ResponseEgressOutcome::DeadlineExceeded { + StatusCode::GATEWAY_TIMEOUT + } else { + StatusCode::INTERNAL_SERVER_ERROR + }; + assert_eq!(response.status(), status); + let mut body = Box::pin(response.into_body()); + if method == Method::HEAD { + assert!(body.is_end_stream()); + assert!( + poll_future(|context| body.as_mut().poll_frame(context)) + .await + .is_none() + ); + } else { + assert!(!body.is_end_stream(), "detached payload is not exhausted"); + let frame = poll_future(|context| body.as_mut().poll_frame(context)) + .await + .expect("fallback frame") + .expect("fallback result"); + assert_eq!(frame.into_data().expect("payload"), payload); + assert!( + body.is_end_stream(), + "final detached frame exhausts the source" + ); + assert!( + poll_future(|context| body.as_mut().poll_frame(context)) + .await + .is_none() + ); + } + assert!(connection.is_empty()); + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, cause); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn declared_stream_is_incremental_and_releases_staged_data_on_failure() { + struct DropProbe(Arc); + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropProbe { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + let observer = RecordingObserver::default(); + let polls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let source_polls = Arc::clone(&polls); + let drop_probe = DropProbe(Arc::clone(&drops)); + let source = poll_stream(move |_context| { + let _keep_source_alive = &drop_probe; + match source_polls.fetch_add(1, Ordering::SeqCst) { + 0 => Poll::Ready(Some(Ok(Bytes::from_static(b"ab")))), + 1 => Poll::Ready(Some(Ok(Bytes::from_static(b"c")))), + _ => Poll::Pending, } - data }); + let response = response_builder() + .header("content-length", "3") + .body(Body::from_stream(source)) + .expect("response"); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + response, + ) + .expect("prepared response"); + let started_at = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || started_at); + let connection = EgressConnection::default(); + let Ok(registered_response) = start_prepared( + prepared, + clock.deadline_after(Duration::from_secs(1)), + attempt(&observer, started_at), + clock, + EgressKind::Application, + &connection, + ) else { + panic!("registered response"); + }; + let mut body = Box::pin(registered_response.into_body()); + + let first = poll_future(|context| body.as_mut().poll_frame(context)) + .await + .expect("first frame") + .expect("valid frame"); + assert_eq!(first.into_data().expect("data"), "ab"); + assert_eq!(polls.load(Ordering::SeqCst), 1, "no eager whole-body read"); + assert!( + poll_future(|context| body.as_mut().poll_frame(context)) + .now_or_never() + .is_none() + ); + assert_eq!(polls.load(Ordering::SeqCst), 3); + assert_eq!(drops.load(Ordering::SeqCst), 0); - assert_eq!(collected, b"hello"); + connection.signal_transport_error(); + let error = poll_future(|context| body.as_mut().poll_frame(context)) + .await + .expect("failure frame") + .expect_err("transport signal wins"); + assert_eq!(error.0, ResponseEgressOutcome::TransportError); + assert!(body.is_end_stream()); + assert_eq!( + drops.load(Ordering::SeqCst), + 1, + "failed source drops before body drop" + ); + assert!( + poll_future(|context| body.as_mut().poll_frame(context)) + .await + .is_none() + ); + drop(body); + let reports = observer.0.lock().expect("reports lock"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].bytes_written, 2); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::TransportError); + assert!(connection.is_empty()); } } diff --git a/crates/edgezero-adapter-axum/src/secret_store.rs b/crates/edgezero-adapter-axum/src/secret_store.rs index abf1c3d03..70ac1f058 100644 --- a/crates/edgezero-adapter-axum/src/secret_store.rs +++ b/crates/edgezero-adapter-axum/src/secret_store.rs @@ -13,6 +13,7 @@ use std::env; use async_trait::async_trait; use bytes::Bytes; use edgezero_core::secret_store::{SecretError, SecretStore}; +use edgezero_core::{BoundedStoreRead, Deadline, MonotonicClock}; use crate::secret_env::secret_env_var; @@ -68,6 +69,82 @@ impl SecretStore for EnvSecretStore { } } } + + #[inline] + async fn get_bytes_bounded( + &self, + _store_name: &str, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError> { + if deadline.is_expired_at(clock.now()) { + return Err(SecretError::DeadlineExceeded); + } + + let var = secret_env_var(key).map_err(SecretError::Validation)?; + + #[cfg(unix)] + let stored_value = env::var_os(&var); + + #[cfg(not(unix))] + let stored_value = env::var(&var); + + if deadline.is_expired_at(clock.now()) { + return Err(SecretError::DeadlineExceeded); + } + + #[cfg(not(unix))] + let stored_value = match stored_value { + Ok(value) => Some(value), + Err(std::env::VarError::NotPresent) => None, + Err(std::env::VarError::NotUnicode(_)) => { + return Err(SecretError::Internal(anyhow::anyhow!( + "secret store returned an invalid Unicode value" + ))); + } + }; + + #[cfg(unix)] + let backend_bytes = { + use std::os::unix::ffi::OsStrExt as _; + + stored_value.as_ref().map_or(Ok(0_u64), |value| { + u64::try_from(value.as_os_str().as_bytes().len()) + .map_err(|_length_error| SecretError::ValueTooLarge) + })? + }; + + #[cfg(not(unix))] + let backend_bytes = stored_value.as_ref().map_or(Ok(0_u64), |value| { + u64::try_from(value.len()).map_err(|_length_error| SecretError::ValueTooLarge) + })?; + + if backend_bytes > max_backend_bytes || backend_bytes > max_value_bytes { + return Err(SecretError::ValueTooLarge); + } + + #[cfg(unix)] + let value = { + use std::os::unix::ffi::OsStringExt as _; + + stored_value.map(|value| Bytes::from(value.into_vec())) + }; + + #[cfg(not(unix))] + let value = stored_value.map(|value| Bytes::from(value.into_bytes())); + + if deadline.is_expired_at(clock.now()) { + return Err(SecretError::DeadlineExceeded); + } + + Ok(BoundedStoreRead { + backend_bytes, + value, + }) + } } #[cfg(test)] @@ -85,17 +162,201 @@ mod tests { use super::*; use crate::test_utils::env_guard; use bytes::Bytes; - use edgezero_core::secret_store::InMemorySecretStore; + use edgezero_core::secret_store::{InMemorySecretStore, SecretHandle}; use edgezero_core::secret_store_contract_tests; use edgezero_core::test_env::EnvOverride; + use edgezero_core::{Deadline, MonotonicClock, MonotonicInstant}; use futures::executor::block_on; #[cfg(unix)] use std::ffi::OsString; + use std::sync::Arc; + use std::time::Duration; + + #[tokio::test(flavor = "current_thread")] + async fn bounded_get_bytes_accepts_exact_caps_and_reports_exact_backend_bytes() { + let _guard = env_guard().lock().await; + let _env = EnvOverride::set( + "EDGEZERO__SECRETS____EDGEZERO_TEST_BOUNDED_SECRET__", + "hello", + ); + let handle = SecretHandle::new(Arc::new(EnvSecretStore::new())); + let clock = MonotonicClock::default(); + + let read = handle + .get_bytes_bounded( + "env", + "__EDGEZERO_TEST_BOUNDED_SECRET__", + &clock, + Deadline::after(Duration::from_secs(1)), + 5, + 5, + ) + .await + .expect("exact cap must succeed"); + + assert_eq!(read.backend_bytes, 5); + assert_eq!(read.value, Some(Bytes::from_static(b"hello"))); + } + + #[tokio::test(flavor = "current_thread")] + async fn bounded_get_bytes_rejects_either_cap() { + let _guard = env_guard().lock().await; + let _env = EnvOverride::set( + "EDGEZERO__SECRETS____EDGEZERO_TEST_OVERSIZE_SECRET__", + "hello", + ); + let handle = SecretHandle::new(Arc::new(EnvSecretStore::new())); + let clock = MonotonicClock::default(); + + for (max_backend_bytes, max_value_bytes) in [(4, 5), (5, 4)] { + let error = handle + .get_bytes_bounded( + "env", + "__EDGEZERO_TEST_OVERSIZE_SECRET__", + &clock, + Deadline::after(Duration::from_secs(1)), + max_backend_bytes, + max_value_bytes, + ) + .await + .expect_err("over-cap secret must fail"); + + assert!(matches!(error, SecretError::ValueTooLarge)); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn bounded_get_bytes_rejects_an_expired_deadline() { + let _guard = env_guard().lock().await; + let _env = EnvOverride::set( + "EDGEZERO__SECRETS____EDGEZERO_TEST_EXPIRED_SECRET__", + "hello", + ); + let handle = SecretHandle::new(Arc::new(EnvSecretStore::new())); + let expired = Deadline::at_instant(MonotonicInstant::now()); + let clock = MonotonicClock::default(); + + let error = handle + .get_bytes_bounded( + "env", + "__EDGEZERO_TEST_EXPIRED_SECRET__", + &clock, + expired, + 5, + 5, + ) + .await + .expect_err("expired deadline must fail"); + + assert!(matches!(error, SecretError::DeadlineExceeded)); + } + + #[tokio::test(flavor = "current_thread")] + async fn bounded_get_bytes_preserves_handle_name_validation() { + let handle = SecretHandle::new(Arc::new(EnvSecretStore::new())); + let clock = MonotonicClock::default(); + + let error = handle + .get_bytes_bounded( + "", + "key", + &clock, + Deadline::after(Duration::from_secs(1)), + 5, + 5, + ) + .await + .expect_err("empty store name must be rejected"); + + assert!(matches!(error, SecretError::Validation(_))); + } + + #[tokio::test(flavor = "current_thread")] + async fn bounded_secret_read_uses_injected_clock() { + let _guard = env_guard().lock().await; + let _env = EnvOverride::set( + "EDGEZERO__SECRETS____EDGEZERO_TEST_CLOCKED_SECRET__", + "hello", + ); + let handle = SecretHandle::new(Arc::new(EnvSecretStore::new())); + let process_now = MonotonicInstant::now(); + let injected_now = process_now + .checked_sub(Duration::from_mins(1)) + .expect("injected instant"); + let clock = MonotonicClock::new(move || injected_now); + let deadline = Deadline::at_instant( + injected_now + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), + ); + + let read = handle + .get_bytes_bounded( + "env", + "__EDGEZERO_TEST_CLOCKED_SECRET__", + &clock, + deadline, + 5, + 5, + ) + .await + .expect("the application clock is still before its deadline"); + + assert_eq!(read.value, Some(Bytes::from_static(b"hello"))); + } fn read(key: &str) -> Result, SecretError> { block_on(EnvSecretStore::new().get_bytes("env", key)) } + #[tokio::test(flavor = "current_thread")] + async fn bounded_and_unbounded_reads_share_prefixed_secret_mapping() { + let _guard = env_guard().lock().await; + let _bare = EnvOverride::set("EDGEZERO_TEST_BOUNDED_MAPPING", "bare"); + let prefixed_guard = EnvOverride::set( + "EDGEZERO__SECRETS__EDGEZERO_TEST_BOUNDED_MAPPING", + "prefixed", + ); + let store = EnvSecretStore::new(); + let clock = MonotonicClock::default(); + let deadline = clock.deadline_after(Duration::from_secs(1)); + let bounded = store + .get_bytes_bounded( + "env", + "edgezero_test_bounded_mapping", + &clock, + deadline, + 8, + 8, + ) + .await + .unwrap(); + assert_eq!(bounded.value, Some(Bytes::from_static(b"prefixed"))); + assert_eq!(bounded.backend_bytes, 8); + assert_eq!( + bounded.value, + store + .get_bytes("env", "edgezero_test_bounded_mapping") + .await + .unwrap() + ); + + drop(prefixed_guard); + let _missing = EnvOverride::remove("EDGEZERO__SECRETS__EDGEZERO_TEST_BOUNDED_MAPPING"); + let missing = store + .get_bytes_bounded( + "env", + "EDGEZERO_TEST_BOUNDED_MAPPING", + &clock, + deadline, + 8, + 8, + ) + .await + .unwrap(); + assert!(missing.value.is_none()); + } + #[cfg(unix)] #[test] fn get_bytes_preserves_non_utf8_secret_values() { diff --git a/crates/edgezero-adapter-axum/src/service.rs b/crates/edgezero-adapter-axum/src/service.rs index ecaf9b1c1..d460b9d4a 100644 --- a/crates/edgezero-adapter-axum/src/service.rs +++ b/crates/edgezero-adapter-axum/src/service.rs @@ -1,51 +1,105 @@ -use std::convert::Infallible; use std::future::Future; +use std::net::SocketAddr; use std::pin::Pin; +use std::sync::Arc; +#[cfg(test)] use std::task::{Context, Poll}; use axum::body::Body as AxumBody; +use axum::extract::connect_info::ConnectInfo; use axum::http::{Request, Response}; +use edgezero_core::OutboundScopeOwner; +use edgezero_core::app::App; use edgezero_core::config_store::ConfigStoreHandle; -use edgezero_core::http::StatusCode; +use edgezero_core::error::EdgeError; +use edgezero_core::http::Method; +use edgezero_core::ingress::{ + IngressBeginOutcome, IngressDispatchOutcome, IngressFraming, IngressHeadAccounting, + IngressHeadParts, validate_normalized_ingress_parts, +}; use edgezero_core::key_value_store::KvHandle; +use edgezero_core::outbound::HttpClient; +#[cfg(test)] use edgezero_core::router::RouterService; use edgezero_core::secret_store::SecretHandle; use edgezero_core::store_registry::{ BoundSecretStore, ConfigRegistry, ConfigStoreBinding, KvRegistry, SecretRegistry, }; -use tokio::{runtime::Handle, task}; -use tower::Service; +use edgezero_core::time::{MonotonicClock, MonotonicInstant}; +use hyper::body::Incoming; +use hyper::service::Service as HyperService; +#[cfg(test)] +use tower::Service as TowerService; -use crate::request::into_core_request; -use crate::response::into_axum_response; +#[cfg(test)] +use crate::connection::{ConnectionExit, serve_http1}; +#[cfg(test)] +use crate::ingress_config::AxumIngressConfig; +use crate::outbound::{AxumOutboundClient, native_time}; +use crate::request::into_core_request_parts; +use crate::response::{AxumEgressBody, EgressConnection, prepare_egress_response}; -/// Tower service that adapts `EdgeZero` router requests to Axum/Hyper compatible responses. +/// Private Hyper service error used only to close/reset an admission-aborted connection. +#[derive(Debug, thiserror::Error)] +#[error("ingress admission aborted")] +pub(crate) struct AxumIngressAbort; + +/// Shared application state used to construct one private Hyper service per HTTP/1 connection. #[derive(Clone)] -pub struct EdgeZeroAxumService { +pub(crate) struct AxumServiceState { + app: Arc, config_registry: Option, config_store_handle: Option, kv_handle: Option, kv_registry: Option, - router: RouterService, + outbound_client: Option, secret_handle: Option, secret_registry: Option, } -impl EdgeZeroAxumService { +#[expect( + clippy::arbitrary_source_item_ordering, + reason = "constructor and store wiring methods remain grouped ahead of request dispatch" +)] +impl AxumServiceState { + /// Creates a service that preserves all policies configured on `App`. #[must_use] #[inline] - pub fn new(router: RouterService) -> Self { + pub fn from_app(app: App) -> Self { + Self::from_app_with_client_factory(app, |clock| { + AxumOutboundClient::try_with_time(native_time(clock)) + .ok() + .map(HttpClient::with_client) + }) + } + + fn from_app_with_client_factory( + app: App, + initialize: impl FnOnce(MonotonicClock) -> Option, + ) -> Self { + let outbound_client = app + .state::() + .cloned() + .or_else(|| initialize(app.monotonic_clock())); Self { + app: Arc::new(app), config_registry: None, config_store_handle: None, kv_handle: None, kv_registry: None, - router, + outbound_client, secret_handle: None, secret_registry: None, } } + #[must_use] + #[inline] + #[cfg(test)] + pub fn new(router: RouterService) -> Self { + Self::from_app(App::new(router)) + } + /// Attach an id-keyed config-store registry to this service. #[must_use] #[inline] @@ -121,30 +175,28 @@ impl EdgeZeroAxumService { self.secret_registry = Some(registry); self } -} -impl Service> for EdgeZeroAxumService { - type Error = Infallible; - type Future = Pin> + Send>>; - type Response = Response; + pub(crate) fn for_connection( + &self, + remote_addr: SocketAddr, + connection: EgressConnection, + ) -> AxumConnectionService { + AxumConnectionService { + connection, + remote_addr, + state: self.clone(), + } + } - #[inline] - fn call(&mut self, req: Request) -> Self::Future { - let router = self.router.clone(); - // Hard-cutoff: legacy bare `KvHandle` / - // `ConfigStoreHandle` / `SecretHandle` entries are NO - // LONGER inserted into request extensions. The legacy - // `with_*_handle` constructors still take a single - // handle, but the dispatcher synthesises a one-id - // `Registry` under the conventional `"default"` - // id from that handle — and only the registry goes into - // extensions. Handlers must use the registry-aware - // `RequestContext` accessors (`kv_store_default`, - // `config_store_default`, `secret_store_default`) or - // the `Kv` / `Config` / `Secrets` extractors. The - // pre-rewrite `ctx.kv_handle()` / `config_handle()` / - // `secret_handle()` accessors are gone (spec - // hard-cutoff). + fn resolved_store_registries( + &self, + ) -> ( + Option, + Option, + Option, + ) { + // Legacy single-handle constructors synthesize only the conventional + // `default` registry entry; request extensions contain registries only. let config_registry = self.config_registry.clone().or_else(|| { self.config_store_handle.clone().map(|handle| { ConfigRegistry::single_id( @@ -169,68 +221,572 @@ impl Service> for EdgeZeroAxumService { ) }) }); - Box::pin(async move { - let mut core_request = match into_core_request(req).await { - Ok(converted) => converted, - Err(err) => { - let mut err_response = Response::new(AxumBody::from(err.clone())); - *err_response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + (config_registry, kv_registry, secret_registry) + } - return Ok(err_response); - } - }; + async fn dispatch_request( + &self, + request: Request, + connection: &EgressConnection, + remote_addr: Option, + ) -> Result, AxumIngressAbort> { + let callback = OutboundScopeOwner::callback(); + callback + .run(self.dispatch_scoped_request(request, connection, remote_addr)) + .await + } - if let Some(registry) = config_registry { - core_request.extensions_mut().insert(registry); + async fn dispatch_scoped_request( + &self, + mut request: Request, + connection: &EgressConnection, + remote_addr: Option, + ) -> Result, AxumIngressAbort> { + if let Some(peer_addr) = remote_addr { + request.extensions_mut().insert(ConnectInfo(peer_addr)); + } + let request_start = self.app.monotonic_now(); + let app = Arc::clone(&self.app); + let configured_client = self.outbound_client.clone(); + let (config_registry, kv_registry, secret_registry) = self.resolved_store_registries(); + let (parts, native_body) = request.into_parts(); + let request_method = parts.method.clone(); + if let Err(error) = validate_normalized_ingress_parts(&parts, app.ingress_head_limits()) { + return prepare_detached_ingress_error( + &app, + error, + request_method.clone(), + request_start, + connection, + ) + .await; + } + let head_parts = IngressHeadParts::from_parts( + &parts, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ); + let prepared = match app.begin_ingress(head_parts, request_start) { + Ok(IngressBeginOutcome::Admitted(prepared)) => prepared, + Ok(IngressBeginOutcome::Refused(response)) => { + return Ok(prepare_egress_response(response, connection).await); } - if let Some(registry) = kv_registry { - core_request.extensions_mut().insert(registry); + Ok(IngressBeginOutcome::Aborted) => return Err(AxumIngressAbort), + Ok(_) => { + return prepare_detached_ingress_error( + &app, + EdgeError::internal(anyhow::anyhow!("unsupported ingress admission outcome")), + request_method, + request_start, + connection, + ) + .await; } - if let Some(registry) = secret_registry { - core_request.extensions_mut().insert(registry); + Err(error) => { + return prepare_detached_ingress_error( + &app, + error, + request_method.clone(), + request_start, + connection, + ) + .await; } + }; + let read_deadline = prepared.read_deadline(); + let monotonic_clock = prepared.monotonic_clock(); + let Some(outbound_client) = configured_client else { + let egress = app.admitted_error_egress( + prepared, + EdgeError::internal(anyhow::anyhow!( + "failed to initialize outbound HTTP transport" + )), + ); + return Ok(prepare_egress_response(egress, connection).await); + }; + let mut core_request = into_core_request_parts( + parts, + native_body, + Some((read_deadline, monotonic_clock)), + outbound_client, + ); - let core_response = task::block_in_place(move || { - Handle::current().block_on(router.oneshot(core_request)) - }); - let response = match core_response { - Ok(response) => into_axum_response(response), - Err(err) => { - let body = AxumBody::from(format!("internal error: {err}")); - let mut fallback = Response::new(body); - *fallback.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - fallback - } - }; - Ok(response) - }) + if let Some(registry) = config_registry { + core_request.extensions_mut().insert(registry); + } + if let Some(registry) = kv_registry { + core_request.extensions_mut().insert(registry); + } + if let Some(registry) = secret_registry { + core_request.extensions_mut().insert(registry); + } + + let egress = app.dispatch_admitted(prepared, core_request).await; + Ok(prepare_egress_response(egress, connection).await) } +} + +#[derive(Clone)] +pub(crate) struct AxumConnectionService { + connection: EgressConnection, + remote_addr: SocketAddr, + state: AxumServiceState, +} + +impl HyperService> for AxumConnectionService { + type Error = AxumIngressAbort; + type Future = Pin>>>; + type Response = Response; #[inline] - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + fn call(&self, req: Request) -> Self::Future { + let state = self.state.clone(); + let connection = self.connection.clone(); + let remote_addr = self.remote_addr; + let converted_request = req.map(AxumBody::new); + Box::pin(async move { + state + .dispatch_request(converted_request, &connection, Some(remote_addr)) + .await + }) + } +} + +#[cfg(test)] +impl TowerService> for AxumServiceState { + type Error = AxumIngressAbort; + type Future = Pin>>>; + type Response = Response; + + fn call(&mut self, req: Request) -> Self::Future { + let state = self.clone(); + Box::pin(async move { + state + .dispatch_request(req, &EgressConnection::default(), None) + .await + }) + } + + fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } } +async fn prepare_detached_ingress_error( + app: &App, + error: EdgeError, + request_method: Method, + request_start: MonotonicInstant, + connection: &EgressConnection, +) -> Result, AxumIngressAbort> { + match app.detached_ingress_error_egress(error, request_method, request_start) { + IngressDispatchOutcome::Response(envelope) => { + Ok(prepare_egress_response(*envelope, connection).await) + } + IngressDispatchOutcome::Aborted | _ => Err(AxumIngressAbort), + } +} + #[cfg(test)] mod tests { + #![expect( + clippy::arbitrary_source_item_ordering, + reason = "shared test collectors precede fixtures and behavior cases" + )] + use super::*; - use axum::body::to_bytes; + use async_stream::stream; + use bytes::{Bytes, BytesMut}; use edgezero_core::body::Body; - use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; + use edgezero_core::config_store::{ + BoundedStoreRead, ConfigStore, ConfigStoreError, ConfigStoreHandle, + finish_bounded_config_read, + }; use edgezero_core::context::RequestContext; use edgezero_core::error::EdgeError; - use edgezero_core::http::{StatusCode, response_builder}; + use edgezero_core::http::{ + HeaderMap, HeaderValue, Response as CoreResponse, StatusCode, response_builder, + }; + use edgezero_core::ingress::{ + AdmissionDecision, BufferedIngressResponse, IngressGrant, IngressHeadLimits, + }; use edgezero_core::key_value_store::KvStore; - use std::sync::Arc; + use edgezero_core::middleware::{Middleware, Next}; + use edgezero_core::outbound::{ + HttpClient, OutboundBatch, OutboundFuture, OutboundPending, OutboundRequest, + OutboundResponse, + }; + use edgezero_core::response_egress::{ + DEFAULT_RESPONSE_WRITE_BUDGET, DetachedResponseEgressDecision, ResponseEgressCompletion, + ResponseEgressFallbackDisposition, ResponseEgressObserver, ResponseEgressOutcome, + ResponseEgressPolicy, ResponseEgressReport, + }; + use edgezero_core::router::{RouteMetadata, RouteResolution}; + use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; + use futures_util::future::pending; + use futures_util::stream::poll_fn; + use http_body::Body as _; + use std::cell::RefCell; + use std::future::poll_fn as poll_future; + use std::io; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::Poll; + use std::time::Duration; + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + use tokio::net::{TcpListener, TcpStream}; + use tokio::task::{JoinHandle, JoinSet, LocalSet, yield_now}; + use tokio::time::{sleep, timeout}; use tower::ServiceExt as _; + thread_local! { + static RETAINED_SEND: RefCell>> = const { RefCell::new(None) }; + static RETAINED_PROGRESS: RefCell> = const { RefCell::new(None) }; + } + + async fn waiting_outbound_origin() -> (String, Arc, JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("origin"); + let target = format!("http://{}/", listener.local_addr().expect("address")); + let received = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&received); + let server = tokio::spawn(async move { + let mut connections = JoinSet::new(); + for _ in 0_u8..3 { + let (mut socket, _) = listener.accept().await.expect("connection"); + let ready = Arc::clone(&observed); + connections.spawn(async move { + let mut head = Vec::new(); + while !head.ends_with(b"\r\n\r\n") { + assert!(head.len() < 8192, "bounded request head"); + head.push(socket.read_u8().await.expect("request head")); + } + ready.fetch_add(1, Ordering::SeqCst); + assert_eq!( + socket.read(&mut [0_u8]).await.expect("cancelled transport"), + 0 + ); + }); + } + while let Some(result) = connections.join_next().await { + result.expect("origin connection"); + } + }); + (target, received, server) + } + + async fn start_retained_progress(client: &HttpClient, target: &str) -> Result<(), EdgeError> { + let pending = client.begin(OutboundRequest::get(target)?).await?; + let batch = client.start_batch_until( + vec![OutboundRequest::get(target)?, OutboundRequest::get(target)?], + Deadline::after(Duration::from_secs(10)), + ); + RETAINED_PROGRESS.with(|retained| *retained.borrow_mut() = Some((pending, batch))); + Ok(()) + } + + async fn await_outbound_requests(received: &AtomicUsize) { + timeout(Duration::from_secs(2), async { + while received.load(Ordering::SeqCst) != 3 { + yield_now().await; + } + }) + .await + .expect("all begin/batch exchanges enter native I/O"); + } + + async fn assert_retained_progress_cancelled(mut server: JoinHandle<()>) { + let result = timeout(Duration::from_secs(2), &mut server).await; + let retained = RETAINED_PROGRESS.with(|retained| retained.borrow_mut().take()); + assert!(retained.is_some(), "caller retains both progress handles"); + drop(retained); + if result.is_err() { + server.abort(); + let _cancelled = server.await; + } + result + .expect("scope exit closes every native exchange") + .expect("origin task"); + } + + #[tokio::test(flavor = "current_thread")] + async fn callback_handoff_cancels_native_begin_and_batch_while_handles_are_retained() { + LocalSet::new() + .run_until(async { + let (target, received, server) = waiting_outbound_origin().await; + let router = RouterService::builder() + .get("/", move |ctx: RequestContext| { + let destination = target.clone(); + let ready = Arc::clone(&received); + async move { + start_retained_progress( + &ctx.http_client().expect("HTTP client"), + &destination, + ) + .await?; + await_outbound_requests(&ready).await; + Ok::<_, EdgeError>(CoreResponse::new(Body::empty())) + } + }) + .build(); + let response = AxumServiceState::new(router) + .oneshot(Request::new(AxumBody::empty())) + .await + .expect("native handoff"); + assert_eq!(response.status(), StatusCode::OK); + assert_retained_progress_cancelled(server).await; + drop(response); + }) + .await; + } + + #[tokio::test(flavor = "current_thread")] + async fn attached_body_owner_keeps_pre_head_jobs_live_without_changing_framing() { + LocalSet::new() + .run_until(async { + for streamed in [false, true] { + let (target, received, server) = waiting_outbound_origin().await; + let bytes = Bytes::from_static(b"live"); + let payload = bytes.clone(); + let router = RouterService::builder() + .get("/", move |ctx: RequestContext| { + let destination = target.clone(); + let data = payload.clone(); + async move { + let owner = OutboundScopeOwner::response_body(); + owner + .run(start_retained_progress( + &ctx.http_client().expect("HTTP client"), + &destination, + )) + .await?; + let body = if streamed { + Body::from_stream(stream! { + yield Ok(data); + }) + } else { + Body::from(data) + }; + owner.attach(CoreResponse::new(body)) + } + }) + .build(); + let response = AxumServiceState::new(router) + .oneshot(Request::new(AxumBody::empty())) + .await + .expect("native handoff before origin heads"); + assert_eq!(response.status(), StatusCode::OK); + if streamed { + assert!(response.headers().get("content-length").is_none()); + } else { + assert_eq!(response.headers()["content-length"], "4"); + } + await_outbound_requests(&received).await; + assert!( + !server.is_finished(), + "attached jobs survive callback return" + ); + let mut body = response.into_body(); + let first = poll_future(|cx| Pin::new(&mut body).poll_frame(cx)) + .await + .expect("first frame") + .expect("live body") + .into_data() + .expect("data"); + assert_eq!(first, bytes); + assert_eq!(first.as_ptr(), bytes.as_ptr(), "buffer is not copied"); + assert!( + poll_future(|cx| Pin::new(&mut body).poll_frame(cx)) + .await + .is_none() + ); + assert_retained_progress_cancelled(server).await; + drop(body); + } + }) + .await; + } + + #[tokio::test(flavor = "current_thread")] + async fn response_body_scope_keeps_native_work_live_until_eof_or_drop() { + LocalSet::new() + .run_until(async { + for clean_eof in [false, true] { + let (target, received, server) = waiting_outbound_origin().await; + let router = RouterService::builder() + .get("/", move |ctx: RequestContext| { + let destination = target.clone(); + async move { + let client = ctx.http_client().expect("HTTP client"); + let source = stream! { + start_retained_progress(&client, &destination).await.expect("body progress"); + yield Ok(Bytes::from_static(b"live")); + if !clean_eof { + pending::<()>().await; + } + }; + Ok::<_, EdgeError>(CoreResponse::new(Body::from_stream(source))) + } + }) + .build(); + let response = AxumServiceState::new(router) + .oneshot(Request::new(AxumBody::empty())) + .await + .expect("native streaming handoff"); + let mut body = response.into_body(); + let first = poll_future(|cx| Pin::new(&mut body).poll_frame(cx)) + .await.expect("first frame").expect("live body"); + assert_eq!(first.into_data().expect("data"), "live"); + await_outbound_requests(&received).await; + assert!(!server.is_finished(), "body work survives callback handoff"); + if clean_eof { + assert!(poll_future(|cx| Pin::new(&mut body).poll_frame(cx)).await.is_none()); + } + drop(body); + assert_retained_progress_cancelled(server).await; + } + }) + .await; + } + + #[tokio::test(flavor = "current_thread")] + async fn callback_handoff_releases_retained_unpolled_send_without_polling_upload() { + let drops = Arc::new(AtomicUsize::new(0)); + let polls = Arc::new(AtomicUsize::new(0)); + let observed_drops = Arc::clone(&drops); + let observed_polls = Arc::clone(&polls); + let router = RouterService::builder() + .get("/", move |ctx: RequestContext| { + let source = DropSignal(Arc::clone(&observed_drops)); + let counted = Arc::clone(&observed_polls); + async move { + let body = Body::from_stream(poll_fn(move |_| { + let _keep_source_alive = &source; + counted.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + })); + let client = ctx.http_client().expect("request HTTP client"); + let send = client.send( + OutboundRequest::post("http://127.0.0.1:9/") + .expect("request") + .body(body), + ); + RETAINED_SEND.with(|retained| *retained.borrow_mut() = Some(send)); + Ok::<_, EdgeError>(CoreResponse::new(Body::empty())) + } + }) + .build(); + let response = AxumServiceState::new(router) + .oneshot(Request::new(AxumBody::empty())) + .await + .expect("native handoff"); + let released_at_handoff = drops.load(Ordering::SeqCst); + let retained = RETAINED_SEND.with(|slot| slot.borrow_mut().take()); + assert!(retained.is_some(), "caller still retains the unpolled send"); + drop(retained); + drop(response); + assert_eq!(polls.load(Ordering::SeqCst), 0); + assert_eq!(released_at_handoff, 1, "callback closes at native handoff"); + assert_eq!(drops.load(Ordering::SeqCst), 1, "source releases once"); + } + + async fn to_bytes(mut body: AxumEgressBody, limit: usize) -> Result { + let mut buffered = BytesMut::new(); + loop { + let next_frame = poll_future(|context| Pin::new(&mut body).poll_frame(context)).await; + let Some(frame_result) = next_frame else { + return Ok(buffered.freeze()); + }; + let response_frame = frame_result.map_err(anyhow::Error::new)?; + let Ok(bytes) = response_frame.into_data() else { + continue; + }; + if buffered.len().saturating_add(bytes.len()) > limit { + return Err(anyhow::anyhow!("response body exceeded test limit")); + } + buffered.extend_from_slice(&bytes); + } + } + struct FixedConfigStore(String); + #[test] + fn state_client_skips_real_transport_initialization_factory() { + let supplied = HttpClient::with_client(AxumOutboundClient::try_new().expect("test client")) + .with_proxy_marker(false); + let app = App::new(RouterService::builder().with_state(supplied).build()); + let service = AxumServiceState::from_app_with_client_factory(app, |_| { + panic!("supplied client must avoid transport initialization") + }); + assert!( + !service + .outbound_client + .expect("supplied client") + .proxy_marker_enabled() + ); + } + + struct CountingMiddleware(Arc); + + struct DropSignal(Arc); + + #[derive(Clone)] + struct RecordingEgressObserver(Arc>>); + + impl ResponseEgressObserver for RecordingEgressObserver { + fn complete(&self, report: &ResponseEgressReport) { + self.0.lock().expect("reports lock").push(report.clone()); + } + } + + #[async_trait::async_trait(?Send)] + impl Middleware for CountingMiddleware { + async fn handle( + &self, + ctx: RequestContext, + next: Next<'_>, + ) -> Result { + self.0.fetch_add(1, Ordering::SeqCst); + next.run(ctx).await + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + #[async_trait::async_trait(?Send)] impl ConfigStore for FixedConfigStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.clone())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.clone().into())) + } + + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + finish_bounded_config_read( + self.get(key).await, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) } } @@ -245,13 +801,1002 @@ mod tests { Ok::<_, EdgeError>(response) }) .build(); - let mut service = EdgeZeroAxumService::new(router); + let mut service = AxumServiceState::new(router); let request = Request::builder().uri("/").body(AxumBody::empty()).unwrap(); let response = service.ready().await.unwrap().call(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn admission_abort_skips_body_handler_middleware_and_egress() { + let body_polls = Arc::new(AtomicUsize::new(0)); + let observed_body_polls = Arc::clone(&body_polls); + let native_body = AxumBody::from_stream(poll_fn(move |_cx| { + observed_body_polls.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + })); + let handler_calls = Arc::new(AtomicUsize::new(0)); + let observed_handler_calls = Arc::clone(&handler_calls); + let middleware_calls = Arc::new(AtomicUsize::new(0)); + let reports = Arc::new(Mutex::new(Vec::new())); + let router = RouterService::builder() + .post("/upload", move |_ctx: RequestContext| { + let calls = Arc::clone(&observed_handler_calls); + async move { + calls.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("unexpected") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware_calls))) + .build(); + let mut app = App::new(router); + app.set_ingress_admission_policy(|_| AdmissionDecision::Abort); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let request = Request::builder() + .method("POST") + .uri("/upload") + .body(native_body) + .expect("request"); + + let result = AxumServiceState::from_app(app).oneshot(request).await; + + assert!(result.is_err()); + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + assert_eq!(handler_calls.load(Ordering::SeqCst), 0); + assert_eq!(middleware_calls.load(Ordering::SeqCst), 0); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[tokio::test(flavor = "current_thread")] + async fn admission_abort_closes_http1_without_response_bytes() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind listener"); + let address = listener.local_addr().expect("listener address"); + let mut app = App::new(RouterService::builder().build()); + app.set_ingress_admission_policy(|_| AdmissionDecision::Abort); + let state = AxumServiceState::from_app(app); + + let server = async move { + let (stream, remote_addr) = listener.accept().await.expect("accept client"); + let responses = EgressConnection::default(); + serve_http1( + stream, + state.for_connection(remote_addr, responses.clone()), + responses, + AxumIngressConfig::default(), + ) + .await + .expect("intentional admission abort") + }; + let client = async move { + let mut stream = TcpStream::connect(address).await.expect("connect client"); + stream + .write_all( + b"POST /missing HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\n\r\ndata", + ) + .await + .expect("write request"); + let mut response = Vec::new(); + let read = stream.read_to_end(&mut response).await; + (read, response) + }; + + let (exit, (read, response)) = tokio::join!(server, client); + assert_eq!(exit, ConnectionExit::AdmissionAborted); + if let Err(error) = read { + assert!(matches!( + error.kind(), + io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe + )); + } + assert!(response.is_empty()); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn standard_service_installs_the_exact_application_outbound_clock() { + async fn elapsed(ctx: RequestContext) -> Result { + let client = ctx + .http_client() + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("missing HTTP client")))?; + let request = OutboundRequest::get("https://example.com/")?.stream_response(); + let results = client + .send_all_until(vec![request], Deadline::after(Duration::from_secs(1))) + .await + .map_err(|failure| failure.into_parts().0)?; + let edgezero_core::OutboundCollectedSlot::Resolved(slot) = &results.slots()[0] else { + return Err(EdgeError::internal(anyhow::anyhow!("unresolved HTTP slot"))); + }; + Ok(slot.elapsed.as_millis().to_string()) + } + + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(7)) + .expect("completed instant"); + let observations = Arc::new(AtomicUsize::new(0)); + let clock_observations = Arc::clone(&observations); + let router = RouterService::builder().get("/clock", elapsed).build(); + let mut app = App::new(router); + app.set_monotonic_clock(MonotonicClock::new(move || { + if clock_observations.fetch_add(1, Ordering::SeqCst) < 2 { + start + } else { + completed + } + })); + let request = Request::builder() + .uri("/clock") + .body(AxumBody::empty()) + .expect("request"); + + let response = AxumServiceState::from_app(app) + .oneshot(request) + .await + .expect("response"); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("response body"); + + assert_eq!(body, "7"); + assert!(observations.load(Ordering::SeqCst) >= 3); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn configured_admission_refuses_before_native_body_poll() { + let body_polls = Arc::new(AtomicUsize::new(0)); + let observed_polls = Arc::clone(&body_polls); + let native_body = AxumBody::from_stream(poll_fn(move |_cx| { + observed_polls.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + })); + let admission_calls = Arc::new(AtomicUsize::new(0)); + let observed_calls = Arc::clone(&admission_calls); + let observed_during_admission = Arc::clone(&body_polls); + + let router = RouterService::builder() + .post("/upload", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("handler must not run") + }) + .build(); + let mut app = App::new(router); + app.set_ingress_admission_policy(move |head| { + observed_calls.fetch_add(1, Ordering::SeqCst); + assert_eq!(observed_during_admission.load(Ordering::SeqCst), 0); + assert!(matches!( + head.route_resolution(), + RouteResolution::Matched(_) + )); + AdmissionDecision::Refuse { + completion: ResponseEgressCompletion::empty(), + response: response_builder() + .status(StatusCode::TOO_MANY_REQUESTS) + .body(Body::empty()) + .expect("refusal"), + } + }); + let mut service = AxumServiceState::from_app(app); + let request = Request::builder() + .method("POST") + .uri("/upload") + .body(native_body) + .expect("request"); + + let response = service.ready().await.unwrap().call(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); + assert_eq!(admission_calls.load(Ordering::SeqCst), 1); + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn normalized_ingress_error_returns_typed_response_without_application_observation() { + let reports = Arc::new(Mutex::new(Vec::new())); + let completion_reports = Arc::new(Mutex::new(Vec::new())); + let clock_samples = Arc::new(AtomicUsize::new(0)); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let observed_clock_samples = Arc::clone(&clock_samples); + let now = MonotonicInstant::now(); + let mut app = App::new(RouterService::builder().build()); + app.set_monotonic_clock(MonotonicClock::new(move || { + observed_clock_samples.fetch_add(1, Ordering::SeqCst); + now + })); + app.set_ingress_head_limits( + IngressHeadLimits::default() + .with_max_request_target_bytes(4) + .expect("target limit"), + ); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let observed_factory_calls = Arc::clone(&factory_calls); + let observed_completion_calls = Arc::clone(&completion_calls); + let observed_completion_reports = Arc::clone(&completion_reports); + app.set_detached_response_egress_decision_factory(move |head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + let terminal_calls = Arc::clone(&observed_completion_calls); + let terminal_reports = Arc::clone(&observed_completion_reports); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |report| { + terminal_calls.fetch_add(1, Ordering::SeqCst); + terminal_reports + .lock() + .expect("completion reports lock") + .push(report.clone()); + }), + deadline: head.write_deadline_after(Duration::ZERO), + } + }); + let request = Request::builder() + .uri("/too-long") + .body(AxumBody::empty()) + .expect("request"); + + let response = AxumServiceState::from_app(app) + .oneshot(request) + .await + .expect("typed response"); + assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("response body"); + assert_eq!(body, b"response write deadline exceeded".as_slice()); + assert!(reports.lock().expect("reports lock").is_empty()); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + let completed = completion_reports.lock().expect("completion reports lock"); + assert_eq!(completed.len(), 1); + assert_eq!( + completed[0].outcome, + ResponseEgressOutcome::DeadlineExceeded + ); + assert_eq!( + completed[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Completed) + ); + assert!(clock_samples.load(Ordering::SeqCst) >= 2); + } + + #[tokio::test(flavor = "current_thread")] + async fn normalized_ingress_error_decision_abort_closes_without_response() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind listener"); + let address = listener.local_addr().expect("listener address"); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + let mut app = App::new(RouterService::builder().build()); + app.set_ingress_head_limits( + IngressHeadLimits::default() + .with_max_request_target_bytes(4) + .expect("target limit"), + ); + app.set_detached_response_egress_decision_factory(move |_head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + DetachedResponseEgressDecision::Abort + }); + let state = AxumServiceState::from_app(app); + + let server = async move { + let (stream, remote_addr) = listener.accept().await.expect("accept client"); + let responses = EgressConnection::default(); + serve_http1( + stream, + state.for_connection(remote_addr, responses.clone()), + responses, + AxumIngressConfig::default(), + ) + .await + .expect("intentional detached error abort") + }; + let client = async move { + let mut stream = TcpStream::connect(address).await.expect("connect client"); + stream + .write_all(b"GET /too-long HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .expect("write request"); + let mut response = Vec::new(); + let read = stream.read_to_end(&mut response).await; + (read, response) + }; + + let (exit, (read, response)) = tokio::join!(server, client); + assert_eq!(exit, ConnectionExit::AdmissionAborted); + if let Err(error) = read { + assert!(matches!( + error.kind(), + io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe + )); + } + assert!(response.is_empty()); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn admission_policy_error_decision_send_uses_detached_completion() { + let body_polls = Arc::new(AtomicUsize::new(0)); + let observed_body_polls = Arc::clone(&body_polls); + let native_body = AxumBody::from_stream(poll_fn(move |_cx| { + observed_body_polls.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + })); + let handler_calls = Arc::new(AtomicUsize::new(0)); + let observed_handler_calls = Arc::clone(&handler_calls); + let middleware_calls = Arc::new(AtomicUsize::new(0)); + let router = RouterService::builder() + .get("/matched", move |_ctx: RequestContext| { + let request_handler_calls = Arc::clone(&observed_handler_calls); + async move { + request_handler_calls.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("handler must not run") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware_calls))) + .build(); + let decision_completion_calls = Arc::new(AtomicUsize::new(0)); + let decision_resource_drops = Arc::new(AtomicUsize::new(0)); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let mut app = App::new(router); + let observed_decision_completion_calls = Arc::clone(&decision_completion_calls); + let observed_decision_resource_drops = Arc::clone(&decision_resource_drops); + let observed_grant_drops = Arc::clone(&grant_drops); + app.set_ingress_admission_policy(move |head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::Matched(_) + )); + let decision_resource = DropSignal(Arc::clone(&observed_decision_resource_drops)); + let terminal_calls = Arc::clone(&observed_decision_completion_calls); + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::new(move |_report| { + let _resource = decision_resource; + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes: 1, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text( + StatusCode::PAYLOAD_TOO_LARGE, + "too large", + ), + on_timeout: BufferedIngressResponse::text(StatusCode::REQUEST_TIMEOUT, "timeout"), + } + }); + let reports = Arc::new(Mutex::new(Vec::new())); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + let observed_completion_calls = Arc::clone(&completion_calls); + app.set_detached_response_egress_decision_factory(move |head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + let terminal_calls = Arc::clone(&observed_completion_calls); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |_report| { + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), + } + }); + let request = Request::builder() + .uri("/matched") + .body(native_body) + .expect("request"); + + let response = AxumServiceState::from_app(app) + .oneshot(request) + .await + .expect("typed policy error response"); + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + let _body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("response body"); + + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + assert_eq!(handler_calls.load(Ordering::SeqCst), 0); + assert_eq!(middleware_calls.load(Ordering::SeqCst), 0); + assert_eq!(decision_completion_calls.load(Ordering::SeqCst), 0); + assert_eq!(decision_resource_drops.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn admission_policy_error_decision_abort_skips_response_and_application_work() { + let body_polls = Arc::new(AtomicUsize::new(0)); + let observed_body_polls = Arc::clone(&body_polls); + let native_body = AxumBody::from_stream(poll_fn(move |_cx| { + observed_body_polls.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + })); + let handler_calls = Arc::new(AtomicUsize::new(0)); + let observed_handler_calls = Arc::clone(&handler_calls); + let middleware_calls = Arc::new(AtomicUsize::new(0)); + let router = RouterService::builder() + .get("/matched", move |_ctx: RequestContext| { + let calls = Arc::clone(&observed_handler_calls); + async move { + calls.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("handler must not run") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware_calls))) + .build(); + let decision_resource_drops = Arc::new(AtomicUsize::new(0)); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_decision_resource_drops = Arc::clone(&decision_resource_drops); + let observed_grant_drops = Arc::clone(&grant_drops); + let mut app = App::new(router); + app.set_ingress_admission_policy(move |head| AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::new({ + let resource = DropSignal(Arc::clone(&observed_decision_resource_drops)); + move |_report| drop(resource) + }), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes: 1, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text(StatusCode::PAYLOAD_TOO_LARGE, "too large"), + on_timeout: BufferedIngressResponse::text(StatusCode::REQUEST_TIMEOUT, "timeout"), + }); + let reports = Arc::new(Mutex::new(Vec::new())); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + app.set_detached_response_egress_decision_factory(move |_head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + DetachedResponseEgressDecision::Abort + }); + let request = Request::builder() + .uri("/matched") + .body(native_body) + .expect("request"); + + let outcome = AxumServiceState::from_app(app).oneshot(request).await; + + assert!(outcome.is_err()); + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + assert_eq!(handler_calls.load(Ordering::SeqCst), 0); + assert_eq!(middleware_calls.load(Ordering::SeqCst), 0); + assert_eq!(decision_resource_drops.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn post_admission_failure_uses_owned_error_egress() { + let reports = Arc::new(Mutex::new(Vec::new())); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_grant_drops = Arc::clone(&grant_drops); + let router = RouterService::builder() + .get("/owned", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("handler must not run") + }) + .build(); + let mut app = App::new(router); + app.set_ingress_admission_policy(move |head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + }); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let mut service = AxumServiceState::from_app(app); + service.outbound_client = None; + let request = Request::builder() + .uri("/owned") + .body(AxumBody::empty()) + .expect("request"); + + let response = service + .oneshot(request) + .await + .expect("owned error response"); + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + let _body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("response body"); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + let observed = reports.lock().expect("reports lock"); + assert_eq!(observed.len(), 1); + assert_eq!(observed[0].outcome, ResponseEgressOutcome::HostHandoff); + assert_eq!( + observed[0].route.as_ref().map(RouteMetadata::pattern), + Some("/owned") + ); + } + + fn guarded_fallback_app() -> (App, Arc, Arc) { + let handler_call_count = Arc::new(AtomicUsize::new(0)); + let observed_handler_calls = Arc::clone(&handler_call_count); + let middleware_calls = Arc::new(AtomicUsize::new(0)); + let router = RouterService::builder() + .get("/known", move |_ctx: RequestContext| { + let request_handler_calls = Arc::clone(&observed_handler_calls); + async move { + request_handler_calls.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("handler must not run") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware_calls))) + .build(); + (App::new(router), handler_call_count, middleware_calls) + } + + fn fallback_body_app( + max_body_bytes: usize, + read_budget: Duration, + grant_drop_count: &Arc, + ) -> (App, Arc, Arc) { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let observed_grant_drops = Arc::clone(grant_drop_count); + app.set_ingress_admission_policy(move |head| match head.route_resolution() { + RouteResolution::Matched(_) => AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(read_budget), + }, + RouteResolution::MethodNotAllowed { .. } | RouteResolution::NotFound | _ => { + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes, + read_deadline: head.read_deadline_after(read_budget), + on_exceeded: buffered_terminal_response( + StatusCode::PAYLOAD_TOO_LARGE, + "exceeded", + b"configured overflow\0response", + ), + on_timeout: buffered_terminal_response( + StatusCode::GATEWAY_TIMEOUT, + "timeout", + b"configured timeout\0response", + ), + } + } + }); + (app, handler_calls, middleware_calls) + } + + fn buffered_terminal_response( + status: StatusCode, + marker: &'static str, + body: &'static [u8], + ) -> BufferedIngressResponse { + BufferedIngressResponse::new( + status, + terminal_response_headers(marker), + Bytes::from_static(body), + ) + } + + fn terminal_response_headers(marker: &'static str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("x-fallback-terminal", HeaderValue::from_static(marker)); + headers + } + + fn tracked_lengthless_request( + path: &str, + body_chunks: Vec, + grant_drops: &Arc, + source_drops: &Arc, + body_polls: &Arc, + ) -> Request { + let observed_grant_drops = Arc::clone(grant_drops); + let source_drop = DropSignal(Arc::clone(source_drops)); + let observed_body_polls = Arc::clone(body_polls); + let mut pending_chunks = body_chunks.into_iter(); + let stream = poll_fn(move |_cx| { + let _keep_source_alive = &source_drop; + observed_body_polls.fetch_add(1, Ordering::SeqCst); + assert_eq!(observed_grant_drops.load(Ordering::SeqCst), 0); + Poll::Ready(pending_chunks.next().map(Ok::)) + }); + let request = Request::builder() + .method("POST") + .uri(path) + .body(AxumBody::from_stream(stream)) + .expect("request"); + assert!(request.headers().get("content-length").is_none()); + request + } + + fn assert_no_fallback_dispatch(handler_calls: &AtomicUsize, middleware_calls: &AtomicUsize) { + assert_eq!(handler_calls.load(Ordering::SeqCst), 0); + assert_eq!(middleware_calls.load(Ordering::SeqCst), 0); + } + + async fn assert_terminal_response( + response: Response, + status: StatusCode, + marker: &'static str, + body: &[u8], + ) { + assert_eq!(response.status(), status); + let mut expected_headers = terminal_response_headers(marker); + expected_headers.insert( + "content-length", + body.len().to_string().parse().expect("body length"), + ); + assert_eq!(response.headers(), &expected_headers); + assert_eq!( + to_bytes(response.into_body(), usize::MAX) + .await + .expect("response body"), + body + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn bounded_fallback_preserves_404_and_405_at_exact_cap() { + for (path, expected) in [ + ("/missing", StatusCode::NOT_FOUND), + ("/known", StatusCode::METHOD_NOT_ALLOWED), + ] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + fallback_body_app(4, Duration::from_secs(1), &grant_drops); + let request = tracked_lengthless_request( + path, + vec![Bytes::from_static(b"ab"), Bytes::from_static(b"cd")], + &grant_drops, + &source_drops, + &body_polls, + ); + let response = AxumServiceState::from_app(app) + .ready() + .await + .expect("ready") + .call(request) + .await + .expect("response"); + assert_eq!(response.status(), expected); + if expected == StatusCode::METHOD_NOT_ALLOWED { + assert_eq!( + response.headers().get("allow").expect("Allow header"), + "GET" + ); + } + assert_eq!(body_polls.load(Ordering::SeqCst), 3); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_fallback_dispatch(&handler_calls, &middleware_calls); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn bounded_fallback_overflow_precedes_404_and_405() { + for path in ["/missing", "/known"] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + fallback_body_app(4, Duration::from_secs(1), &grant_drops); + let request = tracked_lengthless_request( + path, + vec![Bytes::from_static(b"abcd"), Bytes::from_static(b"e")], + &grant_drops, + &source_drops, + &body_polls, + ); + let response = AxumServiceState::from_app(app) + .ready() + .await + .expect("ready") + .call(request) + .await + .expect("response"); + assert_terminal_response( + response, + StatusCode::PAYLOAD_TOO_LARGE, + "exceeded", + b"configured overflow\0response", + ) + .await; + assert_eq!(body_polls.load(Ordering::SeqCst), 2); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_fallback_dispatch(&handler_calls, &middleware_calls); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn bounded_fallback_deadline_interrupts_pending_native_body() { + let body_polls = Arc::new(AtomicUsize::new(0)); + let observed_polls = Arc::clone(&body_polls); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_grant_drops = Arc::clone(&grant_drops); + let source_drops = Arc::new(AtomicUsize::new(0)); + let source_drop = DropSignal(Arc::clone(&source_drops)); + let native_body = AxumBody::from_stream(poll_fn(move |_cx| { + let _keep_source_alive = &source_drop; + assert_eq!(observed_grant_drops.load(Ordering::SeqCst), 0); + observed_polls.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + })); + let (app, handler_calls, middleware_calls) = + fallback_body_app(4_096, Duration::from_millis(500), &grant_drops); + let mut service = AxumServiceState::from_app(app); + let request = Request::builder() + .method("POST") + .uri("/missing") + .body(native_body) + .expect("request"); + + service.ready().await.expect("ready"); + let response = timeout(Duration::from_secs(5), service.call(request)) + .await + .expect("fallback deadline must preempt the pending native body") + .expect("response"); + + assert_terminal_response( + response, + StatusCode::GATEWAY_TIMEOUT, + "timeout", + b"configured timeout\0response", + ) + .await; + assert!(body_polls.load(Ordering::SeqCst) > 0); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_no_fallback_dispatch(&handler_calls, &middleware_calls); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + #[expect( + clippy::integer_division_remainder_used, + reason = "tokio::select! expands to internal randomized branch selection arithmetic" + )] + async fn cancelled_service_releases_pending_fallback_drain_and_grant() { + let body_polls = Arc::new(AtomicUsize::new(0)); + let observed_polls = Arc::clone(&body_polls); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_grant_drops = Arc::clone(&grant_drops); + let source_drops = Arc::new(AtomicUsize::new(0)); + let source_drop = DropSignal(Arc::clone(&source_drops)); + let native_body = AxumBody::from_stream(poll_fn(move |_context| { + let _keep_source_alive = &source_drop; + assert_eq!(observed_grant_drops.load(Ordering::SeqCst), 0); + observed_polls.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + })); + let (app, handler_calls, middleware_calls) = + fallback_body_app(4_096, Duration::from_secs(5), &grant_drops); + let mut service = AxumServiceState::from_app(app); + let request = Request::builder() + .method("POST") + .uri("/missing") + .body(native_body) + .expect("request"); + + service.ready().await.expect("ready"); + let mut response = Box::pin(service.call(request)); + tokio::select! { + _result = &mut response => panic!("pending drain completed unexpectedly"), + () = sleep(Duration::from_millis(25)) => {} + } + assert!(body_polls.load(Ordering::SeqCst) > 0); + assert_eq!(grant_drops.load(Ordering::SeqCst), 0); + assert_eq!(source_drops.load(Ordering::SeqCst), 0); + drop(response); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_fallback_dispatch(&handler_calls, &middleware_calls); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn fallback_refusal_returns_503_without_polling_native_body() { + for path in ["/missing", "/known"] { + let body_polls = Arc::new(AtomicUsize::new(0)); + let observed_polls = Arc::clone(&body_polls); + let source_drops = Arc::new(AtomicUsize::new(0)); + let source_drop = DropSignal(Arc::clone(&source_drops)); + let native_body = AxumBody::from_stream(poll_fn(move |_cx| { + let _keep_source_alive = &source_drop; + observed_polls.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + })); + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + app.set_ingress_admission_policy(|head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::MethodNotAllowed { .. } | RouteResolution::NotFound + )); + AdmissionDecision::Refuse { + completion: ResponseEgressCompletion::empty(), + response: response_builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .body(Body::from("fallback unavailable\n")) + .expect("refusal response"), + } + }); + let request = Request::builder() + .method("POST") + .uri(path) + .body(native_body) + .expect("request"); + + let response = AxumServiceState::from_app(app) + .ready() + .await + .expect("ready") + .call(request) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!( + to_bytes(response.into_body(), usize::MAX) + .await + .expect("response body"), + b"fallback unavailable\n".as_slice() + ); + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_fallback_dispatch(&handler_calls, &middleware_calls); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn consumed_response_reports_one_host_handoff_with_accepted_bytes() { + let router = RouterService::builder() + .get("/observed", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("ok") + }) + .build(); + let reports = Arc::new(Mutex::new(Vec::new())); + let mut app = App::new(router); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let mut service = AxumServiceState::from_app(app); + let request = Request::builder() + .method("GET") + .uri("/observed") + .body(AxumBody::empty()) + .expect("request"); + + let response = service.ready().await.unwrap().call(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + to_bytes(response.into_body(), usize::MAX).await.unwrap(), + b"ok".as_slice() + ); + + let observed = reports.lock().expect("reports lock"); + assert_eq!(observed.len(), 1); + assert_eq!(observed[0].outcome, ResponseEgressOutcome::HostHandoff); + assert_eq!(observed[0].bytes_written, 2); + assert_eq!( + observed[0].route.as_ref().map(RouteMetadata::pattern), + Some("/observed") + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn injected_clock_controls_response_write_deadline() { + let router = RouterService::builder() + .get("/deadline", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("too late") + }) + .build(); + let reports = Arc::new(Mutex::new(Vec::new())); + let start = MonotonicInstant::now(); + let observed_now = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&observed_now); + let policy_now = Arc::clone(&observed_now); + let mut app = App::new(router); + app.set_monotonic_clock(MonotonicClock::new(move || { + *clock_now.lock().expect("clock lock") + })); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + app.set_response_egress_policy(move |_head, started_at| { + let deadline = started_at + .checked_add(Duration::from_secs(1)) + .expect("write deadline"); + *policy_now.lock().expect("clock lock") = deadline; + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(deadline), + } + }); + let request = Request::builder() + .method("GET") + .uri("/deadline") + .body(AxumBody::empty()) + .expect("request"); + + let response = AxumServiceState::from_app(app) + .ready() + .await + .expect("ready") + .call(request) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("fallback body"); + assert_eq!(body, b"response write deadline exceeded".as_slice()); + let observed = reports.lock().expect("reports lock"); + assert_eq!(observed.len(), 1); + assert_eq!(observed[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert_eq!(observed[0].elapsed, Duration::from_secs(1)); + assert_eq!( + observed[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Completed) + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn admitted_deadline_reaches_body_cell_as_request_timeout() { + let router = RouterService::builder() + .post("/upload", |ctx: RequestContext| async move { + let _bytes = ctx.body_bytes(1024).await?; + Ok::<_, EdgeError>("unexpected success") + }) + .build(); + let mut app = App::new(router); + let request_start = MonotonicInstant::now(); + app.set_monotonic_clock(MonotonicClock::new(move || request_start)); + app.set_ingress_admission_policy(move |head| { + assert_eq!(head.request_start(), request_start); + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: Deadline::at_instant(head.request_start()), + } + }); + let mut service = AxumServiceState::from_app(app); + let request = Request::builder() + .method("POST") + .uri("/upload") + .body(AxumBody::from("data")) + .expect("request"); + + let response = service.ready().await.unwrap().call(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT); + } + + #[tokio::test] + async fn known_empty_first_body_read_retains_sticky_admission_timeout() { + use edgezero_core::context::BodyKind; + + #[edgezero_core::action] + async fn read_empty(ctx: RequestContext) -> Result<&'static str, EdgeError> { + assert_eq!(ctx.body_kind(), BodyKind::Initial); + let error = ctx + .body_bytes(0) + .await + .expect_err("initial deadline still applies"); + assert_eq!(error.status(), StatusCode::REQUEST_TIMEOUT); + assert_eq!(ctx.body_kind(), BodyKind::Poisoned); + assert_eq!( + ctx.take_body().expect_err("sticky timeout").status(), + error.status() + ); + Err(error) + } + + let mut app = App::new(RouterService::builder().get("/", read_empty).build()); + app.set_ingress_admission_policy(|head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: Deadline::at_instant(head.request_start()), + }); + let mut service = AxumServiceState::from_app(app); + let response = service + .ready() + .await + .expect("service ready") + .call(Request::new(AxumBody::empty())) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn with_config_store_handle_injects_into_request() { // Hard-cutoff: legacy `ctx.config_handle()` is @@ -272,12 +1817,12 @@ mod tests { .unwrap_or_default(); let response = response_builder() .status(StatusCode::OK) - .body(Body::from(val)) + .body(Body::from(val.as_str())) .expect("response"); Ok::<_, EdgeError>(response) }) .build(); - let mut service = EdgeZeroAxumService::new(router).with_config_store_handle(handle); + let mut service = AxumServiceState::new(router).with_config_store_handle(handle); let request = Request::builder() .uri("/check") @@ -313,7 +1858,7 @@ mod tests { Ok::<_, EdgeError>(response) }) .build(); - let mut service = EdgeZeroAxumService::new(router).with_kv_handle(handle); + let mut service = AxumServiceState::new(router).with_kv_handle(handle); let request = Request::builder() .uri("/check") @@ -386,7 +1931,7 @@ mod tests { // Wire BOTH: registry first, then a bare handle. The bare // handle would synthesise a "default" id under the legacy // path; the dispatcher's `or_else` precedence must skip it. - let mut service = EdgeZeroAxumService::new(router) + let mut service = AxumServiceState::new(router) .with_kv_registry(registry) .with_kv_handle(bare_handle); @@ -439,7 +1984,7 @@ mod tests { Ok::<_, EdgeError>(response) }) .build(); - let mut service = EdgeZeroAxumService::new(router).with_kv_handle(handle); + let mut service = AxumServiceState::new(router).with_kv_handle(handle); let request = Request::builder() .uri("/probe") @@ -471,7 +2016,7 @@ mod tests { Ok::<_, EdgeError>(response) }) .build(); - let mut service = EdgeZeroAxumService::new(router); + let mut service = AxumServiceState::new(router); let request = Request::builder() .uri("/no-config") @@ -520,7 +2065,7 @@ mod tests { Ok::<_, EdgeError>(response) }) .build(); - let mut service = EdgeZeroAxumService::new(router).with_secret_handle(handle); + let mut service = AxumServiceState::new(router).with_secret_handle(handle); let request = Request::builder() .uri("/check") @@ -546,7 +2091,7 @@ mod tests { Ok::<_, EdgeError>(response) }) .build(); - let mut service = EdgeZeroAxumService::new(router); + let mut service = AxumServiceState::new(router); let request = Request::builder() .uri("/no-kv") @@ -623,7 +2168,7 @@ mod tests { Ok::<_, EdgeError>(response) }) .build(); - let service = EdgeZeroAxumService::new(router).with_kv_registry(registry); + let service = AxumServiceState::new(router).with_kv_registry(registry); assert_eq!( body_at(&service, "/named/sessions").await, @@ -665,7 +2210,7 @@ mod tests { Ok::<_, EdgeError>(response) }) .build(); - let service = EdgeZeroAxumService::new(router).with_kv_registry(registry); + let service = AxumServiceState::new(router).with_kv_registry(registry); assert_eq!(body_at(&service, "/lookup/only").await, "present=true"); assert_eq!(body_at(&service, "/lookup/missing").await, "present=false"); @@ -674,7 +2219,7 @@ mod tests { /// Send a GET request through `service` and return the response body as a UTF-8 string. /// Lifted out of the registry-aware tests so each can stay flat (clippy /// `items_after_statements` rejects nested `async fn` definitions). - async fn body_at(service: &EdgeZeroAxumService, path: &str) -> String { + async fn body_at(service: &AxumServiceState, path: &str) -> String { let request = Request::builder() .uri(path) .body(AxumBody::empty()) diff --git a/crates/edgezero-adapter-axum/src/templates/axum.toml.hbs b/crates/edgezero-adapter-axum/src/templates/axum.toml.hbs index 30fc6e192..322d0aee2 100644 --- a/crates/edgezero-adapter-axum/src/templates/axum.toml.hbs +++ b/crates/edgezero-adapter-axum/src/templates/axum.toml.hbs @@ -3,3 +3,8 @@ crate = "{{proj_axum}}" crate_dir = "." host = "127.0.0.1" port = 8787 + +# Config snapshots load before listener bind; restart after config push. +# Set EDGEZERO__ADAPTER__CONFIG_STORE__* for allocation budgets. +# Defaults: file 16 MiB, 1024 entries, key 1024 bytes, value 8 MiB, +# all-store residency 32 MiB, startup residency plus staging 112 MiB. diff --git a/crates/edgezero-adapter-axum/src/test_utils.rs b/crates/edgezero-adapter-axum/src/test_utils.rs index e32a190aa..1e79b59ad 100644 --- a/crates/edgezero-adapter-axum/src/test_utils.rs +++ b/crates/edgezero-adapter-axum/src/test_utils.rs @@ -1,5 +1,87 @@ +use anyhow::Context as _; +use std::net::SocketAddr; use std::sync::OnceLock; +use tokio::net::TcpListener; use tokio::sync::Mutex; +use tokio::sync::oneshot::{Sender, channel}; +use tokio::task::JoinHandle; + +use crate::dev_server::AxumDevServer; + +/// An assembled application running through the production bounded HTTP/1 path. +/// +/// Startup binds the configured address; use loopback port zero to obtain a free port. +/// This installs its own local executor, including under a plain `#[tokio::test]`. +/// It does not initialize logging, run startup preflight or create stores from hook metadata: +/// attach stores to the supplied server explicitly. Foreign provider contexts are absent. +/// Drop signals cancellation without blocking; await [`Self::shutdown`] to observe cleanup. +#[must_use] +pub struct AxumTestServer { + address: SocketAddr, + shutdown: Option>, + task: Option>>, +} + +impl AxumTestServer { + /// Returns the actual bound address, including the assigned ephemeral port. + #[must_use] + #[inline] + pub const fn address(&self) -> SocketAddr { + self.address + } + + /// Cancels active connections and awaits the serving result and owned resource cleanup. + /// + /// # Errors + /// Returns the original serving failure or a serving-task join error. + #[inline] + pub async fn shutdown(mut self) -> anyhow::Result<()> { + self.signal_shutdown(); + match self.task.take() { + Some(task) => task.await.context("axum test server task failed")?, + None => Ok(()), + } + } + + fn signal_shutdown(&mut self) { + if let Some(sender) = self.shutdown.take() { + let _sent = sender.send(()); + } + } + + /// Binds and starts the supplied server without replacing its application or policies. + /// + /// # Errors + /// Returns listener binding or address inspection failures. Later serving failures are + /// returned by [`Self::shutdown`]. + #[inline] + pub async fn start(server: AxumDevServer) -> anyhow::Result { + let listener: TcpListener = server.bind_listener().await?; + let address = listener + .local_addr() + .context("failed to inspect axum test listener")?; + let (sender, receiver) = channel(); + let task = tokio::spawn(server.serve(listener, async move { + let _closed = receiver.await; + })); + Ok(Self { + address, + shutdown: Some(sender), + task: Some(task), + }) + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "Drop::pin_drop is unstable and conflicts with drop" +)] +impl Drop for AxumTestServer { + #[inline] + fn drop(&mut self) { + self.signal_shutdown(); + } +} /// Returns a process-wide mutex used to serialize tests that mutate environment variables. /// @@ -12,3 +94,1131 @@ pub fn env_guard() -> &'static Mutex<()> { static GUARD: OnceLock> = OnceLock::new(); GUARD.get_or_init(|| Mutex::new(())) } + +#[cfg(test)] +mod tests { + use std::io::Error as IoError; + use std::iter::repeat_n; + use std::mem::take; + use std::net::SocketAddr; + use std::rc::Rc; + use std::sync::Arc; + use std::sync::Mutex as StdMutex; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::Poll; + use std::time::Duration; + + use edgezero_core::EdgeError; + use edgezero_core::app::{App, Hooks}; + use edgezero_core::body::Body; + use edgezero_core::config_store::ConfigExtractionLimits; + use edgezero_core::context::RequestContext; + use edgezero_core::extractor::{Path, State}; + use edgezero_core::http::{HeaderMap, Method, Response, StatusCode}; + use edgezero_core::ingress::{AdmissionDecision, IngressGrant, IngressHeadLimits}; + use edgezero_core::middleware::{Middleware, Next}; + use edgezero_core::outbound::{ + HttpClient, OutboundBatch, OutboundBatchDriverEvent, OutboundDispatchRequest, + OutboundExchangeOwner, OutboundFuture, OutboundHead, OutboundHeaderFidelity, + OutboundHeaderInput, OutboundHttpClient, OutboundPending, OutboundPreparedBatch, + OutboundRequest, OutboundResponse, OutboundSendMode, OutboundTime, + }; + use edgezero_core::response::IntoResponse as _; + use edgezero_core::response_egress::{ + DetachedResponseEgressDecision, ResponseEgressCompletion, ResponseEgressObserver, + ResponseEgressOutcome, ResponseEgressPolicy, ResponseEgressReport, + }; + use edgezero_core::router::RouterService; + use edgezero_core::{ + Deadline, IndependentOutboundProgress, MonotonicClock, MonotonicInstant, + OutboundProgressRuntime, + }; + use futures_util::future::ready; + use futures_util::stream::{iter, poll_fn, unfold}; + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + use tokio::net::TcpListener; + use tokio::net::TcpStream; + use tokio::sync::mpsc; + use tokio::sync::oneshot::channel; + use tokio::task::{spawn_local, yield_now}; + use tokio::time::{sleep, timeout}; + + use super::AxumTestServer; + use crate::dev_server::{AxumDevServer, AxumDevServerConfig}; + use crate::ingress_config::AxumIngressConfig; + use crate::outbound::native_time; + + struct ParityState { + configured: AtomicUsize, + limits: ConfigExtractionLimits, + start: MonotonicInstant, + } + + #[derive(serde::Deserialize)] + struct ParityPath { + id: String, + } + + struct ParityApp; + + #[expect( + clippy::missing_trait_methods, + reason = "test constructs its router from explicit settings" + )] + impl Hooks for ParityApp { + fn configure(app: &mut App) -> Result<(), EdgeError> { + let shared = Arc::clone(app.state::>().expect("registered state")); + shared.configured.fetch_add(1, Ordering::SeqCst); + app.set_name("configured parity app"); + app.set_config_extraction_limits(shared.limits)?; + let start = shared.start; + app.set_monotonic_clock(MonotonicClock::new(move || start)); + Ok(()) + } + + fn routes() -> Result { + Err(EdgeError::service_unavailable( + "manual assembly must retain the supplied router", + )) + } + } + + #[edgezero_core::action] + async fn parity_handler( + State(shared): State>, + Path(path): Path, + ctx: RequestContext, + ) -> Result { + assert_eq!(ctx.method(), Method::PUT); + assert_eq!(path.id, "42"); + assert_eq!(ctx.request_start(), shared.start); + assert_eq!(ctx.monotonic_clock().now(), shared.start); + assert_eq!(ctx.config_extraction_limits(), shared.limits); + let named = ctx.config_store("settings").expect("named config"); + let default = ctx.config_store_default().expect("default config"); + let deadline = + Deadline::at_instant(shared.start.checked_add(shared.limits.timeout).unwrap()); + for handle in [named, default] { + let read = handle + .get_bounded( + "marker", + &ctx.monotonic_clock(), + deadline, + shared.limits.max_backend_bytes, + shared.limits.max_blob_bytes, + ) + .await + .unwrap(); + assert_eq!(read.value.unwrap().as_str(), "configured"); + } + assert_eq!( + ctx.config_store_default_binding().unwrap().default_key, + "marker" + ); + for secrets in [ + ctx.secret_store("credentials").unwrap(), + ctx.secret_store_default().unwrap(), + ] { + assert_eq!( + secrets.get_bytes("token").await.unwrap().unwrap(), + bytes::Bytes::from_static(b"test-secret") + ); + } + let kv = ctx.kv_store("sessions").expect("named kv"); + let visits: u32 = kv.get_or("visits", 0).await.unwrap(); + let next_visit = visits.checked_add(1).unwrap(); + kv.put("visits", &next_visit).await.unwrap(); + assert_eq!( + ctx.kv_store_default() + .unwrap() + .get::("visits") + .await + .unwrap(), + Some(next_visit) + ); + Ok(next_visit.to_string()) + } + + struct ConfiguredApp; + + struct DropProbe(Arc); + + struct CountingMiddleware(Arc); + + struct RecordingClient { + calls: Arc, + time: OutboundTime, + } + + struct Reports(Arc>>); + + impl ResponseEgressObserver for Reports { + fn complete(&self, report: &ResponseEgressReport) { + self.0.lock().unwrap().push(report.clone()); + } + } + + impl RecordingClient { + fn finish_ready( + &self, + request: OutboundDispatchRequest, + ) -> Result { + use futures_util::{FutureExt as _, TryFutureExt as _}; + let mut future = Box::pin(self.dispatch_head(request).and_then(OutboundHead::finish)); + // Fixed one-chunk replies need one cooperative turn before EOF. + for _ in 0_u8..2 { + if let Some(outcome) = future.as_mut().now_or_never() { + return outcome; + } + } + Err(EdgeError::internal(IoError::other( + "scripted response exceeded its fixed poll bound", + ))) + } + } + + impl OutboundHttpClient for RecordingClient { + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let outcome = self.finish_ready(request); + Ok(OutboundPending::new( + Box::pin(ready(outcome)), + OutboundExchangeOwner::new(|| {}), + )) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + self.calls.fetch_add(1, Ordering::SeqCst); + let (_, context) = request.into_parts(); + let head = OutboundHead::new( + StatusCode::OK, + OutboundHeaderInput::new( + "supplied", + HeaderMap::new(), + OutboundHeaderFidelity::AdapterVisibleFields, + ), + Box::pin(iter([Ok(bytes::Bytes::from_static(b"supplied"))])), + context, + OutboundExchangeOwner::new(|| {}), + ); + Box::pin(ready(head)) + } + + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, requests) = batch.into_slots(); + let events = requests + .into_iter() + .map(|(index, request)| { + let outcome = self.finish_ready(request); + OutboundBatchDriverEvent::Item { + index, + completed_at: self.time.clock().now(), + outcome, + } + }) + .collect::>(); + OutboundBatch::from_driver(context, iter(events), OutboundExchangeOwner::new(|| {})) + } + + fn time(&self) -> OutboundTime { + self.time.clone() + } + + fn validate_supported_options( + &self, + _request: &OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + Ok(()) + } + } + + #[async_trait::async_trait(?Send)] + impl Middleware for CountingMiddleware { + async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result { + self.0.fetch_add(1, Ordering::SeqCst); + next.run(ctx).await + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "Drop::pin_drop is unstable and conflicts with drop" + )] + impl Drop for DropProbe { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "only assembly policies are under test" + )] + impl Hooks for ConfiguredApp { + fn configure(app: &mut App) -> Result<(), EdgeError> { + app.set_ingress_head_limits( + IngressHeadLimits::default().with_max_request_target_bytes(64)?, + ); + app.set_error_response_renderer(|error| { + let mut response = error.into_response().expect("test renderer"); + response + .headers_mut() + .insert("x-renderer", "configured".parse().unwrap()); + response + }); + Ok(()) + } + + fn routes() -> Result { + Ok(RouterService::builder() + .get("/", |_| async { Ok::<_, EdgeError>("hello") }) + .get("/target/{*rest}", |_| async { Ok::<_, EdgeError>("hello") }) + .build()) + } + } + + fn server(app: App) -> AxumDevServer { + AxumDevServer::with_config( + app, + AxumDevServerConfig { + addr: SocketAddr::from(([127, 0, 0, 1], 0)), + enable_ctrl_c: false, + ..AxumDevServerConfig::default() + }, + ) + } + + async fn exchange(address: SocketAddr, request: &[u8]) -> Vec { + timeout(Duration::from_secs(5), async { + let mut socket = TcpStream::connect(address).await.unwrap(); + socket.write_all(request).await.unwrap(); + let mut response = Vec::new(); + socket.read_to_end(&mut response).await.unwrap(); + response + }) + .await + .expect("bounded exchange") + } + + async fn read_head(socket: &mut TcpStream) -> String { + let mut head = Vec::new(); + while !head.ends_with(b"\r\n\r\n") { + head.push(socket.read_u8().await.unwrap()); + assert!(head.len() <= 8192, "bounded test response head"); + } + String::from_utf8(head).unwrap() + } + + async fn read_chunk(socket: &mut TcpStream) -> Vec { + let mut line = Vec::new(); + while !line.ends_with(b"\r\n") { + line.push(socket.read_u8().await.unwrap()); + assert!(line.len() <= 32, "bounded chunk size line"); + } + let size = usize::from_str_radix(String::from_utf8(line).unwrap().trim(), 16).unwrap(); + let mut chunk = vec![0; size]; + socket.read_exact(&mut chunk).await.unwrap(); + assert_eq!(socket.read_u16().await.unwrap(), 0x0d0a); + chunk + } + + #[tokio::test] + async fn configured_target_limit_applies_on_tcp() { + let target = format!("/target/{}", "a".repeat(92)); + assert_eq!(target.len(), 100); + let app = App::build::(crate::AXUM_PLATFORM).unwrap(); + let running = AxumTestServer::start(server(app)).await.unwrap(); + let response = reqwest::get(format!("http://{}{target}", running.address())) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::URI_TOO_LONG); + assert_eq!(response.headers()["x-renderer"], "configured"); + running.shutdown().await.unwrap(); + + let direct_app = App::build::(crate::AXUM_PLATFORM).unwrap(); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let (stop, stopped) = channel(); + let task = tokio::spawn(server(direct_app).serve(listener, async { + let _closed = stopped.await; + })); + let direct_response = exchange( + address, + format!("GET {target} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") + .as_bytes(), + ) + .await; + assert!(direct_response.starts_with(b"HTTP/1.1 414")); + stop.send(()).unwrap(); + task.await.unwrap().unwrap(); + + let unconfigured = App::new(ConfiguredApp::routes().unwrap()); + let control = AxumTestServer::start(server(unconfigured)).await.unwrap(); + let control_response = exchange( + control.address(), + format!("GET {target} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") + .as_bytes(), + ) + .await; + assert!(control_response.starts_with(b"HTTP/1.1 200")); + assert!(control_response.ends_with(b"\r\n\r\nhello")); + control.shutdown().await.unwrap(); + } + + #[tokio::test] + async fn configured_renderer_applies_to_not_found() { + let app = App::build::(crate::AXUM_PLATFORM).unwrap(); + let running = AxumTestServer::start(server(app)).await.unwrap(); + let response = reqwest::get(format!("http://{}/missing", running.address())) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert_eq!(response.headers()["x-renderer"], "configured"); + running.shutdown().await.unwrap(); + } + + #[tokio::test] + async fn admission_abort_prevents_response_and_dispatch() { + let handlers = Arc::new(AtomicUsize::new(0)); + let middleware = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&handlers); + let router = RouterService::builder() + .get("/", move |_| { + let calls = Arc::clone(&observed); + async move { + calls.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("hello") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware))) + .build(); + let mut app = App::new(router); + app.set_ingress_admission_policy(|_| AdmissionDecision::Abort); + let running = AxumTestServer::start(server(app)).await.unwrap(); + let mut socket = TcpStream::connect(running.address()).await.unwrap(); + socket + .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .unwrap(); + let mut response = Vec::new(); + let _closed = timeout(Duration::from_secs(2), socket.read_to_end(&mut response)) + .await + .unwrap(); + assert!(response.is_empty()); + assert_eq!(handlers.load(Ordering::SeqCst), 0); + assert_eq!(middleware.load(Ordering::SeqCst), 0); + running.shutdown().await.unwrap(); + } + + #[tokio::test] + #[expect( + clippy::redundant_test_prefix, + reason = "matches the approved EZ-23 acceptance sentinel" + )] + async fn test_server_preserves_upload_and_read_deadline() { + let router = RouterService::builder() + .post("/", |ctx: RequestContext| async move { + let bytes = ctx.body_bytes(2 * 1024 * 1024).await?; + assert!(bytes.iter().all(|byte| *byte == b'x')); + Ok::<_, EdgeError>(bytes.len().to_string()) + }) + .build(); + let mut app = App::new(router); + app.set_ingress_admission_policy(|head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_millis(500)), + }); + let running = AxumTestServer::start(server(app)).await.unwrap(); + let mut request = b"POST / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n".to_vec(); + for _chunk in 0_u32..16 { + request.extend_from_slice(b"10000\r\n"); + request.extend(repeat_n(b'x', 0x0001_0000)); + request.extend_from_slice(b"\r\n"); + } + request.extend_from_slice(b"0\r\n\r\n"); + let response = exchange(running.address(), &request).await; + assert!(response.starts_with(b"HTTP/1.1 200")); + assert!(response.ends_with(b"1048576")); + let timeout_response = exchange( + running.address(), + b"POST / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: 1\r\n\r\n", + ) + .await; + assert!(timeout_response.starts_with(b"HTTP/1.1 408")); + running.shutdown().await.unwrap(); + } + + #[tokio::test] + #[expect( + clippy::redundant_test_prefix, + reason = "matches the approved EZ-23 acceptance sentinel" + )] + async fn test_server_streams_before_source_completion() { + let produced = Arc::new(AtomicUsize::new(0)); + let (acknowledge, acknowledgement_receiver) = channel(); + let acknowledgement = Arc::new(StdMutex::new(Some(acknowledgement_receiver))); + let observed = Arc::clone(&produced); + let router = RouterService::builder() + .get("/", move |_| { + let production = Arc::clone(&observed); + let acknowledged = acknowledgement.lock().unwrap().take().unwrap(); + async move { + let body = async_stream::stream! { + production.store(1, Ordering::SeqCst); + yield Ok::<_, EdgeError>(bytes::Bytes::from_static(b"first")); + acknowledged.await.unwrap(); + sleep(Duration::from_millis(50)).await; + production.store(2, Ordering::SeqCst); + yield Ok(bytes::Bytes::from_static(b"second")); + sleep(Duration::from_millis(50)).await; + production.store(3, Ordering::SeqCst); + yield Ok(bytes::Bytes::from_static(b"third")); + }; + Ok::<_, EdgeError>(Response::new(Body::from_external_stream(body))) + } + }) + .build(); + let running = AxumTestServer::start(server(App::new(router))) + .await + .unwrap(); + timeout(Duration::from_secs(3), async { + let mut socket = TcpStream::connect(running.address()).await.unwrap(); + socket + .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") + .await + .unwrap(); + let head = read_head(&mut socket).await; + assert!(head.contains("transfer-encoding: chunked\r\n")); + assert_eq!(read_chunk(&mut socket).await, b"first"); + assert_eq!(produced.load(Ordering::SeqCst), 1); + acknowledge.send(()).unwrap(); + assert_eq!(read_chunk(&mut socket).await, b"second"); + assert_eq!(read_chunk(&mut socket).await, b"third"); + assert!(read_chunk(&mut socket).await.is_empty()); + }) + .await + .unwrap(); + running.shutdown().await.unwrap(); + } + + #[tokio::test] + #[expect( + clippy::redundant_test_prefix, + reason = "matches the approved EZ-23 acceptance sentinel" + )] + async fn test_server_buffers_with_exact_content_length() { + let running = AxumTestServer::start(server( + App::build::(crate::AXUM_PLATFORM).unwrap(), + )) + .await + .unwrap(); + let response = exchange( + running.address(), + b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ) + .await; + let wire = String::from_utf8(response).unwrap(); + assert!(wire.contains("content-length: 5\r\n")); + assert!(wire.ends_with("\r\n\r\nhello")); + assert!(!wire.contains("transfer-encoding")); + running.shutdown().await.unwrap(); + } + + #[tokio::test] + #[expect( + clippy::redundant_test_prefix, + reason = "matches the approved EZ-23 acceptance sentinel" + )] + async fn test_server_suppresses_head_payload() { + let drops = Arc::new(AtomicUsize::new(0)); + let polls = Arc::new(AtomicUsize::new(0)); + let resources = Arc::clone(&drops); + let observed = Arc::clone(&polls); + let router = RouterService::builder() + .route("/", Method::HEAD, move |_| { + let guard = DropProbe(Arc::clone(&resources)); + let body_polls = Arc::clone(&observed); + async move { + let body = poll_fn(move |_| { + let _retained = &guard; + body_polls.fetch_add(1, Ordering::SeqCst); + Poll::Ready(Some(Ok::<_, EdgeError>(bytes::Bytes::from_static( + b"never", + )))) + }); + Ok::<_, EdgeError>(Response::new(Body::from_external_stream(body))) + } + }) + .build(); + let running = AxumTestServer::start(server(App::new(router))) + .await + .unwrap(); + let response = exchange( + running.address(), + b"HEAD / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ) + .await; + assert!(response.starts_with(b"HTTP/1.1 200")); + let head_end = response + .windows(4) + .position(|bytes| bytes == b"\r\n\r\n") + .unwrap() + .checked_add(4); + assert_eq!( + head_end, + Some(response.len()), + "HEAD must transmit no bytes after its first head terminator" + ); + assert_eq!(polls.load(Ordering::SeqCst), 0); + assert_eq!(drops.load(Ordering::SeqCst), 1); + running.shutdown().await.unwrap(); + } + + #[tokio::test] + async fn raw_head_limit_precedes_admission() { + let admissions = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&admissions); + let mut app = App::build::(crate::AXUM_PLATFORM).unwrap(); + app.set_ingress_admission_policy(move |head| { + observed.fetch_add(1, Ordering::SeqCst); + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(2)), + } + }); + let configured = AxumDevServer::with_config( + app, + AxumDevServerConfig { + ingress: AxumIngressConfig::new(2, 8192, 4, Duration::from_secs(2)).unwrap(), + addr: SocketAddr::from(([127, 0, 0, 1], 0)), + enable_ctrl_c: false, + }, + ); + let running = AxumTestServer::start(configured).await.unwrap(); + let response = exchange(running.address(), b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nA: 1\r\nA: 2\r\nA: 3\r\n\r\n").await; + assert!(response.starts_with(b"HTTP/1.1 431")); + assert_eq!(admissions.load(Ordering::SeqCst), 0); + let recovered = exchange( + running.address(), + b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ) + .await; + assert!(recovered.starts_with(b"HTTP/1.1 200")); + assert_eq!(admissions.load(Ordering::SeqCst), 1); + running.shutdown().await.unwrap(); + } + + #[tokio::test] + #[expect( + clippy::redundant_test_prefix, + reason = "matches the approved EZ-23 acceptance sentinel" + )] + async fn test_server_shutdown_releases_live_source() { + let sources = Arc::new(AtomicUsize::new(0)); + let completions = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&sources); + let router = RouterService::builder() + .get("/", move |_| { + let guard = DropProbe(Arc::clone(&observed)); + async move { + let mut first = true; + let body = poll_fn(move |_| { + let _retained = &guard; + if take(&mut first) { + Poll::Ready(Some(Ok::<_, EdgeError>(bytes::Bytes::from_static( + b"first", + )))) + } else { + Poll::Pending + } + }); + Ok::<_, EdgeError>(Response::new(Body::from_external_stream(body))) + } + }) + .build(); + let completion_counter = Arc::clone(&completions); + let mut app = App::new(router); + app.set_ingress_admission_policy(move |head| { + let completed = Arc::clone(&completion_counter); + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::new(move |_| { + completed.fetch_add(1, Ordering::SeqCst); + }), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(2)), + } + }); + let running = AxumTestServer::start(server(app)).await.unwrap(); + let mut socket = TcpStream::connect(running.address()).await.unwrap(); + socket + .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .unwrap(); + timeout(Duration::from_secs(2), async { + read_head(&mut socket).await; + assert_eq!(read_chunk(&mut socket).await, b"first"); + }) + .await + .unwrap(); + timeout(Duration::from_secs(2), running.shutdown()) + .await + .unwrap() + .unwrap(); + assert_eq!(sources.load(Ordering::SeqCst), 1); + assert_eq!(completions.load(Ordering::SeqCst), 1); + assert_eq!(socket.read(&mut [0]).await.unwrap(), 0); + } + + #[tokio::test] + async fn foreign_platform_app_keeps_configured_policies() { + let platform = edgezero_adapter_cloudflare::CLOUDFLARE_PLATFORM; + let app = App::build::(platform).unwrap(); + assert_eq!(app.platform(), platform); + let running = AxumTestServer::start(server(app)).await.unwrap(); + let response = exchange( + running.address(), + format!( + "GET /{} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + "a".repeat(99) + ) + .as_bytes(), + ) + .await; + assert!(response.starts_with(b"HTTP/1.1 414")); + assert!( + String::from_utf8(response) + .unwrap() + .contains("x-renderer: configured\r\n") + ); + running.shutdown().await.unwrap(); + } + + #[tokio::test] + async fn plain_tokio_test_runs_local_handler_work() { + let router = RouterService::builder() + .get("/", |_| async { + let values = Rc::new(["local", "stream", "complete"]); + let (sender, receiver) = mpsc::channel(1); + spawn_local(async move { + for value in values.iter() { + sender + .send(Ok::<_, EdgeError>(bytes::Bytes::copy_from_slice( + value.as_bytes(), + ))) + .await + .unwrap(); + yield_now().await; + } + }); + let body = unfold(receiver, |mut stream_receiver| async { + stream_receiver + .recv() + .await + .map(|value| (value, stream_receiver)) + }); + Ok::<_, EdgeError>(Response::new(Body::from_external_stream(body))) + }) + .build(); + let running = AxumTestServer::start(server(App::new(router))) + .await + .unwrap(); + let body = timeout(Duration::from_secs(2), async { + reqwest::get(format!("http://{}/", running.address())) + .await + .unwrap() + .text() + .await + .unwrap() + }) + .await + .unwrap(); + assert_eq!(body, "localstreamcomplete"); + running.shutdown().await.unwrap(); + } + + #[tokio::test] + async fn state_client_skips_real_transport_initialization() { + let target = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let target_uri = format!("http://{}/should-not-connect", target.local_addr().unwrap()); + let calls = Arc::new(AtomicUsize::new(0)); + let supplied_client = HttpClient::with_client(RecordingClient { + calls: Arc::clone(&calls), + time: native_time(MonotonicClock::default()), + }) + .with_proxy_marker(false); + let router = RouterService::builder() + .with_state(supplied_client) + .get("/", move |ctx: RequestContext| { + let uri = target_uri.clone(); + async move { + let client = ctx.http_client().expect("registered client"); + assert!(!client.proxy_marker_enabled()); + let response = client.send(OutboundRequest::get(&uri)?).await?; + assert_eq!(response.status(), StatusCode::OK); + Ok::<_, EdgeError>("sent") + } + }) + .build(); + let running = AxumTestServer::start(server(App::new(router))) + .await + .unwrap(); + let response = exchange( + running.address(), + b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ) + .await; + assert!(response.starts_with(b"HTTP/1.1 200")); + assert_eq!(calls.load(Ordering::SeqCst), 1); + timeout(Duration::from_millis(25), target.accept()) + .await + .expect_err("supplied client must not open a target socket"); + running.shutdown().await.unwrap(); + } + + #[tokio::test] + async fn configured_egress_policy_clock_and_observer_survive_server_ownership() { + let reports = Arc::new(StdMutex::new(Vec::new())); + let policies = Arc::new(AtomicUsize::new(0)); + let completions = Arc::new(AtomicUsize::new(0)); + let start = MonotonicInstant::now() + .checked_add(Duration::from_secs(60)) + .unwrap(); + let mut app = App::build::(crate::AXUM_PLATFORM).unwrap(); + app.set_monotonic_clock(MonotonicClock::new(move || start)); + app.set_response_egress_observer(Reports(Arc::clone(&reports))); + let observed = Arc::clone(&policies); + app.set_response_egress_policy(move |head, began| { + assert_eq!(head.request_start(), start); + assert_eq!(began, start); + observed.fetch_add(1, Ordering::SeqCst); + ResponseEgressPolicy { + write_deadline: Deadline::at_instant( + began.checked_add(Duration::from_secs(1)).unwrap(), + ), + } + }); + let completion_counter = Arc::clone(&completions); + app.set_ingress_admission_policy(move |head| { + assert_eq!(head.request_start(), start); + let completed = Arc::clone(&completion_counter); + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::new(move |_| { + completed.fetch_add(1, Ordering::SeqCst); + }), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + } + }); + let running = AxumTestServer::start(server(app)).await.unwrap(); + for _request in 0_u32..2 { + let response = exchange( + running.address(), + b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ) + .await; + assert!(response.starts_with(b"HTTP/1.1 200")); + } + running.shutdown().await.unwrap(); + assert_eq!(policies.load(Ordering::SeqCst), 2); + assert_eq!(completions.load(Ordering::SeqCst), 2); + let observed_reports = reports.lock().unwrap(); + assert_eq!(observed_reports.len(), 2); + for report in observed_reports.iter() { + assert_eq!(report.request_start, start); + assert_eq!(report.outcome, ResponseEgressOutcome::HostHandoff); + } + } + + #[tokio::test] + async fn configured_detached_decision_sends_or_aborts_before_admission() { + for abort in [false, true] { + let decisions = Arc::new(AtomicUsize::new(0)); + let completions = Arc::new(AtomicUsize::new(0)); + let mut app = App::build::(crate::AXUM_PLATFORM).unwrap(); + app.set_ingress_admission_policy(|_| panic!("invalid head must precede admission")); + app.set_response_egress_policy(|_, _| { + panic!("detached response skips routed egress policy") + }); + let observed = Arc::clone(&decisions); + let completed = Arc::clone(&completions); + app.set_detached_response_egress_decision_factory(move |head| { + observed.fetch_add(1, Ordering::SeqCst); + if abort { + DetachedResponseEgressDecision::Abort + } else { + let terminal_counter = Arc::clone(&completed); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |_| { + terminal_counter.fetch_add(1, Ordering::SeqCst); + }), + deadline: Deadline::at_instant( + head.request_start() + .checked_add(Duration::from_secs(1)) + .unwrap(), + ), + } + } + }); + let running = AxumTestServer::start(server(app)).await.unwrap(); + let mut socket = TcpStream::connect(running.address()).await.unwrap(); + socket + .write_all( + format!( + "GET /{} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + "a".repeat(99) + ) + .as_bytes(), + ) + .await + .unwrap(); + let mut response = Vec::new(); + let _closed = timeout(Duration::from_secs(2), socket.read_to_end(&mut response)) + .await + .unwrap(); + if abort { + assert!(response.is_empty()); + } else { + assert!(response.starts_with(b"HTTP/1.1 414")); + } + running.shutdown().await.unwrap(); + assert_eq!(decisions.load(Ordering::SeqCst), 1); + assert_eq!(completions.load(Ordering::SeqCst), usize::from(!abort)); + } + } + + #[tokio::test] + async fn dropped_test_server_releases_live_connections_without_blocking() { + let app = App::build::(crate::AXUM_PLATFORM).unwrap(); + let running = AxumTestServer::start(server(app)).await.unwrap(); + let mut socket = TcpStream::connect(running.address()).await.unwrap(); + socket.write_all(b"GET / HTTP/1.1\r\n").await.unwrap(); + drop(running); + let mut response = Vec::new(); + let _closed = timeout(Duration::from_secs(2), socket.read_to_end(&mut response)) + .await + .unwrap(); + assert!(response.is_empty()); + } + + #[tokio::test] + async fn server_reports_binding_and_serving_failures() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let app = App::build::(crate::AXUM_PLATFORM).unwrap(); + let server = AxumDevServer::with_config( + app, + AxumDevServerConfig { + addr: listener.local_addr().unwrap(), + enable_ctrl_c: false, + ..AxumDevServerConfig::default() + }, + ); + assert!(AxumTestServer::start(server).await.is_err()); + let running = AxumTestServer { + address: listener.local_addr().unwrap(), + shutdown: None, + task: Some(tokio::spawn(async { + Err(anyhow::anyhow!("serving failure fixture")) + })), + }; + assert_eq!( + running.shutdown().await.unwrap_err().to_string(), + "serving failure fixture" + ); + } + + #[tokio::test] + async fn explicit_settings_preserve_state_store_and_policy_identity_across_requests() { + use crate::config_store::AxumConfigStore; + use crate::config_store_limits::ConfigStoreLimits; + use crate::key_value_store::PersistentKvStore; + use edgezero_core::config_store::ConfigStoreHandle; + use edgezero_core::key_value_store::KvHandle; + use edgezero_core::secret_store::{InMemorySecretStore, SecretHandle}; + use edgezero_core::store_registry::{ + BoundSecretStore, ConfigRegistry, ConfigStoreBinding, KvRegistry, SecretRegistry, + }; + + let shared = Arc::new(ParityState { + configured: AtomicUsize::new(0), + limits: ConfigExtractionLimits { + max_backend_bytes: 256, + max_blob_bytes: 64, + max_secret_bytes: 64, + max_total_bytes: 512, + timeout: Duration::from_millis(500), + ..ConfigExtractionLimits::default() + }, + start: MonotonicInstant::now() + .checked_add(Duration::from_secs(60)) + .unwrap(), + }); + let router = RouterService::builder() + .with_state(Arc::clone(&shared)) + .route("/state/{id}", Method::PUT, parity_handler) + .build(); + let app = App::from_router::(router, crate::AXUM_PLATFORM).unwrap(); + assert_eq!(app.name(), "configured parity app"); + assert!(Arc::ptr_eq( + app.state::>().unwrap(), + &shared + )); + let config = ConfigStoreHandle::new(Arc::new( + AxumConfigStore::from_map( + [("marker".to_owned(), "configured".to_owned())], + ConfigStoreLimits::default(), + ) + .unwrap(), + )); + let directory = tempfile::tempdir().unwrap(); + let kv = KvHandle::new(Arc::new( + PersistentKvStore::new(directory.path().join("state.redb")).unwrap(), + )); + let secrets = SecretHandle::new(Arc::new(InMemorySecretStore::new([( + "provider/token", + "test-secret", + )]))); + let configured = server(app) + .with_config_registry(ConfigRegistry::single_id( + "settings".to_owned(), + ConfigStoreBinding { + handle: config, + default_key: "marker".to_owned(), + }, + )) + .with_kv_registry(KvRegistry::single_id("sessions".to_owned(), kv)) + .with_secret_registry(SecretRegistry::single_id( + "credentials".to_owned(), + BoundSecretStore::new(secrets, "provider".to_owned()), + )); + let running = AxumTestServer::start(configured).await.unwrap(); + for expected in ["1", "2"] { + let response = exchange(running.address(), b"PUT /state/42 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: 0\r\n\r\n").await; + assert!(response.starts_with(b"HTTP/1.1 200")); + assert!(response.ends_with(expected.as_bytes())); + } + running.shutdown().await.unwrap(); + assert_eq!(shared.configured.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn configured_write_deadline_releases_pending_source_once() { + let sources = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&sources); + let router = RouterService::builder() + .get("/", move |_| { + let guard = DropProbe(Arc::clone(&observed)); + async move { + let mut first = true; + let body = poll_fn(move |_| { + let _retained = &guard; + if take(&mut first) { + Poll::Ready(Some(Ok::<_, EdgeError>(bytes::Bytes::from_static( + b"first", + )))) + } else { + Poll::Pending + } + }); + Ok::<_, EdgeError>(Response::new(Body::from_external_stream(body))) + } + }) + .build(); + let reports = Arc::new(StdMutex::new(Vec::new())); + let mut app = App::new(router); + app.set_response_egress_observer(Reports(Arc::clone(&reports))); + app.set_response_egress_policy(|_, began| ResponseEgressPolicy { + write_deadline: Deadline::at_instant( + began.checked_add(Duration::from_millis(500)).unwrap(), + ), + }); + let running = AxumTestServer::start(server(app)).await.unwrap(); + let mut socket = TcpStream::connect(running.address()).await.unwrap(); + socket + .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .unwrap(); + timeout(Duration::from_secs(3), async { + read_head(&mut socket).await; + assert_eq!(read_chunk(&mut socket).await, b"first"); + assert_eq!(socket.read(&mut [0]).await.unwrap(), 0); + }) + .await + .unwrap(); + running.shutdown().await.unwrap(); + assert_eq!(sources.load(Ordering::SeqCst), 1); + let observed_reports = reports.lock().unwrap(); + assert_eq!(observed_reports.len(), 1); + assert_eq!( + observed_reports[0].outcome, + ResponseEgressOutcome::DeadlineExceeded + ); + } + + #[tokio::test] + async fn admission_refusal_skips_dispatch_and_keeps_server_healthy() { + let handlers = Arc::new(AtomicUsize::new(0)); + let middleware = Arc::new(AtomicUsize::new(0)); + let admissions = Arc::new(AtomicUsize::new(0)); + let completions = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&handlers); + let router = RouterService::builder() + .get("/", move |_| { + let count = Arc::clone(&observed); + async move { + count.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("healthy") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware))) + .build(); + let mut app = App::new(router); + let admission_count = Arc::clone(&admissions); + let completion_count = Arc::clone(&completions); + app.set_ingress_admission_policy(move |head| { + if admission_count.fetch_add(1, Ordering::SeqCst) == 0 { + let completed = Arc::clone(&completion_count); + let mut response = Response::new(Body::from("refused")); + *response.status_mut() = StatusCode::SERVICE_UNAVAILABLE; + AdmissionDecision::Refuse { + completion: ResponseEgressCompletion::new(move |_| { + completed.fetch_add(1, Ordering::SeqCst); + }), + response, + } + } else { + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(2)), + } + } + }); + let running = AxumTestServer::start(server(app)).await.unwrap(); + let refused = exchange( + running.address(), + b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ) + .await; + assert!(refused.starts_with(b"HTTP/1.1 503")); + assert!(refused.ends_with(b"refused")); + assert_eq!(handlers.load(Ordering::SeqCst), 0); + assert_eq!(middleware.load(Ordering::SeqCst), 0); + let healthy = exchange( + running.address(), + b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ) + .await; + assert!(healthy.starts_with(b"HTTP/1.1 200")); + assert!(healthy.ends_with(b"healthy")); + running.shutdown().await.unwrap(); + assert_eq!(handlers.load(Ordering::SeqCst), 1); + assert_eq!(middleware.load(Ordering::SeqCst), 1); + assert_eq!(completions.load(Ordering::SeqCst), 1); + } +} diff --git a/crates/edgezero-adapter-axum/tests/contract.rs b/crates/edgezero-adapter-axum/tests/contract.rs new file mode 100644 index 000000000..8f4856d24 --- /dev/null +++ b/crates/edgezero-adapter-axum/tests/contract.rs @@ -0,0 +1,1383 @@ +#![cfg(all(test, feature = "axum"))] +#![expect( + clippy::expect_used, + clippy::tests_outside_test_module, + reason = "this integration-test crate uses explicit fixture diagnostics and consists only of contract tests" +)] + +use core::convert::Infallible; +use std::future::Future as _; +use std::io::Write as _; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::task::Poll; +use std::time::{Duration, Instant}; + +use async_stream::stream as async_body_stream; +use axum::Router; +use axum::body::Body; +use axum::extract::Path; +use axum::http::HeaderValue; +use axum::http::header::{CACHE_CONTROL, CONTENT_ENCODING, CONTENT_LENGTH, HOST, SET_COOKIE}; +use axum::response::Response; +use axum::routing::{get, post}; +use bytes::Bytes; +use edgezero_adapter_axum::outbound::AxumOutboundClient; +use edgezero_core::body::Body as CoreBody; +use edgezero_core::error::{BadGatewayReason, BudgetSource, EdgeError, ResponseLimitReason}; +use edgezero_core::http::{HeaderMap, Method, StatusCode}; +use edgezero_core::time::Deadline; +use edgezero_core::{ + FLATE_DECODER_FIXED_CHARGE_BYTES, HttpClient, MonotonicClock, OutboundBatchNext, + OutboundBatchTermination, OutboundCachePolicy, OutboundCollectedSlot, OutboundProxyMarker, + OutboundRedirectAction, OutboundRedirectLimit, OutboundRequest, OutboundSlotResult, + OutboundTime, PROXY_HEADER, ResponseDecoding, +}; +use flate2::Compression; +use flate2::write::{GzEncoder, ZlibEncoder}; +use futures_util::future::{Either, select}; +use futures_util::stream; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; +use tokio::net::TcpListener; +use tokio::sync::Notify; +use tokio::task::{JoinSet, LocalSet}; +use tokio::time::{sleep, timeout}; + +fn resolved_slot(slot: &OutboundCollectedSlot) -> &OutboundSlotResult { + match slot { + OutboundCollectedSlot::Resolved(result) => Some(result), + OutboundCollectedSlot::AlreadyDelivered | OutboundCollectedSlot::Unresolved => None, + } + .expect("expected a resolved slot") +} + +fn take_resolved_slot(slot: OutboundCollectedSlot) -> OutboundSlotResult { + match slot { + OutboundCollectedSlot::Resolved(result) => Some(result), + OutboundCollectedSlot::AlreadyDelivered | OutboundCollectedSlot::Unresolved => None, + } + .expect("expected an owned resolved slot") +} + +fn paired_cutoff_time( + clock: MonotonicClock, + advancement: Arc, + wake_count: Arc, +) -> OutboundTime { + OutboundTime::new(clock, move |deadline, paired_clock| { + let notifier = Arc::clone(&advancement); + let counter = Arc::clone(&wake_count); + Box::pin(async move { + let remaining = deadline + .remaining_at(paired_clock.now()) + .unwrap_or_default(); + let notification = notifier.notified(); + let timeout = sleep(remaining); + futures_util::pin_mut!(notification, timeout); + if let Either::Left(((), _)) = select(notification, timeout).await { + counter.fetch_add(1, Ordering::SeqCst); + } + Ok(()) + }) + }) +} + +fn gzip(input: &[u8]) -> Vec { + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(input).expect("gzip input"); + encoder.finish().expect("gzip output") +} + +fn zlib(input: &[u8]) -> Vec { + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(input).expect("deflate input"); + encoder.finish().expect("deflate output") +} + +async fn start_origin(router: Router) -> String { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind origin"); + let address = listener.local_addr().expect("origin address"); + tokio::spawn(async move { + axum::serve(listener, router).await.expect("serve origin"); + }); + format!("http://{address}") +} + +#[tokio::test] +async fn redirect_following_uses_two_origins_and_discards_pending_bodies() { + let final_origin = start_origin(Router::new().route("/final", get(|| async { "final" }))).await; + let target = format!("{final_origin}/final"); + let ordinary_location = target.clone(); + let pending_location = target.clone(); + let first_origin = start_origin( + Router::new() + .route( + "/ordinary", + get(move || { + let location = ordinary_location.clone(); + async move { + Response::builder() + .status(StatusCode::FOUND) + .header("location", location) + .body(Body::empty()) + .expect("redirect") + } + }), + ) + .route( + "/pending", + get(move || { + let location = pending_location.clone(); + async move { + Response::builder() + .status(StatusCode::FOUND) + .header("location", location) + .header(CONTENT_ENCODING, "br") + .header(CONTENT_LENGTH, "10485760") + .body(Body::from_stream(stream::pending::< + Result, + >())) + .expect("pending redirect") + } + }), + ), + ) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let plain = client + .send(OutboundRequest::get(format!("{first_origin}/ordinary")).expect("request")) + .await + .expect("plain redirect"); + assert_eq!(plain.status(), StatusCode::FOUND); + for path in ["ordinary", "pending"] { + let result = timeout( + Duration::from_secs(2), + client.follow( + OutboundRequest::get(format!("{first_origin}/{path}")) + .expect("request") + .max_response_bytes(5) + .timeout(Duration::from_secs(2)), + OutboundRedirectLimit::new(1).expect("limit"), + |_context| Ok::<_, Infallible>(OutboundRedirectAction::FollowPrepared), + ), + ) + .await + .expect("following never waits for intermediate payload") + .expect("followed redirect"); + assert_eq!(result.response().status(), StatusCode::OK); + assert_eq!(result.final_uri().to_string(), target); + assert_eq!(result.redirects_followed(), 1); + assert_eq!(result.stop_reason(), None); + let (response, _, _, _) = result.into_parts(); + assert_eq!(response.into_bytes_bounded(5).await.expect("body"), "final"); + } +} + +#[tokio::test] +async fn follow_without_redirect_preserves_adapter_coding_and_limit_results() { + let origin = start_origin(Router::new().route( + "/{coding}/{length}", + get(|Path((coding, length)): Path<(String, usize)>| async move { + let input = vec![b'x'; length]; + let bytes = match coding.as_str() { + "gzip" => gzip(&input), + "deflate" => zlib(&input), + _ => input, + }; + Response::builder() + .status(StatusCode::OK) + .header("date", "Tue, 27 Oct 2026 00:00:00 GMT") + .header(CONTENT_ENCODING, coding) + .header(CONTENT_LENGTH, bytes.len()) + .body(Body::from(bytes)) + .expect("response") + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + for coding in ["identity", "gzip", "deflate", "unsupported"] { + for length in [10_usize, 11] { + let make_request = || { + OutboundRequest::get(format!("{origin}/{coding}/{length}")) + .expect("request") + .max_response_bytes(10) + }; + let plain = client.send(make_request()).await; + let following = client + .follow( + make_request(), + OutboundRedirectLimit::new(1).expect("limit"), + |_context| Ok::<_, Infallible>(OutboundRedirectAction::FollowPrepared), + ) + .await; + match (plain, following) { + (Ok(sent), Ok(followed)) => { + assert_eq!(length, 10); + assert_eq!(sent.status(), followed.response().status()); + assert_eq!(sent.headers(), followed.response().headers()); + assert_eq!(followed.redirects_followed(), 0); + let (response, _, _, _) = followed.into_parts(); + assert_eq!( + sent.into_bytes_bounded(10).await.expect("sent bytes"), + response + .into_bytes_bounded(10) + .await + .expect("followed bytes") + ); + } + (Err(sent), Err(edgezero_core::OutboundFollowFailure::Hop { index, error })) => { + assert_eq!(length, 11); + assert_eq!(index, 0); + assert_eq!(sent.status(), error.status()); + assert_eq!(sent.message(), error.message()); + } + other => panic!("plain/follow mismatch: {other:?}"), + } + } + } +} + +#[tokio::test(flavor = "current_thread")] +async fn unpolled_batch_dispatches_all_slots() { + LocalSet::new() + .run_until(async { + let arrivals = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&arrivals); + let origin = start_origin(Router::new().route( + "/", + get(move || { + observed.fetch_add(1, Ordering::SeqCst); + async { "ready" } + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let batch = client.start_batch_until( + vec![ + OutboundRequest::get(&origin).expect("first request"), + OutboundRequest::get(&origin).expect("second request"), + ], + Deadline::after(Duration::from_secs(2)), + ); + timeout(Duration::from_secs(1), async { + while arrivals.load(Ordering::SeqCst) < 2 { + sleep(Duration::from_millis(1)).await; + } + }) + .await + .expect("both requests dispatch before batch observation"); + drop(batch); + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn begin_dispatches_without_collection() { + LocalSet::new() + .run_until(async { + let arrived = Arc::new(Notify::new()); + let observed = Arc::clone(&arrived); + let origin = start_origin(Router::new().route( + "/", + get(move || { + observed.notify_one(); + async { "ready" } + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let pending = client + .begin(OutboundRequest::get(&origin).expect("request")) + .await + .expect("scheduled request"); + timeout(Duration::from_secs(1), arrived.notified()) + .await + .expect("request dispatches without collecting its response"); + drop(pending); + }) + .await; +} + +#[tokio::test] +async fn begin_streamed_upload_rejection_precedes_scheduler_and_body_polling() { + let polls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&polls); + let source = stream::poll_fn(move |_| { + observed.fetch_add(1, Ordering::SeqCst); + Poll::Pending + }); + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let error = client + .begin( + OutboundRequest::post("http://127.0.0.1:9/upload") + .expect("request") + .body(CoreBody::from_stream(source)), + ) + .await + .expect_err("streamed Begin upload is unsupported"); + assert!(matches!(error, EdgeError::BadRequest { .. })); + assert_eq!(polls.load(Ordering::SeqCst), 0); +} + +#[tokio::test] +async fn missing_local_scheduler_returns_internal_before_transport_io() { + use futures_util::FutureExt as _; + let listener = TcpListener::bind("127.0.0.1:0").await.expect("origin"); + let target = format!("http://{}/", listener.local_addr().expect("address")); + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let error = client + .begin( + OutboundRequest::post(target) + .expect("request") + .body("buffered"), + ) + .await + .expect_err("missing LocalSet"); + assert!(matches!(error, EdgeError::Internal { .. })); + assert!( + listener.accept().now_or_never().is_none(), + "missing scheduler cannot open a connection" + ); +} + +#[tokio::test] +async fn settlement_timer_wakes_without_body_data() { + let polls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&polls); + let source = stream::poll_fn(move |_| { + observed.fetch_add(1, Ordering::SeqCst); + Poll::Pending + }); + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let error = timeout( + Duration::from_secs(1), + client.send( + OutboundRequest::get("http://127.0.0.1:9/") + .expect("request") + .body(CoreBody::from_stream(source)) + .timeout(Duration::from_millis(20)), + ), + ) + .await + .expect("outbound timer interrupts pending settlement") + .expect_err("settlement timeout"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); + assert!(polls.load(Ordering::SeqCst) > 0); +} + +#[tokio::test(flavor = "current_thread")] +async fn collected_begin_stream_stays_live() { + LocalSet::new() + .run_until(async { + let origin = start_origin(Router::new().route( + "/", + get(|| async { + let source = async_body_stream! { + yield Ok::<_, Infallible>(Bytes::from_static(b"first")); + sleep(Duration::from_millis(20)).await; + yield Ok::<_, Infallible>(Bytes::from_static(b"second")); + }; + Response::new(Body::from_stream(source)) + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let pending = client + .begin( + OutboundRequest::get(origin) + .expect("request") + .stream_response(), + ) + .await + .expect("begin"); + let response = pending.collect().await.expect("live response"); + assert_eq!( + response + .into_bytes_bounded(11) + .await + .expect("complete live stream"), + "firstsecond" + ); + }) + .await; +} + +#[tokio::test] +async fn configured_connect_timeout_reuses_shared_keep_alive_transport() { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("origin"); + let target = format!("http://{}/", listener.local_addr().expect("address")); + let accepts = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&accepts); + let server = tokio::spawn(async move { + let mut connections = JoinSet::new(); + loop { + let (mut socket, _) = listener.accept().await.expect("connection"); + observed.fetch_add(1, Ordering::SeqCst); + connections.spawn(async move { + loop { + let mut head = Vec::new(); + while !head.ends_with(b"\r\n\r\n") { + assert!(head.len() < 8192, "bounded fixture head"); + let Ok(byte) = socket.read_u8().await else { + return; + }; + head.push(byte); + } + if socket + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok") + .await + .is_err() + { + return; + } + } + }); + } + }); + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + for _ in 0_u8..2 { + let response = client + .send( + OutboundRequest::get(&target) + .expect("request") + .connect_timeout(Duration::from_millis(1)) + .timeout(Duration::from_secs(1)), + ) + .await + .expect("configured setting remains accepted"); + assert_eq!(response.into_bytes_bounded(2).await.expect("body"), "ok"); + } + let accepted = accepts.load(Ordering::SeqCst); + server.abort(); + let _cancelled = server.await; + assert_eq!( + accepted, 1, + "configured requests must retain the shared connection pool" + ); +} + +#[tokio::test] +async fn connect_timeout_acceptance_preserves_governing_deadline_without_emulation() { + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + for (connect, total, cause) in [ + ( + Duration::from_millis(1), + Duration::from_millis(50), + BudgetSource::PerCallTimeout, + ), + ( + Duration::from_secs(1), + Duration::from_millis(20), + BudgetSource::PerCallTimeout, + ), + ] { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("TLS origin"); + let target = format!("https://{}/", listener.local_addr().expect("address")); + let server = tokio::spawn(async move { + let (_socket, _) = listener.accept().await.expect("connection"); + sleep(Duration::from_secs(2)).await; + }); + let error = client + .send( + OutboundRequest::get(target) + .expect("request") + .connect_timeout(connect) + .timeout(total), + ) + .await + .expect_err("stalled TLS handshake"); + assert!( + matches!(error, EdgeError::GatewayTimeout { cause: actual, .. } if actual == cause), + "{error:?}" + ); + server.abort(); + let _cancelled = server.await; + } +} + +async fn start_coding_origin(coding: &'static str, origin_body: Vec) -> String { + start_origin(Router::new().route( + "/{status}", + get(move |Path(status): Path, headers: HeaderMap| { + let bytes = origin_body.clone(); + async move { + assert_eq!( + headers.get("accept-encoding").expect("encoding"), + "gzip, deflate", + "request encoding negotiation must survive both response decoding choices" + ); + Response::builder() + .status(status) + .header(CONTENT_ENCODING, coding) + .header(CONTENT_LENGTH, bytes.len()) + .header("etag", "\"original\"") + .header("content-range", "bytes 0-3/8") + .body(Body::from(bytes)) + .expect("origin response") + } + }), + )) + .await +} + +#[tokio::test] +async fn marker_policy_reaches_transport_bodyless_redirect_and_batch_paths() { + LocalSet::new() + .run_until(async { + let origin = start_origin(Router::new().route( + "/{status}", + get(|Path(status): Path, headers: HeaderMap| async move { + let mut response = Response::builder() + .status(status) + .header("location", "/unreachable"); + if headers.contains_key("x-fixture-upstream-marker") { + response = response.header(PROXY_HEADER, "upstream"); + } + response.body(Body::empty()).expect("origin response") + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")) + .with_proxy_marker(false); + for (method, status) in [ + (Method::GET, StatusCode::OK), + (Method::GET, StatusCode::NO_CONTENT), + (Method::HEAD, StatusCode::OK), + (Method::GET, StatusCode::NOT_MODIFIED), + (Method::GET, StatusCode::FOUND), + ] { + for override_setting in [ + OutboundProxyMarker::Inherit, + OutboundProxyMarker::On, + OutboundProxyMarker::Off, + ] { + for upstream in [false, true] { + let mut request = OutboundRequest::new( + method.clone(), + format!("{origin}/{}", status.as_u16()) + .parse() + .expect("target"), + ) + .expect("request") + .proxy_marker(override_setting) + .max_encoded_response_bytes(0) + .max_decoded_response_bytes(0) + .max_response_bytes(0); + if upstream { + request = request + .header("x-fixture-upstream-marker", "yes") + .expect("header"); + } + let enabled = client.effective_proxy_marker_enabled(&request); + let response = client.send(request).await.expect("response"); + assert_eq!(response.status(), status); + let expected = if enabled { + Some("axum") + } else if upstream { + Some("upstream") + } else { + None + }; + assert_eq!( + response + .headers() + .get(PROXY_HEADER) + .and_then(|value| value.to_str().ok()), + expected + ); + } + } + } + let requests = [OutboundProxyMarker::Inherit, OutboundProxyMarker::On] + .into_iter() + .map(|setting| { + OutboundRequest::get(format!("{origin}/200")) + .expect("request") + .proxy_marker(setting) + }) + .collect(); + let results = client + .send_all_until(requests, Deadline::after(Duration::from_secs(2))) + .await + .expect("batch"); + for (slot, expected) in results.into_parts().0.into_iter().zip([None, Some("axum")]) { + let response = take_resolved_slot(slot).outcome.expect("response"); + assert_eq!( + response + .headers() + .get(PROXY_HEADER) + .and_then(|value| value.to_str().ok()), + expected + ); + } + }) + .await; +} + +#[tokio::test] +async fn redirect_response_is_not_followed() { + let origin = start_origin( + Router::new() + .route("/target", get(|| async { "followed" })) + .route( + "/redirect", + get(|| async { (StatusCode::FOUND, [("location", "/target")]) }), + ), + ) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let request = OutboundRequest::get(format!("{origin}/redirect")).expect("request"); + + let response = client.send(request).await.expect("response"); + + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!( + response + .headers() + .get(PROXY_HEADER) + .and_then(|value| value.to_str().ok()), + Some("axum") + ); +} + +#[tokio::test] +async fn batch_collection_reports_per_slot_elapsed() { + LocalSet::new() + .run_until(async { + let origin = start_origin(Router::new().route("/", get(|| async { "ok" }))).await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let requests = vec![ + OutboundRequest::get(format!("{origin}/")).expect("reachable request"), + OutboundRequest::new( + Method::GET, + "http://127.0.0.1:1/unreachable" + .parse() + .expect("unreachable URI"), + ) + .expect("unreachable request"), + ]; + + let results = client + .start_batch_until(requests, Deadline::after(Duration::from_secs(1))) + .collect() + .await + .expect("valid batch driver"); + + assert_eq!(results.slots().len(), 2); + assert_eq!(results.termination(), OutboundBatchTermination::Completed); + resolved_slot(&results.slots()[0]) + .outcome + .as_ref() + .expect("reachable slot succeeds"); + assert!(matches!( + resolved_slot(&results.slots()[1]).outcome, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Unreachable, + .. + }) + )); + }) + .await; +} + +#[tokio::test] +async fn batch_preflight_precedence_and_indices() { + LocalSet::new().run_until(async { + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let streamed_upload = OutboundRequest::post("https://example.com/upload") + .expect("upload request") + .body(CoreBody::stream(stream::iter([bytes::Bytes::from_static( + b"body", + )]))); + let streamed_response = OutboundRequest::get("https://example.com/stream") + .expect("stream request") + .stream_response(); + let method_error = OutboundRequest::get("https://example.com/get") + .expect("GET request") + .body(CoreBody::stream(stream::iter([bytes::Bytes::new()]))); + + let results = client + .start_batch_until( + vec![streamed_upload, streamed_response, method_error], + Deadline::after(Duration::from_secs(1)), + ) + .collect() + .await + .expect("valid batch driver"); + + assert_eq!(results.termination(), OutboundBatchTermination::Completed); + + let messages: Vec<_> = results + .slots() + .iter() + .map( + |slot| match &resolved_slot(slot).outcome { + Err(EdgeError::BadRequest { message, .. }) => message.as_str(), + other => panic!("expected preflight rejection, got {other:?}"), + }, + ) + .collect(); + assert_eq!( + messages, + [ + "outbound batches require buffered request bodies; use send for a streamed upload", + "outbound batches require buffered responses; use send for a streamed response", + "outbound batches require buffered request bodies", + ] + ); + + }).await; +} + +#[tokio::test] +async fn oversized_uploads_never_contact_origin_and_upstream_413_remains_a_response() { + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + let calls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&calls); + let origin = start_origin(Router::new().route( + "/upload", + post(move || { + let observed_calls = Arc::clone(&observed); + async move { + observed_calls.fetch_add(1, Ordering::SeqCst); + Response::builder() + .status(StatusCode::PAYLOAD_TOO_LARGE) + .header("retry-after", "120") + .body(Body::empty()) + .expect("upstream response") + } + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + for streamed in [false, true] { + let bytes = Bytes::from_static(b"123456789"); + let body = if streamed { + CoreBody::stream(stream::iter([bytes])) + } else { + CoreBody::from(bytes) + }; + let request = OutboundRequest::post(format!("{origin}/upload")) + .expect("request") + .body(body) + .max_request_body_bytes(8); + let error = client.send(request).await.expect_err("local overflow"); + assert_eq!( + error.request_body_overflow_origin(), + Some(edgezero_core::RequestBodyOverflowOrigin::OutboundUpload) + ); + assert_eq!(calls.load(Ordering::SeqCst), 0); + } + let request = OutboundRequest::post(format!("{origin}/upload")) + .expect("request") + .body("12345678") + .max_request_body_bytes(8); + let response = client.send(request).await.expect("ordinary upstream 413"); + assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!( + response + .headers() + .get("retry-after") + .expect("upstream retry"), + "120" + ); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + +#[tokio::test] +async fn buffered_response_deadline_covers_body_completion() { + let origin = start_origin(Router::new().route( + "/", + get(|| async { + let body = async_body_stream! { + yield Ok::<_, Infallible>(Bytes::from_static(b"first")); + sleep(Duration::from_millis(100)).await; + yield Ok::<_, Infallible>(Bytes::from_static(b"second")); + }; + Response::new(Body::from_stream(body)) + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let request = OutboundRequest::get(format!("{origin}/")) + .expect("request") + .timeout(Duration::from_millis(10)); + + let error = client + .send(request) + .await + .expect_err("body completion must share the absolute request deadline"); + + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); +} + +#[tokio::test] +async fn batch_preserves_request_deadline_provenance() { + LocalSet::new() + .run_until(async { + let origin = start_origin(Router::new().route( + "/", + get(|| async { + sleep(Duration::from_millis(100)).await; + "late" + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let request = OutboundRequest::get(format!("{origin}/")) + .expect("request") + .deadline(Deadline::after(Duration::from_millis(10))); + + let results = client + .start_batch_until(vec![request], Deadline::after(Duration::from_secs(1))) + .collect() + .await + .expect("valid batch driver"); + + assert_eq!(results.termination(), OutboundBatchTermination::Completed); + + assert!(matches!( + resolved_slot(&results.slots()[0]).outcome, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + }) + .await; +} + +#[tokio::test] +async fn batch_yields_complete_exchanges_in_completion_order() { + LocalSet::new() + .run_until(async { + let origin = start_origin( + Router::new() + .route( + "/slow", + get(|| async { + sleep(Duration::from_millis(75)).await; + "slow" + }), + ) + .route("/fast", get(|| async { "fast" })), + ) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let requests = vec![ + OutboundRequest::get(format!("{origin}/slow")).expect("slow request"), + OutboundRequest::get(format!("{origin}/fast")).expect("fast request"), + ]; + let mut batch = + client.start_batch_until(requests, Deadline::after(Duration::from_secs(1))); + + let OutboundBatchNext::Item(first) = batch.next().await.expect("valid batch driver") + else { + panic!("expected first completion"); + }; + let OutboundBatchNext::Item(second) = batch.next().await.expect("valid batch driver") + else { + panic!("expected second completion"); + }; + + assert_eq!(first.index(), 1); + assert_eq!(second.index(), 0); + assert!(matches!( + batch.next().await.expect("valid batch driver"), + OutboundBatchNext::Finished(OutboundBatchTermination::Completed) + )); + }) + .await; +} + +#[tokio::test(start_paused = true)] +async fn early_observation_cutoff_preserves_completed_slots_and_leaves_pending_none() { + LocalSet::new() + .run_until(async { + use edgezero_core::MonotonicInstant; + use futures_util::future::poll_fn; + + let slow_started = Arc::new(Notify::new()); + let release_slow = Arc::new(Notify::new()); + let started = Arc::clone(&slow_started); + let release = Arc::clone(&release_slow); + let origin = start_origin( + Router::new() + .route("/fast", get(|| async { "fast" })) + .route( + "/slow", + get(move || { + let request_started = Arc::clone(&started); + let response_release = Arc::clone(&release); + async move { + request_started.notify_one(); + response_release.notified().await; + "slow" + } + }), + ), + ) + .await; + let now = Arc::new(Mutex::new(MonotonicInstant::now())); + let sampled = Arc::clone(&now); + let clock = MonotonicClock::new(move || *sampled.lock().expect("fake clock")); + let observation_cutoff = clock.deadline_after(Duration::from_millis(25)); + let clock_advanced = Arc::new(Notify::new()); + let timer_wakes = Arc::new(AtomicUsize::new(0)); + let time = + paired_cutoff_time(clock, Arc::clone(&clock_advanced), Arc::clone(&timer_wakes)); + let client = + HttpClient::with_client(AxumOutboundClient::try_with_time(time).expect("client")); + let mut batch = client.start_batch_until( + vec![ + OutboundRequest::get(format!("{origin}/fast")).expect("fast request"), + OutboundRequest::get(format!("{origin}/slow")).expect("slow request"), + ], + observation_cutoff, + ); + let mut accumulator = batch.accumulator(); + let collected = async move { + let OutboundBatchNext::Item(item) = batch.next().await? else { + panic!("expected fast completion"); + }; + assert_eq!(item.index(), 0); + assert_eq!( + item.result() + .outcome + .as_ref() + .expect("fast response") + .status(), + StatusCode::OK + ); + accumulator.insert(item).expect("matching receipt"); + slow_started.notified().await; + *now.lock().expect("fake clock") = observation_cutoff.instant(); + clock_advanced.notify_waiters(); + let result = batch + .collect() + .await + .map_err(|failure| failure.into_parts().0)?; + accumulator + .merge_success(result) + .map_err(|failure| failure.into_parts().0) + }; + futures_util::pin_mut!(collected); + let containment_started = Instant::now(); + let results = poll_fn(|context| { + assert!( + containment_started.elapsed() < Duration::from_secs(5), + "batch collection exceeded the real-clock containment watchdog" + ); + let result = collected.as_mut().poll(context); + // Keep paused Tokio time from auto-advancing while loopback I/O is pending. + if result.is_pending() { + context.waker().wake_by_ref(); + } + result + }) + .await + .expect("valid batch driver"); + + release_slow.notify_one(); + assert!( + timer_wakes.load(Ordering::SeqCst) > 0, + "advancing the fake clock must wake the paired timer" + ); + assert_eq!(results.termination(), OutboundBatchTermination::Cutoff); + assert!(matches!( + results.slots()[0], + OutboundCollectedSlot::Resolved(_) + )); + assert!(matches!( + results.slots()[1], + OutboundCollectedSlot::Unresolved + )); + }) + .await; +} + +#[tokio::test] +async fn authority_override_changes_host_only_and_cache_bypass_adds_no_origin_header() { + let origin = start_origin(Router::new().route( + "/", + get(|headers: HeaderMap| async move { + let host = headers + .get(HOST) + .and_then(|value| value.to_str().ok()) + .unwrap_or("missing"); + let cache_control = headers + .get(CACHE_CONTROL) + .and_then(|value| value.to_str().ok()) + .unwrap_or("absent"); + format!("{host}|{cache_control}") + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let request = OutboundRequest::get(format!("{origin}/")) + .expect("request") + .host_authority_override("virtual.example:8443") + .expect("authority override") + .cache_policy(OutboundCachePolicy::Bypass); + + let body = client + .send(request) + .await + .expect("response") + .into_bytes_bounded(1_024) + .await + .expect("response body"); + + assert_eq!(body, "virtual.example:8443|absent"); +} + +#[tokio::test] +async fn streamed_response_deadline_preserves_timeout_provenance() { + let origin = start_origin(Router::new().route( + "/", + get(|| async { + let body = async_body_stream! { + sleep(Duration::from_secs(2)).await; + yield Ok::<_, Infallible>(Bytes::from_static(b"late")); + }; + Response::new(Body::from_stream(body)) + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let request = OutboundRequest::get(format!("{origin}/")) + .expect("request") + .stream_response() + .timeout(Duration::from_millis(500)); + + let response = client.send(request).await.expect("response headers"); + let error = response + .into_body() + .into_bytes_bounded(1_024) + .await + .expect_err("stream body must retain the request deadline"); + + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); +} + +#[tokio::test] +async fn encoded_and_decoded_limits_report_independent_origins() { + let decoded = vec![b'x'; 4_096]; + let encoded = gzip(&decoded); + let origin_body = encoded.clone(); + let origin = start_origin(Router::new().route( + "/", + get(move || { + let bytes = origin_body.clone(); + async move { + Response::builder() + .header(CONTENT_ENCODING, "gzip") + .body(Body::from(bytes)) + .expect("origin response") + } + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + + let decoded_error = client + .send( + OutboundRequest::get(format!("{origin}/")) + .expect("decoded-limit request") + .max_encoded_response_bytes(u64::try_from(encoded.len()).expect("encoded length")) + .max_decoded_response_bytes(128), + ) + .await + .expect_err("decoded response must exceed its independent limit"); + assert!(matches!( + decoded_error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::DecodedBody, + .. + } + )); + + let encoded_error = client + .send( + OutboundRequest::get(format!("{origin}/")) + .expect("encoded-limit request") + .max_encoded_response_bytes( + u64::try_from(encoded.len()) + .expect("encoded length") + .saturating_sub(1), + ) + .max_decoded_response_bytes(u64::try_from(decoded.len()).expect("decoded length")), + ) + .await + .expect_err("encoded response must exceed its independent limit"); + assert!(matches!( + encoded_error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::EncodedBody, + .. + } + )); +} + +#[tokio::test] +async fn deflate_response_is_decoded_stripped_and_bounded() { + let decoded = vec![b'd'; 4_096]; + let encoded = zlib(&decoded); + let origin_body = encoded.clone(); + let origin = start_origin(Router::new().route( + "/", + get(move || { + let bytes = origin_body.clone(); + async move { + Response::builder() + .header(CONTENT_ENCODING, "deflate") + .header(CONTENT_LENGTH, bytes.len()) + .body(Body::from(bytes)) + .expect("origin response") + } + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + + let response = client + .send( + OutboundRequest::get(format!("{origin}/")) + .expect("request") + .max_decoder_bytes(FLATE_DECODER_FIXED_CHARGE_BYTES) + .max_encoded_response_bytes(u64::try_from(encoded.len()).expect("encoded length")) + .max_decoded_response_bytes(u64::try_from(decoded.len()).expect("decoded length")), + ) + .await + .expect("decoded response"); + assert!(response.headers().get(CONTENT_ENCODING).is_none()); + assert!(response.headers().get(CONTENT_LENGTH).is_none()); + assert_eq!( + response + .into_bytes_bounded(u64::try_from(decoded.len()).expect("decoded length")) + .await + .expect("decoded body"), + decoded + ); + + let decoded_error = client + .send( + OutboundRequest::get(format!("{origin}/")) + .expect("request") + .max_decoded_response_bytes(128), + ) + .await + .expect_err("decoded response must exceed its limit"); + assert!(matches!( + decoded_error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::DecodedBody, + .. + } + )); +} + +#[tokio::test] +async fn original_chunked_response_obeys_observed_buffer_cap() { + let origin = start_origin(Router::new().route( + "/", + get(|| async { + Response::builder() + .header(CONTENT_ENCODING, "gzip") + .body(Body::from_stream(stream::iter([Ok::<_, Infallible>( + Bytes::from_static(b"raw!"), + )]))) + .expect("chunked origin response") + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let error = client + .send( + OutboundRequest::get(origin) + .expect("request") + .response_decoding(ResponseDecoding::Original) + .max_decoder_bytes(0) + .max_decoded_response_bytes(1) + .max_encoded_response_bytes(20) + .max_response_bytes(3), + ) + .await + .expect_err("observed final buffer overflow"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::BufferedBody, + .. + } + )); +} + +#[tokio::test] +async fn original_and_partial_coding_survive_actual_transport_and_mixed_batch() { + LocalSet::new() + .run_until(async { + let plain = b"original compressed representation"; + for (coding, encoded) in [("gzip", gzip(plain)), ("deflate", zlib(plain))] { + let origin = start_coding_origin(coding, encoded.clone()).await; + let client = + HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let length = u64::try_from(encoded.len()).expect("encoded length"); + for (choice, status) in [ + (ResponseDecoding::Original, 200_u16), + (ResponseDecoding::Original, 206), + (ResponseDecoding::Decode, 206), + ] { + for streamed in [false, true] { + let request = OutboundRequest::get(format!("{origin}/{status}")) + .expect("request") + .header("accept-encoding", "gzip, deflate") + .expect("header") + .response_decoding(choice) + .max_decoder_bytes(0) + .max_decoded_response_bytes(1) + .max_encoded_response_bytes(length) + .max_response_bytes(length); + let outgoing = if streamed { + request.stream_response() + } else { + request + }; + let response = client.send(outgoing).await.expect("raw response"); + assert_eq!( + response.headers().get(CONTENT_ENCODING).expect("coding"), + coding + ); + assert_eq!( + response + .headers() + .get(CONTENT_LENGTH) + .expect("length") + .as_bytes(), + length.to_string().as_bytes() + ); + assert_eq!( + response.headers().get("etag").expect("etag"), + "\"original\"" + ); + assert_eq!( + response.headers().get("content-range").expect("range"), + "bytes 0-3/8" + ); + assert_eq!( + response + .into_bytes_bounded(length) + .await + .expect("raw bytes"), + encoded + ); + } + } + let requests = [ResponseDecoding::Original, ResponseDecoding::Decode] + .into_iter() + .map(|choice| { + OutboundRequest::get(format!("{origin}/200")) + .expect("request") + .header("accept-encoding", "gzip, deflate") + .expect("header") + .response_decoding(choice) + }) + .collect(); + let results = client + .start_batch_until(requests, Deadline::after(Duration::from_secs(2))) + .collect() + .await + .expect("batch"); + assert_eq!(results.termination(), OutboundBatchTermination::Completed); + for (index, slot) in results.into_parts().0.into_iter().enumerate() { + let response = take_resolved_slot(slot).outcome.expect("response"); + let expected = if index == 0 { + encoded.as_slice() + } else { + plain.as_slice() + }; + assert_eq!( + response.into_bytes_bounded(256).await.expect("bytes"), + expected + ); + } + } + }) + .await; +} + +#[tokio::test] +async fn passthrough_coding_is_not_charged_to_decoded_limit() { + let origin = start_origin(Router::new().route( + "/", + get(|| async { + Response::builder() + .header(CONTENT_ENCODING, "zstd") + .body(Body::from("opaque")) + .expect("origin response") + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + let request = OutboundRequest::get(format!("{origin}/")) + .expect("request") + .max_encoded_response_bytes(6) + .max_decoded_response_bytes(1); + + let response = client.send(request).await.expect("response"); + let bytes = response.into_bytes_bounded(6).await.expect("response body"); + + assert_eq!(bytes, "opaque"); +} + +#[tokio::test] +async fn repeated_response_headers_are_preserved() { + let origin = start_origin(Router::new().route( + "/", + get(|| async { + let mut response = Response::new(Body::empty()); + response + .headers_mut() + .append(SET_COOKIE, HeaderValue::from_static("a=1")); + response + .headers_mut() + .append(SET_COOKIE, HeaderValue::from_static("b=2")); + response + }), + )) + .await; + let client = HttpClient::with_client(AxumOutboundClient::try_new().expect("client")); + + let response = client + .send(OutboundRequest::get(format!("{origin}/")).expect("request")) + .await + .expect("response"); + let values: Vec<_> = response + .headers() + .get_all(SET_COOKIE) + .iter() + .map(|value| value.to_str().expect("header value")) + .collect(); + + assert_eq!(values, ["a=1", "b=2"]); +} diff --git a/crates/edgezero-adapter-cloudflare/.cargo/config.toml b/crates/edgezero-adapter-cloudflare/.cargo/config.toml index 6d90049f0..e33593026 100644 --- a/crates/edgezero-adapter-cloudflare/.cargo/config.toml +++ b/crates/edgezero-adapter-cloudflare/.cargo/config.toml @@ -1,7 +1,5 @@ [build] -target = "wasm32-wasip1" +target = "wasm32-unknown-unknown" -[target.'cfg(target_arch = "wasm32")'] -# Use Viceroy to run wasm built from binaries in this crate or dependents. -# Points to the demo service config by default. -runner = "wasmtime run --dir=. -- " +[target.wasm32-unknown-unknown] +runner = "wasm-bindgen-test-runner" diff --git a/crates/edgezero-adapter-cloudflare/Cargo.toml b/crates/edgezero-adapter-cloudflare/Cargo.toml index 5dbb80fc8..e127424f6 100644 --- a/crates/edgezero-adapter-cloudflare/Cargo.toml +++ b/crates/edgezero-adapter-cloudflare/Cargo.toml @@ -12,7 +12,7 @@ workspace = true [features] default = [] -cloudflare = ["dep:worker", "dep:serde_json"] +cloudflare = ["dep:async-stream", "dep:worker", "dep:serde_json", "dep:web-sys", "dep:wasm-streams"] cli = [ "dep:edgezero-adapter", "edgezero-adapter/cli", @@ -22,6 +22,7 @@ cli = [ "dep:toml_edit", "dep:walkdir", ] +test-utils = [] [dependencies] anyhow = { workspace = true } @@ -30,6 +31,7 @@ edgezero-adapter = { path = "../edgezero-adapter", optional = true, features = [ "cli", ] } async-trait = { workspace = true } +async-stream = { workspace = true, optional = true } brotli = { workspace = true } bytes = { workspace = true } flate2 = { workspace = true } @@ -40,18 +42,18 @@ ctor = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } tempfile = { workspace = true, optional = true } toml_edit = { workspace = true, optional = true } -worker = { version = "0.8", default-features = false, features = ["http"], optional = true } +worker = { workspace = true, optional = true } +wasm-streams = { workspace = true, optional = true } walkdir = { workspace = true, optional = true } +web-sys = { version = "0.3", optional = true, features = [ + "AbortSignal", + "ReadableStream", + "TransformStream", + "WritableStream", + "WritableStreamDefaultWriter", +] } [dev-dependencies] edgezero-core = { path = "../edgezero-core", features = ["test-utils"] } tempfile = { workspace = true } wasm-bindgen-test = "0.3" -web-sys = { version = "0.3", features = [ - "Window", - "Response", - "Request", - "Headers", - "ReadableStream", - "Blob", -] } diff --git a/crates/edgezero-adapter-cloudflare/src/cli.rs b/crates/edgezero-adapter-cloudflare/src/cli.rs index 5d93ee3d1..6cef47bd0 100644 --- a/crates/edgezero-adapter-cloudflare/src/cli.rs +++ b/crates/edgezero-adapter-cloudflare/src/cli.rs @@ -10,13 +10,14 @@ use edgezero_adapter::cli_support::{ find_manifest_upwards, find_workspace_root, path_distance, read_package_name, run_native_cli, }; use edgezero_adapter::registry::{ - Adapter, AdapterAction, AdapterPushContext, ProvisionStores, ReadConfigEntry, ResolvedStoreId, - register_adapter, + Adapter, AdapterAction, AdapterExecutionTarget, AdapterPushContext, ProvisionStores, + ReadConfigEntry, ResolvedStoreId, register_adapter, }; use edgezero_adapter::scaffold::{ AdapterBlueprint, AdapterFileSpec, CommandTemplates, DependencySpec, LoggingDefaults, ManifestSpec, ReadmeInfo, TemplateRegistration, register_adapter_blueprint, }; +use edgezero_core::{Capability, CapabilitySupport, PlatformMetadata}; use walkdir::WalkDir; static CLOUDFLARE_ADAPTER: CloudflareCliAdapter = CloudflareCliAdapter; @@ -134,6 +135,38 @@ struct CloudflareCliAdapter; reason = "cloudflare has no validate_app_config_keys / validate_adapter_manifest / validate_typed_secrets requirements; those three trait defaults are intentionally inherited. `read_config_entry` and `read_config_entry_local` are both overridden below (wrangler kv key get --remote / --local). `single_store_kinds` IS overridden below (returns `&[\"secrets\"]`)." )] impl Adapter for CloudflareCliAdapter { + fn capability(&self, capability: Capability) -> CapabilitySupport { + match capability { + Capability::BufferedResponseContentLength + | Capability::ConfigReadDeadlines + | Capability::InboundReadDeadlines + | Capability::IngressAdmissionAbort + | Capability::OutboundAuthorityOverride + | Capability::OutboundBatchCancellation + | Capability::OutboundHeaderFidelity + | Capability::OutboundDeadlines + | Capability::OutboundFailureDiagnostics + | Capability::ResponseEgressAbort + | Capability::ResponseEgressBackpressure + | Capability::ResponseEgressCompletion + | Capability::ResponseWriteDeadlines + | Capability::StreamedUploadDeadlines => CapabilitySupport::BestEffort, + Capability::IngressAdmission + | Capability::LazyStreamedResponsePassthrough + | Capability::OutboundBatchCompletionOrder + | Capability::OutboundBatchSlotIsolation + | Capability::OutboundCacheBypass + | Capability::OutboundFlexiblePhaseBudget + | Capability::OutboundHttp + | Capability::OutboundRedirectFollowing => CapabilitySupport::Native, + Capability::ConfigReadAllocationBounds + | Capability::OutboundCompleteResourceAccounting + | Capability::RawIngressFramingValidation + | Capability::RawIngressHeadLimits + | _ => CapabilitySupport::Unsupported, + } + } + fn execute(&self, action: AdapterAction, args: &[String]) -> Result<(), String> { match action { // `wrangler` is the native sign-in surface for Cloudflare @@ -167,6 +200,30 @@ impl Adapter for CloudflareCliAdapter { } } + fn execute_target( + &self, + action: AdapterAction, + target: &AdapterExecutionTarget, + args: &[String], + ) -> Result<(), String> { + let manifest = target_manifest(target)?; + match action { + AdapterAction::Build => build_from_manifest(&manifest, args).map(|_artifact| ()), + AdapterAction::Deploy => deploy_from_manifest(&manifest, args), + AdapterAction::Serve => serve_from_manifest(&manifest, args), + AdapterAction::AuthLogin + | AdapterAction::AuthLogout + | AdapterAction::AuthStatus + | AdapterAction::DeployStaging + | AdapterAction::EmitVersion + | AdapterAction::Healthcheck + | AdapterAction::Rollback + | _ => Err(format!( + "cloudflare adapter does not support pinned target action {action:?}" + )), + } + } + fn merged_id_kinds(&self) -> &'static [&'static str] { // Both KV and Config back to Worker KV namespaces via the // same `[[kv_namespaces]] binding = ` @@ -189,6 +246,10 @@ impl Adapter for CloudflareCliAdapter { "cloudflare" } + fn platform_metadata(&self) -> PlatformMetadata { + crate::CLOUDFLARE_PLATFORM + } + fn provision( &self, manifest_root: &Path, @@ -929,6 +990,10 @@ fn read_wrangler_kv_key( pub fn build(extra_args: &[String]) -> Result { let manifest = find_wrangler_manifest(env::current_dir().map_err(|err| err.to_string())?.as_path())?; + build_from_manifest(&manifest, extra_args) +} + +fn build_from_manifest(manifest: &Path, extra_args: &[String]) -> Result { let manifest_dir = manifest .parent() .ok_or_else(|| "wrangler manifest has no parent directory".to_owned())?; @@ -971,6 +1036,10 @@ pub fn build(extra_args: &[String]) -> Result { pub fn deploy(extra_args: &[String]) -> Result<(), String> { let manifest = find_wrangler_manifest(env::current_dir().map_err(|err| err.to_string())?.as_path())?; + deploy_from_manifest(&manifest, extra_args) +} + +fn deploy_from_manifest(manifest: &Path, extra_args: &[String]) -> Result<(), String> { let manifest_dir = manifest .parent() .ok_or_else(|| "wrangler manifest has no parent directory".to_owned())?; @@ -1112,6 +1181,10 @@ fn register_ctor() { pub fn serve(extra_args: &[String]) -> Result<(), String> { let manifest = find_wrangler_manifest(env::current_dir().map_err(|err| err.to_string())?.as_path())?; + serve_from_manifest(&manifest, extra_args) +} + +fn serve_from_manifest(manifest: &Path, extra_args: &[String]) -> Result<(), String> { let manifest_dir = manifest .parent() .ok_or_else(|| "wrangler manifest has no parent directory".to_owned())?; @@ -1132,9 +1205,24 @@ pub fn serve(extra_args: &[String]) -> Result<(), String> { Ok(()) } +fn target_manifest(target: &AdapterExecutionTarget) -> Result { + let manifest = target.platform_manifest().map_or_else( + || target.app_root().join("wrangler.toml"), + Path::to_path_buf, + ); + if !manifest.is_file() { + return Err(format!( + "pinned Cloudflare manifest {} is not a regular file", + manifest.display() + )); + } + Ok(manifest) +} + #[cfg(test)] mod tests { use super::*; + #[cfg(unix)] use edgezero_core::test_env::PathPrepend; @@ -1154,6 +1242,177 @@ mod tests { const TEST_CONFIG_ID: &str = "app_config"; const TEST_SECRET_ID: &str = "default"; + #[test] + fn bounded_config_scaffolds_require_gc_cleanup_support() { + for source in [ + include_str!("templates/wrangler.toml.hbs"), + include_str!( + "../../../examples/app-demo/crates/app-demo-adapter-cloudflare/wrangler.toml" + ), + include_str!( + "../../../tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml" + ), + ] { + let document = source + .parse::() + .expect("Wrangler TOML"); + let flags = document + .get("compatibility_flags") + .and_then(toml_edit::Item::as_array) + .expect("explicit compatibility flags"); + assert!( + flags + .iter() + .any(|value| value.as_str() == Some("enable_weak_ref")) + ); + } + } + + #[test] + #[expect( + clippy::too_many_lines, + reason = "keep the complete capability data table together" + )] + fn adapter_capability_matrix_matches_contracts() { + for (capability, support) in [ + ( + Capability::ConfigReadAllocationBounds, + CapabilitySupport::Unsupported, + ), + ( + Capability::ConfigReadDeadlines, + CapabilitySupport::BestEffort, + ), + ( + Capability::InboundReadDeadlines, + CapabilitySupport::BestEffort, + ), + (Capability::IngressAdmission, CapabilitySupport::Native), + ( + Capability::IngressAdmissionAbort, + CapabilitySupport::BestEffort, + ), + ( + Capability::RawIngressFramingValidation, + CapabilitySupport::Unsupported, + ), + ( + Capability::RawIngressHeadLimits, + CapabilitySupport::Unsupported, + ), + ( + Capability::ResponseEgressAbort, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseEgressBackpressure, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseEgressCompletion, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseWriteDeadlines, + CapabilitySupport::BestEffort, + ), + ( + Capability::BufferedResponseContentLength, + CapabilitySupport::BestEffort, + ), + (Capability::OutboundHttp, CapabilitySupport::Native), + ( + Capability::OutboundRedirectFollowing, + CapabilitySupport::Native, + ), + ( + Capability::OutboundCompleteResourceAccounting, + CapabilitySupport::Unsupported, + ), + ( + Capability::OutboundHeaderFidelity, + CapabilitySupport::BestEffort, + ), + (Capability::OutboundDeadlines, CapabilitySupport::BestEffort), + ( + Capability::OutboundFailureDiagnostics, + CapabilitySupport::BestEffort, + ), + ( + Capability::OutboundFlexiblePhaseBudget, + CapabilitySupport::Native, + ), + ( + Capability::OutboundAuthorityOverride, + CapabilitySupport::BestEffort, + ), + ( + Capability::OutboundBatchCancellation, + CapabilitySupport::BestEffort, + ), + ( + Capability::OutboundBatchCompletionOrder, + CapabilitySupport::Native, + ), + ( + Capability::OutboundBatchSlotIsolation, + CapabilitySupport::Native, + ), + (Capability::OutboundCacheBypass, CapabilitySupport::Native), + ( + Capability::OutboundImageOptimizer, + CapabilitySupport::Unsupported, + ), + ( + Capability::StreamedUploadDeadlines, + CapabilitySupport::BestEffort, + ), + ( + Capability::LazyStreamedResponsePassthrough, + CapabilitySupport::Native, + ), + ] { + assert_eq!( + CLOUDFLARE_ADAPTER.capability(capability), + support, + "{capability:?}" + ); + } + } + + #[test] + fn adapter_platform_metadata_matches_runtime_metadata() { + let metadata = CLOUDFLARE_ADAPTER.platform_metadata(); + let source = edgezero_core::PlatformResourceSource::PlatformLimit { + provider: "Cloudflare Workers", + }; + + assert_eq!(metadata, crate::CLOUDFLARE_PLATFORM); + assert_eq!( + metadata.memory_ceiling(), + edgezero_core::PlatformFact::known( + edgezero_core::MemoryCeiling::new( + 128_000_000, + edgezero_core::MemoryCeilingScope::PerInstance, + None, + ), + source, + ) + ); + assert_eq!( + metadata.inbound_request_population_bound(), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::ProviderUnpublished + ) + ); + assert_eq!( + metadata.host_ingress_memory_accounting(), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::ProviderUnpublished + ) + ); + } + // ---------- extract_namespace_id ---------- #[test] diff --git a/crates/edgezero-adapter-cloudflare/src/config_store.rs b/crates/edgezero-adapter-cloudflare/src/config_store.rs index 6fea0ae7c..f5dff5646 100644 --- a/crates/edgezero-adapter-cloudflare/src/config_store.rs +++ b/crates/edgezero-adapter-cloudflare/src/config_store.rs @@ -2,7 +2,8 @@ //! //! Each declared config id maps to its own Cloudflare KV namespace binding, //! resolved at request time from `EDGEZERO__STORES__CONFIG____NAME`. -//! Reads are async (`worker::kv::KvStore::get(key).text().await`). +//! Bounded reads drain a byte stream into caller-sized buffers; raw `get` is +//! explicitly unbounded. Neither path certifies opaque host allocations. //! //! ```toml //! # wrangler.toml @@ -16,16 +17,32 @@ //! arbitrary dotted keys had to be JSON-packed inside one variable. The KV //! backing has no such restriction. +#![cfg_attr( + all(feature = "cloudflare", target_arch = "wasm32"), + expect( + clippy::self_named_module_files, + reason = "the workspace denies both contradictory module-file conventions; retain its self-named-file convention" + ) +)] + +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +mod kv; + +use std::future::Future; +#[cfg(test)] +use std::future::{pending, ready}; +use std::time::Duration; + use async_trait::async_trait; -use edgezero_core::config_store::{ConfigStore, ConfigStoreError}; +use edgezero_core::config_store::{ + BoundedStoreRead, ConfigStore, ConfigStoreError, finish_bounded_config_read, +}; +use edgezero_core::time::{Deadline, MonotonicClock}; +use futures_util::future::{Either, select}; #[cfg(test)] use std::collections::HashMap; -#[cfg(not(any(all(feature = "cloudflare", target_arch = "wasm32"), test)))] -use std::convert::Infallible; -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] -use worker::Env; #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] -use worker::kv::KvStore as WorkerKvStore; +use worker::{Delay, Env}; /// Config store backed by a Cloudflare KV namespace. /// @@ -39,10 +56,7 @@ enum CloudflareConfigBackend { #[cfg(test)] InMemory(HashMap), #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] - Kv(WorkerKvStore), - /// Never constructed; keeps the enum inhabited off production/test cfgs. - #[cfg(not(any(all(feature = "cloudflare", target_arch = "wasm32"), test)))] - _Uninhabited(Infallible), + Kv(kv::KvBinding), } impl CloudflareConfigStore { @@ -61,32 +75,152 @@ impl CloudflareConfigStore { #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] #[inline] pub fn from_env(env: &Env, binding_name: &str) -> Result { - let store = env.kv(binding_name).map_err(|err| { - ConfigStoreError::unavailable(format!( - "failed to open config KV binding '{binding_name}': {err}" - )) - })?; + let store = kv::KvBinding::from_this(env.as_ref(), binding_name)?; Ok(Self { inner: CloudflareConfigBackend::Kv(store), }) } + + #[cfg_attr( + not(all(feature = "cloudflare", target_arch = "wasm32")), + expect( + clippy::unused_async, + clippy::unused_async_trait_impl, + reason = "native fixture is synchronous; the production SDK read is asynchronous" + ) + )] + async fn materialized_get(&self, key: &str) -> Result, ConfigStoreError> { + match &self.inner { + #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] + CloudflareConfigBackend::Kv(store) => store.materialized_get(key).await, + #[cfg(test)] + CloudflareConfigBackend::InMemory(data) => Ok(data.get(key).cloned()), + } + } } #[async_trait(?Send)] impl ConfigStore for CloudflareConfigStore { #[inline] - async fn get(&self, key: &str) -> Result, ConfigStoreError> { + async fn get(&self, key: &str) -> Result, ConfigStoreError> { + self.materialized_get(key) + .await + .map(|value| value.map(Into::into)) + } + + #[inline] + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { match &self.inner { #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] - CloudflareConfigBackend::Kv(store) => store.get(key).text().await.map_err(|err| { - ConfigStoreError::internal(anyhow::anyhow!("kv config get failed: {err}")) - }), + CloudflareConfigBackend::Kv(store) => { + bounded_config_read( + store.streaming_get( + key, + max_backend_bytes.min(max_value_bytes), + clock, + deadline, + ), + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + .await + } #[cfg(test)] - CloudflareConfigBackend::InMemory(data) => Ok(data.get(key).cloned()), - #[cfg(not(any(all(feature = "cloudflare", target_arch = "wasm32"), test)))] - CloudflareConfigBackend::_Uninhabited(never) => { - let _: &str = key; - match *never {} + CloudflareConfigBackend::InMemory(_) => { + bounded_config_read( + self.materialized_get(key), + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + .await + } + } + } +} + +// The timer covers acquisition, draining and conversion. Reader drop requests +// cancellation, but finite host teardown is a separate deployed evidence gate. +async fn bounded_config_read( + read: F, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, +) -> Result, ConfigStoreError> +where + F: Future, ConfigStoreError>>, +{ + #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] + { + return bounded_config_read_with_timer( + read, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + Delay::from, + ) + .await; + } + #[cfg(not(all(feature = "cloudflare", target_arch = "wasm32")))] + { + bounded_config_read_with_timer( + read, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + |_| pending(), + ) + .await + } +} + +async fn bounded_config_read_with_timer( + read: F, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + mut make_timer: MakeTimer, +) -> Result, ConfigStoreError> +where + F: Future, ConfigStoreError>>, + MakeTimer: FnMut(Duration) -> Timer, + Timer: Future, +{ + futures_util::pin_mut!(read); + loop { + let Some(remaining) = deadline.remaining_at(clock.now()) else { + return Err(ConfigStoreError::DeadlineExceeded); + }; + let timer = make_timer(remaining); + futures_util::pin_mut!(timer); + match select(timer, read.as_mut()).await { + Either::Left(((), _read)) => { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + } + Either::Right((result, _timer)) => { + return finish_bounded_config_read( + result, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ); } } } @@ -94,7 +228,333 @@ impl ConfigStore for CloudflareConfigStore { #[cfg(test)] mod tests { + #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] + mod streaming { + use super::*; + use wasm_bindgen_test::wasm_bindgen_test; + use worker::js_sys::{Function, Number, Promise, Reflect, global}; + use worker::wasm_bindgen::JsCast as _; + use worker::wasm_bindgen::JsValue; + use worker::wasm_bindgen_futures::JsFuture; + + fn streaming_store(length: usize, stalled: bool) -> (CloudflareConfigStore, JsValue) { + // An actual KV binding double: text reads fail and the byte source only + // fills caller-owned BYOB views, without materializing a complete value. + let factory = Function::new_with_args( + "length, stalled", + " + // worker::Delay expects browser numeric handles, not Node objects. + if (!globalThis.edgezeroNumericTimers) { + globalThis.edgezeroNumericTimers = true; + const set = globalThis.setTimeout; + const clear = globalThis.clearTimeout; + const timers = new Map(); + let next = 0; + globalThis.setTimeout = (callback, delay, ...args) => { + const id = ++next; + timers.set(id, set(() => { timers.delete(id); callback(...args); }, delay)); + return id; + }; + globalThis.clearTimeout = id => { + clear(timers.get(id) ?? id); + timers.delete(id); + }; + } + const stats = { cancels: 0, gets: 0, pulls: 0, maxView: 0 }; + const kv = { + get: async (key, options) => { + stats.gets++; + if (options.type !== 'stream') throw new Error('text read forbidden'); + if (key === 'missing') return null; + if (key === 'bad-binding') return 'not-a-stream'; + if (key === 'acquisition-error') throw new Error('TOKEN-DO-NOT-LEAK'); + if (key === 'non-byte') return new ReadableStream({ + cancel() { stats.cancels++; return Promise.reject(new Error('TOKEN-DO-NOT-LEAK')); } + }); + let position = 0; + const stream = new ReadableStream({ + type: 'bytes', + pull(controller) { + stats.pulls++; + if (key === 'source-error') throw new Error('TOKEN-DO-NOT-LEAK'); + const view = controller.byobRequest.view; + stats.maxView = Math.max(stats.maxView, view.byteLength); + if (stalled) return new Promise(() => {}); + const count = Math.min(view.byteLength, length - position); + if (count === 0) { + controller.close(); + controller.byobRequest.respond(0); + } else { + view.fill(key === 'invalid-utf8' ? 255 : 97, 0, count); + position += count; + controller.byobRequest.respond(count); + } + }, + cancel() { + stats.cancels++; + if (key === 'reject-cancel') return Promise.reject(new Error('TOKEN-DO-NOT-LEAK')); + } + }, { highWaterMark: 0 }); + if (key === 'late') return new Promise(resolve => { stats.resolve = () => resolve(stream); }); + return stream; + }, + getWithMetadata() {}, put() {}, list() {}, delete() {} + }; + return { kv, stats }; + ", + ); + let js_length = u32::try_from(length).expect("fixture length fits JS integer"); + let binding = factory + .call2( + &JsValue::NULL, + &JsValue::from(js_length), + &JsValue::from(stalled), + ) + .expect("KV fixture"); + let stats = Reflect::get(&binding, &JsValue::from("stats")).expect("stats"); + let kv = kv::KvBinding::from_this(&binding, "kv").expect("KV binding"); + ( + CloudflareConfigStore { + inner: CloudflareConfigBackend::Kv(kv), + }, + stats, + ) + } + + fn counter(stats: &JsValue, field: &str) -> usize { + Number::from(Reflect::get(stats, &JsValue::from(field)).expect("counter")) + .to_string_with_radix(10) + .expect("decimal counter") + .as_string() + .expect("counter string") + .parse() + .expect("integer counter") + } + + #[wasm_bindgen_test] + async fn bounded_kv_requires_gc_cleanup_support_before_provider_dispatch() { + let (store, stats) = streaming_store(1, false); + let global_object = global(); + let property = JsValue::from("FinalizationRegistry"); + let previous = Reflect::get(&global_object, &property).expect("runtime feature"); + Reflect::set(&global_object, &property, &JsValue::UNDEFINED).expect("disable feature"); + let result = store + .get_bounded( + "value", + &MonotonicClock::default(), + Deadline::after(Duration::from_secs(1)), + 1, + 1, + ) + .await; + Reflect::set(&global_object, &property, &previous).expect("restore feature"); + assert!(matches!(result, Err(ConfigStoreError::Unavailable { .. }))); + assert_eq!(counter(&stats, "gets"), 0); + } + + #[wasm_bindgen_test] + async fn bounded_kv_failed_binding_and_cancellation_never_publish_provider_errors() { + for key in [ + "bad-binding", + "acquisition-error", + "non-byte", + "source-error", + "reject-cancel", + ] { + let (store, stats) = streaming_store(2, false); + let error = store + .get_bounded( + key, + &MonotonicClock::default(), + Deadline::after(Duration::from_secs(1)), + 1, + 1, + ) + .await + .expect_err("typed rejection"); + assert!(!error.to_string().contains("TOKEN-DO-NOT-LEAK")); + assert!(matches!( + error, + ConfigStoreError::Unavailable { .. } | ConfigStoreError::ValueTooLarge + )); + if matches!(key, "non-byte" | "reject-cancel") { + assert_eq!(counter(&stats, "cancels"), 1); + } + // Node's runner fails on unhandled promise rejection. + Delay::from(Duration::from_millis(10)).await; + } + } + + #[wasm_bindgen_test] + async fn bounded_kv_cancels_stream_arriving_after_acquisition_is_dropped() { + let (store, stats) = streaming_store(1, false); + let clock = MonotonicClock::default(); + let mut read = Box::pin(store.get_bounded( + "late", + &clock, + Deadline::after(Duration::from_secs(1)), + 1, + 1, + )); + assert!(futures_util::poll!(read.as_mut()).is_pending()); + drop(read); + let resolve = Reflect::get(&stats, &JsValue::from("resolve")) + .expect("resolver") + .dyn_into::() + .expect("resolve function"); + resolve.call0(&JsValue::NULL).expect("resolve late stream"); + Delay::from(Duration::from_millis(10)).await; + assert_eq!( + counter(&stats, "cancels"), + 1, + "late acquisition still owns cancellation" + ); + assert_eq!( + counter(&stats, "pulls"), + 0, + "late stream must never be read" + ); + } + + #[wasm_bindgen_test] + async fn bounded_kv_ready_reads_check_the_clock_between_pulls() { + let (store, stats) = streaming_store(32 * 1024, false); + let start = edgezero_core::MonotonicInstant::now(); + let end = start.checked_add(Duration::from_secs(1)).expect("end"); + let calls = Arc::new(AtomicUsize::new(0)); + let samples = Arc::clone(&calls); + let clock = MonotonicClock::new(move || { + if samples.fetch_add(1, Ordering::SeqCst) < 2 { + start + } else { + end + } + }); + let error = store + .get_bounded( + "value", + &clock, + Deadline::at_instant(end), + 32 * 1024, + 32 * 1024, + ) + .await + .expect_err("deadline"); + assert!(matches!(error, ConfigStoreError::DeadlineExceeded)); + assert!( + counter(&stats, "pulls") <= 1, + "ready microtasks must not drain past expiry" + ); + } + + #[wasm_bindgen_test] + async fn bounded_kv_uses_streaming_and_inclusive_caps() { + for length in [0, 1, 0x4000, 0x8001] { + let (store, stats) = streaming_store(length, false); + let limit = u64::try_from(length).expect("limit"); + let read = store + .get_bounded( + "value", + &MonotonicClock::default(), + Deadline::after(Duration::from_secs(1)), + limit, + limit, + ) + .await + .expect("bounded stream"); + assert_eq!(read.backend_bytes, limit); + assert_eq!(read.value.as_deref().map(str::len), Some(length)); + assert_eq!(counter(&stats, "cancels"), 0); + assert!(counter(&stats, "maxView") <= 0x4000); + } + } + + #[wasm_bindgen_test] + async fn bounded_kv_cancels_before_appending_first_excess_byte() { + for (backend, value) in [(4, 5), (5, 4), (0, 0)] { + let (store, stats) = streaming_store(5, false); + let error = store + .get_bounded( + "value", + &MonotonicClock::default(), + Deadline::after(Duration::from_secs(1)), + backend, + value, + ) + .await + .expect_err("overflow"); + assert!(matches!(error, ConfigStoreError::ValueTooLarge)); + assert_eq!(counter(&stats, "cancels"), 1); + } + } + + #[wasm_bindgen_test] + async fn bounded_kv_missing_and_invalid_utf8_are_distinct() { + let (store, _) = streaming_store(1, false); + let clock = MonotonicClock::default(); + let deadline = Deadline::after(Duration::from_secs(1)); + let read = store + .get_bounded("missing", &clock, deadline, 1, 1) + .await + .expect("missing"); + assert!(read.value.is_none()); + assert_eq!(read.backend_bytes, 0); + let error = store + .get_bounded("invalid-utf8", &clock, deadline, 1, 1) + .await + .expect_err("UTF-8"); + assert!(matches!(error, ConfigStoreError::Unavailable { .. })); + } + + #[wasm_bindgen_test] + async fn bounded_kv_dropped_read_cancels_pending_reader() { + let (store, stats) = streaming_store(1, true); + let clock = MonotonicClock::default(); + let mut read = Box::pin(store.get_bounded( + "value", + &clock, + Deadline::after(Duration::from_mins(1)), + 1, + 1, + )); + // Allow promise acquisition and the first read to become pending. + for _ in 0_u32..4 { + assert!(futures_util::poll!(read.as_mut()).is_pending()); + JsFuture::from(Promise::resolve(&JsValue::NULL)) + .await + .expect("microtask"); + } + assert_eq!(counter(&stats, "pulls"), 1); + drop(read); + assert_eq!(counter(&stats, "cancels"), 1); + } + + #[wasm_bindgen_test] + async fn bounded_kv_timer_cancels_a_permanently_pending_read() { + let (store, stats) = streaming_store(1, true); + let error = store + .get_bounded( + "value", + &MonotonicClock::default(), + Deadline::after(Duration::from_millis(20)), + 1, + 1, + ) + .await + .expect_err("deadline"); + assert!(matches!(error, ConfigStoreError::DeadlineExceeded)); + assert_eq!(counter(&stats, "cancels"), 1); + } + } + use super::*; + use std::cell::Cell; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; + use futures::executor::block_on; edgezero_core::config_store_contract_tests!(cloudflare_config_store_contract, { CloudflareConfigStore::from_entries([ @@ -102,4 +562,225 @@ mod tests { ("contract.key.b".to_owned(), "value_b".to_owned()), ]) }); + + struct DropProbe(Arc); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropProbe { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + fn clock_reaching_deadline() -> (MonotonicClock, Deadline) { + let start = edgezero_core::MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let samples = Arc::new(AtomicUsize::new(0)); + let observed_samples = Arc::clone(&samples); + let clock = MonotonicClock::new(move || { + if observed_samples.fetch_add(1, Ordering::SeqCst) == 0 { + start + } else { + terminal + } + }); + (clock, Deadline::at_instant(terminal)) + } + + #[test] + fn bounded_read_timer_drops_never_ready_provider() { + let drops = Arc::new(AtomicUsize::new(0)); + let guard = DropProbe(Arc::clone(&drops)); + let read = async move { + let _guard = guard; + pending::, ConfigStoreError>>().await + }; + let (clock, deadline) = clock_reaching_deadline(); + + let error = block_on(bounded_config_read_with_timer( + read, + &clock, + deadline, + 1, + 1, + |_| ready(()), + )) + .expect_err("timer must terminate a pending config read"); + + assert!(matches!(error, ConfigStoreError::DeadlineExceeded)); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn bounded_read_rearms_after_an_early_timer_wake() { + let timer_calls = Arc::new(AtomicUsize::new(0)); + let observed_timer_calls = Arc::clone(&timer_calls); + let clock = MonotonicClock::default(); + let error = block_on(bounded_config_read_with_timer( + ready(Err(ConfigStoreError::unavailable("provider error"))), + &clock, + Deadline::after(Duration::from_secs(1)), + 1, + 1, + move |_| { + let early_wake = observed_timer_calls.fetch_add(1, Ordering::SeqCst) == 0; + async move { + if !early_wake { + pending::<()>().await; + } + } + }, + )) + .expect_err("provider error must win after an early timer wake"); + + assert!(matches!(error, ConfigStoreError::Unavailable { .. })); + assert_eq!(timer_calls.load(Ordering::SeqCst), 2); + } + + #[test] + fn bounded_read_timer_wins_simultaneous_ready_error_at_deadline() { + let (clock, deadline) = clock_reaching_deadline(); + let error = block_on(bounded_config_read_with_timer( + ready(Err(ConfigStoreError::unavailable("provider error"))), + &clock, + deadline, + 1, + 1, + |_| ready(()), + )) + .expect_err("timer readiness at the deadline must win equality"); + + assert!(matches!(error, ConfigStoreError::DeadlineExceeded)); + } + + #[test] + fn bounded_read_reports_exact_bytes_and_accepts_exact_caps() { + let clock = MonotonicClock::default(); + let result = block_on(bounded_config_read( + async { Ok(Some("value".to_owned())) }, + &clock, + Deadline::after(Duration::from_secs(1)), + 5, + 5, + )) + .expect("exact caps must succeed"); + + assert_eq!(result.backend_bytes, 5); + assert_eq!(result.value.as_deref(), Some("value")); + } + + #[test] + fn bounded_read_rejects_either_exceeded_cap() { + for (max_backend_bytes, max_value_bytes) in [(4, 5), (5, 4)] { + let clock = MonotonicClock::default(); + let error = block_on(bounded_config_read( + async { Ok(Some("value".to_owned())) }, + &clock, + Deadline::after(Duration::from_secs(1)), + max_backend_bytes, + max_value_bytes, + )) + .expect_err("an exceeded cap must fail"); + + assert!(matches!(error, ConfigStoreError::ValueTooLarge)); + } + } + + #[test] + fn bounded_read_checks_deadline_before_polling_host_call() { + let polled = Cell::new(false); + let clock = MonotonicClock::default(); + let error = block_on(bounded_config_read( + async { + polled.set(true); + Ok(None) + }, + &clock, + Deadline::after(Duration::ZERO), + 1, + 1, + )) + .expect_err("expired deadline must fail"); + + assert!(matches!(error, ConfigStoreError::DeadlineExceeded)); + assert!(!polled.get(), "expired reads must not poll the host call"); + } + + #[test] + fn bounded_read_checks_deadline_after_host_call() { + let start = MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let now = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let error = block_on(bounded_config_read( + async { + *now.lock().expect("clock lock") = terminal; + Ok(None) + }, + &clock, + Deadline::at_instant(terminal), + 1, + 1, + )) + .expect_err("a host call completing after the deadline must fail"); + + assert!(matches!(error, ConfigStoreError::DeadlineExceeded)); + } + + #[test] + fn bounded_read_deadline_wins_over_late_host_error() { + let start = MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let now = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let error = block_on(bounded_config_read( + async { + *now.lock().expect("clock lock") = terminal; + Err(ConfigStoreError::unavailable("late host error")) + }, + &clock, + Deadline::at_instant(terminal), + 1, + 1, + )) + .expect_err("the post-call deadline check must run after host errors"); + + assert!(matches!(error, ConfigStoreError::DeadlineExceeded)); + } + + #[test] + fn bounded_config_read_uses_injected_clock() { + let process_now = MonotonicInstant::now(); + let injected_now = process_now + .checked_sub(Duration::from_mins(1)) + .expect("injected instant"); + let clock = MonotonicClock::new(move || injected_now); + let deadline = Deadline::at_instant( + injected_now + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), + ); + + let result = block_on(bounded_config_read( + async { Ok(Some("value".to_owned())) }, + &clock, + deadline, + 5, + 5, + )) + .expect("the application clock is still before its deadline"); + + assert_eq!(result.value.as_deref(), Some("value")); + } } diff --git a/crates/edgezero-adapter-cloudflare/src/config_store/kv.rs b/crates/edgezero-adapter-cloudflare/src/config_store/kv.rs new file mode 100644 index 000000000..213527c20 --- /dev/null +++ b/crates/edgezero-adapter-cloudflare/src/config_store/kv.rs @@ -0,0 +1,234 @@ +//! Workers-only KV acquisition and caller-sized byte reads. + +use std::cell::RefCell; +use std::rc::Rc; +use std::time::Duration; + +use edgezero_core::config_store::ConfigStoreError; +use edgezero_core::time::{Deadline, MonotonicClock}; +use wasm_streams::readable::ReadableStreamBYOBReader; +use worker::Delay; +use worker::js_sys::{Function, Object, Promise, Reflect, Uint8Array, global}; +use worker::kv::KvStore as WorkerKvStore; +use worker::wasm_bindgen::closure::Closure; +use worker::wasm_bindgen::{JsCast as _, JsValue}; +use worker::wasm_bindgen_futures::JsFuture; +use worker::web_sys::ReadableStream; + +const CONFIG_READ_CHUNK_BYTES: usize = 0x4000; + +struct Acquisition(Rc>); + +#[derive(Default)] +struct AcquisitionState { + abandoned: bool, + missing: bool, + stream: Option, +} + +struct ConfigReader<'stream> { + completed: bool, + reader: ReadableStreamBYOBReader<'stream>, +} + +pub(super) struct KvBinding { + get: Function, + namespace: JsValue, + store: WorkerKvStore, +} + +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for Acquisition { + fn drop(&mut self) { + let acquired = { + let mut state = self.0.borrow_mut(); + state.abandoned = true; + state.stream.take() + }; + if let Some(stream) = acquired { + consume_cancellation(&stream.cancel()); + } + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for ConfigReader<'_> { + fn drop(&mut self) { + if !self.completed { + consume_cancellation(&self.reader.as_raw().cancel()); + } + } +} + +impl KvBinding { + pub(super) fn from_this(env: &JsValue, name: &str) -> Result { + let store = WorkerKvStore::from_this(env, name).map_err(|_error| unavailable())?; + let namespace = Reflect::get(env, &JsValue::from(name)).map_err(|_error| unavailable())?; + let get = Reflect::get(&namespace, &JsValue::from("get")) + .map_err(|_error| unavailable())? + .dyn_into::() + .map_err(|_error| unavailable())?; + Ok(Self { + get, + namespace, + store, + }) + } + + pub(super) async fn materialized_get( + &self, + key: &str, + ) -> Result, ConfigStoreError> { + self.store + .get(key) + .text() + .await + .map_err(|_error| unavailable()) + } + + pub(super) async fn streaming_get( + &self, + key: &str, + max_bytes: u64, + clock: &MonotonicClock, + deadline: Deadline, + ) -> Result, ConfigStoreError> { + // Transferred callbacks require wasm-bindgen's JS-GC cleanup support. + // Older/explicitly disabled Workers profiles must fail before dispatch. + if Reflect::get(&global(), &JsValue::from("FinalizationRegistry")) + .map_err(|_error| unavailable())? + .dyn_into::() + .is_err() + { + return Err(unavailable()); + } + let options = Object::new(); + Reflect::set(&options, &JsValue::from("type"), &JsValue::from("stream")) + .map_err(|_error| unavailable())?; + let promise = self + .get + .call2(&self.namespace, &JsValue::from(key), &options) + .map_err(|_error| unavailable())? + .dyn_into::() + .map_err(|_error| unavailable())?; + let acquisition = Acquisition(Rc::new(RefCell::new(AcquisitionState::default()))); + let state = Rc::clone(&acquisition.0); + // The JS promise retains the callback even if the Rust waiter is dropped. + // Transfer the closure to JS GC ownership; no detached Rust cleanup task. + let resolved = Closure::new(move |value: JsValue| { + if value.is_null() { + state.borrow_mut().missing = true; + return; + } + if let Ok(stream) = value.dyn_into::() { + let mut acquired = state.borrow_mut(); + if acquired.abandoned { + drop(acquired); + consume_cancellation(&stream.cancel()); + } else { + acquired.stream = Some(stream); + } + } + }); + let observed = promise.then(&resolved); + drop(resolved.into_js_value()); + JsFuture::from(observed) + .await + .map_err(|_error| unavailable())?; + if acquisition.0.borrow().missing { + return Ok(None); + } + let raw = acquisition + .0 + .borrow_mut() + .stream + .take() + .ok_or_else(unavailable)?; + drain(raw, max_bytes, clock, deadline).await.map(Some) + } +} + +fn consume_cancellation(promise: &Promise) { + // Both branches must be handled: an ignored rejected cancel promise can + // otherwise publish secret-bearing provider diagnostics as an unhandled rejection. + let ignore = Closure::new(|_value: JsValue| {}); + drop(promise.then2(&ignore, &ignore)); + drop(ignore.into_js_value()); +} + +async fn drain( + raw: ReadableStream, + max_bytes: u64, + clock: &MonotonicClock, + deadline: Deadline, +) -> Result { + let mut stream = wasm_streams::ReadableStream::from_raw(raw.clone().unchecked_into()); + let reader = match stream.try_get_byob_reader() { + Ok(reader) => reader, + Err(_error) => { + consume_cancellation(&raw.cancel()); + return Err(unavailable()); + } + }; + let mut guard = ConfigReader { + completed: false, + reader, + }; + let limit = usize::try_from(max_bytes).unwrap_or(usize::MAX); + let mut bytes = Vec::new(); + let mut scratch = vec![0_u8; CONFIG_READ_CHUNK_BYTES]; + let mut buffer = Uint8Array::new_with_length(0x4000); + let mut pulls = 0_u32; + loop { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + let remaining = limit.saturating_sub(bytes.len()); + let wanted = remaining.saturating_add(1).min(CONFIG_READ_CHUNK_BYTES); + let (count, returned_buffer) = guard + .reader + .read_with_buffer(scratch.get_mut(..wanted).ok_or_else(unavailable)?, buffer) + .await + .map_err(|_error| unavailable())?; + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + buffer = returned_buffer.ok_or_else(unavailable)?; + if count == 0 { + guard.completed = true; + break; + } + if count > remaining { + return Err(ConfigStoreError::ValueTooLarge); + } + let next_len = bytes + .len() + .checked_add(count) + .ok_or(ConfigStoreError::ValueTooLarge)?; + if next_len > bytes.capacity() { + let capacity = next_len.max(bytes.capacity().saturating_mul(2)).min(limit); + bytes + .try_reserve_exact(capacity.saturating_sub(bytes.len())) + .map_err(|_error| ConfigStoreError::ValueTooLarge)?; + } + bytes.extend_from_slice(scratch.get(..count).ok_or_else(unavailable)?); + pulls = pulls.saturating_add(1); + if pulls == 16 { + // Workers may freeze its clock within a task. Yield to the task queue, + // not just the promise microtask queue, so timers can make progress. + Delay::from(Duration::ZERO).await; + pulls = 0; + } + } + String::from_utf8(bytes).map_err(|_error| unavailable()) +} + +fn unavailable() -> ConfigStoreError { + ConfigStoreError::unavailable("config KV byte stream is unavailable") +} diff --git a/crates/edgezero-adapter-cloudflare/src/context.rs b/crates/edgezero-adapter-cloudflare/src/context.rs index a5faf72f9..e03d7a273 100644 --- a/crates/edgezero-adapter-cloudflare/src/context.rs +++ b/crates/edgezero-adapter-cloudflare/src/context.rs @@ -11,6 +11,11 @@ pub struct CloudflareRequestContext { } impl CloudflareRequestContext { + #[inline] + pub(crate) fn context_handle(&self) -> Arc { + Arc::clone(&self.ctx) + } + #[inline] #[must_use] pub fn ctx(&self) -> &Context { diff --git a/crates/edgezero-adapter-cloudflare/src/key_value_store.rs b/crates/edgezero-adapter-cloudflare/src/key_value_store.rs index 651780033..8feb4a96b 100644 --- a/crates/edgezero-adapter-cloudflare/src/key_value_store.rs +++ b/crates/edgezero-adapter-cloudflare/src/key_value_store.rs @@ -7,50 +7,85 @@ //! This module is only compiled when the `cloudflare` feature is enabled //! and the target is `wasm32`. -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] use async_trait::async_trait; -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] use bytes::Bytes; -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] -use edgezero_core::key_value_store::{KvError, KvPage, KvStore}; -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +use edgezero_core::key_value_store::{KvError, KvPage, KvStore, KvStoreOpenFailure}; +use edgezero_core::logging::BOOT_LOG_TARGET; +use std::collections::HashSet; +use std::sync::{Mutex, OnceLock, PoisonError}; use std::time::Duration; -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] -use worker::kv::KvStore as WorkerKvStore; +use worker::js_sys::Reflect; +use worker::kv::{KvError as WorkerKvError, KvStore as WorkerKvStore}; +use worker::wasm_bindgen::JsValue; /// KV store backed by Cloudflare Workers KV. /// -/// Wraps a `worker::kv::KvStore` handle obtained via the environment binding. -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +/// Opens compiled declarations lazily per request, or wraps an explicitly opened handle. pub struct CloudflareKvStore { - store: WorkerKvStore, + source: CloudflareKvStoreSource, +} + +enum CloudflareKvStoreSource { + Declared { + binding: String, + env: worker::Env, + id: &'static str, + opened: OnceLock>, + }, + Opened(WorkerKvStore), } -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] impl CloudflareKvStore { + pub(crate) fn declared(env: worker::Env, id: &'static str, binding: String) -> Self { + Self { + source: CloudflareKvStoreSource::Declared { + binding, + env, + id, + opened: OnceLock::new(), + }, + } + } + /// Create a new Cloudflare KV store from the environment binding name. /// /// The `binding` must match a KV namespace binding in `wrangler.toml`. - /// Uses `env.kv(binding)` which is the idiomatic `worker` 0.7+ API. + /// Validates the namespace's callable methods before constructing the SDK binding. /// /// # Errors - /// Returns [`KvError::Internal`] if the named binding is missing from the - /// Worker environment or otherwise cannot be opened. + /// Returns [`KvError::OpenFailed`] with a payload-free open reason. #[inline] pub fn from_env(env: &worker::Env, binding: &str) -> Result { - let store = env.kv(binding).map_err(|err| { - KvError::Internal(anyhow::anyhow!("failed to open kv binding: {err}")) - })?; - Ok(Self { store }) + let store = open_store(env, binding).map_err(|reason| KvError::OpenFailed { reason })?; + Ok(Self { + source: CloudflareKvStoreSource::Opened(store), + }) + } + + fn store(&self) -> Result<&WorkerKvStore, KvError> { + match &self.source { + CloudflareKvStoreSource::Opened(store) => Ok(store), + CloudflareKvStoreSource::Declared { + binding, + env, + id, + opened, + } => opened + .get_or_init(|| { + open_store(env, binding) + .inspect_err(|reason| warn_declared_open_once(id, *reason)) + }) + .as_ref() + .map_err(|reason| KvError::OpenFailed { reason: *reason }), + } } } -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] #[async_trait(?Send)] impl KvStore for CloudflareKvStore { #[inline] async fn delete(&self, key: &str) -> Result<(), KvError> { - self.store + self.store()? .delete(key) .await .map_err(|err| KvError::Internal(anyhow::anyhow!("delete failed: {err}"))) @@ -64,7 +99,7 @@ impl KvStore for CloudflareKvStore { #[inline] async fn get_bytes(&self, key: &str) -> Result, KvError> { let result = self - .store + .store()? .get(key) .bytes() .await @@ -81,7 +116,7 @@ impl KvStore for CloudflareKvStore { ) -> Result { let limit_u64 = u64::try_from(limit) .map_err(|err| KvError::Validation(format!("list limit exceeds u64: {err}")))?; - let mut request = self.store.list().limit(limit_u64); + let mut request = self.store()?.list().limit(limit_u64); if !prefix.is_empty() { request = request.prefix(prefix.to_owned()); @@ -106,7 +141,7 @@ impl KvStore for CloudflareKvStore { #[inline] async fn put_bytes(&self, key: &str, value: Bytes) -> Result<(), KvError> { - self.store + self.store()? .put_bytes(key, value.as_ref()) .map_err(|err| KvError::Internal(anyhow::anyhow!("put failed: {err}")))? .execute() @@ -125,7 +160,7 @@ impl KvStore for CloudflareKvStore { // truncation via `as_secs()` cannot produce a zero TTL here. let ttl_secs = ttl.as_secs(); - self.store + self.store()? .put_bytes(key, value.as_ref()) .map_err(|err| KvError::Internal(anyhow::anyhow!("put failed: {err}")))? .expiration_ttl(ttl_secs) @@ -135,5 +170,139 @@ impl KvStore for CloudflareKvStore { } } -// TODO: integration tests require a wasm32 target + wrangler. -// Test `CloudflareKvStore` as part of the Cloudflare adapter E2E test suite. +fn open_store(env: &worker::Env, binding: &str) -> Result { + let namespace = Reflect::get(env.as_ref(), &JsValue::from_str(binding)) + .map_err(|_error| KvStoreOpenFailure::Unavailable)?; + if namespace.is_undefined() { + return Err(KvStoreOpenFailure::MissingBinding); + } + if namespace.is_null() || !namespace.is_object() { + return Err(KvStoreOpenFailure::InvalidConfiguration); + } + // The SDK casts these methods unchecked; reject malformed bindings before use. + for method in ["get", "getWithMetadata", "put", "list", "delete"] { + let value = Reflect::get(&namespace, &JsValue::from_str(method)) + .map_err(|_error| KvStoreOpenFailure::Unavailable)?; + if !value.is_function() { + return Err(KvStoreOpenFailure::InvalidConfiguration); + } + } + WorkerKvStore::from_this(env.as_ref(), binding).map_err(|error| match error { + WorkerKvError::InvalidKvStore(_) => KvStoreOpenFailure::MissingBinding, + WorkerKvError::Serialization(_) => KvStoreOpenFailure::InvalidConfiguration, + WorkerKvError::JavaScript(_) => KvStoreOpenFailure::Unavailable, + }) +} + +fn warn_declared_open_once(id: &'static str, reason: KvStoreOpenFailure) { + // Membership is bounded by compiled metadata IDs, never runtime-selected bindings. + static WARNED: OnceLock>> = OnceLock::new(); + let first = WARNED + .get_or_init(|| Mutex::new(HashSet::new())) + .lock() + .unwrap_or_else(PoisonError::into_inner) + .insert(id); + if first { + log::warn!(target: BOOT_LOG_TARGET, "declared Cloudflare KV store '{id}' failed to open: {reason}"); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use wasm_bindgen_test::wasm_bindgen_test; + use worker::js_sys::{Function, Object, Reflect}; + use worker::wasm_bindgen::{JsCast as _, JsValue}; + + fn bind(env: &worker::Env, value: &JsValue) { + Reflect::set(env.as_ref(), &JsValue::from_str("physical"), value).expect("binding"); + } + + fn namespace() -> Object { + let object = Object::new(); + for method in ["get", "getWithMetadata", "put", "list", "delete"] { + Reflect::set( + &object, + &JsValue::from_str(method), + &Function::new_no_args(""), + ) + .expect("method"); + } + object + } + + #[wasm_bindgen_test] + async fn declared_kv_open_failure_retains_binding_and_recovers() { + let env: worker::Env = Object::new().unchecked_into(); + let store = CloudflareKvStore::declared(env.clone(), "declared", "physical".to_owned()); + assert!( + matches!(&store.source, CloudflareKvStoreSource::Declared { opened, .. } if opened.get().is_none()) + ); + let failures = [ + store.get_bytes("key").await.err(), + store.exists("key").await.err(), + store.put_bytes("key", Bytes::new()).await.err(), + store + .put_bytes_with_ttl("key", Bytes::new(), Duration::from_secs(60)) + .await + .err(), + store.delete("key").await.err(), + store.list_keys_page("", None, 1).await.err(), + ]; + for error in failures { + assert!(matches!( + error, + Some(KvError::OpenFailed { + reason: KvStoreOpenFailure::MissingBinding + }) + )); + } + bind(&env, &namespace()); + assert!( + store.store().is_err(), + "request-local failure stays settled" + ); + let restored = CloudflareKvStore::declared(env, "declared", "physical".to_owned()); + assert!(restored.store().is_ok(), "next request retries the binding"); + } + + #[wasm_bindgen_test] + fn malformed_and_throwing_bindings_have_payload_free_reasons() { + let env: worker::Env = Object::new().unchecked_into(); + for value in [ + JsValue::NULL, + JsValue::from_str("provider-token"), + Object::new().into(), + ] { + bind(&env, &value); + assert!(matches!( + CloudflareKvStore::from_env(&env, "physical"), + Err(KvError::OpenFailed { + reason: KvStoreOpenFailure::InvalidConfiguration + }) + )); + } + let getter = Object::new(); + Reflect::set( + &getter, + &JsValue::from_str("get"), + &Function::new_no_args("throw new Error('provider-token')"), + ) + .expect("getter"); + Object::define_property( + env.unchecked_ref::(), + &JsValue::from_str("physical"), + &getter, + ); + let error = CloudflareKvStore::from_env(&env, "physical") + .err() + .expect("throwing binding"); + assert!(matches!( + error, + KvError::OpenFailed { + reason: KvStoreOpenFailure::Unavailable + } + )); + assert!(!error.to_string().contains("provider-token")); + } +} diff --git a/crates/edgezero-adapter-cloudflare/src/lib.rs b/crates/edgezero-adapter-cloudflare/src/lib.rs index 5980c8629..82075e69a 100644 --- a/crates/edgezero-adapter-cloudflare/src/lib.rs +++ b/crates/edgezero-adapter-cloudflare/src/lib.rs @@ -1,6 +1,6 @@ //! Adapter helpers for Cloudflare Workers. -#[cfg(feature = "cli")] +#[cfg(all(feature = "cli", not(target_arch = "wasm32")))] pub mod cli; // `config_store` compiles on host for its `InMemory` test backend; the @@ -11,29 +11,76 @@ pub mod config_store; pub mod context; #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] pub mod key_value_store; -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] -pub mod proxy; +#[cfg(any( + test, + feature = "test-utils", + all(feature = "cloudflare", target_arch = "wasm32") +))] +pub mod outbound; #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] pub mod request; -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] -pub mod response; #[cfg(any(test, all(feature = "cloudflare", target_arch = "wasm32")))] -mod response_headers; +pub mod response; #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] pub mod secret_store; +#[cfg(test)] +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +mod test_runtime; #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] -use edgezero_core::app::{App, Hooks, StoresMetadata}; +use edgezero_core::OutboundScopeOwner; +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +use edgezero_core::app::StoresMetadata; +#[cfg(any(test, all(feature = "cloudflare", target_arch = "wasm32")))] +use edgezero_core::app::{App, Hooks}; +#[cfg(any(test, all(feature = "cloudflare", target_arch = "wasm32")))] +use edgezero_core::context::RuntimeVariables; #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] use edgezero_core::env_config::EnvConfig; +#[cfg(any(test, all(feature = "cloudflare", target_arch = "wasm32")))] +use edgezero_core::error::EdgeError; +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +use edgezero_core::manifest::BakedManifest; +#[cfg(any(test, all(feature = "cloudflare", target_arch = "wasm32")))] +use edgezero_core::manifest::ResolvedEnvironmentBinding; +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +use edgezero_core::time::MonotonicClock; #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] use worker::{Context, Env, Error as WorkerError, Request, Response}; +/// Cloudflare Workers' published per-isolate memory limit. +pub const CLOUDFLARE_PLATFORM: edgezero_core::PlatformMetadata = + edgezero_core::PlatformMetadata::new( + edgezero_core::PlatformFact::known( + edgezero_core::MemoryCeiling::new( + 128_000_000, + edgezero_core::MemoryCeilingScope::PerInstance, + None, + ), + edgezero_core::PlatformResourceSource::PlatformLimit { + provider: "Cloudflare Workers", + }, + ), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::ProviderUnpublished, + ), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::ProviderUnpublished, + ), + ); + +#[cfg(any(test, all(feature = "cloudflare", target_arch = "wasm32")))] +fn build_target_app() -> Result +where + A: Hooks, +{ + App::build::(CLOUDFLARE_PLATFORM) +} + /// # Errors -/// Never; this is currently a no-op on Cloudflare Workers (Workers manages -/// its own logging). The signature still returns [`log::SetLoggerError`] so -/// callers and the non-wasm stub stay drop-in compatible if a real logger -/// is wired in later. +/// Never; this is a no-op. Workers console logging does not install a Rust +/// `log` backend. Applications needing facade output must install a backend +/// and its filters before [`run_app`] and set `Hooks::owns_logging()` to `true`. #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] #[inline] pub fn init_logger() -> Result<(), log::SetLoggerError> { @@ -86,6 +133,18 @@ fn env_config_from_worker(env: &Env, stores: StoresMetadata) -> EnvConfig { EnvConfig::from_vars(vars) } +#[cfg(any(test, all(feature = "cloudflare", target_arch = "wasm32")))] +fn collect_worker_variables( + bindings: &[ResolvedEnvironmentBinding], + mut lookup: impl FnMut(&str) -> Option, +) -> RuntimeVariables { + RuntimeVariables::from_vars(bindings.iter().filter_map(|binding| { + lookup(&binding.env) + .or_else(|| binding.value.clone()) + .map(|value| (binding.name.clone(), value)) + })) +} + /// Entry point for a Cloudflare Workers application. /// /// Portable store config is baked into `A` by the `app!` macro; adapter-specific @@ -93,35 +152,43 @@ fn env_config_from_worker(env: &Env, stores: StoresMetadata) -> EnvConfig { /// variables on the worker `Env`. No `edgezero.toml` is required. /// /// # Errors -/// Returns [`worker::Error`] if the inner dispatch fails or any required -/// store binding cannot be opened. +/// Returns a category-safe [`worker::Error`] for request conversion, admission abort or failed +/// delivery. Assembly failures use their typed HTTP status and the default error response. #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] #[inline] pub async fn run_app(req: Request, env: Env, ctx: Context) -> Result where A: Hooks, { - // Best-effort: if a logger is already installed, ignore the error rather - // than panicking — every Worker request re-enters this function. Skipped - // entirely when the app owns logging. - if !A::owns_logging() { - drop(init_logger()); - } - let app = A::build_app(); - dispatch_app::(&app, req, env, ctx).await + let callback = OutboundScopeOwner::callback(); + callback + .run(async move { + if !A::owns_logging() { + drop(init_logger()); + } + let clock = MonotonicClock::default(); + let request_start = clock.now(); + let app = match build_target_app::() { + Ok(app) => app, + Err(error) => { + drop(env); + return request::send_assembly_error(req, error, ctx, request_start, clock); + } + }; + dispatch_scoped_app::(&app, req, env, ctx).await + }) + .await } -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] -/// Dispatch a caller-owned app using `A::stores()` for fresh request bindings. +/// Dispatch a caller-owned app through canonical admission and response egress. /// -/// This does not build or cache an app or install logging. The caller must pass -/// an app built from `A`; `App` erases its construction type, so this pairing -/// remains a caller precondition. Retain only application-owned values and keep -/// native handles and pending work request-local. Shared state must support -/// overlapping invocations. +/// The app must be built from `A` with [`CLOUDFLARE_PLATFORM`]. This function does not +/// initialize logging or retain an app. Bindings and runtime variables are resolved +/// for every request; only application-owned, concurrency-safe state may be retained. /// /// # Errors -/// Returns conversion or dispatch errors from the existing adapter boundary. +/// Returns category-safe configuration, admission-abort, or adapter delivery errors. +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] #[inline] pub async fn dispatch_app( app: &App, @@ -129,11 +196,38 @@ pub async fn dispatch_app( env: Env, ctx: Context, ) -> Result +where + A: Hooks, +{ + request::with_callback_scope(dispatch_scoped_app::(app, req, env, ctx)).await +} + +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +async fn dispatch_scoped_app( + app: &App, + req: Request, + env: Env, + ctx: Context, +) -> Result where A: Hooks, { let stores = A::stores(); let env_config = env_config_from_worker(&env, stores); + let runtime_variables = match A::manifest() { + BakedManifest::Absent => RuntimeVariables::default(), + BakedManifest::Present(manifest) => { + let resolved = manifest.environment_for("cloudflare"); + collect_worker_variables(&resolved.variables, |key| { + env.var(key).ok().map(|value| value.to_string()) + }) + } + BakedManifest::Malformed(_) | _ => { + return Err(WorkerError::RustError( + "application manifest is invalid".to_owned(), + )); + } + }; request::dispatch_with_registries( app, req, @@ -144,7 +238,95 @@ where kv_meta: stores.kv, secret_meta: stores.secrets, env_config: &env_config, + runtime_variables, }, ) .await } + +#[cfg(test)] +mod platform_tests { + use edgezero_core::app::{App, Hooks}; + use edgezero_core::error::EdgeError; + use edgezero_core::manifest::ResolvedEnvironmentBinding; + use edgezero_core::router::RouterService; + + struct PlatformAwareConfiguration; + + #[expect( + clippy::missing_trait_methods, + reason = "test hook exercises only target metadata propagation" + )] + impl Hooks for PlatformAwareConfiguration { + fn configure(app: &mut App) -> Result<(), EdgeError> { + if app.platform() == crate::CLOUDFLARE_PLATFORM { + Ok(()) + } else { + Err(EdgeError::service_unavailable("wrong application platform")) + } + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + #[test] + fn application_configuration_receives_cloudflare_platform_metadata() { + let app = super::build_target_app::() + .expect("configured application"); + + assert_eq!(app.platform(), crate::CLOUDFLARE_PLATFORM); + } + + #[test] + fn application_route_failure_preserves_target_error() { + struct RouteFailure; + #[expect( + clippy::missing_trait_methods, + reason = "tests typed route failure at target assembly" + )] + impl Hooks for RouteFailure { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Err(EdgeError::bad_request("configuration must not run")) + } + fn routes() -> Result { + Err(EdgeError::service_unavailable("route state unavailable")) + } + } + let Err(error) = super::build_target_app::() else { + panic!("route failure must prevent target application assembly"); + }; + assert_eq!(error.status().as_u16(), 503); + assert_eq!(error.message(), "route state unavailable"); + } + + #[test] + fn declared_worker_variables_prefer_binding_then_manifest_default() { + let bindings = [ + ResolvedEnvironmentBinding { + description: None, + env: "UPSTREAM_ORIGIN".to_owned(), + name: "API_BASE_URL".to_owned(), + value: Some("https://default.example".to_owned()), + }, + ResolvedEnvironmentBinding { + description: None, + env: "OPTIONAL_BINDING".to_owned(), + name: "OPTIONAL".to_owned(), + value: None, + }, + ]; + let runtime = super::collect_worker_variables(&bindings, |key| { + (key == "UPSTREAM_ORIGIN").then(|| "https://worker.example".to_owned()) + }); + assert_eq!(runtime.get("API_BASE_URL"), Some("https://worker.example")); + assert_eq!(runtime.get("OPTIONAL"), None); + + let defaulted = super::collect_worker_variables(&bindings, |_| None); + assert_eq!( + defaulted.get("API_BASE_URL"), + Some("https://default.example") + ); + } +} diff --git a/crates/edgezero-adapter-cloudflare/src/outbound.rs b/crates/edgezero-adapter-cloudflare/src/outbound.rs new file mode 100644 index 000000000..70a434372 --- /dev/null +++ b/crates/edgezero-adapter-cloudflare/src/outbound.rs @@ -0,0 +1,3391 @@ +#![cfg_attr( + all( + feature = "cloudflare", + target_arch = "wasm32", + any(test, feature = "test-utils") + ), + expect( + clippy::self_named_module_files, + reason = "the established outbound module keeps private Workers response fixtures in a submodule" + ) +)] +#![cfg_attr( + any(test, all(feature = "cloudflare", target_arch = "wasm32")), + expect( + clippy::arbitrary_source_item_ordering, + reason = "target and test helper modules stay adjacent to the imports that define their boundaries" + ) +)] +#![cfg_attr( + all(feature = "cloudflare", target_arch = "wasm32"), + expect( + clippy::pub_use, + reason = "the target-gated implementation keeps Workers imports out of native builds" + ) +)] + +use edgezero_core::error::EdgeError; +#[cfg(any( + all(feature = "cloudflare", target_arch = "wasm32"), + feature = "test-utils" +))] +use edgezero_core::error::{BadGatewayReason, BudgetSource}; +#[cfg(any( + all(feature = "cloudflare", target_arch = "wasm32"), + feature = "test-utils" +))] +use edgezero_core::outbound::OutboundRequest; +#[cfg(feature = "test-utils")] +use edgezero_core::outbound::{OutboundSendMode, validate_for_dispatch}; + +#[cfg(any(all(feature = "cloudflare", target_arch = "wasm32"), test))] +mod header_bridge { + use std::str; + + use edgezero_core::error::EdgeError; + use edgezero_core::http::{HeaderMap, HeaderName}; + + pub(crate) trait HeaderSink { + fn append(&mut self, name: &str, value: &str) -> Result<(), EdgeError>; + } + + pub(crate) fn copy_header_values( + source: &HeaderMap, + sink: &mut Sink, + invalid_value: InvalidValue, + ) -> Result<(), EdgeError> + where + Sink: HeaderSink, + InvalidValue: Fn(&HeaderName) -> EdgeError, + { + for (name, value) in source { + let header_value = + str::from_utf8(value.as_bytes()).map_err(|_encoding_error| invalid_value(name))?; + sink.append(name.as_str(), header_value)?; + } + Ok(()) + } +} + +#[cfg(any(all(feature = "cloudflare", target_arch = "wasm32"), test))] +use header_bridge::HeaderSink; +#[cfg(any(all(feature = "cloudflare", target_arch = "wasm32"), test))] +pub(crate) use header_bridge::copy_header_values; + +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +impl HeaderSink for worker::Headers { + fn append(&mut self, name: &str, value: &str) -> Result<(), EdgeError> { + worker::Headers::append(self, name, value).map_err(EdgeError::internal) + } +} + +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +mod worker_impl { + #[cfg(any(test, feature = "test-utils"))] + mod testing; + #[cfg(any(test, feature = "test-utils"))] + use edgezero_core::outbound::{ + OutboundProxyMarker, OutboundRequest, ResponseDecoding, ResponseMode, + }; + use std::cell::{Cell, RefCell}; + use std::future::Future; + use std::pin::Pin; + use std::rc::Rc; + use std::task::{Context, Poll, Waker}; + use std::time::Duration; + #[cfg(any(test, feature = "test-utils"))] + use testing::{finish_fixture_response, finish_fixture_stream}; + + use async_stream::stream; + use bytes::Bytes; + use edgezero_core::body::{Body, BodyPayload, BodyStream}; + #[cfg(any(test, feature = "test-utils"))] + use edgezero_core::error::BudgetSource; + use edgezero_core::error::{BadGatewayReason, EdgeError}; + use edgezero_core::error_detail::{ + CloudflareFailureCause, ProviderFailureDetail, ProviderFailurePhase, + }; + use edgezero_core::http::header::{ACCEPT_ENCODING, HOST}; + use edgezero_core::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode}; + use edgezero_core::outbound::{ + OutboundBatch, OutboundBatchDriverEvent, OutboundCachePolicy, OutboundDispatchRequest, + OutboundExchangeOwner, OutboundFuture, OutboundHead, OutboundHeaderFidelity, + OutboundHeaderInput, OutboundHttpClient, OutboundPending, OutboundPreparedBatch, + OutboundRequestParts, OutboundResponse, OutboundResponseContext, OutboundSendMode, + OutboundTask, OutboundTime, + }; + #[cfg(feature = "test-utils")] + use edgezero_core::time::{Deadline, dispatch_budget}; + use edgezero_core::time::{DispatchBudget, MonotonicClock, MonotonicInstant}; + use edgezero_core::{IndependentOutboundProgress, OutboundProgressRuntime}; + use futures::channel::{mpsc, oneshot}; + use futures_util::StreamExt as _; + use futures_util::future::{FutureExt as _, LocalBoxFuture, poll_fn}; + use futures_util::stream::{FuturesUnordered, once}; + use worker::js_sys::{Function, Reflect, Uint8Array, global}; + use worker::wasm_bindgen::closure::Closure; + use worker::wasm_bindgen::{JsCast as _, JsValue}; + use worker::wasm_bindgen_futures::{JsFuture, spawn_local}; + use worker::web_sys; + use worker::{ + CacheMode, Headers, Method as WorkerMethod, Request as WorkerRequest, RequestInit, + RequestRedirect, Response as WorkerResponse, ResponseBody as WorkerResponseBody, + }; + + use super::timeout_error; + + const READY_ITEM_YIELD_QUOTA: u32 = 64; + + struct HostEventYield { + awoken: Rc>, + callback: Option>, + cancellation: Option<(Function, JsValue)>, + duration: Duration, + timeout_handle: Option, + waker: Rc>>, + } + + impl HostEventYield { + fn new() -> Self { + Self::after(Duration::ZERO) + } + + fn after(duration: Duration) -> Self { + Self { + awoken: Rc::new(Cell::new(false)), + callback: None, + cancellation: None, + duration, + timeout_handle: None, + waker: Rc::new(RefCell::new(None)), + } + } + } + + impl Future for HostEventYield { + type Output = Result<(), EdgeError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + if this.awoken.get() { + drop(this.callback.take()); + drop(this.cancellation.take()); + drop(this.timeout_handle.take()); + let previous = this.waker.borrow_mut().take(); + drop(previous); + return Poll::Ready(Ok(())); + } + let next_waker = cx.waker().clone(); + let previous = this.waker.borrow_mut().replace(next_waker); + drop(previous); + if this.callback.is_some() { + return Poll::Pending; + } + + let awoken = Rc::clone(&this.awoken); + let waker = Rc::clone(&this.waker); + let callback = Closure::::new(move || { + awoken.set(true); + let registered = waker.borrow_mut().take(); + if let Some(waiting) = registered { + waiting.wake(); + } + }); + let global_scope = global(); + let Some(set_timeout) = Reflect::get(&global_scope, &JsValue::from_str("setTimeout")) + .ok() + .and_then(|value| value.dyn_into::().ok()) + else { + return Poll::Ready(Err(host_event_schedule_error())); + }; + let Some(clear_timeout) = + Reflect::get(&global_scope, &JsValue::from_str("clearTimeout")) + .ok() + .and_then(|value| value.dyn_into::().ok()) + else { + return Poll::Ready(Err(host_event_schedule_error())); + }; + let milliseconds = + i32::try_from(this.duration.as_nanos().div_ceil(1_000_000)).unwrap_or(i32::MAX); + let timeout_handle = match set_timeout.call2( + &global_scope, + callback.as_ref(), + &JsValue::from_f64(f64::from(milliseconds)), + ) { + Ok(handle) => handle, + Err(_error) => return Poll::Ready(Err(host_event_schedule_error())), + }; + this.callback = Some(callback); + this.timeout_handle = Some(timeout_handle); + this.cancellation = Some((clear_timeout, global_scope.into())); + Poll::Pending + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for HostEventYield { + fn drop(&mut self) { + if self.awoken.get() { + return; + } + let Some(timeout_handle) = self.timeout_handle.take() else { + return; + }; + if let Some((clear, receiver)) = self.cancellation.take() { + let _ignored = clear.call1(&receiver, &timeout_handle); + } + } + } + + fn host_event_schedule_error() -> EdgeError { + EdgeError::internal(anyhow::anyhow!("Cloudflare host-event scheduling failed")) + } + + /// Native outbound HTTP implementation for Cloudflare Workers. + #[derive(Clone)] + pub struct CloudflareOutboundClient { + time: OutboundTime, + } + + struct AbortGuard { + controller: Option, + } + + impl AbortGuard { + fn disarm(&mut self) { + self.controller = None; + } + + fn new(controller: web_sys::AbortController) -> Self { + Self { + controller: Some(controller), + } + } + } + + impl CloudflareOutboundClient { + async fn execute( + &self, + prepared: OutboundDispatchRequest, + ) -> Result { + let (parts, context) = prepared.into_parts(); + let budget = context.budget(); + let time = context.time().clone(); + let OutboundRequestParts { + body, + cache_policy, + mut headers, + host_authority_override, + max_request_body_bytes, + method, + uri, + .. + } = parts; + + if !headers.contains_key(ACCEPT_ENCODING) { + headers.insert(ACCEPT_ENCODING, HeaderValue::from_static("identity")); + } + if let Some(authority) = host_authority_override { + let value = + HeaderValue::from_str(authority.as_str()).map_err(EdgeError::internal)?; + headers.insert(HOST, value); + } + let worker_body = + request_body(body, max_request_body_bytes, budget, time.clone()).await?; + let url = uri.to_string(); + let request = build_worker_request(&method, &url, &headers, worker_body, cache_policy)?; + let (response, abort_guard) = raw_fetch(request, budget, &time).await?; + response_head(response, abort_guard, context) + } + + /// Builds a client using the process-default monotonic clock. + #[must_use] + #[inline] + pub fn new() -> Self { + Self::with_time(native_time(MonotonicClock::default())) + } + + /// Builds a client that evaluates every outbound lifetime against `clock`. + #[must_use] + #[inline] + pub fn with_time(time: OutboundTime) -> Self { + Self { time } + } + } + + impl Default for CloudflareOutboundClient { + #[inline] + fn default() -> Self { + Self::new() + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for AbortGuard { + fn drop(&mut self) { + if let Some(controller) = self.controller.take() { + controller.abort(); + } + } + } + + impl OutboundHttpClient for CloudflareOutboundClient { + #[inline] + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + #[inline] + fn time(&self) -> OutboundTime { + self.time.clone() + } + + #[inline] + fn validate_supported_options( + &self, + request: &super::OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + super::validate_supported_options(request) + } + + #[inline] + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + let client = self.clone(); + let scope = request.scope().clone(); + scope.run( + request.time().clone(), + request.entered_at(), + Box::pin(async move { client.execute(request).await }), + ) + } + + #[inline] + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let scope = request.scope().clone(); + let time = request.time().clone(); + let entered_at = request.entered_at(); + let head = self.dispatch_head(request); + schedule_response(scope.run( + time, + entered_at, + Box::pin(async move { head.await?.finish().await }), + )) + } + + #[inline] + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + schedule_batch(self, batch) + } + } + + fn schedule_batch( + client: &dyn OutboundHttpClient, + batch: OutboundPreparedBatch, + ) -> OutboundBatch { + let (context, slots) = batch.into_slots(); + if slots.is_empty() { + return OutboundBatch::cutoff(context); + } + let cutoff = context.observation_cutoff(); + let (mut sender, receiver) = mpsc::channel(slots.len().saturating_add(1)); + let mut work = FuturesUnordered::new(); + for (index, request) in slots { + let budget = request.budget(); + let clock = request.clock().clone(); + let scope = request.scope().clone(); + let time = request.time().clone(); + let entered_at = request.entered_at(); + let head = client.dispatch_head(request); + let pipeline = scope.run( + time, + entered_at, + Box::pin(async move { head.await?.finish().await }), + ); + work.push( + async move { + let outcome = pipeline.await; + (index, clock.now(), budget, outcome) + } + .boxed_local(), + ); + } + let task = Box::pin(async move { + let mut cutoff_unresolved = false; + while let Some((index, completed_at, budget, outcome)) = work.next().await { + cutoff_unresolved |= cutoff.is_expired_at(completed_at) + && !budget.own_deadline_expired_before(cutoff, completed_at); + if sender + .try_send(OutboundBatchDriverEvent::Item { + index, + completed_at, + outcome, + }) + .is_err() + { + return; + } + } + if cutoff_unresolved { + let _closed = sender.try_send(OutboundBatchDriverEvent::Cutoff); + } + }); + match spawn_owned(task) { + Ok(owner) => OutboundBatch::from_driver(context, receiver, owner), + Err(error) => OutboundBatch::from_driver( + context, + once(async move { OutboundBatchDriverEvent::Failed(error) }), + OutboundExchangeOwner::new(|| {}), + ), + } + } + + fn spawn_owned(work: LocalBoxFuture<'static, ()>) -> Result { + let (runner, owner) = OutboundTask::new(work)?; + spawn_local(runner); + Ok(owner) + } + + fn schedule_response( + work: OutboundFuture, + ) -> Result { + let (sender, receiver) = oneshot::channel(); + let owner = spawn_owned(Box::pin(async move { + let _closed = sender.send(work.await); + }))?; + Ok(OutboundPending::new( + Box::pin(async move { + receiver.await.map_err(|_closed| { + EdgeError::internal(anyhow::anyhow!("outbound completion task stopped")) + })? + }), + owner, + )) + } + + fn build_headers(headers: &HeaderMap) -> Result { + let mut worker_headers = Headers::new(); + super::copy_header_values(headers, &mut worker_headers, |name| { + EdgeError::bad_request(format!("header value is not valid UTF-8: {name}")) + }) + .map_err(diagnose_worker_preparation_error)?; + Ok(worker_headers) + } + + fn build_worker_request( + method: &Method, + url: &str, + headers: &HeaderMap, + request_body: Option, + cache_policy: OutboundCachePolicy, + ) -> Result { + let mut init = RequestInit::new(); + init.with_headers(build_headers(headers)?) + .with_method(worker_method(method)) + .with_redirect(RequestRedirect::Manual); + if cache_policy == OutboundCachePolicy::Bypass { + init.with_cache(CacheMode::NoStore); + } + if let Some(worker_body) = request_body { + init.with_body(Some(worker_body)); + } + WorkerRequest::new_with_init(url, &init) + .map_err(|error| diagnose_worker_preparation_error(EdgeError::internal(error))) + } + + fn guarded_response_stream(input: BodyStream, guard: AbortGuard) -> BodyStream { + stream! { + let mut source = Some(input); + let mut abort_guard = Some(guard); + let mut ready_items = 0_u32; + loop { + let next_item = source.as_mut().ok_or_else(|| EdgeError::internal(anyhow::anyhow!("response source already released")))?.next().await; + match next_item { + Some(Ok(bytes)) => yield Ok(bytes), + Some(Err(error)) => { + drop(source.take()); + drop(abort_guard.take()); + yield Err(error); + return; + } + None => { + if let Some(completed) = &mut abort_guard { completed.disarm(); } + drop(source.take()); + drop(abort_guard.take()); + if let Err(error) = HostEventYield::new().await { yield Err(error); } + return; + } + } + ready_items = ready_items.saturating_add(1); + if ready_items >= READY_ITEM_YIELD_QUOTA { + let checkpoint = HostEventYield::new().await; + ready_items = 0; + if let Err(error) = checkpoint { + drop(source.take()); + drop(abort_guard.take()); + yield Err(error); + return; + } + } + } + } + .boxed_local() + } + + fn response_head( + mut response: WorkerResponse, + abort_guard: AbortGuard, + context: OutboundResponseContext, + ) -> Result { + let budget = context.budget(); + let clock = context.clock().clone(); + budget_remaining(budget, &clock)?; + let status = StatusCode::from_u16(response.status_code()).map_err(EdgeError::internal)?; + let headers = OutboundHeaderInput::new( + "cloudflare", + response_headers(&response)?, + OutboundHeaderFidelity::ProviderFieldBoundariesUnknown, + ); + let native = stream! { + if matches!(response.body(), WorkerResponseBody::Empty) { return; } + let mut source = response_stream(&mut response)?; + while let Some(item) = source.next().await { yield item; } + } + .boxed_local(); + let encoded = guarded_response_stream(native, abort_guard); + OutboundHead::new( + status, + headers, + encoded, + context, + OutboundExchangeOwner::new(|| {}), + ) + } + + async fn raw_fetch( + request: WorkerRequest, + budget: DispatchBudget, + time: &OutboundTime, + ) -> Result<(WorkerResponse, AbortGuard), EdgeError> { + let clock = time.clock(); + let mut previous = clock.now(); + let mut timer = time.wait_until(budget.deadline); + // Register a checked wakeup before the eager fetch promise can start network work. + poll_fn(|cx| { + Poll::Ready(match timer.as_mut().poll(cx) { + Poll::Ready(result) => { + Err(result.err().unwrap_or_else(|| timeout_error(budget.cause))) + } + Poll::Pending => Ok(()), + }) + }) + .await?; + checked_now(time, &mut previous)?; + let controller = web_sys::AbortController::new().map_err(|error| { + js_preparation_error(&error, "failed to construct abort controller") + })?; + let signal = controller.signal(); + let abort_guard = AbortGuard::new(controller); + let fetch_init = web_sys::RequestInit::new(); + fetch_init.set_signal(Some(&signal)); + let set = Reflect::set( + fetch_init.as_ref(), + &JsValue::from_str("encodeResponseBody"), + &JsValue::from_str("manual"), + ) + .map_err(|error| js_preparation_error(&error, "failed to set manual response encoding"))?; + if !set { + return Err(EdgeError::internal(anyhow::anyhow!( + "runtime refused manual response encoding" + ))); + } + + let global: web_sys::WorkerGlobalScope = global().unchecked_into(); + let (promise, _remaining) = begin_fetch(budget, clock, || { + global.fetch_with_request_and_init(request.inner(), &fetch_init) + })?; + let fetch = JsFuture::from(promise); + let result = race_with_timer( + fetch, + budget, + time, + &mut timer, + &mut previous, + None, + |result| { + let error = result.as_ref().err()?; + map_fetch_failure(error).provider_failure_detail().copied() + }, + ) + .await?; + let value = finish_fetch_result(result, budget_remaining(budget, clock))?; + let web_response: web_sys::Response = value.dyn_into().map_err(|_non_response| { + EdgeError::internal(anyhow::anyhow!("fetch returned a non-response value")) + })?; + Ok((WorkerResponse::from(web_response), abort_guard)) + } + + fn begin_fetch( + budget: DispatchBudget, + clock: &MonotonicClock, + dispatch: impl FnOnce() -> Output, + ) -> Result<(Output, Duration), EdgeError> { + let remaining = budget_remaining(budget, clock)?; + Ok((dispatch(), remaining)) + } + + async fn request_body( + body: Body, + maximum: u64, + budget: DispatchBudget, + time: OutboundTime, + ) -> Result, EdgeError> { + let clock = time.clock(); + let (payload, _scope) = body.into_parts(); + match payload { + BodyPayload::Once(bytes) => { + let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX); + if length > maximum { + return Err(EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + )); + } + budget_remaining(budget, clock)?; + if bytes.is_empty() { + Ok(None) + } else { + Ok(Some(Uint8Array::from(bytes.as_ref()).into())) + } + } + BodyPayload::Stream(source) => { + let mut bounded = upload_stream(source, maximum, budget, time); + let mut collected = Vec::new(); + while let Some(item) = bounded.next().await { + collected.extend_from_slice(&item?); + } + if collected.is_empty() { + Ok(None) + } else { + Ok(Some(Uint8Array::from(collected.as_slice()).into())) + } + } + } + } + + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub async fn response_abort_lifecycle_holds_for_test() -> bool { + async fn run_case( + status: StatusCode, + content_length: Option<&str>, + start: MonotonicInstant, + ) -> Option { + let headers = Headers::new(); + if let Some(value) = content_length { + headers.set("content-length", value).ok()?; + } + let response = WorkerResponse::empty() + .ok()? + .with_status(status.as_u16()) + .with_headers(headers); + let controller = web_sys::AbortController::new().ok()?; + let signal = controller.signal(); + let deadline = start.checked_add(Duration::from_secs(1))?; + let clock = MonotonicClock::new(move || start); + finish_fixture_response( + response, + AbortGuard::new(controller), + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Buffered { max_bytes: 64 }, + DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant(deadline), + duration: Duration::from_secs(1), + }, + 1024, + 24, + None, + None, + None, + None, + None, + native_time(clock), + ) + .await + .ok()?; + Some(signal.aborted()) + } + + let start = MonotonicInstant::now(); + matches!( + run_case(StatusCode::RESET_CONTENT, None, start).await, + Some(false) + ) && matches!( + run_case(StatusCode::RESET_CONTENT, Some("7"), start).await, + Some(true) + ) && matches!( + run_case(StatusCode::NO_CONTENT, None, start).await, + Some(true) + ) + } + + fn response_headers(response: &WorkerResponse) -> Result { + let mut headers = HeaderMap::new(); + for (raw_name, raw_value) in response.headers().entries() { + let parsed_name = HeaderName::from_bytes(raw_name.as_bytes()).map_err(|_error| { + EdgeError::bad_gateway_with_reason( + "upstream response contains an invalid header name", + BadGatewayReason::Protocol, + ) + })?; + let parsed_value = HeaderValue::from_bytes(raw_value.as_bytes()).map_err(|_error| { + EdgeError::bad_gateway_with_reason( + "upstream response contains an invalid header value", + BadGatewayReason::Protocol, + ) + })?; + headers.append(parsed_name, parsed_value); + } + Ok(headers) + } + + fn js_failure_cause(value: &JsValue) -> CloudflareFailureCause { + let Ok(name) = Reflect::get(value, &JsValue::from_str("name")) else { + return CloudflareFailureCause::Unknown; + }; + if name == JsValue::from_str("AbortError") { + CloudflareFailureCause::AbortError + } else if name == JsValue::from_str("TypeError") { + CloudflareFailureCause::TypeError + } else { + CloudflareFailureCause::Unknown + } + } + + fn worker_failure_cause(error: &worker::Error) -> CloudflareFailureCause { + if let worker::Error::Internal(value) = error { + return js_failure_cause(value); + } + if let worker::Error::UnknownJsError { name, .. } = error { + return match name.as_deref() { + Some("AbortError") => CloudflareFailureCause::AbortError, + Some("TypeError") => CloudflareFailureCause::TypeError, + _ => CloudflareFailureCause::Unknown, + }; + } + CloudflareFailureCause::Unknown + } + + fn diagnose_worker_preparation_error(mut error: EdgeError) -> EdgeError { + if let EdgeError::Internal { + source, + provider_detail, + } = &mut error + && let Some(worker_error) = source.downcast_ref::() + { + *provider_detail = Some(ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::RequestPreparation, + cause: worker_failure_cause(worker_error), + }); + } + error + } + + fn js_preparation_error(source: &JsValue, message: &'static str) -> EdgeError { + EdgeError::internal_with_detail( + anyhow::anyhow!(message), + ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::RequestPreparation, + cause: js_failure_cause(source), + }, + ) + } + + fn worker_body_failure(error: &worker::Error, message: &'static str) -> EdgeError { + EdgeError::bad_gateway_with_detail( + message, + BadGatewayReason::Transport, + ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: worker_failure_cause(error), + }, + ) + } + + fn map_fetch_failure(error: &JsValue) -> EdgeError { + let mut mapped = super::generic_fetch_failure(); + if let EdgeError::BadGateway { + provider_detail, .. + } = &mut mapped + { + *provider_detail = Some(ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::Send, + cause: js_failure_cause(error), + }); + } + mapped + } + + fn finish_fetch_result( + result: Result, + remaining: Result, + ) -> Result { + if let Err(selected) = remaining { + let detail = result + .as_ref() + .err() + .map(|source| ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::Send, + cause: js_failure_cause(source), + }); + return Err(preserve_timeout_detail(selected, detail)); + } + result.map_err(|error| map_fetch_failure(&error)) + } + + fn preserve_timeout_detail( + mut selected: EdgeError, + detail: Option, + ) -> EdgeError { + if let EdgeError::GatewayTimeout { + provider_detail, .. + } = &mut selected + { + *provider_detail = detail; + } + selected + } + + fn stream_error_detail( + item: Option<&Result>, + ) -> Option { + item.and_then(|result| result.as_ref().err()) + .and_then(EdgeError::provider_failure_detail) + .copied() + } + + fn response_stream(response: &mut WorkerResponse) -> Result { + let source = response.stream().map_err(|error| { + worker_body_failure(&error, "upstream response body is unavailable") + })?; + Ok(source + .map(|result| { + result + .map(Bytes::from) + .map_err(|error| worker_body_failure(&error, "upstream response body failed")) + }) + .boxed_local()) + } + + fn budget_remaining( + budget: DispatchBudget, + clock: &MonotonicClock, + ) -> Result { + budget + .deadline + .remaining_at(clock.now()) + .map(|remaining| remaining.min(budget.duration)) + .ok_or_else(|| timeout_error(budget.cause)) + } + + async fn yield_host_event( + budget: DispatchBudget, + time: &OutboundTime, + previous: &mut MonotonicInstant, + expiry_detail: Option, + ) -> Result<(), EdgeError> { + let mut timer = time.wait_until(budget.deadline); + race_with_timer( + HostEventYield::new(), + budget, + time, + &mut timer, + previous, + expiry_detail, + |_result| None, + ) + .await? + } + + async fn terminal_error_after_host_event( + error: EdgeError, + budget: DispatchBudget, + time: &OutboundTime, + previous: &mut MonotonicInstant, + ) -> EdgeError { + let detail = error.provider_failure_detail().copied(); + match yield_host_event(budget, time, previous, detail).await { + Ok(()) => error, + Err(selected) => selected, + } + } + + fn upload_stream( + mut source: BodyStream, + maximum: u64, + budget: DispatchBudget, + time: OutboundTime, + ) -> BodyStream { + stream! { + let mut ready_items = 0_u32; + let mut total = 0_u64; + let mut previous = time.clock().now(); + let mut timer = time.wait_until(budget.deadline); + loop { + let next_item = match race_with_timer(source.next(), budget, &time, &mut timer, &mut previous, None, |item| stream_error_detail(item.as_ref())).await { + Ok(item) => item, + Err(error) => { + drop(source); + drop(timer); + drop(time); + yield Err(error); + return; + } + }; + let Some(item) = next_item else { + if let Err(error) = yield_host_event(budget, &time, &mut previous, None).await { + drop(source); + drop(timer); + drop(time); + yield Err(error); + } + return; + }; + let bytes = match item { + Ok(bytes) => bytes, + Err(error) => { + let selected = terminal_error_after_host_event(error, budget, &time, &mut previous).await; + drop(source); + drop(timer); + drop(time); + yield Err(selected); + return; + } + }; + let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX); + let Some(next_total) = total.checked_add(length) else { + let error = EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + ); + let selected = terminal_error_after_host_event(error, budget, &time, &mut previous).await; + drop(source); drop(timer); drop(time); + yield Err(selected); + return; + }; + if next_total > maximum { + let error = EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + ); + let selected = terminal_error_after_host_event(error, budget, &time, &mut previous).await; + drop(source); drop(timer); drop(time); + yield Err(selected); + return; + } + total = next_total; + yield Ok(bytes); + ready_items = ready_items.saturating_add(1); + if ready_items >= READY_ITEM_YIELD_QUOTA { + let checkpoint = yield_host_event(budget, &time, &mut previous, None).await; + ready_items = 0; + if let Err(error) = checkpoint { + drop(source); drop(timer); drop(time); + yield Err(error); + return; + } + } + } + } + .boxed_local() + } + + pub(crate) fn native_time(clock: MonotonicClock) -> OutboundTime { + OutboundTime::new(clock, |deadline, paired_clock| { + Box::pin(async move { + let Some(remaining) = deadline.remaining_at(paired_clock.now()) else { + return Ok(()); + }; + HostEventYield::after(remaining).await + }) + }) + } + + fn checked_now( + time: &OutboundTime, + previous: &mut MonotonicInstant, + ) -> Result { + let now = time.clock().now(); + if now < *previous { + return Err(EdgeError::internal(anyhow::anyhow!( + "outbound clock moved backwards during I/O" + ))); + } + *previous = now; + Ok(now) + } + + async fn race_with_timer( + future: impl Future, + budget: DispatchBudget, + time: &OutboundTime, + timer: &mut OutboundFuture<()>, + previous: &mut MonotonicInstant, + expiry_detail: Option, + detail: impl FnOnce(&Output) -> Option, + ) -> Result { + let mut extract_detail = Some(detail); + futures_util::pin_mut!(future); + poll_fn(|cx| { + checked_now(time, previous)?; + if let Poll::Ready(result) = timer.as_mut().poll(cx) { + result?; + checked_now(time, previous)?; + return Poll::Ready(Err(preserve_timeout_detail( + timeout_error(budget.cause), + expiry_detail, + ))); + } + if budget.deadline.is_expired_at(checked_now(time, previous)?) { + return Poll::Ready(Err(preserve_timeout_detail( + timeout_error(budget.cause), + expiry_detail, + ))); + } + let output = future.as_mut().poll(cx); + if budget.deadline.is_expired_at(checked_now(time, previous)?) { + let observed_detail = if let Poll::Ready(value) = &output { + extract_detail.take().and_then(|extract| extract(value)) + } else { + None + }; + return Poll::Ready(Err(preserve_timeout_detail( + timeout_error(budget.cause), + observed_detail.or(expiry_detail), + ))); + } + output.map(Ok) + }) + .await + } + + fn worker_method(method: &Method) -> WorkerMethod { + match *method { + Method::DELETE => WorkerMethod::Delete, + Method::HEAD => WorkerMethod::Head, + Method::OPTIONS => WorkerMethod::Options, + Method::PATCH => WorkerMethod::Patch, + Method::POST => WorkerMethod::Post, + Method::PUT => WorkerMethod::Put, + _ => WorkerMethod::Get, + } + } + + /// Runs deferred upload and response stream clock checks in the hosted contract binary. + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub async fn deferred_clock_paths_hold_for_test() -> bool { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex, PoisonError}; + + use futures_util::stream::once; + + let start = MonotonicInstant::now(); + let Ok(request) = OutboundRequest::get("https://example.com/") else { + return false; + }; + let Ok(budget) = dispatch_budget(&request.timeout(Duration::from_millis(10)), start, None) + else { + return false; + }; + let observed = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&observed); + let clock = + MonotonicClock::new(move || *sampled.lock().unwrap_or_else(PoisonError::into_inner)); + let polls = Arc::new(AtomicUsize::new(0)); + let upload_clock = Arc::clone(&observed); + let upload_polls = Arc::clone(&polls); + let upload_source = once(async move { + upload_polls.fetch_add(1, Ordering::SeqCst); + *upload_clock.lock().unwrap_or_else(PoisonError::into_inner) = + budget.deadline.instant(); + Ok(Bytes::from_static(b"body")) + }) + .boxed_local(); + let mut upload = upload_stream(upload_source, 16, budget, native_time(clock.clone())); + let upload_holds = matches!( + upload.next().await, + Some(Err(EdgeError::GatewayTimeout { .. })) + ); + + let Ok(controller) = web_sys::AbortController::new() else { + return false; + }; + *observed.lock().unwrap_or_else(PoisonError::into_inner) = start; + let response_polls = Arc::clone(&polls); + let response_source = once(async move { + response_polls.fetch_add(1, Ordering::SeqCst); + *observed.lock().unwrap_or_else(PoisonError::into_inner) = budget.deadline.instant(); + Ok(Bytes::from_static(b"body")) + }) + .boxed_local(); + let Ok(mut response) = finish_fixture_stream( + response_source, + AbortGuard::new(controller), + budget, + native_time(clock), + ) + .await + else { + return false; + }; + let response_holds = matches!( + response.next().await, + Some(Err(EdgeError::GatewayTimeout { .. })) + ); + + upload_holds && response_holds && polls.load(Ordering::SeqCst) == 2 + } + + #[cfg(test)] + mod redirect_tests; + + #[cfg(test)] + mod clock_tests { + use edgezero_core::outbound::{ + HttpClient, OutboundBatchTermination, OutboundCollectedSlot, + }; + fn resolved_slot(slot: &OutboundCollectedSlot) -> &edgezero_core::OutboundSlotResult { + match slot { + OutboundCollectedSlot::Resolved(result) => result, + other @ (OutboundCollectedSlot::AlreadyDelivered + | OutboundCollectedSlot::Unresolved) => { + panic!("expected resolved slot, got {other:?}") + } + } + } + use std::collections::VecDeque; + use std::iter::repeat_with; + use std::ptr; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::{Poll, Wake}; + + use crate::test_runtime::NumericTimeoutGuard; + use edgezero_core::error::{BadGatewayDecodeReason, BudgetSource, ResponseLimitReason}; + use edgezero_core::http::header::CONTENT_TYPE; + use edgezero_core::outbound::ContentTypeProvenance; + use edgezero_core::response::IntoResponse as _; + use edgezero_core::time::DEFAULT_NO_DEADLINE_BUDGET; + use edgezero_core::time::Deadline; + use edgezero_core::{ + CloudflareFailureCause, OutboundScopeOwner, ProviderFailureDetail, ProviderFailurePhase, + }; + use futures_util::future::pending; + use futures_util::stream; + use wasm_bindgen_test::wasm_bindgen_test; + + use super::*; + + pub(super) struct RestoreGlobal(Function); + + impl RestoreGlobal { + pub(super) fn new(restore: Function) -> Self { + Self(restore) + } + } + + #[wasm_bindgen_test] + async fn streamed_send_keeps_original_scope_under_foreign_request_polling() { + let _timers = NumericTimeoutGuard::install(); + let original = OutboundScopeOwner::callback(); + let foreign = OutboundScopeOwner::callback(); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + let nested_client = client.clone(); + let nested = Rc::new(RefCell::new(None::>)); + let retained = Rc::clone(&nested); + let upload = Rc::new(()); + let released_upload = Rc::downgrade(&upload); + let child = Rc::new(()); + let released_child = Rc::downgrade(&child); + let mut pending_child = Some(child); + let body = Body::from_stream(stream::poll_fn(move |_| { + assert_eq!(Rc::strong_count(&upload), 1); + if let Some(owner) = pending_child.take() { + let source = Body::from_stream(stream::poll_fn(move |_| { + assert_eq!(Rc::strong_count(&owner), 1); + Poll::>>::Pending + })); + let send = nested_client.send( + OutboundRequest::post("https://fixture.invalid/") + .expect("child request") + .body(source), + ); + *retained.borrow_mut() = Some(send); + } + Poll::>>::Pending + })); + let mut send = original.with(|| { + client.send( + OutboundRequest::post("https://fixture.invalid/") + .expect("request") + .body(body), + ) + }); + foreign + .run(poll_fn(|cx| { + assert!(send.as_mut().poll(cx).is_pending()); + Poll::Ready(()) + })) + .await; + assert!(nested.borrow().is_some(), "upload poll creates child send"); + drop(foreign); + assert!( + released_child.upgrade().is_some(), + "foreign scope cannot cancel child" + ); + drop(original); + assert!( + released_upload.upgrade().is_none(), + "send upload is revoked" + ); + assert!( + released_child.upgrade().is_none(), + "child belongs to original scope" + ); + assert!(nested.borrow().is_some(), "child future is still retained"); + drop(send); + drop(nested.borrow_mut().take()); + } + + #[wasm_bindgen_test] + async fn scope_exit_aborts_retained_begin_and_batch_fetches_exactly_once() { + let _timers = NumericTimeoutGuard::install(); + let (probe, _restore) = fetch_probe(true); + let callback = OutboundScopeOwner::callback(); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + let (pending, batch) = callback + .run(async { + let pending = client + .begin( + OutboundRequest::get("https://fixture.invalid/begin").expect("request"), + ) + .await + .expect("begin"); + let batch = client.start_batch_until( + vec![ + OutboundRequest::get("https://fixture.invalid/first").expect("first"), + OutboundRequest::get("https://fixture.invalid/second").expect("second"), + ], + Deadline::after(Duration::from_secs(10)), + ); + HostEventYield::after(Duration::from_millis(10)) + .await + .expect("scheduled fetches"); + (pending, batch) + }) + .await; + assert_eq!( + Reflect::get(&probe, &JsValue::from_str("calls")) + .expect("calls") + .as_f64(), + Some(3.0_f64) + ); + assert_eq!( + Reflect::get(&probe, &JsValue::from_str("aborts")) + .expect("aborts") + .as_f64(), + Some(0.0_f64) + ); + drop(callback); + assert_eq!( + Reflect::get(&probe, &JsValue::from_str("aborts")) + .expect("aborts") + .as_f64(), + Some(3.0_f64) + ); + HostEventYield::after(Duration::from_millis(10)) + .await + .expect("cancelled tasks retire"); + assert!(matches!( + pending.collect().await, + Err(EdgeError::Internal { .. }) + )); + batch.collect().await.expect_err("scope revoked driver"); + assert_eq!( + Reflect::get(&probe, &JsValue::from_str("aborts")) + .expect("aborts") + .as_f64(), + Some(3.0_f64) + ); + } + + #[wasm_bindgen_test] + async fn closed_scope_rejects_scheduling_before_work_is_polled() { + let callback = OutboundScopeOwner::callback(); + callback.close(); + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let resource = Rc::new(()); + let released = Rc::downgrade(&resource); + let refused = callback.with(|| { + schedule_response(Box::pin(poll_fn(move |_| { + assert_eq!(Rc::strong_count(&resource), 1); + observed.set(observed.get().saturating_add(1)); + Poll::Pending + }))) + }); + assert!(matches!(refused, Err(EdgeError::Internal { .. }))); + assert_eq!(polls.get(), 0); + assert!(released.upgrade().is_none(), "refused work is released"); + } + + #[wasm_bindgen_test] + async fn scheduled_work_allows_reentrant_cancellation_while_polling() { + let _timers = NumericTimeoutGuard::install(); + let owner = Rc::new(RefCell::new(None::)); + let cancelled = Rc::clone(&owner); + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let work = poll_fn(move |_| { + counted.set(counted.get().saturating_add(1)); + let cancel_owner = cancelled.borrow_mut().take(); + drop(cancel_owner); + Poll::Pending + }) + .boxed_local(); + *owner.borrow_mut() = Some(spawn_owned(work).expect("local scheduler")); + HostEventYield::after(Duration::from_millis(10)) + .await + .expect("unrelated host wait"); + assert_eq!(polls.get(), 1, "cancelled work must not be polled again"); + } + + #[wasm_bindgen_test] + async fn cancelling_idle_work_retires_the_wasm_task_and_source() { + let _timers = NumericTimeoutGuard::install(); + let source = Rc::new(()); + let released = Rc::downgrade(&source); + let entered = Rc::new(Cell::new(false)); + let observed = Rc::clone(&entered); + let (runner, owner) = OutboundTask::new(Box::pin(async move { + observed.set(true); + pending::<()>().await; + drop(source); + })) + .expect("task setup"); + let running = Rc::new(()); + let retired = Rc::downgrade(&running); + spawn_local(async move { + runner.await; + drop(running); + }); + HostEventYield::after(Duration::from_millis(10)) + .await + .expect("work enters pending"); + assert!(entered.get(), "work entered pending"); + drop(owner); + assert!( + released.upgrade().is_none(), + "cancellation immediately releases the source" + ); + HostEventYield::after(Duration::from_millis(10)) + .await + .expect("cancelled task retires"); + assert!( + retired.upgrade().is_none(), + "no retained task/waker/state cycle" + ); + } + + fn fetch_probe(pending: bool) -> (JsValue, RestoreGlobal) { + let probe = Function::new_with_args("pending", "const original = globalThis.fetch; const probe = {calls:0, signal:null, aborts:0}; globalThis.fetch = (request, init) => { probe.calls++; probe.signal = init.signal; init.signal.addEventListener('abort', () => {probe.aborts++;}, {once:true}); if (init.encodeResponseBody !== 'manual' || request.redirect !== 'manual') throw new Error('manual fetch policy lost'); return pending ? new Promise(() => {}) : Promise.resolve(new Response('ready')); }; probe.restore = () => {globalThis.fetch = original;}; return probe;") + .call1(&JsValue::UNDEFINED, &JsValue::from_bool(pending)).expect("fetch probe"); + let restore = Reflect::get(&probe, &JsValue::from_str("restore")) + .expect("restore") + .dyn_into::() + .expect("restore function"); + (probe, RestoreGlobal::new(restore)) + } + + #[wasm_bindgen_test] + async fn clamped_batch_cutoff_uses_actual_observation_cutoff() { + for (elapsed_hours, unresolved) in [(168_u64, false), (192, true)] { + let start = MonotonicInstant::now(); + let now = Arc::new(Mutex::new(start)); + let clock = controlled_clock(Arc::clone(&now)); + let client = HttpClient::with_client(CloudflareOutboundClient::with_time( + native_time(clock), + )); + let cutoff = Deadline::at_instant( + start + .checked_add(Duration::from_hours(192)) + .expect("eight-day cutoff"), + ); + let requests = repeat_with(|| { + OutboundRequest::get("https://fixture.invalid/") + .expect("request") + .timeout(Duration::from_hours(192)) + }) + .take(2) + .collect(); + let batch = client.start_batch_until(requests, cutoff); + *now.lock().expect("clock") = start + .checked_add(Duration::from_hours(elapsed_hours)) + .expect("terminal observation"); + let results = batch + .collect() + .await + .expect("clamped observation remains a valid driver"); + let expected = if unresolved { + OutboundBatchTermination::Cutoff + } else { + OutboundBatchTermination::Completed + }; + assert_eq!(results.termination(), expected); + assert_eq!(results.slots().len(), 2); + for slot in results.slots() { + if unresolved { + assert!(matches!(slot, OutboundCollectedSlot::Unresolved)); + } else { + assert!( + matches!(slot, OutboundCollectedSlot::Resolved(result) if matches!(result.outcome, Err(EdgeError::GatewayTimeout { cause: BudgetSource::BatchCutoff, .. }))) + ); + } + } + } + } + + #[wasm_bindgen_test] + async fn unpolled_batch_dispatches_all_slots() { + let _timers = NumericTimeoutGuard::install(); + let (probe, _restore) = fetch_probe(false); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + let batch = client.start_batch_until( + vec![ + OutboundRequest::get("https://fixture.invalid/first").expect("first"), + OutboundRequest::get("https://fixture.invalid/second").expect("second"), + ], + Deadline::after(Duration::from_secs(1)), + ); + HostEventYield::after(Duration::from_millis(10)) + .await + .expect("unrelated host wait"); + assert_eq!( + Reflect::get(&probe, &JsValue::from_str("calls")) + .expect("calls") + .as_f64(), + Some(2.0_f64) + ); + let results = batch.collect().await.expect("batch"); + assert!(results.slots().iter().all(|slot| matches!(slot, OutboundCollectedSlot::Resolved(result) if result.outcome.is_ok()))); + } + + #[wasm_bindgen_test] + async fn begin_dispatches_without_collection_and_drop_aborts() { + let _timers = NumericTimeoutGuard::install(); + let (probe, _restore) = fetch_probe(true); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + let pending = client + .begin(OutboundRequest::get("https://fixture.invalid/").expect("request")) + .await + .expect("begin"); + HostEventYield::after(Duration::from_millis(10)) + .await + .expect("unrelated host wait"); + assert_eq!( + Reflect::get(&probe, &JsValue::from_str("calls")) + .expect("calls") + .as_f64(), + Some(1.0_f64) + ); + let signal = Reflect::get(&probe, &JsValue::from_str("signal")) + .expect("signal") + .dyn_into::() + .expect("abort signal"); + assert!(!signal.aborted()); + drop(pending); + assert!( + signal.aborted(), + "cancellation releases fetch without another guest poll" + ); + } + + #[wasm_bindgen_test] + async fn collected_begin_stream_stays_live_and_disarms_clean_eof() { + let _timers = NumericTimeoutGuard::install(); + let (probe, _restore) = fetch_probe(false); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + let pending = client + .begin( + OutboundRequest::get("https://fixture.invalid/") + .expect("request") + .stream_response(), + ) + .await + .expect("begin"); + let response = pending.collect().await.expect("live response"); + let signal = Reflect::get(&probe, &JsValue::from_str("signal")) + .expect("signal") + .dyn_into::() + .expect("abort signal"); + assert!( + !signal.aborted(), + "collect disarms pending cancellation without aborting the body" + ); + assert_eq!( + response + .into_bytes_bounded(5) + .await + .expect("complete live stream"), + "ready" + ); + assert!( + !signal.aborted(), + "clean EOF disarms the one transport guard" + ); + } + + #[wasm_bindgen_test] + async fn settlement_timer_wakes_without_body_data() { + let _timers = NumericTimeoutGuard::install(); + let (probe, _restore) = fetch_probe(false); + let reads = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&reads); + let body = Body::from_stream(stream::poll_fn(move |_| { + observed.set(observed.get().saturating_add(1)); + Poll::Pending + })); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + let error = client + .send( + OutboundRequest::get("https://fixture.invalid/") + .expect("request") + .body(body) + .timeout(Duration::from_millis(20)), + ) + .await + .expect_err("settlement timeout"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); + assert!(reads.get() > 0); + assert_eq!( + Reflect::get(&probe, &JsValue::from_str("calls")) + .expect("calls") + .as_f64(), + Some(0.0_f64) + ); + } + + #[wasm_bindgen_test] + async fn review_cross_chunk_regression_releases_retained_source() { + use std::future::pending; + + let _timers = NumericTimeoutGuard::install(); + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(10)); + let advanced = start.checked_add(Duration::from_millis(2)).unwrap(); + let regressed = start.checked_add(Duration::from_millis(1)).unwrap(); + let observed = Arc::new(Mutex::new(start)); + let pair = native_time(controlled_clock(Arc::clone(&observed))); + let owner = Arc::new(()); + let released = Arc::downgrade(&owner); + let source_clock = Arc::clone(&observed); + let source = stream! { + *source_clock.lock().unwrap() = advanced; + yield Ok(Bytes::from_static(b"first")); + yield Ok(Bytes::from_static(b"second")); + pending::<()>().await; + drop(owner); + } + .boxed_local(); + let controller = web_sys::AbortController::new().unwrap(); + let signal = controller.signal(); + let mut guarded = + finish_fixture_stream(source, AbortGuard::new(controller), budget, pair) + .await + .expect("shared response stream"); + assert_eq!( + guarded.next().await.unwrap().unwrap(), + Bytes::from_static(b"first") + ); + *observed.lock().unwrap() = regressed; + let error = guarded + .next() + .await + .unwrap() + .expect_err("cross-chunk clock regression"); + assert!(matches!(error, EdgeError::Internal { .. })); + assert!( + released.upgrade().is_none(), + "release before terminal wrapper drop" + ); + assert!(signal.aborted()); + } + + #[wasm_bindgen_test] + async fn review_host_yield_preserves_supplied_timer_failure_detail() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(10)); + let timer_detail = ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::Send, + cause: CloudflareFailureCause::AbortError, + }; + let source_detail = ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: CloudflareFailureCause::TypeError, + }; + let pair = OutboundTime::new(constant_clock(start), move |_, _| { + Box::pin(async move { + Err(preserve_timeout_detail( + EdgeError::gateway_timeout_caused( + "supplied timer failure", + BudgetSource::Unspecified, + ), + Some(timer_detail), + )) + }) + }); + let original = EdgeError::bad_gateway_with_detail( + "source failed", + BadGatewayReason::Transport, + source_detail, + ); + let mut previous = start; + let selected = + terminal_error_after_host_event(original, budget, &pair, &mut previous).await; + assert_eq!(selected.message(), "supplied timer failure"); + assert_eq!(selected.provider_failure_detail(), Some(&timer_detail)); + } + + #[wasm_bindgen_test] + async fn review_host_yield_keeps_prior_source_high_water() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(10)); + let mut previous = start.checked_add(Duration::from_millis(2)).unwrap(); + let regressed = start.checked_add(Duration::from_millis(1)).unwrap(); + let pair = native_time(constant_clock(regressed)); + let error = yield_host_event(budget, &pair, &mut previous, None) + .await + .expect_err("host yield must retain the preceding source observation"); + assert!(matches!(error, EdgeError::Internal { .. })); + } + + #[derive(Default)] + struct WakeCount(AtomicUsize); + + impl Wake for WakeCount { + fn wake(self: Arc) { + self.0.fetch_add(1, Ordering::SeqCst); + } + fn wake_by_ref(self: &Arc) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + #[wasm_bindgen_test] + fn timer_replaces_waker_and_cancels_only_pending_registration() { + let saved = Function::new_no_args("const set = globalThis.setTimeout, clear = globalThis.clearTimeout; globalThis.__edgezeroTimer = {arms:0, clears:0}; globalThis.setTimeout = (callback, milliseconds) => { const t = globalThis.__edgezeroTimer; t.arms++; t.callback=callback; t.milliseconds=milliseconds; return 123; }; globalThis.clearTimeout = () => {globalThis.__edgezeroTimer.clears++;}; return () => {globalThis.setTimeout=set; globalThis.clearTimeout=clear; delete globalThis.__edgezeroTimer;};") + .call0(&JsValue::UNDEFINED).unwrap().dyn_into::().unwrap(); + let guard = RestoreGlobal(saved); + let first = Arc::new(WakeCount::default()); + let second = Arc::new(WakeCount::default()); + let first_waker = Waker::from(Arc::clone(&first)); + let second_waker = Waker::from(Arc::clone(&second)); + let mut timer = Box::pin(HostEventYield::after(Duration::from_nanos(1))); + assert!( + timer + .as_mut() + .poll(&mut Context::from_waker(&first_waker)) + .is_pending() + ); + assert!( + timer + .as_mut() + .poll(&mut Context::from_waker(&second_waker)) + .is_pending() + ); + let state = Reflect::get(&global(), &JsValue::from_str("__edgezeroTimer")).unwrap(); + assert_eq!( + Reflect::get(&state, &JsValue::from_str("arms")) + .unwrap() + .as_f64(), + Some(1.0_f64) + ); + assert_eq!( + Reflect::get(&state, &JsValue::from_str("milliseconds")) + .unwrap() + .as_f64(), + Some(1.0_f64) + ); + let callback = Reflect::get(&state, &JsValue::from_str("callback")) + .unwrap() + .dyn_into::() + .unwrap(); + callback.call0(&JsValue::UNDEFINED).unwrap(); + assert_eq!(first.0.load(Ordering::SeqCst), 0); + assert_eq!(second.0.load(Ordering::SeqCst), 1); + assert!(matches!( + timer.as_mut().poll(&mut Context::from_waker(&second_waker)), + Poll::Ready(Ok(())) + )); + assert!(timer.callback.is_none()); + assert!(timer.waker.borrow().is_none()); + drop(timer); + assert_eq!( + Reflect::get(&state, &JsValue::from_str("clears")) + .unwrap() + .as_f64(), + Some(0.0_f64) + ); + let mut pending = Box::pin(HostEventYield::new()); + assert!( + pending + .as_mut() + .poll(&mut Context::from_waker(&first_waker)) + .is_pending() + ); + drop(pending); + assert_eq!( + Reflect::get(&state, &JsValue::from_str("clears")) + .unwrap() + .as_f64(), + Some(1.0_f64) + ); + drop(guard); + } + + #[wasm_bindgen_test] + fn timer_validates_cancellation_before_registering() { + let restore = Function::new_no_args("const set = globalThis.setTimeout, clear = globalThis.clearTimeout; globalThis.__edgezeroTimerRegistrations = 0; globalThis.setTimeout = () => { globalThis.__edgezeroTimerRegistrations++; return 123; }; globalThis.clearTimeout = undefined; return () => { globalThis.setTimeout = set; globalThis.clearTimeout = clear; delete globalThis.__edgezeroTimerRegistrations; };") + .call0(&JsValue::UNDEFINED).unwrap().dyn_into::().unwrap(); + let guard = RestoreGlobal(restore); + let mut timer = Box::pin(HostEventYield::new()); + let outcome = timer.as_mut().poll(&mut Context::from_waker(Waker::noop())); + let registrations = Reflect::get( + &global(), + &JsValue::from_str("__edgezeroTimerRegistrations"), + ) + .unwrap(); + drop(timer); + drop(guard); + assert!(matches!( + outcome, + Poll::Ready(Err(EdgeError::Internal { .. })) + )); + assert_eq!(registrations.as_f64(), Some(0.0_f64)); + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for RestoreGlobal { + fn drop(&mut self) { + let _restored = self.0.call0(&JsValue::UNDEFINED); + } + } + + fn assert_same_public_error(plain: EdgeError, diagnosed: EdgeError) { + assert_eq!(plain.to_string(), diagnosed.to_string()); + assert_eq!(plain.message(), diagnosed.message()); + assert_eq!(plain.kind(), diagnosed.kind()); + assert_eq!(plain.status(), diagnosed.status()); + assert!( + !format!("{:?}", diagnosed.provider_failure_detail()) + .contains("provider-secret-token") + ); + let before = plain.into_response().expect("plain wire"); + let after = diagnosed.into_response().expect("diagnosed wire"); + assert_eq!(before.status(), after.status()); + assert_eq!(before.headers(), after.headers()); + assert_eq!(before.body().as_bytes(), after.body().as_bytes()); + } + + #[wasm_bindgen_test] + fn provider_diagnostics_preserve_preparation_errors_without_changing_sources() { + let mut headers = HeaderMap::new(); + headers.insert( + "x-test", + HeaderValue::from_bytes("\u{100}provider-secret-token".as_bytes()) + .expect("valid raw header"), + ); + let header_error = build_headers(&headers).expect_err("Workers header rejection"); + let detail = header_error + .provider_failure_detail() + .expect("header detail"); + assert_eq!( + *detail, + ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::RequestPreparation, + cause: CloudflareFailureCause::TypeError, + } + ); + assert!(header_error.inner().expect("source").is::()); + let header_source = header_error.inner().expect("source").to_string(); + assert_same_public_error( + EdgeError::internal(anyhow::anyhow!(header_source)), + header_error, + ); + + let request_error = build_worker_request( + &Method::GET, + "https://example.com/?token=provider-secret-token", + &HeaderMap::new(), + Some(JsValue::from_str("body")), + OutboundCachePolicy::PlatformDefault, + ) + .expect_err("GET body rejection"); + assert_eq!( + request_error.provider_failure_detail(), + Some(&ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::RequestPreparation, + cause: CloudflareFailureCause::Unknown, + }) + ); + assert!(request_error.inner().expect("source").is::()); + let request_source = request_error.inner().expect("source").to_string(); + assert_same_public_error( + EdgeError::internal(anyhow::anyhow!(request_source)), + request_error, + ); + + headers.insert( + "x-test", + HeaderValue::from_bytes(b"\xff").expect("raw header"), + ); + let invalid = build_headers(&headers).expect_err("non-UTF8 rejection"); + assert!(invalid.provider_failure_detail().is_none()); + assert_eq!(invalid.status(), StatusCode::BAD_REQUEST); + + let mut response_headers = Headers::new(); + headers.insert( + "x-test", + HeaderValue::from_bytes("\u{100}".as_bytes()).expect("raw header"), + ); + let response_error = + super::super::copy_header_values(&headers, &mut response_headers, |_name| { + EdgeError::internal(anyhow::anyhow!("invalid response header")) + }) + .expect_err("shared sink rejection"); + assert!(response_error.provider_failure_detail().is_none()); + } + + #[wasm_bindgen_test] + fn provider_diagnostics_keep_original_worker_source_identity() { + let original = + EdgeError::internal(worker::Error::RustError("provider-secret-token".to_owned())); + let pointer = ptr::from_ref( + original + .inner() + .expect("source") + .downcast_ref::() + .expect("worker source"), + ); + let mapped = diagnose_worker_preparation_error(original); + let retained = mapped + .inner() + .expect("source") + .downcast_ref::() + .expect("retained worker source"); + assert!(ptr::eq(pointer, retained)); + assert!( + matches!(retained, worker::Error::RustError(text) if text == "provider-secret-token") + ); + assert_eq!( + mapped.provider_failure_detail(), + Some(&ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::RequestPreparation, + cause: CloudflareFailureCause::Unknown, + }) + ); + assert_same_public_error( + EdgeError::internal(worker::Error::RustError("provider-secret-token".to_owned())), + mapped, + ); + } + + #[wasm_bindgen_test] + async fn provider_diagnostics_preserve_preparation_setup_exceptions() { + for (setup, message, expected) in [ + ( + "const old = globalThis.AbortController; globalThis.AbortController = class { constructor() { throw new TypeError('provider-secret-token'); } }; return () => { globalThis.AbortController = old; };", + "failed to construct abort controller", + Some(CloudflareFailureCause::TypeError), + ), + ( + "const old = Reflect.set; Reflect.set = () => { throw new DOMException('provider-secret-token', 'AbortError'); }; return () => { Reflect.set = old; };", + "failed to set manual response encoding", + Some(CloudflareFailureCause::AbortError), + ), + ( + "const old = Reflect.set; Reflect.set = () => false; return () => { Reflect.set = old; };", + "runtime refused manual response encoding", + None, + ), + ] { + let request = build_worker_request( + &Method::GET, + "https://example.com/?token=provider-secret-token", + &HeaderMap::new(), + None, + OutboundCachePolicy::PlatformDefault, + ) + .expect("valid request"); + let start = MonotonicInstant::now(); + let budget = DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant( + start.checked_add(Duration::from_secs(1)).expect("deadline"), + ), + duration: Duration::from_secs(1), + }; + let outcome = { + let restore: Function = Function::new_no_args(setup) + .call0(&JsValue::UNDEFINED) + .expect("patch setup") + .dyn_into() + .expect("restore function"); + let _restore = RestoreGlobal::new(restore); + raw_fetch( + request, + budget, + &native_time(MonotonicClock::new(move || start)), + ) + .await + }; + let error = outcome.err().expect("setup refusal"); + assert_eq!( + error.provider_failure_detail(), + expected + .map(|cause| ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::RequestPreparation, + cause, + }) + .as_ref() + ); + assert_same_public_error(EdgeError::internal(anyhow::anyhow!(message)), error); + } + } + + #[wasm_bindgen_test] + fn provider_diagnostics_fetch_settlement_keeps_only_observed_failures() { + let value = Function::new_no_args("return new TypeError('provider-secret-token');") + .call0(&JsValue::UNDEFINED) + .expect("exception"); + let failed = finish_fetch_result( + Err(value), + Err(timeout_error(BudgetSource::RequestDeadline)), + ) + .expect_err("deadline wins"); + assert_eq!( + failed.provider_failure_detail(), + Some(&ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::Send, + cause: CloudflareFailureCause::TypeError, + }) + ); + assert!(matches!( + failed, + EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + } + )); + let late_ok = finish_fetch_result( + Ok(JsValue::UNDEFINED), + Err(timeout_error(BudgetSource::RequestDeadline)), + ) + .expect_err("late success"); + assert!(late_ok.provider_failure_detail().is_none()); + } + + #[wasm_bindgen_test] + async fn provider_diagnostics_survive_reset_content_deadline_checks() { + let _timer_guard = NumericTimeoutGuard::install(); + for during_yield in [false, true] { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + let observed = Arc::new(Mutex::new(start)); + let pair = native_time(controlled_clock(Arc::clone(&observed))); + let late = Arc::clone(&observed); + let classify = Closure::::new(move || { + *late.lock().expect("clock") = budget.deadline.instant(); + }); + let raw = Function::new_with_args("classify, delayed", "const error = new Error('provider-secret-token'); Object.defineProperty(error, 'name', {get() {classify(); return 'TypeError';}}); return new Response(new ReadableStream({start(c) {if (delayed) setTimeout(() => c.error(error), 0); else c.error(error);}}));") + .call2(&JsValue::UNDEFINED, classify.as_ref(), &JsValue::from_bool(during_yield)).expect("error response") + .dyn_into::().expect("native response"); + let native = WorkerResponse::from(raw).with_status(205); + let result = finish_fixture_response( + native, + AbortGuard::new(web_sys::AbortController::new().expect("controller")), + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Streamed, + budget, + 32 * 1024 * 1024, + 24, + None, + None, + None, + None, + None, + pair, + ) + .await; + let error = result.expect_err("expiry overrides body failure"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); + assert_eq!( + error.provider_failure_detail(), + Some(&ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: CloudflareFailureCause::TypeError, + }) + ); + } + } + + #[wasm_bindgen_test] + async fn provider_diagnostics_survive_post_ready_expiry_and_host_yield() { + let _timer_guard = NumericTimeoutGuard::install(); + let detail = ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: CloudflareFailureCause::TypeError, + }; + for upload in [false, true] { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + let observed = Arc::new(Mutex::new(start)); + let clock = controlled_clock(Arc::clone(&observed)); + let source = stream::once(async move { + *observed.lock().expect("clock") = budget.deadline.instant(); + Err(EdgeError::bad_gateway_with_detail( + "source failed", + BadGatewayReason::Transport, + detail, + )) + }) + .boxed_local(); + let mut wrapped = if upload { + upload_stream(source, 32, budget, native_time(clock)) + } else { + finish_fixture_stream( + source, + AbortGuard::new(web_sys::AbortController::new().expect("controller")), + budget, + native_time(clock), + ) + .await + .expect("shared response stream") + }; + let error = wrapped.next().await.expect("terminal").expect_err("expiry"); + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + assert_eq!(error.provider_failure_detail(), Some(&detail)); + } + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + let clock = constant_clock(budget.deadline.instant()); + let mut previous = start; + let error = terminal_error_after_host_event( + EdgeError::bad_gateway_with_detail( + "source failed", + BadGatewayReason::Transport, + detail, + ), + budget, + &native_time(clock), + &mut previous, + ) + .await; + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + assert_eq!(error.provider_failure_detail(), Some(&detail)); + } + + #[wasm_bindgen_test] + fn provider_diagnostics_fetch_names_are_closed_and_never_infer_timeouts() { + for (script, expected) in [ + ( + "return new TypeError('provider-secret-token');", + CloudflareFailureCause::TypeError, + ), + ( + "return Object.assign(new Error('provider-secret-token'), {name:'AbortError'});", + CloudflareFailureCause::AbortError, + ), + ( + "return Object.assign(new Error('provider-secret-token'), {name:'secret-name'});", + CloudflareFailureCause::Unknown, + ), + ( + "return Object.assign(new Error('provider-secret-token'), {name:17});", + CloudflareFailureCause::Unknown, + ), + ( + "return Object.defineProperty({}, 'name', {get() {throw new Error('getter-secret')}});", + CloudflareFailureCause::Unknown, + ), + ( + "return 'TypeError: provider-secret-token';", + CloudflareFailureCause::Unknown, + ), + ] { + let value = Function::new_no_args(script) + .call0(&JsValue::UNDEFINED) + .expect("fixture value"); + let mapped = map_fetch_failure(&value); + assert_eq!( + mapped.provider_failure_detail(), + Some(&ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::Send, + cause: expected, + }) + ); + assert!(matches!( + mapped, + EdgeError::BadGateway { + reason: BadGatewayReason::Unspecified, + .. + } + )); + assert_eq!(mapped.message(), "outbound fetch failed"); + assert_eq!(mapped.to_string(), "outbound fetch failed"); + assert!(!format!("{mapped:?}").contains("secret")); + assert_eq!( + worker_failure_cause(&worker::Error::Internal(value)), + expected + ); + } + for (name, expected) in [ + ("TypeError", CloudflareFailureCause::TypeError), + ("AbortError", CloudflareFailureCause::AbortError), + ] { + let value = Function::new_with_args( + "name", + "return Object.assign(new Error('provider-secret-token'), {name});", + ) + .call1(&JsValue::UNDEFINED, &JsValue::from_str(name)) + .expect("actual Error"); + assert_eq!(worker_failure_cause(&worker::Error::from(value)), expected); + } + for error in [ + worker::Error::JsError("TypeError: provider-secret-token".to_owned()), + worker::Error::RustError("AbortError: provider-secret-token".to_owned()), + ] { + assert_eq!( + worker_failure_cause(&error), + CloudflareFailureCause::Unknown + ); + } + } + + #[wasm_bindgen_test] + async fn provider_diagnostics_preserve_response_stream_exception_without_text() { + for (name, expected) in [ + ("TypeError", CloudflareFailureCause::TypeError), + ("AbortError", CloudflareFailureCause::AbortError), + ("secret-name", CloudflareFailureCause::Unknown), + ] { + let factory = Function::new_with_args( + "name", + "return new Response(new ReadableStream({ start(controller) { const error = new Error('provider-secret-token'); error.name = name; controller.error(error); } }));", + ); + let native = factory + .call1(&JsValue::UNDEFINED, &JsValue::from_str(name)) + .expect("raw response") + .dyn_into::() + .expect("response type"); + let mut response = WorkerResponse::from(native); + let mut source = response_stream(&mut response).expect("stream acquisition"); + let mapped = source + .next() + .await + .expect("terminal error") + .expect_err("upstream failure"); + assert_eq!( + mapped.provider_failure_detail(), + Some(&ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: expected, + }) + ); + let plain = EdgeError::bad_gateway_with_reason( + "upstream response body failed", + BadGatewayReason::Transport, + ); + assert_eq!(plain.to_string(), mapped.to_string()); + assert_eq!(plain.message(), mapped.message()); + assert_eq!(plain.status(), mapped.status()); + assert_eq!(plain.kind(), mapped.kind()); + assert!(!format!("{:?}", mapped.provider_failure_detail()).contains("secret")); + let before = plain.into_response().expect("plain wire"); + let after = mapped.into_response().expect("diagnosed wire"); + assert_eq!(before.status(), after.status()); + assert_eq!(before.headers(), after.headers()); + assert_eq!(before.body().as_bytes(), after.body().as_bytes()); + } + } + + fn scripted_clock(script: Vec) -> MonotonicClock { + let observations = Arc::new(Mutex::new(VecDeque::from(script))); + MonotonicClock::new(move || { + let mut samples = observations.lock().expect("clock observations"); + let now = *samples.front().expect("clock observation"); + if samples.len() > 1 { + samples.pop_front(); + } + now + }) + } + + fn constant_clock(now: MonotonicInstant) -> MonotonicClock { + MonotonicClock::new(move || now) + } + + fn controlled_clock(observed: Arc>) -> MonotonicClock { + MonotonicClock::new(move || *observed.lock().expect("clock")) + } + + fn expire_at_host_yield( + budget: DispatchBudget, + observed: Arc>, + ) -> OutboundTime { + use std::sync::atomic::{AtomicUsize, Ordering}; + let arms = Arc::new(AtomicUsize::new(0)); + OutboundTime::new( + controlled_clock(Arc::clone(&observed)), + move |_deadline, _clock| { + let during_yield = arms.fetch_add(1, Ordering::SeqCst) > 0; + let late = Arc::clone(&observed); + Box::pin(async move { + if during_yield { + HostEventYield::new().await?; + *late.lock().expect("clock") = budget.deadline.instant(); + } else { + HostEventYield::after(budget.duration).await?; + } + Ok(()) + }) + }, + ) + } + + fn empty_worker_response( + status: StatusCode, + content_length: Option<&str>, + ) -> WorkerResponse { + let headers = Headers::new(); + if let Some(value) = content_length { + headers + .set("content-length", value) + .expect("content-length"); + } + WorkerResponse::empty() + .expect("empty response") + .with_status(status.as_u16()) + .with_headers(headers) + } + + fn test_budget(start: MonotonicInstant, duration: Duration) -> DispatchBudget { + DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant(start.checked_add(duration).expect("deadline")), + duration, + } + } + + #[wasm_bindgen_test] + async fn upload_cap_boundaries_preserve_typed_origins() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + for len in [7_usize, 8, 9] { + let clock = MonotonicClock::new(move || start); + let source = stream::iter([Ok(Bytes::from(vec![b'x'; len]))]).boxed_local(); + let mut bounded = upload_stream(source, 8, budget, native_time(clock)); + let result = bounded.next().await.expect("upload item"); + if len > 8 { + let error = result.expect_err("upload overflow"); + assert_eq!(error.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!( + error.request_body_overflow_origin(), + Some(edgezero_core::RequestBodyOverflowOrigin::OutboundUpload) + ); + } else { + assert_eq!(result.expect("body").len(), len); + } + } + let source_error = + EdgeError::payload_too_large(edgezero_core::RequestBodyOverflowOrigin::Inbound); + let mut source = upload_stream( + stream::iter([Err(source_error)]).boxed_local(), + 8, + budget, + native_time(MonotonicClock::new(move || start)), + ); + let error = source + .next() + .await + .expect("item") + .expect_err("source overflow"); + assert_eq!( + error.request_body_overflow_origin(), + Some(edgezero_core::RequestBodyOverflowOrigin::Inbound) + ); + } + + #[wasm_bindgen_test] + fn raw_fetch_timer_arms_from_final_remaining_budget() { + let start = MonotonicInstant::now(); + let observed = start + .checked_add(Duration::from_millis(3)) + .expect("observed instant"); + let budget = test_budget(start, Duration::from_millis(10)); + let sampled = Arc::new(Mutex::new(false)); + let sampled_by_clock = Arc::clone(&sampled); + let clock = MonotonicClock::new(move || { + *sampled_by_clock.lock().expect("sampled lock") = true; + observed + }); + + let (value, remaining) = begin_fetch(budget, &clock, || { + assert!( + *sampled.lock().expect("sampled lock"), + "budget must be sampled before dispatch" + ); + 42_u8 + }) + .expect("live budget"); + + assert_eq!(value, 42); + assert_eq!(remaining, Duration::from_millis(7)); + } + + #[wasm_bindgen_test] + async fn method_entry_and_preflight_elapsed_use_the_injected_clock() { + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(9)) + .expect("completed instant"); + let client = HttpClient::with_client(CloudflareOutboundClient::with_time(native_time( + scripted_clock(vec![start, completed]), + ))); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + + let results = client + .start_batch_until( + vec![request], + Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("cutoff instant"), + ), + ) + .collect() + .await + .expect("valid batch driver"); + + let slot = resolved_slot(&results.slots()[0]); + assert_eq!(slot.elapsed, Duration::from_millis(9)); + assert!(matches!(slot.outcome, Err(EdgeError::BadRequest { .. }))); + } + + #[wasm_bindgen_test] + async fn backwards_clock_fails_slot_without_invalid_elapsed() { + let start = MonotonicInstant::now(); + let earlier = start + .checked_sub(Duration::from_millis(1)) + .expect("earlier instant"); + let client = HttpClient::with_client(CloudflareOutboundClient::with_time(native_time( + scripted_clock(vec![start, earlier]), + ))); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + + let results = client + .start_batch_until( + vec![request], + Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("cutoff instant"), + ), + ) + .collect() + .await + .expect("valid batch driver"); + + let slot = resolved_slot(&results.slots()[0]); + assert_eq!(slot.elapsed, Duration::ZERO); + assert!(matches!(slot.outcome, Err(EdgeError::Internal { .. }))); + } + + #[wasm_bindgen_test] + fn backwards_clock_cannot_expand_the_selected_budget() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let earlier = start + .checked_sub(Duration::from_millis(5)) + .expect("earlier instant"); + let clock = scripted_clock(vec![earlier]); + + assert_eq!( + budget_remaining(budget, &clock).expect("remaining budget"), + budget.duration + ); + } + + #[wasm_bindgen_test] + async fn buffered_request_preparation_reduces_the_remaining_injected_budget() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let observed = start + .checked_add(Duration::from_millis(3)) + .expect("observed instant"); + let clock = scripted_clock(vec![observed, observed]); + let body = request_body(Body::from("body"), 16, budget, native_time(clock.clone())) + .await + .expect("prepared body"); + let remaining = budget_remaining(budget, &clock).expect("remaining budget"); + + assert!(body.is_some()); + assert_eq!(remaining, Duration::from_millis(7)); + } + + #[wasm_bindgen_test] + async fn streamed_request_body_is_drained_before_fetch_construction() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let polls = Arc::new(AtomicUsize::new(0)); + let observed_polls = Arc::clone(&polls); + let source = stream::poll_fn(move |_context| { + let poll = observed_polls.fetch_add(1, Ordering::SeqCst); + if poll == 0 { + Poll::Ready(Some(Bytes::from_static(b"body"))) + } else { + Poll::Ready(None) + } + }) + .boxed_local(); + + let body = request_body( + Body::stream(source), + 16, + budget, + native_time(constant_clock(start)), + ) + .await + .expect("prepared body"); + + assert!(body.is_some()); + assert_eq!(polls.load(Ordering::SeqCst), 2); + } + + #[wasm_bindgen_test] + async fn streamed_upload_checks_injected_clock_after_ready_item() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let observed = Arc::new(Mutex::new(start)); + let clock = controlled_clock(Arc::clone(&observed)); + let source = stream::once(async move { + *observed.lock().expect("clock") = budget.deadline.instant(); + Ok(Bytes::from_static(b"body")) + }) + .boxed_local(); + let mut body = upload_stream(source, 16, budget, native_time(clock)); + + let error = body + .next() + .await + .expect("terminal item") + .expect_err("post-ready expiry"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[wasm_bindgen_test] + async fn streamed_upload_cap_error_yields_before_final_precedence() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let pair = expire_at_host_yield(budget, Arc::new(Mutex::new(start))); + let source = stream::once(async { Ok(Bytes::from_static(b"too large")) }).boxed_local(); + let mut body = upload_stream(source, 1, budget, pair); + + let error = body + .next() + .await + .expect("terminal item") + .expect_err("host-event deadline wins over cap error"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[wasm_bindgen_test] + async fn streamed_upload_eof_yields_before_final_precedence() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let pair = expire_at_host_yield(budget, Arc::new(Mutex::new(start))); + let mut body = upload_stream(stream::empty().boxed_local(), 1, budget, pair); + + let error = body + .next() + .await + .expect("deadline after host-event yield") + .expect_err("host-event deadline wins over EOF"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[wasm_bindgen_test] + fn streamed_upload_fairness_quota_is_at_most_sixty_four() { + const { assert!(READY_ITEM_YIELD_QUOTA <= 64) }; + } + + #[wasm_bindgen_test] + async fn streamed_upload_source_error_yields_before_final_precedence() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let pair = expire_at_host_yield(budget, Arc::new(Mutex::new(start))); + let source = + stream::once(async { Err(EdgeError::bad_gateway("source failed")) }).boxed_local(); + let mut body = upload_stream(source, 1, budget, pair); + + let error = body + .next() + .await + .expect("terminal item") + .expect_err("host-event deadline wins over source error"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[wasm_bindgen_test] + async fn native_response_terminal_paths_preserve_core_errors_and_abort_ownership() { + let _timers = NumericTimeoutGuard::install(); + for (script, mode, decoded_cap, encoded_cap, reason, limit, aborted) in [ + ( + "return new Response('ok');", + ResponseMode::Buffered { max_bytes: 8 }, + None, + None, + None, + None, + false, + ), + ( + "return new Response('ok');", + ResponseMode::Streamed, + None, + None, + None, + None, + true, + ), + ( + "return new Response(new ReadableStream({pull(c) {c.error(new TypeError('provider-secret-token'));}}));", + ResponseMode::Buffered { max_bytes: 8 }, + None, + None, + Some(BadGatewayReason::Transport), + None, + true, + ), + ( + "return new Response(new Uint8Array([31, 139]), {headers: {'content-encoding': 'gzip'}});", + ResponseMode::Buffered { max_bytes: 8 }, + None, + None, + Some(BadGatewayReason::Decode(BadGatewayDecodeReason::Gzip)), + None, + false, + ), + ( + "return new Response('ok');", + ResponseMode::Buffered { max_bytes: 8 }, + None, + Some(1), + None, + Some(ResponseLimitReason::EncodedBody), + true, + ), + ( + "return new Response('ok');", + ResponseMode::Buffered { max_bytes: 8 }, + Some(1), + None, + None, + Some(ResponseLimitReason::DecodedBody), + true, + ), + ( + "return new Response('ok');", + ResponseMode::Buffered { max_bytes: 1 }, + None, + None, + None, + Some(ResponseLimitReason::BufferedBody), + true, + ), + ] { + assert_native_response_terminal_path( + script, + mode, + decoded_cap, + encoded_cap, + reason, + limit, + aborted, + ) + .await; + } + } + + async fn assert_native_response_terminal_path( + script: &str, + mode: ResponseMode, + decoded_cap: Option, + encoded_cap: Option, + reason: Option, + limit: Option, + aborted: bool, + ) { + let start = MonotonicInstant::now(); + let raw = Function::new_no_args(script) + .call0(&JsValue::UNDEFINED) + .expect("native response") + .dyn_into::() + .expect("response type"); + let controller = web_sys::AbortController::new().expect("controller"); + let signal = controller.signal(); + let aborts = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&aborts); + let listener = Closure::::new(move || { + observed.set(observed.get().saturating_add(1)); + }); + signal + .add_event_listener_with_callback("abort", listener.as_ref().unchecked_ref()) + .expect("abort listener"); + let result = finish_fixture_response( + WorkerResponse::from(raw), + AbortGuard::new(controller), + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + mode, + test_budget(start, Duration::from_secs(1)), + 32 * 1024 * 1024, + 24, + None, + decoded_cap, + encoded_cap, + None, + None, + native_time(constant_clock(start)), + ) + .await; + if let Some(expected) = reason { + let error = result.expect_err("shared finalizer rejects native body"); + assert!( + matches!(error, EdgeError::BadGateway {reason: actual, ..} if actual == expected), + "{error:?}" + ); + if expected == BadGatewayReason::Transport { + assert_eq!( + error.provider_failure_detail(), + Some(&ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: CloudflareFailureCause::TypeError, + }) + ); + } else { + assert!(error.provider_failure_detail().is_none()); + } + } else if let Some(expected) = limit { + let error = result.expect_err("shared finalizer bounds native body"); + assert!( + matches!(error, EdgeError::ResponseTooLarge { reason: actual, .. } if actual == expected), + "{error:?}" + ); + assert!(error.provider_failure_detail().is_none()); + } else { + let response = result.expect("response"); + if mode == ResponseMode::Streamed { + assert!( + !signal.aborted(), + "live body is not cancelled at head handoff" + ); + } else { + assert_eq!(response.body().as_bytes(), Some(b"ok".as_slice())); + } + drop(response); + } + assert_eq!(signal.aborted(), aborted); + assert_eq!(aborts.get(), usize::from(aborted)); + signal + .remove_event_listener_with_callback("abort", listener.as_ref().unchecked_ref()) + .expect("remove listener"); + } + + #[wasm_bindgen_test] + async fn reset_content_inspects_only_undeclared_native_streams() { + let _timers = NumericTimeoutGuard::install(); + for declared_body in [false, true] { + for native_body_is_empty in [false, true] { + let start = MonotonicInstant::now(); + let probe = Function::new_no_args("const probe = {pulls: 0}; probe.response = new Response(new ReadableStream({pull(c) {probe.pulls++; c.close();}}, {highWaterMark: 0})); return probe;") + .call0(&JsValue::UNDEFINED).expect("source probe"); + let native = if native_body_is_empty { + empty_worker_response(StatusCode::RESET_CONTENT, None) + } else { + let raw = Reflect::get(&probe, &JsValue::from_str("response")) + .expect("response") + .dyn_into::() + .expect("response type"); + WorkerResponse::from(raw).with_status(205) + }; + let headers = Headers::new(); + if declared_body { + headers.set("content-length", "7").expect("declared body"); + } + let controller = web_sys::AbortController::new().expect("controller"); + let signal = controller.signal(); + let response = finish_fixture_response( + native.with_headers(headers), + AbortGuard::new(controller), + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Buffered { max_bytes: 8 }, + test_budget(start, Duration::from_secs(1)), + 1024, + 24, + None, + None, + None, + None, + None, + native_time(constant_clock(start)), + ) + .await + .expect("reset response"); + assert_eq!(response.status(), StatusCode::RESET_CONTENT); + assert_eq!(response.body().as_bytes(), Some(b"".as_slice())); + assert_eq!(signal.aborted(), declared_body); + assert_eq!( + Reflect::get(&probe, &JsValue::from_str("pulls")) + .expect("pulls") + .as_f64(), + Some(f64::from(!declared_body && !native_body_is_empty)) + ); + } + } + } + + #[wasm_bindgen_test] + async fn response_stream_uses_only_the_shared_core_deadline_timer() { + let _timers = NumericTimeoutGuard::install(); + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + let timers = Arc::new(AtomicUsize::new(0)); + let registered = Arc::clone(&timers); + let pair = OutboundTime::new(constant_clock(start), move |_, _| { + registered.fetch_add(1, Ordering::SeqCst); + Box::pin(pending()) + }); + let controller = web_sys::AbortController::new().expect("abort controller"); + let signal = controller.signal(); + let raw = Function::new_no_args("return new Response('live');") + .call0(&JsValue::UNDEFINED) + .expect("native response") + .dyn_into::() + .expect("response type"); + let native = WorkerResponse::from(raw); + let response = finish_fixture_response( + native, + AbortGuard::new(controller), + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Streamed, + budget, + 1024, + 24, + None, + None, + None, + None, + None, + pair, + ) + .await + .expect("streamed response"); + assert_eq!(response.into_bytes_bounded(4).await.expect("body"), "live"); + assert_eq!( + timers.load(Ordering::SeqCst), + 1, + "only core owns the response deadline wait" + ); + assert!(!signal.aborted(), "clean EOF disarms the SDK resource"); + } + + #[wasm_bindgen_test] + async fn response_stream_retains_clock_for_post_ready_expiry() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let observed = Arc::new(Mutex::new(start)); + let clock = controlled_clock(Arc::clone(&observed)); + let source = stream::once(async move { + *observed.lock().expect("clock") = budget.deadline.instant(); + Ok(Bytes::from_static(b"body")) + }) + .boxed_local(); + let controller = web_sys::AbortController::new().expect("abort controller"); + let mut body = finish_fixture_stream( + source, + AbortGuard::new(controller), + budget, + native_time(clock), + ) + .await + .expect("shared response stream"); + + let error = body + .next() + .await + .expect("terminal item") + .expect_err("post-ready expiry"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[wasm_bindgen_test] + async fn converted_response_retains_injected_clock() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + let observed = Arc::new(Mutex::new(start)); + let clock_observed = Arc::clone(&observed); + let clock = + MonotonicClock::new(move || *clock_observed.lock().expect("clock observation")); + let controller = web_sys::AbortController::new().expect("abort controller"); + let response = finish_fixture_response( + empty_worker_response(StatusCode::NO_CONTENT, None), + AbortGuard::new(controller), + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Streamed, + budget, + 32 * 1024 * 1024, + 24, + None, + None, + None, + None, + None, + native_time(clock), + ) + .await + .expect("converted response"); + *observed.lock().expect("clock observation") = budget.deadline.instant(); + + let error = response + .into_bytes_bounded_until(1, budget.deadline) + .await + .expect_err("retained clock reached deadline"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[wasm_bindgen_test] + async fn converted_response_content_type_provenance_is_always_unknown() { + let _timer_guard = NumericTimeoutGuard::install(); + let cases: &[&[&str]] = &[ + &[], + &["text/plain"], + &["text/plain", "text/plain"], + &["text/plain", "application/json"], + &["text/plain, application/json"], + ]; + for values in cases { + for status in [ + StatusCode::OK, + StatusCode::NO_CONTENT, + StatusCode::RESET_CONTENT, + ] { + for mode in [ + ResponseMode::Streamed, + ResponseMode::Buffered { max_bytes: 64 }, + ] { + let start = MonotonicInstant::now(); + let headers = Headers::new(); + for value in *values { + headers + .append(CONTENT_TYPE.as_str(), value) + .expect("content-type field"); + } + let native = WorkerResponse::from_stream(stream::empty::< + Result, worker::Error>, + >()) + .expect("streaming response") + .with_status(status.as_u16()) + .with_headers(headers); + let bridged = response_headers(&native).expect("bridged headers"); + assert_eq!( + bridged.get_all(CONTENT_TYPE).iter().count(), + usize::from(!values.is_empty()) + ); + if values.len() > 1 { + assert_eq!( + bridged.get(CONTENT_TYPE).map(HeaderValue::as_bytes), + Some(values.join(", ").as_bytes()) + ); + } + let controller = web_sys::AbortController::new().expect("abort controller"); + let mut response = finish_fixture_response( + native, + AbortGuard::new(controller), + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + mode, + test_budget(start, Duration::from_secs(1)), + 1024, + 24, + None, + None, + None, + None, + None, + native_time(constant_clock(start)), + ) + .await + .expect("converted response"); + + assert_eq!( + response.content_type_provenance(), + ContentTypeProvenance::Unknown + ); + response + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + assert_eq!( + response.content_type_provenance(), + ContentTypeProvenance::Unknown + ); + } + } + } + } + + #[wasm_bindgen_test] + async fn null_reset_content_disarms_only_without_a_declared_body() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + for (content_length, expected_aborted) in + [(None, false), (Some("0"), false), (Some("7"), true)] + { + for observed in [ + start, + budget.deadline.instant(), + budget + .deadline + .instant() + .checked_add(Duration::from_nanos(1)) + .expect("after deadline"), + ] { + let controller = web_sys::AbortController::new().expect("abort controller"); + let signal = controller.signal(); + let result = finish_fixture_response( + empty_worker_response(StatusCode::RESET_CONTENT, content_length), + AbortGuard::new(controller), + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Buffered { max_bytes: 64 }, + budget, + 1024, + 24, + None, + None, + None, + None, + None, + native_time(scripted_clock(vec![start, observed])), + ) + .await; + + if observed < budget.deadline.instant() { + let response = result.expect("empty reset response"); + assert_eq!(response.status(), StatusCode::RESET_CONTENT); + assert_eq!(response.body().as_bytes(), Some(&[][..])); + } else { + assert!( + matches!( + result, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + ), + "empty reset handoff must retain its deadline" + ); + } + let expired_before_head = observed >= budget.deadline.instant(); + assert_eq!( + signal.aborted(), + expected_aborted || expired_before_head, + "preparation expiry releases an unhanded native response; length={content_length:?}, observed={observed:?}" + ); + } + } + } + + #[wasm_bindgen_test] + async fn null_reset_content_keeps_clean_disarm_when_final_handoff_expires() { + struct ExpireOnDrop { + observed: Arc>, + deadline: MonotonicInstant, + } + impl Future for ExpireOnDrop { + type Output = Result<(), EdgeError>; + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + Poll::Pending + } + } + #[expect( + clippy::missing_trait_methods, + reason = "Drop::pin_drop is unstable and conflicts with drop" + )] + impl Drop for ExpireOnDrop { + fn drop(&mut self) { + *self.observed.lock().expect("clock") = self.deadline; + } + } + let _timers = NumericTimeoutGuard::install(); + for content_length in [None, Some("0")] { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + let observed = Arc::new(Mutex::new(start)); + let expiry = Arc::clone(&observed); + let pair = OutboundTime::new(controlled_clock(observed), move |_, _| { + Box::pin(ExpireOnDrop { + observed: Arc::clone(&expiry), + deadline: budget.deadline.instant(), + }) + }); + let controller = web_sys::AbortController::new().expect("controller"); + let signal = controller.signal(); + let result = finish_fixture_response( + empty_worker_response(StatusCode::RESET_CONTENT, content_length), + AbortGuard::new(controller), + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Buffered { max_bytes: 64 }, + budget, + 1024, + 24, + None, + None, + None, + None, + None, + pair, + ) + .await; + assert!(matches!( + result, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + )); + assert!( + !signal.aborted(), + "clean transport stays disarmed through a later handoff timeout" + ); + } + } + + #[wasm_bindgen_test] + async fn response_conversion_checks_deadline_before_handoff() { + for (method, status) in [ + (Method::HEAD, StatusCode::OK), + (Method::GET, StatusCode::NOT_MODIFIED), + (Method::GET, StatusCode::NO_CONTENT), + (Method::GET, StatusCode::RESET_CONTENT), + (Method::GET, StatusCode::OK), + ] { + let modes: &[ResponseMode] = if method == Method::GET && status == StatusCode::OK { + &[ResponseMode::Streamed] + } else { + &[ + ResponseMode::Buffered { max_bytes: 1 }, + ResponseMode::Streamed, + ] + }; + for &response_mode in modes { + for cause in [ + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::BatchCutoff, + BudgetSource::Default, + ] { + for offset in [9_u64, 10, 11] { + let start = MonotonicInstant::now(); + let duration = if cause == BudgetSource::Default { + DEFAULT_NO_DEADLINE_BUDGET + } else { + Duration::from_millis(10) + }; + let mut budget = test_budget(start, duration); + budget.cause = cause; + let offset_duration = duration + .checked_sub(Duration::from_millis(10)) + .and_then(|lead| lead.checked_add(Duration::from_millis(offset))) + .expect("fixture duration bound"); + let observed = + start.checked_add(offset_duration).expect("handoff time"); + let controller = web_sys::AbortController::new().expect("controller"); + let signal = controller.signal(); + let probe = Function::new_no_args("const probe = {pulls: 0}; probe.response = new Response(new ReadableStream({pull() {probe.pulls++;}}, {highWaterMark: 0}), {headers: {'content-length': '1'}}); return probe;") + .call0(&JsValue::UNDEFINED).expect("pending native source"); + let native = if status == StatusCode::OK { + let native = Reflect::get(&probe, &JsValue::from_str("response")) + .expect("native response") + .dyn_into::() + .expect("response type"); + WorkerResponse::from(native) + } else { + empty_worker_response(status, Some("1")) + }; + let result = finish_fixture_response( + native, + AbortGuard::new(controller), + method.clone(), + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + response_mode, + budget, + 1024, + 24, + None, + None, + None, + None, + None, + native_time(scripted_clock(vec![start, observed])), + ) + .await; + if cause == BudgetSource::BatchCutoff + && response_mode == ResponseMode::Streamed + { + assert!( + matches!(result, Err(EdgeError::BadRequest { .. })), + "streamed batches reject before response processing" + ); + } else if offset < 10 { + drop(result.expect("on-time handoff")); + } else { + assert!( + matches!(result, Err(EdgeError::GatewayTimeout {cause: actual, ..}) if actual == cause), + "expired handoff must retain its selected budget" + ); + } + assert!( + signal.aborted(), + "terminal handoff or response drop aborts the owned exchange" + ); + assert_eq!( + Reflect::get(&probe, &JsValue::from_str("pulls")) + .expect("source pulls") + .as_f64(), + Some(0.0_f64), + "handoff never reads payload" + ); + } + } + } + } + } + + #[wasm_bindgen_test] + async fn framing_bodyless_response_aborts_its_unread_native_body() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_secs(1)); + let controller = web_sys::AbortController::new().expect("abort controller"); + let signal = controller.signal(); + + let response = finish_fixture_response( + empty_worker_response(StatusCode::NO_CONTENT, None), + AbortGuard::new(controller), + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Buffered { max_bytes: 64 }, + budget, + 1024, + 24, + None, + None, + None, + None, + None, + native_time(constant_clock(start)), + ) + .await + .expect("bodyless response"); + + assert_eq!(response.status(), StatusCode::NO_CONTENT); + assert!(signal.aborted()); + } + } +} + +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +pub use worker_impl::CloudflareOutboundClient; +#[cfg(all(feature = "cloudflare", target_arch = "wasm32", feature = "test-utils"))] +pub use worker_impl::deferred_clock_paths_hold_for_test; +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +pub(crate) use worker_impl::native_time; +#[cfg(all(feature = "cloudflare", target_arch = "wasm32", feature = "test-utils"))] +pub use worker_impl::response_abort_lifecycle_holds_for_test; + +#[cfg(any( + all(feature = "cloudflare", target_arch = "wasm32"), + feature = "test-utils" +))] +fn generic_fetch_failure() -> EdgeError { + // Workers exposes a rejected Fetch promise as an opaque JS exception, without a stable phase + // or connection-failure discriminator. Preserve that limitation as `Unspecified` instead of + // guessing that every rejection happened before a response was reachable. + EdgeError::bad_gateway_with_reason("outbound fetch failed", BadGatewayReason::Unspecified) +} + +#[cfg(any( + all(feature = "cloudflare", target_arch = "wasm32"), + feature = "test-utils" +))] +fn timeout_error(cause: BudgetSource) -> EdgeError { + EdgeError::gateway_timeout_caused("outbound request deadline expired", cause) +} + +#[cfg(any( + all(feature = "cloudflare", target_arch = "wasm32"), + feature = "test-utils" +))] +fn validate_supported_options(request: &OutboundRequest) -> Result<(), EdgeError> { + if request.image_optimizer_options().is_some() { + return Err(EdgeError::bad_request( + "image_optimizer is not supported by cloudflare", + )); + } + Ok(()) +} + +/// Runs the target-neutral Cloudflare batch preflight contract in native tests. +/// +/// # Errors +/// Returns the same portable validation or batch-shape error as production dispatch. +#[cfg(feature = "test-utils")] +#[inline] +pub fn validate_batch_request_for_test(request: &OutboundRequest) -> Result<(), EdgeError> { + validate_supported_options(request)?; + validate_for_dispatch(request, OutboundSendMode::Batch) +} + +/// Returns the target-neutral classification for an opaque Workers fetch rejection. +#[cfg(feature = "test-utils")] +#[must_use] +#[inline] +pub fn generic_fetch_failure_for_test() -> EdgeError { + generic_fetch_failure() +} + +/// Returns the same attributed timeout emitted by Workers budget timers. +#[cfg(feature = "test-utils")] +#[must_use] +#[inline] +pub fn timeout_error_for_test(cause: BudgetSource) -> EdgeError { + timeout_error(cause) +} + +#[cfg(test)] +mod header_bridge_tests { + use std::collections::BTreeMap; + + use edgezero_core::http::{HeaderMap, HeaderValue}; + + use super::*; + + #[derive(Default)] + struct AppendSink(BTreeMap>); + + impl HeaderSink for AppendSink { + fn append(&mut self, name: &str, value: &str) -> Result<(), EdgeError> { + self.0 + .entry(name.to_owned()) + .or_default() + .push(value.to_owned()); + Ok(()) + } + } + + #[test] + fn response_header_copy_preserves_duplicate_set_cookie_values() { + let mut source = HeaderMap::new(); + source.append("set-cookie", HeaderValue::from_static("first=1")); + source.append("set-cookie", HeaderValue::from_static("second=2")); + let mut sink = AppendSink::default(); + + copy_header_values(&source, &mut sink, |_name| { + EdgeError::internal(anyhow::anyhow!("invalid response header")) + }) + .expect("copy response headers"); + + assert_eq!( + sink.0.get("set-cookie"), + Some(&vec!["first=1".to_owned(), "second=2".to_owned()]) + ); + } + + #[test] + fn response_header_copy_accepts_non_ascii_utf8() { + let mut source = HeaderMap::new(); + source.append( + "x-label", + HeaderValue::from_bytes("caf\u{e9}".as_bytes()).expect("valid header bytes"), + ); + let mut sink = AppendSink::default(); + + copy_header_values(&source, &mut sink, |_name| { + EdgeError::internal(anyhow::anyhow!("invalid response header")) + }) + .expect("copy UTF-8 response header"); + + assert_eq!(sink.0.get("x-label"), Some(&vec!["caf\u{e9}".to_owned()])); + } +} diff --git a/crates/edgezero-adapter-cloudflare/src/outbound/worker_impl/redirect_tests.rs b/crates/edgezero-adapter-cloudflare/src/outbound/worker_impl/redirect_tests.rs new file mode 100644 index 000000000..0c4a5c390 --- /dev/null +++ b/crates/edgezero-adapter-cloudflare/src/outbound/worker_impl/redirect_tests.rs @@ -0,0 +1,502 @@ +use core::convert::Infallible; +use std::io::Write as _; +use std::num::NonZeroU64; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use edgezero_core::http::StatusCode; +use edgezero_core::{ + BadGatewayDecodeReason, BadGatewayReason, BudgetSource, ContentTypeProvenance, Deadline, + EdgeError, HttpClient, MonotonicClock, MonotonicInstant, OutboundFollowFailure, + OutboundRedirectAction, OutboundRedirectLimit, OutboundRedirectStopReason, OutboundRequest, + OutboundResponse, ResponseLimitReason, +}; +use flate2::Compression; +use flate2::write::{GzEncoder, ZlibEncoder}; +use futures_util::FutureExt as _; +use serde_json::{Value, json}; +use wasm_bindgen_test::wasm_bindgen_test; +use worker::js_sys::{Function, JSON, Reflect}; +use worker::wasm_bindgen::{JsCast as _, JsValue}; + +use crate::test_runtime::NumericTimeoutGuard; + +use super::clock_tests::RestoreGlobal; +use super::{CloudflareOutboundClient, HostEventYield, native_time}; + +fn fetch_script(responses: &Value) -> (JsValue, RestoreGlobal) { + let wire = serde_json::to_string(responses).expect("fixture JSON"); + let probe = Function::new_with_args("wire", include_str!("../../../tests/redirect_runtime.js")) + .call1(&JsValue::UNDEFINED, &JsValue::from_str(&wire)) + .expect("install fetch fixture"); + let restore = Reflect::get(&probe, &JsValue::from_str("restore")) + .expect("restore") + .dyn_into::() + .expect("restore function"); + (probe, RestoreGlobal::new(restore)) +} + +fn observations(probe: &JsValue) -> Value { + let encoded = JSON::stringify(probe) + .expect("observations JSON") + .as_string() + .expect("observations text"); + serde_json::from_str(&encoded).expect("observations") +} + +fn request(path: &str, maximum: u64) -> OutboundRequest { + OutboundRequest::get(format!("https://fixture.invalid/{path}")) + .expect("request") + .max_response_bytes(maximum) + .timeout(Duration::from_secs(5)) +} + +fn response(status: u16, bytes: &[u8], headers: &Value) -> Value { + json!({"status": status, "kind": "bytes", "bytes": bytes, "headers": headers}) +} + +fn encoded(coding: &str, bytes: &[u8]) -> Vec { + match coding { + "gzip" => { + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(bytes).expect("gzip input"); + encoder.finish().expect("gzip output") + } + "deflate" => { + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(bytes).expect("deflate input"); + encoder.finish().expect("deflate output") + } + _ => bytes.to_vec(), + } +} + +async fn bytes(reply: OutboundResponse, maximum: u64) -> Vec { + reply + .into_bytes_bounded(maximum) + .await + .expect("response body") + .to_vec() +} + +#[wasm_bindgen_test] +async fn follow_without_redirect_matches_actual_fetch_coding_and_limits() { + let _timers = NumericTimeoutGuard::install(); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + for coding in ["identity", "gzip", "deflate", "unsupported"] { + for (length, declared) in [10_usize, 11] + .into_iter() + .flat_map(|length| [false, true].map(move |declared| (length, declared))) + { + let payload = encoded(coding, &vec![b'x'; length]); + let mut head = json!([["content-encoding", coding]]); + if declared { + head.as_array_mut() + .expect("headers") + .push(json!(["content-length", payload.len().to_string()])); + } + let fixture = response(200, &payload, &head); + let (probe, _restore) = fetch_script(&json!([fixture, fixture])); + let sent_result = client.send(request("plain", 10)).await; + let following_result = client + .follow( + request("follow", 10), + OutboundRedirectLimit::new(1).expect("limit"), + |_| Ok::<_, Infallible>(OutboundRedirectAction::FollowPrepared), + ) + .await; + match (sent_result, following_result) { + (Ok(plain), Ok(followed)) => { + assert_eq!(length, 10); + assert_eq!(plain.status(), followed.response().status()); + assert_eq!(plain.headers(), followed.response().headers()); + assert_eq!( + plain.content_type_provenance(), + ContentTypeProvenance::Unknown + ); + assert_eq!(followed.redirects_followed(), 0); + let (final_response, _, _, _) = followed.into_parts(); + let actual = bytes(plain, 10).await; + assert_eq!(actual, vec![b'x'; 10]); + assert_eq!(actual, bytes(final_response, 10).await); + } + (Err(plain), Err(OutboundFollowFailure::Hop { index, error })) => { + assert_eq!(length, 11); + assert_eq!(index, 0); + assert_eq!(plain.status(), error.status()); + assert_eq!(plain.message(), error.message()); + match (plain, error) { + ( + EdgeError::ResponseTooLarge { + reason: sent_reason, + .. + }, + EdgeError::ResponseTooLarge { + reason: followed_reason, + .. + }, + ) => { + assert_eq!(sent_reason, ResponseLimitReason::BufferedBody); + assert_eq!(sent_reason, followed_reason); + } + other => panic!("expected matching typed limit errors: {other:?}"), + } + } + other => panic!("plain/follow mismatch: {other:?}"), + } + let seen = observations(&probe); + assert_eq!(seen["requests"].as_array().expect("requests").len(), 2); + } + } +} + +fn unread_redirect(kind: &str) -> Value { + let mut head = json!([["location", "/final"]]); + match kind { + "brotli" => head + .as_array_mut() + .expect("headers") + .push(json!(["content-encoding", "br"])), + "oversized" => head + .as_array_mut() + .expect("headers") + .push(json!(["content-length", "10485760"])), + "error" | "pending" => {} + _ => panic!("unknown unread fixture kind"), + } + json!({"status":302_u16, "kind": if kind == "error" || kind == "pending" { kind } else { "bytes" }, "bytes": [], "headers":head}) +} + +#[wasm_bindgen_test] +async fn followed_fetch_heads_release_unread_bodies_before_next_hop() { + let _timers = NumericTimeoutGuard::install(); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + for kind in ["error", "brotli", "oversized", "pending"] { + let (probe, _restore) = fetch_script(&json!([ + unread_redirect(kind), + response(200, b"ok", &json!([])) + ])); + let result = client + .follow( + request("start", 1024 * 1024), + OutboundRedirectLimit::new(4).expect("limit"), + |_| Ok::<_, Infallible>(OutboundRedirectAction::FollowPrepared), + ) + .await + .expect("follow"); + assert_eq!(result.response().status(), StatusCode::OK); + assert_eq!( + result.final_uri().to_string(), + "https://fixture.invalid/final" + ); + assert_eq!(result.redirects_followed(), 1); + let (final_response, _, _, _) = result.into_parts(); + assert_eq!(bytes(final_response, 16).await, b"ok"); + let seen = observations(&probe); + assert_eq!(seen["requests"].as_array().expect("requests").len(), 2); + assert_eq!(seen["pulls"][0], 0_u64); + assert_eq!(seen["aborts"][0], 1_u64); + assert_eq!(seen["aborts"][1], 0_u64); + } +} + +#[wasm_bindgen_test] +async fn stopped_fetch_heads_retain_decode_source_and_limit_errors() { + let _timers = NumericTimeoutGuard::install(); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + for (kind, at_limit) in ["error", "brotli", "oversized"] + .into_iter() + .flat_map(|kind| [false, true].map(|at_limit| (kind, at_limit))) + { + let replies = if at_limit { + json!([unread_redirect("pending"), unread_redirect(kind)]) + } else { + json!([unread_redirect(kind)]) + }; + let (probe, _restore) = fetch_script(&replies); + let failed = client + .follow( + request("start", 1024 * 1024), + OutboundRedirectLimit::new(1).expect("limit"), + move |_| { + Ok::<_, Infallible>(if at_limit { + OutboundRedirectAction::FollowPrepared + } else { + OutboundRedirectAction::Stop + }) + }, + ) + .await + .expect_err("final body policy"); + let OutboundFollowFailure::Hop { index, error } = failed; + let final_index = usize::from(at_limit); + assert_eq!(index, final_index); + match kind { + "error" => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Transport, + .. + } + )), + "brotli" => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Decode(BadGatewayDecodeReason::Brotli), + .. + } + )), + _ => assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::BufferedBody, + .. + } + )), + } + let seen = observations(&probe); + assert_eq!( + seen["requests"].as_array().expect("requests").len(), + final_index.saturating_add(1) + ); + assert_eq!( + seen["pulls"][final_index] == json!(0_u64), + kind == "oversized" + ); + // Empty Brotli fails decoding after clean transport EOF has already disarmed abort. + assert_eq!( + seen["aborts"][final_index], + json!(usize::from(kind != "brotli")) + ); + if at_limit { + assert_eq!(seen["pulls"][0], 0_u64); + assert_eq!(seen["aborts"][0], 1_u64); + } + } +} + +#[wasm_bindgen_test] +async fn final_fetch_redirects_preserve_stop_metadata_and_body() { + let _timers = NumericTimeoutGuard::install(); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + for at_limit in [false, true] { + let final_reply = response(302, b"stop", &json!([["location", "/further"]])); + let replies = if at_limit { + json!([unread_redirect("pending"), final_reply]) + } else { + json!([final_reply]) + }; + let (probe, _restore) = fetch_script(&replies); + let result = client + .follow( + request("start", 16), + OutboundRedirectLimit::new(1).expect("limit"), + move |_| { + Ok::<_, Infallible>(if at_limit { + OutboundRedirectAction::FollowPrepared + } else { + OutboundRedirectAction::Stop + }) + }, + ) + .await + .expect("final redirect"); + assert_eq!(result.response().status(), StatusCode::FOUND); + assert_eq!(result.redirects_followed(), usize::from(at_limit)); + assert_eq!( + result.stop_reason(), + Some(if at_limit { + OutboundRedirectStopReason::LimitReached + } else { + OutboundRedirectStopReason::StoppedByPolicy + }) + ); + let (reply, _, _, _) = result.into_parts(); + assert_eq!(bytes(reply, 16).await, b"stop"); + assert_eq!( + observations(&probe)["requests"] + .as_array() + .expect("requests") + .len(), + usize::from(at_limit).saturating_add(1) + ); + } +} + +#[wasm_bindgen_test] +async fn streamed_fetch_final_response_matches_plain_send() { + let _timers = NumericTimeoutGuard::install(); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + for coding in ["identity", "gzip", "unsupported"] { + let fixture = response( + 200, + &encoded(coding, b"0123456789"), + &json!([["content-encoding", coding]]), + ); + let (probe, _restore) = + fetch_script(&json!([fixture, unread_redirect("pending"), fixture])); + let make_request = || { + request("start", 10) + .stream_response() + .max_encoded_response_bytes(1024) + .max_decoded_response_bytes(10) + .max_chunk_bytes(NonZeroU64::new(3).expect("chunk")) + }; + let plain = client.send(make_request()).await.expect("plain head"); + let followed = client + .follow( + make_request(), + OutboundRedirectLimit::new(4).expect("limit"), + |_| Ok::<_, Infallible>(OutboundRedirectAction::FollowPrepared), + ) + .await + .expect("followed head"); + assert!(plain.body().is_stream()); + assert!(followed.response().body().is_stream()); + assert_eq!(plain.status(), followed.response().status()); + assert_eq!(plain.headers(), followed.response().headers()); + let before = observations(&probe); + assert_eq!(before["pulls"], json!([0_u64, 0_u64, 0_u64])); + let (final_response, _, _, _) = followed.into_parts(); + let actual = bytes(plain, 10).await; + assert_eq!(actual, b"0123456789"); + assert_eq!(actual, bytes(final_response, 10).await); + assert_eq!(observations(&probe)["aborts"], json!([0_u64, 1_u64, 0_u64])); + } +} + +#[wasm_bindgen_test] +async fn stopped_pending_fetch_body_keeps_original_absolute_deadline() { + let _timers = NumericTimeoutGuard::install(); + for at_limit in [false, true] { + let now = Arc::new(Mutex::new(MonotonicInstant::now())); + let sampled = Arc::clone(&now); + let clock = MonotonicClock::new(move || *sampled.lock().expect("clock")); + let start = clock.now(); + let deadline = + Deadline::at_instant(start.checked_add(Duration::from_secs(3)).expect("deadline")); + let client = + HttpClient::with_client(CloudflareOutboundClient::with_time(native_time(clock))); + let replies = if at_limit { + json!([unread_redirect("pending"), unread_redirect("pending")]) + } else { + json!([unread_redirect("pending")]) + }; + let (probe, _restore) = fetch_script(&replies); + let mut operation = client.follow( + request("start", 16).deadline(deadline), + OutboundRedirectLimit::new(1).expect("limit"), + move |_| { + Ok::<_, Infallible>(if at_limit { + OutboundRedirectAction::FollowPrepared + } else { + OutboundRedirectAction::Stop + }) + }, + ); + let final_index = usize::from(at_limit); + for _ in 0_u8..4 { + assert!(operation.as_mut().now_or_never().is_none()); + HostEventYield::after(Duration::from_millis(1)) + .await + .expect("fetch settles"); + } + assert!( + observations(&probe)["pulls"][final_index] + .as_u64() + .expect("pulls") + > 0 + ); + *now.lock().expect("clock") = deadline.instant(); + let OutboundFollowFailure::Hop { index, error } = operation.await.expect_err("deadline"); + assert_eq!(index, final_index); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + } + )); + assert_eq!( + observations(&probe)["aborts"], + json!(vec![1_u64; final_index.saturating_add(1)]) + ); + if at_limit { + assert_eq!(observations(&probe)["pulls"][0], 0_u64); + } + } +} + +#[wasm_bindgen_test] +async fn dropping_follow_aborts_current_fetch_without_another_hop() { + let _timers = NumericTimeoutGuard::install(); + let client = HttpClient::with_client(CloudflareOutboundClient::new()); + let (probe, _restore) = fetch_script(&json!([ + unread_redirect("pending"), {"kind":"pending-head"} + ])); + let mut operation = client.follow( + request("start", 16), + OutboundRedirectLimit::new(4).expect("limit"), + |_| Ok::<_, Infallible>(OutboundRedirectAction::FollowPrepared), + ); + assert!(operation.as_mut().now_or_never().is_none()); + HostEventYield::after(Duration::from_millis(1)) + .await + .expect("first head"); + assert!(operation.as_mut().now_or_never().is_none()); + let seen = observations(&probe); + assert_eq!(seen["requests"].as_array().expect("requests").len(), 2); + assert_eq!(seen["aborts"], json!([1_u64, 0_u64])); + drop(operation); + let cancelled = observations(&probe); + assert_eq!(cancelled["requests"].as_array().expect("requests").len(), 2); + assert_eq!(cancelled["pulls"], json!([0_u64, 0_u64])); + assert_eq!(cancelled["aborts"], json!([1_u64, 1_u64])); +} + +#[wasm_bindgen_test] +async fn replacement_fetch_stream_cannot_extend_original_chain_deadline() { + let _timers = NumericTimeoutGuard::install(); + let now = Arc::new(Mutex::new(MonotonicInstant::now())); + let sampled = Arc::clone(&now); + let clock = MonotonicClock::new(move || *sampled.lock().expect("clock")); + let deadline = Deadline::at_instant( + clock + .now() + .checked_add(Duration::from_secs(3)) + .expect("deadline"), + ); + let client = HttpClient::with_client(CloudflareOutboundClient::with_time(native_time(clock))); + let (probe, _restore) = fetch_script(&json!([ + unread_redirect("pending"), {"status":200_u16,"kind":"pending","headers":[]} + ])); + let result = client + .follow( + request("start", 16).deadline(deadline), + OutboundRedirectLimit::new(4).expect("limit"), + |_| { + Ok::<_, Infallible>(OutboundRedirectAction::Follow( + request("replacement", 16).stream_response(), + )) + }, + ) + .await + .expect("stream head"); + assert!(result.response().body().is_stream()); + assert_eq!( + result.final_uri().to_string(), + "https://fixture.invalid/replacement" + ); + assert_eq!(observations(&probe)["pulls"], json!([0_u64, 0_u64])); + *now.lock().expect("clock") = deadline.instant(); + let (reply, _, _, _) = result.into_parts(); + assert!(matches!( + reply.into_bytes_bounded(16).await, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + assert_eq!(observations(&probe)["pulls"], json!([0_u64, 0_u64])); + assert_eq!(observations(&probe)["aborts"], json!([1_u64, 1_u64])); +} diff --git a/crates/edgezero-adapter-cloudflare/src/outbound/worker_impl/testing.rs b/crates/edgezero-adapter-cloudflare/src/outbound/worker_impl/testing.rs new file mode 100644 index 000000000..101f3116f --- /dev/null +++ b/crates/edgezero-adapter-cloudflare/src/outbound/worker_impl/testing.rs @@ -0,0 +1,205 @@ +use std::collections::BTreeMap; +use std::num::NonZeroU64; + +use edgezero_core::outbound::{ + HttpClient, OutboundCollectedSlot, OutboundProxyMarker, OutboundRequest, ResponseDecoding, + ResponseMode, +}; + +use super::*; + +type FixtureHead = Box Result>; + +thread_local! { + static RESPONSES: RefCell> = const { RefCell::new(BTreeMap::new()) }; + static NEXT_RESPONSE: Cell = const { Cell::new(0) }; +} + +struct ResponseClient { + id: u64, + time: OutboundTime, +} + +#[expect( + clippy::missing_trait_methods, + reason = "Drop::pin_drop is unstable and conflicts with drop" +)] +impl Drop for ResponseClient { + fn drop(&mut self) { + let response = RESPONSES.with(|responses| responses.borrow_mut().remove(&self.id)); + drop(response); + } +} + +impl OutboundHttpClient for ResponseClient { + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let head = self.dispatch_head(request); + schedule_response(Box::pin(async move { head.await?.finish().await })) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + let response = RESPONSES.with(|responses| responses.borrow_mut().remove(&self.id)); + let (_, context) = request.into_parts(); + Box::pin(async move { + response + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("fixture response missing")))?( + context, + ) + }) + } + + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + schedule_batch(self, batch) + } + + fn time(&self) -> OutboundTime { + self.time.clone() + } + + fn validate_supported_options( + &self, + request: &OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + super::super::validate_supported_options(request) + } +} + +fn fixture_client(time: OutboundTime, head: Head) -> Result +where + Head: FnOnce(OutboundResponseContext) -> Result + 'static, +{ + let id = NEXT_RESPONSE + .with(|next| { + let id = next.get(); + id.checked_add(1).map(|next_id| { + next.set(next_id); + id + }) + }) + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("fixture identity overflow")))?; + RESPONSES.with(|responses| responses.borrow_mut().insert(id, Box::new(head))); + Ok(HttpClient::with_client(ResponseClient { id, time })) +} + +#[expect( + clippy::too_many_arguments, + reason = "existing response fixtures exercise independent policy boundaries" +)] +pub(super) async fn finish_fixture_response( + response: WorkerResponse, + abort_guard: AbortGuard, + request_method: Method, + proxy_marker: OutboundProxyMarker, + response_decoding: ResponseDecoding, + response_mode: ResponseMode, + budget: DispatchBudget, + max_decoder_bytes: u64, + max_brotli_window_bits: u8, + max_chunk_bytes: Option, + max_decoded_response_bytes: Option, + max_encoded_response_bytes: Option, + max_response_header_bytes: Option, + max_response_header_count: Option, + time: OutboundTime, +) -> Result { + let mut parts = OutboundRequest::get("https://fixture.invalid/")?.into_parts(); + parts.method = request_method; + parts.proxy_marker = proxy_marker; + parts.response_decoding = response_decoding; + parts.response_mode = response_mode; + parts.max_decoder_bytes = max_decoder_bytes; + parts.max_brotli_window_bits = max_brotli_window_bits; + parts.max_chunk_bytes = max_chunk_bytes; + parts.max_decoded_response_bytes = max_decoded_response_bytes; + parts.max_encoded_response_bytes = max_encoded_response_bytes; + parts.max_response_header_bytes = max_response_header_bytes; + parts.max_response_header_count = max_response_header_count; + match budget.cause { + BudgetSource::PerCallTimeout => parts.timeout = Some(budget.duration), + BudgetSource::RequestDeadline => parts.deadline = Some(budget.deadline), + BudgetSource::BatchCutoff | BudgetSource::Default => {} + BudgetSource::ConnectTimeout + | BudgetSource::IdleTimeout + | BudgetSource::Unspecified + | _ => { + return Err(EdgeError::internal(anyhow::anyhow!( + "unsupported fixture governing source" + ))); + } + } + let request = OutboundRequest::from_parts(parts)?; + let client = fixture_client(time, move |context| { + response_head(response, abort_guard, context) + })?; + if budget.cause == BudgetSource::BatchCutoff { + let results = client + .start_batch_until(vec![request], budget.deadline) + .collect() + .await + .map_err(|failure| failure.into_parts().0)?; + let (mut slots, _) = results.into_parts(); + match slots + .pop() + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("fixture slot missing")))? + { + OutboundCollectedSlot::Resolved(slot) => slot.outcome, + OutboundCollectedSlot::Unresolved => Err(timeout_error(BudgetSource::BatchCutoff)), + OutboundCollectedSlot::AlreadyDelivered => Err(EdgeError::internal(anyhow::anyhow!( + "fixture result was already delivered" + ))), + } + } else { + client.send(request).await + } +} + +pub(super) async fn finish_fixture_stream( + source: BodyStream, + guard: AbortGuard, + budget: DispatchBudget, + time: OutboundTime, +) -> Result { + let mut parts = OutboundRequest::get("https://fixture.invalid/")? + .stream_response() + .into_parts(); + match budget.cause { + BudgetSource::PerCallTimeout => parts.timeout = Some(budget.duration), + BudgetSource::RequestDeadline => parts.deadline = Some(budget.deadline), + BudgetSource::Default => {} + BudgetSource::BatchCutoff + | BudgetSource::ConnectTimeout + | BudgetSource::IdleTimeout + | BudgetSource::Unspecified + | _ => { + return Err(EdgeError::internal(anyhow::anyhow!( + "unsupported streamed fixture budget" + ))); + } + } + let client = fixture_client(time, move |context| { + OutboundHead::new( + StatusCode::OK, + OutboundHeaderInput::new( + "cloudflare", + HeaderMap::new(), + OutboundHeaderFidelity::ProviderFieldBoundariesUnknown, + ), + guarded_response_stream(source, guard), + context, + OutboundExchangeOwner::new(|| {}), + ) + })?; + client + .send(OutboundRequest::from_parts(parts)?) + .await? + .into_body() + .into_stream() + .map_err(|_buffered| { + EdgeError::internal(anyhow::anyhow!("streamed fixture returned a buffered body")) + }) +} diff --git a/crates/edgezero-adapter-cloudflare/src/proxy.rs b/crates/edgezero-adapter-cloudflare/src/proxy.rs deleted file mode 100644 index 211d00e42..000000000 --- a/crates/edgezero-adapter-cloudflare/src/proxy.rs +++ /dev/null @@ -1,204 +0,0 @@ -use async_trait::async_trait; -use bytes::Bytes; -use edgezero_core::body::Body; -use edgezero_core::compression::{decode_brotli_stream, decode_gzip_stream}; -use edgezero_core::error::EdgeError; -use edgezero_core::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri, header}; -use edgezero_core::proxy::{PROXY_HEADER, ProxyClient, ProxyRequest, ProxyResponse}; -use futures_util::TryStreamExt as _; -use futures_util::stream::{self, LocalBoxStream, StreamExt as _}; -use std::io; -use worker::{ - Body as WorkerBody, Fetch, Headers, Method as CfMethod, Request as CfRequest, RequestInit, - Response as CfResponse, wasm_bindgen::JsValue, -}; - -type ChunkStream = LocalBoxStream<'static, Result, io::Error>>; - -pub struct CloudflareProxyClient; - -#[async_trait(?Send)] -impl ProxyClient for CloudflareProxyClient { - #[inline] - async fn send(&self, request: ProxyRequest) -> Result { - let (method, uri, headers, body, _ext) = request.into_parts(); - let cf_request = build_cf_request(&method, &uri, &headers, body)?; - let mut cf_response = Fetch::Request(cf_request) - .send() - .await - .map_err(EdgeError::internal)?; - - let mut proxy_response = convert_response(&mut cf_response)?; - proxy_response - .headers_mut() - .insert(PROXY_HEADER, HeaderValue::from_static("cloudflare")); - Ok(proxy_response) - } -} - -fn build_cf_request( - method: &Method, - uri: &Uri, - headers: &HeaderMap, - body: Body, -) -> Result { - let mut init = RequestInit::new(); - init.with_method(http_method_to_cf(method)); - - let cf_headers = Headers::from(headers); - init.with_headers(cf_headers); - - attach_body(&mut init, body)?; - - let request = CfRequest::new_with_init(&uri.to_string(), &init).map_err(EdgeError::internal)?; - Ok(request) -} - -fn attach_body(init: &mut RequestInit, body: Body) -> Result<(), EdgeError> { - match body { - Body::Once(bytes) => { - if bytes.is_empty() { - return Ok(()); - } - let chunk = bytes.to_vec(); - let stream = stream::once(async move { Ok::, JsValue>(chunk) }).boxed_local(); - let worker_body = WorkerBody::from_stream(stream).map_err(EdgeError::internal)?; - if let Some(readable) = worker_body.into_inner() { - init.with_body(Some(JsValue::from(readable))); - } - } - Body::Stream(stream) => { - let mapped = stream - .map(|res| match res { - Ok(bytes) => Ok::, JsValue>(bytes.to_vec()), - Err(err) => Err(JsValue::from_str(&err.to_string())), - }) - .boxed_local(); - let worker_body = WorkerBody::from_stream(mapped).map_err(EdgeError::internal)?; - if let Some(readable) = worker_body.into_inner() { - init.with_body(Some(JsValue::from(readable))); - } - } - } - - Ok(()) -} - -fn convert_response(cf_response: &mut CfResponse) -> Result { - let status = StatusCode::from_u16(cf_response.status_code()).map_err(EdgeError::internal)?; - let mut proxy_response = ProxyResponse::new(status, Body::empty()); - - let mut encoding = None; - for (name, value) in cf_response.headers().entries() { - if name.eq_ignore_ascii_case(header::CONTENT_ENCODING.as_str()) { - encoding = Some(value.to_ascii_lowercase()); - } - if let Ok(header_name) = HeaderName::from_bytes(name.as_bytes()) - && let Ok(header_value) = HeaderValue::from_str(&value) - { - proxy_response - .headers_mut() - .insert(header_name, header_value); - } - } - - let worker_stream = cf_response.stream().map_err(EdgeError::internal)?; - - let chunk_stream: ChunkStream = worker_stream - .map_err(|err| worker_error_to_io(&err)) - .boxed_local(); - let body_stream = transform_stream(chunk_stream, encoding.as_deref()); - *proxy_response.body_mut() = Body::from_stream(body_stream); - - if encoding.is_some() { - proxy_response - .headers_mut() - .remove(header::CONTENT_ENCODING); - proxy_response.headers_mut().remove(header::CONTENT_LENGTH); - } - - Ok(proxy_response) -} - -fn http_method_to_cf(method: &Method) -> CfMethod { - match *method { - Method::POST => CfMethod::Post, - Method::PUT => CfMethod::Put, - Method::PATCH => CfMethod::Patch, - Method::DELETE => CfMethod::Delete, - Method::HEAD => CfMethod::Head, - Method::OPTIONS => CfMethod::Options, - Method::CONNECT => CfMethod::Connect, - Method::TRACE => CfMethod::Trace, - _ => CfMethod::Get, - } -} - -fn transform_stream( - stream: ChunkStream, - encoding: Option<&str>, -) -> LocalBoxStream<'static, Result> { - match encoding { - Some("gzip") => decode_gzip_stream(stream).boxed_local(), - Some("br") => decode_brotli_stream(stream).boxed_local(), - _ => stream.map(|res| res.map(Bytes::from)).boxed_local(), - } -} - -fn worker_error_to_io(err: &worker::Error) -> io::Error { - io::Error::other(err.to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - use brotli::CompressorWriter; - use flate2::{Compression, write::GzEncoder}; - use futures::executor::block_on; - use futures_util::stream; - use std::io::Write as _; - - fn collect_body(body: Body) -> Vec { - match body { - Body::Once(bytes) => bytes.to_vec(), - Body::Stream(mut stream) => block_on(async { - let mut out = Vec::new(); - while let Some(item) = stream.next().await { - let chunk = item.expect("chunk"); - out.extend_from_slice(&chunk); - } - out - }), - } - } - - #[test] - fn streaming_identity_preserves_body() { - let chunks = vec![ - Ok::, io::Error>(b"hello".to_vec()), - Ok(b" world".to_vec()), - ]; - let chunk_stream: ChunkStream = Box::pin(stream::iter(chunks)); - let body = Body::from_stream(transform_stream(chunk_stream, None)); - assert_eq!(collect_body(body), b"hello world"); - } - - #[test] - fn streaming_handles_gzip_and_brotli() { - let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); - encoder.write_all(b"gzip payload").unwrap(); - let gzip = encoder.finish().unwrap(); - let gzip_stream: ChunkStream = Box::pin(stream::iter(vec![Ok::, io::Error>(gzip)])); - let body = Body::from_stream(transform_stream(gzip_stream, Some("gzip"))); - assert_eq!(collect_body(body), b"gzip payload"); - - let mut brotli_data = Vec::new(); - let mut compressor = CompressorWriter::new(&mut brotli_data, 4096, 5, 21); - compressor.write_all(b"brotli payload").unwrap(); - drop(compressor); - let brotli_stream: ChunkStream = - Box::pin(stream::iter(vec![Ok::, io::Error>(brotli_data)])); - let brotli_body = Body::from_stream(transform_stream(brotli_stream, Some("br"))); - assert_eq!(collect_body(brotli_body), b"brotli payload"); - } -} diff --git a/crates/edgezero-adapter-cloudflare/src/request.rs b/crates/edgezero-adapter-cloudflare/src/request.rs index 41a317f22..10c32eaba 100644 --- a/crates/edgezero-adapter-cloudflare/src/request.rs +++ b/crates/edgezero-adapter-cloudflare/src/request.rs @@ -1,28 +1,55 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Display; +use std::future::Future; use std::sync::{Arc, Mutex, OnceLock}; +use std::time::Duration; +#[cfg(feature = "test-utils")] +use std::{ + io, + sync::atomic::{AtomicUsize, Ordering}, + task::Poll, +}; +#[cfg(feature = "test-utils")] +use bytes::Bytes; +use edgezero_core::OutboundScopeOwner; use edgezero_core::app::{App, StoreMetadata}; use edgezero_core::body::Body; -use edgezero_core::config_store::ConfigStoreHandle; +use edgezero_core::config_store::{ConfigStoreHandle, ConfigStoreOpenFailure}; +use edgezero_core::context::RuntimeVariables; use edgezero_core::env_config::EnvConfig; use edgezero_core::error::EdgeError; use edgezero_core::http::{Method as CoreMethod, Request, Uri, request_builder}; +use edgezero_core::ingress::{ + IngressBeginOutcome, IngressDispatchOutcome, IngressFraming, IngressHeadAccounting, + IngressHeadParts, PreparedIngress, +}; use edgezero_core::key_value_store::KvHandle; -use edgezero_core::proxy::ProxyHandle; +use edgezero_core::outbound::HttpClient; +use edgezero_core::response_egress::ResponseEgressEnvelope; use edgezero_core::secret_store::SecretHandle; use edgezero_core::store_registry::{ BoundSecretStore, ConfigRegistry, ConfigStoreBinding, KvRegistry, SecretRegistry, StoreRegistry, }; +use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; +#[cfg(feature = "test-utils")] +use futures::executor::block_on; +use futures_util::future::{Either, select}; +#[cfg(feature = "test-utils")] +use futures_util::stream::Empty; +#[cfg(feature = "test-utils")] +use futures_util::stream::poll_fn; +use futures_util::stream::{LocalBoxStream, unfold}; +use futures_util::{StreamExt as _, TryStreamExt as _}; use worker::{ - Context, Env, Error as WorkerError, Method, Request as CfRequest, Response as CfResponse, + Context, Delay, Env, Error as WorkerError, Method, Request as CfRequest, Response as CfResponse, }; use crate::config_store::CloudflareConfigStore; use crate::context::CloudflareRequestContext; use crate::key_value_store::CloudflareKvStore; -use crate::proxy::CloudflareProxyClient; -use crate::response::from_core_response; +use crate::outbound::{CloudflareOutboundClient, native_time}; +use crate::response::{CloudflareEgressRuntime, from_egress_response}; use crate::secret_store::CloudflareSecretStore; /// Groups the optional per-request store handles injected at dispatch time. @@ -38,6 +65,7 @@ pub(crate) struct Stores { config_store: Option, kv: Option, kv_registry: Option, + runtime_variables: Option, secret_registry: Option, secrets: Option, } @@ -106,8 +134,18 @@ impl<'app> CloudflareService<'app> { env: Env, ctx: Context, ) -> Result { + with_callback_scope(self.dispatch_scoped(req, env, ctx)).await + } + + async fn dispatch_scoped( + self, + req: CfRequest, + env: Env, + ctx: Context, + ) -> Result { + let request_start = self.app.monotonic_now(); let config_store = match self.config { - ConfigSource::Binding(binding) => open_config_or_warn(&env, &binding), + ConfigSource::Binding(binding) => Some(open_config_handle(&env, &binding)), ConfigSource::Handle(handle) => Some(handle), ConfigSource::None => None, }; @@ -134,6 +172,7 @@ impl<'app> CloudflareService<'app> { secrets, ..Default::default() }, + request_start, ) .await } @@ -175,9 +214,11 @@ impl<'app> CloudflareService<'app> { } /// Open the KV namespace bound as `binding` (per `wrangler.toml`) - /// as a Cloudflare config store and inject its handle. If the - /// binding is absent the dispatcher logs once and proceeds - /// without it. + /// as a Cloudflare config store and inject its handle. If opening + /// fails, the dispatcher logs once and retains a failing handle + /// whose reads return + /// [`ConfigStoreError::Unavailable`](edgezero_core::config_store::ConfigStoreError::Unavailable). + /// Only an unconfigured service has no config-store handle. #[must_use] #[inline] pub fn with_config(mut self, binding: S) -> Self @@ -236,9 +277,38 @@ pub(crate) struct RegistryInputs<'env> { pub config_meta: Option, pub env_config: &'env EnvConfig, pub kv_meta: Option, + pub runtime_variables: RuntimeVariables, pub secret_meta: Option, } +#[cfg(feature = "test-utils")] +struct DropSignal(Arc); + +#[cfg(feature = "test-utils")] +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for DropSignal { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } +} + +fn detached_error_envelope( + app: &App, + error: EdgeError, + request_method: CoreMethod, + request_start: MonotonicInstant, +) -> Result { + match app.detached_ingress_error_egress(error, request_method, request_start) { + IngressDispatchOutcome::Response(envelope) => Ok(*envelope), + IngressDispatchOutcome::Aborted | _ => Err(WorkerError::RustError( + "ingress admission aborted".to_owned(), + )), + } +} + /// Convert a Cloudflare Worker request into an `EdgeZero` core request. /// /// # Errors @@ -246,10 +316,26 @@ pub(crate) struct RegistryInputs<'env> { /// and [`EdgeError::internal`] if the body cannot be read or the core /// request cannot be built. #[inline] +#[expect( + clippy::unused_async, + reason = "the public converter retains its established async API while request bodies remain lazy" +)] pub async fn into_core_request( - mut req: CfRequest, + req: CfRequest, env: Env, ctx: Context, +) -> Result { + let mut request = into_core_request_head(&req, env, ctx, MonotonicClock::default())?; + *request.body_mut() = + cloudflare_body_stream(req)?.map_or_else(Body::empty, Body::from_external_stream); + Ok(request) +} + +fn into_core_request_head( + req: &CfRequest, + env: Env, + ctx: Context, + outbound_clock: MonotonicClock, ) -> Result { let method = into_core_method(&req.method()); let url = req @@ -266,37 +352,325 @@ pub async fn into_core_request( builder = builder.header(name.as_str(), value); } - let bytes = req.bytes().await.map_err(EdgeError::internal)?; - - let mut request = builder - .body(Body::from(bytes)) - .map_err(EdgeError::internal)?; + let mut request = builder.body(Body::empty()).map_err(EdgeError::internal)?; CloudflareRequestContext::insert(&mut request, env, ctx); request .extensions_mut() - .insert(ProxyHandle::with_client(CloudflareProxyClient)); + .insert(outbound_client(outbound_clock)); Ok(request) } +fn outbound_client(clock: MonotonicClock) -> HttpClient { + HttpClient::with_client(CloudflareOutboundClient::with_time(native_time(clock))) +} + +fn cloudflare_body_stream( + mut req: CfRequest, +) -> Result>>, EdgeError> { + if req.inner().body().is_none() { + return Ok(None); + } + Ok(Some( + req.stream() + .map_err(EdgeError::internal)? + .map_ok(bytes::Bytes::from) + .boxed_local(), + )) +} + +fn cloudflare_deadline_body( + source: Source, + deadline: Deadline, + monotonic_clock: MonotonicClock, +) -> Body +where + Source: futures_util::Stream> + 'static, + SourceError: Into + 'static, +{ + let boxed_stream = source.map_err(Into::into).boxed_local(); + let stream = unfold(Some(boxed_stream), move |stream_state| { + let clock = monotonic_clock.clone(); + async move { + let mut body_stream = stream_state?; + if deadline.is_expired_at(clock.now()) { + return Some(( + Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )), + None, + )); + } + + Delay::from(Duration::ZERO).await; + + let item = { + let Some(remaining) = deadline.remaining_at(clock.now()) else { + return Some(( + Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )), + None, + )); + }; + let timer = Delay::from(remaining); + let next = body_stream.next(); + match select(timer, next).await { + Either::Left(((), _)) => { + return Some(( + Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )), + None, + )); + } + Either::Right((item, _)) => item, + } + }; + if deadline.is_expired_at(clock.now()) { + return Some(( + Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )), + None, + )); + } + match item { + Some(Ok(bytes)) => Some((Ok(bytes), Some(body_stream))), + Some(Err(error)) => Some((Err(EdgeError::internal(error)), None)), + None => None, + } + } + }); + Body::from_stream(stream) +} + +/// Runs the terminal source-release probe used by the WASM contract suite. +#[cfg(feature = "test-utils")] +#[must_use] +#[inline] +pub fn deadline_body_releases_source_for_test() -> bool { + let dropped = Arc::new(AtomicUsize::new(0)); + let signal = DropSignal(Arc::clone(&dropped)); + let source = poll_fn(move |_cx| { + let _keep_alive = &signal; + Poll::>>::Pending + }); + let start = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || start); + let body = cloudflare_deadline_body(source, Deadline::at_instant(start), clock); + let Ok(mut body_stream) = body.into_stream() else { + return false; + }; + let Some(Err(error)) = block_on(body_stream.next()) else { + return false; + }; + matches!(error, EdgeError::RequestTimeout { .. }) && dropped.load(Ordering::SeqCst) == 1 +} + +/// Dispatches an observable source through the production ingress body wrapper. +#[cfg(feature = "test-utils")] +#[doc(hidden)] +#[inline] +pub async fn dispatch_ingress_stream_for_test( + app: &App, + method: CoreMethod, + uri: Uri, + source: Source, +) -> Result +where + Source: futures_util::Stream> + 'static, + SourceError: Into + 'static, +{ + let request_start = app.monotonic_now(); + let mut core_request = request_builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .map_err(|error| edge_error_to_worker(&EdgeError::internal(error)))?; + core_request + .extensions_mut() + .insert(outbound_client(app.monotonic_clock())); + with_callback_scope(dispatch_ingress_stream_and( + app, + core_request, + Stores::default(), + request_start, + move || Ok(Some(source)), + Ok, + )) + .await +} + +/// Exercises admission ordering with observable source construction and delivery closures. +#[cfg(feature = "test-utils")] +#[doc(hidden)] +#[inline] +pub async fn dispatch_ingress_with_for_test( + app: &App, + method: CoreMethod, + uri: Uri, + make_source: MakeSource, + deliver: Deliver, +) -> Result +where + Source: futures_util::Stream> + 'static, + SourceError: Into + 'static, + MakeSource: FnOnce() -> Result, + Deliver: FnOnce(ResponseEgressEnvelope) -> Result, +{ + let request_start = app.monotonic_now(); + let core_request = request_builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .map_err(|error| edge_error_to_worker(&EdgeError::internal(error)))?; + with_callback_scope(dispatch_ingress_stream_and( + app, + core_request, + Stores::default(), + request_start, + move || make_source().map(Some), + deliver, + )) + .await +} + +/// Exercises a source-construction failure after admission through the production seam. +#[cfg(feature = "test-utils")] +#[doc(hidden)] +#[inline] +pub async fn dispatch_ingress_source_error_for_test( + app: &App, + method: CoreMethod, + uri: Uri, +) -> Result { + let request_start = app.monotonic_now(); + let core_request = request_builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .map_err(|error| edge_error_to_worker(&EdgeError::internal(error)))?; + with_callback_scope(dispatch_ingress_stream_and( + app, + core_request, + Stores::default(), + request_start, + || -> Result>>, EdgeError> { + Err(EdgeError::internal(anyhow::anyhow!( + "cloudflare source construction failed" + ))) + }, + Ok, + )) + .await +} + +pub(crate) async fn with_callback_scope(future: F) -> F::Output +where + F: Future, +{ + match OutboundScopeOwner::callback_if_absent() { + Some(callback) => callback.run(future).await, + None => future.await, + } +} + pub(crate) async fn dispatch_with_handles( app: &App, req: CfRequest, env: Env, ctx: Context, stores: Stores, + request_start: MonotonicInstant, ) -> Result { - let core_request = into_core_request(req, env, ctx) - .await - .map_err(|err| edge_error_to_worker(&err))?; - dispatch_core_request(app, core_request, stores).await + let signal = req.inner().signal(); + let head_request = into_core_request_head(&req, env, ctx, app.monotonic_clock()) + .map_err(|error| edge_error_to_worker(&error))?; + let context = CloudflareRequestContext::get(&head_request) + .map(CloudflareRequestContext::context_handle) + .ok_or_else(|| WorkerError::RustError("missing Cloudflare request context".to_owned()))?; + let runtime = CloudflareEgressRuntime::new(signal, context); + dispatch_ingress_stream_and( + app, + head_request, + stores, + request_start, + move || cloudflare_body_stream(req), + move |egress| from_egress_response(egress, runtime), + ) + .await +} + +async fn dispatch_ingress_stream_and( + app: &App, + mut head_request: Request, + stores: Stores, + request_start: MonotonicInstant, + make_source: MakeSource, + deliver: Deliver, +) -> Result +where + Source: futures_util::Stream> + 'static, + SourceError: Into + 'static, + MakeSource: FnOnce() -> Result, EdgeError>, + Deliver: FnOnce(ResponseEgressEnvelope) -> Result, +{ + let request_method = head_request.method().clone(); + let head_parts = IngressHeadParts::from_request( + &head_request, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ); + if let Err(validation_error) = head_parts.validate_normalized(app.ingress_head_limits()) { + let egress = detached_error_envelope(app, validation_error, request_method, request_start)?; + return deliver(egress).map_err(|delivery_error| edge_error_to_worker(&delivery_error)); + } + let prepared = match app.begin_ingress(head_parts, request_start) { + Ok(outcome) => match outcome { + IngressBeginOutcome::Admitted(prepared) => prepared, + IngressBeginOutcome::Refused(response) => { + return deliver(response).map_err(|error| edge_error_to_worker(&error)); + } + IngressBeginOutcome::Aborted => { + return Err(WorkerError::RustError( + "ingress admission aborted".to_owned(), + )); + } + _ => { + let admission_error = + EdgeError::internal(anyhow::anyhow!("unsupported ingress admission outcome")); + let egress = + detached_error_envelope(app, admission_error, request_method, request_start)?; + return deliver(egress) + .map_err(|delivery_error| edge_error_to_worker(&delivery_error)); + } + }, + Err(admission_error) => { + let egress = + detached_error_envelope(app, admission_error, request_method, request_start)?; + return deliver(egress).map_err(|delivery_error| edge_error_to_worker(&delivery_error)); + } + }; + let source = match make_source() { + Ok(source) => source, + Err(source_error) => { + return deliver(app.admitted_error_egress(prepared, source_error)) + .map_err(|delivery_error| edge_error_to_worker(&delivery_error)); + } + }; + *head_request.body_mut() = source.map_or_else(Body::empty, |stream| { + cloudflare_deadline_body(stream, prepared.read_deadline(), prepared.monotonic_clock()) + }); + dispatch_core_request(app, head_request, stores, prepared, deliver).await } /// Dispatch with per-id store registries built from baked metadata. /// /// Cloudflare capability map: -/// - KV (Multi): each declared id opens its own KV namespace binding via -/// `EDGEZERO__STORES__KV____NAME` (default = id). +/// - KV (Multi): each declared id binds its KV namespace lazily via +/// `EDGEZERO__STORES__KV____NAME` (default = id). Opening occurs on +/// first use, with a typed failure retained for that request only. /// - Config (Multi): each declared id opens its own KV namespace via /// `EDGEZERO__STORES__CONFIG____NAME`, read asynchronously. /// - Secrets (Single): one shared [`CloudflareSecretStore`] is registered @@ -308,7 +682,8 @@ pub(crate) async fn dispatch_with_registries( ctx: Context, inputs: RegistryInputs<'_>, ) -> Result { - let kv_registry = build_kv_registry(&env, inputs.kv_meta, inputs.env_config)?; + let request_start = app.monotonic_now(); + let kv_registry = build_kv_registry(&env, inputs.kv_meta, inputs.env_config); let config_registry = build_config_registry(&env, inputs.config_meta, inputs.env_config); let secret_registry = build_secret_registry(&env, inputs.secret_meta, inputs.env_config); dispatch_with_handles( @@ -320,8 +695,10 @@ pub(crate) async fn dispatch_with_registries( config_registry, kv_registry, secret_registry, + runtime_variables: Some(inputs.runtime_variables), ..Default::default() }, + request_start, ) .await } @@ -335,9 +712,7 @@ pub(crate) fn resolve_kv_handle( Ok(store) => Ok(Some(KvHandle::new(Arc::new(store)))), Err(err) => { if kv_required { - return Err(WorkerError::RustError(format!( - "KV binding '{kv_binding}' is explicitly configured but could not be opened: {err}" - ))); + return Err(WorkerError::RustError(err.to_string())); } warn_missing_kv_binding_once(kv_binding, &err); Ok(None) @@ -374,50 +749,36 @@ fn build_config_registry( let mut by_id: BTreeMap = BTreeMap::new(); for id in meta.ids { let binding_name = env_config.store_name("config", id); - if let Some(handle) = open_config_or_warn(env, &binding_name) { - by_id.insert( - (*id).to_owned(), - ConfigStoreBinding { - handle, - default_key: env_config.store_key("config", id), - }, - ); - } - } - let default_id = meta.default.to_owned(); - if !by_id.contains_key(&default_id) { - log::warn!( - "config registry default id `{default_id}` could not be opened; dropping the config registry" + by_id.insert( + (*id).to_owned(), + ConfigStoreBinding { + handle: open_config_handle(env, &binding_name), + default_key: env_config.store_key("config", id), + }, ); } - StoreRegistry::from_parts(by_id, default_id) + StoreRegistry::from_parts(by_id, meta.default.to_owned()) } fn build_kv_registry( env: &Env, kv_meta: Option, env_config: &EnvConfig, -) -> Result, WorkerError> { - let Some(meta) = kv_meta else { - return Ok(None); - }; +) -> Option { + let meta = kv_meta?; let mut by_id: BTreeMap = BTreeMap::new(); for id in meta.ids { let binding = env_config.store_name("kv", id); - // Required per-id: `[stores.kv]` is declared, so failure to open is a - // runtime error rather than a silent skip. - let Some(handle) = resolve_kv_handle(env, &binding, true)? else { - continue; - }; - by_id.insert((*id).to_owned(), handle); - } - let default_id = meta.default.to_owned(); - if !by_id.contains_key(&default_id) { - log::warn!( - "KV registry default id `{default_id}` could not be opened; dropping the KV registry" + by_id.insert( + (*id).to_owned(), + KvHandle::new(Arc::new(CloudflareKvStore::declared( + env.clone(), + id, + binding, + ))), ); } - Ok(StoreRegistry::from_parts(by_id, default_id)) + StoreRegistry::from_parts(by_id, meta.default.to_owned()) } fn build_secret_registry( @@ -444,15 +805,23 @@ fn build_secret_registry( StoreRegistry::from_parts(by_id, meta.default.to_owned()) } -async fn dispatch_core_request( +async fn dispatch_core_request( app: &App, mut core_request: Request, - stores: Stores, -) -> Result { + mut stores: Stores, + prepared: PreparedIngress, + deliver: Deliver, +) -> Result +where + Deliver: FnOnce(ResponseEgressEnvelope) -> Result, +{ // Hard-cutoff: see fastly's `dispatch_core_request` // for the rationale. Only registries go into extensions — // legacy bare handles are synthesised into a one-id registry // at the dispatch boundary. + if let Some(variables) = stores.runtime_variables.take() { + core_request.extensions_mut().insert(variables); + } let (config_registry, kv_registry, secret_registry) = synthesise_store_registries(stores); if let Some(registry) = config_registry { core_request.extensions_mut().insert(registry); @@ -463,12 +832,8 @@ async fn dispatch_core_request( if let Some(registry) = secret_registry { core_request.extensions_mut().insert(registry); } - let svc = app.router().clone(); - let response = svc - .oneshot(core_request) - .await - .map_err(|err| edge_error_to_worker(&err))?; - from_core_response(response).map_err(|err| edge_error_to_worker(&err)) + let response = app.dispatch_admitted(prepared, core_request).await; + deliver(response).map_err(|err| edge_error_to_worker(&err)) } fn edge_error_to_worker(err: &EdgeError) -> WorkerError { @@ -486,12 +851,37 @@ fn into_core_method(method: &Method) -> CoreMethod { }) } -fn open_config_or_warn(env: &Env, binding_name: &str) -> Option { +/// Begins canonical detached delivery of an assembly error without a configured app. +/// +/// Releases the unread request after capturing its method and abort signal. The caller supplies +/// the pre-assembly start and clock; no app callbacks, stores or configured headers are used. +/// +/// # Errors +/// Returns a category-safe delivery error if conversion and its bounded fallback both fail. +#[inline] +pub fn send_assembly_error( + req: CfRequest, + error: EdgeError, + ctx: Context, + request_start: MonotonicInstant, + clock: MonotonicClock, +) -> Result { + let method = into_core_method(&req.method()); + let runtime = CloudflareEgressRuntime::new(req.inner().signal(), Arc::new(ctx)); + drop(req); + from_egress_response( + App::assembly_error_egress(error, method, request_start, clock), + runtime, + ) + .map_err(|delivery_error| edge_error_to_worker(&delivery_error)) +} + +fn open_config_handle(env: &Env, binding_name: &str) -> ConfigStoreHandle { match CloudflareConfigStore::from_env(env, binding_name) { - Ok(store) => Some(ConfigStoreHandle::new(Arc::new(store))), + Ok(store) => ConfigStoreHandle::new(Arc::new(store)), Err(err) => { warn_missing_config_binding_once(binding_name, &err.to_string()); - None + ConfigStoreHandle::failed_open(ConfigStoreOpenFailure::Unavailable) } } } @@ -576,8 +966,743 @@ fn warn_missing_kv_binding_once(kv_binding: &str, error: &impl Display) { #[cfg(test)] mod tests { + use bytes::Bytes; + use std::cell::RefCell; + use std::rc::{Rc, Weak}; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::Poll; + use std::time::Duration; + use super::*; + use crate::test_runtime::NumericTimeoutGuard; + use async_stream::stream; + use edgezero_core::app::Hooks; + use edgezero_core::context::{BodyKind, RequestContext}; + use edgezero_core::http::{Response, StatusCode, response_builder}; + use edgezero_core::ingress::{AdmissionDecision, IngressGrant}; + use edgezero_core::outbound::{ + OutboundBatch, OutboundFuture, OutboundPending, OutboundRequest, OutboundResponse, + }; + use edgezero_core::response_egress::{ + ResponseEgressCompletion, ResponseEgressObserver, ResponseEgressReport, + }; + use edgezero_core::router::RouterService; + use futures::channel::oneshot; + use futures_util::future::join; + #[cfg(feature = "test-utils")] + use futures_util::stream::empty; + use futures_util::stream::poll_fn; use wasm_bindgen_test::wasm_bindgen_test; + use worker::js_sys::{Function, Object, Reflect, Uint8Array}; + use worker::wasm_bindgen::JsCast as _; + use worker::wasm_bindgen::JsValue; + use worker::worker_sys::Context as WorkerSysContext; + + thread_local! { + static RETAINED_SEND: RefCell>> = const { RefCell::new(None) }; + static RETAINED_UPLOAD: RefCell> = const { RefCell::new(Weak::new()) }; + static RETAINED_PROGRESS: RefCell> = const { RefCell::new(None) }; + } + + struct FailingScopeAssembly; + + struct ScopeFetchProbe { + probe: JsValue, + restore: Function, + } + + struct ScopeRoutes; + + struct ScopeTerminalObserver(Mutex>>); + + impl ResponseEgressObserver for ScopeTerminalObserver { + fn complete(&self, report: &ResponseEgressReport) { + let sender = self + .0 + .lock() + .expect("terminal sender") + .take() + .expect("one terminal report"); + sender.send(report.clone()).expect("terminal receiver"); + } + } + + impl ScopeFetchProbe { + fn count(&self, field: &str) -> Option { + Reflect::get(&self.probe, &JsValue::from_str(field)) + .expect("counter") + .as_f64() + } + + fn install() -> Self { + let probe = Function::new_no_args("const original = globalThis.fetch; const probe = {calls: 0, aborts: 0}; globalThis.fetch = (request, init) => { probe.calls++; if (init.encodeResponseBody !== 'manual' || request.redirect !== 'manual') throw new Error('manual policy lost'); init.signal.addEventListener('abort', () => { probe.aborts++; }, {once: true}); return new Promise(() => {}); }; probe.restore = () => {globalThis.fetch = original;}; return probe;") + .call0(&JsValue::UNDEFINED).expect("pending fetch probe"); + let restore = Reflect::get(&probe, &JsValue::from_str("restore")) + .expect("restore") + .dyn_into::() + .expect("restore function"); + Self { probe, restore } + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "Drop::pin_drop is unstable and conflicts with drop" + )] + impl Drop for ScopeFetchProbe { + fn drop(&mut self) { + self.restore + .call0(&JsValue::UNDEFINED) + .expect("restore fetch"); + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "exercise default application metadata through both native dispatch entrypoints" + )] + impl Hooks for ScopeRoutes { + fn routes() -> Result { + Ok(RouterService::builder() + .get("/", retain_progress_handler) + .build()) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "exercise defaults before typed route-assembly failure" + )] + impl Hooks for FailingScopeAssembly { + fn routes() -> Result { + let client = outbound_client(MonotonicClock::default()); + let send = client.send( + OutboundRequest::post("https://fixture.invalid/")?.body(pending_scope_upload()), + ); + RETAINED_SEND.with(|retained| *retained.borrow_mut() = Some(send)); + Err(EdgeError::service_unavailable("assembly fixture")) + } + } + + fn pending_scope_upload() -> Body { + let source = Rc::new(()); + RETAINED_UPLOAD.with(|retained| *retained.borrow_mut() = Rc::downgrade(&source)); + Body::from_stream(poll_fn(move |_| { + assert_eq!(Rc::strong_count(&source), 1); + Poll::>>::Pending + })) + } + + async fn retain_progress(client: &HttpClient) -> Result<(), EdgeError> { + let pending = client + .begin(OutboundRequest::get("https://fixture.invalid/begin")?) + .await?; + let batch = client.start_batch_until( + vec![ + OutboundRequest::get("https://fixture.invalid/first")?, + OutboundRequest::get("https://fixture.invalid/second")?, + ], + Deadline::after(Duration::from_secs(10)), + ); + RETAINED_PROGRESS.with(|retained| *retained.borrow_mut() = Some((pending, batch))); + Ok(()) + } + + async fn retain_progress_handler(ctx: RequestContext) -> Result { + retain_progress(&ctx.http_client().expect("request HTTP client")).await?; + Delay::from(Duration::from_millis(10)).await; + response_builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty()) + .map_err(EdgeError::internal) + } + + #[wasm_bindgen_test] + async fn canonical_dispatch_handoff_aborts_retained_begin_and_batch_fetches() { + let _timers = NumericTimeoutGuard::install(); + for manifest_dispatch in [false, true] { + let probe = ScopeFetchProbe::install(); + let app = App::build::(crate::CLOUDFLARE_PLATFORM).expect("application"); + let (env, context) = env_context(); + let req = native_request(Method::Get, "/", None); + let response = if manifest_dispatch { + crate::dispatch_app::(&app, req, env, context).await + } else { + CloudflareService::new(&app) + .dispatch(req, env, context) + .await + } + .expect("native handoff"); + let aborts_at_handoff = probe.count("aborts"); + let retained = RETAINED_PROGRESS.with(|retained| retained.borrow_mut().take()); + assert!( + retained.is_some(), + "both progress handles remain caller-retained" + ); + drop(retained); + assert_eq!(response.status_code(), StatusCode::NO_CONTENT.as_u16()); + assert_eq!(probe.count("calls"), Some(3.0_f64)); + assert_eq!( + aborts_at_handoff, + Some(3.0_f64), + "scope closes before handoff returns" + ); + assert_eq!( + probe.count("aborts"), + Some(3.0_f64), + "no duplicate abort on handle drop" + ); + } + } + + #[wasm_bindgen_test] + #[cfg(feature = "test-utils")] + async fn nested_dispatch_return_keeps_outer_callback_work_live() { + let _timers = NumericTimeoutGuard::install(); + for helper in [0_u8, 1, 2] { + let probe = ScopeFetchProbe::install(); + let outer = OutboundScopeOwner::callback(); + let app = App::build::(crate::CLOUDFLARE_PLATFORM).expect("application"); + outer + .run(async { + if helper == 2 { + drop( + dispatch_ingress_stream_for_test( + &app, + CoreMethod::GET, + "/".parse().expect("URI"), + empty::>(), + ) + .await + .expect("test dispatch helper"), + ); + } else { + let (env, context) = env_context(); + let req = native_request(Method::Get, "/", None); + let response = if helper == 1 { + crate::dispatch_app::(&app, req, env, context).await + } else { + CloudflareService::new(&app) + .dispatch(req, env, context) + .await + } + .expect("nested dispatch helper"); + assert_eq!(response.status_code(), StatusCode::NO_CONTENT.as_u16()); + } + }) + .await; + let aborts_at_helper_return = probe.count("aborts"); + drop(outer); + let aborts_at_outer_exit = probe.count("aborts"); + let retained = RETAINED_PROGRESS.with(|retained| retained.borrow_mut().take()); + assert!(retained.is_some(), "outer caller retains both handles"); + drop(retained); + assert_eq!(probe.count("calls"), Some(3.0_f64)); + assert_eq!( + aborts_at_helper_return, + Some(0.0_f64), + "helper return is not callback return" + ); + assert_eq!( + aborts_at_outer_exit, + Some(3.0_f64), + "only the outer callback closes work" + ); + assert_eq!(probe.count("aborts"), Some(3.0_f64), "no duplicate cleanup"); + } + } + + #[wasm_bindgen_test] + async fn closed_nested_dispatch_cannot_create_a_fresh_callback_scope() { + let _timers = NumericTimeoutGuard::install(); + let probe = ScopeFetchProbe::install(); + let outer = OutboundScopeOwner::callback(); + let application_status = Arc::new(Mutex::new(None)); + let observed = Arc::clone(&application_status); + let router = RouterService::builder() + .get("/", move |ctx: RequestContext| { + let recorded = Arc::clone(&observed); + async move { + let client = ctx.http_client().expect("request HTTP client"); + let error = client + .send( + OutboundRequest::get("https://fixture.invalid/")? + .timeout(Duration::ZERO), + ) + .await + .expect_err("closed scope refuses outbound dispatch"); + if !matches!(error, EdgeError::Internal { .. }) { + return Err(error); + } + let response = response_builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty()) + .map_err(EdgeError::internal)?; + *recorded.lock().expect("application status") = Some(response.status()); + Ok(response) + } + }) + .build(); + let app = App::new(router); + let (env, context) = env_context(); + outer.close(); + let error = outer + .run(CloudflareService::new(&app).dispatch( + native_request(Method::Get, "/", None), + env, + context, + )) + .await + .expect_err("closed ambient scope cannot create a new egress lifetime"); + assert_eq!( + *application_status.lock().expect("application status"), + Some(StatusCode::NO_CONTENT), + "helper must preserve the closed ambient scope" + ); + assert!( + matches!(error, WorkerError::RustError(message) if message == "internal error: response conversion failed") + ); + assert_eq!( + probe.count("calls"), + Some(0.0_f64), + "closed dispatch performs no SDK I/O" + ); + } + + #[wasm_bindgen_test] + async fn attached_body_owner_keeps_pre_head_fetches_live_without_changing_framing() { + let _timers = NumericTimeoutGuard::install(); + for streamed in [false, true] { + let probe = ScopeFetchProbe::install(); + let router = RouterService::builder() + .get("/", move |ctx: RequestContext| async move { + let owner = OutboundScopeOwner::response_body(); + owner + .run(retain_progress(&ctx.http_client().expect("HTTP client"))) + .await?; + let body = if streamed { + Body::from_stream(stream! { + yield Ok(Bytes::from_static(b"live")); + }) + } else { + Body::from(Bytes::from_static(b"live")) + }; + owner.attach(Response::new(body)) + }) + .build(); + let mut app = App::new(router); + let (published, terminal) = oneshot::channel(); + app.set_response_egress_observer(ScopeTerminalObserver(Mutex::new(Some(published)))); + let (env, context) = env_context(); + let mut response = CloudflareService::new(&app) + .dispatch(native_request(Method::Get, "/", None), env, context) + .await + .expect("native handoff before origin heads"); + assert_eq!(response.status_code(), StatusCode::OK.as_u16()); + let declared = response + .headers() + .get("content-length") + .expect("length header"); + assert_eq!(declared.as_deref(), if streamed { None } else { Some("4") }); + Delay::from(Duration::from_millis(10)).await; + assert_eq!(probe.count("calls"), Some(3.0_f64)); + assert_eq!( + probe.count("aborts"), + Some(0.0_f64), + "attached jobs survive callback return" + ); + assert_eq!(response.bytes().await.expect("body bytes"), b"live"); + let _report = terminal.await.expect("canonical terminal report"); + let aborts_at_terminal = probe.count("aborts"); + let retained = RETAINED_PROGRESS.with(|retained| retained.borrow_mut().take()); + assert!( + retained.is_some(), + "handles remain retained after body completion" + ); + drop(retained); + assert_eq!( + aborts_at_terminal, + Some(3.0_f64), + "exact attached owner closes at terminal" + ); + assert_eq!( + probe.count("aborts"), + Some(3.0_f64), + "each fetch aborts once" + ); + } + } + + #[wasm_bindgen_test] + async fn response_body_scope_keeps_fetches_live_until_eof_or_source_error() { + let _timers = NumericTimeoutGuard::install(); + for source_error in [false, true] { + let probe = ScopeFetchProbe::install(); + let (finish, completion_rx) = oneshot::channel::<()>(); + let shared_receiver = Arc::new(Mutex::new(Some(completion_rx))); + let router = RouterService::builder() + .get("/", move |ctx: RequestContext| { + let complete_body = shared_receiver + .lock() + .expect("finish receiver") + .take() + .expect("one body"); + async move { + let client = ctx.http_client().expect("request HTTP client"); + let source = stream! { + retain_progress(&client).await.expect("body progress"); + yield Ok(Bytes::from_static(b"live")); + complete_body.await.expect("body release"); + if source_error { + yield Err(EdgeError::bad_gateway("body fixture")); + } + }; + Ok::<_, EdgeError>(Response::new(Body::from_stream(source))) + } + }) + .build(); + let mut app = App::new(router); + let (published, terminal) = oneshot::channel(); + app.set_response_egress_observer(ScopeTerminalObserver(Mutex::new(Some(published)))); + let (env, context) = env_context(); + let mut response = CloudflareService::new(&app) + .dispatch(native_request(Method::Get, "/", None), env, context) + .await + .expect("streaming native handoff"); + let release = async { + Delay::from(Duration::from_millis(10)).await; + assert_eq!(probe.count("calls"), Some(3.0_f64)); + assert_eq!( + probe.count("aborts"), + Some(0.0_f64), + "body work survives callback handoff" + ); + finish.send(()).expect("release body"); + }; + let (received, ()) = join(response.bytes(), release).await; + if source_error { + assert!(received.is_err()); + } else { + assert_eq!(received.expect("body bytes"), b"live"); + } + let _report = terminal.await.expect("canonical egress terminal report"); + let aborts_at_terminal = probe.count("aborts"); + let retained = RETAINED_PROGRESS.with(|retained| retained.borrow_mut().take()); + assert!( + retained.is_some(), + "body still retains both progress handles" + ); + drop(retained); + assert_eq!( + aborts_at_terminal, + Some(3.0_f64), + "body terminal closes its scope" + ); + assert_eq!( + probe.count("aborts"), + Some(3.0_f64), + "each native fetch aborts once" + ); + } + } + + #[wasm_bindgen_test] + async fn run_app_scope_covers_assembly_error_and_native_handoff() { + let _timers = NumericTimeoutGuard::install(); + let (env, context) = env_context(); + let mut response = crate::run_app::( + native_request(Method::Get, "/", None), + env, + context, + ) + .await + .expect("assembly-error native handoff"); + let released_at_handoff = + RETAINED_UPLOAD.with(|retained| retained.borrow().upgrade().is_none()); + let retained = RETAINED_SEND.with(|slot| slot.borrow_mut().take()); + assert!( + retained.is_some(), + "assembly hook retains its unpolled send" + ); + drop(retained); + assert!( + released_at_handoff, + "assembly failure closes the outer callback" + ); + assert_eq!( + response.status_code(), + StatusCode::SERVICE_UNAVAILABLE.as_u16() + ); + assert!( + !response + .bytes() + .await + .expect("assembly-error body") + .is_empty() + ); + } + + #[wasm_bindgen_test] + async fn callback_handoff_releases_retained_unpolled_send_without_polling_upload() { + let polls = Arc::new(AtomicUsize::new(0)); + let counted = Arc::clone(&polls); + let router = RouterService::builder() + .get("/", move |ctx: RequestContext| { + let observed = Arc::clone(&counted); + async move { + let source = Rc::new(()); + RETAINED_UPLOAD + .with(|retained| *retained.borrow_mut() = Rc::downgrade(&source)); + let body = Body::from_stream(poll_fn(move |_| { + assert_eq!(Rc::strong_count(&source), 1); + observed.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + })); + let client = ctx.http_client().expect("request HTTP client"); + let send = client.send( + OutboundRequest::post("https://fixture.invalid/") + .expect("request") + .body(body), + ); + RETAINED_SEND.with(|retained| *retained.borrow_mut() = Some(send)); + response_builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty()) + .map_err(EdgeError::internal) + } + }) + .build(); + let app = App::new(router); + let (env, context) = env_context(); + let response = CloudflareService::new(&app) + .dispatch(native_request(Method::Get, "/", None), env, context) + .await + .expect("native handoff"); + let released_at_handoff = + RETAINED_UPLOAD.with(|retained| retained.borrow().upgrade().is_none()); + let retained = RETAINED_SEND.with(|slot| slot.borrow_mut().take()); + assert!(retained.is_some(), "caller still retains the unpolled send"); + drop(retained); + drop(response); + assert_eq!(polls.load(Ordering::SeqCst), 0); + assert!(released_at_handoff, "callback closes at native handoff"); + assert!(RETAINED_UPLOAD.with(|upload_ref| upload_ref.borrow().upgrade().is_none())); + } + + fn native_request(method: Method, path: &str, body: Option<&[u8]>) -> CfRequest { + let mut init = worker::RequestInit::new(); + init.with_method(method); + if let Some(bytes) = body { + init.with_body(Some(Uint8Array::from(bytes).into())); + } + CfRequest::new_with_init(&format!("https://example.com{path}"), &init) + .expect("native request") + } + + fn env_context() -> (Env, Context) { + Function::new_no_args(include_str!("../tests/worker_runtime.js")) + .call0(&JsValue::UNDEFINED) + .expect("guest Workers stream fixture"); + let context = Object::new(); + Reflect::set( + &context, + &JsValue::from_str("waitUntil"), + Function::new_with_args("_promise", "return undefined;").as_ref(), + ) + .expect("guest waitUntil"); + ( + Object::new().unchecked_into::(), + Context::new(context.unchecked_into::()), + ) + } + + async fn native_ingress( + app: &App, + req: CfRequest, + ) -> Result { + let (env, context) = env_context(); + let head = into_core_request_head(&req, env, context, app.monotonic_clock()) + .expect("request head"); + dispatch_ingress_stream_and( + app, + head, + Stores::default(), + app.monotonic_now(), + move || cloudflare_body_stream(req), + Ok, + ) + .await + } + + #[wasm_bindgen_test] + async fn native_null_body_stays_buffered_without_reading() { + for method in [Method::Get, Method::Head, Method::Post] { + let req = native_request(method, "/", None); + assert!(req.inner().body().is_none()); + let tripwire = Function::new_no_args("throw new Error('unexpected body read');"); + assert!( + Reflect::set(req.inner(), &JsValue::from_str("arrayBuffer"), &tripwire) + .expect("install read tripwire") + ); + let (env, context) = env_context(); + let request = into_core_request(req, env, context).await.expect("request"); + assert_eq!(request.body().as_bytes(), Some(&[][..])); + } + } + + #[wasm_bindgen_test] + async fn native_present_body_remains_a_lazy_stream_even_when_empty() { + for bytes in [b"".as_slice(), b"data".as_slice()] { + for value in [ + JsValue::from(Uint8Array::from(bytes)), + Uint8Array::from(bytes).buffer().into(), + ] { + let mut init = worker::RequestInit::new(); + init.with_method(Method::Post).with_body(Some(value)); + let mut req = + CfRequest::new_with_init("https://example.com/", &init).expect("request"); + req.headers_mut() + .expect("mutable headers") + .set("content-length", "0") + .expect("header"); + assert!(req.inner().body().is_some()); + let (env, context) = env_context(); + let request = into_core_request(req, env, context).await.expect("request"); + assert!(request.body().is_stream()); + assert_eq!( + request + .into_body() + .into_bytes_bounded(4) + .await + .expect("body") + .as_ref(), + bytes + ); + } + } + } + + #[wasm_bindgen_test] + async fn native_absence_does_not_depend_on_sdk_read_once_bookkeeping() { + for body in [None, Some(b"data".as_slice())] { + let mut req = native_request(Method::Post, "/", body); + assert_eq!( + req.bytes().await.expect("original read").as_slice(), + body.unwrap_or_default() + ); + let (env, context) = env_context(); + let outcome = into_core_request(req, env, context).await; + if body.is_none() { + assert_eq!( + outcome.expect("absent body").body().as_bytes(), + Some(&[][..]) + ); + } else { + assert!(matches!(outcome, Err(EdgeError::Internal { .. }))); + } + } + } + + #[wasm_bindgen_test] + async fn admitted_null_body_transfers_but_first_read_keeps_expired_deadline() { + #[edgezero_core::action] + async fn transfer(ctx: RequestContext) -> Result { + assert_eq!(ctx.body_kind(), BodyKind::Initial); + let request = ctx.into_request()?; + response_builder() + .header( + "x-body-buffered", + if request.body().is_stream() { + "no" + } else { + "yes" + }, + ) + .body(Body::empty()) + .map_err(EdgeError::internal) + } + + #[edgezero_core::action] + async fn read(ctx: RequestContext) -> Result { + assert_eq!(ctx.body_kind(), BodyKind::Initial); + let error = ctx.body_bytes(4).await.expect_err("expired first read"); + assert_eq!(error.status(), StatusCode::REQUEST_TIMEOUT); + assert_eq!(ctx.body_kind(), BodyKind::Poisoned); + assert_eq!( + ctx.take_body().expect_err("sticky timeout").status(), + error.status() + ); + Err(error) + } + + let start = MonotonicInstant::now(); + let mut app = App::new( + RouterService::builder() + .post("/transfer", transfer) + .post("/read", read) + .build(), + ); + app.set_monotonic_clock(MonotonicClock::new(move || start)); + app.set_ingress_admission_policy(|head| { + assert_eq!(head.framing(), IngressFraming::HostManaged); + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: Deadline::at_instant(head.request_start()), + } + }); + for body in [None, Some(b"".as_slice()), Some(b"data".as_slice())] { + for path in ["/transfer", "/read"] { + let mut egress = native_ingress(&app, native_request(Method::Post, path, body)) + .await + .expect("response"); + let response = egress.response_mut(); + if path == "/read" { + assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT); + } else { + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get("x-body-buffered").expect("header"), + if body.is_none() { "yes" } else { "no" } + ); + } + } + } + } + + #[wasm_bindgen_test] + async fn native_source_is_not_inspected_on_refusal_or_abort() { + for abort in [false, true] { + let mut app = App::new(RouterService::builder().build()); + app.set_ingress_admission_policy(move |_head| { + if abort { + AdmissionDecision::Abort + } else { + AdmissionDecision::Refuse { + completion: ResponseEgressCompletion::empty(), + response: response_builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .body(Body::empty()) + .expect("refusal"), + } + } + }); + let req = native_request(Method::Post, "/", Some(b"data")); + Function::new_with_args("request", + "Object.defineProperty(request, 'body', {get() {throw new Error('unexpected source inspection');}});") + .call1(&JsValue::UNDEFINED, req.inner()).expect("install source tripwire"); + let outcome = native_ingress(&app, req).await; + if abort { + assert!(outcome.is_err()); + } else { + assert_eq!( + outcome.expect("refusal").response_mut().status(), + StatusCode::SERVICE_UNAVAILABLE + ); + } + } + } #[wasm_bindgen_test] fn into_http_method_defaults_unknown_to_get() { @@ -592,6 +1717,78 @@ mod tests { assert_eq!(into_core_method(&Method::Put), CoreMethod::PUT); assert_eq!(into_core_method(&Method::Delete), CoreMethod::DELETE); } + + #[wasm_bindgen_test] + async fn standard_service_installs_the_exact_application_outbound_clock() { + async fn elapsed(ctx: RequestContext) -> Result { + let client = ctx + .http_client() + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("missing HTTP client")))?; + let request = + OutboundRequest::get("https://example.com/")?.body(Body::from("invalid GET body")); + let results = client + .send_all_until(vec![request], Deadline::after(Duration::from_secs(1))) + .await + .map_err(|failure| failure.into_parts().0)?; + let observed_slot = results + .slots() + .first() + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("missing outbound result")))?; + let edgezero_core::OutboundCollectedSlot::Resolved(result) = observed_slot else { + return Err(EdgeError::internal(anyhow::anyhow!( + "unresolved outbound result" + ))); + }; + if !matches!(&result.outcome, Err(EdgeError::BadRequest { .. })) { + return Err(EdgeError::internal(anyhow::anyhow!( + "clock probe did not fail during batch preflight" + ))); + } + response_builder() + .status(StatusCode::NO_CONTENT) + .header( + "x-edgezero-elapsed-ms", + result.elapsed.as_millis().to_string(), + ) + .body(Body::empty()) + .map_err(EdgeError::internal) + } + + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(7)) + .expect("completed instant"); + let observations = Arc::new(AtomicUsize::new(0)); + let clock_observations = Arc::clone(&observations); + let mut app = App::new(RouterService::builder().get("/clock", elapsed).build()); + app.set_monotonic_clock(MonotonicClock::new(move || { + if clock_observations.fetch_add(1, Ordering::SeqCst) < 2 { + start + } else { + completed + } + })); + let init = worker::RequestInit::new(); + let request = CfRequest::new_with_init("https://example.com/clock", &init) + .expect("Cloudflare request"); + let env = Object::new().unchecked_into::(); + let js_context = Object::new().unchecked_into::(); + + let response = CloudflareService::new(&app) + .dispatch(request, env, Context::new(js_context)) + .await + .expect("Cloudflare response"); + + assert_eq!(response.status_code(), StatusCode::NO_CONTENT.as_u16()); + assert_eq!( + response + .headers() + .get("x-edgezero-elapsed-ms") + .expect("elapsed header"), + Some("7".to_owned()) + ); + assert!(observations.load(Ordering::SeqCst) >= 3); + } } #[cfg(test)] @@ -599,7 +1796,10 @@ mod synthesis_tests { use std::collections::BTreeMap; use std::sync::Arc; - use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; + use edgezero_core::config_store::{ + BoundedStoreRead, ConfigStore, ConfigStoreError, ConfigStoreHandle, + finish_bounded_config_read, + }; use edgezero_core::key_value_store::{KvStore, NoopKvStore}; use edgezero_core::secret_store::{NoopSecretStore, SecretHandle}; @@ -608,9 +1808,32 @@ mod synthesis_tests { struct StubConfig; #[async_trait::async_trait(?Send)] impl ConfigStore for StubConfig { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Ok(None) } + + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + finish_bounded_config_read( + self.get(key).await, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + } } fn config_handle() -> ConfigStoreHandle { diff --git a/crates/edgezero-adapter-cloudflare/src/response.rs b/crates/edgezero-adapter-cloudflare/src/response.rs index 8e6b575cb..72674f949 100644 --- a/crates/edgezero-adapter-cloudflare/src/response.rs +++ b/crates/edgezero-adapter-cloudflare/src/response.rs @@ -1,92 +1,1662 @@ -use crate::response_headers::copy_headers; -use edgezero_core::body::Body; +#![cfg_attr( + all(feature = "cloudflare", target_arch = "wasm32"), + expect( + clippy::arbitrary_source_item_ordering, + reason = "the platform module follows runtime, writer, race, and entrypoint lifecycle order" + ) +)] + +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::time::Duration; + +use bytes::Bytes; +use edgezero_core::body::{Body, BodyPayload, BodyStream}; use edgezero_core::error::EdgeError; -use edgezero_core::http::Response; +use edgezero_core::http::header::CONTENT_ENCODING; +use edgezero_core::http::{HeaderMap, Method, Response, StatusCode}; +use edgezero_core::response_egress::{ + RESPONSE_EGRESS_FALLBACK_SAFETY_BUDGET, ResponseEgressAttempt, + ResponseEgressFallbackDisposition, ResponseEgressOutcome, ResponseEgressPolicy, +}; +use edgezero_core::response_egress_framing::ResponseEgressFramingMode; +use edgezero_core::response_egress_framing::{ + PreparedResponseEgress, response_egress_source_error_category, +}; +use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; use futures_util::StreamExt as _; -use worker::{Error as WorkerError, Response as CfResponse}; - -/// Convert an `EdgeZero` `Response` into a Cloudflare Worker `Response`. -/// -/// # Errors -/// Returns an [`EdgeError`] if the response body cannot be materialised -/// into a Workers response (empty body construction failure, byte body -/// conversion failure, stream adoption failure) or the Workers header table -/// rejects a text header. Non-text header values are skipped. -#[inline] -pub fn from_core_response(response: Response) -> Result { +use futures_util::future::{FutureExt as _, LocalBoxFuture}; +use futures_util::stream; + +const INTERNAL_FALLBACK_BODY: &[u8] = b"internal server error"; +const TIMEOUT_FALLBACK_BODY: &[u8] = b"response write deadline exceeded"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DeliveryError { + Cancelled, + Conversion, + Deadline, + Source, + Transport, +} + +impl DeliveryError { + const fn from_outcome(outcome: ResponseEgressOutcome) -> Self { + match outcome { + ResponseEgressOutcome::ConversionError => Self::Conversion, + ResponseEgressOutcome::DeadlineExceeded => Self::Deadline, + ResponseEgressOutcome::RequestCancelled => Self::Cancelled, + ResponseEgressOutcome::SourceError => Self::Source, + ResponseEgressOutcome::ClientDisconnected + | ResponseEgressOutcome::Completed + | ResponseEgressOutcome::HostHandoff + | ResponseEgressOutcome::TransportError + | ResponseEgressOutcome::Unspecified + | _ => Self::Transport, + } + } + + const fn outcome(self) -> ResponseEgressOutcome { + match self { + Self::Cancelled => ResponseEgressOutcome::RequestCancelled, + Self::Conversion => ResponseEgressOutcome::ConversionError, + Self::Deadline => ResponseEgressOutcome::DeadlineExceeded, + Self::Source => ResponseEgressOutcome::SourceError, + Self::Transport => ResponseEgressOutcome::TransportError, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum EgressKind { + Application, + Fallback, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum IoFailureKind { + Cancelled, + Deadline, + Transport, +} + +impl IoFailureKind { + const fn delivery_error(self) -> DeliveryError { + match self { + Self::Cancelled => DeliveryError::Cancelled, + Self::Deadline => DeliveryError::Deadline, + Self::Transport => DeliveryError::Transport, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct IoFailure { + accepted: usize, + kind: IoFailureKind, +} + +enum StartOutcome { + Precommit { + attempt: Box, + cause: ResponseEgressOutcome, + }, + Started(PlatformResponse), +} + +trait CloudflareEgressCommitter { + type Io: CloudflareEgressIo + 'static; + type Response; + + fn prepare( + &mut self, + status: StatusCode, + headers: &HeaderMap, + declared_length: Option, + transmits_body: bool, + ) -> Result<(Self::Response, Self::Io), DeliveryError>; + + fn spawn(&mut self, task: LocalBoxFuture<'static, ()>); +} + +#[async_trait::async_trait(?Send)] +trait CloudflareEgressIo { + fn abort(&mut self); + + async fn finish( + &mut self, + deadline: Deadline, + clock: &MonotonicClock, + ) -> Result<(), IoFailureKind>; + + async fn next_body( + &mut self, + source: &mut BodyStream, + deadline: Deadline, + clock: &MonotonicClock, + attempt: &mut ResponseEgressAttempt, + ) -> Result>, IoFailureKind>; + + fn preflight(&self, deadline: Deadline, clock: &MonotonicClock) -> Result<(), IoFailureKind>; + + async fn write( + &mut self, + bytes: Vec, + deadline: Deadline, + clock: &MonotonicClock, + ) -> Result; +} + +async fn transmit_committed( + body: Body, + policy: ResponseEgressPolicy, + mut attempt: ResponseEgressAttempt, + clock: MonotonicClock, + kind: EgressKind, + mut io: Io, +) -> Result<(), DeliveryError> +where + Io: CloudflareEgressIo, +{ + let result = pump_body(body, policy.write_deadline, &mut attempt, &clock, &mut io).await; + match result { + Ok(()) => { + settle_success(kind, &mut attempt, clock.now()); + Ok(()) + } + Err(error) => { + abort(&mut io); + let observed_at = terminal_observed_at(error, policy.write_deadline, &clock); + settle_failure(kind, &mut attempt, error.outcome(), observed_at); + Err(error) + } + } +} + +async fn pump_body( + body: Body, + deadline: Deadline, + attempt: &mut ResponseEgressAttempt, + clock: &MonotonicClock, + io: &mut Io, +) -> Result<(), DeliveryError> +where + Io: CloudflareEgressIo, +{ + ensure_deadline(deadline, clock)?; + let mut source = body_source(body); + loop { + let observed = io.next_body(&mut source, deadline, clock, attempt).await; + if let Ok(Some(Err(error))) = &observed { + attempt.record_source_error(error); + } + let next = observed.map_err(IoFailureKind::delivery_error)?; + ensure_deadline(deadline, clock)?; + let Some(item) = next else { + break; + }; + let chunk = item.map_err(|error| { + log::warn!( + "response-egress Cloudflare body source failed after commit: {}", + response_egress_source_error_category(&error) + ); + DeliveryError::Source + })?; + if chunk.is_empty() { + continue; + } + let requested = chunk.len(); + match io.write(chunk.to_vec(), deadline, clock).await { + Ok(accepted) => { + account_prefix(accepted, requested, attempt, clock.now())?; + if accepted != requested { + return Err(DeliveryError::Transport); + } + } + Err(failure) => { + account_prefix(failure.accepted, requested, attempt, clock.now())?; + return Err(failure.kind.delivery_error()); + } + } + ensure_deadline(deadline, clock)?; + } + io.finish(deadline, clock) + .await + .map_err(IoFailureKind::delivery_error)?; + ensure_deadline(deadline, clock) +} + +fn account_prefix( + accepted: usize, + requested: usize, + attempt: &mut ResponseEgressAttempt, + observed_at: MonotonicInstant, +) -> Result<(), DeliveryError> { + if accepted > requested { + return Err(DeliveryError::Transport); + } + let accepted_u64 = u64::try_from(accepted).map_err(|_error| DeliveryError::Transport)?; + if !attempt.account_bytes(accepted_u64, observed_at) { + return Err(DeliveryError::Transport); + } + Ok(()) +} + +fn abort(io: &mut Io) +where + Io: CloudflareEgressIo, +{ + if catch_unwind(AssertUnwindSafe(|| io.abort())).is_err() { + log::error!("response-egress Cloudflare abort panicked"); + } +} + +fn body_source(body: Body) -> BodyStream { + let (payload, _scope) = body.into_parts(); + match payload { + BodyPayload::Once(bytes) => stream::once(async move { Ok(bytes) }).boxed_local(), + BodyPayload::Stream(source) => source, + } +} + +fn ensure_deadline(deadline: Deadline, clock: &MonotonicClock) -> Result<(), DeliveryError> { + if deadline.is_expired_at(clock.now()) { + Err(DeliveryError::Deadline) + } else { + Ok(()) + } +} + +fn terminal_observed_at( + error: DeliveryError, + deadline: Deadline, + clock: &MonotonicClock, +) -> MonotonicInstant { + let observed_at = clock.now(); + if error == DeliveryError::Deadline { + observed_at.max(deadline.instant()) + } else { + observed_at + } +} + +fn settle_failure( + kind: EgressKind, + attempt: &mut ResponseEgressAttempt, + outcome: ResponseEgressOutcome, + observed_at: MonotonicInstant, +) { + match kind { + EgressKind::Application => { + attempt.terminate(outcome, observed_at); + } + EgressKind::Fallback => { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, observed_at); + } + } +} + +fn settle_success( + kind: EgressKind, + attempt: &mut ResponseEgressAttempt, + observed_at: MonotonicInstant, +) { + match kind { + EgressKind::Application => { + attempt.terminate(ResponseEgressOutcome::HostHandoff, observed_at); + } + EgressKind::Fallback => { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Completed, observed_at); + } + } +} + +fn start_prepared( + prepared: PreparedResponseEgress, + policy: ResponseEgressPolicy, + mut attempt: ResponseEgressAttempt, + clock: MonotonicClock, + kind: EgressKind, + committer: &mut Committer, +) -> StartOutcome +where + Committer: CloudflareEgressCommitter, +{ + if policy.write_deadline.is_expired_at(clock.now()) { + return StartOutcome::Precommit { + attempt: Box::new(attempt), + cause: ResponseEgressOutcome::DeadlineExceeded, + }; + } + let declared_length = prepared.declared_length(); + let transmits_body = prepared.transmits_body(); + let response = prepared.into_response(); let (parts, body) = response.into_parts(); + let (platform_response, mut io) = match committer.prepare( + parts.status, + &parts.headers, + declared_length, + transmits_body, + ) { + Ok(pair) => pair, + Err(error) => { + return StartOutcome::Precommit { + attempt: Box::new(attempt), + cause: error.outcome(), + }; + } + }; + if let Err(error) = io.preflight(policy.write_deadline, &clock) { + abort(&mut io); + return StartOutcome::Precommit { + attempt: Box::new(attempt), + cause: error.delivery_error().outcome(), + }; + } + if !attempt.begin_writing() { + abort(&mut io); + return StartOutcome::Precommit { + attempt: Box::new(attempt), + cause: ResponseEgressOutcome::ConversionError, + }; + } + if !transmits_body { + settle_success(kind, &mut attempt, clock.now()); + return StartOutcome::Started(platform_response); + } + let task = AssertUnwindSafe(transmit_committed(body, policy, attempt, clock, kind, io)) + .catch_unwind() + .map(|result| { + if result.is_err() { + log::error!("response-egress Cloudflare coordinator panicked"); + } + }) + .boxed_local(); + committer.spawn(task); + StartOutcome::Started(platform_response) +} + +fn start_fallback( + request_method: &Method, + original_cause: ResponseEgressOutcome, + mut attempt: ResponseEgressAttempt, + clock: &MonotonicClock, + committer: &mut Committer, +) -> Result +where + Committer: CloudflareEgressCommitter, +{ + if !attempt.begin_fallback(original_cause) { + attempt.terminate(original_cause, clock.now()); + return Err(DeliveryError::from_outcome(original_cause)); + } + let started_at = clock.now(); + let fallback_deadline = started_at + .checked_add(RESPONSE_EGRESS_FALLBACK_SAFETY_BUDGET) + .unwrap_or(started_at); + let prepared = attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + request_method, + fallback_response(original_cause), + ) + .map_err(|_error| { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, clock.now()); + DeliveryError::from_outcome(original_cause) + })?; + match start_prepared( + prepared, + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(fallback_deadline), + }, + attempt, + clock.clone(), + EgressKind::Fallback, + committer, + ) { + StartOutcome::Started(response) => Ok(response), + StartOutcome::Precommit { + attempt: mut failed_attempt, + cause: _fallback_cause, + } => { + failed_attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, clock.now()); + Err(DeliveryError::from_outcome(original_cause)) + } + } +} + +fn fallback_response(cause: ResponseEgressOutcome) -> Response { + let (status, body) = if cause == ResponseEgressOutcome::DeadlineExceeded { + (StatusCode::GATEWAY_TIMEOUT, TIMEOUT_FALLBACK_BODY) + } else { + (StatusCode::INTERNAL_SERVER_ERROR, INTERNAL_FALLBACK_BODY) + }; + let mut response = Response::new(Body::from(body)); + *response.status_mut() = status; + response +} + +fn manual_response_encoding(headers: &HeaderMap, transmits_body: bool) -> bool { + transmits_body && headers.contains_key(CONTENT_ENCODING) +} + +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +mod platform { + use super::{ + BodyStream, Bytes, CloudflareEgressCommitter, CloudflareEgressIo, Deadline, DeliveryError, + Duration, EdgeError, EgressKind, HeaderMap, IoFailure, IoFailureKind, LocalBoxFuture, + MonotonicClock, ResponseEgressFramingMode, StartOutcome, StatusCode, ensure_deadline, + manual_response_encoding, start_fallback, start_prepared, + }; + use std::future::Future; + use std::sync::Arc; + + use edgezero_core::response_egress::{ResponseEgressAttempt, ResponseEgressEnvelope}; + use futures_util::FutureExt as _; + use futures_util::StreamExt as _; + use futures_util::future::{Either, select}; + use worker::js_sys::{BigInt, Uint8Array}; + use worker::wasm_bindgen_futures::JsFuture; + use worker::worker_sys::FixedLengthStream; + use worker::{AbortSignal, Context, Delay, EncodeBody, Response as CfResponse}; + + use crate::outbound::copy_header_values; + + const ABORT_POLL_INTERVAL: Duration = Duration::from_millis(1); + + const fn public_message(error: DeliveryError) -> &'static str { + match error { + DeliveryError::Cancelled => "request cancelled during response delivery", + DeliveryError::Conversion => "response conversion failed", + DeliveryError::Deadline => "response write deadline exceeded", + DeliveryError::Source => "response body source failed", + DeliveryError::Transport => "response transport failed", + } + } + + pub(crate) struct CloudflareEgressRuntime { + context: Arc, + signal: AbortSignal, + } + + impl CloudflareEgressRuntime { + pub(crate) fn new(signal: web_sys::AbortSignal, context: Arc) -> Self { + Self { + context, + signal: signal.into(), + } + } + } + + struct WorkersCommitter { + context: Arc, + signal: AbortSignal, + } + + impl CloudflareEgressCommitter for WorkersCommitter { + type Io = WorkersEgressIo; + type Response = CfResponse; + + fn prepare( + &mut self, + status: StatusCode, + headers: &HeaderMap, + declared_length: Option, + transmits_body: bool, + ) -> Result<(Self::Response, Self::Io), DeliveryError> { + let mut worker_headers = worker::Headers::new(); + copy_header_values(headers, &mut worker_headers, |_name| { + EdgeError::internal(anyhow::anyhow!( + "response header cannot be represented by Workers" + )) + }) + .map_err(|_error| DeliveryError::Conversion)?; + let mut builder = CfResponse::builder() + .with_status(status.as_u16()) + .with_headers(worker_headers); + if manual_response_encoding(headers, transmits_body) { + builder = builder.with_encode_body(EncodeBody::Manual); + } + if !transmits_body { + return Ok(( + builder.empty(), + WorkersEgressIo { + signal: self.signal.clone(), + writer: None, + }, + )); + } + let (readable, writable) = if let Some(length) = declared_length { + let fixed = if let Ok(length_u32) = u32::try_from(length) { + FixedLengthStream::new(length_u32) + } else { + FixedLengthStream::new_big_int(BigInt::from(length)) + } + .map_err(|_error| DeliveryError::Conversion)?; + (fixed.readable(), fixed.writable()) + } else { + let transform = + web_sys::TransformStream::new().map_err(|_error| DeliveryError::Conversion)?; + (transform.readable(), transform.writable()) + }; + let writer = writable + .get_writer() + .map_err(|_error| DeliveryError::Conversion)?; + Ok(( + builder.stream(readable), + WorkersEgressIo { + signal: self.signal.clone(), + writer: Some(writer), + }, + )) + } + + fn spawn(&mut self, task: LocalBoxFuture<'static, ()>) { + self.context.wait_until(task); + } + } + + struct WorkersEgressIo { + signal: AbortSignal, + writer: Option, + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for WorkersEgressIo { + fn drop(&mut self) { + self.abort(); + } + } + + enum RaceEvent { + Cancelled, + Deadline, + Operation { + output: Output, + terminal: Option, + }, + } + + async fn wait_for_abort(signal: AbortSignal) { + while !signal.aborted() { + Delay::from(ABORT_POLL_INTERVAL).await; + } + } - let body_response = match body { - Body::Once(bytes) if bytes.is_empty() => { - CfResponse::empty().map_err(EdgeError::internal)? + async fn race_operation( + operation: OperationFactory, + deadline: Deadline, + clock: &MonotonicClock, + signal: &AbortSignal, + ) -> RaceEvent + where + OperationFactory: FnOnce() -> Operation, + Operation: Future, + { + if signal.aborted() { + return RaceEvent::Cancelled; } - Body::Once(bytes) => CfResponse::from_bytes(bytes.to_vec()).map_err(EdgeError::internal)?, - Body::Stream(stream) => { - let worker_stream = stream - .map(|res| match res { - Ok(bytes) => Ok::, WorkerError>(bytes.to_vec()), - Err(err) => Err(WorkerError::RustError(err.to_string())), + let Some(remaining) = deadline.remaining_at(clock.now()) else { + return RaceEvent::Deadline; + }; + let cancellation = wait_for_abort(signal.clone()); + let timer = Delay::from(remaining); + let guard = async move { + futures_util::pin_mut!(cancellation, timer); + match select(cancellation, timer).await { + Either::Left(((), _timer)) => RaceEvent::Cancelled, + Either::Right(((), _cancellation)) => RaceEvent::Deadline, + } + }; + let operation_future = operation(); + futures_util::pin_mut!(guard, operation_future); + match select(guard, operation_future).await { + Either::Left((event, _operation)) => event, + Either::Right((output, _guard)) => { + let terminal = if signal.aborted() { + Some(IoFailureKind::Cancelled) + } else if deadline.is_expired_at(clock.now()) { + Some(IoFailureKind::Deadline) + } else { + None + }; + RaceEvent::Operation { output, terminal } + } + } + } + + #[async_trait::async_trait(?Send)] + impl CloudflareEgressIo for WorkersEgressIo { + fn abort(&mut self) { + if let Some(writer) = self.writer.take() { + drop(writer.abort()); + writer.release_lock(); + } + } + + async fn finish( + &mut self, + deadline: Deadline, + clock: &MonotonicClock, + ) -> Result<(), IoFailureKind> { + let Some(writer) = self.writer.as_ref() else { + return ensure_deadline(deadline, clock).map_err(|_error| IoFailureKind::Deadline); + }; + let event = race_operation( + || JsFuture::from(writer.close()), + deadline, + clock, + &self.signal, + ) + .await; + let result = match event { + RaceEvent::Cancelled => Err(IoFailureKind::Cancelled), + RaceEvent::Deadline => Err(IoFailureKind::Deadline), + RaceEvent::Operation { + terminal: Some(kind), + .. + } => Err(kind), + RaceEvent::Operation { + output: Ok(_value), + terminal: None, + } => Ok(()), + RaceEvent::Operation { + output: Err(_error), + terminal: None, + } => Err(IoFailureKind::Transport), + }; + if result.is_ok() + && let Some(finished_writer) = self.writer.take() + { + finished_writer.release_lock(); + } + result + } + + async fn next_body( + &mut self, + source: &mut BodyStream, + deadline: Deadline, + clock: &MonotonicClock, + attempt: &mut ResponseEgressAttempt, + ) -> Result>, IoFailureKind> { + Delay::from(Duration::ZERO).await; + match race_operation( + || { + source.next().inspect(move |item| { + if let Some(Err(error)) = item { + attempt.record_source_error(error); + } + }) + }, + deadline, + clock, + &self.signal, + ) + .await + { + RaceEvent::Cancelled => Err(IoFailureKind::Cancelled), + RaceEvent::Deadline => Err(IoFailureKind::Deadline), + RaceEvent::Operation { + terminal: Some(kind), + .. + } => Err(kind), + RaceEvent::Operation { + output: item, + terminal: None, + } => Ok(item), + } + } + + fn preflight( + &self, + deadline: Deadline, + clock: &MonotonicClock, + ) -> Result<(), IoFailureKind> { + if self.signal.aborted() { + Err(IoFailureKind::Cancelled) + } else { + ensure_deadline(deadline, clock).map_err(|_error| IoFailureKind::Deadline) + } + } + + async fn write( + &mut self, + bytes: Vec, + deadline: Deadline, + clock: &MonotonicClock, + ) -> Result { + let requested = bytes.len(); + let Some(writer) = self.writer.as_ref() else { + return Err(IoFailure { + accepted: 0, + kind: IoFailureKind::Transport, + }); + }; + let chunk = Uint8Array::from(bytes.as_slice()); + let event = race_operation( + || JsFuture::from(writer.write_with_chunk(&chunk.into())), + deadline, + clock, + &self.signal, + ) + .await; + match event { + RaceEvent::Cancelled => Err(IoFailure { + accepted: 0, + kind: IoFailureKind::Cancelled, + }), + RaceEvent::Deadline => Err(IoFailure { + accepted: 0, + kind: IoFailureKind::Deadline, + }), + RaceEvent::Operation { + output: Ok(_value), + terminal: Some(kind), + } => Err(IoFailure { + accepted: requested, + kind, + }), + RaceEvent::Operation { + output: Ok(_value), + terminal: None, + } => Ok(requested), + RaceEvent::Operation { + output: Err(_error), + terminal, + } => Err(IoFailure { + accepted: 0, + kind: terminal.unwrap_or(IoFailureKind::Transport), + }), + } + } + } + + /// Converts an owned egress envelope into a Workers response backed by one adapter-owned writer. + /// + /// # Errors + /// Returns a category-only error when both the application response and bounded precommit fallback + /// fail before the Workers response is returned. + pub(crate) fn from_egress_response( + egress: ResponseEgressEnvelope, + runtime: CloudflareEgressRuntime, + ) -> Result { + let request_method = egress.request_method().clone(); + let mut committer = WorkersCommitter { + context: runtime.context, + signal: runtime.signal, + }; + let result = match egress.begin(ResponseEgressFramingMode::ExplicitBufferedLength) { + Ok((prepared, policy, attempt, clock)) => { + match start_prepared( + prepared, + policy, + attempt, + clock.clone(), + EgressKind::Application, + &mut committer, + ) { + StartOutcome::Started(response) => Ok(response), + StartOutcome::Precommit { + attempt: failed_attempt, + cause, + } => start_fallback( + &request_method, + cause, + *failed_attempt, + &clock, + &mut committer, + ), + } + } + Err(failure) => { + let (attempt, clock, cause) = failure.into_parts(); + start_fallback(&request_method, cause, attempt, &clock, &mut committer) + } + }; + result.map_err(|error| EdgeError::internal(anyhow::anyhow!(public_message(error)))) + } + + #[cfg(test)] + mod race_tests { + use std::cell::Cell; + use std::rc::Rc; + use std::sync::Mutex; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + + use edgezero_core::error::BadGatewayReason; + use edgezero_core::error_detail::{ + CloudflareFailureCause, ProviderFailureDetail, ProviderFailurePhase, + }; + use edgezero_core::http::Version; + use edgezero_core::response_egress::{ + ResponseEgressBodyKind, ResponseEgressCompletion, ResponseEgressHead, + ResponseEgressObserverHandle, ResponseEgressOutcome, ResponseEgressReport, + }; + use edgezero_core::time::MonotonicInstant; + use futures_util::{future, stream}; + use wasm_bindgen_test::wasm_bindgen_test; + + use super::*; + use crate::test_runtime::NumericTimeoutGuard; + + #[derive(Clone, Copy, Eq, PartialEq)] + enum SourceCase { + AlreadyAborted, + AlreadyExpired, + Eof, + Ready, + ReadyAborted, + ReadyExpired, + } + + async fn observed_source( + case: SourceCase, + with_detail: bool, + ) -> (ResponseEgressReport, usize) { + let controller = web_sys::AbortController::new().expect("controller"); + if case == SourceCase::AlreadyAborted { + controller.abort(); + } + let now = MonotonicInstant::now(); + let deadline = now.checked_add(Duration::from_secs(10)).expect("deadline"); + let expired = Arc::new(AtomicBool::new(case == SourceCase::AlreadyExpired)); + let clock_expired = Arc::clone(&expired); + let clock = MonotonicClock::new(move || { + if clock_expired.load(Ordering::SeqCst) { + deadline + } else { + now + } + }); + let polls = Arc::new(AtomicUsize::new(0)); + let source_polls = Arc::clone(&polls); + let body_controller = controller.clone(); + let mut source = if case == SourceCase::Eof { + stream::empty().boxed_local() + } else { + stream::once(async move { + source_polls.fetch_add(1, Ordering::SeqCst); + expired.store(case == SourceCase::ReadyExpired, Ordering::SeqCst); + if case == SourceCase::ReadyAborted { + body_controller.abort(); + } + let detail = ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: CloudflareFailureCause::TypeError, + }; + Err(if with_detail { + EdgeError::bad_gateway_with_detail( + "provider-secret-token", + BadGatewayReason::Transport, + detail, + ) + } else { + EdgeError::bad_gateway_with_reason( + "provider-secret-token", + BadGatewayReason::Transport, + ) + }) }) - .boxed_local(); - CfResponse::from_stream(worker_stream).map_err(EdgeError::internal)? + .boxed_local() + }; + let reports = Arc::new(Mutex::new(Vec::new())); + let completed = Arc::clone(&reports); + let headers = HeaderMap::new(); + let head = ResponseEgressHead::new( + StatusCode::OK, + Version::HTTP_11, + &headers, + None, + now, + None, + ); + let mut attempt = ResponseEgressAttempt::new( + &head, + now, + ResponseEgressCompletion::new(move |report| { + completed.lock().expect("reports").push(report.clone()); + }), + ResponseEgressObserverHandle::default(), + clock.clone(), + ); + assert!(attempt.begin_writing()); + let mut io = WorkersEgressIo { + signal: controller.signal().into(), + writer: None, + }; + let timers = NumericTimeoutGuard::install(); + let result = io + .next_body( + &mut source, + Deadline::at_instant(deadline), + &clock, + &mut attempt, + ) + .await; + drop(timers); + let outcome = match result { + Ok(Some(Err(_))) => ResponseEgressOutcome::SourceError, + Ok(None) => ResponseEgressOutcome::Completed, + Err(IoFailureKind::Deadline) => ResponseEgressOutcome::DeadlineExceeded, + Err(IoFailureKind::Cancelled) => ResponseEgressOutcome::RequestCancelled, + _ => panic!("unexpected source result"), + }; + assert!(attempt.terminate(outcome, clock.now())); + drop(attempt); + let retained = reports.lock().expect("reports"); + assert_eq!(retained.len(), 1); + (retained[0].clone(), polls.load(Ordering::SeqCst)) } - }; - let mut cf_response = body_response.with_status(parts.status.as_u16()); - let headers = cf_response.headers_mut(); - copy_headers(&parts.headers, |name, value, replace| { - if replace { - headers.set(name, value) - } else { - headers.append(name, value) + #[wasm_bindgen_test] + async fn provider_diagnostics_survive_real_source_race_preemption() { + for case in [ + SourceCase::AlreadyAborted, + SourceCase::AlreadyExpired, + SourceCase::Eof, + SourceCase::Ready, + SourceCase::ReadyAborted, + SourceCase::ReadyExpired, + ] { + for with_detail in [false, true] { + let (report, polls) = observed_source(case, with_detail).await; + let observed = matches!( + case, + SourceCase::Ready | SourceCase::ReadyAborted | SourceCase::ReadyExpired + ); + assert_eq!(polls, usize::from(observed)); + assert_eq!( + report.outcome, + match case { + SourceCase::AlreadyAborted | SourceCase::ReadyAborted => + ResponseEgressOutcome::RequestCancelled, + SourceCase::AlreadyExpired | SourceCase::ReadyExpired => + ResponseEgressOutcome::DeadlineExceeded, + SourceCase::Eof => ResponseEgressOutcome::Completed, + SourceCase::Ready => ResponseEgressOutcome::SourceError, + } + ); + assert_eq!(report.source_failure.is_some(), observed); + if let Some(failure) = report.source_failure { + assert_eq!(failure.category(), "bad_gateway_transport"); + assert_eq!(failure.body_kind(), ResponseEgressBodyKind::Application); + let expected = ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: CloudflareFailureCause::TypeError, + }; + assert_eq!( + failure.provider_failure_detail(), + with_detail.then_some(&expected) + ); + assert!(!format!("{failure:?}").contains("secret")); + } + } + } + } + + #[wasm_bindgen_test] + async fn response_egress_preflight_precedes_writer_operation() { + let controller = web_sys::AbortController::new().expect("abort controller"); + controller.abort(); + let signal: AbortSignal = controller.signal().into(); + let now = MonotonicInstant::now(); + let deadline = + Deadline::at_instant(now.checked_add(Duration::from_secs(1)).expect("deadline")); + let clock = MonotonicClock::new(move || now); + let constructed = Rc::new(Cell::new(false)); + let observed = Rc::clone(&constructed); + + let event = race_operation( + move || { + observed.set(true); + future::ready(()) + }, + deadline, + &clock, + &signal, + ) + .await; + + assert!(matches!(event, RaceEvent::Cancelled)); + assert!(!constructed.get()); } - }) - .map_err(EdgeError::internal)?; - Ok(cf_response) + } } +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +pub(crate) use platform::{CloudflareEgressRuntime, from_egress_response}; + #[cfg(test)] mod tests { - use super::*; - use bytes::Bytes; - use edgezero_core::body::Body; - use edgezero_core::http::response_builder; + use edgezero_core::response_egress_framing::prepare_response_egress; + use std::cell::{Cell, RefCell}; + use std::collections::VecDeque; + use std::mem; + use std::rc::Rc; + use std::sync::{Arc, Mutex}; + use std::task::Poll; + + use edgezero_core::http::header::CONTENT_ENCODING; + use edgezero_core::http::{Version, response_builder}; + use edgezero_core::response_egress::{ + ResponseEgressBodyKind, ResponseEgressCompletion, ResponseEgressHead, + ResponseEgressObserver, ResponseEgressObserverHandle, ResponseEgressReport, + }; use futures::executor::block_on; - use futures_util::stream; + use futures_util::stream::poll_fn; + + use super::*; + + #[derive(Clone, Default)] + struct RecordingObserver(Arc>>); + + impl RecordingObserver { + fn reports(&self) -> Vec { + self.0.lock().expect("reports lock").clone() + } + } + + impl ResponseEgressObserver for RecordingObserver { + fn complete(&self, report: &ResponseEgressReport) { + self.0.lock().expect("reports lock").push(report.clone()); + } + } + + #[derive(Clone, Copy)] + enum MockWrite { + Accept, + Fail { accepted: usize }, + } + + #[derive(Default)] + struct MockState { + aborts: usize, + closes: usize, + writes: Vec>, + } + + struct MockIo { + close_error: Option, + preflight_error: Option, + state: Rc>, + writes: VecDeque, + } + + #[async_trait::async_trait(?Send)] + impl CloudflareEgressIo for MockIo { + fn abort(&mut self) { + let mut state = self.state.borrow_mut(); + state.aborts = state.aborts.saturating_add(1); + } + + async fn finish( + &mut self, + _deadline: Deadline, + _clock: &MonotonicClock, + ) -> Result<(), IoFailureKind> { + self.state.borrow_mut().closes += 1; + self.close_error.map_or(Ok(()), Err) + } + + async fn next_body( + &mut self, + source: &mut BodyStream, + _deadline: Deadline, + _clock: &MonotonicClock, + _attempt: &mut ResponseEgressAttempt, + ) -> Result>, IoFailureKind> { + Ok(source.next().await) + } + + fn preflight( + &self, + _deadline: Deadline, + _clock: &MonotonicClock, + ) -> Result<(), IoFailureKind> { + self.preflight_error.map_or(Ok(()), Err) + } + + async fn write( + &mut self, + bytes: Vec, + _deadline: Deadline, + _clock: &MonotonicClock, + ) -> Result { + self.state.borrow_mut().writes.push(bytes.clone()); + match self.writes.pop_front().unwrap_or(MockWrite::Accept) { + MockWrite::Accept => Ok(bytes.len()), + MockWrite::Fail { accepted } => Err(IoFailure { + accepted, + kind: IoFailureKind::Transport, + }), + } + } + } + + #[derive(Debug)] + struct MockResponse { + declared_length: Option, + headers: HeaderMap, + status: StatusCode, + transmits_body: bool, + } + + struct MockCommitter { + close_error: Option, + preflight_error: Option, + prepare_error: Option, + state: Rc>, + writes: VecDeque, + } + + impl MockCommitter { + fn new() -> (Self, Rc>) { + let state = Rc::new(RefCell::new(MockState::default())); + ( + Self { + close_error: None, + preflight_error: None, + prepare_error: None, + state: Rc::clone(&state), + writes: VecDeque::new(), + }, + state, + ) + } + } + + impl CloudflareEgressCommitter for MockCommitter { + type Io = MockIo; + type Response = MockResponse; + + fn prepare( + &mut self, + status: StatusCode, + headers: &HeaderMap, + declared_length: Option, + transmits_body: bool, + ) -> Result<(Self::Response, Self::Io), DeliveryError> { + if let Some(error) = self.prepare_error.take() { + return Err(error); + } + Ok(( + MockResponse { + declared_length, + headers: headers.clone(), + status, + transmits_body, + }, + MockIo { + close_error: self.close_error, + preflight_error: self.preflight_error, + state: Rc::clone(&self.state), + writes: mem::take(&mut self.writes), + }, + )) + } + + fn spawn(&mut self, task: LocalBoxFuture<'static, ()>) { + block_on(task); + } + } + + fn attempt( + observer: &RecordingObserver, + started_at: MonotonicInstant, + ) -> ResponseEgressAttempt { + let headers = HeaderMap::new(); + let head = ResponseEgressHead::new( + StatusCode::OK, + Version::HTTP_11, + &headers, + None, + started_at, + None, + ); + ResponseEgressAttempt::new( + &head, + started_at, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + ) + } + + fn prepared(method: &Method, body: Body) -> PreparedResponseEgress { + prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + method, + response_builder() + .status(StatusCode::OK) + .body(body) + .expect("response"), + ) + .expect("prepared response") + } + + fn policy(deadline: MonotonicInstant) -> ResponseEgressPolicy { + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(deadline), + } + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn framing_observer_preserves_each_typed_reason() { + use edgezero_core::response_egress_framing::test_support::{ + FRAMING_RULES, framing_fixture, has_full_synthesized_fixture_headers, + }; + for reason in FRAMING_RULES { + for detached in [false, true] { + let fixture = block_on(framing_fixture(reason, detached)).expect("fixture"); + let method = fixture.envelope.request_method().clone(); + let Err(failure) = fixture + .envelope + .begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("framing rejection") + }; + let (attempt, clock, cause) = failure.into_parts(); + let (mut committer, state) = MockCommitter::new(); + let response = start_fallback(&method, cause, attempt, &clock, &mut committer) + .expect("fallback"); + assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(response.headers["x-synthesized-fixture"], "configured"); + assert!(has_full_synthesized_fixture_headers(&response.headers)); + assert_eq!(state.borrow().writes.concat(), INTERNAL_FALLBACK_BODY); + let completed = fixture.completed.lock().expect("completion"); + assert_eq!(completed.len(), 1); + assert_eq!(completed[0].framing_failures.application(), Some(reason)); + assert_eq!(completed[0].framing_failures.fallback(), None); + assert_eq!(completed[0].outcome, ResponseEgressOutcome::ConversionError); + let observed = fixture.observed.lock().expect("observer"); + if detached { + assert!(observed.is_empty()); + } else { + assert_eq!(*observed, *completed); + } + } + } + } #[test] - #[ignore = "requires worker runtime — worker::Response cannot be constructed in unit tests"] - fn propagates_status_and_headers() { - let response = response_builder() - .status(201) - .header("x-test", "value") - .body(Body::text("ok")) - .expect("response"); - let cf = from_core_response(response).expect("cf response"); - assert_eq!(cf.status_code(), 201); - let header = cf.headers().get("x-test").unwrap(); - assert_eq!(header.as_deref(), Some("value")); + fn provider_diagnostics_reach_response_observer_before_deadline_arbitration() { + use edgezero_core::error::BadGatewayReason; + use edgezero_core::error_detail::{ + CloudflareFailureCause, ProviderFailureDetail, ProviderFailurePhase, + }; + use std::sync::atomic::{AtomicBool, Ordering}; + + let detail = ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: CloudflareFailureCause::TypeError, + }; + for with_detail in [false, true] { + for late in [false, true] { + let now = MonotonicInstant::now(); + let deadline = now.checked_add(Duration::from_secs(10)).expect("deadline"); + let expired = Arc::new(AtomicBool::new(false)); + let clock_expired = Arc::clone(&expired); + let clock = MonotonicClock::new(move || { + if clock_expired.load(Ordering::SeqCst) { + deadline + } else { + now + } + }); + let error = if with_detail { + EdgeError::bad_gateway_with_detail( + "provider-secret-token", + BadGatewayReason::Transport, + detail, + ) + } else { + EdgeError::bad_gateway_with_reason( + "provider-secret-token", + BadGatewayReason::Transport, + ) + }; + let body = Body::from_stream(stream::once(async move { + expired.store(late, Ordering::SeqCst); + Err(error) + })); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, now); + assert!(attempt.begin_writing()); + let (committer, state) = MockCommitter::new(); + let io = MockIo { + close_error: None, + preflight_error: None, + state: Rc::clone(&state), + writes: committer.writes, + }; + let result = block_on(transmit_committed( + body, + policy(deadline), + attempt, + clock, + EgressKind::Application, + io, + )); + assert!(result.is_err()); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!( + reports[0].outcome, + if late { + ResponseEgressOutcome::DeadlineExceeded + } else { + ResponseEgressOutcome::SourceError + } + ); + let failure = reports[0] + .source_failure + .as_ref() + .expect("source observation"); + assert_eq!(reports[0].framing_failures.application(), None); + assert_eq!(reports[0].framing_failures.fallback(), None); + assert_eq!(failure.category(), "bad_gateway_transport"); + assert_eq!( + failure.provider_failure_detail(), + with_detail.then_some(&detail) + ); + assert!(!format!("{failure:?}").contains("secret")); + assert_eq!(state.borrow().aborts, 1); + } + } + } + + #[test] + fn once_and_multi_chunk_bodies_account_only_accepted_writes() { + for (body, declared_length) in [ + (Body::from(Bytes::from_static(b"once")), Some(4)), + (Body::empty(), Some(0)), + ( + Body::stream(stream::iter([ + Bytes::from_static(b"one"), + Bytes::from_static(b"two"), + ])), + None, + ), + ] { + let now = MonotonicInstant::now(); + let deadline = now.checked_add(Duration::from_secs(10)).expect("deadline"); + let clock = MonotonicClock::new(move || now); + let observer = RecordingObserver::default(); + let (mut committer, state) = MockCommitter::new(); + let started = start_prepared( + prepared(&Method::GET, body), + policy(deadline), + attempt(&observer, now), + clock, + EgressKind::Application, + &mut committer, + ); + let StartOutcome::Started(response) = started else { + panic!("response must start"); + }; + assert!(response.transmits_body); + assert_eq!(response.declared_length, declared_length); + assert_eq!(state.borrow().closes, 1); + let expected = state.borrow().writes.iter().map(Vec::len).sum::(); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!( + reports[0].bytes_written, + u64::try_from(expected).expect("byte count") + ); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::HostHandoff); + } + } + + #[test] + fn partial_rejection_accounts_prefix_and_aborts_once() { + let now = MonotonicInstant::now(); + let deadline = now.checked_add(Duration::from_secs(10)).expect("deadline"); + let clock = MonotonicClock::new(move || now); + let observer = RecordingObserver::default(); + let (mut committer, state) = MockCommitter::new(); + committer.writes.push_back(MockWrite::Fail { accepted: 2 }); + let started = start_prepared( + prepared(&Method::GET, Body::from(Bytes::from_static(b"body"))), + policy(deadline), + attempt(&observer, now), + clock.clone(), + EgressKind::Application, + &mut committer, + ); + assert!(matches!(started, StartOutcome::Started(_))); + assert_eq!(state.borrow().aborts, 1); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].bytes_written, 2); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::TransportError); } #[test] - fn streaming_body_converts_without_buffering() { - let stream = stream::iter(vec![Bytes::from_static(b"foo"), Bytes::from_static(b"bar")]); - let response = response_builder() - .status(200) - .body(Body::stream(stream)) - .expect("response"); - - let mut cf = from_core_response(response).expect("cf response"); - let mut byte_stream = cf.stream().expect("byte stream"); - let collected = block_on(async { - let mut out = Vec::new(); - while let Some(item) = byte_stream.next().await { - let chunk = item.expect("chunk"); - out.extend_from_slice(&chunk); - } - out + fn head_suppresses_source_without_polling() { + let now = MonotonicInstant::now(); + let deadline = now.checked_add(Duration::from_secs(10)).expect("deadline"); + let clock = MonotonicClock::new(move || now); + let observer = RecordingObserver::default(); + let polled = Rc::new(Cell::new(false)); + let observed_source = Rc::clone(&polled); + let source = poll_fn(move |_context| { + observed_source.set(true); + Poll::Ready(Some(Bytes::from_static(b"hidden"))) }); + let (mut committer, state) = MockCommitter::new(); + let started = start_prepared( + prepared(&Method::HEAD, Body::stream(source)), + policy(deadline), + attempt(&observer, now), + clock, + EgressKind::Application, + &mut committer, + ); + let StartOutcome::Started(response) = started else { + panic!("HEAD response must start"); + }; + assert!(!response.transmits_body); + assert!(!polled.get()); + assert!(state.borrow().writes.is_empty()); + assert_eq!(observer.reports()[0].bytes_written, 0); + } + + #[test] + fn bodyless_response_checks_cancel_and_deadline_before_handoff() { + let now = MonotonicInstant::now(); + let deadline = now.checked_add(Duration::from_secs(10)).expect("deadline"); + let clock = MonotonicClock::new(move || now); + let observer = RecordingObserver::default(); + let (mut committer, state) = MockCommitter::new(); + committer.preflight_error = Some(IoFailureKind::Cancelled); - assert_eq!(collected, b"foobar"); + let StartOutcome::Precommit { attempt, cause } = start_prepared( + prepared(&Method::HEAD, Body::empty()), + policy(deadline), + attempt(&observer, now), + clock.clone(), + EgressKind::Application, + &mut committer, + ) else { + panic!("cancelled request must not be handed off"); + }; + + assert_eq!(cause, ResponseEgressOutcome::RequestCancelled); + assert_eq!( + start_fallback(&Method::HEAD, cause, *attempt, &clock, &mut committer) + .expect_err("cancelled request cannot start fallback delivery"), + DeliveryError::Cancelled + ); + assert_eq!(state.borrow().aborts, 1); + assert_eq!(observer.reports()[0].outcome, cause); + } + + #[test] + fn generated_overflow_retry_after_precedes_synthesized_headers() { + use edgezero_core::RequestBodyOverflowOrigin; + use edgezero_core::response_egress_framing::test_support::synthesized_error_fixture; + for (error, retry) in [ + ( + EdgeError::payload_too_large(RequestBodyOverflowOrigin::Inbound), + None, + ), + ( + EdgeError::payload_too_large(RequestBodyOverflowOrigin::OutboundUpload), + None, + ), + ( + EdgeError::config_out_of_date("private", "field"), + Some("60"), + ), + ] { + let envelope = synthesized_error_fixture(error).expect("fixture"); + let Ok((prepared, policy, attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("prepared error"); + }; + let (mut committer, _) = MockCommitter::new(); + let StartOutcome::Started(response) = start_prepared( + prepared, + policy, + attempt, + clock, + EgressKind::Application, + &mut committer, + ) else { + panic!("started error"); + }; + assert_eq!(response.headers["x-synthesized-fixture"], "configured"); + assert_eq!( + response + .headers + .get("retry-after") + .map(|value| value.to_str().expect("text")), + retry + ); + } + } + + #[test] + fn precommit_deadline_uses_fallback_and_preserves_original_cause() { + let now = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || now); + let observer = RecordingObserver::default(); + let (mut committer, _state) = MockCommitter::new(); + let StartOutcome::Precommit { attempt, cause } = start_prepared( + prepared(&Method::GET, Body::from(Bytes::from_static(b"late"))), + policy(now), + attempt(&observer, now), + clock.clone(), + EgressKind::Application, + &mut committer, + ) else { + panic!("deadline must fail before commit"); + }; + assert_eq!(cause, ResponseEgressOutcome::DeadlineExceeded); + let response = start_fallback(&Method::GET, cause, *attempt, &clock, &mut committer) + .expect("fallback response"); + assert_eq!(response.status, StatusCode::GATEWAY_TIMEOUT); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Completed) + ); + } + + #[test] + fn fallback_failure_preserves_original_conversion_cause() { + let now = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || now); + let observer = RecordingObserver::default(); + let (mut committer, _state) = MockCommitter::new(); + committer.prepare_error = Some(DeliveryError::Conversion); + let result = start_fallback( + &Method::GET, + ResponseEgressOutcome::ConversionError, + attempt(&observer, now), + &clock, + &mut committer, + ); + assert_eq!( + result.expect_err("fallback must fail"), + DeliveryError::Conversion + ); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::ConversionError); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Aborted) + ); + assert_eq!(reports[0].framing_failures.application(), None); + assert_eq!(reports[0].framing_failures.fallback(), None); + } + + #[test] + fn source_and_close_failures_have_distinct_terminal_outcomes() { + let cases = [ + ( + Body::from_stream(stream::once(async { + Err(EdgeError::internal(anyhow::anyhow!("source"))) + })), + None, + ResponseEgressOutcome::SourceError, + ), + ( + Body::from(Bytes::new()), + Some(IoFailureKind::Transport), + ResponseEgressOutcome::TransportError, + ), + ( + Body::from(Bytes::new()), + Some(IoFailureKind::Deadline), + ResponseEgressOutcome::DeadlineExceeded, + ), + ( + Body::from(Bytes::new()), + Some(IoFailureKind::Cancelled), + ResponseEgressOutcome::RequestCancelled, + ), + ]; + for (body, close_error, expected) in cases { + let now = MonotonicInstant::now(); + let deadline = now.checked_add(Duration::from_secs(10)).expect("deadline"); + let clock = MonotonicClock::new(move || now); + let observer = RecordingObserver::default(); + let (mut committer, _state) = MockCommitter::new(); + committer.close_error = close_error; + let started = start_prepared( + prepared(&Method::GET, body), + policy(deadline), + attempt(&observer, now), + clock, + EgressKind::Application, + &mut committer, + ); + assert!(matches!(started, StartOutcome::Started(_))); + let reports = observer.reports(); + assert_eq!(reports[0].outcome, expected); + if expected == ResponseEgressOutcome::DeadlineExceeded { + assert_eq!(reports[0].elapsed, Duration::from_secs(10)); + } + } + } + + #[test] + fn preencoded_payload_uses_manual_worker_encoding() { + let mut headers = HeaderMap::new(); + assert!(!manual_response_encoding(&headers, true)); + headers.insert(CONTENT_ENCODING, "zstd".parse().expect("encoding")); + assert!(manual_response_encoding(&headers, true)); + assert!(!manual_response_encoding(&headers, false)); } } diff --git a/crates/edgezero-adapter-cloudflare/src/response_headers.rs b/crates/edgezero-adapter-cloudflare/src/response_headers.rs deleted file mode 100644 index 5345250be..000000000 --- a/crates/edgezero-adapter-cloudflare/src/response_headers.rs +++ /dev/null @@ -1,94 +0,0 @@ -use edgezero_core::http::HeaderMap; - -/// Replace a body-generated default with the first valid application value, -/// then append additional values. Non-text values retain the adapter's skip policy. -pub(crate) fn copy_headers(headers: &HeaderMap, mut write: F) -> Result<(), E> -where - F: FnMut(&str, &str, bool) -> Result<(), E>, -{ - for name in headers.keys() { - let mut replace = true; - for value in headers.get_all(name) { - if let Ok(text) = value.to_str() { - write(name.as_str(), text, replace)?; - replace = false; - } - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::copy_headers; - use edgezero_core::http::{HeaderMap, HeaderName, HeaderValue}; - - #[test] - fn replaces_defaults_and_preserves_separate_cookies() { - let mut source = HeaderMap::new(); - source.append("set-cookie", HeaderValue::from_static("first=1; Path=/")); - source.append("set-cookie", HeaderValue::from_static("second=2; Path=/")); - source.insert("content-type", HeaderValue::from_static("text/plain")); - let mut target = HeaderMap::new(); - target.insert( - "content-type", - HeaderValue::from_static("application/octet-stream"), - ); - target.insert("set-cookie", HeaderValue::from_static("default=discard")); - copy_headers(&source, |name, value, replace| { - let parsed_name = name.parse::().unwrap(); - let parsed_value = HeaderValue::from_str(value).unwrap(); - if replace { - target.insert(parsed_name, parsed_value); - } else { - target.append(parsed_name, parsed_value); - } - Ok::<_, ()>(()) - }) - .unwrap(); - assert_eq!(target, source); - assert_eq!(target.get_all("set-cookie").iter().count(), 2); - } - - #[test] - fn first_valid_value_replaces_even_after_invalid_value() { - let mut source = HeaderMap::new(); - source.append("x-test", HeaderValue::from_bytes(b"\xff").unwrap()); - source.append("x-test", HeaderValue::from_static("valid")); - source.append("x-test", HeaderValue::from_static("next")); - let mut writes = Vec::new(); - copy_headers(&source, |_, value, replace| { - writes.push((value.to_owned(), replace)); - Ok::<_, ()>(()) - }) - .unwrap(); - assert_eq!( - writes, - [("valid".to_owned(), true), ("next".to_owned(), false)] - ); - } - - #[test] - fn invalid_only_values_do_not_touch_generated_defaults() { - let mut source = HeaderMap::new(); - source.append("x-test", HeaderValue::from_bytes(b"\xff").unwrap()); - copy_headers(&source, |_, _, _| -> Result<(), ()> { - panic!("invalid text must not reach the sink") - }) - .unwrap(); - } - - #[test] - fn stops_at_sink_error() { - let mut source = HeaderMap::new(); - source.append("set-cookie", HeaderValue::from_static("first=1")); - source.append("set-cookie", HeaderValue::from_static("second=2")); - let mut calls = 0_u32; - let result = copy_headers(&source, |_, _, _| { - calls += 1; - Err("header rejected") - }); - assert_eq!(result, Err("header rejected")); - assert_eq!(calls, 1); - } -} diff --git a/crates/edgezero-adapter-cloudflare/src/secret_store.rs b/crates/edgezero-adapter-cloudflare/src/secret_store.rs index bfe5c2b16..f00951e77 100644 --- a/crates/edgezero-adapter-cloudflare/src/secret_store.rs +++ b/crates/edgezero-adapter-cloudflare/src/secret_store.rs @@ -9,25 +9,24 @@ //! `[stores.secrets] name` in `edgezero.toml` is used only for Fastly; //! Cloudflare accesses all secrets via this adapter regardless of name. -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +use std::future::Future; + use async_trait::async_trait; -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] use bytes::Bytes; -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] -use edgezero_core::secret_store::{SecretError, SecretStore}; -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +use edgezero_core::config_store::BoundedStoreRead; +use edgezero_core::secret_store::SecretError; +use edgezero_core::secret_store::SecretStore; +use edgezero_core::time::{Deadline, MonotonicClock}; use worker::Error as WorkerError; /// Secret store backed by Cloudflare Workers `Env`. /// /// Reads secrets via `env.secret(name)`. Clones the `Env` handle at dispatch /// time so secrets remain accessible throughout the request lifetime. -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] pub struct CloudflareSecretStore { env: worker::Env, } -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] impl CloudflareSecretStore { /// Create a secret store from a cloned `Env`. #[inline] @@ -37,7 +36,6 @@ impl CloudflareSecretStore { } } -#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] #[async_trait(?Send)] impl SecretStore for CloudflareSecretStore { #[inline] @@ -59,4 +57,61 @@ impl SecretStore for CloudflareSecretStore { ))), } } + + #[inline] + async fn get_bytes_bounded( + &self, + store_name: &str, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError> { + bounded_secret_read( + self.get_bytes(store_name, key), + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + .await + } +} + +// Workers Secrets returns a complete value, so these bounds are cooperative +// and apply immediately after host materialization rather than during allocation. +pub(crate) async fn bounded_secret_read( + read: F, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, +) -> Result, SecretError> +where + F: Future, SecretError>>, +{ + if deadline.is_expired_at(clock.now()) { + return Err(SecretError::DeadlineExceeded); + } + + let result = read.await; + if deadline.is_expired_at(clock.now()) { + drop(result); + return Err(SecretError::DeadlineExceeded); + } + let value = result?; + + let backend_bytes = value.as_ref().map_or(Ok(0_u64), |stored_value| { + u64::try_from(stored_value.len()).map_err(|_length_error| SecretError::ValueTooLarge) + })?; + if backend_bytes > max_backend_bytes || backend_bytes > max_value_bytes { + drop(value); + return Err(SecretError::ValueTooLarge); + } + + Ok(BoundedStoreRead { + backend_bytes, + value, + }) } diff --git a/crates/edgezero-adapter-cloudflare/src/templates/wrangler.toml.hbs b/crates/edgezero-adapter-cloudflare/src/templates/wrangler.toml.hbs index 4f076e096..766c3d0ec 100644 --- a/crates/edgezero-adapter-cloudflare/src/templates/wrangler.toml.hbs +++ b/crates/edgezero-adapter-cloudflare/src/templates/wrangler.toml.hbs @@ -1,6 +1,7 @@ name = "{{proj_cloudflare}}" main = "build/worker/shim.mjs" compatibility_date = "2023-05-01" +compatibility_flags = ["enable_request_signal", "enable_weak_ref"] [build] -command = "worker-build --release" \ No newline at end of file +command = "worker-build --release" diff --git a/crates/edgezero-adapter-cloudflare/src/test_runtime.rs b/crates/edgezero-adapter-cloudflare/src/test_runtime.rs new file mode 100644 index 000000000..883ae31bc --- /dev/null +++ b/crates/edgezero-adapter-cloudflare/src/test_runtime.rs @@ -0,0 +1,39 @@ +use worker::js_sys::{Function, Reflect, global}; +use worker::wasm_bindgen::JsValue; + +#[derive(Debug)] +pub(crate) struct NumericTimeoutGuard { + original: JsValue, +} + +impl NumericTimeoutGuard { + pub(crate) fn install() -> Self { + let scope = global(); + let key = JsValue::from_str("setTimeout"); + let original = Reflect::get(&scope, &key).expect("original timeout function"); + // Node timer handles coerce to numeric IDs accepted by both timer APIs. + let factory = Function::new_with_args( + "original", + "return (...args) => Number(original.apply(globalThis, args));", + ); + let numeric = factory + .call1(&JsValue::UNDEFINED, &original) + .expect("numeric timeout wrapper"); + assert!(Reflect::set(&scope, &key, &numeric).expect("install numeric timeout wrapper")); + Self { original } + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for NumericTimeoutGuard { + fn drop(&mut self) { + drop(Reflect::set( + &global(), + &JsValue::from_str("setTimeout"), + &self.original, + )); + } +} diff --git a/crates/edgezero-adapter-cloudflare/tests/contract.rs b/crates/edgezero-adapter-cloudflare/tests/contract.rs index 4097601e9..4d7a0d945 100644 --- a/crates/edgezero-adapter-cloudflare/tests/contract.rs +++ b/crates/edgezero-adapter-cloudflare/tests/contract.rs @@ -1,6 +1,16 @@ -#![cfg(all(feature = "cloudflare", target_arch = "wasm32"))] +#![allow( + clippy::expect_used, + clippy::missing_assert_message, + clippy::unwrap_used, + reason = "wasm_bindgen_test functions are tests, but Clippy does not classify the generated wrappers as tests" +)] + +#[cfg(all(test, feature = "cloudflare", target_arch = "wasm32"))] +#[path = "../src/test_runtime.rs"] +mod test_runtime; // Compile-time check: CloudflareSecretStore implements SecretStore. +#[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] mod secret_store_compile_check { use edgezero_adapter_cloudflare::secret_store::CloudflareSecretStore; use edgezero_core::secret_store::SecretStore; @@ -16,42 +26,126 @@ mod secret_store_compile_check { const _: fn() = assert_provider_impl::; } -#[cfg(test)] +#[cfg(all(test, feature = "cloudflare", target_arch = "wasm32"))] +#[cfg_attr( + feature = "test-utils", + expect( + clippy::arbitrary_source_item_ordering, + reason = "ingress contracts are grouped after the provider fixture tests" + ) +)] mod tests { + use crate::test_runtime::NumericTimeoutGuard; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::time::Duration; use bytes::Bytes; use edgezero_adapter_cloudflare::context::CloudflareRequestContext; + use edgezero_adapter_cloudflare::outbound::CloudflareOutboundClient; + #[cfg(feature = "test-utils")] + use edgezero_adapter_cloudflare::outbound::{ + deferred_clock_paths_hold_for_test, response_abort_lifecycle_holds_for_test, + }; + #[cfg(feature = "test-utils")] + use edgezero_adapter_cloudflare::request::deadline_body_releases_source_for_test; use edgezero_adapter_cloudflare::request::{CloudflareService, into_core_request}; - use edgezero_adapter_cloudflare::response::from_core_response; use edgezero_core::app::{App, Hooks}; use edgezero_core::body::Body; - use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; + use edgezero_core::config_store::{ + BoundedStoreRead, ConfigStore, ConfigStoreError, ConfigStoreHandle, + finish_bounded_config_read, + }; use edgezero_core::context::RequestContext; use edgezero_core::error::EdgeError; use edgezero_core::http::{Method, Response, StatusCode, response_builder}; + use edgezero_core::outbound::{ + HttpClient, OutboundCollectedSlot, OutboundRequest, OutboundSlotResult, OutboundTime, + }; use edgezero_core::router::RouterService; + use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; use futures::stream; - use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - use worker::js_sys::Object; + use std::io::Error as IoError; + use wasm_bindgen_test::wasm_bindgen_test; + use worker::js_sys::{Function, Object, Reflect}; use worker::wasm_bindgen::{JsCast as _, JsValue}; use worker::worker_sys::Context as WorkerSysContext; use worker::{Context, Env, Method as CfMethod, Request as CfRequest, RequestInit}; - wasm_bindgen_test_configure!(run_in_browser); - struct FixedConfigStore(&'static str); + #[cfg(feature = "test-utils")] + struct FailingConfiguration; + + #[cfg(feature = "test-utils")] + #[expect( + clippy::missing_trait_methods, + reason = "test hook exercises only adapter startup failure" + )] + impl Hooks for FailingConfiguration { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Err(EdgeError::service_unavailable( + "private configuration detail", + )) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + #[cfg(feature = "test-utils")] + #[wasm_bindgen_test] + fn failing_configuration() { + let result = + App::build::(edgezero_adapter_cloudflare::CLOUDFLARE_PLATFORM); + let error = result.err().expect("configuration must not produce an app"); + assert_eq!(error.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(error.message(), "private configuration detail"); + } + #[async_trait::async_trait(?Send)] impl ConfigStore for FixedConfigStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.to_owned())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.into())) + } + + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + finish_bounded_config_read( + self.get(key).await, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) } } + fn resolved_slot(slot: &OutboundCollectedSlot) -> &OutboundSlotResult { + match slot { + OutboundCollectedSlot::Resolved(result) => Some(result), + OutboundCollectedSlot::AlreadyDelivered | OutboundCollectedSlot::Unresolved => None, + } + .expect("expected a resolved slot") + } + fn build_test_app() -> App { async fn capture_uri(ctx: RequestContext) -> Result { - let body = Body::text(ctx.request().uri().to_string()); + let body = Body::text(ctx.uri().to_string()); let response = response_builder() .status(StatusCode::OK) .body(body) @@ -60,7 +154,7 @@ mod tests { } async fn mirror_body(ctx: RequestContext) -> Result { - let bytes = ctx.request().body().as_bytes().expect("buffered").to_vec(); + let bytes = ctx.body_bytes(1024 * 1024).await?.to_vec(); let response = response_builder() .status(StatusCode::OK) .body(Body::from(bytes)) @@ -107,7 +201,7 @@ mod tests { .await .ok() .flatten() - .unwrap_or_else(|| "missing".to_owned()), + .map_or_else(|| "missing".to_owned(), |value| value.to_string()), None => "missing".to_owned(), }; let response = response_builder() @@ -145,13 +239,47 @@ mod tests { } let url = format!("https://example.com{path}"); - CfRequest::new_with_init(&url, &init).expect("cf request") + let request = CfRequest::new_with_init(&url, &init).expect("cf request"); + assert_eq!( + request.inner().body().is_some(), + body.is_some(), + "request fixture must preserve native body presence before conversion" + ); + request + } + + fn preflight_time(clock: MonotonicClock) -> OutboundTime { + OutboundTime::new(clock, |_, _| { + Box::pin(async { + Err(EdgeError::internal(IoError::other( + "preflight must not register an outbound timer", + ))) + }) + }) } - fn test_env_ctx() -> (Env, Context) { + fn test_env_ctx() -> (Env, Context, NumericTimeoutGuard) { + let timers = NumericTimeoutGuard::install(); + Function::new_no_args(include_str!("worker_runtime.js")) + .call0(&JsValue::UNDEFINED) + .expect("install guest-only Workers stream fixture"); let env = Object::new().unchecked_into::(); - let js_context = Object::new().unchecked_into::(); - (env, Context::new(js_context)) + let context_object = Object::new(); + let wait_until = Function::new_with_args("_promise", "return undefined;"); + Reflect::set( + &context_object, + &JsValue::from_str("waitUntil"), + wait_until.as_ref(), + ) + .expect("install test waitUntil"); + let js_context = context_object.unchecked_into::(); + (env, Context::new(js_context), timers) + } + + #[cfg(feature = "test-utils")] + #[wasm_bindgen_test] + fn deadline_body_releases_source_when_timeout_is_emitted() { + assert!(deadline_body_releases_source_for_test()); } #[wasm_bindgen_test] @@ -163,13 +291,14 @@ mod tests { reason = "exercise default Hooks metadata and construction" )] impl Hooks for TestApp { - fn routes() -> RouterService { - build_test_app().router().clone() + fn routes() -> Result { + Ok(build_test_app().router().clone()) } } - let app = TestApp::build_app(); + let app = App::build::(edgezero_adapter_cloudflare::CLOUDFLARE_PLATFORM) + .expect("prebuilt application"); for body in [b"first".as_slice(), b"second".as_slice()] { - let (env, ctx) = test_env_ctx(); + let (env, ctx, _timers) = test_env_ctx(); let request = cf_request(CfMethod::Post, "/mirror", Some(body)); let mut response = edgezero_adapter_cloudflare::dispatch_app::(&app, request, env, ctx) @@ -184,7 +313,7 @@ mod tests { async fn dispatch_passes_request_body_to_handlers() { let app = build_test_app(); let req = cf_request(CfMethod::Post, "/mirror", Some(b"echo")); - let (env, ctx) = test_env_ctx(); + let (env, ctx, _timers) = test_env_ctx(); let mut response = CloudflareService::new(&app) .dispatch(req, env, ctx) @@ -200,7 +329,7 @@ mod tests { async fn dispatch_runs_router_and_returns_response() { let app = build_test_app(); let req = cf_request(CfMethod::Get, "/uri", None); - let (env, ctx) = test_env_ctx(); + let (env, ctx, _timers) = test_env_ctx(); let mut response = CloudflareService::new(&app) .dispatch(req, env, ctx) @@ -216,7 +345,7 @@ mod tests { async fn dispatch_streaming_route_preserves_chunks() { let app = build_test_app(); let req = cf_request(CfMethod::Get, "/stream", None); - let (env, ctx) = test_env_ctx(); + let (env, ctx, _timers) = test_env_ctx(); let mut response = CloudflareService::new(&app) .dispatch(req, env, ctx) @@ -228,32 +357,10 @@ mod tests { assert_eq!(bytes.as_slice(), b"chunk-1chunk-2"); } - #[wasm_bindgen_test] - async fn from_core_response_translates_status_headers_and_streaming_body() { - let response = response_builder() - .status(StatusCode::CREATED) - .header("x-edgezero-res", "1") - .body(Body::stream(stream::iter(vec![ - Bytes::from_static(b"hello"), - Bytes::from_static(b" "), - Bytes::from_static(b"world"), - ]))) - .expect("response"); - - let mut cf_response = from_core_response(response).expect("cf response"); - - assert_eq!(cf_response.status_code(), StatusCode::CREATED.as_u16()); - let header = cf_response.headers().get("x-edgezero-res").unwrap(); - assert_eq!(header.as_deref(), Some("1")); - - let bytes = cf_response.bytes().await.expect("bytes"); - assert_eq!(bytes.as_slice(), b"hello world"); - } - #[wasm_bindgen_test] async fn into_core_request_preserves_method_uri_headers_body_and_context() { let req = cf_request(CfMethod::Post, "/mirror?foo=bar", Some(b"payload")); - let (env, ctx) = test_env_ctx(); + let (env, ctx, _timers) = test_env_ctx(); let core_request = into_core_request(req, env, ctx) .await @@ -269,19 +376,21 @@ mod tests { .and_then(|value| value.to_str().ok()); assert_eq!(header, Some("1")); - assert_eq!( - core_request.body().as_bytes().expect("buffered"), - b"payload" - ); - assert!(CloudflareRequestContext::get(&core_request).is_some()); + + let body = core_request + .into_body() + .into_bytes_bounded(1024) + .await + .expect("request body"); + assert_eq!(body.as_ref(), b"payload"); } #[wasm_bindgen_test] async fn service_with_config_handle_injects_handle() { let app = build_test_app(); let req = cf_request(CfMethod::Get, "/config-value", None); - let (env, ctx) = test_env_ctx(); + let (env, ctx, _timers) = test_env_ctx(); let handle = ConfigStoreHandle::new(Arc::new(FixedConfigStore("hello from cf test"))); let mut response = CloudflareService::new(&app) @@ -296,17 +405,85 @@ mod tests { } #[wasm_bindgen_test] - async fn service_with_config_missing_binding_skips_injection() { - // The test env is an empty JS object; any env.var() call returns None. - // `CloudflareService::with_config(name)` should log a warning and - // dispatch without injecting a config-store handle, so the handler - // sees `ctx.config_store_default()` return `None`. + async fn service_with_config_missing_binding_preserves_typed_failure() { + #[edgezero_core::action] + async fn read_declared_config(ctx: RequestContext) -> Result { + let binding = ctx + .config_store_default_binding() + .expect("declared binding must remain registered"); + let error = binding + .handle + .get(&binding.default_key) + .await + .expect_err("failed opening must not become a missing value"); + assert!(matches!(&error, ConfigStoreError::Unavailable { .. })); + + let clock = ctx.monotonic_clock(); + let bounded_error = binding + .handle + .get_bounded( + &binding.default_key, + &clock, + Deadline::at_instant( + clock + .now() + .checked_add(Duration::from_secs(1)) + .expect("read deadline"), + ), + 1024, + 1024, + ) + .await + .expect_err("bounded reads must preserve the opening failure"); + assert!(matches!( + bounded_error, + ConfigStoreError::Unavailable { .. } + )); + + Err(EdgeError::from(error)) + } + + // The empty environment cannot open this declared binding. It must + // stay registered with a typed error rather than look unconfigured. + let app = App::new( + RouterService::builder() + .get("/config-value", read_declared_config) + .build(), + ); + let req = cf_request(CfMethod::Get, "/config-value", None); + let (env, ctx, _timers) = test_env_ctx(); + + let mut response = CloudflareService::new(&app) + .with_config("nonexistent_binding") + .dispatch(req, env, ctx) + .await + .expect("cf response"); + + assert_eq!( + response.status_code(), + StatusCode::SERVICE_UNAVAILABLE.as_u16() + ); + let bytes = response.bytes().await.expect("default config error body"); + let payload: serde_json::Value = serde_json::from_slice(&bytes).expect("JSON error"); + assert_eq!( + payload, + serde_json::json!({ + "error": { + "kind": "service_unavailable", + "message": "config store is unavailable", + "status": 503_u16 + } + }) + ); + } + + #[wasm_bindgen_test] + async fn service_without_config_keeps_registry_absent() { let app = build_test_app(); let req = cf_request(CfMethod::Get, "/has-config", None); - let (env, ctx) = test_env_ctx(); + let (env, ctx, _timers) = test_env_ctx(); let mut response = CloudflareService::new(&app) - .with_config("nonexistent_binding") .dispatch(req, env, ctx) .await .expect("cf response"); @@ -315,4 +492,1183 @@ mod tests { let body = response.text().await.expect("text"); assert_eq!(body, "no"); } + + #[wasm_bindgen_test] + async fn outbound_clock_controls_preflight_elapsed_and_backwards_failure() { + use std::collections::VecDeque; + use std::sync::Mutex; + + fn scripted_clock(script: Vec) -> MonotonicClock { + let observations = Arc::new(Mutex::new(VecDeque::from(script))); + MonotonicClock::new(move || { + let mut samples = observations.lock().expect("clock observations"); + let now = *samples.front().expect("clock observation"); + if samples.len() > 1 { + samples.pop_front(); + } + now + }) + } + + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(9)) + .expect("completed instant"); + let forward_client = HttpClient::with_client(CloudflareOutboundClient::with_time( + preflight_time(scripted_clock(vec![start, completed])), + )); + let forward_request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + let observation_cutoff = Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("cutoff instant"), + ); + let forward_results = forward_client + .start_batch_until(vec![forward_request], observation_cutoff) + .collect() + .await + .expect("valid batch driver"); + assert_eq!( + forward_results.termination(), + edgezero_core::OutboundBatchTermination::Completed + ); + assert_eq!(forward_results.slots().len(), 1); + let forward_result = resolved_slot( + forward_results + .slots() + .first() + .expect("forward result slot"), + ); + assert_eq!(forward_result.elapsed, Duration::from_millis(9)); + assert!(matches!( + forward_result.outcome, + Err(EdgeError::BadRequest { .. }) + )); + + let earlier = start + .checked_sub(Duration::from_millis(1)) + .expect("earlier instant"); + let backwards_client = HttpClient::with_client(CloudflareOutboundClient::with_time( + preflight_time(scripted_clock(vec![start, earlier])), + )); + let backwards_request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + let backwards_results = backwards_client + .start_batch_until(vec![backwards_request], observation_cutoff) + .collect() + .await + .expect("valid batch driver"); + assert_eq!( + backwards_results.termination(), + edgezero_core::OutboundBatchTermination::Completed + ); + assert_eq!(backwards_results.slots().len(), 1); + let backwards_result = resolved_slot( + backwards_results + .slots() + .first() + .expect("backwards result slot"), + ); + assert_eq!(backwards_result.elapsed, Duration::ZERO); + assert!(matches!( + backwards_result.outcome, + Err(EdgeError::Internal { .. }) + )); + } + + #[wasm_bindgen_test] + async fn expired_observation_cutoff_leaves_slot_unresolved() { + let now = MonotonicInstant::now(); + let client = HttpClient::with_client(CloudflareOutboundClient::with_time(preflight_time( + MonotonicClock::new(move || now), + ))); + let request = OutboundRequest::get("https://example.com/").expect("request"); + + let results = client + .start_batch_until(vec![request], Deadline::at_instant(now)) + .collect() + .await + .expect("valid batch driver"); + + assert_eq!( + results.termination(), + edgezero_core::OutboundBatchTermination::Cutoff + ); + assert!(matches!( + results.slots().first(), + Some(OutboundCollectedSlot::Unresolved) + )); + } + + #[wasm_bindgen_test] + async fn batch_preserves_three_preflight_slots_in_input_order() { + let now = MonotonicInstant::now(); + let client = HttpClient::with_client(CloudflareOutboundClient::with_time(preflight_time( + MonotonicClock::new(move || now), + ))); + let streamed_upload = OutboundRequest::post("https://example.com/upload") + .expect("upload request") + .body(Body::stream(stream::iter([Bytes::from_static(b"body")]))); + let streamed_response = OutboundRequest::get("https://example.com/stream") + .expect("stream request") + .stream_response(); + let method_error = OutboundRequest::get("https://example.com/get") + .expect("GET request") + .body(Body::stream(stream::iter([Bytes::new()]))); + + let results = client + .start_batch_until( + vec![streamed_upload, streamed_response, method_error], + Deadline::at_instant( + now.checked_add(Duration::from_secs(1)) + .expect("cutoff instant"), + ), + ) + .collect() + .await + .expect("valid batch driver"); + assert_eq!( + results.termination(), + edgezero_core::OutboundBatchTermination::Completed + ); + let messages: Vec<_> = results + .slots() + .iter() + .map(|slot| match &resolved_slot(slot).outcome { + Err(EdgeError::BadRequest { message, .. }) => Some(message.as_str()), + _ => None, + }) + .collect(); + + assert_eq!( + messages, + [ + Some( + "outbound batches require buffered request bodies; use send for a streamed upload" + ), + Some( + "outbound batches require buffered responses; use send for a streamed response" + ), + Some("outbound batches require buffered request bodies"), + ] + ); + assert!(results.slots().iter().all(|slot| { + matches!(slot, OutboundCollectedSlot::Resolved(result) if result.elapsed == Duration::ZERO) + })); + } + + #[wasm_bindgen_test] + async fn standard_service_installs_the_application_outbound_clock() { + async fn elapsed(ctx: RequestContext) -> Result { + let client = ctx + .http_client() + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("missing HTTP client")))?; + let request = + OutboundRequest::get("https://example.com/")?.body(Body::from("invalid GET body")); + let results = client + .send_all_until(vec![request], Deadline::after(Duration::from_secs(1))) + .await + .map_err(|failure| failure.into_parts().0)?; + let observed_slot = results + .slots() + .first() + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("missing outbound result")))?; + let OutboundCollectedSlot::Resolved(result) = observed_slot else { + return Err(EdgeError::internal(anyhow::anyhow!( + "unresolved outbound result" + ))); + }; + if !matches!(&result.outcome, Err(EdgeError::BadRequest { .. })) { + return Err(EdgeError::internal(anyhow::anyhow!( + "clock probe did not fail during batch preflight" + ))); + } + response_builder() + .status(StatusCode::NO_CONTENT) + .header( + "x-edgezero-elapsed-ms", + result.elapsed.as_millis().to_string(), + ) + .body(Body::empty()) + .map_err(EdgeError::internal) + } + + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(7)) + .expect("completed instant"); + let observations = Arc::new(AtomicUsize::new(0)); + let clock_observations = Arc::clone(&observations); + let mut app = App::new(RouterService::builder().get("/clock", elapsed).build()); + app.set_monotonic_clock(MonotonicClock::new(move || { + if clock_observations.fetch_add(1, Ordering::SeqCst) < 2 { + start + } else { + completed + } + })); + let req = cf_request(CfMethod::Get, "/clock", None); + let (env, ctx, _timers) = test_env_ctx(); + + let response = CloudflareService::new(&app) + .dispatch(req, env, ctx) + .await + .expect("Cloudflare response"); + + assert_eq!(response.status_code(), StatusCode::NO_CONTENT.as_u16()); + assert_eq!( + response + .headers() + .get("x-edgezero-elapsed-ms") + .expect("elapsed header"), + Some("7".to_owned()) + ); + assert!(observations.load(Ordering::SeqCst) >= 3); + } + + #[cfg(feature = "test-utils")] + #[wasm_bindgen_test] + async fn deferred_upload_and_response_paths_retain_the_injected_clock() { + assert!(deferred_clock_paths_hold_for_test().await); + } + + #[cfg(feature = "test-utils")] + #[wasm_bindgen_test] + async fn response_abort_lifecycle_is_enforced() { + assert!(response_abort_lifecycle_holds_for_test().await); + } + + #[cfg(feature = "test-utils")] + mod ingress_contract { + use edgezero_core::response_egress_framing::ResponseEgressFramingMode; + use std::future::Future as _; + use std::io; + use std::sync::Mutex; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::Poll; + use std::time::Duration; + + use edgezero_adapter_cloudflare::request::{ + dispatch_ingress_source_error_for_test, dispatch_ingress_stream_for_test, + dispatch_ingress_with_for_test, + }; + use edgezero_core::http::{HeaderMap, HeaderValue}; + use edgezero_core::ingress::{ + AdmissionDecision, BufferedIngressResponse, IngressGrant, IngressHeadLimits, + }; + use edgezero_core::middleware::{Middleware, Next}; + use edgezero_core::response_egress::{ + DEFAULT_RESPONSE_WRITE_BUDGET, DetachedResponseEgressDecision, + ResponseEgressCompletion, ResponseEgressEnvelope, ResponseEgressObserver, + ResponseEgressOutcome, ResponseEgressReport, + }; + use edgezero_core::router::{RouteMetadata, RouteResolution}; + use futures::future::poll_fn as poll_future; + use futures::stream::poll_fn; + use worker::{Delay, Response as CfResponse}; + + use super::*; + + #[wasm_bindgen_test] + async fn admission_abort() { + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + let delivery_calls = Arc::new(AtomicUsize::new(0)); + let observed_delivery_calls = Arc::clone(&delivery_calls); + let mut app = App::new(RouterService::builder().build()); + app.set_ingress_admission_policy(|_| AdmissionDecision::Abort); + + let error = dispatch_ingress_with_for_test( + &app, + Method::POST, + "/missing".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(stream::empty::>()) + }, + move |_response| { + observed_delivery_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + }, + ) + .await + .expect_err("abort must return a platform error"); + + assert!(error.to_string().contains("ingress admission aborted")); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_eq!(delivery_calls.load(Ordering::SeqCst), 0); + } + + struct DropSignal(Arc); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + struct CountingMiddleware(Arc); + + #[derive(Clone)] + struct RecordingEgressObserver(Arc>>); + + impl ResponseEgressObserver for RecordingEgressObserver { + fn complete(&self, report: &ResponseEgressReport) { + self.0.lock().expect("reports lock").push(report.clone()); + } + } + + #[async_trait::async_trait(?Send)] + impl Middleware for CountingMiddleware { + async fn handle( + &self, + ctx: RequestContext, + next: Next<'_>, + ) -> Result { + self.0.fetch_add(1, Ordering::SeqCst); + next.run(ctx).await + } + } + + fn guarded_fallback_app() -> (App, Arc, Arc) { + let handler_calls = Arc::new(AtomicUsize::new(0)); + let handler_counter = Arc::clone(&handler_calls); + let middleware_calls = Arc::new(AtomicUsize::new(0)); + let router = RouterService::builder() + .get("/known", move |_ctx: RequestContext| { + let request_handler_calls = Arc::clone(&handler_counter); + async move { + request_handler_calls.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("handler must not run") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware_calls))) + .build(); + (App::new(router), handler_calls, middleware_calls) + } + + fn fallback_app( + max_body_bytes: usize, + read_budget: Duration, + grant_drops: &Arc, + ) -> (App, Arc, Arc, NumericTimeoutGuard) { + let timers = NumericTimeoutGuard::install(); + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let observed_grant_drops = Arc::clone(grant_drops); + app.set_ingress_admission_policy(move |head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::MethodNotAllowed { .. } | RouteResolution::NotFound + )); + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes, + read_deadline: head.read_deadline_after(read_budget), + on_exceeded: terminal_response( + StatusCode::UNPROCESSABLE_ENTITY, + "overflow", + b"cloudflare overflow\0response", + ), + on_timeout: terminal_response( + StatusCode::GATEWAY_TIMEOUT, + "timeout", + b"cloudflare timeout response\n", + ), + } + }); + (app, handler_calls, middleware_calls, timers) + } + + fn terminal_headers(marker: &'static str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert( + "content-type", + HeaderValue::from_static("application/octet-stream"), + ); + headers.insert("x-ingress-terminal", HeaderValue::from_static(marker)); + headers + } + + fn terminal_response( + status: StatusCode, + marker: &'static str, + body: &'static [u8], + ) -> BufferedIngressResponse { + BufferedIngressResponse::new(status, terminal_headers(marker), Bytes::from_static(body)) + } + + fn internal_error_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("content-length", HeaderValue::from_static("76")); + headers.insert("content-type", HeaderValue::from_static("application/json")); + headers + } + + fn tracked_stream( + chunks: Vec, + grant_drops: &Arc, + source_drops: &Arc, + body_polls: &Arc, + ) -> impl futures::Stream> + 'static { + let observed_grant_drops = Arc::clone(grant_drops); + let observed_body_polls = Arc::clone(body_polls); + let source_drop = DropSignal(Arc::clone(source_drops)); + let mut pending_chunks = chunks.into_iter(); + poll_fn(move |_cx| { + let _keep_source_alive = &source_drop; + assert_eq!(observed_grant_drops.load(Ordering::SeqCst), 0); + observed_body_polls.fetch_add(1, Ordering::SeqCst); + Poll::Ready(pending_chunks.next().map(Ok)) + }) + } + + fn pending_stream( + grant_drops: &Arc, + source_drops: &Arc, + body_polls: &Arc, + ) -> impl futures::Stream> + 'static { + let observed_grant_drops = Arc::clone(grant_drops); + let observed_body_polls = Arc::clone(body_polls); + let source_drop = DropSignal(Arc::clone(source_drops)); + poll_fn(move |_cx| { + let _keep_source_alive = &source_drop; + assert_eq!(observed_grant_drops.load(Ordering::SeqCst), 0); + observed_body_polls.fetch_add(1, Ordering::SeqCst); + Poll::Pending + }) + } + + fn error_stream( + grant_drops: &Arc, + source_drops: &Arc, + body_polls: &Arc, + ) -> impl futures::Stream> + 'static { + let observed_grant_drops = Arc::clone(grant_drops); + let observed_body_polls = Arc::clone(body_polls); + let source_drop = DropSignal(Arc::clone(source_drops)); + let mut emitted = false; + poll_fn(move |_cx| { + let _keep_source_alive = &source_drop; + assert_eq!(observed_grant_drops.load(Ordering::SeqCst), 0); + observed_body_polls.fetch_add(1, Ordering::SeqCst); + if emitted { + Poll::Ready(None) + } else { + emitted = true; + Poll::Ready(Some(Err(io::Error::other( + "cloudflare ingress source failure", + )))) + } + }) + } + + fn assert_no_route_dispatch(handler_calls: &AtomicUsize, middleware_calls: &AtomicUsize) { + assert_eq!(handler_calls.load(Ordering::SeqCst), 0); + assert_eq!(middleware_calls.load(Ordering::SeqCst), 0); + } + + #[wasm_bindgen_test] + async fn normalized_ingress_error_returns_typed_response_without_application_observation() { + let reports = Arc::new(Mutex::new(Vec::new())); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let mut app = App::new(RouterService::builder().build()); + app.set_ingress_head_limits( + IngressHeadLimits::default() + .with_max_request_target_bytes(4) + .expect("target limit"), + ); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let observed_factory_calls = Arc::clone(&factory_calls); + let observed_completion_calls = Arc::clone(&completion_calls); + app.set_detached_response_egress_decision_factory(move |head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + let terminal_calls = Arc::clone(&observed_completion_calls); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |_report| { + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), + } + }); + + let egress = dispatch_ingress_stream_for_test( + &app, + Method::GET, + "/too-long".parse().expect("URI"), + stream::empty::>(), + ) + .await + .expect("typed response"); + let response = capture_response(egress); + + assert_eq!(response.status(), StatusCode::URI_TOO_LONG); + assert!(reports.lock().expect("reports lock").is_empty()); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + } + + #[wasm_bindgen_test] + async fn normalized_ingress_error_decision_abort_uses_provider_error() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + app.set_ingress_head_limits( + IngressHeadLimits::default() + .with_max_request_target_bytes(4) + .expect("target limit"), + ); + let reports = Arc::new(Mutex::new(Vec::new())); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + app.set_detached_response_egress_decision_factory(move |_head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + DetachedResponseEgressDecision::Abort + }); + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + let delivery_calls = Arc::new(AtomicUsize::new(0)); + let observed_delivery_calls = Arc::clone(&delivery_calls); + + let error = dispatch_ingress_with_for_test( + &app, + Method::GET, + "/too-long".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(stream::empty::>()) + }, + move |_egress| { + observed_delivery_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + }, + ) + .await + .expect_err("detached abort must return a platform error"); + + assert!(error.to_string().contains("ingress admission aborted")); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_eq!(delivery_calls.load(Ordering::SeqCst), 0); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[wasm_bindgen_test] + async fn admission_policy_error_decision_send_uses_detached_completion() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let decision_completion_calls = Arc::new(AtomicUsize::new(0)); + let decision_resource_drops = Arc::new(AtomicUsize::new(0)); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_decision_completion_calls = Arc::clone(&decision_completion_calls); + let observed_decision_resource_drops = Arc::clone(&decision_resource_drops); + let observed_grant_drops = Arc::clone(&grant_drops); + app.set_ingress_admission_policy(move |head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::Matched(_) + )); + let decision_resource = DropSignal(Arc::clone(&observed_decision_resource_drops)); + let terminal_calls = Arc::clone(&observed_decision_completion_calls); + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::new(move |_report| { + let _resource = decision_resource; + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes: 1, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text( + StatusCode::PAYLOAD_TOO_LARGE, + "too large", + ), + on_timeout: BufferedIngressResponse::text( + StatusCode::REQUEST_TIMEOUT, + "timeout", + ), + } + }); + let reports = Arc::new(Mutex::new(Vec::new())); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + let observed_completion_calls = Arc::clone(&completion_calls); + app.set_detached_response_egress_decision_factory(move |head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + let terminal_calls = Arc::clone(&observed_completion_calls); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |_report| { + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), + } + }); + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + + let response = dispatch_ingress_with_for_test( + &app, + Method::GET, + "/known".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(stream::empty::>()) + }, + |egress| Ok(capture_response(egress)), + ) + .await + .expect("typed policy error response"); + + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + assert_eq!(decision_completion_calls.load(Ordering::SeqCst), 0); + assert_eq!(decision_resource_drops.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[wasm_bindgen_test] + async fn admission_policy_error_decision_abort_uses_provider_error() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let decision_resource_drops = Arc::new(AtomicUsize::new(0)); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_decision_resource_drops = Arc::clone(&decision_resource_drops); + let observed_grant_drops = Arc::clone(&grant_drops); + app.set_ingress_admission_policy(move |head| { + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::new({ + let resource = DropSignal(Arc::clone(&observed_decision_resource_drops)); + move |_report| drop(resource) + }), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes: 1, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text( + StatusCode::PAYLOAD_TOO_LARGE, + "too large", + ), + on_timeout: BufferedIngressResponse::text( + StatusCode::REQUEST_TIMEOUT, + "timeout", + ), + } + }); + let reports = Arc::new(Mutex::new(Vec::new())); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + app.set_detached_response_egress_decision_factory(move |_head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + DetachedResponseEgressDecision::Abort + }); + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + let delivery_calls = Arc::new(AtomicUsize::new(0)); + let observed_delivery_calls = Arc::clone(&delivery_calls); + + let error = dispatch_ingress_with_for_test( + &app, + Method::GET, + "/known".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(stream::empty::>()) + }, + move |_egress| { + observed_delivery_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + }, + ) + .await + .expect_err("detached abort must return a platform error"); + + assert!(error.to_string().contains("ingress admission aborted")); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_eq!(delivery_calls.load(Ordering::SeqCst), 0); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + assert_eq!(decision_resource_drops.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[wasm_bindgen_test] + async fn post_admission_failure_uses_owned_error_egress() { + let reports = Arc::new(Mutex::new(Vec::new())); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_grant_drops = Arc::clone(&grant_drops); + let mut app = App::new( + RouterService::builder() + .get("/owned", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("handler must not run") + }) + .build(), + ); + app.set_ingress_admission_policy(move |head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + }); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + + let egress = dispatch_ingress_source_error_for_test( + &app, + Method::GET, + "/owned".parse().expect("URI"), + ) + .await + .expect("owned error egress"); + let response = capture_response(egress); + + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + let observed = reports.lock().expect("reports lock"); + assert_eq!(observed.len(), 1); + let report = observed.first().expect("one report"); + assert_eq!(report.outcome, ResponseEgressOutcome::HostHandoff); + assert_eq!( + report.route.as_ref().map(RouteMetadata::pattern), + Some("/owned") + ); + } + + fn capture_response(envelope: ResponseEgressEnvelope) -> Response { + let result = envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength); + assert!(result.is_ok(), "test egress envelope must prepare"); + let Ok((prepared, _policy, mut attempt, clock)) = result else { + return Response::new(Body::empty()); + }; + assert!(attempt.begin_writing()); + assert!(attempt.terminate(ResponseEgressOutcome::HostHandoff, clock.now())); + prepared.into_response() + } + + async fn assert_terminal_response( + envelope: ResponseEgressEnvelope, + status: StatusCode, + expected_headers: HeaderMap, + body: &[u8], + ) { + let response = capture_response(envelope); + assert_eq!(response.status(), status); + assert_eq!(response.headers(), &expected_headers); + let bytes = response + .into_body() + .into_bytes_bounded(usize::MAX) + .await + .expect("captured body"); + assert_eq!(bytes.as_ref(), body); + } + + async fn assert_worker_response( + mut response: CfResponse, + status: StatusCode, + expected_headers: HeaderMap, + body: &[u8], + ) { + assert_eq!(response.status_code(), status.as_u16()); + let mut actual_headers = response.headers().entries().collect::>(); + actual_headers.sort_unstable(); + let mut expected_provider_headers = expected_headers + .iter() + .map(|(name, value)| { + ( + name.as_str().to_owned(), + value.to_str().expect("UTF-8 test header").to_owned(), + ) + }) + .collect::>(); + expected_provider_headers.sort_unstable(); + assert_eq!(actual_headers, expected_provider_headers); + assert_eq!(response.bytes().await.expect("provider body"), body); + } + + #[wasm_bindgen_test] + async fn exact_cap_preserves_not_found_and_method_not_allowed() { + for (path, expected_status) in [ + ("/missing", StatusCode::NOT_FOUND), + ("/known", StatusCode::METHOD_NOT_ALLOWED), + ] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls, _timers) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let source = tracked_stream( + vec![Bytes::from_static(b"ab"), Bytes::from_static(b"cd")], + &grant_drops, + &source_drops, + &body_polls, + ); + + let envelope = dispatch_ingress_stream_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + source, + ) + .await + .expect("response"); + let response = capture_response(envelope); + + assert_eq!(response.status(), expected_status); + if expected_status == StatusCode::METHOD_NOT_ALLOWED { + assert_eq!( + response.headers().get("allow").expect("Allow header"), + "GET" + ); + } + assert_eq!(body_polls.load(Ordering::SeqCst), 3); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[wasm_bindgen_test] + async fn cap_plus_one_precedes_not_found_and_method_not_allowed() { + for path in ["/missing", "/known"] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls, _timers) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let source = tracked_stream( + vec![Bytes::from_static(b"abcd"), Bytes::from_static(b"e")], + &grant_drops, + &source_drops, + &body_polls, + ); + + let response = dispatch_ingress_stream_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + source, + ) + .await + .expect("response"); + + assert_terminal_response( + response, + StatusCode::UNPROCESSABLE_ENTITY, + { + let mut headers = terminal_headers("overflow"); + headers.insert("content-length", HeaderValue::from_static("28")); + headers + }, + b"cloudflare overflow\0response", + ) + .await; + assert_eq!(body_polls.load(Ordering::SeqCst), 2); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[wasm_bindgen_test] + async fn standard_service_enforces_fallback_overflow() { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls, _fallback_timers) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let req = cf_request(CfMethod::Post, "/missing", Some(b"abcde")); + let (env, ctx, _timers) = test_env_ctx(); + + let response = CloudflareService::new(&app) + .dispatch(req, env, ctx) + .await + .expect("Cloudflare response"); + + assert_worker_response( + response, + StatusCode::UNPROCESSABLE_ENTITY, + { + let mut headers = terminal_headers("overflow"); + headers.insert("content-length", HeaderValue::from_static("28")); + headers + }, + b"cloudflare overflow\0response", + ) + .await; + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + + #[wasm_bindgen_test] + async fn wasm_timer_timeout_preserves_application_response() { + for path in ["/missing", "/known"] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls, _timers) = + fallback_app(4, Duration::from_millis(100), &grant_drops); + let source = pending_stream(&grant_drops, &source_drops, &body_polls); + + let response = dispatch_ingress_stream_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + source, + ) + .await + .expect("response"); + + assert_terminal_response( + response, + StatusCode::GATEWAY_TIMEOUT, + { + let mut headers = terminal_headers("timeout"); + headers.insert("content-length", HeaderValue::from_static("28")); + headers + }, + b"cloudflare timeout response\n", + ) + .await; + assert!(body_polls.load(Ordering::SeqCst) > 0); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[wasm_bindgen_test] + async fn saturated_fallback_refuses_without_polling_body() { + for path in ["/missing", "/known"] { + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let observed_completion_calls = Arc::clone(&completion_calls); + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + app.set_ingress_admission_policy(move |head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::MethodNotAllowed { .. } | RouteResolution::NotFound + )); + AdmissionDecision::Refuse { + completion: { + let calls = Arc::clone(&observed_completion_calls); + ResponseEgressCompletion::new(move |_report| { + calls.fetch_add(1, Ordering::SeqCst); + }) + }, + response: response_builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .header("x-ingress-refusal", "saturated") + .body(Body::from("cloudflare unavailable\n")) + .expect("refusal response"), + } + }); + let unobserved_grant = Arc::new(AtomicUsize::new(0)); + let source = pending_stream(&unobserved_grant, &source_drops, &body_polls); + + let response = dispatch_ingress_stream_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + source, + ) + .await + .expect("response"); + + assert_terminal_response( + response, + StatusCode::SERVICE_UNAVAILABLE, + { + let mut headers = HeaderMap::new(); + headers.insert("x-ingress-refusal", HeaderValue::from_static("saturated")); + headers.insert("content-length", HeaderValue::from_static("23")); + headers + }, + b"cloudflare unavailable\n", + ) + .await; + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + assert_eq!(unobserved_grant.load(Ordering::SeqCst), 0); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[wasm_bindgen_test] + async fn source_error_uses_worker_response_boundary_and_releases_lifecycle() { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls, _timers) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let source = error_stream(&grant_drops, &source_drops, &body_polls); + + let response = dispatch_ingress_stream_for_test( + &app, + Method::POST, + "/missing".parse().expect("URI"), + source, + ) + .await + .expect("standard error response"); + + assert_terminal_response( + response, + StatusCode::INTERNAL_SERVER_ERROR, + internal_error_headers(), + br#"{"error":{"kind":"internal","message":"internal server error","status":500}}"#, + ) + .await; + assert_eq!(body_polls.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + + #[wasm_bindgen_test] + async fn poll_then_drop_releases_pending_source_and_grant() { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls, _timers) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let source = pending_stream(&grant_drops, &source_drops, &body_polls); + let mut dispatch = Box::pin(dispatch_ingress_stream_for_test( + &app, + Method::POST, + "/missing".parse().expect("URI"), + source, + )); + + let first_poll = poll_future(|cx| Poll::Ready(dispatch.as_mut().poll(cx))).await; + assert!(first_poll.is_pending()); + Delay::from(Duration::ZERO).await; + let source_poll = poll_future(|cx| Poll::Ready(dispatch.as_mut().poll(cx))).await; + assert!(source_poll.is_pending()); + assert!(body_polls.load(Ordering::SeqCst) > 0); + assert_eq!(grant_drops.load(Ordering::SeqCst), 0); + assert_eq!(source_drops.load(Ordering::SeqCst), 0); + + drop(dispatch); + + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } +} + +#[cfg(test)] +mod native_tests { + #[cfg(all(feature = "test-utils", not(target_arch = "wasm32")))] + mod enabled { + use bytes::Bytes; + use edgezero_adapter_cloudflare::outbound::{ + generic_fetch_failure_for_test, timeout_error_for_test, validate_batch_request_for_test, + }; + use edgezero_core::body::Body; + use edgezero_core::error::{BadGatewayReason, BudgetSource, EdgeError}; + use edgezero_core::outbound::OutboundRequest; + use futures_util::stream; + use std::time::Duration; + + #[test] + fn image_optimizer_rejection_precedes_other_preflight() { + let request = OutboundRequest::get("https://example.com/image") + .unwrap() + .image_optimizer(edgezero_core::OutboundImageOptimizerOptions::new()) + .body(Body::from_stream(stream::pending())) + .timeout(Duration::ZERO); + let error = validate_batch_request_for_test(&request).expect_err("unsupported option"); + assert!(error.message().contains("image_optimizer")); + assert!(error.message().contains("cloudflare")); + } + + fn message(result: Result<(), EdgeError>) -> Option { + if let Err(EdgeError::BadRequest { message, .. }) = result { + Some(message) + } else { + None + } + } + + #[test] + fn buffered_upload_cap_has_typed_preflight_boundaries() { + for len in [7_usize, 8, 9] { + let request = OutboundRequest::post("https://example.com/upload") + .expect("request") + .body(vec![b'x'; len]) + .max_request_body_bytes(8); + let result = validate_batch_request_for_test(&request); + if len > 8 { + let error = result.expect_err("overflow before dispatch"); + assert_eq!( + error.request_body_overflow_origin(), + Some(edgezero_core::RequestBodyOverflowOrigin::OutboundUpload) + ); + } else { + result.expect("within cap"); + } + } + } + + #[test] + fn batch_preflight_precedence_and_indices() { + let streamed_upload = OutboundRequest::post("https://example.com/upload") + .expect("request") + .body(Body::stream(stream::iter([Bytes::from_static(b"body")]))); + let streamed_response = OutboundRequest::get("https://example.com/response") + .expect("request") + .stream_response(); + let get_stream = OutboundRequest::get("https://example.com/get") + .expect("request") + .body(Body::stream(stream::iter([Bytes::new()]))); + + assert_eq!( + [streamed_upload, streamed_response, get_stream] + .iter() + .map(|request| message(validate_batch_request_for_test(request))) + .collect::>(), + [ + Some("outbound batches require buffered request bodies; use send for a streamed upload".to_owned()), + Some("outbound batches require buffered responses; use send for a streamed response".to_owned()), + Some("outbound batches require buffered request bodies".to_owned()), + ] + ); + } + + #[test] + fn opaque_fetch_rejection_does_not_claim_connection_phase_evidence() { + let error = generic_fetch_failure_for_test(); + assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Unspecified, + .. + } + )); + } + + #[test] + fn worker_budget_timeouts_preserve_every_selected_source() { + for selected in [ + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::Default, + ] { + assert!(matches!( + timeout_error_for_test(selected), + EdgeError::GatewayTimeout { cause, .. } if cause == selected + )); + } + } + } } diff --git a/crates/edgezero-adapter-cloudflare/tests/redirect_runtime.js b/crates/edgezero-adapter-cloudflare/tests/redirect_runtime.js new file mode 100644 index 000000000..4ab6a4818 --- /dev/null +++ b/crates/edgezero-adapter-cloudflare/tests/redirect_runtime.js @@ -0,0 +1,50 @@ +// Guest-only fetch fixture. This does not model workerd or prove host cancellation. +const responses = JSON.parse(wire) +const original = globalThis.fetch +const probe = { requests: [], pulls: [], aborts: [], cancellations: [] } +const pending = [] +const untilCleanup = () => new Promise((resolve, reject) => { + pending.push(() => reject(new DOMException('fixture cleanup', 'AbortError'))) +}) +globalThis.fetch = (request, init) => { + if (request.redirect !== 'manual' || init.encodeResponseBody !== 'manual') { + throw new TypeError('manual fetch policy lost') + } + const index = probe.requests.length + const response = responses[index] + if (response === undefined) throw new TypeError('unexpected fixture fetch') + probe.requests.push({ url: request.url, method: request.method }) + probe.pulls.push(0) + probe.aborts.push(0) + probe.cancellations.push(0) + init.signal.addEventListener('abort', () => { probe.aborts[index]++ }, { once: true }) + if (response.kind === 'pending-head') return untilCleanup() + let sent = false + const body = new ReadableStream({ + pull(controller) { + probe.pulls[index]++ + if (response.kind === 'pending') return untilCleanup() + if (response.kind === 'error') { + controller.error(new TypeError('fixture body failure')) + } else if (response.kind === 'bytes') { + if (sent || response.bytes.length === 0) controller.close() + else { + sent = true + controller.enqueue(new Uint8Array(response.bytes)) + } + } else { + throw new TypeError('unknown fixture body kind') + } + }, + cancel() { probe.cancellations[index]++ }, + }, { highWaterMark: 0 }) + return Promise.resolve(new Response(body, { + status: response.status, + headers: response.headers, + })) +} +probe.restore = () => { + globalThis.fetch = original + for (const release of pending.splice(0)) release() +} +return probe diff --git a/crates/edgezero-adapter-cloudflare/tests/secret_store_bounded.rs b/crates/edgezero-adapter-cloudflare/tests/secret_store_bounded.rs new file mode 100644 index 000000000..4500230a1 --- /dev/null +++ b/crates/edgezero-adapter-cloudflare/tests/secret_store_bounded.rs @@ -0,0 +1,148 @@ +#![cfg(all(feature = "cloudflare", not(target_arch = "wasm32")))] + +#[path = "../src/secret_store.rs"] +#[expect( + dead_code, + reason = "the source harness exercises the private bounded-read helper without constructing a worker Env" +)] +mod secret_store; + +#[cfg(test)] +mod tests { + use std::cell::Cell; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + use bytes::Bytes; + use edgezero_core::secret_store::SecretError; + use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; + use futures::executor::block_on; + + use super::secret_store; + + #[test] + fn bounded_secret_reports_exact_bytes_and_accepts_exact_caps() { + let clock = MonotonicClock::default(); + let result = block_on(secret_store::bounded_secret_read( + async { Ok(Some(Bytes::from_static(b"value"))) }, + &clock, + Deadline::after(Duration::from_secs(1)), + 5, + 5, + )) + .expect("exact caps must succeed"); + + assert_eq!(result.backend_bytes, 5); + assert_eq!(result.value, Some(Bytes::from_static(b"value"))); + } + + #[test] + fn bounded_secret_rejects_either_exceeded_cap() { + for (max_backend_bytes, max_value_bytes) in [(4, 5), (5, 4)] { + let clock = MonotonicClock::default(); + let error = block_on(secret_store::bounded_secret_read( + async { Ok(Some(Bytes::from_static(b"value"))) }, + &clock, + Deadline::after(Duration::from_secs(1)), + max_backend_bytes, + max_value_bytes, + )) + .expect_err("an exceeded cap must fail"); + + assert!(matches!(error, SecretError::ValueTooLarge)); + } + } + + #[test] + fn bounded_secret_checks_deadline_before_polling_host_call() { + let polled = Cell::new(false); + let clock = MonotonicClock::default(); + let error = block_on(secret_store::bounded_secret_read( + async { + polled.set(true); + Ok(None) + }, + &clock, + Deadline::after(Duration::ZERO), + 1, + 1, + )) + .expect_err("expired deadline must fail"); + + assert!(matches!(error, SecretError::DeadlineExceeded)); + assert!(!polled.get(), "expired reads must not poll the host call"); + } + + #[test] + fn bounded_secret_checks_deadline_after_host_call() { + let start = MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let now = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let error = block_on(secret_store::bounded_secret_read( + async { + *now.lock().expect("clock lock") = terminal; + Ok(None) + }, + &clock, + Deadline::at_instant(terminal), + 1, + 1, + )) + .expect_err("a host call completing after the deadline must fail"); + + assert!(matches!(error, SecretError::DeadlineExceeded)); + } + + #[test] + fn bounded_secret_deadline_wins_over_late_host_error() { + let start = MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let now = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let error = block_on(secret_store::bounded_secret_read( + async { + *now.lock().expect("clock lock") = terminal; + Err(SecretError::Unavailable) + }, + &clock, + Deadline::at_instant(terminal), + 1, + 1, + )) + .expect_err("the post-call deadline check must run after host errors"); + + assert!(matches!(error, SecretError::DeadlineExceeded)); + } + + #[test] + fn bounded_secret_read_uses_injected_clock() { + let process_now = MonotonicInstant::now(); + let injected_now = process_now + .checked_sub(Duration::from_mins(1)) + .expect("injected instant"); + let clock = MonotonicClock::new(move || injected_now); + let deadline = Deadline::at_instant( + injected_now + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), + ); + + let result = block_on(secret_store::bounded_secret_read( + async { Ok(Some(Bytes::from_static(b"value"))) }, + &clock, + deadline, + 5, + 5, + )) + .expect("the application clock is still before its deadline"); + + assert_eq!(result.value, Some(Bytes::from_static(b"value"))); + } +} diff --git a/crates/edgezero-adapter-cloudflare/tests/worker_runtime.js b/crates/edgezero-adapter-cloudflare/tests/worker_runtime.js new file mode 100644 index 000000000..a90af081e --- /dev/null +++ b/crates/edgezero-adapter-cloudflare/tests/worker_runtime.js @@ -0,0 +1,28 @@ +// Guest-only fixture; this is not evidence of Workers host behavior. +if (globalThis.FixedLengthStream === undefined) { + globalThis.FixedLengthStream = class FixedLengthStream extends TransformStream { + constructor(length) { + if (typeof length !== 'bigint' && !Number.isSafeInteger(length)) { + throw new RangeError('invalid stream length') + } + const expected = BigInt(length) + if (expected < 0n || expected >= 1n << 64n) { + throw new RangeError('invalid stream length') + } + let received = 0n + super({ + transform(chunk, controller) { + if (!(chunk instanceof ArrayBuffer) && !ArrayBuffer.isView(chunk)) { + throw new TypeError('expected a byte chunk') + } + received += BigInt(chunk.byteLength) + if (received > expected) throw new TypeError('stream length exceeded') + controller.enqueue(chunk) + }, + flush() { + if (received !== expected) throw new TypeError('stream length mismatch') + }, + }) + } + } +} diff --git a/crates/edgezero-adapter-cloudflare/tests/worker_runtime.test.mjs b/crates/edgezero-adapter-cloudflare/tests/worker_runtime.test.mjs new file mode 100644 index 000000000..8c0e0bc6f --- /dev/null +++ b/crates/edgezero-adapter-cloudflare/tests/worker_runtime.test.mjs @@ -0,0 +1,187 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const installWorkersStreams = new Function( + readFileSync(new URL('./worker_runtime.js', import.meta.url), 'utf8'), +) + +const installRedirectFetch = new Function( + 'wire', readFileSync(new URL('./redirect_runtime.js', import.meta.url), 'utf8'), +) + +test('redirect fixture never prefetches an unread body and restores fetch', async () => { + const original = globalThis.fetch + const probe = installRedirectFetch(JSON.stringify([ + { status: 302, kind: 'pending', headers: [['location', '/next']] }, + { status: 200, kind: 'bytes', headers: [], bytes: [0, 255, 7] }, + ])) + const first = new AbortController() + const init = signal => ({ signal, encodeResponseBody: 'manual' }) + try { + const reply = await fetch(new Request('https://fixture.invalid/start', { redirect: 'manual' }), init(first.signal)) + await Promise.resolve() + assert.deepEqual(probe.pulls, [0]) + first.abort() + assert.deepEqual(probe.aborts, [1]) + await reply.body.cancel() + assert.deepEqual(probe.cancellations, [1]) + const final = await fetch(new Request('https://fixture.invalid/next', { redirect: 'manual' }), init(new AbortController().signal)) + assert.deepEqual(new Uint8Array(await final.arrayBuffer()), new Uint8Array([0, 255, 7])) + assert.equal(probe.requests.length, 2) + assert.deepEqual(probe.aborts, [1, 0]) + } finally { + probe.restore() + } + assert.equal(globalThis.fetch, original) +}) + +test('redirect fixture rejects implicit redirect or encoding and unexpected fetches', async () => { + const probe = installRedirectFetch(JSON.stringify([ + { status: 200, kind: 'bytes', headers: [], bytes: [] }, + ])) + const init = { signal: new AbortController().signal, encodeResponseBody: 'manual' } + try { + assert.throws(() => fetch(new Request('https://fixture.invalid/'), init), /manual fetch policy/) + const request = new Request('https://fixture.invalid/', { redirect: 'manual' }) + assert.throws(() => fetch(request, { signal: init.signal }), /manual fetch policy/) + assert.equal(probe.requests.length, 0) + assert.equal((await fetch(request, init)).status, 200) + assert.throws(() => fetch(request, init), /unexpected fixture fetch/) + assert.equal(probe.requests.length, 1) + } finally { + probe.restore() + } +}) + +test('redirect fixture settles pending fetch and body callbacks during cleanup', async () => { + const probe = installRedirectFetch(JSON.stringify([ + { kind: 'pending-head' }, + { status: 200, kind: 'pending', headers: [] }, + ])) + const init = { signal: new AbortController().signal, encodeResponseBody: 'manual' } + const request = new Request('https://fixture.invalid/', { redirect: 'manual' }) + try { + const head = assert.rejects(fetch(request, init), { name: 'AbortError' }) + const response = await fetch(request, init) + const reader = response.body.getReader() + const body = assert.rejects(reader.read(), { name: 'AbortError' }) + await Promise.resolve() + assert.deepEqual(probe.pulls, [0, 1]) + probe.restore() + await head + await body + reader.releaseLock() + } finally { + probe.restore() + } +}) + +async function exchange(length, chunks) { + installWorkersStreams() + const stream = new globalThis.FixedLengthStream(length) + const writer = stream.writable.getWriter() + const reading = new Response(stream.readable).arrayBuffer() + const writing = (async () => { + try { + for (const chunk of chunks) await writer.write(chunk) + await writer.close() + } finally { + writer.releaseLock() + } + })() + const [bytes] = await Promise.all([reading, writing]) + return new Uint8Array(bytes) +} + +test('buffered stream preserves exact bytes and closes at the declared length', async () => { + assert.deepEqual( + await exchange(4, [new Uint8Array([0, 255]), new Uint8Array([1, 128])]), + new Uint8Array([0, 255, 1, 128]), + ) +}) + +test('zero-length and bigint declarations use the same byte contract', async () => { + assert.deepEqual(await exchange(0, []), new Uint8Array()) + assert.deepEqual(await exchange(1n, [new Uint8Array([7])]), new Uint8Array([7])) +}) + +test('underflow and overflow fail instead of returning truncated or excess bytes', async () => { + await assert.rejects(exchange(2, [new Uint8Array([1])]), /length/) + await assert.rejects(exchange(1, [new Uint8Array([1, 2])]), /length/) +}) + +test('bigint declarations above u32 do not wrap to a short stream', async () => { + await assert.rejects(exchange((1n << 32n) + 1n, [new Uint8Array([1])]), /length mismatch/) +}) + +test('invalid lengths and non-byte chunks fail closed', async () => { + installWorkersStreams() + for (const length of [-1, 1.5, NaN, Infinity, -1n, 1n << 64n]) { + assert.throws(() => new globalThis.FixedLengthStream(length), RangeError) + } + await assert.rejects(exchange(1, ['not bytes']), TypeError) +}) + +test('writes retain normal stream backpressure', async () => { + installWorkersStreams() + const stream = new globalThis.FixedLengthStream(1) + const writer = stream.writable.getWriter() + let settled = false + const writing = writer.write(new Uint8Array([9])).then(() => { settled = true }) + await Promise.resolve() + assert.equal(settled, false) + const reader = stream.readable.getReader() + assert.deepEqual(await reader.read(), { done: false, value: new Uint8Array([9]) }) + await writing + await writer.close() + assert.deepEqual(await reader.read(), { done: true, value: undefined }) + writer.releaseLock() + reader.releaseLock() +}) + +test('reader cancellation rejects pending writes and closes the writer', async () => { + installWorkersStreams() + const stream = new globalThis.FixedLengthStream(1) + const writer = stream.writable.getWriter() + const reader = stream.readable.getReader() + const outcomes = Promise.allSettled([writer.write(new Uint8Array([1])), writer.closed]) + await reader.cancel('fixture cancellation') + for (const result of await outcomes) { + assert.equal(result.status, 'rejected') + assert.equal(result.reason, 'fixture cancellation') + } + writer.releaseLock() + reader.releaseLock() +}) + +test('writer abort errors the readable stream without a successful EOF', async () => { + installWorkersStreams() + const stream = new globalThis.FixedLengthStream(1) + const writer = stream.writable.getWriter() + const outcomes = Promise.allSettled([ + new Response(stream.readable).arrayBuffer(), + writer.closed, + ]) + const reason = new Error('fixture writer abort') + await writer.abort(reason) + for (const result of await outcomes) { + assert.equal(result.status, 'rejected') + assert.equal(result.reason, reason) + } + writer.releaseLock() +}) + +test('installation is idempotent and never replaces an existing host primitive', () => { + const original = globalThis.FixedLengthStream + const native = class NativeFixedLengthStream {} + try { + globalThis.FixedLengthStream = native + installWorkersStreams() + installWorkersStreams() + assert.equal(globalThis.FixedLengthStream, native) + } finally { + if (original === undefined) delete globalThis.FixedLengthStream + else globalThis.FixedLengthStream = original + } +}) diff --git a/crates/edgezero-adapter-fastly/Cargo.toml b/crates/edgezero-adapter-fastly/Cargo.toml index f080586fe..fd4b22715 100644 --- a/crates/edgezero-adapter-fastly/Cargo.toml +++ b/crates/edgezero-adapter-fastly/Cargo.toml @@ -19,7 +19,8 @@ cli = [ "dep:toml_edit", "dep:walkdir", ] -fastly = ["dep:fastly", "dep:log-fastly"] +fastly = ["dep:fastly", "dep:fastly-shared", "dep:fastly-sys", "dep:log-fastly"] +test-utils = [] [dependencies] anyhow = { workspace = true } @@ -33,6 +34,8 @@ brotli = { workspace = true } bytes = { workspace = true } ctor = { workspace = true, optional = true } fastly = { workspace = true, optional = true } +fastly-shared = { workspace = true, optional = true } +fastly-sys = { workspace = true, optional = true } flate2 = { workspace = true } futures = { workspace = true } futures-util = { workspace = true } @@ -41,7 +44,7 @@ log-fastly = { workspace = true, optional = true } fern = { workspace = true } chrono = { workspace = true } serde = { workspace = true } -serde_json = { workspace = true } +serde_json = { workspace = true, features = ["raw_value"] } base16ct = { workspace = true } sha2 = { workspace = true } thiserror = { workspace = true } @@ -52,3 +55,6 @@ walkdir = { workspace = true, optional = true } edgezero-core = { path = "../edgezero-core", features = ["test-utils"] } handlebars = { workspace = true } tempfile = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +log-fastly = { workspace = true, features = ["native-test-stubs"] } diff --git a/crates/edgezero-adapter-fastly/src/chunked_config.rs b/crates/edgezero-adapter-fastly/src/chunked_config.rs index 4964cff43..7784b37bd 100644 --- a/crates/edgezero-adapter-fastly/src/chunked_config.rs +++ b/crates/edgezero-adapter-fastly/src/chunked_config.rs @@ -20,7 +20,15 @@ //! ``` use base16ct::lower::encode_string; +use serde::de::{IgnoredAny, MapAccess, Visitor}; +use serde_json::value::RawValue; use sha2::{Digest as _, Sha256}; +use std::fmt; + +#[cfg(any(feature = "fastly", test))] +use edgezero_core::config_store::ConfigStoreError; +#[cfg(any(feature = "fastly", test))] +use edgezero_core::{BoundedStoreRead, Deadline, MonotonicClock, MonotonicInstant}; /// Per-entry value limit enforced by Fastly Config Store. Used by the CLI writer /// to gate direct-vs-chunked storage, and by the pointer validator (which both @@ -49,6 +57,17 @@ pub(crate) const CHUNK_KEY_INFIX: &str = ".__edgezero_chunks."; /// (when validating the parsed pointer) -- stays unconditional. pub(crate) const POINTER_KIND: &str = "fastly_config_chunks"; +#[derive(Debug, Eq, PartialEq)] +#[cfg(any(feature = "fastly", test))] +pub(crate) struct FastlyReadTooLarge; + +#[derive(Debug)] +#[cfg(any(feature = "fastly", test))] +pub(crate) enum SyncHostCallError { + Backend(E), + DeadlineExceeded, +} + // --------------------------------------------------------------------------- // Private pointer schema // --------------------------------------------------------------------------- @@ -85,6 +104,17 @@ enum RootValueKind { UnknownKind, } +// Borrow only the two discriminator tokens. Skipped payloads must never become +// a Value tree before the extractor's application-owned structural preflight. +#[derive(Default)] +struct RootMetadata<'raw> { + ambiguous: bool, + edgezero_kind: Option<&'raw RawValue>, + version: Option<&'raw RawValue>, +} + +struct MetadataVisitor; + /// Why resolving a stored root value failed. Keeping the future-format case /// DISTINCT from corruption is the whole point: a newer format must never be /// overwritten (CLI) or read as repairable corruption, whereas genuinely corrupt @@ -92,6 +122,7 @@ enum RootValueKind { /// pointer resolution — a v2 envelope reassembled from v1 chunks is only knowable /// AFTER the chunks are fetched — so it cannot be recovered from the raw value /// alone. +#[derive(Debug)] pub(crate) enum ResolveFailure { /// Corrupt or incomplete state a push can repair by overwriting. Carries a /// redacted, human-readable message. @@ -102,6 +133,17 @@ pub(crate) enum ResolveFailure { FutureFormat(String), } +/// A bounded resolver failure that preserves store boundary errors instead of +/// misclassifying them as corrupt pointer state. +#[derive(Debug)] +#[cfg(any(feature = "fastly", test))] +pub(crate) enum BoundedResolveFailure { + Backend(ConfigStoreError), + DeadlineExceeded, + Resolve(ResolveFailure), + ValueTooLarge, +} + impl ResolveFailure { /// The redacted, human-readable message, discarding the category. pub(crate) fn into_message(self) -> String { @@ -112,7 +154,11 @@ impl ResolveFailure { /// Is this a newer format this build must not overwrite (vs. repairable /// corruption)? - #[cfg(any(feature = "cli", feature = "fastly", test))] + #[cfg(any( + feature = "fastly", + test, + all(feature = "cli", not(target_arch = "wasm32")) + ))] pub(crate) fn is_future_format(&self) -> bool { matches!(self, Self::FutureFormat(_)) } @@ -126,7 +172,7 @@ struct FastlyChunkRef { } /// A chunk reference from a validated pointer, for `config gc`. -#[cfg(any(feature = "cli", test))] +#[cfg(any(test, all(feature = "cli", not(target_arch = "wasm32"))))] pub(crate) struct GcChunkRef { pub key: String, pub len: usize, @@ -134,7 +180,7 @@ pub(crate) struct GcChunkRef { } /// A validated v1 pointer's contents, for `config gc`. -#[cfg(any(feature = "cli", test))] +#[cfg(any(test, all(feature = "cli", not(target_arch = "wasm32"))))] pub(crate) struct GcPointer { /// Validated: non-empty, one generation, dense `0..n-1` in order. pub chunks: Vec, @@ -143,7 +189,7 @@ pub(crate) struct GcPointer { } /// What a root's value IS, once classified for `config gc`. -#[cfg(any(feature = "cli", test))] +#[cfg(any(test, all(feature = "cli", not(target_arch = "wasm32"))))] pub(crate) enum GcRootValue { /// A valid v1 chunk pointer. Its METADATA is validated; its CONTENT must /// still be verified against the store -- see [`gc_verify_generation`]. @@ -156,6 +202,151 @@ pub(crate) enum GcRootValue { // Public helpers // --------------------------------------------------------------------------- +impl<'de> serde::Deserialize<'de> for RootMetadata<'de> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_map(MetadataVisitor) + } + + fn deserialize_in_place(deserializer: D, place: &mut Self) -> Result<(), D::Error> + where + D: serde::Deserializer<'de>, + { + *place = Self::deserialize(deserializer)?; + Ok(()) + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "deserialize_map only dispatches JSON objects to this metadata visitor" +)] +impl<'de> Visitor<'de> for MetadataVisitor { + type Value = RootMetadata<'de>; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a config metadata object") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut metadata = RootMetadata::default(); + while let Some(key) = map.next_key::()? { + match key.as_str() { + "edgezero_kind" => { + metadata.ambiguous |= metadata.edgezero_kind.is_some(); + metadata.edgezero_kind = Some(map.next_value()?); + } + "version" => { + metadata.ambiguous |= metadata.version.is_some(); + metadata.version = Some(map.next_value()?); + } + _ => { + map.next_value::()?; + } + } + } + Ok(metadata) + } +} + +#[cfg(any(feature = "fastly", test))] +fn check_bounded_root( + root_key: &str, + root_value: &str, + remaining_backend: u64, + max_value_bytes: u64, +) -> Result<(), BoundedResolveFailure> { + match classify_root_value(root_value) { + RootValueKind::Pointer + if root_value.len() <= FASTLY_CONFIG_ENTRY_LIMIT + && !value_is_future_format(root_value) => + { + let pointer: FastlyChunkPointer = + serde_json::from_str(root_value).map_err(|_error| { + BoundedResolveFailure::Resolve(ResolveFailure::Corrupt( + "config chunk pointer is malformed".to_owned(), + )) + })?; + validate_pointer_chunks(root_key, &pointer) + .map_err(|error| BoundedResolveFailure::Resolve(ResolveFailure::Corrupt(error)))?; + let envelope_bytes = u64::try_from(pointer.envelope_len) + .map_err(|_error| BoundedResolveFailure::ValueTooLarge)?; + if envelope_bytes > max_value_bytes || envelope_bytes > remaining_backend { + return Err(BoundedResolveFailure::ValueTooLarge); + } + } + RootValueKind::Foreign | RootValueKind::MalformedObject => { + if u64::try_from(root_value.len()).map_or(true, |length| length > max_value_bytes) { + return Err(BoundedResolveFailure::ValueTooLarge); + } + } + RootValueKind::Pointer | RootValueKind::UnknownKind => {} + } + Ok(()) +} + +/// Account for the exact bytes materialized by a Fastly store operation. +#[cfg(any(feature = "fastly", test))] +pub(crate) fn exact_fastly_read( + value: Option, + max_backend_bytes: u64, +) -> Result, FastlyReadTooLarge> +where + T: AsRef<[u8]>, +{ + let backend_bytes = value.as_ref().map_or(Ok(0_u64), |stored_value| { + u64::try_from(stored_value.as_ref().len()).map_err(|_length_error| FastlyReadTooLarge) + })?; + if backend_bytes > max_backend_bytes { + return Err(FastlyReadTooLarge); + } + Ok(BoundedStoreRead { + backend_bytes, + value, + }) +} + +/// Run one synchronous Fastly operation under cooperative deadline checks. +/// +/// The host call itself is not preemptible. An overrun is observed immediately +/// after the call returns and its result is discarded. +#[cfg(any(feature = "fastly", test))] +pub(crate) fn run_sync_host_call( + clock: &MonotonicClock, + deadline: Deadline, + call: F, +) -> Result> +where + F: FnOnce() -> Result, +{ + run_sync_host_call_at(deadline, || clock.now(), call) +} + +#[cfg(any(feature = "fastly", test))] +fn run_sync_host_call_at( + deadline: Deadline, + mut now: N, + call: F, +) -> Result> +where + N: FnMut() -> MonotonicInstant, + F: FnOnce() -> Result, +{ + if deadline.instant() <= now() { + return Err(SyncHostCallError::DeadlineExceeded); + } + let result = call(); + if deadline.instant() <= now() { + return Err(SyncHostCallError::DeadlineExceeded); + } + result.map_err(SyncHostCallError::Backend) +} + /// Compute the lowercase-hex SHA-256 of `bytes`. pub(crate) fn sha256_hex(bytes: &[u8]) -> String { encode_string(&Sha256::digest(bytes)) @@ -176,7 +367,7 @@ pub(crate) fn sha256_hex(bytes: &[u8]) -> String { /// characters (extremely unlikely in practice; recommends restructuring). /// Reject a physical Config Store key that exceeds the store's key limit, /// before any write is attempted. -#[cfg(any(feature = "cli", test))] +#[cfg(any(test, all(feature = "cli", not(target_arch = "wasm32"))))] fn check_config_key_len(key: &str) -> Result<(), String> { // CHARACTERS, not bytes: Fastly's limit is a character count, so a non-ASCII // `--key` must be measured by `chars().count()`, not `len()` (UTF-8 bytes). @@ -193,7 +384,7 @@ fn check_config_key_len(key: &str) -> Result<(), String> { Ok(()) } -#[cfg(any(feature = "cli", test))] +#[cfg(any(test, all(feature = "cli", not(target_arch = "wasm32"))))] pub(crate) fn prepare_fastly_config_entries( root_key: &str, envelope_json: &str, @@ -387,7 +578,7 @@ pub(crate) fn verify_writer_split_layout( /// Only the GC path (`cli.rs`) needs it; the runtime resolver inlines the same /// mapping, so this is gated to the `cli` feature to stay dead-code-free in the /// guest build. -#[cfg(feature = "cli")] +#[cfg(all(feature = "cli", not(target_arch = "wasm32")))] pub(crate) fn chunk_lengths(chunks: &[GcChunkRef]) -> Vec { chunks.iter().map(|chunk| chunk.len).collect() } @@ -418,9 +609,21 @@ pub(crate) fn chunk_lengths(chunks: &[GcChunkRef]) -> Vec { /// not be overwritten) from repairable corruption. [`resolve_fastly_config_value`] /// is the string-erased wrapper for callers that do not need the distinction. pub(crate) fn resolve_fastly_config_value_typed( + root_key: &str, + root_value: String, + fetch: F, +) -> Result +where + F: FnMut(&str) -> Result, String>, +{ + resolve_fastly_config_value_inner(root_key, root_value, fetch, true) +} + +fn resolve_fastly_config_value_inner( root_key: &str, root_value: String, mut fetch: F, + verify_inner_envelope: bool, ) -> Result where F: FnMut(&str) -> Result, String>, @@ -500,11 +703,14 @@ where // Fetch, verify, and concatenate all chunks. // - // NOT `with_capacity(pointer.envelope_len)`: that length is untrusted stored - // metadata, and this runs in the edge guest. A pointer declaring `usize::MAX` - // would abort the worker on a capacity overflow before any of the checks - // below could reject it. Grow from the bytes we actually fetch instead. + // Metadata has already passed checked length summation and per-chunk caps. + // The bounded caller also checks both aggregate allowances before this point. let mut reconstructed = String::new(); + reconstructed + .try_reserve_exact(pointer.envelope_len) + .map_err(|_error| { + ResolveFailure::Corrupt("config reassembly allocation failed".to_owned()) + })?; for (position, chunk_ref) in pointer.chunks.iter().enumerate() { let chunk_value = fetch(&chunk_ref.key) .map_err(|_err| { @@ -554,8 +760,7 @@ where } // Verify total envelope SHA. - let actual_env_sha = sha256_hex(reconstructed.as_bytes()); - if actual_env_sha != pointer.envelope_sha256 { + if sha256_hex(reconstructed.as_bytes()) != pointer.envelope_sha256 { // Neither SHA is echoed: the expected one is `pointer.envelope_sha256`, // a stored, value-controlled string that a malformed pointer can set to // anything (a secret), and this runs on the read path where diagnostics @@ -570,7 +775,145 @@ where verify_writer_split_layout(root_key, &reconstructed, &declared_lens) .map_err(ResolveFailure::Corrupt)?; - finalize_reconstructed_envelope(root_key, reconstructed) + if verify_inner_envelope { + finalize_reconstructed_envelope(root_key, reconstructed) + } else { + // This path verifies storage transport integrity, not typed config. + // The extractor owns envelope verification after its structural scan, + // exactly as for a directly stored envelope. + Ok(reconstructed) + } +} + +/// Resolve a Fastly value while accounting for every materialized store byte. +/// +/// `fetch` receives the backend allowance left after the root pointer and all +/// prior chunks. Deadline checks are cooperative: they bracket each callback, +/// but cannot interrupt a synchronous host call already in progress. +#[cfg(any(feature = "fastly", test))] +#[expect( + clippy::too_many_arguments, + reason = "root accounting, the shared clock/deadline, both byte caps, and the chunk callback are independent contract inputs" +)] +pub(crate) fn resolve_fastly_config_value_typed_bounded( + root_key: &str, + root_value: String, + root_backend_bytes: u64, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + mut fetch: F, +) -> Result, BoundedResolveFailure> +where + F: FnMut(&str, u64) -> Result, ConfigStoreError>, +{ + if deadline.is_expired_at(clock.now()) { + return Err(BoundedResolveFailure::DeadlineExceeded); + } + + let root_value_bytes = u64::try_from(root_value.len()) + .map_err(|_length_error| BoundedResolveFailure::ValueTooLarge)?; + if root_backend_bytes != root_value_bytes || root_backend_bytes > max_backend_bytes { + return Err(BoundedResolveFailure::ValueTooLarge); + } + + let preflight = check_bounded_root( + root_key, + &root_value, + max_backend_bytes.saturating_sub(root_backend_bytes), + max_value_bytes, + ); + if deadline.is_expired_at(clock.now()) { + return Err(BoundedResolveFailure::DeadlineExceeded); + } + preflight?; + + let mut backend_bytes = root_backend_bytes; + let mut chunk_value_bytes = 0_u64; + let mut boundary_error = None; + let resolve_outcome = resolve_fastly_config_value_inner( + root_key, + root_value, + |chunk_key| { + if deadline.is_expired_at(clock.now()) { + boundary_error = Some(BoundedResolveFailure::DeadlineExceeded); + return Err("bounded config read failed".to_owned()); + } + + let Some(remaining_backend_bytes) = max_backend_bytes.checked_sub(backend_bytes) else { + boundary_error = Some(BoundedResolveFailure::ValueTooLarge); + return Err("bounded config read failed".to_owned()); + }; + let remaining_value_bytes = max_value_bytes.saturating_sub(chunk_value_bytes); + let read_limit = remaining_backend_bytes + .min(remaining_value_bytes) + .min(7_000); + let read = match fetch(chunk_key, read_limit) { + Ok(read) => read, + Err(error) => { + boundary_error = Some(BoundedResolveFailure::Backend(error)); + return Err("bounded config read failed".to_owned()); + } + }; + + if deadline.is_expired_at(clock.now()) { + boundary_error = Some(BoundedResolveFailure::DeadlineExceeded); + return Err("bounded config read failed".to_owned()); + } + + let value_bytes = match read.value.as_ref() { + Some(value) => { + let Ok(value_bytes) = u64::try_from(value.len()) else { + boundary_error = Some(BoundedResolveFailure::ValueTooLarge); + return Err("bounded config read failed".to_owned()); + }; + value_bytes + } + None => 0, + }; + if read.backend_bytes != value_bytes || read.backend_bytes > read_limit { + boundary_error = Some(BoundedResolveFailure::ValueTooLarge); + return Err("bounded config read failed".to_owned()); + } + + let Some(next_backend_bytes) = backend_bytes.checked_add(read.backend_bytes) else { + boundary_error = Some(BoundedResolveFailure::ValueTooLarge); + return Err("bounded config read failed".to_owned()); + }; + let Some(next_chunk_value_bytes) = chunk_value_bytes.checked_add(value_bytes) else { + boundary_error = Some(BoundedResolveFailure::ValueTooLarge); + return Err("bounded config read failed".to_owned()); + }; + if next_backend_bytes > max_backend_bytes || next_chunk_value_bytes > max_value_bytes { + boundary_error = Some(BoundedResolveFailure::ValueTooLarge); + return Err("bounded config read failed".to_owned()); + } + + backend_bytes = next_backend_bytes; + chunk_value_bytes = next_chunk_value_bytes; + Ok(read.value) + }, + false, + ); + + if deadline.is_expired_at(clock.now()) { + return Err(BoundedResolveFailure::DeadlineExceeded); + } + if let Some(error) = boundary_error { + return Err(error); + } + let resolved_value = resolve_outcome.map_err(BoundedResolveFailure::Resolve)?; + let resolved_bytes = u64::try_from(resolved_value.len()) + .map_err(|_length_error| BoundedResolveFailure::ValueTooLarge)?; + if resolved_bytes > max_value_bytes { + return Err(BoundedResolveFailure::ValueTooLarge); + } + + Ok(BoundedStoreRead { + backend_bytes, + value: Some(resolved_value), + }) } /// The final gate the chunked path shares with the direct one: reject a NEWER @@ -648,7 +991,7 @@ where /// but fails validation: malformed, unsupported `version`, or a /// referenced key falls outside this root's chunk prefix. Callers log /// `msg` as a warning and skip GC for this root (delete nothing). -#[cfg(any(feature = "cli", test))] +#[cfg(any(test, all(feature = "cli", not(target_arch = "wasm32"))))] pub(crate) fn prior_chunk_keys(root_key: &str, raw: &str) -> Result, String> { // 1. Parse loosely. Not-JSON, or not our pointer kind => silent. let value: serde_json::Value = match serde_json::from_str(raw) { @@ -853,17 +1196,25 @@ fn classify_root_value(raw: &str) -> RootValueKind { } // It LOOKS like an object. If it does not parse, it is a malformed object -- // possibly a truncated/corrupt pointer -- NOT a value GC may assume is inert. - let Ok(value) = serde_json::from_str::(raw) else { + let Ok(value) = serde_json::from_str::>(raw) else { return RootValueKind::MalformedObject; }; + if value.ambiguous { + return RootValueKind::UnknownKind; + } // Discriminate on the PRESENCE of `edgezero_kind`, not just a string match. A // field of ANY type claims our reserved namespace, so a non-string value // (`edgezero_kind: 7`, an object, null) is `UnknownKind`, never `Foreign` -- // otherwise GC would wrongly treat it as an inert value and reclaim chunks a // future/foreign format still references. - match value.get("edgezero_kind") { + match value.edgezero_kind { None => RootValueKind::Foreign, - Some(serde_json::Value::String(kind)) if kind == POINTER_KIND => RootValueKind::Pointer, + Some(kind) + if serde_json::from_str::(kind.get()) + .is_ok_and(|token| token == POINTER_KIND) => + { + RootValueKind::Pointer + } Some(_) => RootValueKind::UnknownKind, } } @@ -886,7 +1237,7 @@ pub(crate) fn value_is_pointer_kind(raw: &str) -> bool { /// a malformed object (a possible truncated/corrupt pointer whose chunk /// references we cannot read), an unknown-kind value, and a pointer: those must /// fail closed, because assuming they reference no chunks could orphan live ones. -#[cfg(any(feature = "cli", test))] +#[cfg(any(test, all(feature = "cli", not(target_arch = "wasm32"))))] pub(crate) fn value_is_inert_foreign(raw: &str) -> bool { matches!(classify_root_value(raw), RootValueKind::Foreign) } @@ -899,7 +1250,7 @@ pub(crate) fn value_is_inert_foreign(raw: &str) -> bool { /// envelope fragment that announces nothing, so anything that DOES announce is /// suspicious (a parked pointer, a future-format value) and must be classified, /// not blindly treated as a deletable fragment. -#[cfg(any(feature = "cli", test))] +#[cfg(any(test, all(feature = "cli", not(target_arch = "wasm32"))))] pub(crate) fn value_announces_our_kind(raw: &str) -> bool { matches!( classify_root_value(raw), @@ -941,19 +1292,27 @@ pub(crate) fn value_is_unknown_kind(raw: &str) -> bool { /// the resolver is compiled regardless of feature set. pub(crate) fn value_is_future_format(raw: &str) -> bool { use edgezero_core::blob_envelope::ENVELOPE_VERSION_V1; - let Ok(serde_json::Value::Object(obj)) = serde_json::from_str::(raw) else { + let Ok(obj) = serde_json::from_str::>(raw) else { return false; }; + if obj.ambiguous { + return true; + } // A `version` field that is PRESENT but not EXACTLY the JSON integer 1 is a // newer format. Keying on `as_u64()` alone fails open: `"2"`, `-1`, `2.5`, or // `1.0` all yield `None` and would slip through as repairable corruption. Any // present-and-not-integer-1 version fails closed. - let version_is_not_one = obj - .get("version") - .is_some_and(|version| version.as_u64() != Some(u64::from(ENVELOPE_VERSION_V1))); - match obj.get("edgezero_kind") { + let version_is_not_one = obj.version.is_some_and(|version| { + serde_json::from_str::(version.get()).ok() != Some(u64::from(ENVELOPE_VERSION_V1)) + }); + match obj.edgezero_kind { // Our pointer kind, but a newer POINTER version. - Some(serde_json::Value::String(kind)) if kind == POINTER_KIND => version_is_not_one, + Some(kind) + if serde_json::from_str::(kind.get()) + .is_ok_and(|token| token == POINTER_KIND) => + { + version_is_not_one + } // A discriminator we do not recognise (unknown string, or non-string). Some(_) => true, // No discriminator: any object carrying a bumped `version` is a newer @@ -981,7 +1340,7 @@ pub(crate) fn value_is_future_format(raw: &str) -> bool { /// every metadata check still passes while the dropped chunk silently leaves the /// live set. Callers MUST reconstruct the referenced chunks and put them through /// [`gc_verify_generation`] before trusting the live set. -#[cfg(any(feature = "cli", test))] +#[cfg(any(test, all(feature = "cli", not(target_arch = "wasm32"))))] pub(crate) fn gc_classify_root(root_key: &str, raw: &str) -> Result { use edgezero_core::blob_envelope::BlobEnvelope; @@ -1079,7 +1438,7 @@ pub(crate) fn gc_classify_root(root_key: &str, raw: &str) -> Result Result<(), String> { use edgezero_core::blob_envelope::BlobEnvelope; @@ -1120,7 +1479,7 @@ pub(crate) fn gc_verify_generation(generation_sha: &str, assembled: &str) -> Res /// This is how reclamation groups the store's ACTUAL keys into generations. It /// validates the shape (`.__edgezero_chunks..`) rather /// than trusting it: a hand-edited or foreign key never becomes a delete target. -#[cfg(any(feature = "cli", test))] +#[cfg(any(test, all(feature = "cli", not(target_arch = "wasm32"))))] pub(crate) fn chunk_key_generation(root_key: &str, key: &str) -> Option { chunk_key_parts(root_key, key).map(|(generation, _)| generation) } @@ -1131,7 +1490,7 @@ pub(crate) fn chunk_key_generation(root_key: &str, key: &str) -> Option /// `config gc` uses this to order a proven generation's chunks by writer index /// before deleting, so preview and failure-recovery order do not depend on the /// remote listing order. -#[cfg(any(feature = "cli", test))] +#[cfg(any(test, all(feature = "cli", not(target_arch = "wasm32"))))] pub(crate) fn chunk_key_index(root_key: &str, key: &str) -> Option { chunk_key_parts(root_key, key).map(|(_, index)| index) } @@ -1193,8 +1552,407 @@ fn canonical_index(index: &str) -> Option { mod tests { use std::collections::HashMap; + use std::time::Duration; use super::*; + use edgezero_core::blob_envelope::BlobEnvelope; + use edgezero_core::{BoundedStoreRead, Deadline, MonotonicInstant}; + + fn unexpected_chunk_fetch( + _key: &str, + _remaining_backend_bytes: u64, + ) -> Result, ConfigStoreError> { + Err(ConfigStoreError::unavailable("unexpected chunk fetch")) + } + + #[test] + fn exact_fastly_read_accepts_exact_bytes_and_rejects_one_over() { + let exact = exact_fastly_read(Some(bytes::Bytes::from_static(b"secret")), 6) + .expect("an exact-sized value must succeed"); + assert_eq!(exact.backend_bytes, 6); + assert_eq!(exact.value.as_deref(), Some(b"secret".as_slice())); + + let error = exact_fastly_read(Some(bytes::Bytes::from_static(b"secret")), 5) + .expect_err("one byte over the cap must fail"); + assert_eq!(error, FastlyReadTooLarge); + } + + #[test] + fn synchronous_host_call_checks_deadline_before_and_after_operation() { + let start = MonotonicInstant::now(); + let deadline = Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), + ); + let after_deadline = start + .checked_add(Duration::from_secs(2)) + .expect("post-deadline instant"); + let mut observations = [start, after_deadline].into_iter(); + let mut calls = 0_usize; + + let error = run_sync_host_call_at( + deadline, + || observations.next().expect("clock observation"), + || { + calls += 1; + Ok::<_, ()>("materialized") + }, + ) + .expect_err("an operation completing after the deadline must fail"); + assert!(matches!(error, SyncHostCallError::DeadlineExceeded)); + assert_eq!( + calls, 1, + "the deadline elapsed while the call was in flight" + ); + + let mut backend_observations = [start, after_deadline].into_iter(); + let backend_error = run_sync_host_call_at( + deadline, + || backend_observations.next().expect("clock observation"), + || Err::<(), _>("backend failed"), + ) + .expect_err("post-call deadline must also be checked after backend failure"); + assert!(matches!(backend_error, SyncHostCallError::DeadlineExceeded)); + + let mut called_after_expiry = false; + let preflight_error = run_sync_host_call_at( + deadline, + || after_deadline, + || { + called_after_expiry = true; + Ok::<_, ()>("unreachable") + }, + ) + .expect_err("an already-expired operation must fail"); + assert!(matches!( + preflight_error, + SyncHostCallError::DeadlineExceeded + )); + assert!(!called_after_expiry, "an expired call must not be started"); + } + + #[test] + fn bounded_failures_preserve_backend_and_resolver_payloads() { + let backend_failure = BoundedResolveFailure::Backend(ConfigStoreError::unavailable( + "temporary store failure", + )); + match backend_failure { + BoundedResolveFailure::Backend(error) => drop(error), + BoundedResolveFailure::DeadlineExceeded + | BoundedResolveFailure::Resolve(_) + | BoundedResolveFailure::ValueTooLarge => { + panic!("backend failure variant changed"); + } + } + + let resolve_failure = + BoundedResolveFailure::Resolve(ResolveFailure::Corrupt("invalid pointer".to_owned())); + match resolve_failure { + BoundedResolveFailure::Resolve(error) => { + assert_eq!(error.into_message(), "invalid pointer"); + } + BoundedResolveFailure::Backend(_) + | BoundedResolveFailure::DeadlineExceeded + | BoundedResolveFailure::ValueTooLarge => { + panic!("resolver failure variant changed"); + } + } + + let start = MonotonicInstant::now(); + let deadline = Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), + ); + let mut observations = [start, start].into_iter(); + let backend_error = run_sync_host_call_at( + deadline, + || observations.next().expect("clock observation"), + || Err::<(), _>("backend failed"), + ) + .expect_err("backend failure must remain inspectable"); + match backend_error { + SyncHostCallError::Backend(message) => assert_eq!(message, "backend failed"), + SyncHostCallError::DeadlineExceeded => panic!("deadline must not win"), + } + + let clock = MonotonicClock::default(); + let wrapper_result = + run_sync_host_call(&clock, Deadline::after(Duration::from_secs(1)), || { + Ok::<_, ()>("materialized") + }) + .expect("wrapper call before its deadline must succeed"); + assert_eq!(wrapper_result, "materialized"); + } + + #[test] + fn metadata_classification_rejects_duplicate_reserved_fields_and_arrays() { + // Duplicate JSON members intentionally cannot be constructed with json!. + for raw in [ + r#"{"version":1,"version":2}"#, + r#"{"version":1,"version":1}"#, + r#"{"edgezero_kind":"fastly_config_chunks","edgezero_kind":"future"}"#, + ] { + assert!( + value_is_future_format(raw), + "ambiguous metadata must never be overwrite-safe" + ); + } + assert!( + !value_is_future_format("[null,2]"), + "arrays are not envelope metadata objects" + ); + } + + #[test] + fn bounded_resolver_rejects_declared_overflow_before_fetching() { + let root_key = "app_config"; + let envelope = serde_json::to_string(&BlobEnvelope::new( + serde_json::json!({ "pad": "x".repeat(9_000) }), + "2026-01-01T00:00:00Z".to_owned(), + )) + .expect("envelope"); + let mut entries = prepare_fastly_config_entries(root_key, &envelope).expect("entries"); + let (_, pointer) = entries.pop().expect("pointer"); + let pointer_bytes = u64::try_from(pointer.len()).expect("pointer bytes"); + let envelope_bytes = u64::try_from(envelope.len()).expect("envelope bytes"); + for (backend_limit, value_limit) in [ + (u64::MAX, envelope_bytes - 1), + (pointer_bytes + envelope_bytes - 1, envelope_bytes), + ] { + let mut calls = 0_u32; + let error = resolve_fastly_config_value_typed_bounded( + root_key, + pointer.clone(), + pointer_bytes, + &MonotonicClock::default(), + Deadline::after(Duration::from_secs(1)), + backend_limit, + value_limit, + |_key, _remaining| { + calls += 1; + Ok(BoundedStoreRead { + backend_bytes: 0, + value: None, + }) + }, + ) + .expect_err("declared overflow"); + assert!(matches!(error, BoundedResolveFailure::ValueTooLarge)); + assert_eq!(calls, 0, "reject metadata before fetching or assembling"); + } + } + + #[test] + fn bounded_resolver_accepts_exact_direct_caps_and_rejects_over_cap() { + let clock = MonotonicClock::default(); + let exact = resolve_fastly_config_value_typed_bounded( + "root", + "value".to_owned(), + 5, + &clock, + Deadline::after(Duration::from_secs(1)), + 5, + 5, + unexpected_chunk_fetch, + ) + .expect("a direct value at both exact caps must succeed"); + + assert_eq!(exact.backend_bytes, 5); + assert_eq!(exact.value.as_deref(), Some("value")); + + let over_backend = resolve_fastly_config_value_typed_bounded( + "root", + "value".to_owned(), + 5, + &clock, + Deadline::after(Duration::from_secs(1)), + 4, + 5, + unexpected_chunk_fetch, + ) + .expect_err("a direct value over the backend cap must fail"); + assert!(matches!(over_backend, BoundedResolveFailure::ValueTooLarge)); + + let over_value = resolve_fastly_config_value_typed_bounded( + "root", + "value".to_owned(), + 5, + &clock, + Deadline::after(Duration::from_secs(1)), + 5, + 4, + unexpected_chunk_fetch, + ) + .expect_err("a direct value over the value cap must fail"); + assert!(matches!(over_value, BoundedResolveFailure::ValueTooLarge)); + } + + #[test] + fn bounded_resolver_deadline_wins_metadata_preflight_errors() { + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + let start = edgezero_core::MonotonicInstant::now(); + let end = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let samples = Arc::new(AtomicUsize::new(0)); + let clock = MonotonicClock::new(move || { + if samples.fetch_add(1, Ordering::SeqCst) == 0 { + start + } else { + end + } + }); + let error = resolve_fastly_config_value_typed_bounded( + "root", + "value".to_owned(), + 5, + &clock, + Deadline::at_instant(end), + 5, + 4, + unexpected_chunk_fetch, + ) + .expect_err("deadline must win a simultaneously observed size failure"); + assert!(matches!(error, BoundedResolveFailure::DeadlineExceeded)); + } + + #[test] + fn bounded_resolver_rejects_expired_deadline_without_fetching() { + let mut fetched = false; + let clock = MonotonicClock::default(); + let error = resolve_fastly_config_value_typed_bounded( + "root", + "value".to_owned(), + 5, + &clock, + Deadline::after(Duration::ZERO), + 5, + 5, + |_key, _remaining_backend_bytes| { + fetched = true; + Ok(BoundedStoreRead { + backend_bytes: 0, + value: None, + }) + }, + ) + .expect_err("an expired deadline must fail"); + + assert!(matches!(error, BoundedResolveFailure::DeadlineExceeded)); + assert!(!fetched, "an expired read must not fetch chunks"); + } + + #[test] + fn bounded_resolver_counts_pointer_and_chunks_with_decreasing_allowance() { + let clock = MonotonicClock::default(); + let root_key = "app_config"; + let envelope = serde_json::to_string(&BlobEnvelope::new( + serde_json::json!({ "pad": "x".repeat(9_000) }), + "2026-01-01T00:00:00Z".to_owned(), + )) + .expect("envelope"); + let mut entries = prepare_fastly_config_entries(root_key, &envelope).expect("entries"); + let (_, pointer) = entries.pop().expect("root pointer"); + let chunks: HashMap<_, _> = entries.into_iter().collect(); + let pointer_bytes = u64::try_from(pointer.len()).expect("pointer length"); + let maybe_chunk_bytes = chunks.values().try_fold(0_u64, |total, chunk| { + total.checked_add(u64::try_from(chunk.len()).ok()?) + }); + let chunk_bytes = maybe_chunk_bytes.expect("chunk byte sum"); + let total_backend_bytes = pointer_bytes + .checked_add(chunk_bytes) + .expect("backend byte sum"); + let mut remaining_allowances = Vec::new(); + + let resolved = resolve_fastly_config_value_typed_bounded( + root_key, + pointer.clone(), + pointer_bytes, + &clock, + Deadline::after(Duration::from_secs(1)), + total_backend_bytes, + u64::try_from(envelope.len()).expect("envelope length"), + |chunk_key, remaining_backend_bytes| { + remaining_allowances.push(remaining_backend_bytes); + let value = chunks.get(chunk_key).cloned(); + Ok(BoundedStoreRead { + backend_bytes: value + .as_ref() + .map_or(0, |chunk| u64::try_from(chunk.len()).expect("chunk length")), + value, + }) + }, + ) + .expect("exact pointer and chunk caps must succeed"); + + assert_eq!(resolved.backend_bytes, total_backend_bytes); + assert_eq!(resolved.value.as_deref(), Some(envelope.as_str())); + assert_eq!(remaining_allowances.first(), Some(&chunk_bytes.min(7_000))); + assert!( + remaining_allowances + .windows(2) + .all(|pair| pair[1] < pair[0]), + "each chunk must receive a smaller backend allowance" + ); + + let error = resolve_fastly_config_value_typed_bounded( + root_key, + pointer, + pointer_bytes, + &clock, + Deadline::after(Duration::from_secs(1)), + total_backend_bytes - 1, + u64::try_from(envelope.len()).expect("envelope length"), + |chunk_key, _remaining_backend_bytes| { + let value = chunks.get(chunk_key).cloned(); + Ok(BoundedStoreRead { + backend_bytes: value + .as_ref() + .map_or(0, |chunk| u64::try_from(chunk.len()).expect("chunk length")), + value, + }) + }, + ) + .expect_err("one byte below the aggregate backend cap must fail"); + assert!(matches!(error, BoundedResolveFailure::ValueTooLarge)); + } + + #[test] + fn bounded_resolver_rejects_inconsistent_chunk_reports() { + let clock = MonotonicClock::default(); + let root_key = "app_config"; + let envelope = serde_json::to_string(&BlobEnvelope::new( + serde_json::json!({ "pad": "x".repeat(9_000) }), + "2026-01-01T00:00:00Z".to_owned(), + )) + .expect("envelope"); + let mut entries = prepare_fastly_config_entries(root_key, &envelope).expect("entries"); + let (_, pointer) = entries.pop().expect("root pointer"); + let chunks: HashMap<_, _> = entries.into_iter().collect(); + let pointer_bytes = u64::try_from(pointer.len()).expect("pointer length"); + + let error = resolve_fastly_config_value_typed_bounded( + root_key, + pointer, + pointer_bytes, + &clock, + Deadline::after(Duration::from_secs(1)), + u64::MAX, + u64::try_from(envelope.len()).expect("envelope length"), + |chunk_key, _remaining_backend_bytes| { + let value = chunks.get(chunk_key).cloned(); + Ok(BoundedStoreRead { + backend_bytes: 0, + value, + }) + }, + ) + .expect_err("a provider report smaller than its result must fail closed"); + + assert!(matches!(error, BoundedResolveFailure::ValueTooLarge)); + } /// the index must be one this writer could emit. The /// chunk loop counts in `usize`, so a digit run that overflows it is not @@ -1318,8 +2076,11 @@ mod tests { ("empty", String::new()), ("whitespace", " ".to_owned()), ("not json", "not-json-at-all".to_owned()), - ("unrelated json", r#"{"some":"value"}"#.to_owned()), - ("json scalar", "42".to_owned()), + ( + "unrelated json", + serde_json::json!({"some": "value"}).to_string(), + ), + ("json scalar", serde_json::json!(42_u32).to_string()), ("valid-JSON partial pointer", reserialise(&partial)), ]; for (label, raw) in cases { @@ -1401,9 +2162,15 @@ mod tests { let root = "app_config"; // `version` is typed `u8`; a string there makes serde quote it: // `invalid type: string "s3cr3t-do-not-log", expected u8`. - let malformed = format!( - r#"{{"edgezero_kind":"{POINTER_KIND}","version":"{SENTINEL}","chunks":[],"data_sha256":"","envelope_len":0,"envelope_sha256":""}}"# - ); + let malformed = serde_json::json!({ + "edgezero_kind": POINTER_KIND, + "version": SENTINEL, + "chunks": [], + "data_sha256": "", + "envelope_len": 0_u32, + "envelope_sha256": "", + }) + .to_string(); let Err(gc_err) = gc_classify_root(root, &malformed) else { panic!("a malformed pointer must fail closed on the gc path"); }; @@ -1510,7 +2277,10 @@ mod tests { for (label, value) in [ ("plain text", "just some plain text"), - ("unrelated json", r#"{"some":"value"}"#), + ( + "unrelated json", + &serde_json::json!({"some": "value"}).to_string(), + ), ("someone's real config", make_envelope_json(200).as_str()), ] { assert!( @@ -1534,10 +2304,15 @@ mod tests { fn pointer_key_does_not_leak_into_diagnostics() { const SENTINEL: &str = "prod-db-password=hunter2"; let root = "app_config"; - let malformed = format!( - r#"{{"edgezero_kind":"{POINTER_KIND}","version":1,"chunks":[{{"key":"{SENTINEL}","len":10,"sha256":"x"}}],"data_sha256":"","envelope_len":10,"envelope_sha256":"{}"}}"#, - "a".repeat(64) - ); + let malformed = serde_json::json!({ + "edgezero_kind": POINTER_KIND, + "version": 1_u32, + "chunks": [{"key": SENTINEL, "len": 10_u32, "sha256": "x"}], + "data_sha256": "", + "envelope_len": 10_u32, + "envelope_sha256": "a".repeat(64), + }) + .to_string(); let err = prior_chunk_keys(root, &malformed).expect_err("must warn"); assert!( !err.contains(SENTINEL), @@ -1554,18 +2329,32 @@ mod tests { let root = "app_config"; let sha = "a".repeat(64); let huge = usize::MAX; - let overflowing = format!( - r#"{{"edgezero_kind":"{POINTER_KIND}","version":1,"chunks":[{{"key":"{root}{CHUNK_KEY_INFIX}{sha}.0","len":{huge},"sha256":"x"}},{{"key":"{root}{CHUNK_KEY_INFIX}{sha}.1","len":{huge},"sha256":"y"}}],"data_sha256":"","envelope_len":{huge},"envelope_sha256":"{sha}"}}"# - ); + let overflowing = serde_json::json!({ + "edgezero_kind": POINTER_KIND, + "version": 1_u32, + "chunks": [ + {"key": format!("{root}{CHUNK_KEY_INFIX}{sha}.0"), "len": huge, "sha256": "x"}, + {"key": format!("{root}{CHUNK_KEY_INFIX}{sha}.1"), "len": huge, "sha256": "y"}, + ], + "data_sha256": "", + "envelope_len": huge, + "envelope_sha256": sha, + }) + .to_string(); assert!( prior_chunk_keys(root, &overflowing).is_err(), "chunk lengths that overflow when summed must be rejected" ); // A single absurd chunk: no overflow, but far beyond what we emit. - let oversized = format!( - r#"{{"edgezero_kind":"{POINTER_KIND}","version":1,"chunks":[{{"key":"{root}{CHUNK_KEY_INFIX}{sha}.0","len":{huge},"sha256":"x"}}],"data_sha256":"","envelope_len":{huge},"envelope_sha256":"{sha}"}}"# - ); + let oversized = serde_json::json!({ + "edgezero_kind": POINTER_KIND, + "version": 1_u32, + "chunks": [{"key": format!("{root}{CHUNK_KEY_INFIX}{sha}.0"), "len": huge, "sha256": "x"}], + "data_sha256": "", + "envelope_len": huge, + "envelope_sha256": sha, + }).to_string(); assert!( prior_chunk_keys(root, &oversized).is_err(), "a chunk larger than the writer's payload target must be rejected" @@ -1592,7 +2381,9 @@ mod tests { ); // Direct envelopes, unrelated JSON and non-JSON are not pointers either. assert!(!value_is_pointer_kind(&make_envelope_json(200))); - assert!(!value_is_pointer_kind(r#"{"some":"value"}"#)); + assert!(!value_is_pointer_kind( + &serde_json::json!({"some": "value"}).to_string() + )); assert!(!value_is_pointer_kind("not json at all")); } @@ -1606,7 +2397,7 @@ mod tests { // (raw, is_pointer, is_inert_foreign, announces_our_kind, is_unknown_kind) let cases: &[(&str, bool, bool, bool, bool)] = &[ ( - r#"{"edgezero_kind":"fastly_config_chunks"}"#, + &serde_json::json!({"edgezero_kind": "fastly_config_chunks"}).to_string(), true, false, true, @@ -1614,24 +2405,42 @@ mod tests { ), // Unknown STRING kind: claims our namespace, not inert, not our pointer. ( - r#"{"edgezero_kind":"fastly_config_chunks_v2"}"#, + &serde_json::json!({"edgezero_kind": "fastly_config_chunks_v2"}).to_string(), false, false, true, true, ), // NON-STRING kind: still claims the namespace -- must NOT be inert. - (r#"{"edgezero_kind":7}"#, false, false, true, true), - (r#"{"edgezero_kind":null}"#, false, false, true, true), ( - r#"{"edgezero_kind":{"nested":true}}"#, + &serde_json::json!({"edgezero_kind": 7_u64}).to_string(), + false, + false, + true, + true, + ), + ( + &serde_json::json!({"edgezero_kind": null}).to_string(), + false, + false, + true, + true, + ), + ( + &serde_json::json!({"edgezero_kind": {"nested": true}}).to_string(), false, false, true, true, ), // Genuinely foreign: no discriminator. - (r#"{"unrelated":"json"}"#, false, true, false, false), + ( + &serde_json::json!({"unrelated": "json"}).to_string(), + false, + true, + false, + false, + ), ("value_a", false, true, false, false), ("", false, true, false, false), // Object-shaped but unparseable (a possible truncated pointer): none. @@ -1679,38 +2488,48 @@ mod tests { // SCHEMA-CHANGING v2: a future shape that DROPS the v1 fields must still be // future -- detection keys on `version` alone, not the four v1 fields. assert!( - value_is_future_format(r#"{"version":2,"payload":{"k":"v"}}"#), + value_is_future_format( + &serde_json::json!({"version": 2_u64, "payload": {"k": "v"}}).to_string() + ), "schema-changing v2 (no v1 fields)" ); // A `version` PRESENT but not EXACTLY the JSON integer 1 is future, // whatever its JSON type -- `as_u64()` alone would let these slip through. for raw in [ - r#"{"version":"2"}"#, // string - r#"{"version":-1}"#, // negative integer - r#"{"version":2.5}"#, // float - r#"{"version":1.0}"#, // float, not the integer 1 - r#"{"version":null}"#, // present but null + json!({"version": "2"}).to_string(), + json!({"version": -1_i32}).to_string(), + json!({"version": 2.5_f64}).to_string(), + json!({"version": 1.0_f64}).to_string(), // float, not the integer 1 + json!({"version": null}).to_string(), ] { assert!( - value_is_future_format(raw), + value_is_future_format(&raw), "a non-integer-1 version must be future: {raw}" ); } // The exact JSON integer 1 (and an absent version) is NOT future. - assert!(!value_is_future_format(r#"{"version":1,"greeting":"hi"}"#)); - assert!(!value_is_future_format(r#"{"greeting":"hi"}"#)); + assert!(!value_is_future_format( + &json!({"version": 1_u32, "greeting": "hi"}).to_string() + )); + assert!(!value_is_future_format( + &serde_json::json!({"greeting": "hi"}).to_string() + )); // A v2 POINTER (our kind, bumped pointer version) -> future. assert!( value_is_future_format( - r#"{"edgezero_kind":"fastly_config_chunks","version":2,"chunks":[]}"# + &serde_json::json!({"edgezero_kind": "fastly_config_chunks", "version": 2_u64, "chunks": []}).to_string() ), "v2 pointer" ); // Unknown/non-string kind -> future. - assert!(value_is_future_format(r#"{"edgezero_kind":"future"}"#)); - assert!(value_is_future_format(r#"{"edgezero_kind":9}"#)); + assert!(value_is_future_format( + &serde_json::json!({"edgezero_kind": "future"}).to_string() + )); + assert!(value_is_future_format( + &serde_json::json!({"edgezero_kind": 9_u64}).to_string() + )); // A valid v1 envelope is NOT future. assert!(!value_is_future_format(&v1), "v1 envelope"); @@ -1721,10 +2540,12 @@ mod tests { assert!(!value_is_future_format(&bad_sha.to_string()), "v1 bad sha"); // A malformed v1 pointer MISSING its version is NOT future (repairable). assert!(!value_is_future_format( - r#"{"edgezero_kind":"fastly_config_chunks","chunks":[]}"# + &serde_json::json!({"edgezero_kind": "fastly_config_chunks", "chunks": []}).to_string() )); // A plain foreign value is not future. - assert!(!value_is_future_format(r#"{"unrelated":"json"}"#)); + assert!(!value_is_future_format( + &serde_json::json!({"unrelated": "json"}).to_string() + )); assert!(!value_is_future_format("not json")); } @@ -1769,9 +2590,17 @@ mod tests { // A metadata-consistent-looking pointer whose per-chunk length is far // over the writer's payload target (and whose envelope is too small to // have been chunked at all). - let bogus = format!( - r#"{{"edgezero_kind":"{POINTER_KIND}","version":1,"chunks":[{{"key":"{root}{CHUNK_KEY_INFIX}{sha}.0","len":99999,"sha256":"x"}},{{"key":"{root}{CHUNK_KEY_INFIX}{sha}.1","len":1,"sha256":"y"}}],"data_sha256":"","envelope_len":100000,"envelope_sha256":"{sha}"}}"# - ); + let bogus = serde_json::json!({ + "edgezero_kind": POINTER_KIND, + "version": 1_u32, + "chunks": [ + {"key": format!("{root}{CHUNK_KEY_INFIX}{sha}.0"), "len": 99_999_u32, "sha256": "x"}, + {"key": format!("{root}{CHUNK_KEY_INFIX}{sha}.1"), "len": 1_u32, "sha256": "y"}, + ], + "data_sha256": "", + "envelope_len": 100_000_u32, + "envelope_sha256": sha, + }).to_string(); // No fetch should even be attempted: fail on metadata alone. let mut fetched = false; let err = resolve_fastly_config_value(root, bogus, |_key| { @@ -1805,24 +2634,27 @@ mod tests { /// fetch fan-out. #[test] fn pointer_with_many_tiny_chunks_is_rejected() { - use std::fmt::Write as _; let root = "app_config"; let sha = "a".repeat(64); // 100 chunks of 100 bytes each = 10_000 bytes (> entry limit, dense). - let mut chunks = String::new(); - for idx in 0..100_u32 { - if idx > 0 { - chunks.push(','); - } - write!( - chunks, - r#"{{"key":"{root}{CHUNK_KEY_INFIX}{sha}.{idx}","len":100,"sha256":"x"}}"# - ) - .expect("write to String"); - } - let bogus = format!( - r#"{{"edgezero_kind":"{POINTER_KIND}","version":1,"chunks":[{chunks}],"data_sha256":"","envelope_len":10000,"envelope_sha256":"{sha}"}}"# - ); + let chunks: Vec<_> = (0..100_u32) + .map(|idx| { + serde_json::json!({ + "key": format!("{root}{CHUNK_KEY_INFIX}{sha}.{idx}"), + "len": 100_u32, + "sha256": "x", + }) + }) + .collect(); + let bogus = serde_json::json!({ + "edgezero_kind": POINTER_KIND, + "version": 1_u32, + "chunks": chunks, + "data_sha256": "", + "envelope_len": 10_000_u32, + "envelope_sha256": sha, + }) + .to_string(); let mut fetched = false; let err = resolve_fastly_config_value(root, bogus, |_key| { fetched = true; @@ -2217,7 +3049,7 @@ mod tests { #[test] fn empty_root_key_is_rejected() { assert!( - prepare_fastly_config_entries("", "{}").is_err(), + prepare_fastly_config_entries("", &serde_json::json!({}).to_string()).is_err(), "an empty key must be rejected on the direct path" ); let big = make_envelope_json(FASTLY_CONFIG_ENTRY_LIMIT.saturating_add(1)); @@ -2245,7 +3077,8 @@ mod tests { // A root over the limit is rejected even for a DIRECT (unchunked) value. let over_limit_root = "r".repeat(FASTLY_CONFIG_KEY_LIMIT.saturating_add(1)); assert!( - prepare_fastly_config_entries(&over_limit_root, "{}").is_err(), + prepare_fastly_config_entries(&over_limit_root, &serde_json::json!({}).to_string()) + .is_err(), "a root key over the limit must be rejected on the direct path too" ); @@ -2261,7 +3094,7 @@ mod tests { multibyte_root.len() > FASTLY_CONFIG_KEY_LIMIT, "fixture must be multi-byte" ); - prepare_fastly_config_entries(&multibyte_root, "{}") + prepare_fastly_config_entries(&multibyte_root, &serde_json::json!({}).to_string()) .expect("a key at the CHARACTER limit must be accepted regardless of byte length"); } @@ -2439,9 +3272,9 @@ mod tests { // it is our reserved field, so an unrecognised value in it is an error, // never an ordinary entry. for raw in [ - r#"{"edgezero_kind":"fastly_config_chunks_v2"}"#, - r#"{"edgezero_kind":null}"#, - r#"{"edgezero_kind":7}"#, + &serde_json::json!({"edgezero_kind": "fastly_config_chunks_v2"}).to_string(), + &serde_json::json!({"edgezero_kind": null}).to_string(), + &serde_json::json!({"edgezero_kind": 7_u64}).to_string(), ] { let err = resolve_fastly_config_value("k", raw.to_owned(), |_chunk_key| { Err("fetch must not be called".to_owned()) @@ -2689,7 +3522,7 @@ mod tests { // A value that ANNOUNCES our pointer kind but is missing every other // required field. It claims to be ours, so it must error rather than // pass through as an ordinary value. - let malformed = format!(r#"{{"edgezero_kind":"{POINTER_KIND}"}}"#); + let malformed = serde_json::json!({"edgezero_kind": POINTER_KIND}).to_string(); let err = resolve_fastly_config_value("my_key", malformed, |_| Ok(None)) .expect_err("malformed pointer must error"); assert!( @@ -2767,14 +3600,18 @@ mod tests { #[test] fn prior_chunk_keys_returns_empty_for_unrelated_json() { assert_eq!( - prior_chunk_keys("app_config", r#"{"hello":"world"}"#).unwrap(), + prior_chunk_keys( + "app_config", + &serde_json::json!({"hello": "world"}).to_string() + ) + .unwrap(), Vec::::new() ); } #[test] fn prior_chunk_keys_returns_empty_for_wrong_kind() { - let raw = r#"{"edgezero_kind":"other","version":1,"chunks":[],"data_sha256":"","envelope_len":0,"envelope_sha256":""}"#; + let raw = &serde_json::json!({"edgezero_kind": "other", "version": 1_u64, "chunks": [], "data_sha256": "", "envelope_len": 0_u64, "envelope_sha256": ""}).to_string(); assert_eq!( prior_chunk_keys("app_config", raw).unwrap(), Vec::::new() @@ -2890,7 +3727,8 @@ mod tests { // be silently dropped by a failed struct deserialize. #[test] fn prior_chunk_keys_warns_on_pointer_kind_with_missing_fields() { - let raw = r#"{"edgezero_kind":"fastly_config_chunks","version":2}"#; + let raw = &serde_json::json!({"edgezero_kind": "fastly_config_chunks", "version": 2_u64}) + .to_string(); let err = prior_chunk_keys("app_config", raw) .expect_err("pointer-kind but malformed must warn, not Ok([])"); assert!(!err.is_empty(), "{err}"); diff --git a/crates/edgezero-adapter-fastly/src/cli.rs b/crates/edgezero-adapter-fastly/src/cli.rs index 01efe05cc..6d54999c9 100644 --- a/crates/edgezero-adapter-fastly/src/cli.rs +++ b/crates/edgezero-adapter-fastly/src/cli.rs @@ -29,8 +29,9 @@ use edgezero_adapter::cli_support::{ find_manifest_upwards, find_workspace_root, path_distance, read_package_name, run_native_cli, }; use edgezero_adapter::registry::{ - Adapter, AdapterAction, AdapterDeployContext, AdapterPushContext, DeployOwnership, - DeployStoreIds, ProvisionStores, ReadConfigEntry, ResolvedStoreId, register_adapter, + Adapter, AdapterAction, AdapterDeployContext, AdapterExecutionTarget, AdapterPushContext, + DeployOwnership, DeployStoreIds, ProvisionStores, ReadConfigEntry, ResolvedStoreId, + register_adapter, }; use edgezero_adapter::release::{VerifiedApplicationRelease, verify_application_release}; use edgezero_adapter::scaffold::{ @@ -38,6 +39,7 @@ use edgezero_adapter::scaffold::{ ManifestSpec, ReadmeInfo, TemplateRegistration, register_adapter_blueprint, }; use edgezero_core::env_config::{EnvConfig, merge_env_defaults}; +use edgezero_core::{Capability, CapabilitySupport, PlatformMetadata}; use walkdir::WalkDir; static FASTLY_ADAPTER: FastlyCliAdapter = FastlyCliAdapter; @@ -2034,6 +2036,39 @@ fn owns_managed_deploy(context: &AdapterDeployContext) -> bool { reason = "see the explanatory block comment immediately above; fastly's no-op defaults for the three validate_* hooks are intentional and documented. `read_config_entry` and `read_config_entry_local` are both overridden below. `single_store_kinds` IS overridden below (returns `&[]`)." )] impl Adapter for FastlyCliAdapter { + fn capability(&self, capability: Capability) -> CapabilitySupport { + match capability { + Capability::IngressAdmission + | Capability::OutboundAuthorityOverride + | Capability::OutboundCacheBypass + | Capability::OutboundFailureDiagnostics + | Capability::OutboundHeaderFidelity + | Capability::OutboundRedirectFollowing => CapabilitySupport::Native, + Capability::BufferedResponseContentLength + | Capability::ConfigReadDeadlines + | Capability::InboundReadDeadlines + | Capability::IngressAdmissionAbort + | Capability::LazyStreamedResponsePassthrough + | Capability::OutboundBatchCancellation + | Capability::OutboundBatchCompletionOrder + | Capability::OutboundBatchSlotIsolation + | Capability::OutboundDeadlines + | Capability::OutboundFlexiblePhaseBudget + | Capability::OutboundHttp + | Capability::OutboundImageOptimizer + | Capability::ResponseEgressAbort + | Capability::ResponseEgressBackpressure + | Capability::ResponseEgressCompletion + | Capability::ResponseWriteDeadlines + | Capability::StreamedUploadDeadlines => CapabilitySupport::BestEffort, + Capability::ConfigReadAllocationBounds + | Capability::OutboundCompleteResourceAccounting + | Capability::RawIngressFramingValidation + | Capability::RawIngressHeadLimits + | _ => CapabilitySupport::Unsupported, + } + } + fn deploy(&self, context: &AdapterDeployContext, args: &[String]) -> Result<(), String> { if owns_managed_deploy(context) { deploy_managed_with_context(context, args) @@ -2063,7 +2098,9 @@ impl Adapter for FastlyCliAdapter { log::info!("[edgezero] Fastly build complete -> {}", artifact.display()); Ok(()) } - AdapterAction::Deploy => deploy(args), + AdapterAction::Deploy => { + Err("Fastly deployment requires a typed deployment context".to_owned()) + } AdapterAction::Serve => serve(args), AdapterAction::DeployStaging => Err( "Fastly staging requires typed deploy context and --application-release".to_owned(), @@ -2075,6 +2112,38 @@ impl Adapter for FastlyCliAdapter { } } + fn execute_target( + &self, + action: AdapterAction, + target: &AdapterExecutionTarget, + args: &[String], + ) -> Result<(), String> { + let manifest = target_manifest(target)?; + let manifest_dir = manifest + .parent() + .ok_or_else(|| "pinned fastly manifest has no parent directory".to_owned())?; + match action { + AdapterAction::Build => { + let artifact = build_from_manifest(&manifest, args)?; + log::info!("[edgezero] Fastly build complete -> {}", artifact.display()); + Ok(()) + } + AdapterAction::Deploy | AdapterAction::DeployStaging => { + Err("Fastly deployment requires a typed deployment context".to_owned()) + } + AdapterAction::Serve => serve_from_dir(manifest_dir, args), + AdapterAction::AuthLogin + | AdapterAction::AuthLogout + | AdapterAction::AuthStatus + | AdapterAction::EmitVersion + | AdapterAction::Healthcheck + | AdapterAction::Rollback + | _ => Err(format!( + "fastly adapter cannot execute operational action {action:?} against a pinned runtime target" + )), + } + } + fn finalize_deploy( &self, context: &AdapterDeployContext, @@ -2123,6 +2192,10 @@ impl Adapter for FastlyCliAdapter { "fastly" } + fn platform_metadata(&self) -> PlatformMetadata { + crate::FASTLY_PLATFORM + } + fn preflight_config_write(&self, key: &str, body: &str) -> Result<(), String> { // Reject an infeasible push here, BEFORE the CLI's remote read, so it // fails offline rather than after a list/describe. The write path @@ -5294,6 +5367,10 @@ fn no_matching_store_error(name: &str) -> String { pub fn build(extra_args: &[String]) -> Result { let manifest = find_fastly_manifest(env::current_dir().map_err(|err| err.to_string())?.as_path())?; + build_from_manifest(&manifest, extra_args) +} + +fn build_from_manifest(manifest: &Path, extra_args: &[String]) -> Result { let manifest_dir = manifest .parent() .ok_or_else(|| "fastly manifest has no parent directory".to_owned())?; @@ -5352,17 +5429,6 @@ fn build_compute_deploy_args(extra_args: &[String]) -> Vec { argv } -/// Legacy direct entry point for callers using [`AdapterAction::Deploy`]. -/// `EdgeZero`'s main deploy path uses the typed [`Adapter::deploy`] hook. -/// -/// # Errors -/// Returns an error when the Fastly CLI cannot deploy the package. -#[inline] -pub fn deploy(extra_args: &[String]) -> Result<(), String> { - let context = legacy_deploy_context(extra_args, false); - deploy_with_context(&context, extra_args) -} - fn deploy_with_context( context: &AdapterDeployContext, extra_args: &[String], @@ -5488,7 +5554,10 @@ pub fn serve(extra_args: &[String]) -> Result<(), String> { let manifest_dir = manifest .parent() .ok_or_else(|| "fastly manifest has no parent directory".to_owned())?; + serve_from_dir(manifest_dir, extra_args) +} +fn serve_from_dir(manifest_dir: &Path, extra_args: &[String]) -> Result<(), String> { let status = Command::new("fastly") .args(["compute", "serve"]) .args(extra_args) @@ -6418,6 +6487,19 @@ fn fastly_api_put(path: &str, token: &str) -> Result { } } +fn target_manifest(target: &AdapterExecutionTarget) -> Result { + let manifest = target + .platform_manifest() + .map_or_else(|| target.app_root().join("fastly.toml"), Path::to_path_buf); + if !manifest.is_file() { + return Err(format!( + "pinned fastly manifest {} is not a regular file", + manifest.display() + )); + } + Ok(manifest) +} + /// `PUT https://api.fastly.com` and return a non-empty response body. /// This is used for mutations, such as cloning a version, whose response /// identifies the newly-created provider object needed for recovery. @@ -6446,25 +6528,16 @@ fn fastly_api_put_capture(path: &str, token: &str) -> Result { Ok(body.to_owned()) } -fn legacy_deploy_context(args: &[String], staging: bool) -> AdapterDeployContext { - AdapterDeployContext { - adapter_manifest_path: arg_value(args, "--manifest-path").map(PathBuf::from), - application_manifest_path: None, - application_release_root: None, - service_id: arg_value(args, "--service-id").map(str::to_owned), - stores: DeployStoreIds::default(), - staging, - variable_defaults: BTreeMap::default(), - } -} - -/// Resolve the directory containing the Fastly manifest selected by the -/// application manifest. Fall back to discovery for direct adapter callers. +/// Resolve only within the already-selected application, never by cwd discovery. fn resolve_deploy_manifest_path(context: &AdapterDeployContext) -> Result { if let Some(path) = context.adapter_manifest_path.as_deref() { return Ok(path.to_path_buf()); } - find_fastly_manifest(env::current_dir().map_err(|err| err.to_string())?.as_path()) + let target = context + .execution_target + .as_ref() + .ok_or_else(|| "Fastly deployment requires a pinned execution target".to_owned())?; + target_manifest(target) } /// Production companion to `deploy`: resolve the active service version via the @@ -6772,27 +6845,179 @@ mod tests { use edgezero_core::env_config::EnvConfig; #[cfg(unix)] use edgezero_core::test_env::{EnvOverride, PathPrepend}; - #[cfg(unix)] - use std::collections::BTreeMap; - use std::collections::HashSet; - #[cfg(unix)] - use std::iter::once; + use std::collections::{BTreeMap, HashSet}; + use std::num::NonZeroU32; #[cfg(unix)] use std::os::unix::fs::PermissionsExt as _; + #[cfg(unix)] use std::sync::Mutex; use tempfile::tempdir; - // Shared fixture names. Pinning these as consts (instead of - // inline `"sessions"` / `"app_config"` per call site) keeps the - // setup-vs-assertion pair in sync -- a typo in one place no - // longer silently divorces from the other, because both reference - // the same const. Also names the intent: these are the LOGICAL - // store ids the fastly adapter operates on, not arbitrary strings. - const TEST_KV_ID: &str = "sessions"; + // Logical store ids shared by setup and assertions. const TEST_CONFIG_ID: &str = "app_config"; + const TEST_KV_ID: &str = "sessions"; const TEST_SECRET_ID: &str = "default"; + #[test] + fn adapter_capability_matrix_matches_contracts() { + adapter_capability_matrix_matches_outbound_spec(); + } + + #[test] + #[expect( + clippy::too_many_lines, + reason = "keep the complete capability data table together" + )] + fn adapter_capability_matrix_matches_outbound_spec() { + for (capability, support) in [ + ( + Capability::ConfigReadAllocationBounds, + CapabilitySupport::Unsupported, + ), + ( + Capability::ConfigReadDeadlines, + CapabilitySupport::BestEffort, + ), + ( + Capability::InboundReadDeadlines, + CapabilitySupport::BestEffort, + ), + (Capability::IngressAdmission, CapabilitySupport::Native), + ( + Capability::IngressAdmissionAbort, + CapabilitySupport::BestEffort, + ), + ( + Capability::RawIngressFramingValidation, + CapabilitySupport::Unsupported, + ), + ( + Capability::RawIngressHeadLimits, + CapabilitySupport::Unsupported, + ), + ( + Capability::ResponseEgressAbort, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseEgressBackpressure, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseEgressCompletion, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseWriteDeadlines, + CapabilitySupport::BestEffort, + ), + ( + Capability::BufferedResponseContentLength, + CapabilitySupport::BestEffort, + ), + (Capability::OutboundHttp, CapabilitySupport::BestEffort), + ( + Capability::OutboundRedirectFollowing, + CapabilitySupport::Native, + ), + ( + Capability::OutboundCompleteResourceAccounting, + CapabilitySupport::Unsupported, + ), + ( + Capability::OutboundHeaderFidelity, + CapabilitySupport::Native, + ), + (Capability::OutboundDeadlines, CapabilitySupport::BestEffort), + ( + Capability::OutboundFailureDiagnostics, + CapabilitySupport::Native, + ), + ( + Capability::OutboundFlexiblePhaseBudget, + CapabilitySupport::BestEffort, + ), + ( + Capability::OutboundAuthorityOverride, + CapabilitySupport::Native, + ), + ( + Capability::OutboundBatchCancellation, + CapabilitySupport::BestEffort, + ), + ( + Capability::OutboundBatchCompletionOrder, + CapabilitySupport::BestEffort, + ), + ( + Capability::OutboundBatchSlotIsolation, + CapabilitySupport::BestEffort, + ), + (Capability::OutboundCacheBypass, CapabilitySupport::Native), + ( + Capability::OutboundImageOptimizer, + CapabilitySupport::BestEffort, + ), + ( + Capability::StreamedUploadDeadlines, + CapabilitySupport::BestEffort, + ), + ( + Capability::LazyStreamedResponsePassthrough, + CapabilitySupport::BestEffort, + ), + ] { + assert_eq!( + FASTLY_ADAPTER.capability(capability), + support, + "{capability:?}" + ); + } + } + + #[test] + fn adapter_platform_metadata_matches_runtime_metadata() { + let metadata = FASTLY_ADAPTER.platform_metadata(); + let source = edgezero_core::PlatformResourceSource::PlatformLimit { + provider: "Fastly Compute", + }; + + assert_eq!(metadata, crate::FASTLY_PLATFORM); + assert_eq!( + metadata.memory_ceiling(), + edgezero_core::PlatformFact::known( + edgezero_core::MemoryCeiling::new( + 128_000_000, + edgezero_core::MemoryCeilingScope::PerExecution, + Some(1_000_000), + ), + source, + ) + ); + assert_eq!( + metadata.inbound_request_population_bound(), + edgezero_core::PlatformFact::known( + edgezero_core::InboundRequestPopulationBound::new(NonZeroU32::MIN), + source, + ) + ); + assert_eq!( + metadata + .inbound_request_population_bound() + .value() + .expect("Fastly publishes the per-execution population") + .max_live_requests(), + NonZeroU32::MIN + ); + assert_eq!( + metadata.host_ingress_memory_accounting(), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::ProviderUnpublished + ) + ); + } + // `PathPrepend` (RAII $PATH guard) is the shared helper imported above from // `edgezero_core::test_env`; the merge with edition-2024 main replaced our // local copy with it (its `set_var` calls are wrapped for 2024's unsafe-env). @@ -7305,6 +7530,7 @@ mod tests { adapter_manifest_path: Some(manifest), application_manifest_path: None, application_release_root: None, + execution_target: None, service_id: Some("SVC1".to_owned()), staging: true, stores: DeployStoreIds { @@ -7523,18 +7749,37 @@ mod tests { let marker = app.path().join("provider.log"); let fake = fake_provider_invocation_marker(&marker); let _path = PathPrepend::new(fake.path()); - let args = owned(&[ - "--manifest-path", - manifest.to_str().expect("utf8 manifest"), - "--service-id", - "SVC-1", - ]); - - let error = deploy(&args).expect_err("invalid service id must fail deploy"); + let context = AdapterDeployContext { + adapter_manifest_path: Some(manifest), + service_id: Some("SVC-1".to_owned()), + ..AdapterDeployContext::default() + }; + let error = FastlyCliAdapter + .deploy(&context, &[]) + .expect_err("invalid service id must fail deploy"); assert_service_id_error(&error); assert_provider_not_invoked(&marker); } + #[test] + fn deployment_requires_a_pinned_manifest_without_cwd_discovery() { + let app = tempdir().expect("selected app"); + let selected = app.path().canonicalize().expect("selected root"); + let context = AdapterDeployContext { + execution_target: Some(AdapterExecutionTarget::new(selected.clone(), None, None)), + ..AdapterDeployContext::default() + }; + let error = resolve_deploy_manifest_path(&context) + .expect_err("missing selected manifest must not discover another application"); + assert!(error.contains(&selected.display().to_string()), "{error}"); + resolve_deploy_manifest_path(&AdapterDeployContext::default()) + .expect_err("missing target must refuse discovery"); + let legacy_error = FastlyCliAdapter + .execute(AdapterAction::Deploy, &[]) + .expect_err("legacy execution must require typed context"); + assert!(legacy_error.contains("typed deployment context")); + } + #[cfg(unix)] #[test] fn active_version_rejects_invalid_service_id_before_provider_api() { @@ -7650,23 +7895,9 @@ mod tests { #[cfg(unix)] #[test] - fn adapter_deploy_discovers_and_rejects_invalid_service_id_before_provider_cli() { - const CHILD_ENV: &str = "EDGEZERO_FASTLY_DISCOVERY_TEST_CHILD"; - const MARKER_ENV: &str = "EDGEZERO_FASTLY_DISCOVERY_TEST_MARKER"; - - if env::var_os(CHILD_ENV).is_some() { - let marker = PathBuf::from(env::var_os(MARKER_ENV).expect("child marker path")); - let error = FastlyCliAdapter - .deploy(&AdapterDeployContext::default(), &[]) - .expect_err("discovered invalid service id must fail direct adapter deploy"); - assert_service_id_error(&error); - assert_provider_not_invoked(&marker); - return; - } - - let dir = tempdir().expect("tempdir"); - fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"app\"\n") - .expect("Cargo manifest"); + fn adapter_deploy_rejects_invalid_pinned_manifest_service_id_before_provider_cli() { + let _lock = path_mutation_guard().lock().expect("guard"); + let dir = tempdir().expect("selected app"); fs::write( dir.path().join("fastly.toml"), "name = \"app\"\nservice_id = \"SVC-1\"\n", @@ -7674,31 +7905,20 @@ mod tests { .expect("manifest"); let marker = dir.path().join("provider.log"); let fake = fake_provider_invocation_marker(&marker); - let path = env::join_paths( - once(fake.path().to_path_buf()) - .chain(env::split_paths(&env::var_os("PATH").unwrap_or_default())), - ) - .expect("test PATH"); - let output = Command::new(env::current_exe().expect("current test binary")) - .args([ - "--exact", - "cli::tests::adapter_deploy_discovers_and_rejects_invalid_service_id_before_provider_cli", - "--nocapture", - ]) - .current_dir(dir.path()) - .env(CHILD_ENV, "1") - .env(MARKER_ENV, &marker) - .env("PATH", path) - .env_remove(FASTLY_SERVICE_ID_ENV) - .output() - .expect("run isolated discovery regression"); - - assert!( - output.status.success(), - "isolated discovery regression failed:\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); + let _path = PathPrepend::new(fake.path()); + let _service_id = EnvOverride::remove(FASTLY_SERVICE_ID_ENV); + let context = AdapterDeployContext { + execution_target: Some(AdapterExecutionTarget::new( + dir.path().canonicalize().expect("selected root"), + None, + None, + )), + ..AdapterDeployContext::default() + }; + let error = FastlyCliAdapter + .deploy(&context, &[]) + .expect_err("invalid pinned service id must fail before provider work"); + assert_service_id_error(&error); assert_provider_not_invoked(&marker); } @@ -7832,11 +8052,11 @@ mod tests { #[test] fn parse_active_version_finds_active_entry() { - let json = r#"[ - {"number":1,"active":false,"locked":true,"staging":false,"deployed":true,"environments":[]}, - {"number":2,"active":true,"locked":true,"staging":false,"deployed":true,"environments":[]}, - {"number":3,"active":false,"locked":false,"staging":false,"deployed":false,"environments":[]} - ]"#; + let json = &serde_json::json!([ + {"number":1_u64,"active":false,"locked":true,"staging":false,"deployed":true,"environments":[]}, + {"number":2_u64,"active":true,"locked":true,"staging":false,"deployed":true,"environments":[]}, + {"number":3_u64,"active":false,"locked":false,"staging":false,"deployed":false,"environments":[]} + ]).to_string(); assert_eq!(resolve_active_version(json), Ok(Some(2))); } @@ -8423,7 +8643,7 @@ mod tests { fn parse_active_version_none_when_no_active() { // A parsed list with no active version is `Ok(None)` — confirmed // no active version (first deploy), NOT an operational failure. - let json = r#"[{"number":1,"active":false,"locked":false,"staging":false,"deployed":false,"environments":[]}]"#; + let json = &serde_json::json!([{"number":1_u64,"active":false,"locked":false,"staging":false,"deployed":false,"environments":[]}]).to_string(); assert_eq!(resolve_active_version(json), Ok(None)); } @@ -8432,7 +8652,7 @@ mod tests { // A truncated / non-array body is an operational failure, distinct from // "no active version" — the caller must fail closed, not record empty. resolve_active_version("not json").expect_err("non-JSON must be an operational error"); - resolve_active_version(r#"{"error":"unauthorized"}"#) + resolve_active_version(&serde_json::json!({"error": "unauthorized"}).to_string()) .expect_err("a non-array body must be an operational error"); } @@ -8441,38 +8661,48 @@ mod tests { // A garbled ACTIVE entry must fail closed, not read as "no active // version" — otherwise a production deploy proceeds with no rollback // target. Each of these is malformed and must be an operational error. - resolve_active_version(r#"[{"active":true}]"#) + resolve_active_version(&serde_json::json!([{"active": true}]).to_string()) .expect_err("active entry with no `number` must error"); - resolve_active_version(r#"[{"active":true,"number":"7"}]"#) + resolve_active_version(&serde_json::json!([{"active": true, "number": "7"}]).to_string()) .expect_err("active entry with a string `number` must error"); - resolve_active_version(r#"[{"active":"true","number":7}]"#) - .expect_err("a non-boolean `active` must error"); + resolve_active_version( + &serde_json::json!([{"active": "true", "number": 7_u64}]).to_string(), + ) + .expect_err("a non-boolean `active` must error"); // A non-boolean `active` ANYWHERE is schema drift — the whole list is // scanned, so it is caught even AFTER a valid active entry (a naive // first-match parser would miss this one). - resolve_active_version(r#"[{"active":"false"},{"active":true,"number":9}]"#) - .expect_err("a non-boolean `active` before the active entry is schema drift"); - resolve_active_version(r#"[{"active":true,"number":9},{"active":"nope"}]"#) - .expect_err("a non-boolean `active` AFTER the active entry is still schema drift"); + resolve_active_version( + &serde_json::json!([{"active": "false"}, {"active": true, "number": 9_u64}]) + .to_string(), + ) + .expect_err("a non-boolean `active` before the active entry is schema drift"); + resolve_active_version( + &serde_json::json!([{"active": true, "number": 9_u64}, {"active": "nope"}]).to_string(), + ) + .expect_err("a non-boolean `active` AFTER the active entry is still schema drift"); // More than one active version is ambiguous — refuse rather than pick one. - resolve_active_version(r#"[{"active":true,"number":9,"locked":true,"staging":false,"deployed":true,"environments":[]},{"active":true,"number":10,"locked":true,"staging":false,"deployed":true,"environments":[]}]"#) + resolve_active_version(&serde_json::json!([{"active":true,"number":9_u64,"locked":true,"staging":false,"deployed":true,"environments":[]},{"active":true,"number":10_u64,"locked":true,"staging":false,"deployed":true,"environments":[]}]).to_string()) .expect_err("two active versions must error as ambiguous"); // EVERY element must be a version object with a numeric `number` — a // garbled entry must fail closed, not be skipped as "not active". - resolve_active_version("[]").expect_err("an empty version list is an invalid response"); - resolve_active_version("[null]").expect_err("a null element must error"); - resolve_active_version("[{}]").expect_err("an entry with no `number` must error"); - resolve_active_version(r#"[{"number":"invalid"}]"#) + resolve_active_version(&serde_json::json!([]).to_string()) + .expect_err("an empty version list is an invalid response"); + resolve_active_version(&serde_json::json!([null]).to_string()) + .expect_err("a null element must error"); + resolve_active_version(&serde_json::json!([{}]).to_string()) + .expect_err("an entry with no `number` must error"); + resolve_active_version(&serde_json::json!([{"number": "invalid"}]).to_string()) .expect_err("a non-numeric `number` must error"); // Every safety field is mandatory; omission is schema drift. resolve_active_version( - r#"[{"number":42,"locked":false,"staging":false,"deployed":false,"environments":[]}]"#, + &serde_json::json!([{"number":42_u64,"locked":false,"staging":false,"deployed":false,"environments":[]}]).to_string(), ) .expect_err("an omitted active field must fail closed"); // Sanity: a well-formed list still resolves. assert_eq!( resolve_active_version( - r#"[{"active":false,"number":1,"locked":true,"staging":false,"deployed":true,"environments":[]},{"active":true,"number":2,"locked":true,"staging":false,"deployed":true,"environments":[]}]"# + &serde_json::json!([{"active":false,"number":1_u64,"locked":true,"staging":false,"deployed":true,"environments":[]},{"active":true,"number":2_u64,"locked":true,"staging":false,"deployed":true,"environments":[]}]).to_string() ), Ok(Some(2)) ); @@ -8545,8 +8775,8 @@ mod tests { #[test] fn active_version_or_require_enforces_require_active() { - let active = r#"[{"active":true,"number":5,"locked":true,"staging":false,"deployed":true,"environments":[]}]"#; - let none = r#"[{"active":false,"number":5,"locked":false,"staging":false,"deployed":false,"environments":[]}]"#; + let active = &serde_json::json!([{"active":true,"number":5_u64,"locked":true,"staging":false,"deployed":true,"environments":[]}]).to_string(); + let none = &serde_json::json!([{"active":false,"number":5_u64,"locked":false,"staging":false,"deployed":false,"environments":[]}]).to_string(); // A resolvable active version is returned regardless of the flag. assert_eq!(active_version_or_require(active, false, "svc"), Ok(Some(5))); @@ -8593,10 +8823,11 @@ mod tests { #[test] fn parse_staging_ip_selects_the_requested_domain() { - let json = r#"[ + let json = &serde_json::json!([ {"name":"other.example.com","staging_ip":"151.101.1.10"}, {"name":"example.com","staging_ip":"151.101.2.10"} - ]"#; + ]) + .to_string(); assert_eq!( parse_staging_ip(json, "example.com"), Ok("151.101.2.10".to_owned()) @@ -8606,11 +8837,11 @@ mod tests { #[test] fn parse_staging_ip_rejects_missing_duplicate_or_malformed_domain_records() { for json in [ - r#"[{"name":"other.example.com","staging_ip":"151.101.1.10"}]"#, - r#"[{"name":"example.com","staging_ip":null}]"#, - r#"[{"name":"example.com","staging_ips":["151.101.2.10"]}]"#, - r#"[{"name":"example.com","staging_ip":"151.101.2.10"},{"name":"example.com","staging_ip":"151.101.2.11"}]"#, - r#"{"name":"example.com","staging_ip":"151.101.2.10"}"#, + &serde_json::json!([{"name":"other.example.com","staging_ip":"151.101.1.10"}]).to_string(), + &serde_json::json!([{"name":"example.com","staging_ip":null}]).to_string(), + &serde_json::json!([{"name":"example.com","staging_ips":["151.101.2.10"]}]).to_string(), + &serde_json::json!([{"name":"example.com","staging_ip":"151.101.2.10"},{"name":"example.com","staging_ip":"151.101.2.11"}]).to_string(), + &serde_json::json!({"name":"example.com","staging_ip":"151.101.2.10"}).to_string(), ] { parse_staging_ip(json, "example.com") .expect_err("ambiguous or malformed domain inventory must fail closed"); @@ -9683,12 +9914,11 @@ build = \"cargo build --release\" #[test] fn find_config_store_id_matches_bare_array_by_name() { - let stdout = format!( - r#"[ - {{"id": "abc123", "name": "{TEST_CONFIG_ID}"}}, - {{"id": "def456", "name": "other_store"}} - ]"# - ); + let stdout = serde_json::json!([ + {"id": "abc123", "name": TEST_CONFIG_ID}, + {"id": "def456", "name": "other_store"}, + ]) + .to_string(); match find_config_store_id(&stdout, TEST_CONFIG_ID) { ConfigStoreLookup::Found(id) => assert_eq!(id, "abc123"), ConfigStoreLookup::NotFound => panic!("expected Found, got NotFound"), @@ -9700,11 +9930,8 @@ build = \"cargo build --release\" #[test] fn find_config_store_id_tolerates_items_envelope() { - let stdout = format!( - r#"{{"items": [ - {{"id": "xyz789", "name": "{TEST_CONFIG_ID}"}} - ]}}"# - ); + let stdout = + serde_json::json!({"items": [{"id": "xyz789", "name": TEST_CONFIG_ID}]}).to_string(); match find_config_store_id(&stdout, TEST_CONFIG_ID) { ConfigStoreLookup::Found(id) => assert_eq!(id, "xyz789"), ConfigStoreLookup::NotFound => panic!("expected Found, got NotFound"), @@ -9719,7 +9946,7 @@ build = \"cargo build --release\" // JSON parses cleanly, entries are well-formed // (`name` + `id` strings present), but no entry matches // → NotFound. Operator likely needs to run `provision`. - let stdout = r#"[{"id": "abc", "name": "other"}]"#; + let stdout = &serde_json::json!([{"id": "abc", "name": "other"}]).to_string(); assert!(matches!( find_config_store_id(stdout, "missing"), ConfigStoreLookup::NotFound @@ -9748,7 +9975,7 @@ build = \"cargo build --release\" fn find_config_store_id_flags_schema_drift_when_shape_unexpected() { // JSON parses but the top-level is neither a bare array // nor an `{items: [...]}` envelope. - let stdout = r#"{"namespace": "fastly", "list": []}"#; + let stdout = &serde_json::json!({"namespace": "fastly", "list": []}).to_string(); match find_config_store_id(stdout, "any") { ConfigStoreLookup::SchemaDrift(detail) => { assert!( @@ -9766,7 +9993,7 @@ build = \"cargo build --release\" // Array of objects but none have BOTH string `name` and // string `id` fields — suggests schema rename (e.g. // fastly renamed `name` → `title`). - let stdout = format!(r#"[{{"title": "{TEST_CONFIG_ID}", "uid": "abc"}}]"#); + let stdout = serde_json::json!([{"title": TEST_CONFIG_ID, "uid": "abc"}]).to_string(); let drift = find_config_store_id(&stdout, TEST_CONFIG_ID); assert!( matches!(drift, ConfigStoreLookup::SchemaDrift(_)), @@ -9781,9 +10008,11 @@ build = \"cargo build --release\" // definite NotFound would authorise an overwrite of a store that exists, // so a malformed row must be SchemaDrift (a hard error), not NotFound -- // even when another row is well-formed. - let stdout = format!( - r#"[{{"name": "", "id": "abc"}}, {{"name": "{TEST_CONFIG_ID}", "id": "store-1"}}]"# - ); + let stdout = serde_json::json!([ + {"name": "", "id": "abc"}, + {"name": TEST_CONFIG_ID, "id": "store-1"}, + ]) + .to_string(); let drift = find_config_store_id(&stdout, "some-other-store"); assert!( matches!(drift, ConfigStoreLookup::SchemaDrift(_)), @@ -9795,9 +10024,11 @@ build = \"cargo build --release\" fn find_config_store_id_rejects_duplicate_names() { // A duplicate name means we are not reading one consistent view of the // store, so resolving an id off it is ambiguous -> fail closed. - let stdout = format!( - r#"[{{"name": "{TEST_CONFIG_ID}", "id": "a"}}, {{"name": "{TEST_CONFIG_ID}", "id": "b"}}]"# - ); + let stdout = serde_json::json!([ + {"name": TEST_CONFIG_ID, "id": "a"}, + {"name": TEST_CONFIG_ID, "id": "b"}, + ]) + .to_string(); let drift = find_config_store_id(&stdout, TEST_CONFIG_ID); assert!( matches!(drift, ConfigStoreLookup::SchemaDrift(_)), @@ -9810,7 +10041,7 @@ build = \"cargo build --release\" // Empty array IS a valid "store doesn't exist yet" signal, // not schema drift — fastly CLI legitimately returns `[]` // when no config-stores exist. - let drift = find_config_store_id("[]", "any"); + let drift = find_config_store_id(&serde_json::json!([]).to_string(), "any"); assert!( matches!(drift, ConfigStoreLookup::NotFound), "empty array must be NotFound, got {drift:?}" @@ -9823,10 +10054,9 @@ build = \"cargo build --release\" // malformed entry could be the store we're looking for (hidden behind a // missing name/id). Deciding NotFound here would fail OPEN — staging // would then mirror no production overrides. Any malformed entry is drift. - let stdout = r#"[ - {"id": "abc123", "name": "some_other_store"}, - {"id": "def456"} - ]"#; + let stdout = + &serde_json::json!([{"id": "abc123", "name": "some_other_store"}, {"id": "def456"}]) + .to_string(); let drift = find_config_store_id(stdout, "app_config"); assert!( matches!(drift, ConfigStoreLookup::SchemaDrift(_)), @@ -9838,10 +10068,9 @@ build = \"cargo build --release\" fn find_config_store_id_scans_past_a_match() { // The full list is scanned: a malformed entry AFTER the match must still // be caught (no short-circuit on the first Found). - let stdout = r#"[ - {"id": "abc123", "name": "app_config"}, - {"name": "broken"} - ]"#; + let stdout = + &serde_json::json!([{"id": "abc123", "name": "app_config"}, {"name": "broken"}]) + .to_string(); let drift = find_config_store_id(stdout, "app_config"); assert!( matches!(drift, ConfigStoreLookup::SchemaDrift(_)), @@ -9851,10 +10080,7 @@ build = \"cargo build --release\" #[test] fn find_config_store_id_flags_duplicate_names_as_ambiguous() { - let stdout = r#"[ - {"id": "abc123", "name": "app_config"}, - {"id": "def456", "name": "app_config"} - ]"#; + let stdout = &serde_json::json!([{"id": "abc123", "name": "app_config"}, {"id": "def456", "name": "app_config"}]).to_string(); let drift = find_config_store_id(stdout, "app_config"); assert!( matches!(drift, ConfigStoreLookup::SchemaDrift(_)), @@ -10162,7 +10388,7 @@ build = \"cargo build --release\" let entry_list = dir.path().join("entries.json"); fs::write( &store_list, - format!(r#"[{{"name":"{TEST_CONFIG_ID}","id":"store-abc123"}}]"#), + serde_json::json!([{"name": TEST_CONFIG_ID, "id": "store-abc123"}]).to_string(), ) .expect("store list"); let entries = current @@ -10243,13 +10469,14 @@ exit 1 let list_file = dir.path().join("list_payload.txt"); let entry_list_file = dir.path().join("entry_list_payload.txt"); // Store-list JSON: bare array with one entry matching TEST_CONFIG_ID. - let list_json = format!(r#"[{{"name":"{TEST_CONFIG_ID}","id":"store-abc123"}}]"#); + let list_json = + serde_json::json!([{"name": TEST_CONFIG_ID, "id": "store-abc123"}]).to_string(); let entry_list_json = { - let items: Vec = entry_list_keys + let items: Vec<_> = entry_list_keys .iter() - .map(|key| format!(r#"{{"item_key":{}}}"#, serde_json::to_string(key).unwrap())) + .map(|key| serde_json::json!({"item_key": key})) .collect(); - format!("[{}]", items.join(",")) + serde_json::json!(items).to_string() }; fs::write(&stdout_file, stdout_body).expect("write stdout payload"); fs::write(&stderr_file, stderr_body).expect("write stderr payload"); @@ -10280,17 +10507,16 @@ exit 1 let script_path = dir.path().join("fastly"); let list_file = dir.path().join("list_payload.txt"); let entry_file = dir.path().join("entry_payload.txt"); - let list_json = format!(r#"[{{"name":"{TEST_CONFIG_ID}","id":"store-abc123"}}]"#); + let list_json = + serde_json::json!([{"name": TEST_CONFIG_ID, "id": "store-abc123"}]).to_string(); // item_value must be a valid BlobEnvelope JSON so the resolver accepts it. let envelope_json = serde_json::to_string(&BlobEnvelope::new( json!({"v": "logged"}), "2026-06-22T00:00:00Z".into(), )) .expect("serialize"); - let entry_json = format!( - r#"{{"item_value":{},"store_id":"store-abc123"}}"#, - serde_json::to_string(&envelope_json).expect("escape") - ); + let entry_json = + json!({"item_value": envelope_json, "store_id": "store-abc123"}).to_string(); fs::write(&list_file, list_json).expect("write list payload"); fs::write(&entry_file, &entry_json).expect("write entry payload"); let script = format!( @@ -10330,10 +10556,7 @@ exit 1 "2026-06-22T00:00:00Z".into(), )) .expect("serialize"); - let entry_json = format!( - r#"{{"item_value":{},"store_id":"store-abc123"}}"#, - serde_json::to_string(&envelope).expect("escape") - ); + let entry_json = json!({"item_value": envelope, "store_id": "store-abc123"}).to_string(); let fake = fake_fastly_returning(&entry_json, "", 0); let _path = PathPrepend::new(fake.path()); let result = FastlyCliAdapter @@ -10533,7 +10756,8 @@ exit 1 use std::os::unix::fs::PermissionsExt as _; let fake_dir = tempdir().expect("tempdir"); let list_file = fake_dir.path().join("list.json"); - let list_json = format!(r#"[{{"name":"{TEST_CONFIG_ID}","id":"store-abc123"}}]"#); + let list_json = + serde_json::json!([{"name": TEST_CONFIG_ID, "id": "store-abc123"}]).to_string(); fs::write(&list_file, list_json).expect("write list"); // The `config-store-entry list` response: a bare array of the keys present // in `key_responses`. Absence confirmation lists the store and checks @@ -10541,13 +10765,11 @@ exit 1 // `item_key` is needed (the keys-only listing is value-tolerant). let entry_list_file = fake_dir.path().join("entry_list.json"); let entries_json = { - let items: Vec = key_responses + let items: Vec<_> = key_responses .iter() - .map(|(key, _)| { - format!(r#"{{"item_key":{}}}"#, serde_json::to_string(key).unwrap()) - }) + .map(|(key, _)| serde_json::json!({"item_key": key})) .collect(); - format!("[{}]", items.join(",")) + serde_json::json!(items).to_string() }; fs::write(&entry_list_file, entries_json).expect("write entry list"); // Write each key response to a named file. @@ -10628,16 +10850,13 @@ echo 'unexpected' >&2; exit 1 let list_file = dir.path().join("list.json"); fs::write( &list_file, - format!(r#"[{{"name":"{TEST_CONFIG_ID}","id":"store-abc123"}}]"#), + serde_json::json!([{"name": TEST_CONFIG_ID, "id": "store-abc123"}]).to_string(), ) .expect("list"); let entries_file = dir.path().join("entries.json"); fs::write(&entries_file, entry_list_json(entry_list)).expect("entries"); for (index, value) in root_describe_seq.iter().enumerate() { - let wrapped = format!( - r#"{{"item_value":{}}}"#, - serde_json::to_string(value).expect("escape") - ); + let wrapped = serde_json::json!({"item_value": value}).to_string(); let nth = index.saturating_add(1); fs::write( dir.path().join(format!("resp_{root_key}_{nth}.json")), @@ -10813,7 +11032,7 @@ echo 'unexpected' >&2; exit 1 let dir = tempdir().expect("tempdir"); // The sentinel is an OBJECT KEY (not a value): the earlier redactor joined // keys into the diagnostic, so this is what pins the key-disclosure fix. - let drift = format!(r#"{{"{SENTINEL}":"x"}}"#); + let drift = serde_json::json!({(SENTINEL): "x"}).to_string(); let fake = fake_fastly_returning(&drift, "", 0); let _path = PathPrepend::new(fake.path()); @@ -11217,10 +11436,7 @@ echo 'unexpected' >&2; exit 1 let envelope = BlobEnvelope::new(json!({"hello": "world"}), "2026-06-22T00:00:00Z".into()); let json_str = serde_json::to_string(&envelope).unwrap(); - let item_json = format!( - r#"{{"item_value":{}}}"#, - serde_json::to_string(&json_str).unwrap() - ); + let item_json = json!({"item_value": json_str}).to_string(); let fake = fake_fastly_returning(&item_json, "", 0); let _path = PathPrepend::new(fake.path()); @@ -11253,14 +11469,11 @@ echo 'unexpected' >&2; exit 1 // Build a key→response map for every physical entry. let mut key_responses: Vec<(String, String)> = Vec::new(); for (pk, pv) in &physical { - let resp = format!(r#"{{"item_value":{}}}"#, serde_json::to_string(pv).unwrap()); + let resp = serde_json::json!({"item_value": pv}).to_string(); key_responses.push((pk.clone(), resp)); } // The root key should return the pointer. - let ptr_resp = format!( - r#"{{"item_value":{}}}"#, - serde_json::to_string(pointer_json).unwrap() - ); + let ptr_resp = serde_json::json!({"item_value": pointer_json}).to_string(); key_responses.push((TEST_CONFIG_ID.to_owned(), ptr_resp)); let fake = fake_fastly_with_key_dispatch(dir.path(), &key_responses); @@ -11297,10 +11510,7 @@ echo 'unexpected' >&2; exit 1 let (_, pointer_json) = physical.last().unwrap(); // Only provide the root pointer; omit chunk responses so the chunk fetch // gets a CLEAN not-found (`Error: item not found`, no operational marker). - let ptr_resp = format!( - r#"{{"item_value":{}}}"#, - serde_json::to_string(pointer_json).unwrap() - ); + let ptr_resp = serde_json::json!({"item_value": pointer_json}).to_string(); let key_responses = vec![(TEST_CONFIG_ID.to_owned(), ptr_resp)]; let fake = fake_fastly_with_key_dispatch(dir.path(), &key_responses); let _path = PathPrepend::new(fake.path()); @@ -11340,10 +11550,7 @@ echo 'unexpected' >&2; exit 1 // Corrupt first chunk's content. let (first_chunk_key, first_chunk_val) = &physical[0]; let corrupted: String = first_chunk_val.chars().map(|_| 'Z').collect(); - let corrupt_resp = format!( - r#"{{"item_value":{}}}"#, - serde_json::to_string(&corrupted).unwrap() - ); + let corrupt_resp = serde_json::json!({"item_value": corrupted}).to_string(); key_responses.push((first_chunk_key.clone(), corrupt_resp)); // Remaining chunks as normal. for (pk, pv) in physical @@ -11353,15 +11560,12 @@ echo 'unexpected' >&2; exit 1 { key_responses.push(( pk.clone(), - format!(r#"{{"item_value":{}}}"#, serde_json::to_string(pv).unwrap()), + serde_json::json!({"item_value": pv}).to_string(), )); } key_responses.push(( TEST_CONFIG_ID.to_owned(), - format!( - r#"{{"item_value":{}}}"#, - serde_json::to_string(pointer_json).unwrap() - ), + serde_json::json!({"item_value": pointer_json}).to_string(), )); let fake = fake_fastly_with_key_dispatch(dir.path(), &key_responses); let _path = PathPrepend::new(fake.path()); @@ -11392,11 +11596,10 @@ echo 'unexpected' >&2; exit 1 // `describe` SUCCEEDS (the entry exists), so a resolve failure is CORRUPT // stored state, not an IO error: the read must report `Corrupt` so a push // can overwrite it (in-band repair), NOT hard-error and block recovery. - let bad_json = r#"{"edgezero_kind":"fastly_config_chunks","some_field":"x"}"#; - let item_json = format!( - r#"{{"item_value":{}}}"#, - serde_json::to_string(bad_json).unwrap() - ); + let bad_json = + &serde_json::json!({"edgezero_kind": "fastly_config_chunks", "some_field": "x"}) + .to_string(); + let item_json = serde_json::json!({"item_value": bad_json}).to_string(); let fake = fake_fastly_returning(&item_json, "", 0); let _path = PathPrepend::new(fake.path()); @@ -11454,7 +11657,7 @@ echo 'unexpected' >&2; exit 1 // error: the read was incomplete, so a push must not overwrite. let infra = classify_resolved_read( Err(ResolveFailure::Corrupt("boom".to_owned())), - "{\"edgezero_kind\":\"fastly_config_chunks\"}", + &json!({"edgezero_kind": "fastly_config_chunks"}).to_string(), true, ); assert!( @@ -11468,7 +11671,7 @@ echo 'unexpected' >&2; exit 1 // never offered for overwrite. let unknown = classify_resolved_read( Err(ResolveFailure::FutureFormat("x".to_owned())), - r#"{"edgezero_kind":"fastly_config_chunks_v2"}"#, + &serde_json::json!({"edgezero_kind": "fastly_config_chunks_v2"}).to_string(), false, ); assert!( @@ -11487,7 +11690,7 @@ echo 'unexpected' >&2; exit 1 Err(ResolveFailure::FutureFormat( "newer inner envelope".to_owned(), )), - r#"{"edgezero_kind":"fastly_config_chunks","version":1,"chunks":[]}"#, + &serde_json::json!({"edgezero_kind": "fastly_config_chunks", "version": 1_u64, "chunks": []}).to_string(), false, ); assert!( @@ -11501,7 +11704,8 @@ echo 'unexpected' >&2; exit 1 assert!(matches!( classify_resolved_read( Err(ResolveFailure::Corrupt("bad chunk".to_owned())), - r#"{"edgezero_kind":"fastly_config_chunks","chunks":[]}"#, + &serde_json::json!({"edgezero_kind": "fastly_config_chunks", "chunks": []}) + .to_string(), false ), Ok(ReadConfigEntry::Corrupt(_)) @@ -11521,7 +11725,7 @@ echo 'unexpected' >&2; exit 1 // A future POINTER version (resolve fails on the version check) is a hard // error too -- the pointer kind is ours, but the version is newer. - let v2_ptr = r#"{"edgezero_kind":"fastly_config_chunks","version":2,"chunks":[]}"#; + let v2_ptr = &serde_json::json!({"edgezero_kind": "fastly_config_chunks", "version": 2_u64, "chunks": []}).to_string(); assert!( classify_resolved_read( Err(ResolveFailure::FutureFormat( @@ -11618,10 +11822,15 @@ echo 'unexpected' >&2; exit 1 // A pointer-KIND value that is invalid (missing the chunks it needs). // The resolver would error on this; the local read must NOT propagate // that as `Err`. - let broken_pointer = format!( - r#"{{"edgezero_kind":"{POINTER_KIND}","version":1,"chunks":[{{"key":"cfg{CHUNK_KEY_INFIX}{sha}.0","len":10,"sha256":"x"}}],"data_sha256":"","envelope_len":10,"envelope_sha256":"{sha}"}}"#, - sha = "a".repeat(64), - ); + let sha = "a".repeat(64); + let broken_pointer = serde_json::json!({ + "edgezero_kind": POINTER_KIND, + "version": 1_u32, + "chunks": [{"key": format!("cfg{CHUNK_KEY_INFIX}{sha}.0"), "len": 10_u32, "sha256": "x"}], + "data_sha256": "", + "envelope_len": 10_u32, + "envelope_sha256": sha, + }).to_string(); write_fastly_local_config_store( &fastly_toml, TEST_CONFIG_ID, @@ -12907,10 +13116,9 @@ echo 'unexpected' >&2; exit 1 let live = gen_envelope("live"); let mut listing = vec![listed_root(TEST_CONFIG_ID, &live, 999_999)]; listing.extend(listed_generation(TEST_CONFIG_ID, &live, 999_999)); - let enveloped = format!( - r#"{{"items":{},"next_cursor":"abc"}}"#, - entry_list_json(&listing) - ); + let items: serde_json::Value = + serde_json::from_str(&entry_list_json(&listing)).expect("entry list"); + let enveloped = serde_json::json!({"items": items, "next_cursor": "abc"}).to_string(); let fake = fake_fastly_gc_raw_list(TEST_CONFIG_ID, &enveloped, &oplog); let _path = PathPrepend::new(fake.path()); @@ -13096,13 +13304,7 @@ echo 'unexpected' >&2; exit 1 #[cfg(unix)] #[test] - fn deploy_honours_threaded_manifest_path_and_strips_it_from_the_fastly_argv() { - // Production deploys used to ignore the CLI-threaded - // `--manifest-path` and fall back to `find_fastly_manifest(cwd)`, - // which in a monorepo picks the CLOSEST fastly.toml — the wrong - // app. The threaded path must select the app directory, and must - // be STRIPPED from the argv (`fastly compute deploy` has no such - // flag and would exit non-zero). + fn deploy_uses_pinned_root_when_adapter_manifest_is_not_explicit() { let _lock = path_mutation_guard().lock().expect("guard"); let (fake, record) = fake_fastly_cwd_recorder(); let _path = PathPrepend::new(fake.path()); @@ -13111,13 +13313,18 @@ echo 'unexpected' >&2; exit 1 let manifest = app.path().join("fastly.toml"); fs::write(&manifest, "name = \"app\"\n").expect("write fastly.toml"); - let args = vec![ - "--manifest-path".to_owned(), - manifest.display().to_string(), - "--service-id".to_owned(), - "SVC1".to_owned(), - ]; - deploy(&args).expect("deploy must run against the threaded manifest"); + let context = AdapterDeployContext { + execution_target: Some(AdapterExecutionTarget::new( + app.path().canonicalize().expect("app root"), + None, + None, + )), + service_id: Some("SVC1".to_owned()), + ..AdapterDeployContext::default() + }; + FastlyCliAdapter + .deploy(&context, &[]) + .expect("deploy must run against the pinned root"); let recorded = fs::read_to_string(&record).expect("fastly was invoked"); let (cwd, recorded_argv) = recorded @@ -13224,7 +13431,7 @@ echo 'unexpected' >&2; exit 1 let dead = gen_envelope("dead"); // Envelope-shaped, no discriminator, VERSION 2 -> a newer direct envelope. - let future = r#"{"data":{"x":1},"sha256":"0000000000000000000000000000000000000000000000000000000000000000","generated_at":"2026-01-01T00:00:00Z","version":2}"#; + let future = &serde_json::json!({"data": {"x": 1_u64}, "sha256": "0000000000000000000000000000000000000000000000000000000000000000", "generated_at": "2026-01-01T00:00:00Z", "version": 2_u64}).to_string(); let mut listing = vec![( "app_config".to_owned(), stamp_secs_ago(172_800), @@ -13370,7 +13577,8 @@ echo 'unexpected' >&2; exit 1 let dead = gen_envelope("dead"); // Root value is pointer-kind but invalid. - let bad = r#"{"edgezero_kind":"fastly_config_chunks","version":2}"#.to_owned(); + let bad = serde_json::json!({"edgezero_kind": "fastly_config_chunks", "version": 2_u64}) + .to_string(); let mut listing = vec![(TEST_CONFIG_ID.to_owned(), stamp_secs_ago(3_600), bad)]; listing.extend(listed_generation(TEST_CONFIG_ID, &dead, 604_800)); @@ -13822,7 +14030,10 @@ echo 'unexpected' >&2; exit 1 .expect("a chunk key"); contents.insert( &victim, - toml_edit::value(r#"{"edgezero_kind":"fastly_config_chunks_v2","new":true}"#), + toml_edit::value( + serde_json::json!({"edgezero_kind": "fastly_config_chunks_v2", "new": true}) + .to_string(), + ), ); fs::write(&fastly_toml, doc.to_string()).expect("write"); @@ -14005,13 +14216,19 @@ echo 'unexpected' >&2; exit 1 let fastly_toml = dir.path().join("fastly.toml"); // Seed the root with a pointer-kind-but-invalid value AND a real // chunk-like key so "no deletes" is non-vacuous. - let seed = concat!( - "name = \"demo\"\n\n", - "[local_server.config_stores.app_config]\n", - "format = \"inline-toml\"\n\n", - "[local_server.config_stores.app_config.contents]\n", - "app_config = \"{\\\"edgezero_kind\\\":\\\"fastly_config_chunks\\\",\\\"version\\\":1}\"\n", - "\"app_config.__edgezero_chunks.deadbeef.0\" = \"seeded-chunk-payload\"\n", + let seed = format!( + concat!( + "name = \"demo\"\n\n", + "[local_server.config_stores.app_config]\n", + "format = \"inline-toml\"\n\n", + "[local_server.config_stores.app_config.contents]\n", + "app_config = {prior}\n", + "\"app_config.__edgezero_chunks.deadbeef.0\" = \"seeded-chunk-payload\"\n", + ), + prior = toml_edit::value( + serde_json::json!({"edgezero_kind": "fastly_config_chunks", "version": 1_u32}) + .to_string() + ), ); fs::write(&fastly_toml, seed).expect("seed"); @@ -14068,14 +14285,17 @@ echo 'unexpected' >&2; exit 1 let dir = tempdir().expect("tempdir"); let fastly_toml = dir.path().join("fastly.toml"); // The root now holds a v2 direct envelope from a newer writer. - let seed = concat!( - "name = \"demo\"\n\n", - "[local_server.config_stores.app_config]\n", - "format = \"inline-toml\"\n\n", - "[local_server.config_stores.app_config.contents]\n", - "app_config = \"{\\\"data\\\":{},\\\"sha256\\\":\\\"x\\\",\\\"generated_at\\\":\\\"t\\\",\\\"version\\\":2}\"\n", + let seed = format!( + concat!( + "name = \"demo\"\n\n", + "[local_server.config_stores.app_config]\n", + "format = \"inline-toml\"\n\n", + "[local_server.config_stores.app_config.contents]\n", + "app_config = {prior}\n", + ), + prior = toml_edit::value(serde_json::json!({"data": {}, "sha256": "x", "generated_at": "t", "version": 2_u32}).to_string()), ); - fs::write(&fastly_toml, seed).expect("seed"); + fs::write(&fastly_toml, &seed).expect("seed"); let direct = make_test_envelope(FASTLY_CONFIG_ENTRY_LIMIT); let err = FastlyCliAdapter @@ -14095,10 +14315,7 @@ echo 'unexpected' >&2; exit 1 ); // The v2 value must survive untouched. let after = fs::read_to_string(&fastly_toml).expect("read"); - assert!( - after.contains("\\\"version\\\":2"), - "the newer-format value must be left intact: {after}" - ); + assert_eq!(after, seed, "the newer-format file must be left intact"); } /// The locked downgrade guard must catch a future INNER envelope hidden behind @@ -14738,12 +14955,18 @@ echo 'unexpected' >&2; exit 1 let fastly_toml = dir.path().join("fastly.toml"); // A pointer-kind prior value missing its required fields — malformed, so // `prior_chunk_keys` returns Err (warn, delete nothing). - let seed = concat!( - "name = \"demo\"\n\n", - "[local_server.config_stores.app_config]\n", - "format = \"inline-toml\"\n\n", - "[local_server.config_stores.app_config.contents]\n", - "app_config = \"{\\\"edgezero_kind\\\":\\\"fastly_config_chunks\\\",\\\"version\\\":1}\"\n", + let seed = format!( + concat!( + "name = \"demo\"\n\n", + "[local_server.config_stores.app_config]\n", + "format = \"inline-toml\"\n\n", + "[local_server.config_stores.app_config.contents]\n", + "app_config = {prior}\n", + ), + prior = toml_edit::value( + serde_json::json!({"edgezero_kind": "fastly_config_chunks", "version": 1_u32}) + .to_string() + ), ); fs::write(&fastly_toml, seed).expect("seed"); @@ -14832,12 +15055,18 @@ echo 'unexpected' >&2; exit 1 use crate::chunked_config::FASTLY_CONFIG_ENTRY_LIMIT; let dir = tempdir().expect("tempdir"); let fastly_toml = dir.path().join("fastly.toml"); - let seed = concat!( - "name = \"demo\"\n\n", - "[local_server.config_stores.app_config]\n", - "format = \"inline-toml\"\n\n", - "[local_server.config_stores.app_config.contents]\n", - "app_config = \"{\\\"edgezero_kind\\\":\\\"fastly_config_chunks\\\",\\\"version\\\":1}\"\n", + let seed = format!( + concat!( + "name = \"demo\"\n\n", + "[local_server.config_stores.app_config]\n", + "format = \"inline-toml\"\n\n", + "[local_server.config_stores.app_config.contents]\n", + "app_config = {prior}\n", + ), + prior = toml_edit::value( + serde_json::json!({"edgezero_kind": "fastly_config_chunks", "version": 1_u32}) + .to_string() + ), ); fs::write(&fastly_toml, seed).expect("seed"); diff --git a/crates/edgezero-adapter-fastly/src/config_store.rs b/crates/edgezero-adapter-fastly/src/config_store.rs index bad34170b..3b31c78f3 100644 --- a/crates/edgezero-adapter-fastly/src/config_store.rs +++ b/crates/edgezero-adapter-fastly/src/config_store.rs @@ -1,14 +1,23 @@ -//! Fastly adapter config store: wraps `fastly::ConfigStore`. +//! Fastly config reads use one caller-sized host buffer, without SDK retries. use std::cell::Cell; #[cfg(test)] use std::collections::HashMap; -use crate::chunked_config::resolve_fastly_config_value_typed; +use crate::chunked_config::{ + BoundedResolveFailure, ResolveFailure, SyncHostCallError, exact_fastly_read, + resolve_fastly_config_value_typed, resolve_fastly_config_value_typed_bounded, + run_sync_host_call, +}; use async_trait::async_trait; -use edgezero_core::config_store::{ConfigStore, ConfigStoreError}; -use fastly::ConfigStore as FastlyConfigStoreInner; -use fastly::config_store::{LookupError, OpenError}; +use edgezero_core::config_store::{BoundedStoreRead, ConfigStore, ConfigStoreError}; +use edgezero_core::{Deadline, MonotonicClock}; +use fastly_shared::FastlyStatus; +use fastly_sys::fastly_config_store; + +// A physical entry is at most 8,000 Unicode scalars. This adapter also imposes +// a finite byte ceiling independently of changes to the provider's quota. +const MAX_ENTRY_BYTES: u64 = 32_000; /// Config store backed by a Fastly Config Store resource link. pub struct FastlyConfigStore { @@ -16,7 +25,7 @@ pub struct FastlyConfigStore { } enum FastlyConfigStoreBackend { - Fastly(FastlyConfigStoreInner), + Fastly(u32), #[cfg(test)] InMemory(HashMap), } @@ -29,16 +38,65 @@ impl FastlyConfigStore { } } + #[expect( + unsafe_code, + reason = "the pinned ABI receives a live initialized slice and cannot resize it" + )] + fn read_entry(&self, key: &str, limit: u64) -> Result, ConfigStoreError> { + let read_limit = limit.min(MAX_ENTRY_BYTES); + match &self.inner { + FastlyConfigStoreBackend::Fastly(handle) => { + read_host_buffer(read_limit, |buffer| { + let mut written = 0; + // SAFETY: host output is confined to this initialized slice; + // reported lengths are validated before indexing or truncation. + let status = unsafe { + fastly_config_store::get( + *handle, + key.as_ptr(), + key.len(), + buffer.as_mut_ptr(), + buffer.len(), + &raw mut written, + ) + }; + (status, written) + }) + } + #[cfg(test)] + FastlyConfigStoreBackend::InMemory(data) => match data.get(key) { + Some(value) + if u64::try_from(value.len()).map_or(true, |length| length > read_limit) => + { + Err(ConfigStoreError::ValueTooLarge) + } + value => Ok(value.cloned()), + }, + } + } + /// Open a Fastly Config Store by resource link name. /// - /// Returns an error if the configured store cannot be opened. - /// /// # Errors - /// Returns the underlying [`fastly::config_store::OpenError`] when the named store does not exist or cannot be opened. + /// Returns a redacted typed failure if the host cannot open the store. #[inline] - pub fn try_open(name: &str) -> Result { - FastlyConfigStoreInner::try_open(name).map(|inner| Self { - inner: FastlyConfigStoreBackend::Fastly(inner), + #[expect( + unsafe_code, + reason = "the pinned host ABI writes only the supplied handle out-parameter" + )] + pub fn try_open(name: &str) -> Result { + let mut handle = fastly_shared::INVALID_CONFIG_STORE_HANDLE; + // SAFETY: the UTF-8 name slice and initialized out-parameter remain live + // for this synchronous call. Config Store exposes no close operation. + let status = + unsafe { fastly_config_store::open(name.as_ptr(), name.len(), &raw mut handle) }; + if status != FastlyStatus::OK || handle == fastly_shared::INVALID_CONFIG_STORE_HANDLE { + return Err(ConfigStoreError::unavailable( + "config store could not be opened", + )); + } + Ok(Self { + inner: FastlyConfigStoreBackend::Fastly(handle), }) } } @@ -46,14 +104,8 @@ impl FastlyConfigStore { #[async_trait(?Send)] impl ConfigStore for FastlyConfigStore { #[inline] - async fn get(&self, key: &str) -> Result, ConfigStoreError> { - let root_value = match &self.inner { - FastlyConfigStoreBackend::Fastly(inner) => { - inner.try_get(key).map_err(|err| map_lookup_error(&err))? - } - #[cfg(test)] - FastlyConfigStoreBackend::InMemory(data) => data.get(key).cloned(), - }; + async fn get(&self, key: &str) -> Result, ConfigStoreError> { + let root_value = self.read_entry(key, MAX_ENTRY_BYTES)?; let Some(value) = root_value else { return Ok(None); }; @@ -90,20 +142,14 @@ impl ConfigStore for FastlyConfigStore { // here, because those ARE our reserved namespace. let transient = Cell::new(false); let outcome = resolve_fastly_config_value_typed(key, value, |chunk_key| { - let got = match &self.inner { - FastlyConfigStoreBackend::Fastly(inner) => { - inner.try_get(chunk_key).map_err(|err| { - if is_transient_lookup(&err) { - transient.set(true); - } - // The pointer-controlled chunk key is not echoed (the resolver - // adds a safe position locator); the SDK `err` carries no value. - format!("config store lookup failed: {err}") - })? - } - #[cfg(test)] - FastlyConfigStoreBackend::InMemory(data) => data.get(chunk_key).cloned(), - }; + let got = self + .read_entry(chunk_key, MAX_ENTRY_BYTES) + .map_err(|error| { + if !matches!(error, ConfigStoreError::InvalidKey { .. }) { + transient.set(true); + } + "config store chunk read failed".to_owned() + })?; if got.is_none() { // Referenced chunk absent at this POP: treat as propagation lag // (transient) rather than corruption. @@ -111,84 +157,151 @@ impl ConfigStore for FastlyConfigStore { } Ok(got) }); + outcome + .map(edgezero_core::ConfigValue::from) + .map(Some) + .map_err(|error| map_resolve_failure(key, transient.get(), error)) + } + + #[inline] + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + let root_limit = max_backend_bytes.min(max_value_bytes.max(8_000)); + let materialized_root = + run_sync_host_call(clock, deadline, || self.read_entry(key, root_limit)) + .map_err(map_sync_config_error)?; + let root_read = exact_fastly_read(materialized_root, max_backend_bytes) + .map_err(|_size_error| ConfigStoreError::ValueTooLarge)?; + let Some(root_value) = root_read.value else { + return Ok(BoundedStoreRead { + backend_bytes: root_read.backend_bytes, + value: None, + }); + }; + + let transient = Cell::new(false); + let outcome = resolve_fastly_config_value_typed_bounded( + key, + root_value, + root_read.backend_bytes, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + |chunk_key, remaining_backend_bytes| { + let chunk_value = run_sync_host_call(clock, deadline, || { + self.read_entry(chunk_key, remaining_backend_bytes) + }) + .map_err(map_sync_config_error)?; + if chunk_value.is_none() { + transient.set(true); + } + exact_fastly_read(chunk_value, remaining_backend_bytes) + .map_err(|_size_error| ConfigStoreError::ValueTooLarge) + }, + ); + + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } match outcome { - Ok(resolved) => Ok(Some(resolved)), - Err(err) if err.is_future_format() => { - // A NEWER format in OUR reserved namespace (an unknown - // `edgezero_kind`, or a future pointer/inner-envelope version): - // re-pushing the same config will not help; the deployed build - // must be UPGRADED. - log::warn!( - "Fastly config-store value for `{key}` uses a NEWER format than this build \ - understands: {}. Re-pushing the same config will not help -- redeploy this \ - service with an updated EdgeZero build.", - err.into_message() - ); - Err(ConfigStoreError::internal(anyhow::anyhow!( - "config store value uses a newer format than this build understands; redeploy \ - this service with an updated build (re-pushing will not help)" - ))) - } - Err(err) => { - let message = err.into_message(); - if transient.get() { - log::warn!( - "Fastly config-store chunk lookup for `{key}` was transiently \ - unavailable: {message}" - ); - Err(ConfigStoreError::unavailable( - "config store temporarily unavailable", - )) - } else { - log::warn!( - "Fastly config-store chunk resolution failed for `{key}`: {message}. \ - Re-run ` config push` to repair the store." - ); - Err(ConfigStoreError::internal(anyhow::anyhow!( - "config store entry is corrupt or incomplete; re-run config push to \ - repair: {message}" - ))) + Ok(read) => { + let value = read.value.map(Into::into); + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); } + Ok(BoundedStoreRead { + backend_bytes: read.backend_bytes, + value, + }) + } + Err(BoundedResolveFailure::Backend(error)) => Err(error), + Err(BoundedResolveFailure::DeadlineExceeded) => Err(ConfigStoreError::DeadlineExceeded), + Err(BoundedResolveFailure::Resolve(error)) => { + Err(map_resolve_failure(key, transient.get(), error)) } + Err(BoundedResolveFailure::ValueTooLarge) => Err(ConfigStoreError::ValueTooLarge), } } } -/// Is a CHUNK lookup failure environmental (retry) rather than corrupt config -/// (`config push` to repair)? Only a bad KEY names corrupt state a re-push -/// rewrites; everything else — an invalid store handle, lookup exhaustion, an -/// unclassified/future failure, or a value that outgrew the read buffer — is -/// transient, because re-pushing cannot fix a request-scoped condition. -fn is_transient_lookup(err: &LookupError) -> bool { - // `ValueTooLong` is TRANSIENT, not corruption: the SDK already retried with - // the reported buffer size, so a `ValueTooLong` reaching us means the value - // GREW between host calls (a concurrent write) — a race a retry resolves, not - // a re-push-me corruption. - !matches!(err, LookupError::KeyInvalid | LookupError::KeyTooLong) -} - -fn map_lookup_error(err: &LookupError) -> ConfigStoreError { - // `LookupError` is from the `fastly` crate; using a wildcard arm guards - // against new variants being added in upstream point releases without - // forcing us into a breaking match every bump. - #[expect( - clippy::wildcard_enum_match_arm, - reason = "external enum; new variants must remain unavailable→unavailable" - )] - match err { - LookupError::KeyInvalid | LookupError::KeyTooLong => { - ConfigStoreError::invalid_key("invalid config key") +fn read_host_buffer( + limit: u64, + read: impl FnOnce(&mut [u8]) -> (FastlyStatus, usize), +) -> Result, ConfigStoreError> { + let length = usize::try_from(limit).map_err(|_error| ConfigStoreError::ValueTooLarge)?; + let mut buffer = Vec::new(); + buffer + .try_reserve_exact(length) + .map_err(|_error| ConfigStoreError::ValueTooLarge)?; + buffer.resize(length, 0); + let (status, written) = read(&mut buffer); + match status { + FastlyStatus::OK if written <= buffer.len() => { + buffer.truncate(written); + String::from_utf8(buffer) + .map(Some) + .map_err(|_error| ConfigStoreError::unavailable("config value is not UTF-8")) } - _ => { - log::warn!("Fastly config store lookup failed: {err}"); - ConfigStoreError::unavailable("config store temporarily unavailable") + FastlyStatus::NONE => Ok(None), + FastlyStatus::BUFLEN => Err(ConfigStoreError::ValueTooLarge), + FastlyStatus::INVAL | FastlyStatus::UNSUPPORTED => { + Err(ConfigStoreError::invalid_key("invalid config key")) } + _ => Err(ConfigStoreError::unavailable("config store read failed")), + } +} + +fn map_sync_config_error(call_error: SyncHostCallError) -> ConfigStoreError { + match call_error { + SyncHostCallError::Backend(backend_error) => backend_error, + SyncHostCallError::DeadlineExceeded => ConfigStoreError::DeadlineExceeded, + } +} + +fn map_resolve_failure(key: &str, transient: bool, error: ResolveFailure) -> ConfigStoreError { + if error.is_future_format() { + log::warn!( + "Fastly config-store value for `{key}` uses a NEWER format than this build \ + understands: {}. Re-pushing the same config will not help -- redeploy this \ + service with an updated EdgeZero build.", + error.into_message() + ); + return ConfigStoreError::internal(anyhow::anyhow!( + "config store value uses a newer format than this build understands; redeploy \ + this service with an updated build (re-pushing will not help)" + )); + } + + let message = error.into_message(); + if transient { + log::warn!( + "Fastly config-store chunk lookup for `{key}` was transiently unavailable: {message}" + ); + ConfigStoreError::unavailable("config store temporarily unavailable") + } else { + log::warn!( + "Fastly config-store chunk resolution failed for `{key}`: {message}. \ + Re-run ` config push` to repair the store." + ); + ConfigStoreError::internal(anyhow::anyhow!( + "config store entry is corrupt or incomplete; re-run config push to repair: {message}" + )) } } #[cfg(test)] mod tests { use super::*; + use edgezero_core::{Deadline, MonotonicClock, MonotonicInstant}; + use std::sync::{Arc, Mutex}; + use std::time::Duration; edgezero_core::config_store_contract_tests!(fastly_config_store_contract, { FastlyConfigStore::from_entries([ @@ -198,31 +311,131 @@ mod tests { }); #[test] - fn key_invalid_maps_to_invalid_key_error() { - let err = map_lookup_error(&LookupError::KeyInvalid); - assert!(matches!(err, ConfigStoreError::InvalidKey { .. })); + fn bounded_chunked_read_counts_root_pointer_and_all_chunks() { + use crate::chunked_config::prepare_fastly_config_entries; + use edgezero_core::blob_envelope::BlobEnvelope; + use futures::executor::block_on; + use serde_json::json; + + let envelope = serde_json::to_string(&BlobEnvelope::new( + json!({ "pad": "x".repeat(9_000) }), + "2026-01-01T00:00:00Z".to_owned(), + )) + .expect("envelope"); + let entries = prepare_fastly_config_entries("app_config", &envelope).expect("entries"); + let maybe_backend_bytes = entries.iter().try_fold(0_u64, |total, (_, value)| { + total.checked_add(u64::try_from(value.len()).ok()?) + }); + let expected_backend_bytes = maybe_backend_bytes.expect("backend byte sum"); + let store = FastlyConfigStore::from_entries(entries); + let clock = MonotonicClock::default(); + + let read = block_on(store.get_bounded( + "app_config", + &clock, + Deadline::after(Duration::from_secs(1)), + expected_backend_bytes, + u64::try_from(envelope.len()).expect("envelope length"), + )) + .expect("exact aggregate cap must succeed"); + + assert_eq!(read.backend_bytes, expected_backend_bytes); + assert_eq!(read.value.as_deref(), Some(envelope.as_str())); } #[test] - fn key_too_long_maps_to_invalid_key_error() { - let err = map_lookup_error(&LookupError::KeyTooLong); - assert!(matches!(err, ConfigStoreError::InvalidKey { .. })); + fn bounded_config_read_uses_injected_clock() { + use futures::executor::block_on; + + let process_now = MonotonicInstant::now(); + let injected_now = process_now + .checked_sub(Duration::from_mins(1)) + .expect("injected instant"); + let clock = MonotonicClock::new(move || injected_now); + let deadline = Deadline::at_instant( + injected_now + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), + ); + let store = FastlyConfigStore::from_entries([("key".to_owned(), "value".to_owned())]); + let read = block_on(store.get_bounded("key", &clock, deadline, 5, 5)) + .expect("the application clock is still before its deadline"); + assert_eq!(read.value.as_deref(), Some("value")); + + let start = MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let now = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&now); + let advancing_clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let error = run_sync_host_call(&advancing_clock, Deadline::at_instant(terminal), || { + *now.lock().expect("clock lock") = terminal; + Err::<(), _>(ConfigStoreError::unavailable("provider failed at expiry")) + }) + .expect_err("equality expiry must beat a ready provider error"); + assert!(matches!(error, SyncHostCallError::DeadlineExceeded)); } - /// A CHUNK lookup that fails transiently (lookup exhaustion, an invalid - /// store handle, an unclassified error, or a value that outgrew the read - /// buffer) must keep Unavailable semantics — re-pushing cannot repair a - /// request-scoped condition. Only a bad KEY is corrupt state a re-push fixes. #[test] - fn transient_chunk_lookups_are_not_treated_as_corruption() { - assert!(is_transient_lookup(&LookupError::TooManyLookups)); - assert!(is_transient_lookup(&LookupError::ConfigStoreInvalid)); - assert!(is_transient_lookup(&LookupError::Other)); - // ValueTooLong is TRANSIENT: the SDK already retried at the reported size, - // so it reaching us means the value grew between host calls (a race). - assert!(is_transient_lookup(&LookupError::ValueTooLong)); - assert!(!is_transient_lookup(&LookupError::KeyInvalid)); - assert!(!is_transient_lookup(&LookupError::KeyTooLong)); + fn fixture_matches_physical_entry_byte_ceiling() { + let store = FastlyConfigStore::from_entries([("key".to_owned(), "x".repeat(32_001))]); + let error = store.read_entry("key", u64::MAX).expect_err("physical cap"); + assert!(matches!(error, ConfigStoreError::ValueTooLarge)); + } + + #[test] + fn host_buffer_accepts_exact_cap_without_copying_the_string() { + let value = read_host_buffer(4, |buffer| { + assert_eq!(buffer.len(), 4); + buffer.copy_from_slice(b"test"); + (FastlyStatus::OK, 4) + }) + .expect("exact cap"); + assert_eq!(value.as_deref(), Some("test")); + } + + #[test] + fn host_buffer_never_retries_or_allocates_the_reported_oversize_length() { + for limit in [0, 1, 7_000] { + let calls = Cell::new(0_u32); + let error = read_host_buffer(limit, |buffer| { + calls.set(calls.get() + 1); + assert_eq!(u64::try_from(buffer.len()).expect("buffer length"), limit); + (FastlyStatus::BUFLEN, usize::MAX) + }) + .expect_err("oversize"); + assert!(matches!(error, ConfigStoreError::ValueTooLarge)); + assert_eq!(calls.get(), 1); + } + } + + #[test] + fn host_buffer_status_and_malformed_results_are_typed_and_redacted() { + for status in [FastlyStatus::INVAL, FastlyStatus::UNSUPPORTED] { + let error = read_host_buffer(1, |_buffer| (status, 0)).expect_err("key failure"); + assert!(matches!(error, ConfigStoreError::InvalidKey { .. })); + } + for status in [ + FastlyStatus::ERROR, + FastlyStatus::BADF, + FastlyStatus::LIMITEXCEEDED, + FastlyStatus::OK, + ] { + let error = read_host_buffer(1, |_buffer| (status, 2)).expect_err("bad result"); + assert!(matches!(error, ConfigStoreError::Unavailable { .. })); + } + assert!( + read_host_buffer(0, |_buffer| (FastlyStatus::NONE, 0)) + .expect("missing") + .is_none() + ); + let error = read_host_buffer(1, |buffer| { + buffer.fill(255); + (FastlyStatus::OK, 1) + }) + .expect_err("invalid UTF-8"); + assert!(matches!(error, ConfigStoreError::Unavailable { .. })); } /// A referenced chunk that is ABSENT maps to Unavailable (HTTP 503), not @@ -270,7 +483,7 @@ mod tests { // would not exercise the corruption path at all. let store = FastlyConfigStore::from_entries([( "app_config".to_owned(), - r#"{"edgezero_kind":"fastly_config_chunks"}"#.to_owned(), + serde_json::json!({"edgezero_kind": "fastly_config_chunks"}).to_string(), )]); let err = block_on(store.get("app_config")) .expect_err("corrupt root must map to a ConfigStoreError"); @@ -296,7 +509,7 @@ mod tests { use futures::executor::block_on; let store = FastlyConfigStore::from_entries([( "app_config".to_owned(), - r#"{"edgezero_kind":"fastly_config_chunks","version":2,"chunks":[]}"#.to_owned(), + serde_json::json!({"edgezero_kind": "fastly_config_chunks", "version": 2_u64, "chunks": []}).to_string(), )]); let err = block_on(store.get("app_config")).expect_err("a future format must error"); assert!( diff --git a/crates/edgezero-adapter-fastly/src/delivery.rs b/crates/edgezero-adapter-fastly/src/delivery.rs new file mode 100644 index 000000000..39c7e3f1a --- /dev/null +++ b/crates/edgezero-adapter-fastly/src/delivery.rs @@ -0,0 +1,534 @@ +//! Owned request-delivery results for custom Fastly lifecycles. + +/// SDK-free fixtures for the same finalization/report path used by production custom helpers. +#[cfg(feature = "test-utils")] +pub mod testing { + use super::{ + DeliveryError, FastlyNoAttemptReason, FastlyRequestDelivery, FastlyResponseCommitState, + FastlyResponseDelivery, + }; + use edgezero_core::http::Response; + use edgezero_core::response_egress::{ResponseEgressEnvelope, ResponseEgressReport}; + + pub enum Dispatch { + Aborted, + Detached(ResponseEgressEnvelope), + NoAttempt(FastlyNoAttemptReason), + Routed(ResponseEgressEnvelope), + } + + /// Supplies dispatch and delivery outcomes; does not simulate SDK head conversion or transport. + #[inline] + pub fn deliver_with_hooks( + dispatch: Dispatch, + finalize: Finalize, + deliver: Deliver, + ) -> FastlyRequestDelivery + where + Finalize: FnOnce(&mut Response) -> State, + Deliver: FnOnce(ResponseEgressEnvelope) -> FastlyResponseDelivery, + { + let outcome = match dispatch { + Dispatch::Aborted => Ok(super::DispatchOutcome::Aborted), + Dispatch::Detached(envelope) => Ok(super::DispatchOutcome::Detached(envelope)), + Dispatch::NoAttempt(reason) => Err(super::FastlyNoAttempt::new( + reason, + anyhow::Error::msg(reason.message()), + )), + Dispatch::Routed(envelope) => Ok(super::DispatchOutcome::Routed(envelope)), + }; + super::finish_request(outcome, finalize, deliver) + } + + /// Constructs a synthetic terminal fixture. Production constructors remain adapter-owned. + #[inline] + pub const fn response_delivery( + commitment: FastlyResponseCommitState, + report: ResponseEgressReport, + result: Result<(), DeliveryError>, + ) -> FastlyResponseDelivery { + FastlyResponseDelivery::new(commitment, report, result) + } +} + +use std::fmt; + +#[cfg(any(test, feature = "fastly", feature = "test-utils"))] +use edgezero_core::http::Response; +use edgezero_core::key_value_store::KvStoreOpenFailure; +#[cfg(any(test, feature = "fastly", feature = "test-utils"))] +use edgezero_core::response_egress::ResponseEgressEnvelope; +use edgezero_core::response_egress::{ResponseEgressOutcome, ResponseEgressReport}; + +/// Category of the last delivery failure, without provider text. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum DeliveryError { + Conversion, + Deadline, + Source, + Transport, +} + +impl DeliveryError { + #[cfg(any(test, feature = "fastly"))] + pub(crate) const fn from_outcome(outcome: ResponseEgressOutcome) -> Self { + match outcome { + ResponseEgressOutcome::ConversionError => Self::Conversion, + ResponseEgressOutcome::DeadlineExceeded => Self::Deadline, + ResponseEgressOutcome::SourceError => Self::Source, + ResponseEgressOutcome::ClientDisconnected + | ResponseEgressOutcome::Completed + | ResponseEgressOutcome::HostHandoff + | ResponseEgressOutcome::RequestCancelled + | ResponseEgressOutcome::TransportError + | ResponseEgressOutcome::Unspecified + | _ => Self::Transport, + } + } + + /// Fixed, payload-free diagnostic for the last delivery failure. + #[must_use] + #[inline] + pub const fn message(self) -> &'static str { + match self { + Self::Conversion => "response conversion failed", + Self::Deadline => "response write deadline exceeded", + Self::Source => "response body source failed", + Self::Transport => "response transport failed", + } + } + + #[must_use] + #[inline] + pub const fn outcome(self) -> ResponseEgressOutcome { + match self { + Self::Conversion => ResponseEgressOutcome::ConversionError, + Self::Deadline => ResponseEgressOutcome::DeadlineExceeded, + Self::Source => ResponseEgressOutcome::SourceError, + Self::Transport => ResponseEgressOutcome::TransportError, + } + } +} + +#[cfg(any(test, feature = "fastly", feature = "test-utils"))] +pub(crate) enum DispatchOutcome { + Aborted, + Detached(ResponseEgressEnvelope), + Routed(ResponseEgressEnvelope), +} + +/// A pre-attempt failure with safe public detail and a privately retained standard-path error. +pub struct FastlyNoAttempt { + reason: FastlyNoAttemptReason, + #[cfg(any(test, feature = "fastly"))] + standard_error: anyhow::Error, +} + +impl fmt::Debug for FastlyNoAttempt { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FastlyNoAttempt") + .field("reason", &self.reason) + .finish_non_exhaustive() + } +} + +impl FastlyNoAttempt { + #[must_use] + #[inline] + pub const fn message(&self) -> &'static str { + self.reason.message() + } + + #[cfg(any(test, feature = "fastly", feature = "test-utils"))] + pub(crate) fn new(reason: FastlyNoAttemptReason, standard_error: anyhow::Error) -> Self { + // Own both inputs even when this build does not retain the source field. + let failure = (reason, standard_error); + Self { + reason: failure.0, + #[cfg(any(test, feature = "fastly"))] + standard_error: failure.1, + } + } + + #[must_use] + #[inline] + pub const fn reason(&self) -> FastlyNoAttemptReason { + self.reason + } +} + +/// A failure before any egress attempt, completion, observer or send. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum FastlyNoAttemptReason { + RequestHeadConversion(FastlyRequestHeadFailure), + RequiredStore(KvStoreOpenFailure), +} + +impl FastlyNoAttemptReason { + #[must_use] + #[inline] + pub const fn message(self) -> &'static str { + match self { + Self::RequestHeadConversion(_) => "request head conversion failed", + Self::RequiredStore(_) => "required store could not be opened", + } + } +} + +/// The custom send's disposition, retaining routed finalization state even on delivery failure. +#[derive(Debug)] +#[must_use = "inspect disposition and commitment before sending or scheduling post-delivery work"] +#[non_exhaustive] +pub enum FastlyRequestDelivery { + /// Application policy intentionally selected no response. Replacement is forbidden. + Aborted, + Detached(FastlyResponseDelivery), + /// No response attempt or completion occurred. A native response is permitted. + NoAttempt(FastlyNoAttempt), + Routed { + state: State, + delivery: FastlyResponseDelivery, + }, +} + +impl FastlyRequestDelivery { + #[must_use] + #[inline] + pub const fn delivery(&self) -> Option<&FastlyResponseDelivery> { + match self { + Self::Detached(delivery) | Self::Routed { delivery, .. } => Some(delivery), + Self::Aborted | Self::NoAttempt(_) => None, + } + } + + #[cfg(any(test, feature = "fastly"))] + pub(crate) fn into_standard_result(self) -> Result, anyhow::Error> { + match self { + Self::Aborted => Err(anyhow::Error::msg("ingress admission aborted")), + Self::NoAttempt(failure) => Err(failure.standard_error), + Self::Detached(delivery) => delivery.standard_result().map(|()| None), + Self::Routed { state, delivery } => delivery.standard_result().map(|()| Some(state)), + } + } + + /// Whether a caller may send its own native response after this result. + /// + /// An attempted response has already notified completion and any applicable observer. + /// A native replacement is outside `EdgeZero` accounting. Policy abort, unknown commitment + /// and committed delivery never permit replacement, including after body failure. + #[must_use] + #[inline] + pub const fn may_send_native_response(&self) -> bool { + match self { + Self::NoAttempt(_) => true, + Self::Aborted => false, + Self::Detached(delivery) | Self::Routed { delivery, .. } => { + matches!(delivery.commitment, FastlyResponseCommitState::NotCommitted) + } + } + } + + #[must_use] + #[inline] + pub const fn no_attempt_reason(&self) -> Option { + match self { + Self::NoAttempt(failure) => Some(failure.reason()), + Self::Aborted | Self::Detached(_) | Self::Routed { .. } => None, + } + } +} + +/// The observed, text-free request-head conversion boundary. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum FastlyRequestHeadFailure { + InvalidUri, + RequestBuilder, +} + +/// Commitment of the last selected response body, not proof of client delivery. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum FastlyResponseCommitState { + /// Fastly accepted the response head. + Committed, + /// The response head never reached the host send operation. + NotCommitted, + /// The host rejected the send; client-visible commitment cannot be established. + Unknown, +} + +/// One attempted response's last-body commitment and exact terminal completion report. +#[derive(Clone, Debug, Eq, PartialEq)] +#[must_use = "inspect the terminal report; successful fallback delivery is not application delivery"] +pub struct FastlyResponseDelivery { + commitment: FastlyResponseCommitState, + report: ResponseEgressReport, + result: Result<(), DeliveryError>, +} + +impl FastlyResponseDelivery { + #[must_use] + #[inline] + pub const fn commitment(&self) -> FastlyResponseCommitState { + self.commitment + } + + /// Last send/pump failure, independent of the fallback report's retained original cause. + #[must_use] + #[inline] + pub fn error(&self) -> Option { + self.result.err() + } + + #[cfg(any(test, feature = "fastly", feature = "test-utils"))] + pub(crate) const fn new( + commitment: FastlyResponseCommitState, + report: ResponseEgressReport, + result: Result<(), DeliveryError>, + ) -> Self { + Self { + commitment, + report, + result, + } + } + + #[must_use] + #[inline] + pub const fn report(&self) -> &ResponseEgressReport { + &self.report + } + + #[cfg(any(test, feature = "fastly"))] + fn standard_result(&self) -> Result<(), anyhow::Error> { + self.result + .map_err(|error| anyhow::Error::msg(error.message())) + } +} + +#[cfg(any(test, feature = "fastly", feature = "test-utils"))] +pub(crate) fn finish_request( + outcome: Result, + finalize: Finalize, + deliver: Deliver, +) -> FastlyRequestDelivery +where + Finalize: FnOnce(&mut Response) -> State, + Deliver: FnOnce(ResponseEgressEnvelope) -> FastlyResponseDelivery, +{ + match outcome { + Err(reason) => FastlyRequestDelivery::NoAttempt(reason), + Ok(DispatchOutcome::Aborted) => FastlyRequestDelivery::Aborted, + Ok(DispatchOutcome::Detached(envelope)) => { + FastlyRequestDelivery::Detached(deliver(envelope)) + } + Ok(DispatchOutcome::Routed(mut envelope)) => { + let state = finalize(envelope.response_mut()); + let delivery = deliver(envelope); + FastlyRequestDelivery::Routed { state, delivery } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use edgezero_core::http::{HeaderMap, StatusCode, Version}; + use edgezero_core::key_value_store::{KvError, KvStoreOpenFailure}; + use edgezero_core::response_egress::{ + ResponseEgressAttempt, ResponseEgressCompletion, ResponseEgressHead, + ResponseEgressObserverHandle, ResponseEgressOutcome, + }; + use edgezero_core::time::{MonotonicClock, MonotonicInstant}; + + fn report(outcome: ResponseEgressOutcome) -> edgezero_core::ResponseEgressReport { + let now = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let head = + ResponseEgressHead::new(StatusCode::OK, Version::HTTP_11, &headers, None, now, None); + let mut attempt = ResponseEgressAttempt::new( + &head, + now, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::default(), + MonotonicClock::new(move || now), + ); + assert!(attempt.begin_writing()); + assert!(attempt.terminate(outcome, now)); + attempt.into_terminal_report() + } + + fn no_attempt(reason: FastlyNoAttemptReason) -> FastlyNoAttempt { + let error = match reason { + FastlyNoAttemptReason::RequiredStore(open_reason) => { + anyhow::Error::new(KvError::OpenFailed { + reason: open_reason, + }) + } + FastlyNoAttemptReason::RequestHeadConversion(_) => { + anyhow::Error::msg("request head conversion failed") + } + }; + FastlyNoAttempt::new(reason, error) + } + + #[test] + fn fastly_delivery_report_prevents_unsafe_replacement() { + for commitment in [ + FastlyResponseCommitState::NotCommitted, + FastlyResponseCommitState::Unknown, + FastlyResponseCommitState::Committed, + ] { + let terminal = report(ResponseEgressOutcome::TransportError); + let delivery = FastlyResponseDelivery::new( + commitment, + terminal.clone(), + Err(DeliveryError::Transport), + ); + assert_eq!(delivery.commitment(), commitment); + assert_eq!(delivery.report(), &terminal); + let result = FastlyRequestDelivery::Routed { + state: 42_u8, + delivery, + }; + assert_eq!( + result.may_send_native_response(), + commitment == FastlyResponseCommitState::NotCommitted + ); + let FastlyRequestDelivery::Routed { + state, + delivery: returned_delivery, + } = result + else { + panic!("routed result lost its state"); + }; + assert_eq!(state, 42); + assert_eq!(returned_delivery.error(), Some(DeliveryError::Transport)); + } + for reason in [ + FastlyNoAttemptReason::RequestHeadConversion(FastlyRequestHeadFailure::InvalidUri), + FastlyNoAttemptReason::RequiredStore(KvStoreOpenFailure::MissingBinding), + ] { + let result = FastlyRequestDelivery::<()>::NoAttempt(no_attempt(reason)); + assert!(result.may_send_native_response()); + assert_eq!(result.no_attempt_reason(), Some(reason)); + } + assert!(!FastlyRequestDelivery::<()>::Aborted.may_send_native_response()); + } + + #[test] + fn standard_serving_policy_keeps_delivered_fallbacks_successful() { + let mut terminal = report(ResponseEgressOutcome::ConversionError); + terminal.body_kind = edgezero_core::ResponseEgressBodyKind::Fallback; + terminal.fallback_disposition = + Some(edgezero_core::ResponseEgressFallbackDisposition::Completed); + let delivery = + FastlyResponseDelivery::new(FastlyResponseCommitState::Committed, terminal, Ok(())); + assert_eq!( + FastlyRequestDelivery::Routed { + state: 7_u8, + delivery: delivery.clone() + } + .into_standard_result() + .unwrap(), + Some(7) + ); + assert_eq!( + FastlyRequestDelivery::<()>::Detached(delivery) + .into_standard_result() + .unwrap(), + None + ); + for error in [ + DeliveryError::Conversion, + DeliveryError::Deadline, + DeliveryError::Source, + DeliveryError::Transport, + ] { + let failed_delivery = FastlyResponseDelivery::new( + FastlyResponseCommitState::Committed, + report(error.outcome()), + Err(error), + ); + assert_eq!( + FastlyRequestDelivery::Routed { + state: 7_u8, + delivery: failed_delivery + } + .into_standard_result() + .unwrap_err() + .to_string(), + error.message() + ); + } + assert_eq!( + FastlyRequestDelivery::<()>::Aborted + .into_standard_result() + .unwrap_err() + .to_string(), + "ingress admission aborted" + ); + assert_eq!( + FastlyRequestDelivery::<()>::NoAttempt(no_attempt( + FastlyNoAttemptReason::RequestHeadConversion(FastlyRequestHeadFailure::InvalidUri) + )) + .into_standard_result() + .unwrap_err() + .to_string(), + "request head conversion failed" + ); + } + + #[test] + fn standard_no_attempt_errors_keep_original_text() { + let result = FastlyRequestDelivery::<()>::NoAttempt(no_attempt( + FastlyNoAttemptReason::RequiredStore(KvStoreOpenFailure::MissingBinding), + )); + let error = result.into_standard_result().unwrap_err(); + assert_eq!(error.to_string(), "kv store open failed: missing binding"); + assert!(matches!( + error.downcast_ref::(), + Some(KvError::OpenFailed { + reason: KvStoreOpenFailure::MissingBinding + }) + )); + let cause = + FastlyNoAttemptReason::RequestHeadConversion(FastlyRequestHeadFailure::RequestBuilder); + let head_result = FastlyRequestDelivery::<()>::NoAttempt(FastlyNoAttempt::new( + cause, + anyhow::Error::msg("private-token conversion text"), + )); + assert_eq!(head_result.no_attempt_reason(), Some(cause)); + assert!(!format!("{head_result:?}").contains("private-token")); + assert_eq!( + head_result.into_standard_result().unwrap_err().to_string(), + "private-token conversion text" + ); + } + + #[test] + fn abort_and_no_attempt_skip_finalization_and_delivery() { + for outcome in [ + Ok(DispatchOutcome::Aborted), + Err(no_attempt(FastlyNoAttemptReason::RequestHeadConversion( + FastlyRequestHeadFailure::InvalidUri, + ))), + Err(no_attempt(FastlyNoAttemptReason::RequiredStore( + KvStoreOpenFailure::MissingBinding, + ))), + ] { + let aborted = matches!(outcome, Ok(DispatchOutcome::Aborted)); + let result = finish_request::( + outcome, + |_| panic!("unexpected finalization"), + |_| panic!("unexpected delivery"), + ); + assert_eq!(result.may_send_native_response(), !aborted); + assert_eq!(matches!(result, FastlyRequestDelivery::Aborted), aborted); + } + } +} diff --git a/crates/edgezero-adapter-fastly/src/fastly_response_abi.rs b/crates/edgezero-adapter-fastly/src/fastly_response_abi.rs new file mode 100644 index 000000000..6397ef1cf --- /dev/null +++ b/crates/edgezero-adapter-fastly/src/fastly_response_abi.rs @@ -0,0 +1,120 @@ +#![allow( + unsafe_code, + reason = "the status-returning Fastly ABI avoids panic-on-error SDK response wrappers" +)] + +use fastly_shared::{ + BodyWriteEnd, FastlyStatus, FramingHeadersMode, INVALID_BODY_HANDLE, INVALID_RESPONSE_HANDLE, +}; +use fastly_sys::{BodyHandle, ResponseHandle, fastly_http_body, fastly_http_resp}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct FastlyAbiError; + +pub(super) fn abandon_body(body: BodyHandle) -> Result<(), FastlyAbiError> { + // SAFETY: `body` is an owned, live body handle and this call consumes it. + result(unsafe { fastly_http_body::abandon(body) }) +} + +pub(super) fn append_header( + response: ResponseHandle, + name: &[u8], + value: &[u8], +) -> Result<(), FastlyAbiError> { + // SAFETY: both byte slices remain live for the duration of the synchronous hostcall. + result(unsafe { + fastly_http_resp::header_append( + response, + name.as_ptr(), + name.len(), + value.as_ptr(), + value.len(), + ) + }) +} + +pub(super) fn close_body(body: BodyHandle) -> Result<(), FastlyAbiError> { + // SAFETY: `body` is an owned, live body handle and this call consumes it. + result(unsafe { fastly_http_body::close(body) }) +} + +pub(super) fn close_response(response: ResponseHandle) -> Result<(), FastlyAbiError> { + // SAFETY: `response` is an owned, live response handle and this call consumes it. + result(unsafe { fastly_http_resp::close(response) }) +} + +pub(super) fn new_body() -> Result { + let mut handle = INVALID_BODY_HANDLE; + // SAFETY: the output pointer is valid and points to initialized `u32` storage. + result(unsafe { fastly_http_body::new(&raw mut handle) })?; + Ok(handle) +} + +pub(super) fn new_response() -> Result { + let mut handle = INVALID_RESPONSE_HANDLE; + // SAFETY: the output pointer is valid and points to initialized `u32` storage. + result(unsafe { fastly_http_resp::new(&raw mut handle) })?; + Ok(handle) +} + +fn result(status: FastlyStatus) -> Result<(), FastlyAbiError> { + status.result().map_err(|_status| FastlyAbiError) +} + +pub(super) fn send_buffered( + response: ResponseHandle, + body: BodyHandle, +) -> Result<(), FastlyAbiError> { + // SAFETY: both handles are exclusively owned and consumed by this non-streaming send. + result(unsafe { fastly_http_resp::send_downstream(response, body, 0) }) +} + +pub(super) fn send_streaming( + response: ResponseHandle, + body: BodyHandle, +) -> Result<(), FastlyAbiError> { + // SAFETY: both handles are exclusively owned and are consumed by the send attempt. + result(unsafe { fastly_http_resp::send_downstream(response, body, 1) }) +} + +pub(super) fn set_manual_framing(response: ResponseHandle) -> Result<(), FastlyAbiError> { + // SAFETY: `response` is a live, exclusively owned response handle. + result(unsafe { + fastly_http_resp::framing_headers_mode_set( + response, + FramingHeadersMode::ManuallyFromHeaders, + ) + }) +} + +pub(super) fn set_status(response: ResponseHandle, status: u16) -> Result<(), FastlyAbiError> { + // SAFETY: `response` is a live, exclusively owned response handle. + result(unsafe { fastly_http_resp::status_set(response, status) }) +} + +pub(super) fn write_body(body: BodyHandle, bytes: &[u8]) -> Result { + let mut written = 0; + // SAFETY: `body` is live, `bytes` remains live during the synchronous hostcall, and the + // output pointer refers to initialized `usize` storage. + result(unsafe { + fastly_http_body::write( + body, + bytes.as_ptr(), + bytes.len(), + BodyWriteEnd::Back, + &raw mut written, + ) + })?; + Ok(written) +} + +#[cfg(test)] +mod tests { + use super::{FastlyAbiError, FastlyStatus, result}; + + #[test] + fn status_result_preserves_success_and_collapses_host_failure() { + assert_eq!(result(FastlyStatus::OK), Ok(())); + assert_eq!(result(FastlyStatus::ERROR), Err(FastlyAbiError)); + } +} diff --git a/crates/edgezero-adapter-fastly/src/key_value_store.rs b/crates/edgezero-adapter-fastly/src/key_value_store.rs index 111d18fda..b7551cb33 100644 --- a/crates/edgezero-adapter-fastly/src/key_value_store.rs +++ b/crates/edgezero-adapter-fastly/src/key_value_store.rs @@ -6,50 +6,81 @@ //! //! This module is only compiled when the `fastly` feature is enabled. -#[cfg(feature = "fastly")] use async_trait::async_trait; -#[cfg(feature = "fastly")] use bytes::Bytes; -#[cfg(feature = "fastly")] -use edgezero_core::key_value_store::{KvError, KvPage, KvStore}; -#[cfg(feature = "fastly")] +use edgezero_core::key_value_store::{KvError, KvPage, KvStore, KvStoreOpenFailure}; +use edgezero_core::logging::BOOT_LOG_TARGET; use fastly::kv_store::{KVStore, KVStoreError}; -#[cfg(feature = "fastly")] +use fastly_shared::FastlyStatus; +use std::collections::HashSet; +use std::sync::{Mutex, OnceLock, PoisonError}; use std::time::Duration; /// KV store backed by Fastly's KV Store API. /// -/// Wraps a `fastly::kv_store::KVStore` handle obtained via `KVStore::open(name)`. -#[cfg(feature = "fastly")] +/// Opens compiled declarations lazily per request, or wraps an explicitly opened handle. pub struct FastlyKvStore { - store: KVStore, + source: FastlyKvStoreSource, +} + +enum FastlyKvStoreSource { + Declared { + name: &'static str, + opened: OnceLock>, + }, + Opened(KVStore), } -#[cfg(feature = "fastly")] impl FastlyKvStore { + pub(crate) fn declared(name: &'static str) -> Self { + Self { + source: FastlyKvStoreSource::Declared { + name, + opened: OnceLock::new(), + }, + } + } + /// Open a Fastly KV Store by name. /// - /// Returns `KvError::Unavailable` if the store does not exist. - /// /// # Errors - /// Returns [`KvError::Internal`] if the named KV store cannot be opened. + /// Returns [`KvError::OpenFailed`] with a payload-free reason when opening fails. #[inline] pub fn open(name: &str) -> Result { - let store = KVStore::open(name) - .map_err(|err| KvError::Internal(anyhow::anyhow!("failed to open kv store: {err}")))? - .ok_or(KvError::Unavailable)?; - Ok(Self { store }) + let store = + open_result(KVStore::open(name)).map_err(|reason| KvError::OpenFailed { reason })?; + Ok(Self { + source: FastlyKvStoreSource::Opened(store), + }) + } + + fn with_store( + &self, + operation: impl FnOnce(&KVStore) -> Result, + ) -> Result { + match &self.source { + FastlyKvStoreSource::Opened(store) => operation(store), + FastlyKvStoreSource::Declared { name, opened } => use_declared_store( + opened, + || { + open_result(KVStore::open(name)) + .inspect_err(|reason| warn_declared_open_once(name, *reason)) + }, + operation, + ), + } } } -#[cfg(feature = "fastly")] #[async_trait(?Send)] impl KvStore for FastlyKvStore { #[inline] async fn delete(&self, key: &str) -> Result<(), KvError> { - self.store - .delete(key) - .map_err(|err| KvError::Internal(anyhow::anyhow!("delete failed: {err}"))) + self.with_store(|store| { + store + .delete(key) + .map_err(|err| KvError::Internal(anyhow::anyhow!("delete failed: {err}"))) + }) } #[inline] @@ -59,14 +90,14 @@ impl KvStore for FastlyKvStore { #[inline] async fn get_bytes(&self, key: &str) -> Result, KvError> { - match self.store.lookup(key) { + self.with_store(|store| match store.lookup(key) { Ok(mut response) => { let bytes = response.take_body_bytes(); Ok(Some(Bytes::from(bytes))) } Err(KVStoreError::ItemNotFound) => Ok(None), Err(err) => Err(KvError::Internal(anyhow::anyhow!("lookup failed: {err}"))), - } + }) } #[inline] @@ -79,31 +110,35 @@ impl KvStore for FastlyKvStore { let limit_u32 = u32::try_from(limit) .map_err(|_e| KvError::Validation("list limit exceeds u32".to_owned()))?; - let mut request = self.store.build_list().limit(limit_u32); + self.with_store(|store| { + let mut request = store.build_list().limit(limit_u32); - if !prefix.is_empty() { - request = request.prefix(prefix); - } - if let Some(token) = cursor.filter(|token| !token.is_empty()) { - request = request.cursor(token); - } + if !prefix.is_empty() { + request = request.prefix(prefix); + } + if let Some(token) = cursor.filter(|token| !token.is_empty()) { + request = request.cursor(token); + } - let page = request - .execute() - .map_err(|err| KvError::Internal(anyhow::anyhow!("list failed: {err}")))?; - let next_cursor = page.next_cursor().filter(|token| !token.is_empty()); + let page = request + .execute() + .map_err(|err| KvError::Internal(anyhow::anyhow!("list failed: {err}")))?; + let next_cursor = page.next_cursor().filter(|token| !token.is_empty()); - Ok(KvPage { - cursor: next_cursor, - keys: page.into_keys(), + Ok(KvPage { + cursor: next_cursor, + keys: page.into_keys(), + }) }) } #[inline] async fn put_bytes(&self, key: &str, value: Bytes) -> Result<(), KvError> { - self.store - .insert(key, value.as_ref()) - .map_err(|err| KvError::Internal(anyhow::anyhow!("insert failed: {err}"))) + self.with_store(|store| { + store + .insert(key, value.as_ref()) + .map_err(|err| KvError::Internal(anyhow::anyhow!("insert failed: {err}"))) + }) } #[inline] @@ -113,13 +148,225 @@ impl KvStore for FastlyKvStore { value: Bytes, ttl: Duration, ) -> Result<(), KvError> { - self.store - .build_insert() - .time_to_live(ttl) - .execute(key, value.as_ref()) - .map_err(|err| KvError::Internal(anyhow::anyhow!("insert with ttl failed: {err}"))) + self.with_store(|store| { + store + .build_insert() + .time_to_live(ttl) + .execute(key, value.as_ref()) + .map_err(|err| KvError::Internal(anyhow::anyhow!("insert with ttl failed: {err}"))) + }) } } -// TODO: integration tests require the Fastly compute environment. -// Test `FastlyKvStore` as part of the Fastly adapter E2E test suite. +fn open_result( + result: Result, KVStoreError>, +) -> Result { + result + .map_err(|error| match error { + KVStoreError::StoreNotFound(_) => KvStoreOpenFailure::MissingBinding, + KVStoreError::InvalidKey + | KVStoreError::InvalidStoreHandle + | KVStoreError::InvalidStoreOptions + | KVStoreError::ItemBadRequest + | KVStoreError::Unexpected( + FastlyStatus::INVAL + | FastlyStatus::BADF + | FastlyStatus::BUFLEN + | FastlyStatus::UNSUPPORTED + | FastlyStatus::BADALIGN, + ) => KvStoreOpenFailure::InvalidConfiguration, + KVStoreError::ItemNotFound + | KVStoreError::ItemPreconditionFailed + | KVStoreError::ItemPayloadTooLarge + | KVStoreError::TooManyRequests + | KVStoreError::Unexpected(_) + | _ => KvStoreOpenFailure::Unavailable, + })? + .ok_or(KvStoreOpenFailure::MissingBinding) +} + +fn mark_warning(warned: &Mutex>, name: &'static str) -> bool { + warned + .lock() + .unwrap_or_else(PoisonError::into_inner) + .insert(name) +} + +fn warn_declared_open_once(name: &'static str, reason: KvStoreOpenFailure) { + // Only compiled metadata IDs enter this set; never retain availability or provider payloads. + static WARNED: OnceLock>> = OnceLock::new(); + if mark_warning(WARNED.get_or_init(|| Mutex::new(HashSet::new())), name) { + log::warn!(target: BOOT_LOG_TARGET, "declared Fastly KV store '{name}' failed to open: {reason}"); + } +} + +fn use_declared_store( + opened: &OnceLock>, + open: impl FnOnce() -> Result, + operation: impl FnOnce(&Store) -> Result, +) -> Result { + let store = opened + .get_or_init(open) + .as_ref() + .map_err(|reason| KvError::OpenFailed { reason: *reason })?; + operation(store) +} + +#[cfg(test)] +mod tests { + use super::*; + use edgezero_core::EdgeError; + use edgezero_core::http::StatusCode; + use std::sync::atomic::{AtomicUsize, Ordering}; + + #[test] + fn open_failure_classification_drops_provider_payloads() { + assert_eq!(open_result(Ok(Some(7_u8))), Ok(7)); + assert_eq!( + open_result::<()>(Ok(None)), + Err(KvStoreOpenFailure::MissingBinding) + ); + for (error, expected) in [ + ( + KVStoreError::StoreNotFound("credential-bearing-store-token".to_owned()), + KvStoreOpenFailure::MissingBinding, + ), + ( + KVStoreError::InvalidKey, + KvStoreOpenFailure::InvalidConfiguration, + ), + ( + KVStoreError::InvalidStoreHandle, + KvStoreOpenFailure::InvalidConfiguration, + ), + ( + KVStoreError::InvalidStoreOptions, + KvStoreOpenFailure::InvalidConfiguration, + ), + ( + KVStoreError::ItemBadRequest, + KvStoreOpenFailure::InvalidConfiguration, + ), + ( + KVStoreError::Unexpected(FastlyStatus::INVAL), + KvStoreOpenFailure::InvalidConfiguration, + ), + ( + KVStoreError::Unexpected(FastlyStatus::BADF), + KvStoreOpenFailure::InvalidConfiguration, + ), + ( + KVStoreError::Unexpected(FastlyStatus::BUFLEN), + KvStoreOpenFailure::InvalidConfiguration, + ), + ( + KVStoreError::Unexpected(FastlyStatus::UNSUPPORTED), + KvStoreOpenFailure::InvalidConfiguration, + ), + ( + KVStoreError::Unexpected(FastlyStatus::BADALIGN), + KvStoreOpenFailure::InvalidConfiguration, + ), + ( + KVStoreError::TooManyRequests, + KvStoreOpenFailure::Unavailable, + ), + ( + KVStoreError::Unexpected(FastlyStatus::ERROR), + KvStoreOpenFailure::Unavailable, + ), + ( + KVStoreError::Unexpected(FastlyStatus::AGAIN), + KvStoreOpenFailure::Unavailable, + ), + ( + KVStoreError::Unexpected(FastlyStatus::LIMITEXCEEDED), + KvStoreOpenFailure::Unavailable, + ), + ( + KVStoreError::Unexpected(FastlyStatus { code: i32::MAX }), + KvStoreOpenFailure::Unavailable, + ), + ] { + let reason = open_result::<()>(Err(error)).expect_err("open failure"); + assert_eq!(reason, expected); + let mapped = KvError::OpenFailed { reason }; + assert!(!format!("{mapped:?} {mapped}").contains("credential-bearing-store-token")); + assert_eq!( + EdgeError::from(mapped).status(), + if reason == KvStoreOpenFailure::Unavailable { + StatusCode::SERVICE_UNAVAILABLE + } else { + StatusCode::INTERNAL_SERVER_ERROR + } + ); + } + } + + #[test] + fn declared_open_is_once_per_request_and_next_request_recovers() { + let opens = AtomicUsize::new(0); + let operations = AtomicUsize::new(0); + let first = OnceLock::new(); + assert_eq!(opens.load(Ordering::SeqCst), 0); + for _ in 0_u8..6 { + let result = use_declared_store( + &first, + || { + opens.fetch_add(1, Ordering::SeqCst); + Err::(KvStoreOpenFailure::Unavailable) + }, + |_| { + operations.fetch_add(1, Ordering::SeqCst); + Ok(()) + }, + ); + assert!(matches!( + result, + Err(KvError::OpenFailed { + reason: KvStoreOpenFailure::Unavailable + }) + )); + } + assert_eq!(opens.load(Ordering::SeqCst), 1); + assert_eq!(operations.load(Ordering::SeqCst), 0); + let next_request = OnceLock::new(); + for _ in 0_u8..6 { + assert_eq!( + use_declared_store( + &next_request, + || { + opens.fetch_add(1, Ordering::SeqCst); + Ok(42_u8) + }, + |store| { + operations.fetch_add(1, Ordering::SeqCst); + Ok(*store) + } + ) + .unwrap(), + 42 + ); + } + assert_eq!(opens.load(Ordering::SeqCst), 2); + assert_eq!(operations.load(Ordering::SeqCst), 6); + } + + #[test] + fn declared_warning_ids_are_never_evicted_or_availability_cached() { + const IDS: &str = concat!( + "s00 s01 s02 s03 s04 s05 s06 s07 s08 s09 s10 s11 s12 s13 s14 s15 s16 ", + "s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 s32 s33 ", + "s34 s35 s36 s37 s38 s39 s40 s41 s42 s43 s44 s45 s46 s47 s48 s49 s50 ", + "s51 s52 s53 s54 s55 s56 s57 s58 s59 s60 s61 s62 s63 s64 s65 s66 s67" + ); + let warned = Mutex::new(HashSet::new()); + for name in IDS.split_ascii_whitespace() { + assert!(mark_warning(&warned, name)); + } + for name in IDS.split_ascii_whitespace() { + assert!(!mark_warning(&warned, name)); + } + assert_eq!(warned.lock().unwrap().len(), 68); + } +} diff --git a/crates/edgezero-adapter-fastly/src/lib.rs b/crates/edgezero-adapter-fastly/src/lib.rs index ba3751931..791e2f00d 100644 --- a/crates/edgezero-adapter-fastly/src/lib.rs +++ b/crates/edgezero-adapter-fastly/src/lib.rs @@ -5,43 +5,76 @@ feature = "fastly", expect( clippy::pub_use, - reason = "re-export the SDK serving builder rather than duplicate its API" + reason = "reuse the platform serving limits and summary" ) )] -// Only compiled where it is actually used by the CLI push/GC path. Gating it -// keeps a `--no-default-features` build dead-code clean. -#[cfg(any(feature = "cli", feature = "fastly", test))] +// Compile storage helpers only for their runtime, CLI or colocated tests. +#[cfg(any( + feature = "fastly", + test, + all(feature = "cli", not(target_arch = "wasm32")) +))] pub(crate) mod chunked_config; -#[cfg(feature = "cli")] +#[cfg(all(feature = "cli", not(target_arch = "wasm32")))] pub mod cli; #[cfg(feature = "fastly")] pub mod config_store; pub mod context; +pub mod delivery; #[cfg(feature = "fastly")] pub mod key_value_store; pub mod lifecycle; -#[cfg(feature = "fastly")] +#[cfg(any(feature = "fastly", all(test, not(target_arch = "wasm32"))))] pub mod logger; -#[cfg(feature = "fastly")] -pub mod proxy; +#[cfg(any(test, feature = "test-utils", feature = "fastly"))] +pub mod outbound; #[cfg(feature = "fastly")] pub mod request; -#[cfg(feature = "fastly")] +#[cfg(any(test, feature = "fastly"))] pub mod response; #[cfg(feature = "fastly")] pub mod secret_store; #[cfg(any(feature = "fastly", test))] -use edgezero_core::app::App; -#[cfg(feature = "fastly")] -use edgezero_core::app::Hooks; +use edgezero_core::app::{App, Hooks}; +#[cfg(any(feature = "fastly", test))] +use edgezero_core::error::EdgeError; #[cfg(feature = "fastly")] -use edgezero_core::http::Extensions; +use edgezero_core::http::{Extensions, Response}; #[cfg(any(feature = "fastly", test))] use edgezero_core::manifest::ResolvedLoggingConfig; #[cfg(feature = "fastly")] +use edgezero_core::time::MonotonicClock; +#[cfg(feature = "fastly")] pub use fastly::http::serve::{Serve, ServeSummary}; +use std::num::NonZeroU32; +#[cfg(feature = "fastly")] +use std::sync::Once; + +/// Fastly Compute's published per-execution heap and stack limits. +pub const FASTLY_PLATFORM: edgezero_core::PlatformMetadata = edgezero_core::PlatformMetadata::new( + edgezero_core::PlatformFact::known( + edgezero_core::MemoryCeiling::new( + 128_000_000, + edgezero_core::MemoryCeilingScope::PerExecution, + Some(1_000_000), + ), + edgezero_core::PlatformResourceSource::PlatformLimit { + provider: "Fastly Compute", + }, + ), + edgezero_core::PlatformFact::known( + edgezero_core::InboundRequestPopulationBound::new(NonZeroU32::MIN), + edgezero_core::PlatformResourceSource::PlatformLimit { + provider: "Fastly Compute", + }, + ), + edgezero_core::PlatformFact::unknown(edgezero_core::PlatformUnknownReason::ProviderUnpublished), +); + +#[cfg(feature = "fastly")] +static FASTLY_ABI_INIT: Once = Once::new(); #[cfg(any(feature = "fastly", test))] #[derive(Debug, Clone)] @@ -67,29 +100,16 @@ impl From for FastlyLogging { } #[cfg(any(feature = "fastly", test))] -#[derive(Default)] -struct RetainedApp { - app: Option, +fn build_target_app() -> Result +where + A: Hooks, +{ + App::build::(FASTLY_PLATFORM) } -#[cfg(any(feature = "fastly", test))] -impl RetainedApp { - fn get_or_init( - &mut self, - logging: &FastlyLogging, - owns_logging: impl FnOnce() -> bool, - install_logger: impl FnOnce(&str, log::LevelFilter, bool) -> Result<(), E>, - build: impl FnOnce() -> App, - ) -> Result<&App, E> { - if self.app.is_none() && logging.use_fastly_logger && !owns_logging() { - install_logger( - logging.endpoint.as_deref().unwrap_or("stdout"), - logging.level, - logging.echo_stdout, - )?; - } - Ok(self.app.get_or_insert_with(build)) - } +#[cfg(feature = "fastly")] +fn init_fastly_abi() { + FASTLY_ABI_INIT.call_once(fastly::init); } /// # Errors @@ -126,71 +146,168 @@ pub fn init_logger( /// IDs through Fastly resource links. /// /// # Errors -/// Logger setup failures and unavailable required stores return errors. +/// Returns logger setup, admission-abort or terminal-delivery errors. Assembly failures are +/// delivered as typed HTTP error responses rather than returned as host-generic errors. #[cfg(feature = "fastly")] #[inline] -pub fn run_app(req: fastly::Request) -> Result +pub fn run_app() -> Result<(), fastly::Error> where A: Hooks, { - run_app_with_request_extensions::(req, |_req, _extensions| {}) + run_app_with_hooks::(|_req, _extensions| {}, |_response| ()).map(|_state| ()) } -/// Like [`run_app`], but runs `extend` against a scratch -/// [`Extensions`] populated from the raw -/// `fastly::Request` (TLS JA4, H2 fingerprint, client IP, …) before the request -/// is converted; the scratch values are merged into the core request's -/// extensions and are visible to middleware and the `State`/extractor layer. +/// Runs the manifest-wired app through Fastly's closed request/response lifecycle. +/// +/// `prepare` borrows the raw request and extension bag before conversion. `finalize` borrows only +/// routed responses before response-egress policy and framing. `EdgeZero` retains transmission +/// ownership; routed hook state is returned only after terminal delivery, while detached ingress +/// responses skip `finalize` and return `None`. /// /// # Errors -/// Logger setup failures and unavailable required stores return errors. +/// Returns logger setup, admission-abort or terminal-delivery errors. Assembly failures are +/// delivered as detached HTTP responses and return `None` after successful delivery. #[cfg(feature = "fastly")] #[inline] -pub fn run_app_with_request_extensions( - req: fastly::Request, - extend: F, -) -> Result +pub fn run_app_with_hooks( + prepare: Prepare, + finalize: Finalize, +) -> Result, fastly::Error> where A: Hooks, - F: FnOnce(&fastly::Request, &mut Extensions), + Prepare: FnOnce(&mut fastly::Request, &mut Extensions), + Finalize: FnOnce(&mut Response) -> State, { + init_fastly_abi(); let stores = A::stores(); let logging = FastlyLogging::from(A::logging_for("fastly")); if logging.use_fastly_logger && !A::owns_logging() { let endpoint = logging.endpoint.as_deref().unwrap_or("stdout"); init_logger(endpoint, logging.level, logging.echo_stdout)?; } - let app = A::build_app(); - request::dispatch_with_registries(&app, req, stores, extend) + let req = fastly::Request::from_client(); + let clock = MonotonicClock::default(); + let request_start = clock.now(); + let app = match build_target_app::() { + Ok(app) => app, + Err(error) => { + return request::send_assembly_error(req, error, request_start, clock) + .into_standard_result() + .map(|_state| None); + } + }; + request::send_request_with_registries_and_hooks(&app, stores, req, prepare, finalize) + .into_standard_result() +} + +/// Retain a successfully built app while the SDK serves sequential requests. +/// +/// Each callback resolves fresh logical store registries. Logging uses baked +/// settings, and the adapter owns response delivery without SDK resends. +#[cfg(feature = "fastly")] +#[must_use = "inspect the summary or handle its terminal error with into_result()"] +#[inline] +pub fn serve_app(serve: Serve) -> ServeSummary +where + A: Hooks, +{ + serve_app_with_hooks::(serve, |_request, _extensions| {}, |_response| ()) +} + +/// Retained serving with request-local preparation and routed response finalization. +/// +/// The hooks follow [`run_app_with_hooks`]. Preparation runs before admission; +/// finalization runs only for routed responses, before adapter-owned delivery. +/// Captures persist across callbacks and must not accidentally retain request resources. +#[cfg(feature = "fastly")] +#[must_use = "inspect the summary or handle its terminal error with into_result()"] +#[inline] +pub fn serve_app_with_hooks( + serve: Serve, + mut prepare: Prepare, + mut finalize: Finalize, +) -> ServeSummary +where + A: Hooks, + Prepare: FnMut(&mut fastly::Request, &mut Extensions), + Finalize: FnMut(&mut Response) -> State, +{ + init_fastly_abi(); + let stores = A::stores(); + let logging = FastlyLogging::from(A::logging_for("fastly")); + lifecycle::serve_custom(serve, move |req, retained: &mut lifecycle::Sandbox| { + retained.setup_once(|| { + if logging.use_fastly_logger && !A::owns_logging() { + init_logger( + logging.endpoint.as_deref().unwrap_or("stdout"), + logging.level, + logging.echo_stdout, + )?; + } + Ok::<(), fastly::Error>(()) + })?; + let clock = MonotonicClock::default(); + let request_start = clock.now(); + if let Err(error) = retained.initialize(build_target_app::) { + return request::send_assembly_error(req, error, request_start, clock) + .into_standard_result() + .map(|_state| ()); + } + let app = retained.state().ok_or_else(|| { + fastly::Error::msg("application initialization did not retain an app") + })?; + request::send_request_with_registries_and_hooks( + app, + stores, + req, + &mut prepare, + &mut finalize, + ) + .into_standard_result() + .map(|_state| ()) + }) } -/// Dispatch with a config store wired explicitly. Its name and default key are -/// provided by the caller rather than derived from manifest store metadata. -/// KV is not auto-injected on this path; chain `.with_kv(name)` on a -/// [`request::FastlyService`] builder if you need KV alongside the config store. +/// Dispatch with an explicitly wired config store. Its name comes from the caller, +/// and its default key is `"default"`, not a manifest logical ID. KV is not +/// automatically injected; use [`request::FastlyService`] for explicit bindings. /// /// # Errors -/// Returns an error if logger setup fails or the underlying handler returns an error. +/// Returns logger setup, required-store, admission-abort or terminal-delivery errors. +/// Assembly failures are delivered as typed HTTP error responses. #[cfg(feature = "fastly")] #[inline] pub fn run_app_with_config( logging: &FastlyLogging, - req: fastly::Request, config_store_name: Option<&str>, -) -> Result +) -> Result<(), fastly::Error> where A: Hooks, { + init_fastly_abi(); if logging.use_fastly_logger && !A::owns_logging() { let endpoint = logging.endpoint.as_deref().unwrap_or("stdout"); init_logger(endpoint, logging.level, logging.echo_stdout)?; } - let app = A::build_app(); + let req = fastly::Request::from_client(); + let clock = MonotonicClock::default(); + let request_start = clock.now(); + let app = match build_target_app::() { + Ok(app) => app, + Err(error) => { + return request::send_assembly_error(req, error, request_start, clock) + .into_standard_result() + .map(|_state| ()); + } + }; let mut service = request::FastlyService::new(&app); if let Some(name) = config_store_name { service = service.with_config(name); } - service.dispatch(req) + service + .send_request_with_hooks(req, |_request, _extensions| {}, |_response| ()) + .into_standard_result() + .map(|_state| ()) } #[cfg(test)] @@ -225,168 +342,58 @@ mod fastly_logging_tests { } #[cfg(test)] -mod retained_app_tests { - use super::*; +mod platform_tests { use edgezero_core::app::{App, Hooks}; + use edgezero_core::error::EdgeError; use edgezero_core::router::RouterService; - use std::cell::{Cell, RefCell}; - struct CountedApp; - thread_local! { static CONFIGURES: Cell = const { Cell::new(0) }; } + struct PlatformAwareConfiguration; #[expect( clippy::missing_trait_methods, - reason = "exercise default app construction" + reason = "test hook exercises only target metadata propagation" )] - impl Hooks for CountedApp { - fn configure(app: &mut App) { - CONFIGURES.with(|count| count.set(count.get().checked_add(1).unwrap())); - app.set_name("retained"); + impl Hooks for PlatformAwareConfiguration { + fn configure(app: &mut App) -> Result<(), EdgeError> { + if app.platform() == crate::FASTLY_PLATFORM { + Ok(()) + } else { + Err(EdgeError::service_unavailable("wrong application platform")) + } } - fn routes() -> RouterService { - RouterService::builder().build() - } - } - - fn configured_logging() -> FastlyLogging { - FastlyLogging { - echo_stdout: true, - endpoint: Some("fixture-logs".to_owned()), - level: log::LevelFilter::Debug, - use_fastly_logger: true, + fn routes() -> Result { + Ok(RouterService::builder().build()) } } #[test] - fn retained_app_initializes_logging_before_build_once() { - let mut retained = RetainedApp::default(); - let events = RefCell::new(Vec::new()); - CONFIGURES.with(|count| count.set(0)); - for _ in 0_usize..2 { - let app = retained - .get_or_init( - &configured_logging(), - || false, - |endpoint, level, echo| { - assert_eq!(endpoint, "fixture-logs"); - assert_eq!(level, log::LevelFilter::Debug); - assert!(echo); - events.borrow_mut().push("logger"); - Ok::<(), &'static str>(()) - }, - || { - events.borrow_mut().push("build"); - CountedApp::build_app() - }, - ) - .unwrap(); - assert_eq!(app.name(), "retained"); - } - assert_eq!(*events.borrow(), ["logger", "build"]); - CONFIGURES.with(|count| assert_eq!(count.get(), 1)); - } + fn application_configuration_receives_fastly_platform_metadata() { + let app = super::build_target_app::() + .expect("configured application"); - #[test] - fn retained_app_skips_disabled_logging_and_builds_once() { - let mut retained = RetainedApp::default(); - let builds = Cell::new(0_usize); - let logging = FastlyLogging::from(ResolvedLoggingConfig::default()); - for _ in 0_usize..2 { - retained - .get_or_init( - &logging, - || false, - |_, _, _| -> Result<(), &'static str> { panic!("logger is disabled") }, - || { - builds.set(builds.get().checked_add(1).unwrap()); - CountedApp::build_app() - }, - ) - .unwrap(); - } - assert_eq!(builds.get(), 1); + assert_eq!(app.platform(), crate::FASTLY_PLATFORM); } #[test] - fn retained_app_respects_owned_logging_and_fresh_owners() { - let builds = Cell::new(0_usize); - for _ in 0_usize..2 { - let mut retained = RetainedApp::default(); - retained - .get_or_init( - &configured_logging(), - || true, - |_, _, _| -> Result<(), &'static str> { panic!("caller owns logging") }, - || { - builds.set(builds.get().checked_add(1).unwrap()); - CountedApp::build_app() - }, - ) - .unwrap(); + fn application_route_failure_preserves_target_error() { + struct RouteFailure; + #[expect( + clippy::missing_trait_methods, + reason = "tests typed route failure at target assembly" + )] + impl Hooks for RouteFailure { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Err(EdgeError::bad_request("configuration must not run")) + } + fn routes() -> Result { + Err(EdgeError::service_unavailable("route state unavailable")) + } } - assert_eq!(builds.get(), 2); - } - - #[test] - fn retained_app_logger_error_prevents_construction() { - let mut retained = RetainedApp::default(); - let result = retained.get_or_init( - &configured_logging(), - || false, - |_, _, _| Err("logger failed"), - || panic!("must not build"), - ); - assert_eq!(result.err(), Some("logger failed")); - assert!(retained.app.is_none()); + let Err(error) = super::build_target_app::() else { + panic!("route failure must prevent target application assembly"); + }; + assert_eq!(error.status().as_u16(), 503); + assert_eq!(error.message(), "route state unavailable"); } } - -/// Serve requests with an app initialized once on the first callback. -/// -/// Opt in using an ordinary `main` and an explicitly bounded [`Serve`]. The -/// runtime may exit before any configured limit; every request must tolerate -/// fresh initialization. The standard response conversion buffers core streams. -/// Inspect the returned summary (whose request count includes failed attempts) -/// or call its `into_result()` method to propagate terminal callback errors. -#[cfg(feature = "fastly")] -#[must_use = "inspect the serving summary or call into_result() to handle terminal errors"] -#[inline] -pub fn serve_app(serve: Serve) -> ServeSummary -where - A: Hooks, -{ - serve_app_with_request_extensions::(serve, |_request, _extensions| {}) -} - -/// Serve a retained app with fresh request extensions and store registries. -/// -/// Store aliases and logging settings are baked into `A`. The app and logging -/// initialization are retained for the serving loop, while request extensions -/// and store handles are recreated for every callback. Any returned error -/// terminates the SDK loop; handler `EdgeError`s that render as responses do not. -/// Construction panics remain sandbox failures. -/// -/// The callback runs per request, but the closure and its captured state live -/// for the entire serving loop. Create request-local mutable data inside the -/// callback. For mutable native requests, manual streaming, response -/// finalization, or custom initialization, use [`lifecycle::serve_custom`]. -#[cfg(feature = "fastly")] -#[must_use = "inspect the serving summary or call into_result() to handle terminal errors"] -#[inline] -pub fn serve_app_with_request_extensions( - serve: Serve, - mut extend: F, -) -> ServeSummary -where - A: Hooks, - F: FnMut(&fastly::Request, &mut Extensions), -{ - let stores = A::stores(); - let logging = FastlyLogging::from(A::logging_for("fastly")); - let mut retained = RetainedApp::default(); - serve.run(move |req| -> Result { - let app = retained.get_or_init(&logging, A::owns_logging, init_logger, A::build_app)?; - request::dispatch_with_registries(app, req, stores, &mut extend) - }) -} diff --git a/crates/edgezero-adapter-fastly/src/lifecycle.rs b/crates/edgezero-adapter-fastly/src/lifecycle.rs index 11400e696..c5eba513e 100644 --- a/crates/edgezero-adapter-fastly/src/lifecycle.rs +++ b/crates/edgezero-adapter-fastly/src/lifecycle.rs @@ -3,6 +3,10 @@ //! Only application-owned state belongs here. Native request handles, bodies, //! metadata, extensions, and pending work must remain local to each callback. +#[cfg(any(feature = "fastly", test))] +use crate::outbound::scope; +#[cfg(any(feature = "fastly", test))] +use edgezero_core::OutboundScopeOwner; #[cfg(feature = "fastly")] use fastly::http::serve::{HandlerResult, Serve, ServeSummary}; @@ -17,6 +21,18 @@ pub struct Sandbox { state: Option, } +#[cfg(any(feature = "fastly", test))] +struct DeliveryResult(Result<(), E>); + +#[cfg(feature = "fastly")] +impl HandlerResult for DeliveryResult { + type Error = E; + + fn send(self) -> Result<(), E> { + self.0 + } +} + impl Default for Sandbox { #[inline] fn default() -> Self { @@ -98,61 +114,363 @@ impl Sandbox { F: FnOnce(Q, &mut Self) -> R, { self.requests = self.requests.saturating_add(1); - handler(request, self) + let scope = OutboundScopeOwner::callback(); + scope.with(|| handler(request, self)) + } +} + +#[cfg(any(feature = "fastly", test))] +pub(crate) fn with_callback(action: impl FnOnce() -> R) -> R { + if scope::capture().is_managed() { + action() + } else { + OutboundScopeOwner::callback().with(action) } } /// Serve custom callbacks with lazy retained state and the supplied SDK limits. /// /// The callback controls initialization, dispatch, finalization and streaming. -/// Its result goes directly to the SDK's sending boundary. A callback that sends -/// its own response should return `()` or `Ok(())`; after commitment, handle -/// failures locally rather than return an error that would send another response. +/// The callback owns sending and returns only a terminal `Result<(), E>`. The +/// SDK records that result without sending a synthetic error response. Errors +/// terminate serving, including failures after commitment, without a second send. +/// Callback-owned batch drivers are released at callback exit, even if a batch is retained. +/// Batches created while polling a response body have an independent delivery lifetime. /// /// SDK limits are upper bounds, not a guarantee of reuse. This function does not /// read configuration or enable reuse in existing entry points. #[cfg(feature = "fastly")] #[must_use = "inspect the serving summary or call into_result() to handle terminal errors"] #[inline] -pub fn serve_custom(serve: Serve, mut handler: F) -> ServeSummary +pub fn serve_custom(serve: Serve, mut handler: F) -> ServeSummary where - F: FnMut(fastly::Request, &mut Sandbox) -> R, - R: HandlerResult, + F: FnMut(fastly::Request, &mut Sandbox) -> Result<(), E>, { + crate::init_fastly_abi(); let mut sandbox = Sandbox::default(); serve.run_with_context( - |request, context: &mut Sandbox| context.handle(request, &mut handler), + |request, context: &mut Sandbox| DeliveryResult(context.handle(request, &mut handler)), &mut sandbox, ) } /// Handle one request with fresh state, without entering the SDK serving loop. /// -/// Takes a request already received by the caller and completes the callback's -/// `HandlerResult` exactly once. Use from an ordinary `fn main`: propagating a -/// returned error through `#[fastly::main]` could attempt a second error response. -/// Explicitly sending callbacks follow the same rules as [`serve_custom`]. +/// Takes a request already received by the caller. Sending remains owned by the +/// callback; this function never asks the SDK to render an error. Use an ordinary `main`. +/// Callback exit releases its batch drivers without waiting for retained handles to drop. /// /// # Errors -/// Returns the error from completing the callback's SDK result. +/// Returns the callback error unchanged without another response attempt. #[cfg(feature = "fastly")] #[inline] -pub fn run_custom(request: fastly::Request, handler: F) -> Result<(), R::Error> +pub fn run_custom(request: fastly::Request, handler: F) -> Result<(), E> where - R: HandlerResult, - F: FnOnce(fastly::Request, &mut Sandbox) -> R, + F: FnOnce(fastly::Request, &mut Sandbox) -> Result<(), E>, { - Sandbox::default().handle(request, handler).send() + crate::init_fastly_abi(); + Sandbox::default().handle(request, handler) } #[cfg(test)] mod tests { - use super::Sandbox; + use crate::outbound::testing; + use std::cell::{Cell, RefCell}; + use std::rc::Rc; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + use edgezero_core::{EdgeError, OutboundBatch, OutboundBatchDriverEvent}; + use futures::executor::block_on; + use futures_util::stream::poll_fn; + use std::task::Poll; + + use super::{DeliveryResult, Sandbox}; + #[cfg(feature = "fastly")] + use crate::outbound::native_time; + use crate::outbound::scope::bind_batch; // Neither the retained value nor an error payload needs framework traits. struct App(u64); struct Fallback(&'static str); + struct DriverOwner(Rc>); + + struct RequestData(Arc); + + impl AsRef<[u8]> for RequestData { + fn as_ref(&self) -> &[u8] { + b"buffered" + } + } + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for RequestData { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for DriverOwner { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + fn pending_batch(released: Rc>) -> OutboundBatch { + let owner = DriverOwner(released); + let source = poll_fn(move |_cx| { + let _keep_owner = &owner; + Poll::>::Pending + }); + let context = testing::batch_context(1); + let driver = bind_batch(context.scope(), source); + OutboundBatch::from_driver(context, driver, testing::owner()) + } + + #[test] + fn callback_scope_exit_releases_retained_exchange() { + let mut sandbox = Sandbox::<()>::default(); + for failed in [false, true] { + let released = Rc::new(Cell::new(0)); + let saved = Rc::new(RefCell::new(None)); + let result = sandbox.handle((), |(), _| { + *saved.borrow_mut() = Some(pending_batch(Rc::clone(&released))); + assert_eq!(released.get(), 0); + if failed { + Err("original callback error") + } else { + Ok(()) + } + }); + assert_eq!(result.is_err(), failed); + if failed { + assert_eq!(result, Err("original callback error")); + } + assert_eq!(released.get(), 1); + let batch = saved.borrow_mut().take().expect("retained batch"); + assert!(matches!( + block_on(batch.collect()).unwrap_err().error(), + EdgeError::Internal { .. } + )); + assert_eq!(released.get(), 1); + } + assert_eq!(sandbox.requests(), 2); + } + + #[cfg(panic = "unwind")] + #[test] + fn callback_unwind_releases_retained_exchange() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + + let released = Rc::new(Cell::new(0)); + let saved = Rc::new(RefCell::new(None)); + let mut sandbox = Sandbox::<()>::default(); + assert!( + catch_unwind(AssertUnwindSafe(|| { + sandbox.handle((), |(), _| { + *saved.borrow_mut() = Some(pending_batch(Rc::clone(&released))); + panic!("original callback panic"); + }); + })) + .is_err() + ); + assert_eq!(released.get(), 1); + let batch = saved.borrow_mut().take().expect("retained batch"); + assert!(matches!( + block_on(batch.collect()).unwrap_err().error(), + EdgeError::Internal { .. } + )); + assert_eq!(released.get(), 1); + } + + #[test] + fn callback_scope_exit_releases_unpolled_send_and_begin_before_cross_scope_polling() { + #[cfg(feature = "fastly")] + use crate::outbound::FastlyOutboundClient; + use bytes::Bytes; + #[cfg(feature = "fastly")] + use edgezero_core::HttpClient; + use edgezero_core::{ + Body, MonotonicClock, OutboundFuture, OutboundRequest, OutboundScopeOwner, OutboundTime, + }; + + let time = OutboundTime::new(MonotonicClock::default(), |_, _| { + panic!("closed unpolled roots must not register timers") + }); + #[cfg(feature = "fastly")] + let client = HttpClient::with_client(FastlyOutboundClient::with_time(time)); + #[cfg(not(feature = "fastly"))] + let client = testing::client(time); + let mut sandbox = Sandbox::<()>::default(); + for begun in [false, true] { + for failed in [false, true] { + let released = Arc::new(AtomicUsize::new(0)); + let reads = Rc::new(Cell::new(0_usize)); + let source_reads = Rc::clone(&reads); + let owner = RequestData(Arc::clone(&released)); + let payload = if begun { + Body::from(Bytes::from_owner(owner)) + } else { + Body::from_stream(poll_fn(move |_cx| { + let _owner = &owner; + source_reads.set(source_reads.get().saturating_add(1)); + Poll::Pending + })) + }; + let request = OutboundRequest::post("http://127.0.0.1:9/unpolled-root") + .unwrap() + .body(payload); + let mut retained = None; + let result = sandbox.handle((), |(), _| { + let future: OutboundFuture<()> = if begun { + let pending = client.begin(request); + Box::pin(async move { pending.await.map(drop) }) + } else { + let send = client.send(request); + Box::pin(async move { send.await.map(drop) }) + }; + retained = Some(future); + assert_eq!( + released.load(Ordering::SeqCst), + 0, + "valid invocation retains its data before callback exit" + ); + if failed { + Err("original callback error") + } else { + Ok(()) + } + }); + assert_eq!(result.is_err(), failed); + assert_eq!( + released.load(Ordering::SeqCst), + 1, + "callback exit revokes unpolled request data" + ); + assert_eq!(reads.get(), 0); + let later = OutboundScopeOwner::callback(); + let body = later.with(OutboundScopeOwner::response_body); + let error = body + .with(|| block_on(retained.take().unwrap())) + .unwrap_err(); + assert!( + matches!(error, EdgeError::Internal { .. }), + "original callback closure must stay typed Internal: {error:?}" + ); + assert_eq!(released.load(Ordering::SeqCst), 1); + assert_eq!(reads.get(), 0); + } + } + assert_eq!(sandbox.requests(), 4); + } + + #[cfg(feature = "fastly")] + #[test] + fn retained_client_resolves_fresh_callback_scope() { + use crate::outbound::FastlyOutboundClient; + use edgezero_core::{Deadline, OutboundBatchNext, OutboundRequest}; + use std::time::Duration; + + let client = edgezero_core::HttpClient::with_client(FastlyOutboundClient::new()); + let mut sandbox = Sandbox::<()>::default(); + let mut previous = None; + for _ in 0_u8..2 { + let batch = sandbox.handle((), |(), _| { + if let Some(stale) = previous.take() { + let error = block_on(OutboundBatch::collect(stale)).unwrap_err(); + assert!(matches!(error.error(), EdgeError::Internal { .. })); + } + let request = OutboundRequest::get("https://example.com") + .unwrap() + .stream_response(); + let mut batch = client.start_batch_until( + vec![ + request, + OutboundRequest::post("http://127.0.0.1:9/callback-scope") + .unwrap() + .body("pending"), + ], + Deadline::after(Duration::from_secs(1)), + ); + let OutboundBatchNext::Item(_) = block_on(batch.next()).unwrap() else { + panic!("fresh scope"); + }; + batch + }); + previous = Some(batch); + } + let failure = block_on(previous.unwrap().collect()).unwrap_err(); + assert!(matches!(failure.error(), EdgeError::Internal { .. })); + assert_eq!(sandbox.requests(), 2); + } + + #[cfg(feature = "fastly")] + #[test] + fn callback_scope_exit_releases_retained_batch_driver() { + use crate::outbound::FastlyOutboundClient; + use edgezero_core::OutboundRequest; + use edgezero_core::{Deadline, MonotonicClock, MonotonicInstant}; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::time::Duration; + + struct ClockOwner(Arc); + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for ClockOwner { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + let released = Arc::new(AtomicUsize::new(0)); + let owner = ClockOwner(Arc::clone(&released)); + let now = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || { + let _keep_owner = &owner; + now + }); + let mut sandbox = Sandbox::<()>::default(); + let batch = sandbox.handle((), move |(), _| { + let client = edgezero_core::HttpClient::with_client(FastlyOutboundClient::with_time( + native_time(clock), + )); + let request = OutboundRequest::post("http://127.0.0.1:9/callback-release") + .unwrap() + .body("pending"); + client.start_batch_until(vec![request], Deadline::after(Duration::from_secs(1))) + }); + + assert_eq!( + released.load(Ordering::SeqCst), + 0, + "the opaque batch context retains its clock metadata" + ); + let failure = block_on(batch.collect()).expect_err("closed callback scope"); + assert!(matches!(failure.error(), EdgeError::Internal { .. })); + assert_eq!(released.load(Ordering::SeqCst), 1, "release exactly once"); + } + + #[test] + fn owned_delivery_retains_terminal_error_without_synthetic_send() { + let result = DeliveryResult(Err::<(), _>("post-commit error")); + assert_eq!(result.0, Err("post-commit error")); + } + + #[cfg(feature = "fastly")] + #[test] + fn sdk_delivery_result_returns_terminal_error_without_synthetic_send() { + use fastly::http::serve::HandlerResult as _; + + assert_eq!( + DeliveryResult(Err::<(), _>("post-commit error")).send(), + Err("post-commit error") + ); + assert_eq!(DeliveryResult(Ok::<(), &str>(())).send(), Ok(())); + } + #[test] fn early_return_counts_request_without_setup_or_build() { let mut sandbox = Sandbox::::default(); @@ -201,6 +519,81 @@ mod tests { assert_eq!(fresh.initialization_attempts(), 0); } + #[test] + fn failed_app_assembly_retries_without_retaining_partial_state() { + use edgezero_core::EdgeError; + use edgezero_core::app::{App as AssembledApp, Hooks}; + use edgezero_core::router::RouterService; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + static BUILDS: AtomicUsize = AtomicUsize::new(0); + static CONFIGURES: AtomicUsize = AtomicUsize::new(0); + static DROPS: AtomicUsize = AtomicUsize::new(0); + struct StateProbe; + struct RetryHooks; + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for StateProbe { + fn drop(&mut self) { + DROPS.fetch_add(1, Ordering::SeqCst); + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "tests route and configure failure retention" + )] + impl Hooks for RetryHooks { + fn configure(app: &mut AssembledApp) -> Result<(), EdgeError> { + if app.state::>().is_none() { + return Err(EdgeError::service_unavailable("registered state missing")); + } + if CONFIGURES.fetch_add(1, Ordering::SeqCst) == 0 { + return Err(EdgeError::service_unavailable("configuration unavailable")); + } + Ok(()) + } + + fn routes() -> Result { + if BUILDS.fetch_add(1, Ordering::SeqCst) == 0 { + return Err(EdgeError::service_unavailable("routes unavailable")); + } + Ok(RouterService::builder() + .with_state(Arc::new(StateProbe)) + .build()) + } + } + + let mut sandbox = Sandbox::::default(); + for (attempt, message) in [(1, "routes unavailable"), (2, "configuration unavailable")] { + let error = sandbox + .initialize(|| AssembledApp::build::(crate::FASTLY_PLATFORM)) + .unwrap_err(); + assert_eq!(error.message(), message); + assert!(sandbox.state().is_none()); + assert_eq!(sandbox.initialization_attempts(), attempt); + } + assert_eq!(DROPS.load(Ordering::SeqCst), 1); + sandbox + .initialize(|| AssembledApp::build::(crate::FASTLY_PLATFORM)) + .unwrap(); + let retained = Arc::clone(sandbox.state().unwrap().state::>().unwrap()); + sandbox + .initialize::(|| panic!("successful app must not be rebuilt")) + .unwrap(); + assert_eq!(sandbox.initialization_attempts(), 3); + assert_eq!(BUILDS.load(Ordering::SeqCst), 3); + assert_eq!(CONFIGURES.load(Ordering::SeqCst), 2); + assert!(Arc::ptr_eq( + &retained, + sandbox.state().unwrap().state::>().unwrap() + )); + drop(retained); + drop(sandbox); + assert_eq!(DROPS.load(Ordering::SeqCst), 2); + } + #[test] fn failed_setup_can_retry_without_initializing_the_app() { let mut sandbox = Sandbox::::default(); diff --git a/crates/edgezero-adapter-fastly/src/logger.rs b/crates/edgezero-adapter-fastly/src/logger.rs index 1b040eafd..86d1a7ef2 100644 --- a/crates/edgezero-adapter-fastly/src/logger.rs +++ b/crates/edgezero-adapter-fastly/src/logger.rs @@ -1,3 +1,4 @@ +use edgezero_core::logging::{BOOT_LOG_LEVEL, BOOT_LOG_TARGET}; use log::LevelFilter; /// Errors that can occur when initialising the Fastly logger. @@ -13,29 +14,23 @@ pub enum InitLoggerError { SetLogger(#[from] log::SetLoggerError), } -/// Initialize logging (opinionated): formatted timestamps using `fern`, -/// chained to the Fastly logger. -/// -/// # Errors -/// Returns [`InitLoggerError::Build`] if the underlying logger builder -/// rejects its inputs (e.g. an empty endpoint), or -/// [`InitLoggerError::SetLogger`] if a global logger is already installed. -#[inline] -pub fn init_logger( +fn build_backend( endpoint: &str, level: LevelFilter, echo_stdout: bool, -) -> Result<(), InitLoggerError> { - let logger = log_fastly::Logger::builder() +) -> Result { + log_fastly::Logger::builder() .default_endpoint(endpoint) .echo_stdout(echo_stdout) - .max_level(level) + .max_level(level.max(BOOT_LOG_LEVEL)) .build() - .map_err(|err| InitLoggerError::Build(err.to_string()))?; + .map_err(|err| InitLoggerError::Build(err.to_string())) +} - // Format timestamps in RFC3339 with milliseconds using UTC to avoid TZ issues in WASM. - let dispatch = fern::Dispatch::new() +fn build_dispatch(logger: Box, level: LevelFilter) -> fern::Dispatch { + fern::Dispatch::new() .level(level) + .level_for(BOOT_LOG_TARGET, BOOT_LOG_LEVEL) .format(|out, message, record| { out.finish(format_args!( "{} {} {}", @@ -44,12 +39,122 @@ pub fn init_logger( message )); }) - .chain({ - let boxed: Box = Box::new(logger); - boxed - }); + .chain(logger) +} - dispatch.apply()?; - log::set_max_level(level); +/// Initialize logging (opinionated): formatted timestamps using `fern`, +/// chained to the Fastly logger. Boot warnings are retained independently of +/// the ordinary runtime level, including `Off`. +/// +/// # Errors +/// Returns [`InitLoggerError::Build`] if the underlying logger builder +/// rejects its inputs (e.g. an empty endpoint), or +/// [`InitLoggerError::SetLogger`] if a global logger is already installed. +#[inline] +pub fn init_logger( + endpoint: &str, + level: LevelFilter, + echo_stdout: bool, +) -> Result<(), InitLoggerError> { + let logger = build_backend(endpoint, level, echo_stdout)?; + build_dispatch(Box::new(logger), level).apply()?; Ok(()) } + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod tests { + use edgezero_core::logging::{BOOT_LOG_LEVEL, BOOT_LOG_TARGET}; + use log::{Level, LevelFilter, Log as _, Metadata, Record}; + + #[test] + fn backend_rejects_records_even_when_facade_allows_them() { + let logger = log_fastly::Logger::builder() + .default_endpoint("logging-test") + .max_level(LevelFilter::Error) + .build() + .expect("SDK logger"); + assert!( + !logger.enabled( + &Metadata::builder() + .target(BOOT_LOG_TARGET) + .level(Level::Warn) + .build() + ) + ); + } + + #[test] + fn production_filters_preserve_boot_warnings_and_runtime_level() { + let guard = log_fastly::reset_logger( + log_fastly::Logger::builder() + .max_level(LevelFilter::Off) + .build() + .expect("isolated SDK capture"), + ); + + for runtime_level in [ + LevelFilter::Off, + LevelFilter::Error, + LevelFilter::Warn, + LevelFilter::Info, + LevelFilter::Debug, + LevelFilter::Trace, + ] { + let backend = + super::build_backend("logging-test", runtime_level, false).expect("SDK backend"); + let (ceiling, logger) = + super::build_dispatch(Box::new(backend), runtime_level).into_log(); + assert_eq!(ceiling, runtime_level.max(BOOT_LOG_LEVEL)); + let preserved_warning = format!("preserved boot warning {runtime_level}"); + + for level in [ + Level::Error, + Level::Warn, + Level::Info, + Level::Debug, + Level::Trace, + ] { + let boot = Metadata::builder() + .target(BOOT_LOG_TARGET) + .level(level) + .build(); + let ordinary = Metadata::builder() + .target("application") + .level(level) + .build(); + assert_eq!(logger.enabled(&boot), level <= BOOT_LOG_LEVEL); + assert_eq!(logger.enabled(&ordinary), level <= runtime_level); + } + + logger.log( + &Record::builder() + .target(BOOT_LOG_TARGET) + .level(Level::Warn) + .args(format_args!("{preserved_warning}")) + .build(), + ); + guard.assert_contains("logging-test", &preserved_warning); + + logger.log( + &Record::builder() + .target(BOOT_LOG_TARGET) + .level(Level::Info) + .args(format_args!("suppressed boot info")) + .build(), + ); + guard.assert_absent("logging-test", "suppressed boot info"); + + if runtime_level <= LevelFilter::Error { + logger.log( + &Record::builder() + .target("application") + .level(Level::Warn) + .args(format_args!("suppressed runtime warning")) + .build(), + ); + guard.assert_absent("logging-test", "suppressed runtime warning"); + } + } + } +} diff --git a/crates/edgezero-adapter-fastly/src/outbound.rs b/crates/edgezero-adapter-fastly/src/outbound.rs new file mode 100644 index 000000000..f9d96ab11 --- /dev/null +++ b/crates/edgezero-adapter-fastly/src/outbound.rs @@ -0,0 +1,4573 @@ +#![cfg_attr( + any(feature = "fastly", test), + expect( + clippy::self_named_module_files, + reason = "retain the repository module-file convention despite contradictory restriction lints" + ) +)] +#![cfg_attr( + feature = "fastly", + expect( + clippy::arbitrary_source_item_ordering, + reason = "the target-gated implementation module is kept before shared test seams" + ) +)] +#![cfg_attr( + feature = "fastly", + expect( + clippy::pub_use, + reason = "the target-gated implementation keeps Fastly imports out of native builds" + ) +)] + +#[cfg(feature = "fastly")] +mod image_optimizer; + +#[cfg(any(feature = "fastly", test))] +mod progress; + +#[cfg(test)] +pub(crate) mod testing; + +#[cfg(any(feature = "fastly", test))] +pub(crate) mod scope; + +#[cfg(test)] +use edgezero_core::Deadline; +#[cfg(any(feature = "fastly", test))] +use std::time::Duration; + +use edgezero_core::error::EdgeError; +#[cfg(any(feature = "fastly", test))] +use edgezero_core::error::{BadGatewayReason, BudgetSource}; +#[cfg(test)] +use edgezero_core::outbound::OutboundBatchDriverEvent; +#[cfg(feature = "test-utils")] +use edgezero_core::outbound::validate_for_dispatch; +#[cfg(any(feature = "fastly", feature = "test-utils"))] +use edgezero_core::outbound::{OutboundRequest, OutboundSendMode}; +#[cfg(any(feature = "fastly", test))] +use edgezero_core::time::{DispatchBudget, MonotonicInstant}; + +#[cfg(any(feature = "fastly", test))] +#[derive(Clone, Copy)] +struct BackendTimers { + between_bytes: Duration, + connect: Duration, + first_byte: Duration, +} + +#[cfg(any(feature = "fastly", test))] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum SendFailure { + BudgetedTimeout, + ConnectTimeout, + ImageOptimizerUnavailable, + LocalInvariant, + PlatformInternal, + ProviderTimeout, + Transport, + Unknown, + Unreachable, + UpstreamProtocol, +} + +#[cfg(any(feature = "fastly", test))] +fn classify_send_failure( + failure: SendFailure, + budget: DispatchBudget, + observed_at: MonotonicInstant, +) -> EdgeError { + if budget.deadline.is_expired_at(observed_at) { + return timeout_error(budget.cause); + } + match failure { + SendFailure::BudgetedTimeout => timeout_error(budget.cause), + SendFailure::ConnectTimeout => timeout_error(BudgetSource::ConnectTimeout), + SendFailure::ImageOptimizerUnavailable => EdgeError::bad_gateway_with_reason( + "Fastly outbound request failed", + BadGatewayReason::ImageOptimizerUnavailable, + ), + SendFailure::ProviderTimeout => timeout_error(BudgetSource::Unspecified), + SendFailure::Unreachable => EdgeError::bad_gateway_with_reason( + "outbound destination could not be reached", + BadGatewayReason::Unreachable, + ), + SendFailure::Transport => EdgeError::bad_gateway_with_reason( + "outbound connection failed after dispatch", + BadGatewayReason::Transport, + ), + SendFailure::UpstreamProtocol => EdgeError::bad_gateway_with_reason( + "upstream response violated the HTTP protocol", + BadGatewayReason::Protocol, + ), + SendFailure::Unknown => EdgeError::bad_gateway_with_reason( + "Fastly outbound request failed", + BadGatewayReason::Unspecified, + ), + SendFailure::LocalInvariant | SendFailure::PlatformInternal => EdgeError::internal( + anyhow::anyhow!("Fastly rejected an adapter-owned outbound operation"), + ), + } +} + +#[cfg(test)] +fn test_budget(started_at: MonotonicInstant, cause: BudgetSource) -> DispatchBudget { + let duration = Duration::from_secs(1); + DispatchBudget { + cause, + deadline: Deadline::at_instant(started_at.checked_add(duration).expect("deadline instant")), + duration, + } +} + +#[cfg(any(feature = "fastly", test))] +fn synchronous_send_until( + budget: DispatchBudget, + clock: &edgezero_core::MonotonicClock, + send: Send, +) -> Result +where + Send: FnOnce() -> Result, +{ + let entered = clock.now(); + if budget.deadline.is_expired_at(entered) { + return Err(timeout_error(budget.cause)); + } + let outcome = send(); + let returned = clock.now(); + if returned < entered { + return Err(EdgeError::internal(anyhow::anyhow!( + "outbound clock moved backwards during native send" + ))); + } + if budget.deadline.is_expired_at(returned) { + let mut selected = timeout_error(budget.cause); + if let EdgeError::GatewayTimeout { + provider_detail, .. + } = &mut selected + { + *provider_detail = outcome + .as_ref() + .err() + .and_then(EdgeError::provider_failure_detail) + .copied(); + } + return Err(selected); + } + outcome +} + +#[cfg(test)] +fn assert_send_failure_error(failure: SendFailure, error: &EdgeError, selected: BudgetSource) { + match failure { + SendFailure::BudgetedTimeout => { + if let EdgeError::GatewayTimeout { cause, .. } = error { + assert_eq!(*cause, selected); + } else { + panic!("expected selected timeout"); + } + } + SendFailure::ConnectTimeout => assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::ConnectTimeout, + .. + } + )), + SendFailure::ProviderTimeout => assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::Unspecified, + .. + } + )), + SendFailure::LocalInvariant | SendFailure::PlatformInternal => { + assert!(matches!(error, EdgeError::Internal { .. })); + } + SendFailure::Transport => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Transport, + .. + } + )), + SendFailure::ImageOptimizerUnavailable => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::ImageOptimizerUnavailable, + .. + } + )), + SendFailure::Unknown => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Unspecified, + .. + } + )), + SendFailure::Unreachable => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Unreachable, + .. + } + )), + SendFailure::UpstreamProtocol => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Protocol, + .. + } + )), + } +} + +#[cfg(any(feature = "fastly", test))] +fn with_backend_timers( + phase_ms: u64, + connect_ms: Option, + configure: impl FnOnce(BackendTimers) -> Output, +) -> Output { + let phase = Duration::from_millis(phase_ms); + configure(BackendTimers { + connect: connect_ms.map_or(phase, Duration::from_millis), + first_byte: phase, + between_bytes: phase, + }) +} + +#[cfg(any(feature = "fastly", test))] +fn effective_connect_ms(configured: Option, governing: Duration) -> Option { + configured + .filter(|duration| *duration <= governing) + .map(|duration| { + u64::try_from(duration.as_nanos().div_ceil(1_000_000)) + .unwrap_or(u64::MAX) + .max(1) + }) +} + +#[cfg(feature = "fastly")] +mod fastly_impl { + use core::future::Future; + use core::task::{Context, Poll}; + #[cfg(test)] + use std::cell::Cell; + use std::collections::HashMap; + use std::io::{Read as _, Result as IoResult, Write}; + use std::net::IpAddr; + use std::rc::Rc; + use std::sync::{Mutex, OnceLock}; + use std::time::Duration; + + use async_stream::stream; + use base16ct::lower::encode_string; + use bytes::Bytes; + #[cfg(test)] + use edgezero_core::body::Body; + use edgezero_core::body::{BodyPayload, BodyStream}; + use edgezero_core::error::{BadGatewayReason, EdgeError}; + use edgezero_core::error_detail::{ + FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase, + }; + use edgezero_core::http::header::ACCEPT_ENCODING; + use edgezero_core::http::{HeaderMap, HeaderValue, Method, StatusCode, Uri}; + #[cfg(test)] + use edgezero_core::outbound::{ + HttpClient, OutboundProxyMarker, ResponseDecoding, ResponseMode, normalize_for_dispatch, + }; + use edgezero_core::outbound::{ + OutboundBatch, OutboundBatchDriverEvent, OutboundCachePolicy, OutboundDispatchRequest, + OutboundExchangeOwner, OutboundFuture, OutboundHead, OutboundHeaderFidelity, + OutboundHeaderInput, OutboundHttpClient, OutboundPending, OutboundPreparedBatch, + OutboundProgressRuntime, OutboundRequest, OutboundRequestParts, OutboundResponse, + OutboundResponseContext, OutboundScope, OutboundSendMode, OutboundTime, + }; + use edgezero_core::time::{ + DEADLINE_FAR_FUTURE, DispatchBudget, MonotonicClock, MonotonicInstant, + }; + #[cfg(test)] + use fastly::Body as FastlyBody; + use fastly::backend::BackendCreationError; + use fastly::handle::{BodyHandle, PendingRequestHandle, PollHandleResult}; + use fastly::http::body::StreamingBody; + use fastly::http::request::{PendingRequest, PollResult, SendErrorCause}; + use fastly::{Backend, Request as FastlyRequest, Response as FastlyResponse}; + use fastly_sys::fastly_async_io; + use futures_util::future::poll_fn; + use futures_util::stream::{FuturesUnordered, iter, once}; + use futures_util::{FutureExt as _, StreamExt as _}; + use sha2::{Digest as _, Sha256}; + + use super::progress::{HeadPoll, Progress, Transport}; + use super::{timeout_error, validate_supported_options}; + + pub(crate) type NativeProgress = Progress; + + pub(crate) async fn drive_callback( + future: F, + clock: MonotonicClock, + entered: MonotonicInstant, + ) -> Result + where + F: Future, + { + let scope = super::scope::capture(); + let progress = super::scope::native_progress(&scope, &native_time(clock), entered)?; + progress.drive(future).await + } + + pub(crate) fn native_time(clock: MonotonicClock) -> OutboundTime { + OutboundTime::new(clock, super::progress::wait_current) + } + + pub(crate) enum PendingHead { + Direct(PendingRequestHandle, bool), + Sdk(Box, bool), + } + + pub(crate) struct NativeTransport; + + pub(crate) struct NativeRuntime; + + impl OutboundProgressRuntime for NativeRuntime { + fn poll( + &self, + scope: &OutboundScope, + time: &OutboundTime, + entered_at: MonotonicInstant, + cx: &mut Context<'_>, + inner: &mut dyn FnMut(&mut Context<'_>) -> Poll<()>, + ) -> Poll> { + let progress = super::scope::native_progress(scope, time, entered_at)?; + progress.poll_once(cx, inner) + } + } + + impl Transport for NativeTransport { + type Body = BodyHandle; + type Head = FastlyResponse; + type Pending = PendingHead; + + #[expect( + unsafe_code, + reason = "readiness borrows the live SDK-owned body handle without transferring ownership" + )] + #[cfg_attr( + not(target_arch = "wasm32"), + expect( + deprecated, + reason = "the pinned SDK exposes WASI handle identity only" + ) + )] + fn body_handle(body: &BodyHandle) -> u32 { + // SAFETY: this observes identity only; the slot retains the owning SDK handle. + unsafe { body.as_u32() } + } + + fn head_handle(pending: &PendingHead) -> Option { + match pending { + PendingHead::Direct(handle, _) => Some(pending_handle_id(handle)), + PendingHead::Sdk(_, _) => None, + } + } + + fn poll_head( + &mut self, + pending: PendingHead, + budget: DispatchBudget, + clock: &MonotonicClock, + ) -> HeadPoll { + #[cfg(test)] + NATIVE_HEAD_POLLS.set(NATIVE_HEAD_POLLS.get().saturating_add(1)); + match pending { + PendingHead::Direct(handle, configured) => match handle.poll() { + PollHandleResult::Pending(waiting) => { + HeadPoll::Pending(PendingHead::Direct(waiting, configured)) + } + PollHandleResult::Done(result) => HeadPoll::Ready( + result + .map(|(head, body)| FastlyResponse::from_handles(head, body)) + .map_err(|cause| { + map_configured_send_cause(&cause, budget, clock, configured) + }), + ), + }, + PendingHead::Sdk(mut sdk, configured) => match (*sdk).poll() { + PollResult::Pending(waiting) => { + *sdk = waiting; + HeadPoll::Pending(PendingHead::Sdk(sdk, configured)) + } + PollResult::Done(result) => HeadPoll::Ready(result.map_err(|error| { + map_configured_send_cause(error.root_cause(), budget, clock, configured) + })), + }, + } + } + + fn read_body( + &mut self, + body: &mut BodyHandle, + maximum: usize, + budget: DispatchBudget, + clock: &MonotonicClock, + ) -> Result { + let mut bytes = vec![0; maximum]; + let count = body.read(&mut bytes).map_err(|error| { + let classified = if budget.deadline.is_expired_at(clock.now()) { + timeout_error(budget.cause) + } else { + EdgeError::bad_gateway_with_reason( + "Fastly upstream response body failed", + BadGatewayReason::Transport, + ) + }; + with_fastly_detail( + classified, + ProviderFailurePhase::ResponseBodyRead, + FastlyFailureCause::IoError { kind: error.kind() }, + ) + })?; + if count > maximum { + return Err(EdgeError::internal(anyhow::anyhow!( + "Fastly body read exceeded the supplied buffer" + ))); + } + bytes.truncate(count); + Ok(Bytes::from(bytes)) + } + + #[expect( + unsafe_code, + reason = "the timed readiness ABI borrows stable IDs from owning SDK handles" + )] + fn select(&mut self, handles: &[u32], timeout_ms: u32) -> Result, EdgeError> { + #[cfg(test)] + NATIVE_SELECTIONS.set(NATIVE_SELECTIONS.get().saturating_add(1)); + let mut selected = u32::MAX; + // SAFETY: the slice remains live through the call; zero means indefinite, so callers use a positive timeout. + let status = unsafe { + fastly_async_io::select( + handles.as_ptr(), + handles.len(), + timeout_ms, + &raw mut selected, + ) + }; + if !status.is_ok() { + return Err(with_fastly_detail( + EdgeError::internal(anyhow::anyhow!("Fastly timed readiness failed")), + ProviderFailurePhase::Send, + FastlyFailureCause::InternalError { + status: Some(status.code), + }, + )); + } + if selected == u32::MAX { + return Ok(None); + } + usize::try_from(selected).map(Some).map_err(|_error| { + EdgeError::internal(anyhow::anyhow!("invalid Fastly readiness index")) + }) + } + } + + pub const DYNAMIC_BACKENDS_DISABLED_MESSAGE: &str = "Fastly dynamic backends are not enabled on this service; enable them in the service configuration"; + + static BACKENDS: OnceLock>> = OnceLock::new(); + + #[cfg(test)] + std::thread_local! { + static NATIVE_HEAD_POLLS: Cell = const { Cell::new(0) }; + static NATIVE_SELECTIONS: Cell = const { Cell::new(0) }; + } + + #[derive(Clone, Debug, Eq, Hash, PartialEq)] + struct BackendIdentity { + host: String, + phase_ms: u64, + connect_ms: Option, + port: u16, + scheme: String, + tls: bool, + wire_authority: String, + } + + struct PreparedRequest { + backend: Backend, + context: OutboundResponseContext, + parts: OutboundRequestParts, + } + + /// Native outbound HTTP implementation for Fastly Compute. + pub struct FastlyOutboundClient { + time: OutboundTime, + } + + impl FastlyOutboundClient { + #[must_use] + #[inline] + pub fn new() -> Self { + Self::with_time(native_time(MonotonicClock::default())) + } + + /// Builds a client that evaluates every outbound lifetime against `clock`. + #[must_use] + #[inline] + pub fn with_time(time: OutboundTime) -> Self { + Self { time } + } + + fn ensure_backend( + &self, + identity: BackendIdentity, + parts: &OutboundRequestParts, + budget: DispatchBudget, + batch_started_at: Option, + ) -> Result { + let registry = BACKENDS.get_or_init(|| Mutex::new(HashMap::new())); + let backend = cached_backend( + registry, + identity, + budget, + self.time.clock(), + |name, settings| { + let target = + format!("{}:{}", parts.uri.host().unwrap_or_default(), settings.port); + let mut builder = super::with_backend_timers( + settings.phase_ms, + settings.connect_ms, + |timers| { + Backend::builder(name, target) + .override_host(&settings.wire_authority) + .connect_timeout(timers.connect) + .first_byte_timeout(timers.first_byte) + .between_bytes_timeout(timers.between_bytes) + }, + ); + if settings.tls { + builder = builder.enable_ssl().check_certificate(&settings.host); + if settings.host.parse::().is_err() { + builder = builder.sni_hostname(&settings.host); + } + } + register_backend_while_live(batch_started_at, budget, self.time.clock(), || { + builder.finish() + }) + }, + )?; + Ok(backend) + } + + fn configure( + &self, + request: OutboundDispatchRequest, + ) -> Result { + request.scope().check()?; + let budget = request.budget(); + let (mut identity, ()) = backend_identity_view(request.request(), budget)?; + let (mut parts, context) = request.into_parts(); + identity.phase_ms = backend_timer_ms(parts.timeout.unwrap_or(budget.duration)); + if !parts.headers.contains_key(ACCEPT_ENCODING) { + parts + .headers + .insert(ACCEPT_ENCODING, HeaderValue::from_static("identity")); + } + budget_remaining(budget, context.clock())?; + let batch_start = + (context.mode() == OutboundSendMode::Batch).then_some(context.entered_at()); + let backend = self.ensure_backend(identity, &parts, budget, batch_start)?; + budget_remaining(budget, context.clock())?; + Ok(PreparedRequest { + backend, + context, + parts, + }) + } + + async fn execute_head( + &self, + prepared: PreparedRequest, + progress: &NativeProgress, + ) -> Result { + let PreparedRequest { + backend, + context, + parts, + } = prepared; + let budget = context.budget(); + let started_at = context.entered_at(); + let time = context.time().clone(); + let OutboundRequestParts { + body, + cache_policy, + headers, + image_optimizer, + max_request_body_bytes, + method, + uri, + .. + } = parts; + let configured = + super::effective_connect_ms(parts.connect_timeout, budget.duration).is_some(); + let fastly_request = build_fastly_request(&method, &uri, &headers, cache_policy); + let (payload, _scope) = body.into_parts(); + let response = match payload { + BodyPayload::Once(bytes) => { + validate_request_body_length(&bytes, max_request_body_bytes)?; + let mut request = fastly_request; + if let Some(options) = image_optimizer { + request + .set_image_optimizer(super::image_optimizer::native_options(&options)?); + ensure_dispatch_budget(started_at, budget, time.clock())?; + super::synchronous_send_until(budget, time.clock(), || { + request.send(backend).map_err(|error| { + map_configured_send_cause( + error.root_cause(), + budget, + time.clock(), + configured, + ) + }) + })? + } else { + request.set_body(bytes.to_vec()); + ensure_dispatch_budget(started_at, budget, time.clock())?; + let pending = if context.mode() == OutboundSendMode::Batch { + let (handle, optional_body) = request.into_handles(); + let native_body = optional_body.ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!( + "Fastly buffered request did not produce a body handle" + )) + })?; + PendingHead::Direct( + handle + .send_async_to_backend(native_body, &backend) + .map_err(|cause| { + map_configured_send_cause( + &cause, + budget, + time.clock(), + configured, + ) + })?, + configured, + ) + } else { + PendingHead::Sdk( + Box::new(request.send_async(backend).map_err(|error| { + map_configured_send_cause( + error.root_cause(), + budget, + time.clock(), + configured, + ) + })?), + configured, + ) + }; + progress.head(pending, budget).receive().await? + } + } + BodyPayload::Stream(source) => { + send_streamed( + fastly_request, + backend, + source, + max_request_body_bytes, + budget, + started_at, + time.clock().clone(), + progress, + configured, + ) + .await? + } + }; + process_head(response, context, progress) + } + + fn pipeline( + request: OutboundDispatchRequest, + progress: NativeProgress, + ) -> OutboundFuture { + let client = Self::with_time(request.time().clone()); + Box::pin(async move { + let budget = request.budget(); + let time = request.time().clone(); + race_deadline( + async { + let prepared = client.configure(request)?; + client + .execute_head(prepared, &progress) + .boxed_local() + .await? + .finish() + .await + }, + budget, + &time, + ) + .await + }) + } + } + + impl Default for FastlyOutboundClient { + #[inline] + fn default() -> Self { + Self::new() + } + } + + impl OutboundHttpClient for FastlyOutboundClient { + #[inline] + fn progress(&self) -> Rc { + super::scope::runtime() + } + + #[inline] + fn time(&self) -> OutboundTime { + self.time.clone() + } + + #[inline] + fn validate_supported_options( + &self, + request: &OutboundRequest, + mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + validate_supported_options(request, mode) + } + + #[inline] + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + let client = Self::with_time(request.time().clone()); + let selected_progress = super::scope::native_progress( + request.scope(), + request.time(), + request.entered_at(), + ); + Box::pin(async move { + let progress = selected_progress?; + let budget = request.budget(); + let time = request.time().clone(); + progress + .drive(race_deadline( + async { + let prepared = client.configure(request)?; + client.execute_head(prepared, &progress).boxed_local().await + }, + budget, + &time, + )) + .await? + }) + } + + #[inline] + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let scope = request.scope().clone(); + scope.check()?; + let time = request.time().clone(); + let entered = request.entered_at(); + let progress = super::scope::native_progress(&scope, &time, entered)?; + super::scope::begin( + &scope, + Self::pipeline(request, progress.clone()), + &progress, + &time, + entered, + ) + } + + #[inline] + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, requests) = batch.into_slots(); + let cutoff = context.observation_cutoff(); + let observed_at = context.clock().now(); + if cutoff.is_expired_at(observed_at) { + if requests.is_empty() { + return OutboundBatch::cutoff(context); + } + // Handoff after cutoff must still publish expired earlier-own-deadline slots. + let mut events = Vec::new(); + let mut unresolved = false; + for (index, request) in requests { + let budget = request.budget(); + if budget.own_deadline_expired_before(cutoff, observed_at) { + events.push(OutboundBatchDriverEvent::Item { + index, + completed_at: observed_at, + outcome: Err(timeout_error(budget.cause)), + }); + } else { + unresolved = true; + } + } + if unresolved { + events.push(OutboundBatchDriverEvent::Cutoff); + } + return OutboundBatch::from_driver( + context, + iter(events), + OutboundExchangeOwner::new(|| {}), + ); + } + let scope = context.scope().clone(); + let selected_progress = + super::scope::native_progress(&scope, context.time(), context.started_at()); + let progress = match selected_progress { + Ok(available) => available, + Err(error) => { + return OutboundBatch::from_driver( + context, + once(async move { OutboundBatchDriverEvent::Failed(error) }), + OutboundExchangeOwner::new(|| {}), + ); + } + }; + let time = context.time().clone(); + let exchanges = requests + .into_iter() + .map(|(index, request)| { + let budget = request.budget(); + let future = Self::pipeline(request, progress.clone()); + let clock = time.clock().clone(); + async move { + let outcome = future.await; + let completed_at = clock.now(); + let beyond_cutoff = cutoff.is_expired_at(completed_at) + && !budget.own_deadline_expired_before(cutoff, completed_at); + ( + OutboundBatchDriverEvent::Item { + index, + completed_at, + outcome, + }, + beyond_cutoff, + ) + } + .boxed_local() + }) + .collect::>(); + let driver = progress.clone(); + let completions = stream! { + let mut active = exchanges; + let mut cutoff_reached = false; + while !active.is_empty() { + match driver.drive(active.next()).await { + Ok(Some((event, beyond_cutoff))) => { cutoff_reached |= beyond_cutoff; yield event; }, + Ok(None) => break, + Err(error) => { + yield OutboundBatchDriverEvent::Failed(error); + return; + } + } + } + if cutoff_reached { yield OutboundBatchDriverEvent::Cutoff; } + }; + let receiver = super::scope::schedule_batch( + &scope, + completions, + &progress, + context.time(), + context.started_at(), + ); + OutboundBatch::from_driver(context, receiver, OutboundExchangeOwner::new(|| {})) + } + } + + #[cfg(test)] + fn backend_identity( + request: OutboundRequest, + budget: DispatchBudget, + ) -> Result<(BackendIdentity, OutboundRequestParts), EdgeError> { + let (mut identity, ()) = backend_identity_view(&request, budget)?; + let parts = request.into_parts(); + identity.phase_ms = backend_timer_ms(parts.timeout.unwrap_or(budget.duration)); + Ok((identity, parts)) + } + + fn backend_identity_view( + request: &OutboundRequest, + budget: DispatchBudget, + ) -> Result<(BackendIdentity, ()), EdgeError> { + let scheme = request + .uri() + .scheme_str() + .ok_or_else(|| EdgeError::bad_request("outbound request URI has no scheme"))?; + Ok(( + BackendIdentity { + host: request.host_name().to_owned(), + phase_ms: backend_timer_ms(budget.duration), + // Keep the configured tuple stable across aggregate-budget ladder rungs. + connect_ms: request + .connect_timeout_setting() + .map(|configured| ceil_millis(configured.min(DEADLINE_FAR_FUTURE))), + port: request + .uri() + .port_u16() + .unwrap_or(if scheme == "https" { 443 } else { 80 }), + scheme: scheme.to_owned(), + tls: scheme == "https", + wire_authority: request.host_authority().to_owned(), + }, + (), + )) + } + + #[expect( + clippy::big_endian_bytes, + reason = "versioned backend identity uses a fixed byte order independent of the host architecture" + )] + fn backend_name(identity: &BackendIdentity) -> String { + let mut hasher = Sha256::new(); + for field in [&identity.scheme, &identity.host, &identity.wire_authority] { + hasher.update(field.as_bytes()); + hasher.update([0_u8]); + } + hasher.update(identity.port.to_be_bytes()); + hasher.update([u8::from(identity.tls)]); + hasher.update(identity.phase_ms.to_be_bytes()); + if let Some(connect_ms) = identity.connect_ms { + hasher.update(b"connect-ms-v1"); + hasher.update(connect_ms.to_be_bytes()); + } + format!("ez_b31v1_{}", encode_string(&hasher.finalize())) + } + + fn backend_timer_ms(anchor: Duration) -> u64 { + let maximum = ceil_millis(DEADLINE_FAR_FUTURE); + ceil_millis(anchor.min(DEADLINE_FAR_FUTURE)) + .checked_next_power_of_two() + .unwrap_or(maximum) + .min(maximum) + } + + #[cfg(test)] + fn backend_timers(phase_ms: u64) -> super::BackendTimers { + super::with_backend_timers(phase_ms, None, |timers| timers) + } + + fn cached_backend( + registry: &Mutex>, + identity: BackendIdentity, + budget: DispatchBudget, + clock: &MonotonicClock, + register: Register, + ) -> Result + where + Value: Clone, + Register: FnOnce( + &str, + &BackendIdentity, + ) -> Result, EdgeError>, + { + let name = backend_name(&identity); + // Serialize registration and insertion; NameInUse alone cannot certify a configuration. + let mut cache = registry.lock().map_err(|_poisoned| { + EdgeError::internal(anyhow::anyhow!("Fastly backend cache was poisoned")) + })?; + budget_remaining(budget, clock)?; + if let Some((registered_identity, backend)) = cache.get(&name) { + if registered_identity != &identity { + return Err(EdgeError::internal(anyhow::anyhow!( + "dynamic backend name collision; refusing to reuse" + ))); + } + return Ok(backend.clone()); + } + let result = register(&name, &identity)?; + finish_backend_creation(result, budget, clock, |backend| { + cache.insert(name, (identity, backend.clone())); + Ok(backend) + }) + } + + fn build_fastly_request( + method: &Method, + uri: &Uri, + headers: &HeaderMap, + cache_policy: OutboundCachePolicy, + ) -> FastlyRequest { + let mut request = FastlyRequest::new(method.clone(), uri.to_string()); + request.set_method(method.clone()); + for (name, value) in headers { + request.append_header(name.as_str(), value.as_bytes()); + } + if cache_policy == OutboundCachePolicy::Bypass { + request.set_pass(true); + } + request + } + + fn ceil_millis(duration: Duration) -> u64 { + let rounded = duration.as_nanos().div_ceil(1_000_000); + u64::try_from(rounded).unwrap_or(u64::MAX).max(1) + } + + fn register_backend_while_live( + batch_started_at: Option, + budget: DispatchBudget, + clock: &MonotonicClock, + register: Register, + ) -> Result, EdgeError> + where + Register: FnOnce() -> Result, + { + if let Some(started_at) = batch_started_at { + ensure_dispatch_budget(started_at, budget, clock)?; + } + Ok(register()) + } + + #[cfg_attr( + not(target_env = "p1"), + expect( + deprecated, + reason = "Fastly exposes only the raw handle identity needed to restore select order" + ) + )] + fn pending_handle_id(handle: &PendingRequestHandle) -> u32 { + handle.as_u32() + } + + fn map_backend_creation_error(error: &BackendCreationError) -> EdgeError { + let classified = match error { + BackendCreationError::Disallowed => EdgeError::bad_gateway_with_reason( + DYNAMIC_BACKENDS_DISABLED_MESSAGE, + BadGatewayReason::Unspecified, + ), + BackendCreationError::NameInUse => EdgeError::internal(anyhow::anyhow!( + "dynamic backend name collision; refusing to reuse" + )), + BackendCreationError::BetweenBytesTimeoutTooLarge(_) + | BackendCreationError::ConnectTimeoutTooLarge(_) + | BackendCreationError::EncodingError(_) + | BackendCreationError::FirstByteTimeoutTooLarge(_) + | BackendCreationError::InvalidHealthcheckValue(_) + | BackendCreationError::NameTooLong(_) => EdgeError::internal(anyhow::anyhow!( + "Fastly rejected deterministic dynamic backend configuration" + )), + BackendCreationError::HostError(_) => EdgeError::bad_gateway_with_reason( + "Fastly failed to register a dynamic backend", + BadGatewayReason::Unspecified, + ), + }; + with_fastly_detail( + classified, + ProviderFailurePhase::BackendRegistration, + backend_failure_cause(error), + ) + } + + fn with_fastly_detail( + mut error: EdgeError, + phase: ProviderFailurePhase, + cause: FastlyFailureCause, + ) -> EdgeError { + if let EdgeError::BadGateway { + provider_detail, .. + } + | EdgeError::GatewayTimeout { + provider_detail, .. + } + | EdgeError::Internal { + provider_detail, .. + } = &mut error + { + *provider_detail = Some(ProviderFailureDetail::Fastly { phase, cause }); + } + error + } + + fn backend_failure_cause(error: &BackendCreationError) -> FastlyFailureCause { + match error { + BackendCreationError::BetweenBytesTimeoutTooLarge(_) => { + FastlyFailureCause::BackendBetweenBytesTimeoutTooLarge + } + BackendCreationError::ConnectTimeoutTooLarge(_) => { + FastlyFailureCause::BackendConnectTimeoutTooLarge + } + BackendCreationError::FirstByteTimeoutTooLarge(_) => { + FastlyFailureCause::BackendFirstByteTimeoutTooLarge + } + BackendCreationError::Disallowed => FastlyFailureCause::BackendDisallowed, + BackendCreationError::EncodingError(_) => FastlyFailureCause::BackendEncodingError, + BackendCreationError::InvalidHealthcheckValue(_) => { + FastlyFailureCause::BackendInvalidHealthcheckValue + } + BackendCreationError::NameInUse => FastlyFailureCause::BackendNameInUse, + BackendCreationError::NameTooLong(_) => FastlyFailureCause::BackendNameTooLong, + BackendCreationError::HostError(status) => FastlyFailureCause::BackendHostError { + status: status.code, + }, + } + } + + fn send_failure_cause(error: &SendErrorCause) -> FastlyFailureCause { + match error { + SendErrorCause::DnsTimeout => FastlyFailureCause::DnsTimeout, + SendErrorCause::DnsError { rcode, info_code } => FastlyFailureCause::DnsError { + rcode: *rcode, + info_code: *info_code, + }, + SendErrorCause::DestinationNotFound => FastlyFailureCause::DestinationNotFound, + SendErrorCause::DestinationUnavailable => FastlyFailureCause::DestinationUnavailable, + SendErrorCause::DestinationIpUnroutable => FastlyFailureCause::DestinationIpUnroutable, + SendErrorCause::ConnectionRefused => FastlyFailureCause::ConnectionRefused, + SendErrorCause::ConnectionTerminated => FastlyFailureCause::ConnectionTerminated, + SendErrorCause::ConnectionTimeout => FastlyFailureCause::ConnectionTimeout, + SendErrorCause::ConnectionLimitReached => FastlyFailureCause::ConnectionLimitReached, + SendErrorCause::TlsProtocolError => FastlyFailureCause::TlsProtocolError, + SendErrorCause::TlsCertificateError => FastlyFailureCause::TlsCertificateError, + SendErrorCause::TlsConfigurationError => FastlyFailureCause::TlsConfigurationError, + SendErrorCause::TlsAlertReceived { alert_id } => FastlyFailureCause::TlsAlertReceived { + alert_id: *alert_id, + }, + SendErrorCause::HttpIncompleteResponse => FastlyFailureCause::HttpIncompleteResponse, + SendErrorCause::HttpProtocolError => FastlyFailureCause::HttpProtocolError, + SendErrorCause::HttpResponseHeaderSectionTooLarge => { + FastlyFailureCause::HttpResponseHeaderSectionTooLarge + } + SendErrorCause::HttpResponseBodyTooLarge => { + FastlyFailureCause::HttpResponseBodyTooLarge + } + SendErrorCause::HttpResponseTimeout => FastlyFailureCause::HttpResponseTimeout, + SendErrorCause::HttpResponseStatusInvalid => { + FastlyFailureCause::HttpResponseStatusInvalid + } + SendErrorCause::HttpUpgradeFailed => FastlyFailureCause::HttpUpgradeFailed, + SendErrorCause::HttpRequestCacheKeyInvalid => { + FastlyFailureCause::HttpRequestCacheKeyInvalid + } + SendErrorCause::HttpRequestUriInvalid => FastlyFailureCause::HttpRequestUriInvalid, + SendErrorCause::HttpCacheLimitExceeded => FastlyFailureCause::HttpCacheLimitExceeded, + SendErrorCause::HttpCacheApiUnsupported => FastlyFailureCause::HttpCacheApiUnsupported, + SendErrorCause::IoError(source) => FastlyFailureCause::IoError { + kind: source.kind(), + }, + SendErrorCause::FanoutNotEnabled => FastlyFailureCause::FanoutNotEnabled, + SendErrorCause::ImageOptimizerUnsupported => { + FastlyFailureCause::ImageOptimizerUnsupported + } + SendErrorCause::InternalError(status) => FastlyFailureCause::InternalError { + status: status.as_ref().map(|value| value.code), + }, + SendErrorCause::Http2StreamError { + frame_type, + error_code, + } => FastlyFailureCause::Http2StreamError { + frame_type: *frame_type, + error_code: *error_code, + }, + SendErrorCause::RequestCollapse => FastlyFailureCause::RequestCollapse, + SendErrorCause::Custom(_) => FastlyFailureCause::Custom, + _ => FastlyFailureCause::Unknown, + } + } + + fn finish_backend_creation( + result: Result, + budget: DispatchBudget, + clock: &MonotonicClock, + retain_success: RetainSuccess, + ) -> Result + where + RetainSuccess: FnOnce(BackendValue) -> Result, + { + match result { + Ok(backend) => { + let retained = retain_success(backend); + if budget.deadline.is_expired_at(clock.now()) { + Err(timeout_error(budget.cause)) + } else { + retained + } + } + Err(error) => { + if budget.deadline.is_expired_at(clock.now()) { + Err(with_fastly_detail( + timeout_error(budget.cause), + ProviderFailurePhase::BackendRegistration, + backend_failure_cause(&error), + )) + } else { + Err(map_backend_creation_error(&error)) + } + } + } + } + + fn cause_to_failure(cause: &SendErrorCause) -> super::SendFailure { + match cause { + SendErrorCause::HttpResponseTimeout => super::SendFailure::BudgetedTimeout, + SendErrorCause::ConnectionTimeout | SendErrorCause::DnsTimeout => { + super::SendFailure::ProviderTimeout + } + SendErrorCause::ConnectionLimitReached + | SendErrorCause::ConnectionRefused + | SendErrorCause::DestinationIpUnroutable + | SendErrorCause::DestinationNotFound + | SendErrorCause::DestinationUnavailable + | SendErrorCause::DnsError { .. } + | SendErrorCause::TlsAlertReceived { .. } + | SendErrorCause::TlsCertificateError + | SendErrorCause::TlsConfigurationError + | SendErrorCause::TlsProtocolError => super::SendFailure::Unreachable, + SendErrorCause::ConnectionTerminated | SendErrorCause::IoError(_) => { + super::SendFailure::Transport + } + SendErrorCause::Http2StreamError { .. } + | SendErrorCause::HttpIncompleteResponse + | SendErrorCause::HttpProtocolError + | SendErrorCause::HttpResponseBodyTooLarge + | SendErrorCause::HttpResponseHeaderSectionTooLarge + | SendErrorCause::HttpResponseStatusInvalid + | SendErrorCause::HttpUpgradeFailed => super::SendFailure::UpstreamProtocol, + SendErrorCause::HttpCacheApiUnsupported + | SendErrorCause::HttpCacheLimitExceeded + | SendErrorCause::HttpRequestCacheKeyInvalid + | SendErrorCause::HttpRequestUriInvalid => super::SendFailure::LocalInvariant, + SendErrorCause::InternalError(_) => super::SendFailure::PlatformInternal, + SendErrorCause::ImageOptimizerUnsupported => { + super::SendFailure::ImageOptimizerUnavailable + } + SendErrorCause::FanoutNotEnabled + | SendErrorCause::RequestCollapse + | SendErrorCause::Custom(_) + | _ => super::SendFailure::Unknown, + } + } + + #[cfg(test)] + fn map_send_cause( + cause: &SendErrorCause, + budget: DispatchBudget, + clock: &MonotonicClock, + ) -> EdgeError { + with_fastly_detail( + super::classify_send_failure(cause_to_failure(cause), budget, clock.now()), + ProviderFailurePhase::Send, + send_failure_cause(cause), + ) + } + + fn map_configured_send_cause( + cause: &SendErrorCause, + budget: DispatchBudget, + clock: &MonotonicClock, + configured: bool, + ) -> EdgeError { + let failure = if configured && matches!(cause, SendErrorCause::ConnectionTimeout) { + super::SendFailure::ConnectTimeout + } else { + cause_to_failure(cause) + }; + with_fastly_detail( + super::classify_send_failure(failure, budget, clock.now()), + ProviderFailurePhase::Send, + send_failure_cause(cause), + ) + } + + fn process_head( + mut response: FastlyResponse, + context: OutboundResponseContext, + progress: &NativeProgress, + ) -> Result { + budget_remaining(context.budget(), context.clock())?; + let status = StatusCode::from_u16(response.get_status().as_u16()).map_err(|_error| { + EdgeError::bad_gateway_with_reason( + "Fastly returned an invalid upstream status code", + BadGatewayReason::Protocol, + ) + })?; + let headers = OutboundHeaderInput::new( + "fastly", + response_headers(&response), + OutboundHeaderFidelity::AdapterVisibleFields, + ); + let encoded = progress.body(response.take_body().into_handle(), context.budget()); + OutboundHead::new( + status, + headers, + encoded, + context, + OutboundExchangeOwner::new(|| {}), + ) + } + + fn response_headers(response: &FastlyResponse) -> HeaderMap { + let mut headers = HeaderMap::new(); + for name in response.get_header_names() { + for value in response.get_header_all(name) { + headers.append(name.clone(), value.clone()); + } + } + headers + } + + trait StreamedUploadWriter { + fn finish(self) -> IoResult<()>; + fn flush(&mut self) -> IoResult<()>; + fn write_all(&mut self, bytes: &[u8]) -> IoResult<()>; + } + + impl StreamedUploadWriter for StreamingBody { + fn finish(self) -> IoResult<()> { + StreamingBody::finish(self) + } + + fn flush(&mut self) -> IoResult<()> { + Write::flush(self) + } + + fn write_all(&mut self, bytes: &[u8]) -> IoResult<()> { + Write::write_all(self, bytes) + } + } + + async fn complete_streamed_exchange( + mut writer: Writer, + mut source: BodyStream, + maximum: u64, + budget: DispatchBudget, + clock: &MonotonicClock, + wait: Wait, + ) -> Result + where + Writer: StreamedUploadWriter, + Wait: FnOnce() -> Result, + { + let mut total = 0_u64; + loop { + budget_remaining(budget, clock)?; + let next_item = source.next().await; + budget_remaining(budget, clock)?; + let Some(item) = next_item else { + break; + }; + let bytes = item?; + let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX); + let Some(next_total) = total.checked_add(length) else { + return Err(EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + )); + }; + if next_total > maximum { + return Err(EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + )); + } + resolve_stream_io_result( + writer.write_all(&bytes), + budget, + clock.now(), + "Fastly outbound request body write failed", + )?; + resolve_stream_io_result( + writer.flush(), + budget, + clock.now(), + "Fastly outbound request body flush failed", + )?; + total = next_total; + } + resolve_stream_io_result( + writer.finish(), + budget, + clock.now(), + "Fastly outbound request completion failed", + )?; + wait() + } + + #[expect( + clippy::too_many_arguments, + reason = "upload ownership and original dispatch budget stay explicit at the provider boundary" + )] + async fn send_streamed( + request: FastlyRequest, + backend: Backend, + source: BodyStream, + maximum: u64, + budget: DispatchBudget, + started_at: MonotonicInstant, + clock: MonotonicClock, + progress: &NativeProgress, + configured: bool, + ) -> Result { + ensure_dispatch_budget(started_at, budget, &clock)?; + let (writer, pending) = request.send_async_streaming(backend).map_err(|error| { + map_configured_send_cause(error.root_cause(), budget, &clock, configured) + })?; + let uploaded = + complete_streamed_exchange(writer, source, maximum, budget, &clock, || Ok(pending)) + .await?; + progress + .head(PendingHead::Sdk(Box::new(uploaded), configured), budget) + .receive() + .await + } + + fn resolve_stream_io_result( + result: IoResult<()>, + budget: DispatchBudget, + observed_at: MonotonicInstant, + failure_message: &'static str, + ) -> Result<(), EdgeError> { + if budget.deadline.is_expired_at(observed_at) { + let selected = timeout_error(budget.cause); + return Err(match result { + Err(source) => with_fastly_detail( + selected, + ProviderFailurePhase::RequestBodyUpload, + FastlyFailureCause::IoError { + kind: source.kind(), + }, + ), + Ok(()) => selected, + }); + } + result.map_err(|error| { + EdgeError::bad_gateway_with_detail( + failure_message, + BadGatewayReason::Transport, + ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::RequestBodyUpload, + cause: FastlyFailureCause::IoError { kind: error.kind() }, + }, + ) + }) + } + + #[cfg(test)] + fn fastly_body_stream( + body: FastlyBody, + budget: DispatchBudget, + clock: MonotonicClock, + ) -> BodyStream { + let entered = budget + .deadline + .instant() + .checked_sub(budget.duration) + .unwrap_or(budget.deadline.instant()); + NativeProgress::new(NativeTransport, clock, entered, Some(budget.deadline)) + .body(body.into_handle(), budget) + } + + async fn race_deadline( + future: impl Future>, + budget: DispatchBudget, + time: &OutboundTime, + ) -> Result { + let mut timer = time.wait_until(budget.deadline); + let mut previous = time.clock().now(); + race_with_timer(future, budget, time, &mut timer, &mut previous, |result| { + result + .as_ref() + .err() + .and_then(EdgeError::provider_failure_detail) + .copied() + }) + .await? + } + + fn checked_now( + time: &OutboundTime, + previous: &mut MonotonicInstant, + ) -> Result { + let observed = time.clock().now(); + if observed < *previous { + return Err(EdgeError::internal(anyhow::anyhow!( + "outbound clock moved backwards during I/O" + ))); + } + *previous = observed; + Ok(observed) + } + + async fn race_with_timer( + future: impl Future, + budget: DispatchBudget, + time: &OutboundTime, + timer: &mut OutboundFuture<()>, + previous: &mut MonotonicInstant, + detail: impl FnOnce(&Output) -> Option, + ) -> Result { + let mut extract_detail = Some(detail); + futures_util::pin_mut!(future); + poll_fn(|cx| { + checked_now(time, previous)?; + if let Poll::Ready(result) = timer.as_mut().poll(cx) { + return Poll::Ready(Err(result + .err() + .unwrap_or_else(|| timeout_error(budget.cause)))); + } + if budget.deadline.is_expired_at(checked_now(time, previous)?) { + return Poll::Ready(Err(timeout_error(budget.cause))); + } + let output = future.as_mut().poll(cx); + if budget.deadline.is_expired_at(checked_now(time, previous)?) { + let mut error = timeout_error(budget.cause); + if let EdgeError::GatewayTimeout { + provider_detail, .. + } = &mut error + && let Poll::Ready(value) = &output + { + *provider_detail = extract_detail.take().and_then(|extract| extract(value)); + } + return Poll::Ready(Err(error)); + } + output.map(Ok) + }) + .await + } + + fn validate_request_body_length(bytes: &Bytes, maximum: u64) -> Result<(), EdgeError> { + let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX); + if length > maximum { + return Err(EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + )); + } + Ok(()) + } + + fn budget_remaining( + budget: DispatchBudget, + clock: &MonotonicClock, + ) -> Result { + budget + .deadline + .remaining_at(clock.now()) + .map(|remaining| remaining.min(budget.duration)) + .ok_or_else(|| timeout_error(budget.cause)) + } + + fn ensure_dispatch_budget( + started_at: MonotonicInstant, + budget: DispatchBudget, + clock: &MonotonicClock, + ) -> Result<(), EdgeError> { + check_dispatch_budget_at(started_at, budget, clock.now()) + } + + fn check_dispatch_budget_at( + started_at: MonotonicInstant, + budget: DispatchBudget, + observed_at: MonotonicInstant, + ) -> Result<(), EdgeError> { + if budget.deadline.is_expired_at(observed_at) { + return Err(timeout_error(budget.cause)); + } + if observed_at.checked_duration_since(started_at).is_none() { + return Err(EdgeError::internal(anyhow::anyhow!( + "monotonic clock moved backwards before Fastly SDK dispatch" + ))); + } + Ok(()) + } + + #[cfg(test)] + mod tests { + use std::cell::Cell; + use std::collections::{BTreeSet, VecDeque}; + use std::io::{Error, ErrorKind}; + use std::mem::discriminant; + use std::num::NonZeroU64; + use std::sync::{Arc, Mutex}; + use std::thread; + + use super::super::testing; + use edgezero_core::error::{BudgetSource, ResponseLimitReason}; + use edgezero_core::http::header::{ + CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, + }; + use edgezero_core::outbound::{ContentTypeIneligibility, ContentTypeProvenance}; + use edgezero_core::time::{Deadline, dispatch_budget}; + use flate2::{Compression, write::GzEncoder}; + use futures::executor::block_on; + use futures_util::stream; + + use super::*; + + #[expect( + clippy::too_many_arguments, + reason = "legacy policy fixtures exercise the shared head finalizer through the real facade" + )] + async fn finish_native_fixture( + response: FastlyResponse, + method: Method, + marker: OutboundProxyMarker, + decoding: ResponseDecoding, + mode: ResponseMode, + budget: DispatchBudget, + decoder_memory: u64, + brotli: u8, + chunk: Option, + decoded: Option, + encoded: Option, + header_bytes: Option, + header_count: Option, + time: OutboundTime, + progress: &NativeProgress, + ) -> Result { + let mut request = + OutboundRequest::new(method, "https://example.com/".parse().unwrap())? + .proxy_marker(marker) + .response_decoding(decoding) + .max_decoder_bytes(decoder_memory) + .max_brotli_window_bits(brotli); + let mut parts = request.into_parts(); + parts.timeout = Some(budget.duration); + parts.deadline = + (budget.cause != BudgetSource::PerCallTimeout).then_some(budget.deadline); + parts.max_chunk_bytes = chunk; + parts.max_decoded_response_bytes = decoded; + parts.max_encoded_response_bytes = encoded; + parts.max_response_header_bytes = header_bytes; + parts.max_response_header_count = header_count; + parts.response_mode = mode; + request = OutboundRequest::from_parts(parts)?; + let (_, context) = testing::dispatch_at( + request, + time, + budget + .deadline + .instant() + .checked_sub(budget.duration) + .unwrap(), + ) + .into_parts(); + progress + .drive(async { process_head(response, context, progress)?.finish().await }) + .await? + } + + fn shared_head_stream( + source: BodyStream, + budget: DispatchBudget, + time: OutboundTime, + progress: NativeProgress, + ) -> BodyStream { + let start = budget + .deadline + .instant() + .checked_sub(budget.duration) + .unwrap(); + let request = OutboundRequest::get("https://example.com/") + .unwrap() + .stream_response() + .timeout(budget.duration) + .proxy_marker(OutboundProxyMarker::Off); + let (_, context) = testing::dispatch_at(request, time, start).into_parts(); + stream! { + let head = OutboundHead::new(StatusCode::OK, + OutboundHeaderInput::new("fastly", HeaderMap::new(), OutboundHeaderFidelity::AdapterVisibleFields), + source, context, OutboundExchangeOwner::new(|| {}))?; + let response = progress.drive(head.finish()).await??; + let (BodyPayload::Stream(mut payload), _scope) = response.into_body().into_parts() else { return; }; + while let Some(item) = progress.drive(payload.next()).await? { + yield item; + } + }.boxed_local() + } + + fn resolved( + slot: &edgezero_core::OutboundCollectedSlot, + ) -> &edgezero_core::OutboundSlotResult { + match slot { + edgezero_core::OutboundCollectedSlot::Resolved(result) => result, + edgezero_core::OutboundCollectedSlot::AlreadyDelivered + | edgezero_core::OutboundCollectedSlot::Unresolved => { + panic!("expected resolved slot") + } + } + } + + fn scripted_clock(script: Vec) -> MonotonicClock { + let last = *script.last().expect("clock script"); + let observations = Arc::new(Mutex::new(VecDeque::from(script))); + MonotonicClock::new(move || { + observations + .lock() + .expect("clock observations") + .pop_front() + .unwrap_or(last) + }) + } + + fn constant_clock(now: MonotonicInstant) -> MonotonicClock { + MonotonicClock::new(move || now) + } + + fn clock_budget(start: MonotonicInstant, duration: Duration) -> DispatchBudget { + DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant(start.checked_add(duration).expect("deadline")), + duration, + } + } + + #[test] + fn image_optimizer_response_processing_rejects_late_handoff() { + for (method, status) in [ + (Method::HEAD, StatusCode::OK), + (Method::GET, StatusCode::NO_CONTENT), + (Method::GET, StatusCode::OK), + ] { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let clock = scripted_clock(vec![start, budget.deadline.instant()]); + let response = FastlyResponse::from_status(status); + let result = block_on(finish_native_fixture( + response, + method, + OutboundProxyMarker::Off, + ResponseDecoding::Decode, + ResponseMode::Streamed, + budget, + 0x0010_0000, + 24, + None, + Some(16), + Some(16), + Some(1024), + Some(10), + native_time(clock.clone()), + &NativeProgress::new(NativeTransport, clock, start, Some(budget.deadline)), + )); + assert!( + matches!( + result, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + ), + "late finalization must not hand off a successful response" + ); + } + } + + #[test] + fn image_optimizer_response_uses_shared_limits_and_lazy_stream() { + use std::sync::atomic::{AtomicUsize, Ordering}; + for (mode, encoded, decoded, expected) in [ + (ResponseMode::Streamed, 8, 8, None), + ( + ResponseMode::Streamed, + 7, + 8, + Some(ResponseLimitReason::EncodedBody), + ), + ( + ResponseMode::Streamed, + 8, + 7, + Some(ResponseLimitReason::DecodedBody), + ), + ( + ResponseMode::Buffered { max_bytes: 7 }, + 8, + 8, + Some(ResponseLimitReason::BufferedBody), + ), + ] { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let samples = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&samples); + let clock = MonotonicClock::new(move || { + observed.fetch_add(1, Ordering::SeqCst); + start + }); + let progress = NativeProgress::new( + NativeTransport, + clock.clone(), + start, + Some(budget.deadline), + ); + let native = FastlyResponse::from_body("12345678"); + let processed = block_on(finish_native_fixture( + native, + Method::GET, + OutboundProxyMarker::Off, + ResponseDecoding::Decode, + mode, + budget, + 0x0010_0000, + 24, + None, + Some(decoded), + Some(encoded), + Some(1024), + Some(10), + native_time(clock.clone()), + &progress, + )); + let outcome = match processed { + Ok(response) => { + assert!(response.body().is_stream()); + assert_eq!( + samples.load(Ordering::SeqCst), + 3, + "one provider and two shared finalization clock checks, without body reads" + ); + block_on(progress.drive(response.into_bytes_bounded(8))) + .expect("callback progress") + } + Err(error) => Err(error), + }; + if let Some(expected_reason) = expected { + assert!( + matches!(outcome, Err(EdgeError::ResponseTooLarge { reason, .. }) if reason == expected_reason) + ); + } else { + assert_eq!(outcome.unwrap(), Bytes::from_static(b"12345678")); + } + } + } + + #[test] + fn method_entry_and_preflight_elapsed_use_the_injected_clock() { + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(9)) + .expect("completed instant"); + let client = HttpClient::with_client(FastlyOutboundClient::with_time(native_time( + scripted_clock(vec![start, completed]), + ))); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + + let results = block_on( + client + .start_batch_until( + vec![request], + Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("batch cutoff"), + ), + ) + .collect(), + ) + .expect("valid batch driver"); + let slot = resolved(&results.slots()[0]); + + assert_eq!(slot.elapsed, Duration::from_millis(9)); + assert!(matches!(slot.outcome, Err(EdgeError::BadRequest { .. }))); + } + + #[test] + fn batch_reports_per_slot_elapsed() { + let start = MonotonicInstant::now(); + let first = start + .checked_add(Duration::from_millis(3)) + .expect("first completion"); + let second = start + .checked_add(Duration::from_millis(8)) + .expect("second completion"); + let client = HttpClient::with_client(FastlyOutboundClient::with_time(native_time( + scripted_clock(vec![start, first, second]), + ))); + let requests = vec![ + OutboundRequest::get("https://example.com/") + .expect("request") + .body("invalid GET body"), + OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(), + ]; + + let results = block_on( + client + .start_batch_until( + requests, + Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("batch cutoff"), + ), + ) + .collect(), + ) + .expect("valid batch driver"); + let first_slot = resolved(&results.slots()[0]); + let second_slot = resolved(&results.slots()[1]); + + assert_eq!(first_slot.elapsed, Duration::from_millis(3)); + assert_eq!(second_slot.elapsed, Duration::from_millis(8)); + assert!( + results + .slots() + .iter() + .map(resolved) + .all(|slot| matches!(slot.outcome, Err(EdgeError::BadRequest { .. }))) + ); + } + + #[test] + fn one_slot_batch_matches_send() { + let now = MonotonicInstant::now(); + let invalid = || { + OutboundRequest::get("https://example.com/") + .expect("request") + .body("invalid GET body") + }; + let single_outcome = block_on( + HttpClient::with_client(FastlyOutboundClient::with_time(native_time( + constant_clock(now), + ))) + .send(invalid()), + ); + let batch = block_on( + HttpClient::with_client(FastlyOutboundClient::with_time(native_time( + constant_clock(now), + ))) + .start_batch_until( + vec![invalid()], + Deadline::at_instant( + now.checked_add(Duration::from_secs(1)) + .expect("batch cutoff"), + ), + ) + .collect(), + ) + .expect("valid batch driver"); + let collected = batch.into_parts().0.into_iter().next().unwrap(); + let edgezero_core::OutboundCollectedSlot::Resolved(slot) = collected else { + panic!("resolved slot"); + }; + let batched_outcome = slot.outcome; + + let (Err(single_error), Err(batched_error)) = (single_outcome, batched_outcome) else { + panic!("single and batch paths must return errors"); + }; + assert_eq!(discriminant(&single_error), discriminant(&batched_error)); + assert_eq!(single_error.status(), batched_error.status()); + assert_eq!(single_error.message(), batched_error.message()); + } + + #[test] + fn preexpired_batch_handoff_retains_earlier_own_timeouts_without_setup() { + use edgezero_core::outbound::{OutboundBatchTermination, OutboundCollectedSlot}; + use std::sync::atomic::{AtomicUsize, Ordering}; + + for selected in [BudgetSource::PerCallTimeout, BudgetSource::RequestDeadline] { + for mixed in [false, true] { + let started_at = MonotonicInstant::now(); + let handoff = started_at.checked_add(Duration::from_millis(150)).unwrap(); + let observation_cutoff = Deadline::at_instant( + started_at.checked_add(Duration::from_millis(100)).unwrap(), + ); + let reads = AtomicUsize::new(0); + let clock = MonotonicClock::new(move || { + if reads.fetch_add(1, Ordering::SeqCst) == 0 { + started_at + } else { + handoff + } + }); + let time = OutboundTime::new(clock, |_, _| { + panic!("preexpired work must not set up a timer") + }); + let client = HttpClient::with_client(FastlyOutboundClient::with_time(time)); + let mut own = OutboundRequest::post("https://preexpired-no-io.example/bid") + .unwrap() + .body("buffered") + .timeout(Duration::from_millis(50)); + if selected == BudgetSource::RequestDeadline { + own = + own.timeout(Duration::from_millis(500)) + .deadline(Deadline::at_instant( + started_at.checked_add(Duration::from_millis(50)).unwrap(), + )); + } + let source_polls = Arc::new(AtomicUsize::new(0)); + let source_reads = Arc::clone(&source_polls); + let requests = if mixed { + let invalid = + OutboundRequest::post("https://preexpired-no-io.example/invalid") + .unwrap() + .body(Body::from_stream(stream::poll_fn(move |_cx| { + source_reads.fetch_add(1, Ordering::SeqCst); + Poll::Pending + }))); + let cutoff_slot = + OutboundRequest::post("https://preexpired-no-io.example/cutoff") + .unwrap() + .body("buffered") + .timeout(Duration::from_millis(500)); + vec![invalid, own, cutoff_slot] + } else { + vec![own] + }; + let backend_count = || { + BACKENDS + .get() + .map_or(0, |registry| registry.lock().expect("backends").len()) + }; + let registered_before = backend_count(); + let result = block_on( + client + .start_batch_until(requests, observation_cutoff) + .collect(), + ) + .expect("earlier own deadlines must resolve before cutoff validation"); + assert_eq!(backend_count(), registered_before); + assert_eq!(source_polls.load(Ordering::SeqCst), 0); + let own_slot = resolved(&result.slots()[usize::from(mixed)]); + assert_eq!(own_slot.elapsed, Duration::from_millis(50)); + assert!( + matches!(own_slot.outcome, Err(EdgeError::GatewayTimeout { cause, .. }) if cause == selected) + ); + if mixed { + assert!(matches!( + resolved(&result.slots()[0]).outcome, + Err(EdgeError::BadRequest { .. }) + )); + assert!(matches!( + result.slots()[2], + OutboundCollectedSlot::Unresolved + )); + assert_eq!(result.termination(), OutboundBatchTermination::Cutoff); + } else { + assert_eq!(result.termination(), OutboundBatchTermination::Completed); + } + } + } + } + + #[test] + fn batch_cutoff_clamping_preserves_cause_and_observation_boundary() { + use super::super::scope; + use edgezero_core::outbound::{ + OutboundBatchTermination, OutboundCollectedSlot, OutboundScopeOwner, + }; + + for selected in [ + BudgetSource::BatchCutoff, + BudgetSource::RequestDeadline, + BudgetSource::PerCallTimeout, + ] { + let start = MonotonicInstant::now(); + let cutoff = Deadline::at_instant( + start + .checked_add(Duration::from_hours(192)) + .expect("eight days"), + ); + for completed_at in [ + start.checked_add(DEADLINE_FAR_FUTURE).expect("seven days"), + cutoff + .instant() + .checked_sub(Duration::from_nanos(1)) + .expect("before cutoff"), + cutoff.instant(), + cutoff + .instant() + .checked_add(Duration::from_nanos(1)) + .expect("after cutoff"), + ] { + let now = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&now); + let clock = MonotonicClock::new(move || *sampled.lock().expect("clock")); + let callback = OutboundScopeOwner::callback(); + let time = native_time(clock); + let progress = + scope::native_progress(&callback.with(scope::capture), &time, start) + .unwrap(); + let client = HttpClient::with_client(FastlyOutboundClient::with_time(time)); + let mut request = OutboundRequest::post("http://127.0.0.1:9/clamped-cutoff") + .expect("request") + .body("pending") + .timeout(DEADLINE_FAR_FUTURE); + if selected == BudgetSource::RequestDeadline { + request = request.deadline(Deadline::at_instant( + start + .checked_add(Duration::from_hours(144)) + .expect("six days"), + )); + } + if selected == BudgetSource::PerCallTimeout { + request = request.timeout(Duration::from_hours(144)); + } + let budget = dispatch_budget(&request, start, Some(cutoff)).expect("budget"); + assert_eq!(budget.cause, selected); + assert!(budget.deadline.instant() < cutoff.instant()); + let batch = callback.with(|| client.start_batch_until(vec![request], cutoff)); + *now.lock().expect("clock") = completed_at; + let result = block_on(callback.run(progress.drive(batch.collect()))) + .expect("callback progress") + .expect("clamped cutoff is not premature EOF"); + + if selected == BudgetSource::BatchCutoff && cutoff.is_expired_at(completed_at) { + assert_eq!(result.termination(), OutboundBatchTermination::Cutoff); + assert!(matches!( + result.slots(), + [OutboundCollectedSlot::Unresolved] + )); + } else { + assert_eq!(result.termination(), OutboundBatchTermination::Completed); + let slot = resolved(&result.slots()[0]); + assert!( + matches!(slot.outcome, Err(EdgeError::GatewayTimeout { cause, .. }) if cause == selected) + ); + let terminal = if selected == BudgetSource::BatchCutoff { + completed_at + } else { + budget.deadline.instant() + }; + assert_eq!(slot.elapsed, terminal.saturating_duration_since(start)); + } + drop(callback); + } + } + } + + #[test] + fn backwards_clock_fails_slot_without_invalid_elapsed() { + let start = MonotonicInstant::now(); + let earlier = start + .checked_sub(Duration::from_millis(1)) + .expect("earlier instant"); + let client = HttpClient::with_client(FastlyOutboundClient::with_time(native_time( + scripted_clock(vec![start, earlier]), + ))); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + + let results = block_on( + client + .start_batch_until( + vec![request], + Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("batch cutoff"), + ), + ) + .collect(), + ) + .expect("typed regression outcome"); + let slot = resolved(&results.slots()[0]); + assert_eq!(slot.elapsed, Duration::ZERO); + assert!(matches!(slot.outcome, Err(EdgeError::Internal { .. }))); + } + + #[test] + fn backwards_clock_cannot_expand_the_selected_budget() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_millis(10)); + let earlier = start + .checked_sub(Duration::from_millis(5)) + .expect("earlier instant"); + let clock = scripted_clock(vec![earlier]); + + assert_eq!( + budget_remaining(budget, &clock).expect("remaining budget"), + budget.duration + ); + } + + #[test] + fn backend_identity_uses_the_method_entry_budget_snapshot() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_millis(10)); + let request = OutboundRequest::get("https://example.com/").expect("request"); + + let (identity, _) = backend_identity(request, budget).expect("backend identity"); + + assert_eq!(identity.phase_ms, 16); + } + + #[test] + fn b31_identity_ignores_shorter_deadlines_with_equal_timeouts() { + let start = MonotonicInstant::now(); + let mut identities = Vec::new(); + for milliseconds in [511_u64, 513, 640, 655] { + let request = OutboundRequest::get("https://example.com/path") + .expect("request") + .timeout(Duration::from_millis(800)) + .deadline(Deadline::at_instant( + start + .checked_add(Duration::from_millis(milliseconds)) + .expect("deadline"), + )); + let budget = dispatch_budget(&request, start, None).expect("selected budget"); + assert_eq!(budget.duration, Duration::from_millis(milliseconds)); + assert_eq!(budget.cause, BudgetSource::RequestDeadline); + identities.push(backend_identity(request, budget).expect("identity").0); + } + let first = identities.first().expect("first identity"); + for identity in &identities { + assert_eq!(identity, first); + assert_eq!(identity.phase_ms, 1024); + } + } + + #[test] + fn b31_timeout_anchor_is_clamped_and_independent_of_cutoff() { + let start = MonotonicInstant::now(); + for timeout in [ + Duration::from_nanos(1), + Duration::from_millis(800), + Duration::from_secs(1), + Duration::from_secs(15), + DEADLINE_FAR_FUTURE, + Duration::MAX, + ] { + let anchor = timeout.min(DEADLINE_FAR_FUTURE); + let mut names = BTreeSet::new(); + for selected in BTreeSet::from([ + Duration::from_nanos(1), + anchor, + anchor + .checked_div(2) + .unwrap_or(anchor) + .max(Duration::from_nanos(1)), + ]) { + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .timeout(timeout); + let observation_cutoff = + Deadline::at_instant(start.checked_add(selected).expect("cutoff")); + let budget = + dispatch_budget(&request, start, Some(observation_cutoff)).expect("budget"); + assert_eq!(budget.duration, selected); + assert_eq!(budget.deadline.instant(), observation_cutoff.instant()); + assert_eq!(budget.cause, BudgetSource::BatchCutoff); + let (identity, parts) = backend_identity(request, budget).expect("identity"); + assert_eq!(parts.timeout, Some(timeout)); + assert!(Duration::from_millis(identity.phase_ms) >= anchor); + assert!(Duration::from_millis(identity.phase_ms) <= DEADLINE_FAR_FUTURE); + names.insert(backend_name(&identity)); + } + assert_eq!(names.len(), 1); + } + } + + #[test] + fn b31_identity_omits_nontransport_policies_and_preserves_owned_parts() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_millis(800)); + let first_request = OutboundRequest::get("https://example.com/first?key=one") + .expect("request") + .timeout(Duration::from_millis(800)); + let second_request = OutboundRequest::post("https://example.com/second?key=two") + .expect("request") + .timeout(Duration::from_millis(800)) + .body("payload") + .max_response_bytes(64) + .proxy_marker(OutboundProxyMarker::Off) + .response_decoding(ResponseDecoding::Original) + .cache_policy(OutboundCachePolicy::Bypass); + let (first, _) = backend_identity(first_request, budget).expect("identity"); + let (second, parts) = backend_identity(second_request, budget).expect("identity"); + assert_eq!(first, second); + assert_eq!(backend_name(&first), backend_name(&second)); + assert_eq!(parts.body.as_bytes(), Some(&b"payload"[..])); + assert_eq!(parts.method, Method::POST); + assert_eq!(parts.proxy_marker, OutboundProxyMarker::Off); + assert_eq!(parts.response_decoding, ResponseDecoding::Original); + assert_eq!( + parts.response_mode, + ResponseMode::Buffered { max_bytes: 64 } + ); + assert_eq!(parts.cache_policy, OutboundCachePolicy::Bypass); + } + + #[test] + fn b31_deadline_only_boundaries_have_thirty_one_configurations() { + let maximum = Duration::from_hours(168); + let ceilings = (0_u32..30) + .map(|shift| Duration::from_millis(1_u64 << shift)) + .chain([maximum]) + .collect::>(); + let mut samples = BTreeSet::from([Duration::from_nanos(1), maximum]); + for ceiling in &ceilings { + for sample in [ + ceiling.checked_sub(Duration::from_nanos(1)), + Some(*ceiling), + ceiling.checked_add(Duration::from_nanos(1)), + ] + .into_iter() + .flatten() + { + if !sample.is_zero() && sample <= maximum { + samples.insert(sample); + } + } + } + assert_eq!(samples.len(), 93); + let start = MonotonicInstant::now(); + for configured in [ + None, + Some(Duration::from_secs(1)), + Some(DEADLINE_FAR_FUTURE), + ] { + let mut names = BTreeSet::new(); + for sample in &samples { + let mut request = + OutboundRequest::get("https://example.com/").expect("request"); + if let Some(connect) = configured { + request = request.connect_timeout(connect); + } + let budget = clock_budget(start, *sample); + let (identity, _) = backend_identity(request, budget).expect("identity"); + let expected = ceilings + .iter() + .find(|ceiling| **ceiling >= *sample) + .expect("ceiling"); + assert_eq!(Duration::from_millis(identity.phase_ms), *expected); + names.insert(backend_name(&identity)); + } + assert_eq!(names.len(), 31, "fixed connect tuple {configured:?}"); + } + } + + #[test] + fn b31_phase_timers_are_coupled_without_a_rigid_split() { + for milliseconds in [1_u64, 2, 4, 128, 1024, 604_800_000] { + let timers = backend_timers(milliseconds); + let expected = Duration::from_millis(milliseconds); + assert_eq!(timers.connect, expected); + assert_eq!(timers.first_byte, expected); + assert_eq!(timers.between_bytes, expected); + } + } + + #[cfg(target_env = "p1")] + #[test] + fn b31_independent_clients_share_verified_backend() { + let start = MonotonicInstant::now(); + let first = FastlyOutboundClient::with_time(native_time(constant_clock(start))); + let second = FastlyOutboundClient::with_time(native_time(constant_clock(start))); + let first_request = OutboundRequest::get("http://127.0.0.1:9/b31-client-reuse") + .expect("request") + .timeout(Duration::from_millis(800)) + .deadline(Deadline::at_instant( + start + .checked_add(Duration::from_millis(511)) + .expect("deadline"), + )); + let second_request = OutboundRequest::get("http://127.0.0.1:9/another-path") + .expect("request") + .timeout(Duration::from_millis(800)) + .deadline(Deadline::at_instant( + start + .checked_add(Duration::from_millis(513)) + .expect("deadline"), + )); + let first_prepared = first + .configure(testing::dispatch(first_request, first.time())) + .expect("first backend"); + let second_prepared = second + .configure(testing::dispatch(second_request, second.time())) + .expect("second backend"); + assert_eq!( + first_prepared.backend.name(), + second_prepared.backend.name() + ); + assert_eq!( + first_prepared.context.budget().duration, + Duration::from_millis(511) + ); + assert_eq!( + second_prepared.context.budget().duration, + Duration::from_millis(513) + ); + assert_eq!( + first_prepared.context.budget().cause, + BudgetSource::RequestDeadline + ); + assert_eq!( + second_prepared.context.budget().cause, + BudgetSource::RequestDeadline + ); + } + + #[cfg(target_env = "p1")] + #[test] + fn b31_registered_transport_preserves_origin_and_wire_authority() { + let start = MonotonicInstant::now(); + let client = FastlyOutboundClient::with_time(native_time(constant_clock(start))); + for (target, override_authority, host, authority, port, tls) in [ + ( + "https://api.example:8443/", + Some("tenant.example:9443"), + "api.example", + "tenant.example:9443", + 8443, + true, + ), + ( + "http://[::1]:8443/", + None, + "[::1]", + "[::1]:8443", + 8443, + false, + ), + ( + "https://127.0.0.1:8443/", + None, + "127.0.0.1", + "127.0.0.1:8443", + 8443, + true, + ), + ] { + let mut request = OutboundRequest::get(target).expect("request"); + if let Some(value) = override_authority { + request = request.host_authority_override(value).expect("authority"); + } + let prepared = client + .configure(testing::dispatch(request, client.time())) + .expect("registered backend"); + assert_eq!(prepared.backend.get_host(), host); + assert_eq!( + prepared.backend.get_host_override(), + Some(HeaderValue::from_str(authority).expect("header")) + ); + assert_eq!(prepared.backend.get_port(), port); + assert_eq!(prepared.backend.is_ssl(), tls); + } + } + + #[test] + fn b31_registry_reuses_matching_identity_and_refuses_a_collision() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let clock = constant_clock(start); + let request = OutboundRequest::get("https://example.com/").expect("request"); + let (identity, _) = backend_identity(request, budget).expect("identity"); + let registry = Mutex::new(HashMap::new()); + let registered = Cell::new(0_usize); + for _ in 0_u8..2 { + let backend = cached_backend( + ®istry, + identity.clone(), + budget, + &clock, + |name, settings| { + assert_eq!(name, backend_name(settings)); + registered.set(registered.get() + 1); + Ok(Ok(7_u8)) + }, + ) + .expect("verified backend"); + assert_eq!(backend, 7); + } + assert_eq!(registered.get(), 1); + let mut collision = identity.clone(); + collision.wire_authority = "other.example".to_owned(); + registry + .lock() + .expect("registry") + .insert(backend_name(&identity), (collision, 9)); + let error = cached_backend(®istry, identity, budget, &clock, |_, _| { + panic!("collision must not register or reuse") + }) + .expect_err("identity collision"); + assert!(matches!(error, EdgeError::Internal { .. })); + } + + #[test] + fn b31_registry_does_not_trust_failed_registrations() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let clock = constant_clock(start); + let request = OutboundRequest::get("https://example.com/").expect("request"); + let (identity, _) = backend_identity(request, budget).expect("identity"); + for failure in [ + BackendCreationError::NameInUse, + BackendCreationError::Disallowed, + ] { + let registry = Mutex::new(HashMap::new()); + cached_backend::(®istry, identity.clone(), budget, &clock, |_, _| { + Ok(Err(failure)) + }) + .expect_err("unverified registration must fail"); + assert!(registry.lock().expect("registry").is_empty()); + let backend = + cached_backend(®istry, identity.clone(), budget, &clock, |_, _| { + Ok(Ok(7)) + }) + .expect("retry may register"); + assert_eq!(backend, 7); + } + } + + #[test] + fn b31_registry_retains_late_success_and_rechecks_cached_deadlines() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let request = OutboundRequest::get("https://example.com/").expect("request"); + let (identity, _) = backend_identity(request, budget).expect("identity"); + let registry = Mutex::new(HashMap::new()); + let clock = scripted_clock(vec![start, budget.deadline.instant()]); + let error = cached_backend(®istry, identity.clone(), budget, &clock, |_, _| { + Ok(Ok(7_u8)) + }) + .expect_err("late registration"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); + assert_eq!(registry.lock().expect("registry").len(), 1); + let expired_clock = constant_clock(budget.deadline.instant()); + cached_backend( + ®istry, + identity.clone(), + budget, + &expired_clock, + |_, _| panic!("expired cache hit must not register"), + ) + .expect_err("cached backend does not renew an expired budget"); + let next_budget = clock_budget(budget.deadline.instant(), Duration::from_secs(1)); + let backend = + cached_backend(®istry, identity, next_budget, &expired_clock, |_, _| { + panic!("successful late registration must already be retained") + }) + .expect("reuse under a new operation's budget"); + assert_eq!(backend, 7); + } + + #[test] + fn prepared_facade_conversion_removes_nominated_encoding_before_provider_default() { + let mut request = OutboundRequest::get("https://example.com/").expect("request"); + request + .headers_mut() + .insert(CONNECTION, HeaderValue::from_static("accept-encoding")); + request + .headers_mut() + .insert(ACCEPT_ENCODING, HeaderValue::from_static("gzip")); + + let time = native_time(MonotonicClock::default()); + let prepared = FastlyOutboundClient::with_time(time.clone()) + .configure(testing::dispatch(request, time)) + .expect("production prepared conversion"); + let native = build_fastly_request( + &prepared.parts.method, + &prepared.parts.uri, + &prepared.parts.headers, + prepared.parts.cache_policy, + ); + + assert!(native.get_header(CONNECTION.as_str()).is_none()); + assert_eq!( + native.get_header(ACCEPT_ENCODING.as_str()), + Some(&HeaderValue::from_static("identity")) + ); + } + + #[test] + fn canonical_uri_wire_serialization_table() { + for raw in [ + "https://example.com", + "https://example.com/a/../b?x=%2F", + "http://127.0.0.1:8080/path?empty=", + "https://[::1]:8443/", + "https://xn--bcher-kva.example/catalog", + ] { + let request = OutboundRequest::get(raw).expect("canonical request"); + let native = build_fastly_request( + request.method(), + request.uri(), + request.headers(), + OutboundCachePolicy::PlatformDefault, + ); + + assert_eq!(native.get_url_str(), request.uri().to_string(), "{raw}"); + } + } + + #[test] + fn backend_builder_keeps_pooling_for_identical_identity_and_settings() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_millis(100)); + let first = OutboundRequest::get("https://example.com/path").expect("first request"); + let second = OutboundRequest::get("https://example.com/other").expect("second request"); + let (first_identity, _) = backend_identity(first, budget).expect("first identity"); + let (second_identity, _) = backend_identity(second, budget).expect("second identity"); + + assert_eq!(first_identity, second_identity); + assert_eq!( + backend_name(&first_identity), + backend_name(&second_identity) + ); + let first_timers = backend_timers(first_identity.phase_ms); + let second_timers = backend_timers(second_identity.phase_ms); + assert_eq!(first_timers.connect, second_timers.connect); + assert_eq!(first_timers.first_byte, second_timers.first_byte); + assert_eq!(first_timers.between_bytes, second_timers.between_bytes); + } + + #[cfg(target_env = "p1")] + #[test] + fn proposed_b31_timer_vectors_register() { + let register = |name: &str, connect, first_byte, between_bytes| { + Backend::builder(name, "127.0.0.1:9") + .connect_timeout(connect) + .first_byte_timeout(first_byte) + .between_bytes_timeout(between_bytes) + .finish() + }; + let ceilings = (0_u32..30).map(|shift| 1_u64 << shift).chain([604_800_000]); + let mut registered = 0_usize; + for milliseconds in ceilings { + let phase = Duration::from_millis(milliseconds); + for (scope, connect) in + [("coupled", phase), ("explicit", Duration::from_millis(200))] + { + let name = format!("ez-b31-probe-{scope}-{milliseconds}"); + let backend = register(&name, connect, phase, phase) + .expect("proposed timer vector must register through the pinned SDK"); + assert_eq!(backend.name(), name); + assert!(backend.exists(), "successful registration must be visible"); + registered += 1; + } + } + assert_eq!(registered, 62); + + let valid = Duration::from_millis(200); + let oversized = Duration::from_millis(u64::from(u32::MAX) + 1); + assert!(matches!( + register("ez-b31-probe-overflow-connect", oversized, valid, valid), + Err(BackendCreationError::ConnectTimeoutTooLarge(value)) if value == oversized + )); + assert!(matches!( + register("ez-b31-probe-overflow-first", valid, oversized, valid), + Err(BackendCreationError::FirstByteTimeoutTooLarge(value)) if value == oversized + )); + assert!(matches!( + register("ez-b31-probe-overflow-between", valid, valid, oversized), + Err(BackendCreationError::BetweenBytesTimeoutTooLarge(value)) if value == oversized + )); + } + + #[test] + fn authority_override_has_a_distinct_backend_identity() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_millis(100)); + let original = OutboundRequest::get("https://origin.example/path").expect("request"); + let overridden = OutboundRequest::get("https://origin.example/path") + .expect("request") + .host_authority_override("tenant.example:8443") + .expect("authority override"); + + let (original_identity, _) = backend_identity(original, budget).expect("identity"); + let (overridden_identity, _) = backend_identity(overridden, budget).expect("identity"); + + assert_eq!(overridden_identity.host, "origin.example"); + assert_eq!(overridden_identity.port, 443); + assert_ne!(original_identity, overridden_identity); + assert_ne!( + backend_name(&original_identity), + backend_name(&overridden_identity) + ); + } + + #[test] + fn ceil_millis_rounds_up_and_saturates_without_wrapping() { + assert_eq!(ceil_millis(Duration::ZERO), 1); + assert_eq!(ceil_millis(Duration::from_nanos(1)), 1); + assert_eq!(ceil_millis(Duration::from_micros(999)), 1); + assert_eq!(ceil_millis(Duration::from_millis(1)), 1); + assert_eq!(ceil_millis(Duration::from_nanos(1_000_001)), 2); + assert_eq!(ceil_millis(Duration::from_micros(1_001)), 2); + assert_eq!(ceil_millis(Duration::MAX), u64::MAX); + } + + #[test] + fn heterogeneous_hosts_and_timer_buckets_have_distinct_backend_identities() { + let started_at = MonotonicInstant::now(); + let first_budget = clock_budget(started_at, Duration::from_millis(1)); + let second_budget = clock_budget(started_at, Duration::from_nanos(1_000_001)); + let first_request = + OutboundRequest::get("https://first.example/path").expect("first request"); + let second_request = + OutboundRequest::get("https://second.example/path").expect("second request"); + + let (first, _) = backend_identity(first_request, first_budget).expect("first identity"); + let (second, _) = + backend_identity(second_request, second_budget).expect("second identity"); + + assert_eq!(first.phase_ms, 1); + assert_eq!(second.phase_ms, 2); + assert_ne!(first, second); + assert_ne!(backend_name(&first), backend_name(&second)); + } + + #[test] + fn constant_clock_can_be_sampled_without_exhaustion() { + let now = MonotonicInstant::now(); + let clock = constant_clock(now); + + assert_eq!(clock.now(), now); + assert_eq!(clock.now(), now); + assert_eq!(clock.now(), now); + } + + #[test] + fn native_body_read_rejects_expiry_before_payload_read() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_millis(10)); + let clock = constant_clock(budget.deadline.instant()); + let mut body = fastly_body_stream(FastlyBody::from("body"), budget, clock); + + let error = block_on(body.next()) + .expect("terminal item") + .expect_err("post-ready expiry"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[test] + fn deadline_stream_rejects_clock_regression_between_chunks_before_polling_source() { + use futures_util::stream::poll_fn as stream_poll_fn; + use std::sync::atomic::{AtomicUsize, Ordering}; + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let observed = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&observed); + let clock = MonotonicClock::new(move || *sampled.lock().expect("clock")); + let source_now = Arc::clone(&observed); + let polls = Arc::new(AtomicUsize::new(0)); + let source_polls = Arc::clone(&polls); + let source = stream_poll_fn(move |_context| { + source_polls.fetch_add(1, Ordering::SeqCst); + *source_now.lock().expect("clock") = + start.checked_add(Duration::from_millis(20)).expect("later"); + Poll::Ready(Some(Ok(Bytes::from_static(b"chunk")))) + }) + .boxed_local(); + let progress = + NativeProgress::new(NativeTransport, clock.clone(), start, Some(budget.deadline)); + let mut guarded = shared_head_stream(source, budget, native_time(clock), progress); + assert_eq!( + block_on(guarded.next()) + .expect("first item") + .expect("first chunk"), + Bytes::from_static(b"chunk") + ); + *observed.lock().expect("clock") = start + .checked_add(Duration::from_millis(15)) + .expect("regression"); + assert!(matches!( + block_on(guarded.next()), + Some(Err(EdgeError::Internal { .. })) + )); + assert_eq!(polls.load(Ordering::SeqCst), 1); + } + + #[test] + fn deadline_stream_checks_clock_after_ready_bytes_and_eof() { + use futures_util::stream::poll_fn as stream_poll_fn; + use std::sync::atomic::{AtomicUsize, Ordering}; + for eof in [false, true] { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let observed = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&observed); + let clock = MonotonicClock::new(move || *sampled.lock().expect("clock")); + let reads = Arc::new(AtomicUsize::new(0)); + let source_reads = Arc::clone(&reads); + let source = stream_poll_fn(move |_context| { + source_reads.fetch_add(1, Ordering::SeqCst); + *observed.lock().expect("clock") = budget.deadline.instant(); + Poll::Ready((!eof).then_some(Ok(Bytes::from_static(b"late")))) + }) + .boxed_local(); + let progress = NativeProgress::new( + NativeTransport, + clock.clone(), + start, + Some(budget.deadline), + ); + let mut guarded = shared_head_stream(source, budget, native_time(clock), progress); + assert!(matches!( + block_on(guarded.next()), + Some(Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + })) + )); + assert_eq!( + reads.load(Ordering::SeqCst), + 1, + "must sample after source completion" + ); + } + } + + #[test] + fn deadline_stream_preserves_source_detail_and_releases_before_terminal_yield() { + for expired_before_poll in [false, true] { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_millis(10)); + let observed = Arc::new(Mutex::new(if expired_before_poll { + budget.deadline.instant() + } else { + start + })); + let source_now = Arc::clone(&observed); + let clock_now = Arc::clone(&observed); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock")); + let retained = Arc::new(()); + let source_resource = Arc::clone(&retained); + let detail = ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: FastlyFailureCause::IoError { + kind: ErrorKind::UnexpectedEof, + }, + }; + let source = stream! { + let owned_until_source_drop = source_resource; + *source_now.lock().expect("clock") = budget.deadline.instant(); + yield Err(EdgeError::bad_gateway_with_detail("fixture source failed", BadGatewayReason::Transport, detail)); + drop(owned_until_source_drop); + }.boxed_local(); + let progress = NativeProgress::new( + NativeTransport, + clock.clone(), + start, + Some(budget.deadline), + ); + let mut guarded = shared_head_stream(source, budget, native_time(clock), progress); + let error = block_on(guarded.next()) + .expect("terminal item") + .expect_err("deadline"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); + assert_eq!( + error.provider_failure_detail(), + (!expired_before_poll).then_some(&detail) + ); + assert_eq!( + Arc::strong_count(&retained), + 1, + "terminal stream must not retain source ownership" + ); + assert!(block_on(guarded.next()).is_none()); + assert_eq!(Arc::strong_count(&retained), 1); + } + } + + #[test] + fn converted_response_retains_injected_clock() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let observed = Arc::new(Mutex::new(start)); + let clock_observed = Arc::clone(&observed); + let clock = + MonotonicClock::new(move || *clock_observed.lock().expect("clock observation")); + let native = FastlyResponse::from_status(StatusCode::NO_CONTENT.as_u16()); + let response = block_on(finish_native_fixture( + native, + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Streamed, + budget, + 32 * 1024 * 1024, + 24, + None, + None, + None, + None, + None, + native_time(clock.clone()), + &NativeProgress::new(NativeTransport, clock, start, Some(budget.deadline)), + )) + .expect("converted response"); + *observed.lock().expect("clock observation") = budget.deadline.instant(); + + let error = block_on(response.into_bytes_bounded_until(1, budget.deadline)) + .expect_err("retained clock reached deadline"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + type ContentTypeCase = (&'static [&'static [u8]], bool, ContentTypeProvenance, usize); + + fn content_type_cases() -> &'static [ContentTypeCase] { + use ContentTypeIneligibility::{Missing, MultipleValues, NonText, Removed}; + use ContentTypeProvenance::{Ineligible, SingleTextUnchanged}; + + &[ + (&[], false, Ineligible(Missing), 0), + (&[b"text/plain"], false, SingleTextUnchanged, 1), + ( + &[b"text/plain; label=caf\xc3\xa9"], + false, + SingleTextUnchanged, + 1, + ), + ( + &[b"text/plain", b"text/plain"], + false, + Ineligible(MultipleValues), + 2, + ), + ( + &[b"text/plain", b"application/json"], + false, + Ineligible(MultipleValues), + 2, + ), + ( + &[b"text/plain", b"\xff"], + false, + Ineligible(MultipleValues), + 1, + ), + (&[b"\xff"], false, Ineligible(NonText), 0), + ( + &[b"text/plain, application/json"], + false, + SingleTextUnchanged, + 1, + ), + (&[b"not a MIME type"], false, SingleTextUnchanged, 1), + (&[b"text/plain"], true, Ineligible(Removed), 0), + ] + } + + #[test] + fn converted_response_preserves_original_content_type_provenance() { + use ContentTypeIneligibility::Modified; + use ContentTypeProvenance::{Ineligible, SingleTextUnchanged}; + + for &(values, nominated, expected, retained_count) in content_type_cases() { + for status in [ + StatusCode::OK, + StatusCode::NO_CONTENT, + StatusCode::RESET_CONTENT, + ] { + for mode in [ + ResponseMode::Streamed, + ResponseMode::Buffered { max_bytes: 64 }, + ] { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let mut native = FastlyResponse::from_status(status.as_u16()); + for value in values { + native.append_header(CONTENT_TYPE, *value); + } + if nominated { + native.set_header(CONNECTION, "content-type"); + } + let mut response = block_on(finish_native_fixture( + native, + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + mode, + budget, + 1024, + 24, + None, + None, + None, + None, + None, + native_time(constant_clock(start)), + &NativeProgress::new( + NativeTransport, + constant_clock(start), + start, + Some(budget.deadline), + ), + )) + .expect("converted response"); + + assert_eq!(response.content_type_provenance(), expected); + assert_eq!( + response.headers().get_all(CONTENT_TYPE).iter().count(), + retained_count + ); + if expected == SingleTextUnchanged { + assert_eq!( + response + .headers() + .get(CONTENT_TYPE) + .map(HeaderValue::as_bytes), + values.first().copied() + ); + } + response + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + let after_mutation = if expected == SingleTextUnchanged { + Ineligible(Modified) + } else { + expected + }; + assert_eq!(response.content_type_provenance(), after_mutation); + } + } + } + } + + #[test] + fn expired_native_body_read_does_not_accept_eof() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_millis(10)); + let clock = constant_clock(budget.deadline.instant()); + let mut body = fastly_body_stream(FastlyBody::new(), budget, clock); + + assert!(matches!( + block_on(body.next()), + Some(Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + })) + )); + } + + #[test] + fn decoder_stalls_timeout_at_all_completion_boundaries() { + let started_at = MonotonicInstant::now(); + let budget = clock_budget(started_at, Duration::from_millis(100)); + let mut gzip_encoder = GzEncoder::new(Vec::new(), Compression::default()); + gzip_encoder.write_all(b"ab").expect("gzip input"); + let gzip_bytes = gzip_encoder.finish().expect("gzip output"); + let mut native = FastlyResponse::from_status(StatusCode::OK.as_u16()); + native.set_header(CONTENT_ENCODING, "gzip"); + native.set_body(gzip_bytes); + + let progress = NativeProgress::new( + NativeTransport, + MonotonicClock::default(), + started_at, + Some(budget.deadline), + ); + let response = block_on(finish_native_fixture( + native, + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Streamed, + budget, + 32 * 1024 * 1024, + 24, + NonZeroU64::new(1), + None, + None, + None, + None, + native_time(MonotonicClock::default()), + &progress, + )) + .expect("response head"); + let mut body = response.into_body().into_stream().expect("streamed body"); + + assert_eq!( + block_on(progress.drive(body.next())) + .expect("callback progress") + .expect("first item") + .expect("first byte"), + Bytes::from_static(b"a") + ); + thread::sleep(Duration::from_millis(125)); + assert!(matches!( + block_on(progress.drive(body.next())).expect("callback progress"), + Some(Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + })) + )); + } + + #[test] + fn response_content_length_rejects_before_body_poll() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let mut native = FastlyResponse::from_status(StatusCode::OK.as_u16()); + native.set_header(CONTENT_LENGTH, "2"); + native.set_body("xx"); + + let error = block_on(finish_native_fixture( + native, + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Buffered { max_bytes: 1 }, + budget, + 32 * 1024 * 1024, + 24, + None, + None, + None, + None, + None, + native_time(constant_clock(start)), + &NativeProgress::new( + NativeTransport, + constant_clock(start), + start, + Some(budget.deadline), + ), + )) + .expect_err("content length exceeds final buffer cap"); + + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::BufferedBody, + .. + } + )); + } + + #[test] + fn response_content_length_explicit_identity_rejects_before_body_poll() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let mut native = FastlyResponse::from_status(StatusCode::OK.as_u16()); + native.set_header(CONTENT_ENCODING, "identity"); + native.set_header(CONTENT_LENGTH, "2"); + native.set_body("xx"); + + let error = block_on(finish_native_fixture( + native, + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Buffered { max_bytes: 16 }, + budget, + 32 * 1024 * 1024, + 24, + None, + Some(1), + None, + None, + None, + native_time(constant_clock(start)), + &NativeProgress::new( + NativeTransport, + constant_clock(start), + start, + Some(budget.deadline), + ), + )) + .expect_err("content length exceeds decoded cap"); + + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::DecodedBody, + .. + } + )); + } + + #[derive(Default)] + struct UploadEvents { + finishes: usize, + flushes: usize, + waits: usize, + writes: Vec, + } + + struct TestUploadWriter { + events: Arc>, + } + + impl StreamedUploadWriter for TestUploadWriter { + fn finish(self) -> IoResult<()> { + let mut events = self + .events + .lock() + .map_err(|_poisoned| Error::other("upload events lock poisoned"))?; + events.finishes = events.finishes.saturating_add(1); + Ok(()) + } + + fn flush(&mut self) -> IoResult<()> { + let mut events = self + .events + .lock() + .map_err(|_poisoned| Error::other("upload events lock poisoned"))?; + events.flushes = events.flushes.saturating_add(1); + Ok(()) + } + + fn write_all(&mut self, bytes: &[u8]) -> IoResult<()> { + self.events + .lock() + .map_err(|_poisoned| Error::other("upload events lock poisoned"))? + .writes + .push(Bytes::copy_from_slice(bytes)); + Ok(()) + } + } + + #[test] + fn streamed_upload_overflow_never_finishes_or_waits() { + let start = MonotonicInstant::now(); + let budget = clock_budget(start, Duration::from_secs(1)); + let clock = constant_clock(start); + for len in [7_usize, 8, 9] { + let events = Arc::new(Mutex::new(UploadEvents::default())); + let writer = TestUploadWriter { + events: Arc::clone(&events), + }; + let source = stream::iter([ + Ok(Bytes::from_static(b"xxxx")), + Ok(Bytes::from(vec![b'x'; len - 4])), + ]) + .boxed_local(); + let wait_events = Arc::clone(&events); + let result = block_on(complete_streamed_exchange( + writer, + source, + 8, + budget, + &clock, + move || { + wait_events.lock().expect("events").waits += 1; + Ok(7_u8) + }, + )); + let observed = events.lock().expect("events"); + if len > 8 { + let error = result.expect_err("upload overflow"); + assert_eq!(error.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!( + error.request_body_overflow_origin(), + Some(edgezero_core::RequestBodyOverflowOrigin::OutboundUpload) + ); + assert_eq!(observed.finishes, 0); + assert_eq!(observed.waits, 0); + assert_eq!(observed.writes, [Bytes::from_static(b"xxxx")]); + } else { + result.expect("within cap"); + assert_eq!(observed.finishes, 1); + assert_eq!(observed.waits, 1); + } + } + } + + #[test] + fn streamed_upload_finishes_exactly_once() { + let started_at = MonotonicInstant::now(); + let budget = clock_budget(started_at, Duration::from_secs(1)); + let clock = constant_clock(started_at); + let events = Arc::new(Mutex::new(UploadEvents::default())); + let writer = TestUploadWriter { + events: Arc::clone(&events), + }; + let source = stream::iter([ + Ok(Bytes::from_static(b"first")), + Ok(Bytes::from_static(b"second")), + ]) + .boxed_local(); + let wait_events = Arc::clone(&events); + + let outcome = block_on(complete_streamed_exchange( + writer, + source, + 32, + budget, + &clock, + move || { + wait_events.lock().expect("upload events").waits += 1; + Ok(7_u8) + }, + )); + let observed = events.lock().expect("upload events"); + + assert_eq!(outcome.expect("exchange"), 7); + assert_eq!( + observed.writes, + [Bytes::from_static(b"first"), Bytes::from_static(b"second")] + ); + assert_eq!(observed.flushes, 2); + assert_eq!(observed.finishes, 1); + assert_eq!(observed.waits, 1); + } + + #[test] + fn streamed_upload_failure_never_waits() { + let started_at = MonotonicInstant::now(); + let budget = clock_budget(started_at, Duration::from_secs(1)); + let clock = constant_clock(started_at); + let events = Arc::new(Mutex::new(UploadEvents::default())); + let writer = TestUploadWriter { + events: Arc::clone(&events), + }; + let source = stream::iter([Err(EdgeError::bad_gateway_with_reason( + "source failed", + BadGatewayReason::Transport, + ))]) + .boxed_local(); + let wait_events = Arc::clone(&events); + + let outcome = block_on(complete_streamed_exchange( + writer, + source, + 32, + budget, + &clock, + move || { + wait_events.lock().expect("upload events").waits += 1; + Ok(7_u8) + }, + )); + let observed = events.lock().expect("upload events"); + + assert!(matches!( + outcome, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Transport, + .. + }) + )); + assert!(observed.writes.is_empty()); + assert_eq!(observed.finishes, 0); + assert_eq!(observed.waits, 0); + } + + #[test] + fn backend_creation_error_table_is_exhaustive() { + use fastly_shared::FastlyStatus; + + let mapped = map_backend_creation_error(&BackendCreationError::Disallowed); + assert_eq!(mapped.status(), StatusCode::BAD_GATEWAY); + assert!(matches!( + mapped, + EdgeError::BadGateway { + message, + reason: BadGatewayReason::Unspecified, + .. + } if message == DYNAMIC_BACKENDS_DISABLED_MESSAGE + )); + + for local_invariant in [ + BackendCreationError::BetweenBytesTimeoutTooLarge(Duration::from_secs(1)), + BackendCreationError::ConnectTimeoutTooLarge(Duration::from_secs(1)), + BackendCreationError::EncodingError( + String::from_utf8(vec![0xff]).expect_err("invalid UTF-8"), + ), + BackendCreationError::FirstByteTimeoutTooLarge(Duration::from_secs(1)), + BackendCreationError::InvalidHealthcheckValue("invalid".to_owned()), + BackendCreationError::NameTooLong("x".to_owned()), + BackendCreationError::NameInUse, + ] { + assert!(matches!( + map_backend_creation_error(&local_invariant), + EdgeError::Internal { .. } + )); + } + + assert!(matches!( + map_backend_creation_error(&BackendCreationError::HostError(FastlyStatus::ERROR)), + EdgeError::BadGateway { + reason: BadGatewayReason::Unspecified, + .. + } + )); + } + + #[test] + #[expect( + clippy::too_many_lines, + reason = "one exhaustive pinned SDK table checks all causes and unchanged wire behavior" + )] + fn provider_diagnostics_cover_send_causes_without_changing_public_errors() { + use edgezero_core::error_detail::{ + FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase, + }; + use edgezero_core::response::IntoResponse as _; + use fastly_shared::FastlyStatus; + + macro_rules! unit_causes { + ($($variant:ident),* $(,)?) => { vec![$((SendErrorCause::$variant, FastlyFailureCause::$variant)),*] }; + } + let mut cases = unit_causes![ + DnsTimeout, + DestinationNotFound, + DestinationUnavailable, + DestinationIpUnroutable, + ConnectionRefused, + ConnectionTerminated, + ConnectionTimeout, + ConnectionLimitReached, + TlsProtocolError, + TlsCertificateError, + TlsConfigurationError, + HttpIncompleteResponse, + HttpResponseHeaderSectionTooLarge, + HttpResponseBodyTooLarge, + HttpResponseTimeout, + HttpResponseStatusInvalid, + HttpUpgradeFailed, + HttpProtocolError, + HttpRequestCacheKeyInvalid, + HttpRequestUriInvalid, + HttpCacheLimitExceeded, + HttpCacheApiUnsupported, + FanoutNotEnabled, + ImageOptimizerUnsupported, + RequestCollapse, + ]; + cases.extend([ + ( + SendErrorCause::DnsError { + rcode: Some(u16::MAX), + info_code: Some(u16::MAX), + }, + FastlyFailureCause::DnsError { + rcode: Some(u16::MAX), + info_code: Some(u16::MAX), + }, + ), + ( + SendErrorCause::DnsError { + rcode: None, + info_code: None, + }, + FastlyFailureCause::DnsError { + rcode: None, + info_code: None, + }, + ), + ( + SendErrorCause::TlsAlertReceived { + alert_id: Some(u8::MAX), + }, + FastlyFailureCause::TlsAlertReceived { + alert_id: Some(u8::MAX), + }, + ), + ( + SendErrorCause::TlsAlertReceived { alert_id: None }, + FastlyFailureCause::TlsAlertReceived { alert_id: None }, + ), + ( + SendErrorCause::Http2StreamError { + frame_type: u8::MAX, + error_code: u32::MAX, + }, + FastlyFailureCause::Http2StreamError { + frame_type: u8::MAX, + error_code: u32::MAX, + }, + ), + ( + SendErrorCause::InternalError(Some(FastlyStatus::ERROR)), + FastlyFailureCause::InternalError { + status: Some(FastlyStatus::ERROR.code), + }, + ), + ( + SendErrorCause::InternalError(None), + FastlyFailureCause::InternalError { status: None }, + ), + ( + SendErrorCause::IoError(Error::new( + ErrorKind::UnexpectedEof, + "provider-secret-token", + )), + FastlyFailureCause::IoError { + kind: ErrorKind::UnexpectedEof, + }, + ), + ( + SendErrorCause::Custom(anyhow::anyhow!( + "https://example.com/?token=provider-secret-token" + )), + FastlyFailureCause::Custom, + ), + ]); + let now = MonotonicInstant::now(); + let clock = constant_clock(now); + for (source, expected) in cases { + for expired in [false, true] { + let mut budget = clock_budget(now, Duration::from_secs(1)); + if expired { + budget.deadline = Deadline::at_instant(now); + } + let plain = + super::super::classify_send_failure(cause_to_failure(&source), budget, now); + let mapped = map_send_cause(&source, budget, &clock); + assert_eq!( + mapped.provider_failure_detail(), + Some(&ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::Send, + cause: expected, + }) + ); + assert_eq!(mapped.message(), plain.message()); + assert_eq!(mapped.to_string(), plain.to_string()); + assert_eq!(mapped.status(), plain.status()); + assert_eq!(mapped.kind(), plain.kind()); + assert!( + !format!("{:?}", mapped.provider_failure_detail()) + .contains("provider-secret-token") + ); + let before = plain.into_response().expect("plain wire"); + let after = mapped.into_response().expect("mapped wire"); + assert_eq!(before.status(), after.status()); + assert_eq!(before.headers(), after.headers()); + assert_eq!(before.body().as_bytes(), after.body().as_bytes()); + } + } + } + + #[test] + fn provider_diagnostics_keep_backend_registration_and_upload_io() { + use edgezero_core::error_detail::{ + FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase, + }; + use fastly_shared::FastlyStatus; + + let backend = + map_backend_creation_error(&BackendCreationError::HostError(FastlyStatus::ERROR)); + assert_eq!( + backend.provider_failure_detail(), + Some(&ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::BackendRegistration, + cause: FastlyFailureCause::BackendHostError { + status: FastlyStatus::ERROR.code + }, + }) + ); + let now = MonotonicInstant::now(); + let error = resolve_stream_io_result( + Err(Error::new(ErrorKind::BrokenPipe, "provider-secret-token")), + clock_budget(now, Duration::from_secs(1)), + now, + "Fastly outbound request body write failed", + ) + .expect_err("upload failure"); + assert_eq!( + error.provider_failure_detail(), + Some(&ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::RequestBodyUpload, + cause: FastlyFailureCause::IoError { + kind: ErrorKind::BrokenPipe + }, + }) + ); + assert_eq!(error.message(), "Fastly outbound request body write failed"); + } + + #[test] + fn backend_creation_failure_observed_at_deadline_is_a_timeout() { + let started_at = MonotonicInstant::now(); + let budget = super::super::test_budget(started_at, BudgetSource::BatchCutoff); + let retained = Cell::new(false); + let clock = scripted_clock(vec![budget.deadline.instant()]); + + let error = finish_backend_creation::<(), _>( + Err(BackendCreationError::Disallowed), + budget, + &clock, + |_backend| { + retained.set(true); + Ok(()) + }, + ) + .expect_err("deadline must outrank backend failure"); + + assert!(!retained.get()); + assert_eq!( + error.provider_failure_detail(), + Some(&ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::BackendRegistration, + cause: FastlyFailureCause::BackendDisallowed, + }) + ); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::BatchCutoff, + .. + } + )); + } + + #[test] + fn successful_backend_creation_is_retained_before_late_timeout() { + let started_at = MonotonicInstant::now(); + let budget = super::super::test_budget(started_at, BudgetSource::BatchCutoff); + let retained = Cell::new(false); + let observed_at = Arc::new(Mutex::new(started_at)); + let clock_observation = Arc::clone(&observed_at); + let clock = + MonotonicClock::new(move || *clock_observation.lock().expect("clock observation")); + + let error = finish_backend_creation(Ok(7_u8), budget, &clock, |backend| { + retained.set(true); + *observed_at.lock().expect("clock observation") = budget.deadline.instant(); + Ok(backend) + }) + .expect_err("late success must still return the attributed timeout"); + + assert!(retained.get()); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::BatchCutoff, + .. + } + )); + } + + #[test] + fn late_timeout_outranks_retention_error_after_success_is_retained() { + let started_at = MonotonicInstant::now(); + let budget = super::super::test_budget(started_at, BudgetSource::BatchCutoff); + let retained = Cell::new(false); + let observed_at = Arc::new(Mutex::new(started_at)); + let clock_observation = Arc::clone(&observed_at); + let clock = + MonotonicClock::new(move || *clock_observation.lock().expect("clock observation")); + + let error = finish_backend_creation(Ok(7_u8), budget, &clock, |_backend| { + retained.set(true); + *observed_at.lock().expect("clock observation") = budget.deadline.instant(); + Err(EdgeError::internal(anyhow::anyhow!("retention failed"))) + }) + .expect_err("late timeout must outrank retention failure"); + + assert!(retained.get()); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::BatchCutoff, + .. + } + )); + } + + #[test] + fn streamed_write_failure_observed_at_expiry_is_a_timeout() { + let started_at = MonotonicInstant::now(); + let budget = super::super::test_budget(started_at, BudgetSource::PerCallTimeout); + + let error = resolve_stream_io_result( + Err(Error::other("write failed")), + budget, + budget.deadline.instant(), + "Fastly outbound request body write failed", + ) + .expect_err("deadline must outrank transport failure"); + + assert_eq!( + error.provider_failure_detail(), + Some(&ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::RequestBodyUpload, + cause: FastlyFailureCause::IoError { + kind: ErrorKind::Other + }, + }) + ); + + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); + } + + #[test] + fn backend_identity_uses_every_canonical_property() { + let first = BackendIdentity { + phase_ms: 128, + connect_ms: None, + host: "example.com".to_owned(), + port: 443, + scheme: "https".to_owned(), + tls: true, + wire_authority: "example.com".to_owned(), + }; + assert_eq!( + backend_name(&first), + "ez_b31v1_a165932b532b7ff063ee219444cb3cb093847ee427d4f76b54a68909b2e279fc" + ); + let mut second = first.clone(); + second.phase_ms = 256; + assert_ne!(backend_name(&first), backend_name(&second)); + second = first.clone(); + second.port = 8443; + assert_ne!(backend_name(&first), backend_name(&second)); + second = first.clone(); + second.host = "other.example".to_owned(); + assert_ne!(backend_name(&first), backend_name(&second)); + second = first.clone(); + second.scheme = "http".to_owned(); + assert_ne!(backend_name(&first), backend_name(&second)); + second = first.clone(); + second.tls = false; + assert_ne!(backend_name(&first), backend_name(&second)); + second = first.clone(); + second.wire_authority = "tenant.example:8443".to_owned(); + assert_ne!(backend_name(&first), backend_name(&second)); + } + + #[test] + fn backend_authority_preserves_explicit_port() { + let mut request = + OutboundRequest::get("https://api.example.com:8443/resource").expect("request"); + normalize_for_dispatch(&mut request).expect("normalize"); + + let (identity, _) = backend_identity( + request, + clock_budget(MonotonicInstant::now(), Duration::from_secs(1)), + ) + .expect("identity"); + assert_eq!(identity.wire_authority, "api.example.com:8443"); + } + + #[test] + fn backend_authority_preserves_bracketed_ipv6() { + let mut request = OutboundRequest::get("http://[::1]:8443/resource").expect("request"); + normalize_for_dispatch(&mut request).expect("normalize"); + + let (identity, _) = backend_identity( + request, + clock_budget(MonotonicInstant::now(), Duration::from_secs(1)), + ) + .expect("identity"); + assert_eq!(identity.wire_authority, "[::1]:8443"); + } + + #[test] + fn dispatch_accepts_live_budget_after_preparation() { + use edgezero_core::time::Deadline; + + let started_at = MonotonicInstant::now(); + let duration = Duration::from_secs(1); + let budget = DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant( + started_at.checked_add(duration).expect("deadline instant"), + ), + duration, + }; + let observed_at = started_at + .checked_add(Duration::from_millis(26)) + .expect("preparation time"); + check_dispatch_budget_at(started_at, budget, observed_at) + .expect("preparation does not expire a live absolute budget"); + + let expired_budget = DispatchBudget { + deadline: Deadline::at_instant(observed_at), + ..budget + }; + assert!(matches!( + check_dispatch_budget_at(started_at, expired_budget, observed_at), + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + )); + } + + #[test] + fn dispatch_budget_rejects_backwards_clock() { + let started_at = MonotonicInstant::now(); + let earlier = started_at.checked_sub(Duration::from_nanos(1)).unwrap(); + let budget = clock_budget(started_at, Duration::from_millis(500)); + let error = ensure_dispatch_budget(started_at, budget, &constant_clock(earlier)) + .expect_err("backwards clock must fail before SDK dispatch"); + assert!(matches!( + error, + EdgeError::Internal { source, .. } + if source.to_string() == "monotonic clock moved backwards before Fastly SDK dispatch" + )); + } + + #[test] + fn get_head_empty_settlement_after_26ms_reaches_sdk_dispatch() { + use futures_util::future::pending; + use futures_util::task::{ArcWake, waker}; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::Context; + + struct DispatchWake; + impl ArcWake for DispatchWake { + fn wake(self: Arc) { + Self::wake_by_ref(&self); + } + + fn wake_by_ref(_arc_self: &Arc) {} + } + + for method in [Method::GET, Method::HEAD] { + let started_at = MonotonicInstant::now(); + let settled_at = started_at.checked_add(Duration::from_millis(26)).unwrap(); + let deadline = started_at.checked_add(Duration::from_millis(500)).unwrap(); + let observed = Arc::new(Mutex::new(started_at)); + let clock_now = Arc::clone(&observed); + let clock = MonotonicClock::new(move || *clock_now.lock().unwrap()); + let timers = Arc::new(Mutex::new(Vec::new())); + let registered = Arc::clone(&timers); + let time = OutboundTime::new(clock.clone(), move |bound, _| { + registered.lock().unwrap().push(bound.instant()); + Box::pin(pending()) + }); + let polls = Arc::new(AtomicUsize::new(0)); + let source_polls = Arc::clone(&polls); + let source = stream::poll_fn(move |_cx| { + source_polls.fetch_add(1, Ordering::SeqCst); + *observed.lock().unwrap() = settled_at; + Poll::Ready(None) + }); + let request = OutboundRequest::new( + method, + "http://127.0.0.1:9/settled-empty-dispatch".parse().unwrap(), + ) + .unwrap() + .body(Body::from_stream(source)) + .timeout(Duration::from_millis(500)); + let dispatch = testing::dispatch(request, time.clone()); + assert_eq!(polls.load(Ordering::SeqCst), 1); + assert_eq!(*timers.lock().unwrap(), vec![deadline]); + assert_eq!(dispatch.entered_at(), started_at); + assert_eq!(dispatch.budget().deadline.instant(), deadline); + assert_eq!(dispatch.budget().duration, Duration::from_millis(500)); + assert_eq!(dispatch.budget().cause, BudgetSource::PerCallTimeout); + assert!(!dispatch.request().is_stream_body()); + assert_eq!(clock.now(), settled_at); + let client = FastlyOutboundClient::with_time(time); + let prepared = client.configure(dispatch).expect("live backend setup"); + assert_eq!(prepared.context.mode(), OutboundSendMode::Send); + let progress = NativeProgress::new(NativeTransport, clock, started_at, None); + let mut head = Box::pin(client.execute_head(prepared, &progress)); + let signal = Arc::new(DispatchWake); + let waiting = waker(Arc::clone(&signal)); + let references = Arc::strong_count(&signal); + let observation = progress + .with_current(|| head.as_mut().poll(&mut Context::from_waker(&waiting))); + assert!( + observation.is_pending(), + "SDK dispatch must issue a pending handle: {observation:?}" + ); + assert_eq!(Arc::strong_count(&signal), references + 1); + drop(head); + assert_eq!( + Arc::strong_count(&signal), + references, + "dropping the future must release its pending SDK lease" + ); + drop(progress); + } + } + + #[test] + fn invocation_runtime_precedes_settlement_and_survives_foreign_body_close() { + use super::super::progress::wait_current; + use super::super::scope; + use edgezero_core::{OutboundScopeKind, OutboundScopeOwner}; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::{Context, Waker}; + + struct TimerOwner(Arc); + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for TimerOwner { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + let callback = OutboundScopeOwner::callback(); + let installed = Arc::new(AtomicUsize::new(0)); + let released = Arc::new(AtomicUsize::new(0)); + let armed = Arc::clone(&installed); + let retired = Arc::clone(&released); + let time = OutboundTime::new(MonotonicClock::default(), move |deadline, clock| { + armed.fetch_add(1, Ordering::SeqCst); + let owner = TimerOwner(Arc::clone(&retired)); + let timer = wait_current(deadline, clock); + Box::pin(async move { + let _owner = owner; + timer.await + }) + }); + let reads = Rc::new(Cell::new(0_usize)); + let source_reads = Rc::clone(&reads); + let source = stream::poll_fn(move |_cx| { + assert_eq!(scope::capture().kind(), Some(OutboundScopeKind::Callback)); + source_reads.set(source_reads.get().saturating_add(1)); + Poll::Ready(None) + }); + let client = HttpClient::with_client(FastlyOutboundClient::with_time(time)); + let request = OutboundRequest::get("http://127.0.0.1:9/captured-settlement") + .unwrap() + .body(Body::from_stream(source)) + .timeout(Duration::from_millis(500)); + let mut send = callback.with(|| client.send(request)); + let body = callback.with(OutboundScopeOwner::response_body); + let first = body.with(|| send.as_mut().poll(&mut Context::from_waker(Waker::noop()))); + assert!( + first.is_pending(), + "native timer context must precede settlement: {first:?}" + ); + assert_eq!(reads.get(), 1); + assert_eq!(installed.load(Ordering::SeqCst), 2); + assert_eq!( + released.load(Ordering::SeqCst), + 1, + "only settlement has finished" + ); + assert!( + !scope::capture().is_managed(), + "activation cannot leak between polls" + ); + body.close(); + assert_eq!( + released.load(Ordering::SeqCst), + 1, + "body cleanup cannot cancel callback work" + ); + callback.close(); + assert_eq!( + released.load(Ordering::SeqCst), + 2, + "callback releases the active request timer" + ); + let error = block_on(send).unwrap_err(); + assert!( + matches!(error, EdgeError::Internal { source: reason, .. } if reason.to_string() == "outbound Callback scope closed") + ); + } + + #[test] + fn scoped_begin_readiness_never_polls_the_sdk() { + use edgezero_core::OutboundScopeOwner; + + let callback = OutboundScopeOwner::callback(); + let client = HttpClient::with_client(FastlyOutboundClient::new()); + NATIVE_HEAD_POLLS.set(0); + let mut pending = callback + .with(|| { + block_on( + client.begin( + OutboundRequest::post("http://127.0.0.1:9/scoped-ready") + .unwrap() + .body("buffered") + .timeout(Duration::from_millis(500)), + ), + ) + }) + .unwrap(); + assert_eq!( + NATIVE_HEAD_POLLS.get(), + 0, + "begin only issues the SDK request" + ); + let body = callback.with(OutboundScopeOwner::response_body); + assert!(!body.with(|| pending.is_ready())); + assert_eq!( + NATIVE_HEAD_POLLS.get(), + 0, + "readiness cannot turn the SDK driver" + ); + body.close(); + assert!(!pending.is_ready()); + assert_eq!(NATIVE_HEAD_POLLS.get(), 0); + callback.close(); + assert!(pending.is_ready()); + assert!(matches!( + block_on(pending.collect()), + Err(EdgeError::Internal { .. }) + )); + assert_eq!(NATIVE_HEAD_POLLS.get(), 0); + } + + #[test] + fn scoped_batch_check_never_turns_the_sdk() { + use edgezero_core::{OutboundBatchCheck, OutboundScopeOwner}; + + let callback = OutboundScopeOwner::callback(); + let client = HttpClient::with_client(FastlyOutboundClient::new()); + NATIVE_SELECTIONS.set(0); + NATIVE_HEAD_POLLS.set(0); + let mut batch = callback.with(|| { + client.start_batch_until( + vec![ + OutboundRequest::post("http://127.0.0.1:9/scoped-check") + .unwrap() + .body("buffered") + .timeout(Duration::from_secs(1)), + ], + Deadline::after(Duration::from_millis(500)), + ) + }); + assert_eq!(NATIVE_SELECTIONS.get(), 0, "eager batch setup never waits"); + let body = callback.with(OutboundScopeOwner::response_body); + assert!(matches!( + body.with(|| batch.check()).unwrap(), + OutboundBatchCheck::Pending + )); + assert_eq!( + NATIVE_SELECTIONS.get(), + 0, + "batch check cannot enter a native wait" + ); + assert_eq!(NATIVE_HEAD_POLLS.get(), 0); + body.close(); + assert!(matches!( + batch.check().unwrap(), + OutboundBatchCheck::Pending + )); + callback.close(); + assert!(matches!(batch.check(), Err(EdgeError::Internal { .. }))); + assert_eq!(NATIVE_SELECTIONS.get(), 0); + assert_eq!(NATIVE_HEAD_POLLS.get(), 0); + } + + #[test] + fn cold_backend_registration_uses_the_original_deadline() { + use edgezero_core::time::Deadline; + + let started_at = MonotonicInstant::now(); + let delayed = started_at + .checked_add(Duration::from_millis(26)) + .expect("preparation time"); + let budget = DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant( + started_at + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), + ), + duration: Duration::from_secs(1), + }; + let clock = scripted_clock(vec![started_at, delayed, budget.deadline.instant()]); + let registrations = Cell::new(0_u8); + let register = || { + registrations.set(registrations.get().saturating_add(1)); + Ok::<_, ()>(()) + }; + + let first = register_backend_while_live(Some(started_at), budget, &clock, register); + assert!(matches!(first, Ok(Ok(())))); + let second = register_backend_while_live(Some(started_at), budget, &clock, register); + assert!(matches!(second, Ok(Ok(())))); + let expired = register_backend_while_live(Some(started_at), budget, &clock, register); + assert!(matches!( + expired, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + )); + assert_eq!(registrations.get(), 2); + } + + #[test] + #[expect( + clippy::too_many_lines, + reason = "the pinned SDK cause table intentionally constructs every known variant" + )] + fn send_error_cause_table_preserves_timeout_source() { + let cases = [ + ( + SendErrorCause::DnsTimeout, + super::super::SendFailure::ProviderTimeout, + ), + ( + SendErrorCause::DnsError { + rcode: None, + info_code: None, + }, + super::super::SendFailure::Unreachable, + ), + ( + SendErrorCause::DestinationNotFound, + super::super::SendFailure::Unreachable, + ), + ( + SendErrorCause::DestinationUnavailable, + super::super::SendFailure::Unreachable, + ), + ( + SendErrorCause::DestinationIpUnroutable, + super::super::SendFailure::Unreachable, + ), + ( + SendErrorCause::ConnectionRefused, + super::super::SendFailure::Unreachable, + ), + ( + SendErrorCause::ConnectionTerminated, + super::super::SendFailure::Transport, + ), + ( + SendErrorCause::ConnectionTimeout, + super::super::SendFailure::ProviderTimeout, + ), + ( + SendErrorCause::ConnectionLimitReached, + super::super::SendFailure::Unreachable, + ), + ( + SendErrorCause::TlsProtocolError, + super::super::SendFailure::Unreachable, + ), + ( + SendErrorCause::TlsCertificateError, + super::super::SendFailure::Unreachable, + ), + ( + SendErrorCause::TlsAlertReceived { alert_id: None }, + super::super::SendFailure::Unreachable, + ), + ( + SendErrorCause::TlsConfigurationError, + super::super::SendFailure::Unreachable, + ), + ( + SendErrorCause::HttpIncompleteResponse, + super::super::SendFailure::UpstreamProtocol, + ), + ( + SendErrorCause::HttpResponseHeaderSectionTooLarge, + super::super::SendFailure::UpstreamProtocol, + ), + ( + SendErrorCause::HttpResponseBodyTooLarge, + super::super::SendFailure::UpstreamProtocol, + ), + ( + SendErrorCause::HttpResponseTimeout, + super::super::SendFailure::BudgetedTimeout, + ), + ( + SendErrorCause::HttpResponseStatusInvalid, + super::super::SendFailure::UpstreamProtocol, + ), + ( + SendErrorCause::HttpUpgradeFailed, + super::super::SendFailure::UpstreamProtocol, + ), + ( + SendErrorCause::Http2StreamError { + frame_type: 0, + error_code: 0, + }, + super::super::SendFailure::UpstreamProtocol, + ), + ( + SendErrorCause::HttpProtocolError, + super::super::SendFailure::UpstreamProtocol, + ), + ( + SendErrorCause::HttpRequestCacheKeyInvalid, + super::super::SendFailure::LocalInvariant, + ), + ( + SendErrorCause::HttpRequestUriInvalid, + super::super::SendFailure::LocalInvariant, + ), + ( + SendErrorCause::HttpCacheLimitExceeded, + super::super::SendFailure::LocalInvariant, + ), + ( + SendErrorCause::HttpCacheApiUnsupported, + super::super::SendFailure::LocalInvariant, + ), + ( + SendErrorCause::IoError(Error::other("test")), + super::super::SendFailure::Transport, + ), + ( + SendErrorCause::FanoutNotEnabled, + super::super::SendFailure::Unknown, + ), + ( + SendErrorCause::ImageOptimizerUnsupported, + super::super::SendFailure::ImageOptimizerUnavailable, + ), + ( + SendErrorCause::InternalError(None), + super::super::SendFailure::PlatformInternal, + ), + ( + SendErrorCause::RequestCollapse, + super::super::SendFailure::Unknown, + ), + ( + SendErrorCause::Custom(anyhow::anyhow!("test")), + super::super::SendFailure::Unknown, + ), + ]; + + let observed_at = MonotonicInstant::now(); + let selected = BudgetSource::PerCallTimeout; + let budget = super::super::test_budget(observed_at, selected); + for (cause, expected) in cases { + let failure = cause_to_failure(&cause); + assert_eq!(failure, expected, "{cause:?}"); + super::super::assert_send_failure_error( + failure, + &super::super::classify_send_failure(failure, budget, observed_at), + selected, + ); + } + } + } +} + +#[cfg(feature = "fastly")] +pub use fastly_impl::{DYNAMIC_BACKENDS_DISABLED_MESSAGE, FastlyOutboundClient}; +#[cfg(feature = "fastly")] +pub(crate) use fastly_impl::{drive_callback, native_time}; + +#[cfg(any(feature = "fastly", feature = "test-utils"))] +fn validate_supported_options( + request: &OutboundRequest, + mode: OutboundSendMode, +) -> Result<(), EdgeError> { + if request.image_optimizer_options().is_some() + && matches!(mode, OutboundSendMode::Batch | OutboundSendMode::Begin) + { + return Err(EdgeError::bad_request( + "image_optimizer is not supported by fastly batches", + )); + } + Ok(()) +} + +#[cfg(any(feature = "fastly", test))] +#[cfg(test)] +fn driver_selection_failure(error: EdgeError) -> OutboundBatchDriverEvent { + OutboundBatchDriverEvent::Failed(error) +} + +/// Runs the target-neutral Fastly batch preflight contract in native tests. +/// +/// # Errors +/// Returns the same portable validation or batch-shape error as production dispatch. +#[cfg(feature = "test-utils")] +#[inline] +pub fn validate_batch_request_for_test(request: &OutboundRequest) -> Result<(), EdgeError> { + validate_supported_options(request, OutboundSendMode::Batch)?; + validate_for_dispatch(request, OutboundSendMode::Batch) +} + +#[cfg(any(feature = "fastly", test))] +fn timeout_error(cause: BudgetSource) -> EdgeError { + EdgeError::gateway_timeout_caused("outbound request deadline expired", cause) +} + +#[cfg(test)] +mod send_failure_policy_tests { + use std::sync::atomic::{AtomicU64, Ordering}; + + use edgezero_core::outbound::{ + OutboundBatch, OutboundBatchDriverEvent, OutboundBatchTermination, OutboundCollectedSlot, + OutboundSlotResult, + }; + use futures::executor::block_on; + use futures_util::stream; + + use super::testing; + use super::*; + + #[test] + fn image_optimizer_sync_call_preserves_original_deadline_and_releases_late_response() { + use edgezero_core::MonotonicClock; + use std::sync::{Arc, Mutex}; + + struct Release(Arc); + #[expect( + clippy::missing_trait_methods, + reason = "Drop::pin_drop is unstable and conflicts with drop" + )] + impl Drop for Release { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + for cause in [ + BudgetSource::Default, + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::BatchCutoff, + ] { + for delta in [999_999_999, 1_000_000_000, 1_000_000_001] { + let start = MonotonicInstant::now(); + let now = Arc::new(Mutex::new(start)); + let samples = Arc::clone(&now); + let clock = MonotonicClock::new(move || *samples.lock().unwrap()); + let drops = Arc::new(AtomicU64::new(0)); + let released = Arc::clone(&drops); + let result = synchronous_send_until(test_budget(start, cause), &clock, || { + *now.lock().unwrap() = start.checked_add(Duration::from_nanos(delta)).unwrap(); + Ok(Release(released)) + }); + if delta < 1_000_000_000 { + assert!(result.is_ok()); + assert_eq!(drops.load(Ordering::SeqCst), 0); + drop(result); + } else { + assert!( + matches!(result, Err(EdgeError::GatewayTimeout { cause: selected, .. }) if selected == cause) + ); + } + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + } + } + + #[test] + fn image_optimizer_sync_call_preserves_late_error_detail_and_skips_expired_dispatch() { + use edgezero_core::{ + FastlyFailureCause, MonotonicClock, ProviderFailureDetail, ProviderFailurePhase, + }; + use std::sync::{Arc, Mutex}; + + let start = MonotonicInstant::now(); + let budget = test_budget(start, BudgetSource::RequestDeadline); + let now = Arc::new(Mutex::new(start)); + let samples = Arc::clone(&now); + let clock = MonotonicClock::new(move || *samples.lock().unwrap()); + let detail = ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::Send, + cause: FastlyFailureCause::ImageOptimizerUnsupported, + }; + let error = synchronous_send_until::<(), _>(budget, &clock, || { + *now.lock().unwrap() = budget.deadline.instant(); + Err(EdgeError::bad_gateway_with_detail( + "Fastly outbound request failed", + BadGatewayReason::ImageOptimizerUnavailable, + detail, + )) + }) + .expect_err("late provider failure"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + } + )); + assert_eq!(error.provider_failure_detail(), Some(&detail)); + synchronous_send_until::<(), _>(budget, &clock, || { + panic!("expired request must not dispatch") + }) + .expect_err("already expired"); + let unavailable = + classify_send_failure(SendFailure::ImageOptimizerUnavailable, budget, start); + assert_eq!(unavailable.message(), "Fastly outbound request failed"); + assert_eq!(unavailable.to_string(), "Fastly outbound request failed"); + } + + #[test] + fn selection_failure_preserves_its_diagnostic_and_previously_collected_slots() { + let event = driver_selection_failure(EdgeError::internal(anyhow::anyhow!( + "Fastly readiness returned an invalid handle index" + ))); + let OutboundBatchDriverEvent::Failed(_) = &event else { + panic!("selection failure must become a driver failure event"); + }; + let completed = OutboundBatchDriverEvent::Item { + index: 0, + completed_at: testing::instant(), + outcome: Err(EdgeError::bad_gateway("completed slot failure")), + }; + let batch = OutboundBatch::from_driver( + testing::batch_context(2), + stream::iter([completed, event]), + testing::owner(), + ); + let failure = + block_on(batch.collect()).expect_err("selection failure must fail collection"); + + assert_eq!( + failure.error().to_string(), + "internal error: Fastly readiness returned an invalid handle index" + ); + assert!(matches!( + failure.slots().first(), + Some(OutboundCollectedSlot::Resolved(_)) + )); + assert!(matches!( + failure.slots().get(1), + Some(OutboundCollectedSlot::Unresolved) + )); + } + + #[test] + fn early_observation_cutoff_attributed_timeout_preserves_a_later_sibling() { + let started_at = MonotonicInstant::now(); + let events = [ + OutboundBatchDriverEvent::Item { + index: 0, + completed_at: started_at.checked_add(Duration::from_millis(1)).unwrap(), + outcome: Err(timeout_error(BudgetSource::BatchCutoff)), + }, + OutboundBatchDriverEvent::Item { + index: 1, + completed_at: started_at.checked_add(Duration::from_millis(2)).unwrap(), + outcome: Err(EdgeError::bad_gateway("sibling completed")), + }, + ]; + let context = testing::batch_context_at(2, started_at, Duration::from_millis(10)); + let batch = OutboundBatch::from_driver(context, stream::iter(events), testing::owner()); + + let results = block_on(batch.collect()).expect("valid Fastly batch driver"); + + assert_eq!(results.termination(), OutboundBatchTermination::Completed); + assert!(matches!( + results.slots().first(), + Some(OutboundCollectedSlot::Resolved(OutboundSlotResult { + outcome: Err(EdgeError::GatewayTimeout { + cause: BudgetSource::BatchCutoff, + .. + }), + .. + })) + )); + assert!(matches!( + results.slots().get(1), + Some(OutboundCollectedSlot::Resolved(OutboundSlotResult { + outcome: Err(EdgeError::BadGateway { .. }), + .. + })) + )); + } + + #[test] + fn connect_timeout_reaches_provider_settings() { + let record = + |timers: BackendTimers| (timers.connect, timers.first_byte, timers.between_bytes); + assert_eq!( + with_backend_timers(0x4000, Some(1_000), record), + ( + Duration::from_secs(1), + Duration::from_millis(0x4000), + Duration::from_millis(0x4000) + ) + ); + assert_eq!( + effective_connect_ms(Some(Duration::from_secs(20)), Duration::from_secs(15)), + None + ); + assert_eq!( + effective_connect_ms(Some(Duration::from_nanos(1)), Duration::from_secs(15)), + Some(1) + ); + assert_eq!( + with_backend_timers(0x4000, None, record), + ( + Duration::from_millis(0x4000), + Duration::from_millis(0x4000), + Duration::from_millis(0x4000) + ) + ); + } + + #[test] + fn connect_expiry_preserves_governing_budget_precedence() { + let start = MonotonicInstant::now(); + for cause in [ + BudgetSource::Default, + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::BatchCutoff, + ] { + let budget = test_budget(start, cause); + for at in [start, budget.deadline.instant()] { + let error = classify_send_failure(SendFailure::ConnectTimeout, budget, at); + let expected = if at == start { + BudgetSource::ConnectTimeout + } else { + cause + }; + assert!( + matches!(error, EdgeError::GatewayTimeout { cause: actual, .. } if actual == expected) + ); + } + } + } + + fn failures() -> [SendFailure; 10] { + [ + SendFailure::BudgetedTimeout, + SendFailure::ConnectTimeout, + SendFailure::ImageOptimizerUnavailable, + SendFailure::LocalInvariant, + SendFailure::PlatformInternal, + SendFailure::ProviderTimeout, + SendFailure::Transport, + SendFailure::Unknown, + SendFailure::Unreachable, + SendFailure::UpstreamProtocol, + ] + } + + #[test] + fn send_failure_policy_maps_every_known_category() { + let observed_at = MonotonicInstant::now(); + for selected in [ + BudgetSource::PerCallTimeout, + BudgetSource::BatchCutoff, + BudgetSource::RequestDeadline, + BudgetSource::Default, + ] { + let budget = test_budget(observed_at, selected); + for failure in failures() { + assert_send_failure_error( + failure, + &classify_send_failure(failure, budget, observed_at), + selected, + ); + } + } + } + + #[test] + fn absolute_deadline_wins_every_send_failure() { + let started_at = MonotonicInstant::now(); + for selected in [ + BudgetSource::PerCallTimeout, + BudgetSource::BatchCutoff, + BudgetSource::RequestDeadline, + BudgetSource::Default, + ] { + let budget = test_budget(started_at, selected); + let after_deadline = budget + .deadline + .instant() + .checked_add(Duration::from_nanos(1)) + .expect("after deadline"); + for observed_at in [budget.deadline.instant(), after_deadline] { + for failure in failures() { + let error = classify_send_failure(failure, budget, observed_at); + if let EdgeError::GatewayTimeout { cause, .. } = error { + assert_eq!(cause, selected); + } else { + panic!("deadline did not win {failure:?} for {selected:?}"); + } + } + } + } + } +} diff --git a/crates/edgezero-adapter-fastly/src/outbound/image_optimizer.rs b/crates/edgezero-adapter-fastly/src/outbound/image_optimizer.rs new file mode 100644 index 000000000..95e3e3abf --- /dev/null +++ b/crates/edgezero-adapter-fastly/src/outbound/image_optimizer.rs @@ -0,0 +1,211 @@ +use edgezero_core::{ + EdgeError, OutboundCropFocus, OutboundImageFormat, OutboundImageOptimizerOptions, + OutboundImageOptimizerRegion, OutboundResizeFilter, +}; +use fastly::image_optimizer::{ + Area, Crop, CropMode, Format, ImageOptimizerOptions, ImageOptimizerRegion, PixelsOrPercentage, + PointOrOffset, Position, ResizeAlgorithm, +}; + +pub(super) fn native_options( + options: &OutboundImageOptimizerOptions, +) -> Result { + let region = match options.region_setting() { + Some(OutboundImageOptimizerRegion::Asia) => ImageOptimizerRegion::Asia, + Some(OutboundImageOptimizerRegion::Australia) => ImageOptimizerRegion::Australia, + Some(OutboundImageOptimizerRegion::EuCentral) => ImageOptimizerRegion::EuCentral, + Some(OutboundImageOptimizerRegion::EuWest) => ImageOptimizerRegion::EuWest, + Some(OutboundImageOptimizerRegion::UsCentral) => ImageOptimizerRegion::UsCentral, + Some(OutboundImageOptimizerRegion::UsEast) => ImageOptimizerRegion::UsEast, + Some(OutboundImageOptimizerRegion::UsWest) => ImageOptimizerRegion::UsWest, + None => { + return Err(EdgeError::bad_request( + "image_optimizer requires an explicit region", + )); + } + }; + let mut native = ImageOptimizerOptions::from_region(region); + native.preserve_query_string_on_origin_request = Some(options.preserve_origin_query_setting()); + native.format = options.format_setting().map(|format| match format { + OutboundImageFormat::Auto => Format::Auto, + OutboundImageFormat::Avif => Format::AVIF, + OutboundImageFormat::Gif => Format::GIF, + OutboundImageFormat::Jpeg => Format::JPEG, + OutboundImageFormat::JpegXl => Format::JPEGXL, + OutboundImageFormat::Mp4 => Format::MP4, + OutboundImageFormat::Png => Format::PNG, + OutboundImageFormat::WebP => Format::WebP, + }); + native.quality = options.quality_setting().map(u32::from); + native.width = options.width_setting().map(PixelsOrPercentage::Pixels); + native.height = options.height_setting().map(PixelsOrPercentage::Pixels); + native.resize_filter = options.resize_filter_setting().map(|filter| match filter { + OutboundResizeFilter::Bicubic => ResizeAlgorithm::Bicubic, + OutboundResizeFilter::Bilinear => ResizeAlgorithm::Bilinear, + OutboundResizeFilter::Lanczos2 => ResizeAlgorithm::Lanczos2, + OutboundResizeFilter::Lanczos3 => ResizeAlgorithm::Lanczos3, + OutboundResizeFilter::Nearest => ResizeAlgorithm::Nearest, + }); + native.crop = options.crop_setting().map(|(width, height, focus)| { + let (position, mode) = match focus { + OutboundCropFocus::Default => (None, None), + OutboundCropFocus::Smart => (None, Some(CropMode::Smart)), + OutboundCropFocus::Percent { x, y } => ( + Some(Position { + x: Some(PointOrOffset::Offset(u32::from(x))), + y: Some(PointOrOffset::Offset(u32::from(y))), + }), + None, + ), + }; + Crop { + size: Area::AspectRatio((width, height)), + position, + mode, + } + }); + Ok(native) +} + +#[cfg(test)] +mod tests { + use edgezero_core::{ + OutboundCropFocus, OutboundImageFormat, OutboundImageOptimizerOptions, + OutboundImageOptimizerRegion, OutboundResizeFilter, + }; + use fastly::image_optimizer::{ + Area, Crop, CropMode, Format, ImageOptimizerOptions, ImageOptimizerRegion, + PixelsOrPercentage, PointOrOffset, Position, ResizeAlgorithm, + }; + + use super::native_options; + + #[test] + fn image_optimizer_native_mapping_matches_sdk() { + for (region, native_region) in [ + ( + OutboundImageOptimizerRegion::Asia, + ImageOptimizerRegion::Asia, + ), + ( + OutboundImageOptimizerRegion::Australia, + ImageOptimizerRegion::Australia, + ), + ( + OutboundImageOptimizerRegion::EuCentral, + ImageOptimizerRegion::EuCentral, + ), + ( + OutboundImageOptimizerRegion::EuWest, + ImageOptimizerRegion::EuWest, + ), + ( + OutboundImageOptimizerRegion::UsCentral, + ImageOptimizerRegion::UsCentral, + ), + ( + OutboundImageOptimizerRegion::UsEast, + ImageOptimizerRegion::UsEast, + ), + ( + OutboundImageOptimizerRegion::UsWest, + ImageOptimizerRegion::UsWest, + ), + ] { + for preserve in [false, true] { + let options = OutboundImageOptimizerOptions::new() + .region(region) + .preserve_origin_query(preserve); + let mut expected = ImageOptimizerOptions::from_region(native_region.clone()); + expected.preserve_query_string_on_origin_request = Some(preserve); + assert_eq!( + serde_json::to_value(native_options(&options).unwrap()).unwrap(), + serde_json::to_value(expected).unwrap() + ); + } + } + for (format, native_format) in [ + (OutboundImageFormat::Auto, Format::Auto), + (OutboundImageFormat::Avif, Format::AVIF), + (OutboundImageFormat::Gif, Format::GIF), + (OutboundImageFormat::Jpeg, Format::JPEG), + (OutboundImageFormat::JpegXl, Format::JPEGXL), + (OutboundImageFormat::Mp4, Format::MP4), + (OutboundImageFormat::Png, Format::PNG), + (OutboundImageFormat::WebP, Format::WebP), + ] { + let options = OutboundImageOptimizerOptions::new() + .region(OutboundImageOptimizerRegion::UsEast) + .format(format); + let actual = native_options(&options).unwrap(); + assert_eq!( + serde_json::to_value(actual.format).unwrap(), + serde_json::to_value(native_format).unwrap() + ); + assert!(actual.auto.is_none()); + } + for (filter, native_filter) in [ + (OutboundResizeFilter::Bicubic, ResizeAlgorithm::Bicubic), + (OutboundResizeFilter::Bilinear, ResizeAlgorithm::Bilinear), + (OutboundResizeFilter::Lanczos2, ResizeAlgorithm::Lanczos2), + (OutboundResizeFilter::Lanczos3, ResizeAlgorithm::Lanczos3), + (OutboundResizeFilter::Nearest, ResizeAlgorithm::Nearest), + ] { + let options = OutboundImageOptimizerOptions::new() + .region(OutboundImageOptimizerRegion::Asia) + .resize_filter(filter); + let actual = native_options(&options).unwrap(); + assert_eq!( + serde_json::to_value(actual.resize_filter).unwrap(), + serde_json::to_value(native_filter).unwrap() + ); + } + } + + #[test] + fn image_optimizer_native_crop_and_dimensions_match_sdk() { + for (focus, position, mode) in [ + (OutboundCropFocus::Default, None, None), + (OutboundCropFocus::Smart, None, Some(CropMode::Smart)), + ( + OutboundCropFocus::Percent { x: 0, y: 100 }, + Some(Position { + x: Some(PointOrOffset::Offset(0)), + y: Some(PointOrOffset::Offset(100)), + }), + None, + ), + ] { + for quality in [0, 100] { + let options = OutboundImageOptimizerOptions::new() + .region(OutboundImageOptimizerRegion::UsWest) + .crop(16, 9, focus) + .unwrap() + .width(u32::MAX) + .unwrap() + .height(1) + .unwrap() + .quality(quality) + .unwrap(); + let actual = native_options(&options).unwrap(); + let crop = Crop { + size: Area::AspectRatio((16, 9)), + position: position.clone(), + mode: mode.clone(), + }; + assert_eq!( + serde_json::to_value(actual.crop).unwrap(), + serde_json::to_value(crop).unwrap() + ); + assert!(matches!( + actual.width, + Some(PixelsOrPercentage::Pixels(u32::MAX)) + )); + assert!(matches!(actual.height, Some(PixelsOrPercentage::Pixels(1)))); + assert_eq!(actual.quality, Some(u32::from(quality))); + } + } + native_options(&OutboundImageOptimizerOptions::new()) + .expect_err("explicit region required"); + } +} diff --git a/crates/edgezero-adapter-fastly/src/outbound/progress.rs b/crates/edgezero-adapter-fastly/src/outbound/progress.rs new file mode 100644 index 000000000..1a1494edb --- /dev/null +++ b/crates/edgezero-adapter-fastly/src/outbound/progress.rs @@ -0,0 +1,2144 @@ +use std::cell::{Cell, RefCell}; +use std::collections::BTreeMap; +use std::future::Future; +use std::mem::take; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::task::{Context, Poll, Waker}; +use std::time::Duration; + +use async_stream::stream; +use bytes::Bytes; +use edgezero_core::{ + BodyStream, Deadline, DispatchBudget, EdgeError, MonotonicClock, MonotonicInstant, + OutboundFuture, +}; +use futures_util::StreamExt as _; +use futures_util::future::poll_fn; +use futures_util::task::{ArcWake, waker}; + +const READ_BYTES: usize = 16 * 1024; + +type WaitFactory = Rc OutboundFuture<()>>; +type WaitLimit = Rc Result, EdgeError>>; +std::thread_local! { + static CURRENT_WAIT: RefCell> = const { RefCell::new(None) }; + static CURRENT_DRIVER: RefCell>> = const { RefCell::new(None) }; +} + +struct DriverContext { + limit: WaitLimit, + parent: Option>, + repoll: Rc>, + waker: Waker, +} + +impl DriverContext { + fn ancestor_limit(&self) -> Result, EdgeError> { + let mut remaining = None; + let mut ancestor = self.parent.as_ref(); + while let Some(parent) = ancestor { + if let Some(limit) = (parent.limit)()? { + remaining = Some(remaining.map_or(limit, |prior: Duration| prior.min(limit))); + } + ancestor = parent.parent.as_ref(); + } + Ok(remaining) + } + + fn repoll_parent(&self) { + if let Some(parent) = &self.parent { + self.repoll.set(true); + parent.waker.wake_by_ref(); + } + } +} + +struct Timer { + clock: MonotonicClock, + deadline: Deadline, + outcome: Option>, + previous: MonotonicInstant, + waker: Option, +} + +struct TimerLease { + id: u64, + progress: Progress, +} + +pub(super) enum HeadPoll { + Pending(Pending), + Ready(Result), +} + +pub(super) trait Transport { + type Body; + type Head; + type Pending; + + fn body_handle(body: &Self::Body) -> u32; + fn head_handle(pending: &Self::Pending) -> Option; + fn poll_head( + &mut self, + pending: Self::Pending, + budget: DispatchBudget, + clock: &MonotonicClock, + ) -> HeadPoll; + fn read_body( + &mut self, + body: &mut Self::Body, + maximum: usize, + budget: DispatchBudget, + clock: &MonotonicClock, + ) -> Result; + fn select(&mut self, handles: &[u32], timeout_ms: u32) -> Result, EdgeError>; +} + +enum Phase { + Body { + handle: Option, + next: Option>, + requested: bool, + }, + Head(Option), + HeadReady(Option>), +} + +struct Slot { + budget: DispatchBudget, + clock: MonotonicClock, + expired: bool, + phase: Phase, + previous: MonotonicInstant, + waker: Option, +} + +struct State { + boundary: Option, + clock: MonotonicClock, + cursor: usize, + opaque_cursor: usize, + previous: MonotonicInstant, + slots: Vec>>, + timer_id: u64, + timers: BTreeMap, + transport: T, + wakes: Vec, +} + +pub(super) struct Progress { + clock: MonotonicClock, + driving: Rc>, + entered: MonotonicInstant, + state: Rc>>, + wake_requested: Arc, +} + +struct SourceWake { + parent: Waker, + requested: Arc, +} + +impl ArcWake for SourceWake { + fn wake(self: Arc) { + Self::wake_by_ref(&self); + } + + fn wake_by_ref(arc_self: &Arc) { + arc_self.requested.store(true, Ordering::Relaxed); + arc_self.parent.wake_by_ref(); + } +} + +pub(super) struct Lease { + index: usize, + progress: Progress, +} + +struct Driving { + active: Rc>, + previous: bool, + previous_driver: Option>, + previous_wait: Option, +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for Driving { + fn drop(&mut self) { + self.active.set(self.previous); + let retired = CURRENT_WAIT.with(|active| active.replace(self.previous_wait.take())); + let retired_driver = + CURRENT_DRIVER.with(|active| active.replace(self.previous_driver.take())); + drop(retired); + drop(retired_driver); + } +} + +impl Clone for Progress { + fn clone(&self) -> Self { + Self { + clock: self.clock.clone(), + driving: Rc::clone(&self.driving), + entered: self.entered, + state: Rc::clone(&self.state), + wake_requested: Arc::clone(&self.wake_requested), + } + } + + fn clone_from(&mut self, source: &Self) { + self.clock.clone_from(&source.clock); + self.driving.clone_from(&source.driving); + self.entered = source.entered; + self.state.clone_from(&source.state); + self.wake_requested.clone_from(&source.wake_requested); + } +} + +impl Progress { + pub(super) fn body(&self, handle: T::Body, budget: DispatchBudget) -> BodyStream { + let lease = self.register( + budget, + Phase::Body { + handle: Some(handle), + next: None, + requested: false, + }, + ); + let progress = self.clone(); + stream! { + loop { + let read = poll_fn(|cx| lease.poll_body(cx)); + let bytes = if progress.driving.get() { read.await } else { progress.drive(read).await? }?; + if bytes.is_empty() { return; } + yield Ok(bytes); + } + }.boxed_local() + } + + pub(super) fn clock(&self) -> MonotonicClock { + self.clock.clone() + } + + pub(super) async fn drive(&self, future: F) -> Result + where + F: Future, + { + futures_util::pin_mut!(future); + poll_fn(|cx| self.poll_once(cx, &mut |inner_cx| future.as_mut().poll(inner_cx))).await + } + + pub(super) fn head(&self, pending: T::Pending, budget: DispatchBudget) -> Lease { + self.register(budget, Phase::Head(Some(pending))) + } + + pub(super) fn new( + transport: T, + clock: MonotonicClock, + previous: MonotonicInstant, + boundary: Option, + ) -> Self { + Self { + clock: clock.clone(), + driving: Rc::new(Cell::new(false)), + entered: previous, + state: Rc::new(RefCell::new(State { + boundary, + clock, + cursor: 0, + opaque_cursor: 0, + previous, + slots: Vec::new(), + timer_id: 0, + timers: BTreeMap::new(), + transport, + wakes: Vec::new(), + })), + wake_requested: Arc::new(AtomicBool::new(false)), + } + } + + pub(super) fn poll_once( + &self, + cx: &mut Context<'_>, + step: &mut dyn FnMut(&mut Context<'_>) -> Poll, + ) -> Poll> { + let nested = self.driving.get(); + let polled_waker = if nested { + cx.waker().clone() + } else { + self.wake_requested.store(false, Ordering::Relaxed); + waker(Arc::new(SourceWake { + parent: cx.waker().clone(), + requested: Arc::clone(&self.wake_requested), + })) + }; + let mut inner_cx = Context::from_waker(&polled_waker); + let progress = self.clone(); + let wait: WaitFactory = Rc::new(move |deadline, clock| progress.waiter(deadline, clock)); + let parent = CURRENT_DRIVER.with(|active| active.borrow().clone()); + let repoll = parent.as_ref().map_or_else( + || Rc::new(Cell::new(false)), + |ancestor| Rc::clone(&ancestor.repoll), + ); + let state = Rc::clone(&self.state); + let current = Rc::new(DriverContext { + limit: Rc::new(move || state.borrow_mut().wait_limit()), + parent, + repoll, + waker: cx.waker().clone(), + }); + let guard = Driving { + previous: self.driving.replace(true), + active: Rc::clone(&self.driving), + previous_wait: CURRENT_WAIT.with(|active| active.replace(Some(Rc::clone(&wait)))), + previous_driver: CURRENT_DRIVER + .with(|active| active.replace(Some(Rc::clone(¤t)))), + }; + super::scope::poll_current(&mut inner_cx); + let result = step(&mut inner_cx); + drop(guard); + if let Poll::Ready(output) = result { + current.repoll_parent(); + return Poll::Ready(Ok(output)); + } + if nested { + return Poll::Pending; + } + if self.wake_requested.load(Ordering::Relaxed) { + // A cooperative yield is runnable work, not permission to block in host selection. + current.repoll.set(true); + current.repoll_parent(); + cx.waker().wake_by_ref(); + } + let (turned, wakes) = { + let mut shared = self.state.borrow_mut(); + let turned = shared.turn(!current.repoll.get(), || current.ancestor_limit()); + (turned, take(&mut shared.wakes)) + }; + if turned.as_ref().is_ok_and(|made_progress| *made_progress) || turned.is_err() { + current.repoll.set(true); + current.repoll_parent(); + cx.waker().wake_by_ref(); + } + for waiting in wakes { + waiting.wake(); + } + turned?; + Poll::Pending + } + + fn register(&self, budget: DispatchBudget, phase: Phase) -> Lease { + let mut state = self.state.borrow_mut(); + let index = state.slots.len(); + // An operation registers each head/body once. Never reuse an index while a lease can live. + state.slots.push(Some(Slot { + budget, + clock: self.clock.clone(), + expired: false, + phase, + previous: self.entered, + waker: None, + })); + Lease { + index, + progress: self.clone(), + } + } + + fn waiter(&self, deadline: Deadline, clock: MonotonicClock) -> OutboundFuture<()> { + let mut state = self.state.borrow_mut(); + let id = state.timer_id; + let Some(next) = id.checked_add(1) else { + return Box::pin(async { + Err(EdgeError::internal(anyhow::anyhow!( + "Fastly timer registration exhausted" + ))) + }); + }; + state.timer_id = next; + state.timers.insert( + id, + Timer { + previous: clock.now(), + clock, + deadline, + outcome: None, + waker: None, + }, + ); + let lease = TimerLease { + id, + progress: self.clone(), + }; + Box::pin(async move { + poll_fn(|cx| { + let replacement = cx.waker().clone(); + let (observation, previous) = { + let mut shared = lease.progress.state.borrow_mut(); + let timer = shared.timers.get_mut(&lease.id).ok_or_else(missing_slot)?; + let prior = timer.waker.replace(replacement); + ( + timer.outcome.take().map_or(Poll::Pending, Poll::Ready), + prior, + ) + }; + drop(previous); + observation + }) + .await + }) + } + + pub(super) fn with_clock(&self, clock: MonotonicClock, entered: MonotonicInstant) -> Self { + Self { + clock, + driving: Rc::clone(&self.driving), + entered, + state: Rc::clone(&self.state), + wake_requested: Arc::clone(&self.wake_requested), + } + } + + pub(super) fn with_current(&self, operation: impl FnOnce() -> Output) -> Output { + let progress = self.clone(); + let wait: WaitFactory = Rc::new(move |deadline, clock| progress.waiter(deadline, clock)); + let _guard = Driving { + previous: self.driving.replace(true), + active: Rc::clone(&self.driving), + previous_wait: CURRENT_WAIT.with(|active| active.replace(Some(wait))), + previous_driver: CURRENT_DRIVER.with(|active| active.borrow().clone()), + }; + operation() + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for TimerLease { + fn drop(&mut self) { + let retired = self.progress.state.borrow_mut().timers.remove(&self.id); + drop(retired); + } +} + +impl Lease { + fn poll_body(&self, cx: &mut Context<'_>) -> Poll> { + let mut state = self.progress.state.borrow_mut(); + let slot = state + .slots + .get_mut(self.index) + .and_then(Option::as_mut) + .ok_or_else(missing_slot)?; + let Phase::Body { + next, requested, .. + } = &mut slot.phase + else { + return Poll::Ready(Err(missing_slot())); + }; + if let Some(result) = next.take() { + return Poll::Ready(result); + } + *requested = true; + slot.waker = Some(cx.waker().clone()); + Poll::Pending + } + + pub(super) async fn receive(self) -> Result { + poll_fn(|cx| { + let mut state = self.progress.state.borrow_mut(); + let slot = state + .slots + .get_mut(self.index) + .and_then(Option::as_mut) + .ok_or_else(missing_slot)?; + match &mut slot.phase { + Phase::HeadReady(result) => Poll::Ready(result.take().ok_or_else(missing_slot)?), + Phase::Head(_) => { + slot.waker = Some(cx.waker().clone()); + Poll::Pending + } + Phase::Body { .. } => Poll::Ready(Err(missing_slot())), + } + }) + .await + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for Lease { + fn drop(&mut self) { + let retired = self + .progress + .state + .borrow_mut() + .slots + .get_mut(self.index) + .and_then(Option::take); + drop(retired); + } +} + +impl State { + fn opaque_head(&self) -> Option { + self.slots + .iter() + .enumerate() + .skip(self.opaque_cursor) + .chain(self.slots.iter().enumerate().take(self.opaque_cursor)) + .find_map(|(index, entry)| { + entry + .as_ref() + .is_some_and(|slot| { + matches!(&slot.phase, + Phase::Head(Some(pending)) if T::head_handle(pending).is_none()) + }) + .then_some(index) + }) + } + + fn poll_head(&mut self, index: usize) -> Result<(), EdgeError> { + let slot = self + .slots + .get_mut(index) + .and_then(Option::as_mut) + .ok_or_else(missing_slot)?; + let Phase::Head(owned) = &mut slot.phase else { + return Err(missing_slot()); + }; + let pending = owned.take().ok_or_else(missing_slot)?; + match self.transport.poll_head(pending, slot.budget, &slot.clock) { + HeadPoll::Pending(waiting) => slot.phase = Phase::Head(Some(waiting)), + HeadPoll::Ready(result) => { + slot.phase = Phase::HeadReady(Some(result)); + if let Some(waker) = slot.waker.take() { + self.wakes.push(waker); + } + } + } + let polled = self + .slots + .get_mut(index) + .and_then(Option::as_mut) + .ok_or_else(missing_slot)?; + let now = polled.sample()?; + if polled.budget.deadline.is_expired_at(now) { + expire(polled, &mut self.wakes); + } + Ok(()) + } + + fn sample(&mut self) -> Result { + let now = self.clock.now(); + if now < self.previous { + return Err(EdgeError::internal(anyhow::anyhow!( + "outbound clock moved backwards during Fastly progress" + ))); + } + self.previous = now; + Ok(now) + } + + fn select_ready( + &mut self, + remaining: Duration, + active: &[(usize, u32)], + ) -> Result { + let milliseconds = u32::try_from(remaining.as_millis()) + .unwrap_or(u32::MAX) + .max(1); + let handles = active.iter().map(|(_, handle)| *handle).collect::>(); + let selection = self.transport.select(&handles, milliseconds)?; + let Some(selected) = selection else { + return Ok(true); + }; + let (index, _) = *active.get(selected).ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!( + "Fastly readiness returned an invalid handle index" + )) + })?; + self.cursor = index.saturating_add(1); + self.service(index)?; + Ok(true) + } + + fn service(&mut self, index: usize) -> Result<(), EdgeError> { + let slot = self + .slots + .get_mut(index) + .and_then(Option::as_mut) + .ok_or_else(missing_slot)?; + let now = slot.sample()?; + if slot.budget.deadline.is_expired_at(now) { + expire(slot, &mut self.wakes); + return Ok(()); + } + match slot.phase { + Phase::Head(_) => self.poll_head(index)?, + Phase::Body { .. } => { + let Phase::Body { + handle, + next, + requested, + } = &mut slot.phase + else { + return Err(missing_slot()); + }; + let body = handle.as_mut().ok_or_else(missing_slot)?; + let result = self + .transport + .read_body(body, READ_BYTES, slot.budget, &slot.clock); + *requested = false; + if result.as_ref().map_or(true, Bytes::is_empty) { + *handle = None; + } + *next = Some(result); + if let Some(waker) = slot.waker.take() { + self.wakes.push(waker); + } + } + Phase::HeadReady(_) => return Err(missing_slot()), + } + let serviced = self + .slots + .get_mut(index) + .and_then(Option::as_mut) + .ok_or_else(missing_slot)?; + let observed_at = serviced.sample()?; + if serviced.budget.deadline.is_expired_at(observed_at) { + expire(serviced, &mut self.wakes); + } + Ok(()) + } + + fn timers(&mut self) -> (bool, Option) { + let mut newly_expired = false; + let mut timer_remaining = None; + for timer in self + .timers + .values_mut() + .filter(|timer| timer.outcome.is_none()) + { + let observed = timer.clock.now(); + let outcome = if observed < timer.previous { + Some(Err(EdgeError::internal(anyhow::anyhow!( + "Fastly timer clock moved backwards" + )))) + } else if let Some(remaining) = timer.deadline.remaining_at(observed) { + timer_remaining = + Some(timer_remaining.map_or(remaining, |prior: Duration| prior.min(remaining))); + None + } else { + Some(Ok(())) + }; + timer.previous = observed; + if let Some(result) = outcome { + timer.outcome = Some(result); + newly_expired = true; + if let Some(waiting) = timer.waker.take() { + self.wakes.push(waiting); + } + } + } + (newly_expired, timer_remaining) + } + + fn turn( + &mut self, + allow_wait: bool, + mut ancestor_limit: impl FnMut() -> Result, EdgeError>, + ) -> Result { + let (mut newly_expired, timer_remaining) = self.timers(); + for slot in self.slots.iter_mut().flatten() { + let now = slot.sample()?; + if slot.budget.deadline.is_expired_at(now) { + newly_expired |= expire(slot, &mut self.wakes); + } + } + // Publish a new timeout promptly, but an occupied mailbox backpressures only its slot. + if newly_expired { + return Ok(true); + } + + // A child turn still permits expiry publication, but never a second host wait. + if !allow_wait { + return Ok(false); + } + + let boundary_remaining = if let Some(deadline) = self.boundary { + Some(deadline.remaining_at(self.sample()?).unwrap_or_default()) + } else { + None + }; + if boundary_remaining.is_some_and(|remaining| remaining.is_zero()) { + return Ok(true); + } + if self.slots.is_empty() && timer_remaining.is_none() && boundary_remaining.is_none() { + return Ok(false); + } + if ancestor_limit()?.is_some_and(|remaining| remaining.is_zero()) { + return Ok(true); + } + let opaque = self.opaque_head(); + if let Some(index) = opaque { + self.opaque_cursor = index.saturating_add(1); + self.poll_head(index)?; + if matches!( + self.slots + .get(index) + .and_then(Option::as_ref) + .map(|slot| &slot.phase), + Some(Phase::HeadReady(_)) + ) { + return Ok(true); + } + } + + let mut earliest = timer_remaining.into_iter().chain(boundary_remaining).min(); + let mut active = Vec::new(); + for (index, entry) in self + .slots + .iter() + .enumerate() + .skip(self.cursor) + .chain(self.slots.iter().enumerate().take(self.cursor)) + { + let Some(slot) = entry else { + continue; + }; + if let Some(duration) = slot.budget.deadline.remaining_at(slot.previous) { + earliest = Some(earliest.map_or(duration, |prior: Duration| prior.min(duration))); + } + let handle = match &slot.phase { + Phase::Head(Some(pending)) => T::head_handle(pending), + Phase::Body { + handle: Some(handle), + requested: true, + next: None, + } => Some(T::body_handle(handle)), + Phase::Head(_) | Phase::Body { .. } | Phase::HeadReady(_) => None, + }; + if let Some(id) = handle { + active.push((index, id)); + } + } + let Some(mut remaining) = earliest else { + return Ok(false); + }; + if active.is_empty() + && opaque.is_none() + && timer_remaining.is_none() + && boundary_remaining.is_none() + { + return Ok(false); + } + // Opaque SDK cache stages expose nonblocking poll, but not their changing wait handle. + // One shared pulse services them without changing caching policy or sleeping per slot. + if opaque.is_some() { + remaining = remaining.min(Duration::from_millis(1)); + } + if let Some(limit) = ancestor_limit()? { + if limit.is_zero() { + return Ok(true); + } + remaining = remaining.min(limit); + } + if let Some(limit) = self.wait_limit()? { + if limit.is_zero() { + return Ok(true); + } + remaining = remaining.min(limit); + } + self.select_ready(remaining, &active) + } + + fn wait_limit(&mut self) -> Result, EdgeError> { + let mut remaining = if let Some(deadline) = self.boundary { + Some(deadline.remaining_at(self.sample()?).unwrap_or_default()) + } else { + None + }; + for slot in self.slots.iter_mut().flatten().filter(|slot| { + !slot.expired + && matches!( + &slot.phase, + Phase::Head(Some(_)) + | Phase::Body { + handle: Some(_), + next: None, + requested: true + } + ) + }) { + let now = slot.sample()?; + let limit = slot.budget.deadline.remaining_at(now).unwrap_or_default(); + remaining = Some(remaining.map_or(limit, |prior| prior.min(limit))); + } + for timer in self + .timers + .values_mut() + .filter(|timer| timer.outcome.is_none()) + { + let observed = timer.clock.now(); + if observed < timer.previous { + return Err(EdgeError::internal(anyhow::anyhow!( + "Fastly timer clock moved backwards" + ))); + } + timer.previous = observed; + let limit = timer.deadline.remaining_at(observed).unwrap_or_default(); + remaining = Some(remaining.map_or(limit, |prior| prior.min(limit))); + } + Ok(remaining) + } +} + +impl Slot { + fn sample(&mut self) -> Result { + let now = self.clock.now(); + if now < self.previous { + return Err(EdgeError::internal(anyhow::anyhow!( + "outbound clock moved backwards during Fastly progress" + ))); + } + self.previous = now; + Ok(now) + } +} + +fn expire(slot: &mut Slot, wakes: &mut Vec) -> bool +where + T: Transport, +{ + if slot.expired { + return false; + } + slot.expired = true; + let detail = match &slot.phase { + Phase::HeadReady(Some(Err(error))) + | Phase::Body { + next: Some(Err(error)), + .. + } => error.provider_failure_detail().copied(), + Phase::Head(_) | Phase::HeadReady(_) | Phase::Body { .. } => None, + }; + let mut error = super::timeout_error(slot.budget.cause); + if let EdgeError::GatewayTimeout { + provider_detail, .. + } = &mut error + { + *provider_detail = detail; + } + match &mut slot.phase { + Phase::Head(_) | Phase::HeadReady(_) => slot.phase = Phase::HeadReady(Some(Err(error))), + Phase::Body { + handle, + next, + requested, + } => { + *handle = None; + *next = Some(Err(error)); + *requested = false; + } + } + if let Some(waker) = slot.waker.take() { + wakes.push(waker); + } + true +} + +fn missing_slot() -> EdgeError { + EdgeError::internal(anyhow::anyhow!( + "Fastly progress exchange is no longer owned" + )) +} + +pub(super) fn wait_current(deadline: Deadline, clock: MonotonicClock) -> OutboundFuture<()> { + let factory = CURRENT_WAIT.with(|active| active.borrow().clone()); + factory.map_or_else( + || -> OutboundFuture<()> { + Box::pin(async { + Err(EdgeError::internal(anyhow::anyhow!( + "Fastly timer requires an active outbound progress scope" + ))) + }) + }, + |wait| wait(deadline, clock), + ) +} + +#[cfg(test)] +#[expect( + clippy::arithmetic_side_effects, + clippy::decimal_literal_representation, + clippy::panic_in_result_fn, + clippy::shadow_reuse, + clippy::unwrap_in_result, + reason = "deterministic test transport asserts scheduling invariants with small bounded counters and byte sizes" +)] +mod tests { + use std::io::ErrorKind; + use std::pin::Pin; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::Wake; + + use edgezero_core::{ + BadGatewayReason, BudgetSource, Deadline, FastlyFailureCause, ProviderFailureDetail, + ProviderFailurePhase, + }; + use futures::executor::block_on; + use futures_util::future::{join, pending}; + use futures_util::task::{AtomicWaker, noop_waker_ref}; + + use super::*; + + struct Handle { + id: u32, + opaque: bool, + released: Rc>, + remaining: usize, + } + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for Handle { + fn drop(&mut self) { + self.released.set(self.released.get() + 1); + } + } + + #[derive(Default)] + struct Observations { + clock_samples: Vec, + complete_at: Option, + domains: Vec>, + invalid_index: bool, + permission: Option, + polls: Vec, + reads: Vec, + source_error: bool, + spurious_heads: usize, + waits: Vec, + } + struct TestTransport { + now: Arc>, + observations: Rc>, + } + + struct WakeCount(AtomicUsize); + + #[expect( + clippy::missing_trait_methods, + reason = "default wake_by_ref delegates to wake" + )] + impl Wake for WakeCount { + fn wake(self: Arc) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + std::thread_local! { + static CANCEL_TIMER: RefCell>> = const { RefCell::new(None) }; + } + + struct CancelTimer; + + #[expect( + clippy::missing_trait_methods, + reason = "default wake_by_ref delegates to wake" + )] + impl Wake for CancelTimer { + fn wake(self: Arc) { + let retired = CANCEL_TIMER.with(|slot| slot.borrow_mut().take()); + drop(retired); + } + } + impl Transport for TestTransport { + type Body = Handle; + type Head = u32; + type Pending = Handle; + fn body_handle(body: &Handle) -> u32 { + body.id + } + fn head_handle(pending: &Handle) -> Option { + (!pending.opaque).then_some(pending.id) + } + fn poll_head( + &mut self, + pending: Handle, + _budget: DispatchBudget, + clock: &MonotonicClock, + ) -> HeadPoll { + let mut observed = self.observations.borrow_mut(); + observed.clock_samples.push(clock.now()); + observed.polls.push(pending.id); + if pending.id == 0 || observed.spurious_heads > 0 { + observed.spurious_heads = observed.spurious_heads.saturating_sub(1); + HeadPoll::Pending(pending) + } else { + if let Some(at) = observed.complete_at { + *self.now.lock().expect("clock") = at; + } + HeadPoll::Ready(if observed.source_error { + Err(source_failure()) + } else { + Ok(pending.id) + }) + } + } + fn read_body( + &mut self, + body: &mut Handle, + maximum: usize, + _budget: DispatchBudget, + clock: &MonotonicClock, + ) -> Result { + let mut observed = self.observations.borrow_mut(); + observed.clock_samples.push(clock.now()); + assert_eq!( + observed.permission.take(), + Some(body.id), + "every read needs its own same-slot selection" + ); + assert_eq!(maximum, 16_384); + observed.reads.push(body.id); + if let Some(at) = observed.complete_at { + *self.now.lock().expect("clock") = at; + } + if observed.source_error { + return Err(source_failure()); + } + let count = body.remaining.min(maximum); + body.remaining -= count; + Ok(Bytes::from(vec![b'x'; count])) + } + fn select(&mut self, handles: &[u32], timeout_ms: u32) -> Result, EdgeError> { + let mut observed = self.observations.borrow_mut(); + assert!(timeout_ms > 0); + observed.waits.push(timeout_ms); + observed.domains.push(handles.to_vec()); + if observed.invalid_index { + return Ok(Some(handles.len())); + } + let selected = handles.iter().position(|handle| *handle != 0); + if let Some(index) = selected { + observed.permission = Some(handles[index]); + } else { + let mut now = self.now.lock().expect("clock"); + *now = now + .checked_add(Duration::from_millis(u64::from(timeout_ms))) + .expect("advance"); + } + Ok(selected) + } + } + + fn source_failure() -> EdgeError { + EdgeError::bad_gateway_with_detail( + "fixture source failed", + BadGatewayReason::Transport, + ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: FastlyFailureCause::IoError { + kind: ErrorKind::UnexpectedEof, + }, + }, + ) + } + + fn fixture() -> ( + Progress, + Rc>, + Arc>, + ) { + fixture_at(Arc::new(Mutex::new(MonotonicInstant::now()))) + } + + fn fixture_at( + now: Arc>, + ) -> ( + Progress, + Rc>, + Arc>, + ) { + let source = Arc::clone(&now); + let clock = MonotonicClock::new(move || *source.lock().expect("clock")); + let observations = Rc::new(RefCell::new(Observations::default())); + let previous = *now.lock().expect("clock"); + let progress = Progress::new( + TestTransport { + observations: Rc::clone(&observations), + now: Arc::clone(&now), + }, + clock, + previous, + None, + ); + (progress, observations, now) + } + fn budget( + now: &Arc>, + millis: u64, + cause: BudgetSource, + ) -> DispatchBudget { + let duration = Duration::from_millis(millis); + let deadline = Deadline::at_instant( + now.lock() + .expect("clock") + .checked_add(duration) + .expect("deadline"), + ); + DispatchBudget { + duration, + deadline, + cause, + } + } + fn handle(id: u32, remaining: usize, opaque: bool, released: &Rc>) -> Handle { + Handle { + id, + remaining, + opaque, + released: Rc::clone(released), + } + } + async fn collect(mut source: BodyStream) -> Result { + let mut bytes = 0; + while let Some(chunk) = source.next().await { + bytes += chunk?.len(); + } + Ok(bytes) + } + + #[test] + fn shared_mux_slots_keep_request_clock_and_original_deadline() { + for head in [false, true] { + for expired in [false, true] { + let (first, observed, first_now) = fixture(); + let second_entered = first_now + .lock() + .unwrap() + .checked_add(Duration::from_secs(3600)) + .unwrap(); + let second_now = Arc::new(Mutex::new(second_entered)); + let sampled = Arc::clone(&second_now); + let second = first.with_clock( + MonotonicClock::new(move || *sampled.lock().unwrap()), + second_entered, + ); + let selected = budget(&second_now, 50, BudgetSource::RequestDeadline); + let released = Rc::new(Cell::new(0)); + let first_head = first.head( + handle(1, 0, false, &released), + budget(&first_now, 50, BudgetSource::PerCallTimeout), + ); + let operation: OutboundFuture<()> = if head { + let lease = second.head(handle(2, 0, false, &released), selected); + Box::pin(async move { lease.receive().await.map(|_| ()) }) + } else { + let body = second.body(handle(2, 1, false, &released), selected); + Box::pin(async move { collect(body).await.map(|_| ()) }) + }; + if expired { + *second_now.lock().unwrap() = selected.deadline.instant(); + } + let (first_result, second_result) = + block_on(first.drive(join(first_head.receive(), operation))).unwrap(); + assert_eq!( + first_result.unwrap(), + 1, + "another client's domain cannot expire this slot" + ); + if expired { + assert!(matches!( + second_result, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + assert!(!observed.borrow().clock_samples.contains(&second_entered)); + } else { + second_result.unwrap(); + assert!( + observed.borrow().clock_samples.contains(&second_entered), + "provider classification receives this slot's paired clock" + ); + } + assert_eq!(released.get(), 2); + } + } + } + + #[test] + fn callback_idle_consumer_observes_complete_pipeline_at_terminal_time() { + use super::super::scope::schedule_batch; + use edgezero_core::{OutboundBatchDriverEvent, OutboundScopeOwner, OutboundTime}; + + let callback = OutboundScopeOwner::callback(); + let (progress, observed, now) = fixture(); + let started = *now.lock().unwrap(); + let released = Rc::new(Cell::new(0)); + let body = progress.body( + handle(3, 32_768, false, &released), + budget(&now, 100, BudgetSource::PerCallTimeout), + ); + let finished = Rc::new(Cell::new(false)); + let terminal = Rc::clone(&finished); + let clock = progress.clock(); + let source = stream! { + assert_eq!(collect(body).await.unwrap(), 32_768); + terminal.set(true); + yield OutboundBatchDriverEvent::Item { index: 4, completed_at: clock.now(), outcome: Err(EdgeError::bad_gateway("terminal fixture")) }; + }; + let time = OutboundTime::new(progress.clock(), super::wait_current); + let mut receiver = + callback.with(|| schedule_batch(&callback.scope(), source, &progress, &time, started)); + assert!( + observed.borrow().waits.is_empty(), + "scheduling never waits for the host" + ); + assert!( + receiver + .as_mut() + .poll_next(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert!( + observed.borrow().waits.is_empty(), + "readiness checks never wait for the host" + ); + let response_scope = callback.with(OutboundScopeOwner::response_body); + let deadline = budget(&now, 20, BudgetSource::Default).deadline; + block_on( + response_scope.run( + progress.drive(async { super::wait_current(deadline, progress.clock()).await }), + ), + ) + .unwrap() + .unwrap(); + assert!( + finished.get(), + "buffered completion runs during an unrelated wait" + ); + assert_eq!(released.get(), 1); + let Some(OutboundBatchDriverEvent::Item { + index: 4, + completed_at, + .. + }) = block_on(receiver.next()) + else { + panic!("original indexed outcome"); + }; + assert_eq!( + completed_at, started, + "collection time must not replace terminal time" + ); + drop(response_scope); + drop(callback); + } + + #[test] + fn begin_runs_without_collection_and_readiness_never_waits() { + use super::super::scope::begin; + use edgezero_core::{OutboundScopeOwner, OutboundTime}; + let callback = OutboundScopeOwner::callback(); + let (progress, observed, now) = fixture(); + let released = Rc::new(Cell::new(0)); + let body = progress.body( + handle(7, 16_385, false, &released), + budget(&now, 100, BudgetSource::PerCallTimeout), + ); + let mut pending = begin( + &callback.scope(), + Box::pin(async move { + assert_eq!(collect(body).await?, 16_385); + Err(EdgeError::bad_gateway("complete pipeline failure")) + }), + &progress, + &OutboundTime::new(progress.clock(), super::wait_current), + *now.lock().unwrap(), + ) + .unwrap(); + assert!(!pending.is_ready()); + assert!(observed.borrow().waits.is_empty()); + let deadline = budget(&now, 20, BudgetSource::Default).deadline; + block_on( + callback.run( + progress.drive(async { super::wait_current(deadline, progress.clock()).await }), + ), + ) + .unwrap() + .unwrap(); + assert_eq!(released.get(), 1); + assert!(pending.is_ready()); + assert!(pending.is_ready(), "readiness does not consume the error"); + assert_eq!( + block_on(pending.collect()).unwrap_err().message(), + "complete pipeline failure" + ); + drop(callback); + } + + #[test] + fn collected_begin_stream_outlives_callback_and_pending_cancellation() { + use super::super::scope::begin; + use edgezero_core::http::{HeaderMap, Method, StatusCode}; + use edgezero_core::{ + Body, ContentTypeProvenance, OutboundResponse, OutboundScopeOwner, OutboundTime, + }; + let callback = OutboundScopeOwner::callback(); + let (progress, observed, now) = fixture(); + let released = Rc::new(Cell::new(0)); + let body = progress.body( + handle(9, 42, false, &released), + budget(&now, 100, BudgetSource::PerCallTimeout), + ); + let response = OutboundResponse::new( + Method::GET, + StatusCode::OK, + HeaderMap::new(), + Body::from_stream(body), + progress.clock(), + ContentTypeProvenance::Unknown, + ); + let entered = *now.lock().unwrap(); + let pending = begin( + &callback.scope(), + Box::pin(async move { Ok(response) }), + &progress, + &OutboundTime::new(progress.clock(), super::wait_current), + entered, + ) + .unwrap(); + let collected = block_on(pending.collect()).unwrap(); + drop(callback); + assert_eq!( + released.get(), + 0, + "stream owns its transport after successful collection" + ); + assert!(observed.borrow().reads.is_empty()); + assert_eq!( + block_on(collected.into_bytes_bounded(42)).unwrap().len(), + 42 + ); + assert_eq!(released.get(), 1); + } + + #[test] + fn self_woken_source_yield_skips_host_wait_with_live_deadline() { + for nested in [false, true] { + let (progress, observed, now) = fixture(); + let selected = budget(&now, 30_000, BudgetSource::RequestDeadline); + let start = *now.lock().expect("clock"); + let mut timer = + progress.with_current(|| super::wait_current(selected.deadline, progress.clock())); + let mut yielded = false; + let source = poll_fn(move |cx| { + if yielded { + return Poll::Ready(()); + } + assert!(timer.as_mut().poll(cx).is_pending()); + yielded = true; + cx.waker().wake_by_ref(); + Poll::Pending + }); + let source: Pin>> = if nested { + Box::pin(async { progress.drive(source).await.expect("nested progress") }) + } else { + Box::pin(source) + }; + let mut operation = Box::pin(progress.drive(source)); + let wake_count = Arc::new(WakeCount(AtomicUsize::new(0))); + let waker = Waker::from(Arc::clone(&wake_count)); + let mut context = Context::from_waker(&waker); + assert!(operation.as_mut().poll(&mut context).is_pending()); + assert!(wake_count.0.load(Ordering::SeqCst) > 0); + assert!( + observed.borrow().waits.is_empty(), + "a ready-to-repoll source must not sleep until its deadline" + ); + assert_eq!(*now.lock().expect("clock"), start); + assert!(matches!( + operation.as_mut().poll(&mut context), + Poll::Ready(Ok(())) + )); + } + } + + #[test] + fn retained_source_waker_skips_wait_after_a_later_poll() { + let (progress, observed, now) = fixture(); + let selected = budget(&now, 30_000, BudgetSource::RequestDeadline); + let start = *now.lock().expect("clock"); + let releases = Rc::new(Cell::new(0)); + let head = progress.head(handle(101, 0, false, &releases), selected); + let mut timer = + progress.with_current(|| super::wait_current(selected.deadline, progress.clock())); + let mut retained: Option = None; + let mut yielded = false; + let source = poll_fn(move |cx| { + if yielded { + return Poll::Ready(()); + } + assert!(timer.as_mut().poll(cx).is_pending()); + if let Some(waker) = &retained { + yielded = true; + waker.wake_by_ref(); + } else { + retained = Some(cx.waker().clone()); + } + Poll::Pending + }); + let mut operation = Box::pin(progress.drive(source)); + let wake_count = Arc::new(WakeCount(AtomicUsize::new(0))); + let waker = Waker::from(Arc::clone(&wake_count)); + let mut context = Context::from_waker(&waker); + assert!(operation.as_mut().poll(&mut context).is_pending()); + assert_eq!(observed.borrow().waits.len(), 1); + let notifications = Arc::new(WakeCount(AtomicUsize::new(0))); + let replacement = Waker::from(Arc::clone(¬ifications)); + let mut replaced_context = Context::from_waker(&replacement); + assert!(operation.as_mut().poll(&mut replaced_context).is_pending()); + assert!(notifications.0.load(Ordering::SeqCst) > 0); + assert_eq!( + observed.borrow().waits.len(), + 1, + "a retained source waker still makes the current operation runnable" + ); + assert_eq!(*now.lock().expect("clock"), start); + assert!(matches!( + operation.as_mut().poll(&mut replaced_context), + Poll::Ready(Ok(())) + )); + drop(head); + assert_eq!(releases.get(), 1); + } + + #[test] + fn self_woken_poll_preserves_later_host_waits_and_expiry_publication() { + for expires_during_yield in [false, true] { + let (progress, observed, now) = fixture(); + let selected = budget(&now, 200, BudgetSource::RequestDeadline); + let mut timer = + progress.with_current(|| super::wait_current(selected.deadline, progress.clock())); + let source_now = Arc::clone(&now); + let mut yielded = false; + let source = poll_fn(move |cx| { + if yielded { + timer.as_mut().poll(cx) + } else { + assert!(timer.as_mut().poll(cx).is_pending()); + yielded = true; + if expires_during_yield { + *source_now.lock().expect("clock") = selected.deadline.instant(); + } + cx.waker().wake_by_ref(); + Poll::Pending + } + }); + let mut operation = Box::pin(progress.drive(source)); + let mut context = Context::from_waker(Waker::noop()); + assert!(operation.as_mut().poll(&mut context).is_pending()); + assert!(observed.borrow().waits.is_empty()); + assert_eq!( + progress + .state + .borrow() + .timers + .values() + .next() + .unwrap() + .outcome + .is_some(), + expires_during_yield, + "expiry must still be published during a self-woken turn" + ); + if !expires_during_yield { + assert!(operation.as_mut().poll(&mut context).is_pending()); + assert_eq!(observed.borrow().waits, [200]); + assert!(operation.as_mut().poll(&mut context).is_pending()); + assert!( + progress + .state + .borrow() + .timers + .values() + .next() + .unwrap() + .outcome + .is_some() + ); + } + assert_eq!(*now.lock().expect("clock"), selected.deadline.instant()); + assert!(matches!( + operation.as_mut().poll(&mut context), + Poll::Ready(Ok(Ok(()))) + )); + assert!(progress.state.borrow().timers.is_empty()); + } + } + + #[test] + fn nested_wait_is_bounded_by_all_ancestor_deadlines() { + let (parent, parent_observed, now) = fixture(); + let (middle, middle_observed, _) = fixture_at(Arc::clone(&now)); + let (child, child_observed, _) = fixture_at(Arc::clone(&now)); + let released = Rc::new(Cell::new(0)); + let selected = budget(&now, 200, BudgetSource::RequestDeadline); + let parent_head = parent.head(handle(0, 0, false, &released), selected); + let middle_head = middle.head( + handle(0, 0, false, &released), + budget(&now, 1_000, BudgetSource::Default), + ); + let child_head = child.head( + handle(0, 0, false, &released), + budget(&now, 10_000, BudgetSource::Default), + ); + let mut operation = Box::pin(parent.drive(middle.drive(child.drive(child_head.receive())))); + assert!( + operation + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert_eq!(child_observed.borrow().waits, [200]); + assert!(middle_observed.borrow().waits.is_empty()); + assert!(parent_observed.borrow().waits.is_empty()); + assert_eq!(*now.lock().expect("clock"), selected.deadline.instant()); + drop(operation); + assert!(matches!( + block_on(parent_head.receive()), + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + drop(middle_head); + assert_eq!(released.get(), 3); + } + + #[test] + fn custom_pair_upload_boundary_limits_child_without_parent_slots_or_timers() { + let (parent, parent_observed, now) = fixture(); + let (child, child_observed, _) = fixture_at(Arc::clone(&now)); + let selected = budget(&now, 200, BudgetSource::RequestDeadline); + parent.state.borrow_mut().boundary = Some(selected.deadline); + let clock = parent.state.borrow().clock.clone(); + let registered = Arc::new(AtomicWaker::new()); + let time = edgezero_core::OutboundTime::new(clock, move |deadline, timer_clock| { + let waiter = Arc::clone(®istered); + Box::pin(poll_fn(move |cx| { + waiter.register(cx.waker()); + if deadline.is_expired_at(timer_clock.now()) { + Poll::Ready(Ok(())) + } else { + Poll::Pending + } + })) + }); + let mut timer = time.wait_until(selected.deadline); + let released = Rc::new(Cell::new(0)); + let child_head = child.head( + handle(0, 0, false, &released), + budget(&now, 10_000, BudgetSource::Default), + ); + let mut source = Box::pin(child.drive(child_head.receive())); + let mut operation = Box::pin(parent.drive(poll_fn(|cx| { + if let Poll::Ready(result) = timer.as_mut().poll(cx) { + return Poll::Ready(result); + } + assert!(source.as_mut().poll(cx).is_pending()); + Poll::Pending + }))); + let mut context = Context::from_waker(Waker::noop()); + assert!(operation.as_mut().poll(&mut context).is_pending()); + assert!(parent.state.borrow().slots.is_empty()); + assert!(parent.state.borrow().timers.is_empty()); + assert_eq!(child_observed.borrow().waits, [200]); + assert!(parent_observed.borrow().waits.is_empty()); + assert_eq!(*now.lock().expect("clock"), selected.deadline.instant()); + assert!(matches!( + operation.as_mut().poll(&mut context), + Poll::Ready(Ok(Ok(()))) + )); + drop(operation); + drop(source); + assert_eq!(released.get(), 1); + } + + #[test] + fn child_turn_and_ready_result_repoll_parent_without_another_selection() { + let (parent, parent_observed, now) = fixture(); + let (child, child_observed, _) = fixture_at(Arc::clone(&now)); + let released = Rc::new(Cell::new(0)); + let selected = budget(&now, 1_000, BudgetSource::Default); + let parent_head = parent.head(handle(0, 0, false, &released), selected); + let child_head = child.head(handle(101, 0, false, &released), selected); + let wake_count = Arc::new(WakeCount(AtomicUsize::new(0))); + let waker = Waker::from(Arc::clone(&wake_count)); + let mut context = Context::from_waker(&waker); + let mut operation = Box::pin(parent.drive(async { + assert_eq!( + child.drive(child_head.receive()).await.unwrap().unwrap(), + 101 + ); + pending::<()>().await; + })); + assert!(operation.as_mut().poll(&mut context).is_pending()); + assert_eq!(child_observed.borrow().polls, [101]); + assert!(parent_observed.borrow().waits.is_empty()); + wake_count.0.store(0, Ordering::SeqCst); + assert!(operation.as_mut().poll(&mut context).is_pending()); + assert!( + wake_count.0.load(Ordering::SeqCst) > 0, + "a ready child must wake its parent" + ); + assert!(parent_observed.borrow().waits.is_empty()); + drop(operation); + drop(parent_head); + assert_eq!(released.get(), 2); + assert!(!parent.driving.get()); + assert!(!child.driving.get()); + } + + #[test] + fn expired_timer_can_cancel_a_sibling_from_its_waker() { + let (progress, _, now) = fixture(); + let clock = progress.state.borrow().clock.clone(); + let deadline = budget(&now, 10, BudgetSource::Default).deadline; + let sibling_deadline = budget(&now, 1_000, BudgetSource::Default).deadline; + let mut first = progress.with_current(|| super::wait_current(deadline, clock.clone())); + let sibling = progress.with_current(|| super::wait_current(sibling_deadline, clock)); + CANCEL_TIMER.with(|slot| *slot.borrow_mut() = Some(sibling)); + let cancel_waker = Waker::from(Arc::new(CancelTimer)); + assert!( + first + .as_mut() + .poll(&mut Context::from_waker(&cancel_waker)) + .is_pending() + ); + let mut operation = Box::pin(progress.drive(pending::<()>())); + let mut context = Context::from_waker(Waker::noop()); + assert!(operation.as_mut().poll(&mut context).is_pending()); + assert!(operation.as_mut().poll(&mut context).is_pending()); + assert!(CANCEL_TIMER.with(|slot| slot.borrow().is_none())); + assert_eq!(progress.state.borrow().timers.len(), 1); + assert!(matches!( + first.as_mut().poll(&mut context), + Poll::Ready(Ok(())) + )); + drop(first); + drop(operation); + assert!(progress.state.borrow().timers.is_empty()); + } + + #[test] + fn nested_live_wait_ignores_unconsumed_ancestor_timeout_mailboxes() { + let (parent, parent_observed, now) = fixture(); + let (child, child_observed, _) = fixture_at(Arc::clone(&now)); + let released = Rc::new(Cell::new(0)); + let expired = budget(&now, 10, BudgetSource::PerCallTimeout); + let retained = parent.head(handle(0, 0, false, &released), expired); + let clock = parent.state.borrow().clock.clone(); + let mut retained_timer = + parent.with_current(|| super::wait_current(expired.deadline, clock)); + *now.lock().expect("clock") = expired.deadline.instant(); + let mut publisher = Box::pin(parent.drive(pending::<()>())); + let mut context = Context::from_waker(Waker::noop()); + assert!(publisher.as_mut().poll(&mut context).is_pending()); + drop(publisher); + let selected = budget(&now, 50, BudgetSource::RequestDeadline); + let live = parent.head(handle(0, 0, false, &released), selected); + let child_head = child.head( + handle(0, 0, false, &released), + budget(&now, 10_000, BudgetSource::Default), + ); + let mut operation = Box::pin(parent.drive(child.drive(child_head.receive()))); + assert!(operation.as_mut().poll(&mut context).is_pending()); + assert_eq!(child_observed.borrow().waits, [50]); + assert!(parent_observed.borrow().waits.is_empty()); + drop(operation); + assert!(matches!( + block_on(live.receive()), + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + assert!(matches!( + block_on(retained.receive()), + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + )); + assert!(matches!( + retained_timer.as_mut().poll(&mut context), + Poll::Ready(Ok(())) + )); + drop(retained_timer); + assert!(parent.state.borrow().timers.is_empty()); + assert_eq!(released.get(), 3); + } + + #[test] + fn nested_poll_cost_keeps_original_deadline_and_checks_before_opaque_poll() { + for elapsed in [199, 200] { + let (parent, parent_observed, now) = fixture(); + let (child, child_observed, _) = fixture_at(Arc::clone(&now)); + let released = Rc::new(Cell::new(0)); + let selected = budget(&now, 200, BudgetSource::RequestDeadline); + let after_poll = budget(&now, elapsed, BudgetSource::Default) + .deadline + .instant(); + let parent_head = parent.head(handle(0, 0, false, &released), selected); + let child_head = child.head( + handle(0, 0, true, &released), + budget(&now, 10_000, BudgetSource::Default), + ); + let mut operation = Box::pin(parent.drive(child.drive(async { + *now.lock().expect("clock") = after_poll; + child_head.receive().await + }))); + assert!( + operation + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + if elapsed == 200 { + assert!( + child_observed.borrow().polls.is_empty(), + "expired ancestor must precede opaque host polling" + ); + assert!(child_observed.borrow().waits.is_empty()); + } else { + assert_eq!(child_observed.borrow().polls, [0]); + assert_eq!(child_observed.borrow().waits, [1]); + } + assert!(parent_observed.borrow().waits.is_empty()); + assert_eq!(*now.lock().expect("clock"), selected.deadline.instant()); + drop(operation); + assert!(matches!( + block_on(parent_head.receive()), + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + assert_eq!(released.get(), 2); + } + } + + #[test] + fn single_poll_progress_installs_and_restores_timer_context() { + let (progress, observed, now) = fixture(); + let deadline = budget(&now, 10, BudgetSource::RequestDeadline).deadline; + let clock = progress.clock(); + let polls = Cell::new(0_usize); + let mut timer = None; + let mut step = |cx: &mut Context<'_>| { + polls.set(polls.get().saturating_add(1)); + timer + .get_or_insert_with(|| super::wait_current(deadline, clock.clone())) + .as_mut() + .poll(cx) + }; + let mut cx = Context::from_waker(Waker::noop()); + assert!(progress.poll_once(&mut cx, &mut step).is_pending()); + assert_eq!(polls.get(), 1); + assert_eq!(observed.borrow().waits, [10]); + assert!(progress.poll_once(&mut cx, &mut step).is_pending()); + assert_eq!(polls.get(), 2); + assert!(matches!( + progress.poll_once(&mut cx, &mut step), + Poll::Ready(Ok(Ok(()))) + )); + assert_eq!(polls.get(), 3); + drop(timer); + assert!(progress.state.borrow().timers.is_empty()); + let error = block_on(super::wait_current(deadline, clock)).unwrap_err(); + assert!(matches!(error, EdgeError::Internal { .. })); + } + + #[test] + fn timer_only_wait_uses_shared_selection_and_unregisters() { + let (progress, observed, now) = fixture(); + let deadline = budget(&now, 10, BudgetSource::RequestDeadline).deadline; + let clock = progress.state.borrow().clock.clone(); + block_on(progress.drive(async { super::wait_current(deadline, clock).await })) + .expect("progress") + .expect("timer"); + assert_eq!(observed.borrow().waits, [10]); + assert_eq!(observed.borrow().domains, [Vec::::new()]); + assert!(progress.state.borrow().timers.is_empty()); + } + + #[test] + fn ready_operation_cancels_its_timer_without_host_selection() { + let (progress, observed, now) = fixture(); + let deadline = budget(&now, 10, BudgetSource::RequestDeadline).deadline; + let clock = progress.state.borrow().clock.clone(); + block_on(progress.drive(async { + let mut timer = super::wait_current(deadline, clock); + assert!( + timer + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert_eq!(progress.state.borrow().timers.len(), 1); + drop(timer); + })) + .expect("ready operation"); + assert!(observed.borrow().waits.is_empty()); + assert!(progress.state.borrow().timers.is_empty()); + } + + #[test] + fn nested_timer_is_driven_only_by_the_outer_progress_loop() { + let (progress, observed, now) = fixture(); + let deadline = budget(&now, 10, BudgetSource::RequestDeadline).deadline; + let clock = progress.state.borrow().clock.clone(); + block_on( + progress.drive(progress.drive(async { super::wait_current(deadline, clock).await })), + ) + .expect("outer") + .expect("inner") + .expect("timer"); + assert_eq!(observed.borrow().waits, [10]); + assert!(progress.state.borrow().timers.is_empty()); + assert!( + block_on(super::wait_current( + deadline, + progress.state.borrow().clock.clone() + )) + .is_err(), + "activation must be restored" + ); + } + + #[test] + fn cancelling_driver_releases_pending_timer_registration() { + let (progress, _observed, now) = fixture(); + let deadline = budget(&now, 10, BudgetSource::RequestDeadline).deadline; + let clock = progress.state.borrow().clock.clone(); + let mut operation = + Box::pin(progress.drive(async { super::wait_current(deadline, clock).await })); + assert!( + operation + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert_eq!(progress.state.borrow().timers.len(), 1); + drop(operation); + assert!(progress.state.borrow().timers.is_empty()); + } + + #[test] + fn fastly_multiplexer_preserves_sibling_progress() { + for opaque in [false, true] { + let (progress, observed, now) = fixture(); + let released = Rc::new(Cell::new(0)); + let selected = budget(&now, 50, BudgetSource::RequestDeadline); + let pending = progress.head(handle(0, 0, opaque, &released), selected); + let body = progress.body(handle(101, 32_769, false, &released), selected); + assert_eq!( + block_on(progress.drive(collect(body))) + .expect("driver") + .expect("body"), + 32_769 + ); + assert_eq!(observed.borrow().reads, [101, 101, 101, 101]); + if opaque { + assert_eq!(observed.borrow().polls, [0, 0, 0, 0]); + assert!(observed.borrow().waits.iter().all(|wait| *wait == 1)); + } else { + assert!( + observed + .borrow() + .domains + .iter() + .all(|domain| domain.contains(&0) && domain.contains(&101)) + ); + } + assert_eq!(released.get(), 1); + drop(pending); + assert_eq!(released.get(), 2); + } + } + + #[test] + fn continuously_ready_bodies_rotate_and_require_fresh_readiness() { + let (progress, observed, now) = fixture(); + let released = Rc::new(Cell::new(0)); + let selected = budget(&now, 50, BudgetSource::PerCallTimeout); + let first = progress.body(handle(101, 32_769, false, &released), selected); + let second = progress.body(handle(102, 32_769, false, &released), selected); + let (first, second) = + block_on(progress.drive(join(collect(first), collect(second)))).expect("driver"); + assert_eq!(first.expect("first"), 32_769); + assert_eq!(second.expect("second"), 32_769); + assert_eq!( + observed.borrow().reads, + [101, 102, 101, 102, 101, 102, 101, 102] + ); + assert_eq!(released.get(), 2); + } + + #[test] + fn earliest_deadlines_expire_at_equality_without_restarting_siblings() { + let (progress, observed, now) = fixture(); + let released = Rc::new(Cell::new(0)); + let first = progress.head( + handle(0, 0, false, &released), + budget(&now, 10, BudgetSource::RequestDeadline), + ); + let second = progress.head( + handle(0, 0, false, &released), + budget(&now, 50, BudgetSource::BatchCutoff), + ); + let (first, second) = + block_on(progress.drive(join(first.receive(), second.receive()))).expect("driver"); + assert!(matches!( + first, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + assert!(matches!( + second, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::BatchCutoff, + .. + }) + )); + assert_eq!(observed.borrow().waits, [10, 40]); + assert_eq!(released.get(), 2); + } + + #[test] + fn leases_cancel_only_their_slots_and_streams_retain_the_engine() { + let (progress, observed, now) = fixture(); + let weak = Rc::downgrade(&progress.state); + let released = Rc::new(Cell::new(0)); + let selected = budget(&now, 50, BudgetSource::PerCallTimeout); + let abandoned = progress.head(handle(0, 0, false, &released), selected); + let mut body = progress.body(handle(101, 16_385, false, &released), selected); + drop(abandoned); + drop(progress); + assert!(weak.upgrade().is_some()); + assert_eq!( + block_on(body.next()).expect("chunk").expect("body").len(), + 16_384 + ); + assert_eq!(observed.borrow().reads, [101]); + drop(body); + assert_eq!(released.get(), 2); + assert!(weak.upgrade().is_none()); + } + + #[test] + fn invalid_readiness_index_is_an_internal_driver_error_without_io() { + let (progress, observed, now) = fixture(); + observed.borrow_mut().invalid_index = true; + let released = Rc::new(Cell::new(0)); + let source = progress.body( + handle(101, 10, false, &released), + budget(&now, 50, BudgetSource::Default), + ); + let error = block_on(progress.drive(collect(source))).expect_err("invalid selection"); + assert!(matches!(error, EdgeError::Internal { .. })); + assert!(observed.borrow().reads.is_empty()); + assert_eq!(released.get(), 1); + } + + #[test] + fn backwards_clock_fails_before_readiness_or_source_io() { + let (progress, observed, now) = fixture(); + let released = Rc::new(Cell::new(0)); + let source = progress.body( + handle(101, 10, false, &released), + budget(&now, 50, BudgetSource::Default), + ); + let previous = *now.lock().expect("clock"); + *now.lock().expect("clock") = previous + .checked_sub(Duration::from_millis(1)) + .expect("earlier"); + assert!(matches!( + block_on(progress.drive(collect(source))), + Err(EdgeError::Internal { .. }) + )); + assert!(observed.borrow().domains.is_empty()); + assert_eq!(released.get(), 1); + } + + #[test] + fn original_deadline_outranks_late_io_and_preserves_typed_source_errors() { + for head in [false, true] { + for source_error in [false, true] { + for late in [false, true] { + let (progress, observed, now) = fixture(); + let released = Rc::new(Cell::new(0)); + let selected = budget(&now, 10, BudgetSource::RequestDeadline); + observed.borrow_mut().source_error = source_error; + if late { + observed.borrow_mut().complete_at = Some(selected.deadline.instant()); + } + let outcome = if head { + let lease = progress.head(handle(101, 0, false, &released), selected); + block_on(progress.drive(lease.receive())) + .expect("driver") + .map(|_| ()) + } else { + let source = progress.body(handle(101, 10, false, &released), selected); + block_on(progress.drive(collect(source))) + .expect("driver") + .map(|_| ()) + }; + if late { + assert!(matches!( + outcome, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + } else if source_error { + assert!(matches!( + outcome, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Transport, + .. + }) + )); + } else { + assert!(outcome.is_ok()); + } + if let Err(error) = outcome { + assert_eq!(error.provider_failure_detail().is_some(), source_error); + if source_error { + assert_eq!( + error.provider_failure_detail(), + source_failure().provider_failure_detail() + ); + } + } + assert_eq!(released.get(), 1); + } + } + } + } + + #[test] + fn spurious_head_readiness_rearms_without_reading_a_body() { + let (progress, observed, now) = fixture(); + observed.borrow_mut().spurious_heads = 2; + let released = Rc::new(Cell::new(0)); + let lease = progress.head( + handle(101, 0, false, &released), + budget(&now, 50, BudgetSource::Default), + ); + assert_eq!( + block_on(progress.drive(lease.receive())) + .expect("driver") + .expect("head"), + 101 + ); + assert_eq!(observed.borrow().polls, [101, 101, 101]); + assert_eq!(observed.borrow().domains, [vec![101], vec![101], vec![101]]); + assert!(observed.borrow().reads.is_empty()); + assert_eq!(released.get(), 1); + } + + #[test] + fn opaque_heads_rotate_with_one_shared_pulse_not_one_wait_per_slot() { + let (progress, observed, now) = fixture(); + let released = Rc::new(Cell::new(0)); + let selected = budget(&now, 50, BudgetSource::Default); + let silent = progress.head(handle(0, 0, true, &released), selected); + let ready = progress.head(handle(102, 0, true, &released), selected); + assert_eq!( + block_on(progress.drive(ready.receive())) + .expect("driver") + .expect("head"), + 102 + ); + assert_eq!(observed.borrow().polls, [0, 102]); + assert_eq!(observed.borrow().waits, [1]); + drop(silent); + assert_eq!(released.get(), 2); + } + + #[test] + fn dropping_unpolled_streams_and_heads_releases_without_host_io() { + let (progress, observed, now) = fixture(); + let released = Rc::new(Cell::new(0)); + let selected = budget(&now, 50, BudgetSource::Default); + drop(progress.head(handle(101, 0, false, &released), selected)); + drop(progress.body(handle(102, 10, false, &released), selected)); + assert_eq!(released.get(), 2); + assert!(observed.borrow().reads.is_empty()); + assert!(observed.borrow().polls.is_empty()); + assert!(observed.borrow().waits.is_empty()); + } + + #[test] + fn unconsumed_ready_mailbox_does_not_block_a_sibling() { + let (progress, observed, now) = fixture(); + let released = Rc::new(Cell::new(0)); + let selected = budget(&now, 50, BudgetSource::Default); + let retained = progress.head(handle(101, 0, false, &released), selected); + let second = progress.head(handle(102, 0, false, &released), selected); + let mut driver = Box::pin(progress.drive(second.receive())); + let mut context = Context::from_waker(noop_waker_ref()); + let mut result = Poll::Pending; + for _ in 0_u8..4 { + result = driver.as_mut().poll(&mut context); + if result.is_ready() { + break; + } + } + assert!(matches!(result, Poll::Ready(Ok(Ok(102))))); + assert_eq!(observed.borrow().polls, [101, 102]); + assert_eq!(block_on(retained.receive()).expect("retained result"), 101); + drop(driver); + assert_eq!(released.get(), 2); + } + + #[test] + fn unconsumed_timeout_is_published_once_without_starving_a_sibling() { + let (progress, observed, now) = fixture(); + let released = Rc::new(Cell::new(0)); + let selected = budget(&now, 5, BudgetSource::RequestDeadline); + let retained = progress.head(handle(0, 0, false, &released), selected); + let second = progress.head( + handle(102, 0, false, &released), + budget(&now, 50, BudgetSource::Default), + ); + *now.lock().expect("clock") = selected.deadline.instant(); + let mut driver = Box::pin(progress.drive(second.receive())); + let mut context = Context::from_waker(noop_waker_ref()); + let mut result = Poll::Pending; + for _ in 0_u8..4 { + result = driver.as_mut().poll(&mut context); + if result.is_ready() { + break; + } + } + assert!(matches!(result, Poll::Ready(Ok(Ok(102))))); + assert_eq!(observed.borrow().polls, [102]); + assert!(matches!( + block_on(retained.receive()), + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + drop(driver); + assert_eq!(released.get(), 2); + } +} diff --git a/crates/edgezero-adapter-fastly/src/outbound/scope.rs b/crates/edgezero-adapter-fastly/src/outbound/scope.rs new file mode 100644 index 000000000..e81cf4395 --- /dev/null +++ b/crates/edgezero-adapter-fastly/src/outbound/scope.rs @@ -0,0 +1,885 @@ +//! Cooperative provider scheduling; core owns every callback/body lifetime. + +use core::convert::identity; +use std::cell::{Cell, RefCell}; +use std::collections::{BTreeMap, VecDeque}; +use std::mem; +use std::pin::Pin; +use std::rc::{Rc, Weak}; +use std::task::{Context, Poll, Waker}; + +use super::progress::{Progress, Transport}; +use edgezero_core::{ + EdgeError, MonotonicInstant, OutboundBatchDriverEvent, OutboundExchangeOwner, OutboundFuture, + OutboundPending, OutboundProgressRuntime, OutboundResponse, OutboundScope, OutboundTime, +}; +use futures_util::StreamExt as _; +use futures_util::stream::{LocalBoxStream, Stream, iter, once, poll_fn}; + +#[derive(Default)] +struct Drivers { + active: RefCell>>>, + next_id: Cell, + pumping: Cell, +} + +struct Driver { + abandoned: bool, + polling: bool, + ready: VecDeque, + stream: Option>, + waker: Option, +} + +struct Scheduled { + driver: Rc>, + registration: Option<(Rc, u64)>, +} + +struct Pumping(Rc); + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for Pumping { + fn drop(&mut self) { + self.0.pumping.set(false); + } +} + +impl Scheduled { + fn unregister(&mut self) { + if let Some((drivers, id)) = self.registration.take() { + drivers.active.borrow_mut().remove(&id); + } + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for Scheduled { + fn drop(&mut self) { + self.unregister(); + let cleanup = { + let mut driver = self.driver.borrow_mut(); + driver.abandoned = true; + ( + driver.stream.take(), + mem::take(&mut driver.ready), + driver.waker.take(), + ) + }; + drop(cleanup); + } +} + +impl Stream for Scheduled { + type Item = OutboundBatchDriverEvent; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + let observation = { + let mut driver = this.driver.borrow_mut(); + if let Some(event) = driver.ready.pop_front() { + Poll::Ready(Some(event)) + } else if driver.stream.is_none() && !driver.polling { + Poll::Ready(None) + } else { + Poll::Pending + } + }; + let terminal = matches!( + &observation, + Poll::Ready( + None | Some(OutboundBatchDriverEvent::Cutoff | OutboundBatchDriverEvent::Failed(_)) + ) + ); + if terminal { + this.unregister(); + } + let previous = { + let mut driver = this.driver.borrow_mut(); + mem::replace( + &mut driver.waker, + observation.is_pending().then(|| cx.waker().clone()), + ) + }; + drop(previous); + observation + } + + fn size_hint(&self) -> (usize, Option) { + (0, None) + } +} + +pub(super) fn begin( + scope: &OutboundScope, + future: OutboundFuture, + progress: &Progress, + time: &OutboundTime, + entered: MonotonicInstant, +) -> Result +where + T: Transport + 'static, +{ + scope.check()?; + let driver = progress.clone(); + let clock = progress.clock(); + let source = async_stream::stream! { + let outcome = driver.drive(future).await.and_then(identity); + yield OutboundBatchDriverEvent::Item { index: 0, completed_at: clock.now(), outcome }; + }; + let mut receiver = schedule_batch(scope, source, progress, time, entered); + Ok(OutboundPending::new( + Box::pin(async move { + match receiver.next().await { + Some(OutboundBatchDriverEvent::Item { outcome, .. }) => outcome, + Some(OutboundBatchDriverEvent::Failed(error)) => Err(error), + _ => Err(EdgeError::internal(anyhow::anyhow!( + "Fastly begun exchange ended without a result" + ))), + } + }), + OutboundExchangeOwner::new(|| {}), + )) +} + +pub(crate) fn bind_batch( + scope: &OutboundScope, + stream: S, +) -> LocalBoxStream<'static, OutboundBatchDriverEvent> +where + S: Stream + 'static, +{ + let captured = scope.clone(); + let mut pending = stream.boxed_local(); + let activated = poll_fn(move |cx| { + captured + .with(|| pending.as_mut().poll_next(cx)) + .unwrap_or_else(|error| Poll::Ready(Some(OutboundBatchDriverEvent::Failed(error)))) + }); + let registered = scope.provider_state(Drivers::default).and_then(|drivers| { + let id = drivers.next_id.get(); + let next = id.checked_add(1).ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!( + "Fastly outbound scheduling registration exhausted" + )) + })?; + drivers.next_id.set(next); + let driver = Rc::new(RefCell::new(Driver { + abandoned: false, + polling: false, + ready: VecDeque::new(), + stream: Some(activated.boxed_local()), + waker: None, + })); + drivers + .active + .borrow_mut() + .insert(id, Rc::downgrade(&driver)); + Ok(Scheduled { + driver, + registration: Some((drivers, id)), + } + .boxed_local()) + }); + registered.unwrap_or_else(|error| { + once(async move { OutboundBatchDriverEvent::Failed(error) }).boxed_local() + }) +} + +pub(crate) fn capture() -> OutboundScope { + OutboundScope::capture(runtime()) +} + +#[cfg(feature = "fastly")] +pub(super) fn native_progress( + scope: &OutboundScope, + time: &OutboundTime, + entered: MonotonicInstant, +) -> Result { + use super::fastly_impl::{NativeProgress, NativeTransport}; + scope + .provider_state(|| { + NativeProgress::new(NativeTransport, time.clock().clone(), entered, None) + }) + .map(|progress| progress.with_clock(time.clock().clone(), entered)) +} + +pub(crate) fn poll_current(cx: &mut Context<'_>) { + let Ok(drivers) = capture().provider_state(Drivers::default) else { + return; + }; + if drivers.pumping.replace(true) { + return; + } + let _guard = Pumping(Rc::clone(&drivers)); + let active: Vec<_> = drivers + .active + .borrow() + .values() + .filter_map(Weak::upgrade) + .collect(); + for driver in active { + pump(&driver, cx); + } +} + +fn pump(driver: &Rc>, cx: &mut Context<'_>) { + let owned = { + let mut root = driver.borrow_mut(); + if root.polling || root.abandoned { + return; + } + let source = root.stream.take(); + root.polling = source.is_some(); + source + }; + let Some(mut source) = owned else { + return; + }; + let observation = source.as_mut().poll_next(cx); + let mut root = driver.borrow_mut(); + root.polling = false; + if root.abandoned { + let ready = mem::take(&mut root.ready); + drop(root); + drop(ready); + return; + } + let terminal = matches!( + &observation, + Poll::Ready( + None | Some(OutboundBatchDriverEvent::Cutoff | OutboundBatchDriverEvent::Failed(_)) + ) + ); + let waiting = if observation.is_ready() { + root.waker.take() + } else { + None + }; + if let Poll::Ready(Some(event)) = observation { + root.ready.push_back(event); + } + if !terminal { + root.stream = Some(source); + } + drop(root); + if let Some(waker) = waiting { + waker.wake(); + } +} + +pub(super) fn runtime() -> Rc { + #[cfg(feature = "fastly")] + { + Rc::new(super::fastly_impl::NativeRuntime) + } + #[cfg(not(feature = "fastly"))] + { + Rc::new(edgezero_core::IndependentOutboundProgress) + } +} + +pub(super) fn schedule_batch( + scope: &OutboundScope, + source: S, + progress: &Progress, + time: &OutboundTime, + entered: MonotonicInstant, +) -> LocalBoxStream<'static, OutboundBatchDriverEvent> +where + S: Stream + 'static, + T: Transport + 'static, +{ + let mut pending = source.boxed_local(); + let driver = progress.clone(); + let captured = scope.clone(); + let paired = time.clone(); + let polling = poll_fn(move |cx| { + let next = pending.next(); + futures_util::pin_mut!(next); + driver + .with_current(|| captured.poll(&paired, entered, cx, next.as_mut())) + .map(|result| match result { + Ok(event) => event, + Err(error) => Some(OutboundBatchDriverEvent::Failed(error)), + }) + }); + let mut receiver = bind_batch(scope, polling); + let observation = progress.with_current(|| { + scope + .with(|| { + let mut cx = Context::from_waker(Waker::noop()); + poll_current(&mut cx); + receiver.as_mut().poll_next(&mut cx) + }) + .unwrap_or_else(|error| Poll::Ready(Some(OutboundBatchDriverEvent::Failed(error)))) + }); + if let Poll::Ready(Some(event)) = observation { + iter([event]).chain(receiver).boxed_local() + } else { + receiver + } +} + +#[cfg(test)] +mod tests { + use super::super::testing; + use super::{Drivers, bind_batch, capture}; + use crate::lifecycle::with_callback; + use std::cell::{Cell, RefCell}; + use std::future::Future as _; + use std::rc::Rc; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::{Context, Poll, Waker}; + + use edgezero_core::{ + EdgeError, OutboundBatch, OutboundBatchCheck, OutboundBatchDriverEvent, OutboundBatchNext, + OutboundBatchTermination, OutboundCollectedSlot, OutboundScope, OutboundScopeKind, + OutboundScopeOwner, + }; + use futures::executor::block_on; + use futures_util::StreamExt as _; + use futures_util::stream::{LocalBoxStream, Stream, iter, pending as pending_stream, poll_fn}; + use futures_util::task::{ArcWake, waker, waker_ref}; + + struct Release(Rc>); + #[derive(Default)] + struct WakeCount(AtomicUsize); + struct CloseOnDrop; + + #[test] + #[cfg(feature = "fastly")] + fn family_progress_preserves_each_explicit_client_clock() { + use edgezero_core::{MonotonicClock, MonotonicInstant, OutboundTime}; + use std::time::Duration; + + let callback = OutboundScopeOwner::callback(); + let captured = callback.with(capture); + let first = MonotonicInstant::now(); + let second = first.checked_add(Duration::from_secs(3600)).unwrap(); + let first_time = OutboundTime::new( + MonotonicClock::new(move || first), + super::super::progress::wait_current, + ); + let second_time = OutboundTime::new( + MonotonicClock::new(move || second), + super::super::progress::wait_current, + ); + let first_progress = super::native_progress(&captured, &first_time, first).unwrap(); + let second_progress = super::native_progress(&captured, &second_time, second).unwrap(); + assert_eq!(first_progress.clock().now(), first); + assert_eq!(second_progress.clock().now(), second); + } + + std::thread_local! { + static CLOSE_ON_WAKER_DROP: RefCell> = const { RefCell::new(None) }; + } + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for Release { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for CloseOnDrop { + fn drop(&mut self) { + drop(CLOSE_ON_WAKER_DROP.with(|saved| saved.borrow_mut().take())); + } + } + impl ArcWake for CloseOnDrop { + fn wake(self: Arc) { + Self::wake_by_ref(&self); + } + fn wake_by_ref(_arc_self: &Arc) {} + } + impl ArcWake for WakeCount { + fn wake(self: Arc) { + Self::wake_by_ref(&self); + } + fn wake_by_ref(arc_self: &Arc) { + arc_self.0.fetch_add(1, Ordering::SeqCst); + } + } + + #[test] + fn scheduled_receiver_observation_never_polls_producer() { + let callback = OutboundScopeOwner::callback(); + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let mut events = [OutboundBatchDriverEvent::Item { + index: 0, + completed_at: testing::instant(), + outcome: Err(EdgeError::bad_gateway("queued fixture")), + }] + .into_iter(); + let source = poll_fn(move |_cx| { + counted.set(counted.get().saturating_add(1)); + Poll::Ready(events.next()) + }); + let mut receiver = callback.with(|| bind_batch(&callback.scope(), source)); + let mut context = Context::from_waker(Waker::noop()); + assert!(receiver.as_mut().poll_next(&mut context).is_pending()); + assert_eq!( + polls.get(), + 0, + "receiver observation cannot poll the producer" + ); + callback.with(|| super::poll_current(&mut context)); + assert_eq!(polls.get(), 1); + assert!(matches!( + receiver.as_mut().poll_next(&mut context), + Poll::Ready(Some(OutboundBatchDriverEvent::Item { index: 0, .. })) + )); + assert!(receiver.as_mut().poll_next(&mut context).is_pending()); + assert_eq!(polls.get(), 1, "empty receiver cannot resume the producer"); + callback.with(|| super::poll_current(&mut context)); + assert!(matches!( + receiver.as_mut().poll_next(&mut context), + Poll::Ready(None) + )); + assert_eq!(polls.get(), 2); + } + + fn closed(error: &EdgeError, kind: OutboundScopeKind) { + let EdgeError::Internal { source, .. } = error else { + panic!("scope closure must stay Internal: {error:?}"); + }; + assert_eq!( + source.to_string(), + format!("outbound {kind:?} scope closed") + ); + } + + fn pending_driver( + released: Rc>, + ) -> LocalBoxStream<'static, OutboundBatchDriverEvent> { + let owner = Release(released); + poll_fn(move |_cx| { + let _owner = &owner; + Poll::Pending + }) + .boxed_local() + } + + fn batch(slots: usize, source: S) -> OutboundBatch + where + S: Stream + 'static, + { + let context = testing::batch_context(slots); + let receiver = bind_batch(context.scope(), source); + OutboundBatch::from_driver(context, receiver, testing::owner()) + } + + fn item(index: usize) -> OutboundBatchDriverEvent { + OutboundBatchDriverEvent::Item { + index, + completed_at: testing::instant(), + outcome: Err(EdgeError::bad_request("slot fixture")), + } + } + + #[test] + fn callback_progress_finishes_pipeline_without_consumer_polling() { + let owner = OutboundScopeOwner::callback(); + let ready = Rc::new(Cell::new(false)); + let completed = Rc::new(Cell::new(0_usize)); + let signal = Rc::clone(&ready); + let terminal = Rc::clone(&completed); + let driver = owner.with(|| { + batch( + 1, + poll_fn(move |_cx| { + if signal.get() { + if terminal.replace(1) == 0 { + Poll::Ready(Some(item(0))) + } else { + Poll::Ready(None) + } + } else { + Poll::Pending + } + }), + ) + }); + ready.set(true); + owner.with(|| super::poll_current(&mut Context::from_waker(Waker::noop()))); + assert_eq!( + completed.get(), + 1, + "pipeline must run while its consumer is idle" + ); + owner.with(|| super::poll_current(&mut Context::from_waker(Waker::noop()))); + assert_eq!( + block_on(driver.collect()).unwrap().termination(), + OutboundBatchTermination::Completed + ); + } + + #[test] + fn callback_progress_wakes_idle_completion_consumer() { + let owner = OutboundScopeOwner::callback(); + let ready = Rc::new(Cell::new(false)); + let signal = Rc::clone(&ready); + let mut emitted = false; + let mut retained = owner.with(|| { + batch( + 1, + poll_fn(move |_cx| { + if signal.get() && !emitted { + emitted = true; + Poll::Ready(Some(item(0))) + } else if emitted { + Poll::Ready(None) + } else { + Poll::Pending + } + }), + ) + }); + let counter = Arc::new(WakeCount::default()); + let waiting = waker_ref(&counter); + let mut receive = Box::pin(retained.next()); + assert!( + receive + .as_mut() + .poll(&mut Context::from_waker(&waiting)) + .is_pending() + ); + drop(receive); + ready.set(true); + owner.with(|| super::poll_current(&mut Context::from_waker(Waker::noop()))); + assert_eq!( + counter.0.load(Ordering::SeqCst), + 1, + "producer completion wakes the original idle consumer" + ); + assert!(matches!( + block_on(retained.next()).unwrap(), + OutboundBatchNext::Item(_) + )); + } + + #[test] + fn captured_root_keeps_invocation_scope_across_response_polling() { + let callback = OutboundScopeOwner::callback(); + let captured = callback.with(capture); + let released = Rc::new(Cell::new(0)); + let mut retained = callback.with(|| batch(1, pending_driver(Rc::clone(&released)))); + let response = callback.with(OutboundScopeOwner::response_body); + let readiness = response.with(|| retained.check()).unwrap(); + assert!(matches!(readiness, OutboundBatchCheck::Pending)); + response.close(); + assert_eq!( + released.get(), + 0, + "foreign response cannot reparent callback roots" + ); + callback.close(); + assert_eq!( + released.get(), + 1, + "callback exit revokes an unpolled producer" + ); + closed(&captured.check().unwrap_err(), OutboundScopeKind::Callback); + let later = OutboundScopeOwner::callback(); + let failure = later.with(|| block_on(retained.collect()).unwrap_err()); + closed(failure.error(), OutboundScopeKind::Callback); + assert_eq!(released.get(), 1); + } + + #[test] + fn closing_scope_releases_unpolled_driver_without_polling() { + let owner = OutboundScopeOwner::callback(); + let released = Rc::new(Cell::new(0)); + let retained = owner.with(|| batch(1, pending_driver(Rc::clone(&released)))); + owner.close(); + assert_eq!(released.get(), 1); + closed( + block_on(retained.collect()).unwrap_err().error(), + OutboundScopeKind::Callback, + ); + assert_eq!(released.get(), 1); + } + + #[test] + fn closing_scope_wakes_pending_consumer_after_releasing_driver() { + let owner = OutboundScopeOwner::callback(); + let released = Rc::new(Cell::new(0)); + let retained = owner.with(|| batch(1, pending_driver(Rc::clone(&released)))); + let counter = Arc::new(WakeCount::default()); + let waiting = waker_ref(&counter); + let mut receive = Box::pin(retained.collect()); + assert!( + receive + .as_mut() + .poll(&mut Context::from_waker(&waiting)) + .is_pending() + ); + owner.close(); + assert_eq!(released.get(), 1); + assert_eq!(counter.0.load(Ordering::SeqCst), 1); + closed( + block_on(receive).unwrap_err().error(), + OutboundScopeKind::Callback, + ); + } + + #[test] + fn replacing_waker_allows_reentrant_scope_close() { + let owner = OutboundScopeOwner::callback(); + let released = Rc::new(Cell::new(0)); + let mut retained = owner.with(|| batch(1, pending_driver(Rc::clone(&released)))); + CLOSE_ON_WAKER_DROP.with(|saved| *saved.borrow_mut() = Some(owner)); + let original = waker(Arc::new(CloseOnDrop)); + let mut next = Box::pin(retained.next()); + assert!( + next.as_mut() + .poll(&mut Context::from_waker(&original)) + .is_pending() + ); + drop(next); + drop(original); + let replacement = waker(Arc::new(WakeCount::default())); + let result = { + let mut replacement_wait = Box::pin(retained.next()); + replacement_wait + .as_mut() + .poll(&mut Context::from_waker(&replacement)) + }; + assert!(matches!( + result, + Poll::Ready(Err(EdgeError::Internal { .. })) + )); + assert_eq!(released.get(), 1); + } + + #[test] + fn closing_scope_revokes_siblings_before_reentrant_cleanup() { + struct Cleanup { + rejected: Rc>, + released: Rc>, + scope: OutboundScope, + sibling: Rc>>, + } + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for Cleanup { + fn drop(&mut self) { + self.rejected.set(self.scope.check().is_err()); + let sibling = self.sibling.borrow_mut().take().expect("sibling"); + closed( + block_on(sibling.collect()).unwrap_err().error(), + OutboundScopeKind::Callback, + ); + let source = pending_driver(Rc::clone(&self.released)); + let error = self + .scope + .with(move || batch(1, source)) + .expect_err("closed scope rejects cleanup-created work"); + closed(&error, OutboundScopeKind::Callback); + } + } + let owner = OutboundScopeOwner::callback(); + let sibling = Rc::new(RefCell::new(None)); + let released = Rc::new(Cell::new(0)); + let rejected = Rc::new(Cell::new(false)); + let cleanup = Cleanup { + scope: owner.with(capture), + rejected: Rc::clone(&rejected), + released: Rc::clone(&released), + sibling: Rc::clone(&sibling), + }; + let first = owner.with(|| { + batch( + 1, + poll_fn(move |_cx| { + let _cleanup = &cleanup; + Poll::Pending + }), + ) + }); + let reads = Rc::new(Cell::new(0_usize)); + let source_reads = Rc::clone(&reads); + *sibling.borrow_mut() = Some(owner.with(|| { + batch( + 1, + poll_fn(move |_cx| { + source_reads.set(source_reads.get().saturating_add(1)); + Poll::Ready(Some(item(0))) + }), + ) + })); + owner.close(); + assert!(rejected.get()); + assert_eq!(reads.get(), 0, "cleanup cannot poll a sibling source"); + assert_eq!(released.get(), 1, "cleanup-created work cannot escape"); + drop(first); + } + + #[test] + fn driver_drop_and_terminal_unregister_without_accumulating_entries() { + let owner = OutboundScopeOwner::callback(); + let drivers = owner + .with(capture) + .provider_state(Drivers::default) + .unwrap(); + for _ in 0_u8..32 { + let released = Rc::new(Cell::new(0)); + let retained = owner.with(|| batch(1, pending_driver(Rc::clone(&released)))); + assert_eq!(drivers.active.borrow().len(), 1); + drop(retained); + assert_eq!(released.get(), 1); + assert!(drivers.active.borrow().is_empty()); + } + let retained = owner.with(|| batch(1, iter([item(0)]))); + owner.with(|| super::poll_current(&mut Context::from_waker(Waker::noop()))); + owner.with(|| super::poll_current(&mut Context::from_waker(Waker::noop()))); + assert_eq!( + block_on(retained.collect()).unwrap().termination(), + OutboundBatchTermination::Completed + ); + assert!(drivers.active.borrow().is_empty()); + } + + #[test] + fn closure_after_last_item_requires_failure_instead_of_completed() { + let owner = OutboundScopeOwner::callback(); + let mut retained = owner.with(|| batch(1, iter([item(0)]).chain(pending_stream()))); + owner.with(|| super::poll_current(&mut Context::from_waker(Waker::noop()))); + assert!(matches!( + block_on(retained.next()).unwrap(), + OutboundBatchNext::Item(_) + )); + owner.close(); + closed( + &block_on(retained.next()).unwrap_err(), + OutboundScopeKind::Callback, + ); + assert!(matches!( + block_on(retained.next()).unwrap_err(), + EdgeError::Internal { .. } + )); + } + + #[test] + fn failed_collection_preserves_results_observed_before_scope_closure() { + let owner = OutboundScopeOwner::callback(); + let retained = owner.with(|| batch(2, iter([item(0)]).chain(pending_stream()))); + owner.with(|| super::poll_current(&mut Context::from_waker(Waker::noop()))); + let mut receive = Box::pin(retained.collect()); + assert!( + receive + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + owner.close(); + let failure = block_on(receive).unwrap_err(); + closed(failure.error(), OutboundScopeKind::Callback); + assert!(matches!( + failure.slots().first(), + Some(OutboundCollectedSlot::Resolved(_)) + )); + assert!(matches!( + failure.slots().get(1), + Some(OutboundCollectedSlot::Unresolved) + )); + } + + #[test] + fn response_scope_closes_only_response_created_work() { + let callback = OutboundScopeOwner::callback(); + let callback_release = Rc::new(Cell::new(0)); + let callback_batch = + callback.with(|| batch(1, pending_driver(Rc::clone(&callback_release)))); + let body = callback.with(OutboundScopeOwner::response_body); + let body_release = Rc::new(Cell::new(0)); + let body_batch = body.with(|| batch(1, pending_driver(Rc::clone(&body_release)))); + body.close(); + assert_eq!(body_release.get(), 1); + assert_eq!(callback_release.get(), 0); + closed( + block_on(body_batch.collect()).unwrap_err().error(), + OutboundScopeKind::ResponseBody, + ); + let later_release = Rc::new(Cell::new(0)); + let later = callback.with(|| batch(1, pending_driver(Rc::clone(&later_release)))); + callback.close(); + assert_eq!(callback_release.get(), 1); + assert_eq!(later_release.get(), 1); + closed( + block_on(callback_batch.collect()).unwrap_err().error(), + OutboundScopeKind::Callback, + ); + drop(later); + } + + #[test] + fn standalone_helper_scope_ends_at_helper_return() { + let released = Rc::new(Cell::new(0)); + let retained = with_callback(|| batch(1, pending_driver(Rc::clone(&released)))); + assert_eq!(released.get(), 1); + closed( + block_on(retained.collect()).unwrap_err().error(), + OutboundScopeKind::Callback, + ); + } + + #[test] + fn unscoped_direct_driver_remains_owned_by_its_consumer() { + let released = Rc::new(Cell::new(0)); + let retained = batch(1, pending_driver(Rc::clone(&released))); + assert_eq!(released.get(), 0); + drop(retained); + assert_eq!(released.get(), 1); + } + + #[test] + fn terminal_cutoff_and_driver_failure_are_not_rewritten_by_scope_exit() { + for cutoff in [false, true] { + let owner = OutboundScopeOwner::callback(); + let scope = owner.with(capture); + let original = if cutoff { + OutboundBatchDriverEvent::Cutoff + } else { + OutboundBatchDriverEvent::Failed(EdgeError::bad_request("original driver error")) + }; + let mut driver = bind_batch(&scope, iter([original])); + owner.with(|| super::poll_current(&mut Context::from_waker(Waker::noop()))); + let terminal = block_on(driver.next()).expect("original terminal"); + owner.close(); + match terminal { + OutboundBatchDriverEvent::Cutoff => assert!(cutoff), + OutboundBatchDriverEvent::Failed(error) => { + assert!(!cutoff); + assert!(matches!(error, EdgeError::BadRequest { .. })); + assert_eq!(error.message(), "original driver error"); + } + OutboundBatchDriverEvent::Item { .. } => panic!("unexpected item"), + } + assert!(block_on(driver.next()).is_none()); + } + } + + #[test] + fn completed_batch_remains_completed_after_scope_exit() { + let owner = OutboundScopeOwner::callback(); + let mut retained = owner.with(|| batch(1, iter([item(0)]))); + owner.with(|| super::poll_current(&mut Context::from_waker(Waker::noop()))); + owner.with(|| super::poll_current(&mut Context::from_waker(Waker::noop()))); + assert!(matches!( + block_on(retained.next()).unwrap(), + OutboundBatchNext::Item(_) + )); + assert!(matches!( + block_on(retained.next()).unwrap(), + OutboundBatchNext::Finished(OutboundBatchTermination::Completed) + )); + owner.close(); + assert!(matches!( + block_on(retained.next()).unwrap(), + OutboundBatchNext::Finished(OutboundBatchTermination::Completed) + )); + } +} diff --git a/crates/edgezero-adapter-fastly/src/outbound/testing.rs b/crates/edgezero-adapter-fastly/src/outbound/testing.rs new file mode 100644 index 000000000..ac382d254 --- /dev/null +++ b/crates/edgezero-adapter-fastly/src/outbound/testing.rs @@ -0,0 +1,189 @@ +//! Obtain opaque test inputs through the real facade, never by reconstructing core policies. + +use futures_util::future::pending; +use std::cell::RefCell; +use std::iter::repeat_with; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(feature = "fastly")] +use std::task::{Context, Waker}; +use std::time::Duration; + +use edgezero_core::{ + Deadline, EdgeError, HttpClient, IndependentOutboundProgress, MonotonicClock, MonotonicInstant, + OutboundBatch, OutboundBatchContext, OutboundDispatchRequest, OutboundExchangeOwner, + OutboundFuture, OutboundHead, OutboundHttpClient, OutboundPending, OutboundPreparedBatch, + OutboundProgressRuntime, OutboundRequest, OutboundSendMode, OutboundTime, +}; + +std::thread_local! { + static DISPATCH: RefCell> = const { RefCell::new(None) }; + static BATCH: RefCell> = const { RefCell::new(None) }; +} + +struct Capture(OutboundTime, bool); + +impl OutboundHttpClient for Capture { + fn begin(&self, _: OutboundDispatchRequest) -> Result { + Err(EdgeError::internal(anyhow::anyhow!( + "capture fixture does not begin exchanges" + ))) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + DISPATCH.with(|saved| saved.replace(Some(request))); + Box::pin(pending()) + } + + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, _) = batch.into_slots(); + if !self.1 { + return OutboundBatch::cutoff(context); + } + let cutoff = context.observation_cutoff(); + BATCH.with(|saved| saved.replace(Some(context))); + HttpClient::with_client(Capture(self.0.clone(), false)) + .start_batch_until(Vec::new(), cutoff) + } + + fn time(&self) -> OutboundTime { + self.0.clone() + } + + fn validate_supported_options( + &self, + _: &OutboundRequest, + _: OutboundSendMode, + ) -> Result<(), EdgeError> { + Ok(()) + } +} + +#[cfg(feature = "fastly")] +pub(super) fn dispatch(request: OutboundRequest, time: OutboundTime) -> OutboundDispatchRequest { + let mut future = HttpClient::with_client(Capture(time, false)).send(request); + assert!( + future + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + DISPATCH.with(|saved| { + saved + .borrow_mut() + .take() + .expect("facade dispatched test request") + }) +} + +pub(crate) fn batch_context(slot_count: usize) -> OutboundBatchContext { + batch_context_at(slot_count, instant(), Duration::from_secs(60)) +} + +pub(super) fn batch_context_at( + slot_count: usize, + start: MonotonicInstant, + duration: Duration, +) -> OutboundBatchContext { + let first = AtomicBool::new(true); + let observed = start.checked_add(duration.checked_div(2).unwrap()).unwrap(); + let time = OutboundTime::new( + MonotonicClock::new(move || { + if first.swap(false, Ordering::SeqCst) { + start + } else { + observed + } + }), + |_, _| Box::pin(pending()), + ); + let client = HttpClient::with_client(Capture(time, true)); + let requests = repeat_with(|| { + OutboundRequest::get("https://example.com/") + .unwrap() + .timeout(duration) + }) + .take(slot_count) + .collect(); + drop(client.start_batch_until( + requests, + Deadline::at_instant(start.checked_add(duration).unwrap()), + )); + BATCH.with(|saved| { + saved + .borrow_mut() + .take() + .expect("facade prepared batch context") + }) +} + +#[cfg(feature = "fastly")] +pub(crate) fn batch_context_using( + slot_count: usize, + time: OutboundTime, + observation_cutoff: Deadline, +) -> OutboundBatchContext { + let requests = repeat_with(|| { + OutboundRequest::get("https://example.com/") + .unwrap() + .timeout(Duration::from_secs(60)) + }) + .take(slot_count) + .collect(); + drop( + HttpClient::with_client(Capture(time, true)) + .start_batch_until(requests, observation_cutoff), + ); + BATCH.with(|saved| { + saved + .borrow_mut() + .take() + .expect("facade prepared timed context") + }) +} + +#[cfg(not(feature = "fastly"))] +pub(crate) fn client(time: OutboundTime) -> HttpClient { + HttpClient::with_client(Capture(time, false)) +} + +pub(crate) fn owner() -> OutboundExchangeOwner { + OutboundExchangeOwner::new(|| {}) +} + +std::thread_local! { static START: MonotonicInstant = MonotonicInstant::now(); } +pub(crate) fn instant() -> MonotonicInstant { + START.with(|start| *start) +} + +#[cfg(feature = "fastly")] +pub(super) fn dispatch_at( + request: OutboundRequest, + time: OutboundTime, + start: MonotonicInstant, +) -> OutboundDispatchRequest { + use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }; + let capturing = Arc::new(AtomicBool::new(true)); + let during_preparation = Arc::clone(&capturing); + let original = time.clock().clone(); + let clock = MonotonicClock::new(move || { + if during_preparation.load(Ordering::SeqCst) { + start + } else { + original.now() + } + }); + let prepared = dispatch( + request, + OutboundTime::new(clock, move |deadline, _| time.wait_until(deadline)), + ); + capturing.store(false, Ordering::SeqCst); + prepared +} diff --git a/crates/edgezero-adapter-fastly/src/proxy.rs b/crates/edgezero-adapter-fastly/src/proxy.rs deleted file mode 100644 index eb7efc99f..000000000 --- a/crates/edgezero-adapter-fastly/src/proxy.rs +++ /dev/null @@ -1,276 +0,0 @@ -use async_stream::try_stream; -use async_trait::async_trait; -use bytes::Bytes; -use edgezero_core::body::Body; -use edgezero_core::compression::{decode_brotli_stream, decode_gzip_stream}; -use edgezero_core::error::EdgeError; -use edgezero_core::http::{HeaderMap, HeaderValue, Method, Uri, header}; -use edgezero_core::proxy::{PROXY_HEADER, ProxyClient, ProxyRequest, ProxyResponse}; -use fastly::{ - Backend, Request as FastlyRequest, Response as FastlyResponse, error::anyhow, - http::body::StreamingBody, -}; -use futures_util::stream::{BoxStream, StreamExt as _}; -use std::io::{self, Write as _}; -use std::time::Duration; - -const BACKEND_PREFIX: &str = "edgezero-dynamic-"; - -type ChunkStream = BoxStream<'static, Result, io::Error>>; - -pub struct FastlyProxyClient; - -#[async_trait(?Send)] -impl ProxyClient for FastlyProxyClient { - #[inline] - async fn send(&self, request: ProxyRequest) -> Result { - let (method, uri, headers, body, _ext) = request.into_parts(); - let backend_name = ensure_backend(&uri)?; - let fastly_request = build_fastly_request(method, &uri, &headers); - let (mut streaming_body, pending_request) = fastly_request - .send_async_streaming(&backend_name) - .map_err(EdgeError::internal)?; - forward_request_body(body, &mut streaming_body).await?; - streaming_body.finish().map_err(EdgeError::internal)?; - let mut fastly_response = pending_request.wait().map_err(EdgeError::internal)?; - - let mut proxy_response = convert_response(&mut fastly_response); - proxy_response - .headers_mut() - .insert(PROXY_HEADER, HeaderValue::from_static("fastly")); - Ok(proxy_response) - } -} - -fn build_fastly_request(method: Method, uri: &Uri, headers: &HeaderMap) -> FastlyRequest { - let mut fastly_request = FastlyRequest::new(method.clone(), uri.to_string()); - fastly_request.set_method(method); - - // Append (not set) so a multi-value client header survives; `Host` below is - // set explicitly as a single value. - for (name, value) in headers { - if name.as_str().eq_ignore_ascii_case("host") { - continue; - } - fastly_request.append_header(name.as_str(), value.clone()); - } - - // Build `Host` from host + explicit port so a non-default target port is - // preserved (origin-form fidelity) WITHOUT leaking any `user:pass@` userinfo - // that `uri.authority()` would include. (Backend connection + TLS SNI still - // key off `uri.host()` in `ensure_backend`.) - if let Some(host) = uri.host() { - let value = match uri.port() { - Some(port) => format!("{host}:{}", port.as_str()), - None => host.to_owned(), - }; - fastly_request.set_header("Host", value.as_str()); - } - - fastly_request -} - -fn convert_response(fastly_response: &mut FastlyResponse) -> ProxyResponse { - let status = fastly_response.get_status(); - let mut proxy_response = ProxyResponse::new(status, Body::empty()); - - // Preserve multi-value ORIGIN response headers (e.g. Set-Cookie): read ALL - // values per name and append, instead of first-value + insert (which - // replaced). `get_header_names()` yields `&HeaderName`, usable for both - // `get_header_all` and `append`. - for name in fastly_response.get_header_names() { - for value in fastly_response.get_header_all(name) { - proxy_response.headers_mut().append(name, value.clone()); - } - } - - let encoding = proxy_response - .headers() - .get(header::CONTENT_ENCODING) - .and_then(|value| value.to_str().ok()) - .map(str::to_ascii_lowercase); - - let body = fastly_response.take_body(); - - let chunk_stream = fastly_body_stream(body); - let body_stream = transform_stream(chunk_stream, encoding.as_deref()); - *proxy_response.body_mut() = Body::from_stream(body_stream); - if encoding.as_deref() == Some("gzip") || encoding.as_deref() == Some("br") { - proxy_response - .headers_mut() - .remove(header::CONTENT_ENCODING); - proxy_response.headers_mut().remove(header::CONTENT_LENGTH); - } - - proxy_response -} - -fn ensure_backend(uri: &Uri) -> Result { - let host = uri - .host() - .ok_or_else(|| EdgeError::bad_request("proxy target must include host"))?; - - let scheme = uri.scheme_str().unwrap_or("https"); - let is_https = scheme.eq_ignore_ascii_case("https"); - - let target_port = match (uri.port_u16(), is_https) { - (Some(port), _) => port, - (None, true) => 443, - (None, false) => 80, - }; - - let host_with_port = format!("{host}:{target_port}"); - - // Human-readable name: backend_{scheme}_{host}_{port} with dots/colons sanitised - let name_base = format!("{scheme}_{host}_{target_port}"); - let backend_name = format!("{}{}", BACKEND_PREFIX, name_base.replace(['.', ':'], "_")); - - let mut builder = Backend::builder(&backend_name, &host_with_port) - .override_host(host) - .connect_timeout(Duration::from_secs(1)) - .first_byte_timeout(Duration::from_secs(15)) - .between_bytes_timeout(Duration::from_secs(10)); - - if is_https { - builder = builder - .enable_ssl() - .sni_hostname(host) - .check_certificate(host); - log::debug!("enable ssl for backend: {backend_name}"); - } - - match builder.finish() { - Ok(_) => { - log::debug!("created dynamic backend: {backend_name} -> {host_with_port}"); - Ok(backend_name) - } - Err(err) => { - let msg = err.to_string(); - if msg.contains("NameInUse") || msg.contains("already in use") { - log::debug!("reusing existing dynamic backend: {backend_name}"); - Ok(backend_name) - } else { - Err(EdgeError::internal(anyhow!( - "dynamic backend creation failed ({backend_name} -> {host_with_port}): {msg}" - ))) - } - } - } -} - -fn fastly_body_stream(mut body: fastly::Body) -> ChunkStream { - try_stream! { - for result in body.read_chunks(8 * 1024) { - let chunk = result?; - yield chunk; - } - } - .boxed() -} - -async fn forward_request_body( - body: Body, - streaming_body: &mut StreamingBody, -) -> Result<(), EdgeError> { - match body { - Body::Once(bytes) => { - if !bytes.is_empty() { - streaming_body - .write_all(bytes.as_ref()) - .map_err(EdgeError::internal)?; - } - } - Body::Stream(mut stream) => { - while let Some(result) = stream.next().await { - let chunk = result.map_err(EdgeError::internal)?; - streaming_body - .write_all(&chunk) - .map_err(EdgeError::internal)?; - } - } - } - - streaming_body.flush().map_err(EdgeError::internal)?; - - Ok(()) -} - -fn transform_stream( - stream: ChunkStream, - encoding: Option<&str>, -) -> BoxStream<'static, Result> { - match encoding { - Some("gzip") => decode_gzip_stream(stream).boxed(), - Some("br") => decode_brotli_stream(stream).boxed(), - _ => stream.map(|res| res.map(Bytes::from)).boxed(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use brotli::CompressorWriter; - use flate2::{Compression, write::GzEncoder}; - use futures::executor::block_on; - - fn collect_body(body: Body) -> Vec { - match body { - Body::Once(bytes) => bytes.to_vec(), - Body::Stream(mut stream) => block_on(async { - let mut out = Vec::new(); - while let Some(chunk) = stream.next().await { - out.extend_from_slice(&chunk.expect("chunk")); - } - out - }), - } - } - - #[test] - fn convert_response_preserves_multi_value_set_cookie() { - let mut fastly_response = FastlyResponse::from_status(200); - fastly_response.append_header("set-cookie", "a=1"); - fastly_response.append_header("set-cookie", "b=2"); - - let proxy_response = convert_response(&mut fastly_response); - - let cookies: Vec = proxy_response - .headers() - .get_all("set-cookie") - .into_iter() - .map(|value| value.to_str().expect("utf8").to_owned()) - .collect(); - assert_eq!(cookies, vec!["a=1".to_owned(), "b=2".to_owned()]); - } - - #[test] - fn stream_handles_brotli() { - let mut compressed = Vec::new(); - let mut compressor = CompressorWriter::new(&mut compressed, 4096, 5, 21); - compressor.write_all(b"hello brotli").unwrap(); - drop(compressor); - - let mut br_body = fastly::Body::new(); - br_body.write_all(&compressed).unwrap(); - let body = Body::from_stream(transform_stream(fastly_body_stream(br_body), Some("br"))); - let collected = collect_body(body); - assert_eq!(collected, b"hello brotli"); - } - - #[test] - fn stream_handles_identity_and_gzip() { - let mut plain = fastly::Body::new(); - plain.write_all(b"plain").unwrap(); - let plain_body = Body::from_stream(transform_stream(fastly_body_stream(plain), None)); - assert_eq!(collect_body(plain_body), b"plain"); - - let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); - encoder.write_all(b"hello gzip").unwrap(); - let compressed = encoder.finish().unwrap(); - let mut gz_body = fastly::Body::new(); - gz_body.write_all(&compressed).unwrap(); - let gzip_body = - Body::from_stream(transform_stream(fastly_body_stream(gz_body), Some("gzip"))); - assert_eq!(collect_body(gzip_body), b"hello gzip"); - } -} diff --git a/crates/edgezero-adapter-fastly/src/request.rs b/crates/edgezero-adapter-fastly/src/request.rs index bbaf6c23f..e6cd0e8da 100644 --- a/crates/edgezero-adapter-fastly/src/request.rs +++ b/crates/edgezero-adapter-fastly/src/request.rs @@ -1,28 +1,48 @@ use std::collections::{HashSet, VecDeque}; use std::fmt::Display; -use std::io::Read as _; +#[cfg(all(feature = "test-utils", target_arch = "wasm32"))] +use std::io::Cursor; +use std::io::Read; use std::sync::{Arc, Mutex, OnceLock, PoisonError}; +use std::task::Poll; +use bytes::Bytes; use edgezero_core::app::{App, StoreMetadata, StoresMetadata}; use edgezero_core::body::Body; -use edgezero_core::config_store::ConfigStoreHandle; +use edgezero_core::config_store::{ConfigStoreHandle, ConfigStoreOpenFailure}; use edgezero_core::error::EdgeError; -use edgezero_core::http::{Extensions, Request, request_builder}; -use edgezero_core::key_value_store::KvHandle; -use edgezero_core::proxy::ProxyHandle; +#[cfg(all(feature = "test-utils", target_arch = "wasm32"))] +use edgezero_core::http::Uri; +use edgezero_core::http::{Extensions, Method, Request, Response, request_builder}; +use edgezero_core::ingress::{ + IngressBeginOutcome, IngressDispatchOutcome, IngressFraming, IngressHeadAccounting, + IngressHeadParts, PreparedIngress, +}; +use edgezero_core::key_value_store::{KvError, KvHandle, KvStoreOpenFailure}; +use edgezero_core::outbound::HttpClient; +use edgezero_core::response_egress::ResponseEgressEnvelope; use edgezero_core::secret_store::SecretHandle; use edgezero_core::store_registry::{ BoundSecretStore, ConfigRegistry, ConfigStoreBinding, KvRegistry, SecretRegistry, StoreRegistry, }; -use fastly::{Error as FastlyError, Request as FastlyRequest, Response as FastlyResponse}; +use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; +use fastly::{Error as FastlyError, Request as FastlyRequest}; use futures::executor; +use futures_util::stream; use std::collections::BTreeMap; use crate::config_store::FastlyConfigStore; use crate::context::FastlyRequestContext; +use crate::delivery::{ + DispatchOutcome, FastlyNoAttempt, FastlyNoAttemptReason, FastlyRequestDelivery, + FastlyRequestHeadFailure, FastlyResponseDelivery, finish_request, +}; +use crate::init_fastly_abi; use crate::key_value_store::FastlyKvStore; -use crate::proxy::FastlyProxyClient; -use crate::response::{from_core_response, parse_uri}; +use crate::lifecycle::with_callback; +use crate::outbound::drive_callback; +use crate::outbound::{FastlyOutboundClient, native_time}; +use crate::response::{parse_uri, send_egress_response}; use crate::secret_store::FastlySecretStore; const WARNED_STORE_CACHE_LIMIT: usize = 64; @@ -93,7 +113,7 @@ enum ConfigSource { /// .with_kv("sessions").require_kv() /// .with_config("app_config") /// .with_secrets() -/// .dispatch(req) +/// .send() /// ``` pub struct FastlyService<'app> { app: &'app App, @@ -107,30 +127,52 @@ struct KvSource { required: bool, } +struct HeadConversionFailure { + cause: FastlyRequestHeadFailure, + error: EdgeError, +} + enum SecretSource { Off, On { required: bool }, } +impl DispatchOutcome { + #[cfg(any(test, feature = "test-utils"))] + fn into_envelope(self) -> Result { + match self { + Self::Detached(envelope) | Self::Routed(envelope) => Ok(envelope), + Self::Aborted => Err(FastlyError::msg("ingress admission aborted")), + } + } +} + impl<'app> FastlyService<'app> { - /// Resolve every wired store at request time and dispatch - /// against the wrapped `App`. Consumes the service so a builder - /// can't be reused with stale wiring. - /// - /// # Errors - /// Returns an error if a required store cannot be opened or - /// the underlying handler returns an error. + /// Test-only request-in/egress-envelope dispatch seam. + #[cfg(feature = "test-utils")] + #[doc(hidden)] #[inline] - pub fn dispatch(self, req: FastlyRequest) -> Result { + pub fn capture_egress_for_test( + self, + req: FastlyRequest, + ) -> Result { + self.dispatch_with(req, |_req, _extensions| {}) + .map_err(|reason| FastlyError::msg(reason.message())) + .and_then(DispatchOutcome::into_envelope) + } + + fn dispatch_with( + self, + req: FastlyRequest, + prepare: Prepare, + ) -> Result + where + Prepare: FnOnce(&mut FastlyRequest, &mut Extensions), + { + let request_start = self.app.monotonic_now(); let config_store = match self.config { ConfigSource::Handle(handle) => Some(handle), - ConfigSource::Name(name) => match FastlyConfigStore::try_open(&name) { - Ok(store) => Some(ConfigStoreHandle::new(Arc::new(store))), - Err(err) => { - warn_missing_store_once(&name, &err.to_string()); - None - } - }, + ConfigSource::Name(name) => Some(open_config_handle(&name)), ConfigSource::None => None, }; let kv = match self.kv { @@ -150,7 +192,8 @@ impl<'app> FastlyService<'app> { secrets, ..Default::default() }, - |_req, _extensions| {}, + request_start, + prepare, ) } @@ -191,6 +234,83 @@ impl<'app> FastlyService<'app> { self } + fn run_request_with_delivery( + self, + req: FastlyRequest, + prepare: Prepare, + finalize: Finalize, + deliver: Deliver, + ) -> FastlyRequestDelivery + where + Prepare: FnOnce(&mut FastlyRequest, &mut Extensions), + Finalize: FnOnce(&mut Response) -> State, + Deliver: FnOnce(ResponseEgressEnvelope) -> FastlyResponseDelivery, + { + with_callback(|| { + init_fastly_abi(); + let outcome = self.dispatch_with(req, prepare); + finish_request(outcome, finalize, deliver) + }) + } + + /// Exercises the closed request/response lifecycle with an injected delivery boundary. + #[cfg(any(test, feature = "test-utils"))] + #[doc(hidden)] + #[inline] + pub fn run_request_with_hooks_for_test( + self, + req: FastlyRequest, + prepare: Prepare, + finalize: Finalize, + deliver: Deliver, + ) -> FastlyRequestDelivery + where + Prepare: FnOnce(&mut FastlyRequest, &mut Extensions), + Finalize: FnOnce(&mut Response) -> State, + Deliver: FnOnce(ResponseEgressEnvelope) -> FastlyResponseDelivery, + { + self.run_request_with_delivery(req, prepare, finalize, deliver) + } + + /// Resolve every wired store, receive the client request, and transmit its response. + /// + /// # Errors + /// Returns an error if a required store cannot be opened or + /// the underlying handler returns an error. + #[inline] + pub fn send(self) -> Result<(), FastlyError> { + with_callback(|| { + init_fastly_abi(); + let req = FastlyRequest::from_client(); + self.send_request_with_hooks(req, |_req, _extensions| {}, |_response| ()) + .into_standard_result() + .map(|_state| ()) + }) + } + + /// Runs an already-received request through the closed `EdgeZero` lifecycle. + /// + /// `prepare` may mutate the borrowed raw request and add portable extensions before conversion and + /// ingress validation. Insert a validated `SynthesizedResponseHeaders` to replace the app's + /// generated-error/fallback headers for this request, including an explicitly empty set. + /// `finalize` may mutate only a routed response before egress policy/framing. The + /// adapter retains response ownership and returns its disposition, last-body commitment and + /// exact terminal report. Routed hook state survives failed delivery and fallback. Detached + /// responses skip `finalize`. Inspect replacement permission before sending a native response. + #[inline] + pub fn send_request_with_hooks( + self, + req: FastlyRequest, + prepare: Prepare, + finalize: Finalize, + ) -> FastlyRequestDelivery + where + Prepare: FnOnce(&mut FastlyRequest, &mut Extensions), + Finalize: FnOnce(&mut Response) -> State, + { + self.run_request_with_delivery(req, prepare, finalize, send_egress_response) + } + /// Open the Fastly Config Store named `name` and inject its /// handle into request extensions. If the store is unavailable /// at request time, the dispatcher logs the warning once and @@ -224,9 +344,9 @@ impl<'app> FastlyService<'app> { /// Open a Fastly KV Store by `name` and inject its handle. /// Non-required by default: an absent store logs once and - /// dispatch continues. Pair with `require_kv()` when the - /// manifest declares `[stores.kv]` and a missing store should - /// fail loudly. + /// dispatch continues. Pair with `require_kv()` to explicitly fail + /// before dispatch. Canonical metadata-driven declarations instead + /// open lazily and return typed failures only when used. #[must_use] #[inline] pub fn with_kv(mut self, name: S) -> Self @@ -262,18 +382,24 @@ impl<'app> FastlyService<'app> { } } +fn detached_error_outcome( + app: &App, + error: EdgeError, + request_method: Method, + request_start: MonotonicInstant, +) -> DispatchOutcome { + match app.detached_ingress_error_egress(error, request_method, request_start) { + IngressDispatchOutcome::Response(envelope) => DispatchOutcome::Detached(*envelope), + IngressDispatchOutcome::Aborted | _ => DispatchOutcome::Aborted, + } +} + fn dispatch_core_request( app: &App, mut core_request: Request, stores: Stores, -) -> Result { - insert_store_registries(&mut core_request, stores); - let response = executor::block_on(app.router().oneshot(core_request)) - .map_err(|err| map_edge_error(&err))?; - from_core_response(response).map_err(|err| map_edge_error(&err)) -} - -fn insert_store_registries(core_request: &mut Request, stores: Stores) { + prepared: PreparedIngress, +) -> DispatchOutcome { // Hard-cutoff: legacy bare handles are no longer // inserted into request extensions. `with_config_handle` // still accepts a `ConfigStoreHandle`, but the dispatcher @@ -292,38 +418,118 @@ fn insert_store_registries(core_request: &mut Request, stores: Stores) { if let Some(registry) = secret_registry { core_request.extensions_mut().insert(registry); } + let clock = prepared.monotonic_clock(); + let entered = prepared.request_start(); + let method = prepared.request_method().clone(); + match executor::block_on(drive_callback( + Box::pin(app.dispatch_admitted(prepared, core_request)), + clock, + entered, + )) { + Ok(response) => DispatchOutcome::Routed(response), + Err(error) => detached_error_outcome(app, error, method, entered), + } } -/// Run an app-provided closure against a scratch `Extensions` populated from the -/// RAW `fastly::Request` (JA4 / H2 / etc.), BEFORE `into_core_request` consumes -/// the request. Returns the scratch bag to be `extend`ed into the core request. -fn apply_request_extend(req: &FastlyRequest, extend: F) -> Extensions +/// Run an app-provided closure against the borrowed raw request and a scratch extension bag before +/// conversion consumes the request. +fn apply_request_prepare(req: &mut FastlyRequest, prepare: Prepare) -> Extensions where - F: FnOnce(&FastlyRequest, &mut Extensions), + Prepare: FnOnce(&mut FastlyRequest, &mut Extensions), { let mut scratch = Extensions::default(); - extend(req, &mut scratch); + prepare(req, &mut scratch); scratch } -fn dispatch_with_handles( +fn dispatch_with_handles( app: &App, - req: FastlyRequest, + mut req: FastlyRequest, stores: Stores, - extend: F, -) -> Result + request_start: MonotonicInstant, + prepare: Prepare, +) -> Result where - F: FnOnce(&FastlyRequest, &mut Extensions), + Prepare: FnOnce(&mut FastlyRequest, &mut Extensions), { - // Read raw-request signals into a scratch bag BEFORE conversion consumes `req`. - let scratch = apply_request_extend(&req, extend); - let mut core_request = into_core_request(req).map_err(|err| map_edge_error(&err))?; - core_request.extensions_mut().extend(scratch); - dispatch_core_request(app, core_request, stores) + let mut scratch = apply_request_prepare(&mut req, prepare); + let synthesized_headers = scratch.remove::(); + let mut head_request = + into_core_request_head(&req, app.monotonic_clock()).map_err(|failure| { + FastlyNoAttempt::new( + FastlyNoAttemptReason::RequestHeadConversion(failure.cause), + map_edge_error(&failure.error), + ) + })?; + head_request.extensions_mut().extend(scratch); + let outcome = dispatch_ingress_reader(app, head_request, stores, request_start, move || { + Ok(req.try_take_body()) + }); + Ok(match (outcome, synthesized_headers) { + (DispatchOutcome::Detached(envelope), Some(headers)) => { + DispatchOutcome::Detached(envelope.with_synthesized_response_headers(headers)) + } + (DispatchOutcome::Routed(envelope), Some(headers)) => { + DispatchOutcome::Routed(envelope.with_synthesized_response_headers(headers)) + } + (unchanged, _) => unchanged, + }) +} + +fn dispatch_ingress_reader( + app: &App, + mut head_request: Request, + stores: Stores, + request_start: MonotonicInstant, + make_source: MakeSource, +) -> DispatchOutcome +where + Source: Read + 'static, + MakeSource: FnOnce() -> Result, EdgeError>, +{ + let request_method = head_request.method().clone(); + let head_parts = IngressHeadParts::from_request( + &head_request, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ); + if let Err(error) = head_parts.validate_normalized(app.ingress_head_limits()) { + return detached_error_outcome(app, error, request_method, request_start); + } + let prepared = match app.begin_ingress(head_parts, request_start) { + Ok(outcome) => match outcome { + IngressBeginOutcome::Admitted(prepared) => prepared, + IngressBeginOutcome::Refused(response) => { + return DispatchOutcome::Detached(response); + } + IngressBeginOutcome::Aborted => return DispatchOutcome::Aborted, + _ => { + return detached_error_outcome( + app, + EdgeError::internal(anyhow::anyhow!("unsupported ingress admission outcome")), + request_method, + request_start, + ); + } + }, + Err(error) => { + return detached_error_outcome(app, error, request_method, request_start); + } + }; + let source = match make_source() { + Ok(source) => source, + Err(error) => { + return DispatchOutcome::Detached(app.admitted_error_egress(prepared, error)); + } + }; + *head_request.body_mut() = fastly_deadline_body( + source, + Some((prepared.read_deadline(), prepared.monotonic_clock())), + ); + dispatch_core_request(app, head_request, stores, prepared) } -/// Dispatch with per-id store registries built from baked metadata — the same -/// store wiring [`run_app`](crate::run_app) uses. +/// Dispatch with per-id store registries built from baked metadata and closed lifecycle hooks. /// /// Fastly is `Multi` for all three kinds. Each declared ID is the stable /// resource-link alias opened by the runtime. Config always uses the logical ID @@ -332,71 +538,97 @@ where /// /// ```rust,ignore /// let stores = MyHooks::stores(); -/// dispatch_with_registries(&app, req, stores, |_req, _extensions| {}) +/// send_request_with_registries_and_hooks(&app, stores, req, |_req, _extensions| {}, |_response| ()) /// ``` /// /// [`FastlyService`]'s bare-handle path binds `default_key: "default"` instead /// of the logical ID. /// -/// KV failures escalate via `resolve_kv_handle`'s `kv_required=true` path; -/// missing config / secret stores degrade silently with a one-time warning. +/// Declared KV bindings open on first use, at most once per request, without blocking +/// preparation or routes that ignore KV. Missing/invalid bindings fail at use with 500; +/// transient open failures fail with 503. Explicit `FastlyService::require_kv()` still +/// fails before dispatch. Failed declared config bindings remain typed failing handles, and secret +/// bindings open their logical alias when extraction requests the value. /// -/// # Errors -/// Returns an error if a declared KV store cannot be opened, or if the -/// underlying handler returns an error. +/// Reports no-attempt, policy abort, detached or routed delivery. Declared KV opening errors are +/// handled through the application's normal error response path when the store is used. #[inline] -pub fn dispatch_with_registries( +pub fn send_with_registries_and_hooks( app: &App, - req: FastlyRequest, stores: StoresMetadata, - extend: F, -) -> Result + prepare: Prepare, + finalize: Finalize, +) -> FastlyRequestDelivery where - F: FnOnce(&FastlyRequest, &mut Extensions), + Prepare: FnOnce(&mut FastlyRequest, &mut Extensions), + Finalize: FnOnce(&mut Response) -> State, { - let core_request = into_core_request_with_registries(req, stores, extend)?; - let response = executor::block_on(app.router().oneshot(core_request)) - .map_err(|err| map_edge_error(&err))?; - from_core_response(response).map_err(|err| map_edge_error(&err)) + with_callback(|| { + init_fastly_abi(); + let req = FastlyRequest::from_client(); + send_request_with_registries_and_hooks(app, stores, req, prepare, finalize) + }) } -/// Convert a Fastly request and insert the registries derived from baked store -/// metadata, without dispatching or converting a response. +/// Deliver an already-received request through canonical admission and owned egress. /// -/// The extension callback reads the native request before conversion. The -/// registries are then built from the logical aliases linked to the deployed -/// service version. Drive `app.router().oneshot(request)` with -/// `futures::executor::block_on` when the caller needs response extensions or -/// progressive streaming. +/// Use this inside a retained serving loop; it never receives another client request. +/// Request registries, preparation, finalization, and body handles remain request-local. /// -/// # Errors -/// Returns an error if a declared KV store cannot be opened or the request -/// cannot be converted, including an inbound body read failure. +/// Returns an owned disposition and delivery report without sending a second response. +/// Routed finalization state is retained even when delivery fails or a fallback is delivered. #[inline] -pub fn into_core_request_with_registries( - req: FastlyRequest, +pub fn send_request_with_registries_and_hooks( + app: &App, stores: StoresMetadata, - extend: F, -) -> Result + req: FastlyRequest, + prepare: Prepare, + finalize: Finalize, +) -> FastlyRequestDelivery where - F: FnOnce(&FastlyRequest, &mut Extensions), + Prepare: FnOnce(&mut FastlyRequest, &mut Extensions), + Finalize: FnOnce(&mut Response) -> State, { - let kv_registry = build_kv_registry(stores.kv)?; - let config_registry = build_config_registry(stores.config); - let secret_registry = build_secret_registry(stores.secrets); - let scratch = apply_request_extend(&req, extend); - let mut core_request = into_core_request(req).map_err(|err| map_edge_error(&err))?; - core_request.extensions_mut().extend(scratch); - insert_store_registries( - &mut core_request, - Stores { - config_registry, - kv_registry, - secret_registry, - ..Default::default() - }, - ); - Ok(core_request) + with_callback(|| { + init_fastly_abi(); + let request_start = app.monotonic_now(); + let kv_registry = build_kv_registry(stores.kv); + let config_registry = build_config_registry(stores.config); + let secret_registry = build_secret_registry(stores.secrets); + let outcome = dispatch_with_handles( + app, + req, + Stores { + config_registry, + kv_registry, + secret_registry, + ..Default::default() + }, + request_start, + prepare, + ); + finish_request(outcome, finalize, send_egress_response) + }) +} + +/// Delivers an assembly failure for an already-received request without an application. +/// +/// Drops the unread request and uses the default redacted renderer and finite write deadline. +/// No preparation, finalization, store opening or app observer runs. The returned detached +/// delivery owns the terminal report. Inspect commitment before an explicit native replacement; +/// this helper never resends, and unknown or committed delivery never permits replacement. +#[inline] +pub fn send_assembly_error( + req: FastlyRequest, + error: EdgeError, + request_start: MonotonicInstant, + clock: MonotonicClock, +) -> FastlyRequestDelivery<()> { + init_fastly_abi(); + let method = req.get_method().clone(); + drop(req); + let envelope = App::assembly_error_egress(error, method, request_start, clock); + FastlyRequestDelivery::Detached(send_egress_response(envelope)) } /// Pure synthesis: collapse a `Stores` (which may carry both a @@ -443,52 +675,42 @@ fn synthesise_store_registries( (config_registry, kv_registry, secret_registry) } -fn build_kv_registry(kv_meta: Option) -> Result, FastlyError> { - let Some(meta) = kv_meta else { - return Ok(None); - }; +fn build_kv_registry(kv_meta: Option) -> Option { + let meta = kv_meta?; let mut by_id: BTreeMap = BTreeMap::new(); for id in meta.ids { - // KV is required: if `[stores.kv]` is declared, an id failing to open - // is a runtime error rather than a silent degradation. - let Some(handle) = resolve_kv_handle(id, true)? else { - continue; - }; - by_id.insert((*id).to_owned(), handle); - } - let default_id = meta.default.to_owned(); - if !by_id.contains_key(&default_id) { - log::warn!( - "KV registry default id `{default_id}` could not be opened; dropping the KV registry" + by_id.insert( + (*id).to_owned(), + KvHandle::new(Arc::new(FastlyKvStore::declared(id))), ); } - Ok(StoreRegistry::from_parts(by_id, default_id)) + let default_id = meta.default.to_owned(); + StoreRegistry::from_parts(by_id, default_id) } fn build_config_registry(config_meta: Option) -> Option { let meta = config_meta?; let mut by_id: BTreeMap = BTreeMap::new(); for id in meta.ids { - match FastlyConfigStore::try_open(id) { - Ok(store) => { - by_id.insert( - (*id).to_owned(), - ConfigStoreBinding { - handle: ConfigStoreHandle::new(Arc::new(store)), - default_key: (*id).to_owned(), - }, - ); - } - Err(err) => warn_missing_store_once(id, &err.to_string()), - } - } - let default_id = meta.default.to_owned(); - if !by_id.contains_key(&default_id) { - log::warn!( - "config registry default id `{default_id}` could not be opened; dropping the config registry" + by_id.insert( + (*id).to_owned(), + ConfigStoreBinding { + handle: open_config_handle(id), + default_key: (*id).to_owned(), + }, ); } - StoreRegistry::from_parts(by_id, default_id) + StoreRegistry::from_parts(by_id, meta.default.to_owned()) +} + +fn open_config_handle(store_name: &str) -> ConfigStoreHandle { + match FastlyConfigStore::try_open(store_name) { + Ok(store) => ConfigStoreHandle::new(Arc::new(store)), + Err(err) => { + warn_missing_store_once(store_name, &err.to_string()); + ConfigStoreHandle::failed_open(ConfigStoreOpenFailure::InvalidConfiguration) + } + } } fn build_secret_registry(secret_meta: Option) -> Option { @@ -515,21 +737,32 @@ fn build_secret_registry(secret_meta: Option) -> Option Result { + let request = + into_core_request_head(&req, MonotonicClock::default()).map_err(|failure| failure.error)?; + Ok(attach_core_body(&mut req, request, None)) +} + +fn into_core_request_head( + req: &FastlyRequest, + outbound_clock: MonotonicClock, +) -> Result { let method = req.get_method().clone(); - let uri = parse_uri(req.get_url_str())?; + let uri = parse_uri(req.get_url_str()).map_err(|error| HeadConversionFailure { + cause: FastlyRequestHeadFailure::InvalidUri, + error, + })?; let mut builder = request_builder().method(method).uri(uri); for (name, value) in req.get_headers() { builder = builder.header(name.as_str(), value.as_bytes()); } - let mut body = req.take_body(); - let mut bytes = Vec::new(); - body.read_to_end(&mut bytes).map_err(EdgeError::internal)?; - let mut request = builder - .body(Body::from(bytes)) - .map_err(EdgeError::internal)?; + .body(Body::empty()) + .map_err(|error| HeadConversionFailure { + cause: FastlyRequestHeadFailure::RequestBuilder, + error: EdgeError::internal(error), + })?; let context = FastlyRequestContext { client_ip: req.get_client_ip_addr(), @@ -537,11 +770,192 @@ pub fn into_core_request(mut req: FastlyRequest) -> Result { FastlyRequestContext::insert(&mut request, context); request .extensions_mut() - .insert(ProxyHandle::with_client(FastlyProxyClient)); + .insert(outbound_client(outbound_clock)); Ok(request) } +fn outbound_client(clock: MonotonicClock) -> HttpClient { + HttpClient::with_client(FastlyOutboundClient::with_time(native_time(clock))) +} + +fn attach_core_body( + req: &mut FastlyRequest, + mut request: Request, + read_lifetime: Option<(Deadline, MonotonicClock)>, +) -> Request { + *request.body_mut() = fastly_deadline_body(req.try_take_body(), read_lifetime); + request +} + +fn fastly_deadline_body( + source: Option, + read_lifetime: Option<(Deadline, MonotonicClock)>, +) -> Body +where + Source: Read + 'static, +{ + // SDK host length omits unread/read-ahead and pending-write buffers; only absence is enough. + let Some(reader) = source else { + return Body::empty(); + }; + let mut native_body = Some(reader); + let mut terminal = false; + let stream = stream::poll_fn(move |_cx| { + if terminal { + return Poll::Ready(None); + } + if read_lifetime + .as_ref() + .is_some_and(|(deadline, clock)| deadline.is_expired_at(clock.now())) + { + terminal = true; + native_body.take(); + return Poll::Ready(Some(Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )))); + } + + let Some(body) = native_body.as_mut() else { + terminal = true; + return Poll::Ready(None); + }; + let mut chunk = vec![0_u8; 16 * 1024]; + let result = body.read(&mut chunk); + if read_lifetime + .as_ref() + .is_some_and(|(deadline, clock)| deadline.is_expired_at(clock.now())) + { + terminal = true; + native_body.take(); + return Poll::Ready(Some(Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )))); + } + match result { + Ok(0) => { + terminal = true; + native_body.take(); + Poll::Ready(None) + } + Ok(read) => { + chunk.truncate(read); + Poll::Ready(Some(Ok(Bytes::from(chunk)))) + } + Err(error) => { + terminal = true; + native_body.take(); + Poll::Ready(Some(Err(EdgeError::internal(error)))) + } + } + }); + Body::from_stream(stream) +} + +/// Dispatches an observable reader through the production ingress body wrapper. +#[cfg(all(feature = "test-utils", target_arch = "wasm32"))] +#[doc(hidden)] +#[inline] +pub fn dispatch_ingress_reader_for_test( + app: &App, + method: Method, + uri: Uri, + source: Source, +) -> Result +where + Source: Read + 'static, +{ + let request_start = app.monotonic_now(); + let mut core_request = request_builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .map_err(|error| map_edge_error(&EdgeError::internal(error)))?; + core_request + .extensions_mut() + .insert(outbound_client(app.monotonic_clock())); + dispatch_ingress_reader( + app, + core_request, + Stores::default(), + request_start, + move || Ok(Some(source)), + ) + .into_envelope() +} + +/// Exercises admission ordering with observable reader construction and delivery closures. +#[cfg(all(feature = "test-utils", target_arch = "wasm32"))] +#[doc(hidden)] +#[inline] +pub fn dispatch_ingress_with_for_test( + app: &App, + method: Method, + uri: Uri, + make_source: MakeSource, + deliver: Deliver, +) -> Result<(), FastlyError> +where + Source: Read + 'static, + MakeSource: FnOnce() -> Result, + Deliver: FnOnce(ResponseEgressEnvelope) -> Result<(), FastlyError>, +{ + let request_start = app.monotonic_now(); + let mut core_request = request_builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .map_err(|error| map_edge_error(&EdgeError::internal(error)))?; + core_request + .extensions_mut() + .insert(outbound_client(app.monotonic_clock())); + let outcome = dispatch_ingress_reader( + app, + core_request, + Stores::default(), + request_start, + move || make_source().map(Some), + ); + match outcome { + DispatchOutcome::Aborted => Err(FastlyError::msg("ingress admission aborted")), + DispatchOutcome::Detached(envelope) | DispatchOutcome::Routed(envelope) => { + deliver(envelope) + } + } +} + +/// Exercises a reader-construction failure after admission through the production seam. +#[cfg(all(feature = "test-utils", target_arch = "wasm32"))] +#[doc(hidden)] +#[inline] +pub fn dispatch_ingress_source_error_for_test( + app: &App, + method: Method, + uri: Uri, +) -> Result { + let request_start = app.monotonic_now(); + let mut core_request = request_builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .map_err(|error| map_edge_error(&EdgeError::internal(error)))?; + core_request + .extensions_mut() + .insert(outbound_client(app.monotonic_clock())); + dispatch_ingress_reader( + app, + core_request, + Stores::default(), + request_start, + || -> Result>>, EdgeError> { + Err(EdgeError::internal(anyhow::anyhow!( + "fastly source construction failed" + ))) + }, + ) + .into_envelope() +} + fn map_edge_error(err: &EdgeError) -> FastlyError { FastlyError::msg(err.to_string()) } @@ -549,14 +963,20 @@ fn map_edge_error(err: &EdgeError) -> FastlyError { fn resolve_kv_handle( kv_store_name: &str, kv_required: bool, -) -> Result, FastlyError> { +) -> Result, FastlyNoAttempt> { match FastlyKvStore::open(kv_store_name) { Ok(store) => Ok(Some(KvHandle::new(Arc::new(store)))), Err(err) => { if kv_required { - return Err(FastlyError::msg(format!( - "KV store '{kv_store_name}' is explicitly configured but could not be opened: {err}" - ))); + let reason = if let KvError::OpenFailed { reason } = &err { + *reason + } else { + KvStoreOpenFailure::Unavailable + }; + return Err(FastlyNoAttempt::new( + FastlyNoAttemptReason::RequiredStore(reason), + FastlyError::new(err), + )); } warn_missing_kv_store_once(kv_store_name, &err); Ok(None) @@ -617,18 +1037,49 @@ fn warn_missing_store_once(store_name: &str, detail: &str) { #[cfg(test)] mod synthesis_tests { use super::*; - use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; + use edgezero_core::config_store::{ + BoundedStoreRead, ConfigStore, ConfigStoreError, ConfigStoreHandle, + finish_bounded_config_read, + }; + use edgezero_core::context::RequestContext; use edgezero_core::key_value_store::{KvStore, NoopKvStore}; + use edgezero_core::response_egress_framing::ResponseEgressFramingMode; + use edgezero_core::router::RouterService; use edgezero_core::secret_store::{NoopSecretStore, SecretHandle}; use std::collections::BTreeMap; use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::time::Duration; struct StubConfig; #[async_trait::async_trait(?Send)] impl ConfigStore for StubConfig { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Ok(None) } + + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + finish_bounded_config_read( + self.get(key).await, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + } } fn kv_handle() -> KvHandle { @@ -645,14 +1096,97 @@ mod synthesis_tests { } #[test] - fn apply_request_extend_populates_scratch_from_raw_request() { + fn declared_kv_registry_construction_never_opens_a_provider() { + let registry = build_kv_registry(Some(StoreMetadata { + default: "unlinked_f8_fixture", + ids: &["unlinked_f8_fixture"], + })) + .expect("declared registry"); + assert!(registry.named("unlinked_f8_fixture").is_some()); + assert!(registry.default().is_some()); + assert!(build_kv_registry(None).is_none()); + } + + #[cfg(target_arch = "wasm32")] + #[test] + fn declared_kv_open_failure_retains_binding_and_recovers() { + use edgezero_core::http::StatusCode; + + let metadata = StoreMetadata { + default: "unlinked_f8_fixture", + ids: &["unlinked_f8_fixture"], + }; + let registry = build_kv_registry(Some(metadata)).expect("declared registry"); + assert!(registry.named("unlinked_f8_fixture").is_some()); + assert!(registry.named("undeclared").is_none()); + let handle = registry.default().expect("declared default binding"); + let first_error = + executor::block_on(handle.get_bytes("marker")).expect_err("unlinked store"); + assert_eq!( + EdgeError::from(first_error).status(), + StatusCode::INTERNAL_SERVER_ERROR + ); + for error in [ + executor::block_on(handle.exists("marker")).unwrap_err(), + executor::block_on(handle.put_bytes("marker", Bytes::from_static(b"value"))) + .unwrap_err(), + executor::block_on(handle.put_bytes_with_ttl( + "marker", + Bytes::from_static(b"value"), + Duration::from_mins(1), + )) + .unwrap_err(), + executor::block_on(handle.delete("marker")).unwrap_err(), + executor::block_on(handle.list_keys_page("", None, 1)).unwrap_err(), + ] { + assert_eq!( + EdgeError::from(error).status(), + StatusCode::INTERNAL_SERVER_ERROR + ); + } + } + + #[cfg(target_arch = "wasm32")] + #[test] + fn explicitly_required_kv_failure_precedes_prepare_and_delivery() { + let app = App::new(RouterService::builder().build()); + let prepared = AtomicUsize::new(0); + let delivered = AtomicUsize::new(0); + let result = FastlyService::new(&app) + .with_kv("unlinked_f8_fixture") + .require_kv() + .run_request_with_hooks_for_test( + FastlyRequest::get("https://example.com/"), + |_, _| { + prepared.fetch_add(1, Ordering::SeqCst); + }, + |_| (), + |_| { + delivered.fetch_add(1, Ordering::SeqCst); + panic!("required store failure must not deliver") + }, + ); + let FastlyRequestDelivery::NoAttempt(reason) = result else { + panic!("explicit required store must fail before dispatch"); + }; + assert!(matches!( + reason.reason(), + FastlyNoAttemptReason::RequiredStore(_) + )); + assert!(!reason.message().contains("unlinked_f8_fixture")); + assert_eq!(prepared.load(Ordering::SeqCst), 0); + assert_eq!(delivered.load(Ordering::SeqCst), 0); + } + + #[test] + fn apply_request_prepare_populates_scratch_from_raw_request() { use edgezero_core::http::Method; #[derive(Clone, Debug, PartialEq)] struct Ja4(String); - let raw = FastlyRequest::new(Method::GET, "http://example.test/"); - let scratch = apply_request_extend(&raw, |req, extensions| { + let mut raw = FastlyRequest::new(Method::GET, "http://example.test/"); + let scratch = apply_request_prepare(&mut raw, |req, extensions| { // A real closure would call req.get_tls_ja4(); deriving from the URL // keeps the assertion deterministic under Viceroy. let marker = req.get_url_str().to_owned(); @@ -665,6 +1199,67 @@ mod synthesis_tests { ); } + #[test] + fn standard_service_installs_the_exact_application_outbound_clock() { + use edgezero_core::http::Method; + + async fn elapsed(ctx: RequestContext) -> Result { + let client = ctx + .http_client() + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("missing HTTP client")))?; + let request = + edgezero_core::OutboundRequest::get("https://example.com/")?.stream_response(); + let results = client + .send_all_until(vec![request], Deadline::after(Duration::from_secs(1))) + .await + .map_err(|failure| failure.into_parts().0)?; + let elapsed = results + .slots() + .first() + .and_then(|slot| match slot { + edgezero_core::OutboundCollectedSlot::Resolved(result) => Some(result), + edgezero_core::OutboundCollectedSlot::AlreadyDelivered + | edgezero_core::OutboundCollectedSlot::Unresolved => None, + }) + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("missing batch slot")))? + .elapsed; + Ok(elapsed.as_millis().to_string()) + } + + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(7)) + .expect("completed instant"); + let observations = Arc::new(AtomicUsize::new(0)); + let clock_observations = Arc::clone(&observations); + let mut app = App::new(RouterService::builder().get("/clock", elapsed).build()); + app.set_monotonic_clock(MonotonicClock::new(move || { + if clock_observations.fetch_add(1, Ordering::SeqCst) < 2 { + start + } else { + completed + } + })); + let request = FastlyRequest::new(Method::GET, "http://example.test/clock"); + + let envelope = FastlyService::new(&app) + .dispatch_with(request, |_request, _extensions| {}) + .expect("dispatch") + .into_envelope() + .expect("response envelope"); + let Ok((prepared, _, mut attempt, egress_clock)) = + envelope.begin(ResponseEgressFramingMode::HostAutomatic) + else { + panic!("begin response egress"); + }; + assert!(attempt.begin_writing()); + assert!(attempt.complete(egress_clock.now())); + let response = prepared.into_response(); + + assert_eq!(response.body().as_bytes(), Some(b"7".as_slice())); + assert!(observations.load(Ordering::SeqCst) >= 3); + } + #[test] fn extended_request_extensions_are_visible_to_handler() { use edgezero_core::body::Body; @@ -678,7 +1273,6 @@ mod synthesis_tests { async fn handler(ctx: RequestContext) -> Result { let ja4 = ctx - .request() .extensions() .get::() .map_or_else(|| "missing".to_owned(), |value| value.0.clone()); @@ -786,7 +1380,7 @@ mod synthesis_tests { // Regression for the `.with_secrets()` bug — the pre-fix // `resolve_secret_handle(false)` short-circuited to `None`, so the - // documented `.with_secrets().dispatch(...)` path silently ran + // documented `.with_secrets().send()` path silently ran // handlers without a `SecretRegistry`. After the fix, the handle // is always built when `SecretSource::On` is selected; `_required` // is reserved for whichever future per-secret-lookup availability diff --git a/crates/edgezero-adapter-fastly/src/response.rs b/crates/edgezero-adapter-fastly/src/response.rs index f9eb4d020..0ddc65b00 100644 --- a/crates/edgezero-adapter-fastly/src/response.rs +++ b/crates/edgezero-adapter-fastly/src/response.rs @@ -1,38 +1,744 @@ -use edgezero_core::body::Body; +#![cfg_attr( + feature = "fastly", + expect( + clippy::arbitrary_source_item_ordering, + reason = "portable protocol traits precede the gated Fastly ABI lifecycle implementation" + ) +)] + +#[cfg(feature = "fastly")] +#[path = "fastly_response_abi.rs"] +mod fastly_abi; + +#[cfg(feature = "fastly")] +use core::convert::identity; +use std::task::Poll; + +use bytes::Bytes; +use edgezero_core::body::{Body, BodyPayload, BodyStream}; use edgezero_core::error::EdgeError; -use edgezero_core::http::{Response, Uri}; -use fastly::Response as FastlyResponse; +use edgezero_core::http::{HeaderMap, Method, Response, StatusCode, Uri}; +use edgezero_core::outbound::{OutboundFuture, OutboundTime}; +use edgezero_core::response_egress::{ + RESPONSE_EGRESS_FALLBACK_SAFETY_BUDGET, ResponseEgressAttempt, ResponseEgressEnvelope, + ResponseEgressFallbackDisposition, ResponseEgressOutcome, ResponseEgressPolicy, +}; +use edgezero_core::response_egress_framing::ResponseEgressFramingMode; +use edgezero_core::response_egress_framing::{ + PreparedResponseEgress, response_egress_source_error_category, +}; +use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; use futures::executor; use futures_util::StreamExt as _; -use std::io::Write as _; +use futures_util::future::poll_fn; + +use crate::delivery::{DeliveryError, FastlyResponseCommitState, FastlyResponseDelivery}; +#[cfg(feature = "fastly")] +use crate::outbound::{drive_callback, native_time}; + +const INTERNAL_FALLBACK_BODY: &[u8] = b"internal server error"; +const TIMEOUT_FALLBACK_BODY: &[u8] = b"response write deadline exceeded"; + +struct BufferedBody<'body> { + bytes: &'body [u8], + clock: &'body MonotonicClock, + deadline: Deadline, +} + +impl BufferedBody<'_> { + fn stage( + &self, + mut write: impl FnMut(&[u8]) -> Result, + ) -> Result<(), DeliveryError> { + let mut remaining = self.bytes; + while !remaining.is_empty() { + ensure_deadline(self.deadline, self.clock)?; + let result = write(remaining); + ensure_deadline(self.deadline, self.clock)?; + let written = result.map_err(|_error| DeliveryError::Transport)?; + if written == 0 || written > remaining.len() { + return Err(DeliveryError::Transport); + } + remaining = remaining.get(written..).ok_or(DeliveryError::Transport)?; + } + Ok(()) + } +} + +#[derive(Debug)] +enum StreamRead { + Source(Option>), + WriteDeadline, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum TransmitFailure { + Postcommit(DeliveryError), + Precommit { + cause: ResponseEgressOutcome, + error: DeliveryError, + }, + Uncommitted(DeliveryError), + Unknown(DeliveryError), +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum TransmitKind { + Application, + Fallback, +} + +trait EgressSink { + type Error; + + fn abandon(&mut self) -> Result<(), Self::Error>; + fn finish(&mut self) -> Result<(), Self::Error>; + fn write(&mut self, bytes: &[u8]) -> Result; +} + +trait EgressCommitter { + type Error; + type Prepared; + type Sink: EgressSink; + + fn commit(&mut self, prepared: Self::Prepared) -> Result; + fn prepare( + &mut self, + status: StatusCode, + headers: &HeaderMap, + transmits_body: bool, + buffered_body: Option>, + ) -> Result; +} -/// # Errors -/// Returns [`EdgeError::Internal`] if the response body cannot be streamed to the Fastly send-channel. -#[inline] -pub fn from_core_response(response: Response) -> Result { - let (parts, body) = response.into_parts(); - let mut fastly_response = FastlyResponse::from_status(parts.status.as_u16()); +#[cfg(feature = "fastly")] +mod platform { + use super::{ + BufferedBody, DeliveryError, EgressCommitter, EgressSink, FastlyResponseDelivery, + HeaderMap, ResponseEgressEnvelope, StatusCode, manual_stream_framing, send_egress_with, + }; + use std::mem; - match body { - Body::Once(bytes) => fastly_response.set_body(bytes.to_vec()), - Body::Stream(mut stream) => { - let mut fastly_body = fastly::Body::new(); - while let Some(result) = executor::block_on(stream.next()) { - let chunk = result.map_err(EdgeError::internal)?; - fastly_body.write_all(&chunk).map_err(EdgeError::internal)?; + use fastly_shared::{INVALID_BODY_HANDLE, INVALID_RESPONSE_HANDLE}; + use fastly_sys::{BodyHandle as FastlyBodyHandle, ResponseHandle as FastlyResponseHandle}; + + use super::fastly_abi::{self, FastlyAbiError}; + + pub(super) struct FastlyPrepared { + body: FastlyBodyHandle, + response: FastlyResponseHandle, + streaming: bool, + } + + impl FastlyPrepared { + fn commit(mut self) -> Result { + let response = mem::replace(&mut self.response, INVALID_RESPONSE_HANDLE); + let body = mem::replace(&mut self.body, INVALID_BODY_HANDLE); + if self.streaming { + if fastly_abi::send_streaming(response, body).is_err() { + let _result = fastly_abi::abandon_body(body); + return Err(FastlyAbiError); + } + Ok(FastlyBodySink(body)) + } else { + fastly_abi::send_buffered(response, body)?; + Ok(FastlyBodySink(INVALID_BODY_HANDLE)) + } + } + + fn new( + status: StatusCode, + headers: &HeaderMap, + transmits_body: bool, + buffered_body: Option>, + ) -> Result { + let response = + fastly_abi::new_response().map_err(|_error| DeliveryError::Conversion)?; + let mut prepared = Self { + body: INVALID_BODY_HANDLE, + response, + streaming: buffered_body.is_none(), + }; + prepared.body = fastly_abi::new_body().map_err(|_error| DeliveryError::Conversion)?; + fastly_abi::set_status(prepared.response, status.as_u16()) + .map_err(|_error| DeliveryError::Conversion)?; + for (name, value) in headers { + fastly_abi::append_header( + prepared.response, + name.as_str().as_bytes(), + value.as_bytes(), + ) + .map_err(|_error| DeliveryError::Conversion)?; + } + if manual_stream_framing(headers, transmits_body, prepared.streaming) { + fastly_abi::set_manual_framing(prepared.response) + .map_err(|_error| DeliveryError::Conversion)?; + } + if let Some(buffered) = buffered_body { + buffered.stage(|remaining| fastly_abi::write_body(prepared.body, remaining))?; + } + Ok(prepared) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for FastlyPrepared { + fn drop(&mut self) { + if self.body != INVALID_BODY_HANDLE { + let _result = fastly_abi::close_body(self.body); + self.body = INVALID_BODY_HANDLE; + } + if self.response != INVALID_RESPONSE_HANDLE { + let _result = fastly_abi::close_response(self.response); + self.response = INVALID_RESPONSE_HANDLE; } - fastly_response.set_body(fastly_body); } } - // `append_header` preserves multi-value headers (e.g. N `Set-Cookie`). The - // response starts empty (`from_status`) and `http::HeaderMap` iteration - // yields one entry per value, so appending is unconditionally correct. - for (name, value) in &parts.headers { - fastly_response.append_header(name.as_str(), value.as_bytes()); + pub(super) struct FastlyBodySink(FastlyBodyHandle); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for FastlyBodySink { + fn drop(&mut self) { + if self.0 != INVALID_BODY_HANDLE { + let _result = fastly_abi::abandon_body(self.0); + self.0 = INVALID_BODY_HANDLE; + } + } } - Ok(fastly_response) + impl EgressSink for FastlyBodySink { + type Error = FastlyAbiError; + + fn abandon(&mut self) -> Result<(), Self::Error> { + if self.0 == INVALID_BODY_HANDLE { + return Ok(()); + } + let handle = mem::replace(&mut self.0, INVALID_BODY_HANDLE); + fastly_abi::abandon_body(handle) + } + + fn finish(&mut self) -> Result<(), Self::Error> { + if self.0 == INVALID_BODY_HANDLE { + return Err(FastlyAbiError); + } + let handle = mem::replace(&mut self.0, INVALID_BODY_HANDLE); + fastly_abi::close_body(handle) + } + + fn write(&mut self, bytes: &[u8]) -> Result { + if self.0 == INVALID_BODY_HANDLE { + return Err(FastlyAbiError); + } + fastly_abi::write_body(self.0, bytes) + } + } + + pub(super) struct FastlyCommitter; + + impl EgressCommitter for FastlyCommitter { + type Error = FastlyAbiError; + type Prepared = FastlyPrepared; + type Sink = FastlyBodySink; + + fn commit(&mut self, prepared: Self::Prepared) -> Result { + prepared.commit() + } + + fn prepare( + &mut self, + status: StatusCode, + headers: &HeaderMap, + transmits_body: bool, + buffered_body: Option>, + ) -> Result { + FastlyPrepared::new(status, headers, transmits_body, buffered_body) + } + } + + /// Sends buffers atomically and streams through an owned downstream body handle. + /// + /// Returns its exact terminal report and last-body commitment. Precommit conversion and + /// deadline failures first attempt the bounded fallback on the same response-egress attempt. + pub(crate) fn send_egress_response(egress: ResponseEgressEnvelope) -> FastlyResponseDelivery { + send_egress_with(egress, &mut FastlyCommitter) + } +} + +#[cfg(feature = "fastly")] +pub(crate) use platform::send_egress_response; + +fn send_egress_with( + egress: ResponseEgressEnvelope, + committer: &mut Committer, +) -> FastlyResponseDelivery +where + Committer: EgressCommitter, +{ + let request_method = egress.request_method().clone(); + let (attempt, transmit_result) = match egress.begin(ResponseEgressFramingMode::HostAutomatic) { + Ok((prepared, policy, mut attempt, clock)) => { + let result = match transmit_prepared( + prepared, + policy, + &mut attempt, + &clock, + TransmitKind::Application, + committer, + ) { + Err(TransmitFailure::Precommit { cause, .. }) => { + send_fallback(&request_method, cause, &mut attempt, &clock, committer) + } + result => result, + }; + (attempt, result) + } + Err(failure) => { + let (mut attempt, clock, cause) = failure.into_parts(); + let result = send_fallback(&request_method, cause, &mut attempt, &clock, committer); + (attempt, result) + } + }; + let (commitment, delivery_result) = match transmit_result { + Ok(()) => (FastlyResponseCommitState::Committed, Ok(())), + Err(TransmitFailure::Postcommit(error)) => { + (FastlyResponseCommitState::Committed, Err(error)) + } + Err(TransmitFailure::Unknown(error)) => (FastlyResponseCommitState::Unknown, Err(error)), + Err(TransmitFailure::Uncommitted(error) | TransmitFailure::Precommit { error, .. }) => { + (FastlyResponseCommitState::NotCommitted, Err(error)) + } + }; + FastlyResponseDelivery::new(commitment, attempt.into_terminal_report(), delivery_result) +} + +fn send_fallback( + request_method: &Method, + original_cause: ResponseEgressOutcome, + attempt: &mut ResponseEgressAttempt, + clock: &MonotonicClock, + committer: &mut Committer, +) -> Result<(), TransmitFailure> +where + Committer: EgressCommitter, +{ + if !attempt.begin_fallback(original_cause) { + log::error!("response-egress fallback state transition failed"); + attempt.terminate(ResponseEgressOutcome::ConversionError, clock.now()); + return Err(TransmitFailure::Uncommitted(DeliveryError::Conversion)); + } + + let fallback_started_at = clock.now(); + let fallback_deadline = fallback_started_at + .checked_add(RESPONSE_EGRESS_FALLBACK_SAFETY_BUDGET) + .unwrap_or(fallback_started_at); + let fallback_response = fallback_response(original_cause); + let prepared = match attempt.prepare_response( + ResponseEgressFramingMode::HostAutomatic, + request_method, + fallback_response, + ) { + Ok(prepared) => prepared, + Err(_error) => { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, clock.now()); + return Err(TransmitFailure::Uncommitted(DeliveryError::Conversion)); + } + }; + let policy = ResponseEgressPolicy { + write_deadline: Deadline::at_instant(fallback_deadline), + }; + transmit_prepared( + prepared, + policy, + attempt, + clock, + TransmitKind::Fallback, + committer, + ) +} + +fn fallback_response(cause: ResponseEgressOutcome) -> Response { + let (status, body) = if cause == ResponseEgressOutcome::DeadlineExceeded { + (StatusCode::GATEWAY_TIMEOUT, TIMEOUT_FALLBACK_BODY) + } else { + (StatusCode::INTERNAL_SERVER_ERROR, INTERNAL_FALLBACK_BODY) + }; + let mut response = Response::new(Body::from(body)); + *response.status_mut() = status; + response +} + +fn transmit_prepared( + prepared: PreparedResponseEgress, + policy: ResponseEgressPolicy, + attempt: &mut ResponseEgressAttempt, + clock: &MonotonicClock, + kind: TransmitKind, + committer: &mut Committer, +) -> Result<(), TransmitFailure> +where + Committer: EgressCommitter, +{ + if policy.write_deadline.is_expired_at(clock.now()) { + return precommit_failure( + kind, + attempt, + clock, + ResponseEgressOutcome::DeadlineExceeded, + ); + } + + let transmits_body = prepared.transmits_body(); + let (parts, body) = prepared.into_response().into_parts(); + // Suppressed payload size is not representation size. An empty bulk body would + // let HTTP/2 synthesize a false Content-Length: 0 on HEAD/304. + let buffered_bytes = if transmits_body { + body.as_bytes() + } else { + None + }; + let buffered_length = buffered_bytes.map(<[u8]>::len); + let buffered_body = buffered_bytes.map(|bytes| BufferedBody { + bytes, + clock, + deadline: policy.write_deadline, + }); + let prepared_head = + match committer.prepare(parts.status, &parts.headers, transmits_body, buffered_body) { + Ok(prepared_head) => prepared_head, + Err(error) => { + log::error!("response-egress Fastly head preparation failed"); + let cause = if error == DeliveryError::Deadline + || policy.write_deadline.is_expired_at(clock.now()) + { + ResponseEgressOutcome::DeadlineExceeded + } else { + ResponseEgressOutcome::ConversionError + }; + return precommit_failure(kind, attempt, clock, cause); + } + }; + + let commit_started_at = clock.now(); + if policy.write_deadline.is_expired_at(commit_started_at) { + return precommit_failure( + kind, + attempt, + clock, + ResponseEgressOutcome::DeadlineExceeded, + ); + } + if !attempt.begin_writing() { + log::error!("response-egress writing state transition failed"); + settle_failure( + kind, + attempt, + ResponseEgressOutcome::TransportError, + clock.now(), + ); + return Err(TransmitFailure::Uncommitted(DeliveryError::Transport)); + } + + let sink = match committer.commit(prepared_head) { + Ok(sink) => sink, + Err(_error) => { + log::error!("response-egress Fastly downstream commit failed"); + settle_failure( + kind, + attempt, + ResponseEgressOutcome::TransportError, + clock.now(), + ); + return Err(TransmitFailure::Unknown(DeliveryError::Transport)); + } + }; + + let committed_at = clock.now(); + if let Some(length) = buffered_length { + // A successful bulk send consumes the complete native body. Never truncate it by + // entering streaming cleanup, even if the clock expires during that hostcall. + let accepted = + u64::try_from(length).is_ok_and(|bytes| attempt.account_bytes(bytes, committed_at)); + let terminal_error = if policy.write_deadline.is_expired_at(committed_at) { + Some(DeliveryError::Deadline) + } else if !accepted { + Some(DeliveryError::Transport) + } else { + None + }; + if let Some(error) = terminal_error { + settle_failure(kind, attempt, error.outcome(), committed_at); + return Err(TransmitFailure::Postcommit(error)); + } + settle_success(kind, attempt, committed_at); + return Ok(()); + } + transmit_stream(body, sink, policy, attempt, clock, kind, committed_at) +} + +fn transmit_stream( + body: Body, + mut sink: Sink, + policy: ResponseEgressPolicy, + attempt: &mut ResponseEgressAttempt, + clock: &MonotonicClock, + kind: TransmitKind, + committed_at: MonotonicInstant, +) -> Result<(), TransmitFailure> +where + Sink: EgressSink, +{ + if policy.write_deadline.is_expired_at(committed_at) { + abandon(&mut sink); + settle_failure( + kind, + attempt, + ResponseEgressOutcome::DeadlineExceeded, + committed_at, + ); + return Err(TransmitFailure::Postcommit(DeliveryError::Deadline)); + } + + let pump_result = pump_body(body, &mut sink, policy.write_deadline, attempt, clock); + if let Err(error) = pump_result { + abandon(&mut sink); + settle_failure(kind, attempt, error.outcome(), clock.now()); + return Err(TransmitFailure::Postcommit(error)); + } + + if policy.write_deadline.is_expired_at(clock.now()) { + abandon(&mut sink); + settle_failure( + kind, + attempt, + ResponseEgressOutcome::DeadlineExceeded, + clock.now(), + ); + return Err(TransmitFailure::Postcommit(DeliveryError::Deadline)); + } + // Fastly consumes the native handle before reporting a close failure, so no + // second abandon operation is available after this boundary. + let finish_result = sink.finish(); + let observed_at = clock.now(); + let finish_failed = finish_result.is_err(); + let finish_error = if policy.write_deadline.is_expired_at(observed_at) { + Some(DeliveryError::Deadline) + } else if finish_failed { + Some(DeliveryError::Transport) + } else { + None + }; + if let Some(error) = finish_error { + if finish_failed { + log::error!("response-egress Fastly body finish failed"); + } + settle_failure(kind, attempt, error.outcome(), observed_at); + return Err(TransmitFailure::Postcommit(error)); + } + + settle_success(kind, attempt, observed_at); + Ok(()) +} + +fn pump_body( + body: Body, + sink: &mut Sink, + deadline: Deadline, + attempt: &mut ResponseEgressAttempt, + clock: &MonotonicClock, +) -> Result<(), DeliveryError> +where + Sink: EgressSink, +{ + let (payload, _scope) = body.into_parts(); + match payload { + BodyPayload::Once(bytes) => write_chunk(bytes.as_ref(), sink, deadline, attempt, clock), + BodyPayload::Stream(mut source) => { + #[cfg(feature = "fastly")] + let time = native_time(clock.clone()); + #[cfg(feature = "fastly")] + let mut write_timer = None; + loop { + let observed_at = clock.now(); + if deadline.is_expired_at(observed_at) { + return Err(DeliveryError::Deadline); + } + #[cfg(feature = "fastly")] + let read = executor::block_on(drive_callback( + read_stream_until(&mut source, &time, deadline, &mut write_timer), + clock.clone(), + observed_at, + )) + .and_then(identity) + .map_err(|error| { + attempt.record_source_error(&error); + DeliveryError::Source + })?; + #[cfg(feature = "fastly")] + let next = match read { + StreamRead::WriteDeadline => return Err(DeliveryError::Deadline), + StreamRead::Source(next) => next, + }; + #[cfg(not(feature = "fastly"))] + let next = executor::block_on(source.next()); + if let Some(Err(error)) = &next { + attempt.record_source_error(error); + } + ensure_deadline(deadline, clock)?; + match next { + Some(Ok(bytes)) => { + write_chunk(bytes.as_ref(), sink, deadline, attempt, clock)?; + } + Some(Err(error)) => { + log::warn!( + "response-egress Fastly body source failed after commit: {}", + response_egress_source_error_category(&error) + ); + return Err(DeliveryError::Source); + } + None => return Ok(()), + } + } + } + } +} + +async fn read_stream_until( + source: &mut BodyStream, + time: &OutboundTime, + deadline: Deadline, + write_timer: &mut Option>, +) -> Result { + poll_fn(|cx| { + let timer = write_timer.get_or_insert_with(|| time.wait_until(deadline)); + if let Poll::Ready(result) = timer.as_mut().poll(cx) { + drop(write_timer.take()); + return Poll::Ready(result.map(|()| StreamRead::WriteDeadline)); + } + source + .poll_next_unpin(cx) + .map(|next| Ok(StreamRead::Source(next))) + }) + .await +} + +fn write_chunk( + bytes: &[u8], + sink: &mut Sink, + deadline: Deadline, + attempt: &mut ResponseEgressAttempt, + clock: &MonotonicClock, +) -> Result<(), DeliveryError> +where + Sink: EgressSink, +{ + let mut offset = 0; + while offset < bytes.len() { + ensure_deadline(deadline, clock)?; + let remaining = bytes.get(offset..).ok_or(DeliveryError::Transport)?; + let write_result = sink.write(remaining); + let observed_at = clock.now(); + if deadline.is_expired_at(observed_at) { + if let Ok(accepted_size) = write_result + && accepted_size <= remaining.len() + { + let accepted_u64 = + u64::try_from(accepted_size).map_err(|_error| DeliveryError::Transport)?; + if !attempt.account_bytes(accepted_u64, observed_at) { + return Err(DeliveryError::Transport); + } + } + return Err(DeliveryError::Deadline); + } + let accepted_size = match write_result { + Ok(0) => return Err(DeliveryError::Transport), + Ok(accepted) if accepted <= remaining.len() => accepted, + Ok(_) | Err(_) => return Err(DeliveryError::Transport), + }; + let accepted_u64 = + u64::try_from(accepted_size).map_err(|_error| DeliveryError::Transport)?; + if !attempt.account_bytes(accepted_u64, observed_at) { + return Err(DeliveryError::Transport); + } + offset = offset + .checked_add(accepted_size) + .ok_or(DeliveryError::Transport)?; + } + Ok(()) +} + +fn ensure_deadline(deadline: Deadline, clock: &MonotonicClock) -> Result<(), DeliveryError> { + if deadline.is_expired_at(clock.now()) { + Err(DeliveryError::Deadline) + } else { + Ok(()) + } +} + +fn abandon(sink: &mut Sink) +where + Sink: EgressSink, +{ + if sink.abandon().is_err() { + log::error!("response-egress Fastly body abandon failed"); + } +} + +fn precommit_failure( + kind: TransmitKind, + attempt: &mut ResponseEgressAttempt, + clock: &MonotonicClock, + cause: ResponseEgressOutcome, +) -> Result<(), TransmitFailure> { + let error = DeliveryError::from_outcome(cause); + if kind == TransmitKind::Fallback { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, clock.now()); + } + Err(TransmitFailure::Precommit { cause, error }) +} + +fn settle_failure( + kind: TransmitKind, + attempt: &mut ResponseEgressAttempt, + outcome: ResponseEgressOutcome, + observed_at: MonotonicInstant, +) { + match kind { + TransmitKind::Application => { + attempt.terminate(outcome, observed_at); + } + TransmitKind::Fallback => { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, observed_at); + } + } +} + +fn settle_success( + kind: TransmitKind, + attempt: &mut ResponseEgressAttempt, + observed_at: MonotonicInstant, +) { + match kind { + TransmitKind::Application => { + attempt.complete(observed_at); + } + TransmitKind::Fallback => { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Completed, observed_at); + } + } +} + +fn manual_stream_framing(headers: &HeaderMap, transmits_body: bool, streaming: bool) -> bool { + streaming + && transmits_body + && headers.contains_key("content-length") + && !headers.contains_key("x-compress-hint") } pub(crate) fn parse_uri(uri: &str) -> Result { @@ -42,11 +748,550 @@ pub(crate) fn parse_uri(uri: &str) -> Result { #[cfg(test)] mod tests { - use super::*; + use crate::outbound::testing; + use edgezero_core::response_egress_framing::prepare_response_egress; + use std::cell::{Cell, RefCell}; + use std::collections::VecDeque; + use std::rc::Rc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::Poll; + use std::time::Duration; + use bytes::Bytes; - use edgezero_core::body::Body; - use edgezero_core::http::response_builder; - use futures_util::stream; + use edgezero_core::app::App; + use edgezero_core::context::RequestContext; + use edgezero_core::http::{Version, request_builder, response_builder}; + use edgezero_core::ingress::{IngressDispatchOutcome, IngressFraming, IngressHeadAccounting}; + use edgezero_core::response_egress::{ + ResponseEgressBodyKind, ResponseEgressCompletion, ResponseEgressHead, + ResponseEgressObserver, ResponseEgressObserverHandle, ResponseEgressReport, + }; + use edgezero_core::router::RouterService; + use edgezero_core::time::MonotonicInstant; + use futures_util::future::{pending, poll_fn as poll_future}; + use futures_util::stream::{self, poll_fn}; + + use super::*; + + #[derive(Clone, Default)] + struct RecordingObserver(Arc>>); + + impl RecordingObserver { + fn reports(&self) -> Vec { + self.0.lock().expect("reports lock").clone() + } + } + + impl ResponseEgressObserver for RecordingObserver { + fn complete(&self, report: &ResponseEgressReport) { + self.0.lock().expect("reports lock").push(report.clone()); + } + } + + #[derive(Default)] + struct SinkState { + abandon: MockTerminal, + accepted: Vec, + advance_on_finish: Option<(Arc>, MonotonicInstant)>, + advance_on_write: Option<(Arc>, MonotonicInstant)>, + commits: Vec<(StatusCode, HeaderMap, bool)>, + events: Vec<&'static str>, + finish: MockTerminal, + staged: Vec>, + write_plan: VecDeque>, + } + + struct MockSink(Rc>); + + #[derive(Clone, Copy, Default)] + enum MockTerminal { + Error, + #[default] + Success, + } + + impl EgressSink for MockSink { + type Error = (); + + fn abandon(&mut self) -> Result<(), Self::Error> { + let mut state = self.0.borrow_mut(); + state.events.push("abandon"); + match state.abandon { + MockTerminal::Error => Err(()), + MockTerminal::Success => Ok(()), + } + } + + fn finish(&mut self) -> Result<(), Self::Error> { + let mut state = self.0.borrow_mut(); + state.events.push("finish"); + if let Some((clock, observed)) = &state.advance_on_finish { + *clock.lock().map_err(|_poisoned| ())? = *observed; + } + match state.finish { + MockTerminal::Error => Err(()), + MockTerminal::Success => Ok(()), + } + } + + fn write(&mut self, bytes: &[u8]) -> Result { + let mut state = self.0.borrow_mut(); + state.events.push("write"); + let result = state.write_plan.pop_front().unwrap_or(Ok(bytes.len())); + if let Ok(accepted) = result + && accepted <= bytes.len() + { + state.accepted.extend_from_slice(&bytes[..accepted]); + } + if let Some((clock, observed)) = &state.advance_on_write { + *clock.lock().map_err(|_poisoned| ())? = *observed; + } + result + } + } + + struct MockCommitter { + commit_plan: VecDeque, + prepare_plan: VecDeque>, + state: Rc>, + } + + enum MockCommit { + Accept, + Error, + } + + struct MockPrepared { + buffered_body: Option>, + head: (StatusCode, HeaderMap, bool), + } + + impl MockCommitter { + fn new(state: Rc>) -> Self { + Self { + commit_plan: VecDeque::new(), + prepare_plan: VecDeque::new(), + state, + } + } + } + + impl EgressCommitter for MockCommitter { + type Error = (); + type Prepared = MockPrepared; + type Sink = MockSink; + + fn commit(&mut self, prepared: Self::Prepared) -> Result { + let transmits_body = prepared.head.2; + self.state.borrow_mut().commits.push(prepared.head); + match self.commit_plan.pop_front().unwrap_or(MockCommit::Accept) { + MockCommit::Accept => { + if transmits_body && let Some(bytes) = prepared.buffered_body { + self.state.borrow_mut().accepted.extend(bytes); + } + Ok(MockSink(Rc::clone(&self.state))) + } + MockCommit::Error => Err(()), + } + } + + fn prepare( + &mut self, + status: StatusCode, + headers: &HeaderMap, + transmits_body: bool, + buffered_body: Option>, + ) -> Result { + self.prepare_plan + .pop_front() + .unwrap_or(Ok(())) + .map_err(|_error| DeliveryError::Conversion)?; + let staged_body = if let Some(buffered) = buffered_body { + let mut staged = Vec::new(); + buffered.stage(|remaining| { + let accepted = self + .state + .borrow_mut() + .write_plan + .pop_front() + .unwrap_or(Ok(remaining.len()))?; + if accepted <= remaining.len() { + staged.extend_from_slice(&remaining[..accepted]); + } + Ok::(accepted) + })?; + self.state.borrow_mut().staged.push(staged.clone()); + Some(staged) + } else { + None + }; + Ok(MockPrepared { + buffered_body: staged_body, + head: (status, headers.clone(), transmits_body), + }) + } + } + + fn response_egress(outcome: IngressDispatchOutcome) -> ResponseEgressEnvelope { + let IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("expected response egress"); + }; + *envelope + } + + fn stable_clock(now: MonotonicInstant) -> MonotonicClock { + MonotonicClock::new(move || now) + } + + fn attempt(observer: &RecordingObserver, now: MonotonicInstant) -> ResponseEgressAttempt { + let headers = HeaderMap::new(); + let head = + ResponseEgressHead::new(StatusCode::OK, Version::HTTP_11, &headers, None, now, None); + ResponseEgressAttempt::new( + &head, + now, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + ) + } + + fn future_policy(now: MonotonicInstant) -> ResponseEgressPolicy { + ResponseEgressPolicy { + write_deadline: Deadline::at_instant( + now.checked_add(Duration::from_secs(10)).expect("deadline"), + ), + } + } + + fn prepared(body: Body) -> PreparedResponseEgress { + prepare_response_egress( + ResponseEgressFramingMode::HostAutomatic, + &Method::GET, + response_builder() + .status(StatusCode::OK) + .body(body) + .expect("response"), + ) + .expect("prepared response") + } + + fn transmit( + body: Body, + state: Rc>, + ) -> (Result<(), TransmitFailure>, Vec) { + let now = MonotonicInstant::now(); + let clock = stable_clock(now); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, now); + let mut committer = MockCommitter::new(state); + let prepared = attempt + .prepare_response( + ResponseEgressFramingMode::HostAutomatic, + &Method::GET, + Response::new(body), + ) + .expect("prepared scoped response"); + let result = transmit_prepared( + prepared, + future_policy(now), + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); + drop(attempt); + (result, observer.reports()) + } + + #[test] + fn body_created_batches_end_at_egress_without_closing_callback_work() { + use crate::outbound::scope::bind_batch; + use edgezero_core::{OutboundBatch, OutboundBatchDriverEvent, OutboundScopeOwner}; + use futures_util::future::poll_fn as poll_future; + use std::future::Future as _; + + struct Release(Rc>); + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for Release { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + fn batch(released: Rc>) -> OutboundBatch { + let owner = Release(released); + let source = poll_fn(move |_cx| { + let _keep_owner = &owner; + Poll::>::Pending + }); + let context = testing::batch_context(1); + let receiver = bind_batch(context.scope(), source); + OutboundBatch::from_driver(context, receiver, testing::owner()) + } + + for failure in 0_u8..4 { + let callback = OutboundScopeOwner::callback(); + let callback_released = Rc::new(Cell::new(0)); + let callback_batch = Rc::new(RefCell::new( + callback.with(|| batch(Rc::clone(&callback_released))), + )); + let polled_callback = Rc::clone(&callback_batch); + let response_released = Rc::new(Cell::new(0)); + let captured_release = Rc::clone(&response_released); + let saved = Rc::new(RefCell::new(None)); + let captured_batch = Rc::clone(&saved); + let body = Body::from_stream(async_stream::stream! { + poll_future(|cx| { + let mut borrowed = polled_callback.borrow_mut(); + let mut next = Box::pin(borrowed.next()); + assert!(next.as_mut().poll(cx).is_pending()); + Poll::Ready(()) + }).await; + *captured_batch.borrow_mut() = Some(batch(captured_release)); + if failure == 1 { + yield Err(EdgeError::bad_gateway("fixture source failed")); + } else { + yield Ok(Bytes::from_static(b"response")); + } + }); + let state = Rc::new(RefCell::new(SinkState::default())); + if failure == 2 { + state.borrow_mut().write_plan.push_back(Err(())); + } + if failure == 3 { + state.borrow_mut().finish = MockTerminal::Error; + } + let (result, reports) = callback.with(|| transmit(body, state)); + assert_eq!(result.is_err(), failure != 0); + assert_eq!(reports.len(), 1); + assert_eq!(response_released.get(), 1, "egress owns body-created work"); + assert_eq!( + callback_released.get(), + 0, + "callback remains active after egress" + ); + let closed_batch = saved.borrow_mut().take().expect("body created a batch"); + assert!(matches!( + executor::block_on(closed_batch.collect()) + .unwrap_err() + .error(), + EdgeError::Internal { .. } + )); + let later_released = Rc::new(Cell::new(0)); + let later_batch = callback.with(|| batch(Rc::clone(&later_released))); + assert_eq!(later_released.get(), 0); + drop(callback); + assert_eq!(callback_released.get(), 1); + assert_eq!(later_released.get(), 1); + drop(callback_batch); + drop(later_batch); + assert_eq!(response_released.get(), 1); + } + } + + #[test] + fn buffered_handoff_does_not_reenter_the_stream_writer() { + for bytes in ["abcdef", ""] { + let state = Rc::new(RefCell::new(SinkState::default())); + let (result, reports) = transmit(Body::from(bytes), Rc::clone(&state)); + assert_eq!(result, Ok(())); + assert_eq!(state.borrow().accepted, bytes.as_bytes()); + assert!( + state.borrow().events.is_empty(), + "buffered handoff must not write or finish a consumed streaming handle" + ); + assert_eq!(reports.len(), 1); + assert_eq!( + reports[0].bytes_written, + u64::try_from(bytes.len()).expect("length") + ); + } + } + + #[test] + fn manual_framing_is_only_for_transmitted_unhinted_declared_streams() { + let mut headers = HeaderMap::new(); + assert!(!manual_stream_framing(&headers, true, true)); + headers.insert("content-length", "6".parse().expect("length")); + assert!(manual_stream_framing(&headers, true, true)); + assert!(!manual_stream_framing(&headers, false, true)); + assert!(!manual_stream_framing(&headers, true, false)); + for hint in ["on", "gzip", "br", "off", "ON", " on ", "", "unrecognized"] { + headers.insert("x-compress-hint", hint.parse().expect("hint")); + assert!(!manual_stream_framing(&headers, true, true)); + } + headers.append("x-compress-hint", "on".parse().expect("repeated hint")); + assert!(!manual_stream_framing(&headers, true, true)); + } + + #[test] + fn buffered_staging_stops_at_deadline_before_another_write() { + use std::sync::atomic::AtomicBool; + + let start = MonotonicInstant::now(); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let expired = Arc::new(AtomicBool::new(false)); + let clock_expired = Arc::clone(&expired); + let clock = MonotonicClock::new(move || { + if clock_expired.load(Ordering::SeqCst) { + deadline + } else { + start + } + }); + let staged = BufferedBody { + bytes: b"abcdef", + clock: &clock, + deadline: Deadline::at_instant(deadline), + }; + let mut writes = 0_usize; + assert_eq!( + staged.stage(|_| { + writes += 1; + expired.store(true, Ordering::SeqCst); + Ok::(2) + }), + Err(DeliveryError::Deadline) + ); + assert_eq!(writes, 1); + } + + #[test] + fn observed_staging_deadline_survives_a_later_backwards_clock_sample() { + struct DeadlinePreparer; + impl EgressCommitter for DeadlinePreparer { + type Error = DeliveryError; + type Prepared = (); + type Sink = MockSink; + + fn commit(&mut self, _prepared: ()) -> Result { + Err(DeliveryError::Transport) + } + + fn prepare( + &mut self, + _status: StatusCode, + _headers: &HeaderMap, + _transmits_body: bool, + _buffered_body: Option>, + ) -> Result<(), DeliveryError> { + Err(DeliveryError::Deadline) + } + } + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, now); + assert_eq!( + transmit_prepared( + prepared(Body::from("abcdef")), + future_policy(now), + &mut attempt, + &stable_clock(now), + TransmitKind::Application, + &mut DeadlinePreparer + ), + Err(TransmitFailure::Precommit { + cause: ResponseEgressOutcome::DeadlineExceeded, + error: DeliveryError::Deadline + }) + ); + assert!(observer.reports().is_empty()); + } + + #[test] + fn bulk_commit_expiry_preserves_the_complete_handed_off_buffer() { + let start = MonotonicInstant::now(); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let reads = Arc::new(AtomicUsize::new(0)); + let clock_reads = Arc::clone(&reads); + let clock = MonotonicClock::new(move || { + if clock_reads.fetch_add(1, Ordering::SeqCst) < 4 { + start + } else { + deadline + } + }); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, start); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + let result = transmit_prepared( + prepared(Body::from("abcdef")), + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(deadline), + }, + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); + assert_eq!( + result, + Err(TransmitFailure::Postcommit(DeliveryError::Deadline)) + ); + assert_eq!(state.borrow().accepted, b"abcdef"); + assert_eq!(state.borrow().commits.len(), 1); + assert!(state.borrow().events.is_empty()); + assert_eq!(observer.reports().len(), 1); + assert_eq!(observer.reports()[0].bytes_written, 6); + } + + #[test] + fn buffered_head_never_restores_suppressed_payload() { + for status in [ + StatusCode::OK, + StatusCode::NO_CONTENT, + StatusCode::RESET_CONTENT, + StatusCode::NOT_MODIFIED, + ] { + let response = response_builder() + .status(status) + .body(Body::from("abcdef")) + .expect("response"); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::HostAutomatic, + &Method::HEAD, + response, + ) + .expect("prepared"); + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, now); + let state = Rc::new(RefCell::new(SinkState::default())); + assert_eq!( + transmit_prepared( + prepared, + future_policy(now), + &mut attempt, + &stable_clock(now), + TransmitKind::Application, + &mut MockCommitter::new(Rc::clone(&state)) + ), + Ok(()) + ); + assert!(state.borrow().staged.is_empty()); + assert!(state.borrow().accepted.is_empty()); + assert_eq!(observer.reports()[0].bytes_written, 0); + } + } + + async fn invalid_length_response(_context: RequestContext) -> Result { + response_builder() + .status(StatusCode::OK) + .header("content-length", "2") + .body(Body::from("x")) + .map_err(EdgeError::internal) + } + + async fn ordinary_response(_context: RequestContext) -> Result { + response_builder() + .status(StatusCode::OK) + .body(Body::from("application")) + .map_err(EdgeError::internal) + } #[test] fn parse_valid_uri() { @@ -61,36 +1306,1512 @@ mod tests { } #[test] - fn multi_value_set_cookie_survives_conversion() { - // http::response::Builder::header APPENDS, so this is two Set-Cookie values. + #[cfg(all(feature = "fastly", target_arch = "wasm32"))] + fn raw_abi_commits_repeated_set_cookie_and_streams_body() { + let mut headers = HeaderMap::new(); + headers.append("set-cookie", "a=1".parse().expect("header")); + headers.append("set-cookie", "b=2".parse().expect("header")); + let mut committer = platform::FastlyCommitter; + let prepared = committer + .prepare(StatusCode::OK, &headers, true, None) + .expect("prepare raw response"); + let mut sink = committer.commit(prepared).expect("commit raw response"); + assert_eq!(sink.write(b"raw-egress"), Ok(10)); + assert_eq!(sink.finish(), Ok(())); + } + + #[test] + fn streams_empty_once_and_multiple_chunks_with_short_writes() { + for (body, plan, expected) in [ + ( + Body::stream(stream::empty()), + VecDeque::new(), + b"".as_slice(), + ), + ( + Body::stream(stream::iter([Bytes::from_static(b"hello")])), + VecDeque::from([Ok(2), Ok(3)]), + b"hello".as_slice(), + ), + ( + Body::stream(stream::iter([ + Bytes::from_static(b"hello "), + Bytes::from_static(b"world"), + ])), + VecDeque::new(), + b"hello world".as_slice(), + ), + ] { + let state = Rc::new(RefCell::new(SinkState { + write_plan: plan, + ..SinkState::default() + })); + let (result, reports) = transmit(body, Rc::clone(&state)); + assert_eq!(result, Ok(())); + assert_eq!(state.borrow().accepted, expected); + assert_eq!(state.borrow().events.last(), Some(&"finish")); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::Completed); + assert_eq!( + reports[0].bytes_written, + u64::try_from(expected.len()).expect("body length") + ); + } + } + + #[test] + fn fastly_delivery_report_tracks_the_last_fallback_commitment() { + use crate::delivery::{DispatchOutcome, FastlyRequestDelivery, finish_request}; + use edgezero_core::response_egress_framing::{ + ResponseEgressFramingError, test_support::framing_fixture, + }; + for (prepare, commit, expected) in [ + ( + Err(()), + MockCommit::Accept, + FastlyResponseCommitState::NotCommitted, + ), + ( + Ok(()), + MockCommit::Error, + FastlyResponseCommitState::Unknown, + ), + ( + Ok(()), + MockCommit::Accept, + FastlyResponseCommitState::Committed, + ), + ] { + let fixture = executor::block_on(framing_fixture( + ResponseEgressFramingError::UnsupportedTrailers, + false, + )) + .unwrap(); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter { + prepare_plan: VecDeque::from([prepare]), + commit_plan: VecDeque::from([commit]), + state, + }; + let result = finish_request( + Ok(DispatchOutcome::Routed(fixture.envelope)), + |_| 42_u8, + |envelope| send_egress_with(envelope, &mut committer), + ); + assert_eq!( + result.may_send_native_response(), + expected == FastlyResponseCommitState::NotCommitted + ); + let FastlyRequestDelivery::Routed { + state: hook_state, + delivery, + } = result + else { + panic!("routed result"); + }; + assert_eq!(hook_state, 42); + assert_eq!(delivery.commitment(), expected); + let completed = fixture.completed.lock().unwrap(); + let observed = fixture.observed.lock().unwrap(); + assert_eq!(completed.len(), 1); + assert_eq!(completed.first(), Some(delivery.report())); + assert_eq!(*completed, *observed); + assert_eq!( + delivery.report().body_kind, + ResponseEgressBodyKind::Fallback + ); + assert_eq!( + delivery.report().fallback_disposition, + Some(if expected == FastlyResponseCommitState::Committed { + ResponseEgressFallbackDisposition::Completed + } else { + ResponseEgressFallbackDisposition::Aborted + }) + ); + } + } + + #[test] + fn framing_observer_preserves_each_typed_reason() { + use crate::delivery::{DispatchOutcome, FastlyRequestDelivery, finish_request}; + use edgezero_core::response_egress_framing::test_support::{ + FRAMING_RULES, framing_fixture, has_full_synthesized_fixture_headers, + }; + for reason in FRAMING_RULES { + for detached in [false, true] { + let fixture = + executor::block_on(framing_fixture(reason, detached)).expect("fixture"); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + let dispatched = if detached { + DispatchOutcome::Detached(fixture.envelope) + } else { + DispatchOutcome::Routed(fixture.envelope) + }; + let result = finish_request( + Ok(dispatched), + |_| { + assert!(!detached); + 42_u8 + }, + |envelope| send_egress_with(envelope, &mut committer), + ); + let delivery = match &result { + FastlyRequestDelivery::Routed { + state: hook_state, + delivery, + } => { + assert_eq!(*hook_state, 42); + delivery + } + FastlyRequestDelivery::Detached(delivery) => delivery, + FastlyRequestDelivery::Aborted | FastlyRequestDelivery::NoAttempt(_) => { + panic!("lost attempted delivery") + } + }; + assert_eq!(delivery.error(), None); + assert_eq!( + state.borrow().commits[0].1["x-synthesized-fixture"], + "configured" + ); + assert!(has_full_synthesized_fixture_headers( + &state.borrow().commits[0].1 + )); + assert_eq!(delivery.commitment(), FastlyResponseCommitState::Committed); + let snapshot = state.borrow(); + assert_eq!(snapshot.commits.len(), 1); + assert_eq!(snapshot.commits[0].0, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(snapshot.accepted, INTERNAL_FALLBACK_BODY); + let completed = fixture.completed.lock().expect("completion"); + assert_eq!(completed.len(), 1); + assert_eq!(delivery.report(), &completed[0]); + assert_eq!(completed[0].framing_failures.application(), Some(reason)); + assert_eq!(completed[0].framing_failures.fallback(), None); + assert_eq!(completed[0].outcome, ResponseEgressOutcome::ConversionError); + let observed = fixture.observed.lock().expect("observer"); + if detached { + assert!(observed.is_empty()); + } else { + assert_eq!(*observed, *completed); + } + } + } + } + + #[test] + fn provider_diagnostics_reach_response_observer_before_deadline_arbitration() { + use edgezero_core::error::BadGatewayReason; + use edgezero_core::error_detail::{ + FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase, + }; + use std::sync::atomic::{AtomicBool, Ordering}; + + let detail = ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: FastlyFailureCause::ConnectionTerminated, + }; + for with_detail in [false, true] { + for late in [false, true] { + let now = MonotonicInstant::now(); + let policy = future_policy(now); + let deadline = policy.write_deadline.instant(); + let expired = Arc::new(AtomicBool::new(false)); + let clock_expired = Arc::clone(&expired); + let clock = MonotonicClock::new(move || { + if clock_expired.load(Ordering::SeqCst) { + deadline + } else { + now + } + }); + let error = if with_detail { + EdgeError::bad_gateway_with_detail( + "provider-secret-token", + BadGatewayReason::Transport, + detail, + ) + } else { + EdgeError::bad_gateway_with_reason( + "provider-secret-token", + BadGatewayReason::Transport, + ) + }; + let body = Body::from_stream(stream::once(async move { + expired.store(late, Ordering::SeqCst); + Err(error) + })); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, now); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + let result = transmit_prepared( + prepared(body), + policy, + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); + assert!(result.is_err()); + drop(attempt); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!( + reports[0].outcome, + if late { + ResponseEgressOutcome::DeadlineExceeded + } else { + ResponseEgressOutcome::SourceError + } + ); + let failure = reports[0] + .source_failure + .as_ref() + .expect("source observation"); + assert_eq!(reports[0].framing_failures.application(), None); + assert_eq!(reports[0].framing_failures.fallback(), None); + assert_eq!(failure.category(), "bad_gateway_transport"); + assert_eq!( + failure.provider_failure_detail(), + with_detail.then_some(&detail) + ); + assert!(!format!("{failure:?}").contains("secret")); + assert_eq!( + state + .borrow() + .events + .iter() + .filter(|event| **event == "abandon") + .count(), + 1 + ); + } + } + } + + #[test] + fn zero_write_write_error_source_error_and_finish_error_abort_once() { + let cases = [ + ( + Body::stream(stream::iter([Bytes::from_static(b"x")])), + VecDeque::from([Ok(0)]), + false, + ResponseEgressOutcome::TransportError, + ), + ( + Body::stream(stream::iter([Bytes::from_static(b"x")])), + VecDeque::from([Err(())]), + false, + ResponseEgressOutcome::TransportError, + ), + ( + Body::from_stream(stream::iter([Err(EdgeError::internal(anyhow::anyhow!( + "private source detail" + )))])), + VecDeque::new(), + false, + ResponseEgressOutcome::SourceError, + ), + ( + Body::stream(stream::empty()), + VecDeque::new(), + true, + ResponseEgressOutcome::TransportError, + ), + ]; + for (body, write_plan, finish_error, expected) in cases { + let state = Rc::new(RefCell::new(SinkState { + abandon: MockTerminal::Error, + finish: if finish_error { + MockTerminal::Error + } else { + MockTerminal::Success + }, + write_plan, + ..SinkState::default() + })); + let (result, reports) = transmit(body, Rc::clone(&state)); + assert!(matches!(result, Err(TransmitFailure::Postcommit(_)))); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, expected); + assert_eq!( + reports[0].source_failure.is_some(), + expected == ResponseEgressOutcome::SourceError + ); + let expected_terminal = if finish_error { "finish" } else { "abandon" }; + assert_eq!(state.borrow().events.last(), Some(&expected_terminal)); + } + } + + #[cfg(feature = "fastly")] + use edgezero_core::OutboundBatchDriverEvent; + #[cfg(feature = "fastly")] + use futures_util::stream::LocalBoxStream; + + #[cfg(feature = "fastly")] + struct TimedWorkOwner(Rc>); + + #[cfg(feature = "fastly")] + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for TimedWorkOwner { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + #[cfg(feature = "fastly")] + fn timed_scope_work( + released: Rc>, + time: OutboundTime, + observation_cutoff: Deadline, + ) -> LocalBoxStream<'static, OutboundBatchDriverEvent> { + let owner = TimedWorkOwner(released); + async_stream::stream! { + let _owner = owner; + yield match time.wait_until(observation_cutoff).await { + Ok(()) => OutboundBatchDriverEvent::Cutoff, + Err(error) => OutboundBatchDriverEvent::Failed(error), + }; + } + .boxed_local() + } + + #[cfg(feature = "fastly")] + fn assert_unavailable_sdk_origin(outcome: &Result) { + use edgezero_core::{FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase}; + let Err(error) = outcome else { + panic!("the controlled unavailable SDK origin cannot succeed"); + }; + assert!( + matches!( + error.provider_failure_detail(), + Some(ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::Send, + cause: FastlyFailureCause::ConnectionRefused + | FastlyFailureCause::InternalError { + status: Some(1_i32) + }, + }) + ), + "unavailable origin must retain its actual SDK cause: {error:?}" + ); + } + + #[cfg(feature = "fastly")] + async fn callback_origin_until( + client: &edgezero_core::HttpClient, + time: &OutboundTime, + origin_bound: Deadline, + ) -> Result<(), EdgeError> { + use edgezero_core::OutboundRequest; + let origin = client + .send( + OutboundRequest::get("http://127.0.0.1:9/callback-origin") + .unwrap() + .timeout(Duration::from_secs(1)), + ) + .await; + assert_unavailable_sdk_origin(&origin); + time.wait_until(origin_bound).await + } + + #[cfg(feature = "fastly")] + fn early_batch_timeline_body( + mut batch: edgezero_core::OutboundBatch, + mut delayed: edgezero_core::OutboundBatch, + time: OutboundTime, + entered: MonotonicInstant, + handoff_elapsed: Duration, + ready_elapsed: Duration, + checks: Rc>>, + ) -> Body { + use edgezero_core::{OutboundBatchCheck, OutboundBatchTermination}; + Body::from_stream(async_stream::stream! { + for millis in [260_u64, 320, 400] { + time.wait_until(Deadline::at_instant(entered.checked_add(Duration::from_millis(millis)).unwrap())).await?; + let observed = batch.check()?; + if millis == 260 { + let OutboundBatchCheck::Item(item) = observed else { panic!("body-first SDK pipeline must already be recorded at first check"); }; + assert_eq!(item.index(), 0); + assert!(item.result().elapsed < handoff_elapsed, "callback origin work must progress the idle body-owned pipeline before handoff"); + assert_unavailable_sdk_origin(&item.result().outcome); + } else { + assert!(matches!(observed, OutboundBatchCheck::Finished(OutboundBatchTermination::Completed))); + } + let delayed_result = delayed.check()?; + match millis { + 260 => assert!(matches!(delayed_result, OutboundBatchCheck::Pending)), + 320 => { + let OutboundBatchCheck::Item(item) = delayed_result else { panic!("body-owned native timer must keep progressing after callback exit"); }; + assert!(item.result().elapsed >= ready_elapsed); + assert!(matches!(item.result().outcome, Err(EdgeError::BadGateway { .. }))); + } + _ => assert!(matches!(delayed_result, OutboundBatchCheck::Finished(OutboundBatchTermination::Completed))), + } + checks.borrow_mut().push(millis); + yield Ok(Bytes::from_static(b"tick")); + } + }) + } + + #[cfg(feature = "fastly")] + fn delayed_body_scope_batch( + time: OutboundTime, + ready_at: Deadline, + observation_cutoff: Deadline, + ) -> (edgezero_core::OutboundBatch, Duration) { + use crate::outbound::scope::bind_batch; + let context = testing::batch_context_using(1, time.clone(), observation_cutoff); + let ready_elapsed = ready_at + .instant() + .saturating_duration_since(context.started_at()); + let source = async_stream::stream! { + if let Err(error) = time.wait_until(ready_at).await { + yield OutboundBatchDriverEvent::Failed(error); + return; + } + yield OutboundBatchDriverEvent::Item { + index: 0, completed_at: time.clock().now(), + outcome: Err(EdgeError::bad_gateway("delayed pipeline fixture")), + }; + }; + let receiver = bind_batch(context.scope(), source); + ( + edgezero_core::OutboundBatch::from_driver(context, receiver, testing::owner()), + ready_elapsed, + ) + } + + #[test] + #[cfg(feature = "fastly")] + fn precreated_body_batch_progresses_with_callback_origin_before_attachment() { + use crate::outbound::FastlyOutboundClient; + use crate::outbound::scope; + use edgezero_core::{HttpClient, OutboundRequest, OutboundScopeOwner}; + + let callback = OutboundScopeOwner::callback(); + let body_owner = callback.with(OutboundScopeOwner::response_body); + let body_scope = body_owner.with(scope::capture); + let clock = MonotonicClock::default(); + let entered = clock.now(); + let time = native_time(clock.clone()); + let client = HttpClient::with_client(FastlyOutboundClient::with_time(time.clone())); + let batch = body_owner.with(|| { + client.start_batch_until( + vec![ + OutboundRequest::post("http://127.0.0.1:9/body-first") + .unwrap() + .body("body-owned") + .timeout(Duration::from_secs(1)), + ], + Deadline::at_instant(entered.checked_add(Duration::from_millis(500)).unwrap()), + ) + }); + let (delayed, ready_elapsed) = body_owner.with(|| { + delayed_body_scope_batch( + time.clone(), + Deadline::at_instant(entered.checked_add(Duration::from_millis(300)).unwrap()), + Deadline::at_instant(entered.checked_add(Duration::from_millis(500)).unwrap()), + ) + }); + executor::block_on(body_scope.run(time.clone(), entered, Box::pin(async { Ok(()) }))) + .unwrap(); + let origin_bound = + Deadline::at_instant(entered.checked_add(Duration::from_millis(120)).unwrap()); + callback + .with(|| { + executor::block_on(drive_callback( + callback_origin_until(&client, &time, origin_bound), + clock.clone(), + entered, + )) + }) + .unwrap() + .unwrap(); + assert!(origin_bound.is_expired_at(clock.now())); + let handoff_elapsed = clock.now().saturating_duration_since(entered); + let checks = Rc::new(RefCell::new(Vec::new())); + let response = body_owner + .attach(Response::new(early_batch_timeline_body( + batch, + delayed, + time.clone(), + entered, + handoff_elapsed, + ready_elapsed, + Rc::clone(&checks), + ))) + .unwrap(); + assert_eq!(checks.borrow().len(), 0, "attachment cannot poll the body"); + assert!(response.body().as_bytes().is_none()); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, entered); + let prepared = attempt + .prepare_response( + ResponseEgressFramingMode::HostAutomatic, + &Method::GET, + response, + ) + .unwrap(); + body_scope + .check() + .expect("framing adopts the exact precreated body owner"); + callback.close(); + body_scope + .check() + .expect("handler return cannot close precreated body work"); + assert!(attempt.begin_writing()); + let delivered = body_scope.run( + time, + entered, + Box::pin(async move { + prepared + .into_response() + .into_body() + .into_bytes_bounded(64) + .await + }), + ); + assert_eq!( + executor::block_on(delivered).unwrap(), + Bytes::from_static(b"tickticktick") + ); + assert_eq!(*checks.borrow(), [260, 320, 400]); + body_scope + .check() + .expect("source EOF is not transmission completion"); + assert!(attempt.complete(clock.now())); + assert!(matches!( + body_scope.check(), + Err(EdgeError::Internal { .. }) + )); + } + + #[cfg(feature = "fastly")] + fn timed_scope_batch( + released: Rc>, + time: OutboundTime, + observation_cutoff: Deadline, + ) -> edgezero_core::OutboundBatch { + use crate::outbound::scope::bind_batch; + let context = testing::batch_context_using(1, time.clone(), observation_cutoff); + let receiver = bind_batch( + context.scope(), + timed_scope_work(released, time, observation_cutoff), + ); + edgezero_core::OutboundBatch::from_driver(context, receiver, testing::owner()) + } + + #[cfg(feature = "fastly")] + fn pending_scoped_body( + time: OutboundTime, + observation_cutoff: Deadline, + task: Rc>>, + task_released: Rc>, + source_released: Rc>, + ) -> Body { + use edgezero_core::BudgetSource; + let owner = TimedWorkOwner(source_released); + let clock = time.clock().clone(); + Body::from_stream(poll_fn(move |_cx| { + let _owner = &owner; + if task.borrow().is_none() { + task.borrow_mut().replace(timed_scope_batch( + Rc::clone(&task_released), + time.clone(), + observation_cutoff, + )); + } + // Bound the old failure path without changing the egress or outbound deadline. + if observation_cutoff.is_expired_at(clock.now()) { + Poll::Ready(Some(Err(EdgeError::gateway_timeout_caused( + "outbound fixture safety fence", + BudgetSource::BatchCutoff, + )))) + } else { + Poll::Pending + } + })) + } + + #[test] + fn write_timer_failure_precedes_source_and_releases_once() { + #[derive(Debug, thiserror::Error)] + #[error("write timer fixture failed")] + struct TimerFailure; + + struct TimerOwner(Arc); + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for TimerOwner { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + for delayed in [false, true] { + let entered = MonotonicInstant::now(); + let deadline = + Deadline::at_instant(entered.checked_add(Duration::from_secs(1)).unwrap()); + let released = Arc::new(AtomicUsize::new(0)); + let timer_released = Arc::clone(&released); + let armed = Arc::new(AtomicUsize::new(0)); + let timer_armed = Arc::clone(&armed); + let time = OutboundTime::new(stable_clock(entered), move |_, _| { + timer_armed.fetch_add(1, Ordering::SeqCst); + let owner = TimerOwner(Arc::clone(&timer_released)); + let mut first = true; + Box::pin(poll_future(move |cx| { + let _owner = &owner; + if delayed && first { + first = false; + cx.waker().wake_by_ref(); + Poll::Pending + } else { + Poll::Ready(Err(EdgeError::internal(TimerFailure))) + } + })) + }); + let reads = Rc::new(Cell::new(0_usize)); + let source_reads = Rc::clone(&reads); + let mut source = poll_fn(move |_cx| { + assert_eq!( + armed.load(Ordering::SeqCst), + 1, + "timer setup precedes payload reads" + ); + source_reads.set(source_reads.get().saturating_add(1)); + Poll::Pending + }) + .boxed_local(); + let mut timer = None; + let error = + executor::block_on(read_stream_until(&mut source, &time, deadline, &mut timer)) + .expect_err("timer failure is terminal"); + let EdgeError::Internal { + source: failure, .. + } = error + else { + panic!("typed timer failure"); + }; + assert!(failure.is::()); + assert_eq!(reads.get(), usize::from(delayed)); + assert!(timer.is_none()); + assert_eq!(released.load(Ordering::SeqCst), 1); + drop(timer); + assert_eq!(released.load(Ordering::SeqCst), 1); + } + } + + #[test] + fn ready_body_chunks_keep_one_absolute_write_timer() { + let entered = MonotonicInstant::now(); + let deadline = Deadline::at_instant(entered.checked_add(Duration::from_secs(1)).unwrap()); + let registered = Arc::new(Mutex::new(Vec::new())); + let timer_registered = Arc::clone(®istered); + let time = OutboundTime::new(stable_clock(entered), move |bound, _| { + timer_registered.lock().unwrap().push(bound.instant()); + Box::pin(pending()) + }); + let mut source = stream::iter([ + Ok(Bytes::from_static(b"first")), + Ok(Bytes::from_static(b"second")), + ]) + .boxed_local(); + let mut timer = None; + for expected in [ + Some(Bytes::from_static(b"first")), + Some(Bytes::from_static(b"second")), + None, + ] { + let next = + executor::block_on(read_stream_until(&mut source, &time, deadline, &mut timer)) + .unwrap(); + let StreamRead::Source(outcome) = next else { + panic!("write deadline is still live"); + }; + assert_eq!(outcome.transpose().unwrap(), expected); + } + assert_eq!(*registered.lock().unwrap(), [deadline.instant()]); + } + + #[test] + #[cfg(feature = "fastly")] + fn pending_body_write_deadline_releases_only_response_work() { + use edgezero_core::{OutboundBatchNext, OutboundBatchTermination, OutboundScopeOwner}; + + let entered = MonotonicInstant::now(); + let clock = MonotonicClock::default(); + let write_deadline = + Deadline::at_instant(entered.checked_add(Duration::from_millis(50)).unwrap()); + let outbound_cutoff = + Deadline::at_instant(entered.checked_add(Duration::from_millis(500)).unwrap()); + let callback = OutboundScopeOwner::callback(); + callback + .with(|| executor::block_on(drive_callback(async {}, clock.clone(), entered))) + .unwrap(); + let time = native_time(clock.clone()); + let callback_released = Rc::new(Cell::new(0)); + let mut sibling = callback.with(|| { + timed_scope_batch(Rc::clone(&callback_released), time.clone(), outbound_cutoff) + }); + let response_released = Rc::new(Cell::new(0)); + let body_task = Rc::new(RefCell::new(None)); + let source_released = Rc::new(Cell::new(0)); + let body = pending_scoped_body( + time, + outbound_cutoff, + Rc::clone(&body_task), + Rc::clone(&response_released), + Rc::clone(&source_released), + ); + let observer = RecordingObserver::default(); + let mut attempt = callback.with(|| attempt(&observer, entered)); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + let prepared = callback + .with(|| { + attempt.prepare_response( + ResponseEgressFramingMode::HostAutomatic, + &Method::GET, + Response::new(body), + ) + }) + .unwrap(); + let result = callback.with(|| { + transmit_prepared( + prepared, + ResponseEgressPolicy { write_deadline }, + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ) + }); + assert_eq!( + result, + Err(TransmitFailure::Postcommit(DeliveryError::Deadline)) + ); + assert!( + !outbound_cutoff.is_expired_at(clock.now()), + "egress must not wait for the outbound cutoff" + ); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert!( + reports[0].source_failure.is_none(), + "write expiry is not a source timeout" + ); + assert_eq!(source_released.get(), 1); + assert_eq!(response_released.get(), 1); + assert_eq!(callback_released.get(), 0); + assert_eq!(state.borrow().events, ["abandon"]); + let mut closed = body_task + .borrow_mut() + .take() + .expect("response-created work"); + assert!(matches!( + executor::block_on(closed.next()), + Err(EdgeError::Internal { .. }) + )); + let callback_result = callback + .with(|| executor::block_on(drive_callback(sibling.next(), clock.clone(), clock.now()))) + .unwrap(); + assert!(matches!( + callback_result, + Ok(OutboundBatchNext::Finished( + OutboundBatchTermination::Cutoff + )) + )); + assert!( + outbound_cutoff.is_expired_at(clock.now()), + "callback deadline is unchanged" + ); + assert_eq!(callback_released.get(), 1); + drop(callback); + assert_eq!(source_released.get(), 1); + assert_eq!(response_released.get(), 1); + } + + #[test] + fn accepted_prefix_at_deadline_is_accounted_before_deadline_wins() { + let start = MonotonicInstant::now(); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let now = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&now); + let clock = MonotonicClock::new(move || *sampled.lock().expect("clock")); + let report_observer = RecordingObserver::default(); + let mut attempt = attempt(&report_observer, start); + let state = Rc::new(RefCell::new(SinkState { + advance_on_write: Some((Arc::clone(&now), deadline)), + ..SinkState::default() + })); + let mut committer = MockCommitter::new(Rc::clone(&state)); + let result = transmit_prepared( + prepared(Body::stream(stream::iter([Bytes::from_static(b"abc")]))), + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(deadline), + }, + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); + assert_eq!( + result, + Err(TransmitFailure::Postcommit(DeliveryError::Deadline)) + ); + let reports = report_observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert_eq!(reports[0].bytes_written, 3); + } + + #[test] + fn deadline_at_commit_abandons_without_polling_or_writing() { + let start = MonotonicInstant::now(); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let reads = Arc::new(AtomicUsize::new(0)); + let clock_reads = Arc::clone(&reads); + let clock = MonotonicClock::new(move || { + if clock_reads.fetch_add(1, Ordering::SeqCst) < 2 { + start + } else { + deadline + } + }); + let polls = Rc::new(Cell::new(0_usize)); + let observed_polls = Rc::clone(&polls); + let body = Body::from_stream(poll_fn(move |_| { + observed_polls.set(observed_polls.get() + 1); + Poll::Pending + })); + let report_observer = RecordingObserver::default(); + let mut attempt = attempt(&report_observer, start); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + let result = transmit_prepared( + prepared(body), + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(deadline), + }, + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); + + assert_eq!( + result, + Err(TransmitFailure::Postcommit(DeliveryError::Deadline)) + ); + assert_eq!(polls.get(), 0); + assert!(state.borrow().accepted.is_empty()); + assert_eq!(state.borrow().events, ["abandon"]); + assert_eq!( + report_observer.reports()[0].outcome, + ResponseEgressOutcome::DeadlineExceeded + ); + } + + #[test] + fn deadline_after_head_preparation_prevents_irreversible_commit() { + let start = MonotonicInstant::now(); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let reads = Arc::new(AtomicUsize::new(0)); + let clock_reads = Arc::clone(&reads); + let clock = MonotonicClock::new(move || { + if clock_reads.fetch_add(1, Ordering::SeqCst) == 0 { + start + } else { + deadline + } + }); + let report_observer = RecordingObserver::default(); + let mut attempt = attempt(&report_observer, start); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + + let result = transmit_prepared( + prepared(Body::from("uncommitted")), + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(deadline), + }, + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); + + assert_eq!( + result, + Err(TransmitFailure::Precommit { + cause: ResponseEgressOutcome::DeadlineExceeded, + error: DeliveryError::Deadline, + }) + ); + assert!(state.borrow().commits.is_empty()); + assert!(report_observer.reports().is_empty()); + } + + #[test] + fn deadline_at_source_poll_abandons_before_writing_the_chunk() { + let start = MonotonicInstant::now(); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let now = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&now); + let clock = MonotonicClock::new(move || *sampled.lock().expect("clock")); + let polls = Rc::new(Cell::new(0_usize)); + let observed_polls = Rc::clone(&polls); + let source_now = Arc::clone(&now); + let body = Body::from_stream(poll_fn(move |_| { + *source_now.lock().expect("source clock") = deadline; + observed_polls.set(observed_polls.get() + 1); + Poll::Ready(Some(Ok(Bytes::from_static(b"late")))) + })); + let report_observer = RecordingObserver::default(); + let mut attempt = attempt(&report_observer, start); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + + let result = transmit_prepared( + prepared(body), + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(deadline), + }, + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); + + assert_eq!( + result, + Err(TransmitFailure::Postcommit(DeliveryError::Deadline)) + ); + assert_eq!(polls.get(), 1); + assert!(state.borrow().accepted.is_empty()); + assert_eq!(state.borrow().events, ["abandon"]); + let reports = report_observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert_eq!(reports[0].bytes_written, 0); + } + + #[test] + fn deadline_at_finish_wins_after_the_handle_is_consumed() { + let start = MonotonicInstant::now(); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let now = Arc::new(Mutex::new(start)); + let sampled = Arc::clone(&now); + let clock = MonotonicClock::new(move || *sampled.lock().expect("clock")); + let report_observer = RecordingObserver::default(); + let mut attempt = attempt(&report_observer, start); + let state = Rc::new(RefCell::new(SinkState { + advance_on_finish: Some((Arc::clone(&now), deadline)), + ..SinkState::default() + })); + let mut committer = MockCommitter::new(Rc::clone(&state)); + + let result = transmit_prepared( + prepared(Body::stream(stream::empty())), + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(deadline), + }, + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); + + assert_eq!( + result, + Err(TransmitFailure::Postcommit(DeliveryError::Deadline)) + ); + assert_eq!(state.borrow().events, ["finish"]); + let reports = report_observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + } + + #[test] + fn head_suppression_releases_source_without_polling() { + struct DropSignal(Rc>); + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.set(self.0.get() + 1); + } + } + + let polls = Rc::new(Cell::new(0_usize)); + let drops = Rc::new(Cell::new(0_usize)); + let observed_polls = Rc::clone(&polls); + let signal = DropSignal(Rc::clone(&drops)); + let body = Body::from_stream(poll_fn(move |_| { + let _keep_alive = &signal; + observed_polls.set(observed_polls.get() + 1); + Poll::Pending + })); let response = response_builder() - .status(200) - .header("set-cookie", "a=1") - .header("set-cookie", "b=2") - .body(Body::empty()) + .status(StatusCode::OK) + .header("content-length", "99") + .body(body) .expect("response"); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::HostAutomatic, + &Method::HEAD, + response, + ) + .expect("prepared"); + let start = MonotonicInstant::now(); + let clock = stable_clock(start); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, start); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + let result = transmit_prepared( + prepared, + future_policy(start), + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); + assert_eq!(result, Ok(())); + assert_eq!(polls.get(), 0); + assert_eq!(drops.get(), 1); + assert!(state.borrow().accepted.is_empty()); + assert_eq!(observer.reports()[0].bytes_written, 0); + } + + #[test] + fn body_suppressed_response_disables_manual_framing() { + for (method, status) in [ + (Method::HEAD, StatusCode::OK), + (Method::GET, StatusCode::NOT_MODIFIED), + ] { + let response = response_builder() + .status(status) + .header("content-length", "99") + .body(Body::from("suppressed")) + .expect("response"); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::HostAutomatic, + &method, + response, + ) + .expect("prepared"); + let now = MonotonicInstant::now(); + let clock = stable_clock(now); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, now); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); - let fastly_response = from_core_response(response).expect("fastly response"); + let result = transmit_prepared( + prepared, + future_policy(now), + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); - let cookies: Vec = fastly_response - .get_header_all("set-cookie") - .map(|value| value.to_str().expect("utf8").to_owned()) - .collect(); - assert_eq!(cookies, vec!["a=1".to_owned(), "b=2".to_owned()]); + assert_eq!(result, Ok(())); + let observed_state = state.borrow(); + let (committed_status, headers, transmits_body) = &observed_state.commits[0]; + assert_eq!(*committed_status, status); + assert_eq!(headers.get("content-length").expect("length"), "99"); + assert!(!transmits_body); + assert!(observed_state.accepted.is_empty()); + } } #[test] - fn stream_body_is_written_to_fastly_response() { + fn payload_response_allows_manual_framing() { let response = response_builder() - .status(200) - .body(Body::stream(stream::iter(vec![ - Bytes::from_static(b"hello "), - Bytes::from_static(b"world"), - ]))) + .status(StatusCode::OK) + .header("content-length", "3") + .body(Body::from("abc")) .expect("response"); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::HostAutomatic, + &Method::GET, + response, + ) + .expect("prepared"); + let now = MonotonicInstant::now(); + let clock = stable_clock(now); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, now); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + + let result = transmit_prepared( + prepared, + future_policy(now), + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); + + assert_eq!(result, Ok(())); + let observed_state = state.borrow(); + let (_, headers, transmits_body) = &observed_state.commits[0]; + assert_eq!(headers.get("content-length").expect("length"), "3"); + assert!(*transmits_body); + assert_eq!(observed_state.accepted, b"abc"); + } + + #[test] + fn generated_overflow_retry_after_precedes_synthesized_headers() { + use edgezero_core::RequestBodyOverflowOrigin; + use edgezero_core::response_egress_framing::test_support::synthesized_error_fixture; + for (error, retry) in [ + ( + EdgeError::payload_too_large(RequestBodyOverflowOrigin::Inbound), + None, + ), + ( + EdgeError::payload_too_large(RequestBodyOverflowOrigin::OutboundUpload), + None, + ), + ( + EdgeError::config_out_of_date("private", "field"), + Some("60"), + ), + ] { + let fixture_envelope = synthesized_error_fixture(error).expect("fixture"); + let mut replacement = HeaderMap::new(); + replacement.insert("x-synthesized-fixture", "request".parse().expect("field")); + replacement.insert("retry-after", "360".parse().expect("retry")); + let envelope = fixture_envelope.with_synthesized_response_headers( + edgezero_core::SynthesizedResponseHeaders::new(replacement) + .expect("request policy"), + ); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + assert_eq!(send_egress_with(envelope, &mut committer).error(), None); + let snapshot = state.borrow(); + let headers = &snapshot.commits[0].1; + assert_eq!(headers["x-synthesized-fixture"], "request"); + assert_eq!( + headers + .get("retry-after") + .map(|value| value.to_str().expect("text")), + retry + ); + } + } + + #[test] + fn framing_failure_sends_bounded_fallback_with_original_cause() { + let mut app = App::new( + RouterService::builder() + .get("/", invalid_length_response) + .build(), + ); + let observer = RecordingObserver::default(); + app.set_response_egress_observer(observer.clone()); + let request_start = app.monotonic_now(); + let request = request_builder() + .method(Method::GET) + .uri("/") + .body(Body::empty()) + .expect("request"); + let egress = response_egress( + executor::block_on(app.dispatch_ingress( + request, + request_start, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("egress"), + ); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + assert_eq!(send_egress_with(egress, &mut committer).error(), None); + + let snapshot = state.borrow(); + assert_eq!(snapshot.commits.len(), 1); + assert_eq!(snapshot.commits[0].0, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(snapshot.accepted, INTERNAL_FALLBACK_BODY); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::ConversionError); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Completed) + ); + } + + #[test] + fn expired_application_deadline_uses_independent_fallback_budget() { + let mut app = App::new(RouterService::builder().get("/", ordinary_response).build()); + let now = MonotonicInstant::now(); + app.set_monotonic_clock(stable_clock(now)); + app.set_response_egress_policy(move |_, _| ResponseEgressPolicy { + write_deadline: Deadline::at_instant(now), + }); + let observer = RecordingObserver::default(); + app.set_response_egress_observer(observer.clone()); + let request = request_builder() + .method(Method::GET) + .uri("/") + .body(Body::empty()) + .expect("request"); + let egress = response_egress( + executor::block_on(app.dispatch_ingress( + request, + now, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("egress"), + ); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + assert_eq!(send_egress_with(egress, &mut committer).error(), None); + assert_eq!(state.borrow().commits[0].0, StatusCode::GATEWAY_TIMEOUT); + assert_eq!(state.borrow().accepted, TIMEOUT_FALLBACK_BODY); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + } + + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn head_fallback_suppresses_payload_and_policy_panic_stays_one_attempt() { + let mut app = App::new( + RouterService::builder() + .route("/", Method::HEAD, ordinary_response) + .build(), + ); + app.set_response_egress_policy(|_, _| panic!("private policy payload")); + let observer = RecordingObserver::default(); + app.set_response_egress_observer(observer.clone()); + let request_start = app.monotonic_now(); + let request = request_builder() + .method(Method::HEAD) + .uri("/") + .body(Body::empty()) + .expect("request"); + let egress = response_egress( + executor::block_on(app.dispatch_ingress( + request, + request_start, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("egress"), + ); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + assert_eq!(send_egress_with(egress, &mut committer).error(), None); + assert!(state.borrow().accepted.is_empty()); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!(reports[0].bytes_written, 0); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::ConversionError); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Completed) + ); + assert_eq!(reports[0].framing_failures.application(), None); + assert_eq!(reports[0].framing_failures.fallback(), None); + } + + #[test] + fn fallback_safety_deadline_aborts_and_preserves_original_cause() { + let mut app = App::new( + RouterService::builder() + .get("/", invalid_length_response) + .build(), + ); + let start = MonotonicInstant::now(); + let safety_deadline = start + .checked_add(RESPONSE_EGRESS_FALLBACK_SAFETY_BUDGET) + .expect("safety deadline"); + let reads = Arc::new(AtomicUsize::new(0)); + let clock_reads = Arc::clone(&reads); + app.set_monotonic_clock(MonotonicClock::new(move || { + if clock_reads.fetch_add(1, Ordering::SeqCst) < 3 { + start + } else { + safety_deadline + } + })); + let observer = RecordingObserver::default(); + app.set_response_egress_observer(observer.clone()); + let request = request_builder() + .method(Method::GET) + .uri("/") + .body(Body::empty()) + .expect("request"); + let egress = response_egress( + executor::block_on(app.dispatch_ingress( + request, + start, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("egress"), + ); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + assert_eq!( + send_egress_with(egress, &mut committer).error(), + Some(DeliveryError::Deadline) + ); + assert!(state.borrow().accepted.is_empty()); + assert!(state.borrow().commits.is_empty()); + assert!(state.borrow().events.is_empty()); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::ConversionError); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Aborted) + ); + } + + #[test] + fn fallback_staging_and_bulk_commit_failure_preserve_conversion_cause() { + for commit_error in [false, true] { + let mut app = App::new( + RouterService::builder() + .get("/", invalid_length_response) + .build(), + ); + let observer = RecordingObserver::default(); + app.set_response_egress_observer(observer.clone()); + let request_start = app.monotonic_now(); + let request = request_builder() + .method(Method::GET) + .uri("/") + .body(Body::empty()) + .expect("request"); + let egress = response_egress( + executor::block_on(app.dispatch_ingress( + request, + request_start, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("egress"), + ); + let state = Rc::new(RefCell::new(SinkState { + write_plan: if commit_error { + VecDeque::new() + } else { + VecDeque::from([Ok(3), Err(())]) + }, + ..SinkState::default() + })); + let mut committer = MockCommitter::new(Rc::clone(&state)); + if commit_error { + committer.commit_plan = VecDeque::from([MockCommit::Error]); + } + assert!(send_egress_with(egress, &mut committer).error().is_some()); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::ConversionError); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Aborted) + ); + assert_eq!(reports[0].bytes_written, 0); + assert!(state.borrow().accepted.is_empty()); + assert_eq!(state.borrow().commits.len(), usize::from(commit_error)); + } + } + + #[test] + fn head_preparation_error_uses_one_successful_fallback_attempt() { + let mut app = App::new(RouterService::builder().get("/", ordinary_response).build()); + let observer = RecordingObserver::default(); + app.set_response_egress_observer(observer.clone()); + let request_start = app.monotonic_now(); + let request = request_builder() + .method(Method::GET) + .uri("/") + .body(Body::empty()) + .expect("request"); + let egress = response_egress( + executor::block_on(app.dispatch_ingress( + request, + request_start, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("egress"), + ); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter { + commit_plan: VecDeque::new(), + prepare_plan: VecDeque::from([Err(()), Ok(())]), + state: Rc::clone(&state), + }; + assert_eq!(send_egress_with(egress, &mut committer).error(), None); + assert_eq!(state.borrow().commits.len(), 1); + assert_eq!( + state.borrow().commits[0].0, + StatusCode::INTERNAL_SERVER_ERROR + ); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::ConversionError); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Completed) + ); + assert_eq!(reports[0].framing_failures.application(), None); + assert_eq!(reports[0].framing_failures.fallback(), None); + } + + #[test] + fn downstream_commit_error_is_unknown_and_does_not_send_fallback() { + let now = MonotonicInstant::now(); + let clock = stable_clock(now); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, now); + let state = Rc::new(RefCell::new(SinkState::default())); + let mut committer = MockCommitter { + commit_plan: VecDeque::from([MockCommit::Error]), + prepare_plan: VecDeque::new(), + state: Rc::clone(&state), + }; + + let result = transmit_prepared( + prepared(Body::from("application")), + future_policy(now), + &mut attempt, + &clock, + TransmitKind::Application, + &mut committer, + ); - let mut fastly_response = from_core_response(response).expect("fastly response"); - let body_bytes = fastly_response.take_body_bytes(); - assert_eq!(body_bytes, b"hello world"); + assert_eq!( + result, + Err(TransmitFailure::Unknown(DeliveryError::Transport)) + ); + assert_eq!(state.borrow().commits.len(), 1); + assert_eq!(observer.reports().len(), 1); + assert_eq!( + observer.reports()[0].outcome, + ResponseEgressOutcome::TransportError + ); } } diff --git a/crates/edgezero-adapter-fastly/src/secret_store.rs b/crates/edgezero-adapter-fastly/src/secret_store.rs index e83b2b337..f8eca569b 100644 --- a/crates/edgezero-adapter-fastly/src/secret_store.rs +++ b/crates/edgezero-adapter-fastly/src/secret_store.rs @@ -4,23 +4,52 @@ //! `FastlySecretStore`, which opens a named Fastly `SecretStore` on //! each lookup. -#[cfg(feature = "fastly")] +use crate::chunked_config::{SyncHostCallError, exact_fastly_read, run_sync_host_call}; use async_trait::async_trait; -#[cfg(feature = "fastly")] use bytes::Bytes; -#[cfg(feature = "fastly")] +use edgezero_core::config_store::BoundedStoreRead; use edgezero_core::secret_store::{SecretError, SecretStore}; -#[cfg(feature = "fastly")] +use edgezero_core::{Deadline, MonotonicClock}; use fastly::secret_store::SecretStore as FastlyNativeSecretStore; /// Internal helper that opens a single named Fastly `SecretStore`. -#[cfg(feature = "fastly")] pub struct FastlyNamedStore { store: FastlyNativeSecretStore, } -#[cfg(feature = "fastly")] impl FastlyNamedStore { + fn get_bytes_bounded_sync( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError> { + let lookup = run_sync_host_call(clock, deadline, || { + self.store.try_get(key).map_err(|err| { + SecretError::Internal(anyhow::anyhow!("secret lookup failed: {err}")) + }) + }) + .map_err(map_sync_secret_error)?; + + let Some(secret) = lookup else { + return Ok(BoundedStoreRead { + backend_bytes: 0, + value: None, + }); + }; + let plaintext = run_sync_host_call(clock, deadline, || { + secret.try_plaintext().map_err(|err| { + SecretError::Internal(anyhow::anyhow!("secret decryption failed: {err}")) + }) + }) + .map_err(map_sync_secret_error)?; + + exact_fastly_read(Some(plaintext), max_backend_bytes.min(max_value_bytes)) + .map_err(|_size_error| SecretError::ValueTooLarge) + } + pub(crate) fn get_bytes_sync(&self, key: &str) -> Result, SecretError> { let lookup = self .store @@ -53,16 +82,22 @@ impl FastlyNamedStore { })?; Ok(Self { store }) } + + fn open_bounded( + name: &str, + clock: &MonotonicClock, + deadline: Deadline, + ) -> Result { + run_sync_host_call(clock, deadline, || Self::open(name)).map_err(map_sync_secret_error) + } } /// Multi-store provider backed by Fastly's `SecretStore` API. /// /// Opens the named store per call — `FastlyNamedStore::open` is cheap /// (no network; just a handle) so there is no caching. -#[cfg(feature = "fastly")] pub struct FastlySecretStore; -#[cfg(feature = "fastly")] #[async_trait(?Send)] impl SecretStore for FastlySecretStore { #[inline] @@ -70,8 +105,70 @@ impl SecretStore for FastlySecretStore { let store = FastlyNamedStore::open(store_name)?; store.get_bytes_sync(key) } + + #[inline] + async fn get_bytes_bounded( + &self, + store_name: &str, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError> { + let store = FastlyNamedStore::open_bounded(store_name, clock, deadline)?; + store.get_bytes_bounded_sync(key, clock, deadline, max_backend_bytes, max_value_bytes) + } +} + +fn map_sync_secret_error(call_error: SyncHostCallError) -> SecretError { + match call_error { + SyncHostCallError::Backend(backend_error) => backend_error, + SyncHostCallError::DeadlineExceeded => SecretError::DeadlineExceeded, + } } // TODO: integration tests require the Fastly compute environment. // Test `FastlyNamedStore` and `FastlySecretStore` as part of the // Fastly adapter E2E test suite. + +#[cfg(test)] +mod tests { + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + use edgezero_core::{Deadline, MonotonicClock, MonotonicInstant}; + + use super::*; + + #[test] + fn bounded_secret_read_uses_injected_clock() { + let process_now = MonotonicInstant::now(); + let injected_now = process_now + .checked_sub(Duration::from_mins(1)) + .expect("injected instant"); + let clock = MonotonicClock::new(move || injected_now); + let deadline = Deadline::at_instant( + injected_now + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), + ); + let value = run_sync_host_call(&clock, deadline, || Ok::<_, SecretError>("value")) + .expect("the application clock is still before its deadline"); + assert_eq!(value, "value"); + + let start = MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let now = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&now); + let advancing_clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let error = run_sync_host_call(&advancing_clock, Deadline::at_instant(terminal), || { + *now.lock().expect("clock lock") = terminal; + Err::<(), _>(SecretError::Unavailable) + }) + .expect_err("equality expiry must beat a ready provider error"); + assert!(matches!(error, SyncHostCallError::DeadlineExceeded)); + } +} diff --git a/crates/edgezero-adapter-fastly/src/templates/src/main.rs.hbs b/crates/edgezero-adapter-fastly/src/templates/src/main.rs.hbs index 42d97d16f..0b7c16ce8 100644 --- a/crates/edgezero-adapter-fastly/src/templates/src/main.rs.hbs +++ b/crates/edgezero-adapter-fastly/src/templates/src/main.rs.hbs @@ -4,12 +4,8 @@ )] #[cfg(target_arch = "wasm32")] -use fastly::{Error, Request, Response}; - -#[cfg(target_arch = "wasm32")] -#[fastly::main] -pub fn main(req: Request) -> Result { - edgezero_adapter_fastly::run_app::<{{proj_core_mod}}::App>(req) +pub fn main() -> Result<(), fastly::Error> { + edgezero_adapter_fastly::run_app::<{{proj_core_mod}}::App>() } #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/edgezero-adapter-fastly/tests/contract.rs b/crates/edgezero-adapter-fastly/tests/contract.rs index 09c5dbef1..fe615a684 100644 --- a/crates/edgezero-adapter-fastly/tests/contract.rs +++ b/crates/edgezero-adapter-fastly/tests/contract.rs @@ -1,6 +1,5 @@ -#![cfg(all(feature = "fastly", target_arch = "wasm32"))] - // Compile-time check: FastlySecretStore implements SecretStore. +#[cfg(all(feature = "fastly", target_arch = "wasm32"))] mod secret_store_compile_check { use edgezero_adapter_fastly::secret_store::FastlySecretStore; use edgezero_core::secret_store::SecretStore; @@ -17,38 +16,133 @@ mod secret_store_compile_check { } #[cfg(test)] +#[cfg(all(feature = "fastly", feature = "test-utils", target_arch = "wasm32"))] +#[expect( + clippy::arbitrary_source_item_ordering, + reason = "ingress contracts are grouped after the provider fixture tests" +)] mod tests { + use edgezero_core::response_egress_framing::ResponseEgressFramingMode; + use std::io::{Cursor, Read as _, Write as _}; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + use bytes::Bytes; use edgezero_adapter_fastly::context::FastlyRequestContext; - use edgezero_adapter_fastly::request::{ - FastlyService, into_core_request, into_core_request_with_registries, + use edgezero_adapter_fastly::delivery::{ + FastlyRequestDelivery, FastlyResponseCommitState, testing::response_delivery, }; - use edgezero_adapter_fastly::response::from_core_response; - use edgezero_core::app::App; + use edgezero_adapter_fastly::request::{FastlyService, into_core_request}; + use edgezero_core::app::{App, Hooks}; use edgezero_core::body::Body; - use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; - use edgezero_core::context::RequestContext; + use edgezero_core::config_store::{ + BoundedStoreRead, ConfigStore, ConfigStoreError, ConfigStoreHandle, + finish_bounded_config_read, + }; + use edgezero_core::context::{BodyKind, RequestContext}; use edgezero_core::error::EdgeError; - use edgezero_core::http::{Method, Response, StatusCode, response_builder}; - use edgezero_core::router::RouterService; + use edgezero_core::http::{HeaderMap, Method, Response, StatusCode, response_builder}; + use edgezero_core::ingress::{AdmissionDecision, IngressGrant}; + use edgezero_core::outbound::{ + OutboundRequest, OutboundSendMode, OutboundTime, validate_for_dispatch, + }; + use edgezero_core::response_egress::ResponseEgressCompletion; + use edgezero_core::response_egress::{ResponseEgressEnvelope, ResponseEgressReport}; + use edgezero_core::router::{RouteMetadata, RouterService}; + use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; use fastly::Request as FastlyRequest; - use fastly::http::{Method as FastlyMethod, StatusCode as FastlyStatus}; - use futures::executor::block_on; - use futures::stream; - use std::sync::Arc; + use fastly::http::Method as FastlyMethod; + use futures::{executor::block_on, stream}; + + fn complete_envelope(envelope: ResponseEgressEnvelope) -> Response { + complete_envelope_with_report(envelope).0 + } + + fn complete_envelope_with_report( + envelope: ResponseEgressEnvelope, + ) -> (Response, ResponseEgressReport) { + let Ok((prepared, _, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::HostAutomatic) + else { + panic!("begin response egress"); + }; + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + (prepared.into_response(), attempt.into_terminal_report()) + } struct FixedConfigStore(&'static str); + struct FailingConfiguration; + + #[expect( + clippy::missing_trait_methods, + reason = "test hook exercises only adapter startup failure" + )] + impl Hooks for FailingConfiguration { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Err(EdgeError::service_unavailable("configuration unavailable")) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + #[test] + fn failing_configuration() { + let Err(error) = + App::build::(edgezero_adapter_fastly::FASTLY_PLATFORM) + else { + panic!("configuration must not produce an app"); + }; + let clock = MonotonicClock::default(); + let response = complete_envelope(App::assembly_error_egress( + error, + Method::GET, + clock.now(), + clock, + )); + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + let bytes = block_on(response.into_body().into_bytes_bounded(2048)).unwrap(); + let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(value["error"]["message"], "configuration unavailable"); + } + #[async_trait::async_trait(?Send)] impl ConfigStore for FixedConfigStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.to_owned())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.into())) + } + + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + finish_bounded_config_read( + self.get(key).await, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) } } fn build_test_app() -> App { async fn capture_uri(ctx: RequestContext) -> Result { - let body = Body::text(ctx.request().uri().to_string()); + let body = Body::text(ctx.uri().to_string()); let response = response_builder() .status(StatusCode::OK) .body(body) @@ -57,7 +151,7 @@ mod tests { } async fn mirror_body(ctx: RequestContext) -> Result { - let bytes = ctx.request().body().as_bytes().expect("buffered").to_vec(); + let bytes = ctx.body_bytes(1024 * 1024).await?.to_vec(); let response = response_builder() .status(StatusCode::OK) .body(Body::from(bytes)) @@ -88,7 +182,7 @@ mod tests { .await .ok() .flatten() - .unwrap_or_else(|| "missing".to_owned()), + .map_or_else(|| "missing".to_owned(), |value| value.to_string()), None => "missing".to_owned(), }; let response = response_builder() @@ -139,113 +233,1617 @@ mod tests { Some("1") ); + let context = FastlyRequestContext::get(&core_request).expect("context"); + assert_eq!(context.client_ip, expected_ip); + + let body = + block_on(core_request.into_body().into_bytes_bounded(1024)).expect("request body"); + assert_eq!(body.as_ref(), b"payload"); + } + + #[test] + fn sdk_body_absence_is_preserved_without_creating_a_stream() { + for method in [FastlyMethod::GET, FastlyMethod::HEAD, FastlyMethod::POST] { + let request = fastly_request(method, "/empty", None); + assert!(!request.has_body()); + let core = into_core_request(request).expect("conversion"); + assert_eq!(core.body().as_bytes(), Some(b"".as_slice())); + } + } + + #[test] + fn sdk_body_zero_host_length_preserves_unread_read_buffer() { + let mut request = fastly_request(FastlyMethod::POST, "/buffered", Some(b"data")); + let body = request.get_body_mut(); + let mut prefix = [0_u8; 1]; + body.read_exact(&mut prefix).expect("prefetch body"); + assert_eq!(&prefix, b"d"); assert_eq!( - core_request.body().as_bytes().expect("buffered"), - b"payload" + body.known_length(), + Some(0), + "host no longer holds the read buffer" ); - let context = FastlyRequestContext::get(&core_request).expect("context"); - assert_eq!(context.client_ip, expected_ip); + let core = into_core_request(request).expect("conversion"); + assert!( + core.body().is_stream(), + "a host length is not a terminal signal" + ); + let bytes = block_on(core.into_body().into_bytes_bounded(4)).expect("remaining bytes"); + assert_eq!(bytes.as_ref(), b"ata"); } #[test] - fn registry_conversion_preserves_extensions_without_collecting_a_response() { - use edgezero_core::app::{StoreMetadata, StoresMetadata}; - use edgezero_core::store_registry::SecretRegistry; - let request = fastly_request(FastlyMethod::GET, "/stream", None); - let core = into_core_request_with_registries( - request, - StoresMetadata { - secrets: Some(StoreMetadata { - default: "fixture", - ids: &["fixture"], - }), - ..Default::default() - }, - |raw, extensions| { - extensions.insert(raw.get_url_str().to_owned()); - }, - ) - .expect("registry-aware conversion"); - assert!(core.extensions().get::().is_some()); + fn sdk_body_zero_host_length_preserves_pending_write_buffer() { + let mut request = fastly_request(FastlyMethod::POST, "/buffered", None); + let body = request.get_body_mut(); + body.write_all(b"data").expect("buffer body write"); assert_eq!( - core.extensions().get::().unwrap(), - "http://example.com/stream" + body.known_length(), + Some(0), + "host does not hold buffered writes" ); - let response = block_on(build_test_app().router().oneshot(core)).unwrap(); - assert!(matches!(response.body(), Body::Stream(_))); + + let core = into_core_request(request).expect("conversion"); + assert!( + core.body().is_stream(), + "a host length is not a terminal signal" + ); + let bytes = block_on(core.into_body().into_bytes_bounded(4)).expect("buffered bytes"); + assert_eq!(bytes.as_ref(), b"data"); } #[test] - fn from_core_response_translates_status_headers_and_streaming_body() { - let response = response_builder() - .status(StatusCode::CREATED) - .header("x-edgezero-res", "1") - .body(Body::stream(stream::iter(vec![ - Bytes::from_static(b"hello"), - Bytes::from_static(b" "), - Bytes::from_static(b"world"), - ]))) - .expect("response"); + fn sdk_body_present_empty_handle_is_not_certified_as_absent() { + let mut request = fastly_request(FastlyMethod::GET, "/empty", Some(b"")); + assert!(request.has_body()); + assert_eq!(request.get_body_mut().known_length(), Some(0)); + let core = into_core_request(request).expect("conversion"); + assert!(core.body().is_stream()); + let target = "http://example.com/forward".parse().expect("target"); + let outbound = OutboundRequest::from_request(core, target).expect("forward"); + validate_for_dispatch(&outbound, OutboundSendMode::Send) + .expect("ordinary preflight precedes shared GET settlement"); + assert!( + outbound.is_stream_body(), + "preflight cannot certify absence" + ); + let bytes = block_on(outbound.into_parts().body.into_bytes_bounded(0)) + .expect("the SDK stream still must reach EOF"); + assert!(bytes.is_empty()); + } - let mut fastly_response = from_core_response(response).expect("fastly response"); + #[test] + fn sdk_body_shape_is_resolved_after_prepare_without_losing_buffered_bytes() { + #[edgezero_core::action] + async fn inspect_body(ctx: RequestContext) -> Result { + let absent = *ctx.extensions().get::().expect("prepare marker"); + assert_eq!(ctx.body_kind(), BodyKind::Initial); + let body = ctx.take_body()?; + assert_eq!(!body.is_stream(), absent); + let bytes = body.into_bytes_bounded(4).await?; + response_builder() + .body(Body::from(bytes)) + .map_err(EdgeError::internal) + } - assert_eq!(fastly_response.get_status(), FastlyStatus::CREATED); - assert!(fastly_response.get_header("x-edgezero-res").is_some()); - assert_eq!(fastly_response.take_body_bytes(), b"hello world"); + for mode in ["absent", "taken", "prefetched", "pending-write"] { + let absent = matches!(mode, "absent" | "taken"); + let mut app = App::new( + RouterService::builder() + .post("/hook-body", inspect_body) + .build(), + ); + let started_at = MonotonicInstant::now(); + app.set_monotonic_clock(MonotonicClock::new(move || started_at)); + app.set_ingress_admission_policy(move |head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(if absent { + Duration::ZERO + } else { + Duration::from_secs(1) + }), + }); + let request = fastly_request( + FastlyMethod::POST, + "/hook-body", + matches!(mode, "taken" | "prefetched").then_some(b"data".as_slice()), + ); + let request_delivery = FastlyService::new(&app).run_request_with_hooks_for_test( + request, + |raw, extensions| { + extensions.insert(absent); + match mode { + "taken" => drop(raw.try_take_body().expect("existing body")), + "prefetched" => { + let body = raw.get_body_mut(); + let mut prefix = [0_u8; 1]; + body.read_exact(&mut prefix).expect("prepare read"); + assert_eq!(&prefix, b"d"); + assert_eq!(body.known_length(), Some(0)); + } + "pending-write" => { + let body = raw.get_body_mut(); + body.write_all(b"data").expect("prepare write"); + assert_eq!(body.known_length(), Some(0)); + } + _ => assert!(!raw.has_body()), + } + }, + |_| (), + |envelope| { + let (response, report) = complete_envelope_with_report(envelope); + assert_eq!(response.status(), StatusCode::OK); + let bytes = block_on(response.into_body().into_bytes_bounded(4)) + .expect("response bytes"); + let expected: &[u8] = match mode { + "prefetched" => b"ata", + "pending-write" => b"data", + _ => b"", + }; + assert_eq!(bytes.as_ref(), expected); + response_delivery(FastlyResponseCommitState::Committed, report, Ok(())) + }, + ); + let FastlyRequestDelivery::Routed { delivery, .. } = request_delivery else { + panic!("expected routed response"); + }; + assert_eq!(delivery.error(), None); + } + } + + #[test] + fn sdk_body_absence_first_read_retains_sticky_admission_timeout() { + #[edgezero_core::action] + async fn read_empty(ctx: RequestContext) -> Result { + assert_eq!(ctx.body_kind(), BodyKind::Initial); + let error = ctx + .body_bytes(0) + .await + .expect_err("first-read deadline applies"); + assert_eq!(error.status(), StatusCode::REQUEST_TIMEOUT); + assert_eq!(ctx.body_kind(), BodyKind::Poisoned); + assert_eq!( + ctx.take_body().expect_err("sticky failure").status(), + error.status() + ); + Err(error) + } + + let mut app = App::new(RouterService::builder().get("/empty", read_empty).build()); + app.set_ingress_admission_policy(|head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: Deadline::at_instant(head.request_start()), + }); + let response = complete_envelope( + FastlyService::new(&app) + .capture_egress_for_test(fastly_request(FastlyMethod::GET, "/empty", None)) + .expect("egress"), + ); + assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT); } #[test] + #[cfg(feature = "test-utils")] fn dispatch_runs_router_and_returns_response() { let app = build_test_app(); let req = fastly_request(FastlyMethod::GET, "/uri", None); - let mut response = FastlyService::new(&app) - .dispatch(req) - .expect("fastly response"); + let response = complete_envelope( + FastlyService::new(&app) + .capture_egress_for_test(req) + .expect("egress envelope"), + ); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + block_on(response.into_body().into_bytes_bounded(64)).expect("response body"), + Bytes::from_static(b"http://example.com/uri") + ); + } + + #[test] + fn helper_batch_owner_is_local_or_reuses_custom_callback() { + use edgezero_adapter_fastly::lifecycle::run_custom; + use edgezero_adapter_fastly::outbound::FastlyOutboundClient; + use edgezero_core::{HttpClient, OutboundBatch}; + use futures_util::future::pending; + use std::cell::RefCell; + use std::rc::Rc; + + struct TimerOwner(Arc); + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for TimerOwner { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } - assert_eq!(response.get_status(), FastlyStatus::OK); - assert_eq!(response.take_body_bytes(), b"http://example.com/uri"); + for wrapped in [false, true] { + let app = build_test_app(); + let released = Arc::new(AtomicUsize::new(0)); + let saved: Rc>> = Rc::new(RefCell::new(None)); + let invoke = |request| { + let now = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || now); + let timer_released = Arc::clone(&released); + let time = OutboundTime::new(clock, move |_, _| { + let owner = TimerOwner(Arc::clone(&timer_released)); + Box::pin(async move { + let _owner = owner; + pending().await + }) + }); + let delivered = FastlyService::new(&app).run_request_with_hooks_for_test( + request, + |_raw, _extensions| { + let client = HttpClient::with_client(FastlyOutboundClient::with_time(time)); + let outbound = OutboundRequest::post("http://127.0.0.1:9/helper-scope") + .unwrap() + .body("pending"); + *saved.borrow_mut() = Some(client.start_batch_until( + vec![outbound], + Deadline::after(Duration::from_secs(1)), + )); + }, + |_response| (), + |envelope| { + let (_response, report) = complete_envelope_with_report(envelope); + response_delivery(FastlyResponseCommitState::Committed, report, Ok(())) + }, + ); + assert!(matches!(delivered, FastlyRequestDelivery::Routed { .. })); + assert_eq!(released.load(Ordering::SeqCst), usize::from(!wrapped)); + Ok::<(), ()>(()) + }; + let request = fastly_request(FastlyMethod::GET, "/uri", None); + if wrapped { + run_custom::<(), (), _>(request, |raw, _state| invoke(raw)).unwrap(); + } else { + invoke(request).unwrap(); + } + assert_eq!(released.load(Ordering::SeqCst), 1); + let batch = saved.borrow_mut().take().expect("prepared batch"); + assert!(matches!( + block_on(batch.collect()).unwrap_err().error(), + EdgeError::Internal { .. } + )); + assert_eq!(released.load(Ordering::SeqCst), 1); + } } #[test] + #[cfg(feature = "test-utils")] fn dispatch_streaming_route_preserves_chunks() { + use futures_util::StreamExt as _; + let app = build_test_app(); let req = fastly_request(FastlyMethod::GET, "/stream", None); + let envelope = FastlyService::new(&app) + .capture_egress_for_test(req) + .expect("egress envelope"); + let Ok((prepared, _, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::HostAutomatic) + else { + panic!("begin streamed response egress"); + }; + assert!(attempt.begin_writing()); + let response = prepared.into_response(); + assert_eq!(response.status(), StatusCode::OK); + let mut source = response + .into_body() + .into_stream() + .expect("streamed response"); + assert_eq!( + block_on(source.next()).unwrap().expect("first chunk"), + Bytes::from_static(b"chunk-1") + ); + assert_eq!( + block_on(source.next()).unwrap().expect("second chunk"), + Bytes::from_static(b"chunk-2") + ); + assert!(block_on(source.next()).is_none()); + assert!(attempt.complete(clock.now())); + } + + #[test] + #[cfg(feature = "test-utils")] + fn closed_lifecycle_orders_hooks_and_returns_state_after_delivery() { + #[derive(Clone)] + struct HookValue(&'static str); + + let events = Arc::new(Mutex::new(Vec::new())); + let handler_events = Arc::clone(&events); + let router = RouterService::builder() + .get("/hook", move |ctx: RequestContext| { + let request_events = Arc::clone(&handler_events); + async move { + request_events.lock().expect("events").push("handler"); + let value = ctx + .extensions() + .get::() + .map_or("missing", |value| value.0); + Ok::<_, EdgeError>(value) + } + }) + .build(); + let app = App::new(router); + let request = fastly_request(FastlyMethod::GET, "/ignored", None); + let prepare_events = Arc::clone(&events); + let finalize_events = Arc::clone(&events); + let delivery_events = Arc::clone(&events); - let mut response = FastlyService::new(&app) - .dispatch(req) - .expect("fastly response"); + let state = FastlyService::new(&app).run_request_with_hooks_for_test( + request, + move |raw, extensions| { + prepare_events.lock().expect("events").push("prepare"); + raw.set_url("http://example.com/hook"); + extensions.insert(HookValue("visible")); + }, + move |response| { + finalize_events.lock().expect("events").push("finalize"); + *response.status_mut() = StatusCode::CREATED; + response + .headers_mut() + .insert("x-finalized", "yes".parse().expect("header")); + 42_u8 + }, + move |envelope| { + delivery_events.lock().expect("events").push("deliver"); + let (response, report) = complete_envelope_with_report(envelope); + assert_eq!(response.status(), StatusCode::CREATED); + assert_eq!( + response.headers().get("x-finalized"), + Some(&"yes".parse().expect("header")) + ); + assert_eq!( + block_on(response.into_body().into_bytes_bounded(64)).expect("response body"), + Bytes::from_static(b"visible") + ); + response_delivery(FastlyResponseCommitState::Committed, report, Ok(())) + }, + ); - assert_eq!(response.get_status(), FastlyStatus::OK); - assert_eq!(response.take_body_bytes(), b"chunk-1chunk-2"); + let FastlyRequestDelivery::Routed { + state: hook_state, + delivery, + } = state + else { + panic!("closed lifecycle lost routed state"); + }; + assert_eq!(hook_state, 42); + assert_eq!(delivery.error(), None); + assert_eq!( + *events.lock().expect("events"), + ["prepare", "handler", "finalize", "deliver"] + ); } #[test] + #[cfg(feature = "test-utils")] + fn prepare_synthesized_headers_replace_app_policy_before_head_validation() { + for replacement in ["request", "invalid", "empty"] { + let mut app = App::new(RouterService::builder().build()); + let mut app_fields = HeaderMap::new(); + app_fields.insert("x-build", "app".parse().expect("field")); + app.set_synthesized_response_headers(app_fields) + .expect("configured"); + let request = + fastly_request(FastlyMethod::GET, &format!("/{}", "a".repeat(9000)), None); + let delivery = FastlyService::new(&app).run_request_with_hooks_for_test( + request, + |_, extensions| { + let mut fields = HeaderMap::new(); + if replacement != "empty" { + let value = if replacement == "invalid" { + "a".repeat(4096) + } else { + "request".to_owned() + }; + fields.insert("x-build", value.parse().expect("value")); + } + match edgezero_core::SynthesizedResponseHeaders::new(fields) { + Ok(headers) => { + extensions.insert(headers); + } + Err(reason) => { + assert_eq!( + reason, + edgezero_core::SynthesizedResponseHeadersError::TooManyBytes + ); + } + } + }, + |_| panic!("detached response must skip finalize"), + |envelope| { + let (response, report) = complete_envelope_with_report(envelope); + assert_eq!(response.status(), StatusCode::URI_TOO_LONG); + let expected = match replacement { + "request" => Some("request"), + "invalid" => Some("app"), + _ => None, + }; + assert_eq!( + response + .headers() + .get("x-build") + .map(|value| value.to_str().expect("text")), + expected + ); + response_delivery(FastlyResponseCommitState::Committed, report, Ok(())) + }, + ); + assert!(matches!(delivery, FastlyRequestDelivery::Detached(_))); + } + } + + #[test] + #[cfg(feature = "test-utils")] + fn prepare_synthesized_headers_survive_routed_framing_failure() { + let app = App::new( + RouterService::builder() + .get("/", |_ctx: RequestContext| async { + Ok::<_, EdgeError>( + response_builder() + .header("trailer", "private") + .body(Body::from("x")) + .expect("invalid response"), + ) + }) + .build(), + ); + let result = FastlyService::new(&app).run_request_with_hooks_for_test( + fastly_request(FastlyMethod::GET, "/", None), + |_, extensions| { + let mut fields = HeaderMap::new(); + fields.insert("x-build", "request".parse().expect("field")); + extensions.insert( + edgezero_core::SynthesizedResponseHeaders::new(fields).expect("validated"), + ); + }, + |_| (), + |envelope| { + let Err(failure) = envelope.begin(ResponseEgressFramingMode::HostAutomatic) else { + panic!("framing failure"); + }; + let (mut attempt, clock, cause) = failure.into_parts(); + assert!(attempt.begin_fallback(cause)); + let prepared = attempt + .prepare_response( + ResponseEgressFramingMode::HostAutomatic, + &Method::GET, + response_builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("fixed fallback")) + .expect("fallback"), + ) + .expect("prepared fallback"); + assert_eq!(prepared.into_response().headers()["x-build"], "request"); + assert!(attempt.begin_writing()); + assert!(attempt.finish_fallback( + edgezero_core::ResponseEgressFallbackDisposition::Completed, + clock.now() + )); + response_delivery( + FastlyResponseCommitState::Committed, + attempt.into_terminal_report(), + Ok(()), + ) + }, + ); + assert!(matches!(result, FastlyRequestDelivery::Routed { .. })); + } + + #[test] + #[cfg(feature = "test-utils")] + fn detached_ingress_response_skips_routed_response_hook() { + use edgezero_core::ingress::AdmissionDecision; + + let mut app = build_test_app(); + app.set_ingress_admission_policy(|_head| AdmissionDecision::Refuse { + completion: edgezero_core::ResponseEgressCompletion::empty(), + response: response_builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .body(Body::from("refused")) + .expect("refusal"), + }); + let finalized = Arc::new(AtomicUsize::new(0)); + let finalized_hook = Arc::clone(&finalized); + let delivered = Arc::new(AtomicUsize::new(0)); + let delivered_hook = Arc::clone(&delivered); + + let state = FastlyService::new(&app).run_request_with_hooks_for_test( + fastly_request(FastlyMethod::GET, "/uri", None), + |_raw, _extensions| {}, + move |_response| { + finalized_hook.fetch_add(1, Ordering::SeqCst); + 7_u8 + }, + move |envelope| { + delivered_hook.fetch_add(1, Ordering::SeqCst); + let (_response, report) = complete_envelope_with_report(envelope); + response_delivery(FastlyResponseCommitState::Committed, report, Ok(())) + }, + ); + + assert!(matches!(state, FastlyRequestDelivery::Detached(_))); + assert_eq!(finalized.load(Ordering::SeqCst), 0); + assert_eq!(delivered.load(Ordering::SeqCst), 1); + } + + #[test] + #[cfg(feature = "test-utils")] fn dispatch_passes_request_body_to_handlers() { let app = build_test_app(); let req = fastly_request(FastlyMethod::POST, "/mirror", Some(b"echo")); - let mut response = FastlyService::new(&app) - .dispatch(req) - .expect("fastly response"); + let response = complete_envelope( + FastlyService::new(&app) + .capture_egress_for_test(req) + .expect("egress envelope"), + ); - assert_eq!(response.get_status(), FastlyStatus::OK); - assert_eq!(response.take_body_bytes(), b"echo"); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + block_on(response.into_body().into_bytes_bounded(64)).expect("response body"), + Bytes::from_static(b"echo") + ); } #[test] + #[cfg(feature = "test-utils")] fn service_with_config_handle_injects_handle() { let app = build_test_app(); let req = fastly_request(FastlyMethod::GET, "/config", None); let handle = ConfigStoreHandle::new(Arc::new(FixedConfigStore("hello from fastly test"))); - let mut response = FastlyService::new(&app) - .with_config_handle(handle) - .dispatch(req) - .expect("fastly response"); + let response = complete_envelope( + FastlyService::new(&app) + .with_config_handle(handle) + .capture_egress_for_test(req) + .expect("egress envelope"), + ); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + block_on(response.into_body().into_bytes_bounded(64)).expect("response body"), + Bytes::from_static(b"hello from fastly test") + ); + } + + #[cfg(feature = "test-utils")] + mod ingress_contract { + use std::collections::VecDeque; + use std::io::{self, Read}; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + use edgezero_adapter_fastly::request::{ + dispatch_ingress_reader_for_test, dispatch_ingress_source_error_for_test, + dispatch_ingress_with_for_test, + }; + use edgezero_core::http::{HeaderMap, HeaderValue}; + use edgezero_core::ingress::{ + AdmissionDecision, BufferedIngressResponse, IngressGrant, IngressHeadLimits, + }; + use edgezero_core::middleware::{Middleware, Next}; + use edgezero_core::response_egress::{ + DEFAULT_RESPONSE_WRITE_BUDGET, DetachedResponseEgressDecision, + ResponseEgressCompletion, ResponseEgressEnvelope, ResponseEgressObserver, + ResponseEgressOutcome, ResponseEgressReport, + }; + use edgezero_core::router::RouteResolution; + use edgezero_core::time::{MonotonicClock, MonotonicInstant}; + + use super::*; + + #[test] + fn admission_abort() { + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + let delivery_calls = Arc::new(AtomicUsize::new(0)); + let observed_delivery_calls = Arc::clone(&delivery_calls); + let mut app = App::new(RouterService::builder().build()); + app.set_ingress_admission_policy(|_| AdmissionDecision::Abort); + + let error = dispatch_ingress_with_for_test( + &app, + Method::POST, + "/missing".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(Cursor::new(Vec::::new())) + }, + move |_response| { + observed_delivery_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + }, + ) + .expect_err("abort must return a platform error"); + + assert!(error.to_string().contains("ingress admission aborted")); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_eq!(delivery_calls.load(Ordering::SeqCst), 0); + } + + struct DropSignal(Arc); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + struct CountingMiddleware(Arc); + + #[derive(Clone)] + struct RecordingEgressObserver(Arc>>); + + impl ResponseEgressObserver for RecordingEgressObserver { + fn complete(&self, report: &ResponseEgressReport) { + self.0.lock().expect("reports lock").push(report.clone()); + } + } + + #[async_trait::async_trait(?Send)] + impl Middleware for CountingMiddleware { + async fn handle( + &self, + ctx: RequestContext, + next: Next<'_>, + ) -> Result { + self.0.fetch_add(1, Ordering::SeqCst); + next.run(ctx).await + } + } + + struct TrackedReader { + body_reads: Arc, + chunks: VecDeque, + expire_on_read: Option<(Arc>, MonotonicInstant)>, + grant_drops: Arc, + _source_drop: DropSignal, + } + + struct ErrorReader { + body_reads: Arc, + grant_drops: Arc, + _source_drop: DropSignal, + } + + #[expect( + clippy::missing_trait_methods, + reason = "the test source only needs the production wrapper's read method" + )] + impl Read for ErrorReader { + fn read(&mut self, _buf: &mut [u8]) -> io::Result { + if self.grant_drops.load(Ordering::SeqCst) != 0 { + return Err(io::Error::other("ingress grant dropped during body read")); + } + self.body_reads.fetch_add(1, Ordering::SeqCst); + Err(io::Error::other("fastly ingress source failure")) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "the test source only needs the production wrapper's read method" + )] + impl Read for TrackedReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if self.grant_drops.load(Ordering::SeqCst) != 0 { + return Err(io::Error::other("ingress grant dropped during body read")); + } + self.body_reads.fetch_add(1, Ordering::SeqCst); + if let Some((now, deadline)) = &self.expire_on_read { + *now.lock() + .map_err(|_poisoned| io::Error::other("clock lock poisoned"))? = *deadline; + } + let Some(chunk) = self.chunks.pop_front() else { + return Ok(0); + }; + if chunk.len() > buf.len() { + return Err(io::Error::other("test chunk exceeds read buffer")); + } + buf[..chunk.len()].copy_from_slice(&chunk); + Ok(chunk.len()) + } + } + + fn tracked_reader( + chunks: Vec, + grant_drops: &Arc, + source_drops: &Arc, + body_reads: &Arc, + expire_on_read: Option<(Arc>, MonotonicInstant)>, + ) -> TrackedReader { + TrackedReader { + body_reads: Arc::clone(body_reads), + chunks: chunks.into(), + expire_on_read, + grant_drops: Arc::clone(grant_drops), + _source_drop: DropSignal(Arc::clone(source_drops)), + } + } + + fn error_reader( + grant_drops: &Arc, + source_drops: &Arc, + body_reads: &Arc, + ) -> ErrorReader { + ErrorReader { + body_reads: Arc::clone(body_reads), + grant_drops: Arc::clone(grant_drops), + _source_drop: DropSignal(Arc::clone(source_drops)), + } + } + + fn guarded_fallback_app() -> (App, Arc, Arc) { + let handler_calls = Arc::new(AtomicUsize::new(0)); + let handler_counter = Arc::clone(&handler_calls); + let middleware_calls = Arc::new(AtomicUsize::new(0)); + let router = RouterService::builder() + .get("/known", move |_ctx: RequestContext| { + let request_handler_calls = Arc::clone(&handler_counter); + async move { + request_handler_calls.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("handler must not run") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware_calls))) + .build(); + (App::new(router), handler_calls, middleware_calls) + } + + fn fallback_app( + max_body_bytes: usize, + read_budget: Duration, + grant_drops: &Arc, + ) -> (App, Arc, Arc) { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let observed_grant_drops = Arc::clone(grant_drops); + app.set_ingress_admission_policy(move |head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::MethodNotAllowed { .. } | RouteResolution::NotFound + )); + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes, + read_deadline: head.read_deadline_after(read_budget), + on_exceeded: terminal_response( + StatusCode::UNPROCESSABLE_ENTITY, + "overflow", + b"fastly overflow\0response", + ), + on_timeout: terminal_response( + StatusCode::GATEWAY_TIMEOUT, + "timeout", + b"fastly timeout response\n", + ), + } + }); + (app, handler_calls, middleware_calls) + } + + fn terminal_headers(marker: &'static str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert( + "content-type", + HeaderValue::from_static("application/octet-stream"), + ); + headers.insert("x-ingress-terminal", HeaderValue::from_static(marker)); + headers + } + + fn terminal_response( + status: StatusCode, + marker: &'static str, + body: &'static [u8], + ) -> BufferedIngressResponse { + BufferedIngressResponse::new(status, terminal_headers(marker), Bytes::from_static(body)) + } + + fn internal_error_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("content-length", HeaderValue::from_static("76")); + headers.insert("content-type", HeaderValue::from_static("application/json")); + headers + } + + fn assert_no_route_dispatch(handler_calls: &AtomicUsize, middleware_calls: &AtomicUsize) { + assert_eq!(handler_calls.load(Ordering::SeqCst), 0); + assert_eq!(middleware_calls.load(Ordering::SeqCst), 0); + } + + fn complete_response(envelope: ResponseEgressEnvelope) -> Response { + let Ok((prepared, _policy, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::HostAutomatic) + else { + panic!("test egress envelope must prepare"); + }; + assert!(attempt.begin_writing()); + assert!(attempt.terminate(ResponseEgressOutcome::HostHandoff, clock.now())); + prepared.into_response() + } + + #[test] + fn normalized_ingress_error_returns_typed_response_without_application_observation() { + let reports = Arc::new(Mutex::new(Vec::new())); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let mut app = App::new(RouterService::builder().build()); + app.set_ingress_head_limits( + IngressHeadLimits::default() + .with_max_request_target_bytes(4) + .expect("target limit"), + ); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let observed_factory_calls = Arc::clone(&factory_calls); + let observed_completion_calls = Arc::clone(&completion_calls); + app.set_detached_response_egress_decision_factory(move |head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + let terminal_calls = Arc::clone(&observed_completion_calls); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |_report| { + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), + } + }); + + let egress = dispatch_ingress_reader_for_test( + &app, + Method::GET, + "/too-long".parse().expect("URI"), + Cursor::new(Vec::::new()), + ) + .expect("typed response"); + let response = complete_response(egress); + + assert_eq!(response.status(), StatusCode::URI_TOO_LONG); + assert!(reports.lock().expect("reports lock").is_empty()); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn normalized_ingress_error_decision_abort_uses_provider_error() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + app.set_ingress_head_limits( + IngressHeadLimits::default() + .with_max_request_target_bytes(4) + .expect("target limit"), + ); + let reports = Arc::new(Mutex::new(Vec::new())); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + app.set_detached_response_egress_decision_factory(move |_head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + DetachedResponseEgressDecision::Abort + }); + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + let delivery_calls = Arc::new(AtomicUsize::new(0)); + let observed_delivery_calls = Arc::clone(&delivery_calls); + + let error = dispatch_ingress_with_for_test( + &app, + Method::GET, + "/too-long".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(Cursor::new(Vec::::new())) + }, + move |_egress| { + observed_delivery_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + }, + ) + .expect_err("detached abort must return a platform error"); + + assert!(error.to_string().contains("ingress admission aborted")); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_eq!(delivery_calls.load(Ordering::SeqCst), 0); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[test] + fn admission_policy_error_decision_send_uses_detached_completion() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let decision_completion_calls = Arc::new(AtomicUsize::new(0)); + let decision_resource_drops = Arc::new(AtomicUsize::new(0)); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_decision_completion_calls = Arc::clone(&decision_completion_calls); + let observed_decision_resource_drops = Arc::clone(&decision_resource_drops); + let observed_grant_drops = Arc::clone(&grant_drops); + app.set_ingress_admission_policy(move |head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::Matched(_) + )); + let decision_resource = DropSignal(Arc::clone(&observed_decision_resource_drops)); + let terminal_calls = Arc::clone(&observed_decision_completion_calls); + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::new(move |_report| { + let _resource = decision_resource; + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes: 1, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text( + StatusCode::PAYLOAD_TOO_LARGE, + "too large", + ), + on_timeout: BufferedIngressResponse::text( + StatusCode::REQUEST_TIMEOUT, + "timeout", + ), + } + }); + let reports = Arc::new(Mutex::new(Vec::new())); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + let observed_completion_calls = Arc::clone(&completion_calls); + app.set_detached_response_egress_decision_factory(move |head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + let terminal_calls = Arc::clone(&observed_completion_calls); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |_report| { + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), + } + }); + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + let response_status = Arc::new(Mutex::new(None)); + let observed_response_status = Arc::clone(&response_status); + + dispatch_ingress_with_for_test( + &app, + Method::GET, + "/known".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(Cursor::new(Vec::::new())) + }, + move |egress| { + *observed_response_status.lock().expect("status lock") = + Some(complete_response(egress).status()); + Ok(()) + }, + ) + .expect("typed policy error response"); + + assert_eq!( + *response_status.lock().expect("status lock"), + Some(StatusCode::INTERNAL_SERVER_ERROR) + ); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + assert_eq!(decision_completion_calls.load(Ordering::SeqCst), 0); + assert_eq!(decision_resource_drops.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[test] + fn admission_policy_error_decision_abort_uses_provider_error() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let decision_resource_drops = Arc::new(AtomicUsize::new(0)); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_decision_resource_drops = Arc::clone(&decision_resource_drops); + let observed_grant_drops = Arc::clone(&grant_drops); + app.set_ingress_admission_policy(move |head| { + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::new({ + let resource = DropSignal(Arc::clone(&observed_decision_resource_drops)); + move |_report| drop(resource) + }), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes: 1, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text( + StatusCode::PAYLOAD_TOO_LARGE, + "too large", + ), + on_timeout: BufferedIngressResponse::text( + StatusCode::REQUEST_TIMEOUT, + "timeout", + ), + } + }); + let reports = Arc::new(Mutex::new(Vec::new())); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + app.set_detached_response_egress_decision_factory(move |_head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + DetachedResponseEgressDecision::Abort + }); + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + let delivery_calls = Arc::new(AtomicUsize::new(0)); + let observed_delivery_calls = Arc::clone(&delivery_calls); + + let error = dispatch_ingress_with_for_test( + &app, + Method::GET, + "/known".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(Cursor::new(Vec::::new())) + }, + move |_egress| { + observed_delivery_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + }, + ) + .expect_err("detached abort must return a platform error"); + + assert!(error.to_string().contains("ingress admission aborted")); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_eq!(delivery_calls.load(Ordering::SeqCst), 0); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + assert_eq!(decision_resource_drops.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[test] + fn post_admission_failure_uses_owned_error_egress() { + let reports = Arc::new(Mutex::new(Vec::new())); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_grant_drops = Arc::clone(&grant_drops); + let mut app = App::new( + RouterService::builder() + .get("/owned", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("handler must not run") + }) + .build(), + ); + app.set_ingress_admission_policy(move |head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + }); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + + let egress = dispatch_ingress_source_error_for_test( + &app, + Method::GET, + "/owned".parse().expect("URI"), + ) + .expect("owned error egress"); + let response = complete_response(egress); + + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + let observed = reports.lock().expect("reports lock"); + assert_eq!(observed.len(), 1); + let report = observed.first().expect("one report"); + assert_eq!(report.outcome, ResponseEgressOutcome::HostHandoff); + assert_eq!( + report.route.as_ref().map(RouteMetadata::pattern), + Some("/owned") + ); + } + + fn assert_terminal_response( + response: Response, + status: StatusCode, + expected_headers: &HeaderMap, + body: &[u8], + ) { + assert_eq!(response.status(), status); + assert_eq!(response.headers(), expected_headers); + assert_eq!( + block_on(response.into_body().into_bytes_bounded(body.len())) + .expect("terminal body"), + Bytes::copy_from_slice(body) + ); + } + + #[test] + fn exact_cap_preserves_not_found_and_method_not_allowed() { + for (path, expected_status) in [ + ("/missing", StatusCode::NOT_FOUND), + ("/known", StatusCode::METHOD_NOT_ALLOWED), + ] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_reads = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let reader = tracked_reader( + vec![Bytes::from_static(b"ab"), Bytes::from_static(b"cd")], + &grant_drops, + &source_drops, + &body_reads, + None, + ); + + let response = complete_response( + dispatch_ingress_reader_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + reader, + ) + .expect("egress envelope"), + ); + + assert_eq!(response.status(), expected_status); + if expected_status == StatusCode::METHOD_NOT_ALLOWED { + assert_eq!( + response.headers().get("allow").expect("Allow header"), + "GET" + ); + } + assert_eq!(body_reads.load(Ordering::SeqCst), 3); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[test] + fn cap_plus_one_precedes_not_found_and_method_not_allowed() { + for path in ["/missing", "/known"] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_reads = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let reader = tracked_reader( + vec![Bytes::from_static(b"abcd"), Bytes::from_static(b"e")], + &grant_drops, + &source_drops, + &body_reads, + None, + ); + + let response = complete_response( + dispatch_ingress_reader_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + reader, + ) + .expect("egress envelope"), + ); + + assert_terminal_response( + response, + StatusCode::UNPROCESSABLE_ENTITY, + &terminal_headers("overflow"), + b"fastly overflow\0response", + ); + assert_eq!(body_reads.load(Ordering::SeqCst), 2); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[test] + fn standard_service_enforces_fallback_overflow() { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let request = fastly_request(FastlyMethod::POST, "/missing", Some(b"abcde")); + + let response = complete_response( + FastlyService::new(&app) + .capture_egress_for_test(request) + .expect("egress envelope"), + ); + + assert_terminal_response( + response, + StatusCode::UNPROCESSABLE_ENTITY, + &terminal_headers("overflow"), + b"fastly overflow\0response", + ); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + + #[test] + fn cooperative_pre_read_expiry_preserves_application_response() { + for path in ["/missing", "/known"] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_reads = Arc::new(AtomicUsize::new(0)); + let start = MonotonicInstant::now(); + let (mut app, handler_calls, middleware_calls) = + fallback_app(4, Duration::ZERO, &grant_drops); + app.set_monotonic_clock(MonotonicClock::new(move || start)); + let reader = tracked_reader( + vec![Bytes::from_static(b"body")], + &grant_drops, + &source_drops, + &body_reads, + None, + ); + + let response = complete_response( + dispatch_ingress_reader_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + reader, + ) + .expect("egress envelope"), + ); - assert_eq!(response.get_status(), FastlyStatus::OK); - assert_eq!(response.take_body_bytes(), b"hello from fastly test"); + assert_terminal_response( + response, + StatusCode::GATEWAY_TIMEOUT, + &terminal_headers("timeout"), + b"fastly timeout response\n", + ); + assert_eq!(body_reads.load(Ordering::SeqCst), 0); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[test] + fn cooperative_post_read_expiry_preserves_application_response() { + for path in ["/missing", "/known"] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_reads = Arc::new(AtomicUsize::new(0)); + let start = MonotonicInstant::now(); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let now = Arc::new(Mutex::new(start)); + let observed_now = Arc::clone(&now); + let (mut app, handler_calls, middleware_calls) = + fallback_app(4, Duration::from_secs(1), &grant_drops); + app.set_monotonic_clock(MonotonicClock::new(move || { + *observed_now.lock().expect("clock lock") + })); + let reader = tracked_reader( + vec![Bytes::from_static(b"body")], + &grant_drops, + &source_drops, + &body_reads, + Some((now, deadline)), + ); + + let response = complete_response( + dispatch_ingress_reader_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + reader, + ) + .expect("egress envelope"), + ); + + assert_terminal_response( + response, + StatusCode::GATEWAY_TIMEOUT, + &terminal_headers("timeout"), + b"fastly timeout response\n", + ); + assert_eq!(body_reads.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[test] + fn saturated_fallback_refuses_without_reading_body() { + for path in ["/missing", "/known"] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_reads = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let observed_completion_calls = Arc::clone(&completion_calls); + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + app.set_ingress_admission_policy(move |head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::MethodNotAllowed { .. } | RouteResolution::NotFound + )); + AdmissionDecision::Refuse { + completion: { + let calls = Arc::clone(&observed_completion_calls); + ResponseEgressCompletion::new(move |_report| { + calls.fetch_add(1, Ordering::SeqCst); + }) + }, + response: response_builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .header("x-ingress-refusal", "saturated") + .body(Body::from("fastly unavailable\n")) + .expect("refusal response"), + } + }); + let reader = tracked_reader( + vec![Bytes::from_static(b"body")], + &grant_drops, + &source_drops, + &body_reads, + None, + ); + + let response = complete_response( + dispatch_ingress_reader_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + reader, + ) + .expect("egress envelope"), + ); + + let mut expected_headers = HeaderMap::new(); + expected_headers.insert("x-ingress-refusal", HeaderValue::from_static("saturated")); + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(response.headers(), &expected_headers); + assert_eq!( + block_on(response.into_body().into_bytes_bounded(64)).expect("response body"), + Bytes::from_static(b"fastly unavailable\n") + ); + assert_eq!(body_reads.load(Ordering::SeqCst), 0); + assert_eq!(grant_drops.load(Ordering::SeqCst), 0); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + // Fastly exposes the inbound body as synchronous `Read`; there is no + // pending future boundary at which a poll-then-drop cancellation can be + // represented. Source-error termination is the available lifecycle seam. + #[test] + fn synchronous_source_error_uses_fastly_response_boundary_and_releases_lifecycle() { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_reads = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let reader = error_reader(&grant_drops, &source_drops, &body_reads); + + let response = complete_response( + dispatch_ingress_reader_for_test( + &app, + Method::POST, + "/missing".parse().expect("URI"), + reader, + ) + .expect("egress envelope"), + ); + + assert_terminal_response( + response, + StatusCode::INTERNAL_SERVER_ERROR, + &internal_error_headers(), + br#"{"error":{"status":500,"kind":"internal","message":"internal server error"}}"#, + ); + assert_eq!(body_reads.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } +} + +#[cfg(test)] +#[cfg(feature = "test-utils")] +#[cfg_attr( + all(feature = "fastly", target_arch = "wasm32"), + expect( + clippy::arbitrary_source_item_ordering, + reason = "the target-runtime probe follows shared batch contracts" + ) +)] +mod outbound_contract_tests { + use bytes::Bytes; + use edgezero_adapter_fastly::outbound::validate_batch_request_for_test; + use edgezero_core::body::Body; + use edgezero_core::http::{Method, Uri}; + use edgezero_core::outbound::OutboundRequest; + use futures_util::stream; + use std::time::Duration; + + #[test] + fn image_optimizer_batch_rejection_precedes_other_preflight() { + let request = OutboundRequest::get("https://example.com/image") + .unwrap() + .image_optimizer(edgezero_core::OutboundImageOptimizerOptions::new()) + .body(Body::from_stream(stream::pending())) + .timeout(Duration::ZERO); + let error = + validate_batch_request_for_test(&request).expect_err("unsupported batch option"); + assert!(error.message().contains("image_optimizer")); + assert!(error.message().contains("fastly")); + } + fn request() -> OutboundRequest { + OutboundRequest::new( + Method::POST, + "https://example.com/bid".parse::().expect("URI"), + ) + .expect("request") + } + + #[test] + fn buffered_upload_cap_has_typed_preflight_boundaries() { + for len in [7_usize, 8, 9] { + let request = OutboundRequest::post("https://example.com/upload") + .expect("request") + .body(vec![b'x'; len]) + .max_request_body_bytes(8); + let result = validate_batch_request_for_test(&request); + if len > 8 { + let error = result.expect_err("overflow before dispatch"); + assert_eq!( + error.request_body_overflow_origin(), + Some(edgezero_core::RequestBodyOverflowOrigin::OutboundUpload) + ); + } else { + result.expect("within cap"); + } + } + } + + #[test] + fn batch_preflight_rejects_streamed_slots_without_poisoning_siblings() { + let requests = [ + request().body(Bytes::from_static(b"first")), + request().body(Body::stream(stream::once(async { + Bytes::from_static(b"streamed") + }))), + request().stream_response(), + request().body(Bytes::from_static(b"last")), + ]; + let outcomes: Vec<_> = requests + .iter() + .map(validate_batch_request_for_test) + .collect(); + + let accepted: Vec<_> = outcomes.iter().map(Result::is_ok).collect(); + assert_eq!(accepted, vec![true, false, false, true]); + } + + #[cfg(all(feature = "fastly", target_arch = "wasm32"))] + mod runtime_tests { + use std::time::Duration; + + use edgezero_adapter_fastly::outbound::FastlyOutboundClient; + use edgezero_core::HttpClient; + use edgezero_core::error::EdgeError; + use edgezero_core::time::Deadline; + use futures::executor::block_on; + + use super::*; + + #[test] + fn image_optimizer_unavailable_is_typed_on_viceroy() { + let request = OutboundRequest::get("http://127.0.0.1:9/native-image") + .unwrap() + .image_optimizer( + edgezero_core::OutboundImageOptimizerOptions::new() + .region(edgezero_core::OutboundImageOptimizerRegion::UsEast), + ) + .stream_response() + .timeout(Duration::from_secs(5)); + let error = + block_on(HttpClient::with_client(FastlyOutboundClient::new()).send(request)) + .expect_err("Viceroy has no optimizer"); + assert!(matches!( + error, + EdgeError::BadGateway { + reason: edgezero_core::BadGatewayReason::ImageOptimizerUnavailable, + .. + } + )); + assert!(matches!( + error.provider_failure_detail(), + Some(edgezero_core::ProviderFailureDetail::Fastly { + cause: edgezero_core::FastlyFailureCause::ImageOptimizerUnsupported, + .. + }) + )); + assert!(!error.message().contains("127.0.0.1")); + } + + #[test] + fn request_preparation_consumes_entry_budget() { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::Poll; + + for selected in [ + edgezero_core::BudgetSource::PerCallTimeout, + edgezero_core::BudgetSource::RequestDeadline, + ] { + for streamed in [false, true] { + let started_at = edgezero_core::MonotonicInstant::now(); + let observed = Arc::new(Mutex::new(started_at)); + let clock_now = Arc::clone(&observed); + let time = edgezero_core::OutboundTime::new( + edgezero_core::MonotonicClock::new(move || *clock_now.lock().unwrap()), + |_, _| panic!("preexpired requests must not register timers"), + ); + let client = HttpClient::with_client(FastlyOutboundClient::with_time(time)); + let polls = Arc::new(AtomicUsize::new(0)); + let source_polls = Arc::clone(&polls); + let body = if streamed { + Body::from_stream(stream::poll_fn(move |_cx| { + source_polls.fetch_add(1, Ordering::SeqCst); + Poll::Pending + })) + } else { + Body::from(Bytes::from_static(b"body")) + }; + let mut outbound = request().body(body).timeout(Duration::from_secs(1)); + if selected == edgezero_core::BudgetSource::RequestDeadline { + outbound = outbound.deadline(Deadline::at_instant( + started_at.checked_add(Duration::from_millis(500)).unwrap(), + )); + } + let send = client.send(outbound); + *observed.lock().unwrap() = + started_at.checked_add(Duration::from_secs(2)).unwrap(); + assert!(matches!( + block_on(send), + Err(EdgeError::GatewayTimeout { cause, .. }) if cause == selected + )); + assert_eq!(polls.load(Ordering::SeqCst), 0); + } + } + } + + #[test] + fn expired_observation_cutoff_leaves_slot_unresolved() { + let now = edgezero_core::MonotonicInstant::now(); + let time = edgezero_core::OutboundTime::new( + edgezero_core::MonotonicClock::new(move || now), + |_, _| panic!("expired cutoff must not register an outbound timer"), + ); + let client = HttpClient::with_client(FastlyOutboundClient::with_time(time)); + let request = request().body(Bytes::from_static(b"body")); + + let results = block_on( + client + .start_batch_until(vec![request], Deadline::at_instant(now)) + .collect(), + ) + .expect("valid batch driver"); + + assert_eq!( + results.termination(), + edgezero_core::OutboundBatchTermination::Cutoff + ); + assert!(matches!( + results.slots().first(), + Some(edgezero_core::OutboundCollectedSlot::Unresolved) + )); + } } } diff --git a/crates/edgezero-adapter-spin/.cargo/config.toml b/crates/edgezero-adapter-spin/.cargo/config.toml index 788dbb508..773cb447b 100644 --- a/crates/edgezero-adapter-spin/.cargo/config.toml +++ b/crates/edgezero-adapter-spin/.cargo/config.toml @@ -1,9 +1,8 @@ [build] target = "wasm32-wasip2" -# Wasmtime runs the spin contract tests (no Fastly host imports needed). -# Only applies when cargo is invoked from inside this package directory. -# CI overrides via `CARGO_TARGET_WASM32_WASIP2_RUNNER` env var in -# `.github/workflows/test.yml`. +# Wasmtime 48.0.2 runs the Spin SDK resource tests with Preview 3 async and +# WASI HTTP imports enabled. Only applies when Cargo is invoked from this +# package directory. CI uses the same command through the target runner env. [target.'cfg(target_arch = "wasm32")'] -runner = "wasmtime run" +runner = "wasmtime run -W component-model-async=y -S p3=y -S http=y" diff --git a/crates/edgezero-adapter-spin/Cargo.toml b/crates/edgezero-adapter-spin/Cargo.toml index f1eeef4d9..f167d7a6c 100644 --- a/crates/edgezero-adapter-spin/Cargo.toml +++ b/crates/edgezero-adapter-spin/Cargo.toml @@ -13,16 +13,16 @@ workspace = true [features] default = [] spin = ["dep:spin-sdk"] +test-utils = [] cli = ["dep:edgezero-adapter", "edgezero-adapter/cli", "dep:ctor", "dep:rusqlite", "dep:toml", "dep:toml_edit", "dep:walkdir"] [dependencies] edgezero-core = { path = "../edgezero-core" } edgezero-adapter = { path = "../edgezero-adapter", optional = true, features = ["cli"] } anyhow = { workspace = true } +async-stream = { workspace = true } async-trait = { workspace = true } -brotli = { workspace = true } bytes = { workspace = true } -flate2 = { workspace = true } futures = { workspace = true } futures-util = { workspace = true } log = { workspace = true } @@ -47,3 +47,15 @@ rusqlite = { workspace = true, optional = true } edgezero-core = { path = "../edgezero-core", features = ["test-utils"] } http-body-util = { workspace = true } tempfile = { workspace = true } + +[[example]] +name = "outbound-eager" +path = "examples/outbound_eager.rs" +crate-type = ["cdylib"] +required-features = ["spin", "test-utils"] + +[[example]] +name = "outbound-bodyless-wire" +path = "examples/outbound_bodyless_wire.rs" +crate-type = ["cdylib"] +required-features = ["spin", "test-utils"] diff --git a/crates/edgezero-adapter-spin/examples/outbound-bodyless-wire.spin.toml b/crates/edgezero-adapter-spin/examples/outbound-bodyless-wire.spin.toml new file mode 100644 index 000000000..2a8c9e6ed --- /dev/null +++ b/crates/edgezero-adapter-spin/examples/outbound-bodyless-wire.spin.toml @@ -0,0 +1,13 @@ +spin_manifest_version = 2 + +[application] +name = "outbound-bodyless-wire" +version = "0.1.0" + +[[trigger.http]] +route = "/..." +component = "wire" + +[component.wire] +source = "../../../target/wasm32-wasip2/debug/examples/outbound_bodyless_wire.wasm" +allowed_outbound_hosts = ["http://127.0.0.1:*"] diff --git a/crates/edgezero-adapter-spin/examples/outbound_bodyless_wire.rs b/crates/edgezero-adapter-spin/examples/outbound_bodyless_wire.rs new file mode 100644 index 000000000..6d22a10fb --- /dev/null +++ b/crates/edgezero-adapter-spin/examples/outbound_bodyless_wire.rs @@ -0,0 +1,394 @@ +#[cfg(target_arch = "wasm32")] +mod sdk { + use core::str::from_utf8; + use std::cell::{Cell, RefCell}; + use std::convert::Infallible; + use std::rc::Rc; + use std::sync::{Arc, Weak}; + use std::time::Duration; + + use async_stream::stream; + use bytes::Bytes; + use edgezero_adapter_spin::outbound::{SpinOutboundClient, canonical_scopes_hold_for_test}; + use edgezero_core::app::{App, Hooks}; + use edgezero_core::context::RequestContext; + use edgezero_core::http::{Method, Response as CoreResponse, Uri}; + use edgezero_core::ingress::{AdmissionDecision, IngressGrant}; + use edgezero_core::outbound::{ + HttpClient, IndependentOutboundProgress, OutboundBatch, OutboundBatchTermination, + OutboundCollectedSlot, OutboundFollowFailure, OutboundFuture, OutboundRedirectAction, + OutboundRedirectLimit, OutboundRequest, OutboundResponse, OutboundScope, + OutboundScopeOwner, + }; + use edgezero_core::response_egress::{ + ResponseEgressCompletion, ResponseEgressOutcome, ResponseEgressReport, + }; + use edgezero_core::router::RouterService; + use edgezero_core::{Body, EdgeError, action}; + use futures_util::{StreamExt as _, stream}; + use serde_json::{Value, json}; + use spin_sdk::http::{FromRequest as _, IntoResponse as _, Request as SpinRequest}; + use spin_sdk::wasip3::exports::http::handler::Guest; + use spin_sdk::wasip3::http::types::{ErrorCode, Fields, Request, Response}; + use spin_sdk::wasip3::{http, wit_future}; + + struct BodylessWire; + + struct HandoffEvidence { + callback: OutboundScope, + eof: bool, + retained: OutboundFuture, + scope: OutboundScope, + upload: Weak<[u8]>, + } + + struct HandoffHooks; + + std::thread_local! { + static HANDOFF: RefCell> = const { RefCell::new(None) }; + } + + impl Guest for BodylessWire { + async fn handle(request: Request) -> Result { + let path = request.get_path_with_query().unwrap_or_default(); + if path == "/handoff" { + let incoming = SpinRequest::from_request(request)?; + return edgezero_adapter_spin::run_app::(incoming) + .await + .map_err(|_error| { + ErrorCode::InternalError(Some("handoff fixture failed".to_owned())) + }); + } + if path == "/scope" { + drop(request); + canonical_scopes_hold_for_test() + .await + .map_err(|error| ErrorCode::InternalError(Some(error.to_string())))?; + return Ok(empty_response()); + } + let method = match path.as_str() { + "/get" | "/get-settled" | "/p5" => Method::GET, + "/head" | "/head-settled" => Method::HEAD, + "/post" => Method::POST, + _ => return Err(ErrorCode::HttpRequestUriInvalid), + }; + let headers = request.get_headers(); + let origins = headers.get("x-origin"); + let origin = origins.first().ok_or(ErrorCode::HttpRequestUriInvalid)?; + let uri: Uri = from_utf8(origin) + .map_err(|_error| ErrorCode::HttpRequestUriInvalid)? + .parse() + .map_err(|_error| ErrorCode::HttpRequestUriInvalid)?; + if uri.scheme_str() != Some("http") + || uri.host() != Some("127.0.0.1") + || uri.port_u16().is_none() + { + return Err(ErrorCode::HttpRequestUriInvalid); + } + if path == "/p5" { + return p5_exchange(uri, &headers).await; + } + let mut outgoing = OutboundRequest::new(method, uri) + .map_err(|_error| ErrorCode::HttpRequestUriInvalid)?; + if path.ends_with("-settled") { + outgoing = outgoing.body(Body::stream(stream::iter([Bytes::new(), Bytes::new()]))); + } + if path == "/post" { + outgoing = outgoing.body(Body::from("upload-control")); + } + HttpClient::with_client(SpinOutboundClient::new()) + .send(outgoing) + .await + .map_err(|_error| { + ErrorCode::InternalError(Some("outbound wire check failed".to_owned())) + })?; + Ok(empty_response()) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "fixture configures only its route and terminal proof" + )] + impl Hooks for HandoffHooks { + fn configure(app: &mut App) -> Result<(), EdgeError> { + app.set_ingress_admission_policy(|head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::new(check_handoff_terminal), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(5)), + }); + Ok(()) + } + + fn routes() -> Result { + Ok(RouterService::builder() + .get("/handoff", handoff_response) + .build()) + } + } + + #[expect( + clippy::print_stdout, + clippy::print_stderr, + reason = "HTTP harness checks the SDK-terminal proof in component logs" + )] + fn check_handoff_terminal(report: &ResponseEgressReport) { + let evidence = HANDOFF.with(|slot| slot.borrow_mut().take()); + if let Some(proof) = evidence + && proof.eof + && proof.callback.check().is_err() + && proof.scope.check().is_err() + && proof.upload.upgrade().is_none() + && report.outcome == ResponseEgressOutcome::HostHandoff + { + drop(proof.retained); + println!("PASS: Spin attached Begin/Batch handoff closed at SDK terminal"); + return; + } + eprintln!("FAIL: Spin attached Begin/Batch handoff terminal ownership"); + } + + fn empty_response() -> Response { + let (complete, trailers) = wit_future::new(|| Ok(None)); + drop(complete); + let (response, result) = Response::new(Fields::new(), None, trailers); + drop(result); + response + } + + fn fixture_error(message: &'static str) -> EdgeError { + EdgeError::internal(anyhow::anyhow!(message)) + } + + fn p5_error(error: &EdgeError) -> Value { + let reason = if let EdgeError::BadGateway { reason, .. } = error { + format!("{reason:?}") + } else if let EdgeError::ResponseTooLarge { reason, .. } = error { + format!("{reason:?}") + } else if let EdgeError::GatewayTimeout { cause, .. } = error { + format!("{cause:?}") + } else { + String::new() + }; + json!({ + "error": { + "kind": error.kind(), + "status": error.status().as_u16(), + "reason": reason, + "provider": error.provider_failure_detail().map(|detail| format!("{detail:?}")), + } + }) + } + + async fn p5_exchange(uri: Uri, headers: &Fields) -> Result { + let modes = headers.get("x-p5-mode"); + let mode = modes.first().map(Vec::as_slice).unwrap_or_default(); + if !matches!(mode, b"send" | b"follow" | b"stop" | b"limit") { + return Err(ErrorCode::HttpRequestUriInvalid); + } + let streamed = headers.get("x-p5-stream") == [b"true".to_vec()]; + let mut request = OutboundRequest::new(Method::GET, uri.clone()) + .map_err(|_error| ErrorCode::HttpRequestUriInvalid)? + .timeout(Duration::from_secs(3)) + .max_response_bytes(10); + if streamed { + request = request.stream_response().max_decoded_response_bytes(10); + } + if headers.get("x-p5-encoded-cap") == [b"true".to_vec()] { + request = request.max_encoded_response_bytes(10); + } + let client = HttpClient::with_client(SpinOutboundClient::new()); + let policies = Rc::new(Cell::new(0_usize)); + let exchange = if mode == b"send" { + client + .send(request) + .await + .map(|response| (response, uri, 0, None)) + .map_err(|error| (0, error)) + } else { + let observed = Rc::clone(&policies); + let stop = mode == b"stop"; + let limit = OutboundRedirectLimit::new(if mode == b"limit" { 1 } else { 4 }) + .map_err(|_error| ErrorCode::HttpRequestUriInvalid)?; + client + .follow(request, limit, move |_| { + observed.set(observed.get().saturating_add(1)); + Ok::<_, Infallible>(if stop { + OutboundRedirectAction::Stop + } else { + OutboundRedirectAction::FollowPrepared + }) + }) + .await + .map(edgezero_core::OutboundFollowResult::into_parts) + .map_err(|failure| match failure { + OutboundFollowFailure::Hop { index, error } => (index, error), + OutboundFollowFailure::Policy { error, .. } => match error {}, + }) + }; + let report = match exchange { + Ok((response, final_uri, redirects, stop)) => { + let status = response.status().as_u16(); + let body_streamed = response.body().is_stream(); + let mut fields = response + .headers() + .iter() + .map(|(name, value)| (name.as_str().to_owned(), value.as_bytes().to_vec())) + .collect::>(); + fields.sort_unstable(); + // This safety bound is above every tested cap, including streamed caps. + let outcome = match response.into_bytes_bounded(1024).await { + Ok(bytes) => { + json!({"status": status, "headers": fields, "bytes": bytes.to_vec()}) + } + Err(error) => p5_error(&error), + }; + json!({ + "outcome": outcome, + "hop": redirects, + "final_uri": final_uri.to_string(), + "redirects": redirects, + "stop": stop.map(|reason| format!("{reason:?}")), + "streamed": body_streamed, + "policies": policies.get(), + }) + } + Err((hop, error)) => json!({ + "outcome": p5_error(&error), "hop": hop, "policies": policies.get(), + }), + }; + report.to_string().into_response() + } + + #[action] + async fn handoff_response( + RequestContext(ctx): RequestContext, + ) -> Result { + let base_uri: Uri = ctx + .headers() + .get("x-origin") + .and_then(|value| value.to_str().ok()) + .ok_or_else(|| fixture_error("handoff origin missing"))? + .parse() + .map_err(EdgeError::internal)?; + if base_uri.scheme_str() != Some("http") + || base_uri.host() != Some("127.0.0.1") + || base_uri.port_u16().is_none() + { + return Err(fixture_error("handoff origin must be loopback")); + } + let base = format!( + "http://{}", + base_uri + .authority() + .ok_or_else(|| fixture_error("handoff authority"))? + ); + let client = ctx + .http_client() + .ok_or_else(|| fixture_error("handoff client missing"))?; + let callback = OutboundScope::capture(Rc::new(IndependentOutboundProgress)); + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let upload = Arc::<[u8]>::from(&b"retained beyond source EOF"[..]); + let probe = Arc::downgrade(&upload); + let unused = OutboundRequest::post(format!("{base}/handoff-unused"))? + .body(Bytes::from_owner(upload)); + let retained = owner.with(|| client.send(unused)); + let origin_request = + OutboundRequest::get(format!("{base}/handoff-origin"))?.stream_response(); + let origin = owner.with(|| client.begin(origin_request)).await?; + let requests = ["A", "B", "C"] + .map(|bidder| OutboundRequest::get(format!("{base}/handoff-bid-{bidder}"))) + .into_iter() + .collect::, _>>()?; + let observation_cutoff = ctx.monotonic_clock().deadline_after(Duration::from_secs(5)); + let batch = owner.with(|| client.start_batch_until(requests, observation_cutoff)); + let previous = HANDOFF.with(|slot| { + slot.replace(Some(HandoffEvidence { + callback, + eof: false, + retained, + scope, + upload: probe, + })) + }); + drop(previous); + // All bidder requests are still pending when the origin head is handed to the handler. + let origin_response = origin.collect().await?; + owner.attach(CoreResponse::new(handoff_source( + client, + base, + origin_response, + batch, + ))) + } + + fn handoff_source( + client: HttpClient, + base: String, + origin_response: OutboundResponse, + batch: OutboundBatch, + ) -> Body { + let source = stream! { + let after_callback = HANDOFF.with(|slot| slot.borrow().as_ref() + .is_some_and(|proof| proof.callback.check().is_err() && proof.scope.check().is_ok())); + if !after_callback { + yield Err(fixture_error("body polled before accepted callback handoff")); + return; + } + let release = match OutboundRequest::get(format!("{base}/handoff-release")) { + Ok(request) => request, + Err(error) => { yield Err(error); return; } + }; + if let Err(error) = client.send(release).await { + yield Err(error); + return; + } + let Ok(mut chunks) = origin_response.into_body().into_stream() else { + yield Err(fixture_error("begun origin must remain streamed")); + return; + }; + while let Some(chunk) = chunks.next().await { + match chunk { + Ok(bytes) => yield Ok(bytes), + Err(error) => { yield Err(error); return; } + } + } + let results = match batch.collect().await { + Ok(results) => results, + Err(failure) => { yield Err(failure.into_parts().0); return; } + }; + let (slots, termination) = results.into_parts(); + if termination != OutboundBatchTermination::Completed || slots.len() != 3 { + yield Err(fixture_error("handoff batch did not complete")); + return; + } + for slot in slots { + let OutboundCollectedSlot::Resolved(result) = slot else { + yield Err(fixture_error("handoff batch lost a slot")); + return; + }; + match result.outcome { + Ok(response) => match response.into_body().into_bytes_bounded(64).await { + Ok(bytes) => yield Ok(bytes), + Err(error) => { yield Err(error); return; } + }, + Err(error) => { yield Err(error); return; } + } + } + let live_at_eof = HANDOFF.with(|slot| { + let mut evidence = slot.borrow_mut(); + evidence.as_mut().is_some_and(|proof| { + proof.eof = true; + proof.scope.check().is_ok() && proof.upload.upgrade().is_some() + }) + }); + if !live_at_eof { + yield Err(fixture_error("response owner closed before source EOF")); + } + }; + Body::from_stream(source) + } + + http::service::export!(BodylessWire); +} diff --git a/crates/edgezero-adapter-spin/examples/outbound_eager.rs b/crates/edgezero-adapter-spin/examples/outbound_eager.rs new file mode 100644 index 000000000..3321cd8dc --- /dev/null +++ b/crates/edgezero-adapter-spin/examples/outbound_eager.rs @@ -0,0 +1,29 @@ +#[cfg(target_arch = "wasm32")] +mod sdk { + use edgezero_adapter_spin::outbound::eager_outbound_paths_hold_for_test; + use spin_sdk::wasip3; + use spin_sdk::wasip3::exports::cli::run::Guest; + + struct OutboundEager; + + impl Guest for OutboundEager { + #[expect( + clippy::print_stdout, + clippy::print_stderr, + reason = "the executable SDK fixture reports its nonzero result to the local runner" + )] + async fn run() -> Result<(), ()> { + eager_outbound_paths_hold_for_test() + .await + .map_err(|error| { + eprintln!("Spin eager SDK checks failed: {error}"); + })?; + println!( + "Spin eager SDK checks passed: callback cleanup, foreign scope, bodyless GET/HEAD resources, begin, batch, late collection, body handoff, cutoff" + ); + Ok(()) + } + } + + wasip3::cli::command::export!(OutboundEager); +} diff --git a/crates/edgezero-adapter-spin/src/cli.rs b/crates/edgezero-adapter-spin/src/cli.rs index 4e14c8322..57b4e8fa8 100644 --- a/crates/edgezero-adapter-spin/src/cli.rs +++ b/crates/edgezero-adapter-spin/src/cli.rs @@ -17,13 +17,14 @@ use edgezero_adapter::cli_support::{ find_manifest_upwards, find_workspace_root, path_distance, read_package_name, run_native_cli, }; use edgezero_adapter::registry::{ - Adapter, AdapterAction, AdapterPushContext, ProvisionStores, ReadConfigEntry, ResolvedStoreId, - TypedSecretEntry, register_adapter, + Adapter, AdapterAction, AdapterExecutionTarget, AdapterPushContext, ProvisionStores, + ReadConfigEntry, ResolvedStoreId, TypedSecretEntry, register_adapter, }; use edgezero_adapter::scaffold::{ AdapterBlueprint, AdapterFileSpec, CommandTemplates, DependencySpec, LoggingDefaults, ManifestSpec, ReadmeInfo, TemplateRegistration, register_adapter_blueprint, }; +use edgezero_core::{Capability, CapabilitySupport, PlatformMetadata}; use walkdir::WalkDir; mod push_cloud; @@ -136,6 +137,38 @@ struct SpinCliAdapter; reason = "KV-backed config dropped Spin's `^[a-z][a-z0-9_]*$` key rule and the config-vs-secret collision check, so `validate_app_config_keys` falls back to the trait default `Ok(())`. `validate_typed_secrets` IS overridden below (secret-value canonicalisation + within-secrets uniqueness still apply). `validate_adapter_manifest` IS overridden below (Spin's multi-component disambiguation). `read_config_entry` and `read_config_entry_local` are both overridden below (four-branch SQLite-direct / Fermyon Cloud / non-Spin-backend dispatch)." )] impl Adapter for SpinCliAdapter { + fn capability(&self, capability: Capability) -> CapabilitySupport { + match capability { + Capability::IngressAdmission + | Capability::OutboundBatchCompletionOrder + | Capability::OutboundBatchSlotIsolation + | Capability::OutboundCacheBypass + | Capability::OutboundFailureDiagnostics + | Capability::OutboundHttp + | Capability::OutboundRedirectFollowing => CapabilitySupport::Native, + Capability::BufferedResponseContentLength + | Capability::ConfigReadDeadlines + | Capability::InboundReadDeadlines + | Capability::IngressAdmissionAbort + | Capability::LazyStreamedResponsePassthrough + | Capability::OutboundBatchCancellation + | Capability::OutboundDeadlines + | Capability::OutboundFlexiblePhaseBudget + | Capability::OutboundHeaderFidelity + | Capability::ResponseEgressAbort + | Capability::ResponseEgressBackpressure + | Capability::ResponseEgressCompletion + | Capability::ResponseWriteDeadlines + | Capability::StreamedUploadDeadlines => CapabilitySupport::BestEffort, + Capability::ConfigReadAllocationBounds + | Capability::OutboundAuthorityOverride + | Capability::OutboundCompleteResourceAccounting + | Capability::RawIngressFramingValidation + | Capability::RawIngressHeadLimits + | _ => CapabilitySupport::Unsupported, + } + } + fn execute(&self, action: AdapterAction, args: &[String]) -> Result<(), String> { match action { // `spin cloud {login|logout|info}` is the native sign-in @@ -168,6 +201,34 @@ impl Adapter for SpinCliAdapter { } } + fn execute_target( + &self, + action: AdapterAction, + target: &AdapterExecutionTarget, + args: &[String], + ) -> Result<(), String> { + let manifest = target_manifest(target)?; + match action { + AdapterAction::Build => { + let artifact = build_from_manifest(&manifest, args)?; + log::info!("[edgezero] Spin build complete -> {}", artifact.display()); + Ok(()) + } + AdapterAction::Deploy => deploy_from_manifest(&manifest, args), + AdapterAction::Serve => serve_from_manifest(&manifest, args), + AdapterAction::AuthLogin + | AdapterAction::AuthLogout + | AdapterAction::AuthStatus + | AdapterAction::DeployStaging + | AdapterAction::EmitVersion + | AdapterAction::Healthcheck + | AdapterAction::Rollback + | _ => Err(format!( + "spin adapter cannot execute operational action {action:?} against a pinned runtime target" + )), + } + } + fn merged_id_kinds(&self) -> &'static [&'static str] { // Both KV and Config back to `spin_sdk::key_value::Store` via // the same `provision` path; declaring the same logical id @@ -180,6 +241,10 @@ impl Adapter for SpinCliAdapter { "spin" } + fn platform_metadata(&self) -> PlatformMetadata { + crate::SPIN_PLATFORM + } + fn provision( &self, manifest_root: &Path, @@ -1053,6 +1118,10 @@ fn ensure_kv_label_in_component( pub fn build(extra_args: &[String]) -> Result { let manifest = find_spin_manifest(env::current_dir().map_err(|err| err.to_string())?.as_path())?; + build_from_manifest(&manifest, extra_args) +} + +fn build_from_manifest(manifest: &Path, extra_args: &[String]) -> Result { let manifest_dir = manifest .parent() .ok_or_else(|| "spin manifest has no parent directory".to_owned())?; @@ -1095,6 +1164,10 @@ pub fn build(extra_args: &[String]) -> Result { pub fn deploy(extra_args: &[String]) -> Result<(), String> { let manifest = find_spin_manifest(env::current_dir().map_err(|err| err.to_string())?.as_path())?; + deploy_from_manifest(&manifest, extra_args) +} + +fn deploy_from_manifest(manifest: &Path, extra_args: &[String]) -> Result<(), String> { let manifest_dir = manifest .parent() .ok_or_else(|| "spin manifest has no parent directory".to_owned())?; @@ -1144,6 +1217,19 @@ fn find_spin_manifest(start: &Path) -> Result { Ok(candidates.remove(0)) } +fn target_manifest(target: &AdapterExecutionTarget) -> Result { + let manifest = target + .platform_manifest() + .map_or_else(|| target.app_root().join("spin.toml"), Path::to_path_buf); + if !manifest.is_file() { + return Err(format!( + "pinned spin manifest {} is not a regular file", + manifest.display() + )); + } + Ok(manifest) +} + fn locate_artifact( workspace_root: &Path, manifest_dir: &Path, @@ -1202,6 +1288,10 @@ fn register_ctor() { pub fn serve(extra_args: &[String]) -> Result<(), String> { let manifest = find_spin_manifest(env::current_dir().map_err(|err| err.to_string())?.as_path())?; + serve_from_manifest(&manifest, extra_args) +} + +fn serve_from_manifest(manifest: &Path, extra_args: &[String]) -> Result<(), String> { let manifest_dir = manifest .parent() .ok_or_else(|| "spin manifest has no parent directory".to_owned())?; @@ -1222,6 +1312,7 @@ pub fn serve(extra_args: &[String]) -> Result<(), String> { #[cfg(test)] mod tests { use super::*; + use std::num::NonZeroU32; use tempfile::tempdir; // Shared fixture names. Pinning these as consts (instead of @@ -1237,6 +1328,180 @@ mod tests { const TEST_SECRET_ID: &str = "default"; const TEST_COMPONENT_ID: &str = "demo"; + #[test] + #[expect( + clippy::too_many_lines, + reason = "keep the complete capability data table together" + )] + fn adapter_capability_matrix_matches_contracts() { + for (capability, support) in [ + ( + Capability::ConfigReadAllocationBounds, + CapabilitySupport::Unsupported, + ), + ( + Capability::ConfigReadDeadlines, + CapabilitySupport::BestEffort, + ), + ( + Capability::InboundReadDeadlines, + CapabilitySupport::BestEffort, + ), + (Capability::IngressAdmission, CapabilitySupport::Native), + ( + Capability::IngressAdmissionAbort, + CapabilitySupport::BestEffort, + ), + ( + Capability::RawIngressFramingValidation, + CapabilitySupport::Unsupported, + ), + ( + Capability::RawIngressHeadLimits, + CapabilitySupport::Unsupported, + ), + ( + Capability::ResponseEgressAbort, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseEgressBackpressure, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseEgressCompletion, + CapabilitySupport::BestEffort, + ), + ( + Capability::ResponseWriteDeadlines, + CapabilitySupport::BestEffort, + ), + ( + Capability::BufferedResponseContentLength, + CapabilitySupport::BestEffort, + ), + (Capability::OutboundHttp, CapabilitySupport::Native), + ( + Capability::OutboundRedirectFollowing, + CapabilitySupport::Native, + ), + ( + Capability::OutboundCompleteResourceAccounting, + CapabilitySupport::Unsupported, + ), + ( + Capability::OutboundHeaderFidelity, + CapabilitySupport::BestEffort, + ), + (Capability::OutboundDeadlines, CapabilitySupport::BestEffort), + ( + Capability::OutboundFailureDiagnostics, + CapabilitySupport::Native, + ), + ( + Capability::OutboundFlexiblePhaseBudget, + CapabilitySupport::BestEffort, + ), + ( + Capability::OutboundAuthorityOverride, + CapabilitySupport::Unsupported, + ), + ( + Capability::OutboundBatchCancellation, + CapabilitySupport::BestEffort, + ), + ( + Capability::OutboundBatchCompletionOrder, + CapabilitySupport::Native, + ), + ( + Capability::OutboundBatchSlotIsolation, + CapabilitySupport::Native, + ), + (Capability::OutboundCacheBypass, CapabilitySupport::Native), + ( + Capability::OutboundImageOptimizer, + CapabilitySupport::Unsupported, + ), + ( + Capability::StreamedUploadDeadlines, + CapabilitySupport::BestEffort, + ), + ( + Capability::LazyStreamedResponsePassthrough, + CapabilitySupport::BestEffort, + ), + ] { + assert_eq!( + SPIN_ADAPTER.capability(capability), + support, + "{capability:?}" + ); + } + } + + #[test] + fn adapter_platform_metadata_distinguishes_generic_and_hosted_profiles() { + let generic = SPIN_ADAPTER.platform_metadata(); + + assert_eq!(generic, crate::SPIN_PLATFORM); + assert_eq!( + generic.memory_ceiling(), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::RuntimeConfigured + ) + ); + assert_eq!( + generic.inbound_request_population_bound(), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::RuntimeConfigured + ) + ); + assert_eq!( + generic.host_ingress_memory_accounting(), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::RuntimeConfigured + ) + ); + + let hosted = crate::AKAMAI_FUNCTIONS_PLATFORM; + let source = edgezero_core::PlatformResourceSource::HostedDefault { + provider: "Akamai Functions", + }; + assert_eq!( + hosted.memory_ceiling(), + edgezero_core::PlatformFact::known( + edgezero_core::MemoryCeiling::new( + 128 * 1024 * 1024, + edgezero_core::MemoryCeilingScope::PerExecution, + None, + ), + source, + ) + ); + assert_eq!( + hosted.inbound_request_population_bound(), + edgezero_core::PlatformFact::known( + edgezero_core::InboundRequestPopulationBound::new(NonZeroU32::MIN), + source, + ) + ); + assert_eq!( + hosted + .inbound_request_population_bound() + .value() + .expect("the hosted profile publishes the per-execution population") + .max_live_requests(), + NonZeroU32::MIN + ); + assert_eq!( + hosted.host_ingress_memory_accounting(), + edgezero_core::PlatformFact::unknown( + edgezero_core::PlatformUnknownReason::ProviderUnpublished + ) + ); + } + #[test] fn is_valid_spin_key_accepts_lowercase_with_digits_and_underscores() { assert!(is_valid_spin_key("foo")); diff --git a/crates/edgezero-adapter-spin/src/cli/push_sqlite.rs b/crates/edgezero-adapter-spin/src/cli/push_sqlite.rs index b37b10cc7..5b3ff440e 100644 --- a/crates/edgezero-adapter-spin/src/cli/push_sqlite.rs +++ b/crates/edgezero-adapter-spin/src/cli/push_sqlite.rs @@ -19,13 +19,13 @@ //! //! 1. **Vendored schema constants** — `SPIN_KV_CREATE_TABLE` and //! `SPIN_KV_SET` are copied byte-for-byte from -//! [spinframework/spin's `crates/key-value-spin/src/store.rs`](https://github.com/spinframework/spin/blob/main/crates/key-value-spin/src/store.rs). +//! [Spin 4.1.0's `crates/key-value-spin/src/store.rs`](https://github.com/spinframework/spin/blob/v4.1.0/crates/key-value-spin/src/store.rs). //! A unit test (`vendored_schema_matches_upstream_byte_for_byte`) //! pins both strings; a PRAGMA-shape test pins the resulting table //! columns. Drift in OUR copy fails CI. -//! 2. **Build-time SDK pin** — `Cargo.toml` pins `spin-sdk = "~7.0"`, -//! so a Spin minor bump that touches the schema fails the build -//! until the operator opts in and re-verifies. +//! 2. **Build-time SDK pin** — `Cargo.toml` pins `spin-sdk = "=7.0.0"`, +//! so an SDK bump requires an explicit dependency update and +//! schema re-verification. //! 3. **Run-time CLI check** — [`verify_spin_runtime_compat`] shells //! `spin --version` before the first write of a session and //! `log::warn!`s if the major version is outside our verified @@ -34,7 +34,7 @@ //! is told if their runtime is unknown. //! //! What the layered guards CANNOT catch: a Spin point-release that -//! changes the schema WITHOUT bumping past `~7.0` AND with a +//! changes the schema WITHOUT changing the SDK pin AND with a //! same-major CLI version. Operators must verify with `spin up` //! after the first push against a new Spin runtime; the warning //! above is a heads-up, not a guarantee. @@ -51,7 +51,7 @@ use rusqlite::{Connection, params}; /// has used this schema since at least Spin 2.x; we last verified /// against Spin 3.x (CLI 3.6.3, factor-key-value 3.x on /// spinframework/spin main, 2026-06-04) and re-checked against the -/// v4.1.0 tag on 2026-09-07, where the statement is byte-identical. +/// v4.1.0 tag on 2026-09-13, where the statement is byte-identical. /// /// When a major-version bump touches the schema, expect the /// verification to be repeated and this constant updated. Until then, @@ -71,7 +71,7 @@ const VERIFIED_SPIN_MAJOR_RANGE: &[u32] = &[2, 3, 4]; /// EXACT `CREATE TABLE IF NOT EXISTS spin_key_value (…)` statement /// Spin's `crates/key-value-spin/src/store.rs::KeyValueSqlite::create_connection` /// runs. Source-of-truth: pulled from the file linked at the module -/// header on 2026-06-04 against an OPEN version of Spin's main branch. +/// header on 2026-09-13 against Spin 4.1.0. /// **Do not reformat:** the contract test below compares this string /// byte-for-byte against the upstream statement (whitespace included). pub(crate) const SPIN_KV_CREATE_TABLE: &str = "CREATE TABLE IF NOT EXISTS spin_key_value ( @@ -94,7 +94,7 @@ pub(crate) const SPIN_KV_SET: &str = /// hard-codes `.spin/sqlite_key_value.db` for its `type = "spin"` /// backend when no `path` is set in `runtime-config.toml`. Vendored /// from `crates/factor-key-value/src/runtime_config/spin.rs::path` in -/// spinframework/spin (June 2026). +/// spinframework/spin 4.1.0 (September 2026). pub(crate) const DEFAULT_SQLITE_RELATIVE_PATH: &str = ".spin/sqlite_key_value.db"; /// Resolve the `SQLite` path for a `[key_value_store.(platform: edgezero_core::PlatformMetadata) -> Result +where + A: Hooks, +{ + App::build::(platform) +} + +#[cfg(any(test, all(feature = "spin", target_arch = "wasm32")))] +fn in_outbound_callback<'future, F>(future: F) -> LocalBoxFuture<'future, F::Output> +where + F: Future + 'future, + F::Output: 'future, +{ + // Bound composed dispatch frames without retaining ambient activation between polls. + Box::pin(async move { + match OutboundScopeOwner::callback_if_absent() { + Some(owner) => owner.run(future).await, + None => future.await, + } + }) +} + /// Initialize the logger for Spin. /// -/// Currently a no-op — Spin manages its own logging internally. -/// When a real logger is needed for one target, split this into -/// `#[cfg(all(feature = "spin", target_arch = "wasm32"))]` / -/// `#[cfg(not(...))]` branches following the Fastly/Cloudflare pattern. -// TODO: wire in real Spin logger when available +/// This is a no-op. Host stdout/stderr capture does not install a Rust `log` +/// backend. Applications needing facade output must install their backend and +/// filters before the adapter entrypoint and set `Hooks::owns_logging()` to `true`. /// /// # Errors -/// Never; this is currently a no-op because Spin manages logging -/// internally. The signature still returns [`log::SetLoggerError`] so -/// the future "wire in a real logger" branch stays drop-in compatible. +/// Never; no logger is installed or reconfigured. #[inline] pub fn init_logger() -> Result<(), log::SetLoggerError> { Ok(()) @@ -94,53 +153,251 @@ pub fn init_logger() -> Result<(), log::SetLoggerError> { /// use my_core::App; /// /// #[http_service] -/// async fn handle(req: spin_sdk::http::Request) -> anyhow::Result { +/// async fn handle( +/// req: spin_sdk::http::Request, +/// ) -> anyhow::Result { /// edgezero_adapter_spin::run_app::(req).await /// } /// ``` /// -/// Returns the concrete [`SpinFullResponse`] (was `impl IntoResponse` up -/// through 2026-Q2). Source-compatible with the generated scaffold handler -/// signature because `SpinFullResponse: spin_sdk::http::IntoResponse`. +/// Returns the concrete raw [`SpinResponse`]. Its body writer and transmission-result observer +/// remain owned by the adapter's spawned response-egress coordinator. /// /// # Errors -/// Returns [`anyhow::Error`] when the inner dispatch fails — transport, -/// router, store binding, or response translation errors propagate here. +/// Returns [`anyhow::Error`] for request conversion, admission abort or failed delivery. +/// Assembly failures use their typed HTTP status and the default error response. #[cfg(all(feature = "spin", target_arch = "wasm32"))] #[inline] -pub async fn run_app(req: SpinRequest) -> anyhow::Result +pub async fn run_app(req: SpinRequest) -> anyhow::Result where A: Hooks, { - // Best-effort: every Spin `#[http_service]` re-enters this function, so a - // second `log::set_logger` call returns Err — drop the result instead of - // `.expect()` to avoid panicking on every subsequent request. Skipped - // entirely when the app owns logging. - if !A::owns_logging() { - drop(init_logger()); - } - let app = A::build_app(); - dispatch_app::(&app, req).await + run_app_with_platform::(req, SPIN_PLATFORM).await } +/// Build and dispatch a Spin application with explicit runtime platform metadata. +/// +/// Use [`AKAMAI_FUNCTIONS_PLATFORM`] for Akamai Functions. Self-hosted Spin +/// deployments can construct metadata from their configured `max_instance_memory`. +/// +/// # Errors +/// Returns [`anyhow::Error`] for request conversion, admission abort or failed delivery. +/// Assembly failures use their typed HTTP status and the default error response. #[cfg(all(feature = "spin", target_arch = "wasm32"))] -/// Dispatch a caller-owned app using `A::stores()` for fresh request bindings. +#[inline] +pub async fn run_app_with_platform( + req: SpinRequest, + platform: edgezero_core::PlatformMetadata, +) -> anyhow::Result +where + A: Hooks, +{ + in_outbound_callback(async move { + if !A::owns_logging() { + drop(init_logger()); + } + let clock = MonotonicClock::default(); + let request_start = clock.now(); + let app = match build_target_app::(platform) { + Ok(app) => app, + Err(error) => { + return Ok(request::send_assembly_error( + req, + error, + request_start, + clock, + )?); + } + }; + dispatch_app::(&app, req).await + }) + .await +} + +/// Dispatch a caller-owned app through canonical admission and response egress. /// /// This does not build or cache an app or install logging. The caller must pass -/// an app built from `A`; `App` erases its construction type, so this pairing +/// an app built from `A` with its selected platform; `App` erases its construction type, so this pairing /// remains a caller precondition. Retain only application-owned values and keep /// native handles and pending work request-local. Shared state must support /// overlapping invocations. /// /// # Errors -/// Returns conversion or dispatch errors from the existing adapter boundary. +/// Returns configuration, admission-abort, or adapter delivery errors. +#[cfg(all(feature = "spin", target_arch = "wasm32"))] #[inline] -pub async fn dispatch_app(app: &App, req: SpinRequest) -> anyhow::Result +pub async fn dispatch_app(app: &App, req: SpinRequest) -> anyhow::Result where A: Hooks, { - let stores = A::stores(); - let env = EnvConfig::from_env(); - request::dispatch_with_registries(app, req, stores.config, stores.kv, stores.secrets, &env) - .await + in_outbound_callback(async move { + let stores = A::stores(); + let env = EnvConfig::from_env(); + request::dispatch_with_registries(app, req, stores.config, stores.kv, stores.secrets, &env) + .await + }) + .await +} + +#[cfg(test)] +mod callback_scope_tests { + use std::rc::Rc; + use std::task::Poll; + + use edgezero_core::outbound::{ + IndependentOutboundProgress, OutboundScope, OutboundScopeKind, OutboundScopeOwner, + }; + use futures::executor::block_on; + use futures_util::future::{FutureExt as _, poll_fn}; + + fn capture() -> OutboundScope { + OutboundScope::capture(Rc::new(IndependentOutboundProgress)) + } + + #[test] + fn callback_return_closes_captured_scope_without_leaking_ambient_state() { + assert!(!capture().is_managed()); + let captured = block_on(super::in_outbound_callback(async { + let captured = capture(); + assert!(captured.is_managed()); + assert_eq!(captured.kind(), Some(OutboundScopeKind::Callback)); + captured.check().expect("active callback"); + captured + })); + assert!(captured.check().is_err()); + assert!(!capture().is_managed()); + } + + #[test] + fn nested_dispatch_reuses_callback_without_closing_outer_work() { + let outer = OutboundScopeOwner::callback(); + let outer_state = outer.scope().provider_state(|| 7_u8).expect("outer state"); + let captured = block_on(outer.run(super::in_outbound_callback(async { + super::in_outbound_callback(async { capture() }).await + }))); + let state = captured + .provider_state(|| 9_u8) + .expect("same callback state"); + assert!(Rc::ptr_eq(&state, &outer_state)); + captured.check().expect("outer callback remains live"); + drop(outer); + assert!(captured.check().is_err()); + } + + #[test] + fn nested_dispatch_reuses_response_body_owner_without_reparenting() { + let body = OutboundScopeOwner::response_body(); + let captured = block_on(body.run(super::in_outbound_callback(async { capture() }))); + assert_eq!(captured.kind(), Some(OutboundScopeKind::ResponseBody)); + captured.check().expect("body remains live"); + drop(body); + assert!(captured.check().is_err()); + } + + #[test] + fn pending_callback_restores_ambient_and_drop_closes_retained_work() { + let mut captured = None; + let callback = super::in_outbound_callback(poll_fn(|_cx| { + captured = Some(capture()); + Poll::<()>::Pending + })); + let mut pending = Box::pin(callback); + assert!(pending.as_mut().now_or_never().is_none()); + assert!(!capture().is_managed()); + drop(pending); + assert!(captured.expect("polled callback").check().is_err()); + } + + #[test] + fn callback_error_closes_scope_and_accepts_borrowed_future() { + let mut captured = None; + let failure = "caller-owned error"; + let result = block_on(super::in_outbound_callback(async { + captured = Some(capture()); + Err::<(), _>(&failure) + })); + assert_eq!(result, Err(&failure)); + assert!(captured.expect("callback error").check().is_err()); + assert!(!capture().is_managed()); + } +} + +#[cfg(test)] +mod platform_tests { + use edgezero_core::app::{App, Hooks}; + use edgezero_core::error::EdgeError; + use edgezero_core::router::RouterService; + + macro_rules! platform_hooks { + ($hooks:ty, $platform:expr) => { + #[expect( + clippy::missing_trait_methods, + reason = "test hook exercises only target metadata propagation" + )] + impl Hooks for $hooks { + fn configure(app: &mut App) -> Result<(), EdgeError> { + if app.platform() == $platform { + Ok(()) + } else { + Err(EdgeError::service_unavailable("wrong application platform")) + } + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + }; + } + + struct GenericPlatformConfiguration; + struct HostedPlatformConfiguration; + + platform_hooks!(GenericPlatformConfiguration, crate::SPIN_PLATFORM); + platform_hooks!( + HostedPlatformConfiguration, + crate::AKAMAI_FUNCTIONS_PLATFORM + ); + + #[test] + fn application_configuration_receives_generic_spin_platform_metadata() { + let app = super::build_target_app::(crate::SPIN_PLATFORM) + .expect("configured application"); + + assert_eq!(app.platform(), crate::SPIN_PLATFORM); + } + + #[test] + fn application_configuration_receives_explicit_hosted_platform_metadata() { + let app = super::build_target_app::( + crate::AKAMAI_FUNCTIONS_PLATFORM, + ) + .expect("configured application"); + + assert_eq!(app.platform(), crate::AKAMAI_FUNCTIONS_PLATFORM); + } + + #[test] + fn application_route_failure_preserves_target_error() { + struct RouteFailure; + #[expect( + clippy::missing_trait_methods, + reason = "tests typed route failure at target assembly" + )] + impl Hooks for RouteFailure { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Err(EdgeError::bad_request("configuration must not run")) + } + fn routes() -> Result { + Err(EdgeError::service_unavailable("route state unavailable")) + } + } + for platform in [crate::SPIN_PLATFORM, crate::AKAMAI_FUNCTIONS_PLATFORM] { + let Err(error) = super::build_target_app::(platform) else { + panic!("route failure must prevent target application assembly"); + }; + assert_eq!(error.status().as_u16(), 503); + assert_eq!(error.message(), "route state unavailable"); + } + } } diff --git a/crates/edgezero-adapter-spin/src/outbound.rs b/crates/edgezero-adapter-spin/src/outbound.rs new file mode 100644 index 000000000..9693476f9 --- /dev/null +++ b/crates/edgezero-adapter-spin/src/outbound.rs @@ -0,0 +1,3358 @@ +#![cfg_attr( + any(test, all(feature = "spin", target_arch = "wasm32")), + expect( + clippy::self_named_module_files, + reason = "retain the repository module-file convention despite contradictory restriction lints" + ) +)] +#![cfg_attr( + any(test, all(feature = "spin", target_arch = "wasm32")), + expect( + clippy::arbitrary_source_item_ordering, + reason = "target and test helper modules stay adjacent to the imports that define their boundaries" + ) +)] +#![cfg_attr( + all(feature = "spin", target_arch = "wasm32"), + expect( + clippy::pub_use, + reason = "the target-gated implementation keeps Spin SDK imports out of native builds" + ) +)] + +#[cfg(all(feature = "spin", any(feature = "test-utils", target_arch = "wasm32")))] +use std::time::Duration; + +use edgezero_core::error::EdgeError; + +#[cfg(any(test, all(feature = "spin", target_arch = "wasm32")))] +mod scheduling; +#[cfg(any(feature = "test-utils", all(feature = "spin", target_arch = "wasm32")))] +use edgezero_core::outbound::{OutboundRequest, OutboundSendMode, validate_for_dispatch}; + +#[cfg(all(feature = "spin", any(feature = "test-utils", target_arch = "wasm32")))] +use edgezero_core::error::{BadGatewayReason, BudgetSource}; +#[cfg(all(feature = "spin", any(feature = "test-utils", target_arch = "wasm32")))] +use edgezero_core::error_detail::{ + ProviderFailureDetail, ProviderFailurePhase, SpinDnsRcode, SpinFailureCause, SpinFieldSize, +}; +#[cfg(all(feature = "spin", any(feature = "test-utils", target_arch = "wasm32")))] +use edgezero_core::time::Deadline; +#[cfg(all(feature = "spin", any(feature = "test-utils", target_arch = "wasm32")))] +use spin_sdk::wasip3::http::types::{ErrorCode, FieldSizePayload}; + +#[cfg(any(test, all(feature = "spin", target_arch = "wasm32")))] +mod exchange { + use std::future::{Future, IntoFuture}; + use std::task::Poll; + use std::time::Duration; + + use async_stream::stream; + use edgezero_core::body::BodyStream; + use edgezero_core::error::EdgeError; + use edgezero_core::error_detail::{ProviderFailureDetail, ProviderFailurePhase}; + use edgezero_core::{DispatchBudget, MonotonicInstant, OutboundTime}; + use futures_util::{StreamExt as _, future::poll_fn}; + + pub(super) const READY_ITEM_YIELD_QUOTA: usize = 64; + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(super) enum UploadCompletion { + Complete, + ReaderGone, + } + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(super) struct RequestTimeouts { + between_bytes: u64, + connect: u64, + first_byte: u64, + } + + impl RequestTimeouts { + pub(super) fn between_bytes(self) -> u64 { + self.between_bytes + } + + pub(super) fn connect(self) -> u64 { + self.connect + } + + pub(super) fn first_byte(self) -> u64 { + self.first_byte + } + } + + pub(super) fn request_timeouts( + remaining: Duration, + connect: Option, + ) -> RequestTimeouts { + let full_remaining = duration_nanos(remaining); + RequestTimeouts { + between_bytes: full_remaining, + connect: duration_nanos( + connect.map_or(remaining, |configured| configured.min(remaining)), + ), + first_byte: full_remaining, + } + } + + pub(super) fn should_emit_response_chunk(length: usize) -> bool { + length != 0 + } + + pub(super) fn duration_nanos(duration: Duration) -> u64 { + u64::try_from(duration.as_nanos()) + .unwrap_or(u64::MAX) + .max(1) + } + + pub(super) async fn cooperative_yield_once() { + let mut yielded = false; + poll_fn(move |context| { + if yielded { + Poll::Ready(()) + } else { + yielded = true; + context.waker().wake_by_ref(); + Poll::Pending + } + }) + .await; + } + + pub(super) async fn cooperate_after_response_read(ready_reads: &mut usize) { + *ready_reads = ready_reads.saturating_add(1); + if *ready_reads >= READY_ITEM_YIELD_QUOTA { + *ready_reads = 0; + cooperative_yield_once().await; + } + } + + pub(super) fn cooperative_stream(mut source: BodyStream) -> BodyStream { + stream! { + let mut ready_items = 0_usize; + while let Some(item) = source.next().await { + let terminal = item.is_err(); + yield item; + if terminal { + return; + } + ready_items = ready_items.saturating_add(1); + if ready_items >= READY_ITEM_YIELD_QUOTA { + cooperative_yield_once().await; + ready_items = 0; + } + } + } + .boxed_local() + } + + #[cfg(test)] + pub(super) async fn settle_reset_content( + mut source: BodyStream, + declared_body: bool, + ) -> Result<(), EdgeError> { + if !declared_body && let Some(item) = source.next().await { + item?; + } + Ok(()) + } + + pub(super) fn checked_now( + time: &OutboundTime, + previous: &mut MonotonicInstant, + ) -> Result { + let now = time.clock().now(); + if now < *previous { + return Err(EdgeError::internal(anyhow::anyhow!( + "outbound clock moved backwards during Spin exchange" + ))); + } + *previous = now; + Ok(now) + } + + pub(super) async fn race_deadline( + future: impl Future, + budget: DispatchBudget, + time: &OutboundTime, + previous: &mut MonotonicInstant, + failure_detail: impl FnOnce(&Output) -> Option, + ) -> Result { + let timer = time.wait_until(budget.deadline); + futures_util::pin_mut!(future, timer); + let output = poll_fn(|context| { + checked_now(time, previous)?; + if let Poll::Ready(result) = timer.as_mut().poll(context) { + result?; + return Poll::Ready(Err(super::timeout_error(budget.cause))); + } + if budget.deadline.is_expired_at(checked_now(time, previous)?) { + return Poll::Ready(Err(super::timeout_error(budget.cause))); + } + match future.as_mut().poll(context) { + Poll::Ready(output) => Poll::Ready(Ok(output)), + Poll::Pending => { + if budget.deadline.is_expired_at(checked_now(time, previous)?) { + return Poll::Ready(Err(super::timeout_error(budget.cause))); + } + Poll::Pending + } + } + }) + .await?; + if budget.deadline.is_expired_at(checked_now(time, previous)?) { + let mut selected = super::timeout_error(budget.cause); + if let EdgeError::GatewayTimeout { + provider_detail, .. + } = &mut selected + { + *provider_detail = failure_detail(&output); + } + return Err(selected); + } + Ok(output) + } + + #[cfg(any(test, feature = "test-utils"))] + pub(super) fn deadline_stream( + mut source: BodyStream, + budget: DispatchBudget, + time: OutboundTime, + ) -> BodyStream { + stream! { + let mut previous = time.clock().now(); + loop { + let next_item = match race_deadline(source.next(), budget, &time, &mut previous, |item| { + item.as_ref().and_then(|result| result.as_ref().err()).and_then(EdgeError::provider_failure_detail).copied() + }).await { + Ok(item) => item, + Err(error) => { + drop(source); + drop(time); + yield Err(error); + return; + } + }; + match next_item { + Some(Ok(bytes)) => yield Ok(bytes), + Some(Err(error)) => { + drop(source); + drop(time); + yield Err(error); + return; + } + None => return, + } + } + } + .boxed_local() + } + + pub(super) async fn run_exchange( + send_future: Send, + pump_future: Pump, + request_done_reader: RequestDone, + map_error: MapError, + ) -> Result + where + Send: Future>, + Pump: Future>, + RequestDone: IntoFuture>, + MapError: Fn(&HostError, ProviderFailurePhase) -> EdgeError, + { + #[derive(Clone, Copy)] + enum State { + AwaitingRequestDone, + ReaderGone, + RequestComplete, + Uploading, + } + + enum Outcome { + PumpError(EdgeError), + RequestDoneError(HostError), + Send(Result), + } + + let mut state = State::Uploading; + let mut retained_send = None; + let mut send = Box::pin(send_future); + let mut pump = Box::pin(pump_future); + let mut request_done = Box::pin(request_done_reader.into_future()); + + let outcome = poll_fn(|context| { + loop { + match state { + State::Uploading => match pump.as_mut().poll(context) { + Poll::Ready(Err(error)) => return Poll::Ready(Outcome::PumpError(error)), + Poll::Ready(Ok(UploadCompletion::Complete)) => { + state = State::AwaitingRequestDone; + } + Poll::Ready(Ok(UploadCompletion::ReaderGone)) => { + state = State::ReaderGone; + } + Poll::Pending => match send.as_mut().poll(context) { + Poll::Ready(result) => return Poll::Ready(Outcome::Send(result)), + Poll::Pending => return Poll::Pending, + }, + }, + State::AwaitingRequestDone => match request_done.as_mut().poll(context) { + Poll::Ready(Err(error)) => { + return Poll::Ready(Outcome::RequestDoneError(error)); + } + Poll::Ready(Ok(())) => state = State::RequestComplete, + Poll::Pending => { + if retained_send.is_none() + && let Poll::Ready(result) = send.as_mut().poll(context) + { + retained_send = Some(result); + } + return Poll::Pending; + } + }, + State::ReaderGone | State::RequestComplete => { + if let Some(result) = retained_send.take() { + return Poll::Ready(Outcome::Send(result)); + } + match send.as_mut().poll(context) { + Poll::Ready(result) => return Poll::Ready(Outcome::Send(result)), + Poll::Pending => return Poll::Pending, + } + } + } + } + }) + .await; + + drop(request_done); + drop(pump); + drop(send); + match outcome { + Outcome::PumpError(error) => Err(error), + Outcome::RequestDoneError(error) => { + Err(map_error(&error, ProviderFailurePhase::RequestBodyUpload)) + } + Outcome::Send(Err(error)) => Err(map_error(&error, ProviderFailurePhase::Send)), + Outcome::Send(Ok(output)) => Ok(output), + } + } +} + +#[cfg(any( + test, + all(feature = "spin", target_arch = "wasm32", feature = "test-utils") +))] +use exchange::READY_ITEM_YIELD_QUOTA; +#[cfg(any( + test, + all(feature = "spin", target_arch = "wasm32", feature = "test-utils") +))] +use exchange::deadline_stream; +#[cfg(any(test, all(feature = "spin", target_arch = "wasm32")))] +use exchange::{ + UploadCompletion, cooperate_after_response_read, cooperative_stream, cooperative_yield_once, + race_deadline, request_timeouts, run_exchange, should_emit_response_chunk, +}; +#[cfg(test)] +use exchange::{duration_nanos, settle_reset_content}; + +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +mod spin_impl { + #[cfg(feature = "test-utils")] + mod scope_tests; + + use core::ffi::c_void; + use core::ptr::null_mut; + #[cfg(any(test, feature = "test-utils"))] + use std::cell::RefCell; + #[cfg(any(test, feature = "test-utils"))] + use std::num::NonZeroU64; + use std::rc::Rc; + use std::time::Duration; + + use async_stream::stream; + use bytes::Bytes; + use edgezero_core::body::{Body, BodyPayload, BodyStream}; + #[cfg(feature = "test-utils")] + use edgezero_core::error::BudgetSource; + use edgezero_core::error::{BadGatewayReason, EdgeError}; + use edgezero_core::error_detail::{ + ProviderFailureDetail, ProviderFailurePhase, SpinFailureCause, SpinTimeoutOption, + }; + use edgezero_core::http::header::ACCEPT_ENCODING; + use edgezero_core::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri}; + use edgezero_core::outbound::{ + IndependentOutboundProgress, OutboundBatch, OutboundBatchDriverEvent, + OutboundDispatchRequest, OutboundExchangeOwner, OutboundFuture, OutboundHead, + OutboundHeaderFidelity, OutboundHeaderInput, OutboundHttpClient, OutboundPending, + OutboundPreparedBatch, OutboundProgressRuntime, OutboundRequest, OutboundRequestParts, + OutboundResponseContext, OutboundSendMode, OutboundTime, + }; + #[cfg(any(test, feature = "test-utils"))] + use edgezero_core::time::{Deadline, MonotonicInstant}; + use edgezero_core::time::{DispatchBudget, MonotonicClock}; + use futures_util::StreamExt as _; + use futures_util::future::FutureExt as _; + use futures_util::stream::{iter, once}; + use spin_sdk::time::sleep; + use spin_sdk::wasip3::http::client; + use spin_sdk::wasip3::http::types::{ + ErrorCode, Fields, HeaderError, Method as WasiMethod, Request, RequestOptions, + RequestOptionsError, Response, Scheme, + }; + use spin_sdk::wasip3::http_compat::BodyWriter; + use spin_sdk::wasip3::spawn_local; + use spin_sdk::wasip3::wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamResult}; + use spin_sdk::wasip3::wit_future; + + #[cfg(any(test, feature = "test-utils"))] + use super::deadline_stream; + use super::{ + UploadCompletion, cooperate_after_response_read, cooperative_yield_once, + map_spin_dispatch_error, map_spin_host_error, race_deadline, run_exchange, + should_emit_response_chunk, timeout_error, + }; + + const RESPONSE_READ_BYTES: usize = 16 * 1024; + + #[expect( + unsafe_code, + reason = "query and immediately restore the pinned wit-bindgen task-pointer ABI without dereferencing it" + )] + fn ensure_p3_context() -> Result<(), EdgeError> { + unsafe extern "C" { + fn wasip3_task_set(task: *mut c_void) -> *mut c_void; + } + // SAFETY: the pinned ABI permits this getter sequence; no callback or suspension intervenes. + let prior = unsafe { wasip3_task_set(null_mut()) }; + // SAFETY: restore the identical opaque pointer immediately, without dereferencing it. + let _restored = unsafe { wasip3_task_set(prior) }; + let active = !prior.is_null(); + if active { + Ok(()) + } else { + Err(EdgeError::internal(anyhow::anyhow!( + "Spin outbound scheduling requires an active WASIp3 task" + ))) + } + } + + #[cfg(feature = "test-utils")] + use edgezero_core::time::dispatch_budget; + #[cfg(any(test, feature = "test-utils"))] + use edgezero_core::{OutboundProxyMarker, OutboundResponse, ResponseDecoding, ResponseMode}; + + pub(crate) fn native_time(clock: MonotonicClock) -> OutboundTime { + OutboundTime::new(clock, |deadline, paired_clock| { + Box::pin(async move { + ensure_p3_context()?; + if let Some(remaining) = deadline.remaining_at(paired_clock.now()) { + sleep(remaining).await; + } + Ok(()) + }) + }) + } + + /// Native outbound HTTP implementation for Spin's WASI HTTP 0.3 host. + #[derive(Clone)] + pub struct SpinOutboundClient { + time: OutboundTime, + } + + #[derive(Clone, Copy)] + enum RequestTargetPart { + Authority, + Method, + Path, + Scheme, + } + + struct PreparedRequestBody { + writer: Option, + contents: Option>, + trailers: FutureReader, ErrorCode>>, + } + + const EMPTY_REQUEST_TRAILERS: fn() -> Result, ErrorCode> = || Ok(None); + + fn prepare_request_body(method: &Method, body: &Body) -> PreparedRequestBody { + if matches!(*method, Method::GET | Method::HEAD) + && body.as_bytes().is_some_and(<[u8]>::is_empty) + { + // Publish clean trailers, not BodyWriter's error-on-drop default. + let (complete, trailers) = wit_future::new(EMPTY_REQUEST_TRAILERS); + drop(complete); + return PreparedRequestBody { + writer: None, + contents: None, + trailers, + }; + } + let (writer, contents, trailers) = BodyWriter::new(); + PreparedRequestBody { + writer: Some(writer), + contents: Some(contents), + trailers, + } + } + + #[cfg(feature = "test-utils")] + fn bodyless_request_resources_hold() -> bool { + if !matches!(EMPTY_REQUEST_TRAILERS(), Ok(None)) { + return false; + } + for method in [Method::GET, Method::HEAD] { + let body = prepare_request_body(&method, &Body::empty()); + if body.writer.is_some() || body.contents.is_some() { + drop((body.contents, body.trailers, body.writer)); + return false; + } + let (request, request_done) = + Request::new(Fields::new(), body.contents, body.trailers, None); + let (complete, completed) = wit_future::new(|| Ok(())); + let (mut contents, trailers) = Request::consume_body(request, completed); + let closed_empty = matches!( + contents.read(Vec::with_capacity(1)).now_or_never(), + Some((StreamResult::Dropped, bytes)) if bytes.is_empty() + ); + // The local wire fixture checks the non-numeric trailer/completion values + // across components; the pinned host cannot loop them back within one. + drop((contents, trailers, request_done, complete)); + if !closed_empty { + return false; + } + } + for (method, source) in [ + (Method::POST, Body::empty()), + (Method::GET, Body::from("payload")), + ] { + let PreparedRequestBody { + writer, + contents, + trailers, + } = prepare_request_body(&method, &source); + let upload_retained = writer.is_some() && contents.is_some(); + drop((contents, trailers, writer)); + if !upload_retained { + return false; + } + } + true + } + + impl SpinOutboundClient { + /// Builds a client using the process-default monotonic clock. + #[must_use] + #[inline] + pub fn new() -> Self { + Self::with_time(native_time(MonotonicClock::default())) + } + + /// Builds a client using one paired clock and deadline wakeup factory. + #[must_use] + #[inline] + pub fn with_time(time: OutboundTime) -> Self { + Self { time } + } + + async fn execute(prepared: OutboundDispatchRequest) -> Result { + ensure_p3_context()?; + let (parts, context) = prepared.into_parts(); + let time = context.time().clone(); + let budget = context.budget(); + let mut previous = context.entered_at(); + let observed = super::exchange::checked_now(&time, &mut previous)?; + if budget.deadline.is_expired_at(observed) { + return Err(timeout_error(budget.cause)); + } + let OutboundRequestParts { + body, + connect_timeout, + mut headers, + max_request_body_bytes, + method, + uri, + .. + } = parts; + + if !headers.contains_key(ACCEPT_ENCODING) { + headers.insert(ACCEPT_ENCODING, HeaderValue::from_static("identity")); + } + + let fields = request_fields(&headers)?; + let options = request_options(budget, &time, connect_timeout)?; + let PreparedRequestBody { + writer, + contents, + trailers, + } = prepare_request_body(&method, &body); + let (request, request_done) = Request::new(fields, contents, trailers, Some(options)); + set_request_target(&request, &method, &uri)?; + let exchange_time = time.clone(); + let exchange = async move { + let upload = async { + match writer { + Some(upload_writer) => { + pump_request_body( + body, + max_request_body_bytes, + upload_writer, + budget, + exchange_time.clone(), + ) + .await + } + None => Ok(UploadCompletion::Complete), + } + }; + let send = client::send(request); + run_exchange(send, upload, request_done, |error, phase| { + map_spin_dispatch_error( + error, + budget.deadline, + budget.cause, + exchange_time.clock().now(), + phase, + connect_timeout, + ) + }) + .await + }; + let response = race_deadline(exchange, budget, &time, &mut previous, |result| { + result + .as_ref() + .err() + .and_then(EdgeError::provider_failure_detail) + .copied() + }) + .await??; + + response_head(response, context) + } + } + + impl Default for SpinOutboundClient { + #[inline] + fn default() -> Self { + Self::new() + } + } + + impl OutboundHttpClient for SpinOutboundClient { + #[inline] + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + #[inline] + fn time(&self) -> OutboundTime { + self.time.clone() + } + + #[inline] + fn validate_supported_options( + &self, + request: &OutboundRequest, + mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + super::validate_request(request, mode) + } + + #[inline] + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + let scope = request.scope().clone(); + let time = request.time().clone(); + let entered_at = request.entered_at(); + scope.run(time, entered_at, Self::execute(request).boxed_local()) + } + + #[inline] + fn begin(&self, request: OutboundDispatchRequest) -> Result { + ensure_p3_context()?; + let future = self.dispatch_head(request); + super::scheduling::schedule_exchange( + async move { future.await?.finish().await }.boxed_local(), + spawn_local, + ) + } + + #[inline] + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, slots) = batch.into_slots(); + if slots.is_empty() { + return OutboundBatch::cutoff(context); + } + let completed_at = context.clock().now(); + if context.observation_cutoff().is_expired_at(completed_at) { + let mut events = Vec::new(); + let mut unresolved = false; + for (index, request) in slots { + let budget = request.budget(); + if budget + .own_deadline_expired_before(context.observation_cutoff(), completed_at) + { + events.push(OutboundBatchDriverEvent::Item { + index, + completed_at, + outcome: Err(timeout_error(budget.cause)), + }); + } else { + unresolved = true; + } + } + if unresolved { + events.push(OutboundBatchDriverEvent::Cutoff); + } + return OutboundBatch::from_driver( + context, + iter(events), + OutboundExchangeOwner::new(|| {}), + ); + } + if let Err(error) = ensure_p3_context() { + return OutboundBatch::from_driver( + context, + once(async move { edgezero_core::OutboundBatchDriverEvent::Failed(error) }), + OutboundExchangeOwner::new(|| {}), + ); + } + let exchanges = slots + .into_iter() + .map(|(index, request)| { + let budget = request.budget(); + let head = self.dispatch_head(request); + ( + index, + budget, + async move { head.await?.finish().await }.boxed_local(), + ) + }) + .collect(); + super::scheduling::schedule_prepared_batch(context, exchanges, spawn_local) + } + } + + fn request_fields(headers: &HeaderMap) -> Result { + let fields = Fields::new(); + for (name, value) in headers { + fields + .append(name.as_str(), value.as_bytes()) + .map_err(|error| map_spin_preparation_header(&error))?; + } + Ok(fields) + } + + fn request_options( + budget: DispatchBudget, + time: &OutboundTime, + connect_timeout: Option, + ) -> Result { + let options = RequestOptions::new(); + let remaining = budget_remaining(budget, time)?; + let timeouts = super::request_timeouts(remaining, connect_timeout); + apply_timeout_settings(timeouts, |option, value| match option { + SpinTimeoutOption::Connect => options.set_connect_timeout(Some(value)), + SpinTimeoutOption::FirstByte => options.set_first_byte_timeout(Some(value)), + SpinTimeoutOption::BetweenBytes => options.set_between_bytes_timeout(Some(value)), + _ => Err(RequestOptionsError::Other(None)), + })?; + Ok(options) + } + + fn apply_timeout_settings( + timeouts: super::exchange::RequestTimeouts, + mut setter: impl FnMut(SpinTimeoutOption, u64) -> Result<(), RequestOptionsError>, + ) -> Result<(), EdgeError> { + for (option, value) in [ + (SpinTimeoutOption::Connect, timeouts.connect()), + (SpinTimeoutOption::FirstByte, timeouts.first_byte()), + (SpinTimeoutOption::BetweenBytes, timeouts.between_bytes()), + ] { + set_timeout_option(option, &setter(option, value))?; + } + Ok(()) + } + + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub fn request_timeout_settings_for_test( + remaining: Duration, + configured_connect: Option, + setter: Setter, + ) -> Result<(), EdgeError> + where + Setter: FnMut(SpinTimeoutOption, u64) -> Result<(), RequestOptionsError>, + { + apply_timeout_settings( + super::request_timeouts(remaining, configured_connect), + setter, + ) + } + + fn set_timeout_option( + option: SpinTimeoutOption, + outcome: &Result<(), RequestOptionsError>, + ) -> Result<(), EdgeError> { + let name = match option { + SpinTimeoutOption::BetweenBytes => "between-bytes", + SpinTimeoutOption::Connect => "connect", + SpinTimeoutOption::FirstByte => "first-byte", + _ => "unknown", + }; + match outcome { + Ok(()) => Ok(()), + Err(RequestOptionsError::NotSupported) => { + log::debug!("Spin host does not support the outbound {name} timeout option"); + Ok(()) + } + Err(RequestOptionsError::Immutable | RequestOptionsError::Other(_)) + if option == SpinTimeoutOption::Connect => + { + log::debug!( + "Spin host rejected the outbound connect timeout option; retaining the aggregate deadline race" + ); + Ok(()) + } + Err(RequestOptionsError::Immutable) => Err(EdgeError::internal_with_detail( + anyhow::anyhow!("Spin outbound {name} timeout option is immutable"), + preparation_detail(SpinFailureCause::RequestOptionsImmutable(option)), + )), + Err(RequestOptionsError::Other(_)) => Err(EdgeError::internal_with_detail( + anyhow::anyhow!("Spin host rejected the outbound {name} timeout option"), + preparation_detail(SpinFailureCause::RequestOptionsOther(option)), + )), + } + } + + fn map_spin_preparation_header(error: &HeaderError) -> EdgeError { + let cause = match error { + HeaderError::Forbidden => SpinFailureCause::HeaderForbidden, + HeaderError::Immutable => SpinFailureCause::HeaderImmutable, + HeaderError::InvalidSyntax => SpinFailureCause::HeaderInvalidSyntax, + HeaderError::Other(_) => SpinFailureCause::HeaderOther, + HeaderError::SizeExceeded => SpinFailureCause::HeaderSizeExceeded, + }; + EdgeError::bad_request_with_detail( + "Spin rejected an outbound request header", + preparation_detail(cause), + ) + } + + fn preparation_detail(cause: SpinFailureCause) -> ProviderFailureDetail { + ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::RequestPreparation, + cause, + } + } + + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[must_use] + #[inline] + pub fn map_spin_preparation_header_for_test(error: &HeaderError) -> EdgeError { + map_spin_preparation_header(error) + } + + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub fn request_fields_for_test(headers: &HeaderMap) -> Result<(), EdgeError> { + request_fields(headers).map(|_fields| ()) + } + + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub fn set_timeout_option_for_test( + option: SpinTimeoutOption, + outcome: &Result<(), RequestOptionsError>, + ) -> Result<(), EdgeError> { + set_timeout_option(option, outcome) + } + + fn request_target_error(part: RequestTargetPart) -> EdgeError { + let (message, cause) = match part { + RequestTargetPart::Authority => ( + "Spin rejected the outbound request authority", + SpinFailureCause::RequestAuthorityRejected, + ), + RequestTargetPart::Method => ( + "Spin rejected the outbound request method", + SpinFailureCause::RequestMethodRejected, + ), + RequestTargetPart::Path => ( + "Spin rejected the outbound request path", + SpinFailureCause::RequestPathRejected, + ), + RequestTargetPart::Scheme => ( + "Spin rejected the outbound request scheme", + SpinFailureCause::RequestSchemeRejected, + ), + }; + EdgeError::bad_request_with_detail(message, preparation_detail(cause)) + } + + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[must_use] + #[inline] + pub fn request_target_errors_for_test() -> [EdgeError; 4] { + [ + RequestTargetPart::Authority, + RequestTargetPart::Method, + RequestTargetPart::Path, + RequestTargetPart::Scheme, + ] + .map(request_target_error) + } + + fn set_request_target(request: &Request, method: &Method, uri: &Uri) -> Result<(), EdgeError> { + request + .set_method(&wasi_method(method)) + .map_err(|()| request_target_error(RequestTargetPart::Method))?; + let scheme = match uri.scheme_str() { + Some("http") => Scheme::Http, + Some("https") => Scheme::Https, + Some(other) => Scheme::Other(other.to_owned()), + None => return Err(EdgeError::bad_request("outbound request URI has no scheme")), + }; + request + .set_scheme(Some(&scheme)) + .map_err(|()| request_target_error(RequestTargetPart::Scheme))?; + let authority = uri + .authority() + .ok_or_else(|| EdgeError::bad_request("outbound request URI has no authority"))?; + request + .set_authority(Some(authority.as_str())) + .map_err(|()| request_target_error(RequestTargetPart::Authority))?; + request + .set_path_with_query(Some( + uri.path_and_query() + .map_or("/", |path_and_query| path_and_query.as_str()), + )) + .map_err(|()| request_target_error(RequestTargetPart::Path))?; + Ok(()) + } + + fn wasi_method(method: &Method) -> WasiMethod { + match *method { + Method::GET => WasiMethod::Get, + Method::HEAD => WasiMethod::Head, + Method::POST => WasiMethod::Post, + Method::PUT => WasiMethod::Put, + Method::DELETE => WasiMethod::Delete, + Method::CONNECT => WasiMethod::Connect, + Method::OPTIONS => WasiMethod::Options, + Method::TRACE => WasiMethod::Trace, + Method::PATCH => WasiMethod::Patch, + _ => WasiMethod::Other(method.as_str().to_owned()), + } + } + + async fn pump_request_body( + body: Body, + maximum: u64, + writer: BodyWriter, + budget: DispatchBudget, + time: OutboundTime, + ) -> Result { + let BodyWriter { + mut stream_writer, + result_writer, + .. + } = writer; + let mut total = 0_u64; + let (payload, _scope) = body.into_parts(); + let mut source = match payload { + BodyPayload::Once(bytes) => once(async move { Ok(bytes) }).boxed_local(), + BodyPayload::Stream(source) => source, + }; + + // `BodyWriter` configures `result_writer` to publish a typed host error on drop. + // Deadline returns can therefore signal failure without awaiting after budget expiry. + while let Some(item) = source.next().await { + budget_remaining(budget, &time)?; + let bytes = match item { + Ok(bytes) => bytes, + Err(error) => { + drop(stream_writer); + signal_upload_failure(result_writer, ErrorCode::InternalError(None)).await; + return Err(error); + } + }; + let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX); + let Some(next_total) = total.checked_add(length) else { + drop(stream_writer); + signal_upload_failure(result_writer, ErrorCode::HttpRequestBodySize(None)).await; + return Err(EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + )); + }; + if next_total > maximum { + drop(stream_writer); + signal_upload_failure( + result_writer, + ErrorCode::HttpRequestBodySize(Some(next_total)), + ) + .await; + return Err(EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + )); + } + let unwritten = stream_writer.write_all(bytes.to_vec()).await; + budget_remaining(budget, &time)?; + if !unwritten.is_empty() { + drop(stream_writer); + drop(result_writer.write(Ok(None)).await); + return Ok(UploadCompletion::ReaderGone); + } + total = next_total; + cooperative_yield_once().await; + } + + drop(stream_writer); + if result_writer.write(Ok(None)).await.is_err() { + return Ok(UploadCompletion::ReaderGone); + } + Ok(UploadCompletion::Complete) + } + + async fn signal_upload_failure( + writer: FutureWriter, ErrorCode>>, + error: ErrorCode, + ) { + match writer.write(Err(error)).await { + Ok(()) | Err(_) => {} + } + } + + fn response_head( + response: Response, + context: OutboundResponseContext, + ) -> Result { + let time = context.time().clone(); + let budget = context.budget(); + budget_remaining(budget, &time)?; + let status = StatusCode::from_u16(response.get_status_code()).map_err(|_error| { + EdgeError::bad_gateway_with_reason( + "Spin returned an invalid upstream status code", + BadGatewayReason::Protocol, + ) + })?; + let headers = OutboundHeaderInput::new( + "spin", + response_headers(&response)?, + OutboundHeaderFidelity::AdapterVisibleFields, + ); + // The encoded stream alone owns the WASI reader, trailers and completion writer. + OutboundHead::new( + status, + headers, + super::cooperative_stream(response_stream(response, budget, time)), + context, + OutboundExchangeOwner::new(|| {}), + ) + } + + #[cfg(any(test, feature = "test-utils"))] + std::thread_local! { + static FIXTURE_RESPONSE: RefCell> = const { RefCell::new(None) }; + } + + #[cfg(any(test, feature = "test-utils"))] + struct FixtureClient(OutboundTime); + + #[cfg(any(test, feature = "test-utils"))] + impl OutboundHttpClient for FixtureClient { + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn time(&self) -> OutboundTime { + self.0.clone() + } + + fn validate_supported_options( + &self, + request: &OutboundRequest, + mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + super::validate_request(request, mode) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + let optional_response = FIXTURE_RESPONSE.with(|slot| slot.borrow_mut().take()); + Box::pin(async move { + let response = optional_response.ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("missing Spin fixture response")) + })?; + response_head(response, request.into_parts().1) + }) + } + + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let head = self.dispatch_head(request); + super::scheduling::schedule_exchange( + async move { head.await?.finish().await }.boxed_local(), + spawn_local, + ) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, requests) = batch.into_slots(); + if requests.is_empty() + || context + .observation_cutoff() + .is_expired_at(context.clock().now()) + { + return OutboundBatch::cutoff(context); + } + let slots = requests + .into_iter() + .map(|(index, request)| { + let budget = request.budget(); + let head = self.dispatch_head(request); + ( + index, + budget, + async move { head.await?.finish().await }.boxed_local(), + ) + }) + .collect(); + super::scheduling::schedule_prepared_batch(context, slots, spawn_local) + } + } + + #[cfg(any(test, feature = "test-utils"))] + #[expect( + clippy::too_many_arguments, + reason = "existing response fixtures retain each independent policy assertion" + )] + async fn process_response( + response: Response, + request_method: Method, + proxy_marker: OutboundProxyMarker, + response_decoding: ResponseDecoding, + response_mode: ResponseMode, + budget: DispatchBudget, + max_decoder_bytes: u64, + max_brotli_window_bits: u8, + max_chunk_bytes: Option, + max_decoded_response_bytes: Option, + max_encoded_response_bytes: Option, + max_response_header_bytes: Option, + max_response_header_count: Option, + time: OutboundTime, + ) -> Result { + use edgezero_core::{BudgetSource, HttpClient}; + + struct Reset; + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for Reset { + fn drop(&mut self) { + FIXTURE_RESPONSE.with(|slot| drop(slot.borrow_mut().take())); + } + } + + let mut parts = OutboundRequest::get("https://example.com/")?.into_parts(); + parts.method = request_method; + parts.proxy_marker = proxy_marker; + parts.response_decoding = response_decoding; + parts.response_mode = response_mode; + parts.max_decoder_bytes = max_decoder_bytes; + parts.max_brotli_window_bits = max_brotli_window_bits; + parts.max_chunk_bytes = max_chunk_bytes; + parts.max_decoded_response_bytes = max_decoded_response_bytes; + parts.max_encoded_response_bytes = max_encoded_response_bytes; + parts.max_response_header_bytes = max_response_header_bytes; + parts.max_response_header_count = max_response_header_count; + match budget.cause { + BudgetSource::PerCallTimeout => parts.timeout = Some(budget.duration), + BudgetSource::RequestDeadline => parts.deadline = Some(budget.deadline), + BudgetSource::Default => {} + BudgetSource::BatchCutoff + | BudgetSource::ConnectTimeout + | BudgetSource::IdleTimeout + | BudgetSource::Unspecified + | _ => { + return Err(EdgeError::internal(anyhow::anyhow!( + "fixture requires a genuine single-send budget" + ))); + } + } + FIXTURE_RESPONSE.with(|slot| *slot.borrow_mut() = Some(response)); + let _reset = Reset; + HttpClient::with_client(FixtureClient(time)) + .send(OutboundRequest::from_parts(parts)?) + .await + } + + #[cfg(feature = "test-utils")] + async fn check_eager_begin() -> Result<(), EdgeError> { + use edgezero_core::HttpClient; + + ensure_p3_context()?; + let (mut writer, contents, trailers) = BodyWriter::new(); + let (native, response_done) = Response::new(Fields::new(), Some(contents), trailers); + FIXTURE_RESPONSE.with(|slot| *slot.borrow_mut() = Some(native)); + let client = HttpClient::with_client(FixtureClient(native_time(MonotonicClock::default()))); + let mut pending = client + .begin(OutboundRequest::get("https://example.com/")?.stream_response()) + .await?; + sleep(Duration::from_millis(1)).await; + if !pending.is_ready() { + return Err(EdgeError::internal(anyhow::anyhow!( + "SDK begin did not progress during an unrelated wait" + ))); + } + let response = pending.collect().await?; + if writer + .stream_writer + .write_all(vec![1]) + .now_or_never() + .is_some() + { + return Err(EdgeError::internal(anyhow::anyhow!( + "begun stream consumed or dropped its payload before application polling" + ))); + } + drop(response); + if writer.stream_writer.write_all(vec![1]).now_or_never() != Some(vec![1]) { + return Err(EdgeError::internal(anyhow::anyhow!( + "collected stream failed to release its SDK reader" + ))); + } + drop((writer, response_done)); + Ok(()) + } + + /// Exercises canonical dispatch and assembly scope cleanup under a Spin HTTP host. + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub async fn canonical_scopes_hold_for_test() -> Result<(), EdgeError> { + scope_tests::canonical_entrypoints_release_retained_handles().await + } + + /// Runs in a real asynchronous WASIp3 export, unlike the synchronous SDK-resource binary. + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub async fn eager_outbound_paths_hold_for_test() -> Result<(), EdgeError> { + use std::sync::{Arc, Mutex}; + + use edgezero_core::{HttpClient, OutboundBatchTermination, OutboundCollectedSlot}; + + ensure_p3_context()?; + scope_tests::foreign_poll_cannot_reparent_prepared_dispatch().await?; + scope_tests::canonical_callback_releases_retained_handles().await?; + if !bodyless_request_resources_hold() { + return Err(EdgeError::internal(anyhow::anyhow!( + "certified empty GET/HEAD must omit WASI contents and close cleanly; other methods retain upload resources" + ))); + } + check_eager_begin().await?; + + let start = MonotonicInstant::now(); + let end = start + .checked_add(Duration::from_secs(1)) + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("fixture cutoff overflow")))?; + let observed = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&observed); + let time = native_time(MonotonicClock::new(move || { + clock_now.lock().map_or(start, |current| *current) + })); + let (mut writer, contents, trailers) = BodyWriter::new(); + let (native, response_done) = Response::new(Fields::new(), Some(contents), trailers); + native + .set_status_code(StatusCode::NO_CONTENT.as_u16()) + .map_err(|()| EdgeError::internal(anyhow::anyhow!("fixture status rejected")))?; + FIXTURE_RESPONSE.with(|slot| *slot.borrow_mut() = Some(native)); + let client = HttpClient::with_client(FixtureClient(time)); + let batch = client.start_batch_until( + vec![ + OutboundRequest::get("https://example.com/rejected")?.stream_response(), + OutboundRequest::get("https://example.com/ready")?, + ], + Deadline::at_instant(end), + ); + sleep(Duration::from_millis(1)).await; + *observed.lock().map_err(|_poisoned| { + EdgeError::internal(anyhow::anyhow!("fixture clock poisoned")) + })? = end; + let results = batch + .collect() + .await + .map_err(|failure| failure.into_parts().0)?; + if results.termination() != OutboundBatchTermination::Completed + || !matches!(results.slots(), [OutboundCollectedSlot::Resolved(rejected), OutboundCollectedSlot::Resolved(ready)] + if matches!(&rejected.outcome, Err(EdgeError::BadRequest { .. })) && ready.outcome.is_ok()) + { + return Err(EdgeError::internal(anyhow::anyhow!( + "eager batch lost an original index, preflight outcome or on-time arrival" + ))); + } + drop(results); + if writer.stream_writer.write_all(vec![1]).now_or_never() != Some(vec![1]) { + return Err(EdgeError::internal(anyhow::anyhow!( + "buffered batch retained its SDK consumer" + ))); + } + drop((writer, response_done)); + handoff_tests::check_batch_handoff().await + } + + fn response_headers(response: &Response) -> Result { + let fields = response.get_headers(); + let mut headers = HeaderMap::new(); + for (raw_name, raw_value) in fields.copy_all() { + let header_name = HeaderName::from_bytes(raw_name.as_bytes()).map_err(|_error| { + EdgeError::bad_gateway_with_reason( + "Spin returned an invalid upstream header name", + BadGatewayReason::Protocol, + ) + })?; + let header_value = HeaderValue::from_bytes(&raw_value).map_err(|_error| { + EdgeError::bad_gateway_with_reason( + "Spin returned an invalid upstream header value", + BadGatewayReason::Protocol, + ) + })?; + headers.append(header_name, header_value); + } + Ok(headers) + } + + fn default_response_result() -> Result<(), ErrorCode> { + Err(ErrorCode::InternalError(Some( + "response body consumer dropped before completion".to_owned(), + ))) + } + + fn response_stream( + response: Response, + budget: DispatchBudget, + time: OutboundTime, + ) -> BodyStream { + let (completion_writer, result_reader) = wit_future::new(default_response_result); + let (mut body_reader, trailer_reader) = Response::consume_body(response, result_reader); + stream! { + let mut ready_reads = 0_usize; + let mut previous = time.clock().now(); + loop { + let read = body_reader.read(Vec::with_capacity(RESPONSE_READ_BYTES)); + let (result, chunk) = match race_deadline(read, budget, &time, &mut previous, |_result| None).await { + Ok(value) => value, + Err(error) => { + yield Err(error); + return; + } + }; + match result { + StreamResult::Complete(length) => { + if should_emit_response_chunk(length) { + yield Ok(Bytes::from(chunk)); + } + cooperate_after_response_read(&mut ready_reads).await; + } + StreamResult::Dropped => { + let trailer_result = race_deadline( + async move { trailer_reader.await }, + budget, + &time, + &mut previous, + |trailers| trailers.as_ref().err().map(|source| super::ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::ResponseBodyRead, cause: super::spin_failure_cause(source), + }), + ) + .await; + match trailer_result { + Ok(Ok(_trailers)) => {} + Ok(Err(error)) => { + yield Err(map_spin_host_error( + &error, + budget.deadline, + budget.cause, + time.clock().now(), + ProviderFailurePhase::ResponseBodyRead, + )); + return; + } + Err(error) => { + yield Err(error); + return; + } + } + let completion = completion_writer.write(Ok(())); + match race_deadline(completion, budget, &time, &mut previous, |_result| None).await { + Ok(Ok(())) => return, + Ok(Err(_closed)) => { + yield Err(EdgeError::bad_gateway_with_reason( + "Spin response completion reader closed", + BadGatewayReason::Protocol, + )); + return; + } + Err(error) => { + yield Err(error); + return; + } + } + } + StreamResult::Cancelled => { + yield Err(EdgeError::bad_gateway_with_reason( + "Spin cancelled the upstream response body read", + BadGatewayReason::Transport, + )); + return; + } + } + } + } + .boxed_local() + } + + fn budget_remaining( + budget: DispatchBudget, + time: &OutboundTime, + ) -> Result { + budget + .deadline + .remaining_at(time.clock().now()) + .map(|remaining| remaining.min(budget.duration)) + .ok_or_else(|| timeout_error(budget.cause)) + } + + /// Exercises real timer wakeups; requires an asynchronous WASI service export. + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub async fn connect_timeout_fallback_holds_for_test() -> bool { + use std::cell::Cell; + use std::rc::Rc; + use std::sync::{Arc, Mutex}; + + use futures_util::future::pending; + + for outcome in [ + Ok(()), + Err(RequestOptionsError::NotSupported), + Err(RequestOptionsError::Immutable), + Err(RequestOptionsError::Other(None)), + Err(RequestOptionsError::Other(Some( + "provider-secret-token".to_owned(), + ))), + ] { + if set_timeout_option(SpinTimeoutOption::Connect, &outcome).is_err() { + return false; + } + for cause in [ + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::BatchCutoff, + BudgetSource::Default, + ] { + for expired in [false, true] { + let start = MonotonicInstant::now(); + let duration = Duration::from_millis(2); + let Some(end) = start.checked_add(duration) else { + return false; + }; + let budget = DispatchBudget { + cause, + deadline: Deadline::at_instant(end), + duration, + }; + let now = Arc::new(Mutex::new(if expired { end } else { start })); + let observed = Arc::clone(&now); + let time = OutboundTime::new( + MonotonicClock::new(move || { + observed.lock().map_or(start, |current| *current) + }), + move |deadline, clock| { + let advanced = Arc::clone(&now); + Box::pin(async move { + if let Some(remaining) = deadline.remaining_at(clock.now()) { + sleep(remaining).await; + } + *advanced.lock().map_err(|_poisoned| { + EdgeError::internal(anyhow::anyhow!("fixture clock poisoned")) + })? = deadline.instant(); + Ok(()) + }) + }, + ); + let owner = Rc::new(()); + let released = Rc::downgrade(&owner); + let polls = Rc::new(Cell::new(0_usize)); + let operation_polls = Rc::clone(&polls); + let operation = async move { + operation_polls.set(operation_polls.get().saturating_add(1)); + pending::<()>().await; + drop(owner); + }; + let mut previous = time.clock().now(); + let result = + race_deadline(operation, budget, &time, &mut previous, |()| None).await; + if !matches!(result, Err(EdgeError::GatewayTimeout { cause: actual, provider_detail: None, .. }) if actual == cause) + || polls.get() != usize::from(!expired) + || released.upgrade().is_some() + { + return false; + } + } + let start = MonotonicInstant::now(); + let duration = Duration::from_secs(1); + let Some(end) = start.checked_add(duration) else { + return false; + }; + let budget = DispatchBudget { + cause, + deadline: Deadline::at_instant(end), + duration, + }; + let time = native_time(MonotonicClock::new(move || start)); + let mut previous = start; + if !matches!( + race_deadline(async { 7_u8 }, budget, &time, &mut previous, |_output| None) + .await, + Ok(7) + ) { + return false; + } + } + } + true + } + + /// Runs request-option, response-clock, and fairness checks in the hosted contract binary. + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub async fn deferred_clock_paths_hold_for_test() -> bool { + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + let start = MonotonicInstant::now(); + let Ok(request) = OutboundRequest::get("https://example.com/") else { + return false; + }; + let Ok(budget) = dispatch_budget(&request.timeout(Duration::from_millis(10)), start, None) + else { + return false; + }; + let observed = start.checked_add(Duration::from_millis(3)).unwrap_or(start); + let option_observations = Arc::new(AtomicUsize::new(0)); + let observed_options = Arc::clone(&option_observations); + let option_clock = MonotonicClock::new(move || { + observed_options.fetch_add(1, Ordering::SeqCst); + observed + }); + if request_options(budget, &native_time(option_clock), None).is_err() + || option_observations.load(Ordering::SeqCst) != 1 + { + return false; + } + + let mut ready_reads = 0_usize; + for _ in 0..super::READY_ITEM_YIELD_QUOTA { + cooperate_after_response_read(&mut ready_reads).await; + } + if ready_reads != 0 { + return false; + } + + let stream_observations = Arc::new(AtomicUsize::new(0)); + let observed_stream = Arc::clone(&stream_observations); + let deadline = budget.deadline.instant(); + let stream_clock = MonotonicClock::new(move || { + if observed_stream.fetch_add(1, Ordering::SeqCst) == 0 { + start + } else { + deadline + } + }); + let source = once(async { Ok(Bytes::from_static(b"body")) }).boxed_local(); + let mut body = deadline_stream(source, budget, native_time(stream_clock)); + + matches!( + body.next().await, + Some(Err(EdgeError::GatewayTimeout { .. })) + ) && handoff_tests::check_response_handoff().await.is_ok() + } + + /// Checks ready-error diagnostic retention through paired deadline races. + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub async fn provider_deadline_diagnostics_hold_for_test() -> bool { + use std::sync::{Arc, Mutex}; + use std::task::{Poll, Waker}; + + use futures_util::future::poll_fn; + + let detail = super::ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::Send, + cause: super::SpinFailureCause::ConnectionRefused, + }; + for body in [false, true] { + let start = MonotonicInstant::now(); + let Some(end) = start.checked_add(Duration::from_secs(1)) else { + return false; + }; + let budget = DispatchBudget { + deadline: Deadline::at_instant(end), + duration: Duration::from_secs(1), + cause: edgezero_core::BudgetSource::PerCallTimeout, + }; + let observations = Arc::new(Mutex::new((start, None::))); + let observed = Arc::clone(&observations); + let timer_clock = Arc::clone(&observations); + let time = OutboundTime::new( + MonotonicClock::new(move || observed.lock().map_or(start, |current| current.0)), + move |deadline, _clock| { + let pending_clock = Arc::clone(&timer_clock); + Box::pin(poll_fn(move |context| { + let mut pending = pending_clock.lock().map_err(|_poisoned| { + EdgeError::internal(anyhow::anyhow!("fixture clock poisoned")) + })?; + if deadline.is_expired_at(pending.0) { + Poll::Ready(Ok(())) + } else { + pending.1 = Some(context.waker().clone()); + Poll::Pending + } + })) + }, + ); + let source_error = EdgeError::bad_gateway_with_detail( + "host failed", + BadGatewayReason::Unreachable, + detail, + ); + let operation = async move { + let waiting = { + let mut advanced = observations.lock().map_err(|_poisoned| { + EdgeError::internal(anyhow::anyhow!("fixture clock poisoned")) + })?; + advanced.0 = end; + advanced.1.take() + }; + if let Some(registered_waker) = waiting { + registered_waker.wake(); + } + Err::<(), _>(source_error) + }; + let error = if body { + let source = once(async move { + operation.await?; + Ok(Bytes::new()) + }) + .boxed_local(); + let Some(Err(error)) = deadline_stream(source, budget, time).next().await else { + return false; + }; + error + } else { + let mut previous = time.clock().now(); + let Err(error) = race_deadline(operation, budget, &time, &mut previous, |result| { + result + .as_ref() + .err() + .and_then(EdgeError::provider_failure_detail) + .copied() + }) + .await + else { + return false; + }; + error + }; + if error.provider_failure_detail() != Some(&detail) + || !matches!(error, EdgeError::GatewayTimeout { .. }) + { + return false; + } + } + true + } + + /// Exercises original Content-Type evidence through hosted response conversion. + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub async fn converted_response_content_type_provenance_holds_for_test() -> bool { + content_type_tests::check_converted_response_content_type_provenance() + .await + .is_ok() + } + + /// Proves response conversion retains the client clock in the hosted contract binary. + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub async fn converted_response_retains_injected_clock_for_test() -> bool { + use std::sync::{Arc, Mutex}; + + fn set_clock(clock: &Mutex, observed_at: MonotonicInstant) -> bool { + let Ok(mut current) = clock.lock() else { + return false; + }; + *current = observed_at; + true + } + + let start = MonotonicInstant::now(); + let duration = Duration::from_secs(1); + let budget = DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant(start.checked_add(duration).unwrap_or(start)), + duration, + }; + let clock_now = Arc::new(Mutex::new(start)); + let clock_observed = Arc::clone(&clock_now); + let clock = + MonotonicClock::new(move || clock_observed.lock().map_or(start, |current| *current)); + let (writer, contents, trailers) = BodyWriter::new(); + let (native, response_done) = Response::new(Fields::new(), Some(contents), trailers); + if native + .set_status_code(StatusCode::NO_CONTENT.as_u16()) + .is_err() + { + return false; + } + let Ok(response) = process_response( + native, + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + ResponseMode::Streamed, + budget, + 32 * 1024 * 1024, + 24, + None, + None, + None, + None, + None, + native_time(clock), + ) + .await + else { + return false; + }; + drop((writer, response_done)); + if !set_clock(&clock_now, budget.deadline.instant()) { + return false; + } + + matches!( + response.into_bytes_bounded_until(1, budget.deadline).await, + Err(EdgeError::GatewayTimeout { .. }) + ) + } + + #[cfg(test)] + mod clock_tests { + use std::collections::VecDeque; + use std::sync::{Arc, Mutex}; + + use edgezero_core::error::BudgetSource; + use edgezero_core::time::Deadline; + use futures_util::stream; + use spin_sdk::wasip3::wit_bindgen::block_on; + + use super::*; + + fn scripted_clock(script: Vec) -> MonotonicClock { + let last = *script.last().expect("clock observations"); + let observations = Arc::new(Mutex::new(VecDeque::from(script))); + MonotonicClock::new(move || { + observations + .lock() + .expect("clock observations") + .pop_front() + .unwrap_or(last) + }) + } + + fn test_budget(start: MonotonicInstant, duration: Duration) -> DispatchBudget { + DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant(start.checked_add(duration).expect("deadline")), + duration, + } + } + + #[test] + fn paired_constructors_do_not_sample_the_clock() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + let observations = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&observations); + let start = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || { + observed.fetch_add(1, Ordering::SeqCst); + start + }); + let _client = SpinOutboundClient::with_time(native_time(clock)); + assert_eq!(observations.load(Ordering::SeqCst), 0); + } + + #[test] + fn method_entry_and_preflight_elapsed_use_the_injected_clock() { + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(9)) + .expect("completed instant"); + let client = edgezero_core::HttpClient::with_client(SpinOutboundClient::with_time( + native_time(scripted_clock(vec![start, completed])), + )); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + + let results = block_on( + client + .start_batch_until( + vec![request], + Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("cutoff instant"), + ), + ) + .collect(), + ) + .expect("valid batch driver"); + + let edgezero_core::OutboundCollectedSlot::Resolved(slot) = &results.slots()[0] else { + panic!("resolved slot"); + }; + assert_eq!(slot.elapsed, Duration::from_millis(9)); + assert!(matches!(slot.outcome, Err(EdgeError::BadRequest { .. }))); + } + + #[test] + fn backwards_clock_fails_slot_without_invalid_elapsed() { + let start = MonotonicInstant::now(); + let earlier = start + .checked_sub(Duration::from_millis(1)) + .expect("earlier instant"); + let client = edgezero_core::HttpClient::with_client(SpinOutboundClient::with_time( + native_time(scripted_clock(vec![start, earlier])), + )); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + + let results = block_on( + client + .start_batch_until( + vec![request], + Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("cutoff instant"), + ), + ) + .collect(), + ) + .expect("valid batch driver"); + + let edgezero_core::OutboundCollectedSlot::Resolved(slot) = &results.slots()[0] else { + panic!("resolved slot"); + }; + assert_eq!(slot.elapsed, Duration::ZERO); + assert!(matches!(slot.outcome, Err(EdgeError::Internal { .. }))); + } + + #[test] + fn backwards_clock_cannot_expand_the_selected_budget() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let earlier = start + .checked_sub(Duration::from_millis(5)) + .expect("earlier instant"); + let clock = scripted_clock(vec![earlier]); + + assert_eq!( + budget_remaining(budget, &native_time(clock)).expect("remaining budget"), + budget.duration + ); + } + + #[test] + fn request_option_preparation_consumes_injected_budget() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let observed = start + .checked_add(Duration::from_millis(3)) + .expect("observed instant"); + let observations = Arc::new(Mutex::new(VecDeque::from([observed]))); + let clock_observations = Arc::clone(&observations); + let clock = MonotonicClock::new(move || { + clock_observations + .lock() + .expect("clock observations") + .pop_front() + .expect("clock observation") + }); + + let _options = + request_options(budget, &native_time(clock), None).expect("request options"); + + assert!(observations.lock().expect("clock observations").is_empty()); + } + + #[test] + fn response_conversion_checks_deadline_before_handoff() { + block_on(super::handoff_tests::check_response_handoff()).expect("handoff deadlines"); + } + + #[test] + fn response_stream_retains_clock_for_post_ready_expiry() { + let start = MonotonicInstant::now(); + let budget = test_budget(start, Duration::from_millis(10)); + let observed = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&observed); + let time = native_time(MonotonicClock::new(move || { + *clock_now.lock().expect("clock") + })); + let source = stream::once(async move { + *observed.lock().expect("clock") = budget.deadline.instant(); + Ok(Bytes::from_static(b"body")) + }) + .boxed_local(); + let mut body = deadline_stream(source, budget, time); + + let error = block_on(body.next()) + .expect("terminal item") + .expect_err("post-ready expiry"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[cfg(feature = "test-utils")] + #[test] + fn converted_response_retains_injected_clock() { + assert!(block_on( + converted_response_retains_injected_clock_for_test() + )); + } + + #[test] + fn converted_response_preserves_original_content_type_provenance() { + block_on(super::content_type_tests::check_converted_response_content_type_provenance()) + .expect("converted content-type provenance"); + } + } + + #[cfg(any(test, feature = "test-utils"))] + mod handoff_tests { + use std::sync::atomic::{AtomicUsize, Ordering}; + + use edgezero_core::error::BudgetSource; + use edgezero_core::time::DEFAULT_NO_DEADLINE_BUDGET; + + use super::*; + + #[expect( + clippy::too_many_lines, + reason = "preserve the full method, status, response-mode and deadline boundary matrix" + )] + pub(super) async fn check_response_handoff() -> Result<(), EdgeError> { + for (method, status) in [ + (Method::HEAD, StatusCode::OK), + (Method::GET, StatusCode::NOT_MODIFIED), + (Method::GET, StatusCode::NO_CONTENT), + (Method::GET, StatusCode::RESET_CONTENT), + (Method::GET, StatusCode::OK), + ] { + let modes: &[ResponseMode] = if method == Method::GET && status == StatusCode::OK { + &[ResponseMode::Streamed] + } else { + &[ + ResponseMode::Buffered { max_bytes: 1 }, + ResponseMode::Streamed, + ] + }; + for &response_mode in modes { + for cause in [ + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::Default, + ] { + for offset in [9_u64, 10, 11] { + let start = MonotonicInstant::now(); + let duration = if cause == BudgetSource::Default { + DEFAULT_NO_DEADLINE_BUDGET + } else { + Duration::from_millis(10) + }; + let deadline = start.checked_add(duration).ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("fixture deadline overflow")) + })?; + let observed = start + .checked_add( + duration + .saturating_sub(Duration::from_millis(10)) + .saturating_add(Duration::from_millis(offset)), + ) + .ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!( + "fixture observation overflow" + )) + })?; + let budget = DispatchBudget { + cause, + deadline: Deadline::at_instant(deadline), + duration, + }; + let observed_samples = AtomicUsize::new(0); + let clock = MonotonicClock::new(move || { + if observed_samples.fetch_add(1, Ordering::SeqCst) == 0 { + start + } else { + observed + } + }); + let fields = Fields::new(); + fields + .append("content-length", b"1") + .map_err(EdgeError::internal)?; + let (mut writer, contents, trailers) = BodyWriter::new(); + let (native, _response_done) = + Response::new(fields, Some(contents), trailers); + native.set_status_code(status.as_u16()).map_err(|()| { + EdgeError::internal(anyhow::anyhow!("fixture status rejected")) + })?; + let result = process_response( + native, + method.clone(), + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + response_mode, + budget, + 1024, + 24, + None, + None, + None, + None, + None, + native_time(clock), + ) + .await; + let expected = if offset < 10 { + drop(result?); + true + } else { + matches!(result, Err(EdgeError::GatewayTimeout {cause: actual, ..}) if actual == cause) + }; + if !expected { + return Err(EdgeError::internal(anyhow::anyhow!( + "expired handoff lost its selected budget" + ))); + } + if writer.stream_writer.write_all(vec![1]).now_or_never() + != Some(vec![1]) + { + return Err(EdgeError::internal(anyhow::anyhow!( + "handoff retained its native body consumer" + ))); + } + } + } + } + } + Ok(()) + } + + #[cfg(feature = "test-utils")] + pub(super) async fn check_batch_handoff() -> Result<(), EdgeError> { + use edgezero_core::{HttpClient, OutboundBatchTermination, OutboundCollectedSlot}; + + for (method, status) in [ + (Method::HEAD, StatusCode::OK), + (Method::GET, StatusCode::NOT_MODIFIED), + (Method::GET, StatusCode::NO_CONTENT), + (Method::GET, StatusCode::RESET_CONTENT), + ] { + for offset in [9_u64, 10, 11] { + let start = MonotonicInstant::now(); + let end = start + .checked_add(Duration::from_millis(10)) + .ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("fixture cutoff overflow")) + })?; + let observed = start + .checked_add(Duration::from_millis(offset)) + .ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("fixture observation overflow")) + })?; + let samples = AtomicUsize::new(0); + let clock = MonotonicClock::new(move || { + if samples.fetch_add(1, Ordering::SeqCst) == 0 { + start + } else { + observed + } + }); + let fields = Fields::new(); + fields + .append("content-length", b"1") + .map_err(EdgeError::internal)?; + let (mut writer, contents, trailers) = BodyWriter::new(); + let (native, response_done) = Response::new(fields, Some(contents), trailers); + native.set_status_code(status.as_u16()).map_err(|()| { + EdgeError::internal(anyhow::anyhow!("fixture status rejected")) + })?; + FIXTURE_RESPONSE.with(|slot| *slot.borrow_mut() = Some(native)); + let request = OutboundRequest::new( + method.clone(), + Uri::from_static("https://example.com/"), + )? + .max_response_bytes(1); + let result = HttpClient::with_client(FixtureClient(native_time(clock))) + .start_batch_until(vec![request], Deadline::at_instant(end)) + .collect() + .await; + FIXTURE_RESPONSE.with(|slot| drop(slot.borrow_mut().take())); + let results = result.map_err(|failure| failure.into_parts().0)?; + let correct = if offset < 10 { + results.termination() == OutboundBatchTermination::Completed + && matches!(results.slots(), [OutboundCollectedSlot::Resolved(slot)] if slot.outcome.is_ok()) + } else { + results.termination() == OutboundBatchTermination::Cutoff + && matches!(results.slots(), [OutboundCollectedSlot::Unresolved]) + }; + drop(results); + drop(response_done); + if !correct + || writer.stream_writer.write_all(vec![1]).now_or_never() != Some(vec![1]) + { + return Err(EdgeError::internal(anyhow::anyhow!( + "batch handoff lost cutoff state or retained its SDK consumer" + ))); + } + } + } + Ok(()) + } + } + + #[cfg(any(test, feature = "test-utils"))] + mod content_type_tests { + use edgezero_core::error::BudgetSource; + use edgezero_core::http::header::{CONNECTION, CONTENT_LENGTH, CONTENT_TYPE}; + use edgezero_core::outbound::{ContentTypeIneligibility, ContentTypeProvenance}; + use spin_sdk::wasip3::http::types::HeaderError; + + use super::*; + + type ContentTypeCase = (&'static [&'static [u8]], ContentTypeProvenance, usize); + + fn content_type_cases() -> &'static [ContentTypeCase] { + use ContentTypeIneligibility::{Missing, MultipleValues, NonText}; + use ContentTypeProvenance::{Ineligible, SingleTextUnchanged}; + + &[ + (&[], Ineligible(Missing), 0), + (&[b"text/plain"], SingleTextUnchanged, 1), + (&[b"text/plain; label=caf\xc3\xa9"], SingleTextUnchanged, 1), + ( + &[b"text/plain", b"text/plain"], + Ineligible(MultipleValues), + 2, + ), + ( + &[b"text/plain", b"application/json"], + Ineligible(MultipleValues), + 2, + ), + (&[b"text/plain", b"\xff"], Ineligible(MultipleValues), 1), + (&[b"\xff"], Ineligible(NonText), 0), + (&[b"text/plain, application/json"], SingleTextUnchanged, 1), + (&[b"not a MIME type"], SingleTextUnchanged, 1), + ] + } + + async fn convert_response( + values: &[&[u8]], + status: StatusCode, + mode: ResponseMode, + expected: ContentTypeProvenance, + retained_count: usize, + ) -> Result<(), EdgeError> { + let start = MonotonicInstant::now(); + let fields = Fields::new(); + for value in values { + fields + .append(CONTENT_TYPE.as_str(), value) + .map_err(EdgeError::internal)?; + } + if status == StatusCode::RESET_CONTENT { + fields + .append(CONTENT_LENGTH.as_str(), b"1") + .map_err(EdgeError::internal)?; + } + let (writer, contents, trailers) = BodyWriter::new(); + let (native, response_done) = Response::new(fields, Some(contents), trailers); + native + .set_status_code(status.as_u16()) + .map_err(|()| EdgeError::bad_gateway("invalid test response status"))?; + let deadline = start + .checked_add(Duration::from_secs(1)) + .ok_or_else(|| EdgeError::bad_gateway("test deadline overflow"))?; + let result = process_response( + native, + Method::GET, + OutboundProxyMarker::Inherit, + ResponseDecoding::Decode, + mode, + DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant(deadline), + duration: Duration::from_secs(1), + }, + 1024, + 24, + None, + None, + None, + None, + None, + native_time(MonotonicClock::new(move || start)), + ) + .await; + let checked = result + .and_then(|response| check_response(response, values, expected, retained_count)); + drop((writer, response_done)); + checked + } + + fn check_response( + mut response: OutboundResponse, + values: &[&[u8]], + expected: ContentTypeProvenance, + retained_count: usize, + ) -> Result<(), EdgeError> { + use ContentTypeIneligibility::Modified; + use ContentTypeProvenance::{Ineligible, SingleTextUnchanged}; + + if response.content_type_provenance() != expected + || response.headers().get_all(CONTENT_TYPE).iter().count() != retained_count + { + return Err(EdgeError::bad_gateway( + "converted test response lost original content-type provenance", + )); + } + if expected == SingleTextUnchanged + && response + .headers() + .get(CONTENT_TYPE) + .map(HeaderValue::as_bytes) + != values.first().copied() + { + return Err(EdgeError::bad_gateway( + "converted test response changed content-type bytes", + )); + } + response + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + let after_mutation = if expected == SingleTextUnchanged { + Ineligible(Modified) + } else { + expected + }; + if response.content_type_provenance() != after_mutation { + return Err(EdgeError::bad_gateway( + "test response mutation upgraded original provenance", + )); + } + Ok(()) + } + + pub(super) async fn check_converted_response_content_type_provenance() + -> Result<(), EdgeError> { + if !matches!( + Fields::new().append(CONNECTION.as_str(), b"content-type"), + Err(HeaderError::Forbidden) + ) { + return Err(EdgeError::bad_gateway( + "test host must forbid connection fields", + )); + } + for &(values, expected, retained_count) in content_type_cases() { + for (status, mode) in [ + (StatusCode::OK, ResponseMode::Streamed), + (StatusCode::NO_CONTENT, ResponseMode::Streamed), + ( + StatusCode::NO_CONTENT, + ResponseMode::Buffered { max_bytes: 64 }, + ), + (StatusCode::RESET_CONTENT, ResponseMode::Streamed), + ( + StatusCode::RESET_CONTENT, + ResponseMode::Buffered { max_bytes: 64 }, + ), + ] { + convert_response(values, status, mode, expected, retained_count).await?; + } + } + Ok(()) + } + } +} + +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +pub use spin_impl::SpinOutboundClient; +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +pub(crate) use spin_impl::native_time; +#[cfg(all(feature = "spin", target_arch = "wasm32", feature = "test-utils"))] +pub use spin_impl::{ + canonical_scopes_hold_for_test, connect_timeout_fallback_holds_for_test, + converted_response_content_type_provenance_holds_for_test, + converted_response_retains_injected_clock_for_test, deferred_clock_paths_hold_for_test, + eager_outbound_paths_hold_for_test, map_spin_preparation_header_for_test, + provider_deadline_diagnostics_hold_for_test, request_fields_for_test, + request_target_errors_for_test, request_timeout_settings_for_test, set_timeout_option_for_test, +}; + +#[cfg(all(feature = "spin", any(feature = "test-utils", target_arch = "wasm32")))] +fn map_spin_dispatch_error( + error: &ErrorCode, + deadline: Deadline, + cause: BudgetSource, + observed_at: edgezero_core::MonotonicInstant, + phase: ProviderFailurePhase, + connect_timeout: Option, +) -> EdgeError { + if connect_timeout.is_some() + && phase == ProviderFailurePhase::Send + && matches!(error, ErrorCode::ConnectionTimeout) + && !deadline.is_expired_at(observed_at) + { + return with_spin_detail(timeout_error(BudgetSource::ConnectTimeout), error, phase); + } + map_spin_host_error(error, deadline, cause, observed_at, phase) +} + +#[cfg(all(feature = "spin", any(feature = "test-utils", target_arch = "wasm32")))] +fn map_spin_host_error( + error: &ErrorCode, + deadline: Deadline, + cause: BudgetSource, + observed_at: edgezero_core::MonotonicInstant, + phase: ProviderFailurePhase, +) -> EdgeError { + if deadline.is_expired_at(observed_at) { + return with_spin_detail(timeout_error(cause), error, phase); + } + + let classified = match error { + ErrorCode::DnsTimeout + | ErrorCode::ConnectionTimeout + | ErrorCode::ConnectionReadTimeout + | ErrorCode::ConnectionWriteTimeout + | ErrorCode::HttpResponseTimeout => timeout_error(BudgetSource::Unspecified), + ErrorCode::HttpRequestDenied + | ErrorCode::HttpRequestBodySize(_) + | ErrorCode::HttpRequestUriTooLong + | ErrorCode::HttpRequestHeaderSectionSize(_) + | ErrorCode::HttpRequestHeaderSize(_) => { + EdgeError::bad_request("outbound request was rejected by the Spin HTTP host") + } + ErrorCode::HttpRequestLengthRequired + | ErrorCode::HttpRequestMethodInvalid + | ErrorCode::HttpRequestUriInvalid + | ErrorCode::HttpRequestTrailerSectionSize(_) + | ErrorCode::HttpRequestTrailerSize(_) + | ErrorCode::ConfigurationError => EdgeError::internal(anyhow::anyhow!( + "Spin rejected an adapter-owned outbound request invariant" + )), + ErrorCode::DnsError(_) + | ErrorCode::DestinationNotFound + | ErrorCode::DestinationUnavailable + | ErrorCode::DestinationIpProhibited + | ErrorCode::DestinationIpUnroutable + | ErrorCode::ConnectionRefused + | ErrorCode::ConnectionLimitReached + | ErrorCode::TlsProtocolError + | ErrorCode::TlsCertificateError + | ErrorCode::TlsAlertReceived(_) => EdgeError::bad_gateway_with_reason( + "outbound destination could not be reached", + BadGatewayReason::Unreachable, + ), + ErrorCode::ConnectionTerminated => EdgeError::bad_gateway_with_reason( + "outbound connection failed after dispatch", + BadGatewayReason::Transport, + ), + ErrorCode::HttpResponseIncomplete + | ErrorCode::HttpResponseHeaderSectionSize(_) + | ErrorCode::HttpResponseHeaderSize(_) + | ErrorCode::HttpResponseBodySize(_) + | ErrorCode::HttpResponseTrailerSectionSize(_) + | ErrorCode::HttpResponseTrailerSize(_) + | ErrorCode::HttpResponseTransferCoding(_) + | ErrorCode::HttpResponseContentCoding(_) + | ErrorCode::HttpUpgradeFailed + | ErrorCode::HttpProtocolError + | ErrorCode::LoopDetected => EdgeError::bad_gateway_with_reason( + "upstream response violated the HTTP protocol", + BadGatewayReason::Protocol, + ), + ErrorCode::InternalError(_) => EdgeError::bad_gateway_with_reason( + "Spin outbound HTTP host failed", + BadGatewayReason::Unspecified, + ), + }; + with_spin_detail(classified, error, phase) +} + +#[cfg(all(feature = "spin", any(feature = "test-utils", target_arch = "wasm32")))] +fn with_spin_detail( + mut error: EdgeError, + source: &ErrorCode, + phase: ProviderFailurePhase, +) -> EdgeError { + if let EdgeError::BadGateway { + provider_detail, .. + } + | EdgeError::BadRequest { + provider_detail, .. + } + | EdgeError::GatewayTimeout { + provider_detail, .. + } + | EdgeError::Internal { + provider_detail, .. + } = &mut error + { + *provider_detail = Some(ProviderFailureDetail::Spin { + phase, + cause: spin_failure_cause(source), + }); + } + error +} + +#[cfg(all(feature = "spin", any(feature = "test-utils", target_arch = "wasm32")))] +fn spin_failure_cause(error: &ErrorCode) -> SpinFailureCause { + let field = |payload: &FieldSizePayload| SpinFieldSize { + field_size: payload.field_size, + }; + match error { + ErrorCode::DnsTimeout => SpinFailureCause::DnsTimeout, + ErrorCode::DnsError(payload) => SpinFailureCause::DnsError { + rcode: payload + .rcode + .as_deref() + .map(SpinDnsRcode::from_provider_text), + info_code: payload.info_code, + }, + ErrorCode::DestinationNotFound => SpinFailureCause::DestinationNotFound, + ErrorCode::DestinationUnavailable => SpinFailureCause::DestinationUnavailable, + ErrorCode::DestinationIpProhibited => SpinFailureCause::DestinationIpProhibited, + ErrorCode::DestinationIpUnroutable => SpinFailureCause::DestinationIpUnroutable, + ErrorCode::ConnectionRefused => SpinFailureCause::ConnectionRefused, + ErrorCode::ConnectionTerminated => SpinFailureCause::ConnectionTerminated, + ErrorCode::ConnectionTimeout => SpinFailureCause::ConnectionTimeout, + ErrorCode::ConnectionReadTimeout => SpinFailureCause::ConnectionReadTimeout, + ErrorCode::ConnectionWriteTimeout => SpinFailureCause::ConnectionWriteTimeout, + ErrorCode::ConnectionLimitReached => SpinFailureCause::ConnectionLimitReached, + ErrorCode::TlsProtocolError => SpinFailureCause::TlsProtocolError, + ErrorCode::TlsCertificateError => SpinFailureCause::TlsCertificateError, + ErrorCode::TlsAlertReceived(payload) => SpinFailureCause::TlsAlertReceived { + alert_id: payload.alert_id, + }, + ErrorCode::HttpRequestDenied => SpinFailureCause::HttpRequestDenied, + ErrorCode::HttpRequestLengthRequired => SpinFailureCause::HttpRequestLengthRequired, + ErrorCode::HttpRequestBodySize(size) => SpinFailureCause::HttpRequestBodySize(*size), + ErrorCode::HttpRequestMethodInvalid => SpinFailureCause::HttpRequestMethodInvalid, + ErrorCode::HttpRequestUriInvalid => SpinFailureCause::HttpRequestUriInvalid, + ErrorCode::HttpRequestUriTooLong => SpinFailureCause::HttpRequestUriTooLong, + ErrorCode::HttpRequestHeaderSectionSize(size) => { + SpinFailureCause::HttpRequestHeaderSectionSize(*size) + } + ErrorCode::HttpRequestHeaderSize(payload) => { + SpinFailureCause::HttpRequestHeaderSize(payload.as_ref().map(field)) + } + ErrorCode::HttpRequestTrailerSectionSize(size) => { + SpinFailureCause::HttpRequestTrailerSectionSize(*size) + } + ErrorCode::HttpRequestTrailerSize(payload) => { + SpinFailureCause::HttpRequestTrailerSize(field(payload)) + } + ErrorCode::HttpResponseIncomplete => SpinFailureCause::HttpResponseIncomplete, + ErrorCode::HttpResponseHeaderSectionSize(size) => { + SpinFailureCause::HttpResponseHeaderSectionSize(*size) + } + ErrorCode::HttpResponseHeaderSize(payload) => { + SpinFailureCause::HttpResponseHeaderSize(field(payload)) + } + ErrorCode::HttpResponseBodySize(size) => SpinFailureCause::HttpResponseBodySize(*size), + ErrorCode::HttpResponseTrailerSectionSize(size) => { + SpinFailureCause::HttpResponseTrailerSectionSize(*size) + } + ErrorCode::HttpResponseTrailerSize(payload) => { + SpinFailureCause::HttpResponseTrailerSize(field(payload)) + } + ErrorCode::HttpResponseTransferCoding(_) => SpinFailureCause::HttpResponseTransferCoding, + ErrorCode::HttpResponseContentCoding(_) => SpinFailureCause::HttpResponseContentCoding, + ErrorCode::HttpResponseTimeout => SpinFailureCause::HttpResponseTimeout, + ErrorCode::HttpUpgradeFailed => SpinFailureCause::HttpUpgradeFailed, + ErrorCode::HttpProtocolError => SpinFailureCause::HttpProtocolError, + ErrorCode::LoopDetected => SpinFailureCause::LoopDetected, + ErrorCode::ConfigurationError => SpinFailureCause::ConfigurationError, + ErrorCode::InternalError(_) => SpinFailureCause::InternalError, + } +} + +#[cfg(any( + test, + all(feature = "spin", any(feature = "test-utils", target_arch = "wasm32")) +))] +fn timeout_error(cause: edgezero_core::BudgetSource) -> EdgeError { + EdgeError::gateway_timeout_caused("outbound request deadline expired", cause) +} + +#[cfg(feature = "test-utils")] +fn validate_batch_request(request: &OutboundRequest) -> Result<(), EdgeError> { + validate_request(request, OutboundSendMode::Batch) +} + +#[cfg(any(feature = "test-utils", all(feature = "spin", target_arch = "wasm32")))] +fn validate_request(request: &OutboundRequest, mode: OutboundSendMode) -> Result<(), EdgeError> { + if request.image_optimizer_options().is_some() { + return Err(EdgeError::bad_request( + "image_optimizer is not supported by spin", + )); + } + if request.has_host_authority_override() { + return Err(EdgeError::bad_request( + "Spin cannot separate the outbound Host authority from the connection target", + )); + } + validate_for_dispatch(request, mode) +} + +/// Runs the target-neutral Spin batch preflight contract in native tests. +/// +/// # Errors +/// Returns the same portable validation or batch-shape error as production dispatch. +#[cfg(feature = "test-utils")] +#[inline] +pub fn validate_batch_request_for_test(request: &OutboundRequest) -> Result<(), EdgeError> { + validate_batch_request(request) +} + +/// Runs the target-neutral Spin request policy before any provider dispatch. +/// +/// # Errors +/// Returns the same portable validation or unsupported-authority error as production dispatch. +#[cfg(feature = "test-utils")] +#[inline] +pub fn validate_request_for_test(request: &OutboundRequest) -> Result<(), EdgeError> { + validate_request(request, OutboundSendMode::Send) +} + +/// Exposes the real pinned SDK classifier to WASI resource contract tests. +#[cfg(all(feature = "spin", feature = "test-utils"))] +#[must_use] +#[inline] +pub fn map_spin_host_error_for_test( + error: &ErrorCode, + deadline: Deadline, + cause: BudgetSource, + observed_at: edgezero_core::MonotonicInstant, + phase: ProviderFailurePhase, +) -> EdgeError { + map_spin_host_error(error, deadline, cause, observed_at, phase) +} + +/// Exercises the dispatch-boundary classifier with the retained per-request connect setting. +#[cfg(all(feature = "spin", feature = "test-utils"))] +#[must_use] +#[inline] +pub fn map_spin_dispatch_error_for_test( + error: &ErrorCode, + deadline: Deadline, + cause: BudgetSource, + observed_at: edgezero_core::MonotonicInstant, + phase: ProviderFailurePhase, + connect_timeout: Option, +) -> EdgeError { + map_spin_dispatch_error(error, deadline, cause, observed_at, phase, connect_timeout) +} + +#[cfg(test)] +mod exchange_tests { + use std::cell::Cell; + use std::collections::VecDeque; + use std::future::Future; + use std::pin::Pin; + use std::rc::Rc; + use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll, Waker}; + use std::time::Duration; + + use edgezero_core::{ + BudgetSource, Deadline, DispatchBudget, MonotonicClock, MonotonicInstant, OutboundTime, + }; + use futures::executor::block_on; + use futures_util::future::poll_fn; + use futures_util::task::noop_waker_ref; + + use super::{ + EdgeError, READY_ITEM_YIELD_QUOTA, UploadCompletion, cooperate_after_response_read, + cooperative_stream, cooperative_yield_once, deadline_stream, duration_nanos, race_deadline, + request_timeouts, run_exchange, settle_reset_content, should_emit_response_chunk, + }; + + struct ScriptedFuture { + drops: Rc>, + polls: Rc>, + steps: VecDeque>, + } + + impl Unpin for ScriptedFuture {} + + impl ScriptedFuture { + fn new( + steps: impl IntoIterator>, + polls: Rc>, + drops: Rc>, + ) -> Self { + Self { + drops, + polls, + steps: steps.into_iter().collect(), + } + } + } + + impl Future for ScriptedFuture { + type Output = Output; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.polls.set(self.polls.get().saturating_add(1)); + let step = self.steps.pop_front().expect("scripted future exhausted"); + if step.is_pending() { + cx.waker().wake_by_ref(); + } + step + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for ScriptedFuture { + fn drop(&mut self) { + self.drops.set(self.drops.get().saturating_add(1)); + } + } + + fn counters() -> (Rc>, Rc>) { + (Rc::new(Cell::new(0)), Rc::new(Cell::new(0))) + } + + fn map_host_error( + _error: &&'static str, + _phase: edgezero_core::ProviderFailurePhase, + ) -> EdgeError { + EdgeError::bad_gateway("scripted host failure") + } + + struct TimerState { + creations: usize, + drops: usize, + now: MonotonicInstant, + waiter: Option, + } + + struct TimerRegistration(Arc>); + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for TimerRegistration { + fn drop(&mut self) { + let mut state = self.0.lock().expect("timer registration"); + state.drops = state.drops.saturating_add(1); + state.waiter = None; + } + } + + fn paired_time() -> (OutboundTime, Arc>, DispatchBudget) { + let start = MonotonicInstant::now() + .checked_add(Duration::from_hours(1)) + .expect("synthetic clock"); + let state = Arc::new(Mutex::new(TimerState { + creations: 0, + drops: 0, + now: start, + waiter: None, + })); + let observed = Arc::clone(&state); + let clock = MonotonicClock::new(move || observed.lock().expect("clock").now); + let shared = Arc::clone(&state); + let time = OutboundTime::new(clock, move |deadline, factory_clock| { + assert_eq!( + factory_clock.now(), + shared.lock().expect("paired clock").now + ); + let registration = TimerRegistration(Arc::clone(&shared)); + let mut factory_state = shared.lock().expect("timer factory"); + factory_state.creations = factory_state.creations.saturating_add(1); + drop(factory_state); + Box::pin(poll_fn(move |context| { + let mut polled = registration.0.lock().expect("timer poll"); + if deadline.is_expired_at(polled.now) { + Poll::Ready(Ok(())) + } else { + polled.waiter = Some(context.waker().clone()); + Poll::Pending + } + })) + }); + let budget = DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant( + start + .checked_add(Duration::from_millis(10)) + .expect("deadline"), + ), + duration: Duration::from_millis(10), + }; + (time, state, budget) + } + + #[test] + fn paired_timer_failure_precedes_ready_source_and_releases_it() { + let clock = MonotonicClock::default(); + let budget = DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: clock.deadline_after(Duration::from_secs(1)), + duration: Duration::from_secs(1), + }; + let time = OutboundTime::new(clock, |_, _| { + Box::pin(async { Err(EdgeError::bad_gateway("paired timer failed")) }) + }); + let (polls, drops) = counters(); + let source = ScriptedFuture::new([Poll::Ready(7_u8)], Rc::clone(&polls), Rc::clone(&drops)); + let mut previous = time.clock().now(); + let mut guarded = Box::pin(race_deadline(source, budget, &time, &mut previous, |_| { + None + })); + let error = block_on(&mut guarded).expect_err("timer setup must win"); + assert_eq!(error.message(), "paired timer failed"); + assert_eq!(polls.get(), 0); + assert_eq!(drops.get(), 1, "release before completed future is dropped"); + } + + #[test] + fn paired_expiry_wakes_and_releases_owned_pending_source_at_equality() { + for cause in [ + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::BatchCutoff, + BudgetSource::Default, + ] { + let (time, state, mut budget) = paired_time(); + budget.cause = cause; + let (polls, drops) = counters(); + let source = + ScriptedFuture::<()>::new([Poll::Pending], Rc::clone(&polls), Rc::clone(&drops)); + let mut previous = time.clock().now(); + let mut guarded = Box::pin(race_deadline(source, budget, &time, &mut previous, |()| { + None + })); + let mut context = Context::from_waker(noop_waker_ref()); + assert!(guarded.as_mut().poll(&mut context).is_pending()); + let wake = { + let mut advanced = state.lock().expect("advance clock"); + assert_eq!(advanced.creations, 1); + advanced.now = budget.deadline.instant(); + advanced.waiter.take().expect("registered timer waker") + }; + wake.wake(); + assert!( + matches!(guarded.as_mut().poll(&mut context), Poll::Ready(Err(EdgeError::GatewayTimeout { cause: actual, .. })) if actual == cause) + ); + assert_eq!(polls.get(), 1, "timer must win before repolling source"); + assert_eq!(drops.get(), 1); + let released = state.lock().expect("released timer"); + assert_eq!(released.drops, 1); + assert!(released.waiter.is_none()); + } + } + + #[test] + fn dropping_paired_guard_cancels_pending_timer_and_source() { + let (time, state, budget) = paired_time(); + let (polls, drops) = counters(); + let source = ScriptedFuture::<()>::new([Poll::Pending], polls, Rc::clone(&drops)); + let mut previous = time.clock().now(); + let mut guarded = Box::pin(race_deadline(source, budget, &time, &mut previous, |()| { + None + })); + assert!( + guarded + .as_mut() + .poll(&mut Context::from_waker(noop_waker_ref())) + .is_pending() + ); + drop(guarded); + assert_eq!(drops.get(), 1); + let released = state.lock().expect("released timer"); + assert_eq!(released.drops, 1); + assert!(released.waiter.is_none()); + } + + #[test] + fn paired_guard_rechecks_ready_source_for_expiry_and_backwards_time() { + use edgezero_core::{ + BadGatewayReason, ProviderFailureDetail, ProviderFailurePhase, SpinFailureCause, + }; + + for backwards in [false, true] { + let (time, state, budget) = paired_time(); + let detail = ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: SpinFailureCause::HttpResponseIncomplete, + }; + let observed = Arc::clone(&state); + let source = async move { + let mut source_clock = observed.lock().expect("source clock"); + source_clock.now = if backwards { + source_clock + .now + .checked_sub(Duration::from_nanos(1)) + .expect("earlier time") + } else { + budget.deadline.instant() + }; + Err::<(), _>(EdgeError::bad_gateway_with_detail( + "truncated response", + BadGatewayReason::Protocol, + detail, + )) + }; + let mut previous = time.clock().now(); + let error = block_on(race_deadline( + source, + budget, + &time, + &mut previous, + |result| { + result + .as_ref() + .err() + .and_then(EdgeError::provider_failure_detail) + .copied() + }, + )) + .expect_err("post-ready clock check"); + if backwards { + assert!(matches!(error, EdgeError::Internal { .. })); + } else { + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); + assert_eq!(error.provider_failure_detail(), Some(&detail)); + } + assert_eq!(state.lock().expect("released timer").drops, 1); + } + } + + #[test] + fn already_expired_pair_drops_source_without_registering_timer() { + let (time, state, budget) = paired_time(); + state.lock().expect("expired clock").now = budget.deadline.instant(); + let (polls, drops) = counters(); + let source = ScriptedFuture::new([Poll::Ready(())], Rc::clone(&polls), Rc::clone(&drops)); + let mut previous = time.clock().now(); + assert!(matches!( + block_on(race_deadline(source, budget, &time, &mut previous, |()| { + None + })), + Err(EdgeError::GatewayTimeout { .. }) + )); + assert_eq!(polls.get(), 0); + assert_eq!(drops.get(), 1); + assert_eq!(state.lock().expect("no timer").creations, 0); + } + + #[test] + fn review_cross_chunk_regression_is_rejected_between_races() { + let (time, state, budget) = paired_time(); + let start = state.lock().unwrap().now; + let mut previous = start; + let advanced = start.checked_add(Duration::from_millis(2)).unwrap(); + let regressed = start.checked_add(Duration::from_millis(1)).unwrap(); + let first_clock = Arc::clone(&state); + let first = async move { + first_clock.lock().unwrap().now = advanced; + 1_u8 + }; + assert_eq!( + block_on(race_deadline(first, budget, &time, &mut previous, |_| None)).unwrap(), + 1 + ); + state.lock().unwrap().now = regressed; + let error = block_on(race_deadline( + async { 2_u8 }, + budget, + &time, + &mut previous, + |_| None, + )) + .expect_err("cross-chunk clock regression"); + assert!(matches!(error, EdgeError::Internal { .. })); + } + + #[test] + fn review_cross_chunk_regression_releases_retained_source() { + use std::future::pending; + + use bytes::Bytes; + use futures_util::StreamExt as _; + + let (time, state, budget) = paired_time(); + let start = state.lock().unwrap().now; + let advanced = start.checked_add(Duration::from_millis(2)).unwrap(); + let regressed = start.checked_add(Duration::from_millis(1)).unwrap(); + let owner = Arc::new(()); + let released = Arc::downgrade(&owner); + let source_clock = Arc::clone(&state); + let source = async_stream::stream! { + source_clock.lock().unwrap().now = advanced; + yield Ok(Bytes::from_static(b"first")); + yield Ok(Bytes::from_static(b"second")); + pending::<()>().await; + drop(owner); + } + .boxed_local(); + let mut guarded = deadline_stream(source, budget, time); + assert_eq!( + block_on(guarded.next()).unwrap().unwrap(), + Bytes::from_static(b"first") + ); + state.lock().unwrap().now = regressed; + let error = block_on(guarded.next()) + .unwrap() + .expect_err("cross-chunk clock regression"); + assert!(matches!(error, EdgeError::Internal { .. })); + assert!( + released.upgrade().is_none(), + "release before terminal wrapper drop" + ); + } + + #[test] + fn provider_diagnostics_distinguish_send_and_request_completion_phases() { + for send_fails in [false, true] { + let (send_polls, send_drops) = counters(); + let (pump_polls, pump_drops) = counters(); + let (done_polls, done_drops) = counters(); + let send = ScriptedFuture::new( + [Poll::Ready(Err::("host failed"))], + send_polls, + send_drops, + ); + let pump = ScriptedFuture::new( + [if send_fails { + Poll::Pending + } else { + Poll::Ready(Ok(UploadCompletion::Complete)) + }], + pump_polls, + pump_drops, + ); + let done = + ScriptedFuture::new([Poll::Ready(Err("host failed"))], done_polls, done_drops); + let expected = if send_fails { + edgezero_core::ProviderFailurePhase::Send + } else { + edgezero_core::ProviderFailurePhase::RequestBodyUpload + }; + let outcome = block_on(run_exchange(send, pump, done, |_error, phase| { + assert_eq!(phase, expected); + EdgeError::bad_gateway("host failed") + })); + assert_eq!(outcome.expect_err("host error").message(), "host failed"); + } + } + + #[test] + fn continuously_ready_chunks_yield_after_each_item() { + use futures_util::StreamExt as _; + use futures_util::stream; + use futures_util::task::noop_waker; + + let processed = Rc::new(Cell::new(0_u32)); + let observed = Rc::clone(&processed); + let mut source = stream::iter([(), (), ()]); + let mut pump = Box::pin(async move { + while source.next().await.is_some() { + observed.set(observed.get().saturating_add(1)); + cooperative_yield_once().await; + } + }); + let waker = noop_waker(); + let mut context = Context::from_waker(&waker); + + assert!(pump.as_mut().poll(&mut context).is_pending()); + assert_eq!(processed.get(), 1); + assert!(pump.as_mut().poll(&mut context).is_pending()); + assert_eq!(processed.get(), 2); + assert!(pump.as_mut().poll(&mut context).is_pending()); + assert_eq!(processed.get(), 3); + assert!(pump.as_mut().poll(&mut context).is_ready()); + } + + #[test] + fn request_timeouts_use_full_remaining_budget_for_every_phase() { + let timeouts = request_timeouts(Duration::from_millis(40), None); + assert_eq!(timeouts.connect(), 40_000_000); + assert_eq!(timeouts.first_byte(), 40_000_000); + assert_eq!(timeouts.between_bytes(), 40_000_000); + } + + #[test] + fn configured_connect_timeout_is_independent_of_response_timeouts() { + for (configured, expected) in [(7, 7_000_000), (80, 40_000_000)] { + let timeouts = request_timeouts( + Duration::from_millis(40), + Some(Duration::from_millis(configured)), + ); + assert_eq!(timeouts.connect(), expected); + assert_eq!(timeouts.first_byte(), 40_000_000); + assert_eq!(timeouts.between_bytes(), 40_000_000); + } + } + + #[test] + fn duration_nanos_floors_and_saturates_without_wrapping() { + assert_eq!(duration_nanos(Duration::ZERO), 1); + assert_eq!(duration_nanos(Duration::from_nanos(1)), 1); + assert_eq!(duration_nanos(Duration::from_nanos(999)), 999); + assert_eq!(duration_nanos(Duration::MAX), u64::MAX); + } + + #[test] + fn cooperative_stream_stops_after_the_first_error() { + use bytes::Bytes; + use futures_util::StreamExt as _; + use futures_util::stream; + + let source = stream::iter([ + Err(EdgeError::bad_gateway("terminal")), + Ok(Bytes::from_static(b"must not escape")), + ]) + .boxed_local(); + let mut cooperative = cooperative_stream(source); + + assert!(matches!( + block_on(cooperative.next()), + Some(Err(EdgeError::BadGateway { .. })) + )); + assert!(block_on(cooperative.next()).is_none()); + } + + #[test] + fn zero_length_complete_does_not_emit_an_empty_response_chunk() { + assert!(!should_emit_response_chunk(0)); + assert!(should_emit_response_chunk(1)); + } + + #[test] + fn reset_content_settlement_is_bounded_and_releases_source() { + use bytes::Bytes; + use futures_util::StreamExt as _; + use futures_util::stream::poll_fn; + + struct DropSignal(Rc>); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + let polls = Rc::new(Cell::new(0_u32)); + let drops = Rc::new(Cell::new(0_u32)); + let observed_polls = Rc::clone(&polls); + let signal = DropSignal(Rc::clone(&drops)); + let source = poll_fn(move |_context| { + let _keep_signal_alive = &signal; + observed_polls.set(observed_polls.get().saturating_add(1)); + Poll::Ready(Some(Ok(Bytes::from_static(b"illegal")))) + }) + .boxed_local(); + + block_on(settle_reset_content(source, false)).expect("205 settlement"); + + assert_eq!(polls.get(), 1); + assert_eq!(drops.get(), 1); + + let declared_polls = Rc::new(Cell::new(0_u32)); + let declared_drops = Rc::new(Cell::new(0_u32)); + let observed_declared_polls = Rc::clone(&declared_polls); + let declared_signal = DropSignal(Rc::clone(&declared_drops)); + let declared_source = poll_fn(move |_context| { + let _keep_signal_alive = &declared_signal; + observed_declared_polls.set(observed_declared_polls.get().saturating_add(1)); + Poll::Pending::>> + }) + .boxed_local(); + + block_on(settle_reset_content(declared_source, true)).expect("declared 205 settlement"); + + assert_eq!(declared_polls.get(), 0); + assert_eq!(declared_drops.get(), 1); + + let error_drops = Rc::new(Cell::new(0_u32)); + let error_signal = DropSignal(Rc::clone(&error_drops)); + let error_source = poll_fn(move |_context| { + let _keep_signal_alive = &error_signal; + Poll::Ready(Some(Err(EdgeError::bad_gateway("205 source failed")))) + }) + .boxed_local(); + + block_on(settle_reset_content(error_source, false)).expect_err("205 source error"); + + assert_eq!(error_drops.get(), 1); + } + + #[test] + fn zero_length_response_reads_force_a_pending_boundary_at_the_quota() { + use futures_util::task::noop_waker; + + let processed = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&processed); + let mut task = Box::pin(async move { + let mut ready_reads = 0_usize; + for _ in 0..READY_ITEM_YIELD_QUOTA.saturating_add(1) { + observed.set(observed.get().saturating_add(1)); + cooperate_after_response_read(&mut ready_reads).await; + } + }); + let waker = noop_waker(); + let mut context = Context::from_waker(&waker); + + assert!(task.as_mut().poll(&mut context).is_pending()); + assert_eq!(processed.get(), READY_ITEM_YIELD_QUOTA); + assert!(task.as_mut().poll(&mut context).is_ready()); + assert_eq!(processed.get(), READY_ITEM_YIELD_QUOTA.saturating_add(1)); + } + + #[test] + fn cooperative_stream_forces_a_pending_boundary_at_its_quota() { + use std::iter::repeat_with; + + use bytes::Bytes; + use futures_util::StreamExt as _; + use futures_util::stream; + use futures_util::task::noop_waker; + + let mut source = cooperative_stream( + stream::iter( + repeat_with(|| Ok::(Bytes::new())) + .take(READY_ITEM_YIELD_QUOTA.saturating_add(1)), + ) + .boxed_local(), + ); + let waker = noop_waker(); + let mut context = Context::from_waker(&waker); + + for _ in 0..READY_ITEM_YIELD_QUOTA { + assert!(matches!( + source.as_mut().poll_next(&mut context), + Poll::Ready(Some(Ok(_))) + )); + } + assert!(source.as_mut().poll_next(&mut context).is_pending()); + assert!(matches!( + source.as_mut().poll_next(&mut context), + Poll::Ready(Some(Ok(_))) + )); + } + + #[test] + fn upload_failure_wins_over_simultaneously_ready_send() { + let (send_polls, send_drops) = counters(); + let (pump_polls, pump_drops) = counters(); + let (done_polls, done_drops) = counters(); + let send = ScriptedFuture::new( + [Poll::Ready(Ok(7_u8))], + Rc::clone(&send_polls), + Rc::clone(&send_drops), + ); + let pump = ScriptedFuture::new( + [Poll::Ready(Err(EdgeError::payload_too_large( + edgezero_core::RequestBodyOverflowOrigin::OutboundUpload, + )))], + Rc::clone(&pump_polls), + Rc::clone(&pump_drops), + ); + let done = ScriptedFuture::new( + [Poll::Ready(Ok(()))], + Rc::clone(&done_polls), + Rc::clone(&done_drops), + ); + + let outcome = block_on(run_exchange(send, pump, done, map_host_error)); + + assert!(matches!( + outcome, + Err(EdgeError::PayloadTooLarge { + origin: edgezero_core::RequestBodyOverflowOrigin::OutboundUpload + }) + )); + assert_eq!(pump_polls.get(), 1); + assert_eq!(send_polls.get(), 0); + assert_eq!(done_polls.get(), 0); + assert_eq!(send_drops.get(), 1); + assert_eq!(pump_drops.get(), 1); + assert_eq!(done_drops.get(), 1); + } + + #[test] + fn ready_send_after_pending_upload_is_authoritative() { + let (send_polls, send_drops) = counters(); + let (pump_polls, pump_drops) = counters(); + let (done_polls, done_drops) = counters(); + let send = ScriptedFuture::new( + [Poll::Ready(Ok(7_u8))], + Rc::clone(&send_polls), + Rc::clone(&send_drops), + ); + let pump = ScriptedFuture::new( + [ + Poll::Pending, + Poll::Ready(Err(EdgeError::bad_request("late upload failure"))), + ], + Rc::clone(&pump_polls), + Rc::clone(&pump_drops), + ); + let done = ScriptedFuture::new( + [Poll::Ready(Ok(()))], + Rc::clone(&done_polls), + Rc::clone(&done_drops), + ); + + let outcome = block_on(run_exchange(send, pump, done, map_host_error)); + + assert_eq!(outcome.expect("send result"), 7); + assert_eq!(pump_polls.get(), 1); + assert_eq!(send_polls.get(), 1); + assert_eq!(done_polls.get(), 0); + assert_eq!(send_drops.get(), 1); + assert_eq!(pump_drops.get(), 1); + assert_eq!(done_drops.get(), 1); + } + + #[test] + fn request_done_error_wins_over_retained_send() { + let (send_polls, send_drops) = counters(); + let (pump_polls, pump_drops) = counters(); + let (done_polls, done_drops) = counters(); + let send = ScriptedFuture::new( + [Poll::Ready(Ok(7_u8))], + Rc::clone(&send_polls), + Rc::clone(&send_drops), + ); + let pump = ScriptedFuture::new( + [Poll::Ready(Ok(UploadCompletion::Complete))], + pump_polls, + Rc::clone(&pump_drops), + ); + let done = ScriptedFuture::new( + [Poll::Pending, Poll::Ready(Err("request completion failed"))], + Rc::clone(&done_polls), + Rc::clone(&done_drops), + ); + + let observed_send_drops = Rc::clone(&send_drops); + let observed_pump_drops = Rc::clone(&pump_drops); + let observed_done_drops = Rc::clone(&done_drops); + let outcome = block_on(run_exchange(send, pump, done, move |_error, phase| { + assert_eq!( + phase, + edgezero_core::ProviderFailurePhase::RequestBodyUpload + ); + assert_eq!(observed_send_drops.get(), 1); + assert_eq!(observed_pump_drops.get(), 1); + assert_eq!(observed_done_drops.get(), 1); + EdgeError::bad_gateway("scripted host failure") + })); + + assert!(matches!(outcome, Err(EdgeError::BadGateway { .. }))); + assert_eq!(send_polls.get(), 1); + assert_eq!(done_polls.get(), 2); + } + + #[test] + fn request_done_success_releases_retained_send_without_repolling_it() { + let (send_polls, send_drops) = counters(); + let (pump_polls, pump_drops) = counters(); + let (done_polls, done_drops) = counters(); + let send = ScriptedFuture::new([Poll::Ready(Ok(7_u8))], Rc::clone(&send_polls), send_drops); + let pump = ScriptedFuture::new( + [Poll::Ready(Ok(UploadCompletion::Complete))], + pump_polls, + pump_drops, + ); + let done = ScriptedFuture::new( + [Poll::Pending, Poll::Ready(Ok(()))], + Rc::clone(&done_polls), + done_drops, + ); + + let outcome = block_on(run_exchange(send, pump, done, map_host_error)); + + assert_eq!(outcome.expect("retained send result"), 7); + assert_eq!(send_polls.get(), 1); + assert_eq!(done_polls.get(), 2); + } + + #[test] + fn reader_gone_never_polls_request_done() { + let (send_polls, send_drops) = counters(); + let (pump_polls, pump_drops) = counters(); + let (done_polls, done_drops) = counters(); + let send = ScriptedFuture::new( + [Poll::Pending, Poll::Ready(Ok(7_u8))], + Rc::clone(&send_polls), + send_drops, + ); + let pump = ScriptedFuture::new( + [Poll::Ready(Ok(UploadCompletion::ReaderGone))], + pump_polls, + pump_drops, + ); + let done = ScriptedFuture::new([Poll::Ready(Ok(()))], Rc::clone(&done_polls), done_drops); + + let outcome = block_on(run_exchange(send, pump, done, map_host_error)); + + assert_eq!(outcome.expect("early response"), 7); + assert_eq!(send_polls.get(), 2); + assert_eq!(done_polls.get(), 0); + } +} diff --git a/crates/edgezero-adapter-spin/src/outbound/scheduling.rs b/crates/edgezero-adapter-spin/src/outbound/scheduling.rs new file mode 100644 index 000000000..7e48e7ccf --- /dev/null +++ b/crates/edgezero-adapter-spin/src/outbound/scheduling.rs @@ -0,0 +1,843 @@ +use std::task::Poll; + +use edgezero_core::{ + Deadline, DispatchBudget, EdgeError, OutboundBatch, OutboundBatchContext, + OutboundBatchDriverEvent, OutboundExchangeOwner, OutboundFuture, OutboundPending, + OutboundResponse, OutboundTask, OutboundTime, +}; +use futures::channel::{mpsc, oneshot}; +use futures_util::future::{LocalBoxFuture, poll_fn}; +use futures_util::stream::{FuturesUnordered, LocalBoxStream, once}; +use futures_util::{FutureExt as _, StreamExt as _}; + +type Slot = (usize, DispatchBudget, OutboundFuture); + +fn spawn_owned( + task_future: LocalBoxFuture<'static, ()>, + spawn: impl FnOnce(LocalBoxFuture<'static, ()>), +) -> Result { + let (task, owner) = OutboundTask::new(task_future)?; + spawn(task.boxed_local()); + Ok(owner) +} + +pub(super) fn schedule_exchange( + future: OutboundFuture, + spawn: impl FnOnce(LocalBoxFuture<'static, ()>), +) -> Result { + let (sender, receiver) = oneshot::channel(); + let task = async move { + drop(sender.send(future.await)); + } + .boxed_local(); + let owner = spawn_owned(task, spawn)?; + Ok(OutboundPending::new( + Box::pin(async move { + receiver.await.map_err(|_closed| { + EdgeError::internal(anyhow::anyhow!( + "Spin outbound exchange ended without a completion" + )) + })? + }), + owner, + )) +} + +pub(super) fn schedule_batch( + slots: Vec, + time: OutboundTime, + cutoff: Deadline, + spawn: impl FnOnce(LocalBoxFuture<'static, ()>), +) -> Result< + ( + LocalBoxStream<'static, OutboundBatchDriverEvent>, + OutboundExchangeOwner, + ), + EdgeError, +> { + // Each finite input emits at most one observation, plus one terminal event. + let (sender, receiver) = mpsc::unbounded(); + let task = async move { + let mut exchanges = slots + .into_iter() + .map(|(index, budget, future)| { + let clock = time.clock().clone(); + async move { + let outcome = future.await; + (index, budget, clock.now(), outcome) + } + }) + .collect::>(); + let mut timer = time.wait_until(cutoff); + let mut timer_done = false; + let mut late = false; + loop { + let observation = poll_fn(|cx| { + if !timer_done && let Poll::Ready(result) = timer.as_mut().poll(cx) { + timer_done = true; + if let Err(error) = result { + return Poll::Ready(Err(error)); + } + } + let expired = cutoff.is_expired_at(time.clock().now()); + match exchanges.poll_next_unpin(cx) { + Poll::Ready(item) => Poll::Ready(Ok(item)), + Poll::Pending if expired => Poll::Ready(Ok(None)), + Poll::Pending => Poll::Pending, + } + }) + .await; + match observation { + Err(error) => { + drop(sender.unbounded_send(OutboundBatchDriverEvent::Failed(error))); + return; + } + Ok(Some((index, budget, completed_at, outcome))) => { + let own_timeout = budget.own_deadline_expired_before(cutoff, completed_at); + late |= cutoff.is_expired_at(completed_at) && !own_timeout; + if sender + .unbounded_send(OutboundBatchDriverEvent::Item { + index, + completed_at, + outcome, + }) + .is_err() + { + return; + } + } + Ok(None) => { + if late || !exchanges.is_empty() { + drop(sender.unbounded_send(OutboundBatchDriverEvent::Cutoff)); + } + return; + } + } + } + } + .boxed_local(); + let owner = spawn_owned(task, spawn)?; + Ok((receiver.boxed_local(), owner)) +} + +pub(super) fn schedule_prepared_batch( + context: OutboundBatchContext, + slots: Vec, + spawn: impl FnOnce(LocalBoxFuture<'static, ()>), +) -> OutboundBatch { + match schedule_batch( + slots, + context.time().clone(), + context.observation_cutoff(), + spawn, + ) { + Ok((events, owner)) => OutboundBatch::from_driver(context, events, owner), + Err(error) => OutboundBatch::from_driver( + context, + once(async move { OutboundBatchDriverEvent::Failed(error) }), + OutboundExchangeOwner::new(|| {}), + ), + } +} + +#[cfg(test)] +mod tests { + use core::ptr::null; + use std::cell::{Cell, RefCell}; + use std::rc::Rc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll, RawWaker, RawWakerVTable, Wake, Waker}; + use std::time::Duration; + + use bytes::Bytes; + use edgezero_core::http::{HeaderMap, Method, StatusCode}; + use edgezero_core::outbound::validate_for_dispatch; + use edgezero_core::outbound::{IndependentOutboundProgress, OutboundProgressRuntime}; + use edgezero_core::{ + Body, BudgetSource, ContentTypeProvenance, Deadline, DispatchBudget, EdgeError, HttpClient, + MonotonicClock, MonotonicInstant, OutboundBatch, OutboundBatchDriverEvent, + OutboundBatchTermination, OutboundCollectedSlot, OutboundDispatchRequest, + OutboundExchangeOwner, OutboundFuture, OutboundHead, OutboundHttpClient, OutboundPending, + OutboundPreparedBatch, OutboundRequest, OutboundResponse, OutboundScopeOwner, + OutboundSendMode, OutboundTime, + }; + use futures::executor::{LocalPool, block_on}; + use futures::task::LocalSpawnExt as _; + use futures_util::future::{LocalBoxFuture, pending, poll_fn}; + use futures_util::stream::once; + use futures_util::{FutureExt as _, StreamExt as _}; + + use super::{schedule_batch, schedule_exchange, schedule_prepared_batch, spawn_owned}; + + #[derive(Default)] + struct WakerHooks { + clone: Option>, + drop: Option>, + wake: Option>, + } + + #[derive(Clone, Copy)] + enum Hook { + Clone, + Drop, + Wake, + } + + std::thread_local! { + static WAKER_HOOKS: RefCell = const { RefCell::new(WakerHooks { clone: None, drop: None, wake: None }) }; + } + + fn run_hook(hook: Hook) { + let pending_hook = WAKER_HOOKS.with(|slot| { + let mut hooks = slot.borrow_mut(); + match hook { + Hook::Clone => hooks.clone.take(), + Hook::Drop => hooks.drop.take(), + Hook::Wake => hooks.wake.take(), + } + }); + if let Some(callback) = pending_hook { + callback(); + } + } + + fn clone_waker(data: *const ()) -> RawWaker { + run_hook(Hook::Clone); + RawWaker::new(data, &WAKER_VTABLE) + } + + fn drop_waker(_data: *const ()) { + run_hook(Hook::Drop); + } + fn wake_waker(_data: *const ()) { + run_hook(Hook::Wake); + } + + const WAKER_VTABLE: RawWakerVTable = + RawWakerVTable::new(clone_waker, wake_waker, wake_waker, drop_waker); + + #[expect( + unsafe_code, + reason = "exercise reentrant clone, drop and wake callbacks with a data-free static test waker" + )] + fn controlled_waker() -> Waker { + // SAFETY: every callback ignores the null data; the static vtable owns no allocation. + // Hooks use only the calling thread's TLS, so moving the waker across threads is safe. + unsafe { Waker::from_raw(RawWaker::new(null(), &WAKER_VTABLE)) } + } + + #[derive(Default)] + struct WakeCount(AtomicUsize); + + #[expect( + clippy::missing_trait_methods, + reason = "the default wake_by_ref delegates to wake" + )] + impl Wake for WakeCount { + fn wake(self: Arc) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + fn stalled_work( + releases: &Rc>, + polls: &Rc>, + ) -> LocalBoxFuture<'static, ()> { + let release = Release(Rc::clone(releases)); + let observed = Rc::clone(polls); + async move { + let _release = release; + poll_fn(move |_cx| { + observed.set(observed.get().saturating_add(1)); + Poll::<()>::Pending + }) + .await; + } + .boxed_local() + } + + struct Release(Rc>); + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for Release { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + fn response(body: Body) -> OutboundResponse { + OutboundResponse::new( + Method::GET, + StatusCode::OK, + HeaderMap::new(), + body, + MonotonicClock::default(), + ContentTypeProvenance::Unknown, + ) + } + + #[test] + fn begin_progresses_without_collection() { + let mut executor = LocalPool::new(); + let spawner = executor.spawner(); + let calls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&calls); + let mut pending = schedule_exchange( + async move { + observed.set(observed.get().saturating_add(1)); + Ok(response(Body::from("ready"))) + } + .boxed_local(), + move |task| spawner.spawn_local(task).expect("schedule exchange"), + ) + .expect("schedule exchange"); + executor.run_until_stalled(); + assert_eq!(calls.get(), 1); + assert!(pending.is_ready()); + assert!(pending.is_ready()); + let response = executor.run_until(pending.collect()).expect("response"); + assert_eq!(response.status(), StatusCode::OK); + } + + #[test] + fn refused_exchange_registration_releases_source_without_spawning() { + let owner = OutboundScopeOwner::callback(); + owner.close(); + let releases = Rc::new(Cell::new(0_usize)); + let polls = Rc::new(Cell::new(0_usize)); + let release = Release(Rc::clone(&releases)); + let observed = Rc::clone(&polls); + let spawns = Cell::new(0_usize); + let outcome = owner.with(|| { + schedule_exchange( + async move { + let _retained = release; + observed.set(observed.get().saturating_add(1)); + pending().await + } + .boxed_local(), + |_task| spawns.set(spawns.get().saturating_add(1)), + ) + }); + assert!(matches!(outcome, Err(EdgeError::Internal { .. }))); + assert_eq!(releases.get(), 1); + assert_eq!(polls.get(), 0); + assert_eq!(spawns.get(), 0); + } + + #[test] + fn refused_batch_registration_releases_sources_without_spawning() { + let owner = OutboundScopeOwner::callback(); + owner.close(); + let releases = Rc::new(Cell::new(0_usize)); + let release = Release(Rc::clone(&releases)); + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let spawns = Cell::new(0_usize); + let now = MonotonicInstant::now(); + let deadline = Deadline::at_instant(now + Duration::from_secs(1)); + let time = OutboundTime::new(MonotonicClock::new(move || now), |_, _| { + panic!("refused batch must not register a timer") + }); + let budget = DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline, + duration: Duration::from_secs(1), + }; + let work = async move { + let _retained = release; + observed.set(observed.get().saturating_add(1)); + pending().await + } + .boxed_local(); + let outcome = owner.with(|| { + schedule_batch(vec![(0, budget, work)], time, deadline, |_task| { + spawns.set(spawns.get().saturating_add(1)); + }) + }); + assert!(matches!(outcome, Err(EdgeError::Internal { .. }))); + assert_eq!(releases.get(), 1); + assert_eq!(polls.get(), 0); + assert_eq!(spawns.get(), 0); + } + + #[test] + fn cancelling_unpolled_exchange_releases_without_running_it() { + let mut executor = LocalPool::new(); + let spawner = executor.spawner(); + let releases = Rc::new(Cell::new(0_usize)); + let owner = Release(Rc::clone(&releases)); + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let pending = schedule_exchange( + async move { + let _owner = owner; + observed.set(observed.get().saturating_add(1)); + pending().await + } + .boxed_local(), + move |task| spawner.spawn_local(task).expect("schedule exchange"), + ) + .expect("schedule exchange"); + pending.cancel(); + assert_eq!(releases.get(), 1); + executor.run_until_stalled(); + assert_eq!(polls.get(), 0); + assert_eq!(releases.get(), 1); + } + + #[test] + fn cancelling_pending_exchange_releases_its_source_once() { + let mut executor = LocalPool::new(); + let spawner = executor.spawner(); + let releases = Rc::new(Cell::new(0_usize)); + let owner = Release(Rc::clone(&releases)); + let pending = schedule_exchange( + async move { + let _owner = owner; + pending().await + } + .boxed_local(), + move |task| spawner.spawn_local(task).expect("schedule exchange"), + ) + .expect("schedule exchange"); + executor.run_until_stalled(); + assert_eq!(releases.get(), 0); + drop(pending); + assert_eq!(releases.get(), 1); + executor.run_until_stalled(); + assert_eq!(releases.get(), 1); + } + + #[test] + fn cancellation_during_poll_releases_the_active_future_once() { + let releases = Rc::new(Cell::new(0_usize)); + let release = Release(Rc::clone(&releases)); + let cancellation = Rc::new(RefCell::new(None::)); + let cancel_from_poll = Rc::clone(&cancellation); + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let mut scheduled = None; + let owner = spawn_owned( + async move { + let _release = release; + poll_fn(move |_cx| { + observed.set(observed.get().saturating_add(1)); + drop(cancel_from_poll.borrow_mut().take()); + Poll::<()>::Pending + }) + .await; + } + .boxed_local(), + |task| scheduled = Some(task), + ) + .expect("schedule owned task"); + *cancellation.borrow_mut() = Some(owner); + let mut task = scheduled.expect("scheduled task"); + assert!( + task.as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_ready() + ); + assert_eq!(polls.get(), 1); + assert_eq!(releases.get(), 1); + drop(task); + assert_eq!(releases.get(), 1); + } + + #[test] + fn waker_clone_can_cancel_before_the_source_is_polled() { + let releases = Rc::new(Cell::new(0_usize)); + let polls = Rc::new(Cell::new(0_usize)); + let mut scheduled = None; + let owner = spawn_owned(stalled_work(&releases, &polls), |task| { + scheduled = Some(task); + }) + .expect("schedule owned task"); + WAKER_HOOKS.with(|hooks| hooks.borrow_mut().clone = Some(Box::new(move || drop(owner)))); + let mut task = scheduled.expect("scheduled task"); + let waker = controlled_waker(); + assert!( + task.as_mut() + .poll(&mut Context::from_waker(&waker)) + .is_ready() + ); + assert_eq!(polls.get(), 0); + assert_eq!(releases.get(), 1); + drop((task, waker)); + assert_eq!(releases.get(), 1); + } + + #[test] + fn replacing_a_waker_allows_reentrant_drop_cancellation() { + let releases = Rc::new(Cell::new(0_usize)); + let polls = Rc::new(Cell::new(0_usize)); + let mut scheduled = None; + let owner = spawn_owned(stalled_work(&releases, &polls), |task| { + scheduled = Some(task); + }) + .expect("schedule owned task"); + let mut task = scheduled.expect("scheduled task"); + let first = controlled_waker(); + assert!( + task.as_mut() + .poll(&mut Context::from_waker(&first)) + .is_pending() + ); + WAKER_HOOKS.with(|hooks| hooks.borrow_mut().drop = Some(Box::new(move || drop(owner)))); + let wakes = Arc::new(WakeCount::default()); + let replacement = Waker::from(Arc::clone(&wakes)); + assert!( + task.as_mut() + .poll(&mut Context::from_waker(&replacement)) + .is_ready() + ); + assert_eq!( + polls.get(), + 1, + "waker-drop cancellation must prevent another work poll" + ); + assert_eq!(releases.get(), 1); + assert_eq!(wakes.0.load(Ordering::SeqCst), 1); + assert!( + task.as_mut() + .poll(&mut Context::from_waker(&replacement)) + .is_ready() + ); + assert_eq!(polls.get(), 1); + drop((task, first, replacement)); + assert_eq!(releases.get(), 1); + } + + #[test] + fn cancellation_wakeup_can_reentrantly_cancel_a_sibling() { + let releases = Rc::new(Cell::new(0_usize)); + let polls = Rc::new(Cell::new(0_usize)); + let mut first_task = None; + let first_owner = spawn_owned(stalled_work(&releases, &polls), |task| { + first_task = Some(task); + }) + .expect("schedule owned task"); + let mut sibling_task = None; + let sibling_owner = spawn_owned(stalled_work(&releases, &polls), |task| { + sibling_task = Some(task); + }) + .expect("schedule owned task"); + let mut first = first_task.expect("first task"); + let mut sibling = sibling_task.expect("sibling task"); + let waker = controlled_waker(); + assert!( + first + .as_mut() + .poll(&mut Context::from_waker(&waker)) + .is_pending() + ); + assert!( + sibling + .as_mut() + .poll(&mut Context::from_waker(&waker)) + .is_pending() + ); + WAKER_HOOKS + .with(|hooks| hooks.borrow_mut().wake = Some(Box::new(move || drop(sibling_owner)))); + drop(first_owner); + assert_eq!(releases.get(), 2); + assert_eq!(polls.get(), 2); + assert!( + first + .as_mut() + .poll(&mut Context::from_waker(&waker)) + .is_ready() + ); + assert!( + sibling + .as_mut() + .poll(&mut Context::from_waker(&waker)) + .is_ready() + ); + drop((first, sibling, waker)); + assert_eq!(releases.get(), 2); + } + + #[test] + fn executor_exit_revokes_a_retained_pending_exchange() { + let mut executor = LocalPool::new(); + let spawner = executor.spawner(); + let releases = Rc::new(Cell::new(0_usize)); + let owner = Release(Rc::clone(&releases)); + let mut pending = schedule_exchange( + async move { + let _owner = owner; + pending().await + } + .boxed_local(), + move |task| spawner.spawn_local(task).expect("schedule exchange"), + ) + .expect("schedule exchange"); + executor.run_until_stalled(); + drop(executor); + assert_eq!(releases.get(), 1); + assert!(pending.is_ready()); + let failure = block_on(pending.collect()).expect_err("executor exited"); + assert!(matches!(failure, edgezero_core::EdgeError::Internal { .. })); + assert_eq!(releases.get(), 1); + } + + #[test] + fn collected_stream_retains_its_owner_without_reading_payload() { + let mut executor = LocalPool::new(); + let spawner = executor.spawner(); + let releases = Rc::new(Cell::new(0_usize)); + let owner = Release(Rc::clone(&releases)); + let reads = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&reads); + let body = Body::from_stream(once(async move { + let _owner = owner; + observed.set(observed.get().saturating_add(1)); + Ok(Bytes::from_static(b"body")) + })); + let pending = schedule_exchange( + async move { Ok(response(body)) }.boxed_local(), + move |task| spawner.spawn_local(task).expect("schedule exchange"), + ) + .expect("schedule exchange"); + executor.run_until_stalled(); + let response = executor + .run_until(pending.collect()) + .expect("streamed response"); + assert_eq!(reads.get(), 0); + assert_eq!(releases.get(), 0); + let bytes = executor + .run_until(response.into_bytes_bounded(8)) + .expect("live collected stream"); + assert_eq!(bytes, Bytes::from_static(b"body")); + assert_eq!(reads.get(), 1); + assert_eq!(releases.get(), 1); + } + + #[test] + fn unpolled_batch_progresses_and_preserves_explicit_input_indices() { + let mut executor = LocalPool::new(); + let spawner = executor.spawner(); + let now = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || now); + let time = OutboundTime::new(clock, |_, _| Box::pin(pending())); + let deadline = + Deadline::at_instant(now.checked_add(Duration::from_secs(1)).expect("deadline")); + let budget = DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline, + duration: Duration::from_secs(1), + }; + let calls = Rc::new(Cell::new(0_usize)); + let slots = [2, 5] + .into_iter() + .map(|index| { + let observed = Rc::clone(&calls); + ( + index, + budget, + async move { + observed.set(observed.get().saturating_add(1)); + Ok(response(Body::empty())) + } + .boxed_local(), + ) + }) + .collect(); + let (mut events, owner) = schedule_batch(slots, time, deadline, move |task| { + spawner.spawn_local(task).expect("schedule batch"); + }) + .expect("schedule batch"); + executor.run_until_stalled(); + assert_eq!(calls.get(), 2); + for expected in [2, 5] { + let Some(OutboundBatchDriverEvent::Item { + index, + completed_at, + outcome, + }) = executor.run_until(events.next()) + else { + panic!("expected original input index"); + }; + assert_eq!(index, expected); + assert_eq!(completed_at, now); + outcome.expect("successful batch response"); + } + assert!(executor.run_until(events.next()).is_none()); + drop(owner); + } + + #[test] + fn cutoff_retains_prior_own_timeout_but_not_equal_deadline() { + let mut executor = LocalPool::new(); + let spawner = executor.spawner(); + let start = MonotonicInstant::now(); + let end = start + .checked_add(Duration::from_millis(10)) + .expect("cutoff"); + let now = Arc::new(Mutex::new(start)); + let observed = Arc::clone(&now); + let clock = MonotonicClock::new(move || *observed.lock().expect("clock")); + let time = OutboundTime::new(clock, |_, _| Box::pin(pending())); + let cutoff = Deadline::at_instant(end); + let own = DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: Deadline::at_instant( + start + .checked_add(Duration::from_millis(5)) + .expect("own deadline"), + ), + duration: Duration::from_millis(5), + }; + let equal = DispatchBudget { + deadline: cutoff, + duration: Duration::from_millis(10), + ..own + }; + let slots = vec![ + ( + 3, + own, + async move { + *now.lock().expect("clock") = end; + Err(edgezero_core::EdgeError::gateway_timeout_caused( + "own timeout", + own.cause, + )) + } + .boxed_local(), + ), + (7, equal, Box::pin(pending())), + ]; + let (mut events, owner) = schedule_batch(slots, time, cutoff, move |task| { + spawner.spawn_local(task).expect("schedule batch"); + }) + .expect("schedule batch"); + executor.run_until_stalled(); + let Some(OutboundBatchDriverEvent::Item { + index: 3, + outcome: Err(error), + .. + }) = executor.run_until(events.next()) + else { + panic!("own deadline outcome must precede cutoff"); + }; + assert!(matches!( + error, + edgezero_core::EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); + assert!(matches!( + executor.run_until(events.next()), + Some(OutboundBatchDriverEvent::Cutoff) + )); + assert!(executor.run_until(events.next()).is_none()); + drop(owner); + } + + #[test] + fn late_operational_timer_error_completes_at_the_retained_own_deadline() { + struct Client { + time: OutboundTime, + observed: Arc>, + completed_at: MonotonicInstant, + } + + impl OutboundHttpClient for Client { + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn time(&self) -> OutboundTime { + self.time.clone() + } + + fn validate_supported_options( + &self, + request: &OutboundRequest, + mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + validate_for_dispatch(request, mode) + } + + fn dispatch_head( + &self, + _request: OutboundDispatchRequest, + ) -> OutboundFuture { + let observed = Arc::clone(&self.observed); + let completed_at = self.completed_at; + Box::pin(async move { + *observed.lock().expect("clock") = completed_at; + Err(EdgeError::internal(anyhow::anyhow!( + "operational timer failure" + ))) + }) + } + + fn begin( + &self, + _request: OutboundDispatchRequest, + ) -> Result { + Err(EdgeError::internal(anyhow::anyhow!("batch-only fixture"))) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, requests) = batch.into_slots(); + let slots = requests + .into_iter() + .map(|(index, request)| { + let budget = request.budget(); + let head = self.dispatch_head(request); + ( + index, + budget, + async move { head.await?.finish().await }.boxed_local(), + ) + }) + .collect(); + schedule_prepared_batch(context, slots, block_on) + } + } + + let start = MonotonicInstant::now(); + let end = start + .checked_add(Duration::from_millis(10)) + .expect("cutoff"); + let observed = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&observed); + let time = OutboundTime::new( + MonotonicClock::new(move || *clock_now.lock().expect("clock")), + |_, _| Box::pin(pending()), + ); + let client = HttpClient::with_client(Client { + time, + observed, + completed_at: end, + }); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .timeout(Duration::from_millis(5)); + let result = block_on( + client + .start_batch_until(vec![request], Deadline::at_instant(end)) + .collect(), + ) + .expect("own timeout completes the batch without a trailing cutoff"); + assert_eq!(result.termination(), OutboundBatchTermination::Completed); + let [OutboundCollectedSlot::Resolved(slot)] = result.slots() else { + panic!("one resolved own-deadline timeout"); + }; + assert_eq!(slot.elapsed, Duration::from_millis(5)); + assert!(matches!( + &slot.outcome, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + )); + } +} diff --git a/crates/edgezero-adapter-spin/src/outbound/spin_impl/scope_tests.rs b/crates/edgezero-adapter-spin/src/outbound/spin_impl/scope_tests.rs new file mode 100644 index 000000000..d00c2aeba --- /dev/null +++ b/crates/edgezero-adapter-spin/src/outbound/spin_impl/scope_tests.rs @@ -0,0 +1,322 @@ +use std::cell::RefCell; +use std::io; +use std::rc::Rc; +use std::sync::{Arc, Mutex, Weak}; +use std::time::Duration; + +use crate::request::dispatch_ingress_stream_for_test; +use bytes::Bytes; +use edgezero_core::app::{App, Hooks}; +use edgezero_core::context::RequestContext; +use edgezero_core::http::{Method, Response as CoreResponse, Uri}; +use edgezero_core::outbound::{ + HttpClient, IndependentOutboundProgress, OutboundBatch, OutboundCollectedSlot, + OutboundDispatchRequest, OutboundFuture, OutboundHead, OutboundHttpClient, OutboundPending, + OutboundPreparedBatch, OutboundProgressRuntime, OutboundRequest, OutboundScopeOwner, + OutboundSendMode, OutboundTime, +}; +use edgezero_core::router::RouterService; +use edgezero_core::{Body, EdgeError, MonotonicClock, MonotonicInstant, action}; +use futures_util::{FutureExt as _, stream}; +use spin_sdk::http::body::stream as native_stream; +use spin_sdk::http::{FromRequest as _, IntoRequest as _, Request as SpinRequest}; +use spin_sdk::wasip3::http::types::{ErrorCode, Fields, Response}; +use spin_sdk::wasip3::http_compat::BodyWriter; +use spin_sdk::wasip3::wit_bindgen::FutureReader; + +use super::{FIXTURE_RESPONSE, FixtureClient, SpinOutboundClient, native_time}; + +#[derive(Default)] +struct Retained { + batch: Option, + begun: Option, + leases: Vec<(BodyWriter, FutureReader>)>, + send: Option>, + unpolled: Option>, + upload: Option>, +} + +struct DeferredClient(OutboundTime); + +struct ScopeHooks; + +#[expect( + clippy::missing_trait_methods, + reason = "fixture only supplies canonical routes" +)] +impl Hooks for ScopeHooks { + fn routes() -> Result { + Ok(RouterService::builder() + .get("/retain", retain_handles) + .build()) + } +} + +struct AssemblyFailure; + +#[expect( + clippy::missing_trait_methods, + reason = "fixture fails after retaining assembly work" +)] +impl Hooks for AssemblyFailure { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + let mut retained = Retained::default(); + retain_unpolled( + &mut retained, + &HttpClient::with_client(SpinOutboundClient::new()), + )?; + let previous = RETAINED.with(|slot| slot.replace(Some(retained))); + drop(previous); + Err(EdgeError::service_unavailable( + "scope fixture assembly failure", + )) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } +} + +std::thread_local! { + static RETAINED: RefCell> = const { RefCell::new(None) }; + static PREPARED: RefCell> = const { RefCell::new(None) }; +} + +impl OutboundHttpClient for DeferredClient { + fn begin(&self, _request: OutboundDispatchRequest) -> Result { + Err(failure("deferred fixture only captures sends")) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + let previous = PREPARED.with(|slot| slot.replace(Some(request))); + drop(previous); + Box::pin(async { Err(failure("prepared request captured")) }) + } + + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, _) = batch.into_slots(); + OutboundBatch::from_driver( + context, + stream::iter([edgezero_core::OutboundBatchDriverEvent::Failed(failure( + "deferred fixture only captures sends", + ))]), + edgezero_core::OutboundExchangeOwner::new(|| {}), + ) + } + + fn time(&self) -> OutboundTime { + self.0.clone() + } + + fn validate_supported_options( + &self, + _request: &OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + Ok(()) + } +} + +fn failure(message: &'static str) -> EdgeError { + EdgeError::internal(anyhow::anyhow!(message)) +} + +fn install_response(retained: &mut Retained) { + let (writer, contents, trailers) = BodyWriter::new(); + let (response, done) = Response::new(Fields::new(), Some(contents), trailers); + retained.leases.push((writer, done)); + let previous = FIXTURE_RESPONSE.with(|slot| slot.replace(Some(response))); + drop(previous); +} + +fn native_request() -> Result { + let (sender, body) = native_stream::(); + drop(sender); + let outgoing = SpinRequest::builder() + .uri("http://fixture.invalid/retain") + .body(body) + .map_err(EdgeError::internal)?; + let request = outgoing.into_request().map_err(EdgeError::internal)?; + SpinRequest::from_request(request).map_err(EdgeError::internal) +} + +fn retain_unpolled(retained: &mut Retained, client: &HttpClient) -> Result<(), EdgeError> { + let upload = Arc::<[u8]>::from(&b"unpolled owned upload"[..]); + retained.upload = Some(Arc::downgrade(&upload)); + retained.unpolled = Some(client.send( + OutboundRequest::post("https://fixture.invalid/unpolled")?.body(Bytes::from_owner(upload)), + )); + Ok(()) +} + +#[action] +async fn retain_handles(RequestContext(ctx): RequestContext) -> Result { + let mut retained = Retained::default(); + let native = ctx + .http_client() + .ok_or_else(|| failure("canonical client missing"))?; + retain_unpolled(&mut retained, &native)?; + let time = native_time(ctx.monotonic_clock()); + let client = HttpClient::with_client(FixtureClient(time.clone())); + + install_response(&mut retained); + let mut send = client.send(OutboundRequest::get("https://fixture.invalid/send")?); + if send.as_mut().now_or_never().is_some() { + return Err(failure("SDK send must wait for its retained body reader")); + } + retained.send = Some(send); + + install_response(&mut retained); + retained.begun = Some( + client + .begin(OutboundRequest::get("https://fixture.invalid/begin")?) + .await?, + ); + + install_response(&mut retained); + retained.batch = Some(client.start_batch_until( + vec![OutboundRequest::get("https://fixture.invalid/batch")?], + time.clock().deadline_after(Duration::from_secs(1)), + )); + let previous = RETAINED.with(|slot| slot.replace(Some(retained))); + drop(previous); + Ok(CoreResponse::new(Body::empty())) +} + +pub(super) async fn canonical_callback_releases_retained_handles() -> Result<(), EdgeError> { + let app = App::build::(crate::SPIN_PLATFORM)?; + let envelope = dispatch_ingress_stream_for_test( + &app, + Method::GET, + Uri::from_static("/retain"), + stream::empty::>(), + ) + .await + .map_err(EdgeError::internal)?; + assert_retained_handles_closed().await?; + drop(envelope); + Ok(()) +} + +pub(super) async fn canonical_entrypoints_release_retained_handles() -> Result<(), EdgeError> { + let app = App::build::(crate::SPIN_PLATFORM)?; + let response = crate::dispatch_app::(&app, native_request()?) + .await + .map_err(EdgeError::internal)?; + assert_retained_handles_closed().await?; + drop(response); + + let assembly_response = crate::run_app::(native_request()?) + .await + .map_err(EdgeError::internal)?; + if assembly_response.get_status_code() != 503 { + return Err(failure("assembly scope fixture lost its typed response")); + } + let mut retained = take_retained()?; + assert_upload_released(&retained)?; + let future = retained + .unpolled + .take() + .ok_or_else(|| failure("assembly upload missing"))?; + if !matches!(future.await, Err(EdgeError::Internal { .. })) { + return Err(failure( + "assembly callback did not close its retained upload", + )); + } + drop(assembly_response); + Ok(()) +} + +fn take_retained() -> Result { + RETAINED + .with(|slot| slot.borrow_mut().take()) + .ok_or_else(|| failure("canonical handler did not retain its outbound handles")) +} + +fn assert_upload_released(retained: &Retained) -> Result<(), EdgeError> { + if retained.upload.as_ref().and_then(Weak::upgrade).is_some() { + return Err(failure("callback return retained an unpolled Send upload")); + } + Ok(()) +} + +async fn assert_retained_handles_closed() -> Result<(), EdgeError> { + let mut retained = take_retained()?; + assert_upload_released(&retained)?; + for (writer, _completion) in &mut retained.leases { + if writer.stream_writer.write_all(vec![1]).now_or_never() != Some(vec![1]) { + return Err(failure("callback return retained an SDK response reader")); + } + } + for future in [retained.send.take(), retained.unpolled.take()] + .into_iter() + .flatten() + { + if !matches!(future.await, Err(EdgeError::Internal { .. })) { + return Err(failure("closed Send did not retain its scope failure")); + } + } + let begun = retained + .begun + .take() + .ok_or_else(|| failure("begun fixture missing"))?; + if !matches!(begun.collect().await, Err(EdgeError::Internal { .. })) { + return Err(failure("closed Begin did not retain its scope failure")); + } + let batch = retained + .batch + .take() + .ok_or_else(|| failure("batch fixture missing"))?; + let batch_failure = batch + .collect() + .await + .err() + .ok_or_else(|| failure("closed batch must fail"))?; + if !matches!(batch_failure.error(), EdgeError::Internal { .. }) + || !matches!(batch_failure.slots(), [OutboundCollectedSlot::Unresolved]) + { + return Err(failure("closed batch lost its scope failure or slot state")); + } + drop(retained); + Ok(()) +} + +pub(super) async fn foreign_poll_cannot_reparent_prepared_dispatch() -> Result<(), EdgeError> { + let start = MonotonicInstant::now(); + let observed = Arc::new(Mutex::new(start)); + let clock = Arc::clone(&observed); + let time = native_time(MonotonicClock::new(move || { + clock.lock().map_or(start, |current| *current) + })); + let owner = OutboundScopeOwner::callback(); + let client = HttpClient::with_client(DeferredClient(time)); + let request = + OutboundRequest::get("https://fixture.invalid/deferred")?.timeout(Duration::from_millis(1)); + let captured = owner.with(|| client.send(request)); + if !matches!(captured.await, Err(EdgeError::Internal { .. })) { + return Err(failure("capture-only transport must fail")); + } + let prepared = PREPARED + .with(|slot| slot.borrow_mut().take()) + .ok_or_else(|| failure("prepared fixture missing"))?; + let deferred = SpinOutboundClient::new().dispatch_head(prepared); + drop(owner); + *observed + .lock() + .map_err(|_poisoned| failure("fixture clock poisoned"))? = start + .checked_add(Duration::from_secs(1)) + .ok_or_else(|| failure("fixture clock overflow"))?; + let foreign = OutboundScopeOwner::callback(); + let outcome = foreign.run(deferred).await; + if !matches!(outcome, Err(EdgeError::Internal { .. })) { + return Err(failure( + "foreign poll reparented a closed prepared dispatch", + )); + } + foreign.scope().check()?; + Ok(()) +} diff --git a/crates/edgezero-adapter-spin/src/proxy.rs b/crates/edgezero-adapter-spin/src/proxy.rs deleted file mode 100644 index 81754c772..000000000 --- a/crates/edgezero-adapter-spin/src/proxy.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::decompress::decompress_body; -use crate::response::collect_body_bytes; -use async_trait::async_trait; -use bytes::Bytes; -use edgezero_core::body::Body; -use edgezero_core::error::EdgeError; -use edgezero_core::http::{HeaderValue, header}; -use edgezero_core::proxy::{PROXY_HEADER, ProxyClient, ProxyRequest, ProxyResponse}; -use spin_sdk::http::body::IncomingBodyExt as _; -use spin_sdk::http::{FullBody, Request as SpinRequest, send}; - -/// A proxy client that uses Spin's outbound HTTP (`spin_sdk::http::send`) -/// to forward requests to upstream services. -pub struct SpinProxyClient; - -#[async_trait(?Send)] -impl ProxyClient for SpinProxyClient { - #[inline] - async fn send(&self, request: ProxyRequest) -> Result { - let (method, uri, headers, body, _extensions) = request.into_parts(); - - let mut builder = SpinRequest::builder().method(method).uri(uri.to_string()); - - for (name, value) in &headers { - builder = builder.header(name, value); - } - - let request_body_bytes = collect_body_bytes(body).await?; - - let spin_request = builder - .body(FullBody::new(Bytes::from(request_body_bytes))) - .map_err(|err| { - EdgeError::internal(anyhow::anyhow!("failed to build proxy request: {err}")) - })?; - - let spin_response = send(spin_request).await.map_err(|err| { - EdgeError::internal(anyhow::anyhow!("Spin outbound HTTP error: {err}")) - })?; - - let (response_parts, response_body) = spin_response.into_parts(); - - let encoding = response_parts - .headers - .get(header::CONTENT_ENCODING) - .and_then(|value| value.to_str().ok()) - .map(str::to_ascii_lowercase); - - let response_body_bytes = response_body.bytes().await.map_err(|err| { - EdgeError::internal(anyhow::anyhow!("failed to read proxy response body: {err}")) - })?; - let decompressed = decompress_body(response_body_bytes.to_vec(), encoding.as_deref())?; - let mut proxy_response = - ProxyResponse::new(response_parts.status, Body::from(decompressed)); - - for (name, value) in &response_parts.headers { - proxy_response - .headers_mut() - .insert(name.clone(), value.clone()); - } - - // Strip encoding headers after decompression so downstream - // handlers see plain bytes (consistent with Fastly/Cloudflare). - if matches!(encoding.as_deref(), Some("gzip" | "br")) { - proxy_response - .headers_mut() - .remove(header::CONTENT_ENCODING); - proxy_response.headers_mut().remove(header::CONTENT_LENGTH); - } - - // `HeaderValue::from_static("spin")` is infallible at compile time so - // it cannot panic at runtime — replaces the previous - // `.parse().expect(...)` which tripped expect_used under restriction. - proxy_response - .headers_mut() - .insert(PROXY_HEADER, HeaderValue::from_static("spin")); - - Ok(proxy_response) - } -} diff --git a/crates/edgezero-adapter-spin/src/request.rs b/crates/edgezero-adapter-spin/src/request.rs index bf9543bf0..6ccf08cd6 100644 --- a/crates/edgezero-adapter-spin/src/request.rs +++ b/crates/edgezero-adapter-spin/src/request.rs @@ -1,29 +1,61 @@ use std::collections::BTreeMap; +use std::future::Future; +#[cfg(feature = "test-utils")] +use std::future::pending; use std::sync::Arc; +use std::time::Duration; +#[cfg(feature = "test-utils")] +use std::{ + io, + sync::atomic::{AtomicUsize, Ordering}, + task::Poll, +}; -use anyhow::Context as _; +#[cfg(feature = "test-utils")] +use bytes::Bytes; -use crate::SpinFullResponse; +use crate::SpinResponse; use crate::config_store::SpinConfigStore; use crate::context::{SpinRequestContext, parse_client_addr}; use crate::key_value_store::{DEFAULT_MAX_LIST_KEYS, SpinKvStore}; -use crate::proxy::SpinProxyClient; -use crate::response::from_core_response; +use crate::response::from_egress_response; use crate::secret_store::SpinSecretStore; use edgezero_core::app::{App, StoreMetadata}; use edgezero_core::body::Body; use edgezero_core::config_store::ConfigStoreHandle; use edgezero_core::env_config::EnvConfig; use edgezero_core::error::EdgeError; -use edgezero_core::http::{Request, request_builder}; +#[cfg(feature = "test-utils")] +use edgezero_core::http::Uri; +#[cfg(any(test, feature = "test-utils"))] +use edgezero_core::http::request_builder; +use edgezero_core::http::{Method, Request, RequestParts}; +use edgezero_core::ingress::{ + IngressBeginOutcome, IngressDispatchOutcome, IngressFraming, IngressHeadAccounting, + IngressHeadParts, PreparedIngress, +}; use edgezero_core::key_value_store::KvHandle; -use edgezero_core::proxy::ProxyHandle; +use edgezero_core::outbound::HttpClient; +use edgezero_core::response_egress::ResponseEgressEnvelope; use edgezero_core::secret_store::SecretHandle; use edgezero_core::store_registry::{ BoundSecretStore, ConfigRegistry, ConfigStoreBinding, KvRegistry, SecretRegistry, StoreRegistry, }; +use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; +#[cfg(any(test, feature = "test-utils"))] +use futures::executor::block_on; +use futures_util::future::{Either, select}; +#[cfg(feature = "test-utils")] +use futures_util::stream::Empty; +#[cfg(feature = "test-utils")] +use futures_util::stream::poll_fn; +use futures_util::stream::unfold; +use futures_util::{StreamExt as _, TryStreamExt as _}; use spin_sdk::http::Request as SpinRequest; use spin_sdk::http::body::IncomingBodyExt as _; +use spin_sdk::time::sleep; + +use crate::outbound::{SpinOutboundClient, native_time}; /// Per-dispatch store wiring assembled before the request enters the router. /// The struct itself is `pub(crate)` because `dispatch_with_handles` takes it @@ -39,18 +71,77 @@ pub(crate) struct Stores { secrets: Option, } +#[cfg(feature = "test-utils")] +struct DropSignal(Arc); + +#[cfg(feature = "test-utils")] +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for DropSignal { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } +} + +fn detached_error_envelope( + app: &App, + error: EdgeError, + request_method: Method, + request_start: MonotonicInstant, +) -> anyhow::Result { + match app.detached_ingress_error_egress(error, request_method, request_start) { + IngressDispatchOutcome::Response(envelope) => Ok(*envelope), + IngressDispatchOutcome::Aborted | _ => Err(anyhow::anyhow!("ingress admission aborted")), + } +} + /// Convert a Spin `Request` into an `EdgeZero` core `Request`. /// -/// Reads the full body into a buffered `Body::Once`, inserts -/// `SpinRequestContext` and a `ProxyHandle` into extensions. +/// Preserves the body as a lazy stream, inserts `SpinRequestContext`, and adds +/// an outbound [`HttpClient`] to extensions. /// /// # Errors /// Returns [`EdgeError::bad_request`] if the request body cannot be read or /// the core `Request` cannot be built from the resulting parts. #[inline] +#[expect( + clippy::unused_async, + reason = "the public converter retains its established async API while request bodies remain lazy" +)] pub async fn into_core_request(req: SpinRequest) -> Result { let (parts, body) = req.into_parts(); + let mut request = into_core_request_head(parts, MonotonicClock::default()); + *request.body_mut() = Body::from_external_stream(body.stream()); + Ok(request) +} + +/// Begins canonical detached delivery of an assembly error without a configured app. +/// +/// Releases the unread request. The supplied start and clock must share a domain; the finite +/// default write deadline starts before assembly. No configured app callbacks or headers apply. +/// +/// # Errors +/// Returns a category-safe delivery error if conversion and its bounded fallback both fail. +#[inline] +pub fn send_assembly_error( + req: SpinRequest, + error: EdgeError, + request_start: MonotonicInstant, + clock: MonotonicClock, +) -> Result { + let method = req.method().clone(); + drop(req); + from_egress_response(App::assembly_error_egress( + error, + method, + request_start, + clock, + )) +} +fn into_core_request_head(parts: RequestParts, outbound_clock: MonotonicClock) -> Request { let client_addr = parts .headers .get("spin-client-addr") @@ -62,23 +153,7 @@ pub async fn into_core_request(req: SpinRequest) -> Result { .and_then(|value| value.to_str().ok()) .map(str::to_owned); - let mut builder = request_builder().method(parts.method).uri(parts.uri); - for (name, value) in &parts.headers { - builder = builder.header(name, value); - } - - // Inbound body size is not capped at the adapter level. The Spin runtime - // enforces its own request body limit (configurable via `spin.toml`), which - // is consistent with how the Fastly and Cloudflare adapters delegate inbound - // size enforcement to their respective platform runtimes. - let body_bytes = body - .bytes() - .await - .map_err(|err| EdgeError::bad_request(format!("failed to read request body: {err}")))?; - - let mut request = builder - .body(Body::from(body_bytes.to_vec())) - .map_err(|err| EdgeError::bad_request(format!("failed to build request: {err}")))?; + let mut request = Request::from_parts(parts, Body::empty()); SpinRequestContext::insert( &mut request, @@ -89,9 +164,249 @@ pub async fn into_core_request(req: SpinRequest) -> Result { ); request .extensions_mut() - .insert(ProxyHandle::with_client(SpinProxyClient)); + .insert(outbound_client(outbound_clock)); - Ok(request) + request +} + +fn into_core_request_head_for_app(parts: RequestParts, app: &App) -> Request { + into_core_request_head(parts, app.monotonic_clock()) +} + +fn outbound_client(clock: MonotonicClock) -> HttpClient { + HttpClient::with_client(SpinOutboundClient::with_time(native_time(clock))) +} + +fn deadline_body_with_timer( + source: Source, + deadline: Deadline, + monotonic_clock: MonotonicClock, + make_timer: MakeTimer, +) -> Body +where + Source: futures_util::Stream> + 'static, + SourceError: Into + 'static, + Timer: Future + 'static, + MakeTimer: Fn(Duration) -> Timer + Clone + 'static, +{ + let boxed_stream = source.map_err(Into::into).boxed_local(); + let stream = unfold(Some(boxed_stream), move |stream_state| { + let clock = monotonic_clock.clone(); + let timer_factory = make_timer.clone(); + async move { + let mut body_stream = stream_state?; + let Some(remaining) = deadline.remaining_at(clock.now()) else { + return Some(( + Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )), + None, + )); + }; + let timer = timer_factory(remaining); + let next_item = body_stream.next(); + futures_util::pin_mut!(timer, next_item); + let item = match select(timer, next_item).await { + Either::Left(((), _next_item)) => { + return Some(( + Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )), + None, + )); + } + Either::Right((item, _timer)) => item, + }; + if deadline.is_expired_at(clock.now()) { + return Some(( + Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )), + None, + )); + } + match item { + Some(Ok(bytes)) => Some((Ok(bytes), Some(body_stream))), + Some(Err(error)) => Some((Err(EdgeError::internal(error)), None)), + None => None, + } + } + }); + Body::from_stream(stream) +} + +/// Runs the terminal source-release probe used by the WASM contract suite. +#[cfg(feature = "test-utils")] +#[must_use] +#[inline] +pub fn deadline_body_releases_source_for_test() -> bool { + let dropped = Arc::new(AtomicUsize::new(0)); + let signal = DropSignal(Arc::clone(&dropped)); + let source = poll_fn(move |_cx| { + let _keep_alive = &signal; + Poll::>>::Pending + }); + let start = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || start); + let body = deadline_body_with_timer(source, Deadline::at_instant(start), clock, |_| { + pending::<()>() + }); + let Ok(mut body_stream) = body.into_stream() else { + return false; + }; + let Some(Err(error)) = block_on(body_stream.next()) else { + return false; + }; + matches!(error, EdgeError::RequestTimeout { .. }) && dropped.load(Ordering::SeqCst) == 1 +} + +/// Dispatches an observable source through the production ingress body wrapper without a +/// provider timer. Tests that exercise timer expiry use +/// [`dispatch_ingress_stream_with_timer_for_test`] instead. +#[cfg(feature = "test-utils")] +#[doc(hidden)] +#[inline] +pub async fn dispatch_ingress_stream_for_test( + app: &App, + method: Method, + uri: Uri, + source: Source, +) -> anyhow::Result +where + Source: futures_util::Stream> + 'static, + SourceError: Into + 'static, +{ + let request_start = app.monotonic_now(); + let core_request = request_builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .map_err(EdgeError::internal)?; + dispatch_ingress_stream_with_timer_and( + app, + core_request, + Stores::default(), + request_start, + move || Ok(source), + |_| pending::<()>(), + Ok, + ) + .await +} + +/// Exercises admission ordering with observable source construction and delivery closures. +#[cfg(feature = "test-utils")] +#[doc(hidden)] +#[inline] +pub async fn dispatch_ingress_with_for_test( + app: &App, + method: Method, + uri: Uri, + make_source: MakeSource, + deliver: Deliver, +) -> anyhow::Result +where + Source: futures_util::Stream> + 'static, + SourceError: Into + 'static, + MakeSource: FnOnce() -> Result, + Deliver: FnOnce(ResponseEgressEnvelope) -> Result, +{ + let request_start = app.monotonic_now(); + let core_request = request_builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .map_err(EdgeError::internal)?; + dispatch_ingress_stream_with_timer_and( + app, + core_request, + Stores::default(), + request_start, + make_source, + |_| pending::<()>(), + deliver, + ) + .await +} + +/// Exercises a source-construction failure after admission through the production seam. +#[cfg(feature = "test-utils")] +#[doc(hidden)] +#[inline] +pub async fn dispatch_ingress_source_error_for_test( + app: &App, + method: Method, + uri: Uri, +) -> anyhow::Result { + let request_start = app.monotonic_now(); + let core_request = request_builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .map_err(EdgeError::internal)?; + dispatch_ingress_stream_with_timer_and( + app, + core_request, + Stores::default(), + request_start, + || -> Result>, EdgeError> { + Err(EdgeError::internal(anyhow::anyhow!( + "spin source construction failed" + ))) + }, + |_| pending::<()>(), + Ok, + ) + .await +} + +/// Dispatches an observable source through the production ingress deadline race. +#[cfg(feature = "test-utils")] +#[doc(hidden)] +#[inline] +pub async fn dispatch_ingress_stream_with_timer_for_test( + app: &App, + method: Method, + uri: Uri, + source: Source, + make_timer: MakeTimer, +) -> anyhow::Result +where + Source: futures_util::Stream> + 'static, + SourceError: Into + 'static, + Timer: Future + 'static, + MakeTimer: Fn(Duration) -> Timer + Clone + 'static, +{ + let request_start = app.monotonic_now(); + let core_request = request_builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .map_err(EdgeError::internal)?; + dispatch_ingress_stream_with_timer_and( + app, + core_request, + Stores::default(), + request_start, + move || Ok(source), + make_timer, + Ok, + ) + .await +} + +/// Dispatches a native Spin request through the production outer request seam. +/// +/// # Errors +/// Returns the same request-conversion, routing, or response-conversion error as standard dispatch. +#[cfg(feature = "test-utils")] +#[doc(hidden)] +#[inline] +pub async fn dispatch_request_for_test( + app: &App, + req: SpinRequest, +) -> anyhow::Result { + dispatch_with_handles_and(app, req, Stores::default(), app.monotonic_now(), Ok).await } /// Dispatch a Spin request through the `EdgeZero` router using the `"default"` @@ -106,15 +421,16 @@ pub async fn into_core_request(req: SpinRequest) -> Result { /// cannot be converted, the router dispatch fails, or response translation /// fails. #[inline] -pub async fn dispatch(app: &App, req: SpinRequest) -> anyhow::Result { - dispatch_with_kv_label(app, req, "default").await +pub async fn dispatch(app: &App, req: SpinRequest) -> anyhow::Result { + let request_start = app.monotonic_now(); + dispatch_with_kv_label_at(app, req, "default", request_start).await } /// Dispatch a Spin request through the `EdgeZero` router and return /// a Spin-compatible response, opening the KV store under `kv_label`. /// /// Injects all available stores into request extensions: -/// - `ConfigStoreHandle` backed by `SpinConfigStore` opened on `kv_label` +/// - `ConfigStoreHandle` backed by `SpinConfigStore` bound to `kv_label` /// (KV-backed since 2026-Q3; was variables-backed before that). The /// same label opens the config-store backend as the KV store on this /// low-level path — registry-aware callers should use [`run_app`] @@ -128,29 +444,187 @@ pub async fn dispatch(app: &App, req: SpinRequest) -> anyhow::Result anyhow::Result { - let stores = Stores { - config_store: resolve_config_handle(kv_label).await?, - kv: resolve_kv_handle(kv_label, false).await?, - secrets: resolve_secret_handle(true), - ..Default::default() - }; - dispatch_with_handles(app, req, stores).await +) -> anyhow::Result { + let request_start = app.monotonic_now(); + dispatch_with_kv_label_at(app, req, kv_label, request_start).await +} + +async fn dispatch_with_kv_label_at( + app: &App, + req: SpinRequest, + kv_label: &str, + request_start: MonotonicInstant, +) -> anyhow::Result { + crate::in_outbound_callback(async move { + let stores = Stores { + config_store: Some(config_handle(kv_label.to_owned())), + kv: resolve_kv_handle(kv_label, false).await?, + secrets: resolve_secret_handle(true), + ..Default::default() + }; + dispatch_with_handles(app, req, stores, request_start).await + }) + .await } pub(crate) async fn dispatch_with_handles( app: &App, req: SpinRequest, stores: Stores, -) -> anyhow::Result { - let mut core_request = into_core_request(req).await?; + request_start: MonotonicInstant, +) -> anyhow::Result { + dispatch_with_handles_and(app, req, stores, request_start, from_egress_response).await +} + +async fn dispatch_with_handles_and( + app: &App, + req: SpinRequest, + stores: Stores, + request_start: MonotonicInstant, + deliver: Deliver, +) -> anyhow::Result +where + Deliver: FnOnce(ResponseEgressEnvelope) -> Result, +{ + crate::in_outbound_callback(async move { + let (parts, native_body) = req.into_parts(); + let core_request = into_core_request_head_for_app(parts, app); + dispatch_ingress_stream_and( + app, + core_request, + stores, + request_start, + move || Ok(native_body.stream()), + deliver, + ) + .await + }) + .await +} + +async fn dispatch_ingress_stream_and( + app: &App, + head_request: Request, + stores: Stores, + request_start: MonotonicInstant, + make_source: MakeSource, + deliver: Deliver, +) -> anyhow::Result +where + Source: futures_util::Stream> + 'static, + SourceError: Into + 'static, + MakeSource: FnOnce() -> Result, + Deliver: FnOnce(ResponseEgressEnvelope) -> Result, +{ + dispatch_ingress_stream_with_timer_and( + app, + head_request, + stores, + request_start, + make_source, + sleep, + deliver, + ) + .await +} + +async fn dispatch_ingress_stream_with_timer_and< + Source, + SourceError, + MakeSource, + Timer, + MakeTimer, + Output, + Deliver, +>( + app: &App, + mut head_request: Request, + stores: Stores, + request_start: MonotonicInstant, + make_source: MakeSource, + make_timer: MakeTimer, + deliver: Deliver, +) -> anyhow::Result +where + Source: futures_util::Stream> + 'static, + SourceError: Into + 'static, + MakeSource: FnOnce() -> Result, + Timer: Future + 'static, + MakeTimer: Fn(Duration) -> Timer + Clone + 'static, + Deliver: FnOnce(ResponseEgressEnvelope) -> Result, +{ + crate::in_outbound_callback(async move { + head_request + .extensions_mut() + .insert(outbound_client(app.monotonic_clock())); + let request_method = head_request.method().clone(); + let head_parts = IngressHeadParts::from_request( + &head_request, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ); + if let Err(error) = head_parts.validate_normalized(app.ingress_head_limits()) { + let egress = detached_error_envelope(app, error, request_method, request_start)?; + return Ok(deliver(egress)?); + } + let prepared = match app.begin_ingress(head_parts, request_start) { + Ok(outcome) => match outcome { + IngressBeginOutcome::Admitted(prepared) => prepared, + IngressBeginOutcome::Refused(response) => { + return Ok(deliver(response)?); + } + IngressBeginOutcome::Aborted => { + return Err(anyhow::anyhow!("ingress admission aborted")); + } + _ => { + let egress = detached_error_envelope( + app, + EdgeError::internal(anyhow::anyhow!( + "unsupported ingress admission outcome" + )), + request_method, + request_start, + )?; + return Ok(deliver(egress)?); + } + }, + Err(error) => { + let egress = detached_error_envelope(app, error, request_method, request_start)?; + return Ok(deliver(egress)?); + } + }; + let source = match make_source() { + Ok(source) => source, + Err(error) => return Ok(deliver(app.admitted_error_egress(prepared, error))?), + }; + *head_request.body_mut() = deadline_body_with_timer( + source, + prepared.read_deadline(), + prepared.monotonic_clock(), + make_timer, + ); + dispatch_core_request(app, head_request, stores, prepared, deliver).await + }) + .await +} + +async fn dispatch_core_request( + app: &App, + mut core_request: Request, + stores: Stores, + prepared: PreparedIngress, + deliver: Deliver, +) -> anyhow::Result +where + Deliver: FnOnce(ResponseEgressEnvelope) -> Result, +{ // Hard-cutoff: see fastly's `dispatch_core_request` // for the rationale. Only registries go into extensions — // legacy bare handles are synthesised into a one-id registry @@ -165,17 +639,18 @@ pub(crate) async fn dispatch_with_handles( if let Some(registry) = secret_registry { core_request.extensions_mut().insert(registry); } - let response = app.router().oneshot(core_request).await?; - Ok(from_core_response(response).await?) + let response = app.dispatch_admitted(prepared, core_request).await; + Ok(deliver(response)?) } /// Dispatch with per-id store registries built from baked metadata. /// /// Spin capability map: -/// - KV: **Multi** — each declared id opens its own [`SpinKvStore`] under the +/// - KV: **Multi** — each declared id lazily opens its [`SpinKvStore`] on use under the /// label resolved from `EDGEZERO__STORES__KV____NAME`. Optional /// `EDGEZERO__STORES__KV____MAX_LIST_KEYS` overrides the paging cap. -/// - Config: **Multi** — each declared id opens its own [`SpinConfigStore`] +/// A settled open failure is typed and retained for that request only. +/// - Config: **Multi** — each declared id binds its own [`SpinConfigStore`] /// under the label resolved from `EDGEZERO__STORES__CONFIG____NAME`. /// KV-backed under the hood (was variables-backed up through 2026-Q2). /// - Secrets: **Single** — every declared id maps to the one shared @@ -187,9 +662,10 @@ pub(crate) async fn dispatch_with_registries( kv_meta: Option, secret_meta: Option, env: &EnvConfig, -) -> anyhow::Result { - let kv_registry = build_kv_registry(kv_meta, env).await?; - let config_registry = build_config_registry(config_meta, env).await?; +) -> anyhow::Result { + let request_start = app.monotonic_now(); + let kv_registry = build_kv_registry(kv_meta, env); + let config_registry = build_config_registry(config_meta, env); let secret_registry = build_secret_registry(secret_meta, env); dispatch_with_handles( app, @@ -200,6 +676,7 @@ pub(crate) async fn dispatch_with_registries( secret_registry, ..Default::default() }, + request_start, ) .await } @@ -246,13 +723,8 @@ fn synthesise_store_registries( (config_registry, kv_registry, secret_registry) } -async fn build_kv_registry( - kv_meta: Option, - env: &EnvConfig, -) -> anyhow::Result> { - let Some(meta) = kv_meta else { - return Ok(None); - }; +fn build_kv_registry(kv_meta: Option, env: &EnvConfig) -> Option { + let meta = kv_meta?; let mut by_id: BTreeMap = BTreeMap::new(); for id in meta.ids { let label = env.store_name("kv", id); @@ -260,59 +732,33 @@ async fn build_kv_registry( .store_setting("kv", id, "MAX_LIST_KEYS") .and_then(|raw| raw.parse::().ok()) .unwrap_or(DEFAULT_MAX_LIST_KEYS); - match SpinKvStore::open_with_max_list_keys(&label, max_list_keys).await { - Ok(store) => { - by_id.insert((*id).to_owned(), KvHandle::new(Arc::new(store))); - } - Err(err) => { - // Required: `[stores.kv]` is declared, so a missing label is a - // configuration error rather than a silent degradation. - return Err(anyhow::anyhow!( - "Spin KV store '{label}' (id `{id}`) is explicitly configured but could not be opened: {err}" - )); - } - } + by_id.insert( + (*id).to_owned(), + KvHandle::new(Arc::new(SpinKvStore::declared(id, label, max_list_keys))), + ); } - // For Spin, KV open is required — any failure already returns Err - // above, so the default id is guaranteed to be in `by_id` here. - // `from_parts` keeps the API symmetric with the other adapters. - Ok(StoreRegistry::from_parts(by_id, meta.default.to_owned())) + StoreRegistry::from_parts(by_id, meta.default.to_owned()) } -async fn build_config_registry( +fn build_config_registry( config_meta: Option, env: &EnvConfig, -) -> anyhow::Result> { - let Some(meta) = config_meta else { - return Ok(None); - }; - // Spin is `Multi` for config (KV-backed): each declared id opens its - // own `key_value::Store` under the label resolved from - // `EDGEZERO__STORES__CONFIG____NAME`. Mirrors `build_kv_registry` - // so missing `key_value_stores = [...]` declarations surface at - // dispatch setup, not on first config read. - // Preserve the `ConfigStoreError` as the anyhow source so the - // caller can downcast to distinguish `Internal` (structural — - // label not declared / registered) from `Unavailable` (transient - // hostcall failure). `with_context` chains rather than - // stringifying. +) -> Option { + let meta = config_meta?; + // Bind every declared id without host calls. Opening inside `get_bounded` + // lets admission run first and subjects the open to extraction's deadline. let mut by_id: BTreeMap = BTreeMap::new(); for id in meta.ids { let label = env.store_name("config", id); - let store = SpinConfigStore::open(label.clone()) - .await - .with_context(|| format!("config store id `{id}` (label `{label}`) failed to open"))?; by_id.insert( (*id).to_owned(), ConfigStoreBinding { - handle: ConfigStoreHandle::new(Arc::new(store)), + handle: config_handle(label), default_key: env.store_key("config", id), }, ); } - // Every id is required to open (any failure returns Err above), so - // `from_parts` is guaranteed to have the default id present. - Ok(StoreRegistry::from_parts(by_id, meta.default.to_owned())) + StoreRegistry::from_parts(by_id, meta.default.to_owned()) } fn build_secret_registry( @@ -337,15 +783,8 @@ fn build_secret_registry( StoreRegistry::from_parts(by_id, meta.default.to_owned()) } -async fn resolve_config_handle(label: &str) -> anyhow::Result> { - // Low-level path (`dispatch` / `dispatch_with_kv_label`): open the - // KV-backed config store under the same label used for KV. Registry - // callers (`dispatch_with_registries`) use `build_config_registry` - // instead, which resolves per-id labels via env. - let store = SpinConfigStore::open(label.to_owned()) - .await - .with_context(|| format!("low-level dispatch: open config store `{label}`"))?; - Ok(Some(ConfigStoreHandle::new(Arc::new(store)))) +fn config_handle(label: String) -> ConfigStoreHandle { + ConfigStoreHandle::new(Arc::new(SpinConfigStore::for_label(label))) } async fn resolve_kv_handle(kv_label: &str, kv_required: bool) -> anyhow::Result> { @@ -376,18 +815,47 @@ fn resolve_secret_handle(secrets_enabled: bool) -> Option { #[cfg(test)] mod synthesis_tests { use super::*; - use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; + use edgezero_core::config_store::{ + BoundedStoreRead, ConfigStore, ConfigStoreError, ConfigStoreHandle, + finish_bounded_config_read, + }; use edgezero_core::key_value_store::{KvStore, NoopKvStore}; + use edgezero_core::router::RouterService; use edgezero_core::secret_store::{NoopSecretStore, SecretHandle}; use std::collections::BTreeMap; - use std::sync::Arc; + use std::collections::VecDeque; + use std::sync::{Arc, Mutex}; + use std::time::Duration; struct StubConfig; #[async_trait::async_trait(?Send)] impl ConfigStore for StubConfig { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Ok(None) } + + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + finish_bounded_config_read( + self.get(key).await, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + } } fn kv_handle() -> KvHandle { @@ -424,6 +892,55 @@ mod synthesis_tests { ); } + #[test] + fn production_head_conversion_installs_the_exact_application_outbound_clock() { + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(7)) + .expect("completed instant"); + let observations = Arc::new(Mutex::new(VecDeque::from([start, completed]))); + let clock_observations = Arc::clone(&observations); + let mut app = App::new(RouterService::builder().build()); + app.set_monotonic_clock(MonotonicClock::new(move || { + clock_observations + .lock() + .expect("clock observations") + .pop_front() + .expect("clock observation") + })); + let request = request_builder() + .method(Method::GET) + .uri("http://example.test/clock") + .body(Body::empty()) + .expect("request"); + let (parts, _body) = request.into_parts(); + let core_request = into_core_request_head_for_app(parts, &app); + let client = core_request + .extensions() + .get::() + .cloned() + .expect("HTTP client"); + let outbound_request = edgezero_core::OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + + let results = block_on(client.send_all_until( + vec![outbound_request], + Deadline::after(Duration::from_secs(1)), + )) + .expect("valid batch driver"); + + assert_eq!( + match &results.slots()[0] { + edgezero_core::OutboundCollectedSlot::Resolved(result) => result.elapsed, + edgezero_core::OutboundCollectedSlot::AlreadyDelivered + | edgezero_core::OutboundCollectedSlot::Unresolved => + panic!("expected resolved outbound slot"), + }, + Duration::from_millis(7) + ); + } + #[test] fn synthesis_registry_wins_over_bare_handle_when_both_wired() { let mut by_id: BTreeMap = BTreeMap::new(); @@ -488,9 +1005,8 @@ mod synthesis_tests { /// Spec 12.7 / plan line 1526: `EDGEZERO__STORES__CONFIG____KEY` /// must surface as `ConfigStoreBinding.default_key`. /// - /// `build_config_registry` calls `SpinConfigStore::open` which requires - /// the Spin executor and cannot be unit-tested here; this test exercises - /// the env-resolution layer that `build_config_registry` reads from. + /// `build_config_registry` binds labels without opening the host store; + /// this test exercises the env-resolution layer used by that builder. /// Platform-integration coverage relies on the E2 smoke scripts. #[test] fn config_default_key_env_override_resolved() { diff --git a/crates/edgezero-adapter-spin/src/response.rs b/crates/edgezero-adapter-spin/src/response.rs index 7a6525111..ca4fccdfe 100644 --- a/crates/edgezero-adapter-spin/src/response.rs +++ b/crates/edgezero-adapter-spin/src/response.rs @@ -1,74 +1,1931 @@ +#![cfg_attr( + test, + expect( + clippy::self_named_module_files, + reason = "retain the existing response module file while colocating coordinator tests" + ) +)] +#![cfg_attr( + target_arch = "wasm32", + expect( + clippy::arbitrary_source_item_ordering, + reason = "the platform module follows response construction, cancellation, and entrypoint lifecycle order" + ) +)] + +use std::future::Future; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::pin::Pin; +use std::task::{Context, Poll}; +#[cfg(test)] +use std::time::Duration; + use bytes::Bytes; -use edgezero_core::body::Body; +use edgezero_core::body::{Body, BodyPayload, BodyStream}; use edgezero_core::error::EdgeError; -use edgezero_core::http::Response; +use edgezero_core::http::{HeaderMap, Method, Response, StatusCode}; +use edgezero_core::response_egress::{ + RESPONSE_EGRESS_FALLBACK_SAFETY_BUDGET, ResponseEgressAttempt, + ResponseEgressFallbackDisposition, ResponseEgressOutcome, ResponseEgressPolicy, +}; +use edgezero_core::response_egress_framing::ResponseEgressFramingMode; +use edgezero_core::response_egress_framing::{ + PreparedResponseEgress, response_egress_source_error_category, +}; +use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; use futures_util::StreamExt as _; -use spin_sdk::http::{FullBody, Response as SpinResponse}; +use futures_util::future::{FutureExt as _, LocalBoxFuture}; +use futures_util::stream; -use crate::SpinFullResponse; +const INTERNAL_FALLBACK_BODY: &[u8] = b"internal server error"; +const TIMEOUT_FALLBACK_BODY: &[u8] = b"response write deadline exceeded"; -/// Maximum body size (16 MiB) when collecting a streamed body into a buffer. -/// Prevents unbounded memory growth from malicious or misconfigured upstreams. -/// -/// Note: this cap only applies to `Body::Stream` variants. `Body::Once` is -/// already materialised in memory and bypasses this check. The proxy module -/// uses a separate, larger limit ([`MAX_DECOMPRESSED_SIZE`](crate::proxy) = -/// 64 MiB) because proxy responses are untrusted external data that may -/// decompress to a much larger size. -const MAX_BODY_SIZE: usize = 16 * 1024 * 1024; - -/// Collect a `Body` into a `Vec`, consuming streamed chunks if necessary. -/// -/// Stream bodies are capped at [`MAX_BODY_SIZE`] bytes. If the accumulated -/// size exceeds the limit, collection stops and an error is returned. -pub(crate) async fn collect_body_bytes(body: Body) -> Result, EdgeError> { - match body { - Body::Once(bytes) => Ok(bytes.to_vec()), - Body::Stream(mut stream) => { - let mut collected = Vec::new(); - while let Some(chunk) = stream.next().await { - match chunk { - Ok(bytes) => { - // `usize::saturating_add` keeps the bound check - // honest against pathological inputs without - // triggering arithmetic_side_effects. - if collected.len().saturating_add(bytes.len()) > MAX_BODY_SIZE { - return Err(EdgeError::internal(anyhow::anyhow!( - "body exceeds maximum size of {MAX_BODY_SIZE} bytes" - ))); +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DeliveryError { + Conversion, + Deadline, + Source, + Transport, +} + +impl DeliveryError { + const fn from_outcome(outcome: ResponseEgressOutcome) -> Self { + match outcome { + ResponseEgressOutcome::ConversionError => Self::Conversion, + ResponseEgressOutcome::DeadlineExceeded => Self::Deadline, + ResponseEgressOutcome::SourceError => Self::Source, + ResponseEgressOutcome::ClientDisconnected + | ResponseEgressOutcome::Completed + | ResponseEgressOutcome::HostHandoff + | ResponseEgressOutcome::RequestCancelled + | ResponseEgressOutcome::TransportError + | ResponseEgressOutcome::Unspecified + | _ => Self::Transport, + } + } + + const fn outcome(self) -> ResponseEgressOutcome { + match self { + Self::Conversion => ResponseEgressOutcome::ConversionError, + Self::Deadline => ResponseEgressOutcome::DeadlineExceeded, + Self::Source => ResponseEgressOutcome::SourceError, + Self::Transport => ResponseEgressOutcome::TransportError, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum EgressKind { + Application, + Fallback, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum IoFailureKind { + Deadline, + Transport, +} + +impl IoFailureKind { + const fn delivery_error(self) -> DeliveryError { + match self { + Self::Deadline => DeliveryError::Deadline, + Self::Transport => DeliveryError::Transport, + } + } +} + +#[derive(Debug)] +struct IoFailure { + accepted: usize, + kind: IoFailureKind, +} + +#[derive(Debug)] +struct WriteProgress { + accepted: usize, + remaining: Vec, +} + +enum EgressRace { + Deadline, + Operation(Output), + ResponseFailure, +} + +enum StartOutcome { + Precommit { + attempt: Box, + cause: ResponseEgressOutcome, + }, + Started(Response), +} + +trait SpinEgressCommitter { + type Io: SpinEgressIo + 'static; + type Response; + + fn commit( + &mut self, + status: StatusCode, + headers: &HeaderMap, + transmits_body: bool, + ) -> Result<(Self::Response, Self::Io), DeliveryError>; + + fn spawn(&mut self, task: LocalBoxFuture<'static, ()>); +} + +#[async_trait::async_trait(?Send)] +trait SpinEgressIo { + fn abort(&mut self); + + async fn finish( + &mut self, + deadline: Deadline, + clock: &MonotonicClock, + ) -> Result<(), IoFailureKind>; + + async fn next_body( + &mut self, + source: &mut BodyStream, + deadline: Deadline, + clock: &MonotonicClock, + attempt: &mut ResponseEgressAttempt, + ) -> Result>, IoFailureKind>; + + async fn write( + &mut self, + bytes: Vec, + deadline: Deadline, + clock: &MonotonicClock, + ) -> Result; +} + +fn poll_egress_operation( + context: &mut Context<'_>, + mut timer: Pin<&mut Timer>, + response_result: &mut Option>>, + mut operation: Pin<&mut Operation>, +) -> Poll> +where + Timer: Future, + ResponseResult: Future>, + Operation: Future, +{ + if timer.as_mut().poll(context).is_ready() { + return Poll::Ready(EgressRace::Deadline); + } + let observed = + response_result + .as_mut() + .and_then(|future| match future.as_mut().poll(context) { + Poll::Pending => None, + Poll::Ready(result) => Some(result), + }); + if let Some(result) = observed { + // A successful host observation does not mean the response body has been drained. + // Retire it once, and keep the same source/write operation and deadline alive. + drop(response_result.take()); + if result.is_err() { + return Poll::Ready(EgressRace::ResponseFailure); + } + } + operation.as_mut().poll(context).map(EgressRace::Operation) +} + +async fn transmit_committed( + body: Body, + policy: ResponseEgressPolicy, + mut attempt: ResponseEgressAttempt, + clock: MonotonicClock, + kind: EgressKind, + mut io: Io, +) -> Result<(), DeliveryError> +where + Io: SpinEgressIo, +{ + let result = pump_body(body, policy.write_deadline, &mut attempt, &clock, &mut io).await; + match result { + Ok(()) => { + settle_success(kind, &mut attempt, clock.now()); + Ok(()) + } + Err(error) => { + abort(&mut io); + let observed_at = terminal_observed_at(error, policy.write_deadline, &clock); + settle_failure(kind, &mut attempt, error.outcome(), observed_at); + Err(error) + } + } +} + +async fn pump_body( + body: Body, + deadline: Deadline, + attempt: &mut ResponseEgressAttempt, + clock: &MonotonicClock, + io: &mut Io, +) -> Result<(), DeliveryError> +where + Io: SpinEgressIo, +{ + ensure_deadline(deadline, clock)?; + let mut source = body_source(body); + loop { + let observed = io.next_body(&mut source, deadline, clock, attempt).await; + if let Ok(Some(Err(error))) = &observed { + attempt.record_source_error(error); + } + let next = observed.map_err(IoFailureKind::delivery_error)?; + ensure_deadline(deadline, clock)?; + let Some(item) = next else { + break; + }; + let chunk = item.map_err(|error| { + log::warn!( + "response-egress Spin body source failed after commit: {}", + response_egress_source_error_category(&error) + ); + DeliveryError::Source + })?; + write_chunk(chunk.to_vec(), deadline, attempt, clock, io).await?; + } + io.finish(deadline, clock) + .await + .map_err(IoFailureKind::delivery_error)?; + ensure_deadline(deadline, clock) +} + +async fn write_chunk( + mut remaining: Vec, + deadline: Deadline, + attempt: &mut ResponseEgressAttempt, + clock: &MonotonicClock, + io: &mut Io, +) -> Result<(), DeliveryError> +where + Io: SpinEgressIo, +{ + while !remaining.is_empty() { + ensure_deadline(deadline, clock)?; + let requested = remaining.len(); + match io.write(remaining, deadline, clock).await { + Ok(progress) => { + account_prefix(progress.accepted, requested, attempt, clock.now())?; + if progress.remaining.len() != requested.saturating_sub(progress.accepted) { + return Err(DeliveryError::Transport); + } + if progress.accepted == 0 { + return Err(DeliveryError::Transport); + } + remaining = progress.remaining; + ensure_deadline(deadline, clock)?; + } + Err(failure) => { + account_prefix(failure.accepted, requested, attempt, clock.now())?; + return Err(failure.kind.delivery_error()); + } + } + } + Ok(()) +} + +fn account_prefix( + accepted: usize, + requested: usize, + attempt: &mut ResponseEgressAttempt, + observed_at: MonotonicInstant, +) -> Result<(), DeliveryError> { + if accepted > requested { + return Err(DeliveryError::Transport); + } + let accepted_u64 = u64::try_from(accepted).map_err(|_error| DeliveryError::Transport)?; + if !attempt.account_bytes(accepted_u64, observed_at) { + return Err(DeliveryError::Transport); + } + Ok(()) +} + +fn abort(io: &mut Io) +where + Io: SpinEgressIo, +{ + if catch_unwind(AssertUnwindSafe(|| io.abort())).is_err() { + log::error!("response-egress Spin abort panicked"); + } +} + +fn body_source(body: Body) -> BodyStream { + let (payload, _scope) = body.into_parts(); + match payload { + BodyPayload::Once(bytes) => stream::once(async move { Ok(bytes) }).boxed_local(), + BodyPayload::Stream(source) => source, + } +} + +fn ensure_deadline(deadline: Deadline, clock: &MonotonicClock) -> Result<(), DeliveryError> { + if deadline.is_expired_at(clock.now()) { + Err(DeliveryError::Deadline) + } else { + Ok(()) + } +} + +fn terminal_observed_at( + error: DeliveryError, + deadline: Deadline, + clock: &MonotonicClock, +) -> MonotonicInstant { + let observed_at = clock.now(); + if error == DeliveryError::Deadline { + observed_at.max(deadline.instant()) + } else { + observed_at + } +} + +fn settle_failure( + kind: EgressKind, + attempt: &mut ResponseEgressAttempt, + outcome: ResponseEgressOutcome, + observed_at: MonotonicInstant, +) { + match kind { + EgressKind::Application => { + attempt.terminate(outcome, observed_at); + } + EgressKind::Fallback => { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, observed_at); + } + } +} + +fn settle_success( + kind: EgressKind, + attempt: &mut ResponseEgressAttempt, + observed_at: MonotonicInstant, +) { + match kind { + EgressKind::Application => { + attempt.terminate(ResponseEgressOutcome::HostHandoff, observed_at); + } + EgressKind::Fallback => { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Completed, observed_at); + } + } +} + +#[cfg(target_arch = "wasm32")] +mod platform { + use futures_util::StreamExt as _; + + use super::{ + BodyStream, Bytes, Deadline, DeliveryError, EdgeError, EgressKind, EgressRace, HeaderMap, + IoFailure, IoFailureKind, LocalBoxFuture, MonotonicClock, ResponseEgressFramingMode, + SpinEgressCommitter, SpinEgressIo, StartOutcome, StatusCode, WriteProgress, + ensure_deadline, poll_egress_operation, start_fallback, start_prepared, + }; + use std::future::{Future, IntoFuture as _}; + use std::pin::Pin; + + use edgezero_core::response_egress::{ResponseEgressAttempt, ResponseEgressEnvelope}; + use futures_util::FutureExt as _; + use futures_util::future::poll_fn; + use spin_sdk::time::sleep; + use spin_sdk::wasip3::http::types::{ErrorCode, Fields}; + use spin_sdk::wasip3::http_compat::{BodyResult, BodyWriter}; + use spin_sdk::wasip3::spawn_local; + use spin_sdk::wit_bindgen::rt::async_support::{ + FutureRead, FutureWrite, FutureWriteCancel, FutureWriter, StreamResult, StreamWrite, + StreamWriter, + }; + + use crate::SpinResponse; + + type ResponseObservation = Option>>>>; + + const fn public_message(error: DeliveryError) -> &'static str { + match error { + DeliveryError::Conversion => "response conversion failed", + DeliveryError::Deadline => "response write deadline exceeded", + DeliveryError::Source => "response body source failed", + DeliveryError::Transport => "response transport failed", + } + } + + struct WasiCommitter; + + impl SpinEgressCommitter for WasiCommitter { + type Io = WasiEgressIo; + type Response = SpinResponse; + + fn commit( + &mut self, + status: StatusCode, + headers: &HeaderMap, + transmits_body: bool, + ) -> Result<(Self::Response, Self::Io), DeliveryError> { + let entries = headers + .iter() + .map(|(name, value)| (name.as_str().to_owned(), value.as_bytes().to_vec())) + .collect::>(); + let fields = Fields::from_list(&entries).map_err(|_error| DeliveryError::Conversion)?; + let (body_writer, contents, body_result) = BodyWriter::new(); + let BodyWriter { + stream_writer, + result_writer, + .. + } = body_writer; + let response_contents = transmits_body.then_some(contents); + let (response, response_result) = + SpinResponse::new(fields, response_contents, body_result); + response + .set_status_code(status.as_u16()) + .map_err(|()| DeliveryError::Conversion)?; + Ok(( + response, + WasiEgressIo { + response_result: Some(Box::pin(response_result.into_future())), + result_writer: Some(result_writer), + stream_writer: Some(stream_writer), + }, + )) + } + + fn spawn(&mut self, task: LocalBoxFuture<'static, ()>) { + spawn_local(task); + } + } + + struct WasiEgressIo { + response_result: ResponseObservation, + result_writer: Option>, + stream_writer: Option>, + } + + async fn race_operation( + deadline: Deadline, + clock: &MonotonicClock, + response_result: &mut ResponseObservation, + mut operation: Pin<&mut Operation>, + ) -> EgressRace + where + Operation: Future, + { + let Some(remaining) = deadline.remaining_at(clock.now()) else { + return EgressRace::Deadline; + }; + let timer = sleep(remaining); + futures_util::pin_mut!(timer); + poll_fn(|context| { + poll_egress_operation(context, timer.as_mut(), response_result, operation.as_mut()) + }) + .await + } + + impl WasiEgressIo { + fn cancel_response_result(&mut self) { + if let Some(mut result) = self.response_result.take() { + drop(result.as_mut().cancel()); + } + } + + fn cancelled_write( + write: Pin<&mut StreamWrite<'_, u8>>, + requested: usize, + kind: IoFailureKind, + ) -> IoFailure { + let (_status, buffer) = write.cancel(); + IoFailure { + accepted: requested.saturating_sub(buffer.remaining()), + kind, + } + } + } + + #[async_trait::async_trait(?Send)] + impl SpinEgressIo for WasiEgressIo { + fn abort(&mut self) { + drop(self.stream_writer.take()); + drop(self.result_writer.take()); + } + + async fn finish( + &mut self, + deadline: Deadline, + clock: &MonotonicClock, + ) -> Result<(), IoFailureKind> { + ensure_deadline(deadline, clock).map_err(|_error| IoFailureKind::Deadline)?; + drop(self.stream_writer.take()); + let result_writer = self.result_writer.take().ok_or(IoFailureKind::Transport)?; + let result_write = result_writer.write(Ok(None)); + futures_util::pin_mut!(result_write); + let event = race_operation( + deadline, + clock, + &mut self.response_result, + result_write.as_mut(), + ) + .await; + match event { + EgressRace::Deadline => { + drop(cancel_result_write(result_write.as_mut())); + self.cancel_response_result(); + return Err(IoFailureKind::Deadline); + } + EgressRace::ResponseFailure => { + drop(cancel_result_write(result_write.as_mut())); + return Err(IoFailureKind::Transport); + } + EgressRace::Operation(Ok(())) => {} + EgressRace::Operation(Err(_error)) => return Err(IoFailureKind::Transport), + } + if deadline.is_expired_at(clock.now()) { + self.cancel_response_result(); + return Err(IoFailureKind::Deadline); + } + + let Some(mut response_result) = self.response_result.take() else { + return Ok(()); + }; + let mut no_observer = None; + match race_operation(deadline, clock, &mut no_observer, response_result.as_mut()).await + { + EgressRace::Deadline => { + drop(response_result.as_mut().cancel()); + Err(IoFailureKind::Deadline) + } + EgressRace::Operation(Ok(())) if !deadline.is_expired_at(clock.now()) => Ok(()), + EgressRace::Operation(Ok(()) | Err(_)) => { + if deadline.is_expired_at(clock.now()) { + Err(IoFailureKind::Deadline) + } else { + Err(IoFailureKind::Transport) + } + } + EgressRace::ResponseFailure => Err(IoFailureKind::Transport), + } + } + + async fn next_body( + &mut self, + source: &mut BodyStream, + deadline: Deadline, + clock: &MonotonicClock, + attempt: &mut ResponseEgressAttempt, + ) -> Result>, IoFailureKind> { + ensure_deadline(deadline, clock).map_err(|_error| IoFailureKind::Deadline)?; + let next = source.next().inspect(move |item| { + if let Some(Err(error)) = item { + attempt.record_source_error(error); + } + }); + futures_util::pin_mut!(next); + match race_operation(deadline, clock, &mut self.response_result, next.as_mut()).await { + EgressRace::Deadline => { + self.cancel_response_result(); + Err(IoFailureKind::Deadline) + } + EgressRace::ResponseFailure => Err(IoFailureKind::Transport), + EgressRace::Operation(item) if deadline.is_expired_at(clock.now()) => { + drop(item); + self.cancel_response_result(); + Err(IoFailureKind::Deadline) + } + EgressRace::Operation(item) => Ok(item), + } + } + + async fn write( + &mut self, + bytes: Vec, + deadline: Deadline, + clock: &MonotonicClock, + ) -> Result { + if ensure_deadline(deadline, clock).is_err() { + return Err(IoFailure { + accepted: 0, + kind: IoFailureKind::Deadline, + }); + } + let requested = bytes.len(); + let (result, cancel_response) = { + let stream_writer = self.stream_writer.as_mut().ok_or(IoFailure { + accepted: 0, + kind: IoFailureKind::Transport, + })?; + let write = stream_writer.write(bytes); + futures_util::pin_mut!(write); + let event = + race_operation(deadline, clock, &mut self.response_result, write.as_mut()) + .await; + match event { + EgressRace::Deadline => ( + Err(Self::cancelled_write( + write.as_mut(), + requested, + IoFailureKind::Deadline, + )), + true, + ), + EgressRace::ResponseFailure => ( + Err(Self::cancelled_write( + write.as_mut(), + requested, + IoFailureKind::Transport, + )), + false, + ), + EgressRace::Operation((status, buffer)) => { + let accepted = requested.saturating_sub(buffer.remaining()); + if deadline.is_expired_at(clock.now()) { + ( + Err(IoFailure { + accepted, + kind: IoFailureKind::Deadline, + }), + true, + ) + } else { + let result = match status { + StreamResult::Complete(_) => Ok(WriteProgress { + accepted, + remaining: buffer.into_vec(), + }), + StreamResult::Cancelled | StreamResult::Dropped => Err(IoFailure { + accepted, + kind: IoFailureKind::Transport, + }), + }; + (result, false) } - collected.extend_from_slice(&bytes); } - Err(err) => return Err(EdgeError::internal(err)), } + }; + if cancel_response { + self.cancel_response_result(); } - Ok(collected) + result } } + + fn cancel_result_write( + write: Pin<&mut FutureWrite>, + ) -> Option> { + match write.cancel() { + FutureWriteCancel::AlreadySent | FutureWriteCancel::Dropped(_) => None, + FutureWriteCancel::Cancelled(_value, writer) => Some(writer), + } + } + + #[cfg(feature = "test-utils")] + mod diagnostic_tests { + use super::*; + use core::time::Duration; + use edgezero_core::error::BadGatewayReason; + use edgezero_core::error_detail::{ + ProviderFailureDetail, ProviderFailurePhase, SpinFailureCause, + }; + use edgezero_core::http::Version; + use edgezero_core::response_egress::{ + ResponseEgressBodyKind, ResponseEgressCompletion, ResponseEgressHead, + ResponseEgressObserverHandle, ResponseEgressOutcome, ResponseEgressReport, + }; + use edgezero_core::time::MonotonicInstant; + use futures_util::stream; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex, PoisonError}; + + #[derive(Clone, Copy, Eq, PartialEq)] + pub(super) enum SourceCase { + AlreadyExpired, + Eof, + Ready, + ReadyExpired, + } + + fn attempt( + now: MonotonicInstant, + clock: MonotonicClock, + ) -> (ResponseEgressAttempt, Arc>>) { + let reports = Arc::new(Mutex::new(Vec::new())); + let completed = Arc::clone(&reports); + let headers = HeaderMap::new(); + let head = ResponseEgressHead::new( + StatusCode::OK, + Version::HTTP_11, + &headers, + None, + now, + None, + ); + ( + ResponseEgressAttempt::new( + &head, + now, + ResponseEgressCompletion::new(move |report| { + completed + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push(report.clone()); + }), + ResponseEgressObserverHandle::default(), + clock, + ), + reports, + ) + } + + pub(super) async fn check_source_case( + case: SourceCase, + with_detail: bool, + ) -> Result<(), EdgeError> { + let now = MonotonicInstant::now(); + let deadline = now + .checked_add(Duration::from_secs(10)) + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("test deadline overflow")))?; + let expired = Arc::new(AtomicBool::new(case == SourceCase::AlreadyExpired)); + let clock_expired = Arc::clone(&expired); + let clock = MonotonicClock::new(move || { + if clock_expired.load(Ordering::SeqCst) { + deadline + } else { + now + } + }); + let polls = Arc::new(AtomicUsize::new(0)); + let source_polls = Arc::clone(&polls); + let detail = ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: SpinFailureCause::ConnectionTerminated, + }; + let mut source = if case == SourceCase::Eof { + stream::empty().boxed_local() + } else { + stream::once(async move { + source_polls.fetch_add(1, Ordering::SeqCst); + expired.store(case == SourceCase::ReadyExpired, Ordering::SeqCst); + Err(if with_detail { + EdgeError::bad_gateway_with_detail( + "provider-secret-token", + BadGatewayReason::Transport, + detail, + ) + } else { + EdgeError::bad_gateway_with_reason( + "provider-secret-token", + BadGatewayReason::Transport, + ) + }) + }) + .boxed_local() + }; + let (mut attempt, reports) = attempt(now, clock.clone()); + if !attempt.begin_writing() { + return Err(EdgeError::internal(anyhow::anyhow!( + "test writing state failed" + ))); + } + let mut io = WasiEgressIo { + response_result: None, + result_writer: None, + stream_writer: None, + }; + let result = io + .next_body( + &mut source, + Deadline::at_instant(deadline), + &clock, + &mut attempt, + ) + .await; + let expected = match case { + SourceCase::AlreadyExpired | SourceCase::ReadyExpired => { + ResponseEgressOutcome::DeadlineExceeded + } + SourceCase::Eof => ResponseEgressOutcome::Completed, + SourceCase::Ready => ResponseEgressOutcome::SourceError, + }; + let actual = match result { + Ok(Some(Err(_))) => ResponseEgressOutcome::SourceError, + Ok(None) => ResponseEgressOutcome::Completed, + Err(IoFailureKind::Deadline) => ResponseEgressOutcome::DeadlineExceeded, + _ => { + return Err(EdgeError::internal(anyhow::anyhow!( + "unexpected test read result" + ))); + } + }; + if actual != expected || !attempt.terminate(actual, clock.now()) { + return Err(EdgeError::internal(anyhow::anyhow!( + "test terminal outcome changed" + ))); + } + drop(attempt); + let retained = reports.lock().unwrap_or_else(PoisonError::into_inner); + let report = retained.first().ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("test terminal report missing")) + })?; + let observed = matches!(case, SourceCase::Ready | SourceCase::ReadyExpired); + if retained.len() != 1 + || report.bytes_written != 0 + || report.source_failure.is_some() != observed + || polls.load(Ordering::SeqCst) != usize::from(observed) + { + return Err(EdgeError::internal(anyhow::anyhow!( + "test source observation changed" + ))); + } + check_report(report, with_detail.then_some(&detail)) + } + + fn check_report( + report: &ResponseEgressReport, + expected: Option<&ProviderFailureDetail>, + ) -> Result<(), EdgeError> { + if let Some(failure) = report.source_failure + && (failure.body_kind() != ResponseEgressBodyKind::Application + || failure.category() != "bad_gateway_transport" + || failure.provider_failure_detail() != expected) + { + return Err(EdgeError::internal(anyhow::anyhow!( + "test source diagnostic changed" + ))); + } + Ok(()) + } + } + + /// Exercises the production source-read race without creating a downstream writer. + /// + /// # Errors + /// Returns an internal test error if observation or terminal behavior changes. + #[cfg(feature = "test-utils")] + #[doc(hidden)] + #[inline] + pub(super) async fn response_source_diagnostics_hold_for_test() -> Result<(), EdgeError> { + use diagnostic_tests::{SourceCase, check_source_case}; + for case in [ + SourceCase::AlreadyExpired, + SourceCase::Eof, + SourceCase::Ready, + SourceCase::ReadyExpired, + ] { + for with_detail in [false, true] { + check_source_case(case, with_detail).await?; + } + } + Ok(()) + } + + /// Converts an owned response-egress envelope into a raw `WASIp3` response and spawns the sole + /// body/transmission coordinator. + /// + /// # Errors + /// Returns a category-only error if both the application response and bounded precommit fallback + /// fail before the raw response is returned to the Spin export. + pub(crate) fn from_egress_response( + egress: ResponseEgressEnvelope, + ) -> Result { + let request_method = egress.request_method().clone(); + let mut committer = WasiCommitter; + let result = match egress.begin(ResponseEgressFramingMode::ExplicitBufferedLength) { + Ok((prepared, policy, attempt, clock)) => { + match start_prepared( + prepared, + policy, + attempt, + clock.clone(), + EgressKind::Application, + &mut committer, + ) { + StartOutcome::Started(response) => Ok(response), + StartOutcome::Precommit { + attempt: failed_attempt, + cause, + } => start_fallback( + &request_method, + cause, + *failed_attempt, + &clock, + &mut committer, + ), + } + } + Err(failure) => { + let (attempt, clock, cause) = failure.into_parts(); + start_fallback(&request_method, cause, attempt, &clock, &mut committer) + } + }; + result.map_err(|error| EdgeError::internal(anyhow::anyhow!(public_message(error)))) + } } -/// Convert an `EdgeZero` core `Response` into a Spin SDK `Response`. -/// -/// Both `Body::Once` and `Body::Stream` are converted to a buffered -/// byte body. Streaming bodies are collected into a single `Vec`. +#[cfg(target_arch = "wasm32")] +pub(crate) use platform::from_egress_response; +#[cfg(all(feature = "spin", target_arch = "wasm32", feature = "test-utils"))] +#[doc(hidden)] +#[inline] +/// Exercises the SDK response-read race and verifies bounded observation metadata. /// /// # Errors -/// Returns [`EdgeError::internal`] if the response body cannot be collected -/// (stream error or size cap exceeded) or if the resulting Spin response -/// cannot be built from the collected bytes. -#[inline] -pub async fn from_core_response(response: Response) -> Result { +/// Returns an internal test error if an expected observation changes. +pub async fn response_source_diagnostics_hold_for_test() -> Result<(), EdgeError> { + platform::response_source_diagnostics_hold_for_test().await +} + +fn start_prepared( + prepared: PreparedResponseEgress, + policy: ResponseEgressPolicy, + mut attempt: ResponseEgressAttempt, + clock: MonotonicClock, + kind: EgressKind, + committer: &mut Committer, +) -> StartOutcome +where + Committer: SpinEgressCommitter, +{ + if policy.write_deadline.is_expired_at(clock.now()) { + return StartOutcome::Precommit { + attempt: Box::new(attempt), + cause: ResponseEgressOutcome::DeadlineExceeded, + }; + } + let transmits_body = prepared.transmits_body(); + let response = prepared.into_response(); let (parts, body) = response.into_parts(); + let (platform_response, mut io) = + match committer.commit(parts.status, &parts.headers, transmits_body) { + Ok(pair) => pair, + Err(error) => { + return StartOutcome::Precommit { + attempt: Box::new(attempt), + cause: error.outcome(), + }; + } + }; + if policy.write_deadline.is_expired_at(clock.now()) { + abort(&mut io); + return StartOutcome::Precommit { + attempt: Box::new(attempt), + cause: ResponseEgressOutcome::DeadlineExceeded, + }; + } + if !attempt.begin_writing() { + abort(&mut io); + return StartOutcome::Precommit { + attempt: Box::new(attempt), + cause: ResponseEgressOutcome::ConversionError, + }; + } + let task = AssertUnwindSafe(transmit_committed(body, policy, attempt, clock, kind, io)) + .catch_unwind() + .map(|result| { + if result.is_err() { + log::error!("response-egress Spin coordinator panicked"); + } + }) + .boxed_local(); + committer.spawn(task); + StartOutcome::Started(platform_response) +} + +fn start_fallback( + request_method: &Method, + original_cause: ResponseEgressOutcome, + mut attempt: ResponseEgressAttempt, + clock: &MonotonicClock, + committer: &mut Committer, +) -> Result +where + Committer: SpinEgressCommitter, +{ + if !attempt.begin_fallback(original_cause) { + attempt.terminate(ResponseEgressOutcome::ConversionError, clock.now()); + return Err(DeliveryError::Conversion); + } + let started_at = clock.now(); + let fallback_deadline = started_at + .checked_add(RESPONSE_EGRESS_FALLBACK_SAFETY_BUDGET) + .unwrap_or(started_at); + let prepared = attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + request_method, + fallback_response(original_cause), + ) + .map_err(|_error| { + attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, clock.now()); + DeliveryError::from_outcome(original_cause) + })?; + match start_prepared( + prepared, + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(fallback_deadline), + }, + attempt, + clock.clone(), + EgressKind::Fallback, + committer, + ) { + StartOutcome::Started(response) => Ok(response), + StartOutcome::Precommit { + attempt: mut failed_attempt, + cause: _fallback_cause, + } => { + failed_attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, clock.now()); + Err(DeliveryError::from_outcome(original_cause)) + } + } +} + +fn fallback_response(cause: ResponseEgressOutcome) -> Response { + let (status, body) = if cause == ResponseEgressOutcome::DeadlineExceeded { + (StatusCode::GATEWAY_TIMEOUT, TIMEOUT_FALLBACK_BODY) + } else { + (StatusCode::INTERNAL_SERVER_ERROR, INTERNAL_FALLBACK_BODY) + }; + let mut response = Response::new(Body::from(body)); + *response.status_mut() = status; + response +} + +#[cfg(test)] +mod race_tests { + use super::{EgressRace, poll_egress_operation}; + use futures_util::future::poll_fn; + use std::cell::Cell; + use std::future::{pending, ready}; + use std::task::{Context, Poll, Waker}; + #[test] + fn early_host_success_preserves_later_deadline_and_operation_failure() { + let mut context = Context::from_waker(Waker::noop()); + let mut timer = Box::pin(pending::<()>()); + let mut observer = Some(Box::pin(ready(Ok::<(), u8>(())))); + let mut pending_operation = Box::pin(pending::>()); + assert!( + poll_egress_operation( + &mut context, + timer.as_mut(), + &mut observer, + pending_operation.as_mut() + ) + .is_pending() + ); + let mut failed = Box::pin(ready(Err::<(), u8>(9))); + assert!(matches!( + poll_egress_operation(&mut context, timer.as_mut(), &mut observer, failed.as_mut()), + Poll::Ready(EgressRace::Operation(Err(9))) + )); + let mut expired = Box::pin(ready(())); + let mut ready_operation = Box::pin(ready(Ok::<(), u8>(()))); + assert!(matches!( + poll_egress_operation( + &mut context, + expired.as_mut(), + &mut observer, + ready_operation.as_mut() + ), + Poll::Ready(EgressRace::Deadline) + )); + } + + #[test] + fn host_failure_still_preempts_work_and_deadline_still_has_priority() { + let mut context = Context::from_waker(Waker::noop()); + let mut timer = Box::pin(pending::<()>()); + let mut observer = Some(Box::pin(ready(Err::<(), u8>(1)))); + let mut operation = Box::pin(poll_fn(|_| -> Poll<()> { + panic!("failed observer must preempt operation") + })); + assert!(matches!( + poll_egress_operation( + &mut context, + timer.as_mut(), + &mut observer, + operation.as_mut() + ), + Poll::Ready(EgressRace::ResponseFailure) + )); + assert!(observer.is_none()); + let mut unpolled_observer = Some(Box::pin(ready(Err::<(), u8>(1)))); + let mut expired = Box::pin(ready(())); + assert!(matches!( + poll_egress_operation( + &mut context, + expired.as_mut(), + &mut unpolled_observer, + operation.as_mut() + ), + Poll::Ready(EgressRace::Deadline) + )); + assert!( + unpolled_observer.is_some(), + "deadline must win before polling the host result" + ); + } + + #[test] + fn successful_early_host_handoff_does_not_abort_pending_body_work() { + let mut context = Context::from_waker(Waker::noop()); + let mut timer = Box::pin(pending::<()>()); + let mut result = Some(Box::pin(ready(Ok::<(), u8>(())))); + let polls = Cell::new(0_u8); + let mut operation = Box::pin(poll_fn(|_| { + polls.set(polls.get() + 1); + if polls.get() == 1 { + Poll::Pending + } else { + Poll::Ready(7_u8) + } + })); + assert!( + poll_egress_operation( + &mut context, + timer.as_mut(), + &mut result, + operation.as_mut() + ) + .is_pending() + ); + assert!( + result.is_none(), + "completed host result must not be polled again" + ); + assert!(matches!( + poll_egress_operation( + &mut context, + timer.as_mut(), + &mut result, + operation.as_mut() + ), + Poll::Ready(EgressRace::Operation(7)) + )); + assert_eq!( + polls.get(), + 2, + "the original pending operation must be resumed" + ); + } +} + +#[cfg(test)] +mod tests { + mod handoff; + + use edgezero_core::response_egress_framing::prepare_response_egress; + use std::cell::{Cell, RefCell}; + use std::collections::VecDeque; + use std::future::pending; + use std::rc::Rc; + use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll}; + + use super::*; + use edgezero_core::http::{HeaderMap, Method, Version, response_builder}; + use edgezero_core::response_egress::{ + ResponseEgressBodyKind, ResponseEgressCompletion, ResponseEgressHead, + ResponseEgressObserver, ResponseEgressObserverHandle, ResponseEgressReport, + }; + use futures::executor::block_on; + use futures_util::future::{LocalBoxFuture, poll_fn}; + use futures_util::stream; + use futures_util::task::noop_waker_ref; + + #[derive(Clone, Default)] + struct RecordingObserver(Arc>>); + + impl RecordingObserver { + fn reports(&self) -> Vec { + self.0.lock().expect("reports lock").clone() + } + } + + impl ResponseEgressObserver for RecordingObserver { + fn complete(&self, report: &ResponseEgressReport) { + self.0.lock().expect("reports lock").push(report.clone()); + } + } + + #[derive(Clone, Copy)] + enum MockWrite { + Accept(usize), + Fail(IoFailureKind, usize), + } + + #[derive(Default)] + struct MockState { + aborts: usize, + accepted: Vec, + drops: usize, + events: Vec<&'static str>, + finish: Option, + next_body: Option, + pending: Option, + writes: VecDeque, + } + + struct MockIo(Rc>); + + #[derive(Clone, Copy, Eq, PartialEq)] + enum PendingAt { + Finish, + Source, + Write, + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for MockIo { + fn drop(&mut self) { + let drops = self.0.borrow().drops; + self.0.borrow_mut().drops = drops.saturating_add(1); + } + } + + #[derive(Clone)] + struct MockResponse { + headers: HeaderMap, + status: StatusCode, + transmits_body: bool, + } + + struct MockCommitter { + io: Rc>, + responses: Rc>>, + tasks: VecDeque>, + } + + impl MockCommitter { + fn new(io: Rc>) -> Self { + Self { + io, + responses: Rc::new(RefCell::new(Vec::new())), + tasks: VecDeque::new(), + } + } + + fn run_task(&mut self) { + let task = self.tasks.pop_front().expect("spawned task"); + block_on(task); + } + } + + impl SpinEgressCommitter for MockCommitter { + type Io = MockIo; + type Response = MockResponse; + + fn commit( + &mut self, + status: StatusCode, + headers: &HeaderMap, + transmits_body: bool, + ) -> Result<(Self::Response, Self::Io), DeliveryError> { + let response = MockResponse { + headers: headers.clone(), + status, + transmits_body, + }; + self.responses.borrow_mut().push(response.clone()); + Ok((response, MockIo(Rc::clone(&self.io)))) + } + + fn spawn(&mut self, task: LocalBoxFuture<'static, ()>) { + self.tasks.push_back(task); + } + } + + #[async_trait::async_trait(?Send)] + impl SpinEgressIo for MockIo { + fn abort(&mut self) { + let mut state = self.0.borrow_mut(); + state.aborts = state.aborts.saturating_add(1); + state.events.push("abort"); + } + + async fn finish( + &mut self, + _deadline: Deadline, + _clock: &MonotonicClock, + ) -> Result<(), IoFailureKind> { + let should_wait = { + let mut state = self.0.borrow_mut(); + state.events.push("finish"); + state.pending == Some(PendingAt::Finish) + }; + if should_wait { + poll_fn(|_cx| { + if self.0.borrow().pending == Some(PendingAt::Finish) { + Poll::Pending + } else { + Poll::Ready(()) + } + }) + .await; + } + self.0.borrow().finish.map_or(Ok(()), Err) + } + + async fn next_body( + &mut self, + source: &mut BodyStream, + _deadline: Deadline, + _clock: &MonotonicClock, + _attempt: &mut ResponseEgressAttempt, + ) -> Result>, IoFailureKind> { + let (should_wait, failure) = { + let mut state = self.0.borrow_mut(); + state.events.push("source"); + (state.pending == Some(PendingAt::Source), state.next_body) + }; + if should_wait { + pending::<()>().await; + } + if let Some(next_failure) = failure { + return Err(next_failure); + } + Ok(source.next().await) + } + + async fn write( + &mut self, + mut bytes: Vec, + _deadline: Deadline, + _clock: &MonotonicClock, + ) -> Result { + let (plan, should_wait) = { + let mut state = self.0.borrow_mut(); + state.events.push("write"); + ( + state.writes.pop_front(), + state.pending == Some(PendingAt::Write), + ) + }; + if should_wait { + pending::<()>().await; + } + let (accepted, failure) = match plan { + Some(MockWrite::Accept(accepted)) => (accepted, None), + Some(MockWrite::Fail(kind, accepted)) => (accepted, Some(kind)), + None => (bytes.len(), None), + }; + let remaining = if accepted <= bytes.len() { + let remaining = bytes.split_off(accepted); + self.0.borrow_mut().accepted.extend_from_slice(&bytes); + remaining + } else { + Vec::new() + }; + let progress = WriteProgress { + accepted, + remaining, + }; + match failure { + Some(kind) => Err(IoFailure { accepted, kind }), + None => Ok(progress), + } + } + } + + fn stable_clock(now: MonotonicInstant) -> MonotonicClock { + MonotonicClock::new(move || now) + } + + fn future_policy(now: MonotonicInstant) -> ResponseEgressPolicy { + ResponseEgressPolicy { + write_deadline: Deadline::at_instant( + now.checked_add(Duration::from_secs(10)).expect("deadline"), + ), + } + } + + fn new_attempt(observer: &RecordingObserver, now: MonotonicInstant) -> ResponseEgressAttempt { + let headers = HeaderMap::new(); + let head = + ResponseEgressHead::new(StatusCode::OK, Version::HTTP_11, &headers, None, now, None); + ResponseEgressAttempt::new( + &head, + now, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + ) + } - let mut builder = SpinResponse::builder().status(parts.status); + fn attempt(observer: &RecordingObserver, now: MonotonicInstant) -> ResponseEgressAttempt { + let mut attempt = new_attempt(observer, now); + assert!(attempt.begin_writing()); + attempt + } + + fn transmit( + body: Body, + state: Rc>, + ) -> (Result<(), DeliveryError>, Vec) { + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let result = block_on(transmit_committed( + body, + future_policy(now), + attempt(&observer, now), + stable_clock(now), + EgressKind::Application, + MockIo(state), + )); + (result, observer.reports()) + } + + #[test] + fn coordinator_streams_once_and_multiple_chunks_with_partial_writes() { + for (body, writes, expected_bytes) in [ + (Body::empty(), VecDeque::new(), 0_u64), + ( + Body::from("hello"), + VecDeque::from([MockWrite::Accept(2), MockWrite::Accept(3)]), + 5, + ), + ( + Body::stream(stream::iter([ + Bytes::from_static(b"ab"), + Bytes::from_static(b"cd"), + ])), + VecDeque::new(), + 4, + ), + ] { + let state = Rc::new(RefCell::new(MockState { + writes, + ..MockState::default() + })); + + let (result, reports) = transmit(body, Rc::clone(&state)); - for (name, value) in &parts.headers { - builder = builder.header(name, value); + assert_eq!(result, Ok(())); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::HostHandoff); + assert_eq!(reports[0].bytes_written, expected_bytes); + assert_eq!(state.borrow().aborts, 0); + assert_eq!(state.borrow().events.last(), Some(&"finish")); + } } - let collected = collect_body_bytes(body).await?; + #[test] + fn cancellation_accounts_the_accepted_prefix_before_deadline_wins() { + let state = Rc::new(RefCell::new(MockState { + writes: VecDeque::from([MockWrite::Fail(IoFailureKind::Deadline, 2)]), + ..MockState::default() + })); + + let (result, reports) = transmit(Body::from("hello"), Rc::clone(&state)); - builder - .body(FullBody::new(Bytes::from(collected))) - .map_err(|err| EdgeError::internal(anyhow::anyhow!("failed to build response: {err}"))) + assert_eq!(result, Err(DeliveryError::Deadline)); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert_eq!(reports[0].bytes_written, 2); + assert_eq!(reports[0].elapsed, Duration::from_secs(10)); + assert_eq!(state.borrow().aborts, 1); + } + + #[test] + fn generated_overflow_retry_after_precedes_synthesized_headers() { + use edgezero_core::RequestBodyOverflowOrigin; + use edgezero_core::response_egress_framing::test_support::synthesized_error_fixture; + for (error, retry) in [ + ( + EdgeError::payload_too_large(RequestBodyOverflowOrigin::Inbound), + None, + ), + ( + EdgeError::payload_too_large(RequestBodyOverflowOrigin::OutboundUpload), + None, + ), + ( + EdgeError::config_out_of_date("private", "field"), + Some("60"), + ), + ] { + let envelope = synthesized_error_fixture(error).expect("fixture"); + let Ok((prepared, policy, attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("prepared error"); + }; + let state = Rc::new(RefCell::new(MockState::default())); + let mut committer = MockCommitter::new(state); + let StartOutcome::Started(response) = start_prepared( + prepared, + policy, + attempt, + clock, + EgressKind::Application, + &mut committer, + ) else { + panic!("started error"); + }; + committer.run_task(); + assert_eq!(response.headers["x-synthesized-fixture"], "configured"); + assert_eq!( + response + .headers + .get("retry-after") + .map(|value| value.to_str().expect("text")), + retry + ); + } + } + + #[test] + fn framing_observer_preserves_each_typed_reason() { + use edgezero_core::response_egress_framing::test_support::{ + FRAMING_RULES, framing_fixture, has_full_synthesized_fixture_headers, + }; + for reason in FRAMING_RULES { + for detached in [false, true] { + let fixture = block_on(framing_fixture(reason, detached)).expect("fixture"); + let method = fixture.envelope.request_method().clone(); + let Err(failure) = fixture + .envelope + .begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("framing rejection") + }; + let (attempt, clock, cause) = failure.into_parts(); + let state = Rc::new(RefCell::new(MockState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + let response = start_fallback(&method, cause, attempt, &clock, &mut committer) + .expect("fallback"); + assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(response.headers["x-synthesized-fixture"], "configured"); + assert!(has_full_synthesized_fixture_headers(&response.headers)); + committer.run_task(); + assert_eq!(state.borrow().accepted, INTERNAL_FALLBACK_BODY); + let completed = fixture.completed.lock().expect("completion"); + assert_eq!(completed.len(), 1); + assert_eq!(completed[0].framing_failures.application(), Some(reason)); + assert_eq!(completed[0].framing_failures.fallback(), None); + assert_eq!(completed[0].outcome, ResponseEgressOutcome::ConversionError); + let observed = fixture.observed.lock().expect("observer"); + if detached { + assert!(observed.is_empty()); + } else { + assert_eq!(*observed, *completed); + } + } + } + } + + #[test] + fn provider_diagnostics_reach_response_observer_before_deadline_arbitration() { + use edgezero_core::error::BadGatewayReason; + use edgezero_core::error_detail::{ + ProviderFailureDetail, ProviderFailurePhase, SpinFailureCause, + }; + use std::sync::atomic::{AtomicBool, Ordering}; + + let detail = ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: SpinFailureCause::ConnectionTerminated, + }; + for with_detail in [false, true] { + for late in [false, true] { + let now = MonotonicInstant::now(); + let policy = future_policy(now); + let deadline = policy.write_deadline.instant(); + let expired = Arc::new(AtomicBool::new(false)); + let clock_expired = Arc::clone(&expired); + let clock = MonotonicClock::new(move || { + if clock_expired.load(Ordering::SeqCst) { + deadline + } else { + now + } + }); + let error = if with_detail { + EdgeError::bad_gateway_with_detail( + "provider-secret-token", + BadGatewayReason::Transport, + detail, + ) + } else { + EdgeError::bad_gateway_with_reason( + "provider-secret-token", + BadGatewayReason::Transport, + ) + }; + let body = Body::from_stream(stream::once(async move { + expired.store(late, Ordering::SeqCst); + Err(error) + })); + let observer = RecordingObserver::default(); + let state = Rc::new(RefCell::new(MockState::default())); + let result = block_on(transmit_committed( + body, + policy, + attempt(&observer, now), + clock, + EgressKind::Application, + MockIo(Rc::clone(&state)), + )); + assert!(result.is_err()); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!( + reports[0].outcome, + if late { + ResponseEgressOutcome::DeadlineExceeded + } else { + ResponseEgressOutcome::SourceError + } + ); + let failure = reports[0] + .source_failure + .as_ref() + .expect("source observation"); + assert_eq!(reports[0].framing_failures.application(), None); + assert_eq!(reports[0].framing_failures.fallback(), None); + assert_eq!(failure.category(), "bad_gateway_transport"); + assert_eq!( + failure.provider_failure_detail(), + with_detail.then_some(&detail) + ); + assert!(!format!("{failure:?}").contains("secret")); + assert_eq!(state.borrow().aborts, 1); + } + } + } + + #[test] + fn zero_progress_source_error_and_finish_error_each_abort_once() { + let zero_state = Rc::new(RefCell::new(MockState { + writes: VecDeque::from([MockWrite::Accept(0)]), + ..MockState::default() + })); + let (zero_result, zero_reports) = transmit(Body::from("x"), Rc::clone(&zero_state)); + assert_eq!(zero_result, Err(DeliveryError::Transport)); + assert_eq!( + zero_reports[0].outcome, + ResponseEgressOutcome::TransportError + ); + assert_eq!(zero_state.borrow().aborts, 1); + + let source_state = Rc::new(RefCell::new(MockState::default())); + let source_body = Body::from_stream(stream::iter([Err(EdgeError::internal( + anyhow::anyhow!("private source detail"), + ))])); + let (source_result, source_reports) = transmit(source_body, Rc::clone(&source_state)); + assert_eq!(source_result, Err(DeliveryError::Source)); + assert_eq!( + source_reports[0].outcome, + ResponseEgressOutcome::SourceError + ); + assert_eq!(source_state.borrow().aborts, 1); + + let finish_state = Rc::new(RefCell::new(MockState { + finish: Some(IoFailureKind::Transport), + ..MockState::default() + })); + let (finish_result, finish_reports) = transmit(Body::empty(), Rc::clone(&finish_state)); + assert_eq!(finish_result, Err(DeliveryError::Transport)); + assert_eq!( + finish_reports[0].outcome, + ResponseEgressOutcome::TransportError + ); + assert_eq!(finish_state.borrow().aborts, 1); + } + + #[test] + fn fallback_uses_its_own_budget_and_preserves_original_cause() { + let now = MonotonicInstant::now(); + let clock = stable_clock(now); + let observer = RecordingObserver::default(); + let state = Rc::new(RefCell::new(MockState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + + let fallback = start_fallback( + &Method::GET, + ResponseEgressOutcome::ConversionError, + new_attempt(&observer, now), + &clock, + &mut committer, + ) + .expect("fallback response"); + + assert_eq!(fallback.status, StatusCode::INTERNAL_SERVER_ERROR); + assert!(fallback.transmits_body); + committer.run_task(); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::ConversionError); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Completed) + ); + assert_eq!(state.borrow().aborts, 0); + assert_eq!(state.borrow().accepted, INTERNAL_FALLBACK_BODY); + assert_eq!( + reports[0].bytes_written, + u64::try_from(INTERNAL_FALLBACK_BODY.len()).expect("fallback length") + ); + } + + #[test] + fn fallback_failures_abort_once_and_preserve_original_cause() { + for (writes, finish, next_body, expected_bytes) in [ + ( + VecDeque::from([ + MockWrite::Accept(3), + MockWrite::Fail(IoFailureKind::Transport, 0), + ]), + None, + None, + 3, + ), + ( + VecDeque::new(), + Some(IoFailureKind::Transport), + None, + u64::try_from(INTERNAL_FALLBACK_BODY.len()).expect("fallback length"), + ), + (VecDeque::new(), None, Some(IoFailureKind::Deadline), 0), + ] { + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let state = Rc::new(RefCell::new(MockState { + finish, + next_body, + writes, + ..MockState::default() + })); + let mut committer = MockCommitter::new(Rc::clone(&state)); + let clock = stable_clock(now); + + start_fallback( + &Method::GET, + ResponseEgressOutcome::ConversionError, + new_attempt(&observer, now), + &clock, + &mut committer, + ) + .expect("fallback response"); + committer.run_task(); + + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::ConversionError); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Aborted) + ); + assert_eq!(reports[0].bytes_written, expected_bytes); + assert_eq!(state.borrow().aborts, 1); + } + } + + #[test] + fn head_fallback_suppresses_body_without_writing() { + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let state = Rc::new(RefCell::new(MockState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + + let clock = stable_clock(now); + let fallback = start_fallback( + &Method::HEAD, + ResponseEgressOutcome::DeadlineExceeded, + new_attempt(&observer, now), + &clock, + &mut committer, + ) + .expect("HEAD fallback response"); + assert_eq!(fallback.status, StatusCode::GATEWAY_TIMEOUT); + assert!(!fallback.transmits_body); + committer.run_task(); + + assert!(state.borrow().accepted.is_empty()); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + assert_eq!(reports[0].bytes_written, 0); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Completed) + ); + } + + #[test] + fn dropping_coordinator_in_each_pending_state_reports_once_and_releases_io() { + for pending_at in [PendingAt::Source, PendingAt::Write, PendingAt::Finish] { + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let state = Rc::new(RefCell::new(MockState { + pending: Some(pending_at), + ..MockState::default() + })); + let mut future = Box::pin(transmit_committed( + Body::from("body"), + future_policy(now), + attempt(&observer, now), + stable_clock(now), + EgressKind::Application, + MockIo(Rc::clone(&state)), + )); + let mut context = Context::from_waker(noop_waker_ref()); + + assert!(matches!(future.as_mut().poll(&mut context), Poll::Pending)); + drop(future); + + assert_eq!(state.borrow().drops, 1); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::TransportError); + } + } + + #[test] + fn deadline_equality_aborts_before_polling_the_source() { + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let state = Rc::new(RefCell::new(MockState::default())); + let result = block_on(transmit_committed( + Body::from("body"), + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(now), + }, + attempt(&observer, now), + stable_clock(now), + EgressKind::Application, + MockIo(Rc::clone(&state)), + )); + + assert_eq!(result, Err(DeliveryError::Deadline)); + assert_eq!(state.borrow().events, ["abort"]); + assert_eq!(state.borrow().aborts, 1); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::DeadlineExceeded); + } + + #[test] + fn suppressed_application_source_is_dropped_without_polling() { + struct DropSignal(Rc>); + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.set(self.0.get() + 1); + } + } + + let polls = Rc::new(Cell::new(0_usize)); + let drops = Rc::new(Cell::new(0_usize)); + let observed_polls = Rc::clone(&polls); + let signal = DropSignal(Rc::clone(&drops)); + let body = Body::from_stream(stream::poll_fn(move |_context| { + let _keep_alive = &signal; + observed_polls.set(observed_polls.get() + 1); + Poll::Pending + })); + let core_response = response_builder() + .status(StatusCode::OK) + .header("content-length", "99") + .body(body) + .expect("response"); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::HEAD, + core_response, + ) + .expect("prepared"); + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let state = Rc::new(RefCell::new(MockState::default())); + let mut committer = MockCommitter::new(Rc::clone(&state)); + + let platform_response = match start_prepared( + prepared, + future_policy(now), + new_attempt(&observer, now), + stable_clock(now), + EgressKind::Application, + &mut committer, + ) { + StartOutcome::Started(response) => response, + StartOutcome::Precommit { cause, .. } => { + panic!("unexpected start failure: {cause:?}") + } + }; + assert!(!platform_response.transmits_body); + committer.run_task(); + + assert_eq!(polls.get(), 0); + assert_eq!(drops.get(), 1); + assert!(state.borrow().accepted.is_empty()); + assert_eq!(observer.reports()[0].bytes_written, 0); + } } diff --git a/crates/edgezero-adapter-spin/src/response/tests/handoff.rs b/crates/edgezero-adapter-spin/src/response/tests/handoff.rs new file mode 100644 index 000000000..732f61a4e --- /dev/null +++ b/crates/edgezero-adapter-spin/src/response/tests/handoff.rs @@ -0,0 +1,440 @@ +use std::sync::Weak; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use edgezero_core::action; +use edgezero_core::app::App; +use edgezero_core::context::RequestContext; +use edgezero_core::http::{Request, Uri}; +use edgezero_core::ingress::{ + AdmissionDecision, IngressDispatchOutcome, IngressFraming, IngressGrant, IngressHeadAccounting, +}; +use edgezero_core::outbound::{ + HttpClient, IndependentOutboundProgress, OutboundBatch, OutboundBatchDriverEvent, + OutboundDispatchRequest, OutboundExchangeOwner, OutboundFuture, OutboundHead, + OutboundHttpClient, OutboundPending, OutboundPreparedBatch, OutboundProgressRuntime, + OutboundRequest, OutboundResponse, OutboundScope, OutboundScopeOwner, OutboundSendMode, + OutboundTime, +}; +use edgezero_core::response_egress::ResponseEgressBeginFailure; +use edgezero_core::router::RouterService; + +use super::*; + +type BegunEgress = Result< + ( + PreparedResponseEgress, + ResponseEgressPolicy, + ResponseEgressAttempt, + MonotonicClock, + ), + Box, +>; + +struct UnpolledClient(OutboundTime); + +struct Handoff { + begun: Option, + callback: Option, + completed: Arc, + observer: RecordingObserver, + retained: OutboundFuture, + scope: OutboundScope, + upload: Weak<[u8]>, +} + +std::thread_local! { + static RESPONSE: RefCell> = const { RefCell::new(None) }; +} + +impl OutboundHttpClient for UnpolledClient { + fn begin(&self, _request: OutboundDispatchRequest) -> Result { + Err(EdgeError::internal(anyhow::anyhow!( + "fixture only retains unpolled Send" + ))) + } + + fn dispatch_head(&self, _request: OutboundDispatchRequest) -> OutboundFuture { + Box::pin(async { + Err(EdgeError::internal(anyhow::anyhow!( + "fixture Send must remain unpolled" + ))) + }) + } + + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, _) = batch.into_slots(); + OutboundBatch::from_driver( + context, + stream::iter([OutboundBatchDriverEvent::Failed(EdgeError::internal( + anyhow::anyhow!("fixture only retains unpolled Send"), + ))]), + OutboundExchangeOwner::new(|| {}), + ) + } + + fn time(&self) -> OutboundTime { + self.0.clone() + } + + fn validate_supported_options( + &self, + _request: &OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + Ok(()) + } +} + +#[action] +async fn attached_response(_context: RequestContext) -> Result { + RESPONSE + .with(|slot| slot.borrow_mut().take()) + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("attached response missing"))) +} + +impl Handoff { + fn assert_closed(self, outcome: ResponseEgressOutcome) { + assert!(self.scope.check().is_err()); + assert!( + self.upload.upgrade().is_none(), + "retained handle cannot retain its request body" + ); + assert!(matches!( + block_on(self.retained), + Err(EdgeError::Internal { .. }) + )); + assert_eq!(self.completed.load(Ordering::SeqCst), 1); + let reports = self.observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].outcome, outcome); + } + + fn new(response: Response, method: Method) -> Self { + let now = MonotonicInstant::now(); + let callback = OutboundScopeOwner::callback(); + let owner = callback.with(OutboundScopeOwner::response_body); + let scope = owner.scope(); + let bytes = Arc::<[u8]>::from(&b"owned before callback return"[..]); + let upload = Arc::downgrade(&bytes); + let client = HttpClient::with_client(UnpolledClient(OutboundTime::new( + stable_clock(now), + |_, _| panic!("unpolled Send must not register timers"), + ))); + let retained = owner.with(|| { + client.send( + OutboundRequest::post("https://fixture.invalid/retained") + .expect("fixture request") + .body(Bytes::from_owner(bytes)), + ) + }); + let attached = owner + .attach(response) + .expect("unique body owner attachment"); + let previous = RESPONSE.with(|slot| slot.replace(Some(attached))); + assert!(previous.is_none()); + + let mut app = App::new( + RouterService::builder() + .route("/", method.clone(), attached_response) + .build(), + ); + app.set_monotonic_clock(stable_clock(now)); + let observer = RecordingObserver::default(); + app.set_response_egress_observer(observer.clone()); + let completed = Arc::new(AtomicUsize::new(0)); + let calls = Arc::clone(&completed); + let completion_upload = Weak::clone(&upload); + app.set_ingress_admission_policy(move |head| { + let call_count = Arc::clone(&calls); + let upload_probe = Weak::clone(&completion_upload); + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::new(move |_report| { + assert!( + upload_probe.upgrade().is_none(), + "scope closes before completion" + ); + call_count.fetch_add(1, Ordering::SeqCst); + }), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + } + }); + let mut request = Request::new(Body::empty()); + *request.method_mut() = method; + *request.uri_mut() = Uri::from_static("/"); + let outcome = block_on(callback.run(app.dispatch_ingress( + request, + now, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ))) + .expect("admitted response"); + let IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("response envelope"); + }; + let begun = + callback.with(|| envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength)); + Self { + begun: Some(begun), + callback: Some(callback), + completed, + observer, + retained, + scope, + upload, + } + } + + fn start(&mut self, committer: &mut MockCommitter) -> MockResponse { + match self.begun.take().expect("one egress attempt") { + Ok((prepared, policy, attempt, clock)) => { + let StartOutcome::Started(response) = start_prepared( + prepared, + policy, + attempt, + clock, + EgressKind::Application, + committer, + ) else { + panic!("started response"); + }; + response + } + Err(failure) => { + let (attempt, clock, cause) = failure.into_parts(); + start_fallback(&Method::GET, cause, attempt, &clock, committer) + .expect("fallback response") + } + } + } +} + +#[test] +fn attached_owner_survives_callback_return_and_eof_until_sdk_completion() { + let mut handoff = Handoff::new(Response::new(Body::stream(stream::empty())), Method::GET); + let state = Rc::new(RefCell::new(MockState { + pending: Some(PendingAt::Finish), + ..MockState::default() + })); + let mut committer = MockCommitter::new(Rc::clone(&state)); + handoff.start(&mut committer); + drop(handoff.callback.take()); + handoff + .scope + .check() + .expect("accepted owner outlives callback"); + let mut task = committer.tasks.pop_front().expect("coordinator"); + assert!(task.as_mut().now_or_never().is_none()); + assert!( + state.borrow().events.contains(&"finish"), + "source reached EOF" + ); + handoff.scope.check().expect("EOF is not SDK completion"); + assert!(handoff.upload.upgrade().is_some()); + assert!(handoff.observer.reports().is_empty()); + state.borrow_mut().pending = None; + block_on(task); + handoff.assert_closed(ResponseEgressOutcome::HostHandoff); +} + +#[test] +fn attached_owner_closes_on_coordinator_cancellation_at_every_await() { + for pending_at in [PendingAt::Source, PendingAt::Write, PendingAt::Finish] { + let mut handoff = Handoff::new(Response::new(Body::from("payload")), Method::GET); + let state = Rc::new(RefCell::new(MockState { + pending: Some(pending_at), + ..MockState::default() + })); + let mut committer = MockCommitter::new(state); + handoff.start(&mut committer); + drop(handoff.callback.take()); + let mut task = committer.tasks.pop_front().expect("coordinator"); + assert!(task.as_mut().now_or_never().is_none()); + handoff.scope.check().expect("coordinator still owns scope"); + drop(task); + handoff.assert_closed(ResponseEgressOutcome::TransportError); + } +} + +#[test] +fn attached_owner_closes_on_source_deadline_write_and_finish_failures() { + for (body, state, expected) in [ + ( + Body::from_stream(stream::iter([Err(EdgeError::bad_gateway( + "fixture source", + ))])), + MockState::default(), + ResponseEgressOutcome::SourceError, + ), + ( + Body::from("payload"), + MockState { + next_body: Some(IoFailureKind::Deadline), + ..MockState::default() + }, + ResponseEgressOutcome::DeadlineExceeded, + ), + ( + Body::from("payload"), + MockState { + writes: VecDeque::from([MockWrite::Fail(IoFailureKind::Transport, 0)]), + ..MockState::default() + }, + ResponseEgressOutcome::TransportError, + ), + ( + Body::empty(), + MockState { + finish: Some(IoFailureKind::Transport), + ..MockState::default() + }, + ResponseEgressOutcome::TransportError, + ), + ] { + let mut handoff = Handoff::new(Response::new(body), Method::GET); + let mut committer = MockCommitter::new(Rc::new(RefCell::new(state))); + handoff.start(&mut committer); + drop(handoff.callback.take()); + handoff + .scope + .check() + .expect("accepted work remains live before its egress failure"); + committer.run_task(); + handoff.assert_closed(expected); + } +} + +#[test] +fn head_suppression_keeps_attached_owner_until_sdk_terminal() { + let response = Response::new(Body::from_stream(stream::poll_fn(|_| { + panic!("HEAD must not read source") + }))); + let mut handoff = Handoff::new(response, Method::HEAD); + handoff + .scope + .check() + .expect("owner live after envelope begin"); + let state = Rc::new(RefCell::new(MockState { + pending: Some(PendingAt::Finish), + ..MockState::default() + })); + let mut committer = MockCommitter::new(Rc::clone(&state)); + let delivered = handoff.start(&mut committer); + assert!(!delivered.transmits_body); + drop(handoff.callback.take()); + let mut task = committer.tasks.pop_front().expect("coordinator"); + assert!(task.as_mut().now_or_never().is_none()); + assert!(state.borrow().events.contains(&"finish")); + handoff.scope.check().expect("HEAD retains accepted scope"); + assert!(handoff.upload.upgrade().is_some()); + assert_eq!(handoff.completed.load(Ordering::SeqCst), 0); + assert!(handoff.observer.reports().is_empty()); + state.borrow_mut().pending = None; + block_on(task); + handoff.assert_closed(ResponseEgressOutcome::HostHandoff); +} + +#[test] +fn fallback_revokes_application_work_before_sdk_terminal_without_completing_egress() { + let mut invalid = Response::new(Body::empty()); + invalid + .headers_mut() + .insert("content-length", "invalid".parse().expect("header")); + let mut handoff = Handoff::new(invalid, Method::GET); + handoff + .scope + .check() + .expect("failed application owner retained until fallback selection"); + assert!(handoff.upload.upgrade().is_some()); + let state = Rc::new(RefCell::new(MockState { + pending: Some(PendingAt::Finish), + ..MockState::default() + })); + let mut committer = MockCommitter::new(Rc::clone(&state)); + let delivered = handoff.start(&mut committer); + assert_eq!(delivered.status, StatusCode::INTERNAL_SERVER_ERROR); + assert!(delivered.transmits_body); + assert!(handoff.scope.check().is_err()); + assert!(handoff.upload.upgrade().is_none()); + handoff + .callback + .as_ref() + .expect("callback") + .scope() + .check() + .expect("fallback must not close callback siblings"); + assert_eq!(handoff.completed.load(Ordering::SeqCst), 0); + assert!(handoff.observer.reports().is_empty()); + drop(handoff.callback.take()); + let mut task = committer.tasks.pop_front().expect("fallback coordinator"); + assert!(task.as_mut().now_or_never().is_none()); + assert!(state.borrow().events.contains(&"finish")); + assert_eq!(state.borrow().accepted, INTERNAL_FALLBACK_BODY); + assert_eq!(handoff.completed.load(Ordering::SeqCst), 0); + assert!(handoff.observer.reports().is_empty()); + assert!(task.as_mut().now_or_never().is_none()); + assert_eq!(handoff.completed.load(Ordering::SeqCst), 0); + assert!(handoff.observer.reports().is_empty()); + state.borrow_mut().pending = None; + block_on(task); + assert_eq!(state.borrow().aborts, 0); + assert_eq!(state.borrow().drops, 1); + let reports = handoff.observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Completed) + ); + handoff.assert_closed(ResponseEgressOutcome::ConversionError); +} + +#[test] +fn externally_retained_attachment_is_revoked_when_callback_returns_error() { + let now = MonotonicInstant::now(); + let client = HttpClient::with_client(UnpolledClient(OutboundTime::new( + stable_clock(now), + |_, _| panic!("unpolled Send must not register timers"), + ))); + let bytes = Arc::<[u8]>::from(&b"unaccepted response upload"[..]); + let upload = Arc::downgrade(&bytes); + let mut escaped_response = None; + let mut escaped_work = None; + let mut captured = None; + let result = block_on(crate::in_outbound_callback(async { + let owner = OutboundScopeOwner::response_body(); + captured = Some(owner.scope()); + escaped_work = Some(owner.with(|| { + client.send( + OutboundRequest::post("https://fixture.invalid/escaped") + .expect("fixture request") + .body(Bytes::from_owner(bytes)), + ) + })); + escaped_response = Some( + owner + .attach(Response::new(Body::empty())) + .expect("attachment"), + ); + Err::<(), _>("callback failed before returning its response") + })); + assert!(result.is_err()); + assert!( + escaped_response.is_some(), + "public response remains retained" + ); + assert!(captured.expect("body scope").check().is_err()); + assert!( + upload.upgrade().is_none(), + "attachment alone cannot promote work" + ); + assert!(matches!( + block_on(escaped_work.expect("retained Send")), + Err(EdgeError::Internal { .. }) + )); + drop(escaped_response); +} diff --git a/crates/edgezero-adapter-spin/src/secret_store.rs b/crates/edgezero-adapter-spin/src/secret_store.rs index f160f1786..ae1793a7e 100644 --- a/crates/edgezero-adapter-spin/src/secret_store.rs +++ b/crates/edgezero-adapter-spin/src/secret_store.rs @@ -4,9 +4,19 @@ //! The `store_name` parameter is intentionally ignored; provision secrets as //! application variables in `spin.toml`. +use std::future::Future; +#[cfg(test)] +use std::future::{pending, ready}; +use std::time::Duration; + use async_trait::async_trait; use bytes::Bytes; -use edgezero_core::secret_store::{SecretError, SecretStore}; +use edgezero_core::config_store::BoundedStoreRead; +use edgezero_core::secret_store::{SecretError, SecretStore, finish_bounded_secret_read}; +use edgezero_core::time::{Deadline, MonotonicClock}; +use futures_util::future::{Either, select}; +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +use spin_sdk::time::sleep; /// Secret store backed by Spin component variables. /// @@ -58,7 +68,207 @@ impl SecretStore for SpinSecretStore { ))), } } + + #[inline] + async fn get_bytes_bounded( + &self, + store_name: &str, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError> { + bounded_secret_read( + self.get_bytes(store_name, key), + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + .await + } +} + +// Spin Variables returns a complete value, so the byte bounds apply after host +// materialization. The timer race bounds guest return but does not prove host cancellation. +pub(crate) async fn bounded_secret_read( + read: F, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, +) -> Result, SecretError> +where + F: Future, SecretError>>, +{ + #[cfg(all(feature = "spin", target_arch = "wasm32"))] + { + return bounded_secret_read_with_timer( + read, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + sleep, + ) + .await; + } + #[cfg(not(all(feature = "spin", target_arch = "wasm32")))] + { + bounded_secret_read_with_timer( + read, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + |_| pending(), + ) + .await + } } -// TODO: integration tests require the Spin runtime. -// Test SpinSecretStore as part of a Spin E2E test suite. +async fn bounded_secret_read_with_timer( + read: F, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + mut make_timer: MakeTimer, +) -> Result, SecretError> +where + F: Future, SecretError>>, + MakeTimer: FnMut(Duration) -> Timer, + Timer: Future, +{ + futures_util::pin_mut!(read); + loop { + let Some(remaining) = deadline.remaining_at(clock.now()) else { + return Err(SecretError::DeadlineExceeded); + }; + let timer = make_timer(remaining); + futures_util::pin_mut!(timer); + match select(timer, read.as_mut()).await { + Either::Left(((), _read)) => { + if deadline.is_expired_at(clock.now()) { + return Err(SecretError::DeadlineExceeded); + } + } + Either::Right((result, _timer)) => { + return finish_bounded_secret_read( + result, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::time::Duration; + + use edgezero_core::time::MonotonicClock; + use futures::executor::block_on; + + struct DropProbe(Arc); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropProbe { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + fn clock_reaching_deadline() -> (MonotonicClock, Deadline) { + let start = edgezero_core::MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let samples = Arc::new(AtomicUsize::new(0)); + let observed_samples = Arc::clone(&samples); + let clock = MonotonicClock::new(move || { + if observed_samples.fetch_add(1, Ordering::SeqCst) == 0 { + start + } else { + terminal + } + }); + (clock, Deadline::at_instant(terminal)) + } + + #[test] + fn bounded_secret_timer_drops_never_ready_provider() { + let drops = Arc::new(AtomicUsize::new(0)); + let guard = DropProbe(Arc::clone(&drops)); + let read = async move { + let _guard = guard; + pending::, SecretError>>().await + }; + let (clock, deadline) = clock_reaching_deadline(); + + let error = block_on(bounded_secret_read_with_timer( + read, + &clock, + deadline, + 1, + 1, + |_| ready(()), + )) + .expect_err("timer must terminate a pending secret read"); + + assert!(matches!(error, SecretError::DeadlineExceeded)); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn bounded_secret_read_rearms_after_an_early_timer_wake() { + let timer_calls = Arc::new(AtomicUsize::new(0)); + let observed_timer_calls = Arc::clone(&timer_calls); + let clock = MonotonicClock::default(); + let error = block_on(bounded_secret_read_with_timer( + ready(Err(SecretError::Unavailable)), + &clock, + Deadline::after(Duration::from_secs(1)), + 1, + 1, + move |_| { + let early_wake = observed_timer_calls.fetch_add(1, Ordering::SeqCst) == 0; + async move { + if !early_wake { + pending::<()>().await; + } + } + }, + )) + .expect_err("provider error must win after an early timer wake"); + + assert!(matches!(error, SecretError::Unavailable)); + assert_eq!(timer_calls.load(Ordering::SeqCst), 2); + } + + #[test] + fn bounded_secret_timer_wins_simultaneous_ready_error_at_deadline() { + let (clock, deadline) = clock_reaching_deadline(); + let error = block_on(bounded_secret_read_with_timer( + ready(Err(SecretError::Unavailable)), + &clock, + deadline, + 1, + 1, + |_| ready(()), + )) + .expect_err("timer readiness at the deadline must win equality"); + + assert!(matches!(error, SecretError::DeadlineExceeded)); + } +} diff --git a/crates/edgezero-adapter-spin/src/templates/src/lib.rs.hbs b/crates/edgezero-adapter-spin/src/templates/src/lib.rs.hbs index eb7e6e610..a22581d09 100644 --- a/crates/edgezero-adapter-spin/src/templates/src/lib.rs.hbs +++ b/crates/edgezero-adapter-spin/src/templates/src/lib.rs.hbs @@ -7,12 +7,12 @@ )] #[cfg(target_arch = "wasm32")] -use spin_sdk::http::{IntoResponse, Request}; +use spin_sdk::http::Request; #[cfg(target_arch = "wasm32")] use spin_sdk::http_service; #[cfg(target_arch = "wasm32")] #[http_service] -async fn handle(req: Request) -> anyhow::Result { +async fn handle(req: Request) -> anyhow::Result { edgezero_adapter_spin::run_app::<{{proj_core_mod}}::App>(req).await } diff --git a/crates/edgezero-adapter-spin/tests/contract.rs b/crates/edgezero-adapter-spin/tests/contract.rs index a8c238fc4..b3d4ee306 100644 --- a/crates/edgezero-adapter-spin/tests/contract.rs +++ b/crates/edgezero-adapter-spin/tests/contract.rs @@ -1,10 +1,6 @@ -// Adapter contract tests run on the Spin wasm32 target, matching the -// fastly and cloudflare contract suites. Gating the whole file keeps the -// host `cargo test`/`clippy` runs consistent across adapters. -#![cfg(all(feature = "spin", target_arch = "wasm32"))] - // Compile-time check: SpinKvStore and SpinSecretStore implement their // respective core store traits. +#[cfg(all(feature = "spin", target_arch = "wasm32"))] mod store_trait_compile_checks { use edgezero_adapter_spin::key_value_store::SpinKvStore; use edgezero_adapter_spin::secret_store::SpinSecretStore; @@ -29,107 +25,74 @@ mod store_trait_compile_checks { } #[cfg(test)] +#[cfg(all(feature = "spin", target_arch = "wasm32"))] +#[cfg_attr( + feature = "test-utils", + expect( + clippy::arbitrary_source_item_ordering, + reason = "ingress contracts are grouped after the provider fixture tests" + ) +)] mod tests { - // `from_core_response` tests live in a nested module so they're grouped - // together; the `tests_outside_test_module` lint is satisfied by the - // outer `#[cfg(test)] mod tests` wrapper. - mod from_core_response_tests { - use super::*; - use edgezero_adapter_spin::response::from_core_response; - use http_body_util::BodyExt as _; - - #[test] - fn from_core_response_translates_status_and_headers() { - block_on(async { - let response = response_builder() - .status(StatusCode::CREATED) - .header("x-edgezero-res", "1") - .body(Body::from(b"hello".to_vec())) - .expect("response"); - - let spin_response = from_core_response(response).await.expect("spin response"); - - assert_eq!( - spin_response.status(), - StatusCode::CREATED, - "status translated" - ); - assert!( - spin_response.headers().get("x-edgezero-res").is_some(), - "response header preserved" - ); - }); - } - - #[test] - fn from_core_response_collects_streaming_body() { - block_on(async { - let response = response_builder() - .status(StatusCode::OK) - .body(Body::stream(stream::iter(vec![ - Bytes::from_static(b"chunk-1"), - Bytes::from_static(b"chunk-2"), - ]))) - .expect("response"); - - let spin_response = from_core_response(response).await.expect("spin response"); - - assert_eq!(spin_response.status(), StatusCode::OK, "status translated"); - let body = spin_response - .into_body() - .collect() - .await - .expect("collect") - .to_bytes(); - assert_eq!(body.as_ref(), b"chunk-1chunk-2", "streaming body collected"); - }); - } - - #[test] - fn from_core_response_handles_empty_body() { - block_on(async { - let response = response_builder() - .status(StatusCode::NO_CONTENT) - .body(Body::from(Vec::new())) - .expect("response"); - - let spin_response = from_core_response(response).await.expect("spin response"); - - assert_eq!( - spin_response.status(), - StatusCode::NO_CONTENT, - "status translated" - ); - let body = spin_response - .into_body() - .collect() - .await - .expect("collect") - .to_bytes(); - assert!(body.is_empty(), "empty body preserved"); - }); - } - } - use bytes::Bytes; use edgezero_adapter_spin::context::SpinRequestContext; + #[cfg(feature = "test-utils")] + use edgezero_adapter_spin::request::deadline_body_releases_source_for_test; use edgezero_core::app::App; + #[cfg(feature = "test-utils")] + use edgezero_core::app::Hooks; use edgezero_core::body::Body; - use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; + use edgezero_core::config_store::{ + BoundedStoreRead, ConfigStore, ConfigStoreError, ConfigStoreHandle, + finish_bounded_config_read, + }; use edgezero_core::context::RequestContext; use edgezero_core::error::EdgeError; use edgezero_core::http::{Response, StatusCode, request_builder, response_builder}; use edgezero_core::key_value_store::{KvError, KvHandle, KvPage, KvStore}; use edgezero_core::router::RouterService; - use edgezero_core::secret_store::{SecretError, SecretHandle, SecretStore}; + use edgezero_core::secret_store::{ + SecretError, SecretHandle, SecretStore, finish_bounded_secret_read, + }; use edgezero_core::store_registry::{ BoundSecretStore, ConfigRegistry, ConfigStoreBinding, KvRegistry, SecretRegistry, }; + use edgezero_core::time::{Deadline, MonotonicClock}; use futures::executor::block_on; use futures::stream; use std::sync::Arc; use std::time::Duration; + /// Config store that returns a value only for the expected key. + #[cfg(feature = "test-utils")] + struct FailingConfiguration; + + #[cfg(feature = "test-utils")] + #[expect( + clippy::missing_trait_methods, + reason = "test hook exercises only adapter startup failure" + )] + impl Hooks for FailingConfiguration { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Err(EdgeError::service_unavailable("configuration unavailable")) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + #[cfg(feature = "test-utils")] + #[test] + fn failing_configuration() { + let Err(error) = App::build::(edgezero_adapter_spin::SPIN_PLATFORM) + else { + panic!("configuration must not produce an app"); + }; + assert_eq!(error.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(error.message(), "configuration unavailable"); + } + /// Config store that returns a value only for the expected key. struct FixedConfigStore { key: &'static str, @@ -138,13 +101,36 @@ mod tests { #[async_trait::async_trait(?Send)] impl ConfigStore for FixedConfigStore { - async fn get(&self, key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + key: &str, + ) -> Result, ConfigStoreError> { if key == self.key { - Ok(Some(self.value.to_owned())) + Ok(Some(self.value.into())) } else { Ok(None) } } + + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + finish_bounded_config_read( + self.get(key).await, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + } } /// KV store that returns a fixed value for one key; everything else is absent. @@ -211,11 +197,32 @@ mod tests { Ok(None) } } + + async fn get_bytes_bounded( + &self, + store_name: &str, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError> { + if deadline.is_expired_at(clock.now()) { + return Err(SecretError::DeadlineExceeded); + } + finish_bounded_secret_read( + self.get_bytes(store_name, key).await, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + } } fn build_test_app() -> App { async fn capture_uri(ctx: RequestContext) -> Result { - let body = Body::text(ctx.request().uri().to_string()); + let body = Body::text(ctx.uri().to_string()); let response = response_builder() .status(StatusCode::OK) .body(body) @@ -224,12 +231,7 @@ mod tests { } async fn mirror_body(ctx: RequestContext) -> Result { - let bytes = ctx - .request() - .body() - .as_bytes() - .expect("buffered request body") - .to_vec(); + let bytes = ctx.body_bytes(1024 * 1024).await?.to_vec(); let response = response_builder() .status(StatusCode::OK) .body(Body::from(bytes)) @@ -260,7 +262,7 @@ mod tests { .await .ok() .flatten() - .unwrap_or_else(|| "missing".to_owned()), + .map_or_else(|| "missing".to_owned(), |value| value.to_string()), None => "missing".to_owned(), }; let response = response_builder() @@ -341,6 +343,12 @@ mod tests { let _app = build_test_app(); } + #[cfg(feature = "test-utils")] + #[test] + fn deadline_body_releases_source_when_timeout_is_emitted() { + assert!(deadline_body_releases_source_for_test()); + } + #[test] fn router_dispatches_get_and_returns_response() { let app = build_test_app(); @@ -544,4 +552,1364 @@ mod tests { "no secret handle yields the no-handle marker" ); } + + #[cfg(feature = "test-utils")] + mod ingress_contract { + use edgezero_core::response_egress_framing::ResponseEgressFramingMode; + use std::collections::VecDeque; + use std::future::{Future, poll_fn as poll_future, ready}; + use std::io; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::Poll; + + use edgezero_adapter_spin::outbound::{ + SpinOutboundClient, converted_response_content_type_provenance_holds_for_test, + converted_response_retains_injected_clock_for_test, deferred_clock_paths_hold_for_test, + }; + use edgezero_adapter_spin::request::{ + dispatch_ingress_source_error_for_test, dispatch_ingress_stream_for_test, + dispatch_ingress_stream_with_timer_for_test, dispatch_ingress_with_for_test, + dispatch_request_for_test, + }; + use edgezero_core::http::{HeaderMap, HeaderValue, Method}; + use edgezero_core::ingress::{ + AdmissionDecision, BufferedIngressResponse, IngressGrant, IngressHeadLimits, + }; + use edgezero_core::middleware::{Middleware, Next}; + use edgezero_core::outbound::{ + HttpClient, OutboundCollectedSlot, OutboundRequest, OutboundTime, + }; + use edgezero_core::response_egress::{ + DEFAULT_RESPONSE_WRITE_BUDGET, DetachedResponseEgressDecision, + ResponseEgressCompletion, ResponseEgressEnvelope, ResponseEgressObserver, + ResponseEgressOutcome, ResponseEgressReport, + }; + use edgezero_core::router::{RouteMetadata, RouteResolution}; + use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; + use futures::FutureExt as _; + use futures::stream::{empty, poll_fn}; + use spin_sdk::http::{FromRequest as _, IntoRequest as _, Request as SpinRequest}; + + use super::*; + + #[test] + fn admission_abort() { + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + let delivery_calls = Arc::new(AtomicUsize::new(0)); + let observed_delivery_calls = Arc::clone(&delivery_calls); + let mut app = App::new(RouterService::builder().build()); + app.set_ingress_admission_policy(|_| AdmissionDecision::Abort); + + let error = block_on(dispatch_ingress_with_for_test( + &app, + Method::POST, + "/missing".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(stream::empty::>()) + }, + move |_response| { + observed_delivery_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + }, + )) + .expect_err("abort must return a platform error"); + + assert!(error.to_string().contains("ingress admission aborted")); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_eq!(delivery_calls.load(Ordering::SeqCst), 0); + } + + struct DropSignal(Arc); + + #[derive(Clone)] + struct RecordingEgressObserver(Arc>>); + + impl ResponseEgressObserver for RecordingEgressObserver { + fn complete(&self, report: &ResponseEgressReport) { + self.0.lock().expect("reports lock").push(report.clone()); + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + fn preflight_time(clock: MonotonicClock) -> OutboundTime { + OutboundTime::new(clock, |_, _| { + panic!("preflight must not register an outbound timer") + }) + } + + fn scripted_clock(script: Vec) -> MonotonicClock { + let last = *script.last().expect("clock observations"); + let observations = Arc::new(Mutex::new(VecDeque::from(script))); + MonotonicClock::new(move || { + observations + .lock() + .expect("clock observations") + .pop_front() + .unwrap_or(last) + }) + } + + fn resolved(slot: &OutboundCollectedSlot) -> &edgezero_core::OutboundSlotResult { + let OutboundCollectedSlot::Resolved(result) = slot else { + panic!("resolved outbound slot"); + }; + result + } + + #[test] + fn outbound_clock_controls_preflight_elapsed_and_backwards_failure() { + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(9)) + .expect("completed instant"); + let forward_client = HttpClient::with_client(SpinOutboundClient::with_time( + preflight_time(scripted_clock(vec![start, completed])), + )); + let forward_request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + let observation_cutoff = Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("cutoff instant"), + ); + let forward_results = block_on( + forward_client + .start_batch_until(vec![forward_request], observation_cutoff) + .collect(), + ) + .expect("valid batch driver"); + assert_eq!( + forward_results.termination(), + edgezero_core::OutboundBatchTermination::Completed + ); + assert_eq!(forward_results.slots().len(), 1); + let forward_result = resolved(&forward_results.slots()[0]); + assert_eq!(forward_result.elapsed, Duration::from_millis(9)); + assert!(matches!( + forward_result.outcome, + Err(EdgeError::BadRequest { .. }) + )); + + let earlier = start + .checked_sub(Duration::from_millis(1)) + .expect("earlier instant"); + let backwards_client = HttpClient::with_client(SpinOutboundClient::with_time( + preflight_time(scripted_clock(vec![start, earlier])), + )); + let backwards_request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + let backwards_results = block_on( + backwards_client + .start_batch_until(vec![backwards_request], observation_cutoff) + .collect(), + ) + .expect("valid batch driver"); + assert_eq!( + backwards_results.termination(), + edgezero_core::OutboundBatchTermination::Completed + ); + assert_eq!(backwards_results.slots().len(), 1); + let backwards_result = resolved(&backwards_results.slots()[0]); + assert_eq!(backwards_result.elapsed, Duration::ZERO); + assert!(matches!( + backwards_result.outcome, + Err(EdgeError::Internal { .. }) + )); + } + + #[test] + fn expired_observation_cutoff_leaves_slot_unresolved() { + let now = MonotonicInstant::now(); + let client = HttpClient::with_client(SpinOutboundClient::with_time(preflight_time( + MonotonicClock::new(move || now), + ))); + let request = OutboundRequest::get("https://example.com/").expect("request"); + + let results = block_on( + client + .start_batch_until(vec![request], Deadline::at_instant(now)) + .collect(), + ) + .expect("valid batch driver"); + + assert_eq!( + results.termination(), + edgezero_core::OutboundBatchTermination::Cutoff + ); + assert!(matches!( + results.slots().first(), + Some(OutboundCollectedSlot::Unresolved) + )); + } + + #[test] + fn batch_preserves_three_preflight_slots_in_input_order() { + let now = MonotonicInstant::now(); + let client = HttpClient::with_client(SpinOutboundClient::with_time(preflight_time( + MonotonicClock::new(move || now), + ))); + let streamed_upload = OutboundRequest::post("https://example.com/upload") + .expect("upload request") + .body(Body::stream(stream::iter([Bytes::from_static(b"body")]))); + let streamed_response = OutboundRequest::get("https://example.com/stream") + .expect("stream request") + .stream_response(); + let method_error = OutboundRequest::get("https://example.com/get") + .expect("GET request") + .body(Body::stream(stream::iter([Bytes::new()]))); + + let results = block_on( + client + .start_batch_until( + vec![streamed_upload, streamed_response, method_error], + Deadline::at_instant( + now.checked_add(Duration::from_secs(1)) + .expect("cutoff instant"), + ), + ) + .collect(), + ) + .expect("valid batch driver"); + assert_eq!( + results.termination(), + edgezero_core::OutboundBatchTermination::Completed + ); + let messages: Vec<_> = results + .slots() + .iter() + .map(|slot| match &resolved(slot).outcome { + Err(EdgeError::BadRequest { message, .. }) => message.as_str(), + other => panic!("expected preflight rejection, got {other:?}"), + }) + .collect(); + + assert_eq!( + messages, + [ + "outbound batches require buffered request bodies; use send for a streamed upload", + "outbound batches require buffered responses; use send for a streamed response", + "outbound batches require buffered request bodies", + ] + ); + assert!( + results + .slots() + .iter() + .all(|slot| resolved(slot).elapsed == Duration::ZERO) + ); + } + + #[test] + fn deferred_request_and_response_paths_retain_the_injected_clock() { + assert!(block_on(deferred_clock_paths_hold_for_test())); + } + + #[test] + fn converted_response_retains_injected_clock() { + assert!(block_on( + converted_response_retains_injected_clock_for_test() + )); + } + + #[test] + fn converted_response_preserves_original_content_type_provenance() { + assert!(block_on( + converted_response_content_type_provenance_holds_for_test() + )); + } + + #[test] + fn standard_dispatch_installs_the_application_outbound_clock() { + async fn elapsed(ctx: RequestContext) -> Result { + let client = ctx + .http_client() + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("missing HTTP client")))?; + let request = OutboundRequest::get("https://example.com/")?.stream_response(); + let results = client + .send_all_until(vec![request], Deadline::after(Duration::from_secs(1))) + .await + .map_err(|failure| failure.into_parts().0)?; + let result = results.slots().first().map(resolved).ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("missing outbound result")) + })?; + Ok(result.elapsed.as_millis().to_string()) + } + + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(7)) + .expect("completed instant"); + let observations = Arc::new(AtomicUsize::new(0)); + let clock_observations = Arc::clone(&observations); + let mut app = App::new(RouterService::builder().get("/clock", elapsed).build()); + app.set_monotonic_clock(MonotonicClock::new(move || { + if clock_observations.fetch_add(1, Ordering::SeqCst) < 2 { + start + } else { + completed + } + })); + let source = empty::>(); + + let response = block_on(dispatch_ingress_stream_for_test( + &app, + Method::GET, + "/clock".parse().expect("URI"), + source, + )) + .expect("Spin response"); + + let bytes = captured_body(response); + assert_eq!(bytes.as_ref(), b"7"); + assert!(observations.load(Ordering::SeqCst) >= 3); + } + + #[test] + fn standard_native_request_refuses_before_body_read() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + app.set_ingress_admission_policy(|head| { + assert!(matches!(head.route_resolution(), RouteResolution::NotFound)); + AdmissionDecision::Refuse { + completion: ResponseEgressCompletion::empty(), + response: response_builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .header("x-ingress-refusal", "saturated") + .body(Body::from("spin unavailable\n")) + .expect("refusal response"), + } + }); + let outgoing_request = SpinRequest::builder() + .method("POST") + .uri("http://example.test/missing") + .body(http_body_util::Full::new(Bytes::from_static(b"abcde"))) + .expect("outgoing-compatible request"); + let wasi_request = outgoing_request.into_request().expect("WASI request"); + let incoming_request = + SpinRequest::from_request(wasi_request).expect("incoming Spin request"); + + let response = + block_on(dispatch_request_for_test(&app, incoming_request)).expect("Spin response"); + + let mut expected_headers = HeaderMap::new(); + expected_headers.insert("x-ingress-refusal", HeaderValue::from_static("saturated")); + assert_terminal_response( + response, + StatusCode::SERVICE_UNAVAILABLE, + &expected_headers, + b"spin unavailable\n", + ); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + + struct CountingMiddleware(Arc); + + #[async_trait::async_trait(?Send)] + impl Middleware for CountingMiddleware { + async fn handle( + &self, + ctx: RequestContext, + next: Next<'_>, + ) -> Result { + self.0.fetch_add(1, Ordering::SeqCst); + next.run(ctx).await + } + } + + fn guarded_fallback_app() -> (App, Arc, Arc) { + let handler_calls = Arc::new(AtomicUsize::new(0)); + let handler_counter = Arc::clone(&handler_calls); + let middleware_calls = Arc::new(AtomicUsize::new(0)); + let router = RouterService::builder() + .get("/known", move |_ctx: RequestContext| { + let request_handler_calls = Arc::clone(&handler_counter); + async move { + request_handler_calls.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("handler must not run") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware_calls))) + .build(); + (App::new(router), handler_calls, middleware_calls) + } + + fn fallback_app( + max_body_bytes: usize, + read_budget: Duration, + grant_drops: &Arc, + ) -> (App, Arc, Arc) { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let observed_grant_drops = Arc::clone(grant_drops); + app.set_ingress_admission_policy(move |head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::MethodNotAllowed { .. } | RouteResolution::NotFound + )); + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes, + read_deadline: head.read_deadline_after(read_budget), + on_exceeded: terminal_response( + StatusCode::UNPROCESSABLE_ENTITY, + "overflow", + b"spin overflow\0response", + ), + on_timeout: terminal_response( + StatusCode::GATEWAY_TIMEOUT, + "timeout", + b"spin timeout response\n", + ), + } + }); + (app, handler_calls, middleware_calls) + } + + fn matched_body_app( + read_budget: Duration, + grant_drops: &Arc, + ) -> (App, Arc, Arc) { + let handler_calls = Arc::new(AtomicUsize::new(0)); + let handler_counter = Arc::clone(&handler_calls); + let middleware_calls = Arc::new(AtomicUsize::new(0)); + let router = RouterService::builder() + .post("/body", move |ctx: RequestContext| { + let request_handler_calls = Arc::clone(&handler_counter); + async move { + request_handler_calls.fetch_add(1, Ordering::SeqCst); + let _bytes = ctx.body_bytes(4).await?; + Ok::<_, EdgeError>("body read completed unexpectedly") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware_calls))) + .build(); + let mut app = App::new(router); + let observed_grant_drops = Arc::clone(grant_drops); + app.set_ingress_admission_policy(move |head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::Matched(metadata) if metadata.pattern() == "/body" + )); + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + read_deadline: head.read_deadline_after(read_budget), + } + }); + (app, handler_calls, middleware_calls) + } + + fn deadline_timer() -> impl Future { + let mut first_poll = true; + poll_future(move |cx| { + if first_poll { + first_poll = false; + cx.waker().wake_by_ref(); + Poll::Pending + } else { + Poll::Ready(()) + } + }) + } + + fn terminal_headers(marker: &'static str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert( + "content-type", + HeaderValue::from_static("application/octet-stream"), + ); + headers.insert("x-ingress-terminal", HeaderValue::from_static(marker)); + headers + } + + fn terminal_response( + status: StatusCode, + marker: &'static str, + body: &'static [u8], + ) -> BufferedIngressResponse { + BufferedIngressResponse::new(status, terminal_headers(marker), Bytes::from_static(body)) + } + + fn internal_error_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("content-length", HeaderValue::from_static("76")); + headers.insert("content-type", HeaderValue::from_static("application/json")); + headers + } + + fn tracked_stream( + chunks: Vec, + grant_drops: &Arc, + source_drops: &Arc, + body_polls: &Arc, + expire_on_poll: Option<(Arc>, MonotonicInstant)>, + ) -> impl futures::Stream> + 'static { + let observed_grant_drops = Arc::clone(grant_drops); + let observed_body_polls = Arc::clone(body_polls); + let source_drop = DropSignal(Arc::clone(source_drops)); + let mut pending_chunks = chunks.into_iter(); + poll_fn(move |_cx| { + let _keep_source_alive = &source_drop; + assert_eq!(observed_grant_drops.load(Ordering::SeqCst), 0); + observed_body_polls.fetch_add(1, Ordering::SeqCst); + if let Some((now, deadline)) = &expire_on_poll { + *now.lock().expect("clock lock") = *deadline; + } + Poll::Ready(pending_chunks.next().map(Ok)) + }) + } + + fn error_stream( + grant_drops: &Arc, + source_drops: &Arc, + body_polls: &Arc, + ) -> impl futures::Stream> + 'static { + let observed_grant_drops = Arc::clone(grant_drops); + let observed_body_polls = Arc::clone(body_polls); + let source_drop = DropSignal(Arc::clone(source_drops)); + let mut emitted = false; + poll_fn(move |_cx| { + let _keep_source_alive = &source_drop; + assert_eq!(observed_grant_drops.load(Ordering::SeqCst), 0); + observed_body_polls.fetch_add(1, Ordering::SeqCst); + if emitted { + Poll::Ready(None) + } else { + emitted = true; + Poll::Ready(Some(Err(io::Error::other("spin ingress source failure")))) + } + }) + } + + fn pending_stream( + grant_drops: &Arc, + source_drops: &Arc, + body_polls: &Arc, + ) -> impl futures::Stream> + 'static { + let observed_grant_drops = Arc::clone(grant_drops); + let observed_body_polls = Arc::clone(body_polls); + let source_drop = DropSignal(Arc::clone(source_drops)); + poll_fn(move |_cx| { + let _keep_source_alive = &source_drop; + assert_eq!(observed_grant_drops.load(Ordering::SeqCst), 0); + observed_body_polls.fetch_add(1, Ordering::SeqCst); + Poll::Pending + }) + } + + fn assert_no_route_dispatch(handler_calls: &AtomicUsize, middleware_calls: &AtomicUsize) { + assert_eq!(handler_calls.load(Ordering::SeqCst), 0); + assert_eq!(middleware_calls.load(Ordering::SeqCst), 0); + } + + fn capture_response(envelope: ResponseEgressEnvelope) -> Response { + let Ok((prepared, _policy, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("test egress envelope must prepare"); + }; + assert!(attempt.begin_writing()); + assert!(attempt.terminate(ResponseEgressOutcome::HostHandoff, clock.now())); + prepared.into_response() + } + + fn assert_terminal_response( + envelope: ResponseEgressEnvelope, + status: StatusCode, + expected_headers: &HeaderMap, + body: &[u8], + ) { + let response = capture_response(envelope); + let mut framed_headers = expected_headers.clone(); + framed_headers.insert( + "content-length", + HeaderValue::from_str(&body.len().to_string()).expect("buffered body length"), + ); + assert_eq!(response.status(), status); + assert_eq!(response.headers(), &framed_headers); + let bytes = captured_response_body(response); + assert_eq!(bytes.as_ref(), body); + } + + fn captured_body(envelope: ResponseEgressEnvelope) -> Bytes { + captured_response_body(capture_response(envelope)) + } + + #[test] + fn normalized_ingress_error_returns_typed_response_without_application_observation() { + let reports = Arc::new(Mutex::new(Vec::new())); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let mut app = App::new(RouterService::builder().build()); + app.set_ingress_head_limits( + IngressHeadLimits::default() + .with_max_request_target_bytes(4) + .expect("target limit"), + ); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let observed_factory_calls = Arc::clone(&factory_calls); + let observed_completion_calls = Arc::clone(&completion_calls); + app.set_detached_response_egress_decision_factory(move |head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + let terminal_calls = Arc::clone(&observed_completion_calls); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |_report| { + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), + } + }); + + let egress = block_on(dispatch_ingress_stream_for_test( + &app, + Method::GET, + "/too-long".parse().expect("URI"), + empty::>(), + )) + .expect("typed response"); + let response = capture_response(egress); + + assert_eq!(response.status(), StatusCode::URI_TOO_LONG); + assert!(reports.lock().expect("reports lock").is_empty()); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn normalized_ingress_error_decision_abort_uses_provider_error() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + app.set_ingress_head_limits( + IngressHeadLimits::default() + .with_max_request_target_bytes(4) + .expect("target limit"), + ); + let reports = Arc::new(Mutex::new(Vec::new())); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + app.set_detached_response_egress_decision_factory(move |_head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + DetachedResponseEgressDecision::Abort + }); + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + let delivery_calls = Arc::new(AtomicUsize::new(0)); + let observed_delivery_calls = Arc::clone(&delivery_calls); + + let error = block_on(dispatch_ingress_with_for_test( + &app, + Method::GET, + "/too-long".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(empty::>()) + }, + move |_egress| { + observed_delivery_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + }, + )) + .expect_err("detached abort must return a platform error"); + + assert!(error.to_string().contains("ingress admission aborted")); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_eq!(delivery_calls.load(Ordering::SeqCst), 0); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[test] + fn admission_policy_error_decision_send_uses_detached_completion() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let decision_completion_calls = Arc::new(AtomicUsize::new(0)); + let decision_resource_drops = Arc::new(AtomicUsize::new(0)); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_decision_completion_calls = Arc::clone(&decision_completion_calls); + let observed_decision_resource_drops = Arc::clone(&decision_resource_drops); + let observed_grant_drops = Arc::clone(&grant_drops); + app.set_ingress_admission_policy(move |head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::Matched(_) + )); + let decision_resource = DropSignal(Arc::clone(&observed_decision_resource_drops)); + let terminal_calls = Arc::clone(&observed_decision_completion_calls); + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::new(move |_report| { + let _resource = decision_resource; + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes: 1, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text( + StatusCode::PAYLOAD_TOO_LARGE, + "too large", + ), + on_timeout: BufferedIngressResponse::text( + StatusCode::REQUEST_TIMEOUT, + "timeout", + ), + } + }); + let reports = Arc::new(Mutex::new(Vec::new())); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + let observed_completion_calls = Arc::clone(&completion_calls); + app.set_detached_response_egress_decision_factory(move |head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + let terminal_calls = Arc::clone(&observed_completion_calls); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |_report| { + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), + } + }); + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + + let response = block_on(dispatch_ingress_with_for_test( + &app, + Method::GET, + "/known".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(empty::>()) + }, + |egress| Ok(capture_response(egress)), + )) + .expect("typed policy error response"); + + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + assert_eq!(decision_completion_calls.load(Ordering::SeqCst), 0); + assert_eq!(decision_resource_drops.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[test] + fn admission_policy_error_decision_abort_uses_provider_error() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let decision_resource_drops = Arc::new(AtomicUsize::new(0)); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_decision_resource_drops = Arc::clone(&decision_resource_drops); + let observed_grant_drops = Arc::clone(&grant_drops); + app.set_ingress_admission_policy(move |head| { + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::new({ + let resource = DropSignal(Arc::clone(&observed_decision_resource_drops)); + move |_report| drop(resource) + }), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + max_body_bytes: 1, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text( + StatusCode::PAYLOAD_TOO_LARGE, + "too large", + ), + on_timeout: BufferedIngressResponse::text( + StatusCode::REQUEST_TIMEOUT, + "timeout", + ), + } + }); + let reports = Arc::new(Mutex::new(Vec::new())); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + app.set_detached_response_egress_decision_factory(move |_head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + DetachedResponseEgressDecision::Abort + }); + let source_calls = Arc::new(AtomicUsize::new(0)); + let observed_source_calls = Arc::clone(&source_calls); + let delivery_calls = Arc::new(AtomicUsize::new(0)); + let observed_delivery_calls = Arc::clone(&delivery_calls); + + let error = block_on(dispatch_ingress_with_for_test( + &app, + Method::GET, + "/known".parse().expect("URI"), + move || { + observed_source_calls.fetch_add(1, Ordering::SeqCst); + Ok(empty::>()) + }, + move |_egress| { + observed_delivery_calls.fetch_add(1, Ordering::SeqCst); + Ok(()) + }, + )) + .expect_err("detached abort must return a platform error"); + + assert!(error.to_string().contains("ingress admission aborted")); + assert_eq!(source_calls.load(Ordering::SeqCst), 0); + assert_eq!(delivery_calls.load(Ordering::SeqCst), 0); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + assert_eq!(decision_resource_drops.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[test] + fn post_admission_failure_uses_owned_error_egress() { + let reports = Arc::new(Mutex::new(Vec::new())); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_grant_drops = Arc::clone(&grant_drops); + let mut app = App::new( + RouterService::builder() + .get("/owned", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("handler must not run") + }) + .build(), + ); + app.set_ingress_admission_policy(move |head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(DropSignal(Arc::clone(&observed_grant_drops))), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + }); + app.set_response_egress_observer(RecordingEgressObserver(Arc::clone(&reports))); + + let egress = block_on(dispatch_ingress_source_error_for_test( + &app, + Method::GET, + "/owned".parse().expect("URI"), + )) + .expect("owned error egress"); + let response = capture_response(egress); + + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + let observed = reports.lock().expect("reports lock"); + assert_eq!(observed.len(), 1); + let report = observed.first().expect("one report"); + assert_eq!(report.outcome, ResponseEgressOutcome::HostHandoff); + assert_eq!( + report.route.as_ref().map(RouteMetadata::pattern), + Some("/owned") + ); + } + + fn captured_response_body(response: Response) -> Bytes { + block_on(response.into_body().into_bytes_bounded(usize::MAX)) + .expect("captured response body") + } + + #[test] + fn exact_cap_preserves_not_found_and_method_not_allowed() { + for (path, expected_status) in [ + ("/missing", StatusCode::NOT_FOUND), + ("/known", StatusCode::METHOD_NOT_ALLOWED), + ] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let source = tracked_stream( + vec![Bytes::from_static(b"ab"), Bytes::from_static(b"cd")], + &grant_drops, + &source_drops, + &body_polls, + None, + ); + + let envelope = block_on(dispatch_ingress_stream_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + source, + )) + .expect("response"); + let response = capture_response(envelope); + + assert_eq!(response.status(), expected_status); + if expected_status == StatusCode::METHOD_NOT_ALLOWED { + assert_eq!( + response.headers().get("allow").expect("Allow header"), + "GET" + ); + } + assert_eq!(body_polls.load(Ordering::SeqCst), 3); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[test] + fn cap_plus_one_precedes_not_found_and_method_not_allowed() { + for path in ["/missing", "/known"] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let source = tracked_stream( + vec![Bytes::from_static(b"abcd"), Bytes::from_static(b"e")], + &grant_drops, + &source_drops, + &body_polls, + None, + ); + + let response = block_on(dispatch_ingress_stream_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + source, + )) + .expect("response"); + + assert_terminal_response( + response, + StatusCode::UNPROCESSABLE_ENTITY, + &terminal_headers("overflow"), + b"spin overflow\0response", + ); + assert_eq!(body_polls.load(Ordering::SeqCst), 2); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[test] + fn best_effort_pre_read_expiry_preserves_application_response() { + for path in ["/missing", "/known"] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let start = MonotonicInstant::now(); + let (mut app, handler_calls, middleware_calls) = + fallback_app(4, Duration::ZERO, &grant_drops); + app.set_monotonic_clock(MonotonicClock::new(move || start)); + let source = tracked_stream( + vec![Bytes::from_static(b"body")], + &grant_drops, + &source_drops, + &body_polls, + None, + ); + + let response = block_on(dispatch_ingress_stream_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + source, + )) + .expect("response"); + + assert_terminal_response( + response, + StatusCode::GATEWAY_TIMEOUT, + &terminal_headers("timeout"), + b"spin timeout response\n", + ); + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[test] + fn best_effort_post_ready_expiry_preserves_application_response() { + for path in ["/missing", "/known"] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let start = MonotonicInstant::now(); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let now = Arc::new(Mutex::new(start)); + let observed_now = Arc::clone(&now); + let (mut app, handler_calls, middleware_calls) = + fallback_app(4, Duration::from_secs(1), &grant_drops); + app.set_monotonic_clock(MonotonicClock::new(move || { + *observed_now.lock().expect("clock lock") + })); + let source = tracked_stream( + vec![Bytes::from_static(b"body")], + &grant_drops, + &source_drops, + &body_polls, + Some((now, deadline)), + ); + + let response = block_on(dispatch_ingress_stream_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + source, + )) + .expect("response"); + + assert_terminal_response( + response, + StatusCode::GATEWAY_TIMEOUT, + &terminal_headers("timeout"), + b"spin timeout response\n", + ); + assert_eq!(body_polls.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[test] + fn pending_matched_body_read_returns_408_and_releases_lifecycle() { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + matched_body_app(Duration::from_millis(100), &grant_drops); + let source = pending_stream(&grant_drops, &source_drops, &body_polls); + + let envelope = block_on(dispatch_ingress_stream_with_timer_for_test( + &app, + Method::POST, + "/body".parse().expect("URI"), + source, + |_remaining| deadline_timer(), + )) + .expect("Spin response"); + let response = capture_response(envelope); + + assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT); + assert_eq!( + response.headers().get("content-type"), + Some(&HeaderValue::from_static("application/json")) + ); + let bytes = captured_response_body(response); + assert_eq!( + bytes.as_ref(), + br#"{"error":{"kind":"request_timeout","message":"inbound body read deadline exceeded","status":408}}"# + ); + assert!(body_polls.load(Ordering::SeqCst) > 0); + assert_eq!(handler_calls.load(Ordering::SeqCst), 1); + assert_eq!(middleware_calls.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn simultaneously_ready_timer_precedes_matched_body() { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + matched_body_app(Duration::from_millis(100), &grant_drops); + let source = tracked_stream( + vec![Bytes::from_static(b"body")], + &grant_drops, + &source_drops, + &body_polls, + None, + ); + + let envelope = block_on(dispatch_ingress_stream_with_timer_for_test( + &app, + Method::POST, + "/body".parse().expect("URI"), + source, + |_remaining| ready(()), + )) + .expect("Spin response"); + let response = capture_response(envelope); + + assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT); + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + assert_eq!(handler_calls.load(Ordering::SeqCst), 1); + assert_eq!(middleware_calls.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn pending_fallback_body_read_preserves_timeout_response_and_releases_lifecycle() { + for path in ["/missing", "/known"] { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + fallback_app(4, Duration::from_millis(100), &grant_drops); + let source = pending_stream(&grant_drops, &source_drops, &body_polls); + + let response = block_on(dispatch_ingress_stream_with_timer_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + source, + |_remaining| deadline_timer(), + )) + .expect("Spin response"); + + assert_terminal_response( + response, + StatusCode::GATEWAY_TIMEOUT, + &terminal_headers("timeout"), + b"spin timeout response\n", + ); + assert!(body_polls.load(Ordering::SeqCst) > 0); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[test] + fn saturated_fallback_refuses_without_polling_body() { + for path in ["/missing", "/known"] { + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let observed_completion_calls = Arc::clone(&completion_calls); + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + app.set_ingress_admission_policy(move |head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::MethodNotAllowed { .. } | RouteResolution::NotFound + )); + AdmissionDecision::Refuse { + completion: { + let calls = Arc::clone(&observed_completion_calls); + ResponseEgressCompletion::new(move |_report| { + calls.fetch_add(1, Ordering::SeqCst); + }) + }, + response: response_builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .header("x-ingress-refusal", "saturated") + .body(Body::from("spin unavailable\n")) + .expect("refusal response"), + } + }); + let unobserved_grant = Arc::new(AtomicUsize::new(0)); + let source = tracked_stream( + vec![Bytes::from_static(b"body")], + &unobserved_grant, + &source_drops, + &body_polls, + None, + ); + + let envelope = block_on(dispatch_ingress_stream_for_test( + &app, + Method::POST, + path.parse().expect("URI"), + source, + )) + .expect("response"); + let mut expected_headers = HeaderMap::new(); + expected_headers.insert("x-ingress-refusal", HeaderValue::from_static("saturated")); + assert_terminal_response( + envelope, + StatusCode::SERVICE_UNAVAILABLE, + &expected_headers, + b"spin unavailable\n", + ); + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + assert_eq!(unobserved_grant.load(Ordering::SeqCst), 0); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } + + #[test] + fn source_error_uses_spin_response_boundary_and_releases_lifecycle() { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let source = error_stream(&grant_drops, &source_drops, &body_polls); + + let response = block_on(dispatch_ingress_stream_for_test( + &app, + Method::POST, + "/missing".parse().expect("URI"), + source, + )) + .expect("standard error response"); + + assert_terminal_response( + response, + StatusCode::INTERNAL_SERVER_ERROR, + &internal_error_headers(), + br#"{"error":{"kind":"internal","message":"internal server error","status":500}}"#, + ); + assert_eq!(body_polls.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + + #[test] + fn poll_then_drop_releases_pending_source_and_grant() { + let grant_drops = Arc::new(AtomicUsize::new(0)); + let source_drops = Arc::new(AtomicUsize::new(0)); + let body_polls = Arc::new(AtomicUsize::new(0)); + let (app, handler_calls, middleware_calls) = + fallback_app(4, Duration::from_secs(30), &grant_drops); + let source = pending_stream(&grant_drops, &source_drops, &body_polls); + + let outcome = dispatch_ingress_stream_for_test( + &app, + Method::POST, + "/missing".parse().expect("URI"), + source, + ) + .now_or_never(); + + assert!(outcome.is_none()); + assert_eq!(body_polls.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!(source_drops.load(Ordering::SeqCst), 1); + assert_no_route_dispatch(&handler_calls, &middleware_calls); + } + } +} + +#[cfg(test)] +#[cfg(all(feature = "test-utils", not(target_arch = "wasm32")))] +mod outbound_contract_tests { + use bytes::Bytes; + use edgezero_adapter_spin::outbound::{ + validate_batch_request_for_test, validate_request_for_test, + }; + use edgezero_core::body::Body; + use edgezero_core::http::{Method, Uri}; + use edgezero_core::outbound::OutboundRequest; + use futures_util::stream; + use std::time::Duration; + + #[test] + fn image_optimizer_rejection_precedes_other_preflight() { + let request = OutboundRequest::get("https://example.com/image") + .unwrap() + .image_optimizer(edgezero_core::OutboundImageOptimizerOptions::new()) + .host_authority_override("virtual.example") + .expect("authority") + .body(Body::from_stream(stream::pending())) + .timeout(Duration::ZERO); + for validate in [validate_request_for_test, validate_batch_request_for_test] { + let error = validate(&request).expect_err("unsupported option"); + assert!(error.message().contains("image_optimizer")); + assert!(error.message().contains("spin")); + } + } + + #[test] + fn unsupported_authority_precedes_generic_validation_without_polling() { + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::Poll; + + for method in [Method::GET, Method::HEAD, Method::CONNECT, Method::POST] { + let polls = Arc::new(AtomicUsize::new(0)); + let source_polls = Arc::clone(&polls); + let source = stream::poll_fn(move |_cx| { + source_polls.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + }); + let request = OutboundRequest::new(method, Uri::from_static("https://example.com/")) + .expect("request") + .host_authority_override("virtual.example") + .expect("authority") + .body(Body::from_stream(source)) + .max_brotli_window_bits(9) + .timeout(Duration::ZERO); + for validate in [validate_request_for_test, validate_batch_request_for_test] { + let error = validate(&request).expect_err("unsupported authority wins"); + assert_eq!( + error.message(), + "Spin cannot separate the outbound Host authority from the connection target" + ); + } + assert_eq!(polls.load(Ordering::SeqCst), 0); + } + } + + fn request() -> OutboundRequest { + OutboundRequest::new( + Method::POST, + "https://example.com/bid".parse::().expect("URI"), + ) + .expect("request") + } + + #[test] + fn buffered_upload_cap_has_typed_preflight_boundaries() { + for len in [7_usize, 8, 9] { + let request = OutboundRequest::post("https://example.com/upload") + .expect("request") + .body(vec![b'x'; len]) + .max_request_body_bytes(8); + let result = validate_batch_request_for_test(&request); + if len > 8 { + let error = result.expect_err("overflow before dispatch"); + assert_eq!( + error.request_body_overflow_origin(), + Some(edgezero_core::RequestBodyOverflowOrigin::OutboundUpload) + ); + } else { + result.expect("within cap"); + } + } + } + + #[test] + fn batch_preflight_precedence_and_indices() { + let requests = [ + request().body(Bytes::from_static(b"buffered")), + request().body(Body::stream(stream::once(async { + Bytes::from_static(b"streamed") + }))), + request().stream_response(), + request().body(Bytes::from_static(b"last")), + ]; + + let outcomes: Vec<_> = requests + .iter() + .map(validate_batch_request_for_test) + .collect(); + + outcomes[0].as_ref().expect("buffered slot 0"); + assert!(outcomes[1].is_err(), "streamed upload keeps slot index 1"); + assert!(outcomes[2].is_err(), "streamed response keeps slot index 2"); + outcomes[3].as_ref().expect("buffered slot 3"); + } + + #[test] + fn authority_override_fails_before_spin_dispatch() { + let request = request() + .host_authority_override("virtual.example") + .expect("valid portable authority"); + + let error = validate_request_for_test(&request).expect_err("unsupported on Spin"); + assert!(matches!(error, edgezero_core::EdgeError::BadRequest { .. })); + } } diff --git a/crates/edgezero-adapter-spin/tests/key_value_store_lazy.rs b/crates/edgezero-adapter-spin/tests/key_value_store_lazy.rs new file mode 100644 index 000000000..63870b77a --- /dev/null +++ b/crates/edgezero-adapter-spin/tests/key_value_store_lazy.rs @@ -0,0 +1,166 @@ +#![cfg(not(target_arch = "wasm32"))] + +extern crate self as spin_sdk; + +mod key_value { + use std::cell::Cell; + use std::fmt; + use std::future::{pending, ready}; + + thread_local! { + pub static OPENS: Cell = const { Cell::new(0) }; + pub static MODE: Cell = const { Cell::new(0) }; + } + + #[derive(Debug)] + pub enum Error { + AccessDenied, + NoSuchStore, + Other(String), + StoreTableFull, + } + + impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Other(message) => f.write_str(message), + Self::AccessDenied => f.write_str("access denied"), + Self::NoSuchStore => f.write_str("no such store"), + Self::StoreTableFull => f.write_str("store table full"), + } + } + } + + pub struct Store; + impl Store { + pub async fn delete(&self, _key: &str) -> Result<(), Error> { + ready(Ok(())).await + } + pub async fn exists(&self, _key: &str) -> Result { + ready(Ok(true)).await + } + pub async fn get(&self, key: &str) -> Result>, Error> { + if key == "pending" { + pending().await + } else { + Ok(Some(b"value".to_vec())) + } + } + pub async fn get_keys(&self) -> Keys { + ready(Keys).await + } + pub async fn open(_label: &str) -> Result { + OPENS.set(OPENS.get().saturating_add(1)); + match MODE.get() { + 0 => Ok(Self), + 1 => Err(Error::NoSuchStore), + 2 => Err(Error::AccessDenied), + 3 => Err(Error::StoreTableFull), + 4 => Err(Error::Other("credential-bearing-provider-token".to_owned())), + _ => pending().await, + } + } + pub async fn set(&self, _key: &str, _value: &[u8]) -> Result<(), Error> { + ready(Ok(())).await + } + } + pub struct Keys; + impl Keys { + pub async fn collect(self) -> Result, Error> { + ready(Ok(vec!["key".to_owned()])).await + } + } +} + +#[path = "../src/key_value_store.rs"] +mod key_value_store; +#[path = "../src/kv_pagination.rs"] +mod kv_pagination; + +#[cfg(test)] +mod tests { + use bytes::Bytes; + use edgezero_core::key_value_store::{KvError, KvStore as _, KvStoreOpenFailure}; + use futures::{FutureExt as _, executor::block_on}; + use std::time::Duration; + + use super::{key_value, key_value_store::SpinKvStore}; + + #[test] + fn declared_kv_async_open_is_request_local_and_shared() { + let first = SpinKvStore::declared("declared", "physical".to_owned(), 100); + assert_eq!(key_value::OPENS.get(), 0); + let mut read = Box::pin(first.get_bytes("pending")); + assert!(read.as_mut().now_or_never().is_none()); + assert!(block_on(first.exists("key")).unwrap()); + assert_eq!( + key_value::OPENS.get(), + 1, + "operation must not retain initialization lock" + ); + drop(read); + let second = SpinKvStore::declared("declared", "physical".to_owned(), 100); + assert!(block_on(second.exists("key")).unwrap()); + assert_eq!(key_value::OPENS.get(), 2); + } + + #[test] + fn declared_kv_open_failure_retains_binding_and_recovers() { + for (mode, expected) in [ + (1, KvStoreOpenFailure::MissingBinding), + (2, KvStoreOpenFailure::InvalidConfiguration), + (3, KvStoreOpenFailure::Unavailable), + (4, KvStoreOpenFailure::Unavailable), + ] { + key_value::MODE.set(mode); + let store = SpinKvStore::declared("failed", "provider-name-token".to_owned(), 100); + let before = key_value::OPENS.get(); + let failures = block_on(async { + vec![ + store.get_bytes("key").await.err(), + store.exists("key").await.err(), + store.put_bytes("key", Bytes::new()).await.err(), + store + .put_bytes_with_ttl("key", Bytes::new(), Duration::from_secs(60)) + .await + .err(), + store.delete("key").await.err(), + store.list_keys_page("", None, 1).await.err(), + ] + }); + for failure in failures { + let error = failure.expect("declared open failure"); + assert!(matches!(error, KvError::OpenFailed { reason } if reason == expected)); + assert!(!error.to_string().contains("token")); + } + assert_eq!(key_value::OPENS.get(), before + 1); + } + key_value::MODE.set(0); + let restored = SpinKvStore::declared("failed", "provider-name-token".to_owned(), 100); + assert_eq!( + block_on(restored.get_bytes("key")).unwrap(), + Some(Bytes::from_static(b"value")) + ); + assert!(matches!( + block_on(restored.put_bytes_with_ttl("key", Bytes::new(), Duration::from_secs(60))), + Err(KvError::Unsupported { .. }) + )); + let _store = block_on(SpinKvStore::open("physical")).expect("explicit open"); + } + + #[test] + fn cancelled_initializer_and_waiter_do_not_poison_request() { + key_value::MODE.set(5); + let store = SpinKvStore::declared("pending", "physical".to_owned(), 100); + let mut first = Box::pin(store.exists("key")); + assert!(first.as_mut().now_or_never().is_none()); + let mut waiter = Box::pin(store.exists("key")); + assert!(waiter.as_mut().now_or_never().is_none()); + assert_eq!(key_value::OPENS.get(), 1); + drop(waiter); + drop(first); + key_value::MODE.set(0); + assert!(block_on(store.exists("key")).unwrap()); + assert_eq!(key_value::OPENS.get(), 2); + } +} diff --git a/crates/edgezero-adapter-spin/tests/sdk_resources.rs b/crates/edgezero-adapter-spin/tests/sdk_resources.rs new file mode 100644 index 000000000..2f4fcd1bc --- /dev/null +++ b/crates/edgezero-adapter-spin/tests/sdk_resources.rs @@ -0,0 +1,1090 @@ +#![cfg(all(feature = "spin", feature = "test-utils", target_arch = "wasm32"))] + +#[cfg(test)] +#[expect( + clippy::arbitrary_source_item_ordering, + reason = "SDK construction checks stay before the exhaustive classifier fixture" +)] +mod tests { + use std::collections::VecDeque; + use std::rc::Rc; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + use edgezero_adapter_spin::outbound::{ + SpinOutboundClient, deferred_clock_paths_hold_for_test, map_spin_dispatch_error_for_test, + map_spin_host_error_for_test, map_spin_preparation_header_for_test, + provider_deadline_diagnostics_hold_for_test, request_fields_for_test, + request_target_errors_for_test, request_timeout_settings_for_test, + set_timeout_option_for_test, + }; + use edgezero_core::error::{BadGatewayReason, BudgetSource, EdgeError}; + use edgezero_core::error_detail::{ + ProviderFailureAdapter, ProviderFailureDetail, ProviderFailurePhase, SpinDnsRcode, + SpinFailureCause, SpinFieldSize, SpinTimeoutOption, + }; + use edgezero_core::http::{HeaderMap, HeaderValue, Method, Uri}; + use edgezero_core::outbound::{ + HttpClient, IndependentOutboundProgress, OutboundBatch, OutboundBatchTermination, + OutboundCollectedSlot, OutboundDispatchRequest, OutboundFuture, OutboundHead, + OutboundHttpClient, OutboundPending, OutboundPreparedBatch, OutboundProgressRuntime, + OutboundRequest, OutboundSendMode, OutboundTime, + }; + use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; + use futures::executor::block_on; + use spin_sdk::wasip3::http::types::{ + DnsErrorPayload, ErrorCode, FieldSizePayload, Fields, HeaderError, Request, RequestOptions, + RequestOptionsError, Response, TlsAlertReceivedPayload, + }; + use spin_sdk::wasip3::http_compat::BodyWriter; + + fn preflight_time(clock: MonotonicClock) -> OutboundTime { + OutboundTime::new(clock, |_, _| { + panic!("preflight must not register an outbound timer") + }) + } + + fn assert_outbound_client() + where + Client: OutboundHttpClient, + { + } + + struct HandoffClient { + inner: SpinOutboundClient, + observed: Arc>, + handoff_at: MonotonicInstant, + } + + impl OutboundHttpClient for HandoffClient { + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn time(&self) -> OutboundTime { + self.inner.time() + } + + fn validate_supported_options( + &self, + request: &OutboundRequest, + mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + self.inner.validate_supported_options(request, mode) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + self.inner.dispatch_head(request) + } + + fn begin(&self, request: OutboundDispatchRequest) -> Result { + self.inner.begin(request) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + *self.observed.lock().expect("paired clock") = self.handoff_at; + self.inner.start_batch_until(batch) + } + } + + fn handoff_client(start: MonotonicInstant, handoff_at: MonotonicInstant) -> HttpClient { + let observed = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&observed); + HttpClient::with_client(HandoffClient { + inner: SpinOutboundClient::with_time(preflight_time(MonotonicClock::new(move || { + *clock_now.lock().expect("paired clock") + }))), + observed, + handoff_at, + }) + } + + #[test] + fn expired_batch_handoff_completes_an_earlier_own_timeout_without_runtime() { + let start = MonotonicInstant::now(); + let observation_cutoff = Deadline::at_instant( + start + .checked_add(Duration::from_millis(100)) + .expect("cutoff"), + ); + let handoff_at = start + .checked_add(Duration::from_millis(150)) + .expect("handoff"); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .timeout(Duration::from_millis(50)); + let result = block_on( + handoff_client(start, handoff_at) + .start_batch_until(vec![request], observation_cutoff) + .collect(), + ) + .expect("an earlier own timeout must not be omitted at the expired handoff"); + assert_eq!(result.termination(), OutboundBatchTermination::Completed); + let [OutboundCollectedSlot::Resolved(slot)] = result.slots() else { + panic!("resolved own timeout"); + }; + assert_eq!(slot.elapsed, Duration::from_millis(50)); + assert!(matches!( + slot.outcome, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + )); + } + + #[test] + fn expired_batch_handoff_keeps_preflight_indices_and_cutoff_slots_without_runtime() { + use bytes::Bytes; + + let start = MonotonicInstant::now(); + let observation_cutoff = Deadline::at_instant( + start + .checked_add(Duration::from_millis(100)) + .expect("cutoff"), + ); + let handoff_at = start + .checked_add(Duration::from_millis(150)) + .expect("handoff"); + let upload = Arc::<[u8]>::from(&b"buffered upload"[..]); + let released = Arc::downgrade(&upload); + let requests = vec![ + OutboundRequest::post("https://example.com/upload") + .expect("request") + .body(Bytes::from_owner(upload)) + .timeout(Duration::from_millis(50)), + OutboundRequest::get("https://example.com/rejected") + .expect("request") + .stream_response(), + OutboundRequest::get("https://example.com/equal") + .expect("request") + .timeout(Duration::from_millis(100)), + OutboundRequest::get("https://example.com/later") + .expect("request") + .timeout(Duration::from_millis(200)), + OutboundRequest::get("https://example.com/earlier") + .expect("request") + .deadline(Deadline::at_instant( + start + .checked_add(Duration::from_millis(30)) + .expect("deadline"), + )), + ]; + let batch = + handoff_client(start, handoff_at).start_batch_until(requests, observation_cutoff); + assert!( + released.upgrade().is_none(), + "upload released before observation" + ); + let result = block_on(batch.collect()).expect("expired mixed handoff"); + assert_eq!(result.termination(), OutboundBatchTermination::Cutoff); + let [ + OutboundCollectedSlot::Resolved(own_timeout), + OutboundCollectedSlot::Resolved(preflight), + OutboundCollectedSlot::Unresolved, + OutboundCollectedSlot::Unresolved, + OutboundCollectedSlot::Resolved(earlier), + ] = result.slots() + else { + panic!("input indices and unresolved cutoff slots retained"); + }; + assert_eq!(own_timeout.elapsed, Duration::from_millis(50)); + assert!(matches!( + own_timeout.outcome, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + )); + assert_eq!(preflight.elapsed, Duration::ZERO); + assert!(matches!( + preflight.outcome, + Err(EdgeError::BadRequest { .. }) + )); + assert_eq!(earlier.elapsed, Duration::from_millis(30)); + assert!(matches!( + earlier.outcome, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + } + + #[test] + fn spin_outbound_client_implements_portable_contract() { + assert_outbound_client::(); + } + + #[test] + fn eager_spawn_outside_p3_context_is_a_typed_setup_error() { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::Poll; + + use edgezero_core::Body; + use futures_util::stream::poll_fn; + + let client = HttpClient::with_client(SpinOutboundClient::new()); + for begin in [false, true] { + let polls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&polls); + let owner = Arc::new(()); + let released = Arc::downgrade(&owner); + let source = poll_fn(move |_cx| { + let _keep_alive = Arc::clone(&owner); + observed.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + }); + let request = OutboundRequest::new( + if begin { Method::GET } else { Method::POST }, + Uri::from_static("https://example.com/"), + ) + .expect("request") + .body(Body::from_stream(source)); + let error = if begin { + block_on(client.begin(request)).expect_err("missing P3 scheduler") + } else { + block_on(client.send(request)).expect_err("missing P3 scheduler") + }; + assert!(matches!(error, EdgeError::Internal { .. })); + assert_eq!( + error.inner().expect("setup error").to_string(), + "Spin outbound scheduling requires an active WASIp3 task" + ); + assert_eq!(polls.load(Ordering::SeqCst), 0); + assert!(released.upgrade().is_none(), "unpolled upload released"); + } + let failure = block_on( + client + .start_batch_until( + vec![OutboundRequest::get("https://example.com/").expect("request")], + Deadline::after(Duration::from_secs(1)), + ) + .collect(), + ) + .expect_err("missing P3 batch scheduler"); + assert!(matches!(failure.error(), EdgeError::Internal { .. })); + assert_eq!( + failure.error().inner().expect("setup error").to_string(), + "Spin outbound scheduling requires an active WASIp3 task" + ); + assert!(matches!( + failure.slots(), + [OutboundCollectedSlot::Unresolved] + )); + } + + #[test] + fn connect_timeout_reaches_provider_settings() { + use std::mem::replace; + for (configured, expected) in [ + (None, 40_000_000), + (Some(Duration::from_millis(7)), 7_000_000), + (Some(Duration::from_millis(80)), 40_000_000), + ] { + for refusal in [ + Ok(()), + Err(RequestOptionsError::NotSupported), + Err(RequestOptionsError::Immutable), + Err(RequestOptionsError::Other(None)), + Err(RequestOptionsError::Other(Some( + "provider-private".to_owned(), + ))), + ] { + let mut retained = refusal; + let mut settings = Vec::new(); + request_timeout_settings_for_test( + Duration::from_millis(40), + configured, + |option, value| { + settings.push((option, value)); + if option == SpinTimeoutOption::Connect { + replace(&mut retained, Ok(())) + } else { + Ok(()) + } + }, + ) + .expect("connect refusals retain aggregate enforcement"); + assert_eq!( + settings, + [ + (SpinTimeoutOption::Connect, expected), + (SpinTimeoutOption::FirstByte, 40_000_000), + (SpinTimeoutOption::BetweenBytes, 40_000_000), + ] + ); + } + } + } + + #[test] + fn response_conversion_checks_deadline_before_handoff() { + assert!( + block_on(deferred_clock_paths_hold_for_test()), + "response handoff must retain deadlines and release SDK body consumers" + ); + } + + #[test] + fn injected_clock_controls_method_entry_and_preflight_elapsed() { + let start = MonotonicInstant::now(); + let completed = start + .checked_add(Duration::from_millis(7)) + .expect("completed instant"); + let observations = Arc::new(Mutex::new(VecDeque::from([start, completed]))); + let clock_observations = Arc::clone(&observations); + let client = HttpClient::with_client(SpinOutboundClient::with_time(preflight_time( + MonotonicClock::new(move || { + clock_observations + .lock() + .expect("clock observations") + .pop_front() + .unwrap_or(completed) + }), + ))); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + + let results = block_on( + client + .start_batch_until(vec![request], Deadline::after(Duration::from_secs(1))) + .collect(), + ) + .expect("valid batch driver"); + + let OutboundCollectedSlot::Resolved(slot) = &results.slots()[0] else { + panic!("resolved slot"); + }; + assert_eq!(slot.elapsed, Duration::from_millis(7)); + assert!(matches!(slot.outcome, Err(EdgeError::BadRequest { .. }))); + } + + #[test] + fn spin_unsupported_authority_precedes_generic_validation() { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::Poll; + + use edgezero_core::Body; + use edgezero_core::http::{Method, Uri}; + use futures_util::stream::poll_fn; + + let start = MonotonicInstant::now(); + let client = HttpClient::with_client(SpinOutboundClient::with_time(preflight_time( + MonotonicClock::new(move || start), + ))); + let observation_cutoff = Deadline::at_instant( + start + .checked_add(Duration::from_secs(1)) + .expect("observation cutoff"), + ); + for method in [Method::GET, Method::HEAD, Method::CONNECT, Method::POST] { + for batch in [false, true] { + let polls = Arc::new(AtomicUsize::new(0)); + let source_polls = Arc::clone(&polls); + let owner = Arc::new(()); + let released = Arc::downgrade(&owner); + let source = poll_fn(move |_cx| { + let _keep_alive = Arc::clone(&owner); + source_polls.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + }); + let request = + OutboundRequest::new(method.clone(), Uri::from_static("https://example.com/")) + .expect("request") + .host_authority_override("virtual.example") + .expect("authority") + .body(Body::from_stream(source)) + .max_brotli_window_bits(9) + .timeout(Duration::ZERO); + assert!(released.upgrade().is_some()); + let error = if batch { + let results = block_on( + client + .start_batch_until(vec![request], observation_cutoff) + .collect(), + ) + .expect("batch"); + assert_eq!(results.termination(), OutboundBatchTermination::Completed); + let (slots, _) = results.into_parts(); + let OutboundCollectedSlot::Resolved(slot) = + slots.into_iter().next().expect("slot") + else { + panic!("resolved unsupported authority"); + }; + slot.outcome.expect_err("unsupported authority") + } else { + block_on(client.send(request)).expect_err("unsupported authority") + }; + assert!(matches!(error, EdgeError::BadRequest { .. })); + assert_eq!( + error.message(), + "Spin cannot separate the outbound Host authority from the connection target" + ); + assert_eq!(polls.load(Ordering::SeqCst), 0); + assert!(released.upgrade().is_none(), "rejected source released"); + } + } + } + + #[test] + fn sdk_fields_preserve_duplicate_values() { + let fields = Fields::new(); + fields + .append("set-cookie", b"first=1") + .expect("first field append"); + fields + .append("set-cookie", b"second=2") + .expect("second field append"); + + assert_eq!( + fields.get("set-cookie"), + vec![b"first=1".to_vec(), b"second=2".to_vec()] + ); + } + + #[test] + fn sdk_request_options_execute_all_setters() { + let options = RequestOptions::new(); + options + .set_connect_timeout(Some(1_000_000)) + .expect("connect timeout is supported"); + options + .set_first_byte_timeout(Some(2_000_000)) + .expect("first-byte timeout is supported"); + options + .set_between_bytes_timeout(Some(3_000_000)) + .expect("between-bytes timeout is supported"); + } + + #[test] + fn provider_diagnostics_survive_ready_deadlines() { + assert!(block_on(provider_deadline_diagnostics_hold_for_test())); + } + + #[test] + fn provider_diagnostics_preserve_actual_preparation_header_rejection() { + let mut headers = HeaderMap::new(); + headers.insert("connection", HeaderValue::from_static("close")); + let error = request_fields_for_test(&headers).expect_err("host forbids connection"); + assert_eq!( + error.provider_failure_detail(), + Some(&ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::RequestPreparation, + cause: SpinFailureCause::HeaderForbidden, + }) + ); + assert_preparation_wire_unchanged( + EdgeError::bad_request("Spin rejected an outbound request header"), + error, + ); + headers.clear(); + headers.insert("x-test", HeaderValue::from_static("valid")); + request_fields_for_test(&headers).expect("accepted field"); + } + + #[test] + fn connect_timeout_refusals_remain_advisory() { + for outcome in [ + Ok(()), + Err(RequestOptionsError::NotSupported), + Err(RequestOptionsError::Immutable), + Err(RequestOptionsError::Other(None)), + Err(RequestOptionsError::Other(Some( + "provider-secret-token".to_owned(), + ))), + ] { + set_timeout_option_for_test(SpinTimeoutOption::Connect, &outcome) + .expect("connect settings retain aggregate deadline fallback"); + } + } + + #[test] + fn provider_diagnostics_preserve_preparation_setter_causes_without_text() { + for (source, cause) in [ + (HeaderError::Forbidden, SpinFailureCause::HeaderForbidden), + (HeaderError::Immutable, SpinFailureCause::HeaderImmutable), + ( + HeaderError::InvalidSyntax, + SpinFailureCause::HeaderInvalidSyntax, + ), + ( + HeaderError::SizeExceeded, + SpinFailureCause::HeaderSizeExceeded, + ), + (HeaderError::Other(None), SpinFailureCause::HeaderOther), + ( + HeaderError::Other(Some("provider-secret-token".to_owned())), + SpinFailureCause::HeaderOther, + ), + ] { + let error = map_spin_preparation_header_for_test(&source); + assert_eq!( + error.provider_failure_detail(), + Some(&ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::RequestPreparation, + cause, + }) + ); + assert_preparation_wire_unchanged( + EdgeError::bad_request("Spin rejected an outbound request header"), + error, + ); + } + for (option, name) in [ + (SpinTimeoutOption::FirstByte, "first-byte"), + (SpinTimeoutOption::BetweenBytes, "between-bytes"), + ] { + set_timeout_option_for_test(option, &Ok(())).expect("accepted option"); + set_timeout_option_for_test(option, &Err(RequestOptionsError::NotSupported)) + .expect("unsupported option stays advisory"); + for (source, cause, message) in [ + ( + RequestOptionsError::Immutable, + SpinFailureCause::RequestOptionsImmutable(option), + format!("Spin outbound {name} timeout option is immutable"), + ), + ( + RequestOptionsError::Other(Some("provider-secret-token".to_owned())), + SpinFailureCause::RequestOptionsOther(option), + format!("Spin host rejected the outbound {name} timeout option"), + ), + ( + RequestOptionsError::Other(None), + SpinFailureCause::RequestOptionsOther(option), + format!("Spin host rejected the outbound {name} timeout option"), + ), + ] { + let error = + set_timeout_option_for_test(option, &Err(source)).expect_err("rejection"); + assert_eq!( + error.provider_failure_detail(), + Some(&ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::RequestPreparation, + cause, + }) + ); + assert_preparation_wire_unchanged( + EdgeError::internal(anyhow::anyhow!(message)), + error, + ); + } + } + } + + fn assert_preparation_wire_unchanged(plain: EdgeError, diagnosed: EdgeError) { + use edgezero_core::response::IntoResponse as _; + + assert_eq!(plain.to_string(), diagnosed.to_string()); + assert_eq!(plain.message(), diagnosed.message()); + assert_eq!(plain.kind(), diagnosed.kind()); + assert_eq!(plain.status(), diagnosed.status()); + assert!( + !format!("{:?}", diagnosed.provider_failure_detail()).contains("provider-secret-token") + ); + let before = plain.into_response().expect("plain response"); + let after = diagnosed.into_response().expect("diagnosed response"); + assert_eq!(before.status(), after.status()); + assert_eq!(before.headers(), after.headers()); + assert_eq!(before.body().as_bytes(), after.body().as_bytes()); + } + + #[test] + fn provider_diagnostics_preserve_unit_target_rejection_without_fabricating_error_codes() { + let expected = [ + (SpinFailureCause::RequestAuthorityRejected, "authority"), + (SpinFailureCause::RequestMethodRejected, "method"), + (SpinFailureCause::RequestPathRejected, "path"), + (SpinFailureCause::RequestSchemeRejected, "scheme"), + ]; + for (error, (cause, name)) in request_target_errors_for_test().into_iter().zip(expected) { + assert_eq!( + error.provider_failure_detail(), + Some(&ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::RequestPreparation, + cause, + }) + ); + assert_preparation_wire_unchanged( + EdgeError::bad_request(format!("Spin rejected the outbound request {name}")), + error, + ); + } + } + + #[test] + fn sdk_request_and_response_completion_resources_construct() { + let (request_writer, request_body, request_trailers) = BodyWriter::new(); + let (request, request_done) = Request::new( + Fields::new(), + Some(request_body), + request_trailers, + Some(RequestOptions::new()), + ); + + let (response_writer, response_body, response_trailers) = BodyWriter::new(); + let (response, response_done) = + Response::new(Fields::new(), Some(response_body), response_trailers); + + drop((request, request_done, request_writer)); + drop((response, response_done, response_writer)); + } + + #[derive(Clone, Copy, Debug)] + enum ExpectedError { + BadRequest, + Internal, + Protocol, + Timeout, + Transport, + Unknown, + Unreachable, + } + + #[expect( + clippy::too_many_lines, + reason = "the pinned SDK table intentionally constructs every known ErrorCode variant" + )] + fn known_error_codes() -> Vec<(ErrorCode, ExpectedError)> { + let field = FieldSizePayload { + field_name: Some("x-test".to_owned()), + field_size: Some(1), + }; + vec![ + (ErrorCode::DnsTimeout, ExpectedError::Timeout), + ( + ErrorCode::DnsError(DnsErrorPayload { + rcode: Some("NXDOMAIN".to_owned()), + info_code: Some(3), + }), + ExpectedError::Unreachable, + ), + (ErrorCode::DestinationNotFound, ExpectedError::Unreachable), + ( + ErrorCode::DestinationUnavailable, + ExpectedError::Unreachable, + ), + ( + ErrorCode::DestinationIpProhibited, + ExpectedError::Unreachable, + ), + ( + ErrorCode::DestinationIpUnroutable, + ExpectedError::Unreachable, + ), + (ErrorCode::ConnectionRefused, ExpectedError::Unreachable), + (ErrorCode::ConnectionTerminated, ExpectedError::Transport), + (ErrorCode::ConnectionTimeout, ExpectedError::Timeout), + (ErrorCode::ConnectionReadTimeout, ExpectedError::Timeout), + (ErrorCode::ConnectionWriteTimeout, ExpectedError::Timeout), + ( + ErrorCode::ConnectionLimitReached, + ExpectedError::Unreachable, + ), + (ErrorCode::TlsProtocolError, ExpectedError::Unreachable), + (ErrorCode::TlsCertificateError, ExpectedError::Unreachable), + ( + ErrorCode::TlsAlertReceived(TlsAlertReceivedPayload { + alert_id: Some(42), + alert_message: Some("fixture".to_owned()), + }), + ExpectedError::Unreachable, + ), + (ErrorCode::HttpRequestDenied, ExpectedError::BadRequest), + ( + ErrorCode::HttpRequestLengthRequired, + ExpectedError::Internal, + ), + ( + ErrorCode::HttpRequestBodySize(Some(1)), + ExpectedError::BadRequest, + ), + (ErrorCode::HttpRequestMethodInvalid, ExpectedError::Internal), + (ErrorCode::HttpRequestUriInvalid, ExpectedError::Internal), + (ErrorCode::HttpRequestUriTooLong, ExpectedError::BadRequest), + ( + ErrorCode::HttpRequestHeaderSectionSize(Some(1)), + ExpectedError::BadRequest, + ), + ( + ErrorCode::HttpRequestHeaderSize(Some(field.clone())), + ExpectedError::BadRequest, + ), + ( + ErrorCode::HttpRequestTrailerSectionSize(Some(1)), + ExpectedError::Internal, + ), + ( + ErrorCode::HttpRequestTrailerSize(field.clone()), + ExpectedError::Internal, + ), + (ErrorCode::HttpResponseIncomplete, ExpectedError::Protocol), + ( + ErrorCode::HttpResponseHeaderSectionSize(Some(1)), + ExpectedError::Protocol, + ), + ( + ErrorCode::HttpResponseHeaderSize(field.clone()), + ExpectedError::Protocol, + ), + ( + ErrorCode::HttpResponseBodySize(Some(1)), + ExpectedError::Protocol, + ), + ( + ErrorCode::HttpResponseTrailerSectionSize(Some(1)), + ExpectedError::Protocol, + ), + ( + ErrorCode::HttpResponseTrailerSize(field), + ExpectedError::Protocol, + ), + ( + ErrorCode::HttpResponseTransferCoding(Some("fixture".to_owned())), + ExpectedError::Protocol, + ), + ( + ErrorCode::HttpResponseContentCoding(Some("fixture".to_owned())), + ExpectedError::Protocol, + ), + (ErrorCode::HttpResponseTimeout, ExpectedError::Timeout), + (ErrorCode::HttpUpgradeFailed, ExpectedError::Protocol), + (ErrorCode::HttpProtocolError, ExpectedError::Protocol), + (ErrorCode::LoopDetected, ExpectedError::Protocol), + (ErrorCode::ConfigurationError, ExpectedError::Internal), + ( + ErrorCode::InternalError(Some("fixture".to_owned())), + ExpectedError::Unknown, + ), + ] + } + + fn assert_expected_error(error: &EdgeError, expected: ExpectedError) { + match expected { + ExpectedError::BadRequest => assert!(matches!(error, EdgeError::BadRequest { .. })), + ExpectedError::Internal => assert!(matches!(error, EdgeError::Internal { .. })), + ExpectedError::Protocol => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Protocol, + .. + } + )), + ExpectedError::Timeout => assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::Unspecified, + .. + } + )), + ExpectedError::Transport => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Transport, + .. + } + )), + ExpectedError::Unknown => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Unspecified, + .. + } + )), + ExpectedError::Unreachable => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Unreachable, + .. + } + )), + } + } + + #[test] + fn spin_error_code_table_is_exhaustive() { + let now = MonotonicInstant::now(); + let live = Deadline::at_instant( + now.checked_add(Duration::from_secs(30)) + .expect("live deadline"), + ); + for (code, expected) in known_error_codes() { + let mapped = map_spin_host_error_for_test( + &code, + live, + BudgetSource::Default, + now, + ProviderFailurePhase::Send, + ); + assert_expected_error(&mapped, expected); + let detail = mapped + .provider_failure_detail() + .expect("every SDK cause retains detail"); + assert_eq!(detail.adapter(), ProviderFailureAdapter::Spin); + assert_eq!(detail.phase(), ProviderFailurePhase::Send); + + for connect in [None, Some(Duration::from_millis(7))] { + let dispatched = map_spin_dispatch_error_for_test( + &code, + live, + BudgetSource::Default, + now, + ProviderFailurePhase::Send, + connect, + ); + if connect.is_some() && matches!(&code, ErrorCode::ConnectionTimeout) { + assert!(matches!( + dispatched, + EdgeError::GatewayTimeout { + cause: BudgetSource::ConnectTimeout, + .. + } + )); + } else { + assert_expected_error(&dispatched, expected); + } + assert_eq!( + dispatched.provider_failure_detail(), + mapped.provider_failure_detail() + ); + } + + let expired = map_spin_host_error_for_test( + &code, + Deadline::at_instant(now), + BudgetSource::RequestDeadline, + now, + ProviderFailurePhase::Send, + ); + assert!(matches!( + expired, + EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + } + )); + } + } + + #[test] + fn provider_diagnostics_preserve_spin_numbers_and_discard_provider_text() { + let now = MonotonicInstant::now(); + let live = Deadline::at_instant(now.checked_add(Duration::from_secs(1)).expect("deadline")); + for (source, expected) in [ + ( + ErrorCode::TlsAlertReceived(TlsAlertReceivedPayload { + alert_id: Some(u8::MAX), + alert_message: Some("provider-secret-token".to_owned()), + }), + SpinFailureCause::TlsAlertReceived { + alert_id: Some(u8::MAX), + }, + ), + ( + ErrorCode::DnsError(DnsErrorPayload { + rcode: Some("provider-secret-token".to_owned()), + info_code: Some(u16::MAX), + }), + SpinFailureCause::DnsError { + rcode: Some(SpinDnsRcode::Unknown), + info_code: Some(u16::MAX), + }, + ), + ( + ErrorCode::DnsError(DnsErrorPayload { + rcode: Some("NXDOMAIN".to_owned()), + info_code: None, + }), + SpinFailureCause::DnsError { + rcode: Some(SpinDnsRcode::NameError), + info_code: None, + }, + ), + ( + ErrorCode::DnsError(DnsErrorPayload { + rcode: None, + info_code: None, + }), + SpinFailureCause::DnsError { + rcode: None, + info_code: None, + }, + ), + ( + ErrorCode::HttpRequestHeaderSize(Some(FieldSizePayload { + field_name: Some("provider-secret-token".to_owned()), + field_size: Some(u32::MAX), + })), + SpinFailureCause::HttpRequestHeaderSize(Some(SpinFieldSize { + field_size: Some(u32::MAX), + })), + ), + ( + ErrorCode::HttpRequestBodySize(Some(u64::MAX)), + SpinFailureCause::HttpRequestBodySize(Some(u64::MAX)), + ), + ( + ErrorCode::HttpResponseBodySize(Some(u64::MAX)), + SpinFailureCause::HttpResponseBodySize(Some(u64::MAX)), + ), + ( + ErrorCode::HttpResponseContentCoding(Some("provider-secret-token".to_owned())), + SpinFailureCause::HttpResponseContentCoding, + ), + ( + ErrorCode::HttpResponseTransferCoding(Some("provider-secret-token".to_owned())), + SpinFailureCause::HttpResponseTransferCoding, + ), + ( + ErrorCode::InternalError(Some("provider-secret-token".to_owned())), + SpinFailureCause::InternalError, + ), + ] { + let mapped = map_spin_host_error_for_test( + &source, + live, + BudgetSource::Default, + now, + ProviderFailurePhase::Send, + ); + assert_eq!( + mapped.provider_failure_detail(), + Some(&ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::Send, + cause: expected, + }) + ); + assert!(!format!("{mapped:?}").contains("provider-secret-token")); + } + } + + #[test] + fn spin_timeout_provenance_before_and_after_deadline() { + let now = MonotonicInstant::now(); + let live = Deadline::at_instant( + now.checked_add(Duration::from_secs(30)) + .expect("live deadline"), + ); + let early = map_spin_host_error_for_test( + &ErrorCode::DnsTimeout, + live, + BudgetSource::PerCallTimeout, + now, + ProviderFailurePhase::Send, + ); + assert!(matches!( + early, + EdgeError::GatewayTimeout { + cause: BudgetSource::Unspecified, + .. + } + )); + + let unreachable = map_spin_host_error_for_test( + &ErrorCode::ConnectionRefused, + live, + BudgetSource::Default, + now, + ProviderFailurePhase::Send, + ); + assert!(matches!( + unreachable, + EdgeError::BadGateway { + reason: BadGatewayReason::Unreachable, + .. + } + )); + } + + #[test] + fn connect_expiry_preserves_governing_budget_precedence() { + let start = MonotonicInstant::now(); + let end = start.checked_add(Duration::from_secs(1)).expect("deadline"); + for (observed, expected) in [ + (start, BudgetSource::Unspecified), + (end, BudgetSource::RequestDeadline), + ( + end.checked_add(Duration::from_nanos(1)) + .expect("late observation"), + BudgetSource::RequestDeadline, + ), + ] { + let error = map_spin_host_error_for_test( + &ErrorCode::ConnectionTimeout, + Deadline::at_instant(end), + BudgetSource::RequestDeadline, + observed, + ProviderFailurePhase::Send, + ); + assert!( + matches!(&error, EdgeError::GatewayTimeout { cause, .. } if *cause == expected) + ); + assert_eq!( + error.provider_failure_detail(), + Some(&ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::Send, + cause: SpinFailureCause::ConnectionTimeout, + }) + ); + } + } + + #[test] + fn connect_expiry_uses_each_request_setting_and_retains_provider_details() { + let start = MonotonicInstant::now(); + let end = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let late = end + .checked_add(Duration::from_nanos(1)) + .expect("late observation"); + for selected in [ + BudgetSource::Default, + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::BatchCutoff, + ] { + for phase in [ + ProviderFailurePhase::Send, + ProviderFailurePhase::RequestBodyUpload, + ProviderFailurePhase::ResponseBodyRead, + ] { + for configured in [None, Some(Duration::from_millis(7)), None] { + let mut request = + OutboundRequest::get("https://example.com/").expect("request"); + if let Some(timeout) = configured { + request = request.connect_timeout(timeout); + } + let parts = request.into_parts(); + assert_eq!(parts.connect_timeout, configured); + for observed in [start, end, late] { + let expected = if observed >= end { + selected + } else if configured.is_some() && phase == ProviderFailurePhase::Send { + BudgetSource::ConnectTimeout + } else { + BudgetSource::Unspecified + }; + let error = map_spin_dispatch_error_for_test( + &ErrorCode::ConnectionTimeout, + Deadline::at_instant(end), + selected, + observed, + phase, + parts.connect_timeout, + ); + assert!( + matches!(&error, EdgeError::GatewayTimeout { cause, .. } if *cause == expected) + ); + assert_eq!( + error.provider_failure_detail(), + Some(&ProviderFailureDetail::Spin { + phase, + cause: SpinFailureCause::ConnectionTimeout, + }) + ); + } + } + } + } + } +} diff --git a/crates/edgezero-adapter-spin/tests/secret_store_bounded.rs b/crates/edgezero-adapter-spin/tests/secret_store_bounded.rs new file mode 100644 index 000000000..908d9d667 --- /dev/null +++ b/crates/edgezero-adapter-spin/tests/secret_store_bounded.rs @@ -0,0 +1,193 @@ +#![cfg(not(target_arch = "wasm32"))] + +extern crate self as spin_sdk; + +mod variables { + use std::fmt; + + use futures::future; + + #[derive(Debug)] + pub(crate) enum Error { + InvalidName(String), + Other, + Undefined(String), + } + + impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidName(message) => write!(f, "invalid name: {message}"), + Self::Other => f.write_str("other"), + Self::Undefined(message) => write!(f, "undefined: {message}"), + } + } + } + + pub(crate) async fn get(_key: &str) -> Result { + future::pending().await + } +} + +#[path = "../src/secret_store.rs"] +mod secret_store; + +#[cfg(test)] +mod tests { + use std::cell::Cell; + use std::mem; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + use bytes::Bytes; + use edgezero_core::secret_store::SecretError; + use edgezero_core::time::{Deadline, MonotonicClock, MonotonicInstant}; + use futures::executor::block_on; + + use super::{secret_store, variables}; + + #[test] + fn source_harness_constructs_spin_types() { + let store = secret_store::SpinSecretStore::new(); + assert_eq!(mem::size_of_val(&store), 0); + let errors = [ + variables::Error::Undefined(String::new()), + variables::Error::InvalidName(String::new()), + variables::Error::Other, + ]; + for error in errors { + match error { + variables::Error::Undefined(message) | variables::Error::InvalidName(message) => { + assert!(message.is_empty()); + } + variables::Error::Other => {} + } + } + } + + #[test] + fn bounded_secret_reports_exact_bytes_and_accepts_exact_caps() { + let clock = MonotonicClock::default(); + let result = block_on(secret_store::bounded_secret_read( + async { Ok(Some(Bytes::from_static(b"value"))) }, + &clock, + Deadline::after(Duration::from_secs(1)), + 5, + 5, + )) + .expect("exact caps must succeed"); + + assert_eq!(result.backend_bytes, 5); + assert_eq!(result.value, Some(Bytes::from_static(b"value"))); + } + + #[test] + fn bounded_secret_rejects_either_exceeded_cap() { + for (max_backend_bytes, max_value_bytes) in [(4, 5), (5, 4)] { + let clock = MonotonicClock::default(); + let error = block_on(secret_store::bounded_secret_read( + async { Ok(Some(Bytes::from_static(b"value"))) }, + &clock, + Deadline::after(Duration::from_secs(1)), + max_backend_bytes, + max_value_bytes, + )) + .expect_err("an exceeded cap must fail"); + + assert!(matches!(error, SecretError::ValueTooLarge)); + } + } + + #[test] + fn bounded_secret_checks_deadline_before_polling_host_call() { + let polled = Cell::new(false); + let clock = MonotonicClock::default(); + let error = block_on(secret_store::bounded_secret_read( + async { + polled.set(true); + Ok(None) + }, + &clock, + Deadline::after(Duration::ZERO), + 1, + 1, + )) + .expect_err("expired deadline must fail"); + + assert!(matches!(error, SecretError::DeadlineExceeded)); + assert!(!polled.get(), "expired reads must not poll the host call"); + } + + #[test] + fn bounded_secret_checks_deadline_after_host_call() { + let start = MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let now = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let error = block_on(secret_store::bounded_secret_read( + async { + *now.lock().expect("clock lock") = terminal; + Ok(None) + }, + &clock, + Deadline::at_instant(terminal), + 1, + 1, + )) + .expect_err("a host call completing after the deadline must fail"); + + assert!(matches!(error, SecretError::DeadlineExceeded)); + } + + #[test] + fn bounded_secret_deadline_wins_over_late_host_error() { + let start = MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let now = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let error = block_on(secret_store::bounded_secret_read( + async { + *now.lock().expect("clock lock") = terminal; + Err(SecretError::Unavailable) + }, + &clock, + Deadline::at_instant(terminal), + 1, + 1, + )) + .expect_err("the post-call deadline check must run after host errors"); + + assert!(matches!(error, SecretError::DeadlineExceeded)); + } + + #[test] + fn bounded_secret_read_uses_injected_clock() { + let process_now = MonotonicInstant::now(); + let injected_now = process_now + .checked_sub(Duration::from_mins(1)) + .expect("injected instant"); + let clock = MonotonicClock::new(move || injected_now); + let deadline = Deadline::at_instant( + injected_now + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), + ); + + let result = block_on(secret_store::bounded_secret_read( + async { Ok(Some(Bytes::from_static(b"value"))) }, + &clock, + deadline, + 5, + 5, + )) + .expect("the application clock is still before its deadline"); + + assert_eq!(result.value, Some(Bytes::from_static(b"value"))); + } +} diff --git a/crates/edgezero-adapter/Cargo.toml b/crates/edgezero-adapter/Cargo.toml index 5fbbe1fec..c1dd41ffe 100644 --- a/crates/edgezero-adapter/Cargo.toml +++ b/crates/edgezero-adapter/Cargo.toml @@ -15,6 +15,7 @@ default = [] cli = ["dep:base16ct", "dep:serde", "dep:serde_json", "dep:sha2", "dep:toml", "dep:walkdir"] [dependencies] +edgezero-core = { workspace = true } toml = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } diff --git a/crates/edgezero-adapter/src/cli_support.rs b/crates/edgezero-adapter/src/cli_support.rs index a5e664d9f..33d895a68 100644 --- a/crates/edgezero-adapter/src/cli_support.rs +++ b/crates/edgezero-adapter/src/cli_support.rs @@ -8,6 +8,10 @@ use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::process::Command; +#[cfg(test)] +use edgezero_core::manifest::ManifestLoader; +use edgezero_core::manifest::{Manifest, canonicalize_outbound_hosts}; + /// Walks up the directory tree looking for `manifest_name` alongside a `Cargo.toml`. #[inline] #[must_use] @@ -126,6 +130,112 @@ pub fn read_package_name(manifest: &Path) -> Result { )) } +/// Verifies the selected Spin component's outbound host set against the application contract. +/// +/// # Errors +/// Returns an operator-facing error when the Spin manifest/component is invalid or its canonical +/// `allowed_outbound_hosts` set differs from `[capabilities.outbound].hosts`. +#[inline] +pub fn validate_spin_outbound_host_contract( + manifest: &Manifest, + manifest_path: &Path, + spin_manifest_path: &Path, + component_selector: Option<&str>, +) -> Result<(), String> { + let raw = fs::read_to_string(spin_manifest_path).map_err(|error| { + format!( + "failed to read Spin manifest at {}: {error}", + spin_manifest_path.display() + ) + })?; + let parsed: toml::Value = toml::from_str(&raw).map_err(|error| { + format!( + "failed to parse {} as TOML: {error}", + spin_manifest_path.display() + ) + })?; + let components = parsed + .get("component") + .and_then(toml::Value::as_table) + .ok_or_else(|| { + format!( + "{}: no [component.*] declarations found", + spin_manifest_path.display() + ) + })?; + let component = match component_selector { + Some(selected) if components.contains_key(selected) => selected.to_owned(), + Some(selected) => { + return Err(format!( + "Spin component {selected:?} is not declared in {}", + spin_manifest_path.display() + )); + } + None if components.len() == 1 => components + .keys() + .next() + .cloned() + .ok_or_else(|| "Spin component selection failed".to_owned())?, + None => { + return Err(format!( + "{} declares {} components but no Spin component was selected", + spin_manifest_path.display(), + components.len() + )); + } + }; + let component_table = components + .get(&component) + .and_then(toml::Value::as_table) + .ok_or_else(|| { + format!( + "[component.{component}] in {} must be a table", + spin_manifest_path.display() + ) + })?; + let platform_values = component_table + .get("allowed_outbound_hosts") + .and_then(toml::Value::as_array) + .ok_or_else(|| { + format!( + "[component.{component}].allowed_outbound_hosts is missing or not an array in {}", + spin_manifest_path.display() + ) + })?; + let platform_hosts = platform_values + .iter() + .map(|value| { + value.as_str().map(str::to_owned).ok_or_else(|| { + format!( + "[component.{component}].allowed_outbound_hosts in {} must contain only strings", + spin_manifest_path.display() + ) + }) + }) + .collect::, _>>()?; + let expected = canonicalize_outbound_hosts(manifest.capabilities.outbound.hosts.as_deref()) + .map_err(|error| { + format!( + "invalid outbound host contract in {}: {error}", + manifest_path.display() + ) + })?; + let actual = canonicalize_outbound_hosts(Some(&platform_hosts)).map_err(|error| { + format!( + "invalid [component.{component}].allowed_outbound_hosts in {}: {error}", + spin_manifest_path.display() + ) + })?; + if actual == expected { + return Ok(()); + } + Err(format!( + "Spin outbound host drift: {} selects component `{component}` in {}; expected canonical allowed_outbound_hosts {expected:?}, found {actual:?}. Update that component field and rerun.", + manifest_path.display(), + spin_manifest_path.display() + )) +} + #[cfg(test)] mod tests { use super::*; @@ -242,4 +352,105 @@ mod tests { "expected the exit-status branch, got: {err}" ); } + + #[test] + fn spin_selected_component_outbound_hosts_are_canonical_and_order_independent() { + let dir = tempdir().expect("temp dir"); + let manifest_path = dir.path().join("edgezero.toml"); + let spin_path = dir.path().join("spin.toml"); + fs::write( + &manifest_path, + "[capabilities.outbound]\nhosts = [\"*\", \"api.example.com\"]\n", + ) + .expect("write app manifest"); + fs::write( + &spin_path, + "[component.app]\nallowed_outbound_hosts = [\"https://api.example.com\", \"https://*:*\", \"http://*:*\"]\n", + ) + .expect("write Spin manifest"); + let loader = ManifestLoader::from_path(&manifest_path).expect("load manifest"); + + validate_spin_outbound_host_contract(loader.manifest(), &manifest_path, &spin_path, None) + .expect("canonical host sets match"); + } + + #[test] + fn spin_selected_component_outbound_host_drift_fails_closed() { + let dir = tempdir().expect("temp dir"); + let manifest_path = dir.path().join("edgezero.toml"); + let spin_path = dir.path().join("spin.toml"); + fs::write(&manifest_path, "").expect("write app manifest"); + fs::write( + &spin_path, + "[component.first]\nallowed_outbound_hosts = [\"https://*:*\"]\n[component.second]\nallowed_outbound_hosts = [\"https://api.example.com\"]\n", + ) + .expect("write Spin manifest"); + let loader = ManifestLoader::from_path(&manifest_path).expect("load manifest"); + + let error = validate_spin_outbound_host_contract( + loader.manifest(), + &manifest_path, + &spin_path, + Some("second"), + ) + .expect_err("selected component drift"); + + assert!(error.contains("component `second`"), "{error}"); + assert!(error.contains("https://*:*"), "{error}"); + assert!(error.contains("https://api.example.com"), "{error}"); + } + + #[test] + fn spin_selected_component_applies_default_and_rejects_malformed_platform_hosts() { + let dir = tempdir().expect("temp dir"); + let manifest_path = dir.path().join("edgezero.toml"); + let spin_path = dir.path().join("spin.toml"); + fs::write(&manifest_path, "").expect("write app manifest"); + fs::write( + &spin_path, + "[component.app]\nallowed_outbound_hosts = [\"https://*:*\"]\n", + ) + .expect("write Spin manifest"); + let loader = ManifestLoader::from_path(&manifest_path).expect("load manifest"); + validate_spin_outbound_host_contract(loader.manifest(), &manifest_path, &spin_path, None) + .expect("absent app hosts retain the HTTPS-only default"); + + fs::write( + &spin_path, + "[component.app]\nallowed_outbound_hosts = [\"ftp://example.com\"]\n", + ) + .expect("replace Spin manifest"); + let error = validate_spin_outbound_host_contract( + loader.manifest(), + &manifest_path, + &spin_path, + None, + ) + .expect_err("malformed platform host"); + assert!(error.contains("invalid [component.app]"), "{error}"); + } + + #[test] + fn spin_selected_component_requires_unambiguous_component() { + let dir = tempdir().expect("temp dir"); + let manifest_path = dir.path().join("edgezero.toml"); + let spin_path = dir.path().join("spin.toml"); + fs::write(&manifest_path, "").expect("write app manifest"); + fs::write( + &spin_path, + "[component.first]\nallowed_outbound_hosts = [\"https://*:*\"]\n[component.second]\nallowed_outbound_hosts = [\"https://*:*\"]\n", + ) + .expect("write Spin manifest"); + let loader = ManifestLoader::from_path(&manifest_path).expect("load manifest"); + + let error = validate_spin_outbound_host_contract( + loader.manifest(), + &manifest_path, + &spin_path, + None, + ) + .expect_err("ambiguous component"); + + assert!(error.contains("2 components"), "{error}"); + } } diff --git a/crates/edgezero-adapter/src/registry.rs b/crates/edgezero-adapter/src/registry.rs index 28cc78140..cfb8f90f4 100644 --- a/crates/edgezero-adapter/src/registry.rs +++ b/crates/edgezero-adapter/src/registry.rs @@ -2,6 +2,8 @@ use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use std::sync::{LazyLock, PoisonError, RwLock}; +use edgezero_core::{Capability, CapabilitySupport, PlatformMetadata}; + static REGISTRY: LazyLock>> = LazyLock::new(|| RwLock::new(HashMap::new())); @@ -41,6 +43,71 @@ pub enum AdapterAction { Serve, } +/// Canonical application target selected by the CLI before adapter dispatch. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct AdapterExecutionTarget { + app_root: PathBuf, + component: Option, + environment_defaults: Vec<(String, String)>, + platform_manifest: Option, +} + +impl AdapterExecutionTarget { + /// Canonical application root used for this action. + #[must_use] + #[inline] + pub fn app_root(&self) -> &Path { + &self.app_root + } + + /// Optional platform component selected by the manifest. + #[must_use] + #[inline] + pub fn component(&self) -> Option<&str> { + self.component.as_deref() + } + + /// Selected-manifest defaults not already supplied by the parent environment. + /// Declaration order is retained; child commands apply the last applicable value. + #[must_use] + #[inline] + pub fn environment_defaults(&self) -> &[(String, String)] { + &self.environment_defaults + } + + /// Construct a target after the caller has canonicalized and validated it. + #[must_use] + #[inline] + pub fn new( + app_root: PathBuf, + component: Option, + platform_manifest: Option, + ) -> Self { + Self { + app_root, + component, + environment_defaults: Vec::new(), + platform_manifest, + } + } + + /// Canonical platform manifest selected for this action, when applicable. + #[must_use] + #[inline] + pub fn platform_manifest(&self) -> Option<&Path> { + self.platform_manifest.as_deref() + } + + /// Carry filtered runtime defaults to the adapter without mutating process state. + #[must_use] + #[inline] + pub fn with_environment_defaults(mut self, defaults: Vec<(String, String)>) -> Self { + self.environment_defaults = defaults; + self + } +} + /// Logical store ids declared by the application manifest for a deploy. /// /// This stays platform-neutral: each adapter decides whether and how its @@ -79,6 +146,8 @@ pub struct AdapterDeployContext { pub application_manifest_path: Option, /// Canonical root of an extracted immutable application release. pub application_release_root: Option, + /// Pinned runtime target; deployment must not rediscover source from cwd. + pub execution_target: Option, pub service_id: Option, pub staging: bool, pub stores: DeployStoreIds, @@ -300,7 +369,7 @@ pub enum ReadConfigEntry { /// or pushed under a different key). MissingKey, /// The store itself is absent — wrangler.toml has no matching binding, - /// fastly.toml has no setup table, axum's local-config-.json file + /// fastly.toml has no setup table, axum's `local-config-.json` file /// doesn't exist yet. MissingStore, /// The remote held the key; the body is the serialised envelope JSON. @@ -314,33 +383,23 @@ pub enum ReadConfigEntry { /// Interface implemented by adapter crates to integrate with the `EdgeZero` CLI. /// /// The non-`execute` methods carry the adapter's `config validate` -/// rules. They take primitive parameters (no `Manifest` / -/// `SecretField` from `edgezero-core`) so this crate stays dep-free -/// of `edgezero-core`. Defaults are no-ops; adapters override what -/// they actually need. -#[expect( - clippy::arbitrary_source_item_ordering, - reason = "deploy lifecycle hooks read in invocation order: preflight before deploy" -)] +/// rules. Defaults are no-ops; adapters override what they actually need. pub trait Adapter: Sync + Send { - /// Decide whether the manifest command or adapter owns this deployment. + /// Report this adapter's support for an application capability. /// - /// # Errors - /// Returns an error string when the adapter cannot safely select a deploy path. + /// The fail-closed default prevents an adapter from accidentally claiming + /// support merely because a new capability was added to core. + #[must_use] #[inline] - fn preflight_deploy( - &self, - _context: &AdapterDeployContext, - _args: &[String], - ) -> Result { - Ok(DeployOwnership::ManifestCommand) + fn capability(&self, _capability: Capability) -> CapabilitySupport { + CapabilitySupport::Unsupported } /// Deploy with EdgeZero-owned inputs carried as typed context and only /// provider-native passthrough in `args`. /// /// Adapters that do not need structured deploy data can use the default - /// dispatch to their existing `execute` implementation. + /// dispatch to their pinned `execute_target` implementation. /// /// # Errors /// Returns an error string when the adapter deploy fails. @@ -351,7 +410,11 @@ pub trait Adapter: Sync + Send { } else { AdapterAction::Deploy }; - self.execute(action, args) + let target = context + .execution_target + .as_ref() + .ok_or_else(|| "adapter deployment requires a pinned execution target".to_owned())?; + self.execute_target(action, target, args) } /// Execute the requested action with optional adapter-specific args. @@ -370,6 +433,27 @@ pub trait Adapter: Sync + Send { /// Returns an error string if the requested adapter action fails. fn execute(&self, action: AdapterAction, args: &[String]) -> Result<(), String>; + /// Execute against a target already selected and validated by the CLI. + /// + /// The default refuses rather than falling back to [`Self::execute`], which + /// could rediscover a different project from the process working directory. + /// + /// # Errors + /// Returns an explicit unsupported error unless the adapter implements + /// pinned-target dispatch. + #[inline] + fn execute_target( + &self, + _action: AdapterAction, + _target: &AdapterExecutionTarget, + _args: &[String], + ) -> Result<(), String> { + Err(format!( + "adapter `{}` does not support pinned execution targets", + self.name() + )) + } + /// Finish a successful deploy. /// /// `command_output` is present when a manifest-defined deploy command ran @@ -452,6 +536,15 @@ pub trait Adapter: Sync + Send { /// Name used to reference the adapter (case-insensitive). fn name(&self) -> &'static str; + /// Report the target's resource metadata. + /// + /// The default leaves every fact unknown. + #[must_use] + #[inline] + fn platform_metadata(&self) -> PlatformMetadata { + PlatformMetadata::default() + } + /// Reject a config `(key, body)` that this adapter cannot store, BEFORE any /// provider I/O. Called by `config push` ahead of the remote read, so an /// infeasible push fails offline instead of after a `list`/`describe` @@ -472,27 +565,17 @@ pub trait Adapter: Sync + Send { Ok(()) } - /// Validate the final config key selected by CLI and environment precedence - /// before config push or diff performs provider I/O. - /// - /// `logical_store_id` is the portable manifest ID. `key` is the final key - /// the operation would read or write. `staging` identifies the requested - /// deployment target, and `local` identifies an emulator operation. - /// Adapters whose runtimes use fixed keys can reject a divergent selection; - /// the default preserves configurable keys. + /// Decide whether the manifest command or adapter owns this deployment. /// /// # Errors - /// Returns a human-readable error when the selected key cannot be read by - /// this adapter's runtime for the requested target. + /// Returns an error string when the adapter cannot safely select a deploy path. #[inline] - fn validate_config_key_for_target( + fn preflight_deploy( &self, - _logical_store_id: &str, - _key: &str, - _staging: bool, - _local: bool, - ) -> Result<(), String> { - Ok(()) + _context: &AdapterDeployContext, + _args: &[String], + ) -> Result { + Ok(DeployOwnership::ManifestCommand) } /// Provision the platform resources backing each store id the @@ -710,6 +793,29 @@ pub trait Adapter: Sync + Send { Ok(()) } + /// Validate the final config key selected by CLI and environment precedence + /// before config push or diff performs provider I/O. + /// + /// `logical_store_id` is the portable manifest ID. `key` is the final key + /// the operation would read or write. `staging` identifies the requested + /// deployment target, and `local` identifies an emulator operation. + /// Adapters whose runtimes use fixed keys can reject a divergent selection; + /// the default preserves configurable keys. + /// + /// # Errors + /// Returns a human-readable error when the selected key cannot be read by + /// this adapter's runtime for the requested target. + #[inline] + fn validate_config_key_for_target( + &self, + _logical_store_id: &str, + _key: &str, + _staging: bool, + _local: bool, + ) -> Result<(), String> { + Ok(()) + } + /// Typed-only check that needs `#[secret]` field values — the /// CLI calls this only from the typed validation flow. /// `entries` carries both `KeyInDefault` and `KeyInNamedStore` @@ -786,6 +892,35 @@ mod tests { name: &'static str, } + struct CapabilityAdapter; + + #[expect( + clippy::missing_trait_methods, + reason = "fixture overrides only the behavior under test" + )] + impl Adapter for CapabilityAdapter { + #[expect( + clippy::wildcard_enum_match_arm, + reason = "Capability is non-exhaustive and this fixture defaults all untested capabilities to unsupported" + )] + fn capability(&self, capability: Capability) -> CapabilitySupport { + match capability { + Capability::LazyStreamedResponsePassthrough => CapabilitySupport::BestEffort, + Capability::OutboundDeadlines => CapabilitySupport::BoundedCooperative, + Capability::OutboundHttp => CapabilitySupport::Native, + _ => CapabilitySupport::Unsupported, + } + } + + fn execute(&self, _action: AdapterAction, _args: &[String]) -> Result<(), String> { + Ok(()) + } + + fn name(&self) -> &'static str { + "capability-fixture" + } + } + #[expect( clippy::missing_trait_methods, reason = "TestAdapter only exercises register / get / execute; the validation methods inherit the trait defaults (no-ops)" @@ -807,6 +942,66 @@ mod tests { HIT.store(0, Ordering::SeqCst); } + #[test] + fn adapter_capability_default_is_unsupported() { + assert_eq!( + FIRST.capability(Capability::OutboundHttp), + CapabilitySupport::Unsupported + ); + } + + #[test] + fn adapter_platform_metadata_default_is_unknown() { + let metadata = FIRST.platform_metadata(); + + assert_eq!(metadata, edgezero_core::PlatformMetadata::default()); + assert_eq!( + metadata.memory_ceiling(), + edgezero_core::PlatformFact::unknown(edgezero_core::PlatformUnknownReason::Unspecified) + ); + assert_eq!( + metadata.inbound_request_population_bound(), + edgezero_core::PlatformFact::unknown(edgezero_core::PlatformUnknownReason::Unspecified) + ); + assert_eq!( + metadata.host_ingress_memory_accounting(), + edgezero_core::PlatformFact::unknown(edgezero_core::PlatformUnknownReason::Unspecified) + ); + } + + #[test] + fn adapter_capability_reports_each_support_level() { + let adapter = CapabilityAdapter; + assert_eq!( + adapter.capability(Capability::OutboundHttp), + CapabilitySupport::Native + ); + assert_eq!( + adapter.capability(Capability::OutboundDeadlines), + CapabilitySupport::BoundedCooperative + ); + assert_eq!( + adapter.capability(Capability::LazyStreamedResponsePassthrough), + CapabilitySupport::BestEffort + ); + assert_eq!( + adapter.capability(Capability::OutboundCompleteResourceAccounting), + CapabilitySupport::Unsupported + ); + } + + #[test] + fn adapter_execute_target_default_refuses_rediscovery() { + let _guard = TEST_LOCK.lock().expect("lock"); + reset(); + let target = AdapterExecutionTarget::new(PathBuf::from("/tmp"), None, None); + let error = FIRST + .execute_target(AdapterAction::Build, &target, &[]) + .expect_err("default target execution must fail closed"); + assert!(error.contains("pinned execution targets")); + assert_eq!(HIT.load(Ordering::SeqCst), 0); + } + #[test] fn default_deploy_preflight_keeps_manifest_command() { let context = AdapterDeployContext::default(); @@ -819,6 +1014,29 @@ mod tests { assert!(context.variable_defaults.is_empty()); } + #[test] + fn default_deploy_never_rediscovers_an_unpinned_target() { + let _guard = TEST_LOCK.lock().expect("lock"); + reset(); + let missing = FIRST + .deploy(&AdapterDeployContext::default(), &[]) + .expect_err("unpinned deploy must fail before execute"); + assert!(missing.contains("pinned execution target")); + let context = AdapterDeployContext { + execution_target: Some(AdapterExecutionTarget::new( + PathBuf::from("/selected"), + None, + None, + )), + ..AdapterDeployContext::default() + }; + let unsupported = FIRST + .deploy(&context, &[]) + .expect_err("default pinned dispatch must not fall back to execute"); + assert!(unsupported.contains("pinned execution targets")); + assert_eq!(HIT.load(Ordering::SeqCst), 0); + } + #[test] fn registers_and_fetches_adapter() { let _guard = TEST_LOCK.lock().expect("lock"); @@ -941,4 +1159,19 @@ mod tests { ); assert!(local_err.contains("--local"), "msg: {local_err}"); } + + #[test] + fn execution_target_retains_ordered_defaults_without_changing_paths() { + let empty = AdapterExecutionTarget::new(PathBuf::from("/tmp"), None, None); + assert!(empty.environment_defaults().is_empty()); + let defaults = vec![ + ("key".to_owned(), "first".to_owned()), + ("KEY".to_owned(), "last".to_owned()), + ]; + let selected = empty.with_environment_defaults(defaults.clone()); + let cloned = selected.clone(); + assert_eq!(cloned.environment_defaults(), defaults); + assert_eq!(cloned.app_root(), Path::new("/tmp")); + assert_eq!(cloned, selected); + } } diff --git a/crates/edgezero-cli/Cargo.toml b/crates/edgezero-cli/Cargo.toml index ce988d6a5..ea387f958 100644 --- a/crates/edgezero-cli/Cargo.toml +++ b/crates/edgezero-cli/Cargo.toml @@ -30,7 +30,7 @@ app-demo-core = { path = "../../examples/app-demo/crates/app-demo-core", package chrono = { workspace = true } clap = { version = "4", features = ["derive"], optional = true } futures = { workspace = true } -handlebars = { workspace = true } +handlebars = { workspace = true, optional = true } log = { workspace = true } serde = { workspace = true } similar = { workspace = true } @@ -48,6 +48,7 @@ toml = { workspace = true } [dev-dependencies] edgezero-core = { workspace = true, features = ["test-utils"] } +httparse = { workspace = true } tempfile = { workspace = true } [features] @@ -58,6 +59,6 @@ default = [ "edgezero-adapter-cloudflare", "edgezero-adapter-spin", ] -cli = ["dep:clap"] +cli = ["dep:clap", "dep:handlebars"] demo-example = ["dep:app-demo-core", "edgezero-adapter-axum"] nested-app-config-check = ["dep:proc-macro2", "dep:syn", "dep:walkdir"] diff --git a/crates/edgezero-cli/README.md b/crates/edgezero-cli/README.md index d04dec2b7..77157550e 100644 --- a/crates/edgezero-cli/README.md +++ b/crates/edgezero-cli/README.md @@ -46,6 +46,13 @@ Contributor build with the bundled example: cargo run -p edgezero-cli --features "cli,demo-example" -- demo ``` +The bundled demo uses the same Axum logger as the standalone app-demo adapter, not the CLI's +fixed-Info backend. It honors `EDGEZERO__LOGGING__LEVEL`; runtime `Off` suppresses ordinary +startup messages, while managed boot warnings remain at `Warn`. Capability checks run after +logger setup and before application configuration or listener binding. Terminal failures print +to stderr even with logging disabled. Generated adapter entrypoints likewise delegate to reusable +runners; no logging implementation is copied into their templates. + ## Built-in commands `edgezero` ships with these subcommands (see `edgezero --help` for full diff --git a/crates/edgezero-cli/src/adapter.rs b/crates/edgezero-cli/src/adapter.rs index 3b410bf86..894e7168b 100644 --- a/crates/edgezero-cli/src/adapter.rs +++ b/crates/edgezero-cli/src/adapter.rs @@ -1,7 +1,12 @@ +use edgezero_adapter::cli_support::validate_spin_outbound_host_contract; use edgezero_adapter::registry::{ self as adapter_registry, AdapterAction, AdapterDeployContext, DeployOwnership, }; -use edgezero_core::manifest::{Manifest, ManifestLoader, ResolvedEnvironment}; +use edgezero_core::manifest::{ + CapabilitySupport, Manifest, ManifestContract, ManifestLoader, ResolvedEnvironment, +}; + +use crate::manifest_source::{ResolvedAdapterTarget, ResolvedRuntime}; use std::env; use std::fmt; @@ -82,14 +87,7 @@ fn apply_environment( // `env::var_os` first and skip the explicit set when the parent // already has one. This mirrors the precedence the plan + the // typed-config env-overlay docs both promise. - for binding in &environment.variables { - if let Some(value) = &binding.value { - if env::var_os(&binding.env).is_some() { - continue; - } - command.env(&binding.env, value); - } - } + command.envs(resolve_environment_defaults(environment)); let mut missing = Vec::new(); for binding in &environment.secrets { @@ -162,7 +160,7 @@ fn require_adapter( } /// Select deployment ownership through the registered adapter's preflight. -/// Manifest-owned and fallback deployments retain the legacy finalizer, while +/// Manifest-owned and fallback deployments invoke the finalizer, while /// adapter-managed deployments own the complete lifecycle in `deploy`. pub fn deploy( adapter_name: &str, @@ -236,6 +234,234 @@ pub fn deploy( fallback_adapter.finalize_deploy(context, None) } +/// Check the selected runtime contract before deployment preflight or provider work. +pub(crate) fn deploy_runtime( + runtime: &ResolvedRuntime, + context: &AdapterDeployContext, + adapter_manifest_path_error: Option<&str>, + adapter_args: &[String], +) -> Result<(), String> { + ensure_action_capabilities(runtime)?; + deploy( + runtime.adapter_name(), + context, + adapter_manifest_path_error, + runtime.manifest_loader(), + adapter_args, + ) +} + +/// Dispatch a runtime-producing action against one pre-resolved target/contract pair. +pub(crate) fn execute_runtime( + runtime: &ResolvedRuntime, + adapter_args: &[String], +) -> Result<(), String> { + ensure_action_capabilities(runtime)?; + execute_after_gate(runtime, adapter_args) +} + +fn ensure_action_capabilities(runtime: &ResolvedRuntime) -> Result<(), String> { + if !produces_current_runtime(runtime.action()) { + return Err("operational action entered outbound runtime dispatcher".to_owned()); + } + ensure_spin_outbound_hosts(runtime)?; + ensure_capabilities( + runtime.adapter_name(), + ManifestContract::from_opt(runtime.manifest()), + ) +} + +fn ensure_spin_outbound_hosts(runtime: &ResolvedRuntime) -> Result<(), String> { + if !runtime.adapter_name().eq_ignore_ascii_case("spin") { + return Ok(()); + } + let Some(manifest) = runtime.manifest() else { + return Ok(()); + }; + let manifest_path = runtime + .manifest_path() + .ok_or_else(|| "resolved Spin runtime lost its application manifest path".to_owned())?; + let (platform_manifest, component) = match runtime.target() { + ResolvedAdapterTarget::Registered(target) => { + (target.platform_manifest(), target.component()) + } + ResolvedAdapterTarget::Shell(shell) => (shell.platform_manifest(), shell.component()), + }; + let platform_manifest_path = platform_manifest.ok_or_else(|| { + format!( + "Spin runtime in {} has no selected spin.toml; set [adapters.spin.adapter].manifest", + manifest_path.display() + ) + })?; + validate_spin_outbound_host_contract(manifest, manifest_path, platform_manifest_path, component) +} + +fn execute_after_gate(runtime: &ResolvedRuntime, adapter_args: &[String]) -> Result<(), String> { + match runtime.target() { + ResolvedAdapterTarget::Registered(_target) => { + execute_registered_after_gate(runtime, adapter_args) + } + ResolvedAdapterTarget::Shell(shell) => run_shell( + shell.command(), + shell.root(), + runtime.adapter_name(), + runtime.action(), + Some(shell.environment().clone()), + (shell.bind_host().map(str::to_owned), shell.bind_port()), + adapter_args, + ), + } +} + +fn execute_registered_after_gate( + runtime: &ResolvedRuntime, + adapter_args: &[String], +) -> Result<(), String> { + let ResolvedAdapterTarget::Registered(target) = runtime.target() else { + return Err("registered runtime dispatcher received a shell target".to_owned()); + }; + let adapter = adapter_registry::get_adapter(runtime.adapter_name()).ok_or_else(|| { + let available = adapter_registry::registered_adapters(); + if available.is_empty() { + format!( + "adapter `{}` is not registered (no adapters available)", + runtime.adapter_name() + ) + } else { + format!( + "adapter `{}` is not registered (available: {})", + runtime.adapter_name(), + available.join(", ") + ) + } + })?; + let defaults = runtime.manifest().map_or_else(Vec::new, |manifest| { + resolve_environment_defaults(&manifest.environment_for(runtime.adapter_name())) + }); + let selected = target.clone().with_environment_defaults(defaults); + adapter.execute_target( + AdapterAction::from(runtime.action()), + &selected, + adapter_args, + ) +} + +pub(crate) fn resolve_environment_defaults( + environment: &ResolvedEnvironment, +) -> Vec<(String, String)> { + let mut defaults = Vec::new(); + for binding in &environment.variables { + if let Some(value) = &binding.value + && env::var_os(&binding.env).is_none() + { + // Keep declaration order; Command owns platform-specific key identity. + defaults.push((binding.env.clone(), value.clone())); + } + } + defaults +} + +pub(crate) fn ensure_capabilities( + adapter_name: &str, + contract: ManifestContract<'_>, +) -> Result<(), String> { + let verified_manifest = match contract { + ManifestContract::Malformed(reason) => { + return Err(format!( + "capability check aborted: {reason}. This is an EdgeZero/app! contract bug; the baked manifest is unreadable, so required capabilities cannot be verified. Refusing to proceed rather than silently skipping enforcement." + )); + } + ManifestContract::None => return Ok(()), + ManifestContract::Present(present_manifest) => present_manifest, + _ => { + return Err( + "capability check aborted: unrecognized manifest-contract state. Refusing to proceed rather than skipping enforcement." + .to_owned(), + ); + } + }; + let capabilities = &verified_manifest.capabilities; + let Some(adapter) = adapter_registry::get_adapter(adapter_name) else { + if capabilities.required.is_empty() { + if capabilities.optional.is_empty() { + log::warn!( + target: edgezero_core::BOOT_LOG_TARGET, + "adapter '{adapter_name}' not in registry; capability check skipped (no capabilities declared)" + ); + } else { + log::warn!( + target: edgezero_core::BOOT_LOG_TARGET, + "adapter '{adapter_name}' not in registry; cannot verify its OPTIONAL capabilities; proceeding, since optional capabilities never hard-fail" + ); + } + return Ok(()); + } + return Err(format!( + "adapter '{adapter_name}' is not in the registry; cannot verify REQUIRED capabilities. Register an adapter stub that returns capability metadata, or move those entries to `optional`." + )); + }; + + let mut best_effort = Vec::new(); + let mut unsupported = Vec::new(); + for capability in capabilities.required.iter().copied() { + match adapter.capability(capability) { + CapabilitySupport::BestEffort => best_effort.push(capability.as_str()), + CapabilitySupport::BoundedCooperative => log::info!( + "adapter '{adapter_name}': required capability '{}' is bounded-cooperative; see capability docs for the bound", + capability.as_str() + ), + CapabilitySupport::Native => {} + CapabilitySupport::Unsupported | _ => unsupported.push(capability.as_str()), + } + } + if !unsupported.is_empty() { + return Err(format!( + "adapter '{adapter_name}' does not support required capabilities: {}", + unsupported.join(", ") + )); + } + if !best_effort.is_empty() { + return Err(format!( + "adapter '{adapter_name}': required capabilities are only best-effort: {}. See https://iabtechlab.github.io/edgezero/guide/capabilities and declare them `optional` only when the documented limitation is acceptable.", + best_effort.join(", ") + )); + } + + for capability in capabilities.optional.iter().copied() { + match adapter.capability(capability) { + CapabilitySupport::BestEffort => log::warn!( + target: edgezero_core::BOOT_LOG_TARGET, + "adapter '{adapter_name}': optional capability '{}' is best-effort; see https://iabtechlab.github.io/edgezero/guide/capabilities", + capability.as_str() + ), + CapabilitySupport::BoundedCooperative | CapabilitySupport::Native => {} + CapabilitySupport::Unsupported => log::warn!( + target: edgezero_core::BOOT_LOG_TARGET, + "adapter '{adapter_name}': optional capability '{}' unavailable", + capability.as_str() + ), + _ => log::warn!( + target: edgezero_core::BOOT_LOG_TARGET, + "adapter '{adapter_name}': optional capability '{}' reports an unrecognized support level; treating as degraded", + capability.as_str() + ), + } + } + Ok(()) +} + +fn produces_current_runtime(action: Action) -> bool { + match action { + Action::Build | Action::Deploy | Action::DeployStaging | Action::Serve => true, + Action::AuthLogin + | Action::AuthLogout + | Action::AuthStatus + | Action::EmitVersion + | Action::Healthcheck + | Action::Rollback => false, + } +} + fn manifest_command<'manifest>( manifest: &'manifest Manifest, adapter_name: &str, @@ -477,12 +703,178 @@ fn shell_join(args: &[String]) -> String { #[cfg(test)] mod tests { - use super::{AdapterDeployContext, ResolvedEnvironment, apply_environment, deploy}; + use super::{ + AdapterDeployContext, ResolvedEnvironment, apply_environment, deploy, ensure_capabilities, + execute_runtime, + }; + use crate::adapter::Action; + use crate::manifest_source::resolve_runtime_from; use crate::test_support::manifest_guard; - use edgezero_core::manifest::ResolvedEnvironmentBinding; + use edgezero_adapter::registry::{ + Adapter, AdapterAction, AdapterExecutionTarget, register_adapter, + }; + use edgezero_core::manifest::{ + Capability, CapabilitySupport, ManifestContract, ManifestLoader, ResolvedEnvironmentBinding, + }; use edgezero_core::test_env::EnvOverride; + use std::env; + use std::ffi::{OsStr, OsString}; + use std::fs; use std::path::PathBuf; use std::process::Command; + use std::sync::Mutex; + use tempfile::tempdir; + + static BEST_EFFORT_ADAPTER: GateAdapter = GateAdapter { + name: "gate-best-effort", + support: CapabilitySupport::BestEffort, + }; + static ENVIRONMENT_ADAPTER: EnvironmentAdapter = EnvironmentAdapter; + static ENVIRONMENT_TARGETS: Mutex> = Mutex::new(Vec::new()); + static BOUNDED_ADAPTER: GateAdapter = GateAdapter { + name: "gate-bounded", + support: CapabilitySupport::BoundedCooperative, + }; + static NATIVE_ADAPTER: GateAdapter = GateAdapter { + name: "gate-native", + support: CapabilitySupport::Native, + }; + static UNSUPPORTED_ADAPTER: GateAdapter = GateAdapter { + name: "gate-unsupported", + support: CapabilitySupport::Unsupported, + }; + + struct GateAdapter { + name: &'static str, + support: CapabilitySupport, + } + + struct EnvironmentAdapter; + + #[expect( + clippy::missing_trait_methods, + reason = "records pinned target dispatch without launching a child" + )] + impl Adapter for EnvironmentAdapter { + fn execute(&self, _action: AdapterAction, _args: &[String]) -> Result<(), String> { + Err("pinned dispatch must not rediscover the invocation project".to_owned()) + } + + fn execute_target( + &self, + _action: AdapterAction, + target: &AdapterExecutionTarget, + _args: &[String], + ) -> Result<(), String> { + ENVIRONMENT_TARGETS + .lock() + .map_err(|_poison| "fixture target lock poisoned")? + .push(target.clone()); + Ok(()) + } + + fn name(&self) -> &'static str { + "gate-environment" + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "capability-gate fixture overrides only the relevant trait methods" + )] + impl Adapter for GateAdapter { + fn capability(&self, _capability: Capability) -> CapabilitySupport { + self.support + } + + fn execute(&self, _action: AdapterAction, _args: &[String]) -> Result<(), String> { + Ok(()) + } + + fn name(&self) -> &'static str { + self.name + } + } + + fn capability_manifest(section: &str) -> ManifestLoader { + ManifestLoader::load_from_str(&format!("[capabilities]\n{section}\n[adapters.gate]\n")) + } + + #[test] + fn registered_dispatch_carries_only_selected_manifest_defaults_in_order() { + const KEY: &str = "EDGEZERO_TEST_REGISTERED_INGRESS"; + let _lock = manifest_guard().lock().unwrap(); + let _unset = EnvOverride::remove(KEY); + register_adapter(&ENVIRONMENT_ADAPTER); + let invocation = tempdir().unwrap(); + let selected = tempdir().unwrap(); + fs::write(invocation.path().join("edgezero.toml"), + format!("[adapters.gate-environment]\n[[environment.variables]]\nname = '{KEY}'\nvalue = 'wrong-project'\n")).unwrap(); + let manifest_path = selected.path().join("edgezero.toml"); + fs::write(&manifest_path, format!( + "[adapters.gate-environment]\n\ + [[environment.variables]]\nname = 'first'\nenv = '{KEY}'\nvalue = 'global'\n\ + [[environment.variables]]\nname = 'second'\nenv = '{KEY}'\nvalue = 'selected'\nadapters = ['gate-environment']\n\ + [[environment.variables]]\nname = 'excluded'\nenv = '{KEY}'\nvalue = 'wrong-adapter'\nadapters = ['spin']\n\ + [[environment.variables]]\nname = 'last'\nenv = '{KEY}'\nvalue = ''\n" + )).unwrap(); + for action in [Action::Build, Action::Serve] { + let runtime = resolve_runtime_from( + "gate-environment", + action, + invocation.path(), + Some(OsString::from(&manifest_path)), + ) + .unwrap(); + execute_runtime(&runtime, &[]).unwrap(); + } + let mut targets = ENVIRONMENT_TARGETS.lock().unwrap(); + assert_eq!(targets.len(), 2); + for target in targets.drain(..) { + assert_eq!(target.app_root(), selected.path().canonicalize().unwrap()); + assert_eq!( + target.environment_defaults(), + &[ + (KEY.to_owned(), "global".to_owned()), + (KEY.to_owned(), "selected".to_owned()), + (KEY.to_owned(), String::new()) + ] + ); + let mut child = Command::new("echo"); + child.envs(target.environment_defaults().iter().cloned()); + assert!( + child + .get_envs() + .any(|(key, value)| key == KEY && value.is_some_and(OsStr::is_empty)) + ); + } + assert!(env::var_os(KEY).is_none()); + } + + #[test] + fn environment_defaults_filter_before_collapsing_and_preserve_parent_values() { + const KEY: &str = "EDGEZERO_TEST_INGRESS_PROPAGATION"; + let _lock = manifest_guard().lock().unwrap(); + let _unset = EnvOverride::remove(KEY); + let loader = ManifestLoader::load_from_str(&format!( + "[[environment.variables]]\nname = 'first'\nenv = '{KEY}'\nvalue = 'global'\n\ + [[environment.variables]]\nname = 'selected'\nenv = '{KEY}'\nvalue = 'selected'\nadapters = ['AxUm']\n\ + [[environment.variables]]\nname = 'excluded'\nenv = '{KEY}'\nvalue = 'wrong'\nadapters = ['spin']\n\ + [[environment.variables]]\nname = 'valueless'\nenv = '{KEY}'\n" + )); + let resolved = loader.manifest().environment_for("axum"); + let defaults = super::resolve_environment_defaults(&resolved); + let mut child = Command::new("echo"); + child.envs(defaults); + assert!( + child.get_envs().any(|(key, value)| key == KEY + && value.is_some_and(|configured| configured == "selected")) + ); + for value in ["operator", ""] { + let _parent = EnvOverride::set(KEY, value); + assert!(super::resolve_environment_defaults(&resolved).is_empty()); + } + } #[test] fn deploy_rejects_application_release_for_non_managed_adapter() { @@ -605,6 +997,183 @@ mod tests { ); } + #[test] + fn provider_failure_diagnostics_capability_gate() { + for adapter in [ + &NATIVE_ADAPTER, + &BOUNDED_ADAPTER, + &BEST_EFFORT_ADAPTER, + &UNSUPPORTED_ADAPTER, + ] { + register_adapter(adapter); + for disposition in ["required", "optional"] { + let loader = capability_manifest(&format!( + "{disposition} = [\"outbound-failure-diagnostics\"]" + )); + let result = ensure_capabilities( + adapter.name, + ManifestContract::from_opt(Some(loader.manifest())), + ); + let accepted = disposition == "optional" + || matches!( + adapter.support, + CapabilitySupport::Native | CapabilitySupport::BoundedCooperative + ); + assert_eq!(result.is_ok(), accepted, "{}: {result:?}", adapter.name); + if let Err(error) = result { + assert!(error.contains("outbound-failure-diagnostics"), "{error}"); + } + } + } + } + + #[test] + fn capability_gate_accepts_native_and_bounded_required_support() { + register_adapter(&NATIVE_ADAPTER); + register_adapter(&BOUNDED_ADAPTER); + let loader = capability_manifest("required = [\"outbound-http\"]"); + for name in [NATIVE_ADAPTER.name, BOUNDED_ADAPTER.name] { + assert_eq!( + ensure_capabilities(name, ManifestContract::from_opt(Some(loader.manifest()))), + Ok(()) + ); + } + } + + #[test] + fn capability_gate_fails_closed_for_malformed_contract() { + let result = ensure_capabilities( + "gate-missing-malformed", + ManifestContract::Malformed("fixture corruption"), + ); + assert!(result.is_err_and(|error| error.contains("fixture corruption"))); + } + + #[test] + fn capability_gate_rejects_best_effort_and_unsupported_required_support() { + register_adapter(&BEST_EFFORT_ADAPTER); + register_adapter(&UNSUPPORTED_ADAPTER); + let loader = capability_manifest("required = [\"outbound-http\"]"); + for name in [BEST_EFFORT_ADAPTER.name, UNSUPPORTED_ADAPTER.name] { + assert!( + ensure_capabilities(name, ManifestContract::from_opt(Some(loader.manifest()))) + .is_err() + ); + } + } + + #[test] + fn capability_gate_links_canonical_documentation() { + register_adapter(&BEST_EFFORT_ADAPTER); + let loader = capability_manifest("required = [\"outbound-http\"]"); + let error = ensure_capabilities( + BEST_EFFORT_ADAPTER.name, + ManifestContract::from_opt(Some(loader.manifest())), + ) + .expect_err("required best-effort capability must fail"); + assert!( + error.contains("https://iabtechlab.github.io/edgezero/guide/capabilities"), + "capability diagnostics must link to the canonical guide" + ); + } + + #[test] + fn capability_gate_treats_missing_registry_by_requirement_level() { + let required = capability_manifest("required = [\"outbound-http\"]"); + let optional = capability_manifest("optional = [\"outbound-http\"]"); + assert!( + ensure_capabilities( + "gate-missing-required", + ManifestContract::from_opt(Some(required.manifest())) + ) + .is_err() + ); + assert_eq!( + ensure_capabilities( + "gate-missing-optional", + ManifestContract::from_opt(Some(optional.manifest())) + ), + Ok(()) + ); + assert_eq!( + ensure_capabilities("gate-missing-none", ManifestContract::None), + Ok(()) + ); + } + + #[test] + fn optional_capability_degradation_never_hard_fails() { + register_adapter(&BEST_EFFORT_ADAPTER); + register_adapter(&UNSUPPORTED_ADAPTER); + let loader = capability_manifest("optional = [\"outbound-http\"]"); + for name in [BEST_EFFORT_ADAPTER.name, UNSUPPORTED_ADAPTER.name] { + assert_eq!( + ensure_capabilities(name, ManifestContract::from_opt(Some(loader.manifest()))), + Ok(()) + ); + } + } + + #[test] + fn spin_host_drift_blocks_runtime_actions_before_dispatch() { + let dir = tempdir().expect("temp dir"); + let marker = dir.path().join("runtime-started"); + let manifest_path = dir.path().join("edgezero.toml"); + fs::write( + dir.path().join("spin.toml"), + "[component.app]\nallowed_outbound_hosts = [\"https://actual.example\"]\n", + ) + .expect("write Spin manifest"); + fs::write( + &manifest_path, + format!( + "[capabilities.outbound]\nhosts = [\"https://expected.example\"]\n\ + [adapters.spin.adapter]\nmanifest = \"spin.toml\"\ncomponent = \"app\"\n\ + [adapters.spin.commands]\nbuild = \"touch {}\"\nserve = \"touch {}\"\ndeploy = \"touch {}\"\n", + marker.display(), + marker.display(), + marker.display() + ), + ) + .expect("write application manifest"); + + for action in [ + Action::Build, + Action::Serve, + Action::Deploy, + Action::DeployStaging, + ] { + let runtime = resolve_runtime_from( + "spin", + action, + dir.path(), + Some(OsString::from(&manifest_path)), + ) + .expect("resolve runtime"); + let error = execute_runtime(&runtime, &[]).expect_err("host drift must fail closed"); + assert!(error.contains("Spin outbound host drift"), "{error}"); + assert!(!marker.exists(), "runtime side effect for {action}"); + } + + fs::write( + &manifest_path, + "[capabilities.outbound]\nhosts = [\"https://expected.example\"]\n\ + [adapters.spin.adapter]\nmanifest = \"spin.toml\"\ncomponent = \"app\"\n", + ) + .expect("replace application manifest with registered target"); + let registered = resolve_runtime_from( + "spin", + Action::Build, + dir.path(), + Some(OsString::from(&manifest_path)), + ) + .expect("resolve registered runtime"); + let error = execute_runtime(®istered, &[]) + .expect_err("registered target host drift must fail before adapter dispatch"); + assert!(error.contains("Spin outbound host drift"), "{error}"); + assert!(!marker.exists(), "registered adapter must not run on drift"); + } + #[test] fn shell_escape_quotes_and_spaces() { assert_eq!(super::shell_escape("plain"), "plain"); diff --git a/crates/edgezero-cli/src/config.rs b/crates/edgezero-cli/src/config.rs index 438db18bb..498ca08b9 100644 --- a/crates/edgezero-cli/src/config.rs +++ b/crates/edgezero-cli/src/config.rs @@ -720,16 +720,12 @@ where } }, ReadConfigEntry::MissingKey => { - let leaf_count = collect_changes( - &serde_json::Value::Object(serde_json::Map::default()), - &local_envelope.data, - ) - .len(); + let leaf_count = collect_changes(&serde_json::json!({}), &local_envelope.data).len(); diff_info(&format!( "# no remote at key `{key}`; all {leaf_count} leaves added" )); dispatch_diff_format( - &serde_json::Value::Object(serde_json::Map::default()), + &serde_json::json!({}), &local_envelope.data, "(none)", &local_sha, @@ -738,18 +734,14 @@ where DiffOutcome::RemoteAbsent } ReadConfigEntry::MissingStore => { - let leaf_count = collect_changes( - &serde_json::Value::Object(serde_json::Map::default()), - &local_envelope.data, - ) - .len(); + let leaf_count = collect_changes(&serde_json::json!({}), &local_envelope.data).len(); diff_info(&format!( "# store has no matching backend yet \u{2014} run `edgezero provision \ --adapter {}` first if this is the live remote; all {leaf_count} leaves added", args.adapter )); dispatch_diff_format( - &serde_json::Value::Object(serde_json::Map::default()), + &serde_json::json!({}), &local_envelope.data, "(none)", &local_sha, @@ -1074,7 +1066,7 @@ fn render_first_read_diff( ReadConfigEntry::MissingKey if !no_diff => { push_info(&format!("# no remote at key `{key}`; all leaves added")); print_unified_diff_inline( - &serde_json::Value::Object(serde_json::Map::default()), + &serde_json::json!({}), &local_envelope.data, "(none)", local_sha, @@ -1088,7 +1080,7 @@ fn render_first_read_diff( ); push_info("# no remote store; all leaves added"); print_unified_diff_inline( - &serde_json::Value::Object(serde_json::Map::default()), + &serde_json::json!({}), &local_envelope.data, "(none)", local_sha, @@ -2307,7 +2299,11 @@ ids = ["app_config"] ]) .to_string(); let oplog = dir.path().join("fastly-ops.log"); - let fake = fake_fastly_gc_list(r#"[{"name":"app_config","id":"store-1"}]"#, &list, &oplog); + let fake = fake_fastly_gc_list( + &serde_json::json!([{"name": "app_config", "id": "store-1"}]).to_string(), + &list, + &oplog, + ); let _prepend = PathPrepend::new(fake.path()); let args = ConfigGcArgs { @@ -2342,13 +2338,17 @@ ids = ["app_config"] let list = json!([ { "item_key": "app_config", - "item_value": r#"{"edgezero_kind":"fastly_config_chunks_v2","data":{}}"#, + "item_value": &serde_json::json!({"edgezero_kind": "fastly_config_chunks_v2", "data": {}}).to_string(), "created_at": "2026-07-01T00:00:00Z" }, ]) .to_string(); let oplog = dir.path().join("fastly-ops.log"); - let fake = fake_fastly_gc_list(r#"[{"name":"app_config","id":"store-1"}]"#, &list, &oplog); + let fake = fake_fastly_gc_list( + &serde_json::json!([{"name": "app_config", "id": "store-1"}]).to_string(), + &list, + &oplog, + ); let _prepend = PathPrepend::new(fake.path()); let args = ConfigGcArgs { @@ -2402,7 +2402,7 @@ ids = ["app_config"] .to_string(); // The store list is keyed ONLY on the ENV name -> a specific id. If the // overlay were ignored (logical "app_config" looked up), resolution fails. - let stores = r#"[{"name":"shared_config","id":"store-42"}]"#; + let stores = &serde_json::json!([{"name": "shared_config", "id": "store-42"}]).to_string(); let oplog = dir.path().join("fastly-ops.log"); let fake = fake_fastly_gc_list(stores, &entries, &oplog); let _prepend = PathPrepend::new(fake.path()); @@ -3920,12 +3920,17 @@ ids = ["default"] // Every kind of unreadable prior STATE must let a local dry-run reach the // writer's degrading count rather than hitting the Spin-Cloud rejection: // a corrupt-but-parsing pointer, malformed TOML, and a non-string root. + let sha = "a".repeat(64); let corrupt_pointer = format!( "app_config = {}", - toml_string_literal(&format!( - "{{\"edgezero_kind\":\"fastly_config_chunks\",\"version\":1,\"chunks\":[{{\"key\":\"app_config.__edgezero_chunks.{sha}.0\",\"len\":10,\"sha256\":\"x\"}}],\"data_sha256\":\"\",\"envelope_len\":10,\"envelope_sha256\":\"{sha}\"}}", - sha = "a".repeat(64), - )), + toml_string_literal(&serde_json::json!({ + "edgezero_kind": "fastly_config_chunks", + "version": 1_u32, + "chunks": [{"key": format!("app_config.__edgezero_chunks.{sha}.0"), "len": 10_u32, "sha256": "x"}], + "data_sha256": "", + "envelope_len": 10_u32, + "envelope_sha256": sha, + }).to_string()), ); let cases = [ ( diff --git a/crates/edgezero-cli/src/demo_server.rs b/crates/edgezero-cli/src/demo_server.rs index 3efe5a4cd..54d9e5123 100644 --- a/crates/edgezero-cli/src/demo_server.rs +++ b/crates/edgezero-cli/src/demo_server.rs @@ -4,7 +4,7 @@ //! //! `demo` runs the bundled `app-demo` example locally — the **same way** //! `app-demo`'s own axum adapter runs it: via -//! [`edgezero_adapter_axum::dev_server::run_app`], which reads the store +//! [`edgezero_adapter_axum::dev_server::run_app_with_preflight`], which reads the store //! config baked into `App` plus `EDGEZERO__*` environment variables and //! wires the full setup (routing, KV / config / secret stores, logging, //! host/port). @@ -15,15 +15,20 @@ /// Run the bundled `app-demo` example on the local axum server. /// -/// Delegates to `run_app`, so `edgezero demo` behaves identically to -/// `cargo run -p app-demo-adapter-axum`. +/// Delegates logger and server setup to Axum. Capability validation runs after +/// logging is ready and before application configuration or listener binding. /// /// # Errors /// /// Returns an error if the demo server fails to start. pub fn run_demo() -> Result<(), String> { use app_demo_core::App; - use edgezero_adapter_axum::dev_server::run_app; + use edgezero_adapter_axum::dev_server::run_app_with_preflight; + use std::io; - run_app::().map_err(|err| format!("demo server error: {err}")) + run_app_with_preflight::(|| { + crate::demo_capability_gate::().map_err(io::Error::other)?; + Ok(()) + }) + .map_err(|err| format!("demo server error: {err}")) } diff --git a/crates/edgezero-cli/src/generator.rs b/crates/edgezero-cli/src/generator.rs index b34227d3e..f930830c2 100644 --- a/crates/edgezero-cli/src/generator.rs +++ b/crates/edgezero-cli/src/generator.rs @@ -6,7 +6,7 @@ use crate::scaffold::{ use edgezero_adapter::scaffold; use edgezero_adapter::scaffold::AdapterBlueprint; use handlebars::Handlebars; -use serde_json::{Map, Value}; +use serde_json::{Value, json}; use std::collections::BTreeMap; use std::env; use std::fmt::{self, Write as _}; @@ -217,7 +217,7 @@ pub fn generate_new(args: &NewArgs) -> Result<(), GeneratorError> { let adapter_artifacts = collect_adapter_data(&layout, &repo_root, &mut workspace_dependencies)?; - let mut data_map = build_base_data( + let mut data_value = build_base_data( &layout, &core_crate_line, &cli_crate_line, @@ -225,14 +225,14 @@ pub fn generate_new(args: &NewArgs) -> Result<(), GeneratorError> { &workspace_dependencies, ); - for context in &adapter_artifacts.contexts { - for (key, value) in &context.data_entries { - data_map.insert(key.clone(), Value::String(value.clone())); + if let Some(data_map) = data_value.as_object_mut() { + for context in &adapter_artifacts.contexts { + for (key, value) in &context.data_entries { + data_map.insert(key.clone(), json!(value)); + } } } - let data_value = Value::Object(data_map); - render_templates(&layout, &adapter_artifacts.contexts, &data_value)?; initialize_git_repo(&layout.out_dir); @@ -246,7 +246,7 @@ pub fn generate_new(args: &NewArgs) -> Result<(), GeneratorError> { fn seed_workspace_dependencies() -> BTreeMap { let mut deps = BTreeMap::new(); - deps.insert("bytes".to_owned(), "bytes = \"1\"".to_owned()); + deps.insert("bytes".to_owned(), "bytes = \"=1.12.1\"".to_owned()); deps.insert("anyhow".to_owned(), "anyhow = \"1\"".to_owned()); deps.insert( "futures".to_owned(), @@ -269,10 +269,10 @@ fn seed_workspace_dependencies() -> BTreeMap { ); deps.insert( "worker".to_owned(), - "worker = { version = \"0.8\", default-features = false, features = [\"http\"] }" + "worker = { version = \"=0.8.5\", default-features = false, features = [\"http\"] }" .to_owned(), ); - deps.insert("fastly".to_owned(), "fastly = \"0.13\"".to_owned()); + deps.insert("fastly".to_owned(), "fastly = \"=0.13.1\"".to_owned()); deps.insert("once_cell".to_owned(), "once_cell = \"1\"".to_owned()); deps.insert( "tokio".to_owned(), @@ -281,7 +281,12 @@ fn seed_workspace_dependencies() -> BTreeMap { deps.insert("tracing".to_owned(), "tracing = \"0.1\"".to_owned()); deps.insert( "spin-sdk".to_owned(), - "spin-sdk = { version = \"7\", default-features = false }".to_owned(), + concat!( + "spin-sdk = { version = \"=7.0.0\", ", + "git = \"https://github.com/stackpop/spin-rust-sdk.git\", ", + "rev = \"e65ff90dadf4173b08b14194e878ad3acd555337\", default-features = false }", + ) + .to_owned(), ); // Core depends on `validator` for `#[derive(Validate)]` on the // generated `Config` struct. Pinned to the same @@ -564,70 +569,37 @@ fn build_base_data( cli_crate_line: &str, artifacts: &AdapterArtifacts, workspace_dependencies: &BTreeMap, -) -> Map { - let mut data = Map::new(); - data.insert("name".into(), Value::String(layout.name.clone())); - data.insert("proj_core".into(), Value::String(layout.core_name.clone())); - data.insert("proj_cli".into(), Value::String(layout.cli_name.clone())); - data.insert( - "proj_core_mod".into(), - Value::String(layout.core_mod.clone()), - ); - data.insert("proj_mod".into(), Value::String(layout.project_mod.clone())); - data.insert( - "NameUpperCamel".into(), - Value::String(layout.upper_camel.clone()), - ); - data.insert("EnvPrefix".into(), Value::String(layout.env_prefix.clone())); - data.insert( - "dep_edgezero_core".into(), - Value::String(core_crate_line.to_owned()), - ); - data.insert( - "dep_edgezero_cli".into(), - Value::String(cli_crate_line.to_owned()), - ); - +) -> Value { let adapter_list_str = artifacts .adapter_ids .iter() .map(|id| format!("\"{id}\"")) .collect::>() .join(", "); - data.insert("adapter_list".into(), Value::String(adapter_list_str)); - data.insert( - "workspace_members".into(), - Value::String(artifacts.workspace_members.join("\n")), - ); - data.insert( - "adapter_manifest_sections".into(), - Value::String(artifacts.manifest_sections.clone()), - ); - data.insert( - "readme_adapter_crates".into(), - Value::String(artifacts.readme_adapter_crates.clone()), - ); - data.insert( - "readme_adapter_dev".into(), - Value::String(artifacts.readme_adapter_dev.clone()), - ); - let workspace_dep_lines = workspace_dependencies .values() .cloned() .collect::>() .join("\n"); - data.insert( - "workspace_dependencies".into(), - Value::String(workspace_dep_lines), - ); - - data.insert( - "tool_versions_contents".into(), - Value::String(build_tool_versions(&artifacts.adapter_ids)), - ); - - data + json!({ + "name": layout.name, + "proj_core": layout.core_name, + "proj_cli": layout.cli_name, + "proj_core_mod": layout.core_mod, + "proj_mod": layout.project_mod, + "NameUpperCamel": layout.upper_camel, + "EnvPrefix": layout.env_prefix, + "dep_edgezero_core": core_crate_line, + "dep_edgezero_cli": cli_crate_line, + "adapter_list": adapter_list_str, + "native_image_example": artifacts.adapter_ids.iter().any(|id| id == "fastly"), + "workspace_members": artifacts.workspace_members.join("\n"), + "adapter_manifest_sections": artifacts.manifest_sections, + "readme_adapter_crates": artifacts.readme_adapter_crates, + "readme_adapter_dev": artifacts.readme_adapter_dev, + "workspace_dependencies": workspace_dep_lines, + "tool_versions_contents": build_tool_versions(&artifacts.adapter_ids), + }) } /// Render the `.tool-versions` body for a scaffolded project, @@ -642,10 +614,7 @@ fn build_base_data( /// - `fastly` → `fastly` (the Fastly CLI we shell out to for /// provision + config push) plus `viceroy` (what /// `fastly compute serve` uses for local emulation). -/// - `spin` → no asdf pin; the Spin CLI is install-flow-managed -/// (). A header comment points -/// the operator at the URL when `spin` is in the adapter set so -/// they don't wonder why everything else is pinned but spin. +/// - `spin` → `spin` (the runtime used by `spin build` and `spin up`). /// - `axum` → no extra pin (uses the host Rust toolchain only). /// /// Versions are pulled from this repo's own `.tool-versions` (see @@ -660,24 +629,16 @@ fn build_tool_versions(adapter_ids: &[String]) -> String { lines.push("fastly 16.0.0".to_owned()); lines.push("viceroy 0.21.0".to_owned()); } + if has("spin") { + lines.push("spin 4.1.0".to_owned()); + } lines.push("rust 1.98.1".to_owned()); // Sort + dedup so the file is stable regardless of adapter // declaration order (and asdf doesn't care). lines.sort(); lines.dedup(); let mut body = lines.join("\n"); - if has("spin") { - body.push_str(concat!( - "\n\n# Spin is not asdf-managed in this scaffold; install via\n", - "# https://spinframework.dev/install\n", - "# Requires Spin 4.1 or newer: spin-sdk 7 imports\n", - "# wasi:http/types@0.3.0, which Spin 4.0.x does not provide.\n", - "# On an older runtime the component builds and its tests\n", - "# pass, then the wasm linker rejects it at startup.\n", - )); - } else { - body.push('\n'); - } + body.push('\n'); body } @@ -811,8 +772,11 @@ fn initialize_git_repo(out_dir: &Path) { mod tests { use super::*; use crate::test_support::path_mutation_guard; + use edgezero_adapter::cli_support::validate_spin_outbound_host_contract; + use edgezero_core::Capability; use edgezero_core::app_config::app_name_prefix; - use edgezero_core::test_env::PathPrepend as PathOverride; + use edgezero_core::manifest::ManifestLoader; + use edgezero_core::test_env::{PathPrepend as PathOverride, env_lock}; use std::path::Path; use tempfile::TempDir; @@ -874,73 +838,216 @@ mod tests { // ---------- build_tool_versions ---------- + fn dependency_requirement<'manifest>( + dependencies: &'manifest toml::Value, + name: &str, + ) -> Option<&'manifest str> { + let dependency = dependencies.get(name)?; + dependency + .as_str() + .or_else(|| dependency.get("version")?.as_str()) + } + + fn dependency_git_source<'manifest>( + dependencies: &'manifest toml::Value, + name: &str, + ) -> ( + Option<&'manifest toml::Value>, + Option<&'manifest toml::Value>, + ) { + let dependency = dependencies.get(name); + ( + dependency.and_then(|value| value.get("git")), + dependency.and_then(|value| value.get("rev")), + ) + } + + fn workspace_dependencies(manifest: &str) -> toml::Value { + let parsed: toml::Value = toml::from_str(manifest).expect("valid Cargo manifest"); + parsed["workspace"]["dependencies"].clone() + } + #[test] fn seeded_dependencies_track_the_edgezero_workspace() { - // The seeds above are hardcoded, but a generated crate takes - // `edgezero-core` as a path/version dependency and re-derives - // `#[derive(Validate)]` against whatever `validator` the workspace - // resolves. If the two majors drift apart the generated app fails to - // compile with a confusing "trait bound not satisfied" error, so the - // shared crates are checked against the root manifest here. - let root_manifest = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../Cargo.toml")); - let seeds = seed_workspace_dependencies(); - for crate_name in ["validator", "axum", "fastly", "worker", "simple_logger"] { - let want = root_manifest - .lines() - .find(|line| line.starts_with(&format!("{crate_name} = "))) - .unwrap_or_else(|| panic!("root Cargo.toml must declare {crate_name}")); - let want_version = extract_version(want) - .unwrap_or_else(|| panic!("no version in root {crate_name} line: {want}")); - let got = seeds - .get(crate_name) - .unwrap_or_else(|| panic!("scaffold must seed {crate_name}")); - let got_version = extract_version(got) - .unwrap_or_else(|| panic!("no version in seeded {crate_name} line: {got}")); + let root = workspace_dependencies(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../Cargo.toml" + ))); + for name in [ + "bytes", + "validator", + "axum", + "fastly", + "worker", + "simple_logger", + "spin-sdk", + ] { + let seeds = seed_workspace_dependencies(); + let seed: toml::Value = toml::from_str(&seeds[name]).expect("valid dependency seed"); + let want = dependency_requirement(&root, name).expect("root dependency version"); assert_eq!( - got_version, want_version, - "scaffold seeds {crate_name} {got_version}, workspace uses {want_version}" + dependency_requirement(&seed, name), + Some(want), + "{name} seed drifted" ); + if name == "spin-sdk" { + assert_eq!( + dependency_git_source(&seed, name), + dependency_git_source(&root, name), + "{name} seed source drifted" + ); + } } + } - // `spin-sdk` is spelled `~7.0` in the workspace and `7` in the seed, - // so only the major is comparable. It is worth comparing anyway: a - // major mismatch here is the one that survives every compile-time - // check and dies at `spin up` with a WIT linker error. - let want_spin = root_manifest - .lines() - .find(|line| line.starts_with("spin-sdk = ")) - .and_then(extract_version) - .map(major_of) - .expect("root Cargo.toml must declare spin-sdk"); - let got_spin = seeds - .get("spin-sdk") - .and_then(|line| extract_version(line)) - .map(major_of) - .expect("scaffold must seed spin-sdk"); + #[test] + fn independent_dependencies_track_the_edgezero_workspace() { + let root = workspace_dependencies(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../Cargo.toml" + ))); + for (manifest, needs_validator) in [ + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../examples/app-demo/Cargo.toml" + )), + true, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/fixtures/reusable-app/Cargo.toml" + )), + false, + ), + ] { + let dependencies = workspace_dependencies(manifest); + for name in ["validator", "fastly", "worker", "spin-sdk"] { + if name == "validator" && !needs_validator { + continue; // The reusable fixture does not derive app configuration. + } + assert_eq!( + dependency_requirement(&dependencies, name), + dependency_requirement(&root, name), + "{name} differs in an independent workspace" + ); + if name == "spin-sdk" { + assert_eq!( + dependency_git_source(&dependencies, name), + dependency_git_source(&root, name), + "{name} source differs in an independent workspace" + ); + } + } + } + let smoke = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../.github/actions/deploy-fastly/tests/make-smoke-fixture.sh" + )); + let marker = "cat >crates/fixture-app-cli/Cargo.toml <<'TOML'\n"; + let manifest = smoke + .split_once(marker) + .expect("fixture manifest heredoc") + .1 + .split_once("\nTOML") + .expect("fixture manifest end") + .0; + let parsed: toml::Value = toml::from_str(manifest).expect("valid fixture Cargo manifest"); assert_eq!( - got_spin, want_spin, - "scaffold seeds spin-sdk major {got_spin}, workspace uses {want_spin}" + dependency_requirement(&parsed["dependencies"], "validator"), + dependency_requirement(&root, "validator"), + "deploy fixture validator differs" + ); + let spin: toml::Value = toml::from_str(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/fixtures/reusable-app/crates/fixture-spin/Cargo.toml" + ))) + .expect("valid fixture member manifest"); + assert_eq!( + spin["dependencies"]["spin-sdk"]["workspace"].as_bool(), + Some(true) ); } - /// Leading major from a requirement string, ignoring any range - /// operator — `~7.0`, `^7.1`, `7` and `=7.0.1` all yield `7`. - fn major_of(version: &str) -> &str { - version - .trim_start_matches(['~', '^', '=', '>', '<', ' ']) - .split('.') - .next() - .unwrap_or(version) + #[test] + fn dependency_requirement_preserves_exact_pins_and_table_order() { + let parsed: toml::Value = toml::from_str( + r#" +# validator = "wrong" +validator = { features = ["derive"], version = "0.21" } # 99.0 +spin-sdk = "=7.0.0" +unpinned = { path = "somewhere" } +"#, + ) + .expect("valid dependency fixture"); + assert_eq!(dependency_requirement(&parsed, "validator"), Some("0.21")); + assert_eq!(dependency_requirement(&parsed, "spin-sdk"), Some("=7.0.0")); + assert_ne!(dependency_requirement(&parsed, "spin-sdk"), Some("7")); + assert_eq!(dependency_requirement(&parsed, "unpinned"), None); + assert_eq!(dependency_requirement(&parsed, "absent"), None); } - /// Pull the `version = "X"` (or bare `crate = "X"`) value out of a - /// dependency line, for comparing a seeded pin against the workspace. - fn extract_version(line: &str) -> Option<&str> { - let after = line.split_once("version = \"").map_or_else( - || line.split_once("= \"").map(|(_, rest)| rest), - |(_, rest)| Some(rest), - )?; - after.split_once('"').map(|(version, _)| version) + #[test] + fn dependency_git_source_detects_drift_without_changing_version_checks() { + let pinned: toml::Value = toml::from_str( + r#" +spin-sdk = { version = "=7.0.0", git = "https://github.com/stackpop/spin-rust-sdk", rev = "0123456789abcdef0123456789abcdef01234567" } +"#, + ) + .expect("valid pinned dependency"); + let source = dependency_git_source(&pinned, "spin-sdk"); + assert_eq!( + source, + (pinned["spin-sdk"].get("git"), pinned["spin-sdk"].get("rev")) + ); + let reordered: toml::Value = toml::from_str( + r#" +[spin-sdk] +rev = "0123456789abcdef0123456789abcdef01234567" +version = "=7.0.0" +git = "https://github.com/stackpop/spin-rust-sdk" +"#, + ) + .expect("valid reordered dependency"); + assert_eq!(dependency_git_source(&reordered, "spin-sdk"), source); + for (field, replacement) in [ + ( + "git", + Some("https://github.com/spinframework/spin-rust-sdk"), + ), + ("rev", Some("89abcdef0123456789abcdef0123456789abcdef01")), + ("git", None), + ("rev", None), + ] { + let mut drifted = pinned.clone(); + let dependency = drifted["spin-sdk"] + .as_table_mut() + .expect("dependency table"); + if let Some(value) = replacement { + dependency.insert(field.to_owned(), toml::Value::String(value.to_owned())); + } else { + dependency.remove(field); + } + assert_eq!( + dependency_requirement(&drifted, "spin-sdk"), + dependency_requirement(&pinned, "spin-sdk"), + "source drift must not change the version fixture" + ); + assert_ne!( + dependency_git_source(&drifted, "spin-sdk"), + source, + "{field}" + ); + } + let registry: toml::Value = toml::from_str("spin-sdk = \"=7.0.0\"").unwrap(); + assert_eq!( + dependency_requirement(®istry, "spin-sdk"), + dependency_requirement(&pinned, "spin-sdk") + ); + assert_eq!(dependency_git_source(®istry, "spin-sdk"), (None, None)); + assert_ne!(dependency_git_source(®istry, "spin-sdk"), source); + assert_eq!(dependency_git_source(®istry, "absent"), (None, None)); } #[test] @@ -1018,20 +1125,12 @@ mod tests { } #[test] - fn build_tool_versions_spin_adds_install_hint_comment_not_asdf_pin() { - // Spin is install-flow-managed (not consistently asdf- - // managed in our toolchain), so don't write a brittle pin - // we can't honour — explain why with an inline hint so the - // operator isn't left guessing. + fn build_tool_versions_pins_spin_runtime() { let out = build_tool_versions(&["spin".to_owned()]); assert!(out.contains("rust 1.98.1")); assert!( - !out.contains("spin "), - "must NOT pin spin via asdf shape: {out}" - ); - assert!( - out.contains("spinframework.dev/install"), - "must point operators at the spin install URL: {out}" + out.contains("spin 4.1.0"), + "must pin the verified Spin runtime: {out}" ); } @@ -1053,6 +1152,7 @@ mod tests { for pin in [ "nodejs 24.20.0", "fastly 16.0.0", + "spin 4.1.0", "viceroy 0.21.0", "rust 1.98.1", ] { @@ -1062,8 +1162,6 @@ mod tests { "`{pin}` must appear exactly once in: {out}" ); } - // Spin install hint present. - assert!(out.contains("spinframework.dev/install")); // Stable order (alphabetical). let pin_block = out.split("\n\n").next().expect("pin block").to_owned(); let lines: Vec<&str> = pin_block.lines().collect(); @@ -1110,6 +1208,110 @@ mod tests { } } + fn with_generated_demo_app(assertions: impl FnOnce(&Path)) { + let _process_lock = path_mutation_guard().lock().expect("process env lock"); + let _lock = env_lock().lock().expect("env lock"); + let temp = TempDir::new().expect("temp dir"); + let bin_dir = temp.path().join("bin"); + write_git_stub(&bin_dir); + let _path_guard = PathOverride::new(&bin_dir); + + let args = NewArgs { + name: "demo-app".into(), + dir: Some(temp.path().to_string_lossy().into_owned()), + }; + generate_new(&args).expect("scaffold succeeds"); + assertions(&temp.path().join("demo-app")); + } + + #[test] + fn generated_readme_links_canonical_documentation() { + with_generated_demo_app(|project_dir| { + let readme = fs::read_to_string(project_dir.join("README.md")).expect("read README.md"); + for page in ["capabilities", "blob-app-config-migration"] { + assert!( + readme.contains(&format!( + "https://iabtechlab.github.io/edgezero/guide/{page}" + )), + "generated README must link to the canonical {page} guide" + ); + } + }); + } + + #[test] + fn generated_manifest_declares_outbound_http_optional() { + with_generated_demo_app(|project_dir| { + let source = + fs::read_to_string(project_dir.join("edgezero.toml")).expect("read manifest"); + let manifest = ManifestLoader::load_from_str(&source); + assert_eq!( + manifest.manifest().capabilities.optional, + [Capability::OutboundHttp, Capability::OutboundImageOptimizer] + ); + let hosts = manifest + .manifest() + .capabilities + .outbound + .hosts + .as_deref() + .expect("generated outbound hosts"); + assert_eq!(hosts, ["https://*:*"]); + let image = manifest + .manifest() + .triggers + .http + .iter() + .find(|route| route.id.as_deref() == Some("image-demo")) + .expect("Fastly image route"); + assert_eq!(image.adapters, ["fastly"]); + + let readme = fs::read_to_string(project_dir.join("README.md")).expect("read README.md"); + assert!( + readme.contains("may fail at runtime"), + "generated README must explain optional capability behavior" + ); + assert!( + readme.contains("promote `outbound-http` to `required`"), + "generated README must explain how to require outbound success" + ); + }); + } + + #[test] + fn generated_spin_hosts_default_to_https_only() { + with_generated_demo_app(|project_dir| { + let app_manifest_path = project_dir.join("edgezero.toml"); + let spin_manifest_path = project_dir.join("crates/demo-app-adapter-spin/spin.toml"); + let source = fs::read_to_string(&spin_manifest_path).expect("read spin.toml"); + let manifest: toml::Value = toml::from_str(&source).expect("parse spin.toml"); + let hosts = manifest + .get("component") + .and_then(|components| components.get("demo-app-adapter-spin")) + .and_then(|component| component.get("allowed_outbound_hosts")) + .and_then(toml::Value::as_array) + .expect("allowed outbound hosts"); + assert_eq!(hosts.len(), 1); + assert_eq!(hosts[0].as_str(), Some("https://*:*")); + assert!( + hosts.iter().all(|host| { + host.as_str() + .is_some_and(|value| !value.starts_with("http://")) + }), + "generated Spin manifest must not grant cleartext implicitly" + ); + let app_manifest = ManifestLoader::from_path(&app_manifest_path) + .expect("parse generated edgezero.toml"); + validate_spin_outbound_host_contract( + app_manifest.manifest(), + &app_manifest_path, + &spin_manifest_path, + None, + ) + .expect("generated application and selected Spin component must remain aligned"); + }); + } + fn assert_scaffold_files(project_dir: &Path) { assert!(project_dir.is_dir(), "project directory created"); assert!(project_dir.join("Cargo.toml").exists()); @@ -1232,6 +1434,10 @@ mod tests { ); } + #[expect( + clippy::too_many_lines, + reason = "the scaffold contract is clearest as one exhaustive generated-workspace assertion" + )] fn assert_scaffold_workspace(project_dir: &Path) { let cargo_toml = fs::read_to_string(project_dir.join("Cargo.toml")).expect("read Cargo.toml"); @@ -1249,6 +1455,7 @@ mod tests { } assert!(cargo_toml.contains("[workspace.lints.clippy]")); assert!(cargo_toml.contains("blanket_clippy_restriction_lints = \"allow\"")); + assert_scaffold_sdk_dependencies(&cargo_toml); // Generated from a checkout: edgezero crates must resolve to local // path dependencies, not the Git fallback (whose `edgezero-cli` has @@ -1264,6 +1471,27 @@ mod tests { let manifest = fs::read_to_string(project_dir.join("edgezero.toml")).expect("read edgezero.toml"); + let manifest_value: toml::Value = toml::from_str(&manifest).expect("parse edgezero.toml"); + assert_eq!( + manifest_value + .get("capabilities") + .and_then(|capabilities| capabilities.get("optional")) + .and_then(toml::Value::as_array) + .and_then(|values| values.first()) + .and_then(toml::Value::as_str), + Some("outbound-http"), + "generated manifest must explicitly opt into portable outbound HTTP" + ); + assert_eq!( + manifest_value + .get("capabilities") + .and_then(|capabilities| capabilities.get("outbound")) + .and_then(|outbound| outbound.get("hosts")) + .and_then(toml::Value::as_array) + .and_then(|values| values.first()) + .and_then(toml::Value::as_str), + Some("https://*:*") + ); assert!(manifest.contains("[adapters.cloudflare.adapter]")); assert!(manifest.contains("[adapters.fastly.adapter]")); assert!( @@ -1348,6 +1576,37 @@ mod tests { } } + fn assert_scaffold_sdk_dependencies(cargo_toml: &str) { + assert!( + cargo_toml.contains("fastly = \"=0.13.1\""), + "generated Fastly adapter must use the current SDK release line" + ); + assert!( + cargo_toml.contains(concat!( + "worker = { version = \"=0.8.5\", default-features = false, ", + "features = [\"http\"] }", + )), + "generated Cloudflare adapter must use the current SDK release" + ); + let generated = workspace_dependencies(cargo_toml); + let root = workspace_dependencies(include_str!("../../../Cargo.toml")); + assert_eq!( + dependency_requirement(&generated, "spin-sdk"), + dependency_requirement(&root, "spin-sdk"), + "generated Spin adapter must use the current SDK version" + ); + assert_eq!( + dependency_git_source(&generated, "spin-sdk"), + dependency_git_source(&root, "spin-sdk"), + "generated Spin adapter must use the exact SDK source and revision" + ); + assert_eq!( + generated["spin-sdk"]["default-features"].as_bool(), + Some(false), + "generated Spin adapter must keep SDK default features disabled" + ); + } + fn assert_scaffold_crate_lints(project_dir: &Path) { for crate_dir in [ "crates/demo-app-core", @@ -1374,9 +1633,237 @@ mod tests { /// templates shipped a production `.expect(...)` in the `stream` handler, /// infallible `IntoResponse` test usage, and adapter host stubs that /// tripped `print_stderr` / `exit`. - fn assert_generated_sources_are_lint_clean(project_dir: &Path) { + fn fallback_initializer_block(core_lib: &str) -> &str { + const VARIANT: &str = "AdmissionDecision::ReadBodyBeforeFallback"; + let variant_start = core_lib + .find(VARIANT) + .expect("fallback decision initializer"); + let after_variant = core_lib + .get(variant_start..) + .expect("fallback decision start boundary"); + let open_brace = after_variant + .find('{') + .and_then(|offset| variant_start.checked_add(offset)) + .expect("fallback decision opening brace"); + let mut depth = 0_usize; + + let after_open_brace = core_lib + .get(open_brace..) + .expect("fallback opening brace boundary"); + for (offset, character) in after_open_brace.char_indices() { + match character { + '{' => depth = depth.checked_add(1).expect("fallback brace depth"), + '}' => { + depth = depth.checked_sub(1).expect("balanced fallback braces"); + if depth == 0 { + let block_end = open_brace + .checked_add(offset) + .and_then(|end| end.checked_add(character.len_utf8())) + .expect("fallback initializer end"); + return core_lib + .get(variant_start..block_end) + .expect("fallback initializer boundaries"); + } + } + _ => {} + } + } + + panic!("fallback decision closing brace"); + } + + fn normalize_source_whitespace(source: &str) -> String { + source.split_whitespace().collect::>().join(" ") + } + + fn assert_generated_fallback_policy(core_lib: &str) { + let fallback = normalize_source_whitespace(fallback_initializer_block(core_lib)); + assert!( + core_lib.contains("FALLBACK_INGRESS_BODY_BYTES: usize = 4 * 1024") + && fallback.contains("completion: ResponseEgressCompletion::empty()") + && fallback.contains("grant: IngressGrant::empty()") + && fallback.contains("max_body_bytes: FALLBACK_INGRESS_BODY_BYTES") + && fallback.contains(concat!( + "on_exceeded: BufferedIngressResponse::text( ", + "StatusCode::PAYLOAD_TOO_LARGE, ", + "\"request body too large\\n\", ),", + )) + && fallback.contains(concat!( + "on_timeout: BufferedIngressResponse::text( ", + "StatusCode::REQUEST_TIMEOUT, ", + "\"request timeout\\n\", ),", + )), + "generated admission policy must bound fallback reads with exact terminal responses", + ); + } + + #[test] + #[should_panic( + expected = "generated admission policy must bound fallback reads with exact terminal responses" + )] + fn generated_fallback_policy_rejects_swapped_terminal_mappings() { + assert_generated_fallback_policy( + r#"AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + max_body_bytes: FALLBACK_INGRESS_BODY_BYTES, + on_exceeded: BufferedIngressResponse::text( + StatusCode::REQUEST_TIMEOUT, + "request timeout\n", + ), + on_timeout: BufferedIngressResponse::text( + StatusCode::PAYLOAD_TOO_LARGE, + "request body too large\n", + ), + } + const FALLBACK_INGRESS_BODY_BYTES: usize = 4 * 1024; + on_exceeded: BufferedIngressResponse::text( + StatusCode::PAYLOAD_TOO_LARGE, + "request body too large\n", + ), + on_timeout: BufferedIngressResponse::text( + StatusCode::REQUEST_TIMEOUT, + "request timeout\n", + ),"#, + ); + } + + fn assert_generated_axum_entrypoint(project_dir: &Path) { + let axum_main = + fs::read_to_string(project_dir.join("crates/demo-app-adapter-axum/src/main.rs")) + .expect("read axum main.rs"); + assert!( + !axum_main.contains("process::exit") + && !axum_main.contains("EdgeZeroAxumService") + && !axum_main.contains("into_axum_response") + && axum_main.contains("run_app::()"), + "generated Axum entrypoint must use the connection-owning runner", + ); + } + + fn assert_generated_cloudflare_entrypoint(project_dir: &Path) { + let entrypoint = + fs::read_to_string(project_dir.join("crates/demo-app-adapter-cloudflare/src/lib.rs")) + .expect("read Cloudflare lib.rs"); + assert!( + entrypoint.contains("run_app::(req, env, ctx).await") + && !entrypoint.contains("from_core_response"), + "generated Cloudflare entrypoint must use the context-owning runner", + ); + let manifest = fs::read_to_string( + project_dir.join("crates/demo-app-adapter-cloudflare/wrangler.toml"), + ) + .expect("read Cloudflare wrangler.toml"); + let parsed: toml::Value = toml::from_str(&manifest).expect("parse Wrangler manifest"); + let flags = parsed + .get("compatibility_flags") + .and_then(toml::Value::as_array) + .expect("Cloudflare compatibility flags"); + for required in ["enable_request_signal", "enable_weak_ref"] { + assert!( + flags.iter().any(|flag| flag.as_str() == Some(required)), + "generated Cloudflare manifest must enable {required}" + ); + } + } + + fn assert_generated_fastly_entrypoint(project_dir: &Path) { + let entrypoint = + fs::read_to_string(project_dir.join("crates/demo-app-adapter-fastly/src/main.rs")) + .expect("read fastly main.rs"); + assert!( + entrypoint.contains("reason ="), + "adapter attributes must carry a reason for allow_attributes_without_reason", + ); + assert!( + !entrypoint.contains("#[fastly::main]") + && !entrypoint.contains("Request") + && !entrypoint.contains("Response") + && entrypoint.contains("pub fn main() -> Result<(), fastly::Error>") + && entrypoint.contains("run_app::()"), + "generated Fastly entrypoint must let EdgeZero own request receipt and response delivery", + ); + } + + fn assert_generated_spin_entrypoint(project_dir: &Path) { + let entrypoint = + fs::read_to_string(project_dir.join("crates/demo-app-adapter-spin/src/lib.rs")) + .expect("read Spin lib.rs"); + assert!( + !entrypoint.contains("IntoResponse") + && !entrypoint.contains("SpinFullResponse") + && !entrypoint.contains("FullBody") + && entrypoint.contains("anyhow::Result") + && entrypoint.contains("run_app::(req)"), + "generated Spin entrypoint must return EdgeZero's raw WASIp3 response type", + ); + } + + fn assert_generated_core_lifecycle(project_dir: &Path) { + let core_lib = fs::read_to_string(project_dir.join("crates/demo-app-core/src/lib.rs")) + .expect("read core lib.rs"); + assert!( + core_lib.contains("configure = crate::configure_app"), + "generated app macro must install the lifecycle configuration callback", + ); + assert!( + core_lib.contains("fn configure_app(app: &mut EdgeZeroApp) -> Result<(), EdgeError>") + && core_lib.contains("Ok(())"), + "generated lifecycle configuration must propagate initialization failures", + ); + assert!( + core_lib.contains("set_ingress_admission_policy"), + "generated app must configure ingress admission", + ); + let memory_behavior_tests = [ + "platform_memory_startup_accepts_fit", + "platform_memory_startup_preserves_primary_excess", + "platform_memory_startup_preserves_stack_excess", + "platform_memory_startup_keeps_indeterminate_distinct_from_fit", + "platform_memory_startup_preserves_validation_error", + "platform_memory_validation_accounts_for_both_stack_shapes", + ]; + assert!( + core_lib.contains("platform_memory_startup(app.platform())") + && core_lib.contains("platform.validate_memory_envelope(envelope)") + && core_lib.contains("MemoryEnvelopeValidation::Fits") + && core_lib.contains("MemoryEnvelopeValidation::Exceeds") + && core_lib.contains("MemoryEnvelopeValidation::Indeterminate") + && core_lib.contains("PlatformMemoryStartupError::UnsupportedValidationOutcome") + && memory_behavior_tests + .into_iter() + .all(|test_name| core_lib.contains(test_name)) + && !core_lib.contains("Semaphore") + && !core_lib.contains("set_ingress_concurrency"), + "generated app must carry typed memory startup behavior without adding a concurrency gate", + ); + assert!( + core_lib.contains("completion: ResponseEgressCompletion::empty()") + && core_lib.contains("grant: IngressGrant::empty()") + && !core_lib.contains("ResponsePermit") + && !core_lib.contains("AdmissionLease") + && !core_lib.contains("ResponseEgressCompletion::late_bound") + && !core_lib.contains("set_detached_response_egress_decision_factory"), + "generated apps must use the minimal core-owned lifecycle defaults", + ); + assert!( + core_lib.contains( + "EdgeZeroApp::build::(edgezero_core::PlatformMetadata::default())" + ), + "generated tests must handle fallible application assembly", + ); + assert_generated_fallback_policy(&core_lib); + } + + fn assert_generated_outbound_handler(project_dir: &Path) { let handlers = fs::read_to_string(project_dir.join("crates/demo-app-core/src/handlers.rs")) .expect("read handlers.rs"); + assert!( + handlers.contains("ctx.take_ingress_grant().is_some()") + && !handlers.contains("AdmissionLease") + && !handlers.contains("install_response_permit"), + "generated admission handler must demonstrate one-time grant consumption without a fake permit", + ); assert!( handlers.contains("pub async fn stream() -> Result"), "stream handler must be fallible, not panic via expect()", @@ -1389,45 +1876,130 @@ mod tests { handlers.contains(".into_response()"), "handler tests must use the fallible IntoResponse pattern", ); + assert!( + handlers.contains("HttpClient::with_client(TestOutboundClient::default())"), + "generated tests must execute the portable outbound client", + ); + assert!( + handlers.contains("fn generated_outbound_http_smoke()"), + "generated core must contain the outbound smoke sentinel", + ); + for required in [ + ".response_decoding(ResponseDecoding::Original)", + ".with_proxy_marker(false)", + ".cache_policy(", + ".max_request_body_bytes(", + ".max_encoded_response_bytes(", + ".max_decoded_response_bytes(", + ".max_response_bytes(", + ".max_response_header_bytes(", + ".max_response_header_count(", + ".max_chunk_bytes(", + ".max_brotli_window_bits(", + ".max_decoder_bytes(", + ".timeout(", + "send_all_until(", + "map_err(|failure| failure.into_parts().0)", + "OutboundBatchTermination::Completed", + "OutboundBatchTermination::Cutoff", + "OutboundBatch::from_driver", + "OutboundBatchDriverEvent::Item {", + "OutboundCollectedSlot::AlreadyDelivered", + "OutboundCollectedSlot::Resolved", + "OutboundCollectedSlot::Unresolved", + "OutboundPreparedBatch", + "BodyPayload::Stream", + "let (payload, body_owner) = response.into_body().into_parts();", + "fn progress(&self) -> Rc", + "Rc::new(IndependentOutboundProgress)", + "OutboundDispatchRequest", + "OutboundHead::new", + "request.into_parts()", + "batch.into_slots()", + "OutboundExchangeOwner::new", + "let completed_at = self.time.clock().now();", + "time: OutboundTime", + "self.time.clone()", + "generated_outbound_client_retains_its_clock_in_responses", + "scripted_batch_preserves_receipts_through_mixed_collection", + "scripted_batch_cutoff_retains_only_earlier_completions", + "scripted_batch_expired_cutoff_keeps_preflight_outcomes", + "OutboundBatchDriverEvent::Cutoff", + "OutboundBatch::cutoff(context)", + "FanoutSlotOutcome::Unresolved", + "OutboundCachePolicy::Bypass", + "slot.elapsed", + ] { + assert!( + handlers.contains(required), + "generated outbound example must contain `{required}`", + ); + } + assert!( + !handlers.contains(".send_all("), + "generated outbound example must not retain the removed send_all API", + ); + assert!( + !handlers.contains("max_brotli_decoder_bytes") + && !handlers.contains("MAX_BROTLI_DECODER_BYTES"), + "generated outbound example must use the generalized decoder budget", + ); + assert!( + handlers.contains("ResponseEgressDeadline::after(RESPONSE_EGRESS_BUDGET)") + && !handlers.contains(concat!("ResponseEgressDeadline::", "new(")), + "generated responses must use an egress-start-relative deadline", + ); + assert!( + handlers.contains(".variable(\"API_BASE_URL\")"), + "generated handlers must read runtime variables through the request context", + ); + } - let axum_main = - fs::read_to_string(project_dir.join("crates/demo-app-adapter-axum/src/main.rs")) - .expect("read axum main.rs"); + fn assert_generated_route_and_provider_manifests(project_dir: &Path) { + let manifest = + fs::read_to_string(project_dir.join("edgezero.toml")).expect("read edgezero.toml"); + assert!( + manifest.contains("[[environment.variables]]\nname = \"API_BASE_URL\""), + "generated manifest must declare the upstream runtime variable", + ); + assert!( + manifest.contains("path = \"/admission\"") + && manifest.contains("class = \"diagnostic\""), + "generated manifest must wire the admission diagnostic class", + ); assert!( - !axum_main.contains("process::exit"), - "axum host entrypoint must return Result, not call process::exit", + manifest.contains("path = \"/fanout\"") && manifest.contains("class = \"outbound\""), + "generated manifest must wire the outbound fanout class", ); - let fastly_main = - fs::read_to_string(project_dir.join("crates/demo-app-adapter-fastly/src/main.rs")) - .expect("read fastly main.rs"); + let spin_manifest = + fs::read_to_string(project_dir.join("crates/demo-app-adapter-spin/spin.toml")) + .expect("read spin.toml"); assert!( - fastly_main.contains("reason ="), - "adapter attributes must carry a reason for allow_attributes_without_reason", + spin_manifest.contains("allowed_outbound_hosts = [\"https://*:*\"]"), + "generated Spin hosts must default to HTTPS only", ); } + fn assert_generated_sources_are_lint_clean(project_dir: &Path) { + assert_generated_core_lifecycle(project_dir); + assert_generated_outbound_handler(project_dir); + assert_generated_route_and_provider_manifests(project_dir); + assert_generated_axum_entrypoint(project_dir); + assert_generated_cloudflare_entrypoint(project_dir); + assert_generated_fastly_entrypoint(project_dir); + assert_generated_spin_entrypoint(project_dir); + } + #[test] fn generate_new_scaffolds_workspace_layout() { - let _path_lock = path_mutation_guard().lock().expect("path guard"); - let temp = TempDir::new().expect("temp dir"); - let bin_dir = temp.path().join("bin"); - write_git_stub(&bin_dir); - let _path_guard = PathOverride::new(&bin_dir); - - let args = NewArgs { - name: "demo-app".into(), - dir: Some(temp.path().to_string_lossy().into_owned()), - }; - - generate_new(&args).expect("scaffold succeeds"); - - let project_dir = temp.path().join("demo-app"); - assert_scaffold_files(&project_dir); - assert_scaffold_workspace(&project_dir); - assert_scaffold_app_config(&project_dir); - assert_scaffold_crate_lints(&project_dir); - assert_scaffold_cli_full_command_set(&project_dir); + with_generated_demo_app(|project_dir| { + assert_scaffold_files(project_dir); + assert_scaffold_workspace(project_dir); + assert_scaffold_app_config(project_dir); + assert_scaffold_crate_lints(project_dir); + assert_scaffold_cli_full_command_set(project_dir); + }); } /// The scaffolded `-cli` must diff --git a/crates/edgezero-cli/src/lib.rs b/crates/edgezero-cli/src/lib.rs index 37232db75..6e4aea1fc 100644 --- a/crates/edgezero-cli/src/lib.rs +++ b/crates/edgezero-cli/src/lib.rs @@ -32,6 +32,8 @@ mod diff; #[cfg(feature = "cli")] mod generator; #[cfg(feature = "cli")] +mod manifest_source; +#[cfg(feature = "cli")] mod provision; #[cfg(feature = "cli")] mod scaffold; @@ -59,7 +61,9 @@ use args::{ ActiveVersionArgs, BuildArgs, DeployArgs, HealthcheckArgs, NewArgs, RollbackArgs, ServeArgs, }; #[cfg(feature = "cli")] -use edgezero_adapter::registry::{AdapterDeployContext, DeployStoreIds}; +use edgezero_adapter::registry::{AdapterDeployContext, AdapterExecutionTarget, DeployStoreIds}; +#[cfg(any(test, feature = "demo-example"))] +use edgezero_core::app::Hooks; #[cfg(feature = "cli")] use edgezero_core::manifest::{Manifest, ManifestLoader, StoreDeclaration}; #[cfg(feature = "cli")] @@ -124,7 +128,8 @@ impl log::Log for CliLogger { /// Initialize a CLI logger that prints messages without timestamps /// or level prefixes — the CLI's output IS the user-facing UX, not -/// a debug log. See [`CliLogger`] for the routing rules. +/// a debug log. Informational messages go to stdout, warnings and errors go to +/// stderr, and debug and trace messages are filtered out. #[cfg(feature = "cli")] #[inline] pub fn init_cli_logger() { @@ -142,17 +147,11 @@ pub fn init_cli_logger() { #[cfg(feature = "cli")] #[inline] pub fn run_build(args: &BuildArgs) -> Result<(), String> { - let manifest = load_manifest_optional()?; - ensure_adapter_defined(&args.adapter, manifest.as_ref())?; - if let Some(loader) = &manifest { - log_store_bindings(&args.adapter, loader); + let runtime = manifest_source::resolve_runtime(&args.adapter, adapter::Action::Build)?; + if let Some(manifest) = runtime.manifest() { + log_store_bindings(runtime.adapter_name(), manifest); } - adapter::execute( - &args.adapter, - adapter::Action::Build, - manifest.as_ref(), - &args.adapter_args, - ) + adapter::execute_runtime(&runtime, &args.adapter_args) } /// Deploy the project to a target edge adapter. @@ -185,48 +184,69 @@ pub fn run_deploy(args: &DeployArgs) -> Result<(), String> { )); } - let manifest = load_manifest_optional()?; - ensure_adapter_defined(&args.adapter, manifest.as_ref())?; - let manifest_stores = manifest.as_ref().map(|loader| &loader.manifest().stores); + let action = if args.staging { + adapter::Action::DeployStaging + } else { + adapter::Action::Deploy + }; + let runtime = manifest_source::resolve_runtime(&args.adapter, action)?; + let manifest_stores = runtime.manifest().map(|manifest| &manifest.stores); let declared_ids = |declaration: Option<&StoreDeclaration>| { declaration .map(|store| store.ids.clone()) .unwrap_or_default() }; - let deploy_stores = DeployStoreIds { + let stores = DeployStoreIds { config: declared_ids(manifest_stores.and_then(|declared| declared.config.as_ref())), kv: declared_ids(manifest_stores.and_then(|declared| declared.kv.as_ref())), secrets: declared_ids(manifest_stores.and_then(|declared| declared.secrets.as_ref())), }; - let variable_defaults = manifest - .as_ref() - .map(|loader| manifest_variable_defaults(loader.manifest(), &args.adapter)) + let variable_defaults = runtime + .manifest() + .map(|manifest| manifest_variable_defaults(manifest, runtime.adapter_name())) .unwrap_or_default(); - let (adapter_manifest_path, adapter_manifest_path_error) = - match resolve_adapter_manifest_path(manifest.as_ref(), &args.adapter) { - Ok(path) => (path.map(PathBuf::from), None), - Err(err) => (None, Some(err)), - }; - let application_manifest_path = loaded_application_manifest_path(manifest.as_ref())?; + let (adapter_manifest_path, adapter_manifest_path_error) = match runtime.target() { + manifest_source::ResolvedAdapterTarget::Registered(target) => { + (target.platform_manifest().map(Path::to_path_buf), None) + } + manifest_source::ResolvedAdapterTarget::Shell(_) => { + match resolve_adapter_manifest_path(runtime.manifest_loader(), runtime.adapter_name()) { + Ok(path) => (path.map(PathBuf::from), None), + Err(error) => (None, Some(error)), + } + } + }; + let application_manifest_path = runtime.manifest_path().map(Path::to_path_buf); let application_release_root = resolve_application_release_root( args.application_release.as_deref(), application_manifest_path.as_deref(), )?; + let target = match runtime.target() { + manifest_source::ResolvedAdapterTarget::Registered(target) => target.clone(), + manifest_source::ResolvedAdapterTarget::Shell(shell) => AdapterExecutionTarget::new( + shell.root().to_path_buf(), + shell.component().map(str::to_owned), + adapter_manifest_path.clone(), + ), + }; + let defaults = runtime.manifest().map_or_else(Vec::new, |manifest| { + adapter::resolve_environment_defaults(&manifest.environment_for(runtime.adapter_name())) + }); let context = AdapterDeployContext { adapter_manifest_path, application_manifest_path, application_release_root, + execution_target: Some(target.with_environment_defaults(defaults)), service_id: args.service_id.clone(), - stores: deploy_stores, + stores, staging: args.staging, variable_defaults, }; - adapter::deploy( - &args.adapter, + adapter::deploy_runtime( + &runtime, &context, adapter_manifest_path_error.as_deref(), - manifest.as_ref(), &args.adapter_args, ) } @@ -241,30 +261,6 @@ fn manifest_variable_defaults(manifest: &Manifest, adapter: &str) -> BTreeMap, -) -> Result, String> { - if loader.is_none() { - return Ok(None); - } - let path = env::var("EDGEZERO_MANIFEST") - .map_or_else(|_| PathBuf::from("edgezero.toml"), PathBuf::from); - let canonical = path.canonicalize().map_err(|error| { - format!( - "could not resolve loaded application manifest {}: {error}", - path.display() - ) - })?; - if !canonical.is_file() { - return Err(format!( - "loaded application manifest {} is not a regular file", - canonical.display() - )); - } - Ok(Some(canonical)) -} - #[cfg(feature = "cli")] fn resolve_application_release_root( requested_root: Option<&Path>, @@ -486,14 +482,8 @@ pub fn run_active_version(args: &ActiveVersionArgs) -> Result<(), String> { #[cfg(feature = "cli")] #[inline] pub fn run_serve(args: &ServeArgs) -> Result<(), String> { - let manifest = load_manifest_optional()?; - ensure_adapter_defined(&args.adapter, manifest.as_ref())?; - adapter::execute( - &args.adapter, - adapter::Action::Serve, - manifest.as_ref(), - &[], - ) + let runtime = manifest_source::resolve_runtime(&args.adapter, adapter::Action::Serve)?; + adapter::execute_runtime(&runtime, &[]) } /// Create a new `EdgeZero` app skeleton. @@ -521,10 +511,17 @@ pub fn run_demo() -> Result<(), String> { demo_server::run_demo() } +#[cfg(any(test, feature = "demo-example"))] +fn demo_capability_gate() -> Result<(), String> +where + Application: Hooks, +{ + adapter::ensure_capabilities("axum", Application::manifest().as_contract()) +} + #[cfg(feature = "cli")] -fn store_bindings_message(adapter_name: &str, manifest: &ManifestLoader) -> Option { - let manifest_data = manifest.manifest(); - if !manifest_data.secret_store_enabled(adapter_name) { +fn store_bindings_message(adapter_name: &str, manifest: &Manifest) -> Option { + if !manifest.secret_store_enabled(adapter_name) { return None; } @@ -549,7 +546,7 @@ fn store_bindings_message(adapter_name: &str, manifest: &ManifestLoader) -> Opti } #[cfg(feature = "cli")] -fn log_store_bindings(adapter_name: &str, manifest: &ManifestLoader) { +fn log_store_bindings(adapter_name: &str, manifest: &Manifest) { if let Some(message) = store_bindings_message(adapter_name, manifest) { log::info!("{message}"); } @@ -607,7 +604,10 @@ mod tests { use edgezero_adapter::registry::{ self as adapter_registry, Adapter, AdapterAction, DeployOwnership, }; - use edgezero_core::manifest::ManifestLoader; + use edgezero_core::EdgeError; + use edgezero_core::app::Hooks; + use edgezero_core::manifest::{BakedManifest, Manifest, ManifestLoader}; + use edgezero_core::router::RouterService; #[cfg(unix)] use edgezero_core::test_env::PathPrepend; use std::collections::BTreeMap; @@ -632,8 +632,25 @@ mod tests { LazyLock::new(|| Mutex::new(None)); static RECORDING_DEPLOY_ADAPTER: RecordingDeployAdapter = RecordingDeployAdapter; + struct CapabilityGateApp; struct RecordingDeployAdapter; + #[expect( + clippy::missing_trait_methods, + reason = "test hook overrides only routes and the baked manifest under test" + )] + impl Hooks for CapabilityGateApp { + fn manifest() -> BakedManifest { + Manifest::from_baked_json( + r#"{"capabilities":{"required":["outbound-complete-resource-accounting"]}}"#, + ) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + #[expect( clippy::missing_trait_methods, reason = "the recording adapter exercises only deploy preflight, deploy dispatch, and finalization" @@ -696,6 +713,41 @@ mod tests { .expect("deploy preflight context lock") = None; } + #[test] + fn demo_capability_gate_runs_before_server_start() { + assert!(demo_capability_gate::().is_err()); + } + + #[test] + fn deploy_capability_gate_precedes_managed_preflight() { + let _lock = manifest_guard().lock().expect("manifest guard"); + reset_recording_deploy_adapter(PREFLIGHT_ADAPTER_MANAGED); + let temp = TempDir::new().expect("temp dir"); + let manifest = temp.path().join("edgezero.toml"); + fs::write(&manifest, "[app]\nname = \"demo\"\n[capabilities]\nrequired = [\"outbound-complete-resource-accounting\"]\n[adapters.recording_deploy_test.commands]\ndeploy = \"exit 99\"\n").expect("write manifest"); + let _manifest = EnvOverride::set("EDGEZERO_MANIFEST", &manifest); + let error = run_deploy(&DeployArgs { + adapter: "recording_deploy_test".to_owned(), + application_release: None, + adapter_args: Vec::new(), + service_id: None, + staging: false, + }) + .expect_err("unsupported required capability must block deployment"); + assert!( + error.contains("outbound-complete-resource-accounting"), + "{error}" + ); + assert!( + DEPLOY_PREFLIGHT_CONTEXT + .lock() + .expect("preflight lock") + .is_none() + ); + assert_eq!(DEPLOY_CALLS.load(Ordering::SeqCst), 0); + assert_eq!(DEPLOY_FINALIZE_CALLS.load(Ordering::SeqCst), 0); + } + #[test] fn load_manifest_optional_hard_errors_when_explicit_env_path_missing() { // An explicit `EDGEZERO_MANIFEST` pointing at a missing file must @@ -834,7 +886,7 @@ mod tests { .expect_err("staging must resolve the explicitly selected fastly.toml"); assert!( - err.contains("missing/fastly.toml") && err.contains("could not resolve"), + err.contains("missing/fastly.toml") && err.contains("failed to resolve"), "staging reports the selected missing manifest before discovery: {err}" ); } @@ -1476,19 +1528,20 @@ ids = ["MY_SECRETS"] "#, ); - let axum = store_bindings_message("axum", &loader).expect("axum message"); + let axum = store_bindings_message("axum", loader.manifest()).expect("axum message"); assert!(axum.contains("environment variables")); - let cloudflare = store_bindings_message("cloudflare", &loader).expect("cloudflare message"); + let cloudflare = + store_bindings_message("cloudflare", loader.manifest()).expect("cloudflare message"); assert!(cloudflare.contains("wrangler")); - let fastly = store_bindings_message("fastly", &loader).expect("fastly message"); + let fastly = store_bindings_message("fastly", loader.manifest()).expect("fastly message"); assert!(fastly.contains("secrets enabled")); } #[test] fn store_bindings_message_is_absent_without_secret_store() { let loader = ManifestLoader::load_from_str("[app]\nname = \"x\"\n"); - assert!(store_bindings_message("fastly", &loader).is_none()); + assert!(store_bindings_message("fastly", loader.manifest()).is_none()); } } diff --git a/crates/edgezero-cli/src/main.rs b/crates/edgezero-cli/src/main.rs index bdcf076c8..491c44929 100644 --- a/crates/edgezero-cli/src/main.rs +++ b/crates/edgezero-cli/src/main.rs @@ -6,8 +6,14 @@ fn main() { use edgezero_cli::args::{self, Args, Command, ConfigCmd}; use std::process; + let cmd = Args::parse().cmd; + #[cfg(feature = "demo-example")] + if !matches!(&cmd, Command::Demo) { + edgezero_cli::init_cli_logger(); + } + #[cfg(not(feature = "demo-example"))] edgezero_cli::init_cli_logger(); - let result = match Args::parse().cmd { + let result = match cmd { Command::Auth(cmd_args) => edgezero_cli::run_auth(&cmd_args), Command::Build(cmd_args) => edgezero_cli::run_build(&cmd_args), // `config push` and `config diff` require a typed app-config struct @@ -18,7 +24,7 @@ fn main() { #[expect( clippy::print_stderr, reason = "intentional: pointer text must reach the user even when \ - stdout is piped; this is the only stderr write in main" + stdout is piped" )] { eprintln!("{}", args::STUB_POINTER_AFTER_HELP); @@ -42,7 +48,13 @@ fn main() { Command::Serve(cmd_args) => edgezero_cli::run_serve(&cmd_args), }; if let Err(err) = result { - log::error!("[edgezero] {err}"); + #[expect( + clippy::print_stderr, + reason = "terminal command errors must remain visible when runtime logging is Off" + )] + { + eprintln!("[edgezero] {err}"); + }; process::exit(1); } } diff --git a/crates/edgezero-cli/src/manifest_source.rs b/crates/edgezero-cli/src/manifest_source.rs new file mode 100644 index 000000000..cd4f4fb1a --- /dev/null +++ b/crates/edgezero-cli/src/manifest_source.rs @@ -0,0 +1,511 @@ +use std::env; +use std::ffi::OsString; +use std::fs; +use std::path::{Path, PathBuf}; + +use edgezero_adapter::cli_support::{find_workspace_root, path_distance}; +use edgezero_adapter::registry::AdapterExecutionTarget; +use edgezero_core::manifest::{Manifest, ManifestLoader, ResolvedEnvironment}; + +use crate::adapter::Action; + +pub(crate) enum ResolvedAdapterTarget { + Registered(AdapterExecutionTarget), + Shell(ResolvedShellTarget), +} + +pub(crate) struct ResolvedManifest { + loader: ManifestLoader, + path: PathBuf, +} + +pub(crate) struct ResolvedRuntime { + action: Action, + adapter: String, + contract: Option, + target: ResolvedAdapterTarget, +} + +pub(crate) struct ResolvedShellTarget { + bind_host: Option, + bind_port: Option, + command: String, + component: Option, + environment: ResolvedEnvironment, + platform_manifest: Option, + root: PathBuf, +} + +impl ResolvedManifest { + #[inline] + pub(crate) fn path(&self) -> &Path { + &self.path + } +} + +impl ResolvedRuntime { + #[inline] + pub(crate) fn action(&self) -> Action { + self.action + } + + #[inline] + pub(crate) fn adapter_name(&self) -> &str { + &self.adapter + } + + #[inline] + pub(crate) fn manifest(&self) -> Option<&Manifest> { + self.contract + .as_ref() + .map(|contract| contract.loader.manifest()) + } + + #[inline] + pub(crate) fn manifest_loader(&self) -> Option<&ManifestLoader> { + self.contract.as_ref().map(|contract| &contract.loader) + } + + #[inline] + pub(crate) fn manifest_path(&self) -> Option<&Path> { + self.contract.as_ref().map(ResolvedManifest::path) + } + + #[inline] + pub(crate) fn target(&self) -> &ResolvedAdapterTarget { + &self.target + } +} + +impl ResolvedShellTarget { + #[inline] + pub(crate) fn bind_host(&self) -> Option<&str> { + self.bind_host.as_deref() + } + + #[inline] + pub(crate) fn bind_port(&self) -> Option { + self.bind_port + } + + #[inline] + pub(crate) fn command(&self) -> &str { + &self.command + } + + #[inline] + pub(crate) fn component(&self) -> Option<&str> { + self.component.as_deref() + } + + #[inline] + pub(crate) fn environment(&self) -> &ResolvedEnvironment { + &self.environment + } + + #[inline] + pub(crate) fn platform_manifest(&self) -> Option<&Path> { + self.platform_manifest.as_deref() + } + + #[inline] + pub(crate) fn root(&self) -> &Path { + &self.root + } +} + +pub(crate) fn resolve_runtime(adapter: &str, action: Action) -> Result { + let invocation_dir = env::current_dir() + .map_err(|error| format!("failed to read invocation directory: {error}"))?; + resolve_runtime_from( + adapter, + action, + &invocation_dir, + env::var_os("EDGEZERO_MANIFEST"), + ) +} + +fn canonical_directory(path: &Path, label: &str) -> Result { + let canonical = fs::canonicalize(path) + .map_err(|error| format!("failed to resolve {label} {}: {error}", path.display()))?; + if !canonical.is_dir() { + return Err(format!( + "{label} {} is not a directory", + canonical.display() + )); + } + Ok(canonical) +} + +fn canonical_regular_file(path: &Path, label: &str) -> Result { + let canonical = fs::canonicalize(path) + .map_err(|error| format!("failed to resolve {label} {}: {error}", path.display()))?; + if !canonical.is_file() { + return Err(format!( + "{label} {} is not a regular file", + canonical.display() + )); + } + Ok(canonical) +} + +#[expect( + clippy::filetype_is_file, + reason = "manifest discovery intentionally excludes symbolic links before canonicalization" +)] +fn collect_named_files(root: &Path, name: &str, depth: usize, output: &mut Vec) { + if depth == 0 { + return; + } + let Ok(entries) = fs::read_dir(root) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + let Ok(file_type) = entry.file_type() else { + continue; + }; + if file_type.is_file() && path.file_name().is_some_and(|file_name| file_name == name) { + output.push(path); + } else if file_type.is_dir() + && !matches!( + path.file_name().and_then(|file_name| file_name.to_str()), + Some(".git" | "target") + ) + { + collect_named_files(&path, name, depth.saturating_sub(1), output); + } else { + // Other files and excluded directories are not manifest candidates. + } + } +} + +fn command_for(manifest: &Manifest, adapter: &str, action: Action) -> Option { + let (_canonical, config) = manifest.adapter_entry(adapter)?; + match action { + Action::Build => config.commands.build.clone(), + Action::Deploy => config.commands.deploy.clone(), + Action::Serve => config.commands.serve.clone(), + Action::DeployStaging + | Action::AuthLogin + | Action::AuthLogout + | Action::AuthStatus + | Action::EmitVersion + | Action::Healthcheck + | Action::Rollback => None, + } +} + +fn discover_default_manifest(invocation_dir: &Path) -> Result, String> { + for ancestor in invocation_dir.ancestors() { + let candidate = ancestor.join("edgezero.toml"); + if candidate.is_file() { + return canonical_regular_file(&candidate, "application manifest").map(Some); + } + } + + let workspace = canonical_directory(&find_workspace_root(invocation_dir), "workspace root")?; + let mut candidates = Vec::new(); + collect_named_files(&workspace, "edgezero.toml", 9, &mut candidates); + choose_nearest(invocation_dir, candidates, "application manifest") +} + +fn discover_platform_manifest( + adapter: &str, + invocation_dir: &Path, +) -> Result, String> { + let name = platform_manifest_name(adapter); + for ancestor in invocation_dir.ancestors() { + let candidate = ancestor.join(name); + if candidate.is_file() && ancestor.join("Cargo.toml").is_file() { + return canonical_regular_file(&candidate, "platform manifest").map(Some); + } + } + let workspace = canonical_directory(&find_workspace_root(invocation_dir), "workspace root")?; + let mut candidates = Vec::new(); + collect_named_files(&workspace, name, 9, &mut candidates); + candidates.retain(|path| { + path.parent() + .is_some_and(|parent| parent.join("Cargo.toml").is_file()) + }); + choose_nearest(invocation_dir, candidates, "platform manifest") +} + +fn choose_nearest( + invocation_dir: &Path, + candidates: Vec, + label: &str, +) -> Result, String> { + let mut ranked = candidates + .into_iter() + .map(|path| { + let parent = path.parent().unwrap_or(Path::new("")); + (path_distance(invocation_dir, parent), path) + }) + .collect::>(); + ranked.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1))); + let Some((distance, selected)) = ranked.first() else { + return Ok(None); + }; + if let Some((_next_distance, next)) = ranked.get(1).filter(|next| next.0 == *distance) { + return Err(format!( + "ambiguous {label} selection: {} and {} are equally near the invocation directory", + selected.display(), + next.display() + )); + } + canonical_regular_file(selected, label).map(Some) +} + +fn load_resolved_manifest(path: PathBuf) -> Result { + let loader = ManifestLoader::from_path(&path) + .map_err(|error| format!("failed to load {}: {error}", path.display()))?; + Ok(ResolvedManifest { loader, path }) +} + +fn platform_manifest_from_contract( + manifest: &Manifest, + adapter: &str, + app_root: &Path, +) -> Result, String> { + let Some((_canonical, config)) = manifest.adapter_entry(adapter) else { + return Err(format!( + "adapter `{adapter}` is not configured in {}", + app_root.join("edgezero.toml").display() + )); + }; + let Some(relative) = config.adapter.manifest.as_deref() else { + return Ok(None); + }; + let relative_path = Path::new(relative); + if relative_path.is_absolute() { + return Err(format!( + "adapter `{adapter}` platform manifest must be relative to the application root" + )); + } + let platform = canonical_regular_file( + &app_root.join(relative_path), + &format!("adapter `{adapter}` platform manifest"), + )?; + if !platform.starts_with(app_root) { + return Err(format!( + "adapter `{adapter}` platform manifest {} resolves outside application root {}", + platform.display(), + app_root.display() + )); + } + Ok(Some(platform)) +} + +fn platform_manifest_name(adapter: &str) -> &'static str { + if adapter.eq_ignore_ascii_case("axum") { + "axum.toml" + } else if adapter.eq_ignore_ascii_case("cloudflare") { + "wrangler.toml" + } else if adapter.eq_ignore_ascii_case("fastly") { + "fastly.toml" + } else if adapter.eq_ignore_ascii_case("spin") { + "spin.toml" + } else { + "edgezero-platform.toml" + } +} + +fn produces_current_runtime(action: Action) -> bool { + match action { + Action::Build | Action::Deploy | Action::DeployStaging | Action::Serve => true, + Action::AuthLogin + | Action::AuthLogout + | Action::AuthStatus + | Action::EmitVersion + | Action::Healthcheck + | Action::Rollback => false, + } +} + +fn resolve_contract_path( + invocation_dir: &Path, + explicit: Option, +) -> Result, String> { + if let Some(raw) = explicit { + let configured = PathBuf::from(raw); + let candidate = if configured.is_absolute() { + configured + } else { + invocation_dir.join(configured) + }; + return canonical_regular_file(&candidate, "explicit application manifest").map(Some); + } + discover_default_manifest(invocation_dir) +} + +pub(crate) fn resolve_runtime_from( + adapter: &str, + action: Action, + invocation_dir: &Path, + explicit: Option, +) -> Result { + if !produces_current_runtime(action) { + return Err("operational action entered outbound runtime resolver".to_owned()); + } + let canonical_invocation_dir = canonical_directory(invocation_dir, "invocation directory")?; + let contract_path = resolve_contract_path(&canonical_invocation_dir, explicit)?; + let contract = contract_path.map(load_resolved_manifest).transpose()?; + + if let Some(resolved_manifest) = contract { + let manifest = resolved_manifest.loader.manifest(); + let (canonical_adapter, config) = manifest.adapter_entry(adapter).ok_or_else(|| { + format!( + "adapter `{adapter}` is not configured in {}", + resolved_manifest.path.display() + ) + })?; + let app_root = canonical_directory( + manifest + .root() + .ok_or_else(|| "resolved manifest has no application root".to_owned())?, + "application root", + )?; + let adapter_name = canonical_adapter.to_ascii_lowercase(); + let component = config.adapter.component.clone(); + let target = if let Some(command) = command_for(manifest, &adapter_name, action) { + let platform_manifest = if adapter_name == "spin" { + platform_manifest_from_contract(manifest, &adapter_name, &app_root)? + } else { + None + }; + ResolvedAdapterTarget::Shell(ResolvedShellTarget { + bind_host: config.adapter.host.clone(), + bind_port: config.adapter.port, + command, + component, + environment: manifest.environment_for(&adapter_name), + platform_manifest, + root: app_root, + }) + } else { + let platform_manifest = + platform_manifest_from_contract(manifest, &adapter_name, &app_root)?; + ResolvedAdapterTarget::Registered(AdapterExecutionTarget::new( + app_root, + component, + platform_manifest, + )) + }; + return Ok(ResolvedRuntime { + action, + adapter: adapter_name, + contract: Some(resolved_manifest), + target, + }); + } + + let platform_manifest = discover_platform_manifest(adapter, &canonical_invocation_dir)?; + let app_root = platform_manifest + .as_deref() + .and_then(Path::parent) + .map(Path::to_path_buf) + .unwrap_or(canonical_invocation_dir); + Ok(ResolvedRuntime { + action, + adapter: adapter.to_ascii_lowercase(), + contract: None, + target: ResolvedAdapterTarget::Registered(AdapterExecutionTarget::new( + app_root, + None, + platform_manifest, + )), + }) +} + +#[cfg(test)] +mod tests { + use std::ffi::OsString; + + use tempfile::TempDir; + + use super::*; + + fn write_manifest(root: &Path, adapter: &str, platform: &str, command: Option<&str>) { + fs::create_dir_all(root).expect("create app root"); + fs::write(root.join(platform), "# platform fixture\n").expect("write platform manifest"); + let command_section = command.map_or_else(String::new, |configured_command| { + format!("\n[adapters.{adapter}.commands]\nbuild = {configured_command:?}\n") + }); + fs::write( + root.join("edgezero.toml"), + format!( + "[app]\nname = \"fixture\"\n[adapters.{adapter}.adapter]\nmanifest = {platform:?}\n{command_section}" + ), + ) + .expect("write app manifest"); + fs::write(root.join("Cargo.toml"), "[workspace]\n").expect("write workspace manifest"); + } + + #[test] + fn explicit_manifest_is_authoritative() { + let temp = TempDir::new().expect("temp dir"); + let invocation = temp.path().join("invocation"); + let selected = temp.path().join("selected"); + write_manifest(&invocation, "axum", "axum.toml", None); + write_manifest(&selected, "fastly", "fastly.toml", Some("printf selected")); + + let runtime = resolve_runtime_from( + "fastly", + Action::Build, + &invocation, + Some(OsString::from("../selected/edgezero.toml")), + ) + .expect("resolve explicit manifest"); + let expected_path = + fs::canonicalize(selected.join("edgezero.toml")).expect("canonical selected"); + assert_eq!(runtime.adapter_name(), "fastly"); + assert_eq!( + runtime.contract.as_ref().map(ResolvedManifest::path), + Some(expected_path.as_path()) + ); + assert!(matches!(runtime.target(), ResolvedAdapterTarget::Shell(_))); + } + + #[test] + fn runtime_resolver_rejects_cross_app_pair() { + let temp = TempDir::new().expect("temp dir"); + let selected = temp.path().join("selected"); + let other = temp.path().join("other"); + fs::create_dir_all(&selected).expect("selected root"); + fs::create_dir_all(&other).expect("other root"); + fs::write(other.join("fastly.toml"), "# other\n").expect("platform"); + fs::write(selected.join("Cargo.toml"), "[workspace]\n").expect("cargo"); + fs::write( + selected.join("edgezero.toml"), + "[app]\nname = \"fixture\"\n[adapters.fastly.adapter]\nmanifest = \"../other/fastly.toml\"\n", + ) + .expect("manifest"); + + let result = resolve_runtime_from( + "fastly", + Action::Build, + &selected, + Some(OsString::from("edgezero.toml")), + ); + assert!(result.is_err_and(|error| error.contains("outside application root"))); + } + + #[test] + fn runtime_resolver_rejects_equal_distance_ambiguity() { + let temp = TempDir::new().expect("temp dir"); + fs::write(temp.path().join("Cargo.toml"), "[workspace]\n").expect("workspace"); + write_manifest(&temp.path().join("left"), "axum", "axum.toml", None); + write_manifest(&temp.path().join("right"), "axum", "axum.toml", None); + + let result = resolve_runtime_from("axum", Action::Build, temp.path(), None); + assert!(result.is_err_and(|error| error.contains("ambiguous application manifest"))); + } + + #[test] + fn runtime_resolver_rejects_operational_action() { + let temp = TempDir::new().expect("temp dir"); + let result = resolve_runtime_from("axum", Action::AuthStatus, temp.path(), None); + assert!(result.is_err_and(|error| error.contains("operational action"))); + } +} diff --git a/crates/edgezero-cli/src/templates/cli/src/main.rs.hbs b/crates/edgezero-cli/src/templates/cli/src/main.rs.hbs index 5efa10fce..4f74d48ff 100644 --- a/crates/edgezero-cli/src/templates/cli/src/main.rs.hbs +++ b/crates/edgezero-cli/src/templates/cli/src/main.rs.hbs @@ -20,7 +20,11 @@ use edgezero_cli::args::{ use {{proj_core_mod}}::config::{{NameUpperCamel}}Config; #[derive(Parser, Debug)] -#[command(name = "{{proj_cli}}", version, about = "{{name}} edge CLI")] +#[command( + name = "{{proj_cli}}", + version, + about = "{{name}} edge CLI" +)] struct Args { #[command(subcommand)] cmd: Cmd, @@ -69,6 +73,12 @@ enum {{NameUpperCamel}}ConfigCmd { /// store and report changes. Exits 0 (no changes), 1 (changes with /// `--exit-code`), or 2 (unsupported / error). Diff(ConfigDiffArgs), + /// Reclaim orphaned chunk entries the config store leaked from prior + /// oversized pushes. Store-derived and untyped (no `{{NameUpperCamel}}Config`). + /// A dry-run by default; deletes only with `--yes` + an explicit + /// `--older-than` (YOUR assertion that nothing superseded within that + /// window is still being served, and no push is running). + Gc(ConfigGcArgs), /// Push `{{name}}.toml` as a single blob envelope to the /// adapter's config store. The blob carries every field verbatim /// (Model A -- `#[secret]` fields store the key NAME, @@ -78,12 +88,6 @@ enum {{NameUpperCamel}}ConfigCmd { /// Validate `edgezero.toml` and `{{name}}.toml` against the /// typed `{{NameUpperCamel}}Config` contract. Validate(ConfigValidateArgs), - /// Reclaim orphaned chunk entries the config store leaked from prior - /// oversized pushes. Store-derived and untyped (no `{{NameUpperCamel}}Config`). - /// A dry-run by default; deletes only with `--yes` + an explicit - /// `--older-than` (YOUR assertion that nothing superseded within that - /// window is still being served, and no push is running). - Gc(ConfigGcArgs), } fn main() { @@ -105,15 +109,15 @@ fn main() { Err(err) => Err(err), } } + // `gc` inspects the STORE, not the typed config, so it is not + // parameterised over `{{NameUpperCamel}}Config`. + Cmd::Config({{NameUpperCamel}}ConfigCmd::Gc(args)) => edgezero_cli::run_config_gc(&args), Cmd::Config({{NameUpperCamel}}ConfigCmd::Push(args)) => { edgezero_cli::run_config_push_typed::<{{NameUpperCamel}}Config>(&args) } Cmd::Config({{NameUpperCamel}}ConfigCmd::Validate(args)) => { edgezero_cli::run_config_validate_typed::<{{NameUpperCamel}}Config>(&args) } - // `gc` inspects the STORE, not the typed config, so it is not - // parameterised over `{{NameUpperCamel}}Config`. - Cmd::Config({{NameUpperCamel}}ConfigCmd::Gc(args)) => edgezero_cli::run_config_gc(&args), Cmd::Deploy(args) => edgezero_cli::run_deploy(&args), Cmd::Healthcheck(args) => edgezero_cli::run_healthcheck(&args), Cmd::New(args) => edgezero_cli::run_new(&args), diff --git a/crates/edgezero-cli/src/templates/core/Cargo.toml.hbs b/crates/edgezero-cli/src/templates/core/Cargo.toml.hbs index 28b8b0c2c..e17a942e3 100644 --- a/crates/edgezero-cli/src/templates/core/Cargo.toml.hbs +++ b/crates/edgezero-cli/src/templates/core/Cargo.toml.hbs @@ -22,7 +22,5 @@ validator = { workspace = true } [dev-dependencies] async-trait = "0.1" -# `test-utils` exposes `edgezero_core::test_env`, the shared RAII env guards the -# handler tests below use to override `API_BASE_URL` without racing each other. -edgezero-core = { workspace = true, features = ["test-utils"] } +edgezero-core = { workspace = true } serde_json = "1" diff --git a/crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs b/crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs index d9a154920..f3cb18935 100644 --- a/crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs +++ b/crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs @@ -1,16 +1,44 @@ use bytes::Bytes; +use edgezero_core::ResponseEgressDeadline; use edgezero_core::action; use edgezero_core::body::Body; use edgezero_core::context::RequestContext; use edgezero_core::error::EdgeError; use edgezero_core::extractor::{Headers, Json, Path}; -use edgezero_core::http::{self, Response, StatusCode, Uri}; -use edgezero_core::proxy::ProxyRequest; +use edgezero_core::http::{self, Method, Response, StatusCode, Uri}; +use edgezero_core::outbound::{ + OutboundBatchTermination, OutboundCachePolicy, OutboundCollectedSlot, OutboundFollowFailure, + OutboundImageFormat, OutboundImageOptimizerOptions, OutboundImageOptimizerRegion, + OutboundRedirectAction, OutboundRedirectLimit, OutboundRequest, ResponseDecoding, +}; use edgezero_core::response::Text; use futures::{StreamExt as _, stream}; -use std::env; +use std::io::Error as IoError; +use std::num::NonZeroU64; +use std::time::Duration; const DEFAULT_PROXY_BASE: &str = "https://httpbin.org"; +const MAX_DECODER_BYTES: u64 = 0x0200_0000; +const MAX_BROTLI_WINDOW_BITS: u8 = 24; +const MAX_DECODED_RESPONSE_BYTES: u64 = 0x0010_0000; +const MAX_ENCODED_RESPONSE_BYTES: u64 = 0x0020_0000; +const MAX_FANOUT_INPUT_BYTES: usize = 0x4000; +const MAX_FANOUT_REQUESTS: usize = 8; +const MAX_FINAL_RESPONSE_BYTES: u64 = 0x0020_0000; +const MAX_OUTBOUND_REQUEST_BODY_BYTES: u64 = 0x0010_0000; +const MAX_RESPONSE_CHUNK_BYTES: u64 = 0x0001_0000; +const MAX_RESPONSE_HEADER_BYTES: u64 = 0x0001_0000; +const MAX_RESPONSE_HEADER_COUNT: u64 = 100; +const OUTBOUND_BATCH_BUDGET: Duration = Duration::from_secs(5); +const OUTBOUND_FOLLOW_BUDGET: Duration = Duration::from_secs(5); +const OUTBOUND_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); +const RESPONSE_EGRESS_BUDGET: Duration = Duration::from_secs(2); + +#[derive(serde::Serialize)] +struct AdmissionView { + grant_consumed_once: bool, + route_class: Option, +} #[derive(serde::Deserialize)] pub struct EchoBody { @@ -22,6 +50,26 @@ pub struct EchoParams { pub name: String, } +#[derive(serde::Deserialize)] +struct FanoutRequest { + paths: Vec, +} + +#[derive(serde::Serialize)] +struct FanoutSlot { + elapsed_ms: Option, + index: usize, + outcome: FanoutSlotOutcome, +} + +#[derive(serde::Serialize)] +#[serde(rename_all = "snake_case", tag = "kind")] +enum FanoutSlotOutcome { + Error { category: &'static str, status: u16 }, + Response { status: u16 }, + Unresolved, +} + #[derive(serde::Deserialize)] struct ProxyPath { #[serde(default)] @@ -65,23 +113,172 @@ pub async fn echo_json(Json(body): Json) -> Text { Text::new(format!("Hello, {}!", body.name)) } +#[action] +pub async fn admission(RequestContext(ctx): RequestContext) -> Result { + let route_class = ctx + .route_metadata() + .and_then(|metadata| metadata.class()) + .map(str::to_owned); + let grant_consumed_once = + ctx.take_ingress_grant().is_some() && ctx.take_ingress_grant().is_none(); + + json_response(&AdmissionView { + grant_consumed_once, + route_class, + }) +} + +#[action] +pub async fn image_demo(RequestContext(ctx): RequestContext) -> Result { + let Some(client) = ctx.http_client() else { + return proxy_not_available_response(); + }; + client + .with_proxy_marker(false) + .send(image_demo_request()?) + .await? + .into_response() +} + +fn image_demo_request() -> Result { + let options = OutboundImageOptimizerOptions::new() + .region(OutboundImageOptimizerRegion::UsEast) + .format(OutboundImageFormat::WebP) + .width(100)?; + Ok( + outbound_policy(OutboundRequest::get("https://httpbin.org/image/jpeg")?) + .cache_policy(OutboundCachePolicy::PlatformDefault) + .image_optimizer(options) + .stream_response(), + ) +} + +#[action] +pub async fn fanout(RequestContext(ctx): RequestContext) -> Result { + let Some(client) = ctx.http_client() else { + return proxy_not_available_response(); + }; + let input: FanoutRequest = ctx.json_within(MAX_FANOUT_INPUT_BYTES).await?; + if input.paths.len() > MAX_FANOUT_REQUESTS { + return Err(EdgeError::validation(format!( + "fanout accepts at most {MAX_FANOUT_REQUESTS} paths" + ))); + } + + let base = ctx + .variable("API_BASE_URL") + .unwrap_or_else(|| DEFAULT_PROXY_BASE.to_owned()); + let deadline = ctx.monotonic_clock().deadline_after(OUTBOUND_BATCH_BUDGET); + let source_uri = Uri::from_static("/fanout"); + let requests = input + .paths + .into_iter() + .map(|path| { + let target = build_proxy_target(&base, &path, &source_uri)?; + Ok(outbound_policy(OutboundRequest::new(Method::GET, target)?).deadline(deadline)) + }) + .collect::, EdgeError>>()?; + let results = client + .send_all_until(requests, deadline) + .await + .map_err(|failure| failure.into_parts().0)?; + let (slots, termination) = results.into_parts(); + let output = slots + .into_iter() + .enumerate() + .map(|(index, slot)| fanout_slot(index, slot, termination)) + .collect::, EdgeError>>()?; + + let mut response = json_response(&output)?; + response + .extensions_mut() + .insert(ResponseEgressDeadline::after(RESPONSE_EGRESS_BUDGET)); + Ok(response) +} + +#[action] +pub async fn follow_demo(RequestContext(ctx): RequestContext) -> Result { + let Some(client) = ctx.http_client() else { + return proxy_not_available_response(); + }; + let base = ctx + .variable("API_BASE_URL") + .unwrap_or_else(|| DEFAULT_PROXY_BASE.to_owned()); + let allowed = base + .parse::() + .map_err(|_error| EdgeError::bad_request("invalid follow base URI"))?; + let target = build_proxy_target(&base, "redirect/1", &Uri::from_static("/follow"))?; + if !follow_target_allowed(&allowed, &target) { + return follow_denied_response(); + } + let deadline = ctx.monotonic_clock().deadline_after(OUTBOUND_FOLLOW_BUDGET); + let request = outbound_policy(OutboundRequest::new(Method::GET, target)?).deadline(deadline); + let allowed_origin = request.uri().clone(); + let followed = client + .follow(request, OutboundRedirectLimit::new(4)?, move |redirect| { + if redirect + .prepared_request() + .is_some_and(|next| follow_target_allowed(&allowed_origin, next.uri())) + { + Ok(OutboundRedirectAction::FollowPrepared) + } else { + Err(EdgeError::bad_request("redirect target is not allowed")) + } + }) + .await; + let result = match followed { + Ok(result) => result, + Err(OutboundFollowFailure::Hop { error, .. }) => return Err(error), + Err(OutboundFollowFailure::Policy { .. }) => return follow_denied_response(), + }; + let (upstream, _, redirects_followed, _) = result.into_parts(); + let mut response = upstream.into_response()?; + response.headers_mut().insert( + "x-follow-redirects", + http::HeaderValue::from(redirects_followed), + ); + Ok(response) +} + +fn follow_denied_response() -> Result { + http::response_builder() + .status(StatusCode::FORBIDDEN) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::text("redirect target is not allowed\n")) + .map_err(EdgeError::internal) +} + +fn follow_target_allowed(allowed: &Uri, target: &Uri) -> bool { + matches!(allowed.scheme_str(), Some("http" | "https")) + && allowed + .authority() + .is_some_and(|authority| !authority.as_str().contains('@')) + && target.scheme() == allowed.scheme() + && target.authority() == allowed.authority() +} + #[action] pub async fn proxy_demo(RequestContext(ctx): RequestContext) -> Result { let params: ProxyPath = ctx.path()?; - let proxy_handle = ctx.proxy_handle(); - let request = ctx.into_request(); - let target = build_proxy_target(¶ms.rest, request.uri())?; - let proxy_request = ProxyRequest::from_request(request, target); + let http_client = ctx + .http_client() + .map(|client| client.with_proxy_marker(false)); + let base = ctx + .variable("API_BASE_URL") + .unwrap_or_else(|| DEFAULT_PROXY_BASE.to_owned()); + let request = ctx.into_request()?; + let target = build_proxy_target(&base, ¶ms.rest, request.uri())?; + let outbound_request = outbound_policy(OutboundRequest::from_request(request, target)?) + .response_decoding(ResponseDecoding::Original); - if let Some(handle) = proxy_handle { - handle.forward(proxy_request).await + if let Some(client) = http_client { + client.send(outbound_request).await?.into_response() } else { proxy_not_available_response() } } -fn build_proxy_target(rest: &str, original_uri: &Uri) -> Result { - let base = env::var("API_BASE_URL").unwrap_or_else(|_| DEFAULT_PROXY_BASE.to_owned()); +fn build_proxy_target(base: &str, rest: &str, original_uri: &Uri) -> Result { let mut target = base.trim_end_matches('/').to_owned(); let trimmed_rest = rest.trim_start_matches('/'); if !trimmed_rest.is_empty() { @@ -101,10 +298,77 @@ fn build_proxy_target(rest: &str, original_uri: &Uri) -> Result .map_err(|err| EdgeError::bad_request(format!("invalid proxy target URI: {err}"))) } +fn error_category(status: StatusCode) -> &'static str { + match status { + StatusCode::BAD_REQUEST => "bad_request", + StatusCode::BAD_GATEWAY => "bad_gateway", + StatusCode::GATEWAY_TIMEOUT => "gateway_timeout", + StatusCode::REQUEST_TIMEOUT => "request_timeout", + StatusCode::UNPROCESSABLE_ENTITY => "validation", + _ => "error", + } +} + +fn fanout_slot( + index: usize, + slot_result: OutboundCollectedSlot, + termination: OutboundBatchTermination, +) -> Result { + match slot_result { + OutboundCollectedSlot::Resolved(slot) => { + let outcome = match slot.outcome { + Ok(response) => FanoutSlotOutcome::Response { + status: response.status().as_u16(), + }, + Err(error) => FanoutSlotOutcome::Error { + category: error_category(error.status()), + status: error.status().as_u16(), + }, + }; + Ok(FanoutSlot { + elapsed_ms: Some(slot.elapsed.as_millis()), + index, + outcome, + }) + } + OutboundCollectedSlot::Unresolved if termination == OutboundBatchTermination::Cutoff => { + Ok(FanoutSlot { + elapsed_ms: None, + index, + outcome: FanoutSlotOutcome::Unresolved, + }) + } + OutboundCollectedSlot::Unresolved if termination == OutboundBatchTermination::Completed => { + Err(EdgeError::internal(IoError::other( + "completed outbound batch contained an unresolved slot", + ))) + } + OutboundCollectedSlot::AlreadyDelivered => Err(EdgeError::internal(IoError::other( + "fanout collection unexpectedly contained an already-delivered slot", + ))), + OutboundCollectedSlot::Unresolved => Err(EdgeError::internal(IoError::other( + "outbound batch contained an unresolved slot for an unsupported termination reason", + ))), + } +} + +fn outbound_policy(request: OutboundRequest) -> OutboundRequest { + request + .cache_policy(OutboundCachePolicy::Bypass) + .max_decoder_bytes(MAX_DECODER_BYTES) + .max_brotli_window_bits(MAX_BROTLI_WINDOW_BITS) + .max_decoded_response_bytes(MAX_DECODED_RESPONSE_BYTES) + .max_encoded_response_bytes(MAX_ENCODED_RESPONSE_BYTES) + .max_request_body_bytes(MAX_OUTBOUND_REQUEST_BODY_BYTES) + .max_response_bytes(MAX_FINAL_RESPONSE_BYTES) + .max_chunk_bytes(NonZeroU64::new(MAX_RESPONSE_CHUNK_BYTES).unwrap_or(NonZeroU64::MIN)) + .max_response_header_bytes(MAX_RESPONSE_HEADER_BYTES) + .max_response_header_count(MAX_RESPONSE_HEADER_COUNT) + .timeout(OUTBOUND_REQUEST_TIMEOUT) +} + fn proxy_not_available_response() -> Result { - let body = Body::text( - "proxy example is not enabled for this adapter build; enable a proxy-capable adapter", - ); + let body = Body::text("outbound HTTP is not enabled for this adapter build"); http::response_builder() .status(StatusCode::NOT_IMPLEMENTED) .header("content-type", "text/plain; charset=utf-8") @@ -112,6 +376,18 @@ fn proxy_not_available_response() -> Result { .map_err(EdgeError::internal) } +fn json_response(value: &T) -> Result +where + T: serde::Serialize, +{ + let body = Body::json(value).map_err(EdgeError::internal)?; + http::response_builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .body(body) + .map_err(EdgeError::internal) +} + // `cfg.api_token` is the RESOLVED secret value (not the // key name). The framework's secret walk populated it at // extract time; the handler uses it directly. No @@ -132,29 +408,501 @@ fn proxy_not_available_response() -> Result { #[cfg(test)] mod tests { use super::*; - use async_trait::async_trait; + use edgezero_core::app::App as EdgeZeroApp; use edgezero_core::body::Body; - use edgezero_core::context::RequestContext; + use edgezero_core::context::{RequestContext, RuntimeVariables}; use edgezero_core::http::header::{HeaderName, HeaderValue}; - use edgezero_core::http::{Method, StatusCode, Uri, request_builder}; + use edgezero_core::http::{HeaderMap, Method, StatusCode, Uri, request_builder}; + use edgezero_core::outbound::{ + HttpClient, OutboundBatch, OutboundBatchCheck, OutboundDispatchRequest, + OutboundExchangeOwner, OutboundFuture, OutboundHead, OutboundHeaderFidelity, + OutboundHeaderInput, OutboundHttpClient, OutboundPending, OutboundPreparedBatch, + OutboundProxyMarker, OutboundRequestParts, OutboundResponse, OutboundResponseContext, + OutboundSendMode, OutboundTime, ResponseMode, + }; use edgezero_core::params::PathParams; - use edgezero_core::proxy::{ProxyClient, ProxyHandle, ProxyResponse}; use edgezero_core::response::IntoResponse as _; - use edgezero_core::test_env::{EnvOverride, env_lock}; + use edgezero_core::response_egress_framing::ResponseEgressFramingMode; + use edgezero_core::{ + BodyPayload, BudgetSource, Deadline, IndependentOutboundProgress, MonotonicClock, + MonotonicInstant, OutboundBatchDriverEvent, OutboundProgressRuntime, + }; + use futures::FutureExt as _; use futures::executor::block_on; - use std::collections::HashMap; + use futures::future::ready; + use std::collections::{BTreeMap, HashMap}; + use std::future::Future; + use std::pin::Pin; + use std::rc::Rc; + use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll, Waker}; + use std::time::Duration; + + #[derive(Clone)] + struct TestTime { + state: Arc>, + } + + struct TestTimeState { + next_timer: usize, + now: MonotonicInstant, + waiters: BTreeMap, + } + + struct TestTimer { + clock: MonotonicClock, + deadline: Deadline, + id: usize, + state: Arc>, + } + + impl Default for TestTime { + fn default() -> Self { + Self { + state: Arc::new(Mutex::new(TestTimeState { + next_timer: 0, + now: MonotonicInstant::now(), + waiters: BTreeMap::new(), + })), + } + } + } + + impl TestTime { + fn advance_to(&self, now: MonotonicInstant) { + let wakers = { + let mut state = self.state.lock().expect("fake time"); + state.now = now; + state.waiters.values().cloned().collect::>() + }; + for waker in wakers { + waker.wake(); + } + } - struct TestProxyClient; + fn clock(&self) -> MonotonicClock { + let state = Arc::clone(&self.state); + MonotonicClock::new(move || state.lock().expect("clock").now) + } + + fn pair(&self) -> OutboundTime { + let state = Arc::clone(&self.state); + OutboundTime::new(self.clock(), move |deadline, clock| { + let id = { + let mut registration = state.lock().expect("timer registration"); + let id = registration.next_timer; + registration.next_timer = + registration.next_timer.checked_add(1).expect("timer id"); + id + }; + Box::pin(TestTimer { + clock, + deadline, + id, + state: Arc::clone(&state), + }) + }) + } + } + + impl Future for TestTimer { + type Output = Result<(), EdgeError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.deadline.is_expired_at(self.clock.now()) { + Poll::Ready(Ok(())) + } else { + self.state + .lock() + .expect("timer waiter") + .waiters + .insert(self.id, cx.waker().clone()); + Poll::Pending + } + } + } - #[async_trait(?Send)] - impl ProxyClient for TestProxyClient { - async fn send(&self, request: ProxyRequest) -> Result { - let (_method, uri, _headers, _body, _) = request.into_parts(); - assert!(uri.to_string().contains("status/201")); - Ok(ProxyResponse::new(StatusCode::CREATED, Body::empty())) + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for TestTimer { + fn drop(&mut self) { + self.state + .lock() + .expect("timer cancellation") + .waiters + .remove(&self.id); + } + } + + #[derive(Clone)] + struct TestOutboundClient { + clock: TestTime, + follow_requests: Arc>>, + redirect_location: Option<&'static str>, + time: OutboundTime, + } + + impl Default for TestOutboundClient { + fn default() -> Self { + let clock = TestTime::default(); + Self { + time: clock.pair(), + clock, + follow_requests: Arc::default(), + redirect_location: None, + } + } + } + + impl TestOutboundClient { + fn finish_ready( + &self, + request: OutboundDispatchRequest, + expected_deadline: Option, + ) -> Result { + let mut future = + Box::pin(async move { self.head(request, expected_deadline)?.finish().await }); + // Fixed replies have at most one chunk; the finalizer yields once before EOF. + for _ in 0_u8..2 { + if let Some(outcome) = future.as_mut().now_or_never() { + return outcome; + } + } + Err(EdgeError::internal(IoError::other( + "scripted response exceeded its fixed poll bound", + ))) + } + + #[expect( + clippy::panic_in_result_fn, + reason = "scripted transport assertions preserve effective request checks" + )] + fn head( + &self, + request: OutboundDispatchRequest, + expected_deadline: Option, + ) -> Result { + let (parts, context) = request.into_parts(); + assert_outbound_policy(&parts); + if context.mode() == OutboundSendMode::Follow { + assert_eq!(parts.method, Method::GET); + assert_eq!(parts.response_decoding, ResponseDecoding::Decode); + assert_eq!(parts.proxy_marker, OutboundProxyMarker::On); + let deadline = parts.deadline.ok_or_else(|| { + EdgeError::internal(IoError::other("missing scripted chain deadline")) + })?; + self.follow_requests + .lock() + .map_err(|_error| { + EdgeError::internal(IoError::other("scripted request log poisoned")) + })? + .push((parts.uri.clone(), deadline)); + } else if context.mode() == OutboundSendMode::Batch { + assert_eq!(parts.response_decoding, ResponseDecoding::Decode); + assert_eq!(parts.proxy_marker, OutboundProxyMarker::On); + let deadline = parts.deadline.expect("batch deadline"); + if let Some(expected) = expected_deadline { + assert_eq!(deadline.instant(), expected.instant()); + } + } else { + assert_eq!(parts.method, Method::GET); + assert!(parts.uri.to_string().contains("status/201")); + assert!(parts.deadline.is_none()); + assert_eq!(parts.response_decoding, ResponseDecoding::Original); + assert_eq!(parts.proxy_marker, OutboundProxyMarker::Off); + } + response_for(&parts, context, self.redirect_location) + } + } + + impl OutboundHttpClient for TestOutboundClient { + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let outcome = self.finish_ready(request, None); + Ok(OutboundPending::new( + Box::pin(ready(outcome)), + OutboundExchangeOwner::new(|| {}), + )) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + Box::pin(ready(self.head(request, None))) + } + + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let started_at = batch.started_at(); + let observation_cutoff = batch.observation_cutoff(); + let (context, requests) = batch.into_slots(); + if observation_cutoff.is_expired_at(context.clock().now()) { + return OutboundBatch::cutoff(context); + } + let mut results = Vec::with_capacity(requests.len()); + let mut cutoff_unresolved = false; + for (index, request) in requests { + let budget = request.budget(); + let elapsed = Duration::from_millis( + u64::try_from(index).expect("slot index").saturating_add(1), + ); + self.clock + .advance_to(started_at.checked_add(elapsed).expect("terminal instant")); + let outcome = self.finish_ready(request, Some(observation_cutoff)); + let completed_at = self.time.clock().now(); + cutoff_unresolved |= observation_cutoff.is_expired_at(completed_at) + && budget.deadline.instant() >= observation_cutoff.instant(); + results.push(OutboundBatchDriverEvent::Item { + index, + completed_at, + outcome, + }); + } + if cutoff_unresolved { + results.push(OutboundBatchDriverEvent::Cutoff); + } + OutboundBatch::from_driver( + context, + stream::iter(results), + OutboundExchangeOwner::new(|| {}), + ) + } + + fn time(&self) -> OutboundTime { + self.time.clone() + } + + fn validate_supported_options( + &self, + _request: &OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + Ok(()) + } + } + + fn assert_outbound_policy(parts: &OutboundRequestParts) { + assert_eq!(parts.cache_policy, OutboundCachePolicy::Bypass); + assert_eq!(parts.timeout, Some(OUTBOUND_REQUEST_TIMEOUT)); + assert_eq!( + parts.max_request_body_bytes, + MAX_OUTBOUND_REQUEST_BODY_BYTES + ); + assert_eq!( + parts.max_encoded_response_bytes, + Some(MAX_ENCODED_RESPONSE_BYTES) + ); + assert_eq!( + parts.max_decoded_response_bytes, + Some(MAX_DECODED_RESPONSE_BYTES) + ); + assert_eq!( + parts.response_mode, + ResponseMode::Buffered { + max_bytes: MAX_FINAL_RESPONSE_BYTES, + } + ); + assert_eq!( + parts.max_response_header_bytes, + Some(MAX_RESPONSE_HEADER_BYTES) + ); + assert_eq!( + parts.max_response_header_count, + Some(MAX_RESPONSE_HEADER_COUNT) + ); + assert_eq!( + parts.max_chunk_bytes, + Some(NonZeroU64::new(MAX_RESPONSE_CHUNK_BYTES).unwrap_or(NonZeroU64::MIN)) + ); + assert_eq!(parts.max_brotli_window_bits, MAX_BROTLI_WINDOW_BITS); + assert_eq!(parts.max_decoder_bytes, MAX_DECODER_BYTES); + } + + fn response_for( + parts: &OutboundRequestParts, + context: OutboundResponseContext, + redirect_location: Option<&'static str>, + ) -> Result { + if parts.uri.path() == "/fail" { + return Err(EdgeError::gateway_timeout_caused( + "provider URL https://user:token@example.invalid", + BudgetSource::RequestDeadline, + )); + } + let status = match parts.uri.path() { + "/redirect/1" => StatusCode::FOUND, + "/status/201" => StatusCode::CREATED, + "/status/204" => StatusCode::NO_CONTENT, + _ => StatusCode::OK, + }; + let mut headers = HeaderMap::new(); + headers.insert("x-outbound-test", HeaderValue::from_static("preserved")); + if status == StatusCode::FOUND { + headers.insert( + "location", + HeaderValue::from_static(redirect_location.expect("scripted redirect target")), + ); + } + let encoded = stream::empty().boxed_local(); + OutboundHead::new( + status, + OutboundHeaderInput::new( + "scripted", + headers, + OutboundHeaderFidelity::AdapterVisibleFields, + ), + encoded, + context, + OutboundExchangeOwner::new(|| {}), + ) + } + + #[test] + fn follow_demo_uses_supplied_head_client_and_app_deadline() { + let scripted = TestOutboundClient { + redirect_location: Some("/status/201"), + ..TestOutboundClient::default() + }; + let start = scripted.time.clock().now(); + let requests = Arc::clone(&scripted.follow_requests); + let response = run_follow_demo(scripted, "https://allowed.example"); + assert_eq!(response.status(), StatusCode::CREATED); + assert_eq!(response.headers()["x-follow-redirects"], "1"); + assert_eq!(response.headers()["x-outbound-test"], "preserved"); + let recorded = requests.lock().expect("recorded requests"); + let expected_deadline = start.checked_add(Duration::from_secs(5)).expect("deadline"); + assert_eq!(recorded.len(), 2); + for (request, path) in recorded.iter().zip(["/redirect/1", "/status/201"]) { + assert_eq!(request.0.scheme_str(), Some("https")); + assert_eq!( + request.0.authority().expect("authority").as_str(), + "allowed.example" + ); + assert_eq!(request.0.path(), path); + assert_eq!(request.1.instant(), expected_deadline); + } + } + + #[test] + fn follow_demo_allows_canonical_equivalent_origins() { + for base in [ + "https://example.com:443", + "https://ExAmPlE.CoM", + "https://ExAmPlE.CoM:443", + ] { + let scripted = TestOutboundClient { + redirect_location: Some("/status/201"), + ..TestOutboundClient::default() + }; + let requests = Arc::clone(&scripted.follow_requests); + let response = run_follow_demo(scripted, base); + assert_eq!(response.status(), StatusCode::CREATED); + assert_eq!(response.headers()["x-follow-redirects"], "1"); + let recorded = requests.lock().expect("recorded requests"); + assert_eq!(recorded.len(), 2); + for (request, path) in recorded.iter().zip(["/redirect/1", "/status/201"]) { + assert_eq!(request.0.scheme_str(), Some("https")); + assert_eq!( + request.0.authority().expect("authority").as_str(), + "example.com" + ); + assert_eq!(request.0.path(), path); + } } } + #[test] + fn follow_demo_checks_initial_target_before_dispatch() { + for base in [ + "ftp://allowed.example", + "https://user:secret@allowed.example", + ] { + let scripted = TestOutboundClient::default(); + let requests = Arc::clone(&scripted.follow_requests); + let response = run_follow_demo(scripted, base); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.into_body().into_bytes().expect("buffered"), + Bytes::from_static(b"redirect target is not allowed\n") + ); + assert!(requests.lock().expect("recorded requests").is_empty()); + } + } + + #[test] + fn follow_demo_stops_after_four_redirects() { + let scripted = TestOutboundClient { + redirect_location: Some("/redirect/1"), + ..TestOutboundClient::default() + }; + let requests = Arc::clone(&scripted.follow_requests); + let response = run_follow_demo(scripted, "https://allowed.example"); + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(response.headers()["location"], "/redirect/1"); + assert_eq!(response.headers()["x-follow-redirects"], "4"); + assert_eq!(requests.lock().expect("recorded requests").len(), 5); + } + + #[test] + fn follow_demo_denies_changed_origin_without_sending_another_hop() { + for base in ["https://allowed.example", "https://AlLoWeD.ExAmPlE:443"] { + for target in [ + "https://other.example/private?token=secret", + "http://allowed.example/private?token=secret", + "https://allowed.example:8443/private?token=secret", + "mailto:user@example.invalid", + ] { + let scripted = TestOutboundClient { + redirect_location: Some(target), + ..TestOutboundClient::default() + }; + let requests = Arc::clone(&scripted.follow_requests); + let response = run_follow_demo(scripted, base); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.into_body().into_bytes().expect("buffered"), + Bytes::from_static(b"redirect target is not allowed\n") + ); + assert_eq!(requests.lock().expect("recorded requests").len(), 1); + } + } + } + + fn run_follow_demo(scripted: TestOutboundClient, base: &str) -> Response { + let mut app = EdgeZeroApp::new(crate::build_router().expect("generated routes")); + app.set_monotonic_clock(scripted.time.clock().clone()); + let mut request = request_builder() + .uri("/follow") + .body(Body::empty()) + .expect("request"); + request + .extensions_mut() + .insert(HttpClient::with_client(scripted)); + request + .extensions_mut() + .insert(RuntimeVariables::from_vars([( + "API_BASE_URL".to_owned(), + base.to_owned(), + )])); + let outcome = block_on(app.dispatch_ingress( + request, + app.monotonic_now(), + edgezero_core::IngressHeadAccounting::HostManaged, + edgezero_core::IngressFraming::HostManaged, + )) + .expect("ingress dispatch"); + let edgezero_core::IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("response outcome"); + }; + let Ok((prepared, _, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("response egress"); + }; + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + prepared.into_response() + } + #[test] fn root_returns_static_body() { let ctx = empty_context("/"); @@ -199,7 +947,10 @@ mod tests { let response = block_on(stream(ctx)).expect("handler ok"); assert_eq!(response.status(), StatusCode::OK); - let mut chunks = response.into_body().into_stream().expect("stream body"); + let (payload, body_owner) = response.into_body().into_parts(); + let BodyPayload::Stream(mut chunks) = payload else { + panic!("stream body"); + }; let collected = block_on(async { let mut buf = Vec::new(); while let Some(item) = chunks.next().await { @@ -212,11 +963,12 @@ mod tests { String::from_utf8(collected).expect("utf8"), "chunk 0\nchunk 1\nchunk 2\n" ); + drop(body_owner); } #[test] fn echo_json_formats_payload() { - let ctx = context_with_json("/echo", r#"{"name":"Edge"}"#); + let ctx = context_with_json("/echo", &serde_json::json!({"name": "Edge"}).to_string()); let response = block_on(echo_json(ctx)) .expect("handler ok") .into_response() @@ -227,28 +979,237 @@ mod tests { #[test] fn build_proxy_target_merges_segments_and_query() { - let _lock = env_lock().lock().expect("env lock"); - let _env = EnvOverride::set("API_BASE_URL", "https://example.com/api"); let original = Uri::from_static("/proxy/status?foo=bar"); - let target = build_proxy_target("status/200", &original).expect("target uri"); + let target = build_proxy_target("https://example.com/api", "status/200", &original) + .expect("target uri"); assert_eq!( target.to_string(), "https://example.com/api/status/200?foo=bar" ); } + #[test] + fn fanout_rejects_delivered_and_completed_unresolved_slots() { + for slot in [ + OutboundCollectedSlot::AlreadyDelivered, + OutboundCollectedSlot::Unresolved, + ] { + let error = fanout_slot(0, slot, OutboundBatchTermination::Completed) + .err() + .expect("invalid collection state"); + assert_eq!(error.status(), StatusCode::INTERNAL_SERVER_ERROR); + } + let slot = fanout_slot( + 0, + OutboundCollectedSlot::Unresolved, + OutboundBatchTermination::Cutoff, + ) + .expect("cutoff slot"); + assert!(matches!(slot.outcome, FanoutSlotOutcome::Unresolved)); + } + + #[test] + fn scripted_batch_cutoff_retains_only_earlier_completions() { + let scripted = TestOutboundClient::default(); + let time = scripted.time.clone(); + let observation_cutoff = time.clock().deadline_after(Duration::from_millis(2)); + let requests = ["status/200", "status/204"].map(|path| { + outbound_policy( + OutboundRequest::get(format!("https://example.com/{path}")).expect("request"), + ) + .deadline(observation_cutoff) + }); + let batch = HttpClient::with_client(scripted) + .start_batch_until(requests.into(), observation_cutoff); + let results = block_on(batch.collect()).expect("cutoff must not become a driver failure"); + let (collected, termination) = results.into_parts(); + assert_eq!(termination, OutboundBatchTermination::Cutoff); + assert_eq!(collected.len(), 2); + let mut slots = collected.into_iter(); + let Some(OutboundCollectedSlot::Resolved(first)) = slots.next() else { + panic!("first response completed before cutoff"); + }; + assert_eq!(first.elapsed, Duration::from_millis(1)); + assert_eq!( + first.outcome.as_ref().expect("first response").status(), + StatusCode::OK + ); + assert!(matches!( + slots.next(), + Some(OutboundCollectedSlot::Unresolved) + )); + assert!(slots.next().is_none()); + assert_eq!(time.clock().now(), observation_cutoff.instant()); + } + + #[test] + fn scripted_batch_expired_cutoff_keeps_preflight_outcomes() { + for invalid_sibling in [false, true] { + let scripted = TestOutboundClient::default(); + let time = scripted.time.clone(); + let entered_at = time.clock().now(); + let observation_cutoff = Deadline::at_instant(entered_at); + let first = outbound_policy( + OutboundRequest::get("https://example.com/status/200").expect("request"), + ); + let mut second = outbound_policy( + OutboundRequest::get("https://example.com/status/204").expect("request"), + ); + if invalid_sibling { + second = second.stream_response(); + } + let batch = HttpClient::with_client(scripted) + .start_batch_until(vec![first, second], observation_cutoff); + let results = block_on(batch.collect()).expect("expired entry preserves its context"); + let (collected, termination) = results.into_parts(); + assert_eq!(termination, OutboundBatchTermination::Cutoff); + assert_eq!(collected.len(), 2); + assert!(matches!(collected[0], OutboundCollectedSlot::Unresolved)); + let last = collected.into_iter().nth(1).expect("second slot"); + if invalid_sibling { + let OutboundCollectedSlot::Resolved(rejected) = last else { + panic!("original preflight rejection must survive"); + }; + assert_eq!( + rejected + .outcome + .as_ref() + .expect_err("streamed batch response") + .status(), + StatusCode::BAD_REQUEST + ); + } else { + assert!(matches!(last, OutboundCollectedSlot::Unresolved)); + } + assert_eq!(time.clock().now(), entered_at, "no eligible scripted work"); + } + } + + #[test] + fn scripted_batch_preserves_receipts_through_mixed_collection() { + let scripted = TestOutboundClient::default(); + let observation_cutoff = scripted.time.clock().deadline_after(OUTBOUND_BATCH_BUDGET); + let client = HttpClient::with_client(scripted); + let requests = ["status/200", "status/204", "fail"].map(|path| { + outbound_policy( + OutboundRequest::get(format!("https://example.com/{path}")).expect("request"), + ) + .deadline(observation_cutoff) + }); + let mut batch = client.start_batch_until(requests.into(), observation_cutoff); + let mut accumulator = batch.accumulator(); + let OutboundBatchCheck::Item(item) = batch.check().expect("ready scripted item") else { + panic!("scripted work must already be complete"); + }; + assert_eq!(item.index(), 0); + accumulator.insert(item).expect("receipt belongs to batch"); + let result = block_on(batch.collect()).expect("remaining collection"); + assert!(matches!( + result.slots()[0], + OutboundCollectedSlot::AlreadyDelivered + )); + let merged = accumulator + .merge_success(result) + .expect("merge original receipt"); + assert_eq!(merged.termination(), OutboundBatchTermination::Completed); + assert!( + merged + .slots() + .iter() + .all(|slot| matches!(slot, OutboundCollectedSlot::Resolved(_))) + ); + } + + #[test] + fn fanout_reports_positional_elapsed_and_typed_outcomes() { + let ctx = fanout_context( + &serde_json::json!({"paths": ["status/200", "status/204", "fail"]}).to_string(), + ); + let response = block_on(fanout(ctx)).expect("fanout response"); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.headers()["content-type"], "application/json"); + assert!( + response + .extensions() + .get::() + .is_some() + ); + assert!( + !String::from_utf8_lossy(response.body().as_bytes().expect("buffered")) + .contains("token@example.invalid") + ); + + let payload: serde_json::Value = response.body().to_json().expect("json"); + assert_eq!(payload[0]["index"], 0_i64); + assert_eq!(payload[0]["elapsed_ms"], 1_i64); + assert_eq!(payload[0]["outcome"]["kind"], "response"); + assert_eq!(payload[0]["outcome"]["status"], 200_i64); + assert_eq!(payload[1]["index"], 1_i64); + assert_eq!(payload[1]["elapsed_ms"], 2_i64); + assert_eq!(payload[1]["outcome"]["status"], 204_i64); + assert_eq!(payload[2]["index"], 2_i64); + assert_eq!(payload[2]["elapsed_ms"], 3_i64); + assert_eq!(payload[2]["outcome"]["kind"], "error"); + assert_eq!(payload[2]["outcome"]["category"], "gateway_timeout"); + assert_eq!(payload[2]["outcome"]["status"], 504_i64); + } + + #[test] + fn fanout_with_empty_input_returns_an_empty_array() { + let ctx = fanout_context(&serde_json::json!({"paths": []}).to_string()); + let response = block_on(fanout(ctx)).expect("fanout response"); + let payload: serde_json::Value = response.body().to_json().expect("json"); + assert_eq!(payload, serde_json::json!([])); + } + + #[test] + fn fanout_rejects_more_than_eight_paths() { + let ctx = fanout_context( + &serde_json::json!({"paths": ["1", "2", "3", "4", "5", "6", "7", "8", "9"]}) + .to_string(), + ); + let error = block_on(fanout(ctx)).expect_err("oversized fanout must fail"); + assert_eq!(error.status(), StatusCode::UNPROCESSABLE_ENTITY); + } + #[test] fn proxy_demo_without_handle_returns_placeholder() { - let _lock = env_lock().lock().expect("env lock"); let ctx = context_with_params("/proxy/status/200", &[("rest", "status/200")]); let response = block_on(proxy_demo(ctx)).expect("response"); assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED); } #[test] - fn proxy_demo_uses_injected_handle() { - let _lock = env_lock().lock().expect("env lock"); + fn image_demo_uses_default_cache_and_preserves_outbound_limits() { + let request = image_demo_request().expect("native image example"); + let options = request + .image_optimizer_options() + .expect("optimizer options"); + assert_eq!( + options.region_setting(), + Some(&OutboundImageOptimizerRegion::UsEast) + ); + assert_eq!(options.width_setting(), Some(100)); + assert_eq!(options.format_setting(), Some(OutboundImageFormat::WebP)); + let parts = request.into_parts(); + assert_eq!(parts.cache_policy, OutboundCachePolicy::PlatformDefault); + assert_eq!(parts.response_mode, edgezero_core::ResponseMode::Streamed); + assert_eq!( + parts.max_encoded_response_bytes, + Some(MAX_ENCODED_RESPONSE_BYTES) + ); + assert_eq!( + parts.max_decoded_response_bytes, + Some(MAX_DECODED_RESPONSE_BYTES) + ); + assert_eq!( + parts.max_response_header_bytes, + Some(MAX_RESPONSE_HEADER_BYTES) + ); + } + #[test] + fn generated_outbound_http_smoke() { let mut request = request_builder() .method(Method::GET) .uri("/proxy/status/201") @@ -256,7 +1217,7 @@ mod tests { .expect("request"); request .extensions_mut() - .insert(ProxyHandle::with_client(TestProxyClient)); + .insert(HttpClient::with_client(TestOutboundClient::default())); let mut params = HashMap::new(); params.insert("rest".to_owned(), "status/201".to_owned()); @@ -266,6 +1227,26 @@ mod tests { assert_eq!(response.status(), StatusCode::CREATED); } + #[test] + fn generated_outbound_client_retains_its_clock_in_responses() { + let scripted = TestOutboundClient::default(); + let start = scripted.time.clock().now(); + let clock = scripted.clock.clone(); + let client = HttpClient::with_client(scripted).with_proxy_marker(false); + let request = outbound_policy( + OutboundRequest::get("https://example.com/status/201").expect("request"), + ) + .response_decoding(ResponseDecoding::Original); + let response = block_on(client.send(request)).expect("response"); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + clock.advance_to(deadline); + + let error = block_on(response.into_bytes_bounded_until(1, Deadline::at_instant(deadline))) + .expect_err("retained clock reached deadline"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + fn empty_context(path: &str) -> RequestContext { let request = request_builder() .method(Method::GET) @@ -275,6 +1256,18 @@ mod tests { RequestContext::new(request, PathParams::default()) } + fn fanout_context(json: &str) -> RequestContext { + let mut request = request_builder() + .method(Method::POST) + .uri("/fanout") + .body(Body::from(json)) + .expect("request"); + request + .extensions_mut() + .insert(HttpClient::with_client(TestOutboundClient::default())); + RequestContext::new(request, PathParams::default()) + } + fn context_with_params(path: &str, params: &[(&str, &str)]) -> RequestContext { let request = request_builder() .method(Method::GET) diff --git a/crates/edgezero-cli/src/templates/core/src/lib.rs.hbs b/crates/edgezero-cli/src/templates/core/src/lib.rs.hbs index dee67bfee..1c76e0dd3 100644 --- a/crates/edgezero-cli/src/templates/core/src/lib.rs.hbs +++ b/crates/edgezero-cli/src/templates/core/src/lib.rs.hbs @@ -1,4 +1,473 @@ pub mod config; mod handlers; -edgezero_core::app!("../../edgezero.toml"); +use std::error::Error; +use std::fmt; +use std::time::Duration; + +use edgezero_core::app::App as EdgeZeroApp; +use edgezero_core::http::{HeaderMap, HeaderValue, StatusCode}; +use edgezero_core::{ + AdmissionDecision, BufferedIngressResponse, ConfigExtractionLimits, EdgeError, IngressGrant, + MemoryCeiling, MemoryCeilingScope, MemoryEnvelope, MemoryEnvelopeValidation, + MemoryEnvelopeValidationError, MemoryResource, PlatformMetadata, ResponseEgressCompletion, + RouteResolution, +}; + +const DEFAULT_INGRESS_READ_BUDGET: Duration = Duration::from_secs(30); +const FALLBACK_INGRESS_BODY_BYTES: usize = 4 * 1024; +const FALLBACK_INGRESS_READ_BUDGET: Duration = Duration::from_secs(5); +const OUTBOUND_INGRESS_READ_BUDGET: Duration = Duration::from_secs(10); +const EXPECTED_FIXED_MEMORY_BYTES: u64 = 4 * 1024 * 1024; +const EXPECTED_PER_LIVE_REQUEST_BYTES: u64 = 1024 * 1024; +const EXPECTED_SEPARATE_STACK_BYTES: u64 = 256 * 1024; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PlatformMemoryStartupError { + Exceeds { + available_bytes: u64, + required_bytes: u64, + resource: MemoryResource, + }, + UnsupportedValidationOutcome, + Validation { + source: MemoryEnvelopeValidationError, + }, +} + +impl fmt::Display for PlatformMemoryStartupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Self::Exceeds { + available_bytes, + required_bytes, + resource, + } => { + let resource_label = match resource { + MemoryResource::Primary => "primary memory", + MemoryResource::SeparateStack => "separate stack", + _ => "unrecognized memory resource", + }; + write!( + f, + "application memory envelope exceeds {resource_label}: required {required_bytes} bytes, available {available_bytes} bytes" + ) + } + Self::UnsupportedValidationOutcome => { + f.write_str("unsupported platform memory validation outcome") + } + Self::Validation { source } => source.fmt(f), + } + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "the typed validation source is the only nested startup error" +)] +impl Error for PlatformMemoryStartupError { + #[expect( + clippy::pattern_type_mismatch, + reason = "match ergonomics preserve the borrowed validation source" + )] + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Validation { source } => Some(source), + Self::Exceeds { .. } | Self::UnsupportedValidationOutcome => None, + } + } +} + +/// Installs request-lifecycle policy before any adapter begins polling a body. +fn configure_app(app: &mut EdgeZeroApp) -> Result<(), EdgeError> { + platform_memory_startup(app.platform()).map_err(EdgeError::internal)?; + app.set_config_extraction_limits(ConfigExtractionLimits::default())?; + let mut headers = HeaderMap::new(); + headers.insert( + "x-application-version", + HeaderValue::from_static(env!("CARGO_PKG_VERSION")), + ); + app.set_synthesized_response_headers(headers) + .map_err(EdgeError::internal)?; + app.set_ingress_admission_policy(|head| match head.route_resolution().clone() { + RouteResolution::Matched(metadata) => { + let read_budget = if metadata.class() == Some("outbound") { + OUTBOUND_INGRESS_READ_BUDGET + } else { + DEFAULT_INGRESS_READ_BUDGET + }; + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(read_budget), + } + } + RouteResolution::MethodNotAllowed { .. } | RouteResolution::NotFound => { + AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + max_body_bytes: FALLBACK_INGRESS_BODY_BYTES, + read_deadline: head.read_deadline_after(FALLBACK_INGRESS_READ_BUDGET), + on_exceeded: BufferedIngressResponse::text( + StatusCode::PAYLOAD_TOO_LARGE, + "request body too large\n", + ), + on_timeout: BufferedIngressResponse::text( + StatusCode::REQUEST_TIMEOUT, + "request timeout\n", + ), + } + } + _ => AdmissionDecision::Abort, + }); + Ok(()) +} + +fn platform_memory_validation( + platform: PlatformMetadata, +) -> Result { + let known_ceiling = platform.memory_ceiling().value(); + let scope = known_ceiling.map_or(MemoryCeilingScope::PerExecution, MemoryCeiling::scope); + let (fixed_bytes, separate_stack_bytes) = + match known_ceiling.and_then(MemoryCeiling::stack_bytes) { + Some(_) => ( + EXPECTED_FIXED_MEMORY_BYTES, + Some(EXPECTED_SEPARATE_STACK_BYTES), + ), + None => ( + EXPECTED_FIXED_MEMORY_BYTES + EXPECTED_SEPARATE_STACK_BYTES, + None, + ), + }; + let envelope = MemoryEnvelope::new( + scope, + fixed_bytes, + EXPECTED_PER_LIVE_REQUEST_BYTES, + separate_stack_bytes, + ); + platform.validate_memory_envelope(envelope) +} + +fn platform_memory_startup(platform: PlatformMetadata) -> Result<(), PlatformMemoryStartupError> { + let validation = platform_memory_validation(platform) + .map_err(|source| PlatformMemoryStartupError::Validation { source })?; + match validation { + MemoryEnvelopeValidation::Fits { .. } => Ok(()), + MemoryEnvelopeValidation::Indeterminate { .. } => { + // Unknown facts remain indeterminate; startup may continue without claiming `Fits` + // or adding an application concurrency gate. + Ok(()) + } + MemoryEnvelopeValidation::Exceeds { + available_bytes, + required_bytes, + resource, + } => Err(PlatformMemoryStartupError::Exceeds { + available_bytes, + required_bytes, + resource, + }), + _ => Err(PlatformMemoryStartupError::UnsupportedValidationOutcome), + } +} + +edgezero_core::app!("../../edgezero.toml", configure = crate::configure_app); + +#[cfg(test)] +mod lifecycle_tests { + use edgezero_core::response_egress_framing::ResponseEgressFramingMode; + use std::num::NonZeroU32; + use std::time::Duration; + + use edgezero_core::app::App as EdgeZeroApp; + use edgezero_core::body::Body; + use edgezero_core::http::{HeaderMap, Method, Response, StatusCode, Version, request_builder}; + use edgezero_core::ingress::{IngressBeginOutcome, IngressDispatchOutcome, IngressHeadParts}; + use edgezero_core::router::RouteResolution; + use edgezero_core::time::MonotonicInstant; + use edgezero_core::{EdgeError, ResponseEgressEnvelope}; + use futures::executor::block_on; + + #[test] + fn manifest_route_classes_reach_route_resolution() { + let resolved = crate::build_router() + .expect("generated router") + .resolve(&Method::GET, "/proxy/status/200"); + let RouteResolution::Matched(metadata) = resolved.resolution().clone() else { + panic!("proxy route must resolve"); + }; + assert_eq!(metadata.class(), Some("outbound")); + } + + #[test] + fn platform_memory_validation_preserves_unknown_facts() { + let validation = + super::platform_memory_validation(edgezero_core::PlatformMetadata::default()) + .expect("valid generated envelope"); + + let edgezero_core::MemoryEnvelopeValidation::Indeterminate { reasons, .. } = validation + else { + panic!("unknown platform facts must prevent complete validation"); + }; + assert_eq!(reasons.len(), 3); + } + + fn known_platform( + ceiling: edgezero_core::MemoryCeiling, + population: NonZeroU32, + ) -> edgezero_core::PlatformMetadata { + let source = edgezero_core::PlatformResourceSource::PlatformLimit { + provider: "test platform", + }; + edgezero_core::PlatformMetadata::new( + edgezero_core::PlatformFact::known(ceiling, source), + edgezero_core::PlatformFact::known( + edgezero_core::InboundRequestPopulationBound::new(population), + source, + ), + edgezero_core::PlatformFact::known( + edgezero_core::HostIngressMemoryAccounting::OutsideCeiling, + source, + ), + ) + } + + #[test] + fn platform_memory_validation_accounts_for_both_stack_shapes() { + let primary_without_separate_stack = super::EXPECTED_FIXED_MEMORY_BYTES + + super::EXPECTED_SEPARATE_STACK_BYTES + + super::EXPECTED_PER_LIVE_REQUEST_BYTES; + let without_separate_stack = super::platform_memory_validation(known_platform( + edgezero_core::MemoryCeiling::new( + primary_without_separate_stack, + edgezero_core::MemoryCeilingScope::PerExecution, + None, + ), + NonZeroU32::MIN, + )) + .expect("valid envelope without separate stack"); + assert!(matches!( + without_separate_stack, + edgezero_core::MemoryEnvelopeValidation::Fits { + required_primary_bytes, + required_separate_stack_bytes: None, + .. + } if required_primary_bytes == primary_without_separate_stack + )); + + let primary_with_separate_stack = + super::EXPECTED_FIXED_MEMORY_BYTES + super::EXPECTED_PER_LIVE_REQUEST_BYTES; + let with_separate_stack = super::platform_memory_validation(known_platform( + edgezero_core::MemoryCeiling::new( + primary_with_separate_stack, + edgezero_core::MemoryCeilingScope::PerExecution, + Some(super::EXPECTED_SEPARATE_STACK_BYTES), + ), + NonZeroU32::MIN, + )) + .expect("valid envelope with separate stack"); + assert!(matches!( + with_separate_stack, + edgezero_core::MemoryEnvelopeValidation::Fits { + required_primary_bytes, + required_separate_stack_bytes: Some(required_stack_bytes), + .. + } if required_primary_bytes == primary_with_separate_stack + && required_stack_bytes == super::EXPECTED_SEPARATE_STACK_BYTES + )); + } + + #[test] + fn platform_memory_startup_accepts_fit() { + let required_primary = + super::EXPECTED_FIXED_MEMORY_BYTES + super::EXPECTED_PER_LIVE_REQUEST_BYTES; + let platform = known_platform( + edgezero_core::MemoryCeiling::new( + required_primary, + edgezero_core::MemoryCeilingScope::PerExecution, + Some(super::EXPECTED_SEPARATE_STACK_BYTES), + ), + NonZeroU32::MIN, + ); + + assert_eq!(super::platform_memory_startup(platform), Ok(())); + } + + #[test] + fn platform_memory_startup_preserves_primary_excess() { + let platform = known_platform( + edgezero_core::MemoryCeiling::new( + 1, + edgezero_core::MemoryCeilingScope::PerExecution, + None, + ), + NonZeroU32::MIN, + ); + + assert!(matches!( + super::platform_memory_startup(platform), + Err(super::PlatformMemoryStartupError::Exceeds { + available_bytes: 1, + required_bytes, + resource: edgezero_core::MemoryResource::Primary, + }) if required_bytes == super::EXPECTED_FIXED_MEMORY_BYTES + + super::EXPECTED_SEPARATE_STACK_BYTES + + super::EXPECTED_PER_LIVE_REQUEST_BYTES + )); + } + + #[test] + fn platform_memory_startup_preserves_stack_excess() { + let platform = known_platform( + edgezero_core::MemoryCeiling::new( + super::EXPECTED_FIXED_MEMORY_BYTES + super::EXPECTED_PER_LIVE_REQUEST_BYTES, + edgezero_core::MemoryCeilingScope::PerExecution, + Some(1), + ), + NonZeroU32::MIN, + ); + + assert_eq!( + super::platform_memory_startup(platform), + Err(super::PlatformMemoryStartupError::Exceeds { + available_bytes: 1, + required_bytes: super::EXPECTED_SEPARATE_STACK_BYTES, + resource: edgezero_core::MemoryResource::SeparateStack, + }) + ); + } + + #[test] + fn platform_memory_startup_keeps_indeterminate_distinct_from_fit() { + assert_eq!( + super::platform_memory_startup(edgezero_core::PlatformMetadata::default()), + Ok(()) + ); + } + + #[test] + fn platform_memory_startup_preserves_validation_error() { + let platform = known_platform( + edgezero_core::MemoryCeiling::new( + u64::MAX, + edgezero_core::MemoryCeilingScope::PerExecution, + Some(super::EXPECTED_SEPARATE_STACK_BYTES), + ), + NonZeroU32::new(2).expect("non-zero population"), + ); + + assert!(matches!( + super::platform_memory_startup(platform), + Err(super::PlatformMemoryStartupError::Validation { + source: + edgezero_core::MemoryEnvelopeValidationError::NonUnitPerExecutionPopulation { + max_live_requests, + }, + }) if max_live_requests.get() == 2 + )); + } + + #[test] + fn configured_synthesized_headers_cover_detached_errors() { + let app = EdgeZeroApp::build::(edgezero_core::PlatformMetadata::default()) + .expect("configured app"); + let outcome = app.detached_ingress_error_egress( + EdgeError::uri_too_long("private target"), + Method::HEAD, + app.monotonic_now(), + ); + let IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("detached response"); + }; + let response = complete_envelope(*envelope); + assert_eq!(response.status(), StatusCode::URI_TOO_LONG); + assert_eq!( + response.headers()["x-application-version"], + env!("CARGO_PKG_VERSION") + ); + assert_eq!( + response.body().as_bytes().expect("suppressed body").len(), + 0 + ); + } + + #[test] + fn configured_admission_uses_finite_class_aware_deadlines() { + let router = crate::build_router().expect("generated routes"); + let app = EdgeZeroApp::from_router::( + router, + edgezero_core::PlatformMetadata::default(), + ) + .expect("configured app"); + let start = MonotonicInstant::now(); + + let outbound = begin_ingress(&app, "/proxy/status/200", start); + assert_eq!( + outbound.read_deadline().instant(), + start + .checked_add(Duration::from_secs(10)) + .expect("deadline") + ); + + let health = begin_ingress(&app, "/", start); + assert_eq!( + health.read_deadline().instant(), + start + .checked_add(Duration::from_secs(30)) + .expect("deadline") + ); + + let fallback = begin_ingress(&app, "/missing", start); + assert_eq!( + fallback.read_deadline().instant(), + start.checked_add(Duration::from_secs(5)).expect("deadline") + ); + } + + #[test] + fn admission_handler_consumes_the_grant_once() { + let app = EdgeZeroApp::build::(edgezero_core::PlatformMetadata::default()) + .expect("configured app"); + let start = MonotonicInstant::now(); + let prepared = begin_ingress(&app, "/admission", start); + let request = request_builder() + .method(Method::GET) + .uri("/admission") + .body(Body::empty()) + .expect("request"); + + let response = complete_envelope(block_on(app.dispatch_admitted(prepared, request))); + let payload: serde_json::Value = response.body().to_json().expect("json"); + assert_eq!(payload["route_class"], "diagnostic"); + assert_eq!(payload["grant_consumed_once"], true); + } + + fn complete_envelope(envelope: ResponseEgressEnvelope) -> Response { + let Ok((prepared, _, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("response egress begin"); + }; + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + prepared.into_response() + } + + fn begin_ingress( + app: &EdgeZeroApp, + path: &str, + start: MonotonicInstant, + ) -> edgezero_core::PreparedIngress { + let head = IngressHeadParts::new( + Method::GET, + path.parse().expect("URI"), + Version::HTTP_11, + HeaderMap::new(), + ); + match app.begin_ingress(head, start).expect("begin ingress") { + IngressBeginOutcome::Admitted(prepared) => prepared, + IngressBeginOutcome::Refused(_) => panic!("demo policy must admit request"), + IngressBeginOutcome::Aborted | _ => panic!("unknown admission outcome"), + } + } +} diff --git a/crates/edgezero-cli/src/templates/root/README.md.hbs b/crates/edgezero-cli/src/templates/root/README.md.hbs index df485cdc1..761fc3a1b 100644 --- a/crates/edgezero-cli/src/templates/root/README.md.hbs +++ b/crates/edgezero-cli/src/templates/root/README.md.hbs @@ -13,15 +13,65 @@ This workspace demonstrates a multi-target EdgeZero app. - `GET /headers` - prints the incoming `user-agent` - `GET /stream` - emits a short streamed response body - `POST /echo` - echoes JSON payloads -- `GET|POST /proxy/{*rest}` - forwards requests to `API_BASE_URL`, falling back to a 501 when the active adapter does not expose a proxy client +- `GET /admission` - confirms route-aware admission and one-time grant consumption +- `GET|POST /proxy/{*rest}` - sends the request to `API_BASE_URL`, falling back to a 501 when the active adapter does not inject an outbound HTTP client +- `GET /follow` - requests `API_BASE_URL/redirect/1` and follows at most four redirects, allowing only the configured scheme and authority +- `POST /fanout` - attempts up to eight relative paths before harvest and returns index-aligned status, error category, unresolved state, and per-slot elapsed milliseconds + +## Outbound HTTP + +The generated `edgezero.toml` declares `outbound-http` as optional and grants +HTTPS destinations only. An optional capability accepts documented behavioral +deviations and may fail at runtime when a deployment prerequisite has not been +verified. If application behavior depends on successful outbound requests, +promote `outbound-http` to `required` and choose an adapter and deployment whose +capability check passes. See the +[capability guide](https://iabtechlab.github.io/edgezero/guide/capabilities) for +the current support matrix and deployment prerequisites. + +The outbound examples bypass adapter-managed caches and set a 5-second timeout, a 1 MiB request-body cap, +independent 2 MiB encoded / 1 MiB decoded / 2 MiB final-buffer response caps, +64 KiB / 100-field response-header caps, and explicit Brotli window and decoder +memory limits. `/fanout` also gives every slot one shared absolute deadline and +returns the adapter-recorded elapsed time for each terminal slot. This elapsed value +includes EdgeZero and provider processing rather than representing pure wire +RTT. Cross-slot timing isolation is adapter-dependent; deployments that require +it must declare `outbound-batch-slot-isolation` as a required capability. Applications +that retain partial results after cancellation should also require the appropriate +`outbound-batch-completion-order` and `outbound-batch-cancellation` support levels. + +`/proxy` returns upstream redirects unchanged; only `/follow` calls the explicit +redirect API. `/follow` checks its initial destination and every prepared hop +against the same configured scheme and authority, retains one 5-second absolute +application-clock deadline across the chain, and returns the final upstream +response with `x-follow-redirects` recording the number of followed redirects. +A changed scheme, host or port, or a non-HTTP target, is refused with a fixed +403 response before another request is sent. The allowlist does not replace +deployment-level destination restrictions or DNS/network policy. Transport and +limit failures retain their original typed error classification. ## Dev {{{readme_adapter_dev}}} +## Logging + +Axum and Fastly entrypoints delegate logging to the reusable adapter runners; +do not initialize a second CLI logger before a runtime runner. Ordinary levels +come from `EDGEZERO__LOGGING__LEVEL` (Fastly reads its runtime Config Store), with +`Info` as the absent/invalid fallback. Managed boot warnings use `edgezero::boot` +at `Warn`, independently of ordinary `Error` or `Off`. + +Cloudflare and Spin logger initializers are no-ops, not Rust facade backends. +To own logging on any target, set `owns_logging = true` in `app!`, install your +backend before entering the runner, and configure both backend and facade filters. +Reuse `edgezero_core::resolve_logging_level(&EnvConfig)`, `BOOT_LOG_TARGET`, and +`BOOT_LOG_LEVEL`; raising only `log::set_max_level` cannot restore backend-filtered +messages. No logging implementation is duplicated in the generated entrypoints. + ## Configuration -Environment variables are declared in `edgezero.toml`. Set `API_BASE_URL` to the upstream origin you want `/proxy/...` to target. Adapter-specific secrets are opt-in: uncomment the `#[secret]` field in `crates/{{proj_core}}/src/config.rs` and the matching `[stores.secrets]` block in `edgezero.toml`, then provide the secret value through the wired backend (Cloudflare Worker secret, Fastly Secret Store, Spin secret variable, ...). The Axum dev server reads secret key `demo_api_token` from the environment variable `EDGEZERO__SECRETS__DEMO_API_TOKEN`, which is the `EDGEZERO__SECRETS__` prefix plus the key in uppercase. +Environment variables are declared in `edgezero.toml`. Set `API_BASE_URL` to the upstream origin you want `/proxy/...` to target; the handlers read it through `RequestContext::variable`, including from the declared Cloudflare Worker binding. Adapter-specific secrets are opt-in: uncomment the `#[secret]` field in `crates/{{proj_core}}/src/config.rs` and the matching `[stores.secrets]` block in `edgezero.toml`, then provide the secret value through the wired backend (Cloudflare Worker secret, Fastly Secret Store, Spin secret variable, ...). The Axum dev server reads secret key `demo_api_token` from the environment variable `EDGEZERO__SECRETS__DEMO_API_TOKEN`, which is the `EDGEZERO__SECRETS__` prefix plus the key in uppercase. ### Typed app config (`{{name}}.toml`) @@ -50,6 +100,11 @@ cargo run -p {{proj_cli}} -- config diff --adapter --format json --exit-c Uncomment `[stores.config]` in `edgezero.toml` (and the matching adapter binding in the per-adapter manifest) before running `config push`. +For Axum, push before starting the server or restart after updating config: the +runner loads bounded immutable snapshots before listener binding. Configure allocation +budgets through `EDGEZERO__ADAPTER__CONFIG_STORE__*`; invalid budgets fail startup. +Raw custom store implementations return shared `edgezero_core::ConfigValue`, while +the typed config extractor and generated handlers require no manual store reads. Use `--key ` for per-environment overrides (staging / canary) on adapters that support custom runtime keys: diff --git a/crates/edgezero-cli/src/templates/root/edgezero.toml.hbs b/crates/edgezero-cli/src/templates/root/edgezero.toml.hbs index c8e53cfab..78d91e5e5 100644 --- a/crates/edgezero-cli/src/templates/root/edgezero.toml.hbs +++ b/crates/edgezero-cli/src/templates/root/edgezero.toml.hbs @@ -7,12 +7,19 @@ version = "0.1.0" kind = "http" entry = "crates/{{proj_core}}" +[capabilities] +optional = ["outbound-http"{{#if native_image_example}}, "outbound-image-optimizer"{{/if}}] + +[capabilities.outbound] +hosts = ["https://*:*"] + [[triggers.http]] id = "root" path = "/" methods = ["GET"] handler = "{{proj_core_mod}}::handlers::root" adapters = [{{{adapter_list}}}] +class = "health" description = "Default health-check endpoint" [[triggers.http]] @@ -44,6 +51,14 @@ methods = ["POST"] handler = "{{proj_core_mod}}::handlers::echo_json" adapters = [{{{adapter_list}}}] +[[triggers.http]] +id = "admission" +path = "/admission" +methods = ["GET"] +handler = "{{proj_core_mod}}::handlers::admission" +adapters = [{{{adapter_list}}}] +class = "diagnostic" +description = "Confirms route-aware admission and one-time grant consumption" [[triggers.http]] id = "proxy_demo" @@ -51,6 +66,36 @@ path = "/proxy/{*rest}" methods = ["GET", "POST"] handler = "{{proj_core_mod}}::handlers::proxy_demo" adapters = [{{{adapter_list}}}] +class = "outbound" + +[[triggers.http]] +id = "follow_demo" +path = "/follow" +methods = ["GET"] +handler = "{{proj_core_mod}}::handlers::follow_demo" +adapters = [{{{adapter_list}}}] +class = "outbound" +description = "Follows at most four redirects within the configured API origin" + +[[triggers.http]] +id = "fanout" +path = "/fanout" +methods = ["POST"] +handler = "{{proj_core_mod}}::handlers::fanout" +adapters = [{{{adapter_list}}}] +class = "outbound" +description = "Concurrent positional outbound batch with per-slot elapsed time" + +{{#if native_image_example}} +[[triggers.http]] +id = "image-demo" +path = "/image-demo" +methods = ["GET"] +handler = "{{proj_core_mod}}::handlers::image_demo" +adapters = ["fastly"] +class = "outbound" +description = "Native image optimization; requires Fastly entitlement" +{{/if}} # -- Introspection routes ------------------------------------------------------ @@ -60,6 +105,7 @@ path = "/_{{name}}/manifest" methods = ["GET"] handler = "edgezero_core::introspection::manifest" adapters = [{{{adapter_list}}}] +class = "diagnostic" description = "App manifest as JSON" [[triggers.http]] @@ -68,6 +114,7 @@ path = "/_{{name}}/config" methods = ["GET"] handler = "edgezero_core::introspection::config" adapters = [{{{adapter_list}}}] +class = "diagnostic" description = "Effective app config (secret-safe)" [[triggers.http]] @@ -76,6 +123,7 @@ path = "/_{{name}}/routes" methods = ["GET"] handler = "edgezero_core::introspection::routes" adapters = [{{{adapter_list}}}] +class = "diagnostic" description = "Registered route table" # -- Stores ---------------------------------------------------------------- @@ -104,18 +152,40 @@ description = "Registered route table" # [stores.secrets] # ids = ["default"] -# [environment] +[environment] + +[[environment.variables]] +name = "API_BASE_URL" +description = "Base URL for upstream API calls" +env = "API_BASE_URL" + +# Axum enforces finite pre-admission HTTP/1 limits by default. To override a +# limit through the CLI, uncomment an adapter-filtered default below. Parent +# environment values take precedence. See docs/guide/adapters/axum.md. +# [[environment.variables]] +# name = "EDGEZERO__ADAPTER__INGRESS__MAX_CONNECTIONS" +# value = "256" +# adapters = ["axum"] +# +# [[environment.variables]] +# name = "EDGEZERO__ADAPTER__INGRESS__MAX_RAW_HEAD_BYTES" +# value = "65536" +# adapters = ["axum"] # # [[environment.variables]] -# name = "API_BASE_URL" -# description = "Base URL for upstream API calls" -# env = "API_BASE_URL" -# value = "https://example.com/api" +# name = "EDGEZERO__ADAPTER__INGRESS__MAX_HEADER_COUNT" +# value = "100" +# adapters = ["axum"] # +# [[environment.variables]] +# name = "EDGEZERO__ADAPTER__INGRESS__HEAD_READ_TIMEOUT_MS" +# value = "10000" +# adapters = ["axum"] + # [[environment.secrets]] # name = "API_TOKEN" # description = "Secret token used when calling the upstream API" # adapters = [{{{adapter_list}}}] # env = "API_TOKEN" -{{{adapter_manifest_sections}}} \ No newline at end of file +{{{adapter_manifest_sections}}} diff --git a/crates/edgezero-cli/tests/demo_logging.rs b/crates/edgezero-cli/tests/demo_logging.rs new file mode 100644 index 000000000..93a6382c7 --- /dev/null +++ b/crates/edgezero-cli/tests/demo_logging.rs @@ -0,0 +1,73 @@ +//! Subprocess probes keep application logger installation isolated from the test runner. +#![cfg(all(feature = "cli", feature = "demo-example"))] + +#[cfg(test)] +#[path = "support/fallback_tcp.rs"] +mod fallback_tcp; + +#[cfg(test)] +mod tests { + use std::net::TcpListener; + use std::process::Command; + + fn demo_startup(level: &str, host: &str) -> (String, String) { + let directory = tempfile::tempdir().expect("isolated demo state"); + let listener = TcpListener::bind("127.0.0.1:0").expect("reserve demo port"); + let port = listener.local_addr().expect("reserved address").port(); + let output = Command::new(env!("CARGO_BIN_EXE_edgezero")) + .arg("demo") + .current_dir(directory.path()) + .env("EDGEZERO__LOGGING__LEVEL", level) + .env("EDGEZERO__ADAPTER__HOST", host) + .env("EDGEZERO__ADAPTER__PORT", port.to_string()) + .output() + .expect("demo subprocess"); + assert!( + !output.status.success(), + "reserved port must prevent serving" + ); + ( + String::from_utf8(output.stdout).expect("demo stdout"), + String::from_utf8(output.stderr).expect("demo stderr"), + ) + } + + #[test] + fn demo_respects_off_without_hiding_terminal_errors() { + let (stdout, stderr) = demo_startup("off", "not-an-ip"); + assert!(!stdout.contains("starting axum server"), "{stdout}"); + assert!(stdout.contains("[edgezero::boot]"), "{stdout}"); + assert!(stdout.contains("not a valid IP address"), "{stdout}"); + assert!( + stderr.contains("[edgezero] demo server error: failed to bind"), + "{stderr}" + ); + assert!( + !stderr.contains("initialize application logger"), + "{stderr}" + ); + } + + #[test] + fn demo_uses_application_logger_not_the_cli_backend() { + let (stdout, stderr) = demo_startup("info", "127.0.0.1"); + assert!(stdout.contains("INFO"), "{stdout}"); + assert!( + stdout.contains("[edgezero_adapter_axum::dev_server]"), + "{stdout}" + ); + assert!(stdout.contains("starting axum server"), "{stdout}"); + assert!( + stderr.contains("[edgezero] demo server error: failed to bind"), + "{stderr}" + ); + } + + #[test] + fn demo_fallbacks_enforce_body_cap_over_tcp() { + let state = tempfile::tempdir().expect("isolated demo runtime state"); + let mut command = Command::new(env!("CARGO_BIN_EXE_edgezero")); + command.arg("demo").current_dir(state.path()); + super::fallback_tcp::assert_fallbacks(command, "demo", b"app-demo app"); + } +} diff --git a/crates/edgezero-cli/tests/generated_project_builds.rs b/crates/edgezero-cli/tests/generated_project_builds.rs index be2808852..1ba3cf998 100644 --- a/crates/edgezero-cli/tests/generated_project_builds.rs +++ b/crates/edgezero-cli/tests/generated_project_builds.rs @@ -14,11 +14,47 @@ //! cargo test -p edgezero-cli --test generated_project_builds -- --ignored //! ``` +#[cfg(test)] +#[path = "support/fallback_tcp.rs"] +mod fallback_tcp; + #[cfg(test)] mod tests { use std::env; - use std::path::Path; - use std::process::{Command, ExitStatus}; + use std::path::{Path, PathBuf}; + use std::process::{Command, ExitStatus, Stdio}; + + fn require_listed_test(output: &str, sentinel: &str) -> Result<(), String> { + let tests: Vec<_> = output + .lines() + .filter_map(|line| line.trim().strip_suffix(": test")) + .collect(); + if tests.is_empty() { + return Err("generated core test listing contained zero tests".to_owned()); + } + let matches = tests + .iter() + .filter(|name| { + **name == sentinel + || name + .strip_suffix(sentinel) + .is_some_and(|prefix| prefix.ends_with("::")) + }) + .count(); + if matches != 1 { + return Err(format!( + "expected exactly one generated `{sentinel}` test, found {matches}" + )); + } + Ok(()) + } + + #[test] + fn generated_core_test_gate_rejects_zero_tests() { + let error = require_listed_test("0 tests, 0 benchmarks\n", "generated_outbound_http_smoke") + .expect_err("a zero-test listing must fail closed"); + assert!(error.contains("zero tests")); + } /// Targets installed for the toolchain that builds `project`. A wasm /// check is skipped when its target is absent (e.g. a local run where @@ -43,6 +79,130 @@ mod tests { .unwrap_or_else(|err| panic!("run generated CLI with {args:?}: {err}")) } + fn assert_cargo_success(project: &Path, args: &[&str], expectation: &str) { + let status = Command::new(env!("CARGO")) + .args(args) + .current_dir(project) + .status() + .unwrap_or_else(|err| panic!("run cargo with {args:?}: {err}")); + assert!(status.success(), "{expectation}"); + } + + fn assert_edgezero_success(project: &Path, args: &[&str], expectation: &str) { + let status = Command::new(env!("CARGO_BIN_EXE_edgezero")) + .args(args) + .current_dir(project) + .status() + .unwrap_or_else(|err| panic!("run edgezero with {args:?}: {err}")); + assert!(status.success(), "{expectation}"); + } + + fn verify_generated_core_tests(project: &Path, core_package: &str) { + let listed = Command::new(env!("CARGO")) + .args(["test", "-p", core_package, "--lib", "--", "--list"]) + .current_dir(project) + .output() + .expect("list generated core tests"); + assert!( + listed.status.success(), + "generated core test list should succeed" + ); + let listed_stdout = String::from_utf8_lossy(&listed.stdout); + for sentinel in [ + "generated_outbound_http_smoke", + "follow_demo_uses_supplied_head_client_and_app_deadline", + "follow_demo_allows_canonical_equivalent_origins", + "scripted_batch_preserves_receipts_through_mixed_collection", + "fanout_rejects_delivered_and_completed_unresolved_slots", + "scripted_batch_cutoff_retains_only_earlier_completions", + "scripted_batch_expired_cutoff_keeps_preflight_outcomes", + ] { + require_listed_test(&listed_stdout, sentinel) + .expect("generated core sentinel must be listed exactly once"); + } + assert_cargo_success( + project, + &["test", "-p", core_package, "--lib"], + "generated core tests should pass", + ); + } + + fn generated_axum_binary(project: &Path) -> PathBuf { + let version_output = Command::new("rustc") + .arg("-vV") + .current_dir(project) + .output() + .expect("native Rust target"); + assert!( + version_output.status.success(), + "rustc native target must be available" + ); + let version = String::from_utf8(version_output.stdout).expect("rustc version"); + let host = version + .lines() + .find_map(|line| line.strip_prefix("host: ")) + .expect("rustc host triple"); + let output = Command::new(env!("CARGO")) + .args([ + "build", + "--offline", + "--locked", + "-p", + "scaffold-probe-adapter-axum", + "--target", + host, + "--message-format=json-render-diagnostics", + ]) + .current_dir(project) + .stderr(Stdio::inherit()) + .output() + .expect("build generated Axum entrypoint"); + assert!( + output.status.success(), + "generated Axum entrypoint must build" + ); + let manifest = project + .join("crates/scaffold-probe-adapter-axum/Cargo.toml") + .canonicalize() + .expect("generated Axum manifest"); + let manifests: Vec<_> = output + .stdout + .split(|byte| *byte == b'\n') + .filter(|line| !line.is_empty()) + .map(|line| { + serde_json::from_slice::(line).expect("Cargo JSON record") + }) + .filter(|record| { + record["reason"] == "compiler-artifact" + && record["target"]["name"] == "scaffold-probe-adapter-axum" + && record["target"]["kind"] == serde_json::json!(["bin"]) + && record["profile"]["test"] == false + && record["manifest_path"].as_str().is_some_and(|path| { + Path::new(path) + .canonicalize() + .is_ok_and(|candidate| candidate == manifest) + }) + }) + .map(|record| { + PathBuf::from( + record["executable"] + .as_str() + .expect("native binary executable"), + ) + }) + .collect(); + assert_eq!(manifests.len(), 1, "one generated Axum executable"); + manifests.into_iter().next().expect("generated Axum binary") + } + + fn assert_generated_fallback_tcp(project: &Path) { + let binary = generated_axum_binary(project); + let state = tempfile::tempdir().expect("isolated generated runtime state"); + let mut command = Command::new(binary); + command.current_dir(state.path()); + super::fallback_tcp::assert_fallbacks(command, "generated", b"scaffold-probe app"); + } + #[test] #[ignore = "compiles a generated workspace and may fetch crates; run explicitly"] #[expect( @@ -70,13 +230,9 @@ mod tests { // `cargo check` so a manifest/config drift surfaces as a // fast, clear error -- not as a compilation cascade from // a downstream macro tripping over the bad config. - let validate = Command::new(env!("CARGO_BIN_EXE_edgezero")) - .args(["config", "validate"]) - .current_dir(&project) - .status() - .expect("run `edgezero config validate` on the generated workspace"); - assert!( - validate.success(), + assert_edgezero_success( + &project, + &["config", "validate"], "generated workspace should pass `edgezero config validate`", ); @@ -87,28 +243,41 @@ mod tests { // with a malformed handler or a manifest that violates the // adapter capability matrix would silently pass plain // validate but fail under strict. - let validate_strict = Command::new(env!("CARGO_BIN_EXE_edgezero")) - .args(["config", "validate", "--strict"]) - .current_dir(&project) - .status() - .expect("run `edgezero config validate --strict` on the generated workspace"); - assert!( - validate_strict.success(), + assert_edgezero_success( + &project, + &["config", "validate", "--strict"], "generated workspace should pass `edgezero config validate --strict`", ); + assert_cargo_success( + &project, + &["fmt", "--all", "--", "--check"], + "generated workspace should be rustfmt-clean", + ); + // Host target: the whole workspace, including the generated CLI // crate that imports `edgezero_cli`. - let host = Command::new(env!("CARGO")) - .args(["check", "--workspace"]) - .current_dir(&project) - .status() - .expect("run `cargo check` on the generated workspace"); - assert!( - host.success(), + assert_cargo_success( + &project, + &["check", "--workspace"], "generated workspace should compile for the host target", ); + assert_cargo_success( + &project, + &[ + "clippy", + "--workspace", + "--all-targets", + "--all-features", + "--", + "-D", + "warnings", + ], + "generated workspace should pass strict clippy", + ); + + verify_generated_core_tests(&project, "scaffold-probe-core"); // Typed config validation via the generated `-cli` binary. // The raw `edgezero config validate` above exercises the manifest // schema and capability matrix; the generated CLI additionally @@ -146,8 +315,9 @@ mod tests { continue; } let crate_name = format!("scaffold-probe-adapter-{adapter}"); - let wasm = Command::new(env!("CARGO")) - .args([ + assert_cargo_success( + &project, + &[ "check", "-p", &crate_name, @@ -155,14 +325,10 @@ mod tests { target, "--features", adapter, - ]) - .current_dir(&project) - .status() - .expect("run `cargo check` for a wasm adapter target"); - assert!( - wasm.success(), - "generated {adapter} adapter should compile for {target}", + ], + &format!("generated {adapter} adapter should compile for {target}"), ); } + assert_generated_fallback_tcp(&project); } } diff --git a/crates/edgezero-cli/tests/platform_memory_docs.rs b/crates/edgezero-cli/tests/platform_memory_docs.rs new file mode 100644 index 000000000..1967f2144 --- /dev/null +++ b/crates/edgezero-cli/tests/platform_memory_docs.rs @@ -0,0 +1,209 @@ +#![cfg(all( + feature = "edgezero-adapter-axum", + feature = "edgezero-adapter-cloudflare", + feature = "edgezero-adapter-fastly", + feature = "edgezero-adapter-spin" +))] + +#[cfg(test)] +mod tests { + use edgezero_core::{ + HostIngressMemoryAccounting, InboundRequestPopulationBound, MemoryCeiling, + MemoryCeilingScope, PlatformFact, PlatformMetadata, PlatformResourceSource, + PlatformUnknownReason, + }; + + const CAPABILITY_GUIDE: &str = include_str!("../../../docs/guide/capabilities.md"); + + fn normalized_row(row: &str) -> String { + row.trim() + .trim_matches('|') + .split('|') + .map(str::trim) + .collect::>() + .join("|") + } + + fn guide_contains_row(expected: &str) -> bool { + let normalized_expected = normalized_row(expected); + CAPABILITY_GUIDE + .lines() + .any(|line| normalized_row(line) == normalized_expected) + } + + fn scope_label(scope: MemoryCeilingScope) -> &'static str { + match scope { + MemoryCeilingScope::PerExecution => "Per execution", + MemoryCeilingScope::PerInstance => "Per instance", + _ => panic!("unrecognized memory ceiling scope"), + } + } + + fn source_label(source: PlatformResourceSource) -> String { + match source { + PlatformResourceSource::PlatformLimit { provider } => { + format!("Platform limit: {provider}") + } + PlatformResourceSource::HostedDefault { provider } => { + format!("Hosted default: {provider}") + } + PlatformResourceSource::RuntimeConfiguration { runtime } => { + format!("Runtime configuration: {runtime}") + } + _ => panic!("unrecognized platform resource source"), + } + } + + fn unknown_reason_label(reason: PlatformUnknownReason) -> &'static str { + match reason { + PlatformUnknownReason::OperatorConfigured => "Unknown: Operator configured", + PlatformUnknownReason::ProviderUnpublished => "Unknown: Provider unpublished", + PlatformUnknownReason::RuntimeConfigured => "Unknown: Runtime configured", + PlatformUnknownReason::Unspecified => "Unknown: Unspecified", + _ => panic!("unrecognized platform unknown reason"), + } + } + + fn byte_unit_label(bytes: u64) -> String { + const BYTES_PER_MB: u64 = 1_000_000; + const BYTES_PER_MIB: u64 = 1024 * 1024; + + if let Some(megabytes) = bytes + .checked_div(BYTES_PER_MB) + .filter(|_| bytes.is_multiple_of(BYTES_PER_MB)) + { + format!("{megabytes} MB") + } else if let Some(mebibytes) = bytes + .checked_div(BYTES_PER_MIB) + .filter(|_| bytes.is_multiple_of(BYTES_PER_MIB)) + { + format!("{mebibytes} MiB") + } else { + format!("{bytes} B") + } + } + + fn bytes_with_unit(bytes: u64) -> String { + format!("{bytes} bytes ({})", byte_unit_label(bytes)) + } + + #[test] + fn byte_units_are_derived_from_canonical_values() { + let bytes_134_217_728 = 128 * 1024 * 1024; + + assert_eq!(byte_unit_label(128_000_000), "128 MB"); + assert_eq!(byte_unit_label(1_000_000), "1 MB"); + assert_eq!(byte_unit_label(bytes_134_217_728), "128 MiB"); + assert_eq!(byte_unit_label(1_234), "1234 B"); + } + + fn memory_cells(fact: PlatformFact) -> (String, String, String, String) { + match fact { + PlatformFact::Known { value, source } => { + let primary = bytes_with_unit(value.total_bytes()); + let stack = value + .stack_bytes() + .map_or_else(|| "None".to_owned(), bytes_with_unit); + ( + primary, + stack, + scope_label(value.scope()).to_owned(), + source_label(source), + ) + } + PlatformFact::Unknown { reason } => ( + "Unknown".to_owned(), + "Unknown".to_owned(), + "Unknown".to_owned(), + unknown_reason_label(reason).to_owned(), + ), + _ => panic!("unrecognized memory fact variant"), + } + } + + fn population_cells(fact: PlatformFact) -> (String, String) { + match fact { + PlatformFact::Known { value, source } => { + (value.max_live_requests().to_string(), source_label(source)) + } + PlatformFact::Unknown { reason } => ( + "Unknown".to_owned(), + unknown_reason_label(reason).to_owned(), + ), + _ => panic!("unrecognized population fact variant"), + } + } + + fn host_accounting_cells(fact: PlatformFact) -> (String, String) { + match fact { + PlatformFact::Known { value, source } => { + let accounting = match value { + HostIngressMemoryAccounting::CountsTowardCeiling => "Counts toward ceiling", + HostIngressMemoryAccounting::OutsideCeiling => "Outside ceiling", + _ => panic!("unrecognized host ingress memory accounting"), + }; + (accounting.to_owned(), source_label(source)) + } + PlatformFact::Unknown { reason } => ( + "Unknown".to_owned(), + unknown_reason_label(reason).to_owned(), + ), + _ => panic!("unrecognized host accounting fact variant"), + } + } + + fn assert_documented_platform(target: &str, platform: PlatformMetadata) { + let (primary, stack, scope, memory_source) = memory_cells(platform.memory_ceiling()); + let (population, population_source) = + population_cells(platform.inbound_request_population_bound()); + let (host_accounting, host_accounting_source) = + host_accounting_cells(platform.host_ingress_memory_accounting()); + let row = format!( + "| {target} | {primary} | {stack} | {scope} | {memory_source} | {population} | {population_source} | {host_accounting} | {host_accounting_source} |" + ); + assert!( + guide_contains_row(&row), + "capability guide is missing canonical row: {row}" + ); + } + + #[test] + fn failure_diagnostics_matrix_matches_adapters() { + use edgezero_adapter::registry::get_adapter; + use edgezero_core::{Capability, CapabilitySupport}; + + let capability: Capability = + serde_json::from_value(serde_json::json!("outbound-failure-diagnostics")) + .expect("diagnostic capability"); + let mut cells = vec![format!("`{}`", capability.as_str())]; + for (name, expected) in [ + ("axum", CapabilitySupport::BestEffort), + ("cloudflare", CapabilitySupport::BestEffort), + ("fastly", CapabilitySupport::Native), + ("spin", CapabilitySupport::Native), + ] { + let actual = get_adapter(name) + .expect("registered adapter") + .capability(capability); + assert_eq!(actual, expected, "{name}"); + cells.push(format!("{actual:?}")); + } + let row = cells.join("|"); + assert!(guide_contains_row(&row), "missing diagnostic row: {row}"); + } + + #[test] + fn platform_resource_table_matches_adapter_constants() { + assert_documented_platform("Axum", edgezero_adapter_axum::AXUM_PLATFORM); + assert_documented_platform( + "Cloudflare Workers", + edgezero_adapter_cloudflare::CLOUDFLARE_PLATFORM, + ); + assert_documented_platform("Fastly Compute", edgezero_adapter_fastly::FASTLY_PLATFORM); + assert_documented_platform("Spin (generic)", edgezero_adapter_spin::SPIN_PLATFORM); + assert_documented_platform( + "Akamai Functions (Spin)", + edgezero_adapter_spin::AKAMAI_FUNCTIONS_PLATFORM, + ); + } +} diff --git a/crates/edgezero-cli/tests/support/fallback_tcp.rs b/crates/edgezero-cli/tests/support/fallback_tcp.rs new file mode 100644 index 000000000..b556359dc --- /dev/null +++ b/crates/edgezero-cli/tests/support/fallback_tcp.rs @@ -0,0 +1,416 @@ +use std::env; +use std::fmt::Write as _; +use std::fs::File; +use std::io::{self, Read, Seek as _, SeekFrom, Write as _}; +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::process::{Child, Command, Stdio}; +use std::str; +use std::thread; +use std::time::{Duration, Instant}; + +const WIRE_LIMIT: u64 = 8192; +const IO_BUDGET: Duration = Duration::from_secs(8); + +struct DeadlineSocket { + deadline: Instant, + socket: TcpStream, +} + +#[derive(Debug)] +struct Reply { + body: Vec, + headers: Vec<(String, Vec)>, + status: u16, +} + +struct Server { + address: SocketAddr, + child: Child, + log: File, +} + +#[expect( + clippy::missing_trait_methods, + reason = "Read defaults delegate to the same absolute-deadline-checked read" +)] +impl Read for DeadlineSocket { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let remaining = self + .deadline + .checked_duration_since(Instant::now()) + .filter(|remaining| !remaining.is_zero()) + .ok_or(io::ErrorKind::TimedOut)?; + self.socket.set_read_timeout(Some(remaining))?; + self.socket.read(buf) + } +} + +impl Reply { + fn values(&self, name: &str) -> Vec<&[u8]> { + self.headers + .iter() + .filter(|(field, _)| field.eq_ignore_ascii_case(name)) + .map(|(_, value)| value.as_slice()) + .collect() + } +} + +impl Server { + fn assert_running(&mut self) { + assert!( + self.child.try_wait().expect("application state").is_none(), + "application exited during TCP proof: {}", + self.diagnostics() + ); + } + + fn diagnostics(&mut self) -> String { + self.log.seek(SeekFrom::Start(0)).expect("read diagnostics"); + let mut text = String::new(); + (&mut self.log) + .take(64 * 1024) + .read_to_string(&mut text) + .expect("bounded diagnostics"); + text + } + + fn exchange(&mut self, wire: &[u8]) -> (Vec, Duration) { + self.exchange_after_response(wire, &[]) + } + + fn exchange_after_response(&mut self, wire: &[u8], tail: &[u8]) -> (Vec, Duration) { + self.assert_running(); + let wire_limit = usize::try_from(WIRE_LIMIT).expect("wire bound"); + assert!(wire.len().checked_add(tail.len()).expect("request length") <= wire_limit); + let started = Instant::now(); + let mut socket = TcpStream::connect_timeout(&self.address, Duration::from_secs(1)) + .expect("connect application"); + socket + .set_write_timeout(Some(Duration::from_secs(2))) + .expect("write timeout"); + socket.write_all(wire).expect("send bounded request"); + let mut reader = DeadlineSocket { + socket, + deadline: started.checked_add(IO_BUDGET).expect("client deadline"), + }; + let mut bytes = Vec::new(); + let mut complete_at = None; + loop { + let mut buffer = [0_u8; 1024]; + let room = wire_limit + .saturating_add(1) + .saturating_sub(bytes.len()) + .min(buffer.len()); + let count = match reader.read(&mut buffer[..room]) { + Ok(0) => break, + Ok(count) => count, + // A reset is permitted only when all retained replies parse completely below. + Err(error) if error.kind() == io::ErrorKind::ConnectionReset => break, + Err(error) => panic!("bounded response read: {error}"), + }; + bytes.extend_from_slice(&buffer[..count]); + assert!(bytes.len() <= wire_limit, "response capture overflow"); + if complete_at.is_none() && parse_replies(&bytes).is_ok() { + complete_at = Some(started.elapsed()); + if !tail.is_empty() { + let remaining = reader + .deadline + .checked_duration_since(Instant::now()) + .filter(|remaining| !remaining.is_zero()) + .expect("deadline before tail write"); + reader + .socket + .set_write_timeout(Some(remaining.min(Duration::from_secs(2)))) + .expect("tail write timeout"); + if let Err(error) = reader.socket.write_all(tail) { + assert!( + matches!( + error.kind(), + io::ErrorKind::BrokenPipe | io::ErrorKind::ConnectionReset + ), + "deferred tail write: {error}" + ); + } + } + } + } + self.assert_running(); + ( + parse_replies(&bytes).expect("complete bounded buffered response"), + complete_at.expect("response completion observed before closure"), + ) + } + + fn start(mut command: Command) -> Self { + let reservation = TcpListener::bind("127.0.0.1:0").expect("reserve fallback test port"); + let address = reservation.local_addr().expect("reserved address"); + for (key, _) in env::vars_os() { + if key.to_string_lossy().starts_with("EDGEZERO__") { + command.env_remove(key); + } + } + let log = tempfile::tempfile().expect("server diagnostics"); + command + .env("EDGEZERO__ADAPTER__HOST", "127.0.0.1") + .env("EDGEZERO__ADAPTER__PORT", address.port().to_string()) + .env("EDGEZERO__LOGGING__LEVEL", "off") + .stdout(Stdio::from(log.try_clone().expect("stdout diagnostics"))) + .stderr(Stdio::from(log.try_clone().expect("stderr diagnostics"))); + drop(reservation); + let mut server = Self { + child: command.spawn().expect("start application"), + log, + address, + }; + let started = Instant::now(); + loop { + server.assert_running(); + if TcpStream::connect_timeout(&address, Duration::from_millis(100)).is_ok() { + server.assert_running(); + return server; + } + assert!( + started.elapsed() < Duration::from_secs(15), + "application startup timeout: {}", + server.diagnostics() + ); + thread::sleep(Duration::from_millis(20)); + } + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "unpinned test child ownership uses Drop's standard pin forwarding" +)] +impl Drop for Server { + fn drop(&mut self) { + drop(self.child.kill()); + drop(self.child.wait()); + } +} + +fn parse_replies(mut wire: &[u8]) -> io::Result> { + let invalid = || { + io::Error::new( + io::ErrorKind::InvalidData, + "invalid bounded response framing", + ) + }; + let mut replies = Vec::new(); + while !wire.is_empty() { + if replies.len() == 2 { + return Err(invalid()); + } + let mut fields = [httparse::EMPTY_HEADER; 32]; + let mut parsed = httparse::Response::new(&mut fields); + let httparse::Status::Complete(head_bytes) = parsed + .parse(wire) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))? + else { + return Err(invalid()); + }; + if parsed.version != Some(1) { + return Err(invalid()); + } + let mut declared_length = None; + for field in parsed.headers.iter() { + if field.name.eq_ignore_ascii_case("transfer-encoding") { + return Err(invalid()); + } + if field.name.eq_ignore_ascii_case("content-length") { + if declared_length.is_some() + || field.value.is_empty() + || !field.value.iter().all(u8::is_ascii_digit) + { + return Err(invalid()); + } + declared_length = Some( + str::from_utf8(field.value) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))? + .parse::() + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?, + ); + } + } + let length = declared_length + .filter(|length| *length <= 1024) + .ok_or_else(invalid)?; + let body = wire.get(head_bytes..).ok_or_else(invalid)?; + let content = body.get(..length).ok_or_else(invalid)?; + replies.push(Reply { + status: parsed.code.ok_or_else(invalid)?, + headers: parsed + .headers + .iter() + .map(|field| (field.name.to_owned(), field.value.to_vec())) + .collect(), + body: content.to_vec(), + }); + wire = &body[length..]; + } + if replies.is_empty() { + return Err(invalid()); + } + Ok(replies) +} + +fn post(path: &str, len: usize, chunked: bool, close: bool) -> String { + let mut wire = format!("POST {path} HTTP/1.1\r\nHost: localhost\r\n"); + if close { + wire.push_str("Connection: close\r\n"); + } + if chunked { + write!( + wire, + "Transfer-Encoding: chunked\r\n\r\n{len:x}\r\n{}\r\n0\r\n\r\n", + "x".repeat(len) + ) + .expect("bounded chunked fixture"); + } else { + write!(wire, "Content-Length: {len}\r\n\r\n{}", "x".repeat(len)) + .expect("bounded fixed fixture"); + } + wire +} + +fn assert_plain(reply: &Reply, status: u16, body: &[u8]) { + assert_eq!(reply.status, status); + assert_eq!(reply.body, body); + assert_eq!( + reply.values("content-type"), + [b"text/plain; charset=utf-8".as_slice()] + ); + assert!( + reply.values("retry-after").is_empty(), + "fallback gained Retry-After" + ); +} + +#[expect( + clippy::print_stdout, + reason = "synthetic TCP observations are retained by the explicit qualification command" +)] +fn record(app: &str, case: &str, replies: &[Reply], elapsed: Duration) { + std::println!( + "{}", + serde_json::json!({"event":"fallback-tcp","app":app,"case":case, + "responses":replies.iter().map(|reply|serde_json::json!({"status":reply.status, + "body":String::from_utf8_lossy(&reply.body),"allow":reply.values("allow").iter().map(|value|String::from_utf8_lossy(value).into_owned()).collect::>(), + "retry_after_count":reply.values("retry-after").len()})).collect::>(), + "response_complete_ms":elapsed.as_millis(),"status":"pass","qualification":false}) + ); +} + +pub fn assert_fallbacks(command: Command, app: &str, expected_health: &[u8]) { + let mut server = Server::start(command); + let health = b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"; + let (healthy, initial_elapsed) = server.exchange(health); + assert_eq!(healthy.len(), 1); + assert_eq!(healthy[0].status, 200); + assert_eq!( + healthy[0].body, expected_health, + "correct application's root response" + ); + let health_body = healthy[0].body.clone(); + record(app, "initial-health", &healthy, initial_elapsed); + for (path, fallback) in [("/missing", 404), ("/", 405)] { + for chunked in [false, true] { + for len in [4095, 4096, 4097] { + let (replies, elapsed) = server.exchange(post(path, len, chunked, true).as_bytes()); + assert_eq!(replies.len(), 1); + if len > 4096 { + assert_plain(&replies[0], 413, b"request body too large\n"); + } else { + assert_eq!(replies[0].status, fallback); + assert!(replies[0].values("retry-after").is_empty()); + if fallback == 405 { + assert_eq!(replies[0].values("allow"), [b"GET".as_slice()]); + } + } + record( + app, + &format!("{path}-{}-{len}", if chunked { "chunked" } else { "fixed" }), + &replies, + elapsed, + ); + } + } + } + let pending = b"POST /missing HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n"; + let (timed_out, timeout_elapsed) = server.exchange(pending); + assert_eq!(timed_out.len(), 1); + assert_plain(&timed_out[0], 408, b"request timeout\n"); + assert!( + timeout_elapsed >= Duration::from_millis(4500) && timeout_elapsed < IO_BUDGET, + "five-second fallback response deadline: {timeout_elapsed:?}" + ); + record( + app, + "pending-fallback-deadline", + &timed_out, + timeout_elapsed, + ); + // The remaining payload cannot have been consumed: send it only after the complete 413. + let prefix = format!( + "POST /missing HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n1001\r\n{}\r\n", + "x".repeat(4097) + ); + let disguised_request = "GET /echo/not-health HTTP/1.1\r\nHost: localhost\r\n\r\n"; + let tail = format!( + "{:x}\r\n{disguised_request}\r\n0\r\n\r\n{}", + disguised_request.len(), + str::from_utf8(health).expect("HTTP fixture") + ); + let (pipelined, pipeline_elapsed) = + server.exchange_after_response(prefix.as_bytes(), tail.as_bytes()); + assert_plain(&pipelined[0], 413, b"request body too large\n"); + if let Some(next) = pipelined.get(1) { + assert_eq!(next.status, 200); + assert_eq!(next.body, health_body); + } + record( + app, + "overflow-deferred-tail-close-or-health", + &pipelined, + pipeline_elapsed, + ); + let (recovered, recovery_elapsed) = server.exchange(health); + assert_eq!(recovered.len(), 1); + assert_eq!(recovered[0].status, 200); + assert_eq!(recovered[0].body, health_body); + record(app, "recovery-health", &recovered, recovery_elapsed); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn buffered_parser_preserves_response_boundaries_and_overflow_body() { + let wire = b"HTTP/1.1 413 Payload Too Large\r\nContent-Length: 23\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nrequest body too large\nHTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\napp"; + let replies = parse_replies(wire).expect("complete pipelined replies"); + assert_eq!(replies.len(), 2); + assert_eq!(replies[0].status, 413); + assert_eq!(replies[0].body, b"request body too large\n"); + assert_eq!(replies[0].headers[0].0, "Content-Length"); + assert_eq!(replies[1].status, 200); + assert_eq!(replies[1].body, b"app"); + } + + #[test] + fn buffered_parser_rejects_missing_ambiguous_and_truncated_evidence() { + for wire in [ + b"".as_slice(), + b"HTTP/1.1 413 Payload Too Large\r\nContent-Length: 23\r\n\r\nshort", + b"HTTP/1.1 413 Payload Too Large\r\n\r\n", + b"HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\nContent-Length: 0\r\n\r\n", + b"HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n\r\n", + b"HTTP/1.1 413 Payload Too Large\r\nContent-Length: 999999\r\n\r\n", + b"HTTP/1.1 413 Payload Too Large\r\nContent-Length: -1\r\n\r\n", + b"HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\n\r\nHTTP/1.1 200", + ] { + assert!(parse_replies(wire).is_err(), "{wire:?}"); + } + } +} diff --git a/crates/edgezero-core/Cargo.toml b/crates/edgezero-core/Cargo.toml index 80bc206fa..f3d13dd8b 100644 --- a/crates/edgezero-core/Cargo.toml +++ b/crates/edgezero-core/Cargo.toml @@ -12,11 +12,20 @@ workspace = true [dependencies] edgezero-macros = { path = "../edgezero-macros" } +alloc-no-stdlib = { workspace = true } +alloc-stdlib = { workspace = true } +adler2 = { workspace = true } anyhow = { workspace = true } async-compression = { workspace = true } async-stream = { workspace = true } async-trait = { workspace = true } +brotli = { workspace = true } +brotli-decompressor = { workspace = true } bytes = { workspace = true } +compression-codecs = { workspace = true } +compression-core = { workspace = true } +crc32fast = { workspace = true } +flate2 = { workspace = true } futures = { workspace = true } futures-util = { workspace = true } http = { workspace = true } @@ -33,8 +42,11 @@ thiserror = { workspace = true } toml = { workspace = true } tower-service = { workspace = true } tracing = { workspace = true } +url = { workspace = true } validator = { workspace = true } log = { workspace = true } +miniz_oxide = { workspace = true } +simd-adler32 = { workspace = true } # `web-time` is intentionally unconditional: `std::time::Instant` is # unavailable on `wasm32-unknown-unknown` (Cloudflare Workers target). # `web_time::Instant` is a zero-cost drop-in on native and a JS-backed @@ -48,6 +60,4 @@ web-time = { workspace = true } test-utils = [] [dev-dependencies] -brotli = { workspace = true } -flate2 = { workspace = true } tempfile = { workspace = true } diff --git a/crates/edgezero-core/src/app.rs b/crates/edgezero-core/src/app.rs index fc0b6e413..019dafd46 100644 --- a/crates/edgezero-core/src/app.rs +++ b/crates/edgezero-core/src/app.rs @@ -1,5 +1,32 @@ -use crate::manifest::ResolvedLoggingConfig; -use crate::router::RouterService; +use std::sync::Arc; + +use crate::body::Body; +use crate::config_store::ConfigExtractionLimits; +use crate::error::{ + EdgeError, RequestBodyOverflowResponse, mark_body_overflow_response, + suppress_overflow_retry_after, +}; +use crate::http::header::CONTENT_TYPE; +use crate::http::{HeaderMap, HeaderValue, Method, Request, Response, StatusCode}; +use crate::ingress::{ + AdmissionDecision, IngressAdmissionOutcome, IngressAdmissionPolicy, IngressBeginOutcome, + IngressDispatchOutcome, IngressFraming, IngressHead, IngressHeadAccounting, IngressHeadLimits, + IngressHeadParts, PreparedIngress, apply_admission_policy, default_admission_policy, +}; +use crate::logging::BOOT_LOG_TARGET; +use crate::manifest::{BakedManifest, ResolvedLoggingConfig}; +use crate::platform::PlatformMetadata; +use crate::response::IntoResponse as _; +use crate::response_egress::{ + DEFAULT_RESPONSE_WRITE_BUDGET, DetachedResponseEgressDecision, + DetachedResponseEgressDecisionFactory, DetachedResponseEgressHead, ResponseEgressCompletion, + ResponseEgressDeadline, ResponseEgressEnvelope, ResponseEgressHead, ResponseEgressObserver, + ResponseEgressObserverHandle, ResponseEgressPolicy, ResponseEgressPolicyCallback, + ResponseEgressRequestMetadata, default_response_egress_policy, +}; +use crate::response_headers::{SynthesizedResponseHeaders, SynthesizedResponseHeadersError}; +use crate::router::{RouteMetadata, RouterService}; +use crate::time::{MonotonicClock, MonotonicInstant}; /// Canonical adapter name for the Axum adapter. pub const AXUM_ADAPTER: &str = "axum"; @@ -10,14 +37,171 @@ const DEFAULT_APP_NAME: &str = "EdgeZero App"; pub const FASTLY_ADAPTER: &str = "fastly"; /// Canonical adapter name for the Spin adapter. pub const SPIN_ADAPTER: &str = "spin"; +type ErrorResponseRenderer = Arc Response + Send + Sync + 'static>; /// Lightweight container around a `RouterService` that can be extended via hook implementations. pub struct App { + config_extraction_limits: ConfigExtractionLimits, + detached_response_egress_decision_factory: DetachedResponseEgressDecisionFactory, + error_response_renderer: ErrorResponseRenderer, + ingress_head_limits: IngressHeadLimits, + ingress_policy: IngressAdmissionPolicy, + monotonic_clock: MonotonicClock, name: String, + platform: PlatformMetadata, + response_egress_observer: ResponseEgressObserverHandle, + response_egress_policy: ResponseEgressPolicyCallback, router: RouterService, + synthesized_response_headers: SynthesizedResponseHeaders, } impl App { + /// Runs the configured body-blind admission policy exactly once. + /// + /// # Errors + /// Returns an internal policy error if the resulting absolute deadline cannot be + /// normalized safely or if fallback draining is selected for a matched route. + #[inline] + pub fn admit_ingress(&self, head: &IngressHead) -> Result { + match apply_admission_policy(&self.ingress_policy, head, self.monotonic_clock())? { + IngressAdmissionOutcome::Admitted { + completion, + ingress, + } => Ok(IngressAdmissionOutcome::Admitted { + completion, + ingress: ingress.with_config_extraction_limits(self.config_extraction_limits), + }), + IngressAdmissionOutcome::Refused { + completion, + response, + } => Ok(IngressAdmissionOutcome::Refused { + completion, + response, + }), + IngressAdmissionOutcome::Aborted => Ok(IngressAdmissionOutcome::Aborted), + } + } + + /// Converts a failure that occurred after successful admission into app-owned egress. + /// + /// Consuming `prepared` releases its single-use grant exactly once while retaining the + /// admitted request metadata in the resulting response-egress attempt. + /// + #[must_use] + #[inline] + pub fn admitted_error_egress( + &self, + prepared: PreparedIngress, + error: EdgeError, + ) -> ResponseEgressEnvelope { + let request_method = prepared.request_method().clone(); + let request_start = prepared.request_start(); + let route = prepared.route_metadata().cloned(); + let (_resolved, _admitted, completion) = prepared.into_parts(); + self.response_egress_envelope( + self.render_error_response(error), + request_method, + request_start, + route, + completion, + ) + .synthesized() + } + + /// Prepares canonical assembly-error egress without constructing or retaining an application. + /// + /// Logs the original error once on the boot target and uses the default redacted renderer. + /// No configured application callbacks or headers apply. The finite default write budget + /// starts at `request_start`; both that instant and the supplied clock must share a domain. + #[must_use] + #[inline] + pub fn assembly_error_egress( + error: EdgeError, + request_method: Method, + request_start: MonotonicInstant, + clock: MonotonicClock, + ) -> ResponseEgressEnvelope { + log::error!(target: BOOT_LOG_TARGET, "application assembly failed: {error:?}"); + let deadline = DetachedResponseEgressHead::new( + &request_method, + request_start, + error.status(), + error.kind(), + ) + .write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET); + let mut response = render_default_error_response(error); + response + .extensions_mut() + .insert(ResponseEgressDeadline::at(deadline)); + ResponseEgressEnvelope::detached( + response, + request_start, + request_method, + ResponseEgressCompletion::empty(), + clock, + ) + } + + /// Resolves and admits a normalized request head before an adapter transfers native body + /// ownership into a core [`Request`]. + /// + /// # Errors + /// Returns an internal policy error if the admission deadline cannot be normalized safely + /// or if fallback draining is selected for a matched route. + #[inline] + pub fn begin_ingress( + &self, + head_parts: IngressHeadParts, + request_start: MonotonicInstant, + ) -> Result { + let request_method = head_parts.method().clone(); + let resolved = self + .router + .resolve(head_parts.method(), head_parts.target().path()); + let head = head_parts.into_head(request_start, resolved.resolution().clone()); + + match self.admit_ingress(&head)? { + IngressAdmissionOutcome::Admitted { + completion, + ingress, + } => Ok(IngressBeginOutcome::Admitted(PreparedIngress::new( + resolved, ingress, completion, + ))), + IngressAdmissionOutcome::Refused { + completion, + response, + } => Ok(IngressBeginOutcome::Refused(self.detached_response_egress( + response, + request_method, + request_start, + completion, + ))), + IngressAdmissionOutcome::Aborted => Ok(IngressBeginOutcome::Aborted), + } + } + + /// Construct an application from hooks after installing target metadata. + /// + /// Route failure skips configuration. Both this and [`App::from_router`] configure only + /// after installing the selected platform, whose complete metadata must be preserved. + /// + /// # Errors + /// Returns the typed route/configuration error, or an internal error when configuration replaces + /// the selected platform metadata, without exposing a partially configured app. + #[inline] + pub fn build(platform: PlatformMetadata) -> Result + where + A: Hooks, + { + Self::from_router::(A::routes()?, platform) + } + + #[must_use] + #[inline] + pub fn config_extraction_limits(&self) -> ConfigExtractionLimits { + self.config_extraction_limits + } + /// Default name used when none is provided. #[must_use] #[inline] @@ -25,6 +209,162 @@ impl App { DEFAULT_APP_NAME } + /// Chooses detached egress or a non-response abort for a pre-admission failure. + /// + /// A sent response uses the adapter response converter without invoking the application's + /// response-egress policy or observer. An abort constructs no response. + /// + #[must_use] + #[inline] + pub fn detached_ingress_error_egress( + &self, + error: EdgeError, + request_method: Method, + request_start: MonotonicInstant, + ) -> IngressDispatchOutcome { + let head = DetachedResponseEgressHead::new( + &request_method, + request_start, + error.status(), + error.kind(), + ); + match (self.detached_response_egress_decision_factory)(&head) { + DetachedResponseEgressDecision::Abort => IngressDispatchOutcome::Aborted, + DetachedResponseEgressDecision::Send { + completion, + deadline, + } => { + let mut response = self.render_error_response(error); + response + .extensions_mut() + .insert(ResponseEgressDeadline::at(deadline)); + IngressDispatchOutcome::Response(Box::new( + self.detached_response_egress( + response, + request_method, + request_start, + completion, + ) + .synthesized(), + )) + } + } + } + + fn detached_response_egress( + &self, + response: Response, + request_method: Method, + request_start: MonotonicInstant, + completion: ResponseEgressCompletion, + ) -> ResponseEgressEnvelope { + ResponseEgressEnvelope::detached( + response, + request_start, + request_method, + completion, + self.monotonic_clock(), + ) + .with_synthesized_response_headers(self.synthesized_response_headers.clone()) + } + + /// Dispatches a request whose native body was wrapped after admission. + /// + #[inline] + pub async fn dispatch_admitted( + &self, + prepared: PreparedIngress, + request: Request, + ) -> ResponseEgressEnvelope { + let request_method = request.method().clone(); + let request_start = prepared.request_start(); + let route = prepared.route_metadata().cloned(); + let (resolved, admitted, completion) = prepared.into_parts(); + let (response, synthesized) = match self + .router + .dispatch_resolved(resolved, request, admitted) + .await + { + Ok(response) => (response, false), + Err(error) => (self.render_error_response(error), route.is_none()), + }; + let envelope = self.response_egress_envelope( + response, + request_method, + request_start, + route, + completion, + ); + if synthesized { + envelope.synthesized() + } else { + envelope + } + } + + /// Resolves, admits, and dispatches one normalized inbound request into owned egress. + /// + /// Adapters must call this before polling the request body. A response outcome owns its + /// envelope through the transmission boundary; an aborted outcome produces no HTTP response. + /// The route selected for admission is consumed during dispatch, so handlers cannot observe a + /// different route identity. + /// + /// # Errors + /// Returns an error only when admission cannot begin. Handler, routing, body-read, and + /// post-admission conversion errors are converted with the app's error-response renderer. + #[inline] + pub async fn dispatch_ingress( + &self, + request: Request, + request_start: MonotonicInstant, + head_accounting: IngressHeadAccounting, + framing: IngressFraming, + ) -> Result { + let head_parts = IngressHeadParts::from_request(&request, head_accounting, framing); + match self.begin_ingress(head_parts, request_start)? { + IngressBeginOutcome::Admitted(prepared) => Ok(IngressDispatchOutcome::Response( + Box::new(self.dispatch_admitted(prepared, request).await), + )), + IngressBeginOutcome::Refused(response) => { + Ok(IngressDispatchOutcome::Response(Box::new(response))) + } + IngressBeginOutcome::Aborted => Ok(IngressDispatchOutcome::Aborted), + } + } + + /// Assembles an application from routes and state built by the caller. + /// + /// Applies the same name, configuration and platform check as [`App::build`], exactly once, + /// without invoking `A::routes`. Finish asynchronous/provider-specific state loading first. + /// Only a completely configured application is returned or eligible for retention. + /// + /// # Errors + /// Returns the original configuration error, or an internal error if successful configuration + /// replaces the selected platform metadata. + #[inline] + pub fn from_router( + router: RouterService, + platform: PlatformMetadata, + ) -> Result + where + A: Hooks, + { + let mut app = Self::with_name_and_platform(router, A::name(), platform); + A::configure(&mut app)?; + if app.platform() != platform { + return Err(EdgeError::internal(anyhow::anyhow!( + "application configuration changed adapter-selected platform metadata" + ))); + } + Ok(app) + } + + #[must_use] + #[inline] + pub fn ingress_head_limits(&self) -> IngressHeadLimits { + self.ingress_head_limits + } + /// Consume the app and return the contained router service. #[must_use] #[inline] @@ -32,6 +372,20 @@ impl App { self.router } + /// Returns the application clock used to stamp and enforce admitted ingress lifetimes. + #[must_use] + #[inline] + pub fn monotonic_clock(&self) -> MonotonicClock { + self.monotonic_clock.clone() + } + + /// Captures one instant from the application clock. + #[must_use] + #[inline] + pub fn monotonic_now(&self) -> MonotonicInstant { + self.monotonic_clock.now() + } + /// Name assigned to the application. #[must_use] #[inline] @@ -46,6 +400,66 @@ impl App { Self::with_name(router, DEFAULT_APP_NAME) } + /// Target properties installed before application configuration. + #[must_use] + #[inline] + pub const fn platform(&self) -> PlatformMetadata { + self.platform + } + + fn render_error_response(&self, error: EdgeError) -> Response { + let status = error.status(); + let overflow_origin = error.request_body_overflow_origin(); + let required_headers = match error.required_response_headers() { + Ok(headers) => headers, + Err(header_error) => return render_default_error_response(header_error), + }; + let mut response = (self.error_response_renderer)(error); + *response.status_mut() = status; + response.headers_mut().extend(required_headers); + response + .extensions_mut() + .remove::(); + if let Some(origin) = overflow_origin { + mark_body_overflow_response(&mut response, origin); + } + suppress_overflow_retry_after(&mut response); + response + } + + fn response_egress_envelope( + &self, + response: Response, + request_method: Method, + request_start: MonotonicInstant, + route: Option, + completion: ResponseEgressCompletion, + ) -> ResponseEgressEnvelope { + ResponseEgressEnvelope::new( + response, + ResponseEgressRequestMetadata::new(request_method, request_start, route), + completion, + self.response_egress_policy(), + self.response_egress_observer(), + self.monotonic_clock(), + ) + .with_synthesized_response_headers(self.synthesized_response_headers.clone()) + } + + /// Returns an owned handle to the configured terminal response-egress observer. + #[must_use] + #[inline] + pub fn response_egress_observer(&self) -> ResponseEgressObserverHandle { + self.response_egress_observer.clone() + } + + /// Returns an owned handle to the configured synchronous response-egress policy callback. + #[must_use] + #[inline] + pub fn response_egress_policy(&self) -> ResponseEgressPolicyCallback { + Arc::clone(&self.response_egress_policy) + } + /// Access the underlying router service. #[must_use] #[inline] @@ -53,6 +467,67 @@ impl App { &self.router } + /// Installs validated typed-config extraction limits. + /// + /// # Errors + /// Returns an internal startup-policy error when limits are zero, inconsistent, or unbounded. + #[inline] + pub fn set_config_extraction_limits( + &mut self, + limits: ConfigExtractionLimits, + ) -> Result<(), EdgeError> { + self.config_extraction_limits = limits.validate()?; + Ok(()) + } + + /// Installs the synchronous decision factory for detached normalized-ingress errors. + #[inline] + pub fn set_detached_response_egress_decision_factory(&mut self, factory: Factory) + where + Factory: for<'head> Fn(&DetachedResponseEgressHead<'head>) -> DetachedResponseEgressDecision + + Send + + Sync + + 'static, + { + self.detached_response_egress_decision_factory = Arc::new(factory); + } + + /// Installs the synchronous renderer for app-owned framework and handler errors. + /// + /// The renderer is called exactly once for admitted dispatch errors and detached normalized + /// ingress errors. Applications that require a bounded wire format should return a fixed-size + /// response here. `EdgeZero` reapplies the error status and mandatory protocol headers after the + /// callback. Application-built admission refusals bypass this hook. Adapter-built egress + /// fallbacks also bypass it: fallback construction never executes application code. + #[inline] + pub fn set_error_response_renderer(&mut self, renderer: Renderer) + where + Renderer: Fn(EdgeError) -> Response + Send + Sync + 'static, + { + self.error_response_renderer = Arc::new(renderer); + } + + /// Installs the synchronous body-blind ingress admission callback. + #[inline] + pub fn set_ingress_admission_policy(&mut self, policy: Policy) + where + Policy: Fn(&IngressHead) -> AdmissionDecision + Send + Sync + 'static, + { + self.ingress_policy = Arc::new(policy); + } + + /// Installs already-validated finite request-head limits. + #[inline] + pub fn set_ingress_head_limits(&mut self, limits: IngressHeadLimits) { + self.ingress_head_limits = limits; + } + + /// Installs the monotonic clock used by subsequent ingress attempts. + #[inline] + pub fn set_monotonic_clock(&mut self, clock: MonotonicClock) { + self.monotonic_clock = clock; + } + /// Update the application name. #[inline] pub fn set_name(&mut self, name: S) @@ -62,15 +537,95 @@ impl App { self.name = name.into(); } - /// Construct a new application with the provided router and name. + /// Installs the terminal response-egress observer used by adapter conversion attempts. + #[inline] + pub fn set_response_egress_observer(&mut self, observer: Observer) + where + Observer: ResponseEgressObserver, + { + self.response_egress_observer = ResponseEgressObserverHandle::new(observer); + } + + /// Installs the synchronous body-blind response-egress policy callback. + #[inline] + pub fn set_response_egress_policy(&mut self, policy: Policy) + where + Policy: for<'head> Fn(&ResponseEgressHead<'head>, MonotonicInstant) -> ResponseEgressPolicy + + Send + + Sync + + 'static, + { + self.response_egress_policy = Arc::new(policy); + } + + /// Validates and installs fields for framework-generated errors and adapter fallbacks. + /// + /// Routed responses, admission refusals and assembly failures are not decorated. + /// + /// # Errors + /// Returns a typed construction error without replacing the previous valid set. + #[inline] + pub fn set_synthesized_response_headers( + &mut self, + headers: HeaderMap, + ) -> Result<(), SynthesizedResponseHeadersError> { + self.synthesized_response_headers = SynthesizedResponseHeaders::new(headers)?; + Ok(()) + } + + /// Borrows the registered state of exactly this type from the router's shared registry. + /// + /// Configuration, entrypoints and [`crate::extractor::State`] use this same source. + /// Register shared values as `Arc` and retrieve that exact type; no state is rebuilt here. + #[must_use] + #[inline] + pub fn state(&self) -> Option<&T> + where + T: Send + Sync + 'static, + { + self.router.state::() + } + + /// Construct a low-level application without invoking hooks. + /// Use [`App::from_router`] to apply application configuration to a supplied router. #[inline] pub fn with_name(router: RouterService, name: S) -> Self + where + S: Into, + { + Self::with_name_and_platform(router, name, PlatformMetadata::default()) + } + + /// Construct a low-level application with name/metadata but without invoking hooks. + /// Use [`App::from_router`] for fully configured manual application assembly. + #[must_use] + #[inline] + pub fn with_name_and_platform( + router: RouterService, + name: S, + platform: PlatformMetadata, + ) -> Self where S: Into, { Self { - router, + config_extraction_limits: ConfigExtractionLimits::default(), + detached_response_egress_decision_factory: Arc::new(|head| { + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::empty(), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), + } + }), + error_response_renderer: Arc::new(render_default_error_response), + ingress_head_limits: IngressHeadLimits::default(), + ingress_policy: default_admission_policy(), + monotonic_clock: MonotonicClock::default(), name: name.into(), + platform, + response_egress_observer: ResponseEgressObserverHandle::default(), + response_egress_policy: Arc::new(default_response_egress_policy), + router, + synthesized_response_headers: SynthesizedResponseHeaders::default(), } } } @@ -104,22 +659,17 @@ pub struct StoresMetadata { /// Trait implemented by application hook adapters. pub trait Hooks { - /// Construct an `App` by wiring the routes and invoking the configuration hook. - #[must_use] - #[inline] - fn build_app() -> App - where - Self: Sized, - { - let mut app = App::with_name(Self::routes(), Self::name()); - Self::configure(&mut app); - app - } - /// Allow implementations to mutate the freshly constructed application before use. /// The default implementation performs no changes. + /// Successful configuration must preserve the adapter-selected [`PlatformMetadata`]. + /// [`App::build`] rejects changed metadata rather than silently restoring it. + /// + /// # Errors + /// Returns a typed startup-policy error when configuration cannot be completed. #[inline] - fn configure(_app: &mut App) {} + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Ok(()) + } /// Logging settings for one adapter, baked from the application manifest. /// @@ -132,6 +682,23 @@ pub trait Hooks { ResolvedLoggingConfig::default() } + /// Parsed and finalized manifest contract baked by `app!`. + /// + /// The default is deliberately uncached: every macro-generated application + /// supplies its own per-implementation cache. + #[must_use] + #[inline] + fn manifest() -> BakedManifest { + BakedManifest::Absent + } + + /// Raw manifest JSON baked at compile time by `app!`. + #[must_use] + #[inline] + fn manifest_json() -> Option<&'static str> { + None + } + /// Display name for the application. Defaults to `"EdgeZero App"`. #[must_use] #[inline] @@ -139,16 +706,29 @@ pub trait Hooks { App::default_name() } - /// When `true`, an adapter's `run_app` skips its own logger initialization; - /// the app is responsible for installing a `log` backend. Default `false`. + /// When `true`, an adapter's entrypoint skips its own logger initialization + /// and must not reconfigure the application's backend or facade maximum. + /// Default `false`. + /// + /// The application must install its `log` backend before the adapter + /// entrypoint and apply configured levels to both backend filters and the + /// facade maximum. [`crate::logging::resolve_logging_level`] resolves the + /// runtime level without mutating logging state. Startup diagnostics use + /// [`crate::logging::BOOT_LOG_TARGET`]; preserving those warnings requires + /// a separate backend target filter and a facade maximum that admits them. + /// Setting only `log::set_max_level` cannot restore backend-filtered records. #[must_use] #[inline] fn owns_logging() -> bool { false } - /// Build the router service for the application. - fn routes() -> RouterService; + /// Build routes and their application-owned state. + /// + /// # Errors + /// Returns the original state/route assembly error. [`App::build`] skips configuration and + /// returns no application on failure; callers must not retain an error-response router. + fn routes() -> Result; /// Portable store metadata for the application. /// @@ -162,28 +742,122 @@ pub trait Hooks { } } +fn render_default_error_response(error: EdgeError) -> Response { + match error.into_response() { + Ok(response) => response, + Err(render_error) => { + log::error!("failed to render typed ingress error response: {render_error}"); + // Keep the last-resort response independent of JSON serialization. + let mut response = Response::new(Body::from( + r#"{"error":{"kind":"internal","message":"internal server error","status":500}}"#, + )); + *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + response + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + response + } + } +} + #[cfg(test)] mod tests { + use crate::response_egress_framing::ResponseEgressFramingMode; + use crate::response_egress_framing::prepare_response_egress; + use std::num::NonZeroU32; + use std::sync::Arc; + use std::sync::Mutex; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::{Context, Poll}; + use std::time::Duration; + use std::{future::Future as _, str}; + use super::*; use crate::body::Body; + use crate::config_store::ConfigExtractionLimits; use crate::context::RequestContext; - use crate::error::EdgeError; - use crate::http::{Method, StatusCode, request_builder}; - use crate::manifest::LogLevel; + use crate::error::{EdgeError, StoreExtractionReason}; + use crate::http::{ + HeaderMap, HeaderValue, Method, StatusCode, Version, request_builder, response_builder, + }; + use crate::ingress::{ + BufferedIngressResponse, IngressBeginOutcome, IngressDispatchOutcome, IngressFraming, + IngressGrant, IngressHeadAccounting, IngressHeadParts, + }; + use crate::manifest::{BakedManifest, LogLevel}; + use crate::middleware::{Middleware, Next}; + use crate::platform::{ + HostIngressMemoryAccounting, InboundRequestPopulationBound, MemoryCeiling, + MemoryCeilingScope, PlatformFact, PlatformMetadata, PlatformResourceSource, + PlatformUnknownReason, + }; + use crate::response_egress::{ + DEFAULT_RESPONSE_WRITE_BUDGET, DetachedResponseEgressDecision, ResponseEgressAttempt, + ResponseEgressHead, ResponseEgressObserver, ResponseEgressOutcome, ResponseEgressPolicy, + ResponseEgressReport, + }; + use crate::router::{RouteMetadata, RouteResolution}; + use crate::time::{DEADLINE_FAR_FUTURE, Deadline, MonotonicClock, MonotonicInstant}; + use async_trait::async_trait; + use bytes::Bytes; + use futures::StreamExt as _; use futures::executor::block_on; + use futures::stream::{iter, poll_fn}; + use futures::task::noop_waker_ref; use tower_service::Service as _; struct DefaultHooks; + struct FailingHooks; + + struct MemoryAwareHooks; + + struct ReplacingHooks; + + struct ReplacingFailingHooks; + + struct MemoryPreservingReplacingHooks; + struct TestHooks; + struct CountingMiddleware(Arc); + + struct GrantDropProbe(Arc); + + #[derive(Clone)] + struct AppEgressObserver(Arc>>); + + impl ResponseEgressObserver for AppEgressObserver { + fn complete(&self, report: &ResponseEgressReport) { + self.0.lock().expect("reports lock").push(report.clone()); + } + } + + #[async_trait(?Send)] + impl Middleware for CountingMiddleware { + async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result { + self.0.fetch_add(1, Ordering::SeqCst); + next.run(ctx).await + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for GrantDropProbe { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + #[expect( clippy::missing_trait_methods, reason = "test stub — only `routes` is overridden; every other Hooks method intentionally uses its trait default" )] impl Hooks for DefaultHooks { - fn routes() -> RouterService { - RouterService::builder().build() + fn routes() -> Result { + Ok(RouterService::builder().build()) } fn stores() -> StoresMetadata { @@ -191,25 +865,119 @@ mod tests { } } + #[expect( + clippy::missing_trait_methods, + reason = "test stub exercises only fallible application configuration" + )] + impl Hooks for FailingHooks { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Err(EdgeError::service_unavailable("configuration unavailable")) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "test stub exercises platform metadata visibility during configuration" + )] + impl Hooks for MemoryAwareHooks { + fn configure(app: &mut App) -> Result<(), EdgeError> { + let expected = PlatformFact::known( + MemoryCeiling::new(128_000_000, MemoryCeilingScope::PerInstance, None), + PlatformResourceSource::PlatformLimit { + provider: "test-platform", + }, + ); + if app.platform().memory_ceiling() == expected { + Ok(()) + } else { + Err(EdgeError::service_unavailable( + "platform metadata unavailable during configuration", + )) + } + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "test stub exercises replacement of adapter-selected metadata" + )] + impl Hooks for ReplacingHooks { + fn configure(app: &mut App) -> Result<(), EdgeError> { + *app = App::new(Self::routes()?); + Ok(()) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "test stub exercises error precedence after replacing the application" + )] + impl Hooks for ReplacingFailingHooks { + fn configure(app: &mut App) -> Result<(), EdgeError> { + *app = App::new(Self::routes()?); + Err(EdgeError::service_unavailable( + "replacement configuration failed", + )) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "test stub changes a platform fact other than the memory ceiling" + )] + impl Hooks for MemoryPreservingReplacingHooks { + fn configure(app: &mut App) -> Result<(), EdgeError> { + let platform = app.platform(); + let changed = PlatformMetadata::new( + platform.memory_ceiling(), + PlatformFact::unknown(PlatformUnknownReason::OperatorConfigured), + platform.host_ingress_memory_accounting(), + ); + *app = App::build::(changed)?; + Ok(()) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + #[expect( clippy::missing_trait_methods, reason = "test stub — `build_app` intentionally uses the trait default; other methods are overridden for test coverage" )] impl Hooks for TestHooks { - fn configure(app: &mut App) { + fn configure(app: &mut App) -> Result<(), EdgeError> { app.set_name("configured"); + Ok(()) } fn name() -> &'static str { "hooks-name" } - fn routes() -> RouterService { + fn routes() -> Result { async fn handler(_ctx: RequestContext) -> Result { Ok("ok".to_owned()) } - RouterService::builder().get("/test", handler).build() + Ok(RouterService::builder().get("/test", handler).build()) } fn stores() -> StoresMetadata { @@ -227,31 +995,244 @@ mod tests { } } - fn empty_router() -> RouterService { - RouterService::builder().build() + fn assert_buffered_terminal_response( + response: &Response, + status: StatusCode, + marker: &str, + body: &[u8], + ) { + assert_eq!(response.status(), status); + assert_eq!( + response + .headers() + .get("x-fallback-terminal") + .expect("terminal marker"), + marker + ); + assert_eq!(response.body().as_bytes().expect("buffered body"), body); } - #[test] - fn app_can_be_retained_in_a_shared_owner() { - fn assert_send_sync() - where - T: Send + Sync, - { - } - assert_send_sync::(); + fn assert_no_fallback_dispatch(handler_calls: &AtomicUsize, middleware_calls: &AtomicUsize) { + assert_eq!(handler_calls.load(Ordering::SeqCst), 0); + assert_eq!(middleware_calls.load(Ordering::SeqCst), 0); } - #[test] - fn build_app_invokes_hooks_for_routes_and_configuration() { - let app = TestHooks::build_app(); - assert_eq!(app.name(), "configured"); - let stores = TestHooks::stores(); - let config = stores.config.expect("config store metadata"); - assert_eq!(config.default, "app_config"); - assert_eq!(config.ids, &["app_config"]); - let kv = stores.kv.expect("kv store metadata"); - assert_eq!(kv.default, "sessions"); - assert_eq!(kv.ids, &["sessions", "cache"]); + fn buffered_terminal( + status: StatusCode, + marker: &'static str, + body: &'static [u8], + ) -> BufferedIngressResponse { + let mut headers = HeaderMap::new(); + headers.insert("x-fallback-terminal", HeaderValue::from_static(marker)); + BufferedIngressResponse::new(status, headers, Bytes::from_static(body)) + } + + fn configure_fallback_policy( + app: &mut App, + drops: &Arc, + max_body_bytes: usize, + read_deadline: Deadline, + on_exceeded: BufferedIngressResponse, + on_timeout: BufferedIngressResponse, + ) { + let grant_drops = Arc::clone(drops); + app.set_ingress_admission_policy(move |_| AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(GrantDropProbe(Arc::clone(&grant_drops))), + max_body_bytes, + read_deadline, + on_exceeded: on_exceeded.clone(), + on_timeout: on_timeout.clone(), + }); + } + + fn dispatch_fallback(app: &App, body: Body) -> Response { + let request = request_builder() + .method(Method::POST) + .uri("/known") + .body(body) + .expect("request"); + let outcome = block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("response"); + complete_dispatch(outcome) + } + + fn complete_envelope(envelope: ResponseEgressEnvelope) -> Response { + let Ok((prepared, _, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("response egress begin"); + }; + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + prepared.into_response() + } + + fn complete_dispatch(outcome: IngressDispatchOutcome) -> Response { + let IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("expected response"); + }; + complete_envelope(*envelope) + } + + fn dispatch_request(app: &App, method: Method, target: &str, body: Body) -> Response { + complete_dispatch( + block_on( + app.dispatch_ingress( + request_builder() + .method(method) + .uri(target) + .body(body) + .expect("request"), + app.monotonic_now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ), + ) + .expect("dispatch"), + ) + } + + fn counted_completion(calls: &Arc) -> ResponseEgressCompletion { + let observed_calls = Arc::clone(calls); + ResponseEgressCompletion::new(move |_report| { + observed_calls.fetch_add(1, Ordering::SeqCst); + }) + } + + fn empty_router() -> RouterService { + RouterService::builder().build() + } + + fn guarded_fallback_app() -> (App, Arc, Arc) { + let handler_calls = Arc::new(AtomicUsize::new(0)); + let handler_counter = Arc::clone(&handler_calls); + let middleware_calls = Arc::new(AtomicUsize::new(0)); + let router = RouterService::builder() + .get("/known", move |_ctx: RequestContext| { + let call_counter = Arc::clone(&handler_counter); + async move { + call_counter.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("unexpected") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware_calls))) + .build(); + (App::new(router), handler_calls, middleware_calls) + } + + #[test] + fn assembly_error_egress_preserves_status_redaction_and_clock() { + let request_start = MonotonicInstant::now(); + let started_at = request_start + .checked_add(Duration::from_secs(1)) + .expect("egress start"); + let completed_at = started_at + .checked_add(Duration::from_millis(5)) + .expect("egress end"); + for (error, expected_status, expected_kind, expected_message) in [ + ( + EdgeError::service_unavailable("configuration unavailable"), + StatusCode::SERVICE_UNAVAILABLE, + "service_unavailable", + "configuration unavailable", + ), + ( + EdgeError::internal(anyhow::anyhow!("private-token=secret")), + StatusCode::INTERNAL_SERVER_ERROR, + "internal", + "internal server error", + ), + ] { + let now = Arc::new(Mutex::new(started_at)); + let clock_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let envelope = App::assembly_error_egress(error, Method::GET, request_start, clock); + let Ok((prepared, policy, mut attempt, egress_clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("assembly error must enter canonical egress"); + }; + let response = prepared.into_response(); + assert_eq!(response.status(), expected_status); + assert_eq!(response.headers()["content-type"], "application/json"); + assert_eq!( + policy.write_deadline.instant(), + request_start + .checked_add(DEFAULT_RESPONSE_WRITE_BUDGET) + .unwrap() + ); + let bytes = block_on(response.into_body().into_bytes_bounded(2048)).unwrap(); + let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!( + body, + serde_json::json!({"error": { + "kind": expected_kind, "message": expected_message, + "status": expected_status.as_u16(), + }}) + ); + assert!(attempt.begin_writing()); + *now.lock().expect("clock lock") = completed_at; + assert!(attempt.complete(egress_clock.now())); + let report = attempt.into_terminal_report(); + assert_eq!(report.request_start, request_start); + assert_eq!(report.elapsed, Duration::from_millis(5)); + assert_eq!(report.outcome, ResponseEgressOutcome::Completed); + assert!(report.route.is_none()); + } + } + + #[test] + fn assembly_error_egress_suppresses_head_body_without_an_app() { + let clock = MonotonicClock::default(); + let envelope = App::assembly_error_egress( + EdgeError::service_unavailable("configuration unavailable"), + Method::HEAD, + clock.now(), + clock, + ); + let Ok((prepared, _, mut attempt, egress_clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("assembly error must enter canonical egress"); + }; + let response = prepared.into_response(); + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + assert!( + block_on(response.into_body().into_bytes_bounded(0)) + .unwrap() + .is_empty() + ); + assert!(attempt.begin_writing()); + assert!(attempt.complete(egress_clock.now())); + } + + #[test] + fn app_can_be_retained_in_a_shared_owner() { + fn assert_send_sync() + where + T: Send + Sync, + { + } + assert_send_sync::(); + } + + #[test] + fn build_app_invokes_hooks_for_routes_and_configuration() { + let app = App::build::(PlatformMetadata::default()).expect("configured app"); + assert_eq!(app.name(), "configured"); + let stores = TestHooks::stores(); + let config = stores.config.expect("config store metadata"); + assert_eq!(config.default, "app_config"); + assert_eq!(config.ids, &["app_config"]); + let kv = stores.kv.expect("kv store metadata"); + assert_eq!(kv.default, "sessions"); + assert_eq!(kv.ids, &["sessions", "cache"]); assert!(stores.secrets.is_none()); let request = request_builder() @@ -265,12 +1246,2111 @@ mod tests { assert_eq!(response.body().as_bytes().expect("buffered"), b"ok"); } + #[test] + fn route_assembly_failure_skips_configuration_and_preserves_typed_error() { + struct RouteFailure; + static CONFIGURE_CALLS: AtomicUsize = AtomicUsize::new(0); + #[expect( + clippy::missing_trait_methods, + reason = "tests route failure before configure" + )] + impl Hooks for RouteFailure { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + CONFIGURE_CALLS.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + fn routes() -> Result { + Err(EdgeError::service_unavailable("route state unavailable")) + } + } + let Err(error) = App::build::(PlatformMetadata::default()) else { + panic!("failed route assembly must not produce an app"); + }; + assert!(matches!(&error, EdgeError::ServiceUnavailable { message } + if message == "route state unavailable")); + assert_eq!(CONFIGURE_CALLS.load(Ordering::SeqCst), 0); + } + + #[test] + fn assembly_errors_preserve_the_original_internal_source() { + use std::sync::OnceLock; + static IDENTITY: OnceLock> = OnceLock::new(); + #[derive(Debug, thiserror::Error)] + #[error("private assembly source")] + struct SourceIdentity; + struct RouteFailure; + struct ConfigureFailure; + + fn original_error() -> EdgeError { + EdgeError::internal(Arc::clone(IDENTITY.get().unwrap())) + } + + #[expect( + clippy::missing_trait_methods, + reason = "tests original route error source" + )] + impl Hooks for RouteFailure { + fn routes() -> Result { + Err(original_error()) + } + } + #[expect( + clippy::missing_trait_methods, + reason = "tests original configure error source" + )] + impl Hooks for ConfigureFailure { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Err(original_error()) + } + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + let identity = Arc::new(SourceIdentity); + IDENTITY.set(Arc::clone(&identity)).unwrap(); + let platform = PlatformMetadata::default(); + for result in [ + App::build::(platform), + App::build::(platform), + App::from_router::(RouterService::builder().build(), platform), + ] { + let Err(error) = result else { + panic!("assembly must return the original failure"); + }; + let original = error + .inner() + .unwrap() + .downcast_ref::>() + .unwrap(); + assert!(Arc::ptr_eq(&identity, original)); + } + } + + #[test] + fn assembled_state_is_shared_with_configuration_and_canonical_dispatch() { + use crate::extractor::State; + struct StateHooks; + #[expect( + clippy::missing_trait_methods, + reason = "tests manual-router assembly and state" + )] + impl Hooks for StateHooks { + fn configure(app: &mut App) -> Result<(), EdgeError> { + let count = app + .state::>() + .ok_or_else(|| EdgeError::service_unavailable("missing registered state"))?; + count.fetch_add(1, Ordering::SeqCst); + let target_bytes = *app + .state::() + .ok_or_else(|| EdgeError::service_unavailable("missing request limit"))?; + app.set_ingress_head_limits( + IngressHeadLimits::default().with_max_request_target_bytes(target_bytes)?, + ); + app.set_name("configured from state"); + Ok(()) + } + + fn routes() -> Result { + Err(EdgeError::service_unavailable("manual router required")) + } + } + #[crate::action] + async fn state_handler(State(count): State>) -> Result { + Ok(count.fetch_add(1, Ordering::SeqCst).to_string()) + } + let original = Arc::new(AtomicUsize::new(99)); + let registered = Arc::new(AtomicUsize::new(0)); + let router = RouterService::builder() + .with_state(Arc::clone(&original)) + .with_state(Arc::clone(®istered)) + .with_state(64_u64) + .get("/state", state_handler) + .build(); + let router_clone = router.clone(); + let app = App::from_router::(router, PlatformMetadata::default()).unwrap(); + assert_eq!(app.name(), "configured from state"); + assert_eq!(app.ingress_head_limits().max_request_target_bytes(), 64); + assert!(Arc::ptr_eq( + app.state::>().unwrap(), + ®istered + )); + assert!(Arc::ptr_eq( + router_clone.state::>().unwrap(), + ®istered + )); + assert!(app.state::().is_none()); + assert_eq!(registered.load(Ordering::SeqCst), 1); + let request = request_builder().uri("/state").body(Body::empty()).unwrap(); + let Ok(IngressDispatchOutcome::Response(envelope)) = block_on(app.dispatch_ingress( + request, + app.monotonic_now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) else { + panic!("state route must produce a response"); + }; + let Ok((prepared, _, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("state response must be valid"); + }; + let response = prepared.into_response(); + assert_eq!(response.body().as_bytes().unwrap(), b"1"); + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + assert_eq!(registered.load(Ordering::SeqCst), 2); + assert_eq!(original.load(Ordering::SeqCst), 99); + } + + #[test] + fn manual_router_assembly_matches_hook_name_and_configuration() { + struct NamedHooks; + static CONFIGURE_CALLS: AtomicUsize = AtomicUsize::new(0); + #[expect(clippy::missing_trait_methods, reason = "tests both assembly paths")] + impl Hooks for NamedHooks { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + CONFIGURE_CALLS.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + fn name() -> &'static str { + "assembled app" + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + let platform = PlatformMetadata::new( + PlatformFact::unknown(PlatformUnknownReason::OperatorConfigured), + PlatformFact::unknown(PlatformUnknownReason::OperatorConfigured), + PlatformFact::unknown(PlatformUnknownReason::OperatorConfigured), + ); + let from_hooks = App::build::(platform).unwrap(); + assert_eq!(CONFIGURE_CALLS.load(Ordering::SeqCst), 1); + let from_router = + App::from_router::(NamedHooks::routes().unwrap(), platform).unwrap(); + assert_eq!(CONFIGURE_CALLS.load(Ordering::SeqCst), 2); + for app in [from_hooks, from_router] { + assert_eq!(app.name(), "assembled app"); + assert_eq!(app.platform(), platform); + } + } + + #[test] + fn manual_router_assembly_keeps_platform_checks_and_configuration_error_precedence() { + let platform = PlatformMetadata::new( + PlatformFact::unknown(PlatformUnknownReason::OperatorConfigured), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + PlatformFact::unknown(PlatformUnknownReason::OperatorConfigured), + ); + for (result, status) in [ + ( + App::from_router::(RouterService::builder().build(), platform), + StatusCode::INTERNAL_SERVER_ERROR, + ), + ( + App::from_router::( + RouterService::builder().build(), + platform, + ), + StatusCode::SERVICE_UNAVAILABLE, + ), + ( + App::from_router::( + RouterService::builder().build(), + platform, + ), + StatusCode::INTERNAL_SERVER_ERROR, + ), + ] { + let Err(error) = result else { + panic!("invalid configuration must not assemble an app"); + }; + assert_eq!(error.status(), status); + if status == StatusCode::SERVICE_UNAVAILABLE { + assert_eq!(error.message(), "replacement configuration failed"); + } else { + assert_eq!( + error.to_string(), + "internal error: application configuration changed adapter-selected platform metadata" + ); + } + } + } + + #[test] + fn build_app_propagates_configuration_failure() { + let Err(error) = App::build::(PlatformMetadata::default()) else { + panic!("configuration failure must stop application assembly"); + }; + assert_eq!(error.status(), StatusCode::SERVICE_UNAVAILABLE); + } + + #[test] + fn build_app_exposes_memory_ceiling_during_configuration() { + let source = PlatformResourceSource::PlatformLimit { + provider: "test-platform", + }; + let ceiling = MemoryCeiling::new(128_000_000, MemoryCeilingScope::PerInstance, None); + let population = InboundRequestPopulationBound::new(NonZeroU32::MIN); + let host_accounting = HostIngressMemoryAccounting::OutsideCeiling; + let platform = PlatformMetadata::new( + PlatformFact::known(ceiling, source), + PlatformFact::known(population, source), + PlatformFact::known(host_accounting, source), + ); + + let app = App::build::(platform).expect("configured app"); + + assert_eq!(app.platform(), platform); + assert_eq!( + app.platform().inbound_request_population_bound(), + PlatformFact::known(population, source) + ); + assert_eq!( + app.platform().host_ingress_memory_accounting(), + PlatformFact::known(host_accounting, source) + ); + } + + #[test] + fn build_app_rejects_replaced_platform_metadata() { + let platform = PlatformMetadata::new( + PlatformFact::known( + MemoryCeiling::new(128_000_000, MemoryCeilingScope::PerInstance, None), + PlatformResourceSource::PlatformLimit { + provider: "test-platform", + }, + ), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + ); + let Err(error) = App::build::(platform) else { + panic!("replaced platform metadata must stop application assembly"); + }; + assert_eq!(error.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + error.to_string(), + "internal error: application configuration changed adapter-selected platform metadata" + ); + } + + #[test] + fn build_app_preserves_hook_failure_after_platform_replacement() { + let platform = PlatformMetadata::new( + PlatformFact::unknown(PlatformUnknownReason::OperatorConfigured), + PlatformFact::unknown(PlatformUnknownReason::OperatorConfigured), + PlatformFact::unknown(PlatformUnknownReason::OperatorConfigured), + ); + let Err(error) = App::build::(platform) else { + panic!("configuration failure must stop application assembly"); + }; + assert_eq!(error.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(error.message(), "replacement configuration failed"); + } + + #[test] + fn build_app_compares_platform_facts_beyond_the_memory_ceiling() { + let platform = PlatformMetadata::new( + PlatformFact::known( + MemoryCeiling::new(128_000_000, MemoryCeilingScope::PerInstance, None), + PlatformResourceSource::PlatformLimit { + provider: "test-platform", + }, + ), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + ); + let Err(error) = App::build::(platform) else { + panic!("changing only population metadata must stop assembly"); + }; + assert_eq!(error.status(), StatusCode::INTERNAL_SERVER_ERROR); + } + + #[test] + fn build_app_accepts_unknown_platform_metadata() { + let app = App::build::(PlatformMetadata::default()).expect("default app"); + + assert_eq!(app.platform(), PlatformMetadata::default()); + assert_eq!( + app.platform().memory_ceiling(), + PlatformFact::unknown(PlatformUnknownReason::Unspecified) + ); + assert_eq!( + app.platform().inbound_request_population_bound(), + PlatformFact::unknown(PlatformUnknownReason::Unspecified) + ); + assert_eq!( + app.platform().host_ingress_memory_accounting(), + PlatformFact::unknown(PlatformUnknownReason::Unspecified) + ); + } + #[test] fn default_app_uses_constant_name() { let app = App::new(empty_router()); assert_eq!(app.name(), App::default_name()); } + #[test] + fn app_pairs_admitted_ingress_with_its_injected_clock() { + let start = MonotonicInstant::now(); + let now = Arc::new(Mutex::new(start)); + let observed_now = Arc::clone(&now); + let mut app = App::new(empty_router()); + app.set_monotonic_clock(MonotonicClock::new(move || { + *observed_now.lock().expect("clock lock") + })); + + let request_start = app.monotonic_now(); + assert_eq!(request_start, start); + let head = IngressHeadParts::new( + Method::GET, + "/".parse().expect("URI"), + Version::HTTP_11, + HeaderMap::new(), + ); + let IngressBeginOutcome::Admitted(prepared) = + app.begin_ingress(head, request_start).expect("admission") + else { + panic!("expected admission"); + }; + assert_eq!(prepared.request_start(), start); + + let advanced = start + .checked_add(Duration::from_secs(1)) + .expect("advanced instant"); + *now.lock().expect("clock lock") = advanced; + assert_eq!(prepared.monotonic_clock().now(), advanced); + } + + #[test] + fn app_owns_default_and_configurable_response_egress_policy() { + let mut app = App::new(empty_router()); + let headers = HeaderMap::new(); + let request_start = MonotonicInstant::now(); + let egress_started_at = request_start + .checked_add(Duration::from_millis(5)) + .expect("egress start"); + let route = RouteMetadata::new(Method::GET, "/items/{id}"); + let head = ResponseEgressHead::new( + StatusCode::OK, + Version::HTTP_11, + &headers, + None, + request_start, + Some(&route), + ); + + let default_policy = app.response_egress_policy(); + assert_eq!( + default_policy(&head, egress_started_at) + .write_deadline + .instant(), + egress_started_at + .checked_add(DEFAULT_RESPONSE_WRITE_BUDGET) + .expect("default deadline") + ); + + app.set_response_egress_policy(move |response_head, started_at| { + assert_eq!(response_head.status(), StatusCode::OK); + assert_eq!(response_head.request_start(), request_start); + assert_eq!( + response_head.route().map(RouteMetadata::pattern), + Some("/items/{id}") + ); + assert_eq!(started_at, egress_started_at); + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(started_at), + } + }); + let configured_policy = app.response_egress_policy(); + assert_eq!( + configured_policy(&head, egress_started_at) + .write_deadline + .instant(), + egress_started_at + ); + } + + #[test] + fn app_owns_configurable_response_egress_observer() { + let reports = Arc::new(Mutex::new(Vec::new())); + let mut app = App::new(empty_router()); + app.set_response_egress_observer(AppEgressObserver(Arc::clone(&reports))); + + let started_at = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let head = ResponseEgressHead::new( + StatusCode::NO_CONTENT, + Version::HTTP_11, + &headers, + None, + started_at, + None, + ); + let mut attempt = ResponseEgressAttempt::new( + &head, + started_at, + ResponseEgressCompletion::empty(), + app.response_egress_observer(), + app.monotonic_clock(), + ); + assert!(attempt.begin_writing()); + assert!(attempt.complete(started_at)); + + let observed_reports = reports.lock().expect("reports lock"); + assert_eq!(observed_reports.len(), 1); + assert_eq!( + observed_reports[0].outcome, + ResponseEgressOutcome::Completed + ); + } + + #[test] + fn config_extraction_limits_are_validated_and_reach_routed_context() { + let limits = ConfigExtractionLimits { + max_backend_bytes: 64, + max_blob_bytes: 32, + max_secret_bytes: 16, + max_total_bytes: 48, + max_json_depth: 8, + max_json_nodes: 32, + timeout: Duration::from_secs(2), + }; + let router = RouterService::builder() + .get("/limits", move |ctx: RequestContext| async move { + assert_eq!(ctx.config_extraction_limits(), limits); + Ok::<_, EdgeError>("ok") + }) + .build(); + let mut app = App::new(router); + app.set_config_extraction_limits(limits) + .expect("valid extraction limits"); + + let request = request_builder() + .method(Method::GET) + .uri("/limits") + .body(Body::empty()) + .expect("request"); + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("response"), + ); + assert_eq!(response.status(), StatusCode::OK); + } + + #[test] + fn default_app_admits_with_finite_deadline_and_empty_grant() { + let app = App::new(empty_router()); + let start = MonotonicInstant::now(); + let request = request_builder() + .method(Method::GET) + .uri("/") + .body(Body::empty()) + .expect("request"); + let head = IngressHead::from_request( + &request, + start, + RouteResolution::NotFound, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ); + + let IngressAdmissionOutcome::Admitted { + completion, + ingress: admitted, + } = app.admit_ingress(&head).expect("admitted") + else { + panic!("expected admission"); + }; + drop(completion); + assert_eq!(admitted.request_start(), start); + assert!(admitted.read_deadline().instant() > start); + assert!( + admitted.read_deadline().instant() + <= start.checked_add(DEADLINE_FAR_FUTURE).expect("maximum") + ); + let (_, _, grant, _, _) = admitted.into_parts(); + grant.downcast::<()>().expect_err("empty grant"); + } + + #[test] + fn ingress_refusal_skips_handler_and_body_poll() { + let handler_calls = Arc::new(AtomicUsize::new(0)); + let handler_counter = Arc::clone(&handler_calls); + let middleware_calls = Arc::new(AtomicUsize::new(0)); + let router = RouterService::builder() + .post("/upload", move |_ctx: RequestContext| { + let call_counter = Arc::clone(&handler_counter); + async move { + call_counter.fetch_add(1, Ordering::SeqCst); + Ok::<_, EdgeError>("unexpected") + } + }) + .middleware(CountingMiddleware(Arc::clone(&middleware_calls))) + .build(); + let mut app = App::new(router); + let admission_calls = Arc::new(AtomicUsize::new(0)); + let admission_counter = Arc::clone(&admission_calls); + app.set_ingress_admission_policy(move |_| { + admission_counter.fetch_add(1, Ordering::SeqCst); + AdmissionDecision::Refuse { + completion: ResponseEgressCompletion::empty(), + response: response_builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .header("x-admission", "saturated") + .body(Body::from("busy")) + .expect("response"), + } + }); + let body_polls = Arc::new(AtomicUsize::new(0)); + let poll_counter = Arc::clone(&body_polls); + let body = Body::stream(poll_fn(move |_| { + poll_counter.fetch_add(1, Ordering::SeqCst); + Poll::Ready(None::) + })); + let request = request_builder() + .method(Method::POST) + .uri("/upload") + .body(body) + .expect("request"); + + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("response"), + ); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!( + response.headers().get("x-admission").expect("header"), + "saturated" + ); + assert_eq!(response.body().as_bytes().expect("body"), b"busy"); + assert_eq!(admission_calls.load(Ordering::SeqCst), 1); + assert_eq!(handler_calls.load(Ordering::SeqCst), 0); + assert_eq!(middleware_calls.load(Ordering::SeqCst), 0); + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + } + + #[test] + fn admitted_response_retains_completion_until_terminal_egress() { + let completions = Arc::new(AtomicUsize::new(0)); + let observed_completions = Arc::clone(&completions); + let mut app = App::new( + RouterService::builder() + .get("/ok", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("ok") + }) + .build(), + ); + app.set_ingress_admission_policy(move |head| AdmissionDecision::Admit { + completion: counted_completion(&observed_completions), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + }); + let request = request_builder() + .method(Method::GET) + .uri("/ok") + .body(Body::empty()) + .expect("request"); + + let outcome = block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("dispatch"); + assert_eq!(completions.load(Ordering::SeqCst), 0); + let IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("expected response"); + }; + assert_eq!(complete_envelope(*envelope).status(), StatusCode::OK); + assert_eq!(completions.load(Ordering::SeqCst), 1); + } + + #[test] + fn refusal_response_retains_completion_until_terminal_egress() { + let completions = Arc::new(AtomicUsize::new(0)); + let observed_completions = Arc::clone(&completions); + let mut app = App::new(empty_router()); + app.set_ingress_admission_policy(move |_| AdmissionDecision::Refuse { + completion: counted_completion(&observed_completions), + response: response_builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .body(Body::empty()) + .expect("response"), + }); + let head = IngressHeadParts::new( + Method::GET, + "/".parse().expect("URI"), + Version::HTTP_11, + HeaderMap::new(), + ); + + let IngressBeginOutcome::Refused(envelope) = app + .begin_ingress(head, MonotonicInstant::now()) + .expect("begin ingress") + else { + panic!("expected refusal"); + }; + assert_eq!(completions.load(Ordering::SeqCst), 0); + assert_eq!( + complete_envelope(envelope).status(), + StatusCode::SERVICE_UNAVAILABLE + ); + assert_eq!(completions.load(Ordering::SeqCst), 1); + } + + #[test] + fn fallback_terminal_response_retains_completion_until_terminal_egress() { + let completions = Arc::new(AtomicUsize::new(0)); + let observed_completions = Arc::clone(&completions); + let mut app = App::new(empty_router()); + app.set_ingress_admission_policy(move |head| AdmissionDecision::ReadBodyBeforeFallback { + completion: counted_completion(&observed_completions), + grant: IngressGrant::empty(), + max_body_bytes: 1, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text(StatusCode::PAYLOAD_TOO_LARGE, "too large"), + on_timeout: BufferedIngressResponse::text(StatusCode::REQUEST_TIMEOUT, "late"), + }); + let request = request_builder() + .method(Method::POST) + .uri("/missing") + .body(Body::from("ab")) + .expect("request"); + + let outcome = block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("dispatch"); + assert_eq!(completions.load(Ordering::SeqCst), 0); + let IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("expected response"); + }; + assert_eq!( + complete_envelope(*envelope).status(), + StatusCode::PAYLOAD_TOO_LARGE + ); + assert_eq!(completions.load(Ordering::SeqCst), 1); + } + + #[test] + fn admission_abort_has_no_body_or_response_lifecycle() { + let body_polls = Arc::new(AtomicUsize::new(0)); + let observed_polls = Arc::clone(&body_polls); + let mut app = App::new(empty_router()); + app.set_ingress_admission_policy(|_| AdmissionDecision::Abort); + let request = request_builder() + .method(Method::POST) + .uri("/missing") + .body(Body::stream(poll_fn(move |_| { + observed_polls.fetch_add(1, Ordering::SeqCst); + Poll::Ready(None::) + }))) + .expect("request"); + + let outcome = block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("dispatch"); + assert!(matches!(outcome, IngressDispatchOutcome::Aborted)); + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + } + + #[test] + fn ingress_refusal_is_detached_from_application_response_egress() { + let router = RouterService::builder() + .post("/upload/{id}", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("unexpected") + }) + .build(); + let reports = Arc::new(Mutex::new(Vec::new())); + let policy_calls = Arc::new(AtomicUsize::new(0)); + let mut app = App::new(router); + app.set_ingress_admission_policy(|_| AdmissionDecision::Refuse { + completion: ResponseEgressCompletion::empty(), + response: response_builder() + .status(StatusCode::TOO_MANY_REQUESTS) + .body(Body::empty()) + .expect("response"), + }); + app.set_response_egress_observer(AppEgressObserver(Arc::clone(&reports))); + let request_start = MonotonicInstant::now(); + let started_at = request_start + .checked_add(Duration::from_millis(5)) + .expect("egress start"); + app.set_monotonic_clock(MonotonicClock::new(move || started_at)); + let observed_policy_calls = Arc::clone(&policy_calls); + app.set_response_egress_policy(move |_head, egress_started_at| { + observed_policy_calls.fetch_add(1, Ordering::SeqCst); + ResponseEgressPolicy { + write_deadline: Deadline::at_instant( + egress_started_at + .checked_add(Duration::from_secs(1)) + .expect("deadline"), + ), + } + }); + let head = IngressHeadParts::new( + Method::POST, + "/upload/42".parse().expect("URI"), + Version::HTTP_11, + HeaderMap::new(), + ); + + let IngressBeginOutcome::Refused(egress) = + app.begin_ingress(head, request_start).expect("refusal") + else { + panic!("expected refusal"); + }; + let Ok((prepared, _, mut attempt, egress_clock)) = + egress.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("begin egress"); + }; + let response = prepared.into_response(); + assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); + assert_eq!(egress_clock.now(), started_at); + assert!(attempt.begin_writing()); + assert!(attempt.complete(started_at)); + + assert_eq!(policy_calls.load(Ordering::SeqCst), 0); + assert!(reports.lock().expect("reports lock").is_empty()); + } + + #[test] + fn detached_ingress_error_send_uses_application_completion_and_earlier_deadline() { + let router = RouterService::builder().build(); + let factory_calls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let observed_heads = Arc::new(Mutex::new(Vec::new())); + let mut app = App::new(router); + let observed_factory_calls = Arc::clone(&factory_calls); + let observed_completion_calls = Arc::clone(&completion_calls); + let captured_heads = Arc::clone(&observed_heads); + app.set_detached_response_egress_decision_factory(move |head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + captured_heads.lock().expect("heads lock").push(( + head.request_method().clone(), + head.request_start(), + head.status(), + head.error_kind(), + )); + let terminal_calls = Arc::clone(&observed_completion_calls); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |_report| { + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + deadline: head.write_deadline_after(Duration::from_secs(5)), + } + }); + let request_start = MonotonicInstant::now(); + let egress_started_at = request_start + .checked_add(Duration::from_millis(10)) + .expect("egress start"); + app.set_monotonic_clock(MonotonicClock::new(move || egress_started_at)); + + let IngressDispatchOutcome::Response(egress) = app.detached_ingress_error_egress( + EdgeError::uri_too_long("request target exceeded configured limit"), + Method::POST, + request_start, + ) else { + panic!("expected detached response"); + }; + + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 0); + assert_eq!( + observed_heads.lock().expect("heads lock").as_slice(), + &[( + Method::POST, + request_start, + StatusCode::URI_TOO_LONG, + "uri_too_long", + )] + ); + let Ok((prepared, policy, mut attempt, clock)) = + egress.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("begin detached egress"); + }; + assert_eq!( + policy.write_deadline.instant(), + request_start + .checked_add(Duration::from_secs(5)) + .expect("detached deadline") + ); + assert_eq!(prepared.into_response().status(), StatusCode::URI_TOO_LONG); + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + assert_eq!(completion_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn detached_ingress_error_send_uses_earlier_default_policy_deadline() { + let request_start = MonotonicInstant::now(); + let egress_started_at = request_start + .checked_add(Duration::from_secs(1)) + .expect("egress start"); + let mut app = App::new(RouterService::builder().build()); + app.set_monotonic_clock(MonotonicClock::new(move || egress_started_at)); + app.set_detached_response_egress_decision_factory(|head| { + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::empty(), + deadline: head.write_deadline_after(Duration::from_mins(1)), + } + }); + + let IngressDispatchOutcome::Response(egress) = app.detached_ingress_error_egress( + EdgeError::bad_request("invalid request head"), + Method::GET, + request_start, + ) else { + panic!("expected detached response"); + }; + let Ok((_prepared, policy, _attempt, _clock)) = + egress.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("begin detached egress"); + }; + + assert_eq!( + policy.write_deadline.instant(), + egress_started_at + .checked_add(DEFAULT_RESPONSE_WRITE_BUDGET) + .expect("default policy deadline") + ); + } + + #[test] + fn detached_ingress_error_send_preserves_expired_deadline() { + let request_start = MonotonicInstant::now(); + let egress_started_at = request_start + .checked_add(Duration::from_secs(1)) + .expect("egress start"); + let mut app = App::new(RouterService::builder().build()); + app.set_monotonic_clock(MonotonicClock::new(move || egress_started_at)); + app.set_detached_response_egress_decision_factory(move |_| { + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::empty(), + deadline: Deadline::at_instant(request_start), + } + }); + + let IngressDispatchOutcome::Response(egress) = app.detached_ingress_error_egress( + EdgeError::bad_request("invalid request head"), + Method::GET, + request_start, + ) else { + panic!("expected detached response"); + }; + let Ok((_prepared, policy, _attempt, _clock)) = + egress.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("begin detached egress"); + }; + + assert_eq!(policy.write_deadline.instant(), request_start); + assert!(policy.write_deadline.is_expired_at(egress_started_at)); + } + + #[test] + fn abandoned_detached_ingress_error_releases_factory_resource_without_report() { + let resource_drops = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let mut app = App::new(RouterService::builder().build()); + let observed_resource_drops = Arc::clone(&resource_drops); + let observed_completion_calls = Arc::clone(&completion_calls); + app.set_detached_response_egress_decision_factory(move |head| { + let resource = GrantDropProbe(Arc::clone(&observed_resource_drops)); + let terminal_calls = Arc::clone(&observed_completion_calls); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |_report| { + let _resource = resource; + terminal_calls.fetch_add(1, Ordering::SeqCst); + }), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), + } + }); + + let IngressDispatchOutcome::Response(egress) = app.detached_ingress_error_egress( + EdgeError::bad_request("invalid request head"), + Method::GET, + MonotonicInstant::now(), + ) else { + panic!("expected detached response"); + }; + drop(egress); + + assert_eq!(completion_calls.load(Ordering::SeqCst), 0); + assert_eq!(resource_drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn detached_ingress_error_abort_returns_non_response_outcome() { + let factory_calls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::new(AtomicUsize::new(0)); + let observed_factory_calls = Arc::clone(&factory_calls); + let observed_completion_calls = Arc::clone(&completion_calls); + let mut app = App::new(RouterService::builder().build()); + app.set_detached_response_egress_decision_factory(move |_head| { + observed_factory_calls.fetch_add(1, Ordering::SeqCst); + let terminal_calls = Arc::clone(&observed_completion_calls); + drop(ResponseEgressCompletion::new(move |_report| { + terminal_calls.fetch_add(1, Ordering::SeqCst); + })); + DetachedResponseEgressDecision::Abort + }); + + let outcome = app.detached_ingress_error_egress( + EdgeError::bad_request("invalid request head"), + Method::GET, + MonotonicInstant::now(), + ); + + assert!(matches!(outcome, IngressDispatchOutcome::Aborted)); + assert_eq!(factory_calls.load(Ordering::SeqCst), 1); + assert_eq!(completion_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn detached_ingress_error_default_factory_uses_request_start_deadline() { + let request_start = MonotonicInstant::now(); + let egress_started_at = request_start + .checked_add(Duration::from_secs(1)) + .expect("egress start"); + let mut app = App::new(RouterService::builder().build()); + app.set_monotonic_clock(MonotonicClock::new(move || egress_started_at)); + + let IngressDispatchOutcome::Response(egress) = app.detached_ingress_error_egress( + EdgeError::bad_request("invalid request head"), + Method::GET, + request_start, + ) else { + panic!("default detached decision must send a response"); + }; + let Ok((prepared, policy, mut attempt, clock)) = + egress.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("begin detached egress"); + }; + + assert_eq!( + policy.write_deadline.instant(), + request_start + .checked_add(DEFAULT_RESPONSE_WRITE_BUDGET) + .expect("default detached deadline") + ); + assert_eq!(prepared.into_response().status(), StatusCode::BAD_REQUEST); + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + } + + #[test] + fn post_admission_failure_uses_owned_error_egress() { + let router = RouterService::builder() + .post("/upload/{id}", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("unexpected") + }) + .build(); + let reports = Arc::new(Mutex::new(Vec::new())); + let grant_drops = Arc::new(AtomicUsize::new(0)); + let observed_grant_drops = Arc::clone(&grant_drops); + let mut app = App::new(router); + app.set_ingress_admission_policy(move |head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(GrantDropProbe(Arc::clone(&observed_grant_drops))), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + }); + app.set_error_response_renderer(|error| { + let mut response = Response::new(Body::from(error.kind())); + *response.status_mut() = error.status(); + response + }); + app.set_response_egress_observer(AppEgressObserver(Arc::clone(&reports))); + let request_start = MonotonicInstant::now(); + let head = IngressHeadParts::new( + Method::POST, + "/upload/42".parse().expect("URI"), + Version::HTTP_11, + HeaderMap::new(), + ); + let IngressBeginOutcome::Admitted(prepared) = + app.begin_ingress(head, request_start).expect("admission") + else { + panic!("expected admission"); + }; + + let egress = app.admitted_error_egress( + prepared, + EdgeError::internal(anyhow::anyhow!("post-admission failure")), + ); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + let Ok((prepared_egress, _, mut attempt, clock)) = + egress.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("begin egress"); + }; + let response = prepared_egress.into_response(); + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + response.body().as_bytes().expect("buffered body"), + b"internal" + ); + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + + let observed = reports.lock().expect("reports lock"); + assert_eq!(observed.len(), 1); + assert_eq!(observed[0].request_start, request_start); + assert_eq!( + observed[0].route.as_ref().map(RouteMetadata::pattern), + Some("/upload/{id}") + ); + } + + #[test] + fn ingress_method_reaches_owned_response_framing() { + let mut app = App::new(RouterService::builder().build()); + app.set_ingress_admission_policy(|_| AdmissionDecision::Refuse { + completion: ResponseEgressCompletion::empty(), + response: response_builder() + .status(StatusCode::OK) + .header("content-length", "7") + .body(Body::from("ignored")) + .expect("response"), + }); + let head = IngressHeadParts::new( + Method::HEAD, + "/missing".parse().expect("URI"), + Version::HTTP_11, + HeaderMap::new(), + ); + + let IngressBeginOutcome::Refused(egress) = app + .begin_ingress(head, MonotonicInstant::now()) + .expect("refusal") + else { + panic!("expected refusal"); + }; + let Ok((prepared, _, mut attempt, clock)) = + egress.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("owned response egress failed"); + }; + assert!(!prepared.transmits_body()); + assert_eq!( + prepared + .into_response() + .body() + .as_bytes() + .expect("suppressed body"), + b"" + ); + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + } + + #[test] + fn fallback_body_policy_rejects_matched_routes_before_body_construction() { + let router = RouterService::builder() + .post("/upload", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("unexpected") + }) + .build(); + let mut app = App::new(router); + app.set_ingress_admission_policy(|head| AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + max_body_bytes: 4_096, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text(StatusCode::PAYLOAD_TOO_LARGE, "too large"), + on_timeout: BufferedIngressResponse::text(StatusCode::REQUEST_TIMEOUT, "timeout"), + }); + let head = IngressHeadParts::new( + Method::POST, + "/upload".parse().expect("URI"), + Version::HTTP_11, + HeaderMap::new(), + ); + + assert!(matches!( + app.begin_ingress(head, MonotonicInstant::now()), + Err(EdgeError::Internal { .. }) + )); + } + + #[test] + fn fallback_body_exact_cap_preserves_not_found() { + let mut app = App::new(empty_router()); + app.set_ingress_admission_policy(|head| AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + max_body_bytes: 4, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text(StatusCode::PAYLOAD_TOO_LARGE, "too large"), + on_timeout: BufferedIngressResponse::text(StatusCode::REQUEST_TIMEOUT, "timeout"), + }); + let request = request_builder() + .method(Method::POST) + .uri("/missing") + .body(Body::stream(iter([ + Bytes::from_static(b"ab"), + Bytes::from_static(b"cd"), + ]))) + .expect("request"); + + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("response"), + ); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn fallback_body_exact_cap_preserves_method_not_allowed_allow_header() { + let router = RouterService::builder() + .get("/known", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("unexpected") + }) + .build(); + let mut app = App::new(router); + app.set_ingress_admission_policy(|head| AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + max_body_bytes: 4, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text(StatusCode::PAYLOAD_TOO_LARGE, "too large"), + on_timeout: BufferedIngressResponse::text(StatusCode::REQUEST_TIMEOUT, "timeout"), + }); + let request = request_builder() + .method(Method::POST) + .uri("/known") + .body(Body::stream(iter([ + Bytes::from_static(b"ab"), + Bytes::from_static(b"cd"), + ]))) + .expect("request"); + + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("response"), + ); + + assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); + assert_eq!( + response.headers().get("allow").expect("Allow header"), + "GET" + ); + } + + #[test] + fn custom_error_response_renderer_handles_admitted_dispatch_errors() { + let router = RouterService::builder() + .get("/fails", |_ctx: RequestContext| async move { + Err::<&'static str, _>(EdgeError::bad_request("private diagnostic")) + }) + .build(); + let mut app = App::new(router); + app.set_error_response_renderer(|error| { + response_builder() + .status(error.status()) + .header("x-error-renderer", "application") + .body(Body::from(error.kind())) + .expect("response") + }); + let request = request_builder() + .method(Method::GET) + .uri("/fails") + .body(Body::empty()) + .expect("request"); + + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("response"), + ); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.headers()["x-error-renderer"], "application"); + assert_eq!( + response.body().as_bytes().expect("buffered body"), + b"bad_request" + ); + } + + #[test] + fn custom_error_response_renderer_handles_detached_ingress_errors() { + let mut app = App::new(empty_router()); + app.set_error_response_renderer(|error| { + response_builder() + .status(error.status()) + .header("x-error-renderer", "application") + .body(Body::from(error.kind())) + .expect("response") + }); + + let response = complete_dispatch(app.detached_ingress_error_egress( + EdgeError::uri_too_long("private diagnostic"), + Method::GET, + MonotonicInstant::now(), + )); + + assert_eq!(response.status(), StatusCode::URI_TOO_LONG); + assert_eq!(response.headers()["x-error-renderer"], "application"); + assert_eq!( + response.body().as_bytes().expect("buffered body"), + b"uri_too_long" + ); + } + + #[test] + fn custom_error_response_renderer_cannot_remove_required_allow_header() { + let router = RouterService::builder() + .get("/known", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("unexpected") + }) + .build(); + let mut app = App::new(router); + app.set_error_response_renderer(|_error| Response::new(Body::from("bounded error"))); + let request = request_builder() + .method(Method::POST) + .uri("/known") + .body(Body::empty()) + .expect("request"); + + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("response"), + ); + + assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); + assert_eq!( + response.headers().get("allow").expect("Allow header"), + "GET" + ); + assert_eq!( + response.body().as_bytes().expect("buffered body"), + b"bounded error" + ); + } + + #[test] + fn synthesized_headers_preserve_detached_ownership() { + let mut app = App::new(empty_router()); + let mut headers = HeaderMap::new(); + headers.insert("x-build", HeaderValue::from_static("original")); + app.set_synthesized_response_headers(headers) + .expect("configured"); + let calls = Arc::new(AtomicUsize::new(0)); + let drops = Arc::new(AtomicUsize::new(0)); + let completed = Arc::clone(&calls); + let retained = Arc::clone(&drops); + let mut invalid = HeaderMap::new(); + invalid.insert("content-type", HeaderValue::from_static("text/plain")); + assert_eq!( + app.set_synthesized_response_headers(invalid), + Err(SynthesizedResponseHeadersError::ForbiddenName) + ); + app.set_detached_response_egress_decision_factory(move |head| { + let count = Arc::clone(&completed); + let resource = GrantDropProbe(Arc::clone(&retained)); + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::new(move |_| { + count.fetch_add(1, Ordering::SeqCst); + drop(resource); + }), + deadline: head.write_deadline_after(Duration::from_secs(1)), + } + }); + let started = app.monotonic_now(); + let IngressDispatchOutcome::Response(envelope) = app.detached_ingress_error_egress( + EdgeError::uri_too_long("private request"), + Method::HEAD, + started, + ) else { + panic!("expected detached response"); + }; + app.set_synthesized_response_headers(HeaderMap::new()) + .expect("new policy"); + let Ok((prepared, policy, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("valid detached response"); + }; + assert!(!prepared.transmits_body()); + assert_eq!(prepared.into_response().headers()["x-build"], "original"); + assert_eq!( + policy.write_deadline.instant(), + started + Duration::from_secs(1) + ); + assert_eq!(calls.load(Ordering::SeqCst), 0); + assert_eq!(drops.load(Ordering::SeqCst), 0); + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!(drops.load(Ordering::SeqCst), 1); + drop(attempt); + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn synthesized_headers_do_not_decorate_routed_errors_or_refusals() { + let calls = Arc::new(AtomicUsize::new(0)); + let handled = Arc::clone(&calls); + let mut app = App::new( + RouterService::builder() + .get("/", move |_ctx: RequestContext| { + handled.fetch_add(1, Ordering::SeqCst); + async { Err::(EdgeError::bad_request("routed error")) } + }) + .build(), + ); + let mut headers = HeaderMap::new(); + headers.insert("x-build", HeaderValue::from_static("configured")); + app.set_synthesized_response_headers(headers) + .expect("configured"); + let routed = dispatch_request(&app, Method::GET, "/", Body::empty()); + assert_eq!(routed.status(), StatusCode::BAD_REQUEST); + assert!(!routed.headers().contains_key("x-build")); + app.set_ingress_admission_policy(|_| AdmissionDecision::Refuse { + completion: ResponseEgressCompletion::empty(), + response: Response::new(Body::from("refused")), + }); + let refused = dispatch_request(&app, Method::GET, "/", Body::empty()); + assert_eq!(refused.status(), StatusCode::OK); + assert_eq!(refused.body().as_bytes().expect("refusal body"), b"refused"); + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert!(!refused.headers().contains_key("x-build")); + } + + #[test] + fn generated_overflow_retry_after_precedes_synthesized_headers() { + fn source_error(app: &App, error: EdgeError) -> Response { + let request = Request::new(Body::empty()); + let head = IngressHeadParts::from_request( + &request, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ); + let IngressBeginOutcome::Admitted(prepared) = app + .begin_ingress(head, app.monotonic_now()) + .expect("admitted") + else { + panic!("admitted"); + }; + complete_envelope(app.admitted_error_egress(prepared, error)) + } + let mut app = App::new( + RouterService::builder() + .get("/", |_ctx: RequestContext| async { + Ok::<_, EdgeError>( + response_builder() + .status(StatusCode::PAYLOAD_TOO_LARGE) + .header("retry-after", "120") + .body(Body::empty()) + .expect("upstream"), + ) + }) + .build(), + ); + let mut headers = HeaderMap::new(); + headers.insert("retry-after", HeaderValue::from_static("180")); + headers.insert("x-build", HeaderValue::from_static("configured")); + headers.insert("allow", HeaderValue::from_static("POST")); + app.set_synthesized_response_headers(headers) + .expect("configured"); + app.set_error_response_renderer(|_| { + response_builder() + .header("retry-after", "240") + .body(Body::from("fixed error")) + .expect("error") + }); + for origin in [ + crate::RequestBodyOverflowOrigin::Inbound, + crate::RequestBodyOverflowOrigin::OutboundUpload, + ] { + let response = source_error(&app, EdgeError::payload_too_large(origin)); + assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert!(!response.headers().contains_key("retry-after")); + assert_eq!(response.headers()["x-build"], "configured"); + } + let config = source_error(&app, EdgeError::config_out_of_date("private", "field")); + assert_eq!(config.headers()["retry-after"], "60"); + assert_eq!(config.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(config.headers()["x-build"], "configured"); + assert_eq!(config.headers().get_all("retry-after").iter().count(), 1); + let wrong_method = dispatch_request(&app, Method::POST, "/", Body::empty()); + assert_eq!(wrong_method.status(), StatusCode::METHOD_NOT_ALLOWED); + assert_eq!(wrong_method.headers()["x-build"], "configured"); + assert_eq!(wrong_method.headers()["allow"], "GET"); + assert_eq!(wrong_method.headers().get_all("allow").iter().count(), 1); + let upstream = dispatch_request(&app, Method::GET, "/", Body::empty()); + assert_eq!(upstream.headers()["retry-after"], "120"); + assert!(!upstream.headers().contains_key("x-build")); + } + + #[test] + fn synthesized_headers_survive_begin_failure_without_reentering_application_code() { + let drops = Arc::new(AtomicUsize::new(0)); + let retained = Arc::clone(&drops); + let mut app = App::new( + RouterService::builder() + .get("/", |_ctx: RequestContext| async { + Ok::<_, EdgeError>( + response_builder() + .header("trailer", "private") + .body(Body::from("x")) + .expect("invalid response"), + ) + }) + .build(), + ); + let mut headers = HeaderMap::new(); + headers.insert("x-build", HeaderValue::from_static("configured")); + app.set_synthesized_response_headers(headers) + .expect("configured"); + app.set_error_response_renderer(|_| panic!("fallback must not invoke renderer")); + let mut invalid = HeaderMap::new(); + invalid.insert( + "x-build", + HeaderValue::from_str(&"a".repeat(4096)).expect("field"), + ); + assert_eq!( + app.set_synthesized_response_headers(invalid), + Err(SynthesizedResponseHeadersError::TooManyBytes) + ); + app.set_ingress_admission_policy(move |head| { + let owned = GrantDropProbe(Arc::clone(&retained)); + AdmissionDecision::Admit { + grant: IngressGrant::empty(), + completion: ResponseEgressCompletion::new(move |_| drop(owned)), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + } + }); + let IngressDispatchOutcome::Response(envelope) = block_on(app.dispatch_ingress( + Request::new(Body::empty()), + app.monotonic_now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("dispatch") else { + panic!("response"); + }; + let Err(failure) = envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) else { + panic!("framing failure"); + }; + let (mut attempt, clock, cause) = failure.into_parts(); + assert_eq!(drops.load(Ordering::SeqCst), 0); + assert!(attempt.begin_fallback(cause)); + let prepared = attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + response_builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("fixed fallback")) + .expect("fallback"), + ) + .expect("prepared fallback"); + assert_eq!(prepared.into_response().headers()["x-build"], "configured"); + assert!(attempt.begin_writing()); + assert!(attempt.finish_fallback( + crate::ResponseEgressFallbackDisposition::Completed, + clock.now() + )); + assert_eq!(drops.load(Ordering::SeqCst), 1); + drop(attempt); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn custom_renderer_cannot_add_retry_after_to_body_overflow() { + let mut app = App::new(empty_router()); + app.set_error_response_renderer(|error| { + assert_eq!(error.kind(), "payload_too_large"); + response_builder() + .status(StatusCode::BAD_REQUEST) + .header("retry-after", "120") + .body(Body::from("bounded error")) + .expect("response") + }); + let error = block_on(Body::from("over cap").into_bytes_bounded(1)).expect_err("overflow"); + let response = app.render_error_response(error); + assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert!(response.headers().get("retry-after").is_none()); + } + + #[test] + fn custom_renderer_overflow_marker_cannot_override_config_retry_policy() { + let mut app = App::new(empty_router()); + app.set_error_response_renderer(|_| { + EdgeError::payload_too_large(crate::RequestBodyOverflowOrigin::Inbound) + .into_response() + .expect("renderer response") + }); + for error in [ + EdgeError::config_out_of_date("stale", "field"), + EdgeError::store_extraction(StoreExtractionReason::MissingSecret, "missing", None), + ] { + let response = app.render_error_response(error); + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!( + response + .headers() + .get("retry-after") + .expect("required retry"), + "60" + ); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::POST, + response, + ) + .expect("prepared") + .into_response(); + assert_eq!( + prepared + .headers() + .get("retry-after") + .expect("retry survives egress"), + "60" + ); + } + } + + #[test] + fn custom_error_response_renderer_cannot_remove_config_retry_after_header() { + let mut app = App::new(empty_router()); + app.set_error_response_renderer(|_error| { + response_builder() + .header("retry-after", "1") + .body(Body::from("bounded error")) + .expect("response") + }); + + for error in [ + EdgeError::config_out_of_date("stale config", "feature.enabled"), + EdgeError::StoreExtraction { + reason: StoreExtractionReason::MissingSecret, + message: "missing secret".to_owned(), + field_path: Some("credentials.token".to_owned()), + }, + ] { + let response = app.render_error_response(error); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!( + response + .headers() + .get("retry-after") + .expect("Retry-After header"), + "60" + ); + assert_eq!( + response.body().as_bytes().expect("buffered body"), + b"bounded error" + ); + } + } + + #[test] + fn custom_error_response_renderer_does_not_add_retry_after_to_generic_503() { + let mut app = App::new(empty_router()); + app.set_error_response_renderer(|_error| Response::new(Body::from("bounded error"))); + + let response = app.render_error_response(EdgeError::service_unavailable("busy")); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + assert!(response.headers().get("retry-after").is_none()); + } + + #[test] + fn fallback_body_overflow_precedes_method_not_allowed() { + let router = RouterService::builder() + .get("/known", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("unexpected") + }) + .build(); + let mut app = App::new(router); + app.set_ingress_admission_policy(|head| AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + max_body_bytes: 4, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text(StatusCode::PAYLOAD_TOO_LARGE, "too large"), + on_timeout: BufferedIngressResponse::text(StatusCode::REQUEST_TIMEOUT, "timeout"), + }); + let request = request_builder() + .method(Method::POST) + .uri("/known") + .body(Body::stream(iter([ + Bytes::from_static(b"abcd"), + Bytes::from_static(b"e"), + ]))) + .expect("request"); + + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("response"), + ); + + assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); + } + + #[test] + fn fallback_body_zero_cap_accepts_only_empty_body() { + let mut app = App::new(empty_router()); + app.set_ingress_admission_policy(|head| AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + max_body_bytes: 0, + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + on_exceeded: BufferedIngressResponse::text(StatusCode::PAYLOAD_TOO_LARGE, "too large"), + on_timeout: BufferedIngressResponse::text(StatusCode::REQUEST_TIMEOUT, "timeout"), + }); + + for (body, expected) in [ + (Body::empty(), StatusCode::NOT_FOUND), + (Body::from_bytes("a"), StatusCode::PAYLOAD_TOO_LARGE), + ] { + let request = request_builder() + .method(Method::POST) + .uri("/missing") + .body(body) + .expect("request"); + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("response"), + ); + + assert_eq!(response.status(), expected); + } + } + + #[test] + fn fallback_body_deadline_precedes_body_poll_and_not_found() { + let start = MonotonicInstant::now(); + let body_polls = Arc::new(AtomicUsize::new(0)); + let observed_polls = Arc::clone(&body_polls); + let body = Body::stream(poll_fn(move |_| { + observed_polls.fetch_add(1, Ordering::SeqCst); + Poll::Ready(Some(Bytes::from_static(b"a"))) + })); + let mut app = App::new(empty_router()); + app.set_monotonic_clock(MonotonicClock::new(move || start)); + app.set_ingress_admission_policy(|head| AdmissionDecision::ReadBodyBeforeFallback { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + max_body_bytes: 4_096, + read_deadline: Deadline::at_instant(head.request_start()), + on_exceeded: BufferedIngressResponse::text(StatusCode::PAYLOAD_TOO_LARGE, "too large"), + on_timeout: BufferedIngressResponse::text( + StatusCode::GATEWAY_TIMEOUT, + "fallback timeout", + ), + }); + let request = request_builder() + .method(Method::POST) + .uri("/missing") + .body(body) + .expect("request"); + + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + start, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("response"), + ); + + assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT); + assert_eq!( + response.body().as_bytes().expect("buffered body"), + b"fallback timeout" + ); + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + } + + #[test] + fn fallback_grant_is_live_during_exact_cap_eof_and_drops_before_egress() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let grant_drops = Arc::new(AtomicUsize::new(0)); + configure_fallback_policy( + &mut app, + &grant_drops, + 4, + Deadline::after(Duration::from_secs(1)), + buffered_terminal(StatusCode::PAYLOAD_TOO_LARGE, "exceeded", b"over"), + buffered_terminal(StatusCode::GATEWAY_TIMEOUT, "timeout", b"late"), + ); + let observed_drops = Arc::clone(&grant_drops); + let mut poll = 0_usize; + let body = Body::stream(poll_fn(move |_| { + assert_eq!(observed_drops.load(Ordering::SeqCst), 0); + let next = match poll { + 0 => Some(Bytes::from_static(b"ab")), + 1 => Some(Bytes::from_static(b"cd")), + 2 => None, + _ => panic!("body polled after EOF"), + }; + poll += 1; + Poll::Ready(next) + })); + let head = IngressHeadParts::new( + Method::POST, + "/known".parse().expect("URI"), + Version::HTTP_11, + HeaderMap::new(), + ); + let IngressBeginOutcome::Admitted(prepared) = app + .begin_ingress(head, MonotonicInstant::now()) + .expect("begin ingress") + else { + panic!("expected admission"); + }; + let request = request_builder() + .method(Method::POST) + .uri("/known") + .body(body) + .expect("request"); + + let egress = block_on(app.dispatch_admitted(prepared, request)); + + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_eq!( + complete_envelope(egress).status(), + StatusCode::METHOD_NOT_ALLOWED + ); + assert_no_fallback_dispatch(&handler_calls, &middleware_calls); + } + + #[test] + fn fallback_overflow_returns_exact_application_response_and_drops_grant_once() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let grant_drops = Arc::new(AtomicUsize::new(0)); + configure_fallback_policy( + &mut app, + &grant_drops, + 4, + Deadline::after(Duration::from_secs(1)), + buffered_terminal( + StatusCode::PAYLOAD_TOO_LARGE, + "exceeded", + b"limit\0exceeded", + ), + buffered_terminal(StatusCode::GATEWAY_TIMEOUT, "timeout", b"late"), + ); + let observed_drops = Arc::clone(&grant_drops); + let body = Body::stream( + iter([Bytes::from_static(b"abcd"), Bytes::from_static(b"e")]).inspect(move |_| { + assert_eq!(observed_drops.load(Ordering::SeqCst), 0); + }), + ); + + let response = dispatch_fallback(&app, body); + + assert_buffered_terminal_response( + &response, + StatusCode::PAYLOAD_TOO_LARGE, + "exceeded", + b"limit\0exceeded", + ); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_no_fallback_dispatch(&handler_calls, &middleware_calls); + } + + #[test] + fn fallback_deadline_expiry_returns_exact_application_response_and_drops_grant_once() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let start = MonotonicInstant::now(); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let now = Arc::new(Mutex::new(start)); + let observed_now = Arc::clone(&now); + app.set_monotonic_clock(MonotonicClock::new(move || { + *observed_now.lock().expect("clock lock") + })); + let grant_drops = Arc::new(AtomicUsize::new(0)); + configure_fallback_policy( + &mut app, + &grant_drops, + 4, + Deadline::at_instant(deadline), + buffered_terminal(StatusCode::PAYLOAD_TOO_LARGE, "exceeded", b"over"), + buffered_terminal(StatusCode::GATEWAY_TIMEOUT, "timeout", b"deadline\0expired"), + ); + let observed_drops = Arc::clone(&grant_drops); + let body = Body::stream(poll_fn(move |_| { + assert_eq!(observed_drops.load(Ordering::SeqCst), 0); + *now.lock().expect("clock lock") = deadline; + Poll::Ready(Some(Bytes::from_static(b"a"))) + })); + + let response = dispatch_fallback(&app, body); + + assert_buffered_terminal_response( + &response, + StatusCode::GATEWAY_TIMEOUT, + "timeout", + b"deadline\0expired", + ); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_no_fallback_dispatch(&handler_calls, &middleware_calls); + } + + #[test] + fn fallback_adapter_request_timeout_uses_application_timeout_response() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let grant_drops = Arc::new(AtomicUsize::new(0)); + configure_fallback_policy( + &mut app, + &grant_drops, + 4, + Deadline::after(Duration::from_secs(1)), + buffered_terminal(StatusCode::PAYLOAD_TOO_LARGE, "exceeded", b"over"), + buffered_terminal(StatusCode::GATEWAY_TIMEOUT, "timeout", b"adapter timeout"), + ); + let observed_drops = Arc::clone(&grant_drops); + let body = Body::from_stream(poll_fn(move |_| { + assert_eq!(observed_drops.load(Ordering::SeqCst), 0); + Poll::Ready(Some(Err(EdgeError::request_timeout( + "adapter deadline wrapper expired", + )))) + })); + + let response = dispatch_fallback(&app, body); + + assert_buffered_terminal_response( + &response, + StatusCode::GATEWAY_TIMEOUT, + "timeout", + b"adapter timeout", + ); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_no_fallback_dispatch(&handler_calls, &middleware_calls); + } + + #[test] + fn fallback_non_timeout_source_error_is_preserved_and_drops_grant_once() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let grant_drops = Arc::new(AtomicUsize::new(0)); + configure_fallback_policy( + &mut app, + &grant_drops, + 4, + Deadline::after(Duration::from_secs(1)), + buffered_terminal(StatusCode::PAYLOAD_TOO_LARGE, "exceeded", b"over"), + buffered_terminal(StatusCode::GATEWAY_TIMEOUT, "timeout", b"late"), + ); + let observed_drops = Arc::clone(&grant_drops); + let body = Body::from_stream(poll_fn(move |_| { + assert_eq!(observed_drops.load(Ordering::SeqCst), 0); + Poll::Ready(Some(Err(EdgeError::service_unavailable( + "source unavailable", + )))) + })); + + let response = dispatch_fallback(&app, body); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + assert!(response.headers().get("x-fallback-terminal").is_none()); + assert!( + str::from_utf8(response.body().as_bytes().expect("buffered body")) + .expect("JSON body") + .contains("service_unavailable") + ); + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_no_fallback_dispatch(&handler_calls, &middleware_calls); + } + + #[test] + fn cancelling_fallback_dispatch_drops_live_grant_once() { + let (mut app, handler_calls, middleware_calls) = guarded_fallback_app(); + let grant_drops = Arc::new(AtomicUsize::new(0)); + configure_fallback_policy( + &mut app, + &grant_drops, + 4, + Deadline::after(Duration::from_secs(1)), + buffered_terminal(StatusCode::PAYLOAD_TOO_LARGE, "exceeded", b"over"), + buffered_terminal(StatusCode::GATEWAY_TIMEOUT, "timeout", b"late"), + ); + let observed_drops = Arc::clone(&grant_drops); + let body_polls = Arc::new(AtomicUsize::new(0)); + let observed_polls = Arc::clone(&body_polls); + let body = Body::stream(poll_fn(move |_| { + assert_eq!(observed_drops.load(Ordering::SeqCst), 0); + observed_polls.fetch_add(1, Ordering::SeqCst); + Poll::Pending::> + })); + let head = IngressHeadParts::new( + Method::POST, + "/known".parse().expect("URI"), + Version::HTTP_11, + HeaderMap::new(), + ); + let IngressBeginOutcome::Admitted(prepared) = app + .begin_ingress(head, MonotonicInstant::now()) + .expect("begin ingress") + else { + panic!("expected admission"); + }; + let request = request_builder() + .method(Method::POST) + .uri("/known") + .body(body) + .expect("request"); + let mut dispatch = Box::pin(app.dispatch_admitted(prepared, request)); + let mut cx = Context::from_waker(noop_waker_ref()); + + assert!(matches!(dispatch.as_mut().poll(&mut cx), Poll::Pending)); + assert_eq!(body_polls.load(Ordering::SeqCst), 1); + assert_eq!(grant_drops.load(Ordering::SeqCst), 0); + + drop(dispatch); + + assert_eq!(grant_drops.load(Ordering::SeqCst), 1); + assert_no_fallback_dispatch(&handler_calls, &middleware_calls); + } + + #[test] + fn ordinary_admit_does_not_poll_fallback_bodies() { + let router = RouterService::builder() + .get("/known", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("unexpected") + }) + .build(); + let app = App::new(router); + + for (path, expected) in [ + ("/missing", StatusCode::NOT_FOUND), + ("/known", StatusCode::METHOD_NOT_ALLOWED), + ] { + let body_polls = Arc::new(AtomicUsize::new(0)); + let observed_polls = Arc::clone(&body_polls); + let body = Body::stream(poll_fn(move |_| { + observed_polls.fetch_add(1, Ordering::SeqCst); + Poll::Ready(None::) + })); + let request = request_builder() + .method(Method::POST) + .uri(path) + .body(body) + .expect("request"); + + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + )) + .expect("response"), + ); + + assert_eq!(response.status(), expected); + assert_eq!(body_polls.load(Ordering::SeqCst), 0); + } + } + + #[test] + fn ingress_two_phase_admission_precedes_body_construction() { + let router = RouterService::builder() + .post("/upload", |_ctx: RequestContext| async move { + Ok::<_, EdgeError>("accepted") + }) + .build(); + let mut app = App::new(router); + app.set_ingress_admission_policy(|head| { + assert!(matches!( + head.route_resolution(), + RouteResolution::Matched(metadata) if metadata.pattern() == "/upload" + )); + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: Deadline::after(Duration::from_secs(1)), + } + }); + let head = IngressHeadParts::new( + Method::POST, + "/upload".parse().expect("URI"), + Version::HTTP_11, + HeaderMap::new(), + ); + let start = MonotonicInstant::now(); + + let IngressBeginOutcome::Admitted(prepared) = + app.begin_ingress(head, start).expect("begin ingress") + else { + panic!("expected admission"); + }; + assert_eq!(prepared.request_start(), start); + + let request = request_builder() + .method(Method::POST) + .uri("/upload") + .body(Body::from("payload")) + .expect("request"); + let response = block_on(app.dispatch_admitted(prepared, request)); + assert_eq!(complete_envelope(response).status(), StatusCode::OK); + } + #[test] fn default_hooks_do_not_own_logging() { assert!(!DefaultHooks::owns_logging()); @@ -286,8 +3366,10 @@ mod tests { #[test] fn default_hooks_use_default_name_and_into_router() { - let app = DefaultHooks::build_app(); + let app = App::build::(PlatformMetadata::default()).expect("default app"); assert_eq!(app.name(), App::default_name()); + assert!(matches!(DefaultHooks::manifest(), BakedManifest::Absent)); + assert_eq!(DefaultHooks::manifest_json(), None); assert_eq!(DefaultHooks::stores(), StoresMetadata::default()); let router = app.into_router(); assert!(router.routes().is_empty()); diff --git a/crates/edgezero-core/src/body.rs b/crates/edgezero-core/src/body.rs index 43056e296..f3b419217 100644 --- a/crates/edgezero-core/src/body.rs +++ b/crates/edgezero-core/src/body.rs @@ -1,19 +1,38 @@ use std::fmt; use std::io; +use std::mem; +use std::pin::Pin; +use std::task::{Context, Poll}; use bytes::Bytes; -use futures_util::stream::{LocalBoxStream, Stream, StreamExt}; +use futures_util::stream::{self, LocalBoxStream, Stream, StreamExt}; use serde::Serialize; use serde::de::DeserializeOwned; -use crate::error::EdgeError; +use crate::error::{EdgeError, RequestBodyOverflowOrigin}; +use crate::outbound::{OutboundScope, OutboundScopeOwner, Revocable}; -/// Lightweight HTTP body that can either contain a single `Bytes` buffer or a streaming source of -/// chunks. The streaming variant is implemented with `LocalBoxStream` so it remains compatible with -/// `wasm32` targets that lack thread support. -pub enum Body { +pub type BodyStream = LocalBoxStream<'static, Result>; + +/// HTTP payload representation, independent of response-body lifetime ownership. +pub enum BodyPayload { Once(Bytes), - Stream(LocalBoxStream<'static, Result>), + Stream(BodyStream), +} + +/// A buffered or locally streamed HTTP body with optional response-egress ownership. +pub struct Body { + egress_owner: Option>, + payload: BodyPayload, +} + +struct ManualBodyStream { + owner: Option, + source: Option, +} + +struct ScopedBodyStream { + source: Option>, } impl Body { @@ -22,9 +41,47 @@ impl Body { /// [`Body::into_bytes_bounded`]. #[inline] pub fn as_bytes(&self) -> Option<&[u8]> { - match self { - Body::Once(bytes) => Some(bytes.as_ref()), - Body::Stream(_) => None, + self.buffered_bytes().map(Bytes::as_ref) + } + + pub(crate) fn attach_egress_owner( + mut self, + owner: OutboundScopeOwner, + ) -> Result { + if self.egress_owner.is_some() { + return Err(EdgeError::internal(anyhow::anyhow!( + "response body already owns an outbound scope" + ))); + } + self.egress_owner = Some(Box::new(owner)); + Ok(self) + } + + pub(crate) fn bind_outbound_scope(mut self, scope: &OutboundScope) -> Result { + if let Err(error) = scope.check() { + scope.with_cleanup(|| drop(self)); + return Err(error); + } + let payload = mem::replace(&mut self.payload, BodyPayload::Once(Bytes::new())); + self.payload = match payload { + BodyPayload::Once(bytes) => BodyPayload::Once(bytes), + BodyPayload::Stream(source) => { + let registered = Revocable::new(scope, source)?; + BodyPayload::Stream( + ScopedBodyStream { + source: Some(registered), + } + .boxed_local(), + ) + } + }; + Ok(self) + } + + pub(crate) fn buffered_bytes(&self) -> Option<&Bytes> { + match &self.payload { + BodyPayload::Once(bytes) => Some(bytes), + BodyPayload::Stream(_) => None, } } @@ -39,30 +96,59 @@ impl Body { where B: Into, { - Self::Once(bytes.into()) + Self::from_parts(BodyPayload::Once(bytes.into()), None) } #[inline] - pub fn from_stream(stream: S) -> Self + pub fn from_external_stream(stream: S) -> Self where S: Stream> + 'static, anyhow::Error: From, { - Self::Stream( - stream - .map(|res| res.map_err(anyhow::Error::from)) - .boxed_local(), + Self::from_stream( + stream.map(|result| { + result.map_err(|error| EdgeError::internal(anyhow::Error::from(error))) + }), ) } + pub(crate) fn from_parts( + payload: BodyPayload, + egress_owner: Option, + ) -> Self { + Self { + egress_owner: egress_owner.map(Box::new), + payload, + } + } + + #[inline] + pub fn from_stream(stream: S) -> Self + where + S: Stream> + 'static, + { + Self::from_parts(BodyPayload::Stream(stream.boxed_local()), None) + } + + pub(crate) fn has_egress_owner(&self) -> bool { + self.egress_owner.is_some() + } + /// Consume a buffered body and return its bytes, or `None` if this is a /// streaming body. To collect a streaming body, use /// [`Body::into_bytes_bounded`]. #[inline] pub fn into_bytes(self) -> Option { - match self { - Body::Once(bytes) => Some(bytes), - Body::Stream(_) => None, + let (payload, owner) = self.into_parts(); + match payload { + BodyPayload::Once(bytes) => { + drop(owner); + Some(bytes) + } + BodyPayload::Stream(source) => { + drop(Self::from_parts(BodyPayload::Stream(source), owner)); + None + } } } @@ -71,41 +157,67 @@ impl Body { /// Works for both buffered and streaming variants. /// /// # Errors - /// Returns [`EdgeError::bad_request`] if the body exceeds `max_size` bytes; or [`EdgeError::internal`] if the upstream stream errors. + /// Returns [`EdgeError::PayloadTooLarge`] with inbound origin if the body exceeds + /// `max_size` bytes. Source errors retain their original category. #[inline] pub async fn into_bytes_bounded(self, max_size: usize) -> Result { - match self { - Body::Once(bytes) => { + let (payload, owner) = self.into_parts(); + match payload { + BodyPayload::Once(bytes) => { + let _owner = owner; if bytes.len() > max_size { - return Err(EdgeError::bad_request("request body too large")); + return Err(EdgeError::payload_too_large( + RequestBodyOverflowOrigin::Inbound, + )); } Ok(bytes) } - Body::Stream(mut stream) => { + BodyPayload::Stream(source) => { + let mut stream = manual_stream(source, owner); let mut buf = Vec::new(); while let Some(result) = StreamExt::next(&mut stream).await { - let chunk = result.map_err(EdgeError::internal)?; - buf.extend_from_slice(&chunk); - if buf.len() > max_size { - return Err(EdgeError::bad_request("request body too large")); + let chunk = result?; + let next_len = buf.len().checked_add(chunk.len()).ok_or_else(|| { + EdgeError::payload_too_large(RequestBodyOverflowOrigin::Inbound) + })?; + if next_len > max_size { + return Err(EdgeError::payload_too_large( + RequestBodyOverflowOrigin::Inbound, + )); } + buf.extend_from_slice(&chunk); } Ok(Bytes::from(buf)) } } } + /// Separates payload and its exact optional egress owner without copying bytes. + /// The caller must retain the owner through terminal response egress. + #[must_use] #[inline] - pub fn into_stream(self) -> Option>> { - match self { - Body::Once(_) => None, - Body::Stream(stream) => Some(stream), + pub fn into_parts(mut self) -> (BodyPayload, Option) { + let payload = mem::replace(&mut self.payload, BodyPayload::Once(Bytes::new())); + (payload, self.take_egress_owner()) + } + + /// Explicitly converts to a manually consumed stream. An attached owner closes at + /// stream EOF, error, or drop; canonical egress extracts that owner before conversion. + /// + /// # Errors + /// Returns the original buffered body, including its owner, rather than discarding it. + #[inline] + pub fn into_stream(self) -> Result { + let (payload, owner) = self.into_parts(); + match payload { + BodyPayload::Once(bytes) => Err(Self::from_parts(BodyPayload::Once(bytes), owner)), + BodyPayload::Stream(source) => Ok(manual_stream(source, owner)), } } #[inline] pub fn is_stream(&self) -> bool { - matches!(self, Body::Stream(_)) + matches!(self.payload, BodyPayload::Stream(_)) } /// # Errors @@ -123,7 +235,11 @@ impl Body { where S: Stream + 'static, { - Self::Stream(stream.map(Ok::).boxed_local()) + Self::from_stream(stream.map(Ok::)) + } + + pub(crate) fn take_egress_owner(&mut self) -> Option { + self.egress_owner.take().map(|owner| *owner) } #[inline] @@ -141,15 +257,97 @@ impl Body { where T: DeserializeOwned, { - match self { - Body::Once(bytes) => serde_json::from_slice(bytes.as_ref()), - Body::Stream(_) => Err(serde_json::Error::io(io::Error::other( + match &self.payload { + BodyPayload::Once(bytes) => serde_json::from_slice(bytes.as_ref()), + BodyPayload::Stream(_) => Err(serde_json::Error::io(io::Error::other( "streaming body cannot be materialised as JSON", ))), } } } +impl ManualBodyStream { + fn release(&mut self) { + if let Some(owner) = self.owner.take() { + owner.with(|| { + let source = self.source.take(); + owner.close(); + drop(source); + }); + } + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for ManualBodyStream { + fn drop(&mut self) { + self.release(); + } +} + +impl Stream for ManualBodyStream { + type Item = Result; + + #[inline] + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = &mut *self; + let Some(source) = &mut this.source else { + return Poll::Ready(None); + }; + let polled = if let Some(owner) = &this.owner { + match owner.scope().check() { + Ok(()) => owner.with(|| source.as_mut().poll_next(cx)), + Err(error) => Poll::Ready(Some(Err(error))), + } + } else { + source.as_mut().poll_next(cx) + }; + if matches!(polled, Poll::Ready(None | Some(Err(_)))) { + this.release(); + } + polled + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.source.as_ref().map_or((0, Some(0)), Stream::size_hint) + } +} + +impl Stream for ScopedBodyStream { + type Item = Result; + + #[inline] + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let Some(source) = &self.source else { + return Poll::Ready(None); + }; + let observation = source + .with(cx, |payload, context| { + let terminal = payload.poll_next_unpin(context); + if matches!(terminal, Poll::Ready(None | Some(Err(_)))) { + // Include terminal cleanup in the revocation check before exposing EOF. + drop(mem::replace(payload, stream::empty().boxed_local())); + } + terminal + }) + .unwrap_or_else(|error| Poll::Ready(Some(Err(error)))); + if matches!(observation, Poll::Ready(None | Some(Err(_)))) { + drop(self.source.take()); + } + observation + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + if self.source.is_none() { + (0, Some(0)) + } else { + (0, None) + } + } +} + impl Default for Body { #[inline] fn default() -> Self { @@ -157,19 +355,47 @@ impl Default for Body { } } +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for Body { + #[inline] + fn drop(&mut self) { + if let Some(owner) = self.egress_owner.take() { + owner.with(|| { + let payload = mem::replace(&mut self.payload, BodyPayload::Once(Bytes::new())); + owner.close(); + drop(payload); + }); + } + } +} + impl fmt::Debug for Body { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Body") + .field("payload", &self.payload) + .field("egress_owner", &self.egress_owner.is_some()) + .finish() + } +} + +impl fmt::Debug for BodyPayload { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Body::Once(bytes) => f - .debug_struct("Body::Once") - .field("len", &bytes.len()) - .finish(), - Body::Stream(_) => f.debug_tuple("Body::Stream").finish(), + Self::Once(bytes) => f.debug_struct("Once").field("len", &bytes.len()).finish(), + Self::Stream(_) => f.debug_tuple("Stream").finish(), } } } +impl From for Body { + #[inline] + fn from(value: Bytes) -> Self { + Body::from_bytes(value) + } +} + impl From> for Body { #[inline] fn from(value: Vec) -> Self { @@ -198,12 +424,100 @@ impl From for Body { } } +fn manual_stream(source: BodyStream, optional_owner: Option) -> BodyStream { + match optional_owner { + Some(owner) => ManualBodyStream { + owner: Some(owner), + source: Some(source), + } + .boxed_local(), + None => source, + } +} + #[cfg(test)] mod tests { use super::*; + use crate::error::ResponseLimitReason; + use crate::http::StatusCode; + use crate::outbound::{ + IndependentOutboundProgress, OutboundExchangeOwner, OutboundScope, OutboundScopeOwner, + Revocable, + }; use futures::executor::block_on; use futures_util::stream; + use std::cell::Cell; use std::io; + use std::rc::Rc; + + #[cfg(panic = "unwind")] + struct ClosingPanic; + + #[cfg(panic = "unwind")] + struct ClosedSourceDrop(Rc>); + + #[cfg(panic = "unwind")] + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for ClosingPanic { + fn drop(&mut self) { + panic!("fixture owner cleanup failure"); + } + } + + #[cfg(panic = "unwind")] + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for ClosedSourceDrop { + fn drop(&mut self) { + let ambient = OutboundScope::capture(Rc::new(IndependentOutboundProgress)); + self.0.set(ambient.is_managed() && ambient.check().is_err()); + } + } + + #[cfg(panic = "unwind")] + #[test] + fn owner_cleanup_unwind_cannot_retain_or_unbind_payload() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + + for manual in [false, true] { + let owner = OutboundScopeOwner::response_body(); + drop( + owner + .scope() + .provider_state(|| ClosingPanic) + .expect("provider"), + ); + let closed_drop = Rc::new(Cell::new(false)); + let source_drop = ClosedSourceDrop(Rc::clone(&closed_drop)); + let polls = Rc::new(Cell::new(0_usize)); + let source_polls = Rc::clone(&polls); + let body = Body::from_stream(stream::poll_fn(move |_| { + let _held = &source_drop; + source_polls.set(source_polls.get().saturating_add(1)); + Poll::Ready(None) + })) + .attach_egress_owner(owner) + .expect("attached owner"); + if manual { + let mut source = body.into_stream().expect("manual source"); + let failed_close = catch_unwind(AssertUnwindSafe(|| block_on(source.next()))); + assert!(failed_close.is_err(), "fixture exercises cleanup unwind"); + assert!( + closed_drop.get(), + "source drops while its closed owner is active" + ); + assert!(block_on(source.next()).is_none()); + assert_eq!(polls.get(), 1, "source cannot be polled after unwind"); + } else { + let failed_close = catch_unwind(AssertUnwindSafe(|| drop(body))); + assert!(failed_close.is_err(), "fixture exercises cleanup unwind"); + assert!( + closed_drop.get(), + "payload drops before restoring ambient scope" + ); + assert_eq!(polls.get(), 0); + } + } + } #[test] fn as_bytes_returns_none_for_stream() { @@ -211,6 +525,123 @@ mod tests { assert!(body.as_bytes().is_none()); } + #[test] + fn attached_buffered_parts_preserve_bytes_and_exact_owner() { + let bytes = Bytes::from(vec![b'x'; 32]); + let original = bytes.as_ptr(); + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let body = Body::from(bytes) + .attach_egress_owner(owner) + .expect("attach") + .bind_outbound_scope(&scope) + .expect("bind buffered payload without copying or losing its owner"); + assert_eq!(body.as_bytes().expect("buffered").as_ptr(), original); + assert!(!body.is_stream()); + let (payload, transferred_owner) = body.into_parts(); + let BodyPayload::Once(transferred_bytes) = payload else { + panic!("buffered representation must remain Once"); + }; + assert_eq!(transferred_bytes.as_ptr(), original); + assert_eq!(transferred_bytes.len(), 32); + scope.check().expect("transferred owner remains live"); + drop(transferred_owner); + assert!(scope.check().is_err()); + } + + #[test] + fn manual_stream_conversion_retains_owner_until_consumption() { + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let body = Body::from_stream(stream::iter([Ok(Bytes::from_static(b"data"))])) + .attach_egress_owner(owner) + .expect("attach"); + let mut source = body.into_stream().expect("stream"); + scope.check().expect("unpolled stream owns scope"); + assert_eq!( + block_on(source.next()).expect("chunk").expect("bytes"), + "data" + ); + scope.check().expect("nonterminal chunk retains scope"); + assert!(block_on(source.next()).is_none()); + assert!(scope.check().is_err()); + } + + #[test] + fn manual_buffered_stream_conversion_returns_original_owner() { + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let body = Body::from("payload") + .attach_egress_owner(owner) + .expect("attach"); + let returned_body = body + .into_stream() + .err() + .expect("buffered body returned intact"); + assert_eq!(returned_body.as_bytes(), Some(b"payload".as_slice())); + scope.check().expect("returned body keeps ownership"); + drop(returned_body); + assert!(scope.check().is_err()); + } + + #[test] + fn dropping_attached_source_closes_roots_before_closed_scope_cleanup() { + struct Release(Rc>); + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for Release { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + struct Cleanup(Rc>); + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for Cleanup { + fn drop(&mut self) { + let ambient = OutboundScope::capture(Rc::new(IndependentOutboundProgress)); + self.0.set(ambient.check().is_err()); + } + } + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let released = Rc::new(Cell::new(0)); + let retained = Revocable::new(&scope, Release(Rc::clone(&released))).expect("root"); + let cleanup_closed = Rc::new(Cell::new(false)); + let cleanup = Cleanup(Rc::clone(&cleanup_closed)); + let body = Body::from_stream(stream::poll_fn(move |_| { + let _keep = &cleanup; + panic!("cleanup must not poll the source"); + })) + .attach_egress_owner(owner) + .expect("attach"); + drop(body); + assert!(retained.is_revoked()); + assert_eq!(released.get(), 1); + assert!(cleanup_closed.get()); + } + + #[test] + fn manual_stream_error_and_abandonment_close_attached_scope() { + for fail in [false, true] { + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let body = Body::from_stream(stream::once(async { + Err(EdgeError::gateway_timeout("source error")) + })) + .attach_egress_owner(owner) + .expect("attach"); + let mut source = body.into_stream().expect("stream"); + if fail { + assert!(matches!( + block_on(source.next()), + Some(Err(EdgeError::GatewayTimeout { .. })) + )); + assert!(scope.check().is_err()); + } + drop(source); + assert!(scope.check().is_err()); + } + } + #[test] fn collect_stream_body() { let body = Body::stream(stream::iter(vec![ @@ -234,11 +665,17 @@ mod tests { fn debug_formats_both_body_variants() { let buffered = Body::from("payload"); let buffered_debug = format!("{buffered:?}"); - assert!(buffered_debug.contains("Body::Once")); + assert_eq!( + buffered_debug, + "Body { payload: Once { len: 7 }, egress_owner: false }" + ); let stream = Body::stream(stream::iter(vec![Bytes::from_static(b"chunk")])); let stream_debug = format!("{stream:?}"); - assert!(stream_debug.contains("Body::Stream")); + assert_eq!( + stream_debug, + "Body { payload: Stream, egress_owner: false }" + ); } #[test] @@ -248,12 +685,12 @@ mod tests { } #[test] - fn from_stream_maps_errors() { + fn body_from_external_stream_maps_to_internal() { let source = stream::iter(vec![ Ok(Bytes::from_static(b"ok")), Err(io::Error::other("boom")), ]); - let body = Body::from_stream(source); + let body = Body::from_external_stream(source); let mut chunks = body.into_stream().expect("stream"); let (first, second) = block_on(async { let first = chunks.next().await.expect("first").expect("ok"); @@ -262,9 +699,134 @@ mod tests { }); assert_eq!(first, Bytes::from_static(b"ok")); let err = second.expect_err("error"); + assert!(matches!(err, EdgeError::Internal { .. })); assert!(err.to_string().contains("boom")); } + #[test] + fn body_from_stream_preserves_edge_error() { + let source = stream::iter([Err(EdgeError::response_too_large_with_reason( + "encoded cap", + ResponseLimitReason::EncodedBody, + ))]); + let body = Body::from_stream(source); + let mut chunks = body.into_stream().expect("stream"); + let error = block_on(chunks.next()) + .expect("item") + .expect_err("typed error"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::EncodedBody, + .. + } + )); + } + + #[test] + fn body_into_bytes_bounded_preserves_edge_error() { + let source = stream::iter([Err(EdgeError::gateway_timeout("expired"))]); + let error = + block_on(Body::from_stream(source).into_bytes_bounded(100)).expect_err("typed error"); + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[test] + fn body_stream_accepts_infallible_bytes() { + let body = Body::stream(stream::iter([ + Bytes::from_static(b"one"), + Bytes::from_static(b"two"), + ])); + let bytes = block_on(body.into_bytes_bounded(6)).expect("body"); + assert_eq!(bytes, Bytes::from_static(b"onetwo")); + assert_eq!( + Body::from(Bytes::from_static(b"bytes")).as_bytes(), + Some(b"bytes".as_slice()) + ); + } + + #[test] + fn bounded_body_overflow_has_a_distinct_413_category() { + for len in [7_usize, 8, 9] { + for streamed in [false, true] { + let bytes = Bytes::from(vec![b'x'; len]); + let body = if streamed { + Body::stream(stream::iter([bytes])) + } else { + Body::from(bytes) + }; + match block_on(body.into_bytes_bounded(8)) { + Ok(collected) => { + assert!(len <= 8, "overflow must not truncate into success"); + assert_eq!(collected.len(), len); + } + Err(error) => { + assert_eq!(len, 9); + assert_eq!(error.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!(error.kind(), "payload_too_large"); + } + } + } + } + } + + fn assert_scoped_terminal_drop_is_not_completion(errored: bool) { + let callback = Rc::new(OutboundScopeOwner::callback()); + let closing = Rc::clone(&callback); + let drops = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&drops); + let release = OutboundExchangeOwner::new(move || { + counted.set(counted.get().saturating_add(1)); + closing.close(); + }); + let scope = callback.with(|| OutboundScope::capture(Rc::new(IndependentOutboundProgress))); + let raw = stream::poll_fn(move |_| { + let _retained = &release; + Poll::Ready(errored.then(|| Err(EdgeError::bad_request("fixture source error")))) + }); + let body = Body::from_stream(raw) + .bind_outbound_scope(&scope) + .expect("bound source"); + let mut source = body.into_stream().expect("stream"); + let terminal = block_on(source.next()); + assert!( + matches!(terminal, Some(Err(EdgeError::Internal { .. }))), + "terminal source cleanup must not hide scope closure" + ); + assert!(block_on(source.next()).is_none()); + assert_eq!(drops.get(), 1); + } + + #[test] + fn scoped_eof_cleanup_closure_is_reported_as_internal() { + assert_scoped_terminal_drop_is_not_completion(false); + } + + #[test] + fn scoped_error_cleanup_closure_is_reported_as_internal() { + assert_scoped_terminal_drop_is_not_completion(true); + } + + #[test] + fn body_bounded_checks_before_append() { + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let source = stream::iter([ + Ok(Bytes::from_static(b"too-large")), + Ok(Bytes::from_static(b"must-not-poll")), + ]) + .inspect(move |_| observed.set(observed.get().saturating_add(1))); + let error = + block_on(Body::from_stream(source).into_bytes_bounded(3)).expect_err("over limit"); + assert!(matches!( + error, + EdgeError::PayloadTooLarge { + origin: RequestBodyOverflowOrigin::Inbound + } + )); + assert_eq!(polls.get(), 1); + } + #[test] fn from_vec_u8_builds_buffered_body() { let body = Body::from(vec![1_u8, 2_u8, 3_u8]); @@ -310,9 +872,10 @@ mod tests { } #[test] - fn into_stream_returns_none_for_buffered_body() { + fn into_stream_returns_buffered_body() { let body = Body::from("payload"); - assert!(body.into_stream().is_none()); + let returned_body = body.into_stream().err().expect("buffered body"); + assert_eq!(returned_body.as_bytes(), Some(b"payload".as_slice())); } #[test] diff --git a/crates/edgezero-core/src/canonical_form.rs b/crates/edgezero-core/src/canonical_form.rs index eaf67bf80..8b5839ffe 100644 --- a/crates/edgezero-core/src/canonical_form.rs +++ b/crates/edgezero-core/src/canonical_form.rs @@ -18,19 +18,46 @@ use base16ct::lower::encode_string; use serde_json::Value; use sha2::{Digest as _, Sha256}; -use std::fmt::Write as _; +use std::fmt::{self, Write}; -/// SHA-256 of the canonical form of `data`. See module docs. +struct CanonicalHasher(Sha256); + +impl CanonicalHasher { + fn push(&mut self, character: char) { + let mut bytes = [0_u8; 4]; + self.push_str(character.encode_utf8(&mut bytes)); + } + + fn push_str(&mut self, text: &str) { + self.0.update(text.as_bytes()); + } +} + +impl Write for CanonicalHasher { + fn write_char(&mut self, c: char) -> fmt::Result { + self.push(c); + Ok(()) + } + + fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { + fmt::write(self, args) + } + + fn write_str(&mut self, s: &str) -> fmt::Result { + self.push_str(s); + Ok(()) + } +} + +/// SHA-256 of the canonical form of `data`, without a serialized output buffer. #[must_use] #[inline] pub fn canonical_data_sha256(data: &Value) -> String { #[cfg(test)] test_hooks::CALL_COUNT.with(|cell| cell.set(cell.get().saturating_add(1))); - let mut buf = String::new(); - write_canonical(&mut buf, data); - let mut hasher = Sha256::new(); - hasher.update(buf.as_bytes()); - encode_string(&hasher.finalize()) + let mut sink = CanonicalHasher(Sha256::new()); + write_canonical(&mut sink, data); + encode_string(&sink.0.finalize()) } #[cfg(test)] @@ -46,7 +73,7 @@ pub(crate) mod test_hooks { } } -fn write_canonical(out: &mut String, value: &Value) { +fn write_canonical(out: &mut CanonicalHasher, value: &Value) { match value { Value::Null => out.push_str("null"), Value::Bool(true) => out.push_str("true"), @@ -87,9 +114,9 @@ fn write_canonical(out: &mut String, value: &Value) { clippy::panic, reason = "unreachable branch — serde_json::Number always exposes i64/u64/f64" )] -fn write_number(out: &mut String, n: &serde_json::Number) { +fn write_number(out: &mut CanonicalHasher, n: &serde_json::Number) { if let Some(int_val) = n.as_i64() { - // Infallible: writing to a String never fails. + // Infallible: this sink feeds bytes directly into the hasher. write!(out, "{int_val}").unwrap_or_default(); } else if let Some(uint_val) = n.as_u64() { write!(out, "{uint_val}").unwrap_or_default(); @@ -111,7 +138,7 @@ fn write_number(out: &mut String, n: &serde_json::Number) { } } -fn write_string(out: &mut String, raw: &str) { +fn write_string(out: &mut CanonicalHasher, raw: &str) { // Escape table byte-identical to serde_json::to_string(raw)'s // default. Pinned by spec 4.2 — DO NOT change without bumping // BlobEnvelope::version. @@ -142,6 +169,16 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn large_escaped_payload_matches_canonical_wire_hash() { + let value = json!({ "payload": "\n\t\u{0001}\"\\".repeat(0x0001_0000) }); + let wire = serde_json::to_vec(&value).expect("canonical single-key fixture"); + assert_eq!( + canonical_data_sha256(&value), + encode_string(&Sha256::digest(&wire)) + ); + } + #[test] fn object_keys_sorted_by_utf8_bytes() { let val_ba = json!({ "b": 1_i32, "a": 2_i32 }); diff --git a/crates/edgezero-core/src/compression.rs b/crates/edgezero-core/src/compression.rs index 5c1243f73..7db696779 100644 --- a/crates/edgezero-core/src/compression.rs +++ b/crates/edgezero-core/src/compression.rs @@ -1,85 +1,795 @@ +use async_compression::futures::bufread::{ + BrotliDecoder as AsyncBrotliDecoder, GzipDecoder as AsyncGzipDecoder, + ZlibDecoder as AsyncZlibDecoder, +}; +use async_stream::stream; +use bytes::Bytes; +use compression_codecs::{ + BrotliDecoder as PinnedBrotliCodec, GzipDecoder as PinnedGzipCodec, + ZlibDecoder as PinnedZlibCodec, +}; +use compression_core::util::PartialBuffer; +use futures::io::AsyncReadExt as _; +use futures_util::{StreamExt as _, TryStreamExt as _, stream as futures_stream}; use std::io; -use async_compression::futures::bufread::{BrotliDecoder, GzipDecoder}; -use async_stream::try_stream; -use bytes::Bytes; -use futures::TryStream; -use futures::io::{AsyncReadExt as _, BufReader}; -use futures::stream::Stream; -use futures_util::TryStreamExt as _; +use crate::body::BodyStream; +use crate::error::{BadGatewayDecodeReason, BadGatewayReason, EdgeError, ResponseLimitReason}; +use crate::http::HeaderMap; +use crate::http::header::CONTENT_ENCODING; const BUFFER_SIZE: usize = 8 * 1024; +const _: fn(brotli::BrotliResult) -> brotli_decompressor::BrotliResult = |result| result; +const _: fn(PartialBuffer<&[u8]>) = consume_pinned_compression_core; + +/// Conservative non-window charge for the decoder implementation pinned by +/// the workspace lockfiles. +/// +/// The source audit in +/// `docs/audits/2026-09-27-brotli-decoder-memory-accounting.md` derives a +/// 3,442,530-byte allocation-payload subtotal for the boxed decoder state, +/// Huffman storage, context maps, and bridge buffer in the pinned decoder +/// graph. The 16 MiB accounting reservation leaves conservative headroom for +/// wrapper objects and allocator overhead that are not claimed as exact heap +/// bounds. The ring allocation is charged separately as `2^WBITS`; dependency +/// upgrades must repeat the audit before changing either pin or charge. +#[expect( + clippy::decimal_literal_representation, + reason = "the decimal byte count is the reviewed public contract" +)] +pub const BROTLI_DECODER_FIXED_CHARGE_BYTES: u64 = 16_777_216; + +/// Conservative decoder-state charge shared by the pinned gzip and zlib codecs. +/// +/// The source audit in +/// `docs/audits/2026-09-29-flate-decoder-memory-accounting.md` derives a +/// 73,728-byte reviewed subtotal. The 1 MiB reservation leaves conservative +/// headroom for wrapper objects, the boxed coroutine, alignment, and allocator +/// variance that are not claimed as exact heap bounds. Dependency upgrades must +/// repeat the audit before changing either the pins or this charge. +pub const FLATE_DECODER_FIXED_CHARGE_BYTES: u64 = 0x0010_0000; + +#[derive(Debug, thiserror::Error)] +#[error("{0}")] +struct EdgeErrorCarrier(EdgeError); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum BrotliPrefix { + NeedSecondByte, + WindowBits(u8), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ContentEncoding { + Brotli, + Deflate, + Gzip, + Identity, + Passthrough(PassthroughReason), +} + +#[non_exhaustive] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PassthroughReason { + Malformed, + Stacked, + Unsupported(String), +} + +fn consume_pinned_compression_core(_buffer: PartialBuffer<&[u8]>) {} + +#[must_use] +#[inline] +pub fn classify_content_encoding(headers: &HeaderMap) -> ContentEncoding { + let mut values = headers.get_all(CONTENT_ENCODING).iter(); + let Some(header_value) = values.next() else { + return ContentEncoding::Identity; + }; + if values.next().is_some() { + return ContentEncoding::Passthrough(PassthroughReason::Stacked); + } + let raw_encoding = trim_optional_whitespace(header_value.as_bytes()); + let mut codings = raw_encoding.split(|byte| *byte == b','); + let Some(first_coding) = codings.next() else { + return ContentEncoding::Passthrough(PassthroughReason::Malformed); + }; + let first = trim_optional_whitespace(first_coding); + if !is_content_coding_token(first) { + return ContentEncoding::Passthrough(PassthroughReason::Malformed); + } + let mut stacked = false; + for coding in codings { + if !is_content_coding_token(trim_optional_whitespace(coding)) { + return ContentEncoding::Passthrough(PassthroughReason::Malformed); + } + stacked = true; + } + if stacked { + return ContentEncoding::Passthrough(PassthroughReason::Stacked); + } + if first.eq_ignore_ascii_case(b"br") { + ContentEncoding::Brotli + } else if first.eq_ignore_ascii_case(b"deflate") { + ContentEncoding::Deflate + } else if first.eq_ignore_ascii_case(b"gzip") { + ContentEncoding::Gzip + } else if first.eq_ignore_ascii_case(b"identity") { + ContentEncoding::Identity + } else { + let token = first + .iter() + .map(|byte| char::from(byte.to_ascii_lowercase())) + .collect(); + ContentEncoding::Passthrough(PassthroughReason::Unsupported(token)) + } +} + +fn is_content_coding_token(value: &[u8]) -> bool { + !value.is_empty() + && value.iter().all(|byte| { + byte.is_ascii_alphanumeric() + || matches!( + byte, + b'!' | b'#' + | b'$' + | b'%' + | b'&' + | b'\'' + | b'*' + | b'+' + | b'-' + | b'.' + | b'^' + | b'_' + | b'`' + | b'|' + | b'~' + ) + }) +} + +fn trim_optional_whitespace(value: &[u8]) -> &[u8] { + let start = value + .iter() + .position(|byte| !matches!(byte, b' ' | b'\t')) + .unwrap_or(value.len()); + let end = value + .iter() + .rposition(|byte| !matches!(byte, b' ' | b'\t')) + .map_or(start, |index| index.saturating_add(1)); + value.get(start..end).unwrap_or_default() +} + +/// Return the conservative decoder-state charge for an advertised Brotli window. +/// +/// # Errors +/// Returns [`EdgeError::BadRequest`] when `window_bits` is outside the Brotli range accepted by +/// `EdgeZero`, or a typed response-limit error if decoder-memory accounting overflows. +#[inline] +pub fn brotli_decoder_memory_charge(window_bits: u8) -> Result { + if !(10..=30).contains(&window_bits) { + return Err(EdgeError::bad_request( + "brotli window bits must be between 10 and 30 inclusive", + )); + } + let window = 1_u64.checked_shl(u32::from(window_bits)).ok_or_else(|| { + EdgeError::response_too_large_with_reason( + "brotli decoder memory accounting overflow", + ResponseLimitReason::DecoderMemory, + ) + })?; + BROTLI_DECODER_FIXED_CHARGE_BYTES + .checked_add(window) + .ok_or_else(|| { + EdgeError::response_too_large_with_reason( + "brotli decoder memory accounting overflow", + ResponseLimitReason::DecoderMemory, + ) + }) +} + /// Decode a stream of gzip-compressed chunks into plain bytes. +#[must_use] #[inline] -pub fn decode_gzip_stream(stream: S) -> impl Stream> -where - S: TryStream, Error = io::Error> + Unpin, -{ - try_stream! { - let reader = BufReader::new(stream.into_async_read()); - let mut decoder = GzipDecoder::new(reader); +pub fn decode_gzip_stream(stream: BodyStream, max_decoder_bytes: u64) -> BodyStream { + stream! { + if let Err(limit_error) = enforce_flate_decoder_memory(max_decoder_bytes, "gzip") { + yield Err(release_before_terminal(stream, limit_error)); + return; + } + let reader = stream.map_err(edge_error_to_io).into_async_read(); + let mut decoder = AsyncGzipDecoder::with_codec(reader, PinnedGzipCodec::new()); + decoder.multiple_members(true); let mut buffer = vec![0_u8; BUFFER_SIZE]; loop { - let read = decoder.read(&mut buffer).await?; + let read = match decoder.read(&mut buffer).await { + Ok(read) => read, + Err(source_error) => { + let error = release_before_terminal( + (decoder, buffer), + io_to_edge_error(source_error, BadGatewayDecodeReason::Gzip), + ); + yield Err(error); + return; + } + }; if read == 0 { break; } - let chunk = buffer.get(..read).ok_or_else(|| { - io::Error::other(format!( - "decoder reported {read}-byte read into a {BUFFER_SIZE}-byte buffer" - )) - })?; - yield Bytes::copy_from_slice(chunk); + let Some(chunk) = buffer.get(..read) else { + let decoder_error = codec_error( + BadGatewayDecodeReason::Gzip, + format!("decoder reported {read}-byte read into a {BUFFER_SIZE}-byte buffer"), + ); + let terminal_error = + release_before_terminal((decoder, buffer), decoder_error); + yield Err(terminal_error); + return; + }; + yield Ok(Bytes::copy_from_slice(chunk)); + } + } + .boxed_local() +} + +/// Decode one RFC 1950 zlib-wrapped DEFLATE stream into plain bytes. +#[must_use] +#[inline] +pub fn decode_deflate_stream(stream: BodyStream, max_decoder_bytes: u64) -> BodyStream { + stream! { + if let Err(limit_error) = enforce_flate_decoder_memory(max_decoder_bytes, "deflate") { + yield Err(release_before_terminal(stream, limit_error)); + return; } + let reader = stream.map_err(edge_error_to_io).into_async_read(); + let mut decoder = AsyncZlibDecoder::with_codec(reader, PinnedZlibCodec::new()); + let mut buffer = vec![0_u8; BUFFER_SIZE]; + + loop { + let read = match decoder.read(&mut buffer).await { + Ok(read) => read, + Err(source_error) => { + let error = release_before_terminal( + (decoder, buffer), + io_to_edge_error(source_error, BadGatewayDecodeReason::Deflate), + ); + yield Err(error); + return; + } + }; + if read == 0 { + break; + } + let Some(chunk) = buffer.get(..read) else { + let decoder_error = codec_error( + BadGatewayDecodeReason::Deflate, + format!("decoder reported {read}-byte read into a {BUFFER_SIZE}-byte buffer"), + ); + let terminal_error = + release_before_terminal((decoder, buffer), decoder_error); + yield Err(terminal_error); + return; + }; + yield Ok(Bytes::copy_from_slice(chunk)); + } + + let mut native_reader = decoder.into_inner(); + let mut trailing = [0_u8; 1]; + match native_reader.read(&mut trailing).await { + Ok(0) => {} + Ok(_) => { + let trailing_error = codec_error( + BadGatewayDecodeReason::Deflate, + "deflate response contains trailing data or a second stream", + ); + let terminal_error = release_before_terminal(native_reader, trailing_error); + yield Err(terminal_error); + } + Err(source_error) => { + let terminal_error = release_before_terminal( + native_reader, + io_to_edge_error(source_error, BadGatewayDecodeReason::Deflate), + ); + yield Err(terminal_error); + } + } + } + .boxed_local() +} + +fn enforce_flate_decoder_memory(max_decoder_bytes: u64, coding: &str) -> Result<(), EdgeError> { + if FLATE_DECODER_FIXED_CHARGE_BYTES > max_decoder_bytes { + return Err(EdgeError::response_too_large_with_reason( + format!( + "{coding} decoder requires {FLATE_DECODER_FIXED_CHARGE_BYTES} bytes; limit is {max_decoder_bytes}" + ), + ResponseLimitReason::DecoderMemory, + )); } + Ok(()) } /// Decode a stream of brotli-compressed chunks into plain bytes. +#[must_use] #[inline] -pub fn decode_brotli_stream(stream: S) -> impl Stream> -where - S: TryStream, Error = io::Error> + Unpin, -{ - try_stream! { - let reader = BufReader::new(stream.into_async_read()); - let mut decoder = BrotliDecoder::new(reader); +pub fn decode_brotli_stream( + mut source: BodyStream, + max_window_bits: u8, + max_decoder_bytes: u64, +) -> BodyStream { + stream! { + let mut prefix = Vec::with_capacity(2); + let mut initial_chunks = Vec::new(); + let window_bits = loop { + let Some(item) = source.next().await else { + let prefix_error = codec_error( + BadGatewayDecodeReason::Brotli, + "brotli stream ended before its window prefix", + ); + let terminal_error = release_before_terminal( + (source, initial_chunks, prefix), + prefix_error, + ); + yield Err(terminal_error); + return; + }; + let chunk = match item { + Ok(chunk) => chunk, + Err(source_error) => { + let terminal_error = + release_before_terminal((source, initial_chunks, prefix), source_error); + yield Err(terminal_error); + return; + } + }; + if chunk.is_empty() { + continue; + } + let prefix_remaining = 2_usize.saturating_sub(prefix.len()); + prefix.extend(chunk.iter().take(prefix_remaining)); + initial_chunks.push(chunk); + + match parse_brotli_prefix(&prefix) { + Ok(BrotliPrefix::WindowBits(bits)) => break bits, + Ok(BrotliPrefix::NeedSecondByte) => {} + Err(prefix_error) => { + let terminal_error = + release_before_terminal((source, initial_chunks, prefix), prefix_error); + yield Err(terminal_error); + return; + } + } + }; + + if window_bits > max_window_bits { + let limit_error = EdgeError::response_too_large_with_reason( + format!( + "brotli response advertises a {window_bits}-bit window; limit is {max_window_bits}" + ), + ResponseLimitReason::BrotliWindow, + ); + let terminal_error = + release_before_terminal((source, initial_chunks, prefix), limit_error); + yield Err(terminal_error); + return; + } + let charge = match brotli_decoder_memory_charge(window_bits) { + Ok(charge) => charge, + Err(charge_error) => { + let terminal_error = + release_before_terminal((source, initial_chunks, prefix), charge_error); + yield Err(terminal_error); + return; + } + }; + if charge > max_decoder_bytes { + let limit_error = EdgeError::response_too_large_with_reason( + format!("brotli decoder requires {charge} bytes; limit is {max_decoder_bytes}"), + ResponseLimitReason::DecoderMemory, + ); + let terminal_error = + release_before_terminal((source, initial_chunks, prefix), limit_error); + yield Err(terminal_error); + return; + } + + drop(prefix); + let mut decoded = decode_brotli_payload(source, initial_chunks); + while let Some(item) = decoded.next().await { + match item { + Ok(bytes) => yield Ok(bytes), + Err(error) => { + drop(decoded); + yield Err(error); + return; + } + } + } + } + .boxed_local() +} + +fn decode_brotli_payload(source: BodyStream, initial_chunks: Vec) -> BodyStream { + stream! { + let replayed_stream = futures_stream::iter(initial_chunks.into_iter().map(Ok)).chain(source); + let reader = replayed_stream.map_err(edge_error_to_io).into_async_read(); + let mut decoder = + AsyncBrotliDecoder::with_codec(reader, PinnedBrotliCodec::new()); let mut buffer = vec![0_u8; BUFFER_SIZE]; loop { - let read = decoder.read(&mut buffer).await?; + let read = match decoder.read(&mut buffer).await { + Ok(read) => read, + Err(source_error) => { + let terminal_error = release_before_terminal( + (decoder, buffer), + io_to_edge_error(source_error, BadGatewayDecodeReason::Brotli), + ); + yield Err(terminal_error); + return; + } + }; if read == 0 { break; } - let chunk = buffer.get(..read).ok_or_else(|| { - io::Error::other(format!( - "decoder reported {read}-byte read into a {BUFFER_SIZE}-byte buffer" - )) - })?; - yield Bytes::copy_from_slice(chunk); + let Some(chunk) = buffer.get(..read) else { + let decoder_error = codec_error( + BadGatewayDecodeReason::Brotli, + format!("decoder reported {read}-byte read into a {BUFFER_SIZE}-byte buffer"), + ); + let terminal_error = + release_before_terminal((decoder, buffer), decoder_error); + yield Err(terminal_error); + return; + }; + yield Ok(Bytes::copy_from_slice(chunk)); + } + + // Brotli has no multi-member HTTP representation. Recover any input + // read ahead by the decoder, then poll the native source once more so + // trailing bytes and late transport errors cannot be hidden by codec EOF. + let mut native_reader = decoder.into_inner(); + let mut trailing = [0_u8; 1]; + match native_reader.read(&mut trailing).await { + Ok(0) => {} + Ok(_) => { + let trailing_error = codec_error( + BadGatewayDecodeReason::Brotli, + "brotli response contains trailing data or a second stream", + ); + let terminal_error = release_before_terminal(native_reader, trailing_error); + yield Err(terminal_error); + } + Err(source_error) => { + let terminal_error = release_before_terminal( + native_reader, + io_to_edge_error(source_error, BadGatewayDecodeReason::Brotli), + ); + yield Err(terminal_error); + } } } + .boxed_local() +} + +fn release_before_terminal(owned: Owned, error: EdgeError) -> EdgeError { + drop(owned); + error +} + +fn codec_error(reason: BadGatewayDecodeReason, message: impl Into) -> EdgeError { + EdgeError::bad_gateway_with_reason(message, BadGatewayReason::Decode(reason)) +} + +fn edge_error_to_io(error: EdgeError) -> io::Error { + io::Error::other(EdgeErrorCarrier(error)) +} + +fn io_to_edge_error(error: io::Error, reason: BadGatewayDecodeReason) -> EdgeError { + let diagnostic = error.to_string(); + if let Some(source) = error.into_inner() + && let Ok(carrier) = source.downcast::() + { + return carrier.0; + } + codec_error( + reason, + format!("upstream response decode failed: {diagnostic}"), + ) +} + +fn parse_brotli_prefix(prefix: &[u8]) -> Result { + let Some(&first) = prefix.first() else { + return Ok(BrotliPrefix::NeedSecondByte); + }; + if first & 1 == 0 { + return Ok(BrotliPrefix::WindowBits(16)); + } + let short_code = match first & 0x0f { + 0x03 => Some(18), + 0x05 => Some(19), + 0x07 => Some(20), + 0x09 => Some(21), + 0x0b => Some(22), + 0x0d => Some(23), + 0x0f => Some(24), + _ => None, + }; + if let Some(bits) = short_code { + return Ok(BrotliPrefix::WindowBits(bits)); + } + let long_code = match first & 0x7f { + 0x71 => Some(15), + 0x61 => Some(14), + 0x51 => Some(13), + 0x41 => Some(12), + 0x31 => Some(11), + 0x21 => Some(10), + 0x01 => Some(17), + _ => None, + }; + if let Some(bits) = long_code { + return Ok(BrotliPrefix::WindowBits(bits)); + } + if first != 0x11 { + return Err(codec_error( + BadGatewayDecodeReason::Brotli, + "invalid brotli window prefix", + )); + } + let Some(&second) = prefix.get(1) else { + return Ok(BrotliPrefix::NeedSecondByte); + }; + let bits = second & 0x3f; + if !(10..=30).contains(&bits) { + return Err(codec_error( + BadGatewayDecodeReason::Brotli, + "invalid brotli large-window prefix", + )); + } + Ok(BrotliPrefix::WindowBits(bits)) } #[cfg(test)] mod tests { use super::*; - use brotli::CompressorWriter; - use flate2::{Compression, write::GzEncoder}; + use crate::error::BudgetSource; + use crate::http::{HeaderMap, HeaderValue}; + use brotli::{BrotliState, CompressorWriter, HuffmanCode, enc::StandardAlloc}; + use bytes::Bytes; + use flate2::{ + Compression, + write::{DeflateEncoder, GzEncoder, ZlibEncoder}, + }; use futures::executor::block_on; - use futures_util::stream; + use futures_util::FutureExt as _; + use futures_util::stream::{self, poll_fn}; + use std::cell::Cell; use std::io::Write as _; + use std::mem::size_of; + use std::rc::Rc; + use std::task::Poll; - #[test] - fn decode_gzip_stream_yields_plain_bytes() { + struct DropSignal(Rc>); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + fn source(items: Vec>) -> BodyStream { + stream::iter(items).boxed_local() + } + + fn tracked_source(first_item: Result, drops: &Rc>) -> BodyStream { + let signal = DropSignal(Rc::clone(drops)); + let mut pending_item = Some(first_item); + poll_fn(move |_context| { + let _keep_signal_alive = &signal; + Poll::Ready(pending_item.take()) + }) + .boxed_local() + } + + fn gzip(input: &[u8]) -> Vec { let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); - encoder.write_all(b"hello gzip").unwrap(); - let compressed = encoder.finish().unwrap(); + encoder.write_all(input).unwrap(); + encoder.finish().unwrap() + } + + fn raw_deflate(input: &[u8]) -> Vec { + let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(input).unwrap(); + encoder.finish().unwrap() + } + + fn zlib(input: &[u8]) -> Vec { + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(input).unwrap(); + encoder.finish().unwrap() + } + + fn brotli(input: &[u8]) -> Vec { + let mut encoded = Vec::new(); + let mut compressor = CompressorWriter::new(&mut encoded, 4096, 5, 21); + compressor.write_all(input).unwrap(); + drop(compressor); + encoded + } + + #[test] + fn content_encoding_classifier_covers_every_visible_shape() { + let cases = vec![ + (None, ContentEncoding::Identity), + (Some(b"identity".as_slice()), ContentEncoding::Identity), + (Some(b" GZIP \t".as_slice()), ContentEncoding::Gzip), + (Some(b"Br".as_slice()), ContentEncoding::Brotli), + (Some(b" DeFlAtE \t".as_slice()), ContentEncoding::Deflate), + ( + Some(b"zstd".as_slice()), + ContentEncoding::Passthrough(PassthroughReason::Unsupported(String::from("zstd"))), + ), + ( + Some(b" X_Custom ".as_slice()), + ContentEncoding::Passthrough(PassthroughReason::Unsupported(String::from( + "x_custom", + ))), + ), + ( + Some(b"gzip, deflate".as_slice()), + ContentEncoding::Passthrough(PassthroughReason::Stacked), + ), + ( + Some(b"gzip; level=1".as_slice()), + ContentEncoding::Passthrough(PassthroughReason::Malformed), + ), + ( + Some(b"gzip,,deflate".as_slice()), + ContentEncoding::Passthrough(PassthroughReason::Malformed), + ), + ( + Some(b"gzip,".as_slice()), + ContentEncoding::Passthrough(PassthroughReason::Malformed), + ), + ( + Some(b"".as_slice()), + ContentEncoding::Passthrough(PassthroughReason::Malformed), + ), + ( + Some(b" \t".as_slice()), + ContentEncoding::Passthrough(PassthroughReason::Malformed), + ), + ( + Some(b"\xff".as_slice()), + ContentEncoding::Passthrough(PassthroughReason::Malformed), + ), + ]; + + for (value, expected) in cases { + let mut headers = HeaderMap::new(); + if let Some(raw_value) = value { + headers.append( + "content-encoding", + HeaderValue::from_bytes(raw_value).expect("header"), + ); + } + assert_eq!(classify_content_encoding(&headers), expected); + } + + let mut repeated = HeaderMap::new(); + repeated.append("content-encoding", HeaderValue::from_static("gzip")); + repeated.append("content-encoding", HeaderValue::from_static("gzip")); + assert_eq!( + classify_content_encoding(&repeated), + ContentEncoding::Passthrough(PassthroughReason::Stacked) + ); + + let mut repeated_with_malformed_value = HeaderMap::new(); + repeated_with_malformed_value.append( + "content-encoding", + HeaderValue::from_bytes(b"\xff").expect("header"), + ); + repeated_with_malformed_value.append("content-encoding", HeaderValue::from_static("gzip")); + assert_eq!( + classify_content_encoding(&repeated_with_malformed_value), + ContentEncoding::Passthrough(PassthroughReason::Stacked) + ); + } + + #[test] + fn brotli_decoder_memory_charge_is_pinned_and_checked() { + assert_eq!(brotli_decoder_memory_charge(24_u8).unwrap(), 0x0200_0000); + assert!(matches!( + brotli_decoder_memory_charge(9), + Err(EdgeError::BadRequest { .. }) + )); + assert!(matches!( + brotli_decoder_memory_charge(31), + Err(EdgeError::BadRequest { .. }) + )); + } + + #[test] + fn brotli_fixed_charge_covers_source_audited_payload_subtotal() { + type DecoderState = BrotliState; + + const HUFFMAN_CODE_BYTES: u64 = 4; + const MAX_BLOCK_TYPES_OR_TREES: u64 = 256; + const MAX_HUFFMAN_TABLE_ENTRIES: u64 = 1_080; + const DECODER_STATE_RESERVE_BYTES: u64 = 0x0001_0000; + const RING_FIXED_SLACK_BYTES: u64 = 42 + 24; + + let decode_buffer_bytes = u64::try_from(BUFFER_SIZE).expect("buffer size fits u64"); + + assert_eq!( + u64::try_from(size_of::()).expect("HuffmanCode size fits u64"), + HUFFMAN_CODE_BYTES + ); + assert!( + u64::try_from(size_of::()).expect("decoder state size fits u64") + <= DECODER_STATE_RESERVE_BYTES + ); + + let context_map_table = MAX_HUFFMAN_TABLE_ENTRIES * HUFFMAN_CODE_BYTES; + let block_trees = 2 * 3 * MAX_HUFFMAN_TABLE_ENTRIES * HUFFMAN_CODE_BYTES; + let context_modes = MAX_BLOCK_TYPES_OR_TREES; + let context_maps = MAX_BLOCK_TYPES_OR_TREES * 64 + MAX_BLOCK_TYPES_OR_TREES * 4; + let huffman_group_indices = 3 * MAX_BLOCK_TYPES_OR_TREES * 4; + let huffman_group_codes = + 3 * MAX_BLOCK_TYPES_OR_TREES * MAX_HUFFMAN_TABLE_ENTRIES * HUFFMAN_CODE_BYTES; + let audited_payload_bytes = DECODER_STATE_RESERVE_BYTES + + context_map_table + + block_trees + + context_modes + + context_maps + + huffman_group_indices + + huffman_group_codes + + decode_buffer_bytes + + RING_FIXED_SLACK_BYTES; + + assert_eq!(audited_payload_bytes, 3_442_530); + assert!(audited_payload_bytes <= BROTLI_DECODER_FIXED_CHARGE_BYTES); + } + + #[test] + fn flate_fixed_charge_covers_source_audited_payload_subtotal() { + use miniz_oxide::inflate::stream::InflateState; - let stream = stream::iter(vec![Ok::, io::Error>(compressed)]); + const INFLATE_STATE_RESERVE_BYTES: u64 = 0x0001_0000; + const CODEC_WRAPPER_RESERVE_BYTES: u64 = 4_096; + const PUBLIC_CHARGE_BYTES: u64 = 0x0010_0000; + + let decode_buffer_bytes = u64::try_from(BUFFER_SIZE).expect("buffer size fits u64"); + let inflate_state_bytes = + u64::try_from(size_of::()).expect("inflate state size fits u64"); + let gzip_codec_bytes = + u64::try_from(size_of::()).expect("gzip codec size fits u64"); + let zlib_codec_bytes = + u64::try_from(size_of::()).expect("zlib codec size fits u64"); + + assert!(inflate_state_bytes <= INFLATE_STATE_RESERVE_BYTES); + assert!(gzip_codec_bytes <= CODEC_WRAPPER_RESERVE_BYTES); + assert!(zlib_codec_bytes <= CODEC_WRAPPER_RESERVE_BYTES); + assert_eq!(decode_buffer_bytes, 8_192); + assert_eq!(FLATE_DECODER_FIXED_CHARGE_BYTES, PUBLIC_CHARGE_BYTES); + + let audited_payload_bytes = INFLATE_STATE_RESERVE_BYTES + decode_buffer_bytes; + assert_eq!(audited_payload_bytes, 73_728); + assert!(audited_payload_bytes <= FLATE_DECODER_FIXED_CHARGE_BYTES); + } + + #[test] + fn decode_gzip_stream_yields_plain_bytes() { + let stream = source(vec![Ok(Bytes::from(gzip(b"hello gzip")))]); let decoded = block_on(async { - decode_gzip_stream(stream) + decode_gzip_stream(stream, FLATE_DECODER_FIXED_CHARGE_BYTES) .try_collect::>() .await .map(|chunks| chunks.concat()) @@ -90,15 +800,41 @@ mod tests { } #[test] - fn decode_brotli_stream_yields_plain_bytes() { - let mut brotli_bytes = Vec::new(); - let mut compressor = CompressorWriter::new(&mut brotli_bytes, 4096, 5, 21); - compressor.write_all(b"hello brotli").unwrap(); - drop(compressor); + fn decode_deflate_stream_yields_plain_bytes_from_zlib_framing() { + let stream = source(vec![Ok(Bytes::from(zlib(b"hello deflate")))]); + let decoded = block_on(async { + decode_deflate_stream(stream, FLATE_DECODER_FIXED_CHARGE_BYTES) + .try_collect::>() + .await + .map(|chunks| chunks.concat()) + }) + .unwrap(); + + assert_eq!(decoded, b"hello deflate"); + } + + #[test] + fn decode_deflate_stream_rejects_raw_rfc1951_framing() { + let stream = source(vec![Ok(Bytes::from(raw_deflate(b"raw deflate")))]); + let result = block_on( + decode_deflate_stream(stream, FLATE_DECODER_FIXED_CHARGE_BYTES) + .try_collect::>(), + ); - let stream = stream::iter(vec![Ok::, io::Error>(brotli_bytes)]); + assert!(matches!( + result, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Decode(BadGatewayDecodeReason::Deflate), + .. + }) + )); + } + + #[test] + fn decode_brotli_stream_yields_plain_bytes() { + let stream = source(vec![Ok(Bytes::from(brotli(b"hello brotli")))]); let decoded = block_on(async { - decode_brotli_stream(stream) + decode_brotli_stream(stream, 24, 1_u64 << 25) .try_collect::>() .await .map(|chunks| chunks.concat()) @@ -110,23 +846,365 @@ mod tests { #[test] fn decode_gzip_stream_surfaces_error_on_invalid_input() { - let garbage = b"this is definitely not a gzip member".to_vec(); - let stream = stream::iter(vec![Ok::, io::Error>(garbage)]); - let result = - block_on(async { decode_gzip_stream(stream).try_collect::>().await }); - assert!(result.is_err(), "invalid gzip must decode to an error"); + let garbage = Bytes::from_static(b"this is definitely not a gzip member"); + let stream = source(vec![Ok(garbage)]); + let result = block_on(async { + decode_gzip_stream(stream, FLATE_DECODER_FIXED_CHARGE_BYTES) + .try_collect::>() + .await + }); + assert!(matches!( + result, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Decode(BadGatewayDecodeReason::Gzip), + .. + }) + )); + } + + #[test] + fn decode_deflate_stream_surfaces_error_on_invalid_input() { + let garbage = Bytes::from_static(b"this is not a zlib-wrapped deflate stream"); + let result = block_on( + decode_deflate_stream(source(vec![Ok(garbage)]), FLATE_DECODER_FIXED_CHARGE_BYTES) + .try_collect::>(), + ); + assert!(matches!( + result, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Decode(BadGatewayDecodeReason::Deflate), + .. + }) + )); } #[test] fn decode_brotli_stream_surfaces_error_on_invalid_input() { // A high-bit-set lead byte is not a valid brotli stream prefix. - let garbage = vec![0xFF_u8; 64]; - let stream = stream::iter(vec![Ok::, io::Error>(garbage)]); + let garbage = Bytes::from(vec![0xFF_u8; 64]); + let stream = source(vec![Ok(garbage)]); let result = block_on(async { - decode_brotli_stream(stream) + decode_brotli_stream(stream, 24, 1_u64 << 25) .try_collect::>() .await }); - assert!(result.is_err(), "invalid brotli must decode to an error"); + assert!(matches!( + result, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Decode(BadGatewayDecodeReason::Brotli), + .. + }) + )); + } + + #[test] + fn terminal_stream_error_releases_source_before_yield() { + let gzip_drops = Rc::new(Cell::new(0_usize)); + let mut gzip_stream = decode_gzip_stream( + tracked_source(Ok(Bytes::from_static(b"not a gzip stream")), &gzip_drops), + FLATE_DECODER_FIXED_CHARGE_BYTES, + ); + block_on(gzip_stream.next()) + .expect("gzip result") + .expect_err("gzip decode error"); + assert_eq!(gzip_drops.get(), 1_usize); + + let deflate_drops = Rc::new(Cell::new(0_usize)); + let mut deflate_stream = decode_deflate_stream( + tracked_source( + Ok(Bytes::from_static(b"not a deflate stream")), + &deflate_drops, + ), + FLATE_DECODER_FIXED_CHARGE_BYTES, + ); + block_on(deflate_stream.next()) + .expect("deflate result") + .expect_err("deflate decode error"); + assert_eq!(deflate_drops.get(), 1_usize); + + let brotli_drops = Rc::new(Cell::new(0_usize)); + let mut brotli_stream = decode_brotli_stream( + tracked_source(Ok(Bytes::from_static(&[0xff])), &brotli_drops), + 24, + 1_u64 << 25, + ); + block_on(brotli_stream.next()) + .expect("brotli result") + .expect_err("brotli prefix error"); + assert_eq!(brotli_drops.get(), 1_usize); + } + + #[test] + fn decoder_carrier_restores_exact_edge_error() { + let expected = BudgetSource::RequestDeadline; + let stream = source(vec![ + Ok(Bytes::from_static(&[0x1f, 0x8b])), + Err(EdgeError::gateway_timeout_caused("late", expected)), + ]); + let result = block_on( + decode_gzip_stream(stream, FLATE_DECODER_FIXED_CHARGE_BYTES).try_collect::>(), + ); + assert!(matches!( + result, + Err(EdgeError::GatewayTimeout { cause, .. }) if cause == expected + )); + } + + #[test] + fn decode_gzip_drains_every_member_to_native_eof() { + let mut encoded = gzip(b"one"); + encoded.extend(gzip(b"two")); + let decoded = block_on( + decode_gzip_stream( + source(vec![Ok(Bytes::from(encoded))]), + FLATE_DECODER_FIXED_CHARGE_BYTES, + ) + .try_collect::>(), + ) + .unwrap() + .concat(); + assert_eq!(decoded, b"onetwo"); + + let encoded_member = gzip(b"one"); + let result = block_on( + decode_gzip_stream( + source(vec![ + Ok(Bytes::from(encoded_member)), + Err(EdgeError::bad_gateway_with_reason( + "late transport failure", + BadGatewayReason::Transport, + )), + ]), + FLATE_DECODER_FIXED_CHARGE_BYTES, + ) + .try_collect::>(), + ); + assert!(matches!( + result, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Transport, + .. + }) + )); + } + + #[test] + fn decode_deflate_requires_native_eof_and_preserves_late_source_errors() { + let mut with_trailing = zlib(b"one"); + with_trailing.extend_from_slice(b"trailing"); + let trailing_result = block_on( + decode_deflate_stream( + source(vec![Ok(Bytes::from(with_trailing))]), + FLATE_DECODER_FIXED_CHARGE_BYTES, + ) + .try_collect::>(), + ); + assert!(matches!( + trailing_result, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Decode(BadGatewayDecodeReason::Deflate), + .. + }) + )); + + let late_error_result = block_on( + decode_deflate_stream( + source(vec![ + Ok(Bytes::from(zlib(b"one"))), + Err(EdgeError::bad_gateway_with_reason( + "late transport failure", + BadGatewayReason::Transport, + )), + ]), + FLATE_DECODER_FIXED_CHARGE_BYTES, + ) + .try_collect::>(), + ); + assert!(matches!( + late_error_result, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Transport, + .. + }) + )); + } + + #[test] + fn decode_deflate_rejects_a_second_zlib_stream() { + let mut encoded = zlib(b"one"); + encoded.extend(zlib(b"two")); + + let result = block_on( + decode_deflate_stream( + source(vec![Ok(Bytes::from(encoded))]), + FLATE_DECODER_FIXED_CHARGE_BYTES, + ) + .try_collect::>(), + ); + + assert!(matches!( + result, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Decode(BadGatewayDecodeReason::Deflate), + .. + }) + )); + } + + #[test] + fn decode_deflate_waits_for_native_eof_after_codec_eof() { + let mut encoded = Some(Bytes::from(zlib(b"one"))); + let native = poll_fn(move |_context| { + if let Some(chunk) = encoded.take() { + Poll::Ready(Some(Ok(chunk))) + } else { + Poll::Pending + } + }) + .boxed_local(); + let mut decoded = decode_deflate_stream(native, FLATE_DECODER_FIXED_CHARGE_BYTES); + + let first = block_on(decoded.next()) + .expect("decoded item") + .expect("decoded bytes"); + assert_eq!(first, Bytes::from_static(b"one")); + assert!(decoded.next().now_or_never().is_none()); + } + + #[test] + fn flate_memory_limit_rejects_before_source_poll() { + let gzip_polls = Rc::new(Cell::new(0_usize)); + let observed_gzip_polls = Rc::clone(&gzip_polls); + let gzip_source = poll_fn(move |_context| { + observed_gzip_polls.set(observed_gzip_polls.get().saturating_add(1)); + Poll::Ready(None) + }) + .boxed_local(); + let gzip_error = block_on( + decode_gzip_stream( + gzip_source, + FLATE_DECODER_FIXED_CHARGE_BYTES.saturating_sub(1), + ) + .try_collect::>(), + ); + assert!(matches!( + gzip_error, + Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::DecoderMemory, + .. + }) + )); + assert_eq!(gzip_polls.get(), 0_usize); + + let deflate_polls = Rc::new(Cell::new(0_usize)); + let observed_deflate_polls = Rc::clone(&deflate_polls); + let deflate_source = poll_fn(move |_context| { + observed_deflate_polls.set(observed_deflate_polls.get().saturating_add(1)); + Poll::Ready(None) + }) + .boxed_local(); + let deflate_error = block_on( + decode_deflate_stream( + deflate_source, + FLATE_DECODER_FIXED_CHARGE_BYTES.saturating_sub(1), + ) + .try_collect::>(), + ); + assert!(matches!( + deflate_error, + Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::DecoderMemory, + .. + }) + )); + assert_eq!(deflate_polls.get(), 0_usize); + } + + #[test] + fn decode_brotli_rejects_trailing_data() { + let mut encoded = brotli(b"one"); + encoded.extend_from_slice(b"trailing"); + let result = block_on( + decode_brotli_stream(source(vec![Ok(Bytes::from(encoded))]), 24, 1_u64 << 25) + .try_collect::>(), + ); + assert!(matches!( + result, + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Decode(BadGatewayDecodeReason::Brotli), + .. + }) + )); + } + + #[test] + fn brotli_window_parser_covers_standard_and_large_forms() { + let standard = [ + (0x21, 10), + (0x31, 11), + (0x41, 12), + (0x51, 13), + (0x61, 14), + (0x71, 15), + (0x00, 16), + (0x01, 17), + (0x03, 18), + (0x05, 19), + (0x07, 20), + (0x09, 21), + (0x0b, 22), + (0x0d, 23), + (0x0f, 24), + ]; + for (byte, bits) in standard { + assert_eq!( + parse_brotli_prefix(&[byte]).unwrap(), + BrotliPrefix::WindowBits(bits) + ); + } + for bits in 10_u8..=30 { + assert_eq!( + parse_brotli_prefix(&[0x11, bits]).unwrap(), + BrotliPrefix::WindowBits(bits) + ); + } + assert_eq!( + parse_brotli_prefix(&[0x11]).unwrap(), + BrotliPrefix::NeedSecondByte + ); + parse_brotli_prefix(&[0x91, 24]).unwrap_err(); + parse_brotli_prefix(&[0x11, 9]).unwrap_err(); + parse_brotli_prefix(&[0x11, 31]).unwrap_err(); + } + + #[test] + fn brotli_window_rejects_before_decoder_allocation() { + let window_error = block_on( + decode_brotli_stream(source(vec![Ok(Bytes::from_static(&[0x0f]))]), 23, u64::MAX) + .try_collect::>(), + ); + assert!(matches!( + window_error, + Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::BrotliWindow, + .. + }) + )); + + let memory_error = block_on( + decode_brotli_stream( + source(vec![Ok(Bytes::from_static(&[0x0f]))]), + 24, + brotli_decoder_memory_charge(24).unwrap() - 1, + ) + .try_collect::>(), + ); + assert!(matches!( + memory_error, + Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::DecoderMemory, + .. + }) + )); } } diff --git a/crates/edgezero-core/src/config_store.rs b/crates/edgezero-core/src/config_store.rs index 428934616..6241860af 100644 --- a/crates/edgezero-core/src/config_store.rs +++ b/crates/edgezero-core/src/config_store.rs @@ -5,12 +5,17 @@ //! backends complete synchronously and resolve immediately. use std::fmt; +use std::ops::Deref; use std::sync::Arc; +use std::time::Duration; use anyhow::Error as AnyError; use async_trait::async_trait; use thiserror::Error; +use crate::error::EdgeError; +use crate::time::{DEADLINE_FAR_FUTURE, Deadline, MonotonicClock}; + // --------------------------------------------------------------------------- // Contract test macro // --------------------------------------------------------------------------- @@ -32,7 +37,7 @@ use thiserror::Error; /// AxumConfigStore::from_map([ /// ("contract.key.a".to_owned(), "value_a".to_owned()), /// ("contract.key.b".to_owned(), "value_b".to_owned()), -/// ]) +/// ], ConfigStoreLimits::default()).expect("bounded snapshot") /// }); /// ``` #[macro_export] @@ -55,7 +60,7 @@ macro_rules! config_store_contract_tests { run(async { assert_eq!( store.get("contract.key.a").await.expect("config value"), - Some("value_a".to_owned()) + Some($crate::ConfigValue::from("value_a")) ); }); } @@ -77,11 +82,11 @@ macro_rules! config_store_contract_tests { run(async { assert_eq!( store.get("contract.key.a").await.expect("first config value"), - Some("value_a".to_owned()) + Some($crate::ConfigValue::from("value_a")) ); assert_eq!( store.get("contract.key.b").await.expect("second config value"), - Some("value_b".to_owned()) + Some($crate::ConfigValue::from("value_b")) ); }); } @@ -122,7 +127,7 @@ macro_rules! config_store_contract_tests { run(async { assert_eq!( handle.get("contract.key.a").await.expect("handle value"), - Some("value_a".to_owned()) + Some($crate::ConfigValue::from("value_a")) ); assert_eq!( handle.get("contract.key.missing").await.expect("handle miss"), @@ -156,6 +161,76 @@ macro_rules! config_store_contract_tests { }; } +pub const DEFAULT_CONFIG_BACKEND_BYTES: u64 = 0x0100_0000; +pub const DEFAULT_CONFIG_BLOB_BYTES: u64 = 0x0080_0000; +pub const DEFAULT_CONFIG_EXTRACTION_BYTES: u64 = 0x0100_0000; +pub const DEFAULT_CONFIG_EXTRACTION_TIMEOUT: Duration = Duration::from_secs(30); +pub const DEFAULT_CONFIG_JSON_DEPTH: u32 = 64; +pub const DEFAULT_CONFIG_JSON_NODES: u32 = 8_192; +pub const DEFAULT_CONFIG_SECRET_BYTES: u64 = 0x0010_0000; + +/// Per-extraction limits shared by the root config blob and every referenced secret. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ConfigExtractionLimits { + pub max_backend_bytes: u64, + pub max_blob_bytes: u64, + pub max_json_depth: u32, + pub max_json_nodes: u32, + pub max_secret_bytes: u64, + pub max_total_bytes: u64, + pub timeout: Duration, +} + +impl ConfigExtractionLimits { + /// Validates this startup policy before an adapter begins serving. + /// + /// # Errors + /// Returns an internal policy error for zero, inconsistent, or unbounded values. + #[inline] + pub fn validate(self) -> Result { + if self.max_backend_bytes == 0 + || self.max_blob_bytes == 0 + || self.max_secret_bytes == 0 + || self.max_total_bytes == 0 + { + return Err(EdgeError::internal(anyhow::anyhow!( + "config extraction byte limits must be nonzero" + ))); + } + if self.max_total_bytes < self.max_blob_bytes.max(self.max_secret_bytes) { + return Err(EdgeError::internal(anyhow::anyhow!( + "config extraction total byte limit is below a per-value limit" + ))); + } + if self.max_json_depth == 0 || self.max_json_depth > 127 || self.max_json_nodes == 0 { + return Err(EdgeError::internal(anyhow::anyhow!( + "config extraction JSON structure limits must be finite and nonzero" + ))); + } + if self.timeout.is_zero() || self.timeout > DEADLINE_FAR_FUTURE { + return Err(EdgeError::internal(anyhow::anyhow!( + "config extraction timeout must be finite and nonzero" + ))); + } + Ok(self) + } +} + +impl Default for ConfigExtractionLimits { + #[inline] + fn default() -> Self { + Self { + max_backend_bytes: DEFAULT_CONFIG_BACKEND_BYTES, + max_blob_bytes: DEFAULT_CONFIG_BLOB_BYTES, + max_secret_bytes: DEFAULT_CONFIG_SECRET_BYTES, + max_total_bytes: DEFAULT_CONFIG_EXTRACTION_BYTES, + max_json_depth: DEFAULT_CONFIG_JSON_DEPTH, + max_json_nodes: DEFAULT_CONFIG_JSON_NODES, + timeout: DEFAULT_CONFIG_EXTRACTION_TIMEOUT, + } + } +} + // --------------------------------------------------------------------------- // Trait // --------------------------------------------------------------------------- @@ -166,6 +241,9 @@ macro_rules! config_store_contract_tests { #[derive(Debug, Error)] #[non_exhaustive] pub enum ConfigStoreError { + /// The absolute read deadline expired before a complete value was available. + #[error("config store read deadline exceeded")] + DeadlineExceeded, /// An unexpected backend or provider failure occurred. #[error("config store error: {source}")] Internal { source: AnyError }, @@ -175,6 +253,9 @@ pub enum ConfigStoreError { /// The configured backend cannot currently serve requests. #[error("config store unavailable: {message}")] Unavailable { message: String }, + /// The value or guest-visible backend read exceeded its supplied allowance. + #[error("config store value exceeds configured byte limit")] + ValueTooLarge, } impl ConfigStoreError { @@ -212,10 +293,110 @@ impl ConfigStoreError { } } +/// Classification retained when a declared config store cannot be opened. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ConfigStoreOpenFailure { + /// The declared binding or store name is invalid or missing. + InvalidConfiguration, + /// The provider could not resolve or serve the declared binding. + Unavailable, +} + +/// An immutable UTF-8 configuration value. Cloning shares its allocation. +/// +/// Each value owns only its own payload, never a slice retaining a larger store file. +/// Debug output deliberately excludes configuration contents. +#[derive(Clone, Default, Eq, PartialEq)] +pub struct ConfigValue { + value: Arc, +} + +impl ConfigValue { + /// Borrow the UTF-8 payload without allocating. + #[inline] + #[must_use] + pub fn as_str(&self) -> &str { + &self.value + } +} + +impl AsRef for ConfigValue { + #[inline] + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl fmt::Debug for ConfigValue { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ConfigValue") + .field("bytes", &self.len()) + .finish_non_exhaustive() + } +} + +impl Deref for ConfigValue { + type Target = str; + + #[inline] + fn deref(&self) -> &str { + self.as_str() + } +} + +impl From<&str> for ConfigValue { + #[inline] + fn from(value: &str) -> Self { + Self { + value: Arc::from(value), + } + } +} + +impl From for ConfigValue { + #[inline] + fn from(value: String) -> Self { + Self { + value: Arc::from(value), + } + } +} + +impl serde::Serialize for ConfigValue { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl ConfigStoreOpenFailure { + fn as_error(self) -> ConfigStoreError { + match self { + Self::Unavailable => { + ConfigStoreError::unavailable("declared config store is unavailable") + } + Self::InvalidConfiguration => ConfigStoreError::internal(anyhow::anyhow!( + "declared config store could not be opened" + )), + } + } +} + +/// Result of one bounded store lookup, including all bytes exposed to guest code. +#[derive(Debug)] +pub struct BoundedStoreRead { + pub backend_bytes: u64, + pub value: Option, +} + /// Object-safe interface for read-only configuration store backends. /// /// Implementations exist per adapter: -/// - `AxumConfigStore` (axum adapter) — env vars + in-memory defaults for dev +/// - `AxumConfigStore` (axum adapter) — bounded immutable local-file snapshots /// - `FastlyConfigStore` (fastly adapter) — Fastly Config Store /// - `CloudflareConfigStore` (cloudflare adapter) — Cloudflare KV namespace /// - `SpinConfigStore` (spin adapter) — Spin KV (`spin_sdk::key_value::Store`) @@ -225,9 +406,82 @@ pub trait ConfigStore: Send + Sync { /// /// # Errors /// Returns [`ConfigStoreError`] if `key` is invalid or the backend is unavailable. - async fn get(&self, key: &str) -> Result, ConfigStoreError>; + async fn get(&self, key: &str) -> Result, ConfigStoreError>; + + /// Retrieves one value under an absolute deadline and independent backend/value caps. + /// + /// Implementations must define their deadline and allocation behavior explicitly. There is + /// no compatibility default because awaiting an unbounded provider call can hide a + /// never-ready future. + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError>; +} + +#[cfg(test)] +macro_rules! ready_config_store_bounded_read { + () => { + fn get_bounded<'life0, 'life1, 'life2, 'async_trait>( + &'life0 self, + key: &'life1 str, + clock: &'life2 $crate::time::MonotonicClock, + deadline: $crate::time::Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> ::std::pin::Pin< + Box< + dyn ::std::future::Future< + Output = Result< + $crate::config_store::BoundedStoreRead<$crate::ConfigValue>, + $crate::config_store::ConfigStoreError, + >, + > + 'async_trait, + >, + > + where + 'life0: 'async_trait, + 'life1: 'async_trait, + 'life2: 'async_trait, + Self: 'async_trait, + { + Box::pin(async move { + if deadline.is_expired_at(clock.now()) { + return Err($crate::config_store::ConfigStoreError::DeadlineExceeded); + } + let Some(result) = ::futures_util::FutureExt::now_or_never(self.get(key)) else { + return Err($crate::config_store::ConfigStoreError::internal( + ::anyhow::anyhow!("ready config-store test fixture returned pending"), + )); + }; + if deadline.is_expired_at(clock.now()) { + return Err($crate::config_store::ConfigStoreError::DeadlineExceeded); + } + let value = result?; + let backend_bytes = value.as_ref().map_or(Ok(0_u64), |stored_value| { + u64::try_from(stored_value.len()).map_err(|_length_error| { + $crate::config_store::ConfigStoreError::ValueTooLarge + }) + })?; + if backend_bytes > max_backend_bytes || backend_bytes > max_value_bytes { + return Err($crate::config_store::ConfigStoreError::ValueTooLarge); + } + Ok($crate::config_store::BoundedStoreRead { + backend_bytes, + value, + }) + }) + } + }; } +#[cfg(test)] +pub(crate) use ready_config_store_bounded_read; + // --------------------------------------------------------------------------- // Handle // --------------------------------------------------------------------------- @@ -246,15 +500,42 @@ impl fmt::Debug for ConfigStoreHandle { } impl ConfigStoreHandle { + /// Keep a declared binding resolvable when its provider open fails. + /// + /// Provider diagnostics should be logged by the adapter, not stored in this handle. + #[inline] + #[must_use] + pub fn failed_open(failure: ConfigStoreOpenFailure) -> Self { + Self::new(Arc::new(FailedOpenConfigStore { failure })) + } + /// Get a config value by key. /// /// # Errors /// Returns [`ConfigStoreError`] if `key` is invalid or the backend is unavailable. #[inline] - pub async fn get(&self, key: &str) -> Result, ConfigStoreError> { + pub async fn get(&self, key: &str) -> Result, ConfigStoreError> { self.store.get(key).await } + /// Get a config value under one absolute deadline and two byte limits. + /// + /// # Errors + /// Preserves the provider's typed bounded-read error. + #[inline] + pub async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + self.store + .get_bounded(key, clock, deadline, max_backend_bytes, max_value_bytes) + .await + } + /// Create a new handle wrapping a config store implementation. #[inline] pub fn new(store: Arc) -> Self { @@ -262,6 +543,77 @@ impl ConfigStoreHandle { } } +struct FailedOpenConfigStore { + failure: ConfigStoreOpenFailure, +} + +#[async_trait(?Send)] +impl ConfigStore for FailedOpenConfigStore { + async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + Err(self.failure.as_error()) + } + + async fn get_bounded( + &self, + _key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + finish_bounded_config_read::( + Err(self.failure.as_error()), + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + } +} + +/// Applies post-read deadline precedence and byte caps to one materialized config result. +/// +/// This helper does not dispatch, await, cancel, or time out the provider operation. A +/// [`ConfigStore::get_bounded`] implementation must still reject an already-expired deadline and +/// arrange any target-specific timer race before calling it. +/// +/// # Errors +/// Returns the provider error, [`ConfigStoreError::DeadlineExceeded`], or +/// [`ConfigStoreError::ValueTooLarge`] according to post-read precedence. +/// Conversion must preserve the UTF-8 bytes exposed by `AsRef`; `String` +/// becomes shared only after the caps pass. Conversion overlap is adapter-accounted. +#[inline] +pub fn finish_bounded_config_read( + result: Result, ConfigStoreError>, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, +) -> Result, ConfigStoreError> +where + T: AsRef + Into, +{ + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + let value = result?; + let backend_bytes = value.as_ref().map_or(Ok(0_u64), |stored_value| { + u64::try_from(stored_value.as_ref().len()) + .map_err(|_length_error| ConfigStoreError::ValueTooLarge) + })?; + if backend_bytes > max_backend_bytes || backend_bytes > max_value_bytes { + return Err(ConfigStoreError::ValueTooLarge); + } + let shared_value = value.map(Into::into); + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + Ok(BoundedStoreRead { + backend_bytes, + value: shared_value, + }) +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -275,27 +627,34 @@ mod tests { ); use super::*; + use crate::time::{Deadline, MonotonicClock, MonotonicInstant}; use futures::executor::block_on; use std::collections::HashMap; + use std::sync::Mutex; + use std::time::Duration; struct FailingConfigStore; struct TestConfigStore { - data: HashMap, + data: HashMap, } #[async_trait(?Send)] impl ConfigStore for FailingConfigStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get(&self, _key: &str) -> Result, ConfigStoreError> { Err(ConfigStoreError::unavailable("backend offline")) } + + ready_config_store_bounded_read!(); } #[async_trait(?Send)] impl ConfigStore for TestConfigStore { - async fn get(&self, key: &str) -> Result, ConfigStoreError> { + async fn get(&self, key: &str) -> Result, ConfigStoreError> { Ok(self.data.get(key).cloned()) } + + ready_config_store_bounded_read!(); } impl TestConfigStore { @@ -303,7 +662,7 @@ mod tests { Self { data: entries .iter() - .map(|(key, value)| ((*key).to_owned(), (*value).to_owned())) + .map(|(key, value)| ((*key).to_owned(), ConfigValue::from(*value))) .collect(), } } @@ -313,6 +672,70 @@ mod tests { ConfigStoreHandle::new(Arc::new(TestConfigStore::new(entries))) } + #[test] + fn shared_config_value_clones_payload_without_revealing_it_in_debug() { + let value = ConfigValue::from("credential=private"); + let clone = value.clone(); + assert_eq!(clone.as_ptr(), value.as_ptr()); + assert_eq!(clone.as_str(), "credential=private"); + assert!(!format!("{value:?}").contains("credential=private")); + assert_eq!( + serde_json::to_value(&value).expect("serialize"), + serde_json::json!("credential=private") + ); + } + + #[test] + fn bounded_read_caps_precede_conversion_and_conversion_expiry_wins() { + use std::sync::atomic::{AtomicUsize, Ordering}; + struct ConversionProbe { + calls: Arc, + text: String, + } + impl AsRef for ConversionProbe { + fn as_ref(&self) -> &str { + &self.text + } + } + impl From for ConfigValue { + #[inline] + fn from(value: ConversionProbe) -> Self { + value.calls.fetch_add(1, Ordering::SeqCst); + Self::from(value.text) + } + } + let start = MonotonicInstant::now(); + let end = start.checked_add(Duration::from_secs(1)).expect("end"); + for (cap, expected_conversions) in [(4, 0), (5, 1)] { + let calls = Arc::new(AtomicUsize::new(0)); + let samples = Arc::clone(&calls); + let clock = MonotonicClock::new(move || { + if samples.load(Ordering::SeqCst) == 0 { + start + } else { + end + } + }); + let error = finish_bounded_config_read( + Ok(Some(ConversionProbe { + calls: Arc::clone(&calls), + text: "value".to_owned(), + })), + &clock, + Deadline::at_instant(end), + cap, + cap, + ) + .expect_err("bounded failure"); + if cap == 4 { + assert!(matches!(error, ConfigStoreError::ValueTooLarge)); + } else { + assert!(matches!(error, ConfigStoreError::DeadlineExceeded)); + } + assert_eq!(calls.load(Ordering::SeqCst), expected_conversions); + } + } + #[test] fn config_store_get_returns_none_for_missing_key() { let store_handle = handle(&[]); @@ -327,8 +750,139 @@ mod tests { let store_handle = handle(&[("feature.checkout", "true")]); assert_eq!( block_on(store_handle.get("feature.checkout")).expect("config value"), - Some("true".to_owned()) + Some(ConfigValue::from("true")) + ); + } + + #[test] + fn bounded_exact_cap_succeeds_and_over_cap_discards_value() { + let store_handle = handle(&[("feature.checkout", "true")]); + let clock = MonotonicClock::default(); + let exact = block_on(store_handle.get_bounded( + "feature.checkout", + &clock, + Deadline::after(Duration::from_secs(1)), + 4, + 4, + )) + .expect("exact bounded read"); + assert_eq!(exact.backend_bytes, 4); + assert_eq!(exact.value.as_deref(), Some("true")); + + let error = block_on(store_handle.get_bounded( + "feature.checkout", + &clock, + Deadline::after(Duration::from_secs(1)), + 3, + 4, + )) + .expect_err("backend cap"); + assert!(matches!(error, ConfigStoreError::ValueTooLarge)); + } + + #[test] + fn bounded_read_uses_injected_clock() { + let store_handle = handle(&[("feature.checkout", "true")]); + let process_now = MonotonicInstant::now(); + let injected_now = process_now + .checked_sub(Duration::from_mins(1)) + .expect("injected instant"); + let deadline = Deadline::at_instant( + injected_now + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), ); + let clock = MonotonicClock::new(move || injected_now); + + let read = block_on(store_handle.get_bounded("feature.checkout", &clock, deadline, 4, 4)) + .expect("the application clock is still before its deadline"); + + assert_eq!(read.value.as_deref(), Some("true")); + } + + #[test] + fn bounded_read_deadline_wins_ready_provider_error() { + struct AdvancingErrorStore { + now: Arc>, + terminal: MonotonicInstant, + } + + #[async_trait(?Send)] + impl ConfigStore for AdvancingErrorStore { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + *self.now.lock().expect("clock lock") = self.terminal; + Err(ConfigStoreError::unavailable("provider failed at expiry")) + } + + ready_config_store_bounded_read!(); + } + + let start = MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let now = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let store_handle = ConfigStoreHandle::new(Arc::new(AdvancingErrorStore { now, terminal })); + + let error = block_on(store_handle.get_bounded( + "feature.checkout", + &clock, + Deadline::at_instant(terminal), + 4, + 4, + )) + .expect_err("equality expiry must beat a ready provider error"); + + assert!(matches!(error, ConfigStoreError::DeadlineExceeded)); + } + + #[test] + fn bounded_limit_defaults_are_finite_and_validation_rejects_invalid_relationships() { + let limits = ConfigExtractionLimits::default(); + assert_eq!(limits.max_blob_bytes, DEFAULT_CONFIG_BLOB_BYTES); + assert_eq!(limits.max_backend_bytes, DEFAULT_CONFIG_BACKEND_BYTES); + assert_eq!(limits.max_secret_bytes, DEFAULT_CONFIG_SECRET_BYTES); + assert_eq!(limits.max_total_bytes, DEFAULT_CONFIG_EXTRACTION_BYTES); + assert_eq!(limits.max_json_depth, DEFAULT_CONFIG_JSON_DEPTH); + assert_eq!(limits.max_json_nodes, DEFAULT_CONFIG_JSON_NODES); + assert_eq!(limits.timeout, DEFAULT_CONFIG_EXTRACTION_TIMEOUT); + limits.validate().expect("valid defaults"); + + let invalid_total = ConfigExtractionLimits { + max_total_bytes: 1, + ..limits + }; + invalid_total + .validate() + .expect_err("total below per-value cap"); + + let invalid_timeout = ConfigExtractionLimits { + timeout: Duration::ZERO, + ..limits + }; + invalid_timeout.validate().expect_err("zero timeout"); + + for invalid in [ + ConfigExtractionLimits { + max_json_depth: 0, + ..limits + }, + ConfigExtractionLimits { + max_json_depth: 128, + ..limits + }, + ConfigExtractionLimits { + max_json_nodes: 0, + ..limits + }, + ] { + invalid.validate().expect_err("invalid JSON structure cap"); + } } #[test] @@ -354,7 +908,7 @@ mod tests { let store_handle = ConfigStoreHandle::new(store); assert_eq!( block_on(store_handle.get("a")).expect("arc-backed config"), - Some("1".to_owned()) + Some(ConfigValue::from("1")) ); } @@ -365,12 +919,109 @@ mod tests { assert!(matches!(err, ConfigStoreError::Unavailable { .. })); } + #[test] + fn declared_failed_open_remains_resolvable_and_typed() { + use crate::store_registry::{ConfigStoreBinding, StoreRegistry}; + + let binding = ConfigStoreBinding { + handle: ConfigStoreHandle::failed_open(ConfigStoreOpenFailure::Unavailable), + default_key: "config".to_owned(), + }; + let registry = StoreRegistry::from_parts( + [("primary".to_owned(), binding)].into(), + "primary".to_owned(), + ) + .expect("declared binding remains registered"); + + assert_eq!(registry.ids().collect::>(), vec!["primary"]); + let clock = MonotonicClock::default(); + let error = block_on( + registry + .default() + .expect("default handle") + .handle + .get_bounded( + "config", + &clock, + Deadline::after(Duration::from_secs(1)), + 1, + 1, + ), + ) + .expect_err("failed open is not a missing key"); + assert!(matches!(error, ConfigStoreError::Unavailable { .. })); + } + + #[test] + fn failed_named_open_does_not_disappear_beside_healthy_default() { + use crate::store_registry::{ConfigStoreBinding, StoreRegistry}; + + let registry = StoreRegistry::from_parts( + [ + ( + "default".to_owned(), + ConfigStoreBinding { + handle: handle(&[("config", "ready")]), + default_key: "config".to_owned(), + }, + ), + ( + "named".to_owned(), + ConfigStoreBinding { + handle: ConfigStoreHandle::failed_open( + ConfigStoreOpenFailure::InvalidConfiguration, + ), + default_key: "config".to_owned(), + }, + ), + ] + .into(), + "default".to_owned(), + ) + .expect("healthy default registry"); + + assert_eq!(registry.ids().collect::>(), vec!["default", "named"]); + let clock = MonotonicClock::default(); + let error = block_on( + registry + .named("named") + .expect("failed declared id remains present") + .handle + .get_bounded( + "config", + &clock, + Deadline::after(Duration::from_secs(1)), + 1, + 1, + ), + ) + .expect_err("named open failed"); + assert!(matches!(error, ConfigStoreError::Internal { .. })); + } + + #[test] + fn failed_open_bounded_read_preserves_deadline_precedence() { + let handle = ConfigStoreHandle::failed_open(ConfigStoreOpenFailure::InvalidConfiguration); + let clock = MonotonicClock::default(); + let deadline = Deadline::at_instant(clock.now()); + let error = block_on(handle.get_bounded("config", &clock, deadline, 1, 1)) + .expect_err("expired deadline wins"); + assert!(matches!(error, ConfigStoreError::DeadlineExceeded)); + } + + #[test] + fn failed_open_structural_failure_is_typed() { + let handle = ConfigStoreHandle::failed_open(ConfigStoreOpenFailure::InvalidConfiguration); + let error = block_on(handle.get("config")).expect_err("open failure"); + assert!(matches!(error, ConfigStoreError::Internal { .. })); + } + #[test] fn config_store_handle_wraps_and_delegates() { let store_handle = handle(&[("timeout_ms", "1500")]); assert_eq!( block_on(store_handle.get("timeout_ms")).expect("config value"), - Some("1500".to_owned()) + Some(ConfigValue::from("1500")) ); assert_eq!( block_on(store_handle.get("missing")).expect("missing config"), diff --git a/crates/edgezero-core/src/context.rs b/crates/edgezero-core/src/context.rs index 4bde2deaf..f299bc742 100644 --- a/crates/edgezero-core/src/context.rs +++ b/crates/edgezero-core/src/context.rs @@ -1,24 +1,324 @@ +use std::{cell::RefCell, collections::BTreeMap, env, mem}; + use crate::body::Body; -use crate::error::EdgeError; -use crate::http::Request; +use crate::config_store::ConfigExtractionLimits; +use crate::error::{ + BadGatewayReason, BudgetSource, EdgeError, RequestBodyOverflowOrigin, ResponseLimitReason, + StoreExtractionReason, +}; +use crate::error_detail::ProviderFailureDetail; +use crate::http::{Extensions, HeaderMap, Method, Request, RequestParts, Uri, Version}; +use crate::ingress::{AdmittedIngress, IngressGrant}; +use crate::outbound::{ForwardedReadDeadline, HttpClient}; use crate::params::PathParams; -use crate::proxy::ProxyHandle; +use crate::router::RouteMetadata; use crate::store_registry::{ BoundConfigStore, BoundKvStore, BoundSecretStore, ConfigRegistry, ConfigStoreBinding, KvRegistry, SecretRegistry, StoreRegistry, }; +use crate::time::{Deadline, MonotonicClock, MonotonicInstant}; +use futures_util::StreamExt as _; use serde::de::DeserializeOwned; +/// Default maximum body size accepted by JSON extractors (8 MiB). +pub const DEFAULT_INBOUND_JSON_BYTES: usize = 8 * 1024 * 1024; +/// Default maximum body size accepted by form extractors (1 MiB). +pub const DEFAULT_INBOUND_FORM_BYTES: usize = 1024 * 1024; + +/// Non-consuming snapshot of inbound body ownership. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum BodyKind { + Cached { len: usize }, + Draining, + Initial, + Poisoned, + Taken, +} + +/// Request-owned variable values supplied by an adapter that cannot expose process environment. +#[derive(Clone, Debug, Default)] +pub struct RuntimeVariables(BTreeMap); + +impl RuntimeVariables { + #[must_use] + #[inline] + pub fn from_vars(vars: I) -> Self + where + I: IntoIterator, + { + Self(vars.into_iter().collect()) + } + + #[must_use] + #[inline] + pub fn get(&self, name: &str) -> Option<&str> { + self.0.get(name).map(String::as_str) + } +} + +enum BodyState { + Cached(bytes::Bytes), + Draining, + Initial(Body), + Poisoned(StoredError), + Taken, +} + +pub(crate) enum FallbackDrainOutcome { + Complete, + Exceeded, + TimedOut, +} + +enum StoredError { + BadGateway(String, BadGatewayReason, Option), + BadRequest(String, Option), + ConfigOutOfDate(String, String), + GatewayTimeout(String, BudgetSource, Option), + Internal(String, Option), + MethodNotAllowed(Method, Vec), + NotFound(String), + NotImplemented(String), + PayloadTooLarge(RequestBodyOverflowOrigin), + RequestHeaderFieldsTooLarge(String), + RequestTimeout(String), + ResponseTooLarge(String, ResponseLimitReason), + ServiceUnavailable(String), + StoreExtraction(StoreExtractionReason, String, Option), + UriTooLong(String), + Validation(String), +} + +impl StoredError { + fn cancelled() -> Self { + Self::Internal("inbound body drain cancelled".to_owned(), None) + } + + fn capture(error: EdgeError) -> Self { + match error { + EdgeError::BadGateway { + message, + reason, + provider_detail, + } => Self::BadGateway(message, reason, provider_detail), + EdgeError::BadRequest { + message, + provider_detail, + } => Self::BadRequest(message, provider_detail), + EdgeError::ConfigOutOfDate { + message, + field_path, + } => Self::ConfigOutOfDate(message, field_path), + EdgeError::GatewayTimeout { + message, + cause, + provider_detail, + } => Self::GatewayTimeout(message, cause, provider_detail), + EdgeError::Internal { + source, + provider_detail, + } => Self::Internal(source.to_string(), provider_detail), + EdgeError::MethodNotAllowed { method, allowed } => { + Self::MethodNotAllowed(method, allowed) + } + EdgeError::NotFound { path } => Self::NotFound(path), + EdgeError::NotImplemented { message } => Self::NotImplemented(message), + EdgeError::PayloadTooLarge { origin } => Self::PayloadTooLarge(origin), + EdgeError::RequestHeaderFieldsTooLarge { message } => { + Self::RequestHeaderFieldsTooLarge(message) + } + EdgeError::RequestTimeout { message } => Self::RequestTimeout(message), + EdgeError::ResponseTooLarge { message, reason } => { + Self::ResponseTooLarge(message, reason) + } + EdgeError::ServiceUnavailable { message } => Self::ServiceUnavailable(message), + EdgeError::StoreExtraction { + reason, + message, + field_path, + } => Self::StoreExtraction(reason, message, field_path), + EdgeError::UriTooLong { message } => Self::UriTooLong(message), + EdgeError::Validation { message } => Self::Validation(message), + } + } + + fn to_edge_error(&self) -> EdgeError { + match self { + Self::BadGateway(message, reason, detail) => match detail { + Some(evidence) => { + EdgeError::bad_gateway_with_detail(message.clone(), *reason, *evidence) + } + None => EdgeError::bad_gateway_with_reason(message.clone(), *reason), + }, + Self::BadRequest(message, detail) => match detail { + Some(evidence) => EdgeError::bad_request_with_detail(message.clone(), *evidence), + None => EdgeError::bad_request(message.clone()), + }, + Self::ConfigOutOfDate(message, field_path) => { + EdgeError::config_out_of_date(message.clone(), field_path.clone()) + } + Self::GatewayTimeout(message, cause, detail) => match detail { + Some(evidence) => { + EdgeError::gateway_timeout_with_detail(message.clone(), *cause, *evidence) + } + None => EdgeError::gateway_timeout_caused(message.clone(), *cause), + }, + Self::Internal(rendered, detail) => match detail { + Some(evidence) => { + EdgeError::internal_with_detail(anyhow::anyhow!(rendered.clone()), *evidence) + } + None => EdgeError::internal(anyhow::anyhow!(rendered.clone())), + }, + Self::MethodNotAllowed(method, allowed) => EdgeError::MethodNotAllowed { + method: method.clone(), + allowed: allowed.clone(), + }, + Self::NotFound(path) => EdgeError::not_found(path.clone()), + Self::NotImplemented(message) => EdgeError::not_implemented(message.clone()), + Self::PayloadTooLarge(origin) => EdgeError::payload_too_large(*origin), + Self::RequestHeaderFieldsTooLarge(message) => { + EdgeError::request_header_fields_too_large(message.clone()) + } + Self::RequestTimeout(message) => EdgeError::request_timeout(message.clone()), + Self::ResponseTooLarge(message, reason) => { + EdgeError::response_too_large_with_reason(message.clone(), *reason) + } + Self::ServiceUnavailable(message) => EdgeError::service_unavailable(message.clone()), + Self::StoreExtraction(reason, message, field_path) => { + EdgeError::store_extraction(*reason, message.clone(), field_path.clone()) + } + Self::UriTooLong(message) => EdgeError::uri_too_long(message.clone()), + Self::Validation(message) => EdgeError::validation(message.clone()), + } + } +} + /// Request context exposed to handlers and middleware. pub struct RequestContext { + body: RefCell, + config_extraction_limits: ConfigExtractionLimits, + ingress_grant: RefCell>, + monotonic_clock: MonotonicClock, + parts: RequestParts, path_params: PathParams, - request: Request, + read_deadline: Option, + request_start: MonotonicInstant, + route_metadata: Option, +} + +struct DrainGuard<'cell> { + armed: bool, + body: &'cell RefCell, +} + +impl DrainGuard<'_> { + fn disarm(&mut self) { + self.armed = false; + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for DrainGuard<'_> { + fn drop(&mut self) { + if self.armed + && let Ok(mut state) = self.body.try_borrow_mut() + { + *state = BodyState::Poisoned(StoredError::cancelled()); + } + } } impl RequestContext { + /// Drains and caches the inbound body under the caller's byte cap. + /// + /// # Errors + /// Returns 413 on overflow, 408 on an admitted read deadline, and preserves a sticky + /// source error for every later accessor. + #[inline] + pub async fn body_bytes(&self, max: usize) -> Result { + let body = { + let mut state = self.body.borrow_mut(); + if matches!(&*state, BodyState::Initial(_)) { + if let Err(error) = check_read_deadline(self.read_deadline, &self.monotonic_clock) { + let stored = StoredError::capture(error); + let returned = stored.to_edge_error(); + *state = BodyState::Poisoned(stored); + return Err(returned); + } + if declared_body_exceeds(&self.parts.headers, max) { + return Err(EdgeError::payload_too_large( + RequestBodyOverflowOrigin::Inbound, + )); + } + } + match mem::replace(&mut *state, BodyState::Draining) { + BodyState::Cached(bytes) => { + let result = check_cached_body(&bytes, max); + *state = BodyState::Cached(bytes); + return result; + } + BodyState::Draining => { + *state = BodyState::Draining; + return Err(EdgeError::internal(anyhow::anyhow!( + "body read already in progress" + ))); + } + BodyState::Initial(body) => body, + BodyState::Poisoned(error) => { + let returned = error.to_edge_error(); + *state = BodyState::Poisoned(error); + return Err(returned); + } + BodyState::Taken => { + *state = BodyState::Taken; + return Err(EdgeError::internal(anyhow::anyhow!( + "body already consumed via take_body" + ))); + } + } + }; + + let mut guard = DrainGuard { + armed: true, + body: &self.body, + }; + let result = drain_body(body, max, self.read_deadline, &self.monotonic_clock).await; + match result { + Ok(bytes) => { + *self.body.borrow_mut() = BodyState::Cached(bytes.clone()); + guard.disarm(); + Ok(bytes) + } + Err(error) => { + let stored = StoredError::capture(error); + let returned = stored.to_edge_error(); + *self.body.borrow_mut() = BodyState::Poisoned(stored); + guard.disarm(); + Err(returned) + } + } + } + + #[must_use] #[inline] - pub fn body(&self) -> &Body { - self.request.body() + pub fn body_kind(&self) -> BodyKind { + match &*self.body.borrow() { + BodyState::Cached(bytes) => BodyKind::Cached { len: bytes.len() }, + BodyState::Draining => BodyKind::Draining, + BodyState::Initial(_) => BodyKind::Initial, + BodyState::Poisoned(_) => BodyKind::Poisoned, + BodyState::Taken => BodyKind::Taken, + } + } + + #[must_use] + #[inline] + pub fn config_extraction_limits(&self) -> ConfigExtractionLimits { + self.config_extraction_limits } /// Resolve the [`BoundConfigStore`] for `id`. Strict lookup: when a @@ -29,8 +329,8 @@ impl RequestContext { /// bug rather than a hand-wired single-handle adapter (spec hard-cutoff). #[inline] pub fn config_store(&self, id: &str) -> Option { - self.request - .extensions() + self.parts + .extensions .get::() .and_then(|registry| registry.named(id)) .map(|binding| binding.handle) @@ -40,10 +340,8 @@ impl RequestContext { #[must_use] #[inline] pub fn config_store_binding(&self, id: &str) -> Option<&ConfigStoreBinding> { - self.request - .extensions() - .get::() - .and_then(|registry| registry.named_ref(id)) + let registry = self.parts.extensions.get::()?; + registry.named_ref(id) } /// Resolve the default [`BoundConfigStore`] — the wired registry's @@ -51,8 +349,8 @@ impl RequestContext { /// See [`Self::config_store`] for the hard-cutoff rationale. #[inline] pub fn config_store_default(&self) -> Option { - self.request - .extensions() + self.parts + .extensions .get::() .and_then(StoreRegistry::default) .map(|binding| binding.handle) @@ -63,10 +361,8 @@ impl RequestContext { #[must_use] #[inline] pub fn config_store_default_binding(&self) -> Option<&ConfigStoreBinding> { - self.request - .extensions() - .get::() - .and_then(|registry| registry.default_ref()) + let registry = self.parts.extensions.get::()?; + registry.default_ref() } /// Clone a request extension of type `T`, if present. Used by the @@ -78,40 +374,88 @@ impl RequestContext { where T: Clone + Send + Sync + 'static, { - self.request.extensions().get::().cloned() + self.parts.extensions.get::().cloned() + } + + #[must_use] + #[inline] + pub fn extensions(&self) -> &Extensions { + &self.parts.extensions } + #[inline] + pub fn extensions_mut(&mut self) -> &mut Extensions { + &mut self.parts.extensions + } + + /// Buffers at most `max` bytes and deserializes form-urlencoded data. + /// /// # Errors - /// Returns [`EdgeError::bad_request`] if the body cannot be deserialized as form-urlencoded data into `T`, or the body is streaming. + /// Returns 413 on overflow, 400 on malformed input, and preserves body drain errors. #[inline] - pub fn form(&self) -> Result + pub async fn form_within(&self, max: usize) -> Result where T: DeserializeOwned, { - match self.request.body() { - Body::Once(bytes) => serde_urlencoded::from_bytes(bytes.as_ref()) - .map_err(|err| EdgeError::bad_request(format!("invalid form payload: {err}"))), - Body::Stream(_) => Err(EdgeError::bad_request( - "streaming bodies are not supported for form extraction", - )), - } + let bytes = self.body_bytes(max).await?; + serde_urlencoded::from_bytes(bytes.as_ref()) + .map_err(|err| EdgeError::bad_request(format!("invalid form payload: {err}"))) } + #[must_use] #[inline] - pub fn into_request(self) -> Request { - self.request + pub fn headers(&self) -> &HeaderMap { + &self.parts.headers } + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.parts.headers + } + + #[must_use] + #[inline] + pub fn http_client(&self) -> Option { + self.parts.extensions.get::().cloned() + } + + /// Reassembles the request while preserving an initial or cached body. + /// + /// # Errors + /// Returns the sticky body error or an internal error if a drain is in progress. + #[inline] + pub fn into_request(self) -> Result { + let body = match self.body.into_inner() { + BodyState::Cached(bytes) => Body::from(bytes), + BodyState::Draining => { + return Err(EdgeError::internal(anyhow::anyhow!( + "body read in progress" + ))); + } + BodyState::Initial(body) => body, + BodyState::Poisoned(error) => return Err(error.to_edge_error()), + BodyState::Taken => Body::empty(), + }; + let mut parts = self.parts; + if let Some(deadline) = self.read_deadline { + parts + .extensions + .insert(ForwardedReadDeadline::new(deadline)); + } + Ok(Request::from_parts(parts, body)) + } + + /// Buffers at most `max` bytes and deserializes JSON. + /// /// # Errors - /// Returns [`EdgeError::bad_request`] if the body is not valid JSON for `T`. + /// Returns 413 on overflow, 400 on malformed input, and preserves body drain errors. #[inline] - pub fn json(&self) -> Result + pub async fn json_within(&self, max: usize) -> Result where T: DeserializeOwned, { - self.request - .body() - .to_json() + let bytes = self.body_bytes(max).await?; + serde_json::from_slice(bytes.as_ref()) .map_err(|err| EdgeError::bad_request(format!("invalid JSON payload: {err}"))) } @@ -122,10 +466,8 @@ impl RequestContext { /// the conventional `"default"` id (spec hard-cutoff). #[inline] pub fn kv_store(&self, id: &str) -> Option { - self.request - .extensions() - .get::() - .and_then(|registry| registry.named(id)) + let registry = self.parts.extensions.get::()?; + registry.named(id) } /// Resolve the default [`BoundKvStore`] — the wired registry's @@ -133,20 +475,72 @@ impl RequestContext { /// See [`Self::kv_store`] for the hard-cutoff rationale. #[inline] pub fn kv_store_default(&self) -> Option { - self.request - .extensions() - .get::() - .and_then(StoreRegistry::default) + let registry = self.parts.extensions.get::()?; + registry.default() + } + + #[must_use] + #[inline] + pub fn method(&self) -> &Method { + &self.parts.method + } + + /// Returns the clock paired with this request's admitted ingress lifetime. + #[must_use] + #[inline] + pub fn monotonic_clock(&self) -> MonotonicClock { + self.monotonic_clock.clone() } #[inline] pub fn new(request: Request, params: PathParams) -> Self { + let (parts, body) = request.into_parts(); Self { + body: RefCell::new(BodyState::Initial(body)), + config_extraction_limits: ConfigExtractionLimits::default(), + ingress_grant: RefCell::new(None), + monotonic_clock: MonotonicClock::default(), + parts, path_params: params, - request, + read_deadline: None, + request_start: MonotonicInstant::now(), + route_metadata: None, } } + pub(crate) fn new_routed( + request: Request, + params: PathParams, + route_metadata: RouteMetadata, + ingress: AdmittedIngress, + ) -> Self { + let (request_start, read_deadline, grant, config_extraction_limits, monotonic_clock) = + ingress.into_parts(); + let (parts, body) = request.into_parts(); + Self { + body: RefCell::new(BodyState::Initial(body)), + config_extraction_limits, + ingress_grant: RefCell::new(Some(grant)), + monotonic_clock, + parts, + path_params: params, + read_deadline: Some(read_deadline), + request_start, + route_metadata: Some(route_metadata), + } + } + + #[must_use] + #[inline] + pub fn parts(&self) -> &RequestParts { + &self.parts + } + + #[inline] + pub fn parts_mut(&mut self) -> &mut RequestParts { + &mut self.parts + } + /// # Errors /// Returns [`EdgeError::bad_request`] if the path parameters cannot be deserialized into `T`. #[inline] @@ -164,11 +558,6 @@ impl RequestContext { &self.path_params } - #[inline] - pub fn proxy_handle(&self) -> Option { - self.request.extensions().get::().cloned() - } - /// # Errors /// Returns [`EdgeError::bad_request`] if the query string cannot be deserialized into `T`. #[inline] @@ -176,19 +565,31 @@ impl RequestContext { where T: DeserializeOwned, { - let query = self.request.uri().query().unwrap_or(""); + let query = self.parts.uri.query().unwrap_or(""); serde_urlencoded::from_str(query) .map_err(|err| EdgeError::bad_request(format!("invalid query string: {err}"))) } + /// Absolute deadline governing the admitted inbound body, if this context entered + /// through the adapter admission seam. + #[must_use] #[inline] - pub fn request(&self) -> &Request { - &self.request + pub fn read_deadline(&self) -> Option { + self.read_deadline } + /// Monotonic instant captured at ingress entry or low-level context construction. + #[must_use] #[inline] - pub fn request_mut(&mut self) -> &mut Request { - &mut self.request + pub fn request_start(&self) -> MonotonicInstant { + self.request_start + } + + /// Canonical matched route metadata, absent for low-level contexts. + #[must_use] + #[inline] + pub fn route_metadata(&self) -> Option<&RouteMetadata> { + self.route_metadata.as_ref() } /// Resolve the [`BoundSecretStore`] for `id`. Strict lookup: when a @@ -198,10 +599,8 @@ impl RequestContext { /// registry under the conventional `"default"` id (spec hard-cutoff). #[inline] pub fn secret_store(&self, id: &str) -> Option { - self.request - .extensions() - .get::() - .and_then(|registry| registry.named(id)) + let registry = self.parts.extensions.get::()?; + registry.named(id) } /// Resolve the default [`BoundSecretStore`] — the wired registry's @@ -209,37 +608,310 @@ impl RequestContext { /// See [`Self::secret_store`] for the hard-cutoff rationale. #[inline] pub fn secret_store_default(&self) -> Option { - self.request - .extensions() - .get::() - .and_then(StoreRegistry::default) + let registry = self.parts.extensions.get::()?; + registry.default() + } + + /// Consumes body ownership without buffering it. + /// + /// # Errors + /// Returns the sticky body error or an internal error if a drain is in progress. + #[inline] + pub fn take_body(&self) -> Result { + let mut state = self.body.borrow_mut(); + match mem::replace(&mut *state, BodyState::Taken) { + BodyState::Cached(bytes) => Ok(Body::from(bytes)), + BodyState::Draining => { + *state = BodyState::Draining; + Err(EdgeError::internal(anyhow::anyhow!( + "body read in progress" + ))) + } + BodyState::Initial(body) => Ok(body), + BodyState::Poisoned(error) => { + let returned = error.to_edge_error(); + *state = BodyState::Poisoned(error); + Err(returned) + } + BodyState::Taken => { + *state = BodyState::Taken; + Ok(Body::empty()) + } + } + } + + /// Takes the application-owned ingress grant at most once. + #[must_use] + #[inline] + pub fn take_ingress_grant(&self) -> Option { + self.ingress_grant.borrow_mut().take() + } + + #[must_use] + #[inline] + pub fn uri(&self) -> &Uri { + &self.parts.uri + } + + /// Reads a request-scoped variable, or the process environment when the adapter provides none. + #[must_use] + #[inline] + pub fn variable(&self, name: &str) -> Option { + if let Some(variables) = self.parts.extensions.get::() { + return variables.get(name).map(str::to_owned); + } + env::var(name).ok() + } + + #[must_use] + #[inline] + pub fn version(&self) -> Version { + self.parts.version + } +} + +fn check_cached_body(bytes: &bytes::Bytes, max: usize) -> Result { + if bytes.len() > max { + return Err(EdgeError::payload_too_large( + RequestBodyOverflowOrigin::Inbound, + )); + } + Ok(bytes.clone()) +} + +fn declared_body_exceeds(headers: &HeaderMap, max: usize) -> bool { + let mut values = headers.get_all("content-length").iter(); + let Some(value) = values.next() else { + return false; + }; + if values.next().is_some() { + return false; + } + let Ok(raw) = value.to_str() else { + return false; + }; + let text = raw.trim(); + if text.is_empty() || !text.bytes().all(|byte| byte.is_ascii_digit()) { + return false; + } + text.parse::().is_ok_and(|length| { + usize::try_from(length).map_or(true, |native_length| native_length > max) + }) +} + +fn check_read_deadline( + deadline: Option, + monotonic_clock: &MonotonicClock, +) -> Result<(), EdgeError> { + if deadline.is_some_and(|candidate| candidate.is_expired_at(monotonic_clock.now())) { + return Err(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )); + } + Ok(()) +} + +pub(crate) async fn drain_body( + body: Body, + max: usize, + deadline: Option, + monotonic_clock: &MonotonicClock, +) -> Result { + check_read_deadline(deadline, monotonic_clock)?; + match body.into_stream() { + Err(buffered_body) => { + let bytes = buffered_body.into_bytes().ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("body conversion lost buffered payload")) + })?; + check_read_deadline(deadline, monotonic_clock)?; + check_cached_body(&bytes, max) + } + Ok(mut stream) => { + let mut buffered = Vec::new(); + loop { + check_read_deadline(deadline, monotonic_clock)?; + let next = stream.next().await; + // Deadline wins a simultaneous body/error/EOF observation. + check_read_deadline(deadline, monotonic_clock)?; + let Some(result) = next else { + return Ok(bytes::Bytes::from(buffered)); + }; + let chunk = result?; + let next_len = buffered.len().checked_add(chunk.len()).ok_or_else(|| { + EdgeError::payload_too_large(RequestBodyOverflowOrigin::Inbound) + })?; + if next_len > max { + return Err(EdgeError::payload_too_large( + RequestBodyOverflowOrigin::Inbound, + )); + } + buffered.extend_from_slice(&chunk); + } + } + } +} + +fn fallback_deadline_outcome( + deadline: Deadline, + monotonic_clock: &MonotonicClock, +) -> Option { + deadline + .is_expired_at(monotonic_clock.now()) + .then_some(FallbackDrainOutcome::TimedOut) +} + +pub(crate) async fn drain_body_discard( + body: Body, + max: usize, + deadline: Deadline, + monotonic_clock: &MonotonicClock, +) -> Result { + if let Some(outcome) = fallback_deadline_outcome(deadline, monotonic_clock) { + return Ok(outcome); + } + match body.into_stream() { + Err(buffered_body) => { + let bytes = buffered_body.into_bytes().ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("body conversion lost buffered payload")) + })?; + if let Some(outcome) = fallback_deadline_outcome(deadline, monotonic_clock) { + return Ok(outcome); + } + if bytes.len() > max { + return Ok(FallbackDrainOutcome::Exceeded); + } + Ok(FallbackDrainOutcome::Complete) + } + Ok(mut stream) => { + let mut consumed = 0_usize; + loop { + if let Some(outcome) = fallback_deadline_outcome(deadline, monotonic_clock) { + return Ok(outcome); + } + let next = stream.next().await; + // Deadline wins a simultaneous body/error/EOF observation. + if let Some(outcome) = fallback_deadline_outcome(deadline, monotonic_clock) { + return Ok(outcome); + } + let Some(result) = next else { + return Ok(FallbackDrainOutcome::Complete); + }; + let chunk = match result { + Ok(chunk) => chunk, + Err(EdgeError::RequestTimeout { .. }) => { + return Ok(FallbackDrainOutcome::TimedOut); + } + Err(error) => return Err(error), + }; + let Some(next_consumed) = consumed.checked_add(chunk.len()) else { + return Ok(FallbackDrainOutcome::Exceeded); + }; + consumed = next_consumed; + if consumed > max { + return Ok(FallbackDrainOutcome::Exceeded); + } + } + } } } #[cfg(test)] mod tests { use super::*; - use crate::http::{HeaderValue, Method, StatusCode, Uri, request_builder}; + use crate::config_store::ready_config_store_bounded_read; + use crate::http::{HeaderMap, HeaderValue, Method, StatusCode, request_builder}; + use crate::outbound::{ + HttpClient, IndependentOutboundProgress, OutboundBatch, OutboundBatchDriverEvent, + OutboundDispatchRequest, OutboundExchangeOwner, OutboundFuture, OutboundHead, + OutboundHeaderFidelity, OutboundHeaderInput, OutboundHttpClient, OutboundPending, + OutboundPreparedBatch, OutboundProgressRuntime, OutboundRequest, OutboundSendMode, + OutboundTime, + }; use crate::params::PathParams; - use crate::proxy::{ProxyClient, ProxyHandle, ProxyRequest, ProxyResponse}; use async_trait::async_trait; use bytes::Bytes; use futures::executor::block_on; + use futures::future::pending; use futures::stream; + use futures::task::noop_waker_ref; use serde::{Deserialize, Serialize}; + use std::cell::Cell; use std::collections::HashMap; + use std::future::Future as _; + use std::rc::Rc; + use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll}; + use std::time::Duration; - struct DummyClient; + struct DummyOutboundClient; #[derive(Debug, PartialEq, Deserialize, Serialize)] struct PathData { id: String, } - #[async_trait(?Send)] - impl ProxyClient for DummyClient { - async fn send(&self, _request: ProxyRequest) -> Result { - Ok(ProxyResponse::new(StatusCode::OK, Body::empty())) + impl OutboundHttpClient for DummyOutboundClient { + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let head = block_on(self.dispatch_head(request))?; + let outcome = block_on(head.finish()); + Ok(OutboundPending::new( + Box::pin(async move { outcome }), + OutboundExchangeOwner::new(|| {}), + )) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + let (_, context) = request.into_parts(); + Box::pin(async move { + OutboundHead::new( + StatusCode::OK, + OutboundHeaderInput::new( + "dummy", + HeaderMap::new(), + OutboundHeaderFidelity::AdapterVisibleFields, + ), + Box::pin(stream::empty()), + context, + OutboundExchangeOwner::new(|| {}), + ) + }) + } + + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, slots) = batch.into_slots(); + let completed = slots + .into_iter() + .map(|(index, request)| { + let outcome = block_on(self.dispatch_head(request)) + .and_then(|head| block_on(head.finish())); + OutboundBatchDriverEvent::Item { + index, + completed_at: context.clock().now(), + outcome, + } + }) + .collect::>(); + OutboundBatch::from_driver( + context, + stream::iter(completed), + OutboundExchangeOwner::new(|| {}), + ) + } + + fn time(&self) -> OutboundTime { + OutboundTime::new(MonotonicClock::default(), |_, _| Box::pin(pending())) + } + + fn validate_supported_options( + &self, + _: &OutboundRequest, + _: OutboundSendMode, + ) -> Result<(), EdgeError> { + Ok(()) } } @@ -260,6 +932,28 @@ mod tests { PathParams::new(inner) } + #[test] + fn variable_reads_request_scoped_values() { + let mut request = request_builder() + .method(Method::GET) + .uri("/variables") + .body(Body::empty()) + .expect("request"); + request + .extensions_mut() + .insert(RuntimeVariables::from_vars([( + "API_BASE_URL".to_owned(), + "https://api.example.com".to_owned(), + )])); + let ctx = RequestContext::new(request, PathParams::default()); + + assert_eq!( + ctx.variable("API_BASE_URL").as_deref(), + Some("https://api.example.com") + ); + assert_eq!(ctx.variable("UNKNOWN_VARIABLE"), None); + } + // `RequestContext::config_handle()` was removed. The // present/absent behaviour is now covered by // `config_store_*` tests against a wired `ConfigRegistry`. @@ -272,7 +966,8 @@ mod tests { } let body = Body::from("name=demo"); let ctx = ctx("/submit", body, PathParams::default()); - let parsed: FormData = ctx.form().expect("form data"); + let parsed: FormData = + block_on(ctx.form_within(DEFAULT_INBOUND_FORM_BYTES)).expect("form data"); assert_eq!( parsed, FormData { @@ -284,15 +979,381 @@ mod tests { } #[test] - fn form_streaming_body_not_supported() { + fn declared_overflow_does_not_read_or_poison_the_body() { + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let body = Body::stream( + stream::iter([Bytes::from_static(b"0123456789")]) + .inspect(move |_| observed.set(observed.get().saturating_add(1))), + ); + let mut ctx = ctx("/body", body, PathParams::default()); + ctx.headers_mut() + .insert("content-length", HeaderValue::from_static("10")); + + let error = block_on(ctx.body_bytes(5)).expect_err("declared overflow"); + assert_eq!(error.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!(polls.get(), 0); + assert_eq!(ctx.body_kind(), BodyKind::Initial); + assert_eq!(block_on(ctx.body_bytes(10)).expect("larger cap").len(), 10); + assert_eq!(polls.get(), 1); + } + + #[test] + fn padded_declared_length_rejects_without_consuming_the_body() { + let mut ctx = ctx("/body", Body::from("0123456789"), PathParams::default()); + ctx.headers_mut() + .insert("content-length", HeaderValue::from_static(" 10 \t")); + assert_eq!( + block_on(ctx.body_bytes(5)) + .expect_err("declared overflow") + .status(), + StatusCode::PAYLOAD_TOO_LARGE + ); + assert_eq!(ctx.body_kind(), BodyKind::Initial); + assert_eq!(block_on(ctx.body_bytes(10)).expect("larger cap").len(), 10); + } + + #[test] + fn expired_deadline_wins_over_declared_overflow_without_polling() { + let body = Body::from_stream(stream::poll_fn( + |_| -> Poll>> { + panic!("an expired reader must not poll its source") + }, + )); + let mut ctx = ctx("/body", body, PathParams::default()); + ctx.headers_mut() + .insert("content-length", HeaderValue::from_static("10")); + ctx.read_deadline = Some(Deadline::after(Duration::ZERO)); + + for cap in [5, 10] { + assert_eq!( + block_on(ctx.body_bytes(cap)).expect_err("expired").status(), + StatusCode::REQUEST_TIMEOUT + ); + } + assert_eq!(ctx.body_kind(), BodyKind::Poisoned); + } + + #[test] + fn readers_classify_consumed_overflow_before_payload_parsing() { + let json = serde_json::to_vec(&serde_json::json!("123456")).expect("JSON"); + for (reader, bytes) in [ + (0_u8, b"12345678".to_vec()), + (1, json), + (2, b"key=1234".to_vec()), + ] { + for cap in [7_usize, 8, 9] { + let ctx = ctx( + "/body", + Body::stream(stream::iter([Bytes::from(bytes.clone())])), + PathParams::default(), + ); + let result = match reader { + 0 => block_on(ctx.body_bytes(cap)).map(|_| ()), + 1 => block_on(ctx.json_within::(cap)).map(|_| ()), + _ => block_on(ctx.form_within::>(cap)).map(|_| ()), + }; + if cap == 7 { + let first = result.expect_err("overflow"); + let second = block_on(ctx.body_bytes(usize::MAX)).expect_err("sticky"); + assert_eq!(first.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!(second.kind(), "payload_too_large"); + assert_eq!(ctx.body_kind(), BodyKind::Poisoned); + } else { + result.expect("within cap"); + assert_eq!( + block_on(ctx.body_bytes(7)) + .expect_err("cached cap") + .status(), + StatusCode::PAYLOAD_TOO_LARGE + ); + assert_eq!( + block_on(ctx.body_bytes(8)).expect("reusable cache").len(), + 8 + ); + } + } + } + } + + #[test] + fn body_bytes_drains_once_and_rechecks_each_callers_cap() { + let polls = Rc::new(Cell::new(0_u8)); + let observed_polls = Rc::clone(&polls); + let body = Body::from_stream(stream::poll_fn(move |_cx| { + let current = observed_polls.get(); + observed_polls.set(current.saturating_add(1)); + match current { + 0 => Poll::Ready(Some(Ok(Bytes::from_static(b"ok")))), + _ => Poll::Ready(None), + } + })); + let ctx = ctx("/body", body, PathParams::default()); + + assert_eq!( + block_on(ctx.body_bytes(2)).expect("exact cap"), + Bytes::from_static(b"ok") + ); + let stricter = block_on(ctx.body_bytes(1)).expect_err("stricter cached cap"); + assert_eq!(stricter.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!( + block_on(ctx.body_bytes(2)).expect("cached body remains usable"), + Bytes::from_static(b"ok") + ); + assert_eq!(polls.get(), 2, "the source is drained exactly once"); + } + + #[test] + fn initial_drain_overflow_is_sticky_and_never_retries_source() { + let polls = Rc::new(Cell::new(0_u8)); + let observed_polls = Rc::clone(&polls); + let body = Body::from_stream(stream::poll_fn(move |_cx| { + observed_polls.set(observed_polls.get().saturating_add(1)); + Poll::Ready(Some(Ok(Bytes::from_static(b"too large")))) + })); + let ctx = ctx("/body", body, PathParams::default()); + + let first = block_on(ctx.body_bytes(3)).expect_err("overflow"); + let second = block_on(ctx.body_bytes(usize::MAX)).expect_err("sticky overflow"); + assert_eq!(first.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!(second.status(), first.status()); + assert_eq!(second.message(), first.message()); + assert_eq!(ctx.body_kind(), BodyKind::Poisoned); + assert_eq!(polls.get(), 1, "a poisoned source must not be polled again"); + assert_eq!( + ctx.take_body().expect_err("poison survives take").status(), + StatusCode::PAYLOAD_TOO_LARGE + ); + } + + #[test] + fn source_error_replays_typed_fields_without_repolling() { + let body = Body::from_stream(stream::iter([Err(EdgeError::bad_gateway_with_reason( + "upstream body failed", + BadGatewayReason::Transport, + ))])); + let ctx = ctx("/body", body, PathParams::default()); + + for _ in 0_u8..2_u8 { + let error = block_on(ctx.body_bytes(32)).expect_err("sticky source error"); + assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Transport, + .. + } + )); + assert_eq!(error.message(), "upstream body failed"); + } + } + + #[test] + fn provider_diagnostics_survive_sticky_body_replay() { + use crate::error_detail::{ + FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase, + }; + + let detail = ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: FastlyFailureCause::ConnectionRefused, + }; + for source in [ + EdgeError::bad_request_with_detail("fixed request rejection", detail), + EdgeError::bad_gateway_with_detail( + "fixed transport category", + BadGatewayReason::Transport, + detail, + ), + EdgeError::gateway_timeout_with_detail( + "fixed timeout category", + BudgetSource::PerCallTimeout, + detail, + ), + EdgeError::internal_with_detail(anyhow::anyhow!("fixed internal category"), detail), + ] { + let expected_display = source.to_string(); + let expected_message = source.message(); + let expected_kind = source.kind(); + let expected_status = source.status(); + let polls = Rc::new(Cell::new(0_u8)); + let observed = Rc::clone(&polls); + let mut pending_error = Some(source); + let body = Body::from_stream(stream::poll_fn(move |_task| { + observed.set(observed.get().saturating_add(1)); + Poll::Ready(pending_error.take().map(Err)) + })); + let context = ctx("/body", body, PathParams::default()); + for _ in 0_u8..2 { + let replayed = + block_on(context.body_bytes(32)).expect_err("sticky provider failure"); + assert_eq!(replayed.provider_failure_detail(), Some(&detail)); + assert_eq!(replayed.kind(), expected_kind); + assert_eq!(replayed.status(), expected_status); + if let EdgeError::BadGateway { reason, .. } = &replayed { + assert_eq!(*reason, BadGatewayReason::Transport); + } + if let EdgeError::GatewayTimeout { cause, .. } = &replayed { + assert_eq!(*cause, BudgetSource::PerCallTimeout); + } + assert_eq!(replayed.to_string(), expected_display); + assert_eq!(replayed.message(), expected_message); + } + assert_eq!( + context + .take_body() + .expect_err("poisoned body") + .provider_failure_detail(), + Some(&detail) + ); + assert_eq!( + context + .into_request() + .expect_err("poisoned request body") + .provider_failure_detail(), + Some(&detail) + ); + assert_eq!(polls.get(), 1); + } + } + + #[test] + fn cancelled_drain_poison_is_sticky() { + let body = Body::from_stream(stream::poll_fn(|_cx| Poll::Pending)); + let ctx = ctx("/body", body, PathParams::default()); + let mut first = Box::pin(ctx.body_bytes(32)); + let mut task = Context::from_waker(noop_waker_ref()); + assert!(matches!(first.as_mut().poll(&mut task), Poll::Pending)); + assert_eq!(ctx.body_kind(), BodyKind::Draining); + + drop(first); + assert_eq!(ctx.body_kind(), BodyKind::Poisoned); + let error = block_on(ctx.body_bytes(32)).expect_err("cancel poison"); + assert_eq!( + error.message(), + "internal error: inbound body drain cancelled" + ); + } + + #[test] + fn reentrant_read_errors_without_disrupting_first_drain() { + let step = Rc::new(Cell::new(0_u8)); + let observed_step = Rc::clone(&step); + let body = Body::from_stream(stream::poll_fn(move |_cx| match observed_step.get() { + 0 => { + observed_step.set(1); + Poll::Pending + } + 1 => { + observed_step.set(2); + Poll::Ready(Some(Ok(Bytes::from_static(b"ok")))) + } + _ => Poll::Ready(None), + })); + let ctx = ctx("/body", body, PathParams::default()); + let mut first = Box::pin(ctx.body_bytes(2)); + let mut task = Context::from_waker(noop_waker_ref()); + assert!(matches!(first.as_mut().poll(&mut task), Poll::Pending)); + + let reentrant = block_on(ctx.body_bytes(2)).expect_err("reentrant read"); + assert_eq!( + reentrant.message(), + "internal error: body read already in progress" + ); + let Poll::Ready(first_result) = first.as_mut().poll(&mut task) else { + panic!("scripted drain should complete"); + }; + assert_eq!(first_result.expect("first read"), Bytes::from_static(b"ok")); + drop(first); + assert_eq!(ctx.body_kind(), BodyKind::Cached { len: 2 }); + } + + #[test] + fn expired_read_deadline_wins_and_poison_is_request_timeout() { + let mut ctx = ctx("/body", Body::from("available"), PathParams::default()); + ctx.read_deadline = Some(Deadline::after(Duration::ZERO)); + + let first = block_on(ctx.body_bytes(32)).expect_err("expired"); + let second = block_on(ctx.body_bytes(32)).expect_err("sticky expired"); + assert_eq!(first.status(), StatusCode::REQUEST_TIMEOUT); + assert_eq!(second.status(), StatusCode::REQUEST_TIMEOUT); + assert!(matches!(second, EdgeError::RequestTimeout { .. })); + } + + #[test] + fn injected_clock_controls_read_deadline_and_sticky_poison() { + let start = MonotonicInstant::now(); + let now = Arc::new(Mutex::new(start)); + let observed_now = Arc::clone(&now); + let mut ctx = ctx("/body", Body::from("available"), PathParams::default()); + ctx.monotonic_clock = + MonotonicClock::new(move || *observed_now.lock().expect("clock lock")); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + ctx.read_deadline = Some(Deadline::at_instant(deadline)); + *now.lock().expect("clock lock") = deadline; + + let first = block_on(ctx.body_bytes(32)).expect_err("expired"); + let second = block_on(ctx.body_bytes(32)).expect_err("sticky expired"); + assert_eq!(first.status(), StatusCode::REQUEST_TIMEOUT); + assert_eq!(second.status(), StatusCode::REQUEST_TIMEOUT); + assert_eq!(ctx.body_kind(), BodyKind::Poisoned); + } + + #[test] + fn take_body_and_into_request_cover_initial_cached_and_taken() { + let initial_stream = Body::from_stream(stream::iter([Ok(Bytes::from_static(b"stream"))])); + let initial = ctx("/initial", initial_stream, PathParams::default()); + assert!(initial.take_body().expect("initial body").is_stream()); + assert!( + initial + .take_body() + .expect("taken becomes empty") + .into_bytes() + .is_some_and(|body_bytes| body_bytes.is_empty()) + ); + assert_eq!(initial.body_kind(), BodyKind::Taken); + + let cached = ctx("/cached", Body::from("cached"), PathParams::default()); + assert_eq!( + block_on(cached.body_bytes(6)).expect("cache"), + Bytes::from_static(b"cached") + ); + assert_eq!( + cached.take_body().expect("cached body").into_bytes(), + Some(Bytes::from_static(b"cached")) + ); + + let reassembled = ctx("/request", Body::from("body"), PathParams::default()); + assert_eq!( + block_on(reassembled.body_bytes(4)).expect("cache"), + Bytes::from_static(b"body") + ); + let request = reassembled.into_request().expect("reassembled request"); + assert_eq!(request.uri().path(), "/request"); + assert_eq!( + request.into_body().into_bytes(), + Some(Bytes::from_static(b"body")) + ); + } + + #[test] + fn poisoned_into_request_returns_the_stored_error() { + let ctx = ctx("/request", Body::from("oversized"), PathParams::default()); + let first = block_on(ctx.body_bytes(1)).expect_err("overflow"); + let later = ctx.into_request().expect_err("poisoned request"); + assert_eq!(later.status(), first.status()); + assert_eq!(later.message(), first.message()); + } + + #[test] + fn form_streaming_body_is_bounded_and_supported() { let stream = stream::iter(vec![Ok::(Bytes::from("name=demo"))]); - let body = Body::from_stream(stream); + let body = Body::from_external_stream(stream); let ctx = ctx("/submit", body, PathParams::default()); - let err = ctx.form::().expect_err("expected error"); - assert_eq!(err.status(), StatusCode::BAD_REQUEST); - assert!( - err.message() - .contains("streaming bodies are not supported for form extraction") + let parsed: serde_json::Value = + block_on(ctx.form_within(DEFAULT_INBOUND_FORM_BYTES)).expect("form data"); + assert_eq!( + parsed.get("name").and_then(|value| value.as_str()), + Some("demo") ); } @@ -300,7 +1361,8 @@ mod tests { fn form_value_deserialises_successfully() { let body = Body::from("name=demo"); let ctx = ctx("/submit", body, PathParams::default()); - let parsed: serde_json::Value = ctx.form().expect("form data"); + let parsed: serde_json::Value = + block_on(ctx.form_within(DEFAULT_INBOUND_FORM_BYTES)).expect("form data"); assert_eq!( parsed.get("name").and_then(|value| value.as_str()), Some("demo") @@ -310,13 +1372,14 @@ mod tests { #[test] fn invalid_form_returns_bad_request() { #[expect(dead_code, reason = "field exercised only via Deserialize")] - #[derive(Deserialize)] + #[derive(Debug, Deserialize)] struct FormData { age: u8, } let body = Body::from("age=not-a-number"); let ctx = ctx("/submit", body, PathParams::default()); - let err = ctx.form::().err().expect("expected error"); + let err = block_on(ctx.form_within::(DEFAULT_INBOUND_FORM_BYTES)) + .expect_err("expected error"); assert_eq!(err.status(), StatusCode::BAD_REQUEST); assert!(err.message().contains("invalid form payload")); } @@ -325,7 +1388,8 @@ mod tests { fn invalid_json_returns_bad_request() { let body = Body::from(&b"not json"[..]); let ctx = ctx("/echo", body, PathParams::default()); - let err = ctx.json::().expect_err("expected error"); + let err = block_on(ctx.json_within::(DEFAULT_INBOUND_JSON_BYTES)) + .expect_err("expected error"); assert_eq!(err.status(), StatusCode::BAD_REQUEST); assert!(err.message().contains("invalid JSON payload")); } @@ -371,7 +1435,8 @@ mod tests { }) .expect("json body"); let ctx = ctx("/echo", body, PathParams::default()); - let parsed: Payload = ctx.json().expect("json payload"); + let parsed: Payload = + block_on(ctx.json_within(DEFAULT_INBOUND_JSON_BYTES)).expect("json payload"); assert_eq!( parsed, Payload { @@ -380,6 +1445,21 @@ mod tests { ); } + #[test] + fn http_client_is_retrieved_when_present() { + let mut request = request_builder() + .method(Method::GET) + .uri("/outbound") + .body(Body::empty()) + .expect("request"); + request + .extensions_mut() + .insert(HttpClient::with_client(DummyOutboundClient)); + + let ctx = RequestContext::new(request, PathParams::default()); + assert!(ctx.http_client().is_some()); + } + // `RequestContext::kv_handle()` was removed. The // present/absent behaviour is now covered by `kv_store_*` // tests against a wired `KvRegistry`. @@ -393,29 +1473,6 @@ mod tests { assert!(serialized.contains("42")); } - #[test] - fn proxy_handle_forwards_with_dummy_client() { - let handle = ProxyHandle::with_client(DummyClient); - let request = ProxyRequest::new(Method::GET, Uri::from_static("https://example.com")); - let response = block_on(handle.forward(request)).expect("response"); - assert_eq!(response.status(), StatusCode::OK); - } - - #[test] - fn proxy_handle_is_retrieved_when_present() { - let mut request = request_builder() - .method(Method::GET) - .uri("/proxy") - .body(Body::empty()) - .expect("request"); - request - .extensions_mut() - .insert(ProxyHandle::with_client(DummyClient)); - - let ctx = RequestContext::new(request, PathParams::default()); - assert!(ctx.proxy_handle().is_some()); - } - #[test] fn query_defaults_to_empty_when_missing() { #[derive(Debug, Deserialize, PartialEq)] @@ -445,21 +1502,25 @@ mod tests { Body::from("payload"), params(&[("id", "123")]), ); - assert_eq!(ctx.request().uri().path(), "/items/123"); - ctx.request_mut() - .headers_mut() + assert_eq!(ctx.uri().path(), "/items/123"); + ctx.headers_mut() .insert("x-test", HeaderValue::from_static("value")); assert_eq!( - ctx.request() - .headers() + ctx.headers() .get("x-test") .and_then(|value| value.to_str().ok()), Some("value") ); assert_eq!(ctx.path_params().get("id"), Some("123")); - assert_eq!(ctx.body().as_bytes().expect("buffered"), b"payload"); + assert_eq!( + block_on(ctx.body_bytes(7)).expect("buffered"), + Bytes::from_static(b"payload") + ); + assert!(ctx.route_metadata().is_none()); + assert!(ctx.read_deadline().is_none()); + assert!(ctx.take_ingress_grant().is_none()); - let request = ctx.into_request(); + let request = ctx.into_request().expect("request"); assert_eq!(request.uri().path(), "/items/123"); } @@ -545,9 +1606,14 @@ mod tests { struct FixedStore(&'static str); #[async_trait(?Send)] impl ConfigStore for FixedStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.to_owned())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.into())) } + + ready_config_store_bounded_read!(); } let primary_handle = ConfigStoreHandle::new(Arc::new(FixedStore("primary"))); @@ -583,13 +1649,13 @@ mod tests { let resolved = ctx.config_store("analytics").expect("analytics handle"); assert_eq!( block_on(resolved.get("key")).expect("config value"), - Some("analytics".to_owned()) + Some(crate::ConfigValue::from("analytics")) ); assert!(ctx.config_store("unknown").is_none()); let default = ctx.config_store_default().expect("default handle"); assert_eq!( block_on(default.get("key")).expect("default config value"), - Some("primary".to_owned()) + Some(crate::ConfigValue::from("primary")) ); } @@ -663,9 +1729,14 @@ mod tests { struct AnyStore; #[async_trait(?Send)] impl ConfigStore for AnyStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Ok(None) } + + ready_config_store_bounded_read!(); } let binding = ConfigStoreBinding { @@ -710,9 +1781,14 @@ mod tests { struct AnyStore; #[async_trait(?Send)] impl ConfigStore for AnyStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Ok(None) } + + ready_config_store_bounded_read!(); } let registry: ConfigRegistry = StoreRegistry::new( diff --git a/crates/edgezero-core/src/env_config.rs b/crates/edgezero-core/src/env_config.rs index c3f2673c2..3e4f2df03 100644 --- a/crates/edgezero-core/src/env_config.rs +++ b/crates/edgezero-core/src/env_config.rs @@ -9,6 +9,8 @@ //! Every segment is lower-cased on parse, and lookup arguments are lower-cased //! before matching — callers pass lower-case logical ids and get a //! case-insensitive match against the upper-case env-var convention. +//! On Windows the prefix also follows the process environment's case-insensitive +//! key identity; Unix prefixes remain case-sensitive. use std::collections::BTreeMap; use std::env; @@ -66,7 +68,7 @@ impl EnvConfig { { let mut entries = BTreeMap::new(); for (key, value) in vars { - let Some(rest) = key.as_ref().strip_prefix(PREFIX) else { + let Some(rest) = strip_config_prefix(key.as_ref(), cfg!(windows)) else { continue; }; let segments: Vec = @@ -254,7 +256,7 @@ where } fn normalized_merge_key(key: &str) -> String { - if key.starts_with(PREFIX) { + if strip_config_prefix(key, cfg!(windows)).is_some() { key.to_ascii_uppercase() } else { key.to_owned() @@ -271,6 +273,18 @@ fn is_blank_or_control(value: &str) -> bool { || value.chars().any(char::is_control) } +fn strip_config_prefix(key: &str, case_insensitive: bool) -> Option<&str> { + if case_insensitive { + let prefix = key.get(..PREFIX.len())?; + if !prefix.eq_ignore_ascii_case(PREFIX) { + return None; + } + key.get(PREFIX.len()..) + } else { + key.strip_prefix(PREFIX) + } +} + #[cfg(test)] mod tests { use super::*; @@ -329,6 +343,31 @@ mod tests { ]) } + #[test] + fn prefix_matching_preserves_platform_key_identity() { + for (key, exact, folded) in [ + ( + "EDGEZERO__ADAPTER__INGRESS__MAX_CONNECTIONS", + Some("ADAPTER__INGRESS__MAX_CONNECTIONS"), + Some("ADAPTER__INGRESS__MAX_CONNECTIONS"), + ), + ( + "edgezero__ADAPTER__INGRESS__MAX_CONNECTIONS", + None, + Some("ADAPTER__INGRESS__MAX_CONNECTIONS"), + ), + ("EdGeZeRo__adapter__port", None, Some("adapter__port")), + ("EDGEZERO_ADAPTER", None, None), + ("short", None, None), + ("EDGEZERO\u{e9}__adapter", None, None), + ] { + assert_eq!(strip_config_prefix(key, false), exact); + assert_eq!(strip_config_prefix(key, true), folded); + } + let config = EnvConfig::from_vars([("edgezero__ADAPTER__PORT", "9090")]); + assert_eq!(config.adapter_port(), cfg!(windows).then_some("9090")); + } + #[test] fn parses_and_lower_cases_segments() { let cfg = sample(); diff --git a/crates/edgezero-core/src/error.rs b/crates/edgezero-core/src/error.rs index 96dd9ff1c..66773962e 100644 --- a/crates/edgezero-core/src/error.rs +++ b/crates/edgezero-core/src/error.rs @@ -6,42 +6,262 @@ use thiserror::Error; use crate::body::Body; use crate::config_store::ConfigStoreError; +use crate::error_detail::ProviderFailureDetail; use crate::http::{ - HeaderValue, Method, Response, StatusCode, - header::{CONTENT_TYPE, RETRY_AFTER}, + HeaderMap, HeaderValue, Method, Response, StatusCode, + header::{ALLOW, CONTENT_TYPE, RETRY_AFTER}, }; use crate::response::{IntoResponse, response_with_body}; +use crate::response_egress_framing::ResponseEgressFramingError; + +/// Stable identity for an EdgeZero-owned upstream decode failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BadGatewayDecodeReason { + Brotli, + Deflate, + Gzip, + Json, +} + +/// Stable classification for upstream failures that map to HTTP 502. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BadGatewayReason { + Decode(BadGatewayDecodeReason), + /// The native optimizer is unavailable, including local runtimes without the entitlement. + ImageOptimizerUnavailable, + Protocol, + Transport, + Unreachable, + Unspecified, +} + +/// Configured budget input that selected an effective deadline. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BudgetSource { + BatchCutoff, + ConnectTimeout, + Default, + IdleTimeout, + PerCallTimeout, + RequestDeadline, + Unspecified, +} + +/// Where an EdgeZero-owned request-byte limit was exceeded; never serialized on the wire. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum RequestBodyOverflowOrigin { + Inbound, + OutboundUpload, +} + +/// Stable identity for a response resource limit enforced by `EdgeZero`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ResponseLimitReason { + BrotliWindow, + BufferedBody, + DecodedBody, + DecoderMemory, + EncodedBody, + HeaderBytes, + HeaderCount, + Unspecified, +} + +/// Stable classification for typed configuration extraction failures. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum StoreExtractionReason { + BackendFailure, + BackendUnavailable, + DeadlineExceeded, + Deserialization, + IntegrityMismatch, + InvalidKey, + InvalidSecretValue, + MalformedEnvelope, + MissingBlob, + MissingRegistry, + MissingSecret, + SecretBackendUnavailable, + StructuralLimit, + UnknownStore, + UnsupportedVersion, + Validation, + ValueTooLarge, +} + +impl StoreExtractionReason { + fn wire_kind(self) -> &'static str { + match self { + Self::BackendFailure + | Self::IntegrityMismatch + | Self::InvalidSecretValue + | Self::MalformedEnvelope + | Self::MissingRegistry + | Self::StructuralLimit + | Self::UnsupportedVersion + | Self::UnknownStore + | Self::ValueTooLarge => "internal", + Self::BackendUnavailable | Self::DeadlineExceeded | Self::SecretBackendUnavailable => { + "service_unavailable" + } + Self::InvalidKey => "bad_request", + Self::Deserialization | Self::MissingBlob | Self::MissingSecret | Self::Validation => { + "config_out_of_date" + } + } + } + + fn wire_status(self) -> StatusCode { + match self { + Self::BackendFailure + | Self::IntegrityMismatch + | Self::InvalidSecretValue + | Self::MalformedEnvelope + | Self::MissingRegistry + | Self::StructuralLimit + | Self::UnsupportedVersion + | Self::UnknownStore + | Self::ValueTooLarge => StatusCode::INTERNAL_SERVER_ERROR, + Self::BackendUnavailable + | Self::DeadlineExceeded + | Self::Deserialization + | Self::MissingBlob + | Self::MissingSecret + | Self::SecretBackendUnavailable + | Self::Validation => StatusCode::SERVICE_UNAVAILABLE, + Self::InvalidKey => StatusCode::BAD_REQUEST, + } + } +} /// Application-level error that carries an HTTP status code. #[derive(Debug, Error)] #[non_exhaustive] pub enum EdgeError { + /// Upstream or transport failure. HTTP 502. + #[error("{message}")] + BadGateway { + message: String, + reason: BadGatewayReason, + provider_detail: Option, + }, #[error("{message}")] - BadRequest { message: String }, + BadRequest { + message: String, + provider_detail: Option, + }, /// The blob's `data` shape disagrees with the deployed `C` /// type. Re-running ` config push` for the deployed /// code revision fixes it. HTTP 503, kind /// `"config_out_of_date"`, carries `Retry-After: 60`. #[error("config out of date: {message}")] ConfigOutOfDate { message: String, field_path: String }, + /// A wall-clock deadline or per-request timeout fired. HTTP 504. + #[error("{message}")] + GatewayTimeout { + message: String, + cause: BudgetSource, + provider_detail: Option, + }, #[error("internal error: {source}")] Internal { - #[from] + #[source] source: AnyError, + provider_detail: Option, + }, + #[error( + "method {method} not allowed; allowed: {}", + allowed_method_names(.allowed) + )] + #[non_exhaustive] + MethodNotAllowed { + method: Method, + allowed: Vec, }, - #[error("method {method} not allowed; allowed: {allowed}")] - MethodNotAllowed { method: Method, allowed: String }, #[error("no route matched path: {path}")] NotFound { path: String }, #[error("not implemented: {message}")] NotImplemented { message: String }, + /// An EdgeZero-owned inbound or upload byte cap was exceeded. HTTP 413. + #[error("request body too large")] + PayloadTooLarge { origin: RequestBodyOverflowOrigin }, + #[error("{message}")] + RequestHeaderFieldsTooLarge { message: String }, + #[error("{message}")] + RequestTimeout { message: String }, + /// An upstream response exceeded an EdgeZero-owned resource policy. HTTP 502. + #[error("{message}")] + ResponseTooLarge { + message: String, + reason: ResponseLimitReason, + }, #[error("service unavailable: {message}")] ServiceUnavailable { message: String }, + #[error("{message}")] + StoreExtraction { + reason: StoreExtractionReason, + message: String, + field_path: Option, + }, + #[error("{message}")] + UriTooLong { message: String }, #[error("validation error: {message}")] Validation { message: String }, } +#[derive(Clone)] +pub(crate) struct RequestBodyOverflowResponse { + _origin: RequestBodyOverflowOrigin, +} + impl EdgeError { + #[inline] + pub fn bad_gateway(message: S) -> Self + where + S: Into, + { + EdgeError::BadGateway { + message: message.into(), + reason: BadGatewayReason::Unspecified, + provider_detail: None, + } + } + + /// Adds text-free provider detail without changing the gateway's public classification. + #[inline] + pub fn bad_gateway_with_detail( + message: S, + reason: BadGatewayReason, + detail: ProviderFailureDetail, + ) -> Self + where + S: Into, + { + Self::BadGateway { + message: message.into(), + reason, + provider_detail: Some(detail), + } + } + + #[inline] + pub fn bad_gateway_with_reason(message: S, reason: BadGatewayReason) -> Self + where + S: Into, + { + EdgeError::BadGateway { + message: message.into(), + reason, + provider_detail: None, + } + } + #[inline] pub fn bad_request(message: S) -> Self where @@ -49,6 +269,19 @@ impl EdgeError { { EdgeError::BadRequest { message: message.into(), + provider_detail: None, + } + } + + /// Retains provider evidence for host rejections without changing their 400 contract. + #[inline] + pub fn bad_request_with_detail(message: S, detail: ProviderFailureDetail) -> Self + where + S: Into, + { + Self::BadRequest { + message: message.into(), + provider_detail: Some(detail), } } @@ -69,34 +302,44 @@ impl EdgeError { } } - /// Construct from a `serde_path_to_error` error returned by - /// the deserialise wrapper around the blob's `data` field. - #[must_use] #[inline] - pub fn config_out_of_date_from_serde(serde_err: &SerdePathError) -> Self { - // The serde message embeds the offending stored VALUE (e.g. `invalid - // type: string "hunter2", expected u32`), and this message is serialised - // into the HTTP error body. The config blob may hold secrets, so the - // VALUE must not escape — report only the category. - // - // The `field_path` STRING segments are redacted (structure kept): a map - // key is indistinguishable from a struct field here and may be a secret, - // and the redaction invariant forbids a stored string on any path. The - // exact path is available from a local `config validate`. See - // `redact_serde_path`. - use serde_json::error::Category; - let category = match serde_err.inner().classify() { - Category::Data => "wrong type or invalid value", - Category::Syntax => "malformed JSON", - Category::Eof => "unexpected end of input", - Category::Io => "i/o error while reading", - }; - Self::ConfigOutOfDate { - message: format!( - "typed app-config is out of date ({category}; value redacted) — \ - run ` config push` for this deploy" - ), - field_path: redact_serde_path(serde_err.path()), + pub fn gateway_timeout(message: S) -> Self + where + S: Into, + { + EdgeError::GatewayTimeout { + message: message.into(), + cause: BudgetSource::Unspecified, + provider_detail: None, + } + } + + #[inline] + pub fn gateway_timeout_caused(message: S, cause: BudgetSource) -> Self + where + S: Into, + { + EdgeError::GatewayTimeout { + message: message.into(), + cause, + provider_detail: None, + } + } + + /// Adds text-free provider detail without replacing the selected timeout cause. + #[inline] + pub fn gateway_timeout_with_detail( + message: S, + cause: BudgetSource, + detail: ProviderFailureDetail, + ) -> Self + where + S: Into, + { + Self::GatewayTimeout { + message: message.into(), + cause, + provider_detail: Some(detail), } } @@ -110,12 +353,20 @@ impl EdgeError { #[inline] pub fn inner(&self) -> Option<&AnyError> { match self { - EdgeError::Internal { source } => Some(source), - EdgeError::BadRequest { .. } + EdgeError::Internal { source, .. } => Some(source), + EdgeError::BadGateway { .. } + | EdgeError::BadRequest { .. } | EdgeError::ConfigOutOfDate { .. } + | EdgeError::GatewayTimeout { .. } | EdgeError::NotFound { .. } | EdgeError::NotImplemented { .. } + | EdgeError::PayloadTooLarge { .. } + | EdgeError::RequestHeaderFieldsTooLarge { .. } + | EdgeError::RequestTimeout { .. } + | EdgeError::ResponseTooLarge { .. } | EdgeError::MethodNotAllowed { .. } + | EdgeError::StoreExtraction { .. } + | EdgeError::UriTooLong { .. } | EdgeError::Validation { .. } | EdgeError::ServiceUnavailable { .. } => None, } @@ -128,18 +379,42 @@ impl EdgeError { { EdgeError::Internal { source: error.into(), + provider_detail: None, } } - fn kind_str(&self) -> &'static str { + /// Retains the original internal source and its existing Display and wire behavior. + #[inline] + pub fn internal_with_detail(error: E, detail: ProviderFailureDetail) -> Self + where + E: Into, + { + Self::Internal { + source: error.into(), + provider_detail: Some(detail), + } + } + + /// Returns the stable public category without provider or application diagnostics. + #[must_use] + #[inline] + pub fn kind(&self) -> &'static str { match self { + EdgeError::BadGateway { .. } => "bad_gateway", EdgeError::BadRequest { .. } => "bad_request", EdgeError::ConfigOutOfDate { .. } => "config_out_of_date", + EdgeError::GatewayTimeout { .. } => "gateway_timeout", EdgeError::Internal { .. } => "internal", EdgeError::MethodNotAllowed { .. } => "method_not_allowed", EdgeError::NotFound { .. } => "not_found", EdgeError::NotImplemented { .. } => "not_implemented", + EdgeError::PayloadTooLarge { .. } => "payload_too_large", + EdgeError::RequestHeaderFieldsTooLarge { .. } => "request_header_fields_too_large", + EdgeError::RequestTimeout { .. } => "request_timeout", + EdgeError::ResponseTooLarge { .. } => "response_too_large", EdgeError::ServiceUnavailable { .. } => "service_unavailable", + EdgeError::StoreExtraction { reason, .. } => reason.wire_kind(), + EdgeError::UriTooLong { .. } => "uri_too_long", EdgeError::Validation { .. } => "validation", } } @@ -148,35 +423,39 @@ impl EdgeError { #[inline] pub fn message(&self) -> String { match self { - EdgeError::BadRequest { message } + EdgeError::BadGateway { message, .. } + | EdgeError::BadRequest { message, .. } | EdgeError::ConfigOutOfDate { message, .. } + | EdgeError::GatewayTimeout { message, .. } + | EdgeError::RequestHeaderFieldsTooLarge { message } + | EdgeError::RequestTimeout { message } + | EdgeError::ResponseTooLarge { message, .. } + | EdgeError::StoreExtraction { message, .. } + | EdgeError::UriTooLong { message } | EdgeError::Validation { message } | EdgeError::NotImplemented { message } | EdgeError::ServiceUnavailable { message } => message.clone(), EdgeError::NotFound { path } => format!("no route matched path: {path}"), EdgeError::MethodNotAllowed { method, allowed } => { - format!("method {method} not allowed; allowed: {allowed}") + format!( + "method {method} not allowed; allowed: {}", + allowed_method_names(allowed) + ) } - EdgeError::Internal { source } => format!("internal error: {source}"), + EdgeError::Internal { source, .. } => format!("internal error: {source}"), + EdgeError::PayloadTooLarge { .. } => "request body too large".to_owned(), } } #[must_use] #[inline] pub fn method_not_allowed(method: &Method, allowed: &[Method]) -> Self { - let mut names = allowed - .iter() - .map(|name| name.as_str().to_owned()) - .collect::>(); - names.sort(); - let allowed_list = if names.is_empty() { - "(none)".to_owned() - } else { - names.join(", ") - }; + let mut sorted_allowed = allowed.to_vec(); + sorted_allowed.sort_by(|left, right| left.as_str().cmp(right.as_str())); + sorted_allowed.dedup(); EdgeError::MethodNotAllowed { method: method.clone(), - allowed: allowed_list, + allowed: sorted_allowed, } } @@ -198,6 +477,121 @@ impl EdgeError { } } + /// Constructs a request-byte overflow without cap-specific or provider diagnostics. + #[must_use] + #[inline] + pub const fn payload_too_large(origin: RequestBodyOverflowOrigin) -> Self { + Self::PayloadTooLarge { origin } + } + + /// Borrows safe provider evidence. Rendering never serializes or formats this detail. + #[must_use] + #[inline] + pub const fn provider_failure_detail(&self) -> Option<&ProviderFailureDetail> { + match self { + Self::BadGateway { + provider_detail, .. + } + | Self::BadRequest { + provider_detail, .. + } + | Self::GatewayTimeout { + provider_detail, .. + } + | Self::Internal { + provider_detail, .. + } => provider_detail.as_ref(), + Self::ConfigOutOfDate { .. } + | Self::MethodNotAllowed { .. } + | Self::NotFound { .. } + | Self::NotImplemented { .. } + | Self::PayloadTooLarge { .. } + | Self::RequestHeaderFieldsTooLarge { .. } + | Self::RequestTimeout { .. } + | Self::ResponseTooLarge { .. } + | Self::ServiceUnavailable { .. } + | Self::StoreExtraction { .. } + | Self::UriTooLong { .. } + | Self::Validation { .. } => None, + } + } + + /// Returns whether a request-byte cap fired while reading ingress or uploading outbound. + #[must_use] + #[inline] + pub const fn request_body_overflow_origin(&self) -> Option { + if let Self::PayloadTooLarge { origin } = self { + Some(*origin) + } else { + None + } + } + + #[inline] + pub fn request_header_fields_too_large(message: S) -> Self + where + S: Into, + { + Self::RequestHeaderFieldsTooLarge { + message: message.into(), + } + } + + #[inline] + pub fn request_timeout(message: S) -> Self + where + S: Into, + { + Self::RequestTimeout { + message: message.into(), + } + } + + pub(crate) fn required_response_headers(&self) -> Result { + let mut headers = HeaderMap::new(); + if let EdgeError::MethodNotAllowed { allowed, .. } = self { + let value = HeaderValue::from_str(&allowed_header_value(allowed)) + .map_err(EdgeError::internal)?; + headers.insert(ALLOW, value); + } + if self.kind() == "config_out_of_date" { + headers.insert(RETRY_AFTER, HeaderValue::from_static("60")); + } + Ok(headers) + } + + /// Reads an owned precommit framing reason without formatting its source or changing wire errors. + #[must_use] + #[inline] + pub fn response_framing_error(&self) -> Option { + let Self::Internal { source, .. } = self else { + return None; + }; + source.downcast_ref::().copied() + } + + #[inline] + pub fn response_too_large(message: S) -> Self + where + S: Into, + { + EdgeError::ResponseTooLarge { + message: message.into(), + reason: ResponseLimitReason::Unspecified, + } + } + + #[inline] + pub fn response_too_large_with_reason(message: S, reason: ResponseLimitReason) -> Self + where + S: Into, + { + EdgeError::ResponseTooLarge { + message: message.into(), + reason, + } + } + #[inline] pub fn service_unavailable(message: S) -> Self where @@ -212,18 +606,99 @@ impl EdgeError { #[inline] pub fn status(&self) -> StatusCode { match self { + EdgeError::BadGateway { .. } | EdgeError::ResponseTooLarge { .. } => { + StatusCode::BAD_GATEWAY + } EdgeError::BadRequest { .. } => StatusCode::BAD_REQUEST, EdgeError::ConfigOutOfDate { .. } | EdgeError::ServiceUnavailable { .. } => { StatusCode::SERVICE_UNAVAILABLE } + EdgeError::GatewayTimeout { .. } => StatusCode::GATEWAY_TIMEOUT, + EdgeError::RequestHeaderFieldsTooLarge { .. } => { + StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE + } + EdgeError::RequestTimeout { .. } => StatusCode::REQUEST_TIMEOUT, EdgeError::Validation { .. } => StatusCode::UNPROCESSABLE_ENTITY, EdgeError::NotFound { .. } => StatusCode::NOT_FOUND, EdgeError::MethodNotAllowed { .. } => StatusCode::METHOD_NOT_ALLOWED, EdgeError::NotImplemented { .. } => StatusCode::NOT_IMPLEMENTED, + EdgeError::PayloadTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE, + EdgeError::StoreExtraction { reason, .. } => reason.wire_status(), + EdgeError::UriTooLong { .. } => StatusCode::URI_TOO_LONG, EdgeError::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR, } } + /// Constructs a redacted deserialization failure from a serde path error. + #[must_use] + #[inline] + pub fn store_deserialization_from_serde(serde_err: &SerdePathError) -> Self { + use serde_json::error::Category; + + let category = match serde_err.inner().classify() { + Category::Data => "wrong type or invalid value", + Category::Syntax => "malformed JSON", + Category::Eof => "unexpected end of input", + Category::Io => "i/o error while reading", + }; + let path = redact_serde_path(serde_err.path()); + Self::store_extraction( + StoreExtractionReason::Deserialization, + format!("typed app-config is out of date ({category}; value redacted)"), + Some(path), + ) + } + + #[must_use] + #[inline] + pub fn store_extraction( + reason: StoreExtractionReason, + message: Msg, + field_path: Option, + ) -> Self + where + Msg: Into, + { + Self::StoreExtraction { + reason, + message: message.into(), + field_path: field_path.filter(|path| !path.is_empty()), + } + } + + #[must_use] + #[inline] + pub fn store_extraction_reason(&self) -> Option { + match self { + Self::StoreExtraction { reason, .. } => Some(*reason), + Self::BadGateway { .. } + | Self::BadRequest { .. } + | Self::ConfigOutOfDate { .. } + | Self::GatewayTimeout { .. } + | Self::Internal { .. } + | Self::MethodNotAllowed { .. } + | Self::NotFound { .. } + | Self::NotImplemented { .. } + | Self::PayloadTooLarge { .. } + | Self::RequestHeaderFieldsTooLarge { .. } + | Self::RequestTimeout { .. } + | Self::ResponseTooLarge { .. } + | Self::ServiceUnavailable { .. } + | Self::UriTooLong { .. } + | Self::Validation { .. } => None, + } + } + + #[inline] + pub fn uri_too_long(message: S) -> Self + where + S: Into, + { + Self::UriTooLong { + message: message.into(), + } + } + #[inline] pub fn validation(message: S) -> Self where @@ -233,14 +708,54 @@ impl EdgeError { message: message.into(), } } + + fn wire_message(&self) -> String { + match self { + EdgeError::BadGateway { .. } => "bad gateway".to_owned(), + EdgeError::GatewayTimeout { .. } => "gateway timeout".to_owned(), + EdgeError::Internal { .. } => "internal server error".to_owned(), + EdgeError::ResponseTooLarge { .. } => { + "upstream response exceeded configured limits".to_owned() + } + EdgeError::BadRequest { .. } + | EdgeError::ConfigOutOfDate { .. } + | EdgeError::MethodNotAllowed { .. } + | EdgeError::NotFound { .. } + | EdgeError::NotImplemented { .. } + | EdgeError::PayloadTooLarge { .. } + | EdgeError::RequestHeaderFieldsTooLarge { .. } + | EdgeError::RequestTimeout { .. } + | EdgeError::ServiceUnavailable { .. } + | EdgeError::StoreExtraction { .. } + | EdgeError::UriTooLong { .. } + | EdgeError::Validation { .. } => self.message(), + } + } +} + +impl From for EdgeError { + #[inline] + fn from(source: AnyError) -> Self { + Self::internal(source) + } } impl From for EdgeError { #[inline] fn from(err: ConfigStoreError) -> Self { match err { - ConfigStoreError::InvalidKey { message } => EdgeError::bad_request(message), - ConfigStoreError::Unavailable { message } => EdgeError::service_unavailable(message), + ConfigStoreError::DeadlineExceeded => { + EdgeError::service_unavailable("config store read deadline exceeded") + } + ConfigStoreError::InvalidKey { .. } => { + EdgeError::bad_request("config store rejected the requested key") + } + ConfigStoreError::Unavailable { .. } => { + EdgeError::service_unavailable("config store is unavailable") + } + ConfigStoreError::ValueTooLarge => { + EdgeError::internal(anyhow::anyhow!("config store value too large")) + } ConfigStoreError::Internal { source } => EdgeError::internal(source), } } @@ -249,8 +764,8 @@ impl From for EdgeError { impl IntoResponse for EdgeError { #[inline] fn into_response(self) -> Result { - let kind = self.kind_str(); - let is_config_out_of_date = matches!(self, EdgeError::ConfigOutOfDate { .. }); + let required_headers = self.required_response_headers()?; + let kind = self.kind(); // `ConfigOutOfDate { field_path: String::new(), .. }` (the missing-blob // path) must OMIT the `field_path` JSON key entirely, not emit // `"field_path": ""`. Per spec 6.3.1. @@ -258,41 +773,93 @@ impl IntoResponse for EdgeError { EdgeError::ConfigOutOfDate { field_path, .. } if !field_path.is_empty() => { Some(field_path.as_str()) } - EdgeError::BadRequest { .. } + EdgeError::StoreExtraction { + field_path: Some(field_path), + .. + } => Some(field_path.as_str()), + EdgeError::BadGateway { .. } + | EdgeError::BadRequest { .. } | EdgeError::ConfigOutOfDate { .. } + | EdgeError::GatewayTimeout { .. } | EdgeError::Internal { .. } | EdgeError::MethodNotAllowed { .. } | EdgeError::NotFound { .. } | EdgeError::NotImplemented { .. } + | EdgeError::PayloadTooLarge { .. } + | EdgeError::RequestHeaderFieldsTooLarge { .. } + | EdgeError::RequestTimeout { .. } + | EdgeError::ResponseTooLarge { .. } | EdgeError::ServiceUnavailable { .. } + | EdgeError::StoreExtraction { .. } + | EdgeError::UriTooLong { .. } | EdgeError::Validation { .. } => None, }; let status = self.status(); - let message = self.message(); + let message = self.wire_message(); - let mut error_obj = serde_json::Map::new(); - error_obj.insert("status".into(), serde_json::Value::from(status.as_u16())); - error_obj.insert("kind".into(), serde_json::Value::from(kind)); - error_obj.insert("message".into(), serde_json::Value::from(message)); - if let Some(field_path) = field_path_opt { - error_obj.insert("field_path".into(), serde_json::Value::from(field_path)); + let mut payload = json!({ + "error": { + "status": status.as_u16(), + "kind": kind, + "message": message, + }, + }); + if let Some(field_path) = field_path_opt + && let Some(error_obj) = payload + .get_mut("error") + .and_then(serde_json::Value::as_object_mut) + { + error_obj.insert("field_path".into(), json!(field_path)); } - let payload = json!({ "error": serde_json::Value::Object(error_obj) }); let body = json_or_text(&payload); let mut response = response_with_body(status, body)?; response .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - if is_config_out_of_date { - response - .headers_mut() - .insert(RETRY_AFTER, HeaderValue::from_static("60")); + response.headers_mut().extend(required_headers); + if let Some(origin) = self.request_body_overflow_origin() { + mark_body_overflow_response(&mut response, origin); } Ok(response) } } +pub(crate) fn mark_body_overflow_response( + response: &mut Response, + origin: RequestBodyOverflowOrigin, +) { + response + .extensions_mut() + .insert(RequestBodyOverflowResponse { _origin: origin }); +} + +pub(crate) fn suppress_overflow_retry_after(response: &mut Response) { + if response + .extensions() + .get::() + .is_some() + { + response.headers_mut().remove(RETRY_AFTER); + } +} + +fn allowed_header_value(allowed: &[Method]) -> String { + allowed + .iter() + .map(Method::as_str) + .collect::>() + .join(", ") +} + +fn allowed_method_names(allowed: &[Method]) -> String { + if allowed.is_empty() { + "(none)".to_owned() + } else { + allowed_header_value(allowed) + } +} + /// Render a `serde_path_to_error` path with its STRING segments redacted while /// preserving structure (dots and sequence indices). /// @@ -346,10 +913,404 @@ where #[cfg(test)] mod tests { use super::*; + use crate::error_detail::{FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase}; use crate::http::Method; use serde::ser; + use std::io::Error as IoError; use std::str; + #[test] + fn framing_detail_preserves_each_existing_display_and_wire_error() { + use crate::response_egress_framing::{ + ResponseEgressBodyLengthError, response_egress_body_length_error, + test_support::FRAMING_RULES, + }; + for reason in FRAMING_RULES { + let error = EdgeError::internal(reason); + assert_eq!(error.response_framing_error(), Some(reason)); + assert_eq!(error.to_string(), format!("internal error: {reason}")); + assert_eq!(error.kind(), "internal"); + let response = error.into_response().expect("response"); + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + let ordinary = EdgeError::internal(anyhow::anyhow!("private token=secret")) + .into_response() + .expect("ordinary response"); + assert_eq!(response.status(), ordinary.status()); + assert_eq!(response.headers(), ordinary.headers()); + let (Some(actual), Some(expected)) = ( + response.into_body().into_bytes(), + ordinary.into_body().into_bytes(), + ) else { + panic!("buffered wire error") + }; + assert_eq!(actual, expected); + } + assert!( + EdgeError::internal(anyhow::anyhow!("private token=secret")) + .response_framing_error() + .is_none() + ); + for length_error in [ + ResponseEgressBodyLengthError::Exceeded, + ResponseEgressBodyLengthError::Incomplete, + ] { + let error = EdgeError::internal(length_error); + assert_eq!(error.response_framing_error(), None); + assert_eq!( + response_egress_body_length_error(&error), + Some(length_error) + ); + } + } + + #[test] + fn provider_diagnostics_never_change_public_errors() { + let detail = ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::Send, + cause: FastlyFailureCause::ConnectionRefused, + }; + let pairs = [ + ( + EdgeError::bad_request("outbound request rejected"), + EdgeError::bad_request_with_detail("outbound request rejected", detail), + ), + ( + EdgeError::bad_gateway_with_reason( + "upstream unavailable", + BadGatewayReason::Unreachable, + ), + EdgeError::bad_gateway_with_detail( + "upstream unavailable", + BadGatewayReason::Unreachable, + detail, + ), + ), + ( + EdgeError::gateway_timeout_caused( + "upstream timed out", + BudgetSource::PerCallTimeout, + ), + EdgeError::gateway_timeout_with_detail( + "upstream timed out", + BudgetSource::PerCallTimeout, + detail, + ), + ), + ( + EdgeError::internal(anyhow::anyhow!("https://example.com/?token=secret")), + EdgeError::internal_with_detail( + anyhow::anyhow!("https://example.com/?token=secret"), + detail, + ), + ), + ]; + for (plain, diagnosed) in pairs { + assert!(plain.provider_failure_detail().is_none()); + assert_eq!(diagnosed.provider_failure_detail(), Some(&detail)); + assert_eq!(plain.to_string(), diagnosed.to_string()); + assert_eq!(plain.message(), diagnosed.message()); + assert_eq!(plain.status(), diagnosed.status()); + assert_eq!(plain.kind(), diagnosed.kind()); + match (&plain, &diagnosed) { + (EdgeError::BadRequest { .. }, EdgeError::BadRequest { .. }) => {} + ( + EdgeError::BadGateway { reason: left, .. }, + EdgeError::BadGateway { reason: right, .. }, + ) => assert_eq!(left, right), + ( + EdgeError::GatewayTimeout { cause: left, .. }, + EdgeError::GatewayTimeout { cause: right, .. }, + ) => assert_eq!(left, right), + (EdgeError::Internal { .. }, EdgeError::Internal { .. }) => assert_eq!( + plain.inner().unwrap().to_string(), + diagnosed.inner().unwrap().to_string() + ), + _ => panic!("diagnostic attachment must preserve the error variant"), + } + let before = plain.into_response().expect("plain response"); + let after = diagnosed.into_response().expect("diagnosed response"); + assert_eq!(before.status(), after.status()); + assert_eq!(before.headers(), after.headers()); + assert_eq!(before.body().as_bytes(), after.body().as_bytes()); + assert!( + !str::from_utf8(after.body().as_bytes().expect("buffered wire")) + .expect("wire text") + .contains("secret") + ); + } + } + + #[test] + fn internal_from_anyhow_keeps_the_original_source_without_detail() { + let source = IoError::from_raw_os_error(42); + let error = EdgeError::from(anyhow::Error::new(source)); + assert!(error.provider_failure_detail().is_none()); + assert_eq!( + error + .inner() + .expect("source") + .downcast_ref::() + .expect("original concrete source") + .raw_os_error(), + Some(42_i32) + ); + } + + #[test] + fn request_overflow_origins_share_one_fixed_wire_contract() { + for origin in [ + RequestBodyOverflowOrigin::Inbound, + RequestBodyOverflowOrigin::OutboundUpload, + ] { + let error = EdgeError::payload_too_large(origin); + assert_eq!(error.request_body_overflow_origin(), Some(origin)); + assert_eq!(error.to_string(), "request body too large"); + assert_eq!(error.message(), "request body too large"); + assert!(error.inner().is_none()); + assert!(error.store_extraction_reason().is_none()); + let response = error.into_response().expect("response"); + assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert!(!response.headers().contains_key(RETRY_AFTER)); + assert_eq!( + response + .body() + .to_json::() + .expect("JSON"), + json!({"error": { + "status": 413_u16, "kind": "payload_too_large", "message": "request body too large" + }}) + ); + } + assert!( + EdgeError::bad_request("malformed payload") + .request_body_overflow_origin() + .is_none() + ); + } + + #[test] + fn bad_gateway_and_gateway_timeout_surface() { + for (err, code, msg) in [ + ( + EdgeError::bad_gateway("upstream refused"), + StatusCode::BAD_GATEWAY, + "upstream refused", + ), + ( + EdgeError::gateway_timeout("deadline expired"), + StatusCode::GATEWAY_TIMEOUT, + "deadline expired", + ), + ] { + assert_eq!(err.status(), code); + assert_eq!(err.message(), msg); + assert!(err.inner().is_none()); + assert!(err.to_string().contains(msg)); + } + } + + #[test] + fn bad_gateway_and_gateway_timeout_json_shape() { + for (err, code, kind, msg) in [ + ( + EdgeError::bad_gateway("nope"), + 502_u16, + "bad_gateway", + "bad gateway", + ), + ( + EdgeError::bad_gateway_with_reason("nope", BadGatewayReason::Protocol), + 502_u16, + "bad_gateway", + "bad gateway", + ), + ( + EdgeError::bad_gateway_with_reason("nope", BadGatewayReason::Unreachable), + 502_u16, + "bad_gateway", + "bad gateway", + ), + ( + EdgeError::bad_gateway_with_reason("nope", BadGatewayReason::Transport), + 502_u16, + "bad_gateway", + "bad gateway", + ), + ( + EdgeError::gateway_timeout("late"), + 504_u16, + "gateway_timeout", + "gateway timeout", + ), + ( + EdgeError::gateway_timeout_caused("late", BudgetSource::PerCallTimeout), + 504_u16, + "gateway_timeout", + "gateway timeout", + ), + ( + EdgeError::gateway_timeout_caused("late", BudgetSource::RequestDeadline), + 504_u16, + "gateway_timeout", + "gateway timeout", + ), + ( + EdgeError::gateway_timeout_caused("late", BudgetSource::Default), + 504_u16, + "gateway_timeout", + "gateway timeout", + ), + ] { + let response = err.into_response().expect("response"); + assert_eq!(response.status().as_u16(), code); + let body_json = parse_body(response); + assert_eq!(body_json["error"]["status"], code); + assert_eq!(body_json["error"]["kind"], serde_json::Value::from(kind)); + assert_eq!(body_json["error"]["message"], serde_json::Value::from(msg)); + assert!( + body_json["error"].get("field_path").is_none(), + "502/504 carry no field_path" + ); + assert!( + body_json["error"].get("reason").is_none(), + "reason is not part of the wire shape" + ); + assert!( + body_json["error"].get("cause").is_none(), + "cause is not part of the wire shape" + ); + } + } + + #[test] + fn bad_gateway_decode_reason_is_not_serialized() { + for reason in [ + BadGatewayDecodeReason::Brotli, + BadGatewayDecodeReason::Deflate, + BadGatewayDecodeReason::Gzip, + BadGatewayDecodeReason::Json, + ] { + let err = EdgeError::bad_gateway_with_reason("nope", BadGatewayReason::Decode(reason)); + let response = err.into_response().expect("response"); + assert_eq!(response.status().as_u16(), 502); + let body_json = parse_body(response); + assert_eq!(body_json["error"]["status"], 502_u16); + assert_eq!(body_json["error"]["kind"], "bad_gateway"); + assert_eq!(body_json["error"]["message"], "bad gateway"); + assert!(body_json["error"].get("reason").is_none()); + } + } + + #[test] + fn server_error_wire_messages_do_not_expose_internal_diagnostics() { + const TOKEN: &str = "super-secret-token"; + let diagnostic = format!( + "provider failed for https://example.com/bid?access_token={TOKEN}: connection refused" + ); + let cases = [ + ( + EdgeError::bad_gateway_with_reason( + diagnostic.clone(), + BadGatewayReason::Unreachable, + ), + "bad gateway", + ), + ( + EdgeError::gateway_timeout_caused(diagnostic.clone(), BudgetSource::PerCallTimeout), + "gateway timeout", + ), + ( + EdgeError::internal(anyhow::anyhow!(diagnostic.clone())), + "internal server error", + ), + ( + EdgeError::response_too_large_with_reason( + diagnostic, + ResponseLimitReason::EncodedBody, + ), + "upstream response exceeded configured limits", + ), + ]; + + for (error, public_message) in cases { + assert!( + error.message().contains(TOKEN), + "the diagnostic remains available before wire conversion" + ); + let response = error.into_response().expect("response"); + let body = parse_body(response); + assert_eq!(body["error"]["message"], public_message); + let encoded = body.to_string(); + assert!(!encoded.contains(TOKEN), "token escaped in {encoded}"); + assert!( + !encoded.contains("access_token"), + "query escaped in {encoded}" + ); + assert!( + !encoded.contains("connection refused"), + "provider diagnostic escaped in {encoded}" + ); + } + } + + #[test] + fn config_store_provider_diagnostics_do_not_reach_wire() { + const TOKEN: &str = "provider-secret-token"; + let cases = [ + ( + ConfigStoreError::InvalidKey { + message: format!("invalid key includes {TOKEN}"), + }, + "config store rejected the requested key", + ), + ( + ConfigStoreError::Unavailable { + message: format!("provider unavailable at https://user:{TOKEN}@config.invalid"), + }, + "config store is unavailable", + ), + ]; + + for (provider_error, public_message) in cases { + let response = EdgeError::from(provider_error) + .into_response() + .expect("response"); + let body = parse_body(response); + assert_eq!(body["error"]["message"], public_message); + assert!(!body.to_string().contains(TOKEN)); + } + } + + #[test] + fn bad_gateway_reason_is_typed() { + let EdgeError::BadGateway { + reason: default_reason, + .. + } = EdgeError::bad_gateway("x") + else { + panic!("expected BadGateway"); + }; + assert_eq!(default_reason, BadGatewayReason::Unspecified); + + for expected in [ + BadGatewayReason::Decode(BadGatewayDecodeReason::Brotli), + BadGatewayReason::Decode(BadGatewayDecodeReason::Deflate), + BadGatewayReason::Decode(BadGatewayDecodeReason::Gzip), + BadGatewayReason::Decode(BadGatewayDecodeReason::Json), + BadGatewayReason::Protocol, + BadGatewayReason::Transport, + BadGatewayReason::Unreachable, + BadGatewayReason::Unspecified, + ] { + let EdgeError::BadGateway { reason, .. } = + EdgeError::bad_gateway_with_reason("x", expected) + else { + panic!("expected BadGateway"); + }; + assert_eq!(reason, expected); + } + } + #[test] fn bad_request_sets_status_and_message() { let err = EdgeError::bad_request("oops"); @@ -357,6 +1318,14 @@ mod tests { assert_eq!(err.message(), "oops"); } + #[test] + fn bare_gateway_timeout_is_unspecified() { + let EdgeError::GatewayTimeout { cause, .. } = EdgeError::gateway_timeout("x") else { + panic!("expected GatewayTimeout"); + }; + assert_eq!(cause, BudgetSource::Unspecified); + } + #[test] fn config_out_of_date_constructor_round_trips() { let err = EdgeError::config_out_of_date("missing field", "feature.new_checkout"); @@ -368,12 +1337,20 @@ mod tests { assert_eq!(message, "missing field"); assert_eq!(field_path, "feature.new_checkout"); } - EdgeError::BadRequest { .. } + EdgeError::BadGateway { .. } + | EdgeError::BadRequest { .. } + | EdgeError::GatewayTimeout { .. } | EdgeError::Internal { .. } | EdgeError::MethodNotAllowed { .. } | EdgeError::NotFound { .. } | EdgeError::NotImplemented { .. } + | EdgeError::RequestHeaderFieldsTooLarge { .. } + | EdgeError::RequestTimeout { .. } + | EdgeError::ResponseTooLarge { .. } | EdgeError::ServiceUnavailable { .. } + | EdgeError::StoreExtraction { .. } + | EdgeError::UriTooLong { .. } + | EdgeError::PayloadTooLarge { .. } | EdgeError::Validation { .. } => panic!("expected ConfigOutOfDate"), } } @@ -387,7 +1364,7 @@ mod tests { } #[test] - fn config_out_of_date_from_serde_extracts_path_and_message() { + fn store_deserialization_from_serde_extracts_path_and_message() { use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -404,32 +1381,29 @@ mod tests { // Feed JSON that puts a string where u32 is expected to force a // type-mismatch error at path `service.timeout_ms`. - let json = r#"{"service": {"timeout_ms": "not-a-number"}}"#; - let de = &mut serde_json::Deserializer::from_str(json); + let json = json!({"service": {"timeout_ms": "not-a-number"}}).to_string(); + let de = &mut serde_json::Deserializer::from_str(&json); let result: Result = serde_path_to_error::deserialize(de); let serde_err = result.expect_err("expected deserialization error"); - let err = EdgeError::config_out_of_date_from_serde(&serde_err); + let err = EdgeError::store_deserialization_from_serde(&serde_err); assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE); assert!(!err.message().is_empty()); - match err { - EdgeError::ConfigOutOfDate { field_path, .. } => { - // String segments redacted, structure preserved. - assert_eq!(field_path, "."); - } - EdgeError::BadRequest { .. } - | EdgeError::Internal { .. } - | EdgeError::MethodNotAllowed { .. } - | EdgeError::NotFound { .. } - | EdgeError::NotImplemented { .. } - | EdgeError::ServiceUnavailable { .. } - | EdgeError::Validation { .. } => panic!("expected ConfigOutOfDate"), - } + let EdgeError::StoreExtraction { + field_path: Some(field_path), + reason: StoreExtractionReason::Deserialization, + .. + } = err + else { + panic!("expected deserialization store extraction"); + }; + // String segments redacted, structure preserved. + assert_eq!(field_path, "."); } #[test] - fn config_out_of_date_from_serde_redacts_map_key_from_path_and_message() { + fn store_deserialization_from_serde_redacts_map_key_from_path_and_message() { use serde::Deserialize; use std::collections::BTreeMap; @@ -450,40 +1424,34 @@ mod tests { // field in the serde path, so every string segment is redacted (structure // kept). const SENTINEL: &str = "SUPER_SECRET_MAP_KEY"; - let json = format!(r#"{{"items": {{"{SENTINEL}": {{"port": "nope"}}}}}}"#); + let json = json!({"items": {(SENTINEL): {"port": "nope"}}}).to_string(); let de = &mut serde_json::Deserializer::from_str(&json); let result: Result = serde_path_to_error::deserialize(de); let serde_err = result.expect_err("expected deserialization error"); - let err = EdgeError::config_out_of_date_from_serde(&serde_err); - match err { - EdgeError::ConfigOutOfDate { - field_path, - message, - } => { - assert!( - !message.contains(SENTINEL), - "a stored map key must never reach the message: {message}" - ); - assert!( - !field_path.contains(SENTINEL), - "a stored map key must never reach the field_path: {field_path}" - ); - // Structure is preserved (items..port -> redacted, dotted). - assert_eq!(field_path, ".."); - } - EdgeError::BadRequest { .. } - | EdgeError::Internal { .. } - | EdgeError::MethodNotAllowed { .. } - | EdgeError::NotFound { .. } - | EdgeError::NotImplemented { .. } - | EdgeError::ServiceUnavailable { .. } - | EdgeError::Validation { .. } => panic!("expected ConfigOutOfDate"), - } + let err = EdgeError::store_deserialization_from_serde(&serde_err); + let EdgeError::StoreExtraction { + field_path: Some(field_path), + message, + reason: StoreExtractionReason::Deserialization, + } = err + else { + panic!("expected deserialization store extraction"); + }; + assert!( + !message.contains(SENTINEL), + "a stored map key must never reach the message: {message}" + ); + assert!( + !field_path.contains(SENTINEL), + "a stored map key must never reach the field_path: {field_path}" + ); + // Structure is preserved (items..port -> redacted, dotted). + assert_eq!(field_path, ".."); } #[test] - fn config_out_of_date_from_serde_root_error_passes_through_sentinel() { + fn store_deserialization_from_serde_root_error_passes_through_sentinel() { use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -492,33 +1460,30 @@ mod tests { value: u32, } - // A completely invalid JSON causes a root-level error. serde_path_to_error + // A valid JSON string instead of an object causes a root-level error. serde_path_to_error // returns "." as the path sentinel in this case. - let json = r#""not-an-object""#; - let de = &mut serde_json::Deserializer::from_str(json); + let json = json!("not-an-object").to_string(); + let de = &mut serde_json::Deserializer::from_str(&json); let result: Result = serde_path_to_error::deserialize(de); let serde_err = result.expect_err("expected deserialization error"); let expected_path = serde_err.path().to_string(); - let err = EdgeError::config_out_of_date_from_serde(&serde_err); + let err = EdgeError::store_deserialization_from_serde(&serde_err); assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE); - match err { - EdgeError::ConfigOutOfDate { field_path, .. } => { - // The from_serde constructor passes the library's path through - // verbatim; for root-level errors that is ".". - assert_eq!( - field_path, expected_path, - "field_path should match serde_path_to_error sentinel" - ); - } - EdgeError::BadRequest { .. } - | EdgeError::Internal { .. } - | EdgeError::MethodNotAllowed { .. } - | EdgeError::NotFound { .. } - | EdgeError::NotImplemented { .. } - | EdgeError::ServiceUnavailable { .. } - | EdgeError::Validation { .. } => panic!("expected ConfigOutOfDate"), - } + let EdgeError::StoreExtraction { + field_path: Some(field_path), + reason: StoreExtractionReason::Deserialization, + .. + } = err + else { + panic!("expected deserialization store extraction"); + }; + // The serde constructor passes the library's path through verbatim; + // for root-level errors that is ".". + assert_eq!( + field_path, expected_path, + "field_path should match serde_path_to_error sentinel" + ); } #[test] @@ -532,14 +1497,32 @@ mod tests { fn config_store_error_invalid_key_maps_to_bad_request() { let err = EdgeError::from(ConfigStoreError::invalid_key("invalid config key")); assert_eq!(err.status(), StatusCode::BAD_REQUEST); - assert_eq!(err.message(), "invalid config key"); + assert_eq!(err.message(), "config store rejected the requested key"); } #[test] fn config_store_error_unavailable_maps_to_service_unavailable() { let err = EdgeError::from(ConfigStoreError::unavailable("backend offline")); assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE); - assert_eq!(err.message(), "backend offline"); + assert_eq!(err.message(), "config store is unavailable"); + } + + #[test] + fn gateway_timeout_caused_preserves_cause() { + for expected in [ + BudgetSource::BatchCutoff, + BudgetSource::Default, + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::Unspecified, + ] { + let EdgeError::GatewayTimeout { cause, .. } = + EdgeError::gateway_timeout_caused("x", expected) + else { + panic!("expected GatewayTimeout"); + }; + assert_eq!(cause, expected); + } } #[test] @@ -601,9 +1584,36 @@ mod tests { #[test] fn method_not_allowed_lists_methods_sorted() { - let err = EdgeError::method_not_allowed(&Method::POST, &[Method::GET, Method::DELETE]); + let err = EdgeError::method_not_allowed( + &Method::POST, + &[Method::GET, Method::DELETE, Method::GET], + ); assert_eq!(err.status(), StatusCode::METHOD_NOT_ALLOWED); assert!(err.message().contains("allowed: DELETE, GET")); + assert_eq!( + err.to_string(), + "method POST not allowed; allowed: DELETE, GET" + ); + let EdgeError::MethodNotAllowed { allowed, .. } = err else { + panic!("expected MethodNotAllowed"); + }; + assert_eq!(allowed, vec![Method::DELETE, Method::GET]); + } + + #[test] + fn method_not_allowed_response_emits_sorted_deduplicated_allow_header() { + let response = EdgeError::method_not_allowed( + &Method::POST, + &[Method::GET, Method::DELETE, Method::GET], + ) + .into_response() + .expect("response"); + + assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); + assert_eq!( + response.headers().get(ALLOW).expect("Allow header"), + "DELETE, GET" + ); } #[test] @@ -655,6 +1665,7 @@ mod tests { }}; } + assert_kind!(EdgeError::bad_gateway("x"), "bad_gateway", 502_u16); assert_kind!(EdgeError::bad_request("x"), "bad_request", 400_u16); assert_kind!( EdgeError::config_out_of_date("x", "f"), @@ -673,6 +1684,12 @@ mod tests { ); assert_kind!(EdgeError::not_found("/x"), "not_found", 404_u16); assert_kind!(EdgeError::not_implemented("x"), "not_implemented", 501_u16); + assert_kind!( + EdgeError::response_too_large("x"), + "response_too_large", + 502_u16 + ); + assert_kind!(EdgeError::gateway_timeout("x"), "gateway_timeout", 504_u16); assert_kind!( EdgeError::service_unavailable("x"), "service_unavailable", @@ -695,15 +1712,239 @@ mod tests { }}; } + assert_retry_after!(EdgeError::bad_gateway("x"), false); assert_retry_after!(EdgeError::bad_request("x"), false); + assert_retry_after!(EdgeError::gateway_timeout("x"), false); assert_retry_after!(EdgeError::internal(anyhow::anyhow!("x")), false); + assert_retry_after!(EdgeError::response_too_large("x"), false); // ServiceUnavailable is also 503 but must NOT carry Retry-After assert_retry_after!(EdgeError::service_unavailable("x"), false); assert_retry_after!(EdgeError::config_out_of_date("x", "f"), true); } + #[test] + fn response_too_large_constructors_preserve_unspecified_and_specific_reason() { + let EdgeError::ResponseTooLarge { reason, .. } = EdgeError::response_too_large("large") + else { + panic!("expected ResponseTooLarge"); + }; + assert_eq!(reason, ResponseLimitReason::Unspecified); + + let EdgeError::ResponseTooLarge { + reason: specific_reason, + .. + } = EdgeError::response_too_large_with_reason("large", ResponseLimitReason::EncodedBody) + else { + panic!("expected ResponseTooLarge"); + }; + assert_eq!(specific_reason, ResponseLimitReason::EncodedBody); + } + + #[test] + fn response_too_large_preserves_every_reason_without_serializing_it() { + for reason in [ + ResponseLimitReason::BrotliWindow, + ResponseLimitReason::BufferedBody, + ResponseLimitReason::DecodedBody, + ResponseLimitReason::DecoderMemory, + ResponseLimitReason::EncodedBody, + ResponseLimitReason::HeaderBytes, + ResponseLimitReason::HeaderCount, + ResponseLimitReason::Unspecified, + ] { + let error = EdgeError::response_too_large_with_reason("response limit", reason); + assert_eq!(error.status(), StatusCode::BAD_GATEWAY); + let EdgeError::ResponseTooLarge { + reason: stored_reason, + .. + } = &error + else { + panic!("expected ResponseTooLarge"); + }; + assert_eq!(*stored_reason, reason); + + let response = error.into_response().expect("response"); + assert!(response.headers().get(RETRY_AFTER).is_none()); + let body = parse_body(response); + assert_eq!(body["error"]["status"], 502_u16); + assert_eq!(body["error"]["kind"], "response_too_large"); + assert_eq!( + body["error"]["message"], + "upstream response exceeded configured limits" + ); + assert!(body["error"].get("reason").is_none()); + assert!(body["error"].get("field_path").is_none()); + } + } + + #[test] + #[expect( + clippy::too_many_lines, + reason = "the exhaustive extraction-reason wire-policy table is clearer in one test" + )] + fn store_extraction_reason_wire_policy_table() { + let cases = [ + ( + StoreExtractionReason::BackendFailure, + 500_u16, + "internal", + false, + ), + ( + StoreExtractionReason::BackendUnavailable, + 503_u16, + "service_unavailable", + false, + ), + ( + StoreExtractionReason::DeadlineExceeded, + 503_u16, + "service_unavailable", + false, + ), + ( + StoreExtractionReason::IntegrityMismatch, + 500_u16, + "internal", + false, + ), + ( + StoreExtractionReason::MalformedEnvelope, + 500_u16, + "internal", + false, + ), + ( + StoreExtractionReason::InvalidKey, + 400_u16, + "bad_request", + false, + ), + ( + StoreExtractionReason::InvalidSecretValue, + 500_u16, + "internal", + false, + ), + ( + StoreExtractionReason::MissingBlob, + 503_u16, + "config_out_of_date", + true, + ), + ( + StoreExtractionReason::MissingRegistry, + 500_u16, + "internal", + false, + ), + ( + StoreExtractionReason::MissingSecret, + 503_u16, + "config_out_of_date", + true, + ), + ( + StoreExtractionReason::Deserialization, + 503_u16, + "config_out_of_date", + true, + ), + ( + StoreExtractionReason::SecretBackendUnavailable, + 503_u16, + "service_unavailable", + false, + ), + ( + StoreExtractionReason::StructuralLimit, + 500_u16, + "internal", + false, + ), + ( + StoreExtractionReason::UnsupportedVersion, + 500_u16, + "internal", + false, + ), + ( + StoreExtractionReason::UnknownStore, + 500_u16, + "internal", + false, + ), + ( + StoreExtractionReason::Validation, + 503_u16, + "config_out_of_date", + true, + ), + ( + StoreExtractionReason::ValueTooLarge, + 500_u16, + "internal", + false, + ), + ]; + + for (reason, status, kind, retry_after) in cases { + let error = EdgeError::store_extraction( + reason, + "safe extraction diagnostic", + Some(String::from("field")), + ); + assert_eq!(error.store_extraction_reason(), Some(reason)); + let response = error.into_response().expect("response"); + assert_eq!(response.status().as_u16(), status); + assert_eq!(response.headers().contains_key(RETRY_AFTER), retry_after); + let body = parse_body(response); + assert_eq!(body["error"]["kind"], kind); + assert_eq!(body["error"]["field_path"], "field"); + assert!(body["error"].get("reason").is_none()); + } + + let error = EdgeError::store_extraction( + StoreExtractionReason::MissingBlob, + "missing", + Some(String::new()), + ); + let body = parse_body(error.into_response().expect("response")); + assert!(body["error"].get("field_path").is_none()); + } + + #[test] + fn inbound_boundary_errors_have_distinct_status_and_kind() { + for (error, status, kind) in [ + ( + EdgeError::request_header_fields_too_large("headers"), + 431_u16, + "request_header_fields_too_large", + ), + ( + EdgeError::request_timeout("request body deadline exceeded"), + 408_u16, + "request_timeout", + ), + (EdgeError::uri_too_long("target"), 414_u16, "uri_too_long"), + ] { + assert_eq!(error.status().as_u16(), status); + let body = parse_body(error.into_response().expect("response")); + assert_eq!(body["error"]["kind"], kind); + assert!(body["error"].get("field_path").is_none()); + } + } + #[test] fn field_path_only_on_config_out_of_date() { + for err in [EdgeError::bad_gateway("x"), EdgeError::gateway_timeout("x")] { + let body = parse_body(err.into_response().expect("response")); + assert!( + body["error"].get("field_path").is_none(), + "field_path should be absent for gateway errors" + ); + } + let bad_req_err = EdgeError::bad_request("x"); let bad_req_body = parse_body(bad_req_err.into_response().expect("response")); assert!( diff --git a/crates/edgezero-core/src/error_detail.rs b/crates/edgezero-core/src/error_detail.rs new file mode 100644 index 000000000..bdadc1489 --- /dev/null +++ b/crates/edgezero-core/src/error_detail.rs @@ -0,0 +1,378 @@ +//! Fixed-size, text-free provider diagnostics, separate from public error classification. +//! +//! Catalogs describe the pinned SDKs, not a claim that an adapter already populates every field. +//! Provider mappers must project only these enums and numeric payloads, never formatted errors. + +use std::io::ErrorKind; + +/// Reqwest's primary error kind; timeout/connect evidence is retained separately. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum AxumFailureKind { + Body, + Builder, + Decode, + Redirect, + Request, + Status, + Unknown, + Upgrade, +} + +/// Predicate and I/O evidence available from reqwest without copying its URL or text. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AxumFailureCause { + pub io_kind: Option, + pub is_connect: bool, + pub is_timeout: bool, + pub kind: AxumFailureKind, +} + +/// Allowlisted JavaScript exception names; arbitrary names and messages are not retained. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum CloudflareFailureCause { + AbortError, + TypeError, + Unknown, +} + +/// Fastly 0.13.1 send/backend causes, with only safe numeric or I/O-kind payloads. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum FastlyFailureCause { + BackendBetweenBytesTimeoutTooLarge, + BackendConnectTimeoutTooLarge, + BackendDisallowed, + BackendEncodingError, + BackendFirstByteTimeoutTooLarge, + BackendHostError { + status: i32, + }, + BackendInvalidHealthcheckValue, + BackendNameInUse, + BackendNameTooLong, + ConnectionLimitReached, + ConnectionRefused, + ConnectionTerminated, + ConnectionTimeout, + Custom, + DestinationIpUnroutable, + DestinationNotFound, + DestinationUnavailable, + DnsError { + rcode: Option, + info_code: Option, + }, + DnsTimeout, + FanoutNotEnabled, + Http2StreamError { + frame_type: u8, + error_code: u32, + }, + HttpCacheApiUnsupported, + HttpCacheLimitExceeded, + HttpIncompleteResponse, + HttpProtocolError, + HttpRequestCacheKeyInvalid, + HttpRequestUriInvalid, + HttpResponseBodyTooLarge, + HttpResponseHeaderSectionTooLarge, + HttpResponseStatusInvalid, + HttpResponseTimeout, + HttpUpgradeFailed, + ImageOptimizerUnsupported, + InternalError { + status: Option, + }, + IoError { + kind: ErrorKind, + }, + RequestCollapse, + TlsAlertReceived { + alert_id: Option, + }, + TlsCertificateError, + TlsConfigurationError, + TlsProtocolError, + Unknown, +} + +/// Adapter identity derived from the provider-specific diagnostic variant. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ProviderFailureAdapter { + Axum, + Cloudflare, + Fastly, + Spin, +} + +/// Provider detail contains no free text, SDK object, URL, header, or body copy. +/// +/// The provider identity and cause cannot disagree. Numeric unknown values are preserved; +/// unknown textual values must project to a closed `Unknown` variant. +/// +/// ```compile_fail +/// use edgezero_core::error_detail::FastlyFailureCause; +/// let _ = FastlyFailureCause::TlsAlertReceived { +/// alert_id: Some(42), +/// alert_message: "provider-controlled secret".to_owned(), +/// }; +/// ``` +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ProviderFailureDetail { + Axum { + phase: ProviderFailurePhase, + cause: AxumFailureCause, + }, + Cloudflare { + phase: ProviderFailurePhase, + cause: CloudflareFailureCause, + }, + Fastly { + phase: ProviderFailurePhase, + cause: FastlyFailureCause, + }, + Spin { + phase: ProviderFailurePhase, + cause: SpinFailureCause, + }, +} + +impl ProviderFailureDetail { + #[must_use] + #[inline] + pub const fn adapter(self) -> ProviderFailureAdapter { + match self { + Self::Axum { .. } => ProviderFailureAdapter::Axum, + Self::Cloudflare { .. } => ProviderFailureAdapter::Cloudflare, + Self::Fastly { .. } => ProviderFailureAdapter::Fastly, + Self::Spin { .. } => ProviderFailureAdapter::Spin, + } + } + + #[must_use] + #[inline] + pub const fn phase(self) -> ProviderFailurePhase { + match self { + Self::Axum { phase, .. } + | Self::Cloudflare { phase, .. } + | Self::Fastly { phase, .. } + | Self::Spin { phase, .. } => phase, + } + } +} + +/// The provider API boundary at which the original failure was observed. +/// This does not establish whether any request bytes reached the transport. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ProviderFailurePhase { + BackendRegistration, + RequestBodyUpload, + /// Request-resource construction or mutation before dispatch. + RequestPreparation, + ResponseBodyRead, + ResponseHeaders, + Send, +} + +/// Exact allowlist for Spin's text-only DNS rcode. Missing values remain `None` in the cause. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum SpinDnsRcode { + FormatError, + NameError, + NoError, + NotImplemented, + Refused, + ServerFailure, + Unknown, +} + +impl SpinDnsRcode { + /// Projects only NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP and REFUSED. + /// Other spellings, numeric strings and provider text are deliberately unknown. + #[must_use] + #[inline] + pub fn from_provider_text(text: &str) -> Self { + match text { + "NOERROR" => Self::NoError, + "FORMERR" => Self::FormatError, + "SERVFAIL" => Self::ServerFailure, + "NXDOMAIN" => Self::NameError, + "NOTIMP" => Self::NotImplemented, + "REFUSED" => Self::Refused, + _ => Self::Unknown, + } + } +} + +/// Spin SDK 7 / WASI HTTP 0.3 causes, including resource setters, without provider text. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum SpinFailureCause { + ConfigurationError, + ConnectionLimitReached, + ConnectionReadTimeout, + ConnectionRefused, + ConnectionTerminated, + ConnectionTimeout, + ConnectionWriteTimeout, + DestinationIpProhibited, + DestinationIpUnroutable, + DestinationNotFound, + DestinationUnavailable, + DnsError { + rcode: Option, + info_code: Option, + }, + DnsTimeout, + HeaderForbidden, + HeaderImmutable, + HeaderInvalidSyntax, + HeaderOther, + HeaderSizeExceeded, + HttpProtocolError, + HttpRequestBodySize(Option), + HttpRequestDenied, + HttpRequestHeaderSectionSize(Option), + HttpRequestHeaderSize(Option), + HttpRequestLengthRequired, + HttpRequestMethodInvalid, + HttpRequestTrailerSectionSize(Option), + HttpRequestTrailerSize(SpinFieldSize), + HttpRequestUriInvalid, + HttpRequestUriTooLong, + HttpResponseBodySize(Option), + HttpResponseContentCoding, + HttpResponseHeaderSectionSize(Option), + HttpResponseHeaderSize(SpinFieldSize), + HttpResponseIncomplete, + HttpResponseTimeout, + HttpResponseTrailerSectionSize(Option), + HttpResponseTrailerSize(SpinFieldSize), + HttpResponseTransferCoding, + HttpUpgradeFailed, + InternalError, + LoopDetected, + RequestAuthorityRejected, + RequestMethodRejected, + RequestOptionsImmutable(SpinTimeoutOption), + RequestOptionsOther(SpinTimeoutOption), + RequestPathRejected, + RequestSchemeRejected, + TlsAlertReceived { + alert_id: Option, + }, + TlsCertificateError, + TlsProtocolError, + Unknown, +} + +/// Sanitized field-size payload. The optional number is retained, never the provider's name. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SpinFieldSize { + pub field_size: Option, +} + +/// The outbound timeout setter rejected by the Spin host. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum SpinTimeoutOption { + BetweenBytes, + Connect, + FirstByte, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::mem::size_of; + + #[test] + fn provider_detail_is_copy_and_fixed_size() { + fn assert_copy() + where + T: Copy, + { + } + assert_copy::(); + assert!(size_of::() <= 48); + for (detail, adapter, phase) in [ + ( + ProviderFailureDetail::Axum { + phase: ProviderFailurePhase::Send, + cause: AxumFailureCause { + io_kind: Some(ErrorKind::ConnectionRefused), + is_connect: true, + is_timeout: false, + kind: AxumFailureKind::Request, + }, + }, + ProviderFailureAdapter::Axum, + ProviderFailurePhase::Send, + ), + ( + ProviderFailureDetail::Cloudflare { + phase: ProviderFailurePhase::Send, + cause: CloudflareFailureCause::TypeError, + }, + ProviderFailureAdapter::Cloudflare, + ProviderFailurePhase::Send, + ), + ( + ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: FastlyFailureCause::IoError { + kind: ErrorKind::UnexpectedEof, + }, + }, + ProviderFailureAdapter::Fastly, + ProviderFailurePhase::ResponseBodyRead, + ), + ( + ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::ResponseHeaders, + cause: SpinFailureCause::TlsAlertReceived { + alert_id: Some(u8::MAX), + }, + }, + ProviderFailureAdapter::Spin, + ProviderFailurePhase::ResponseHeaders, + ), + ] { + let copied = detail; + assert_eq!(copied, detail); + assert_eq!(copied.adapter(), adapter); + assert_eq!(copied.phase(), phase); + } + } + + #[test] + fn spin_dns_text_projection_is_closed_and_does_not_retain_input() { + for (text, expected) in [ + ("NOERROR", SpinDnsRcode::NoError), + ("FORMERR", SpinDnsRcode::FormatError), + ("SERVFAIL", SpinDnsRcode::ServerFailure), + ("NXDOMAIN", SpinDnsRcode::NameError), + ("NOTIMP", SpinDnsRcode::NotImplemented), + ("REFUSED", SpinDnsRcode::Refused), + ] { + assert_eq!(SpinDnsRcode::from_provider_text(text), expected); + } + for text in [ + "nxdomain", + "NXDOMAIN ", + "3", + "https://example.com/?token=secret", + "\r\nsecret", + ] { + let projected = SpinDnsRcode::from_provider_text(text); + assert_eq!(projected, SpinDnsRcode::Unknown); + assert!(!format!("{projected:?}").contains("secret")); + } + } +} diff --git a/crates/edgezero-core/src/extractor.rs b/crates/edgezero-core/src/extractor.rs index 7c131e7b5..ebe673cdd 100644 --- a/crates/edgezero-core/src/extractor.rs +++ b/crates/edgezero-core/src/extractor.rs @@ -1,24 +1,28 @@ use std::any; +use std::fmt; use std::future::Future; use std::ops::{Deref, DerefMut}; use std::pin::Pin; use async_trait::async_trait; use http::header; -use serde::de::DeserializeOwned; +use serde::de::{ + self, DeserializeOwned, DeserializeSeed, IgnoredAny, MapAccess, SeqAccess, Visitor, +}; use validator::Validate; use crate::app_config::{AppConfigMeta, SecretField, SecretKind, SecretPathSegment}; use crate::blob_envelope::{BlobEnvelope, BlobEnvelopeError}; -use crate::config_store::ConfigStoreHandle; -use crate::context::RequestContext; -use crate::error::EdgeError; +use crate::config_store::{ConfigExtractionLimits, ConfigStoreError, ConfigStoreHandle}; +use crate::context::{DEFAULT_INBOUND_FORM_BYTES, DEFAULT_INBOUND_JSON_BYTES, RequestContext}; +use crate::error::{EdgeError, StoreExtractionReason}; use crate::http::HeaderMap; use crate::secret_store::SecretError; use crate::store_registry::{ BoundConfigStore, BoundKvStore, BoundSecretStore, ConfigRegistry, ConfigStoreBinding, KvRegistry, SecretRegistry, }; +use crate::time::{Deadline, MonotonicClock}; use serde::de::IntoDeserializer as _; #[async_trait(?Send)] @@ -35,7 +39,7 @@ where { #[inline] async fn from_request(ctx: &RequestContext) -> Result { - ctx.json().map(Json) + ctx.json_within(DEFAULT_INBOUND_JSON_BYTES).await.map(Json) } } @@ -79,6 +83,47 @@ where } } +/// Validated JSON extractor with a compile-time byte cap. +pub struct ValidatedJsonWithin(pub T); + +#[async_trait(?Send)] +impl FromRequest for ValidatedJsonWithin +where + T: DeserializeOwned + Validate + Send + 'static, +{ + #[inline] + async fn from_request(ctx: &RequestContext) -> Result { + let value: T = ctx.json_within(MAX).await?; + value + .validate() + .map_err(|err| EdgeError::validation(err.to_string()))?; + Ok(Self(value)) + } +} + +impl Deref for ValidatedJsonWithin { + type Target = T; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ValidatedJsonWithin { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl ValidatedJsonWithin { + #[inline] + pub fn into_inner(self) -> T { + self.0 + } +} + impl Deref for ValidatedJson { type Target = T; @@ -108,7 +153,7 @@ pub struct Headers(pub HeaderMap); impl FromRequest for Headers { #[inline] async fn from_request(ctx: &RequestContext) -> Result { - Ok(Headers(ctx.request().headers().clone())) + Ok(Headers(ctx.headers().clone())) } } @@ -153,7 +198,7 @@ pub struct Host(pub String); impl FromRequest for Host { #[inline] async fn from_request(ctx: &RequestContext) -> Result { - let headers = ctx.request().headers(); + let headers = ctx.headers(); let host = headers .get(header::HOST) .and_then(|value| value.to_str().ok()) @@ -202,7 +247,7 @@ pub struct ForwardedHost(pub String); impl FromRequest for ForwardedHost { #[inline] async fn from_request(ctx: &RequestContext) -> Result { - let headers = ctx.request().headers(); + let headers = ctx.headers(); let host = headers .get("x-forwarded-host") .or_else(|| headers.get(header::HOST)) @@ -391,7 +436,7 @@ where { #[inline] async fn from_request(ctx: &RequestContext) -> Result { - ctx.form().map(Form) + ctx.form_within(DEFAULT_INBOUND_FORM_BYTES).await.map(Form) } } @@ -458,6 +503,47 @@ impl ValidatedForm { } } +/// Validated form extractor with a compile-time byte cap. +pub struct ValidatedFormWithin(pub T); + +#[async_trait(?Send)] +impl FromRequest for ValidatedFormWithin +where + T: DeserializeOwned + Validate + Send + 'static, +{ + #[inline] + async fn from_request(ctx: &RequestContext) -> Result { + let value: T = ctx.form_within(MAX).await?; + value + .validate() + .map_err(|err| EdgeError::validation(err.to_string()))?; + Ok(Self(value)) + } +} + +impl Deref for ValidatedFormWithin { + type Target = T; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ValidatedFormWithin { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl ValidatedFormWithin { + #[inline] + pub fn into_inner(self) -> T { + self.0 + } +} + /// Extractor that yields the per-request [`KvRegistry`]. /// /// Handlers pick a bound store by id at the call site: @@ -493,8 +579,7 @@ impl FromRequest for Kv { // legacy bare-handle inputs to single-id registries at the // dispatch boundary, so this path no longer needs a // fallback — a missing registry is a real bug. - ctx.request() - .extensions() + ctx.extensions() .get::() .cloned() .map(Kv) @@ -615,8 +700,7 @@ impl FromRequest for Secrets { // Hard-cutoff: see `impl FromRequest for Kv`. Adapter // dispatchers normalise legacy bare-handle inputs to // single-id `SecretRegistry`s at the dispatch boundary. - ctx.request() - .extensions() + ctx.extensions() .get::() .cloned() .map(Secrets) @@ -672,8 +756,7 @@ impl FromRequest for Config { // Hard-cutoff: see `impl FromRequest for Kv`. Adapter // dispatchers normalise legacy bare-handle inputs to // single-id `ConfigRegistry`s at the dispatch boundary. - ctx.request() - .extensions() + ctx.extensions() .get::() .cloned() .map(Config) @@ -755,9 +838,11 @@ where #[inline] async fn from_request(ctx: &RequestContext) -> Result { let binding = ctx.config_store_default_binding().ok_or_else(|| { - EdgeError::internal(anyhow::anyhow!( - "no default config store registered \u{2014} check [stores.config] in edgezero.toml" - )) + EdgeError::store_extraction( + StoreExtractionReason::MissingRegistry, + "no default config store registered; check [stores.config] in edgezero.toml", + None, + ) })?; let key = binding.default_key.clone(); extract_from_handle::(ctx, &binding.handle, &key) @@ -783,9 +868,11 @@ where key: Option<&str>, ) -> Result { let binding = ctx.config_store_binding(store_id).ok_or_else(|| { - EdgeError::internal(anyhow::anyhow!( - "no config store registered for id `{store_id}`" - )) + EdgeError::store_extraction( + StoreExtractionReason::UnknownStore, + "no config store is registered for the requested id", + None, + ) })?; let resolved_key = key.unwrap_or(&binding.default_key).to_owned(); extract_from_handle::(ctx, &binding.handle, &resolved_key).await @@ -803,14 +890,303 @@ where #[inline] pub async fn named(ctx: &RequestContext, key: &str) -> Result { let binding = ctx.config_store_default_binding().ok_or_else(|| { - EdgeError::internal(anyhow::anyhow!( - "no default config store registered \u{2014} check [stores.config] in edgezero.toml" - )) + EdgeError::store_extraction( + StoreExtractionReason::MissingRegistry, + "no default config store registered; check [stores.config] in edgezero.toml", + None, + ) })?; extract_from_handle::(ctx, &binding.handle, key).await } } +/// Extraction-scoped accounting. One instance is created for each public +/// `AppConfig` call and shared by the root blob and all secret reads. +struct ConfigExtractionBudget { + clock: MonotonicClock, + deadline: Deadline, + max_blob_bytes: u64, + max_json_depth: u32, + max_json_nodes: u32, + max_secret_bytes: u64, + remaining_backend_bytes: u64, + remaining_total_bytes: u64, +} + +impl ConfigExtractionBudget { + fn accept_read( + &mut self, + backend_bytes: u64, + value_length: Option, + max_value_bytes: u64, + ) -> Result<(), EdgeError> { + self.check_deadline()?; + + let converted_value_bytes = value_length + .map(u64::try_from) + .transpose() + .map_err(|_length_error| backend_contract_error())?; + if backend_bytes > self.remaining_backend_bytes + || converted_value_bytes + .is_some_and(|bytes| bytes > max_value_bytes || backend_bytes < bytes) + { + return Err(backend_contract_error()); + } + + self.remaining_backend_bytes = self + .remaining_backend_bytes + .checked_sub(backend_bytes) + .ok_or_else(backend_contract_error)?; + if let Some(retained_bytes) = converted_value_bytes { + self.remaining_total_bytes = self + .remaining_total_bytes + .checked_sub(retained_bytes) + .ok_or_else(|| { + store_extraction_error( + StoreExtractionReason::ValueTooLarge, + "typed app-config extraction exceeded its cumulative byte limit", + None, + ) + })?; + } + Ok(()) + } + + fn check_deadline(&self) -> Result<(), EdgeError> { + if self.deadline.is_expired_at(self.clock.now()) { + return Err(store_extraction_error( + StoreExtractionReason::DeadlineExceeded, + "typed app-config extraction deadline exceeded", + None, + )); + } + Ok(()) + } + + fn clock(&self) -> &MonotonicClock { + &self.clock + } + + fn deadline(&self) -> Deadline { + self.deadline + } + + fn max_blob_bytes(&self) -> u64 { + self.max_blob_bytes + } + + fn max_secret_bytes(&self) -> u64 { + self.max_secret_bytes + } + + fn remaining_backend_bytes(&self) -> u64 { + self.remaining_backend_bytes + } + + fn start( + configured_limits: ConfigExtractionLimits, + clock: MonotonicClock, + ) -> Result { + let validated_limits = configured_limits.validate()?; + let started_at = clock.now(); + let deadline = started_at + .checked_add(validated_limits.timeout) + .ok_or_else(|| { + EdgeError::store_extraction( + StoreExtractionReason::BackendFailure, + "config extraction deadline could not be represented", + None, + ) + })?; + Ok(Self { + clock, + deadline: Deadline::at_instant(deadline), + max_blob_bytes: validated_limits.max_blob_bytes, + max_json_depth: validated_limits.max_json_depth, + max_json_nodes: validated_limits.max_json_nodes, + max_secret_bytes: validated_limits.max_secret_bytes, + remaining_backend_bytes: validated_limits.max_backend_bytes, + remaining_total_bytes: validated_limits.max_total_bytes, + }) + } +} + +struct JsonStructureBudget { + exceeded: bool, + max_depth: u32, + max_nodes: u32, + nodes: u32, +} + +struct JsonStructureSeed<'budget> { + budget: &'budget mut JsonStructureBudget, + depth: u32, +} + +impl<'de> DeserializeSeed<'de> for JsonStructureSeed<'_> { + type Value = (); + + fn deserialize(self, deserializer: D) -> Result<(), D::Error> + where + D: de::Deserializer<'de>, + { + if self.depth > self.budget.max_depth || self.budget.nodes >= self.budget.max_nodes { + self.budget.exceeded = true; + return Err(de::Error::custom("config JSON structure limit exceeded")); + } + self.budget.nodes = self.budget.nodes.saturating_add(1); + deserializer.deserialize_any(JsonStructureVisitor { + budget: self.budget, + depth: self.depth, + }) + } +} + +struct JsonStructureVisitor<'budget> { + budget: &'budget mut JsonStructureBudget, + depth: u32, +} + +#[expect( + clippy::missing_trait_methods, + reason = "serde_json only calls the JSON value visitor methods implemented below" +)] +impl<'de> Visitor<'de> for JsonStructureVisitor<'_> { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a JSON value") + } + + fn visit_bool(self, _value: bool) -> Result<(), E> + where + E: de::Error, + { + Ok(()) + } + + fn visit_f64(self, _value: f64) -> Result<(), E> + where + E: de::Error, + { + Ok(()) + } + + fn visit_i64(self, _value: i64) -> Result<(), E> + where + E: de::Error, + { + Ok(()) + } + + fn visit_map(self, mut map: A) -> Result<(), A::Error> + where + A: MapAccess<'de>, + { + while map.next_key::()?.is_some() { + map.next_value_seed(JsonStructureSeed { + budget: self.budget, + depth: self.depth.saturating_add(1), + })?; + } + Ok(()) + } + + fn visit_none(self) -> Result<(), E> + where + E: de::Error, + { + Ok(()) + } + + fn visit_seq(self, mut seq: A) -> Result<(), A::Error> + where + A: SeqAccess<'de>, + { + while seq + .next_element_seed(JsonStructureSeed { + budget: self.budget, + depth: self.depth.saturating_add(1), + })? + .is_some() + {} + Ok(()) + } + + fn visit_str(self, _value: &str) -> Result<(), E> + where + E: de::Error, + { + Ok(()) + } + + fn visit_string(self, _value: String) -> Result<(), E> + where + E: de::Error, + { + Ok(()) + } + + fn visit_u64(self, _value: u64) -> Result<(), E> + where + E: de::Error, + { + Ok(()) + } + + fn visit_unit(self) -> Result<(), E> + where + E: de::Error, + { + Ok(()) + } +} + +fn backend_contract_error() -> EdgeError { + store_extraction_error( + StoreExtractionReason::BackendFailure, + "a store backend violated the bounded-read contract", + None, + ) +} + +fn store_extraction_error( + reason: StoreExtractionReason, + message: impl Into, + field_path: Option, +) -> EdgeError { + EdgeError::store_extraction(reason, message, field_path) +} + +fn check_json_structure(raw: &str, max_depth: u32, max_nodes: u32) -> Result<(), EdgeError> { + let mut budget = JsonStructureBudget { + exceeded: false, + max_depth, + max_nodes, + nodes: 0, + }; + let mut deserializer = serde_json::Deserializer::from_str(raw); + let result = JsonStructureSeed { + budget: &mut budget, + depth: 1, + } + .deserialize(&mut deserializer) + .and_then(|()| deserializer.end()); + match result { + Ok(()) => Ok(()), + Err(_) if budget.exceeded => Err(store_extraction_error( + StoreExtractionReason::StructuralLimit, + "typed app-config blob exceeded its JSON structure limit", + None, + )), + Err(_) => Err(store_extraction_error( + StoreExtractionReason::MalformedEnvelope, + "typed app-config blob is not a valid envelope (details redacted)", + None, + )), + } +} + /// A redacted reason when `raw` is a NEWER format this build must not apply, or /// `None` when it is not (that stays ordinary corruption). /// @@ -855,14 +1231,8 @@ fn future_format_reason(raw: &str) -> Option { /// /// # Errors /// -/// - [`EdgeError::ConfigOutOfDate`] — missing blob, missing secret key, -/// deserialise failure, or validation failure on a non-secret field. -/// - [`EdgeError::Internal`] — envelope parse failure or SHA mismatch -/// (envelope integrity failures indicate a corrupt/tampered store entry, -/// not a stale config — they surface as 500 Internal). -/// - [`EdgeError::ServiceUnavailable`] — config-store backend temporarily down -/// (`ConfigStoreError::Unavailable`). -/// - [`EdgeError::BadRequest`] — malformed key (`ConfigStoreError::InvalidKey`). +/// Returns [`EdgeError::StoreExtraction`] with the stable reason corresponding +/// to store, envelope, secret, or schema failure. async fn extract_from_handle( ctx: &RequestContext, handle: &ConfigStoreHandle, @@ -871,107 +1241,137 @@ async fn extract_from_handle( where C: DeserializeOwned + AppConfigMeta + Validate + Send + 'static, { - // ConfigStoreError → EdgeError uses the existing `impl - // From for EdgeError` at - // `crates/edgezero-core/src/error.rs`, which maps: - // Unavailable → ServiceUnavailable (503) - // InvalidKey → BadRequest (400) - // Internal → Internal (500) - // NEVER `map_err(EdgeError::internal)` here — that collapses - // backpressure / bad-key signals into 500s. - let raw = handle - .get(key) + let mut budget = + ConfigExtractionBudget::start(ctx.config_extraction_limits(), ctx.monotonic_clock())?; + let read = handle + .get_bounded( + key, + budget.clock(), + budget.deadline(), + budget.remaining_backend_bytes(), + budget.max_blob_bytes(), + ) .await - .map_err(EdgeError::from)? - .ok_or_else(|| { - EdgeError::config_out_of_date( + .map_err(|store_error| map_config_store_error(&store_error))?; + budget.accept_read( + read.backend_bytes, + read.value.as_ref().map(|value| value.len()), + budget.max_blob_bytes(), + )?; + let raw = read.value.ok_or_else(|| { + store_extraction_error( + StoreExtractionReason::MissingBlob, + format!( + "missing typed app-config blob at key `{key}`; run ` config push` for this deploy" + ), + None, + ) + })?; + + // Neither parsing nor verification diagnostics may echo stored values. + let parsed = (|| { + check_json_structure(&raw, budget.max_json_depth, budget.max_json_nodes)?; + if let Some(reason) = future_format_reason(&raw) { + return Err(store_extraction_error( + StoreExtractionReason::UnsupportedVersion, format!( - "missing typed app-config blob at key `{key}` — \ - run ` config push` for this deploy" + "typed app-config blob uses {reason}, which this build does not understand; redeploy this service with an updated build" ), - String::new(), + None, + )); + } + let envelope: BlobEnvelope = serde_json::from_str(&raw).map_err(|_err| { + store_extraction_error( + StoreExtractionReason::MalformedEnvelope, + "typed app-config blob is not a valid envelope (details redacted)", + None, ) })?; - // Neither the parse error nor the verify error is echoed into the - // client-facing message: a serde error embeds the offending input, and a - // `BlobEnvelope` integrity failure names the stored hashes — both are - // config-store values that may hold secrets, and this message reaches the - // HTTP body. Report a category only. - // - // A value written by a NEWER format (a bumped envelope version OR an unknown - // `edgezero_kind` discriminator) needs a DIFFERENT remediation than - // corruption: re-pushing the same config cannot help a build older than its - // config; the deployed build must be UPGRADED. The version case is already - // caught by `verify()` below (`UnknownVersion`); the two it does NOT catch are - // an `edgezero_kind` on a v1-shaped value (serde ignores the unknown field) - // and a schema-changed envelope that fails the v1 deserialize outright. A - // cheap substring gate keeps the second full JSON parse (`future_format_reason`, - // which reparses the whole blob as a `Value`) OFF the happy path -- this runs - // per request in the edge guest, over a blob that is by definition large - // wherever chunking matters. The reason names only the version/field, never a - // value, so surfacing it is safe. - let kind_tagged = raw.contains("edgezero_kind"); - let envelope: BlobEnvelope = match serde_json::from_str::(&raw) { - Ok(envelope) if !kind_tagged => envelope, - parsed => { - if let Some(reason) = future_format_reason(&raw) { - return Err(EdgeError::internal(anyhow::anyhow!( - "typed app-config blob uses {reason}, which this build does not understand; \ - redeploy this service with an updated build (re-pushing will not help)" - ))); - } - parsed.map_err(|_err| { - EdgeError::internal(anyhow::anyhow!( - "typed app-config blob is not a valid envelope (details redacted)" - )) - })? - } - }; - envelope.verify().map_err(|err| match err { - BlobEnvelopeError::UnknownVersion(version) => EdgeError::internal(anyhow::anyhow!( - "typed app-config blob uses envelope version {version}, which this build does not \ - understand; redeploy this service with an updated build (re-pushing will not help)" - )), - BlobEnvelopeError::ShaMismatch { .. } => EdgeError::internal(anyhow::anyhow!( - "typed app-config blob failed its integrity check (details redacted)" - )), - })?; - let mut data = envelope.into_data(); - // Secret walk per spec 3.3.3. - secret_walk::(ctx, &mut data).await?; - // Deserialise via serde_path_to_error so failures carry a dotted - // field path for ConfigOutOfDate per spec 4.3. - let cfg: C = serde_path_to_error::deserialize(data.into_deserializer()) - .map_err(|err| EdgeError::config_out_of_date_from_serde(&err))?; - // RUNTIME uses cfg.validate(): after secret_walk the fields hold - // RESOLVED values, so every validator rule — including those on - // secret fields (e.g. length/regex on the actual token value) — - // MUST run. Spec 3.3.8 split: PUSH skips secret-field validators - // (via validate_excluding_secrets) because the value at push time - // is a key NAME; RUNTIME runs cfg.validate() because the value is - // now the resolved secret. - cfg.validate().map_err(|err| { - // SECURITY: `secret_walk` has replaced `#[secret]` fields with their - // RESOLVED values, and `validator`'s error params echo the rejected - // value — so `err.to_string()` here can leak a secret into the HTTP - // response body and logs. Keep the field path (structural, not secret) - // but drop the formatted validator details on this runtime path. - let field = first_violating_field(&err).unwrap_or_default(); - let message = if field.is_empty() { - "app config failed validation".to_owned() - } else { - format!("app config failed validation for field `{field}`") - }; - EdgeError::config_out_of_date(message, field) - })?; - Ok(cfg) + envelope.verify().map_err(|err| match err { + BlobEnvelopeError::UnknownVersion(version) => store_extraction_error( + StoreExtractionReason::UnsupportedVersion, + format!( + "typed app-config blob uses envelope version {version}, which this build does not understand; redeploy this service with an updated build" + ), + None, + ), + BlobEnvelopeError::ShaMismatch { .. } => store_extraction_error( + StoreExtractionReason::IntegrityMismatch, + "typed app-config blob failed its integrity check (details redacted)", + None, + ), + })?; + Ok::<_, EdgeError>(envelope.into_data()) + })(); + budget.check_deadline()?; + let mut data = parsed?; + drop(raw); + + let resolved = secret_walk::(ctx, &mut budget, &mut data).await; + budget.check_deadline()?; + resolved?; + + let configured = (|| { + let cfg: C = serde_path_to_error::deserialize(data.into_deserializer()) + .map_err(|serde_error| EdgeError::store_deserialization_from_serde(&serde_error))?; + cfg.validate().map_err(|err| { + let field = first_violating_field(&err).unwrap_or_default(); + let message = if field.is_empty() { + "app config failed validation".to_owned() + } else { + format!("app config failed validation for field `{field}`") + }; + store_extraction_error( + StoreExtractionReason::Validation, + message, + (!field.is_empty()).then_some(field), + ) + })?; + Ok::<_, EdgeError>(cfg) + })(); + budget.check_deadline()?; + configured +} + +fn map_config_store_error(err: &ConfigStoreError) -> EdgeError { + match err { + ConfigStoreError::DeadlineExceeded => store_extraction_error( + StoreExtractionReason::DeadlineExceeded, + "typed app-config store read deadline exceeded", + None, + ), + ConfigStoreError::Internal { .. } => store_extraction_error( + StoreExtractionReason::BackendFailure, + "typed app-config store read failed (details redacted)", + None, + ), + ConfigStoreError::InvalidKey { .. } => store_extraction_error( + StoreExtractionReason::InvalidKey, + "typed app-config store rejected the requested key (details redacted)", + None, + ), + ConfigStoreError::Unavailable { .. } => store_extraction_error( + StoreExtractionReason::BackendUnavailable, + "typed app-config store is unavailable", + None, + ), + ConfigStoreError::ValueTooLarge => store_extraction_error( + StoreExtractionReason::ValueTooLarge, + "typed app-config store value exceeded its read limit", + None, + ), + } } /// Walk `C::secret_fields()` and replace each `#[secret]` key NAME in `data` /// with the resolved secret VALUE from the appropriate secret store. /// /// `StoreRef` fields are skipped — their value is a store id, not a key. -async fn secret_walk(ctx: &RequestContext, data: &mut serde_json::Value) -> Result<(), EdgeError> +async fn secret_walk( + ctx: &RequestContext, + budget: &mut ConfigExtractionBudget, + data: &mut serde_json::Value, +) -> Result<(), EdgeError> where C: AppConfigMeta, { @@ -982,7 +1382,7 @@ where if matches!(field.kind, SecretKind::StoreRef) { continue; } - resolve_secret_field(ctx, data, &field, &field.path, String::new()).await?; + resolve_secret_field(ctx, budget, data, &field, &field.path, String::new()).await?; } Ok(()) } @@ -992,6 +1392,7 @@ where /// indices) for error hints. fn resolve_secret_field<'walk>( ctx: &'walk RequestContext, + budget: &'walk mut ConfigExtractionBudget, node: &'walk mut serde_json::Value, field: &'walk SecretField, remaining: &'walk [SecretPathSegment], @@ -1001,7 +1402,7 @@ fn resolve_secret_field<'walk>( match remaining.split_first() { // Leaf reached: `node` is the PARENT object; the last field is the key. Some((SecretPathSegment::Field(name), [])) => { - resolve_leaf(ctx, node, field, name.as_ref(), &rendered).await + resolve_leaf(ctx, budget, node, field, name.as_ref(), &rendered).await } Some((SecretPathSegment::OptionalField(name), [])) => { if node.as_object().is_some_and(|parent| { @@ -1012,7 +1413,7 @@ fn resolve_secret_field<'walk>( }) { return Ok(()); } - resolve_leaf(ctx, node, field, name.as_ref(), &rendered).await + resolve_leaf(ctx, budget, node, field, name.as_ref(), &rendered).await } // Required intermediates still reject stale blobs. Optional // intermediates are represented explicitly below rather than by the @@ -1020,27 +1421,29 @@ fn resolve_secret_field<'walk>( Some((SecretPathSegment::Field(name), rest)) => { let next_rendered = join_field(&rendered, name.as_ref()); match node.get_mut(name.as_ref()) { - None | Some(serde_json::Value::Null) => Err(EdgeError::config_out_of_date( + None | Some(serde_json::Value::Null) => Err(store_extraction_error( + StoreExtractionReason::Deserialization, format!("missing or null value at `{next_rendered}`"), - next_rendered, + Some(next_rendered), )), Some(child) => { - resolve_secret_field(ctx, child, field, rest, next_rendered).await + resolve_secret_field(ctx, budget, child, field, rest, next_rendered).await } } } Some((SecretPathSegment::OptionalField(name), rest)) => { let Some(parent) = node.as_object_mut() else { - return Err(EdgeError::config_out_of_date( + return Err(store_extraction_error( + StoreExtractionReason::Deserialization, format!("expected an object at `{rendered}`"), - rendered, + Some(rendered), )); }; let next_rendered = join_field(&rendered, name.as_ref()); match parent.get_mut(name.as_ref()) { None | Some(serde_json::Value::Null) => Ok(()), Some(child) => { - resolve_secret_field(ctx, child, field, rest, next_rendered).await + resolve_secret_field(ctx, budget, child, field, rest, next_rendered).await } } } @@ -1048,14 +1451,15 @@ fn resolve_secret_field<'walk>( // intermediate unless its containing field was optional above. Some((SecretPathSegment::ArrayEach, rest)) => { let Some(items) = node.as_array_mut() else { - return Err(EdgeError::config_out_of_date( + return Err(store_extraction_error( + StoreExtractionReason::Deserialization, format!("expected an array at `{rendered}`"), - rendered, + Some(rendered), )); }; for (idx, item) in items.iter_mut().enumerate() { let indexed = format!("{rendered}[{idx}]"); - resolve_secret_field(ctx, item, field, rest, indexed).await?; + resolve_secret_field(ctx, budget, item, field, rest, indexed).await?; } Ok(()) } @@ -1077,6 +1481,7 @@ fn join_field(prefix: &str, name: &str) -> String { /// within `parent`. async fn resolve_leaf( ctx: &RequestContext, + budget: &mut ConfigExtractionBudget, parent: &mut serde_json::Value, field: &SecretField, key: &str, @@ -1089,9 +1494,10 @@ async fn resolve_leaf( let leaf_path = join_field(rendered_parent, key); let Some(parent_obj) = parent.as_object_mut() else { - return Err(EdgeError::config_out_of_date( + return Err(store_extraction_error( + StoreExtractionReason::Deserialization, format!("expected an object containing `{key}` at `{rendered_parent}`"), - leaf_path, + Some(leaf_path), )); }; @@ -1103,9 +1509,10 @@ async fn resolve_leaf( // cases must skip — not just the missing-key case. None | Some(serde_json::Value::Null) if field.optional => return Ok(()), _ => { - return Err(EdgeError::config_out_of_date( + return Err(store_extraction_error( + StoreExtractionReason::Deserialization, format!("missing or non-string value at `{leaf_path}`"), - leaf_path, + Some(leaf_path), )); } }; @@ -1113,12 +1520,13 @@ async fn resolve_leaf( let (bound, resolved_store_id) = match field.kind { SecretKind::KeyInDefault => { let bound = ctx.secret_store_default().ok_or_else(|| { - EdgeError::config_out_of_date( + store_extraction_error( + StoreExtractionReason::MissingRegistry, format!( "secret field `{leaf_path}` has kind KeyInDefault but no default secret \ store is registered" ), - leaf_path.clone(), + Some(leaf_path.clone()), ) })?; let id = bound.store_name().to_owned(); @@ -1130,11 +1538,12 @@ async fn resolve_leaf( .get(store_ref_field) .and_then(|val| val.as_str()) .ok_or_else(|| { - EdgeError::config_out_of_date( + store_extraction_error( + StoreExtractionReason::Deserialization, format!( "missing store_ref `{store_ref_field}` for secret field `{leaf_path}`" ), - leaf_path.clone(), + Some(leaf_path.clone()), ) })? .to_owned(); @@ -1142,22 +1551,50 @@ async fn resolve_leaf( // `store_id_str` is the blob's store_ref VALUE — config data that // may be sensitive — and this message reaches the HTTP body. Name // the field, not the stored id. - EdgeError::config_out_of_date( + store_extraction_error( + StoreExtractionReason::UnknownStore, format!( "secret field `{leaf_path}` names a store_ref that is not declared in \ [stores.secrets] (id redacted)" ), - leaf_path.clone(), + Some(leaf_path.clone()), ) })?; (bound, store_id_str) } }; - let secret = bound - .require_str(&key_name) + let read = bound + .get_bytes_bounded( + &key_name, + budget.clock(), + budget.deadline(), + budget.remaining_backend_bytes(), + budget.max_secret_bytes(), + ) .await .map_err(|err| map_secret_error(err, &leaf_path, &resolved_store_id, &key_name))?; + budget.accept_read( + read.backend_bytes, + read.value.as_ref().map(bytes::Bytes::len), + budget.max_secret_bytes(), + )?; + let secret_bytes = read.value.ok_or_else(|| { + store_extraction_error( + StoreExtractionReason::MissingSecret, + format!( + "the secret referenced by `{leaf_path}` was not found in its store (identifier redacted)" + ), + Some(leaf_path.clone()), + ) + })?; + let secret = String::from_utf8(secret_bytes.to_vec()).map_err(|_utf8_error| { + store_extraction_error( + StoreExtractionReason::InvalidSecretValue, + format!("secret for field `{leaf_path}` is not valid UTF-8"), + Some(leaf_path.clone()), + ) + })?; parent_obj.insert(key.to_owned(), serde_json::Value::String(secret)); Ok(()) } @@ -1175,24 +1612,40 @@ fn map_secret_error( _key_name: &str, ) -> EdgeError { match err { - SecretError::NotFound { .. } => EdgeError::config_out_of_date( + SecretError::DeadlineExceeded => EdgeError::store_extraction( + StoreExtractionReason::DeadlineExceeded, + format!("secret resolution for `{field_name}` exceeded its deadline"), + Some(field_name.to_owned()), + ), + SecretError::Internal(_source) => EdgeError::store_extraction( + StoreExtractionReason::BackendFailure, + format!("secret resolution for `{field_name}` failed (details redacted)"), + Some(field_name.to_owned()), + ), + SecretError::NotFound { .. } => EdgeError::store_extraction( + StoreExtractionReason::MissingSecret, format!( "the secret referenced by `{field_name}` was not found in its store (identifier redacted)" ), - field_name.to_owned(), + Some(field_name.to_owned()), + ), + SecretError::Unavailable => EdgeError::store_extraction( + StoreExtractionReason::SecretBackendUnavailable, + format!("the secret store for `{field_name}` is unreachable"), + Some(field_name.to_owned()), ), - SecretError::Validation(_msg) => EdgeError::config_out_of_date( + SecretError::Validation(_msg) => EdgeError::store_extraction( + StoreExtractionReason::InvalidKey, format!( "the secret referenced by `{field_name}` was rejected by its store (details redacted)" ), - field_name.to_owned(), + Some(field_name.to_owned()), + ), + SecretError::ValueTooLarge => EdgeError::store_extraction( + StoreExtractionReason::ValueTooLarge, + format!("the secret referenced by `{field_name}` exceeds its configured byte limit"), + Some(field_name.to_owned()), ), - SecretError::Unavailable => EdgeError::service_unavailable(format!( - "the secret store for `{field_name}` is unreachable" - )), - SecretError::Internal(_source) => EdgeError::internal(anyhow::anyhow!( - "secret resolution for `{field_name}` failed (details redacted)" - )), } } @@ -1258,17 +1711,20 @@ mod tests { use crate::app_config::{AppConfigMeta, SecretField, SecretKind, SecretPathSegment}; use crate::blob_envelope::BlobEnvelope; use crate::body::Body; - use crate::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; + use crate::config_store::{ + ConfigStore, ConfigStoreError, ConfigStoreHandle, ready_config_store_bounded_read, + }; use crate::context::RequestContext; use crate::http::{HeaderValue, Method, StatusCode, request_builder}; use crate::params::PathParams; use crate::secret_store::{InMemorySecretStore, NoopSecretStore, SecretHandle, SecretStore}; use crate::store_registry::StoreRegistry; + use crate::time::{MonotonicClock, MonotonicInstant}; use futures::executor::block_on; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::collections::HashMap; - use std::sync::Arc; + use std::sync::{Arc, Mutex}; use validator::Validate; #[derive(Clone, Debug, PartialEq)] @@ -1488,6 +1944,55 @@ mod tests { PathParams::new(map) } + #[test] + fn body_extractors_preserve_overflow_category_at_every_boundary() { + for extractor in 0_u8..6 { + let cap = match extractor { + 0 | 1 => DEFAULT_INBOUND_JSON_BYTES, + 3 | 4 => DEFAULT_INBOUND_FORM_BYTES, + _ => 16, + }; + for length in [cap - 1, cap, cap + 1] { + let body = if extractor <= 2 { + let mut bytes = + serde_json::to_vec(&serde_json::json!({"name": "demo"})).expect("JSON"); + bytes.resize(length, b' '); + Body::from(bytes) + } else { + Body::from(format!("username={}", "x".repeat(length - 9))) + }; + let ctx = ctx(body, PathParams::default()); + let result = + match extractor { + 0 => block_on(Json::::from_request(&ctx)).map(|_| ()), + 1 => block_on(ValidatedJson::::from_request(&ctx)) + .map(|_| ()), + 2 => block_on(ValidatedJsonWithin::::from_request( + &ctx, + )) + .map(|_| ()), + 3 => block_on(Form::::from_request(&ctx)).map(|_| ()), + 4 => block_on(ValidatedForm::::from_request(&ctx)) + .map(|_| ()), + _ => block_on(ValidatedFormWithin::::from_request( + &ctx, + )) + .map(|_| ()), + }; + if length > cap { + let error = result.expect_err("overflow"); + assert_eq!(error.kind(), "payload_too_large"); + assert_eq!( + error.request_body_overflow_origin(), + Some(crate::RequestBodyOverflowOrigin::Inbound) + ); + } else { + result.expect("within cap"); + } + } + } + } + #[test] fn json_extractor_parses_payload() { let body = Body::json(&Payload { @@ -1531,8 +2036,7 @@ mod tests { #[test] fn headers_extractor_clones_request_headers() { let mut ctx = ctx(Body::empty(), PathParams::default()); - ctx.request_mut() - .headers_mut() + ctx.headers_mut() .insert("x-test", HeaderValue::from_static("value")); let headers = block_on(Headers::from_request(&ctx)).expect("headers"); assert_eq!( @@ -2079,9 +2583,14 @@ mod tests { struct FixedStore(&'static str); #[async_trait(?Send)] impl ConfigStore for FixedStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.to_owned())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.into())) } + + ready_config_store_bounded_read!(); } let registry: ConfigRegistry = StoreRegistry::new( @@ -2119,7 +2628,7 @@ mod tests { let analytics = config.named("analytics").expect("analytics handle"); assert_eq!( block_on(analytics.get("any")).expect("config value"), - Some("analytics".to_owned()) + Some(crate::ConfigValue::from("analytics")) ); assert!(config.named("missing").is_none()); assert!(config.default().is_some()); @@ -2135,9 +2644,14 @@ mod tests { struct AnyStore; #[async_trait(?Send)] impl ConfigStore for AnyStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some("legacy".to_owned())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some("legacy".into())) } + + ready_config_store_bounded_read!(); } let mut request = request_builder() @@ -2181,9 +2695,14 @@ mod tests { struct AnyStore; #[async_trait(?Send)] impl ConfigStore for AnyStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Ok(None) } + + ready_config_store_bounded_read!(); } let binding = ConfigStoreBinding { @@ -2230,9 +2749,14 @@ mod tests { struct AnyStore; #[async_trait(?Send)] impl ConfigStore for AnyStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Ok(None) } + + ready_config_store_bounded_read!(); } let registry: ConfigRegistry = StoreRegistry::new( @@ -2337,14 +2861,67 @@ mod tests { serde_json::to_string(&envelope).expect("serialise envelope") } + async fn test_secret_walk( + ctx: &RequestContext, + data: &mut serde_json::Value, + ) -> Result<(), EdgeError> + where + C: AppConfigMeta, + { + let mut budget = + ConfigExtractionBudget::start(ConfigExtractionLimits::default(), ctx.monotonic_clock()) + .expect("test budget"); + secret_walk::(ctx, &mut budget, data).await + } + + #[test] + fn config_extraction_budget_uses_the_request_clock() { + let start = MonotonicInstant::now(); + let observed = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&observed); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let limits = ConfigExtractionLimits::default(); + let deadline = start.checked_add(limits.timeout).expect("deadline"); + let mut budget = ConfigExtractionBudget::start(limits, clock).expect("test budget"); + *observed.lock().expect("clock lock") = deadline; + + let error = budget + .accept_read(0, Some(0), 1) + .expect_err("injected clock reached deadline"); + + assert!(matches!( + error, + EdgeError::StoreExtraction { + reason: StoreExtractionReason::DeadlineExceeded, + .. + } + )); + + let deadline_error = budget + .check_deadline() + .expect_err("CPU work also observes the same deadline"); + assert!(matches!( + deadline_error, + EdgeError::StoreExtraction { + reason: StoreExtractionReason::DeadlineExceeded, + .. + } + )); + } + #[test] fn app_config_extractor_happy_path() { struct FixedStore(String); #[async_trait(?Send)] impl ConfigStore for FixedStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.clone())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.clone().into())) } + + ready_config_store_bounded_read!(); } let data = serde_json::json!({ "greeting": "hello", "timeout_ms": 500_u32 }); @@ -2357,21 +2934,201 @@ mod tests { } #[test] - fn app_config_extractor_returns_config_out_of_date_on_missing_blob() { + fn app_config_extractor_uses_bounded_root_read() { + use std::sync::Mutex; + + use crate::config_store::{BoundedStoreRead, ConfigExtractionLimits}; + use crate::time::Deadline; + + struct BoundedOnlyStore { + blob: String, + observed: Arc>>, + } + + #[async_trait(?Send)] + impl ConfigStore for BoundedOnlyStore { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + panic!("typed extraction must not call the unbounded config-store API"); + } + + async fn get_bounded( + &self, + _key: &str, + _clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + *self.observed.lock().expect("record bounded read") = + Some((deadline, max_backend_bytes, max_value_bytes)); + Ok(BoundedStoreRead { + backend_bytes: u64::try_from(self.blob.len()).expect("fixture length"), + value: Some(self.blob.clone().into()), + }) + } + } + + let observed = Arc::new(Mutex::new(None)); + let blob = + make_envelope(serde_json::json!({ "greeting": "bounded", "timeout_ms": 500_u32 })); + let ctx = ctx_with_config_store( + BoundedOnlyStore { + blob, + observed: Arc::clone(&observed), + }, + "the_key", + ); + + let AppConfig(cfg) = + block_on(AppConfig::::from_request(&ctx)).expect("bounded extraction"); + assert_eq!(cfg.greeting, "bounded"); + + let (_, max_backend_bytes, max_value_bytes) = observed + .lock() + .expect("read observation") + .expect("bounded read called"); + let limits = ConfigExtractionLimits::default(); + assert_eq!(max_backend_bytes, limits.max_backend_bytes); + assert_eq!(max_value_bytes, limits.max_blob_bytes); + } + + #[test] + fn app_config_extractor_shares_deadline_and_budget_with_secret_reads() { + use crate::ConfigValue; + use crate::config_store::BoundedStoreRead; + use crate::time::{Deadline, MonotonicInstant}; + use bytes::Bytes; + use std::sync::Mutex; + + type Observation = (MonotonicInstant, u64, u64); + + struct RecordingConfigStore { + blob: String, + observed: Arc>>, + } + + #[async_trait(?Send)] + impl ConfigStore for RecordingConfigStore { + async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + panic!("typed extraction must not call unbounded config reads"); + } + + async fn get_bounded( + &self, + _key: &str, + _clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + self.observed.lock().expect("config observation").push(( + deadline.instant(), + max_backend_bytes, + max_value_bytes, + )); + Ok(BoundedStoreRead { + backend_bytes: u64::try_from(self.blob.len()).expect("fixture length"), + value: Some(self.blob.clone().into()), + }) + } + } + + struct RecordingSecretStore { + observed: Arc>>, + } + + #[async_trait(?Send)] + impl SecretStore for RecordingSecretStore { + async fn get_bytes( + &self, + _store_name: &str, + _key: &str, + ) -> Result, SecretError> { + panic!("typed extraction must not call unbounded secret reads"); + } + + async fn get_bytes_bounded( + &self, + _store_name: &str, + _key: &str, + _clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError> { + self.observed.lock().expect("secret observation").push(( + deadline.instant(), + max_backend_bytes, + max_value_bytes, + )); + Ok(BoundedStoreRead { + backend_bytes: 6, + value: Some(Bytes::from_static(b"secret")), + }) + } + } + + let observed = Arc::new(Mutex::new(Vec::new())); + let blob = + make_envelope(serde_json::json!({ "greeting": "bounded", "api_token": "token-key" })); + let blob_bytes = u64::try_from(blob.len()).expect("fixture length"); + let ctx = ctx_with_config_and_secrets( + RecordingConfigStore { + blob, + observed: Arc::clone(&observed), + }, + "the_key", + RecordingSecretStore { + observed: Arc::clone(&observed), + }, + "vault", + ); + + let AppConfig(cfg) = + block_on(AppConfig::::from_request(&ctx)).expect("bounded extraction"); + assert_eq!(cfg.api_token, "secret"); + + let locked_observations = observed.lock().expect("observations"); + assert_eq!(locked_observations.len(), 2); + assert_eq!( + locked_observations[0].0, locked_observations[1].0, + "deadline must not reset" + ); + assert_eq!( + locked_observations[1].1, + locked_observations[0].1 - blob_bytes, + "secret read receives the remaining backend allowance" + ); + assert_eq!( + locked_observations[1].2, + ConfigExtractionLimits::default().max_secret_bytes + ); + } + + #[test] + fn app_config_extractor_returns_typed_missing_blob() { struct EmptyStore; #[async_trait(?Send)] impl ConfigStore for EmptyStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Ok(None) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(EmptyStore, "the_key"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("missing blob must error"); - assert!( - matches!(err, EdgeError::ConfigOutOfDate { .. }), - "expected ConfigOutOfDate, got {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::MissingBlob) ); assert!( err.message().contains("missing typed app-config blob"), @@ -2384,69 +3141,87 @@ mod tests { } #[test] - fn app_config_extractor_maps_config_store_unavailable_to_service_unavailable() { + fn app_config_extractor_maps_config_store_unavailable() { struct DownStore; #[async_trait(?Send)] impl ConfigStore for DownStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Err(ConfigStoreError::unavailable("backend offline")) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(DownStore, "the_key"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("unavailable store must error"); - assert!( - matches!(err, EdgeError::ServiceUnavailable { .. }), - "ConfigStoreError::Unavailable must map to ServiceUnavailable (not Internal): {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::BackendUnavailable) ); } #[test] - fn app_config_extractor_maps_config_store_invalid_key_to_bad_request() { + fn app_config_extractor_maps_config_store_invalid_key() { struct BadKeyStore; #[async_trait(?Send)] impl ConfigStore for BadKeyStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Err(ConfigStoreError::invalid_key("key is malformed")) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(BadKeyStore, "the_key"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("invalid key must error"); - assert!( - matches!(err, EdgeError::BadRequest { .. }), - "ConfigStoreError::InvalidKey must map to BadRequest (not Internal): {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::InvalidKey) ); } #[test] - fn app_config_extractor_maps_config_store_internal_to_internal() { + fn app_config_extractor_maps_config_store_internal() { struct BrokenStore; #[async_trait(?Send)] impl ConfigStore for BrokenStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Err(ConfigStoreError::internal(anyhow::anyhow!("disk on fire"))) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(BrokenStore, "the_key"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("internal store error must error"); - assert!( - matches!(err, EdgeError::Internal { .. }), - "ConfigStoreError::Internal must map to Internal: {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::BackendFailure) ); } #[test] - fn app_config_extractor_returns_internal_on_sha_mismatch() { + fn app_config_extractor_returns_integrity_mismatch_on_sha_mismatch() { const SENTINEL: &str = "SUPER_SECRET_STORED_HASH"; struct TamperedStore; #[async_trait(?Send)] impl ConfigStore for TamperedStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { // A valid envelope whose stored sha is a SENTINEL secret. The // stored hash is attacker-influenced (it comes from the config // store), and this error becomes the HTTP 500 body — so the @@ -2456,17 +3231,18 @@ mod tests { "2026-01-01T00:00:00Z".into(), ); env.sha256 = SENTINEL.to_owned(); - Ok(Some(serde_json::to_string(&env).unwrap())) + Ok(Some(serde_json::to_string(&env).unwrap().into())) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(TamperedStore, "key"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("sha mismatch must error"); - // SHA mismatch → internal error (envelope integrity failure). - assert!( - matches!(err, EdgeError::Internal { .. }), - "SHA mismatch must surface as Internal: {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::IntegrityMismatch) ); // The client-facing message (the HTTP body) must not carry the stored // hash, only a redacted category. @@ -2489,7 +3265,10 @@ mod tests { struct FutureVersionStore; #[async_trait(?Send)] impl ConfigStore for FutureVersionStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { // A v1-shaped envelope with the version bumped to 2. let env = BlobEnvelope::new( serde_json::json!({ "greeting": "hi", "timeout_ms": 100_u32 }), @@ -2498,16 +3277,18 @@ mod tests { let mut value: serde_json::Value = serde_json::from_str(&serde_json::to_string(&env).unwrap()).unwrap(); value["version"] = serde_json::json!(2_u32); - Ok(Some(value.to_string())) + Ok(Some(value.to_string().into())) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(FutureVersionStore, "key"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("a future envelope version must error"); - assert!( - matches!(err, EdgeError::Internal { .. }), - "a future version is Internal, not transient: {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::UnsupportedVersion) ); let message = err.message().to_lowercase(); assert!( @@ -2529,7 +3310,10 @@ mod tests { struct KindTaggedStore; #[async_trait(?Send)] impl ConfigStore for KindTaggedStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { let env = BlobEnvelope::new( serde_json::json!({ "greeting": "hi", "timeout_ms": 100_u32 }), "2026-01-01T00:00:00Z".into(), @@ -2537,8 +3321,10 @@ mod tests { let mut value: serde_json::Value = serde_json::from_str(&serde_json::to_string(&env).unwrap()).unwrap(); value["edgezero_kind"] = serde_json::json!("new_format"); - Ok(Some(value.to_string())) + Ok(Some(value.to_string().into())) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(KindTaggedStore, "key"); @@ -2546,20 +3332,56 @@ mod tests { .expect_err("an unknown edgezero_kind must error"); let message = err.message().to_lowercase(); assert!( - matches!(err, EdgeError::Internal { .. }) + err.store_extraction_reason() == Some(StoreExtractionReason::UnsupportedVersion) && message.contains("redeploy") && message.contains("edgezero_kind"), "an unknown discriminator must ask to redeploy: {err:?}" ); } + #[test] + fn app_config_extractor_rejects_an_escaped_edgezero_kind_key() { + struct EscapedKindTaggedStore; + #[async_trait(?Send)] + impl ConfigStore for EscapedKindTaggedStore { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + let env = BlobEnvelope::new( + serde_json::json!({ "greeting": "hi", "timeout_ms": 100_u32 }), + "2026-01-01T00:00:00Z".into(), + ); + let serialized = serde_json::to_string(&env).expect("envelope"); + let serialized_body = serialized.strip_prefix('{').expect("JSON object"); + Ok(Some( + format!("{{\"edgezero_\\u006bind\":\"new_format\",{serialized_body}").into(), + )) + } + + ready_config_store_bounded_read!(); + } + + let ctx = ctx_with_config_store(EscapedKindTaggedStore, "key"); + let err = block_on(AppConfig::::from_request(&ctx)) + .expect_err("an escaped edgezero_kind must error"); + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::UnsupportedVersion) + ); + assert!(err.message().contains("edgezero_kind")); + } + #[test] fn app_config_extractor_does_not_leak_data_value_in_deserialize_error() { const SENTINEL: &str = "SUPER_SECRET_FIELD_VALUE"; struct TypeErrorStore; #[async_trait(?Send)] impl ConfigStore for TypeErrorStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { // A VALID envelope (verify passes) whose data has the wrong type // for `timeout_ms` — a string sentinel where u32 is expected. // The deserialize error names that value; it must not reach the @@ -2568,16 +3390,18 @@ mod tests { serde_json::json!({ "greeting": "hi", "timeout_ms": SENTINEL }), "2026-01-01T00:00:00Z".into(), ); - Ok(Some(serde_json::to_string(&env).unwrap())) + Ok(Some(serde_json::to_string(&env).unwrap().into())) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(TypeErrorStore, "key"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("a type mismatch in the typed config must error"); - assert!( - matches!(err, EdgeError::ConfigOutOfDate { .. }), - "a typed deserialize failure must be ConfigOutOfDate: {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::Deserialization) ); assert!( !err.message().contains(SENTINEL), @@ -2585,30 +3409,37 @@ mod tests { ); // The field path segments are redacted (a map key is indistinguishable // from a struct field and may be a secret); structure is kept. - if let EdgeError::ConfigOutOfDate { field_path, .. } = &err { + if let EdgeError::StoreExtraction { field_path, .. } = &err { assert!( - !field_path.contains("timeout_ms") && field_path.contains(""), - "the field path must be redacted: {field_path}" + field_path.as_deref().is_some_and(|path| { + !path.contains("timeout_ms") && path.contains("") + }), + "the field path must be redacted: {field_path:?}" ); } } #[test] - fn app_config_extractor_returns_internal_on_bad_envelope_json() { + fn app_config_extractor_returns_malformed_envelope_on_bad_json() { struct GarbageStore; #[async_trait(?Send)] impl ConfigStore for GarbageStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some("not-json-at-all".to_owned())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some("not-json-at-all".into())) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(GarbageStore, "key"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("bad envelope JSON must error"); - assert!( - matches!(err, EdgeError::Internal { .. }), - "Envelope parse failure must be Internal: {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::MalformedEnvelope) ); assert!( err.message().contains("not a valid envelope"), @@ -2623,62 +3454,136 @@ mod tests { } #[test] - fn app_config_extractor_returns_config_out_of_date_on_deserialise_failure() { + fn app_config_extractor_rejects_structural_amplification_before_materializing() { + struct FixedStore(String); + #[async_trait(?Send)] + impl ConfigStore for FixedStore { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.clone().into())) + } + + ready_config_store_bounded_read!(); + } + + let mut nested = serde_json::json!("SECRET_SENTINEL"); + for _ in 0_u32..70_u32 { + nested = serde_json::json!({"": nested}); + } + let wide = serde_json::json!(vec![(); 8_193]); + for data in [nested, wide] { + let raw = serde_json::json!({ + "data": data, + "generated_at": "2026-01-01", + "sha256": "invalid", + "version": 1_u32, + }) + .to_string(); + let ctx = ctx_with_config_store(FixedStore(raw), "key"); + let err = block_on(AppConfig::::from_request(&ctx)) + .expect_err("structural cap must reject before envelope materialization"); + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::StructuralLimit) + ); + assert!(err.message().contains("JSON structure"), "{err:?}"); + assert!(!err.message().contains("SECRET_SENTINEL")); + } + } + + #[test] + fn config_json_structure_limits_are_inclusive_and_keep_malformed_distinct() { + let raw = serde_json::json!({"data": [null]}).to_string(); + check_json_structure(&raw, 3, 3).expect("exact node and depth caps accept"); + for (depth, nodes) in [(2, 3), (3, 2)] { + let err = check_json_structure(&raw, depth, nodes).expect_err("cap exceeded"); + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::StructuralLimit) + ); + } + for malformed in [r#"{"data":}"#, "{} true"] { + let err = check_json_structure(malformed, 3, 3).expect_err("invalid JSON"); + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::MalformedEnvelope) + ); + } + check_json_structure(r#"{"edgezero_\u006bind":"future"}"#, 2, 2) + .expect("escaped map keys are scanned without changing their semantics"); + } + + #[test] + fn app_config_extractor_returns_deserialization_on_deserialise_failure() { use crate::config_store::{ConfigStore, ConfigStoreError}; // Blob has wrong type for `timeout_ms` (string instead of u32). struct BadDataStore; #[async_trait(?Send)] impl ConfigStore for BadDataStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { let data = serde_json::json!({ "greeting": "hi", "timeout_ms": "not-a-number", }); - Ok(Some(make_envelope(data))) + Ok(Some(make_envelope(data).into())) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(BadDataStore, "key"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("type mismatch must error"); - assert!( - matches!(err, EdgeError::ConfigOutOfDate { .. }), - "deserialise failure must be ConfigOutOfDate: {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::Deserialization) ); // The field path segment is redacted (indistinguishable from a map key). - if let EdgeError::ConfigOutOfDate { field_path, .. } = &err { + if let EdgeError::StoreExtraction { field_path, .. } = &err { assert_eq!( - field_path, "", + field_path.as_deref(), + Some(""), "the field path must be redacted: {err:?}" ); } } #[test] - fn app_config_extractor_returns_config_out_of_date_on_validation_failure() { + fn app_config_extractor_returns_validation_on_validation_failure() { use crate::config_store::{ConfigStore, ConfigStoreError}; // `timeout_ms = 0` violates `range(min = 1)`. struct ZeroTimeoutStore; #[async_trait(?Send)] impl ConfigStore for ZeroTimeoutStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { let data = serde_json::json!({ "greeting": "hi", "timeout_ms": 0_u32 }); - Ok(Some(make_envelope(data))) + Ok(Some(make_envelope(data).into())) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(ZeroTimeoutStore, "key"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("validation failure must error"); - assert!( - matches!(err, EdgeError::ConfigOutOfDate { .. }), - "validation failure must be ConfigOutOfDate: {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::Validation) ); - if let EdgeError::ConfigOutOfDate { field_path, .. } = &err { + if let EdgeError::StoreExtraction { field_path, .. } = &err { assert_eq!( - field_path, "timeout_ms", + field_path.as_deref(), + Some("timeout_ms"), "field_path names the violator: {err:?}" ); } @@ -2690,9 +3595,14 @@ mod tests { struct BlobStore(String); #[async_trait(?Send)] impl ConfigStore for BlobStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.clone())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.clone().into())) } + + ready_config_store_bounded_read!(); } // Blob has `api_token = "my_key_name"` (a key name, not the secret). @@ -2713,14 +3623,19 @@ mod tests { } #[test] - fn app_config_secret_walk_missing_key_in_default_store_is_config_out_of_date() { + fn app_config_secret_walk_missing_key_has_typed_reason() { use crate::config_store::{ConfigStore, ConfigStoreError}; struct BlobStore(String); #[async_trait(?Send)] impl ConfigStore for BlobStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.clone())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.clone().into())) } + + ready_config_store_bounded_read!(); } // Blob references a key that doesn't exist in the secret store. @@ -2730,13 +3645,14 @@ mod tests { let ctx = ctx_with_config_and_secrets(BlobStore(blob), "key", NoopSecretStore, "vault"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("missing secret must error"); - assert!( - matches!(err, EdgeError::ConfigOutOfDate { .. }), - "missing secret must be ConfigOutOfDate: {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::MissingSecret) ); - if let EdgeError::ConfigOutOfDate { field_path, .. } = &err { + if let EdgeError::StoreExtraction { field_path, .. } = &err { assert_eq!( - field_path, "api_token", + field_path.as_deref(), + Some("api_token"), "field_path names the secret field: {err:?}" ); } @@ -2748,9 +3664,14 @@ mod tests { struct BlobStore(String); #[async_trait(?Send)] impl ConfigStore for BlobStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.clone())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.clone().into())) } + + ready_config_store_bounded_read!(); } // The blob's secret field holds the secret's KEY NAME — config data that @@ -2817,7 +3738,7 @@ mod tests { let mut data = serde_json::json!({ "integrations": { "datadome": { "server_side_key": "dd_key" } } }); - block_on(secret_walk::(&ctx, &mut data)).expect("walk"); + block_on(test_secret_walk::(&ctx, &mut data)).expect("walk"); assert_eq!( data["integrations"]["datadome"]["server_side_key"], serde_json::json!("resolved-dd") @@ -2830,7 +3751,7 @@ mod tests { let mut data = serde_json::json!({ "partners": [ { "api_key": "k0" }, { "api_key": "k1" } ] }); - block_on(secret_walk::(&ctx, &mut data)).expect("walk"); + block_on(test_secret_walk::(&ctx, &mut data)).expect("walk"); assert_eq!(data["partners"][0]["api_key"], serde_json::json!("v0")); assert_eq!(data["partners"][1]["api_key"], serde_json::json!("v1")); } @@ -2841,7 +3762,7 @@ mod tests { let mut data = serde_json::json!({ "vaulted": { "token": "tok_key", "vault": "named" } }); - block_on(secret_walk::(&ctx, &mut data)).expect("walk"); + block_on(test_secret_walk::(&ctx, &mut data)).expect("walk"); assert_eq!(data["vaulted"]["token"], serde_json::json!("TOK")); // The store_ref sibling is left intact (it names a store, not a secret). assert_eq!(data["vaulted"]["vault"], serde_json::json!("named")); @@ -2851,7 +3772,7 @@ mod tests { fn secret_walk_nested_named_store_missing_sibling_errors_with_dotted_path() { let ctx = ctx_with_named_secret_store("named", "tok_key", "TOK"); let mut data = serde_json::json!({ "vaulted": { "token": "tok_key" } }); // no `vault` - let err = block_on(secret_walk::(&ctx, &mut data)) + let err = block_on(test_secret_walk::(&ctx, &mut data)) .expect_err("missing store_ref sibling"); assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE); assert!(err.to_string().contains("vaulted.token")); @@ -2861,7 +3782,8 @@ mod tests { fn secret_walk_skips_absent_optional_leaf() { let ctx = ctx_with_default_secret_store("unused", "unused"); let mut data = serde_json::json!({ "greeting": "hi" }); // no maybe_key - block_on(secret_walk::(&ctx, &mut data)).expect("absent optional is fine"); + block_on(test_secret_walk::(&ctx, &mut data)) + .expect("absent optional is fine"); assert!(data.get("maybe_key").is_none()); } @@ -2871,7 +3793,7 @@ mod tests { // not omitted). The walk must skip a null optional leaf, not error it. let ctx = ctx_with_default_secret_store("unused", "unused"); let mut data = serde_json::json!({ "maybe_key": null }); - block_on(secret_walk::(&ctx, &mut data)) + block_on(test_secret_walk::(&ctx, &mut data)) .expect("null optional is skipped, not treated as non-string"); assert_eq!(data["maybe_key"], serde_json::json!(null)); // left untouched } @@ -2880,7 +3802,7 @@ mod tests { fn secret_walk_missing_required_nested_leaf_errors_with_dotted_path() { let ctx = ctx_with_default_secret_store("dd_key", "resolved-dd"); let mut data = serde_json::json!({ "integrations": { "datadome": {} } }); - let err = block_on(secret_walk::(&ctx, &mut data)) + let err = block_on(test_secret_walk::(&ctx, &mut data)) .expect_err("missing required nested leaf"); assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE); assert!( @@ -2897,7 +3819,7 @@ mod tests { // vaguer serde error downstream. let ctx = ctx_with_default_secret_store("unused", "unused"); let mut data = serde_json::json!({ "greeting": "hi" }); // no `integrations` - let err = block_on(secret_walk::(&ctx, &mut data)) + let err = block_on(test_secret_walk::(&ctx, &mut data)) .expect_err("missing required intermediate"); assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE); assert!( @@ -2910,7 +3832,7 @@ mod tests { fn secret_walk_skips_absent_optional_intermediate() { let ctx = ctx_with_default_secret_store("unused", "unused"); let mut data = serde_json::json!({ "integrations": {} }); - block_on(secret_walk::(&ctx, &mut data)) + block_on(test_secret_walk::(&ctx, &mut data)) .expect("absent optional intermediate is fine"); } @@ -2918,7 +3840,7 @@ mod tests { fn secret_walk_skips_null_optional_intermediate() { let ctx = ctx_with_default_secret_store("unused", "unused"); let mut data = serde_json::json!({ "integrations": { "datadome": null } }); - block_on(secret_walk::(&ctx, &mut data)) + block_on(test_secret_walk::(&ctx, &mut data)) .expect("null optional intermediate is fine"); } @@ -2926,7 +3848,7 @@ mod tests { fn secret_walk_rejects_scalar_parent_of_optional_intermediate() { let ctx = ctx_with_default_secret_store("unused", "unused"); let mut data = serde_json::json!({ "integrations": "not-an-object" }); - let err = block_on(secret_walk::(&ctx, &mut data)) + let err = block_on(test_secret_walk::(&ctx, &mut data)) .expect_err("a present optional intermediate must have an object parent"); assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE); @@ -2934,31 +3856,39 @@ mod tests { err.to_string().contains("integrations"), "error names the malformed parent: {err}" ); - let EdgeError::ConfigOutOfDate { field_path, .. } = &err else { - panic!("malformed optional parent must be ConfigOutOfDate: {err:?}"); + let EdgeError::StoreExtraction { + reason, field_path, .. + } = &err + else { + panic!("malformed optional parent must be typed: {err:?}"); }; - assert_eq!(field_path, "integrations"); + assert_eq!(*reason, StoreExtractionReason::Deserialization); + assert_eq!(field_path.as_deref(), Some("integrations")); } #[test] fn secret_walk_rejects_scalar_parent_of_terminal_optional_field() { let ctx = ctx_with_default_secret_store("unused", "unused"); let mut data = serde_json::json!({ "integrations": "not-an-object" }); - let err = block_on(secret_walk::(&ctx, &mut data)) + let err = block_on(test_secret_walk::(&ctx, &mut data)) .expect_err("a terminal optional field must have an object parent"); assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE); - let EdgeError::ConfigOutOfDate { field_path, .. } = &err else { - panic!("malformed optional parent must be ConfigOutOfDate: {err:?}"); + let EdgeError::StoreExtraction { + reason, field_path, .. + } = &err + else { + panic!("malformed optional parent must be typed: {err:?}"); }; - assert_eq!(field_path, "integrations.webhook_key"); + assert_eq!(*reason, StoreExtractionReason::Deserialization); + assert_eq!(field_path.as_deref(), Some("integrations.webhook_key")); } #[test] fn secret_walk_present_intermediate_absent_optional_leaf_is_ok() { let ctx = ctx_with_default_secret_store("unused", "unused"); let mut data = serde_json::json!({ "integrations": { "datadome": {} } }); - block_on(secret_walk::(&ctx, &mut data)) + block_on(test_secret_walk::(&ctx, &mut data)) .expect("absent optional leaf under present intermediates is fine"); } @@ -2967,11 +3897,13 @@ mod tests { struct KeyEchoStore; #[async_trait(?Send)] impl ConfigStore for KeyEchoStore { - async fn get(&self, key: &str) -> Result, ConfigStoreError> { + async fn get(&self, key: &str) -> Result, ConfigStoreError> { // Return a blob whose `greeting` equals the key being looked up. let data = serde_json::json!({ "greeting": key, "timeout_ms": 200_u32 }); - Ok(Some(make_envelope(data))) + Ok(Some(make_envelope(data).into())) } + + ready_config_store_bounded_read!(); } let ctx = ctx_with_config_store(KeyEchoStore, "default_key"); @@ -2988,10 +3920,15 @@ mod tests { struct NamedStore(&'static str); #[async_trait(?Send)] impl ConfigStore for NamedStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { let data = serde_json::json!({ "greeting": self.0, "timeout_ms": 300_u32 }); - Ok(Some(make_envelope(data))) + Ok(Some(make_envelope(data).into())) } + + ready_config_store_bounded_read!(); } // Wire two config stores: "primary" (default) and "secondary". @@ -3027,8 +3964,7 @@ mod tests { } #[test] - fn app_config_no_registry_returns_internal_error() { - // No ConfigRegistry in extensions → Internal error. + fn app_config_no_registry_returns_typed_error() { let request = request_builder() .method(Method::GET) .uri("/cfg") @@ -3037,9 +3973,9 @@ mod tests { let ctx = RequestContext::new(request, PathParams::default()); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("no registry must error"); - assert!( - matches!(err, EdgeError::Internal { .. }), - "no registry must surface as Internal: {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::MissingRegistry) ); assert!( err.message().contains("no default config store registered"), @@ -3077,9 +4013,14 @@ mod tests { struct BlobStore(String); #[async_trait(?Send)] impl ConfigStore for BlobStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.clone())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.clone().into())) } + + ready_config_store_bounded_read!(); } // Blob carries the key name "short" (5 chars) — push skipped the @@ -3096,7 +4037,7 @@ mod tests { } /// Spec 3.3.8: a resolved secret that FAILS a validator on the secret - /// field must produce `ConfigOutOfDate` — the runtime runs the full validator. + /// field must produce a typed schema mismatch; runtime runs the full validator. #[test] fn runtime_rejects_resolved_secret_failing_validator() { use crate::config_store::{ConfigStore, ConfigStoreError}; @@ -3121,9 +4062,14 @@ mod tests { struct BlobStore(String); #[async_trait(?Send)] impl ConfigStore for BlobStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.clone())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.clone().into())) } + + ready_config_store_bounded_read!(); } // The secret store resolves the key to a 5-char value — too short. @@ -3133,13 +4079,14 @@ mod tests { let ctx = ctx_with_config_and_secrets(BlobStore(blob), "key", secret_store, "vault"); let err = block_on(AppConfig::::from_request(&ctx)) .expect_err("short resolved secret must fail validator"); - assert!( - matches!(err, EdgeError::ConfigOutOfDate { .. }), - "validator failure on resolved secret must be ConfigOutOfDate: {err:?}" + assert_eq!( + err.store_extraction_reason(), + Some(StoreExtractionReason::Validation) ); - if let EdgeError::ConfigOutOfDate { field_path, .. } = &err { + if let EdgeError::StoreExtraction { field_path, .. } = &err { assert_eq!( - field_path, "api_token", + field_path.as_deref(), + Some("api_token"), "field_path names the violating secret field: {err:?}" ); } @@ -3171,9 +4118,14 @@ mod tests { struct BlobStore(String); #[async_trait(?Send)] impl ConfigStore for BlobStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.clone())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.clone().into())) } + + ready_config_store_bounded_read!(); } // A distinctive resolved secret that fails the `length(min = 100)` rule. diff --git a/crates/edgezero-core/src/http.rs b/crates/edgezero-core/src/http.rs index 60ead491f..34d0b39fb 100644 --- a/crates/edgezero-core/src/http.rs +++ b/crates/edgezero-core/src/http.rs @@ -12,8 +12,9 @@ pub mod header { use std::future::Future; use std::pin::Pin; -use http::request::Builder as HttpRequestBuilder; +use http::request::{Builder as HttpRequestBuilder, Parts as HttpRequestParts}; use http::response::Builder as HttpResponseBuilder; +use http::uri::Authority as HttpAuthority; use crate::body::Body; use crate::error::EdgeError; @@ -23,6 +24,7 @@ use crate::error::EdgeError; // `Builder` types are exposed via `pub type` aliases (not `pub use`) so // only the `header` re-export remains, scoped to its own child module. pub type Extensions = http::Extensions; +pub type Authority = HttpAuthority; pub type HandlerFuture = Pin> + 'static>>; pub type HeaderMap = http::HeaderMap; pub type HeaderName = header::HeaderName; @@ -30,6 +32,7 @@ pub type HeaderValue = http::HeaderValue; pub type Method = http::Method; pub type Request = http::Request; pub type RequestBuilder = HttpRequestBuilder; +pub type RequestParts = HttpRequestParts; pub type Response = http::Response; pub type ResponseBuilder = HttpResponseBuilder; pub type StatusCode = http::StatusCode; diff --git a/crates/edgezero-core/src/ingress.rs b/crates/edgezero-core/src/ingress.rs new file mode 100644 index 000000000..855825966 --- /dev/null +++ b/crates/edgezero-core/src/ingress.rs @@ -0,0 +1,1180 @@ +use std::any::Any; +use std::fmt; +use std::sync::Arc; +use std::time::Duration; + +use bytes::Bytes; + +use crate::body::Body; +use crate::config_store::ConfigExtractionLimits; +use crate::error::EdgeError; +use crate::http::{ + Extensions, HeaderMap, HeaderValue, Method, Request, RequestParts, Response, StatusCode, Uri, + Version, + header::{CONTENT_LENGTH, CONTENT_TYPE, TRANSFER_ENCODING}, +}; +use crate::response_egress::{ResponseEgressCompletion, ResponseEgressEnvelope}; +use crate::router::{ResolvedDispatch, RouteMetadata, RouteResolution}; +use crate::time::{DEADLINE_FAR_FUTURE, Deadline, MonotonicClock, MonotonicInstant}; + +pub const DEFAULT_INBOUND_READ_BUDGET: Duration = Duration::from_secs(30); +pub const DEFAULT_MAX_REQUEST_HEADER_BYTES: u64 = 0x0001_0000; +pub const DEFAULT_MAX_REQUEST_HEADER_COUNT: u64 = 100; +pub const DEFAULT_MAX_REQUEST_TARGET_BYTES: u64 = 8_192; + +/// Opaque application-owned lease transferred to one admitted request. +pub struct IngressGrant { + value: Option>, +} + +impl IngressGrant { + /// Extracts the application-owned value without exposing its type metadata. + /// + /// # Errors + /// Returns the unchanged grant when it is empty or stores a different type. + #[inline] + pub fn downcast(mut self) -> Result + where + T: Send + Sync + 'static, + { + let Some(boxed) = self.value.take() else { + return Err(self); + }; + match boxed.downcast::() { + Ok(typed) => Ok(*typed), + Err(original) => { + self.value = Some(original); + Err(self) + } + } + } + + #[must_use] + #[inline] + pub fn empty() -> Self { + Self { value: None } + } + + #[must_use] + #[inline] + pub fn new(value: T) -> Self + where + T: Send + Sync + 'static, + { + Self { + value: Some(Box::new(value)), + } + } +} + +impl fmt::Debug for IngressGrant { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("IngressGrant") + .field("occupied", &self.value.is_some()) + .finish() + } +} + +/// Cloneable application response restricted to finite buffered body bytes. +#[derive(Clone, Debug)] +pub struct BufferedIngressResponse { + body: Bytes, + headers: HeaderMap, + status: StatusCode, +} + +impl BufferedIngressResponse { + /// Returns the finite buffered response bytes. + #[must_use] + #[inline] + pub fn body(&self) -> &[u8] { + &self.body + } + + /// Returns the application-selected response headers. + #[must_use] + #[inline] + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + pub(crate) fn into_response(self) -> Response { + let mut response = Response::new(Body::from_bytes(self.body)); + *response.status_mut() = self.status; + *response.headers_mut() = self.headers; + response + } + + /// Constructs an exact buffered response from application-selected parts. + #[must_use] + #[inline] + pub fn new(status: StatusCode, headers: HeaderMap, body: B) -> Self + where + B: Into, + { + Self { + body: body.into(), + headers, + status, + } + } + + /// Returns the application-selected response status. + #[must_use] + #[inline] + pub fn status(&self) -> StatusCode { + self.status + } + + /// Constructs a buffered UTF-8 plain-text response with an exact content length. + #[must_use] + #[inline] + pub fn text(status: StatusCode, text: S) -> Self + where + S: Into, + { + let body = Bytes::from(text.into()); + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("text/plain; charset=utf-8"), + ); + headers.insert(CONTENT_LENGTH, HeaderValue::from(body.len())); + Self::new(status, headers, body) + } +} + +/// Application decision made before an inbound body can be consumed. +#[non_exhaustive] +pub enum AdmissionDecision { + /// Closes or resets the native request without producing an HTTP response. + Abort, + Admit { + completion: ResponseEgressCompletion, + grant: IngressGrant, + read_deadline: Deadline, + }, + /// Drains an unmatched or wrong-method request body before returning the canonical + /// pre-resolved 404/405 response. + ReadBodyBeforeFallback { + completion: ResponseEgressCompletion, + grant: IngressGrant, + max_body_bytes: usize, + read_deadline: Deadline, + on_exceeded: BufferedIngressResponse, + on_timeout: BufferedIngressResponse, + }, + Refuse { + completion: ResponseEgressCompletion, + response: Response, + }, +} + +struct FallbackDispatch { + max_body_bytes: usize, + on_exceeded: BufferedIngressResponse, + on_timeout: BufferedIngressResponse, +} + +enum IngressDispatchDisposition { + Dispatch, + ReadBodyBeforeFallback(Box), +} + +pub(crate) struct FallbackIngress { + dispatch: Box, + grant: IngressGrant, + monotonic_clock: MonotonicClock, + read_deadline: Deadline, +} + +impl FallbackIngress { + pub(crate) fn into_parts( + self, + ) -> ( + IngressGrant, + usize, + Deadline, + MonotonicClock, + BufferedIngressResponse, + BufferedIngressResponse, + ) { + let FallbackDispatch { + max_body_bytes, + on_exceeded, + on_timeout, + } = *self.dispatch; + ( + self.grant, + max_body_bytes, + self.read_deadline, + self.monotonic_clock, + on_exceeded, + on_timeout, + ) + } +} + +/// Validated policy outcome consumed by an adapter before native body ownership moves. +/// +/// `Admitted` includes both normal routed dispatch and an opt-in bounded fallback drain. +#[non_exhaustive] +pub enum IngressAdmissionOutcome { + Aborted, + Admitted { + completion: ResponseEgressCompletion, + ingress: AdmittedIngress, + }, + Refused { + completion: ResponseEgressCompletion, + response: Response, + }, +} + +/// Result of route resolution plus application admission before native body ownership moves. +#[non_exhaustive] +pub enum IngressBeginOutcome { + Aborted, + Admitted(PreparedIngress), + Refused(ResponseEgressEnvelope), +} + +/// Result of complete ingress dispatch, preserving abort as a non-response outcome. +#[non_exhaustive] +pub enum IngressDispatchOutcome { + Aborted, + Response(Box), +} + +/// Proof that the application selected one request disposition with a finite read deadline. +pub struct AdmittedIngress { + config_extraction_limits: ConfigExtractionLimits, + dispatch_disposition: IngressDispatchDisposition, + grant: IngressGrant, + monotonic_clock: MonotonicClock, + read_deadline: Deadline, + request_start: MonotonicInstant, +} + +impl AdmittedIngress { + pub(crate) fn into_fallback(self) -> Option { + let Self { + dispatch_disposition, + grant, + monotonic_clock, + read_deadline, + .. + } = self; + let IngressDispatchDisposition::ReadBodyBeforeFallback(dispatch) = dispatch_disposition + else { + return None; + }; + Some(FallbackIngress { + dispatch, + grant, + monotonic_clock, + read_deadline, + }) + } + + pub(crate) fn into_parts( + self, + ) -> ( + MonotonicInstant, + Deadline, + IngressGrant, + ConfigExtractionLimits, + MonotonicClock, + ) { + ( + self.request_start, + self.read_deadline, + self.grant, + self.config_extraction_limits, + self.monotonic_clock, + ) + } + + pub(crate) fn is_fallback(&self) -> bool { + matches!( + self.dispatch_disposition, + IngressDispatchDisposition::ReadBodyBeforeFallback(_) + ) + } + + #[must_use] + #[inline] + pub fn monotonic_clock(&self) -> MonotonicClock { + self.monotonic_clock.clone() + } + + #[must_use] + #[inline] + pub fn read_deadline(&self) -> Deadline { + self.read_deadline + } + + #[must_use] + #[inline] + pub fn request_start(&self) -> MonotonicInstant { + self.request_start + } + + pub(crate) fn with_config_extraction_limits(mut self, limits: ConfigExtractionLimits) -> Self { + self.config_extraction_limits = limits; + self + } +} + +/// Whether request-head accounting was enforced before normalized request construction. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum IngressHeadAccounting { + HostManaged, + RawValidated { + request_header_bytes: u64, + request_header_count: u64, + request_target_bytes: u64, + }, +} + +/// Trusted framing result, or an explicit marker that the host owns validation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum IngressFraming { + Chunked, + ContentLength(u64), + HostManaged, + NoBody, + ProtocolManaged, +} + +/// Finite request-head limits installed before an adapter begins serving. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct IngressHeadLimits { + header_bytes: u64, + header_count: u64, + target_bytes: u64, +} + +impl IngressHeadLimits { + #[must_use] + #[inline] + pub fn max_request_header_bytes(&self) -> u64 { + self.header_bytes + } + + #[must_use] + #[inline] + pub fn max_request_header_count(&self) -> u64 { + self.header_count + } + + #[must_use] + #[inline] + pub fn max_request_target_bytes(&self) -> u64 { + self.target_bytes + } + + /// # Errors + /// Returns an internal startup-policy error when `max` is zero. + #[inline] + pub fn with_max_request_header_bytes(mut self, max: u64) -> Result { + require_nonzero("max_request_header_bytes", max)?; + self.header_bytes = max; + Ok(self) + } + + /// # Errors + /// Returns an internal startup-policy error when `max` is zero. + #[inline] + pub fn with_max_request_header_count(mut self, max: u64) -> Result { + require_nonzero("max_request_header_count", max)?; + self.header_count = max; + Ok(self) + } + + /// # Errors + /// Returns an internal startup-policy error when `max` is zero. + #[inline] + pub fn with_max_request_target_bytes(mut self, max: u64) -> Result { + require_nonzero("max_request_target_bytes", max)?; + self.target_bytes = max; + Ok(self) + } +} + +impl Default for IngressHeadLimits { + #[inline] + fn default() -> Self { + Self { + header_bytes: DEFAULT_MAX_REQUEST_HEADER_BYTES, + header_count: DEFAULT_MAX_REQUEST_HEADER_COUNT, + target_bytes: DEFAULT_MAX_REQUEST_TARGET_BYTES, + } + } +} + +/// Normalized, body-free request metadata supplied by an adapter before admission. +pub struct IngressHeadParts { + extensions: Extensions, + framing: IngressFraming, + head_accounting: IngressHeadAccounting, + headers: HeaderMap, + method: Method, + target: Uri, + version: Version, +} + +impl IngressHeadParts { + /// Copies normalized metadata from body-free request parts. + #[must_use] + #[inline] + pub fn from_parts( + parts: &RequestParts, + head_accounting: IngressHeadAccounting, + framing: IngressFraming, + ) -> Self { + Self { + extensions: parts.extensions.clone(), + framing, + head_accounting, + headers: parts.headers.clone(), + method: parts.method.clone(), + target: parts.uri.clone(), + version: parts.version, + } + } + + /// Copies normalized metadata from a core request without inspecting its body. + #[must_use] + #[inline] + pub fn from_request( + request: &Request, + head_accounting: IngressHeadAccounting, + framing: IngressFraming, + ) -> Self { + Self { + extensions: request.extensions().clone(), + framing, + head_accounting, + headers: request.headers().clone(), + method: request.method().clone(), + target: request.uri().clone(), + version: request.version(), + } + } + + pub(crate) fn into_head( + self, + request_start: MonotonicInstant, + route_resolution: RouteResolution, + ) -> IngressHead { + IngressHead { + extensions: self.extensions, + framing: self.framing, + head_accounting: self.head_accounting, + headers: self.headers, + method: self.method, + request_start, + route_resolution, + target: self.target, + version: self.version, + } + } + + #[must_use] + #[inline] + pub fn method(&self) -> &Method { + &self.method + } + + /// Creates host-managed request-head metadata. Adapters with a raw parser boundary replace + /// the accounting and framing markers through the builder methods below. + #[must_use] + #[inline] + pub fn new(method: Method, target: Uri, version: Version, headers: HeaderMap) -> Self { + Self { + extensions: Extensions::new(), + framing: IngressFraming::HostManaged, + head_accounting: IngressHeadAccounting::HostManaged, + headers, + method, + target, + version, + } + } + + #[must_use] + #[inline] + pub fn target(&self) -> &Uri { + &self.target + } + + /// Applies normalized defense-in-depth checks without making a raw-boundary claim. + /// + /// # Errors + /// Returns 414/431 for normalized head overages and 400 for visible ambiguous framing. + #[inline] + pub fn validate_normalized(&self, limits: IngressHeadLimits) -> Result<(), EdgeError> { + validate_normalized_head(&self.target, self.version, &self.headers, limits) + } + + #[must_use] + #[inline] + pub fn with_extension(mut self, value: T) -> Self + where + T: Clone + Send + Sync + 'static, + { + self.extensions.insert(value); + self + } + + #[must_use] + #[inline] + pub fn with_framing(mut self, framing: IngressFraming) -> Self { + self.framing = framing; + self + } + + #[must_use] + #[inline] + pub fn with_head_accounting(mut self, head_accounting: IngressHeadAccounting) -> Self { + self.head_accounting = head_accounting; + self + } +} + +/// Immutable request-head view supplied to the application admission policy. +pub struct IngressHead { + extensions: Extensions, + framing: IngressFraming, + head_accounting: IngressHeadAccounting, + headers: HeaderMap, + method: Method, + request_start: MonotonicInstant, + route_resolution: RouteResolution, + target: Uri, + version: Version, +} + +impl IngressHead { + #[must_use] + #[inline] + pub fn extension(&self) -> Option<&T> + where + T: Send + Sync + 'static, + { + self.extensions.get::() + } + + #[must_use] + #[inline] + pub fn framing(&self) -> IngressFraming { + self.framing + } + + /// Builds the body-blind admission view from a normalized core request. + #[must_use] + #[inline] + pub fn from_request( + request: &Request, + request_start: MonotonicInstant, + route_resolution: RouteResolution, + head_accounting: IngressHeadAccounting, + framing: IngressFraming, + ) -> Self { + IngressHeadParts::from_request(request, head_accounting, framing) + .into_head(request_start, route_resolution) + } + + #[must_use] + #[inline] + pub fn head_accounting(&self) -> IngressHeadAccounting { + self.head_accounting + } + + #[must_use] + #[inline] + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + #[must_use] + #[inline] + pub fn method(&self) -> &Method { + &self.method + } + + /// Builds a finite deadline relative to this head's captured request start. + /// + /// The duration is clamped to the configured far-future bound. Arithmetic + /// overflow fails closed by returning a deadline at the request start. + #[must_use] + #[inline] + pub fn read_deadline_after(&self, duration: Duration) -> Deadline { + let bounded = duration.min(DEADLINE_FAR_FUTURE); + Deadline::at_instant( + self.request_start + .checked_add(bounded) + .unwrap_or(self.request_start), + ) + } + + #[must_use] + #[inline] + pub fn request_start(&self) -> MonotonicInstant { + self.request_start + } + + #[must_use] + #[inline] + pub fn route_resolution(&self) -> &RouteResolution { + &self.route_resolution + } + + #[must_use] + #[inline] + pub fn target(&self) -> &Uri { + &self.target + } + + #[must_use] + #[inline] + pub fn version(&self) -> Version { + self.version + } + + #[must_use] + #[inline] + pub fn with_extension(mut self, value: T) -> Self + where + T: Clone + Send + Sync + 'static, + { + self.extensions.insert(value); + self + } +} + +/// Opaque, single-use admission proof paired with the exact resolved dispatch token. +pub struct PreparedIngress { + admitted: AdmittedIngress, + completion: ResponseEgressCompletion, + resolved: ResolvedDispatch, +} + +impl PreparedIngress { + pub(crate) fn into_parts( + self, + ) -> (ResolvedDispatch, AdmittedIngress, ResponseEgressCompletion) { + (self.resolved, self.admitted, self.completion) + } + + #[must_use] + #[inline] + pub fn monotonic_clock(&self) -> MonotonicClock { + self.admitted.monotonic_clock() + } + + pub(crate) fn new( + resolved: ResolvedDispatch, + admitted: AdmittedIngress, + completion: ResponseEgressCompletion, + ) -> Self { + Self { + admitted, + completion, + resolved, + } + } + + #[must_use] + #[inline] + pub fn read_deadline(&self) -> Deadline { + self.admitted.read_deadline() + } + + /// Canonical request method captured before admission. + #[must_use] + #[inline] + pub fn request_method(&self) -> &Method { + self.resolved.method() + } + + #[must_use] + #[inline] + pub fn request_start(&self) -> MonotonicInstant { + self.admitted.request_start() + } + + /// Canonical metadata for the admitted matched route, if any. + #[must_use] + #[inline] + pub fn route_metadata(&self) -> Option<&RouteMetadata> { + match self.resolved.resolution() { + RouteResolution::Matched(route) => Some(route), + RouteResolution::MethodNotAllowed { .. } | RouteResolution::NotFound => None, + } + } + + #[must_use] + #[inline] + pub fn route_resolution(&self) -> &RouteResolution { + self.resolved.resolution() + } +} + +pub(crate) type IngressAdmissionPolicy = + Arc AdmissionDecision + Send + Sync>; + +pub(crate) fn default_admission_policy() -> IngressAdmissionPolicy { + Arc::new(|head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(DEFAULT_INBOUND_READ_BUDGET), + }) +} + +pub(crate) fn apply_admission_policy( + policy: &IngressAdmissionPolicy, + head: &IngressHead, + monotonic_clock: MonotonicClock, +) -> Result { + let (completion, grant, read_deadline, dispatch_disposition) = match policy(head) { + AdmissionDecision::Refuse { + completion, + response, + } => { + return Ok(IngressAdmissionOutcome::Refused { + completion, + response, + }); + } + AdmissionDecision::Abort => return Ok(IngressAdmissionOutcome::Aborted), + AdmissionDecision::Admit { + completion, + grant, + read_deadline, + } => ( + completion, + grant, + read_deadline, + IngressDispatchDisposition::Dispatch, + ), + AdmissionDecision::ReadBodyBeforeFallback { + completion, + grant, + max_body_bytes, + read_deadline, + on_exceeded, + on_timeout, + } => { + match head.route_resolution() { + RouteResolution::MethodNotAllowed { .. } | RouteResolution::NotFound => {} + RouteResolution::Matched(_) => { + return Err(EdgeError::internal(anyhow::anyhow!( + "fallback body policy requires an unmatched or wrong-method route" + ))); + } + } + ( + completion, + grant, + read_deadline, + IngressDispatchDisposition::ReadBodyBeforeFallback(Box::new(FallbackDispatch { + max_body_bytes, + on_exceeded, + on_timeout, + })), + ) + } + }; + let maximum = head + .request_start() + .checked_add(DEADLINE_FAR_FUTURE) + .ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!( + "ingress admission deadline arithmetic overflow" + )) + })?; + let deadline = Deadline::at_instant(read_deadline.instant().min(maximum)); + Ok(IngressAdmissionOutcome::Admitted { + completion, + ingress: AdmittedIngress { + config_extraction_limits: ConfigExtractionLimits::default(), + dispatch_disposition, + grant, + monotonic_clock, + read_deadline: deadline, + request_start: head.request_start(), + }, + }) +} + +fn require_nonzero(name: &str, value: u64) -> Result<(), EdgeError> { + if value == 0 { + return Err(EdgeError::internal(anyhow::anyhow!( + "ingress head limit `{name}` must be nonzero" + ))); + } + Ok(()) +} + +/// Applies finite defense-in-depth limits and framing checks to a normalized head. +/// +/// This cannot recover raw HTTP field-line or request-target octets and therefore never +/// upgrades [`IngressHeadAccounting::HostManaged`] or [`IngressFraming::HostManaged`]. +/// +/// # Errors +/// Returns 414/431 for normalized head overages and 400 for visible ambiguous framing. +#[inline] +pub fn validate_normalized_ingress_parts( + parts: &RequestParts, + limits: IngressHeadLimits, +) -> Result<(), EdgeError> { + validate_normalized_head(&parts.uri, parts.version, &parts.headers, limits) +} + +fn validate_normalized_head( + target: &Uri, + version: Version, + headers: &HeaderMap, + limits: IngressHeadLimits, +) -> Result<(), EdgeError> { + let target_bytes = u64::try_from(target.to_string().len()).map_err(|_length_error| { + EdgeError::uri_too_long("normalized request target is too large") + })?; + if target_bytes > limits.max_request_target_bytes() { + return Err(EdgeError::uri_too_long( + "normalized request target exceeds configured limit", + )); + } + + let header_count = u64::try_from(headers.len()).map_err(|_length_error| { + EdgeError::request_header_fields_too_large("normalized request has too many headers") + })?; + if header_count > limits.max_request_header_count() { + return Err(EdgeError::request_header_fields_too_large( + "normalized request header count exceeds configured limit", + )); + } + + let mut header_bytes = 2_u64; + for (name, value) in headers { + let line_bytes = name + .as_str() + .len() + .checked_add(value.as_bytes().len()) + .and_then(|bytes| bytes.checked_add(4)) + .and_then(|bytes| u64::try_from(bytes).ok()) + .ok_or_else(|| { + EdgeError::request_header_fields_too_large( + "normalized request header size accounting overflow", + ) + })?; + header_bytes = header_bytes.checked_add(line_bytes).ok_or_else(|| { + EdgeError::request_header_fields_too_large( + "normalized request header size accounting overflow", + ) + })?; + if header_bytes > limits.max_request_header_bytes() { + return Err(EdgeError::request_header_fields_too_large( + "normalized request headers exceed configured limit", + )); + } + } + + validate_normalized_framing(version, headers) +} + +fn validate_normalized_framing(version: Version, headers: &HeaderMap) -> Result<(), EdgeError> { + let content_lengths = headers.get_all(CONTENT_LENGTH).iter().collect::>(); + let transfer_encodings = headers + .get_all(TRANSFER_ENCODING) + .iter() + .collect::>(); + + if !content_lengths.is_empty() && !transfer_encodings.is_empty() { + return Err(EdgeError::bad_request( + "ambiguous request framing is not accepted", + )); + } + if content_lengths.len() > 1 { + return Err(EdgeError::bad_request( + "duplicate content-length is not accepted", + )); + } + if let Some(value) = content_lengths.first() { + let raw = value + .to_str() + .map_err(|_utf8_error| EdgeError::bad_request("malformed content-length"))? + .trim(); + if raw.is_empty() + || raw.contains(',') + || raw.starts_with(['+', '-']) + || raw.parse::().is_err() + { + return Err(EdgeError::bad_request("malformed content-length")); + } + } + + if transfer_encodings.len() > 1 { + return Err(EdgeError::bad_request( + "duplicate transfer-encoding is not accepted", + )); + } + if let Some(value) = transfer_encodings.first() { + if version != Version::HTTP_11 { + return Err(EdgeError::bad_request( + "transfer-encoding is not accepted for this HTTP version", + )); + } + let raw = value + .to_str() + .map_err(|_utf8_error| EdgeError::bad_request("malformed transfer-encoding"))?; + if raw.contains(',') || !raw.trim().eq_ignore_ascii_case("chunked") { + return Err(EdgeError::bad_request( + "unsupported or malformed transfer-encoding", + )); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::body::Body; + use crate::http::{HeaderName, HeaderValue, StatusCode, request_builder, response_builder}; + + fn parts_with_headers(version: Version, headers: &[(&str, &str)]) -> RequestParts { + let mut request = request_builder() + .uri("/") + .version(version) + .body(Body::empty()) + .expect("request"); + for (name, value) in headers { + request.headers_mut().append( + name.parse::().expect("header name"), + HeaderValue::from_str(value).expect("header value"), + ); + } + request.into_parts().0 + } + + #[test] + fn ingress_grant_downcasts_once_without_exposing_type_metadata() { + let string_grant = IngressGrant::new(String::from("lease")); + let retained_grant = string_grant.downcast::().expect_err("wrong type"); + assert_eq!( + retained_grant.downcast::().expect("right type"), + "lease" + ); + IngressGrant::empty() + .downcast::() + .expect_err("empty grant"); + } + + #[test] + fn buffered_ingress_response_is_cloneable_and_builds_exact_plain_text() { + let response = + BufferedIngressResponse::text(StatusCode::PAYLOAD_TOO_LARGE, "fallback body exceeded"); + let cloned = response.clone(); + + assert_eq!(cloned.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!( + cloned.headers().get("content-type").expect("content type"), + "text/plain; charset=utf-8" + ); + assert_eq!( + cloned + .headers() + .get("content-length") + .expect("content length"), + "22" + ); + assert_eq!(cloned.body(), b"fallback body exceeded"); + } + + #[test] + fn ingress_head_limits_are_finite_nonzero_and_independently_configurable() { + let limits = IngressHeadLimits::default(); + assert_eq!(limits.max_request_header_bytes(), 0x0001_0000); + assert_eq!(limits.max_request_header_count(), 100); + assert_eq!(limits.max_request_target_bytes(), 8_192); + limits + .with_max_request_header_bytes(0) + .expect_err("zero header bytes"); + limits + .with_max_request_header_count(0) + .expect_err("zero header count"); + limits + .with_max_request_target_bytes(0) + .expect_err("zero target bytes"); + } + + #[test] + fn normalized_head_limits_reject_before_admission_without_raw_claims() { + let target_request = request_builder() + .uri("/1234") + .body(Body::empty()) + .expect("request"); + let target_parts = target_request.into_parts().0; + let target_limits = IngressHeadLimits::default() + .with_max_request_target_bytes(4) + .expect("target limit"); + assert_eq!( + validate_normalized_ingress_parts(&target_parts, target_limits) + .expect_err("target over limit") + .status(), + StatusCode::URI_TOO_LONG + ); + + let count_parts = parts_with_headers(Version::HTTP_11, &[("x-a", "1"), ("x-b", "2")]); + let count_limits = IngressHeadLimits::default() + .with_max_request_header_count(1) + .expect("count limit"); + assert_eq!( + validate_normalized_ingress_parts(&count_parts, count_limits) + .expect_err("header count over limit") + .status(), + StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE + ); + + let bytes_parts = parts_with_headers(Version::HTTP_11, &[("x", "1")]); + let bytes_limits = IngressHeadLimits::default() + .with_max_request_header_bytes(7) + .expect("bytes limit"); + assert_eq!( + validate_normalized_ingress_parts(&bytes_parts, bytes_limits) + .expect_err("header bytes over limit") + .status(), + StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE + ); + } + + #[test] + fn normalized_framing_defense_rejects_visible_ambiguous_shapes() { + let cases = [ + parts_with_headers( + Version::HTTP_11, + &[("content-length", "1"), ("transfer-encoding", "chunked")], + ), + parts_with_headers( + Version::HTTP_11, + &[("content-length", "1"), ("content-length", "1")], + ), + parts_with_headers(Version::HTTP_11, &[("content-length", "1, 1")]), + parts_with_headers(Version::HTTP_11, &[("content-length", "+1")]), + parts_with_headers( + Version::HTTP_11, + &[("content-length", "18446744073709551616")], + ), + parts_with_headers(Version::HTTP_11, &[("transfer-encoding", "gzip, chunked")]), + parts_with_headers(Version::HTTP_2, &[("transfer-encoding", "chunked")]), + ]; + for parts in cases { + assert_eq!( + validate_normalized_ingress_parts(&parts, IngressHeadLimits::default()) + .expect_err("ambiguous framing") + .status(), + StatusCode::BAD_REQUEST + ); + } + + for parts in [ + parts_with_headers(Version::HTTP_11, &[("content-length", "1")]), + parts_with_headers(Version::HTTP_11, &[("transfer-encoding", "chunked")]), + ] { + validate_normalized_ingress_parts(&parts, IngressHeadLimits::default()) + .expect("unambiguous visible framing"); + } + } + + #[test] + fn admission_clamps_deadline_and_preserves_refusal() { + let start = MonotonicInstant::now(); + let request = request_builder() + .method(Method::GET) + .uri("/") + .body(Body::empty()) + .expect("request"); + let head = IngressHead::from_request( + &request, + start, + RouteResolution::NotFound, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ); + let too_late = Deadline::at_instant( + start + .checked_add(DEADLINE_FAR_FUTURE + Duration::from_secs(1)) + .expect("deadline"), + ); + let admit_policy: IngressAdmissionPolicy = Arc::new(move |_| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: too_late, + }); + let IngressAdmissionOutcome::Admitted { + completion, + ingress: admitted, + } = apply_admission_policy(&admit_policy, &head, MonotonicClock::default()) + .expect("admission") + else { + panic!("expected admission"); + }; + drop(completion); + assert_eq!( + admitted.read_deadline().instant(), + start.checked_add(DEADLINE_FAR_FUTURE).expect("maximum") + ); + + let refuse_policy: IngressAdmissionPolicy = Arc::new(|_| AdmissionDecision::Refuse { + completion: ResponseEgressCompletion::empty(), + response: response_builder() + .status(StatusCode::TOO_MANY_REQUESTS) + .body(Body::empty()) + .expect("response"), + }); + let IngressAdmissionOutcome::Refused { + completion: refusal_completion, + response, + } = apply_admission_policy(&refuse_policy, &head, MonotonicClock::default()) + .expect("refusal") + else { + panic!("expected refusal"); + }; + drop(refusal_completion); + assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); + } + + #[test] + fn ingress_head_builds_relative_deadlines_in_its_start_clock_domain() { + let global = MonotonicInstant::now(); + let request_start = global + .checked_add(Duration::from_hours(24)) + .expect("offset request start"); + let request = request_builder() + .method(Method::GET) + .uri("/") + .body(Body::empty()) + .expect("request"); + let head = IngressHead::from_request( + &request, + request_start, + RouteResolution::NotFound, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ); + + assert_eq!( + head.read_deadline_after(Duration::from_secs(2)).instant(), + request_start + .checked_add(Duration::from_secs(2)) + .expect("relative deadline") + ); + } +} diff --git a/crates/edgezero-core/src/introspection.rs b/crates/edgezero-core/src/introspection.rs index d74ba664c..dba10574e 100644 --- a/crates/edgezero-core/src/introspection.rs +++ b/crates/edgezero-core/src/introspection.rs @@ -117,7 +117,9 @@ pub async fn config(ctx: RequestContext) -> Result { #[cfg(test)] mod tests { use super::*; - use crate::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; + use crate::config_store::{ + ConfigStore, ConfigStoreError, ConfigStoreHandle, ready_config_store_bounded_read, + }; use crate::http::{Method, Response, request_builder}; use crate::router::RouterService; use crate::store_registry::{ConfigRegistry, ConfigStoreBinding, StoreRegistry}; @@ -129,10 +131,10 @@ mod tests { // A config store returning a fixed result for `get`, used to drive the // config handler's status-code mapping. Mirrors the pattern in // extractor.rs::config_extractor_resolves_from_registry. - struct StubStore(Result, ConfigStoreError>); + struct StubStore(Result, ConfigStoreError>); #[async_trait(?Send)] impl ConfigStore for StubStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get(&self, _key: &str) -> Result, ConfigStoreError> { match &self.0 { Ok(val) => Ok(val.clone()), Err(ConfigStoreError::Unavailable { .. }) => { @@ -144,10 +146,12 @@ mod tests { Err(_) => Err(ConfigStoreError::internal(anyhow::anyhow!("boom"))), } } + + ready_config_store_bounded_read!(); } // Collect a buffered response body into JSON (introspection responses are - // always `Body::Once`). `Body::to_json` works on the buffered variant. + // always `BodyPayload::Once`). `Body::to_json` works on the buffered payload. fn body_json(resp: Response) -> serde_json::Value { resp.into_body().to_json().expect("buffered JSON body") } @@ -187,7 +191,7 @@ mod tests { #[test] fn manifest_returns_injected_json() { let router = RouterService::builder() - .with_manifest_json("{\"app\":{\"name\":\"t\"}}") + .with_manifest_json(serde_json::json!({"app": {"name": "t"}}).to_string()) .get("/m", manifest) .build(); let req = request_builder() @@ -261,7 +265,7 @@ mod tests { #[test] fn config_happy_path_returns_envelope_data_secret_safe() { let data = serde_json::json!({ "greeting": "hi", "api_token": "demo_api_token" }); - let resp = run_config(StubStore(Ok(Some(valid_envelope_json(data))))); + let resp = run_config(StubStore(Ok(Some(valid_envelope_json(data).into())))); assert_eq!(resp.status(), StatusCode::OK); // Raw envelope `data` verbatim: the secret field holds the KEY NAME, // never a resolved value. @@ -298,22 +302,34 @@ mod tests { #[test] fn config_malformed_envelope_maps_500() { - let resp = run_config(StubStore(Ok(Some("not json".to_owned())))); + let resp = run_config(StubStore(Ok(Some("not json".into())))); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } #[test] fn config_sha_mismatch_maps_500() { // Valid JSON envelope shape but wrong sha → verify() fails. - let bad = r#"{"data":{"a":1},"generated_at":"t","sha256":"deadbeef","version":1}"#; - let resp = run_config(StubStore(Ok(Some(bad.to_owned())))); + let bad = serde_json::json!({ + "data": {"a": 1_u32}, + "generated_at": "t", + "sha256": "deadbeef", + "version": 1_u32, + }) + .to_string(); + let resp = run_config(StubStore(Ok(Some(bad.into())))); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } #[test] fn config_unknown_version_maps_500() { - let bad = r#"{"data":{},"generated_at":"t","sha256":"x","version":99}"#; - let resp = run_config(StubStore(Ok(Some(bad.to_owned())))); + let bad = serde_json::json!({ + "data": {}, + "generated_at": "t", + "sha256": "x", + "version": 99_u32, + }) + .to_string(); + let resp = run_config(StubStore(Ok(Some(bad.into())))); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } } diff --git a/crates/edgezero-core/src/key_value_store.rs b/crates/edgezero-core/src/key_value_store.rs index a7469287c..7cd993811 100644 --- a/crates/edgezero-core/src/key_value_store.rs +++ b/crates/edgezero-core/src/key_value_store.rs @@ -303,6 +303,21 @@ struct KvCursorEnvelope { prefix: String, } +/// Payload-free classification for a declared KV binding that failed to open. +#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] +#[non_exhaustive] +pub enum KvStoreOpenFailure { + /// The platform rejected the binding's configuration. + #[error("invalid binding configuration")] + InvalidConfiguration, + /// The declared platform binding does not exist or is not linked. + #[error("missing binding")] + MissingBinding, + /// The provider could not open the binding because of an availability failure. + #[error("provider unavailable")] + Unavailable, +} + /// Errors returned by KV store operations. #[derive(Debug, thiserror::Error)] #[non_exhaustive] @@ -320,6 +335,10 @@ pub enum KvError { #[error("key not found: {key}")] NotFound { key: String }, + /// A declared binding failed to open; no name or provider text is retained. + #[error("kv store open failed: {reason}")] + OpenFailed { reason: KvStoreOpenFailure }, + /// A serialization or deserialization error. #[error("serialization error: {0}")] Serialization(#[from] serde_json::Error), @@ -825,7 +844,10 @@ impl From for EdgeError { fn from(err: KvError) -> Self { match err { KvError::NotFound { key } => EdgeError::not_found(format!("kv key: {key}")), - KvError::Unavailable => EdgeError::service_unavailable("kv store unavailable"), + KvError::Unavailable + | KvError::OpenFailed { + reason: KvStoreOpenFailure::Unavailable, + } => EdgeError::service_unavailable("kv store unavailable"), KvError::Validation(msg) => { EdgeError::bad_request(format!("kv validation error: {msg}")) } @@ -833,6 +855,10 @@ impl From for EdgeError { EdgeError::internal(anyhow::anyhow!("kv serialization error: {msg}")) } KvError::Internal(source) => EdgeError::internal(source), + KvError::OpenFailed { + reason: + KvStoreOpenFailure::MissingBinding | KvStoreOpenFailure::InvalidConfiguration, + } => EdgeError::internal(err), KvError::Unsupported { operation } => EdgeError::not_implemented(format!( "kv operation not supported by backend: {operation}" )), @@ -1010,12 +1036,46 @@ mod tests { count: i32, } + struct Failed(KvStoreOpenFailure); + // In-memory store with TTL support for contract testing. // Uses `SystemTime` instead of `Instant` for WASM compatibility. struct MockStore { data: Mutex)>>, } + #[async_trait(?Send)] + impl KvStore for Failed { + async fn delete(&self, _key: &str) -> Result<(), KvError> { + Err(KvError::OpenFailed { reason: self.0 }) + } + async fn exists(&self, _key: &str) -> Result { + Err(KvError::OpenFailed { reason: self.0 }) + } + async fn get_bytes(&self, _key: &str) -> Result, KvError> { + Err(KvError::OpenFailed { reason: self.0 }) + } + async fn list_keys_page( + &self, + _prefix: &str, + _cursor: Option<&str>, + _limit: usize, + ) -> Result { + Err(KvError::OpenFailed { reason: self.0 }) + } + async fn put_bytes(&self, _key: &str, _value: Bytes) -> Result<(), KvError> { + Err(KvError::OpenFailed { reason: self.0 }) + } + async fn put_bytes_with_ttl( + &self, + _key: &str, + _value: Bytes, + _ttl: Duration, + ) -> Result<(), KvError> { + Err(KvError::OpenFailed { reason: self.0 }) + } + } + #[async_trait(?Send)] impl KvStore for MockStore { async fn delete(&self, key: &str) -> Result<(), KvError> { @@ -1100,6 +1160,91 @@ mod tests { KvHandle::new(Arc::new(MockStore::new())) } + #[test] + fn declared_kv_open_failure_retains_binding_and_recovers() { + use crate::response::IntoResponse as _; + use crate::store_registry::KvRegistry; + + block_on(async { + for (reason, status) in [ + ( + KvStoreOpenFailure::MissingBinding, + StatusCode::INTERNAL_SERVER_ERROR, + ), + ( + KvStoreOpenFailure::InvalidConfiguration, + StatusCode::INTERNAL_SERVER_ERROR, + ), + ( + KvStoreOpenFailure::Unavailable, + StatusCode::SERVICE_UNAVAILABLE, + ), + ] { + let registry = KvRegistry::single_id( + "credential-bearing-store-token".to_owned(), + KvHandle::new(Arc::new(Failed(reason))), + ); + assert!(registry.named("undeclared").is_none()); + let handle = registry.default().expect("declared binding retained"); + for error in [ + handle.get_bytes("key").await.unwrap_err(), + handle.get::("key").await.unwrap_err(), + handle.exists("key").await.unwrap_err(), + handle + .put_bytes("key", Bytes::from_static(b"value")) + .await + .unwrap_err(), + handle + .put("key", &serde_json::json!({"value":1_i32})) + .await + .unwrap_err(), + handle + .put_bytes_with_ttl( + "key", + Bytes::from_static(b"value"), + Duration::from_mins(1), + ) + .await + .unwrap_err(), + handle.delete("key").await.unwrap_err(), + handle.list_keys_page("", None, 1).await.unwrap_err(), + ] { + assert!( + matches!(error, KvError::OpenFailed { reason: actual } if actual == reason) + ); + assert!(!error.to_string().contains("credential-bearing-store-token")); + let edge = EdgeError::from(error); + assert_eq!(edge.status(), status); + let response = edge.into_response().unwrap(); + let wire = response.into_body().into_bytes_bounded(1024).await.unwrap(); + assert!( + !String::from_utf8_lossy(&wire).contains("credential-bearing-store-token") + ); + } + // A fresh request gets a fresh binding, not an availability verdict retained by id. + let restored = KvRegistry::single_id( + "credential-bearing-store-token".to_owned(), + KvHandle::new(Arc::new(MockStore::new())), + ); + let recovered = restored.default().unwrap(); + recovered + .put_bytes("key", Bytes::from_static(b"restored")) + .await + .unwrap(); + assert_eq!( + recovered.get_bytes("key").await.unwrap(), + Some(Bytes::from_static(b"restored")) + ); + assert_eq!( + recovered.list_keys_page("", None, 1).await.unwrap().keys, + ["key"] + ); + recovered.delete("key").await.unwrap(); + assert!(!recovered.exists("key").await.unwrap()); + } + }); + } + #[test] fn delete_missing_key_is_ok() { let kv = handle(); diff --git a/crates/edgezero-core/src/lib.rs b/crates/edgezero-core/src/lib.rs index 66aa85456..2533f530b 100644 --- a/crates/edgezero-core/src/lib.rs +++ b/crates/edgezero-core/src/lib.rs @@ -24,17 +24,25 @@ pub mod config_store; pub mod context; pub mod env_config; pub mod error; +pub mod error_detail; pub mod extractor; pub mod handler; pub mod http; +pub mod ingress; pub mod introspection; pub mod key_value_store; +pub mod logging; pub mod manifest; pub mod middleware; +pub mod outbound; pub mod params; -pub mod proxy; +pub mod platform; pub mod responder; pub mod response; +pub mod response_egress; +#[doc(hidden)] +pub mod response_egress_framing; +pub mod response_headers; pub mod router; pub mod secret_store; pub mod store_registry; @@ -42,5 +50,131 @@ pub mod store_registry; /// module docs. Enable via the `test-utils` feature in `[dev-dependencies]`. #[cfg(any(test, feature = "test-utils"))] pub mod test_env; +pub mod time; + +pub use body::{Body, BodyPayload, BodyStream}; +pub use compression::{ + BROTLI_DECODER_FIXED_CHARGE_BYTES, ContentEncoding, FLATE_DECODER_FIXED_CHARGE_BYTES, + PassthroughReason, brotli_decoder_memory_charge, classify_content_encoding, + decode_brotli_stream, decode_deflate_stream, decode_gzip_stream, +}; +pub use config_store::{ + BoundedStoreRead, ConfigExtractionLimits, ConfigValue, DEFAULT_CONFIG_BACKEND_BYTES, + DEFAULT_CONFIG_BLOB_BYTES, DEFAULT_CONFIG_EXTRACTION_BYTES, DEFAULT_CONFIG_EXTRACTION_TIMEOUT, + DEFAULT_CONFIG_JSON_DEPTH, DEFAULT_CONFIG_JSON_NODES, DEFAULT_CONFIG_SECRET_BYTES, +}; pub use edgezero_macros::{AppConfig, action, app}; +pub use error::{ + BadGatewayDecodeReason, BadGatewayReason, BudgetSource, EdgeError, RequestBodyOverflowOrigin, + ResponseLimitReason, StoreExtractionReason, +}; +pub use error_detail::{ + AxumFailureCause, AxumFailureKind, CloudflareFailureCause, FastlyFailureCause, + ProviderFailureAdapter, ProviderFailureDetail, ProviderFailurePhase, SpinDnsRcode, + SpinFailureCause, SpinFieldSize, SpinTimeoutOption, +}; +pub use ingress::{ + AdmissionDecision, AdmittedIngress, BufferedIngressResponse, DEFAULT_INBOUND_READ_BUDGET, + DEFAULT_MAX_REQUEST_HEADER_BYTES, DEFAULT_MAX_REQUEST_HEADER_COUNT, + DEFAULT_MAX_REQUEST_TARGET_BYTES, IngressAdmissionOutcome, IngressBeginOutcome, + IngressDispatchOutcome, IngressFraming, IngressGrant, IngressHead, IngressHeadAccounting, + IngressHeadLimits, IngressHeadParts, PreparedIngress, +}; +pub use logging::{BOOT_LOG_LEVEL, BOOT_LOG_TARGET, resolve_logging_level}; +pub use manifest::{ + AtomicHost, BakedManifest, Capability, CapabilitySupport, HostParseError, HostPat, + ManifestCapabilities, ManifestContract, ManifestOutboundCapability, Port, Scheme, + canonicalize_outbound_host, +}; +pub use outbound::{ + ContentTypeIneligibility, ContentTypeProvenance, DEFAULT_MAX_DECODER_BYTES, + DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_OUTBOUND_REQUEST_BODY_BYTES, HttpClient, + IndependentOutboundProgress, OutboundBatch, OutboundBatchAccumulator, OutboundBatchCheck, + OutboundBatchContext, OutboundBatchDriverEvent, OutboundBatchFailure, + OutboundBatchInsertFailure, OutboundBatchItem, OutboundBatchMergeFailure, OutboundBatchNext, + OutboundBatchResults, OutboundBatchTermination, OutboundCachePolicy, OutboundCollectedSlot, + OutboundCropFocus, OutboundDispatchRequest, OutboundExchangeOwner, OutboundFollowFailure, + OutboundFollowResult, OutboundFuture, OutboundHead, OutboundHeaderFidelity, + OutboundHeaderInput, OutboundHttpClient, OutboundImageFormat, OutboundImageOptimizerOptions, + OutboundImageOptimizerRegion, OutboundLocationEvidence, OutboundPending, OutboundPreparedBatch, + OutboundProgressRuntime, OutboundProxyMarker, OutboundRedirectAction, OutboundRedirectContext, + OutboundRedirectLimit, OutboundRedirectStopReason, OutboundRequest, OutboundRequestParts, + OutboundResizeFilter, OutboundResponse, OutboundResponseContext, OutboundScope, + OutboundScopeKind, OutboundScopeOwner, OutboundSendMode, OutboundSlotResult, OutboundTask, + OutboundTime, PROXY_HEADER, ResponseBodyDisposition, ResponseBodyPolicy, ResponseDecoding, + ResponseHeadNormalization, ResponseHeaderLimiter, ResponseMode, apply_response_body_policy, + collect_response_stream, collect_response_stream_until_with_clock, + enforce_payload_content_length, limit_decoded_stream, limit_encoded_stream, + normalize_for_dispatch, rechunk_stream, validate_for_dispatch, +}; +pub use platform::{ + HostIngressMemoryAccounting, InboundRequestPopulationBound, MemoryCeiling, MemoryCeilingScope, + MemoryEnvelope, MemoryEnvelopeValidation, MemoryEnvelopeValidationError, MemoryResource, + MemoryValidationIndeterminateReason, PlatformFact, PlatformMetadata, PlatformResourceSource, + PlatformUnknownReason, +}; +pub use response_egress::{ + DEFAULT_RESPONSE_WRITE_BUDGET, DetachedResponseEgressDecision, + DetachedResponseEgressDecisionFactory, DetachedResponseEgressHead, + RESPONSE_EGRESS_FALLBACK_SAFETY_BUDGET, ResponseEgressAttempt, ResponseEgressBeginFailure, + ResponseEgressBodyKind, ResponseEgressCompletion, ResponseEgressDeadline, + ResponseEgressEnvelope, ResponseEgressFallbackDisposition, ResponseEgressFramingFailures, + ResponseEgressHead, ResponseEgressObserver, ResponseEgressObserverHandle, + ResponseEgressOutcome, ResponseEgressPolicy, ResponseEgressPolicyCallback, + ResponseEgressPreparationError, ResponseEgressReport, ResponseEgressResource, + ResponseEgressResourceInstallError, ResponseEgressSourceFailure, + default_response_egress_policy, +}; +#[doc(inline)] +pub use response_egress_framing::ResponseEgressFramingError; +pub use response_headers::{ + MAX_SYNTHESIZED_RESPONSE_HEADER_BYTES, MAX_SYNTHESIZED_RESPONSE_HEADER_COUNT, + SynthesizedResponseHeaders, SynthesizedResponseHeadersError, +}; +pub use router::{ResolvedDispatch, RouteId, RouteInfo, RouteMetadata, RouteResolution}; +pub use time::{ + DEADLINE_FAR_FUTURE, DEFAULT_NO_DEADLINE_BUDGET, Deadline, DispatchBudget, MonotonicClock, + MonotonicInstant, dispatch_budget, +}; + +#[cfg(test)] +mod public_config_extraction_contract_tests { + use std::time::Duration; + + #[test] + fn bounded_config_types_and_defaults_are_root_exports() { + let limits = crate::ConfigExtractionLimits::default(); + let _: crate::BoundedStoreRead = crate::BoundedStoreRead { + backend_bytes: 0, + value: None, + }; + assert_eq!(limits.max_blob_bytes, crate::DEFAULT_CONFIG_BLOB_BYTES); + assert_eq!( + limits.max_backend_bytes, + crate::DEFAULT_CONFIG_BACKEND_BYTES + ); + assert_eq!(limits.max_secret_bytes, crate::DEFAULT_CONFIG_SECRET_BYTES); + assert_eq!(limits.max_json_depth, crate::DEFAULT_CONFIG_JSON_DEPTH); + assert_eq!(limits.max_json_nodes, crate::DEFAULT_CONFIG_JSON_NODES); + assert_eq!( + limits.max_total_bytes, + crate::DEFAULT_CONFIG_EXTRACTION_BYTES + ); + assert_eq!(limits.timeout, crate::DEFAULT_CONFIG_EXTRACTION_TIMEOUT); + assert_eq!(crate::DEADLINE_FAR_FUTURE, Duration::from_hours(168)); + assert_eq!(crate::DEFAULT_NO_DEADLINE_BUDGET, Duration::from_secs(30)); + } +} + +#[cfg(test)] +mod public_response_egress_contract_tests { + #[test] + fn late_bound_resource_types_are_root_exports() { + let (resource, completion) = crate::ResponseEgressCompletion::late_bound::>(); + let _: crate::ResponseEgressResource> = resource; + let _: crate::ResponseEgressResourceInstallError = + crate::ResponseEgressResourceInstallError::Closed; + drop(completion); + } +} diff --git a/crates/edgezero-core/src/logging.rs b/crates/edgezero-core/src/logging.rs new file mode 100644 index 000000000..5b9ce6e33 --- /dev/null +++ b/crates/edgezero-core/src/logging.rs @@ -0,0 +1,76 @@ +//! Shared logging policy for adapter and application-owned backends. + +use std::str::FromStr as _; + +use log::LevelFilter; + +use crate::env_config::EnvConfig; + +/// Default boot filter for managed backends, independent of ordinary logging. +pub const BOOT_LOG_LEVEL: LevelFilter = LevelFilter::Warn; +/// Logical target for startup diagnostics; this is not a platform log endpoint. +pub const BOOT_LOG_TARGET: &str = "edgezero::boot"; + +/// Resolve `EDGEZERO__LOGGING__LEVEL` without installing or reconfiguring a logger. +/// +/// Valid values are case-insensitive `off`, `error`, `warn`, `info`, `debug`, and +/// `trace`. Missing or invalid values fall back to `Info`. The caller supplies +/// platform-resolved environment configuration; this function does not read the +/// process environment or implicitly apply manifest logging settings. +/// +/// Application-owned backends must apply this level to ordinary records, apply +/// their chosen boot filter to [`BOOT_LOG_TARGET`], and set the facade maximum +/// high enough for both. Raising the facade maximum does not override a backend +/// filter. +#[must_use] +#[inline] +pub fn resolve_logging_level(env: &EnvConfig) -> LevelFilter { + env.logging_level() + .and_then(|raw| LevelFilter::from_str(raw).ok()) + .unwrap_or(LevelFilter::Info) +} + +#[cfg(test)] +mod tests { + use log::LevelFilter; + + use crate::env_config::EnvConfig; + + #[test] + fn boot_target_preserves_warnings_without_runtime_verbosity() { + assert_eq!(super::BOOT_LOG_TARGET, "edgezero::boot"); + assert_eq!(super::BOOT_LOG_LEVEL, LevelFilter::Warn); + } + + #[test] + fn resolver_defaults_absent_and_invalid_levels_to_info() { + for raw in [None, Some(""), Some("invalid"), Some(" debug ")] { + let env = EnvConfig::from_vars(raw.map(|level| ("EDGEZERO__LOGGING__LEVEL", level))); + assert_eq!(super::resolve_logging_level(&env), LevelFilter::Info); + } + } + + #[test] + fn resolver_preserves_all_levels_and_case_insensitivity() { + for (raw, expected) in [ + ("off", LevelFilter::Off), + ("error", LevelFilter::Error), + ("warn", LevelFilter::Warn), + ("info", LevelFilter::Info), + ("debug", LevelFilter::Debug), + ("trace", LevelFilter::Trace), + ("DeBuG", LevelFilter::Debug), + ] { + let env = EnvConfig::from_vars([("EDGEZERO__LOGGING__LEVEL", raw)]); + assert_eq!(super::resolve_logging_level(&env), expected); + } + } + + #[test] + fn resolver_does_not_change_global_logging_state() { + let before = log::max_level(); + let env = EnvConfig::from_vars([("EDGEZERO__LOGGING__LEVEL", "trace")]); + assert_eq!(super::resolve_logging_level(&env), LevelFilter::Trace); + assert_eq!(log::max_level(), before); + } +} diff --git a/crates/edgezero-core/src/manifest.rs b/crates/edgezero-core/src/manifest.rs index 69980e946..8de0a91ec 100644 --- a/crates/edgezero-core/src/manifest.rs +++ b/crates/edgezero-core/src/manifest.rs @@ -2,11 +2,268 @@ use log::LevelFilter; use serde::de::Error as DeError; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; +use std::error::Error; +use std::fmt; +use std::net::Ipv6Addr; use std::path::{Path, PathBuf}; +use std::str::FromStr as _; use std::sync::Arc; use std::{env, fs, io}; use validator::{Validate, ValidationError}; +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct AtomicHost { + pub host: HostPat, + pub port: Port, + pub scheme: Scheme, +} + +impl AtomicHost { + fn render_host_and_port(&self, scheme: &str, host: &str) -> String { + let mut rendered = format!("{scheme}://{host}"); + match self.port { + Port::Any => rendered.push_str(":*"), + Port::Exact(port) if port != self.scheme.default_port() => { + rendered.push(':'); + rendered.push_str(&port.to_string()); + } + Port::Exact(_) => {} + } + rendered + } + + #[inline] + #[must_use] + pub fn render_spin_host(&self) -> String { + let scheme = match self.scheme { + Scheme::Http => "http", + Scheme::Https => "https", + }; + let host = match &self.host { + HostPat::Any => "*", + HostPat::Exact(host) => host, + HostPat::WildcardSubdomain(host) => return self.render_wildcard_host(scheme, host), + }; + self.render_host_and_port(scheme, host) + } + + fn render_wildcard_host(&self, scheme: &str, host: &str) -> String { + self.render_host_and_port(scheme, &format!("*.{host}")) + } +} + +/// Compile-time manifest state exposed by macro-generated application hooks. +/// +/// This distinguishes an application without a baked manifest from a corrupt +/// baked contract so capability enforcement cannot fail open. +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub enum BakedManifest { + /// No manifest was baked into this application. + Absent, + /// A baked manifest was present but could not be reconstructed safely. + Malformed(&'static str), + /// A parsed, validated, and finalized baked manifest. + Present(&'static Manifest), +} + +impl BakedManifest { + /// Borrow this baked state as the lifetime-neutral capability-gate input. + #[must_use] + #[inline] + pub fn as_contract(&self) -> ManifestContract<'_> { + match self { + Self::Absent => ManifestContract::None, + Self::Malformed(reason) => ManifestContract::Malformed(reason), + Self::Present(manifest) => ManifestContract::Present(manifest), + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] +pub enum Capability { + BufferedResponseContentLength, + ConfigReadAllocationBounds, + ConfigReadDeadlines, + InboundReadDeadlines, + IngressAdmission, + IngressAdmissionAbort, + LazyStreamedResponsePassthrough, + OutboundAuthorityOverride, + OutboundBatchCancellation, + OutboundBatchCompletionOrder, + OutboundBatchSlotIsolation, + OutboundCacheBypass, + OutboundCompleteResourceAccounting, + OutboundDeadlines, + /// Retains text-free provider causes at existing SDK failure boundaries. + /// Does not recover causes hidden by the provider or qualify hosted transport behavior. + OutboundFailureDiagnostics, + OutboundFlexiblePhaseBudget, + OutboundHeaderFidelity, + OutboundHttp, + OutboundImageOptimizer, + /// Shared explicit redirect policy; transport deadlines and cancellation stay separate. + OutboundRedirectFollowing, + RawIngressFramingValidation, + RawIngressHeadLimits, + ResponseEgressAbort, + ResponseEgressBackpressure, + ResponseEgressCompletion, + ResponseWriteDeadlines, + StreamedUploadDeadlines, +} + +impl Capability { + #[expect( + clippy::allow_attributes, + reason = "the included macro copy is private while the core API is public" + )] + #[allow( + clippy::trivially_copy_pass_by_ref, + reason = "the public capability contract specifies as_str(&self)" + )] + #[inline] + #[must_use] + pub fn as_str(&self) -> &'static str { + match self { + Self::BufferedResponseContentLength => "buffered-response-content-length", + Self::ConfigReadAllocationBounds => "config-read-allocation-bounds", + Self::ConfigReadDeadlines => "config-read-deadlines", + Self::InboundReadDeadlines => "inbound-read-deadlines", + Self::IngressAdmission => "ingress-admission", + Self::IngressAdmissionAbort => "ingress-admission-abort", + Self::LazyStreamedResponsePassthrough => "lazy-streamed-response-passthrough", + Self::OutboundAuthorityOverride => "outbound-authority-override", + Self::OutboundBatchCancellation => "outbound-batch-cancellation", + Self::OutboundBatchCompletionOrder => "outbound-batch-completion-order", + Self::OutboundBatchSlotIsolation => "outbound-batch-slot-isolation", + Self::OutboundCacheBypass => "outbound-cache-bypass", + Self::OutboundCompleteResourceAccounting => "outbound-complete-resource-accounting", + Self::OutboundDeadlines => "outbound-deadlines", + Self::OutboundFailureDiagnostics => "outbound-failure-diagnostics", + Self::OutboundFlexiblePhaseBudget => "outbound-flexible-phase-budget", + Self::OutboundHeaderFidelity => "outbound-header-fidelity", + Self::OutboundHttp => "outbound-http", + Self::OutboundImageOptimizer => "outbound-image-optimizer", + Self::OutboundRedirectFollowing => "outbound-redirect-following", + Self::RawIngressFramingValidation => "raw-ingress-framing-validation", + Self::RawIngressHeadLimits => "raw-ingress-head-limits", + Self::ResponseEgressAbort => "response-egress-abort", + Self::ResponseEgressBackpressure => "response-egress-backpressure", + Self::ResponseEgressCompletion => "response-egress-completion", + Self::ResponseWriteDeadlines => "response-write-deadlines", + Self::StreamedUploadDeadlines => "streamed-upload-deadlines", + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum CapabilitySupport { + BestEffort, + BoundedCooperative, + Native, + Unsupported, +} + +/// Manifest input accepted by runtime capability gates. +/// +/// File-backed manifests may be borrowed for any lifetime, while a baked +/// manifest converts through [`BakedManifest::as_contract`]. +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub enum ManifestContract<'manifest> { + /// A manifest contract exists but cannot be verified. + Malformed(&'static str), + /// No capability contract exists. + None, + /// A parsed and validated manifest contract. + Present(&'manifest Manifest), +} + +impl<'manifest> ManifestContract<'manifest> { + /// Convert an optional file-backed manifest reference into a contract. + #[must_use] + #[inline] + pub fn from_opt(manifest: Option<&'manifest Manifest>) -> Self { + manifest.map_or(Self::None, Self::Present) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum HostParseError { + Empty, + FragmentNotAllowed, + InvalidHost, + InvalidPort, + InvalidScheme, + InvalidWildcard, + MissingAuthority, + NonAsciiHost, + PathNotAllowed, + QueryNotAllowed, + UserinfoNotAllowed, + Whitespace, +} + +impl fmt::Display for HostParseError { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Empty => "host entry is empty", + Self::FragmentNotAllowed => "fragment is not allowed", + Self::InvalidHost => "host is invalid", + Self::InvalidPort => "port must be * or 1..=65535", + Self::InvalidScheme => "scheme must be http or https", + Self::InvalidWildcard => "wildcard is invalid", + Self::MissingAuthority => "authority is required", + Self::NonAsciiHost => "host must be ASCII", + Self::PathNotAllowed => "path is not allowed", + Self::QueryNotAllowed => "query is not allowed", + Self::UserinfoNotAllowed => "userinfo is not allowed", + Self::Whitespace => "whitespace is not allowed", + }) + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "HostParseError has no source or backtrace data beyond Display" +)] +impl Error for HostParseError {} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum HostPat { + Any, + Exact(String), + WildcardSubdomain(String), +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Port { + Any, + Exact(u16), +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Scheme { + Http, + Https, +} + +impl Scheme { + const fn default_port(self) -> u16 { + match self { + Self::Http => 80, + Self::Https => 443, + } + } +} + pub struct ManifestLoader { manifest: Arc, } @@ -17,8 +274,7 @@ impl ManifestLoader { #[inline] pub fn from_path(path: &Path) -> Result { let contents = fs::read_to_string(path)?; - let mut manifest: Manifest = toml::from_str(&contents) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; + let mut manifest = deserialize_manifest(&contents)?; let cwd = env::current_dir()?; let root_path = resolve_root_path(path, &cwd); manifest.root = Some(root_path); @@ -71,8 +327,7 @@ impl ManifestLoader { /// Returns an [`io::Error`] if `contents` is not valid TOML or fails manifest validation. #[inline] pub fn try_load_from_str(contents: &str) -> Result { - let mut manifest: Manifest = toml::from_str(contents) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; + let mut manifest = deserialize_manifest(contents)?; manifest .validate() .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; @@ -84,6 +339,7 @@ impl ManifestLoader { } #[derive(Debug, Deserialize, Serialize, Validate)] +#[serde(deny_unknown_fields)] #[validate(schema(function = "validate_manifest_adapter_keys_case_unique"))] #[expect( clippy::partial_pub_fields, @@ -98,6 +354,9 @@ pub struct Manifest { pub app: ManifestApp, #[serde(default)] #[validate(nested)] + pub capabilities: ManifestCapabilities, + #[serde(default)] + #[validate(nested)] pub environment: ManifestEnvironment, #[serde(default)] #[validate(nested)] @@ -114,6 +373,29 @@ pub struct Manifest { pub triggers: ManifestTriggers, } +#[derive(Debug, Default, Deserialize, Serialize, Validate)] +#[serde(deny_unknown_fields)] +#[validate(schema(function = "validate_capabilities_disjoint"))] +#[non_exhaustive] +pub struct ManifestCapabilities { + #[serde(default)] + pub optional: Vec, + #[serde(default)] + #[validate(nested)] + pub outbound: ManifestOutboundCapability, + #[serde(default)] + pub required: Vec, +} + +#[derive(Debug, Default, Deserialize, Serialize, Validate)] +#[serde(deny_unknown_fields)] +#[non_exhaustive] +pub struct ManifestOutboundCapability { + #[serde(default)] + #[validate(length(min = 1_u64), custom(function = "validate_outbound_hosts"))] + pub hosts: Option>, +} + impl Manifest { /// Look up a `[adapters.]` entry by adapter name, matching /// case-insensitively against the manifest's declared keys. @@ -186,6 +468,37 @@ impl Manifest { self.logging_resolved = resolved; } + /// Parse, validate, and finalize JSON emitted by the `app!` macro. + /// + /// This is macro-support API. Call it at most once per application process; + /// a successful call intentionally leaks one manifest so generated hooks can + /// expose it for the process lifetime. + #[doc(hidden)] + #[inline] + #[must_use] + pub fn from_baked_json(json: &'static str) -> BakedManifest { + let value: serde_json::Value = match serde_json::from_str(json) { + Ok(value) => value, + Err(_error) => { + return BakedManifest::Malformed("baked manifest did not parse"); + } + }; + if reject_misplaced_capabilities_json(&value).is_err() { + return BakedManifest::Malformed("baked manifest has misplaced capabilities"); + } + let mut manifest: Self = match serde_json::from_value(value) { + Ok(manifest) => manifest, + Err(_error) => { + return BakedManifest::Malformed("baked manifest did not parse"); + } + }; + if manifest.validate().is_err() { + return BakedManifest::Malformed("baked manifest failed validation"); + } + manifest.finalize(); + BakedManifest::Present(Box::leak(Box::new(manifest))) + } + #[must_use] #[inline] pub fn logging_for(&self, adapter: &str) -> Option<&ResolvedLoggingConfig> { @@ -249,6 +562,9 @@ pub struct ManifestHttpTrigger { #[serde(rename = "body-mode")] #[serde(default)] pub body_mode: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[validate(length(min = 1_u64))] + pub class: Option, #[serde(default)] #[validate(length(min = 1_u64))] pub description: Option, @@ -363,7 +679,7 @@ pub struct ManifestAdapter { /// Catch-all for any sub-table other than the four canonical ones /// (`adapter`, `build`, `commands`, `logging`). The pre-rewrite /// `[adapters..stores.*]` tables land here and are rejected by - /// [`validate_manifest_adapter`] with the migration-guide message. + /// `validate_manifest_adapter` with the migration-guide message. #[serde(flatten)] pub legacy: BTreeMap, #[serde(default)] @@ -773,6 +1089,83 @@ impl serde::Serialize for LogLevel { } } +/// Canonicalizes one outbound host declaration into atomic scheme/host/port entries. +/// +/// # Errors +/// Returns [`HostParseError`] when `entry` is outside the manifest host grammar. +#[inline] +pub fn canonicalize_outbound_host(entry: &str) -> Result, HostParseError> { + if entry.is_empty() { + return Err(HostParseError::Empty); + } + if entry.chars().any(char::is_whitespace) { + return Err(HostParseError::Whitespace); + } + if entry == "*" { + return Ok(vec![ + AtomicHost { + host: HostPat::Any, + port: Port::Any, + scheme: Scheme::Http, + }, + AtomicHost { + host: HostPat::Any, + port: Port::Any, + scheme: Scheme::Https, + }, + ]); + } + + let (scheme, authority) = parse_scheme(entry)?; + if authority.is_empty() { + return Err(HostParseError::MissingAuthority); + } + if authority.contains('@') { + return Err(HostParseError::UserinfoNotAllowed); + } + if authority.contains('/') { + return Err(HostParseError::PathNotAllowed); + } + if authority.contains('?') { + return Err(HostParseError::QueryNotAllowed); + } + if authority.contains('#') { + return Err(HostParseError::FragmentNotAllowed); + } + if !authority.is_ascii() { + return Err(HostParseError::NonAsciiHost); + } + + let (host, port) = parse_authority(authority, scheme)?; + Ok(vec![AtomicHost { host, port, scheme }]) +} + +/// Canonicalizes the complete outbound host declaration into a sorted, duplicate-free list. +/// +/// An absent declaration retains Spin's HTTPS-only wildcard default. +/// +/// # Errors +/// Returns [`HostParseError`] when any declared host is outside the portable host grammar. +#[inline] +pub fn canonicalize_outbound_hosts( + hosts: Option<&[String]>, +) -> Result, HostParseError> { + let mut canonical = BTreeSet::new(); + match hosts { + Some(entries) => { + for entry in entries { + for host in canonicalize_outbound_host(entry)? { + canonical.insert(host.render_spin_host()); + } + } + } + None => { + canonical.insert(String::from("https://*:*")); + } + } + Ok(canonical.into_iter().collect()) +} + /// Serialize a `[[environment.secrets]]` list without exposing `value`. /// Secret bindings share `ManifestBinding` with variables, whose `value` /// is safe to emit; secret values must never appear in manifest output. @@ -815,6 +1208,214 @@ fn resolve_root_path(path: &Path, cwd: &Path) -> PathBuf { } } +fn deserialize_manifest(contents: &str) -> Result { + let value: toml::Value = toml::from_str(contents) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; + reject_misplaced_capabilities(&value) + .map_err(|message| io::Error::new(io::ErrorKind::InvalidData, message))?; + value + .try_into() + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error)) +} + +#[must_use] +pub(crate) fn is_reserved_capabilities_key(key: &str) -> bool { + key.eq_ignore_ascii_case("capabilities") +} + +fn parse_authority(authority: &str, scheme: Scheme) -> Result<(HostPat, Port), HostParseError> { + if let Some(bracketed) = authority.strip_prefix('[') { + let Some((address, remainder)) = bracketed.split_once(']') else { + return Err(HostParseError::InvalidHost); + }; + let ipv6 = Ipv6Addr::from_str(address).map_err(|_error| HostParseError::InvalidHost)?; + let port = if remainder.is_empty() { + Port::Exact(scheme.default_port()) + } else if let Some(raw_port) = remainder.strip_prefix(':') { + parse_port(raw_port)? + } else { + return Err(HostParseError::InvalidHost); + }; + return Ok((HostPat::Exact(format!("[{ipv6}]")), port)); + } + + if authority.contains(['[', ']']) || authority.matches(':').count() > 1 { + return Err(HostParseError::InvalidHost); + } + let (raw_host, raw_port) = match authority.split_once(':') { + Some((host, port)) => (host, Some(port)), + None => (authority, None), + }; + let host = parse_host_pattern(raw_host)?; + let port = raw_port.map_or(Ok(Port::Exact(scheme.default_port())), parse_port)?; + Ok((host, port)) +} + +fn parse_host_pattern(host: &str) -> Result { + if host == "*" { + return Ok(HostPat::Any); + } + if host.contains('*') { + let Some(suffix) = host.strip_prefix("*.") else { + return Err(HostParseError::InvalidWildcard); + }; + if suffix.contains('*') || !is_valid_dns_name(suffix) { + return Err(if suffix.contains('*') { + HostParseError::InvalidWildcard + } else { + HostParseError::InvalidHost + }); + } + return Ok(HostPat::WildcardSubdomain(suffix.to_ascii_lowercase())); + } + if !is_valid_dns_name(host) { + return Err(HostParseError::InvalidHost); + } + Ok(HostPat::Exact(host.to_ascii_lowercase())) +} + +fn parse_port(port: &str) -> Result { + if port == "*" { + return Ok(Port::Any); + } + if !port.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(HostParseError::InvalidPort); + } + let parsed_port = port + .parse::() + .map_err(|_error| HostParseError::InvalidPort)?; + if parsed_port == 0 { + return Err(HostParseError::InvalidPort); + } + Ok(Port::Exact(parsed_port)) +} + +fn parse_scheme(entry: &str) -> Result<(Scheme, &str), HostParseError> { + let Some((raw_scheme, authority)) = entry.split_once("://") else { + return Ok((Scheme::Https, entry)); + }; + let parsed_scheme = if raw_scheme.eq_ignore_ascii_case("http") { + Scheme::Http + } else if raw_scheme.eq_ignore_ascii_case("https") { + Scheme::Https + } else { + return Err(HostParseError::InvalidScheme); + }; + Ok((parsed_scheme, authority)) +} + +fn is_valid_dns_name(host: &str) -> bool { + if host.is_empty() || host.len() > 253 { + return false; + } + host.split('.').all(|label| { + !label.is_empty() + && label.len() <= 63 + && !label.starts_with('-') + && !label.ends_with('-') + && label + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + }) +} + +fn validate_capabilities_disjoint( + capabilities: &ManifestCapabilities, +) -> Result<(), ValidationError> { + let mut required = BTreeSet::new(); + if capabilities + .required + .iter() + .any(|capability| !required.insert(capability.as_str())) + { + return Err(ValidationError::new("capability_required_duplicate")); + } + + let mut optional = BTreeSet::new(); + if capabilities + .optional + .iter() + .any(|capability| !optional.insert(capability.as_str())) + { + return Err(ValidationError::new("capability_optional_duplicate")); + } + if required + .iter() + .any(|capability| optional.contains(capability)) + { + return Err(ValidationError::new("capability_required_optional_overlap")); + } + Ok(()) +} + +fn validate_outbound_hosts(hosts: &[String]) -> Result<(), ValidationError> { + for host in hosts { + if let Err(error) = canonicalize_outbound_host(host) { + let mut validation_error = ValidationError::new("outbound_host"); + validation_error.message = Some(error.to_string().into()); + return Err(validation_error); + } + } + Ok(()) +} + +pub(crate) fn reject_misplaced_capabilities(value: &toml::Value) -> Result<(), &'static str> { + fn walk(value: &toml::Value, top_level: bool) -> Result<(), &'static str> { + match value { + toml::Value::Array(values) => { + for array_value in values { + walk(array_value, false)?; + } + } + toml::Value::Table(table) => { + for (key, nested_value) in table { + if is_reserved_capabilities_key(key) && (!top_level || key != "capabilities") { + return Err( + "capabilities is reserved for the exact lowercase top-level table", + ); + } + walk(nested_value, false)?; + } + } + toml::Value::Boolean(_) + | toml::Value::Datetime(_) + | toml::Value::Float(_) + | toml::Value::Integer(_) + | toml::Value::String(_) => {} + } + Ok(()) + } + + walk(value, true) +} + +fn reject_misplaced_capabilities_json(value: &serde_json::Value) -> Result<(), ()> { + fn walk(value: &serde_json::Value, top_level: bool) -> Result<(), ()> { + match value { + serde_json::Value::Array(values) => { + for array_value in values { + walk(array_value, false)?; + } + } + serde_json::Value::Object(object) => { + for (key, nested_value) in object { + if is_reserved_capabilities_key(key) && (!top_level || key != "capabilities") { + return Err(()); + } + walk(nested_value, false)?; + } + } + serde_json::Value::Bool(_) + | serde_json::Value::Null + | serde_json::Value::Number(_) + | serde_json::Value::String(_) => {} + } + Ok(()) + } + + walk(value, true) +} + /// Validates a single `[adapters..adapter]` block. The portable /// manifest model lists the declared fields explicitly; an unknown key /// would otherwise be silently dropped by serde, so we surface it as a @@ -1075,6 +1676,707 @@ name = "API_TOKEN" env = "APP_TOKEN" "#; + #[test] + fn buffered_response_content_length_capability_round_trip() { + let name = "buffered-response-content-length"; + let capability: Capability = + serde_json::from_value(serde_json::json!(name)).expect("buffered framing capability"); + assert_eq!(capability.as_str(), name); + assert_eq!( + serde_json::to_value(capability).expect("serialize capability"), + serde_json::json!(name) + ); + } + + #[test] + fn outbound_image_optimizer_capability_round_trip() { + let name = "outbound-image-optimizer"; + let capability: Capability = serde_json::from_value(serde_json::json!(name)).unwrap(); + assert_eq!(capability, Capability::OutboundImageOptimizer); + assert_eq!(capability.as_str(), name); + assert_eq!( + serde_json::to_value(capability).unwrap(), + serde_json::json!(name) + ); + } + + #[test] + fn outbound_redirect_following_capability_round_trip() { + let name = "outbound-redirect-following"; + let capability: Capability = + serde_json::from_value(serde_json::json!(name)).expect("redirect capability"); + assert_eq!(capability, Capability::OutboundRedirectFollowing); + assert_eq!(capability.as_str(), name); + assert_eq!( + serde_json::to_value(capability).expect("serialize redirect capability"), + serde_json::json!(name) + ); + for disposition in ["required", "optional"] { + let source = format!("[capabilities]\n{disposition} = [\"{name}\"]\n"); + let loader = ManifestLoader::try_load_from_str(&source).expect("redirect manifest"); + let serialized = toml::to_string(loader.manifest()).expect("serialize manifest"); + let reparsed = ManifestLoader::try_load_from_str(&serialized).expect("round trip"); + let declared = &reparsed.manifest().capabilities; + let (selected, other) = if disposition == "required" { + (&declared.required, &declared.optional) + } else { + (&declared.optional, &declared.required) + }; + assert_eq!(selected, &[capability]); + assert!(other.is_empty()); + } + } + + #[test] + fn provider_failure_diagnostics_capability_round_trip() { + let name = "outbound-failure-diagnostics"; + let capability: Capability = + serde_json::from_value(serde_json::json!(name)).expect("diagnostic capability"); + assert_eq!(capability.as_str(), name); + assert_eq!( + serde_json::to_value(capability).unwrap(), + serde_json::json!(name) + ); + + for disposition in ["required", "optional"] { + let source = format!("[capabilities]\n{disposition} = [\"{name}\"]\n"); + let loader = ManifestLoader::try_load_from_str(&source).expect("diagnostic manifest"); + let serialized = toml::to_string(loader.manifest()).expect("serialize manifest"); + let reparsed = ManifestLoader::try_load_from_str(&serialized).expect("round trip"); + let declared = &reparsed.manifest().capabilities; + let (selected, other) = if disposition == "required" { + (&declared.required, &declared.optional) + } else { + (&declared.optional, &declared.required) + }; + assert_eq!(selected, &[capability]); + assert!(other.is_empty()); + } + } + + #[test] + #[expect( + clippy::too_many_lines, + reason = "the exhaustive capability parse-and-validation table is clearer in one test" + )] + fn capability_manifest_rejects_unknown_duplicate_and_overlap() { + let source = r#" +[capabilities] +required = [ + "buffered-response-content-length", + "config-read-allocation-bounds", + "config-read-deadlines", + "inbound-read-deadlines", + "ingress-admission", + "ingress-admission-abort", + "lazy-streamed-response-passthrough", + "outbound-authority-override", + "outbound-batch-cancellation", + "outbound-batch-completion-order", + "outbound-batch-slot-isolation", + "outbound-cache-bypass", + "outbound-complete-resource-accounting", + "outbound-deadlines", + "outbound-failure-diagnostics", + "outbound-flexible-phase-budget", + "outbound-header-fidelity", + "outbound-http", + "outbound-image-optimizer", + "outbound-redirect-following", + "raw-ingress-framing-validation", + "raw-ingress-head-limits", + "response-egress-abort", + "response-egress-backpressure", + "response-egress-completion", + "response-write-deadlines", + "streamed-upload-deadlines", +] +optional = [] + +[capabilities.outbound] +hosts = ["*", "HTTPS://Example.COM", "api.example.com:8443"] +"#; + let loader = ManifestLoader::try_load_from_str(source).expect("capability manifest"); + let expected = [ + Capability::BufferedResponseContentLength, + Capability::ConfigReadAllocationBounds, + Capability::ConfigReadDeadlines, + Capability::InboundReadDeadlines, + Capability::IngressAdmission, + Capability::IngressAdmissionAbort, + Capability::LazyStreamedResponsePassthrough, + Capability::OutboundAuthorityOverride, + Capability::OutboundBatchCancellation, + Capability::OutboundBatchCompletionOrder, + Capability::OutboundBatchSlotIsolation, + Capability::OutboundCacheBypass, + Capability::OutboundCompleteResourceAccounting, + Capability::OutboundDeadlines, + Capability::OutboundFailureDiagnostics, + Capability::OutboundFlexiblePhaseBudget, + Capability::OutboundHeaderFidelity, + Capability::OutboundHttp, + Capability::OutboundImageOptimizer, + Capability::OutboundRedirectFollowing, + Capability::RawIngressFramingValidation, + Capability::RawIngressHeadLimits, + Capability::ResponseEgressAbort, + Capability::ResponseEgressBackpressure, + Capability::ResponseEgressCompletion, + Capability::ResponseWriteDeadlines, + Capability::StreamedUploadDeadlines, + ]; + assert_eq!(loader.manifest().capabilities.required, expected); + assert!(loader.manifest().capabilities.optional.is_empty()); + assert_eq!( + loader.manifest().capabilities.outbound.hosts.as_deref(), + Some( + ["*", "HTTPS://Example.COM", "api.example.com:8443"] + .map(String::from) + .as_slice() + ) + ); + for (capability, expected_name) in expected.iter().zip([ + "buffered-response-content-length", + "config-read-allocation-bounds", + "config-read-deadlines", + "inbound-read-deadlines", + "ingress-admission", + "ingress-admission-abort", + "lazy-streamed-response-passthrough", + "outbound-authority-override", + "outbound-batch-cancellation", + "outbound-batch-completion-order", + "outbound-batch-slot-isolation", + "outbound-cache-bypass", + "outbound-complete-resource-accounting", + "outbound-deadlines", + "outbound-failure-diagnostics", + "outbound-flexible-phase-budget", + "outbound-header-fidelity", + "outbound-http", + "outbound-image-optimizer", + "outbound-redirect-following", + "raw-ingress-framing-validation", + "raw-ingress-head-limits", + "response-egress-abort", + "response-egress-backpressure", + "response-egress-completion", + "response-write-deadlines", + "streamed-upload-deadlines", + ]) { + assert_eq!(capability.as_str(), expected_name); + } + + let serialized = toml::to_string(loader.manifest()).expect("serialize manifest"); + let reparsed = ManifestLoader::try_load_from_str(&serialized).expect("round trip"); + assert_eq!(reparsed.manifest().capabilities.required, expected); + assert_eq!( + reparsed.manifest().capabilities.outbound.hosts.as_deref(), + loader.manifest().capabilities.outbound.hosts.as_deref() + ); + + for invalid in [ + "[capabilities]\nrequired = [\"outbound-retries\"]\n", + "[capabilities]\nrequired = [\"Outbound-Http\"]\n", + "[capabilities]\nrequire = [\"outbound-http\"]\n", + "[capabilities.outbound]\nhost = [\"*\"]\n", + "[capabilites]\nrequired = [\"outbound-http\"]\n", + "[capability]\nrequired = [\"outbound-http\"]\n", + "[custom]\nvalue = true\n", + "[capabilities]\nrequired = [\"outbound-http\", \"outbound-http\"]\n", + "[capabilities]\noptional = [\"outbound-http\", \"outbound-http\"]\n", + "[capabilities]\nrequired = [\"outbound-http\"]\noptional = [\"outbound-http\"]\n", + "[capabilities.outbound]\nhosts = []\n", + ] { + assert!( + ManifestLoader::try_load_from_str(invalid).is_err(), + "manifest unexpectedly accepted: {invalid}" + ); + } + } + + #[test] + fn capability_manifest_rejects_reserved_key_at_every_depth_and_case() { + let valid = + ManifestLoader::try_load_from_str("[capabilities]\nrequired = [\"outbound-http\"]\n") + .expect("exact lowercase top-level key"); + assert_eq!( + valid.manifest().capabilities.required, + [Capability::OutboundHttp] + ); + + let invalid = [ + "[Capabilities]\nrequired = []\n", + "[CAPABILITIES]\nrequired = []\n", + "[capabilitieS]\nrequired = []\n", + "[app.capabilities]\nrequired = []\n", + "[app.Capabilities]\nrequired = []\n", + "[[triggers.http]]\npath = \"/\"\n[triggers.http.CAPABILITIES]\nrequired = []\n", + "[[environment.variables]]\nname = \"X\"\n[environment.variables.capabilitieS]\nrequired = []\n", + "[adapters.axum.build.cApAbIlItIeS]\nrequired = []\n", + "[app]\nlayers = [{ capabilities = { required = [] } }]\n", + "[app]\nlayers = [[{ CaPaBiLiTiEs = { required = [] } }]]\n", + "[capabilities.outbound.capabilities]\nrequired = []\n", + ]; + + for source in invalid { + assert!( + ManifestLoader::try_load_from_str(source).is_err(), + "reserved key unexpectedly accepted: {source}" + ); + } + } + + #[test] + #[expect( + clippy::too_many_lines, + reason = "the required grammar table keeps accepted and rejected cases together" + )] + fn outbound_host_grammar_table() { + let label_63 = "a".repeat(63); + let name_253 = format!( + "{}.{}.{}.{}", + "a".repeat(63), + "b".repeat(63), + "c".repeat(63), + "d".repeat(61) + ); + let accepted = [ + ( + "*".to_owned(), + vec![ + AtomicHost { + scheme: Scheme::Http, + host: HostPat::Any, + port: Port::Any, + }, + AtomicHost { + scheme: Scheme::Https, + host: HostPat::Any, + port: Port::Any, + }, + ], + vec!["http://*:*", "https://*:*"], + ), + ( + "*.example.com".to_owned(), + vec![AtomicHost { + scheme: Scheme::Https, + host: HostPat::WildcardSubdomain("example.com".to_owned()), + port: Port::Exact(443), + }], + vec!["https://*.example.com"], + ), + ( + "x:8443".to_owned(), + vec![AtomicHost { + scheme: Scheme::Https, + host: HostPat::Exact("x".to_owned()), + port: Port::Exact(8443), + }], + vec!["https://x:8443"], + ), + ( + "x:1".to_owned(), + vec![AtomicHost { + scheme: Scheme::Https, + host: HostPat::Exact("x".to_owned()), + port: Port::Exact(1), + }], + vec!["https://x:1"], + ), + ( + "x:65535".to_owned(), + vec![AtomicHost { + scheme: Scheme::Https, + host: HostPat::Exact("x".to_owned()), + port: Port::Exact(u16::MAX), + }], + vec!["https://x:65535"], + ), + ( + "https://x:443".to_owned(), + vec![AtomicHost { + scheme: Scheme::Https, + host: HostPat::Exact("x".to_owned()), + port: Port::Exact(443), + }], + vec!["https://x"], + ), + ( + "https://[::1]".to_owned(), + vec![AtomicHost { + scheme: Scheme::Https, + host: HostPat::Exact("[::1]".to_owned()), + port: Port::Exact(443), + }], + vec!["https://[::1]"], + ), + ( + "HTTP://[2001:0DB8::1]:80".to_owned(), + vec![AtomicHost { + scheme: Scheme::Http, + host: HostPat::Exact("[2001:db8::1]".to_owned()), + port: Port::Exact(80), + }], + vec!["http://[2001:db8::1]"], + ), + ( + "https://127.0.0.1".to_owned(), + vec![AtomicHost { + scheme: Scheme::Https, + host: HostPat::Exact("127.0.0.1".to_owned()), + port: Port::Exact(443), + }], + vec!["https://127.0.0.1"], + ), + ( + "xn--caf-dma.com".to_owned(), + vec![AtomicHost { + scheme: Scheme::Https, + host: HostPat::Exact("xn--caf-dma.com".to_owned()), + port: Port::Exact(443), + }], + vec!["https://xn--caf-dma.com"], + ), + ( + "HTTPS://Example.COM:*".to_owned(), + vec![AtomicHost { + scheme: Scheme::Https, + host: HostPat::Exact("example.com".to_owned()), + port: Port::Any, + }], + vec!["https://example.com:*"], + ), + ( + format!("https://{label_63}"), + vec![AtomicHost { + scheme: Scheme::Https, + host: HostPat::Exact(label_63), + port: Port::Exact(443), + }], + vec![], + ), + ( + name_253.clone(), + vec![AtomicHost { + scheme: Scheme::Https, + host: HostPat::Exact(name_253), + port: Port::Exact(443), + }], + vec![], + ), + ]; + + for (entry, expected, rendered) in accepted { + let actual = canonicalize_outbound_host(&entry).expect("accepted host"); + assert_eq!(actual, expected, "entry: {entry}"); + if !rendered.is_empty() { + assert_eq!( + actual + .iter() + .map(AtomicHost::render_spin_host) + .collect::>(), + rendered, + "entry: {entry}" + ); + for host in &actual { + assert_eq!( + canonicalize_outbound_host(&host.render_spin_host()), + Ok(vec![host.clone()]), + "rendered host must parse to the same atomic" + ); + } + } + + let source = format!("[capabilities.outbound]\nhosts = [{entry:?}]\n"); + ManifestLoader::try_load_from_str(&source).expect("manifest host validation"); + } + + let label_64 = "a".repeat(64); + let name_254 = format!( + "{}.{}.{}.{}", + "a".repeat(63), + "b".repeat(63), + "c".repeat(63), + "d".repeat(62) + ); + let rejected = [ + (String::new(), HostParseError::Empty, "host entry is empty"), + ( + " x".to_owned(), + HostParseError::Whitespace, + "whitespace is not allowed", + ), + ( + "x.com ".to_owned(), + HostParseError::Whitespace, + "whitespace is not allowed", + ), + ( + "x .com".to_owned(), + HostParseError::Whitespace, + "whitespace is not allowed", + ), + ( + "https:// x".to_owned(), + HostParseError::Whitespace, + "whitespace is not allowed", + ), + ( + "ftp://x".to_owned(), + HostParseError::InvalidScheme, + "scheme must be http or https", + ), + ( + "https://".to_owned(), + HostParseError::MissingAuthority, + "authority is required", + ), + ( + "https://u:p@x".to_owned(), + HostParseError::UserinfoNotAllowed, + "userinfo is not allowed", + ), + ( + "https://x/p".to_owned(), + HostParseError::PathNotAllowed, + "path is not allowed", + ), + ( + "https://x?q".to_owned(), + HostParseError::QueryNotAllowed, + "query is not allowed", + ), + ( + "https://x#f".to_owned(), + HostParseError::FragmentNotAllowed, + "fragment is not allowed", + ), + ( + "ex\u{20ac}ample.com".to_owned(), + HostParseError::NonAsciiHost, + "host must be ASCII", + ), + ( + "caf\u{e9}.com".to_owned(), + HostParseError::NonAsciiHost, + "host must be ASCII", + ), + ( + "ex*ample.com".to_owned(), + HostParseError::InvalidWildcard, + "wildcard is invalid", + ), + ( + "*.*.com".to_owned(), + HostParseError::InvalidWildcard, + "wildcard is invalid", + ), + ( + "a.*.com".to_owned(), + HostParseError::InvalidWildcard, + "wildcard is invalid", + ), + ( + "**.com".to_owned(), + HostParseError::InvalidWildcard, + "wildcard is invalid", + ), + ( + "-x.com".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ( + "x-.com".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ( + "x..com".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ( + "x_y.com".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ( + "x.com.".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + (label_64, HostParseError::InvalidHost, "host is invalid"), + (name_254, HostParseError::InvalidHost, "host is invalid"), + ( + "https://[::g]".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ( + "https://[:::1]".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ( + "https://[12345::]".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ( + "https://[1:2:3:4:5:6:7:8:9]".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ( + "https://[::1".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ( + "https://::1]".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ( + "https://::1".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ( + "https://x:0".to_owned(), + HostParseError::InvalidPort, + "port must be * or 1..=65535", + ), + ( + "https://x:70000".to_owned(), + HostParseError::InvalidPort, + "port must be * or 1..=65535", + ), + ( + "https://x:abc".to_owned(), + HostParseError::InvalidPort, + "port must be * or 1..=65535", + ), + ( + "x:+443".to_owned(), + HostParseError::InvalidPort, + "port must be * or 1..=65535", + ), + ( + "https://x:".to_owned(), + HostParseError::InvalidPort, + "port must be * or 1..=65535", + ), + ( + "ftp:// x/p?q#f".to_owned(), + HostParseError::Whitespace, + "whitespace is not allowed", + ), + ( + "ftp://".to_owned(), + HostParseError::InvalidScheme, + "scheme must be http or https", + ), + ( + "https://u@x/p?q#f".to_owned(), + HostParseError::UserinfoNotAllowed, + "userinfo is not allowed", + ), + ( + "https://x/p?q#f".to_owned(), + HostParseError::PathNotAllowed, + "path is not allowed", + ), + ( + "https://x?q#f".to_owned(), + HostParseError::QueryNotAllowed, + "query is not allowed", + ), + ( + "https://x#caf\u{e9}".to_owned(), + HostParseError::FragmentNotAllowed, + "fragment is not allowed", + ), + ( + "caf\u{e9}*.com:0".to_owned(), + HostParseError::NonAsciiHost, + "host must be ASCII", + ), + ( + "ex*ample_.com:0".to_owned(), + HostParseError::InvalidWildcard, + "wildcard is invalid", + ), + ( + "x_y.com:0".to_owned(), + HostParseError::InvalidHost, + "host is invalid", + ), + ]; + + for (entry, expected, message) in rejected { + let error = canonicalize_outbound_host(&entry).expect_err("rejected host"); + assert_eq!(error, expected, "entry: {entry}"); + assert_eq!(error.to_string(), message, "entry: {entry}"); + assert!(!error.to_string().is_empty()); + + let source = format!("[capabilities.outbound]\nhosts = [{entry:?}]\n"); + assert!( + ManifestLoader::try_load_from_str(&source).is_err(), + "invalid manifest host accepted: {entry}" + ); + } + } + + #[test] + fn outbound_host_default_is_https_only() { + let loader = ManifestLoader::try_load_from_str("").expect("empty manifest"); + assert!(loader.manifest().capabilities.outbound.hosts.is_none()); + + let absent = canonicalize_outbound_host("https://*:*").expect("https-only default"); + assert_eq!( + absent, + [AtomicHost { + scheme: Scheme::Https, + host: HostPat::Any, + port: Port::Any, + }] + ); + assert_eq!(absent[0].render_spin_host(), "https://*:*"); + + let explicit = canonicalize_outbound_host("*").expect("explicit wildcard"); + assert_eq!(explicit.len(), 2); + assert_eq!(explicit[0].scheme, Scheme::Http); + assert_eq!(explicit[1].scheme, Scheme::Https); + assert!(explicit.iter().all(|host| host.port == Port::Any)); + } + + #[test] + fn canonical_outbound_host_set_expands_defaults_and_normalizes_order() { + assert_eq!( + canonicalize_outbound_hosts(None).expect("absent default"), + vec!["https://*:*".to_owned()] + ); + + let hosts = vec![ + "HTTPS://API.EXAMPLE.COM:443".to_owned(), + "*".to_owned(), + "api.example.com".to_owned(), + ]; + assert_eq!( + canonicalize_outbound_hosts(Some(&hosts)).expect("canonical hosts"), + vec![ + "http://*:*".to_owned(), + "https://*:*".to_owned(), + "https://api.example.com".to_owned(), + ] + ); + } + #[test] fn parse_manifest_sample() { let loader = ManifestLoader::load_from_str(SAMPLE); @@ -2083,6 +3385,7 @@ ids = ["default"] let manifest = r#" [[triggers.http]] id = "route-1" +class = "auction" path = "/api/users" methods = ["GET", "POST"] handler = "handlers::users" @@ -2093,6 +3396,7 @@ body-mode = "buffered" let loader = ManifestLoader::load_from_str(manifest); let trigger = &loader.manifest().triggers.http[0]; assert_eq!(trigger.id.as_deref(), Some("route-1")); + assert_eq!(trigger.class.as_deref(), Some("auction")); assert_eq!(trigger.path, "/api/users"); assert_eq!(trigger.methods(), vec!["GET", "POST"]); assert_eq!(trigger.handler.as_deref(), Some("handlers::users")); @@ -2104,6 +3408,19 @@ body-mode = "buffered" assert_eq!(trigger.body_mode, Some(BodyMode::Buffered)); } + #[test] + fn http_trigger_rejects_empty_route_class() { + let manifest = r#" +[[triggers.http]] +class = "" +path = "/api/users" +"#; + let error = ManifestLoader::try_load_from_str(manifest) + .err() + .expect("an empty route class must fail validation"); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + } + // -- Secret store config ----------------------------------------------- #[test] @@ -2251,4 +3568,29 @@ default = "feature__flags" .err() .expect("double-underscore store id must fail validation"); } + + #[test] + fn baked_manifest_states_are_distinct() { + assert!(matches!( + Manifest::from_baked_json("not json"), + BakedManifest::Malformed("baked manifest did not parse") + )); + assert!(matches!( + Manifest::from_baked_json(r#"{"app":{"Capabilities":{}}}"#), + BakedManifest::Malformed("baked manifest has misplaced capabilities") + )); + let baked = Manifest::from_baked_json(r#"{"capabilities":{"required":["outbound-http"]}}"#); + let BakedManifest::Present(manifest) = baked else { + panic!("expected present baked manifest"); + }; + assert_eq!(manifest.capabilities.required, [Capability::OutboundHttp]); + assert!(matches!( + BakedManifest::Absent.as_contract(), + ManifestContract::None + )); + assert!(matches!( + ManifestContract::from_opt(Some(manifest)), + ManifestContract::Present(_) + )); + } } diff --git a/crates/edgezero-core/src/middleware.rs b/crates/edgezero-core/src/middleware.rs index a9edaf2c0..7d5b2a454 100644 --- a/crates/edgezero-core/src/middleware.rs +++ b/crates/edgezero-core/src/middleware.rs @@ -77,8 +77,8 @@ pub struct RequestLogger; impl Middleware for RequestLogger { #[inline] async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result { - let method = ctx.request().method().clone(); - let path = ctx.request().uri().path().to_owned(); + let method = ctx.method().clone(); + let path = ctx.uri().path().to_owned(); let start = Instant::now(); match next.run(ctx).await { diff --git a/crates/edgezero-core/src/outbound.rs b/crates/edgezero-core/src/outbound.rs new file mode 100644 index 000000000..506e72e17 --- /dev/null +++ b/crates/edgezero-core/src/outbound.rs @@ -0,0 +1,2264 @@ +#![expect( + clippy::self_named_module_files, + reason = "the workspace denies both contradictory module-file conventions; retain its self-named-file convention" +)] + +mod batch; +mod dispatch; +mod exchange; +mod head; +mod image_optimizer; +mod progress; +mod redirect; +#[cfg(test)] +mod redirect_tests; +mod scope; +#[cfg(test)] +mod scope_tests; +mod task; + +use std::net::IpAddr; +use std::num::NonZeroU64; +use std::rc::Rc; +use std::str; +use std::sync::Arc; +use std::time::Duration; + +use async_stream::stream; +use bytes::Bytes; +use futures_util::future::LocalBoxFuture; +use futures_util::{Stream, StreamExt as _}; +use serde::Serialize; +use serde::de::DeserializeOwned; +use url::Url; + +use crate::body::{Body, BodyPayload, BodyStream}; +use crate::compression::{ + ContentEncoding, classify_content_encoding, decode_brotli_stream, decode_deflate_stream, + decode_gzip_stream, +}; +use crate::error::{ + BadGatewayDecodeReason, BadGatewayReason, EdgeError, RequestBodyOverflowOrigin, + ResponseLimitReason, +}; +use crate::http::header::{ + CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, HOST, PROXY_AUTHENTICATE, + PROXY_AUTHORIZATION, TE, TRAILER, TRANSFER_ENCODING, UPGRADE, +}; +use crate::http::{ + Authority, HeaderMap, HeaderName, HeaderValue, Method, Request, Response, StatusCode, Uri, + response_builder, +}; +use crate::time::{ + DEADLINE_FAR_FUTURE, Deadline, DispatchBudget, MonotonicClock, MonotonicInstant, +}; + +pub use batch::{ + OutboundBatch, OutboundBatchAccumulator, OutboundBatchCheck, OutboundBatchContext, + OutboundBatchDriverEvent, OutboundBatchFailure, OutboundBatchInsertFailure, OutboundBatchItem, + OutboundBatchMergeFailure, OutboundBatchNext, OutboundBatchResults, OutboundBatchTermination, + OutboundCollectedSlot, OutboundPreparedBatch, +}; +pub(crate) use dispatch::ForwardedReadDeadline; +pub use redirect::{ + OutboundFollowFailure, OutboundFollowResult, OutboundRedirectAction, OutboundRedirectContext, + OutboundRedirectLimit, OutboundRedirectStopReason, +}; +pub(crate) use scope::Revocable; +pub use scope::{ + IndependentOutboundProgress, OutboundProgressRuntime, OutboundScope, OutboundScopeKind, + OutboundScopeOwner, +}; +pub use task::OutboundTask; + +pub const DEFAULT_MAX_DECODER_BYTES: u64 = 32 * 1024 * 1024; +pub const DEFAULT_MAX_RESPONSE_BYTES: u64 = 1024 * 1024; +pub const DEFAULT_OUTBOUND_REQUEST_BODY_BYTES: u64 = 8 * 1024 * 1024; +/// Response header identifying the adapter that completed an outbound request. +pub const PROXY_HEADER: &str = "x-edgezero-proxy"; + +/// An owned local outbound operation; neither its future nor its result needs to be `Send`. +pub type OutboundFuture = LocalBoxFuture<'static, Result>; + +type OutboundWaitFactory = + dyn Fn(Deadline, MonotonicClock) -> OutboundFuture<()> + Send + Sync + 'static; + +/// An application clock paired with an adapter-owned deadline wakeup factory. +/// +/// Clones share the same clock source and factory. There is deliberately no default or inert +/// timer: providers must supply wakeups that work in the application's monotonic time domain. +#[derive(Clone)] +pub struct OutboundTime { + clock: MonotonicClock, + wait: Arc, +} + +/// Local, exactly-once teardown ownership for an outbound exchange or task group. +/// +/// The callback may own `Rc` values and SDK handles. It must perform bounded, nonblocking +/// cleanup. Do not assign the same SDK resource two independent teardown callbacks. +/// When used as pending cancellation, destroying the callback's captures without invoking it +/// must be safe after a successful response handoff; do not capture an armed abort-on-drop guard. +#[must_use] +pub struct OutboundExchangeOwner { + release: Option>, +} + +/// A completion receiver and cancellation owner for work that is already scheduled. +/// +/// This handle does not schedule transport work. Its producer must issue or schedule the +/// exchange before construction, provide a nonblocking receiver, and complete transport/body +/// ownership transfer before publishing success. A successful streamed response owns its own +/// transport cleanup; pending cancellation only cancels/unregisters scheduled work. +#[must_use] +pub struct OutboundPending { + ready: bool, + state: progress::PendingStorage, +} + +/// The facade operation whose effective policies were captured before dispatch. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundSendMode { + Batch, + Begin, + Follow, + Send, +} + +/// One request and the immutable policies captured by the shared facade at invocation. +/// Providers inspect or consume this value, never construct or reset it. +#[derive(Debug)] +pub struct OutboundDispatchRequest { + context: OutboundResponseContext, + request: OutboundRequest, +} + +/// Validated, owned policy and time inputs for one response. +/// +/// The facade supplies this context through prepared dispatch. There is no public constructor; +/// fields cannot be reset, cloned or decomposed. +/// +/// ```compile_fail +/// use edgezero_core::OutboundResponseContext; +/// fn duplicate(context: OutboundResponseContext) { let _ = context.clone(); } +/// ``` +/// ```compile_fail +/// use edgezero_core::OutboundResponseContext; +/// fn reset(context: &mut OutboundResponseContext) { context.entered_at = context.clock().now(); } +/// ``` +/// ```compile_fail +/// use edgezero_core::OutboundResponseContext; +/// let _ = OutboundResponseContext::default(); +/// ``` +#[derive(Debug)] +pub struct OutboundResponseContext { + body_policy: ResponseBodyPolicy, + budget: DispatchBudget, + entered_at: MonotonicInstant, + max_response_header_bytes: Option, + max_response_header_count: Option, + mode: OutboundSendMode, + original_chain_deadline: Option, + proxy_marker: OutboundProxyMarker, + request_method: Method, + scope: OutboundScope, + time: OutboundTime, +} + +/// Whether original field boundaries are available at the adapter boundary. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundHeaderFidelity { + AdapterVisibleFields, + ProviderFieldBoundariesUnknown, +} + +/// One adapter-visible header map, consumed before normalization and payload processing. +pub struct OutboundHeaderInput { + adapter: &'static str, + fidelity: OutboundHeaderFidelity, + headers: HeaderMap, +} + +/// Immutable original Location facts; no raw header value is copied into this evidence. +#[derive(Debug)] +pub struct OutboundLocationEvidence { + fidelity: OutboundHeaderFidelity, + has_non_text: bool, + missing_or_empty: bool, + observed_count: usize, + retained_singleton: bool, +} + +/// An owned response head whose payload has not been processed. +/// +/// Construction validates header syntax and caps, not declared payload limits or compression. +/// Discard never polls the body. Finalization applies ordinary body policy and transfers cleanup +/// to a live streamed response without starting another transport exchange. +#[must_use] +pub struct OutboundHead { + context: OutboundResponseContext, + encoded: BodyStream, + headers: HeaderMap, + location_evidence: OutboundLocationEvidence, + normalization: ResponseHeadNormalization, + owner: OutboundExchangeOwner, + status: StatusCode, +} + +/// Explicit image output format; no format is selected by default. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundImageFormat { + Auto, + Avif, + Gif, + Jpeg, + JpegXl, + Mp4, + Png, + WebP, +} + +/// Resampling filter for an explicitly configured image transformation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundResizeFilter { + Bicubic, + Bilinear, + Lanczos2, + Lanczos3, + Nearest, +} + +/// Crop focus with paired percentage coordinates, validated by the options builder. +/// +/// ```compile_fail +/// use edgezero_core::OutboundCropFocus; +/// let _ = OutboundCropFocus::Percent { x: 25 }; +/// ``` +/// ```compile_fail +/// use edgezero_core::OutboundCropFocus; +/// let _ = OutboundCropFocus::Smart { x: 25, y: 75 }; +/// ``` +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundCropFocus { + Default, + Percent { x: u8, y: u8 }, + Smart, +} + +/// An Image Optimizer region from the pinned SDK's portable subset. +/// +/// ``` +/// use edgezero_core::OutboundImageOptimizerRegion; +/// let region = OutboundImageOptimizerRegion::UsWest; +/// assert_eq!(region.as_str(), "us_west"); +/// ``` +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundImageOptimizerRegion { + Asia, + Australia, + EuCentral, + EuWest, + UsCentral, + UsEast, + UsWest, +} + +/// Validated native transformation settings with no implicit selectors. +/// +/// Attach with [`OutboundRequest::image_optimizer`]. Only Fastly Send/Follow hops support these +/// settings; Begin/Batch reject them. The synchronous provider call cannot be interrupted. Query preservation +/// defaults to false. Transformation and performance qualification require hosted evidence. +/// +/// ```compile_fail +/// use edgezero_core::OutboundImageOptimizerOptions; +/// let mut options = OutboundImageOptimizerOptions::new(); +/// options.quality = Some(255); +/// ``` +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct OutboundImageOptimizerOptions { + crop: Option<(u32, u32, OutboundCropFocus)>, + format: Option, + height: Option, + preserve_origin_query: bool, + quality: Option, + region: Option, + resize_filter: Option, + width: Option, +} + +#[derive(Clone, Copy)] +pub(crate) struct BudgetInputs { + pub deadline: Option, + pub timeout: Option, +} + +#[derive(Debug)] +pub struct OutboundRequest { + body: Body, + cache_policy: OutboundCachePolicy, + connect_timeout: Option, + deadline: Option, + headers: HeaderMap, + host_authority_override: Option, + image_optimizer: Option, + inbound_read_deadline: Option, + max_brotli_window_bits: u8, + max_chunk_bytes: Option, + max_decoded_response_bytes: Option, + max_decoder_bytes: u64, + max_encoded_response_bytes: Option, + max_request_body_bytes: u64, + max_response_header_bytes: Option, + max_response_header_count: Option, + method: Method, + proxy_marker: OutboundProxyMarker, + response_decoding: ResponseDecoding, + response_mode: ResponseMode, + timeout: Option, + uri: Uri, +} + +#[derive(Debug)] +#[non_exhaustive] +#[expect( + clippy::partial_pub_fields, + reason = "forwarded deadline provenance is immutable across public option round-trips" +)] +pub struct OutboundRequestParts { + pub body: Body, + pub cache_policy: OutboundCachePolicy, + pub connect_timeout: Option, + pub deadline: Option, + pub headers: HeaderMap, + pub host_authority_override: Option, + pub image_optimizer: Option, + inbound_read_deadline: Option, + pub max_brotli_window_bits: u8, + pub max_chunk_bytes: Option, + pub max_decoded_response_bytes: Option, + pub max_decoder_bytes: u64, + pub max_encoded_response_bytes: Option, + pub max_request_body_bytes: u64, + pub max_response_header_bytes: Option, + pub max_response_header_count: Option, + pub method: Method, + pub proxy_marker: OutboundProxyMarker, + pub response_decoding: ResponseDecoding, + pub response_mode: ResponseMode, + pub timeout: Option, + pub uri: Uri, +} + +/// Whether recognized response content codings are decoded or delivered unchanged. +/// +/// Partial (206) responses always retain their original coding. Identity bodies remain subject +/// to decoded-byte limits under either choice; encoded passthrough skips only that limit. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ResponseDecoding { + Decode, + Original, +} + +/// Per-request override of the client's synthetic adapter-marker policy. +/// The facade resolves Inherit before handing a prepared request to its implementation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundProxyMarker { + Inherit, + Off, + On, +} + +#[derive(Clone, Copy)] +enum PayloadDelivery { + Decoded, + Identity, + Original, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ResponseMode { + Buffered { max_bytes: u64 }, + Streamed, +} + +/// Shared response-body limits and decoding policy applied by the head finalizer. +/// Adapters retain provider reads and transport ownership; core shapes and collects the payload. +#[derive(Clone, Copy, Debug)] +pub struct ResponseBodyPolicy { + pub max_brotli_window_bits: u8, + pub max_chunk_bytes: Option, + pub max_decoded_response_bytes: Option, + pub max_decoder_bytes: u64, + pub max_encoded_response_bytes: Option, + pub response_decoding: ResponseDecoding, + pub response_mode: ResponseMode, + pub status: StatusCode, +} + +/// Controls use of an adapter-managed intermediary cache for one request. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum OutboundCachePolicy { + Bypass, + #[default] + PlatformDefault, +} + +#[derive(Clone)] +pub struct HttpClient { + inner: Arc, + proxy_marker_enabled: bool, +} + +/// Provider I/O for requests prepared by the shared facade. +/// Implementations must schedule eager work before returning pending/batch handles. +pub trait OutboundHttpClient: Send + Sync { + /// Schedules the complete exchange and returns its completion receiver. + /// + /// # Errors + /// Returns setup errors before I/O when the required scheduler is unavailable. + fn begin(&self, request: OutboundDispatchRequest) -> Result; + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture; + /// Captures provider-local progress without I/O, timers or body polling. + fn progress(&self) -> Rc; + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch; + fn time(&self) -> OutboundTime; + /// Checks provider support without I/O, tasks, timers or body polling. + /// + /// # Errors + /// Returns a typed client error for unsupported options or operation modes. + fn validate_supported_options( + &self, + request: &OutboundRequest, + mode: OutboundSendMode, + ) -> Result<(), EdgeError>; +} + +#[derive(Debug)] +pub struct OutboundResponse { + body: Body, + content_type_provenance: ContentTypeProvenance, + headers: HeaderMap, + headers_mutated: bool, + monotonic_clock: MonotonicClock, + request_method: Method, + status: StatusCode, +} + +#[derive(Debug)] +#[non_exhaustive] +/// One terminal result produced by an outbound batch slot. +pub struct OutboundSlotResult { + /// Time from the batch's shared method-entry snapshot until this slot became terminal. + pub elapsed: Duration, + /// This slot's terminal response or typed failure. + pub outcome: Result, +} + +impl OutboundSlotResult { + #[must_use] + #[inline] + pub fn new(elapsed: Duration, outcome: Result) -> Self { + Self { elapsed, outcome } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ResponseBodyDisposition { + FramingBodyless, + Payload, + ResetContent { declared_body: bool }, +} + +/// Why the original Content-Type cannot certify a retained textual singleton. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ContentTypeIneligibility { + Missing, + Modified, + MultipleValues, + NonText, + Removed, +} + +/// Original Content-Type evidence, captured before `EdgeZero` filters response headers. +/// +/// This does not validate MIME syntax, split commas, trim, or redact the value. A textual +/// singleton can still be invalid MIME. `Unknown` is required when the provider hides original +/// bytes or field boundaries. No raw header value is copied into this metadata. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ContentTypeProvenance { + Ineligible(ContentTypeIneligibility), + SingleTextUnchanged, + Unknown, +} + +/// Results of one upstream-header normalization pass, including pre-filter evidence. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ResponseHeadNormalization { + pub body_disposition: ResponseBodyDisposition, + pub content_type_provenance: ContentTypeProvenance, +} + +pub struct ResponseHeaderLimiter { + max_bytes: Option, + max_count: Option, + observed_bytes: u64, + observed_count: u64, +} + +impl ResponseHeaderLimiter { + #[must_use] + #[inline] + pub fn new(max_bytes: Option, max_count: Option) -> Self { + Self { + max_bytes, + max_count, + observed_bytes: 0, + observed_count: 0, + } + } + + /// Adds one adapter-visible upstream field section to the cumulative response-header + /// budget before normalization. + /// + /// # Errors + /// Returns a typed response limit error on count, byte, or accounting overflow. + #[inline] + pub fn observe(&mut self, headers: &HeaderMap) -> Result<(), EdgeError> { + for (name, value) in headers { + self.observe_field(name, value)?; + } + Ok(()) + } + + fn observe_field(&mut self, name: &HeaderName, value: &HeaderValue) -> Result<(), EdgeError> { + let next_count = self + .observed_count + .checked_add(1) + .ok_or_else(|| response_limit_error(ResponseLimitReason::HeaderCount))?; + self.observed_count = next_count; + if self.max_count.is_some_and(|max| next_count > max) { + return Err(response_limit_error(ResponseLimitReason::HeaderCount)); + } + + let name_bytes = u64::try_from(name.as_str().len()).unwrap_or(u64::MAX); + let value_bytes = u64::try_from(value.as_bytes().len()).unwrap_or(u64::MAX); + let next_bytes = self + .observed_bytes + .checked_add(name_bytes) + .and_then(|total| total.checked_add(value_bytes)) + .ok_or_else(|| response_limit_error(ResponseLimitReason::HeaderBytes))?; + self.observed_bytes = next_bytes; + if self.max_bytes.is_some_and(|max| next_bytes > max) { + return Err(response_limit_error(ResponseLimitReason::HeaderBytes)); + } + Ok(()) + } +} + +impl HttpClient { + /// Settles permitted GET/HEAD streams, then schedules the complete exchange. + /// + /// # Errors + /// Returns preparation, settlement or provider scheduling errors unchanged. + #[inline] + pub fn begin(&self, request: OutboundRequest) -> OutboundFuture { + let (scope, time, entered_at, prepared) = + match self.prepare(request, OutboundSendMode::Begin, None) { + Ok(captured) => captured, + Err(error) => return Box::pin(async move { Err(error) }), + }; + let client = Arc::clone(&self.inner); + let pending_scope = scope.clone(); + let pending_time = time.clone(); + scope::bind_future( + scope, + time, + entered_at, + Box::pin(async move { + let settled = dispatch::settle(prepared).await?; + client + .begin(settled)? + .bind(&pending_scope, pending_time, entered_at) + }), + ) + } + + /// Inspects effective marker policy without validation or I/O. + #[must_use] + #[inline] + pub fn effective_proxy_marker_enabled(&self, request: &OutboundRequest) -> bool { + match request.proxy_marker_setting() { + OutboundProxyMarker::Inherit => self.proxy_marker_enabled, + OutboundProxyMarker::On => true, + OutboundProxyMarker::Off => false, + } + } + + /// Follows only with an explicit synchronous target policy and validated hop limit. + /// + /// Every hop uses this client's ordinary prepared dispatch and head finalizer. Followed + /// bodies are dropped unread. Per-call timeouts restart per hop, but the initial absolute + /// deadline remains binding through policy execution and final streamed body consumption. + /// Plain send, begin and batches never follow. Custom credentials are policy-owned: only + /// Authorization, Cookie and authority overrides are stripped on prepared cross-origin hops. + /// + /// # Errors + /// Returns an unchanged hop error with its zero-based index, or an unchanged policy value + /// with its one-based redirect number. The policy and its error need not be Send or Clone. + #[inline] + pub fn follow( + &self, + request: OutboundRequest, + limit: OutboundRedirectLimit, + policy: P, + ) -> LocalBoxFuture<'static, Result>> + where + P: for<'head> FnMut( + &mut OutboundRedirectContext<'head>, + ) -> Result + + 'static, + E: 'static, + { + redirect::follow(self, request, limit, policy) + } + + #[inline] + pub fn new(client: Arc) -> Self { + Self { + inner: client, + proxy_marker_enabled: true, + } + } + + fn prepare( + &self, + request: OutboundRequest, + mode: OutboundSendMode, + cutoff: Option, + ) -> Result< + ( + OutboundScope, + OutboundTime, + MonotonicInstant, + OutboundDispatchRequest, + ), + EdgeError, + > { + let scope = OutboundScope::capture(self.inner.progress()); + scope.check()?; + let time = self.inner.time(); + let entered_at = time.clock().now(); + let prepared = dispatch::prepare( + self, + request, + mode, + time.clone(), + entered_at, + dispatch::DispatchBounds { + observation_cutoff: cutoff, + original_chain_deadline: None, + }, + scope.clone(), + )?; + Ok((scope, time, entered_at, prepared)) + } + + #[must_use] + #[inline] + pub fn proxy_marker_enabled(&self) -> bool { + self.proxy_marker_enabled + } + + fn resolve_proxy_marker(&self, request: OutboundRequest) -> OutboundRequest { + let setting = if self.effective_proxy_marker_enabled(&request) { + OutboundProxyMarker::On + } else { + OutboundProxyMarker::Off + }; + request.proxy_marker(setting) + } + + /// Sends through shared preparation, provider head acquisition and finalization. + /// + /// # Errors + /// Returns typed preparation, transport or response-policy failures unchanged. + #[inline] + pub fn send(&self, request: OutboundRequest) -> OutboundFuture { + let (scope, time, entered_at, prepared) = + match self.prepare(request, OutboundSendMode::Send, None) { + Ok(captured) => captured, + Err(error) => return Box::pin(async move { Err(error) }), + }; + let client = Arc::clone(&self.inner); + scope::bind_future( + scope, + time, + entered_at, + Box::pin(async move { + let settled = dispatch::settle(prepared).await?; + client.dispatch_head(settled).await?.finish().await + }), + ) + } + + /// Starts eagerly and collects in original input order. + /// The observation cutoff can intentionally truncate still-live request budgets. + #[must_use] + #[inline] + pub fn send_all_until( + &self, + requests: Vec, + observation_cutoff: Deadline, + ) -> LocalBoxFuture<'static, Result> { + let batch = self.start_batch_until(requests, observation_cutoff); + Box::pin(batch.collect()) + } + + /// Prepares every input and schedules eligible work before returning. + #[must_use] + #[inline] + pub fn start_batch_until( + &self, + requests: Vec, + observation_cutoff: Deadline, + ) -> OutboundBatch { + let scope = OutboundScope::capture(self.inner.progress()); + let time = self.inner.time(); + let started_at = time.clock().now(); + if scope.check().is_err() { + let count = requests.len(); + drop(requests); + return OutboundBatch::cutoff(OutboundBatchContext::new( + started_at, + time, + observation_cutoff, + vec![None; count], + Vec::new(), + scope, + )); + } + let mut slots = Vec::new(); + let mut budgets = Vec::with_capacity(requests.len()); + let mut preflight = Vec::new(); + for (index, request) in requests.into_iter().enumerate() { + match dispatch::prepare( + self, + request, + OutboundSendMode::Batch, + time.clone(), + started_at, + dispatch::DispatchBounds { + observation_cutoff: Some(observation_cutoff), + original_chain_deadline: None, + }, + scope.clone(), + ) { + Ok(prepared) => { + budgets.push(Some(prepared.budget())); + slots.push((index, prepared)); + } + Err(error) => { + budgets.push(None); + preflight.push((index, time.clock().now(), Err(error))); + } + } + } + let context = OutboundBatchContext::new( + started_at, + time, + observation_cutoff, + budgets, + preflight, + scope, + ); + if context.scope().check().is_err() { + return OutboundBatch::cutoff(context); + } + self.inner + .start_batch_until(OutboundPreparedBatch::new(context, slots)) + } + + #[inline] + pub fn with_client(client: Client) -> Self + where + Client: OutboundHttpClient + 'static, + { + Self::new(Arc::new(client)) + } + + /// Sets the marker default for this handle; explicit per-request settings win. + #[must_use] + #[inline] + pub fn with_proxy_marker(mut self, enabled: bool) -> Self { + self.proxy_marker_enabled = enabled; + self + } +} + +impl OutboundResponse { + #[must_use] + #[inline] + pub fn body(&self) -> &Body { + &self.body + } + + /// Returns original-header evidence, never inferred from the filtered header map. + /// + /// Mutable header access invalidates an originally eligible result conservatively, even + /// when only an unrelated field changes. Original ineligible/unknown evidence cannot be + /// upgraded by application edits. MIME validation and redaction remain application-owned. + #[must_use] + #[inline] + pub fn content_type_provenance(&self) -> ContentTypeProvenance { + if self.headers_mutated + && self.content_type_provenance == ContentTypeProvenance::SingleTextUnchanged + { + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::Modified) + } else { + self.content_type_provenance + } + } + + #[must_use] + #[inline] + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + #[must_use] + /// Returns adapter-facing mutable headers without normalizing them. + /// + /// Callers that mutate framing or hop-by-hop metadata must finish through + /// [`Self::into_response`], which reapplies the defensive normalization pass. + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + self.headers_mutated = true; + &mut self.headers + } + + #[must_use] + #[inline] + pub fn into_body(self) -> Body { + self.body + } + + /// Collects the body under a final response-buffer limit. + /// + /// # Errors + /// Returns a typed source error or `BufferedBody` response-limit error. + #[inline] + pub async fn into_bytes_bounded(self, max: u64) -> Result { + match self.body.into_stream() { + Err(body) => validate_buffered_response_bytes( + body.into_bytes().ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("body conversion lost buffered payload")) + })?, + max, + ), + Ok(stream) => collect_response_stream(stream, max).await, + } + } + + /// Collects the body under a final response-buffer limit and cooperative deadline. + /// + /// # Errors + /// Returns 504 when the deadline is observed expired, otherwise a typed source or + /// `BufferedBody` response-limit error. + #[inline] + pub async fn into_bytes_bounded_until( + self, + max: u64, + deadline: Deadline, + ) -> Result { + let clock = self.monotonic_clock.clone(); + ensure_deadline_live(deadline, &clock)?; + match self.body.into_stream() { + Err(body) => { + let bytes = body.into_bytes().ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("body conversion lost buffered payload")) + })?; + let outcome = validate_buffered_response_bytes(bytes, max); + ensure_deadline_live(deadline, &clock)?; + outcome + } + Ok(stream) => { + collect_response_stream_until_with_clock(stream, max, deadline, &clock).await + } + } + } + + #[must_use] + /// Destructures the adapter-facing response representation without re-running response-header + /// normalization. Call [`Self::into_response`] at an application boundary that requires the + /// final defensive framing pass. + #[inline] + pub fn into_parts(self) -> (Method, StatusCode, HeaderMap, Body, ContentTypeProvenance) { + let provenance = self.content_type_provenance(); + ( + self.request_method, + self.status, + self.headers, + self.body, + provenance, + ) + } + + /// Converts the outbound value after one final defensive metadata pass. + /// + /// # Errors + /// Returns a typed protocol error for malformed framing, or an internal error if the + /// already-validated response cannot be assembled. + #[inline] + pub fn into_response(mut self) -> Result { + let provenance = self.content_type_provenance(); + let normalized = + normalize_response_headers(&self.request_method, self.status, &mut self.headers)?; + if normalized.body_disposition != ResponseBodyDisposition::Payload { + let owner = self.body.take_egress_owner(); + self.body = Body::from_parts(BodyPayload::Once(Bytes::new()), owner); + } + let effective_provenance = if provenance == ContentTypeProvenance::SingleTextUnchanged { + normalized.content_type_provenance + } else { + provenance + }; + let mut builder = response_builder() + .status(self.status) + .extension(effective_provenance); + for (name, value) in &self.headers { + builder = builder.header(name, value); + } + builder.body(self.body).map_err(EdgeError::internal) + } + + #[must_use] + #[inline] + pub fn is_success(&self) -> bool { + self.status.is_success() + } + + /// Deserializes an already-buffered upstream body as JSON. + /// + /// # Errors + /// Returns protocol 502 for a streamed body and decode 502 for malformed JSON. + #[inline] + pub fn json(&self) -> Result + where + Value: DeserializeOwned, + { + let Some(bytes) = self.body.as_bytes() else { + return Err(EdgeError::bad_gateway_with_reason( + "response body not buffered; use json_bounded(max) or json_bounded_until(max, deadline)", + BadGatewayReason::Protocol, + )); + }; + decode_json(bytes) + } + + /// Collects and deserializes an upstream body as JSON. + /// + /// # Errors + /// Returns typed source/limit failures or decode 502 for malformed JSON. + #[inline] + pub async fn json_bounded(self, max: u64) -> Result + where + Value: DeserializeOwned, + { + let bytes = self.into_bytes_bounded(max).await?; + decode_json(&bytes) + } + + /// Collects and deserializes an upstream body under a cooperative deadline. + /// + /// # Errors + /// Returns 504 on observed expiry, typed source/limit failures, or decode 502 for malformed + /// JSON. + #[inline] + pub async fn json_bounded_until( + self, + max: u64, + deadline: Deadline, + ) -> Result + where + Value: DeserializeOwned, + { + let clock = self.monotonic_clock.clone(); + let bytes = self.into_bytes_bounded_until(max, deadline).await?; + ensure_deadline_live(deadline, &clock)?; + decode_json(&bytes) + } + + #[must_use] + /// Constructs an adapter response paired with its application clock. + /// + /// `headers` must already have passed [`OutboundHeaderInput::normalize`], including bodyless + /// disposition handling, before the response is exposed through app-facing accessors. + /// `content_type_provenance` must come from that pre-filter pass, not a second inspection of + /// normalized headers. Use `Unknown` if original field boundaries or bytes are unavailable. + /// The clock must be the same source used for the request and outbound client. This keeps + /// deferred response-body deadlines in the application's monotonic clock domain. + #[inline] + pub fn new( + request_method: Method, + status: StatusCode, + headers: HeaderMap, + body: Body, + monotonic_clock: MonotonicClock, + content_type_provenance: ContentTypeProvenance, + ) -> Self { + Self { + body, + content_type_provenance, + headers, + headers_mutated: false, + monotonic_clock, + request_method, + status, + } + } + + #[must_use] + #[inline] + pub fn status(&self) -> StatusCode { + self.status + } +} + +impl OutboundRequest { + #[must_use] + #[inline] + pub fn backend_target(&self) -> String { + let host = bracket_ipv6(self.host_name()); + format!("{host}:{}", self.resolved_port()) + } + + #[must_use] + #[inline] + pub fn body(mut self, body: BodyValue) -> Self + where + BodyValue: Into, + { + self.body = body.into(); + self + } + + pub(crate) fn budget_inputs(&self) -> BudgetInputs { + BudgetInputs { + deadline: self.deadline, + timeout: self.timeout, + } + } + + #[must_use] + #[inline] + pub fn cache_policy(mut self, policy: OutboundCachePolicy) -> Self { + self.cache_policy = policy; + self + } + + #[must_use] + #[inline] + pub fn cert_host(&self) -> Option<&str> { + (self.uri.scheme_str() == Some("https")).then(|| self.host_name()) + } + + /// Sets an independent connect-phase ceiling; zero is rejected at preflight. + #[must_use] + #[inline] + pub fn connect_timeout(mut self, timeout: Duration) -> Self { + self.connect_timeout = Some(timeout.min(DEADLINE_FAR_FUTURE)); + self + } + + #[must_use] + #[inline] + pub fn connect_timeout_setting(&self) -> Option { + self.connect_timeout + } + + #[must_use] + #[inline] + pub fn deadline(mut self, deadline: Deadline) -> Self { + self.deadline = Some(deadline); + self + } + + /// Reassembles a request and revalidates its target. + /// + /// # Errors + /// Returns [`EdgeError::BadRequest`] when `parts.uri` is not a canonicalizable HTTP(S) + /// target. + #[inline] + pub fn from_parts(parts: OutboundRequestParts) -> Result { + let canonical_uri = canonicalize_typed_uri(&parts.uri)?; + Ok(Self { + body: parts.body, + cache_policy: parts.cache_policy, + connect_timeout: parts + .connect_timeout + .map(|timeout| timeout.min(DEADLINE_FAR_FUTURE)), + deadline: parts.deadline, + headers: parts.headers, + host_authority_override: parts + .host_authority_override + .map(validate_host_authority_override) + .transpose()?, + image_optimizer: parts.image_optimizer, + inbound_read_deadline: parts.inbound_read_deadline, + max_decoder_bytes: parts.max_decoder_bytes, + max_brotli_window_bits: parts.max_brotli_window_bits, + max_chunk_bytes: parts.max_chunk_bytes, + max_decoded_response_bytes: parts.max_decoded_response_bytes, + max_encoded_response_bytes: parts.max_encoded_response_bytes, + max_request_body_bytes: parts.max_request_body_bytes, + max_response_header_bytes: parts.max_response_header_bytes, + max_response_header_count: parts.max_response_header_count, + method: parts.method, + proxy_marker: parts.proxy_marker, + response_decoding: parts.response_decoding, + response_mode: parts.response_mode, + timeout: parts.timeout, + uri: canonical_uri, + }) + } + + fn from_raw(method: Method, raw: &str) -> Result { + reject_ambiguous_raw_target(raw)?; + let uri = canonicalize_url(raw)?; + Ok(Self::with_canonical_uri(method, uri)) + } + + /// Creates an outbound request from an inbound request while removing framing and + /// hop-by-hop metadata. + /// + /// # Errors + /// Returns [`EdgeError::BadRequest`] when the target is invalid or a `connection` + /// nomination is malformed. + #[inline] + pub fn from_request(request: Request, target: Uri) -> Result { + let (parts, body) = request.into_parts(); + let mut outbound = Self::new(parts.method, target)?; + outbound.body = body; + outbound.inbound_read_deadline = parts + .extensions + .get::() + .map(ForwardedReadDeadline::deadline); + outbound.headers = parts.headers; + normalize_request_headers(&mut outbound.headers)?; + Ok(outbound) + } + + /// Creates a canonical GET request from a raw URL. + /// + /// # Errors + /// Returns [`EdgeError::BadRequest`] when the URL is not an absolute HTTP(S) target. + #[inline] + pub fn get(uri: Target) -> Result + where + Target: AsRef, + { + Self::from_raw(Method::GET, uri.as_ref()) + } + + #[must_use] + #[inline] + pub fn has_host_authority_override(&self) -> bool { + self.host_authority_override.is_some() + } + + /// Appends one validated header value. + /// + /// # Errors + /// Returns [`EdgeError::BadRequest`] when the name is invalid, the value is not UTF-8, + /// or the value contains bytes forbidden by HTTP header syntax. + #[inline] + pub fn header(mut self, name: N, value: V) -> Result + where + N: AsRef<[u8]>, + V: AsRef<[u8]>, + { + let header_name = HeaderName::from_bytes(name.as_ref()) + .map_err(|_error| EdgeError::bad_request("invalid outbound header name"))?; + if str::from_utf8(value.as_ref()).is_err() { + return Err(EdgeError::bad_request(format!( + "header value is not valid UTF-8: {header_name}" + ))); + } + let header_value = HeaderValue::from_bytes(value.as_ref()).map_err(|_error| { + EdgeError::bad_request(format!( + "header value contains forbidden bytes: {header_name}" + )) + })?; + self.headers.append(header_name, header_value); + Ok(self) + } + + #[must_use] + #[inline] + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + #[must_use] + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } + + #[must_use] + #[inline] + pub fn host_authority(&self) -> &str { + self.host_authority_override.as_ref().map_or_else( + || self.uri.authority().map_or("", Authority::as_str), + Authority::as_str, + ) + } + + /// Overrides only the outgoing HTTP authority while preserving the URI connection identity. + /// + /// # Errors + /// Returns [`EdgeError::BadRequest`] unless `authority` is a host or host-and-port value with + /// bracketed IPv6 and no user information, scheme, path, query, fragment, or controls. + #[inline] + pub fn host_authority_override(mut self, authority: &str) -> Result { + let parsed = authority + .parse::() + .map_err(|_error| invalid_host_authority_override())?; + self.host_authority_override = Some(validate_host_authority_override(parsed)?); + Ok(self) + } + + #[must_use] + #[inline] + pub fn host_name(&self) -> &str { + let host = self.uri.host().unwrap_or_default(); + host.strip_prefix('[') + .and_then(|unbracketed| unbracketed.strip_suffix(']')) + .unwrap_or(host) + } + + /// Attaches native optimizer settings without changing the target or headers. + /// + /// Fastly supports these only for bodiless GET/HEAD Send/Follow hops with default caching. + /// Its synchronous optimizer call has no established blocking-time bound; late results + /// are rejected against the original deadline when control returns. + #[must_use] + #[inline] + pub fn image_optimizer(mut self, options: OutboundImageOptimizerOptions) -> Self { + self.image_optimizer = Some(options); + self + } + + #[must_use] + #[inline] + pub fn image_optimizer_options(&self) -> Option<&OutboundImageOptimizerOptions> { + self.image_optimizer.as_ref() + } + + #[must_use] + #[inline] + pub fn into_parts(self) -> OutboundRequestParts { + OutboundRequestParts { + body: self.body, + cache_policy: self.cache_policy, + connect_timeout: self.connect_timeout, + deadline: self.deadline, + headers: self.headers, + host_authority_override: self.host_authority_override, + image_optimizer: self.image_optimizer, + inbound_read_deadline: self.inbound_read_deadline, + max_decoder_bytes: self.max_decoder_bytes, + max_brotli_window_bits: self.max_brotli_window_bits, + max_chunk_bytes: self.max_chunk_bytes, + max_decoded_response_bytes: self.max_decoded_response_bytes, + max_encoded_response_bytes: self.max_encoded_response_bytes, + max_request_body_bytes: self.max_request_body_bytes, + max_response_header_bytes: self.max_response_header_bytes, + max_response_header_count: self.max_response_header_count, + method: self.method, + proxy_marker: self.proxy_marker, + response_decoding: self.response_decoding, + response_mode: self.response_mode, + timeout: self.timeout, + uri: self.uri, + } + } + + #[must_use] + #[inline] + pub fn is_stream_body(&self) -> bool { + self.body.is_stream() + } + + #[must_use] + #[inline] + pub fn is_stream_response(&self) -> bool { + self.response_mode == ResponseMode::Streamed + } + + /// Serializes a request body as JSON. + /// + /// # Errors + /// Returns [`EdgeError::Internal`] when serialization fails. + #[inline] + pub fn json(mut self, value: &T) -> Result + where + T: Serialize, + { + self.body = Body::json(value).map_err(EdgeError::internal)?; + if !self.headers.contains_key(CONTENT_TYPE) { + self.headers + .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + Ok(self) + } + + #[must_use] + #[inline] + pub fn max_brotli_window_bits(mut self, bits: u8) -> Self { + self.max_brotli_window_bits = bits; + self + } + + #[must_use] + #[inline] + pub fn max_chunk_bytes(mut self, bytes: NonZeroU64) -> Self { + self.max_chunk_bytes = Some(bytes); + self + } + + #[must_use] + #[inline] + pub fn max_decoded_response_bytes(mut self, bytes: u64) -> Self { + self.max_decoded_response_bytes = Some(bytes); + self + } + + #[must_use] + #[inline] + pub fn max_decoder_bytes(mut self, bytes: u64) -> Self { + self.max_decoder_bytes = bytes; + self + } + + #[must_use] + #[inline] + pub fn max_encoded_response_bytes(mut self, bytes: u64) -> Self { + self.max_encoded_response_bytes = Some(bytes); + self + } + + #[must_use] + #[inline] + pub fn max_request_body_bytes(mut self, bytes: u64) -> Self { + self.max_request_body_bytes = bytes; + self + } + + #[must_use] + /// Selects buffered response mode and sets its final collection limit. + /// + /// This is a last-write-wins mode setter: a later [`Self::stream_response`] call selects + /// streamed mode instead. + #[inline] + pub fn max_response_bytes(mut self, bytes: u64) -> Self { + self.response_mode = ResponseMode::Buffered { max_bytes: bytes }; + self + } + + #[must_use] + #[inline] + pub fn max_response_header_bytes(mut self, bytes: u64) -> Self { + self.max_response_header_bytes = Some(bytes); + self + } + + #[must_use] + #[inline] + pub fn max_response_header_count(mut self, count: u64) -> Self { + self.max_response_header_count = Some(count); + self + } + + #[must_use] + #[inline] + pub fn method(&self) -> &Method { + &self.method + } + + /// Creates an outbound request from a typed URI. + /// + /// # Errors + /// Returns [`EdgeError::BadRequest`] when the URI is not an absolute HTTP(S) target. + #[expect( + clippy::needless_pass_by_value, + reason = "the public request constructor takes ownership consistently with http::Request" + )] + #[inline] + pub fn new(method: Method, uri: Uri) -> Result { + let canonical_uri = canonicalize_typed_uri(&uri)?; + Ok(Self::with_canonical_uri(method, canonical_uri)) + } + + /// Creates a canonical POST request from a raw URL. + /// + /// # Errors + /// Returns [`EdgeError::BadRequest`] when the URL is not an absolute HTTP(S) target. + #[inline] + pub fn post(uri: Target) -> Result + where + Target: AsRef, + { + Self::from_raw(Method::POST, uri.as_ref()) + } + + #[must_use] + #[inline] + pub fn proxy_marker(mut self, setting: OutboundProxyMarker) -> Self { + self.proxy_marker = setting; + self + } + + #[must_use] + #[inline] + pub fn proxy_marker_setting(&self) -> OutboundProxyMarker { + self.proxy_marker + } + + fn resolved_port(&self) -> u16 { + self.uri.port_u16().unwrap_or_else(|| { + if self.uri.scheme_str() == Some("https") { + 443 + } else { + 80 + } + }) + } + + /// Selects response decoding without changing `Accept-Encoding` or any response limit. + #[must_use] + #[inline] + pub fn response_decoding(mut self, choice: ResponseDecoding) -> Self { + self.response_decoding = choice; + self + } + + #[must_use] + #[inline] + pub fn response_decoding_setting(&self) -> ResponseDecoding { + self.response_decoding + } + + #[must_use] + #[inline] + pub fn sni_hostname(&self) -> Option<&str> { + let host = self.host_name(); + (self.uri.scheme_str() == Some("https") && host.parse::().is_err()).then_some(host) + } + + #[must_use] + /// Selects streamed response mode. + /// + /// This is a last-write-wins mode setter: a later [`Self::max_response_bytes`] call selects + /// buffered mode instead. + #[inline] + pub fn stream_response(mut self) -> Self { + self.response_mode = ResponseMode::Streamed; + self + } + + #[must_use] + #[inline] + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + + #[must_use] + #[inline] + pub fn uri(&self) -> &Uri { + &self.uri + } + + fn with_canonical_uri(method: Method, uri: Uri) -> Self { + Self { + body: Body::empty(), + cache_policy: OutboundCachePolicy::PlatformDefault, + connect_timeout: None, + deadline: None, + headers: HeaderMap::new(), + host_authority_override: None, + image_optimizer: None, + inbound_read_deadline: None, + max_decoder_bytes: DEFAULT_MAX_DECODER_BYTES, + max_brotli_window_bits: 24, + max_chunk_bytes: None, + max_decoded_response_bytes: None, + max_encoded_response_bytes: None, + max_request_body_bytes: DEFAULT_OUTBOUND_REQUEST_BODY_BYTES, + max_response_header_bytes: None, + max_response_header_count: None, + method, + proxy_marker: OutboundProxyMarker::Inherit, + response_decoding: ResponseDecoding::Decode, + response_mode: ResponseMode::Buffered { + max_bytes: DEFAULT_MAX_RESPONSE_BYTES, + }, + timeout: None, + uri, + } + } +} + +/// Applies the shared payload policy without polling the native body. +/// +/// Declared lengths are checked first, then streaming encoded limits, recognized decoding, +/// decoded limits, and rechunking. Original coding and all 206 payloads keep their surviving +/// encoding/length headers and bypass decoding, not normalization or transport/final caps. +/// Identity still obeys the decoded cap. Adapters must handle bodyless responses first. +/// +/// # Errors +/// Returns malformed content-length or declared payload-limit errors before rewriting headers. +#[inline] +pub fn apply_response_body_policy( + native: BodyStream, + headers: &mut HeaderMap, + policy: ResponseBodyPolicy, +) -> Result { + let encoding = classify_content_encoding(headers); + let delivery = payload_delivery( + &encoding, + policy.response_decoding == ResponseDecoding::Decode + && policy.status != StatusCode::PARTIAL_CONTENT, + ); + let max_buffered = match policy.response_mode { + ResponseMode::Buffered { max_bytes } => Some(max_bytes), + ResponseMode::Streamed => None, + }; + enforce_declared_payload_length( + headers, + delivery, + max_buffered, + policy.max_decoded_response_bytes, + policy.max_encoded_response_bytes, + )?; + let encoded = limit_encoded_stream(native, policy.max_encoded_response_bytes); + if matches!(delivery, PayloadDelivery::Original) { + return Ok(rechunk_stream(encoded, policy.max_chunk_bytes)); + } + let decoded = match &encoding { + ContentEncoding::Brotli => decode_brotli_stream( + encoded, + policy.max_brotli_window_bits, + policy.max_decoder_bytes, + ), + ContentEncoding::Deflate => decode_deflate_stream(encoded, policy.max_decoder_bytes), + ContentEncoding::Gzip => decode_gzip_stream(encoded, policy.max_decoder_bytes), + ContentEncoding::Identity | ContentEncoding::Passthrough(_) => encoded, + }; + if matches!( + &encoding, + ContentEncoding::Brotli | ContentEncoding::Deflate | ContentEncoding::Gzip + ) { + headers.remove(CONTENT_ENCODING); + headers.remove(CONTENT_LENGTH); + } + let output = match &encoding { + ContentEncoding::Brotli + | ContentEncoding::Deflate + | ContentEncoding::Gzip + | ContentEncoding::Identity => { + limit_decoded_stream(decoded, policy.max_decoded_response_bytes) + } + ContentEncoding::Passthrough(_) => decoded, + }; + Ok(rechunk_stream(output, policy.max_chunk_bytes)) +} + +fn invalid_host_authority_override() -> EdgeError { + EdgeError::bad_request("invalid outbound host authority override") +} + +fn validate_host_authority_override(authority: Authority) -> Result { + let raw = authority.as_str(); + let host = authority.host(); + let invalid_port = match raw.strip_prefix(host) { + Some("") => false, + Some(suffix) if suffix.starts_with(':') => authority.port_u16().is_none(), + _ => true, + }; + let unbracketed_ipv6 = raw.bytes().filter(|byte| *byte == b':').count() > 1 + && !(raw.starts_with('[') && raw.contains(']')); + if raw.is_empty() + || host.is_empty() + || raw.contains('@') + || invalid_port + || unbracketed_ipv6 + || HeaderValue::from_str(raw).is_err() + { + return Err(invalid_host_authority_override()); + } + Ok(authority) +} + +/// Collects a response stream under the final buffered-body cap. +/// +/// # Errors +/// Returns a typed source error unchanged or a `BufferedBody` response-limit error. +#[inline] +pub async fn collect_response_stream(stream: BodyStream, max: u64) -> Result { + collect_response_stream_inner(stream, max, None).await +} + +/// Applies sound pre-poll checks to a normalized payload `content-length`. +/// This low-level check assumes ordinary decoding of a full payload; original/206 delivery uses +/// [`apply_response_body_policy`] so declared limits match the bytes actually delivered. +/// +/// # Errors +/// Returns a protocol 502 for malformed length metadata or a typed response-limit 502 when +/// the declared wire/final size exceeds a comparable configured cap. +#[inline] +pub fn enforce_payload_content_length( + headers: &HeaderMap, + encoding: &ContentEncoding, + max_buffered_bytes: Option, + max_decoded_bytes: Option, + max_encoded_bytes: Option, +) -> Result<(), EdgeError> { + enforce_declared_payload_length( + headers, + payload_delivery(encoding, true), + max_buffered_bytes, + max_decoded_bytes, + max_encoded_bytes, + ) +} + +fn payload_delivery(encoding: &ContentEncoding, decode: bool) -> PayloadDelivery { + match encoding { + ContentEncoding::Identity => PayloadDelivery::Identity, + ContentEncoding::Passthrough(_) => PayloadDelivery::Original, + ContentEncoding::Brotli | ContentEncoding::Deflate | ContentEncoding::Gzip if decode => { + PayloadDelivery::Decoded + } + ContentEncoding::Brotli | ContentEncoding::Deflate | ContentEncoding::Gzip => { + PayloadDelivery::Original + } + } +} + +fn enforce_declared_payload_length( + headers: &HeaderMap, + delivery: PayloadDelivery, + max_buffered_bytes: Option, + max_decoded_bytes: Option, + max_encoded_bytes: Option, +) -> Result<(), EdgeError> { + let Some(length) = parse_content_length(headers)? else { + return Ok(()); + }; + if max_encoded_bytes.is_some_and(|max| length > max) { + return Err(response_limit_error(ResponseLimitReason::EncodedBody)); + } + match delivery { + PayloadDelivery::Identity => { + if max_decoded_bytes.is_some_and(|max| length > max) { + return Err(response_limit_error(ResponseLimitReason::DecodedBody)); + } + if max_buffered_bytes.is_some_and(|max| length > max) { + return Err(response_limit_error(ResponseLimitReason::BufferedBody)); + } + } + PayloadDelivery::Original => { + if max_buffered_bytes.is_some_and(|max| length > max) { + return Err(response_limit_error(ResponseLimitReason::BufferedBody)); + } + } + PayloadDelivery::Decoded => {} + } + Ok(()) +} + +/// Applies the synthetic adapter-marker policy after ordinary header normalization. +/// Off neither charges nor edits headers. Inherit uses the direct adapter default, on; +/// the client facade resolves its own default before handing requests to an implementation. +/// +/// # Errors +/// Returns a typed response-header limit error before insertion when the marker would exceed the +/// configured cumulative byte or field-count cap. +#[inline] +fn insert_proxy_header( + headers: &mut HeaderMap, + limiter: &mut ResponseHeaderLimiter, + setting: OutboundProxyMarker, + value: HeaderValue, +) -> Result<(), EdgeError> { + if setting == OutboundProxyMarker::Off { + return Ok(()); + } + let name = HeaderName::from_static(PROXY_HEADER); + limiter.observe_field(&name, &value)?; + headers.insert(name, value); + Ok(()) +} + +#[must_use] +#[inline] +pub fn limit_decoded_stream(stream: BodyStream, max: Option) -> BodyStream { + limit_response_stream(stream, max, ResponseLimitReason::DecodedBody) +} + +#[must_use] +#[inline] +pub fn limit_encoded_stream(stream: BodyStream, max: Option) -> BodyStream { + limit_response_stream(stream, max, ResponseLimitReason::EncodedBody) +} + +/// Reapplies request header normalization immediately before platform conversion. +/// +/// # Errors +/// Returns [`EdgeError::BadRequest`] when a `connection` nomination is malformed. +#[inline] +pub fn normalize_for_dispatch(request: &mut OutboundRequest) -> Result<(), EdgeError> { + normalize_request_headers(&mut request.headers) +} + +/// Normalizes raw upstream metadata before response-body policy is selected. +/// +/// # Errors +/// Returns a protocol-classified 502 when `connection` or payload framing is malformed. +#[inline] +fn normalize_response_headers( + request_method: &Method, + status: StatusCode, + headers: &mut HeaderMap, +) -> Result { + let original_provenance = original_content_type_provenance(headers); + let nominated = connection_nominations(headers, true)?; + retain_utf8_header_values(headers, true); + strip_hop_by_hop(headers, nominated, false); + + let content_type_provenance = if original_provenance + == ContentTypeProvenance::SingleTextUnchanged + && !headers.contains_key(CONTENT_TYPE) + { + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::Removed) + } else { + original_provenance + }; + + let body_disposition = if status.is_informational() || status == StatusCode::NO_CONTENT { + headers.remove(CONTENT_LENGTH); + ResponseBodyDisposition::FramingBodyless + } else if request_method == Method::HEAD || status == StatusCode::NOT_MODIFIED { + parse_content_length(headers)?; + ResponseBodyDisposition::FramingBodyless + } else { + let content_length = parse_content_length(headers)?; + if status == StatusCode::RESET_CONTENT { + headers.insert(CONTENT_LENGTH, HeaderValue::from_static("0")); + ResponseBodyDisposition::ResetContent { + declared_body: content_length.is_some_and(|length| length > 0), + } + } else { + ResponseBodyDisposition::Payload + } + }; + Ok(ResponseHeadNormalization { + body_disposition, + content_type_provenance, + }) +} + +fn original_content_type_provenance(headers: &HeaderMap) -> ContentTypeProvenance { + let mut values = headers.get_all(CONTENT_TYPE).iter(); + let Some(value) = values.next() else { + return ContentTypeProvenance::Ineligible(ContentTypeIneligibility::Missing); + }; + if values.next().is_some() { + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::MultipleValues) + } else if str::from_utf8(value.as_bytes()).is_err() { + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::NonText) + } else { + ContentTypeProvenance::SingleTextUnchanged + } +} + +#[must_use] +#[inline] +pub fn rechunk_stream(mut source: BodyStream, optional_maximum: Option) -> BodyStream { + let Some(maximum) = optional_maximum else { + return source; + }; + let maximum_chunk = usize::try_from(maximum.get()).unwrap_or(usize::MAX); + stream! { + while let Some(item) = source.next().await { + match item { + Ok(mut bytes) => { + if bytes.is_empty() { + yield Ok(Bytes::new()); + continue; + } + while bytes.len() > maximum_chunk { + yield Ok(bytes.split_to(maximum_chunk)); + } + if !bytes.is_empty() { + yield Ok(bytes); + } + } + Err(error) => { + yield Err(error); + return; + } + } + } + } + .boxed_local() +} + +/// Validates the adapter-independent outbound method, body, and response-policy contract. +/// Adapter-specific supported-option checks must run first. Begin requires buffered +/// uploads; Batch requires buffered uploads and responses. Validation never polls a body. +/// +/// # Errors +/// Returns [`EdgeError::PayloadTooLarge`] for a buffered upload over its byte cap, +/// or [`EdgeError::BadRequest`] when a common contract invariant is invalid. Individual +/// adapters may reject additional target-specific constraints during request preparation. +#[inline] +pub fn validate_for_dispatch( + request: &OutboundRequest, + mode: OutboundSendMode, +) -> Result<(), EdgeError> { + if request.body.has_egress_owner() { + return Err(EdgeError::bad_request( + "response-scoped body cannot be used as an outbound request body", + )); + } + if request.image_optimizer_options().is_some() + && matches!(mode, OutboundSendMode::Begin | OutboundSendMode::Batch) + { + return Err(EdgeError::bad_request( + "image_optimizer is not supported for eager outbound requests", + )); + } + image_optimizer::validate_options(request)?; + if !matches!( + request.method, + Method::GET + | Method::HEAD + | Method::POST + | Method::PUT + | Method::PATCH + | Method::DELETE + | Method::OPTIONS + ) { + return Err(EdgeError::bad_request(format!( + "method {} is not portable; supported: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS", + request.method + ))); + } + + if matches!(request.method, Method::GET | Method::HEAD) { + match request.body.as_bytes() { + Some([]) => {} + Some(_) => { + return Err(EdgeError::bad_request( + "GET/HEAD request must not carry a body", + )); + } + None if mode == OutboundSendMode::Batch => { + return Err(EdgeError::bad_request( + "outbound batches require buffered request bodies", + )); + } + None => {} + } + } + + if !(10..=30).contains(&request.max_brotli_window_bits) { + return Err(EdgeError::bad_request( + "max_brotli_window_bits must be between 10 and 30 inclusive", + )); + } + if let Some(bytes) = request.body.as_bytes() + && u64::try_from(bytes.len()).map_or(true, |length| length > request.max_request_body_bytes) + { + return Err(EdgeError::payload_too_large( + RequestBodyOverflowOrigin::OutboundUpload, + )); + } + if request + .connect_timeout + .is_some_and(|timeout| timeout.is_zero()) + { + return Err(EdgeError::bad_request("connect timeout must be positive")); + } + if mode == OutboundSendMode::Begin + && request.is_stream_body() + && !matches!(request.method, Method::GET | Method::HEAD) + { + return Err(EdgeError::bad_request( + "begin requires a buffered request body; use send for a streamed upload", + )); + } + if mode == OutboundSendMode::Batch { + if request.is_stream_body() { + return Err(EdgeError::bad_request( + "outbound batches require buffered request bodies; use send for a streamed upload", + )); + } + if request.is_stream_response() { + return Err(EdgeError::bad_request( + "outbound batches require buffered responses; use send for a streamed response", + )); + } + } + Ok(()) +} + +fn bracket_ipv6(host: &str) -> String { + if host.contains(':') { + format!("[{host}]") + } else { + host.to_owned() + } +} + +fn canonicalize_typed_uri(uri: &Uri) -> Result { + let raw = uri.to_string(); + reject_ambiguous_raw_target(&raw)?; + canonicalize_url(&raw) +} + +fn canonicalize_url(raw: &str) -> Result { + let mut parsed = Url::parse(raw) + .map_err(|_error| EdgeError::bad_request("outbound URI must be absolute with authority"))?; + if !matches!(parsed.scheme(), "http" | "https") { + return Err(EdgeError::bad_request( + "outbound URI scheme must be http or https", + )); + } + if parsed.host_str().is_none() { + return Err(EdgeError::bad_request( + "outbound URI must be absolute with authority", + )); + } + if !parsed.username().is_empty() || parsed.password().is_some() { + return Err(EdgeError::bad_request( + "outbound URI must not contain userinfo; pass credentials via the `authorization` header", + )); + } + if parsed.fragment().is_some() { + return Err(EdgeError::bad_request( + "outbound URI must not contain a fragment", + )); + } + let default_port = if parsed.scheme() == "http" { 80 } else { 443 }; + if parsed.port() == Some(default_port) { + parsed + .set_port(None) + .map_err(|()| EdgeError::bad_request("outbound URI contains an invalid port"))?; + } + parsed + .as_str() + .parse() + .map_err(|_error| EdgeError::bad_request("outbound URI cannot be represented as HTTP URI")) +} + +async fn collect_response_stream_inner( + mut source: S, + max: u64, + optional_deadline: Option<(Deadline, &MonotonicClock)>, +) -> Result +where + S: Stream> + Unpin, +{ + let mut collected = Vec::new(); + let mut total = 0_u64; + loop { + if let Some((active_deadline, clock)) = optional_deadline { + ensure_deadline_live(active_deadline, clock)?; + } + let next_item = source.next().await; + if let Some((active_deadline, clock)) = optional_deadline { + ensure_deadline_live(active_deadline, clock).map_err(|mut selected| { + if let EdgeError::GatewayTimeout { + provider_detail, .. + } = &mut selected + { + *provider_detail = next_item + .as_ref() + .and_then(|item| item.as_ref().err()) + .and_then(EdgeError::provider_failure_detail) + .copied(); + } + selected + })?; + } + let Some(item) = next_item else { + return Ok(Bytes::from(collected)); + }; + let bytes = item?; + let chunk_len = u64::try_from(bytes.len()) + .map_err(|_error| response_limit_error(ResponseLimitReason::BufferedBody))?; + let next_total = total + .checked_add(chunk_len) + .ok_or_else(|| response_limit_error(ResponseLimitReason::BufferedBody))?; + if next_total > max { + if let Some((active_deadline, clock)) = optional_deadline { + ensure_deadline_live(active_deadline, clock)?; + } + return Err(response_limit_error(ResponseLimitReason::BufferedBody)); + } + collected.extend_from_slice(&bytes); + total = next_total; + } +} + +/// Collects a response stream under an absolute deadline evaluated by `clock`. +/// +/// # Errors +/// Returns a typed buffered-body overflow or gateway timeout while preserving source errors. +#[doc(hidden)] +#[inline] +pub async fn collect_response_stream_until_with_clock( + stream: BodyStream, + max: u64, + deadline: Deadline, + clock: &MonotonicClock, +) -> Result { + collect_response_stream_inner(stream, max, Some((deadline, clock))).await +} + +fn decode_json(bytes: &[u8]) -> Result +where + Value: DeserializeOwned, +{ + serde_json::from_slice(bytes).map_err(|_error| { + EdgeError::bad_gateway_with_reason( + "failed to decode upstream JSON", + BadGatewayReason::Decode(BadGatewayDecodeReason::Json), + ) + }) +} + +fn ensure_deadline_live(deadline: Deadline, clock: &MonotonicClock) -> Result<(), EdgeError> { + if deadline.is_expired_at(clock.now()) { + Err(EdgeError::gateway_timeout("response body deadline expired")) + } else { + Ok(()) + } +} + +fn normalize_request_headers(headers: &mut HeaderMap) -> Result<(), EdgeError> { + let nominated = connection_nominations(headers, false)?; + retain_utf8_header_values(headers, false); + strip_hop_by_hop(headers, nominated, true); + Ok(()) +} + +fn connection_nominations( + headers: &HeaderMap, + response: bool, +) -> Result, EdgeError> { + let mut nominated = Vec::new(); + for header_value in headers.get_all(CONNECTION) { + let raw_value = str::from_utf8(header_value.as_bytes()).map_err(|_error| { + malformed_connection(response, "connection header value is not valid UTF-8") + })?; + for raw_token in raw_value.split(',') { + let token = raw_token.trim_matches([' ', '\t']); + if token.is_empty() { + return Err(malformed_connection( + response, + "connection header contains an empty nomination", + )); + } + let name = HeaderName::from_bytes(token.as_bytes()).map_err(|_error| { + malformed_connection(response, "connection header contains an invalid nomination") + })?; + nominated.push(name); + } + } + Ok(nominated) +} + +fn malformed_connection(response: bool, message: &'static str) -> EdgeError { + if response { + EdgeError::bad_gateway_with_reason(message, BadGatewayReason::Protocol) + } else { + EdgeError::bad_request(message) + } +} + +fn limit_response_stream( + mut source: BodyStream, + optional_maximum: Option, + reason: ResponseLimitReason, +) -> BodyStream { + let Some(maximum) = optional_maximum else { + return source; + }; + stream! { + let mut total = 0_u64; + while let Some(item) = source.next().await { + match item { + Ok(bytes) => { + let Ok(chunk_len) = u64::try_from(bytes.len()) else { + let error = response_limit_error(reason); + drop(source); + yield Err(error); + return; + }; + let Some(next_total) = total.checked_add(chunk_len) else { + let error = response_limit_error(reason); + drop(source); + yield Err(error); + return; + }; + if next_total > maximum { + let error = response_limit_error(reason); + drop(source); + yield Err(error); + return; + } + total = next_total; + yield Ok(bytes); + } + Err(error) => { + drop(source); + yield Err(error); + return; + } + } + } + } + .boxed_local() +} + +fn parse_content_length(headers: &HeaderMap) -> Result, EdgeError> { + let mut parsed = None; + for value in headers.get_all(CONTENT_LENGTH) { + let raw = str::from_utf8(value.as_bytes()).map_err(|_error| protocol_content_length())?; + if raw.contains(',') { + return Err(protocol_content_length()); + } + let trimmed = raw.trim_matches([' ', '\t']); + if trimmed.is_empty() || !trimmed.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(protocol_content_length()); + } + let length = trimmed + .parse::() + .map_err(|_error| protocol_content_length())?; + if parsed.is_some_and(|previous| previous != length) { + return Err(protocol_content_length()); + } + parsed = Some(length); + } + Ok(parsed) +} + +fn protocol_content_length() -> EdgeError { + EdgeError::bad_gateway_with_reason( + "upstream response has malformed or conflicting content-length", + BadGatewayReason::Protocol, + ) +} + +fn response_limit_error(reason: ResponseLimitReason) -> EdgeError { + EdgeError::response_too_large_with_reason("upstream response exceeded configured limit", reason) +} + +fn retain_utf8_header_values(headers: &mut HeaderMap, response: bool) { + let mut retained = HeaderMap::with_capacity(headers.len()); + for (name, value) in headers.iter() { + if name != CONNECTION + && !(response && (name == CONTENT_ENCODING || name == CONTENT_LENGTH)) + && str::from_utf8(value.as_bytes()).is_err() + { + if response { + log::warn!("dropping non-UTF-8 outbound response header: {name}"); + } else { + log::warn!("dropping non-UTF-8 outbound request header: {name}"); + } + continue; + } + retained.append(name.clone(), value.clone()); + } + *headers = retained; +} + +fn strip_hop_by_hop( + headers: &mut HeaderMap, + nominated: Vec, + strip_request_framing: bool, +) { + for name in nominated { + headers.remove(name); + } + for name in [ + CONNECTION, + HeaderName::from_static("keep-alive"), + HeaderName::from_static("proxy-connection"), + PROXY_AUTHENTICATE, + PROXY_AUTHORIZATION, + TE, + TRAILER, + TRANSFER_ENCODING, + UPGRADE, + ] { + headers.remove(name); + } + if strip_request_framing { + headers.remove(HOST); + headers.remove(CONTENT_LENGTH); + } +} + +fn validate_buffered_response_bytes(bytes: Bytes, max: u64) -> Result { + let length = u64::try_from(bytes.len()) + .map_err(|_error| response_limit_error(ResponseLimitReason::BufferedBody))?; + if length > max { + Err(response_limit_error(ResponseLimitReason::BufferedBody)) + } else { + Ok(bytes) + } +} + +fn reject_ambiguous_raw_target(raw: &str) -> Result<(), EdgeError> { + if raw.as_bytes().contains(&b'#') { + return Err(EdgeError::bad_request( + "outbound URI must not contain a fragment", + )); + } + if raw.as_bytes().contains(&b'\\') { + return Err(EdgeError::bad_request( + "outbound URI must not contain a backslash", + )); + } + let Some((_scheme, authority_tail)) = raw.split_once("://") else { + return Err(EdgeError::bad_request( + "outbound URI must be absolute with authority", + )); + }; + let authority = authority_tail + .split(['/', '?', '#']) + .next() + .unwrap_or_default(); + if authority.is_empty() { + return Err(EdgeError::bad_request( + "outbound URI must be absolute with authority", + )); + } + if authority.as_bytes().contains(&b'@') { + return Err(EdgeError::bad_request( + "outbound URI must not contain userinfo; pass credentials via the `authorization` header", + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests; diff --git a/crates/edgezero-core/src/outbound/batch.rs b/crates/edgezero-core/src/outbound/batch.rs new file mode 100644 index 000000000..ba79cafc4 --- /dev/null +++ b/crates/edgezero-core/src/outbound/batch.rs @@ -0,0 +1,1992 @@ +use std::collections::VecDeque; +use std::fmt; +use std::iter; +use std::mem; +use std::pin::Pin; +use std::rc::Rc; +use std::task::{Context, Poll, Waker}; +use std::time::Duration; + +use futures_util::StreamExt as _; +use futures_util::future::poll_fn; +use futures_util::stream::{LocalBoxStream, Stream}; + +use super::{ + OutboundDispatchRequest, OutboundExchangeOwner, OutboundResponse, OutboundScope, + OutboundSlotResult, OutboundTime, Revocable, +}; +use crate::error::{BudgetSource, EdgeError}; +use crate::time::{Deadline, DispatchBudget, MonotonicClock, MonotonicInstant}; + +type Observation = (usize, MonotonicInstant, Result); +type ProducerObservation = fn( + &mut BatchProducer, + &OutboundBatchContext, + &mut Context<'_>, +) -> Poll, EdgeError>>; + +/// Facade-validated, indexed requests and their single-use batch context. +pub struct OutboundPreparedBatch { + context: OutboundBatchContext, + slots: Vec<(usize, OutboundDispatchRequest)>, +} + +/// Immutable timing, budget provenance and preflight outcomes captured by the facade. +#[derive(Debug)] +pub struct OutboundBatchContext { + budgets: Vec>, + observation_cutoff: Deadline, + preflight: Vec, + scope: OutboundScope, + started_at: MonotonicInstant, + time: OutboundTime, +} + +#[derive(Debug)] +struct BatchIdentity { + slot_count: usize, +} + +/// A single-use receipt for a validated result from one particular batch. +#[derive(Debug)] +pub struct OutboundBatchItem { + identity: Rc, + index: usize, + result: Box, +} + +/// The ownership state of one input position after collection. +#[derive(Debug)] +pub enum OutboundCollectedSlot { + AlreadyDelivered, + Resolved(OutboundSlotResult), + Unresolved, +} + +/// Input-index-aligned collection retaining its batch identity until consumed. +#[derive(Debug)] +pub struct OutboundBatchResults { + identity: Rc, + slots: Vec, + termination: OutboundBatchTermination, +} + +/// A precise driver failure together with every retained input position. +#[derive(Debug, thiserror::Error)] +#[error("outbound batch driver failed: {error}")] +pub struct OutboundBatchFailure { + #[source] + error: EdgeError, + identity: Rc, + slots: Vec, +} + +/// Why observation stopped. Completion requires clean driver EOF. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundBatchTermination { + Completed, + Cutoff, +} + +/// One ownership-transferring asynchronous observation. +#[derive(Debug)] +pub enum OutboundBatchNext { + Finished(OutboundBatchTermination), + Item(OutboundBatchItem), +} + +/// One nonblocking observation, using the same ledger as asynchronous observation. +#[derive(Debug)] +pub enum OutboundBatchCheck { + Finished(OutboundBatchTermination), + Item(OutboundBatchItem), + Pending, +} + +/// Raw terminal observations of work already scheduled by the producer. +#[derive(Debug)] +pub enum OutboundBatchDriverEvent { + Cutoff, + Failed(EdgeError), + Item { + index: usize, + completed_at: MonotonicInstant, + outcome: Result, + }, +} + +/// Optional single-use receipt storage for mixed observation and collection. +#[derive(Debug)] +pub struct OutboundBatchAccumulator { + identity: Rc, + slots: Vec>, +} + +/// A rejected insertion, retaining the exact original receipt. +#[derive(Debug, thiserror::Error)] +#[error("outbound batch receipt insertion failed: {error}")] +pub struct OutboundBatchInsertFailure { + #[source] + error: EdgeError, + item: OutboundBatchItem, +} + +/// A rejected merge, retaining both unchanged inputs and the validation error. +#[derive(Debug, thiserror::Error)] +#[error("outbound batch collection merge failed: {error}")] +pub struct OutboundBatchMergeFailure { + accumulator: OutboundBatchAccumulator, + collection: Box>, + #[source] + error: EdgeError, +} + +struct RecordedItem { + completed_at: MonotonicInstant, + index: usize, + result: OutboundSlotResult, +} + +struct BatchProducer { + owner: Option, + stream: LocalBoxStream<'static, OutboundBatchDriverEvent>, +} + +impl BatchProducer { + fn drive( + &mut self, + context: &OutboundBatchContext, + cx: &mut Context<'_>, + ) -> Poll, EdgeError>> { + let mut receiver = self.stream.next(); + context.scope.poll( + &context.time, + context.started_at, + cx, + Pin::new(&mut receiver), + ) + } + + fn observe( + &mut self, + context: &OutboundBatchContext, + cx: &mut Context<'_>, + ) -> Poll, EdgeError>> { + context + .scope + .with(|| self.stream.poll_next_unpin(cx).map(Ok)) + .unwrap_or_else(|error| Poll::Ready(Err(error))) + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for BatchProducer { + fn drop(&mut self) { + drop(self.owner.take()); + } +} + +/// Validates producer observations without scheduling work or introducing an executor. +pub struct OutboundBatch { + context: OutboundBatchContext, + delivered: Vec, + identity: Rc, + poisoned: bool, + producer: Option>, + ready: VecDeque, + resolved: Vec, + seen: Vec, + terminal: Option>, +} + +impl OutboundPreparedBatch { + #[must_use] + #[inline] + pub fn clock(&self) -> &MonotonicClock { + self.context.clock() + } + + #[must_use] + #[inline] + pub fn into_slots(self) -> (OutboundBatchContext, Vec<(usize, OutboundDispatchRequest)>) { + (self.context, self.slots) + } + + pub(super) fn new( + context: OutboundBatchContext, + slots: Vec<(usize, OutboundDispatchRequest)>, + ) -> Self { + Self { context, slots } + } + + #[must_use] + #[inline] + pub fn observation_cutoff(&self) -> Deadline { + self.context.observation_cutoff() + } + + #[must_use] + #[inline] + pub fn scope(&self) -> &OutboundScope { + self.context.scope() + } + + #[must_use] + #[inline] + pub fn slot_count(&self) -> usize { + self.context.slot_count() + } + + #[must_use] + #[inline] + pub fn started_at(&self) -> MonotonicInstant { + self.context.started_at() + } + + #[must_use] + #[inline] + pub fn time(&self) -> &OutboundTime { + self.context.time() + } +} + +impl OutboundBatchContext { + #[must_use] + #[inline] + pub fn clock(&self) -> &MonotonicClock { + self.time.clock() + } + + pub(super) fn new( + started_at: MonotonicInstant, + time: OutboundTime, + observation_cutoff: Deadline, + budgets: Vec>, + preflight: Vec, + scope: OutboundScope, + ) -> Self { + Self { + budgets, + observation_cutoff, + preflight, + scope, + started_at, + time, + } + } + + #[must_use] + #[inline] + pub fn observation_cutoff(&self) -> Deadline { + self.observation_cutoff + } + + #[must_use] + #[inline] + pub fn scope(&self) -> &OutboundScope { + &self.scope + } + + #[must_use] + #[inline] + pub fn slot_count(&self) -> usize { + self.budgets.len() + } + + #[must_use] + #[inline] + pub fn started_at(&self) -> MonotonicInstant { + self.started_at + } + + #[must_use] + #[inline] + pub fn time(&self) -> &OutboundTime { + &self.time + } +} + +impl OutboundBatchItem { + #[must_use] + #[inline] + pub fn index(&self) -> usize { + self.index + } + + #[must_use] + #[inline] + pub fn into_parts(self) -> (usize, OutboundSlotResult) { + (self.index, *self.result) + } + + #[must_use] + #[inline] + pub fn result(&self) -> &OutboundSlotResult { + &self.result + } +} + +impl OutboundBatchResults { + #[must_use] + #[inline] + pub fn into_parts(self) -> (Vec, OutboundBatchTermination) { + (self.slots, self.termination) + } + + #[must_use] + #[inline] + pub fn slots(&self) -> &[OutboundCollectedSlot] { + &self.slots + } + + #[must_use] + #[inline] + pub fn termination(&self) -> OutboundBatchTermination { + self.termination + } +} + +impl OutboundBatchFailure { + #[must_use] + #[inline] + pub fn error(&self) -> &EdgeError { + &self.error + } + + #[must_use] + #[inline] + pub fn into_parts(self) -> (EdgeError, Vec) { + (self.error, self.slots) + } + + #[must_use] + #[inline] + pub fn slots(&self) -> &[OutboundCollectedSlot] { + &self.slots + } +} + +impl OutboundBatchInsertFailure { + #[must_use] + #[inline] + pub fn error(&self) -> &EdgeError { + &self.error + } + + #[must_use] + #[inline] + pub fn into_parts(self) -> (EdgeError, OutboundBatchItem) { + (self.error, self.item) + } +} + +impl OutboundBatchMergeFailure { + #[must_use] + #[inline] + pub fn error(&self) -> &EdgeError { + &self.error + } + + #[inline] + pub fn into_parts( + self, + ) -> ( + EdgeError, + OutboundBatchAccumulator, + Result, + ) { + (self.error, self.accumulator, *self.collection) + } +} + +impl OutboundBatch { + #[must_use] + #[inline] + pub fn accumulator(&self) -> OutboundBatchAccumulator { + OutboundBatchAccumulator { + identity: Rc::clone(&self.identity), + slots: iter::repeat_with(|| None) + .take(self.identity.slot_count) + .collect(), + } + } + + /// Releases scheduled work and returns every index never transferred to the consumer. + #[must_use] + #[inline] + pub fn cancel(self) -> Vec { + self.delivered + .iter() + .enumerate() + .filter_map(|(index, delivered)| (!*delivered).then_some(index)) + .collect() + } + + /// Observes without waiting or advancing the paired clock. + /// + /// # Errors + /// Returns the exact driver failure or a typed protocol invariant error. + #[inline] + pub fn check(&mut self) -> Result { + match self.poll_observation( + &mut Context::from_waker(Waker::noop()), + BatchProducer::observe, + ) { + Poll::Ready(result) => result, + Poll::Pending => Ok(OutboundBatchCheck::Pending), + } + } + + /// Collects new results in input order while identifying previously transferred positions. + /// + /// # Errors + /// Retains all partial outcomes alongside the exact driver or protocol error. + #[inline] + pub async fn collect(mut self) -> Result { + let mut slots: Vec<_> = self + .delivered + .iter() + .map(|delivered| { + if *delivered { + OutboundCollectedSlot::AlreadyDelivered + } else { + OutboundCollectedSlot::Unresolved + } + }) + .collect(); + loop { + match self.next().await { + Ok(OutboundBatchNext::Item(item)) => { + if let Some(slot) = slots.get_mut(item.index) { + *slot = OutboundCollectedSlot::Resolved(*item.result); + } + } + Ok(OutboundBatchNext::Finished(termination)) => { + return Ok(OutboundBatchResults { + identity: Rc::clone(&self.identity), + slots, + termination, + }); + } + Err(error) => { + return Err(OutboundBatchFailure { + error, + identity: Rc::clone(&self.identity), + slots, + }); + } + } + } + } + + /// Constructs a batch only when no transport work was scheduled. + /// Premature cutoff and omitted earlier own-deadline outcomes are protocol failures. + #[must_use] + #[inline] + pub fn cutoff(context: OutboundBatchContext) -> Self { + let mut batch = Self::seed(context, None, None); + if batch.terminal.is_none() { + let terminal = if batch.resolved.iter().all(|resolved| *resolved) { + Ok(OutboundBatchTermination::Completed) + } else { + batch.validate_cutoff() + }; + batch.finish(terminal); + } + batch + } + + fn drain(&mut self, cx: &mut Context<'_>, receiver: ProducerObservation) { + while let Some(producer) = &mut self.producer { + let observation = producer + .with(cx, |value, context| receiver(value, &self.context, context)) + .unwrap_or_else(|error| Poll::Ready(Err(error))); + let terminal = match observation { + Poll::Pending => break, + Poll::Ready(Err(error) | Ok(Some(OutboundBatchDriverEvent::Failed(error)))) => { + Err(error) + } + Poll::Ready(Ok(Some(OutboundBatchDriverEvent::Item { + index, + completed_at, + outcome, + }))) => match self.record(index, completed_at, outcome, false) { + Ok(()) => continue, + Err(error) => Err(error), + }, + Poll::Ready(Ok(Some(OutboundBatchDriverEvent::Cutoff))) => self.validate_cutoff(), + Poll::Ready(Ok(None)) => { + if self.resolved.iter().all(|resolved| *resolved) { + Ok(OutboundBatchTermination::Completed) + } else { + Err(driver_error( + "outbound batch driver ended before every slot resolved", + )) + } + } + }; + self.finish(terminal); + break; + } + self.sort_ready(); + } + + fn finish(&mut self, terminal: Result) { + self.terminal = Some(terminal); + drop(self.producer.take()); + } + + /// Takes ownership of an already-scheduled driver and its task-group cancellation guard. + /// Ready observations are drained and ordered before delivery or terminal error reporting. + #[must_use] + #[inline] + pub fn from_driver( + context: OutboundBatchContext, + events: S, + owner: OutboundExchangeOwner, + ) -> Self + where + S: Stream + 'static, + { + Self::seed(context, Some(events.boxed_local()), Some(owner)) + } + + /// Waits for one new result or termination. Dropping a pending wait consumes no receipt. + /// + /// # Errors + /// Returns the exact driver failure or a typed protocol invariant error. + #[inline] + pub async fn next(&mut self) -> Result { + // Pending is represented by Poll::Pending, never a successful waiting observation. + poll_fn(|cx| match self.poll_observation(cx, BatchProducer::drive) { + Poll::Ready(Ok(OutboundBatchCheck::Item(item))) => { + Poll::Ready(Ok(OutboundBatchNext::Item(item))) + } + Poll::Ready(Ok(OutboundBatchCheck::Finished(termination))) => { + Poll::Ready(Ok(OutboundBatchNext::Finished(termination))) + } + Poll::Ready(Err(error)) => Poll::Ready(Err(error)), + Poll::Pending | Poll::Ready(Ok(OutboundBatchCheck::Pending)) => Poll::Pending, + }) + .await + } + + fn poll_observation( + &mut self, + cx: &mut Context<'_>, + receiver: ProducerObservation, + ) -> Poll> { + if self.poisoned { + return Poll::Ready(Err(driver_error( + "outbound batch driver was polled after an invariant failure", + ))); + } + self.drain(cx, receiver); + if let Some(item) = self.ready.pop_front() { + if let Some(delivered) = self.delivered.get_mut(item.index) { + *delivered = true; + } + return Poll::Ready(Ok(OutboundBatchCheck::Item(OutboundBatchItem { + identity: Rc::clone(&self.identity), + index: item.index, + result: Box::new(item.result), + }))); + } + match self.terminal.take() { + Some(Ok(termination)) => { + self.terminal = Some(Ok(termination)); + Poll::Ready(Ok(OutboundBatchCheck::Finished(termination))) + } + Some(Err(error)) => { + self.poisoned = true; + Poll::Ready(Err(error)) + } + None => Poll::Pending, + } + } + + fn record( + &mut self, + index: usize, + completed_at: MonotonicInstant, + outcome: Result, + preflight: bool, + ) -> Result<(), EdgeError> { + let Some(seen) = self.seen.get_mut(index) else { + return Err(driver_error( + "outbound batch driver emitted an out-of-range slot index", + )); + }; + if *seen { + return Err(driver_error( + "outbound batch driver emitted a duplicate slot index", + )); + } + if completed_at > self.context.clock().now() { + return Err(driver_error( + "outbound batch driver emitted a future terminal observation", + )); + } + *seen = true; + let selected = self.context.budgets.get(index).copied().flatten(); + let expired = selected.filter(|budget| budget.deadline.is_expired_at(completed_at)); + let own_timeout = selected.filter(|budget| { + budget.own_deadline_expired_before(self.context.observation_cutoff, completed_at) + }); + let unbudgeted_rejection = preflight + && selected.is_none() + && !matches!( + &outcome, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::BatchCutoff, + .. + }) + ); + if self.context.observation_cutoff.is_expired_at(completed_at) + && own_timeout.is_none() + && !unbudgeted_rejection + { + return Ok(()); + } + // Only classified own-deadline timeouts use their logical expiry instant. Successful + // responses and earlier typed failures keep their original terminal observation. + let terminal_at = own_timeout.map_or(completed_at, |budget| budget.deadline.instant()); + let (elapsed, classified) = + match completed_at.checked_duration_since(self.context.started_at) { + None => ( + Duration::ZERO, + Err(driver_error( + "monotonic clock moved backwards during outbound dispatch", + )), + ), + Some(observed_elapsed) => { + let classified_outcome = if let Some(budget) = expired { + Err(governing_timeout(budget, outcome.as_ref().err())) + } else { + outcome + }; + let elapsed = if own_timeout.is_some() { + terminal_at.saturating_duration_since(self.context.started_at) + } else { + observed_elapsed + }; + (elapsed, classified_outcome) + } + }; + if let Some(resolved) = self.resolved.get_mut(index) { + *resolved = true; + } + self.ready.push_back(RecordedItem { + completed_at: terminal_at, + index, + result: OutboundSlotResult::new(elapsed, classified), + }); + Ok(()) + } + + fn seed( + context: OutboundBatchContext, + stream: Option>, + owner: Option, + ) -> Self { + let count = context.slot_count(); + let (producer, setup_error) = match (stream, owner) { + (Some(events), Some(cancellation)) => match Revocable::new( + &context.scope, + BatchProducer { + stream: events, + owner: Some(cancellation), + }, + ) { + Ok(value) => (Some(value), None), + Err(error) => (None, Some(error)), + }, + (None, None) => (None, context.scope.check().err()), + _ => ( + None, + Some(driver_error( + "outbound batch producer ownership was incomplete", + )), + ), + }; + let mut batch = Self { + context, + delivered: vec![false; count], + identity: Rc::new(BatchIdentity { slot_count: count }), + poisoned: false, + producer, + ready: VecDeque::new(), + resolved: vec![false; count], + seen: vec![false; count], + terminal: None, + }; + for (index, completed_at, outcome) in mem::take(&mut batch.context.preflight) { + if let Err(error) = batch.record(index, completed_at, outcome, true) { + batch.finish(Err(error)); + break; + } + } + if batch.terminal.is_none() + && let Some(error) = setup_error + { + batch.finish(Err(error)); + } + batch.sort_ready(); + batch + } + + fn sort_ready(&mut self) { + self.ready + .make_contiguous() + .sort_by_key(|item| (item.completed_at, item.index)); + } + + fn validate_cutoff(&self) -> Result { + if self.resolved.iter().all(|resolved| *resolved) { + return Err(driver_error( + "outbound batch driver emitted cutoff after every slot resolved", + )); + } + let observed_at = self.context.clock().now(); + if !self.context.observation_cutoff.is_expired_at(observed_at) { + return Err(driver_error( + "outbound batch driver emitted cutoff before the observation cutoff", + )); + } + if self + .context + .budgets + .iter() + .zip(&self.resolved) + .any(|(selected, resolved)| { + !*resolved + && selected.is_some_and(|budget| { + budget.own_deadline_expired_before( + self.context.observation_cutoff, + observed_at, + ) + }) + }) + { + return Err(driver_error( + "outbound batch driver omitted an earlier own-deadline timeout before cutoff", + )); + } + Ok(OutboundBatchTermination::Cutoff) + } +} + +impl fmt::Debug for OutboundBatch { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutboundBatch") + .field("context", &self.context) + .field("delivered", &self.delivered) + .field("terminal", &self.terminal) + .field("poisoned", &self.poisoned) + .finish_non_exhaustive() + } +} + +impl OutboundBatchAccumulator { + /// Inserts an intact receipt after validating batch identity, index and uniqueness. + /// + /// # Errors + /// Returns the unchanged receipt and leaves this accumulator unchanged. + #[inline] + pub fn insert(&mut self, item: OutboundBatchItem) -> Result<(), OutboundBatchInsertFailure> { + let error = if !Rc::ptr_eq(&self.identity, &item.identity) { + Some(driver_error( + "outbound batch receipt belongs to a different batch", + )) + } else if self.slots.len() != self.identity.slot_count { + Some(driver_error( + "outbound batch accumulator has an invalid slot count", + )) + } else { + match self.slots.get_mut(item.index) { + None => Some(driver_error( + "outbound batch receipt has an out-of-range slot index", + )), + Some(Some(_)) => Some(driver_error("outbound batch receipt was already inserted")), + Some(slot @ None) => { + *slot = Some(*item.result); + return Ok(()); + } + } + }; + Err(OutboundBatchInsertFailure { + error: error + .unwrap_or_else(|| driver_error("outbound batch receipt validation failed")), + item, + }) + } + + /// Merges partial failure results without replacing the original driver error. + /// + /// # Errors + /// Returns both unchanged inputs, keeping merge and driver errors distinct. + #[inline] + pub fn merge_failure( + self, + mut failure: OutboundBatchFailure, + ) -> Result { + if let Err(error) = self.validate_merge(&failure.identity, &failure.slots, None) { + return Err(OutboundBatchMergeFailure { + accumulator: self, + collection: Box::new(Err(failure)), + error, + }); + } + self.merge_slots(&mut failure.slots); + Ok(failure) + } + + fn merge_slots(self, slots: &mut [OutboundCollectedSlot]) { + for (slot, receipt) in slots.iter_mut().zip(self.slots) { + if let Some(result) = receipt { + *slot = OutboundCollectedSlot::Resolved(result); + } + } + } + + /// Substitutes stored receipts only for matching `AlreadyDelivered` positions. + /// + /// # Errors + /// Returns both unchanged inputs if any receipt is missing, foreign or conflicting. + #[inline] + pub fn merge_success( + self, + mut result: OutboundBatchResults, + ) -> Result { + if let Err(error) = + self.validate_merge(&result.identity, &result.slots, Some(result.termination)) + { + return Err(OutboundBatchMergeFailure { + accumulator: self, + collection: Box::new(Ok(result)), + error, + }); + } + self.merge_slots(&mut result.slots); + Ok(result) + } + + fn validate_merge( + &self, + identity: &Rc, + slots: &[OutboundCollectedSlot], + termination: Option, + ) -> Result<(), EdgeError> { + if !Rc::ptr_eq(&self.identity, identity) { + return Err(driver_error( + "outbound batch collection belongs to a different batch", + )); + } + if slots.len() != self.identity.slot_count || self.slots.len() != self.identity.slot_count { + return Err(driver_error( + "outbound batch merge has an invalid slot count", + )); + } + let has_unresolved = slots + .iter() + .any(|slot| matches!(slot, OutboundCollectedSlot::Unresolved)); + if matches!(termination, Some(OutboundBatchTermination::Completed)) && has_unresolved + || matches!(termination, Some(OutboundBatchTermination::Cutoff)) && !has_unresolved + { + return Err(driver_error( + "outbound batch collection has an invalid termination", + )); + } + for (slot, receipt) in slots.iter().zip(&self.slots) { + match (slot, receipt) { + (OutboundCollectedSlot::AlreadyDelivered, None) => { + return Err(driver_error( + "outbound batch merge is missing a delivered receipt", + )); + } + ( + OutboundCollectedSlot::Resolved(_) | OutboundCollectedSlot::Unresolved, + Some(_), + ) => { + return Err(driver_error( + "outbound batch receipt conflicts with collection slot", + )); + } + _ => {} + } + } + Ok(()) + } +} + +fn governing_timeout(budget: DispatchBudget, original: Option<&EdgeError>) -> EdgeError { + let mut error = + EdgeError::gateway_timeout_caused("outbound request deadline expired", budget.cause); + if let EdgeError::GatewayTimeout { + provider_detail, .. + } = &mut error + { + *provider_detail = original + .and_then(EdgeError::provider_failure_detail) + .copied(); + } + error +} + +fn driver_error(message: &'static str) -> EdgeError { + EdgeError::internal(anyhow::anyhow!(message)) +} + +#[cfg(test)] +mod tests { + use std::cell::{Cell, RefCell}; + use std::error::Error as _; + use std::rc::Rc; + use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll, Waker}; + use std::time::Duration; + + use futures::executor::block_on; + use futures_util::FutureExt as _; + use futures_util::stream; + + use super::{ + OutboundBatch, OutboundBatchCheck, OutboundBatchContext, OutboundBatchDriverEvent, + OutboundBatchItem, OutboundBatchNext, OutboundBatchTermination, OutboundCollectedSlot, + }; + use crate::body::Body; + use crate::error::{BudgetSource, EdgeError}; + use crate::http::{HeaderMap, Method, StatusCode}; + use crate::outbound::{ + ContentTypeProvenance, OutboundExchangeOwner, OutboundResponse, OutboundSlotResult, + OutboundTime, + }; + use crate::time::{Deadline, DispatchBudget, MonotonicClock, MonotonicInstant}; + + struct Fixture { + now: Arc>, + start: MonotonicInstant, + time: OutboundTime, + } + + #[derive(Debug, thiserror::Error)] + #[error("original nonclone driver error")] + struct OriginalError(Arc<()>); + + impl Fixture { + fn advance(&self, millis: u64) { + *self.now.lock().expect("advance fake clock") = self.at(millis); + } + + fn at(&self, millis: u64) -> MonotonicInstant { + self.start + .checked_add(Duration::from_millis(millis)) + .expect("fixture instant") + } + + fn batch(&self, count: usize, events: Vec) -> OutboundBatch { + OutboundBatch::from_driver( + self.context(count), + stream::iter(events), + OutboundExchangeOwner::new(|| {}), + ) + } + + fn budget(&self, millis: u64, cause: BudgetSource) -> DispatchBudget { + DispatchBudget { + cause, + deadline: Deadline::at_instant(self.at(millis)), + duration: Duration::from_millis(millis), + } + } + + fn context(&self, count: usize) -> OutboundBatchContext { + OutboundBatchContext::new( + self.start, + self.time.clone(), + Deadline::at_instant(self.at(100)), + vec![Some(self.budget(100, BudgetSource::BatchCutoff)); count], + Vec::new(), + crate::OutboundScope::capture(Rc::new(crate::IndependentOutboundProgress)), + ) + } + + fn event(&self, index: usize, millis: u64) -> OutboundBatchDriverEvent { + OutboundBatchDriverEvent::Item { + index, + completed_at: self.at(millis), + outcome: Ok(self.response(Body::from("original response"))), + } + } + + fn new() -> Self { + let start = MonotonicInstant::now() + .checked_add(Duration::from_hours(1)) + .expect("synthetic clock"); + let now = Arc::new(Mutex::new(start)); + let captured_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *captured_now.lock().expect("fake clock")); + Self { + now, + start, + time: OutboundTime::new(clock, |_, _| { + panic!("batch ledger must not create timers") + }), + } + } + + fn response(&self, body: Body) -> OutboundResponse { + OutboundResponse::new( + Method::GET, + StatusCode::OK, + HeaderMap::new(), + body, + self.time.clock().clone(), + ContentTypeProvenance::Unknown, + ) + } + } + + fn checked_item(batch: &mut OutboundBatch) -> OutboundBatchItem { + let OutboundBatchCheck::Item(item) = batch.check().expect("valid observation") else { + panic!("expected checked item"); + }; + item + } + + fn next_item(batch: &mut OutboundBatch) -> OutboundBatchItem { + let OutboundBatchNext::Item(item) = block_on(batch.next()).expect("valid observation") + else { + panic!("expected next item"); + }; + item + } + + fn assert_resolved(slot: &OutboundCollectedSlot, elapsed: u64) { + let OutboundCollectedSlot::Resolved(result) = slot else { + panic!("expected resolved slot"); + }; + assert_eq!(result.elapsed, Duration::from_millis(elapsed)); + } + + #[test] + fn mixed_batch_collection_preserves_delivery_state() { + for cutoff in [false, true] { + let fixture = Fixture::new(); + fixture.advance(101); + let mut events = vec![ + fixture.event(0, 30), + fixture.event(2, 20), + fixture.event(1, 10), + ]; + if cutoff { + events.push(OutboundBatchDriverEvent::Cutoff); + } + let mut batch = fixture.batch(if cutoff { 4 } else { 3 }, events); + assert_eq!(checked_item(&mut batch).index(), 1); + assert_eq!(next_item(&mut batch).index(), 2); + let result = block_on(batch.collect()).expect("mixed collection"); + assert_resolved(result.slots().first().expect("slot zero"), 30); + assert!(matches!( + result.slots().get(1), + Some(OutboundCollectedSlot::AlreadyDelivered) + )); + assert!(matches!( + result.slots().get(2), + Some(OutboundCollectedSlot::AlreadyDelivered) + )); + if cutoff { + assert!(matches!( + result.slots().get(3), + Some(OutboundCollectedSlot::Unresolved) + )); + assert_eq!(result.termination(), OutboundBatchTermination::Cutoff); + } else { + assert_eq!(result.termination(), OutboundBatchTermination::Completed); + } + } + } + + #[test] + fn arrival_ledger_preserves_late_collection_and_cancel_safety() { + let fixture = Fixture::new(); + fixture.advance(101); + let mut batch = fixture.batch( + 4, + vec![ + fixture.event(0, 99), + fixture.event(2, 20), + fixture.event(1, 20), + fixture.event(3, 100), + OutboundBatchDriverEvent::Cutoff, + ], + ); + for (index, elapsed) in [(1, 20), (2, 20), (0, 99)] { + let item = checked_item(&mut batch); + assert_eq!(item.index(), index); + assert_eq!(item.result().elapsed, Duration::from_millis(elapsed)); + item.result().outcome.as_ref().expect("on-time response"); + } + for _ in 0_u8..2 { + assert!(matches!( + batch.check().expect("stable cutoff"), + OutboundBatchCheck::Finished(OutboundBatchTermination::Cutoff) + )); + } + assert_eq!(batch.cancel(), vec![3]); + + let mut pending_batch = OutboundBatch::from_driver( + fixture.context(1), + stream::pending(), + OutboundExchangeOwner::new(|| {}), + ); + assert!(pending_batch.next().now_or_never().is_none()); + assert_eq!( + pending_batch.cancel(), + vec![0], + "cancelled wait consumed no index" + ); + } + + #[test] + fn earlier_connect_timeout_survives_late_collection() { + let fixture = Fixture::new(); + fixture.advance(200); + let mut context = fixture.context(1); + context.budgets = vec![Some(fixture.budget(80, BudgetSource::PerCallTimeout))]; + let mut batch = OutboundBatch::from_driver( + context, + stream::iter([OutboundBatchDriverEvent::Item { + index: 0, + completed_at: fixture.at(10), + outcome: Err(EdgeError::gateway_timeout_caused( + "original connect timeout", + BudgetSource::ConnectTimeout, + )), + }]), + OutboundExchangeOwner::new(|| {}), + ); + let item = next_item(&mut batch); + assert!(matches!( + &item.result().outcome, + Err(EdgeError::GatewayTimeout { cause: BudgetSource::ConnectTimeout, message, .. }) + if message == "original connect timeout" + )); + assert_eq!(item.result().elapsed, Duration::from_millis(10)); + assert!(matches!( + block_on(batch.next()).expect("completed"), + OutboundBatchNext::Finished(OutboundBatchTermination::Completed) + )); + } + + #[test] + fn own_deadline_exception_uses_retained_metadata_not_collection_time() { + for cause in [ + BudgetSource::PerCallTimeout, + BudgetSource::RequestDeadline, + BudgetSource::Default, + ] { + let fixture = Fixture::new(); + fixture.advance(200); + let mut context = fixture.context(3); + context.budgets = vec![ + Some(fixture.budget(50, cause)), + Some(fixture.budget(100, cause)), + Some(fixture.budget(80, cause)), + ]; + let batch = OutboundBatch::from_driver( + context, + stream::iter([ + fixture.event(0, 150), + fixture.event(1, 100), + fixture.event(2, 79), + OutboundBatchDriverEvent::Cutoff, + ]), + OutboundExchangeOwner::new(|| {}), + ); + let result = block_on(batch.collect()).expect("own deadline exception"); + let Some(OutboundCollectedSlot::Resolved(timeout)) = result.slots().first() else { + panic!("own deadline must be delivered"); + }; + assert!(matches!( + &timeout.outcome, + Err(EdgeError::GatewayTimeout { cause: actual, .. }) if *actual == cause + )); + assert_eq!(timeout.elapsed, Duration::from_millis(50)); + assert!(matches!( + result.slots().get(1), + Some(OutboundCollectedSlot::Unresolved) + )); + let Some(OutboundCollectedSlot::Resolved(early)) = result.slots().get(2) else { + panic!("earlier success is retained"); + }; + early.outcome.as_ref().expect("retained earlier response"); + } + } + + #[test] + fn late_own_timeouts_are_ordered_at_logical_expiry_with_index_ties() { + let fixture = Fixture::new(); + fixture.advance(200); + let mut context = fixture.context(4); + context.budgets = vec![ + Some(fixture.budget(50, BudgetSource::PerCallTimeout)), + Some(fixture.budget(100, BudgetSource::BatchCutoff)), + Some(fixture.budget(100, BudgetSource::BatchCutoff)), + Some(fixture.budget(50, BudgetSource::RequestDeadline)), + ]; + let mut batch = OutboundBatch::from_driver( + context, + stream::iter([ + fixture.event(3, 90), + fixture.event(0, 150), + fixture.event(2, 60), + fixture.event(1, 40), + ]), + OutboundExchangeOwner::new(|| {}), + ); + for (index, elapsed) in [(1, 40), (0, 50), (3, 50), (2, 60)] { + let item = checked_item(&mut batch); + assert_eq!(item.index(), index); + assert_eq!(item.result().elapsed, Duration::from_millis(elapsed)); + if index == 0 || index == 3 { + assert!(matches!( + &item.result().outcome, + Err(EdgeError::GatewayTimeout { .. }) + )); + } else { + item.result() + .outcome + .as_ref() + .expect("unchanged timely success"); + } + } + assert!(matches!( + batch.check().expect("clean EOF"), + OutboundBatchCheck::Finished(OutboundBatchTermination::Completed) + )); + } + + #[test] + fn preflight_errors_without_selected_budget_survive_cutoff_unchanged() { + let fixture = Fixture::new(); + fixture.advance(150); + let mut context = fixture.context(2); + context.budgets = vec![None, Some(fixture.budget(100, BudgetSource::BatchCutoff))]; + context.preflight = vec![( + 0, + fixture.at(120), + Err(EdgeError::bad_request("unsupported option before budget")), + )]; + let result = + block_on(OutboundBatch::cutoff(context).collect()).expect("late preflight retained"); + let Some(OutboundCollectedSlot::Resolved(preflight)) = result.slots().first() else { + panic!("preflight must remain deliverable"); + }; + assert_eq!(preflight.elapsed, Duration::from_millis(120)); + assert!( + matches!(&preflight.outcome, Err(EdgeError::BadRequest { message, .. }) if message == "unsupported option before budget") + ); + assert!(matches!( + result.slots().get(1), + Some(OutboundCollectedSlot::Unresolved) + )); + assert_eq!(result.termination(), OutboundBatchTermination::Cutoff); + } + + #[test] + fn budgetless_batch_cutoff_preflight_stays_unresolved_at_entry() { + for from_driver in [false, true] { + let fixture = Fixture::new(); + let context = OutboundBatchContext::new( + fixture.start, + fixture.time.clone(), + Deadline::at_instant(fixture.start), + vec![None, None], + vec![ + ( + 0, + fixture.start, + Err(EdgeError::gateway_timeout_caused( + "effective budget is zero", + BudgetSource::BatchCutoff, + )), + ), + ( + 1, + fixture.start, + Err(EdgeError::bad_request("unsupported option before budget")), + ), + ], + crate::OutboundScope::capture(Rc::new(crate::IndependentOutboundProgress)), + ); + let batch = if from_driver { + OutboundBatch::from_driver( + context, + stream::iter([OutboundBatchDriverEvent::Cutoff]), + OutboundExchangeOwner::new(|| {}), + ) + } else { + OutboundBatch::cutoff(context) + }; + let result = block_on(batch.collect()).expect("expired observation cutoff"); + assert_eq!(result.termination(), OutboundBatchTermination::Cutoff); + assert!(matches!( + result.slots().first(), + Some(OutboundCollectedSlot::Unresolved) + )); + let Some(OutboundCollectedSlot::Resolved(unsupported)) = result.slots().get(1) else { + panic!("unsupported-option preflight must be retained"); + }; + assert_eq!(unsupported.elapsed, Duration::ZERO); + assert!(matches!( + &unsupported.outcome, + Err(EdgeError::BadRequest { message, .. }) if message == "unsupported option before budget" + )); + } + } + + #[test] + fn future_terminal_observations_preserve_partials_and_poison() { + for own_deadline in [false, true] { + for collect in [false, true] { + let fixture = Fixture::new(); + fixture.advance(10); + let mut context = fixture.context(2); + if own_deadline { + context.budgets[1] = Some(fixture.budget(50, BudgetSource::PerCallTimeout)); + } + let releases = Rc::new(Cell::new(0_usize)); + let owner_releases = Rc::clone(&releases); + let mut batch = OutboundBatch::from_driver( + context, + stream::iter([fixture.event(0, 10), fixture.event(1, 60)]), + OutboundExchangeOwner::new(move || { + owner_releases.set(owner_releases.get() + 1); + }), + ); + if collect { + let failure = block_on(batch.collect()).expect_err("future observation"); + assert!(matches!(failure.error(), EdgeError::Internal { .. })); + assert_eq!( + failure.error().message(), + "internal error: outbound batch driver emitted a future terminal observation" + ); + assert_resolved(failure.slots().first().expect("earlier partial"), 10); + assert!(matches!( + failure.slots().get(1), + Some(OutboundCollectedSlot::Unresolved) + )); + } else { + let earlier = checked_item(&mut batch); + assert_eq!(earlier.index(), 0); + assert_eq!(earlier.result().elapsed, Duration::from_millis(10)); + earlier.result().outcome.as_ref().expect("earlier success"); + let error = batch.check().expect_err("future observation"); + assert!(matches!(&error, EdgeError::Internal { .. })); + assert_eq!( + error.message(), + "internal error: outbound batch driver emitted a future terminal observation" + ); + assert!( + batch + .check() + .expect_err("poisoned driver") + .message() + .contains("polled after an invariant failure") + ); + assert_eq!(batch.cancel(), vec![1]); + } + assert_eq!( + fixture.time.clock().now(), + fixture.at(10), + "checking must not advance time" + ); + assert_eq!( + releases.get(), + 1, + "release the malformed driver exactly once" + ); + } + } + } + + #[test] + fn future_preflight_observations_preserve_earlier_partials() { + let fixture = Fixture::new(); + let mut context = fixture.context(2); + context.budgets = vec![None, None]; + context.preflight = vec![ + ( + 0, + fixture.start, + Err(EdgeError::bad_request("earlier preflight")), + ), + ( + 1, + fixture.at(10), + Err(EdgeError::bad_request("future preflight")), + ), + ]; + let failure = + block_on(OutboundBatch::cutoff(context).collect()).expect_err("future preflight"); + assert!(matches!(failure.error(), EdgeError::Internal { .. })); + assert_eq!( + failure.error().message(), + "internal error: outbound batch driver emitted a future terminal observation" + ); + assert_resolved(failure.slots().first().expect("earlier partial"), 0); + let Some(OutboundCollectedSlot::Resolved(earlier)) = failure.slots().first() else { + panic!("earlier preflight retained"); + }; + assert!( + matches!(&earlier.outcome, Err(EdgeError::BadRequest { message, .. }) if message == "earlier preflight") + ); + assert!(matches!( + failure.slots().get(1), + Some(OutboundCollectedSlot::Unresolved) + )); + } + + #[test] + fn cutoff_requires_expiry_and_every_earlier_own_deadline_observation() { + for omit_timeout in [false, true] { + let fixture = Fixture::new(); + let mut context = fixture.context(1); + if omit_timeout { + fixture.advance(101); + context.budgets = vec![Some(fixture.budget(50, BudgetSource::RequestDeadline))]; + } + let batch = OutboundBatch::from_driver( + context, + stream::iter([OutboundBatchDriverEvent::Cutoff]), + OutboundExchangeOwner::new(|| {}), + ); + let failure = block_on(batch.collect()).expect_err("invalid cutoff"); + let expected = if omit_timeout { + "omitted an earlier own-deadline" + } else { + "before the observation cutoff" + }; + assert!(failure.error().to_string().contains(expected)); + assert!(matches!( + failure.slots().first(), + Some(OutboundCollectedSlot::Unresolved) + )); + } + } + + #[test] + fn no_scheduled_work_preserves_preflight_and_rejects_premature_cutoff() { + let fixture = Fixture::new(); + let mut context = fixture.context(2); + context.preflight = vec![( + 0, + fixture.at(0), + Err(EdgeError::bad_request("exact preflight")), + )]; + fixture.advance(100); + let result = block_on(OutboundBatch::cutoff(context).collect()).expect("expired cutoff"); + let Some(OutboundCollectedSlot::Resolved(preflight)) = result.slots().first() else { + panic!("preflight retained"); + }; + assert!( + matches!(&preflight.outcome, Err(EdgeError::BadRequest { message, .. }) if message == "exact preflight") + ); + assert_eq!(result.termination(), OutboundBatchTermination::Cutoff); + assert_eq!( + block_on(OutboundBatch::cutoff(fixture.context(0)).collect()) + .expect("empty") + .termination(), + OutboundBatchTermination::Completed + ); + + let early_fixture = Fixture::new(); + let failure = block_on(OutboundBatch::cutoff(early_fixture.context(1)).collect()) + .expect_err("premature"); + assert!( + failure + .error() + .to_string() + .contains("before the observation cutoff") + ); + let mut resolved_context = early_fixture.context(1); + resolved_context.preflight = vec![( + 0, + early_fixture.at(0), + Err(EdgeError::bad_request("resolved preflight")), + )]; + assert_eq!( + block_on(OutboundBatch::cutoff(resolved_context).collect()) + .expect("all preflight resolved") + .termination(), + OutboundBatchTermination::Completed + ); + } + + #[test] + fn preflight_seeding_orders_and_applies_own_deadline_exception() { + let fixture = Fixture::new(); + fixture.advance(150); + let mut context = fixture.context(3); + context.budgets = vec![ + Some(fixture.budget(50, BudgetSource::PerCallTimeout)), + None, + None, + ]; + context.preflight = vec![ + ( + 0, + fixture.at(120), + Err(EdgeError::bad_gateway("late preflight")), + ), + ( + 2, + fixture.at(10), + Err(EdgeError::bad_request("second index")), + ), + ( + 1, + fixture.at(10), + Err(EdgeError::bad_request("first index")), + ), + ]; + let mut batch = OutboundBatch::cutoff(context); + assert_eq!(checked_item(&mut batch).index(), 1); + assert_eq!(checked_item(&mut batch).index(), 2); + let item = checked_item(&mut batch); + assert_eq!(item.index(), 0); + assert!(matches!( + &item.result().outcome, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + )); + assert!(matches!( + batch.check().expect("all preflight resolved"), + OutboundBatchCheck::Finished(OutboundBatchTermination::Completed) + )); + } + + #[test] + fn malformed_driver_and_trailing_events_preserve_partials_and_poison() { + let fixture = Fixture::new(); + fixture.advance(100); + for (count, trailing, diagnostic) in [ + (2, None, "ended before every slot resolved"), + (1, Some(fixture.event(0, 20)), "duplicate slot index"), + (1, Some(fixture.event(1, 20)), "out-of-range slot index"), + ( + 1, + Some(OutboundBatchDriverEvent::Cutoff), + "cutoff after every slot resolved", + ), + ( + 1, + Some(OutboundBatchDriverEvent::Failed(EdgeError::bad_gateway( + "exact finalization failure", + ))), + "exact finalization failure", + ), + ] { + let mut events = vec![fixture.event(0, 10)]; + events.extend(trailing); + let mut batch = fixture.batch(count, events); + assert_eq!(checked_item(&mut batch).index(), 0); + assert!( + batch + .check() + .expect_err("precise first failure") + .to_string() + .contains(diagnostic) + ); + assert!( + batch + .check() + .expect_err("poisoned") + .to_string() + .contains("polled after an invariant failure") + ); + } + let failure = block_on(fixture.batch(2, vec![fixture.event(1, 10)]).collect()) + .expect_err("premature EOF"); + assert!(matches!( + failure.slots().first(), + Some(OutboundCollectedSlot::Unresolved) + )); + assert_resolved(failure.slots().get(1).expect("partial result"), 10); + for event in [ + fixture.event(0, 10), + OutboundBatchDriverEvent::Cutoff, + OutboundBatchDriverEvent::Failed(EdgeError::bad_gateway("empty failed")), + ] { + block_on(fixture.batch(0, vec![event]).collect()).expect_err("malformed empty driver"); + } + } + + #[test] + fn owner_released_once_on_terminal_even_when_batch_is_retained() { + for mode in 0_u8..5 { + let fixture = Fixture::new(); + fixture.advance(101); + let releases = Rc::new(Cell::new(0_usize)); + let captured = Rc::clone(&releases); + let events = match mode { + 0 => vec![fixture.event(0, 10)], + 1 => vec![fixture.event(0, 10), OutboundBatchDriverEvent::Cutoff], + 2 => vec![ + fixture.event(0, 10), + OutboundBatchDriverEvent::Failed(EdgeError::bad_gateway( + "setup failed after scheduling", + )), + ], + 3 => vec![fixture.event(0, 10), fixture.event(0, 20)], + _ => Vec::new(), + }; + let count = if mode == 1 || mode == 2 { 2 } else { 1 }; + let mut batch = OutboundBatch::from_driver( + fixture.context(count), + stream::iter(events), + OutboundExchangeOwner::new(move || captured.set(captured.get() + 1)), + ); + assert_eq!(releases.get(), 0); + let _observation = batch.check(); + assert_eq!( + releases.get(), + 1, + "release before retained results are drained" + ); + drop(batch); + assert_eq!(releases.get(), 1); + } + } + + #[test] + fn pending_next_is_cancellation_safe_and_clean_eof_is_required() { + let fixture = Fixture::new(); + let polls = Rc::new(Cell::new(0_usize)); + let source_polls = Rc::clone(&polls); + let event = Rc::new(RefCell::new(Some(fixture.event(0, 10)))); + let source_event = Rc::clone(&event); + let releases = Rc::new(Cell::new(0_usize)); + let source_releases = Rc::clone(&releases); + let mut batch = OutboundBatch::from_driver( + fixture.context(1), + stream::poll_fn(move |_cx| { + let poll = source_polls.get(); + source_polls.set(poll + 1); + match poll { + 0 | 2 => Poll::Pending, + 1 => Poll::Ready(source_event.borrow_mut().take()), + _ => Poll::Ready(None), + } + }), + OutboundExchangeOwner::new(move || source_releases.set(source_releases.get() + 1)), + ); + assert!(batch.next().now_or_never().is_none()); + assert_eq!(releases.get(), 0); + fixture.advance(10); + assert_eq!(next_item(&mut batch).index(), 0); + assert_eq!(releases.get(), 0, "last slot is not clean EOF"); + assert!(matches!( + batch.check().expect("EOF"), + OutboundBatchCheck::Finished(OutboundBatchTermination::Completed) + )); + assert_eq!(releases.get(), 1); + assert!(matches!( + batch.check().expect("repeated EOF"), + OutboundBatchCheck::Finished(OutboundBatchTermination::Completed) + )); + assert_eq!(polls.get(), 4); + } + + #[test] + fn cancellation_includes_recorded_but_undelivered_indices() { + let fixture = Fixture::new(); + fixture.advance(20); + let mut batch = fixture.batch(4, vec![fixture.event(1, 10), fixture.event(2, 20)]); + assert_eq!(checked_item(&mut batch).index(), 1); + assert_eq!(batch.cancel(), vec![0, 2, 3]); + } + + #[test] + fn backward_observation_is_zero_elapsed_typed_internal() { + let fixture = Fixture::new(); + let before = fixture + .start + .checked_sub(Duration::from_millis(1)) + .expect("before entry"); + let mut batch = fixture.batch( + 1, + vec![OutboundBatchDriverEvent::Item { + index: 0, + completed_at: before, + outcome: Ok(fixture.response(Body::empty())), + }], + ); + let item = checked_item(&mut batch); + assert_eq!(item.result().elapsed, Duration::ZERO); + assert!(matches!( + &item.result().outcome, + Err(EdgeError::Internal { .. }) + )); + } + + #[test] + fn batch_accumulator_rejects_foreign_or_missing_items() { + let fixture = Fixture::new(); + fixture.advance(20); + let mut batch = fixture.batch(2, vec![fixture.event(0, 10), fixture.event(1, 20)]); + let accumulator = batch.accumulator(); + let receipt = checked_item(&mut batch); + let collection = block_on(batch.collect()).expect("collection"); + let rejection = accumulator + .merge_success(collection) + .expect_err("missing receipt"); + assert!( + rejection + .error() + .to_string() + .contains("missing a delivered receipt") + ); + let (_, mut recovered_accumulator, recovered_collection) = rejection.into_parts(); + recovered_accumulator + .insert(receipt) + .expect("receipt recovered"); + let recovered_success = recovered_collection.expect("original success tag"); + + let foreign = fixture.batch(2, Vec::new()).accumulator(); + let foreign_rejection = foreign + .merge_success(recovered_success) + .expect_err("foreign collection"); + assert!( + foreign_rejection + .error() + .to_string() + .contains("different batch") + ); + let (_, _, retained_collection) = foreign_rejection.into_parts(); + let result = recovered_accumulator + .merge_success(retained_collection.expect("retained collection")) + .expect("matching merge"); + assert_resolved(result.slots().first().expect("restored receipt"), 10); + assert_resolved(result.slots().get(1).expect("collected result"), 20); + } + + #[test] + fn batch_collections_preserve_identity_until_consumed() { + let fixture = Fixture::new(); + fixture.advance(101); + for failure in [false, true] { + let mut events = vec![ + fixture.event(0, 10), + fixture.event(1, 20), + fixture.event(2, 30), + ]; + if failure { + events.push(OutboundBatchDriverEvent::Failed(EdgeError::bad_gateway( + "original driver failure", + ))); + } else { + events.push(OutboundBatchDriverEvent::Cutoff); + } + let mut batch = fixture.batch(4, events); + let mut accumulator = batch.accumulator(); + let receipt = checked_item(&mut batch); + assert_eq!(receipt.index(), 0); + receipt + .result() + .outcome + .as_ref() + .expect("inspected response"); + accumulator + .insert(receipt) + .expect("intact inspected receipt"); + let next_receipt = next_item(&mut batch); + assert_eq!(next_receipt.index(), 1); + accumulator.insert(next_receipt).expect("next receipt"); + if failure { + let result = block_on(batch.collect()).expect_err("driver failure"); + assert!( + result + .error() + .to_string() + .contains("original driver failure") + ); + assert_eq!(result.slots().len(), 4); + let merged = accumulator.merge_failure(result).expect("failure merge"); + let (error, slots) = merged.into_parts(); + assert!(error.to_string().contains("original driver failure")); + assert_resolved(slots.first().expect("merged receipt"), 10); + assert_resolved(slots.get(1).expect("remaining result"), 20); + assert_resolved(slots.get(2).expect("collected result"), 30); + assert!(matches!( + slots.get(3), + Some(OutboundCollectedSlot::Unresolved) + )); + } else { + let result = block_on(batch.collect()).expect("cutoff"); + assert_eq!(result.slots().len(), 4); + assert_eq!(result.termination(), OutboundBatchTermination::Cutoff); + let (slots, termination) = accumulator + .merge_success(result) + .expect("success merge") + .into_parts(); + assert_resolved(slots.first().expect("merged receipt"), 10); + assert_eq!(termination, OutboundBatchTermination::Cutoff); + } + } + } + + #[test] + fn batch_recovery_returns_exact_nonclone_inputs() { + let fixture = Fixture::new(); + fixture.advance(10); + let body_releases = Rc::new(Cell::new(0_usize)); + let captured_releases = Rc::clone(&body_releases); + let body_owner = + OutboundExchangeOwner::new(move || captured_releases.set(captured_releases.get() + 1)); + let body = Body::from_stream(stream::poll_fn(move |_cx| { + let _keep_owner = &body_owner; + Poll::Pending + })); + let error_identity = Arc::new(()); + let original_error = EdgeError::internal(anyhow::Error::new(OriginalError(Arc::clone( + &error_identity, + )))); + let mut batch = fixture.batch( + 2, + vec![ + OutboundBatchDriverEvent::Item { + index: 0, + completed_at: fixture.at(10), + outcome: Ok(fixture.response(body)), + }, + OutboundBatchDriverEvent::Failed(original_error), + ], + ); + let accumulator = batch.accumulator(); + let item = checked_item(&mut batch); + let mut foreign = fixture.batch(2, Vec::new()).accumulator(); + let rejected = foreign.insert(item).expect_err("foreign insert"); + assert!(rejected.source().is_some()); + assert!(foreign.slots.iter().all(Option::is_none)); + let (_, recovered_item) = rejected.into_parts(); + assert_eq!(body_releases.get(), 0); + + let failure = block_on(batch.collect()).expect_err("precise driver error"); + let rejected_merge = accumulator + .merge_failure(failure) + .expect_err("missing receipt"); + assert!(rejected_merge.source().is_some()); + let (_, mut recovered_accumulator, recovered_collection) = rejected_merge.into_parts(); + let recovered_failure = recovered_collection.expect_err("original failure tag"); + let EdgeError::Internal { source, .. } = recovered_failure.error() else { + panic!("original typed error"); + }; + assert!(Arc::ptr_eq( + &source + .downcast_ref::() + .expect("original source") + .0, + &error_identity + )); + recovered_accumulator + .insert(recovered_item) + .expect("recovered receipt"); + let merged = recovered_accumulator + .merge_failure(recovered_failure) + .expect("recovered merge"); + assert_eq!( + body_releases.get(), + 0, + "no loss during rejected insert or merge" + ); + drop(merged); + assert_eq!( + body_releases.get(), + 1, + "exactly one body owner survives recovery" + ); + } + + #[test] + fn accumulator_rejects_duplicate_index_count_and_conflict_before_mutation() { + let fixture = Fixture::new(); + fixture.advance(10); + let mut batch = fixture.batch(1, vec![fixture.event(0, 10)]); + let mut accumulator = batch.accumulator(); + let receipt = checked_item(&mut batch); + accumulator.insert(receipt).expect("first receipt"); + let duplicate = OutboundBatchItem { + identity: Rc::clone(&accumulator.identity), + index: 0, + result: Box::new(OutboundSlotResult::new( + Duration::ZERO, + Err(EdgeError::bad_request("duplicate")), + )), + }; + let rejected = accumulator + .insert(duplicate) + .expect_err("private malformed duplicate"); + assert!(rejected.error().to_string().contains("already inserted")); + assert!(accumulator.slots.first().expect("unchanged slot").is_some()); + let (_, mut malformed) = rejected.into_parts(); + malformed.index = 1; + assert!( + accumulator + .insert(malformed) + .expect_err("invalid index") + .error() + .to_string() + .contains("out-of-range") + ); + let mut collection = block_on(batch.collect()).expect("collection"); + collection.slots = vec![OutboundCollectedSlot::Resolved(OutboundSlotResult::new( + Duration::ZERO, + Err(EdgeError::bad_request("conflicting result")), + ))]; + let rejected_conflict = accumulator.merge_success(collection).expect_err("conflict"); + assert!(rejected_conflict.error().to_string().contains("conflicts")); + let (_, recovered_accumulator, recovered_collection) = rejected_conflict.into_parts(); + let mut recovered_success = recovered_collection.expect("original success"); + recovered_success + .slots + .push(OutboundCollectedSlot::Unresolved); + let rejected_length = recovered_accumulator + .merge_success(recovered_success) + .expect_err("wrong length"); + assert!( + rejected_length + .error() + .to_string() + .contains("invalid slot count") + ); + } + + #[test] + fn batch_check_does_not_poll_timers_or_move_clock() { + let fixture = Fixture::new(); + let mut batch = OutboundBatch::from_driver( + fixture.context(1), + stream::pending(), + OutboundExchangeOwner::new(|| {}), + ); + assert!(matches!( + batch.check().expect("pending"), + OutboundBatchCheck::Pending + )); + assert_eq!(fixture.time.clock().now(), fixture.start); + let mut pending = Box::pin(batch.next()); + assert!( + pending + .as_mut() + .poll_unpin(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + drop(pending); + assert_eq!(batch.cancel(), vec![0]); + } +} diff --git a/crates/edgezero-core/src/outbound/dispatch.rs b/crates/edgezero-core/src/outbound/dispatch.rs new file mode 100644 index 000000000..574019d46 --- /dev/null +++ b/crates/edgezero-core/src/outbound/dispatch.rs @@ -0,0 +1,756 @@ +use std::mem::take; +use std::task::Poll; + +use futures_util::StreamExt as _; +use futures_util::future::poll_fn; + +use super::{ + HttpClient, Method, OutboundDispatchRequest, OutboundRequest, OutboundRequestParts, + OutboundResponseContext, OutboundScope, OutboundSendMode, OutboundTime, ResponseBodyPolicy, + normalize_for_dispatch, validate_for_dispatch, +}; +use crate::error::EdgeError; +use crate::http::StatusCode; +use crate::time::{ + Deadline, DispatchBudget, MonotonicClock, MonotonicInstant, dispatch_budget_inputs, +}; + +#[derive(Clone, Copy, Default)] +pub(super) struct DispatchBounds { + pub observation_cutoff: Option, + pub original_chain_deadline: Option, +} + +/// Private forwarding metadata, not a second body or a public policy override. +#[derive(Clone)] +pub(crate) struct ForwardedReadDeadline(Deadline); + +impl ForwardedReadDeadline { + pub(crate) fn deadline(&self) -> Deadline { + self.0 + } + pub(crate) fn new(deadline: Deadline) -> Self { + Self(deadline) + } +} + +impl OutboundDispatchRequest { + #[must_use] + #[inline] + pub fn budget(&self) -> DispatchBudget { + self.context.budget + } + + #[must_use] + #[inline] + pub fn clock(&self) -> &MonotonicClock { + self.context.time.clock() + } + + #[must_use] + #[inline] + pub fn entered_at(&self) -> MonotonicInstant { + self.context.entered_at + } + + #[must_use] + #[inline] + pub fn into_parts(self) -> (OutboundRequestParts, OutboundResponseContext) { + (self.request.into_parts(), self.context) + } + + #[must_use] + #[inline] + pub fn request(&self) -> &OutboundRequest { + &self.request + } + + #[must_use] + #[inline] + pub fn scope(&self) -> &OutboundScope { + &self.context.scope + } + + #[must_use] + #[inline] + pub fn time(&self) -> &OutboundTime { + &self.context.time + } +} + +pub(super) fn prepare( + client: &HttpClient, + mut request: OutboundRequest, + mode: OutboundSendMode, + time: OutboundTime, + entered_at: MonotonicInstant, + bounds: DispatchBounds, + scope: OutboundScope, +) -> Result { + scope.check()?; + client.inner.validate_supported_options(&request, mode)?; + scope.check()?; + validate_for_dispatch(&request, mode)?; + let budget = dispatch_budget_inputs( + request.budget_inputs(), + entered_at, + bounds.observation_cutoff, + bounds.original_chain_deadline, + )?; + request = client.resolve_proxy_marker(request); + normalize_for_dispatch(&mut request)?; + request.body = take(&mut request.body).bind_outbound_scope(&scope)?; + scope.check()?; + let body_policy = ResponseBodyPolicy { + max_brotli_window_bits: request.max_brotli_window_bits, + max_chunk_bytes: request.max_chunk_bytes, + max_decoded_response_bytes: request.max_decoded_response_bytes, + max_decoder_bytes: request.max_decoder_bytes, + max_encoded_response_bytes: request.max_encoded_response_bytes, + response_decoding: request.response_decoding, + response_mode: request.response_mode, + status: StatusCode::OK, + }; + let context = OutboundResponseContext { + body_policy, + budget, + entered_at, + max_response_header_bytes: request.max_response_header_bytes, + max_response_header_count: request.max_response_header_count, + mode, + original_chain_deadline: bounds.original_chain_deadline.or(request.deadline), + proxy_marker: request.proxy_marker, + request_method: request.method.clone(), + scope, + time, + }; + Ok(OutboundDispatchRequest { context, request }) +} + +pub(super) async fn settle( + mut prepared: OutboundDispatchRequest, +) -> Result { + let scope = prepared.scope().clone(); + let time = prepared.time().clone(); + let mut previous = prepared.entered_at(); + let entry_now = settlement_now(&time, &scope, &mut previous)?; + if !matches!(prepared.request.method, Method::GET | Method::HEAD) + || !prepared.request.body.is_stream() + { + if prepared.budget().deadline.is_expired_at(entry_now) { + return Err(EdgeError::gateway_timeout_caused( + "outbound request deadline exceeded", + prepared.budget().cause, + )); + } + return Ok(prepared); + } + let outbound = prepared.budget(); + let inbound = prepared.request.inbound_read_deadline; + let deadline = inbound + .filter(|bound| bound.instant() <= outbound.deadline.instant()) + .unwrap_or(outbound.deadline); + let mut timer = time.wait_until(deadline); + let body = take(&mut prepared.request.body); + let mut source = body.into_stream().map_err(|unexpected_body| { + drop(unexpected_body); + EdgeError::internal(anyhow::anyhow!( + "outbound settlement lost its streamed body" + )) + })?; + poll_fn(|cx| { + settlement_now(&time, &scope, &mut previous)?; + // The paired waiter preserves registered faults even at expiry. An expired unarmed + // wait completes without creating a timer, and the body is still never polled. + match timer.as_mut().poll(cx) { + Poll::Ready(Err(error)) => return Poll::Ready(Err(error)), + Poll::Ready(Ok(())) => { + return Poll::Ready(Err(settlement_expiry( + inbound, + outbound, + time.clock().now(), + ) + .unwrap_or_else(|| { + EdgeError::internal(anyhow::anyhow!( + "settlement timer completed before its deadline" + )) + }))); + } + Poll::Pending => {} + } + scope.check()?; + let polled = source.poll_next_unpin(cx); + let after = settlement_now(&time, &scope, &mut previous)?; + if let Some(error) = settlement_expiry(inbound, outbound, after) { + return Poll::Ready(Err(error)); + } + match polled { + Poll::Pending => Poll::Pending, + Poll::Ready(None) => Poll::Ready(Ok(())), + Poll::Ready(Some(Err(error))) => Poll::Ready(Err(error)), + Poll::Ready(Some(Ok(bytes))) if !bytes.is_empty() => Poll::Ready(Err( + EdgeError::bad_request("GET/HEAD request must not carry a body"), + )), + Poll::Ready(Some(Ok(_))) => { + cx.waker().wake_by_ref(); + Poll::Pending + } + } + }) + .await?; + drop(source); + drop(timer); + let released_at = settlement_now(&time, &scope, &mut previous)?; + if outbound.deadline.is_expired_at(released_at) { + return Err(EdgeError::gateway_timeout_caused( + "outbound request deadline exceeded", + outbound.cause, + )); + } + Ok(prepared) +} + +fn settlement_expiry( + inbound: Option, + outbound: DispatchBudget, + now: MonotonicInstant, +) -> Option { + if inbound.is_some_and(|bound| { + bound.instant() <= outbound.deadline.instant() && bound.is_expired_at(now) + }) { + Some(EdgeError::request_timeout( + "inbound body read deadline exceeded", + )) + } else if outbound.deadline.is_expired_at(now) { + Some(EdgeError::gateway_timeout_caused( + "outbound request deadline exceeded", + outbound.cause, + )) + } else { + None + } +} + +fn settlement_now( + time: &OutboundTime, + scope: &OutboundScope, + previous: &mut MonotonicInstant, +) -> Result { + scope.check()?; + let now = time.clock().now(); + scope.check()?; + if now < *previous { + return Err(EdgeError::internal(anyhow::anyhow!( + "monotonic clock moved backwards during outbound settlement" + ))); + } + *previous = now; + Ok(now) +} + +#[cfg(test)] +mod tests { + use std::rc::Rc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll, Waker}; + use std::time::Duration; + + use bytes::Bytes; + use futures::executor::block_on; + use futures_util::future::poll_fn; + use futures_util::{StreamExt as _, stream}; + + use crate::http::{HeaderMap, StatusCode}; + use crate::{ + Body, Deadline, EdgeError, HttpClient, IndependentOutboundProgress, MonotonicClock, + MonotonicInstant, OutboundBatch, OutboundBatchDriverEvent, OutboundDispatchRequest, + OutboundExchangeOwner, OutboundFuture, OutboundHead, OutboundHeaderFidelity, + OutboundHeaderInput, OutboundHttpClient, OutboundPending, OutboundPreparedBatch, + OutboundProgressRuntime, OutboundRequest, OutboundScope, OutboundScopeOwner, + OutboundSendMode, OutboundTime, + }; + + #[derive(Clone)] + struct Fake { + dispatches: Arc, + state: Arc)>>, + timers: Arc, + } + + struct Client { + fake: Fake, + refuse: bool, + } + + #[derive(Debug, thiserror::Error)] + #[error("original operational timer fault")] + struct OperationalTimerFault(Arc<()>); + + impl Fake { + fn advance(&self, duration: Duration) { + let wake = { + let mut state = self.state.lock().expect("clock"); + state.0 = state.0.checked_add(duration).expect("fake advancement"); + state.1.take() + }; + if let Some(waker) = wake { + waker.wake(); + } + } + + fn client(&self, refuse: bool) -> HttpClient { + HttpClient::with_client(Client { + fake: self.clone(), + refuse, + }) + } + + fn new() -> Self { + Self { + state: Arc::new(Mutex::new((MonotonicInstant::now(), None))), + dispatches: Arc::new(AtomicUsize::new(0)), + timers: Arc::new(AtomicUsize::new(0)), + } + } + + fn pair(&self) -> OutboundTime { + let clock_state = Arc::clone(&self.state); + let clock = MonotonicClock::new(move || clock_state.lock().expect("clock").0); + let wait_state = Arc::clone(&self.state); + let timers = Arc::clone(&self.timers); + OutboundTime::new(clock, move |deadline, _| { + timers.fetch_add(1, Ordering::SeqCst); + let state = Arc::clone(&wait_state); + Box::pin(poll_fn(move |cx| { + let mut current = state.lock().expect("timer"); + if deadline.is_expired_at(current.0) { + Poll::Ready(Ok(())) + } else { + current.1 = Some(cx.waker().clone()); + Poll::Pending + } + })) + }) + } + } + + impl OutboundHttpClient for Client { + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let head = block_on(self.dispatch_head(request))?; + let outcome = block_on(head.finish()); + Ok(OutboundPending::new( + Box::pin(async move { outcome }), + OutboundExchangeOwner::new(|| {}), + )) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + self.fake.dispatches.fetch_add(1, Ordering::SeqCst); + let (_, context) = request.into_parts(); + Box::pin(async move { + OutboundHead::new( + StatusCode::OK, + OutboundHeaderInput::new( + "fixture", + HeaderMap::new(), + OutboundHeaderFidelity::AdapterVisibleFields, + ), + stream::empty().boxed_local(), + context, + OutboundExchangeOwner::new(|| {}), + ) + }) + } + + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, slots) = batch.into_slots(); + if slots.is_empty() { + return OutboundBatch::cutoff(context); + } + let events = slots + .into_iter() + .map(|(index, request)| { + let outcome = block_on(self.dispatch_head(request)) + .and_then(|head| block_on(head.finish())); + OutboundBatchDriverEvent::Item { + index, + completed_at: context.clock().now(), + outcome, + } + }) + .collect::>(); + OutboundBatch::from_driver( + context, + stream::iter(events), + OutboundExchangeOwner::new(|| {}), + ) + } + + fn time(&self) -> OutboundTime { + self.fake.pair() + } + + fn validate_supported_options( + &self, + _: &OutboundRequest, + _: OutboundSendMode, + ) -> Result<(), EdgeError> { + if self.refuse { + Err(EdgeError::bad_request("unsupported fixture option")) + } else { + Ok(()) + } + } + } + + #[test] + fn prepared_upload_revokes_a_reentrant_chunk_before_the_transport_can_write() { + let fake = Fake::new(); + let owner = Rc::new(OutboundScopeOwner::callback()); + let close_from_source = Rc::clone(&owner); + let request = OutboundRequest::post("https://example.com/") + .expect("request") + .body(Body::from_stream(stream::poll_fn(move |_| { + close_from_source.close(); + Poll::Ready(Some(Ok(Bytes::from_static(b"must not be written")))) + }))); + let time = fake.pair(); + let prepared = owner + .with(|| { + super::prepare( + &fake.client(false), + request, + OutboundSendMode::Send, + time.clone(), + time.clock().now(), + super::DispatchBounds::default(), + OutboundScope::capture(Rc::new(IndependentOutboundProgress)), + ) + }) + .expect("prepared upload"); + let (parts, _context) = prepared.into_parts(); + let mut source = parts.body.into_stream().expect("streamed upload"); + let observed = block_on(source.next()); + assert!( + matches!(observed, Some(Err(EdgeError::Internal { .. }))), + "reentrant closure must suppress the source chunk before a provider write" + ); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 0); + } + + #[test] + fn fixture_expired_batch_preserves_cutoff_without_dispatch() { + use crate::{OutboundBatchTermination, OutboundCollectedSlot}; + + let fake = Fake::new(); + let observation_cutoff = Deadline::at_instant(fake.pair().clock().now()); + let results = block_on( + fake.client(false) + .start_batch_until( + vec![OutboundRequest::get("https://example.com/").expect("request")], + observation_cutoff, + ) + .collect(), + ) + .expect("an expired observation cutoff is not driver EOF"); + assert_eq!(results.termination(), OutboundBatchTermination::Cutoff); + assert!(matches!( + results.slots(), + [OutboundCollectedSlot::Unresolved] + )); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 0); + assert_eq!(fake.timers.load(Ordering::SeqCst), 0); + } + + #[test] + fn invocation_budget_is_not_restarted_before_first_poll() { + let fake = Fake::new(); + let send = fake.client(false).send( + OutboundRequest::get("https://example.com/") + .expect("request") + .timeout(Duration::from_secs(1)), + ); + fake.advance(Duration::from_secs(2)); + assert!(matches!( + block_on(send), + Err(EdgeError::GatewayTimeout { .. }) + )); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 0); + } + + #[test] + fn settlement_timer_wakes_without_body_data() { + let fake = Fake::new(); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .body(Body::from_stream(stream::pending())) + .timeout(Duration::from_secs(1)); + let mut send = fake.client(false).send(request); + assert!( + send.as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert_eq!(fake.timers.load(Ordering::SeqCst), 1); + fake.advance(Duration::from_secs(1)); + assert!(matches!( + block_on(send), + Err(EdgeError::GatewayTimeout { .. }) + )); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 0); + } + + #[test] + fn adapter_preflight_precedes_pending_settlement() { + let fake = Fake::new(); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .body(Body::from_stream(stream::poll_fn(|_| { + panic!("must not read") + }))) + .deadline(Deadline::at_instant(fake.pair().clock().now())); + assert!(matches!( + block_on(fake.client(true).send(request)), + Err(EdgeError::BadRequest { .. }) + )); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 0); + assert_eq!(fake.timers.load(Ordering::SeqCst), 0); + } + + #[test] + fn get_head_first_nonempty_byte_is_rejected_before_dispatch() { + let fake = Fake::new(); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .body(Body::from_stream(stream::iter([ + Ok(bytes::Bytes::new()), + Ok(bytes::Bytes::from_static(b"x")), + ]))); + assert!(matches!( + block_on(fake.client(false).send(request)), + Err(EdgeError::BadRequest { .. }) + )); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 0); + } + + #[test] + fn get_head_stream_eof_dispatches_certified_empty_body() { + let fake = Fake::new(); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .body(Body::from_stream(stream::empty())); + block_on(fake.client(false).send(request)).expect("empty streamed forwarding"); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 1); + } + + #[test] + fn forwarded_inbound_deadline_wins_earlier_and_equal_limits() { + use super::ForwardedReadDeadline; + use crate::http::request_builder; + + for inbound_ms in [50, 100] { + for round_trip in [false, true] { + for late_first_poll in [false, true] { + let fake = Fake::new(); + let deadline = fake + .pair() + .clock() + .deadline_after(Duration::from_millis(inbound_ms)); + let mut inbound = request_builder() + .method("HEAD") + .uri("/") + .body(Body::from_stream(stream::pending())) + .expect("inbound"); + inbound + .extensions_mut() + .insert(ForwardedReadDeadline::new(deadline)); + let forwarded = OutboundRequest::from_request( + inbound, + "https://example.com/".parse().expect("target"), + ) + .expect("forward") + .timeout(Duration::from_millis(100)); + let request = if round_trip { + OutboundRequest::from_parts(forwarded.into_parts()).expect("round trip") + } else { + forwarded + }; + let mut send = fake.client(false).send(request); + if late_first_poll { + fake.advance(Duration::from_millis(200)); + } else { + assert!( + send.as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + fake.advance(Duration::from_millis(inbound_ms)); + } + assert!(matches!( + block_on(send), + Err(EdgeError::RequestTimeout { .. }) + )); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 0); + if late_first_poll { + assert_eq!(fake.timers.load(Ordering::SeqCst), 0); + } + } + } + } + } + + #[test] + fn earlier_outbound_settlement_deadline_retains_selected_cause() { + use super::ForwardedReadDeadline; + use crate::error::BudgetSource; + use crate::http::request_builder; + + let fake = Fake::new(); + let mut inbound = request_builder() + .method("GET") + .uri("/") + .body(Body::from_stream(stream::pending())) + .expect("inbound"); + inbound.extensions_mut().insert(ForwardedReadDeadline::new( + fake.pair().clock().deadline_after(Duration::from_secs(10)), + )); + let request = + OutboundRequest::from_request(inbound, "https://example.com/".parse().expect("target")) + .expect("forward") + .timeout(Duration::from_millis(100)); + let mut begin = fake.client(false).begin(request); + assert!( + begin + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + fake.advance(Duration::from_millis(100)); + assert!(matches!( + block_on(begin), + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + )); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 0); + } + + #[test] + fn settlement_preserves_original_nonclone_source_error() { + #[derive(Debug, thiserror::Error)] + #[error("fixture source failure")] + struct Original(Arc<()>); + + let fake = Fake::new(); + let identity = Arc::new(()); + let error = EdgeError::internal(anyhow::Error::new(Original(Arc::clone(&identity)))); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .body(Body::from_stream(stream::iter([Err(error)]))); + let observed = block_on(fake.client(false).send(request)).expect_err("source failure"); + let original = observed + .inner() + .expect("source") + .downcast_ref::() + .expect("identity"); + assert!(Arc::ptr_eq(&identity, &original.0)); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 0); + } + + #[test] + fn settlement_registered_timer_fault_survives_deadline_expiry() { + let fake = Fake::new(); + let identity = Arc::new(()); + let timer_identity = Arc::clone(&identity); + let timer_polls = Arc::new(AtomicUsize::new(0)); + let captured_polls = Arc::clone(&timer_polls); + let timer_state = Arc::clone(&fake.state); + let time = OutboundTime::new(fake.pair().clock().clone(), move |_, _| { + let mut fault = Some(EdgeError::internal(OperationalTimerFault(Arc::clone( + &timer_identity, + )))); + let polls = Arc::clone(&captured_polls); + let state = Arc::clone(&timer_state); + let mut armed = false; + Box::pin(poll_fn(move |cx| { + polls.fetch_add(1, Ordering::SeqCst); + if armed { + Poll::Ready(Err(fault.take().expect("single operational fault"))) + } else { + armed = true; + state.lock().expect("timer registration").1 = Some(cx.waker().clone()); + Poll::Pending + } + })) + }); + let body_polls = Arc::new(AtomicUsize::new(0)); + let captured_body_polls = Arc::clone(&body_polls); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .timeout(Duration::from_millis(100)) + .body(Body::from_stream(stream::poll_fn(move |_| { + captured_body_polls.fetch_add(1, Ordering::SeqCst); + Poll::Pending + }))); + let entered_at = time.clock().now(); + let prepared = super::prepare( + &fake.client(false), + request, + OutboundSendMode::Send, + time, + entered_at, + super::DispatchBounds::default(), + OutboundScope::capture(Rc::new(IndependentOutboundProgress)), + ) + .expect("prepared"); + let mut settlement = Box::pin(super::settle(prepared)); + assert!( + settlement + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert_eq!(timer_polls.load(Ordering::SeqCst), 1); + assert_eq!(body_polls.load(Ordering::SeqCst), 1); + fake.advance(Duration::from_millis(100)); + let error = block_on(settlement).expect_err("operational timer failure"); + let EdgeError::Internal { source, .. } = error else { + panic!("registered timer fault must win over expiry, received {error:?}"); + }; + assert!(Arc::ptr_eq( + &source + .downcast_ref::() + .expect("original timer fault") + .0, + &identity + )); + assert_eq!(timer_polls.load(Ordering::SeqCst), 2); + assert_eq!(body_polls.load(Ordering::SeqCst), 1); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 0); + } + + #[test] + fn settlement_expired_before_first_poll_reads_no_body_and_registers_no_timer() { + let fake = Fake::new(); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .timeout(Duration::from_millis(100)) + .body(Body::from_stream(stream::poll_fn(|_| { + panic!("expired settlement must not read") + }))); + let send = fake.client(false).send(request); + fake.advance(Duration::from_millis(100)); + assert!(matches!( + block_on(send), + Err(EdgeError::GatewayTimeout { .. }) + )); + assert_eq!(fake.timers.load(Ordering::SeqCst), 0); + assert_eq!(fake.dispatches.load(Ordering::SeqCst), 0); + } +} diff --git a/crates/edgezero-core/src/outbound/exchange.rs b/crates/edgezero-core/src/outbound/exchange.rs new file mode 100644 index 000000000..7c5b98625 --- /dev/null +++ b/crates/edgezero-core/src/outbound/exchange.rs @@ -0,0 +1,641 @@ +use std::fmt; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use futures_util::future::poll_fn; + +use super::{OutboundFuture, OutboundTime}; +use crate::error::EdgeError; +use crate::time::{Deadline, MonotonicClock, MonotonicInstant}; + +impl OutboundTime { + /// Returns the clock supplied to both this pair and its timer factory. + #[must_use] + #[inline] + pub fn clock(&self) -> &MonotonicClock { + &self.clock + } + + /// Pairs a clock with an owned local timer factory, without adding an executor to core. + /// + /// The factory receives the unchanged absolute deadline and the same application clock. + /// It must register real wakeups, return setup/operational failures as typed errors, and + /// unregister pending work when its returned future is dropped. Only the shared factory + /// must be `Send + Sync`; each timer may own local SDK handles or `Rc` values. + #[must_use] + #[inline] + pub fn new(clock: MonotonicClock, wait: Wait) -> Self + where + Wait: Fn(Deadline, MonotonicClock) -> OutboundFuture<()> + Send + Sync + 'static, + { + Self { + clock, + wait: Arc::new(wait), + } + } + + /// Waits until the paired clock reaches the original absolute deadline. + /// + /// A timer's success is only a wakeup: an early wake retires that timer and yields before + /// rearming the same deadline. At most one timer is created and polled per outer poll. + /// Dropping the wait cancels its timer registration; a terminal poll also releases the + /// captured factory and clock even if the completed boxed future is retained. + /// + /// # Errors + /// Returns timer failures unchanged, or typed Internal for an observed clock regression. + /// Expiry itself is successful wait completion; the caller classifies its governing budget. + #[must_use] + #[inline] + pub fn wait_until(&self, deadline: Deadline) -> OutboundFuture<()> { + let mut paired = Some(self.clone()); + let mut previous = self.clock.now(); + let mut timer = None; + Box::pin(poll_fn(move |context| { + let result = if let Some(time) = &paired { + poll_wait_until(time, deadline, &mut previous, &mut timer, context) + } else { + Poll::Ready(Err(EdgeError::internal(anyhow::anyhow!( + "outbound deadline wait polled after completion" + )))) + }; + if result.is_ready() { + drop(timer.take()); + drop(paired.take()); + } + result + })) + } +} + +impl fmt::Debug for OutboundTime { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutboundTime") + .field("clock", &self.clock) + .finish_non_exhaustive() + } +} + +fn checked_now( + clock: &MonotonicClock, + previous: &mut MonotonicInstant, +) -> Result { + let now = clock.now(); + if now < *previous { + return Err(EdgeError::internal(anyhow::anyhow!( + "monotonic clock moved backwards during outbound wait" + ))); + } + *previous = now; + Ok(now) +} + +fn poll_wait_until( + time: &OutboundTime, + deadline: Deadline, + previous: &mut MonotonicInstant, + timer: &mut Option>, + context: &mut Context<'_>, +) -> Poll> { + let now = checked_now(time.clock(), previous)?; + if timer.is_none() && deadline.is_expired_at(now) { + return Poll::Ready(Ok(())); + } + // Observe an existing timer's failure even when this poll arrives at/past expiry. + let pending = timer.get_or_insert_with(|| (time.wait)(deadline, time.clock.clone())); + let observation = pending.as_mut().poll(context); + let woke = observation.is_ready(); + if let Poll::Ready(outcome) = observation { + drop(timer.take()); + outcome?; + } + if deadline.is_expired_at(checked_now(time.clock(), previous)?) { + return Poll::Ready(Ok(())); + } + if woke { + context.waker().wake_by_ref(); + } + Poll::Pending +} + +#[cfg(test)] +mod tests { + use std::cell::Cell; + use std::collections::BTreeMap; + use std::future::Future; + use std::pin::Pin; + use std::rc::Rc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll, Waker}; + use std::time::Duration; + + use futures_util::future::pending; + use futures_util::task::{ArcWake, waker_ref}; + + use crate::error::EdgeError; + use crate::outbound::{OutboundFuture, OutboundTime}; + use crate::time::{Deadline, MonotonicClock, MonotonicInstant}; + + #[derive(Clone)] + struct FakeTime { + state: Arc>, + } + + struct FakeTimeState { + deadlines: Vec, + drops: usize, + early_ready: bool, + now: MonotonicInstant, + waiters: BTreeMap)>, + } + + struct FakeTimer { + deadline: Deadline, + id: usize, + local_polls: Rc>, + state: Arc>, + } + + struct FaultTimer { + error: Option, + pending_first: bool, + release: ReleaseCount, + succeeds_early: bool, + } + + struct ReleaseCount(Arc); + + #[derive(Debug, thiserror::Error)] + #[error("fixture timer failure")] + struct TimerFault { + identity: Arc<()>, + } + + #[derive(Default)] + struct WakeCount(AtomicUsize); + + impl FakeTime { + fn advance_to(&self, now: MonotonicInstant) { + let wakers: Vec<_> = { + let mut state = self.state.lock().expect("fake time"); + state.now = now; + state + .waiters + .values_mut() + .filter(|(deadline, _)| deadline.is_expired_at(now)) + .filter_map(|(_, waker)| waker.take()) + .collect() + }; + for waker in wakers { + waker.wake(); + } + } + + fn clock(&self) -> MonotonicClock { + let state = Arc::clone(&self.state); + MonotonicClock::new(move || state.lock().expect("clock").now) + } + + fn counts(&self) -> (usize, usize, usize) { + let state = self.state.lock().expect("fake time"); + (state.deadlines.len(), state.waiters.len(), state.drops) + } + + fn new() -> Self { + let now = MonotonicInstant::now() + .checked_add(Duration::from_hours(1)) + .expect("synthetic clock differs from process time on every target"); + Self { + state: Arc::new(Mutex::new(FakeTimeState { + deadlines: Vec::new(), + drops: 0, + early_ready: false, + now, + waiters: BTreeMap::new(), + })), + } + } + + fn pair(&self) -> OutboundTime { + let state = Arc::clone(&self.state); + OutboundTime::new(self.clock(), move |deadline, clock| { + let paired_now = clock.now(); + let mut shared = state.lock().expect("timer factory"); + assert_eq!(paired_now, shared.now, "factory receives the app clock"); + assert!(shared.deadlines.len() < 16, "timer creation must not spin"); + if shared.early_ready { + assert!(shared.waiters.is_empty(), "retire before rearming"); + } + let id = shared.deadlines.len(); + shared.deadlines.push(deadline.instant()); + shared.waiters.insert(id, (deadline, None)); + Box::pin(FakeTimer { + deadline, + id, + local_polls: Rc::new(Cell::new(0)), + state: Arc::clone(&state), + }) + }) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for FakeTimer { + fn drop(&mut self) { + let mut state = self.state.lock().expect("timer drop"); + assert!(state.waiters.remove(&self.id).is_some(), "release once"); + state.drops = state.drops.saturating_add(1); + } + } + + impl Future for FakeTimer { + type Output = Result<(), EdgeError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.local_polls + .set(self.local_polls.get().saturating_add(1)); + assert!(self.local_polls.get() <= 16, "pending timer must not spin"); + let mut state = self.state.lock().expect("timer poll"); + if state.early_ready || self.deadline.is_expired_at(state.now) { + Poll::Ready(Ok(())) + } else { + let (_, waker) = state.waiters.get_mut(&self.id).expect("live registration"); + *waker = Some(cx.waker().clone()); + Poll::Pending + } + } + } + + impl Future for FaultTimer { + type Output = Result<(), EdgeError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let timer = self.get_mut(); + assert!( + Arc::strong_count(&timer.release.0) > 1, + "retain the release probe" + ); + if timer.pending_first { + timer.pending_first = false; + cx.waker().wake_by_ref(); + Poll::Pending + } else if timer.succeeds_early { + Poll::Ready(Ok(())) + } else { + Poll::Ready(Err(timer.error.take().expect("one terminal error"))) + } + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for ReleaseCount { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + impl ArcWake for WakeCount { + fn wake(self: Arc) { + Self::wake_by_ref(&self); + } + + fn wake_by_ref(arc_self: &Arc) { + arc_self.0.fetch_add(1, Ordering::SeqCst); + } + } + + fn poll_wait( + future: &mut OutboundFuture<()>, + wake_count: &Arc, + ) -> Poll> { + let waker = waker_ref(wake_count); + future.as_mut().poll(&mut Context::from_waker(&waker)) + } + + #[test] + fn outbound_time_advancement_wakes_registered_waker() { + let fake = FakeTime::new(); + let pair = fake.pair(); + let deadline = pair.clock().deadline_after(Duration::from_millis(100)); + let mut wait = pair.wait_until(deadline); + drop(pair); + let old_waker = Arc::new(WakeCount::default()); + let new_waker = Arc::new(WakeCount::default()); + assert!(poll_wait(&mut wait, &old_waker).is_pending()); + assert!(poll_wait(&mut wait, &new_waker).is_pending()); + assert_eq!(fake.counts(), (1, 1, 0), "retain the same pending timer"); + fake.advance_to( + deadline + .instant() + .checked_sub(Duration::from_nanos(1)) + .expect("before deadline"), + ); + assert_eq!(old_waker.0.load(Ordering::SeqCst), 0); + assert_eq!(new_waker.0.load(Ordering::SeqCst), 0); + fake.advance_to(deadline.instant()); + assert_eq!(old_waker.0.load(Ordering::SeqCst), 0); + assert_eq!(new_waker.0.load(Ordering::SeqCst), 1); + assert!(matches!( + poll_wait(&mut wait, &new_waker), + Poll::Ready(Ok(())) + )); + assert_eq!(fake.counts(), (1, 0, 1)); + } + + #[test] + fn outbound_time_early_ready_drops_before_rearming_original_deadline() { + let fake = FakeTime::new(); + fake.state.lock().expect("fake time").early_ready = true; + let pair = fake.pair(); + let deadline = pair.clock().deadline_after(Duration::from_millis(100)); + let mut wait = pair.wait_until(deadline); + let wakes = Arc::new(WakeCount::default()); + assert!(poll_wait(&mut wait, &wakes).is_pending()); + assert_eq!(fake.counts(), (1, 0, 1)); + fake.state.lock().expect("fake time").early_ready = false; + fake.advance_to( + deadline + .instant() + .checked_sub(Duration::from_millis(20)) + .expect("before deadline"), + ); + assert!(poll_wait(&mut wait, &wakes).is_pending()); + assert_eq!(fake.counts(), (2, 1, 1)); + assert_eq!( + fake.state.lock().expect("fake time").deadlines, + vec![deadline.instant(), deadline.instant()] + ); + fake.advance_to(deadline.instant()); + assert!(matches!(poll_wait(&mut wait, &wakes), Poll::Ready(Ok(())))); + assert_eq!(fake.counts(), (2, 0, 2)); + } + + #[test] + fn outbound_time_frozen_ready_timer_yields_without_spinning() { + let fake = FakeTime::new(); + fake.state.lock().expect("fake time").early_ready = true; + let pair = fake.pair(); + let deadline = pair.clock().deadline_after(Duration::from_millis(100)); + let mut wait = pair.wait_until(deadline); + let wakes = Arc::new(WakeCount::default()); + for count in 1..=3 { + assert!(poll_wait(&mut wait, &wakes).is_pending()); + assert_eq!(fake.counts(), (count, 0, count)); + assert_eq!(wakes.0.load(Ordering::SeqCst), count); + } + fake.advance_to(deadline.instant()); + assert!(matches!(poll_wait(&mut wait, &wakes), Poll::Ready(Ok(())))); + assert_eq!(fake.counts(), (3, 0, 3)); + } + + #[test] + fn outbound_time_timer_errors_are_preserved() { + for mode in 0_u8..4 { + let fake = FakeTime::new(); + let identity = Arc::new(()); + let captured_identity = Arc::clone(&identity); + let calls = Arc::new(AtomicUsize::new(0)); + let captured_calls = Arc::clone(&calls); + let drops = Arc::new(AtomicUsize::new(0)); + let captured_drops = Arc::clone(&drops); + let pair = OutboundTime::new(fake.clock(), move |_, _| { + let call = captured_calls.fetch_add(1, Ordering::SeqCst); + let timer_identity = Arc::clone(&captured_identity); + assert_eq!( + captured_drops.load(Ordering::SeqCst), + call, + "retire before rearm" + ); + Box::pin(FaultTimer { + error: Some(EdgeError::internal(anyhow::Error::new(TimerFault { + identity: timer_identity, + }))), + pending_first: mode == 1 || mode == 3, + release: ReleaseCount(Arc::clone(&captured_drops)), + succeeds_early: mode == 2 && call == 0, + }) + }); + let deadline = pair.clock().deadline_after(Duration::from_secs(1)); + let mut wait = pair.wait_until(deadline); + let wakes = Arc::new(WakeCount::default()); + if mode != 0 { + assert!(poll_wait(&mut wait, &wakes).is_pending()); + } + if mode == 3 { + fake.advance_to(deadline.instant()); + } + let Poll::Ready(Err(EdgeError::Internal { source, .. })) = poll_wait(&mut wait, &wakes) + else { + panic!("timer failure must remain typed Internal"); + }; + let original = source + .downcast_ref::() + .expect("original source"); + assert!(Arc::ptr_eq(&identity, &original.identity)); + let expected = if mode == 2 { 2 } else { 1 }; + assert_eq!(calls.load(Ordering::SeqCst), expected); + assert_eq!(drops.load(Ordering::SeqCst), expected); + } + } + + #[test] + fn outbound_time_drop_cancels_registration_once() { + let fake = FakeTime::new(); + let pair = fake.pair(); + let deadline = pair.clock().deadline_after(Duration::from_secs(1)); + drop(pair.wait_until(deadline)); + assert_eq!(fake.counts(), (0, 0, 0)); + let mut first = pair.wait_until(deadline); + let mut second = pair.clone().wait_until(deadline); + let first_wakes = Arc::new(WakeCount::default()); + let second_wakes = Arc::new(WakeCount::default()); + assert!(poll_wait(&mut first, &first_wakes).is_pending()); + assert!(poll_wait(&mut second, &second_wakes).is_pending()); + drop(first); + assert_eq!(fake.counts(), (2, 1, 1)); + fake.advance_to(deadline.instant()); + assert_eq!(first_wakes.0.load(Ordering::SeqCst), 0); + assert_eq!(second_wakes.0.load(Ordering::SeqCst), 1); + assert!(matches!( + poll_wait(&mut second, &second_wakes), + Poll::Ready(Ok(())) + )); + drop(second); + assert_eq!(fake.counts(), (2, 0, 2)); + + fake.state.lock().expect("fake time").early_ready = true; + let next_deadline = pair.clock().deadline_after(Duration::from_secs(1)); + let mut early = pair.wait_until(next_deadline); + let early_wakes = Arc::new(WakeCount::default()); + assert!(poll_wait(&mut early, &early_wakes).is_pending()); + assert_eq!(fake.counts(), (3, 0, 3)); + drop(early); + fake.advance_to(next_deadline.instant()); + assert_eq!(early_wakes.0.load(Ordering::SeqCst), 1); + assert_eq!(fake.counts(), (3, 0, 3)); + } + + #[test] + fn outbound_time_terminal_poll_releases_factory_capture() { + for mode in 0_u8..3 { + let fake = FakeTime::new(); + let capture = Arc::new(()); + let weak = Arc::downgrade(&capture); + let state = Arc::clone(&fake.state); + let pair = OutboundTime::new(fake.clock(), move |deadline, _| { + let keep_alive = Arc::clone(&capture); + let timer_state = Arc::clone(&state); + Box::pin(async move { + if mode == 2 { + pending::<()>().await; + } + drop(keep_alive); + if mode == 1 { + Err(EdgeError::internal(anyhow::anyhow!( + "fixture setup failure" + ))) + } else { + timer_state.lock().expect("timer clock").now = deadline.instant(); + Ok(()) + } + }) + }); + let mut wait = pair.wait_until(pair.clock().deadline_after(Duration::from_secs(1))); + drop(pair); + assert!(weak.upgrade().is_some()); + let result = poll_wait(&mut wait, &Arc::new(WakeCount::default())); + if mode == 2 { + assert!(result.is_pending()); + assert!(weak.upgrade().is_some()); + } else { + assert!(result.is_ready()); + assert!(weak.upgrade().is_none(), "release before boxed future drop"); + } + drop(wait); + assert!(weak.upgrade().is_none()); + } + } + + #[test] + fn outbound_time_expired_waits_do_not_register_timers() { + let fake = FakeTime::new(); + let pair = fake.pair(); + let now = pair.clock().now(); + for instant in [ + now, + now.checked_sub(Duration::from_nanos(1)) + .expect("past instant"), + ] { + let mut wait = pair.wait_until(Deadline::at_instant(instant)); + assert!(matches!( + poll_wait(&mut wait, &Arc::new(WakeCount::default())), + Poll::Ready(Ok(())) + )); + } + assert_eq!(fake.counts(), (0, 0, 0)); + } + + #[test] + fn outbound_time_expired_pending_timer_is_cancelled_without_waiting() { + let fake = FakeTime::new(); + let drops = Arc::new(AtomicUsize::new(0)); + let captured_drops = Arc::clone(&drops); + let pair = OutboundTime::new(fake.clock(), move |_, _| { + let guard = ReleaseCount(Arc::clone(&captured_drops)); + Box::pin(async move { + pending::<()>().await; + drop(guard); + Ok(()) + }) + }); + let deadline = pair.clock().deadline_after(Duration::from_secs(1)); + let mut wait = pair.wait_until(deadline); + let wakes = Arc::new(WakeCount::default()); + assert!(poll_wait(&mut wait, &wakes).is_pending()); + assert_eq!(drops.load(Ordering::SeqCst), 0); + fake.advance_to(deadline.instant()); + assert!(matches!(poll_wait(&mut wait, &wakes), Poll::Ready(Ok(())))); + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert_eq!(wakes.0.load(Ordering::SeqCst), 0); + } + + #[test] + fn outbound_time_backwards_clock_is_internal_and_releases_registration() { + for registered in [false, true] { + let fake = FakeTime::new(); + let pair = fake.pair(); + let start = pair.clock().now(); + let mut wait = pair.wait_until(pair.clock().deadline_after(Duration::from_secs(1))); + let wakes = Arc::new(WakeCount::default()); + if registered { + assert!(poll_wait(&mut wait, &wakes).is_pending()); + } + fake.advance_to( + start + .checked_sub(Duration::from_nanos(1)) + .expect("past instant"), + ); + assert!(matches!( + poll_wait(&mut wait, &wakes), + Poll::Ready(Err(EdgeError::Internal { .. })) + )); + let count = usize::from(registered); + assert_eq!(fake.counts(), (count, 0, count)); + } + } + + #[test] + fn outbound_time_rechecks_clock_after_timer_success() { + for backwards in [false, true] { + let fake = FakeTime::new(); + let start = fake.clock().now(); + let deadline = fake.clock().deadline_after(Duration::from_secs(1)); + let state = Arc::clone(&fake.state); + let pair = OutboundTime::new(fake.clock(), move |_, _| { + let timer_state = Arc::clone(&state); + Box::pin(async move { + timer_state.lock().expect("timer clock").now = if backwards { + start + .checked_sub(Duration::from_nanos(1)) + .expect("past instant") + } else { + deadline.instant() + }; + Ok(()) + }) + }); + let mut wait = pair.wait_until(deadline); + let result = poll_wait(&mut wait, &Arc::new(WakeCount::default())); + if backwards { + assert!(matches!( + result, + Poll::Ready(Err(EdgeError::Internal { .. })) + )); + } else { + assert!(matches!(result, Poll::Ready(Ok(())))); + } + } + } + + #[test] + fn outbound_time_is_clone_send_sync_with_owned_local_futures() { + fn assert_shared() + where + T: Clone + Send + Sync, + { + } + assert_shared::(); + let fake = FakeTime::new(); + let pair = fake.pair(); + let wait: crate::OutboundFuture<()> = + pair.wait_until(pair.clock().deadline_after(Duration::from_secs(1))); + drop(wait); + } +} diff --git a/crates/edgezero-core/src/outbound/head.rs b/crates/edgezero-core/src/outbound/head.rs new file mode 100644 index 000000000..aa74e02c9 --- /dev/null +++ b/crates/edgezero-core/src/outbound/head.rs @@ -0,0 +1,1812 @@ +use std::fmt; +use std::pin::Pin; +use std::str; +use std::task::{Context, Poll}; + +use async_stream::stream; +use bytes::Bytes; +use futures_util::future::poll_fn; +use futures_util::{Stream, StreamExt as _}; + +use super::{ + ContentTypeProvenance, OutboundExchangeOwner, OutboundFuture, OutboundHead, + OutboundHeaderFidelity, OutboundHeaderInput, OutboundLocationEvidence, OutboundProxyMarker, + OutboundResponse, OutboundResponseContext, OutboundScope, OutboundScopeKind, + OutboundScopeOwner, OutboundSendMode, OutboundTime, ResponseBodyDisposition, + ResponseBodyPolicy, ResponseHeadNormalization, ResponseHeaderLimiter, ResponseMode, + apply_response_body_policy, collect_response_stream_inner, insert_proxy_header, + normalize_response_headers, +}; +use crate::body::{Body, BodyPayload, BodyStream}; +use crate::error::{BudgetSource, EdgeError}; +use crate::http::header::LOCATION; +use crate::http::{HeaderMap, HeaderValue, Method, StatusCode}; +use crate::time::{Deadline, DispatchBudget, MonotonicClock, MonotonicInstant}; + +struct OwnedPayload { + body_progress: bool, + cause: BudgetSource, + deadline: Deadline, + entered_at: MonotonicInstant, + owner: Option, + previous: MonotonicInstant, + scope: OutboundScope, + source: Option, + time: Option, + timer: Option>, +} + +impl OutboundResponseContext { + #[must_use] + #[inline] + pub fn budget(&self) -> DispatchBudget { + self.budget + } + + #[must_use] + #[inline] + pub fn clock(&self) -> &MonotonicClock { + self.time.clock() + } + + #[must_use] + #[inline] + pub fn entered_at(&self) -> MonotonicInstant { + self.entered_at + } + + #[must_use] + #[inline] + pub fn mode(&self) -> OutboundSendMode { + self.mode + } + + #[must_use] + #[inline] + pub fn original_chain_deadline(&self) -> Option { + self.original_chain_deadline + } + + #[must_use] + #[inline] + pub fn request_method(&self) -> &Method { + &self.request_method + } + + #[must_use] + #[inline] + pub fn response_body_policy(&self) -> &ResponseBodyPolicy { + &self.body_policy + } + + #[must_use] + #[inline] + pub fn scope(&self) -> &OutboundScope { + &self.scope + } + + #[must_use] + #[inline] + pub fn time(&self) -> &OutboundTime { + &self.time + } +} + +impl OutboundHeaderInput { + #[must_use] + #[inline] + pub fn new( + adapter: &'static str, + headers: HeaderMap, + fidelity: OutboundHeaderFidelity, + ) -> Self { + Self { + adapter, + fidelity, + headers, + } + } + + /// Charges original fields, captures their evidence, normalizes, then charges the marker. + /// + /// Pass a limiter that has not yet charged this field section. Body disposition must be + /// retained: normalizing a 205 a second time would lose + /// whether its original declared length requires immediate body suppression. + /// Unknown provider field boundaries never certify Content-Type provenance. + /// + /// # Errors + /// Returns typed header-limit, protocol-framing or invalid adapter-identity errors. + /// + /// ``` + /// use edgezero_core::http::{HeaderMap, HeaderValue, Method, StatusCode}; + /// use edgezero_core::{OutboundHeaderFidelity, OutboundHeaderInput, + /// OutboundProxyMarker, ResponseHeaderLimiter}; + /// let mut headers = HeaderMap::new(); + /// headers.insert("content-type", HeaderValue::from_static("application/json")); + /// let (headers, facts, _) = OutboundHeaderInput::new( + /// "custom", headers, OutboundHeaderFidelity::AdapterVisibleFields, + /// ).normalize( + /// ResponseHeaderLimiter::new(Some(1024), Some(8)), + /// &Method::GET, StatusCode::OK, OutboundProxyMarker::Off, + /// )?; + /// assert_eq!(headers["content-type"], "application/json"); + /// assert_eq!(facts.content_type_provenance, + /// edgezero_core::ContentTypeProvenance::SingleTextUnchanged); + /// # Ok::<(), edgezero_core::EdgeError>(()) + /// ``` + /// + /// ```compile_fail + /// use edgezero_core::normalize_response_headers; + /// ``` + /// ```compile_fail + /// use edgezero_core::outbound::insert_proxy_header; + /// ``` + #[inline] + pub fn normalize( + self, + mut limiter: ResponseHeaderLimiter, + method: &Method, + status: StatusCode, + marker: OutboundProxyMarker, + ) -> Result< + ( + HeaderMap, + ResponseHeadNormalization, + OutboundLocationEvidence, + ), + EdgeError, + > { + let Self { + adapter, + mut headers, + fidelity, + } = self; + limiter.observe(&headers)?; + let mut location = original_location_evidence(&headers, fidelity); + let mut normalized = normalize_response_headers(method, status, &mut headers)?; + location.retained_singleton = location.observed_count == 1 + && !location.has_non_text + && headers.contains_key(LOCATION); + if fidelity == OutboundHeaderFidelity::ProviderFieldBoundariesUnknown { + normalized.content_type_provenance = ContentTypeProvenance::Unknown; + } + let adapter_value = HeaderValue::from_maybe_shared(Bytes::from_static(adapter.as_bytes())) + .map_err(|_error| { + EdgeError::internal(anyhow::anyhow!("invalid outbound adapter identity")) + })?; + insert_proxy_header(&mut headers, &mut limiter, marker, adapter_value)?; + Ok((headers, normalized, location)) + } +} + +impl OutboundLocationEvidence { + #[must_use] + #[inline] + pub fn fidelity(&self) -> OutboundHeaderFidelity { + self.fidelity + } + + #[must_use] + #[inline] + pub fn has_non_text(&self) -> bool { + self.has_non_text + } + + #[must_use] + #[inline] + pub fn is_missing_or_empty(&self) -> bool { + self.missing_or_empty + } + + #[must_use] + #[inline] + pub fn observed_count(&self) -> usize { + self.observed_count + } + + #[must_use] + #[inline] + pub fn retained_singleton(&self) -> bool { + self.retained_singleton + } +} + +impl OutboundHead { + /// Releases this exchange without reading or validating its payload. + #[inline] + pub fn discard(self) { + drop(self); + } + + /// Applies final-response policies once, within the captured absolute budget. + /// + /// # Errors + /// Returns original source/timer failures, typed policy limits or the selected timeout. + #[inline] + #[must_use] + pub fn finish(self) -> OutboundFuture { + let previous = self.context.entered_at; + self.finish_from(previous) + } + + pub(super) fn finish_from( + self, + observed_at: MonotonicInstant, + ) -> OutboundFuture { + let scope = self.context.scope.clone(); + let time = self.context.time.clone(); + let entered_at = self.context.entered_at; + super::scope::bind_future( + scope, + time, + entered_at, + Box::pin(async move { + let Self { + owner, + context, + encoded, + mut headers, + normalization, + status, + .. + } = self; + let clock = context.clock().clone(); + let (deadline, cause) = effective_deadline(&context); + let mut previous = observed_at; + context.scope.check()?; + checked_live(&clock, &mut previous, deadline, cause)?; + context.scope.check()?; + let mode = context.body_policy.response_mode; + let body = match normalization.body_disposition { + ResponseBodyDisposition::FramingBodyless + | ResponseBodyDisposition::ResetContent { + declared_body: true, + } => { + drop(owner); + drop(encoded); + Body::empty() + } + ResponseBodyDisposition::ResetContent { + declared_body: false, + } => { + let mut source = OwnedPayload::new(encoded, owner, &context, previous); + if let Some(item) = source.next().await { + item?; + } + previous = source.previous; + drop(source); + Body::empty() + } + ResponseBodyDisposition::Payload => { + let shaped = apply_response_body_policy( + cooperative_encoded_stream(encoded), + &mut headers, + context.body_policy, + )?; + let mut source = OwnedPayload::new(shaped, owner, &context, previous); + match mode { + ResponseMode::Buffered { max_bytes } => { + let bytes = + collect_response_stream_inner(&mut source, max_bytes, None) + .await?; + previous = source.previous; + drop(source); + Body::from(bytes) + } + ResponseMode::Streamed => { + let (body_scope, body_owner) = source.bind_body_scope(&context)?; + Body::from_parts( + BodyPayload::Stream(source.boxed_local()), + body_owner, + ) + .bind_outbound_scope(&body_scope)? + } + } + } + }; + context.scope.check()?; + checked_live(&clock, &mut previous, deadline, cause)?; + context.scope.check()?; + Ok(OutboundResponse::new( + context.request_method, + status, + headers, + body, + clock, + normalization.content_type_provenance, + )) + }), + ) + } + + #[must_use] + #[inline] + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + #[must_use] + #[inline] + pub fn location_evidence(&self) -> &OutboundLocationEvidence { + &self.location_evidence + } + + /// Captures original metadata and applies header caps before exposing a lazy body. + /// + /// # Errors + /// Returns typed header-limit, protocol-framing or invalid adapter-identity errors. + #[inline] + pub fn new( + status: StatusCode, + input: OutboundHeaderInput, + encoded: BodyStream, + mut context: OutboundResponseContext, + owner: OutboundExchangeOwner, + ) -> Result { + let (headers, normalization, location_evidence) = input.normalize( + ResponseHeaderLimiter::new( + context.max_response_header_bytes, + context.max_response_header_count, + ), + &context.request_method, + status, + context.proxy_marker, + )?; + context.body_policy.status = status; + Ok(Self { + context, + encoded, + headers, + location_evidence, + normalization, + owner, + status, + }) + } + + #[must_use] + #[inline] + pub fn status(&self) -> StatusCode { + self.status + } +} + +impl fmt::Debug for OutboundHead { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutboundHead") + .field("status", &self.status) + .finish_non_exhaustive() + } +} + +impl OwnedPayload { + fn bind_body_scope( + &mut self, + context: &OutboundResponseContext, + ) -> Result<(OutboundScope, Option), EdgeError> { + if context.scope.kind() == Some(OutboundScopeKind::ResponseBody) { + context.scope.check()?; + self.scope = context.scope.clone(); + self.body_progress = true; + return Ok((context.scope.clone(), None)); + } + let owner = OutboundScopeOwner::response_body(); + owner.accept_body_handoff()?; + let scope = owner.with(|| OutboundScope::capture(context.scope.progress())); + self.scope = scope.clone(); + self.body_progress = true; + Ok((scope, Some(owner))) + } + + fn new( + source: BodyStream, + owner: OutboundExchangeOwner, + context: &OutboundResponseContext, + previous: MonotonicInstant, + ) -> Self { + let (deadline, cause) = effective_deadline(context); + Self { + body_progress: false, + entered_at: context.entered_at, + owner: Some(owner), + source: Some(source), + timer: None, + time: Some(context.time.clone()), + cause, + deadline, + previous, + scope: context.scope.clone(), + } + } + + fn poll_active(&mut self, cx: &mut Context<'_>) -> Poll>> { + let Some(time) = &self.time else { + return Poll::Ready(None); + }; + self.scope.check()?; + checked_sample(time.clock(), &mut self.previous)?; + self.scope.check()?; + let timer = self + .timer + .get_or_insert_with(|| time.wait_until(self.deadline)); + self.scope.check()?; + if let Poll::Ready(result) = timer.as_mut().poll(cx) { + result?; + self.scope.check()?; + return Poll::Ready(Some(Err(timeout_error(self.cause)))); + } + self.scope.check()?; + let Some(source) = &mut self.source else { + return Poll::Ready(None); + }; + let next = source.as_mut().poll_next(cx); + self.scope.check()?; + checked_live(time.clock(), &mut self.previous, self.deadline, self.cause).map_err( + |mut selected| { + if let EdgeError::GatewayTimeout { + provider_detail, .. + } = &mut selected + && let Poll::Ready(Some(Err(error))) = &next + { + *provider_detail = error.provider_failure_detail().copied(); + } + selected + }, + )?; + self.scope.check()?; + next + } + + fn release(&mut self) { + if self.time.is_some() { + let scope = self.scope.clone(); + scope.with_cleanup(|| self.release_transport()); + } + } + + fn release_transport(&mut self) { + let resources = ( + self.owner.take(), + self.source.take(), + self.timer.take(), + self.time.take(), + ); + drop(resources); + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for OwnedPayload { + fn drop(&mut self) { + self.release(); + } +} + +impl Stream for OwnedPayload { + type Item = Result; + + #[inline] + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let result = if self.body_progress + && let Some(time) = self.time.clone() + { + let scope = self.scope.clone(); + let entered_at = self.entered_at; + let mut read = poll_fn(|context| self.poll_active(context)); + scope + .poll(&time, entered_at, cx, Pin::new(&mut read)) + .map(|outcome| match outcome { + Ok(item) => item, + Err(error) => Some(Err(error)), + }) + } else { + self.poll_active(cx) + }; + if matches!(result, Poll::Ready(None | Some(Err(_)))) { + self.release(); + } + result + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + if self.source.is_none() { + (0, Some(0)) + } else { + (0, None) + } + } +} + +pub(super) fn checked_live( + clock: &MonotonicClock, + previous: &mut MonotonicInstant, + deadline: Deadline, + cause: BudgetSource, +) -> Result<(), EdgeError> { + let now = checked_sample(clock, previous)?; + if deadline.is_expired_at(now) { + Err(timeout_error(cause)) + } else { + Ok(()) + } +} + +pub(super) fn checked_sample( + clock: &MonotonicClock, + previous: &mut MonotonicInstant, +) -> Result { + let now = clock.now(); + if now < *previous { + return Err(EdgeError::internal(anyhow::anyhow!( + "monotonic clock moved backwards during outbound response" + ))); + } + *previous = now; + Ok(now) +} + +fn cooperative_encoded_stream(mut source: BodyStream) -> BodyStream { + stream! { + while let Some(item) = source.next().await { + yield item; + // A decoder may consume ready chunks without producing output. Return control + // to the outer deadline race between reads, including empty/no-output input. + let mut yielded = false; + poll_fn(|cx| { + if yielded { Poll::Ready(()) } else { + yielded = true; + cx.waker().wake_by_ref(); + Poll::Pending + } + }).await; + } + } + .boxed_local() +} + +fn effective_deadline(context: &OutboundResponseContext) -> (Deadline, BudgetSource) { + if let Some(chain) = context.original_chain_deadline + && chain.instant() < context.budget.deadline.instant() + { + return (chain, BudgetSource::RequestDeadline); + } + (context.budget.deadline, context.budget.cause) +} + +fn original_location_evidence( + headers: &HeaderMap, + fidelity: OutboundHeaderFidelity, +) -> OutboundLocationEvidence { + let mut values = headers.get_all(LOCATION).iter(); + let first = values.next(); + let observed_count = headers.get_all(LOCATION).iter().count(); + OutboundLocationEvidence { + observed_count, + has_non_text: headers + .get_all(LOCATION) + .iter() + .any(|value| str::from_utf8(value.as_bytes()).is_err()), + retained_singleton: false, + missing_or_empty: observed_count == 0 + || (observed_count == 1 && first.is_some_and(|value| value.as_bytes().is_empty())), + fidelity, + } +} + +fn timeout_error(cause: BudgetSource) -> EdgeError { + EdgeError::gateway_timeout_caused("outbound response deadline expired", cause) +} + +#[cfg(test)] +mod tests { + use std::cell::{Cell, RefCell}; + use std::io::Write as _; + use std::rc::Rc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll, Waker}; + use std::time::Duration; + + use bytes::Bytes; + use flate2::Compression; + use flate2::write::GzEncoder; + use futures::executor::block_on; + use futures::future::pending; + use futures_util::future::poll_fn; + use futures_util::task::{ArcWake, waker_ref}; + use futures_util::{StreamExt as _, stream}; + + use crate::http::{HeaderMap, HeaderValue, Method, StatusCode}; + use crate::{ + BodyStream, BudgetSource, ContentTypeProvenance, DispatchBudget, EdgeError, MonotonicClock, + OutboundExchangeOwner, OutboundHead, OutboundHeaderFidelity, OutboundHeaderInput, + OutboundProxyMarker, OutboundResponseContext, OutboundScopeOwner, OutboundSendMode, + OutboundTime, ResponseBodyDisposition, ResponseBodyPolicy, ResponseDecoding, + ResponseHeaderLimiter, ResponseLimitReason, ResponseMode, + }; + + thread_local! { + static REENTRANT_OWNER: RefCell>> = const { RefCell::new(None) }; + } + + #[derive(Clone, Copy, Eq, PartialEq)] + enum CancelAt { + FinalizerClock, + ReadClock, + TimerCreationClock, + TimerFactory, + TimerPoll, + } + + #[derive(Clone)] + struct FakeTime(Arc>); + + struct FakeState { + now: crate::MonotonicInstant, + registrations: usize, + releases: usize, + waker: Option, + } + + struct TimerLease(Arc>); + + struct SourceLease(Rc>); + + #[derive(Default)] + struct WakeCount(AtomicUsize); + + #[derive(Debug, thiserror::Error)] + #[error("fixture identity")] + struct IdentityError(Arc<()>); + + impl FakeTime { + fn advance(&self, duration: Duration) { + let wake = { + let mut state = self.0.lock().expect("time"); + state.now = state.now.checked_add(duration).expect("advance"); + state.waker.take() + }; + if let Some(waker) = wake { + waker.wake(); + } + } + + fn clock(&self) -> MonotonicClock { + let state = Arc::clone(&self.0); + MonotonicClock::new(move || state.lock().expect("clock").now) + } + + fn new() -> Self { + Self(Arc::new(Mutex::new(FakeState { + now: crate::MonotonicInstant::now() + .checked_add(Duration::from_hours(1)) + .expect("future fake domain"), + registrations: 0, + releases: 0, + waker: None, + }))) + } + + fn pair(&self) -> OutboundTime { + let shared = Arc::clone(&self.0); + OutboundTime::new(self.clock(), move |deadline, clock| { + let timer_state = Arc::clone(&shared); + let paired_now = clock.now(); + let lease = { + let mut state = shared.lock().expect("registration"); + assert_eq!(paired_now, state.now); + state.registrations = state.registrations.saturating_add(1); + TimerLease(Arc::clone(&shared)) + }; + Box::pin(async move { + let _lease = lease; + poll_fn(move |cx| { + let mut state = timer_state.lock().expect("timer"); + if deadline.is_expired_at(state.now) { + Poll::Ready(Ok(())) + } else { + state.waker = Some(cx.waker().clone()); + Poll::Pending + } + }) + .await + }) + }) + } + + fn response_context(&self) -> OutboundResponseContext { + let mut captured = context(ResponseMode::Buffered { max_bytes: 16 }); + captured.time = self.pair(); + captured.entered_at = self.clock().now(); + captured.budget = DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: self.clock().deadline_after(Duration::from_millis(20)), + duration: Duration::from_millis(20), + }; + captured + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "Drop::pin_drop is unstable and conflicts with drop" + )] + impl Drop for TimerLease { + fn drop(&mut self) { + let mut state = self.0.lock().expect("unregister"); + state.releases = state.releases.saturating_add(1); + state.waker = None; + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "Drop::pin_drop is unstable and conflicts with drop" + )] + impl Drop for SourceLease { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + impl ArcWake for WakeCount { + fn wake(self: Arc) { + Self::wake_by_ref(&self); + } + + fn wake_by_ref(arc_self: &Arc) { + arc_self.0.fetch_add(1, Ordering::Relaxed); + } + } + + fn context(mode: ResponseMode) -> OutboundResponseContext { + let clock = MonotonicClock::default(); + OutboundResponseContext { + entered_at: clock.now(), + budget: DispatchBudget { + cause: BudgetSource::PerCallTimeout, + deadline: clock.deadline_after(Duration::from_secs(60)), + duration: Duration::from_secs(60), + }, + time: OutboundTime::new(clock, |_, _| Box::pin(pending())), + mode: OutboundSendMode::Send, + request_method: Method::GET, + body_policy: ResponseBodyPolicy { + max_brotli_window_bits: 24, + max_chunk_bytes: None, + max_decoded_response_bytes: Some(16), + max_decoder_bytes: crate::DEFAULT_MAX_DECODER_BYTES, + max_encoded_response_bytes: Some(32), + response_decoding: ResponseDecoding::Decode, + response_mode: mode, + status: StatusCode::OK, + }, + max_response_header_bytes: None, + max_response_header_count: None, + proxy_marker: OutboundProxyMarker::On, + original_chain_deadline: None, + scope: crate::OutboundScope::capture(Rc::new(crate::IndependentOutboundProgress)), + } + } + + fn owner(releases: &Rc>) -> OutboundExchangeOwner { + let counted = Rc::clone(releases); + OutboundExchangeOwner::new(move || counted.set(counted.get().saturating_add(1))) + } + + fn input(headers: HeaderMap) -> OutboundHeaderInput { + OutboundHeaderInput::new( + "test", + headers, + OutboundHeaderFidelity::AdapterVisibleFields, + ) + } + + fn assert_finalizer_scope_guard(cancel_at: CancelAt) { + for status in [StatusCode::OK, StatusCode::RESET_CONTENT] { + let callback = Rc::new(OutboundScopeOwner::callback()); + REENTRANT_OWNER.with(|slot| *slot.borrow_mut() = Some(Rc::clone(&callback))); + let mut captured = callback.with(|| context(ResponseMode::Buffered { max_bytes: 16 })); + let start = captured.entered_at; + let sample_to_cancel = match cancel_at { + CancelAt::FinalizerClock => 1_usize, + CancelAt::ReadClock => 2, + CancelAt::TimerCreationClock => 3, + CancelAt::TimerFactory | CancelAt::TimerPoll => 0, + }; + let samples = Arc::new(AtomicUsize::new(0)); + let clock = MonotonicClock::new(move || { + let sample = samples.fetch_add(1, Ordering::SeqCst).saturating_add(1); + if sample == sample_to_cancel { + cancel_test_scope(); + } + start + }); + captured.time = OutboundTime::new(clock, move |_, _| { + if cancel_at == CancelAt::TimerFactory { + cancel_test_scope(); + } + Box::pin(poll_fn(move |_| { + if cancel_at == CancelAt::TimerPoll { + cancel_test_scope(); + } + Poll::Pending + })) + }); + let polls = Rc::new(Cell::new(0_usize)); + let observed_polls = Rc::clone(&polls); + let source_drops = Rc::new(Cell::new(0)); + let source_lease = SourceLease(Rc::clone(&source_drops)); + let source = stream::poll_fn(move |_| { + let _lease = &source_lease; + observed_polls.set(observed_polls.get().saturating_add(1)); + Poll::Pending + }) + .boxed_local(); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + status, + input(HeaderMap::new()), + source, + captured, + owner(&releases), + ) + .expect("head"); + let result = block_on(callback.with(|| head.finish())); + REENTRANT_OWNER.with(|slot| drop(slot.borrow_mut().take())); + assert!(matches!(result, Err(EdgeError::Internal { .. }))); + assert_eq!(polls.get(), 0, "closed finalization cannot poll the source"); + assert_eq!(source_drops.get(), 1); + assert_eq!(releases.get(), 1); + } + } + + fn cancel_test_scope() { + let callback = REENTRANT_OWNER + .with(|slot| slot.borrow().clone()) + .expect("test owner"); + callback.close(); + } + + #[test] + fn finalizer_scope_closed_by_initial_clock_never_polls_source() { + assert_finalizer_scope_guard(CancelAt::FinalizerClock); + } + + #[test] + fn finalizer_scope_closed_by_read_clock_never_polls_source() { + assert_finalizer_scope_guard(CancelAt::ReadClock); + } + + #[test] + fn finalizer_scope_closed_by_timer_creation_clock_never_polls_source() { + assert_finalizer_scope_guard(CancelAt::TimerCreationClock); + } + + #[test] + fn finalizer_scope_closed_by_timer_factory_never_polls_source() { + assert_finalizer_scope_guard(CancelAt::TimerFactory); + } + + #[test] + fn finalizer_scope_closed_by_timer_poll_never_polls_source() { + assert_finalizer_scope_guard(CancelAt::TimerPoll); + } + + #[test] + fn finalizer_cleanup_cannot_reset_observed_read_clock_history() { + for status in [StatusCode::OK, StatusCode::RESET_CONTENT] { + let fake = FakeTime::new(); + let mut captured = fake.response_context(); + captured.body_policy.response_mode = ResponseMode::Buffered { max_bytes: 16 }; + let restore = captured + .entered_at + .checked_add(Duration::from_millis(5)) + .expect("restored clock"); + let advancing = fake.clone(); + let source = stream::poll_fn(move |_| { + advancing.advance(Duration::from_millis(10)); + Poll::Ready(None) + }) + .boxed_local(); + let releases = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&releases); + let clock_state = Arc::clone(&fake.0); + let transport_owner = OutboundExchangeOwner::new(move || { + counted.set(counted.get().saturating_add(1)); + clock_state.lock().expect("cleanup clock").now = restore; + }); + let head = OutboundHead::new( + status, + input(HeaderMap::new()), + source, + captured, + transport_owner, + ) + .expect("head"); + let result = block_on(head.finish()); + assert!( + matches!(result, Err(EdgeError::Internal { .. })), + "final checks must retain read-time history across transport cleanup" + ); + assert_eq!(releases.get(), 1); + } + } + + #[test] + fn shared_header_policy_preserves_original_evidence_before_filtering() { + for fidelity in [ + OutboundHeaderFidelity::AdapterVisibleFields, + OutboundHeaderFidelity::ProviderFieldBoundariesUnknown, + ] { + let mut headers = HeaderMap::new(); + headers.append("content-type", HeaderValue::from_static("application/json")); + headers.append( + "content-type", + HeaderValue::from_bytes(&[0xff]).expect("opaque"), + ); + headers.append("location", HeaderValue::from_static("/next")); + headers.append( + "location", + HeaderValue::from_bytes(&[0xff]).expect("opaque"), + ); + let (retained, normalized, location) = + OutboundHeaderInput::new("test", headers, fidelity) + .normalize( + ResponseHeaderLimiter::new(None, Some(4)), + &Method::GET, + StatusCode::FOUND, + OutboundProxyMarker::Off, + ) + .expect("all four original fields fit"); + assert_eq!(retained.get_all("content-type").iter().count(), 1); + assert_eq!(retained.get_all("location").iter().count(), 1); + assert_eq!(location.observed_count(), 2); + assert!(location.has_non_text()); + assert!(!location.retained_singleton()); + assert!(!location.is_missing_or_empty()); + assert_eq!(location.fidelity(), fidelity); + assert_eq!( + normalized.content_type_provenance, + if fidelity == OutboundHeaderFidelity::AdapterVisibleFields { + ContentTypeProvenance::Ineligible( + crate::ContentTypeIneligibility::MultipleValues, + ) + } else { + ContentTypeProvenance::Unknown + } + ); + } + } + + #[test] + fn shared_header_policy_charges_filtered_fields_and_marker_once() { + for marker in [ + OutboundProxyMarker::Off, + OutboundProxyMarker::On, + OutboundProxyMarker::Inherit, + ] { + let mut headers = HeaderMap::new(); + headers.append( + "x-filtered", + HeaderValue::from_bytes(&[0xff]).expect("opaque"), + ); + let marker_enabled = marker != OutboundProxyMarker::Off; + let count = if marker_enabled { 2 } else { 1 }; + let bytes = if marker_enabled { 31 } else { 11 }; + for (max_bytes, max_count, expected) in [ + (bytes, count, None), + (bytes - 1, count, Some(ResponseLimitReason::HeaderBytes)), + (bytes, count - 1, Some(ResponseLimitReason::HeaderCount)), + ] { + let result = input(headers.clone()).normalize( + ResponseHeaderLimiter::new(Some(max_bytes), Some(max_count)), + &Method::GET, + StatusCode::OK, + marker, + ); + if let Some(reason) = expected { + assert!( + matches!(result, Err(EdgeError::ResponseTooLarge { reason: actual, .. }) if actual == reason) + ); + } else { + let (normalized, _, _) = result.expect("exact byte/count cap"); + assert!(!normalized.contains_key("x-filtered")); + assert_eq!(normalized.contains_key("x-edgezero-proxy"), marker_enabled); + } + } + } + } + + #[test] + fn shared_header_policy_retains_reset_and_marker_off_semantics() { + let mut headers = HeaderMap::new(); + headers.append("content-length", HeaderValue::from_static("9")); + headers.append("x-edgezero-proxy", HeaderValue::from_static("upstream")); + let (retained, normalized, _) = input(headers) + .normalize( + ResponseHeaderLimiter::new(None, Some(2)), + &Method::GET, + StatusCode::RESET_CONTENT, + OutboundProxyMarker::Off, + ) + .expect("marker-off does not charge or replace the upstream field"); + assert_eq!(retained["x-edgezero-proxy"], "upstream"); + assert_eq!(retained["content-length"], "0"); + assert_eq!( + normalized.body_disposition, + ResponseBodyDisposition::ResetContent { + declared_body: true + } + ); + } + + #[test] + fn head_disposition_precedes_payload_processing() { + for case in 0_u8..4 { + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let body: BodyStream = stream::poll_fn(move |_| { + counted.set(counted.get().saturating_add(1)); + match case { + 0 => Poll::Pending, + 1 => Poll::Ready(Some(Err(EdgeError::bad_gateway("source")))), + 2 => Poll::Ready(None), + _ => Poll::Ready(Some(Ok(Bytes::from_static(b"oversized")))), + } + }) + .boxed_local(); + let mut headers = HeaderMap::new(); + headers.insert("location", HeaderValue::from_static("/next")); + headers.insert("content-length", HeaderValue::from_static("10485760")); + headers.insert("content-encoding", HeaderValue::from_static("br")); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::FOUND, + input(headers), + body, + context(ResponseMode::Buffered { max_bytes: 16 }), + owner(&releases), + ) + .expect("header construction never applies payload caps or decoding"); + assert_eq!(head.status(), StatusCode::FOUND); + assert_eq!(head.headers()["content-length"], "10485760"); + assert_eq!(polls.get(), 0); + head.discard(); + assert_eq!( + polls.get(), + 0, + "discard must never poll an intermediate body" + ); + assert_eq!(releases.get(), 1); + } + } + + #[test] + fn final_head_applies_declared_payload_limits_without_polling() { + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let body = stream::poll_fn(move |_| { + counted.set(counted.get().saturating_add(1)); + Poll::Pending + }) + .boxed_local(); + let mut headers = HeaderMap::new(); + headers.insert("content-length", HeaderValue::from_static("33")); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::FOUND, + input(headers), + body, + context(ResponseMode::Buffered { max_bytes: 16 }), + owner(&releases), + ) + .expect("head"); + let error = block_on(head.finish()).expect_err("final payload cap"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::EncodedBody, + .. + } + )); + assert_eq!(polls.get(), 0); + assert_eq!(releases.get(), 1); + } + + #[test] + fn location_evidence_survives_header_filtering() { + let mut headers = HeaderMap::new(); + headers.append("location", HeaderValue::from_static("/next")); + headers.append( + "location", + HeaderValue::from_bytes(&[0xff]).expect("opaque"), + ); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::FOUND, + input(headers), + stream::pending().boxed_local(), + context(ResponseMode::Streamed), + owner(&releases), + ) + .expect("invalid location is not rejected ahead of redirect stop rules"); + assert_eq!(head.headers().get_all("location").iter().count(), 1); + let evidence = head.location_evidence(); + assert_eq!(evidence.observed_count(), 2); + assert!(evidence.has_non_text()); + assert!(!evidence.retained_singleton()); + assert!(!evidence.is_missing_or_empty()); + assert_eq!( + evidence.fidelity(), + OutboundHeaderFidelity::AdapterVisibleFields + ); + head.discard(); + assert_eq!(releases.get(), 1); + } + + #[test] + fn head_finalization_releases_buffered_owner_and_transfers_streamed_owner() { + for mode in [ + ResponseMode::Buffered { max_bytes: 16 }, + ResponseMode::Streamed, + ] { + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::OK, + input(HeaderMap::new()), + stream::iter([Ok(Bytes::from_static(b"body"))]).boxed_local(), + context(mode), + owner(&releases), + ) + .expect("head"); + let response = block_on(head.finish()).expect("final response"); + assert_eq!( + response.content_type_provenance(), + ContentTypeProvenance::Ineligible(crate::ContentTypeIneligibility::Missing) + ); + assert_eq!( + releases.get(), + usize::from(matches!(mode, ResponseMode::Buffered { .. })) + ); + assert_eq!( + block_on(response.into_bytes_bounded(16)).expect("body"), + b"body".as_slice() + ); + assert_eq!(releases.get(), 1); + } + } + + #[test] + fn empty_response_chunks_yield_without_starving_the_deadline() { + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let body = stream::poll_fn(move |_| { + counted.set(counted.get().saturating_add(1)); + assert!( + counted.get() < 8, + "ready empty chunks must not monopolize one poll" + ); + Poll::Ready(Some(Ok(Bytes::new()))) + }) + .boxed_local(); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::OK, + input(HeaderMap::new()), + body, + context(ResponseMode::Buffered { max_bytes: 16 }), + owner(&releases), + ) + .expect("head"); + let mut finishing = head.finish(); + assert!( + finishing + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert_eq!(polls.get(), 1); + drop(finishing); + assert_eq!(releases.get(), 1); + } + + #[test] + fn pending_final_body_wakes_in_paired_time_and_releases_every_resource() { + let time = FakeTime::new(); + let releases = Rc::new(Cell::new(0)); + let source_drops = Rc::new(Cell::new(0)); + let lease = SourceLease(Rc::clone(&source_drops)); + let body = stream::poll_fn(move |_| { + let _retained = &lease; + Poll::Pending + }) + .boxed_local(); + let head = OutboundHead::new( + StatusCode::OK, + input(HeaderMap::new()), + body, + time.response_context(), + owner(&releases), + ) + .expect("head"); + let wake = Arc::new(WakeCount::default()); + let waker = waker_ref(&wake); + let mut cx = Context::from_waker(&waker); + let mut finishing = head.finish(); + assert!(finishing.as_mut().poll(&mut cx).is_pending()); + assert_eq!(time.0.lock().expect("time").registrations, 1); + time.advance(Duration::from_millis(20)); + assert_eq!(wake.0.load(Ordering::Relaxed), 1); + let Poll::Ready(Err(error)) = finishing.as_mut().poll(&mut cx) else { + panic!("woken deadline"); + }; + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); + assert_eq!(releases.get(), 1); + assert_eq!(source_drops.get(), 1); + assert_eq!(time.0.lock().expect("time").releases, 1); + drop(finishing); + assert_eq!(releases.get(), 1); + } + + #[test] + fn finalizer_timer_setup_error_precedes_body_poll_and_preserves_identity() { + let identity = Arc::new(()); + let timer_identity = Arc::clone(&identity); + let mut captured = context(ResponseMode::Buffered { max_bytes: 16 }); + captured.time = OutboundTime::new(captured.clock().clone(), move |_, _| { + let error = EdgeError::internal(IdentityError(Arc::clone(&timer_identity))); + Box::pin(async move { Err(error) }) + }); + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let body = stream::poll_fn(move |_| { + counted.set(counted.get().saturating_add(1)); + Poll::Pending + }) + .boxed_local(); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::OK, + input(HeaderMap::new()), + body, + captured, + owner(&releases), + ) + .expect("head"); + let error = block_on(head.finish()).expect_err("timer setup"); + let EdgeError::Internal { source, .. } = error else { + panic!("original internal"); + }; + assert!(Arc::ptr_eq( + &source.downcast_ref::().expect("identity").0, + &identity + )); + assert_eq!(polls.get(), 0); + assert_eq!(releases.get(), 1); + } + + #[test] + fn head_drop_and_unpolled_finalization_release_without_body_polling() { + for finish in [false, true] { + let releases = Rc::new(Cell::new(0)); + let source_drops = Rc::new(Cell::new(0)); + let lease = SourceLease(Rc::clone(&source_drops)); + let body = stream::poll_fn(move |_| { + let _retained = &lease; + panic!("no body polling"); + }) + .boxed_local(); + let head = OutboundHead::new( + StatusCode::OK, + input(HeaderMap::new()), + body, + context(ResponseMode::Streamed), + owner(&releases), + ) + .expect("head"); + if finish { + drop(head.finish()); + } else { + drop(head); + } + assert_eq!(releases.get(), 1); + assert_eq!(source_drops.get(), 1); + } + } + + #[test] + fn finalizer_cancels_pending_read_and_timer_on_drop() { + let time = FakeTime::new(); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::OK, + input(HeaderMap::new()), + stream::pending().boxed_local(), + time.response_context(), + owner(&releases), + ) + .expect("head"); + let mut finishing = head.finish(); + assert!( + finishing + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + drop(finishing); + assert_eq!(releases.get(), 1); + let state = time.0.lock().expect("time"); + assert_eq!(state.registrations, 1); + assert_eq!(state.releases, 1); + assert!(state.waker.is_none()); + } + + #[test] + fn ready_body_at_deadline_never_becomes_late_success() { + let time = FakeTime::new(); + let advanced = time.clone(); + let body = stream::poll_fn(move |_| { + advanced.advance(Duration::from_millis(20)); + Poll::Ready(Some(Ok(Bytes::from_static(b"late")))) + }) + .boxed_local(); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::OK, + input(HeaderMap::new()), + body, + time.response_context(), + owner(&releases), + ) + .expect("head"); + assert!(matches!( + block_on(head.finish()), + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + )); + assert_eq!(releases.get(), 1); + } + + #[test] + fn original_chain_deadline_remains_binding_through_final_body() { + let time = FakeTime::new(); + let mut captured = time.response_context(); + captured.original_chain_deadline = + Some(time.clock().deadline_after(Duration::from_millis(5))); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::OK, + input(HeaderMap::new()), + stream::pending().boxed_local(), + captured, + owner(&releases), + ) + .expect("head"); + let mut finishing = head.finish(); + let mut cx = Context::from_waker(Waker::noop()); + assert!(finishing.as_mut().poll(&mut cx).is_pending()); + time.advance(Duration::from_millis(5)); + let Poll::Ready(Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + })) = finishing.as_mut().poll(&mut cx) + else { + panic!("original chain deadline"); + }; + assert_eq!(releases.get(), 1); + } + + #[test] + fn bodyless_and_declared_reset_bodies_bypass_payload_processing() { + for (method, status) in [ + (Method::HEAD, StatusCode::OK), + (Method::GET, StatusCode::NO_CONTENT), + (Method::GET, StatusCode::NOT_MODIFIED), + (Method::GET, StatusCode::RESET_CONTENT), + ] { + let mut captured = context(ResponseMode::Buffered { max_bytes: 0 }); + captured.request_method = method; + let mut headers = HeaderMap::new(); + headers.insert("content-length", HeaderValue::from_static("99")); + headers.insert("content-encoding", HeaderValue::from_static("br")); + let body = stream::poll_fn(|_| panic!("suppression never reads payload")).boxed_local(); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new(status, input(headers), body, captured, owner(&releases)) + .expect("head"); + if status == StatusCode::RESET_CONTENT { + assert_eq!(head.headers()["content-length"], "0"); + } + let response = block_on(head.finish()).expect("suppressed"); + assert!( + block_on(response.into_bytes_bounded(0)) + .expect("empty") + .is_empty() + ); + assert_eq!(releases.get(), 1); + } + } + + #[test] + fn location_evidence_distinguishes_absence_empty_removed_and_unknown() { + for (values, nominated, count, empty, retained) in [ + (vec![], false, 0, true, false), + (vec![""], false, 1, true, true), + (vec!["/next"], false, 1, false, true), + (vec!["/next"], true, 1, false, false), + (vec!["", ""], false, 2, false, false), + (vec!["/a, /b"], false, 1, false, true), + ] { + for fidelity in [ + OutboundHeaderFidelity::AdapterVisibleFields, + OutboundHeaderFidelity::ProviderFieldBoundariesUnknown, + ] { + let mut headers = HeaderMap::new(); + for value in &values { + headers.append("location", HeaderValue::from_str(value).expect("location")); + } + headers.append("content-type", HeaderValue::from_static("application/json")); + if nominated { + headers.append("connection", HeaderValue::from_static("location")); + } + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::FOUND, + OutboundHeaderInput::new("test", headers, fidelity), + stream::empty().boxed_local(), + context(ResponseMode::Streamed), + owner(&releases), + ) + .expect("head"); + let evidence = head.location_evidence(); + assert_eq!(evidence.observed_count(), count); + assert_eq!(evidence.is_missing_or_empty(), empty); + assert_eq!(evidence.retained_singleton(), retained); + assert_eq!(evidence.fidelity(), fidelity); + let response = block_on(head.finish()).expect("final"); + assert_eq!( + response.content_type_provenance(), + if fidelity == OutboundHeaderFidelity::AdapterVisibleFields { + ContentTypeProvenance::SingleTextUnchanged + } else { + ContentTypeProvenance::Unknown + } + ); + drop(response); + assert_eq!(releases.get(), 1); + } + } + } + + #[test] + fn head_caps_charge_original_fields_and_synthetic_marker_before_payload() { + for (cap, marker, values, expected) in [ + (1, OutboundProxyMarker::On, 1_u8, true), + (1, OutboundProxyMarker::Off, 1, false), + (1, OutboundProxyMarker::Off, 2, true), + ] { + let mut captured = context(ResponseMode::Streamed); + captured.max_response_header_count = Some(cap); + captured.proxy_marker = marker; + let mut headers = HeaderMap::new(); + for _ in 0..values { + headers.append( + "x-filtered", + HeaderValue::from_bytes(&[0xff]).expect("opaque"), + ); + } + let releases = Rc::new(Cell::new(0)); + let result = OutboundHead::new( + StatusCode::OK, + input(headers), + stream::poll_fn(|_| panic!("no body reads")).boxed_local(), + captured, + owner(&releases), + ); + if expected { + assert!(matches!( + result, + Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::HeaderCount, + .. + }) + )); + } else { + result.expect("within cap").discard(); + } + assert_eq!(releases.get(), 1); + } + } + + #[test] + fn streamed_source_failure_releases_on_terminal_poll_without_rebuilding_error() { + let identity = Arc::new(()); + let source_identity = Arc::clone(&identity); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::OK, + input(HeaderMap::new()), + stream::iter([Err(EdgeError::internal(IdentityError(source_identity)))]).boxed_local(), + context(ResponseMode::Streamed), + owner(&releases), + ) + .expect("head"); + let response = block_on(head.finish()).expect("stream is lazy"); + assert_eq!(releases.get(), 0); + let mut body = response.into_body().into_stream().expect("streamed body"); + let error = block_on(body.next()) + .expect("terminal item") + .expect_err("source"); + let EdgeError::Internal { source, .. } = error else { + panic!("original error"); + }; + assert!(Arc::ptr_eq( + &source.downcast_ref::().expect("identity").0, + &identity + )); + assert_eq!( + releases.get(), + 1, + "retaining the terminal stream retains no SDK owner" + ); + assert!(block_on(body.next()).is_none()); + drop(body); + assert_eq!(releases.get(), 1); + } + + #[test] + fn final_head_preserves_independent_observed_payload_caps() { + for (reason, encoded_cap, decoded_cap, final_cap) in [ + (ResponseLimitReason::EncodedBody, 8, 16, 16), + (ResponseLimitReason::DecodedBody, 32, 8, 16), + (ResponseLimitReason::BufferedBody, 32, 16, 8), + ] { + let mut captured = context(ResponseMode::Buffered { + max_bytes: final_cap, + }); + captured.body_policy.max_encoded_response_bytes = Some(encoded_cap); + captured.body_policy.max_decoded_response_bytes = Some(decoded_cap); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::OK, + input(HeaderMap::new()), + stream::iter([Ok(Bytes::from_static(b"123456789"))]).boxed_local(), + captured, + owner(&releases), + ) + .expect("head"); + assert!( + matches!(block_on(head.finish()), Err(EdgeError::ResponseTooLarge { reason: actual, .. }) if actual == reason) + ); + assert_eq!(releases.get(), 1); + } + } + + #[test] + fn head_preserves_normalization_and_unknown_location_facts() { + for (value, non_text) in [ + (HeaderValue::from_bytes(&[0xff]).expect("opaque"), true), + ( + HeaderValue::from_bytes("caf\u{e9}".as_bytes()).expect("UTF-8"), + false, + ), + ] { + let mut headers = HeaderMap::new(); + headers.append("location", value); + headers.append("content-type", HeaderValue::from_static("application/json")); + headers.append("content-length", HeaderValue::from_static("malformed")); + headers.append("content-encoding", HeaderValue::from_static("gzip")); + headers.append( + "connection", + HeaderValue::from_static("content-type, content-length, content-encoding"), + ); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::FOUND, + input(headers), + stream::empty().boxed_local(), + context(ResponseMode::Streamed), + owner(&releases), + ) + .expect("nominations precede framing"); + assert_eq!(head.location_evidence().observed_count(), 1); + assert_eq!(head.location_evidence().has_non_text(), non_text); + assert!(!head.location_evidence().is_missing_or_empty()); + assert_eq!(head.location_evidence().retained_singleton(), !non_text); + assert!(!head.headers().contains_key("content-length")); + assert!(!head.headers().contains_key("content-encoding")); + let response = block_on(head.finish()).expect("final"); + assert_eq!( + response.content_type_provenance(), + ContentTypeProvenance::Ineligible(crate::ContentTypeIneligibility::Removed) + ); + drop(response); + assert_eq!(releases.get(), 1); + } + } + + #[test] + fn response_context_readback_preserves_captured_policies_and_clock() { + let time = FakeTime::new(); + let captured = time.response_context(); + assert_eq!(captured.clock().now(), time.clock().now()); + assert_eq!(captured.time().clock().now(), time.clock().now()); + assert_eq!(captured.entered_at(), time.clock().now()); + assert_eq!(captured.budget().cause, BudgetSource::PerCallTimeout); + assert_eq!(captured.budget().duration, Duration::from_millis(20)); + assert_eq!(captured.mode(), OutboundSendMode::Send); + assert_eq!(captured.request_method(), Method::GET); + assert!(captured.original_chain_deadline().is_none()); + assert_eq!( + captured.response_body_policy().max_decoded_response_bytes, + Some(16) + ); + } + + #[test] + fn compressed_empty_reads_yield_before_the_decoder_can_starve_time() { + for coding in ["br", "gzip", "deflate"] { + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let body = stream::poll_fn(move |_| { + counted.set(counted.get().saturating_add(1)); + assert!( + counted.get() < 8, + "decoder must yield across ready empty input" + ); + Poll::Ready(Some(Ok(Bytes::new()))) + }) + .boxed_local(); + let mut headers = HeaderMap::new(); + headers.insert( + "content-encoding", + HeaderValue::from_str(coding).expect("coding"), + ); + let time = FakeTime::new(); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::OK, + input(headers), + body, + time.response_context(), + owner(&releases), + ) + .expect("head"); + let mut finishing = head.finish(); + let mut cx = Context::from_waker(Waker::noop()); + assert!(finishing.as_mut().poll(&mut cx).is_pending()); + assert_eq!(polls.get(), 1); + time.advance(Duration::from_millis(20)); + assert!(matches!( + finishing.as_mut().poll(&mut cx), + Poll::Ready(Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + })) + )); + assert_eq!(releases.get(), 1); + } + } + + #[test] + fn equal_chain_deadline_keeps_the_selected_budget_cause() { + let time = FakeTime::new(); + let mut captured = time.response_context(); + captured.original_chain_deadline = Some(captured.budget.deadline); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + StatusCode::OK, + input(HeaderMap::new()), + stream::empty().boxed_local(), + captured, + owner(&releases), + ) + .expect("head"); + time.advance(Duration::from_millis(20)); + assert!(matches!( + block_on(head.finish()), + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + }) + )); + assert_eq!(releases.get(), 1); + } + + #[test] + fn head_finalization_retains_coding_choices_and_partial_response_rules() { + let mut compressor = GzEncoder::new(Vec::new(), Compression::fast()); + compressor.write_all(b"body").expect("encode"); + let encoded = Bytes::from(compressor.finish().expect("gzip")); + for decoding in [ResponseDecoding::Decode, ResponseDecoding::Original] { + for status in [StatusCode::OK, StatusCode::PARTIAL_CONTENT] { + for mode in [ + ResponseMode::Buffered { max_bytes: 32 }, + ResponseMode::Streamed, + ] { + let mut captured = context(mode); + captured.body_policy.response_decoding = decoding; + let mut headers = HeaderMap::new(); + headers.insert("content-encoding", HeaderValue::from_static("gzip")); + headers.insert( + "content-length", + HeaderValue::from_str(&encoded.len().to_string()).expect("length"), + ); + let releases = Rc::new(Cell::new(0)); + let head = OutboundHead::new( + status, + input(headers), + stream::iter([Ok(encoded.clone())]).boxed_local(), + captured, + owner(&releases), + ) + .expect("head"); + let response = block_on(head.finish()).expect("finalized"); + let original = decoding == ResponseDecoding::Original + || status == StatusCode::PARTIAL_CONTENT; + assert_eq!( + response.headers().contains_key("content-encoding"), + original + ); + assert_eq!(response.headers().contains_key("content-length"), original); + let body = block_on(response.into_bytes_bounded(32)).expect("body"); + assert_eq!( + body.as_ref(), + if original { encoded.as_ref() } else { b"body" } + ); + assert_eq!(releases.get(), 1); + } + } + } + } + + #[test] + fn head_header_errors_release_before_any_payload_poll() { + for malformed_connection in [false, true] { + let mut captured = context(ResponseMode::Streamed); + let mut headers = HeaderMap::new(); + if malformed_connection { + headers.insert("connection", HeaderValue::from_static("bad name")); + } else { + headers.insert("x", HeaderValue::from_static("y")); + captured.max_response_header_bytes = Some(1); + } + let releases = Rc::new(Cell::new(0)); + let result = OutboundHead::new( + StatusCode::FOUND, + input(headers), + stream::poll_fn(|_| panic!("header failures never read payloads")).boxed_local(), + captured, + owner(&releases), + ); + if malformed_connection { + assert!(matches!( + result, + Err(EdgeError::BadGateway { + reason: crate::BadGatewayReason::Protocol, + .. + }) + )); + } else { + assert!(matches!( + result, + Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::HeaderBytes, + .. + }) + )); + } + assert_eq!(releases.get(), 1); + } + } +} diff --git a/crates/edgezero-core/src/outbound/image_optimizer.rs b/crates/edgezero-core/src/outbound/image_optimizer.rs new file mode 100644 index 000000000..c4ae94e3d --- /dev/null +++ b/crates/edgezero-core/src/outbound/image_optimizer.rs @@ -0,0 +1,497 @@ +use super::{ + OutboundCachePolicy, OutboundCropFocus, OutboundImageFormat, OutboundImageOptimizerOptions, + OutboundImageOptimizerRegion, OutboundRequest, OutboundResizeFilter, +}; +use crate::error::EdgeError; +use crate::http::Method; + +impl OutboundImageOptimizerOptions { + /// Selects a nonzero crop ratio and validates both percentage coordinates, if present. + /// + /// # Errors + /// Returns a client error for zero ratio dimensions or a coordinate above 100. + #[inline] + pub fn crop( + mut self, + width: u32, + height: u32, + focus: OutboundCropFocus, + ) -> Result { + if width == 0 || height == 0 { + return Err(EdgeError::bad_request( + "image optimizer crop dimensions must be nonzero", + )); + } + if matches!(focus, OutboundCropFocus::Percent { x, y } if x > 100 || y > 100) { + return Err(EdgeError::bad_request( + "image optimizer crop percentages must be between 0 and 100", + )); + } + self.crop = Some((width, height, focus)); + Ok(self) + } + + #[must_use] + #[inline] + pub fn crop_setting(&self) -> Option<(u32, u32, OutboundCropFocus)> { + self.crop + } + + #[must_use] + #[inline] + pub fn format(mut self, format: OutboundImageFormat) -> Self { + self.format = Some(format); + self + } + + #[must_use] + #[inline] + pub fn format_setting(&self) -> Option { + self.format + } + + /// Selects a nonzero output height. + /// + /// # Errors + /// Returns a client error for zero. + #[inline] + pub fn height(mut self, height: u32) -> Result { + if height == 0 { + return Err(EdgeError::bad_request( + "image optimizer height must be nonzero", + )); + } + self.height = Some(height); + Ok(self) + } + + #[must_use] + #[inline] + pub fn height_setting(&self) -> Option { + self.height + } + + #[must_use] + #[inline] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + #[inline] + pub fn preserve_origin_query(mut self, preserve: bool) -> Self { + self.preserve_origin_query = preserve; + self + } + + #[must_use] + #[inline] + pub fn preserve_origin_query_setting(&self) -> bool { + self.preserve_origin_query + } + + /// Selects quality in the inclusive 0..=100 range. + /// + /// # Errors + /// Returns a client error for values above 100. + #[inline] + pub fn quality(mut self, quality: u8) -> Result { + if quality > 100 { + return Err(EdgeError::bad_request( + "image optimizer quality must be between 0 and 100", + )); + } + self.quality = Some(quality); + Ok(self) + } + + #[must_use] + #[inline] + pub fn quality_setting(&self) -> Option { + self.quality + } + + #[must_use] + #[inline] + pub fn region(mut self, region: OutboundImageOptimizerRegion) -> Self { + self.region = Some(region); + self + } + + #[must_use] + #[inline] + pub fn region_setting(&self) -> Option<&OutboundImageOptimizerRegion> { + self.region.as_ref() + } + + #[must_use] + #[inline] + pub fn resize_filter(mut self, filter: OutboundResizeFilter) -> Self { + self.resize_filter = Some(filter); + self + } + + #[must_use] + #[inline] + pub fn resize_filter_setting(&self) -> Option { + self.resize_filter + } + + /// Selects a nonzero output width. + /// + /// # Errors + /// Returns a client error for zero. + #[inline] + pub fn width(mut self, width: u32) -> Result { + if width == 0 { + return Err(EdgeError::bad_request( + "image optimizer width must be nonzero", + )); + } + self.width = Some(width); + Ok(self) + } + + #[must_use] + #[inline] + pub fn width_setting(&self) -> Option { + self.width + } +} + +impl OutboundImageOptimizerRegion { + #[must_use] + #[inline] + pub fn as_str(&self) -> &str { + match self { + Self::Asia => "asia", + Self::Australia => "australia", + Self::EuCentral => "eu_central", + Self::EuWest => "eu_west", + Self::UsCentral => "us_central", + Self::UsEast => "us_east", + Self::UsWest => "us_west", + } + } + + /// Accepts exactly the seven region tokens serialized by Fastly SDK 0.13.1. + /// + /// # Errors + /// Returns a fixed client error for any other token, without echoing input. + #[inline] + pub fn parse(token: &str) -> Result { + let region = match token { + "asia" => Self::Asia, + "australia" => Self::Australia, + "eu_central" => Self::EuCentral, + "eu_west" => Self::EuWest, + "us_central" => Self::UsCentral, + "us_east" => Self::UsEast, + "us_west" => Self::UsWest, + _ => return Err(EdgeError::bad_request("invalid image optimizer region")), + }; + Ok(region) + } +} + +pub(super) fn validate_options(request: &OutboundRequest) -> Result<(), EdgeError> { + let Some(options) = request.image_optimizer_options() else { + return Ok(()); + }; + if !matches!(request.method(), &Method::GET | &Method::HEAD) { + return Err(EdgeError::bad_request( + "image_optimizer requires GET or HEAD", + )); + } + if request.cache_policy != OutboundCachePolicy::PlatformDefault { + return Err(EdgeError::bad_request( + "image_optimizer requires default cache policy", + )); + } + if options.region_setting().is_none() { + return Err(EdgeError::bad_request( + "image_optimizer requires an explicit region", + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::{ + EdgeError, OutboundCropFocus, OutboundImageFormat, OutboundImageOptimizerOptions, + OutboundImageOptimizerRegion, OutboundRequest, OutboundResizeFilter, + }; + + #[test] + fn image_optimizer_dispatch_rejects_missing_region_method_and_cache_bypass() { + use crate::http::Method; + use crate::outbound::{OutboundCachePolicy, OutboundSendMode, validate_for_dispatch}; + + let options = + OutboundImageOptimizerOptions::new().region(OutboundImageOptimizerRegion::Asia); + for request in [ + OutboundRequest::get("https://example.com/image") + .unwrap() + .image_optimizer(OutboundImageOptimizerOptions::new()), + OutboundRequest::new(Method::POST, "https://example.com/image".parse().unwrap()) + .unwrap() + .image_optimizer(options.clone()), + OutboundRequest::get("https://example.com/image") + .unwrap() + .image_optimizer(options.clone()) + .cache_policy(OutboundCachePolicy::Bypass), + ] { + let error = validate_for_dispatch(&request, OutboundSendMode::Send) + .expect_err("invalid optimizer combination"); + assert!(matches!(error, EdgeError::BadRequest { .. })); + assert!(error.message().contains("image_optimizer")); + } + for method in [Method::GET, Method::HEAD] { + let request = + OutboundRequest::new(method, "https://example.com/image".parse().unwrap()) + .unwrap() + .image_optimizer(options.clone()); + validate_for_dispatch(&request, OutboundSendMode::Send).expect("valid native request"); + } + } + + #[test] + fn image_optimizer_defaults_leave_every_selector_unset() { + let options = OutboundImageOptimizerOptions::new(); + assert_eq!(options, OutboundImageOptimizerOptions::default()); + assert!(options.region_setting().is_none()); + assert!(!options.preserve_origin_query_setting()); + assert!(options.format_setting().is_none()); + assert!(options.quality_setting().is_none()); + assert!(options.resize_filter_setting().is_none()); + assert!(options.width_setting().is_none()); + assert!(options.height_setting().is_none()); + assert!(options.crop_setting().is_none()); + } + + #[test] + fn image_optimizer_regions_match_the_pinned_sdk_tokens() { + for (token, region) in [ + ("asia", OutboundImageOptimizerRegion::Asia), + ("australia", OutboundImageOptimizerRegion::Australia), + ("eu_central", OutboundImageOptimizerRegion::EuCentral), + ("eu_west", OutboundImageOptimizerRegion::EuWest), + ("us_central", OutboundImageOptimizerRegion::UsCentral), + ("us_east", OutboundImageOptimizerRegion::UsEast), + ("us_west", OutboundImageOptimizerRegion::UsWest), + ] { + assert_eq!( + OutboundImageOptimizerRegion::parse(token).expect("SDK region"), + region + ); + assert_eq!(region.as_str(), token); + let options = OutboundImageOptimizerOptions::new().region(region); + assert_eq!(options.region_setting(), Some(®ion)); + } + } + + #[test] + fn image_optimizer_region_rejects_aliases_and_untrusted_tokens() { + for token in [ + "", + "US_EAST", + "us-east", + " us_east", + "us_east ", + "us_east\n", + "us_east\0", + "europe", + "global", + "asia/token-secret", + "\u{e9}", + ] { + let error = OutboundImageOptimizerRegion::parse(token).expect_err("invalid region"); + assert!(matches!(error, EdgeError::BadRequest { .. })); + assert!(!error.to_string().contains("token-secret")); + } + } + + #[test] + fn image_optimizer_quality_accepts_exactly_zero_through_one_hundred() { + for quality in 0_u8..=u8::MAX { + let result = OutboundImageOptimizerOptions::new().quality(quality); + if quality <= 100 { + assert_eq!( + result.expect("valid quality").quality_setting(), + Some(quality) + ); + } else { + assert!(matches!(result, Err(EdgeError::BadRequest { .. }))); + } + } + } + + #[test] + fn image_optimizer_dimensions_and_crop_reject_zero_without_invented_maxima() { + for dimension in [0, 1, u32::MAX] { + let width = OutboundImageOptimizerOptions::new().width(dimension); + let height = OutboundImageOptimizerOptions::new().height(dimension); + if dimension == 0 { + assert!(matches!(width, Err(EdgeError::BadRequest { .. }))); + assert!(matches!(height, Err(EdgeError::BadRequest { .. }))); + } else { + assert_eq!( + width.expect("positive width").width_setting(), + Some(dimension) + ); + assert_eq!( + height.expect("positive height").height_setting(), + Some(dimension) + ); + } + } + for (width, height) in [(0, 0), (0, 1), (1, 0), (1, 1), (u32::MAX, u32::MAX)] { + let result = OutboundImageOptimizerOptions::new().crop( + width, + height, + OutboundCropFocus::Default, + ); + if width == 0 || height == 0 { + assert!(matches!(result, Err(EdgeError::BadRequest { .. }))); + } else { + assert_eq!( + result.expect("positive ratio").crop_setting(), + Some((width, height, OutboundCropFocus::Default)) + ); + } + } + } + + #[test] + fn image_optimizer_crop_checks_both_public_percent_payloads() { + for (x, y) in [ + (0, 0), + (0, 100), + (100, 0), + (100, 100), + (101, 0), + (0, 101), + (255, 255), + ] { + let focus = OutboundCropFocus::Percent { x, y }; + let result = OutboundImageOptimizerOptions::new().crop(16, 9, focus); + if x <= 100 && y <= 100 { + assert_eq!( + result.expect("percent focus").crop_setting(), + Some((16, 9, focus)) + ); + } else { + assert!(matches!(result, Err(EdgeError::BadRequest { .. }))); + } + } + for focus in [OutboundCropFocus::Default, OutboundCropFocus::Smart] { + assert_eq!( + OutboundImageOptimizerOptions::new() + .crop(16, 9, focus) + .expect("focus") + .crop_setting(), + Some((16, 9, focus)) + ); + } + } + + #[test] + fn image_optimizer_readbacks_preserve_independent_settings_and_replacement() { + let region = OutboundImageOptimizerRegion::EuWest; + let options = OutboundImageOptimizerOptions::new() + .region(region) + .preserve_origin_query(true) + .format(OutboundImageFormat::WebP) + .quality(0) + .expect("quality") + .quality(100) + .expect("quality replacement") + .resize_filter(OutboundResizeFilter::Lanczos3) + .width(320) + .expect("width") + .height(180) + .expect("height") + .crop(16, 9, OutboundCropFocus::Percent { x: 20, y: 80 }) + .expect("crop"); + assert_eq!(options.region_setting(), Some(®ion)); + assert!(options.preserve_origin_query_setting()); + assert_eq!(options.format_setting(), Some(OutboundImageFormat::WebP)); + assert_eq!(options.quality_setting(), Some(100)); + assert_eq!( + options.resize_filter_setting(), + Some(OutboundResizeFilter::Lanczos3) + ); + assert_eq!(options.width_setting(), Some(320)); + assert_eq!(options.height_setting(), Some(180)); + assert_eq!( + options.crop_setting(), + Some((16, 9, OutboundCropFocus::Percent { x: 20, y: 80 })) + ); + let changed = options + .clone() + .preserve_origin_query(false) + .format(OutboundImageFormat::Auto) + .resize_filter(OutboundResizeFilter::Nearest) + .width(1) + .expect("width replacement") + .height(2) + .expect("height replacement") + .crop(1, 2, OutboundCropFocus::Smart) + .expect("crop replacement") + .region(OutboundImageOptimizerRegion::Asia); + assert_eq!( + changed.region_setting(), + Some(&OutboundImageOptimizerRegion::Asia) + ); + assert!(!changed.preserve_origin_query_setting()); + assert_eq!(changed.format_setting(), Some(OutboundImageFormat::Auto)); + assert_eq!( + changed.resize_filter_setting(), + Some(OutboundResizeFilter::Nearest) + ); + assert_eq!(changed.width_setting(), Some(1)); + assert_eq!(changed.height_setting(), Some(2)); + assert_eq!( + changed.crop_setting(), + Some((1, 2, OutboundCropFocus::Smart)) + ); + assert_eq!(options.clone(), options); + assert_ne!(changed, options); + } + + #[test] + fn image_optimizer_all_selectors_have_copy_equality_readback() { + for format in [ + OutboundImageFormat::Auto, + OutboundImageFormat::Avif, + OutboundImageFormat::Gif, + OutboundImageFormat::Jpeg, + OutboundImageFormat::JpegXl, + OutboundImageFormat::Mp4, + OutboundImageFormat::Png, + OutboundImageFormat::WebP, + ] { + assert_eq!( + OutboundImageOptimizerOptions::new() + .format(format) + .format_setting(), + Some(format) + ); + } + for filter in [ + OutboundResizeFilter::Nearest, + OutboundResizeFilter::Bilinear, + OutboundResizeFilter::Bicubic, + OutboundResizeFilter::Lanczos2, + OutboundResizeFilter::Lanczos3, + ] { + assert_eq!( + OutboundImageOptimizerOptions::new() + .resize_filter(filter) + .resize_filter_setting(), + Some(filter) + ); + } + } +} diff --git a/crates/edgezero-core/src/outbound/progress.rs b/crates/edgezero-core/src/outbound/progress.rs new file mode 100644 index 000000000..fb663a9db --- /dev/null +++ b/crates/edgezero-core/src/outbound/progress.rs @@ -0,0 +1,780 @@ +use std::fmt; +use std::pin::Pin; +use std::task::{Context, Poll, Waker}; + +use futures_util::future::poll_fn; + +use super::{ + OutboundExchangeOwner, OutboundFuture, OutboundPending, OutboundResponse, OutboundScope, + OutboundTime, Revocable, +}; +use crate::error::EdgeError; +use crate::time::MonotonicInstant; + +type PendingObservation = fn(&mut PendingState, &mut Context<'_>) -> Poll>; + +pub(super) enum PendingStorage { + Owned(Box), + Scoped(Revocable), +} + +struct PendingBinding { + entered_at: MonotonicInstant, + scope: OutboundScope, + time: OutboundTime, +} + +pub(super) struct PendingState { + binding: Option, + completion: Option>, + outcome: Option>, + owner: Option, +} + +impl OutboundExchangeOwner { + fn disarm(mut self) { + drop(self.release.take()); + } + + /// Owns one nonblocking release operation, run when this owner is dropped. + /// Pending collection disarms its cancellation callback when success transfers ownership. + #[inline] + pub fn new(release: Release) -> Self + where + Release: FnOnce() + 'static, + { + Self { + release: Some(Box::new(release)), + } + } +} + +impl fmt::Debug for OutboundExchangeOwner { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutboundExchangeOwner") + .finish_non_exhaustive() + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for OutboundExchangeOwner { + #[inline] + fn drop(&mut self) { + if let Some(release) = self.release.take() { + release(); + } + } +} + +impl OutboundPending { + pub(super) fn bind( + mut self, + scope: &OutboundScope, + time: OutboundTime, + entered_at: MonotonicInstant, + ) -> Result { + let PendingStorage::Owned(mut state) = self.state else { + return Err(EdgeError::internal(anyhow::anyhow!( + "outbound pending was scoped twice" + ))); + }; + state.binding = Some(PendingBinding { + scope: scope.clone(), + time, + entered_at, + }); + self.state = PendingStorage::Scoped(Revocable::new(scope, *state)?); + Ok(self) + } + + /// Cancels scheduled work and drops any cached response without polling its body. + #[inline] + pub fn cancel(self) { + drop(self); + } + + /// Collects the original completion, transferring a successful live response unchanged. + /// Dropping this future, including before its first poll, cancels remaining work. + /// + /// # Errors + /// Returns the original completion error unchanged. + #[inline] + pub fn collect(mut self) -> OutboundFuture { + Box::pin(async move { + poll_fn(|context| self.observe(context, PendingState::drive)).await; + match &mut self.state { + PendingStorage::Owned(state) => state.disarm_success(), + PendingStorage::Scoped(state) => { + state.with(&mut Context::from_waker(Waker::noop()), |value, _cx| { + value.disarm_success(); + })?; + } + } + let mut state = match self.state { + PendingStorage::Owned(value) => *value, + PendingStorage::Scoped(mut value) => value.take()?, + }; + state.outcome.take().ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("outbound completion missing its result")) + })? + }) + } + + /// Checks the completion receiver once without waiting, consuming a result or polling a body. + /// Success and failure are both ready; repeated checks use the cached original result. + #[must_use] + #[inline] + pub fn is_ready(&mut self) -> bool { + self.observe( + &mut Context::from_waker(Waker::noop()), + PendingState::observe_scoped, + ) + .is_ready() + } + + /// Wraps a receiver for already-issued work, not a lazy transport future. + /// The owner cancels scheduled work on failure/cancellation and is disarmed on successful + /// collection. Completed buffered responses must already have released transport resources; + /// streamed responses must already own their transport cleanup before being published. + #[inline] + pub fn new( + already_scheduled: OutboundFuture, + owner: OutboundExchangeOwner, + ) -> Self { + Self { + ready: false, + state: PendingStorage::Owned(Box::new(PendingState { + binding: None, + completion: Some(already_scheduled), + outcome: None, + owner: Some(owner), + })), + } + } + + fn observe(&mut self, context: &mut Context<'_>, receiver: PendingObservation) -> Poll<()> { + let observation = match &mut self.state { + PendingStorage::Owned(state) => receiver(state, context), + PendingStorage::Scoped(state) => state + .with(context, receiver) + .unwrap_or_else(|error| Poll::Ready(Err(error))), + }; + match observation { + Poll::Pending => Poll::Pending, + Poll::Ready(result) => { + self.ready = true; + if let Err(error) = result { + self.state = PendingStorage::Owned(Box::new(PendingState { + binding: None, + completion: None, + outcome: Some(Err(error)), + owner: None, + })); + } + Poll::Ready(()) + } + } + } +} + +impl PendingState { + fn disarm_success(&mut self) { + if self.outcome.as_ref().is_some_and(Result::is_ok) + && let Some(owner) = self.owner.take() + { + owner.disarm(); + } + } + + fn drive(&mut self, context: &mut Context<'_>) -> Poll> { + if self.outcome.is_some() { + return self.observe_scoped(context); + } + let Some(binding) = &self.binding else { + return self.observe(context).map(Ok); + }; + let scope = binding.scope.clone(); + let time = binding.time.clone(); + let entered_at = binding.entered_at; + let mut receiver = poll_fn(|cx| self.observe(cx)); + scope.poll(&time, entered_at, context, Pin::new(&mut receiver)) + } + + fn observe(&mut self, context: &mut Context<'_>) -> Poll<()> { + if self.outcome.is_some() { + return Poll::Ready(()); + } + let outcome = if let Some(completion) = &mut self.completion { + let Poll::Ready(outcome) = completion.as_mut().poll(context) else { + return Poll::Pending; + }; + outcome + } else { + Err(EdgeError::internal(anyhow::anyhow!( + "outbound completion receiver missing" + ))) + }; + let failed = outcome.is_err(); + self.outcome = Some(outcome); + drop(self.completion.take()); + if failed { + drop(self.owner.take()); + } + Poll::Ready(()) + } + + fn observe_scoped(&mut self, context: &mut Context<'_>) -> Poll> { + let Some(binding) = &self.binding else { + return self.observe(context).map(Ok); + }; + let scope = binding.scope.clone(); + scope + .with(|| self.observe(context).map(Ok)) + .unwrap_or_else(|error| Poll::Ready(Err(error))) + } +} + +impl fmt::Debug for OutboundPending { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutboundPending") + .field("ready", &self.ready) + .finish_non_exhaustive() + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for PendingState { + fn drop(&mut self) { + // Stop scheduled work before releasing its receiver or an uncollected response. + drop(self.owner.take()); + } +} + +#[cfg(test)] +mod tests { + use std::cell::{Cell, RefCell}; + use std::future::Future; + use std::pin::Pin; + use std::rc::Rc; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::{Context, Poll, Waker}; + + use bytes::Bytes; + use futures::channel::oneshot; + use futures::executor::block_on; + use futures_util::future::pending; + use futures_util::stream; + use futures_util::task::{ArcWake, waker_ref}; + + use crate::body::Body; + use crate::error::EdgeError; + use crate::http::{HeaderMap, Method, StatusCode}; + use crate::outbound::{ + ContentTypeProvenance, OutboundExchangeOwner, OutboundFuture, OutboundPending, + OutboundResponse, + }; + use crate::time::MonotonicClock; + + struct Completion { + counts: CompletionCounts, + outcome: Option>, + pending_first: bool, + } + + #[derive(Clone, Default)] + struct CompletionCounts { + drops: Rc>, + polls: Rc>, + } + + struct DropCount(Rc>); + + #[derive(Debug, thiserror::Error)] + #[error("completion fixture failure")] + struct IdentityError(Arc<()>); + + struct OrderedDrop { + events: Rc>>, + name: &'static str, + } + + #[derive(Default)] + struct WakeCount(AtomicUsize); + + impl Future for Completion { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.counts + .polls + .set(self.counts.polls.get().saturating_add(1)); + if self.pending_first { + self.pending_first = false; + cx.waker().wake_by_ref(); + return Poll::Pending; + } + Poll::Ready(self.outcome.take().expect("completion polled only once")) + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for Completion { + fn drop(&mut self) { + self.counts + .drops + .set(self.counts.drops.get().saturating_add(1)); + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropCount { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for OrderedDrop { + fn drop(&mut self) { + self.events.borrow_mut().push(self.name); + } + } + + impl ArcWake for WakeCount { + fn wake(self: Arc) { + Self::wake_by_ref(&self); + } + + fn wake_by_ref(arc_self: &Arc) { + arc_self.0.fetch_add(1, Ordering::SeqCst); + } + } + + fn completion( + outcome: Result, + counts: &CompletionCounts, + pending_first: bool, + ) -> OutboundFuture { + Box::pin(Completion { + counts: counts.clone(), + outcome: Some(outcome), + pending_first, + }) + } + + fn owner(calls: &Rc>) -> OutboundExchangeOwner { + let captured_calls = Rc::clone(calls); + OutboundExchangeOwner::new(move || { + captured_calls.set(captured_calls.get().saturating_add(1)); + }) + } + + fn poll_outcome( + future: &mut OutboundFuture, + ) -> Poll> { + future + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + } + + fn response(body: Body) -> OutboundResponse { + OutboundResponse::new( + Method::GET, + StatusCode::OK, + HeaderMap::new(), + body, + MonotonicClock::default(), + ContentTypeProvenance::Unknown, + ) + } + + #[test] + fn exchange_owner_releases_local_capture_once() { + let calls = Rc::new(Cell::new(0_usize)); + let capture_drops = Rc::new(Cell::new(0)); + let captured = DropCount(Rc::clone(&capture_drops)); + let observed = Rc::clone(&calls); + let owner = crate::OutboundExchangeOwner::new(move || { + observed.set(observed.get().saturating_add(1)); + drop(captured); + }); + assert!(format!("{owner:?}").contains("OutboundExchangeOwner")); + assert_eq!(calls.get(), 0); + assert_eq!(capture_drops.get(), 0); + drop(owner); + assert_eq!(calls.get(), 1); + assert_eq!(capture_drops.get(), 1); + } + + #[test] + fn pending_construction_does_not_poll_and_cancel_drops_without_polling() { + for cancel in [false, true] { + let calls = Rc::new(Cell::new(0)); + let counts = CompletionCounts::default(); + let pending = crate::OutboundPending::new( + completion(Ok(response(Body::empty())), &counts, false), + owner(&calls), + ); + assert_eq!(counts.polls.get(), 0); + if cancel { + pending.cancel(); + } else { + drop(pending); + } + assert_eq!(counts.polls.get(), 0); + assert_eq!(counts.drops.get(), 1); + assert_eq!(calls.get(), 1); + } + } + + #[test] + fn pending_readiness_caches_success_and_releases_completed_receiver() { + let calls = Rc::new(Cell::new(0_usize)); + let capture_drops = Rc::new(Cell::new(0)); + let captured = DropCount(Rc::clone(&capture_drops)); + let counted = Rc::clone(&calls); + let counts = CompletionCounts::default(); + let mut pending = OutboundPending::new( + completion(Ok(response(Body::from("answer"))), &counts, true), + OutboundExchangeOwner::new(move || { + counted.set(counted.get().saturating_add(1)); + drop(captured); + }), + ); + assert!(format!("{pending:?}").contains("ready: false")); + assert_eq!(counts.polls.get(), 0, "Debug never polls the receiver"); + assert!(!pending.is_ready()); + assert_eq!(counts.polls.get(), 1); + assert!(pending.is_ready()); + assert_eq!(counts.polls.get(), 2); + assert_eq!(counts.drops.get(), 1); + let ready_debug = format!("{pending:?}"); + assert!(ready_debug.contains("ready: true")); + assert!( + !ready_debug.contains("answer"), + "Debug excludes cached payloads" + ); + for _ in 0_usize..3 { + assert!(pending.is_ready()); + } + assert_eq!(counts.polls.get(), 2); + let mut collect: crate::OutboundFuture = pending.collect(); + let Poll::Ready(Ok(response)) = poll_outcome(&mut collect) else { + panic!("cached response must be ready"); + }; + assert_eq!(response.body().as_bytes(), Some(b"answer".as_slice())); + assert_eq!(counts.polls.get(), 2); + assert_eq!(counts.drops.get(), 1); + assert_eq!(calls.get(), 0, "successful collection disarms cancellation"); + assert_eq!(capture_drops.get(), 1, "completed future retains no owner"); + drop(collect); + assert_eq!(calls.get(), 0); + assert_eq!(capture_drops.get(), 1); + } + + #[test] + fn pending_readiness_preserves_original_nonclone_error() { + use crate::error_detail::{ProviderFailureDetail, ProviderFailurePhase, SpinFailureCause}; + + let detail = ProviderFailureDetail::Spin { + phase: ProviderFailurePhase::ResponseHeaders, + cause: SpinFailureCause::ConnectionRefused, + }; + for checked in [false, true] { + let identity = Arc::new(()); + let counts = CompletionCounts::default(); + let calls = Rc::new(Cell::new(0)); + let mut pending = OutboundPending::new( + completion( + Err(EdgeError::internal_with_detail( + IdentityError(Arc::clone(&identity)), + detail, + )), + &counts, + false, + ), + owner(&calls), + ); + if checked { + assert!(pending.is_ready(), "failure is also a ready result"); + assert_eq!(calls.get(), 1, "failed work is released on observation"); + assert_eq!(counts.drops.get(), 1); + assert!(pending.is_ready()); + } + let mut collect = pending.collect(); + let Poll::Ready(Err(error)) = poll_outcome(&mut collect) else { + panic!("original error must be ready"); + }; + let original = error + .inner() + .expect("internal error") + .downcast_ref::() + .expect("original fixture identity"); + assert!(Arc::ptr_eq(&original.0, &identity)); + assert_eq!(error.provider_failure_detail(), Some(&detail)); + assert_eq!(counts.polls.get(), 1); + assert_eq!(counts.drops.get(), 1); + assert_eq!(calls.get(), 1); + drop(collect); + assert_eq!(calls.get(), 1); + } + } + + #[test] + fn pending_collection_cancellation_releases_unpolled_and_waiting_work() { + for polled in [false, true] { + let calls = Rc::new(Cell::new(0)); + let future_drops = Rc::new(Cell::new(0)); + let captured = DropCount(Rc::clone(&future_drops)); + let future = Box::pin(async move { + pending::<()>().await; + drop(captured); + Ok(response(Body::empty())) + }); + let pending = OutboundPending::new(future, owner(&calls)); + let mut collect = pending.collect(); + if polled { + assert!(poll_outcome(&mut collect).is_pending()); + } + assert_eq!(calls.get(), 0); + assert_eq!(future_drops.get(), 0); + drop(collect); + assert_eq!(calls.get(), 1); + assert_eq!(future_drops.get(), 1); + } + } + + #[test] + fn pending_cancellation_precedes_receiver_and_cached_body_release() { + for checked in [false, true] { + for cancel in [false, true] { + let events = Rc::new(RefCell::new(Vec::new())); + let body_drop = OrderedDrop { + events: Rc::clone(&events), + name: "body", + }; + let body = Body::from_stream(stream::poll_fn(move |_| { + let _retain = &body_drop; + panic!("cancelled response must not be polled"); + })); + let receiver_drop = OrderedDrop { + events: Rc::clone(&events), + name: "receiver", + }; + let cancel_events = Rc::clone(&events); + let mut pending = OutboundPending::new( + Box::pin(async move { + drop(receiver_drop); + Ok(response(body)) + }), + OutboundExchangeOwner::new(move || cancel_events.borrow_mut().push("cancel")), + ); + if checked { + assert!(pending.is_ready()); + assert_eq!(*events.borrow(), ["receiver"]); + } + if cancel { + pending.cancel(); + } else { + drop(pending); + } + let observed = events.borrow(); + assert_eq!(observed.len(), 3); + if checked { + assert_eq!(*observed, ["receiver", "cancel", "body"]); + } else { + assert_eq!(observed.first(), Some(&"cancel")); + for name in ["receiver", "body"] { + assert_eq!(observed.iter().filter(|event| **event == name).count(), 1); + } + } + } + } + } + + #[test] + fn collected_stream_stays_live_after_pending_handoff() { + for checked in [false, true] { + let aborted = Rc::new(Cell::new(false)); + let body_drops = Rc::new(Cell::new(0)); + let body_polls = Rc::new(Cell::new(0_usize)); + let release_body = owner(&body_drops); + let body_aborted = Rc::clone(&aborted); + let counted = Rc::clone(&body_polls); + let mut chunk = Some(Bytes::from_static(b"live")); + let body = Body::from_stream(stream::poll_fn(move |_| { + let _retain_owner = &release_body; + counted.set(counted.get().saturating_add(1)); + assert!(!body_aborted.get(), "pending owner must not abort body"); + Poll::Ready(chunk.take().map(Ok)) + })); + let cancel_aborted = Rc::clone(&aborted); + let counts = CompletionCounts::default(); + let mut pending = OutboundPending::new( + completion(Ok(response(body)), &counts, false), + OutboundExchangeOwner::new(move || cancel_aborted.set(true)), + ); + if checked { + assert!(pending.is_ready()); + } + assert_eq!(body_polls.get(), 0); + let collected = block_on(pending.collect()).expect("collected stream"); + assert_eq!(body_polls.get(), 0, "collection never drains a stream"); + assert_eq!(body_drops.get(), 0); + assert!(!aborted.get()); + let bytes = block_on(collected.into_bytes_bounded(4)).expect("live body"); + assert_eq!(bytes, Bytes::from_static(b"live")); + assert_eq!(body_polls.get(), 2); + assert_eq!(body_drops.get(), 1); + assert!(!aborted.get()); + } + } + + #[test] + fn pending_drop_or_cancel_releases_cached_response_body_once() { + for checked in [false, true] { + for cancel in [false, true] { + let calls = Rc::new(Cell::new(0)); + let body_drops = Rc::new(Cell::new(0)); + let release_body = owner(&body_drops); + let body = Body::from_stream(stream::poll_fn(move |_| { + let _retain_owner = &release_body; + panic!("dropped response body must never be polled"); + })); + let counts = CompletionCounts::default(); + let mut pending = OutboundPending::new( + completion(Ok(response(body)), &counts, false), + owner(&calls), + ); + if checked { + assert!(pending.is_ready()); + } + if cancel { + pending.cancel(); + } else { + drop(pending); + } + assert_eq!(counts.polls.get(), usize::from(checked)); + assert_eq!(counts.drops.get(), 1); + assert_eq!(body_drops.get(), 1); + assert_eq!(calls.get(), 1); + } + } + } + + #[test] + fn pending_collection_registers_real_waker_after_readiness_probe() { + let (sender, receiver) = oneshot::channel(); + let calls = Rc::new(Cell::new(0)); + let mut pending = OutboundPending::new( + Box::pin(async move { receiver.await.map_err(EdgeError::internal) }), + owner(&calls), + ); + assert!(!pending.is_ready()); + let mut collect = pending.collect(); + let count = Arc::new(WakeCount::default()); + let waker = waker_ref(&count); + let mut context = Context::from_waker(&waker); + assert!(collect.as_mut().poll(&mut context).is_pending()); + sender + .send(response(Body::from("scheduled"))) + .expect("receiver alive"); + assert!(count.0.load(Ordering::SeqCst) > 0); + let Poll::Ready(Ok(response)) = collect.as_mut().poll(&mut context) else { + panic!("scheduled completion must wake collector"); + }; + assert_eq!(response.body().as_bytes(), Some(b"scheduled".as_slice())); + assert_eq!(calls.get(), 0); + } + + #[test] + fn scoped_ready_pending_releases_cached_body_and_sdk_owner_at_scope_exit() { + use crate::{IndependentOutboundProgress, OutboundScope, OutboundScopeOwner, OutboundTime}; + + let callback = OutboundScopeOwner::callback(); + let scope = callback.with(|| OutboundScope::capture(Rc::new(IndependentOutboundProgress))); + let time = OutboundTime::new(MonotonicClock::default(), |_, _| { + panic!("no timer for cached completion") + }); + let entered_at = time.clock().now(); + let body_drops = Rc::new(Cell::new(0)); + let cancelled = Rc::new(Cell::new(0)); + let release = DropCount(Rc::clone(&body_drops)); + let body = Body::from_stream(stream::poll_fn(move |_cx| { + let _keep_alive = &release; + panic!("scope cancellation must not read a cached response body"); + })); + let mut retained = OutboundPending::new( + Box::pin(async move { Ok(response(body)) }), + owner(&cancelled), + ) + .bind(&scope, time, entered_at) + .expect("scope registration"); + assert!(retained.is_ready()); + assert_eq!(body_drops.get(), 0); + callback.close(); + assert_eq!(body_drops.get(), 1); + assert_eq!(cancelled.get(), 1); + let error = block_on(retained.collect()).expect_err("closed scope is not success"); + assert!(matches!(error, EdgeError::Internal { .. })); + assert_eq!(body_drops.get(), 1); + assert_eq!(cancelled.get(), 1); + } + + #[test] + fn scoped_pending_successful_stream_handoff_has_no_late_callback_abort() { + use crate::{IndependentOutboundProgress, OutboundScope, OutboundScopeOwner, OutboundTime}; + + let callback = OutboundScopeOwner::callback(); + let scope = callback.with(|| OutboundScope::capture(Rc::new(IndependentOutboundProgress))); + let time = OutboundTime::new(MonotonicClock::default(), |_, _| { + panic!("no timer for ready completion") + }); + let entered_at = time.clock().now(); + let aborted = Rc::new(Cell::new(false)); + let body_aborted = Rc::clone(&aborted); + let body_drops = Rc::new(Cell::new(0)); + let release = DropCount(Rc::clone(&body_drops)); + let mut chunk = Some(Bytes::from_static(b"live")); + let body = Body::from_stream(stream::poll_fn(move |_cx| { + let _keep_alive = &release; + assert!(!body_aborted.get()); + Poll::Ready(chunk.take().map(Ok)) + })); + let cancel_body = Rc::clone(&aborted); + let pending = OutboundPending::new( + Box::pin(async move { Ok(response(body)) }), + OutboundExchangeOwner::new(move || cancel_body.set(true)), + ) + .bind(&scope, time, entered_at) + .expect("scope registration"); + let collected = block_on(pending.collect()).expect("stream handoff"); + callback.close(); + assert!(!aborted.get()); + assert_eq!(body_drops.get(), 0); + assert_eq!( + block_on(collected.into_bytes_bounded(4)).expect("live body"), + Bytes::from_static(b"live") + ); + assert_eq!(body_drops.get(), 1); + assert!(!aborted.get()); + } +} diff --git a/crates/edgezero-core/src/outbound/redirect.rs b/crates/edgezero-core/src/outbound/redirect.rs new file mode 100644 index 000000000..78d06d3ba --- /dev/null +++ b/crates/edgezero-core/src/outbound/redirect.rs @@ -0,0 +1,529 @@ +use std::cell::Cell; +use std::rc::Rc; +use std::str; + +use futures_util::future::LocalBoxFuture; +use url::Url; + +use super::dispatch::{self, DispatchBounds}; +use super::{ + HttpClient, OutboundDispatchRequest, OutboundHead, OutboundRequest, OutboundResponse, + OutboundScope, OutboundSendMode, OutboundTime, canonicalize_url, head, scope, +}; +use crate::body::Body; +use crate::error::{BadGatewayReason, EdgeError}; +use crate::http::header::{ + AUTHORIZATION, CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, + CONTENT_TYPE, COOKIE, EXPECT, LOCATION, +}; +use crate::http::{HeaderMap, Method, StatusCode, Uri}; +use crate::time::{Deadline, MonotonicInstant}; + +struct FollowChain { + client: HttpClient, + index: Rc>, + limit: OutboundRedirectLimit, + original_deadline: Option, + previous: MonotonicInstant, + scope: OutboundScope, + time: OutboundTime, +} + +/// A failed request (initial hop zero) or an unchanged application policy rejection. +#[derive(Debug)] +pub enum OutboundFollowFailure { + Hop { index: usize, error: EdgeError }, + Policy { redirect_number: usize, error: E }, +} + +/// The final response and the actual URI and redirect count that produced it. +#[derive(Debug)] +pub struct OutboundFollowResult { + final_uri: Uri, + redirects_followed: usize, + response: OutboundResponse, + stop_reason: Option, +} + +/// The synchronous policy's disposition of one redirect head. +#[derive(Debug)] +#[expect( + clippy::large_enum_variant, + reason = "the approved owned policy action avoids an allocation on every replacement request" +)] +pub enum OutboundRedirectAction { + /// Send this complete replacement without implicitly inheriting options or stripping again. + Follow(OutboundRequest), + /// Follow the prepared request, unless the policy already took it or none was possible. + FollowPrepared, + /// Apply ordinary body handling and return the answering 3xx. + Stop, +} + +/// One answering head and an optional next request, after method and credential rules. +/// The initial request is never passed to this policy; authorize it before calling `follow`. +#[derive(Debug)] +pub struct OutboundRedirectContext<'head> { + cross_origin: bool, + headers: &'head HeaderMap, + prepared_request: Option, + redirect_number: usize, + request_uri: &'head Uri, + resolved_target: &'head str, + status: StatusCode, +} + +/// An explicit maximum of 1 through 20 followed redirects, with no implicit default. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct OutboundRedirectLimit(usize); + +/// Why a followable status was returned as the final response. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundRedirectStopReason { + BodyNotReplayable, + LimitReached, + MethodNotRewritable, + NoLocation, + StoppedByPolicy, +} + +impl FollowChain { + async fn finish( + &self, + head: OutboundHead, + final_uri: Uri, + stop_reason: Option, + ) -> Result> { + let response = head + .finish_from(self.previous) + .await + .map_err(|error| self.hop(error))?; + Ok(OutboundFollowResult { + final_uri, + redirects_followed: self.index.get(), + response, + stop_reason, + }) + } + + fn hop(&self, error: EdgeError) -> OutboundFollowFailure { + OutboundFollowFailure::Hop { + index: self.index.get(), + error, + } + } + + async fn run( + mut self, + mut prepared: OutboundDispatchRequest, + mut policy: P, + ) -> Result> + where + P: for<'head> FnMut( + &mut OutboundRedirectContext<'head>, + ) -> Result, + { + loop { + let settled = dispatch::settle(prepared) + .await + .map_err(|error| self.hop(error))?; + let budget = settled.budget(); + let (replay, replayable) = replay_request(settled.request()); + let uri = replay.uri.clone(); + let head = self + .client + .inner + .dispatch_head(settled) + .await + .map_err(|error| self.hop(error))?; + self.scope.check().map_err(|error| self.hop(error))?; + head::checked_live( + self.time.clock(), + &mut self.previous, + budget.deadline, + budget.cause, + ) + .map_err(|error| self.hop(error))?; + self.scope.check().map_err(|error| self.hop(error))?; + if !followable(head.status()) { + return self.finish(head, uri, None).await; + } + if let Some(reason) = stop_reason( + &head, + &replay.method, + replayable, + self.index.get() == self.limit.get(), + ) { + return self.finish(head, uri, Some(reason)).await; + } + let target = resolve_location(&head, &uri).map_err(|error| self.hop(error))?; + let base = Url::parse(&uri.to_string()).map_err(|_error| self.hop(protocol()))?; + let cross_origin = target.origin() != base.origin(); + let automatic = prepare_redirect(replay, &target, cross_origin, head.status()) + .map_err(|error| self.hop(error))?; + let redirect_number = self.index.get().saturating_add(1); + let mut context = OutboundRedirectContext { + cross_origin, + headers: head.headers(), + prepared_request: automatic, + redirect_number, + request_uri: &uri, + resolved_target: target.as_str(), + status: head.status(), + }; + // Rejections are application values, including non-Send/non-Clone values. Do not + // replace them with a later clock observation after synchronous policy execution. + let action = policy(&mut context).map_err(|error| OutboundFollowFailure::Policy { + redirect_number, + error, + })?; + self.scope.check().map_err(|error| self.hop(error))?; + let next = match action { + OutboundRedirectAction::Stop => { + drop(context); + return self + .finish(head, uri, Some(OutboundRedirectStopReason::StoppedByPolicy)) + .await; + } + OutboundRedirectAction::Follow(request) => request, + OutboundRedirectAction::FollowPrepared => context + .take_prepared_request() + .ok_or_else(|| self.hop(protocol()))?, + }; + drop(context); + let status = head.status(); + head.discard(); + self.index.set(redirect_number); + let entered_at = head::checked_sample(self.time.clock(), &mut self.previous) + .map_err(|error| self.hop(error))?; + prepared = dispatch::prepare( + &self.client, + next, + OutboundSendMode::Follow, + self.time.clone(), + entered_at, + DispatchBounds { + observation_cutoff: None, + original_chain_deadline: self.original_deadline, + }, + self.scope.clone(), + ) + .map_err(|error| self.hop(error))?; + let sent = prepared.request(); + log::debug!( + target: "edgezero::outbound::redirect", + "redirect status={} scheme={} host={} port={} path={}", + status.as_u16(), + sent.uri().scheme_str().unwrap_or_default(), + sent.host_name(), + sent.resolved_port(), + sent.uri().path(), + ); + } + } +} + +impl OutboundFollowResult { + #[must_use] + #[inline] + pub fn final_uri(&self) -> &Uri { + &self.final_uri + } + + #[must_use] + #[inline] + pub fn into_parts( + self, + ) -> ( + OutboundResponse, + Uri, + usize, + Option, + ) { + ( + self.response, + self.final_uri, + self.redirects_followed, + self.stop_reason, + ) + } + + #[must_use] + #[inline] + pub fn redirects_followed(&self) -> usize { + self.redirects_followed + } + + #[must_use] + #[inline] + pub fn response(&self) -> &OutboundResponse { + &self.response + } + + #[must_use] + #[inline] + pub fn stop_reason(&self) -> Option { + self.stop_reason + } +} + +impl OutboundRedirectContext<'_> { + #[must_use] + #[inline] + pub fn headers(&self) -> &HeaderMap { + self.headers + } + + #[must_use] + #[inline] + pub fn is_cross_origin(&self) -> bool { + self.cross_origin + } + + #[must_use] + #[inline] + pub fn prepared_request(&self) -> Option<&OutboundRequest> { + self.prepared_request.as_ref() + } + + #[must_use] + #[inline] + pub fn redirect_number(&self) -> usize { + self.redirect_number + } + + #[must_use] + #[inline] + pub fn request_uri(&self) -> &Uri { + self.request_uri + } + + #[must_use] + #[inline] + pub fn resolved_target(&self) -> &str { + self.resolved_target + } + + #[must_use] + #[inline] + pub fn status(&self) -> StatusCode { + self.status + } + + #[must_use] + #[inline] + pub fn take_prepared_request(&mut self) -> Option { + self.prepared_request.take() + } +} + +impl OutboundRedirectLimit { + #[must_use] + #[inline] + pub fn get(&self) -> usize { + self.0 + } + + /// Validates a maximum of 1 through 20 redirects (at most limit plus one sends). + /// + /// # Errors + /// Returns a client error outside the supported range. + #[inline] + pub fn new(limit: usize) -> Result { + if (1..=20).contains(&limit) { + Ok(Self(limit)) + } else { + Err(EdgeError::bad_request( + "redirect limit must be between 1 and 20", + )) + } + } +} + +pub(super) fn follow( + client: &HttpClient, + request: OutboundRequest, + limit: OutboundRedirectLimit, + policy: P, +) -> LocalBoxFuture<'static, Result>> +where + P: for<'head> FnMut(&mut OutboundRedirectContext<'head>) -> Result + + 'static, + E: 'static, +{ + let (scope, time, entered_at, prepared) = + match client.prepare(request, OutboundSendMode::Follow, None) { + Ok(captured) => captured, + Err(error) => { + return Box::pin( + async move { Err(OutboundFollowFailure::Hop { index: 0, error }) }, + ); + } + }; + let index = Rc::new(Cell::new(0)); + let chain = FollowChain { + client: client.clone(), + index: Rc::clone(&index), + limit, + original_deadline: prepared.context.original_chain_deadline, + previous: entered_at, + scope: scope.clone(), + time: time.clone(), + }; + // Reuse the exact scope binder; the inner Result retains the application's error type. + let bound = scope::bind_future( + scope, + time, + entered_at, + Box::pin(async move { Ok(chain.run(prepared, policy).await) }), + ); + Box::pin(async move { + bound.await.map_err(|error| OutboundFollowFailure::Hop { + index: index.get(), + error, + })? + }) +} + +fn followable(status: StatusCode) -> bool { + matches!(status.as_u16(), 301 | 302 | 303 | 307 | 308) +} + +fn prepare_redirect( + mut replay: OutboundRequest, + target: &Url, + cross_origin: bool, + status: StatusCode, +) -> Result, EdgeError> { + if !matches!(target.scheme(), "http" | "https") { + return Ok(None); + } + replay.uri = canonicalize_url(target.as_str()).map_err(|_error| protocol())?; + rewrite(&mut replay, status); + if cross_origin { + replay.headers.remove(AUTHORIZATION); + replay.headers.remove(COOKIE); + replay.host_authority_override = None; + } + Ok(Some(replay)) +} + +fn protocol() -> EdgeError { + EdgeError::bad_gateway_with_reason("invalid upstream redirect", BadGatewayReason::Protocol) +} + +fn replay_request(request: &OutboundRequest) -> (OutboundRequest, bool) { + // Exhaustive destructuring makes newly added options a compile-time migration obligation. + // No streamed body is cloned or buffered, and no public request-cloning API is introduced. + let OutboundRequest { + body, + cache_policy, + connect_timeout, + deadline, + headers, + host_authority_override, + image_optimizer, + inbound_read_deadline, + max_brotli_window_bits, + max_chunk_bytes, + max_decoded_response_bytes, + max_decoder_bytes, + max_encoded_response_bytes, + max_request_body_bytes, + max_response_header_bytes, + max_response_header_count, + method, + proxy_marker, + response_decoding, + response_mode, + timeout, + uri, + } = request; + let replayable = body.buffered_bytes().is_some(); + let replayed_body = body + .buffered_bytes() + .map_or_else(Body::empty, |bytes| Body::from(bytes.clone())); + ( + OutboundRequest { + body: replayed_body, + cache_policy: *cache_policy, + connect_timeout: *connect_timeout, + deadline: *deadline, + headers: headers.clone(), + host_authority_override: host_authority_override.clone(), + image_optimizer: image_optimizer.clone(), + inbound_read_deadline: *inbound_read_deadline, + max_brotli_window_bits: *max_brotli_window_bits, + max_chunk_bytes: *max_chunk_bytes, + max_decoded_response_bytes: *max_decoded_response_bytes, + max_decoder_bytes: *max_decoder_bytes, + max_encoded_response_bytes: *max_encoded_response_bytes, + max_request_body_bytes: *max_request_body_bytes, + max_response_header_bytes: *max_response_header_bytes, + max_response_header_count: *max_response_header_count, + method: method.clone(), + proxy_marker: *proxy_marker, + response_decoding: *response_decoding, + response_mode: *response_mode, + timeout: *timeout, + uri: uri.clone(), + }, + replayable, + ) +} + +fn resolve_location(head: &OutboundHead, uri: &Uri) -> Result { + let evidence = head.location_evidence(); + if evidence.observed_count() != 1 || evidence.has_non_text() || !evidence.retained_singleton() { + return Err(protocol()); + } + let raw = head.headers().get(LOCATION).ok_or_else(protocol)?; + let location = str::from_utf8(raw.as_bytes()).map_err(|_error| protocol())?; + let base = Url::parse(&uri.to_string()).map_err(|_error| protocol())?; + let mut target = base.join(location).map_err(|_error| protocol())?; + if !target.username().is_empty() || target.password().is_some() { + return Err(protocol()); + } + target.set_fragment(None); + Ok(target) +} + +fn rewrite(request: &mut OutboundRequest, status: StatusCode) { + if matches!(status.as_u16(), 301..=303) { + if request.method != Method::HEAD { + request.method = Method::GET; + } + request.body = Body::empty(); + for name in [ + CONTENT_TYPE, + CONTENT_LENGTH, + CONTENT_ENCODING, + CONTENT_LANGUAGE, + CONTENT_LOCATION, + EXPECT, + ] { + request.headers.remove(name); + } + } +} + +fn stop_reason( + head: &OutboundHead, + method: &Method, + replayable: bool, + at_limit: bool, +) -> Option { + if head.location_evidence().is_missing_or_empty() { + Some(OutboundRedirectStopReason::NoLocation) + } else if matches!(head.status().as_u16(), 301 | 302) + && !matches!(*method, Method::GET | Method::HEAD | Method::POST) + { + Some(OutboundRedirectStopReason::MethodNotRewritable) + } else if matches!(head.status().as_u16(), 307 | 308) && !replayable { + Some(OutboundRedirectStopReason::BodyNotReplayable) + } else if at_limit { + Some(OutboundRedirectStopReason::LimitReached) + } else { + None + } +} diff --git a/crates/edgezero-core/src/outbound/redirect_tests.rs b/crates/edgezero-core/src/outbound/redirect_tests.rs new file mode 100644 index 000000000..5f73ae615 --- /dev/null +++ b/crates/edgezero-core/src/outbound/redirect_tests.rs @@ -0,0 +1,2179 @@ +#![allow( + clippy::shadow_reuse, + clippy::shadow_unrelated, + reason = "table-driven cases reuse request, result and fixture bindings" +)] + +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::future::pending; +use std::iter::repeat_with; +use std::mem::take; +use std::num::NonZeroU64; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll, Waker}; +use std::time::Duration; + +use bytes::Bytes; +use futures::executor::block_on; +use futures_util::future::poll_fn; +use futures_util::stream::{self, StreamExt as _}; + +use crate::body::{Body, BodyPayload, BodyStream}; +use crate::error::{BadGatewayDecodeReason, BadGatewayReason, BudgetSource, EdgeError}; +use crate::http::{HeaderMap, HeaderValue, Method, StatusCode, Uri}; +use crate::time::{Deadline, DispatchBudget, MonotonicClock, MonotonicInstant}; + +use super::redirect::{ + OutboundFollowFailure, OutboundFollowResult, OutboundRedirectAction, OutboundRedirectContext, + OutboundRedirectLimit, OutboundRedirectStopReason, +}; + +use super::{ + HttpClient, IndependentOutboundProgress, OutboundBatch, OutboundCachePolicy, + OutboundDispatchRequest, OutboundExchangeOwner, OutboundFuture, OutboundHead, + OutboundHeaderFidelity, OutboundHeaderInput, OutboundHttpClient, OutboundImageOptimizerOptions, + OutboundImageOptimizerRegion, OutboundPending, OutboundPreparedBatch, OutboundProgressRuntime, + OutboundProxyMarker, OutboundRequest, OutboundRequestParts, OutboundScopeOwner, + OutboundSendMode, OutboundTime, ResponseDecoding, ResponseMode, +}; + +static REDIRECT_LOGGER: RedirectLogger = RedirectLogger; + +#[derive(Clone)] +struct ManualTime { + now: Arc>, + start: MonotonicInstant, + waiters: Arc>>, +} + +struct RecordingClient(ManualTime); + +struct Recorded { + body: Option, + budget: DispatchBudget, + entered_at: MonotonicInstant, + mode: OutboundSendMode, + parts: OutboundRequestParts, +} + +struct Reply { + headers: HeaderMap, + source: BodyStream, + status: StatusCode, +} + +enum Step { + Error(EdgeError), + Head(Reply), + Pending(Rc>), +} + +#[derive(Default)] +struct Recording { + records: Vec, + steps: VecDeque, + validations: Vec<(Uri, OutboundSendMode)>, +} + +struct Fixture { + client: HttpClient, + state: Rc>, + time: ManualTime, +} + +struct DropCount(Rc>); + +struct ScopeCloseOnDrop { + drops: Rc>, + owner: Option>, +} + +#[derive(Clone, Default)] +struct SourceProbe { + drops: Rc>, + polls: Rc>, +} + +#[derive(Clone, Copy)] +enum UnreadBody { + DeclaredHuge, + EmptyBrotli, + Failed, + Pending, +} + +struct PolicyRejection { + identity: Rc>, +} + +struct RedirectLogger; + +std::thread_local! { + static RECORDING: RefCell>>> = const { RefCell::new(None) }; + static REDIRECT_LOGS: RefCell> = const { RefCell::new(Vec::new()) }; +} + +impl ManualTime { + fn at(&self, millis: u64) -> MonotonicInstant { + self.start + .checked_add(Duration::from_millis(millis)) + .expect("bounded test clock offset") + } + + fn new() -> Self { + let start = MonotonicInstant::now(); + Self { + now: Arc::new(Mutex::new(start)), + start, + waiters: Arc::new(Mutex::new(Vec::new())), + } + } + + fn paired(&self) -> OutboundTime { + let now = Arc::clone(&self.now); + let waiters = Arc::clone(&self.waiters); + OutboundTime::new( + MonotonicClock::new(move || *now.lock().expect("clock")), + move |deadline, clock| { + let registrations = Arc::clone(&waiters); + Box::pin(poll_fn(move |cx| { + if deadline.is_expired_at(clock.now()) { + Poll::Ready(Ok(())) + } else { + registrations + .lock() + .expect("waiters") + .push(cx.waker().clone()); + Poll::Pending + } + })) + }, + ) + } + + fn set(&self, millis: u64) { + *self.now.lock().expect("clock") = self.at(millis); + let waiters = take(&mut *self.waiters.lock().expect("waiters")); + for waiter in waiters { + waiter.wake(); + } + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for DropCount { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for ScopeCloseOnDrop { + fn drop(&mut self) { + self.drops.set(self.drops.get().saturating_add(1)); + if let Some(owner) = &self.owner { + owner.close(); + } + } +} + +impl OutboundHttpClient for RecordingClient { + fn begin(&self, _request: OutboundDispatchRequest) -> Result { + Err(EdgeError::internal(anyhow::anyhow!( + "redirect fixture does not begin" + ))) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + let budget = request.budget(); + let entered_at = request.entered_at(); + let (mut parts, context) = request.into_parts(); + let (payload, owner) = take(&mut parts.body).into_parts(); + assert!( + owner.is_none(), + "request preflight rejects response body ownership" + ); + let body = match payload { + BodyPayload::Once(bytes) => { + parts.body = Body::from(bytes.clone()); + Body::from(bytes) + } + BodyPayload::Stream(source) => Body::from_stream(source), + }; + let recorded = Recorded { + body: body.as_bytes().map(Bytes::copy_from_slice), + budget, + entered_at, + mode: context.mode(), + parts, + }; + let step = RECORDING.with(|slot| { + let active = slot.borrow(); + let mut state = active.as_ref().expect("installed fixture").borrow_mut(); + state.records.push(recorded); + state + .steps + .pop_front() + .expect("one scripted head per dispatch") + }); + Box::pin(async move { + let _upload = body; + match step { + Step::Head(reply) => OutboundHead::new( + reply.status, + OutboundHeaderInput::new( + "fixture", + reply.headers, + OutboundHeaderFidelity::AdapterVisibleFields, + ), + reply.source, + context, + OutboundExchangeOwner::new(|| {}), + ), + Step::Error(error) => Err(error), + Step::Pending(drops) => { + let _release = DropCount(drops); + pending().await + } + } + }) + } + + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, _) = batch.into_slots(); + OutboundBatch::cutoff(context) + } + + fn time(&self) -> OutboundTime { + self.0.paired() + } + + fn validate_supported_options( + &self, + request: &OutboundRequest, + mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + RECORDING.with(|slot| { + slot.borrow() + .as_ref() + .expect("fixture") + .borrow_mut() + .validations + .push((request.uri().clone(), mode)); + }); + if request.headers().contains_key("x-fixture-unsupported") { + Err(EdgeError::bad_request("unsupported fixture option")) + } else { + Ok(()) + } + } +} + +impl Fixture { + fn new(steps: impl IntoIterator) -> Self { + let state = Rc::new(RefCell::new(Recording { + steps: steps.into_iter().collect(), + ..Recording::default() + })); + RECORDING.with(|slot| *slot.borrow_mut() = Some(Rc::clone(&state))); + let time = ManualTime::new(); + Self { + client: HttpClient::with_client(RecordingClient(time.clone())), + state, + time, + } + } + + fn sent(&self) -> usize { + self.state.borrow().records.len() + } +} + +impl Reply { + fn bytes(status: StatusCode, bytes: &'static [u8]) -> Self { + Self { + headers: HeaderMap::new(), + source: stream::once(async move { Ok(Bytes::from_static(bytes)) }).boxed_local(), + status, + } + } + + fn location(mut self, target: &str) -> Self { + self.headers + .insert("location", target.parse().expect("Location")); + self + } + + fn step(self) -> Step { + Step::Head(self) + } +} + +impl SourceProbe { + fn source(&self, kind: UnreadBody) -> BodyStream { + let release = DropCount(Rc::clone(&self.drops)); + let polls = Rc::clone(&self.polls); + stream::poll_fn(move |_| { + let _retained = &release; + polls.set(polls.get().saturating_add(1)); + match kind { + UnreadBody::Failed => Poll::Ready(Some(Err(EdgeError::bad_gateway_with_reason( + "scripted body failure", + BadGatewayReason::Unreachable, + )))), + UnreadBody::EmptyBrotli | UnreadBody::DeclaredHuge => Poll::Ready(None), + UnreadBody::Pending => Poll::Pending, + } + }) + .boxed_local() + } +} + +impl log::Log for RedirectLogger { + fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { + metadata.target() == "edgezero::outbound::redirect" + } + + fn flush(&self) {} + + fn log(&self, record: &log::Record<'_>) { + if self.enabled(record.metadata()) { + REDIRECT_LOGS.with(|messages| messages.borrow_mut().push(record.args().to_string())); + } + } +} + +fn limit(value: usize) -> OutboundRedirectLimit { + OutboundRedirectLimit::new(value).expect("valid redirect limit") +} + +fn success( + result: Result>, +) -> OutboundFollowResult { + match result { + Ok(result) => result, + Err(OutboundFollowFailure::Hop { index, error }) => { + panic!("unexpected hop {index} failure: {error}") + } + Err(OutboundFollowFailure::Policy { + redirect_number, .. + }) => { + panic!("unexpected policy rejection at redirect {redirect_number}") + } + } +} + +fn request(method: Method) -> OutboundRequest { + OutboundRequest::new(method, Uri::from_static("https://a.example/start")).expect("request") +} + +#[expect( + clippy::unnecessary_wraps, + reason = "redirect policies require a Result-returning signature" +)] +fn allow_follow(_: &mut OutboundRedirectContext<'_>) -> Result { + Ok(OutboundRedirectAction::FollowPrepared) +} + +fn hop_error( + result: Result>, + expected: usize, +) -> EdgeError { + match result { + Err(OutboundFollowFailure::Hop { index, error }) => { + assert_eq!(index, expected); + error + } + Err(OutboundFollowFailure::Policy { + redirect_number, .. + }) => { + panic!("expected hop failure, got policy rejection at redirect {redirect_number}") + } + Ok(_) => panic!("expected indexed hop failure"), + } +} + +fn assert_protocol(error: EdgeError) { + let EdgeError::BadGateway { + reason, message, .. + } = error + else { + panic!("expected protocol-classified gateway failure"); + }; + assert_eq!(reason, BadGatewayReason::Protocol, "{message}"); +} + +fn assert_deadline(error: EdgeError) { + let EdgeError::GatewayTimeout { cause, message, .. } = error else { + panic!("expected request-deadline timeout"); + }; + assert_eq!(cause, BudgetSource::RequestDeadline, "{message}"); +} + +fn assert_no_body_headers(headers: &HeaderMap) { + for name in [ + "content-type", + "content-length", + "content-encoding", + "content-language", + "content-location", + "expect", + ] { + assert!(!headers.contains_key(name), "dropped body retained {name}"); + } +} + +fn content_request(method: Method) -> OutboundRequest { + let bytes: &'static [u8] = if matches!(method, Method::GET | Method::HEAD) { + b"" + } else { + b"12345" + }; + request(method) + .body(bytes) + .header("content-type", "text/plain") + .expect("header") + .header("content-language", "en") + .expect("header") + .header("content-location", "/original") + .expect("header") + .header("content-encoding", "identity") + .expect("header") +} + +fn unread_reply(kind: UnreadBody, probe: &SourceProbe) -> Reply { + let mut reply = Reply { + headers: HeaderMap::new(), + source: probe.source(kind), + status: StatusCode::FOUND, + } + .location("/next"); + match kind { + UnreadBody::EmptyBrotli => { + reply + .headers + .insert("content-encoding", HeaderValue::from_static("br")); + } + UnreadBody::DeclaredHuge => { + reply + .headers + .insert("content-length", HeaderValue::from_static("10485760")); + } + UnreadBody::Failed | UnreadBody::Pending => {} + } + reply +} + +#[test] +fn redirect_chain_uses_supplied_head_client_and_original_deadline() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"discarded") + .location("/final") + .step(), + Reply::bytes(StatusCode::OK, b"final").step(), + ]); + let original = Deadline::at_instant(fixture.time.at(50)); + let policy_time = fixture.time.clone(); + let request = OutboundRequest::get("https://a.example/start") + .expect("request") + .timeout(Duration::from_millis(30)) + .deadline(original); + let future = fixture.client.follow(request, limit(4), move |context| { + assert_eq!(context.redirect_number(), 1); + assert_eq!(context.status(), StatusCode::FOUND); + assert_eq!( + context.request_uri(), + &Uri::from_static("https://a.example/start") + ); + assert_eq!(context.resolved_target(), "https://a.example/final"); + policy_time.set(10); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }); + assert_eq!(fixture.sent(), 0, "follow owns its input without eager I/O"); + fixture.time.set(5); + let result = success(block_on(future)); + assert_eq!( + result.final_uri(), + &Uri::from_static("https://a.example/final") + ); + assert_eq!(result.redirects_followed(), 1); + assert_eq!(result.stop_reason(), None); + let state = fixture.state.borrow(); + assert_eq!(state.records.len(), 2); + assert!( + state + .records + .iter() + .all(|record| record.mode == OutboundSendMode::Follow) + ); + assert_eq!(state.records[0].entered_at, fixture.time.start); + assert_eq!(state.records[1].entered_at, fixture.time.at(10)); + assert_eq!( + state.records[0].budget.deadline.instant(), + fixture.time.at(30) + ); + assert_eq!( + state.records[1].budget.deadline.instant(), + fixture.time.at(40) + ); + assert_eq!( + state.records[1] + .parts + .deadline + .as_ref() + .map(Deadline::instant), + Some(original.instant()) + ); + drop(state); + assert_eq!( + block_on(result.into_parts().0.into_bytes_bounded(16)).expect("final body"), + "final" + ); +} + +#[test] +fn redirect_limit_accepts_only_one_through_twenty() { + for value in [0, 21, usize::MAX] { + assert!(matches!( + OutboundRedirectLimit::new(value), + Err(EdgeError::BadRequest { .. }) + )); + } + for value in 1..=20 { + assert_eq!(limit(value).get(), value); + } +} + +#[test] +fn every_status_and_portable_method_obeys_the_redirect_table() { + for status in 300..=308 { + for method in [ + Method::GET, + Method::HEAD, + Method::POST, + Method::PUT, + Method::PATCH, + Method::DELETE, + Method::OPTIONS, + ] { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::from_u16(status).expect("status"), b"") + .location("/next") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + let calls = Rc::new(Cell::new(0)); + let observed = Rc::clone(&calls); + let result = success(block_on(fixture.client.follow( + content_request(method.clone()), + limit(4), + move |_| { + observed.set(observed.get() + 1); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }, + ))); + let next = expected_redirect_method(status, &method); + let state = fixture.state.borrow(); + assert_eq!( + state.records.len(), + 1 + usize::from(next.is_some()), + "{status} {method}" + ); + assert_eq!(calls.get(), usize::from(next.is_some())); + if let Some(next_method) = next { + assert_eq!(state.records[1].parts.method, next_method); + assert_redirect_body(&state.records[1], status, &method); + assert_eq!(result.response().status(), StatusCode::OK); + assert_eq!(result.redirects_followed(), 1); + assert_eq!(result.stop_reason(), None); + } else if matches!(status, 301 | 302) { + assert_eq!( + result.stop_reason(), + Some(OutboundRedirectStopReason::MethodNotRewritable) + ); + } else { + assert_eq!(result.stop_reason(), None); + } + } + } +} + +fn expected_redirect_method(status: u16, method: &Method) -> Option { + match status { + 301 | 302 if *method == Method::POST => Some(Method::GET), + 301 | 302 if matches!(*method, Method::GET | Method::HEAD) => Some(method.clone()), + 303 if *method == Method::HEAD => Some(Method::HEAD), + 303 => Some(Method::GET), + 307 | 308 => Some(method.clone()), + _ => None, + } +} + +fn assert_redirect_body(record: &Recorded, status: u16, method: &Method) { + let replayed = matches!(status, 307 | 308); + let expected = if replayed && !matches!(*method, Method::GET | Method::HEAD) { + &b"12345"[..] + } else { + &b""[..] + }; + assert_eq!(record.body.as_deref(), Some(expected)); + if replayed { + assert_eq!(record.parts.headers["content-type"], "text/plain"); + assert_eq!(record.parts.headers["content-language"], "en"); + assert_eq!(record.parts.headers["content-encoding"], "identity"); + assert_eq!(record.parts.headers["content-location"], "/original"); + } else { + assert_no_body_headers(&record.parts.headers); + } +} + +#[test] +fn rewritten_post_drops_content_metadata_and_expectation_for_later_replay() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("/second") + .step(), + Reply::bytes(StatusCode::TEMPORARY_REDIRECT, b"") + .location("/third") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + let initial = content_request(Method::POST) + .header("expect", "100-continue") + .expect("expectation"); + success(block_on(fixture.client.follow( + initial, + limit(4), + allow_follow, + ))); + let state = fixture.state.borrow(); + assert_eq!(state.records.len(), 3); + assert_eq!(state.records[0].parts.method, Method::POST); + assert_eq!(state.records[0].body.as_deref(), Some(&b"12345"[..])); + assert_eq!(state.records[0].parts.headers["expect"], "100-continue"); + for record in &state.records[1..] { + assert_eq!(record.parts.method, Method::GET); + assert_eq!(record.body.as_deref(), Some(&b""[..])); + assert_no_body_headers(&record.parts.headers); + } +} + +#[test] +fn replaying_empty_post_preserves_content_type() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::TEMPORARY_REDIRECT, b"") + .location("/next") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + let initial = request(Method::POST) + .header("content-type", "text/plain") + .expect("header"); + success(block_on(fixture.client.follow( + initial, + limit(4), + allow_follow, + ))); + let state = fixture.state.borrow(); + for record in &state.records { + assert_eq!(record.parts.method, Method::POST); + assert_eq!(record.body.as_deref(), Some(&b""[..])); + assert_eq!(record.parts.headers["content-type"], "text/plain"); + } + assert_eq!(state.records.len(), 2); +} + +#[test] +fn buffered_redirect_replay_shares_the_original_nonempty_allocation() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::TEMPORARY_REDIRECT, b"") + .location("/next") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + let original = Bytes::from(vec![42_u8; 1024]); + let allocation = original.as_ptr(); + let initial = request(Method::POST).body(original.clone()); + success(block_on(fixture.client.follow( + initial, + limit(4), + allow_follow, + ))); + let state = fixture.state.borrow(); + assert_eq!(state.records.len(), 2); + for record in &state.records { + let bytes = record.parts.body.as_bytes().expect("buffered replay"); + assert_eq!(bytes.len(), 1024); + assert_eq!( + bytes.as_ptr(), + allocation, + "replay must clone Bytes, not copy its slice" + ); + assert_eq!(bytes, original.as_ref()); + } +} + +#[test] +fn streamed_upload_cannot_replay_and_never_consults_policy() { + for status in [ + StatusCode::TEMPORARY_REDIRECT, + StatusCode::PERMANENT_REDIRECT, + ] { + let fixture = Fixture::new([Reply::bytes(status, b"").location("/next").step()]); + let body = Body::from_stream(stream::once(async { Ok(Bytes::from_static(b"upload")) })); + let result = success(block_on(fixture.client.follow( + request(Method::POST).body(body), + limit(4), + |_| -> Result { + panic!("non-replayable stream must stop before policy") + }, + ))); + assert_eq!(fixture.sent(), 1); + assert_eq!( + result.stop_reason(), + Some(OutboundRedirectStopReason::BodyNotReplayable) + ); + } +} + +#[test] +fn inbound_empty_get_stream_is_settled_once_before_all_replayed_hops() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::TEMPORARY_REDIRECT, b"") + .location("/next") + .step(), + Reply::bytes(StatusCode::PERMANENT_REDIRECT, b"") + .location("/last") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let body = Body::from_stream(stream::poll_fn(move |_| { + counted.set(counted.get() + 1); + Poll::Ready(None) + })); + let result = success(block_on(fixture.client.follow( + request(Method::GET).body(body), + limit(4), + allow_follow, + ))); + assert_eq!(polls.get(), 1); + assert_eq!(result.redirects_followed(), 2); + assert_eq!(result.stop_reason(), None); + let state = fixture.state.borrow(); + assert_eq!(state.records.len(), 3); + assert!( + state + .records + .iter() + .all(|record| record.body.as_deref() == Some(&b""[..])) + ); +} + +#[test] +fn followed_bodies_are_dropped_without_polling_or_finalization() { + for kind in [ + UnreadBody::Failed, + UnreadBody::EmptyBrotli, + UnreadBody::DeclaredHuge, + UnreadBody::Pending, + ] { + let probe = SourceProbe::default(); + let fixture = Fixture::new([ + unread_reply(kind, &probe).step(), + Reply::bytes(StatusCode::OK, b"final").step(), + ]); + let policy_probe = probe.clone(); + let result = success(block_on(fixture.client.follow( + request(Method::GET), + limit(4), + move |_| { + assert_eq!( + policy_probe.polls.get(), + 0, + "policy must precede payload processing" + ); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }, + ))); + assert_eq!(fixture.sent(), 2); + assert_eq!(probe.polls.get(), 0); + assert_eq!(probe.drops.get(), 1); + assert_eq!( + block_on(result.into_parts().0.into_bytes_bounded(16)).expect("final body"), + "final" + ); + } +} + +#[test] +fn stopped_and_limit_reached_heads_keep_ordinary_final_body_errors() { + for by_limit in [false, true] { + for kind in [ + UnreadBody::Failed, + UnreadBody::EmptyBrotli, + UnreadBody::DeclaredHuge, + ] { + let probe = SourceProbe::default(); + let mut steps = Vec::new(); + if by_limit { + steps.push( + Reply::bytes(StatusCode::FOUND, b"") + .location("/first") + .step(), + ); + } + steps.push(unread_reply(kind, &probe).step()); + let fixture = Fixture::new(steps); + let error = hop_error( + block_on( + fixture + .client + .follow(request(Method::GET), limit(1), move |_| { + Ok::<_, ()>(if by_limit { + OutboundRedirectAction::FollowPrepared + } else { + OutboundRedirectAction::Stop + }) + }), + ), + usize::from(by_limit), + ); + match kind { + UnreadBody::Failed => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Unreachable, + .. + } + )), + UnreadBody::EmptyBrotli => assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Decode(BadGatewayDecodeReason::Brotli), + .. + } + )), + UnreadBody::DeclaredHuge => { + assert!(matches!(error, EdgeError::ResponseTooLarge { .. })); + } + UnreadBody::Pending => panic!("pending final bodies are tested separately"), + } + assert_eq!(fixture.sent(), 1 + usize::from(by_limit)); + assert_eq!(probe.drops.get(), 1); + if matches!(kind, UnreadBody::DeclaredHuge) { + assert_eq!(probe.polls.get(), 0); + } + } + } +} + +#[test] +fn stopped_pending_final_body_waits_only_until_the_original_deadline() { + let probe = SourceProbe::default(); + let fixture = Fixture::new([unread_reply(UnreadBody::Pending, &probe).step()]); + let initial = request(Method::GET).deadline(Deadline::at_instant(fixture.time.at(20))); + let mut future = fixture.client.follow(initial, limit(4), |_| { + Ok::<_, ()>(OutboundRedirectAction::Stop) + }); + assert!( + future + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert!(probe.polls.get() > 0); + assert_eq!(probe.drops.get(), 0); + fixture.time.set(20); + assert_deadline(hop_error(block_on(future), 0)); + assert_eq!(probe.drops.get(), 1); + assert_eq!(fixture.sent(), 1); +} + +#[test] +fn original_chain_deadline_does_not_replace_a_fresh_requests_default_timeout() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + let clock = fixture.time.clone(); + let initial = request(Method::GET).deadline(Deadline::at_instant(fixture.time.at(60_000))); + success(block_on(fixture.client.follow( + initial, + limit(4), + move |_| { + clock.set(1_000); + Ok::<_, ()>(OutboundRedirectAction::Follow(request(Method::GET))) + }, + ))); + let state = fixture.state.borrow(); + assert_eq!(state.records.len(), 2); + assert_eq!(state.records[0].budget.cause, BudgetSource::RequestDeadline); + assert_eq!( + state.records[0].budget.deadline.instant(), + fixture.time.at(60_000) + ); + assert_eq!(state.records[1].budget.cause, BudgetSource::Default); + assert_eq!( + state.records[1].budget.deadline.instant(), + fixture.time.at(31_000) + ); + assert_eq!(state.records[1].parts.timeout, None); + assert!(state.records[1].parts.deadline.is_none()); +} + +#[test] +fn limit_four_sends_five_requests_and_skips_a_fifth_policy_call() { + let fixture = Fixture::new( + repeat_with(|| { + Reply::bytes(StatusCode::FOUND, b"last") + .location("/next") + .step() + }) + .take(5), + ); + let calls = Rc::new(Cell::new(0)); + let observed = Rc::clone(&calls); + let result = success(block_on(fixture.client.follow( + request(Method::GET), + limit(4), + move |context| { + observed.set(observed.get() + 1); + assert_eq!(context.redirect_number(), observed.get()); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }, + ))); + assert_eq!(fixture.sent(), 5); + assert_eq!(calls.get(), 4); + assert_eq!(result.response().status(), StatusCode::FOUND); + assert_eq!(result.redirects_followed(), 4); + assert_eq!( + result.stop_reason(), + Some(OutboundRedirectStopReason::LimitReached) + ); + assert_eq!( + block_on(result.into_parts().0.into_bytes_bounded(16)).expect("final body"), + "last" + ); +} + +#[test] +fn location_presence_and_method_rules_precede_limit_and_location_resolution() { + for (target, expected) in [ + (None, OutboundRedirectStopReason::NoLocation), + (Some(""), OutboundRedirectStopReason::NoLocation), + ( + Some("http://["), + OutboundRedirectStopReason::MethodNotRewritable, + ), + ] { + let mut last = Reply::bytes(StatusCode::MOVED_PERMANENTLY, b""); + if let Some(target) = target { + last = last.location(target); + } + let fixture = Fixture::new([ + Reply::bytes(StatusCode::TEMPORARY_REDIRECT, b"") + .location("/second") + .step(), + last.step(), + ]); + let result = success(block_on(fixture.client.follow( + content_request(Method::PUT), + limit(1), + |context| { + assert_eq!( + context.redirect_number(), + 1, + "last head must stop before policy" + ); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }, + ))); + assert_eq!(fixture.sent(), 2); + assert_eq!(result.stop_reason(), Some(expected)); + } +} + +#[test] +fn replay_rule_precedes_limit_and_invalid_location() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::TEMPORARY_REDIRECT, b"") + .location("/second") + .step(), + Reply::bytes(StatusCode::PERMANENT_REDIRECT, b"") + .location("http://[") + .step(), + ]); + let result = success(block_on(fixture.client.follow( + content_request(Method::POST), + limit(1), + |context| { + assert_eq!(context.redirect_number(), 1); + let replacement = request(Method::POST).body(Body::from_stream(stream::empty())); + Ok::<_, ()>(OutboundRedirectAction::Follow(replacement)) + }, + ))); + assert_eq!(fixture.sent(), 2); + assert_eq!( + result.stop_reason(), + Some(OutboundRedirectStopReason::BodyNotReplayable) + ); +} + +#[test] +fn limit_precedes_invalid_location_and_policy() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("/second") + .step(), + Reply::bytes(StatusCode::FOUND, b"") + .location("http://[") + .step(), + ]); + let result = success(block_on(fixture.client.follow( + request(Method::GET), + limit(1), + |context| { + assert_eq!(context.redirect_number(), 1); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }, + ))); + assert_eq!(fixture.sent(), 2); + assert_eq!( + result.stop_reason(), + Some(OutboundRedirectStopReason::LimitReached) + ); +} + +#[test] +fn relative_and_absolute_locations_resolve_without_sending_fragments() { + for (location, target) in [ + ("child", "https://a.example/dir/child"), + ("../x", "https://a.example/x"), + ("//b.example/x", "https://b.example/x"), + ("http://b.example/x#private", "http://b.example/x"), + ("/next?q=1#private", "https://a.example/next?q=1"), + ("#private", "https://a.example/dir/start"), + ] { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location(location) + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + let initial = OutboundRequest::get("https://a.example/dir/start").expect("request"); + let result = success(block_on(fixture.client.follow( + initial, + limit(4), + move |context| { + assert_eq!(context.resolved_target(), target); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }, + ))); + assert_eq!( + fixture.state.borrow().records[1].parts.uri.to_string(), + target + ); + assert_eq!(result.final_uri().to_string(), target); + } +} + +#[test] +fn unusable_locations_fail_at_the_answering_hop_before_policy() { + for target in [ + "http://[", + "http://user:secret@b.example/x", + "https://", + "http://b.example:99999/x", + ] { + let fixture = Fixture::new([Reply::bytes(StatusCode::FOUND, b"").location(target).step()]); + let outcome = block_on(fixture.client.follow( + request(Method::GET), + limit(4), + |_| -> Result { + panic!("invalid target must fail before policy") + }, + )); + assert_protocol(hop_error(outcome, 0)); + assert_eq!(fixture.sent(), 1); + } +} + +fn invalid_location_reply(duplicate: bool) -> Reply { + let mut reply = Reply::bytes(StatusCode::FOUND, b""); + if duplicate { + reply = reply.location("/next"); + } + reply.headers.append( + "location", + HeaderValue::from_bytes(b"\xff").expect("non-text header value"), + ); + reply +} + +#[test] +fn original_nontext_and_duplicate_location_evidence_survives_normalization() { + for duplicate in [false, true] { + let fixture = Fixture::new([invalid_location_reply(duplicate).step()]); + let outcome = block_on(fixture.client.follow( + request(Method::GET), + limit(4), + |_| -> Result { + panic!("original invalid Location must not become an eligible singleton") + }, + )); + assert_protocol(hop_error(outcome, 0)); + assert_eq!(fixture.sent(), 1); + } + let mut duplicate = Reply::bytes(StatusCode::FOUND, b"").location("/one"); + duplicate + .headers + .append("location", HeaderValue::from_static("/two")); + let fixture = Fixture::new([duplicate.step()]); + assert_protocol(hop_error( + block_on( + fixture + .client + .follow(request(Method::GET), limit(4), allow_follow), + ), + 0, + )); +} + +#[test] +fn earlier_stop_rules_do_not_bypass_normal_header_filtering() { + for duplicate in [false, true] { + let mut reply = invalid_location_reply(duplicate); + reply.status = StatusCode::MOVED_PERMANENTLY; + let fixture = Fixture::new([reply.step()]); + let result = success(block_on(fixture.client.follow( + content_request(Method::PUT), + limit(4), + |_| -> Result { + panic!("method rule precedes original Location evidence validation") + }, + ))); + assert_eq!( + result.stop_reason(), + Some(OutboundRedirectStopReason::MethodNotRewritable) + ); + assert_eq!( + result + .response() + .headers() + .get_all("location") + .iter() + .count(), + usize::from(duplicate) + ); + assert!( + result + .response() + .headers() + .get_all("location") + .iter() + .all(|value| value.to_str().is_ok()) + ); + } + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("/first") + .step(), + invalid_location_reply(true).step(), + ]); + let result = success(block_on(fixture.client.follow( + request(Method::GET), + limit(1), + allow_follow, + ))); + assert_eq!( + result.stop_reason(), + Some(OutboundRedirectStopReason::LimitReached) + ); + assert_eq!( + result + .response() + .headers() + .get_all("location") + .iter() + .count(), + 1 + ); +} + +#[test] +fn nonredirect_status_precedes_all_location_rules() { + let mut reply = invalid_location_reply(true); + reply.status = StatusCode::OK; + let fixture = Fixture::new([reply.step()]); + let result = success(block_on(fixture.client.follow( + request(Method::GET), + limit(4), + |_| -> Result { + panic!("nonredirect head never invokes policy") + }, + ))); + assert_eq!(result.response().status(), StatusCode::OK); + assert_eq!(result.stop_reason(), None); + assert_eq!(result.redirects_followed(), 0); +} + +#[test] +fn non_http_target_can_be_stopped_rejected_or_replaced_but_not_followed_prepared() { + for follow_prepared in [false, true] { + let fixture = Fixture::new([Reply::bytes(StatusCode::FOUND, b"") + .location("javascript:alert(1)") + .step()]); + let outcome = block_on(fixture.client.follow( + request(Method::GET), + limit(4), + move |context| { + assert_eq!(context.resolved_target(), "javascript:alert(1)"); + assert!(context.prepared_request().is_none()); + Ok::<_, ()>(if follow_prepared { + OutboundRedirectAction::FollowPrepared + } else { + OutboundRedirectAction::Stop + }) + }, + )); + if follow_prepared { + assert_protocol(hop_error(outcome, 0)); + } else { + assert_eq!( + success(outcome).stop_reason(), + Some(OutboundRedirectStopReason::StoppedByPolicy) + ); + } + assert_eq!(fixture.sent(), 1); + } + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("javascript:alert(1)") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + success(block_on(fixture.client.follow( + request(Method::GET), + limit(4), + |context| { + assert!(context.prepared_request().is_none()); + Ok::<_, ()>(OutboundRedirectAction::Follow(request(Method::GET))) + }, + ))); + assert_eq!(fixture.sent(), 2); +} + +#[test] +fn taking_prepared_request_then_following_prepared_is_a_protocol_failure() { + let fixture = Fixture::new([Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step()]); + let outcome = block_on( + fixture + .client + .follow(request(Method::GET), limit(4), |context| { + assert!(context.take_prepared_request().is_some()); + assert!( + context.take_prepared_request().is_none(), + "request transfers only once" + ); + assert!(context.prepared_request().is_none()); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }), + ); + assert_protocol(hop_error(outcome, 0)); + assert_eq!(fixture.sent(), 1); +} + +#[test] +fn policy_observes_normalized_head_and_next_request_before_any_next_dispatch() { + let mut reply = Reply::bytes(StatusCode::SEE_OTHER, b"").location("/next"); + reply + .headers + .insert("x-answer", HeaderValue::from_static("visible")); + let fixture = Fixture::new([reply.step(), Reply::bytes(StatusCode::OK, b"").step()]); + let recorded = Rc::clone(&fixture.state); + success(block_on(fixture.client.follow( + content_request(Method::POST), + limit(4), + move |context| { + assert_eq!(recorded.borrow().records.len(), 1); + assert_eq!(context.redirect_number(), 1); + assert_eq!(context.status(), StatusCode::SEE_OTHER); + assert_eq!( + context.request_uri(), + &Uri::from_static("https://a.example/start") + ); + assert_eq!(context.resolved_target(), "https://a.example/next"); + assert_eq!(context.headers()["x-answer"], "visible"); + assert!(!context.is_cross_origin()); + let prepared = context.prepared_request().expect("prepared next request"); + assert_eq!(prepared.method(), Method::GET); + assert_no_body_headers(prepared.headers()); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }, + ))); + assert_eq!(fixture.sent(), 2); + assert!( + fixture + .state + .borrow() + .validations + .iter() + .all(|(_, mode)| *mode == OutboundSendMode::Follow) + ); +} + +fn credential_request() -> OutboundRequest { + request(Method::GET) + .header("authorization", "original credential") + .expect("header") + .header("cookie", "session=original") + .expect("header") + .header("x-api-key", "application-managed") + .expect("header") + .header("x-amz-date", "20261027T000000Z") + .expect("header") + .host_authority_override("virtual.example") + .expect("authority") +} + +#[test] +fn origin_changes_strip_only_automatic_credentials_before_policy() { + for (target, cross_origin) in [ + ("https://a.example/next", false), + ("https://A.EXAMPLE:443/next", false), + ("https://b.example/next", true), + ("https://a.example:444/next", true), + ("http://a.example/next", true), + ] { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"").location(target).step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + success(block_on(fixture.client.follow( + credential_request(), + limit(4), + move |context| { + assert_eq!(context.is_cross_origin(), cross_origin); + let prepared = context.prepared_request().expect("HTTP target"); + assert_eq!( + prepared.headers().contains_key("authorization"), + !cross_origin + ); + assert_eq!(prepared.headers().contains_key("cookie"), !cross_origin); + assert_eq!(prepared.has_host_authority_override(), !cross_origin); + assert_eq!(prepared.headers()["x-api-key"], "application-managed"); + assert_eq!(prepared.headers()["x-amz-date"], "20261027T000000Z"); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }, + ))); + let state = fixture.state.borrow(); + assert_eq!(state.records.len(), 2); + let next = &state.records[1].parts; + assert_eq!(next.headers.contains_key("authorization"), !cross_origin); + assert_eq!(next.headers.contains_key("cookie"), !cross_origin); + assert_eq!(next.host_authority_override.is_some(), !cross_origin); + assert_eq!(next.headers["x-api-key"], "application-managed"); + assert_eq!(next.headers["x-amz-date"], "20261027T000000Z"); + assert!(!next.headers.contains_key("referer")); + assert!(!next.headers.contains_key("origin")); + } +} + +#[test] +fn policy_authorized_cross_origin_credentials_are_not_stripped_again() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("https://b.example/next") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + success(block_on(fixture.client.follow( + credential_request(), + limit(4), + |context| { + let prepared = context.take_prepared_request().expect("prepared request"); + assert!(!prepared.headers().contains_key("authorization")); + let authorized = prepared + .header("authorization", "new signature") + .expect("header") + .header("cookie", "session=new") + .expect("header") + .host_authority_override("signed.example") + .expect("authority"); + Ok::<_, ()>(OutboundRedirectAction::Follow(authorized)) + }, + ))); + let state = fixture.state.borrow(); + let sent = &state.records[1].parts; + assert_eq!(sent.headers["authorization"], "new signature"); + assert_eq!(sent.headers["cookie"], "session=new"); + assert_eq!( + sent.host_authority_override + .as_ref() + .expect("authority") + .as_str(), + "signed.example" + ); +} + +fn configured_request(time: &ManualTime) -> OutboundRequest { + request(Method::GET) + .timeout(Duration::from_millis(120)) + .deadline(Deadline::at_instant(time.at(200))) + .connect_timeout(Duration::from_millis(30)) + .cache_policy(OutboundCachePolicy::Bypass) + .proxy_marker(OutboundProxyMarker::Off) + .response_decoding(ResponseDecoding::Original) + .max_request_body_bytes(11) + .max_response_bytes(13) + .max_encoded_response_bytes(17) + .max_decoded_response_bytes(19) + .max_decoder_bytes(0x0001_0000) + .max_brotli_window_bits(20) + .max_chunk_bytes(NonZeroU64::new(3).expect("chunk")) + .max_response_header_bytes(4_096) + .max_response_header_count(16) +} + +fn assert_same_options(actual: &OutboundRequestParts, expected: &OutboundRequestParts) { + assert_eq!(actual.timeout, expected.timeout); + assert_eq!( + actual.deadline.as_ref().map(Deadline::instant), + expected.deadline.as_ref().map(Deadline::instant) + ); + assert_eq!(actual.connect_timeout, expected.connect_timeout); + assert_eq!(actual.cache_policy, expected.cache_policy); + assert_eq!(actual.proxy_marker, expected.proxy_marker); + assert_eq!(actual.response_decoding, expected.response_decoding); + assert_eq!(actual.response_mode, expected.response_mode); + assert_eq!( + actual.max_request_body_bytes, + expected.max_request_body_bytes + ); + assert_eq!( + actual.max_response_header_bytes, + expected.max_response_header_bytes + ); + assert_eq!( + actual.max_response_header_count, + expected.max_response_header_count + ); + assert_eq!( + actual.max_encoded_response_bytes, + expected.max_encoded_response_bytes + ); + assert_eq!( + actual.max_decoded_response_bytes, + expected.max_decoded_response_bytes + ); + assert_eq!(actual.max_decoder_bytes, expected.max_decoder_bytes); + assert_eq!( + actual.max_brotli_window_bits, + expected.max_brotli_window_bits + ); + assert_eq!(actual.max_chunk_bytes, expected.max_chunk_bytes); + assert_eq!(actual.image_optimizer, expected.image_optimizer); +} + +#[test] +fn prepared_requests_retain_all_unchanged_caps_and_options() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + let expected = configured_request(&fixture.time).into_parts(); + success(block_on(fixture.client.follow( + configured_request(&fixture.time), + limit(4), + |context| { + let prepared = context.take_prepared_request().expect("prepared request"); + let modified = prepared.header("x-policy", "modified").expect("header"); + Ok::<_, ()>(OutboundRedirectAction::Follow(modified)) + }, + ))); + let state = fixture.state.borrow(); + assert_same_options(&state.records[0].parts, &expected); + assert_same_options(&state.records[1].parts, &expected); + assert_eq!(state.records[1].parts.headers["x-policy"], "modified"); +} + +#[test] +fn image_optimizer_options_survive_every_prepared_follow_hop() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + let options = OutboundImageOptimizerOptions::new() + .region(OutboundImageOptimizerRegion::Asia) + .width(320) + .expect("width") + .preserve_origin_query(true); + let expected = options.clone(); + let result = fixture.client.follow( + request(Method::GET).image_optimizer(options), + limit(4), + move |context| { + assert_eq!( + context + .prepared_request() + .expect("prepared request") + .image_optimizer_options(), + Some(&expected) + ); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }, + ); + success(block_on(result)); + let state = fixture.state.borrow(); + assert_eq!(state.records[0].mode, OutboundSendMode::Follow); + assert_eq!(state.records[1].mode, OutboundSendMode::Follow); + assert_eq!( + state.records[0].parts.image_optimizer, + state.records[1].parts.image_optimizer + ); +} + +#[test] +fn freshly_built_policy_request_uses_its_own_default_caps_and_options() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step(), + Reply::bytes(StatusCode::OK, b"larger than old cap").step(), + ]); + let initial = configured_request(&fixture.time).max_response_bytes(1); + let expected = request(Method::GET) + .proxy_marker(OutboundProxyMarker::On) + .into_parts(); + let result = success(block_on(fixture.client.follow(initial, limit(4), |_| { + Ok::<_, ()>(OutboundRedirectAction::Follow(request(Method::GET))) + }))); + assert_same_options(&fixture.state.borrow().records[1].parts, &expected); + assert_eq!( + block_on(result.into_parts().0.into_bytes_bounded(64)).expect("fresh default cap"), + "larger than old cap" + ); +} + +#[test] +fn later_prepared_hops_inherit_policy_overrides_not_the_original_options() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("/second") + .step(), + Reply::bytes(StatusCode::FOUND, b"") + .location("/third") + .step(), + Reply::bytes(StatusCode::OK, b"data").step(), + ]); + success(block_on(fixture.client.follow( + request(Method::GET).max_response_bytes(1), + limit(4), + |context| { + let prepared = context.take_prepared_request().expect("prepared request"); + if context.redirect_number() == 1 { + Ok::<_, ()>(OutboundRedirectAction::Follow( + prepared + .max_response_bytes(8) + .timeout(Duration::from_millis(40)), + )) + } else { + let parts = prepared.into_parts(); + assert_eq!(parts.response_mode, ResponseMode::Buffered { max_bytes: 8 }); + assert_eq!(parts.timeout, Some(Duration::from_millis(40))); + Ok(OutboundRedirectAction::Follow( + OutboundRequest::from_parts(parts).expect("request"), + )) + } + }, + ))); + let state = fixture.state.borrow(); + for record in &state.records[1..] { + assert_eq!( + record.parts.response_mode, + ResponseMode::Buffered { max_bytes: 8 } + ); + assert_eq!(record.parts.timeout, Some(Duration::from_millis(40))); + } +} + +#[test] +fn policy_owns_local_state_and_returns_nonclone_nonsend_nondebug_error_unchanged() { + for target in ["/next", "javascript:alert(1)"] { + let fixture = Fixture::new([Reply::bytes(StatusCode::FOUND, b"").location(target).step()]); + let identity = Rc::new(Cell::new(7)); + let local = Rc::clone(&identity); + let outcome = block_on( + fixture + .client + .follow(request(Method::GET), limit(4), move |_| { + local.set(local.get() + 1); + Err::(PolicyRejection { + identity: Rc::clone(&local), + }) + }), + ); + match outcome { + Err(OutboundFollowFailure::Policy { + redirect_number, + error, + }) => { + assert_eq!(redirect_number, 1); + assert!(Rc::ptr_eq(&identity, &error.identity)); + assert_eq!(identity.get(), 8); + } + _ => panic!("application policy value must be returned unchanged"), + } + assert_eq!(fixture.sent(), 1); + } +} + +#[test] +fn policy_stop_returns_the_answering_head_as_final() { + let fixture = Fixture::new([Reply::bytes(StatusCode::FOUND, b"stopped") + .location("/next") + .step()]); + let result = success(block_on(fixture.client.follow( + request(Method::GET), + limit(4), + |_| Ok::<_, ()>(OutboundRedirectAction::Stop), + ))); + let (response, uri, redirects, reason) = result.into_parts(); + assert_eq!(uri, Uri::from_static("https://a.example/start")); + assert_eq!(redirects, 0); + assert_eq!(reason, Some(OutboundRedirectStopReason::StoppedByPolicy)); + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!( + block_on(response.into_bytes_bounded(16)).expect("final response"), + "stopped" + ); + assert_eq!(fixture.sent(), 1); +} + +#[test] +fn policy_returned_invalid_request_remains_a_client_error_at_the_selected_hop() { + let fixture = Fixture::new([Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step()]); + let error = hop_error( + block_on(fixture.client.follow(request(Method::GET), limit(4), |_| { + Ok::<_, ()>(OutboundRedirectAction::Follow( + request(Method::GET).body("invalid GET body"), + )) + })), + 1, + ); + assert!(matches!(error, EdgeError::BadRequest { .. })); + assert_eq!(fixture.sent(), 1); +} + +#[test] +fn transport_errors_keep_their_original_category_and_failed_hop_index() { + for index in [0, 1] { + let mut steps = Vec::new(); + if index == 1 { + steps.push( + Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step(), + ); + } + steps.push(Step::Error(EdgeError::bad_gateway_with_reason( + "original transport error", + BadGatewayReason::Unreachable, + ))); + let fixture = Fixture::new(steps); + let error = hop_error( + block_on( + fixture + .client + .follow(request(Method::GET), limit(4), allow_follow), + ), + index, + ); + let EdgeError::BadGateway { + reason, message, .. + } = error + else { + panic!("transport failure must not be reclassified"); + }; + assert_eq!(reason, BadGatewayReason::Unreachable); + assert_eq!(message, "original transport error"); + assert_eq!(fixture.sent(), index + 1); + } +} + +#[test] +fn policy_time_is_charged_to_original_deadline_before_follow_or_finalization() { + for stop in [false, true] { + let fixture = Fixture::new([Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step()]); + let clock = fixture.time.clone(); + let initial = request(Method::GET).deadline(Deadline::at_instant(fixture.time.at(10))); + let outcome = block_on(fixture.client.follow(initial, limit(4), move |_| { + clock.set(10); + Ok::<_, ()>(if stop { + OutboundRedirectAction::Stop + } else { + OutboundRedirectAction::FollowPrepared + }) + })); + assert_deadline(hop_error(outcome, usize::from(!stop))); + assert_eq!(fixture.sent(), 1); + } +} + +#[test] +fn expired_policy_rejection_is_not_replaced_with_a_deadline_error() { + let fixture = Fixture::new([Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step()]); + let clock = fixture.time.clone(); + let identity = Rc::new(Cell::new(19)); + let local = Rc::clone(&identity); + let initial = request(Method::GET).deadline(Deadline::at_instant(fixture.time.at(10))); + let outcome = block_on(fixture.client.follow(initial, limit(4), move |_| { + clock.set(11); + Err::(PolicyRejection { + identity: Rc::clone(&local), + }) + })); + match outcome { + Err(OutboundFollowFailure::Policy { + redirect_number: 1, + error, + }) => assert!(Rc::ptr_eq(&error.identity, &identity)), + _ => panic!("late synchronous policy rejection is still the original application error"), + } + assert_eq!(fixture.sent(), 1); +} + +#[test] +fn unsupported_next_options_win_before_the_expired_chain_budget() { + let fixture = Fixture::new([Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step()]); + let clock = fixture.time.clone(); + let initial = request(Method::GET).deadline(Deadline::at_instant(fixture.time.at(10))); + let outcome = block_on(fixture.client.follow(initial, limit(4), move |_| { + clock.set(10); + let next = request(Method::GET) + .header("x-fixture-unsupported", "yes") + .expect("header"); + Ok::<_, ()>(OutboundRedirectAction::Follow(next)) + })); + let error = hop_error(outcome, 1); + assert!( + matches!(error, EdgeError::BadRequest { message, .. } if message == "unsupported fixture option") + ); + assert_eq!(fixture.sent(), 1); + assert_eq!(fixture.state.borrow().validations.len(), 2); +} + +#[test] +fn replacement_deadlines_cannot_extend_the_original_final_stream_bound() { + for replacement_deadline in [None, Some(100), Some(20)] { + let probe = SourceProbe::default(); + let final_reply = Reply { + status: StatusCode::OK, + headers: HeaderMap::new(), + source: probe.source(UnreadBody::Pending), + }; + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step(), + final_reply.step(), + ]); + let clock = fixture.time.clone(); + let initial = request(Method::GET).deadline(Deadline::at_instant(fixture.time.at(50))); + let result = success(block_on(fixture.client.follow( + initial, + limit(4), + move |_| { + clock.set(10); + let mut replacement = request(Method::GET).stream_response(); + if let Some(millis) = replacement_deadline { + replacement = replacement.deadline(Deadline::at_instant(clock.at(millis))); + } + Ok::<_, ()>(OutboundRedirectAction::Follow(replacement)) + }, + ))); + assert!(result.response().body().is_stream()); + let expires = replacement_deadline.unwrap_or(50).min(50); + let state = fixture.state.borrow(); + assert_eq!( + state.records[1].budget.deadline.instant(), + fixture.time.at(expires) + ); + assert_eq!(state.records[1].budget.cause, BudgetSource::RequestDeadline); + drop(state); + assert_eq!(probe.polls.get(), 0); + fixture.time.set(expires); + assert_deadline( + block_on(result.into_parts().0.into_bytes_bounded(1)) + .expect_err("original bound survives final handoff"), + ); + assert_eq!(probe.drops.get(), 1); + assert_eq!(fixture.sent(), 2); + } +} + +#[test] +fn first_pending_hop_drop_prevents_a_second_dispatch() { + let dropped = Rc::new(Cell::new(0)); + let fixture = Fixture::new([ + Step::Pending(Rc::clone(&dropped)), + Reply::bytes(StatusCode::OK, b"must not send").step(), + ]); + let mut future = fixture + .client + .follow(request(Method::GET), limit(4), allow_follow); + assert!( + future + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert_eq!(fixture.sent(), 1); + drop(future); + assert_eq!(dropped.get(), 1); + assert_eq!(fixture.sent(), 1); + assert_eq!(fixture.state.borrow().steps.len(), 1); +} + +#[test] +fn dropping_inflight_second_hop_releases_it_and_prevents_a_third_dispatch() { + let dropped = Rc::new(Cell::new(0)); + let probe = SourceProbe::default(); + let fixture = Fixture::new([ + unread_reply(UnreadBody::Pending, &probe).step(), + Step::Pending(Rc::clone(&dropped)), + Reply::bytes(StatusCode::OK, b"must not send").step(), + ]); + let mut future = fixture + .client + .follow(request(Method::GET), limit(4), allow_follow); + assert!( + future + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert_eq!(fixture.sent(), 2); + assert_eq!(probe.polls.get(), 0); + assert_eq!(probe.drops.get(), 1); + assert_eq!(dropped.get(), 0); + drop(future); + assert_eq!(dropped.get(), 1); + assert_eq!(fixture.sent(), 2); + assert_eq!(fixture.state.borrow().steps.len(), 1); +} + +#[test] +fn dropping_unpolled_follow_releases_the_owned_request_without_transport_work() { + let fixture = Fixture::new([]); + let probe = SourceProbe::default(); + let initial = request(Method::POST).body(Body::from_stream(probe.source(UnreadBody::Pending))); + let future = fixture.client.follow(initial, limit(4), allow_follow); + assert_eq!(fixture.sent(), 0); + drop(future); + assert_eq!(probe.polls.get(), 0); + assert_eq!(probe.drops.get(), 1); + assert_eq!(fixture.sent(), 0); +} + +#[test] +fn plain_send_does_not_follow_a_scripted_redirect() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"redirect body") + .location("/next") + .step(), + Reply::bytes(StatusCode::OK, b"not sent").step(), + ]); + let response = block_on(fixture.client.send(request(Method::GET))).expect("plain send"); + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!( + block_on(response.into_bytes_bounded(32)).expect("redirect body"), + "redirect body" + ); + assert_eq!(fixture.sent(), 1); + assert_eq!( + fixture.state.borrow().records[0].mode, + OutboundSendMode::Send + ); +} + +#[test] +fn followed_hop_logs_omit_query_names_values_and_fragments() { + log::set_logger(&REDIRECT_LOGGER).expect("one logger for the core unit-test binary"); + log::set_max_level(log::LevelFilter::Trace); + REDIRECT_LOGS.with(|messages| messages.borrow_mut().clear()); + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("/middle?private-query=private-value#private-fragment") + .step(), + Reply::bytes(StatusCode::TEMPORARY_REDIRECT, b"") + .location("https://b.example/final?private-query=private-value") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + let initial = OutboundRequest::get("https://a.example/start?private-query=private-value") + .expect("request"); + success(block_on(fixture.client.follow( + initial, + limit(4), + allow_follow, + ))); + let messages = REDIRECT_LOGS.with(|messages| take(&mut *messages.borrow_mut())); + assert!( + !messages.is_empty(), + "query-safety proof must actually capture redirect logs" + ); + assert!(messages.iter().any(|message| message.contains("b.example"))); + for message in &messages { + assert!(!message.contains("private-query")); + assert!(!message.contains("private-value")); + assert!(!message.contains("private-fragment")); + } + assert_eq!(fixture.sent(), 3); + + let literal = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("https://b.example/final?ts-ec=abc") + .step(), + Reply::bytes(StatusCode::OK, b"").step(), + ]); + let initial = OutboundRequest::get("https://a.example/start?ts-ec=abc").expect("request"); + success(block_on(literal.client.follow( + initial, + limit(4), + allow_follow, + ))); + let messages = REDIRECT_LOGS.with(|messages| take(&mut *messages.borrow_mut())); + assert!( + !messages.is_empty(), + "literal two-hop proof must capture logs" + ); + for message in &messages { + assert!(!message.contains("ts-ec")); + assert!(!message.contains("abc")); + } + assert_eq!(literal.sent(), 2); +} + +#[test] +fn closing_callback_during_policy_prevents_the_next_dispatch() { + let fixture = Fixture::new([ + Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step(), + Reply::bytes(StatusCode::OK, b"must not send").step(), + ]); + let owner = Rc::new(OutboundScopeOwner::callback()); + let cancel = Rc::clone(&owner); + let future = owner.with(|| { + fixture + .client + .follow(request(Method::GET), limit(4), move |_| { + cancel.close(); + Ok::<_, ()>(OutboundRedirectAction::FollowPrepared) + }) + }); + let outcome = block_on(owner.run(future)); + assert_eq!( + fixture.sent(), + 1, + "policy cancellation must prevent provider dispatch, not just replace its result" + ); + assert!(matches!( + outcome, + Err(OutboundFollowFailure::Hop { + error: EdgeError::Internal { .. }, + .. + }) + )); + assert!(owner.scope().check().is_err()); +} + +#[test] +fn closing_callback_from_followed_source_drop_prevents_the_next_dispatch() { + let owner = Rc::new(OutboundScopeOwner::callback()); + let probe = SourceProbe::default(); + let on_drop = ScopeCloseOnDrop { + drops: Rc::clone(&probe.drops), + owner: Some(Rc::clone(&owner)), + }; + let polls = Rc::clone(&probe.polls); + let source = stream::poll_fn(move |_| { + let _retained = &on_drop; + polls.set(polls.get().saturating_add(1)); + Poll::>>::Pending + }) + .boxed_local(); + let first = Reply { + status: StatusCode::FOUND, + headers: HeaderMap::new(), + source, + } + .location("/next"); + let fixture = Fixture::new([ + first.step(), + Reply::bytes(StatusCode::OK, b"must not send").step(), + ]); + let future = owner.with(|| { + fixture + .client + .follow(request(Method::GET), limit(4), allow_follow) + }); + let outcome = block_on(owner.run(future)); + assert_eq!(probe.polls.get(), 0); + assert_eq!(probe.drops.get(), 1); + assert_eq!( + fixture.sent(), + 1, + "discard cleanup must be checked before the next provider dispatch" + ); + assert!(matches!( + outcome, + Err(OutboundFollowFailure::Hop { + error: EdgeError::Internal { .. }, + .. + }) + )); +} + +fn assert_empty_get_scope_cancellation(follow: bool, close_on_drop: bool) { + let owner = Rc::new(OutboundScopeOwner::callback()); + let probe = SourceProbe::default(); + let on_drop = ScopeCloseOnDrop { + drops: Rc::clone(&probe.drops), + owner: close_on_drop.then(|| Rc::clone(&owner)), + }; + let on_eof = (!close_on_drop).then(|| Rc::clone(&owner)); + let polls = Rc::clone(&probe.polls); + let body = Body::from_stream(stream::poll_fn(move |_| { + let _retained = &on_drop; + polls.set(polls.get().saturating_add(1)); + if let Some(cancel) = &on_eof { + cancel.close(); + } + Poll::Ready(None) + })); + let fixture = Fixture::new([Reply::bytes(StatusCode::OK, b"must not send").step()]); + let initial = request(Method::GET).body(body); + let error = if follow { + let future = owner.with(|| fixture.client.follow(initial, limit(4), allow_follow)); + match block_on(owner.run(future)) { + Err(OutboundFollowFailure::Hop { error, .. }) => error, + Err(OutboundFollowFailure::Policy { .. }) | Ok(_) => { + panic!("settlement cancellation must fail before dispatch") + } + } + } else { + let future = owner.with(|| fixture.client.send(initial)); + block_on(owner.run(future)).expect_err("settlement cancellation") + }; + assert_eq!(probe.polls.get(), 1); + assert_eq!(probe.drops.get(), 1); + assert_eq!( + fixture.sent(), + 0, + "empty-body settlement cleanup must prevent any provider dispatch" + ); + assert!(matches!(error, EdgeError::Internal { .. })); + assert!(owner.scope().check().is_err()); +} + +#[test] +fn send_empty_get_scope_closed_from_eof_never_dispatches() { + assert_empty_get_scope_cancellation(false, false); +} + +#[test] +fn send_empty_get_scope_closed_from_source_drop_never_dispatches() { + assert_empty_get_scope_cancellation(false, true); +} + +#[test] +fn follow_empty_get_scope_closed_from_eof_never_dispatches() { + assert_empty_get_scope_cancellation(true, false); +} + +#[test] +fn follow_empty_get_scope_closed_from_source_drop_never_dispatches() { + assert_empty_get_scope_cancellation(true, true); +} + +#[test] +fn policy_stop_clock_rewind_after_observed_head_is_internal() { + let fixture = Fixture::new([Reply::bytes(StatusCode::FOUND, b"stopped") + .location("/next") + .step()]); + let clock = fixture.time.clone(); + let future = fixture + .client + .follow(request(Method::GET), limit(4), move |_| { + clock.set(5); + Ok::<_, ()>(OutboundRedirectAction::Stop) + }); + fixture.time.set(10); + let outcome = block_on(future); + assert_eq!(fixture.sent(), 1); + assert!( + matches!( + outcome, + Err(OutboundFollowFailure::Hop { + index: 0, + error: EdgeError::Internal { .. } + }) + ), + "finalization must not reset the observed head time from 10 to entry time 0" + ); +} + +#[test] +fn policy_error_survives_a_clock_rewind_after_observed_head() { + let fixture = Fixture::new([Reply::bytes(StatusCode::FOUND, b"") + .location("/next") + .step()]); + let clock = fixture.time.clone(); + let identity = Rc::new(Cell::new(7_usize)); + let local = Rc::clone(&identity); + let future = fixture + .client + .follow(request(Method::GET), limit(4), move |_| { + clock.set(5); + Err::(PolicyRejection { + identity: Rc::clone(&local), + }) + }); + fixture.time.set(10); + match block_on(future) { + Err(OutboundFollowFailure::Policy { + redirect_number: 1, + error, + }) => assert!(Rc::ptr_eq(&identity, &error.identity)), + Err(OutboundFollowFailure::Policy { .. } | OutboundFollowFailure::Hop { .. }) | Ok(_) => { + panic!("policy rejection must remain the original application value") + } + } + assert_eq!(fixture.sent(), 1); +} diff --git a/crates/edgezero-core/src/outbound/scope.rs b/crates/edgezero-core/src/outbound/scope.rs new file mode 100644 index 000000000..91bfd0888 --- /dev/null +++ b/crates/edgezero-core/src/outbound/scope.rs @@ -0,0 +1,1870 @@ +use std::any::{Any, TypeId}; +use std::cell::{Cell, RefCell}; +use std::collections::{BTreeMap, HashMap}; +use std::convert::identity; +use std::fmt; +use std::future::Future; +use std::io; +use std::mem; +use std::pin::Pin; +use std::rc::{Rc, Weak}; +use std::task::{Context, Poll, Waker}; + +use futures_util::future::{LocalBoxFuture, poll_fn, ready}; + +use super::{OutboundFuture, OutboundTime}; +use crate::error::EdgeError; +use crate::http::Response; +use crate::time::MonotonicInstant; + +std::thread_local! { + static CURRENT: RefCell> = const { RefCell::new(None) }; +} + +/// The canonical lifecycle boundary that can revoke an outbound operation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutboundScopeKind { + Callback, + ResponseBody, +} + +/// Local provider progress and activation around exactly one inner poll. +/// +/// A runtime must invoke `inner` once and preserve its readiness unless returning an error. +/// Provider setup may fail before polling `inner`. Activation must be restored before return; +/// no provider or thread-local activation guard may survive a Pending result. +pub trait OutboundProgressRuntime { + fn poll( + &self, + scope: &OutboundScope, + time: &OutboundTime, + entered_at: MonotonicInstant, + cx: &mut Context<'_>, + inner: &mut dyn FnMut(&mut Context<'_>) -> Poll<()>, + ) -> Poll>; +} + +/// Progress for providers whose owned futures are driven by their existing local executor. +#[derive(Debug)] +pub struct IndependentOutboundProgress; + +/// Unique local authority to close a callback or response-body lifetime. +/// Holding a captured scope never keeps this owner alive or delays its closure. +pub struct OutboundScopeOwner { + provisional: RefCell>>, + state: Rc, +} + +/// Invocation-bound local scope and provider runtime, independent of the retained client. +/// +/// Canonical captures weakly reference their owner. An explicitly unmanaged capture instead +/// owns its local state until the last invocation token is dropped; it claims no callback TTL. +#[derive(Clone)] +pub struct OutboundScope { + current: Current, + owned: Option>, + progress: Rc, +} + +#[derive(Clone)] +struct Current { + kind: OutboundScopeKind, + managed: bool, + state: Weak, +} + +struct State { + closed: Cell, + kind: OutboundScopeKind, + next_id: Cell, + providers: RefCell>, + provisional_parent: RefCell>, + roots: RefCell>>, +} + +type ProviderCache = Rc>>>; + +struct Provisional { + armed: bool, + child: Weak, +} + +trait Root { + fn drain(&self) -> Cleanup; + fn mark_revoked(&self); +} + +struct Cleanup { + payload: Option>, + waiting: Option, +} + +struct Node { + active: Cell, + revoked: Cell, + value: RefCell>, + waker: RefCell>, +} + +/// A weakly registered owned payload. Its value is never polled or dropped under a borrow. +pub(crate) struct Revocable { + node: Rc>, + registration: Cell>, + scope: OutboundScope, +} + +struct RestoreCurrent(Option); + +struct RestoreActive<'value>(&'value Cell); + +#[derive(Debug, thiserror::Error)] +#[error("outbound {kind:?} scope closed")] +struct ScopeClosed { + kind: OutboundScopeKind, +} + +impl OutboundProgressRuntime for IndependentOutboundProgress { + #[inline] + fn poll( + &self, + _scope: &OutboundScope, + _time: &OutboundTime, + _entered_at: MonotonicInstant, + cx: &mut Context<'_>, + inner: &mut dyn FnMut(&mut Context<'_>) -> Poll<()>, + ) -> Poll> { + inner(cx).map(Ok) + } +} + +impl OutboundScopeOwner { + pub(crate) fn accept_body_handoff(&self) -> Result<(), EdgeError> { + if self.state.kind != OutboundScopeKind::ResponseBody { + return Err(invariant( + "only a response-body outbound scope owner can be handed off", + )); + } + self.scope().check()?; + let registration = self.provisional.borrow_mut().take(); + if let Some(mut lease) = registration { + let mut guard = lease.take()?; + guard.armed = false; + let dependency = self.state.provisional_parent.borrow_mut().take(); + drop(dependency); + } + self.scope().check() + } + + /// Transfers this exact body lifetime into response-body metadata before any body poll. + /// Dropping an unhanded response closes its precreated outbound work. + /// + /// # Errors + /// Returns Internal for a callback owner, a closed owner, or an already attached body. + #[inline] + pub fn attach(self, mut response: Response) -> Result { + if self.state.kind != OutboundScopeKind::ResponseBody { + return Err(invariant( + "only a response-body outbound scope owner can be attached", + )); + } + self.scope().check()?; + let body = mem::take(response.body_mut()).attach_egress_owner(self)?; + *response.body_mut() = body; + Ok(response) + } + + /// Creates one canonical serving-callback owner without activating it. + #[must_use] + #[inline] + pub fn callback() -> Self { + let ambient = CURRENT.with(|current| current.borrow().clone()); + let state = State::new(OutboundScopeKind::Callback, None); + state.closed.set(current_closed(ambient.as_ref())); + Self { + provisional: RefCell::new(None), + state: Rc::new(state), + } + } + + /// Creates a callback owner only when canonical helper composition has no managed owner. + /// A closed managed ambient scope is reused as closed, never replaced by a fresh lifetime. + #[must_use] + #[inline] + pub fn callback_if_absent() -> Option { + let managed = CURRENT.with(|current| { + current + .borrow() + .as_ref() + .is_some_and(|ambient| ambient.managed) + }); + if managed { + None + } else { + Some(Self::callback()) + } + } + + /// Revokes every root before running any payload cleanup or notification. + #[inline] + pub fn close(&self) { + self.with(|| { + let provisional = self.provisional.borrow_mut().take(); + self.state.close(); + drop(provisional); + }); + } + + /// Creates an independently cancellable body owner sharing its active provider family. + /// A family lease keeps neither the callback owner nor its cancellation roots alive. + /// Until framework handoff, a managed parent also revokes this provisional body lifetime. + /// A closed ambient owner cannot be escaped by opening a response-body owner. + #[must_use] + #[inline] + pub fn response_body() -> Self { + let parent = CURRENT.with(|current| current.borrow().clone()); + let closed = current_closed(parent.as_ref()); + let state = Rc::new(State::new(OutboundScopeKind::ResponseBody, parent.clone())); + state.closed.set(closed); + let provisional = parent + .filter(|current| current.managed) + .and_then(|current| { + let scope = OutboundScope { + current, + owned: None, + progress: Rc::new(IndependentOutboundProgress), + }; + Revocable::new( + &scope, + Provisional { + armed: true, + child: Rc::downgrade(&state), + }, + ) + .ok() + }); + Self { + provisional: RefCell::new(provisional), + state, + } + } + + /// Activates this owner only while each poll executes; the returned future borrows no owner. + #[inline] + pub fn run<'future, F>(&self, future: F) -> LocalBoxFuture<'future, F::Output> + where + F: Future + 'future, + F::Output: 'future, + { + let current = self.scope().current; + let mut pinned = Box::pin(future); + Box::pin(poll_fn(move |cx| { + with_current(current.clone(), || pinned.as_mut().poll(cx)) + })) + } + + /// Returns a weak capability with independent progress for synchronous activation. + #[must_use] + #[inline] + pub fn scope(&self) -> OutboundScope { + OutboundScope { + current: Current { + kind: self.state.kind, + managed: true, + state: Rc::downgrade(&self.state), + }, + owned: None, + progress: Rc::new(IndependentOutboundProgress), + } + } + + /// Activates this canonical owner for one synchronous call, restoring the prior scope. + #[expect( + clippy::impl_trait_in_params, + reason = "Keep only the return type as a public generic parameter" + )] + #[inline] + pub fn with(&self, action: impl FnOnce() -> R) -> R { + with_current(self.scope().current, action) + } +} + +impl fmt::Debug for OutboundScopeOwner { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutboundScopeOwner") + .field("closed", &self.state.is_closed()) + .field("kind", &self.state.kind) + .finish_non_exhaustive() + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for OutboundScopeOwner { + #[inline] + fn drop(&mut self) { + self.close(); + } +} + +impl OutboundScope { + /// Captures the current owner once, replacing only the provider runtime, never ownership. + /// With no canonical owner, this creates an explicitly caller-owned manual lifetime. + #[must_use] + #[inline] + pub fn capture(progress: Rc) -> Self { + let ambient = CURRENT.with(|current| current.borrow().clone()); + if let Some(current) = ambient { + let owned = if current.managed { + None + } else { + current.state.upgrade() + }; + Self { + current, + owned, + progress, + } + } else { + let state = Rc::new(State::new(OutboundScopeKind::Callback, None)); + Self { + current: Current { + kind: state.kind, + managed: false, + state: Rc::downgrade(&state), + }, + owned: Some(state), + progress, + } + } + } + + /// Checks the captured owner without consulting or replacing the ambient request. + /// + /// # Errors + /// Returns typed Internal with the original owner kind after closure or owner destruction. + #[inline] + pub fn check(&self) -> Result<(), EdgeError> { + if self.state().is_some_and(|state| !state.is_closed()) { + Ok(()) + } else { + Err(self.closed_error()) + } + } + + fn closed_error(&self) -> EdgeError { + EdgeError::internal(ScopeClosed { + kind: self.current.kind, + }) + } + + #[must_use] + #[inline] + pub fn is_managed(&self) -> bool { + self.current.managed + } + + #[must_use] + #[inline] + pub fn kind(&self) -> Option { + self.current.managed.then_some(self.current.kind) + } + + /// Activates the captured scope before the provider runtime and the inner future are polled. + /// + /// # Errors + /// Preserves provider failures, rejects closed owners, and reports invalid runtime polling. + #[inline] + pub fn poll( + &self, + time: &OutboundTime, + entered_at: MonotonicInstant, + cx: &mut Context<'_>, + mut future: Pin<&mut F>, + ) -> Poll> + where + F: Future + ?Sized, + { + let mut called = false; + let mut multiple = false; + let mut output = None; + let observation = self.with(|| { + self.progress + .poll(self, time, entered_at, cx, &mut |inner_cx| { + if called { + multiple = true; + return Poll::Ready(()); + } + called = true; + match future.as_mut().poll(inner_cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(value) => { + output = Some(value); + Poll::Ready(()) + } + } + }) + }); + if multiple { + return Poll::Ready(Err(invariant( + "outbound progress runtime polled an operation more than once", + ))); + } + match observation { + Err(error) | Ok(Poll::Ready(Err(error))) => Poll::Ready(Err(error)), + Ok(Poll::Pending) if called && output.is_none() => Poll::Pending, + Ok(Poll::Ready(Ok(()))) if called => match output { + Some(value) => Poll::Ready(Ok(value)), + None => Poll::Ready(Err(invariant( + "outbound progress runtime completed a pending operation", + ))), + }, + _ => Poll::Ready(Err(invariant( + "outbound progress runtime did not preserve operation readiness", + ))), + } + } + + pub(super) fn progress(&self) -> Rc { + Rc::clone(&self.progress) + } + + /// Resolves typed provider state in the shared callback and response-body family cache. + /// Initializers and displaced state are never run or dropped under a registry borrow. + /// + /// # Errors + /// Returns typed Internal if initialization races owner closure or registration is invalid. + #[expect( + clippy::impl_trait_in_params, + reason = "Keep the state type as the only public generic parameter" + )] + #[inline] + pub fn provider_state(&self, init: impl FnOnce() -> T) -> Result, EdgeError> + where + T: 'static, + { + self.check()?; + let state = self.state().ok_or_else(|| self.closed_error())?; + let family = state + .providers + .borrow() + .as_ref() + .map(Rc::clone) + .ok_or_else(|| self.closed_error())?; + let id = TypeId::of::(); + let inherited = { family.borrow().get(&id).cloned() }; + let proposed: Rc = inherited.unwrap_or_else(|| Rc::new(init())); + self.check()?; + let retained = { + let mut providers = family.borrow_mut(); + Rc::clone(providers.entry(id).or_insert_with(|| Rc::clone(&proposed))) + }; + drop(proposed); + self.check()?; + retained + .downcast::() + .map_err(|_wrong_type| invariant("outbound provider state has an inconsistent type")) + } + + /// Runs an already-owned provider task with scope/progress activation, without registering + /// another cancellation root. Pending handles and batch producers own that registration. + #[must_use] + #[inline] + pub fn run( + &self, + time: OutboundTime, + entered_at: MonotonicInstant, + future: OutboundFuture, + ) -> OutboundFuture + where + T: 'static, + { + let mut active = Some((self.clone(), time, future)); + Box::pin(poll_fn(move |cx| { + let result = if let Some((scope, paired, pinned)) = &mut active { + scope + .poll(paired, entered_at, cx, pinned.as_mut()) + .map(|outer| outer.and_then(identity)) + } else { + Poll::Ready(Err(invariant( + "outbound scoped task polled after completion", + ))) + }; + if result.is_ready() { + drop(active.take()); + } + result + })) + } + + fn state(&self) -> Option> { + self.owned + .as_ref() + .map(Rc::clone) + .or_else(|| self.current.state.upgrade()) + } + + /// Activates this captured capability for one synchronous operation. + /// + /// # Errors + /// Returns typed Internal before executing the action, or after reentrant owner closure. + #[expect( + clippy::impl_trait_in_params, + reason = "Keep only the return type as a public generic parameter" + )] + #[inline] + pub fn with(&self, action: impl FnOnce() -> R) -> Result { + self.check()?; + let output = self.with_cleanup(action); + self.check()?; + Ok(output) + } + + pub(crate) fn with_cleanup(&self, action: A) -> R + where + A: FnOnce() -> R, + { + with_current(self.current.clone(), action) + } +} + +impl fmt::Debug for OutboundScope { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutboundScope") + .field("kind", &self.kind()) + .field("managed", &self.is_managed()) + .finish_non_exhaustive() + } +} + +impl State { + fn close(&self) { + if self.closed.replace(true) { + return; + } + let roots: Vec<_> = mem::take(&mut *self.roots.borrow_mut()) + .into_values() + .filter_map(|root| root.upgrade()) + .collect(); + let providers = self.providers.borrow_mut().take(); + // No teardown or notification may run until every sibling is visibly revoked. + for root in &roots { + root.mark_revoked(); + } + // Detach every payload before user code runs, including when one destructor unwinds. + let cleanups: Vec<_> = roots.iter().map(|root| root.drain()).collect(); + for cleanup in cleanups { + cleanup.release(); + } + drop(providers); + } + + fn is_closed(&self) -> bool { + let dependency = self.provisional_parent.borrow().clone(); + self.closed.get() || current_closed(dependency.as_ref()) + } + + fn new(kind: OutboundScopeKind, parent: Option) -> Self { + let providers = parent + .as_ref() + .and_then(|captured| captured.state.upgrade()) + .filter(|active| !active.is_closed()) + .and_then(|active| active.providers.borrow().as_ref().map(Rc::clone)) + .unwrap_or_else(|| Rc::new(RefCell::new(HashMap::new()))); + Self { + closed: Cell::new(false), + kind, + next_id: Cell::new(0), + providers: RefCell::new(Some(providers)), + provisional_parent: RefCell::new(parent.filter(|current| current.managed)), + roots: RefCell::new(BTreeMap::new()), + } + } +} + +impl Revocable { + pub(crate) fn is_revoked(&self) -> bool { + self.node.revoked.get() || self.scope.state().is_none_or(|state| state.is_closed()) + } + + pub(crate) fn new(scope: &OutboundScope, value: T) -> Result { + scope.with_cleanup(|| { + scope.check()?; + let state = scope.state().ok_or_else(|| scope.closed_error())?; + let id = state.next_id.get(); + state.next_id.set( + id.checked_add(1) + .ok_or_else(|| invariant("outbound scope registration exhausted"))?, + ); + let node = Rc::new(Node { + active: Cell::new(false), + revoked: Cell::new(false), + value: RefCell::new(Some(value)), + waker: RefCell::new(None), + }); + let erased: Rc = Rc::>::clone(&node); + state.roots.borrow_mut().insert(id, Rc::downgrade(&erased)); + Ok(Self { + node, + registration: Cell::new(Some(id)), + scope: scope.clone(), + }) + }) + } + + pub(crate) fn revoke(&self) { + self.scope.with_cleanup(|| { + self.unregister(); + self.node.mark_revoked(); + self.node.drain().release(); + }); + } + + pub(crate) fn take(&mut self) -> Result { + self.scope.with_cleanup(|| { + self.scope.check()?; + if self.node.revoked.get() { + return Err(self.scope.closed_error()); + } + let value = self + .node + .value + .borrow_mut() + .take() + .ok_or_else(|| invariant("outbound scoped value already consumed or in use"))?; + self.unregister(); + let previous = self.node.waker.borrow_mut().take(); + drop(previous); + self.scope.check()?; + Ok(value) + }) + } + + fn unregister(&self) { + if let Some(id) = self.registration.take() + && let Some(state) = self.scope.state() + { + state.roots.borrow_mut().remove(&id); + } + } + + pub(crate) fn with(&self, cx: &mut Context<'_>, action: A) -> Result + where + A: FnOnce(&mut T, &mut Context<'_>) -> R, + { + // Keep activation through value/output destruction on early return or unwind. + self.scope.with_cleanup(|| { + self.scope.check()?; + if self.node.revoked.get() { + return Err(self.scope.closed_error()); + } + if self.node.active.replace(true) { + return Err(invariant("outbound scoped value is already in use")); + } + let active = RestoreActive(&self.node.active); + let mut value = self + .node + .value + .borrow_mut() + .take() + .ok_or_else(|| invariant("outbound scoped value already consumed"))?; + let replacement = cx.waker().clone(); + self.scope.check()?; + let previous = self.node.waker.borrow_mut().replace(replacement); + drop(previous); + self.scope.check()?; + if self.node.revoked.get() { + return Err(self.scope.closed_error()); + } + let output = action(&mut value, cx); + if self.is_revoked() { + return Err(self.scope.closed_error()); + } + let displaced = self.node.value.borrow_mut().replace(value); + drop(displaced); + drop(active); + Ok(output) + }) + } +} + +impl fmt::Debug for Revocable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Revocable") + .field("revoked", &self.is_revoked()) + .finish_non_exhaustive() + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for Revocable { + fn drop(&mut self) { + self.revoke(); + } +} + +impl Cleanup { + fn release(mut self) { + drop(self.payload.take()); + if let Some(waiting) = self.waiting.take() { + waiting.wake(); + } + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for Provisional { + fn drop(&mut self) { + if self.armed + && let Some(state) = self.child.upgrade() + { + let current = Current { + kind: state.kind, + managed: true, + state: Rc::downgrade(&state), + }; + with_current(current, || state.close()); + } + } +} + +impl Root for Node { + fn drain(&self) -> Cleanup { + let value = self.value.borrow_mut().take(); + let waiting = self.waker.borrow_mut().take(); + Cleanup { + payload: value.map(|payload| -> Box { Box::new(payload) }), + waiting, + } + } + + fn mark_revoked(&self) { + self.revoked.set(true); + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for RestoreCurrent { + fn drop(&mut self) { + CURRENT.with(|current| current.replace(self.0.take())); + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for RestoreActive<'_> { + fn drop(&mut self) { + self.0.set(false); + } +} + +pub(super) fn bind_future( + scope: OutboundScope, + time: OutboundTime, + entered_at: MonotonicInstant, + future: OutboundFuture, +) -> OutboundFuture +where + T: 'static, +{ + let registered = match Revocable::new(&scope, future) { + Ok(value) => value, + Err(error) => return Box::pin(ready(Err(error))), + }; + let mut active = Some((registered, scope, time)); + Box::pin(poll_fn(move |cx| { + let result = if let Some((value, captured, paired)) = &active { + match value.with(cx, |pinned, inner_cx| { + captured.poll(paired, entered_at, inner_cx, pinned.as_mut()) + }) { + Err(error) => Poll::Ready(Err(error)), + Ok(observed) => observed.map(|outer| outer.and_then(identity)), + } + } else { + Poll::Ready(Err(invariant( + "outbound scoped future polled after completion", + ))) + }; + if result.is_ready() { + drop(active.take()); + } + result + })) +} + +fn current_closed(current: Option<&Current>) -> bool { + current.is_some_and(|captured| { + captured + .state + .upgrade() + .is_none_or(|state| state.is_closed()) + }) +} + +fn invariant(message: &'static str) -> EdgeError { + EdgeError::internal(io::Error::other(message)) +} + +fn with_current(current: Current, action: A) -> R +where + A: FnOnce() -> R, +{ + let previous = CURRENT.with(|active| active.replace(Some(current))); + let restore = RestoreCurrent(previous); + let result = action(); + drop(restore); + result +} + +#[cfg(test)] +mod tests { + use std::cell::{Cell, RefCell}; + use std::panic::{AssertUnwindSafe, catch_unwind}; + use std::rc::{Rc, Weak}; + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::task::{Context, Poll, Waker}; + use std::time::Duration; + + use futures::executor::block_on; + use futures_util::future::{pending, poll_fn}; + use futures_util::task::{ArcWake, waker}; + + use super::{ + IndependentOutboundProgress, OutboundProgressRuntime, OutboundScope, OutboundScopeOwner, + Revocable, bind_future, + }; + use crate::body::Body; + use crate::error::EdgeError; + use crate::http::Response; + use crate::outbound::{OutboundFuture, OutboundTime}; + use crate::time::{MonotonicClock, MonotonicInstant}; + + struct Release(Rc>); + + struct DropAction(Option>); + + struct WakeAction; + + struct Runtime { + active: Arc, + polls: Rc>, + } + + struct InvalidRuntime(u8); + + #[derive(Debug, thiserror::Error)] + #[error("original scoped timer fault")] + struct TimerFault(Arc<()>); + + std::thread_local! { + static DROP_ACTION: RefCell>> = const { RefCell::new(None) }; + static WAKE_ACTION: RefCell>> = const { RefCell::new(None) }; + } + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for DropAction { + fn drop(&mut self) { + if let Some(action) = self.0.take() { + action(); + } + } + } + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for WakeAction { + fn drop(&mut self) { + let action = DROP_ACTION.with(|stored| stored.borrow_mut().take()); + if let Some(callback) = action { + callback(); + } + } + } + + impl ArcWake for WakeAction { + fn wake(self: Arc) { + Self::wake_by_ref(&self); + } + fn wake_by_ref(_self: &Arc) { + let action = WAKE_ACTION.with(|stored| stored.borrow_mut().take()); + if let Some(callback) = action { + callback(); + } + } + } + + impl OutboundProgressRuntime for Runtime { + fn poll( + &self, + _: &OutboundScope, + _: &OutboundTime, + _: MonotonicInstant, + cx: &mut Context<'_>, + inner: &mut dyn FnMut(&mut Context<'_>) -> Poll<()>, + ) -> Poll> { + self.polls.set(self.polls.get().saturating_add(1)); + let previous = self.active.swap(true, Ordering::SeqCst); + let active = Arc::clone(&self.active); + let restore = DropAction(Some(Box::new(move || { + active.store(previous, Ordering::SeqCst); + }))); + let result = inner(cx); + drop(restore); + result.map(Ok) + } + } + + impl OutboundProgressRuntime for InvalidRuntime { + fn poll( + &self, + _: &OutboundScope, + _: &OutboundTime, + _: MonotonicInstant, + cx: &mut Context<'_>, + inner: &mut dyn FnMut(&mut Context<'_>) -> Poll<()>, + ) -> Poll> { + match self.0 { + 0 => { + let _first = inner(cx); + inner(cx).map(Ok) + } + 1 => Poll::Ready(Ok(())), + _ => { + let _first = inner(cx); + Poll::Pending + } + } + } + } + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for Release { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + fn capture() -> OutboundScope { + OutboundScope::capture(Rc::new(IndependentOutboundProgress)) + } + + fn time() -> (MonotonicInstant, OutboundTime) { + let now = MonotonicInstant::now(); + ( + now, + OutboundTime::new(MonotonicClock::new(move || now), |_, _| Box::pin(pending())), + ) + } + + #[test] + fn closing_owner_releases_unpolled_owned_future() { + let owner = OutboundScopeOwner::callback(); + let scope = owner.with(capture); + let releases = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&releases)); + let (entered, paired) = time(); + let source: OutboundFuture<()> = Box::pin(async move { + let _lease = lease; + pending().await + }); + let retained = bind_future(scope, paired, entered, source); + owner.close(); + assert_eq!( + releases.get(), + 1, + "close must not wait for polling or handle drop" + ); + assert!(matches!( + block_on(retained), + Err(EdgeError::Internal { .. }) + )); + assert_eq!(releases.get(), 1); + } + + #[test] + fn revocable_with_observes_reentrant_close_and_drops_its_output() { + let owner = OutboundScopeOwner::callback(); + let scope = owner.with(capture); + let input_releases = Rc::new(Cell::new(0_usize)); + let output_releases = Rc::new(Cell::new(0_usize)); + let value = Revocable::new(&scope, Release(Rc::clone(&input_releases))).expect("register"); + let result = value.with(&mut Context::from_waker(Waker::noop()), |_, _| { + owner.close(); + Release(Rc::clone(&output_releases)) + }); + assert!(matches!(result, Err(EdgeError::Internal { .. }))); + assert!(value.is_revoked()); + assert_eq!(input_releases.get(), 1); + assert_eq!(output_releases.get(), 1); + } + + #[test] + fn revocable_take_unregisters_without_revoking_transferred_value() { + let owner = OutboundScopeOwner::callback(); + let scope = owner.with(capture); + let releases = Rc::new(Cell::new(0_usize)); + let mut value = Revocable::new(&scope, Release(Rc::clone(&releases))).expect("register"); + let transferred = value.take().expect("take"); + owner.close(); + assert_eq!(releases.get(), 0); + drop(value); + assert_eq!(releases.get(), 0); + drop(transferred); + assert_eq!(releases.get(), 1); + } + + #[test] + fn scope_capability_does_not_keep_owner_open_or_resurrect_it() { + let owner = OutboundScopeOwner::callback(); + let scope = owner.with(capture); + assert!(scope.is_managed()); + drop(owner); + assert!(matches!(scope.check(), Err(EdgeError::Internal { .. }))); + let mut source = Box::pin(async { panic!("closed scope cannot poll source") }); + let (entered, paired) = time(); + assert!(matches!( + scope.poll( + &paired, + entered, + &mut Context::from_waker(Waker::noop()), + source.as_mut() + ), + Poll::Ready(Err(EdgeError::Internal { .. })) + )); + } + + #[test] + fn caller_owned_scope_releases_on_ordinary_future_drop() { + let scope = capture(); + assert!(!scope.is_managed()); + let releases = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&releases)); + let (entered, paired) = time(); + let retained = bind_future( + scope, + paired, + entered, + Box::pin(async move { + let _lease = lease; + pending::>().await + }), + ); + assert_eq!(releases.get(), 0); + drop(retained); + assert_eq!(releases.get(), 1); + } + + #[test] + fn response_body_shares_provider_state_but_not_callback_cancellation() { + let callback = OutboundScopeOwner::callback(); + let callback_scope = callback.with(capture); + let original = callback_scope + .provider_state(|| 17_u8) + .expect("callback provider state"); + let body = callback.with(OutboundScopeOwner::response_body); + let body_scope = body.with(capture); + let inherited = body_scope + .provider_state(|| 99_u8) + .expect("inherited provider state"); + assert!(Rc::ptr_eq(&original, &inherited)); + let mut value = Revocable::new(&body_scope, 3_u8).expect("body registration"); + body.accept_body_handoff().expect("accepted body handoff"); + callback.close(); + body_scope.check().expect("body lifetime remains open"); + assert_eq!(value.take().expect("independent body root"), 3); + body.close(); + assert!(matches!( + body_scope.check(), + Err(EdgeError::Internal { .. }) + )); + } + + #[test] + fn owner_run_restores_current_scope_between_polls() { + let owner = OutboundScopeOwner::callback(); + let mut polled = false; + let mut wrapped = Box::pin(owner.run(poll_fn(move |cx| { + assert!(capture().is_managed()); + if polled { + Poll::Ready(7_u8) + } else { + polled = true; + cx.waker().wake_by_ref(); + Poll::Pending + } + }))); + let mut cx = Context::from_waker(Waker::noop()); + assert!(wrapped.as_mut().poll(&mut cx).is_pending()); + assert!(!capture().is_managed()); + assert_eq!(wrapped.as_mut().poll(&mut cx), Poll::Ready(7)); + assert!(!capture().is_managed()); + } + + #[test] + fn nested_owner_cannot_resurrect_a_closed_ambient_lifetime() { + let owner = OutboundScopeOwner::callback(); + owner.with(|| { + owner.close(); + let nested = OutboundScopeOwner::callback(); + assert!(matches!( + nested.scope().check(), + Err(EdgeError::Internal { .. }) + )); + let body = OutboundScopeOwner::response_body(); + assert!(matches!( + body.scope().check(), + Err(EdgeError::Internal { .. }) + )); + }); + let fresh = OutboundScopeOwner::callback(); + fresh.scope().check().expect("next request is independent"); + } + + #[test] + fn provider_state_rechecks_closure_after_displaced_initializer_drops() { + let owner = Rc::new(OutboundScopeOwner::callback()); + let scope = owner.with(capture); + let closer = Rc::clone(&owner); + let result = scope.provider_state(|| { + drop( + scope + .provider_state(|| DropAction(None)) + .expect("reentrant initialization"), + ); + DropAction(Some(Box::new(move || closer.close()))) + }); + assert!(matches!(result, Err(EdgeError::Internal { .. }))); + } + + #[test] + fn every_root_is_marked_before_payload_provider_or_waker_cleanup() { + let owner = Rc::new(OutboundScopeOwner::callback()); + let scope = owner.with(capture); + let count = Rc::new(Cell::new(0_usize)); + let checks = Rc::new(RefCell::new(Vec:: bool>>::new())); + let inspect = || { + let inspect_checks = Rc::clone(&checks); + let inspect_count = Rc::clone(&count); + DropAction(Some(Box::new(move || { + assert!(inspect_checks.borrow().iter().all(|check| check())); + inspect_count.set(inspect_count.get().saturating_add(1)); + }))) + }; + let first = Rc::new(Revocable::new(&scope, inspect()).expect("first")); + let second = Rc::new(Revocable::new(&scope, inspect()).expect("second")); + for value in [&first, &second] { + let weak = Rc::downgrade(value); + checks.borrow_mut().push(Box::new(move || { + weak.upgrade().expect("retained root").node.revoked.get() + })); + } + drop(scope.provider_state(inspect).expect("provider")); + let waiting = waker(Arc::new(WakeAction)); + first + .with(&mut Context::from_waker(&waiting), |_, _| ()) + .expect("register wake"); + let closer = Rc::clone(&owner); + let first_weak = Rc::downgrade(&first); + let second_weak = Rc::downgrade(&second); + WAKE_ACTION.with(|stored| { + stored.replace(Some(Box::new(move || { + assert!( + first_weak + .upgrade() + .expect("first retained") + .node + .revoked + .get() + ); + assert!( + second_weak + .upgrade() + .expect("second retained") + .node + .revoked + .get() + ); + closer.close(); + }))) + }); + owner.with(|| owner.close()); + assert_eq!(count.get(), 3); + assert!( + scope + .state() + .expect("retained owner") + .roots + .borrow() + .is_empty() + ); + drop((first, second, waiting)); + assert_eq!(count.get(), 3); + } + + #[test] + fn dropping_replaced_waker_can_close_owner_without_borrow_panics() { + let owner = Rc::new(OutboundScopeOwner::callback()); + let scope = owner.with(capture); + let count = Rc::new(Cell::new(0_usize)); + let value = Revocable::new(&scope, Release(Rc::clone(&count))).expect("register"); + let waiting = waker(Arc::new(WakeAction)); + value + .with(&mut Context::from_waker(&waiting), |_, _| ()) + .expect("register wake"); + drop(waiting); + let closer = Rc::clone(&owner); + DROP_ACTION.with(|stored| stored.replace(Some(Box::new(move || closer.close())))); + let result = value.with(&mut Context::from_waker(Waker::noop()), |_, _| { + panic!("revoked before action") + }); + assert!(matches!(result, Err(EdgeError::Internal { .. }))); + assert_eq!(count.get(), 1); + } + + #[test] + fn failed_registration_drops_its_owned_input() { + let owner = OutboundScopeOwner::callback(); + let scope = owner.with(capture); + owner.close(); + let count = Rc::new(Cell::new(0_usize)); + let result = Revocable::new(&scope, Release(Rc::clone(&count))); + assert!(matches!(result, Err(EdgeError::Internal { .. }))); + assert_eq!(count.get(), 1); + } + + #[test] + fn closing_from_foreign_scope_does_not_reparent_cleanup_work() { + let owner = OutboundScopeOwner::callback(); + let foreign = OutboundScopeOwner::callback(); + let value = Revocable::new( + &owner.scope(), + DropAction(Some(Box::new(|| { + assert!(matches!(capture().check(), Err(EdgeError::Internal { .. }))); + }))), + ) + .expect("register cleanup"); + foreign.with(|| { + owner.close(); + capture() + .check() + .expect("foreign scope restored after cleanup"); + }); + drop(value); + } + + #[test] + fn completed_bound_future_releases_payload_and_registration_before_ready() { + let owner = OutboundScopeOwner::callback(); + let scope = owner.with(capture); + let count = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&count)); + let (entered, paired) = time(); + let source = Box::pin(poll_fn(move |_| { + let _retain = &lease; + Poll::Ready(Ok(11_u8)) + })); + let mut wrapped = bind_future(scope.clone(), paired, entered, source); + assert_eq!(scope.state().expect("owner").roots.borrow().len(), 1); + assert!(matches!( + wrapped + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())), + Poll::Ready(Ok(11)) + )); + assert_eq!(count.get(), 1); + assert!(scope.state().expect("owner").roots.borrow().is_empty()); + drop(wrapped); + assert_eq!(count.get(), 1); + } + + #[test] + fn scope_run_activates_progress_before_timer_setup_without_another_root() { + let owner = OutboundScopeOwner::callback(); + let active = Arc::new(AtomicBool::new(false)); + let polls = Rc::new(Cell::new(0_usize)); + let scope = owner.with(|| { + OutboundScope::capture(Rc::new(Runtime { + active: Arc::clone(&active), + polls: Rc::clone(&polls), + })) + }); + let entered = MonotonicInstant::now(); + let identity = Arc::new(()); + let timer_identity = Arc::clone(&identity); + let timer_active = Arc::clone(&active); + let paired = OutboundTime::new(MonotonicClock::new(move || entered), move |_, _| { + assert!(timer_active.load(Ordering::SeqCst)); + assert!( + capture().is_managed(), + "original scope is installed before timer factory" + ); + let error = EdgeError::internal(TimerFault(Arc::clone(&timer_identity))); + Box::pin(async move { Err(error) }) + }); + let deadline = paired.clock().deadline_after(Duration::from_secs(1)); + let mut timer = paired.wait_until(deadline); + let drops = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&drops)); + let mut running = scope.run( + paired, + entered, + Box::pin(poll_fn(move |cx| { + let _keep = &lease; + timer.as_mut().poll(cx) + })), + ); + assert!(scope.state().expect("owner").roots.borrow().is_empty()); + let Poll::Ready(Err(EdgeError::Internal { source, .. })) = running + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + else { + panic!("original timer failure"); + }; + assert!(Arc::ptr_eq( + &source.downcast_ref::().expect("typed source").0, + &identity + )); + assert_eq!(polls.get(), 1); + assert_eq!( + drops.get(), + 1, + "terminal future releases its captures before Ready" + ); + assert!(!active.load(Ordering::SeqCst)); + assert!(!capture().is_managed()); + assert!(scope.state().expect("owner").roots.borrow().is_empty()); + } + + #[test] + fn foreign_poll_retains_original_scope_and_restores_the_callers_scope() { + let original = OutboundScopeOwner::callback(); + let foreign = OutboundScopeOwner::callback(); + let scope = original.with(capture); + let expected = scope.provider_state(|| 3_u8).expect("original provider"); + let other = foreign + .scope() + .provider_state(|| 7_u8) + .expect("foreign provider"); + let (entered, paired) = time(); + let mut source = Box::pin(async { + let observed = capture() + .provider_state(|| 11_u8) + .expect("captured original provider"); + assert!(Rc::ptr_eq(&observed, &expected)); + }); + foreign.with(|| { + assert!(matches!( + scope.poll( + &paired, + entered, + &mut Context::from_waker(Waker::noop()), + source.as_mut() + ), + Poll::Ready(Ok(())) + )); + let restored = capture() + .provider_state(|| 19_u8) + .expect("restored foreign provider"); + assert!(Rc::ptr_eq(&restored, &other)); + }); + assert!(!capture().is_managed()); + } + + #[test] + fn invalid_runtime_never_double_polls_or_discards_a_ready_output_as_pending() { + for mode in 0_u8..3 { + let owner = OutboundScopeOwner::callback(); + let scope = owner.with(|| OutboundScope::capture(Rc::new(InvalidRuntime(mode)))); + let (entered, paired) = time(); + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let mut source = Box::pin(poll_fn(move |_| { + observed.set(observed.get().saturating_add(1)); + assert_eq!(observed.get(), 1, "a completed future is never repolled"); + Poll::Ready(7_u8) + })); + assert!(matches!( + scope.poll( + &paired, + entered, + &mut Context::from_waker(Waker::noop()), + source.as_mut() + ), + Poll::Ready(Err(EdgeError::Internal { .. })) + )); + assert!(!capture().is_managed()); + } + } + + #[test] + fn a_reentrant_value_borrow_fails_without_mutating_the_outer_value() { + let owner = OutboundScopeOwner::callback(); + let value = Revocable::new(&owner.scope(), 3_u8).expect("register"); + let result = value.with(&mut Context::from_waker(Waker::noop()), |number, cx| { + assert!(matches!( + value.with(cx, |_, _| ()), + Err(EdgeError::Internal { .. }) + )); + *number = 7; + }); + result.expect("outer operation remains valid"); + assert_eq!( + value + .with(&mut Context::from_waker(Waker::noop()), |number, _| *number) + .expect("value retained"), + 7 + ); + } + + #[cfg(panic = "unwind")] + #[test] + fn panicking_payload_cleanup_still_releases_retained_siblings_and_provider_state() { + let owner = OutboundScopeOwner::callback(); + let scope = owner.scope(); + let first = Revocable::new( + &scope, + DropAction(Some(Box::new(|| panic!("application stream drop")))), + ) + .expect("first root"); + let sibling_releases = Rc::new(Cell::new(0_usize)); + let second = Revocable::new(&scope, Release(Rc::clone(&sibling_releases))) + .expect("retained sibling"); + let provider_releases = Rc::new(Cell::new(0_usize)); + drop( + scope + .provider_state(|| Release(Rc::clone(&provider_releases))) + .expect("provider state"), + ); + assert!(catch_unwind(AssertUnwindSafe(|| owner.close())).is_err()); + assert!(first.is_revoked()); + assert!(second.is_revoked()); + assert_eq!( + sibling_releases.get(), + 1, + "a retained sibling cannot delay release during unwind" + ); + assert_eq!(provider_releases.get(), 1); + assert!(!capture().is_managed(), "ambient scope restores on unwind"); + drop((first, second)); + assert_eq!(sibling_releases.get(), 1); + } + + #[test] + fn callback_helpers_reuse_managed_owners_including_closed_ambient_scopes() { + let owner = OutboundScopeOwner::callback_if_absent().expect("outer callback"); + let expected = owner + .scope() + .provider_state(|| 3_u8) + .expect("shared provider state"); + let releases = Rc::new(Cell::new(0_usize)); + let retained = owner.with(|| { + assert!(OutboundScopeOwner::callback_if_absent().is_none()); + let nested = capture(); + let observed = nested.provider_state(|| 7_u8).expect("same owner"); + assert!(Rc::ptr_eq(&expected, &observed)); + Revocable::new(&nested, Release(Rc::clone(&releases))).expect("same registry") + }); + owner.with(|| { + owner.close(); + assert!(OutboundScopeOwner::callback_if_absent().is_none()); + assert!(matches!(capture().check(), Err(EdgeError::Internal { .. }))); + }); + assert!(retained.is_revoked()); + assert_eq!(releases.get(), 1); + assert!(OutboundScopeOwner::callback_if_absent().is_some()); + capture() + .with(|| { + let canonical = OutboundScopeOwner::callback_if_absent() + .expect("manual invocation is not a canonical boundary"); + assert!(canonical.scope().is_managed()); + }) + .expect("manual activation"); + } + + #[test] + fn response_body_family_resources_do_not_retain_callback_or_roots() { + let callback = OutboundScopeOwner::callback(); + let callback_scope = callback.scope(); + let callback_state = Rc::downgrade(&callback.state); + let provider_releases = Rc::new(Cell::new(0_usize)); + let resource = callback_scope + .provider_state(|| Release(Rc::clone(&provider_releases))) + .expect("callback provider"); + let identity = Rc::downgrade(&resource); + drop(resource); + let root_releases = Rc::new(Cell::new(0_usize)); + let retained = Revocable::new(&callback_scope, Release(Rc::clone(&root_releases))) + .expect("callback root"); + let body = callback.with(OutboundScopeOwner::response_body); + body.accept_body_handoff().expect("accepted body handoff"); + callback.close(); + drop(callback); + assert_eq!(root_releases.get(), 1); + assert!(retained.is_revoked()); + assert!(callback_state.upgrade().is_none()); + assert_eq!(provider_releases.get(), 0); + let inherited = body + .scope() + .provider_state::(|| panic!("handoff must not initialize a second provider")) + .expect("shared family provider"); + assert!(Rc::ptr_eq( + &inherited, + &identity.upgrade().expect("retained provider identity") + )); + drop(inherited); + body.close(); + assert_eq!(provider_releases.get(), 1); + assert!(identity.upgrade().is_none()); + } + + #[test] + fn response_body_first_provider_initialization_is_shared_with_callback_and_descendants() { + let callback = OutboundScopeOwner::callback(); + let body = callback.with(OutboundScopeOwner::response_body); + let closest = body.scope().provider_state(|| 2_u8).expect("body provider"); + let callback_resource = callback + .scope() + .provider_state::(|| panic!("body already initialized the family provider")) + .expect("callback reuses body-first provider"); + assert!(Rc::ptr_eq(&callback_resource, &closest)); + let ancestor_only = callback + .scope() + .provider_state(|| 17_u16) + .expect("ancestor-only provider"); + let descendant = body.with(OutboundScopeOwner::response_body); + body.accept_body_handoff() + .expect("accepted outer body handoff"); + descendant + .accept_body_handoff() + .expect("accepted nested body handoff"); + callback.close(); + body.close(); + let scope = descendant.scope(); + let inherited = scope + .provider_state::(|| panic!("closest provider already exists")) + .expect("closest provider retained"); + let inherited_ancestor = scope + .provider_state::(|| panic!("ancestor provider already exists")) + .expect("ancestor-only provider retained"); + assert!(Rc::ptr_eq(&inherited, &closest)); + assert!(Rc::ptr_eq(&inherited_ancestor, &ancestor_only)); + let fresh = OutboundScopeOwner::callback(); + let unrelated = fresh.scope().provider_state(|| 99_u8).expect("new family"); + assert!(!Rc::ptr_eq(&unrelated, &closest)); + assert_eq!(*unrelated, 99); + } + + #[test] + fn last_family_lease_closes_resources_even_while_scope_tokens_are_retained() { + let callback = OutboundScopeOwner::callback(); + let body = callback.with(OutboundScopeOwner::response_body); + let callback_scope = callback.scope(); + let body_scope = body.scope(); + let releases = Rc::new(Cell::new(0_usize)); + drop( + body_scope + .provider_state(|| Release(Rc::clone(&releases))) + .expect("body-first provider"), + ); + body.close(); + assert_eq!(releases.get(), 0, "callback still holds a family lease"); + callback.close(); + assert_eq!( + releases.get(), + 1, + "last owner closure releases the family cache" + ); + assert!(matches!( + callback_scope.check(), + Err(EdgeError::Internal { .. }) + )); + assert!(matches!( + body_scope.check(), + Err(EdgeError::Internal { .. }) + )); + } + + #[test] + fn response_attachment_transfers_the_exact_owner_without_polling_or_adoption() { + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let identity = Rc::downgrade(&owner.state); + let releases = Rc::new(Cell::new(0_usize)); + let retained = Revocable::new(&scope, Release(Rc::clone(&releases))) + .expect("precreated body operation"); + let mut response = owner.attach(Response::new(Body::empty())).expect("attach"); + assert_eq!(releases.get(), 0); + let transferred = response + .body_mut() + .take_egress_owner() + .expect("exact attached body owner"); + assert!(Rc::ptr_eq( + &transferred.state, + &identity.upgrade().expect("same owner state") + )); + assert!(response.body_mut().take_egress_owner().is_none()); + drop(response); + assert_eq!(releases.get(), 0); + transferred.close(); + assert_eq!(releases.get(), 1); + assert!(retained.is_revoked()); + } + + #[test] + fn dropping_unhanded_response_closes_precreated_unpolled_body_work() { + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let releases = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&releases)); + let (entered, paired) = time(); + let retained = bind_future( + scope.clone(), + paired, + entered, + Box::pin(async move { + let _keep = lease; + pending::>().await + }), + ); + let response = owner.attach(Response::new(Body::empty())).expect("attach"); + assert_eq!(releases.get(), 0); + drop(response); + assert_eq!(releases.get(), 1); + assert!(matches!(scope.check(), Err(EdgeError::Internal { .. }))); + assert!(matches!( + block_on(retained), + Err(EdgeError::Internal { .. }) + )); + } + + #[test] + fn response_attachment_rejects_callback_closed_and_duplicate_owners() { + let callback = OutboundScopeOwner::callback(); + assert!(matches!( + callback.attach(Response::new(Body::empty())), + Err(EdgeError::Internal { .. }) + )); + let closed = OutboundScopeOwner::response_body(); + closed.close(); + assert!(matches!( + closed.attach(Response::new(Body::empty())), + Err(EdgeError::Internal { .. }) + )); + let first = OutboundScopeOwner::response_body(); + let first_scope = first.scope(); + let response = first + .attach(Response::new(Body::empty())) + .expect("first attachment"); + let duplicate = OutboundScopeOwner::response_body(); + let duplicate_scope = duplicate.scope(); + assert!(matches!( + duplicate.attach(response), + Err(EdgeError::Internal { .. }) + )); + assert!(matches!( + first_scope.check(), + Err(EdgeError::Internal { .. }) + )); + assert!(matches!( + duplicate_scope.check(), + Err(EdgeError::Internal { .. }) + )); + } + + #[test] + fn retained_provisional_body_is_closed_before_parent_cleanup_can_observe_it() { + let callback = OutboundScopeOwner::callback(); + let observed = Rc::new(RefCell::new(None::)); + let observed_in_drop = Rc::clone(&observed); + let releases = Rc::new(Cell::new(0_usize)); + let releases_in_drop = Rc::clone(&releases); + let earlier = Revocable::new( + &callback.scope(), + DropAction(Some(Box::new(move || { + let child = observed_in_drop.borrow().clone().expect("child scope"); + assert!(matches!(child.check(), Err(EdgeError::Internal { .. }))); + assert_eq!(releases_in_drop.get(), 0, "parent guard has not run yet"); + }))), + ) + .expect("earlier callback root"); + let body = callback.with(OutboundScopeOwner::response_body); + observed.replace(Some(body.scope())); + let retained = Revocable::new(&body.scope(), Release(Rc::clone(&releases))) + .expect("provisional body operation"); + callback.close(); + assert_eq!(releases.get(), 1); + assert!(retained.is_revoked()); + assert!(matches!( + body.scope().check(), + Err(EdgeError::Internal { .. }) + )); + assert!(matches!( + body.accept_body_handoff(), + Err(EdgeError::Internal { .. }) + )); + drop(earlier); + } + + #[test] + fn accepted_body_handoff_unregisters_parent_guard_and_preserves_child_root_identity() { + let callback = OutboundScopeOwner::callback(); + let body = callback.with(OutboundScopeOwner::response_body); + let scope = body.scope(); + let releases = Rc::new(Cell::new(0_usize)); + let retained = + Revocable::new(&scope, Release(Rc::clone(&releases))).expect("precreated body root"); + let identity = Rc::downgrade(&retained.node); + assert_eq!(callback.state.roots.borrow().len(), 1, "one parent guard"); + body.accept_body_handoff() + .expect("framework accepts body handoff"); + assert!(callback.state.roots.borrow().is_empty()); + callback.close(); + scope + .check() + .expect("accepted body lifetime is independent"); + assert_eq!(releases.get(), 0); + assert!(Rc::ptr_eq( + &retained.node, + &identity.upgrade().expect("same root") + )); + body.close(); + assert_eq!(releases.get(), 1); + } + + #[test] + fn attached_response_is_still_provisional_until_framework_acceptance() { + let callback = OutboundScopeOwner::callback(); + let body = callback.with(OutboundScopeOwner::response_body); + let scope = body.scope(); + let releases = Rc::new(Cell::new(0_usize)); + let retained = Revocable::new(&scope, Release(Rc::clone(&releases))) + .expect("body work before attachment"); + let response = body + .attach(Response::new(Body::empty())) + .expect("attach only"); + callback.close(); + assert_eq!(releases.get(), 1); + assert!(retained.is_revoked()); + assert!(matches!(scope.check(), Err(EdgeError::Internal { .. }))); + drop(response); + assert_eq!(releases.get(), 1); + } + + #[test] + fn callback_drop_revokes_externally_retained_provisional_owner_without_a_strong_parent() { + let callback = OutboundScopeOwner::callback(); + let parent_identity = Rc::downgrade(&callback.state); + let retained_owner = callback.with(OutboundScopeOwner::response_body); + let releases = Rc::new(Cell::new(0_usize)); + let retained_work = Revocable::new(&retained_owner.scope(), Release(Rc::clone(&releases))) + .expect("externally retained work"); + drop(callback); + assert!(parent_identity.upgrade().is_none()); + assert_eq!(releases.get(), 1); + assert!(retained_work.is_revoked()); + assert!(matches!( + retained_owner.scope().check(), + Err(EdgeError::Internal { .. }) + )); + drop(retained_owner); + assert_eq!(releases.get(), 1); + } + + #[test] + fn revocable_cleanup_explicit_and_implicit_drop_keep_captured_scope_identity() { + for implicit in [false, true] { + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let foreign = OutboundScopeOwner::callback(); + let observed = Rc::new(RefCell::new(None)); + let captured_on_drop = Rc::clone(&observed); + let value = Revocable::new( + &scope, + DropAction(Some(Box::new(move || { + captured_on_drop.replace(Some(capture())); + }))), + ) + .expect("register owned value"); + foreign.with(|| { + if implicit { + drop(Some(value)); + } else { + drop(value); + } + assert!(Weak::ptr_eq( + &capture().current.state, + &foreign.scope().current.state + )); + }); + let retained = observed.borrow_mut().take().expect("destructor capture"); + assert!(Weak::ptr_eq(&retained.current.state, &scope.current.state)); + retained.check().expect("original body is still live"); + owner.close(); + assert!(matches!(retained.check(), Err(EdgeError::Internal { .. }))); + assert!(!capture().is_managed()); + } + } + + #[test] + fn revocable_cleanup_revoked_active_value_and_output_keep_closed_captured_scope() { + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let foreign = OutboundScopeOwner::callback(); + let input_observed = Rc::new(RefCell::new(None)); + let input_capture = Rc::clone(&input_observed); + let value = Revocable::new( + &scope, + DropAction(Some(Box::new(move || { + input_capture.replace(Some(capture())); + }))), + ) + .expect("register input"); + let output_observed = Rc::new(RefCell::new(None)); + let output_capture = Rc::clone(&output_observed); + foreign.with(|| { + let result = value.with(&mut Context::from_waker(Waker::noop()), |_, _| { + owner.close(); + DropAction(Some(Box::new(move || { + output_capture.replace(Some(capture())); + }))) + }); + assert!(matches!(result, Err(EdgeError::Internal { .. }))); + assert!(Weak::ptr_eq( + &capture().current.state, + &foreign.scope().current.state + )); + }); + for observed in [input_observed, output_observed] { + let retained = observed + .borrow_mut() + .take() + .expect("closed destructor capture"); + assert!(Weak::ptr_eq(&retained.current.state, &scope.current.state)); + assert!(matches!(retained.check(), Err(EdgeError::Internal { .. }))); + } + } + + #[test] + fn revocable_cleanup_failed_registration_keeps_closed_captured_scope() { + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + owner.close(); + let foreign = OutboundScopeOwner::callback(); + let observed = Rc::new(RefCell::new(None)); + let captured_on_drop = Rc::clone(&observed); + foreign.with(|| { + let failed = Revocable::new( + &scope, + DropAction(Some(Box::new(move || { + captured_on_drop.replace(Some(capture())); + }))), + ); + assert!(matches!(failed, Err(EdgeError::Internal { .. }))); + }); + let retained = observed + .borrow_mut() + .take() + .expect("failed input destructor capture"); + assert!(Weak::ptr_eq(&retained.current.state, &scope.current.state)); + assert!(matches!(retained.check(), Err(EdgeError::Internal { .. }))); + } +} diff --git a/crates/edgezero-core/src/outbound/scope_tests.rs b/crates/edgezero-core/src/outbound/scope_tests.rs new file mode 100644 index 000000000..bb35fb85c --- /dev/null +++ b/crates/edgezero-core/src/outbound/scope_tests.rs @@ -0,0 +1,978 @@ +use std::cell::RefCell; +use std::collections::VecDeque; +use std::iter; +use std::mem::take; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, Weak}; +use std::task::{Context, Poll, Wake, Waker}; +use std::time::Duration; + +use bytes::Bytes; +use futures::executor::block_on; +use futures_util::future::{pending, poll_fn, ready}; +use futures_util::{FutureExt as _, StreamExt as _, stream}; + +use super::{ + ContentTypeProvenance, HttpClient, IndependentOutboundProgress, OutboundBatch, + OutboundBatchCheck, OutboundBatchContext, OutboundBatchDriverEvent, OutboundBatchItem, + OutboundBatchTermination, OutboundCollectedSlot, OutboundDispatchRequest, + OutboundExchangeOwner, OutboundFuture, OutboundHead, OutboundHeaderFidelity, + OutboundHeaderInput, OutboundHttpClient, OutboundPending, OutboundPreparedBatch, + OutboundProgressRuntime, OutboundRequest, OutboundResponse, OutboundScope, OutboundScopeKind, + OutboundScopeOwner, OutboundSendMode, OutboundTime, validate_for_dispatch, +}; +use crate::body::{Body, BodyPayload}; +use crate::error::{BudgetSource, EdgeError}; +use crate::http::{HeaderMap, Method, Response, StatusCode}; +use crate::response_egress::{ResponseEgressCompletion, ResponseEgressEnvelope}; +use crate::response_egress_framing::ResponseEgressFramingMode; +use crate::time::{Deadline, DispatchBudget, MonotonicClock, MonotonicInstant}; + +#[derive(Default)] +struct Observations { + aborts: AtomicUsize, + cleanup_closed: AtomicBool, + entered: Mutex>, + polls: Mutex>>, + provider_allocations: AtomicUsize, + releases: AtomicUsize, +} + +struct ProbeClient { + batch: Option>, + observed: Arc, + time: OutboundTime, +} + +struct ProbeProgress(Arc); + +struct ProbeProviderState; + +#[derive(Default)] +struct TimelineBatch { + completions: Mutex>, + releases: AtomicUsize, + started: AtomicUsize, +} + +struct TimelineFixture { + client: HttpClient, + clock: MonotonicClock, + entered: MonotonicInstant, + now: Arc>, + observed: Arc, + producer: Arc, + waiters: Arc>>>, +} + +struct TimerRegistration(Mutex>); + +#[derive(Default)] +struct WakeCount(AtomicUsize); + +impl OutboundHttpClient for ProbeClient { + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let head = self.dispatch_head(request).now_or_never().ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!("probe head unexpectedly pending")) + })??; + let completion = head.finish(); + let observed = Arc::clone(&self.observed); + Ok(OutboundPending::new( + completion, + OutboundExchangeOwner::new(move || { + observed.aborts.fetch_add(1, Ordering::SeqCst); + }), + )) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + self.observed + .entered + .lock() + .expect("entries") + .push(request.entered_at()); + let (parts, captured) = request.into_parts(); + let observed = Arc::clone(&self.observed); + Box::pin(async move { + if parts.uri.path() == "/origin" { + let head_at = Deadline::at_instant( + captured + .entered_at() + .checked_add(Duration::from_millis(118)) + .expect("origin head instant"), + ); + captured.time().wait_until(head_at).await?; + } + if parts.uri.path() == "/mediator" { + captured + .time() + .wait_until(captured.budget().deadline) + .await?; + return Err(EdgeError::gateway_timeout_caused( + "mediator deadline", + captured.budget().cause, + )); + } + OutboundHead::new( + StatusCode::OK, + OutboundHeaderInput::new( + "runtime-probe", + HeaderMap::new(), + OutboundHeaderFidelity::AdapterVisibleFields, + ), + stream::once(ready(Ok(Bytes::from_static(b"live")))).boxed_local(), + captured, + OutboundExchangeOwner::new(move || { + let scope = OutboundScope::capture(Rc::new(IndependentOutboundProgress)); + observed.cleanup_closed.store( + scope.kind() == Some(OutboundScopeKind::ResponseBody) + && scope.check().is_err(), + Ordering::SeqCst, + ); + observed.releases.fetch_add(1, Ordering::SeqCst); + }), + ) + }) + } + + fn progress(&self) -> Rc { + Rc::new(ProbeProgress(Arc::clone(&self.observed))) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let Some(timeline) = &self.batch else { + return OutboundBatch::cutoff(batch.into_slots().0); + }; + let (context, slots) = batch.into_slots(); + let count = slots.len(); + timeline.started.store(count, Ordering::SeqCst); + drop(slots); + let completed = Arc::clone(timeline); + let released = Arc::clone(timeline); + let clock = context.clock().clone(); + let mut delivered = 0_usize; + OutboundBatch::from_driver( + context, + stream::poll_fn(move |_| { + let next = completed + .completions + .lock() + .expect("completed slots") + .pop_front(); + if let Some((index, completed_at)) = next { + delivered = delivered.saturating_add(1); + Poll::Ready(Some(OutboundBatchDriverEvent::Item { + index, + completed_at, + outcome: Ok(OutboundResponse::new( + Method::GET, + StatusCode::OK, + HeaderMap::new(), + Body::empty(), + clock.clone(), + ContentTypeProvenance::Unknown, + )), + })) + } else if delivered == count { + Poll::Ready(None) + } else { + Poll::Pending + } + }), + OutboundExchangeOwner::new(move || { + released.releases.fetch_add(1, Ordering::SeqCst); + }), + ) + } + + fn time(&self) -> OutboundTime { + self.time.clone() + } + + fn validate_supported_options( + &self, + request: &OutboundRequest, + mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + validate_for_dispatch(request, mode) + } +} + +impl OutboundProgressRuntime for ProbeProgress { + fn poll( + &self, + scope: &OutboundScope, + _time: &OutboundTime, + _entered_at: MonotonicInstant, + cx: &mut Context<'_>, + inner: &mut dyn FnMut(&mut Context<'_>) -> Poll<()>, + ) -> Poll> { + if let Err(error) = scope.provider_state(|| { + self.0.provider_allocations.fetch_add(1, Ordering::SeqCst); + ProbeProviderState + }) { + return Poll::Ready(Err(error)); + } + self.0 + .polls + .lock() + .expect("runtime polls") + .push(scope.kind()); + inner(cx).map(Ok) + } +} + +impl TimelineFixture { + fn advance(&self, millis: u64) { + *self.now.lock().expect("advance paired clock") = self.at(millis); + let waiting = take(&mut *self.waiters.lock().expect("timer registry")); + for weak in waiting { + if let Some(registration) = weak.upgrade() { + let wake = registration.0.lock().expect("timer waker").take(); + if let Some(waker) = wake { + waker.wake(); + } + } + } + } + + fn at(&self, millis: u64) -> MonotonicInstant { + self.entered + .checked_add(Duration::from_millis(millis)) + .expect("fixture time") + } + + fn complete_batch(&self, batch: &RefCell) -> Vec { + let mut receipts = Vec::new(); + for (index, completed_ms, observed_ms, elapsed_ms) in + [(0, 180, 260, 146), (1, 320, 320, 286), (2, 400, 400, 366)] + { + self.advance(completed_ms); + self.producer + .completions + .lock() + .expect("queue completion") + .push_back((index, self.at(completed_ms))); + self.advance(observed_ms); + let OutboundBatchCheck::Item(item) = + batch.borrow_mut().check().expect("batch observation") + else { + panic!("body-owned completion must survive callback exit"); + }; + assert_eq!(item.index(), index); + assert_eq!(item.result().elapsed, Duration::from_millis(elapsed_ms)); + item.result() + .outcome + .as_ref() + .expect("successful body-owned slot"); + receipts.push(item); + } + assert!(matches!( + batch.borrow_mut().check(), + Ok(OutboundBatchCheck::Finished( + OutboundBatchTermination::Completed + )) + )); + assert_eq!(self.producer.releases.load(Ordering::SeqCst), 1); + receipts + } + + fn new() -> Self { + let entered = MonotonicInstant::now(); + let now = Arc::new(Mutex::new(entered)); + let clock_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock")); + let waiters = Arc::new(Mutex::new(Vec::new())); + let registrations = Arc::clone(&waiters); + let time = OutboundTime::new(clock.clone(), move |deadline, paired_clock| { + let registry = Arc::clone(®istrations); + let registration = Arc::new(TimerRegistration(Mutex::new(None))); + Box::pin(poll_fn(move |cx| { + if deadline.is_expired_at(paired_clock.now()) { + drop(registration.0.lock().expect("expired timer").take()); + Poll::Ready(Ok(())) + } else { + *registration.0.lock().expect("registered waker") = Some(cx.waker().clone()); + registry + .lock() + .expect("timer registration") + .push(Arc::downgrade(®istration)); + Poll::Pending + } + })) + }); + let observed = Arc::new(Observations::default()); + let producer = Arc::new(TimelineBatch::default()); + let client = HttpClient::with_client(ProbeClient { + batch: Some(Arc::clone(&producer)), + observed: Arc::clone(&observed), + time, + }); + Self { + client, + clock, + entered, + now, + observed, + producer, + waiters, + } + } + + fn ready_pending(&self, owner: &OutboundScopeOwner) -> OutboundPending { + let mut pending_response = block_on(owner.with(|| { + self.client + .begin(OutboundRequest::get("https://example.com/").expect("request")) + })) + .expect("begun pending"); + assert!( + !pending_response.is_ready(), + "buffered finalizer yields between ready reads" + ); + assert!( + pending_response.is_ready(), + "completion is cached before callback exit" + ); + pending_response + } + + fn start_origin( + &self, + owner: &OutboundScopeOwner, + ) -> (OutboundFuture, Arc) { + self.advance(2); + let mut origin = owner.with(|| { + self.client.send( + OutboundRequest::get("https://example.com/origin") + .expect("origin") + .stream_response(), + ) + }); + let notifications = Arc::new(WakeCount::default()); + let waker = Waker::from(Arc::clone(¬ifications)); + assert!( + origin + .poll_unpin(&mut Context::from_waker(&waker)) + .is_pending() + ); + assert_eq!( + self.observed.entered.lock().expect("entries").first(), + Some(&self.at(2)) + ); + (origin, notifications) + } +} + +impl Wake for WakeCount { + fn wake(self: Arc) { + Self::wake_by_ref(&self); + } + + fn wake_by_ref(self: &Arc) { + self.0.fetch_add(1, Ordering::SeqCst); + } +} + +fn client() -> (HttpClient, Arc) { + let observed = Arc::new(Observations::default()); + let now = MonotonicInstant::now(); + let time = OutboundTime::new(MonotonicClock::new(move || now), |_, _| Box::pin(pending())); + ( + HttpClient::with_client(ProbeClient { + batch: None, + observed: Arc::clone(&observed), + time, + }), + observed, + ) +} + +#[test] +fn body_created_before_batch_keeps_original_timeline_until_egress_terminal() { + let fixture = TimelineFixture::new(); + let callback = OutboundScopeOwner::callback(); + let body_owner = callback.with(OutboundScopeOwner::response_body); + let body_scope = body_owner.scope(); + let (origin, wakes) = fixture.start_origin(&body_owner); + fixture.advance(34); + let requests = + iter::repeat_with(|| OutboundRequest::get("https://example.com/").expect("request")) + .take(3) + .collect(); + let batch = Rc::new(RefCell::new(body_owner.with(|| { + fixture + .client + .start_batch_until(requests, Deadline::at_instant(fixture.at(1000))) + }))); + assert_eq!(fixture.producer.started.load(Ordering::SeqCst), 3); + let body_pending = fixture.ready_pending(&body_owner); + let callback_pending = fixture.ready_pending(&callback); + fixture.advance(120); + assert!( + wakes.0.load(Ordering::SeqCst) > 0, + "paired origin timer must wake" + ); + let origin_response = origin + .now_or_never() + .expect("origin head at 120") + .expect("origin head"); + let retained_batch = Rc::clone(&batch); + let response = body_owner + .attach(Response::new(Body::from_stream(stream::once(async move { + let _retain_group = retained_batch; + origin_response.into_body().into_bytes_bounded(4).await + })))) + .expect("attach exact body lifetime"); + let envelope = ResponseEgressEnvelope::detached( + response, + fixture.entered, + Method::GET, + ResponseEgressCompletion::empty(), + fixture.clock.clone(), + ); + let Ok((prepared, _, mut attempt, _)) = + callback.with(|| envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength)) + else { + panic!("body scope must be accepted before callback return"); + }; + assert!(attempt.begin_writing()); + callback.close(); + assert_eq!( + fixture.observed.aborts.load(Ordering::SeqCst), + 1, + "callback cleanup does not cancel body-owned pending" + ); + assert!(matches!( + block_on(callback_pending.collect()), + Err(EdgeError::Internal { .. }) + )); + body_scope + .check() + .expect("accepted body work survives callback exit"); + + let receipts = fixture.complete_batch(&batch); + fixture.advance(410); + assert_eq!( + block_on(prepared.into_response().into_body().into_bytes_bounded(6)).expect("origin body"), + "live" + ); + body_scope + .check() + .expect("source EOF is not transport completion"); + assert_eq!(fixture.observed.aborts.load(Ordering::SeqCst), 1); + fixture.advance(420); + assert!(attempt.complete(fixture.at(420))); + assert!(body_scope.check().is_err()); + assert_eq!( + fixture.observed.aborts.load(Ordering::SeqCst), + 2, + "terminal releases retained body pending before it is collected" + ); + assert!(matches!( + block_on(body_pending.collect()), + Err(EdgeError::Internal { .. }) + )); + assert_eq!( + receipts.len(), + 3, + "already delivered receipts remain owned after scope cleanup" + ); + for item in receipts { + item.result() + .outcome + .as_ref() + .expect("receipt remains valid"); + } +} + +#[test] +fn body_scoped_mediator_keeps_134ms_request_deadline_after_120ms_callback_exit() { + let fixture = TimelineFixture::new(); + let callback = OutboundScopeOwner::callback(); + let owner = callback.with(OutboundScopeOwner::response_body); + fixture.advance(34); + let mut mediator = owner.with(|| { + fixture.client.send( + OutboundRequest::get("https://example.com/mediator") + .expect("mediator") + .deadline(Deadline::at_instant(fixture.at(134))), + ) + }); + let notifications = Arc::new(WakeCount::default()); + let waker = Waker::from(Arc::clone(¬ifications)); + assert!( + mediator + .poll_unpin(&mut Context::from_waker(&waker)) + .is_pending() + ); + let response = owner + .attach(Response::new(Body::empty())) + .expect("attach before callback ends"); + fixture.advance(120); + let envelope = ResponseEgressEnvelope::detached( + response, + fixture.entered, + Method::GET, + ResponseEgressCompletion::empty(), + fixture.clock.clone(), + ); + let Ok((_prepared, _, mut attempt, _)) = + callback.with(|| envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength)) + else { + panic!("accept mediator body owner"); + }; + callback.close(); + assert!( + mediator + .poll_unpin(&mut Context::from_waker(&waker)) + .is_pending() + ); + let before_expiry = notifications.0.load(Ordering::SeqCst); + fixture.advance(134); + assert!( + notifications.0.load(Ordering::SeqCst) > before_expiry, + "the paired deadline timer must wake" + ); + fixture.advance(260); + let error = mediator + .now_or_never() + .expect("expired mediator must be ready") + .expect_err("134ms own deadline cannot restart at handoff"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + } + )); + assert_eq!( + fixture + .observed + .entered + .lock() + .expect("mediator entry") + .as_slice(), + &[fixture.at(34)] + ); + assert!(attempt.begin_writing()); + assert!(attempt.complete(fixture.at(260))); +} + +#[test] +fn nonblocking_observers_activate_scope_without_driving_progress() { + let callback = OutboundScopeOwner::callback(); + let observed = Arc::new(Observations::default()); + let scope = + callback.with(|| OutboundScope::capture(Rc::new(ProbeProgress(Arc::clone(&observed))))); + let entered = MonotonicInstant::now(); + let time = OutboundTime::new(MonotonicClock::new(move || entered), |_, _| { + panic!("observation cannot create a timer") + }); + let receiver = poll_fn(|_cx| { + assert_eq!( + OutboundScope::capture(Rc::new(IndependentOutboundProgress)).kind(), + Some(OutboundScopeKind::Callback) + ); + Poll::Pending + }); + let mut pending_response = + OutboundPending::new(Box::pin(receiver), OutboundExchangeOwner::new(|| {})) + .bind(&scope, time.clone(), entered) + .expect("bind receiver"); + let context = OutboundBatchContext::new( + entered, + time, + Deadline::after(Duration::from_secs(1)), + vec![Some(DispatchBudget { + cause: BudgetSource::BatchCutoff, + deadline: Deadline::after(Duration::from_secs(1)), + duration: Duration::from_secs(1), + })], + Vec::new(), + scope, + ); + let mut batch = OutboundBatch::from_driver( + context, + stream::poll_fn(|_cx| { + assert_eq!( + OutboundScope::capture(Rc::new(IndependentOutboundProgress)).kind(), + Some(OutboundScopeKind::Callback) + ); + Poll::Pending + }), + OutboundExchangeOwner::new(|| {}), + ); + let foreign = callback.with(OutboundScopeOwner::response_body); + assert!(!foreign.with(|| pending_response.is_ready())); + assert!(matches!( + foreign.with(|| batch.check()), + Ok(OutboundBatchCheck::Pending) + )); + assert!( + observed.polls.lock().expect("progress calls").is_empty(), + "neither nonblocking observer may drive the provider" + ); + let mut collect = pending_response.collect(); + assert!( + collect + .poll_unpin(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + let mut next = Box::pin(batch.next()); + assert!( + next.poll_unpin(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert_eq!( + observed.polls.lock().expect("progress calls").as_slice(), + &[ + Some(OutboundScopeKind::Callback), + Some(OutboundScopeKind::Callback) + ] + ); +} + +#[test] +fn captured_runtime_wraps_settlement_and_head_finalization() { + let callback = OutboundScopeOwner::callback(); + let (client, observed) = client(); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .timeout(Duration::from_secs(1)) + .body(crate::Body::from_stream(stream::empty())); + let future = callback.with(|| client.send(request)); + let response = block_on(callback.run(future)).expect("send"); + assert_eq!(response.body().as_bytes(), Some(b"live".as_slice())); + assert_eq!(observed.releases.load(Ordering::SeqCst), 1); + assert_eq!( + observed.provider_allocations.load(Ordering::SeqCst), + 1, + "one captured provider state in the callback pipeline" + ); + let polls = observed.polls.lock().expect("runtime polls"); + assert!( + polls.len() >= 2, + "the facade and head finalizer both run captured progress" + ); + assert!( + polls + .iter() + .all(|kind| *kind == Some(OutboundScopeKind::Callback)) + ); +} + +#[test] +fn response_owned_body_is_rejected_before_any_outbound_transport() { + for mode in [ + OutboundSendMode::Send, + OutboundSendMode::Begin, + OutboundSendMode::Batch, + ] { + let (client, observed) = client(); + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let response = owner + .attach(Response::new(Body::from("payload"))) + .expect("attach response lifetime"); + let request = OutboundRequest::post("https://example.com/") + .expect("request") + .body(response.into_body()); + let error = match mode { + OutboundSendMode::Send => block_on(client.send(request)) + .expect_err("response ownership is not upload ownership"), + OutboundSendMode::Begin => block_on(client.begin(request)) + .expect_err("response ownership is not upload ownership"), + OutboundSendMode::Batch => { + let batch = client + .start_batch_until(vec![request], Deadline::after(Duration::from_secs(1))); + let (mut results, _) = block_on(batch.collect()) + .expect("preflight receipts") + .into_parts(); + let OutboundCollectedSlot::Resolved(result) = results.pop().expect("slot") else { + panic!("preflight result must be resolved"); + }; + result + .outcome + .expect_err("response ownership is not upload ownership") + } + OutboundSendMode::Follow => panic!("only direct facade modes are tested"), + }; + assert!(matches!(error, EdgeError::BadRequest { .. })); + assert!( + observed + .entered + .lock() + .expect("provider entries") + .is_empty() + ); + assert!( + scope.check().is_err(), + "rejected upload releases response work" + ); + } +} + +#[test] +fn outbound_response_normalization_preserves_attached_owner_and_buffered_length() { + for status in [StatusCode::OK, StatusCode::NO_CONTENT] { + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let bytes = Bytes::from(vec![b'x'; 16]); + let pointer = bytes.as_ptr(); + let response = owner + .attach(Response::new(Body::from(bytes))) + .expect("attach"); + let normalized_response = OutboundResponse::new( + Method::GET, + status, + HeaderMap::new(), + response.into_body(), + MonotonicClock::default(), + ContentTypeProvenance::Unknown, + ) + .into_response() + .expect("normalized response"); + let retained_bytes = normalized_response + .body() + .as_bytes() + .expect("Once remains Once"); + if status == StatusCode::OK { + assert_eq!(retained_bytes.as_ptr(), pointer); + assert_eq!(retained_bytes.len(), 16); + } else { + assert!(retained_bytes.is_empty()); + } + scope + .check() + .expect("normalization keeps the exact response owner"); + drop(normalized_response); + assert!(scope.check().is_err()); + } +} + +#[test] +fn reused_body_scope_releases_retained_outbound_transport_at_egress_terminal() { + let callback = OutboundScopeOwner::callback(); + let owner = callback.with(OutboundScopeOwner::response_body); + let (client, observed) = client(); + let retained = block_on(owner.with(|| { + client.send( + OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(), + ) + })) + .expect("retained origin response"); + let response = owner + .attach(Response::new(Body::empty())) + .expect("attach precreated owner"); + let now = MonotonicInstant::now(); + let envelope = ResponseEgressEnvelope::detached( + response, + now, + Method::GET, + ResponseEgressCompletion::empty(), + MonotonicClock::new(move || now), + ); + let Ok((_prepared, _, mut attempt, _)) = + callback.with(|| envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength)) + else { + panic!("accept body owner"); + }; + callback.close(); + assert_eq!(observed.releases.load(Ordering::SeqCst), 0); + assert!(attempt.begin_writing()); + assert!(attempt.complete(now)); + assert_eq!( + observed.releases.load(Ordering::SeqCst), + 1, + "terminal must drop SDK resources before retained handles are polled or dropped" + ); + assert!( + observed.cleanup_closed.load(Ordering::SeqCst), + "transport cleanup keeps its closed captured body identity" + ); + assert!(matches!( + block_on(retained.into_bytes_bounded(4)), + Err(EdgeError::Internal { .. }) + )); + assert_eq!(observed.releases.load(Ordering::SeqCst), 1); +} + +#[test] +fn precreated_body_owner_attaches_origin_without_creating_another_owner() { + let callback = OutboundScopeOwner::callback(); + let owner = callback.with(OutboundScopeOwner::response_body); + let (client, observed) = client(); + let origin = block_on(owner.with(|| { + client.send( + OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(), + ) + })) + .expect("origin"); + let response = owner + .attach(origin.into_response().expect("origin response")) + .expect("precreated owner is the sole body owner"); + drop(response); + assert_eq!(observed.releases.load(Ordering::SeqCst), 1); + callback + .scope() + .check() + .expect("body cleanup does not close callback siblings"); +} + +#[test] +fn standalone_origin_rejects_duplicate_body_owner_attachment() { + let callback = OutboundScopeOwner::callback(); + let (client, observed) = client(); + let origin = block_on(callback.with(|| { + client.send( + OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(), + ) + })) + .expect("origin"); + assert!(matches!( + OutboundScopeOwner::response_body() + .attach(origin.into_response().expect("origin response")), + Err(EdgeError::Internal { .. }) + )); + assert_eq!(observed.releases.load(Ordering::SeqCst), 1); +} + +#[test] +fn collected_stream_transfers_exact_body_owner_and_eof_does_not_close_it() { + let callback = OutboundScopeOwner::callback(); + let (client, observed) = client(); + let response = block_on(callback.with(|| { + client.send( + OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(), + ) + })) + .expect("stream handoff"); + let (payload, retained_owner) = response.into_body().into_parts(); + let owner = retained_owner.expect("the accepted stream lifetime must travel in Body metadata"); + let scope = owner.scope(); + let BodyPayload::Stream(mut source) = payload else { + panic!("stream remains streamed"); + }; + callback.close(); + assert_eq!( + block_on(source.next()).expect("chunk").expect("bytes"), + "live" + ); + assert!(block_on(source.next()).is_none()); + assert_eq!(observed.releases.load(Ordering::SeqCst), 1); + scope + .check() + .expect("upstream EOF is not downstream egress terminal"); + drop(owner); + assert!(scope.check().is_err()); +} + +#[test] +fn collected_stream_uses_captured_runtime_after_callback_exit() { + let callback = OutboundScopeOwner::callback(); + let (client, observed) = client(); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + let future = callback.with(|| client.send(request)); + let response = block_on(callback.run(future)).expect("head handoff"); + callback.close(); + assert_eq!(observed.releases.load(Ordering::SeqCst), 0); + assert_eq!( + block_on(response.into_bytes_bounded(4)).expect("independent body"), + Bytes::from_static(b"live") + ); + assert_eq!(observed.releases.load(Ordering::SeqCst), 1); + assert_eq!( + observed.provider_allocations.load(Ordering::SeqCst), + 1, + "body handoff retains the captured provider state before its first read" + ); + assert!( + observed + .polls + .lock() + .expect("runtime polls") + .contains(&Some(OutboundScopeKind::ResponseBody)), + "stream reads must activate retained provider progress under a body owner" + ); +} + +#[test] +fn facade_begin_stream_handoff_unregisters_callback_cancellation() { + let callback = OutboundScopeOwner::callback(); + let (client, observed) = client(); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + let future = callback.with(|| client.begin(request)); + let pending = block_on(future).expect("begun stream"); + let response = block_on(pending.collect()).expect("stream transfer"); + callback.close(); + assert_eq!(observed.aborts.load(Ordering::SeqCst), 0); + assert_eq!(observed.releases.load(Ordering::SeqCst), 0); + assert_eq!( + block_on(response.into_bytes_bounded(4)).expect("body after callback"), + Bytes::from_static(b"live") + ); + assert_eq!(observed.aborts.load(Ordering::SeqCst), 0); + assert_eq!(observed.releases.load(Ordering::SeqCst), 1); + assert_eq!(observed.provider_allocations.load(Ordering::SeqCst), 1); +} + +#[test] +fn facade_begin_uncollected_ready_stream_is_released_at_callback_exit() { + let callback = OutboundScopeOwner::callback(); + let (client, observed) = client(); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + let future = callback.with(|| client.begin(request)); + let mut retained = block_on(future).expect("begun stream"); + assert!(retained.is_ready()); + callback.close(); + assert_eq!(observed.aborts.load(Ordering::SeqCst), 1); + assert_eq!(observed.releases.load(Ordering::SeqCst), 1); + assert!(matches!( + block_on(retained.collect()), + Err(EdgeError::Internal { .. }) + )); + assert!( + !observed + .polls + .lock() + .expect("runtime polls") + .contains(&Some(OutboundScopeKind::ResponseBody)), + "uncollected bodies are released without reading them" + ); +} + +#[test] +fn streamed_body_drop_closes_ownership_before_sdk_cleanup_callbacks() { + let callback = OutboundScopeOwner::callback(); + let (client, observed) = client(); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .stream_response(); + let future = callback.with(|| client.send(request)); + let response = block_on(future).expect("stream handoff"); + drop(response); + assert_eq!(observed.releases.load(Ordering::SeqCst), 1); + assert!( + observed.cleanup_closed.load(Ordering::SeqCst), + "SDK cleanup runs inside a closed body scope, not a fresh manual scope" + ); + callback + .scope() + .check() + .expect("callback siblings stay live"); +} diff --git a/crates/edgezero-core/src/outbound/task.rs b/crates/edgezero-core/src/outbound/task.rs new file mode 100644 index 000000000..40d58a127 --- /dev/null +++ b/crates/edgezero-core/src/outbound/task.rs @@ -0,0 +1,346 @@ +use std::fmt; +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; +use std::task::{Context, Poll}; + +use futures_util::future::LocalBoxFuture; + +use super::{IndependentOutboundProgress, OutboundExchangeOwner, OutboundScope, Revocable}; +use crate::error::EdgeError; + +type Work = Rc>>; + +/// SDK-neutral local runner and cancellation ownership for one outbound producer. +/// Dropping either the runner or its paired owner promptly releases the owned future, +/// even if the other side is retained. Scheduling remains entirely provider-owned. +pub struct OutboundTask { + work: Option, +} + +impl OutboundTask { + /// Owns a producer without polling or spawning it, using the shared revocation protocol. + /// + /// # Errors + /// Returns the unchanged scope-registration failure after dropping the input. Providers must + /// complete this construction before spawning or performing any SDK activity. + #[inline] + pub fn new( + future: LocalBoxFuture<'static, ()>, + ) -> Result<(Self, OutboundExchangeOwner), EdgeError> { + let scope = OutboundScope::capture(Rc::new(IndependentOutboundProgress)); + let work = Rc::new(Revocable::new(&scope, future)?); + let cancellation = Rc::clone(&work); + let owner = OutboundExchangeOwner::new(move || { + cancellation.revoke(); + }); + Ok((Self { work: Some(work) }, owner)) + } +} + +impl fmt::Debug for OutboundTask { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutboundTask") + .field( + "active", + &self.work.as_ref().is_some_and(|work| !work.is_revoked()), + ) + .finish_non_exhaustive() + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for OutboundTask { + #[inline] + fn drop(&mut self) { + if let Some(work) = self.work.take() { + work.revoke(); + } + } +} + +impl Future for OutboundTask { + type Output = (); + + #[inline] + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + let Some(work) = this.work.as_ref().map(Rc::clone) else { + return Poll::Ready(()); + }; + match work.with(cx, |future, inner_cx| future.as_mut().poll(inner_cx)) { + Ok(Poll::Pending) => Poll::Pending, + Ok(Poll::Ready(())) | Err(_) => { + work.revoke(); + drop(this.work.take()); + Poll::Ready(()) + } + } + } +} + +#[cfg(test)] +mod tests { + use std::cell::{Cell, RefCell}; + use std::future::{Future as _, pending, poll_fn}; + use std::pin::Pin; + use std::rc::Rc; + use std::sync::Arc; + use std::sync::mpsc::{TryRecvError, channel}; + use std::task::{Context, Poll, Wake, Waker}; + + use super::OutboundTask; + use crate::error::EdgeError; + use crate::outbound::{OutboundExchangeOwner, OutboundScopeOwner}; + + struct Release(Rc>); + + struct ReentrantWake; + + std::thread_local! { + static DROP_ACTION: RefCell>> = const { RefCell::new(None) }; + static WAKE_ACTION: RefCell>> = const { RefCell::new(None) }; + } + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for ReentrantWake { + fn drop(&mut self) { + let action = DROP_ACTION + .try_with(|stored| stored.borrow_mut().take()) + .ok() + .flatten(); + if let Some(callback) = action { + callback(); + } + } + } + + impl Wake for ReentrantWake { + fn wake(self: Arc) { + Self::wake_by_ref(&self); + } + + fn wake_by_ref(self: &Arc) { + let action = WAKE_ACTION + .try_with(|stored| stored.borrow_mut().take()) + .ok() + .flatten(); + if let Some(callback) = action { + callback(); + } + } + } + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for Release { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + #[test] + fn executor_drop_releases_unpolled_future_and_sender_even_with_retained_owner() { + let releases = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&releases)); + let (sender, receiver) = channel::<()>(); + let (task, owner) = OutboundTask::new(Box::pin(async move { + let _owned = (lease, sender); + pending::<()>().await; + })) + .expect("task registration"); + drop(task); + assert_eq!(releases.get(), 1); + assert_eq!(receiver.try_recv(), Err(TryRecvError::Disconnected)); + drop(owner); + assert_eq!(releases.get(), 1); + } + + #[test] + fn owner_drop_cancels_before_first_poll_and_runner_remains_terminal() { + let releases = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&releases)); + let (mut task, owner) = OutboundTask::new(Box::pin(async move { + let _owned = lease; + panic!("cancelled input must not be polled"); + })) + .expect("task registration"); + drop(owner); + assert_eq!(releases.get(), 1); + for _ in 0_u8..2 { + assert!( + Pin::new(&mut task) + .poll(&mut Context::from_waker(Waker::noop())) + .is_ready() + ); + } + drop(task); + assert_eq!(releases.get(), 1); + } + + #[test] + fn closed_scope_rejects_registration_before_spawn_and_releases_input() { + let scope_owner = OutboundScopeOwner::callback(); + scope_owner.close(); + let releases = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&releases)); + let (sender, receiver) = channel::<()>(); + let refused = scope_owner.with(|| { + OutboundTask::new(Box::pin(async move { + let _owned = (lease, sender); + panic!("refused task cannot be polled or spawned"); + })) + }); + assert!(matches!(refused, Err(EdgeError::Internal { .. }))); + assert_eq!(releases.get(), 1); + assert_eq!(receiver.try_recv(), Err(TryRecvError::Disconnected)); + } + + #[test] + fn completion_releases_input_before_ready_even_with_retained_runner_and_owner() { + let releases = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&releases)); + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let (mut task, owner) = OutboundTask::new(Box::pin(poll_fn(move |_| { + let _retain = &lease; + observed.set(observed.get().saturating_add(1)); + Poll::Ready(()) + }))) + .expect("task registration"); + for _ in 0_u8..2 { + assert!( + Pin::new(&mut task) + .poll(&mut Context::from_waker(Waker::noop())) + .is_ready() + ); + assert_eq!(releases.get(), 1); + } + assert_eq!(polls.get(), 1); + drop((task, owner)); + assert_eq!(releases.get(), 1); + } + + #[test] + fn cancellation_inside_pending_or_ready_poll_never_restores_the_active_future() { + for ready in [false, true] { + let cancellation = Rc::new(RefCell::new(None::)); + let cancel_from_poll = Rc::downgrade(&cancellation); + let releases = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&releases)); + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let (mut task, owner) = OutboundTask::new(Box::pin(poll_fn(move |_| { + let _retain = &lease; + observed.set(observed.get().saturating_add(1)); + let slot = cancel_from_poll.upgrade().expect("cancellation slot"); + let owner_to_drop = slot.borrow_mut().take(); + drop(owner_to_drop); + if ready { + Poll::Ready(()) + } else { + Poll::Pending + } + }))) + .expect("task registration"); + cancellation.replace(Some(owner)); + for _ in 0_u8..2 { + assert!( + Pin::new(&mut task) + .poll(&mut Context::from_waker(Waker::noop())) + .is_ready() + ); + } + assert_eq!(polls.get(), 1); + assert_eq!(releases.get(), 1); + } + } + + #[test] + fn cancellation_wake_can_reentrantly_poll_the_retained_runner() { + let releases = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&releases)); + let (task, owner) = OutboundTask::new(Box::pin(async move { + let _retain = lease; + pending::<()>().await; + })) + .expect("task registration"); + let retained = Rc::new(RefCell::new(task)); + let waiting = Waker::from(Arc::new(ReentrantWake)); + assert!( + Pin::new(&mut *retained.borrow_mut()) + .poll(&mut Context::from_waker(&waiting)) + .is_pending() + ); + let target = Rc::downgrade(&retained); + let wakes = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&wakes); + WAKE_ACTION.with(|stored| { + stored.replace(Some(Box::new(move || { + observed.set(observed.get().saturating_add(1)); + let runner = target.upgrade().expect("retained runner"); + assert!( + Pin::new(&mut *runner.borrow_mut()) + .poll(&mut Context::from_waker(Waker::noop())) + .is_ready() + ); + }))) + }); + drop(owner); + assert_eq!(wakes.get(), 1); + assert_eq!(releases.get(), 1); + } + + #[test] + fn replaced_waker_drop_can_cancel_without_borrowing_or_repolling_input() { + let releases = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&releases)); + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let (mut task, owner) = OutboundTask::new(Box::pin(poll_fn(move |_| { + let _retain = &lease; + observed.set(observed.get().saturating_add(1)); + Poll::Pending + }))) + .expect("task registration"); + let waiting = Waker::from(Arc::new(ReentrantWake)); + assert!( + Pin::new(&mut task) + .poll(&mut Context::from_waker(&waiting)) + .is_pending() + ); + drop(waiting); + DROP_ACTION.with(|stored| stored.replace(Some(Box::new(move || drop(owner))))); + assert!( + Pin::new(&mut task) + .poll(&mut Context::from_waker(Waker::noop())) + .is_ready() + ); + assert_eq!(polls.get(), 1); + assert_eq!(releases.get(), 1); + } + + #[test] + fn scope_close_cancels_unpolled_task_while_both_sdk_handles_are_retained() { + let scope_owner = OutboundScopeOwner::callback(); + let releases = Rc::new(Cell::new(0_usize)); + let lease = Release(Rc::clone(&releases)); + let (mut task, owner) = scope_owner + .with(|| { + OutboundTask::new(Box::pin(async move { + let _retain = lease; + pending::<()>().await; + })) + }) + .expect("task registration"); + scope_owner.close(); + assert_eq!(releases.get(), 1); + assert!( + Pin::new(&mut task) + .poll(&mut Context::from_waker(Waker::noop())) + .is_ready() + ); + drop((task, owner)); + assert_eq!(releases.get(), 1); + } +} diff --git a/crates/edgezero-core/src/outbound/tests.rs b/crates/edgezero-core/src/outbound/tests.rs new file mode 100644 index 000000000..d8dffb0ba --- /dev/null +++ b/crates/edgezero-core/src/outbound/tests.rs @@ -0,0 +1,3978 @@ +#![allow( + clippy::shadow_reuse, + clippy::shadow_unrelated, + reason = "table-driven tests intentionally reuse conventional request, response, and budget names" +)] + +use std::cell::Cell; +use std::future::Future as _; +use std::io::{Error as IoError, ErrorKind, Write as _}; +use std::mem::take; +use std::num::NonZeroU64; +use std::rc::Rc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll, Waker}; +use std::time::Duration; + +use bytes::Bytes; +use flate2::write::{GzEncoder, ZlibEncoder}; +use futures::FutureExt as _; +use futures::channel::oneshot; +use futures::executor::block_on; +use futures::future::{poll_fn, ready}; +use futures::pin_mut; +use futures::task::noop_waker_ref; +use futures_util::{StreamExt as _, stream}; + +use crate::body::Body; +use crate::compression::{ContentEncoding, PassthroughReason, classify_content_encoding}; +use crate::error::{ + BadGatewayDecodeReason, BadGatewayReason, BudgetSource, EdgeError, RequestBodyOverflowOrigin, + ResponseLimitReason, +}; +use crate::http::{Authority, HeaderMap, HeaderValue, Method, StatusCode, Uri, request_builder}; +use crate::time::{ + DEADLINE_FAR_FUTURE, DEFAULT_NO_DEADLINE_BUDGET, Deadline, MonotonicClock, MonotonicInstant, + dispatch_budget, +}; + +use super::{ + HttpClient, OutboundBatch, OutboundBatchContext, OutboundBatchDriverEvent, OutboundBatchNext, + OutboundBatchTermination, OutboundCachePolicy, OutboundCollectedSlot, OutboundDispatchRequest, + OutboundExchangeOwner, OutboundFuture, OutboundHead, OutboundHeaderFidelity, + OutboundHeaderInput, OutboundHttpClient, OutboundPending, OutboundPreparedBatch, + OutboundProxyMarker, OutboundRequest, OutboundResponse, OutboundSendMode, OutboundTime, + PROXY_HEADER, ResponseBodyDisposition, ResponseBodyPolicy, ResponseDecoding, + ResponseHeaderLimiter, ResponseMode, apply_response_body_policy, collect_response_stream, + enforce_payload_content_length, insert_proxy_header, limit_decoded_stream, + limit_encoded_stream, normalize_for_dispatch, normalize_response_headers, rechunk_stream, + validate_for_dispatch, +}; + +struct MockClient { + batch_calls: AtomicUsize, + send_calls: AtomicUsize, + time: TestTime, +} + +struct MarkerClient; + +struct BodyDropSignal(Rc>); + +#[derive(Clone)] +struct TestTime { + now: Arc>, + waiters: Arc>>, +} + +struct BatchFixture { + cutoff: Deadline, + started_at: MonotonicInstant, + time: TestTime, +} + +impl OutboundHttpClient for MarkerClient { + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let outcome = block_on(marker_head(request)?.finish()); + Ok(OutboundPending::new(Box::pin(ready(outcome)), no_owner())) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + Box::pin(ready(marker_head(request))) + } + + fn progress(&self) -> Rc { + Rc::new(super::IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, slots) = batch.into_slots(); + let items = slots + .into_iter() + .map(|(index, request)| { + let outcome = marker_head(request).and_then(|head| block_on(head.finish())); + OutboundBatchDriverEvent::Item { + index, + completed_at: context.clock().now(), + outcome, + } + }) + .collect::>(); + OutboundBatch::from_driver(context, stream::iter(items), no_owner()) + } + + fn time(&self) -> OutboundTime { + TestTime::new(MonotonicInstant::now()).pair() + } + + fn validate_supported_options( + &self, + _request: &OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + Ok(()) + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for BodyDropSignal { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } +} + +impl OutboundHttpClient for MockClient { + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let outcome = + block_on(self.dispatch_head(request)).and_then(|head| block_on(head.finish())); + Ok(OutboundPending::new(Box::pin(ready(outcome)), no_owner())) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + self.send_calls.fetch_add(1, Ordering::Relaxed); + Box::pin(ready(test_head( + request, + StatusCode::CREATED, + Body::from("single"), + ))) + } + + fn progress(&self) -> Rc { + Rc::new(super::IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + self.batch_calls.fetch_add(1, Ordering::Relaxed); + let (context, slots) = batch.into_slots(); + let items = slots + .into_iter() + .rev() + .map(|(index, request)| { + self.time.advance_to( + self.time + .now() + .checked_add(Duration::from_millis(1)) + .expect("completion instant"), + ); + let outcome = if request.request().method() == Method::DELETE { + Err(EdgeError::bad_gateway("mock failure")) + } else { + test_head(request, StatusCode::OK, Body::empty()) + .and_then(|head| block_on(head.finish())) + }; + OutboundBatchDriverEvent::Item { + index, + completed_at: context.clock().now(), + outcome, + } + }) + .collect::>(); + OutboundBatch::from_driver(context, stream::iter(items), no_owner()) + } + + fn time(&self) -> OutboundTime { + self.time.pair() + } + + fn validate_supported_options( + &self, + _request: &OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + Ok(()) + } +} + +impl TestTime { + fn advance_to(&self, now: MonotonicInstant) { + *self.now.lock().expect("fake clock") = now; + for waker in take(&mut *self.waiters.lock().expect("fake waiters")) { + waker.wake(); + } + } + + fn new(now: MonotonicInstant) -> Self { + Self { + now: Arc::new(Mutex::new(now)), + waiters: Arc::default(), + } + } + + fn now(&self) -> MonotonicInstant { + *self.now.lock().expect("fake clock") + } + + fn pair(&self) -> OutboundTime { + let now = Arc::clone(&self.now); + let waiters = Arc::clone(&self.waiters); + OutboundTime::new( + MonotonicClock::new(move || *now.lock().expect("fake clock")), + move |deadline, clock| { + let waiters = Arc::clone(&waiters); + Box::pin(poll_fn(move |cx| { + if deadline.is_expired_at(clock.now()) { + Poll::Ready(Ok(())) + } else { + waiters + .lock() + .expect("fake waiters") + .push(cx.waker().clone()); + Poll::Pending + } + })) + }, + ) + } +} + +impl BatchFixture { + fn context(&self, count: usize) -> OutboundBatchContext { + let request = OutboundRequest::get("https://example.com").expect("request"); + let budget = dispatch_budget(&request, self.started_at, Some(self.cutoff)).expect("budget"); + OutboundBatchContext::new( + self.started_at, + self.time.pair(), + self.cutoff, + vec![Some(budget); count], + Vec::new(), + super::OutboundScope::capture(Rc::new(super::IndependentOutboundProgress)), + ) + } + + fn new() -> Self { + let started_at = MonotonicInstant::now(); + Self { + started_at, + cutoff: Deadline::at_instant( + started_at + .checked_add(Duration::from_secs(1)) + .expect("cutoff instant"), + ), + time: TestTime::new(started_at), + } + } + + fn observation(&self, index: usize) -> OutboundBatchDriverEvent { + OutboundBatchDriverEvent::Item { + index, + completed_at: self.time.now(), + outcome: Ok(response_with_body(Body::empty())), + } + } +} + +fn marker_head(request: OutboundDispatchRequest) -> Result { + if request.request().proxy_marker_setting() == OutboundProxyMarker::Inherit { + return Err(EdgeError::internal(anyhow::anyhow!( + "client failed to resolve marker policy" + ))); + } + test_head(request, StatusCode::OK, Body::empty()) +} + +fn test_head( + request: OutboundDispatchRequest, + status: StatusCode, + body: Body, +) -> Result { + let (_parts, context) = request.into_parts(); + OutboundHead::new( + status, + OutboundHeaderInput::new( + "test", + HeaderMap::new(), + OutboundHeaderFidelity::AdapterVisibleFields, + ), + match body.into_stream() { + Err(body) => Box::pin(stream::once(ready(Ok(body + .into_bytes() + .expect("buffered body"))))), + Ok(source) => source, + }, + context, + no_owner(), + ) +} + +fn no_owner() -> OutboundExchangeOwner { + OutboundExchangeOwner::new(|| {}) +} + +#[test] +fn proxy_marker_overwrite_and_normalization_precedence_are_unchanged() { + for setting in [ + OutboundProxyMarker::Inherit, + OutboundProxyMarker::On, + OutboundProxyMarker::Off, + ] { + for nominated in [false, true] { + let mut headers = HeaderMap::new(); + headers.append(PROXY_HEADER, HeaderValue::from_static("upstream")); + headers.append(PROXY_HEADER, HeaderValue::from_static("other")); + if nominated { + headers.insert("connection", HeaderValue::from_static(PROXY_HEADER)); + } + let mut limiter = ResponseHeaderLimiter::new(None, None); + limiter.observe(&headers).expect("upstream accounting"); + normalize_response_headers(&Method::GET, StatusCode::OK, &mut headers) + .expect("normalize"); + insert_proxy_header( + &mut headers, + &mut limiter, + setting, + HeaderValue::from_static("test"), + ) + .expect("marker policy"); + let values = headers + .get_all(PROXY_HEADER) + .iter() + .map(HeaderValue::as_bytes) + .collect::>(); + let expected = if setting != OutboundProxyMarker::Off { + vec![b"test".as_slice()] + } else if nominated { + vec![] + } else { + vec![b"upstream".as_slice(), b"other".as_slice()] + }; + assert_eq!(values, expected); + } + } +} + +#[test] +fn proxy_marker_request_setting_round_trips() { + let request = OutboundRequest::get("https://example.com").expect("request"); + assert_eq!(request.proxy_marker_setting(), OutboundProxyMarker::Inherit); + let setting: crate::OutboundProxyMarker = OutboundProxyMarker::Off; + let parts = request.proxy_marker(setting).into_parts(); + assert_eq!(parts.proxy_marker, setting); + let request = OutboundRequest::from_parts(parts).expect("round trip"); + assert_eq!(request.proxy_marker_setting(), setting); + assert_eq!( + request + .proxy_marker(OutboundProxyMarker::On) + .proxy_marker_setting(), + OutboundProxyMarker::On + ); +} + +#[test] +fn proxy_marker_client_resolves_overrides_for_supplied_send_and_batch() { + let default_client = HttpClient::with_client(MarkerClient); + let default_arc = HttpClient::new(Arc::new(MarkerClient)); + assert!(default_client.proxy_marker_enabled()); + assert!(default_arc.proxy_marker_enabled()); + let off_client = default_client.clone().with_proxy_marker(false); + assert!( + default_client.proxy_marker_enabled(), + "configuring a clone must not mutate the original handle" + ); + assert!(!off_client.proxy_marker_enabled()); + for client in [default_client, off_client] { + for setting in [ + OutboundProxyMarker::Inherit, + OutboundProxyMarker::On, + OutboundProxyMarker::Off, + ] { + let expected = match setting { + OutboundProxyMarker::Inherit => client.proxy_marker_enabled(), + OutboundProxyMarker::On => true, + OutboundProxyMarker::Off => false, + }; + let request = OutboundRequest::get("https://example.com") + .expect("request") + .proxy_marker(setting); + assert_eq!(client.effective_proxy_marker_enabled(&request), expected); + assert_eq!( + request.proxy_marker_setting(), + setting, + "inspection is immutable" + ); + let response = block_on(client.send(request)).expect("supplied response"); + assert_eq!(response.headers().contains_key(PROXY_HEADER), expected); + } + let requests = [ + OutboundProxyMarker::Inherit, + OutboundProxyMarker::On, + OutboundProxyMarker::Off, + ] + .into_iter() + .map(|setting| { + OutboundRequest::get("https://example.com") + .expect("request") + .proxy_marker(setting) + }) + .collect(); + let results = + block_on(client.send_all_until(requests, Deadline::after(Duration::from_secs(1)))) + .expect("batch"); + for (slot, expected) in + results + .into_parts() + .0 + .into_iter() + .zip([client.proxy_marker_enabled(), true, false]) + { + let OutboundCollectedSlot::Resolved(result) = slot else { + panic!("terminal slot"); + }; + let response = result.outcome.expect("response"); + assert_eq!(response.headers().contains_key(PROXY_HEADER), expected); + } + } +} + +#[test] +fn proxy_marker_inspector_neither_validates_nor_polls_body() { + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let body = Body::from_stream(stream::poll_fn(move |_context| { + counted.set(counted.get() + 1); + Poll::Pending + })); + let request = OutboundRequest::get("https://example.com") + .expect("request") + .body(body) + .max_brotli_window_bits(0); + let client = HttpClient::with_client(MarkerClient).with_proxy_marker(false); + assert!(!client.effective_proxy_marker_enabled(&request)); + assert_eq!(request.proxy_marker_setting(), OutboundProxyMarker::Inherit); + assert_eq!(polls.get(), 0); +} + +#[test] +fn proxy_marker_off_preserves_upstream_and_consumes_no_header_budget() { + let mut upstream = HeaderMap::new(); + upstream.append(PROXY_HEADER, HeaderValue::from_static("upstream")); + upstream.append(PROXY_HEADER, HeaderValue::from_static("other")); + upstream.insert("x", HeaderValue::from_static("y")); + let bytes = upstream + .iter() + .map(|(name, value)| { + u64::try_from(name.as_str().len() + value.as_bytes().len()).expect("header size") + }) + .sum::(); + for adapter in ["axum", "cloudflare", "fastly", "spin"] { + for (byte_cap, count_cap, reason) in [ + (Some(bytes), None, ResponseLimitReason::HeaderBytes), + (None, Some(3), ResponseLimitReason::HeaderCount), + ] { + for setting in [ + OutboundProxyMarker::Off, + OutboundProxyMarker::On, + OutboundProxyMarker::Inherit, + ] { + let mut headers = upstream.clone(); + let mut limiter = ResponseHeaderLimiter::new(byte_cap, count_cap); + limiter.observe(&headers).expect("exact upstream limit"); + let result = insert_proxy_header( + &mut headers, + &mut limiter, + setting, + HeaderValue::from_static(adapter), + ); + if setting == OutboundProxyMarker::Off { + result.expect("off adds no synthetic charge"); + } else { + assert!( + matches!(result, Err(EdgeError::ResponseTooLarge { reason: actual, .. }) if actual == reason) + ); + } + assert_eq!( + headers, upstream, + "off and failed insertions must leave all upstream values intact" + ); + } + } + } +} + +#[test] +fn ready_driver_retains_terminal_timestamps_before_delivering_preflight_failures() { + let fixture = BatchFixture::new(); + fixture + .time + .advance_to(fixture.started_at + Duration::from_millis(2)); + let context = OutboundBatchContext::new( + fixture.started_at, + fixture.time.pair(), + fixture.cutoff, + vec![ + None, + Some( + dispatch_budget( + &OutboundRequest::get("https://example.com").expect("request"), + fixture.started_at, + Some(fixture.cutoff), + ) + .expect("budget"), + ), + ], + vec![( + 0, + fixture.time.now(), + Err(EdgeError::bad_request("preflight")), + )], + super::OutboundScope::capture(Rc::new(super::IndependentOutboundProgress)), + ); + fixture + .time + .advance_to(fixture.started_at + Duration::from_millis(5)); + let event = OutboundBatchDriverEvent::Item { + index: 1, + completed_at: fixture.time.now(), + outcome: Err(EdgeError::bad_request("ready slot")), + }; + let mut batch = OutboundBatch::from_driver(context, stream::iter([event]), no_owner()); + let OutboundBatchNext::Item(first) = block_on(batch.next()).expect("preflight") else { + panic!("expected preflight item"); + }; + assert_eq!(first.index(), 0); + assert_eq!(first.result().elapsed, Duration::from_millis(2)); + fixture + .time + .advance_to(fixture.started_at + Duration::from_secs(2)); + let OutboundBatchNext::Item(second) = block_on(batch.next()).expect("retained result") else { + panic!("expected retained result"); + }; + assert_eq!(second.index(), 1); + assert_eq!(second.result().elapsed, Duration::from_millis(5)); + assert_eq!( + second + .result() + .outcome + .as_ref() + .expect_err("retained failure") + .message(), + "ready slot" + ); + assert!(matches!( + block_on(batch.next()).expect("EOF"), + OutboundBatchNext::Finished(OutboundBatchTermination::Completed) + )); +} + +#[test] +fn ready_driver_cutoff_depends_on_observation_not_timeout_attribution() { + let fixture = BatchFixture::new(); + fixture + .time + .advance_to(fixture.started_at + Duration::from_millis(5)); + let early = OutboundBatchDriverEvent::Item { + index: 1, + completed_at: fixture.time.now(), + outcome: Err(EdgeError::gateway_timeout_caused( + "phase timeout", + BudgetSource::BatchCutoff, + )), + }; + fixture.time.advance_to(fixture.cutoff.instant()); + let at_cutoff = OutboundBatchDriverEvent::Item { + index: 0, + completed_at: fixture.time.now(), + outcome: Err(EdgeError::bad_request("late")), + }; + let batch = OutboundBatch::from_driver( + fixture.context(2), + stream::iter([early, at_cutoff, OutboundBatchDriverEvent::Cutoff]), + no_owner(), + ); + let result = block_on(batch.collect()).expect("observation_cutoff"); + assert_eq!(result.termination(), OutboundBatchTermination::Cutoff); + assert!(matches!( + result.slots().first(), + Some(OutboundCollectedSlot::Unresolved) + )); + let OutboundCollectedSlot::Resolved(early_result) = &result.slots()[1] else { + panic!("early slot retained"); + }; + assert!(matches!( + early_result.outcome, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::BatchCutoff, + .. + }) + )); +} + +#[test] +fn ready_driver_orders_controlled_completions_and_drops_cancelled_work() { + let fixture = BatchFixture::new(); + let drops = Rc::new(Cell::new(0)); + let signal = BodyDropSignal(Rc::clone(&drops)); + let (send_first, receive_first) = oneshot::channel::<()>(); + let (send_second, receive_second) = oneshot::channel::<()>(); + let first_time = fixture.time.clone(); + let first = async move { + let _signal = signal; + receive_first.await.expect("first completion"); + OutboundBatchDriverEvent::Item { + index: 0, + completed_at: first_time.now(), + outcome: Err(EdgeError::bad_request("first")), + } + } + .boxed_local(); + let second_time = fixture.time.clone(); + let second = async move { + receive_second.await.expect("second completion"); + OutboundBatchDriverEvent::Item { + index: 1, + completed_at: second_time.now(), + outcome: Err(EdgeError::bad_request("second")), + } + } + .boxed_local(); + let mut batch = OutboundBatch::from_driver( + fixture.context(2), + stream::FuturesUnordered::from_iter([first, second]), + no_owner(), + ); + fixture + .time + .advance_to(fixture.started_at + Duration::from_millis(3)); + send_second.send(()).expect("second ready"); + let OutboundBatchNext::Item(item) = block_on(batch.next()).expect("second completes first") + else { + panic!("expected completion"); + }; + assert_eq!(item.index(), 1); + assert_eq!(item.result().elapsed, Duration::from_millis(3)); + assert_eq!(batch.cancel(), vec![0]); + assert_eq!(drops.get(), 1); + assert!(send_first.send(()).is_err()); +} + +#[test] +fn ready_driver_validates_empty_and_premature_drivers() { + let fixture = BatchFixture::new(); + let completed = block_on( + OutboundBatch::from_driver(fixture.context(0), stream::empty(), no_owner()).collect(), + ) + .expect("empty completion"); + assert_eq!(completed.termination(), OutboundBatchTermination::Completed); + let budget = dispatch_budget( + &OutboundRequest::get("https://example.com").expect("request"), + fixture.started_at, + Some(fixture.cutoff), + ) + .expect("budget"); + let context = OutboundBatchContext::new( + fixture.started_at, + fixture.time.pair(), + fixture.cutoff, + vec![Some(budget), None], + vec![( + 1, + fixture.time.now(), + Err(EdgeError::bad_request("preflight")), + )], + super::OutboundScope::capture(Rc::new(super::IndependentOutboundProgress)), + ); + let failure = + block_on(OutboundBatch::from_driver(context, stream::empty(), no_owner()).collect()) + .expect_err("missing future"); + assert!(matches!(failure.error(), EdgeError::Internal { .. })); + assert!(matches!( + failure.slots().get(1), + Some(OutboundCollectedSlot::Resolved(_)) + )); +} + +#[test] +fn ready_driver_cutoff_releases_pending_work_before_reporting_finished() { + let fixture = BatchFixture::new(); + let drops = Rc::new(Cell::new(0)); + let signal = BodyDropSignal(Rc::clone(&drops)); + let (cancelled, pending_work) = oneshot::channel::<()>(); + fixture.time.advance_to(fixture.cutoff.instant()); + let late = OutboundBatchDriverEvent::Item { + index: 0, + completed_at: fixture.time.now(), + outcome: Err(EdgeError::bad_request("late completion")), + }; + let mut batch = OutboundBatch::from_driver( + fixture.context(2), + stream::iter([late, OutboundBatchDriverEvent::Cutoff]), + OutboundExchangeOwner::new(move || { + drop(signal); + drop(pending_work); + }), + ); + assert!(matches!( + block_on(batch.next()).expect("cutoff"), + OutboundBatchNext::Finished(OutboundBatchTermination::Cutoff) + )); + assert_eq!( + drops.get(), + 1, + "finished cutoff must release pending work even while batch remains owned" + ); + assert_eq!(batch.cancel(), vec![0, 1]); + assert_eq!(drops.get(), 1); + assert!( + cancelled.send(()).is_err(), + "pending work released at cutoff" + ); +} + +#[test] +fn batch_terminal_and_poisoned_states_release_the_driver_while_retained() { + let fixture = BatchFixture::new(); + for (count, events) in [ + (1, vec![OutboundBatchDriverEvent::Cutoff]), + ( + 1, + vec![OutboundBatchDriverEvent::Failed(EdgeError::internal( + anyhow::anyhow!("fixture failure"), + ))], + ), + (1, vec![fixture.observation(1)]), + (1, vec![fixture.observation(0), fixture.observation(0)]), + (1, vec![]), + (0, vec![]), + (0, vec![OutboundBatchDriverEvent::Cutoff]), + ] { + let drops = Rc::new(Cell::new(0)); + let signal = BodyDropSignal(Rc::clone(&drops)); + let mut remaining = events.into_iter(); + let driver = stream::poll_fn(move |_context| { + let _keep_driver_owned = &signal; + Poll::Ready(remaining.next()) + }); + fixture.time.advance_to(fixture.cutoff.instant()); + let mut batch = OutboundBatch::from_driver(fixture.context(count), driver, no_owner()); + while matches!(block_on(batch.next()), Ok(OutboundBatchNext::Item(_))) {} + assert_eq!( + drops.get(), + 1, + "a retained terminal batch must not retain its driver" + ); + let _terminal_again = block_on(batch.next()); + drop(batch); + assert_eq!(drops.get(), 1, "driver must release exactly once"); + } +} + +fn body_policy() -> ResponseBodyPolicy { + ResponseBodyPolicy { + max_brotli_window_bits: 24, + max_chunk_bytes: None, + max_decoded_response_bytes: Some(32), + max_decoder_bytes: super::DEFAULT_MAX_DECODER_BYTES, + max_encoded_response_bytes: Some(256), + response_decoding: ResponseDecoding::Decode, + response_mode: ResponseMode::Buffered { max_bytes: 32 }, + status: StatusCode::OK, + } +} + +#[test] +fn response_decoding_request_option_round_trips() { + let request = OutboundRequest::get("https://example.com").expect("request"); + assert_eq!( + request.response_decoding_setting(), + ResponseDecoding::Decode + ); + let parts = request + .response_decoding(ResponseDecoding::Original) + .into_parts(); + assert_eq!(parts.response_decoding, ResponseDecoding::Original); + let request = OutboundRequest::from_parts(parts).expect("round trip"); + assert_eq!( + request.response_decoding_setting(), + ResponseDecoding::Original + ); + assert_eq!( + request + .response_decoding(ResponseDecoding::Decode) + .response_decoding_setting(), + ResponseDecoding::Decode + ); + let choice: crate::ResponseDecoding = ResponseDecoding::Original; + assert_eq!(choice, ResponseDecoding::Original); +} + +#[test] +fn original_coding_skips_decoder_but_keeps_transport_caps() { + for coding in ["gzip", "deflate", "br", "unknown", "gzip, br", "gzip;"] { + for response_mode in [ + ResponseMode::Buffered { max_bytes: 4 }, + ResponseMode::Streamed, + ] { + for (choice, status) in [ + (ResponseDecoding::Original, StatusCode::OK), + (ResponseDecoding::Original, StatusCode::PARTIAL_CONTENT), + (ResponseDecoding::Decode, StatusCode::PARTIAL_CONTENT), + ] { + let mut headers = HeaderMap::new(); + headers.insert("content-encoding", HeaderValue::from_static(coding)); + headers.insert("content-length", HeaderValue::from_static("4")); + headers.insert("content-range", HeaderValue::from_static("bytes 0-3/8")); + let original = headers.clone(); + let source = stream::iter([Ok(Bytes::from_static(b"raw!"))]).boxed_local(); + let policy = ResponseBodyPolicy { + max_decoded_response_bytes: Some(1), + max_decoder_bytes: 0, + max_encoded_response_bytes: Some(4), + max_chunk_bytes: NonZeroU64::new(2), + response_decoding: choice, + response_mode, + status, + ..body_policy() + }; + let output = + apply_response_body_policy(source, &mut headers, policy).expect("raw policy"); + assert_eq!(headers, original, "{coding} {choice:?} {status}"); + let chunks = block_on(output.collect::>()); + let mut bytes = Vec::new(); + for chunk in chunks { + let chunk = chunk.expect("no decoder and no decoded cap"); + assert!(chunk.len() <= 2); + bytes.extend_from_slice(&chunk); + } + assert_eq!(bytes, b"raw!"); + } + } + } +} + +#[test] +fn original_coding_declared_caps_fail_before_polling() { + for (encoded_cap, response_mode, reason) in [ + ( + 3, + ResponseMode::Buffered { max_bytes: 4 }, + ResponseLimitReason::EncodedBody, + ), + ( + 4, + ResponseMode::Buffered { max_bytes: 3 }, + ResponseLimitReason::BufferedBody, + ), + (3, ResponseMode::Streamed, ResponseLimitReason::EncodedBody), + ] { + for (choice, status) in [ + (ResponseDecoding::Original, StatusCode::OK), + (ResponseDecoding::Original, StatusCode::PARTIAL_CONTENT), + (ResponseDecoding::Decode, StatusCode::PARTIAL_CONTENT), + ] { + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let source = stream::poll_fn(move |_context| { + counted.set(counted.get() + 1); + Poll::Ready(None) + }) + .boxed_local(); + let mut headers = HeaderMap::new(); + headers.insert("content-encoding", HeaderValue::from_static("gzip")); + headers.insert("content-length", HeaderValue::from_static("4")); + let original = headers.clone(); + let policy = ResponseBodyPolicy { + max_encoded_response_bytes: Some(encoded_cap), + response_decoding: choice, + response_mode, + status, + ..body_policy() + }; + let error = apply_response_body_policy(source, &mut headers, policy) + .err() + .expect("declared cap"); + assert!( + matches!(error, EdgeError::ResponseTooLarge { reason: actual, .. } if actual == reason) + ); + assert_eq!(polls.get(), 0); + assert_eq!(headers, original); + } + } +} + +#[test] +fn original_coding_observed_caps_and_source_errors_are_preserved() { + for response_mode in [ + ResponseMode::Buffered { max_bytes: 4 }, + ResponseMode::Streamed, + ] { + let policy = ResponseBodyPolicy { + response_decoding: ResponseDecoding::Original, + response_mode, + max_encoded_response_bytes: Some(3), + ..body_policy() + }; + let mut headers = HeaderMap::new(); + headers.insert("content-encoding", HeaderValue::from_static("gzip")); + let source = stream::iter([Ok(Bytes::from_static(b"raw!"))]).boxed_local(); + let output = apply_response_body_policy(source, &mut headers, policy).expect("lazy policy"); + assert!(matches!( + block_on(collect_response_stream(output, 4)), + Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::EncodedBody, + .. + }) + )); + + let source = stream::iter([Err(EdgeError::gateway_timeout_caused( + "source timeout", + BudgetSource::RequestDeadline, + ))]) + .boxed_local(); + let output = apply_response_body_policy(source, &mut headers, policy).expect("lazy policy"); + assert!(matches!( + block_on(collect_response_stream(output, 4)), + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + } + let mut headers = HeaderMap::new(); + headers.insert("content-encoding", HeaderValue::from_static("br")); + let source = stream::iter([Ok(Bytes::from_static(b"raw!"))]).boxed_local(); + let policy = ResponseBodyPolicy { + response_decoding: ResponseDecoding::Original, + ..body_policy() + }; + let output = apply_response_body_policy(source, &mut headers, policy).expect("raw policy"); + assert!(matches!( + block_on(collect_response_stream(output, 3)), + Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::BufferedBody, + .. + }) + )); +} + +#[test] +fn original_identity_retains_declared_and_observed_decoded_caps() { + for choice in [ResponseDecoding::Decode, ResponseDecoding::Original] { + for status in [StatusCode::OK, StatusCode::PARTIAL_CONTENT] { + for response_mode in [ + ResponseMode::Buffered { max_bytes: 20 }, + ResponseMode::Streamed, + ] { + for declared in [false, true] { + for (bytes, exceeds) in [ + (b"0123456789".as_slice(), false), + (b"0123456789x".as_slice(), true), + ] { + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let mut chunk = Some(Bytes::copy_from_slice(bytes)); + let source = stream::poll_fn(move |_context| { + counted.set(counted.get() + 1); + Poll::Ready(chunk.take().map(Ok)) + }) + .boxed_local(); + let mut headers = HeaderMap::new(); + if declared { + headers.insert( + "content-length", + HeaderValue::from_str(&bytes.len().to_string()).expect("length"), + ); + } + let policy = ResponseBodyPolicy { + response_decoding: choice, + status, + response_mode, + max_decoded_response_bytes: Some(10), + max_encoded_response_bytes: Some(20), + ..body_policy() + }; + let result = apply_response_body_policy(source, &mut headers, policy) + .and_then(|output| block_on(collect_response_stream(output, 20))); + if exceeds { + assert!(matches!( + result, + Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::DecodedBody, + .. + }) + )); + } else { + assert_eq!(result.expect("exact cap"), bytes); + } + if declared && exceeds { + assert_eq!(polls.get(), 0); + } + } + } + } + } + } +} + +#[test] +fn body_policy_rejects_declared_limits_without_polling_or_rewriting_headers() { + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let source = stream::poll_fn(move |_context| { + counted.set(counted.get() + 1); + Poll::Ready(None) + }) + .boxed_local(); + let mut headers = HeaderMap::new(); + headers.insert("content-encoding", HeaderValue::from_static("gzip")); + headers.insert("content-length", HeaderValue::from_static("257")); + let error = apply_response_body_policy(source, &mut headers, body_policy()) + .err() + .expect("encoded preflight"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::EncodedBody, + .. + } + )); + assert_eq!(polls.get(), 0); + assert!(headers.contains_key("content-encoding")); + assert!(headers.contains_key("content-length")); +} + +#[test] +fn original_coding_preserves_normalization_precedence() { + for choice in [ResponseDecoding::Decode, ResponseDecoding::Original] { + for status in [StatusCode::OK, StatusCode::PARTIAL_CONTENT] { + for response_mode in [ + ResponseMode::Streamed, + ResponseMode::Buffered { max_bytes: 20 }, + ] { + let policy = ResponseBodyPolicy { + response_decoding: choice, + status, + response_mode, + max_decoded_response_bytes: Some(1), + ..body_policy() + }; + let mut headers = HeaderMap::new(); + headers.insert( + "connection", + HeaderValue::from_static("content-encoding, content-length, etag"), + ); + headers.insert("content-encoding", HeaderValue::from_static("gzip")); + headers.insert("content-length", HeaderValue::from_static("malformed")); + headers.insert("etag", HeaderValue::from_static("\"removed\"")); + headers.insert( + "last-modified", + HeaderValue::from_static("Wed, 21 Oct 2015 07:28:00 GMT"), + ); + normalize_response_headers(&Method::GET, status, &mut headers) + .expect("nominations before length validation"); + let normalized = headers.clone(); + let source = stream::iter([Ok(Bytes::from_static(b"ab"))]).boxed_local(); + let output = apply_response_body_policy(source, &mut headers, policy) + .expect("identity policy"); + assert_eq!(headers, normalized); + assert!(!headers.contains_key("etag")); + assert!(!headers.contains_key("content-encoding")); + assert!(!headers.contains_key("content-length")); + assert!(matches!( + block_on(collect_response_stream(output, 20)), + Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::DecodedBody, + .. + }) + )); + + let mut headers = HeaderMap::new(); + headers.insert( + "content-encoding", + HeaderValue::from_bytes(&[0xff]).expect("opaque coding"), + ); + headers.insert( + "etag", + HeaderValue::from_bytes(&[0xff]).expect("opaque etag"), + ); + normalize_response_headers(&Method::GET, status, &mut headers) + .expect("normalization"); + assert!(!headers.contains_key("etag")); + assert_eq!( + classify_content_encoding(&headers), + ContentEncoding::Passthrough(PassthroughReason::Malformed) + ); + let normalized = headers.clone(); + let source = stream::iter([Ok(Bytes::from_static(b"ab"))]).boxed_local(); + let output = apply_response_body_policy(source, &mut headers, policy) + .expect("malformed coding passthrough"); + assert_eq!(headers, normalized); + assert_eq!( + block_on(collect_response_stream(output, 20)).expect("raw body"), + b"ab".as_slice() + ); + + headers.insert("content-length", HeaderValue::from_static("malformed")); + assert!(matches!( + normalize_response_headers(&Method::GET, status, &mut headers), + Err(EdgeError::BadGateway { + reason: BadGatewayReason::Protocol, + .. + }) + )); + } + } + } +} + +#[test] +fn original_coding_retains_valid_compressed_bytes_and_representation_fields() { + let plain = b"decoded body"; + let mut gzip = GzEncoder::new(Vec::new(), flate2::Compression::default()); + gzip.write_all(plain).expect("gzip"); + let mut deflate = ZlibEncoder::new(Vec::new(), flate2::Compression::default()); + deflate.write_all(plain).expect("deflate"); + let mut brotli = Vec::new(); + let mut compressor = brotli::CompressorWriter::new(&mut brotli, 4096, 5, 21); + compressor.write_all(plain).expect("brotli"); + drop(compressor); + for (coding, encoded) in [ + ("gzip", gzip.finish().expect("gzip finish")), + ("deflate", deflate.finish().expect("deflate finish")), + ("br", brotli), + ] { + for response_mode in [ + ResponseMode::Buffered { max_bytes: 256 }, + ResponseMode::Streamed, + ] { + for (response_decoding, status) in [ + (ResponseDecoding::Original, StatusCode::OK), + (ResponseDecoding::Original, StatusCode::PARTIAL_CONTENT), + (ResponseDecoding::Decode, StatusCode::PARTIAL_CONTENT), + ] { + let mut headers = HeaderMap::new(); + for (name, value) in [ + ("etag", "\"strong\""), + ("last-modified", "Wed, 21 Oct 2015 07:28:00 GMT"), + ("vary", "Accept-Encoding"), + ("content-range", "bytes 0-3/8"), + ("accept-ranges", "bytes"), + ("cache-control", "public, max-age=60"), + ("content-digest", "sha-256=:c2hhMjU2:"), + ] { + headers.insert(name, HeaderValue::from_static(value)); + } + headers.insert("content-encoding", HeaderValue::from_static(coding)); + headers.insert( + "content-length", + HeaderValue::from_str(&encoded.len().to_string()).expect("length"), + ); + normalize_response_headers(&Method::GET, status, &mut headers) + .expect("normalization"); + let normalized = headers.clone(); + let policy = ResponseBodyPolicy { + response_decoding, + status, + response_mode, + max_decoder_bytes: 0, + max_decoded_response_bytes: Some(1), + ..body_policy() + }; + let source = stream::iter([Ok(Bytes::copy_from_slice(&encoded))]).boxed_local(); + let output = + apply_response_body_policy(source, &mut headers, policy).expect("raw policy"); + assert_eq!(headers, normalized); + assert_eq!( + block_on(collect_response_stream(output, 256)).expect("original bytes"), + encoded + ); + } + } + } +} + +#[test] +fn body_policy_decodes_recognized_codings_before_rechunking() { + let plain = b"decoded body"; + let mut gzip = GzEncoder::new(Vec::new(), flate2::Compression::default()); + gzip.write_all(plain).expect("gzip"); + let mut deflate = ZlibEncoder::new(Vec::new(), flate2::Compression::default()); + deflate.write_all(plain).expect("deflate"); + let mut brotli = Vec::new(); + let mut compressor = brotli::CompressorWriter::new(&mut brotli, 4096, 5, 21); + compressor.write_all(plain).expect("brotli"); + drop(compressor); + for (coding, encoded) in [ + ("gzip", gzip.finish().expect("gzip finish")), + ("deflate", deflate.finish().expect("deflate finish")), + ("br", brotli), + ] { + let mut headers = HeaderMap::new(); + headers.insert("content-encoding", HeaderValue::from_static(coding)); + headers.insert( + "content-length", + HeaderValue::from_str(&encoded.len().to_string()).expect("length"), + ); + let source = stream::iter([Ok(Bytes::from(encoded))]).boxed_local(); + let mut policy = body_policy(); + policy.max_chunk_bytes = NonZeroU64::new(2); + let output = apply_response_body_policy(source, &mut headers, policy).expect("policy"); + assert!(!headers.contains_key("content-encoding")); + assert!(!headers.contains_key("content-length")); + let chunks = block_on(output.collect::>()); + let mut collected = Vec::new(); + for chunk in chunks { + let bytes = chunk.expect("decoded chunk"); + assert!(bytes.len() <= 2); + collected.extend_from_slice(&bytes); + } + assert_eq!(collected, plain); + } +} + +#[test] +fn provider_diagnostics_survive_response_body_policy_and_collection() { + use crate::error_detail::{FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase}; + + let detail = ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: FastlyFailureCause::IoError { + kind: ErrorKind::UnexpectedEof, + }, + }; + let plain = b"diagnostic body"; + let mut gzip = GzEncoder::new(Vec::new(), flate2::Compression::default()); + gzip.write_all(plain).expect("gzip"); + let mut deflate = ZlibEncoder::new(Vec::new(), flate2::Compression::default()); + deflate.write_all(plain).expect("deflate"); + let mut brotli = Vec::new(); + let mut compressor = brotli::CompressorWriter::new(&mut brotli, 4096, 5, 21); + compressor.write_all(plain).expect("brotli"); + drop(compressor); + for (coding, encoded) in [ + ("identity", plain.to_vec()), + ("gzip", gzip.finish().expect("gzip finish")), + ("deflate", deflate.finish().expect("deflate finish")), + ("br", brotli), + ] { + for response_decoding in [ResponseDecoding::Decode, ResponseDecoding::Original] { + for status in [StatusCode::OK, StatusCode::PARTIAL_CONTENT] { + for error in [ + EdgeError::bad_request_with_detail("source rejected", detail), + EdgeError::bad_gateway_with_detail( + "source failed", + BadGatewayReason::Transport, + detail, + ), + EdgeError::gateway_timeout_with_detail( + "source timed out", + BudgetSource::RequestDeadline, + detail, + ), + EdgeError::internal_with_detail(IoError::from_raw_os_error(42), detail), + ] { + let expected_display = error.to_string(); + let expected_message = error.message(); + let expected_kind = error.kind(); + let expected_status = error.status(); + let source = stream::iter([Ok(Bytes::copy_from_slice(&encoded)), Err(error)]) + .boxed_local(); + let mut headers = HeaderMap::new(); + headers.insert("content-encoding", HeaderValue::from_static(coding)); + let mut policy = body_policy(); + policy.max_chunk_bytes = NonZeroU64::new(2); + policy.max_encoded_response_bytes = Some(512); + policy.max_decoded_response_bytes = Some(512); + policy.response_mode = ResponseMode::Buffered { max_bytes: 512 }; + policy.response_decoding = response_decoding; + policy.status = status; + let output = + apply_response_body_policy(source, &mut headers, policy).expect("policy"); + let retained = block_on( + response_with_body(Body::from_stream(output)).into_bytes_bounded(512), + ) + .expect_err("original source failure"); + assert_eq!(retained.provider_failure_detail(), Some(&detail)); + assert_eq!(retained.to_string(), expected_display); + assert_eq!(retained.message(), expected_message); + assert_eq!(retained.kind(), expected_kind); + assert_eq!(retained.status(), expected_status); + if let EdgeError::BadGateway { reason, .. } = &retained { + assert_eq!(*reason, BadGatewayReason::Transport); + } + if let EdgeError::GatewayTimeout { cause, .. } = &retained { + assert_eq!(*cause, BudgetSource::RequestDeadline); + } + if let Some(inner) = retained.inner() { + assert_eq!( + inner + .downcast_ref::() + .expect("concrete source") + .raw_os_error(), + Some(42_i32) + ); + } + } + } + } + } +} + +#[test] +fn provider_diagnostics_survive_bounded_collection_deadline() { + use crate::error_detail::{FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase}; + + let detail = ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: FastlyFailureCause::ConnectionTerminated, + }; + for via_response in [false, true] { + for (item, expected) in [ + ( + Some(Err(EdgeError::bad_gateway_with_detail( + "source failed", + BadGatewayReason::Transport, + detail, + ))), + Some(&detail), + ), + (Some(Err(EdgeError::bad_gateway("source failed"))), None), + (Some(Ok(Bytes::from_static(b"ready"))), None), + (None, None), + ] { + let start = MonotonicInstant::now(); + let end = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let deadline = Deadline::at_instant(end); + let samples = AtomicUsize::new(0); + let live_samples = if via_response { 2_usize } else { 1_usize }; + let clock = MonotonicClock::new(move || { + if samples.fetch_add(1, Ordering::SeqCst) < live_samples { + start + } else { + end + } + }); + let source = stream::iter(item).boxed_local(); + let result = if via_response { + block_on( + OutboundResponse::new( + Method::GET, + StatusCode::OK, + HeaderMap::new(), + Body::from_stream(source), + clock, + super::ContentTypeProvenance::Unknown, + ) + .into_bytes_bounded_until(32, deadline), + ) + } else { + block_on(super::collect_response_stream_until_with_clock( + source, 32, deadline, &clock, + )) + }; + let error = result.expect_err("post-ready deadline wins"); + assert_eq!(error.provider_failure_detail(), expected); + assert_eq!(error.message(), "response body deadline expired"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::Unspecified, + .. + } + )); + } + } +} + +#[test] +fn body_policy_passthrough_preserves_headers_and_bypasses_decoded_limit_only() { + for coding in ["unknown", "gzip, br", "gzip;"] { + let mut headers = HeaderMap::new(); + headers.insert("content-encoding", HeaderValue::from_static(coding)); + headers.insert("content-length", HeaderValue::from_static("4")); + let source = stream::iter([Ok(Bytes::from_static(b"abcd"))]).boxed_local(); + let mut policy = body_policy(); + policy.max_decoded_response_bytes = Some(1); + let output = + apply_response_body_policy(source, &mut headers, policy).expect("passthrough policy"); + assert_eq!( + headers.get("content-encoding"), + Some(&HeaderValue::from_static(coding)) + ); + assert!(headers.contains_key("content-length")); + assert_eq!( + block_on(collect_response_stream(output, 4)).expect("passthrough"), + Bytes::from_static(b"abcd") + ); + } + let mut headers = HeaderMap::new(); + let mut policy = body_policy(); + policy.max_decoded_response_bytes = Some(1); + policy.max_chunk_bytes = NonZeroU64::new(1); + let source = stream::iter([Ok(Bytes::from_static(b"ab"))]).boxed_local(); + let mut output = apply_response_body_policy(source, &mut headers, policy).expect("identity"); + assert!(matches!( + block_on(output.next()), + Some(Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::DecodedBody, + .. + })) + )); +} + +#[test] +fn body_policy_encoded_limit_precedes_decoder_and_preserves_source_errors() { + let mut headers = HeaderMap::new(); + headers.insert("content-encoding", HeaderValue::from_static("gzip")); + let mut policy = body_policy(); + policy.max_encoded_response_bytes = Some(1); + let source = stream::iter([Ok(Bytes::from_static(b"invalid gzip"))]).boxed_local(); + let output = apply_response_body_policy(source, &mut headers, policy).expect("policy"); + assert!(matches!( + block_on(collect_response_stream(output, 32)), + Err(EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::EncodedBody, + .. + }) + )); + + for coding in ["identity", "gzip", "deflate", "br", "unknown"] { + let mut headers = HeaderMap::new(); + headers.insert("content-encoding", HeaderValue::from_static(coding)); + let source = stream::iter([Err(EdgeError::gateway_timeout_caused( + "source timeout", + BudgetSource::RequestDeadline, + ))]) + .boxed_local(); + let output = + apply_response_body_policy(source, &mut headers, body_policy()).expect("policy"); + assert!(matches!( + block_on(collect_response_stream(output, 32)), + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + }) + )); + } +} + +#[test] +fn batch_ledger_preserves_an_on_time_terminal_outcome() { + let mut fixture = BatchFixture::new(); + fixture.cutoff = Deadline::at_instant(fixture.started_at + Duration::from_millis(10)); + fixture + .time + .advance_to(fixture.started_at + Duration::from_millis(7)); + let event = fixture.observation(3); + let mut batch = + OutboundBatch::from_driver(fixture.context(4), stream::iter([event]), no_owner()); + fixture + .time + .advance_to(fixture.started_at + Duration::from_millis(20)); + let OutboundBatchNext::Item(item) = block_on(batch.next()).expect("on-time outcome") else { + panic!("expected on-time terminal outcome"); + }; + assert_eq!(item.index(), 3); + assert_eq!(item.result().elapsed, Duration::from_millis(7)); + let _response = item + .into_parts() + .1 + .outcome + .expect("successful on-time outcome"); +} + +#[test] +fn batch_ledger_rejects_an_outcome_observed_at_observation_cutoff() { + let mut fixture = BatchFixture::new(); + fixture.cutoff = Deadline::at_instant(fixture.started_at + Duration::from_millis(10)); + fixture.time.advance_to(fixture.cutoff.instant()); + let batch = OutboundBatch::from_driver( + fixture.context(1), + stream::iter([ + OutboundBatchDriverEvent::Item { + index: 0, + completed_at: fixture.time.now(), + outcome: Err(EdgeError::bad_gateway("late")), + }, + OutboundBatchDriverEvent::Cutoff, + ]), + no_owner(), + ); + let result = block_on(batch.collect()).expect("cutoff"); + assert_eq!(result.termination(), OutboundBatchTermination::Cutoff); + assert!(matches!( + result.slots(), + [OutboundCollectedSlot::Unresolved] + )); +} + +#[test] +fn batch_ledger_preserves_an_early_observation_cutoff_attributed_timeout() { + let mut fixture = BatchFixture::new(); + fixture.cutoff = Deadline::at_instant(fixture.started_at + Duration::from_millis(10)); + fixture + .time + .advance_to(fixture.started_at + Duration::from_millis(1)); + let event = OutboundBatchDriverEvent::Item { + index: 0, + completed_at: fixture.time.now(), + outcome: Err(EdgeError::gateway_timeout_caused( + "batch cutoff", + BudgetSource::BatchCutoff, + )), + }; + let mut batch = + OutboundBatch::from_driver(fixture.context(1), stream::iter([event]), no_owner()); + let OutboundBatchNext::Item(item) = block_on(batch.next()).expect("early phase timeout") else { + panic!("expected early phase timeout"); + }; + assert_eq!(item.result().elapsed, Duration::from_millis(1)); + assert!(matches!( + item.result().outcome, + Err(EdgeError::GatewayTimeout { + cause: BudgetSource::BatchCutoff, + .. + }) + )); +} + +#[test] +fn batch_ledger_maps_a_backward_terminal_sample_to_internal() { + let fixture = BatchFixture::new(); + let context = fixture.context(5); + let completed_at = fixture + .started_at + .checked_sub(Duration::from_millis(1)) + .expect("earlier instant"); + fixture.time.advance_to(completed_at); + let mut batch = + OutboundBatch::from_driver(context, stream::iter([fixture.observation(4)]), no_owner()); + let OutboundBatchNext::Item(item) = + block_on(batch.next()).expect("clock fault remains terminal") + else { + panic!("expected clock fault outcome"); + }; + assert_eq!(item.index(), 4); + assert_eq!(item.result().elapsed, Duration::ZERO); + let error = item + .into_parts() + .1 + .outcome + .expect_err("clock fault outcome"); + assert_eq!( + error.to_string(), + "internal error: monotonic clock moved backwards during outbound dispatch" + ); +} + +#[expect( + clippy::wildcard_enum_match_arm, + reason = "EdgeError is non-exhaustive and this assertion helper must reject future variants" +)] +fn bad_request_message(error: EdgeError) -> String { + match error { + EdgeError::BadRequest { message, .. } => message, + other => panic!("expected bad request, got {other:?}"), + } +} + +fn response_with_body(body: Body) -> OutboundResponse { + OutboundResponse::new( + Method::GET, + StatusCode::OK, + HeaderMap::new(), + body, + MonotonicClock::default(), + super::ContentTypeProvenance::Unknown, + ) +} + +#[test] +fn buffered_collection_limit_is_independent() { + let source = Body::from_stream(stream::iter([ + Ok(Bytes::from_static(b"ab")), + Ok(Bytes::from_static(b"cd")), + ])) + .into_stream() + .expect("stream"); + let bytes = block_on(collect_response_stream(source, 4)).expect("exact cap"); + assert_eq!(bytes, Bytes::from_static(b"abcd")); + + let error = block_on(response_with_body(Body::from("raw-passthrough")).into_bytes_bounded(3)) + .expect_err("final cap"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::BufferedBody, + .. + } + )); +} + +#[test] +fn dispatch_budget_rejects_expired_or_zero() { + let now = MonotonicInstant::now(); + let expired = OutboundRequest::get("https://example.com") + .expect("request") + .deadline(Deadline::at_instant(now)); + let error = dispatch_budget(&expired, now, None).expect_err("expired"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + } + )); + + let tied = OutboundRequest::get("https://example.com") + .expect("request") + .timeout(Duration::ZERO) + .deadline(Deadline::at_instant(now)); + let error = dispatch_budget(&tied, now, None).expect_err("zero tie"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::PerCallTimeout, + .. + } + )); +} + +#[test] +fn dispatch_budget_selects_and_attributes_minimum() { + let now = MonotonicInstant::now(); + + let default = OutboundRequest::get("https://example.com").expect("request"); + let budget = dispatch_budget(&default, now, None).expect("default budget"); + assert_eq!(budget.cause, BudgetSource::Default); + assert_eq!(budget.duration, DEFAULT_NO_DEADLINE_BUDGET); + assert_eq!( + budget.deadline.instant(), + now.checked_add(DEFAULT_NO_DEADLINE_BUDGET) + .expect("no overflow") + ); + + let timeout = OutboundRequest::get("https://example.com") + .expect("request") + .timeout(Duration::from_secs(3)); + let budget = dispatch_budget(&timeout, now, None).expect("timeout budget"); + assert_eq!(budget.cause, BudgetSource::PerCallTimeout); + assert_eq!(budget.duration, Duration::from_secs(3)); + + let deadline = Deadline::at_instant( + now.checked_add(Duration::from_secs(4)) + .expect("no overflow"), + ); + let request = OutboundRequest::get("https://example.com") + .expect("request") + .deadline(deadline); + let budget = dispatch_budget(&request, now, None).expect("deadline budget"); + assert_eq!(budget.cause, BudgetSource::RequestDeadline); + assert_eq!(budget.duration, Duration::from_secs(4)); + assert_eq!(budget.deadline.instant(), deadline.instant()); + + let timeout_wins = OutboundRequest::get("https://example.com") + .expect("request") + .timeout(Duration::from_secs(2)) + .deadline(deadline); + let budget = dispatch_budget(&timeout_wins, now, None).expect("timeout wins"); + assert_eq!(budget.cause, BudgetSource::PerCallTimeout); + assert_eq!(budget.duration, Duration::from_secs(2)); + + let deadline_wins = OutboundRequest::get("https://example.com") + .expect("request") + .timeout(Duration::from_secs(5)) + .deadline(deadline); + let budget = dispatch_budget(&deadline_wins, now, None).expect("deadline wins"); + assert_eq!(budget.cause, BudgetSource::RequestDeadline); + assert_eq!(budget.duration, Duration::from_secs(4)); + + let tied_deadline = Deadline::at_instant( + now.checked_add(Duration::from_secs(5)) + .expect("no overflow"), + ); + let tied = OutboundRequest::get("https://example.com") + .expect("request") + .timeout(Duration::from_secs(5)) + .deadline(tied_deadline); + let budget = dispatch_budget(&tied, now, None).expect("equal budget"); + assert_eq!(budget.cause, BudgetSource::PerCallTimeout); + + let huge_timeout = OutboundRequest::get("https://example.com") + .expect("request") + .timeout(Duration::MAX); + let budget = dispatch_budget(&huge_timeout, now, None).expect("clamped timeout"); + assert_eq!(budget.duration, DEADLINE_FAR_FUTURE); + + let far_deadline = Deadline::at_instant( + now.checked_add(Duration::from_hours(8_760)) + .expect("no overflow"), + ); + let far = OutboundRequest::get("https://example.com") + .expect("request") + .deadline(far_deadline); + let budget = dispatch_budget(&far, now, None).expect("clamped deadline"); + assert_eq!(budget.duration, DEADLINE_FAR_FUTURE); + assert_eq!(budget.cause, BudgetSource::RequestDeadline); + + let observation_cutoff = Deadline::at_instant( + now.checked_add(Duration::from_secs(1)) + .expect("cutoff instant"), + ); + let budget = dispatch_budget(&default, now, Some(observation_cutoff)).expect("batch cutoff"); + assert_eq!(budget.cause, BudgetSource::BatchCutoff); + assert_eq!(budget.deadline.instant(), observation_cutoff.instant()); + + let observation_cutoff_tie = OutboundRequest::get("https://example.com") + .expect("request") + .timeout(Duration::from_secs(1)) + .deadline(observation_cutoff); + let budget = dispatch_budget(&observation_cutoff_tie, now, Some(observation_cutoff)) + .expect("cutoff tie"); + assert_eq!(budget.cause, BudgetSource::BatchCutoff); + + let error = dispatch_budget(&default, now, Some(Deadline::at_instant(now))) + .expect_err("cutoff equality is expired"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::BatchCutoff, + .. + } + )); +} + +#[test] +fn encoded_limit_stops_before_decoder() { + let polls = Rc::new(Cell::new(0_usize)); + let observed = Rc::clone(&polls); + let source = Body::from_stream( + stream::iter([ + Ok(Bytes::from_static(b"four")), + Ok(Bytes::from_static(b"later")), + ]) + .inspect(move |_| observed.set(observed.get().saturating_add(1))), + ) + .into_stream() + .expect("stream"); + let mut limited = limit_encoded_stream(source, Some(3)); + let error = block_on(limited.next()) + .expect("limit result") + .expect_err("over limit"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::EncodedBody, + .. + } + )); + assert_eq!(polls.get(), 1); + assert!(block_on(limited.next()).is_none()); +} + +#[test] +fn terminal_stream_error_releases_source_before_yield() { + struct DropSignal(Rc>); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + let limit_drops = Rc::new(Cell::new(0_usize)); + let limit_signal = DropSignal(Rc::clone(&limit_drops)); + let limit_source = stream::poll_fn(move |_context| { + let _keep_signal_alive = &limit_signal; + Poll::Ready(Some(Ok(Bytes::from_static(b"over")))) + }) + .boxed_local(); + let mut limited = limit_encoded_stream(limit_source, Some(3)); + block_on(limited.next()) + .expect("limit result") + .expect_err("over limit"); + assert_eq!(limit_drops.get(), 1_usize); + + let source_drops = Rc::new(Cell::new(0_usize)); + let source_signal = DropSignal(Rc::clone(&source_drops)); + let source_error = stream::poll_fn(move |_context| { + let _keep_signal_alive = &source_signal; + Poll::Ready(Some(Err(EdgeError::bad_gateway("source failed")))) + }) + .boxed_local(); + let mut limited = limit_decoded_stream(source_error, Some(3)); + block_on(limited.next()) + .expect("source result") + .expect_err("source error"); + assert_eq!(source_drops.get(), 1_usize); +} + +#[test] +fn http_client_delegates_send_and_empty_batch_collection() { + let client = HttpClient::with_client(MockClient { + batch_calls: AtomicUsize::new(0), + send_calls: AtomicUsize::new(0), + time: TestTime::new(MonotonicInstant::now()), + }); + + let response = block_on( + client.send( + OutboundRequest::post("https://example.com") + .expect("request") + .body("request"), + ), + ) + .expect("response"); + assert_eq!(response.status(), StatusCode::CREATED); + assert_eq!(response.body().as_bytes(), Some(b"single".as_slice())); + + let empty = + block_on(client.send_all_until(Vec::new(), Deadline::after(Duration::from_secs(1)))) + .expect("valid empty batch"); + assert!(empty.slots().is_empty()); + assert_eq!(empty.termination(), OutboundBatchTermination::Completed); +} + +#[test] +fn batch_next_yields_completion_order_and_collection_restores_input_order() { + let client = HttpClient::with_client(MockClient { + batch_calls: AtomicUsize::new(0), + send_calls: AtomicUsize::new(0), + time: TestTime::new(MonotonicInstant::now()), + }); + let requests = vec![ + OutboundRequest::get("https://example.com/one").expect("one"), + OutboundRequest::new(Method::DELETE, Uri::from_static("https://example.com/two")) + .expect("two"), + OutboundRequest::post("https://example.com/three").expect("three"), + ]; + + let observation_cutoff = Deadline::after(Duration::from_secs(1)); + let mut batch = client.start_batch_until(requests, observation_cutoff); + let OutboundBatchNext::Item(first) = block_on(batch.next()).expect("valid batch driver") else { + panic!("expected first completion"); + }; + assert_eq!(first.index(), 2); + assert_eq!(first.result().elapsed, Duration::from_millis(1)); + let OutboundBatchNext::Item(second) = block_on(batch.next()).expect("valid batch driver") + else { + panic!("expected second completion"); + }; + assert_eq!(second.index(), 1); + assert!(matches!( + second.result().outcome, + Err(EdgeError::BadGateway { .. }) + )); + let OutboundBatchNext::Item(third) = block_on(batch.next()).expect("valid batch driver") else { + panic!("expected third completion"); + }; + assert_eq!(third.index(), 0); + assert!(matches!( + block_on(batch.next()).expect("valid batch driver"), + OutboundBatchNext::Finished(OutboundBatchTermination::Completed) + )); + + let requests = vec![ + OutboundRequest::get("https://example.com/one").expect("one"), + OutboundRequest::new(Method::DELETE, Uri::from_static("https://example.com/two")) + .expect("two"), + OutboundRequest::post("https://example.com/three").expect("three"), + ]; + let results = + block_on(client.send_all_until(requests, observation_cutoff)).expect("valid batch driver"); + assert_eq!(results.termination(), OutboundBatchTermination::Completed); + assert_eq!(results.slots().len(), 3); + let [ + OutboundCollectedSlot::Resolved(first), + OutboundCollectedSlot::Resolved(second), + OutboundCollectedSlot::Resolved(third), + ] = results.slots() + else { + panic!("all input slots must resolve"); + }; + assert_eq!(first.elapsed, Duration::from_millis(3)); + assert_eq!(second.elapsed, Duration::from_millis(2)); + assert_eq!(third.elapsed, Duration::from_millis(1)); + first.outcome.as_ref().expect("first slot success"); + assert!(matches!(second.outcome, Err(EdgeError::BadGateway { .. }))); + third.outcome.as_ref().expect("third slot success"); +} + +#[test] +fn batch_next_is_cancellation_safe_and_cancel_reports_unresolved_indices() { + fn poll_next_once(batch: &mut OutboundBatch) -> Poll> { + let next = batch.next(); + pin_mut!(next); + let waker = noop_waker_ref(); + let mut context = Context::from_waker(waker); + next.as_mut().poll(&mut context) + } + + let fixture = BatchFixture::new(); + let context = fixture.context(3); + let time = fixture.time.clone(); + let poll_count = Rc::new(Cell::new(0_u8)); + let observed_poll_count = Rc::clone(&poll_count); + let source = stream::poll_fn(move |_context| { + let current = observed_poll_count.get(); + observed_poll_count.set(current.saturating_add(1)); + if current == 1 { + Poll::Ready(Some(OutboundBatchDriverEvent::Item { + index: 1, + completed_at: time.now(), + outcome: Err(EdgeError::bad_gateway("second")), + })) + } else { + Poll::Pending + } + }); + let mut batch = OutboundBatch::from_driver(context, source, no_owner()); + + assert!(poll_next_once(&mut batch).is_pending()); + fixture + .time + .advance_to(fixture.started_at + Duration::from_millis(2)); + + let OutboundBatchNext::Item(item) = + block_on(batch.next()).expect("item survives cancelled next future") + else { + panic!("expected terminal item"); + }; + assert_eq!(item.index(), 1); + assert_eq!(item.result().elapsed, Duration::from_millis(2)); + assert_eq!(batch.cancel(), vec![0, 2]); +} + +#[test] +fn batch_collection_reports_cutoff_and_preserves_unresolved_slots() { + let fixture = BatchFixture::new(); + let event = fixture.observation(2); + fixture.time.advance_to(fixture.cutoff.instant()); + let batch = OutboundBatch::from_driver( + fixture.context(3), + stream::iter([event, OutboundBatchDriverEvent::Cutoff]), + no_owner(), + ); + + let results = block_on(batch.collect()).expect("valid cutoff"); + assert_eq!(results.termination(), OutboundBatchTermination::Cutoff); + assert!(matches!( + results.slots().first(), + Some(OutboundCollectedSlot::Unresolved) + )); + assert!(matches!( + results.slots()[1], + OutboundCollectedSlot::Unresolved + )); + assert!(matches!( + results.slots()[2], + OutboundCollectedSlot::Resolved(_) + )); +} + +#[test] +fn batch_cutoff_constructor_reports_every_slot_unresolved() { + let fixture = BatchFixture::new(); + fixture.time.advance_to(fixture.cutoff.instant()); + let results = + block_on(OutboundBatch::cutoff(fixture.context(3)).collect()).expect("valid cutoff"); + + assert_eq!(results.termination(), OutboundBatchTermination::Cutoff); + assert_eq!(results.slots().len(), 3); + assert!( + results + .slots() + .iter() + .all(|slot| matches!(slot, OutboundCollectedSlot::Unresolved)) + ); +} + +#[test] +fn batch_collection_rejects_premature_driver_end() { + let fixture = BatchFixture::new(); + let batch = OutboundBatch::from_driver( + fixture.context(3), + stream::iter([fixture.observation(2)]), + no_owner(), + ); + + let failure = block_on(batch.collect()).expect_err("premature EOF must fail"); + assert_eq!( + failure.error().to_string(), + "internal error: outbound batch driver ended before every slot resolved" + ); + assert!(matches!( + failure.slots().first(), + Some(OutboundCollectedSlot::Unresolved) + )); + assert!(matches!( + failure.slots().get(1), + Some(OutboundCollectedSlot::Unresolved) + )); + assert!(matches!( + failure.slots().get(2), + Some(OutboundCollectedSlot::Resolved(_)) + )); +} + +#[test] +fn batch_collection_preserves_driver_failure_and_completed_slots() { + use crate::error_detail::{FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase}; + + let detail = ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::ResponseHeaders, + cause: FastlyFailureCause::InternalError { + status: Some(-1_i32), + }, + }; + let fixture = BatchFixture::new(); + let completed = OutboundBatchDriverEvent::Item { + index: 1, + completed_at: fixture.time.now(), + outcome: Err(EdgeError::bad_gateway_with_detail( + "slot failed", + BadGatewayReason::Transport, + detail, + )), + }; + let batch = OutboundBatch::from_driver( + fixture.context(3), + stream::iter([ + completed, + OutboundBatchDriverEvent::Failed(EdgeError::internal_with_detail( + anyhow::anyhow!("Fastly omitted a pending handle from selection"), + detail, + )), + ]), + no_owner(), + ); + + let failure = block_on(batch.collect()).expect_err("driver failure must surface"); + assert_eq!( + failure.error().to_string(), + "internal error: Fastly omitted a pending handle from selection" + ); + assert!(matches!( + failure.slots().first(), + Some(OutboundCollectedSlot::Unresolved) + )); + assert!(matches!( + failure.slots().get(1), + Some(OutboundCollectedSlot::Resolved(_)) + )); + assert!(matches!( + failure.slots().get(2), + Some(OutboundCollectedSlot::Unresolved) + )); + assert_eq!(failure.error().provider_failure_detail(), Some(&detail)); + let OutboundCollectedSlot::Resolved(completed) = &failure.slots()[1] else { + panic!("completed slot"); + }; + let retained = completed + .outcome + .as_ref() + .expect_err("original slot failure"); + assert_eq!(retained.provider_failure_detail(), Some(&detail)); + assert_eq!(retained.message(), "slot failed"); +} + +#[test] +fn batch_collection_validates_events_after_the_final_slot() { + let fixture = BatchFixture::new(); + for (event, diagnostic) in [ + ( + OutboundBatchDriverEvent::Failed(EdgeError::internal(anyhow::anyhow!( + "driver finalization failed" + ))), + "internal error: driver finalization failed", + ), + ( + fixture.observation(0), + "internal error: outbound batch driver emitted a duplicate slot index", + ), + ( + fixture.observation(1), + "internal error: outbound batch driver emitted an out-of-range slot index", + ), + ( + OutboundBatchDriverEvent::Cutoff, + "internal error: outbound batch driver emitted cutoff after every slot resolved", + ), + ] { + let batch = OutboundBatch::from_driver( + fixture.context(1), + stream::iter([fixture.observation(0), event]), + no_owner(), + ); + + let failure = block_on(batch.collect()).expect_err("trailing event must be checked"); + assert_eq!(failure.error().to_string(), diagnostic); + assert_eq!(failure.slots().len(), 1); + assert!(matches!( + failure.slots().first(), + Some(OutboundCollectedSlot::Resolved(_)) + )); + } +} + +#[test] +fn batch_zero_slot_driver_is_validated_before_completion() { + let fixture = BatchFixture::new(); + let completed = block_on( + OutboundBatch::from_driver(fixture.context(0), stream::empty(), no_owner()).collect(), + ) + .expect("empty driver completes"); + assert_eq!(completed.termination(), OutboundBatchTermination::Completed); + let completed = + block_on(OutboundBatch::cutoff(fixture.context(0)).collect()).expect("no work to cut off"); + assert_eq!(completed.termination(), OutboundBatchTermination::Completed); + + for (event, diagnostic) in [ + ( + OutboundBatchDriverEvent::Failed(EdgeError::internal(anyhow::anyhow!( + "empty driver failed" + ))), + "internal error: empty driver failed", + ), + ( + fixture.observation(0), + "internal error: outbound batch driver emitted an out-of-range slot index", + ), + ( + OutboundBatchDriverEvent::Cutoff, + "internal error: outbound batch driver emitted cutoff after every slot resolved", + ), + ] { + let failure = block_on( + OutboundBatch::from_driver(fixture.context(0), stream::iter([event]), no_owner()) + .collect(), + ) + .expect_err("zero slots do not hide malformed driver events"); + assert_eq!(failure.error().to_string(), diagnostic); + assert!(failure.slots().is_empty()); + } +} + +#[test] +fn batch_finalization_remains_pending_and_cancellation_safe_until_eof() { + let fixture = BatchFixture::new(); + let context = fixture.context(1); + let mut event = Some(fixture.observation(0)); + let finish = Rc::new(Cell::new(false)); + let driver_finish = Rc::clone(&finish); + let polls = Rc::new(Cell::new(0_usize)); + let source_polls = Rc::clone(&polls); + let source = stream::poll_fn(move |_context| { + let index = source_polls.get(); + source_polls.set(index + 1); + if let Some(event) = event.take() { + Poll::Ready(Some(event)) + } else if driver_finish.get() { + Poll::Ready(None) + } else { + Poll::Pending + } + }); + let mut batch = OutboundBatch::from_driver(context, source, no_owner()); + assert!(matches!( + block_on(batch.next()).expect("terminal item"), + OutboundBatchNext::Item(_) + )); + assert!(batch.next().now_or_never().is_none()); + finish.set(true); + assert!(matches!( + block_on(batch.next()).expect("validated EOF"), + OutboundBatchNext::Finished(OutboundBatchTermination::Completed) + )); + assert_eq!(polls.get(), 4); + assert!(matches!( + block_on(batch.next()).expect("cached termination"), + OutboundBatchNext::Finished(OutboundBatchTermination::Completed) + )); + assert_eq!(polls.get(), 4); + assert!(batch.cancel().is_empty()); +} + +#[test] +fn batch_cancel_releases_pending_finalization_after_every_slot_resolves() { + struct DropProbe(Rc>); + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropProbe { + fn drop(&mut self) { + self.0.set(self.0.get() + 1); + } + } + let fixture = BatchFixture::new(); + let context = fixture.context(1); + let mut event = Some(fixture.observation(0)); + let drops = Rc::new(Cell::new(0)); + let drop_probe = DropProbe(Rc::clone(&drops)); + let source = stream::poll_fn(move |_context| { + let _keep_driver_alive = &drop_probe; + if let Some(event) = event.take() { + Poll::Ready(Some(event)) + } else { + Poll::Pending + } + }); + let mut batch = OutboundBatch::from_driver(context, source, no_owner()); + assert!(matches!( + block_on(batch.next()).expect("slot"), + OutboundBatchNext::Item(_) + )); + assert!(batch.next().now_or_never().is_none()); + assert_eq!(drops.get(), 0); + assert!(batch.cancel().is_empty()); + assert_eq!(drops.get(), 1); +} + +#[test] +fn batch_collection_rejects_duplicate_driver_indices() { + let fixture = BatchFixture::new(); + let batch = OutboundBatch::from_driver( + fixture.context(2), + stream::iter([fixture.observation(0), fixture.observation(0)]), + no_owner(), + ); + + let failure = block_on(batch.collect()).expect_err("duplicate index must fail"); + assert_eq!( + failure.error().to_string(), + "internal error: outbound batch driver emitted a duplicate slot index" + ); + assert!(matches!( + failure.slots().first(), + Some(OutboundCollectedSlot::Resolved(_)) + )); + assert!(matches!( + failure.slots().get(1), + Some(OutboundCollectedSlot::Unresolved) + )); +} + +#[test] +fn batch_collection_rejects_out_of_range_driver_indices() { + let fixture = BatchFixture::new(); + let batch = OutboundBatch::from_driver( + fixture.context(2), + stream::iter([fixture.observation(2)]), + no_owner(), + ); + + let failure = block_on(batch.collect()).expect_err("out-of-range index must fail"); + assert_eq!( + failure.error().to_string(), + "internal error: outbound batch driver emitted an out-of-range slot index" + ); + assert!( + failure + .slots() + .iter() + .all(|slot| matches!(slot, OutboundCollectedSlot::Unresolved)) + ); +} + +#[test] +fn batch_poisoned_repoll_reports_the_guard_diagnostic() { + let fixture = BatchFixture::new(); + let mut batch = OutboundBatch::from_driver( + fixture.context(1), + stream::iter([fixture.observation(1)]), + no_owner(), + ); + + let first = block_on(batch.next()).expect_err("out-of-range index must poison driver"); + assert_eq!( + first.to_string(), + "internal error: outbound batch driver emitted an out-of-range slot index" + ); + let second = block_on(batch.next()).expect_err("poisoned driver must reject re-poll"); + assert_eq!( + second.to_string(), + "internal error: outbound batch driver was polled after an invariant failure" + ); +} + +#[test] +fn outbound_response_parts_preserve_method_headers_and_body() { + let mut headers = HeaderMap::new(); + headers.append("set-cookie", HeaderValue::from_static("a=1")); + headers.append("set-cookie", HeaderValue::from_static("b=2")); + let mut response = OutboundResponse::new( + Method::HEAD, + StatusCode::CREATED, + headers, + Body::from("payload"), + MonotonicClock::default(), + super::ContentTypeProvenance::Unknown, + ); + assert_eq!(response.status(), StatusCode::CREATED); + assert!(response.is_success()); + assert_eq!(response.body().as_bytes(), Some(b"payload".as_slice())); + assert_eq!(response.headers().get_all("set-cookie").iter().count(), 2); + response + .headers_mut() + .append("x-adapter", HeaderValue::from_static("yes")); + + let (method, status, headers, body, provenance) = response.into_parts(); + assert_eq!(provenance, super::ContentTypeProvenance::Unknown); + assert_eq!(method, Method::HEAD); + assert_eq!(status, StatusCode::CREATED); + assert_eq!( + headers.get("x-adapter"), + Some(&HeaderValue::from_static("yes")) + ); + assert_eq!(body.as_bytes(), Some(b"payload".as_slice())); + + let body = OutboundResponse::new( + Method::GET, + StatusCode::OK, + HeaderMap::new(), + Body::from("owned"), + MonotonicClock::default(), + super::ContentTypeProvenance::Unknown, + ) + .into_body(); + assert_eq!(body.as_bytes(), Some(b"owned".as_slice())); +} + +#[test] +fn payload_content_length_explicit_identity_rejects_before_body_poll() { + let mut headers = HeaderMap::new(); + headers.append("content-encoding", HeaderValue::from_static("identity")); + headers.append("content-length", HeaderValue::from_static("11")); + let error = enforce_payload_content_length( + &headers, + &ContentEncoding::Identity, + Some(20), + Some(10), + Some(20), + ) + .expect_err("decoded cap"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::DecodedBody, + .. + } + )); +} + +#[test] +fn payload_content_length_passthrough_uses_buffered_not_decoded_cap() { + let mut headers = HeaderMap::new(); + headers.append("content-length", HeaderValue::from_static("15")); + enforce_payload_content_length( + &headers, + &ContentEncoding::Passthrough(PassthroughReason::Unsupported(String::from("zstd"))), + Some(20), + Some(10), + Some(20), + ) + .expect("decoded cap bypassed"); + + let error = enforce_payload_content_length( + &headers, + &ContentEncoding::Passthrough(PassthroughReason::Unsupported(String::from("zstd"))), + Some(14), + Some(100), + Some(20), + ) + .expect_err("buffered cap"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::BufferedBody, + .. + } + )); +} + +#[test] +fn payload_content_length_rejects_before_body_poll() { + let mut headers = HeaderMap::new(); + headers.append("content-length", HeaderValue::from_static("21")); + let error = enforce_payload_content_length( + &headers, + &ContentEncoding::Identity, + Some(30), + Some(30), + Some(20), + ) + .expect_err("encoded cap"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::EncodedBody, + .. + } + )); + + for value in ["", "+1", "-1", "bad", "1, 1"] { + let mut headers = HeaderMap::new(); + headers.append( + "content-length", + HeaderValue::from_bytes(value.as_bytes()).expect("header"), + ); + enforce_payload_content_length(&headers, &ContentEncoding::Identity, None, None, None) + .expect_err("malformed content-length"); + } + + let mut headers = HeaderMap::new(); + headers.append( + "content-length", + HeaderValue::from_bytes(b"\xc2\xa05").expect("opaque whitespace"), + ); + enforce_payload_content_length(&headers, &ContentEncoding::Identity, None, None, None) + .expect_err("non-HTTP whitespace must be rejected"); +} + +#[test] +fn payload_content_length_skips_output_caps_for_compressed_body() { + let mut headers = HeaderMap::new(); + headers.append("content-length", HeaderValue::from_static("100")); + for encoding in [ContentEncoding::Brotli, ContentEncoding::Gzip] { + enforce_payload_content_length(&headers, &encoding, Some(1), Some(1), Some(100)) + .expect("wire length is not output length"); + } +} + +#[test] +fn rechunk_stream_is_lazy_and_ordered() { + let source_polls = Rc::new(Cell::new(0_u8)); + let observed_polls = Rc::clone(&source_polls); + let source = Body::from_stream(stream::poll_fn(move |_context| { + let poll = source_polls.get(); + source_polls.set(poll.saturating_add(1)); + Poll::Ready(match poll { + 0 => Some(Ok(Bytes::from_static(b"abcde"))), + 1 => Some(Ok(Bytes::new())), + 2 => Some(Err(EdgeError::bad_gateway("source"))), + _ => None, + }) + })) + .into_stream() + .expect("stream"); + let mut chunks = rechunk_stream(source, NonZeroU64::new(2)); + assert_eq!(observed_polls.get(), 0); + + assert_eq!( + block_on(chunks.next()).expect("first").expect("bytes"), + "ab" + ); + assert_eq!(observed_polls.get(), 1); + assert_eq!( + block_on(chunks.next()).expect("second").expect("bytes"), + "cd" + ); + assert_eq!(observed_polls.get(), 1); + assert_eq!(block_on(chunks.next()).expect("third").expect("bytes"), "e"); + assert_eq!(observed_polls.get(), 1); + assert_eq!( + block_on(chunks.next()).expect("empty").expect("bytes"), + Bytes::new() + ); + assert_eq!(observed_polls.get(), 2); + assert!(matches!( + block_on(chunks.next()).expect("error"), + Err(EdgeError::BadGateway { .. }) + )); + assert_eq!(observed_polls.get(), 3); +} + +#[test] +fn response_header_limiter_accumulates_field_sections() { + let mut limiter = ResponseHeaderLimiter::new(Some(12), Some(3)); + let mut first = HeaderMap::new(); + first.append("a", HeaderValue::from_static("123")); + limiter.observe(&first).expect("first section"); + + let mut second = HeaderMap::new(); + second.append("b", HeaderValue::from_static("456")); + limiter.observe(&second).expect("second section"); + + let mut third = HeaderMap::new(); + third.append("c", HeaderValue::from_static("789")); + limiter.observe(&third).expect("third section"); + + let mut fourth = HeaderMap::new(); + fourth.append("d", HeaderValue::from_static("0")); + let error = limiter.observe(&fourth).expect_err("cumulative count"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::HeaderCount, + .. + } + )); + + let mut byte_limiter = ResponseHeaderLimiter::new(Some(11), Some(4)); + byte_limiter.observe(&first).expect("first byte section"); + byte_limiter.observe(&second).expect("second byte section"); + let error = byte_limiter + .observe(&third) + .expect_err("cumulative byte limit"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::HeaderBytes, + .. + } + )); +} + +#[test] +fn proxy_header_is_included_in_exact_byte_and_count_limits() { + let cases = [ + ("axum", 20_u64), + ("cloudflare", 26_u64), + ("fastly", 22_u64), + ("spin", 20_u64), + ]; + + for (adapter, exact_bytes) in cases { + let value = HeaderValue::from_str(adapter).expect("adapter marker"); + let mut exact_headers = HeaderMap::new(); + let mut exact = ResponseHeaderLimiter::new(Some(exact_bytes), Some(1)); + insert_proxy_header( + &mut exact_headers, + &mut exact, + OutboundProxyMarker::On, + value.clone(), + ) + .expect("exact proxy header budget"); + assert_eq!(exact_headers.get(PROXY_HEADER), Some(&value)); + + let mut byte_headers = HeaderMap::new(); + let mut byte_limited = + ResponseHeaderLimiter::new(Some(exact_bytes.saturating_sub(1)), None); + let byte_error = insert_proxy_header( + &mut byte_headers, + &mut byte_limited, + OutboundProxyMarker::On, + value.clone(), + ) + .expect_err("proxy header exceeds byte cap"); + assert!(matches!( + byte_error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::HeaderBytes, + .. + } + )); + + let mut count_headers = HeaderMap::new(); + let mut count_limited = ResponseHeaderLimiter::new(None, Some(0)); + let count_error = insert_proxy_header( + &mut count_headers, + &mut count_limited, + OutboundProxyMarker::On, + value, + ) + .expect_err("proxy header exceeds count cap"); + assert!(matches!( + count_error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::HeaderCount, + .. + } + )); + } +} + +#[test] +fn response_resource_header_limit_count_wins_tie() { + let mut limiter = ResponseHeaderLimiter::new(Some(0), Some(0)); + let mut headers = HeaderMap::new(); + headers.append("x", HeaderValue::from_static("y")); + let error = limiter.observe(&headers).expect_err("both limits"); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::HeaderCount, + .. + } + )); +} + +#[test] +fn decoded_limit_bypasses_raw_passthrough() { + let source = Body::from_stream(stream::iter([ + Ok(Bytes::from_static(b"ab")), + Ok(Bytes::from_static(b"cd")), + ])) + .into_stream() + .expect("stream"); + let error = block_on(async { + let mut limited = limit_decoded_stream(source, Some(3)); + assert_eq!(limited.next().await.expect("first").expect("bytes"), "ab"); + limited.next().await.expect("second").expect_err("over cap") + }); + assert!(matches!( + error, + EdgeError::ResponseTooLarge { + reason: ResponseLimitReason::DecodedBody, + .. + } + )); + + let raw = Body::from_stream(stream::iter([Ok(Bytes::from_static(b"raw-body"))])) + .into_stream() + .expect("raw passthrough"); + let bytes = block_on(async { raw.collect::>().await }); + assert_eq!(bytes[0].as_ref().expect("raw").as_ref(), b"raw-body"); +} + +#[test] +fn outbound_response_into_response_reapplies_normalization() { + let mut headers = HeaderMap::new(); + headers.append("connection", HeaderValue::from_static("x-private")); + headers.append("x-private", HeaderValue::from_static("secret")); + headers.append("set-cookie", HeaderValue::from_static("a=1")); + headers.append("set-cookie", HeaderValue::from_static("b=2")); + let body = Body::stream(stream::iter([Bytes::from_static(b"lazy")])); + let response = OutboundResponse::new( + Method::GET, + StatusCode::OK, + headers, + body, + MonotonicClock::default(), + super::ContentTypeProvenance::Unknown, + ) + .into_response() + .expect("response"); + + assert!(response.headers().get("connection").is_none()); + assert!(response.headers().get("x-private").is_none()); + assert_eq!(response.headers().get_all("set-cookie").iter().count(), 2); + assert!(response.into_body().is_stream()); +} + +#[test] +fn outbound_response_into_response_drops_bodyless_status_stream_without_polling() { + for (method, status, expected_content_length) in [ + (Method::GET, StatusCode::EARLY_HINTS, None), + (Method::GET, StatusCode::NO_CONTENT, None), + (Method::GET, StatusCode::RESET_CONTENT, Some("0")), + (Method::GET, StatusCode::NOT_MODIFIED, None), + (Method::HEAD, StatusCode::OK, None), + ] { + let polls = Rc::new(Cell::new(0_usize)); + let drops = Rc::new(Cell::new(0_usize)); + let observed_polls = Rc::clone(&polls); + let observed_drops = Rc::clone(&drops); + let drop_signal = BodyDropSignal(observed_drops); + let body = Body::from_stream(stream::poll_fn(move |_| { + let _keep_signal_alive = &drop_signal; + observed_polls.set(observed_polls.get().saturating_add(1)); + Poll::Ready(Some(Ok(Bytes::from_static(b"must-not-escape")))) + })); + + let response = OutboundResponse::new( + method, + status, + HeaderMap::new(), + body, + MonotonicClock::default(), + super::ContentTypeProvenance::Unknown, + ) + .into_response() + .expect("bodyless response"); + + assert_eq!(polls.get(), 0, "{status}"); + assert_eq!(drops.get(), 1, "{status}"); + assert_eq!(response.body().as_bytes(), Some([].as_slice()), "{status}"); + assert_eq!( + response + .headers() + .get("content-length") + .and_then(|value| value.to_str().ok()), + expected_content_length, + "{status}" + ); + } +} + +#[test] +fn outbound_response_json_error_classification() { + let response = response_with_body(Body::from(serde_json::json!({"ok": true}).to_string())); + let value: serde_json::Value = response.json().expect("json"); + assert_eq!(value["ok"], true); + + let malformed = response_with_body(Body::from("not-json")); + let error = malformed + .json::() + .expect_err("malformed JSON"); + assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Decode(BadGatewayDecodeReason::Json), + .. + } + )); + + let streamed = response_with_body(Body::stream(stream::iter([Bytes::from( + serde_json::json!({}).to_string(), + )]))) + .json::() + .expect_err("stream requires bounded helper"); + assert!(matches!( + streamed, + EdgeError::BadGateway { + reason: BadGatewayReason::Protocol, + .. + } + )); + + let streamed = response_with_body(Body::stream(stream::iter([Bytes::from( + serde_json::json!({}).to_string(), + )]))); + let value: serde_json::Value = block_on(streamed.json_bounded(2)).expect("bounded json"); + assert_eq!(value, serde_json::json!({})); +} + +#[test] +fn outbound_response_until_deadline_wins_ready_result() { + let deadline = Deadline::at_instant(MonotonicInstant::now()); + let response = response_with_body(Body::from_stream(stream::iter([Err( + EdgeError::bad_gateway("ready source error"), + )]))); + let error = + block_on(response.into_bytes_bounded_until(100, deadline)).expect_err("expired deadline"); + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::Unspecified, + .. + } + )); +} + +#[test] +fn outbound_response_deadline_uses_its_injected_clock() { + let start = MonotonicInstant::now(); + let observed = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&observed); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + let response = OutboundResponse::new( + Method::GET, + StatusCode::OK, + HeaderMap::new(), + Body::from("ready"), + clock, + super::ContentTypeProvenance::Unknown, + ); + *observed.lock().expect("clock lock") = deadline; + + let error = block_on(response.into_bytes_bounded_until(100, Deadline::at_instant(deadline))) + .expect_err("injected clock reached deadline"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); +} + +#[test] +fn response_content_type_provenance_precedes_filtering() { + use super::{ContentTypeIneligibility, ContentTypeProvenance}; + + let cases: &[(&[&[u8]], ContentTypeProvenance)] = &[ + ( + &[], + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::Missing), + ), + ( + &[b"application/json"], + ContentTypeProvenance::SingleTextUnchanged, + ), + ( + &[b"caf\xc3\xa9"], + ContentTypeProvenance::SingleTextUnchanged, + ), + ( + &[b"not a MIME type"], + ContentTypeProvenance::SingleTextUnchanged, + ), + ( + &[b"application/json, text/plain"], + ContentTypeProvenance::SingleTextUnchanged, + ), + ( + &[b"application/json; note=\"x,y\""], + ContentTypeProvenance::SingleTextUnchanged, + ), + ( + &[b"application/json", b"application/json"], + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::MultipleValues), + ), + ( + &[b"application/json", b"text/plain"], + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::MultipleValues), + ), + ( + &[b"application/json", b"\xff"], + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::MultipleValues), + ), + ( + &[b"\xff"], + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::NonText), + ), + ]; + for (values, expected) in cases { + let mut headers = HeaderMap::new(); + for value in *values { + headers.append( + "content-type", + HeaderValue::from_bytes(value).expect("header bytes"), + ); + } + let normalized = normalize_response_headers(&Method::GET, StatusCode::OK, &mut headers) + .expect("normalized head"); + assert_eq!(normalized.content_type_provenance, *expected); + if *expected == ContentTypeProvenance::SingleTextUnchanged { + assert_eq!( + headers.get("content-type").expect("retained").as_bytes(), + values[0] + ); + } + assert!( + headers + .get_all("content-type") + .iter() + .all(|value| str::from_utf8(value.as_bytes()).is_ok()) + ); + } + + let mut headers = HeaderMap::new(); + headers.insert("content-type", HeaderValue::from_static("application/json")); + headers.insert("connection", HeaderValue::from_static("content-type")); + let normalized = normalize_response_headers(&Method::HEAD, StatusCode::OK, &mut headers) + .expect("bodyless normalized head"); + assert_eq!( + normalized.body_disposition, + ResponseBodyDisposition::FramingBodyless + ); + assert_eq!( + normalized.content_type_provenance, + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::Removed) + ); + assert!(!headers.contains_key("content-type")); +} + +#[test] +fn response_content_type_provenance_survives_ownership_without_upgrades() { + use super::{ContentTypeIneligibility, ContentTypeProvenance}; + + for initial in [ + ContentTypeProvenance::SingleTextUnchanged, + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::MultipleValues), + ContentTypeProvenance::Unknown, + ] { + let mut headers = HeaderMap::new(); + headers.insert("content-type", HeaderValue::from_static("application/json")); + let mut response = OutboundResponse::new( + Method::GET, + StatusCode::OK, + headers, + Body::empty(), + MonotonicClock::default(), + initial, + ); + assert_eq!(response.content_type_provenance(), initial); + response + .headers_mut() + .insert("content-type", HeaderValue::from_static("text/plain")); + let expected = if initial == ContentTypeProvenance::SingleTextUnchanged { + ContentTypeProvenance::Ineligible(ContentTypeIneligibility::Modified) + } else { + initial + }; + assert_eq!(response.content_type_provenance(), expected); + let response = response.into_response().expect("converted response"); + assert_eq!( + response.extensions().get::(), + Some(&expected) + ); + } + let response = OutboundResponse::new( + Method::GET, + StatusCode::OK, + HeaderMap::new(), + Body::empty(), + MonotonicClock::default(), + ContentTypeProvenance::Unknown, + ); + let (_, _, _, _, provenance) = response.into_parts(); + assert_eq!(provenance, ContentTypeProvenance::Unknown); +} + +#[test] +fn response_normalization_precedence_table() { + let mut nominated = HeaderMap::new(); + nominated.append( + "connection", + HeaderValue::from_static("content-encoding, content-length"), + ); + nominated.append("content-encoding", HeaderValue::from_static("gzip")); + nominated.append("content-length", HeaderValue::from_static("malformed")); + nominated.append("keep-alive", HeaderValue::from_static("timeout=5")); + assert_eq!( + normalize_response_headers(&Method::GET, StatusCode::OK, &mut nominated) + .expect("nominations precede semantics") + .body_disposition, + ResponseBodyDisposition::Payload + ); + assert!(nominated.get("content-encoding").is_none()); + assert!(nominated.get("content-length").is_none()); + assert!(nominated.get("keep-alive").is_none()); + + let mut ambiguous_encoding = HeaderMap::new(); + ambiguous_encoding.append("content-encoding", HeaderValue::from_static("gzip")); + ambiguous_encoding.append( + "content-encoding", + HeaderValue::from_bytes(&[0xff]).expect("opaque header value"), + ); + normalize_response_headers(&Method::GET, StatusCode::OK, &mut ambiguous_encoding) + .expect("invalid encoding sibling must remain visible to classification"); + assert_eq!( + classify_content_encoding(&ambiguous_encoding), + ContentEncoding::Passthrough(PassthroughReason::Stacked) + ); + assert_eq!( + ambiguous_encoding + .get_all("content-encoding") + .iter() + .count(), + 2 + ); + + for malformed in ["x-good,,x-other", "bad name", ",x-good", "x-good,"] { + let mut headers = HeaderMap::new(); + headers.append( + "connection", + HeaderValue::from_bytes(malformed.as_bytes()).expect("header"), + ); + let error = normalize_response_headers(&Method::GET, StatusCode::OK, &mut headers) + .expect_err("malformed connection"); + assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Protocol, + .. + } + )); + } + + for status in [ + StatusCode::CONTINUE, + StatusCode::NO_CONTENT, + StatusCode::NOT_MODIFIED, + ] { + let mut headers = HeaderMap::new(); + headers.append("content-length", HeaderValue::from_static("7")); + assert_eq!( + normalize_response_headers(&Method::GET, status, &mut headers) + .expect("bodyless") + .body_disposition, + ResponseBodyDisposition::FramingBodyless + ); + if status == StatusCode::NOT_MODIFIED { + assert_eq!(headers.get("content-length").expect("metadata"), "7"); + } else { + assert!(headers.get("content-length").is_none()); + } + } + + for (value, declared_body) in [(None, false), (Some("0"), false), (Some("7"), true)] { + let mut headers = HeaderMap::new(); + if let Some(value) = value { + headers.append( + "content-length", + HeaderValue::from_bytes(value.as_bytes()).expect("length"), + ); + } + assert_eq!( + normalize_response_headers(&Method::GET, StatusCode::RESET_CONTENT, &mut headers) + .expect("205") + .body_disposition, + ResponseBodyDisposition::ResetContent { declared_body } + ); + assert_eq!(headers.get("content-length").expect("normalized"), "0"); + } + + let mut conflicting = HeaderMap::new(); + conflicting.append("content-length", HeaderValue::from_static("7")); + conflicting.append("content-length", HeaderValue::from_static("8")); + normalize_response_headers(&Method::GET, StatusCode::OK, &mut conflicting) + .expect_err("conflicting content-length"); + + let mut idempotent = HeaderMap::new(); + idempotent.append("x-keep", HeaderValue::from_static("yes")); + normalize_response_headers(&Method::GET, StatusCode::OK, &mut idempotent).expect("first"); + let once = idempotent.clone(); + normalize_response_headers(&Method::GET, StatusCode::OK, &mut idempotent).expect("second"); + assert_eq!(idempotent, once); +} + +#[test] +fn response_normalization_rejects_non_utf8_content_length() { + let mut headers = HeaderMap::new(); + headers.append( + "content-length", + HeaderValue::from_bytes(&[0xff]).expect("opaque content-length"), + ); + + let error = normalize_response_headers(&Method::GET, StatusCode::OK, &mut headers) + .expect_err("non-UTF-8 content-length must fail closed"); + + assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Protocol, + .. + } + )); +} + +#[test] +fn head_and_not_modified_validate_and_retain_representation_length() { + for (method, status) in [ + (Method::HEAD, StatusCode::OK), + (Method::GET, StatusCode::NOT_MODIFIED), + ] { + for invalid in ["not-a-number", "18446744073709551616"] { + let mut headers = HeaderMap::new(); + headers.append( + "content-length", + HeaderValue::from_bytes(invalid.as_bytes()).expect("header value"), + ); + let error = normalize_response_headers(&method, status, &mut headers) + .expect_err("invalid bodyless content-length"); + assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Protocol, + .. + } + )); + } + + let mut conflicting = HeaderMap::new(); + conflicting.append("content-length", HeaderValue::from_static("7")); + conflicting.append("content-length", HeaderValue::from_static("8")); + let error = normalize_response_headers(&method, status, &mut conflicting) + .expect_err("conflicting bodyless content-length"); + assert!(matches!( + error, + EdgeError::BadGateway { + reason: BadGatewayReason::Protocol, + .. + } + )); + + let mut valid = HeaderMap::new(); + valid.append("content-length", HeaderValue::from_static("4294967296")); + assert_eq!( + normalize_response_headers(&method, status, &mut valid) + .expect("valid bodyless metadata") + .body_disposition, + ResponseBodyDisposition::FramingBodyless + ); + assert_eq!(valid.get("content-length").expect("length"), "4294967296"); + } +} + +#[test] +fn portable_dispatch_modes_share_batch_shape_validation_without_polling() { + for mode in [ + OutboundSendMode::Send, + OutboundSendMode::Follow, + OutboundSendMode::Begin, + OutboundSendMode::Batch, + ] { + for streamed_upload in [false, true] { + let polls = Arc::new(AtomicUsize::new(0)); + let source_polls = Arc::clone(&polls); + let mut request = OutboundRequest::post("https://example.com/") + .expect("request") + .stream_response() + .timeout(Duration::ZERO); + if streamed_upload { + request = request.body(Body::from_stream(stream::poll_fn(move |_cx| { + source_polls.fetch_add(1, Ordering::SeqCst); + Poll::>>::Pending + }))); + } + let result = validate_for_dispatch(&request, mode); + if mode == OutboundSendMode::Batch + || (mode == OutboundSendMode::Begin && streamed_upload) + { + assert_eq!( + bad_request_message(result.expect_err("batch shape")), + if mode == OutboundSendMode::Begin { + "begin requires a buffered request body; use send for a streamed upload" + } else if streamed_upload { + "outbound batches require buffered request bodies; use send for a streamed upload" + } else { + "outbound batches require buffered responses; use send for a streamed response" + } + ); + } else { + result.expect("single exchange accepts streams before budget selection"); + } + assert_eq!(polls.load(Ordering::SeqCst), 0); + } + } +} + +#[test] +fn portable_dispatch_modes_keep_method_and_upload_cap_precedence() { + for mode in [ + OutboundSendMode::Send, + OutboundSendMode::Follow, + OutboundSendMode::Begin, + OutboundSendMode::Batch, + ] { + for method in [Method::GET, Method::HEAD] { + let polls = Rc::new(Cell::new(0_usize)); + let counted = Rc::clone(&polls); + let request = OutboundRequest::new(method, Uri::from_static("https://example.com/")) + .expect("request") + .body(Body::from_stream(stream::poll_fn(move |_cx| { + counted.set(counted.get() + 1); + Poll::Pending + }))) + .stream_response(); + let result = validate_for_dispatch(&request, mode); + if mode == OutboundSendMode::Batch { + assert_eq!( + bad_request_message(result.expect_err("batch GET/HEAD stream")), + "outbound batches require buffered request bodies" + ); + } else { + result.expect("GET/HEAD stream is settled by the single-exchange facade"); + } + assert_eq!( + polls.get(), + 0, + "validation must not settle or poll the body" + ); + } + let request = OutboundRequest::post("https://example.com/") + .expect("request") + .body("too large") + .max_request_body_bytes(1) + .stream_response(); + assert_eq!( + validate_for_dispatch(&request, mode) + .expect_err("upload cap") + .request_body_overflow_origin(), + Some(RequestBodyOverflowOrigin::OutboundUpload) + ); + } +} + +#[test] +fn image_optimizer_mode_rejection_precedes_ordinary_validation() { + for mode in [OutboundSendMode::Begin, OutboundSendMode::Batch] { + let request = OutboundRequest::post("https://example.com/") + .expect("request") + .image_optimizer(super::OutboundImageOptimizerOptions::new()) + .body(Body::from_stream(stream::pending())) + .max_brotli_window_bits(9) + .timeout(Duration::ZERO); + assert_eq!( + bad_request_message( + validate_for_dispatch(&request, mode).expect_err("unsupported mode") + ), + "image_optimizer is not supported for eager outbound requests" + ); + } + for mode in [OutboundSendMode::Send, OutboundSendMode::Follow] { + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .image_optimizer( + super::OutboundImageOptimizerOptions::new() + .region(super::OutboundImageOptimizerRegion::Asia), + ); + validate_for_dispatch(&request, mode).expect("single-send optimizer combination"); + } +} + +#[test] +fn buffered_upload_overflow_is_rejected_during_common_preflight() { + for len in [7_usize, 8, 9] { + let request = OutboundRequest::post("https://example.com/upload") + .expect("request") + .body(vec![b'x'; len]) + .max_request_body_bytes(8); + let result = validate_for_dispatch(&request, OutboundSendMode::Send); + if len > 8 { + let error = result.expect_err("buffered overflow before dispatch"); + assert_eq!(error.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!(error.kind(), "payload_too_large"); + } else { + result.expect("within cap"); + } + } +} + +#[test] +fn dispatch_validation_precedence_table() { + for method in [ + Method::GET, + Method::HEAD, + Method::POST, + Method::PUT, + Method::PATCH, + Method::DELETE, + Method::OPTIONS, + ] { + let request = + OutboundRequest::new(method, Uri::from_static("https://example.com/resource")) + .expect("portable request"); + validate_for_dispatch(&request, OutboundSendMode::Send).expect("portable method"); + } + + let request = OutboundRequest::new( + Method::CONNECT, + Uri::from_static("https://example.com/resource"), + ) + .expect("request"); + assert_eq!( + bad_request_message( + validate_for_dispatch(&request, OutboundSendMode::Send).expect_err("custom method") + ), + "method CONNECT is not portable; supported: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" + ); + + let request = OutboundRequest::get("https://example.com") + .expect("request") + .body("not empty"); + assert_eq!( + bad_request_message( + validate_for_dispatch(&request, OutboundSendMode::Send).expect_err("GET body") + ), + "GET/HEAD request must not carry a body" + ); + + let request = OutboundRequest::get("https://example.com") + .expect("request") + .body(Body::stream(stream::iter([Bytes::new()]))); + assert!(request.is_stream_body()); + validate_for_dispatch(&request, OutboundSendMode::Send).expect("GET stream awaits settlement"); + let response = block_on(HttpClient::with_client(MarkerClient).send(request)) + .expect("GET stream with only empty chunks settles to an empty request body"); + assert_eq!(response.status(), StatusCode::OK); + let nonempty = OutboundRequest::get("https://example.com") + .expect("request") + .body(Body::stream(stream::iter([Bytes::from_static(b"data")]))); + assert_eq!( + bad_request_message( + block_on(HttpClient::with_client(MarkerClient).send(nonempty)) + .expect_err("GET payload rejected during settlement") + ), + "GET/HEAD request must not carry a body" + ); + + let request = OutboundRequest::post("https://example.com") + .expect("request") + .body(Body::stream(stream::iter([Bytes::from_static(b"data")]))) + .stream_response(); + assert!(request.is_stream_body()); + assert!(request.is_stream_response()); + validate_for_dispatch(&request, OutboundSendMode::Send).expect("POST stream"); + + for bits in [9, 31] { + let request = OutboundRequest::get("https://example.com") + .expect("request") + .max_brotli_window_bits(bits); + assert!( + validate_for_dispatch(&request, OutboundSendMode::Send).is_err(), + "accepted {bits}" + ); + } + for bits in [10, 30] { + let request = OutboundRequest::get("https://example.com") + .expect("request") + .max_brotli_window_bits(bits); + validate_for_dispatch(&request, OutboundSendMode::Send).expect("valid Brotli window"); + } +} + +#[test] +fn request_normalization_is_idempotent() { + let mut request = OutboundRequest::post("https://example.com") + .expect("request") + .header("connection", "x-first, x-second") + .expect("connection") + .header("x-first", "one") + .expect("first") + .header("x-second", "two") + .expect("second") + .header("x-keep", "yes") + .expect("keep"); + + normalize_for_dispatch(&mut request).expect("first pass"); + let once = request.headers().clone(); + normalize_for_dispatch(&mut request).expect("second pass"); + assert_eq!(request.headers(), &once); + assert_eq!( + request.headers().get("x-keep"), + Some(&HeaderValue::from_static("yes")) + ); +} + +#[test] +fn request_normalization_strips_connection_nominations() { + let mut request = OutboundRequest::post("https://example.com") + .expect("request") + .header("connection", "x-first") + .expect("connection") + .header("connection", "x-second, X-Third") + .expect("connection") + .header("x-first", "one") + .expect("first") + .header("x-second", "two") + .expect("second") + .header("x-third", "three") + .expect("third") + .header("keep-alive", "timeout=5") + .expect("keep-alive") + .header("proxy-authenticate", "challenge") + .expect("proxy-authenticate") + .header("proxy-authorization", "secret") + .expect("proxy-authorization") + .header("te", "trailers") + .expect("te") + .header("trailer", "x-checksum") + .expect("trailer") + .header("transfer-encoding", "chunked") + .expect("transfer-encoding") + .header("upgrade", "websocket") + .expect("upgrade") + .header("host", "wrong.example") + .expect("host") + .header("content-length", "100") + .expect("content-length") + .header("x-keep", "yes") + .expect("keep"); + + normalize_for_dispatch(&mut request).expect("normalize"); + for name in [ + "connection", + "x-first", + "x-second", + "x-third", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "host", + "content-length", + ] { + assert!(request.headers().get(name).is_none(), "retained {name}"); + } + assert_eq!( + request.headers().get("x-keep"), + Some(&HeaderValue::from_static("yes")) + ); +} + +#[test] +fn request_normalization_rejects_malformed_connection() { + for value in ["x-valid,,x-other", "bad name", ",x-valid", "x-valid,"] { + let mut request = OutboundRequest::get("https://example.com").expect("request"); + request.headers_mut().append( + "connection", + HeaderValue::from_str(value).expect("header value"), + ); + assert!( + normalize_for_dispatch(&mut request).is_err(), + "accepted {value}" + ); + } + + let mut request = OutboundRequest::get("https://example.com").expect("request"); + request.headers_mut().append( + "connection", + HeaderValue::from_bytes(b"x-private,\xff").expect("opaque header"), + ); + normalize_for_dispatch(&mut request).expect_err("opaque connection nomination"); + + let mut request = OutboundRequest::get("https://example.com").expect("request"); + request.headers_mut().append( + "connection", + HeaderValue::from_bytes(b"\xc2\xa0x-private").expect("UTF-8 header value"), + ); + normalize_for_dispatch(&mut request).expect_err("non-HTTP whitespace nomination"); + + let mut headers = HeaderMap::new(); + headers.append( + "connection", + HeaderValue::from_bytes(b"\xc2\xa0x-private").expect("UTF-8 header value"), + ); + normalize_response_headers(&Method::GET, StatusCode::OK, &mut headers) + .expect_err("non-HTTP whitespace response nomination"); +} + +#[test] +fn request_normalization_drops_non_utf8_content_encoding() { + let mut request = OutboundRequest::post("https://example.com").expect("request"); + request.headers_mut().append( + "content-encoding", + HeaderValue::from_bytes(&[0xff]).expect("opaque header value"), + ); + + normalize_for_dispatch(&mut request).expect("normalize"); + + assert!(request.headers().get("content-encoding").is_none()); +} + +#[test] +#[expect( + clippy::cognitive_complexity, + reason = "the round-trip contract intentionally verifies every independent outbound field" +)] +fn outbound_request_defaults_and_parts_round_trip() { + let deadline = Deadline::after(Duration::from_secs(9)); + let request = OutboundRequest::post("https://example.com/items") + .expect("request") + .body("payload") + .cache_policy(OutboundCachePolicy::Bypass) + .deadline(deadline) + .header("x-test", "one") + .expect("header") + .host_authority_override("[2001:db8::1]:8443") + .expect("authority override") + .max_decoder_bytes(40 * 1024 * 1024) + .max_brotli_window_bits(23) + .max_chunk_bytes(NonZeroU64::new(4096).expect("nonzero")) + .max_decoded_response_bytes(2_000_000) + .max_encoded_response_bytes(1_000_000) + .max_request_body_bytes(3_000_000) + .max_response_bytes(4_000_000) + .max_response_header_bytes(32_000) + .max_response_header_count(80) + .timeout(Duration::from_secs(8)); + + let parts = request.into_parts(); + assert_eq!(parts.method, Method::POST); + assert_eq!(parts.uri, Uri::from_static("https://example.com/items")); + assert_eq!(parts.cache_policy, OutboundCachePolicy::Bypass); + assert_eq!( + parts + .host_authority_override + .as_ref() + .map(Authority::as_str), + Some("[2001:db8::1]:8443") + ); + assert_eq!(parts.body.as_bytes(), Some(b"payload".as_slice())); + assert_eq!( + parts.deadline.expect("deadline").instant(), + deadline.instant() + ); + assert_eq!( + parts.headers.get("x-test"), + Some(&HeaderValue::from_static("one")) + ); + assert_eq!(parts.max_decoder_bytes, 40 * 1024 * 1024); + assert_eq!(parts.max_brotli_window_bits, 23); + assert_eq!(parts.max_chunk_bytes.map(NonZeroU64::get), Some(4096)); + assert_eq!(parts.max_decoded_response_bytes, Some(2_000_000)); + assert_eq!(parts.max_encoded_response_bytes, Some(1_000_000)); + assert_eq!(parts.max_request_body_bytes, 3_000_000); + assert_eq!(parts.max_response_header_bytes, Some(32_000)); + assert_eq!(parts.max_response_header_count, Some(80)); + assert_eq!( + parts.response_mode, + ResponseMode::Buffered { + max_bytes: 4_000_000 + } + ); + assert_eq!(parts.timeout, Some(Duration::from_secs(8))); + + let request = OutboundRequest::from_parts(parts).expect("round trip"); + assert_eq!(request.method(), &Method::POST); + assert_eq!(request.host_authority(), "[2001:db8::1]:8443"); + assert_eq!(request.backend_target(), "example.com:443"); + assert_eq!(request.sni_hostname(), Some("example.com")); + assert_eq!(request.cert_host(), Some("example.com")); + assert_eq!( + request.uri(), + &Uri::from_static("https://example.com/items") + ); + + let defaults = OutboundRequest::get("https://example.com") + .expect("defaults") + .into_parts(); + assert_eq!(defaults.max_decoder_bytes, 0x0200_0000); + assert_eq!(defaults.max_brotli_window_bits, 24); + assert_eq!(defaults.max_request_body_bytes, 0x0080_0000); + assert_eq!(defaults.cache_policy, OutboundCachePolicy::PlatformDefault); + assert!(defaults.host_authority_override.is_none()); + assert_eq!( + defaults.response_mode, + ResponseMode::Buffered { + max_bytes: 0x0010_0000 + } + ); + assert!(defaults.deadline.is_none()); + assert!(defaults.max_decoded_response_bytes.is_none()); + assert!(defaults.max_encoded_response_bytes.is_none()); + assert!(defaults.max_response_header_bytes.is_none()); + assert!(defaults.max_response_header_count.is_none()); + assert!(defaults.timeout.is_none()); +} + +#[test] +fn outbound_request_policy_accepts_only_http_authorities() { + for authority in [ + "api.example.com", + "api.example.com:8443", + "api.example.com:65535", + "192.0.2.1:8080", + "[2001:db8::1]", + "[2001:db8::1]:8443", + ] { + let request = OutboundRequest::get("https://origin.example/resource") + .expect("request") + .host_authority_override(authority) + .expect("valid authority"); + assert!(request.has_host_authority_override()); + assert_eq!(request.host_authority(), authority); + assert_eq!(request.backend_target(), "origin.example:443"); + assert_eq!(request.sni_hostname(), Some("origin.example")); + assert_eq!(request.cert_host(), Some("origin.example")); + } + + for invalid in [ + "", + "user@api.example.com", + "https://api.example.com", + "api.example.com/path", + "api example.com", + "api.example.com\r\nx-added: value", + "2001:db8::1", + "api.example.com:", + "api.example.com:abc", + "api.example.com:65536", + "[2001:db8::1]:abc", + "[2001:db8::1]:65536", + ] { + let error = OutboundRequest::get("https://origin.example") + .expect("request") + .host_authority_override(invalid) + .expect_err("invalid authority"); + assert_eq!( + bad_request_message(error), + "invalid outbound host authority override" + ); + } +} + +#[test] +fn outbound_request_response_mode_setters_are_last_write_wins() { + let streamed = OutboundRequest::get("https://example.com") + .expect("streamed request") + .max_response_bytes(7) + .stream_response() + .into_parts(); + assert_eq!(streamed.response_mode, ResponseMode::Streamed); + + let buffered = OutboundRequest::get("https://example.com") + .expect("buffered request") + .stream_response() + .max_response_bytes(7) + .into_parts(); + assert_eq!( + buffered.response_mode, + ResponseMode::Buffered { max_bytes: 7 } + ); +} + +#[test] +fn outbound_request_canonicalizes_url_table() { + let cases = [ + ("HTTPS://EXAMPLE.com:443/a/../b", "https://example.com/b"), + ("http://example.com:80", "http://example.com/"), + ("https://example.com:8443", "https://example.com:8443/"), + ("https://127.0.0.1", "https://127.0.0.1/"), + ("https://[::1]:443/a", "https://[::1]/a"), + ("https://caf%C3%A9.example/", "https://xn--caf-dma.example/"), + ]; + + for (input, expected) in cases { + let request = OutboundRequest::get(input).expect(input); + assert_eq!(request.uri().to_string(), expected, "{input}"); + } + + let dns = OutboundRequest::get("https://example.com").expect("dns"); + assert_eq!(dns.backend_target(), "example.com:443"); + assert_eq!(dns.cert_host(), Some("example.com")); + assert_eq!(dns.host_authority(), "example.com"); + assert_eq!(dns.host_name(), "example.com"); + assert_eq!(dns.sni_hostname(), Some("example.com")); + + let ip = OutboundRequest::get("https://[::1]:8443").expect("ip"); + assert_eq!(ip.backend_target(), "[::1]:8443"); + assert_eq!(ip.cert_host(), Some("::1")); + assert_eq!(ip.host_authority(), "[::1]:8443"); + assert_eq!(ip.host_name(), "::1"); + assert_eq!(ip.sni_hostname(), None); +} + +#[test] +fn outbound_request_rejects_invalid_target_table() { + for target in [ + "ftp://example.com", + "/relative", + "https:///missing", + "https://user:pass@example.com", + "https://example.com/path#fragment", + ] { + assert!(OutboundRequest::get(target).is_err(), "accepted {target}"); + } +} + +#[test] +fn outbound_request_rejects_empty_userinfo() { + OutboundRequest::get("https://@example.com/").expect_err("empty userinfo"); +} + +#[test] +fn typed_outbound_request_rejects_empty_userinfo() { + let typed = "https://@example.com/" + .parse::() + .expect("typed URI retains empty userinfo syntax"); + OutboundRequest::new(Method::GET, typed.clone()).expect_err("typed empty userinfo"); + + let mut parts = OutboundRequest::get("https://example.com/") + .expect("baseline request") + .into_parts(); + parts.uri = typed; + OutboundRequest::from_parts(parts).expect_err("parts empty userinfo"); +} + +#[test] +fn outbound_request_rejects_backslash_authority_forms() { + for target in [ + r"https:\\@example.com/", + r"https:/\@example.com/", + r"https:\/\@example.com/", + r"https://example.com\path", + ] { + assert!(OutboundRequest::get(target).is_err(), "accepted {target}"); + } +} + +#[test] +fn outbound_request_preserves_percent_encoded_hash() { + let request = + OutboundRequest::get("https://example.com/a%23b?q=%40user").expect("encoded delimiters"); + assert_eq!( + request.uri().to_string(), + "https://example.com/a%23b?q=%40user" + ); +} + +#[test] +fn outbound_request_from_request_normalizes_immediately() { + let request = request_builder() + .method(Method::POST) + .uri("/local") + .header("connection", "x-remove") + .header("x-remove", "gone") + .header("host", "local.invalid") + .header("content-length", "7") + .header("transfer-encoding", "chunked") + .header("x-keep", "yes") + .body(Body::from("payload")) + .expect("inbound request"); + + let outbound = + OutboundRequest::from_request(request, Uri::from_static("https://example.com/target")) + .expect("outbound request"); + assert_eq!(outbound.method(), &Method::POST); + assert_eq!( + outbound.headers().get("x-keep"), + Some(&HeaderValue::from_static("yes")) + ); + for removed in [ + "connection", + "x-remove", + "host", + "content-length", + "transfer-encoding", + ] { + assert!( + !outbound.headers().contains_key(removed), + "retained {removed}" + ); + } + assert_eq!( + outbound.into_parts().body.as_bytes(), + Some(b"payload".as_slice()) + ); + + let request = request_builder() + .method(Method::GET) + .uri("/local") + .header("x-keep", "yes") + .body(Body::empty()) + .expect("inbound request"); + let outbound = + OutboundRequest::from_request(request, Uri::from_static("https://example.com/target")) + .expect("outbound request"); + assert_eq!( + outbound.headers().get("x-keep"), + Some(&HeaderValue::from_static("yes")) + ); +} + +#[test] +fn outbound_normalization_strips_proxy_connection() { + let mut request = OutboundRequest::get("https://example.com/").expect("request"); + request + .headers_mut() + .insert("proxy-connection", HeaderValue::from_static("keep-alive")); + normalize_for_dispatch(&mut request).expect("request normalization"); + assert!(!request.headers().contains_key("proxy-connection")); + + let mut response_headers = HeaderMap::new(); + response_headers.insert("proxy-connection", HeaderValue::from_static("keep-alive")); + normalize_response_headers(&Method::GET, StatusCode::OK, &mut response_headers) + .expect("response normalization"); + assert!(!response_headers.contains_key("proxy-connection")); +} + +#[test] +fn outbound_request_rejects_invalid_header_bytes() { + let request = OutboundRequest::get("https://example.com").expect("request"); + request + .header(b"bad name", b"value") + .expect_err("invalid header name"); + + let request = OutboundRequest::get("https://example.com").expect("request"); + request + .header(b"x-test", [0xff]) + .expect_err("invalid header value"); + + let request = OutboundRequest::get("https://example.com").expect("request"); + request + .header(b"x-test", b"line\nbreak") + .expect_err("header value line break"); +} + +#[test] +fn forwarding_parts_preserve_inbound_deadline_metadata() { + use super::ForwardedReadDeadline; + + let deadline = Deadline::after(Duration::from_millis(100)); + let mut inbound = request_builder() + .method("GET") + .uri("/") + .body(Body::from_stream(stream::pending())) + .expect("inbound"); + inbound + .extensions_mut() + .insert(ForwardedReadDeadline::new(deadline)); + let request = + OutboundRequest::from_request(inbound, "https://example.com/".parse().expect("target")) + .expect("forward"); + let rebuilt = OutboundRequest::from_parts(request.into_parts()).expect("rebuild"); + assert_eq!( + rebuilt.inbound_read_deadline.map(|bound| bound.instant()), + Some(deadline.instant()) + ); +} + +#[test] +fn callback_exit_revokes_the_entire_unpolled_send_future() { + let owner = super::OutboundScopeOwner::callback(); + let drops = Rc::new(Cell::new(0)); + let captured = BodyDropSignal(Rc::clone(&drops)); + let body = Body::from_stream(stream::poll_fn(move |_cx| { + let _keep_alive = &captured; + Poll::Pending + })); + let client = HttpClient::with_client(MarkerClient); + let request = OutboundRequest::post("https://example.com/unpolled") + .expect("request") + .body(body); + let future = owner.with(|| client.send(request)); + assert_eq!(drops.get(), 0); + drop(owner); + assert_eq!( + drops.get(), + 1, + "retaining the future must not retain its request" + ); + assert!(matches!(block_on(future), Err(EdgeError::Internal { .. }))); + assert_eq!(drops.get(), 1); +} + +#[test] +fn callback_exit_revokes_a_begin_future_waiting_in_settlement() { + let owner = super::OutboundScopeOwner::callback(); + let drops = Rc::new(Cell::new(0)); + let polls = Rc::new(Cell::new(0_usize)); + let captured = BodyDropSignal(Rc::clone(&drops)); + let observed = Rc::clone(&polls); + let body = Body::from_stream(stream::poll_fn(move |_cx| { + let _keep_alive = &captured; + observed.set(observed.get().saturating_add(1)); + Poll::Pending + })); + let provider = Arc::new(MockClient { + batch_calls: AtomicUsize::new(0), + send_calls: AtomicUsize::new(0), + time: TestTime::new(MonotonicInstant::now()), + }); + let shared: Arc = Arc::::clone(&provider); + let client = HttpClient::new(shared); + let request = OutboundRequest::get("https://example.com/settling") + .expect("request") + .body(body); + let mut future = owner.with(|| client.begin(request)); + assert!( + future + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + .is_pending() + ); + assert_eq!(polls.get(), 1); + assert_eq!(provider.send_calls.load(Ordering::Relaxed), 0); + drop(owner); + assert_eq!(drops.get(), 1); + assert!(matches!(block_on(future), Err(EdgeError::Internal { .. }))); + assert_eq!(polls.get(), 1); + assert_eq!(provider.send_calls.load(Ordering::Relaxed), 0); +} + +#[test] +fn facade_begin_keeps_a_ready_pending_registered_after_begin_returns() { + let owner = super::OutboundScopeOwner::callback(); + let client = HttpClient::with_client(MarkerClient); + let request = OutboundRequest::get("https://example.com/ready").expect("request"); + let mut pending = owner + .with(|| client.begin(request)) + .now_or_never() + .expect("ready fixture") + .expect("begun exchange"); + assert!(pending.is_ready()); + owner.close(); + assert!( + pending.is_ready(), + "scope closure is a ready terminal error" + ); + let error = + block_on(pending.collect()).expect_err("retained pending belongs to closed callback"); + assert!(matches!(error, EdgeError::Internal { .. })); +} + +#[test] +fn facade_future_polled_in_another_scope_keeps_its_invocation_scope() { + let initiating = super::OutboundScopeOwner::callback(); + let foreign = super::OutboundScopeOwner::callback(); + let original_scope = initiating + .with(|| super::OutboundScope::capture(Rc::new(super::IndependentOutboundProgress))); + let expected = original_scope + .provider_state(|| 31_u8) + .expect("original provider state"); + let foreign_scope = + foreign.with(|| super::OutboundScope::capture(Rc::new(super::IndependentOutboundProgress))); + assert_eq!( + *foreign_scope + .provider_state(|| 99_u8) + .expect("foreign provider state"), + 99 + ); + let observed = Rc::new(Cell::new(false)); + let did_poll = Rc::clone(&observed); + let source = stream::poll_fn(move |_cx| { + let active = super::OutboundScope::capture(Rc::new(super::IndependentOutboundProgress)); + let current = active + .provider_state(|| 7_u8) + .expect("active provider state"); + assert!(Rc::ptr_eq(&expected, ¤t)); + did_poll.set(true); + Poll::Ready(None) + }); + let client = HttpClient::with_client(MarkerClient); + let request = OutboundRequest::get("https://example.com/foreign") + .expect("request") + .body(Body::from_stream(source)); + let future = initiating.with(|| client.send(request)); + block_on(foreign.run(future)).expect("original callback is still open"); + assert!(observed.get()); + assert_eq!( + *foreign_scope + .provider_state(|| 0_u8) + .expect("foreign state unchanged"), + 99 + ); +} + +#[test] +fn batch_scope_exit_releases_only_producer_and_retains_recorded_partials() { + let owner = super::OutboundScopeOwner::callback(); + let fixture = BatchFixture::new(); + let stream_drops = Rc::new(Cell::new(0)); + let owner_drops = Rc::new(Cell::new(0_usize)); + let release = BodyDropSignal(Rc::clone(&stream_drops)); + let waiting = stream::poll_fn(move |_cx| { + let _keep_alive = &release; + Poll::Pending + }); + let released_owner = Rc::clone(&owner_drops); + let mut batch = owner.with(|| { + OutboundBatch::from_driver( + fixture.context(3), + stream::iter([fixture.observation(0), fixture.observation(1)]).chain(waiting), + OutboundExchangeOwner::new(move || { + released_owner.set(released_owner.get().saturating_add(1)); + }), + ) + }); + let super::OutboundBatchCheck::Item(first) = batch.check().expect("record ready arrivals") + else { + panic!("first ready receipt"); + }; + assert_eq!(first.index(), 0); + owner.close(); + assert_eq!( + stream_drops.get(), + 1, + "producer stream released before another poll" + ); + assert_eq!( + owner_drops.get(), + 1, + "producer cancellation released immediately" + ); + let failure = block_on(batch.collect()).expect_err("precise scope closure"); + assert!(matches!(failure.error(), EdgeError::Internal { .. })); + assert!( + failure + .error() + .inner() + .expect("scope error") + .to_string() + .contains("scope") + ); + assert!(matches!( + failure.slots(), + [ + OutboundCollectedSlot::AlreadyDelivered, + OutboundCollectedSlot::Resolved(_), + OutboundCollectedSlot::Unresolved + ] + )); + first + .result() + .outcome + .as_ref() + .expect("delivered receipt remains valid"); +} diff --git a/crates/edgezero-core/src/platform.rs b/crates/edgezero-core/src/platform.rs new file mode 100644 index 000000000..016b0033d --- /dev/null +++ b/crates/edgezero-core/src/platform.rs @@ -0,0 +1,1047 @@ +//! Target platform properties that affect application resource policy. + +use std::num::NonZeroU32; + +/// Where a platform resource fact comes from. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PlatformResourceSource { + /// A default published by a named hosted runtime profile. + HostedDefault { + /// Hosted runtime profile that publishes the default. + provider: &'static str, + }, + /// A limit published for the named platform. + PlatformLimit { + /// Platform that publishes the limit. + provider: &'static str, + }, + /// A value supplied by a configurable runtime deployment. + RuntimeConfiguration { + /// Runtime whose deployment configuration supplied the value. + runtime: &'static str, + }, +} + +/// Why a platform resource fact is not known. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PlatformUnknownReason { + /// The operator must supply the value. + OperatorConfigured, + /// The provider does not publish the value. + ProviderUnpublished, + /// The runtime deployment determines the value. + RuntimeConfigured, + /// No more specific reason was supplied. + Unspecified, +} + +/// A platform resource fact that preserves both provenance and unknown state. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PlatformFact { + /// A known value and its source. + Known { + /// Published or configured value. + value: Value, + /// Where the value comes from. + source: PlatformResourceSource, + }, + /// A value that is not known. + Unknown { + /// Why the value is not known. + reason: PlatformUnknownReason, + }, +} + +impl PlatformFact { + /// Construct a known platform fact. + #[must_use] + #[inline] + pub const fn known(value: Value, source: PlatformResourceSource) -> Self { + Self::Known { value, source } + } + + /// Return the source of a known fact. + #[must_use] + #[inline] + pub const fn source(&self) -> Option { + match self { + Self::Known { source, .. } => Some(*source), + Self::Unknown { .. } => None, + } + } + + /// Construct an unknown platform fact. + #[must_use] + #[inline] + pub const fn unknown(reason: PlatformUnknownReason) -> Self { + Self::Unknown { reason } + } + + /// Return the reason for an unknown fact. + #[must_use] + #[inline] + pub const fn unknown_reason(&self) -> Option { + match self { + Self::Known { .. } => None, + Self::Unknown { reason } => Some(*reason), + } + } + + /// Return the known value, if present. + #[must_use] + #[inline] + pub fn value(self) -> Option { + match self { + Self::Known { value, .. } => Some(value), + Self::Unknown { .. } => None, + } + } +} + +/// What shares a target's published memory ceiling. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum MemoryCeilingScope { + /// Every request execution receives an independent budget. + PerExecution, + /// Concurrent requests served by one runtime instance share one budget. + PerInstance, +} + +/// A target's primary memory ceiling and its accounting semantics. +/// +/// `total_bytes` is the primary memory allowance. When `stack_bytes` is present, +/// the platform publishes that stack allowance separately and it is not +/// included in `total_bytes`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct MemoryCeiling { + scope: MemoryCeilingScope, + stack_bytes: Option, + total_bytes: u64, +} + +impl MemoryCeiling { + /// Construct a memory ceiling from exact byte counts. + #[must_use] + #[inline] + pub const fn new( + total_bytes: u64, + scope: MemoryCeilingScope, + stack_bytes: Option, + ) -> Self { + Self { + scope, + stack_bytes, + total_bytes, + } + } + + /// What shares this memory budget. + #[must_use] + #[inline] + pub const fn scope(self) -> MemoryCeilingScope { + self.scope + } + + /// Separately published stack allowance, in bytes. + #[must_use] + #[inline] + pub const fn stack_bytes(self) -> Option { + self.stack_bytes + } + + /// Primary memory allowance, in bytes, excluding a separately reported stack. + #[must_use] + #[inline] + pub const fn total_bytes(self) -> u64 { + self.total_bytes + } +} + +/// Maximum live inbound request executions charged to one memory ceiling. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct InboundRequestPopulationBound { + max_live_requests: NonZeroU32, +} + +impl InboundRequestPopulationBound { + /// Maximum live inbound requests charged to one memory ceiling. + #[must_use] + #[inline] + pub const fn max_live_requests(self) -> NonZeroU32 { + self.max_live_requests + } + + /// Construct an inbound request population bound. + #[must_use] + #[inline] + pub const fn new(max_live_requests: NonZeroU32) -> Self { + Self { max_live_requests } + } +} + +/// Whether host-owned ingress memory is charged to the published ceiling. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum HostIngressMemoryAccounting { + /// Host-owned ingress memory is charged to the published ceiling. + CountsTowardCeiling, + /// Host-owned ingress memory is outside the published ceiling. + OutsideCeiling, +} + +/// Application memory requirements declared for one platform memory domain. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct MemoryEnvelope { + fixed_bytes: u64, + per_live_request_bytes: u64, + scope: MemoryCeilingScope, + separate_stack_bytes: Option, +} + +impl MemoryEnvelope { + /// Bytes charged once to the memory domain. + #[must_use] + #[inline] + pub const fn fixed_bytes(self) -> u64 { + self.fixed_bytes + } + + /// Construct a memory envelope from exact byte counts. + #[must_use] + #[inline] + pub const fn new( + scope: MemoryCeilingScope, + fixed_bytes: u64, + per_live_request_bytes: u64, + separate_stack_bytes: Option, + ) -> Self { + Self { + fixed_bytes, + per_live_request_bytes, + scope, + separate_stack_bytes, + } + } + + /// Bytes charged for each live inbound request. + #[must_use] + #[inline] + pub const fn per_live_request_bytes(self) -> u64 { + self.per_live_request_bytes + } + + /// Scope in which this envelope's accounting applies. + #[must_use] + #[inline] + pub const fn scope(self) -> MemoryCeilingScope { + self.scope + } + + /// Required bytes from a separately published stack allowance. + #[must_use] + #[inline] + pub const fn separate_stack_bytes(self) -> Option { + self.separate_stack_bytes + } +} + +/// Result of validating an application memory envelope against platform facts. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum MemoryEnvelopeValidation { + /// A known memory resource cannot satisfy its requirement. + Exceeds { + /// Published bytes available for the resource. + available_bytes: u64, + /// Bytes required from the resource. + required_bytes: u64, + /// Resource whose requirement exceeds its allowance. + /// + /// Primary memory excess is reported before separate-stack excess when both apply. + resource: MemoryResource, + }, + /// All required facts are known and every requirement fits. + Fits { + /// Published primary memory allowance. + available_primary_bytes: u64, + /// Separately published stack allowance, when present. + available_separate_stack_bytes: Option, + /// Required primary memory. + required_primary_bytes: u64, + /// Required separately published stack memory, when present. + required_separate_stack_bytes: Option, + }, + /// No known requirement exceeds its allowance, but facts needed for a fit are missing. + Indeterminate { + /// Known primary allowance, when published. + available_primary_bytes: Option, + /// Checked minimum primary memory requirement. + minimum_required_primary_bytes: u64, + /// Missing facts in deterministic validation order: unknown memory ceiling, + /// unknown inbound request population, unknown host ingress memory accounting, + /// then missing separate-stack requirement. + reasons: Vec, + }, +} + +/// Invalid memory-envelope or platform-fact combinations. +#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] +#[non_exhaustive] +pub enum MemoryEnvelopeValidationError { + /// Checked primary requirement arithmetic overflowed. + #[error("memory envelope primary requirement arithmetic overflow")] + ArithmeticOverflow { + /// Bytes charged once to the memory domain. + fixed_bytes: u64, + /// Known request population used in the calculation, if any. + max_live_requests: Option, + /// Bytes charged for each live inbound request. + per_live_request_bytes: u64, + }, + /// A per-execution memory domain published a population other than one. + #[error("invalid per-execution request population")] + NonUnitPerExecutionPopulation { + /// Published maximum live request population. + max_live_requests: NonZeroU32, + }, + /// The envelope and published ceiling use different accounting scopes. + #[error("memory envelope scope mismatch")] + ScopeMismatch { + /// Scope of the published ceiling. + ceiling_scope: MemoryCeilingScope, + /// Scope declared by the envelope. + envelope_scope: MemoryCeilingScope, + }, + /// The envelope requires a separate stack but the known ceiling does not publish one. + #[error("unexpected separate stack requirement")] + UnexpectedSeparateStackRequirement { + /// Required separate stack bytes. + required_bytes: u64, + }, +} + +/// Memory resource compared by envelope validation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum MemoryResource { + /// Primary memory allowance. + Primary, + /// Separately published stack allowance. + SeparateStack, +} + +/// Missing fact that prevents a complete memory-envelope validation result. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum MemoryValidationIndeterminateReason { + /// The platform publishes a separate stack allowance but no requirement was supplied. + MissingSeparateStackRequirement, + /// Host ingress memory accounting is unknown. + UnknownHostIngressMemoryAccounting { + /// Why the accounting fact is unknown. + reason: PlatformUnknownReason, + }, + /// The maximum live inbound request population is unknown. + UnknownInboundRequestPopulation { + /// Why the population fact is unknown. + reason: PlatformUnknownReason, + }, + /// The platform memory ceiling is unknown. + UnknownMemoryCeiling { + /// Why the ceiling fact is unknown. + reason: PlatformUnknownReason, + }, +} + +/// Immutable target properties supplied to application construction. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct PlatformMetadata { + host_ingress_memory_accounting: PlatformFact, + inbound_request_population_bound: PlatformFact, + memory_ceiling: PlatformFact, +} + +impl PlatformMetadata { + /// The target's host ingress memory accounting fact. + #[must_use] + #[inline] + pub const fn host_ingress_memory_accounting(self) -> PlatformFact { + self.host_ingress_memory_accounting + } + + /// The target's live inbound request population bound fact. + #[must_use] + #[inline] + pub const fn inbound_request_population_bound( + self, + ) -> PlatformFact { + self.inbound_request_population_bound + } + + /// The target's published memory ceiling fact. + #[must_use] + #[inline] + pub const fn memory_ceiling(self) -> PlatformFact { + self.memory_ceiling + } + + /// Construct target metadata from explicit platform resource facts. + #[must_use] + #[inline] + pub const fn new( + memory_ceiling: PlatformFact, + inbound_request_population_bound: PlatformFact, + host_ingress_memory_accounting: PlatformFact, + ) -> Self { + Self { + host_ingress_memory_accounting, + inbound_request_population_bound, + memory_ceiling, + } + } + + /// Validate an application memory envelope against the target's published facts. + /// + /// # Errors + /// + /// Returns an error for contradictory scopes or stack shapes, a non-unit + /// per-execution population, or checked primary requirement overflow. + #[inline] + pub fn validate_memory_envelope( + self, + envelope: MemoryEnvelope, + ) -> Result { + let known_ceiling = self.memory_ceiling.value(); + let known_population = self + .inbound_request_population_bound + .value() + .map(InboundRequestPopulationBound::max_live_requests); + + if let Some(ceiling) = known_ceiling { + if envelope.scope() != ceiling.scope() { + return Err(MemoryEnvelopeValidationError::ScopeMismatch { + ceiling_scope: ceiling.scope(), + envelope_scope: envelope.scope(), + }); + } + if let (Some(required_bytes), None) = + (envelope.separate_stack_bytes(), ceiling.stack_bytes()) + { + return Err( + MemoryEnvelopeValidationError::UnexpectedSeparateStackRequirement { + required_bytes, + }, + ); + } + } + + if envelope.scope() == MemoryCeilingScope::PerExecution + && let Some(max_live_requests) = known_population + && max_live_requests != NonZeroU32::MIN + { + return Err( + MemoryEnvelopeValidationError::NonUnitPerExecutionPopulation { max_live_requests }, + ); + } + + let population_multiplier = match envelope.scope() { + MemoryCeilingScope::PerExecution => 1, + MemoryCeilingScope::PerInstance => { + known_population.map_or(1, |value| value.get().into()) + } + }; + let minimum_required_primary_bytes = envelope + .per_live_request_bytes() + .checked_mul(population_multiplier) + .and_then(|request_bytes| envelope.fixed_bytes().checked_add(request_bytes)) + .ok_or(MemoryEnvelopeValidationError::ArithmeticOverflow { + fixed_bytes: envelope.fixed_bytes(), + max_live_requests: known_population, + per_live_request_bytes: envelope.per_live_request_bytes(), + })?; + + if let Some(ceiling) = known_ceiling { + if minimum_required_primary_bytes > ceiling.total_bytes() { + return Ok(MemoryEnvelopeValidation::Exceeds { + available_bytes: ceiling.total_bytes(), + required_bytes: minimum_required_primary_bytes, + resource: MemoryResource::Primary, + }); + } + if let (Some(required_bytes), Some(available_bytes)) = + (envelope.separate_stack_bytes(), ceiling.stack_bytes()) + && required_bytes > available_bytes + { + return Ok(MemoryEnvelopeValidation::Exceeds { + available_bytes, + required_bytes, + resource: MemoryResource::SeparateStack, + }); + } + } + + let mut reasons = Vec::new(); + if let PlatformFact::Unknown { reason } = self.memory_ceiling { + reasons.push(MemoryValidationIndeterminateReason::UnknownMemoryCeiling { reason }); + } + if let PlatformFact::Unknown { reason } = self.inbound_request_population_bound { + reasons.push( + MemoryValidationIndeterminateReason::UnknownInboundRequestPopulation { reason }, + ); + } + if let PlatformFact::Unknown { reason } = self.host_ingress_memory_accounting { + reasons.push( + MemoryValidationIndeterminateReason::UnknownHostIngressMemoryAccounting { reason }, + ); + } + if known_ceiling.is_some_and(|ceiling| ceiling.stack_bytes().is_some()) + && envelope.separate_stack_bytes().is_none() + { + reasons.push(MemoryValidationIndeterminateReason::MissingSeparateStackRequirement); + } + + if let Some(ceiling) = known_ceiling + && reasons.is_empty() + { + return Ok(MemoryEnvelopeValidation::Fits { + available_primary_bytes: ceiling.total_bytes(), + available_separate_stack_bytes: ceiling.stack_bytes(), + required_primary_bytes: minimum_required_primary_bytes, + required_separate_stack_bytes: envelope.separate_stack_bytes(), + }); + } + + Ok(MemoryEnvelopeValidation::Indeterminate { + available_primary_bytes: known_ceiling.map(MemoryCeiling::total_bytes), + minimum_required_primary_bytes, + reasons, + }) + } +} + +impl Default for PlatformMetadata { + #[inline] + fn default() -> Self { + Self::new( + PlatformFact::unknown(PlatformUnknownReason::Unspecified), + PlatformFact::unknown(PlatformUnknownReason::Unspecified), + PlatformFact::unknown(PlatformUnknownReason::Unspecified), + ) + } +} + +#[cfg(test)] +mod tests { + use std::error::Error; + use std::num::NonZeroU32; + + use super::*; + + const TEST_SOURCE: PlatformResourceSource = PlatformResourceSource::PlatformLimit { + provider: "test-platform", + }; + + fn known(value: Value) -> PlatformFact { + PlatformFact::known(value, TEST_SOURCE) + } + + fn population(max_live_requests: u32) -> InboundRequestPopulationBound { + InboundRequestPopulationBound::new( + NonZeroU32::new(max_live_requests).expect("test population must be non-zero"), + ) + } + + #[test] + fn platform_fact_distinguishes_known_and_unknown_metadata() { + const CEILING: MemoryCeiling = + MemoryCeiling::new(128_000_000, MemoryCeilingScope::PerInstance, None); + const SOURCE: PlatformResourceSource = PlatformResourceSource::PlatformLimit { + provider: "test-platform", + }; + const KNOWN: PlatformFact = PlatformFact::known(CEILING, SOURCE); + const UNKNOWN: PlatformFact = + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished); + + assert_eq!(KNOWN.value(), Some(CEILING)); + assert_eq!(KNOWN.source(), Some(SOURCE)); + assert_eq!(KNOWN.unknown_reason(), None); + assert_eq!(UNKNOWN.value(), None); + assert_eq!(UNKNOWN.source(), None); + assert_eq!( + UNKNOWN.unknown_reason(), + Some(PlatformUnknownReason::ProviderUnpublished) + ); + } + + #[test] + fn platform_fact_accessors_support_non_copy_values() { + assert_eq!( + PlatformFact::known(String::from("value"), TEST_SOURCE).source(), + Some(TEST_SOURCE) + ); + assert_eq!( + PlatformFact::known(String::from("value"), TEST_SOURCE).unknown_reason(), + None + ); + assert_eq!( + PlatformFact::known(String::from("value"), TEST_SOURCE).value(), + Some(String::from("value")) + ); + } + + #[test] + fn memory_envelope_accessors_preserve_declared_shape() { + const ENVELOPE: MemoryEnvelope = + MemoryEnvelope::new(MemoryCeilingScope::PerInstance, 8_192, 4_096, Some(1_024)); + + assert_eq!(ENVELOPE.scope(), MemoryCeilingScope::PerInstance); + assert_eq!(ENVELOPE.fixed_bytes(), 8_192); + assert_eq!(ENVELOPE.per_live_request_bytes(), 4_096); + assert_eq!(ENVELOPE.separate_stack_bytes(), Some(1_024)); + } + + #[test] + fn validation_error_messages_are_stable_and_category_only() { + let errors = [ + MemoryEnvelopeValidationError::ArithmeticOverflow { + fixed_bytes: 123, + max_live_requests: Some(population(2).max_live_requests()), + per_live_request_bytes: 456, + }, + MemoryEnvelopeValidationError::NonUnitPerExecutionPopulation { + max_live_requests: population(2).max_live_requests(), + }, + MemoryEnvelopeValidationError::ScopeMismatch { + ceiling_scope: MemoryCeilingScope::PerExecution, + envelope_scope: MemoryCeilingScope::PerInstance, + }, + MemoryEnvelopeValidationError::UnexpectedSeparateStackRequirement { + required_bytes: 789, + }, + ]; + + for error in &errors { + let _: &dyn Error = error; + } + assert_eq!( + errors[0].to_string(), + "memory envelope primary requirement arithmetic overflow" + ); + assert_eq!( + errors[1].to_string(), + "invalid per-execution request population" + ); + assert_eq!(errors[2].to_string(), "memory envelope scope mismatch"); + assert_eq!( + errors[3].to_string(), + "unexpected separate stack requirement" + ); + } + + #[test] + fn validation_fits_per_execution_with_counted_host_memory() { + let metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + 100, + MemoryCeilingScope::PerExecution, + Some(20), + )), + known(population(1)), + known(HostIngressMemoryAccounting::CountsTowardCeiling), + ); + let envelope = MemoryEnvelope::new(MemoryCeilingScope::PerExecution, 40, 30, Some(15)); + + assert_eq!(population(1).max_live_requests(), NonZeroU32::MIN); + let validation: crate::MemoryEnvelopeValidation = metadata + .validate_memory_envelope(envelope) + .expect("valid envelope should produce a validation result"); + assert_eq!( + validation, + crate::MemoryEnvelopeValidation::Fits { + available_primary_bytes: 100, + available_separate_stack_bytes: Some(20), + required_primary_bytes: 70, + required_separate_stack_bytes: Some(15), + } + ); + } + + #[test] + fn validation_fits_per_instance_with_host_memory_outside_ceiling() { + let metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + 1_000, + MemoryCeilingScope::PerInstance, + None, + )), + known(population(3)), + known(HostIngressMemoryAccounting::OutsideCeiling), + ); + let envelope = MemoryEnvelope::new(MemoryCeilingScope::PerInstance, 100, 200, None); + + assert_eq!( + metadata.validate_memory_envelope(envelope), + Ok(crate::MemoryEnvelopeValidation::Fits { + available_primary_bytes: 1_000, + available_separate_stack_bytes: None, + required_primary_bytes: 700, + required_separate_stack_bytes: None, + }) + ); + } + + #[test] + fn validation_known_minimum_excess_precedes_unknown_facts() { + let primary_metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + 25, + MemoryCeilingScope::PerInstance, + Some(5), + )), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + ); + let primary_envelope = MemoryEnvelope::new(MemoryCeilingScope::PerInstance, 20, 10, None); + + assert_eq!( + primary_metadata.validate_memory_envelope(primary_envelope), + Ok(crate::MemoryEnvelopeValidation::Exceeds { + available_bytes: 25, + required_bytes: 30, + resource: crate::MemoryResource::Primary, + }) + ); + + let stack_metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + 100, + MemoryCeilingScope::PerInstance, + Some(5), + )), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + ); + let stack_envelope = MemoryEnvelope::new(MemoryCeilingScope::PerInstance, 20, 10, Some(6)); + + assert_eq!( + stack_metadata.validate_memory_envelope(stack_envelope), + Ok(crate::MemoryEnvelopeValidation::Exceeds { + available_bytes: 5, + required_bytes: 6, + resource: crate::MemoryResource::SeparateStack, + }) + ); + } + + #[test] + fn validation_missing_published_stack_requirement_is_indeterminate() { + let metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + 100, + MemoryCeilingScope::PerExecution, + Some(20), + )), + known(population(1)), + known(HostIngressMemoryAccounting::OutsideCeiling), + ); + let envelope = MemoryEnvelope::new(MemoryCeilingScope::PerExecution, 20, 10, None); + + assert_eq!( + metadata.validate_memory_envelope(envelope), + Ok(crate::MemoryEnvelopeValidation::Indeterminate { + available_primary_bytes: Some(100), + minimum_required_primary_bytes: 30, + reasons: vec![ + crate::MemoryValidationIndeterminateReason::MissingSeparateStackRequirement, + ], + }) + ); + } + + #[test] + fn validation_rejects_non_unit_per_execution_population() { + let metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + 100, + MemoryCeilingScope::PerExecution, + None, + )), + known(population(2)), + known(HostIngressMemoryAccounting::OutsideCeiling), + ); + let envelope = MemoryEnvelope::new(MemoryCeilingScope::PerExecution, 20, 10, None); + let validation: Result< + crate::MemoryEnvelopeValidation, + crate::MemoryEnvelopeValidationError, + > = metadata.validate_memory_envelope(envelope); + + assert_eq!( + validation, + Err( + crate::MemoryEnvelopeValidationError::NonUnitPerExecutionPopulation { + max_live_requests: population(2).max_live_requests(), + }, + ) + ); + } + + #[test] + fn validation_rejects_primary_arithmetic_overflow() { + let metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + u64::MAX, + MemoryCeilingScope::PerInstance, + None, + )), + known(population(1)), + known(HostIngressMemoryAccounting::CountsTowardCeiling), + ); + let envelope = MemoryEnvelope::new(MemoryCeilingScope::PerInstance, u64::MAX, 1, None); + + assert_eq!( + metadata.validate_memory_envelope(envelope), + Err(crate::MemoryEnvelopeValidationError::ArithmeticOverflow { + fixed_bytes: u64::MAX, + max_live_requests: Some(NonZeroU32::MIN), + per_live_request_bytes: 1, + }) + ); + + let multiplication_metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + u64::MAX, + MemoryCeilingScope::PerInstance, + None, + )), + known(population(2)), + known(HostIngressMemoryAccounting::CountsTowardCeiling), + ); + let multiplication_envelope = + MemoryEnvelope::new(MemoryCeilingScope::PerInstance, 0, u64::MAX, None); + + assert_eq!( + multiplication_metadata.validate_memory_envelope(multiplication_envelope), + Err(crate::MemoryEnvelopeValidationError::ArithmeticOverflow { + fixed_bytes: 0, + max_live_requests: Some(population(2).max_live_requests()), + per_live_request_bytes: u64::MAX, + }) + ); + } + + #[test] + fn validation_rejects_scope_mismatch() { + let metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + 100, + MemoryCeilingScope::PerExecution, + None, + )), + known(population(1)), + known(HostIngressMemoryAccounting::OutsideCeiling), + ); + let envelope = MemoryEnvelope::new(MemoryCeilingScope::PerInstance, 20, 10, None); + + assert_eq!( + metadata.validate_memory_envelope(envelope), + Err(crate::MemoryEnvelopeValidationError::ScopeMismatch { + ceiling_scope: MemoryCeilingScope::PerExecution, + envelope_scope: MemoryCeilingScope::PerInstance, + }) + ); + } + + #[test] + fn validation_rejects_stack_shape_mismatch() { + let metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + 100, + MemoryCeilingScope::PerExecution, + None, + )), + known(population(1)), + known(HostIngressMemoryAccounting::OutsideCeiling), + ); + let envelope = MemoryEnvelope::new(MemoryCeilingScope::PerExecution, 20, 10, Some(5)); + + assert_eq!( + metadata.validate_memory_envelope(envelope), + Err( + crate::MemoryEnvelopeValidationError::UnexpectedSeparateStackRequirement { + required_bytes: 5, + }, + ) + ); + } + + #[test] + fn validation_rejects_unknown_population_lower_bound_overflow() { + let metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + u64::MAX, + MemoryCeilingScope::PerInstance, + None, + )), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + known(HostIngressMemoryAccounting::OutsideCeiling), + ); + let envelope = MemoryEnvelope::new(MemoryCeilingScope::PerInstance, u64::MAX, 1, None); + + assert_eq!( + metadata.validate_memory_envelope(envelope), + Err(crate::MemoryEnvelopeValidationError::ArithmeticOverflow { + fixed_bytes: u64::MAX, + max_live_requests: None, + per_live_request_bytes: 1, + }) + ); + } + + #[test] + fn validation_reports_primary_or_stack_excess() { + let metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + 100, + MemoryCeilingScope::PerExecution, + Some(20), + )), + known(population(1)), + known(HostIngressMemoryAccounting::CountsTowardCeiling), + ); + + assert_eq!( + metadata.validate_memory_envelope(MemoryEnvelope::new( + MemoryCeilingScope::PerExecution, + 80, + 30, + Some(15), + )), + Ok(crate::MemoryEnvelopeValidation::Exceeds { + available_bytes: 100, + required_bytes: 110, + resource: crate::MemoryResource::Primary, + }) + ); + assert_eq!( + metadata.validate_memory_envelope(MemoryEnvelope::new( + MemoryCeilingScope::PerExecution, + 40, + 30, + Some(21), + )), + Ok(crate::MemoryEnvelopeValidation::Exceeds { + available_bytes: 20, + required_bytes: 21, + resource: crate::MemoryResource::SeparateStack, + }) + ); + assert_eq!( + metadata.validate_memory_envelope(MemoryEnvelope::new( + MemoryCeilingScope::PerExecution, + 80, + 30, + Some(21), + )), + Ok(crate::MemoryEnvelopeValidation::Exceeds { + available_bytes: 100, + required_bytes: 110, + resource: crate::MemoryResource::Primary, + }) + ); + } + + #[test] + fn validation_unknown_ceiling_is_indeterminate() { + let metadata = PlatformMetadata::new( + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + PlatformFact::unknown(PlatformUnknownReason::RuntimeConfigured), + PlatformFact::unknown(PlatformUnknownReason::OperatorConfigured), + ); + let envelope = MemoryEnvelope::new(MemoryCeilingScope::PerInstance, 10, 20, None); + + assert_eq!( + metadata.validate_memory_envelope(envelope), + Ok(crate::MemoryEnvelopeValidation::Indeterminate { + available_primary_bytes: None, + minimum_required_primary_bytes: 30, + reasons: vec![ + crate::MemoryValidationIndeterminateReason::UnknownMemoryCeiling { + reason: PlatformUnknownReason::ProviderUnpublished, + }, + crate::MemoryValidationIndeterminateReason::UnknownInboundRequestPopulation { + reason: PlatformUnknownReason::RuntimeConfigured, + }, + crate::MemoryValidationIndeterminateReason::UnknownHostIngressMemoryAccounting { + reason: PlatformUnknownReason::OperatorConfigured, + }, + ], + }) + ); + } + + #[test] + fn validation_unknown_host_accounting_is_indeterminate() { + let metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + 100, + MemoryCeilingScope::PerExecution, + None, + )), + known(population(1)), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + ); + let envelope = MemoryEnvelope::new(MemoryCeilingScope::PerExecution, 20, 10, None); + + assert_eq!( + metadata.validate_memory_envelope(envelope), + Ok(crate::MemoryEnvelopeValidation::Indeterminate { + available_primary_bytes: Some(100), + minimum_required_primary_bytes: 30, + reasons: vec![ + crate::MemoryValidationIndeterminateReason::UnknownHostIngressMemoryAccounting { + reason: PlatformUnknownReason::ProviderUnpublished, + }, + ], + }) + ); + } + + #[test] + fn validation_unknown_population_is_indeterminate() { + let metadata = PlatformMetadata::new( + known(MemoryCeiling::new( + 100, + MemoryCeilingScope::PerInstance, + Some(20), + )), + PlatformFact::unknown(PlatformUnknownReason::ProviderUnpublished), + PlatformFact::unknown(PlatformUnknownReason::RuntimeConfigured), + ); + let envelope = MemoryEnvelope::new(MemoryCeilingScope::PerInstance, 10, 20, None); + + assert_eq!( + metadata.validate_memory_envelope(envelope), + Ok(crate::MemoryEnvelopeValidation::Indeterminate { + available_primary_bytes: Some(100), + minimum_required_primary_bytes: 30, + reasons: vec![ + crate::MemoryValidationIndeterminateReason::UnknownInboundRequestPopulation { + reason: PlatformUnknownReason::ProviderUnpublished, + }, + crate::MemoryValidationIndeterminateReason::UnknownHostIngressMemoryAccounting { + reason: PlatformUnknownReason::RuntimeConfigured, + }, + crate::MemoryValidationIndeterminateReason::MissingSeparateStackRequirement, + ], + }) + ); + } +} diff --git a/crates/edgezero-core/src/proxy.rs b/crates/edgezero-core/src/proxy.rs deleted file mode 100644 index 1a5537772..000000000 --- a/crates/edgezero-core/src/proxy.rs +++ /dev/null @@ -1,656 +0,0 @@ -use std::fmt; -use std::sync::Arc; - -use async_trait::async_trait; - -use crate::body::Body; -use crate::error::EdgeError; -use crate::http::{ - Extensions, HeaderMap, Method, Request, Response, StatusCode, Uri, response_builder, -}; - -/// Header name attached to proxied responses to identify which adapter -/// forwarded the request (e.g. "fastly", "cloudflare", "spin"). -pub const PROXY_HEADER: &str = "x-edgezero-proxy"; - -#[async_trait(?Send)] -pub trait ProxyClient: Send + Sync { - async fn send(&self, request: ProxyRequest) -> Result; -} - -#[derive(Clone)] -pub struct ProxyHandle { - client: Arc, -} - -impl ProxyHandle { - #[must_use] - #[inline] - pub fn client(&self) -> Arc { - Arc::clone(&self.client) - } - - /// # Errors - /// Returns [`EdgeError`] if the underlying [`ProxyClient`] fails or the - /// response cannot be assembled. - #[inline] - pub async fn forward(&self, request: ProxyRequest) -> Result { - let response = self.client.send(request).await?; - response.into_response() - } - - #[inline] - pub fn new(client: Arc) -> Self { - Self { client } - } - - #[inline] - pub fn with_client(client: C) -> Self - where - C: ProxyClient + 'static, - { - Self { - client: Arc::new(client), - } - } -} - -/// Outbound request description for a proxy operation. -pub struct ProxyRequest { - body: Body, - extensions: Extensions, - headers: HeaderMap, - method: Method, - uri: Uri, -} - -impl fmt::Debug for ProxyRequest { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ProxyRequest") - .field("method", &self.method) - .field("uri", &self.uri) - .field("headers", &self.headers) - .finish_non_exhaustive() - } -} - -impl ProxyRequest { - #[inline] - pub fn body(&self) -> &Body { - &self.body - } - - #[inline] - pub fn body_mut(&mut self) -> &mut Body { - &mut self.body - } - - #[inline] - pub fn extensions(&self) -> &Extensions { - &self.extensions - } - - #[inline] - pub fn extensions_mut(&mut self) -> &mut Extensions { - &mut self.extensions - } - - #[inline] - pub fn from_request(request: Request, uri: Uri) -> Self { - let (parts, body) = request.into_parts(); - Self { - body, - extensions: parts.extensions, - headers: parts.headers, - method: parts.method, - uri, - } - } - - #[inline] - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - #[inline] - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.headers - } - - #[inline] - pub fn into_parts(self) -> (Method, Uri, HeaderMap, Body, Extensions) { - ( - self.method, - self.uri, - self.headers, - self.body, - self.extensions, - ) - } - - #[inline] - pub fn method(&self) -> &Method { - &self.method - } - - #[inline] - pub fn new(method: Method, uri: Uri) -> Self { - Self { - body: Body::empty(), - extensions: Extensions::new(), - headers: HeaderMap::new(), - method, - uri, - } - } - - #[inline] - pub fn uri(&self) -> &Uri { - &self.uri - } -} - -pub struct ProxyResponse { - body: Body, - extensions: Extensions, - headers: HeaderMap, - status: StatusCode, -} - -impl fmt::Debug for ProxyResponse { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ProxyResponse") - .field("status", &self.status) - .finish_non_exhaustive() - } -} - -impl ProxyResponse { - #[inline] - pub fn body(&self) -> &Body { - &self.body - } - - #[inline] - pub fn body_mut(&mut self) -> &mut Body { - &mut self.body - } - - #[inline] - pub fn extensions(&self) -> &Extensions { - &self.extensions - } - - #[inline] - pub fn extensions_mut(&mut self) -> &mut Extensions { - &mut self.extensions - } - - #[inline] - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - #[inline] - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.headers - } - - /// # Errors - /// Returns [`EdgeError::internal`] if the underlying `http::Response::builder()` - /// rejects a header — should be unreachable since we only store names/values - /// that were already validated, but propagation lets a faulty upstream stream - /// fail the request instead of crashing the worker. - #[inline] - pub fn into_response(self) -> Result { - let mut builder = response_builder().status(self.status); - for (name, value) in &self.headers { - builder = builder.header(name, value); - } - builder.body(self.body).map_err(EdgeError::internal) - } - - #[inline] - pub fn new(status: StatusCode, body: Body) -> Self { - Self { - body, - extensions: Extensions::new(), - headers: HeaderMap::new(), - status, - } - } - - #[inline] - pub fn status(&self) -> StatusCode { - self.status - } -} - -pub struct ProxyService { - client: C, -} - -impl ProxyService { - #[inline] - pub fn new(client: C) -> Self { - Self { client } - } -} - -impl ProxyService -where - C: ProxyClient, -{ - /// # Errors - /// Returns [`EdgeError`] if the underlying [`ProxyClient`] fails or the - /// response cannot be assembled. - #[inline] - pub async fn forward(&self, request: ProxyRequest) -> Result { - let response = self.client.send(request).await?; - response.into_response() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::body::Body; - use crate::http::header::HeaderName; - use crate::http::{HeaderValue, Method, StatusCode, Uri, request_builder}; - use bytes::Bytes; - use futures::executor::block_on; - use futures_util::{StreamExt as _, stream}; - - struct EchoBodyClient; - - struct EchoHeadersClient; - - struct EchoMethodClient; - - struct ErrorClient; - - struct StreamingClient; - - struct TestClient; - - #[async_trait(?Send)] - impl ProxyClient for EchoBodyClient { - async fn send(&self, request: ProxyRequest) -> Result { - let (_, _, _, body, _) = request.into_parts(); - Ok(ProxyResponse::new(StatusCode::OK, body)) - } - } - - #[async_trait(?Send)] - impl ProxyClient for EchoHeadersClient { - async fn send(&self, request: ProxyRequest) -> Result { - let mut resp = ProxyResponse::new(StatusCode::OK, Body::empty()); - // Echo back headers with x-echo- prefix - for (name, value) in request.headers() { - let echo_name = format!("x-echo-{}", name.as_str()); - if let Ok(header_name) = echo_name.parse::() { - resp.headers_mut().insert(header_name, value.clone()); - } - } - Ok(resp) - } - } - - #[async_trait(?Send)] - impl ProxyClient for EchoMethodClient { - async fn send(&self, request: ProxyRequest) -> Result { - let method_str = request.method().as_str(); - Ok(ProxyResponse::new( - StatusCode::OK, - Body::from(method_str.to_owned()), - )) - } - } - - #[async_trait(?Send)] - impl ProxyClient for ErrorClient { - async fn send(&self, _request: ProxyRequest) -> Result { - Err(EdgeError::bad_request("connection failed")) - } - } - - #[async_trait(?Send)] - impl ProxyClient for StreamingClient { - async fn send(&self, request: ProxyRequest) -> Result { - let (_method, _uri, _headers, _body, _ext) = request.into_parts(); - let chunks = stream::iter(vec![ - Bytes::from_static(b"stream-one"), - Bytes::from_static(b"stream-two"), - ]); - Ok(ProxyResponse::new(StatusCode::OK, Body::stream(chunks))) - } - } - - #[async_trait(?Send)] - impl ProxyClient for TestClient { - async fn send(&self, request: ProxyRequest) -> Result { - let (method, uri, headers, _body, _) = request.into_parts(); - assert_eq!(method, Method::GET); - assert_eq!(uri, Uri::from_static("https://example.com")); - assert_eq!( - headers.get("x-demo"), - Some(&HeaderValue::from_static("true")) - ); - - let chunks = stream::iter(vec![ - Bytes::from_static(b"hello"), - Bytes::from_static(b" world"), - ]); - Ok(ProxyResponse::new(StatusCode::OK, Body::stream(chunks))) - } - } - - fn collect_body(body: Body) -> Vec { - match body { - Body::Once(bytes) => bytes.to_vec(), - Body::Stream(mut stream) => block_on(async { - let mut data = Vec::new(); - while let Some(result) = stream.next().await { - let chunk = result.expect("chunk"); - data.extend_from_slice(&chunk); - } - data - }), - } - } - - #[test] - fn proxy_forward_preserves_streaming_body() { - let request = request_builder() - .method(Method::GET) - .uri("/local-stream") - .body(Body::empty()) - .expect("request"); - - let target = Uri::from_static("https://example.com/stream"); - let proxy_request = ProxyRequest::from_request(request, target); - let service = ProxyService::new(StreamingClient); - let response = block_on(service.forward(proxy_request)).expect("response"); - assert_eq!(response.status(), StatusCode::OK); - - let body = response.into_body(); - let collected = collect_body(body); - assert_eq!(collected, b"stream-onestream-two"); - } - - #[test] - fn proxy_forward_roundtrips() { - let request = request_builder() - .method(Method::GET) - .uri("/local") - .header("x-demo", "true") - .body(Body::empty()) - .expect("request"); - - let target = Uri::from_static("https://example.com"); - let proxy_request = ProxyRequest::from_request(request, target); - let service = ProxyService::new(TestClient); - let response = block_on(service.forward(proxy_request)).expect("response"); - assert_eq!(response.status(), StatusCode::OK); - } - - #[test] - fn proxy_forwards_request_body() { - let service = ProxyService::new(EchoBodyClient); - let request = request_builder() - .method(Method::POST) - .uri("/test") - .body(Body::from("request body content")) - .expect("request"); - - let proxy_req = - ProxyRequest::from_request(request, Uri::from_static("https://example.com")); - let response = block_on(service.forward(proxy_req)).expect("response"); - - let body_bytes = collect_body(response.into_body()); - assert_eq!(body_bytes, b"request body content"); - } - - #[test] - fn proxy_forwards_request_headers() { - let service = ProxyService::new(EchoHeadersClient); - let request = request_builder() - .method(Method::GET) - .uri("/test") - .header("x-custom-header", "custom-value") - .header("authorization", "Bearer token123") - .body(Body::empty()) - .expect("request"); - - let proxy_req = - ProxyRequest::from_request(request, Uri::from_static("https://example.com")); - let response = block_on(service.forward(proxy_req)).expect("response"); - - assert_eq!( - response - .headers() - .get("x-echo-x-custom-header") - .and_then(|value| value.to_str().ok()), - Some("custom-value") - ); - assert_eq!( - response - .headers() - .get("x-echo-authorization") - .and_then(|value| value.to_str().ok()), - Some("Bearer token123") - ); - } - - #[test] - fn proxy_forwards_various_methods() { - let service = ProxyService::new(EchoMethodClient); - - for method in [ - Method::GET, - Method::POST, - Method::PUT, - Method::DELETE, - Method::PATCH, - Method::HEAD, - Method::OPTIONS, - ] { - let req = ProxyRequest::new(method.clone(), Uri::from_static("https://example.com")); - let response = block_on(service.forward(req)).expect("response"); - assert_eq!(response.status(), StatusCode::OK); - } - } - - #[test] - fn proxy_handle_forward_returns_response() { - let handle = ProxyHandle::with_client(TestClient); - let request = request_builder() - .method(Method::GET) - .uri("/test") - .header("x-demo", "true") - .body(Body::empty()) - .expect("request"); - - let proxy_req = - ProxyRequest::from_request(request, Uri::from_static("https://example.com")); - let response = block_on(handle.forward(proxy_req)).expect("response"); - assert_eq!(response.status(), StatusCode::OK); - } - - #[test] - fn proxy_handle_new_wraps_client() { - let client = Arc::new(TestClient); - let handle = ProxyHandle::new(client); - assert!(Arc::strong_count(&handle.client()) >= 1); - } - - #[test] - fn proxy_handle_propagates_client_errors() { - let handle = ProxyHandle::with_client(ErrorClient); - let req = ProxyRequest::new(Method::GET, Uri::from_static("https://example.com")); - block_on(handle.forward(req)).expect_err("ErrorClient propagates an error"); - } - - #[test] - fn proxy_handle_with_client_creates_arc() { - let handle = ProxyHandle::with_client(TestClient); - assert!(Arc::strong_count(&handle.client()) >= 1); - } - - #[test] - fn proxy_request_body_mut_allows_modification() { - let mut req = ProxyRequest::new(Method::POST, Uri::from_static("https://example.com")); - *req.body_mut() = Body::from("new body content"); - assert!(matches!( - req.body(), - Body::Once(bytes) if bytes.as_ref() == b"new body content" - )); - } - - #[test] - fn proxy_request_debug_format() { - let mut req = ProxyRequest::new(Method::GET, Uri::from_static("https://example.com")); - req.headers_mut() - .insert("x-debug", HeaderValue::from_static("test")); - let debug = format!("{req:?}"); - assert!(debug.contains("ProxyRequest")); - assert!(debug.contains("GET")); - assert!(debug.contains("example.com")); - } - - #[test] - fn proxy_request_extensions_mut_allows_modification() { - let mut req = ProxyRequest::new(Method::GET, Uri::from_static("https://example.com")); - req.extensions_mut().insert("custom-data".to_owned()); - assert_eq!( - req.extensions().get::(), - Some(&"custom-data".to_owned()) - ); - } - - #[test] - fn proxy_request_from_request_preserves_all_parts() { - let request = request_builder() - .method(Method::POST) - .uri("/original") - .header("x-custom", "value") - .body(Body::from("request body")) - .expect("request"); - - let target = Uri::from_static("https://backend.example.com/api"); - let proxy_req = ProxyRequest::from_request(request, target.clone()); - - assert_eq!(proxy_req.method(), &Method::POST); - assert_eq!(proxy_req.uri(), &target); - assert_eq!( - proxy_req - .headers() - .get("x-custom") - .and_then(|value| value.to_str().ok()), - Some("value") - ); - } - - #[test] - fn proxy_request_headers_mut_allows_modification() { - let mut req = ProxyRequest::new(Method::GET, Uri::from_static("https://example.com")); - req.headers_mut() - .insert("authorization", HeaderValue::from_static("Bearer token")); - assert!(req.headers().get("authorization").is_some()); - } - - #[test] - fn proxy_request_into_parts_destructures() { - let mut req = ProxyRequest::new( - Method::DELETE, - Uri::from_static("https://example.com/resource"), - ); - req.headers_mut() - .insert("x-test", HeaderValue::from_static("value")); - *req.body_mut() = Body::from("body"); - - let (method, uri, headers, body, _extensions) = req.into_parts(); - assert_eq!(method, Method::DELETE); - assert_eq!(uri, Uri::from_static("https://example.com/resource")); - assert!(headers.get("x-test").is_some()); - assert!(matches!( - &body, - Body::Once(bytes) if bytes.as_ref() == b"body" - )); - } - - #[test] - fn proxy_request_new_creates_empty_request() { - let req = ProxyRequest::new(Method::GET, Uri::from_static("https://example.com")); - assert_eq!(req.method(), &Method::GET); - assert_eq!(req.uri(), &Uri::from_static("https://example.com")); - assert!(req.headers().is_empty()); - assert!(matches!(req.body(), Body::Once(bytes) if bytes.is_empty())); - } - - #[test] - fn proxy_response_body_mut_allows_modification() { - let mut resp = ProxyResponse::new(StatusCode::OK, Body::empty()); - *resp.body_mut() = Body::from("updated body"); - assert!(matches!( - resp.body(), - Body::Once(bytes) if bytes.as_ref() == b"updated body" - )); - } - - #[test] - fn proxy_response_debug_format() { - let resp = ProxyResponse::new(StatusCode::NOT_FOUND, Body::empty()); - let debug = format!("{resp:?}"); - assert!(debug.contains("ProxyResponse")); - assert!(debug.contains("404")); - } - - #[test] - fn proxy_response_extensions_mut_allows_modification() { - let mut resp = ProxyResponse::new(StatusCode::OK, Body::empty()); - resp.extensions_mut().insert(42_i32); - assert_eq!(resp.extensions().get::(), Some(&42_i32)); - } - - #[test] - fn proxy_response_headers_mut_allows_modification() { - let mut resp = ProxyResponse::new(StatusCode::OK, Body::empty()); - resp.headers_mut() - .insert("content-type", HeaderValue::from_static("application/json")); - assert!(resp.headers().get("content-type").is_some()); - } - - #[test] - fn proxy_response_into_response_converts() { - let mut resp = ProxyResponse::new(StatusCode::CREATED, Body::from("created")); - resp.headers_mut() - .insert("x-custom", HeaderValue::from_static("header")); - - let http_resp = resp.into_response().expect("response"); - assert_eq!(http_resp.status(), StatusCode::CREATED); - assert!(http_resp.headers().get("x-custom").is_some()); - } - - #[test] - fn proxy_response_new_creates_response() { - let resp = ProxyResponse::new(StatusCode::OK, Body::from("response body")); - assert_eq!(resp.status(), StatusCode::OK); - assert!(matches!( - resp.body(), - Body::Once(bytes) if bytes.as_ref() == b"response body" - )); - } - - #[test] - fn proxy_service_propagates_client_errors() { - let service = ProxyService::new(ErrorClient); - let req = ProxyRequest::new(Method::GET, Uri::from_static("https://example.com")); - let result = block_on(service.forward(req)); - assert!(result.is_err()); - let err = result.unwrap_err(); - assert_eq!(err.status(), StatusCode::BAD_REQUEST); - } -} diff --git a/crates/edgezero-core/src/response.rs b/crates/edgezero-core/src/response.rs index 8040035b5..93ae4e735 100644 --- a/crates/edgezero-core/src/response.rs +++ b/crates/edgezero-core/src/response.rs @@ -95,7 +95,7 @@ pub fn response_with_body(status: StatusCode, body: Body) -> Result { + error_kind: &'static str, + request_method: &'head Method, + request_start: MonotonicInstant, + status: StatusCode, +} + +impl<'head> DetachedResponseEgressHead<'head> { + /// Returns the stable public error category without diagnostic text. + #[must_use] + #[inline] + pub const fn error_kind(&self) -> &'static str { + self.error_kind + } + + #[must_use] + #[inline] + pub(crate) const fn new( + request_method: &'head Method, + request_start: MonotonicInstant, + status: StatusCode, + error_kind: &'static str, + ) -> Self { + Self { + error_kind, + request_method, + request_start, + status, + } + } + + /// Returns the request method captured before body polling. + #[must_use] + #[inline] + pub const fn request_method(&self) -> &'head Method { + self.request_method + } + + /// Returns the request start in the application's monotonic clock domain. + #[must_use] + #[inline] + pub const fn request_start(&self) -> MonotonicInstant { + self.request_start + } + + /// Returns the status of the bounded error response. + #[must_use] + #[inline] + pub const fn status(&self) -> StatusCode { + self.status + } + + /// Builds a response-write deadline relative to this head's captured request start. + /// + /// The duration is clamped to [`DEADLINE_FAR_FUTURE`]. Arithmetic overflow fails closed by + /// returning a deadline at the captured request start. + #[must_use] + #[inline] + pub fn write_deadline_after(&self, duration: Duration) -> Deadline { + let bounded = duration.min(DEADLINE_FAR_FUTURE); + Deadline::at_instant( + self.request_start + .checked_add(bounded) + .unwrap_or(self.request_start), + ) + } +} + +/// Application decision for a normalized ingress error before response construction. +#[non_exhaustive] +pub enum DetachedResponseEgressDecision { + /// Terminates the request through the adapter's non-response abort/error boundary. + Abort, + /// Constructs detached egress and retains the supplied completion until terminal delivery. + Send { + completion: ResponseEgressCompletion, + deadline: Deadline, + }, +} + +/// Application-selected upper bound for one response's egress lifetime. +/// +/// Insert this value into the response extensions before returning the response. `EdgeZero` +/// resolves it when egress begins, exposes the resulting absolute deadline to the egress policy +/// callback, and enforces the earlier of that deadline and the callback-selected write deadline. +#[derive(Clone, Copy, Debug)] +pub struct ResponseEgressDeadline(ResponseEgressDeadlineKind); + +#[derive(Clone, Copy, Debug)] +enum ResponseEgressDeadlineKind { + After(Duration), + At(Deadline), +} + +impl ResponseEgressDeadline { + /// Selects a deadline relative to the instant response egress begins. + /// + /// The duration is clamped to [`DEADLINE_FAR_FUTURE`]. Arithmetic overflow fails closed by + /// resolving to the sampled egress-start instant. + #[must_use] + #[inline] + pub const fn after(duration: Duration) -> Self { + Self(ResponseEgressDeadlineKind::After(duration)) + } + + /// Selects an absolute deadline in the application's monotonic clock domain. + #[must_use] + #[inline] + pub const fn at(deadline: Deadline) -> Self { + Self(ResponseEgressDeadlineKind::At(deadline)) + } + + #[must_use] + #[inline] + fn resolve_at(self, egress_started_at: MonotonicInstant) -> Deadline { + match self.0 { + ResponseEgressDeadlineKind::At(deadline) => deadline, + ResponseEgressDeadlineKind::After(duration) => Deadline::at_instant( + egress_started_at + .checked_add(duration.min(DEADLINE_FAR_FUTURE)) + .unwrap_or(egress_started_at), + ), + } + } +} + +/// One finite absolute deadline selected for a response-conversion attempt. +#[derive(Clone, Copy, Debug)] +pub struct ResponseEgressPolicy { + pub write_deadline: Deadline, +} + +impl ResponseEgressPolicy { + /// Clamps this policy to the portable far-future bound relative to egress start. + /// + /// An already-expired deadline remains valid and is not moved forward. + /// + /// # Errors + /// Returns [`ResponseEgressOutcome::ConversionError`] if the clamp instant cannot be + /// represented by the monotonic clock. + #[inline] + pub fn normalize_at( + mut self, + egress_started_at: MonotonicInstant, + ) -> Result { + let maximum = egress_started_at + .checked_add(DEADLINE_FAR_FUTURE) + .ok_or(ResponseEgressOutcome::ConversionError)?; + if self.write_deadline.instant() > maximum { + self.write_deadline = Deadline::at_instant(maximum); + } + Ok(self) + } +} + +/// Immutable response metadata visible to the synchronous egress policy callback. +/// +/// This type deliberately has no response-body or mutation access. +#[derive(Clone, Copy, Debug)] +pub struct ResponseEgressHead<'head> { + application_deadline: Option, + headers: &'head HeaderMap, + request_start: MonotonicInstant, + route: Option<&'head RouteMetadata>, + status: StatusCode, + version: Version, +} + +impl<'head> ResponseEgressHead<'head> { + /// Returns the response-owned deadline after resolving it to an absolute instant. + #[must_use] + #[inline] + pub fn application_deadline(&self) -> Option { + self.application_deadline + } + + #[must_use] + #[inline] + pub fn headers(&self) -> &'head HeaderMap { + self.headers + } + + /// Creates a body-blind response-head view for an adapter conversion attempt. + #[must_use] + #[inline] + pub fn new( + status: StatusCode, + version: Version, + headers: &'head HeaderMap, + application_deadline: Option, + request_start: MonotonicInstant, + route: Option<&'head RouteMetadata>, + ) -> Self { + Self { + application_deadline, + headers, + request_start, + route, + status, + version, + } + } + + #[must_use] + #[inline] + pub fn request_start(&self) -> MonotonicInstant { + self.request_start + } + + #[must_use] + #[inline] + pub fn route(&self) -> Option<&'head RouteMetadata> { + self.route + } + + #[must_use] + #[inline] + pub fn status(&self) -> StatusCode { + self.status + } + + #[must_use] + #[inline] + pub fn version(&self) -> Version { + self.version + } +} + +/// Synchronous, body-blind policy callback retained by an application. +pub type ResponseEgressPolicyCallback = Arc< + dyn for<'head> Fn(&ResponseEgressHead<'head>, MonotonicInstant) -> ResponseEgressPolicy + + Send + + Sync + + 'static, +>; + +/// Terminal result of one response-conversion attempt. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ResponseEgressOutcome { + ClientDisconnected, + Completed, + ConversionError, + DeadlineExceeded, + HostHandoff, + RequestCancelled, + SourceError, + TransportError, + Unspecified, +} + +/// Identifies which bounded response body contributed the reported byte count. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ResponseEgressBodyKind { + Application, + Fallback, +} + +/// Records whether a selected fallback reached its finish boundary. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ResponseEgressFallbackDisposition { + Aborted, + Completed, +} + +/// Independent, bounded precommit framing observations for the application and fallback body. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct ResponseEgressFramingFailures { + application: Option, + fallback: Option, +} + +impl ResponseEgressFramingFailures { + #[must_use] + #[inline] + pub const fn application(&self) -> Option { + self.application + } + + #[must_use] + #[inline] + pub const fn fallback(&self) -> Option { + self.fallback + } +} + +/// Bounded terminal report for one response-conversion attempt. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ResponseEgressReport { + pub body_kind: ResponseEgressBodyKind, + pub bytes_written: u64, + pub elapsed: Duration, + pub fallback_disposition: Option, + /// First precommit framing rejection for each body kind; never a wire diagnostic. + pub framing_failures: ResponseEgressFramingFailures, + pub outcome: ResponseEgressOutcome, + pub request_start: MonotonicInstant, + pub route: Option, + /// First converter-observed body-source error, independent of the terminal outcome. + pub source_failure: Option, +} + +/// Text-free snapshot of a body-source error observed during response conversion. +/// +/// This is observation evidence, not the terminal cause. A deadline/cancellation can still win, +/// and an application-body observation remains tagged as such when fallback delivery follows. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ResponseEgressSourceFailure { + body_kind: ResponseEgressBodyKind, + category: &'static str, + provider_detail: Option, +} + +impl ResponseEgressSourceFailure { + #[must_use] + #[inline] + pub const fn body_kind(&self) -> ResponseEgressBodyKind { + self.body_kind + } + + /// Returns the existing fixed source-error category, never its message or provider text. + #[must_use] + #[inline] + pub const fn category(&self) -> &'static str { + self.category + } + + #[must_use] + #[inline] + pub const fn provider_failure_detail(&self) -> Option<&ProviderFailureDetail> { + self.provider_detail.as_ref() + } +} + +type ResponseEgressCompletionCallback = Box; + +/// Non-clone owner of one response-scoped terminal callback. +/// +/// The callback moves with the ingress/egress lifecycle and runs at most once. Use [`Self::empty`] +/// when no application resource needs to remain live through response transmission. +/// +/// ```compile_fail +/// use edgezero_core::ResponseEgressCompletion; +/// +/// let completion = ResponseEgressCompletion::empty(); +/// let _duplicate = completion.clone(); +/// ``` +/// +/// ```compile_fail +/// use edgezero_core::{Extensions, ResponseEgressCompletion}; +/// +/// let mut extensions = Extensions::new(); +/// extensions.insert(ResponseEgressCompletion::empty()); +/// ``` +pub struct ResponseEgressCompletion { + callback: Option, +} + +/// Non-clone installer for one application resource retained through response egress. +/// +/// Create a paired installer and completion with [`ResponseEgressCompletion::late_bound`]. The +/// installer remains usable after the completion closes only to report [`ResponseEgressResourceInstallError::Closed`]. +/// +/// ```compile_fail +/// use edgezero_core::ResponseEgressCompletion; +/// +/// let (resource, _completion) = ResponseEgressCompletion::late_bound::>(); +/// let _duplicate = resource.clone(); +/// ``` +pub struct ResponseEgressResource { + state: Arc>>, +} + +impl fmt::Debug for ResponseEgressResource { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ResponseEgressResource") + .finish_non_exhaustive() + } +} + +impl ResponseEgressResource +where + T: Send + 'static, +{ + /// Installs the response-scoped resource exactly once. + /// + /// A rejected resource is dropped before this method returns and is never returned to the + /// caller. `Closed` covers both terminal completion and completion abandonment. + /// + /// # Errors + /// + /// Returns [`ResponseEgressResourceInstallError::AlreadyInstalled`] when the slot already + /// owns a resource, or [`ResponseEgressResourceInstallError::Closed`] after terminal egress or + /// completion abandonment. + #[inline] + pub fn install(&self, resource: T) -> Result<(), ResponseEgressResourceInstallError> { + let mut candidate = Some(resource); + let install_error = { + let mut state = self.state(); + if state.closed { + Some(ResponseEgressResourceInstallError::Closed) + } else if state.resource.is_some() { + Some(ResponseEgressResourceInstallError::AlreadyInstalled) + } else { + state.resource = candidate.take(); + None + } + }; + drop(candidate); + match install_error { + Some(rejection) => Err(rejection), + None => Ok(()), + } + } + + fn state(&self) -> MutexGuard<'_, ResponseEgressResourceState> { + self.state.lock().unwrap_or_else(PoisonError::into_inner) + } +} + +/// Stable reason a late-bound response-egress resource was rejected. +#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] +#[non_exhaustive] +pub enum ResponseEgressResourceInstallError { + #[error("a response-egress resource is already installed")] + AlreadyInstalled, + #[error("the response-egress resource owner is closed")] + Closed, +} + +struct ResponseEgressResourceOwner { + state: Option>>>, +} + +impl ResponseEgressResourceOwner { + fn close(mut self) { + let Some(state) = self.state.take() else { + return; + }; + close_response_egress_resource(&state); + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for ResponseEgressResourceOwner { + fn drop(&mut self) { + let Some(state) = self.state.take() else { + return; + }; + close_response_egress_resource(&state); + } +} + +struct ResponseEgressResourceState { + closed: bool, + resource: Option, +} + +impl ResponseEgressCompletion { + fn complete(&mut self, report: &ResponseEgressReport) { + let Some(callback) = self.callback.take() else { + return; + }; + let result = catch_unwind(AssertUnwindSafe(|| callback(report))); + if result.is_err() { + log::error!("response-egress completion panicked after terminal transition"); + } + } + + /// Creates a completion with no response-scoped callback. + #[must_use] + #[inline] + pub const fn empty() -> Self { + Self { callback: None } + } + + /// Joins this completion with `other` into one response-scoped completion. + /// + /// At terminal egress, both callbacks receive the same borrowed report and run at most once + /// in deterministic left-to-right order: this completion first, then `other`. Dropping the + /// joined completion before terminal egress abandons both callbacks and releases resources + /// captured by either callback exactly once. + /// + /// On targets where panics unwind, each callback has an independent panic boundary, so a + /// panic in the left callback does not suppress the right callback. On panic-abort targets, + /// the process may terminate during the left callback before the right callback runs. + #[must_use] + #[inline] + pub fn join(mut self, mut other: Self) -> Self { + Self::new(move |report| { + self.complete(report); + other.complete(report); + }) + } + + /// Creates one late-bound application-resource installer and its paired completion. + /// + /// The completion closes the installer at terminal egress or when abandoned. An installed + /// resource remains live until that close operation and is then dropped exactly once. + #[must_use] + #[inline] + pub fn late_bound() -> (ResponseEgressResource, Self) + where + T: Send + 'static, + { + let state = Arc::new(Mutex::new(ResponseEgressResourceState { + closed: false, + resource: None, + })); + let resource = ResponseEgressResource { + state: Arc::clone(&state), + }; + let owner = ResponseEgressResourceOwner { state: Some(state) }; + let completion = Self::new(move |_report| owner.close()); + (resource, completion) + } + + /// Retains one response-scoped callback until terminal egress. + #[must_use] + #[inline] + pub fn new(complete: Complete) -> Self + where + Complete: FnOnce(&ResponseEgressReport) + Send + 'static, + { + Self { + callback: Some(Box::new(complete)), + } + } +} + +/// Synchronous factory for one detached normalized-ingress-error disposition. +pub type DetachedResponseEgressDecisionFactory = Arc< + dyn for<'head> Fn(&DetachedResponseEgressHead<'head>) -> DetachedResponseEgressDecision + + Send + + Sync + + 'static, +>; + +/// Synchronous observer for terminal response-egress reports. +pub trait ResponseEgressObserver: Send + Sync + 'static { + fn complete(&self, report: &ResponseEgressReport); +} + +struct NoopResponseEgressObserver; + +impl ResponseEgressObserver for NoopResponseEgressObserver { + #[inline] + fn complete(&self, _report: &ResponseEgressReport) {} +} + +/// Cloneable handle to an application response-egress observer. +#[derive(Clone)] +pub struct ResponseEgressObserverHandle { + observer: Arc, +} + +/// Adapter-facing response plus immutable ingress metadata and app-owned egress hooks. +#[doc(hidden)] +pub struct ResponseEgressEnvelope { + clock: MonotonicClock, + completion: ResponseEgressCompletion, + observer: ResponseEgressObserverHandle, + policy: ResponseEgressPolicyCallback, + request_method: Method, + request_start: MonotonicInstant, + response: Response, + route: Option, + synthesized: bool, + synthesized_headers: SynthesizedResponseHeaders, +} + +pub(crate) struct ResponseEgressRequestMetadata { + request_method: Method, + request_start: MonotonicInstant, + route: Option, +} + +impl ResponseEgressRequestMetadata { + #[inline] + pub(crate) const fn new( + request_method: Method, + request_start: MonotonicInstant, + route: Option, + ) -> Self { + Self { + request_method, + request_start, + route, + } + } +} + +/// Live response-egress state returned when application response preparation fails precommit. +#[doc(hidden)] +pub struct ResponseEgressBeginFailure { + attempt: ResponseEgressAttempt, + cause: ResponseEgressOutcome, + clock: MonotonicClock, +} + +/// Distinct precommit failures without conflating lifecycle ownership with HTTP framing. +#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] +#[non_exhaustive] +pub enum ResponseEgressPreparationError { + #[error("outbound response-body scope closed before handoff")] + ClosedOutboundScope, + #[error(transparent)] + Framing(#[from] ResponseEgressFramingError), +} + +impl ResponseEgressBeginFailure { + /// Consumes the failure into the sole live attempt, its clock, and original cause. + #[must_use] + #[inline] + pub fn into_parts(self) -> (ResponseEgressAttempt, MonotonicClock, ResponseEgressOutcome) { + (self.attempt, self.clock, self.cause) + } +} + +impl ResponseEgressEnvelope { + /// Starts an owned response-egress attempt without pre-terminalizing fallback-eligible errors. + /// + /// # Errors + /// Returns the sole live attempt, its injected clock, and a bounded precommit cause when the + /// policy panics on an unwind-capable target or its deadline cannot be normalized. A target + /// compiled with panic-abort cannot contain application callback panics. + #[inline] + pub fn begin( + mut self, + framing: ResponseEgressFramingMode, + ) -> Result< + ( + PreparedResponseEgress, + ResponseEgressPolicy, + ResponseEgressAttempt, + MonotonicClock, + ), + Box, + > { + let egress_started_at = self.clock.now(); + if self.synthesized { + self.synthesized_headers.apply(self.response.headers_mut()); + suppress_overflow_retry_after(&mut self.response); + } + let application_deadline = self + .response + .extensions_mut() + .remove::() + .map(|deadline| deadline.resolve_at(egress_started_at)); + let body_owner = self.response.body_mut().take_egress_owner(); + let head = ResponseEgressHead::new( + self.response.status(), + self.response.version(), + self.response.headers(), + application_deadline, + self.request_start, + self.route.as_ref(), + ); + let mut attempt = ResponseEgressAttempt::new( + &head, + egress_started_at, + self.completion, + self.observer, + self.clock.clone(), + ); + attempt.synthesized_headers = self.synthesized_headers; + if attempt.adopt_outbound_scope(body_owner).is_err() { + return Err(Box::new(ResponseEgressBeginFailure { + attempt, + cause: ResponseEgressOutcome::ConversionError, + clock: self.clock, + })); + } + let Ok(selected_policy) = + catch_unwind(AssertUnwindSafe(|| (self.policy)(&head, egress_started_at))) + else { + log::error!("response-egress policy panicked before conversion"); + return Err(Box::new(ResponseEgressBeginFailure { + attempt, + cause: ResponseEgressOutcome::ConversionError, + clock: self.clock, + })); + }; + let normalized_policy = match selected_policy.normalize_at(egress_started_at) { + Ok(normalized) => normalized, + Err(outcome) => { + return Err(Box::new(ResponseEgressBeginFailure { + attempt, + cause: outcome, + clock: self.clock, + })); + } + }; + let bounded_policy = ResponseEgressPolicy { + write_deadline: Deadline::at_instant(application_deadline.map_or( + normalized_policy.write_deadline.instant(), + |deadline| { + deadline + .instant() + .min(normalized_policy.write_deadline.instant()) + }, + )), + }; + let Ok(prepared) = attempt.prepare_response(framing, &self.request_method, self.response) + else { + return Err(Box::new(ResponseEgressBeginFailure { + attempt, + cause: ResponseEgressOutcome::ConversionError, + clock: self.clock, + })); + }; + Ok((prepared, bounded_policy, attempt, self.clock)) + } + + pub(crate) fn detached( + response: Response, + request_start: MonotonicInstant, + request_method: Method, + completion: ResponseEgressCompletion, + clock: MonotonicClock, + ) -> Self { + Self::new( + response, + ResponseEgressRequestMetadata::new(request_method, request_start, None), + completion, + Arc::new(default_response_egress_policy), + ResponseEgressObserverHandle::default(), + clock, + ) + } + + pub(crate) fn new( + response: Response, + metadata: ResponseEgressRequestMetadata, + completion: ResponseEgressCompletion, + policy: ResponseEgressPolicyCallback, + observer: ResponseEgressObserverHandle, + clock: MonotonicClock, + ) -> Self { + Self { + clock, + completion, + observer, + policy, + request_method: metadata.request_method, + request_start: metadata.request_start, + response, + route: metadata.route, + synthesized: false, + synthesized_headers: SynthesizedResponseHeaders::default(), + } + } + + /// Returns the original request method used for response framing. + #[must_use] + #[inline] + pub fn request_method(&self) -> &Method { + &self.request_method + } + + /// Borrows the application response before egress policy and framing are evaluated. + #[doc(hidden)] + #[must_use] + #[inline] + pub fn response_mut(&mut self) -> &mut Response { + &mut self.response + } + + pub(crate) fn synthesized(mut self) -> Self { + self.synthesized = true; + self + } + + /// Replaces the immutable policy before conversion, without decorating a routed/refused body. + #[doc(hidden)] + #[must_use] + #[inline] + pub fn with_synthesized_response_headers( + mut self, + headers: SynthesizedResponseHeaders, + ) -> Self { + self.synthesized_headers = headers; + self + } +} + +impl ResponseEgressObserverHandle { + fn complete(&self, report: &ResponseEgressReport) { + let result = catch_unwind(AssertUnwindSafe(|| self.observer.complete(report))); + if result.is_err() { + log::error!("response-egress observer panicked after terminal transition"); + } + } + + #[must_use] + #[inline] + pub fn new(observer: Observer) -> Self + where + Observer: ResponseEgressObserver, + { + Self { + observer: Arc::new(observer), + } + } +} + +impl Default for ResponseEgressObserverHandle { + #[inline] + fn default() -> Self { + Self::new(NoopResponseEgressObserver) + } +} + +enum AttemptState { + Initial, + Terminal(ResponseEgressReport), + Writing, +} + +/// Adapter-facing exactly-once response-egress completion guard. +/// +/// The guard is intentionally non-clone. Dropping it before a terminal signal reports a +/// conversion error from the initial state or a transport error from the writing state. +#[doc(hidden)] +pub struct ResponseEgressAttempt { + body_kind: ResponseEgressBodyKind, + bytes_written: u64, + clock: MonotonicClock, + completion: ResponseEgressCompletion, + egress_started_at: MonotonicInstant, + fallback_cause: Option, + fallback_disposition: Option, + framing_failures: ResponseEgressFramingFailures, + observer: ResponseEgressObserverHandle, + outbound_scope: Option, + request_start: MonotonicInstant, + route: Option, + source_failure: Option, + state: AttemptState, + synthesized_headers: SynthesizedResponseHeaders, +} + +impl ResponseEgressAttempt { + /// Accounts payload bytes accepted at the adapter's documented write boundary. + /// + /// Returns `false` outside the writing state. Overflow terminalizes the attempt as + /// [`ResponseEgressOutcome::TransportError`] and also returns `false`. + #[inline] + pub fn account_bytes(&mut self, bytes: u64, observed_at: MonotonicInstant) -> bool { + if !matches!(self.state, AttemptState::Writing) { + return false; + } + let Some(total) = self.bytes_written.checked_add(bytes) else { + self.fail_current_body(ResponseEgressOutcome::TransportError, observed_at); + return false; + }; + self.bytes_written = total; + true + } + + fn adopt_outbound_scope( + &mut self, + attached: Option, + ) -> Result<(), EdgeError> { + if let Some(owner) = attached { + owner.accept_body_handoff()?; + drop(self.outbound_scope.replace(owner)); + } + if self.outbound_scope.is_none() { + let owner = OutboundScopeOwner::response_body(); + owner.accept_body_handoff()?; + self.outbound_scope = Some(owner); + } + Ok(()) + } + + /// Selects the bounded adapter fallback while retaining the original precommit cause. + #[inline] + pub fn begin_fallback(&mut self, cause: ResponseEgressOutcome) -> bool { + if !matches!(self.state, AttemptState::Initial) + || self.fallback_cause.is_some() + || !matches!( + cause, + ResponseEgressOutcome::ConversionError | ResponseEgressOutcome::DeadlineExceeded + ) + { + return false; + } + self.body_kind = ResponseEgressBodyKind::Fallback; + self.fallback_cause = Some(cause); + drop(self.outbound_scope.take()); + true + } + + /// Moves an initial attempt into the writing state. + /// + /// Returns `false` after writing has already begun or the attempt is terminal. + #[inline] + pub fn begin_writing(&mut self) -> bool { + if !matches!(self.state, AttemptState::Initial) { + return false; + } + self.state = AttemptState::Writing; + true + } + + /// Successfully completes a writing attempt. + /// + /// Calling this in the initial state terminalizes as `ConversionError`; a converter must + /// enter writing even for an empty response. Later terminal signals return `false`. + #[inline] + pub fn complete(&mut self, observed_at: MonotonicInstant) -> bool { + if self.body_kind == ResponseEgressBodyKind::Fallback { + return false; + } + match self.state { + AttemptState::Initial => { + self.transition(ResponseEgressOutcome::ConversionError, observed_at) + } + AttemptState::Writing => self.transition(ResponseEgressOutcome::Completed, observed_at), + AttemptState::Terminal(_) => false, + } + } + + fn fail_current_body( + &mut self, + application_outcome: ResponseEgressOutcome, + observed_at: MonotonicInstant, + ) -> bool { + if let Some(cause) = self.fallback_cause { + self.fallback_disposition = Some(ResponseEgressFallbackDisposition::Aborted); + self.transition(cause, observed_at) + } else { + self.transition(application_outcome, observed_at) + } + } + + /// Settles a selected fallback while preserving its original precommit cause. + #[inline] + pub fn finish_fallback( + &mut self, + disposition: ResponseEgressFallbackDisposition, + observed_at: MonotonicInstant, + ) -> bool { + let Some(cause) = self.fallback_cause else { + return false; + }; + let valid_state = match disposition { + ResponseEgressFallbackDisposition::Aborted => { + matches!(self.state, AttemptState::Initial | AttemptState::Writing) + } + ResponseEgressFallbackDisposition::Completed => { + matches!(self.state, AttemptState::Writing) + } + }; + if !valid_state { + return false; + } + self.fallback_disposition = Some(disposition); + self.transition(cause, observed_at) + } + + /// Returns the exact terminal snapshot after its completion and observer notifications. + /// + /// A live attempt is abandoned using the same failure classification as `Drop`, not completed + /// successfully. An already terminal attempt samples no clock and invokes no callback again. + #[must_use] + #[inline] + pub fn into_terminal_report(mut self) -> ResponseEgressReport { + let application_outcome = match &self.state { + AttemptState::Terminal(report) => return report.clone(), + AttemptState::Initial => ResponseEgressOutcome::ConversionError, + AttemptState::Writing => ResponseEgressOutcome::TransportError, + }; + let outcome = if let Some(cause) = self.fallback_cause { + self.fallback_disposition = Some(ResponseEgressFallbackDisposition::Aborted); + cause + } else { + application_outcome + }; + self.publish_report(outcome, self.clock.now()) + } + + #[must_use] + #[inline] + pub fn new( + head: &ResponseEgressHead<'_>, + egress_started_at: MonotonicInstant, + completion: ResponseEgressCompletion, + observer: ResponseEgressObserverHandle, + clock: MonotonicClock, + ) -> Self { + Self { + body_kind: ResponseEgressBodyKind::Application, + bytes_written: 0, + clock, + completion, + egress_started_at, + framing_failures: ResponseEgressFramingFailures::default(), + fallback_cause: None, + fallback_disposition: None, + observer, + outbound_scope: None, + request_start: head.request_start(), + route: head.route().cloned(), + source_failure: None, + state: AttemptState::Initial, + synthesized_headers: SynthesizedResponseHeaders::default(), + } + } + + /// Normalizes one response and retains its first precommit framing error per body kind. + /// Recording and logging are initial-only; writing/terminal calls still normalize and return + /// the original result without manufacturing a precommit observation. + /// + /// # Errors + /// Returns the original framing error. Recording changes no clock, polling or terminal state. + #[inline] + pub fn prepare_response( + &mut self, + framing: ResponseEgressFramingMode, + request_method: &Method, + mut response: Response, + ) -> Result { + if self.body_kind == ResponseEgressBodyKind::Fallback + && matches!(self.state, AttemptState::Initial) + { + self.synthesized_headers.apply(response.headers_mut()); + } + let preparation = if matches!(self.state, AttemptState::Terminal(_)) { + Ok(()) + } else { + self.adopt_outbound_scope(response.body_mut().take_egress_owner()) + .map_err(|_error| ResponseEgressPreparationError::ClosedOutboundScope) + }; + preparation + .and_then(|()| { + if let Some(owner) = &self.outbound_scope { + let body = take(response.body_mut()); + *response.body_mut() = body + .bind_outbound_scope(&owner.scope()) + .map_err(|_error| ResponseEgressPreparationError::ClosedOutboundScope)?; + } + prepare_response_egress(framing, request_method, response) + .map_err(ResponseEgressPreparationError::from) + }) + .inspect_err(|reason| { + let ResponseEgressPreparationError::Framing(framing_error) = reason else { + return; + }; + if !matches!(self.state, AttemptState::Initial) { + return; + } + let slot = match self.body_kind { + ResponseEgressBodyKind::Application => &mut self.framing_failures.application, + ResponseEgressBodyKind::Fallback => &mut self.framing_failures.fallback, + }; + slot.get_or_insert(*framing_error); + log::error!( + "response-egress {:?} framing validation failed before commit: {reason}", + self.body_kind + ); + }) + } + + fn publish_report( + &mut self, + mut outcome: ResponseEgressOutcome, + observed_at: MonotonicInstant, + ) -> ResponseEgressReport { + let elapsed = + if let Some(elapsed) = observed_at.checked_duration_since(self.egress_started_at) { + elapsed + } else { + log::error!("response-egress monotonic clock moved backwards"); + outcome = ResponseEgressOutcome::Unspecified; + Duration::ZERO + }; + let report = ResponseEgressReport { + bytes_written: self.bytes_written, + body_kind: self.body_kind, + elapsed, + fallback_disposition: self.fallback_disposition, + framing_failures: self.framing_failures, + outcome, + request_start: self.request_start, + route: self.route.clone(), + source_failure: self.source_failure, + }; + self.state = AttemptState::Terminal(report.clone()); + let cleanup = catch_unwind(AssertUnwindSafe(|| drop(self.outbound_scope.take()))); + if cleanup.is_err() { + log::error!("response-egress outbound cleanup panicked after terminal transition"); + } + self.completion.complete(&report); + self.observer.complete(&report); + report + } + + /// Retains the first body-source error without changing state, clocks or terminal outcome. + /// + /// Returns false after an observation or terminal transition. Missing provider detail is + /// meaningful and is never replaced by evidence from a later source or fallback body. + #[inline] + pub fn record_source_error(&mut self, error: &EdgeError) -> bool { + if self.source_failure.is_some() || matches!(self.state, AttemptState::Terminal(_)) { + return false; + } + self.source_failure = Some(ResponseEgressSourceFailure { + body_kind: self.body_kind, + category: response_egress_source_error_category(error), + provider_detail: error.provider_failure_detail().copied(), + }); + true + } + + /// Copies the validated snapshot for a last-resort adapter fallback after ownership transfer. + #[doc(hidden)] + #[must_use] + #[inline] + pub fn synthesized_response_headers(&self) -> SynthesizedResponseHeaders { + self.synthesized_headers.clone() + } + + /// Terminalizes an attempt with the supplied outcome. + /// + /// `Completed` follows [`Self::complete`] state validation. + #[inline] + pub fn terminate( + &mut self, + outcome: ResponseEgressOutcome, + observed_at: MonotonicInstant, + ) -> bool { + if self.body_kind == ResponseEgressBodyKind::Fallback { + return false; + } + if matches!( + outcome, + ResponseEgressOutcome::Completed | ResponseEgressOutcome::HostHandoff + ) { + if outcome == ResponseEgressOutcome::HostHandoff + && matches!(self.state, AttemptState::Writing) + { + self.transition(outcome, observed_at) + } else { + self.complete(observed_at) + } + } else { + self.transition(outcome, observed_at) + } + } + + fn transition( + &mut self, + outcome: ResponseEgressOutcome, + observed_at: MonotonicInstant, + ) -> bool { + if matches!(self.state, AttemptState::Terminal(_)) { + return false; + } + self.publish_report(outcome, observed_at); + true + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for ResponseEgressAttempt { + #[inline] + fn drop(&mut self) { + let outcome = match self.state { + AttemptState::Initial => ResponseEgressOutcome::ConversionError, + AttemptState::Writing => ResponseEgressOutcome::TransportError, + AttemptState::Terminal(_) => return, + }; + self.fail_current_body(outcome, self.clock.now()); + } +} + +fn close_response_egress_resource(shared: &Mutex>) { + let resource = { + let mut state = shared.lock().unwrap_or_else(PoisonError::into_inner); + state.closed = true; + state.resource.take() + }; + drop(resource); +} + +/// Selects the portable default response-write policy. +#[must_use] +#[inline] +pub fn default_response_egress_policy( + _head: &ResponseEgressHead<'_>, + egress_started_at: MonotonicInstant, +) -> ResponseEgressPolicy { + let deadline = egress_started_at + .checked_add(DEFAULT_RESPONSE_WRITE_BUDGET) + .unwrap_or(egress_started_at); + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(deadline), + } +} + +#[cfg(test)] +mod tests { + use std::cell::{Cell, RefCell}; + use std::error::Error; + use std::panic::{AssertUnwindSafe, catch_unwind}; + use std::ptr::from_ref; + use std::rc::Rc; + use std::slice::from_ref as slice_from_ref; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Barrier, Mutex}; + use std::thread; + use std::time::Duration; + + use super::*; + use crate::body::Body; + use crate::error::{BadGatewayReason, EdgeError}; + use crate::error_detail::{FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase}; + use crate::http::{HeaderMap, HeaderValue, Method, StatusCode, Version, response_builder}; + use crate::outbound::{IndependentOutboundProgress, OutboundScope}; + use crate::router::RouteMetadata; + use crate::time::{DEADLINE_FAR_FUTURE, Deadline, MonotonicClock, MonotonicInstant}; + + #[derive(Clone, Default)] + struct RecordingObserver { + reports: Arc>>, + } + + impl RecordingObserver { + fn reports(&self) -> Vec { + self.reports.lock().expect("reports lock").clone() + } + } + + impl ResponseEgressObserver for RecordingObserver { + fn complete(&self, report: &ResponseEgressReport) { + self.reports + .lock() + .expect("reports lock") + .push(report.clone()); + } + } + + struct PanickingObserver; + + impl ResponseEgressObserver for PanickingObserver { + fn complete(&self, _report: &ResponseEgressReport) { + panic!("observer panic"); + } + } + + #[derive(Clone)] + struct OrderedObserver(Arc>>); + + impl ResponseEgressObserver for OrderedObserver { + fn complete(&self, _report: &ResponseEgressReport) { + self.0.lock().expect("ordered events lock").push("observer"); + } + } + + struct CompletionDropProbe(Arc>); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for CompletionDropProbe { + fn drop(&mut self) { + let mut drops = self.0.lock().expect("completion drops lock"); + *drops = (*drops).saturating_add(1); + } + } + + struct ResourceDropProbe(Arc); + + #[cfg(panic = "unwind")] + struct PanickingProvider(Arc); + + struct SourceScopeProbe(Rc>>); + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for SourceScopeProbe { + fn drop(&mut self) { + self.0 + .borrow_mut() + .replace(OutboundScope::capture(Rc::new(IndependentOutboundProgress))); + } + } + + #[cfg(panic = "unwind")] + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for PanickingProvider { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + panic!("fixture provider cleanup failure"); + } + } + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for ResourceDropProbe { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + fn head<'head>( + headers: &'head HeaderMap, + request_start: MonotonicInstant, + route: Option<&'head RouteMetadata>, + ) -> ResponseEgressHead<'head> { + ResponseEgressHead::new( + StatusCode::CREATED, + Version::HTTP_2, + headers, + None, + request_start, + route, + ) + } + + #[test] + fn egress_body_scope_ends_at_terminal_not_source_eof() { + use std::cell::RefCell; + use std::rc::Rc; + + use bytes::Bytes; + use futures::executor::block_on; + use futures_util::stream; + + use crate::{IndependentOutboundProgress, OutboundScope, OutboundScopeOwner}; + + let callback = OutboundScopeOwner::callback(); + let callback_scope = callback.scope(); + let captured = Rc::new(RefCell::new(None)); + let source_capture = Rc::clone(&captured); + let source = Body::from_stream(stream::once(async move { + source_capture + .borrow_mut() + .replace(OutboundScope::capture(Rc::new(IndependentOutboundProgress))); + Ok::<_, EdgeError>(Bytes::from_static(b"payload")) + })); + let now = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let mut attempt = callback.with(|| { + ResponseEgressAttempt::new( + &head(&headers, now, None), + now, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::default(), + MonotonicClock::new(move || now), + ) + }); + let prepared = attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + Response::new(source), + ) + .expect("valid response"); + assert!(attempt.begin_writing()); + let bytes = block_on(prepared.into_response().into_body().into_bytes_bounded(64)) + .expect("body bytes"); + assert_eq!(bytes.as_ref(), b"payload"); + let body_scope = captured.borrow_mut().take().expect("source was polled"); + assert!(body_scope.is_managed(), "body polling has an egress owner"); + assert!( + body_scope.check().is_ok(), + "EOF is not transmission completion" + ); + assert!(attempt.complete(now)); + assert!( + body_scope.check().is_err(), + "terminal egress closes body work" + ); + assert!(callback_scope.check().is_ok(), "callback work remains live"); + } + + #[test] + fn egress_terminal_revokes_retained_body_before_completion() { + use std::task::Poll; + + use futures_util::stream; + + for explicit in [false, true] { + let drops = Arc::new(AtomicUsize::new(0)); + let probe = ResourceDropProbe(Arc::clone(&drops)); + let source = Body::from_stream(stream::poll_fn(move |_cx| { + let _retained = &probe; + Poll::Pending + })); + let completed_drops = Arc::clone(&drops); + let completion = ResponseEgressCompletion::new(move |_report| { + assert_eq!(completed_drops.load(Ordering::SeqCst), 1); + }); + let now = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let mut attempt = ResponseEgressAttempt::new( + &head(&headers, now, None), + now, + completion, + ResponseEgressObserverHandle::default(), + MonotonicClock::new(move || now), + ); + let retained = attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + Response::new(source), + ) + .expect("valid response"); + assert!(attempt.begin_writing()); + assert_eq!(drops.load(Ordering::SeqCst), 0); + if explicit { + assert!(attempt.complete(now)); + } + drop(attempt); + assert_eq!( + drops.load(Ordering::SeqCst), + 1, + "retained handles do not retain sources" + ); + drop(retained); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + } + + #[test] + fn source_disposal_keeps_body_scope_until_terminal() { + use std::task::Poll; + + use futures::executor::block_on; + use futures_util::stream; + + for consume in [false, true] { + let captured = Rc::new(RefCell::new(None)); + let source_probe = SourceScopeProbe(Rc::clone(&captured)); + let source = Body::from_stream(stream::poll_fn(move |_| { + let _held = &source_probe; + Poll::Ready(None) + })); + let now = MonotonicInstant::now(); + let envelope = ResponseEgressEnvelope::detached( + Response::new(source), + now, + Method::GET, + ResponseEgressCompletion::empty(), + MonotonicClock::new(move || now), + ); + let Ok((prepared, _, mut attempt, _)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("valid body"); + }; + assert!(attempt.begin_writing()); + if consume { + block_on(prepared.into_response().into_body().into_bytes_bounded(0)) + .expect("source EOF"); + } else { + drop(prepared); + } + let cleanup_scope = captured.borrow_mut().take().expect("source dropped"); + assert!( + cleanup_scope.is_managed(), + "cleanup retains the body identity" + ); + cleanup_scope + .check() + .expect("EOF/drop is not terminal egress"); + assert!(attempt.complete(now)); + assert!( + cleanup_scope.check().is_err(), + "destructor work cannot escape egress" + ); + } + } + + #[test] + fn attached_work_survives_callback_and_eof_until_actual_egress_terminal() { + use futures::executor::block_on; + + use crate::outbound::{OutboundScopeOwner, Revocable}; + + let callback = OutboundScopeOwner::callback(); + let owner = callback.with(OutboundScopeOwner::response_body); + let scope = owner.scope(); + let drops = Arc::new(AtomicUsize::new(0)); + let retained_work = Revocable::new(&scope, ResourceDropProbe(Arc::clone(&drops))) + .expect("body-owned work starts before response creation"); + let response = owner + .attach(Response::new(Body::from("payload"))) + .expect("attach body ownership without changing payload"); + assert_eq!(response.body().as_bytes(), Some(b"payload".as_slice())); + let now = MonotonicInstant::now(); + let envelope = ResponseEgressEnvelope::detached( + response, + now, + Method::GET, + ResponseEgressCompletion::empty(), + MonotonicClock::new(move || now), + ); + let Ok((prepared, _, mut attempt, _)) = + callback.with(|| envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength)) + else { + panic!("attached egress begins"); + }; + drop(callback); + assert!( + scope.check().is_ok(), + "the accepted body outlives callback return" + ); + assert_eq!(drops.load(Ordering::SeqCst), 0); + assert!(attempt.begin_writing()); + let bytes = block_on(prepared.into_response().into_body().into_bytes_bounded(64)) + .expect("body is consumed"); + assert_eq!(bytes.as_ref(), b"payload"); + assert_eq!( + drops.load(Ordering::SeqCst), + 0, + "EOF does not release live body work" + ); + assert!(attempt.complete(now)); + assert!(scope.check().is_err()); + assert_eq!(drops.load(Ordering::SeqCst), 1); + drop(retained_work); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn retained_unaccepted_response_cannot_escape_callback_cleanup() { + use crate::outbound::{OutboundScopeOwner, Revocable}; + + let callback = OutboundScopeOwner::callback(); + let owner = callback.with(OutboundScopeOwner::response_body); + let scope = owner.scope(); + let drops = Arc::new(AtomicUsize::new(0)); + let retained_work = + Revocable::new(&scope, ResourceDropProbe(Arc::clone(&drops))).expect("body work"); + let response = owner + .attach(Response::new(Body::from("not handed off"))) + .expect("attach is not acceptance"); + drop(callback); + assert!( + scope.check().is_err(), + "unaccepted work remains callback-bound" + ); + assert_eq!(drops.load(Ordering::SeqCst), 1); + drop((response, retained_work)); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn suppressed_response_retains_attached_work_until_egress_terminal() { + use crate::outbound::{OutboundScopeOwner, Revocable}; + + for method in [Method::HEAD, Method::GET] { + let callback = OutboundScopeOwner::callback(); + let owner = callback.with(OutboundScopeOwner::response_body); + let scope = owner.scope(); + let drops = Arc::new(AtomicUsize::new(0)); + let retained = Revocable::new(&scope, ResourceDropProbe(Arc::clone(&drops))) + .expect("body-owned work"); + let mut response = Response::new(Body::from("suppressed")); + if method == Method::GET { + *response.status_mut() = StatusCode::NOT_MODIFIED; + } + let attached_response = owner.attach(response).expect("attached response"); + let now = MonotonicInstant::now(); + let envelope = ResponseEgressEnvelope::detached( + attached_response, + now, + method, + ResponseEgressCompletion::empty(), + MonotonicClock::new(move || now), + ); + let Ok((prepared, _, mut attempt, _)) = + callback.with(|| envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength)) + else { + panic!("bodyless egress begins"); + }; + assert!(!prepared.transmits_body()); + drop(callback); + scope.check().expect("accepted body scope remains live"); + assert_eq!(drops.load(Ordering::SeqCst), 0); + assert!(attempt.begin_writing()); + assert!(attempt.complete(now)); + assert_eq!(drops.load(Ordering::SeqCst), 1); + drop((prepared, retained)); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + } + + #[test] + fn closed_attachment_is_rejected_before_policy_callback() { + use crate::outbound::OutboundScopeOwner; + + let callback = OutboundScopeOwner::callback(); + let owner = callback.with(OutboundScopeOwner::response_body); + let response = owner + .attach(Response::new(Body::from("payload"))) + .expect("attach before close"); + drop(callback); + let now = MonotonicInstant::now(); + let mut envelope = ResponseEgressEnvelope::detached( + response, + now, + Method::GET, + ResponseEgressCompletion::empty(), + MonotonicClock::new(move || now), + ); + let calls = Arc::new(AtomicUsize::new(0)); + let policy_calls = Arc::clone(&calls); + envelope.policy = Arc::new(move |_head, start| { + policy_calls.fetch_add(1, Ordering::SeqCst); + ResponseEgressPolicy { + write_deadline: Deadline::at_instant(start), + } + }); + let Err(failure) = envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) else { + panic!("closed response ownership must not be revived"); + }; + let (_attempt, _, cause) = failure.into_parts(); + assert_eq!(cause, ResponseEgressOutcome::ConversionError); + assert_eq!(calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn scope_rejection_is_not_reported_as_a_framing_failure() { + use crate::outbound::OutboundScopeOwner; + + let callback = OutboundScopeOwner::callback(); + let owner = callback.with(OutboundScopeOwner::response_body); + let response = owner + .attach(Response::new(Body::empty())) + .expect("attached response"); + callback.close(); + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, now); + attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + response, + ) + .err() + .expect("closed ownership is rejected"); + let report = attempt.into_terminal_report(); + assert_eq!(report.framing_failures.application(), None); + } + + #[cfg(panic = "unwind")] + #[test] + fn cleanup_panic_cannot_suppress_exactly_once_terminal_notifications() { + use crate::outbound::OutboundScopeOwner; + + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let drops = Arc::new(AtomicUsize::new(0)); + drop( + scope + .provider_state(|| PanickingProvider(Arc::clone(&drops))) + .expect("provider state"), + ); + let calls = Arc::new(AtomicUsize::new(0)); + let completion_calls = Arc::clone(&calls); + let observer = RecordingObserver::default(); + let now = MonotonicInstant::now(); + let mut envelope = ResponseEgressEnvelope::detached( + owner + .attach(Response::new(Body::empty())) + .expect("attached response"), + now, + Method::GET, + ResponseEgressCompletion::new(move |_| { + completion_calls.fetch_add(1, Ordering::SeqCst); + }), + MonotonicClock::new(move || now), + ); + envelope.observer = ResponseEgressObserverHandle::new(observer.clone()); + let Ok((_prepared, _, mut attempt, _)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("valid egress"); + }; + assert!(attempt.begin_writing()); + let completion = catch_unwind(AssertUnwindSafe(|| attempt.complete(now))); + assert!(matches!(completion, Ok(true)), "cleanup panic is contained"); + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!(observer.reports().len(), 1); + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert!(!attempt.complete(now)); + drop(attempt); + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!(observer.reports().len(), 1); + } + + #[test] + fn fallback_selection_revokes_failed_application_body_work() { + use crate::outbound::{OutboundScopeOwner, Revocable}; + + let callback = OutboundScopeOwner::callback(); + let owner = callback.with(OutboundScopeOwner::response_body); + let scope = owner.scope(); + let drops = Arc::new(AtomicUsize::new(0)); + let retained = + Revocable::new(&scope, ResourceDropProbe(Arc::clone(&drops))).expect("body work"); + let mut response = Response::new(Body::from("payload")); + response + .headers_mut() + .insert("content-length", HeaderValue::from_static("1")); + let now = MonotonicInstant::now(); + let envelope = ResponseEgressEnvelope::detached( + owner.attach(response).expect("attached response"), + now, + Method::GET, + ResponseEgressCompletion::empty(), + MonotonicClock::new(move || now), + ); + let Err(failure) = + callback.with(|| envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength)) + else { + panic!("invalid body framing"); + }; + let (mut attempt, _, cause) = failure.into_parts(); + assert_eq!(drops.load(Ordering::SeqCst), 0); + assert!(attempt.begin_fallback(cause)); + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert!(scope.check().is_err()); + drop(retained); + assert_eq!(drops.load(Ordering::SeqCst), 1); + callback.scope().check().expect("callback remains live"); + } + + fn completed_report(request_start: MonotonicInstant) -> ResponseEgressReport { + ResponseEgressReport { + body_kind: ResponseEgressBodyKind::Application, + bytes_written: 0, + elapsed: Duration::ZERO, + fallback_disposition: None, + framing_failures: ResponseEgressFramingFailures::default(), + outcome: ResponseEgressOutcome::Completed, + request_start, + route: None, + source_failure: None, + } + } + + fn attempt( + observer: &RecordingObserver, + started_at: MonotonicInstant, + ) -> ResponseEgressAttempt { + let headers = HeaderMap::new(); + let head = head(&headers, started_at, None); + ResponseEgressAttempt::new( + &head, + started_at, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + ) + } + + #[test] + fn terminal_report_extraction_preserves_callbacks_and_drop_semantics() { + for (fallback, writing, terminal) in [ + (false, false, false), + (false, true, false), + (false, true, true), + (true, false, false), + (true, true, false), + (true, true, true), + ] { + let observed = RecordingObserver::default(); + let completed = Arc::new(Mutex::new(Vec::new())); + let captured = Arc::clone(&completed); + let reads = Arc::new(AtomicUsize::new(0)); + let clock_reads = Arc::clone(&reads); + let now = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let mut attempt = ResponseEgressAttempt::new( + &head(&headers, now, None), + now, + ResponseEgressCompletion::new(move |report| { + captured.lock().unwrap().push(report.clone()); + }), + ResponseEgressObserverHandle::new(observed.clone()), + MonotonicClock::new(move || { + clock_reads.fetch_add(1, Ordering::SeqCst); + now + }), + ); + attempt.record_source_error(&EdgeError::bad_request("private source")); + prepare_extraction_state(&mut attempt, now, (fallback, writing, terminal)); + let before_reads = reads.load(Ordering::SeqCst); + let report = attempt.into_terminal_report(); + assert_eq!( + reads.load(Ordering::SeqCst) - before_reads, + usize::from(!terminal) + ); + assert_eq!( + completed.lock().unwrap().as_slice(), + slice_from_ref(&report) + ); + assert_eq!( + observed.reports.lock().unwrap().as_slice(), + slice_from_ref(&report) + ); + assert!(report.source_failure.is_some()); + assert_eq!(report.bytes_written, if writing { 3 } else { 0 }); + if fallback { + assert_eq!(report.body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!(report.outcome, ResponseEgressOutcome::ConversionError); + assert_eq!( + report.fallback_disposition, + Some(if terminal { + ResponseEgressFallbackDisposition::Completed + } else { + ResponseEgressFallbackDisposition::Aborted + }) + ); + } else { + assert_eq!(report.body_kind, ResponseEgressBodyKind::Application); + assert_eq!( + report.outcome, + if terminal { + ResponseEgressOutcome::Completed + } else if writing { + ResponseEgressOutcome::TransportError + } else { + ResponseEgressOutcome::ConversionError + } + ); + } + } + } + + fn prepare_extraction_state( + attempt: &mut ResponseEgressAttempt, + now: MonotonicInstant, + (fallback, writing, terminal): (bool, bool, bool), + ) { + if fallback { + assert!(attempt.begin_fallback(ResponseEgressOutcome::ConversionError)); + } + if writing { + assert!(attempt.begin_writing()); + assert!(attempt.account_bytes(3, now)); + } + if !terminal { + return; + } + if fallback { + assert!(attempt.finish_fallback(ResponseEgressFallbackDisposition::Completed, now)); + } else { + assert!(attempt.complete(now)); + } + } + + #[test] + fn provider_diagnostics_reach_completion_and_observer_without_terminal_changes() { + let detail = ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: FastlyFailureCause::ConnectionTerminated, + }; + for with_detail in [false, true] { + for (fallback_stage, observed_body, terminal_outcome) in [ + ( + 0_u8, + ResponseEgressBodyKind::Application, + ResponseEgressOutcome::SourceError, + ), + ( + 1, + ResponseEgressBodyKind::Application, + ResponseEgressOutcome::ConversionError, + ), + ( + 2, + ResponseEgressBodyKind::Fallback, + ResponseEgressOutcome::ConversionError, + ), + ] { + let observer = RecordingObserver::default(); + let completed = Arc::new(Mutex::new(Vec::new())); + let completed_reports = Arc::clone(&completed); + let started_at = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let mut attempt = ResponseEgressAttempt::new( + &head(&headers, started_at, None), + started_at, + ResponseEgressCompletion::new(move |report| { + completed_reports + .lock() + .expect("completed reports") + .push(report.clone()); + }), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::new(move || started_at), + ); + if fallback_stage == 2 { + assert!(attempt.begin_fallback(ResponseEgressOutcome::ConversionError)); + } + let error = if with_detail { + EdgeError::bad_gateway_with_detail( + "provider-secret-token", + BadGatewayReason::Transport, + detail, + ) + } else { + EdgeError::bad_gateway_with_reason( + "provider-secret-token", + BadGatewayReason::Transport, + ) + }; + assert!(attempt.record_source_error(&error)); + assert!( + !attempt.record_source_error(&EdgeError::bad_gateway_with_detail( + "different-secret", + BadGatewayReason::Unreachable, + ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::Send, + cause: FastlyFailureCause::DnsTimeout + }, + )) + ); + if fallback_stage == 1 { + assert!(attempt.begin_fallback(ResponseEgressOutcome::ConversionError)); + } + assert!(attempt.begin_writing()); + if fallback_stage == 0 { + assert!(attempt.terminate(ResponseEgressOutcome::SourceError, started_at)); + } else { + assert!( + attempt.finish_fallback( + ResponseEgressFallbackDisposition::Completed, + started_at + ) + ); + } + assert!(!attempt.record_source_error(&error)); + assert!(!attempt.terminate(ResponseEgressOutcome::TransportError, started_at)); + drop(attempt); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(*completed.lock().expect("completion"), reports); + let failure = reports[0].source_failure.as_ref().expect("source failure"); + assert_eq!(failure.category(), "bad_gateway_transport"); + assert_eq!( + failure.provider_failure_detail(), + with_detail.then_some(&detail) + ); + assert_eq!(failure.body_kind(), observed_body); + assert!(!format!("{failure:?}").contains("secret")); + assert_eq!(reports[0].outcome, terminal_outcome); + assert_eq!(reports[0].bytes_written, 0); + assert_eq!(reports[0].elapsed, Duration::ZERO); + } + } + } + + #[test] + fn recording_source_failure_has_no_clock_or_terminal_side_effects() { + for writing in [false, true] { + let now = MonotonicInstant::now(); + let reads = Arc::new(AtomicUsize::new(0)); + let clock_reads = Arc::clone(&reads); + let clock = MonotonicClock::new(move || { + clock_reads.fetch_add(1, Ordering::SeqCst); + now + }); + let observer = RecordingObserver::default(); + let headers = HeaderMap::new(); + let mut attempt = ResponseEgressAttempt::new( + &head(&headers, now, None), + now, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + clock, + ); + if writing { + assert!(attempt.begin_writing()); + } + assert!( + attempt.record_source_error(&EdgeError::internal(anyhow::anyhow!( + "provider-secret-token" + ))) + ); + assert_eq!(reads.load(Ordering::SeqCst), 0); + assert!(observer.reports().is_empty()); + drop(attempt); + assert_eq!(reads.load(Ordering::SeqCst), 1); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!( + reports[0].outcome, + if writing { + ResponseEgressOutcome::TransportError + } else { + ResponseEgressOutcome::ConversionError + } + ); + let failure = reports[0].source_failure.as_ref().expect("source failure"); + assert_eq!(failure.category(), "internal"); + assert!(failure.provider_failure_detail().is_none()); + assert!(!format!("{failure:?}").contains("secret")); + } + } + + #[test] + fn terminal_without_source_failure_rejects_later_observation() { + let observer = RecordingObserver::default(); + let now = MonotonicInstant::now(); + let mut attempt = attempt(&observer, now); + assert!(attempt.begin_writing()); + assert!(attempt.complete(now)); + assert!(!attempt.record_source_error(&EdgeError::bad_gateway("later-secret"))); + drop(attempt); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert!(reports[0].source_failure.is_none()); + } + + #[test] + fn framing_observer_preserves_each_typed_reason() { + use crate::response_egress_framing::test_support::{FRAMING_RULES, framing_fixture}; + use futures::executor::block_on; + + for reason in FRAMING_RULES { + for detached in [false, true] { + let fixture = block_on(framing_fixture(reason, detached)).expect("fixture"); + let Err(failure) = fixture + .envelope + .begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("framing must reject the response") + }; + let (mut attempt, clock, cause) = failure.into_parts(); + assert_eq!(cause, ResponseEgressOutcome::ConversionError); + assert!(attempt.begin_fallback(cause)); + assert!(attempt.begin_writing()); + assert!( + attempt + .finish_fallback(ResponseEgressFallbackDisposition::Completed, clock.now()) + ); + let completed = fixture.completed.lock().expect("completion"); + assert_eq!(completed.len(), 1); + assert_eq!(completed[0].framing_failures.application(), Some(reason)); + assert_eq!(completed[0].framing_failures.fallback(), None); + assert!(completed[0].source_failure.is_none()); + assert!(!format!("{:?}", completed[0]).contains("secret")); + let observed = fixture.observed.lock().expect("observer"); + if detached { + assert!(observed.is_empty()); + } else { + assert_eq!(*observed, *completed); + } + } + } + } + + #[test] + fn framing_observations_preserve_both_body_slots_without_sampling_the_clock() { + use crate::response_egress_framing::test_support::framing_response; + + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let reads = Arc::new(AtomicUsize::new(0)); + let clock_reads = Arc::clone(&reads); + let clock = MonotonicClock::new(move || { + clock_reads.fetch_add(1, Ordering::SeqCst); + now + }); + let headers = HeaderMap::new(); + let mut attempt = ResponseEgressAttempt::new( + &head(&headers, now, None), + now, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + clock, + ); + assert!(attempt.record_source_error(&EdgeError::bad_gateway("private token=secret"))); + for reason in [ + ResponseEgressFramingError::InvalidContentLength, + ResponseEgressFramingError::UnsupportedStatus, + ] { + let (method, response) = framing_response(reason).expect("framing fixture"); + assert_eq!( + attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + &method, + response + ) + .err(), + Some(ResponseEgressPreparationError::Framing(reason)) + ); + } + assert!(attempt.begin_fallback(ResponseEgressOutcome::ConversionError)); + let reason = ResponseEgressFramingError::UnsupportedTrailers; + let (method, response) = framing_response(reason).expect("framing fixture"); + assert_eq!( + attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + &method, + response + ) + .err(), + Some(ResponseEgressPreparationError::Framing(reason)) + ); + let (later_method, later_response) = + framing_response(ResponseEgressFramingError::UnsupportedStatus) + .expect("framing fixture"); + assert_eq!( + attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + &later_method, + later_response + ) + .err(), + Some(ResponseEgressPreparationError::Framing( + ResponseEgressFramingError::UnsupportedStatus + )) + ); + assert_eq!(reads.load(Ordering::SeqCst), 0); + assert!(observer.reports().is_empty()); + assert!(attempt.begin_writing()); + assert!(attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, now)); + let (terminal_method, terminal_response) = + framing_response(ResponseEgressFramingError::UnsupportedTunnel) + .expect("framing fixture"); + assert_eq!( + attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + &terminal_method, + terminal_response + ) + .err(), + Some(ResponseEgressPreparationError::Framing( + ResponseEgressFramingError::UnsupportedTunnel + )) + ); + drop(attempt); + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!( + reports[0].framing_failures.application(), + Some(ResponseEgressFramingError::InvalidContentLength) + ); + assert_eq!(reports[0].framing_failures.fallback(), Some(reason)); + assert_eq!( + reports[0] + .source_failure + .expect("source observation") + .category(), + "bad_gateway_unspecified" + ); + assert_eq!(reads.load(Ordering::SeqCst), 0); + } + + #[test] + fn framing_fallback_slot_retains_each_rule_from_the_real_normalizer() { + use crate::response_egress_framing::test_support::{FRAMING_RULES, framing_response}; + for reason in FRAMING_RULES { + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, now); + assert!(attempt.begin_fallback(ResponseEgressOutcome::ConversionError)); + let (method, response) = framing_response(reason).expect("framing fixture"); + assert_eq!( + attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + &method, + response + ) + .err(), + Some(ResponseEgressPreparationError::Framing(reason)) + ); + assert!(attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, now)); + let report = observer.reports().remove(0); + assert_eq!(report.framing_failures.application(), None); + assert_eq!(report.framing_failures.fallback(), Some(reason)); + assert_eq!(report.body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!(report.outcome, ResponseEgressOutcome::ConversionError); + assert!(report.source_failure.is_none()); + } + } + + #[test] + fn expired_policy_does_not_invent_a_framing_reason() { + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let method = Method::GET; + let response = Response::new(Body::from("valid")); + let envelope = ResponseEgressEnvelope::new( + response, + ResponseEgressRequestMetadata::new(method, now, None), + ResponseEgressCompletion::empty(), + Arc::new(move |_, _| ResponseEgressPolicy { + write_deadline: Deadline::at_instant(now), + }), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::new(move || now), + ); + let Ok((_prepared, policy, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("valid expired policy remains valid") + }; + assert!(policy.write_deadline.is_expired_at(clock.now())); + assert!(attempt.terminate(ResponseEgressOutcome::DeadlineExceeded, now)); + assert_eq!( + observer.reports()[0].framing_failures, + ResponseEgressFramingFailures::default() + ); + } + + #[test] + fn framing_rejection_after_terminal_does_not_create_an_observation() { + use crate::response_egress_framing::test_support::framing_response; + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let mut attempt = attempt(&observer, now); + assert!(attempt.begin_writing()); + assert!(attempt.complete(now)); + let (method, response) = framing_response(ResponseEgressFramingError::UnsupportedTrailers) + .expect("framing fixture"); + assert_eq!( + attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + &method, + response + ) + .err(), + Some(ResponseEgressPreparationError::Framing( + ResponseEgressFramingError::UnsupportedTrailers + )) + ); + assert_eq!( + attempt.framing_failures, + ResponseEgressFramingFailures::default() + ); + drop(attempt); + assert_eq!(observer.reports().len(), 1); + assert_eq!( + observer.reports()[0].framing_failures, + ResponseEgressFramingFailures::default() + ); + } + + #[test] + fn writing_attempt_does_not_mislabel_a_late_normalization_error() { + use crate::response_egress_framing::test_support::framing_response; + let now = MonotonicInstant::now(); + let observer = RecordingObserver::default(); + let reads = Arc::new(AtomicUsize::new(0)); + let clock_reads = Arc::clone(&reads); + let clock = MonotonicClock::new(move || { + clock_reads.fetch_add(1, Ordering::SeqCst); + now + }); + let headers = HeaderMap::new(); + let mut attempt = ResponseEgressAttempt::new( + &head(&headers, now, None), + now, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + clock, + ); + assert!(attempt.begin_writing()); + let (method, response) = framing_response(ResponseEgressFramingError::UnsupportedTrailers) + .expect("framing fixture"); + assert_eq!( + attempt + .prepare_response( + ResponseEgressFramingMode::ExplicitBufferedLength, + &method, + response + ) + .err(), + Some(ResponseEgressPreparationError::Framing( + ResponseEgressFramingError::UnsupportedTrailers + )) + ); + assert!(observer.reports().is_empty()); + assert_eq!(reads.load(Ordering::SeqCst), 0); + assert!(attempt.terminate(ResponseEgressOutcome::SourceError, now)); + drop(attempt); + assert_eq!(reads.load(Ordering::SeqCst), 0); + assert_eq!( + observer.reports()[0].framing_failures, + ResponseEgressFramingFailures::default() + ); + } + + #[test] + fn default_policy_is_finite_and_exactly_thirty_seconds() { + let headers = HeaderMap::new(); + let started_at = MonotonicInstant::now(); + let head = head(&headers, started_at, None); + let policy = default_response_egress_policy(&head, started_at); + + assert_eq!( + policy.write_deadline.instant(), + started_at + .checked_add(DEFAULT_RESPONSE_WRITE_BUDGET) + .expect("default deadline") + ); + assert_eq!(DEFAULT_RESPONSE_WRITE_BUDGET, Duration::from_secs(30)); + } + + #[test] + fn detached_response_egress_head_deadline_is_anchored_to_request_start() { + let request_start = MonotonicInstant::now() + .checked_sub(Duration::from_mins(1)) + .expect("earlier request start"); + let head = DetachedResponseEgressHead::new( + &Method::GET, + request_start, + StatusCode::BAD_REQUEST, + "bad_request", + ); + + assert_eq!( + head.write_deadline_after(Duration::from_secs(2)).instant(), + request_start + .checked_add(Duration::from_secs(2)) + .expect("relative deadline") + ); + } + + #[test] + fn detached_response_egress_head_deadline_clamps_to_far_future() { + let request_start = MonotonicInstant::now(); + let head = DetachedResponseEgressHead::new( + &Method::GET, + request_start, + StatusCode::BAD_REQUEST, + "bad_request", + ); + + assert_eq!( + head.write_deadline_after(Duration::MAX).instant(), + request_start + .checked_add(DEADLINE_FAR_FUTURE) + .expect("maximum deadline") + ); + } + + #[test] + fn detached_response_egress_head_deadline_overflow_fails_closed() { + let origin = MonotonicInstant::now(); + let mut low = 0_u64; + let mut high = u64::MAX; + while low < high { + let midpoint = low + (high - low).div_ceil(2); + if origin.checked_add(Duration::from_secs(midpoint)).is_some() { + low = midpoint; + } else { + high = midpoint - 1; + } + } + let request_start = origin + .checked_add(Duration::from_secs(low)) + .expect("latest representable instant"); + assert!(request_start.checked_add(Duration::from_secs(1)).is_none()); + let head = DetachedResponseEgressHead::new( + &Method::GET, + request_start, + StatusCode::BAD_REQUEST, + "bad_request", + ); + + assert_eq!( + head.write_deadline_after(Duration::from_secs(1)).instant(), + request_start + ); + } + + #[test] + fn policy_normalization_clamps_far_future_and_preserves_expiry() { + let started_at = MonotonicInstant::now(); + let maximum = started_at + .checked_add(DEADLINE_FAR_FUTURE) + .expect("maximum deadline"); + let beyond = maximum + .checked_add(Duration::from_secs(1)) + .expect("deadline beyond maximum"); + let clamped = ResponseEgressPolicy { + write_deadline: Deadline::at_instant(beyond), + } + .normalize_at(started_at) + .expect("normalization"); + assert_eq!(clamped.write_deadline.instant(), maximum); + + let expired = ResponseEgressPolicy { + write_deadline: Deadline::at_instant(started_at), + } + .normalize_at(started_at) + .expect("expired policy remains valid"); + assert_eq!(expired.write_deadline.instant(), started_at); + } + + #[test] + fn head_is_body_blind_and_exposes_only_immutable_metadata() { + let mut headers = HeaderMap::new(); + headers.insert("x-test", HeaderValue::from_static("visible")); + let request_start = MonotonicInstant::now(); + let route = RouteMetadata::new(Method::GET, "/items/{id}"); + let head = head(&headers, request_start, Some(&route)); + + assert_eq!(head.status(), StatusCode::CREATED); + assert_eq!(head.version(), Version::HTTP_2); + assert_eq!(head.headers(), &headers); + assert_eq!(head.request_start(), request_start); + assert_eq!(head.route(), Some(&route)); + } + + #[test] + fn observer_trait_is_object_safe_and_handle_has_a_noop_default() { + fn assert_object_safe(_observer: &dyn ResponseEgressObserver) {} + + let observer = RecordingObserver::default(); + assert_object_safe(&observer); + let _: ResponseEgressObserverHandle = ResponseEgressObserverHandle::default(); + } + + #[test] + fn late_bound_resource_is_send_and_sync_for_send_not_sync_values() { + fn assert_error() + where + T: Error, + { + } + fn assert_send_sync() + where + T: Send + Sync, + { + } + + assert_error::(); + assert_send_sync::>>(); + } + + #[test] + fn late_bound_install_and_close_race_releases_candidate_once() { + let drops = Arc::new(AtomicUsize::new(0)); + let barrier = Arc::new(Barrier::new(2)); + let close_barrier = Arc::clone(&barrier); + let (resource, completion) = ResponseEgressCompletion::late_bound(); + + let install_result = thread::scope(|scope| { + let close = scope.spawn(move || { + close_barrier.wait(); + drop(completion); + }); + barrier.wait(); + let result = resource.install(ResourceDropProbe(Arc::clone(&drops))); + close.join().expect("close thread"); + result + }); + + assert!(matches!( + install_result, + Ok(()) | Err(ResponseEgressResourceInstallError::Closed) + )); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn late_bound_poisoned_state_recovers_and_releases_once() { + let drops = Arc::new(AtomicUsize::new(0)); + let (resource, completion) = ResponseEgressCompletion::late_bound(); + let poisoned_state = Arc::clone(&resource.state); + let poison_result = catch_unwind(AssertUnwindSafe(move || { + let _guard = poisoned_state.lock().expect("unpoisoned state"); + panic!("poison response resource state"); + })); + assert!(poison_result.is_err()); + + resource + .install(ResourceDropProbe(Arc::clone(&drops))) + .expect("poison recovery installation"); + drop(completion); + + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn late_bound_install_before_terminal_releases_at_terminal_once() { + let drops = Arc::new(AtomicUsize::new(0)); + let (resource, mut completion) = ResponseEgressCompletion::late_bound(); + resource + .install(ResourceDropProbe(Arc::clone(&drops))) + .expect("first installation"); + + assert_eq!(drops.load(Ordering::SeqCst), 0); + completion.complete(&completed_report(MonotonicInstant::now())); + completion.complete(&completed_report(MonotonicInstant::now())); + assert_eq!(drops.load(Ordering::SeqCst), 1); + + drop(completion); + drop(resource); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn late_bound_terminal_before_install_rejects_and_releases_candidate() { + let drops = Arc::new(AtomicUsize::new(0)); + let (resource, mut completion) = ResponseEgressCompletion::late_bound(); + completion.complete(&completed_report(MonotonicInstant::now())); + + assert_eq!( + resource.install(ResourceDropProbe(Arc::clone(&drops))), + Err(ResponseEgressResourceInstallError::Closed) + ); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn late_bound_duplicate_install_rejects_and_releases_only_candidate() { + let installed_drops = Arc::new(AtomicUsize::new(0)); + let rejected_drops = Arc::new(AtomicUsize::new(0)); + let (resource, completion) = ResponseEgressCompletion::late_bound(); + resource + .install(ResourceDropProbe(Arc::clone(&installed_drops))) + .expect("first installation"); + + assert_eq!( + resource.install(ResourceDropProbe(Arc::clone(&rejected_drops))), + Err(ResponseEgressResourceInstallError::AlreadyInstalled) + ); + assert_eq!(installed_drops.load(Ordering::SeqCst), 0); + assert_eq!(rejected_drops.load(Ordering::SeqCst), 1); + + drop(completion); + assert_eq!(installed_drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn late_bound_abandonment_before_install_closes_surviving_installer() { + let drops = Arc::new(AtomicUsize::new(0)); + let (resource, completion) = ResponseEgressCompletion::late_bound(); + drop(completion); + + assert_eq!( + resource.install(ResourceDropProbe(Arc::clone(&drops))), + Err(ResponseEgressResourceInstallError::Closed) + ); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn late_bound_abandonment_after_install_releases_with_surviving_installer() { + let drops = Arc::new(AtomicUsize::new(0)); + let (resource, completion) = ResponseEgressCompletion::late_bound(); + resource + .install(ResourceDropProbe(Arc::clone(&drops))) + .expect("first installation"); + + drop(completion); + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert_eq!( + resource.install(ResourceDropProbe(Arc::clone(&drops))), + Err(ResponseEgressResourceInstallError::Closed) + ); + assert_eq!(drops.load(Ordering::SeqCst), 2); + } + + #[test] + fn late_bound_competing_terminal_signals_release_once() { + let drops = Arc::new(AtomicUsize::new(0)); + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let head = head(&headers, started_at, None); + let (resource, completion) = ResponseEgressCompletion::late_bound(); + resource + .install(ResourceDropProbe(Arc::clone(&drops))) + .expect("first installation"); + let mut attempt = ResponseEgressAttempt::new( + &head, + started_at, + completion, + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + ); + + assert!(attempt.begin_writing()); + assert!(attempt.complete(started_at)); + assert!(!attempt.complete(started_at)); + assert!(!attempt.terminate(ResponseEgressOutcome::TransportError, started_at)); + drop(attempt); + + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert_eq!(observer.reports().len(), 1); + } + + #[test] + fn late_bound_unbegun_envelope_releases_without_reporting() { + let drops = Arc::new(AtomicUsize::new(0)); + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let response = response_builder() + .status(StatusCode::OK) + .body(Body::empty()) + .expect("response"); + let (resource, completion) = ResponseEgressCompletion::late_bound(); + resource + .install(ResourceDropProbe(Arc::clone(&drops))) + .expect("first installation"); + + drop(ResponseEgressEnvelope::new( + response, + ResponseEgressRequestMetadata::new(Method::GET, started_at, None), + completion, + Arc::new(default_response_egress_policy), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + )); + + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert!(observer.reports().is_empty()); + } + + #[test] + fn response_completion_runs_once_before_the_global_observer() { + let events = Arc::new(Mutex::new(Vec::new())); + let completion_events = Arc::clone(&events); + let observer = OrderedObserver(Arc::clone(&events)); + let started_at = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let head = head(&headers, started_at, None); + let mut attempt = ResponseEgressAttempt::new( + &head, + started_at, + ResponseEgressCompletion::new(move |_| { + completion_events + .lock() + .expect("ordered events lock") + .push("completion"); + }), + ResponseEgressObserverHandle::new(observer), + MonotonicClock::default(), + ); + + assert!(attempt.begin_writing()); + assert!(attempt.complete(started_at)); + assert!(!attempt.complete(started_at)); + assert!(!attempt.terminate(ResponseEgressOutcome::TransportError, started_at)); + drop(attempt); + + assert_eq!( + events.lock().expect("ordered events lock").as_slice(), + &["completion", "observer"] + ); + } + + #[test] + fn response_completion_panic_does_not_suppress_the_global_observer() { + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let head = head(&headers, started_at, None); + let result = catch_unwind(AssertUnwindSafe(|| { + let mut attempt = ResponseEgressAttempt::new( + &head, + started_at, + ResponseEgressCompletion::new(|_| panic!("completion panic")), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + ); + assert!(attempt.terminate(ResponseEgressOutcome::ConversionError, started_at)); + })); + + result.unwrap_or_else(|_| panic!("completion panic escaped terminal transition")); + assert_eq!(observer.reports().len(), 1); + } + + #[test] + fn response_egress_completion_join_runs_left_to_right_once_with_same_report() { + let events = Arc::new(Mutex::new(Vec::new())); + let left_events = Arc::clone(&events); + let right_events = Arc::clone(&events); + let mut completion = ResponseEgressCompletion::new(move |report| { + left_events.lock().expect("completion events lock").push(( + "left", + from_ref(report).addr(), + report.clone(), + )); + }) + .join(ResponseEgressCompletion::new(move |report| { + right_events.lock().expect("completion events lock").push(( + "right", + from_ref(report).addr(), + report.clone(), + )); + })); + let report = completed_report(MonotonicInstant::now()); + + completion.complete(&report); + completion.complete(&report); + + let recorded_events = events.lock().expect("completion events lock"); + assert_eq!(recorded_events.len(), 2); + assert_eq!(recorded_events[0].0, "left"); + assert_eq!(recorded_events[1].0, "right"); + assert_eq!(recorded_events[0].1, recorded_events[1].1); + assert_eq!(recorded_events[0].2, report); + assert_eq!(recorded_events[1].2, report); + } + + #[cfg(panic = "unwind")] + #[test] + fn response_egress_completion_join_left_panic_does_not_suppress_right() { + let right_calls = Arc::new(AtomicUsize::new(0)); + let observed_right_calls = Arc::clone(&right_calls); + let mut completion = ResponseEgressCompletion::new(|_| panic!("left completion panic")) + .join(ResponseEgressCompletion::new(move |_| { + observed_right_calls.fetch_add(1, Ordering::SeqCst); + })); + let report = completed_report(MonotonicInstant::now()); + + let result = catch_unwind(AssertUnwindSafe(|| completion.complete(&report))); + + result.unwrap_or_else(|_| panic!("left completion panic escaped join")); + assert_eq!(right_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn response_egress_completion_join_abandonment_releases_both_resources_once() { + let left_drops = Arc::new(AtomicUsize::new(0)); + let right_drops = Arc::new(AtomicUsize::new(0)); + let left_probe = ResourceDropProbe(Arc::clone(&left_drops)); + let right_probe = ResourceDropProbe(Arc::clone(&right_drops)); + let completion = ResponseEgressCompletion::new(move |_| { + let _retain_until_completion = &left_probe; + }) + .join(ResponseEgressCompletion::new(move |_| { + let _retain_until_completion = &right_probe; + })); + + drop(completion); + + assert_eq!(left_drops.load(Ordering::SeqCst), 1); + assert_eq!(right_drops.load(Ordering::SeqCst), 1); + } + + #[test] + fn response_egress_completion_join_repeated_terminal_attempts_run_each_once() { + let left_calls = Arc::new(AtomicUsize::new(0)); + let right_calls = Arc::new(AtomicUsize::new(0)); + let observed_left_calls = Arc::clone(&left_calls); + let observed_right_calls = Arc::clone(&right_calls); + let completion = ResponseEgressCompletion::new(move |_| { + observed_left_calls.fetch_add(1, Ordering::SeqCst); + }) + .join(ResponseEgressCompletion::new(move |_| { + observed_right_calls.fetch_add(1, Ordering::SeqCst); + })); + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let head = head(&headers, started_at, None); + let mut attempt = ResponseEgressAttempt::new( + &head, + started_at, + completion, + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + ); + + assert!(attempt.begin_writing()); + assert!(attempt.complete(started_at)); + assert!(!attempt.complete(started_at)); + assert!(!attempt.terminate(ResponseEgressOutcome::TransportError, started_at)); + drop(attempt); + + assert_eq!(left_calls.load(Ordering::SeqCst), 1); + assert_eq!(right_calls.load(Ordering::SeqCst), 1); + assert_eq!(observer.reports().len(), 1); + } + + #[test] + fn unbegun_envelope_drops_completion_without_a_terminal_report() { + let observer = RecordingObserver::default(); + let callback_calls = Arc::new(Mutex::new(0_usize)); + let completion_drops = Arc::new(Mutex::new(0_usize)); + let observed_calls = Arc::clone(&callback_calls); + let probe = CompletionDropProbe(Arc::clone(&completion_drops)); + let started_at = MonotonicInstant::now(); + let response = response_builder() + .status(StatusCode::OK) + .body(Body::empty()) + .expect("response"); + + drop(ResponseEgressEnvelope::new( + response, + ResponseEgressRequestMetadata::new(Method::GET, started_at, None), + ResponseEgressCompletion::new(move |_| { + let _keep_probe_until_completion = &probe; + let mut calls = observed_calls.lock().expect("completion calls lock"); + *calls += 1; + }), + Arc::new(default_response_egress_policy), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + )); + + assert_eq!(*callback_calls.lock().expect("completion calls lock"), 0); + assert_eq!(*completion_drops.lock().expect("completion drops lock"), 1); + assert!(observer.reports().is_empty()); + } + + #[test] + fn every_outcome_is_reported_without_collapsing_variants_from_valid_states() { + let outcomes = [ + ResponseEgressOutcome::Completed, + ResponseEgressOutcome::ClientDisconnected, + ResponseEgressOutcome::ConversionError, + ResponseEgressOutcome::DeadlineExceeded, + ResponseEgressOutcome::HostHandoff, + ResponseEgressOutcome::RequestCancelled, + ResponseEgressOutcome::SourceError, + ResponseEgressOutcome::TransportError, + ResponseEgressOutcome::Unspecified, + ]; + + for outcome in outcomes { + let states = if matches!( + outcome, + ResponseEgressOutcome::Completed | ResponseEgressOutcome::HostHandoff + ) { + &[true][..] + } else { + &[false, true][..] + }; + for &writing in states { + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let mut attempt = attempt(&observer, started_at); + if writing { + assert!(attempt.begin_writing()); + } + if outcome == ResponseEgressOutcome::Completed { + assert!(attempt.complete(started_at)); + } else { + assert!(attempt.terminate(outcome, started_at)); + } + assert_eq!(observer.reports()[0].outcome, outcome); + } + } + } + + #[test] + fn completed_cannot_skip_the_writing_state() { + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let mut attempt = attempt(&observer, started_at); + + assert!(attempt.complete(started_at)); + assert_eq!(observer.reports().len(), 1); + assert_eq!( + observer.reports()[0].outcome, + ResponseEgressOutcome::ConversionError + ); + } + + #[test] + fn host_handoff_cannot_skip_the_writing_state() { + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let mut attempt = attempt(&observer, started_at); + + assert!(attempt.terminate(ResponseEgressOutcome::HostHandoff, started_at)); + assert_eq!(observer.reports().len(), 1); + assert_eq!( + observer.reports()[0].outcome, + ResponseEgressOutcome::ConversionError + ); + } + + #[test] + fn writing_completion_records_metadata_bytes_and_elapsed_once() { + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let terminal_at = started_at + .checked_add(Duration::from_millis(25)) + .expect("terminal instant"); + let request_start = started_at + .checked_sub(Duration::from_millis(10)) + .expect("request start"); + let route = RouteMetadata::new(Method::POST, "/submit"); + let headers = HeaderMap::new(); + let head = head(&headers, request_start, Some(&route)); + let mut attempt = ResponseEgressAttempt::new( + &head, + started_at, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + MonotonicClock::default(), + ); + + assert!(attempt.begin_writing()); + assert!(attempt.account_bytes(0, started_at)); + assert!(attempt.account_bytes(7, started_at)); + assert!(attempt.complete(terminal_at)); + assert!(!attempt.complete(terminal_at)); + assert!(!attempt.terminate(ResponseEgressOutcome::TransportError, terminal_at)); + drop(attempt); + + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].bytes_written, 7); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Application); + assert_eq!(reports[0].elapsed, Duration::from_millis(25)); + assert_eq!(reports[0].fallback_disposition, None); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::Completed); + assert_eq!(reports[0].request_start, request_start); + assert_eq!(reports[0].route, Some(route)); + } + + #[test] + fn drop_and_duplicate_signals_preserve_exactly_once_terminal_state() { + let initial_observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + drop(attempt(&initial_observer, started_at)); + assert_eq!(initial_observer.reports().len(), 1); + assert_eq!( + initial_observer.reports()[0].outcome, + ResponseEgressOutcome::ConversionError + ); + + let writing_observer = RecordingObserver::default(); + let mut writing = attempt(&writing_observer, started_at); + assert!(writing.begin_writing()); + assert!(!writing.begin_writing()); + drop(writing); + assert_eq!(writing_observer.reports().len(), 1); + assert_eq!( + writing_observer.reports()[0].outcome, + ResponseEgressOutcome::TransportError + ); + + let disconnected_observer = RecordingObserver::default(); + let mut disconnected = attempt(&disconnected_observer, started_at); + assert!(disconnected.begin_writing()); + assert!(disconnected.terminate(ResponseEgressOutcome::ClientDisconnected, started_at)); + drop(disconnected); + assert_eq!(disconnected_observer.reports().len(), 1); + } + + #[test] + fn dropped_attempt_uses_its_injected_clock() { + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let completed_at = started_at + .checked_add(Duration::from_millis(17)) + .expect("completed instant"); + let clock = MonotonicClock::new(move || completed_at); + let headers = HeaderMap::new(); + let head = head(&headers, started_at, None); + + drop(ResponseEgressAttempt::new( + &head, + started_at, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(observer.clone()), + clock, + )); + + assert_eq!(observer.reports()[0].elapsed, Duration::from_millis(17)); + } + + #[test] + fn byte_overflow_terminalizes_as_transport_error() { + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let mut attempt = attempt(&observer, started_at); + assert!(attempt.begin_writing()); + assert!(attempt.account_bytes(u64::MAX, started_at)); + assert!(!attempt.account_bytes(1, started_at)); + assert!(!attempt.complete(started_at)); + + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].bytes_written, u64::MAX); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::TransportError); + } + + #[test] + fn zero_byte_completion_has_zero_accounting() { + let completed_observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let mut completed = attempt(&completed_observer, started_at); + assert!(completed.begin_writing()); + assert!(completed.complete(started_at)); + assert_eq!(completed_observer.reports()[0].bytes_written, 0); + } + + #[test] + fn backwards_clock_is_zero_elapsed_unspecified_and_still_once() { + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let earlier = started_at + .checked_sub(Duration::from_millis(1)) + .expect("earlier instant"); + let mut attempt = attempt(&observer, started_at); + assert!(attempt.begin_writing()); + assert!(attempt.complete(earlier)); + drop(attempt); + + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].elapsed, Duration::ZERO); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::Unspecified); + } + + #[test] + fn observer_panics_do_not_escape_terminal_or_drop_paths() { + let direct = catch_unwind(AssertUnwindSafe(|| { + let started_at = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let head = head(&headers, started_at, None); + let mut attempt = ResponseEgressAttempt::new( + &head, + started_at, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(PanickingObserver), + MonotonicClock::default(), + ); + assert!(attempt.terminate(ResponseEgressOutcome::ConversionError, started_at)); + })); + direct.unwrap_or_else(|_| panic!("observer panic escaped direct completion")); + + let dropped = catch_unwind(AssertUnwindSafe(|| { + let started_at = MonotonicInstant::now(); + let headers = HeaderMap::new(); + let head = head(&headers, started_at, None); + drop(ResponseEgressAttempt::new( + &head, + started_at, + ResponseEgressCompletion::empty(), + ResponseEgressObserverHandle::new(PanickingObserver), + MonotonicClock::default(), + )); + })); + dropped.unwrap_or_else(|_| panic!("observer panic escaped guard drop")); + } + + #[test] + fn fallback_reports_its_bytes_and_disposition_with_original_cause() { + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let mut fallback = attempt(&observer, started_at); + + assert!(fallback.begin_fallback(ResponseEgressOutcome::ConversionError)); + assert!(fallback.begin_writing()); + assert!(fallback.account_bytes(7, started_at)); + assert!( + fallback.finish_fallback(ResponseEgressFallbackDisposition::Completed, started_at,) + ); + + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].bytes_written, 7); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Completed) + ); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::ConversionError); + } + + #[test] + fn fallback_selection_is_one_shot_and_preserves_its_first_cause() { + let observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let mut fallback = attempt(&observer, started_at); + + assert!(fallback.begin_fallback(ResponseEgressOutcome::ConversionError)); + assert!(!fallback.begin_fallback(ResponseEgressOutcome::DeadlineExceeded)); + assert!(fallback.begin_writing()); + assert!(fallback.account_bytes(3, started_at)); + assert!(fallback.finish_fallback(ResponseEgressFallbackDisposition::Aborted, started_at,)); + + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].bytes_written, 3); + assert_eq!( + reports[0].fallback_disposition, + Some(ResponseEgressFallbackDisposition::Aborted) + ); + assert_eq!(reports[0].outcome, ResponseEgressOutcome::ConversionError); + } + + #[test] + fn fallback_drop_and_accounting_overflow_abort_with_original_cause() { + let drop_observer = RecordingObserver::default(); + let started_at = MonotonicInstant::now(); + let mut dropped = attempt(&drop_observer, started_at); + assert!(!dropped.begin_fallback(ResponseEgressOutcome::SourceError)); + assert!(dropped.begin_fallback(ResponseEgressOutcome::DeadlineExceeded)); + drop(dropped); + let dropped_report = &drop_observer.reports()[0]; + assert_eq!( + dropped_report.outcome, + ResponseEgressOutcome::DeadlineExceeded + ); + assert_eq!( + dropped_report.fallback_disposition, + Some(ResponseEgressFallbackDisposition::Aborted) + ); + + let overflow_observer = RecordingObserver::default(); + let mut overflow = attempt(&overflow_observer, started_at); + assert!(overflow.begin_fallback(ResponseEgressOutcome::ConversionError)); + assert!(overflow.begin_writing()); + assert!(overflow.account_bytes(u64::MAX, started_at)); + assert!(!overflow.account_bytes(1, started_at)); + let overflow_report = &overflow_observer.reports()[0]; + assert_eq!(overflow_report.bytes_written, u64::MAX); + assert_eq!( + overflow_report.outcome, + ResponseEgressOutcome::ConversionError + ); + assert_eq!( + overflow_report.fallback_disposition, + Some(ResponseEgressFallbackDisposition::Aborted) + ); + } + + #[test] + fn policy_panic_returns_live_attempt_for_one_fallback_lifecycle() { + let observer = RecordingObserver::default(); + let request_start = MonotonicInstant::now(); + let started_at = request_start + .checked_add(Duration::from_millis(1)) + .expect("egress start"); + let clock = MonotonicClock::new(move || started_at); + let response = response_builder() + .status(StatusCode::OK) + .body(Body::from("private token=secret")) + .expect("response"); + let envelope = ResponseEgressEnvelope::new( + response, + ResponseEgressRequestMetadata::new(Method::GET, request_start, None), + ResponseEgressCompletion::empty(), + Arc::new(|_, _| panic!("private token=secret")), + ResponseEgressObserverHandle::new(observer.clone()), + clock, + ); + assert_eq!(envelope.request_method(), &Method::GET); + + let Err(failure) = envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) else { + panic!("policy panic must select fallback"); + }; + assert!(observer.reports().is_empty()); + let (mut attempt, failure_clock, cause) = failure.into_parts(); + assert_eq!(cause, ResponseEgressOutcome::ConversionError); + assert_eq!(failure_clock.now(), started_at); + assert!(attempt.begin_fallback(cause)); + assert!(attempt.begin_writing()); + assert!(attempt.finish_fallback(ResponseEgressFallbackDisposition::Completed, started_at,)); + + let reports = observer.reports(); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].body_kind, ResponseEgressBodyKind::Fallback); + assert_eq!(reports[0].bytes_written, 0); + assert_eq!( + reports[0].framing_failures, + ResponseEgressFramingFailures::default() + ); + } + + #[test] + fn framing_failure_drops_source_unpolled_and_returns_one_live_attempt() { + use std::cell::Cell; + use std::rc::Rc; + use std::task::Poll; + + use futures_util::stream::poll_fn; + + struct DropSignal(Rc>); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.set(self.0.get() + 1); + } + } + + let observer = RecordingObserver::default(); + let request_start = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || request_start); + let polls = Rc::new(Cell::new(0_usize)); + let drops = Rc::new(Cell::new(0_usize)); + let observed_polls = Rc::clone(&polls); + let drop_signal = DropSignal(Rc::clone(&drops)); + let body = Body::from_stream(poll_fn(move |_| { + let _keep_signal_alive = &drop_signal; + observed_polls.set(observed_polls.get() + 1); + Poll::Ready(Some(Ok(bytes::Bytes::from_static(b"private")))) + })); + let response = response_builder() + .status(StatusCode::OK) + .header("trailer", "x-checksum") + .body(body) + .expect("response"); + let envelope = ResponseEgressEnvelope::new( + response, + ResponseEgressRequestMetadata::new(Method::GET, request_start, None), + ResponseEgressCompletion::empty(), + Arc::new(default_response_egress_policy), + ResponseEgressObserverHandle::new(observer.clone()), + clock, + ); + + let Err(failure) = envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) else { + panic!("framing failure must retain the attempt"); + }; + assert_eq!(polls.get(), 0); + assert_eq!(drops.get(), 1); + assert!(observer.reports().is_empty()); + let (mut attempt, _, cause) = failure.into_parts(); + assert_eq!(cause, ResponseEgressOutcome::ConversionError); + assert!(attempt.begin_fallback(cause)); + assert!( + attempt.finish_fallback(ResponseEgressFallbackDisposition::Aborted, request_start,) + ); + assert_eq!(observer.reports().len(), 1); + } + + #[test] + fn response_carried_deadline_is_visible_to_policy_and_clamps_write_lifetime() { + let observer = RecordingObserver::default(); + let request_start = MonotonicInstant::now(); + let egress_started_at = request_start + .checked_add(Duration::from_millis(10)) + .expect("egress start"); + let application_deadline = Deadline::at_instant( + request_start + .checked_add(Duration::from_millis(50)) + .expect("application deadline"), + ); + let callback_deadline = Deadline::at_instant( + request_start + .checked_add(Duration::from_millis(100)) + .expect("callback deadline"), + ); + let mut response = response_builder() + .status(StatusCode::OK) + .body(Body::from("body")) + .expect("response"); + response + .extensions_mut() + .insert(ResponseEgressDeadline::at(application_deadline)); + let envelope = ResponseEgressEnvelope::new( + response, + ResponseEgressRequestMetadata::new(Method::GET, request_start, None), + ResponseEgressCompletion::empty(), + Arc::new(move |head, observed_start| { + assert_eq!(observed_start, egress_started_at); + assert_eq!( + head.application_deadline() + .map(|deadline| deadline.instant()), + Some(application_deadline.instant()) + ); + ResponseEgressPolicy { + write_deadline: callback_deadline, + } + }), + ResponseEgressObserverHandle::new(observer), + MonotonicClock::new(move || egress_started_at), + ); + + let Ok((prepared, policy, _attempt, _clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("egress preparation failed"); + }; + + assert_eq!( + policy.write_deadline.instant(), + application_deadline.instant() + ); + assert!( + prepared + .into_response() + .extensions() + .get::() + .is_none() + ); + } + + #[test] + fn response_relative_deadline_resolves_from_injected_egress_start() { + let request_start = MonotonicInstant::now(); + let egress_started_at = request_start + .checked_add(Duration::from_millis(10)) + .expect("egress start"); + let expected_deadline = egress_started_at + .checked_add(Duration::from_millis(40)) + .expect("relative deadline"); + let callback_deadline = Deadline::at_instant( + egress_started_at + .checked_add(Duration::from_millis(100)) + .expect("callback deadline"), + ); + let mut response = response_builder() + .status(StatusCode::OK) + .body(Body::from("body")) + .expect("response"); + response + .extensions_mut() + .insert(ResponseEgressDeadline::after(Duration::from_millis(40))); + let envelope = ResponseEgressEnvelope::new( + response, + ResponseEgressRequestMetadata::new(Method::GET, request_start, None), + ResponseEgressCompletion::empty(), + Arc::new(move |head, observed_start| { + assert_eq!(observed_start, egress_started_at); + assert_eq!( + head.application_deadline() + .map(|deadline| deadline.instant()), + Some(expected_deadline) + ); + ResponseEgressPolicy { + write_deadline: callback_deadline, + } + }), + ResponseEgressObserverHandle::default(), + MonotonicClock::new(move || egress_started_at), + ); + + let Ok((prepared, policy, _attempt, _clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("egress preparation failed"); + }; + + assert_eq!(policy.write_deadline.instant(), expected_deadline); + assert!( + prepared + .into_response() + .extensions() + .get::() + .is_none() + ); + } + + #[test] + fn response_relative_deadline_zero_expires_at_egress_start() { + let egress_started_at = MonotonicInstant::now(); + let deadline = ResponseEgressDeadline::after(Duration::ZERO).resolve_at(egress_started_at); + + assert_eq!(deadline.instant(), egress_started_at); + assert!(deadline.is_expired_at(egress_started_at)); + } + + #[test] + fn response_relative_deadline_clamps_to_portable_far_future() { + let egress_started_at = MonotonicInstant::now(); + let expected = egress_started_at + .checked_add(DEADLINE_FAR_FUTURE) + .expect("portable far-future deadline"); + let deadline = ResponseEgressDeadline::after(Duration::MAX).resolve_at(egress_started_at); + + assert_eq!(deadline.instant(), expected); + } + + #[test] + fn response_absolute_deadline_is_not_reanchored_at_egress_start() { + let absolute = MonotonicInstant::now(); + let egress_started_at = absolute + .checked_add(Duration::from_secs(1)) + .expect("egress start"); + let deadline = ResponseEgressDeadline::at(Deadline::at_instant(absolute)) + .resolve_at(egress_started_at); + + assert_eq!(deadline.instant(), absolute); + assert!(deadline.is_expired_at(egress_started_at)); + } + + #[test] + fn adapter_response_hook_runs_before_policy_and_framing() { + let request_start = MonotonicInstant::now(); + let observed_status = Arc::new(Mutex::new(None)); + let policy_status = Arc::clone(&observed_status); + let response = response_builder() + .status(StatusCode::OK) + .body(Body::from("body")) + .expect("response"); + let mut envelope = ResponseEgressEnvelope::new( + response, + ResponseEgressRequestMetadata::new(Method::HEAD, request_start, None), + ResponseEgressCompletion::empty(), + Arc::new(move |head, started_at| { + *policy_status.lock().expect("policy status") = Some(head.status()); + ResponseEgressPolicy { + write_deadline: Deadline::at_instant( + started_at + .checked_add(Duration::from_secs(1)) + .expect("write deadline"), + ), + } + }), + ResponseEgressObserverHandle::default(), + MonotonicClock::new(move || request_start), + ); + + *envelope.response_mut().status_mut() = StatusCode::CREATED; + let Ok((prepared, _policy, _attempt, _clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("egress preparation failed"); + }; + let prepared_response = prepared.into_response(); + + assert_eq!( + *observed_status.lock().expect("observed status"), + Some(StatusCode::CREATED) + ); + assert_eq!(prepared_response.status(), StatusCode::CREATED); + assert!( + prepared_response + .into_body() + .into_bytes() + .expect("buffered body") + .is_empty() + ); + } +} diff --git a/crates/edgezero-core/src/response_egress_framing.rs b/crates/edgezero-core/src/response_egress_framing.rs new file mode 100644 index 000000000..0c757ca68 --- /dev/null +++ b/crates/edgezero-core/src/response_egress_framing.rs @@ -0,0 +1,1256 @@ +#[cfg(any(test, feature = "test-utils"))] +#[doc(hidden)] +#[path = "response_egress_framing_test_support.rs"] +pub mod test_support; + +use bytes::Bytes; +use std::mem; + +use crate::body::{Body, BodyPayload, BodyStream}; +use crate::error::suppress_overflow_retry_after; +use crate::error::{BadGatewayDecodeReason, BadGatewayReason, EdgeError, ResponseLimitReason}; +use crate::http::header::{ + CONNECTION, CONTENT_LENGTH, PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, TE, TRAILER, + TRANSFER_ENCODING, UPGRADE, +}; +use crate::http::{HeaderMap, HeaderName, HeaderValue, Method, Response, StatusCode}; +use futures_util::StreamExt as _; +use futures_util::stream::unfold; + +/// Typed streamed-body violation used by adapter coordinators for terminal classification. +#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] +#[non_exhaustive] +pub enum ResponseEgressBodyLengthError { + #[error("response body exceeded declared content-length")] + Exceeded, + #[error("response body ended before declared content-length")] + Incomplete, +} + +/// Category-only response framing failure reported before platform commitment. +#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] +#[non_exhaustive] +pub enum ResponseEgressFramingError { + #[error("response body length does not match content-length")] + ContentLengthMismatch, + #[error("invalid connection response header")] + InvalidConnectionHeader, + #[error("invalid response content-length")] + InvalidContentLength, + #[error("unsupported final response status")] + UnsupportedStatus, + #[error("response trailers are unsupported")] + UnsupportedTrailers, + #[error("successful connect response requires a tunnel lifecycle")] + UnsupportedTunnel, +} + +/// Selects whether buffered wire framing is explicit or owned by the platform sender. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[doc(hidden)] +pub enum ResponseEgressFramingMode { + /// Derive an exact length for transmitted buffered bodies after normalization. + ExplicitBufferedLength, + /// Preserve source framing and let a known-buffer platform send choose wire framing. + HostAutomatic, +} + +/// Canonical response head and body ready for one adapter-owned writer. +#[doc(hidden)] +pub struct PreparedResponseEgress { + declared_length: Option, + response: Response, + transmits_body: bool, +} + +impl PreparedResponseEgress { + #[must_use] + #[inline] + pub fn declared_length(&self) -> Option { + self.declared_length + } + + #[must_use] + #[inline] + pub fn into_response(self) -> Response { + self.response + } + + #[must_use] + #[inline] + pub fn transmits_body(&self) -> bool { + self.transmits_body + } +} + +/// Validates and normalizes response framing before an adapter commits the head. +/// +/// # Errors +/// Returns a category-only framing error for invalid or conflicting field values and for a +/// buffered body whose length disagrees with an explicit `Content-Length`. +#[doc(hidden)] +#[inline] +pub fn prepare_response_egress( + framing: ResponseEgressFramingMode, + request_method: &Method, + mut response: Response, +) -> Result { + suppress_overflow_retry_after(&mut response); + let mut declared_length = parse_content_length(response.headers())?; + if response.status().is_informational() { + return Err(ResponseEgressFramingError::UnsupportedStatus); + } + if *request_method == Method::CONNECT && response.status().is_success() { + return Err(ResponseEgressFramingError::UnsupportedTunnel); + } + + let suppress_body = if response.status() == StatusCode::NO_CONTENT { + response.headers_mut().remove(CONTENT_LENGTH); + declared_length = None; + true + } else if response.status() == StatusCode::RESET_CONTENT { + if declared_length.is_some_and(|length| length != 0) { + return Err(ResponseEgressFramingError::ContentLengthMismatch); + } + true + } else { + response.status() == StatusCode::NOT_MODIFIED || *request_method == Method::HEAD + }; + + if suppress_body { + let mut original_body = mem::take(response.body_mut()); + let owner = original_body.take_egress_owner(); + drop(original_body); + *response.body_mut() = Body::from_parts(BodyPayload::Once(Bytes::new()), owner); + } + + if response.headers().contains_key(TRAILER) { + return Err(ResponseEgressFramingError::UnsupportedTrailers); + } + let content_length_was_nominated = strip_connection_fields(response.headers_mut())?; + response.headers_mut().remove(TRANSFER_ENCODING); + if content_length_was_nominated { + declared_length = None; + } + + if framing == ResponseEgressFramingMode::ExplicitBufferedLength + && !suppress_body + && declared_length.is_none() + && let Some(bytes) = response.body().as_bytes() + { + declared_length = Some( + u64::try_from(bytes.len()) + .map_err(|_error| ResponseEgressFramingError::ContentLengthMismatch)?, + ); + } + + if let Some(length) = declared_length { + response.headers_mut().remove(CONTENT_LENGTH); + let canonical = HeaderValue::from_str(&length.to_string()) + .map_err(|_error| ResponseEgressFramingError::InvalidContentLength)?; + response.headers_mut().insert(CONTENT_LENGTH, canonical); + if !suppress_body { + if let Some(bytes) = response.body().as_bytes() { + let actual = u64::try_from(bytes.len()) + .map_err(|_error| ResponseEgressFramingError::ContentLengthMismatch)?; + if actual != length { + return Err(ResponseEgressFramingError::ContentLengthMismatch); + } + } else { + let body = mem::take(response.body_mut()); + let (payload, owner) = body.into_parts(); + let limited_payload = match payload { + BodyPayload::Stream(stream) => { + BodyPayload::Stream(limit_declared_stream(stream, length)) + } + buffered @ BodyPayload::Once(_) => buffered, + }; + *response.body_mut() = Body::from_parts(limited_payload, owner); + } + } + } + + Ok(PreparedResponseEgress { + declared_length, + response, + transmits_body: !suppress_body, + }) +} + +fn limit_declared_stream(stream: BodyStream, length: u64) -> BodyStream { + unfold( + (Some(stream), length), + |(optional_source, remaining)| async move { + let mut source = optional_source?; + match source.next().await { + Some(Ok(bytes)) => { + let Ok(chunk_length) = u64::try_from(bytes.len()) else { + drop(source); + return Some(( + Err(EdgeError::internal(ResponseEgressBodyLengthError::Exceeded)), + (None, remaining), + )); + }; + let Some(next_remaining) = remaining.checked_sub(chunk_length) else { + drop(source); + return Some(( + Err(EdgeError::internal(ResponseEgressBodyLengthError::Exceeded)), + (None, remaining), + )); + }; + Some((Ok(bytes), (Some(source), next_remaining))) + } + Some(Err(error)) => { + drop(source); + Some((Err(error), (None, remaining))) + } + None if remaining == 0 => None, + None => { + drop(source); + Some(( + Err(EdgeError::internal( + ResponseEgressBodyLengthError::Incomplete, + )), + (None, remaining), + )) + } + } + }, + ) + .boxed_local() +} + +/// Returns the typed streamed-body length violation carried by an internal body error. +#[must_use] +#[doc(hidden)] +#[inline] +pub fn response_egress_body_length_error( + error: &EdgeError, +) -> Option { + let EdgeError::Internal { source, .. } = error else { + return None; + }; + source + .downcast_ref::() + .copied() +} + +/// Returns a fixed diagnostic category for a post-commit response body source failure. +/// +/// The result never includes an application/provider message and is therefore safe for adapter +/// logs after the response head has already been committed. +#[must_use] +#[doc(hidden)] +#[inline] +pub fn response_egress_source_error_category(error: &EdgeError) -> &'static str { + if let Some(length_error) = response_egress_body_length_error(error) { + return match length_error { + ResponseEgressBodyLengthError::Exceeded => "declared_length_exceeded", + ResponseEgressBodyLengthError::Incomplete => "declared_length_incomplete", + }; + } + match error { + EdgeError::BadGateway { reason, .. } => match reason { + BadGatewayReason::Decode(decode) => match decode { + BadGatewayDecodeReason::Brotli => "bad_gateway_decode_brotli", + BadGatewayDecodeReason::Deflate => "bad_gateway_decode_deflate", + BadGatewayDecodeReason::Gzip => "bad_gateway_decode_gzip", + BadGatewayDecodeReason::Json => "bad_gateway_decode_json", + }, + BadGatewayReason::ImageOptimizerUnavailable => { + "bad_gateway_image_optimizer_unavailable" + } + BadGatewayReason::Protocol => "bad_gateway_protocol", + BadGatewayReason::Transport => "bad_gateway_transport", + BadGatewayReason::Unreachable => "bad_gateway_unreachable", + BadGatewayReason::Unspecified => "bad_gateway_unspecified", + }, + EdgeError::BadRequest { .. } => "bad_request", + EdgeError::ConfigOutOfDate { .. } => "config_out_of_date", + EdgeError::GatewayTimeout { .. } => "gateway_timeout", + EdgeError::Internal { .. } => "internal", + EdgeError::MethodNotAllowed { .. } => "method_not_allowed", + EdgeError::NotFound { .. } => "not_found", + EdgeError::NotImplemented { .. } => "not_implemented", + EdgeError::PayloadTooLarge { .. } => "payload_too_large", + EdgeError::RequestHeaderFieldsTooLarge { .. } => "request_header_fields_too_large", + EdgeError::RequestTimeout { .. } => "request_timeout", + EdgeError::ResponseTooLarge { reason, .. } => match reason { + ResponseLimitReason::BrotliWindow => "response_too_large_brotli_window", + ResponseLimitReason::BufferedBody => "response_too_large_buffered_body", + ResponseLimitReason::DecodedBody => "response_too_large_decoded_body", + ResponseLimitReason::DecoderMemory => "response_too_large_decoder_memory", + ResponseLimitReason::EncodedBody => "response_too_large_encoded_body", + ResponseLimitReason::HeaderBytes => "response_too_large_header_bytes", + ResponseLimitReason::HeaderCount => "response_too_large_header_count", + ResponseLimitReason::Unspecified => "response_too_large_unspecified", + }, + EdgeError::ServiceUnavailable { .. } => "service_unavailable", + EdgeError::StoreExtraction { .. } => "store_extraction", + EdgeError::UriTooLong { .. } => "uri_too_long", + EdgeError::Validation { .. } => "validation", + } +} + +fn parse_content_length(headers: &HeaderMap) -> Result, ResponseEgressFramingError> { + let mut parsed = None; + for header_value in headers.get_all(CONTENT_LENGTH) { + let text = header_value + .to_str() + .map_err(|_error| ResponseEgressFramingError::InvalidContentLength)?; + for member in text.split(',') { + let candidate = member.trim(); + if candidate.is_empty() || !candidate.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(ResponseEgressFramingError::InvalidContentLength); + } + let parsed_value = candidate + .parse::() + .map_err(|_error| ResponseEgressFramingError::InvalidContentLength)?; + if parsed.is_some_and(|existing| existing != parsed_value) { + return Err(ResponseEgressFramingError::InvalidContentLength); + } + parsed = Some(parsed_value); + } + } + Ok(parsed) +} + +fn strip_connection_fields(headers: &mut HeaderMap) -> Result { + let mut nominated = Vec::new(); + for header_value in headers.get_all(CONNECTION) { + let text = header_value + .to_str() + .map_err(|_error| ResponseEgressFramingError::InvalidConnectionHeader)?; + for token in text.split(',') { + let name = HeaderName::from_bytes(token.trim().as_bytes()) + .map_err(|_error| ResponseEgressFramingError::InvalidConnectionHeader)?; + nominated.push(name); + } + } + let content_length_was_nominated = nominated.contains(&CONTENT_LENGTH); + for name in nominated { + headers.remove(name); + } + for name in [ + CONNECTION, + PROXY_AUTHENTICATE, + PROXY_AUTHORIZATION, + TE, + TRAILER, + TRANSFER_ENCODING, + UPGRADE, + ] { + headers.remove(name); + } + headers.remove("keep-alive"); + headers.remove("proxy-connection"); + Ok(content_length_was_nominated) +} + +#[cfg(test)] +mod tests { + use std::cell::Cell; + use std::collections::VecDeque; + use std::rc::Rc; + use std::task::Poll; + + use bytes::Bytes; + use futures::executor::block_on; + use futures_util::StreamExt as _; + use futures_util::stream::{self, poll_fn}; + + use crate::body::{Body, BodyPayload}; + use crate::error::{BadGatewayDecodeReason, BadGatewayReason, EdgeError}; + use crate::http::header::{CONTENT_LENGTH, TRANSFER_ENCODING}; + use crate::http::{HeaderValue, Method, Response, StatusCode, response_builder}; + use crate::outbound::OutboundScopeOwner; + use crate::response::IntoResponse as _; + + use super::{ + ResponseEgressBodyLengthError, ResponseEgressFramingError, ResponseEgressFramingMode, + prepare_response_egress, response_egress_body_length_error, + response_egress_source_error_category, + }; + + struct DropSignal(Rc>); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.set(self.0.get().saturating_add(1)); + } + } + + #[test] + fn framing_transforms_preserve_exact_egress_owner() { + for (streamed, suppressed) in [(false, false), (true, false), (true, true)] { + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let bytes = Bytes::from(vec![b'x'; 4]); + let original_pointer = bytes.as_ptr(); + let body = if streamed { + Body::from_stream(stream::iter([Ok(bytes)])) + } else { + Body::from(bytes) + }; + let mut response = owner.attach(Response::new(body)).expect("attach"); + response + .headers_mut() + .insert(CONTENT_LENGTH, HeaderValue::from_static("4")); + let method = if suppressed { + Method::HEAD + } else { + Method::GET + }; + let normalized_response = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &method, + response, + ) + .expect("normalize") + .into_response(); + assert_eq!( + normalized_response + .headers() + .get(CONTENT_LENGTH) + .expect("exact length"), + "4" + ); + scope + .check() + .expect("framing never closes response ownership"); + let (payload, retained_owner) = normalized_response.into_body().into_parts(); + assert!(retained_owner.is_some()); + match payload { + BodyPayload::Once(retained_bytes) => { + assert!(!streamed || suppressed); + if suppressed { + assert!(retained_bytes.is_empty()); + } else { + assert_eq!(retained_bytes.as_ptr(), original_pointer); + } + } + BodyPayload::Stream(mut source) => { + assert!(streamed && !suppressed); + assert_eq!( + block_on(source.next()) + .expect("chunk") + .expect("bytes") + .len(), + 4 + ); + assert!(block_on(source.next()).is_none()); + } + } + scope + .check() + .expect("stream EOF does not drop the separate owner"); + drop(retained_owner); + assert!(scope.check().is_err()); + } + } + + #[test] + fn buffered_framing_respects_suppression_and_host_transform() { + for bytes in [Bytes::from_static(b"abcdef"), Bytes::new()] { + let length = bytes.len().to_string(); + let response = response_builder() + .body(Body::from_bytes(bytes)) + .expect("response"); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + response, + ) + .expect("buffered response"); + assert!(prepared.transmits_body()); + assert_eq!( + prepared.declared_length().map(|value| value.to_string()), + Some(length.clone()) + ); + let converted = prepared.into_response(); + assert_eq!( + converted + .headers() + .get(CONTENT_LENGTH) + .expect("derived length"), + length.as_str() + ); + assert!(!converted.headers().contains_key(TRANSFER_ENCODING)); + } + + for (method, status) in [ + (Method::HEAD, StatusCode::OK), + (Method::GET, StatusCode::NO_CONTENT), + (Method::GET, StatusCode::RESET_CONTENT), + (Method::GET, StatusCode::NOT_MODIFIED), + ] { + let response = response_builder() + .status(status) + .body(Body::from("abcdef")) + .expect("bodyless response"); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &method, + response, + ) + .expect("suppressed body"); + assert!(!prepared.transmits_body()); + assert!( + !prepared + .into_response() + .headers() + .contains_key(CONTENT_LENGTH) + ); + } + + let stream = poll_fn(|_| Poll::Pending::>>); + let streamed_response = response_builder() + .body(Body::from_stream(stream)) + .expect("stream response"); + let prepared_stream = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + streamed_response, + ) + .expect("stream remains lazy"); + assert_eq!(prepared_stream.declared_length(), None); + + let transformed_response = response_builder() + .header("x-compress-hint", "on") + .header(CONTENT_LENGTH, "6") + .body(Body::from("abcdef")) + .expect("host-transform source"); + let prepared_transform = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + transformed_response, + ) + .expect("validated source length"); + assert_eq!(prepared_transform.declared_length(), Some(6)); + assert_eq!( + prepared_transform + .into_response() + .headers() + .get(CONTENT_LENGTH) + .expect("source length"), + "6" + ); + } + + #[test] + fn overflow_retry_suppression_survives_header_reinsertion_but_not_status_guessing() { + for origin in [ + crate::RequestBodyOverflowOrigin::Inbound, + crate::RequestBodyOverflowOrigin::OutboundUpload, + ] { + let mut response = EdgeError::payload_too_large(origin) + .into_response() + .expect("response"); + response + .headers_mut() + .append("retry-after", HeaderValue::from_static("120")); + response + .headers_mut() + .append("retry-after", HeaderValue::from_static("240")); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::POST, + response, + ) + .expect("prepared") + .into_response(); + assert_eq!(prepared.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert!(!prepared.headers().contains_key("retry-after")); + } + let upstream = response_builder() + .status(StatusCode::PAYLOAD_TOO_LARGE) + .header("retry-after", "120") + .body(Body::empty()) + .expect("upstream response"); + let prepared_upstream = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::POST, + upstream, + ) + .expect("prepared") + .into_response(); + assert_eq!( + prepared_upstream + .headers() + .get("retry-after") + .expect("upstream retry"), + "120" + ); + let config = EdgeError::config_out_of_date("stale", "field") + .into_response() + .expect("config response"); + let prepared_config = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::POST, + config, + ) + .expect("prepared") + .into_response(); + assert_eq!( + prepared_config + .headers() + .get("retry-after") + .expect("config retry"), + "60" + ); + } + + fn tracked_body(source_items: Vec>, drops: &Rc>) -> Body { + let signal = DropSignal(Rc::clone(drops)); + let mut pending_items = VecDeque::from(source_items); + Body::from_stream(poll_fn(move |_context| { + let _keep_signal_alive = &signal; + Poll::Ready(pending_items.pop_front()) + })) + } + + fn prepare_error( + method: &Method, + response: Response, + failure_message: &str, + ) -> ResponseEgressFramingError { + let Err(error) = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + method, + response, + ) else { + panic!("{failure_message}"); + }; + error + } + + #[test] + fn normalizes_duplicate_content_length_and_strips_connection_fields() { + let source_response = response_builder() + .status(StatusCode::OK) + .header("content-length", "5, 5") + .header("content-length", "5") + .header("connection", "x-private, keep-alive") + .header("x-private", "secret") + .header("keep-alive", "timeout=5") + .body(Body::from("hello")) + .expect("response"); + + let prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + source_response, + ) + .expect("prepared response"); + assert!(prepared.transmits_body()); + assert_eq!(prepared.declared_length(), Some(5)); + let converted_response = prepared.into_response(); + assert_eq!( + converted_response + .headers() + .get("content-length") + .expect("length"), + "5" + ); + assert!(!converted_response.headers().contains_key("connection")); + assert!(!converted_response.headers().contains_key("x-private")); + assert!(!converted_response.headers().contains_key("keep-alive")); + } + + #[test] + fn connection_nominated_content_length_is_not_reused_for_framing() { + let source_response = response_builder() + .status(StatusCode::OK) + .header("connection", "content-length") + .header("content-length", "5") + .body(Body::from("hello")) + .expect("response"); + + let prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + source_response, + ) + .expect("prepared response"); + assert_eq!(prepared.declared_length(), Some(5)); + assert_eq!(prepared.into_response().headers()[CONTENT_LENGTH], "5"); + } + + #[test] + fn host_automatic_preserves_suppression_and_does_not_derive_wire_length() { + for status in [ + StatusCode::OK, + StatusCode::NO_CONTENT, + StatusCode::RESET_CONTENT, + StatusCode::NOT_MODIFIED, + ] { + let response = response_builder() + .status(status) + .body(Body::from("abcdef")) + .expect("response"); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::HostAutomatic, + &Method::HEAD, + response, + ) + .expect("prepared"); + assert!(!prepared.transmits_body()); + assert_eq!(prepared.declared_length(), None); + assert_eq!( + prepared.into_response().body().as_bytes(), + Some(b"".as_slice()) + ); + } + let response = response_builder() + .header("x-compress-hint", "on") + .body(Body::from("abcdef")) + .expect("response"); + let prepared = prepare_response_egress( + ResponseEgressFramingMode::HostAutomatic, + &Method::GET, + response, + ) + .expect("host-owned framing"); + assert!(prepared.transmits_body()); + assert_eq!(prepared.declared_length(), None); + assert!( + !prepared + .into_response() + .headers() + .contains_key(CONTENT_LENGTH) + ); + } + + #[test] + fn strips_every_hop_by_hop_field_across_connection_lines() { + let source_response = response_builder() + .status(StatusCode::OK) + .header("connection", "x-first") + .header("connection", "x-second, upgrade") + .header("x-first", "private") + .header("x-second", "private") + .header("keep-alive", "timeout=5") + .header("proxy-authenticate", "challenge") + .header("proxy-authorization", "credential") + .header("proxy-connection", "keep-alive") + .header("te", "trailers") + .header("transfer-encoding", "chunked") + .header("upgrade", "websocket") + .body(Body::from("body")) + .expect("response"); + + let converted_response = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + source_response, + ) + .expect("prepared response") + .into_response(); + for name in [ + "connection", + "x-first", + "x-second", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "proxy-connection", + "te", + "transfer-encoding", + "upgrade", + ] { + assert!( + !converted_response.headers().contains_key(name), + "retained {name}" + ); + } + } + + #[test] + fn accepts_whitespace_and_rejects_buffered_length_mismatch() { + let spaced = response_builder() + .status(StatusCode::OK) + .header("content-length", " 5 , 5 ") + .body(Body::from("hello")) + .expect("response"); + assert_eq!( + prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + spaced + ) + .expect("whitespace is valid") + .declared_length(), + Some(5) + ); + + let mismatch = response_builder() + .status(StatusCode::OK) + .header("content-length", "4") + .body(Body::from("hello")) + .expect("response"); + let error = prepare_error( + &Method::GET, + mismatch, + "buffered mismatch must fail before commit", + ); + assert_eq!(error, ResponseEgressFramingError::ContentLengthMismatch); + } + + #[test] + fn exact_declared_stream_reaches_clean_eof() { + let response = response_builder() + .status(StatusCode::OK) + .header("content-length", "4") + .body(Body::from_stream(stream::iter([ + Ok(Bytes::from_static(b"ab")), + Ok(Bytes::from_static(b"cd")), + ]))) + .expect("response"); + let mut body = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + response, + ) + .expect("prepared response") + .into_response() + .into_body() + .into_stream() + .expect("stream"); + + block_on(async { + assert_eq!(body.next().await.expect("first").expect("chunk"), "ab"); + assert_eq!(body.next().await.expect("second").expect("chunk"), "cd"); + assert!(body.next().await.is_none()); + }); + } + + #[test] + fn head_suppression_releases_stream_without_polling_and_preserves_length() { + struct DropSignal(Rc>); + + #[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" + )] + impl Drop for DropSignal { + fn drop(&mut self) { + self.0.set(self.0.get() + 1); + } + } + + let polls = Rc::new(Cell::new(0_usize)); + let drops = Rc::new(Cell::new(0_usize)); + let observed_polls = Rc::clone(&polls); + let signal = DropSignal(Rc::clone(&drops)); + let body = Body::from_stream(poll_fn(move |_| { + let _keep_signal_alive = &signal; + observed_polls.set(observed_polls.get() + 1_usize); + Poll::Ready(Some(Ok(Bytes::from_static(b"body")))) + })); + let response = response_builder() + .status(StatusCode::OK) + .header("content-length", "99") + .body(body) + .expect("response"); + + let prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::HEAD, + response, + ) + .expect("prepared response"); + + assert!(!prepared.transmits_body()); + assert_eq!(prepared.declared_length(), Some(99)); + assert_eq!(polls.get(), 0_usize); + assert_eq!(drops.get(), 1_usize); + assert_eq!( + prepared + .into_response() + .body() + .as_bytes() + .expect("suppressed body"), + b"" + ); + } + + #[test] + fn declared_stream_length_rejects_overrun_and_early_eof() { + let overrun = response_builder() + .status(StatusCode::OK) + .header("content-length", "3") + .body(Body::from_stream(stream::iter([ + Ok(Bytes::from_static(b"ab")), + Ok(Bytes::from_static(b"cd")), + ]))) + .expect("response"); + let mut overrun_stream = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + overrun, + ) + .expect("prepared response") + .into_response() + .into_body() + .into_stream() + .expect("stream body"); + block_on(async { + assert_eq!( + overrun_stream + .next() + .await + .expect("first item") + .expect("first chunk"), + Bytes::from_static(b"ab") + ); + let error = overrun_stream + .next() + .await + .expect("overrun item") + .expect_err("overrun error"); + assert_eq!( + response_egress_body_length_error(&error), + Some(ResponseEgressBodyLengthError::Exceeded) + ); + assert!(overrun_stream.next().await.is_none()); + }); + + let early_eof = response_builder() + .status(StatusCode::OK) + .header("content-length", "3") + .body(Body::from_stream(stream::iter([Ok(Bytes::from_static( + b"ab", + ))]))) + .expect("response"); + let mut early_eof_stream = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + early_eof, + ) + .expect("prepared response") + .into_response() + .into_body() + .into_stream() + .expect("stream body"); + block_on(async { + assert_eq!( + early_eof_stream + .next() + .await + .expect("chunk") + .expect("valid first chunk"), + Bytes::from_static(b"ab") + ); + let error = early_eof_stream + .next() + .await + .expect("early EOF item") + .expect_err("early EOF error"); + assert_eq!( + response_egress_body_length_error(&error), + Some(ResponseEgressBodyLengthError::Incomplete) + ); + assert!(early_eof_stream.next().await.is_none()); + }); + } + + #[test] + fn terminal_stream_error_releases_source_before_yield() { + let overrun_drops = Rc::new(Cell::new(0_usize)); + let overrun = response_builder() + .status(StatusCode::OK) + .header("content-length", "1") + .body(tracked_body( + vec![Ok(Bytes::from_static(b"over"))], + &overrun_drops, + )) + .expect("response"); + let mut overrun_stream = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + overrun, + ) + .expect("prepared response") + .into_response() + .into_body() + .into_stream() + .expect("stream body"); + block_on(overrun_stream.next()) + .expect("overrun result") + .expect_err("overrun error"); + assert_eq!(overrun_drops.get(), 1_usize); + + let source_drops = Rc::new(Cell::new(0_usize)); + let source_error = response_builder() + .status(StatusCode::OK) + .header("content-length", "1") + .body(tracked_body( + vec![Err(EdgeError::bad_gateway("source failed"))], + &source_drops, + )) + .expect("response"); + let mut source_stream = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + source_error, + ) + .expect("prepared response") + .into_response() + .into_body() + .into_stream() + .expect("stream body"); + block_on(source_stream.next()) + .expect("source result") + .expect_err("source error"); + assert_eq!(source_drops.get(), 1_usize); + + let eof_drops = Rc::new(Cell::new(0_usize)); + let early_eof = response_builder() + .status(StatusCode::OK) + .header("content-length", "1") + .body(tracked_body(Vec::new(), &eof_drops)) + .expect("response"); + let mut eof_stream = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + early_eof, + ) + .expect("prepared response") + .into_response() + .into_body() + .into_stream() + .expect("stream body"); + block_on(eof_stream.next()) + .expect("early EOF result") + .expect_err("early EOF error"); + assert_eq!(eof_drops.get(), 1_usize); + } + + #[test] + fn rejects_application_trailer_declaration_before_commit() { + let response = response_builder() + .status(StatusCode::OK) + .header("trailer", "x-checksum") + .body(Body::from("body")) + .expect("response"); + + let error = prepare_error(&Method::GET, response, "trailers must be rejected"); + assert_eq!( + error, + super::ResponseEgressFramingError::UnsupportedTrailers + ); + } + + #[test] + fn rejects_every_invalid_content_length_shape_before_suppression() { + for value in ["", "+1", "-1", "1a", "1,,1", "1, 2", "18446744073709551616"] { + let response = response_builder() + .status(StatusCode::NO_CONTENT) + .header("content-length", value) + .body(Body::empty()) + .expect("response"); + let error = prepare_error( + &Method::GET, + response, + "invalid content-length was accepted", + ); + assert_eq!(error, ResponseEgressFramingError::InvalidContentLength); + } + } + + #[test] + fn applies_status_suppression_before_method_suppression() { + let no_content = response_builder() + .status(StatusCode::NO_CONTENT) + .header("content-length", "9") + .body(Body::from("ignored")) + .expect("response"); + let no_content_prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::HEAD, + no_content, + ) + .expect("204 response"); + assert!(!no_content_prepared.transmits_body()); + assert_eq!(no_content_prepared.declared_length(), None); + assert!( + !no_content_prepared + .into_response() + .headers() + .contains_key("content-length") + ); + + let reset_content = response_builder() + .status(StatusCode::RESET_CONTENT) + .header("content-length", "1") + .body(Body::empty()) + .expect("response"); + let reset_error = prepare_error( + &Method::GET, + reset_content, + "205 with nonzero length must fail", + ); + assert_eq!( + reset_error, + ResponseEgressFramingError::ContentLengthMismatch + ); + + let not_modified = response_builder() + .status(StatusCode::NOT_MODIFIED) + .header("content-length", "9") + .body(Body::from("ignored")) + .expect("response"); + let not_modified_prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + not_modified, + ) + .expect("304 response"); + assert!(!not_modified_prepared.transmits_body()); + assert_eq!(not_modified_prepared.declared_length(), Some(9)); + + let zero_reset_content = response_builder() + .status(StatusCode::RESET_CONTENT) + .header("content-length", "0") + .body(Body::empty()) + .expect("response"); + let zero_reset_prepared = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + zero_reset_content, + ) + .expect("zero-length 205"); + assert!(!zero_reset_prepared.transmits_body()); + assert_eq!(zero_reset_prepared.declared_length(), Some(0)); + + let nominated_nonzero = response_builder() + .status(StatusCode::RESET_CONTENT) + .header("connection", "content-length") + .header("content-length", "1") + .body(Body::empty()) + .expect("response"); + let nominated_error = prepare_error( + &Method::GET, + nominated_nonzero, + "205 validation must precede connection nomination stripping", + ); + assert_eq!( + nominated_error, + ResponseEgressFramingError::ContentLengthMismatch + ); + } + + #[test] + fn rejects_tunnel_and_informational_responses_and_removes_transfer_encoding() { + for status in [StatusCode::CONTINUE, StatusCode::SWITCHING_PROTOCOLS] { + let informational = response_builder() + .status(status) + .body(Body::empty()) + .expect("response"); + let informational_error = prepare_error( + &Method::GET, + informational, + "final informational response must fail", + ); + assert_eq!( + informational_error, + ResponseEgressFramingError::UnsupportedStatus + ); + } + + let tunnel = response_builder() + .status(StatusCode::OK) + .body(Body::empty()) + .expect("response"); + let tunnel_error = prepare_error(&Method::CONNECT, tunnel, "successful CONNECT must fail"); + assert_eq!(tunnel_error, ResponseEgressFramingError::UnsupportedTunnel); + + let transfer_encoded = response_builder() + .status(StatusCode::OK) + .header("transfer-encoding", "chunked") + .body(Body::from("body")) + .expect("response"); + let transfer_response = prepare_response_egress( + ResponseEgressFramingMode::ExplicitBufferedLength, + &Method::GET, + transfer_encoded, + ) + .expect("response") + .into_response(); + assert!( + !transfer_response + .headers() + .contains_key("transfer-encoding") + ); + + let informational_with_bad_connection = response_builder() + .status(StatusCode::CONTINUE) + .header("connection", "not a valid field name") + .header("trailer", "x-checksum") + .body(Body::empty()) + .expect("response"); + let informational_precedence_error = prepare_error( + &Method::GET, + informational_with_bad_connection, + "status rejection must win", + ); + assert_eq!( + informational_precedence_error, + ResponseEgressFramingError::UnsupportedStatus + ); + + let tunnel_with_trailer = response_builder() + .status(StatusCode::OK) + .header("trailer", "x-checksum") + .body(Body::empty()) + .expect("response"); + let tunnel_precedence_error = prepare_error( + &Method::CONNECT, + tunnel_with_trailer, + "tunnel rejection must win", + ); + assert_eq!( + tunnel_precedence_error, + ResponseEgressFramingError::UnsupportedTunnel + ); + } + + #[test] + fn source_error_diagnostics_are_typed_and_never_include_private_messages() { + let cases = [ + ( + EdgeError::internal(ResponseEgressBodyLengthError::Exceeded), + "declared_length_exceeded", + ), + ( + EdgeError::bad_gateway_with_reason( + "private token=secret", + BadGatewayReason::Decode(BadGatewayDecodeReason::Brotli), + ), + "bad_gateway_decode_brotli", + ), + ( + EdgeError::bad_gateway_with_reason( + "private token=secret", + BadGatewayReason::Decode(BadGatewayDecodeReason::Deflate), + ), + "bad_gateway_decode_deflate", + ), + ( + EdgeError::internal(anyhow::anyhow!("private token=secret")), + "internal", + ), + ]; + + for (error, expected) in cases { + let category = response_egress_source_error_category(&error); + assert_eq!(category, expected); + assert!(!category.contains("private")); + assert!(!category.contains("secret")); + } + } +} diff --git a/crates/edgezero-core/src/response_egress_framing_test_support.rs b/crates/edgezero-core/src/response_egress_framing_test_support.rs new file mode 100644 index 000000000..d113dda36 --- /dev/null +++ b/crates/edgezero-core/src/response_egress_framing_test_support.rs @@ -0,0 +1,251 @@ +use std::sync::{Arc, Mutex, PoisonError}; +use std::time::Duration; + +use super::ResponseEgressFramingError; +use crate::action; +use crate::app::App; +use crate::body::Body; +use crate::error::EdgeError; +use crate::extractor::State; +use crate::http::{ + HeaderMap, HeaderValue, Method, Request, Response, StatusCode, response_builder, +}; +use crate::ingress::{ + AdmissionDecision, IngressBeginOutcome, IngressDispatchOutcome, IngressFraming, IngressGrant, + IngressHeadAccounting, IngressHeadParts, +}; +use crate::response_egress::{ + ResponseEgressCompletion, ResponseEgressEnvelope, ResponseEgressObserver, ResponseEgressReport, +}; +use crate::router::RouterService; +use crate::time::{MonotonicClock, MonotonicInstant}; + +pub const FRAMING_RULES: [ResponseEgressFramingError; 6] = [ + ResponseEgressFramingError::ContentLengthMismatch, + ResponseEgressFramingError::InvalidConnectionHeader, + ResponseEgressFramingError::InvalidContentLength, + ResponseEgressFramingError::UnsupportedStatus, + ResponseEgressFramingError::UnsupportedTrailers, + ResponseEgressFramingError::UnsupportedTunnel, +]; + +type Reports = Arc>>; + +pub struct FramingFixture { + pub completed: Reports, + pub envelope: ResponseEgressEnvelope, + pub observed: Reports, +} + +struct Observer(Reports); + +impl ResponseEgressObserver for Observer { + #[inline] + fn complete(&self, report: &ResponseEgressReport) { + self.0 + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push(report.clone()); + } +} + +/// Constructs a malformed framing fixture. +/// +/// # Errors +/// Reserved for fixture-construction errors. +#[inline] +pub fn framing_response( + reason: ResponseEgressFramingError, +) -> Result<(Method, Response), EdgeError> { + let mut response = response_builder() + .body(Body::from("x")) + .map_err(EdgeError::internal)?; + let mut method = Method::GET; + match reason { + ResponseEgressFramingError::ContentLengthMismatch => { + response + .headers_mut() + .insert("content-length", HeaderValue::from_static("2")); + } + ResponseEgressFramingError::InvalidConnectionHeader => { + response.headers_mut().insert( + "connection", + HeaderValue::from_static("private token=secret"), + ); + } + ResponseEgressFramingError::InvalidContentLength => { + response + .headers_mut() + .insert("content-length", HeaderValue::from_static("token=secret")); + } + ResponseEgressFramingError::UnsupportedStatus => { + *response.status_mut() = StatusCode::SWITCHING_PROTOCOLS; + } + ResponseEgressFramingError::UnsupportedTrailers => { + response + .headers_mut() + .insert("trailer", HeaderValue::from_static("private-token-secret")); + } + ResponseEgressFramingError::UnsupportedTunnel => method = Method::CONNECT, + } + Ok((method, response)) +} + +#[action] +async fn invalid_response( + State(reason): State, +) -> Result { + Ok(framing_response(reason)?.1) +} + +/// Dispatches the same invalid response through a routed handler or admission refusal. +/// +/// # Errors +/// Returns the unchanged framework dispatch error if the fixture cannot produce an envelope. +#[inline] +pub async fn framing_fixture( + reason: ResponseEgressFramingError, + detached: bool, +) -> Result { + let now = MonotonicInstant::now(); + let (method, fixture_response) = framing_response(reason)?; + let fixture_status = fixture_response.status(); + let fixture_version = fixture_response.version(); + let fixture_headers = fixture_response.headers().clone(); + let completed = Reports::default(); + let observed = Reports::default(); + let mut app = App::new( + RouterService::builder() + .with_state(reason) + .route("/", method.clone(), invalid_response) + .build(), + ); + app.set_monotonic_clock(MonotonicClock::new(move || now)); + let mut headers = HeaderMap::new(); + headers.insert( + "x-synthesized-fixture", + HeaderValue::from_static("configured"), + ); + for _ in 0..30_u8 { + headers.append("x-padding", HeaderValue::from_static("x")); + } + let used = headers.iter().try_fold(2_usize, |sum, (name, value)| { + sum.checked_add(name.as_str().len()) + .and_then(|total| total.checked_add(value.as_bytes().len())) + .and_then(|total| total.checked_add(4)) + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("fixture header charge overflow"))) + })?; + let padding = 4096_usize + .checked_sub(used) + .and_then(|remaining| remaining.checked_sub("x-padding".len())) + .and_then(|remaining| remaining.checked_sub(4)) + .ok_or_else(|| EdgeError::internal(anyhow::anyhow!("fixture header padding overflow")))?; + headers.append( + "x-padding", + HeaderValue::from_str(&"a".repeat(padding)).map_err(EdgeError::internal)?, + ); + app.set_synthesized_response_headers(headers) + .map_err(EdgeError::internal)?; + app.set_response_egress_observer(Observer(Arc::clone(&observed))); + let completed_reports = Arc::clone(&completed); + app.set_ingress_admission_policy(move |head| { + let retained_reports = Arc::clone(&completed_reports); + let completion = ResponseEgressCompletion::new(move |report| { + retained_reports + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push(report.clone()); + }); + if detached { + let mut response = Response::new(Body::from("x")); + *response.status_mut() = fixture_status; + *response.version_mut() = fixture_version; + *response.headers_mut() = fixture_headers.clone(); + AdmissionDecision::Refuse { + completion, + response, + } + } else { + AdmissionDecision::Admit { + completion, + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + } + } + }); + let mut request = Request::new(Body::empty()); + *request.method_mut() = method; + let outcome = app + .dispatch_ingress( + request, + now, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ) + .await?; + let IngressDispatchOutcome::Response(envelope) = outcome else { + return Err(EdgeError::internal(anyhow::anyhow!( + "framing fixture did not produce a response" + ))); + }; + Ok(FramingFixture { + completed, + envelope: *envelope, + observed, + }) +} + +/// Makes a typed source error after admission with competing renderer/configured retry fields. +/// +/// # Errors +/// Returns the original construction/admission failure if the fixture cannot produce an envelope. +#[inline] +pub fn synthesized_error_fixture(error: EdgeError) -> Result { + let app_router = RouterService::builder().build(); + let mut app = App::new(app_router); + let mut headers = HeaderMap::new(); + headers.insert( + "x-synthesized-fixture", + HeaderValue::from_static("configured"), + ); + headers.insert("retry-after", HeaderValue::from_static("180")); + app.set_synthesized_response_headers(headers) + .map_err(EdgeError::internal)?; + app.set_error_response_renderer(|_| { + let mut response = Response::new(Body::from("fixed framework error")); + response + .headers_mut() + .insert("retry-after", HeaderValue::from_static("240")); + response + }); + let request = Request::new(Body::empty()); + let head = IngressHeadParts::from_request( + &request, + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ); + let IngressBeginOutcome::Admitted(prepared) = app.begin_ingress(head, app.monotonic_now())? + else { + return Err(EdgeError::internal(anyhow::anyhow!( + "source-error fixture was not admitted" + ))); + }; + Ok(app.admitted_error_egress(prepared, error)) +} + +/// Verifies every configured value in the maximum-size fixture, ignoring converter-owned fields. +#[must_use] +#[inline] +pub fn has_full_synthesized_fixture_headers(headers: &HeaderMap) -> bool { + let padding: Vec<_> = headers.get_all("x-padding").iter().collect(); + headers.get("x-synthesized-fixture") == Some(&HeaderValue::from_static("configured")) + && headers.get_all("x-synthesized-fixture").iter().count() == 1 + && padding.len() == 31 + && padding + .iter() + .take(30) + .all(|value| value.as_bytes() == b"x") + && padding.last().is_some_and(|value| { + value.as_bytes().len() == 3626 && value.as_bytes().iter().all(|byte| *byte == b'a') + }) +} diff --git a/crates/edgezero-core/src/response_headers.rs b/crates/edgezero-core/src/response_headers.rs new file mode 100644 index 000000000..f3f26a172 --- /dev/null +++ b/crates/edgezero-core/src/response_headers.rs @@ -0,0 +1,274 @@ +use std::fmt; +use std::sync::Arc; + +use crate::http::{HeaderMap, HeaderName, HeaderValue}; + +/// Maximum synthesized field-section bytes: final CRLF plus name/value/`: `/CRLF per value. +pub const MAX_SYNTHESIZED_RESPONSE_HEADER_BYTES: usize = 4096; +/// Maximum synthesized field values, counting repeated names separately. +pub const MAX_SYNTHESIZED_RESPONSE_HEADER_COUNT: usize = 32; + +/// Immutable, prevalidated decoration for framework-generated responses, not routed responses. +/// +/// Values must be visible ASCII or interior HTAB, without leading/trailing whitespace. This +/// portable subset avoids provider conversion failures or host trimming during fallback delivery. +/// Clones share the bounded field map; debugging never exposes configured values. +#[derive(Clone, Default)] +pub struct SynthesizedResponseHeaders { + fields: Option>, +} + +/// Payload-free construction failure; invalid sets never replace an installed valid snapshot. +#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] +#[non_exhaustive] +pub enum SynthesizedResponseHeadersError { + #[error("synthesized response headers contain a forbidden field")] + ForbiddenName, + #[error("synthesized response headers contain a nonportable value")] + NonPortableValue, + #[error("synthesized response headers exceed the byte limit")] + TooManyBytes, + #[error("synthesized response headers exceed the field limit")] + TooManyFields, +} + +impl fmt::Debug for SynthesizedResponseHeaders { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SynthesizedResponseHeaders") + .field( + "field_count", + &self.fields.as_ref().map_or(0, |fields| fields.len()), + ) + .finish_non_exhaustive() + } +} + +impl SynthesizedResponseHeaders { + /// Adds absent names without replacing a response-owned name or losing repeated values. + #[doc(hidden)] + #[inline] + pub fn apply(&self, target: &mut HeaderMap) { + let Some(fields) = &self.fields else { + return; + }; + for name in fields.keys() { + if !target.contains_key(name) { + for value in fields.get_all(name) { + target.append(name.clone(), value.clone()); + } + } + } + } + + /// Validates a complete replacement before it can be installed on an app or request. + /// + /// # Errors + /// Returns a fixed category for forbidden framing/coding fields, nonportable values, or + /// either inclusive bound. Field names are already syntax-validated by `HeaderMap`. + #[inline] + pub fn new(fields: HeaderMap) -> Result { + if fields.is_empty() { + return Ok(Self::default()); + } + if fields.len() > MAX_SYNTHESIZED_RESPONSE_HEADER_COUNT { + return Err(SynthesizedResponseHeadersError::TooManyFields); + } + let mut bytes = 2_usize; + for (name, value) in &fields { + if matches!( + name.as_str(), + "content-length" + | "transfer-encoding" + | "trailer" + | "te" + | "connection" + | "keep-alive" + | "proxy-connection" + | "upgrade" + | "proxy-authenticate" + | "proxy-authorization" + | "content-type" + | "content-encoding" + ) { + return Err(SynthesizedResponseHeadersError::ForbiddenName); + } + let value_bytes = value.as_bytes(); + if value_bytes + .iter() + .any(|byte| !matches!(*byte, b'\t' | b' '..=b'~')) + || value_bytes + .first() + .is_some_and(|byte| matches!(*byte, b' ' | b'\t')) + || value_bytes + .last() + .is_some_and(|byte| matches!(*byte, b' ' | b'\t')) + { + return Err(SynthesizedResponseHeadersError::NonPortableValue); + } + bytes = bytes + .checked_add(name.as_str().len()) + .and_then(|sum| sum.checked_add(value_bytes.len())) + .and_then(|sum| sum.checked_add(4)) + .filter(|sum| *sum <= MAX_SYNTHESIZED_RESPONSE_HEADER_BYTES) + .ok_or(SynthesizedResponseHeadersError::TooManyBytes)?; + } + // Detach caller-owned spare capacity and shared backing allocations before retention. + let mut compact = HeaderMap::with_capacity(fields.len()); + for (name, value) in &fields { + let copied_name = HeaderName::from_bytes(name.as_str().as_bytes()) + .map_err(|_invalid_name| SynthesizedResponseHeadersError::ForbiddenName)?; + let mut copied_value = HeaderValue::from_bytes(value.as_bytes()) + .map_err(|_invalid_value| SynthesizedResponseHeadersError::NonPortableValue)?; + copied_value.set_sensitive(value.is_sensitive()); + compact.append(copied_name, copied_value); + } + drop(fields); + Ok(Self { + fields: Some(Arc::new(compact)), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::{HeaderMap, HeaderName, HeaderValue}; + + #[test] + fn synthesized_header_bounds_count_every_value_and_wire_delimiter() { + for count in [31_usize, 32, 33] { + let mut headers = HeaderMap::new(); + for _ in 0..count { + headers.append("x", HeaderValue::from_static("value")); + } + let result = SynthesizedResponseHeaders::new(headers); + if count > 32 { + assert_eq!( + result.err(), + Some(SynthesizedResponseHeadersError::TooManyFields) + ); + } else { + result.expect("within count limit"); + } + } + for bytes in [4095, 4096, 4097] { + let mut headers = HeaderMap::new(); + headers.insert( + "x", + HeaderValue::from_str(&"a".repeat(bytes - 7)).expect("value"), + ); + let result = SynthesizedResponseHeaders::new(headers); + if bytes > 4096 { + assert_eq!( + result.err(), + Some(SynthesizedResponseHeadersError::TooManyBytes) + ); + } else { + result.expect("within byte limit"); + } + } + for bytes in [4095_usize, 4096, 4097] { + let mut headers = HeaderMap::new(); + headers.append("longer-name", HeaderValue::from_static("first")); + headers.append( + "longer-name", + HeaderValue::from_str(&"a".repeat(bytes - 37)).expect("value"), + ); + let result = SynthesizedResponseHeaders::new(headers); + if bytes > 4096 { + assert_eq!( + result.err(), + Some(SynthesizedResponseHeadersError::TooManyBytes) + ); + } else { + result.expect("within aggregate byte limit"); + } + } + } + + #[test] + fn synthesized_headers_compact_retained_storage_and_accept_portable_values() { + let mut fields = HeaderMap::with_capacity(10000); + fields.insert("x-empty", HeaderValue::from_static("")); + fields.insert("x-tab", HeaderValue::from_static("a\tb")); + let set = SynthesizedResponseHeaders::new(fields).expect("portable values"); + assert!(set.fields.as_ref().expect("nonempty set").capacity() < 100); + let mut output = HeaderMap::new(); + set.apply(&mut output); + assert_eq!(output["x-empty"], ""); + assert_eq!(output["x-tab"], "a\tb"); + } + + #[test] + fn synthesized_headers_reject_framing_and_nonportable_values() { + for name in [ + "Content-Length", + "Transfer-Encoding", + "Trailer", + "TE", + "Connection", + "Keep-Alive", + "Proxy-Connection", + "Upgrade", + "Proxy-Authenticate", + "Proxy-Authorization", + "Content-Type", + "Content-Encoding", + ] { + let mut headers = HeaderMap::new(); + headers.insert( + HeaderName::from_bytes(name.as_bytes()).expect("name"), + HeaderValue::from_static("x"), + ); + assert_eq!( + SynthesizedResponseHeaders::new(headers).err(), + Some(SynthesizedResponseHeadersError::ForbiddenName) + ); + } + for value in [b"\x80".as_slice(), b" leading", b"trailing\t"] { + let mut headers = HeaderMap::new(); + headers.insert( + "x", + HeaderValue::from_bytes(value).expect("syntactically valid field"), + ); + assert_eq!( + SynthesizedResponseHeaders::new(headers).err(), + Some(SynthesizedResponseHeadersError::NonPortableValue) + ); + } + for value in [b"\x7f".as_slice(), b"\r", b"\n", b"\0"] { + HeaderValue::from_bytes(value).expect_err("invalid field bytes"); + } + } + + #[test] + fn synthesized_headers_preserve_existing_names_and_repeated_value_order() { + let mut configured = HeaderMap::new(); + configured.append("set-cookie", HeaderValue::from_static("first=1")); + configured.append("set-cookie", HeaderValue::from_static("second=2")); + configured.insert("allow", HeaderValue::from_static("POST")); + configured.insert("cache-control", HeaderValue::from_static("public")); + let set = SynthesizedResponseHeaders::new(configured).expect("validated"); + let mut target = HeaderMap::new(); + target.insert("allow", HeaderValue::from_static("GET")); + target.insert("cache-control", HeaderValue::from_static("private")); + set.apply(&mut target); + assert_eq!(target["allow"], "GET"); + assert_eq!(target["cache-control"], "private"); + assert_eq!(target.get_all("allow").iter().count(), 1); + assert_eq!(target.get_all("cache-control").iter().count(), 1); + assert_eq!( + target.get_all("set-cookie").iter().collect::>(), + [ + &HeaderValue::from_static("first=1"), + &HeaderValue::from_static("second=2") + ] + ); + set.apply(&mut target); + assert_eq!(target.get_all("set-cookie").iter().count(), 2); + assert_eq!(target.get_all("allow").iter().count(), 1); + assert_eq!(target.get_all("cache-control").iter().count(), 1); + assert!(!format!("{set:?}").contains("first=1")); + } +} diff --git a/crates/edgezero-core/src/router.rs b/crates/edgezero-core/src/router.rs index ae77619af..ac2ded849 100644 --- a/crates/edgezero-core/src/router.rs +++ b/crates/edgezero-core/src/router.rs @@ -1,14 +1,15 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::sync::Arc; use std::task::{Context, Poll}; use matchit::Router as PathRouter; use tower_service::Service; -use crate::context::RequestContext; +use crate::context::{FallbackDrainOutcome, RequestContext, drain_body_discard}; use crate::error::EdgeError; use crate::handler::{BoxHandler, IntoHandler, IntrospectionNeeds}; use crate::http::{Extensions, HandlerFuture, Method, Request, Response}; +use crate::ingress::AdmittedIngress; use crate::introspection::{ManifestJson, RouteTable}; use crate::middleware::{BoxMiddleware, Middleware, Next}; use crate::params::PathParams; @@ -17,6 +18,7 @@ use crate::response::IntoResponse as _; struct RouteEntry { handler: BoxHandler, introspection_needs: IntrospectionNeeds, + metadata: RouteMetadata, } impl Clone for RouteEntry { @@ -24,49 +26,155 @@ impl Clone for RouteEntry { Self { handler: Arc::clone(&self.handler), introspection_needs: self.introspection_needs, + metadata: self.metadata.clone(), } } fn clone_from(&mut self, source: &Self) { self.handler = Arc::clone(&source.handler); self.introspection_needs = source.introspection_needs; + self.metadata.clone_from(&source.metadata); } } -#[derive(Clone, Debug)] -pub struct RouteInfo { +/// Stable identity for one registered route. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct RouteId { method: Method, - path: String, + pattern: String, } -impl RouteInfo { +impl RouteId { #[must_use] #[inline] pub fn method(&self) -> &Method { &self.method } - #[inline] - pub fn new(method: Method, path: S) -> Self + fn new(method: Method, pattern: S) -> Self where S: Into, { Self { method, - path: path.into(), + pattern: pattern.into(), + } + } + + #[must_use] + #[inline] + pub fn pattern(&self) -> &str { + &self.pattern + } +} + +/// Canonical metadata for one registered route. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RouteMetadata { + class: Option>, + id: RouteId, +} + +impl RouteMetadata { + /// Opaque manifest-sourced route class used by admission policy. + #[must_use] + #[inline] + pub fn class(&self) -> Option<&str> { + self.class.as_deref() + } + + #[must_use] + #[inline] + pub fn id(&self) -> &RouteId { + &self.id + } + + #[must_use] + #[inline] + pub fn method(&self) -> &Method { + self.id.method() + } + + #[inline] + pub fn new(method: Method, pattern: S) -> Self + where + S: Into, + { + Self { + class: None, + id: RouteId::new(method, pattern), + } + } + + fn new_with_class(method: Method, pattern: S, class: C) -> Self + where + C: Into>, + S: Into, + { + Self { + class: Some(class.into()), + id: RouteId::new(method, pattern), } } + /// Registered route pattern. Preserved for existing route-introspection consumers. #[must_use] #[inline] pub fn path(&self) -> &str { - &self.path + self.pattern() + } + + #[must_use] + #[inline] + pub fn pattern(&self) -> &str { + self.id.pattern() + } +} + +/// Backward-compatible name for route-table entries; the value is canonical route metadata. +pub type RouteInfo = RouteMetadata; + +/// Stable result exposed to ingress admission before route dispatch. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum RouteResolution { + Matched(RouteMetadata), + MethodNotAllowed { allowed: Arc<[RouteMetadata]> }, + NotFound, +} + +enum ResolvedTarget { + Found(RouteEntry, PathParams), + MethodNotAllowed(Vec), + NotFound, +} + +/// Opaque route-match token bound to the router that created it. +pub struct ResolvedDispatch { + method: Method, + owner: Arc, + path: String, + resolution: RouteResolution, + target: ResolvedTarget, +} + +impl ResolvedDispatch { + #[must_use] + #[inline] + pub(crate) fn method(&self) -> &Method { + &self.method + } + + #[must_use] + #[inline] + pub fn resolution(&self) -> &RouteResolution { + &self.resolution } } enum RouteMatch<'route> { Found(&'route RouteEntry, PathParams), - MethodNotAllowed(Vec), + MethodNotAllowed(Vec), NotFound, } @@ -86,11 +194,15 @@ impl RouterBuilder { clippy::panic, reason = "duplicate route is a build-time programmer error, not a runtime condition" )] - fn add_route(&mut self, path: &str, method: Method, handler: H) + fn add_route(&mut self, path: &str, method: Method, class: Option>, handler: H) where H: IntoHandler, { - let router = self.routes.entry(method.clone()).or_default(); + let metadata = class.map_or_else( + || RouteMetadata::new(method.clone(), path), + |route_class| RouteMetadata::new_with_class(method.clone(), path, route_class), + ); + let router = self.routes.entry(method).or_default(); // The handler reports which introspection payloads its route needs; the // flag is read once here and consulted per request in `dispatch`. @@ -103,12 +215,12 @@ impl RouterBuilder { RouteEntry { handler: boxed, introspection_needs, + metadata: metadata.clone(), }, ) .unwrap_or_else(|err| panic!("duplicate route definition for {path}: {err}")); - self.route_info - .push(RouteInfo::new(method, path.to_owned())); + self.route_info.push(metadata); } #[must_use] @@ -190,7 +302,25 @@ impl RouterBuilder { where H: IntoHandler, { - self.add_route(path, method, handler); + self.add_route(path, method, None, handler); + self + } + + /// Registers a route with opaque admission metadata from the manifest. + #[must_use] + #[inline] + pub fn route_with_class( + mut self, + path: &str, + method: Method, + class: C, + handler: H, + ) -> Self + where + C: Into>, + H: IntoHandler, + { + self.add_route(path, method, Some(class.into()), handler); self } @@ -233,45 +363,62 @@ struct RouterInner { } impl RouterInner { - async fn dispatch(&self, mut request: Request) -> Result { + async fn dispatch(&self, request: Request) -> Result { let method = request.method().clone(); let path = request.uri().path().to_owned(); match self.find_route(&method, &path) { RouteMatch::Found(entry, params) => { - // Inject only the introspection payloads this route asked for — - // nothing for the vast majority of routes that need none. - let needs = entry.introspection_needs; - if needs.manifest - && let Some(json) = &self.manifest_json - { - request - .extensions_mut() - .insert(ManifestJson(Arc::clone(json))); - } - if needs.routes { - request - .extensions_mut() - .insert(RouteTable(Arc::clone(&self.route_index))); - } - // App-owned state registered via RouterBuilder::with_state. - // Runs after introspection inserts; `extend` overwrites by - // TypeId, so app state wins last-write on any collision. - request - .extensions_mut() - .extend(self.state_extensions.clone()); - let ctx = RequestContext::new(request, params); - let next = Next::new(&self.middlewares, entry.handler.as_ref()); - next.run(ctx).await + self.dispatch_found(request, entry, params, None).await } - RouteMatch::MethodNotAllowed(mut allowed) => { - allowed.sort_by(|left, right| left.as_str().cmp(right.as_str())); - Err(EdgeError::method_not_allowed(&method, &allowed)) + RouteMatch::MethodNotAllowed(allowed) => { + let methods = allowed + .iter() + .map(|metadata| metadata.method().clone()) + .collect::>(); + Err(EdgeError::method_not_allowed(&method, &methods)) } RouteMatch::NotFound => Err(EdgeError::not_found(path)), } } + async fn dispatch_found( + &self, + mut request: Request, + entry: &RouteEntry, + params: PathParams, + ingress: Option, + ) -> Result { + // Inject only the introspection payloads this route asked for — + // nothing for the vast majority of routes that need none. + let needs = entry.introspection_needs; + if needs.manifest + && let Some(json) = &self.manifest_json + { + request + .extensions_mut() + .insert(ManifestJson(Arc::clone(json))); + } + if needs.routes { + request + .extensions_mut() + .insert(RouteTable(Arc::clone(&self.route_index))); + } + // App-owned state registered via RouterBuilder::with_state. + // Runs after introspection inserts; `extend` overwrites by TypeId, so app state wins. + request + .extensions_mut() + .extend(self.state_extensions.clone()); + let ctx = match ingress { + Some(admitted) => { + RequestContext::new_routed(request, params, entry.metadata.clone(), admitted) + } + None => RequestContext::new(request, params), + }; + let next = Next::new(&self.middlewares, entry.handler.as_ref()); + next.run(ctx).await + } + fn find_route(&self, method: &Method, path: &str) -> RouteMatch<'_> { if let Some(router) = self.routes.get(method) && let Ok(matched) = router.at(path) @@ -286,17 +433,24 @@ impl RouterInner { return RouteMatch::Found(matched.value, params); } - let allowed: HashSet = self + let mut allowed: Vec = self .routes - .iter() - .filter(|(_, router)| router.at(path).is_ok()) - .map(|(candidate_method, _)| candidate_method.clone()) + .values() + .filter_map(|router| router.at(path).ok()) + .map(|matched| matched.value.metadata.clone()) .collect(); + allowed.sort_by(|left, right| { + left.method() + .as_str() + .cmp(right.method().as_str()) + .then_with(|| left.pattern().cmp(right.pattern())) + }); + if allowed.is_empty() { RouteMatch::NotFound } else { - RouteMatch::MethodNotAllowed(allowed.into_iter().collect()) + RouteMatch::MethodNotAllowed(allowed) } } } @@ -330,6 +484,59 @@ impl RouterService { RouterBuilder::new() } + /// Dispatches the exact route selected before ingress admission without rematching. + /// + /// # Errors + /// Returns [`EdgeError::Internal`] if the token belongs to another router or the request + /// method/path changed after resolution. Normal handler and routing errors are preserved. + #[inline] + pub async fn dispatch_resolved( + &self, + resolved: ResolvedDispatch, + request: Request, + ingress: AdmittedIngress, + ) -> Result { + if !Arc::ptr_eq(&self.inner, &resolved.owner) { + return Err(EdgeError::internal(anyhow::anyhow!( + "resolved dispatch token belongs to another router" + ))); + } + if request.method() != resolved.method || request.uri().path() != resolved.path { + return Err(EdgeError::internal(anyhow::anyhow!( + "request method or path changed after ingress route resolution" + ))); + } + + match resolved.target { + ResolvedTarget::Found(entry, params) => { + if ingress.is_fallback() { + return Err(EdgeError::internal(anyhow::anyhow!( + "fallback body policy cannot dispatch a matched route" + ))); + } + self.inner + .dispatch_found(request, &entry, params, Some(ingress)) + .await + } + ResolvedTarget::MethodNotAllowed(allowed) => { + if let Some(response) = fallback_terminal_response(request, ingress).await? { + return Ok(response); + } + let methods = allowed + .iter() + .map(|metadata| metadata.method().clone()) + .collect::>(); + Err(EdgeError::method_not_allowed(&resolved.method, &methods)) + } + ResolvedTarget::NotFound => { + if let Some(response) = fallback_terminal_response(request, ingress).await? { + return Ok(response); + } + Err(EdgeError::not_found(resolved.path)) + } + } + } + fn new( routes: HashMap>, middlewares: Vec, @@ -360,11 +567,73 @@ impl RouterService { } } + /// Resolves a route without invoking middleware, handlers, or request-body code. + #[must_use] + #[inline] + pub fn resolve(&self, method: &Method, path: &str) -> ResolvedDispatch { + let (resolution, target) = match self.inner.find_route(method, path) { + RouteMatch::Found(entry, params) => ( + RouteResolution::Matched(entry.metadata.clone()), + ResolvedTarget::Found(entry.clone(), params), + ), + RouteMatch::MethodNotAllowed(allowed) => ( + RouteResolution::MethodNotAllowed { + allowed: Arc::from(allowed.clone()), + }, + ResolvedTarget::MethodNotAllowed(allowed), + ), + RouteMatch::NotFound => (RouteResolution::NotFound, ResolvedTarget::NotFound), + }; + + ResolvedDispatch { + method: method.clone(), + owner: Arc::clone(&self.inner), + path: path.to_owned(), + resolution, + target, + } + } + #[must_use] #[inline] pub fn routes(&self) -> Vec { self.inner.route_index.to_vec() } + + /// Borrows state registered with [`RouterBuilder::with_state`] without cloning or rebuilding it. + #[must_use] + #[inline] + pub fn state(&self) -> Option<&T> + where + T: Send + Sync + 'static, + { + self.inner.state_extensions.get::() + } +} + +async fn fallback_terminal_response( + request: Request, + ingress: AdmittedIngress, +) -> Result, EdgeError> { + let Some(fallback) = ingress.into_fallback() else { + return Ok(None); + }; + let (grant, max_body_bytes, read_deadline, monotonic_clock, on_exceeded, on_timeout) = + fallback.into_parts(); + let outcome = drain_body_discard( + request.into_body(), + max_body_bytes, + read_deadline, + &monotonic_clock, + ) + .await; + drop(grant); + + match outcome? { + FallbackDrainOutcome::Complete => Ok(None), + FallbackDrainOutcome::Exceeded => Ok(Some(on_exceeded.into_response())), + FallbackDrainOutcome::TimedOut => Ok(Some(on_timeout.into_response())), + } } #[cfg(test)] @@ -402,7 +671,8 @@ mod tests { fn manifest_and_routes_route_injects_both() { // Combined `#[action(manifest, routes)]` → both payloads injected. let seen = run_probe( - RouterService::builder().with_manifest_json("{\"app\":{\"name\":\"t\"}}"), + RouterService::builder() + .with_manifest_json(serde_json::json!({"app": {"name": "t"}}).to_string()), IntrospectionNeeds { manifest: true, routes: true, @@ -415,7 +685,8 @@ mod tests { fn manifest_route_injects_only_manifest() { // Manifest available AND requested → ManifestJson present, RouteTable absent. let seen = run_probe( - RouterService::builder().with_manifest_json("{\"app\":{\"name\":\"t\"}}"), + RouterService::builder() + .with_manifest_json(serde_json::json!({"app": {"name": "t"}}).to_string()), IntrospectionNeeds { manifest: true, routes: false, @@ -443,7 +714,7 @@ mod tests { let saw: Arc>> = Arc::new(Mutex::new(None)); let router = RouterService::builder() - .with_manifest_json("{\"app\":{\"name\":\"t\"}}") + .with_manifest_json(serde_json::json!({"app": {"name": "t"}}).to_string()) .middleware(Probe(Arc::clone(&saw))) .get( "/", @@ -469,7 +740,8 @@ mod tests { fn plain_route_injects_neither() { // Manifest IS baked but the route requested nothing → neither injected. let seen = run_probe( - RouterService::builder().with_manifest_json("{\"app\":{\"name\":\"t\"}}"), + RouterService::builder() + .with_manifest_json(serde_json::json!({"app": {"name": "t"}}).to_string()), IntrospectionNeeds::default(), ); assert_eq!(seen, (false, false)); @@ -512,17 +784,178 @@ mod tests { } use super::*; + use crate::app::App; use crate::body::Body; use crate::context::RequestContext; use crate::error::EdgeError; use crate::http::{Method, Request, Response, StatusCode, request_builder}; + use crate::ingress::{ + AdmissionDecision, IngressAdmissionOutcome, IngressFraming, IngressGrant, IngressHead, + IngressHeadAccounting, + }; use crate::params::PathParams; use crate::response::response_with_body; + use crate::response_egress::ResponseEgressCompletion; + use crate::time::{Deadline, MonotonicInstant}; use futures::executor::block_on; use futures::task::noop_waker_ref; use serde::Deserialize; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; + use std::time::Duration; + + #[test] + fn resolve_exposes_stable_route_identity_and_sorted_method_candidates() { + async fn handler(_ctx: RequestContext) -> Result { + response_with_body(StatusCode::OK, Body::empty()) + } + + let router = RouterService::builder() + .post("/users/{id}", handler) + .route_with_class("/users/{id}", Method::GET, "auction", handler) + .build(); + + let matched = router.resolve(&Method::GET, "/users/42"); + let RouteResolution::Matched(metadata) = matched.resolution() else { + panic!("expected matched route"); + }; + assert_eq!(metadata.method(), Method::GET); + assert_eq!(metadata.pattern(), "/users/{id}"); + assert_eq!(metadata.id().method(), Method::GET); + assert_eq!(metadata.id().pattern(), "/users/{id}"); + assert_eq!(metadata.class(), Some("auction")); + + let rejected = router.resolve(&Method::DELETE, "/users/99"); + let RouteResolution::MethodNotAllowed { allowed } = rejected.resolution() else { + panic!("expected method-not-allowed route"); + }; + assert_eq!(allowed.len(), 2); + assert_eq!(allowed[0].method(), Method::GET); + assert_eq!(allowed[1].method(), Method::POST); + assert_eq!(allowed[0].class(), Some("auction")); + assert_eq!(allowed[1].class(), None); + + assert!(matches!( + router.resolve(&Method::GET, "/missing").resolution(), + RouteResolution::NotFound + )); + } + + fn admitted_for(resolved: &ResolvedDispatch, method: Method, path: &str) -> AdmittedIngress { + let app = App::new(RouterService::builder().build()); + let request = request_builder() + .method(method) + .uri(path) + .body(Body::empty()) + .expect("request"); + let head = IngressHead::from_request( + &request, + MonotonicInstant::now(), + resolved.resolution().clone(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ); + let IngressAdmissionOutcome::Admitted { + completion, + ingress: admitted, + } = app.admit_ingress(&head).expect("admission") + else { + panic!("expected admission"); + }; + drop(completion); + admitted + } + + #[test] + fn dispatch_resolved_preserves_metadata_and_grant() { + async fn handler(ctx: RequestContext) -> Result { + let metadata = ctx.route_metadata().expect("route metadata"); + let grant = ctx.take_ingress_grant().expect("ingress grant"); + assert!(ctx.take_ingress_grant().is_none()); + let lease = grant.downcast::().expect("lease type"); + Ok(format!( + "{}:{}:{lease}", + metadata.pattern(), + metadata.class().expect("route class") + )) + } + + let router = RouterService::builder() + .route_with_class("/users/{id}", Method::GET, "auction", handler) + .build(); + let resolved = router.resolve(&Method::GET, "/users/42"); + let start = MonotonicInstant::now(); + let request = request_builder() + .method(Method::GET) + .uri("/users/42") + .body(Body::empty()) + .expect("request"); + let head = IngressHead::from_request( + &request, + start, + resolved.resolution().clone(), + IngressHeadAccounting::HostManaged, + IngressFraming::HostManaged, + ); + let mut app = App::new(router.clone()); + app.set_ingress_admission_policy(|_| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::new(String::from("lease")), + read_deadline: Deadline::after(Duration::from_secs(1)), + }); + let IngressAdmissionOutcome::Admitted { + completion, + ingress: admitted, + } = app.admit_ingress(&head).expect("admission") + else { + panic!("expected admission"); + }; + drop(completion); + assert_eq!(admitted.request_start(), start); + let response = block_on(router.dispatch_resolved(resolved, request, admitted)) + .expect("resolved response"); + assert_eq!( + response.body().as_bytes().expect("buffered"), + b"/users/{id}:auction:lease" + ); + } + + #[test] + fn dispatch_resolved_rejects_foreign_router_token() { + let router = RouterService::builder() + .get("/users/{id}", ok_handler) + .build(); + let resolved = router.resolve(&Method::GET, "/users/42"); + let admitted = admitted_for(&resolved, Method::GET, "/users/42"); + let request = request_builder() + .method(Method::GET) + .uri("/users/42") + .body(Body::empty()) + .expect("request"); + let foreign = RouterService::builder().build(); + assert!(matches!( + block_on(foreign.dispatch_resolved(resolved, request, admitted)), + Err(EdgeError::Internal { .. }) + )); + } + + #[test] + fn dispatch_resolved_rejects_mutated_request_path() { + let router = RouterService::builder() + .get("/users/{id}", ok_handler) + .build(); + let resolved = router.resolve(&Method::GET, "/users/42"); + let admitted = admitted_for(&resolved, Method::GET, "/users/42"); + let request = request_builder() + .method(Method::GET) + .uri("/users/43") + .body(Body::empty()) + .expect("request"); + assert!(matches!( + block_on(router.dispatch_resolved(resolved, request, admitted)), + Err(EdgeError::Internal { .. }) + )); + } async fn ok_handler(_ctx: RequestContext) -> Result { response_with_body(StatusCode::OK, Body::empty()) @@ -725,6 +1158,7 @@ mod tests { let entry = RouteEntry { handler: ok_handler.into_handler(), introspection_needs: IntrospectionNeeds::default(), + metadata: RouteMetadata::new(Method::GET, "/test"), }; let cloned = entry.clone(); @@ -894,16 +1328,16 @@ mod tests { } #[crate::action] async fn probe(ctx: RequestContext) -> Result { - let request = ctx.request(); - let counter = request.extensions().get::>().unwrap(); + let body = ctx.body_bytes(1024).await?; + let counter = ctx.extensions().get::>().unwrap(); counter.fetch_add(1, Ordering::SeqCst); Ok(format!( "{}:{}:{}:{}:{}", ctx.path_params().get("id").unwrap(), - request.headers()["x-request-token"].to_str().unwrap(), - from_utf8(ctx.body().as_bytes().unwrap()).unwrap(), - request.extensions().get::().unwrap().0, - request.extensions().get::().unwrap().0 + ctx.headers()["x-request-token"].to_str().unwrap(), + from_utf8(&body).unwrap(), + ctx.extensions().get::().unwrap().0, + ctx.extensions().get::().unwrap().0 )) } let (send1, recv1) = oneshot::channel(); @@ -962,12 +1396,7 @@ mod tests { #[crate::action] async fn probe(ctx: RequestContext) -> Result { - assert!( - ctx.request() - .extensions() - .get::() - .is_none() - ); + assert!(ctx.extensions().get::().is_none()); Ok(ctx.path_params().get("id").unwrap().to_owned()) } diff --git a/crates/edgezero-core/src/secret_store.rs b/crates/edgezero-core/src/secret_store.rs index 509b4fbe0..274e690fa 100644 --- a/crates/edgezero-core/src/secret_store.rs +++ b/crates/edgezero-core/src/secret_store.rs @@ -26,7 +26,9 @@ use std::sync::Arc; use async_trait::async_trait; use bytes::Bytes; +use crate::config_store::BoundedStoreRead; use crate::error::EdgeError; +use crate::time::{Deadline, MonotonicClock}; // --------------------------------------------------------------------------- // Contract test macro @@ -114,6 +116,10 @@ pub const MAX_NAME_LEN: usize = 512; #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum SecretError { + /// The absolute read deadline expired before a complete value was available. + #[error("secret store read deadline exceeded")] + DeadlineExceeded, + /// A general internal error. #[error("secret store error: {0}")] Internal(#[from] anyhow::Error), @@ -129,12 +135,19 @@ pub enum SecretError { /// A validation error (e.g., invalid secret name). #[error("validation error: {0}")] Validation(String), + + /// The value or guest-visible backend read exceeded its supplied allowance. + #[error("secret value exceeds configured byte limit")] + ValueTooLarge, } impl From for EdgeError { #[inline] fn from(err: SecretError) -> Self { match err { + SecretError::DeadlineExceeded => { + EdgeError::service_unavailable("secret store read deadline exceeded") + } SecretError::NotFound { .. } => { EdgeError::internal(anyhow::anyhow!("required secret is not configured")) } @@ -142,6 +155,9 @@ impl From for EdgeError { SecretError::Validation(..) => { EdgeError::internal(anyhow::anyhow!("secret lookup failed")) } + SecretError::ValueTooLarge => { + EdgeError::internal(anyhow::anyhow!("secret value too large")) + } SecretError::Internal(..) => { EdgeError::internal(anyhow::anyhow!("secret store operation failed")) } @@ -189,6 +205,36 @@ impl SecretStore for InMemorySecretStore { let compound = format!("{store_name}/{key}"); Ok(self.secrets.get(&compound).cloned()) } + + #[inline] + async fn get_bytes_bounded( + &self, + store_name: &str, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError> { + if deadline.is_expired_at(clock.now()) { + return Err(SecretError::DeadlineExceeded); + } + let result = self.get_bytes(store_name, key).await; + if deadline.is_expired_at(clock.now()) { + return Err(SecretError::DeadlineExceeded); + } + let value = result?; + let backend_bytes = value.as_ref().map_or(Ok(0_u64), |stored_value| { + u64::try_from(stored_value.len()).map_err(|_length_error| SecretError::ValueTooLarge) + })?; + if backend_bytes > max_backend_bytes || backend_bytes > max_value_bytes { + return Err(SecretError::ValueTooLarge); + } + Ok(BoundedStoreRead { + backend_bytes, + value, + }) + } } // --------------------------------------------------------------------------- @@ -208,6 +254,25 @@ impl SecretStore for NoopSecretStore { async fn get_bytes(&self, _store_name: &str, _key: &str) -> Result, SecretError> { Ok(None) } + + #[inline] + async fn get_bytes_bounded( + &self, + _store_name: &str, + _key: &str, + clock: &MonotonicClock, + deadline: Deadline, + _max_backend_bytes: u64, + _max_value_bytes: u64, + ) -> Result, SecretError> { + if deadline.is_expired_at(clock.now()) { + return Err(SecretError::DeadlineExceeded); + } + Ok(BoundedStoreRead { + backend_bytes: 0, + value: None, + }) + } } // --------------------------------------------------------------------------- @@ -245,6 +310,34 @@ impl SecretHandle { self.provider.get_bytes(store_name, key).await } + /// Retrieve a secret under one absolute deadline and two byte limits. + /// + /// # Errors + /// Preserves validation and the provider's typed bounded-read errors. + #[inline] + pub async fn get_bytes_bounded( + &self, + store_name: &str, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError> { + validate_name(store_name)?; + validate_name(key)?; + self.provider + .get_bytes_bounded( + store_name, + key, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + .await + } + /// Create a new handle wrapping a multi-store provider. #[inline] pub fn new(provider: Arc) -> Self { @@ -297,12 +390,119 @@ impl SecretHandle { pub trait SecretStore: Send + Sync { /// Retrieve a secret from a named store. Returns `Ok(None)` if not found. async fn get_bytes(&self, store_name: &str, key: &str) -> Result, SecretError>; + + /// Retrieves one value under an absolute deadline and independent backend/value caps. + /// + /// Implementations must define their deadline and allocation behavior explicitly. There is + /// no compatibility default because awaiting an unbounded provider call can hide a + /// never-ready future. + async fn get_bytes_bounded( + &self, + store_name: &str, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError>; +} + +#[cfg(test)] +macro_rules! ready_secret_store_bounded_read { + () => { + fn get_bytes_bounded<'life0, 'life1, 'life2, 'life3, 'async_trait>( + &'life0 self, + store_name: &'life1 str, + key: &'life2 str, + clock: &'life3 $crate::time::MonotonicClock, + deadline: $crate::time::Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> ::std::pin::Pin< + Box< + dyn ::std::future::Future< + Output = Result< + $crate::config_store::BoundedStoreRead<::bytes::Bytes>, + $crate::secret_store::SecretError, + >, + > + 'async_trait, + >, + > + where + 'life0: 'async_trait, + 'life1: 'async_trait, + 'life2: 'async_trait, + 'life3: 'async_trait, + Self: 'async_trait, + { + Box::pin(async move { + if deadline.is_expired_at(clock.now()) { + return Err($crate::secret_store::SecretError::DeadlineExceeded); + } + let Some(result) = + ::futures_util::FutureExt::now_or_never(self.get_bytes(store_name, key)) + else { + return Err($crate::secret_store::SecretError::Internal( + ::anyhow::anyhow!("ready secret-store test fixture returned pending"), + )); + }; + if deadline.is_expired_at(clock.now()) { + return Err($crate::secret_store::SecretError::DeadlineExceeded); + } + let value = result?; + let backend_bytes = value.as_ref().map_or(Ok(0_u64), |stored_value| { + u64::try_from(stored_value.len()) + .map_err(|_length_error| $crate::secret_store::SecretError::ValueTooLarge) + })?; + if backend_bytes > max_backend_bytes || backend_bytes > max_value_bytes { + return Err($crate::secret_store::SecretError::ValueTooLarge); + } + Ok($crate::config_store::BoundedStoreRead { + backend_bytes, + value, + }) + }) + } + }; } // --------------------------------------------------------------------------- // Shared validation // --------------------------------------------------------------------------- +/// Applies post-read deadline precedence and byte caps to one materialized secret result. +/// +/// This helper does not dispatch, await, cancel, or time out the provider operation. A +/// [`SecretStore::get_bytes_bounded`] implementation must still reject an already-expired +/// deadline and arrange any target-specific timer race before calling it. +/// +/// # Errors +/// Returns the provider error, [`SecretError::DeadlineExceeded`], or +/// [`SecretError::ValueTooLarge`] according to post-read precedence. +#[inline] +pub fn finish_bounded_secret_read( + result: Result, SecretError>, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, +) -> Result, SecretError> { + if deadline.is_expired_at(clock.now()) { + return Err(SecretError::DeadlineExceeded); + } + let value = result?; + let backend_bytes = value.as_ref().map_or(Ok(0_u64), |stored_value| { + u64::try_from(stored_value.len()).map_err(|_length_error| SecretError::ValueTooLarge) + })?; + if backend_bytes > max_backend_bytes || backend_bytes > max_value_bytes { + return Err(SecretError::ValueTooLarge); + } + Ok(BoundedStoreRead { + backend_bytes, + value, + }) +} + fn validate_name(name: &str) -> Result<(), SecretError> { if name.is_empty() { return Err(SecretError::Validation( @@ -339,8 +539,11 @@ mod tests { use super::*; use crate::http::StatusCode; + use crate::time::{MonotonicClock, MonotonicInstant}; use bytes::Bytes; use futures::executor::block_on; + use std::sync::Mutex; + use std::time::Duration; fn provider_handle_with(entries: &[(&str, &str)]) -> SecretHandle { let provider = InMemorySecretStore::new( @@ -378,6 +581,100 @@ mod tests { }); } + #[test] + fn bounded_secret_exact_cap_succeeds_and_over_cap_discards_value() { + use crate::time::Deadline; + + let handle = provider_handle_with(&[("signing-keys/current", "abc123")]); + let clock = MonotonicClock::default(); + let exact = block_on(handle.get_bytes_bounded( + "signing-keys", + "current", + &clock, + Deadline::after(Duration::from_secs(1)), + 6, + 6, + )) + .expect("exact bounded read"); + assert_eq!(exact.backend_bytes, 6); + assert_eq!(exact.value, Some(Bytes::from_static(b"abc123"))); + + let error = block_on(handle.get_bytes_bounded( + "signing-keys", + "current", + &clock, + Deadline::after(Duration::from_secs(1)), + 6, + 5, + )) + .expect_err("value cap"); + assert!(matches!(error, SecretError::ValueTooLarge)); + } + + #[test] + fn bounded_secret_read_uses_injected_clock() { + let handle = provider_handle_with(&[("signing-keys/current", "abc123")]); + let process_now = MonotonicInstant::now(); + let injected_now = process_now + .checked_sub(Duration::from_mins(1)) + .expect("injected instant"); + let deadline = Deadline::at_instant( + injected_now + .checked_add(Duration::from_secs(1)) + .expect("deadline instant"), + ); + let clock = MonotonicClock::new(move || injected_now); + + let read = + block_on(handle.get_bytes_bounded("signing-keys", "current", &clock, deadline, 6, 6)) + .expect("the application clock is still before its deadline"); + + assert_eq!(read.value, Some(Bytes::from_static(b"abc123"))); + } + + #[test] + fn bounded_secret_read_deadline_wins_ready_provider_error() { + struct AdvancingErrorStore { + now: Arc>, + terminal: MonotonicInstant, + } + + #[async_trait(?Send)] + impl SecretStore for AdvancingErrorStore { + async fn get_bytes( + &self, + _store_name: &str, + _key: &str, + ) -> Result, SecretError> { + *self.now.lock().expect("clock lock") = self.terminal; + Err(SecretError::Unavailable) + } + + ready_secret_store_bounded_read!(); + } + + let start = MonotonicInstant::now(); + let terminal = start + .checked_add(Duration::from_secs(1)) + .expect("terminal instant"); + let now = Arc::new(Mutex::new(start)); + let clock_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *clock_now.lock().expect("clock lock")); + let handle = SecretHandle::new(Arc::new(AdvancingErrorStore { now, terminal })); + + let error = block_on(handle.get_bytes_bounded( + "signing-keys", + "current", + &clock, + Deadline::at_instant(terminal), + 6, + 6, + )) + .expect_err("equality expiry must beat a ready provider error"); + + assert!(matches!(error, SecretError::DeadlineExceeded)); + } + #[test] fn provider_handle_require_bytes_errors_for_missing() { let handle = provider_handle_with(&[]); diff --git a/crates/edgezero-core/src/store_registry.rs b/crates/edgezero-core/src/store_registry.rs index e0ffed595..28180fb61 100644 --- a/crates/edgezero-core/src/store_registry.rs +++ b/crates/edgezero-core/src/store_registry.rs @@ -25,9 +25,10 @@ use std::collections::BTreeMap; use bytes::Bytes; -use crate::config_store::ConfigStoreHandle; +use crate::config_store::{BoundedStoreRead, ConfigStoreHandle}; use crate::key_value_store::KvHandle; use crate::secret_store::{SecretError, SecretHandle}; +use crate::time::{Deadline, MonotonicClock}; /// A per-bind KV handle, returned by [`KvRegistry::named`] / [`KvRegistry::default`]. pub type BoundKvStore = KvHandle; @@ -71,6 +72,31 @@ impl BoundSecretStore { self.handle.get_bytes(&self.store_name, key).await } + /// Retrieve a secret against the bound store under one absolute deadline and byte budget. + /// + /// # Errors + /// Preserves validation and the provider's typed bounded-read errors. + #[inline] + pub async fn get_bytes_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError> { + self.handle + .get_bytes_bounded( + &self.store_name, + key, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + .await + } + /// Underlying [`SecretHandle`] (escape hatch for callers that need the /// store-name argument explicitly). #[inline] diff --git a/crates/edgezero-core/src/test_env.rs b/crates/edgezero-core/src/test_env.rs index a3b4ab035..51c96bc86 100644 --- a/crates/edgezero-core/src/test_env.rs +++ b/crates/edgezero-core/src/test_env.rs @@ -16,7 +16,8 @@ //! The guards below are RAII: they capture the prior value on construction and //! restore it on drop, so a panicking test cannot leak state into the next one. //! That alone does not make concurrent mutation sound — callers must also -//! serialise env-mutating tests on a process-wide mutex ([`env_lock`], or the +//! serialise env-mutating tests on a process-wide mutex +//! ([`crate::test_env::env_lock()`], or the //! calling crate's own equivalent) and hold it for the guard's whole lifetime. //! The `unsafe` here is sound only under that discipline, which is why this //! module is test-only and never compiled into a production build. @@ -36,7 +37,8 @@ use std::sync::{Mutex, OnceLock}; /// and restores the prior value — or removes the variable if it was previously /// unset — on drop. /// -/// Construct it while holding [`env_lock`] (or the calling crate's equivalent); +/// Construct it while holding [`crate::test_env::env_lock()`] (or the calling +/// crate's equivalent); /// see the module docs for the full safety contract. pub struct EnvOverride { key: OsString, diff --git a/crates/edgezero-core/src/time.rs b/crates/edgezero-core/src/time.rs new file mode 100644 index 000000000..84e5bf50f --- /dev/null +++ b/crates/edgezero-core/src/time.rs @@ -0,0 +1,400 @@ +use std::fmt; +use std::sync::Arc; +use std::time::Duration; + +use crate::error::{BudgetSource, EdgeError}; +use crate::outbound::{BudgetInputs, OutboundRequest}; + +/// Hard clamp on any caller-supplied duration, so construction cannot panic. +pub const DEADLINE_FAR_FUTURE: Duration = Duration::from_hours(168); +/// Budget applied when a request sets neither a timeout nor a deadline. +pub const DEFAULT_NO_DEADLINE_BUDGET: Duration = Duration::from_secs(30); + +/// An absolute, copyable monotonic deadline. A deadline at or before now is expired. +#[derive(Debug, Clone, Copy)] +pub struct Deadline(MonotonicInstant); + +/// Effective timeout selected for one outbound dispatch. +#[derive(Clone, Copy, Debug)] +pub struct DispatchBudget { + pub cause: BudgetSource, + pub deadline: Deadline, + pub duration: Duration, +} + +/// Portable monotonic clock instant used by `EdgeZero` timing APIs. +pub type MonotonicInstant = web_time::Instant; + +/// Cloneable monotonic clock source shared by one application and its admitted requests. +#[derive(Clone)] +pub struct MonotonicClock { + now: Arc MonotonicInstant + Send + Sync>, +} + +impl MonotonicClock { + /// Returns a deadline anchored to this clock and clamped to the portable maximum. + #[must_use] + #[inline] + pub fn deadline_after(&self, duration: Duration) -> Deadline { + let now = self.now(); + let clamped = duration.min(DEADLINE_FAR_FUTURE); + Deadline(now.checked_add(clamped).unwrap_or(now)) + } + + #[must_use] + #[inline] + pub fn new(now: Now) -> Self + where + Now: Fn() -> MonotonicInstant + Send + Sync + 'static, + { + Self { now: Arc::new(now) } + } + + #[must_use] + #[inline] + pub fn now(&self) -> MonotonicInstant { + (self.now)() + } +} + +impl Default for MonotonicClock { + #[inline] + fn default() -> Self { + Self::new(MonotonicInstant::now) + } +} + +impl fmt::Debug for MonotonicClock { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MonotonicClock").finish_non_exhaustive() + } +} + +impl Deadline { + /// Returns a deadline anchored to the process-default clock; never panics. + /// + /// Use [`MonotonicClock::deadline_after`] when work is paired with an injected clock. + #[inline] + #[must_use] + pub fn after(duration: Duration) -> Self { + let now = MonotonicInstant::now(); + let clamped = duration.min(DEADLINE_FAR_FUTURE); + Deadline(now.checked_add(clamped).unwrap_or(now)) + } + + /// Constructs a deadline from an absolute instant. + #[inline] + #[must_use] + pub fn at_instant(instant: MonotonicInstant) -> Self { + Deadline(instant) + } + + /// Returns the absolute deadline instant. + #[inline] + #[must_use] + pub fn instant(&self) -> MonotonicInstant { + self.0 + } + + /// Returns `true` once the deadline instant is at or before the process-default clock. + #[inline] + #[must_use] + pub fn is_expired(&self) -> bool { + self.is_expired_at(MonotonicInstant::now()) + } + + /// Returns `true` when this deadline is at or before the supplied clock snapshot. + #[inline] + #[must_use] + pub fn is_expired_at(&self, now: MonotonicInstant) -> bool { + self.remaining_at(now).is_none() + } + + /// Returns process-default-clock time remaining, or `None` once reached or passed. + #[inline] + #[must_use] + pub fn remaining(&self) -> Option { + self.remaining_at(MonotonicInstant::now()) + } + + /// Returns time remaining at an explicit clock snapshot, or `None` at/past expiry. + #[inline] + #[must_use] + pub fn remaining_at(&self, now: MonotonicInstant) -> Option { + self.0 + .checked_duration_since(now) + .filter(|remaining| !remaining.is_zero()) + } +} + +impl DispatchBudget { + /// Whether a terminal observation retains an earlier request-owned expiry across cutoff. + /// A cutoff-attributed phase timeout never triggers this exception; equal deadlines lose. + #[must_use] + #[inline] + pub fn own_deadline_expired_before( + self, + observation_cutoff: Deadline, + observed_at: MonotonicInstant, + ) -> bool { + self.cause != BudgetSource::BatchCutoff + && self.deadline.instant() < observation_cutoff.instant() + && self.deadline.is_expired_at(observed_at) + } +} + +/// Computes one effective outbound deadline from a shared monotonic snapshot. +/// +/// # Errors +/// Returns [`EdgeError::GatewayTimeout`] when the selected budget is already exhausted. +#[inline] +pub fn dispatch_budget( + request: &OutboundRequest, + now: MonotonicInstant, + observation_cutoff: Option, +) -> Result { + dispatch_budget_inputs(request.budget_inputs(), now, observation_cutoff, None) +} + +pub(crate) fn dispatch_budget_inputs( + inputs: BudgetInputs, + now: MonotonicInstant, + observation_cutoff: Option, + original_chain_deadline: Option, +) -> Result { + let deadline_from_duration = |duration: Duration| { + let bounded_duration = duration.min(DEADLINE_FAR_FUTURE); + Deadline::at_instant(now.checked_add(bounded_duration).unwrap_or(now)) + }; + + let from_timeout = inputs.timeout.map(deadline_from_duration); + let from_caller = inputs.deadline.map(|deadline| { + let far = now.checked_add(DEADLINE_FAR_FUTURE).unwrap_or(now); + Deadline::at_instant(deadline.instant().min(far)) + }); + let from_observation_cutoff = observation_cutoff.map(|deadline| { + let far = now.checked_add(DEADLINE_FAR_FUTURE).unwrap_or(now); + Deadline::at_instant(deadline.instant().min(far)) + }); + let from_chain = original_chain_deadline.map(|deadline| { + let far = now.checked_add(DEADLINE_FAR_FUTURE).unwrap_or(now); + Deadline::at_instant(deadline.instant().min(far)) + }); + let from_default = (inputs.timeout.is_none() && inputs.deadline.is_none()) + .then(|| deadline_from_duration(DEFAULT_NO_DEADLINE_BUDGET)); + + let (cause, deadline) = [ + from_observation_cutoff.map(|deadline| (BudgetSource::BatchCutoff, deadline)), + from_timeout.map(|deadline| (BudgetSource::PerCallTimeout, deadline)), + from_caller.map(|deadline| (BudgetSource::RequestDeadline, deadline)), + from_chain.map(|deadline| (BudgetSource::RequestDeadline, deadline)), + from_default.map(|deadline| (BudgetSource::Default, deadline)), + ] + .into_iter() + .flatten() + .min_by_key(|(_, deadline)| deadline.instant()) + .ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!( + "dispatch_budget: no deadline candidate; invariant violated" + )) + })?; + + let duration = deadline.instant().saturating_duration_since(now); + if duration.is_zero() { + return Err(EdgeError::gateway_timeout_caused( + "effective budget is zero", + cause, + )); + } + Ok(DispatchBudget { + cause, + deadline, + duration, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + + #[test] + fn earlier_own_deadline_cutoff_rule_is_shared() { + let start = MonotonicInstant::now(); + let at = |millis| { + start + .checked_add(Duration::from_millis(millis)) + .expect("fixture time") + }; + let observation_cutoff = Deadline::at_instant(at(100)); + for (cause, expiry_ms, observed_ms, expected) in [ + (BudgetSource::BatchCutoff, 90, 120, false), + (BudgetSource::PerCallTimeout, 90, 89, false), + (BudgetSource::PerCallTimeout, 90, 90, true), + (BudgetSource::RequestDeadline, 90, 100, true), + (BudgetSource::Default, 90, 200, true), + (BudgetSource::RequestDeadline, 100, 100, false), + (BudgetSource::RequestDeadline, 110, 120, false), + (BudgetSource::RequestDeadline, 90, 99, true), + ] { + let budget = DispatchBudget { + cause, + deadline: Deadline::at_instant(at(expiry_ms)), + duration: Duration::from_millis(expiry_ms), + }; + assert_eq!( + budget.own_deadline_expired_before(observation_cutoff, at(observed_ms)), + expected, + "{cause:?}, expiry={expiry_ms}, observation={observed_ms}", + ); + } + } + + #[test] + fn monotonic_clock_uses_the_injected_source() { + let start = MonotonicInstant::now(); + let now = Arc::new(Mutex::new(start)); + let observed_now = Arc::clone(&now); + let clock = MonotonicClock::new(move || *observed_now.lock().expect("clock lock")); + + assert_eq!(clock.now(), start); + let advanced = start + .checked_add(Duration::from_secs(2)) + .expect("advanced instant"); + *now.lock().expect("clock lock") = advanced; + assert_eq!(clock.now(), advanced); + assert_eq!(clock.clone().now(), advanced); + } + + #[test] + fn monotonic_clock_deadline_after_uses_the_injected_source() { + let start = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || start); + let deadline = clock.deadline_after(Duration::from_secs(2)); + + assert_eq!( + deadline.instant(), + start.checked_add(Duration::from_secs(2)).expect("deadline") + ); + } + + #[test] + fn monotonic_clock_deadline_after_clamps_to_far_future() { + let start = MonotonicInstant::now(); + let clock = MonotonicClock::new(move || start); + let deadline = clock.deadline_after(Duration::MAX); + + assert_eq!( + deadline.instant(), + start.checked_add(DEADLINE_FAR_FUTURE).expect("deadline") + ); + } + + #[test] + fn monotonic_instant_is_public_clock_type() { + let start = MonotonicInstant::now(); + let deadline = Deadline::at_instant(start); + assert_eq!(deadline.instant(), start); + } + + #[test] + fn deadline_is_copy() { + fn assert_copy() + where + T: Copy, + { + } + assert_copy::(); + } + + #[test] + fn constants_have_exact_values() { + assert_eq!(DEFAULT_NO_DEADLINE_BUDGET, Duration::from_secs(30)); + assert_eq!(DEADLINE_FAR_FUTURE, Duration::from_hours(168)); + } + + #[test] + fn deadline_before_now_is_expired() { + let base = MonotonicInstant::now(); + let past = Deadline::at_instant(base); + let now = base + .checked_add(Duration::from_secs(1)) + .expect("no overflow"); + assert!(past.is_expired_at(now)); + assert_eq!(past.remaining_at(now), None); + } + + #[test] + fn deadline_exactly_now_is_expired() { + let base = MonotonicInstant::now(); + let at_now = Deadline::at_instant(base); + assert_eq!( + at_now.remaining_at(base), + None, + "zero remaining is expired, not Some(0)" + ); + assert!( + at_now.is_expired_at(base), + "a deadline exactly at now is expired" + ); + } + + #[test] + fn deadline_in_future_has_exact_remaining() { + let base = MonotonicInstant::now(); + let future = Deadline::at_instant( + base.checked_add(Duration::from_mins(1)) + .expect("no overflow"), + ); + assert!(!future.is_expired_at(base)); + assert_eq!(future.remaining_at(base), Some(Duration::from_mins(1))); + } + + #[test] + fn after_clamps_duration_max_to_far_future() { + let before = MonotonicInstant::now(); + let deadline = Deadline::after(Duration::MAX); + let after = MonotonicInstant::now(); + let lower = before + .checked_add(DEADLINE_FAR_FUTURE) + .expect("no overflow"); + let upper = after.checked_add(DEADLINE_FAR_FUTURE).expect("no overflow"); + assert!(deadline.instant() >= lower, "clamped below the 7-day bound"); + assert!( + deadline.instant() <= upper, + "Duration::MAX was not clamped to 7 days" + ); + } + + #[test] + fn public_remaining_and_is_expired_smoke() { + let before = MonotonicInstant::now(); + let far = Deadline::after(Duration::from_hours(1)); + let after = MonotonicInstant::now(); + assert!(!far.is_expired()); + let lower = before + .checked_add(Duration::from_hours(1)) + .expect("no overflow"); + let upper = after + .checked_add(Duration::from_hours(1)) + .expect("no overflow"); + assert!( + far.instant() >= lower && far.instant() <= upper, + "after() must land exactly at now plus duration" + ); + assert!(far.remaining().is_some()); + + let now_deadline = Deadline::after(Duration::ZERO); + assert!(now_deadline.is_expired()); + assert_eq!(now_deadline.remaining(), None); + } + + #[test] + fn instant_round_trips() { + let base = MonotonicInstant::now() + .checked_add(Duration::from_secs(10)) + .expect("no overflow"); + assert_eq!(Deadline::at_instant(base).instant(), base); + } +} diff --git a/crates/edgezero-core/tests/assembly_failure_logging.rs b/crates/edgezero-core/tests/assembly_failure_logging.rs new file mode 100644 index 000000000..dda0782aa --- /dev/null +++ b/crates/edgezero-core/tests/assembly_failure_logging.rs @@ -0,0 +1,67 @@ +#[cfg(test)] +mod tests { + use edgezero_core::response_egress_framing::ResponseEgressFramingMode; + use std::sync::{Mutex, PoisonError}; + + use edgezero_core::app::App; + use edgezero_core::http::Method; + use edgezero_core::{BOOT_LOG_TARGET, EdgeError, MonotonicClock}; + use futures::executor::block_on; + + static LOGGER: BootLogger = BootLogger(Mutex::new(Vec::new())); + + struct BootLogger(Mutex>); + + impl log::Log for BootLogger { + fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { + metadata.target() == BOOT_LOG_TARGET + } + + fn flush(&self) {} + + fn log(&self, record: &log::Record<'_>) { + if self.enabled(record.metadata()) { + self.0 + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push(format!("{}: {}", record.level(), record.args())); + } + } + } + + #[test] + fn assembly_error_logs_full_source_once_and_never_exposes_it_on_the_wire() { + log::set_logger(&LOGGER).unwrap(); + log::set_max_level(log::LevelFilter::Error); + let source = anyhow::anyhow!("private-token=secret").context("state initialization failed"); + let clock = MonotonicClock::default(); + let envelope = App::assembly_error_egress( + EdgeError::internal(source), + Method::GET, + clock.now(), + clock, + ); + let Ok((prepared, _, mut attempt, egress_clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("assembly failure response must prepare"); + }; + let bytes = block_on( + prepared + .into_response() + .into_body() + .into_bytes_bounded(2048), + ) + .unwrap(); + let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body["error"]["message"], "internal server error"); + assert!(attempt.begin_writing()); + assert!(attempt.complete(egress_clock.now())); + drop(attempt); + let records = LOGGER.0.lock().unwrap(); + assert_eq!(records.len(), 1); + assert!(records[0].starts_with("ERROR: ")); + assert!(records[0].contains("state initialization failed")); + assert!(records[0].contains("private-token=secret")); + } +} diff --git a/crates/edgezero-core/tests/outbound_prepared_client.rs b/crates/edgezero-core/tests/outbound_prepared_client.rs new file mode 100644 index 000000000..22936021e --- /dev/null +++ b/crates/edgezero-core/tests/outbound_prepared_client.rs @@ -0,0 +1,630 @@ +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::future::Future; + use std::pin::Pin; + use std::rc::Rc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll, Waker}; + use std::time::Duration; + + use edgezero_core::http::{HeaderMap, StatusCode, Uri}; + use edgezero_core::{ + BudgetSource, Deadline, DispatchBudget, EdgeError, HttpClient, MonotonicClock, + MonotonicInstant, OutboundBatch, OutboundBatchCheck, OutboundBatchDriverEvent, + OutboundBatchNext, OutboundBatchTermination, OutboundCollectedSlot, + OutboundDispatchRequest, OutboundExchangeOwner, OutboundFuture, OutboundHead, + OutboundHeaderFidelity, OutboundHeaderInput, OutboundHttpClient, OutboundPending, + OutboundPreparedBatch, OutboundProxyMarker, OutboundRequest, OutboundResponse, + OutboundSendMode, OutboundTime, + }; + use futures::executor::block_on; + use futures_util::future::ready; + use futures_util::task::{ArcWake, waker_ref}; + use futures_util::{FutureExt as _, StreamExt as _, stream}; + + struct BatchRecord { + indices: Vec, + observation_cutoff: Deadline, + slot_count: usize, + started_at: MonotonicInstant, + } + + #[derive(Clone)] + struct FakeClock(Arc>); + + struct ClockState { + next_id: usize, + now: MonotonicInstant, + waiters: BTreeMap)>, + } + + struct Fixture { + clock: FakeClock, + state: Arc>, + } + + #[derive(Default)] + struct Recording { + batches: Vec, + exchanges_released: usize, + groups_released: usize, + requests: Vec, + support_calls: Vec<(Uri, OutboundSendMode)>, + } + + struct RequestRecord { + budget: DispatchBudget, + connect_timeout: Option, + entered_at: MonotonicInstant, + mode: OutboundSendMode, + uri: Uri, + } + + struct SuppliedClient { + state: Arc>, + time: OutboundTime, + } + + struct Timer { + deadline: Deadline, + id: usize, + state: Arc>, + } + + #[derive(Default)] + struct WakeCount(AtomicUsize); + + impl FakeClock { + fn advance(&self, duration: Duration) { + let wakers: Vec<_> = { + let mut state = self.0.lock().expect("clock advancement"); + state.now = state.now.checked_add(duration).expect("bounded fake time"); + let now = state.now; + state + .waiters + .values_mut() + .filter(|(deadline, _)| deadline.is_expired_at(now)) + .filter_map(|(_, waker)| waker.take()) + .collect() + }; + for waker in wakers { + waker.wake(); + } + } + + fn clock(&self) -> MonotonicClock { + let state = Arc::clone(&self.0); + MonotonicClock::new(move || state.lock().expect("clock sample").now) + } + + fn new() -> Self { + Self(Arc::new(Mutex::new(ClockState { + next_id: 0, + now: MonotonicInstant::now(), + waiters: BTreeMap::new(), + }))) + } + + fn pair(&self) -> OutboundTime { + let shared = Arc::clone(&self.0); + OutboundTime::new(self.clock(), move |deadline, clock| { + let now = clock.now(); + let mut state = shared.lock().expect("timer registration"); + assert_eq!(now, state.now); + let id = state.next_id; + state.next_id = state.next_id.saturating_add(1); + state.waiters.insert(id, (deadline, None)); + Box::pin(Timer { + deadline, + id, + state: Arc::clone(&shared), + }) + }) + } + } + + impl Fixture { + fn client(&self, shared_constructor: bool) -> HttpClient { + let supplied = SuppliedClient { + state: Arc::clone(&self.state), + time: self.clock.pair(), + }; + if shared_constructor { + HttpClient::new(Arc::new(supplied)) + } else { + HttpClient::with_client(supplied) + } + } + + fn new() -> Self { + Self { + clock: FakeClock::new(), + state: Arc::new(Mutex::new(Recording::default())), + } + } + } + + impl SuppliedClient { + fn head(&self, prepared: OutboundDispatchRequest) -> Result { + let budget = prepared.budget(); + let connect_timeout = prepared.request().connect_timeout_setting(); + let entered_at = prepared.entered_at(); + let uri = prepared.request().uri().clone(); + if prepared.clock().now() != prepared.time().clock().now() { + return Err(EdgeError::internal(anyhow::anyhow!( + "prepared clocks differ" + ))); + } + let (parts, context) = prepared.into_parts(); + if parts.connect_timeout != connect_timeout || context.entered_at() != entered_at { + return Err(EdgeError::internal(anyhow::anyhow!( + "prepared parts changed policy" + ))); + } + let record = RequestRecord { + budget, + connect_timeout, + entered_at, + mode: context.mode(), + uri, + }; + self.state + .lock() + .map_err(|_error| { + EdgeError::internal(anyhow::anyhow!("request recording lock poisoned")) + })? + .requests + .push(record); + let state = Arc::clone(&self.state); + let local_owner = Rc::new(()); + OutboundHead::new( + StatusCode::OK, + OutboundHeaderInput::new( + "supplied", + HeaderMap::new(), + OutboundHeaderFidelity::AdapterVisibleFields, + ), + stream::empty().boxed_local(), + context, + OutboundExchangeOwner::new(move || { + drop(local_owner); + let mut recording = state.lock().expect("exchange release"); + recording.exchanges_released = recording.exchanges_released.saturating_add(1); + }), + ) + } + + fn response( + &self, + prepared: OutboundDispatchRequest, + ) -> Result { + self.head(prepared)? + .finish() + .now_or_never() + .unwrap_or_else(|| { + Err(EdgeError::internal(anyhow::anyhow!( + "in-memory exchange unexpectedly pending" + ))) + }) + } + } + + impl OutboundHttpClient for SuppliedClient { + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let completed = self.response(request); + let local_owner = Rc::new(()); + Ok(OutboundPending::new( + Box::pin(ready(completed)), + OutboundExchangeOwner::new(move || drop(local_owner)), + )) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + let completed = self.head(request); + Box::pin(ready(completed)) + } + + fn progress(&self) -> Rc { + Rc::new(edgezero_core::IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let record = BatchRecord { + indices: Vec::new(), + observation_cutoff: batch.observation_cutoff(), + slot_count: batch.slot_count(), + started_at: batch.started_at(), + }; + assert_eq!(batch.clock().now(), batch.time().clock().now()); + let (context, slots) = batch.into_slots(); + assert_eq!(context.slot_count(), record.slot_count); + assert_eq!(context.started_at(), record.started_at); + assert_eq!( + context.observation_cutoff().instant(), + record.observation_cutoff.instant() + ); + assert_eq!(context.clock().now(), context.time().clock().now()); + let completed_record = BatchRecord { + indices: slots.iter().map(|(index, _)| *index).collect(), + ..record + }; + self.state + .lock() + .expect("batch recording") + .batches + .push(completed_record); + if slots.is_empty() { + return OutboundBatch::cutoff(context); + } + // Execute all eligible in-memory exchanges before handing out the observation stream. + let events: Vec<_> = slots + .into_iter() + .map(|(index, prepared)| { + assert_eq!(prepared.entered_at(), context.started_at()); + let outcome = self.response(prepared); + OutboundBatchDriverEvent::Item { + index, + completed_at: context.clock().now(), + outcome, + } + }) + .collect(); + let state = Arc::clone(&self.state); + let local_owner = Rc::new(()); + OutboundBatch::from_driver( + context, + stream::iter(events), + OutboundExchangeOwner::new(move || { + drop(local_owner); + let mut recording = state.lock().expect("group release"); + recording.groups_released = recording.groups_released.saturating_add(1); + }), + ) + } + + fn time(&self) -> OutboundTime { + self.time.clone() + } + + fn validate_supported_options( + &self, + request: &OutboundRequest, + mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + self.state + .lock() + .map_err(|_error| { + EdgeError::internal(anyhow::anyhow!("support recording lock poisoned")) + })? + .support_calls + .push((request.uri().clone(), mode)); + if request.has_host_authority_override() { + Err(EdgeError::bad_request( + "host_authority_override unsupported by supplied fixture", + )) + } else { + Ok(()) + } + } + } + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for Timer { + #[inline] + fn drop(&mut self) { + assert!( + self.state + .lock() + .expect("timer release") + .waiters + .remove(&self.id) + .is_some() + ); + } + } + + impl Future for Timer { + type Output = Result<(), EdgeError>; + + #[inline] + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut state = self.state.lock().expect("timer poll"); + if self.deadline.is_expired_at(state.now) { + Poll::Ready(Ok(())) + } else { + state.waiters.get_mut(&self.id).expect("registered timer").1 = + Some(cx.waker().clone()); + Poll::Pending + } + } + } + + impl ArcWake for WakeCount { + fn wake(self: Arc) { + Self::wake_by_ref(&self); + } + fn wake_by_ref(arc_self: &Arc) { + arc_self.0.fetch_add(1, Ordering::SeqCst); + } + } + + fn assert_eager_batch( + fixture: &Fixture, + started_at: MonotonicInstant, + observation_cutoff: Deadline, + ) { + let recording = fixture.state.lock().expect("eager recording"); + assert_eq!( + recording.requests.len(), + 2, + "already complete before first observation" + ); + assert_eq!(recording.exchanges_released, 2); + assert_eq!(recording.groups_released, 0); + assert_eq!(recording.support_calls.len(), 4); + assert!( + recording + .support_calls + .iter() + .all(|(_, mode)| *mode == OutboundSendMode::Batch) + ); + let captured = recording.batches.first().expect("captured batch"); + assert_eq!(captured.indices, vec![0, 2]); + assert_eq!(captured.slot_count, 4); + assert_eq!(captured.started_at, started_at); + assert_eq!( + captured.observation_cutoff.instant(), + observation_cutoff.instant() + ); + assert_eq!( + recording.requests.first().expect("zero").uri.path(), + "/zero" + ); + assert_eq!(recording.requests.get(1).expect("two").uri.path(), "/two"); + for request in &recording.requests { + assert_eq!(request.entered_at, started_at); + assert_eq!( + request.budget.deadline.instant(), + observation_cutoff.instant() + ); + assert_eq!(request.budget.cause, BudgetSource::BatchCutoff); + } + } + + #[test] + fn batch_producer_uses_prepared_context() { + for shared_constructor in [false, true] { + let fixture = Fixture::new(); + let client = fixture.client(shared_constructor); + let started_at = fixture.clock.clock().now(); + let observation_cutoff = fixture + .clock + .clock() + .deadline_after(Duration::from_millis(100)); + let requests = vec![ + OutboundRequest::get("https://example.com/zero").expect("request"), + OutboundRequest::get("https://example.com/one") + .expect("request") + .stream_response(), + OutboundRequest::get("https://example.com/two").expect("request"), + OutboundRequest::get("https://example.com/three") + .expect("request") + .host_authority_override("override.example") + .expect("authority") + .timeout(Duration::ZERO), + ]; + let mut batch = client.start_batch_until(requests, observation_cutoff); + assert_eager_batch(&fixture, started_at, observation_cutoff); + let mut accumulator = batch.accumulator(); + let OutboundBatchCheck::Item(first) = batch.check().expect("first observation") else { + panic!("first receipt"); + }; + assert_eq!(first.index(), 0); + accumulator.insert(first).expect("first receipt stored"); + let OutboundBatchNext::Item(preflight) = + block_on(batch.next()).expect("preflight observation") + else { + panic!("preflight receipt"); + }; + assert_eq!(preflight.index(), 1); + assert!(matches!( + &preflight.result().outcome, + Err(EdgeError::BadRequest { .. }) + )); + accumulator + .insert(preflight) + .expect("preflight receipt stored"); + fixture.clock.advance(Duration::from_millis(200)); + let collected = + block_on(batch.collect()).expect("recorded outcomes survive late collection"); + assert_eq!(collected.termination(), OutboundBatchTermination::Completed); + assert!(matches!( + collected.slots().first(), + Some(OutboundCollectedSlot::AlreadyDelivered) + )); + let merged = accumulator + .merge_success(collected) + .expect("same-batch merge"); + let (slots, termination) = merged.into_parts(); + assert_eq!(termination, OutboundBatchTermination::Completed); + let Some(OutboundCollectedSlot::Resolved(rejected)) = slots.get(3) else { + panic!("unsupported preflight retained"); + }; + assert!( + matches!(&rejected.outcome, Err(EdgeError::BadRequest { message, .. }) if message == "host_authority_override unsupported by supplied fixture") + ); + assert!( + slots + .iter() + .all(|slot| matches!(slot, OutboundCollectedSlot::Resolved(_))) + ); + assert_eq!( + fixture.state.lock().expect("group cleanup").groups_released, + 1 + ); + assert!( + fixture + .clock + .0 + .lock() + .expect("timer cleanup") + .waiters + .is_empty() + ); + } + } + + #[test] + fn configured_connect_timeout_reaches_dispatch() { + let fixture = Fixture::new(); + let entered_at = fixture.clock.clock().now(); + let deadline = fixture.clock.clock().deadline_after(Duration::from_secs(2)); + let operation: OutboundFuture = { + let client = fixture.client(true).with_proxy_marker(false); + let request = OutboundRequest::get("https://example.com/send") + .expect("request") + .connect_timeout(Duration::from_secs(1)) + .timeout(Duration::from_secs(3)) + .deadline(deadline); + assert!(!client.effective_proxy_marker_enabled(&request)); + client.send(request) + }; + assert_eq!( + fixture + .state + .lock() + .expect("synchronous support hook") + .support_calls + .len(), + 1 + ); + fixture.clock.advance(Duration::from_millis(10)); + let response = block_on(operation).expect("owned send after client drop"); + assert!(!response.headers().contains_key("x-edgezero-proxy")); + let recording = fixture.state.lock().expect("dispatch recording"); + let captured = recording.requests.first().expect("prepared request"); + assert_eq!(captured.connect_timeout, Some(Duration::from_secs(1))); + assert_eq!(captured.entered_at, entered_at); + assert_eq!(captured.budget.deadline.instant(), deadline.instant()); + assert_eq!(captured.budget.duration, Duration::from_secs(2)); + assert_eq!(captured.budget.cause, BudgetSource::RequestDeadline); + assert_eq!(captured.mode, OutboundSendMode::Send); + } + + #[test] + fn connect_timeout_survives_request_parts_round_trip() { + let fixture = Fixture::new(); + let request = OutboundRequest::get("https://example.com/parts") + .expect("request") + .connect_timeout(Duration::from_secs(1)) + .proxy_marker(OutboundProxyMarker::Off); + let mut parts = request.into_parts(); + assert_eq!(parts.connect_timeout, Some(Duration::from_secs(1))); + parts.connect_timeout = Some(Duration::from_secs(2)); + let rebuilt = OutboundRequest::from_parts(parts).expect("round trip"); + assert_eq!( + rebuilt.connect_timeout_setting(), + Some(Duration::from_secs(2)) + ); + let response = block_on(fixture.client(false).send(rebuilt)).expect("round-trip dispatch"); + assert!(!response.headers().contains_key("x-edgezero-proxy")); + assert_eq!( + fixture + .state + .lock() + .expect("dispatch recording") + .requests + .first() + .expect("prepared request") + .connect_timeout, + Some(Duration::from_secs(2)) + ); + } + + #[test] + fn exact_observation_cutoff_is_unresolved_without_dispatch() { + for shared_constructor in [false, true] { + let fixture = Fixture::new(); + let observation_cutoff = Deadline::at_instant(fixture.clock.clock().now()); + let batch = fixture.client(shared_constructor).start_batch_until( + vec![ + OutboundRequest::get("https://example.com/expired") + .expect("request") + .timeout(Duration::from_secs(1)), + ], + observation_cutoff, + ); + let result = + block_on(batch.collect()).expect("exact cutoff is a normal unresolved batch"); + assert_eq!(result.termination(), OutboundBatchTermination::Cutoff); + assert!(matches!( + result.slots(), + [OutboundCollectedSlot::Unresolved] + )); + let recording = fixture.state.lock().expect("zero dispatches"); + assert!(recording.requests.is_empty()); + assert_eq!(recording.exchanges_released, 0); + assert_eq!(recording.groups_released, 0); + assert_eq!(recording.support_calls.len(), 1); + assert!( + recording + .batches + .first() + .expect("prepared batch") + .indices + .is_empty() + ); + let clock = fixture.clock.0.lock().expect("zero timers"); + assert_eq!(clock.next_id, 0); + assert!(clock.waiters.is_empty()); + } + } + + #[test] + fn supplied_begin_returns_owned_ready_exchange() { + let fixture = Fixture::new(); + let operation: OutboundFuture = fixture + .client(false) + .begin(OutboundRequest::get("https://example.com/begin").expect("request")); + let mut pending = block_on(operation).expect("owned begin"); + assert_eq!(fixture.state.lock().expect("eager begin").requests.len(), 1); + assert!(pending.is_ready()); + let response = block_on(pending.collect()).expect("completed exchange"); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + fixture + .state + .lock() + .expect("begin mode") + .requests + .first() + .expect("request") + .mode, + OutboundSendMode::Begin + ); + } + + #[test] + fn supplied_timer_pair_wakes_and_unregisters() { + let clock = FakeClock::new(); + let time = clock.pair(); + let deadline = time.clock().deadline_after(Duration::from_millis(50)); + let mut wait = time.wait_until(deadline); + let wake = Arc::new(WakeCount::default()); + let waker = waker_ref(&wake); + assert!( + wait.as_mut() + .poll(&mut Context::from_waker(&waker)) + .is_pending() + ); + clock.advance(Duration::from_millis(50)); + assert_eq!(wake.0.load(Ordering::SeqCst), 1); + assert!(matches!( + wait.as_mut().poll(&mut Context::from_waker(&waker)), + Poll::Ready(Ok(())) + )); + assert!(clock.0.lock().expect("timer cleanup").waiters.is_empty()); + } +} diff --git a/crates/edgezero-core/tests/outbound_public_api.rs b/crates/edgezero-core/tests/outbound_public_api.rs new file mode 100644 index 000000000..1da10f697 --- /dev/null +++ b/crates/edgezero-core/tests/outbound_public_api.rs @@ -0,0 +1,249 @@ +use edgezero_core::{ + OutboundCropFocus, OutboundImageFormat, OutboundImageOptimizerOptions, + OutboundImageOptimizerRegion, OutboundRequest, OutboundResizeFilter, +}; + +#[test] +#[expect( + clippy::tests_outside_test_module, + reason = "external supplied-client API integration test" +)] +fn image_optimizer_options_round_trip() { + let options = OutboundImageOptimizerOptions::new() + .region(OutboundImageOptimizerRegion::UsWest) + .preserve_origin_query(true) + .format(OutboundImageFormat::WebP) + .quality(80) + .expect("quality") + .resize_filter(OutboundResizeFilter::Bicubic) + .width(100) + .expect("width") + .height(50) + .expect("height") + .crop(2, 1, OutboundCropFocus::Percent { x: 25, y: 75 }) + .expect("crop"); + let request = OutboundRequest::get("https://example.com/image%20one.jpg?signed=one%2Ftwo") + .expect("target") + .header("authorization", "fixture-signature") + .expect("signature") + .image_optimizer(options.clone()); + assert_eq!(request.image_optimizer_options(), Some(&options)); + let target = request.uri().clone(); + let parts = request.into_parts(); + assert_eq!(parts.image_optimizer, Some(options.clone())); + let rebuilt = OutboundRequest::from_parts(parts).expect("parts"); + assert_eq!(rebuilt.image_optimizer_options(), Some(&options)); + assert_eq!(rebuilt.uri(), &target); + assert_eq!(rebuilt.headers()["authorization"], "fixture-signature"); + let replacement = + OutboundImageOptimizerOptions::new().region(OutboundImageOptimizerRegion::Asia); + let replaced = rebuilt.image_optimizer(replacement.clone()); + assert_eq!(replaced.image_optimizer_options(), Some(&replacement)); + let mut cleared_parts = replaced.into_parts(); + cleared_parts.image_optimizer = None; + let cleared = OutboundRequest::from_parts(cleared_parts).expect("cleared parts"); + assert!(cleared.image_optimizer_options().is_none()); +} + +#[test] +#[expect( + clippy::tests_outside_test_module, + reason = "external supplied-client API integration test" +)] +fn public_image_optimizer_value_readbacks_compile_contract() { + let region = OutboundImageOptimizerRegion::UsWest; + let options = OutboundImageOptimizerOptions::new() + .region(region) + .preserve_origin_query(true) + .format(OutboundImageFormat::WebP) + .quality(80) + .expect("quality") + .resize_filter(OutboundResizeFilter::Bicubic) + .width(100) + .expect("width") + .height(50) + .expect("height") + .crop(2, 1, OutboundCropFocus::Percent { x: 25, y: 75 }) + .expect("crop"); + assert_eq!(options.region_setting(), Some(®ion)); + assert_eq!(region.as_str(), "us_west"); + assert!(options.preserve_origin_query_setting()); + assert_eq!(options.format_setting(), Some(OutboundImageFormat::WebP)); + assert_eq!(options.quality_setting(), Some(80)); + assert_eq!( + options.resize_filter_setting(), + Some(OutboundResizeFilter::Bicubic) + ); + assert_eq!(options.width_setting(), Some(100)); + assert_eq!(options.height_setting(), Some(50)); + assert_eq!( + options.crop_setting(), + Some((2, 1, OutboundCropFocus::Percent { x: 25, y: 75 })) + ); + assert_eq!(options.clone(), options); + assert_eq!( + OutboundImageOptimizerOptions::new(), + OutboundImageOptimizerOptions::default() + ); +} + +#[test] +#[expect( + clippy::tests_outside_test_module, + reason = "external supplied-client API integration test" +)] +fn public_image_optimizer_regions_support_exhaustive_matching() { + for region in [ + OutboundImageOptimizerRegion::Asia, + OutboundImageOptimizerRegion::Australia, + OutboundImageOptimizerRegion::EuCentral, + OutboundImageOptimizerRegion::EuWest, + OutboundImageOptimizerRegion::UsCentral, + OutboundImageOptimizerRegion::UsEast, + OutboundImageOptimizerRegion::UsWest, + ] { + let token = match region { + OutboundImageOptimizerRegion::Asia => "asia", + OutboundImageOptimizerRegion::Australia => "australia", + OutboundImageOptimizerRegion::EuCentral => "eu_central", + OutboundImageOptimizerRegion::EuWest => "eu_west", + OutboundImageOptimizerRegion::UsCentral => "us_central", + OutboundImageOptimizerRegion::UsEast => "us_east", + OutboundImageOptimizerRegion::UsWest => "us_west", + }; + assert_eq!(region.as_str(), token); + assert_eq!( + OutboundImageOptimizerRegion::parse(token).expect("region"), + region + ); + } +} + +#[test] +#[expect( + clippy::tests_outside_test_module, + reason = "external implementer validation API contract" +)] +fn public_dispatch_validation_modes_compile_contract() { + use edgezero_core::{Body, EdgeError, OutboundSendMode, validate_for_dispatch}; + use futures_util::stream; + + let validate: fn(&OutboundRequest, OutboundSendMode) -> Result<(), EdgeError> = + validate_for_dispatch; + let request = OutboundRequest::post("https://example.com/") + .expect("request") + .stream_response(); + for mode in [ + OutboundSendMode::Send, + OutboundSendMode::Begin, + OutboundSendMode::Follow, + ] { + validate(&request, mode).expect("single exchange shape"); + } + assert!(matches!( + validate(&request, OutboundSendMode::Batch), + Err(EdgeError::BadRequest { .. }) + )); + let streamed_upload = OutboundRequest::post("https://example.com/") + .expect("request") + .body(Body::from_stream(stream::pending())); + validate(&streamed_upload, OutboundSendMode::Send).expect("send accepts streamed upload"); + validate(&streamed_upload, OutboundSendMode::Follow) + .expect("follow accepts non-replayable upload"); + assert!(matches!( + validate(&streamed_upload, OutboundSendMode::Begin), + Err(EdgeError::BadRequest { .. }) + )); +} + +#[test] +#[expect( + clippy::tests_outside_test_module, + reason = "external head-first API contract" +)] +fn owned_head_first_facade_compile_contract() { + use edgezero_core::{HttpClient, OutboundDispatchRequest, OutboundFuture, OutboundPending}; + use std::time::Duration; + + fn owned_send( + client: &HttpClient, + request: OutboundRequest, + ) -> OutboundFuture { + client.send(request) + } + fn owned_begin( + client: &HttpClient, + request: OutboundRequest, + ) -> OutboundFuture { + client.begin(request) + } + fn inspect(request: &OutboundDispatchRequest) { + assert!(request.entered_at() <= request.budget().deadline.instant()); + assert!(request.request().uri().scheme().is_some()); + assert!(request.clock().now() <= request.time().clock().now()); + } + let _ = (owned_send, owned_begin, inspect); + let request = OutboundRequest::get("https://example.com/") + .expect("request") + .connect_timeout(Duration::from_millis(10)); + assert_eq!( + request.connect_timeout_setting(), + Some(Duration::from_millis(10)) + ); +} + +#[test] +#[expect( + clippy::tests_outside_test_module, + reason = "external redirect API contract" +)] +fn public_follow_and_option_readback_compile_contract() { + use std::rc::Rc; + + use edgezero_core::http::{HeaderMap, StatusCode, Uri}; + use edgezero_core::{ + HttpClient, OutboundFollowFailure, OutboundFollowResult, OutboundRedirectAction, + OutboundRedirectContext, OutboundRedirectLimit, + }; + use futures_util::future::LocalBoxFuture; + + fn owned_follow( + client: &HttpClient, + request: OutboundRequest, + rejection: Rc<()>, + ) -> LocalBoxFuture<'static, Result>>> { + client.follow( + request, + OutboundRedirectLimit::new(4).expect("limit"), + move |context: &mut OutboundRedirectContext<'_>| { + let _: ( + usize, + StatusCode, + &Uri, + &str, + &HeaderMap, + bool, + Option<&OutboundRequest>, + ) = ( + context.redirect_number(), + context.status(), + context.request_uri(), + context.resolved_target(), + context.headers(), + context.is_cross_origin(), + context.prepared_request(), + ); + match context.take_prepared_request() { + Some(prepared) => Ok(OutboundRedirectAction::Follow(prepared)), + None => Err(Rc::clone(&rejection)), + } + }, + ) + } + let _ = owned_follow; + assert_eq!(OutboundRedirectLimit::new(4).expect("limit").get(), 4); + for invalid in [0, 21] { + let error = OutboundRedirectLimit::new(invalid).expect_err("invalid limit"); + assert_eq!(error.status(), StatusCode::BAD_REQUEST); + } +} diff --git a/crates/edgezero-macros/src/app.rs b/crates/edgezero-macros/src/app.rs index f1dda1aee..6f0adc1c7 100644 --- a/crates/edgezero-macros/src/app.rs +++ b/crates/edgezero-macros/src/app.rs @@ -1,11 +1,13 @@ -use crate::manifest_definitions::{LogLevel, Manifest, ResolvedLoggingConfig, StoreDeclaration}; +use crate::manifest_definitions::{ + LogLevel, Manifest, ResolvedLoggingConfig, StoreDeclaration, reject_misplaced_capabilities, +}; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; use std::collections::BTreeSet; use std::env; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use syn::parse::{Parse, ParseStream}; use syn::{Ident, LitStr, Token, parse_macro_input}; use validator::Validate as _; @@ -13,6 +15,7 @@ use validator::Validate as _; #[derive(Debug)] struct AppArgs { app_ident: Option, + configure: Option, owns_logging: Option, path: LitStr, state: Option, @@ -22,6 +25,7 @@ impl Parse for AppArgs { fn parse(input: ParseStream) -> syn::Result { let path: LitStr = input.parse()?; let mut app_ident: Option = None; + let mut configure: Option = None; let mut owns_logging: Option = None; let mut state: Option = None; let mut seen_keyword = false; @@ -35,6 +39,15 @@ impl Parse for AppArgs { input.parse::()?; seen_keyword = true; match key.to_string().as_str() { + "configure" => { + if configure.is_some() { + return Err(syn::Error::new( + key.span(), + "duplicate `configure` argument", + )); + } + configure = Some(input.parse::()?); + } "owns_logging" => { if owns_logging.is_some() { return Err(syn::Error::new( @@ -55,7 +68,7 @@ impl Parse for AppArgs { return Err(syn::Error::new( key.span(), format!( - "unknown `app!` argument `{other}`; expected `state` or `owns_logging`" + "unknown `app!` argument `{other}`; expected `configure`, `state`, or `owns_logging`" ), )); } @@ -82,6 +95,7 @@ impl Parse for AppArgs { } Ok(Self { app_ident, + configure, owns_logging, path, state, @@ -124,6 +138,29 @@ fn build_stores_tokens(manifest: &Manifest) -> TokenStream2 { } } +fn build_configure_tokens(callback: Option<&syn::Expr>) -> TokenStream2 { + callback.map_or_else( + || { + quote! { + fn configure( + _app: &mut edgezero_core::app::App, + ) -> Result<(), edgezero_core::error::EdgeError> { + Ok(()) + } + } + }, + |configure| { + quote! { + fn configure( + app: &mut edgezero_core::app::App, + ) -> Result<(), edgezero_core::error::EdgeError> { + (#configure)(app) + } + } + }, + ) +} + fn logging_config_tokens(config: &ResolvedLoggingConfig) -> TokenStream2 { let level = match config.level { LogLevel::Trace => quote! { edgezero_core::manifest::LogLevel::Trace }, @@ -199,14 +236,75 @@ fn build_route_tokens(manifest: &Manifest) -> Result, String> }; let handler_path = parse_handler_path(handler)?; let path_lit = LitStr::new(&trigger.path, Span::call_site()); + let class_lit = trigger + .class + .as_deref() + .map(|class| LitStr::new(class, Span::call_site())); for method in trigger.methods() { - tokens.push(route_for_method(method, &path_lit, &handler_path)); + tokens.push(route_for_method( + method, + &path_lit, + class_lit.as_ref(), + &handler_path, + )); } } Ok(tokens) } +fn build_hooks_tokens( + app_ident: &Ident, + configure_tokens: &TokenStream2, + logging_tokens: &TokenStream2, + manifest_json_lit: &LitStr, + owns_logging: bool, + app_name_lit: &LitStr, + stores_tokens: &TokenStream2, +) -> TokenStream2 { + quote! { + pub struct #app_ident; + + impl edgezero_core::app::Hooks for #app_ident { + fn routes() -> Result { + build_router() + } + + #configure_tokens + + #logging_tokens + + fn manifest() -> ::edgezero_core::manifest::BakedManifest { + static CACHE: ::std::sync::OnceLock< + ::edgezero_core::manifest::BakedManifest, + > = ::std::sync::OnceLock::new(); + *CACHE.get_or_init(|| { + match ::manifest_json() { + None => ::edgezero_core::manifest::BakedManifest::Absent, + Some(json) => { + ::edgezero_core::manifest::Manifest::from_baked_json(json) + } + } + }) + } + + fn manifest_json() -> Option<&'static str> { + Some(#manifest_json_lit) + } + + fn owns_logging() -> bool { + #owns_logging + } + + fn name() -> &'static str { + #app_name_lit + } + + #stores_tokens + } + } +} + pub fn expand_app(input: TokenStream) -> TokenStream { let args = parse_macro_input!(input as AppArgs); @@ -219,18 +317,10 @@ pub fn expand_app(input: TokenStream) -> TokenStream { } }; - let mut manifest: Manifest = match toml::from_str(&manifest_source) { - Ok(parsed) => parsed, - Err(err) => { - let msg = format!("failed to parse {}: {err}", manifest_path.display()); - return quote!(compile_error!(#msg);).into(); - } + let manifest = match parse_manifest(&manifest_source, &manifest_path) { + Ok(manifest) => manifest, + Err(msg) => return quote!(compile_error!(#msg);).into(), }; - if let Err(err) = manifest.validate() { - let msg = format!("failed to validate {}: {err}", manifest_path.display()); - return quote!(compile_error!(#msg);).into(); - } - manifest.finalize(); let manifest_json = match serde_json::to_string(&manifest) { Ok(json) => json, @@ -264,62 +354,54 @@ pub fn expand_app(input: TokenStream) -> TokenStream { let manifest_path_lit = LitStr::new(&manifest_path.to_string_lossy(), Span::call_site()); let owns_logging_lit = args.owns_logging.unwrap_or(false); - // Emitted only when `state = ` is given; `Option: ToTokens` - // renders `None` as nothing, so an app without `state` is unchanged. + let configure_tokens = build_configure_tokens(args.configure.as_ref()); let state_call = args.state.as_ref().map(|state_expr| { quote! { builder = builder.with_state(#state_expr); } }); + let hooks_tokens = build_hooks_tokens( + &app_ident, + &configure_tokens, + &logging_tokens, + &manifest_json_lit, + owns_logging_lit, + &app_name_lit, + &stores_tokens, + ); - // The emitted `Hooks` impl below explicitly defines `configure`, - // `owns_logging`, and `build_app` even though their bodies mirror the trait - // defaults. This is required because `missing_trait_methods` (restriction = - // deny) forbids relying on trait defaults in the impl. If those `Hooks` - // defaults change, update these emitted bodies to match. let output = quote! { // Force a rebuild when the manifest file changes (include_bytes tracks it as a build input). const _: &[u8] = include_bytes!(#manifest_path_lit); - pub struct #app_ident; + #hooks_tokens - impl edgezero_core::app::Hooks for #app_ident { - fn routes() -> edgezero_core::router::RouterService { - build_router() - } - - fn configure(_app: &mut edgezero_core::app::App) {} - - fn owns_logging() -> bool { - #owns_logging_lit - } - - #logging_tokens - - fn name() -> &'static str { - #app_name_lit - } - - #stores_tokens - - fn build_app() -> edgezero_core::app::App { - let mut app = edgezero_core::app::App::with_name(Self::routes(), Self::name()); - Self::configure(&mut app); - app - } - } - - pub fn build_router() -> edgezero_core::router::RouterService { + pub fn build_router() -> Result { let mut builder = edgezero_core::router::RouterService::builder(); builder = builder.with_manifest_json(#manifest_json_lit); #state_call #(#middleware_tokens)* #(#route_tokens)* - builder.build() + Ok(builder.build()) } }; output.into() } +fn parse_manifest(source: &str, path: &Path) -> Result { + let value: toml::Value = toml::from_str(source) + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + reject_misplaced_capabilities(&value) + .map_err(|error| format!("failed to validate {}: {error}", path.display()))?; + let mut manifest: Manifest = value + .try_into() + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + manifest + .validate() + .map_err(|error| format!("failed to validate {}: {error}", path.display()))?; + manifest.finalize(); + Ok(manifest) +} + /// Parses a handler reference like `crate::handlers::root` from `edgezero.toml` /// into the `syn::ExprPath` that the generated router code references. /// @@ -370,7 +452,32 @@ fn resolve_manifest_path(relative: String) -> PathBuf { PathBuf::from(manifest_dir).join(relative) } -fn route_for_method(method: &str, path: &LitStr, handler: &syn::ExprPath) -> TokenStream2 { +fn route_for_method( + method: &str, + path: &LitStr, + class: Option<&LitStr>, + handler: &syn::ExprPath, +) -> TokenStream2 { + if let Some(route_class) = class { + let method_tokens = match method { + "GET" => quote! { edgezero_core::http::Method::GET }, + "POST" => quote! { edgezero_core::http::Method::POST }, + "PUT" => quote! { edgezero_core::http::Method::PUT }, + "DELETE" => quote! { edgezero_core::http::Method::DELETE }, + custom_method => { + let method_bytes = + syn::LitByteStr::new(custom_method.as_bytes(), Span::call_site()); + quote! { + edgezero_core::http::Method::from_bytes(#method_bytes) + .expect("invalid HTTP method in manifest") + } + } + }; + return quote! { + builder = builder.route_with_class(#path, #method_tokens, #route_class, #handler); + }; + } + match method { "GET" => quote! { builder = builder.get(#path, #handler); }, "POST" => quote! { builder = builder.post(#path, #handler); }, @@ -442,6 +549,25 @@ mod tests { assert_eq!(args.owns_logging, None); } + #[test] + fn app_args_parses_configure_expr() { + let args: AppArgs = + parse_str(r#""edgezero.toml", configure = crate::configure_app"#).expect("parse"); + let rendered = args.configure.map(|expr| quote::quote!(#expr).to_string()); + assert_eq!(rendered, Some("crate :: configure_app".to_owned())); + } + + #[test] + fn app_args_parses_configure_with_other_keywords() { + let args: AppArgs = parse_str( + r#""edgezero.toml", MyApp, state = crate::app_state(), configure = crate::configure_app, owns_logging = true"#, + ) + .expect("parse"); + assert!(args.configure.is_some()); + assert!(args.state.is_some()); + assert_eq!(args.owns_logging, Some(true)); + } + #[test] fn app_args_parses_state_with_app_ident_and_owns_logging() { let args: AppArgs = @@ -462,6 +588,18 @@ mod tests { assert!(err.to_string().contains("duplicate `state`"), "got: {err}"); } + #[test] + fn app_args_rejects_duplicate_configure() { + let err = parse_str::( + r#""edgezero.toml", configure = crate::first, configure = crate::second"#, + ) + .expect_err("duplicate configure"); + assert!( + err.to_string().contains("duplicate `configure`"), + "got: {err}" + ); + } + #[test] fn app_args_rejects_duplicate_key() { let err = @@ -492,6 +630,7 @@ mod tests { err.to_string().contains("unknown `app!` argument `bogus`"), "got: {err}" ); + assert!(err.to_string().contains("`configure`"), "got: {err}"); } #[test] @@ -560,6 +699,31 @@ handler = "crate::handlers::root" assert_eq!(tokens.len(), 3); } + #[test] + fn build_route_tokens_propagates_manifest_route_class() { + let manifest: Manifest = toml::from_str( + r#" +[app] +name = "demo" +entry = "crates/demo-core" + +[[triggers.http]] +class = "auction" +path = "/bid" +methods = ["GET", "POST"] +handler = "crate::handlers::bid" +"#, + ) + .expect("manifest TOML should parse"); + let tokens = build_route_tokens(&manifest).expect("valid manifest builds routes"); + assert_eq!(tokens.len(), 2); + for token in tokens { + let rendered = token.to_string(); + assert!(rendered.contains("route_with_class"), "{rendered}"); + assert!(rendered.contains("auction"), "{rendered}"); + } + } + #[test] fn build_route_tokens_skips_trigger_without_handler() { let manifest: Manifest = toml::from_str( diff --git a/crates/edgezero-macros/tests/app_macro.rs b/crates/edgezero-macros/tests/app_macro.rs index f32cf2d5c..24e05bb84 100644 --- a/crates/edgezero-macros/tests/app_macro.rs +++ b/crates/edgezero-macros/tests/app_macro.rs @@ -12,14 +12,23 @@ edgezero_core::app!( #[cfg(test)] mod tests { + use edgezero_core::EdgeError; use edgezero_core::app::Hooks as _; use edgezero_core::manifest::LogLevel; + use edgezero_core::router::RouterService; #[test] fn app_macro_emits_owns_logging_true() { assert!(super::OwnedLoggingApp::owns_logging()); } + #[test] + fn app_macro_routes_and_generated_builder_are_fallible() { + let routes: Result = super::OwnedLoggingApp::routes(); + let built: Result = super::build_router(); + assert_eq!(routes.unwrap().routes(), built.unwrap().routes()); + } + #[test] fn app_macro_bakes_adapter_logging_from_the_manifest() { let logging = super::OwnedLoggingApp::logging_for("FASTLY"); @@ -33,3 +42,212 @@ mod tests { assert!(missing.echo_stdout.is_none()); } } + +#[cfg(test)] +mod configured_app { + use edgezero_core::app::App; + use edgezero_core::body::Body; + use edgezero_core::config_store::ConfigExtractionLimits; + use edgezero_core::http::{HeaderMap, Method, Response, StatusCode, Uri, Version}; + use edgezero_core::ingress::{AdmissionDecision, IngressBeginOutcome, IngressHeadParts}; + use edgezero_core::response_egress::ResponseEgressCompletion; + use edgezero_core::response_egress_framing::ResponseEgressFramingMode; + use edgezero_core::time::MonotonicInstant; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + static STATE_BUILDS: AtomicUsize = AtomicUsize::new(0); + + fn app_state() -> Arc { + STATE_BUILDS.fetch_add(1, Ordering::SeqCst); + Arc::new(AtomicUsize::new(0)) + } + + fn configure_app(app: &mut App) -> Result<(), edgezero_core::EdgeError> { + app.state::>() + .ok_or_else(|| edgezero_core::EdgeError::service_unavailable("missing app state"))? + .fetch_add(1, Ordering::SeqCst); + app.set_config_extraction_limits(ConfigExtractionLimits::default())?; + app.set_ingress_admission_policy(|_| { + let mut response = Response::new(Body::empty()); + *response.status_mut() = StatusCode::TOO_MANY_REQUESTS; + AdmissionDecision::Refuse { + completion: ResponseEgressCompletion::empty(), + response, + } + }); + Ok(()) + } + + edgezero_core::app!( + "tests/fixtures/owns_logging.toml", + ConfiguredApp, + configure = configure_app, + state = app_state() + ); + + #[test] + fn app_macro_configure_callback_installs_ingress_policy() { + let app = App::build::(edgezero_core::PlatformMetadata::default()) + .expect("configured app"); + assert_eq!(STATE_BUILDS.load(Ordering::SeqCst), 1); + assert_eq!( + app.state::>() + .unwrap() + .load(Ordering::SeqCst), + 1 + ); + let head = IngressHeadParts::new( + Method::GET, + Uri::from_static("/"), + Version::HTTP_11, + HeaderMap::new(), + ); + let outcome = app + .begin_ingress(head, MonotonicInstant::now()) + .expect("begin ingress"); + let IngressBeginOutcome::Refused(response) = outcome else { + panic!("configured policy must refuse ingress"); + }; + let Ok((prepared, _, mut attempt, clock)) = + response.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("begin response egress"); + }; + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + assert_eq!( + prepared.into_response().status(), + StatusCode::TOO_MANY_REQUESTS + ); + } +} + +#[cfg(test)] +mod fallible_state_app { + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + use edgezero_core::app::App; + use edgezero_core::http::StatusCode; + use edgezero_core::{EdgeError, PlatformMetadata}; + + static CONFIGURATIONS: AtomicUsize = AtomicUsize::new(0); + static STATE_ATTEMPTS: AtomicUsize = AtomicUsize::new(0); + + fn app_state() -> Result, EdgeError> { + if STATE_ATTEMPTS.fetch_add(1, Ordering::SeqCst) == 0 { + return Err(EdgeError::service_unavailable("state unavailable")); + } + Ok(Arc::new(AtomicUsize::new(0))) + } + + fn configure_app(app: &mut App) -> Result<(), EdgeError> { + let state = app + .state::>() + .ok_or_else(|| EdgeError::service_unavailable("missing state"))?; + state.fetch_add(1, Ordering::SeqCst); + CONFIGURATIONS.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + edgezero_core::app!( + "tests/fixtures/owns_logging.toml", + FallibleStateApp, + configure = configure_app, + state = app_state()? + ); + + #[test] + fn fallible_macro_state_skips_configuration_and_retries_without_caching_failure() { + let Err(error) = App::build::(PlatformMetadata::default()) else { + panic!("failed state must prevent app assembly"); + }; + assert_eq!(error.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(error.message(), "state unavailable"); + assert_eq!(CONFIGURATIONS.load(Ordering::SeqCst), 0); + let app = App::build::(PlatformMetadata::default()).unwrap(); + assert_eq!( + app.state::>() + .unwrap() + .load(Ordering::SeqCst), + 1 + ); + assert_eq!(STATE_ATTEMPTS.load(Ordering::SeqCst), 2); + assert_eq!(CONFIGURATIONS.load(Ordering::SeqCst), 1); + } +} + +#[cfg(test)] +mod failing_configured_app { + use edgezero_core::app::App; + use edgezero_core::error::EdgeError; + use edgezero_core::http::StatusCode; + + fn configure_app(_app: &mut App) -> Result<(), EdgeError> { + Err(EdgeError::service_unavailable("configuration unavailable")) + } + + edgezero_core::app!( + "tests/fixtures/owns_logging.toml", + FailingConfiguredApp, + configure = configure_app + ); + + #[test] + fn app_macro_propagates_configuration_failure() { + let Err(error) = + App::build::(edgezero_core::PlatformMetadata::default()) + else { + panic!("configuration failure must stop application assembly"); + }; + assert_eq!(error.status(), StatusCode::SERVICE_UNAVAILABLE); + } +} + +#[cfg(test)] +mod platform_configured_app { + use std::num::NonZeroU32; + + use edgezero_core::app::App; + use edgezero_core::{ + EdgeError, HostIngressMemoryAccounting, InboundRequestPopulationBound, MemoryCeiling, + MemoryCeilingScope, PlatformFact, PlatformMetadata, PlatformResourceSource, + }; + + const SOURCE: PlatformResourceSource = PlatformResourceSource::PlatformLimit { + provider: "macro-test", + }; + const TEST_PLATFORM: PlatformMetadata = PlatformMetadata::new( + PlatformFact::known( + MemoryCeiling::new(64_000_000, MemoryCeilingScope::PerExecution, None), + SOURCE, + ), + PlatformFact::known(InboundRequestPopulationBound::new(NonZeroU32::MIN), SOURCE), + PlatformFact::known(HostIngressMemoryAccounting::OutsideCeiling, SOURCE), + ); + + fn configure_app(app: &mut App) -> Result<(), EdgeError> { + if app.platform() == TEST_PLATFORM { + Ok(()) + } else { + Err(EdgeError::service_unavailable( + "platform metadata unavailable during configuration", + )) + } + } + + edgezero_core::app!( + "tests/fixtures/owns_logging.toml", + PlatformConfiguredApp, + configure = configure_app + ); + + #[test] + fn app_macro_installs_platform_metadata_before_configuration() { + let app = App::build::(TEST_PLATFORM) + .expect("platform-aware configured app"); + + assert_eq!(app.platform(), TEST_PLATFORM); + } +} diff --git a/crates/edgezero-macros/tests/nested_secrets_e2e.rs b/crates/edgezero-macros/tests/nested_secrets_e2e.rs index 5f8a7d1f7..e9876948c 100644 --- a/crates/edgezero-macros/tests/nested_secrets_e2e.rs +++ b/crates/edgezero-macros/tests/nested_secrets_e2e.rs @@ -14,7 +14,9 @@ use async_trait::async_trait; use edgezero_core::blob_envelope::BlobEnvelope; use edgezero_core::body::Body; -use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; +use edgezero_core::config_store::{ + BoundedStoreRead, ConfigStore, ConfigStoreError, ConfigStoreHandle, +}; use edgezero_core::context::RequestContext; use edgezero_core::extractor::{AppConfig as AppConfigExtractor, FromRequest as _}; use edgezero_core::http::{Method, request_builder}; @@ -23,6 +25,7 @@ use edgezero_core::secret_store::{InMemorySecretStore, SecretHandle}; use edgezero_core::store_registry::{ BoundSecretStore, ConfigRegistry, ConfigStoreBinding, SecretRegistry, StoreRegistry, }; +use edgezero_core::time::{Deadline, MonotonicClock}; use futures::executor::block_on; use std::collections::BTreeMap; use std::sync::Arc; @@ -80,8 +83,39 @@ struct BlobStore(String); #[async_trait(?Send)] impl ConfigStore for BlobStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.clone())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.clone().into())) + } + + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + let value = self.get(key).await?; + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + let backend_bytes = value.as_ref().map_or(Ok(0_u64), |stored_value| { + u64::try_from(stored_value.len()) + .map_err(|_length_error| ConfigStoreError::ValueTooLarge) + })?; + if backend_bytes > max_backend_bytes || backend_bytes > max_value_bytes { + return Err(ConfigStoreError::ValueTooLarge); + } + Ok(BoundedStoreRead { + backend_bytes, + value, + }) } } diff --git a/crates/edgezero-test-harness/Cargo.lock b/crates/edgezero-test-harness/Cargo.lock new file mode 100644 index 000000000..4ebccad3a --- /dev/null +++ b/crates/edgezero-test-harness/Cargo.lock @@ -0,0 +1,204 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "edgezero-test-harness" +version = "0.1.0" +dependencies = [ + "ctrlc", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/crates/edgezero-test-harness/Cargo.toml b/crates/edgezero-test-harness/Cargo.toml new file mode 100644 index 000000000..a78f53aaa --- /dev/null +++ b/crates/edgezero-test-harness/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "edgezero-test-harness" +version = "0.1.0" +edition = "2024" +publish = false + +[workspace] +resolver = "2" + +[dependencies] +ctrlc = { version = "3.5.2", features = ["termination"] } +serde_json = "1" + +[lints.rust] +unsafe_code = "deny" diff --git a/crates/edgezero-test-harness/README.md b/crates/edgezero-test-harness/README.md new file mode 100644 index 000000000..067b56072 --- /dev/null +++ b/crates/edgezero-test-harness/README.md @@ -0,0 +1,17 @@ +# EdgeZero Test Harness + +Standalone, non-published native Rust runner for cross-adapter HTTP integration tests. +Its workspace and lockfile are isolated from production crates and deployable fixture +applications. Unit and adapter contract tests remain in their owning crates. + +```sh +cargo test --offline --locked --manifest-path crates/edgezero-test-harness/Cargo.toml +scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-wire +scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-lifetimes +``` + +The shell entrypoint selects this runner; fixture applications, their target directory, +runtime configuration and per-run evidence remain under `tests/fixtures/reusable-app`. +Runs record both the fixture lock and the separate harness lock. See the +[fixture guide](../../tests/fixtures/reusable-app/README.md) for runtime tooling, +suite-specific assertions and qualification boundaries. diff --git a/tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs b/crates/edgezero-test-harness/src/evidence.rs similarity index 64% rename from tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs rename to crates/edgezero-test-harness/src/evidence.rs index a139f9b83..c71332bb7 100644 --- a/tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs +++ b/crates/edgezero-test-harness/src/evidence.rs @@ -1,6 +1,15 @@ use serde_json::{Value, json}; use std::collections::{BTreeMap, HashMap, HashSet}; +mod readiness; +pub use readiness::{ + p5_cases, readiness_client_legs, readiness_legs, readiness_progress_legs, + validate_outbound_readiness, validate_outbound_readiness_client, + validate_outbound_readiness_client_probe, validate_outbound_readiness_progress, +}; +mod timers; +pub use timers::{timer_legs, validate_timer_controls}; + pub type Result = std::result::Result>; pub fn require(condition: bool, message: impl Into) -> Result<()> { if condition { @@ -9,6 +18,82 @@ pub fn require(condition: bool, message: impl Into) -> Result<()> { Err(message.into().into()) } } +pub fn validate_outbound_progress(probe: &Value, receipts: &[Value]) -> Result<()> { + require( + probe["pause_ns"] + .as_u64() + .is_some_and(|ns| ns >= 100_000_000), + "unrelated pause shorter than 100ms or unmeasured", + )?; + require( + matches!( + probe["adapter"].as_str(), + Some("axum" | "cloudflare" | "fastly" | "spin") + ), + "unknown or missing progress adapter", + )?; + require( + probe["protocol"] == "http1", + "unsupported or unobserved protocol", + )?; + require( + probe["consumer_polls_before_window_end"] == 0, + "consumer polled during window", + )?; + let case = probe["case"].as_str().ok_or("missing progress case")?; + let slots = match case { + "eager-single" | "lazy-single" => 1, + "eager-batch" | "response-owned" | "baseline-batch" => 2, + _ => return Err("unknown progress case".into()), + }; + require(probe["slots"] == slots, "wrong slot count")?; + let token = probe["token"].as_str().ok_or("missing probe token")?; + require( + !token.is_empty() + && token + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-'), + "invalid probe token", + )?; + let prefix = format!("/g3/{token}/"); + require( + receipts.len() == slots + 2, + "missing or extra origin receipts", + )?; + let one = |suffix: &str| -> Result { + let path = format!("{prefix}{suffix}"); + let matching = receipts + .iter() + .filter(|r| r["path"] == path) + .collect::>(); + require( + matching.len() == 1, + "missing, duplicate or foreign origin receipt", + )?; + matching[0]["received_ns"] + .as_u64() + .ok_or_else(|| "missing origin timestamp".into()) + }; + let start = one("start")?; + let end = one("end")?; + require( + end.saturating_sub(start) >= 100_000_000, + "unrelated wait shorter than 100ms", + )?; + let eager = case != "lazy-single" && (case != "baseline-batch" || probe["adapter"] == "fastly"); + for slot in 0..slots { + let received = one(&format!("slot-{slot}"))?; + require( + if eager { + received >= start && received - start < 100_000_000 + } else { + received >= end + }, + "origin receipt did not match eager/lazy dispatch window", + )?; + } + Ok(()) +} pub fn validate_shared_state(guest: &Value, retained: bool) -> Result<()> { let expected = if retained { guest["ordinal"].as_u64() @@ -77,6 +162,64 @@ pub fn validate_negative_logging(replies: &[Value], starts: &[Value]) -> Result< } Ok(if reused { "pass" } else { "unverified" }) } +pub fn validate_fastly_delivery_report( + token: &str, + events: &[Value], + expected: &Value, +) -> Result<()> { + let reports = events + .iter() + .filter(|event| event["event"] == "delivery_report" && event["token"] == token) + .collect::>(); + require(reports.len() == 1, "missing or duplicate delivery report")?; + let report = reports[0]; + for key in [ + "disposition", + "commitment", + "body_kind", + "outcome", + "fallback", + "may_send_native", + ] { + require( + expected.get(key).is_some(), + "incomplete delivery expectation", + )?; + require( + report.get(key) == expected.get(key), + format!("delivery report {key} mismatch"), + )?; + } + if expected["disposition"] == "no-attempt" { + require( + expected.get("no_attempt_reason").is_some() + && report.get("no_attempt_reason") == expected.get("no_attempt_reason"), + "delivery report no-attempt reason mismatch", + )?; + } + let attempted = matches!( + expected["disposition"].as_str(), + Some("routed" | "detached") + ); + let terminals = events + .iter() + .filter(|event| event["event"] == "egress_terminal" && event["token"] == token) + .collect::>(); + require( + terminals.len() == usize::from(attempted), + "delivery completion count mismatch", + )?; + if attempted { + for key in ["outcome", "body_kind", "fallback", "bytes_written"] { + require( + report.get(key).is_some() && report.get(key) == terminals[0].get(key), + format!("delivery/completion {key} mismatch"), + )?; + } + } + Ok(()) +} + pub fn validate_post_send(token: &str, receipts: &[String], events: &[Value]) -> Result<()> { require( receipts.contains(&format!("/post-send?token={token}")), @@ -384,6 +527,147 @@ pub fn summarize(records: &[Value]) -> Value { #[cfg(test)] mod tests { use super::*; + + #[test] + fn outbound_evidence_assembly_retains_case_counts_and_hashes() -> Result<()> { + let directory = std::env::temp_dir().join(format!("outbound-record-{}", crate::token())); + std::fs::create_dir(&directory)?; + let run = || -> Result<()> { + let repository = crate::root().join("../../..").canonicalize()?; + let script = repository.join("scripts/check_outbound_cutover_evidence.mjs"); + let inventory: Value = serde_json::from_slice(&std::fs::read( + repository.join("tests/fixtures/reusable-app/outbound-cutover-cases.json"), + )?)?; + let candidate = "a".repeat(40); + let build = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"; + let raw_hash = "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; + std::fs::write(directory.join("app.bin"), b"abc")?; + let mut results = Vec::new(); + for (index, case) in inventory["cases"] + .as_array() + .ok_or("missing inventory cases")? + .iter() + .take(2) + .enumerate() + { + let path = + directory.join(case["artifact_path"].as_str().ok_or("missing artifact")?); + std::fs::create_dir_all(path.parent().ok_or("missing artifact parent")?)?; + std::fs::write(path, b"{}")?; + results.push(json!({ + "case_id":case["case_id"], "environment":case["environment"], + "candidate_sha":candidate, "build_id":build, + "source_digest":case["source_digest"], "command":case["command"], + "sentinel":case["sentinel"], "artifact_path":case["artifact_path"], + "status":if index == 0 {"failed"} else {"pending"}, + "samples":if index == 0 {3} else {0}, + "assertions":[{"name":"synthetic negative control", "passed":false}], + "measurements":[] + })); + } + let input = json!({"schema":1, "candidate_sha":candidate, + "source_state":"working-tree", "qualification":false, + "builds":[{"build_id":build,"candidate_sha":candidate,"artifact_path":"app.bin"}], + "results":results}); + let input_path = directory.join("input.json"); + let output_path = directory.join("draft.json"); + std::fs::write(&input_path, serde_json::to_vec(&input)?)?; + let invoke = || { + std::process::Command::new("node") + .arg(&script) + .args(["--assemble", "--candidate", &candidate, "--input"]) + .arg(&input_path) + .arg("--artifacts") + .arg(&directory) + .arg("--output") + .arg(&output_path) + .output() + }; + let output = invoke()?; + require( + output.status.code() == Some(1) && output_path.is_file(), + format!( + "draft assembly did not expose blocked qualification: {}", + String::from_utf8_lossy(&output.stderr) + ), + )?; + let bytes = std::fs::read(&output_path)?; + let draft: Value = serde_json::from_slice(&bytes)?; + require( + draft["candidate_sha"] == candidate && draft["builds"][0]["build_id"] == build, + "assembly changed candidate or build identity", + )?; + require( + draft["qualified"] == false + && draft["stage"] == "pre-freeze" + && draft["qualification"] == false + && draft["source_state"] == "working-tree", + "assembly promoted development evidence", + )?; + require( + draft["assembly"] + == json!({"observed_legs":2,"passed":0,"failed":1,"pending":1,"samples":3}), + "assembly lost outcome or sample counts", + )?; + for (index, result) in draft["results"] + .as_array() + .ok_or("missing results")? + .iter() + .enumerate() + { + require( + result["artifact_sha256"] == raw_hash + && result["build_id"] == build + && result["candidate_sha"] == candidate + && result["case_id"] == input["results"][index]["case_id"] + && result["samples"] == input["results"][index]["samples"], + "assembly changed case counts, provenance or raw hashes", + )?; + } + require( + !invoke()?.status.success() && std::fs::read(&output_path)? == bytes, + "assembly overwrote existing evidence", + )?; + let report = std::process::Command::new("node") + .arg(&script) + .args(["--report", "--candidate", &candidate, "--record"]) + .arg(&output_path) + .arg("--artifacts") + .arg(&directory) + .output()?; + require( + report.status.code() == Some(1), + "report lost blocked exit status", + )?; + let report: Value = serde_json::from_slice(&report.stdout)?; + require( + report["status"] == "blocked" + && report["qualified"] == false + && report["passed"] == 0 + && report["findings"] + .as_array() + .is_some_and(|findings| findings.len() > 2) + && std::fs::read(&output_path)? == bytes, + "report omitted independent gates or modified evidence", + )?; + let release = std::process::Command::new("node") + .arg(&script) + .args(["--release", "--candidate", &candidate, "--record"]) + .arg(&output_path) + .arg("--artifacts") + .arg(&directory) + .output()?; + require( + !release.status.success() + && String::from_utf8_lossy(&release.stderr).contains("pre-freeze"), + "pre-freeze draft passed release validation", + ) + }; + let result = run(); + std::fs::remove_dir_all(directory)?; + result + } + #[test] fn missing_observation_is_unknown_and_mixed_builds_are_rejected() { let summary = summarize(&[ @@ -614,6 +898,55 @@ mod tests { "pass" ); } + #[test] + fn delivery_reports_require_exactly_one_matching_completion() { + let expected = json!({"disposition":"routed","commitment":"Committed", "body_kind":"Application", + "outcome":"SourceError","fallback":null,"may_send_native":false}); + let mut delivery = expected.clone(); + delivery["event"] = json!("delivery_report"); + delivery["token"] = json!("x"); + delivery["bytes_written"] = json!(7); + let terminal = json!({"event":"egress_terminal","token":"x","body_kind":"Application", + "outcome":"SourceError","fallback":null,"bytes_written":7}); + assert!( + validate_fastly_delivery_report("x", &[delivery.clone(), terminal.clone()], &expected) + .is_ok() + ); + assert!(validate_fastly_delivery_report("x", &[delivery.clone()], &expected).is_err()); + assert!( + validate_fastly_delivery_report( + "x", + &[delivery.clone(), delivery.clone(), terminal.clone()], + &expected + ) + .is_err() + ); + for key in [ + "commitment", + "body_kind", + "outcome", + "fallback", + "may_send_native", + ] { + let mut changed = delivery.clone(); + changed[key] = json!("wrong"); + assert!( + validate_fastly_delivery_report("x", &[changed, terminal.clone()], &expected) + .is_err() + ); + } + let expected = json!({"disposition":"no-attempt","commitment":null,"body_kind":null, + "outcome":null,"fallback":null,"may_send_native":true,"no_attempt_reason":"required-store"}); + let mut no_attempt = expected.clone(); + no_attempt["event"] = json!("delivery_report"); + no_attempt["token"] = json!("x"); + assert!(validate_fastly_delivery_report("x", &[no_attempt.clone()], &expected).is_ok()); + let mut wrong_cause = no_attempt.clone(); + wrong_cause["no_attempt_reason"] = json!("request-head-conversion"); + assert!(validate_fastly_delivery_report("x", &[wrong_cause], &expected).is_err()); + assert!(validate_fastly_delivery_report("x", &[no_attempt, terminal], &expected).is_err()); + } + #[test] fn post_send_requires_both_receipts() { assert!(validate_post_send("x", &[], &[]).is_err()); @@ -646,3 +979,61 @@ mod tests { assert!(validate_limit_summaries(&events).is_err()); } } + +#[cfg(test)] +mod progress_tests { + use super::*; + + #[test] + fn outbound_progress_probe_requires_origin_receipts() { + let probe = json!({"adapter":"axum", "case":"eager-batch", "token":"one", "protocol":"http1", + "slots":2, "consumer_polls_before_window_end":0,"pause_ns":100_000_000}); + assert!(validate_outbound_progress(&probe, &[]).is_err()); + let events = vec![ + json!({"path":"/g3/one/start", "received_ns":0}), + json!({"path":"/g3/one/slot-0", "received_ns":10_000_000}), + json!({"path":"/g3/one/slot-1", "received_ns":20_000_000}), + json!({"path":"/g3/one/end", "received_ns":110_000_000}), + ]; + validate_outbound_progress(&probe, &events).unwrap(); + for mutation in [ + "missing", + "duplicate", + "late", + "foreign", + "protocol", + "polled", + "adapter", + "pause", + ] { + let mut events = events.clone(); + let mut probe = probe.clone(); + match mutation { + "missing" => { + events.remove(1); + } + "duplicate" => events.push(events[1].clone()), + "late" => events[1]["received_ns"] = json!(105_000_000), + "foreign" => events[1]["path"] = json!("/g3/another/slot-0"), + "protocol" => probe["protocol"] = json!("unsupported"), + "polled" => probe["consumer_polls_before_window_end"] = json!(1), + "adapter" => probe["adapter"] = json!("unknown"), + "pause" => probe["pause_ns"] = json!(0), + _ => unreachable!(), + } + assert!( + validate_outbound_progress(&probe, &events).is_err(), + "{mutation}" + ); + } + let mut lazy = probe; + lazy["case"] = json!("lazy-single"); + lazy["slots"] = json!(1); + let events = vec![ + events[0].clone(), + events[3].clone(), + json!({"path":"/g3/one/slot-0", "received_ns":120_000_000}), + ]; + validate_outbound_progress(&lazy, &events).unwrap(); + } +} diff --git a/crates/edgezero-test-harness/src/evidence/readiness.rs b/crates/edgezero-test-harness/src/evidence/readiness.rs new file mode 100644 index 000000000..26c00469d --- /dev/null +++ b/crates/edgezero-test-harness/src/evidence/readiness.rs @@ -0,0 +1,2751 @@ +use super::{Result, require}; +use serde_json::Value; +use std::collections::HashSet; + +const CASES: [&str; 11] = [ + "truncated", + "silent-head", + "pending-body", + "trickle", + "siblings", + "exhausted", + "eof", + "empty-eof", + "malformed-head", + "malformed-body", + "head-error", +]; + +#[derive(Clone, Copy, Debug)] +pub struct ReadinessLeg { + pub case: &'static str, + pub mode: &'static str, + pub consumption: &'static str, + pub read_limit: usize, + pub repetition: usize, +} + +impl ReadinessLeg { + pub fn key(&self) -> String { + format!( + "{}/{}/{}/{}/{}", + self.case, self.mode, self.consumption, self.read_limit, self.repetition + ) + } +} + +pub fn readiness_legs() -> Vec { + let mut legs = Vec::new(); + for repetition in 0..3 { + legs.push(ReadinessLeg { + case: "truncated", + mode: "direct", + consumption: "buffered", + read_limit: 1, + repetition, + }); + for case in CASES { + for consumption in ["buffered", "streamed"] { + for read_limit in [1, 16_384] { + legs.push(ReadinessLeg { + case, + mode: "selected", + consumption, + read_limit, + repetition, + }); + } + } + } + } + legs +} + +pub fn readiness_progress_legs() -> Vec { + let mut legs = Vec::new(); + for repetition in 0..3 { + for case in ["mixed-head-body", "mixed-body-head", "full-body-error"] { + for consumption in ["buffered", "streamed"] { + legs.push(ReadinessLeg { + case, + mode: "selected", + consumption, + read_limit: 16_384, + repetition, + }); + } + } + } + legs +} + +pub fn readiness_client_legs() -> Vec { + let mut legs = Vec::new(); + for repetition in 0..3 { + for (case, consumption) in [ + ("mixed-head-body", "buffered"), + ("mixed-body-head", "buffered"), + ("full-body-error", "buffered"), + ("full-body-error", "streamed"), + ] { + legs.push(ReadinessLeg { + case, + mode: "client", + consumption, + read_limit: 16_384, + repetition, + }); + } + } + legs +} + +fn client_leg(probe: &Value) -> Result { + readiness_client_legs() + .into_iter() + .find(|leg| { + probe["case"] == leg.case + && probe["consumption"] == leg.consumption + && probe["repetition"] == leg.repetition + }) + .ok_or_else(|| "unknown actual-client leg".into()) +} + +pub fn validate_outbound_readiness_client_probe(probe: &Value, revision: &str) -> Result<()> { + if probe["event"] == "fastly-p5-probe" { + return validate_p5_probe(probe, revision); + } + let leg = client_leg(probe)?; + let token = probe["token"] + .as_str() + .ok_or("missing actual-client token")?; + require( + revision.len() == 40 + && revision.bytes().all(|b| b.is_ascii_hexdigit()) + && probe["event"] == "fastly-client-progress" + && probe["revision"] == revision + && probe["qualification"] == false + && !token.is_empty() + && token.len() <= 128 + && token + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-'), + "invalid actual-client identity", + )?; + origin(probe, slot_count(leg.case))?; + let completed = probe["completed_us"] + .as_u64() + .ok_or("missing actual-client completion")?; + let envelope = probe["result"] + .as_object() + .ok_or("missing actual-client result")?; + require(envelope.len() == 1, "contradictory actual-client result")?; + let result = envelope + .get("Ok") + .filter(|result| result.is_object()) + .ok_or("actual-client operation did not complete its evidence")?; + if leg.case == "full-body-error" { + require( + result["kind"] == "error" + && result["failure"]["category"] == "bad_gateway" + && result["failure"]["source_unexpected_eof"] == true, + "actual-client typed UnexpectedEof failure was lost", + )?; + let wanted = if leg.consumption == "streamed" { + payload(leg.case, 0) + } else { + b"" + }; + require( + result["bytes"] == wanted.len() && result["checksum"] == fingerprint(wanted), + "wrong actual-client error prefix bytes/checksum", + )?; + } else { + let slots = result["slots"] + .as_array() + .ok_or("missing actual-client slots")?; + require( + result["termination"] == "Cutoff" && slots.len() == 2, + "wrong actual-client mixed termination/slot count", + )?; + require( + slots[0] == serde_json::json!({"kind":"pending"}), + "actual-client pending slot was replaced", + )?; + let ready = &slots[1]; + let wanted = payload(leg.case, 1); + require( + ready["kind"] == "ok" + && ready["failure"].is_null() + && ready["bytes"] == wanted.len() + && ready["checksum"] == fingerprint(wanted) + && ready["elapsed_us"] + .as_u64() + .is_some_and(|elapsed| elapsed < 500_000 && elapsed <= completed), + "actual-client ready sibling is missing, late or has wrong bytes/checksum", + )?; + } + Ok(()) +} + +pub fn validate_outbound_readiness_client(probes: &[Value], revision: &str) -> Result<()> { + if probes + .iter() + .any(|probe| probe["event"] == "fastly-p5-probe") + { + require( + probes.len() == p5_cases().len(), + "missing P5 case inventory", + )?; + let mut cases = HashSet::new(); + let mut tokens = HashSet::new(); + for probe in probes { + validate_p5_probe(probe, revision)?; + require( + cases.insert(probe["case"].as_str()) && tokens.insert(probe["token"].as_str()), + "duplicate P5 case/token", + )?; + } + return Ok(()); + } + require( + probes.len() == readiness_client_legs().len(), + "missing actual-client case inventory", + )?; + let mut identities = HashSet::new(); + let mut tokens = HashSet::new(); + let failures: Vec<_> = probes + .iter() + .enumerate() + .filter_map(|(index, probe)| { + let validation = (|| -> Result<()> { + validate_outbound_readiness_client_probe(probe, revision)?; + require( + identities.insert(client_leg(probe)?.key()) + && tokens.insert( + probe["token"] + .as_str() + .ok_or("missing actual-client token")?, + ), + "duplicate actual-client identity/token", + ) + })(); + validation.err().map(|error| { + let key = + client_leg(probe).map_or_else(|_| format!("row {index}"), |leg| leg.key()); + format!("{key}: {error}") + }) + }) + .collect(); + require( + failures.is_empty(), + format!( + "{} actual-client leg(s) failed:\n{}", + failures.len(), + failures.join("\n") + ), + ) +} + +pub fn p5_cases() -> Vec { + let mut cases: Vec<_> = [ + "two-origin", + "pending-head", + "optimizer-send", + "optimizer-initial", + "optimizer-next", + ] + .into_iter() + .map(str::to_owned) + .collect(); + for coding in ["identity", "gzip", "passthrough"] { + for mode in ["buffered", "streamed"] { + for size in ["ten", "eleven", "declared"] { + cases.push(format!("parity/{coding}/{mode}/{size}")); + } + } + } + cases +} + +fn validate_p5_probe(probe: &Value, revision: &str) -> Result<()> { + let case = probe["case"].as_str().ok_or("missing P5 case")?; + let token = probe["token"].as_str().ok_or("missing P5 token")?; + require( + probe["event"] == "fastly-p5-probe" + && p5_cases().iter().any(|known| known == case) + && revision.len() == 40 + && revision.bytes().all(|byte| byte.is_ascii_hexdigit()) + && probe["revision"] == revision + && probe["qualification"] == false + && probe["response_decoding"] == "Decode" + && !token.is_empty() + && token.len() <= 128 + && token + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-'), + "invalid P5 identity", + )?; + let origin = &probe["origin"]; + let a = origin["a_port"] + .as_u64() + .filter(|port| (1..=65535).contains(port)) + .ok_or("invalid P5 origin A")?; + let b = origin["b_port"] + .as_u64() + .filter(|port| (1..=65535).contains(port)) + .ok_or("invalid P5 origin B")?; + require(a != b, "P5 origins are not distinct")?; + let shape = match case { + "two-origin" | "optimizer-next" => "redirect", + "pending-head" => "pending", + "optimizer-send" | "optimizer-initial" => "image", + _ => case, + }; + let path = format!("/p5/{token}/{b}/{shape}"); + let final_path = format!("/p5/{token}/{b}/final"); + let initial = format!("http://127.0.0.1:{a}{path}"); + let final_uri = format!("http://127.0.0.1:{b}{final_path}"); + require( + probe["a_uri"] == initial && probe["b_uri"] == final_uri, + "foreign P5 target", + )?; + let (a_count, b_count) = match case { + "optimizer-send" | "optimizer-initial" => (0, 0), + "optimizer-next" => (1, 0), + "two-origin" | "pending-head" => (2, 1), + _ => (2, 0), + }; + require( + origin["a"] == serde_json::json!(vec![&path; a_count]) + && origin["b"] == serde_json::json!(vec![&final_path; b_count]) + && origin["pending_released_before_reply"] == false + && origin["pending_heads"] == if case == "pending-head" { 2 } else { 0 }, + "missing, duplicate or foreign P5 receipts/pending-head evidence", + )?; + let envelope = probe["result"].as_object().ok_or("missing P5 result")?; + require(envelope.len() == 1, "contradictory P5 result")?; + let result = envelope + .get("Ok") + .filter(|value| value.is_object()) + .ok_or("P5 guest failed")?; + let follow_metadata = |value: &Value, redirects| -> Result<()> { + require( + value["final_uri"] + == if redirects == 1 { + final_uri.as_str() + } else { + initial.as_str() + } + && value["redirects"] == redirects + && value.get("stop_reason") == Some(&Value::Null) + && value["policy_calls"] == redirects + && value.get("hop").is_none(), + "wrong P5 follow metadata/policy count", + ) + }; + let followed = |value: &Value, redirects, streamed| -> Result<()> { + follow_metadata(value, redirects)?; + let response = &value["response"]; + require( + response["kind"] == "ok" + && response["status"] == 200 + && response["bytes"] == serde_json::json!(b"0123456789") + && response["streamed"] == streamed, + "wrong P5 final bytes/mode/status", + ) + }; + if case.starts_with("optimizer-") { + let failure = &result["failure"]; + let hop = match case { + "optimizer-send" => Value::Null, + "optimizer-initial" => serde_json::json!(0), + _ => serde_json::json!(1), + }; + return require( + failure["kind"] == "error" + && failure["category"] == "bad_gateway" + && failure["status"] == 502 + && failure["reason"] == "ImageOptimizerUnavailable" + && failure["provider_optimizer_unsupported"] == true + && failure.get("hop") == Some(&hop) + && result["policy_calls"] == usize::from(case == "optimizer-next") + && result["options_checked"] == true, + "P5 optimizer did not reach the expected native unavailable boundary", + ); + } + if case == "two-origin" { + require( + result["send"]["kind"] == "ok" + && result["send"]["status"] == 302 + && result["send"]["bytes"] == serde_json::json!([]) + && result["send"]["streamed"] == false, + "P5 plain Send followed or changed the redirect", + )?; + return followed(&result["follow"], 1, false); + } + if case == "pending-head" { + followed(&result["follow"], 1, true)?; + return require( + result["stop"]["response"]["kind"] == "error" + && result["stop"]["response"]["category"] == "response_too_large" + && result["stop"]["response"]["status"] == 502 + && result["stop"]["response"]["reason"] == "EncodedBody" + && result["stop"]["hop"] == 0 + && result["stop"]["policy_calls"] == 1, + "P5 stopped pending redirect bypassed final body limits", + ); + } + let follow = &result["follow"]; + require( + result["send"] == follow["response"] && follow["policy_calls"] == 0, + "P5 Send/Follow parity changed", + )?; + if case.ends_with("/ten") { + followed(follow, 0, case.contains("/streamed/"))?; + let headers = result["send"]["headers"] + .as_array() + .ok_or("missing P5 response headers")?; + let encoding: Vec<_> = headers + .iter() + .filter(|field| field[0] == "content-encoding") + .collect(); + require( + if case.contains("/passthrough/") { + encoding.len() == 1 && encoding[0][1] == "x-fixture-opaque" + } else { + encoding.is_empty() + }, + "P5 coding normalization changed", + ) + } else { + // Identity's declared decoded length can reject even before a streamed head is returned. + if case.contains("/buffered/") || case == "parity/identity/streamed/declared" { + require( + follow.get("hop") == Some(&serde_json::json!(0)) + && ["final_uri", "redirects", "stop_reason"] + .iter() + .all(|field| follow.get(*field).is_none()), + "wrong P5 finalization failure hop/metadata", + )?; + } else { + follow_metadata(follow, 0)?; + } + // Passthrough skips the decoded cap; both modes still collect under the final 10-byte cap. + let expected_reason = if case.contains("/passthrough/") { + "BufferedBody" + } else { + "DecodedBody" + }; + require( + result["send"]["kind"] == "error" + && result["send"]["category"] == "response_too_large" + && result["send"]["status"] == 502 + && result["send"]["reason"] == expected_reason, + "P5 overflow did not retain its typed response limit", + ) + } +} + +fn slot_count(case: &str) -> usize { + if matches!(case, "siblings" | "mixed-head-body" | "mixed-body-head") { + 2 + } else { + 1 + } +} + +fn payload(case: &str, slot: usize) -> &'static [u8] { + match case { + "mixed-head-body" | "mixed-body-head" if slot == 1 => &[b'x'; 32_769], + "full-body-error" => &[b'x'; 32_769], + "truncated" | "exhausted" | "malformed-body" => b"x", + "trickle" => b"xy", + "eof" => b"xyz", + "siblings" if slot == 1 => b"xyz", + _ => b"", + } +} + +fn fingerprint(bytes: &[u8]) -> String { + format!( + "{:016x}", + bytes + .iter() + .fold(0xcbf29ce484222325u64, |n, b| (n ^ u64::from(*b)) + .wrapping_mul(0x100000001b3)) + ) +} + +fn expected(case: &str, slot: usize) -> (&'static str, &'static str) { + match case { + "mixed-head-body" if slot == 0 => ("timeout", "head"), + "mixed-body-head" if slot == 0 => ("timeout", "body"), + "mixed-head-body" | "mixed-body-head" => ("eof", "body"), + "silent-head" => ("timeout", "head"), + "pending-body" | "trickle" | "exhausted" => ("timeout", "body"), + "siblings" if slot == 0 => ("timeout", "body"), + "eof" | "empty-eof" | "siblings" => ("eof", "body"), + "head-error" | "malformed-head" => ("error", "head"), + _ => ("error", "body"), + } +} + +fn origin(probe: &Value, slots: usize) -> Result<()> { + let origin = &probe["origin"]; + require( + origin["case"] == probe["case"] && origin["token"] == probe["token"], + "foreign readiness origin", + )?; + require( + origin["accepted"] == slots + && origin["workers_joined"] == slots + && origin["max_workers"] == slots + && origin["harness_shutdown"] == true + && origin["watchdog"] == false + && origin["errors"].as_array().is_some_and(Vec::is_empty), + "origin workers, errors or containment invalid", + )?; + let requests = origin["requests"] + .as_array() + .ok_or("missing origin requests")?; + require(requests.len() == slots, "wrong origin request count")?; + let mut seen = HashSet::new(); + for request in requests { + let slot = request["slot"].as_u64().ok_or("missing origin slot")? as usize; + require( + slot < slots + && seen.insert(slot) + && request["token"] == probe["token"] + && request["path"] + == format!( + "/readiness/{}/{slot}", + probe["case"].as_str().unwrap_or_default() + ) + && request["received_us"].as_u64().is_some(), + "wrong/duplicate origin request", + )?; + } + let terminations = origin["terminations"] + .as_array() + .ok_or("missing origin terminations")?; + require( + terminations.len() == slots, + "wrong origin termination count", + )?; + let mut terminated = HashSet::new(); + for termination in terminations { + let slot = termination["slot"] + .as_u64() + .ok_or("missing termination slot")?; + require( + slot < slots as u64 + && terminated.insert(slot) + && matches!( + termination["reason"].as_str(), + Some("peer-close" | "harness-stop" | "complete") + ), + "wrong/duplicate origin termination or failed worker", + )?; + } + let writes = origin["writes"].as_array().ok_or("missing origin writes")?; + for write in writes { + require( + write["slot"] + .as_u64() + .is_some_and(|slot| slot < slots as u64) + && write["at_us"].as_u64().is_some_and(|at| at <= 3_000_000) + && write["bytes"].as_u64().is_some_and(|bytes| bytes > 0) + && matches!(write["kind"].as_str(), Some("head" | "body")), + "invalid origin write", + )?; + require( + !(matches!(probe["case"].as_str(), Some("silent-head" | "head-error")) + || (probe["case"] == "mixed-head-body" && write["slot"] == 0)) + || write["kind"] != "head", + "head written by silent/head-error origin", + )?; + } + let case = probe["case"].as_str().ok_or("missing origin case")?; + if matches!( + case, + "mixed-head-body" | "mixed-body-head" | "full-body-error" + ) { + for slot in 0..slots { + let body_bytes: u64 = writes + .iter() + .filter(|w| w["slot"] == slot && w["kind"] == "body") + .try_fold(0u64, |sum, w| { + sum.checked_add(w["bytes"].as_u64().unwrap()) + .ok_or("origin byte total overflow") + })?; + require( + body_bytes == payload(case, slot).len() as u64, + "wrong origin payload receipt", + )?; + let heads = writes + .iter() + .filter(|w| w["slot"] == slot && w["kind"] == "head") + .count(); + require( + heads == usize::from(case != "mixed-head-body" || slot != 0), + "wrong origin head count", + )?; + } + if case == "mixed-body-head" { + let first = writes + .iter() + .position(|w| w["slot"] == 0 && w["kind"] == "head") + .ok_or("missing pending-body head")?; + let second = writes + .iter() + .position(|w| w["slot"] == 1 && w["kind"] == "head") + .ok_or("missing delayed head")?; + let first_at = writes[first]["at_us"].as_u64().unwrap(); + let second_at = writes[second]["at_us"].as_u64().unwrap(); + require( + first < second && second_at >= first_at + 50_000, + "delayed head control not observed", + )?; + } + } + Ok(()) +} + +fn trace(probe: &Value, leg: &ReadinessLeg) -> Result<()> { + let count = slot_count(leg.case); + origin(probe, count)?; + let samples = probe["samples"] + .as_array() + .ok_or("missing readiness samples")?; + require( + !samples.is_empty() && samples.len() <= 512, + "empty/oversized readiness trace", + )?; + let mut phase = vec![None; count]; + let mut dispatched = vec![false; count]; + let mut finished = vec![false; count]; + let mut bytes = vec![0u64; count]; + let mut terminal = vec![None; count]; + let mut terminal_slot = None; + let mut permission = None; + let mut last_selection = Vec::new(); + let mut last_end = 0; + let mut handoffs = 0; + let mut read_count = 0; + let mut nonzero_selection = false; + let mut mixed_selection = false; + let mut full_buffer_reads = 0; + let rows = probe["slots"] + .as_array() + .ok_or("missing readiness slot results")?; + require(rows.len() == count, "wrong readiness slot count")?; + for sample in samples { + let operation = sample["operation"].as_str().ok_or("missing operation")?; + let start = sample["start_us"] + .as_u64() + .ok_or("missing operation start")?; + let end = sample["end_us"].as_u64().ok_or("missing operation end")?; + require( + last_end <= start && start <= end && end <= 505_000, + "reordered or late operation", + )?; + last_end = end; + require( + operation == "finish" || start < 500_000, + "operation starts after expiry", + )?; + if let Some(slot) = terminal_slot { + require( + operation == "finish" && sample["slot"] == slot, + "source EOF/error must be followed by immediate same-slot finish", + )?; + } + if let Some((slot, kind)) = permission { + let matching = sample["slot"] == slot + && operation == if kind == "head" { "head-poll" } else { "read" }; + require( + matching + || (operation == "finish" + && sample["outcome"] == "timeout" + && start >= 500_000), + "unused selection permits only matching I/O or deadline finish", + )?; + } + if operation == "select" { + if sample.get("abi_enter_us").is_some() || sample.get("abi_return_us").is_some() { + let entered = sample["abi_enter_us"] + .as_u64() + .ok_or("missing raw ABI entry")?; + let returned = sample["abi_return_us"] + .as_u64() + .ok_or("missing raw ABI return")?; + require( + start <= entered && entered <= returned && returned <= end, + "raw ABI timestamps outside observed selection", + )?; + } + require( + permission.is_none() && dispatched.iter().all(|v| *v), + "unused readiness or undispatched slot", + )?; + let heads_only = leg.case == "siblings" && phase.contains(&Some("head")); + let live: Vec<_> = (0..count) + .filter(|i| phase[*i].is_some_and(|p| !heads_only || p == "head")) + .collect(); + let selected_slots = sample["slots"] + .as_array() + .ok_or("missing selection slots")?; + let selected_phases = sample["phases"] + .as_array() + .ok_or("missing selection phases")?; + mixed_selection |= selected_phases.contains(&Value::from("head")) + && selected_phases.contains(&Value::from("body")); + require( + !live.is_empty() + && selected_slots.len() == live.len() + && selected_phases.len() == live.len(), + "wrong selection domain", + )?; + for (index, slot) in live.iter().enumerate() { + require( + selected_slots[index] == *slot + && selected_phases[index].as_str() == phase[*slot], + "wrong selection map", + )?; + } + let wait = ((500_000 - start) / 1000).max(1); + require( + sample["status"] == "FastlyStatus::OK" && sample["timeout_ms"] == wait, + "invalid select status/remaining budget", + )?; + let selected = sample.get("selected").ok_or("missing selected outcome")?; + if !selected.is_null() { + let index = usize::try_from(selected.as_u64().ok_or("invalid selected index")?)?; + require(index < live.len(), "selected index outside domain")?; + if end < 500_000 { + let slot = live[index]; + permission = Some((slot, phase[slot].unwrap())); + nonzero_selection |= index > 0 && phase[slot] == Some("body"); + } + } + last_selection = live + .iter() + .map(|slot| (*slot, phase[*slot].unwrap())) + .collect(); + continue; + } + if operation == "handoff" { + require( + permission.is_none() + && leg.consumption == "streamed" + && handoffs == 0 + && read_count == 0 + && sample["body_reads"] == 0 + && sample["deadline_us"] == 500_000, + "streamed handoff changed deadline or already consumed the body", + )?; + handoffs += 1; + last_selection.clear(); + continue; + } + let slot = sample["slot"].as_u64().ok_or("missing operation slot")? as usize; + require( + slot < count && !finished[slot], + "invalid/completed operation slot", + )?; + match operation { + "dispatch" => { + require( + !dispatched[slot] + && sample["outcome"] == "pending" + && sample["error"].is_null() + && sample["bytes"].is_null(), + "bad or duplicate dispatch", + )?; + dispatched[slot] = true; + phase[slot] = Some("head"); + } + "head-poll" => { + require( + permission.take() == Some((slot, "head")) && phase[slot] == Some("head"), + "head poll without matching readiness", + )?; + require(sample["bytes"].is_null(), "head poll contains body bytes")?; + if end < 500_000 { + last_selection.clear(); + } + match sample["outcome"].as_str() { + Some("ready") => { + require( + sample["error"].is_null(), + "ready head also contains an error", + )?; + if end < 500_000 { + phase[slot] = Some("body"); + } + } + Some("error") => { + let error = sample["error"] + .as_str() + .filter(|e| !e.is_empty()) + .ok_or("missing head error category")?; + terminal[slot] = Some(("error", "head", Some(error), end)); + terminal_slot = Some(slot); + } + _ => return Err("selected head was not ready".into()), + } + } + "read" => { + require( + phase[slot] == Some("body") && sample["capacity"] == leg.read_limit, + "wrong read phase/capacity", + )?; + require( + leg.consumption != "streamed" || handoffs == 1, + "streamed read before handoff", + )?; + if leg.mode == "selected" { + require( + permission.take() == Some((slot, "body")), + "read without fresh same-slot readiness", + )?; + } else { + require( + leg.case == "truncated", + "unbounded direct read outside finite control", + )?; + } + read_count += 1; + if end < 500_000 { + last_selection.clear(); + } + require( + sample.get("bytes").is_some() != sample.get("error").is_some(), + "contradictory read outcome", + )?; + if let Some(n) = sample["bytes"].as_u64() { + require(n <= leg.read_limit as u64, "oversized read")?; + if end < 500_000 { + bytes[slot] += n; + full_buffer_reads += usize::from(n == leg.read_limit as u64); + } + if n == 0 { + terminal[slot] = Some(("eof", "body", None, end)); + terminal_slot = Some(slot); + } + } else { + let error = sample["error"] + .as_str() + .filter(|e| !e.is_empty()) + .ok_or("missing read error category")?; + terminal[slot] = Some(("error", "body", Some(error), end)); + terminal_slot = Some(slot); + } + } + "finish" => { + let outcome = sample["outcome"] + .as_str() + .ok_or("missing terminal outcome")?; + let actual_phase = sample["phase"].as_str().ok_or("missing terminal phase")?; + require(Some(actual_phase) == phase[slot], "terminal phase mismatch")?; + if outcome == "timeout" { + require(start >= 500_000, "premature deadline terminal")?; + require( + last_selection.contains(&(slot, actual_phase)), + "timeout lacks a final matching readiness wait", + )?; + require( + terminal[slot].is_none_or(|(_, _, _, at)| at >= 500_000), + "source terminal replaced by delayed timeout", + )?; + } else { + last_selection.clear(); + require( + end < 500_000 + && terminal[slot].is_some_and(|(kind, phase, _, _)| { + (kind, phase) == (outcome, actual_phase) + }), + "invented or late terminal", + )?; + } + require( + (outcome, actual_phase) == expected(leg.case, slot), + "wrong case terminal", + )?; + require( + rows[slot]["outcome"] == outcome + && rows[slot]["phase"] == actual_phase + && rows[slot]["at_us"] == end, + "slot summary differs from trace", + )?; + if matches!(leg.case, "truncated" | "full-body-error") { + require( + sample["error"] == "UnexpectedEof", + "truncated source error was lost", + )?; + } + if let Some((_, _, Some(error), _)) = terminal[slot] { + require( + sample["error"].as_str() == Some(error) + && rows[slot]["error"].as_str() == Some(error), + "terminal error differs from source", + )?; + } else { + require( + sample["error"].is_null() && rows[slot]["error"].is_null(), + "terminal contains an unobserved source error", + )?; + } + phase[slot] = None; + finished[slot] = true; + permission = None; + terminal_slot = None; + } + _ => return Err("unknown readiness operation".into()), + } + } + require( + permission.is_none() + && terminal_slot.is_none() + && finished.iter().all(|v| *v) + && handoffs == usize::from(leg.consumption == "streamed"), + "missing terminal or streamed handoff", + )?; + require( + leg.case != "siblings" || nonzero_selection, + "ready sibling at nonzero index was not observed", + )?; + if matches!(leg.case, "mixed-head-body" | "mixed-body-head") { + require( + mixed_selection, + "mixed head/body selection was not observed", + )?; + } + if matches!( + leg.case, + "mixed-head-body" | "mixed-body-head" | "full-body-error" + ) { + require(full_buffer_reads > 0, "full-capacity read was not observed")?; + } + for (slot, row) in rows.iter().enumerate() { + let wanted = payload(leg.case, slot); + require( + bytes[slot] == wanted.len() as u64 + && row["bytes"] == bytes[slot] + && row["checksum"] == fingerprint(wanted) + && row["buffered_bytes"] + == if leg.consumption == "buffered" { + wanted.len() + } else { + 0 + }, + "read bytes, checksum or consumption differs", + )?; + } + require( + probe["completed_us"] + .as_u64() + .is_some_and(|end| (last_end..=505_000).contains(&end)), + "missing/late probe completion", + ) +} + +pub fn validate_outbound_readiness(probes: &[Value], revision: &str) -> Result<()> { + validate_readiness(probes, revision, readiness_legs()) +} + +pub fn validate_outbound_readiness_progress(probes: &[Value], revision: &str) -> Result<()> { + validate_readiness(probes, revision, readiness_progress_legs()) +} + +fn validate_readiness(probes: &[Value], revision: &str, legs: Vec) -> Result<()> { + require( + revision.len() == 40 && revision.bytes().all(|b| b.is_ascii_hexdigit()), + "invalid expected revision", + )?; + require( + probes.len() == legs.len(), + "missing readiness case inventory", + )?; + let mut identities = HashSet::new(); + let mut tokens = HashSet::new(); + let mut matched = Vec::with_capacity(legs.len()); + for probe in probes { + let leg = legs + .iter() + .find(|leg| { + probe["case"] == leg.case + && probe["mode"] == leg.mode + && probe["consumption"] == leg.consumption + && probe["read_limit"] == leg.read_limit + && probe["repetition"] == leg.repetition + }) + .ok_or("unknown readiness leg")?; + let token = probe["token"].as_str().ok_or("missing readiness token")?; + require( + probe["schema"] == 2 + && probe["revision"] == revision + && probe["deadline_us"] == 500_000 + && probe["tolerance_us"] == 5000 + && probe["qualification"] == false + && !token.is_empty() + && token.len() <= 128 + && token + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-') + && identities.insert(leg.key()) + && tokens.insert(token), + "invalid/duplicate readiness identity", + )?; + matched.push((probe, leg)); + } + let failures: Vec<_> = matched + .into_iter() + .filter_map(|(probe, leg)| { + trace(probe, leg) + .err() + .map(|error| format!("{}: {error}", leg.key())) + }) + .collect(); + require( + failures.is_empty(), + format!( + "{} readiness leg(s) failed:\n{}", + failures.len(), + failures.join("\n") + ), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + const REVISION: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + struct Trace { + samples: Vec, + phases: Vec>, + now: u64, + } + + impl Trace { + fn push(&mut self, mut sample: Value) { + sample["start_us"] = json!(self.now); + self.now += 1; + sample["end_us"] = json!(self.now); + self.samples.push(sample); + } + + fn select(&mut self, case: &str, slot: Option) { + let heads_only = case == "siblings" && self.phases.contains(&Some("head")); + let live: Vec<_> = (0..self.phases.len()) + .filter(|i| self.phases[*i].is_some_and(|p| !heads_only || p == "head")) + .collect(); + let selected = slot.map(|slot| live.iter().position(|i| *i == slot).unwrap()); + let phases: Vec<_> = live.iter().map(|i| self.phases[*i].unwrap()).collect(); + let start = self.now; + let end = if slot.is_some() { start + 1 } else { 500_000 }; + self.samples + .push(json!({"operation":"select", "start_us":start, "end_us":end, + "slots":live, "phases":phases, "selected":selected, + "status":"FastlyStatus::OK", "timeout_ms":((500_000 - start) / 1000).max(1)})); + self.now = end; + } + } + + fn matrix() -> Vec { + matrix_for(readiness_legs()) + } + + fn matrix_for(legs: Vec) -> Vec { + legs.iter().enumerate().map(|(index, leg)| { + let count = slot_count(leg.case); + let token = format!("readiness-{index}"); + let mut trace = Trace { samples: Vec::new(), phases: vec![Some("head"); count], now: 0 }; + for slot in 0..count { + trace.push(json!({"operation":"dispatch", "slot":slot, "outcome":"pending"})); + } + if leg.consumption == "streamed" { + trace.push(json!({"operation":"handoff", "body_reads":0, "deadline_us":500_000})); + } + if leg.case != "silent-head" { + for slot in 0..count { + if leg.case == "mixed-head-body" && slot == 0 { continue; } + trace.select(leg.case, Some(slot)); + let mut poll = json!({"operation":"head-poll", "slot":slot, "outcome":"ready"}); + if expected(leg.case, slot) == ("error", "head") { + poll["outcome"] = json!("error"); + poll["error"] = json!("HttpProtocolError"); + } else { + trace.phases[slot] = Some("body"); + } + trace.push(poll); + } + } + let mut rows = vec![Value::Null; count]; + // Finish the ready sibling before observing the pending slot's deadline. + for slot in (0..count).rev() { + let wanted = payload(leg.case, slot); + for chunk in wanted.chunks(leg.read_limit) { + if leg.mode == "selected" { trace.select(leg.case, Some(slot)); } + trace.push(json!({"operation":"read", "slot":slot, "capacity":leg.read_limit, "bytes":chunk.len()})); + } + let (outcome, phase) = expected(leg.case, slot); + let error = match (outcome, phase) { + ("error", "head") => Some("HttpProtocolError"), + ("error", _) if matches!(leg.case, "truncated" | "full-body-error") => Some("UnexpectedEof"), + ("error", _) => Some("Other"), + _ => None, + }; + if outcome == "timeout" { + trace.select(leg.case, None); + } else if phase == "body" { + if leg.mode == "selected" { trace.select(leg.case, Some(slot)); } + let mut read = json!({"operation":"read", "slot":slot, "capacity":leg.read_limit}); + if let Some(error) = error { read["error"] = json!(error); } + else { read["bytes"] = json!(0); } + trace.push(read); + } + let mut finish = json!({"operation":"finish", "slot":slot, "outcome":outcome, "phase":phase}); + if let Some(error) = error { finish["error"] = json!(error); } + trace.push(finish); + trace.phases[slot] = None; + let mut row = json!({"outcome":outcome, "phase":phase, "at_us":trace.now, + "bytes":wanted.len(), "checksum":fingerprint(wanted), + "buffered_bytes":if leg.consumption == "buffered" { wanted.len() } else { 0 }}); + if let Some(error) = error { row["error"] = json!(error); } + rows[slot] = row; + } + let requests: Vec<_> = (0..count).map(|slot| json!({"slot":slot, "token":token, + "path":format!("/readiness/{}/{slot}", leg.case), "received_us":1})).collect(); + let terminations: Vec<_> = (0..count).map(|slot| json!({"slot":slot, "reason":"complete"})).collect(); + let mut writes: Vec<_> = if matches!(leg.case, "silent-head" | "head-error") { + Vec::new() + } else { + (0..count).filter(|slot| leg.case != "mixed-head-body" || *slot != 0) + .map(|slot| json!({"slot":slot, "kind":"head", "at_us":1, "bytes":1})).collect() + }; + if leg.case == "mixed-body-head" { + writes[1]["at_us"] = json!(50_001); + } + if matches!(leg.case, "mixed-head-body" | "mixed-body-head" | "full-body-error") { + for slot in 0..count { + let bytes = payload(leg.case, slot).len(); + if bytes > 0 { writes.push(json!({"slot":slot,"kind":"body","at_us":50_002,"bytes":bytes})); } + } + } + json!({"schema":2, "case":leg.case, "mode":leg.mode, "consumption":leg.consumption, + "read_limit":leg.read_limit, "repetition":leg.repetition, "revision":REVISION, + "token":token, "deadline_us":500_000, "tolerance_us":5000, "qualification":false, + "samples":trace.samples, "slots":rows, "completed_us":trace.now, + "origin":{"case":leg.case, "token":token, "accepted":count, "workers_joined":count, + "max_workers":count, "harness_shutdown":true, "watchdog":false, "errors":[], + "requests":requests, "terminations":terminations, "writes":writes}}) + }).collect() + } + + fn leg<'a>(probes: &'a mut [Value], case: &str, consumption: &str) -> &'a mut Value { + probes + .iter_mut() + .find(|p| { + p["case"] == case + && p["consumption"] == consumption + && p["mode"] == "selected" + && p["read_limit"] == 1 + && p["repetition"] == 0 + }) + .unwrap() + } + + fn samples(probe: &mut Value) -> &mut Vec { + probe["samples"].as_array_mut().unwrap() + } + + fn client_matrix() -> Vec { + matrix_for(readiness_client_legs()) + .into_iter() + .map(|source| { + let result = if source["case"] == "full-body-error" { + let bytes = if source["consumption"] == "streamed" { + payload("full-body-error", 0) + } else { + b"" + }; + json!({"kind":"error", "bytes":bytes.len(), "checksum":fingerprint(bytes), + "failure":{"category":"bad_gateway", "source_unexpected_eof":true}}) + } else { + json!({"termination":"Cutoff", "slots":[{"kind":"pending"}, + {"kind":"ok", "bytes":32_769, "checksum":fingerprint(payload("full-body-error", 0)), + "elapsed_us":60_000}]}) + }; + json!({"event":"fastly-client-progress", "token":source["token"], + "case":source["case"], "consumption":source["consumption"], + "revision":REVISION, "qualification":false, "repetition":source["repetition"], + "completed_us":500_100, "origin":source["origin"], "result":{"Ok":result}}) + }) + .collect() + } + + fn p5_matrix() -> Vec { + p5_cases().into_iter().enumerate().map(|(index, case)| { + let token = format!("p5-{index}"); + let shape = match case.as_str() { + "two-origin" | "optimizer-next" => "redirect", + "pending-head" => "pending", + "optimizer-send" | "optimizer-initial" => "image", + _ => &case, + }; + let path = format!("/p5/{token}/8002/{shape}"); + let final_path = format!("/p5/{token}/8002/final"); + let initial = format!("http://127.0.0.1:8001{path}"); + let final_uri = format!("http://127.0.0.1:8002{final_path}"); + let response = json!({"kind":"ok", "status":200, + "headers":[["content-type","application/octet-stream"], + ["date","Tue, 27 Oct 2026 00:00:00 GMT"]], + "bytes":b"0123456789", "streamed":case == "pending-head" || case.contains("/streamed/")}); + let followed = |response: Value, redirects| json!({"response":response, + "final_uri":if redirects == 1 { &final_uri } else { &initial }, + "redirects":redirects,"stop_reason":null,"policy_calls":redirects}); + let limit = json!({"kind":"error","category":"response_too_large","status":502, + "reason":if case == "pending-head" {"EncodedBody"} else if case.contains("/passthrough/") {"BufferedBody"} else {"DecodedBody"}, + "provider_optimizer_unsupported":false,"hop":null}); + let mut a = vec![path.clone(), path.clone()]; + let mut b = Vec::new(); + let result = if case == "two-origin" { + let mut sent = response.clone(); + sent["status"] = json!(302); + sent["bytes"] = json!([]); + b.push(final_path.clone()); + json!({"send":sent,"follow":followed(response,1)}) + } else if case == "pending-head" { + b.push(final_path.clone()); + json!({"follow":followed(response,1),"stop":{"response":limit,"hop":0,"policy_calls":1}}) + } else if case.starts_with("optimizer-") { + a.clear(); + let next = case == "optimizer-next"; + if next { a.push(path.clone()); } + json!({"failure":{"kind":"error","category":"bad_gateway","status":502, + "reason":"ImageOptimizerUnavailable","provider_optimizer_unsupported":true, + "hop":if case == "optimizer-send" { Value::Null } else { json!(usize::from(next)) }}, + "policy_calls":usize::from(next),"options_checked":true}) + } else { + let mut response = response; + if case.ends_with("/ten") { + if case.contains("/passthrough/") { + response["headers"].as_array_mut().unwrap().push(json!(["content-encoding","x-fixture-opaque"])); + } + } else { response = limit; } + let follow = if !case.ends_with("/ten") + && (case.contains("/buffered/") || case == "parity/identity/streamed/declared") + { + json!({"response":response,"hop":0,"policy_calls":0}) + } else { + followed(response.clone(),0) + }; + json!({"send":response,"follow":follow}) + }; + json!({"event":"fastly-p5-probe","case":case,"token":token,"revision":REVISION, + "qualification":false,"a_uri":initial,"b_uri":final_uri,"response_decoding":"Decode","result":{"Ok":result}, + "origin":{"a_port":8001,"b_port":8002,"a":a,"b":b, + "pending_released_before_reply":false, + "pending_heads":if case == "pending-head" { 2 } else { 0 }}}) + }).collect() + } + + #[test] + fn p5_inventory_is_twenty_three_unique_cases() { + let cases = p5_cases(); + assert_eq!(cases.len(), 23); + assert_eq!(cases.iter().collect::>().len(), 23); + for coding in ["identity", "gzip", "passthrough"] { + assert_eq!( + cases + .iter() + .filter(|case| case.starts_with(&format!("parity/{coding}/"))) + .count(), + 6 + ); + } + for case in [ + "two-origin", + "pending-head", + "optimizer-send", + "optimizer-initial", + "optimizer-next", + ] { + assert!(cases.iter().any(|known| known == case)); + } + } + + #[test] + fn p5_inventory_requires_complete_correlated_evidence() { + let mut probes = p5_matrix(); + assert_eq!(probes.len(), 23); + validate_outbound_readiness_client(&probes, REVISION).unwrap(); + probes.reverse(); + validate_outbound_readiness_client(&probes, REVISION).unwrap(); + validate_outbound_readiness_client(&client_matrix(), REVISION).unwrap(); + } + + #[test] + fn p5_inventory_rejects_missing_duplicate_and_foreign_records() { + let baseline = p5_matrix(); + assert!(validate_outbound_readiness_client(&[], REVISION).is_err()); + for index in 0..baseline.len() { + let mut missing = baseline.clone(); + missing.remove(index); + assert!(validate_outbound_readiness_client(&missing, REVISION).is_err()); + let mut duplicate = baseline.clone(); + duplicate[index] = baseline[(index + 1) % baseline.len()].clone(); + assert!(validate_outbound_readiness_client(&duplicate, REVISION).is_err()); + for field in [ + "event", + "case", + "token", + "revision", + "qualification", + "response_decoding", + "result", + "origin", + "a_uri", + "b_uri", + ] { + let mut probes = baseline.clone(); + probes[index].as_object_mut().unwrap().remove(field); + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{index}: {field}" + ); + } + } + for (field, value) in [ + ("case", json!("other")), + ("revision", json!("foreign")), + ("token", json!("invalid token")), + ("qualification", json!(true)), + ] { + let mut probes = baseline.clone(); + probes[0][field] = value; + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{field}" + ); + } + let mut duplicate = baseline.clone(); + duplicate.push(baseline[0].clone()); + assert!(validate_outbound_readiness_client(&duplicate, REVISION).is_err()); + let mut foreign = baseline.clone(); + foreign[0] = client_matrix()[0].clone(); + assert!(validate_outbound_readiness_client(&foreign, REVISION).is_err()); + + let mut reused_token = baseline.clone(); + let token = baseline[0]["token"].as_str().unwrap(); + let path = format!("/p5/{token}/8002/pending"); + let final_path = format!("/p5/{token}/8002/final"); + let probe = &mut reused_token[1]; + probe["token"] = json!(token); + probe["a_uri"] = json!(format!("http://127.0.0.1:8001{path}")); + probe["b_uri"] = json!(format!("http://127.0.0.1:8002{final_path}")); + probe["origin"]["a"] = json!([path, path]); + probe["origin"]["b"] = json!([final_path]); + probe["result"]["Ok"]["follow"]["final_uri"] = probe["b_uri"].clone(); + validate_p5_probe(probe, REVISION).unwrap(); + assert!(validate_outbound_readiness_client(&reused_token, REVISION).is_err()); + } + + #[test] + fn p5_inventory_requires_explicit_null_fields() { + for (case, parent, field) in [ + ("two-origin", "/result/Ok/follow", "stop_reason"), + ("optimizer-send", "/result/Ok/failure", "hop"), + ] { + let mut probes = p5_matrix(); + probes + .iter_mut() + .find(|probe| probe["case"] == case) + .unwrap() + .pointer_mut(parent) + .unwrap() + .as_object_mut() + .unwrap() + .remove(field); + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{case}: {field}" + ); + } + } + + #[test] + fn p5_inventory_rejects_unrelated_limit_causes() { + let baseline = p5_matrix(); + validate_outbound_readiness_client(&baseline, REVISION).unwrap(); + for (index, probe) in baseline.iter().enumerate() { + let case = probe["case"].as_str().unwrap(); + let pointers: &[&str] = if case == "pending-head" { + &["/result/Ok/stop/response/reason"] + } else if case.starts_with("parity/") && !case.ends_with("/ten") { + &[ + "/result/Ok/send/reason", + "/result/Ok/follow/response/reason", + ] + } else { + continue; + }; + let expected = probe.pointer(pointers[0]).unwrap().as_str().unwrap(); + for reason in [ + "BrotliWindow", + "BufferedBody", + "DecodedBody", + "DecoderMemory", + "EncodedBody", + "HeaderBytes", + "HeaderCount", + "Unspecified", + ] { + if reason == expected { + continue; + } + let mut probes = baseline.clone(); + for pointer in pointers { + *probes[index].pointer_mut(pointer).unwrap() = json!(reason); + } + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{case}: accepted {reason} instead of {expected}" + ); + } + } + } + + #[test] + fn p5_inventory_rejects_original_mode_evidence() { + let baseline = p5_matrix(); + for index in 0..baseline.len() { + let mut probes = baseline.clone(); + probes[index]["response_decoding"] = json!("Original"); + assert!(validate_outbound_readiness_client(&probes, REVISION).is_err()); + } + } + + #[test] + fn p5_overflow_requires_correlated_follow_terminal_metadata() { + let baseline = p5_matrix(); + validate_outbound_readiness_client(&baseline, REVISION).unwrap(); + for (index, probe) in baseline.iter().enumerate() { + let case = probe["case"].as_str().unwrap(); + if !case.starts_with("parity/") || case.ends_with("/ten") { + continue; + } + let original = probe.pointer("/result/Ok/follow").unwrap(); + let mutations = if original.get("hop").is_some() { + vec![ + ("hop", None), + ("hop", Some(json!(99))), + ("hop", Some(Value::Null)), + ("final_uri", Some(probe["a_uri"].clone())), + ("redirects", Some(json!(0))), + ("stop_reason", Some(Value::Null)), + ] + } else { + vec![ + ("final_uri", None), + ("redirects", None), + ("stop_reason", None), + ("final_uri", Some(json!("http://127.0.0.1:8001/p5/foreign"))), + ("redirects", Some(json!(20))), + ("stop_reason", Some(json!("LimitReached"))), + ("hop", Some(json!(99))), + ] + }; + for (field, replacement) in mutations { + let mut probes = baseline.clone(); + let follow = probes[index] + .pointer_mut("/result/Ok/follow") + .unwrap() + .as_object_mut() + .unwrap(); + if let Some(replacement) = replacement { + follow.insert(field.to_owned(), replacement); + } else { + follow.remove(field); + } + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{case}: accepted invalid {field}" + ); + } + } + } + + #[test] + fn p5_inventory_rejects_receipt_and_outcome_substitution() { + let baseline = p5_matrix(); + for (case, pointer, replacement) in [ + ("two-origin", "/origin/a", json!([])), + ("two-origin", "/origin/b", json!([])), + ("two-origin", "/origin/b", json!(["foreign"])), + ("two-origin", "/result/Ok/follow/redirects", json!(0)), + ("two-origin", "/result/Ok/follow/policy_calls", json!(0)), + ("two-origin", "/result/Ok/send/status", json!(200)), + ( + "pending-head", + "/origin/pending_released_before_reply", + json!(true), + ), + ("pending-head", "/origin/pending_heads", json!(0)), + ( + "pending-head", + "/result/Ok/follow/response/streamed", + json!(false), + ), + ( + "pending-head", + "/result/Ok/stop/response/category", + json!("bad_gateway"), + ), + ("optimizer-send", "/origin/b", json!(["unexpected"])), + ("optimizer-initial", "/result/Ok/failure/hop", json!(1)), + ("optimizer-next", "/origin/a", json!([])), + ("optimizer-next", "/result/Ok/failure/hop", json!(0)), + ( + "optimizer-next", + "/result/Ok/failure/provider_optimizer_unsupported", + json!(false), + ), + ( + "optimizer-next", + "/result/Ok/failure/reason", + json!("Unspecified"), + ), + ("optimizer-next", "/result/Ok/options_checked", json!(false)), + ( + "parity/gzip/streamed/ten", + "/result/Ok/follow/response/bytes", + json!([]), + ), + ( + "parity/identity/buffered/eleven", + "/result/Ok/send/category", + json!("bad_gateway"), + ), + ] { + let mut probes = baseline.clone(); + let probe = probes + .iter_mut() + .find(|probe| probe["case"] == case) + .unwrap(); + *probe.pointer_mut(pointer).unwrap() = replacement; + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{case}: {pointer}" + ); + } + } + + #[test] + fn readiness_client_inventory_is_twelve_unique_legs() { + let legs = readiness_client_legs(); + assert_eq!(legs.len(), 12); + assert_eq!( + legs.iter() + .map(ReadinessLeg::key) + .collect::>() + .len(), + 12 + ); + for repetition in 0..3 { + assert_eq!( + legs.iter() + .filter(|leg| leg.repetition == repetition) + .count(), + 4 + ); + } + assert!( + legs.iter() + .all(|leg| leg.mode == "client" && leg.read_limit == 16_384) + ); + } + + #[test] + fn readiness_client_accepts_complete_correlated_typed_evidence() { + let mut probes = client_matrix(); + for probe in &probes { + validate_outbound_readiness_client_probe(probe, REVISION).unwrap(); + } + validate_outbound_readiness_client(&probes, REVISION).unwrap(); + probes.reverse(); + validate_outbound_readiness_client(&probes, REVISION).unwrap(); + } + + #[test] + fn readiness_client_rejects_missing_duplicate_and_foreign_evidence() { + let baseline = client_matrix(); + let mut missing = baseline.clone(); + missing.pop(); + assert!(validate_outbound_readiness_client(&missing, REVISION).is_err()); + let mut duplicate = baseline.clone(); + duplicate[1] = duplicate[0].clone(); + assert!(validate_outbound_readiness_client(&duplicate, REVISION).is_err()); + let mut duplicate_token = baseline.clone(); + duplicate_token[1]["token"] = duplicate_token[0]["token"].clone(); + duplicate_token[1]["origin"]["token"] = duplicate_token[0]["token"].clone(); + for request in duplicate_token[1]["origin"]["requests"] + .as_array_mut() + .unwrap() + { + request["token"] = baseline[0]["token"].clone(); + } + assert!(validate_outbound_readiness_client(&duplicate_token, REVISION).is_err()); + for (field, value) in [ + ("event", json!("readiness-probe")), + ("case", json!("eof")), + ("consumption", json!("streamed")), + ( + "revision", + json!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + ), + ("qualification", json!(true)), + ("repetition", json!(3)), + ("token", json!("foreign-token")), + ("token", json!("invalid token")), + ] { + let mut probes = baseline.clone(); + probes[0][field] = value; + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{field}" + ); + } + assert!(validate_outbound_readiness_client(&baseline, "not-a-revision").is_err()); + for field in [ + "event", + "token", + "case", + "consumption", + "revision", + "qualification", + "repetition", + "completed_us", + "origin", + "result", + ] { + let mut probes = baseline.clone(); + probes[0].as_object_mut().unwrap().remove(field); + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "missing {field}" + ); + } + } + + #[test] + fn readiness_client_rejects_collapsed_or_untyped_source_errors() { + for consumption in ["buffered", "streamed"] { + for mutation in ["eof", "category", "source", "outer-error"] { + let mut probes = client_matrix(); + let probe = probes + .iter_mut() + .find(|p| p["case"] == "full-body-error" && p["consumption"] == consumption) + .unwrap(); + match mutation { + "eof" => { + probe["result"]["Ok"]["kind"] = json!("eof"); + probe["result"]["Ok"]["failure"] = Value::Null; + } + "category" => probe["result"]["Ok"]["failure"]["category"] = json!("internal"), + "source" => { + probe["result"]["Ok"]["failure"]["source_unexpected_eof"] = json!(false) + } + _ => { + probe["result"] = + json!({"Err":{"category":"bad_gateway", "source_unexpected_eof":true}}) + } + } + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{consumption}/{mutation}" + ); + } + } + } + + #[test] + fn readiness_client_rejects_wrong_bytes_checksums_and_error_prefixes() { + let baseline = client_matrix(); + for index in 0..4 { + for field in ["bytes", "checksum"] { + let mut probes = baseline.clone(); + let result = &mut probes[index]["result"]["Ok"]; + let body = if index < 2 { + &mut result["slots"][1] + } else { + result + }; + body[field] = if field == "bytes" { + json!(123) + } else { + json!("0000000000000000") + }; + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{index}/{field}" + ); + } + } + for (consumption, prefix) in [ + ("buffered", payload("full-body-error", 0)), + ("streamed", b""), + ("streamed", b"x"), + ] { + let mut probes = baseline.clone(); + let result = &mut probes + .iter_mut() + .find(|p| p["case"] == "full-body-error" && p["consumption"] == consumption) + .unwrap()["result"]["Ok"]; + result["bytes"] = json!(prefix.len()); + result["checksum"] = json!(fingerprint(prefix)); + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{consumption}/{}", + prefix.len() + ); + } + } + + #[test] + fn readiness_client_rejects_late_or_reassociated_mixed_results() { + let baseline = client_matrix(); + for case in ["mixed-head-body", "mixed-body-head"] { + for mutation in [ + "equal", + "after", + "missing-elapsed", + "termination", + "slot-order", + "slot-count", + ] { + let mut probes = baseline.clone(); + let probe = probes.iter_mut().find(|p| p["case"] == case).unwrap(); + let result = &mut probe["result"]["Ok"]; + match mutation { + "equal" => result["slots"][1]["elapsed_us"] = json!(500_000), + "after" => result["slots"][1]["elapsed_us"] = json!(500_001), + "missing-elapsed" => { + result["slots"][1] + .as_object_mut() + .unwrap() + .remove("elapsed_us"); + } + "termination" => result["termination"] = json!("Complete"), + "slot-order" => result["slots"].as_array_mut().unwrap().swap(0, 1), + _ => { + result["slots"].as_array_mut().unwrap().pop(); + } + } + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{case}/{mutation}" + ); + } + } + } + + #[test] + fn readiness_client_rejects_invalid_origin_receipts_and_aggregates_failures() { + let baseline = client_matrix(); + for mutation in ["accepted", "body", "delay", "token"] { + let mut probes = baseline.clone(); + let probe = probes + .iter_mut() + .find(|p| p["case"] == "mixed-body-head") + .unwrap(); + match mutation { + "accepted" => probe["origin"]["accepted"] = json!(1), + "body" => probe["origin"]["writes"] + .as_array_mut() + .unwrap() + .retain(|w| w["kind"] != "body"), + "delay" => probe["origin"]["writes"][1]["at_us"] = json!(1), + _ => probe["origin"]["requests"][0]["token"] = json!("foreign-token"), + } + assert!( + validate_outbound_readiness_client(&probes, REVISION).is_err(), + "{mutation}" + ); + } + let mut probes = baseline; + probes[0]["result"]["Ok"]["slots"][1]["bytes"] = json!(0); + probes[1]["result"]["Ok"]["slots"][1]["bytes"] = json!(0); + let error = validate_outbound_readiness_client(&probes, REVISION) + .unwrap_err() + .to_string(); + assert!( + error.starts_with("2 actual-client leg(s) failed:\n"), + "{error}" + ); + assert!( + error.contains("mixed-head-body/client/buffered/16384/0"), + "{error}" + ); + assert!( + error.contains("mixed-body-head/client/buffered/16384/0"), + "{error}" + ); + } + + fn operation<'a>(probe: &'a mut Value, name: &str) -> &'a mut Value { + samples(probe) + .iter_mut() + .find(|s| s["operation"] == name) + .unwrap() + } + + #[test] + fn readiness_accepts_complete_135_leg_matrix() { + let probes = matrix(); + assert_eq!(probes.len(), 135); + assert_eq!(probes.iter().filter(|p| p["mode"] == "direct").count(), 3); + assert_eq!( + probes + .iter() + .map(|p| p["slots"].as_array().unwrap().len()) + .sum::(), + 147 + ); + validate_outbound_readiness(&probes, REVISION).unwrap(); + } + + #[test] + fn readiness_progress_inventory_requires_mixed_phases_and_full_buffers() { + for case in ["mixed-head-body", "mixed-body-head", "full-body-error"] { + let legs: Vec<_> = readiness_progress_legs() + .into_iter() + .filter(|leg| leg.case == case) + .collect(); + assert_eq!(legs.len(), 6, "{case} must run both consumers three times"); + assert!( + legs.iter() + .all(|leg| leg.mode == "selected" && leg.read_limit == 16_384) + ); + } + } + + #[test] + fn readiness_progress_accepts_complete_matrix_and_rejects_lost_evidence() { + let baseline = matrix_for(readiness_progress_legs()); + assert_eq!(baseline.len(), 18); + assert_eq!( + baseline + .iter() + .map(|p| p["slots"].as_array().unwrap().len()) + .sum::(), + 30 + ); + validate_outbound_readiness_progress(&baseline, REVISION).unwrap(); + let mut missing = baseline.clone(); + missing.pop(); + assert!(validate_outbound_readiness_progress(&missing, REVISION).is_err()); + let mut collapsed = baseline.clone(); + let probe = collapsed + .iter_mut() + .find(|p| p["case"] == "full-body-error") + .unwrap(); + let error = operation(probe, "finish"); + error["outcome"] = json!("eof"); + error.as_object_mut().unwrap().remove("error"); + assert!(validate_outbound_readiness_progress(&collapsed, REVISION).is_err()); + let mut filtered = baseline; + let probe = filtered + .iter_mut() + .find(|p| p["case"] == "mixed-head-body") + .unwrap(); + let selection = samples(probe) + .iter_mut() + .find(|s| { + s["phases"] + .as_array() + .is_some_and(|p| p.contains(&json!("head")) && p.contains(&json!("body"))) + }) + .unwrap(); + selection["slots"] = json!([1]); + selection["phases"] = json!(["body"]); + selection["selected"] = json!(0); + let error = validate_outbound_readiness_progress(&filtered, REVISION) + .unwrap_err() + .to_string(); + assert!(error.contains("wrong selection domain"), "{error}"); + } + + #[test] + fn readiness_progress_rejects_only_partial_buffer_reads_even_with_correct_payload() { + let mut probes = matrix_for(readiness_progress_legs()); + let probe = probes + .iter_mut() + .find(|p| p["case"] == "full-body-error") + .unwrap(); + let mut leg = readiness_progress_legs() + .into_iter() + .find(|leg| leg.case == "full-body-error") + .unwrap(); + // Keep every actual byte/checksum/selection intact, but no read now fills the buffer. + leg.read_limit = 32_770; + for read in samples(probe) + .iter_mut() + .filter(|s| s["operation"] == "read") + { + read["capacity"] = json!(leg.read_limit); + } + assert_eq!( + trace(probe, &leg).unwrap_err().to_string(), + "full-capacity read was not observed" + ); + } + + #[test] + fn readiness_progress_rejects_missing_or_reordered_origin_writes() { + let baseline = matrix_for(readiness_progress_legs()); + validate_outbound_readiness_progress(&baseline, REVISION).unwrap(); + for mutation in ["delay", "head-order", "body-receipt", "pending-body"] { + let mut probes = baseline.clone(); + let probe = probes + .iter_mut() + .find(|p| p["case"] == "mixed-body-head") + .unwrap(); + let writes = probe["origin"]["writes"].as_array_mut().unwrap(); + match mutation { + "delay" => { + writes + .iter_mut() + .find(|w| w["slot"] == 1 && w["kind"] == "head") + .unwrap()["at_us"] = json!(1) + } + "head-order" => writes.swap(0, 1), + "body-receipt" => writes.retain(|w| w["kind"] != "body"), + _ => writes.push(json!({"slot":0,"kind":"body","at_us":50_002,"bytes":1})), + } + assert!( + validate_outbound_readiness_progress(&probes, REVISION).is_err(), + "{mutation}" + ); + } + } + + #[test] + fn readiness_progress_rejects_consistent_eof_collapse_and_reused_readiness() { + let baseline = matrix_for(readiness_progress_legs()); + for terminal in [false, true] { + let mut probes = baseline.clone(); + let probe = probes + .iter_mut() + .find(|p| p["case"] == "full-body-error") + .unwrap(); + let events = samples(probe); + let read = if terminal { + events + .iter() + .position(|s| s["operation"] == "read" && s["error"] == "UnexpectedEof") + .unwrap() + } else { + events + .iter() + .enumerate() + .filter(|(_, s)| { + s["operation"] == "read" && s["bytes"].as_u64().is_some_and(|b| b > 0) + }) + .nth(1) + .unwrap() + .0 + }; + assert_eq!(events[read - 1]["operation"], "select"); + events.remove(read - 1); + let error = validate_outbound_readiness_progress(&probes, REVISION) + .unwrap_err() + .to_string(); + assert!( + error.contains("read without fresh same-slot readiness"), + "{error}" + ); + } + let mut collapsed = baseline.clone(); + let probe = collapsed + .iter_mut() + .find(|p| p["case"] == "full-body-error") + .unwrap(); + for event in samples(probe) { + if event["operation"] == "read" && event["error"] == "UnexpectedEof" { + event.as_object_mut().unwrap().remove("error"); + event["bytes"] = json!(0); + } else if event["operation"] == "finish" { + event.as_object_mut().unwrap().remove("error"); + event["outcome"] = json!("eof"); + } + } + probe["slots"][0].as_object_mut().unwrap().remove("error"); + probe["slots"][0]["outcome"] = json!("eof"); + let error = validate_outbound_readiness_progress(&collapsed, REVISION) + .unwrap_err() + .to_string(); + assert!(error.contains("wrong case terminal"), "{error}"); + + let mut head_only = baseline; + let probe = head_only + .iter_mut() + .find(|p| p["case"] == "mixed-head-body") + .unwrap(); + let selection = samples(probe) + .iter_mut() + .find(|s| { + s["phases"] + .as_array() + .is_some_and(|p| p.contains(&json!("head")) && p.contains(&json!("body"))) + }) + .unwrap(); + selection["slots"] = json!([0]); + selection["phases"] = json!(["head"]); + selection["selected"] = json!(0); + let error = validate_outbound_readiness_progress(&head_only, REVISION) + .unwrap_err() + .to_string(); + assert!(error.contains("wrong selection domain"), "{error}"); + } + + #[test] + fn outbound_readiness_rejects_empty_or_unbounded_measurements() { + assert!(validate_outbound_readiness(&[], REVISION).is_err()); + let baseline = matrix(); + validate_outbound_readiness(&baseline, REVISION).unwrap(); + for wait in [0, 501, u64::MAX] { + let mut probes = baseline.clone(); + operation(&mut probes[0], "select")["timeout_ms"] = json!(wait); + assert!( + validate_outbound_readiness(&probes, REVISION).is_err(), + "wait={wait}" + ); + } + } + + #[test] + fn readiness_accepts_selection_abandoned_at_absolute_deadline() { + let mut probes = matrix(); + let probe = leg(&mut probes, "silent-head", "buffered"); + let selection = operation(probe, "select"); + selection["selected"] = json!(0); + selection["end_us"] = json!(499_999); + validate_outbound_readiness(&probes, REVISION).unwrap(); + } + + #[test] + fn readiness_rearms_early_timer_against_original_deadline() { + let mut probes = matrix(); + let probe = leg(&mut probes, "silent-head", "buffered"); + let events = samples(probe); + let selection = events + .iter() + .position(|s| s["operation"] == "select") + .unwrap(); + events[selection]["end_us"] = json!(499_001); + let mut rearmed = events[selection].clone(); + rearmed["start_us"] = json!(499_001); + rearmed["end_us"] = json!(500_000); + rearmed["timeout_ms"] = json!(1); + events.insert(selection + 1, rearmed); + validate_outbound_readiness(&probes, REVISION).unwrap(); + } + + #[test] + fn readiness_aggregates_contextual_trace_failures() { + let mut probes = matrix(); + operation(&mut probes[0], "finish")["error"] = json!("Other"); + leg(&mut probes, "pending-body", "streamed")["completed_us"] = json!(507_794); + operation(leg(&mut probes, "malformed-head", "buffered"), "finish")["error"] = + json!("Other"); + let error = validate_outbound_readiness(&probes, REVISION) + .unwrap_err() + .to_string(); + assert!(error.starts_with("3 readiness leg(s) failed:"), "{error}"); + for key in [ + "truncated/direct/buffered/1/0", + "pending-body/selected/streamed/1/0", + "malformed-head/selected/buffered/1/0", + ] { + assert!(error.contains(&format!("{key}: ")), "{error}"); + } + } + + #[test] + fn readiness_replays_retained_actual_negative_matrices() { + use std::{ + collections::BTreeMap, + fs::File, + io::{BufRead, BufReader}, + }; + + let path = crate::root() + .join("../../../docs/audits/evidence/2026-10-23-fastly-readiness-matrix-probes.jsonl"); + let mut runs: BTreeMap> = BTreeMap::new(); + for line in BufReader::new(File::open(path).unwrap()).lines() { + let row: Value = serde_json::from_str(&line.unwrap()).unwrap(); + let run = row["run"].as_str().unwrap().to_owned(); + runs.entry(run).or_default().push(row["probe"].clone()); + } + assert_eq!(runs.len(), 4); + assert_eq!(runs.values().map(Vec::len).sum::(), 540); + let source_error_keys: std::collections::BTreeSet<_> = readiness_legs() + .iter() + .filter(|leg| { + leg.mode == "selected" && matches!(leg.case, "truncated" | "malformed-body") + }) + .map(ReadinessLeg::key) + .collect(); + for (run, failed, late) in [ + ("repaired-before-trace-hardening", 1, 1), + ("stock-before-trace-hardening", 28, 4), + ("repaired-current", 1, 1), + ("stock-current", 24, 0), + ] { + let probes = runs.get(run).unwrap(); + assert_eq!(probes.len(), 135, "{run}"); + assert_eq!( + probes + .iter() + .map(|p| p["origin"]["accepted"].as_u64().unwrap()) + .sum::(), + 147, + "{run}" + ); + let error = + validate_outbound_readiness(probes, "d12730fda2704ba4c95e878dccd50aef8c4ec22b") + .expect_err("retained failed measurements must never pass") + .to_string(); + eprintln!("{run}: {error}"); + assert!( + error.starts_with(&format!("{failed} readiness leg(s) failed:\n")), + "{run}: {error}" + ); + let failures: BTreeMap<_, _> = error + .lines() + .skip(1) + .map(|line| line.split_once(": ").unwrap()) + .collect(); + assert_eq!(failures.len(), failed, "{run}: {error}"); + assert_eq!( + failures + .values() + .filter(|reason| matches!( + **reason, + "reordered or late operation" | "missing/late probe completion" + )) + .count(), + late, + "{run}: {error}" + ); + if run == "stock-current" { + assert_eq!( + failures + .keys() + .map(|key| (*key).to_owned()) + .collect::>(), + source_error_keys + ); + assert!( + failures + .values() + .all(|reason| *reason == "wrong case terminal") + ); + } else if run.starts_with("repaired") { + let (key, completed) = if run == "repaired-current" { + ("siblings/selected/streamed/1/1", 507_233) + } else { + ("pending-body/selected/streamed/1/0", 507_794) + }; + assert!(failures.contains_key(key), "{run}: {error}"); + let leg = readiness_legs() + .into_iter() + .find(|leg| leg.key() == key) + .unwrap(); + let probe = probes + .iter() + .find(|p| { + p["case"] == leg.case + && p["mode"] == leg.mode + && p["consumption"] == leg.consumption + && p["read_limit"] == leg.read_limit + && p["repetition"] == leg.repetition + }) + .unwrap(); + assert_eq!(probe["completed_us"], completed, "{run}"); + } + } + } + + #[test] + fn readiness_replays_raw_instrumented_matrices_without_overwriting_history() { + use std::{ + collections::BTreeMap, + fs::File, + io::{BufRead, BufReader}, + }; + let path = crate::root() + .join("../../../docs/audits/evidence/2026-10-23-fastly-timer-controls-probes.jsonl"); + let mut runs: BTreeMap> = BTreeMap::new(); + for line in BufReader::new(File::open(path).unwrap()).lines() { + let row: Value = serde_json::from_str(&line.unwrap()).unwrap(); + if row["kind"] == "body-probe" { + runs.entry(row["run"].as_str().unwrap().to_owned()) + .or_default() + .push(row["probe"].clone()); + } + } + assert_eq!(runs.len(), 4); + for (name, probes) in &runs { + assert_eq!(probes.len(), 135); + assert_eq!( + probes + .iter() + .map(|p| p["origin"]["accepted"].as_u64().unwrap()) + .sum::(), + 147 + ); + for probe in probes { + for sample in probe["samples"] + .as_array() + .unwrap() + .iter() + .filter(|s| s["operation"] == "select") + { + assert!(sample["abi_enter_us"].is_u64()); + assert!(sample["abi_return_us"].is_u64()); + } + assert_eq!(probe["qualification"], false); + } + let result = + validate_outbound_readiness(probes, "77819f712ad971ada5494c563dd86a3b64a2afa6"); + if name.starts_with("repaired") { + result.unwrap(); + } else { + let message = result.unwrap_err().to_string(); + assert!( + message.starts_with("24 readiness leg(s) failed:\n"), + "{message}" + ); + assert_eq!(message.matches("wrong case terminal").count(), 24); + } + } + } + + #[test] + fn readiness_replays_local_cleanup_overrun_without_hiding_failures() { + use std::{ + fs::File, + io::{BufRead, BufReader}, + }; + let path = crate::root() + .join("../../../docs/audits/evidence/2026-10-24-fastly-local-recheck-probes.jsonl"); + let rows: Vec = BufReader::new(File::open(path).unwrap()) + .lines() + .map(|line| serde_json::from_str(&line.unwrap()).unwrap()) + .collect(); + assert_eq!(rows.len(), 414); + let revision = "98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8"; + for (run, late_controls, failures) in [("repaired", 2, 1), ("stock", 3, 24)] { + let probes: Vec = rows + .iter() + .filter(|row| row["run"] == run && row["kind"] == "body-probe") + .map(|row| row["probe"].clone()) + .collect(); + let controls: Vec = rows + .iter() + .filter(|row| row["run"] == run && row["kind"] == "timer-control") + .map(|row| row["probe"].clone()) + .collect(); + assert_eq!(probes.len(), 135); + assert_eq!(controls.len(), 72); + assert_eq!( + probes + .iter() + .map(|p| p["origin"]["accepted"].as_u64().unwrap()) + .sum::(), + 147 + ); + crate::evidence::validate_timer_controls(&controls, revision).unwrap(); + assert_eq!( + controls + .iter() + .filter(|p| p["within_five_ms"] == false) + .count(), + late_controls + ); + let message = validate_outbound_readiness(&probes, revision) + .unwrap_err() + .to_string(); + assert!(message.starts_with(&format!("{failures} readiness leg(s) failed:\n"))); + if run == "stock" { + assert_eq!(message.matches("wrong case terminal").count(), 24); + continue; + } + assert!( + message.contains("trickle/selected/buffered/16384/1: reordered or late operation") + ); + let probe = probes + .iter() + .find(|p| { + p["case"] == "trickle" + && p["mode"] == "selected" + && p["consumption"] == "buffered" + && p["read_limit"] == 16384 + && p["repetition"] == 1 + }) + .unwrap(); + let samples = probe["samples"].as_array().unwrap(); + let selected = samples + .iter() + .rev() + .find(|s| s["operation"] == "select") + .unwrap(); + let finish = samples.iter().find(|s| s["operation"] == "finish").unwrap(); + let bound = + probe["deadline_us"].as_u64().unwrap() + probe["tolerance_us"].as_u64().unwrap(); + assert!(selected["abi_return_us"].as_u64().unwrap() <= bound); + assert!(finish["start_us"].as_u64().unwrap() <= bound); + assert!(finish["end_us"].as_u64().unwrap() > bound); + assert!(probe["completed_us"].as_u64().unwrap() > bound); + } + } + + #[test] + fn readiness_replays_mixed_and_full_buffer_runtime_comparisons() { + use std::{ + collections::BTreeMap, + fs::File, + io::{BufRead, BufReader}, + }; + let path = crate::root() + .join("../../../docs/audits/evidence/2026-10-25-fastly-mixed-readiness-probes.jsonl"); + let rows: Vec = BufReader::new(File::open(path).unwrap()) + .lines() + .map(|line| serde_json::from_str(&line.unwrap()).unwrap()) + .collect(); + assert_eq!(rows.len(), 675); + let revision = "92daf00ab8bb76b2d4791e727e698e3304cc15ca"; + for (run, baseline_failures, progress_failures, losses) in [ + ("repaired-before-formatting", 0, 2, 0), + ("stock-formatted", 25, 7, 30), + ("repaired-formatted", 0, 0, 0), + ] { + let probes: Vec<_> = rows + .iter() + .filter(|r| r["run"] == run && r["kind"] == "body-probe") + .map(|r| r["probe"].clone()) + .collect(); + let controls: Vec<_> = rows + .iter() + .filter(|r| r["run"] == run && r["kind"] == "timer-control") + .map(|r| r["probe"].clone()) + .collect(); + assert_eq!(probes.len(), 153); + assert_eq!(controls.len(), 72); + assert_eq!( + probes + .iter() + .map(|p| p["origin"]["accepted"].as_u64().unwrap()) + .sum::(), + 177 + ); + crate::evidence::validate_timer_controls(&controls, revision).unwrap(); + let (baseline, progress) = probes.split_at(135); + for (result, count) in [ + ( + validate_outbound_readiness(baseline, revision), + baseline_failures, + ), + ( + validate_outbound_readiness_progress(progress, revision), + progress_failures, + ), + ] { + if count == 0 { + result.unwrap(); + } else { + assert!( + result + .unwrap_err() + .to_string() + .starts_with(&format!("{count} readiness leg(s) failed:\n")) + ); + } + } + let actual_losses = probes + .iter() + .filter(|p| { + (p["case"] == "truncated" && p["mode"] == "selected") + || p["case"] == "malformed-body" + || p["case"] == "full-body-error" + }) + .filter(|p| p["slots"][0]["outcome"] != "error") + .count(); + assert_eq!(actual_losses, losses, "{run}"); + let groups: BTreeMap<_, Vec<_>> = + progress.iter().fold(BTreeMap::new(), |mut groups, p| { + groups + .entry(p["case"].as_str().unwrap()) + .or_default() + .push(p); + groups + }); + assert_eq!(groups.len(), 3); + for (case, samples) in groups { + assert_eq!(samples.len(), 6, "{run}/{case}"); + assert!(samples.iter().all(|p| { + p["samples"] + .as_array() + .unwrap() + .iter() + .any(|s| s["operation"] == "read" && s["bytes"] == 16_384) + })); + } + } + } + + #[test] + fn readiness_rejects_mutated_matrix() { + type Mutation = (&'static str, fn(&mut Vec)); + let mutations: &[Mutation] = &[ + ("missing leg", |p| { + p.pop(); + }), + ("duplicate leg", |p| { + p[1] = p[0].clone(); + }), + ("duplicate identity with unique token", |p| { + for key in ["case", "mode", "consumption", "read_limit", "repetition"] { + p[1][key] = p[0][key].clone(); + } + }), + ("foreign revision", |p| { + p[0]["revision"] = json!("b".repeat(40)); + }), + ("wrong token", |p| { + p[0]["token"] = json!("foreign"); + }), + ("duplicate token", |p| { + p[1]["token"] = p[0]["token"].clone(); + }), + ("invalid token", |p| { + p[0]["token"] = json!("bad/token"); + }), + ("wrong schema", |p| { + p[0]["schema"] = json!(1); + }), + ("wrong deadline", |p| { + p[0]["deadline_us"] = json!(500_001); + }), + ("widened tolerance", |p| { + p[0]["tolerance_us"] = json!(5001); + }), + ("qualification promotion", |p| { + p[0]["qualification"] = json!(true); + }), + ("unknown case", |p| { + p[0]["case"] = json!("unknown"); + }), + ("empty samples", |p| { + p[0]["samples"] = json!([]); + }), + ("missing samples", |p| { + p[0].as_object_mut().unwrap().remove("samples"); + }), + ("late completion", |p| { + p[0]["completed_us"] = json!(505_001); + }), + ("reordered sample", |p| { + operation(&mut p[0], "select")["start_us"] = json!(0); + }), + ("missing read exit", |p| { + operation(&mut p[0], "read") + .as_object_mut() + .unwrap() + .remove("end_us"); + }), + ("late read", |p| { + operation(&mut p[0], "read")["start_us"] = json!(500_000); + }), + ("wrong read capacity", |p| { + operation(&mut p[0], "read")["capacity"] = json!(16_384); + }), + ("oversized read", |p| { + operation(&mut p[0], "read")["bytes"] = json!(2); + }), + ("contradictory read", |p| { + operation(&mut p[0], "read")["error"] = json!("Other"); + }), + ("negative bytes", |p| { + operation(&mut p[0], "read")["bytes"] = json!(-1); + }), + ("string bytes", |p| { + operation(&mut p[0], "read")["bytes"] = json!("1"); + }), + ("null bytes", |p| { + operation(&mut p[0], "read")["bytes"] = Value::Null; + }), + ("missing read outcome", |p| { + operation(&mut p[0], "read") + .as_object_mut() + .unwrap() + .remove("bytes"); + }), + ("bad selection status", |p| { + operation(&mut p[0], "select")["status"] = json!("FastlyStatus::BADF"); + }), + ("missing raw return timestamp", |p| { + let selection = operation(&mut p[0], "select"); + selection["abi_enter_us"] = selection["start_us"].clone(); + }), + ("reversed raw interval", |p| { + let selection = operation(&mut p[0], "select"); + selection["abi_enter_us"] = selection["end_us"].clone(); + selection["abi_return_us"] = selection["start_us"].clone(); + }), + ("raw return outside observed interval", |p| { + let selection = operation(&mut p[0], "select"); + selection["abi_enter_us"] = selection["start_us"].clone(); + selection["abi_return_us"] = json!(selection["end_us"].as_u64().unwrap() + 1); + }), + ("out of range selection", |p| { + operation(&mut p[0], "select")["selected"] = json!(1); + }), + ("missing selection outcome", |p| { + operation(&mut p[0], "select") + .as_object_mut() + .unwrap() + .remove("selected"); + }), + ("wrong selected slot", |p| { + operation(&mut p[0], "select")["slots"] = json!([1]); + }), + ("wrong selected phase", |p| { + operation(&mut p[0], "select")["phases"] = json!(["body"]); + }), + ("selected head pending", |p| { + operation(&mut p[0], "head-poll")["outcome"] = json!("pending"); + }), + ("ready head also errors", |p| { + operation(&mut p[0], "head-poll")["error"] = json!("HttpProtocolError"); + }), + ("missing streamed handoff", |p| { + samples(leg(p, "eof", "streamed")).retain(|s| s["operation"] != "handoff"); + }), + ("duplicate handoff", |p| { + let probe = leg(p, "eof", "streamed"); + let mut handoff = operation(probe, "handoff").clone(); + handoff["end_us"] = handoff["start_us"].clone(); + samples(probe).insert(1, handoff); + }), + ("handoff retains selection", |p| { + let probe = leg(p, "eof", "streamed"); + let events = samples(probe); + let handoff = events + .iter() + .position(|s| s["operation"] == "handoff") + .unwrap(); + let mut event = events.remove(handoff); + let select = events + .iter() + .position(|s| s["operation"] == "select") + .unwrap(); + event["start_us"] = events[select]["end_us"].clone(); + event["end_us"] = event["start_us"].clone(); + events.insert(select + 1, event); + }), + ("handoff moved after first read", |p| { + let events = samples(leg(p, "eof", "streamed")); + let handoff = events + .iter() + .position(|s| s["operation"] == "handoff") + .unwrap(); + let mut event = events.remove(handoff); + let read = events + .iter() + .position(|s| s["operation"] == "read") + .unwrap(); + event["start_us"] = events[read]["end_us"].clone(); + event["end_us"] = event["start_us"].clone(); + events.insert(read + 1, event); + }), + ("source/terminal error mismatch", |p| { + let probe = leg(p, "malformed-body", "buffered"); + operation(probe, "finish")["error"] = json!("UnexpectedEof"); + probe["slots"][0]["error"] = json!("UnexpectedEof"); + }), + ("head source/terminal error mismatch", |p| { + let probe = leg(p, "malformed-head", "buffered"); + operation(probe, "finish")["error"] = json!("HttpIncompleteResponse"); + probe["slots"][0]["error"] = json!("HttpIncompleteResponse"); + }), + ("terminal/summary error mismatch", |p| { + p[0]["slots"][0]["error"] = json!("Other"); + }), + ("empty source error", |p| { + let probe = leg(p, "malformed-body", "buffered"); + samples(probe) + .iter_mut() + .find(|s| s.get("error").is_some()) + .unwrap()["error"] = json!(""); + }), + ("malformed late head error", |p| { + let probe = leg(p, "silent-head", "buffered"); + let events = samples(probe); + let wait = events + .iter() + .position(|s| s["operation"] == "select") + .unwrap(); + events[wait]["selected"] = json!(0); + events[wait]["end_us"] = json!(499_999); + events.insert( + wait + 1, + json!({"operation":"head-poll", "slot":0, + "start_us":499_999, "end_us":500_000, "outcome":"error"}), + ); + }), + ("EOF read again", |p| { + let probe = leg(p, "empty-eof", "buffered"); + let events = samples(probe); + let eof = events + .iter() + .position(|s| s["operation"] == "read") + .unwrap(); + let at = events[eof]["end_us"].as_u64().unwrap(); + let selection = json!({"operation":"select", "slots":[0], "phases":["body"], + "start_us":at, "end_us":at, "timeout_ms":((500_000 - at) / 1000).max(1), + "status":"FastlyStatus::OK", "selected":0}); + let read = json!({"operation":"read", "slot":0, "capacity":1, "bytes":0, "start_us":at, "end_us":at}); + events.splice(eof + 1..eof + 1, [selection, read]); + }), + ("source error read again", |p| { + let probe = &mut p[0]; + let events = samples(probe); + let error = events + .iter() + .position(|s| s["operation"] == "read" && s.get("error").is_some()) + .unwrap(); + let mut read = events[error].clone(); + read["start_us"] = read["end_us"].clone(); + events.insert(error + 1, read); + }), + ("EOF finishes another slot first", |p| { + let probe = leg(p, "siblings", "buffered"); + let events = samples(probe); + let terminal = events + .iter() + .position(|s| s["operation"] == "finish") + .unwrap(); + let eof_finish = events.remove(terminal); + let timeout = events.last().unwrap()["start_us"].clone(); + let mut eof_finish = eof_finish; + eof_finish["start_us"] = timeout.clone(); + eof_finish["end_us"] = timeout; + events.push(eof_finish); + }), + ("premature timeout", |p| { + operation(leg(p, "silent-head", "buffered"), "finish")["start_us"] = json!(499_999); + }), + ("silent head timeout without readiness", |p| { + samples(leg(p, "silent-head", "buffered")).retain(|s| s["operation"] != "select"); + }), + ("pending body timeout without body readiness", |p| { + samples(leg(p, "pending-body", "buffered")) + .retain(|s| s["operation"] != "select" || s["phases"][0] != "body"); + }), + ("exhausted timeout without another readiness wait", |p| { + let events = samples(leg(p, "exhausted", "buffered")); + let wait = events + .iter() + .rposition(|s| s["operation"] == "select") + .unwrap(); + events.remove(wait); + }), + ("bad origin token", |p| { + p[0]["origin"]["requests"][0]["token"] = json!("foreign"); + }), + ("origin watchdog", |p| { + p[0]["origin"]["watchdog"] = json!(true); + }), + ("missing origin terminations", |p| { + p[0]["origin"] + .as_object_mut() + .unwrap() + .remove("terminations"); + }), + ("missing worker termination", |p| { + p[0]["origin"]["terminations"] = json!([]); + }), + ("duplicate worker termination", |p| { + let probe = leg(p, "siblings", "buffered"); + probe["origin"]["terminations"][1] = probe["origin"]["terminations"][0].clone(); + }), + ("invalid termination slot", |p| { + p[0]["origin"]["terminations"][0]["slot"] = json!(1); + }), + ("watchdog worker termination", |p| { + p[0]["origin"]["terminations"][0]["reason"] = json!("watchdog"); + }), + ("I/O error worker termination", |p| { + p[0]["origin"]["terminations"][0]["reason"] = json!("io-error"); + }), + ("unknown worker termination", |p| { + p[0]["origin"]["terminations"][0]["reason"] = json!("unknown"); + }), + ("missing origin writes", |p| { + p[0]["origin"].as_object_mut().unwrap().remove("writes"); + }), + ("invalid write slot", |p| { + p[0]["origin"]["writes"][0]["slot"] = json!(1); + }), + ("late origin write", |p| { + p[0]["origin"]["writes"][0]["at_us"] = json!(3_000_001); + }), + ("zero-byte origin write", |p| { + p[0]["origin"]["writes"][0]["bytes"] = json!(0); + }), + ("silent origin head write", |p| { + leg(p, "silent-head", "buffered")["origin"]["writes"] = json!([ + {"slot":0, "kind":"head", "at_us":1, "bytes":1} + ]); + }), + ("head-error origin head write", |p| { + leg(p, "head-error", "buffered")["origin"]["writes"] = json!([ + {"slot":0, "kind":"head", "at_us":1, "bytes":1} + ]); + }), + ("missing origin receipt", |p| { + p[0]["origin"]["requests"] = json!([]); + }), + ("wrong checksum", |p| { + p[0]["slots"][0]["checksum"] = json!("0000000000000000"); + }), + ("wrong consumption", |p| { + leg(p, "eof", "streamed")["slots"][0]["buffered_bytes"] = json!(3); + }), + ]; + let baseline = matrix(); + validate_outbound_readiness(&baseline, REVISION).unwrap(); + let accepted: Vec<_> = mutations + .iter() + .filter_map(|(name, mutate)| { + let mut probes = baseline.clone(); + mutate(&mut probes); + validate_outbound_readiness(&probes, REVISION) + .is_ok() + .then_some(*name) + }) + .collect(); + assert!( + accepted.is_empty(), + "accepted malformed traces: {accepted:?}" + ); + } +} diff --git a/crates/edgezero-test-harness/src/evidence/timers.rs b/crates/edgezero-test-harness/src/evidence/timers.rs new file mode 100644 index 000000000..b59f48a09 --- /dev/null +++ b/crates/edgezero-test-harness/src/evidence/timers.rs @@ -0,0 +1,463 @@ +use super::{Result, require}; +use serde_json::Value; +use std::collections::HashSet; + +#[derive(Clone, Copy, Debug)] +pub struct TimerLeg { + pub source: &'static str, + pub budget_ms: u64, + pub repetition: usize, +} + +impl TimerLeg { + pub fn key(&self) -> String { + format!("{}/{}/{}", self.source, self.budget_ms, self.repetition) + } +} + +pub fn timer_legs() -> Vec { + let mut legs = Vec::with_capacity(72); + for repetition in 0..8 { + for budget_ms in [1, 100, 500] { + for source in ["native-sleep", "empty-select", "wasi-sleep"] { + legs.push(TimerLeg { + source, + budget_ms, + repetition, + }); + } + } + } + legs +} + +pub fn validate_timer_controls(probes: &[Value], revision: &str) -> Result<()> { + require( + revision.len() == 40 && revision.bytes().all(|b| b.is_ascii_hexdigit()), + "invalid expected timer revision", + )?; + let legs = timer_legs(); + require( + probes.len() == legs.len(), + "missing timer control inventory", + )?; + let mut identities = HashSet::new(); + let mut tokens = HashSet::new(); + for probe in probes { + let leg = legs + .iter() + .find(|leg| { + probe["source"] == leg.source + && probe["budget_ms"].as_u64() == Some(leg.budget_ms) + && probe["repetition"].as_u64() == Some(leg.repetition as u64) + }) + .ok_or("unknown timer control leg")?; + let token = probe["token"] + .as_str() + .ok_or("missing timer control token")?; + require( + probe["schema"].as_u64() == Some(1) + && probe["event"] == "readiness-timer-control" + && probe["revision"] == revision + && probe["qualification"].as_bool() == Some(false) + && !token.is_empty() + && token.len() <= 128 + && token + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-') + && identities.insert(leg.key()) + && tokens.insert(token), + "invalid/duplicate timer control identity", + )?; + let elapsed = probe["elapsed_us"] + .as_u64() + .ok_or("missing timer elapsed")?; + let budget = leg.budget_ms * 1000; + require( + (budget..=12_000_000).contains(&elapsed), + "early or unbounded timer control", + )?; + require( + probe["overrun_us"].as_u64() == Some(elapsed - budget) + && probe["within_five_ms"].as_bool() == Some(elapsed <= budget + 5000), + "incorrect timer overrun or lateness classification", + )?; + let primitive_end = if let Some(endpoint) = probe.get("measurement_end") { + require( + endpoint == "primitive-return", + "unknown timer measurement endpoint", + )?; + true + } else { + false + }; + if leg.source == "empty-select" { + let entered = probe["abi_enter_us"] + .as_u64() + .ok_or("missing timer ABI entry")?; + let returned = probe["abi_return_us"] + .as_u64() + .ok_or("missing timer ABI return")?; + require( + entered <= returned + && returned <= elapsed + && returned - entered >= budget + && (!primitive_end || returned == elapsed) + && probe["abi_status"] == "FastlyStatus::OK" + && probe.get("selected").is_some_and(Value::is_null), + "invalid timer ABI timestamps, status or selection", + )?; + } else { + require( + ["abi_enter_us", "abi_return_us", "abi_status", "selected"] + .iter() + .all(|field| probe.get(field).is_none()), + "sleep control contains ABI observations", + )?; + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::TimerLeg; + use crate::evidence::{timer_legs, validate_timer_controls}; + use serde_json::{Value, json}; + use std::collections::HashSet; + + const REVISION: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + fn matrix() -> Vec { + let mut probes = Vec::new(); + for repetition in 0..8 { + for budget_ms in [1, 100, 500] { + for source in ["native-sleep", "empty-select", "wasi-sleep"] { + let elapsed = budget_ms * 1000; + let mut probe = json!({ + "schema":1, "event":"readiness-timer-control", "source":source, + "budget_ms":budget_ms, "repetition":repetition, + "token":format!("{source}-{budget_ms}-{repetition}"), + "revision":REVISION, "qualification":false, + "elapsed_us":elapsed, "overrun_us":0, "within_five_ms":true, + }); + if source == "empty-select" { + probe["abi_enter_us"] = json!(0); + probe["abi_return_us"] = json!(elapsed); + probe["abi_status"] = json!("FastlyStatus::OK"); + probe["selected"] = Value::Null; + } + probes.push(probe); + } + } + } + probes + } + + fn elapsed(probe: &mut Value, us: u64) { + let budget = probe["budget_ms"].as_u64().unwrap() * 1000; + probe["elapsed_us"] = json!(us); + probe["overrun_us"] = json!(us - budget); + probe["within_five_ms"] = json!(us <= budget + 5000); + if probe["source"] == "empty-select" { + probe["abi_return_us"] = json!(us); + } + } + + #[test] + fn timer_inventory_is_exactly_seventy_two_unique_legs() { + let legs = timer_legs(); + assert_eq!(legs.len(), 72); + let actual: HashSet<_> = legs.iter().map(TimerLeg::key).collect(); + let expected: HashSet<_> = matrix() + .iter() + .map(|probe| { + format!( + "{}/{}/{}", + probe["source"].as_str().unwrap(), + probe["budget_ms"], + probe["repetition"] + ) + }) + .collect(); + assert_eq!(actual, expected); + for leg in legs { + assert_eq!( + leg.key(), + format!("{}/{}/{}", leg.source, leg.budget_ms, leg.repetition) + ); + } + } + + #[test] + fn timer_accepts_complete_inventory() { + validate_timer_controls(&matrix(), REVISION).unwrap(); + } + + #[test] + fn timer_replays_retained_runtime_controls_without_hiding_lateness() { + use std::{ + collections::BTreeMap, + fs::File, + io::{BufRead, BufReader}, + }; + let path = crate::root() + .join("../../../docs/audits/evidence/2026-10-23-fastly-timer-controls-probes.jsonl"); + let mut runs: BTreeMap> = BTreeMap::new(); + for line in BufReader::new(File::open(path).unwrap()).lines() { + let row: Value = serde_json::from_str(&line.unwrap()).unwrap(); + if row["kind"] == "timer-control" { + runs.entry(row["run"].as_str().unwrap().to_owned()) + .or_default() + .push(row["probe"].clone()); + } + } + assert_eq!(runs.len(), 4); + for (name, late, late_source) in [ + ("repaired", 5, "native-sleep"), + ("stock", 7, "native-sleep"), + ("repaired-reviewed", 0, "native-sleep"), + ("stock-reviewed", 1, "wasi-sleep"), + ] { + let probes = &runs[name]; + validate_timer_controls(probes, "77819f712ad971ada5494c563dd86a3b64a2afa6").unwrap(); + assert_eq!(probes.len(), 72); + let retained_late: Vec<_> = probes + .iter() + .filter(|p| p["within_five_ms"] == false) + .collect(); + assert_eq!(retained_late.len(), late); + assert!(retained_late.iter().all(|p| p["source"] == late_source)); + assert!(probes.iter().all(|p| p["qualification"] == false)); + if name.ends_with("-reviewed") { + assert!( + probes + .iter() + .all(|p| p["measurement_end"] == "primitive-return") + ); + for probe in probes.iter().filter(|p| p["source"] == "empty-select") { + assert_eq!(probe["elapsed_us"], probe["abi_return_us"]); + } + } + } + } + + #[test] + fn timer_accepts_late_observations_without_relabelling_them() { + let mut probes = matrix(); + for probe in &mut probes { + let budget = probe["budget_ms"].as_u64().unwrap() * 1000; + elapsed(probe, budget + 5001); + } + let retained = probes.clone(); + validate_timer_controls(&probes, REVISION).unwrap(); + assert_eq!(probes, retained); + assert!(probes.iter().all(|probe| probe["within_five_ms"] == false)); + } + + #[test] + fn timer_accepts_exact_tolerance_and_containment_boundaries() { + for overrun in [0, 5000, 5001] { + let mut probes = matrix(); + for probe in &mut probes { + let budget = probe["budget_ms"].as_u64().unwrap() * 1000; + elapsed(probe, budget + overrun); + } + validate_timer_controls(&probes, REVISION).unwrap(); + } + let mut probes = matrix(); + for probe in &mut probes { + elapsed(probe, 12_000_000); + } + probes[1]["abi_enter_us"] = json!(12_000_000 - 1000); + probes[0]["token"] = json!("a".repeat(128)); + validate_timer_controls(&probes, REVISION).unwrap(); + } + + #[test] + fn timer_rejects_missing_extra_or_duplicate_inventory() { + assert!(validate_timer_controls(&[], REVISION).is_err()); + let baseline = matrix(); + for missing in 0..baseline.len() { + let mut probes = baseline.clone(); + probes.remove(missing); + assert!(validate_timer_controls(&probes, REVISION).is_err()); + } + let mut probes = baseline.clone(); + probes.push(probes[0].clone()); + assert!(validate_timer_controls(&probes, REVISION).is_err()); + let mut probes = baseline; + probes[1] = probes[0].clone(); + probes[1]["token"] = json!("unique-duplicate-leg-token"); + assert!(validate_timer_controls(&probes, REVISION).is_err()); + } + + #[test] + fn timer_rejects_invalid_or_foreign_identity() { + let mutations = [ + ("schema", json!(2)), + ("event", json!("readiness-sample")), + ("source", json!("unknown")), + ("budget_ms", json!(0)), + ("budget_ms", json!(493)), + ("budget_ms", json!("1")), + ("budget_ms", json!(1.0)), + ("repetition", json!(8)), + ("repetition", json!(-1)), + ("repetition", json!(0.0)), + ("revision", json!("b".repeat(40))), + ("revision", json!("a".repeat(39))), + ("revision", json!("g".repeat(40))), + ("qualification", json!(true)), + ("qualification", json!("false")), + ("token", json!("")), + ("token", json!("bad/token")), + ("token", json!("bad_token")), + ("token", json!("bad token")), + ("token", json!("bad\n")), + ("token", json!("a".repeat(129))), + ("token", json!(123)), + ("token", json!("native-sleep-100-0")), + ]; + for (field, value) in mutations { + let mut probes = matrix(); + probes[0][field] = value.clone(); + assert!( + validate_timer_controls(&probes, REVISION).is_err(), + "accepted {field}={value}" + ); + } + let mut probes = matrix(); + probes[0]["token"] = json!("\u{e9}"); + assert!(validate_timer_controls(&probes, REVISION).is_err()); + } + + #[test] + fn timer_rejects_invalid_expected_revision() { + for revision in ["", &"a".repeat(39), &"g".repeat(40), &"a".repeat(41)] { + assert!(validate_timer_controls(&matrix(), revision).is_err()); + } + } + + #[test] + fn timer_rejects_missing_required_metadata() { + for field in [ + "schema", + "event", + "source", + "budget_ms", + "repetition", + "token", + "revision", + "qualification", + "elapsed_us", + "overrun_us", + "within_five_ms", + ] { + let mut probes = matrix(); + probes[0].as_object_mut().unwrap().remove(field); + assert!( + validate_timer_controls(&probes, REVISION).is_err(), + "accepted missing {field}" + ); + } + } + + #[test] + fn timer_rejects_forged_or_unbounded_elapsed_accounting() { + let mutations = [ + ("elapsed_us", json!(999)), + ("elapsed_us", json!(12_000_001)), + ("elapsed_us", json!(-1)), + ("elapsed_us", json!(1000.0)), + ("elapsed_us", json!("1000")), + ("overrun_us", json!(1)), + ("overrun_us", json!(-1)), + ("overrun_us", json!(0.0)), + ("within_five_ms", json!(false)), + ("within_five_ms", json!("true")), + ]; + for (field, value) in mutations { + let mut probes = matrix(); + probes[0][field] = value.clone(); + assert!( + validate_timer_controls(&probes, REVISION).is_err(), + "accepted {field}={value}" + ); + } + let mut probes = matrix(); + elapsed(&mut probes[0], 6001); + probes[0]["within_five_ms"] = json!(true); + assert!(validate_timer_controls(&probes, REVISION).is_err()); + probes[0]["within_five_ms"] = json!(false); + probes[0]["overrun_us"] = json!(5000); + assert!(validate_timer_controls(&probes, REVISION).is_err()); + } + + #[test] + fn timer_rejects_invalid_or_missing_empty_select_abi_observations() { + let mutations = [ + ("abi_enter_us", json!(-1)), + ("abi_enter_us", json!(1001)), + ("abi_enter_us", json!(0.0)), + ("abi_return_us", json!(1001)), + ("abi_return_us", json!("1000")), + ("abi_status", json!("FastlyStatus::INVAL")), + ("abi_status", Value::Null), + ("selected", json!(0)), + ("selected", json!(u32::MAX)), + ("selected", json!("null")), + ]; + for (field, value) in mutations { + let mut probes = matrix(); + probes[1][field] = value.clone(); + assert!( + validate_timer_controls(&probes, REVISION).is_err(), + "accepted {field}={value}" + ); + } + for field in ["abi_enter_us", "abi_return_us", "abi_status", "selected"] { + let mut probes = matrix(); + probes[1].as_object_mut().unwrap().remove(field); + assert!( + validate_timer_controls(&probes, REVISION).is_err(), + "accepted missing {field}" + ); + } + let mut probes = matrix(); + probes[1]["abi_enter_us"] = json!(999); + probes[1]["abi_return_us"] = json!(998); + assert!(validate_timer_controls(&probes, REVISION).is_err()); + } + + #[test] + fn timer_rejects_a_declared_primitive_end_that_includes_later_work() { + let mut probes = matrix(); + elapsed(&mut probes[1], 1100); + probes[1]["abi_return_us"] = json!(1000); + probes[1]["measurement_end"] = json!("primitive-return"); + assert!(validate_timer_controls(&probes, REVISION).is_err()); + } + + #[test] + fn timer_rejects_an_abi_wait_shorter_than_its_positive_timeout() { + let mut probes = matrix(); + probes[1]["abi_return_us"] = json!(999); + assert!(validate_timer_controls(&probes, REVISION).is_err()); + } + + #[test] + fn timer_rejects_abi_fields_on_native_and_wasi_sleep() { + for index in [0, 2] { + for field in ["abi_enter_us", "abi_return_us", "abi_status", "selected"] { + let mut probes = matrix(); + probes[index][field] = Value::Null; + assert!( + validate_timer_controls(&probes, REVISION).is_err(), + "accepted {field} on {}", + probes[index]["source"] + ); + } + } + } +} diff --git a/crates/edgezero-test-harness/src/main.rs b/crates/edgezero-test-harness/src/main.rs new file mode 100644 index 000000000..aebaf7b36 --- /dev/null +++ b/crates/edgezero-test-harness/src/main.rs @@ -0,0 +1,1130 @@ +mod evidence; +mod net; +mod runners; +use evidence::{Result, require}; +use serde_json::{Value, json}; +use std::{ + fs::{self, OpenOptions}, + io::{Read, Write}, + path::{Path, PathBuf}, + process::{Child, Command, ExitStatus, Stdio}, + sync::{ + Mutex, PoisonError, + atomic::{AtomicU64, Ordering}, + }, + thread, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + +pub struct Args { + adapter: String, + suite: String, + output: Option, + requests: usize, + repetitions: usize, + seed: u64, + construction_rounds: u64, + max_requests: usize, +} +impl Args { + fn parse() -> Result { + Self::parse_from(std::env::args().skip(1)) + } + + fn parse_from(arguments: I) -> Result + where + I: IntoIterator, + { + let mut a = Self { + adapter: "fastly".into(), + suite: "smoke".into(), + output: None, + requests: 100, + repetitions: 3, + seed: 856, + construction_rounds: 0, + max_requests: 10, + }; + let mut adapter_selected = false; + let mut args = arguments.into_iter(); + while let Some(key) = args.next() { + if key == "--help" || key == "-h" { + println!( + "edgezero-test-harness [--adapter fastly|cloudflare|spin|axum|all] [--suite smoke|benchmark|assembly|outbound-readiness|outbound-client-progress|outbound-progress|outbound-overflow|outbound-wire|outbound-lifetimes|response-framing|synthesized-headers] [--output EMPTY_DIR] [--requests 100] [--repetitions 3] [--seed 856] [--construction-rounds 0] [--max-requests 10]" + ); + std::process::exit(0); + } + let value = args.next().ok_or(format!("missing value for {key}"))?; + match key.as_str() { + "--adapter" => { + a.adapter = value; + adapter_selected = true; + } + "--suite" => a.suite = value, + "--output" => a.output = Some(value.into()), + "--requests" => a.requests = value.parse()?, + "--repetitions" => a.repetitions = value.parse()?, + "--seed" => a.seed = value.parse()?, + "--construction-rounds" => a.construction_rounds = value.parse()?, + "--max-requests" => a.max_requests = value.parse()?, + _ => return Err(format!("unknown option {key}").into()), + } + } + require( + ["fastly", "cloudflare", "spin", "axum", "all"].contains(&a.adapter.as_str()), + "unknown adapter", + )?; + require( + [ + "smoke", + "benchmark", + "assembly", + "outbound-readiness", + "outbound-client-progress", + "outbound-progress", + "outbound-overflow", + "outbound-wire", + "outbound-lifetimes", + "response-framing", + "synthesized-headers", + ] + .contains(&a.suite.as_str()), + "unknown suite", + )?; + require( + !matches!( + a.suite.as_str(), + "outbound-readiness" | "outbound-client-progress" + ) || a.adapter == "fastly", + "Fastly receive suites require --adapter fastly", + )?; + require( + !matches!(a.suite.as_str(), "outbound-wire" | "outbound-lifetimes") + || (adapter_selected && a.adapter != "all"), + "wire and lifetime suites require one explicit adapter", + )?; + require( + a.requests > 0 && a.repetitions > 0, + "requests and repetitions must be positive", + )?; + require( + (1..=1000).contains(&a.max_requests), + "max-requests must be 1..1000", + )?; + Ok(a) + } +} +pub fn root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/fixtures/reusable-app") + .canonicalize() + .expect("fixture workspace root") +} +pub fn token() -> String { + static NEXT: AtomicU64 = AtomicU64::new(0); + format!( + "{:x}-{:x}-{:x}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(), + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + ) +} +pub struct Records { + path: PathBuf, + values: Vec, +} +impl Records { + fn collect_clients(&mut self, fields: Value) -> Result<()> { + for event in net::take_client_events() { + if !self.values.iter().any(|existing| { + existing["event"] == "client_completed" && existing["token"] == event["token"] + }) { + self.push(annotate(event, fields.clone()))?; + } + } + Ok(()) + } + fn push(&mut self, v: Value) -> Result<()> { + if matches!( + v["status"].as_str(), + Some("fail" | "unverified" | "unsupported") + ) { + eprintln!("edgezero-test-harness: {v}"); + } + writeln!( + OpenOptions::new() + .create(true) + .append(true) + .open(&self.path)?, + "{v}" + )?; + self.values.push(v); + Ok(()) + } +} +pub fn logs(path: &Path) -> Vec { + fs::read_to_string(path) + .unwrap_or_default() + .lines() + .filter(|l| l.starts_with('{')) + .filter_map(|l| serde_json::from_str(l).ok()) + .collect() +} +pub fn await_evidence(mut check: impl FnMut() -> Result<()>) -> Result<()> { + let start = Instant::now(); + loop { + match check() { + Ok(()) => return Ok(()), + Err(e) if start.elapsed() >= Duration::from_secs(5) => return Err(e), + Err(_) => thread::sleep(Duration::from_millis(50)), + } + } +} +pub fn executable(variable: &str, default: &str) -> Option { + let value = std::env::var(variable).unwrap_or_else(|_| default.into()); + let path = PathBuf::from(&value); + if path.components().count() > 1 { + return path.canonicalize().ok(); + } + std::env::split_paths(&std::env::var_os("PATH")?) + .map(|p| p.join(&value)) + .find(|p| p.is_file()) + .and_then(|p| p.canonicalize().ok()) +} +pub fn checked(command: &mut Command) -> Result<()> { + let status = command.status()?; + require( + status.success(), + format!("command {command:?} failed: {status}"), + ) +} +/// Process groups of live runtimes, killed by the Ctrl-C handler because a +/// terminal SIGINT reaches only the harness's own foreground group. +static LIVE_GROUPS: Mutex> = Mutex::new(Vec::new()); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum SignalOutcome { + Sent, + Absent, +} + +fn requested_termination(status: ExitStatus, term_sent: bool) -> bool { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + status.signal() == Some(15) || (term_sent && status.code() == Some(128 + 15)) + } + #[cfg(not(unix))] + { + let _ = (status, term_sent); + false + } +} +fn wait_for_exit(child: &mut Child, deadline: Instant) -> Result> { + loop { + if let Some(status) = child.try_wait()? { + return Ok(Some(status)); + } + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Ok(None); + } + thread::sleep(remaining.min(Duration::from_millis(10))); + } +} +fn signal_group(id: u32, signal: &str, deadline: Instant) -> Result { + #[cfg(unix)] + let target = format!("-{id}"); + #[cfg(not(unix))] + let target = id.to_string(); + signal_group_command( + Command::new("kill") + .args([signal, "--", &target]) + .env("LC_ALL", "C"), + deadline, + ) +} +fn signal_group_command(command: &mut Command, deadline: Instant) -> Result { + let remaining = deadline.saturating_duration_since(Instant::now()); + require( + !remaining.is_zero(), + "process-group signal deadline expired", + )?; + let execution_deadline = deadline - (remaining / 2).min(Duration::from_secs(1)); + let path = std::env::temp_dir().join(format!("fixture-signal-{}.log", token())); + let file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .ok(); + let captured = file.is_some(); + let outcome = (|| { + let mut helper = command + .stdout(Stdio::null()) + .stderr(file.map_or_else(Stdio::null, Stdio::from)) + .spawn()?; + let status = match wait_for_exit(&mut helper, execution_deadline) { + Ok(Some(status)) => status, + outcome => { + let mut failure = outcome.err().map_or_else( + || "process-group signal helper timed out".to_owned(), + |error| error.to_string(), + ); + if let Err(error) = helper.kill() { + failure.push_str(&format!("; kill signal helper: {error}")); + } + match wait_for_exit(&mut helper, deadline) { + Ok(Some(_)) => {} + Ok(None) => failure.push_str("; signal helper was not reaped before deadline"), + Err(error) => failure.push_str(&format!("; reap signal helper: {error}")), + } + return Err(failure.into()); + } + }; + if status.success() { + return Ok(SignalOutcome::Sent); + } + require( + captured, + format!("process-group signal failed: {status}; diagnostics unavailable"), + )?; + let mut diagnostic = String::new(); + // A descendant may retain stderr; a regular-file read never waits for its EOF. + fs::File::open(&path)? + .take(4096) + .read_to_string(&mut diagnostic)?; + require( + diagnostic.to_ascii_lowercase().contains("no such process"), + format!("process-group signal failed: {status}: {diagnostic}"), + )?; + Ok(SignalOutcome::Absent) + })(); + if captured { + // Diagnostics must not determine whether a successfully sent signal is confirmed. + let _ = fs::remove_file(&path); + } + outcome +} +fn kill_live_groups_and_exit() { + for id in LIVE_GROUPS + .lock() + .unwrap_or_else(PoisonError::into_inner) + .iter() + { + let _ = signal_group(*id, "-KILL", Instant::now() + Duration::from_secs(5)); + } + std::process::exit(130); +} +pub struct Process(Child, ProcessState); +struct ProcessState { + log: PathBuf, + finished: Option>, + shutdown_started: bool, + failures: Vec, +} +impl Process { + pub fn spawn(command: &mut Command, log: &Path) -> Result { + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + command.process_group(0); + } + let file = fs::File::create(log)?; + let p = Self( + command + .stdout(Stdio::from(file.try_clone()?)) + .stderr(Stdio::from(file)) + .spawn()?, + ProcessState { + log: log.to_owned(), + finished: None, + shutdown_started: false, + failures: Vec::new(), + }, + ); + LIVE_GROUPS + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push(p.0.id()); + Ok(p) + } + pub fn start(command: &mut Command, log: &Path, port: u16) -> Result { + let mut p = Self::spawn(command, log)?; + let start = Instant::now(); + loop { + p.check_running()?; + if std::net::TcpStream::connect_timeout( + &std::net::SocketAddr::from(([127, 0, 0, 1], port)), + Duration::from_millis(200), + ) + .is_ok() + { + p.check_running()?; + return Ok(p); + } + require( + start.elapsed() < Duration::from_secs(45), + "runtime did not listen", + )?; + thread::sleep(Duration::from_millis(50)); + } + } + pub fn check_running(&mut self) -> Result<()> { + if let Some(outcome) = &self.1.finished { + return Err(outcome + .as_ref() + .err() + .cloned() + .unwrap_or_else(|| { + format!("runtime already finished; see {}", self.1.log.display()) + }) + .into()); + } + require(!self.1.shutdown_started, "runtime cleanup is still pending")?; + match self.0.try_wait()? { + None => Ok(()), + Some(status) => Err(format!( + "runtime exited before shutdown: {status}; see {}", + self.1.log.display() + ) + .into()), + } + } + pub fn finish(&mut self) -> Result<()> { + if let Some(outcome) = &self.1.finished { + return outcome.clone().map_err(Into::into); + } + let mut failures = self.1.failures.clone(); + if !self.1.shutdown_started { + if let Err(error) = self.check_running() { + failures.push(error.to_string()); + } + self.1.shutdown_started = true; + } + let id = self.0.id(); + let term_deadline = Instant::now() + Duration::from_secs(5); + let term_sent = match signal_group(id, "-TERM", term_deadline) { + Ok(outcome) => outcome == SignalOutcome::Sent, + Err(error) => { + failures.push(error.to_string()); + false + } + }; + let mut status = match wait_for_exit(&mut self.0, term_deadline) { + Ok(status) => status, + Err(error) => { + failures.push(error.to_string()); + None + } + }; + if status.is_none() { + failures.push("runtime required SIGKILL after its five-second TERM grace".to_owned()); + } + // The parent may have exited while a descendant still holds sockets. + let kill_deadline = Instant::now() + Duration::from_secs(5); + #[cfg(unix)] + let group_cleaned = match signal_group(id, "-KILL", kill_deadline) { + Ok(_) => true, + Err(error) => { + failures.push(error.to_string()); + false + } + }; + #[cfg(not(unix))] + let group_cleaned = true; + if status.is_none() + && (cfg!(not(unix)) || !group_cleaned) + && let Err(error) = self.0.kill() + { + failures.push(error.to_string()); + } + if status.is_none() { + match wait_for_exit(&mut self.0, kill_deadline) { + Ok(observed) => status = observed, + Err(error) => failures.push(error.to_string()), + } + } + match status { + Some(status) => { + let terminated = requested_termination(status, term_sent); + if !status.success() && !terminated { + failures.push(format!( + "runtime exited unexpectedly during shutdown: {status}" + )); + } + } + None => { + failures.push("runtime did not exit within its five-second KILL grace".to_owned()) + } + } + let confirmed = status.is_some() && group_cleaned; + if confirmed { + LIVE_GROUPS + .lock() + .unwrap_or_else(PoisonError::into_inner) + .retain(|live| *live != id); + } + self.1.failures = failures.clone(); + let outcome = if failures.is_empty() { + Ok(()) + } else { + Err(format!( + "{}; see {}", + failures.join("; "), + self.1.log.display() + )) + }; + if confirmed { + self.1.finished = Some(outcome.clone()); + } + outcome.map_err(Into::into) + } +} +impl Drop for Process { + fn drop(&mut self) { + let _ = self.finish(); + } +} +pub fn req(port: u16, path: &str) -> Result { + net::request(port, path, &token(), None, None) +} +pub fn guest(reply: &Value) -> Result { + require( + reply["status"] == 200, + format!("unexpected response: {reply}"), + )?; + Ok(serde_json::from_str( + reply["body"].as_str().ok_or("missing body")?, + )?) +} +pub fn annotate(mut value: Value, fields: Value) -> Value { + value + .as_object_mut() + .unwrap() + .extend(fields.as_object().unwrap().clone()); + value +} +pub fn artifact(path: &Path) -> Result { + let bytes = fs::read(path)?; + let fingerprint = bytes.iter().fold(0xcbf29ce484222325u64, |hash, byte| { + (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3) + }); + Ok( + json!({"path":path,"bytes":bytes.len(),"fnv1a64":format!("{fingerprint:016x}"),"purpose":"non-security build identity"}), + ) +} +/// The compiler that `$CARGO` builds with: `$RUSTC` when set, else the +/// `rustc` beside `$CARGO`, else whichever `rustc` is on `PATH`. +fn rustc() -> PathBuf { + if let Some(rustc) = std::env::var_os("RUSTC") { + return rustc.into(); + } + std::env::var_os("CARGO") + .map(|cargo| { + Path::new(&cargo).with_file_name(format!("rustc{}", std::env::consts::EXE_SUFFIX)) + }) + .filter(|rustc| rustc.is_file()) + .unwrap_or_else(|| "rustc".into()) +} +fn run() -> Result { + let args = Args::parse()?; + ctrlc::set_handler(kill_live_groups_and_exit)?; + let path = args + .output + .clone() + .unwrap_or_else(|| root().join(".runs").join(token())); + if path.exists() { + require( + fs::read_dir(&path)?.next().is_none(), + "output directory must be empty; refusing to overwrite evidence", + )?; + } + fs::create_dir_all(&path)?; + let path = path.canonicalize()?; + fs::copy(root().join("Cargo.lock"), path.join("Cargo.lock"))?; + fs::copy( + Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.lock"), + path.join("harness-Cargo.lock"), + )?; + let mut records = Records { + path: path.join("events.jsonl"), + values: Vec::new(), + }; + records.push(json!({"event":"run","seed":args.seed,"suite":args.suite,"construction_rounds":args.construction_rounds,"max_requests":args.max_requests}))?; + records.push(json!({"event":"build_environment","rustc":String::from_utf8_lossy(&Command::new(rustc()).arg("--version").output()?.stdout).trim(),"host_os":std::env::consts::OS,"host_arch":std::env::consts::ARCH,"lockfile":artifact(&path.join("Cargo.lock"))?,"harness_lockfile":artifact(&path.join("harness-Cargo.lock"))?}))?; + let adapters = if args.adapter == "all" { + vec!["fastly", "cloudflare", "spin", "axum"] + } else { + vec![args.adapter.as_str()] + }; + for adapter in adapters { + let recorded = records.values.len(); + let result = if adapter == "fastly" { + runners::fastly(&args, &path, &mut records) + } else { + runners::provider(adapter, &args, &path, &mut records) + }; + if let Err(error) = result { + // Runners record their own contextual failure for this error; add one only + // when they did not (an earlier non-fatal fail record must not hide it). + let error = error.to_string(); + if !records.values[recorded..] + .iter() + .any(|r| r["status"] == "fail" && r["error"] == error.as_str()) + { + records.push(json!({"status":"fail","adapter":adapter,"error":error}))?; + } + } + } + let summary = evidence::summarize(&records.values); + if summary["build_identity"]["status"] == "invalid" { + let builds = records + .values + .iter() + .filter(|r| r["event"] == "build") + .map(|r| json!({"variant":r["variant"],"adapter":r["adapter"],"repetition":r["repetition"],"fnv1a64":r["artifact"]["fnv1a64"]})) + .collect::>(); + records + .push(json!({"status":"fail","reason":"mixed build fingerprints","builds":builds}))?; + } + let code = if records.values.iter().any(|r| r["status"] == "fail") { + 1 + } else if records + .values + .iter() + .any(|r| r["status"] == "unverified" || r["status"] == "unsupported") + { + 2 + } else { + 0 + }; + fs::write( + path.join("summary.json"), + serde_json::to_string_pretty(&summary)?, + )?; + for (key, value) in summary["variants"].as_object().unwrap() { + println!( + "{key}: {} samples, completion p50 {:.3} ms", + value["samples"], + value["completion_ns"]["p50"].as_f64().unwrap_or(0.) / 1_000_000. + ); + } + println!("{}", json!({"exit_code":code,"evidence":path})); + Ok(code) +} +fn main() { + match run() { + Ok(code) => std::process::exit(code), + Err(error) => { + eprintln!("edgezero-test-harness: {error}"); + std::process::exit(1); + } + } +} + +#[cfg(test)] +mod layout_tests { + use super::*; + + fn metadata(manifest: &Path) -> Value { + let output = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + .args([ + "metadata", + "--offline", + "--locked", + "--no-deps", + "--format-version", + "1", + "--manifest-path", + ]) + .arg(manifest) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice(&output.stdout).unwrap() + } + + #[test] + fn runner_workspace_is_separate_from_runtime_and_fixture_graphs() { + let runner = Path::new(env!("CARGO_MANIFEST_DIR")) + .canonicalize() + .unwrap(); + assert_eq!(runner.file_name().unwrap(), "edgezero-test-harness"); + let repository = root().join("../../..").canonicalize().unwrap(); + assert_eq!(runner, repository.join("crates/edgezero-test-harness")); + let own = metadata(&runner.join("Cargo.toml")); + assert_eq!( + Path::new(own["workspace_root"].as_str().unwrap()) + .canonicalize() + .unwrap(), + runner + ); + assert_eq!(own["packages"].as_array().unwrap().len(), 1); + assert_eq!(own["packages"][0]["name"], "edgezero-test-harness"); + assert_eq!(own["packages"][0]["publish"], json!([])); + assert!( + own["packages"][0]["dependencies"] + .as_array() + .unwrap() + .iter() + .all(|dependency| { + !dependency["name"] + .as_str() + .unwrap() + .starts_with("edgezero-") + }) + ); + for manifest in [repository.join("Cargo.toml"), root().join("Cargo.toml")] { + let other = metadata(&manifest); + assert!(other["packages"].as_array().unwrap().iter().all(|package| { + package["name"] != "edgezero-test-harness" && package["name"] != "fixture-harness" + })); + } + assert!(!root().join("crates/fixture-harness/Cargo.toml").exists()); + let launcher = + fs::read_to_string(repository.join("scripts/smoke_test_reusable_app.sh")).unwrap(); + assert!(launcher.contains("crates/edgezero-test-harness/Cargo.toml")); + assert!(launcher.find("cd \"$REPO_DIR\"").unwrap() < launcher.find("exec ").unwrap()); + } +} + +#[cfg(test)] +mod argument_tests { + use super::Args; + + #[test] + fn outbound_lifetimes_selects_exactly_one_explicit_provider() { + for adapter in ["fastly", "axum", "cloudflare", "spin", "all"] { + let result = Args::parse_from( + ["--adapter", adapter, "--suite", "outbound-lifetimes"].map(str::to_owned), + ); + assert_eq!(result.is_ok(), adapter != "all", "{adapter}"); + } + assert!(Args::parse_from(["--suite", "outbound-lifetimes"].map(str::to_owned)).is_err()); + } + + #[test] + fn outbound_wire_suite_is_explicit_and_selects_one_supported_provider() { + for adapter in ["fastly", "axum", "cloudflare", "spin", "all"] { + let result = Args::parse_from( + ["--adapter", adapter, "--suite", "outbound-wire"].map(str::to_owned), + ); + assert_eq!(result.is_ok(), adapter != "all", "{adapter}"); + } + assert!(Args::parse_from(["--suite", "outbound-wire"].map(str::to_owned)).is_err()); + } + + #[test] + fn client_progress_suite_is_explicit_and_fastly_only() { + for adapter in ["fastly", "axum", "cloudflare", "spin", "all"] { + let result = Args::parse_from( + ["--adapter", adapter, "--suite", "outbound-client-progress"].map(str::to_owned), + ); + assert_eq!(result.is_ok(), adapter == "fastly", "{adapter}"); + } + } + + #[test] + fn full_readiness_suite_remains_explicit_and_fastly_only() { + for adapter in ["fastly", "axum", "cloudflare", "spin", "all"] { + let result = Args::parse_from( + ["--adapter", adapter, "--suite", "outbound-readiness"].map(str::to_owned), + ); + assert_eq!(result.is_ok(), adapter == "fastly", "{adapter}"); + } + } +} + +#[cfg(all(test, unix))] +mod process_tests { + use super::*; + use std::{net::TcpListener, os::unix::process::ExitStatusExt}; + + struct Fixture(PathBuf); + + impl Fixture { + fn new() -> Self { + let path = std::env::temp_dir().join(format!("fixture-process-{}", token())); + fs::create_dir(&path).unwrap(); + Self(path) + } + + fn command(&self, mode: &str) -> Command { + let mut command = Command::new(std::env::current_exe().unwrap()); + command + .args([ + "--ignored", + "--exact", + "process_tests::outbound_wire_process_helper", + "--nocapture", + "--test-threads=1", + ]) + .env("EDGEZERO_PROCESS_TEST_MODE", mode) + .env("EDGEZERO_PROCESS_TEST_DIR", &self.0); + command + } + + fn spawn(&self, mode: &str) -> Process { + Process::spawn(&mut self.command(mode), &self.0.join("runtime.log")).unwrap() + } + + fn ready(&self) { + until(|| self.0.join("ready").is_file()); + } + + fn descendant_port(&self) -> u16 { + fs::read_to_string(self.0.join("descendant")) + .unwrap() + .parse() + .unwrap() + } + } + + impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } + + fn until(mut done: impl FnMut() -> bool) { + let deadline = Instant::now() + Duration::from_secs(5); + while !done() { + assert!( + Instant::now() < deadline, + "subprocess observation timed out" + ); + thread::sleep(Duration::from_millis(10)); + } + } + + #[test] + #[ignore = "invoked only as a controlled Rust subprocess"] + fn outbound_wire_process_helper() { + let Ok(mode) = std::env::var("EDGEZERO_PROCESS_TEST_MODE") else { + return; + }; + let path = PathBuf::from(std::env::var_os("EDGEZERO_PROCESS_TEST_DIR").unwrap()); + if mode != "term-signal" { + let ignore = matches!(mode.as_str(), "ignore-term" | "descendant-worker"); + let code = match mode.as_str() { + "bad-shutdown" => 7, + "term-status" => 143, + _ => 0, + }; + ctrlc::set_handler(move || { + if !ignore { + std::process::exit(code); + } + }) + .unwrap(); + } + if mode == "cleanup-tmp" || mode == "cleanup-retry" { + let fixture = Fixture(path.clone()); + let kill = std::env::var_os("EDGEZERO_PROCESS_TEST_KILL").unwrap(); + let mut runtime = fixture.spawn(if mode == "cleanup-tmp" { + "graceful" + } else { + "descendant" + }); + fixture.ready(); + let outcome = (|| -> Result<()> { + let _ = runtime.finish(); + require(runtime.0.try_wait()?.is_some(), "runtime survived finish")?; + if mode == "cleanup-retry" { + require( + runtime.1.finished.is_none(), + "unconfirmed cleanup was cached", + )?; + require( + LIVE_GROUPS.lock().unwrap().contains(&runtime.0.id()), + "unconfirmed group cleanup lost ownership", + )?; + std::os::unix::fs::symlink(&kill, path.join("commands").join("kill"))?; + } else { + fs::create_dir(std::env::temp_dir())?; + } + let _ = runtime.finish(); + require( + runtime.1.finished.is_some(), + "retry did not confirm cleanup", + )?; + require( + !LIVE_GROUPS.lock().unwrap().contains(&runtime.0.id()), + "confirmed cleanup retained the group", + )?; + Ok(()) + })(); + // Clean up the nested group even when exercising the unfixed implementation. + if outcome.is_err() { + let _ = Command::new(kill) + .args(["-KILL", "--", &format!("-{}", runtime.0.id())]) + .status(); + let _ = runtime.0.kill(); + let _ = wait_for_exit(&mut runtime.0, Instant::now() + Duration::from_secs(5)); + } + std::mem::forget(fixture); + outcome.unwrap(); + fs::write(path.join("cleanup-checked"), b"complete").unwrap(); + } + if mode == "signal-capture" { + let fixture = Fixture(path.clone()); + assert_eq!( + signal_group_command( + &mut fixture.command("signal-orphan"), + Instant::now() + Duration::from_secs(2), + ) + .unwrap(), + SignalOutcome::Absent + ); + std::mem::forget(fixture); + fs::write(path.join("signal-captured"), b"complete").unwrap(); + } + if mode == "descendant-worker" { + let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); + fs::write( + path.join("descendant"), + listener.local_addr().unwrap().port().to_string(), + ) + .unwrap(); + loop { + thread::sleep(Duration::from_secs(1)); + std::hint::black_box(&listener); + } + } + if matches!(mode.as_str(), "descendant" | "orphan" | "signal-orphan") { + let fixture = Fixture(path.clone()); + let mut descendant = fixture.command("descendant-worker").spawn().unwrap(); + // The child intentionally inherits our process group and log descriptors. + std::mem::forget(fixture); + until(|| path.join("descendant").is_file()); + assert!(descendant.try_wait().unwrap().is_none()); + } + println!("helper-stdout"); + eprintln!("helper-stderr"); + std::io::stdout().flush().unwrap(); + std::io::stderr().flush().unwrap(); + fs::write(path.join("ready"), b"ready").unwrap(); + if mode == "signal-timeout" { + fs::write(path.join("signal-pid"), std::process::id().to_string()).unwrap(); + } + match mode.as_str() { + "exit-zero" | "orphan" => std::process::exit(0), + "signal-orphan" => { + eprintln!("No such process"); + std::process::exit(1); + } + "exit-nonzero" => std::process::exit(7), + "exit-term-status" => std::process::exit(143), + _ => loop { + thread::sleep(Duration::from_secs(1)); + }, + } + } + + #[test] + fn outbound_wire_process_rejects_premature_zero_and_nonzero_exits() { + for mode in ["exit-zero", "exit-nonzero", "exit-term-status"] { + let fixture = Fixture::new(); + let mut process = fixture.spawn(mode); + fixture.ready(); + until(|| process.0.try_wait().unwrap().is_some()); + let error = process.check_running().unwrap_err().to_string(); + assert!(error.contains("runtime exited before shutdown"), "{error}"); + assert!(error.contains("runtime.log"), "{error}"); + assert!(process.finish().unwrap_err().to_string().contains(&error)); + let started = Instant::now(); + assert!(process.finish().unwrap_err().to_string().contains(&error)); + assert!(started.elapsed() < Duration::from_millis(200)); + } + } + + #[test] + fn outbound_wire_process_finish_detects_exit_without_a_prior_health_check() { + let fixture = Fixture::new(); + let mut process = fixture.spawn("exit-zero"); + fixture.ready(); + until(|| process.0.try_wait().unwrap().is_some()); + assert!( + process + .finish() + .unwrap_err() + .to_string() + .contains("runtime exited before shutdown") + ); + } + + #[test] + fn outbound_wire_process_signal_diagnostics_do_not_wait_for_descendant_eof() { + let fixture = Fixture::new(); + let mut process = fixture.spawn("signal-capture"); + until(|| fixture.0.join("signal-captured").is_file()); + process.check_running().unwrap(); + let port = fixture.descendant_port(); + process.finish().unwrap(); + until(|| std::net::TcpStream::connect(("127.0.0.1", port)).is_err()); + } + + #[test] + fn outbound_wire_process_cleanup_survives_missing_diagnostics_and_retries_owned_groups() { + for mode in ["cleanup-tmp", "cleanup-retry"] { + let fixture = Fixture::new(); + let mut command = fixture.command(mode); + command.env( + "EDGEZERO_PROCESS_TEST_KILL", + executable("KILL_BIN", "kill").unwrap(), + ); + if mode == "cleanup-tmp" { + command.env("TMPDIR", fixture.0.join("missing-temp")); + } else { + let commands = fixture.0.join("commands"); + fs::create_dir(&commands).unwrap(); + command.env("PATH", commands); + } + let mut process = Process::spawn(&mut command, &fixture.0.join("runtime.log")).unwrap(); + let deadline = Instant::now() + Duration::from_secs(15); + while !fixture.0.join("cleanup-checked").is_file() { + let health = process.check_running(); + assert!( + health.is_ok(), + "{mode}: {health:?}: {}", + fs::read_to_string(fixture.0.join("runtime.log")).unwrap() + ); + assert!(Instant::now() < deadline, "{mode}: cleanup fixture stalled"); + thread::sleep(Duration::from_millis(10)); + } + process.finish().unwrap(); + } + } + + #[test] + fn outbound_wire_process_signal_timeout_reaps_the_helper_before_returning() { + let fixture = Fixture::new(); + let started = Instant::now(); + let error = signal_group_command( + &mut fixture.command("signal-timeout"), + Instant::now() + Duration::from_millis(300), + ) + .unwrap_err(); + assert!(error.to_string().contains("timed out"), "{error}"); + assert!(started.elapsed() < Duration::from_secs(2)); + let pid = fs::read_to_string(fixture.0.join("signal-pid")).unwrap(); + assert!( + !Command::new("ps") + .args(["-p", &pid]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .unwrap() + .success(), + "signal helper {pid} was not reaped" + ); + } + + #[test] + fn outbound_wire_process_accepts_requested_zero_or_sigterm_and_keeps_file_logs() { + for mode in ["graceful", "term-signal", "term-status"] { + let fixture = Fixture::new(); + let mut process = fixture.spawn(mode); + fixture.ready(); + process.check_running().unwrap(); + process.finish().unwrap(); + process.finish().unwrap(); + let status = process.0.try_wait().unwrap().unwrap(); + assert!(status.success() || status.signal() == Some(15) || status.code() == Some(143)); + let output = fs::read_to_string(fixture.0.join("runtime.log")).unwrap(); + assert!(output.contains("helper-stdout") && output.contains("helper-stderr")); + } + } + + #[test] + fn shell_term_exit_requires_a_confirmed_sent_signal() { + let status = ExitStatus::from_raw(143 << 8); + assert!(requested_termination(status, true)); + assert!(!requested_termination(status, false)); + } + + #[test] + fn outbound_wire_process_rejects_unexpected_shutdown_status() { + let fixture = Fixture::new(); + let mut process = fixture.spawn("bad-shutdown"); + fixture.ready(); + let error = process.finish().unwrap_err().to_string(); + assert!(error.contains("unexpectedly during shutdown"), "{error}"); + assert_eq!(process.0.try_wait().unwrap().unwrap().code(), Some(7)); + } + + #[test] + fn outbound_wire_process_escalates_ignored_term_within_two_deadlines() { + let fixture = Fixture::new(); + let mut process = fixture.spawn("ignore-term"); + fixture.ready(); + let started = Instant::now(); + let error = process.finish().unwrap_err().to_string(); + assert!(error.contains("SIGKILL"), "{error}"); + assert!(started.elapsed() >= Duration::from_secs(5)); + assert!(started.elapsed() < Duration::from_secs(11)); + assert_eq!(process.0.try_wait().unwrap().unwrap().signal(), Some(9)); + let started = Instant::now(); + assert_eq!(process.finish().unwrap_err().to_string(), error); + assert!(started.elapsed() < Duration::from_millis(200)); + } + + #[test] + fn outbound_wire_process_cleans_descendants_after_graceful_or_premature_parent_exit() { + for mode in ["descendant", "orphan"] { + let fixture = Fixture::new(); + let mut process = fixture.spawn(mode); + fixture.ready(); + let port = fixture.descendant_port(); + assert!(std::net::TcpStream::connect(("127.0.0.1", port)).is_ok()); + if mode == "orphan" { + until(|| process.0.try_wait().unwrap().is_some()); + } + assert_eq!(process.finish().is_ok(), mode == "descendant"); + until(|| std::net::TcpStream::connect(("127.0.0.1", port)).is_err()); + assert!( + !LIVE_GROUPS + .lock() + .unwrap_or_else(PoisonError::into_inner) + .contains(&process.0.id()) + ); + } + } + + #[test] + fn outbound_wire_process_drop_uses_the_same_group_cleanup() { + let fixture = Fixture::new(); + let process = fixture.spawn("descendant"); + fixture.ready(); + let port = fixture.descendant_port(); + drop(process); + until(|| std::net::TcpStream::connect(("127.0.0.1", port)).is_err()); + } + + #[test] + fn outbound_wire_process_start_reports_exit_and_spawn_failure() { + let fixture = Fixture::new(); + let log = fixture.0.join("runtime.log"); + let started = Instant::now(); + let error = Process::start( + &mut fixture.command("exit-zero"), + &log, + net::port().unwrap(), + ) + .err() + .expect("premature exit must fail startup") + .to_string(); + assert!(error.contains("runtime exited before shutdown"), "{error}"); + assert!(started.elapsed() < Duration::from_secs(5)); + assert!( + Process::spawn(&mut Command::new(fixture.0.join("missing-runtime")), &log).is_err() + ); + } +} diff --git a/crates/edgezero-test-harness/src/net.rs b/crates/edgezero-test-harness/src/net.rs new file mode 100644 index 000000000..5c515ada4 --- /dev/null +++ b/crates/edgezero-test-harness/src/net.rs @@ -0,0 +1,1157 @@ +use crate::evidence::{Result, require}; +pub mod overflow; +mod readiness; +pub use readiness::ReadinessOrigin; +mod wire; +use serde_json::{Value, json}; +use std::{ + io::{BufRead, BufReader, Read, Write}, + net::{TcpListener, TcpStream}, + sync::{ + Arc, Condvar, Mutex, + atomic::{AtomicBool, Ordering}, + }, + thread, + time::{Duration, Instant}, +}; +pub use wire::WireOrigins; + +pub type Signal = Arc<(Mutex, Condvar)>; +static CLIENT_EVENTS: Mutex> = Mutex::new(Vec::new()); +pub fn take_client_events() -> Vec { + std::mem::take(&mut *CLIENT_EVENTS.lock().unwrap()) +} + +#[derive(Debug)] +pub struct InterruptedResponse { + pub status: u16, + pub body: Vec, + pub reason: String, +} +impl std::fmt::Display for InterruptedResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "response {} interrupted after {} bytes: {}", + self.status, + self.body.len(), + self.reason + ) + } +} +impl std::error::Error for InterruptedResponse {} + +#[derive(Debug)] +pub struct ResponseHeadAborted(std::io::ErrorKind); +impl std::fmt::Display for ResponseHeadAborted { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "response ended before its status: {:?}", self.0) + } +} +impl std::error::Error for ResponseHeadAborted {} + +fn read_status(reader: &mut impl BufRead) -> Result { + let mut line = String::new(); + match reader.read_line(&mut line) { + Ok(0) => { + return Err(Box::new(ResponseHeadAborted( + std::io::ErrorKind::UnexpectedEof, + ))); + } + Err(error) + if line.is_empty() + && matches!( + error.kind(), + std::io::ErrorKind::ConnectionReset + | std::io::ErrorKind::ConnectionAborted + | std::io::ErrorKind::UnexpectedEof + ) => + { + return Err(Box::new(ResponseHeadAborted(error.kind()))); + } + Err(error) => return Err(error.into()), + Ok(_) => {} + } + Ok(line + .split_whitespace() + .nth(1) + .ok_or("missing HTTP response status")? + .parse()?) +} + +pub fn release(signal: &Signal) { + *signal.0.lock().unwrap() = true; + signal.1.notify_all(); +} +pub struct Backend { + pub port: u16, + pub receipts: Arc>>, + pub progress: Arc>>, + pub release: Signal, + stop: Arc, + worker: Option>, +} +impl Backend { + pub fn start() -> Result { + Self::start_with_progress_gate(false) + } + pub fn start_gated_progress() -> Result { + Self::start_with_progress_gate(true) + } + fn start_with_progress_gate(gate_progress: bool) -> Result { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let port = listener.local_addr()?.port(); + listener.set_nonblocking(true)?; + let receipts = Arc::new(Mutex::new(Vec::new())); + let progress = Arc::new(Mutex::new(Vec::new())); + let origin_clock = Instant::now(); + let release = Arc::new((Mutex::new(false), Condvar::new())); + let barrier = Arc::new((Mutex::new(0usize), Condvar::new())); + let stop = Arc::new(AtomicBool::new(false)); + let (r, p, s, b, done) = ( + receipts.clone(), + progress.clone(), + release.clone(), + barrier.clone(), + stop.clone(), + ); + let worker = thread::spawn(move || { + while !done.load(Ordering::Relaxed) { + match listener.accept() { + Ok((stream, _)) => { + let (r, p, s, b) = (r.clone(), p.clone(), s.clone(), b.clone()); + thread::spawn(move || { + let _ = serve(stream, r, p, origin_clock, s, b, gate_progress); + }); + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(10)) + } + Err(_) => break, + } + } + }); + Ok(Self { + port, + receipts, + progress, + release, + stop, + worker: Some(worker), + }) + } + pub fn url(&self, path: &str) -> String { + format!("http://127.0.0.1:{}{path}", self.port) + } +} +impl Drop for Backend { + fn drop(&mut self) { + release(&self.release); + self.stop.store(true, Ordering::Relaxed); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} +fn serve( + mut stream: TcpStream, + receipts: Arc>>, + progress: Arc>>, + origin_clock: Instant, + signal: Signal, + barrier: Arc<(Mutex, Condvar)>, + gate_progress: bool, +) -> Result<()> { + stream.set_nonblocking(false)?; + stream.set_read_timeout(Some(Duration::from_secs(10)))?; + let mut reader = BufReader::new(stream.try_clone()?); + let mut line = String::new(); + reader.read_line(&mut line)?; + let method = line + .split_whitespace() + .next() + .ok_or("missing request method")? + .to_owned(); + let path = line + .split_whitespace() + .nth(1) + .ok_or("missing request path")? + .to_owned(); + let mut request_token = None; + loop { + line.clear(); + reader.read_line(&mut line)?; + if line == "\r\n" || line.is_empty() { + break; + } + if let Some((name, value)) = line.split_once(':') + && name.eq_ignore_ascii_case("x-request-token") + { + request_token = Some(value.trim().to_owned()); + } + } + receipts.lock().unwrap().push(path.clone()); + if let Some(target) = path.strip_prefix("/p5/") { + return serve_p5( + &mut stream, + target, + &method, + request_token.as_deref(), + &signal, + &progress, + ); + } + if let Some(path) = path.strip_prefix("/marker/") { + let parts: Vec<_> = path.split('/').collect(); + let [ + expected_method @ ("get" | "head"), + status @ ("200" | "204" | "304" | "302"), + shape @ ("absent" | "upstream" | "nominated"), + ] = parts.as_slice() + else { + return Err("invalid marker path".into()); + }; + require( + method.eq_ignore_ascii_case(expected_method), + "marker origin method mismatch", + )?; + write!( + stream, + "HTTP/1.1 {status} Probe\r\nContent-Length: 0\r\nConnection: close\r\n" + )?; + if *shape != "absent" { + stream.write_all(b"x-edgezero-proxy: upstream\r\n")?; + } + if *shape == "nominated" { + stream.write_all(b"Connection: x-edgezero-proxy\r\n")?; + } + stream.write_all(b"Location: /not-followed\r\n\r\n")?; + return Ok(()); + } + if let Some(path) = path.strip_prefix("/coding/") { + let (coding, status) = path.split_once('/').ok_or("invalid coding path")?; + require(["200", "206"].contains(&status), "invalid coding status")?; + let body = coding_body(coding)?; + write!( + stream, + "HTTP/1.1 {status} {}\r\nContent-Encoding: {coding}\r\nContent-Length: {}\r\nConnection: close\r\n", + if status == "206" { + "Partial Content" + } else { + "OK" + }, + body.len() + )?; + for (name, value) in coding_representation_headers(coding, status.parse()?)? { + write!(stream, "{name}: {value}\r\n")?; + } + stream.write_all(b"\r\n")?; + stream.write_all(body)?; + return Ok(()); + } + if path.starts_with("/g3/") { + progress + .lock() + .unwrap() + .push(json!({"path":path,"received_ns":origin_clock.elapsed().as_nanos()})); + if path + .rsplit('/') + .next() + .is_some_and(|s| s.starts_with("slot-")) + { + thread::sleep(Duration::from_millis(300)); + if gate_progress { + let (released, _) = signal + .1 + .wait_timeout_while( + signal.0.lock().unwrap(), + Duration::from_secs(3), + |released| !*released, + ) + .unwrap(); + require( + *released, + "progress response not released by first client byte", + )?; + } + } + stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")?; + return Ok(()); + } + if path.starts_with("/barrier") { + let mut count = barrier.0.lock().unwrap(); + *count += 1; + barrier.1.notify_all(); + let (count, _) = barrier + .1 + .wait_timeout_while(count, Duration::from_secs(8), |n| *n < 2) + .map_err(|_| "barrier poisoned")?; + if *count < 2 { + stream.write_all(b"HTTP/1.1 504 Gateway Timeout\r\nContent-Length: 0\r\n\r\n")?; + return Ok(()); + } + } + if path.starts_with("/chunks") { + stream.write_all(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n5\r\nfirst\r\n")?; + stream.flush()?; + let (ready, _) = signal + .1 + .wait_timeout_while(signal.0.lock().unwrap(), Duration::from_secs(8), |v| !*v) + .map_err(|_| "release poisoned")?; + if *ready { + if path.contains("large=1") { + let body = vec![b'z'; 256 * 1024]; + write!(stream, "{:x}\r\n", body.len())?; + stream.write_all(&body)?; + stream.write_all(b"\r\n0\r\n\r\n")?; + } else { + stream.write_all(b"6\r\nsecond\r\n0\r\n\r\n")?; + } + } + } else if path.starts_with("/abort") { + stream.write_all( + b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\nConnection: close\r\n\r\npartial", + )?; + if path.contains("gated=1") { + stream.flush()?; + let (_observed, _) = signal + .1 + .wait_timeout_while(signal.0.lock().unwrap(), Duration::from_secs(8), |v| !*v) + .map_err(|_| "release poisoned")?; + } + } else { + stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")?; + } + Ok(()) +} + +fn serve_p5( + stream: &mut TcpStream, + target: &str, + method: &str, + request_token: Option<&str>, + signal: &Signal, + progress: &Mutex>, +) -> Result<()> { + let mut fields = target.splitn(3, '/'); + let token = fields.next().ok_or("missing P5 token")?; + let port: u16 = fields.next().ok_or("missing P5 redirect port")?.parse()?; + let case = fields.next().ok_or("missing P5 origin case")?; + require( + method == "GET" && request_token == Some(token) && port != 0, + "foreign P5 origin request", + )?; + let redirect = matches!(case, "redirect" | "pending"); + write!( + stream, + "HTTP/1.1 {} Probe\r\nDate: Tue, 27 Oct 2026 00:00:00 GMT\r\nContent-Type: application/octet-stream\r\nCache-Control: no-store\r\nConnection: close\r\n", + if redirect { 302 } else { 200 } + )?; + if redirect { + write!( + stream, + "Location: http://127.0.0.1:{port}/p5/{token}/{port}/final\r\n" + )?; + if case == "pending" { + stream.write_all(b"Content-Encoding: br\r\nContent-Length: 10485760\r\n\r\n")?; + progress + .lock() + .unwrap() + .push(json!({"event":"p5-pending-head","token":token})); + stream.flush()?; + let (released, _) = signal + .1 + .wait_timeout_while( + signal.0.lock().unwrap(), + Duration::from_secs(3), + |released| !*released, + ) + .map_err(|_| "P5 gate poisoned")?; + require(*released, "P5 body gate expired before client completion")?; + } else { + stream.write_all(b"Content-Length: 0\r\n\r\n")?; + } + return Ok(()); + } + let (coding, size) = if let Some(parity) = case.strip_prefix("parity/") { + let fields: Vec<_> = parity.split('/').collect(); + let [ + coding @ ("identity" | "gzip" | "passthrough"), + "buffered" | "streamed", + size @ ("ten" | "eleven" | "declared"), + ] = fields.as_slice() + else { + return Err("invalid P5 parity origin".into()); + }; + (*coding, *size) + } else { + require(matches!(case, "final" | "image"), "invalid P5 origin case")?; + ("identity", "ten") + }; + let body: &[u8] = match (coding, size) { + ("gzip", "ten") => &[ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 51, 48, 52, 50, 54, 49, 53, 51, 183, 176, 4, 0, 198, + 199, 132, 166, 10, 0, 0, 0, + ], + ("gzip", _) => &[ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 51, 48, 52, 50, 54, 49, 53, 51, 183, 176, 172, 0, 0, + 193, 245, 125, 254, 11, 0, 0, 0, + ], + (_, "ten") => b"0123456789", + _ => b"0123456789x", + }; + match coding { + "gzip" => stream.write_all(b"Content-Encoding: gzip\r\n")?, + "passthrough" => stream.write_all(b"Content-Encoding: x-fixture-opaque\r\n")?, + _ => {} + } + if size == "declared" || !case.starts_with("parity/") { + write!(stream, "Content-Length: {}\r\n\r\n", body.len())?; + stream.write_all(body)?; + } else { + write!( + stream, + "Transfer-Encoding: chunked\r\n\r\n{:x}\r\n", + body.len() + )?; + stream.write_all(body)?; + stream.write_all(b"\r\n0\r\n\r\n")?; + } + Ok(()) +} + +pub fn port() -> Result { + Ok(TcpListener::bind(("127.0.0.1", 0))?.local_addr()?.port()) +} + +pub fn coding_body(coding: &str) -> Result<&'static [u8]> { + // Deterministic gzip, zlib and Brotli encodings of b"raw coding probe\n". + match coding { + "gzip" => Ok(&[ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, + 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, 17, 0, 0, 0, + ]), + "deflate" => Ok(&[ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, + 0, 57, 147, 6, 33, + ]), + "br" => Ok(&[ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, 98, 101, + 10, 3, + ]), + _ => Err("unknown coding fixture".into()), + } +} + +pub fn coding_representation_headers( + coding: &str, + status: u16, +) -> Result> { + let digest = match coding { + "gzip" => "BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=", + "deflate" => "8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=", + "br" => "nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=", + _ => return Err("unknown coding fixture".into()), + }; + let mut fields = vec![ + ("content-type", "application/octet-stream".to_owned()), + ("etag", "\"coding-probe\"".to_owned()), + ("last-modified", "Tue, 20 Oct 2026 00:00:00 GMT".to_owned()), + ("vary", "Accept-Encoding".to_owned()), + ("accept-ranges", "bytes".to_owned()), + ("cache-control", "max-age=60".to_owned()), + ("content-digest", format!("sha-256=:{digest}:")), + ]; + if status == 206 { + let length = coding_body(coding)?.len(); + fields.push(("content-range", format!("bytes 0-{}/{length}", length - 1))); + } + Ok(fields) +} + +pub fn malformed_request(port: u16, token: &str) -> Result { + let mut stream = TcpStream::connect(("127.0.0.1", port))?; + stream.set_read_timeout(Some(Duration::from_secs(5)))?; + write!( + stream, + "POST /probe/malformed HTTP/1.1\r\nHost: localhost\r\nx-request-token: {token}\r\nContent-Length: invalid\r\nConnection: close\r\n\r\n" + )?; + let mut line = String::new(); + BufReader::new(stream).read_line(&mut line)?; + Ok(line.trim().to_owned()) +} +pub fn request( + port: u16, + path: &str, + token: &str, + backend: Option<&str>, + signal: Option<&Signal>, +) -> Result { + request_with_framing(port, path, token, backend, signal, None, Capture::Text) +} + +#[derive(Clone, Copy)] +enum Capture<'a> { + Text, + Binary, + Headers(&'a [(&'a str, &'a str)]), +} + +pub fn binary_request(port: u16, path: &str, token: &str, backend: Option<&str>) -> Result { + request_with_framing(port, path, token, backend, None, None, Capture::Binary) +} + +pub fn header_request( + port: u16, + path: &str, + token: &str, + headers: &[(&str, &str)], +) -> Result { + require( + path.starts_with('/') + && path.len() <= 4096 + && path.bytes().all(|byte| byte.is_ascii_graphic()) + && !token.is_empty() + && token.len() <= 128 + && token.bytes().all(|byte| byte.is_ascii_graphic()), + "invalid fixture request path or token", + )?; + let mut size = 0usize; + for (name, value) in headers { + require( + !name.is_empty() + && name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte)) + && value.is_ascii() + && value + .bytes() + .all(|byte| byte == b'\t' || (32..127).contains(&byte)) + && ![ + "host", + "connection", + "content-length", + "transfer-encoding", + "x-request-token", + ] + .iter() + .any(|reserved| name.eq_ignore_ascii_case(reserved)), + "invalid fixture request header", + )?; + size = size + .saturating_add(name.len()) + .saturating_add(value.len()) + .saturating_add(4); + require(size <= 16 * 1024, "fixture request headers exceed limit")?; + } + request_with_framing( + port, + path, + token, + None, + None, + None, + Capture::Headers(headers), + ) +} + +#[derive(Clone, Copy, Debug)] +pub enum Framing { + Absent, + LengthZero, + ChunkedEmpty, + FixedBody, + ChunkedBody, +} + +impl Framing { + fn wire(self) -> (&'static str, &'static str) { + match self { + Self::Absent => ("", ""), + Self::LengthZero => ("Content-Length: 0\r\n", ""), + Self::ChunkedEmpty => ("Transfer-Encoding: chunked\r\n", "0\r\n\r\n"), + Self::FixedBody => ("Content-Length: 4\r\n", "body"), + Self::ChunkedBody => ("Transfer-Encoding: chunked\r\n", "4\r\nbody\r\n0\r\n\r\n"), + } + } +} + +pub fn framed_request(port: u16, framing: Framing) -> Result { + request_with_framing( + port, + "/progress/empty", + &crate::token(), + None, + None, + Some(framing), + Capture::Text, + ) +} + +fn request_with_framing( + port: u16, + path: &str, + token: &str, + backend: Option<&str>, + signal: Option<&Signal>, + framing: Option, + capture: Capture<'_>, +) -> Result { + let start = Instant::now(); + let mut stream = TcpStream::connect(("127.0.0.1", port))?; + stream.set_read_timeout(Some(Duration::from_secs(12)))?; + stream.set_write_timeout(Some(Duration::from_secs(12)))?; + let probe = path.starts_with("/probe/"); + let body = if probe { token } else { "" }; + let framing_header = framing.map_or_else( + || format!("Content-Length: {}\r\n", body.len()), + |f| f.wire().0.to_owned(), + ); + let body = framing.map_or(body, |f| f.wire().1); + write!( + stream, + "{} {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nx-request-token: {token}\r\n{framing_header}Connection: close\r\n", + if probe { "POST" } else { "GET" }, + )?; + if let Some(url) = backend { + write!(stream, "x-fixture-backend: {url}\r\n")?; + } + if let Capture::Headers(headers) = capture { + for (name, value) in headers { + write!(stream, "{name}: {value}\r\n")?; + } + } + write!(stream, "\r\n{body}")?; + stream.flush()?; + let mut reader = BufReader::new(stream); + let status = read_status(&mut reader)?; + let mut line = String::new(); + let mut headers = Vec::new(); + let mut length = None; + let mut chunked = false; + loop { + line.clear(); + require(reader.read_line(&mut line)? > 0, "truncated HTTP headers")?; + if line == "\r\n" { + break; + } + let (name, value) = line + .trim_end() + .split_once(':') + .ok_or("malformed HTTP header")?; + let value = value.trim(); + if name.eq_ignore_ascii_case("content-length") { + length = Some(value.parse::()?); + } + if name.eq_ignore_ascii_case("transfer-encoding") { + chunked = value.to_ascii_lowercase().contains("chunked"); + } + headers.push(json!([name, value])); + } + let header_ns = start.elapsed().as_nanos() as u64; + let mut first_byte_ns = None; + let mut bytes = Vec::new(); + let capture_limit = match capture { + Capture::Text => usize::MAX, + Capture::Binary => 256, + Capture::Headers(_) => 64 * 1024, + }; + let outcome = (|| -> Result<()> { + let mut read_part = |reader: &mut BufReader, count: usize| -> Result<()> { + require( + count <= capture_limit.saturating_sub(bytes.len()), + "response exceeds fixture capture limit", + )?; + if count == 0 { + return Ok(()); + } + let mut first = [0]; + reader.read_exact(&mut first)?; + bytes.push(first[0]); + if first_byte_ns.is_none() { + first_byte_ns = Some(start.elapsed().as_nanos() as u64); + if let Some(signal) = signal { + release(signal); + } + } + let mut remaining = count - 1; + let mut buffer = [0u8; 8192]; + while remaining > 0 { + let size = remaining.min(buffer.len()); + let read = reader.read(&mut buffer[..size])?; + require(read > 0, "truncated HTTP body")?; + bytes.extend_from_slice(&buffer[..read]); + remaining -= read; + } + Ok(()) + }; + if chunked { + loop { + line.clear(); + require(reader.read_line(&mut line)? > 0, "truncated chunk header")?; + let count = usize::from_str_radix(line.trim().split(';').next().unwrap_or(""), 16)?; + if count == 0 { + loop { + line.clear(); + require(reader.read_line(&mut line)? > 0, "truncated chunk trailer")?; + if line == "\r\n" { + break; + } + } + break; + } + read_part(&mut reader, count)?; + let mut crlf = [0; 2]; + reader.read_exact(&mut crlf)?; + require(crlf == *b"\r\n", "invalid chunk terminator")?; + } + } else if let Some(length) = length { + read_part(&mut reader, length)?; + } else { + let mut first = [0]; + if reader.read(&mut first)? != 0 { + bytes.push(first[0]); + first_byte_ns = Some(start.elapsed().as_nanos() as u64); + if let Some(signal) = signal { + release(signal); + } + let mut buffer = [0u8; 8192]; + loop { + let capacity = capture_limit + .saturating_sub(bytes.len()) + .saturating_add(1) + .min(buffer.len()); + let read = reader.read(&mut buffer[..capacity])?; + if read == 0 { + break; + } + require( + read <= capture_limit.saturating_sub(bytes.len()), + "response exceeds fixture capture limit", + )?; + bytes.extend_from_slice(&buffer[..read]); + } + } + } + Ok(()) + })(); + if let Err(error) = outcome { + return Err(Box::new(InterruptedResponse { + status, + body: bytes, + reason: error.to_string(), + })); + } + let mut event = json!({"event":"client_completed","token":token,"status":status,"headers":headers,"header_ns":header_ns,"first_byte_ns":first_byte_ns.unwrap_or(header_ns),"complete_ns":start.elapsed().as_nanos() as u64}); + match capture { + Capture::Text => event["body"] = json!(String::from_utf8(bytes)?), + Capture::Binary | Capture::Headers(_) => event["body_bytes"] = json!(bytes), + } + CLIENT_EVENTS.lock().unwrap().push(event.clone()); + Ok(event) +} +#[cfg(test)] +mod tests { + use super::*; + + fn binary_reply(wire: Vec) -> Result { + captured_reply(wire, |port| { + binary_request(port, "/relay", "binary-test", None) + }) + } + + fn captured_reply(wire: Vec, request: impl FnOnce(u16) -> Result) -> Result { + let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = thread::spawn(move || { + let (mut socket, _) = listener.accept().unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let mut reader = BufReader::new(socket.try_clone().unwrap()); + loop { + let mut line = String::new(); + assert!(reader.read_line(&mut line).unwrap() > 0); + if line == "\r\n" { + break; + } + } + socket.write_all(&wire).unwrap(); + }); + let result = request(port); + server.join().unwrap(); + result + } + + #[test] + fn header_request_preserves_custom_and_duplicate_headers_and_binary_response() { + let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + let worker = thread::spawn(move || { + let (mut socket, _) = listener.accept().unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let mut reader = BufReader::new(socket.try_clone().unwrap()); + let mut head = String::new(); + loop { + let mut line = String::new(); + assert!(reader.read_line(&mut line).unwrap() > 0); + head.push_str(&line); + if line == "\r\n" { + break; + } + } + socket.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\n\x00\x80\xff").unwrap(); + head + }); + let response = header_request( + port, + "/p5", + "header-token", + &[ + ("x-origin", "http://127.0.0.1:1234/p5/final"), + ("x-duplicate", "first"), + ("x-duplicate", "second"), + ], + ) + .unwrap(); + let head = worker.join().unwrap(); + assert!(head.starts_with("GET /p5 HTTP/1.1\r\n")); + assert!(head.contains("x-request-token: header-token\r\n")); + assert!(head.contains("x-origin: http://127.0.0.1:1234/p5/final\r\n")); + assert!(head.contains("x-duplicate: first\r\nx-duplicate: second\r\n")); + assert_eq!(response["body_bytes"], json!([0, 128, 255])); + } + + #[test] + fn header_request_caps_all_response_framings_at_sixty_four_kib() { + for size in [65_536, 65_537] { + for framing in ["fixed", "chunked", "close"] { + let field = match framing { + "fixed" => format!("Content-Length: {size}\r\n"), + "chunked" => "Transfer-Encoding: chunked\r\n".to_owned(), + _ => String::new(), + }; + let mut wire = + format!("HTTP/1.1 200 OK\r\n{field}Connection: close\r\n\r\n").into_bytes(); + if framing == "chunked" { + wire.extend(format!("{size:X}\r\n").as_bytes()); + } + wire.extend(std::iter::repeat_n(255, size)); + if framing == "chunked" { + wire.extend(b"\r\n0\r\n\r\n"); + } + let result = captured_reply(wire, |port| header_request(port, "/p5", "cap", &[])); + if size == 65_536 { + assert_eq!(result.unwrap()["body_bytes"], json!(vec![255; size])); + } else { + let error = result.unwrap_err(); + let partial = error.downcast_ref::().unwrap(); + assert!(partial.reason.contains("capture limit")); + assert!(partial.body.len() <= 65_536); + } + } + } + } + + #[test] + fn header_request_rejects_injection_before_connecting() { + let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); + listener.set_nonblocking(true).unwrap(); + let port = listener.local_addr().unwrap().port(); + for (path, token, headers) in [ + ("/p5\r\nx-injected: yes", "t", vec![]), + ("/p5", "t\r\nx-injected: yes", vec![]), + ("/p5", "t", vec![("x-name\r\n", "value")]), + ("/p5", "t", vec![("x-name", "value\r\nx-injected: yes")]), + ("/p5", "t", vec![("bad name", "value")]), + ("/p5", "t", vec![("x-name", "bad\0value")]), + ] { + assert!(header_request(port, path, token, &headers).is_err()); + assert_eq!( + listener.accept().unwrap_err().kind(), + std::io::ErrorKind::WouldBlock + ); + } + } + + #[test] + fn binary_capture_preserves_encoded_bytes_without_utf8_or_decoding() { + let backend = Backend::start().unwrap(); + for coding in ["gzip", "deflate", "br"] { + let response = binary_request( + backend.port, + &format!("/coding/{coding}/200"), + "coding", + None, + ) + .unwrap(); + assert_eq!(response["status"], 200); + assert_eq!(response["body_bytes"], json!(coding_body(coding).unwrap())); + assert!(response["body"].is_null()); + } + } + + #[test] + fn binary_capture_bounds_fixed_chunked_and_close_delimited_payloads() { + for size in [256, 257] { + for framing in ["fixed", "chunked", "close"] { + let header = match framing { + "fixed" => format!("Content-Length: {size}\r\n"), + "chunked" => "Transfer-Encoding: chunked\r\n".to_owned(), + _ => String::new(), + }; + let mut wire = + format!("HTTP/1.1 200 OK\r\n{header}Connection: close\r\n\r\n").into_bytes(); + if framing == "chunked" { + wire.extend(format!("{size:x}\r\n").as_bytes()); + } + wire.extend(std::iter::repeat_n(255, size)); + if framing == "chunked" { + wire.extend(b"\r\n0\r\n\r\n"); + } + let result = binary_reply(wire); + if size == 256 { + assert_eq!( + result.unwrap()["body_bytes"], + json!(vec![255; size]), + "{framing}" + ); + } else { + let error = result.unwrap_err(); + let partial = error.downcast_ref::().unwrap(); + assert!( + partial.reason.contains("capture limit"), + "{framing}: {partial}" + ); + assert!(partial.body.len() <= 256); + } + } + } + } + + #[test] + fn p5_origin_has_deterministic_body_and_cross_port_redirect() { + let origin = Backend::start().unwrap(); + let reply = + binary_request(origin.port, "/p5/p5-origin/8002/final", "p5-origin", None).unwrap(); + assert_eq!(reply["body_bytes"], json!(b"0123456789")); + let redirect = binary_request( + origin.port, + "/p5/p5-origin/8002/redirect", + "p5-origin", + None, + ) + .unwrap(); + assert_eq!(redirect["status"], 302); + assert!(redirect["headers"].as_array().unwrap().iter().any(|field| { + field[0] + .as_str() + .is_some_and(|name| name.eq_ignore_ascii_case("location")) + && field[1] == "http://127.0.0.1:8002/p5/p5-origin/8002/final" + })); + assert_eq!(origin.receipts.lock().unwrap().len(), 2); + } + + #[test] + fn p5_passthrough_origin_uses_an_unsupported_coding_token() { + let origin = Backend::start().unwrap(); + let reply = binary_request( + origin.port, + "/p5/p5-origin/8002/parity/passthrough/streamed/ten", + "p5-origin", + None, + ) + .unwrap(); + assert_eq!(reply["body_bytes"], json!(b"0123456789")); + assert!(reply["headers"].as_array().unwrap().iter().any(|field| { + field[0] + .as_str() + .is_some_and(|name| name.eq_ignore_ascii_case("content-encoding")) + && field[1] == "x-fixture-opaque" + })); + } + + #[test] + fn binary_capture_preserves_truncation_as_failure() { + for framing in [ + "Content-Length: 5\r\n\r\nabc", + "Transfer-Encoding: chunked\r\n\r\n5\r\nabc", + ] { + let wire = format!("HTTP/1.1 200 OK\r\n{framing}").into_bytes(); + let error = binary_reply(wire).unwrap_err(); + let partial = error.downcast_ref::().unwrap(); + assert_eq!(partial.status, 200); + assert_eq!(partial.body, b"abc"); + } + } + + #[test] + fn progress_response_gate_waits_for_client_release() { + let backend = Backend::start_gated_progress().unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", backend.port)).unwrap(); + stream + .write_all( + b"GET /g3/gated/slot-0 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ) + .unwrap(); + thread::sleep(Duration::from_millis(350)); + stream + .set_read_timeout(Some(Duration::from_millis(50))) + .unwrap(); + let mut first = [0]; + assert!(stream.read(&mut first).is_err()); + release(&backend.release); + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let mut response = String::new(); + stream.read_to_string(&mut response).unwrap(); + assert!(response.starts_with("HTTP/1.1 200")); + } + #[test] + fn empty_body_framing_cases_preserve_wire_distinctions() { + assert_eq!(Framing::Absent.wire(), ("", "")); + assert_eq!(Framing::LengthZero.wire(), ("Content-Length: 0\r\n", "")); + assert_eq!( + Framing::ChunkedEmpty.wire(), + ("Transfer-Encoding: chunked\r\n", "0\r\n\r\n") + ); + assert_eq!(Framing::FixedBody.wire().1, "body"); + assert_eq!(Framing::ChunkedBody.wire().1, "4\r\nbody\r\n0\r\n\r\n"); + } + + #[test] + fn progress_origin_records_distinct_receipts_before_delayed_completion() { + let backend = Backend::start().unwrap(); + request(backend.port, "/g3/test/start", "test", None, None).unwrap(); + request(backend.port, "/g3/test/slot-0", "test", None, None).unwrap(); + let receipts = backend.progress.lock().unwrap().clone(); + assert_eq!(receipts.len(), 2); + assert_eq!(receipts[0]["path"], "/g3/test/start"); + assert_eq!(receipts[1]["path"], "/g3/test/slot-0"); + assert!( + receipts[0]["received_ns"].as_u64().unwrap() + <= receipts[1]["received_ns"].as_u64().unwrap() + ); + } + + #[test] + fn no_response_head_classification_excludes_timeouts_and_malformed_bytes() { + struct FailedRead(std::io::ErrorKind); + impl Read for FailedRead { + fn read(&mut self, _buffer: &mut [u8]) -> std::io::Result { + Err(std::io::Error::from(self.0)) + } + } + let eof = read_status(&mut std::io::Cursor::new(b"")).unwrap_err(); + assert!(eof.downcast_ref::().is_some()); + for kind in [ + std::io::ErrorKind::ConnectionReset, + std::io::ErrorKind::TimedOut, + ] { + let error = read_status(&mut BufReader::new(FailedRead(kind))).unwrap_err(); + assert_eq!( + error.downcast_ref::().is_some(), + kind == std::io::ErrorKind::ConnectionReset + ); + } + let invalid = read_status(&mut std::io::Cursor::new(b"invalid\r\n")).unwrap_err(); + assert!(invalid.downcast_ref::().is_none()); + assert_eq!( + read_status(&mut std::io::Cursor::new(b"HTTP/1.1 500 Error\r\n")).unwrap(), + 500 + ); + } + + #[test] + fn nonblocking_connection_waits_for_request_bytes() { + let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap(); + client + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let (server, _) = listener.accept().unwrap(); + server.set_nonblocking(true).unwrap(); + let worker = thread::spawn(move || { + serve( + server, + Arc::new(Mutex::new(Vec::new())), + Arc::new(Mutex::new(Vec::new())), + Instant::now(), + Arc::new((Mutex::new(false), Condvar::new())), + Arc::new((Mutex::new(0), Condvar::new())), + false, + ) + }); + thread::sleep(Duration::from_millis(100)); + client + .write_all(b"GET /ok HTTP/1.1\r\nHost: localhost\r\n\r\n") + .unwrap(); + let mut reply = String::new(); + client.read_to_string(&mut reply).unwrap(); + worker.join().unwrap().unwrap(); + assert!(reply.starts_with("HTTP/1.1 200")); + } + #[test] + fn backend_readiness_paths_have_no_special_response() { + let backend = Backend::start().unwrap(); + for path in ["/readiness/truncated", "/readiness/truncated/0"] { + let response = request(backend.port, path, "generic", None, None).unwrap(); + assert_eq!(response["status"], 200); + assert_eq!(response["body"], "ok"); + } + } + + #[test] + fn progressive_body_and_abort() { + let backend = Backend::start().unwrap(); + let result = request(backend.port, "/chunks", "x", None, Some(&backend.release)).unwrap(); + assert_eq!(result["body"], "firstsecond"); + let failure = request(backend.port, "/abort", "x", None, None).unwrap_err(); + let partial = failure + .downcast_ref::() + .expect("response started"); + assert_eq!(partial.status, 200); + assert_eq!(partial.body, b"partial"); + } + + #[test] + fn gated_abort_waits_for_client_observation_before_closing() { + let backend = Backend::start().unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", backend.port)).unwrap(); + stream + .set_read_timeout(Some(Duration::from_millis(200))) + .unwrap(); + stream + .write_all(b"GET /abort?gated=1 HTTP/1.1\r\nHost: localhost\r\n\r\n") + .unwrap(); + let mut reader = BufReader::new(stream); + loop { + let mut line = String::new(); + reader.read_line(&mut line).unwrap(); + if line == "\r\n" { + break; + } + } + let mut partial = [0; 7]; + reader.read_exact(&mut partial).unwrap(); + assert_eq!(&partial, b"partial"); + let before_release = reader.read(&mut [0]); + release(&backend.release); + assert!(matches!( + before_release, + Err(error) if matches!(error.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) + )); + assert_eq!(reader.read(&mut [0]).unwrap(), 0); + } + + #[test] + fn connection_failure_is_not_an_interrupted_response() { + let unused = port().unwrap(); + let failure = request(unused, "/abort", "x", None, None).unwrap_err(); + assert!(failure.downcast_ref::().is_none()); + } +} diff --git a/crates/edgezero-test-harness/src/net/overflow.rs b/crates/edgezero-test-harness/src/net/overflow.rs new file mode 100644 index 000000000..e25e040ca --- /dev/null +++ b/crates/edgezero-test-harness/src/net/overflow.rs @@ -0,0 +1,898 @@ +//! Bounded HTTP/1 evidence collection for synthetic upload probes only. + +use crate::evidence::{Result, require}; +use serde_json::{Value, json}; +use std::io::{self, BufRead, BufReader, Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; +use std::thread; +use std::time::{Duration, Instant}; + +const METADATA_BYTES: usize = 16 * 1024; +const ORIGIN_BODY_BYTES: usize = 256; +const SOCKET_BUDGET: Duration = Duration::from_secs(2); + +#[derive(Clone, Copy, Debug)] +pub(super) enum Failure { + Eof, + Reset, + Timeout, + Io, + Invalid, + MetadataLimit, + CaptureLimit, +} + +impl Failure { + pub(super) fn name(self) -> &'static str { + match self { + Self::Eof => "eof", + Self::Reset => "reset", + Self::Timeout => "timeout", + Self::Io => "io_error", + Self::Invalid => "invalid_framing", + Self::MetadataLimit => "metadata_limit", + Self::CaptureLimit => "capture_limit", + } + } +} + +impl From for Failure { + fn from(error: io::Error) -> Self { + match error.kind() { + io::ErrorKind::UnexpectedEof => Self::Eof, + io::ErrorKind::ConnectionReset | io::ErrorKind::ConnectionAborted => Self::Reset, + io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => Self::Timeout, + _ => Self::Io, + } + } +} + +pub(super) struct DeadlineSocket { + pub(super) socket: TcpStream, + pub(super) deadline: Instant, +} +impl Read for DeadlineSocket { + fn read(&mut self, buffer: &mut [u8]) -> io::Result { + let remaining = self + .deadline + .checked_duration_since(Instant::now()) + .filter(|duration| !duration.is_zero()) + .ok_or(io::ErrorKind::TimedOut)?; + self.socket.set_read_timeout(Some(remaining))?; + self.socket.read(buffer) + } +} + +pub(super) struct Head { + pub(super) first: String, + pub(super) fields: Vec<(String, String)>, +} + +fn line( + reader: &mut impl BufRead, + budget: &mut usize, + metadata_limit: usize, + line_limit: usize, +) -> std::result::Result { + let mut bytes = Vec::new(); + let read = (&mut *reader) + .take( + metadata_limit + .saturating_sub(*budget) + .min(line_limit) + .saturating_add(1) as u64, + ) + .read_until(b'\n', &mut bytes); + let count = bytes.len(); + *budget += count; + if *budget > metadata_limit || count > line_limit { + return Err(Failure::MetadataLimit); + } + if let Err(error) = read { + return Err(if count == 0 { + error.into() + } else { + Failure::Invalid + }); + } + if count == 0 { + return Err(Failure::Eof); + } + if !bytes.ends_with(b"\r\n") { + return Err(Failure::Invalid); + } + bytes.truncate(bytes.len() - 2); + String::from_utf8(bytes).map_err(|_| Failure::Invalid) +} + +fn field_parts(field: &str) -> std::result::Result<(&str, &str), Failure> { + let (name, value) = field.split_once(':').ok_or(Failure::Invalid)?; + if name.is_empty() + || !name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte)) + || value + .bytes() + .any(|byte| (byte < 32 && byte != b'\t') || byte == 127) + { + return Err(Failure::Invalid); + } + Ok((name, value.trim_matches([' ', '\t']))) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum Profile { + Overflow, + Wire, +} + +pub(super) fn read_head( + reader: &mut impl BufRead, + budget: &mut usize, + profile: Profile, +) -> std::result::Result { + let line_limit = if profile == Profile::Wire { + 4096 + } else { + METADATA_BYTES + }; + let first = line(reader, budget, METADATA_BYTES, line_limit)?; + if profile == Profile::Wire { + *budget = 0; + } + let mut fields = Vec::new(); + loop { + let field = line(reader, budget, METADATA_BYTES, line_limit)?; + if field.is_empty() { + return Ok(Head { first, fields }); + } + if profile == Profile::Overflow && fields.len() >= 100 { + return Err(Failure::MetadataLimit); + } + let (name, value) = field_parts(&field)?; + fields.push((name.to_ascii_lowercase(), value.to_owned())); + } +} + +pub(super) fn framing(head: &Head) -> std::result::Result<(Option, bool), Failure> { + let mut length = None; + let mut transfers = Vec::new(); + for (name, value) in &head.fields { + if name == "content-length" { + for part in value.split(',').map(str::trim) { + if part.is_empty() || !part.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(Failure::Invalid); + } + let parsed = part.parse::().map_err(|_| Failure::Invalid)?; + if length.is_some_and(|previous| previous != parsed) { + return Err(Failure::Invalid); + } + length = Some(parsed); + } + } else if name == "transfer-encoding" { + transfers.push(value.as_str()); + } + } + if !transfers.is_empty() + && (length.is_some() + || transfers.len() != 1 + || !transfers[0].eq_ignore_ascii_case("chunked")) + { + return Err(Failure::Invalid); + } + Ok((length, !transfers.is_empty())) +} + +fn amount( + reader: &mut impl Read, + mut count: u64, + body: &mut Vec, + cap: usize, +) -> std::result::Result<(), Failure> { + let mut buffer = [0_u8; 256]; + while count > 0 { + let room = cap.saturating_add(1).saturating_sub(body.len()); + if room == 0 { + return Err(Failure::CaptureLimit); + } + let size = usize::try_from(count) + .unwrap_or(usize::MAX) + .min(buffer.len()) + .min(room); + let read = reader.read(&mut buffer[..size])?; + if read == 0 { + return Err(Failure::Eof); + } + body.extend_from_slice(&buffer[..read]); + if body.len() > cap { + return Err(Failure::CaptureLimit); + } + count -= read as u64; + } + Ok(()) +} + +fn payload( + reader: &mut impl BufRead, + head: &Head, + budget: &mut usize, + body: &mut Vec, + cap: usize, + response: bool, + profile: Profile, +) -> std::result::Result<(), Failure> { + let (length, chunked) = framing(head)?; + if chunked { + loop { + let size = line( + reader, + budget, + METADATA_BYTES, + if profile == Profile::Wire { + 128 + } else { + METADATA_BYTES + }, + )?; + let size = if profile == Profile::Wire { + size.split(';').next().unwrap_or("") + } else { + &size + }; + if size.is_empty() || !size.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(Failure::Invalid); + } + let count = u64::from_str_radix(size, 16).map_err(|_| Failure::Invalid)?; + if count == 0 { + loop { + let trailer = line( + reader, + budget, + METADATA_BYTES, + if profile == Profile::Wire { + 2 + } else { + METADATA_BYTES + }, + )?; + if trailer.is_empty() { + return Ok(()); + } + if profile == Profile::Wire { + return Err(Failure::Invalid); + } + field_parts(&trailer)?; + } + } + amount(reader, count, body, cap)?; + for expected in b"\r\n" { + let mut byte = [0]; + reader.read_exact(&mut byte)?; + if byte[0] != *expected { + return Err(Failure::Invalid); + } + } + } + } + if let Some(length) = length { + return amount(reader, length, body, cap); + } + if response { + loop { + let mut byte = [0]; + if reader.read(&mut byte)? == 0 { + return Ok(()); + } + body.push(byte[0]); + if body.len() > cap { + return Err(Failure::CaptureLimit); + } + } + } + Ok(()) +} + +pub(super) fn capture_wire_body( + reader: &mut impl BufRead, + head: &Head, +) -> std::result::Result<(Vec, Vec), Failure> { + struct Recording<'a, R> { + inner: &'a mut R, + bytes: Vec, + pending: Vec, + position: usize, + } + impl Read for Recording<'_, R> { + fn read(&mut self, bytes: &mut [u8]) -> io::Result { + let available = self.fill_buf()?; + let count = available.len().min(bytes.len()); + bytes[..count].copy_from_slice(&available[..count]); + self.consume(count); + Ok(count) + } + } + impl BufRead for Recording<'_, R> { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + let remaining = 4097usize.saturating_sub(self.bytes.len()); + if remaining == 0 { + return Err(io::ErrorKind::InvalidData.into()); + } + if self.position == self.pending.len() { + self.pending.clear(); + self.position = 0; + let available = self.inner.fill_buf()?; + self.pending + .extend_from_slice(&available[..available.len().min(remaining)]); + } + Ok(&self.pending[self.position..]) + } + fn consume(&mut self, count: usize) { + self.bytes + .extend_from_slice(&self.pending[self.position..self.position + count]); + self.position += count; + self.inner.consume(count); + } + } + let mut recording = Recording { + inner: reader, + bytes: Vec::new(), + pending: Vec::new(), + position: 0, + }; + let mut body = Vec::new(); + let outcome = payload( + &mut recording, + head, + &mut 0, + &mut body, + 1024, + false, + Profile::Wire, + ); + if recording.bytes.len() > 4096 { + return Err(Failure::CaptureLimit); + } + outcome?; + Ok((body, recording.bytes)) +} + +pub(crate) fn inspect(reader: &mut impl BufRead) -> Value { + let mut budget = 0; + let mut body = Vec::new(); + let mut receipt = json!({"head":null,"head_bytes":0,"head_state":"unobserved"}); + let result = (|| { + let head = match read_head(reader, &mut budget, Profile::Overflow) { + Ok(head) => head, + Err(error) => { + let seen = budget; + receipt["head_bytes"] = json!(seen); + receipt["head_state"] = json!(if seen == 0 { "absent" } else { "partial" }); + return Err(error); + } + }; + receipt["head_bytes"] = json!(budget); + receipt["head_state"] = json!("complete"); + let parts: Vec<_> = head.first.split_whitespace().collect(); + if parts.len() != 3 || parts[0] != "POST" || !matches!(parts[2], "HTTP/1.0" | "HTTP/1.1") { + return Err(Failure::Invalid); + } + receipt["head"] = + json!({"method":parts[0],"target":parts[1],"version":parts[2],"headers":head.fields}); + payload( + reader, + &head, + &mut budget, + &mut body, + ORIGIN_BODY_BYTES, + false, + Profile::Overflow, + ) + })(); + receipt["terminal"] = json!(result.err().map_or("complete", Failure::name)); + receipt["body_bytes"] = json!(body.len()); + receipt["body_prefix"] = json!(body); + receipt +} + +pub struct UploadOrigin { + pub port: u16, + stop: Arc, + worker: Option>>, +} + +impl UploadOrigin { + pub fn start() -> Result { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let port = listener.local_addr()?.port(); + listener.set_nonblocking(true)?; + let stop = Arc::new(AtomicBool::new(false)); + let observed_stop = Arc::clone(&stop); + let worker = thread::spawn(move || { + let mut accepted = 0_usize; + let mut receipts = Vec::new(); + loop { + match listener.accept() { + Ok((mut socket, _)) => { + accepted += 1; + if accepted > 2 { + break; + } + socket.set_nonblocking(false)?; + let deadline = Instant::now() + SOCKET_BUDGET; + let mut reader = BufReader::new(DeadlineSocket { + socket: socket.try_clone()?, + deadline, + }); + let receipt = inspect(&mut reader); + let complete = receipt["terminal"] == "complete"; + let upstream_413 = receipt["head"]["target"] + .as_str() + .is_some_and(|target| target.ends_with("/upstream-413")); + receipts.push(receipt); + if complete { + let wire = if upstream_413 { + "HTTP/1.1 413 Payload Too Large\r\nRetry-After: 120\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + } else { + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + }; + // Framing completion is recorded first, even if the guest abandoned the reply. + let wrote = deadline + .checked_duration_since(Instant::now()) + .filter(|remaining| !remaining.is_zero()) + .is_some_and(|remaining| { + socket + .set_write_timeout(Some(remaining)) + .and_then(|()| socket.write_all(wire.as_bytes())) + .is_ok() + }); + receipts.last_mut().unwrap()["reply_written"] = json!(wrote); + } + } + Err(error) if error.kind() == io::ErrorKind::WouldBlock => { + if observed_stop.load(Ordering::SeqCst) { + break; + } + thread::sleep(Duration::from_millis(5)); + } + Err(error) => return Err(error.into()), + } + } + Ok( + json!({"accepted":accepted,"connections":receipts,"overflow":accepted > 2, + "collection_complete":true,"origin_deadline_ms":SOCKET_BUDGET.as_millis(),"capture_bytes":ORIGIN_BODY_BYTES}), + ) + }); + Ok(Self { + port, + stop, + worker: Some(worker), + }) + } + + pub fn url(&self, token: &str, case: &str) -> String { + format!("http://127.0.0.1:{}/overflow/{token}/{case}", self.port) + } + + pub fn finish(mut self) -> Result { + // This is an explicit bounded observation window, not proof of indefinite host cancellation. + thread::sleep(Duration::from_millis(100)); + self.stop.store(true, Ordering::SeqCst); + self.worker + .take() + .ok_or("origin collector missing")? + .join() + .map_err(|_| "origin collector panicked")? + } +} + +impl Drop for UploadOrigin { + fn drop(&mut self) { + self.stop.store(true, Ordering::SeqCst); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + +pub fn request( + port: u16, + case: &str, + token: &str, + backend: Option<&str>, + incoming: Option, +) -> Result { + let started = Instant::now(); + require( + !token.is_empty() + && token.len() <= 64 + && token + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-'), + "invalid overflow token", + )?; + require( + case.len() <= 32 + && case + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-'), + "invalid overflow case", + )?; + require( + incoming.is_none_or(|length| length <= 2048), + "unbounded overflow input", + )?; + let mut socket = TcpStream::connect(("127.0.0.1", port))?; + socket.set_write_timeout(Some(SOCKET_BUDGET))?; + let mut head = format!( + "POST /progress/overflow/{case} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nx-request-token: {token}\r\nConnection: close\r\n" + ); + if let Some(backend) = backend { + require( + !backend.contains(['\r', '\n']) && backend.len() <= 512, + "invalid overflow backend", + )?; + head.push_str(&format!("x-fixture-backend: {backend}\r\n")); + } + if let Some(length) = incoming { + head.push_str("Transfer-Encoding: chunked\r\n\r\n"); + socket.write_all(head.as_bytes())?; + write!(socket, "{length:x}\r\n{}\r\n0\r\n\r\n", "x".repeat(length))?; + } else { + head.push_str("Content-Length: 0\r\n\r\n"); + socket.write_all(head.as_bytes())?; + } + let deadline = Instant::now() + Duration::from_secs(6); + let mut reader = BufReader::new(DeadlineSocket { socket, deadline }); + let mut metadata = 0; + let response = read_head(&mut reader, &mut metadata, Profile::Overflow) + .map_err(|failure| failure.name())?; + let header_ns = started.elapsed().as_nanos(); + let parts: Vec<_> = response.first.split_whitespace().collect(); + require( + parts.len() >= 2 && parts[0] == "HTTP/1.1", + "invalid overflow response status", + )?; + let status = parts[1].parse::()?; + let mut body = Vec::new(); + payload( + &mut reader, + &response, + &mut metadata, + &mut body, + 4096, + true, + Profile::Overflow, + ) + .map_err(|failure| failure.name())?; + let event = json!({"event":"client_completed","token":token,"status":status,"headers":response.fields, + "body":String::from_utf8(body)?,"header_ns":header_ns,"complete_ns":started.elapsed().as_nanos(), + "first_byte_ns":null,"first_byte_timing":"not-measured"}); + super::CLIENT_EVENTS.lock().unwrap().push(event.clone()); + Ok(event) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + fn observed(wire: &[u8]) -> Value { + inspect(&mut Cursor::new(wire)) + } + + #[test] + fn wire_capture_propagates_buffer_errors_without_refilling_in_consume() { + struct FailingBuffer { + first: bool, + } + impl Read for FailingBuffer { + fn read(&mut self, _: &mut [u8]) -> io::Result { + unreachable!() + } + } + impl BufRead for FailingBuffer { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + if std::mem::take(&mut self.first) { + Ok(b"a") + } else { + Err(io::ErrorKind::ConnectionReset.into()) + } + } + fn consume(&mut self, amount: usize) { + assert_eq!(amount, 1); + } + } + let head = Head { + first: "POST /post HTTP/1.1".to_owned(), + fields: vec![("content-length".to_owned(), "2".to_owned())], + }; + let result = capture_wire_body(&mut FailingBuffer { first: true }, &head); + assert!(matches!(result, Err(Failure::Reset))); + } + + #[test] + fn wire_head_limits_count_lines_and_headers_separately_and_preserve_duplicates() { + let first = format!("GET /{} HTTP/1.1\r\n", "x".repeat(4080)); + assert_eq!(first.len(), 4096); + let mut headers = String::new(); + for size in [4096, 4096, 4096, 4094] { + headers.push_str(&format!("X: {}\r\n", "a".repeat(size - 5))); + } + headers.push_str("\r\n"); + assert_eq!(headers.len(), 16 * 1024); + let mut budget = 0; + let head = read_head( + &mut Cursor::new(format!("{first}{headers}")), + &mut budget, + Profile::Wire, + ) + .unwrap(); + assert_eq!(budget, 16 * 1024); + assert_eq!(head.fields.len(), 4); + assert!(head.fields.iter().all(|(name, _)| name == "x")); + for wire in [ + format!("GET /{} HTTP/1.1\r\n\r\n", "x".repeat(4081)), + format!("GET / HTTP/1.1\r\nX: {}\r\n\r\n", "x".repeat(4092)), + format!("GET / HTTP/1.1\r\nY: x\r\n{headers}"), + ] { + assert!(matches!( + read_head(&mut Cursor::new(wire), &mut 0, Profile::Wire), + Err(Failure::MetadataLimit) + )); + } + } + + #[test] + fn wire_body_profile_enforces_payload_chunk_line_and_raw_framing_limits() { + for size in [1024, 1025] { + let head = Head { + first: "POST /post HTTP/1.1".into(), + fields: vec![("content-length".into(), size.to_string())], + }; + let result = capture_wire_body(&mut Cursor::new(vec![b'a'; size]), &head); + if size == 1024 { + let (body, encoded) = result.unwrap(); + assert_eq!(body, encoded); + assert_eq!(body.len(), 1024); + } else { + assert!(matches!(result, Err(Failure::CaptureLimit))); + } + } + let head = Head { + first: "POST /post HTTP/1.1".into(), + fields: vec![("transfer-encoding".into(), "chunked".into())], + }; + for size in [128, 129] { + let wire = format!("1;{}\r\na\r\n0\r\n\r\n", "x".repeat(size - 4)); + let result = capture_wire_body(&mut Cursor::new(&wire), &head); + if size == 128 { + let (body, encoded) = result.unwrap(); + assert_eq!(body, b"a"); + assert_eq!(encoded, wire.as_bytes()); + } else { + assert!(matches!(result, Err(Failure::MetadataLimit))); + } + } + for extra in [0, 1] { + let wire = format!( + "{}1;{}\r\na\r\n0\r\n\r\n", + format!("1;{}\r\na\r\n", "x".repeat(123)).repeat(31), + "x".repeat(54 + extra) + ); + assert_eq!(wire.len(), 4096 + extra); + let result = capture_wire_body(&mut Cursor::new(&wire), &head); + if extra == 0 { + let (body, encoded) = result.unwrap(); + assert_eq!(body, vec![b'a'; 32]); + assert_eq!(encoded, wire.as_bytes()); + } else { + assert!(matches!(result, Err(Failure::CaptureLimit))); + } + } + assert!(capture_wire_body(&mut Cursor::new(b"0\r\nX: trailer\r\n\r\n"), &head).is_err()); + } + + #[test] + fn upload_completion_depends_on_framing_not_payload_size_or_socket_eof() { + for (framing, body, terminal, count) in [ + ("Content-Length: 9\r\n", "xxxx", "eof", 4), + ("Content-Length: 4\r\n", "xxxx", "complete", 4), + ("Transfer-Encoding: chunked\r\n", "4\r\nxxxx\r\n", "eof", 4), + ( + "Transfer-Encoding: chunked\r\n", + "4\r\nxxxx\r\n0\r\n\r\n", + "complete", + 4, + ), + ( + "Transfer-Encoding: chunked\r\n", + "8\r\nxxxxxxxx\r\n0\r\n", + "eof", + 8, + ), + ("Content-Length: 9\r\n", "xxxxxxxxx", "complete", 9), + ] { + let wire = format!("POST /overflow/test/streamed-9 HTTP/1.1\r\n{framing}\r\n{body}"); + let receipt = observed(wire.as_bytes()); + assert_eq!(receipt["terminal"], terminal); + assert_eq!(receipt["body_bytes"], count); + } + } + + #[test] + fn malformed_or_unbounded_upload_evidence_is_not_an_abort() { + for wire in [ + "POST /overflow/t/x HTTP/1.1\r\nContent-Length: 4\r\n", + "POST /overflow/t/x HTTP/1.1\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\nxxxx", + "POST /overflow/t/x HTTP/1.1\r\nContent-Length: 4\r\nTransfer-Encoding: chunked\r\n\r\n", + "POST /overflow/t/x HTTP/1.1\r\nTransfer-Encoding: not-chunked\r\n\r\n", + "POST /overflow/t/x HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nxxxxXX0\r\n\r\n", + ] { + let receipt = observed(wire.as_bytes()); + assert_ne!(receipt["terminal"], "complete"); + } + let huge = format!( + "POST /overflow/t/x HTTP/1.1\r\nX-Large: {}\r\n\r\n", + "x".repeat(20_000) + ); + assert_eq!(observed(huge.as_bytes())["terminal"], "metadata_limit"); + let body = format!( + "POST /overflow/t/x HTTP/1.1\r\nContent-Length: 300\r\n\r\n{}", + "x".repeat(300) + ); + let receipt = observed(body.as_bytes()); + assert_eq!(receipt["terminal"], "capture_limit"); + assert!(receipt["body_bytes"].as_u64().unwrap() <= 257); + } + + #[test] + fn oversized_head_counts_the_limit_witness() { + let receipt = observed(&vec![b'x'; METADATA_BYTES + 100]); + assert_eq!(receipt["terminal"], "metadata_limit"); + assert_eq!(receipt["head_state"], "partial"); + assert_eq!(receipt["head_bytes"], METADATA_BYTES + 1); + } + + #[test] + fn partial_head_errors_record_consumed_bytes_not_zero_contact() { + struct Partial { + input: Cursor>, + kind: io::ErrorKind, + } + impl Read for Partial { + fn read(&mut self, bytes: &mut [u8]) -> io::Result { + let read = self.input.read(bytes)?; + if read == 0 { + Err(self.kind.into()) + } else { + Ok(read) + } + } + } + for kind in [io::ErrorKind::ConnectionReset, io::ErrorKind::TimedOut] { + let receipt = inspect(&mut BufReader::new(Partial { + input: Cursor::new(b"POST /".to_vec()), + kind, + })); + assert_eq!(receipt["head_bytes"], 6); + assert_eq!(receipt["head_state"], "partial"); + assert_eq!(receipt["terminal"], "invalid_framing"); + } + } + + #[test] + fn dropping_origin_signals_stop_and_joins_the_worker_before_returning() { + use std::sync::mpsc; + + let stop = Arc::new(AtomicBool::new(false)); + let observed_stop = Arc::clone(&stop); + let (started_tx, started_rx) = mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); + let (exited_tx, exited_rx) = mpsc::channel(); + let worker = thread::spawn(move || { + started_tx.send(()).unwrap(); + release_rx.recv().unwrap(); + exited_tx.send(()).unwrap(); + Ok(json!({})) + }); + started_rx.recv_timeout(Duration::from_secs(2)).unwrap(); + let origin = UploadOrigin { + port: 0, + stop, + worker: Some(worker), + }; + let (dropped_tx, dropped_rx) = mpsc::channel(); + let dropper = thread::spawn(move || { + drop(origin); + dropped_tx.send(()).unwrap(); + }); + let deadline = Instant::now() + Duration::from_secs(2); + while !observed_stop.load(Ordering::SeqCst) && Instant::now() < deadline { + thread::yield_now(); + } + let signalled = observed_stop.load(Ordering::SeqCst); + let while_gated = dropped_rx.recv_timeout(Duration::from_millis(50)); + // Release and join before assertions so a failed proof cannot strand the worker. + let released = release_tx.send(()); + let exited = exited_rx.recv_timeout(Duration::from_secs(2)); + let after_release = dropped_rx.recv_timeout(Duration::from_secs(2)); + let joined = dropper.join(); + assert!(signalled, "Drop must signal the collector to stop"); + assert!(matches!(while_gated, Err(mpsc::RecvTimeoutError::Timeout))); + assert!(released.is_ok()); + assert!(exited.is_ok()); + assert!(after_release.is_ok()); + assert!(joined.is_ok()); + } + + #[test] + fn third_connection_fails_the_bounded_collector_and_joins_it() { + let origin = UploadOrigin::start().unwrap(); + let port = origin.port; + let mut clients = Vec::new(); + for _ in 0..3 { + let socket = TcpStream::connect(("127.0.0.1", port)).unwrap(); + socket.shutdown(std::net::Shutdown::Write).unwrap(); + clients.push(socket); + } + let receipt = origin.finish().unwrap(); + assert_eq!(receipt["accepted"], 3); + assert_eq!(receipt["connections"].as_array().unwrap().len(), 2); + assert_eq!(receipt["overflow"], true); + assert_eq!(receipt["collection_complete"], true); + assert!(TcpStream::connect(("127.0.0.1", port)).is_err()); + } + + #[test] + fn joined_origin_retains_completed_incomplete_and_headless_connections() { + for (wire, terminal, count) in [ + ( + "POST /overflow/test/buffered-8 HTTP/1.1\r\nContent-Length: 8\r\n\r\nxxxxxxxx", + "complete", + 8, + ), + ( + "POST /overflow/test/streamed-9 HTTP/1.1\r\nContent-Length: 9\r\n\r\nxxxx", + "eof", + 4, + ), + ("", "eof", 0), + ] { + let origin = UploadOrigin::start().unwrap(); + let mut client = TcpStream::connect(("127.0.0.1", origin.port)).unwrap(); + client + .set_read_timeout(Some(Duration::from_secs(3))) + .unwrap(); + client.write_all(wire.as_bytes()).unwrap(); + client.shutdown(std::net::Shutdown::Write).unwrap(); + let mut response = Vec::new(); + client.read_to_end(&mut response).unwrap(); + let snapshot = origin.finish().unwrap(); + assert_eq!(snapshot["accepted"], 1); + assert_eq!(snapshot["connections"][0]["terminal"], terminal); + assert_eq!(snapshot["connections"][0]["body_bytes"], count); + } + let empty = UploadOrigin::start().unwrap().finish().unwrap(); + assert_eq!(empty["accepted"], 0); + assert_eq!(empty["connections"], json!([])); + assert_eq!(empty["collection_complete"], true); + } + + #[test] + fn stalled_origin_read_is_a_timeout_not_an_abort() { + let origin = UploadOrigin::start().unwrap(); + let mut client = TcpStream::connect(("127.0.0.1", origin.port)).unwrap(); + client + .write_all(b"POST /overflow/test/streamed-9 HTTP/1.1\r\nContent-Length: 9\r\n\r\nxxxx") + .unwrap(); + let started = Instant::now(); + let snapshot = origin.finish().unwrap(); + assert_eq!(snapshot["connections"][0]["terminal"], "timeout"); + assert_eq!(snapshot["connections"][0]["body_bytes"], 4); + assert!(started.elapsed() < Duration::from_secs(4)); + } +} diff --git a/crates/edgezero-test-harness/src/net/readiness.rs b/crates/edgezero-test-harness/src/net/readiness.rs new file mode 100644 index 000000000..70eedf9de --- /dev/null +++ b/crates/edgezero-test-harness/src/net/readiness.rs @@ -0,0 +1,965 @@ +use crate::evidence::{Result, require}; +use serde_json::{Value, json}; +use std::{ + io::{self, Read, Write}, + net::{Shutdown, TcpListener, TcpStream}, + sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError}, + thread, + time::{Duration, Instant}, +}; + +const LIFETIME: Duration = Duration::from_secs(3); +const POLL: Duration = Duration::from_millis(5); +const HEADER_LIMIT: usize = 4096; +const CASES: &[&str] = &[ + "truncated", + "silent-head", + "pending-body", + "trickle", + "siblings", + "exhausted", + "eof", + "empty-eof", + "malformed-head", + "malformed-body", + "head-error", + "mixed-head-body", + "mixed-body-head", + "full-body-error", +]; + +struct State { + stop: bool, + sockets: Vec>, + slots: [bool; 2], + heads: [bool; 2], + snapshot: Value, +} + +struct Shared { + case: String, + token: String, + started: Instant, + deadline: Instant, + max_workers: usize, + state: Mutex, + wake: Condvar, +} + +impl Shared { + fn lock(&self) -> MutexGuard<'_, State> { + self.state.lock().unwrap_or_else(PoisonError::into_inner) + } + + fn error(&self, error: &'static str) { + self.lock().snapshot["errors"] + .as_array_mut() + .unwrap() + .push(json!(error)); + } + + fn stop(&self, watchdog: bool) { + let mut state = self.lock(); + if watchdog && !state.stop { + state.snapshot["watchdog"] = json!(true); + state.snapshot["errors"] + .as_array_mut() + .unwrap() + .push(json!("origin-watchdog")); + } + state.stop = true; + for socket in state.sockets.iter().flatten() { + let _ = socket.shutdown(Shutdown::Both); + } + self.wake.notify_all(); + } + + fn remaining(&self) -> io::Result { + if self.lock().stop { + return Err(io::ErrorKind::ConnectionAborted.into()); + } + match self.deadline.checked_duration_since(Instant::now()) { + Some(remaining) if !remaining.is_zero() => Ok(remaining), + _ => { + self.stop(true); + Err(io::ErrorKind::TimedOut.into()) + } + } + } + + fn wait_until(&self, at: Instant) -> bool { + let mut state = self.lock(); + loop { + if state.stop { + return false; + } + let now = Instant::now(); + if now >= self.deadline { + drop(state); + self.stop(true); + return false; + } + if now >= at { + return true; + } + state = self + .wake + .wait_timeout(state, at.min(self.deadline) - now) + .unwrap_or_else(PoisonError::into_inner) + .0; + } + } + + fn stopped_reason(&self) -> Option<&'static str> { + let state = self.lock(); + state.stop.then_some(if state.snapshot["watchdog"] == true { + "watchdog" + } else { + "harness-stop" + }) + } +} + +pub struct ReadinessOrigin { + port: u16, + shared: Arc, + worker: Option>, +} + +impl ReadinessOrigin { + pub fn start(case: &str, token: &str) -> Result { + require(CASES.contains(&case), "unknown-readiness-case")?; + require( + !token.is_empty() + && token.len() <= 128 + && token.bytes().all(|byte| byte.is_ascii_graphic()), + "invalid-readiness-token", + )?; + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let port = listener.local_addr()?.port(); + listener.set_nonblocking(true)?; + let started = Instant::now(); + let max_workers = if matches!(case, "siblings" | "mixed-head-body" | "mixed-body-head") { + 2 + } else { + 1 + }; + let shared = Arc::new(Shared { + case: case.to_owned(), + token: token.to_owned(), + started, + deadline: started + LIFETIME, + max_workers, + state: Mutex::new(State { + stop: false, + sockets: Vec::new(), + slots: [false; 2], + heads: [false; 2], + snapshot: json!({"case":case,"token":token,"accepted":0,"workers_joined":0, + "max_workers":max_workers,"harness_shutdown":false,"watchdog":false, + "errors":[],"requests":[],"writes":[],"terminations":[]}), + }), + wake: Condvar::new(), + }); + let observed = Arc::clone(&shared); + let worker = thread::Builder::new() + .name("readiness-origin".to_owned()) + .spawn(move || collect(listener, &observed))?; + Ok(Self { + port, + shared, + worker: Some(worker), + }) + } + + pub fn url(&self, slot: usize) -> String { + format!( + "http://127.0.0.1:{}/readiness/{}/{slot}", + self.port, self.shared.case + ) + } + + pub fn finish(mut self) -> Value { + self.shutdown(); + self.snapshot() + } + + fn shutdown(&mut self) { + self.shared.lock().snapshot["harness_shutdown"] = json!(true); + self.shared.stop(false); + if let Some(worker) = self.worker.take() + && worker.join().is_err() + { + self.shared.error("collector-panicked"); + } + } + + fn snapshot(&self) -> Value { + self.shared.lock().snapshot.clone() + } +} + +impl Drop for ReadinessOrigin { + fn drop(&mut self) { + self.shutdown(); + } +} + +fn collect(listener: TcpListener, shared: &Arc) { + thread::scope(|scope| { + let mut workers = Vec::new(); + while shared.remaining().is_ok() { + match listener.accept() { + Ok((socket, _)) => { + let mut state = shared.lock(); + let accepted = state.snapshot["accepted"].as_u64().unwrap() + 1; + state.snapshot["accepted"] = json!(accepted); + if state.stop || workers.len() == shared.max_workers { + state.snapshot["errors"] + .as_array_mut() + .unwrap() + .push(json!("unexpected-connection")); + state.snapshot["terminations"] + .as_array_mut() + .unwrap() + .push(json!({"slot":null,"reason":"io-error"})); + let _ = socket.shutdown(Shutdown::Both); + drop(state); + shared.stop(false); + break; + } + let control = socket + .set_nonblocking(false) + .and_then(|()| socket.try_clone()); + let Ok(control) = control else { + state.snapshot["terminations"] + .as_array_mut() + .unwrap() + .push(json!({"slot":null,"reason":"io-error"})); + drop(state); + shared.error("socket-setup-failed"); + shared.stop(false); + break; + }; + let index = state.sockets.len(); + state.sockets.push(Some(control)); + drop(state); + let shared = Arc::clone(shared); + workers.push(scope.spawn(move || serve(socket, index, &shared))); + } + Err(error) if error.kind() == io::ErrorKind::WouldBlock => { + shared.wait_until(Instant::now() + POLL); + } + Err(_) => { + shared.error("origin-accept-failed"); + shared.stop(false); + break; + } + } + } + for worker in workers { + let joined = worker.join(); + let mut state = shared.lock(); + let count = state.snapshot["workers_joined"].as_u64().unwrap() + 1; + state.snapshot["workers_joined"] = json!(count); + if joined.is_err() { + state.snapshot["errors"] + .as_array_mut() + .unwrap() + .push(json!("worker-panicked")); + } + } + }); +} + +struct DeadlineSocket<'a> { + socket: &'a TcpStream, + shared: &'a Shared, +} + +impl Read for DeadlineSocket<'_> { + fn read(&mut self, bytes: &mut [u8]) -> io::Result { + loop { + self.socket.set_read_timeout(Some( + self.shared.remaining()?.min(Duration::from_millis(50)), + ))?; + match self.socket.read(bytes) { + Err(error) + if matches!( + error.kind(), + io::ErrorKind::WouldBlock + | io::ErrorKind::TimedOut + | io::ErrorKind::Interrupted + ) => {} + outcome => return outcome, + } + } + } +} + +impl Write for DeadlineSocket<'_> { + fn write(&mut self, bytes: &[u8]) -> io::Result { + self.socket + .set_write_timeout(Some(self.shared.remaining()?))?; + self.socket.write(bytes) + } + + fn flush(&mut self) -> io::Result<()> { + self.socket.flush() + } +} + +fn request(socket: &TcpStream, shared: &Shared) -> std::result::Result { + let mut reader = DeadlineSocket { socket, shared }; + let mut bytes = Vec::new(); + while !bytes.ends_with(b"\r\n\r\n") { + if bytes.len() == HEADER_LIMIT { + return Err("request-head-limit"); + } + let mut byte = [0]; + reader + .read_exact(&mut byte) + .map_err(|_| "request-head-incomplete")?; + bytes.push(byte[0]); + } + let text = std::str::from_utf8(&bytes).map_err(|_| "request-head-invalid")?; + let mut lines = text.split("\r\n"); + let parts: Vec<_> = lines + .next() + .unwrap_or_default() + .split_whitespace() + .collect(); + let ["GET", path, "HTTP/1.1"] = parts.as_slice() else { + return Err("request-head-invalid"); + }; + let prefix = format!("/readiness/{}/", shared.case); + let slot = path + .strip_prefix(&prefix) + .and_then(|slot| slot.parse::().ok()) + .filter(|slot| *slot < shared.max_workers && *path == format!("{prefix}{slot}")) + .ok_or("request-path-mismatch")?; + let mut token = None; + for line in lines.take_while(|line| !line.is_empty()) { + let (name, value) = line.split_once(':').ok_or("request-head-invalid")?; + if name.eq_ignore_ascii_case("x-readiness-token") && token.replace(value.trim()).is_some() { + return Err("request-token-mismatch"); + } + } + if token != Some(shared.token.as_str()) { + return Err("request-token-mismatch"); + } + let mut state = shared.lock(); + if state.slots[slot] { + return Err("duplicate-slot"); + } + state.slots[slot] = true; + state.snapshot["requests"].as_array_mut().unwrap().push(json!({ + "slot":slot,"path":path,"token":shared.token,"received_us":shared.started.elapsed().as_micros()})); + Ok(slot) +} + +fn send( + socket: &TcpStream, + shared: &Shared, + slot: usize, + kind: &str, + bytes: &[u8], +) -> io::Result<()> { + let mut writer = DeadlineSocket { socket, shared }; + writer.write_all(bytes)?; + writer.flush()?; + let mut state = shared.lock(); + state.snapshot["writes"] + .as_array_mut() + .unwrap() + .push(json!({ + "slot":slot,"at_us":shared.started.elapsed().as_micros(),"kind":kind,"bytes":bytes.len()})); + if kind == "head" { + state.heads[slot] = true; + shared.wake.notify_all(); + } + Ok(()) +} + +fn hold(socket: &TcpStream, shared: &Shared) -> io::Result<&'static str> { + socket.set_read_timeout(Some(Duration::from_millis(10)))?; + while shared.remaining().is_ok() { + match socket.peek(&mut [0]) { + Ok(0) => return Ok(shared.stopped_reason().unwrap_or("peer-close")), + Err(error) + if matches!( + error.kind(), + io::ErrorKind::ConnectionReset | io::ErrorKind::ConnectionAborted + ) => + { + return Ok("peer-close"); + } + Err(error) + if !matches!( + error.kind(), + io::ErrorKind::WouldBlock + | io::ErrorKind::TimedOut + | io::ErrorKind::Interrupted + ) => + { + return Err(error); + } + _ => {} + } + shared.wait_until(Instant::now() + POLL); + } + Ok(shared.stopped_reason().unwrap_or("watchdog")) +} + +fn respond(socket: &TcpStream, shared: &Shared, slot: usize) -> io::Result<&'static str> { + let case = shared.case.as_str(); + if case == "silent-head" || (case == "mixed-head-body" && slot == 0) { + return hold(socket, shared); + } + if case == "head-error" { + return Ok("complete"); + } + if case == "malformed-head" { + send(socket, shared, slot, "head", b"NOT HTTP\r\n\r\n")?; + return Ok("complete"); + } + if case == "mixed-body-head" && slot == 1 { + while !shared.lock().heads[0] { + if !shared.wait_until(Instant::now() + POLL) { + return Ok(shared.stopped_reason().unwrap_or("watchdog")); + } + } + if !shared.wait_until(Instant::now() + Duration::from_millis(50)) { + return Ok(shared.stopped_reason().unwrap_or("watchdog")); + } + } + let length = match case { + "empty-eof" => 0, + "pending-body" => 1, + "siblings" if slot == 0 => 1, + "mixed-body-head" if slot == 0 => 1, + "mixed-head-body" | "mixed-body-head" => 32_769, + "full-body-error" => 32_770, + "exhausted" => 2, + _ => 3, + }; + let head = if case == "malformed-body" { + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n".to_owned() + } else { + format!("HTTP/1.1 200 OK\r\nContent-Length: {length}\r\nConnection: close\r\n\r\n") + }; + send(socket, shared, slot, "head", head.as_bytes())?; + match case { + "pending-body" => return hold(socket, shared), + "mixed-body-head" if slot == 0 => return hold(socket, shared), + "mixed-head-body" | "mixed-body-head" | "full-body-error" => { + send(socket, shared, slot, "body", &[b'x'; 32_769])?; + if case == "full-body-error" + && !shared.wait_until(Instant::now() + Duration::from_millis(50)) + { + return Ok(shared.stopped_reason().unwrap_or("watchdog")); + } + } + "exhausted" => { + send(socket, shared, slot, "body", b"x")?; + return hold(socket, shared); + } + "siblings" if slot == 0 => return hold(socket, shared), + "siblings" => { + while !shared.lock().heads.iter().all(|ready| *ready) { + if !shared.wait_until(Instant::now() + POLL) { + return Ok(shared.stopped_reason().unwrap_or("watchdog")); + } + } + send(socket, shared, slot, "body", b"xyz")?; + } + "trickle" => { + let start = Instant::now(); + for (index, byte) in [b"x", b"y", b"z"].into_iter().enumerate() { + if !shared.wait_until(start + Duration::from_millis(index as u64 * 300)) { + return Ok(shared.stopped_reason().unwrap_or("watchdog")); + } + send(socket, shared, slot, "body", byte)?; + } + } + "truncated" | "malformed-body" => { + send( + socket, + shared, + slot, + "body", + if case == "truncated" { + b"x" + } else { + b"1\r\nx\r\n" + }, + )?; + if !shared.wait_until(Instant::now() + Duration::from_millis(50)) { + return Ok(shared.stopped_reason().unwrap_or("watchdog")); + } + if case == "malformed-body" { + send(socket, shared, slot, "body", b"Z\r\n")?; + } + } + "eof" => send(socket, shared, slot, "body", b"xyz")?, + _ => {} + } + Ok("complete") +} + +fn serve(socket: TcpStream, index: usize, shared: &Shared) { + let mut slot = None; + let outcome = match request(&socket, shared) { + Ok(request_slot) => { + slot = Some(request_slot); + respond(&socket, shared, request_slot) + } + Err(error) => { + shared.error(error); + Err(io::ErrorKind::InvalidData.into()) + } + }; + let reason = shared.stopped_reason().unwrap_or_else(|| match outcome { + Ok(reason) => reason, + Err(ref error) + if matches!( + error.kind(), + io::ErrorKind::ConnectionReset + | io::ErrorKind::BrokenPipe + | io::ErrorKind::ConnectionAborted + ) => + { + "peer-close" + } + Err(_) => "io-error", + }); + if outcome.is_err() && reason == "io-error" && slot.is_some() { + shared.error("response-io-error"); + } + let _ = socket.shutdown(Shutdown::Both); + let mut state = shared.lock(); + state.sockets[index].take(); + state.snapshot["terminations"] + .as_array_mut() + .unwrap() + .push(json!({"slot":slot,"reason":reason})); +} + +#[cfg(test)] +mod tests { + use crate::net::ReadinessOrigin; + use serde_json::json; + use std::io::{BufRead, BufReader, Read, Write}; + use std::net::{Shutdown, TcpStream}; + use std::thread; + use std::time::{Duration, Instant}; + + const TOKEN: &str = "readiness-test"; + + fn connect(origin: &ReadinessOrigin, slot: usize, token: &str) -> TcpStream { + let url = origin.url(slot); + let authority = url + .strip_prefix("http://") + .unwrap() + .split('/') + .next() + .unwrap(); + let mut socket = TcpStream::connect(authority).unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + socket + .set_write_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let path = url.strip_prefix(&format!("http://{authority}")).unwrap(); + write!( + socket, + "GET {path} HTTP/1.1\r\nHost: localhost\r\nx-readiness-token: {token}\r\n\r\n" + ) + .unwrap(); + socket + } + + fn head(socket: TcpStream) -> BufReader { + let mut reader = BufReader::new(socket); + loop { + let mut line = String::new(); + assert!(reader.read_line(&mut line).unwrap() > 0); + if line == "\r\n" { + return reader; + } + } + } + + fn await_count(origin: &ReadinessOrigin, field: &str, count: usize) { + let deadline = Instant::now() + Duration::from_secs(2); + loop { + let snapshot = origin.snapshot(); + let observed = if field == "accepted" { + snapshot[field].as_u64().unwrap() as usize + } else { + snapshot[field].as_array().unwrap().len() + }; + if observed >= count { + return; + } + assert!(Instant::now() < deadline, "missing {field}: {snapshot}"); + thread::sleep(Duration::from_millis(5)); + } + } + + #[test] + fn finite_origins_retain_correlated_receipts_and_written_bytes() { + for (case, suffix, body_bytes) in [ + ("eof", "\r\n\r\nxyz", 3), + ("empty-eof", "\r\n\r\n", 0), + ("truncated", "\r\n\r\nx", 1), + ] { + let origin = ReadinessOrigin::start(case, TOKEN).unwrap(); + let mut socket = connect(&origin, 0, TOKEN); + let mut wire = String::new(); + let read = socket.read_to_string(&mut wire); + let snapshot = origin.finish(); + read.unwrap(); + assert!(wire.ends_with(suffix), "{case}: {wire}"); + assert_eq!(snapshot["case"], case); + assert_eq!(snapshot["token"], TOKEN); + assert_eq!(snapshot["accepted"], 1); + assert_eq!(snapshot["workers_joined"], 1); + assert_eq!(snapshot["max_workers"], 1); + assert_eq!(snapshot["harness_shutdown"], true); + assert_eq!(snapshot["watchdog"], false); + assert_eq!(snapshot["errors"], json!([])); + assert_eq!(snapshot["requests"].as_array().unwrap().len(), 1); + assert_eq!( + snapshot["requests"][0]["path"], + format!("/readiness/{case}/0") + ); + assert_eq!(snapshot["requests"][0]["slot"], 0); + assert_eq!(snapshot["requests"][0]["token"], TOKEN); + assert!(snapshot["requests"][0]["received_us"].is_u64()); + let body_written: u64 = snapshot["writes"] + .as_array() + .unwrap() + .iter() + .filter(|write| write["kind"] == "body") + .map(|write| write["bytes"].as_u64().unwrap()) + .sum(); + assert_eq!(body_written, body_bytes); + assert_eq!( + snapshot["terminations"], + json!([{ "slot":0, "reason":"complete" }]) + ); + } + } + + #[test] + fn pending_origin_finish_and_drop_close_sockets_and_join_workers() { + for case in ["silent-head", "pending-body", "exhausted"] { + for finish in [false, true] { + let origin = ReadinessOrigin::start(case, TOKEN).unwrap(); + let mut socket = connect(&origin, 0, TOKEN); + await_count(&origin, "requests", 1); + let started = Instant::now(); + if finish { + let snapshot = origin.finish(); + assert_eq!(snapshot["workers_joined"], 1); + assert_eq!(snapshot["terminations"][0]["reason"], "harness-stop"); + assert_eq!(snapshot["errors"], json!([])); + } else { + drop(origin); + } + assert!(started.elapsed() < Duration::from_secs(1)); + let mut remaining = Vec::new(); + socket.read_to_end(&mut remaining).unwrap(); + } + } + } + + #[test] + fn siblings_flush_both_heads_before_only_slot_one_completes() { + let origin = ReadinessOrigin::start("siblings", TOKEN).unwrap(); + let ready = connect(&origin, 1, TOKEN); + let mut ready = head(ready); + ready + .get_mut() + .set_read_timeout(Some(Duration::from_millis(30))) + .unwrap(); + assert!(ready.read(&mut [0]).is_err()); + ready + .get_mut() + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let mut pending = head(connect(&origin, 0, TOKEN)); + let mut body = String::new(); + ready.read_to_string(&mut body).unwrap(); + assert_eq!(body, "xyz"); + let snapshot = origin.finish(); + assert_eq!(snapshot["accepted"], 2); + assert_eq!(snapshot["workers_joined"], 2); + assert_eq!(snapshot["max_workers"], 2); + assert_eq!(snapshot["errors"], json!([])); + let writes = snapshot["writes"].as_array().unwrap(); + assert_eq!(writes[0]["kind"], "head"); + assert_eq!(writes[1]["kind"], "head"); + assert_eq!(writes[2]["slot"], 1); + assert_eq!(writes[2]["kind"], "body"); + assert_eq!(pending.read(&mut [0]).unwrap(), 0); + } + + #[test] + fn mixed_head_body_origin_finishes_body_without_waiting_for_silent_head() { + let origin = ReadinessOrigin::start("mixed-head-body", TOKEN).unwrap(); + let pending = connect(&origin, 0, TOKEN); + let mut ready = head(connect(&origin, 1, TOKEN)); + let mut bytes = Vec::new(); + ready.read_to_end(&mut bytes).unwrap(); + assert_eq!(bytes, vec![b'x'; 32_769]); + // The body may finish before slot zero's request head is parsed; do not cancel that parse. + while !origin.shared.lock().slots[0] { + assert!(Instant::now() < origin.shared.deadline); + thread::yield_now(); + } + let snapshot = origin.finish(); + assert_eq!(snapshot["workers_joined"], 2); + assert_eq!(snapshot["errors"], json!([])); + assert!( + snapshot["writes"] + .as_array() + .unwrap() + .iter() + .all(|row| row["slot"] == 1) + ); + drop(pending); + } + + #[test] + fn mixed_body_head_origin_delays_second_head_while_first_body_is_pending() { + let origin = ReadinessOrigin::start("mixed-body-head", TOKEN).unwrap(); + let pending = head(connect(&origin, 0, TOKEN)); + let mut ready = head(connect(&origin, 1, TOKEN)); + let mut bytes = Vec::new(); + ready.read_to_end(&mut bytes).unwrap(); + assert_eq!(bytes, vec![b'x'; 32_769]); + let snapshot = origin.finish(); + assert_eq!(snapshot["workers_joined"], 2); + assert_eq!(snapshot["errors"], json!([])); + let writes = snapshot["writes"].as_array().unwrap(); + assert_eq!(writes[0]["slot"], 0); + assert_eq!(writes[0]["kind"], "head"); + assert_eq!(writes[1]["slot"], 1); + assert_eq!(writes[1]["kind"], "head"); + assert!( + writes[1]["at_us"].as_u64().unwrap() >= writes[0]["at_us"].as_u64().unwrap() + 50_000 + ); + drop(pending); + } + + #[test] + fn full_body_error_origin_closes_after_multiple_read_capacities() { + let origin = ReadinessOrigin::start("full-body-error", TOKEN).unwrap(); + let mut ready = head(connect(&origin, 0, TOKEN)); + let mut bytes = Vec::new(); + ready.read_to_end(&mut bytes).unwrap(); + assert_eq!(bytes, vec![b'x'; 32_769]); + let snapshot = origin.finish(); + assert_eq!(snapshot["workers_joined"], 1); + assert_eq!(snapshot["errors"], json!([])); + assert_eq!(snapshot["terminations"][0]["reason"], "complete"); + } + + #[test] + fn malformed_and_head_error_origins_keep_distinct_wire_shapes() { + for (case, expected) in [ + ("head-error", ""), + ("malformed-head", "NOT HTTP\r\n\r\n"), + ( + "malformed-body", + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n1\r\nx\r\nZ\r\n", + ), + ] { + let origin = ReadinessOrigin::start(case, TOKEN).unwrap(); + let mut socket = connect(&origin, 0, TOKEN); + let mut wire = String::new(); + let read = socket.read_to_string(&mut wire); + let snapshot = origin.finish(); + read.unwrap(); + assert_eq!(wire, expected); + assert_eq!(snapshot["errors"], json!([])); + assert_eq!(snapshot["terminations"][0]["reason"], "complete"); + } + } + + #[test] + fn trickle_uses_absolute_interruptible_three_hundred_ms_intervals() { + let origin = ReadinessOrigin::start("trickle", TOKEN).unwrap(); + let mut reader = head(connect(&origin, 0, TOKEN)); + let mut body = String::new(); + reader.read_to_string(&mut body).unwrap(); + let snapshot = origin.finish(); + assert_eq!(body, "xyz"); + let writes: Vec<_> = snapshot["writes"] + .as_array() + .unwrap() + .iter() + .filter(|write| write["kind"] == "body") + .collect(); + assert_eq!(writes.len(), 3); + let first = writes[0]["at_us"].as_u64().unwrap(); + assert!(writes[1]["at_us"].as_u64().unwrap() >= first + 295_000); + assert!(writes[2]["at_us"].as_u64().unwrap() >= first + 595_000); + assert_eq!(snapshot["errors"], json!([])); + } + + #[test] + fn trickle_shutdown_interrupts_the_next_scheduled_write() { + let origin = ReadinessOrigin::start("trickle", TOKEN).unwrap(); + let mut reader = head(connect(&origin, 0, TOKEN)); + let mut first = [0]; + reader.read_exact(&mut first).unwrap(); + assert_eq!(&first, b"x"); + let started = Instant::now(); + let snapshot = origin.finish(); + assert!(started.elapsed() < Duration::from_millis(250)); + assert_eq!(snapshot["workers_joined"], 1); + assert_eq!(snapshot["terminations"][0]["reason"], "harness-stop"); + assert_eq!( + snapshot["writes"] + .as_array() + .unwrap() + .iter() + .filter(|write| write["kind"] == "body") + .count(), + 1 + ); + assert_eq!(reader.read(&mut [0]).unwrap(), 0); + } + + #[test] + fn watchdog_expiry_is_retained_as_failure_and_joins_the_pending_worker() { + let started = Instant::now(); + let origin = ReadinessOrigin::start("pending-body", TOKEN).unwrap(); + let mut reader = head(connect(&origin, 0, TOKEN)); + reader + .get_mut() + .set_read_timeout(Some(Duration::from_secs(4))) + .unwrap(); + let end = reader.read(&mut [0]); + let snapshot = origin.finish(); + assert_eq!(end.unwrap(), 0); + assert!(started.elapsed() >= Duration::from_secs(3)); + assert!(started.elapsed() < Duration::from_secs(4)); + assert_eq!(snapshot["watchdog"], true); + assert_eq!(snapshot["harness_shutdown"], true); + assert_eq!(snapshot["workers_joined"], 1); + assert_eq!(snapshot["terminations"][0]["reason"], "watchdog"); + assert!( + snapshot["errors"] + .as_array() + .unwrap() + .contains(&json!("origin-watchdog")) + ); + } + + #[test] + fn held_origin_records_peer_close_separately_from_harness_shutdown() { + let origin = ReadinessOrigin::start("pending-body", TOKEN).unwrap(); + let reader = head(connect(&origin, 0, TOKEN)); + reader.get_ref().shutdown(Shutdown::Both).unwrap(); + await_count(&origin, "terminations", 1); + let snapshot = origin.finish(); + assert_eq!(snapshot["terminations"][0]["reason"], "peer-close"); + assert_eq!(snapshot["workers_joined"], 1); + assert_eq!(snapshot["watchdog"], false); + assert_eq!(snapshot["errors"], json!([])); + } + + #[test] + fn unexpected_connection_is_retained_without_spawning_an_extra_worker() { + let origin = ReadinessOrigin::start("eof", TOKEN).unwrap(); + connect(&origin, 0, TOKEN) + .read_to_end(&mut Vec::new()) + .unwrap(); + let _extra = connect(&origin, 0, TOKEN); + await_count(&origin, "accepted", 2); + let snapshot = origin.finish(); + assert_eq!(snapshot["accepted"], 2); + assert_eq!(snapshot["workers_joined"], 1); + assert!( + snapshot["errors"] + .as_array() + .unwrap() + .contains(&json!("unexpected-connection")) + ); + assert_eq!(snapshot["terminations"].as_array().unwrap().len(), 2); + assert!( + snapshot["terminations"] + .as_array() + .unwrap() + .contains(&json!({"slot":null,"reason":"io-error"})) + ); + } + + #[test] + fn partial_and_oversized_heads_are_bounded_and_do_not_leak_headers() { + for oversized in [false, true] { + let origin = ReadinessOrigin::start("eof", TOKEN).unwrap(); + let url = origin.url(0); + let authority = url + .strip_prefix("http://") + .unwrap() + .split('/') + .next() + .unwrap(); + let mut socket = TcpStream::connect(authority).unwrap(); + socket + .write_all(b"GET /readiness/eof/0 HTTP/1.1\r\nx-private: private-secret\r\n") + .unwrap(); + if oversized { + socket.write_all(&[b'x'; 4096]).unwrap(); + await_count(&origin, "terminations", 1); + } else { + await_count(&origin, "accepted", 1); + } + let started = Instant::now(); + let snapshot = origin.finish(); + assert!(started.elapsed() < Duration::from_secs(1)); + assert_eq!(snapshot["accepted"], 1); + assert_eq!(snapshot["workers_joined"], 1); + assert!(!snapshot["errors"].as_array().unwrap().is_empty()); + assert_eq!(snapshot["requests"], json!([])); + assert!(!snapshot.to_string().contains("private-secret")); + let _ = socket.shutdown(Shutdown::Both); + } + } + + #[test] + fn foreign_tokens_and_duplicate_sibling_slots_fail_without_echoing_input() { + let origin = ReadinessOrigin::start("eof", TOKEN).unwrap(); + let _socket = connect(&origin, 0, "private-secret"); + await_count(&origin, "terminations", 1); + let snapshot = origin.finish(); + assert_eq!(snapshot["requests"], json!([])); + assert!( + snapshot["errors"] + .as_array() + .unwrap() + .contains(&json!("request-token-mismatch")) + ); + assert!(!snapshot.to_string().contains("private-secret")); + + let origin = ReadinessOrigin::start("siblings", TOKEN).unwrap(); + let _first = head(connect(&origin, 0, TOKEN)); + let _second = connect(&origin, 0, TOKEN); + await_count(&origin, "terminations", 1); + let snapshot = origin.finish(); + assert_eq!(snapshot["workers_joined"], 2); + assert_eq!(snapshot["requests"].as_array().unwrap().len(), 1); + assert!( + snapshot["errors"] + .as_array() + .unwrap() + .contains(&json!("duplicate-slot")) + ); + } +} diff --git a/crates/edgezero-test-harness/src/net/wire.rs b/crates/edgezero-test-harness/src/net/wire.rs new file mode 100644 index 000000000..a057757d3 --- /dev/null +++ b/crates/edgezero-test-harness/src/net/wire.rs @@ -0,0 +1,858 @@ +use super::overflow::{DeadlineSocket, Profile, capture_wire_body, read_head}; +use crate::evidence::{Result, require}; +use serde_json::{Value, json}; +use std::{ + collections::{HashMap, VecDeque}, + io::{self, BufReader, Read, Write}, + net::{TcpListener, TcpStream}, + sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError}, + thread, + time::{Duration, Instant}, +}; + +mod lifetimes; + +const GZIP_TEN: &[u8] = &[ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 76, 132, 1, 0, 240, 205, 17, 76, 10, 0, 0, 0, +]; +const GZIP_ELEVEN: &[u8] = &[ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 76, 132, 3, 0, 146, 93, 70, 85, 11, 0, 0, 0, +]; +const IO_BUDGET: Duration = Duration::from_secs(5); +const GATE_BUDGET: Duration = Duration::from_secs(6); +const CONNECTION_LIMIT: usize = 128; +const ACTIVE_LIMIT: usize = 8; + +#[derive(Default)] +struct State { + stop: bool, + accepted: usize, + active: usize, + workers: Vec>, + errors: Vec, + wire: VecDeque, + requests: Vec<(u16, String)>, + gates: HashMap, + bidders: [bool; 3], + release_bidders: bool, + lifetimes: HashMap, +} + +#[derive(Default)] +struct Shared { + state: Mutex, + wake: Condvar, +} + +impl Shared { + fn lock(&self) -> MutexGuard<'_, State> { + self.state.lock().unwrap_or_else(PoisonError::into_inner) + } + + fn error(&self, error: impl ToString) { + self.lock().errors.push(error.to_string()); + self.wake.notify_all(); + } + + fn release(&self, name: &str) -> Result<()> { + let mut state = self.lock(); + let gate = state.gates.get_mut(name).ok_or("unarmed P5 gate")?; + *gate = true; + self.wake.notify_all(); + Ok(()) + } + + fn wait(&self, budget: Duration, ready: impl Fn(&State) -> bool) -> Result<()> { + let deadline = Instant::now() + budget; + let mut state = self.lock(); + while !ready(&state) { + let remaining = deadline + .checked_duration_since(Instant::now()) + .ok_or("wire gate timed out")?; + state = self + .wake + .wait_timeout(state, remaining) + .unwrap_or_else(PoisonError::into_inner) + .0; + } + Ok(()) + } +} + +pub struct WireOrigins { + ports: [u16; 2], + shared: Arc, + collectors: Vec>, +} + +impl WireOrigins { + pub fn start() -> Result { + let first = TcpListener::bind(("127.0.0.1", 0))?; + let second = TcpListener::bind(("127.0.0.1", 0))?; + let ports = [first.local_addr()?.port(), second.local_addr()?.port()]; + first.set_nonblocking(true)?; + second.set_nonblocking(true)?; + let mut origins = Self { + ports, + shared: Arc::new(Shared::default()), + collectors: Vec::new(), + }; + for (index, listener) in [first, second].into_iter().enumerate() { + let shared = Arc::clone(&origins.shared); + origins.collectors.push( + thread::Builder::new() + .name(format!("spin-wire-origin-{index}")) + .spawn(move || collect(listener, ports[index], ports[1], &shared))?, + ); + } + Ok(origins) + } + + pub fn port(&self, index: usize) -> u16 { + self.ports[index] + } + + pub fn register_lifetime(&self, token: &str, case: &str) -> Result<()> { + lifetimes::register(&self.shared, token, case) + } + + pub fn mark_lifetime_start(&self, token: &str) -> Result<()> { + lifetimes::mark_start(&self.shared, token) + } + + pub fn take_lifetime_events(&self, token: &str) -> Result { + lifetimes::take_events(&self.shared, token) + } + + pub fn arm(&self, name: &str) { + let mut state = self.shared.lock(); + if name.is_empty() + || name.len() > 128 + || !name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + { + state.errors.push("invalid P5 gate name".to_owned()); + } else if state.gates.contains_key(name) { + state.errors.push("P5 gate armed twice".to_owned()); + } else if state.gates.len() >= CONNECTION_LIMIT { + state.errors.push("P5 gate limit".to_owned()); + } else { + state.gates.insert(name.to_owned(), false); + } + } + + pub fn release(&self, name: &str) { + if let Err(error) = self.shared.release(name) { + self.shared.error(error); + } + } + + pub fn take_wire(&self) -> Result { + self.shared.wait(IO_BUDGET, |state| { + !state.wire.is_empty() || !state.errors.is_empty() + })?; + let mut state = self.shared.lock(); + require(state.errors.is_empty(), state.errors.join("; "))?; + state + .wire + .pop_front() + .ok_or_else(|| "missing wire receipt".into()) + } + + pub fn take_requests(&self) -> Vec<(u16, String)> { + std::mem::take(&mut self.shared.lock().requests) + } + + pub fn finish(mut self) -> Result<()> { + self.shutdown(); + let state = self.shared.lock(); + let mut errors = state.errors.clone(); + if !state.wire.is_empty() { + errors.push(format!("{} undrained wire receipts", state.wire.len())); + } + if !state.requests.is_empty() { + errors.push(format!("{} undrained P5 requests", state.requests.len())); + } + for (token, lifetime) in &state.lifetimes { + if lifetime.undrained() != 0 { + errors.push(format!( + "{} undrained lifetime events for {token}", + lifetime.undrained() + )); + } + } + require(errors.is_empty(), errors.join("; ")) + } + + fn shutdown(&mut self) { + { + let mut state = self.shared.lock(); + state.stop = true; + state.bidders.fill(true); + state.release_bidders = true; + for released in state.gates.values_mut() { + *released = true; + } + self.shared.wake.notify_all(); + } + for collector in self.collectors.drain(..) { + if collector.join().is_err() { + self.shared.error("wire collector panicked"); + } + } + let workers = std::mem::take(&mut self.shared.lock().workers); + for worker in workers { + if worker.join().is_err() { + self.shared.error("wire worker panicked"); + } + } + } +} + +impl Drop for WireOrigins { + fn drop(&mut self) { + self.shutdown(); + } +} + +fn collect(listener: TcpListener, port: u16, second: u16, shared: &Arc) { + loop { + if shared.lock().stop { + break; + } + match listener.accept() { + Ok((socket, _)) => { + let mut state = shared.lock(); + state.accepted += 1; + if state.accepted > CONNECTION_LIMIT || state.active >= ACTIVE_LIMIT { + state.errors.push("wire origin connection limit".to_owned()); + state.stop = true; + shared.wake.notify_all(); + continue; + } + state.active += 1; + let observed = Arc::clone(shared); + match thread::Builder::new() + .name("spin-wire-request".to_owned()) + .spawn(move || { + let outcome = serve(socket, port, second, &observed); + let mut state = observed.lock(); + if let Err(error) = outcome { + state.errors.push(error.to_string()); + } + state.active -= 1; + observed.wake.notify_all(); + }) { + Ok(worker) => state.workers.push(worker), + Err(error) => { + state.active -= 1; + state.errors.push(error.to_string()); + shared.wake.notify_all(); + } + } + } + Err(error) if error.kind() == io::ErrorKind::WouldBlock => { + let state = shared.lock(); + if state.stop { + break; + } + drop( + shared + .wake + .wait_timeout(state, Duration::from_millis(5)) + .unwrap_or_else(PoisonError::into_inner), + ); + } + Err(error) => { + shared.error(error); + break; + } + } + } +} + +fn serve(mut socket: TcpStream, port: u16, second: u16, shared: &Shared) -> Result<()> { + socket.set_nonblocking(false)?; + socket.set_write_timeout(Some(IO_BUDGET))?; + let mut reader = BufReader::new(DeadlineSocket { + socket: socket.try_clone()?, + deadline: Instant::now() + IO_BUDGET, + }); + let head_started = Instant::now(); + let head = read_head(&mut reader, &mut 0, Profile::Wire).map_err(|error| error.name())?; + let head_received = Instant::now(); + let parts: Vec<_> = head.first.split(' ').collect(); + require( + head.first.is_ascii() && parts.len() == 3 && matches!(parts[2], "HTTP/1.0" | "HTTP/1.1"), + "invalid wire request line", + )?; + let (method, target) = (parts[0], parts[1]); + if target.starts_with("/lifetimes/") { + require( + method == "GET" + && !head + .fields + .iter() + .any(|(name, _)| name == "transfer-encoding") + && head + .fields + .iter() + .filter(|(name, _)| name == "content-length") + .count() + <= 1 + && head + .fields + .iter() + .filter(|(name, _)| name == "content-length") + .all(|(_, value)| value == "0") + && reader.buffer().is_empty(), + "invalid lifetime method or framing", + )?; + return lifetimes::serve( + &mut socket, + target, + &head.fields, + shared, + head_started, + head_received, + ); + } + if target.starts_with("/p5/") { + require( + method == "GET" + && !head + .fields + .iter() + .any(|(name, _)| name == "transfer-encoding") + && head + .fields + .iter() + .filter(|(name, _)| name == "content-length") + .all(|(_, value)| value == "0"), + "P5 request carried framing", + )?; + shared.lock().requests.push((port, target.to_owned())); + return p5(&mut socket, target, second, shared); + } + if target.starts_with("/handoff-") { + require(method == "GET", "handoff request method changed")?; + return handoff(&mut socket, target, shared); + } + require( + matches!( + target, + "/get" | "/head" | "/get-settled" | "/head-settled" | "/post" + ), + "unknown wire control route", + )?; + let (body, mut encoded) = + capture_wire_body(&mut reader, &head).map_err(|error| error.name())?; + reader.get_mut().deadline = Instant::now() + Duration::from_millis(50); + let mut extra = [0]; + match reader.read(&mut extra) { + Ok(0) => {} + Ok(_) => encoded.push(extra[0]), + Err(error) + if matches!( + error.kind(), + io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock + ) => {} + Err(error) => return Err(error.into()), + } + require(body.is_ascii(), "wire payload was not ASCII")?; + let hex: String = encoded.iter().map(|byte| format!("{byte:02x}")).collect(); + let fields = |field: &str| { + head.fields + .iter() + .filter(|(name, _)| name == field) + .map(|(_, value)| value.clone()) + .collect::>() + }; + shared + .lock() + .wire + .push_back(json!({"method":method,"target":target, + "content_length":fields("content-length"),"transfer_encoding":fields("transfer-encoding"), + "encoded_body_hex":hex,"payload":String::from_utf8(body)?})); + shared.wake.notify_all(); + socket.write_all(b"HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n")?; + Ok(()) +} + +fn handoff(socket: &mut TcpStream, target: &str, shared: &Shared) -> Result<()> { + let body = match target { + "/handoff-origin" => { + shared.wait(IO_BUDGET, |state| { + state.bidders.iter().all(|started| *started) + })?; + b"origin".as_slice() + } + "/handoff-release" => { + shared.lock().release_bidders = true; + shared.wake.notify_all(); + b"".as_slice() + } + "/handoff-bid-A" | "/handoff-bid-B" | "/handoff-bid-C" => { + let index = usize::from(target.as_bytes()[target.len() - 1] - b'A'); + shared.lock().bidders[index] = true; + shared.wake.notify_all(); + shared.wait(IO_BUDGET, |state| state.release_bidders)?; + [b"bid-A", b"bid-B", b"bid-C"][index].as_slice() + } + _ => return Err("unpolled retained Send performed provider work".into()), + }; + write!( + socket, + "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: {}\r\n\r\n", + body.len() + )?; + socket.write_all(body)?; + Ok(()) +} + +fn p5(socket: &mut TcpStream, target: &str, second: u16, shared: &Shared) -> Result<()> { + let parts: Vec<_> = target + .strip_prefix("/p5/") + .ok_or("invalid P5 route")? + .split('/') + .collect(); + let mut status = 200; + let mut fields = Vec::<(&str, String)>::new(); + let mut body = b"final".to_vec(); + let mut pending = None; + match parts.as_slice() { + [ + "body", + coding @ ("identity" | "gzip" | "passthrough"), + size @ ("10" | "11"), + ] => { + let bytes = if *coding == "gzip" { + if *size == "10" { GZIP_TEN } else { GZIP_ELEVEN } + } else { + &[b'a'; 11][..if *size == "10" { 10 } else { 11 }] + }; + fields.push(( + "Content-Encoding", + if *coding == "passthrough" { + "x-p5-opaque" + } else { + coding + } + .to_string(), + )); + fields.push(("Transfer-Encoding", "chunked".to_owned())); + body = format!("{:X}\r\n", bytes.len()).into_bytes(); + body.extend_from_slice(bytes); + body.extend_from_slice(b"\r\n0\r\n\r\n"); + } + ["declared", coding @ ("identity" | "gzip" | "passthrough")] => { + fields.push(( + "Content-Encoding", + if *coding == "passthrough" { + "x-p5-opaque" + } else { + coding + } + .to_string(), + )); + fields.push(("Content-Length", "11".to_owned())); + body = vec![b'a'; 11]; + } + ["cross"] | ["limit", _, _] | ["to-body", _, _] => { + status = 302; + body.clear(); + let location = match parts.as_slice() { + ["cross"] => format!("http://127.0.0.1:{second}/p5/final"), + ["limit", kind, name] => format!("/p5/redirect/{kind}/{name}"), + ["to-body", coding, size] => format!("/p5/body/{coding}/{size}"), + _ => unreachable!(), + }; + fields.extend([("Location", location), ("Content-Length", "0".to_owned())]); + } + ["redirect", kind, name] => { + status = 302; + body.clear(); + fields.push(("Location", format!("/p5/next/{name}"))); + match *kind { + "emptybr" => fields.extend([ + ("Content-Encoding", "br".to_owned()), + ("Content-Length", "0".to_owned()), + ]), + "oversized" => { + fields.push(("Content-Length", "10485760".to_owned())); + pending = Some(*name); + } + "pending" => { + fields.push(("Transfer-Encoding", "chunked".to_owned())); + pending = Some(*name); + } + "error" => { + fields.push(("Transfer-Encoding", "chunked".to_owned())); + body.extend_from_slice(b"3\r\nbad\r\n"); + } + "clean" => { + body = b"found".to_vec(); + fields.push(("Content-Length", "5".to_owned())); + } + _ => return Err("unknown P5 redirect kind".into()), + } + if let Some(name) = pending { + require(shared.lock().gates.contains_key(name), "unarmed P5 gate")?; + } + } + ["next", name] => { + shared.release(name)?; + fields.push(("Content-Length", "5".to_owned())); + } + ["final"] => fields.push(("Content-Length", "5".to_owned())), + _ => return Err("unknown P5 origin route".into()), + } + write!( + socket, + "HTTP/1.1 {status} Fixture\r\nDate: Tue, 27 Oct 2026 00:00:00 GMT\r\nConnection: close\r\nContent-Type: application/octet-stream\r\nX-P5-Fixture: spin-wire\r\n" + )?; + for (name, value) in fields { + write!(socket, "{name}: {value}\r\n")?; + } + socket.write_all(b"\r\n")?; + socket.write_all(&body)?; + socket.flush()?; + if let Some(name) = pending { + shared.wait(GATE_BUDGET, |state| state.gates.get(name) == Some(&true))?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::{ + io::{Read, Write}, + net::TcpStream, + time::Duration, + }; + + fn exchange(origins: &WireOrigins, index: usize, wire: &[u8]) -> Vec { + let mut socket = TcpStream::connect(("127.0.0.1", origins.port(index))).unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(3))) + .unwrap(); + socket.write_all(wire).unwrap(); + let mut response = Vec::new(); + socket.read_to_end(&mut response).unwrap(); + response + } + + #[test] + fn raw_capture_preserves_duplicate_fields_chunk_framing_and_extra_byte() { + let origins = WireOrigins::start().unwrap(); + exchange( + &origins, + 0, + b"GET /get HTTP/1.1\r\nContent-Length: 0\r\nContent-Length: 0\r\n\r\n", + ); + let receipt = origins.take_wire().unwrap(); + assert_eq!(receipt["content_length"], json!(["0", "0"])); + assert_eq!(receipt["transfer_encoding"], json!([])); + assert_eq!(receipt["encoded_body_hex"], ""); + exchange( + &origins, + 0, + b"GET /get-settled HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n\xff", + ); + let receipt = origins.take_wire().unwrap(); + assert_eq!(receipt["transfer_encoding"], json!(["chunked"])); + assert_eq!(receipt["encoded_body_hex"], "300d0a0d0aff"); + assert_eq!(receipt["payload"], ""); + origins.finish().unwrap(); + } + + #[test] + fn controls_capture_exact_payload_and_bodyless_distinctions() { + let origins = WireOrigins::start().unwrap(); + for (method, target) in [("GET", "/get"), ("HEAD", "/head-settled")] { + exchange( + &origins, + 0, + format!("{method} {target} HTTP/1.1\r\n\r\n").as_bytes(), + ); + assert_eq!( + origins.take_wire().unwrap(), + json!({"method":method,"target":target, + "content_length":[],"transfer_encoding":[],"encoded_body_hex":"","payload":""}) + ); + } + exchange( + &origins, + 0, + b"POST /post HTTP/1.1\r\nContent-Length: 14\r\n\r\nupload-control", + ); + assert_eq!(origins.take_wire().unwrap()["payload"], "upload-control"); + origins.finish().unwrap(); + } + + #[test] + fn p5_cross_origin_receipts_and_literal_gzip_are_preserved() { + let origins = WireOrigins::start().unwrap(); + let response = exchange(&origins, 0, b"GET /p5/cross HTTP/1.1\r\n\r\n"); + assert!(String::from_utf8(response).unwrap().contains(&format!( + "Location: http://127.0.0.1:{}/p5/final\r\n", + origins.port(1) + ))); + exchange(&origins, 1, b"GET /p5/final HTTP/1.1\r\n\r\n"); + assert_eq!( + origins.take_requests(), + vec![ + (origins.port(0), "/p5/cross".into()), + (origins.port(1), "/p5/final".into()) + ] + ); + for size in [10, 11] { + let response = exchange( + &origins, + 0, + format!("GET /p5/body/gzip/{size} HTTP/1.1\r\n\r\n").as_bytes(), + ); + assert!( + response.starts_with( + b"HTTP/1.1 200 Fixture\r\nDate: Tue, 27 Oct 2026 00:00:00 GMT\r\n" + ) + ); + let expected = if size == 10 { + [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 76, 132, 1, 0, 240, 205, 17, 76, 10, 0, 0, + 0, + ] + } else { + [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 76, 132, 3, 0, 146, 93, 70, 85, 11, 0, 0, + 0, + ] + }; + let start = response + .windows(4) + .position(|part| part == b"\r\n\r\n") + .unwrap() + + 4; + let mut expected_body = b"17\r\n".to_vec(); + expected_body.extend(expected); + expected_body.extend(b"\r\n0\r\n\r\n"); + assert_eq!(&response[start..], expected_body); + } + assert_eq!(origins.take_requests().len(), 2); + origins.finish().unwrap(); + } + + #[test] + fn finish_rejects_undrained_receipts_and_unpolled_dispatch() { + for wire in [ + &b"GET /get HTTP/1.1\r\n\r\n"[..], + &b"GET /p5/final HTTP/1.1\r\n\r\n"[..], + &b"GET /handoff-unused HTTP/1.1\r\n\r\n"[..], + ] { + let origins = WireOrigins::start().unwrap(); + exchange(&origins, 0, wire); + assert!(origins.finish().is_err()); + } + } + + fn assert_rejected_without_receipt(wire: &[u8], expected: &str) { + let origins = WireOrigins::start().unwrap(); + let shared = Arc::clone(&origins.shared); + let response = exchange(&origins, 0, wire); + let error = origins.finish().unwrap_err().to_string(); + assert_eq!(error, expected); + assert!( + response.is_empty(), + "rejected request received a success response" + ); + let state = shared.lock(); + assert!( + state.wire.is_empty(), + "rejected request produced a capture receipt" + ); + assert!( + state.requests.is_empty(), + "rejected request produced a P5 receipt" + ); + } + + #[test] + fn lifetime_routes_reject_unregistered_tokens() { + assert_rejected_without_receipt( + b"GET /lifetimes/missing/begin-progress/origin HTTP/1.1\r\nx-fixture-at-us: 2\r\n\r\n", + "unknown lifetime token", + ); + } + + #[test] + fn non_ascii_payloads_are_rejected_without_success_receipts() { + for wire in [ + &b"POST /post HTTP/1.1\r\nContent-Length: 1\r\n\r\n\xe1"[..], + &b"POST /post HTTP/1.1\r\nContent-Length: 2\r\n\r\n\xc3\xa9"[..], + &b"POST /post HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n2\r\n\xc3\xa9\r\n0\r\n\r\n" + [..], + ] { + assert_rejected_without_receipt(wire, "wire payload was not ASCII"); + } + } + + #[test] + fn ambiguous_framing_is_rejected_by_wire_origins_without_success_receipts() { + for wire in [ + &b"POST /post HTTP/1.1\r\nTransfer-Encoding: chunked\r\nTransfer-Encoding: chunked\r\n\r\n"[..], + &b"POST /post HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 2\r\n\r\n"[..], + &b"POST /post HTTP/1.1\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n\r\n"[..], + ] { + assert_rejected_without_receipt(wire, "invalid_framing"); + } + } + + fn pending(origins: &WireOrigins, name: &str) -> TcpStream { + use std::io::{BufRead, BufReader}; + origins.arm(name); + let mut socket = TcpStream::connect(("127.0.0.1", origins.port(0))).unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + write!(socket, "GET /p5/redirect/pending/{name} HTTP/1.1\r\n\r\n").unwrap(); + let mut reader = BufReader::new(socket); + loop { + let mut line = String::new(); + assert!(reader.read_line(&mut line).unwrap() > 0); + if line == "\r\n" { + break; + } + } + reader.into_inner() + } + + #[test] + fn p5_next_releases_the_pending_intermediate_without_body_data() { + let origins = WireOrigins::start().unwrap(); + let mut socket = pending(&origins, "pending-follow"); + let response = exchange(&origins, 0, b"GET /p5/next/pending-follow HTTP/1.1\r\n\r\n"); + assert!(response.ends_with(b"\r\n\r\nfinal")); + let mut bytes = Vec::new(); + socket.read_to_end(&mut bytes).unwrap(); + assert!(bytes.is_empty()); + assert_eq!( + origins.take_requests(), + vec![ + ( + origins.port(0), + "/p5/redirect/pending/pending-follow".into() + ), + (origins.port(0), "/p5/next/pending-follow".into()) + ] + ); + origins.finish().unwrap(); + } + + #[test] + fn finish_and_drop_release_pending_gates_join_workers_and_close_both_ports() { + for finish in [false, true] { + let origins = WireOrigins::start().unwrap(); + let ports = [origins.port(0), origins.port(1)]; + let mut socket = pending(&origins, "pending-cleanup"); + assert_eq!(origins.take_requests().len(), 1); + let started = Instant::now(); + if finish { + origins.finish().unwrap(); + } else { + drop(origins); + } + assert!(started.elapsed() < Duration::from_secs(1)); + let mut bytes = Vec::new(); + socket.read_to_end(&mut bytes).unwrap(); + assert!(bytes.is_empty()); + for port in ports { + assert!(TcpStream::connect(("127.0.0.1", port)).is_err()); + } + } + } + + #[test] + fn finish_waits_for_late_worker_errors_before_validating_drained_queues() { + use std::sync::mpsc; + let origins = WireOrigins::start().unwrap(); + exchange(&origins, 0, b"GET /get HTTP/1.1\r\n\r\n"); + origins.take_wire().unwrap(); + let (release_tx, release_rx) = mpsc::channel(); + let observed = Arc::clone(&origins.shared); + // Inject a tracked worker failure after a successful client reply and queue drain. + origins.shared.lock().workers.push(thread::spawn(move || { + release_rx.recv().unwrap(); + observed.error("injected late worker failure"); + observed.lock().requests.push((1, "/p5/late".to_owned())); + })); + let (finished_tx, finished_rx) = mpsc::channel(); + let finisher = thread::spawn(move || { + finished_tx.send(origins.finish()).unwrap(); + }); + let before_release = finished_rx.recv_timeout(Duration::from_millis(50)); + release_tx.send(()).unwrap(); + let result = finished_rx.recv_timeout(Duration::from_secs(2)).unwrap(); + finisher.join().unwrap(); + assert!(matches!( + before_release, + Err(mpsc::RecvTimeoutError::Timeout) + )); + let error = result.unwrap_err().to_string(); + assert!(error.contains("injected late worker failure")); + assert!(error.contains("undrained P5 requests")); + } + + #[test] + fn handoff_waits_for_three_bidders_and_keeps_them_pending_until_release() { + let origins = WireOrigins::start().unwrap(); + let connect = |path: &str| { + let mut socket = TcpStream::connect(("127.0.0.1", origins.port(0))).unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + write!(socket, "GET {path} HTTP/1.1\r\n\r\n").unwrap(); + socket + }; + let mut origin = connect("/handoff-origin"); + let mut bidders = ["A", "B", "C"].map(|name| connect(&format!("/handoff-bid-{name}"))); + let mut bytes = Vec::new(); + origin.read_to_end(&mut bytes).unwrap(); + assert!(bytes.ends_with(b"\r\n\r\norigin")); + for bidder in &mut bidders { + bidder + .set_read_timeout(Some(Duration::from_millis(20))) + .unwrap(); + let error = bidder.read(&mut [0]).unwrap_err(); + assert!(matches!( + error.kind(), + io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock + )); + bidder + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + } + exchange(&origins, 0, b"GET /handoff-release HTTP/1.1\r\n\r\n"); + for (index, bidder) in bidders.iter_mut().enumerate() { + bytes.clear(); + bidder.read_to_end(&mut bytes).unwrap(); + assert!(bytes.ends_with([b"bid-A", b"bid-B", b"bid-C"][index])); + } + origins.finish().unwrap(); + } + + #[test] + fn bad_gate_operations_are_retained_for_final_validation() { + let origins = WireOrigins::start().unwrap(); + origins.arm("test"); + origins.release("test"); + origins.release("test"); + origins.arm("test"); + origins.release("unknown"); + let error = origins.finish().unwrap_err().to_string(); + assert!(error.contains("armed twice")); + assert!(error.contains("unarmed P5 gate")); + } +} diff --git a/crates/edgezero-test-harness/src/net/wire/lifetimes.rs b/crates/edgezero-test-harness/src/net/wire/lifetimes.rs new file mode 100644 index 000000000..d5cc185b3 --- /dev/null +++ b/crates/edgezero-test-harness/src/net/wire/lifetimes.rs @@ -0,0 +1,791 @@ +use super::*; +use std::{collections::HashSet, net::Shutdown}; + +const EVENT_LIMIT: usize = 32; + +#[derive(Clone, Copy)] +enum Case { + Slow, + Fast, + Begin, + Batch, +} + +impl Case { + fn parse(case: &str) -> Result { + match case { + "slow-origin" => Ok(Self::Slow), + "fast-origin" => Ok(Self::Fast), + "begin-progress" => Ok(Self::Begin), + "batch-progress" => Ok(Self::Batch), + _ => Err("unknown lifetime case".into()), + } + } + + fn name(self) -> &'static str { + match self { + Self::Slow => "slow-origin", + Self::Fast => "fast-origin", + Self::Begin => "begin-progress", + Self::Batch => "batch-progress", + } + } + + fn permits(self, role: &str) -> bool { + match self { + Self::Slow => matches!(role, "origin" | "a" | "b" | "c"), + Self::Fast => matches!(role, "origin" | "a" | "b" | "c" | "mediator"), + Self::Begin => role == "origin", + Self::Batch => matches!(role, "a" | "b" | "c" | "quiet"), + } + } +} + +pub(super) struct Lifetime { + case: Case, + anchor: Option, + scenario_anchor: Option, + roles: HashSet, + active: usize, + events: Vec, +} + +impl Lifetime { + pub(super) fn undrained(&self) -> usize { + self.events.len() + } +} + +pub(super) fn register(shared: &Shared, token: &str, case: &str) -> Result<()> { + require( + !token.is_empty() + && token.len() <= 64 + && token + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')), + "invalid lifetime token", + )?; + let case = Case::parse(case)?; + let mut state = shared.lock(); + require(!state.stop, "wire origin stopped")?; + require( + !state.lifetimes.contains_key(token), + "duplicate lifetime token", + )?; + require( + state.lifetimes.len() < CONNECTION_LIMIT, + "lifetime registration limit", + )?; + state.lifetimes.insert( + token.to_owned(), + Lifetime { + case, + anchor: None, + scenario_anchor: None, + roles: HashSet::new(), + active: 0, + events: Vec::new(), + }, + ); + Ok(()) +} + +pub(super) fn mark_start(shared: &Shared, token: &str) -> Result<()> { + let mut state = shared.lock(); + require(!state.stop, "wire origin stopped")?; + let lifetime = state + .lifetimes + .get_mut(token) + .ok_or("unknown lifetime token")?; + require(lifetime.anchor.is_none(), "lifetime already started")?; + lifetime.anchor = Some(Instant::now()); + Ok(()) +} + +pub(super) fn take_events(shared: &Shared, token: &str) -> Result { + let deadline = Instant::now() + IO_BUDGET; + let mut state = shared.lock(); + loop { + let lifetime = state.lifetimes.get(token).ok_or("unknown lifetime token")?; + require(lifetime.anchor.is_some(), "lifetime not started")?; + if lifetime.active == 0 { + let events = std::mem::take(&mut state.lifetimes.get_mut(token).unwrap().events); + return Ok(Value::Array(events)); + } + require(!state.stop, "wire origin stopped")?; + let remaining = deadline + .checked_duration_since(Instant::now()) + .ok_or("lifetime event collection timed out")?; + state = shared + .wake + .wait_timeout(state, remaining) + .unwrap_or_else(PoisonError::into_inner) + .0; + } +} + +struct Exchange<'a> { + token: &'a str, + role: &'a str, + case: Case, + anchor: Instant, + scenario_anchor: Option, + received: Instant, +} + +fn offset(instant: Instant, anchor: Instant) -> i64 { + if instant >= anchor { + instant.duration_since(anchor).as_micros() as i64 + } else { + -(anchor.duration_since(instant).as_micros() as i64) + } +} + +impl Exchange<'_> { + fn event(&self, op: &str, now: Instant, scheduled: Option) -> Value { + let mut event = json!({"op":op,"token":self.token,"case":self.case.name(), + "role":self.role,"at_us":offset(now,self.anchor)}); + if let Some(scheduled) = scheduled { + event["scheduled_us"] = json!(offset(scheduled, self.anchor)); + if let Some(anchor) = self.scenario_anchor { + event["scenario_scheduled_us"] = json!(offset(scheduled, anchor)); + event["schedule_basis"] = json!("origin-receipt-minus-guest-at-us"); + } else { + event["schedule_basis"] = json!("request-receipt"); + } + } + event + } +} + +fn record(shared: &Shared, exchange: &Exchange<'_>, event: Value) -> Result<()> { + let mut state = shared.lock(); + let lifetime = state.lifetimes.get_mut(exchange.token).unwrap(); + require(lifetime.events.len() < EVENT_LIMIT, "lifetime event limit")?; + lifetime.events.push(event); + shared.wake.notify_all(); + Ok(()) +} + +fn wait_until(shared: &Shared, deadline: Instant) -> bool { + let mut state = shared.lock(); + loop { + if state.stop { + return false; + } + let Some(remaining) = deadline.checked_duration_since(Instant::now()) else { + return true; + }; + state = shared + .wake + .wait_timeout(state, remaining) + .unwrap_or_else(PoisonError::into_inner) + .0; + } +} + +pub(super) fn serve( + socket: &mut TcpStream, + target: &str, + fields: &[(String, String)], + shared: &Shared, + head_started: Instant, + head_received: Instant, +) -> Result<()> { + let parts: Vec<_> = target.split('/').collect(); + let ["", "lifetimes", token, case, role] = parts.as_slice() else { + return Err("invalid lifetime route".into()); + }; + let mut exchange = { + let mut state = shared.lock(); + require(!state.stop, "wire origin stopped")?; + let lifetime = state + .lifetimes + .get_mut(*token) + .ok_or("unknown lifetime token")?; + require(*case == lifetime.case.name(), "lifetime case mismatch")?; + require(lifetime.case.permits(role), "unknown lifetime role")?; + let anchor = lifetime.anchor.ok_or("lifetime not started")?; + let values: Vec<_> = fields + .iter() + .filter(|(name, _)| name == "x-fixture-at-us") + .map(|(_, value)| value.as_str()) + .collect(); + require( + values.len() == 1 + && !values[0].is_empty() + && values[0].bytes().all(|byte| byte.is_ascii_digit()), + "invalid lifetime guest clock", + )?; + let guest_at = values[0] + .parse::() + .map_err(|_| "invalid lifetime guest clock")?; + require(guest_at <= 15_000_000, "invalid lifetime guest clock")?; + require(!lifetime.roles.contains(*role), "duplicate lifetime role")?; + if matches!(lifetime.case, Case::Slow | Case::Fast) && *role == "origin" { + lifetime.scenario_anchor = Some( + head_received + .checked_sub(Duration::from_micros(guest_at)) + .ok_or("invalid lifetime guest clock")?, + ); + } + let exchange = Exchange { + token, + role, + case: lifetime.case, + anchor, + scenario_anchor: lifetime.scenario_anchor, + received: head_received, + }; + let mut event = exchange.event("request", exchange.received, None); + event["guest_at_us"] = json!(guest_at); + if matches!(lifetime.case, Case::Slow | Case::Fast) && *role == "origin" { + event["alignment"] = json!({ + "basis":"origin-receipt-minus-guest-at-us", + "scenario_anchor_host_offset_us":offset(lifetime.scenario_anchor.unwrap(),anchor), + "boundary_start_us":offset(head_started,anchor), + "boundary_end_us":offset(head_received,anchor), + "boundary_span_us":head_received.duration_since(head_started).as_micros() as u64, + "transit_uncertainty_us":null,"clock_sync_proven":false + }); + } + lifetime.events.push(event); + lifetime.roles.insert((*role).to_owned()); + lifetime.active += 1; + shared.wake.notify_all(); + exchange + }; + let outcome = align(shared, &mut exchange).and_then(|ready| { + if ready { + respond(socket, shared, &exchange) + } else { + Ok(false) + } + }); + let expected_close = matches!(exchange.case, Case::Slow) + && exchange.role == "c" + && exchange + .scenario_anchor + .is_some_and(|anchor| Instant::now() >= anchor + Duration::from_millis(534)) + && outcome + .as_ref() + .err() + .and_then(|error| error.downcast_ref::()) + .is_some_and(|error| { + matches!( + error.kind(), + io::ErrorKind::BrokenPipe + | io::ErrorKind::ConnectionReset + | io::ErrorKind::ConnectionAborted + ) + }); + let close = socket.shutdown(Shutdown::Both); + let scheduled_close = if matches!(exchange.case, Case::Fast) && exchange.role == "mediator" { + exchange + .scenario_anchor + .map(|anchor| anchor + Duration::from_millis(1500)) + } else { + None + }; + let mut event = exchange.event("closed", Instant::now(), scheduled_close); + event["reason"] = json!(match &outcome { + Err(_) if expected_close => "peer_closed_after_cutoff", + Ok(true) if exchange.role == "mediator" => "silent", + Ok(true) => "complete", + Ok(false) => "stopped", + Err(_) => "error", + }); + if let Err(error) = &outcome { + event["error"] = json!(error.to_string()); + } + let recorded = record(shared, &exchange, event); + { + let mut state = shared.lock(); + state.lifetimes.get_mut(*token).unwrap().active -= 1; + shared.wake.notify_all(); + } + if !expected_close { + outcome?; + } + recorded?; + match close { + Ok(()) => Ok(()), + Err(error) if error.kind() == io::ErrorKind::NotConnected => Ok(()), + Err(error) => Err(error.into()), + } +} + +fn align(shared: &Shared, exchange: &mut Exchange<'_>) -> Result { + if !matches!(exchange.case, Case::Slow | Case::Fast) { + return Ok(true); + } + let deadline = Instant::now() + IO_BUDGET; + let mut state = shared.lock(); + loop { + if state.stop { + return Ok(false); + } + if let Some(anchor) = state.lifetimes[exchange.token].scenario_anchor { + exchange.scenario_anchor = Some(anchor); + return Ok(true); + } + let remaining = deadline + .checked_duration_since(Instant::now()) + .ok_or("lifetime origin alignment timed out")?; + state = shared + .wake + .wait_timeout(state, remaining) + .unwrap_or_else(PoisonError::into_inner) + .0; + } +} + +fn respond(socket: &mut TcpStream, shared: &Shared, exchange: &Exchange<'_>) -> Result { + let ms = |value| Duration::from_millis(value); + let absolute = |value| exchange.scenario_anchor.unwrap() + ms(value); + let received = |value| exchange.received + ms(value); + let (head_at, chunks): (Instant, Vec<(Instant, &[u8])>) = match (exchange.case, exchange.role) { + (Case::Slow, "origin") => ( + absolute(700), + vec![ + (absolute(700), b"O"), + (absolute(900), b"o"), + (absolute(1300), b"!"), + ], + ), + (Case::Fast, "origin") => (absolute(120), vec![(absolute(260), b"O")]), + (Case::Begin, "origin") => (received(150), vec![(received(150), b"O")]), + (Case::Batch, "quiet") => (received(300), Vec::new()), + (Case::Fast, "mediator") => { + // Stay silent beyond both the auction cutoff and the 1000 ms per-call timer. + return Ok(wait_until(shared, absolute(1500))); + } + (case, role @ ("a" | "b" | "c")) => { + let index = usize::from(role.as_bytes()[0] - b'a'); + let at = match case { + Case::Slow => absolute([180, 320, 900][index]), + Case::Fast => absolute([180, 320, 400][index]), + Case::Batch => received([20, 40, 60][index]), + Case::Begin => unreachable!(), + }; + (at, vec![(at, [b"A".as_slice(), b"B", b"C"][index])]) + } + _ => unreachable!(), + }; + if !wait_until(shared, head_at) { + return Ok(false); + } + let chunked = exchange.role == "origin"; + let head = if chunked { + b"HTTP/1.1 200 OK\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n".as_slice() + } else if chunks.is_empty() { + b"HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n" + } else { + b"HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 1\r\n\r\n" + }; + let write_started = Instant::now(); + socket.write_all(head)?; + socket.flush()?; + let mut event = exchange.event("head", Instant::now(), Some(head_at)); + event["write_started_us"] = json!(offset(write_started, exchange.anchor)); + record(shared, exchange, event)?; + for (at, bytes) in chunks { + if !wait_until(shared, at) { + return Ok(false); + } + if chunked { + write!(socket, "{:X}\r\n", bytes.len())?; + } + socket.write_all(bytes)?; + if chunked { + socket.write_all(b"\r\n")?; + } + socket.flush()?; + let mut event = exchange.event("chunk", Instant::now(), Some(at)); + event["bytes"] = json!(bytes); + record(shared, exchange, event)?; + } + if chunked { + socket.write_all(b"0\r\n\r\n")?; + socket.flush()?; + } + Ok(true) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn registration_accepts_the_same_ascii_token_alphabet_as_the_guest() { + let shared = Shared::default(); + register(&shared, "Run_1-a", "begin-progress").unwrap(); + } + + fn connect( + origins: &WireOrigins, + token: &str, + case: &str, + role: &str, + clock: &str, + ) -> TcpStream { + let mut socket = TcpStream::connect(("127.0.0.1", origins.port(0))).unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(3))) + .unwrap(); + write!( + socket, + "GET /lifetimes/{token}/{case}/{role} HTTP/1.1\r\nx-fixture-at-us: {clock}\r\n\r\n" + ) + .unwrap(); + socket + } + + fn read(mut socket: TcpStream) -> Vec { + let mut bytes = Vec::new(); + socket.read_to_end(&mut bytes).unwrap(); + bytes + } + + fn event<'a>(events: &'a Value, role: &str, op: &str) -> &'a Value { + events + .as_array() + .unwrap() + .iter() + .find(|event| event["role"] == role && event["op"] == op) + .unwrap() + } + + #[test] + fn registration_and_start_reject_unknown_duplicate_and_unbounded_inputs() { + let shared = Shared::default(); + for token in [ + "", + "with/slash", + "with?query", + "with space", + &"x".repeat(65), + ] { + assert!(register(&shared, token, "begin-progress").is_err()); + } + assert!(register(&shared, "case", "unknown").is_err()); + assert!(mark_start(&shared, "unknown").is_err()); + assert!(take_events(&shared, "unknown").is_err()); + register(&shared, "case", "begin-progress").unwrap(); + assert!(register(&shared, "case", "begin-progress").is_err()); + assert!(take_events(&shared, "case").is_err()); + mark_start(&shared, "case").unwrap(); + assert!(mark_start(&shared, "case").is_err()); + assert_eq!(take_events(&shared, "case").unwrap(), json!([])); + for index in 1..CONNECTION_LIMIT { + register(&shared, &format!("case-{index}"), "batch-progress").unwrap(); + } + assert!(register(&shared, "overflow", "batch-progress").is_err()); + shared.lock().stop = true; + assert!(register(&shared, "stopped", "begin-progress").is_err()); + assert!(mark_start(&shared, "case-1").is_err()); + } + + #[test] + fn begin_schedule_uses_receipt_and_only_requests_have_guest_clock() { + let origins = WireOrigins::start().unwrap(); + origins + .register_lifetime("begin", "begin-progress") + .unwrap(); + origins.mark_lifetime_start("begin").unwrap(); + let reply = read(connect( + &origins, + "begin", + "begin-progress", + "origin", + "2000", + )); + assert!(reply.ends_with(b"1\r\nO\r\n0\r\n\r\n")); + let events = origins.take_lifetime_events("begin").unwrap(); + assert_eq!(events.as_array().unwrap().len(), 4); + let request = event(&events, "origin", "request"); + assert_eq!(request["guest_at_us"], 2000); + assert!(request.get("alignment").is_none()); + let head = event(&events, "origin", "head"); + let write_started = head["write_started_us"].as_u64().unwrap(); + assert!(write_started >= head["scheduled_us"].as_u64().unwrap()); + assert!(write_started <= head["at_us"].as_u64().unwrap()); + assert_eq!( + head["scheduled_us"].as_u64().unwrap(), + request["at_us"].as_u64().unwrap() + 150_000 + ); + assert!(head["at_us"].as_u64().unwrap() >= head["scheduled_us"].as_u64().unwrap()); + for entry in events.as_array().unwrap() { + assert_eq!(entry["token"], "begin"); + assert_eq!(entry["case"], "begin-progress"); + assert_eq!(entry.get("guest_at_us").is_some(), entry["op"] == "request"); + } + assert_eq!(origins.take_lifetime_events("begin").unwrap(), json!([])); + assert!(origins.take_requests().is_empty()); + origins.finish().unwrap(); + } + + #[test] + fn slow_schedule_keeps_host_anchor_and_explicitly_derives_scenario_anchor() { + let origins = WireOrigins::start().unwrap(); + origins.register_lifetime("slow", "slow-origin").unwrap(); + origins.mark_lifetime_start("slow").unwrap(); + thread::sleep(Duration::from_millis(30)); + let origin = connect(&origins, "slow", "slow-origin", "origin", "2000"); + let bids = + ["a", "b", "c"].map(|role| connect(&origins, "slow", "slow-origin", role, "34000")); + let reply = read(origin); + assert!(reply.ends_with(b"1\r\nO\r\n1\r\no\r\n1\r\n!\r\n0\r\n\r\n")); + for (index, bid) in bids.into_iter().enumerate() { + assert_eq!(*read(bid).last().unwrap(), b'A' + index as u8); + } + let events = origins.take_lifetime_events("slow").unwrap(); + let request = event(&events, "origin", "request"); + let alignment = &request["alignment"]; + assert_eq!(alignment["basis"], "origin-receipt-minus-guest-at-us"); + let offset = alignment["scenario_anchor_host_offset_us"] + .as_i64() + .unwrap(); + assert!(offset >= 28_000); + assert_eq!(offset, request["at_us"].as_i64().unwrap() - 2000); + assert_eq!(alignment["clock_sync_proven"], false); + assert!(alignment["transit_uncertainty_us"].is_null()); + let span = alignment["boundary_span_us"].as_i64().unwrap(); + let boundary_delta = alignment["boundary_end_us"].as_i64().unwrap() + - alignment["boundary_start_us"].as_i64().unwrap(); + // Offsets and the duration are independently truncated to whole microseconds. + assert!((span..=span + 1).contains(&boundary_delta)); + for (role, expected) in [ + ("origin", 700_000), + ("a", 180_000), + ("b", 320_000), + ("c", 900_000), + ] { + let head = event(&events, role, "head"); + assert_eq!(head["scenario_scheduled_us"], expected); + assert_eq!(head["scheduled_us"].as_i64().unwrap(), offset + expected); + assert!(head["at_us"].as_i64().unwrap() >= offset + expected); + } + let chunks: Vec<_> = events + .as_array() + .unwrap() + .iter() + .filter(|entry| entry["role"] == "origin" && entry["op"] == "chunk") + .map(|entry| entry["scenario_scheduled_us"].clone()) + .collect(); + assert_eq!(json!(chunks), json!([700_000, 900_000, 1_300_000])); + origins.finish().unwrap(); + } + + #[test] + fn fast_origin_and_silent_mediator_record_actual_completion() { + let origins = WireOrigins::start().unwrap(); + origins.register_lifetime("fast", "fast-origin").unwrap(); + origins.mark_lifetime_start("fast").unwrap(); + let origin = connect(&origins, "fast", "fast-origin", "origin", "2000"); + let bids = + ["a", "b", "c"].map(|role| connect(&origins, "fast", "fast-origin", role, "34000")); + let mediator = connect(&origins, "fast", "fast-origin", "mediator", "400000"); + assert!(read(origin).ends_with(b"1\r\nO\r\n0\r\n\r\n")); + for (index, bid) in bids.into_iter().enumerate() { + assert_eq!(*read(bid).last().unwrap(), b'A' + index as u8); + } + assert!(read(mediator).is_empty()); + let events = origins.take_lifetime_events("fast").unwrap(); + for (role, expected) in [ + ("origin", 120_000), + ("a", 180_000), + ("b", 320_000), + ("c", 400_000), + ] { + assert_eq!( + event(&events, role, "head")["scenario_scheduled_us"], + expected + ); + } + assert_eq!( + event(&events, "origin", "chunk")["scenario_scheduled_us"], + 260_000 + ); + assert_eq!(event(&events, "mediator", "closed")["reason"], "silent"); + assert_eq!( + event(&events, "mediator", "closed")["scenario_scheduled_us"], + 1_500_000 + ); + assert!( + !events + .as_array() + .unwrap() + .iter() + .any(|entry| entry["role"] == "mediator" + && matches!(entry["op"].as_str(), Some("head" | "chunk"))) + ); + origins.finish().unwrap(); + } + + #[test] + fn batch_and_quiet_schedules_are_relative_to_each_actual_receipt() { + let origins = WireOrigins::start().unwrap(); + origins + .register_lifetime("batch", "batch-progress") + .unwrap(); + origins.mark_lifetime_start("batch").unwrap(); + let sockets = ["a", "b", "c", "quiet"] + .map(|role| connect(&origins, "batch", "batch-progress", role, "34000")); + for socket in sockets { + read(socket); + } + let events = origins.take_lifetime_events("batch").unwrap(); + for (role, delay) in [ + ("a", 20_000), + ("b", 40_000), + ("c", 60_000), + ("quiet", 300_000), + ] { + let request = event(&events, role, "request"); + let head = event(&events, role, "head"); + let write_started = head["write_started_us"].as_u64().unwrap(); + assert!(write_started >= head["scheduled_us"].as_u64().unwrap()); + assert!(write_started <= head["at_us"].as_u64().unwrap()); + assert_eq!( + head["scheduled_us"].as_u64().unwrap(), + request["at_us"].as_u64().unwrap() + delay + ); + assert!(head.get("scenario_scheduled_us").is_none()); + assert!(request.get("alignment").is_none()); + } + origins.finish().unwrap(); + } + + #[test] + fn invalid_routes_clocks_and_duplicate_roles_remain_cleanup_failures() { + for (case, role, clock, expected) in [ + ("wrong", "origin", "2", "lifetime case mismatch"), + ("begin-progress", "a", "2", "unknown lifetime role"), + ( + "begin-progress", + "origin/extra", + "2", + "invalid lifetime route", + ), + ( + "begin-progress", + "origin", + "", + "invalid lifetime guest clock", + ), + ( + "begin-progress", + "origin", + "-1", + "invalid lifetime guest clock", + ), + ( + "begin-progress", + "origin", + "18446744073709551616", + "invalid lifetime guest clock", + ), + ( + "begin-progress", + "origin", + "1\r\nx-fixture-at-us: 2", + "invalid lifetime guest clock", + ), + ] { + let origins = WireOrigins::start().unwrap(); + origins + .register_lifetime("invalid", "begin-progress") + .unwrap(); + origins.mark_lifetime_start("invalid").unwrap(); + assert!(read(connect(&origins, "invalid", case, role, clock)).is_empty()); + assert!(origins.finish().unwrap_err().to_string().contains(expected)); + } + let origins = WireOrigins::start().unwrap(); + origins + .register_lifetime("duplicate", "begin-progress") + .unwrap(); + origins.mark_lifetime_start("duplicate").unwrap(); + read(connect( + &origins, + "duplicate", + "begin-progress", + "origin", + "2", + )); + origins.take_lifetime_events("duplicate").unwrap(); + assert!( + read(connect( + &origins, + "duplicate", + "begin-progress", + "origin", + "2" + )) + .is_empty() + ); + assert!( + origins + .finish() + .unwrap_err() + .to_string() + .contains("duplicate lifetime role") + ); + } + + #[test] + fn undrained_lifetime_events_fail_finish() { + let origins = WireOrigins::start().unwrap(); + origins + .register_lifetime("undrained", "begin-progress") + .unwrap(); + origins.mark_lifetime_start("undrained").unwrap(); + read(connect( + &origins, + "undrained", + "begin-progress", + "origin", + "2", + )); + assert!( + origins + .finish() + .unwrap_err() + .to_string() + .contains("undrained lifetime events") + ); + } + + #[test] + fn shutdown_interrupts_schedules_and_retains_late_closed_events() { + let origins = WireOrigins::start().unwrap(); + origins.register_lifetime("stopped", "slow-origin").unwrap(); + origins.mark_lifetime_start("stopped").unwrap(); + let origin = connect(&origins, "stopped", "slow-origin", "origin", "2000"); + let shared = Arc::clone(&origins.shared); + shared + .wait(IO_BUDGET, |state| { + !state.lifetimes["stopped"].events.is_empty() + }) + .unwrap(); + let start = Instant::now(); + assert!( + origins + .finish() + .unwrap_err() + .to_string() + .contains("undrained lifetime events") + ); + assert!(start.elapsed() < Duration::from_secs(1)); + assert!(read(origin).is_empty()); + let state = shared.lock(); + assert_eq!(state.active, 0); + assert!(state.workers.is_empty()); + assert!( + state.lifetimes["stopped"] + .events + .iter() + .any(|entry| entry["op"] == "closed" && entry["reason"] == "stopped") + ); + } +} diff --git a/crates/edgezero-test-harness/src/runners.rs b/crates/edgezero-test-harness/src/runners.rs new file mode 100644 index 000000000..3103cc8d9 --- /dev/null +++ b/crates/edgezero-test-harness/src/runners.rs @@ -0,0 +1,2206 @@ +use crate::{evidence::*, net::Backend, *}; +use std::collections::{BTreeSet, HashMap, HashSet}; +mod assembly; +mod framing; +mod lifetimes; +mod overflow; +mod progress; +mod readiness; +mod response_headers; +mod wire; +fn cargo_build() -> Command { + let mut command = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())); + command + .current_dir(root()) + .env("CARGO_TARGET_DIR", root().join("target")); + command +} + +fn built_executable(output: &[u8], name: &str) -> Result { + String::from_utf8_lossy(output) + .lines() + .filter_map(|line| serde_json::from_str::(line).ok()) + .find_map(|record| { + (record["reason"] == "compiler-artifact" && record["target"]["name"] == name) + .then(|| record["executable"].as_str().map(PathBuf::from)) + .flatten() + }) + .ok_or_else(|| format!("Cargo did not report an executable for {name}").into()) +} +/// Copies a built artifact (file or directory) into the run directory, so a +/// concurrent build into the shared target cannot swap it under a running +/// server; the copy is what gets served and fingerprinted. +fn stage(from: &Path, run: &Path) -> Result { + let to = run + .join("artifacts") + .join(from.file_name().ok_or("artifact has no file name")?); + copy_tree(from, &to)?; + Ok(to) +} +fn copy_tree(from: &Path, to: &Path) -> Result<()> { + if from.is_dir() { + fs::create_dir_all(to)?; + for entry in fs::read_dir(from)? { + let entry = entry?; + copy_tree(&entry.path(), &to.join(entry.file_name()))?; + } + } else { + fs::create_dir_all(to.parent().ok_or("artifact has no parent")?)?; + fs::copy(from, to)?; + } + Ok(()) +} +/// Teardown shared by every runtime run. The runtime lives inside `outcome`'s +/// computation, so it has stopped by now: record client completions and guest +/// logs, then a failure carrying `fields` and `failure` context. +fn settle( + records: &mut Records, + log: &Path, + fields: Value, + failure: Value, + outcome: Result<()>, +) -> Result<()> { + records.collect_clients(fields.clone())?; + for event in logs(log) { + records.push(annotate(event, fields.clone()))?; + } + if let Err(error) = &outcome { + records.push(annotate( + annotate(json!({"status":"fail","error":error.to_string()}), fields), + failure, + ))?; + } + outcome +} +fn wrangler_runtime_config(package: &Path, main: &Path) -> Result { + let config = fs::read_to_string(package.join("wrangler.toml"))?; + let mut skip = false; + let mut lines = Vec::new(); + for line in config.lines() { + if line.trim().starts_with('[') { + skip = line.trim() == "[build]"; + } + if skip { + continue; + } + lines.push(if line.starts_with("main =") { + format!("main = {main:?}") + } else { + line.to_owned() + }); + } + Ok(lines.join("\n") + "\n") +} + +fn wasm(run: &Path, variant: &str) -> PathBuf { + run.join("artifacts").join(wasm_name(variant)) +} +fn wasm_name(variant: &str) -> String { + let name = match variant { + "a" => "single-request", + "b" => "rebuild-per-request", + "c" => "retained-app", + "custom-a" => "custom-single-request", + "custom-b" => "custom-rebuild-per-request", + "custom-c" => "custom-retained-app", + other => other, + }; + format!("fixture-fastly-{name}.wasm") +} +fn stage_fastly_artifacts(run: &Path) -> Result<()> { + let release = root().join("target/wasm32-wasip1/release"); + for variant in [ + "a", + "b", + "c", + "custom-a", + "custom-b", + "custom-c", + "logger-negative", + "limit-requests", + "limit-lifetime", + "limit-memory", + "faults", + ] { + stage(&release.join(wasm_name(variant)), run)?; + } + Ok(()) +} +fn fastly_command(exe: &Path, config: &Path, run: &Path, variant: &str, port: u16) -> Command { + let mut cmd = Command::new(exe); + cmd.args(["serve", "--addr", &format!("127.0.0.1:{port}"), "--config"]) + .arg(config) + .arg(wasm(run, variant)); + cmd +} + +fn custom_initialization_contract( + exe: &Path, + config: &Path, + run: &Path, + records: &mut Records, +) -> Result<()> { + let log = run.join("custom-initialization.log"); + let outcome = (|| -> Result<()> { + let port = net::port()?; + let _process = Process::start( + &mut fastly_command(exe, config, run, "custom-c", port), + &log, + port, + )?; + let mut instances = Vec::new(); + for (path, status, attempts, initialized) in [ + ("/health", 200, 0, false), + ("/initialization-error", 503, 1, false), + ("/health", 200, 1, false), + ("/probe/recovered", 200, 2, true), + ("/probe/retained", 200, 2, true), + ] { + let reply = req(port, path)?; + require( + reply["status"] == status, + "custom initialization status mismatch", + )?; + let parsed: Value = + serde_json::from_str(reply["body"].as_str().ok_or("missing body")?)?; + let value = if status == 503 { + require( + parsed + == json!({"error":{"kind":"service_unavailable","message":"state unavailable","status":503}}), + "initialization did not use the canonical error body", + )?; + await_evidence(|| { + require( + logs(&log) + .iter() + .filter(|event| { + event["event"] == "assembly-rejected" + && event["token"] == reply["token"] + }) + .count() + == 1, + "missing correlated assembly failure", + ) + })?; + logs(&log) + .into_iter() + .find(|event| { + event["event"] == "assembly-rejected" && event["token"] == reply["token"] + }) + .ok_or("missing assembly failure metadata")? + } else { + parsed + }; + instances.push(value["instance"].clone()); + let same_instance = instances.iter().all(|id| id == &instances[0]); + if same_instance { + require( + value["ordinal"] == instances.len(), + "recovery ordinal mismatch", + )?; + if initialized { + require(value["builds"] == 1, "recovered application was rebuilt")?; + } else { + require( + value["retained"] == false, + "health or failure retained an app", + )?; + } + if status == 503 { + require( + value["attempts"] == attempts, + "failed assembly attempt count mismatch", + )?; + require( + !reply["headers"].as_array().unwrap().iter().any(|h| { + h[0].as_str() + .is_some_and(|name| name.eq_ignore_ascii_case("x-fixture-attempts")) + }), + "failed assembly used application headers", + )?; + } else { + require( + reply["headers"].as_array().unwrap().iter().any(|h| { + h[0].as_str() + .is_some_and(|name| name.eq_ignore_ascii_case("x-fixture-attempts")) + && h[1].as_str().and_then(|v| v.parse::().ok()) + == Some(attempts) + }), + "lazy build attempt count mismatch", + )?; + } + } + records.push(annotate( + reply, + json!({"variant":"custom-initialization", "guest":value}), + ))?; + } + records.push(json!({"assertion":"custom_lazy_initialization_recovery", "status":if instances.iter().all(|id| id == &instances[0]) {"pass"} else {"unverified"}, "reason":"recovery requires all five callbacks in the same guest"})) + })(); + settle( + records, + &log, + json!({"variant":"custom-initialization","repetition":0}), + json!({"adapter":"fastly"}), + outcome, + ) +} + +pub fn fastly(args: &Args, run: &Path, records: &mut Records) -> Result<()> { + if args.suite == "outbound-lifetimes" { + return lifetimes::run("fastly", run, records); + } + let Some(exe) = executable("VICEROY_BIN", "viceroy") else { + return records.push(json!({"status":"unsupported","reason":"Viceroy unavailable"})); + }; + let version = Command::new(&exe).arg("--version").output()?; + require(version.status.success(), "Viceroy version command failed")?; + let version = String::from_utf8(version.stdout)?.trim().to_owned(); + let pins = fs::read_to_string(root().join("../../../.tool-versions"))?; + let pin = pins + .lines() + .find_map(|line| line.strip_prefix("viceroy ")) + .map(str::trim) + .ok_or("missing Viceroy pin")?; + records.push(json!({"event":"runtime","adapter":"fastly","executable":exe,"version":version,"expected":pin}))?; + if version.split_whitespace().nth(1) != Some(pin) { + // Keep going: an unpinned Viceroy still gets signal, and a failure still exits 1. + records.push(json!({"status":"unverified","adapter":"fastly","reason":format!("Viceroy version {version} does not match pinned {pin}")}))?; + } + if args.suite == "outbound-wire" { + return wire::run("fastly", run, records); + } + if args.suite == "response-framing" { + return framing::run("fastly", run, records); + } + if args.suite == "synthesized-headers" { + return response_headers::run("fastly", run, records); + } + if args.suite == "outbound-overflow" { + return overflow::run("fastly", run, records); + } + if args.suite == "assembly" { + return assembly::fastly(&exe, run, records); + } + if args.suite == "outbound-progress" { + return progress::run("fastly", run, records); + } + if let Some(suite) = readiness::ReadinessSuite::parse(&args.suite) { + require( + version.split_whitespace().nth(1) == Some(pin), + "readiness proof requires the pinned runtime", + )?; + return readiness::run(&exe, run, records, suite); + } + checked( + cargo_build() + .args([ + "build", + "--locked", + "--release", + "-p", + "fixture-fastly", + "--bins", + "--target", + "wasm32-wasip1", + ]) + .env("FIXTURE_BUILD_ROUNDS", args.construction_rounds.to_string()) + .env("FIXTURE_MAX_REQUESTS", args.max_requests.to_string()), + )?; + stage_fastly_artifacts(run)?; + let config_snapshot = run.join("fastly.toml"); + fs::copy( + root().join("crates/fixture-fastly/fastly.toml"), + &config_snapshot, + )?; + if args.suite == "smoke" { + custom_initialization_contract(&exe, &config_snapshot, run, records)?; + } + let mut variants = vec!["a", "b", "c", "custom-a", "custom-b", "custom-c"]; + if args.suite == "smoke" { + variants.extend(["limit-requests", "limit-lifetime", "limit-memory"]); + } + let mut rng = args.seed; + for repetition in 0..if args.suite == "benchmark" { + args.repetitions + } else { + 1 + } { + // A recorded deterministic Fisher-Yates order; independent of language RNG versions. + for i in (1..variants.len()).rev() { + rng = rng + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + variants.swap(i, (rng as usize) % (i + 1)); + } + records + .push(json!({"event":"variant_order","repetition":repetition,"variants":variants}))?; + for variant in &variants { + fastly_variant( + args, + &exe, + &config_snapshot, + run, + variant, + repetition, + records, + )?; + } + } + if args.suite == "smoke" { + logging(&exe, run, records)?; + faults(&exe, run, records)?; + progress::run("fastly", run, records)?; + } + if records + .values + .iter() + .any(|e| e["event"] == "heap_preflight" && e["heap_mib"].is_null()) + { + records.push(json!({"status":"unsupported","reason":"heap snapshot unavailable; memory limit unverified"}))?; + } + Ok(()) +} + +fn fastly_variant( + args: &Args, + exe: &Path, + config: &Path, + run: &Path, + variant: &str, + repetition: usize, + records: &mut Records, +) -> Result<()> { + records.push(json!({"event":"build","variant":variant,"repetition":repetition,"target":"wasm32-wasip1","artifact":artifact(&wasm(run, variant))?,"config":artifact(config)?}))?; + let log = run.join(format!("{variant}-{repetition}.log")); + let mut declared = DeclaredKvChecks::default(); + let outcome = (|| -> Result<()> { + let backend = Backend::start()?; + let port = net::port()?; + let _process = Process::start( + &mut fastly_command(exe, config, run, variant, port), + &log, + port, + )?; + let seen = fastly_probes(args, port, variant, repetition, records)?; + if ["c", "custom-c"].contains(&variant) { + thread::sleep(Duration::from_secs(2)); + let reply = req(port, "/probe/after-idle")?; + require( + !seen.contains_key( + guest(&reply)?["instance"] + .as_str() + .ok_or("missing instance")?, + ), + "idle guest was reused", + )?; + records.push(annotate( + reply, + json!({"variant":variant,"repetition":repetition,"assertion":"idle_fresh_initialization"}), + ))?; + } + if args.suite == "smoke" && variant == "c" { + declared_kv_checks("fastly", port, &log, records, &mut declared)?; + } + let bindings = req(port, "/bindings")?; + require( + guest(&bindings)? == json!({"config":true,"kv":true,"secrets":true,"unknown":false}), + "binding mismatch", + )?; + records.push(annotate( + bindings, + json!({"variant":variant,"repetition":repetition,"assertion":"binding_reads"}), + ))?; + require( + cookie_values(&req(port, "/cookies")?["headers"]) + == vec!["first=1; Path=/", "second=2; Path=/"], + "duplicate cookies lost", + )?; + require( + req(port, "/rendered-error")?["status"] == 400, + "rendered error mismatch", + )?; + require( + req(port, "/probe/after")?["status"] == 200, + "request after error failed", + )?; + if variant.starts_with("custom-") { + custom_stream_checks(port, &backend, &log, variant, repetition, records)?; + } + if args.suite == "smoke" { + smoke_origin_checks(port, &backend, &log, variant, repetition, records)?; + } + if variant.starts_with("limit-") { + await_evidence(|| validate_limit_summaries(&logs(&log)))?; + } + records.push(json!({"status":"pass","variant":variant,"repetition":repetition,"assertions":["initialization","isolation","cookies","rendered_error"]})) + })(); + let teardown = if args.suite == "smoke" && variant == "c" { + finish_declared_kv_checks("fastly", &log, records, &declared) + } else { + Ok(()) + }; + settle( + records, + &log, + json!({"variant":variant,"repetition":repetition}), + json!({"adapter":"fastly"}), + outcome.and(teardown), + ) +} + +const DECLARED_KV_FAILURE_CASES: [(&str, u16, usize); 7] = [ + ("/declared-kv/free", 200, 0), + ("/declared-kv/missing/read", 500, 1), + ("/declared-kv/missing/read", 500, 1), + ("/declared-kv/free", 200, 1), + ("/declared-kv/missing/write", 500, 1), + ("/declared-kv/missing/delete", 500, 1), + ("/declared-kv/missing/list", 500, 1), +]; +const DECLARED_KV_LINKED_OPERATIONS: [&str; 6] = + ["write", "read", "list", "delete", "read", "list"]; + +struct DeclaredKvRequest { + token: String, + path: String, + instance: Option, +} + +#[derive(Default)] +struct DeclaredKvChecks { + instance: Option, + requests: Vec, +} + +impl DeclaredKvChecks { + fn failed_instances(&self) -> BTreeSet<&str> { + self.requests + .iter() + .filter(|request| request.path.starts_with("/declared-kv/missing/")) + .filter_map(|request| request.instance.as_deref()) + .collect() + } + + fn warning_count(&self, text: &str, adapter: &str) -> Result { + let failed = self.failed_instances(); + let count = if adapter == "fastly" { + declared_kv_warning_count(text) + } else { + let instances: BTreeSet<_> = self + .requests + .iter() + .filter_map(|request| request.instance.as_deref()) + .collect(); + let mut count = 0; + for instance in instances { + let observed = provider_kv_warning_count(text, adapter, instance); + require( + observed == usize::from(failed.contains(instance)), + "declared KV warning count mismatch within an instance", + )?; + count += observed; + } + count + }; + require(count == failed.len(), "declared KV warning count mismatch")?; + Ok(count) + } + + fn validate(&self, events: &[Value], adapter: &str) -> Result<()> { + require( + self.requests.len() + == DECLARED_KV_FAILURE_CASES.len() + DECLARED_KV_LINKED_OPERATIONS.len(), + "declared KV matrix did not execute every request", + )?; + let prefix_instance = self + .instance + .as_deref() + .ok_or("missing declared KV instance")?; + let mut tokens = HashSet::new(); + for (index, request) in self.requests.iter().enumerate() { + require( + tokens.insert(request.token.as_str()), + "declared KV request token reused", + )?; + let expected = request + .instance + .as_deref() + .ok_or("missing declared KV request instance")?; + if let Some((path, _, _)) = DECLARED_KV_FAILURE_CASES.get(index) { + require( + request.path == *path && (index >= 4 || expected == prefix_instance), + "declared KV failure prefix changed or turned over", + )?; + } else { + let operation = + DECLARED_KV_LINKED_OPERATIONS[index - DECLARED_KV_FAILURE_CASES.len()]; + require( + request.path == format!("/declared-kv/linked/{operation}"), + "declared KV linked sequence changed", + )?; + } + validate_declared_kv_lifecycle( + events, + &request.token, + Some(expected), + if adapter == "fastly" { + "Completed" + } else { + "HostHandoff" + }, + )?; + validate_declared_kv_operation( + events, + &request.token, + expected, + adapter, + &request.path, + )?; + } + Ok(()) + } +} + +fn validate_declared_kv_operation( + events: &[Value], + token: &str, + instance: &str, + adapter: &str, + path: &str, +) -> Result<()> { + let matching: Vec<_> = events + .iter() + .filter(|event| { + event["token"] == token + && matches!( + event["event"].as_str(), + Some( + "request_start" + | "declared_kv_enter" + | "declared_kv_result" + | "egress_terminal" + ) + ) + }) + .collect(); + if path == "/declared-kv/free" { + return require( + matching.len() == 2 + && matching[0]["event"] == "request_start" + && matching[1]["event"] == "egress_terminal", + "KV-free route invoked an operation handler or reordered its lifecycle", + ); + } + let (store, operation) = path + .strip_prefix("/declared-kv/") + .and_then(|path| path.split_once('/')) + .ok_or("invalid declared KV fixture path")?; + require( + matching.len() == 4 + && matching[0]["event"] == "request_start" + && matching[1]["event"] == "declared_kv_enter" + && matching[2]["event"] == "declared_kv_result" + && matching[3]["event"] == "egress_terminal", + "declared KV handler/result receipt missing, repeated or reordered", + )?; + require( + matching.iter().all(|event| event["instance"] == instance) + && matching[1..3] + .iter() + .all(|event| event["store"] == store && event["operation"] == operation), + "declared KV operation receipt mismatched its request", + )?; + let (outcome, reason) = if store == "missing" { + ( + "open-failed", + json!(if adapter == "spin" { + "InvalidConfiguration" + } else { + "MissingBinding" + }), + ) + } else { + ("success", json!(null)) + }; + require( + matching[2]["outcome"] == outcome && matching[2]["reason"] == reason, + "declared KV operation lost its typed outcome", + ) +} + +fn finish_declared_kv_checks( + adapter: &str, + log: &Path, + records: &mut Records, + checks: &DeclaredKvChecks, +) -> Result<()> { + let text = fs::read_to_string(log)?; + let warnings = checks.warning_count(&text, adapter); + let count = warnings.as_ref().ok().copied(); + let result = checks + .validate(&logs(log), adapter) + .and(warnings.map(|_count| ())); + records.push(json!({"event":"declared-kv-after-shutdown","adapter":adapter,"instance":checks.instance,"failed_instances":checks.failed_instances(),"requests":checks.requests.len(),"warnings":count,"status":if result.is_ok() {"pass"} else {"fail"},"error":result.as_ref().err().map(ToString::to_string),"qualification":false}))?; + result +} + +fn declared_kv_warning_count(text: &str) -> usize { + text.lines() + .filter(|line| { + line.starts_with("fixture-logs :: ") + && line.contains( + "declared Fastly KV store 'missing_fixture_kv' failed to open: missing binding", + ) + }) + .count() +} + +fn validate_declared_kv_lifecycle( + events: &[Value], + token: &str, + expected_instance: Option<&str>, + expected_outcome: &str, +) -> Result { + let starts: Vec<_> = events + .iter() + .filter(|event| event["event"] == "request_start" && event["token"] == token) + .collect(); + let ends: Vec<_> = events + .iter() + .filter(|event| event["event"] == "egress_terminal" && event["token"] == token) + .collect(); + require( + starts.len() == 1 && ends.len() == 1, + "declared KV request lost exactly-once lifecycle evidence", + )?; + let current = starts[0]["instance"] + .as_str() + .filter(|value| !value.is_empty()) + .ok_or("missing declared KV instance")?; + require( + expected_instance.is_none_or(|expected| expected == current), + "declared KV sandbox turnover: deduplication unverified", + )?; + require( + ends[0]["instance"] == starts[0]["instance"] + && ends[0]["outcome"] == expected_outcome + && ends[0]["body_kind"] == "Application" + && ends[0].get("fallback") == Some(&Value::Null), + "declared KV terminal disposition mismatch", + )?; + Ok(current.to_owned()) +} + +fn provider_kv_warning_count(text: &str, adapter: &str, instance: &str) -> usize { + let (platform, reason) = if adapter == "spin" { + ("Spin", "invalid binding configuration") + } else { + ("Cloudflare", "missing binding") + }; + let message = + format!("declared {platform} KV store 'missing_fixture_kv' failed to open: {reason}"); + text.lines() + .filter_map(|line| serde_json::from_str::(line).ok()) + .filter(|event| { + event["event"] == "fixture_log" + && event["instance"] == instance + && event["level"] == "WARN" + && event["target"] == "edgezero::boot" + && event["message"] == message + }) + .count() +} + +fn validate_declared_kv_linked_response(response: &Value, expected: &Value) -> Result<()> { + require( + guest(response)?.get("kv_outcome") == Some(expected), + "linked KV operation failed after declared failure", + ) +} + +fn validate_declared_kv_guest(response: &Value, token: &str, instance: &str) -> Result<()> { + let body = guest(response)?; + require( + body.get("token").and_then(Value::as_str) == Some(token) + && body.get("instance").and_then(Value::as_str) == Some(instance), + "declared KV success body does not match its request lifecycle", + ) +} + +fn declared_kv_checks( + adapter: &str, + port: u16, + log: &Path, + records: &mut Records, + checks: &mut DeclaredKvChecks, +) -> Result<()> { + for (index, (path, status, warnings)) in DECLARED_KV_FAILURE_CASES.into_iter().enumerate() { + let token = token(); + checks.requests.push(DeclaredKvRequest { + token: token.clone(), + path: path.to_owned(), + instance: None, + }); + let response = net::request(port, path, &token, None, None)?; + require(response["status"] == status, "declared KV status mismatch")?; + if status == 200 { + let value = guest(&response)?; + require( + value["declared_kv"] == true + && value["default_kv"] == true + && value["unknown_kv"] == false + && value["builds"] == 1, + "declared KV registry or retained application mismatch", + )?; + } else { + let body = response["body"] + .as_str() + .ok_or("missing declared KV error body")?; + let public: Value = serde_json::from_str(body)?; + require( + public + == json!({"error":{"kind":"internal","message":"internal server error","status":500}}), + "declared KV error gained provider diagnostics", + )?; + } + let mut current = None; + await_evidence(|| { + current = Some(validate_declared_kv_lifecycle( + &logs(log), + &token, + if index < 4 { + checks.instance.as_deref() + } else { + None + }, + if adapter == "fastly" { + "Completed" + } else { + "HostHandoff" + }, + )?); + validate_declared_kv_operation( + &logs(log), + &token, + current.as_deref().ok_or("missing declared KV instance")?, + adapter, + path, + )?; + Ok(()) + })?; + checks + .requests + .last_mut() + .ok_or("missing declared KV request")? + .instance = current.clone(); + if index < 4 { + checks.instance = current.clone(); + } + if status == 200 { + validate_declared_kv_guest( + &response, + &token, + current.as_deref().ok_or("missing declared KV instance")?, + )?; + } + let text = fs::read_to_string(log)?; + let count = checks.warning_count(&text, adapter)?; + require( + index >= 4 || count == warnings, + "declared KV prefix warning count mismatch", + )?; + records.push(json!({"event":"declared-kv-case","adapter":adapter,"path":path,"token":token,"instance":current, + "client":response,"warnings":count,"status":"pass","qualification":false}))?; + } + for (operation, expected) in [ + ("write", json!("written")), + ("read", json!("f8-value")), + ("list", json!(["f8-key"])), + ("delete", json!("deleted")), + ("read", json!(null)), + ("list", json!([])), + ] { + let path = format!("/declared-kv/linked/{operation}"); + let token = token(); + checks.requests.push(DeclaredKvRequest { + token: token.clone(), + path: path.clone(), + instance: None, + }); + let response = net::request(port, &path, &token, None, None)?; + validate_declared_kv_linked_response(&response, &expected)?; + let mut linked_instance = None; + await_evidence(|| { + linked_instance = Some(validate_declared_kv_lifecycle( + &logs(log), + &token, + None, + if adapter == "fastly" { + "Completed" + } else { + "HostHandoff" + }, + )?); + validate_declared_kv_operation( + &logs(log), + &token, + linked_instance + .as_deref() + .ok_or("missing linked KV instance")?, + adapter, + &path, + )?; + Ok(()) + })?; + checks + .requests + .last_mut() + .ok_or("missing linked KV request")? + .instance = linked_instance.clone(); + validate_declared_kv_guest( + &response, + &token, + linked_instance + .as_deref() + .ok_or("missing linked KV instance")?, + )?; + records.push(json!({"event":"declared-kv-linked","adapter":adapter,"operation":operation,"instance":linked_instance,"client":response,"status":"pass","qualification":false}))?; + } + checks.warning_count(&fs::read_to_string(log)?, adapter)?; + checks.validate(&logs(log), adapter) +} +fn fastly_probes( + args: &Args, + port: u16, + variant: &str, + repetition: usize, + records: &mut Records, +) -> Result> { + let mut seen: HashMap = HashMap::new(); + let mut reused = false; + for _ in 0..if args.suite == "benchmark" { + args.requests + } else { + 5 + } { + let token = token(); + let reply = net::request(port, &format!("/probe/{token}"), &token, None, None)?; + let value = guest(&reply)?; + require( + value["token"] == token && value["path"] == token && value["body"] == token, + format!("request isolation: {value}"), + )?; + if let Some(previous) = seen.get(value["instance"].as_str().ok_or("missing instance")?) { + reused = true; + require( + value["ordinal"].as_u64() > previous["ordinal"].as_u64() + && value["correlation"] != previous["correlation"], + "reuse ordinal/correlation mismatch", + )?; + } + require( + value["mutated"] == variant.starts_with("custom-"), + "raw request mutation mismatch", + )?; + if variant.starts_with("custom-") { + require( + reply["headers"].as_array().unwrap().iter().any(|h| { + h[0].as_str() + .is_some_and(|name| name.eq_ignore_ascii_case("x-fixture-finalized")) + && h[1] == token + }), + "request-specific response extension was lost or leaked", + )?; + } + match variant { + "c" | "custom-c" => require( + value["ordinal"] + .as_u64() + .is_some_and(|n| n <= args.max_requests as u64) + && value["builds"] == 1, + "retained app initialization mismatch", + )?, + "b" | "custom-b" => require( + value["builds"] == value["ordinal"], + "per-request initialization mismatch", + )?, + _ => require( + value["ordinal"] == 1 && value["builds"] == 1, + "single-request initialization mismatch", + )?, + } + validate_shared_state(&value, ["c", "custom-c"].contains(&variant))?; + seen.insert(value["instance"].as_str().unwrap().into(), value.clone()); + records.push(annotate( + reply, + json!({"variant":variant,"repetition":repetition,"guest":value,"workload":"probe"}), + ))?; + } + if ["b", "c", "custom-b", "custom-c"].contains(&variant) && !reused { + records.push(json!({"status":"unverified","variant":variant,"repetition":repetition,"reason":"no actual guest reuse observed"}))?; + } + Ok(seen) +} +fn custom_stream_checks( + port: u16, + backend: &Backend, + log: &Path, + variant: &str, + repetition: usize, + records: &mut Records, +) -> Result<()> { + let token = token(); + let stream = net::request( + port, + "/stream", + &token, + Some(&backend.url(&format!("/chunks?token={token}"))), + Some(&backend.release), + )?; + require( + stream["body"] == "firstsecond", + "progressive stream mismatch", + )?; + require( + stream["headers"].as_array().unwrap().iter().any(|h| { + h[0].as_str() + .is_some_and(|s| s.eq_ignore_ascii_case("x-fixture-finalized")) + && h[1] == token + }), + "response not finalized", + )?; + await_evidence(|| validate_post_send(&token, &backend.receipts.lock().unwrap(), &logs(log)))?; + records.push(annotate( + stream, + json!({"variant":variant,"repetition":repetition,"assertion":"progressive_stream_and_post_send"}), + ))?; + *backend.release.0.lock().unwrap() = false; + let large_token = crate::token(); + let large = net::request( + port, + "/stream", + &large_token, + Some(&backend.url("/chunks?large=1")), + Some(&backend.release), + )?; + let large_body = large["body"].as_str().ok_or("missing large body")?; + require( + large_body.starts_with("first") && large_body.len() == 5 + 256 * 1024, + "large stream truncated", + )?; + records.push(annotate( + large, + json!({"variant":variant,"repetition":repetition,"assertion":"large_progressive_stream"}), + ))?; + // Trigger truncation only after the downstream client has observed the partial body. + *backend.release.0.lock().unwrap() = false; + let failure_token = crate::token(); + let failure = net::request( + port, + "/stream-error", + &failure_token, + Some(&backend.url(&format!("/abort?gated=1&token={failure_token}"))), + Some(&backend.release), + ); + let failure = match failure { + Ok(reply) => { + return Err(format!("interrupted stream unexpectedly completed: {reply}").into()); + } + Err(error) => error, + }; + let partial = failure + .downcast_ref::() + .ok_or("stream failure occurred before response body: not post-commit evidence")?; + require( + partial.status == 200 && partial.body == b"partial", + "missing partial committed response", + )?; + await_evidence(|| { + require( + logs(log) + .iter() + .any(|e| e["event"] == "post_commit_error" && e["token"] == failure_token), + "missing correlated post-commit error", + ) + })?; + await_evidence(|| { + validate_post_send( + &failure_token, + &backend.receipts.lock().unwrap(), + &logs(log), + ) + })?; + validate_fastly_delivery_report( + &failure_token, + &logs(log), + &json!({"disposition":"routed", + "commitment":"Committed","body_kind":"Application","outcome":"SourceError", + "fallback":null,"may_send_native":false}), + )?; + records.push(json!({"event":"expected_stream_error","variant":variant,"repetition":repetition,"token":failure_token,"status_code":partial.status,"partial_body":String::from_utf8_lossy(&partial.body),"source":"controlled_backend_abort"}))?; + let fallback_token = crate::token(); + let fallback = net::request( + port, + "/probe/framing-fallback", + &fallback_token, + Some(&backend.url(&format!("/chunks?token={fallback_token}"))), + None, + )?; + require( + fallback["status"] == 500 && fallback["body"] == "internal server error", + "framing fallback wire mismatch", + )?; + await_evidence(|| { + require( + logs(log).iter().any(|event| { + event["event"] == "guest_completed" && event["token"] == fallback_token + }), + "missing fallback callback completion", + ) + })?; + validate_fastly_delivery_report( + &fallback_token, + &logs(log), + &json!({"disposition":"routed", + "commitment":"Committed","body_kind":"Fallback","outcome":"ConversionError", + "fallback":"Completed","may_send_native":false}), + )?; + require( + !backend + .receipts + .lock() + .unwrap() + .contains(&format!("/post-send?token={fallback_token}")), + "fallback incorrectly scheduled post-send work", + )?; + records.push(annotate(fallback, json!({"variant":variant,"repetition":repetition,"token":fallback_token,"assertion":"reported_fallback_skips_post_send"}))) +} +fn smoke_origin_checks( + port: u16, + backend: &Backend, + log: &Path, + variant: &str, + repetition: usize, + records: &mut Records, +) -> Result<()> { + for repeat in 0..3 { + let reply = net::request( + port, + "/origin/repeated", + &token(), + Some(&backend.url("/ok")), + None, + )?; + require( + reply["status"] == 200 && reply["body"] == "ok", + "repeated origin failed", + )?; + records.push(annotate(reply, json!({"variant":variant,"repetition":repetition,"origin_repeat":repeat,"assertion":"bounded_repeated_origin"})))?; + } + for origin in 0..3 { + let distinct = Backend::start()?; + let reply = net::request( + port, + &format!("/origin/{origin}"), + &token(), + Some(&distinct.url("/ok")), + None, + )?; + require( + reply["status"] == 200 && reply["body"] == "ok", + "distinct origin failed", + )?; + records.push(annotate( + reply, + json!({"variant":variant,"repetition":repetition,"assertion":"bounded_distinct_origin"}), + ))?; + } + if ["custom-b", "custom-c"].contains(&variant) { + let token = token(); + if let Ok(failed) = net::request(port, "/panic", &token, None, None) { + require( + failed["status"].as_u64().is_some_and(|s| s >= 500), + "panic did not fail", + )?; + } + let fresh = req(port, "/probe/fresh")?; + let value = guest(&fresh)?; + let mut failed_guest = Value::Null; + await_evidence(|| { + failed_guest = logs(log) + .iter() + .find(|e| e["event"] == "request_start" && e["token"] == token) + .map(|e| e["instance"].clone()) + .ok_or("missing panic attempt")?; + Ok(()) + })?; + require(value["instance"] != failed_guest, "terminated guest reused")?; + records.push(annotate(fresh,json!({"variant":variant,"repetition":repetition,"assertion":"terminated_guest_not_reused","failed_guest":failed_guest})))?; + } + Ok(()) +} +fn logging(exe: &Path, run: &Path, records: &mut Records) -> Result<()> { + let base = fs::read_to_string(root().join("crates/fixture-fastly/fastly.toml"))?; + let config = run.join("logging.toml"); + fs::write(&config, base)?; + for variant in ["b", "c", "custom-c", "logger-negative"] { + let log = run.join(format!("logging-{variant}.log")); + let outcome = (|| -> Result<()> { + let port = net::port()?; + let process = Process::start( + &mut fastly_command(exe, &config, run, variant, port), + &log, + port, + )?; + let mut replies = Vec::new(); + for _ in 0..if variant == "logger-negative" { 2 } else { 3 } { + replies.push(req(port, "/probe/log")?); + } + drop(process); + records.push(json!({"event":"logging_check","variant":variant,"statuses":replies.iter().map(|r|r["status"].clone()).collect::>(),"log_file":log.file_name().map(|n| n.to_string_lossy())}))?; + if variant == "logger-negative" { + let starts = logs(&log) + .into_iter() + .filter(|e| e["event"] == "request_start") + .collect::>(); + let status = validate_negative_logging(&replies, &starts)?; + records.push(json!({"status":status,"assertion":"repeated_logger_installation","variant":variant}))?; + } else { + let guests = replies.iter().map(guest).collect::>>()?; + let text = fs::read_to_string(&log)?; + let delivered = text + .lines() + .filter(|l| l.starts_with("fixture-logs :: ")) + .filter_map(|l| { + l.rsplit_once("fixture request ") + .map(|(_, v)| v.trim().to_owned()) + }) + .collect::>(); + let status = validate_logging(&guests, &delivered)?; + records.push(json!({"status":status,"assertion":"named_endpoint_receipt","variant":variant,"received":delivered.len()}))?; + } + Ok(()) + })(); + settle( + records, + &log, + json!({"variant":format!("logging-{variant}"),"repetition":0}), + json!({"adapter":"fastly"}), + outcome, + )?; + } + Ok(()) +} +pub fn provider(name: &str, args: &Args, run: &Path, records: &mut Records) -> Result<()> { + if args.suite == "outbound-lifetimes" { + return lifetimes::run(name, run, records); + } + if args.suite == "outbound-wire" { + return wire::run(name, run, records); + } + if args.suite == "synthesized-headers" { + return response_headers::run(name, run, records); + } + if args.suite == "response-framing" { + return framing::run(name, run, records); + } + if args.suite == "assembly" { + return assembly::provider(name, run, records); + } + if args.suite == "outbound-overflow" { + return overflow::run(name, run, records); + } + if args.suite == "outbound-progress" { + return progress::run(name, run, records); + } + let package = root().join("crates").join(format!("fixture-{name}")); + let Some((exe, staged)) = provider_build(name, &package, run, records)? else { + return Ok(()); + }; + let modes = if name == "axum" { + vec!["retained"] + } else { + vec!["per-request", "retained"] + }; + for mode in modes { + let port = net::port()?; + let mut command = provider_command(name, mode, &exe, &staged, &package, run, port)?; + let log = run.join(format!("{name}-{mode}.log")); + let mut failure = json!({"phase":"startup"}); + let mut declared = DeclaredKvChecks::default(); + let outcome = Process::start(&mut command, &log, port).and_then(|_process| { + // Establish single-instance dedup before overlap creates additional instances. + if args.suite == "smoke" && mode == "retained" && name != "axum" { + failure = json!({"phase":"declared_kv"}); + declared_kv_checks(name, port, &log, records, &mut declared)?; + } + provider_checks(name, mode, port, args, records, &mut failure)?; + Ok(()) + }); + let teardown = if args.suite == "smoke" && mode == "retained" && name != "axum" { + finish_declared_kv_checks(name, &log, records, &declared) + } else { + Ok(()) + }; + settle( + records, + &log, + json!({"adapter":name,"mode":mode}), + failure, + outcome.and(teardown), + )?; + } + if args.suite == "smoke" { + progress::run(name, run, records)?; + } + Ok(()) +} +/// Builds the provider fixture and stages its artifact in the run directory. +/// Returns the runtime executable and the staged artifact, or `None` when a +/// required tool is unavailable. +fn provider_build( + name: &str, + package: &Path, + run: &Path, + records: &mut Records, +) -> Result> { + if name == "axum" { + let output = cargo_build() + .args([ + "build", + "--locked", + "-p", + "fixture-axum", + "--message-format=json-render-diagnostics", + ]) + .stderr(Stdio::inherit()) + .output()?; + require(output.status.success(), "Axum fixture build failed")?; + let staged = stage(&built_executable(&output.stdout, "fixture-axum")?, run)?; + records.push(json!({"event":"build","adapter":name,"artifact":artifact(&staged)?}))?; + return Ok(Some((staged.clone(), staged))); + } + let (variable, tool) = if name == "cloudflare" { + ("WRANGLER_BIN", "wrangler") + } else { + ("SPIN_BIN", "spin") + }; + let Some(exe) = executable(variable, tool) else { + records.push( + json!({"status":"unsupported","adapter":name,"reason":format!("{tool} unavailable")}), + )?; + return Ok(None); + }; + records.push(json!({"event":"runtime","adapter":name,"executable":exe,"version":String::from_utf8_lossy(&Command::new(&exe).arg("--version").output()?.stdout).trim()}))?; + let (staged, wasm) = if name == "spin" { + let help = Command::new(&exe).args(["up", "--help"]).output()?; + fs::write(run.join("spin-up-help.txt"), &help.stdout)?; + records.push(json!({"event":"runtime_controls","adapter":"spin","selected":"host defaults","source":"spin-up-help.txt"}))?; + checked(cargo_build().args([ + "build", + "--locked", + "--release", + "-p", + "fixture-spin", + "--target", + "wasm32-wasip2", + ]))?; + let staged = stage( + &root().join("target/wasm32-wasip2/release/fixture_spin.wasm"), + run, + )?; + (staged.clone(), staged) + } else { + let Some(builder) = executable("WORKER_BUILD_BIN", "worker-build") else { + records.push(json!({"status":"unsupported","reason":"worker-build unavailable"}))?; + return Ok(None); + }; + if !progress::check_bindgen(records)? { + return Ok(None); + } + checked( + Command::new(builder) + .args(["--release", ".", "--", "--locked"]) + .env("CARGO_TARGET_DIR", root().join("target")) + .current_dir(package), + )?; + let staged = stage(&package.join("build"), run)?; + // Only the explicit builder runs before artifact fingerprinting. + fs::write( + run.join("wrangler.toml"), + wrangler_runtime_config(package, &staged.join("worker/shim.mjs"))?, + )?; + let wasm = staged.join("index_bg.wasm"); + (staged, wasm) + }; + records.push(json!({"event":"build","adapter":name,"artifact":artifact(&wasm)?}))?; + Ok(Some((exe, staged))) +} +fn provider_command( + name: &str, + mode: &str, + exe: &Path, + staged: &Path, + package: &Path, + run: &Path, + port: u16, +) -> Result { + let mut command = Command::new(exe); + command + .env("CARGO_TARGET_DIR", root().join("target")) + .current_dir(package) + .env("EDGEZERO__ADAPTER__HOST", "127.0.0.1") + .env("EDGEZERO__ADAPTER__PORT", port.to_string()); + if name == "axum" { + let state = run.join("axum-state"); + fs::create_dir_all(state.join(".edgezero"))?; + fs::write( + state.join(".edgezero/local-config-fixture_config.json"), + "{\"marker\":\"local-fixture\"}", + )?; + command + .current_dir(state) + .env("EDGEZERO__SECRETS__FIXTURE_MARKER", "fixture-only-value"); + } else if name == "cloudflare" { + let state = run.join(mode).join("worker-state"); + checked( + Command::new(exe) + .args([ + "kv", + "key", + "put", + "marker", + "local-fixture", + "--binding", + "fixture_config", + "--local", + "--persist-to", + ]) + .arg(&state) + .current_dir(package), + )?; + command + .arg("--config") + .arg(run.join("wrangler.toml")) + .args([ + "dev", + "--local", + "--ip", + "127.0.0.1", + "--port", + &port.to_string(), + "--var", + &format!("FIXTURE_MODE:{mode}"), + "--persist-to", + ]) + .arg(state); + } else { + let manifest = fs::read_to_string(package.join("spin.toml"))? + .replace( + "../../target/wasm32-wasip2/release/fixture_spin.wasm", + &staged.display().to_string(), + ) + .replace( + "FIXTURE_MODE = \"retained\"", + &format!("FIXTURE_MODE = \"{mode}\""), + ); + let config = run.join(format!("spin-{mode}.toml")); + fs::write(&config, manifest)?; + command + .args(["up", "--listen", &format!("127.0.0.1:{port}"), "--from"]) + .arg(config) + .arg("--runtime-config-file") + .arg(package.join("runtime-config.toml")); + } + Ok(command) +} +/// Runs the provider assertions against a listening runtime, keeping +/// `failure` pointed at the current phase so a failure record names its step. +fn provider_checks( + name: &str, + mode: &str, + port: u16, + args: &Args, + records: &mut Records, + failure: &mut Value, +) -> Result<()> { + let mut seen: HashMap = HashMap::new(); + let mut reused = false; + let repetitions = if args.suite == "benchmark" { + args.repetitions + } else { + 1 + }; + let requests = if args.suite == "benchmark" { + args.requests + } else { + 5 + }; + for repetition in 0..repetitions { + *failure = json!({"phase":"probe","repetition":repetition}); + for _ in 0..requests { + let token = token(); + let reply = net::request(port, &format!("/probe/{token}"), &token, None, None)?; + let value = guest(&reply)?; + require( + value["token"] == token && value["path"] == token && value["body"] == token, + "probe request isolation mismatch", + )?; + if mode == "retained" { + require(value["builds"] == 1, "retained initialization mismatch")?; + } + validate_shared_state(&value, mode == "retained")?; + let instance = value["instance"].as_str().ok_or("missing instance")?; + if let Some(previous) = seen.get(instance) { + reused = true; + require( + value["ordinal"].as_u64() > previous["ordinal"].as_u64(), + "ordinal did not increase", + )?; + if mode == "per-request" { + require( + value["builds"].as_u64() > previous["builds"].as_u64(), + "per-request app was not rebuilt", + )?; + } + } + seen.insert(instance.into(), value.clone()); + records.push(annotate( + reply, + json!({"guest":value,"adapter":name,"mode":mode,"repetition":repetition,"workload":"probe"}), + ))?; + } + } + if !reused { + records.push(json!({"status":"unverified","adapter":name,"mode":mode,"reason":"no same-guest sequential reuse"}))?; + } + if mode == "retained" && name != "axum" { + *failure = json!({"phase":"binding_failure"}); + let failed = req(port, "/binding-failure")?; + require( + failed["status"] == 503, + "injected required binding did not fail", + )?; + let failed_guest: Value = serde_json::from_str(failed["body"].as_str().unwrap())?; + require( + failed_guest["error"].as_str().is_some_and(|error| { + error.starts_with("kv store open failed:") && !error.contains("missing_fixture_kv") + }), + "binding failure lost its sanitized open category", + )?; + let recovered = guest(&req(port, "/probe/after-binding-failure")?)?; + require( + recovered["builds"] == 1, + "app rebuilt after binding failure", + )?; + records.push(json!({"adapter":name,"mode":mode,"status":if failed_guest["instance"] == recovered["instance"] {"pass"} else {"unverified"},"assertion":"same_guest_binding_recovery","source":"injected_required_binding"}))?; + } + *failure = json!({"phase":"bindings"}); + let bindings = req(port, "/bindings")?; + require( + guest(&bindings)? == json!({"config":true,"kv":true,"secrets":true,"unknown":false}), + "binding mismatch", + )?; + records.push(annotate( + bindings, + json!({"adapter":name,"mode":mode,"assertion":"binding_reads"}), + ))?; + *failure = json!({"phase":"cookies"}); + let cookies = req(port, "/cookies")?; + if cookie_values(&cookies["headers"]) != vec!["first=1; Path=/", "second=2; Path=/"] { + records.push(annotate(cookies,json!({"status":"fail","adapter":name,"mode":mode,"phase":"cookies","reason":"duplicate Set-Cookie values were not preserved"})))?; + } + *failure = json!({"phase":"rendered_error"}); + require( + req(port, "/rendered-error")?["status"] == 400, + "rendered error mismatch", + )?; + if name != "axum" { + *failure = json!({"phase":"alternate_app"}); + require( + req(port, "/other")?["body"] == "other-app", + "alternate app mismatch", + )?; + } + *failure = json!({"phase":"overlap"}); + overlap_check(name, mode, port, records) +} +fn overlap_check(name: &str, mode: &str, port: u16, records: &mut Records) -> Result<()> { + let mut observed_overlap = false; + for attempt in 0..3 { + let backend = Backend::start()?; + let tokens = [token(), token()]; + let replies = thread::scope(|scope| { + let jobs = tokens + .iter() + .map(|token| { + let url = backend.url("/barrier"); + scope.spawn(move || { + net::request(port, &format!("/overlap/{token}"), token, Some(&url), None) + }) + }) + .collect::>(); + jobs.into_iter() + .map(|job| { + job.join() + .map_err(|_| "overlap request thread panicked".into()) + .and_then(|v| v) + }) + .collect::>>() + })?; + let values = replies.iter().map(guest).collect::>>()?; + for (value, token) in values.iter().zip(&tokens) { + require( + value["token"] == *token + && value["before"]["token"] == *token + && value["path"] == *token + && value["before"]["path"] == *token, + "overlap request-local values changed", + )?; + } + if let Err(error) = validate_overlap(&values[0], &values[1]) { + records.push(json!({"event":"overlap_observation","adapter":name,"mode":mode,"attempt":attempt,"reason":error.to_string(),"observations":values}))?; + continue; + } + observed_overlap = true; + break; + } + records.push(json!({"status":if observed_overlap {"pass"} else {"unverified"},"adapter":name,"mode":mode,"assertion":"same_guest_overlap","attempt_limit":3})) +} + +fn faults(exe: &Path, run: &Path, records: &mut Records) -> Result<()> { + let backend = Backend::start()?; + let config = run.join("faults.toml"); + fs::write( + &config, + format!( + "{}\n[local_server.backends.fault-origin]\nurl = {:?}\n", + fs::read_to_string(root().join("crates/fixture-fastly/fastly.toml"))?, + backend.url("/") + ), + )?; + for (index, path) in [ + "/constructor-panic", + "/collect-error", + "/inbound-error", + "/terminal-store-error", + "/recoverable-store-error", + ] + .iter() + .enumerate() + { + let log = run.join(format!("fault-{index}.log")); + let outcome = (|| -> Result<()> { + let port = net::port()?; + let _process = Process::start( + &mut fastly_command(exe, &config, run, "faults", port), + &log, + port, + )?; + let malformed_token = token(); + let rejected = net::malformed_request(port, &malformed_token)?; + require( + rejected.contains(" 400 "), + format!("malformed request was not rejected: {rejected}"), + )?; + require( + !logs(&log).iter().any(|e| e["token"] == malformed_token), + "malformed request reached callback unexpectedly", + )?; + records.push(json!({"status":"pass","assertion":"malformed_rejected_before_dispatch","token":malformed_token,"status_line":rejected}))?; + let failed_token = token(); + let reply = if *path == "/collect-error" { + let failure = net::request(port, path, &failed_token, None, None) + .err() + .ok_or("owned egress must abandon the failed response stream")?; + if let Some(partial) = failure.downcast_ref::() { + require( + partial.status == 200 && partial.body.is_empty(), + "egress source failure sent a replacement response", + )?; + json!({"status":partial.status,"body":"","interrupted":true}) + } else { + // An immediate source error can reset delivery before the head is observed. + require( + failure.downcast_ref::().is_some(), + format!("unexpected response-head failure: {failure}"), + )?; + json!({"no_response_head":true,"interrupted":true,"error":failure.to_string()}) + } + } else { + net::request(port, path, &failed_token, None, None)? + }; + let recoverable = *path == "/recoverable-store-error"; + let rendered_ingress = *path == "/inbound-error"; + require( + (*path == "/collect-error" && reply["no_response_head"] == true) + || reply["status"] + == if *path == "/collect-error" { + 200 + } else if recoverable { + 503 + } else { + 500 + }, + format!("fault not observed: {path}: {reply}"), + )?; + let following = req(port, "/probe/recovered")?; + let value = guest(&following)?; + let mut failed_guest = Value::Null; + await_evidence(|| { + let events = logs(&log); + failed_guest = events + .iter() + .find(|e| e["event"] == "request_start" && e["token"] == failed_token) + .map(|e| e["instance"].clone()) + .ok_or("missing fault attempt")?; + let event = if *path == "/constructor-panic" { + "constructor_panic" + } else if rendered_ingress { + "ingress_error_response" + } else if *path == "/collect-error" { + "post_commit_error" + } else { + "adapter_error" + }; + require( + events + .iter() + .any(|e| e["event"] == event && e["instance"] == failed_guest), + "missing actual failure boundary", + ) + })?; + if *path == "/collect-error" { + validate_fastly_delivery_report( + &failed_token, + &logs(&log), + &json!({"disposition":"routed", + "commitment":"Committed","body_kind":"Application","outcome":"SourceError", + "fallback":null,"may_send_native":false}), + )?; + let terminal = logs(&log) + .into_iter() + .filter(|event| { + event["event"] == "egress_terminal" && event["token"] == failed_token + }) + .collect::>(); + require( + terminal.len() == 1 + && terminal[0]["outcome"] == "SourceError" + && terminal[0]["body_kind"] == "Application" + && terminal[0]["bytes_written"] == 0 + && terminal[0]["fallback"].is_null(), + "immediate source failure must terminate once without fallback delivery", + )?; + } + if matches!(*path, "/recoverable-store-error" | "/terminal-store-error") { + validate_fastly_delivery_report( + &failed_token, + &logs(&log), + &json!({"disposition":"no-attempt", + "commitment":null,"body_kind":null,"outcome":null,"fallback":null,"may_send_native":true, + "no_attempt_reason":"required-store"}), + )?; + } + if recoverable || rendered_ingress { + let status = if value["instance"] == failed_guest { + "pass" + } else { + "unverified" + }; + require( + value["builds"] == 1, + "retained app rebuilt after handled failure", + )?; + require( + guest(&req(port, "/bindings")?)? + == json!({"config":true,"kv":true,"secrets":true,"unknown":false}), + "bindings failed to recover", + )?; + if rendered_ingress { + records.push(json!({"status":status,"assertion":"admitted_body_failure_rendered","instance":failed_guest,"source":"canonical_admitted_request_dispatch"}))?; + } else { + records.push(json!({"status":status,"assertion":"injected_selector_failure_recovery","instance":failed_guest,"source":"custom_callback_handles_real_registry_error"}))?; + } + } else { + require( + value["instance"] != failed_guest, + "terminal failure guest reused", + )?; + if *path != "/constructor-panic" { + await_evidence(|| { + require( + logs(&log).iter().any(|e| { + e["event"] == "sdk_summary" + && e["instance"] == failed_guest + && e["attempted"] == 1 + }), + "missing terminal attempt summary", + ) + })?; + } + records.push(json!({"status":"pass","assertion":"terminal_fault","path":path,"failed_guest":failed_guest,"source":"controlled_fixture_input"}))?; + } + records.push(annotate( + reply, + json!({"variant":"faults","repetition":index,"assertion":path}), + ))?; + Ok(()) + })(); + settle( + records, + &log, + json!({"variant":"faults","repetition":index}), + json!({"adapter":"fastly","path":path}), + outcome, + )?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn declared_kv_success_body_requires_request_and_instance_correlation() { + let response = |body: Value| json!({"status":200,"body":body.to_string()}); + let valid = json!({"token":"request","instance":"one"}); + validate_declared_kv_guest(&response(valid.clone()), "request", "one").unwrap(); + for body in [ + json!({}), + json!({"token":"request"}), + json!({"instance":"one"}), + json!({"token":"other","instance":"one"}), + json!({"token":"request","instance":"other"}), + ] { + assert!(validate_declared_kv_guest(&response(body), "request", "one").is_err()); + } + assert!( + validate_declared_kv_guest( + &json!({"status":500,"body":valid.to_string()}), + "request", + "one" + ) + .is_err() + ); + } + + #[test] + fn declared_kv_linked_result_requires_a_present_field_even_when_null() { + let response = |body: Value| json!({"status":200,"body":body.to_string()}); + validate_declared_kv_linked_response(&response(json!({"kv_outcome":null})), &json!(null)) + .unwrap(); + assert!(validate_declared_kv_linked_response(&response(json!({})), &json!(null)).is_err()); + assert!( + validate_declared_kv_linked_response( + &response(json!({"kv_outcome":"stale"})), + &json!(null) + ) + .is_err() + ); + validate_declared_kv_linked_response(&response(json!({"kv_outcome":[]})), &json!([])) + .unwrap(); + assert!( + validate_declared_kv_linked_response( + &response(json!({"kv_outcome":["stale"]})), + &json!([]) + ) + .is_err() + ); + assert!( + validate_declared_kv_linked_response(&json!({"status":500,"body":"{}"}), &json!(null)) + .is_err() + ); + } + + #[test] + fn declared_kv_operation_receipts_require_the_actual_typed_result() { + let start = json!({"event":"request_start","token":"case","instance":"one"}); + let terminal = json!({"event":"egress_terminal","token":"case","instance":"one"}); + let entry = json!({"event":"declared_kv_enter","token":"case","instance":"one","store":"missing","operation":"write"}); + let result = json!({"event":"declared_kv_result","token":"case","instance":"one","store":"missing","operation":"write","outcome":"open-failed","reason":"MissingBinding"}); + let valid = vec![ + start.clone(), + entry.clone(), + result.clone(), + terminal.clone(), + ]; + validate_declared_kv_operation( + &valid, + "case", + "one", + "fastly", + "/declared-kv/missing/write", + ) + .unwrap(); + assert!( + validate_declared_kv_operation( + &valid, + "case", + "one", + "spin", + "/declared-kv/missing/write" + ) + .is_err() + ); + assert!( + validate_declared_kv_operation(&valid, "case", "one", "fastly", "/declared-kv/free") + .is_err() + ); + validate_declared_kv_operation( + &[start.clone(), terminal.clone()], + "case", + "one", + "fastly", + "/declared-kv/free", + ) + .unwrap(); + for invalid in [ + vec![], + vec![entry.clone()], + vec![result.clone()], + vec![entry.clone(), entry.clone(), result.clone()], + vec![entry.clone(), result.clone(), result.clone()], + vec![ + terminal.clone(), + start.clone(), + entry.clone(), + result.clone(), + ], + vec![ + start.clone(), + terminal.clone(), + entry.clone(), + result.clone(), + ], + vec![ + start.clone(), + result.clone(), + entry.clone(), + terminal.clone(), + ], + ] { + assert!( + validate_declared_kv_operation( + &invalid, + "case", + "one", + "fastly", + "/declared-kv/missing/write" + ) + .is_err() + ); + } + for (field, value) in [ + ("token", json!("other")), + ("instance", json!("other")), + ("store", json!("linked")), + ("operation", json!("read")), + ("outcome", json!("operation-failed")), + ("reason", json!("Unavailable")), + ("reason", json!(null)), + ] { + let mut changed = result.clone(); + changed[field] = value; + assert!( + validate_declared_kv_operation( + &[start.clone(), entry.clone(), changed, terminal.clone()], + "case", + "one", + "fastly", + "/declared-kv/missing/write" + ) + .is_err() + ); + } + let mut linked_entry = entry.clone(); + linked_entry["store"] = json!("linked"); + let mut linked_result = result; + linked_result["store"] = json!("linked"); + linked_result["outcome"] = json!("success"); + linked_result["reason"] = json!(null); + validate_declared_kv_operation( + &[start, linked_entry, linked_result, terminal], + "case", + "one", + "spin", + "/declared-kv/linked/write", + ) + .unwrap(); + } + + #[test] + fn declared_kv_final_validation_rejects_late_duplicates_and_missing_cases() { + let mut checks = DeclaredKvChecks::default(); + let mut events = Vec::new(); + for (index, path) in [ + "/declared-kv/free", + "/declared-kv/missing/read", + "/declared-kv/missing/read", + "/declared-kv/free", + "/declared-kv/missing/write", + "/declared-kv/missing/delete", + "/declared-kv/missing/list", + "/declared-kv/linked/write", + "/declared-kv/linked/read", + "/declared-kv/linked/list", + "/declared-kv/linked/delete", + "/declared-kv/linked/read", + "/declared-kv/linked/list", + ] + .into_iter() + .enumerate() + { + let token = index.to_string(); + let instance = if index < 7 { "one" } else { "healthy-turnover" }; + checks.requests.push(DeclaredKvRequest { + token: token.clone(), + path: path.to_owned(), + instance: Some(instance.to_owned()), + }); + events.push(json!({"event":"request_start","token":token,"instance":instance})); + if path != "/declared-kv/free" { + let (store, operation) = path + .strip_prefix("/declared-kv/") + .unwrap() + .split_once('/') + .unwrap(); + events.push(json!({"event":"declared_kv_enter","token":token,"instance":instance,"store":store,"operation":operation})); + events.push(json!({"event":"declared_kv_result","token":token,"instance":instance,"store":store,"operation":operation,"outcome":if store == "missing" {"open-failed"} else {"success"},"reason":if store == "missing" {json!("MissingBinding")} else {json!(null)}})); + } + events.push(json!({"event":"egress_terminal","token":token,"instance":instance,"outcome":"Completed","body_kind":"Application","fallback":null})); + } + checks.instance = Some("one".to_owned()); + checks.validate(&events, "fastly").unwrap(); + let mut premature = events.clone(); + let result = premature + .iter() + .position(|event| event["token"] == "1" && event["event"] == "declared_kv_result") + .unwrap(); + let terminal = premature + .iter() + .position(|event| event["token"] == "1" && event["event"] == "egress_terminal") + .unwrap(); + premature.swap(result, terminal); + assert!(checks.validate(&premature, "fastly").is_err()); + let mut late = events.clone(); + late.push(events[1].clone()); + assert!(checks.validate(&late, "fastly").is_err()); + let mut changed = events.clone(); + for event in &mut changed { + if event["token"] == "2" { + event["instance"] = json!("turned-over"); + } + } + assert!(checks.validate(&changed, "fastly").is_err()); + let mut natural_turnover = events.clone(); + for event in &mut natural_turnover { + if event["token"] == "4" { + event["instance"] = json!("next-missing-instance"); + } + } + checks.requests[4].instance = Some("next-missing-instance".to_owned()); + checks.validate(&natural_turnover, "fastly").unwrap(); + assert_eq!(checks.failed_instances().len(), 2); + let warning = "fixture-logs :: WARN declared Fastly KV store 'missing_fixture_kv' failed to open: missing binding\n"; + assert!(checks.warning_count(warning, "fastly").is_err()); + assert_eq!( + checks.warning_count(&warning.repeat(2), "fastly").unwrap(), + 2 + ); + assert!(checks.warning_count(&warning.repeat(3), "fastly").is_err()); + checks.requests.pop(); + assert!(checks.validate(&events, "fastly").is_err()); + checks.requests.clear(); + assert!(checks.validate(&events, "fastly").is_err()); + } + + #[test] + fn declared_kv_provider_warnings_cannot_offset_between_instances() { + let checks = DeclaredKvChecks { + instance: Some("one".to_owned()), + requests: [ + ("one", "/declared-kv/missing/read"), + ("two", "/declared-kv/missing/write"), + ("healthy", "/declared-kv/linked/read"), + ] + .into_iter() + .enumerate() + .map(|(index, (instance, path))| DeclaredKvRequest { + token: index.to_string(), + instance: Some(instance.to_owned()), + path: path.to_owned(), + }) + .collect(), + }; + for (adapter, platform, reason) in [ + ("spin", "Spin", "invalid binding configuration"), + ("cloudflare", "Cloudflare", "missing binding"), + ] { + let warning = |instance: &str| { + json!({"event":"fixture_log","instance":instance,"level":"WARN","target":"edgezero::boot","message":format!("declared {platform} KV store 'missing_fixture_kv' failed to open: {reason}")}).to_string() + }; + let valid = format!("{}\n{}\n", warning("one"), warning("two")); + assert_eq!(checks.warning_count(&valid, adapter).unwrap(), 2); + assert!(checks.warning_count(&warning("one"), adapter).is_err()); + assert!( + checks + .warning_count( + &format!("{}\n{}\n", warning("one"), warning("one")), + adapter + ) + .is_err() + ); + assert!( + checks + .warning_count(&format!("{valid}{}\n", warning("healthy")), adapter) + .is_err() + ); + assert!( + checks + .warning_count(&format!("{valid}{}\n", warning("two")), adapter) + .is_err() + ); + } + } + + #[test] + fn declared_kv_lifecycle_rejects_missing_duplicate_failed_and_cross_instance() { + let start = json!({"event":"request_start","token":"case","instance":"one"}); + let end = json!({"event":"egress_terminal","token":"case","instance":"one","outcome":"Completed","body_kind":"Application","fallback":null}); + let valid = vec![start.clone(), end.clone()]; + assert_eq!( + validate_declared_kv_lifecycle(&valid, "case", Some("one"), "Completed").unwrap(), + "one" + ); + assert!( + validate_declared_kv_lifecycle(&valid, "case", Some("other"), "Completed").is_err() + ); + for invalid in [ + vec![], + vec![start.clone()], + vec![end.clone()], + vec![start.clone(), start.clone(), end.clone()], + vec![start.clone(), end.clone(), end.clone()], + ] { + assert!(validate_declared_kv_lifecycle(&invalid, "case", None, "Completed").is_err()); + } + for (field, value) in [ + ("instance", json!("other")), + ("token", json!("other")), + ("outcome", json!("TransportError")), + ("outcome", json!("HostHandoff")), + ("body_kind", json!("Fallback")), + ("fallback", json!("Completed")), + ] { + let mut changed = end.clone(); + changed[field] = value; + assert!( + validate_declared_kv_lifecycle( + &[start.clone(), changed], + "case", + None, + "Completed" + ) + .is_err() + ); + } + } + + #[test] + fn declared_kv_warning_counter_ignores_echoes_but_not_duplicates() { + let warning = + "declared Fastly KV store 'missing_fixture_kv' failed to open: missing binding"; + assert_eq!( + declared_kv_warning_count(&format!("fixture-logs :: WARN {warning}\nWARN {warning}\n")), + 1 + ); + assert_eq!( + declared_kv_warning_count(&format!( + "fixture-logs :: WARN {warning}\nfixture-logs :: WARN {warning}\n" + )), + 2 + ); + assert_eq!( + declared_kv_warning_count("fixture-logs :: ordinary warning\n"), + 0 + ); + } + #[test] + fn provider_warning_receipts_require_instance_target_and_exact_category() { + let valid = json!({"event":"fixture_log","instance":"one","level":"WARN","target":"edgezero::boot", + "message":"declared Spin KV store 'missing_fixture_kv' failed to open: invalid binding configuration"}); + assert_eq!( + provider_kv_warning_count(&valid.to_string(), "spin", "one"), + 1 + ); + assert_eq!( + provider_kv_warning_count(&valid.to_string(), "spin", "other"), + 0 + ); + assert_eq!( + provider_kv_warning_count(&format!("{valid}\n{valid}\n"), "spin", "one"), + 2 + ); + for (field, value) in [ + ("target", "other"), + ("event", "other"), + ("level", "INFO"), + ("message", "provider-token"), + ] { + let mut invalid = valid.clone(); + invalid[field] = json!(value); + assert_eq!( + provider_kv_warning_count(&invalid.to_string(), "spin", "one"), + 0 + ); + } + } + #[test] + fn cargo_artifacts_select_the_fresh_executable_in_a_configured_target() { + let output = concat!( + "{\"reason\":\"compiler-artifact\",\"target\":{\"name\":\"dependency\"},\"executable\":null}\n", + "{\"reason\":\"compiler-artifact\",\"target\":{\"name\":\"fixture-axum\"},\"executable\":\"/tmp/target/host-triple/debug/fixture-axum\"}\n", + "{\"reason\":\"build-finished\",\"success\":true}\n", + ); + assert_eq!( + built_executable(output.as_bytes(), "fixture-axum").unwrap(), + PathBuf::from("/tmp/target/host-triple/debug/fixture-axum") + ); + assert!(built_executable(b"", "fixture-axum").is_err()); + } + #[test] + fn wrangler_runtime_configuration_does_not_rebuild_fingerprinted_artifacts() { + let package = root().join("crates/fixture-cloudflare"); + let staged = Path::new("/run/artifacts/build/worker/shim.mjs"); + let config = wrangler_runtime_config(&package, staged).unwrap(); + assert!(!config.contains("[build]") && !config.contains("worker-build")); + assert!(config.contains(&format!("main = {staged:?}"))); + assert!(config.contains("binding = \"fixture_kv\"")); + } + #[test] + fn builds_share_a_fixed_target_directory() { + let command = cargo_build(); + let target = root().join("target"); + assert!( + command + .get_envs() + .any(|(key, value)| key == "CARGO_TARGET_DIR" && value == Some(target.as_os_str())) + ); + assert_eq!(command.get_current_dir(), Some(root().as_path())); + } + #[test] + fn served_artifacts_are_copies_inside_the_run_directory() { + let run = std::env::temp_dir().join(format!("edgezero-test-harness-stage-{}", token())); + let source = run.join("source/fixture-fastly-retained-app.wasm"); + fs::create_dir_all(source.parent().unwrap()).unwrap(); + fs::write(&source, b"first").unwrap(); + let staged = stage(&source, &run).unwrap(); + assert_eq!(staged, wasm(&run, "c")); + fs::write(&source, b"second").unwrap(); + assert_eq!(fs::read(&staged).unwrap(), b"first"); + fs::remove_dir_all(&run).unwrap(); + } +} diff --git a/crates/edgezero-test-harness/src/runners/assembly.rs b/crates/edgezero-test-harness/src/runners/assembly.rs new file mode 100644 index 000000000..a4db33d1e --- /dev/null +++ b/crates/edgezero-test-harness/src/runners/assembly.rs @@ -0,0 +1,995 @@ +use super::*; + +fn header<'a>(reply: &'a Value, name: &str) -> Option<&'a str> { + reply["headers"].as_array()?.iter().find_map(|field| { + field[0] + .as_str()? + .eq_ignore_ascii_case(name) + .then(|| field[1].as_str()) + .flatten() + }) +} + +fn header_values<'a>(reply: &'a Value, name: &str) -> Result> { + let fields = reply["headers"] + .as_array() + .ok_or("missing assembly headers")?; + let mut values = Vec::new(); + for field in fields { + let pair = field + .as_array() + .filter(|pair| pair.len() == 2) + .ok_or("malformed assembly field pair")?; + let key = pair[0].as_str().ok_or("non-text assembly field name")?; + let value = pair[1].as_str().ok_or("non-text assembly field value")?; + if key.eq_ignore_ascii_case(name) { + values.push(value); + } + } + Ok(values) +} + +fn error_contract(case: &str) -> (u16, &'static str, &'static str) { + let internal = case == "single-internal"; + let config = matches!( + case, + "single-config" | "single-hooks" | "single-config-explicit" + ); + if internal { + (500, "internal", "internal server error") + } else if config { + (503, "config_out_of_date", "configuration unavailable") + } else { + ( + 503, + "service_unavailable", + if case == "custom-retained" { + "configuration unavailable" + } else { + "state unavailable" + }, + ) + } +} + +fn validate_error_headers(reply: &Value, case: &str) -> Result<()> { + let (status, kind, _) = error_contract(case); + require(reply["status"] == status, "assembly failure status changed")?; + require( + header_values(reply, "content-type")? == ["application/json"], + "assembly failure lost JSON content type", + )?; + require( + header_values(reply, "x-assembly-configured")?.is_empty() + && header_values(reply, "x-assembly-synthesized")?.is_empty() + && header_values(reply, "set-cookie")?.is_empty(), + "failed app headers escaped", + )?; + require( + header_values(reply, "retry-after")? + == if kind == "config_out_of_date" { + vec!["60"] + } else { + vec![] + }, + "assembly retry policy changed", + ) +} + +fn validate_error(reply: &Value, case: &str) -> Result<()> { + validate_error_headers(reply, case)?; + let (status, kind, message) = error_contract(case); + let body: Value = serde_json::from_str(reply["body"].as_str().ok_or("missing assembly body")?)?; + require( + body == json!({"error":{"kind":kind,"message":message,"status":status}}), + "assembly default error body changed", + ) +} + +fn validate_recovered_headers(reply: &Value) -> Result<()> { + require( + reply["status"] == 404 + && header_values(reply, "x-assembly-synthesized")? == ["retry-2"] + && header_values(reply, "set-cookie")? == ["assembly=retry-2; Path=/"] + && header_values(reply, "content-type")? == ["application/json"], + "recovered app synthesized headers missing, duplicated or stale", + )?; + let body: Value = serde_json::from_str( + reply["body"] + .as_str() + .ok_or("missing recovered error body")?, + )?; + require( + body == json!({"error":{"kind":"not_found","message":"no route matched path: /assembly/retry/missing","status":404}}), + "recovered app canonical 404 body changed", + ) +} + +fn validate_installs(events: &[Value], expected: &[(&str, usize)]) -> Result<()> { + let installed = events + .iter() + .filter(|event| event["event"] == "assembly-synthesized-installed") + .collect::>(); + require( + installed.len() == expected.len(), + "missing/duplicate partial-app header installation witness", + )?; + for (event, (owner, generation)) in installed.iter().zip(expected) { + require( + event["owner"] == *owner + && event["generation"] == *generation + && event["fields"] == 2 + && event["instance"] + .as_str() + .is_some_and(|instance| !instance.is_empty()), + "wrong assembly header installation witness", + )?; + } + Ok(()) +} + +fn wire_request( + name: &str, + case: &str, + port: u16, + path: &str, + head: bool, + run: &Path, + records: &mut Records, +) -> Result { + let reply = response_headers::assembly_request(port, path, head, run, name == "cloudflare")?; + records.push(json!({"event":"assembly-wire-capture","adapter":name,"case":case,"method":if head {"HEAD"} else {"GET"}, + "client":reply,"qualification":false}))?; + require( + reply["protocol"] == "HTTP/1.1" + && reply["eof"] == true + && reply.get("capture_error") == Some(&Value::Null), + "assembly capture did not finish cleanly", + )?; + let lengths = header_values(&reply, "content-length")?; + require( + lengths.len() <= 1 + && lengths + .iter() + .all(|value| !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())) + && header_values(&reply, "transfer-encoding")?.is_empty(), + "ambiguous assembly response framing", + )?; + if !head { + let bytes = reply["body"] + .as_str() + .ok_or("missing captured assembly payload")? + .len(); + require( + lengths.len() == 1 && lengths[0].parse::()? == bytes, + "assembly Content-Length disagrees with wire payload", + )?; + } + if head { + require( + reply["body"] == "", + "HEAD assembly response transmitted payload", + )?; + } + Ok(reply) +} + +fn validate_failed_drop(events: &[Value]) -> Result<()> { + let attempts = events + .iter() + .filter(|event| event["event"] == "assembly-attempt") + .collect::>(); + require( + attempts.len() == 1 && attempts[0]["attempt"] == 1, + "recovery began before partial-state release was observed", + )?; + let drops = events + .iter() + .filter(|event| event["event"] == "assembly-state-drop" && event["generation"] == 1) + .collect::>(); + require( + drops.len() == 1 && drops[0]["instance"] == attempts[0]["instance"], + "failed app state was not released before recovery", + ) +} + +fn validate_retry( + replies: &[Value], + events: &[Value], + custom: bool, + positive_token: Option<&str>, +) -> Result<()> { + require(replies.len() == 3, "assembly retry needs three requests")?; + validate_error( + &replies[0], + if custom { + "custom-retained" + } else { + "standard-retained" + }, + )?; + let mut instance = None; + for (index, reply) in replies[1..].iter().enumerate() { + require( + reply["status"] == 200, + "failed assembly was cached or retry did not succeed", + )?; + require( + header(reply, "x-assembly-configured") == Some("true"), + "successful app configuration missing", + )?; + require( + header_values(reply, "x-assembly-synthesized")?.is_empty() + && header_values(reply, "set-cookie")?.is_empty(), + "routed recovery response gained synthesized headers", + )?; + let value: Value = + serde_json::from_str(reply["body"].as_str().ok_or("missing retry body")?)?; + require( + value["generation"] == 2 && value["attempts"] == 2, + "retained app was rebuilt", + )?; + require( + value["visits"] == index + 1, + "successful registered state was not retained", + )?; + require( + value["configurations"] == if custom { 2 } else { 1 }, + "configuration count changed", + )?; + let observed = value["instance"] + .as_str() + .filter(|id| !id.is_empty()) + .ok_or("missing assembly instance")?; + if let Some(expected) = &instance { + require(observed == expected, "recovery crossed instances")?; + } + instance = Some(observed.to_owned()); + } + let attempts = events + .iter() + .filter(|e| e["event"] == "assembly-attempt") + .collect::>(); + require( + attempts.len() == 2, + "expected exactly two assembly attempts", + )?; + for (index, event) in attempts.iter().enumerate() { + require( + event["attempt"] == index + 1 && event["instance"].as_str() == instance.as_deref(), + "assembly attempts crossed instances or ordinals", + )?; + } + require( + events + .iter() + .filter(|event| { + event["event"] == "assembly-synthesized-installed" && event["owner"] == "retry" + }) + .all(|event| event["instance"].as_str() == instance.as_deref()), + "retry header installation crossed instances", + )?; + if custom { + let drops = events + .iter() + .filter(|e| { + e["event"] == "assembly-state-drop" + && e["generation"] == 1 + && e["instance"].as_str() == instance.as_deref() + }) + .count(); + require( + drops == 1, + "failed configuration did not release partial state exactly once", + )?; + let deliveries = events + .iter() + .filter(|e| e["event"] == "assembly-delivery") + .collect::>(); + require( + deliveries.len() == 4, + "missing owned assembly delivery report", + )?; + let mut tokens = replies + .iter() + .map(|reply| { + reply["token"] + .as_str() + .ok_or("missing recovery request token") + }) + .collect::, _>>()?; + tokens.push(positive_token.ok_or("missing positive-control request token")?); + require( + tokens + .iter() + .enumerate() + .all(|(index, token)| !token.is_empty() && !tokens[..index].contains(token)), + "recovery request tokens are not unique", + )?; + for (index, delivery) in deliveries.iter().enumerate() { + require( + delivery["token"].as_str() == Some(tokens[index]), + "missing, duplicate or foreign owned delivery report", + )?; + require( + delivery["disposition"] == if index == 0 { "detached" } else { "routed" }, + "assembly delivery disposition changed", + )?; + require( + delivery["commitment"] == "Committed" + && delivery["outcome"] == "Completed" + && delivery["fallback"] == false + && delivery["may_send_native"] == false, + "assembly delivery failed or allowed a native resend", + )?; + require( + delivery["attempts"] == if index == 0 { 1 } else { 2 }, + "retained build attempt count changed", + )?; + require( + delivery["retained"] == (index != 0), + "partial application was retained", + )?; + } + } + Ok(()) +} + +fn validate_boot(events: &[Value], expected: usize, expected_internal: usize) -> Result<()> { + let logs = events + .iter() + .filter(|event| { + event["event"] == "fixture_log" + && event["target"] == "edgezero::boot" + && event["message"] + .as_str() + .is_some_and(|m| m.starts_with("application assembly failed:")) + }) + .collect::>(); + require( + logs.len() == expected, + "assembly errors were not logged exactly once per failure", + )?; + require( + logs.iter().all(|event| event["level"] == "ERROR"), + "assembly boot diagnostic level changed", + )?; + if expected_internal > 0 { + require( + logs.iter() + .filter(|event| { + event["message"] + .as_str() + .is_some_and(|m| m.contains("assembly-private-token=secret")) + }) + .count() + == expected_internal, + "full internal error was not logged exactly once", + )?; + } + Ok(()) +} + +pub(super) fn fastly(exe: &Path, run: &Path, records: &mut Records) -> Result<()> { + checked(cargo_build().args([ + "build", + "--locked", + "--release", + "-p", + "fixture-fastly", + "--bin", + "fixture-fastly-assembly", + "--target", + "wasm32-wasip1", + ]))?; + let artifact_path = stage( + &root().join("target/wasm32-wasip1/release/fixture-fastly-assembly.wasm"), + run, + )?; + records.push(json!({"event":"build","adapter":"fastly","variant":"assembly","artifact":artifact(&artifact_path)?}))?; + let mut failed = 0; + for case in [ + "standard-retained", + "custom-retained", + "single-routes", + "single-config", + "single-internal", + "single-hooks", + "single-config-explicit", + ] { + let log = run.join(format!("assembly-{case}.log")); + let config = run.join(format!("assembly-{case}.toml")); + let mut contents = fs::read_to_string(root().join("crates/fixture-fastly/fastly.toml"))?; + contents.push_str(&format!("\n[local_server.config_stores.assembly]\nformat = \"inline-toml\"\n[local_server.config_stores.assembly.contents]\ncase = {case:?}\n")); + fs::write(&config, contents)?; + let outcome = (|| -> Result<()> { + let port = net::port()?; + let process = Process::start( + &mut fastly_command(exe, &config, run, "assembly", port), + &log, + port, + )?; + let retained = case.ends_with("retained"); + let installs: Vec<_> = match case { + "standard-retained" => vec![("retry", 2)], + "custom-retained" => vec![("retry", 1), ("retry", 2)], + "single-config" | "single-hooks" | "single-config-explicit" => { + vec![("configuration", 1), ("configuration", 1)] + } + _ => vec![], + }; + let mut replies = Vec::new(); + for index in 0..if retained { 3 } else { 1 } { + replies.push(wire_request( + "fastly", + case, + port, + "/assembly/retry", + false, + run, + records, + )?); + if case == "custom-retained" && index == 0 { + await_evidence(|| validate_failed_drop(&logs(&log)))?; + records.push(json!({"assertion":"failed-state-released-before-retry","adapter":"fastly","token":replies[0]["token"],"status":"pass","qualification":false}))?; + } + } + let mut positive_token = None; + if retained { + let positive = wire_request( + "fastly", + case, + port, + "/assembly/retry/missing", + false, + run, + records, + )?; + validate_recovered_headers(&positive)?; + positive_token = positive["token"].as_str().map(str::to_owned); + await_evidence(|| { + validate_retry( + &replies, + &logs(&log), + case == "custom-retained", + positive_token.as_deref(), + ) + })?; + } else { + validate_error(&replies[0], case)?; + let head = + wire_request("fastly", case, port, "/assembly/retry", true, run, records)?; + validate_error_headers(&head, case)?; + require( + head["status"] == replies[0]["status"] && head["body"] == "", + "HEAD assembly failure transmitted bytes or changed status", + )?; + records.push(annotate( + head, + json!({"adapter":"fastly","case":case,"method":"HEAD"}), + ))?; + } + await_evidence(|| { + validate_installs(&logs(&log), &installs)?; + validate_boot( + &logs(&log), + if retained { 1 } else { 2 }, + if case == "single-internal" { 2 } else { 0 }, + ) + })?; + drop(process); + validate_installs(&logs(&log), &installs)?; + if retained { + validate_retry( + &replies, + &logs(&log), + case == "custom-retained", + positive_token.as_deref(), + )?; + } + validate_boot( + &logs(&log), + if retained { 1 } else { 2 }, + if case == "single-internal" { 2 } else { 0 }, + )?; + for reply in replies { + records.push(annotate(reply, json!({"adapter":"fastly","case":case})))?; + } + records.push(json!({"assertion":"canonical-assembly-delivery","adapter":"fastly","case":case,"status":"pass","qualification":false})) + })(); + let result = settle( + records, + &log, + json!({"adapter":"fastly","case":case}), + json!({"phase":"assembly"}), + outcome, + ); + failed += usize::from(result.is_err()); + } + require(failed == 0, "one or more Fastly assembly cases failed") +} + +pub(super) fn provider(name: &str, run: &Path, records: &mut Records) -> Result<()> { + if name == "axum" { + return axum_startup(run, records); + } + let package = root().join("crates").join(format!("fixture-{name}")); + let Some((exe, staged)) = provider_build(name, &package, run, records)? else { + return Ok(()); + }; + let mut failed = 0; + for mode in ["per-request", "retained"] { + let log = run.join(format!("assembly-{name}-{mode}.log")); + let outcome = (|| -> Result<()> { + let port = net::port()?; + let process = Process::start( + &mut provider_command(name, mode, &exe, &staged, &package, run, port)?, + &log, + port, + )?; + for (path, case) in [ + ("/assembly/configuration", "single-config"), + ("/assembly/internal", "single-internal"), + ] { + let reply = wire_request(name, case, port, path, false, run, records)?; + validate_error(&reply, case)?; + records.push(annotate( + reply, + json!({"adapter":name,"mode":mode,"case":case}), + ))?; + } + let head = wire_request( + name, + "single-config", + port, + "/assembly/configuration", + true, + run, + records, + )?; + validate_error_headers(&head, "single-config")?; + require( + head["status"] == 503 + && head["body"] == "" + && header(&head, "retry-after") == Some("60"), + "HEAD config failure lost its status, suppression or retry policy", + )?; + records.push(annotate( + head, + json!({"adapter":name,"mode":mode,"method":"HEAD"}), + ))?; + let mut replies = vec![wire_request( + name, + "standard-retained", + port, + "/assembly/retry", + false, + run, + records, + )?]; + validate_error(&replies[0], "standard-retained")?; + if mode == "retained" { + replies.push(wire_request( + name, + "standard-retained", + port, + "/assembly/retry", + false, + run, + records, + )?); + replies.push(wire_request( + name, + "standard-retained", + port, + "/assembly/retry", + false, + run, + records, + )?); + await_evidence(|| { + require( + logs(&log) + .iter() + .filter(|e| e["event"] == "assembly-attempt") + .count() + >= 2, + "assembly attempts not observed", + ) + })?; + let events = logs(&log); + let attempts = events + .iter() + .filter(|e| e["event"] == "assembly-attempt") + .collect::>(); + if replies[1]["status"] == 503 + && attempts.len() == 3 + && attempts.iter().all(|e| e["attempt"] == 1) + && attempts + .windows(2) + .all(|pair| pair[0]["instance"] != pair[1]["instance"]) + { + records.push(json!({"assertion":"same-instance-assembly-retry","adapter":name,"mode":mode,"status":"unverified","reason":"runtime supplied fresh instances; same-instance retry was not observable","qualification":false}))?; + } else { + validate_retry(&replies, &events, false, None)?; + let positive = wire_request( + name, + "standard-retained", + port, + "/assembly/retry/missing", + false, + run, + records, + )?; + validate_recovered_headers(&positive)?; + } + } + let mut installs = vec![("configuration", 1), ("configuration", 1)]; + if mode == "retained" && replies[1]["status"] == 200 { + installs.push(("retry", 2)); + } + let errors = 3 + replies.iter().filter(|r| r["status"] == 503).count(); + await_evidence(|| { + validate_installs(&logs(&log), &installs)?; + validate_boot(&logs(&log), errors, 1) + })?; + drop(process); + validate_installs(&logs(&log), &installs)?; + if mode == "retained" && replies[1]["status"] == 200 { + validate_retry(&replies, &logs(&log), false, None)?; + } + validate_boot(&logs(&log), errors, 1)?; + for reply in replies { + records.push(annotate( + reply, + json!({"adapter":name,"mode":mode,"case":"retry"}), + ))?; + } + records.push(json!({"assertion":"canonical-assembly-delivery","adapter":name,"mode":mode,"status":"pass","qualification":false})) + })(); + let result = settle( + records, + &log, + json!({"adapter":name,"mode":mode}), + json!({"phase":"assembly"}), + outcome, + ); + failed += usize::from(result.is_err()); + } + require(failed == 0, "one or more provider assembly cases failed") +} + +fn axum_startup(run: &Path, records: &mut Records) -> Result<()> { + let output = cargo_build() + .args([ + "build", + "--offline", + "--locked", + "--release", + "-p", + "fixture-axum", + "--bin", + "fixture-axum-assembly", + "--message-format=json-render-diagnostics", + ]) + .stderr(Stdio::inherit()) + .output()?; + require( + output.status.success(), + "native assembly fixture build failed", + )?; + let binary = stage( + &built_executable(&output.stdout, "fixture-axum-assembly")?, + run, + )?; + records.push(json!({"event":"build","adapter":"axum","variant":"assembly-startup","artifact":artifact(&binary)?}))?; + let log = run.join("assembly-axum-startup.log"); + let outcome = (|| -> Result<()> { + let occupied = std::net::TcpListener::bind(("127.0.0.1", 0))?; + let addr = occupied.local_addr()?; + let mut command = Command::new(&binary); + command + .current_dir(run) + .env("EDGEZERO__ADAPTER__HOST", "127.0.0.1") + .env("EDGEZERO__ADAPTER__PORT", addr.port().to_string()); + let mut process = Process::spawn(&mut command, &log)?; + let deadline = Instant::now() + Duration::from_secs(5); + let status = loop { + if let Some(status) = process.0.try_wait()? { + break status; + } + require( + Instant::now() < deadline, + "native assembly failure did not exit before deadline", + )?; + thread::sleep(Duration::from_millis(5)); + }; + drop(process); + let events = logs(&log); + validate_installs(&events, &[("configuration", 1)])?; + validate_boot(&events, 1, 0)?; + let text = fs::read_to_string(&log)?; + require( + !status.success() + && text.contains("application configuration failed") + && text.contains("configuration unavailable") + && !text.contains("Address already in use") + && !text.contains("panicked"), + "native failure was not canonical pre-listener configuration rejection", + )?; + drop(occupied); + let _recovered = std::net::TcpListener::bind(addr)?; + records.push( + json!({"assertion":"assembly-before-bind","adapter":"axum","status":"pass", + "exit_code":status.code(),"occupied_port_guard":true,"http_response_attempted":false, + "qualification":false}), + ) + })(); + settle( + records, + &log, + json!({"adapter":"axum","case":"assembly-startup"}), + json!({"phase":"assembly-startup"}), + outcome, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn head_capture_accepts_representation_length_but_rejects_transmitted_bytes() { + use std::io::{BufRead, BufReader}; + for body in ["", "body"] { + let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + let mut reader = BufReader::new(stream.try_clone().unwrap()); + let mut line = String::new(); + reader.read_line(&mut line).unwrap(); + assert!(line.starts_with("HEAD ")); + while line != "\r\n" { + line.clear(); + assert!(reader.read_line(&mut line).unwrap() > 0); + } + write!(stream, "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 4\r\nConnection: close\r\n\r\n{body}").unwrap(); + }); + let run = std::env::temp_dir().join(format!("assembly-head-{}", token())); + fs::create_dir(&run).unwrap(); + let mut records = Records { + path: run.join("events.jsonl"), + values: Vec::new(), + }; + let response = wire_request( + "fastly", + "head-control", + port, + "/assembly/configuration", + true, + &run, + &mut records, + ); + server.join().unwrap(); + assert_eq!(response.is_ok(), body.is_empty()); + fs::remove_dir_all(run).unwrap(); + } + } + + #[test] + fn assembly_capture_rejects_wrong_or_ambiguous_get_framing() { + use std::io::{BufRead, BufReader}; + for fields in [ + "Content-Length: 0", + "Content-Length: 14\r\nContent-Length: 14", + "Transfer-Encoding: chunked", + ] { + let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + let mut reader = BufReader::new(stream.try_clone().unwrap()); + loop { + let mut line = String::new(); + assert!(reader.read_line(&mut line).unwrap() > 0); + if line == "\r\n" { + break; + } + } + write!( + stream, + "HTTP/1.1 200 OK\r\n{fields}\r\nConnection: close\r\n\r\nrouted success" + ) + .unwrap(); + }); + let run = std::env::temp_dir().join(format!("assembly-framing-{}", token())); + fs::create_dir(&run).unwrap(); + let mut records = Records { + path: run.join("events.jsonl"), + values: Vec::new(), + }; + let result = wire_request( + "fastly", + "framing-control", + port, + "/assembly/retry", + false, + &run, + &mut records, + ); + server.join().unwrap(); + fs::remove_dir_all(run).unwrap(); + assert!(result.is_err(), "{fields}"); + } + } + + #[test] + fn assembly_boot_evidence_counts_each_failed_request_including_head() { + let event = json!({"event":"fixture_log","target":"edgezero::boot","level":"ERROR", + "message":"application assembly failed: assembly-private-token=secret"}); + validate_boot(&[event.clone(), event], 2, 2).unwrap(); + } + + #[test] + fn assembly_evidence_rejects_changed_status_body_and_required_headers() { + let valid = json!({"status":503,"headers":[["content-type","application/json"],["retry-after","60"]], + "body":json!({"error":{"kind":"config_out_of_date","message":"configuration unavailable","status":503}}).to_string()}); + validate_error(&valid, "single-config").unwrap(); + for (field, value) in [ + ("status", json!(500)), + ("body", json!("private-configured-renderer")), + ("headers", json!([["content-type", "application/json"]])), + ] { + let mut invalid = valid.clone(); + invalid[field] = value; + assert!(validate_error(&invalid, "single-config").is_err()); + } + } + + #[test] + fn failed_assembly_evidence_rejects_synthesized_fields_from_a_partial_app() { + let valid = json!({"status":503,"headers":[["content-type","application/json"],["retry-after","60"]], + "body":json!({"error":{"kind":"config_out_of_date","message":"configuration unavailable","status":503}}).to_string()}); + validate_error(&valid, "single-config").unwrap(); + for name in ["X-Assembly-Synthesized", "Set-Cookie"] { + let mut leaked = valid.clone(); + leaked["headers"] + .as_array_mut() + .unwrap() + .push(json!([name, "private-configured-header"])); + assert!(validate_error(&leaked, "single-config").is_err(), "{name}"); + } + } + + #[test] + fn assembly_header_evidence_rejects_malformed_and_conflicting_metadata() { + let valid = json!({"status":503,"headers":[["content-type","application/json"],["retry-after","60"]]}); + validate_error_headers(&valid, "single-config").unwrap(); + for extra in [ + json!(["CONTENT-TYPE", "text/plain"]), + json!(["Retry-After", "1"]), + json!(["broken"]), + json!([false, "value"]), + ] { + let mut invalid = valid.clone(); + invalid["headers"].as_array_mut().unwrap().push(extra); + assert!(validate_error_headers(&invalid, "single-config").is_err()); + } + for missing in [Value::Null, json!([]), json!("missing")] { + let mut invalid = valid.clone(); + invalid["headers"] = missing; + assert!(validate_error_headers(&invalid, "single-config").is_err()); + } + } + + #[test] + fn recovered_headers_require_the_successful_generation_and_canonical_body() { + let valid = json!({"status":404,"headers":[["x-assembly-synthesized","retry-2"],["set-cookie","assembly=retry-2; Path=/"],["content-type","application/json"]], + "body":json!({"error":{"kind":"not_found","message":"no route matched path: /assembly/retry/missing","status":404}}).to_string()}); + validate_recovered_headers(&valid).unwrap(); + for mutation in [ + "missing", + "stale-header", + "stale-cookie", + "duplicate", + "body", + "status", + ] { + let mut invalid = valid.clone(); + match mutation { + "missing" => { + invalid["headers"].as_array_mut().unwrap().remove(0); + } + "stale-header" => invalid["headers"][0][1] = json!("retry-1"), + "stale-cookie" => invalid["headers"][1][1] = json!("assembly=retry-1; Path=/"), + "duplicate" => invalid["headers"] + .as_array_mut() + .unwrap() + .push(json!(["X-Assembly-Synthesized", "retry-2"])), + "body" => invalid["body"] = json!("private-configured-renderer"), + "status" => invalid["status"] = json!(503), + _ => unreachable!(), + } + assert!(validate_recovered_headers(&invalid).is_err(), "{mutation}"); + } + } + + #[test] + fn installation_witness_requires_exact_count_owner_and_generation() { + let valid = vec![ + json!({"event":"assembly-synthesized-installed","instance":"one","owner":"retry","generation":1,"fields":2}), + json!({"event":"assembly-synthesized-installed","instance":"one","owner":"retry","generation":2,"fields":2}), + ]; + let expected = [("retry", 1), ("retry", 2)]; + validate_installs(&valid, &expected).unwrap(); + assert!(validate_installs(&valid[..1], &expected).is_err()); + let mut duplicate = valid.clone(); + duplicate.push(valid[1].clone()); + assert!(validate_installs(&duplicate, &expected).is_err()); + for field in ["instance", "owner", "generation", "fields"] { + let mut invalid = valid.clone(); + invalid[1][field] = Value::Null; + assert!(validate_installs(&invalid, &expected).is_err(), "{field}"); + } + } + + #[test] + fn owned_delivery_evidence_rejects_count_preserving_duplicate_requests() { + let replies = vec![ + json!({"status":503,"token":"failed","headers":[["content-type","application/json"]], + "body":json!({"error":{"status":503,"kind":"service_unavailable","message":"configuration unavailable"}}).to_string()}), + json!({"status":200,"token":"first","headers":[["x-assembly-configured","true"]],"body":json!({"instance":"one","generation":2,"attempts":2,"visits":1,"configurations":2}).to_string()}), + json!({"status":200,"token":"second","headers":[["x-assembly-configured","true"]],"body":json!({"instance":"one","generation":2,"attempts":2,"visits":2,"configurations":2}).to_string()}), + ]; + let mut events = vec![ + json!({"event":"assembly-attempt","instance":"one","attempt":1}), + json!({"event":"assembly-attempt","instance":"one","attempt":2}), + json!({"event":"assembly-state-drop","instance":"one","generation":1}), + ]; + for (index, token) in ["failed", "first", "second", "positive"] + .into_iter() + .enumerate() + { + events.push(json!({"event":"assembly-delivery","token":token,"disposition":if index==0 {"detached"} else {"routed"}, + "commitment":"Committed","outcome":"Completed","fallback":false,"may_send_native":false, + "attempts":if index==0 {1} else {2},"retained":index!=0})); + } + validate_retry(&replies, &events, true, Some("positive")).unwrap(); + let mut duplicate = events.clone(); + duplicate[6] = events[4].clone(); + assert!(validate_retry(&replies, &duplicate, true, Some("positive")).is_err()); + assert!(validate_retry(&replies, &events, true, Some("first")).is_err()); + let mut foreign = events; + foreign[6]["token"] = json!("other-request"); + assert!(validate_retry(&replies, &foreign, true, Some("positive")).is_err()); + } + + #[test] + fn partial_state_release_requires_a_drop_before_recovery_starts() { + let attempt = json!({"event":"assembly-attempt","instance":"one","attempt":1}); + let release = json!({"event":"assembly-state-drop","instance":"one","generation":1}); + validate_failed_drop(&[attempt.clone(), release.clone()]).unwrap(); + assert!(validate_failed_drop(std::slice::from_ref(&attempt)).is_err()); + assert!( + validate_failed_drop(&[attempt.clone(), release.clone(), release.clone()]).is_err() + ); + let mut foreign = release.clone(); + foreign["instance"] = json!("other"); + assert!(validate_failed_drop(&[attempt.clone(), foreign]).is_err()); + assert!( + validate_failed_drop(&[ + attempt, + release, + json!({"event":"assembly-attempt","instance":"one","attempt":2}) + ]) + .is_err() + ); + } +} diff --git a/crates/edgezero-test-harness/src/runners/framing.rs b/crates/edgezero-test-harness/src/runners/framing.rs new file mode 100644 index 000000000..fd4463e6e --- /dev/null +++ b/crates/edgezero-test-harness/src/runners/framing.rs @@ -0,0 +1,476 @@ +use super::*; + +const MAX_BODY: u64 = 128 * 1024; +const MAX_HEAD: u64 = 16 * 1024; + +pub(super) fn run(name: &str, parent: &Path, records: &mut Records) -> Result<()> { + let run = parent.join(format!("framing-{name}")); + fs::create_dir_all(&run)?; + let revision = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(root()) + .output()?; + require(revision.status.success(), "missing framing source revision")?; + let revision = String::from_utf8(revision.stdout)?.trim().to_owned(); + let Some((mut command, port)) = + progress::build(name, &run, records, &revision, "response-framing")? + else { + return Ok(()); + }; + let log = run.join("runtime.log"); + let outcome = Process::start(&mut command, &log, port) + .and_then(|_process| checks(name, port, &run, records)); + settle( + records, + &log, + json!({"adapter":name,"suite":"response-framing"}), + json!({"phase":"response-framing"}), + outcome, + ) +} + +fn read_bounded(path: &Path, limit: u64) -> Result> { + require( + fs::metadata(path)?.len() <= limit, + "framing capture exceeds its fixed bound", + )?; + Ok(fs::read(path)?) +} + +fn parse_head(bytes: &[u8]) -> Result { + require( + bytes.ends_with(b"\r\n\r\n"), + "truncated framing header section", + )?; + let text = std::str::from_utf8(bytes)?; + let mut lines = text.trim_end_matches("\r\n").split("\r\n"); + let mut status = lines + .next() + .ok_or("missing HTTP framing status")? + .split_whitespace(); + let protocol = status.next().ok_or("missing HTTP framing version")?; + let status: u16 = status + .next() + .ok_or("missing framing status code")? + .parse()?; + let mut headers = Vec::new(); + for line in lines { + let (name, value) = line.split_once(':').ok_or("invalid framing header")?; + headers.push(json!([name.to_ascii_lowercase(), value.trim()])); + } + let values = |key: &str| { + headers + .iter() + .filter(|pair| pair[0] == key) + .map(|pair| pair[1].clone()) + .collect::>() + }; + let lengths = values("content-length"); + let transfer = values("transfer-encoding"); + require( + lengths.len() <= 1 && transfer.len() <= 1 && (lengths.is_empty() || transfer.is_empty()), + "ambiguous response framing", + )?; + let length = if let Some(value) = lengths.first() { + Some( + value + .as_str() + .ok_or("non-text content-length")? + .parse::()?, + ) + } else { + None + }; + Ok( + json!({"protocol":protocol,"status":status,"headers":headers,"length":length,"transfer":transfer.first()}), + ) +} + +fn request(port: u16, path: &str, head: bool, h2: bool, run: &Path) -> Result { + let id = token(); + let header_path = run.join(format!("{id}.headers")); + let body_path = run.join(format!("{id}.body")); + fs::write(&header_path, [])?; + fs::write(&body_path, [])?; + let mut command = Command::new(std::env::var_os("CURL_BIN").unwrap_or_else(|| "curl".into())); + command + .args([ + "--silent", + "--show-error", + "--noproxy", + "*", + "--max-time", + "5", + "--max-filesize", + "131072", + ]) + .arg(if h2 { + "--http2-prior-knowledge" + } else { + "--http1.1" + }) + .args(["--header", "Accept-Encoding: identity", "--dump-header"]) + .arg(&header_path) + .arg("--output") + .arg(&body_path); + if head { + command.arg("--head"); + } + let output = command + .arg(format!("http://127.0.0.1:{port}{path}")) + .output()?; + let mut response = capture( + head, + &header_path, + &body_path, + output.status.success(), + &output.stderr, + )?; + response["curl_exit"] = json!(output.status.code()); + Ok(response) +} + +fn capture( + head: bool, + header_path: &Path, + body_path: &Path, + success: bool, + diagnostics: &[u8], +) -> Result { + let mut response = match parse_head(&read_bounded(header_path, MAX_HEAD)?) { + Ok(response) => response, + Err(error) => json!({"head_parse_error":error.to_string()}), + }; + let bytes = read_bounded(body_path, MAX_BODY)?; + response["body"] = if head { + Value::Null + } else { + json!(std::str::from_utf8(&bytes)?) + }; + response["body_observation"] = json!(if head { "header-only" } else { "captured" }); + response["curl_success"] = json!(success); + response["head_artifact"] = artifact(header_path)?; + response["capture_artifact"] = artifact(body_path)?; + let diagnostic_path = header_path.with_extension("curl-stderr"); + fs::write( + &diagnostic_path, + &diagnostics[..diagnostics.len().min(4096)], + )?; + response["diagnostic_artifact"] = artifact(&diagnostic_path)?; + response["diagnostic_truncated"] = json!(diagnostics.len() > 4096); + Ok(response) +} + +fn validate(response: &Value, adapter: &str, case: &str, h2: bool) -> Result<()> { + if case == "short-stream" && response["head_artifact"]["bytes"] == 0 { + return require( + response["curl_success"] == false + && response["curl_exit"] == if h2 { 92 } else { 52 } + && response["body"] == "", + "headless short-source abort lacks an empty-reply witness", + ); + } + require( + response["protocol"] == if h2 { "HTTP/2" } else { "HTTP/1.1" }, + "framing probe negotiated the wrong protocol or lost its head", + )?; + if case == "short-stream" { + let partial = response["body"] + .as_str() + .ok_or("missing partial body observation")?; + return require( + response["curl_success"] == false + && response["curl_exit"] == if h2 { 92 } else { 18 } + && response["status"] == 200 + && response["length"] == 7 + && b"abcdef".starts_with(partial.as_bytes()), + "short source did not terminate as an incomplete response", + ); + } + require( + response["curl_success"] == true, + "controlled framing request failed; see bounded curl diagnostics", + )?; + let (status, body) = match case { + "empty" => (200, ""), + "trailers" | "mismatch" => (500, "internal server error"), + "expired" => (504, "response write deadline exceeded"), + "not-modified" | "not-modified-explicit" => (304, ""), + "no-content" => (204, ""), + "reset-content" => (205, ""), + "head" | "head-explicit" => (200, ""), + "rendered-not-found" => ( + 404, + response["body"] + .as_str() + .ok_or("missing rendered 404 body")?, + ), + _ => (200, "abcdef"), + }; + require( + response["status"] == status + && if case.starts_with("head") { + response["body"].is_null() && response["body_observation"] == "header-only" + } else { + response["body"] == body + }, + "framing response status/body mismatch", + )?; + if case == "rendered-not-found" { + let rendered: Value = serde_json::from_str(body)?; + require( + rendered["error"]["status"] == 404, + "404 did not pass through framework rendering", + )?; + } + if matches!( + case, + "head" + | "head-explicit" + | "not-modified" + | "not-modified-explicit" + | "no-content" + | "reset-content" + ) { + if case == "no-content" { + require( + response["length"].is_null() && response["transfer"].is_null(), + "204 carried forbidden transfer framing", + )?; + } + if case == "reset-content" { + require( + (response["length"].is_null() || response["length"] == 0) + && (response["transfer"].is_null() + || (!h2 && response["transfer"] == "chunked")), + "205 carried nonzero or invalid transfer framing", + )?; + } + if adapter == "fastly" && case != "no-content" && case != "reset-content" { + require( + response["length"].is_null() || response["length"] == 6, + "Fastly replaced a known representation length with the suppressed payload size", + )?; + } + if adapter != "fastly" { + let eligible = if case == "reset-content" { + response["length"].is_null() || response["length"] == 0 + } else if case == "not-modified-explicit" { + response["length"].is_null() || response["length"] == 6 + } else if case.ends_with("explicit") { + response["length"] == 6 + } else { + response["length"].is_null() || (case == "head" && response["length"] == 0) + }; + require( + eligible, + "suppressed response gained or lost a representation length", + )?; + } + } else if case == "streamed" { + require( + response["length"].is_null() + && response["transfer"] == if h2 { Value::Null } else { json!("chunked") }, + "unknown stream acquired guessed framing", + )?; + } else { + require( + response["length"] == body.len() && response["transfer"].is_null(), + "buffered/declared response lacks exact framing", + )?; + } + Ok(()) +} + +fn checks(adapter: &str, port: u16, run: &Path, records: &mut Records) -> Result<()> { + let curl = Command::new(std::env::var_os("CURL_BIN").unwrap_or_else(|| "curl".into())) + .arg("--version") + .output()?; + require( + curl.status.success(), + "curl unavailable for response-framing probes", + )?; + let version = String::from_utf8(curl.stdout)?; + require( + adapter != "fastly" || version.contains("HTTP2"), + "Fastly parity requires an HTTP/2 curl build", + )?; + records.push( + json!({"event":"framing-client","adapter":adapter,"version":version,"qualification":false}), + )?; + let mut passed = true; + for h2 in [false, true] { + if h2 && adapter != "fastly" { + continue; + } + for case in [ + "buffered", + "empty", + "explicit", + "streamed", + "declared-stream", + "short-stream", + "rendered-not-found", + "mismatch", + "trailers", + "expired", + "head", + "head-explicit", + "not-modified", + "not-modified-explicit", + "no-content", + "reset-content", + ] { + let head = case.starts_with("head"); + let client = request(port, &format!("/progress/framing/{case}"), head, h2, run); + let check = client + .as_ref() + .map_err(|error| error.to_string().into()) + .and_then(|response| validate(response, adapter, case, h2)); + records.push(json!({"event":"framing-case","adapter":adapter,"case":case,"http2":h2,"client":client.as_ref().ok(),"status":if check.is_ok(){"pass"}else{"fail"},"error":check.as_ref().err().map(ToString::to_string),"qualification":false}))?; + passed &= check.is_ok(); + if adapter == "fastly" + && !matches!( + case, + "streamed" + | "declared-stream" + | "short-stream" + | "rendered-not-found" + | "trailers" + | "mismatch" + | "expired" + ) + { + let reference = + request(port, &format!("/progress/reference/{case}"), head, h2, run); + let parity = reference + .as_ref() + .map_err(|error| error.to_string().into()) + .and_then(|reference| { + let client = client.as_ref().map_err(|error| error.to_string())?; + validate_sdk_parity(client, reference) + }); + records.push(json!({"event":"framing-sdk-parity","adapter":adapter,"case":case,"http2":h2,"reference":reference.as_ref().ok(),"status":if parity.is_ok(){"pass"}else{"fail"},"error":parity.as_ref().err().map(ToString::to_string),"qualification":false}))?; + passed &= parity.is_ok(); + } + } + } + records.push(json!({"event":"framing-hosted-gates","adapter":adapter,"status":"open","qualification":false,"reason":"local runtime results do not qualify hosted compression, deployed header delivery, or consumer response parity"}))?; + require( + passed, + "one or more independent response-framing cases failed", + ) +} + +fn validate_sdk_parity(candidate: &Value, reference: &Value) -> Result<()> { + require( + candidate["curl_success"] == true + && reference["curl_success"] == true + && candidate.get("head_parse_error").is_none() + && reference.get("head_parse_error").is_none() + && ["protocol", "status", "length", "transfer", "body"] + .iter() + .all(|key| reference[key] == candidate[key]), + "Fastly SDK automatic framing parity mismatch", + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn transport_timeouts_and_failed_candidates_cannot_pass_framing_gates() { + let mut truncated = json!({"curl_success":false,"curl_exit":28,"protocol":"HTTP/1.1","status":200,"body":"abc","length":7,"transfer":null}); + assert!(validate(&truncated, "axum", "short-stream", false).is_err()); + truncated["curl_exit"] = json!(18); + assert!(validate(&truncated, "axum", "short-stream", false).is_ok()); + truncated["protocol"] = json!("HTTP/2"); + truncated["curl_exit"] = json!(92); + assert!(validate(&truncated, "fastly", "short-stream", true).is_ok()); + let headless = + json!({"curl_success":false,"curl_exit":92,"head_artifact":{"bytes":0},"body":""}); + assert!(validate(&headless, "fastly", "short-stream", true).is_ok()); + let mut stalled = headless; + stalled["curl_exit"] = json!(28); + assert!(validate(&stalled, "fastly", "short-stream", true).is_err()); + let reference = json!({"curl_success":true,"protocol":"HTTP/1.1","status":200,"body":"abcdef","length":6,"transfer":null}); + let mut candidate = reference.clone(); + candidate["curl_success"] = json!(false); + assert!(validate_sdk_parity(&candidate, &reference).is_err()); + } + + #[test] + fn forbidden_204_framing_is_rejected_even_on_fastly() { + for (length, transfer) in [ + (json!(0), Value::Null), + (json!(6), Value::Null), + (Value::Null, json!("chunked")), + ] { + let response = json!({"curl_success":true,"protocol":"HTTP/1.1","status":204,"body":"","length":length,"transfer":transfer}); + assert!(validate(&response, "fastly", "no-content", false).is_err()); + } + } + + #[test] + fn bodyless_wire_validation_distinguishes_host_framing_and_unobserved_head_body() { + let mut response = json!({"curl_success":true,"protocol":"HTTP/1.1","status":205,"body":"","length":0,"transfer":null}); + assert!(validate(&response, "axum", "reset-content", false).is_ok()); + response["length"] = json!(6); + assert!(validate(&response, "axum", "reset-content", false).is_err()); + response = json!({"curl_success":true,"protocol":"HTTP/1.1","status":200,"body":null,"body_observation":"header-only","length":6,"transfer":null}); + assert!(validate(&response, "axum", "head-explicit", false).is_ok()); + response["body"] = json!(""); + assert!(validate(&response, "axum", "head-explicit", false).is_err()); + response = json!({"curl_success":true,"protocol":"HTTP/2","status":304,"body":"","length":0,"transfer":null}); + assert!(validate(&response, "fastly", "not-modified-explicit", true).is_err()); + } + + #[test] + fn parser_rejects_ambiguous_or_multiple_header_sections() { + for text in [ + "HTTP/1.1 200 OK\r\nContent-Length: 6\r\nContent-Length: 6\r\n\r\n", + "HTTP/1.1 200 OK\r\nContent-Length: 6\r\nTransfer-Encoding: chunked\r\n\r\n", + "HTTP/1.1 200 OK\r\n\r\nHTTP/1.1 200 OK\r\n\r\n", + "HTTP/2 200\r\ncontent-length: 6\r\n", + ] { + assert!(parse_head(text.as_bytes()).is_err()); + } + assert_eq!( + parse_head(b"HTTP/2 200\r\ncontent-length: 6\r\n\r\n").unwrap()["length"], + 6 + ); + } + + #[test] + fn failed_and_head_only_captures_retain_artifacts_without_claiming_payload() { + let run = std::env::temp_dir().join(format!("framing-capture-{}", token())); + fs::create_dir_all(&run).unwrap(); + let headers = run.join("capture.headers"); + let body = run.join("capture.body"); + fs::write(&headers, b"HTTP/2 200\r\ncontent-length: 6\r\n\r\n").unwrap(); + fs::write(&body, b"abc").unwrap(); + let failed = capture( + false, + &headers, + &body, + false, + b"controlled transport failure", + ) + .unwrap(); + assert_eq!(failed["curl_success"], false); + assert_eq!(failed["body"], "abc"); + assert_eq!(failed["capture_artifact"]["bytes"], 3); + assert!(failed["diagnostic_artifact"]["bytes"].as_u64().unwrap() > 0); + assert!(validate(&failed, "fastly", "buffered", true).is_err()); + let head = capture(true, &headers, &body, true, b"").unwrap(); + assert!(head["body"].is_null()); + assert_eq!(head["body_observation"], "header-only"); + fs::write(&headers, b"HTTP/1.1 304 Not Modified\r\n\r\n").unwrap(); + fs::write(&body, []).unwrap(); + let empty = capture(false, &headers, &body, true, b"").unwrap(); + assert_eq!(empty["capture_artifact"]["bytes"], 0); + assert!(validate(&empty, "axum", "not-modified", false).is_ok()); + fs::remove_dir_all(run).unwrap(); + } +} diff --git a/crates/edgezero-test-harness/src/runners/lifetimes.rs b/crates/edgezero-test-harness/src/runners/lifetimes.rs new file mode 100644 index 000000000..c7913511c --- /dev/null +++ b/crates/edgezero-test-harness/src/runners/lifetimes.rs @@ -0,0 +1,776 @@ +use super::*; +use crate::net::WireOrigins; + +const CASES: [&str; 4] = [ + "slow-origin", + "fast-origin", + "begin-progress", + "batch-progress", +]; + +pub(super) fn run(name: &str, run: &Path, records: &mut Records) -> Result<()> { + let count = wire::progress_suite(name, "outbound-lifetimes", run, records, checks)?; + if let Some(count) = count { + records.push(json!({"event":"lifetime-inventory","adapter":name, + "status":"pass","cases":count,"qualification":false}))?; + } + Ok(()) +} + +fn checks(port: u16, origins: &WireOrigins, records: &mut Records) -> Result { + let mut failures = Vec::new(); + for case in CASES { + let token = token(); + origins.register_lifetime(&token, case)?; + let backend = format!( + "http://127.0.0.1:{}/lifetimes/{token}/{case}", + origins.port(0) + ); + origins.mark_lifetime_start(&token)?; + let reply = net::header_request( + port, + &format!("/progress/lifetimes/{case}"), + &token, + &[("x-fixture-backend", &backend)], + ); + let origin = origins.take_lifetime_events(&token)?; + let mut report = Value::Null; + let result = (|| -> Result<()> { + let reply = reply.as_ref().map_err(|error| error.to_string())?; + require( + reply["status"] == 200, + "lifetime guest returned an error response", + )?; + let bytes: Vec = serde_json::from_value(reply["body_bytes"].clone())?; + report = serde_json::from_slice(&bytes)?; + validate(case, &token, &report, &origin) + })(); + records.push(json!({"event":"lifetime-case","case":case,"token":token, + "status":if result.is_ok(){"pass"}else{"fail"}, + "error":result.as_ref().err().map(ToString::to_string),"report":report, + "origin":origin,"client":reply.as_ref().ok().map(|reply| json!({ + "status":reply["status"],"complete_ns":reply["complete_ns"], + "header_ns":reply["header_ns"],"headers":reply["headers"]})),"qualification":false}))?; + if let Err(error) = result { + failures.push(format!("{case}: {error}")); + } + } + require(failures.is_empty(), failures.join("; "))?; + Ok(CASES.len()) +} + +fn validate(case: &str, token: &str, report: &Value, origin: &Value) -> Result<()> { + require( + report["schema"] == 1 + && report["case"] == case + && report["token"] == token + && report["qualification"] == false + && report.get("error") == Some(&Value::Null), + "lifetime report identity or error mismatch", + )?; + let events = report["events"] + .as_array() + .ok_or("missing guest lifetime events")?; + for entry in origin.as_array().ok_or("missing host lifetime events")? { + require( + entry["token"] == token && entry["case"] == case, + "foreign host lifetime event", + )?; + } + for check in events.iter().filter(|entry| entry["op"] == "check") { + require( + number(check, "duration_us")? < 5_000, + "nonblocking batch check stalled", + )?; + } + for pair in events.windows(2) { + require( + number(&pair[0], "at_us")? <= number(&pair[1], "at_us")?, + "guest lifetime events are reordered", + )?; + } + let receipts: Vec<_> = origin + .as_array() + .ok_or("missing host lifetime events")? + .iter() + .filter(|event| event["op"] == "request") + .collect(); + let mut roles: Vec<_> = receipts + .iter() + .map(|entry| entry["role"].as_str().ok_or("missing role")) + .collect::>()?; + roles.sort_unstable(); + let expected: &[&str] = match case { + "slow-origin" => &["a", "b", "c", "origin"], + "fast-origin" => &["a", "b", "c", "mediator", "origin"], + "begin-progress" => &["origin"], + "batch-progress" => &["a", "b", "c", "quiet"], + _ => return Err("unknown lifetime case".into()), + }; + require( + roles == expected, + "missing, duplicate or foreign origin receipt", + )?; + if case == "begin-progress" || case == "batch-progress" { + let start = number(event(events, "quiet-start")?, "at_us")?; + let end = number(event(events, "quiet-end")?, "at_us")?; + let budget = if case == "begin-progress" { + 100_000 + } else { + 300_000 + }; + require( + end.saturating_sub(start) >= budget, + "unobserved quiet window was too short", + )?; + let lower = clock_offset_lower(case, events, origin)?; + for receipt in &receipts { + require( + i128::from(number(receipt, "at_us")?) <= lower + i128::from(end), + "request did not reach origin before unrelated operation completed", + )?; + } + for observation in events + .iter() + .filter(|event| matches!(event["op"].as_str(), Some("check" | "next" | "collect"))) + { + require( + number(observation, "at_us")? >= end, + "batch was observed during quiet window", + )?; + } + if case == "begin-progress" { + let begin = event(events, "begin")?; + require( + number(begin, "at_us")? + number(begin, "duration_us")? <= start, + "begin was not awaited before quiet window", + )?; + require( + report["origin"] == json!({"status":200,"bytes":[79]}), + "begun response changed", + )?; + require( + report["items"] == json!([]) + && report["slots"] == json!([]) + && report["termination"].is_null(), + "begin unexpectedly observed a batch", + )?; + return Ok(()); + } + let batch = event(events, "batch-start")?; + require( + number(batch, "at_us")? + number(batch, "duration_us")? <= start, + "batch was not started before quiet window", + )?; + validate_items( + report, + &[], + &["collect", "collect", "collect"], + &["Resolved", "Resolved", "Resolved"], + "Completed", + )?; + let batch_start = number(batch, "at_us")?; + for (index, item) in report["items"].as_array().unwrap().iter().enumerate() { + let role = ["a", "b", "c"][index]; + let chunk = origin + .as_array() + .unwrap() + .iter() + .find(|entry| entry["op"] == "chunk" && entry["role"] == role) + .ok_or("missing bidder write receipt")?; + let upper = + i128::from(number(chunk, "at_us")?) - lower - i128::from(batch_start) + 5_000; + require( + i128::from(number(item, "elapsed_us")?) <= upper + && number(item, "elapsed_us")? < end - start, + "batch elapsed was stamped at collection instead of arrival", + )?; + } + return Ok(()); + } + within( + number(event(events, "begin")?, "at_us")?, + 2_000, + "origin begin missed 2 ms", + )?; + within( + number(event(events, "batch-start")?, "at_us")?, + 34_000, + "batch start missed 34 ms", + )?; + validate_timeline(case, events, origin)?; + if case == "slow-origin" { + let check = events + .iter() + .find(|entry| entry["op"] == "check") + .ok_or("missing first batch check")?; + within( + number(check, "at_us")?, + 701_000, + "first batch check missed 701 ms", + )?; + require( + report["origin"] == json!({"status":200,"bytes":[79,111,33]}), + "slow origin body changed", + )?; + validate_items( + report, + &[146_000, 286_000], + &["check", "check"], + &["AlreadyDelivered", "AlreadyDelivered", "Unresolved"], + "Cutoff", + ) + } else { + within( + number(event(events, "check")?, "at_us")?, + 260_000, + "batch check missed origin EOF", + )?; + for (op, expected) in [("next", 320_000), ("collect", 400_000)] { + let observed = event(events, op)?; + within( + number(observed, "at_us")? + number(observed, "duration_us")?, + expected, + "batch observation missed scenario completion", + )?; + } + require( + report["origin"] == json!({"status":200,"bytes":[79]}), + "fast origin body changed", + )?; + validate_items( + report, + &[146_000, 286_000, 366_000], + &["check", "next", "collect"], + &["AlreadyDelivered", "AlreadyDelivered", "Resolved"], + "Completed", + )?; + let mediator = &report["mediator"]; + within( + number(mediator, "sent_at_us")?, + 400_000, + "mediator did not start at 400 ms", + )?; + within( + number(mediator, "remaining_us")?, + 134_000, + "mediator did not retain original cutoff", + )?; + require( + mediator["error"] + == json!({"kind":"gateway_timeout","status":504,"reason":"RequestDeadline"}), + "mediator lost typed request-deadline cause", + )?; + let send = event(events, "mediator-send")?; + within( + number(send, "at_us")? + number(send, "duration_us")?, + 534_000, + "mediator deadline was extended", + ) + } +} + +fn number(value: &Value, field: &str) -> Result { + value[field] + .as_u64() + .filter(|value| *value <= 30_000_000) + .ok_or_else(|| format!("missing measured {field}").into()) +} + +fn clock_offset_lower(case: &str, events: &[Value], origin: &Value) -> Result { + let (role, observed) = if case == "begin-progress" { + let head = event(events, "origin-head")?; + ( + "origin", + number(head, "at_us")? + number(head, "duration_us")?, + ) + } else { + ("quiet", number(event(events, "quiet-end")?, "at_us")?) + }; + let heads: Vec<_> = origin + .as_array() + .unwrap() + .iter() + .filter(|entry| entry["op"] == "head" && entry["role"] == role) + .collect(); + require( + heads.len() == 1, + "missing or duplicate quiet clock boundary", + )?; + let started = number(heads[0], "write_started_us")?; + require( + started <= number(heads[0], "at_us")?, + "head write started after completion", + )?; + // Sending precedes guest observation. This lower bound survives cold-entry offsets; + // the measurement guard makes receipt-within-window checks more conservative. + Ok(i128::from(started) - i128::from(observed) - 5_000) +} + +fn event<'a>(events: &'a [Value], op: &str) -> Result<&'a Value> { + let matching: Vec<_> = events.iter().filter(|event| event["op"] == op).collect(); + require( + matching.len() == 1, + format!("missing or duplicate {op} event"), + )?; + Ok(matching[0]) +} + +fn within(actual: u64, expected: u64, message: &str) -> Result<()> { + require( + actual.abs_diff(expected) <= 5_000, + format!("{message}: measured {actual} us, expected {expected} +/- 5000 us"), + ) +} + +fn validate_items( + report: &Value, + elapsed: &[u64], + via: &[&str], + slots: &[&str], + termination: &str, +) -> Result<()> { + require( + report["slots"] == json!(slots) && report["termination"] == termination, + "wrong lifetime collection states or termination", + )?; + let items = report["items"].as_array().ok_or("missing lifetime items")?; + require(items.len() == via.len(), "missing or extra lifetime items")?; + for (index, item) in items.iter().enumerate() { + require( + item["index"] == index + && item["via"] == via[index] + && item["outcome"] == json!({"status":200,"bytes":[b'A' + index as u8]}), + "wrong lifetime item identity, observation or outcome", + )?; + if let Some(expected) = elapsed.get(index) { + within( + number(item, "elapsed_us")?, + *expected, + "slot elapsed did not record arrival", + )?; + } + } + Ok(()) +} + +fn validate_timeline(case: &str, events: &[Value], origin: &Value) -> Result<()> { + let slow = case == "slow-origin"; + let head = event(events, "origin-head")?; + within( + number(head, "at_us")? + number(head, "duration_us")?, + if slow { 700_000 } else { 120_000 }, + "origin head missed scenario time", + )?; + let body = event(events, "origin-body")?; + within( + number(body, "at_us")? + number(body, "duration_us")?, + if slow { 1_300_000 } else { 260_000 }, + "origin EOF missed scenario time", + )?; + let origin = origin.as_array().unwrap(); + let request = origin + .iter() + .find(|entry| entry["op"] == "request" && entry["role"] == "origin") + .ok_or("missing origin receipt")?; + require( + request["alignment"]["basis"] == "origin-receipt-minus-guest-at-us" + && request["alignment"]["clock_sync_proven"] == false + && request["alignment"].get("transit_uncertainty_us") == Some(&Value::Null), + "scenario alignment must disclose unproven clock synchronization", + )?; + let aligned = request["alignment"]["scenario_anchor_host_offset_us"] + .as_i64() + .filter(|value| value.unsigned_abs() <= 30_000_000) + .ok_or("missing measured scenario alignment")?; + let aligned = i128::from(aligned); + require( + (i128::from(number(request, "at_us")?) + - i128::from(number(request, "guest_at_us")?) + - aligned) + .abs() + <= 1, + "scenario alignment differs from original origin receipt", + )?; + for (role, head_at, chunk_times, bytes) in [ + ( + "origin", + if slow { 700_000 } else { 120_000 }, + if slow { + vec![700_000, 900_000, 1_300_000] + } else { + vec![260_000] + }, + if slow { b"Oo!".as_slice() } else { b"O" }, + ), + ("a", 180_000, vec![180_000], b"A"), + ("b", 320_000, vec![320_000], b"B"), + ] + .into_iter() + .chain((!slow).then_some(("c", 400_000, vec![400_000], b"C".as_slice()))) + { + let heads: Vec<_> = origin + .iter() + .filter(|entry| entry["op"] == "head" && entry["role"] == role) + .collect(); + let chunks: Vec<_> = origin + .iter() + .filter(|entry| entry["op"] == "chunk" && entry["role"] == role) + .collect(); + require( + heads.len() == 1 && chunks.len() == chunk_times.len(), + "missing or extra origin writes", + )?; + for (entry, scheduled) in std::iter::once((heads[0], head_at)).chain( + chunks + .iter() + .zip(chunk_times) + .map(|(entry, time)| (*entry, time)), + ) { + require( + number(entry, "scenario_scheduled_us")? == scheduled + && entry["schedule_basis"] == "origin-receipt-minus-guest-at-us", + "wrong origin scenario schedule", + )?; + let actual = number(entry, "at_us")?; + let scheduled = number(entry, "scheduled_us")?; + require( + (i128::from(scheduled) + - aligned + - i128::from(number(entry, "scenario_scheduled_us")?)) + .abs() + <= 1, + "host schedule was reanchored after registration", + )?; + require(actual >= scheduled, "origin wrote before scheduled time")?; + within(actual, scheduled, "origin scheduling target missed")?; + } + for (entry, byte) in chunks.iter().zip(bytes) { + require(entry["bytes"] == json!([byte]), "origin chunk bytes differ")?; + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample(case: &str) -> (Value, Value) { + let mut report = json!({"schema":1,"case":case,"token":"sample","qualification":false, + "events":[],"items":[],"slots":[],"termination":null,"error":null, + "origin":null,"mediator":null}); + let mut origin = json!([]); + if case == "begin-progress" { + report["events"] = json!([ + {"op":"begin","at_us":2000,"duration_us":100}, + {"op":"quiet-start","at_us":2100,"duration_us":0}, + {"op":"quiet-end","at_us":102100,"duration_us":0}, + {"op":"origin-head","at_us":102100,"duration_us":47900} + ]); + report["origin"] = json!({"status":200,"bytes":[79]}); + origin = json!([ + {"op":"request","role":"origin","at_us":3000,"guest_at_us":2000}, + {"op":"head","role":"origin","at_us":151000,"write_started_us":151000} + ]); + } else if case == "batch-progress" { + report["events"] = json!([ + {"op":"batch-start","at_us":34000,"duration_us":100}, + {"op":"quiet-start","at_us":34100,"duration_us":0}, + {"op":"quiet-end","at_us":335000,"duration_us":0} + ]); + report["slots"] = json!(["Resolved", "Resolved", "Resolved"]); + report["termination"] = json!("Completed"); + report["items"] = json!([ + {"index":0,"via":"collect","elapsed_us":20000,"outcome":{"status":200,"bytes":[65]}}, + {"index":1,"via":"collect","elapsed_us":40000,"outcome":{"status":200,"bytes":[66]}}, + {"index":2,"via":"collect","elapsed_us":60000,"outcome":{"status":200,"bytes":[67]}} + ]); + origin = json!([ + {"op":"request","role":"a","at_us":35000,"guest_at_us":34000}, + {"op":"request","role":"b","at_us":35010,"guest_at_us":34000}, + {"op":"request","role":"c","at_us":35020,"guest_at_us":34000}, + {"op":"request","role":"quiet","at_us":35100,"guest_at_us":34100}, + {"op":"head","role":"quiet","at_us":336000,"write_started_us":336000}, + {"op":"chunk","role":"a","at_us":55000}, + {"op":"chunk","role":"b","at_us":75000}, + {"op":"chunk","role":"c","at_us":95000} + ]); + } else { + let slow = case == "slow-origin"; + report["events"] = json!([ + {"op":"begin","at_us":2000,"duration_us":100}, + {"op":"batch-start","at_us":34000,"duration_us":100}, + {"op":"check","at_us":if slow {701000}else{260000},"duration_us":10} + ]); + report["items"] = json!([ + {"index":0,"via":"check","elapsed_us":146000,"outcome":{"status":200,"bytes":[65]}}, + {"index":1,"via":if slow{"check"}else{"next"},"elapsed_us":286000,"outcome":{"status":200,"bytes":[66]}} + ]); + report["slots"] = json!([ + "AlreadyDelivered", + "AlreadyDelivered", + if slow { "Unresolved" } else { "Resolved" } + ]); + report["termination"] = json!(if slow { "Cutoff" } else { "Completed" }); + report["origin"] = + json!({"status":200,"bytes":if slow {vec![79,111,33]} else {vec![79]}}); + origin = json!([ + {"op":"request","role":"origin","at_us":3000,"guest_at_us":2000}, + {"op":"request","role":"a","at_us":35000,"guest_at_us":34000}, + {"op":"request","role":"b","at_us":35010,"guest_at_us":34000}, + {"op":"request","role":"c","at_us":35020,"guest_at_us":34000} + ]); + if !slow { + report["items"].as_array_mut().unwrap().push(json!( + {"index":2,"via":"collect","elapsed_us":366000,"outcome":{"status":200,"bytes":[67]}})); + report["events"].as_array_mut().unwrap().push(json!( + {"op":"mediator-send","at_us":400000,"duration_us":134000})); + report["mediator"] = json!({"sent_at_us":400000,"remaining_us":134000, + "error":{"kind":"gateway_timeout","status":504,"reason":"RequestDeadline"}}); + origin.as_array_mut().unwrap().push(json!( + {"op":"request","role":"mediator","at_us":401000,"guest_at_us":400000})); + } + report["events"].as_array_mut().unwrap().extend(if slow { + vec![json!({"op":"collect","at_us":701010,"duration_us":1})] + } else { + vec![ + json!({"op":"next","at_us":260010,"duration_us":59990}), + json!({"op":"collect","at_us":320000,"duration_us":80000}), + ] + }); + report["events"].as_array_mut().unwrap().extend([ + json!({"op":"origin-head","at_us":34000,"duration_us":if slow{666000}else{86000}}), + json!({"op":"origin-body","at_us":if slow{701000}else{120000}, + "duration_us":if slow{599000}else{140000}}), + ]); + origin[0]["alignment"] = json!({"basis":"origin-receipt-minus-guest-at-us", + "clock_sync_proven":false,"transit_uncertainty_us":null,"scenario_anchor_host_offset_us":1000}); + let scripts = vec![ + ( + "origin", + if slow { 700000 } else { 120000 }, + if slow { + vec![(700000, 79), (900000, 111), (1300000, 33)] + } else { + vec![(260000, 79)] + }, + ), + ("a", 180000, vec![(180000, 65)]), + ("b", 320000, vec![(320000, 66)]), + ]; + for (role, head, chunks) in + scripts + .into_iter() + .chain((!slow).then_some(("c", 400000, vec![(400000, 67)]))) + { + origin + .as_array_mut() + .unwrap() + .push(json!({"op":"head","role":role, + "at_us":head+1000,"scheduled_us":head+1000,"scenario_scheduled_us":head, + "schedule_basis":"origin-receipt-minus-guest-at-us"})); + for (time, byte) in chunks { + origin.as_array_mut().unwrap().push( + json!({"op":"chunk","role":role,"bytes":[byte], + "at_us":time+1000,"scheduled_us":time+1000,"scenario_scheduled_us":time, + "schedule_basis":"origin-receipt-minus-guest-at-us"}), + ); + } + } + } + report["events"] + .as_array_mut() + .unwrap() + .sort_by_key(|entry| number(entry, "at_us").unwrap()); + for entry in origin.as_array_mut().unwrap() { + entry["token"] = json!("sample"); + entry["case"] = json!(case); + } + (report, origin) + } + + #[test] + fn progress_evidence_requires_actual_receipts_inside_unobserved_windows() { + for case in ["begin-progress", "batch-progress"] { + let (report, origin) = sample(case); + validate(case, "sample", &report, &origin).unwrap(); + let mut late = origin.clone(); + late[0]["at_us"] = json!(999000); + assert!(validate(case, "sample", &report, &late).is_err()); + assert!(validate(case, "sample", &report, &json!([])).is_err()); + let mut duplicate = origin.clone(); + duplicate.as_array_mut().unwrap().push(origin[0].clone()); + assert!(validate(case, "sample", &report, &duplicate).is_err()); + } + } + + #[test] + fn report_identity_and_terminal_states_cannot_be_guessed() { + for case in ["begin-progress", "batch-progress"] { + let (report, origin) = sample(case); + for (field, value) in [ + ("schema", json!(2)), + ("case", json!("other")), + ("token", json!("foreign")), + ("qualification", json!(true)), + ("error", json!({"kind":"internal"})), + ("events", json!([])), + ] { + let mut invalid = report.clone(); + invalid[field] = value; + assert!( + validate(case, "sample", &invalid, &origin).is_err(), + "{case} {field}" + ); + } + } + let (report, origin) = sample("batch-progress"); + for (field, value) in [ + ("termination", json!("Cutoff")), + ("slots", json!(["Resolved", "Unresolved", "Resolved"])), + ("items", json!([])), + ] { + let mut invalid = report.clone(); + invalid[field] = value; + assert!(validate("batch-progress", "sample", &invalid, &origin).is_err()); + } + } + + #[test] + fn slow_and_fast_origin_reject_collection_time_elapsed_and_renewed_deadlines() { + for case in ["slow-origin", "fast-origin"] { + let (report, origin) = sample(case); + validate(case, "sample", &report, &origin).unwrap(); + for index in 0..2 { + let mut late = report.clone(); + late["items"][index]["elapsed_us"] = json!(667000); + assert!(validate(case, "sample", &late, &origin).is_err()); + let mut wrong = report.clone(); + wrong["items"][index]["index"] = json!(2); + assert!(validate(case, "sample", &wrong, &origin).is_err()); + } + let mut states = report.clone(); + states["slots"][0] = json!("Unresolved"); + assert!(validate(case, "sample", &states, &origin).is_err()); + } + let (report, origin) = sample("fast-origin"); + for (field, value) in [ + ("remaining_us", json!(1000000)), + ("sent_at_us", json!(534000)), + ( + "error", + json!({"kind":"gateway_timeout","status":504,"reason":"PerCallTimeout"}), + ), + ] { + let mut invalid = report.clone(); + invalid["mediator"][field] = value; + assert!(validate("fast-origin", "sample", &invalid, &origin).is_err()); + } + let mut extended = report; + extended["events"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|entry| entry["op"] == "mediator-send") + .unwrap()["duration_us"] = json!(1000000); + assert!(validate("fast-origin", "sample", &extended, &origin).is_err()); + } + + #[test] + fn timeline_evidence_cannot_omit_heads_or_replace_actual_times_with_schedules() { + for case in ["slow-origin", "fast-origin"] { + let (report, origin) = sample(case); + validate(case, "sample", &report, &origin).unwrap(); + for op in ["head", "chunk"] { + let mut missing = origin.clone(); + missing + .as_array_mut() + .unwrap() + .retain(|entry| entry["op"] != op); + assert!( + validate(case, "sample", &report, &missing).is_err(), + "missing {op}" + ); + } + let mut late = origin.clone(); + let head = late + .as_array_mut() + .unwrap() + .iter_mut() + .find(|entry| entry["op"] == "head") + .unwrap(); + head["at_us"] = json!(number(head, "scheduled_us").unwrap() + 6000); + assert!(validate(case, "sample", &report, &late).is_err()); + let mut reanchored = origin.clone(); + for entry in reanchored.as_array_mut().unwrap() { + if entry.get("scheduled_us").is_some() { + entry["at_us"] = json!(number(entry, "at_us").unwrap() + 10_000); + entry["scheduled_us"] = json!(number(entry, "scheduled_us").unwrap() + 10_000); + } + } + assert!(validate(case, "sample", &report, &reanchored).is_err()); + } + } + + #[test] + fn quiet_window_proof_survives_cold_entry_clock_offsets() { + for case in ["begin-progress", "batch-progress"] { + let (report, origin) = sample(case); + validate(case, "sample", &report, &origin).unwrap(); + for offset in [100_000, 1_000_000] { + let mut shifted = origin.clone(); + for entry in shifted.as_array_mut().unwrap() { + for field in ["at_us", "write_started_us"] { + if let Some(value) = entry[field].as_u64() { + entry[field] = json!(value + offset); + } + } + } + validate(case, "sample", &report, &shifted).unwrap(); + } + } + } + + #[test] + fn quiet_proof_rejects_late_receipts_and_collection_stamps_even_after_cold_entry() { + for case in ["begin-progress", "batch-progress"] { + let (report, origin) = sample(case); + for shift in [0, 1_000_000] { + let mut shifted = origin.clone(); + for entry in shifted.as_array_mut().unwrap() { + for field in ["at_us", "write_started_us"] { + if let Some(value) = entry[field].as_u64() { + entry[field] = json!(value + shift); + } + } + } + let events = report["events"].as_array().unwrap(); + let lower = clock_offset_lower(case, events, &shifted).unwrap(); + let end = number(event(events, "quiet-end").unwrap(), "at_us").unwrap(); + let mut threshold = shifted.clone(); + threshold[0]["at_us"] = json!(u64::try_from(lower + i128::from(end)).unwrap()); + validate(case, "sample", &report, &threshold).unwrap(); + threshold[0]["at_us"] = json!(number(&threshold[0], "at_us").unwrap() + 1); + assert!(validate(case, "sample", &report, &threshold).is_err()); + let mut invalid = shifted.clone(); + invalid[0]["token"] = json!("foreign"); + assert!(validate(case, "sample", &report, &invalid).is_err()); + if case == "batch-progress" { + let mut observed = report.clone(); + observed["items"][0]["elapsed_us"] = json!(300_000); + assert!(validate(case, "sample", &observed, &shifted).is_err()); + } + let mut recorded_late = shifted; + let head = recorded_late + .as_array_mut() + .unwrap() + .iter_mut() + .find(|entry| entry["op"] == "head") + .unwrap(); + head["at_us"] = json!(number(head, "at_us").unwrap() + 50_000); + validate(case, "sample", &report, &recorded_late).unwrap(); + } + } + } +} diff --git a/crates/edgezero-test-harness/src/runners/overflow.rs b/crates/edgezero-test-harness/src/runners/overflow.rs new file mode 100644 index 000000000..de031e152 --- /dev/null +++ b/crates/edgezero-test-harness/src/runners/overflow.rs @@ -0,0 +1,414 @@ +use super::*; +use crate::net::overflow::{self as wire, UploadOrigin}; + +pub(super) fn run(name: &str, parent: &Path, records: &mut Records) -> Result<()> { + let run = parent.join(format!("overflow-{name}")); + fs::create_dir_all(&run)?; + let revision = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(root()) + .output()?; + require( + revision.status.success(), + "missing overflow source revision", + )?; + let revision = String::from_utf8(revision.stdout)?.trim().to_owned(); + let Some((mut command, port)) = + progress::build(name, &run, records, &revision, "outbound-overflow")? + else { + return Ok(()); + }; + let log = run.join("runtime.log"); + let outcome = Process::start(&mut command, &log, port).and_then(|_process| { + let checked = checks(name, port, records); + let panic = fs::read_to_string(&log)?.contains("worker failed: worker panicked"); + records.push(json!({"event":"overflow-before-shutdown","adapter":name,"worker_panicked":panic,"qualification":false}))?; + checked.and(require(!panic, "runtime worker panicked during overflow probes")) + }); + if name == "spin" { + for file in ["fixture_stdout.txt", "fixture_stderr.txt"] { + for event in logs(&run.join(".spin/logs").join(file)) { + records.push(annotate( + event, + json!({"adapter":name,"diagnostic_source":file}), + ))?; + } + } + } + settle( + records, + &log, + json!({"adapter":name,"suite":"outbound-overflow"}), + json!({"phase":"outbound-overflow"}), + outcome, + ) +} + +fn validate_origin(snapshot: &Value, adapter: &str, token: &str, case: &str) -> Result<()> { + require( + snapshot["collection_complete"] == true && snapshot["overflow"] == false, + "origin collection incomplete or exceeded connection bound", + )?; + let connections = snapshot["connections"] + .as_array() + .ok_or("missing origin receipts")?; + require( + snapshot["accepted"].as_u64() == Some(connections.len() as u64), + "origin accepted/terminal count mismatch", + )?; + let over = case.ends_with("-9"); + if over && (case.starts_with("buffered") || matches!(adapter, "axum" | "cloudflare")) { + return require( + connections.is_empty(), + "over-cap upload contacted the origin before rejection", + ); + } + if over && connections.is_empty() { + return Ok(()); + } + require( + connections.len() == 1, + "missing or duplicate upload origin request", + )?; + let receipt = &connections[0]; + if over && receipt["head"].is_null() { + return require( + receipt.get("head") == Some(&Value::Null) + && receipt["head_state"] == "absent" + && receipt["head_bytes"] == 0 + && receipt["body_bytes"] == 0 + && receipt["body_prefix"] == json!([]) + && matches!(receipt["terminal"].as_str(), Some("eof" | "reset")), + "headless contact lacks an explicit zero-byte abort witness", + ); + } + require( + receipt["head_state"] == "complete" + && receipt["head_bytes"] + .as_u64() + .is_some_and(|bytes| bytes > 0 && bytes <= 16 * 1024) + && receipt["head"]["method"] == "POST" + && receipt["head"]["target"] == format!("/overflow/{token}/{case}"), + "foreign or headless upload origin receipt", + )?; + let length = receipt["body_bytes"] + .as_u64() + .ok_or("missing upload byte count")?; + let bytes = receipt["body_prefix"] + .as_array() + .ok_or("missing bounded upload bytes")?; + require( + length == bytes.len() as u64 + && bytes + .iter() + .all(|byte| byte.as_u64() == Some(u64::from(b'x'))), + "upload content/count mismatch", + )?; + if over { + return require( + length <= 8 && matches!(receipt["terminal"].as_str(), Some("eof" | "reset")), + "streamed overflow completed a truncated request or lacks abort evidence", + ); + } + let expected = if case.ends_with("-7") { 7 } else { 8 }; + require( + receipt["terminal"] == "complete" && length == expected && receipt["reply_written"] == true, + "valid upload did not complete exactly", + ) +} + +fn validate_response(reply: &Value, case: &str, len: usize) -> Result<()> { + let overflow = (case == "inbound" && len > 1024) || case.ends_with("-9"); + let expected = if overflow { 413 } else { 200 }; + require( + reply["status"] == expected, + "overflow probe status mismatch", + )?; + let headers = reply["headers"] + .as_array() + .ok_or("missing response headers")?; + require( + !headers.iter().any(|field| { + field[0] + .as_str() + .is_some_and(|name| name.eq_ignore_ascii_case("retry-after")) + }), + "generated overflow/probe response gained Retry-After", + )?; + let report: Value = + serde_json::from_str(reply["body"].as_str().ok_or("missing response body")?)?; + if overflow { + return require( + report + == json!({"error":{"status":413,"kind":"payload_too_large","message":"request body too large"}}), + "generated overflow lost its fixed public category/body", + ); + } + if case == "inbound" { + return require( + report["case"] == case && report["received"] == len, + "inbound cap boundary byte count mismatch", + ); + } + require( + report["case"] == case + && report["outcome"] == "response" + && report["upstream_status"] == if case == "upstream-413" { 413 } else { 200 }, + "upstream status was not an ordinary response", + )?; + require( + if case == "upstream-413" { + report["retry_after"] == "120" + } else { + report["retry_after"].is_null() + }, + "ordinary upstream Retry-After changed", + ) +} + +fn check_inbound(adapter: &str, port: u16, records: &mut Records, len: usize) -> Result { + let token = token(); + let response = wire::request(port, "inbound", &token, None, Some(len)); + let validation = response + .as_ref() + .map_err(|error| error.to_string().into()) + .and_then(|reply| validate_response(reply, "inbound", len)); + records.push(json!({"event":"overflow-case","adapter":adapter,"case":format!("inbound-{len}"),"token":token, + "client":response.as_ref().ok(),"status":if validation.is_ok(){"pass"}else{"fail"}, + "error":validation.as_ref().err().map(ToString::to_string),"qualification":false}))?; + Ok(validation.is_ok()) +} + +fn checks(adapter: &str, port: u16, records: &mut Records) -> Result<()> { + let mut failed = false; + for len in [1023_usize, 1024, 1025] { + failed |= !check_inbound(adapter, port, records, len)?; + } + for case in [ + "buffered-7", + "buffered-8", + "buffered-9", + "streamed-7", + "streamed-8", + "streamed-9", + "upstream-413", + ] { + let token = token(); + let origin = UploadOrigin::start()?; + let response = wire::request(port, case, &token, Some(&origin.url(&token, case)), None); + // Always close and join the origin collector, including failed client operations. + let snapshot = origin.finish(); + let validation = (|| -> Result<()> { + let reply = response.as_ref().map_err(|error| error.to_string())?; + let receipt = snapshot.as_ref().map_err(|error| error.to_string())?; + validate_response(reply, case, 0)?; + validate_origin(receipt, adapter, &token, case) + })(); + records.push(json!({"event":"overflow-case","adapter":adapter,"case":case,"token":token, + "client":response.as_ref().ok(),"origin":snapshot.as_ref().ok(), + "origin_collection_error":snapshot.as_ref().err().map(ToString::to_string), + "status":if validation.is_ok(){"pass"}else{"fail"},"error":validation.as_ref().err().map(ToString::to_string),"qualification":false}))?; + failed |= validation.is_err(); + } + // Preserve the recorded overflow-to-upload recovery sequence before this exact EZ-10 case. + failed |= !check_inbound(adapter, port, records, 2048)?; + records.push( + json!({"event":"overflow-coverage","adapter":adapter,"attempted":11,"qualification":false, + "status":if failed{"fail"}else{"pass"}}), + )?; + require(!failed, "one or more overflow probes failed") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn two_kib_ingress_requires_fixed_overflow_without_retry_after() { + let reply = json!({"status":413,"headers":[],"body":serde_json::to_string(&json!({ + "error":{"status":413,"kind":"payload_too_large","message":"request body too large"} + })).unwrap()}); + validate_response(&reply, "inbound", 2048).unwrap(); + let mut invalid = reply.clone(); + invalid["status"] = json!(200); + assert!(validate_response(&invalid, "inbound", 2048).is_err()); + invalid = reply.clone(); + invalid["headers"] = json!([["ReTrY-AfTeR", "120"]]); + assert!(validate_response(&invalid, "inbound", 2048).is_err()); + invalid = reply; + invalid["body"] = json!( + serde_json::to_string(&json!({"error":{"status":413, + "kind":"payload_too_large","message":"provider diagnostic"}})) + .unwrap() + ); + assert!(validate_response(&invalid, "inbound", 2048).is_err()); + } + + fn receipt(terminal: &str, length: usize) -> Value { + json!({"head":{"method":"POST","target":"/overflow/test/streamed-9"},"head_state":"complete","head_bytes":64, + "terminal":terminal,"body_bytes":length,"body_prefix":vec![b'x';length]}) + } + + fn snapshot(receipts: Vec) -> Value { + json!({"accepted":receipts.len(),"connections":receipts,"collection_complete":true,"overflow":false}) + } + + #[test] + fn parsed_malformed_metadata_never_passes_as_an_upload_abort() { + for tail in ["4\n", "4\r\nxxxxX", "Z", "4\r\nxxxx\r\n0\r\n:bad\r\n"] { + let raw = format!( + "POST /overflow/test/streamed-9 HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n{tail}" + ); + let receipt = wire::inspect(&mut std::io::Cursor::new(raw)); + assert!( + validate_origin(&snapshot(vec![receipt]), "fastly", "test", "streamed-9").is_err() + ); + } + let raw = "POST /overflow/test/streamed-9 HTTP/1.1\r\nX-Bad: a\0b\r\nContent-Length: 9\r\n\r\nxxxx"; + let receipt = wire::inspect(&mut std::io::Cursor::new(raw)); + assert!(validate_origin(&snapshot(vec![receipt]), "fastly", "test", "streamed-9").is_err()); + for raw in [ + "POST /overflow/test/streamed-9 HTTP/1.1\r\nContent-Length: 0\r\n\r\n", + "POST /overflow/test/streamed-9 HTTP/1.1\r\n\r\n", + "POST /overflow/test/streamed-9 HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n", + ] { + let receipt = wire::inspect(&mut std::io::Cursor::new(raw)); + assert!( + validate_origin(&snapshot(vec![receipt]), "fastly", "test", "streamed-9").is_err() + ); + } + } + + #[test] + fn full_head_abort_requires_complete_numeric_head_evidence() { + let raw = "POST /overflow/test/streamed-9 HTTP/1.1\r\nContent-Length: 9\r\n\r\nxxxx"; + let receipt = wire::inspect(&mut std::io::Cursor::new(raw)); + validate_origin( + &snapshot(vec![receipt.clone()]), + "fastly", + "test", + "streamed-9", + ) + .unwrap(); + for field in ["head_state", "head_bytes"] { + let mut missing = receipt.clone(); + missing.as_object_mut().unwrap().remove(field); + assert!( + validate_origin(&snapshot(vec![missing]), "fastly", "test", "streamed-9").is_err() + ); + } + for (field, value) in [ + ("head_state", json!("partial")), + ("head_state", json!("absent")), + ("head_bytes", json!(0)), + ("head_bytes", json!("64")), + ] { + let mut invalid = receipt.clone(); + invalid[field] = value; + assert!( + validate_origin(&snapshot(vec![invalid]), "fastly", "test", "streamed-9").is_err() + ); + } + } + + #[test] + fn only_correlated_incomplete_prefixes_prove_streamed_upload_abort() { + for adapter in ["fastly", "spin"] { + for terminal in ["eof", "reset"] { + validate_origin( + &snapshot(vec![receipt(terminal, 4)]), + adapter, + "test", + "streamed-9", + ) + .unwrap(); + } + for terminal in [ + "complete", + "timeout", + "io_error", + "invalid_framing", + "capture_limit", + "reading", + ] { + assert!( + validate_origin( + &snapshot(vec![receipt(terminal, 4)]), + adapter, + "test", + "streamed-9" + ) + .is_err() + ); + } + assert!( + validate_origin( + &snapshot(vec![receipt("eof", 9)]), + adapter, + "test", + "streamed-9" + ) + .is_err() + ); + let duplicate = receipt("eof", 4); + assert!( + validate_origin( + &snapshot(vec![duplicate.clone(), duplicate]), + adapter, + "test", + "streamed-9" + ) + .is_err() + ); + let mut foreign = receipt("eof", 4); + foreign["head"]["target"] = json!("/overflow/other/streamed-9"); + assert!( + validate_origin(&snapshot(vec![foreign]), adapter, "test", "streamed-9").is_err() + ); + let mut headless = receipt("eof", 4); + headless["head"] = Value::Null; + assert!( + validate_origin(&snapshot(vec![headless]), adapter, "test", "streamed-9").is_err() + ); + let empty_contact = json!({"head":null,"head_state":"absent","head_bytes":0, + "terminal":"eof","body_bytes":0,"body_prefix":[]}); + validate_origin( + &snapshot(vec![empty_contact.clone()]), + adapter, + "test", + "streamed-9", + ) + .unwrap(); + let mut partial = empty_contact.clone(); + partial["head_state"] = json!("partial"); + partial["head_bytes"] = json!(5); + assert!( + validate_origin(&snapshot(vec![partial]), adapter, "test", "streamed-9").is_err() + ); + let mut unobserved = empty_contact; + unobserved.as_object_mut().unwrap().remove("head_state"); + assert!( + validate_origin(&snapshot(vec![unobserved]), adapter, "test", "streamed-9") + .is_err() + ); + } + for adapter in ["axum", "cloudflare"] { + assert!( + validate_origin( + &snapshot(vec![receipt("eof", 4)]), + adapter, + "test", + "streamed-9" + ) + .is_err() + ); + validate_origin(&snapshot(vec![]), adapter, "test", "streamed-9").unwrap(); + } + let mut missing = snapshot(vec![]); + missing["accepted"] = json!(1); + assert!(validate_origin(&missing, "fastly", "test", "streamed-9").is_err()); + missing["accepted"] = json!(0); + missing["collection_complete"] = json!(false); + assert!(validate_origin(&missing, "fastly", "test", "streamed-9").is_err()); + } +} diff --git a/crates/edgezero-test-harness/src/runners/progress.rs b/crates/edgezero-test-harness/src/runners/progress.rs new file mode 100644 index 000000000..58e2cd504 --- /dev/null +++ b/crates/edgezero-test-harness/src/runners/progress.rs @@ -0,0 +1,1129 @@ +use super::*; + +pub(super) fn run(name: &str, parent: &Path, records: &mut Records) -> Result<()> { + let run = parent.join(format!("progress-{name}")); + fs::create_dir_all(&run)?; + let revision = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(root()) + .output()?; + require( + revision.status.success(), + "cannot determine progress source revision", + )?; + let revision = String::from_utf8(revision.stdout)?.trim().to_owned(); + let Some((mut command, port)) = build(name, &run, records, &revision, "outbound-progress")? + else { + return Ok(()); + }; + let log = run.join("runtime.log"); + let mut worker_panic_before_shutdown = None; + let outcome = Process::start(&mut command, &log, port).and_then(|_process| { + // Characterize independent groups even when an earlier policy probe fails. + let outcomes = check_groups(|group| match group { + "coding" => coding_checks(name, port, records), + "marker" => marker_checks(name, port, records), + "relay" => relay_checks(name, port, records), + _ => checks(name, port, &revision, records), + }); + for (group, result) in &outcomes { + records.push(json!({"event":"progress-group","adapter":name,"group":group, + "status":if result.is_ok(){"pass"}else{"fail"}, + "error":result.as_ref().err().map(ToString::to_string),"qualification":false}))?; + } + let checked = outcomes.into_iter().try_for_each(|(_, result)| result); + let checked = if name == "fastly" { + checked.and(await_evidence(|| { + require(!fs::read_to_string(&log)?.contains("private-token-secret"), "private sentinel escaped into runtime diagnostics")?; + validate_framing_evidence(&logs(&log)) + })) + } else { checked }; + if name!="spin" {return checked;} + let expected:Vec<_> = records.values.iter().filter(|e|e["event"]=="response-stream-control" && e["status"]=="pass" + && e["case"].as_str().is_some_and(|case|case.starts_with("sdk-stream-"))) + .map(|e|json!({"case":e["case"],"token":e["observation"]["probe"]["token"]})).collect(); + let stderr = run.join(".spin/logs/fixture_stderr.txt"); + let diagnostics = await_evidence(||validate_sdk_terminals(&logs(&stderr),&expected)); + thread::sleep(Duration::from_millis(100)); + let diagnostics = diagnostics.and_then(|()|validate_sdk_terminals(&logs(&stderr),&expected)); + let premature_panic = fs::read_to_string(&log)?.contains("worker failed: worker panicked"); + worker_panic_before_shutdown = Some(premature_panic); + let diagnostics = diagnostics.and_then(|()|require(!premature_panic,"Spin worker panicked before controlled shutdown")); + records.push(json!({"event":"spin-before-shutdown","worker_panic":premature_panic,"sdk_terminal_evidence":if diagnostics.is_ok(){"pass"}else{"fail"}, + "error":diagnostics.as_ref().err().map(ToString::to_string),"qualification":false}))?; + checked.and(diagnostics) + }); + if name == "spin" { + let after = fs::read_to_string(&log) + .unwrap_or_default() + .contains("worker failed: worker panicked"); + records.push(json!({"event":"spin-shutdown-observation","worker_panic_before_shutdown":worker_panic_before_shutdown, + "worker_panic_after_controlled_shutdown":after,"qualification":false}))?; + for file in ["fixture_stdout.txt", "fixture_stderr.txt"] { + let path = run.join(".spin/logs").join(file); + for event in logs(&path) { + records.push(annotate( + event, + json!({"adapter":name,"diagnostic_source":file}), + ))?; + } + } + } + settle( + records, + &log, + json!({"adapter":name,"variant":format!("{name}-outbound-progress")}), + json!({"phase":"outbound-progress"}), + outcome, + ) +} + +fn check_groups(mut check: impl FnMut(&str) -> Result<()>) -> [(&'static str, Result<()>); 4] { + ["coding", "marker", "relay", "progress-and-framing"].map(|group| (group, check(group))) +} + +fn validate_relay_response(reply: &Value, coding: &str, status: u16, raw: bool) -> Result<()> { + let fields = reply["headers"].as_array().ok_or("missing relay headers")?; + let values = |name: &str| { + fields + .iter() + .filter(|field| { + field[0] + .as_str() + .is_some_and(|field| field.eq_ignore_ascii_case(name)) + }) + .map(|field| field[1].as_str().ok_or("non-text relay header")) + .collect::, _>>() + }; + let expected = if raw { + net::coding_body(coding)? + } else { + b"raw coding probe\n" + }; + require( + reply["status"] == status && reply["body_bytes"] == json!(expected), + "relay wire bytes/status mismatch", + )?; + require( + values("x-edgezero-proxy")?.is_empty(), + "relay added a synthetic proxy marker", + )?; + let encoding = values("content-encoding")?; + require( + if raw { + encoding == [coding] + } else { + encoding.is_empty() + }, + "relay wire coding mismatch", + )?; + if raw { + for (name, value) in net::coding_representation_headers(coding, status)? { + require( + values(name)? == [value.as_str()], + "relay representation metadata changed", + )?; + } + } + let length = values("content-length")?; + let transfer = values("transfer-encoding")?; + require( + length.len() <= 1 && transfer.len() <= 1 && (length.is_empty() || transfer.is_empty()), + "ambiguous relay wire framing", + )?; + if let Some(length) = length.first() { + require( + length.parse::()? == expected.len(), + "relay wire length mismatch", + )?; + } + if let Some(transfer) = transfer.first() { + require( + transfer.eq_ignore_ascii_case("chunked"), + "unsupported relay wire framing", + )?; + } + Ok(()) +} + +fn relay_checks(name: &str, port: u16, records: &mut Records) -> Result<()> { + let backend = Backend::start()?; + let mut expected_receipts = Vec::new(); + for coding in ["gzip", "deflate", "br"] { + for mode in ["buffered", "streamed"] { + for (choice, status) in [ + ("original", 200), + ("original", 206), + ("decode", 206), + ("decode", 200), + ] { + let case = format!("relay-{coding}-{mode}-{status}-{choice}"); + let path = format!("/coding/{coding}/{status}"); + let response = net::binary_request( + port, + &format!("/progress/{case}"), + &token(), + Some(&backend.url(&path)), + )?; + let result = validate_relay_response( + &response, + coding, + status, + choice == "original" || status == 206, + ); + records.push(json!({"event":"outbound-relay","adapter":name,"case":case, + "status":if result.is_ok(){"pass"}else{"fail"},"error":result.as_ref().err().map(ToString::to_string), + "client":response,"qualification":false}))?; + result?; + expected_receipts.push(path); + } + } + } + let receipts = backend.receipts.lock().unwrap().clone(); + let result = require( + receipts == expected_receipts && receipts.len() == 24, + "relay origin receipts mismatch", + ); + records.push( + json!({"event":"outbound-relay-coverage","adapter":name,"attempted":24,"receipts":receipts, + "status":if result.is_ok(){"pass"}else{"fail"},"qualification":false}), + )?; + result +} + +fn marker_checks(name: &str, port: u16, records: &mut Records) -> Result<()> { + let mut provider_hides_nomination = false; + if name == "spin" { + let control = Backend::start()?; + for shape in ["upstream", "nominated"] { + let reply = net::request( + port, + "/progress/sdk-marker-headers", + &token(), + Some(&control.url(&format!("/marker/get/200/{shape}"))), + None, + )?; + let probe = guest(&reply)?; + require( + probe["sdk_native"] == true && probe["markers"] == json!(["upstream"]), + "missing SDK-only marker control", + )?; + if shape == "nominated" { + provider_hides_nomination = probe["connection"] == json!([]); + } + records.push(json!({"event":"marker-sdk-header-visibility","adapter":name,"shape":shape,"probe":probe,"client":reply,"qualification":false}))?; + } + require( + control.receipts.lock().unwrap().len() == 2, + "SDK header control receipt mismatch", + )?; + } + let backend = Backend::start()?; + for (method, status) in [ + ("get", 200), + ("get", 204), + ("head", 200), + ("get", 304), + ("get", 302), + ] { + for shape in ["absent", "upstream", "nominated"] { + for setting in ["inherit", "on", "off"] { + let case = format!("marker-{method}-{status}-{shape}-{setting}"); + let reply = net::request( + port, + &format!("/progress/{case}"), + &token(), + Some(&backend.url(&format!("/marker/{method}/{status}/{shape}"))), + None, + )?; + let result = (|| -> Result<()> { + let probe = guest(&reply)?; + let expected = if setting == "on" { + json!([name]) + } else if shape == "upstream" + || (shape == "nominated" && provider_hides_nomination) + { + json!(["upstream"]) + } else { + json!([]) + }; + require( + probe["case"] == case + && probe["status"] == status + && probe["markers"] == expected + && probe["enabled"] == (setting == "on"), + "marker policy/status mismatch", + ) + })(); + records.push(json!({"event":"outbound-marker","adapter":name,"case":case,"status":if result.is_ok(){"pass"}else{"fail"},"provider_hidden_nomination":shape == "nominated" && provider_hides_nomination,"error":result.as_ref().err().map(ToString::to_string),"client":reply,"qualification":false}))?; + result?; + } + } + } + let reply = net::request( + port, + "/progress/marker-batch", + &token(), + Some(&backend.url("/marker/get/200/absent")), + None, + )?; + let result = (|| -> Result<()> { + let probe = guest(&reply)?; + require( + probe["case"] == "marker-batch" && probe["markers"] == json!([null, name]), + "batch marker policy mismatch", + ) + })(); + records.push(json!({"event":"outbound-marker","adapter":name,"case":"marker-batch","status":if result.is_ok(){"pass"}else{"fail"},"error":result.as_ref().err().map(ToString::to_string),"client":reply,"qualification":false}))?; + result?; + require( + backend.receipts.lock().unwrap().len() == 47, + "marker origin receipt count mismatch", + ) +} + +fn coding_checks(name: &str, port: u16, records: &mut Records) -> Result<()> { + let backend = Backend::start()?; + for coding in ["gzip", "deflate", "br"] { + for mode in ["buffered", "streamed"] { + for (choice, status) in [ + ("original", 200), + ("original", 206), + ("decode", 206), + ("decode", 200), + ] { + let case = format!("coding-{coding}-{mode}-{status}-{choice}"); + let response = net::request( + port, + &format!("/progress/{case}"), + &token(), + Some(&backend.url(&format!("/coding/{coding}/{status}"))), + None, + )?; + let result = (|| -> Result<()> { + let probe = guest(&response)?; + let raw = choice == "original" || status == 206; + let expected = if raw { + net::coding_body(coding)? + } else { + b"raw coding probe\n" + }; + require( + probe["case"] == case + && probe["status"] == status + && probe["bytes"] == json!(expected), + "coding bytes/status mismatch", + )?; + if raw { + require( + probe["content_encoding"] == coding + && probe["content_length"] + .as_str() + .and_then(|value| value.parse::().ok()) + == Some(expected.len()), + "original coding metadata lost", + )?; + } else { + require( + probe["content_encoding"].is_null() + && probe["content_length"].is_null(), + "decoded coding metadata retained", + )?; + } + Ok(()) + })(); + records.push(json!({"event":"outbound-coding","adapter":name,"case":case,"status":if result.is_ok(){"pass"}else{"fail"},"error":result.as_ref().err().map(ToString::to_string),"client":response,"qualification":false}))?; + result?; + } + } + } + require( + backend.receipts.lock().unwrap().len() == 24, + "coding origin receipt count mismatch", + ) +} + +pub(super) fn build( + name: &str, + run: &Path, + records: &mut Records, + revision: &str, + suite: &str, +) -> Result> { + let package = root().join("crates").join(format!("fixture-{name}")); + let (variable, fallback) = match name { + "fastly" => ("VICEROY_BIN", "viceroy"), + "spin" => ("SPIN_BIN", "spin"), + "cloudflare" => ("WRANGLER_BIN", "wrangler"), + "axum" => ("CARGO", "cargo"), + _ => return Err("unknown progress adapter".into()), + }; + let Some(exe) = executable(variable, fallback) else { + records.push(json!({"status":"unsupported","adapter":name,"phase":"outbound-progress","reason":format!("{fallback} unavailable"),"qualification":false}))?; + return Ok(None); + }; + records.push(json!({"event":"progress-runtime","adapter":name,"executable":exe, + "version":String::from_utf8_lossy(&Command::new(&exe).arg("--version").output()?.stdout).trim(),"revision":revision,"source_state":"working-tree-characterization"}))?; + let artifact_path = if name == "cloudflare" { + let Some(builder) = executable("WORKER_BUILD_BIN", "worker-build") else { + records.push(json!({"status":"unsupported","adapter":name,"phase":"outbound-progress","reason":"worker-build unavailable"}))?; + return Ok(None); + }; + if !check_bindgen(records)? { + return Ok(None); + } + checked( + Command::new(builder) + .args([ + "--release", + ".", + "--", + "--offline", + "--locked", + "--features", + "progress", + ]) + .env("CARGO_TARGET_DIR", root().join("target")) + .env("EDGEZERO_QUALIFICATION_REVISION", revision) + .env("EDGEZERO_QUALIFICATION_SUITE", suite) + .current_dir(&package), + )?; + let staged = stage(&package.join("build"), run)?; + fs::write( + run.join("wrangler.toml"), + wrangler_runtime_config(&package, &staged.join("worker/shim.mjs"))?, + )?; + staged + } else { + let mut command = cargo_build(); + command + .args([ + "build", + "--offline", + "--locked", + "--release", + "-p", + &format!("fixture-{name}"), + "--features", + "progress", + ]) + .env("EDGEZERO_QUALIFICATION_REVISION", revision) + .env("EDGEZERO_QUALIFICATION_SUITE", suite); + if name == "spin" { + command.args(["--target", "wasm32-wasip2"]); + } else if name == "fastly" { + command.args([ + "--target", + "wasm32-wasip1", + "--bin", + "fixture-fastly-outbound-progress", + ]); + } else { + command.args(["--bin", "fixture-axum-outbound-progress"]); + } + command.arg("--message-format=json-render-diagnostics"); + let output = command.stderr(Stdio::inherit()).output()?; + require( + output.status.success(), + format!("{name} progress fixture build failed"), + )?; + let source = match name { + "spin" => root().join("target/wasm32-wasip2/release/fixture_spin.wasm"), + "fastly" => { + root().join("target/wasm32-wasip1/release/fixture-fastly-outbound-progress.wasm") + } + _ => built_executable(&output.stdout, "fixture-axum-outbound-progress")?, + }; + stage(&source, run)? + }; + let fingerprint = if name == "cloudflare" { + artifact_path.join("index_bg.wasm") + } else { + artifact_path.clone() + }; + records.push(json!({"event":"build","adapter":name,"variant":format!("{name}-outbound-progress"),"revision":revision,"artifact":artifact(&fingerprint)?}))?; + let port = net::port()?; + let command = if name == "fastly" { + let config = run.join("fastly.toml"); + fs::copy(package.join("fastly.toml"), &config)?; + fastly_command(&exe, &config, run, "outbound-progress", port) + } else { + let exe = if name == "axum" { &artifact_path } else { &exe }; + provider_command(name, "retained", exe, &artifact_path, &package, run, port)? + }; + Ok(Some((command, port))) +} + +// Prevent worker-build from implicitly downloading/replacing tools during characterization. +pub(super) fn check_bindgen(records: &mut Records) -> Result { + let output = cargo_build() + .args([ + "metadata", + "--offline", + "--locked", + "--format-version", + "1", + "--filter-platform", + "wasm32-unknown-unknown", + ]) + .output()?; + require( + output.status.success(), + "cannot inspect locked wasm-bindgen version", + )?; + let metadata: Value = serde_json::from_slice(&output.stdout)?; + let version = metadata["packages"] + .as_array() + .ok_or("missing Cargo packages")? + .iter() + .find(|p| p["name"] == "wasm-bindgen") + .and_then(|p| p["version"].as_str()) + .ok_or("missing locked wasm-bindgen")?; + let installed = executable("WASM_BINDGEN_BIN", "wasm-bindgen"); + let observed = installed + .as_ref() + .map(|exe| Command::new(exe).arg("--version").output()) + .transpose()?; + let observed = observed + .as_ref() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned()); + let ready = observed.as_deref() == Some(format!("wasm-bindgen {version}").as_str()); + if !ready { + records.push(json!({"status":"unsupported","adapter":"cloudflare","phase":"build-tooling","required_wasm_bindgen":version,"observed":observed,"reason":"matching wasm-bindgen CLI unavailable; no download or dependency repin attempted","qualification":false}))?; + } else if std::env::var_os("WASM_BINDGEN_BIN").is_none() { + // worker-build does not use PATH by default; require an explicit override. + records.push(json!({"status":"unsupported","adapter":"cloudflare","phase":"build-tooling","reason":"set WASM_BINDGEN_BIN to the matching executable to prevent automatic installation","qualification":false}))?; + return Ok(false); + } + if ready { + for variable in ["WASM_BINDGEN_BIN", "ESBUILD_BIN", "WASM_OPT_BIN"] { + let explicit = std::env::var_os(variable).map(PathBuf::from); + let Some(path) = explicit.filter(|p| p.is_absolute() && p.is_file()) else { + records.push(json!({"status":"unsupported","adapter":"cloudflare","phase":"build-tooling","reason":format!("set {variable} to an existing absolute executable; automatic installation is disabled"),"qualification":false}))?; + return Ok(false); + }; + let output = Command::new(&path).arg("--version").output()?; + require( + output.status.success(), + format!("{variable} version command failed"), + )?; + records.push(json!({"event":"worker-build-tool","variable":variable,"executable":path,"version":String::from_utf8_lossy(&output.stdout).trim()}))?; + } + } + Ok(ready) +} + +fn validate_identity( + probe: &Value, + name: &str, + revision: &str, + case: &str, + token: &str, +) -> Result<()> { + require( + probe["revision"] == revision + && probe["adapter"] == name + && probe["case"] == case + && probe["token"] == token, + "probe identity mismatch", + ) +} + +fn validate_sdk_terminals(events: &[Value], expected: &[Value]) -> Result<()> { + require(expected.len() == 2, "missing SDK comparison controls")?; + require( + !events.iter().any(|e| e["event"] == "progress-guest-panic"), + "guest panic in response-stream controls", + )?; + let terminals: Vec<_> = events + .iter() + .filter(|e| e["event"] == "sdk-stream-coordinator-terminal") + .collect(); + require( + terminals.len() == expected.len(), + "missing or duplicate SDK terminal observations", + )?; + for control in expected { + let matching: Vec<_> = terminals + .iter() + .filter(|e| e["case"] == control["case"] && e["token"] == control["token"]) + .collect(); + require( + matching.len() == 1 && matching[0]["coordinator_ok"] == true, + "SDK terminal missing, failed or uncorrelated", + )?; + } + Ok(()) +} + +fn validate_empty_signal(name: &str, framing: net::Framing, probe: &Value) -> Result<()> { + let expected = if matches!(framing, net::Framing::FixedBody | net::Framing::ChunkedBody) { + 4 + } else { + 0 + }; + require( + probe["bytes_read"] == expected, + "empty-body premise discarded or invented body bytes", + )?; + if name == "cloudflare" { + let absent = matches!(framing, net::Framing::Absent | net::Framing::LengthZero); + require( + probe["adapter"] == name && probe["case"] == "empty" && probe["method"] == "GET", + "empty-body probe identity mismatch", + )?; + require( + probe["host_signal"]["source"] == "workerd-before-conversion" + && probe["host_signal"]["body_absent"] == absent, + "missing or contradictory original Workers body signal", + )?; + require( + probe["body_kind_before"] == "Initial" && probe["body_is_stream"] == !absent, + "Workers body storage did not preserve the original source presence", + )?; + } + Ok(()) +} + +fn checks(name: &str, port: u16, revision: &str, records: &mut Records) -> Result<()> { + framing_checks(name, port, records)?; + if name == "spin" { + source_diagnostic_checks(port, records)?; + } + let control_passes = if name == "spin" { + response_controls(name, port, revision, records)? + } else { + 0 + }; + let mut count = 0; + let mut origin_requests = 0; + let mut failures = Vec::new(); + for case in [ + "eager-single", + "lazy-single", + "eager-batch", + "baseline-batch", + "response-owned", + ] { + let backend = if case == "response-owned" { + Backend::start_gated_progress()? + } else { + Backend::start()? + }; + let token = token(); + let result = (|| -> Result<()> { + let reply = net::request( + port, + &format!("/progress/{case}"), + &token, + Some(&backend.url(&format!("/g3/{token}"))), + (case == "response-owned").then_some(&backend.release), + )?; + let mut probe = guest(&reply)?; + probe["protocol"] = json!("http1"); + validate_identity(&probe, name, revision, case, &token)?; + if case == "response-owned" { + require( + probe["handler_returned"] == true, + "body consumed inside handler", + )?; + if name == "cloudflare" || name == "spin" { + require( + probe["dispatch_returned"] == true, + "body consumed before SDK dispatch returned", + )?; + } + } + let receipts = backend.progress.lock().unwrap().clone(); + let result = validate_outbound_progress(&probe, &receipts); + records.push(json!({"event":"outbound-progress","adapter":name,"case":case,"status":if result.is_ok(){"pass"}else{"fail"},"probe":probe,"receipts":receipts,"client":reply,"qualification":false}))?; + result + })(); + let receipts = backend.progress.lock().unwrap().clone(); + origin_requests += receipts.len(); + match result { + Ok(()) => count += 1, + Err(error) => { + records.push(json!({"event":"outbound-progress-failure","status":"fail","adapter":name,"case":case, + "error":error.to_string(),"receipts":receipts,"qualification":false}))?; + failures.push(case.to_owned()); + } + } + } + let mut empty_count = 0; + for framing in [ + net::Framing::Absent, + net::Framing::LengthZero, + net::Framing::ChunkedEmpty, + net::Framing::FixedBody, + net::Framing::ChunkedBody, + ] { + let result = (|| -> Result<()> { + let reply = net::framed_request(port, framing)?; + let probe = guest(&reply)?; + validate_empty_signal(name, framing, &probe)?; + records.push(json!({"event":"inbound-empty-signal","adapter":name,"framing":format!("{framing:?}"),"protocol":"http1","probe":probe,"client":reply,"qualification":false})) + })(); + match result { + Ok(()) => empty_count += 1, + Err(error) => { + records.push(json!({"event":"inbound-empty-failure","status":"fail","adapter":name,"framing":format!("{framing:?}"),"error":error.to_string(),"qualification":false}))?; + failures.push(format!("{framing:?}")); + } + } + } + records.push(json!({"event":"progress-coverage","adapter":name,"progress_passes":count,"empty_body_passes":empty_count,"attempted_progress_cases":5,"attempted_empty_body_cases":5,"origin_requests":origin_requests,"failures":failures,"qualification":false, + "limitations":["HTTP/2 and HTTP/3 not exercised","hosted execution pending","response-owned tests handler/dispatch return, not host callback return","missing-context detection unproved except native absent-runtime control","no cancellation or G2 readiness qualification"]}))?; + require( + count == 5 + && empty_count == 5 + && origin_requests == 18 + && (name != "spin" || control_passes == 4), + "SDK progress or empty-body characterization has failed cases; see recorded observations", + ) +} + +fn framing_checks(name: &str, port: u16, records: &mut Records) -> Result<()> { + let reply = net::request(port, "/progress/framing-trailers", &token(), None, None)?; + let result = validate_framing_wire(&reply); + records.push( + json!({"event":"framing-rule", "adapter":name, "reason":"UnsupportedTrailers", + "client":reply,"status":if result.is_ok(){"pass"}else{"fail"},"qualification":false}), + )?; + result +} + +fn validate_framing_wire(reply: &Value) -> Result<()> { + require( + reply["status"] == 500 + && reply["body"] == "internal server error" + && !reply.to_string().contains("private-token-secret"), + "framing fallback wire contract changed", + ) +} + +fn validate_framing_evidence(events: &[Value]) -> Result<()> { + require( + !events + .iter() + .any(|event| event.to_string().contains("private-token-secret")), + "private sentinel escaped into captured diagnostics", + )?; + let reports: Vec<_> = events + .iter() + .filter(|event| event["event"] == "framing-terminal") + .collect(); + require( + reports.len() == 1 + && reports[0]["application"] == "UnsupportedTrailers" + && reports[0]["fallback"].is_null() + && reports[0]["outcome"] == "ConversionError" + && reports[0]["body_kind"] == "Fallback" + && reports[0]["disposition"] == "Completed" + && reports[0]["bytes_written"] == 21, + "missing or altered framing terminal report", + )?; + require( + events.iter().any(|event| { + event["event"] == "fixture_log" + && event["level"] == "ERROR" + && event["target"] == "edgezero_core::response_egress" + && event["message"].as_str().is_some_and(|message| { + message.contains("response trailers are unsupported") + && !message.contains("secret") + }) + }), + "missing bounded framing error log", + ) +} + +fn source_diagnostic_checks(port: u16, records: &mut Records) -> Result<()> { + for (path, event, cases, expected) in [ + ( + "/progress/sdk-source-diagnostics", + "source-diagnostic-race", + 8, + json!({"source_diagnostics":"pass", "cases":8, "qualification":false}), + ), + ( + "/progress/sdk-connect-fallback", + "connect-timeout-fallback", + 60, + json!({"connect_fallback":"pass", "outcomes":5, "budget_sources":4, "checks":60, "qualification":false}), + ), + ] { + let reply = net::request(port, path, &token(), None, None)?; + let probe = guest(&reply)?; + let result = require( + reply["status"] == 200 && probe == expected, + "Spin SDK policy probe changed", + ); + records.push( + json!({"event":event, "adapter":"spin", "cases":cases, "observation":probe, + "status":if result.is_ok(){"pass"}else{"fail"}, "client":reply, "qualification":false}), + )?; + result?; + } + Ok(()) +} + +fn response_controls( + name: &str, + port: u16, + revision: &str, + records: &mut Records, +) -> Result { + let mut passes = 0; + let mut failures = Vec::new(); + for case in [ + "response-owned-ready", + "response-owned-timer-only", + "sdk-stream-ready", + "sdk-stream-timer-only", + ] { + let token = token(); + let result = (|| -> Result { + let reply = net::request(port, &format!("/progress/{case}"), &token, None, None)?; + let probe = guest(&reply)?; + validate_identity(&probe, name, revision, case, &token)?; + if case.starts_with("response-owned") { + require( + probe["handler_returned_at_first_poll"] == true + && probe["dispatch_returned_at_first_poll"] == true + && probe["handler_returned"] == true + && probe["dispatch_returned"] == true, + "stream control was polled before return", + )?; + } else { + require( + probe["sdk_native"] == true, + "SDK control did not use the SDK response writer", + )?; + } + if case.ends_with("timer-only") { + require( + probe["pause_ns"] + .as_u64() + .is_some_and(|ns| ns >= 100_000_000), + "timer control did not complete its measured wait", + )?; + } + Ok(json!({"probe":probe,"client":reply})) + })(); + match result { + Ok(observation) => { + passes += 1; + records.push(json!({"event":"response-stream-control","adapter":name,"case":case,"status":"pass","observation":observation,"qualification":false}))?; + } + Err(error) => { + failures.push(case); + let partial = error + .downcast_ref::() + .map(|r| json!({"status":r.status,"body_bytes":r.body.len()})); + records.push(json!({"event":"response-stream-control","adapter":name,"case":case,"status":"fail","error":error.to_string(),"partial":partial,"qualification":false}))?; + } + } + } + records.push(json!({"event":"response-stream-control-coverage","adapter":name,"attempted":4,"passes":passes,"failures":failures,"qualification":false}))?; + Ok(passes) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn relay_wire_assertions_reject_recoding_and_missing_representation_fields() { + let mut headers = json!([ + ["content-encoding", "gzip"], + ["content-length", "37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"] + ]); + let reply = + json!({"status":206,"headers":headers,"body_bytes":net::coding_body("gzip").unwrap()}); + validate_relay_response(&reply, "gzip", 206, true).unwrap(); + for change in [ + "body", + "encoding", + "length", + "metadata", + "range", + "marker", + "status", + "ambiguous-framing", + ] { + let mut broken = reply.clone(); + match change { + "body" => broken["body_bytes"] = json!([0, 1]), + "encoding" => broken["headers"][0][1] = json!("br"), + "length" => broken["headers"][1][1] = json!("1"), + "metadata" => { + broken["headers"].as_array_mut().unwrap().remove(3); + } + "range" => { + broken["headers"].as_array_mut().unwrap().pop(); + } + "marker" => broken["headers"] + .as_array_mut() + .unwrap() + .push(json!(["x-edgezero-proxy", "cloudflare"])), + "status" => broken["status"] = json!(200), + "ambiguous-framing" => broken["headers"] + .as_array_mut() + .unwrap() + .push(json!(["transfer-encoding", "chunked"])), + _ => unreachable!(), + } + assert!( + validate_relay_response(&broken, "gzip", 206, true).is_err(), + "{change}" + ); + } + headers.as_array_mut().unwrap().retain(|field| { + !matches!( + field[0].as_str(), + Some("content-encoding" | "content-range") + ) + }); + for field in headers.as_array_mut().unwrap() { + if field[0] == "content-length" { + field[1] = json!("17"); + } + } + let decoded = json!({"status":200,"headers":headers,"body_bytes":b"raw coding probe\n"}); + validate_relay_response(&decoded, "gzip", 200, false).unwrap(); + let mut stale = decoded.clone(); + stale["headers"] + .as_array_mut() + .unwrap() + .push(json!(["content-encoding", "gzip"])); + assert!(validate_relay_response(&stale, "gzip", 200, false).is_err()); + } + + #[test] + fn progress_groups_keep_later_observations_after_policy_failures() { + for mask in 0..16 { + let failed: Vec<_> = ["coding", "marker", "relay", "progress-and-framing"] + .into_iter() + .enumerate() + .filter_map(|(index, group)| (mask & (1 << index) != 0).then_some(group)) + .collect(); + let mut visited = Vec::new(); + let outcomes = check_groups(|group| { + visited.push(group.to_owned()); + if failed.contains(&group) { + Err(format!("{group} failure").into()) + } else { + Ok(()) + } + }); + assert_eq!( + visited, + ["coding", "marker", "relay", "progress-and-framing"] + ); + let errors: Vec<_> = outcomes + .iter() + .filter_map(|(group, result)| { + result + .as_ref() + .err() + .map(|error| (*group, error.to_string())) + }) + .collect(); + assert_eq!( + errors, + failed + .iter() + .map(|group| (*group, format!("{group} failure"))) + .collect::>() + ); + let result = outcomes.into_iter().try_for_each(|(_, result)| result); + assert_eq!(result.is_ok(), failed.is_empty()); + assert_eq!( + result.err().map(|error| error.to_string()), + failed.first().map(|group| format!("{group} failure")) + ); + } + } + + #[test] + fn cloudflare_empty_signal_requires_the_original_host_snapshot() { + for (framing, bytes, absent) in [ + (net::Framing::Absent, 0, true), + (net::Framing::LengthZero, 0, true), + (net::Framing::ChunkedEmpty, 0, false), + (net::Framing::FixedBody, 4, false), + (net::Framing::ChunkedBody, 4, false), + ] { + let probe = json!({"adapter":"cloudflare","case":"empty","method":"GET", + "bytes_read":bytes,"body_is_stream":!absent,"body_kind_before":"Initial", + "host_signal":{"source":"workerd-before-conversion","body_absent":absent}}); + validate_empty_signal("cloudflare", framing, &probe).unwrap(); + for change in [ + "missing", + "missing-boolean", + "null-boolean", + "contradictory", + "string", + "source", + "bytes", + "adapter", + "method", + "case", + "storage-missing", + "storage-null", + "storage-string", + "storage-inverted", + "cached-before-transfer", + ] { + let mut invalid = probe.clone(); + match change { + "missing" => invalid["host_signal"] = Value::Null, + "missing-boolean" => { + invalid["host_signal"] + .as_object_mut() + .unwrap() + .remove("body_absent"); + } + "null-boolean" => invalid["host_signal"]["body_absent"] = Value::Null, + "contradictory" => invalid["host_signal"]["body_absent"] = json!(!absent), + "string" => invalid["host_signal"]["body_absent"] = json!(absent.to_string()), + "source" => invalid["host_signal"]["source"] = json!("after-conversion"), + "bytes" => invalid["bytes_read"] = json!(bytes + 1), + "adapter" => invalid["adapter"] = json!("spin"), + "method" => invalid["method"] = json!("HEAD"), + "case" => invalid["case"] = json!("other"), + "storage-missing" => { + invalid.as_object_mut().unwrap().remove("body_is_stream"); + } + "storage-null" => invalid["body_is_stream"] = Value::Null, + "storage-string" => invalid["body_is_stream"] = json!((!absent).to_string()), + "storage-inverted" => invalid["body_is_stream"] = json!(absent), + "cached-before-transfer" => { + invalid["body_kind_before"] = json!("Cached { len: 0 }") + } + _ => unreachable!(), + } + assert!( + validate_empty_signal("cloudflare", framing, &invalid).is_err(), + "{framing:?}: {change}" + ); + } + } + } + + #[test] + fn other_empty_probes_do_not_claim_cloudflare_raw_signals() { + for name in ["axum", "fastly", "spin"] { + for (framing, bytes) in [(net::Framing::Absent, 0), (net::Framing::FixedBody, 4)] { + let probe = + json!({"adapter":name,"case":"empty","method":"GET","bytes_read":bytes}); + validate_empty_signal(name, framing, &probe).unwrap(); + assert!( + validate_empty_signal(name, framing, &json!({"bytes_read":bytes+1})).is_err() + ); + } + } + } + + #[test] + fn framing_evidence_requires_exact_report_and_bounded_error_log() { + let events = vec![ + json!({"event":"framing-terminal", "application":"UnsupportedTrailers", + "fallback":null,"outcome":"ConversionError","body_kind":"Fallback","disposition":"Completed","bytes_written":21}), + json!({"event":"fixture_log","level":"ERROR","target":"edgezero_core::response_egress", + "message":"response-egress Application framing validation failed before commit: response trailers are unsupported"}), + ]; + validate_framing_evidence(&events).unwrap(); + for (index, key, value) in [ + (0, "application", json!("InvalidContentLength")), + (0, "fallback", json!("UnsupportedTrailers")), + (0, "outcome", json!("DeadlineExceeded")), + (0, "disposition", json!("Aborted")), + (0, "bytes_written", json!(0)), + (1, "level", json!("DEBUG")), + ( + 1, + "message", + json!("response trailers are unsupported private-token-secret"), + ), + ] { + let mut altered = events.clone(); + altered[index][key] = value; + assert!(validate_framing_evidence(&altered).is_err()); + } + assert!(validate_framing_evidence(&[]).is_err()); + let mut duplicate = events.clone(); + duplicate.push(events[0].clone()); + assert!(validate_framing_evidence(&duplicate).is_err()); + let mut additional_leak = events; + additional_leak.push(json!({"event":"fixture_log", "message":"private-token-secret"})); + assert!(validate_framing_evidence(&additional_leak).is_err()); + let reply = json!({"status":500,"body":"internal server error","headers":[["content-type","text/plain"]]}); + validate_framing_wire(&reply).unwrap(); + let mut leaked_header = reply; + leaked_header["headers"] + .as_array_mut() + .unwrap() + .push(json!(["x-private", "private-token-secret"])); + assert!(validate_framing_wire(&leaked_header).is_err()); + } + + #[test] + fn sdk_terminal_evidence_rejects_missing_failed_duplicate_and_foreign_completion() { + let expected = vec![ + json!({"case":"sdk-stream-ready","token":"one"}), + json!({"case":"sdk-stream-timer-only","token":"two"}), + ]; + let events:Vec<_> = expected.iter().map(|e|json!({"event":"sdk-stream-coordinator-terminal","case":e["case"],"token":e["token"],"coordinator_ok":true})).collect(); + validate_sdk_terminals(&events, &expected).unwrap(); + for change in ["missing", "failed", "duplicate", "foreign", "panic"] { + let mut mutated = events.clone(); + match change { + "missing" => { + mutated.pop(); + } + "failed" => mutated[0]["coordinator_ok"] = json!(false), + "duplicate" => mutated.push(mutated[0].clone()), + "foreign" => mutated[0]["token"] = json!("other"), + "panic" => mutated.push(json!({"event":"progress-guest-panic"})), + _ => unreachable!(), + } + assert!( + validate_sdk_terminals(&mutated, &expected).is_err(), + "{change}" + ); + } + assert!(validate_sdk_terminals(&[], &[]).is_err()); + } + + #[test] + fn progress_identity_rejects_a_different_case_or_request() { + let probe = + json!({"adapter":"axum","revision":"candidate","case":"eager-single","token":"one"}); + validate_identity(&probe, "axum", "candidate", "eager-single", "one").unwrap(); + assert!(validate_identity(&probe, "axum", "candidate", "lazy-single", "one").is_err()); + assert!(validate_identity(&probe, "axum", "candidate", "eager-single", "two").is_err()); + assert!(validate_identity(&probe, "spin", "candidate", "eager-single", "one").is_err()); + assert!(validate_identity(&probe, "axum", "different", "eager-single", "one").is_err()); + } + + #[test] + fn progress_builds_use_adapter_qualified_variants() { + let events = ["axum", "fastly"].map(|name| { + json!({"event":"build","adapter":name, + "variant":format!("{name}-outbound-progress"),"artifact":{"fnv1a64":name}}) + }); + assert_eq!(summarize(&events)["build_identity"]["status"], "consistent"); + } +} diff --git a/crates/edgezero-test-harness/src/runners/readiness.rs b/crates/edgezero-test-harness/src/runners/readiness.rs new file mode 100644 index 000000000..a7dd879fa --- /dev/null +++ b/crates/edgezero-test-harness/src/runners/readiness.rs @@ -0,0 +1,400 @@ +use super::*; +use crate::net::ReadinessOrigin; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum ReadinessSuite { + ClientProgress, + FullDiagnostic, +} + +impl ReadinessSuite { + pub(super) fn parse(name: &str) -> Option { + match name { + "outbound-client-progress" => Some(Self::ClientProgress), + "outbound-readiness" => Some(Self::FullDiagnostic), + _ => None, + } + } + + fn name(self) -> &'static str { + match self { + Self::ClientProgress => "outbound-client-progress", + Self::FullDiagnostic => "outbound-readiness", + } + } +} + +fn client_outcome(validation: Result<()>, mut failures: Vec) -> Result<()> { + if let Err(error) = validation { + failures.push(error.to_string()); + } + require(failures.is_empty(), failures.join("\n")) +} + +fn timer_control(port: u16, source: &str, budget_ms: u64, revision: &str) -> Result { + let token = token(); + let result = if source == "native-sleep" { + let start = Instant::now(); + thread::sleep(Duration::from_millis(budget_ms)); + let elapsed_us = start.elapsed().as_micros() as u64; + json!({"schema":1,"event":"readiness-timer-control","source":source, + "budget_ms":budget_ms,"token":token,"revision":revision,"qualification":false, + "elapsed_us":elapsed_us,"overrun_us":elapsed_us.saturating_sub(budget_ms * 1000), + "within_five_ms":elapsed_us <= budget_ms * 1000 + 5000, + "measurement_end":"primitive-return"}) + } else { + guest(&net::request( + port, + &format!("/timer-control/{source}/{budget_ms}"), + &token, + None, + None, + )?)? + }; + require( + result["token"] == token + && result["source"] == source + && result["budget_ms"] == budget_ms + && result["revision"] == revision, + "foreign timer-control result", + )?; + Ok(result) +} + +fn timer_controls(port: u16, revision: &str, records: &mut Records) -> Result> { + for source in ["native-sleep", "empty-select", "wasi-sleep"] { + let mut warmup = timer_control(port, source, 1, revision)?; + warmup["event"] = json!("readiness-timer-warmup"); + records.push(warmup)?; + } + let mut controls = Vec::new(); + for leg in timer_legs() { + let mut value = timer_control(port, leg.source, leg.budget_ms, revision)?; + value["repetition"] = json!(leg.repetition); + records.push(value.clone())?; + controls.push(value); + } + records.push(json!({"event":"readiness-timer-inventory", "samples":controls.len(), + "late_samples":controls.iter().filter(|c| c["within_five_ms"] == false).count(), + "status":"unverified", "qualification":false, + "reason":"timer controls characterize scheduling; no deterministic bound or G2 acceptance is inferred"}))?; + Ok(controls) +} + +fn client_probes( + port: u16, + revision: &str, + records: &mut Records, +) -> Result<(Vec, Vec)> { + let mut probes = Vec::new(); + let mut failures = Vec::new(); + for leg in readiness_client_legs() { + let token = token(); + let result = (|| -> Result { + let origin = ReadinessOrigin::start(leg.case, &token)?; + let reply = net::request( + port, + &format!( + "/{}/client/{}/{}", + leg.case, leg.consumption, leg.read_limit + ), + &token, + Some(&origin.url(0)), + None, + ); + let snapshot = origin.finish(); + records.push( + json!({"event":"readiness-client-origin", "leg":leg.key(), "origin":snapshot}), + )?; + let mut probe = guest(&reply?)?; + require(probe.is_object(), "actual-client reply is not an object")?; + probe["repetition"] = json!(leg.repetition); + probe["origin"] = snapshot; + records.push(probe.clone())?; + Ok(probe) + })(); + let validation = match result { + Ok(probe) => { + let validation = require( + probe["token"] == token + && probe["case"] == leg.case + && probe["consumption"] == leg.consumption, + "foreign actual-client reply", + ) + .and_then(|()| validate_outbound_readiness_client_probe(&probe, revision)); + probes.push(probe); + validation + } + Err(error) => Err(error), + }; + if let Err(error) = validation { + failures.push(format!("{}: {error}", leg.key())); + records.push( + json!({"event":"fastly-client-progress-failure", "status":"fail", + "leg":leg.key(), "token":token, "case":leg.case, "consumption":leg.consumption, + "repetition":leg.repetition, "error":error.to_string(), "qualification":false}), + )?; + } + } + Ok((probes, failures)) +} + +fn p5_probes(port: u16, revision: &str, records: &mut Records) -> Result<()> { + let cases = p5_cases(); + let expected = cases.len(); + let mut probes = Vec::new(); + let mut failures = Vec::new(); + for case in cases { + let token = token(); + let result = (|| -> Result { + let a = Backend::start()?; + let b = Backend::start()?; + let shape = match case.as_str() { + "two-origin" | "optimizer-next" => "redirect", + "pending-head" => "pending", + "optimizer-send" | "optimizer-initial" => "image", + _ => &case, + }; + let initial = a.url(&format!("/p5/{token}/{}/{shape}", b.port)); + let reply = net::request(port, &format!("/p5/{case}"), &token, Some(&initial), None); + let origin = json!({"a_port":a.port,"b_port":b.port, + "a":a.receipts.lock().unwrap().clone(),"b":b.receipts.lock().unwrap().clone(), + "pending_released_before_reply":*a.release.0.lock().unwrap(), + "pending_heads":a.progress.lock().unwrap().iter().filter(|event| + event["event"] == "p5-pending-head" && event["token"] == token).count()}); + // Never release a pending origin body until after the guest's final response. + net::release(&a.release); + let mut probe = guest(&reply?)?; + require( + probe["token"] == token && probe["case"] == case && probe["a_uri"] == initial, + "foreign P5 guest response", + )?; + probe["origin"] = origin; + records.push(probe.clone())?; + Ok(probe) + })(); + let validation = match result { + Ok(probe) => { + let validation = validate_outbound_readiness_client_probe(&probe, revision); + probes.push(probe); + validation + } + Err(error) => Err(error), + }; + if let Err(error) = validation { + failures.push(format!("{case}: {error}")); + records.push( + json!({"event":"fastly-p5-failure","status":"fail","case":case, + "token":token,"error":error.to_string(),"qualification":false}), + )?; + } + } + let validation = client_outcome( + validate_outbound_readiness_client(&probes, revision), + failures, + ); + records.push(json!({"event":"fastly-p5-inventory","status":if validation.is_ok(){"pass"}else{"fail"}, + "probes":probes.len(),"expected_probes":expected, + "origin_requests":probes.iter().map(|probe| ["a","b"].into_iter().map(|origin| + probe["origin"][origin].as_array().map_or(0, Vec::len)).sum::()).sum::(), + "error":validation.as_ref().err().map(ToString::to_string),"qualification":false, + "coding_evidence":"identity/gzip/unsupported-token passthrough with ResponseDecoding::Decode", + "optimizer_evidence":"compositional native unavailable boundaries; no optimized-first-hop redirect, transformation or hosted performance proof"}))?; + validation +} + +pub(super) fn run( + exe: &Path, + run: &Path, + records: &mut Records, + suite: ReadinessSuite, +) -> Result<()> { + let revision = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(root()) + .output()?; + require(revision.status.success(), "cannot record source revision")?; + let revision = String::from_utf8(revision.stdout)?.trim().to_owned(); + checked( + cargo_build() + .args([ + "build", + "--offline", + "--locked", + "--release", + "-p", + "fixture-fastly", + "--features", + "readiness", + "--bin", + "fixture-fastly-outbound-readiness", + "--target", + "wasm32-wasip1", + ]) + .env("EDGEZERO_QUALIFICATION_REVISION", &revision), + )?; + let binary = stage( + &root().join("target/wasm32-wasip1/release/fixture-fastly-outbound-readiness.wasm"), + run, + )?; + records.push(json!({"event":"readiness-build", "revision":revision, + "source_state":"working-tree-characterization", "sdk":"0.13.1", "artifact":artifact(&binary)?}))?; + let config = run.join("fastly.toml"); + fs::copy(root().join("crates/fixture-fastly/fastly.toml"), &config)?; + let log = run.join("readiness.log"); + let outcome = (|| -> Result<()> { + let port = net::port()?; + let _process = Process::start( + &mut fastly_command(exe, &config, run, "outbound-readiness", port), + &log, + port, + )?; + let controls = match suite { + ReadinessSuite::ClientProgress => Vec::new(), + ReadinessSuite::FullDiagnostic => timer_controls(port, &revision, records)?, + }; + let (clients, client_failures) = client_probes(port, &revision, records)?; + let client_validation = client_outcome( + validate_outbound_readiness_client(&clients, &revision), + client_failures, + ); + records.push(json!({"event":"fastly-client-progress-inventory", + "status":if client_validation.is_ok() {"pass"} else {"fail"}, + "probes":clients.len(), "expected_probes":readiness_client_legs().len(), + "origin_requests":clients.iter().map(|p| p["origin"]["accepted"].as_u64().unwrap_or(0)).sum::(), + "error":client_validation.as_ref().err().map(ToString::to_string), "qualification":false}))?; + if suite == ReadinessSuite::ClientProgress { + let p5_validation = p5_probes(port, &revision, records); + client_validation?; + p5_validation?; + return records.push(json!({"status":"unverified", "suite":suite.name(), + "reason":"actual-client receive cases passed; SDK timing diagnostics, callback-wide progress, resource and hosted qualification remain open", + "qualification":false})); + } + let mut probes = Vec::new(); + for leg in readiness_legs() + .into_iter() + .chain(readiness_progress_legs()) + { + let token = token(); + let origin = ReadinessOrigin::start(leg.case, &token)?; + let reply = net::request( + port, + &format!( + "/{}/{}/{}/{}", + leg.case, leg.mode, leg.consumption, leg.read_limit + ), + &token, + Some(&origin.url(0)), + None, + ); + // Always stop, wake and join origin workers, including failed client reads. + let snapshot = origin.finish(); + records + .push(json!({"event":"readiness-origin", "leg":leg.key(), "origin":snapshot}))?; + let reply = reply.map_err(|error| format!("readiness {}: {error}", leg.key()))?; + let mut probe = guest(&reply) + .map_err(|error| format!("readiness {}: {error}; reply={reply}", leg.key()))?; + probe["repetition"] = json!(leg.repetition); + probe["origin"] = snapshot; + records.push(probe.clone())?; + probes.push(probe); + } + for repetition in 0..3 { + let control = probes + .iter() + .find(|probe| probe["repetition"] == repetition && probe["mode"] == "direct") + .ok_or("missing direct control")?; + let selected = probes + .iter() + .find(|probe| { + probe["repetition"] == repetition + && probe["case"] == "truncated" + && probe["mode"] == "selected" + && probe["consumption"] == "buffered" + && probe["read_limit"] == 1 + }) + .ok_or("missing selected control")?; + let direct = &control["slots"][0]["error"]; + let selected = &selected["slots"][0]["error"]; + records.push(json!({"event":"readiness-error-preservation", "repetition":repetition, + "direct":direct, "selected":selected, + "status":if direct == "UnexpectedEof" && direct == selected {"pass"} else {"fail"}, "qualification":false}))?; + } + let (baseline, progress) = probes.split_at(readiness_legs().len()); + let failures: Vec<_> = [ + ("actual outbound client", client_validation), + ( + "timer controls", + validate_timer_controls(&controls, &revision), + ), + ( + "readiness", + validate_outbound_readiness(baseline, &revision), + ), + ( + "mixed/full-buffer progress", + validate_outbound_readiness_progress(progress, &revision), + ), + ] + .into_iter() + .filter_map(|(scope, result)| result.err().map(|error| format!("{scope}: {error}"))) + .collect(); + require(failures.is_empty(), failures.join("\n"))?; + records.push(json!({"event":"readiness-inventory", "status":"pass", "probes":probes.len(), + "origin_requests":probes.iter().map(|probe| probe["origin"]["accepted"].as_u64().unwrap_or(0)).sum::(), + "baseline_probes":baseline.len(), "progress_probes":progress.len(), "qualification":false}))?; + records.push(json!({"status":"unverified", "reason":"local SDK and client cases passed; full dispatch-mode coverage, callback ownership, resource and hosted qualification remain open", "qualification":false})) + })(); + settle( + records, + &log, + json!({"adapter":"fastly", "suite":suite.name()}), + json!({"case":"readiness-inventory"}), + outcome, + ) +} + +#[cfg(test)] +mod tests { + use super::{ReadinessSuite, client_outcome}; + + #[test] + fn functional_and_sdk_diagnostic_suites_have_distinct_explicit_names() { + for suite in [ + ReadinessSuite::ClientProgress, + ReadinessSuite::FullDiagnostic, + ] { + assert_eq!(ReadinessSuite::parse(suite.name()), Some(suite)); + } + assert_ne!( + ReadinessSuite::ClientProgress.name(), + ReadinessSuite::FullDiagnostic.name() + ); + assert_eq!(ReadinessSuite::parse("outbound-progress"), None); + assert_eq!(ReadinessSuite::parse("smoke"), None); + } + + #[test] + fn client_outcome_retains_inventory_and_transport_failures() { + let error = client_outcome( + Err("missing client inventory".into()), + vec![ + "first request failed".into(), + "second reply malformed".into(), + ], + ) + .unwrap_err() + .to_string(); + for diagnostic in [ + "missing client inventory", + "first request failed", + "second reply malformed", + ] { + assert!(error.contains(diagnostic)); + } + assert!(client_outcome(Ok(()), vec!["request failed".into()]).is_err()); + assert!(client_outcome(Err("invalid inventory".into()), Vec::new()).is_err()); + assert!(client_outcome(Ok(()), Vec::new()).is_ok()); + } +} diff --git a/crates/edgezero-test-harness/src/runners/response_headers.rs b/crates/edgezero-test-harness/src/runners/response_headers.rs new file mode 100644 index 000000000..8e75ccc60 --- /dev/null +++ b/crates/edgezero-test-harness/src/runners/response_headers.rs @@ -0,0 +1,770 @@ +use super::*; +use std::io::Read; +use std::net::TcpStream; + +const MAX_HEAD: usize = 16 * 1024; +const MAX_BODY: usize = 4 * 1024; + +struct Case { + name: String, + method: &'static str, + path: String, + status: u16, + source: Option<&'static str>, + mode: &'static str, + large_headers: bool, +} + +fn cases(adapter: &str) -> Vec { + let mut cases = Vec::new(); + for method in ["GET", "HEAD"] { + for (name, status, source) in [ + ("success", 200, None), + ("handler-error", 400, None), + ("refused", 503, None), + ("missing/path", 404, Some("app")), + ("fallback", 500, Some("app")), + ] { + cases.push(Case { + name: format!("{name}-{method}"), + method, + path: format!("/progress/synthesized/{name}"), + status, + source, + mode: "app", + large_headers: false, + }); + } + } + cases.push(Case { + name: "wrong-method".into(), + method: "POST", + path: "/progress/synthesized/success".into(), + status: 405, + source: Some("app"), + mode: "app", + large_headers: false, + }); + if adapter == "fastly" { + cases.push(Case { + name: "normalized-header-overflow".into(), + method: "GET", + path: "/progress/synthesized/success".into(), + status: 431, + source: Some("app"), + mode: "app", + large_headers: true, + }); + for (mode, source) in [ + ("app", Some("app")), + ("replace", Some("request")), + ("invalid", Some("app")), + ("empty", None), + ] { + cases.push(Case { + name: format!("normalized-target-overflow-{mode}"), + method: "GET", + path: format!( + "http://localhost/progress/synthesized/{}", + "x".repeat(9 * 1024) + ), + status: 414, + source, + mode, + large_headers: false, + }); + if mode != "app" { + for method in ["GET", "HEAD"] { + cases.push(Case { + name: format!("fallback-{mode}-{method}"), + method, + path: "/progress/synthesized/fallback".into(), + status: 500, + source, + mode, + large_headers: false, + }); + } + } + } + } + cases +} + +pub(super) fn run(name: &str, parent: &Path, records: &mut Records) -> Result<()> { + let run = parent.join(format!("synthesized-headers-{name}")); + fs::create_dir_all(&run)?; + let revision = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(root()) + .output()?; + require( + revision.status.success(), + "missing header-probe source revision", + )?; + let revision = String::from_utf8(revision.stdout)?.trim().to_owned(); + let Some((mut command, port)) = + progress::build(name, &run, records, &revision, "synthesized-headers")? + else { + return Ok(()); + }; + let log = run.join("runtime.log"); + let mut observed = Vec::new(); + let outcome = Process::start(&mut command, &log, port).and_then(|_process| { + check_cases( + name, + records, + |case, id| capture(port, case, id, &run, name == "cloudflare"), + |case, id, response| { + let bytes = response["body"].as_str().map_or(0, str::len); + observed.push((id.to_owned(), case.name.clone(), bytes)); + if name == "fastly" { + await_evidence(|| validate_guest(&logs(&log), id, case, bytes)) + } else { + Ok(()) + } + }, + ) + }); + // Recheck after runtime teardown, so a later duplicate cannot hide behind an earlier pass. + let final_events = logs(&log); + let final_result = if name == "fastly" { + let result = require( + observed.len() == cases(name).len(), + "final guest evidence omitted attempted cases", + ) + .and_then(|()| { + observed.iter().try_for_each(|(id, case_name, bytes)| { + let case = cases(name) + .into_iter() + .find(|case| case.name == *case_name) + .ok_or("unknown observed header case")?; + validate_guest(&final_events, id, &case, *bytes) + }) + }); + records.push(json!({"event":"synthesized-final-evidence","adapter":name,"checked":observed.len(), + "status":if result.is_ok(){"pass"}else{"fail"},"error":result.as_ref().err().map(ToString::to_string),"qualification":false}))?; + result + } else { + Ok(()) + }; + let outcome = outcome.and(final_result); + if name == "spin" { + for file in ["fixture_stdout.txt", "fixture_stderr.txt"] { + for event in logs(&run.join(".spin/logs").join(file)) { + records.push(annotate( + event, + json!({"adapter":name,"diagnostic_source":file}), + ))?; + } + } + } + settle( + records, + &log, + json!({"adapter":name,"suite":"synthesized-headers"}), + json!({"phase":"synthesized-headers"}), + outcome, + ) +} + +fn check_cases( + name: &str, + records: &mut Records, + mut capture: impl FnMut(&Case, &str) -> Result, + mut guest: impl FnMut(&Case, &str, &Value) -> Result<()>, +) -> Result<()> { + let (mut failures, mut attempted) = (0, 0); + for case in cases(name) { + attempted += 1; + let id = token(); + let (response, result) = match capture(&case, &id) { + Ok(response) => { + let result = + validate_case(&response, &case).and_then(|()| guest(&case, &id, &response)); + (response, result) + } + Err(error) => (Value::Null, Err(error)), + }; + failures += usize::from(result.is_err()); + records.push(json!({"event":"synthesized-header-case","adapter":name,"case":case.name,"token":id, + "status":if result.is_ok(){"pass"}else{"fail"},"error":result.err().map(|error|error.to_string()), + "client":response,"qualification":false}))?; + } + records.push( + json!({"event":"synthesized-header-coverage","adapter":name,"attempted":attempted, + "failed":failures,"protocol":"HTTP/1.1","qualification":false}), + )?; + require( + failures == 0, + "one or more synthesized-header wire cases failed", + ) +} + +fn parse_wire(bytes: &[u8]) -> Result { + let split = bytes + .windows(4) + .position(|part| part == b"\r\n\r\n") + .ok_or("incomplete captured response head")? + + 4; + require( + split <= MAX_HEAD && bytes.len() - split <= MAX_BODY, + "response exceeded head/body capture bound", + )?; + let text = std::str::from_utf8(&bytes[..split])?; + let mut lines = text.trim_end_matches("\r\n").split("\r\n"); + let mut status = lines + .next() + .ok_or("missing response status")? + .split_whitespace(); + let protocol = status.next().ok_or("missing response version")?; + let status: u16 = status.next().ok_or("missing response code")?.parse()?; + let fields = lines + .map(|line| { + let (name, value) = line.split_once(':').ok_or("malformed response field")?; + Ok(json!([name.to_ascii_lowercase(), value.trim()])) + }) + .collect::>>()?; + Ok( + json!({"protocol":protocol,"status":status,"headers":fields,"body":std::str::from_utf8(&bytes[split..])?}), + ) +} + +fn capture(port: u16, case: &Case, id: &str, run: &Path, half_close: bool) -> Result { + let mut request = format!( + "{} {} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\nContent-Length: 0\r\nx-request-token: {id}\r\nx-fixture-synthesized-mode: {}\r\n", + case.method, case.path, case.mode + ); + if case.large_headers { + for index in 0..60 { + request.push_str(&format!("x-large-{index:02}: {}\r\n", "x".repeat(1230))); + } + } + request.push_str("\r\n"); + let mut bytes = Vec::new(); + let started = Instant::now(); + let deadline = started + Duration::from_secs(5); + let outcome = (|| -> Result<()> { + require( + request.len() <= 96 * 1024, + "request capture input exceeded its bound", + )?; + let mut stream = TcpStream::connect_timeout( + &std::net::SocketAddr::from(([127, 0, 0, 1], port)), + Duration::from_secs(5), + )?; + stream.set_nonblocking(true)?; + let mut written = 0; + while written < request.len() { + let remaining = deadline.saturating_duration_since(Instant::now()); + require( + !remaining.is_zero(), + "absolute request-write deadline expired", + )?; + match stream.write(&request.as_bytes()[written..]) { + Ok(0) => return Err("wire request writer closed".into()), + Ok(bytes) => written += bytes, + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(remaining.min(Duration::from_millis(2))) + } + Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {} + Err(error) => return Err(format!("write captured request: {error}").into()), + } + } + if half_close { + stream.shutdown(std::net::Shutdown::Write)?; + } + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + require( + !remaining.is_zero(), + "absolute wire-capture deadline expired", + )?; + let mut buffer = [0; 1024]; + let room = (MAX_HEAD + MAX_BODY).saturating_sub(bytes.len()); + require(room > 0, "wire capture exceeded its allocation bound")?; + let read = match stream.read(&mut buffer[..room.min(1024)]) { + Ok(read) => read, + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(remaining.min(Duration::from_millis(2))); + continue; + } + Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, + Err(error) => return Err(format!("read captured wire: {error}").into()), + }; + if read == 0 { + return Ok(()); + } + bytes.extend_from_slice(&buffer[..read]); + if let Some(head) = bytes + .windows(4) + .position(|part| part == b"\r\n\r\n") + .map(|at| at + 4) + { + require( + head <= MAX_HEAD && bytes.len() - head <= MAX_BODY, + "wire head/body exceeded capture bound", + )?; + } else { + require(bytes.len() <= MAX_HEAD, "wire head exceeded capture bound")?; + } + } + })(); + let path = run.join(format!("{id}.http1")); + fs::write(&path, &bytes)?; + let mut response = + parse_wire(&bytes).unwrap_or_else(|error| json!({"parse_error":error.to_string()})); + response["eof"] = json!(outcome.is_ok()); + response["capture_error"] = json!(outcome.err().map(|error| error.to_string())); + response["capture_artifact"] = artifact(&path)?; + response["elapsed_ms"] = json!(started.elapsed().as_millis()); + response["request_target_bytes"] = json!(case.path.len()); + response["request_wire_bytes"] = json!(request.len()); + response["request_shutdown"] = json!(if half_close { + "write-half-close" + } else { + "connection-close" + }); + response["request_field_values"] = json!( + request + .split("\r\n") + .skip(1) + .take_while(|line| !line.is_empty()) + .count() + ); + response["request_field_wire_bytes"] = json!( + request + .split_once("\r\n") + .ok_or("missing request-line terminator")? + .1 + .len() + ); + Ok(response) +} + +pub(super) fn assembly_request( + port: u16, + path: &str, + head: bool, + run: &Path, + half_close: bool, +) -> Result { + let case = Case { + name: "assembly".into(), + method: if head { "HEAD" } else { "GET" }, + path: path.into(), + status: 0, + source: None, + mode: "app", + large_headers: false, + }; + let id = token(); + let mut response = capture(port, &case, &id, run, half_close)?; + response["token"] = json!(id); + Ok(response) +} + +fn validate_case(response: &Value, case: &Case) -> Result<()> { + validate_wire(response, case.method == "HEAD", case.status, case.source)?; + require( + values(response, "transfer-encoding")?.is_empty(), + "controlled buffered response gained transfer coding", + )?; + if case.method != "HEAD" { + let body = response["body"].as_str().ok_or("missing captured body")?; + require( + values(response, "content-length")? == [body.len().to_string()], + "buffered wire length mismatch", + )?; + match case.status { + 200 => require(body == "routed success", "routed body changed")?, + 500 => require(body == "internal server error", "fallback body changed")?, + 503 => require(body == "refused by probe", "refusal body changed")?, + status => { + let value: Value = serde_json::from_str(body)?; + require( + value["error"]["status"] == status, + "canonical error body changed", + )?; + } + } + } + if case.status == 405 { + let allow = values(response, "allow")?; + require(allow.len() == 1, "missing or duplicate Allow")?; + let mut methods = allow[0].split(',').map(str::trim).collect::>(); + methods.sort_unstable(); + require(methods == ["GET", "HEAD"], "canonical Allow changed")?; + } + Ok(()) +} + +fn validate_guest(events: &[Value], id: &str, case: &Case, bytes: usize) -> Result<()> { + let matching = |event: &str| { + events + .iter() + .filter(|value| value["event"] == event && value["token"] == id) + .collect::>() + }; + let terminal = matching("synthesized-terminal"); + require(terminal.len() == 1, "missing/duplicate correlated terminal")?; + let fallback = case.status == 500; + require( + terminal[0]["outcome"] + == if fallback { + "ConversionError" + } else { + "Completed" + } + && terminal[0]["body_kind"] == if fallback { "Fallback" } else { "Application" } + && terminal[0]["fallback"] + == if fallback { + json!("Completed") + } else { + Value::Null + } + && terminal[0]["bytes_written"] == bytes, + "terminal outcome/body-kind/disposition/bytes mismatch", + )?; + if case.status == 414 || case.status == 431 { + let detached = matching("synthesized-detached"); + require( + detached.len() == 1 + && detached[0]["status"] == case.status + && matching("synthesized-admission").is_empty() + && matching("synthesized-handler").is_empty() + && matching("synthesized-finalize").is_empty(), + "normalized rejection lost detached/zero-dispatch witness", + )?; + } else { + require( + matching("synthesized-admission").len() == 1, + "missing/duplicate admission witness", + )?; + require( + matching("synthesized-handler").len() + == usize::from(matches!(case.status, 200 | 400 | 500)) + && matching("synthesized-finalize").len() == usize::from(case.status != 503), + "handler/finalize count mismatch", + )?; + } + if case.mode != "app" { + let prepare = matching("synthesized-prepare"); + require( + prepare.len() == 1 + && prepare[0]["mode"] == case.mode + && prepare[0]["outcome"] + == if case.mode == "invalid" { + "TooManyBytes" + } else { + "installed" + }, + "missing typed per-request preparation result", + )?; + } + Ok(()) +} + +fn values<'a>(response: &'a Value, name: &str) -> Result> { + response["headers"] + .as_array() + .ok_or("missing captured fields")? + .iter() + .filter(|field| { + field[0] + .as_str() + .is_some_and(|key| key.eq_ignore_ascii_case(name)) + }) + .map(|field| { + field[1] + .as_str() + .ok_or_else(|| "non-text captured field".into()) + }) + .collect() +} + +fn validate_wire(response: &Value, head: bool, status: u16, source: Option<&str>) -> Result<()> { + require( + response["protocol"] == "HTTP/1.1" + && response["status"] == status + && response["eof"] == true + && response.get("capture_error") == Some(&Value::Null), + "wire status/protocol or clean completion missing", + )?; + if head { + require(response["body"] == "", "HEAD transmitted payload octets")?; + } + let marker = values(response, "x-synthesized-fixture")?; + let cookies = values(response, "set-cookie")?; + let Some(source) = source else { + return require( + marker.is_empty() && cookies.is_empty(), + "ineligible response gained synthesized fields", + ); + }; + require( + marker == [source] && cookies.len() == 31, + "missing, collapsed or extra synthesized values", + )?; + for (index, cookie) in cookies.iter().take(30).enumerate() { + require( + *cookie == format!("slot-{index:02}=value; Path=/"), + "synthesized cookie value/order changed", + )?; + } + let charge = 2 + + "x-synthesized-fixture".len() + + source.len() + + 4 + + cookies + .iter() + .map(|value| "set-cookie".len() + value.len() + 4) + .sum::(); + require( + charge == 4096 + && cookies[30] + .strip_prefix("padding=") + .is_some_and(|padding| padding.bytes().all(|byte| byte == b'a')), + "maximum synthesized field-section charge/padding changed", + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn response(source: &str) -> Value { + let mut fields = vec![json!(["x-synthesized-fixture", source])]; + for index in 0..30 { + fields.push(json!([ + "set-cookie", + format!("slot-{index:02}=value; Path=/") + ])); + } + let used = 2 + + fields + .iter() + .map(|field| { + field[0].as_str().unwrap().len() + field[1].as_str().unwrap().len() + 4 + }) + .sum::() + + "set-cookie".len() + + 4; + fields.push(json!([ + "set-cookie", + format!("padding={}", "a".repeat(4096 - used - "padding=".len())) + ])); + json!({"protocol":"HTTP/1.1","status":500,"headers":fields, + "body":"internal server error","eof":true,"capture_error":null}) + } + + #[test] + fn synthesized_wire_requires_every_value_in_order_at_the_exact_byte_bound() { + let valid = response("app"); + validate_wire(&valid, false, 500, Some("app")).unwrap(); + for mutation in [ + "missing", + "reordered", + "changed", + "extra", + "source", + "status", + "protocol", + "eof", + "error", + ] { + let mut changed = valid.clone(); + match mutation { + "missing" => { + changed["headers"].as_array_mut().unwrap().pop(); + } + "reordered" => changed["headers"].as_array_mut().unwrap().swap(1, 2), + "changed" => changed["headers"][1][1] = json!("wrong"), + "extra" => changed["headers"] + .as_array_mut() + .unwrap() + .push(json!(["set-cookie", "extra"])), + "source" => changed["headers"][0][1] = json!("request"), + "status" => changed["status"] = json!(200), + "protocol" => changed["protocol"] = json!("HTTP/2"), + "eof" => changed["eof"] = json!(false), + "error" => changed["capture_error"] = json!("deadline"), + _ => unreachable!(), + } + assert!( + validate_wire(&changed, false, 500, Some("app")).is_err(), + "{mutation}" + ); + } + } + + #[test] + fn synthesized_wire_head_requires_clean_eof_and_zero_actual_payload() { + let mut valid = response("app"); + valid["body"] = json!(""); + validate_wire(&valid, true, 500, Some("app")).unwrap(); + for mutation in ["payload", "timeout", "header-only"] { + let mut changed = valid.clone(); + match mutation { + "payload" => changed["body"] = json!("x"), + "timeout" => changed["capture_error"] = json!("deadline"), + "header-only" => changed["eof"] = Value::Null, + _ => unreachable!(), + } + assert!( + validate_wire(&changed, true, 500, Some("app")).is_err(), + "{mutation}" + ); + } + assert!(validate_wire(&valid, true, 500, None).is_err()); + valid["headers"] = json!([]); + validate_wire(&valid, true, 500, None).unwrap(); + } + + #[test] + fn wire_capture_finishes_the_request_before_waiting_for_response_eof() { + let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = thread::spawn(move || { + let (mut socket, _) = listener.accept().unwrap(); + socket + .set_read_timeout(Some(Duration::from_millis(200))) + .unwrap(); + let mut request = Vec::new(); + if socket.read_to_end(&mut request).is_err() { + return false; + } + socket + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 14\r\n\r\nrouted success") + .unwrap(); + true + }); + let run = std::env::temp_dir().join(format!("synthesized-capture-{}", token())); + fs::create_dir(&run).unwrap(); + let case = cases("axum").remove(0); + let response = capture(port, &case, "half-close-control", &run, true).unwrap(); + let accepted_eof = server.join().unwrap(); + fs::remove_dir_all(run).unwrap(); + assert!( + accepted_eof, + "capture did not terminate the request write side" + ); + assert!(validate_case(&response, &case).is_ok(), "{response}"); + } + + #[test] + fn wire_capture_rejects_head_payload_in_the_head_packet_or_a_later_write() { + for delayed in [false, true] { + let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = thread::spawn(move || { + let (mut socket, _) = listener.accept().unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + let mut request = Vec::new(); + socket.read_to_end(&mut request).unwrap(); + let head = b"HTTP/1.1 200 OK\r\nContent-Length: 14\r\n\r\n"; + if delayed { + socket.write_all(head).unwrap(); + thread::sleep(Duration::from_millis(20)); + socket.write_all(b"x").unwrap(); + } else { + socket.write_all(&[head.as_slice(), b"x"].concat()).unwrap(); + } + }); + let run = std::env::temp_dir().join(format!("synthesized-head-{}", token())); + fs::create_dir(&run).unwrap(); + let case = cases("axum") + .into_iter() + .find(|case| case.name == "success-HEAD") + .unwrap(); + let response = capture(port, &case, "head-payload-control", &run, true).unwrap(); + server.join().unwrap(); + fs::remove_dir_all(run).unwrap(); + assert_eq!(response["eof"], true); + assert_eq!(response["body"], "x"); + assert!(validate_case(&response, &case).is_err()); + } + } + + #[test] + fn guest_evidence_rejects_changed_terminal_fields_and_late_duplicates() { + let case = cases("fastly") + .into_iter() + .find(|case| case.name == "fallback-GET") + .unwrap(); + let valid = vec![ + json!({"event":"synthesized-terminal","token":"one","outcome":"ConversionError", "body_kind":"Fallback", "fallback":"Completed","bytes_written":21}), + json!({"event":"synthesized-admission","token":"one"}), + json!({"event":"synthesized-handler","token":"one"}), + json!({"event":"synthesized-finalize","token":"one"}), + ]; + validate_guest(&valid, "one", &case, 21).unwrap(); + for key in ["outcome", "body_kind", "fallback", "bytes_written", "token"] { + let mut changed = valid.clone(); + changed[0][key] = json!("wrong"); + assert!(validate_guest(&changed, "one", &case, 21).is_err(), "{key}"); + } + let mut late = valid.clone(); + late.push(valid[0].clone()); + assert!(validate_guest(&late, "one", &case, 21).is_err()); + let case = cases("fastly") + .into_iter() + .find(|case| case.status == 414) + .unwrap(); + let valid = vec![ + json!({"event":"synthesized-terminal","token":"one","outcome":"Completed", "body_kind":"Application", "fallback":null,"bytes_written":21}), + json!({"event":"synthesized-detached","token":"one","status":414}), + ]; + validate_guest(&valid, "one", &case, 21).unwrap(); + for event in [ + "synthesized-admission", + "synthesized-handler", + "synthesized-finalize", + ] { + let mut late = valid.clone(); + late.push(json!({"event":event,"token":"one"})); + assert!(validate_guest(&late, "one", &case, 21).is_err(), "{event}"); + } + } + + #[test] + fn capture_artifact_errors_do_not_skip_later_cases_or_claim_coverage() { + let run = std::env::temp_dir().join(format!("synthesized-failure-{}", token())); + fs::create_dir(&run).unwrap(); + let mut records = Records { + path: run.join("events.jsonl"), + values: Vec::new(), + }; + let mut attempted = Vec::new(); + let result = check_cases( + "axum", + &mut records, + |case, _| { + attempted.push(case.name.clone()); + Err("controlled capture artifact failure".into()) + }, + |_, _, _| panic!("failed capture must not be validated"), + ); + assert!(result.is_err()); + assert_eq!( + attempted, + cases("axum") + .into_iter() + .map(|case| case.name) + .collect::>() + ); + assert_eq!( + records + .values + .iter() + .filter(|value| value["status"] == "fail") + .count(), + 11 + ); + assert_eq!(records.values.last().unwrap()["attempted"], 11); + assert_eq!(records.values.last().unwrap()["failed"], 11); + fs::remove_dir_all(run).unwrap(); + } +} diff --git a/crates/edgezero-test-harness/src/runners/wire.rs b/crates/edgezero-test-harness/src/runners/wire.rs new file mode 100644 index 000000000..597f89e54 --- /dev/null +++ b/crates/edgezero-test-harness/src/runners/wire.rs @@ -0,0 +1,745 @@ +use super::*; +use crate::net::WireOrigins; +use std::io::Read as _; + +fn body(reply: &Value) -> Result> { + Ok(serde_json::from_value(reply["body_bytes"].clone())?) +} + +fn handoff(bytes: &[u8]) -> Result<()> { + require( + bytes.len() == 21 && bytes.starts_with(b"origin"), + "handoff origin bytes differ", + )?; + let mut bids = bytes[6..].as_chunks::<5>().0.to_vec(); + bids.sort_unstable(); + require( + bids == [*b"bid-A", *b"bid-B", *b"bid-C"], + "handoff lost or changed bidder bytes", + ) +} + +fn error_is(report: &Value, kind: &str, reason: &str) -> Result<()> { + let error = &report["outcome"]["error"]; + require( + error["kind"] == kind + && error["reason"] == reason + && error["status"] == if kind == "gateway_timeout" { 504 } else { 502 }, + "wrong typed wire outcome", + ) +} + +struct Matrix<'a> { + adapter: &'static str, + endpoint: &'static str, + port: u16, + origins: &'a WireOrigins, + records: &'a mut Records, + exchanges: usize, + dispatches: usize, +} + +impl Matrix<'_> { + fn call( + &mut self, + path: &str, + mode: &str, + streamed: bool, + encoded: bool, + expected: Option>, + gate: Option<&str>, + ) -> Result { + require( + self.origins.take_requests().is_empty(), + "request escaped the preceding wire exchange", + )?; + if let Some(name) = gate { + self.origins.arm(name); + } + let uri = format!("http://127.0.0.1:{}{path}", self.origins.port(0)); + let response = net::header_request( + self.port, + self.endpoint, + &token(), + &[ + ("x-origin", &uri), + ("x-p5-mode", mode), + ("x-p5-stream", if streamed { "true" } else { "false" }), + ("x-p5-encoded-cap", if encoded { "true" } else { "false" }), + ], + ); + if let Some(name) = gate { + self.origins.release(name); + } + let response = response?; + require( + response["status"] == 200, + "wire guest rejected the exchange", + )?; + let observed = self.origins.take_requests(); + let expected = expected.unwrap_or_else(|| vec![(self.origins.port(0), path.to_owned())]); + if observed != expected { + self.records.push(json!({"event":format!("{}-wire-receipt-failure", self.adapter),"status":"fail", + "path":path,"mode":mode,"observed":observed,"expected":expected,"qualification":false}))?; + } + require( + observed == expected, + "missing, extra or foreign wire origin requests", + )?; + let report: Value = serde_json::from_slice(&body(&response)?)?; + self.exchanges += 1; + self.dispatches += observed.len(); + self.records.push( + json!({"event":format!("{}-wire-case", self.adapter),"path":path,"mode":mode, + "streamed":streamed,"encoded_cap":encoded,"origin_requests":observed, + "report":report,"qualification":false}), + )?; + Ok(report) + } + + fn parity(&mut self, path: &str, streamed: bool, encoded: bool) -> Result { + let sent = self.call(path, "send", streamed, encoded, None, None)?; + let followed = self.call(path, "follow", streamed, encoded, None, None)?; + require( + sent["outcome"] == followed["outcome"], + "Send/Follow outcomes differ", + )?; + Ok(followed) + } + + fn run(&mut self) -> Result<()> { + let first = self.origins.port(0); + let second = self.origins.port(1); + let base = format!("http://127.0.0.1:{first}"); + for streamed in [false, true] { + for coding in ["identity", "gzip", "passthrough"] { + for size in [10, 11] { + let path = format!("/p5/body/{coding}/{size}"); + let encoded = streamed && coding == "passthrough"; + let followed = self.parity(&path, streamed, encoded)?; + require( + followed["policies"] == 0 && followed["hop"] == 0, + "unexpected no-redirect policy/hop", + )?; + if size == 10 { + let outcome = &followed["outcome"]; + require( + outcome["status"] == 200 + && outcome["bytes"] == json!(vec![b'a'; 10]) + && followed["streamed"] == streamed + && followed["final_uri"] == format!("{base}{path}") + && followed["redirects"] == 0 + && followed.get("stop") == Some(&Value::Null), + "wrong no-redirect final response", + )?; + let fields = outcome["headers"] + .as_array() + .ok_or("missing wire headers")?; + let field = |name: &str| { + fields + .iter() + .find(|entry| entry[0] == name) + .map(|entry| &entry[1]) + }; + require( + field("x-p5-fixture") == Some(&json!(b"spin-wire".as_slice())), + "missing wire fixture header", + )?; + if coding == "gzip" { + require( + field("content-encoding").is_none() + && field("content-length").is_none(), + "decoded representation fields retained", + )?; + } else { + let value: &[u8] = if coding == "identity" { + b"identity" + } else { + b"x-p5-opaque" + }; + require( + field("content-encoding") == Some(&json!(value)), + "coding field changed", + )?; + } + } else { + error_is( + &followed, + "response_too_large", + if encoded { + "EncodedBody" + } else if streamed { + "DecodedBody" + } else { + "BufferedBody" + }, + )?; + } + } + let followed = self.parity(&format!("/p5/declared/{coding}"), streamed, true)?; + error_is(&followed, "response_too_large", "EncodedBody")?; + } + } + for kind in ["pending", "error", "emptybr", "oversized"] { + let mut errors = Vec::new(); + for mode in ["follow", "send", "stop", "limit"] { + let name = format!("{kind}-{mode}"); + let redirect = format!("/p5/redirect/{kind}/{name}"); + let path = if mode == "limit" { + format!("/p5/limit/{kind}/{name}") + } else { + redirect.clone() + }; + let mut expected = vec![(first, path.clone())]; + if mode == "follow" { + expected.push((first, format!("/p5/next/{name}"))); + } else if mode == "limit" { + expected.push((first, redirect)); + } + let report = self.call(&path, mode, false, false, Some(expected), Some(&name))?; + if mode == "follow" { + require( + report["outcome"]["status"] == 200 + && report["outcome"]["bytes"] == json!(b"final".as_slice()) + && report["redirects"] == 1 + && report["policies"] == 1 + && report.get("stop") == Some(&Value::Null) + && report["final_uri"] == format!("{base}/p5/next/{name}"), + "wrong intermediate-body follow result", + )?; + } else { + require( + report["hop"] == usize::from(mode == "limit") + && report["policies"] == usize::from(mode != "send"), + "wrong stopped/limit hop or policy count", + )?; + match kind { + "pending" => error_is(&report, "gateway_timeout", "PerCallTimeout")?, + "error" => { + source_error(&report, self.adapter)?; + } + "emptybr" => error_is(&report, "bad_gateway", "Decode(Brotli)")?, + _ => error_is(&report, "response_too_large", "BufferedBody")?, + } + errors.push(report["outcome"].clone()); + } + } + require( + errors.len() == 3 && errors.iter().all(|error| error == &errors[0]), + "Send/stop/limit final errors differ", + )?; + } + for mode in ["stop", "limit"] { + let name = format!("clean-{mode}"); + let redirect = format!("/p5/redirect/clean/{name}"); + let path = if mode == "limit" { + format!("/p5/limit/clean/{name}") + } else { + redirect.clone() + }; + let mut expected = vec![(first, path.clone())]; + if mode == "limit" { + expected.push((first, redirect)); + } + let report = self.call(&path, mode, false, false, Some(expected), Some(&name))?; + require( + report["outcome"]["status"] == 302 + && report["outcome"]["bytes"] == json!(b"found".as_slice()) + && report["stop"] + == if mode == "limit" { + "LimitReached" + } else { + "StoppedByPolicy" + } + && report["redirects"] == usize::from(mode == "limit"), + "wrong clean stop/limit response", + )?; + } + let sent = self.call("/p5/cross", "send", false, false, None, None)?; + require( + sent["outcome"]["status"] == 302, + "plain Send followed the cross-origin redirect", + )?; + let report = self.call( + "/p5/cross", + "follow", + false, + false, + Some(vec![ + (first, "/p5/cross".to_owned()), + (second, "/p5/final".to_owned()), + ]), + None, + )?; + require( + report["outcome"]["status"] == 200 + && report["outcome"]["bytes"] == json!(b"final".as_slice()) + && report["redirects"] == 1 + && report["final_uri"] == format!("http://127.0.0.1:{second}/p5/final"), + "wrong cross-origin final response", + )?; + for coding in ["identity", "gzip", "passthrough"] { + for size in [10, 11] { + let final_path = format!("/p5/body/{coding}/{size}"); + let path = format!("/p5/to-body/{coding}/{size}"); + let encoded = coding == "passthrough"; + let sent = self.call(&final_path, "send", true, encoded, None, None)?; + let followed = self.call( + &path, + "follow", + true, + encoded, + Some(vec![(first, path.clone()), (first, final_path.clone())]), + None, + )?; + require( + sent["outcome"] == followed["outcome"] + && followed["streamed"] == true + && followed["redirects"] == 1 + && followed["policies"] == 1 + && followed["final_uri"] == format!("{base}{final_path}"), + "streamed final Send/Follow parity differs", + )?; + } + } + require( + self.exchanges == 68 && self.dispatches == 84, + "incomplete provider wire matrix", + ) + } +} + +fn source_error_reason(adapter: &str) -> &'static str { + if adapter == "spin" { + "Protocol" + } else { + "Transport" + } +} + +fn source_error(report: &Value, adapter: &str) -> Result<()> { + error_is(report, "bad_gateway", source_error_reason(adapter))?; + let provider = report["outcome"]["error"]["provider"].as_str(); + require( + provider.is_some_and(|detail| detail.contains("ResponseBodyRead")), + "lost source-error provider phase", + )?; + require( + adapter != "cloudflare" + || provider == Some("Cloudflare { phase: ResponseBodyRead, cause: Unknown }"), + "unexpected workerd source-error cause", + ) +} + +fn component_text(directory: &Path) -> Result { + let mut text = String::new(); + let mut directories = vec![directory.to_owned()]; + let mut entries = 0; + while let Some(directory) = directories.pop() { + if !directory.exists() { + continue; + } + for entry in fs::read_dir(directory)? { + entries += 1; + require(entries <= 128, "too many component log entries")?; + let entry = entry?; + let kind = entry.file_type()?; + if kind.is_dir() { + directories.push(entry.path()); + } else if kind.is_file() { + fs::File::open(entry.path())? + .take( + (1024 * 1024_usize) + .saturating_sub(text.len()) + .saturating_add(1) as u64, + ) + .read_to_string(&mut text)?; + require(text.len() <= 1024 * 1024, "component logs exceeded 1 MiB")?; + } + } + } + Ok(text) +} + +fn controls(port: u16, origins: &WireOrigins, log_dir: &Path, process: &mut Process) -> Result<()> { + for name in ["get", "head", "get-settled", "head-settled", "post"] { + let uri = format!("http://127.0.0.1:{}/{name}", origins.port(0)); + let reply = + net::header_request(port, &format!("/{name}"), &token(), &[("x-origin", &uri)])?; + require(reply["status"] == 200, "wire control guest failed")?; + let captured = origins.take_wire()?; + require( + captured["method"] + == if name == "post" { + "POST" + } else if name.starts_with("head") { + "HEAD" + } else { + "GET" + } + && captured["target"] == format!("/{name}"), + "wire control method/target changed", + )?; + if name == "post" { + require( + captured["payload"] == "upload-control", + "upload control bytes differ", + )?; + } else { + require( + captured["content_length"] == json!([]) + && captured["transfer_encoding"] == json!([]) + && captured["encoded_body_hex"] == "", + "bodyless request carried framing or bytes", + )?; + } + } + require( + net::header_request(port, "/scope", &token(), &[])?["status"] == 200, + "canonical scope control failed", + )?; + let uri = format!("http://127.0.0.1:{}", origins.port(0)); + let reply = net::header_request(port, "/handoff", &token(), &[("x-origin", &uri)])?; + require(reply["status"] == 200, "handoff guest failed")?; + handoff(&body(&reply)?)?; + let deadline = Instant::now() + Duration::from_secs(10); + loop { + process.check_running()?; + let text = component_text(log_dir)?; + if text.contains("PASS: Spin attached Begin/Batch handoff closed at SDK terminal") { + return Ok(()); + } + require( + !text.contains("FAIL: Spin attached") && Instant::now() < deadline, + "SDK-terminal ownership proof missing", + )?; + thread::sleep(Duration::from_millis(10)); + } +} + +pub(super) fn run(name: &str, run: &Path, records: &mut Records) -> Result<()> { + match name { + "cloudflare" => return progress_wire("cloudflare", run, records), + "axum" => return progress_wire("axum", run, records), + "fastly" => return progress_wire("fastly", run, records), + _ => {} + } + require(name == "spin", "wire suite requires Spin")?; + let repo = root().join("../../..").canonicalize()?; + let executable = executable("SPIN_BIN", "spin").ok_or("Spin runtime unavailable")?; + let version = Command::new(&executable).arg("--version").output()?; + require(version.status.success(), "Spin version probe failed")?; + let version = String::from_utf8(version.stdout)?; + let pins = fs::read_to_string(repo.join(".tool-versions"))?; + let pin = pins + .lines() + .find_map(|line| line.strip_prefix("spin ")) + .ok_or("missing Spin pin")?; + require( + version.split_whitespace().nth(1) == Some(pin), + "Spin runtime does not match repository pin", + )?; + records.push(json!({"event":"runtime","adapter":"spin","version":version.trim(),"executable":executable,"qualification":false}))?; + checked( + Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) + .current_dir(&repo) + .env("CARGO_TARGET_DIR", repo.join("target")) + .args([ + "build", + "--offline", + "--locked", + "-p", + "edgezero-adapter-spin", + "--no-default-features", + "--features", + "spin,test-utils", + "--target", + "wasm32-wasip2", + "--example", + "outbound-bodyless-wire", + ]), + )?; + let relative_wasm = "target/wasm32-wasip2/debug/examples/outbound_bodyless_wire.wasm"; + let wasm = run.join(relative_wasm); + copy_tree(&repo.join(relative_wasm), &wasm)?; + let relative_manifest = + "crates/edgezero-adapter-spin/examples/outbound-bodyless-wire.spin.toml"; + let manifest = run.join(relative_manifest); + copy_tree(&repo.join(relative_manifest), &manifest)?; + records.push(json!({"event":"build","adapter":"spin","artifact":artifact(&wasm)?,"root_lockfile":artifact(&repo.join("Cargo.lock"))?,"qualification":false}))?; + let origins = WireOrigins::start()?; + let port = net::port()?; + let state = run.join("spin-wire-state"); + let component_logs = run.join("component-logs"); + let log = run.join("spin-wire.log"); + let mut command = Command::new(executable); + command + .args(["up", "--from"]) + .arg(manifest) + .args(["--listen", &format!("127.0.0.1:{port}")]) + .arg("--state-dir") + .arg(&state) + .arg("--log-dir") + .arg(&component_logs) + .arg("--disable-cache"); + let mut process = None; + let outcome: Result<(usize, usize)> = (|| { + let running = process.insert(Process::start(&mut command, &log, port)?); + controls(port, &origins, &component_logs, running)?; + let mut matrix = Matrix { + adapter: "spin", + endpoint: "/p5", + port, + origins: &origins, + records, + exchanges: 0, + dispatches: 0, + }; + matrix.run()?; + running.check_running()?; + Ok((matrix.exchanges, matrix.dispatches)) + })(); + let (exchanges, dispatches) = finish_wire( + "spin", + &mut process, + Some(origins), + &state, + records, + outcome, + )?; + records.push(json!({"event":"spin-wire-inventory","status":"pass","exchanges":exchanges,"origin_requests":dispatches, + "bodyless_controls":4,"upload_controls":1,"scope_controls":1,"handoff_controls":1,"qualification":false})) +} + +fn progress_wire(name: &'static str, run: &Path, records: &mut Records) -> Result<()> { + let result = progress_suite( + name, + "outbound-wire", + run, + records, + |port, origins, records| { + let mut matrix = Matrix { + adapter: name, + endpoint: "/progress/redirect", + port, + origins, + records, + exchanges: 0, + dispatches: 0, + }; + matrix.run()?; + Ok((matrix.exchanges, matrix.dispatches)) + }, + )?; + if let Some((exchanges, dispatches)) = result { + records.push( + json!({"event":format!("{name}-wire-inventory"),"status":"pass", + "exchanges":exchanges,"origin_requests":dispatches,"qualification":false}), + )?; + } + Ok(()) +} + +pub(super) fn progress_suite( + name: &str, + suite: &str, + run: &Path, + records: &mut Records, + check: impl FnOnce(u16, &WireOrigins, &mut Records) -> Result, +) -> Result> { + let workerd = if name == "cloudflare" { + let Some(workerd) = executable("MINIFLARE_WORKERD_PATH", "workerd") else { + records.push(json!({"status":"unsupported","adapter":"cloudflare", + "reason":"set MINIFLARE_WORKERD_PATH to the local workerd executable","qualification":false}))?; + return Ok(None); + }; + let version = Command::new(&workerd).arg("--version").output()?; + require(version.status.success(), "workerd version probe failed")?; + records.push(json!({"event":"workerd-runtime","adapter":"cloudflare", + "version":String::from_utf8(version.stdout)?.trim(),"artifact":artifact(&workerd)?,"qualification":false}))?; + Some(workerd) + } else { + None + }; + let revision = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(root()) + .output()?; + require( + revision.status.success(), + "cannot determine wire source revision", + )?; + let revision = String::from_utf8(revision.stdout)?; + let state = if name == "axum" { + run.join("axum-state") + } else { + run.join("retained/worker-state") + }; + let log = run.join(format!("{name}-{suite}.log")); + let mut process = None; + let mut origins = None; + let outcome: Result> = (|| { + let Some((mut command, port)) = + progress::build(name, run, records, revision.trim(), suite)? + else { + return Ok(None); + }; + if let Some(workerd) = workerd { + command + .env("MINIFLARE_WORKERD_PATH", workerd) + .env("WRANGLER_SEND_METRICS", "false") + .env("WRANGLER_LOG_PATH", run.join("wrangler-logs")); + records.push( + json!({"event":"workerd-configuration","adapter":"cloudflare", + "configuration":fs::read_to_string(run.join("wrangler.toml"))?,"qualification":false}), + )?; + } + let origins = origins.insert(WireOrigins::start()?); + let running = process.insert(Process::start(&mut command, &log, port)?); + let result = check(port, origins, records)?; + running.check_running()?; + Ok(Some(result)) + })(); + finish_wire(name, &mut process, origins, &state, records, outcome) +} + +fn finish_wire( + adapter: &str, + process: &mut Option, + origins: Option, + state: &Path, + records: &mut Records, + outcome: Result, +) -> Result { + let process_result = process.as_mut().map_or(Ok(()), Process::finish); + let origin_result = origins.map_or(Ok(()), WireOrigins::finish); + let state_result: Result<()> = if state.exists() { + fs::remove_dir_all(state).map_err(Into::into) + } else { + Ok(()) + }; + for (stage, result) in [ + ("process", &process_result), + ("origin", &origin_result), + ("state", &state_result), + ] { + if let Err(error) = result { + records.push(json!({"status":"fail","adapter":adapter,"stage":stage, + "error":error.to_string(),"qualification":false}))?; + } + } + let inventory = outcome?; + process_result.and(origin_result).and(state_result)?; + Ok(inventory) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pre_startup_failure_removes_partial_state_and_preserves_primary_error() { + let run = std::env::temp_dir().join(format!("wire-cleanup-{}", token())); + let state = run.join("retained/worker-state"); + fs::create_dir_all(&state).unwrap(); + fs::write(state.join("partial-value"), b"synthetic").unwrap(); + let mut records = Records { + path: run.join("events.jsonl"), + values: Vec::new(), + }; + let outcome: Result<()> = Err("build failed before runtime startup".into()); + let error = + finish_wire("cloudflare", &mut None, None, &state, &mut records, outcome).unwrap_err(); + assert_eq!(error.to_string(), "build failed before runtime startup"); + assert!(!state.exists()); + assert!(records.values.is_empty()); + fs::remove_dir_all(run).unwrap(); + } + + #[test] + fn late_origin_failure_is_not_hidden_by_success_or_primary_check_failure() { + for primary in [None, Some("primary lifetime check failed")] { + let run = std::env::temp_dir().join(format!("wire-late-cleanup-{}", token())); + let state = run.join("worker-state"); + fs::create_dir_all(&state).unwrap(); + let mut records = Records { + path: run.join("events.jsonl"), + values: Vec::new(), + }; + let origins = WireOrigins::start().unwrap(); + origins.arm("invalid++"); + let outcome = primary.map_or(Ok(()), |error| Err(error.into())); + let error = finish_wire( + "axum", + &mut None, + Some(origins), + &state, + &mut records, + outcome, + ) + .unwrap_err(); + assert_eq!(error.to_string(), primary.unwrap_or("invalid P5 gate name")); + assert!(!state.exists()); + assert_eq!(records.values.len(), 1); + assert_eq!(records.values[0]["stage"], "origin"); + assert_eq!(records.values[0]["status"], "fail"); + fs::remove_dir_all(run).unwrap(); + } + } + + #[test] + fn source_error_expectations_are_provider_specific() { + assert_eq!(source_error_reason("spin"), "Protocol"); + assert_eq!(source_error_reason("cloudflare"), "Transport"); + assert_eq!(source_error_reason("axum"), "Transport"); + assert_eq!(source_error_reason("fastly"), "Transport"); + } + + #[test] + fn workerd_source_errors_require_the_observed_typed_reason_phase_and_cause() { + let valid = json!({"outcome":{"error":{"kind":"bad_gateway","status":502, + "reason":"Transport","provider":"Cloudflare { phase: ResponseBodyRead, cause: Unknown }"}}}); + source_error(&valid, "cloudflare").unwrap(); + for (field, value) in [ + ("reason", json!("Protocol")), + ("provider", Value::Null), + ( + "provider", + json!("Cloudflare { phase: Fetch, cause: Unknown }"), + ), + ( + "provider", + json!("Cloudflare { phase: ResponseBodyRead, cause: TypeError }"), + ), + ("status", json!(500)), + ] { + let mut changed = valid.clone(); + changed["outcome"]["error"][field] = value; + assert!(source_error(&changed, "cloudflare").is_err(), "{changed}"); + } + } + + #[test] + fn handoff_retains_exact_bytes_and_all_results() { + for bytes in [b"originbid-Abid-Bbid-C", b"originbid-Cbid-Abid-B"] { + handoff(bytes).unwrap(); + } + for index in [0, 6, 11, 16] { + let mut altered = b"originbid-Abid-Bbid-C".to_vec(); + altered[index] |= 0x80; + assert!(handoff(&altered).is_err()); + } + assert!(handoff(b"originbid-Abid-Abid-C").is_err()); + assert!(handoff(b"origin").is_err()); + } + + #[test] + fn body_capture_does_not_clamp_or_erase_nontext_bytes() { + assert_eq!( + body(&json!({"body_bytes":[0,255,128]})).unwrap(), + [0, 255, 128] + ); + assert!(body(&json!({"body_bytes":[256]})).is_err()); + assert!(body(&json!({})).is_err()); + } +} diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 9bb57280a..4300afc75 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -36,8 +36,9 @@ export default defineConfig({ { text: 'Routing', link: '/guide/routing' }, { text: 'Handlers & Extractors', link: '/guide/handlers' }, { text: 'Middleware', link: '/guide/middleware' }, + { text: 'Capabilities', link: '/guide/capabilities' }, { text: 'Streaming', link: '/guide/streaming' }, - { text: 'Proxying', link: '/guide/proxying' }, + { text: 'Outbound HTTP', link: '/guide/proxying' }, { text: 'KV Storage', link: '/guide/kv' }, ], }, diff --git a/docs/audits/2026-09-27-brotli-decoder-memory-accounting.md b/docs/audits/2026-09-27-brotli-decoder-memory-accounting.md new file mode 100644 index 000000000..49d994fb2 --- /dev/null +++ b/docs/audits/2026-09-27-brotli-decoder-memory-accounting.md @@ -0,0 +1,62 @@ +# Brotli Decoder Memory Accounting Audit + +This audit owns `BROTLI_DECODER_FIXED_CHARGE_BYTES = 16,777,216`. It derives an +allocation-payload subtotal for the source-owned non-window storage in EdgeZero's pinned Brotli +response decoder. It does not claim an exact total-heap bound. The ring buffer's `2^WBITS` bytes +are excluded from the fixed term because `brotli_decoder_memory_charge` charges them +independently; the fixed term below includes its additional 66-byte write-ahead/dictionary slack. + +## Audited Graph + +| Package | Version | Registry checksum | +| --------------------- | -------- | ------------------------------------------------------------------ | +| `alloc-no-stdlib` | `2.0.4` | `cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3` | +| `alloc-stdlib` | `0.2.4` | `0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195` | +| `async-compression` | `0.4.43` | `3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8` | +| `brotli` | `8.0.4` | `5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3` | +| `brotli-decompressor` | `5.0.1` | `5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924` | +| `compression-codecs` | `0.4.38` | `ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf` | +| `compression-core` | `0.4.32` | `cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789` | + +`compression-codecs` boxes one `brotli::BrotliState` and constructs it with the pinned +`alloc-stdlib::StandardAlloc` for byte, `u32`, and `HuffmanCode` storage. +`brotli-decompressor`'s `DecodeVarLenUint8` produces at most 255 before the caller adds one, so +every block-type or Huffman-tree count is at most 256. `BROTLI_HUFFMAN_MAX_TABLE_SIZE` is 1080, +and the pinned `#[repr(C)] HuffmanCode` occupies four bytes. The colocated core regression test +checks the compiled state/code sizes and EdgeZero bridge-buffer size. The lockfile checksum gate +protects private table, parser, and ring-layout assumptions that cannot be imported as public +constants. + +## Audited Allocation-Payload Subtotal + +| Allocation family | Maximum bytes | +| ------------------------------------------ | -------------------------------: | +| Boxed `BrotliState` payload reserve | 65,536 | +| Initial context-map Huffman table | `1080 * 4 = 4,320` | +| Block-type and block-length trees | `2 * 3 * 1080 * 4 = 25,920` | +| Literal context modes | `256` | +| Literal and distance context maps | `256 * 64 + 256 * 4 = 17,408` | +| Three Huffman-tree index arrays | `3 * 256 * 4 = 3,072` | +| Three Huffman-tree code arrays | `3 * 256 * 1080 * 4 = 3,317,760` | +| EdgeZero decoder output buffer | `8,192` | +| Ring write-ahead and dictionary-word slack | `42 + 24 = 66` | +| **Source-audited payload subtotal** | **3,442,530** | + +The public 16,777,216-byte fixed charge therefore leaves 13,334,686 bytes within the accounting +reservation above the source-audited payload subtotal. That remainder is deliberately reserved +for async wrapper/generator/replay objects, collection capacity, allocator metadata, and +fragmentation, but this audit does not claim an exact upper bound for those terms. The charge is a +conservative policy reservation, not a complete RSS certificate. + +Encoded source chunks, decoded response retention, task stacks, adapter/SDK storage, and +provider-owned allocations are outside this decoder charge. Those terms are either separately +bounded or remain outside `outbound-complete-resource-accounting`, as documented in the +capability matrix. + +## Change Gate + +The dependency contract script pins every listed normal dependency, version, registry source, and +checksum in both workspace lockfiles. Any change to a pinned source, decoder construction path, +allocator, grammar limit, table-size constant, or bridge buffer requires repeating this source +audit before retaining or changing the fixed charge. Allocation measurements may be added as +regression evidence, but they do not replace this source-level accounting proof. diff --git a/docs/audits/2026-09-29-flate-decoder-memory-accounting.md b/docs/audits/2026-09-29-flate-decoder-memory-accounting.md new file mode 100644 index 000000000..2efcd88bb --- /dev/null +++ b/docs/audits/2026-09-29-flate-decoder-memory-accounting.md @@ -0,0 +1,35 @@ +# Flate Decoder Memory Accounting + +This audit pins the flate decoder graph used by `edgezero-core` for HTTP +`deflate` and gzip response decoding. The audited graph is locked to: + +- `async-compression 0.4.43` +- `compression-codecs 0.4.38` +- `compression-core 0.4.32` +- `flate2 1.1.9` with the Rust backend +- `miniz_oxide 0.8.9` +- `adler2 2.0.1`, `crc32fast 1.5.0`, and `simd-adler32 0.3.9` + +The lockfile versions, registry source, checksums, metadata graph, and direct +normal dependencies are enforced by `scripts/check_flate_dependency_contract.mjs` +for both the root workspace and the excluded `examples/app-demo` workspace. + +## Charge + +The pinned `miniz_oxide` state reserves a 32 KiB sliding window and a 64 KiB +inflate state. The codec wrapper reserve is 4 KiB and the shared EdgeZero +stream output buffer is 8 KiB. The measured source subtotal is therefore +73,728 bytes before allocator metadata, fragmentation, task stacks, and host +buffers. `FLATE_DECODER_FIXED_CHARGE_BYTES` is pinned to 1 MiB, giving more +than 14x headroom over that subtotal while keeping the public policy uniform +for Brotli, gzip, and deflate. + +The charge is a conservative guest-visible policy reservation, not a claim +about total process RSS. The documented outbound memory model continues to +exclude allocator metadata, fragmentation, adapter/SDK buffers, source-owned +chunks, output/body buffers, task stacks, and provider host allocations. +Those exclusions are why complete resource accounting remains a separate +capability. + +Any dependency upgrade must repeat this audit, update the exact pins and +checksum contract, and re-evaluate the fixed charge before it is merged. diff --git a/docs/audits/2026-10-18-outbound-api-approval.md b/docs/audits/2026-10-18-outbound-api-approval.md new file mode 100644 index 000000000..dc03b9e3f --- /dev/null +++ b/docs/audits/2026-10-18-outbound-api-approval.md @@ -0,0 +1,698 @@ +# Outbound API Review and Approval Record + +## Decision Status + +Prepared on 2026-10-19 for [task G4](https://github.com/IABTechLab/edgezero/blob/7cccaf10734914161803317aec6c55c22c2bccc9/docs/superpowers/plans/2026-10-18-trusted-server-outbound-01-gates.md#task-g4-maintainer-api-and-feasibility-approval). +**API-only approval: AG, 2026-10-19.** The requester replied `continue` to the conversation +question naming the exact Interfaces digest below, then supplied `AG` as the approving maintainer. +That scoped response authorizes API implementation, not Fastly policy, capability promotion, +requirement changes, dependency repinning or production adoption. The other decisions remain +pending. Preparing this record or passing tests does not supply their approval. + +The [2026-10-24 region-enum amendment](#scoped-region-enum-amendment) records the requester's +later, narrower approval separately. The [scoped timing and native integration amendment](#scoped-timing-and-native-integration-amendment) +records the subsequent implementation authorization. Neither supplies a newly approved +whole-contract digest or changes the frozen Interfaces signatures. + +The inspected checkout is PR 275 at `7cccaf10734914161803317aec6c55c22c2bccc9` plus the uncommitted +G1-G3 work and Spin response-egress repair. Its local characterization does not qualify that commit. +The [implementation index](https://github.com/IABTechLab/edgezero/blob/7cccaf10734914161803317aec6c55c22c2bccc9/docs/superpowers/plans/2026-10-18-trusted-server-outbound-implementation.md) +continues to require a buildable hard migration preserving the implemented outbound baseline. +There is no compatibility branch, alternate transport or lazy default proposed here. + +| Decision ID | Decision | Approver | Decision Date | Approved Subject Digest | +| -------------------- | ---------------------------------------- | ------------------------------------------ | ------------- | ------------------------------------------------------------------- | +| G4-API | Approved, API only | AG; scoped exact-digest conversation reply | 2026-10-19 | `c623e7fc5b189e2d9122756f29297a92031a9936b13c6cc475484d4e028a4a7a` | +| G4-SCOPE-CAPTURE | Authorized scoped architectural redesign | Requester; explicit conversation reply | 2026-10-27 | Structured ownership design below; not the earlier closure proposal | +| G4-FASTLY-SLACK | Approved, scoped retirement | Requester; explicit conversation reply | 2026-10-27 | Remove slack rejection; retain original deadlines, B31 and limits | +| G4-FASTLY-POLICY | Pending; local primitive failed | Unset | Unset | Unset | +| G4-FASTLY-B31 | Approved, scoped implementation only | Requester; scoped conversation reply | 2026-10-25 | Proposal referenced below; not new whole-contract digest approval | +| G4-OPTIMIZER-HORIZON | Pending; finite rule unproved | Unset | Unset | Unset | +| G4-APPLICABILITY | Pending; G1 scope lock open | Unset | Unset | Unset | +| G4-MEASUREMENTS | Pending; replay definitions unconfirmed | Unset | Unset | Unset | + +The [2026-10-25 policy/readiness packet](2026-10-25-fastly-backend-policy-proposal.md) now proposes +B31 and a B1 comparison with complete aggregate-budget arithmetic. On 2026-10-25 the requester +selected `B = 31 bounded ladder (Recommended)` as the proposal to finalize for review. The question +explicitly excluded production integration, runtime changes and hosted qualification. +That selection alone did not authorize implementation. The subsequent scoped approval below +authorizes B31 production policy changes; the full G4-FASTLY-POLICY resource/primitive decision +remains pending. +Review resource exposure, emitted settings, anchor rules, provider maxima and the repaired-versus- +stock primitive disposition separately. Preparing or selecting a proposal does not authorize +runtime promotion or hosted execution beyond the separately approved scope. + +The scoped API response and its exact digest are attributed to the maintainer name supplied in +the conversation, not inferred from Git. This does not close G4 as a whole. A completed +decision must retain its maintainer, date, exact subject digest and scope. Proposal hashes do not +approve the other subjects; missing, rejected or stale approval blocks their dependent work. + +## Scoped B31 Implementation Approval + +On 2026-10-25 the requester answered `Approve scoped B31 implementation` to this question: + +> May I implement the B31 aggregate-budget backend policy from the review packet in PR #275, +> including stable backend identity and ceiling-rounded timers, while preserving original logical +> deadlines? This would authorize those production policy changes only; runtime-pin changes, the +> readiness engine, and hosted qualification would remain separate. + +The referenced [proposal](2026-10-25-fastly-backend-policy-proposal.md), SHA-256 +`011b090e21c7e8e6424b53303c55b1b2eacb7c896e51a81b6492c13fa65ce3e1`, remains the historical +submission. This is an attributed scoped authorization, not a new exact-digest API approval or +approval of every resource/primitive decision listed in that proposal. It supersedes the D1a/G2 +sequencing dependency only for the existing aggregate-policy Send/Batch backend identity and timer +implementation. Keep current dispatch guards, pending selection, body reads and error handling; +do not remove slack or change runtime pins/readiness as part of this authorization. + +Configured per-call timeouts select a stable, seven-day-clamped anchor even when a caller deadline +or observation cutoff is earlier. Deadline-only/default work uses the originally selected duration, +not the later remainder. Ceiling-millisecond rounding then selects one of 31 coupled provider +timer vectors. Names are versioned, configuration-derived and client-independent. A shared verified +registry must compare full identities, serialize registration, retain successful late registrations +before reporting the original timeout, and reject unknown `NameInUse` rather than trusting it. + +At that checkpoint the transport still captured its budget at async method entry, as documented +by the Send-first checkpoint; that approval did not claim facade-invocation capture or the atomic +trait migration. The subsequent scope amendment and implementation dashboard track that migration. +Explicit Connect/idle options, retirement/total resource bounds, host enforcement, pooling, +optimizer horizon and hosted qualification remain separate work. No public signature, application +policy, portable cap, timeout cause or logical deadline changes under this scope. + +## Original Exact API Subject + +### Reopened Invocation-Scope Interface (2026-10-27) + +The atomic implementation review found that the approved response context requires an +invocation-time local progress-scope token, but the frozen implementer surface provides no way +to capture/install it across crate boundaries. Capturing at dispatch, timer polling or collection +is too late for a retained, never-polled Send future; binding it at client construction would +incorrectly retain a previous request's scope. This is an implementation-interface gap, not a +provider impossibility or permission to weaken request-lifetime cleanup. + +The initial narrow proposal added a required `capture_scope` hook returning an opaque local +`OutboundScope`, read-only scope access on prepared contexts, and provider-facing construction, +activation and cancellation-registration operations. Activation restores the previous scope on +drop and cannot create, close or reparent a captured lifetime. Check/register alone is insufficient: +Begin or Batch work later polled inside another response must retain its original provider +runtime/progress identity. Pending/group registrations survive completion of the outer Begin +future; successful streamed handoff changes cleanup ownership without resetting deadlines. + +The requester answered that EdgeZero owns this interface, requested an ideal architecture with +"No hacks!", and explicitly replied `ok to reopen the interface!`. The earlier closure-based +proposal is not a constraint to preserve. Review shared typed scope ownership against a +provider-owned protocol, record the chosen contract, and hard-migrate its implementation and +callers. No compatibility default, clock/timer side channel or retained obsolete path is authorized. +Reopening is explicit, not inferred from the current implementation or passing tests. +Canonical callback/body owners, unpolled-root revocation and retained-handle lifetime coverage +remain open in the current dashboard in +`docs/superpowers/plans/2026-10-18-trusted-server-outbound-implementation.md` (2026-10-27). +The original Interfaces and requirements digests below remain historical, not restrictions on +this authorized redesign. Record new contract bytes and scope separately rather than silently +refreshing the original approval subject. Runtime promotion and hosted qualification remain separate. + +The selected architecture uses one core `OutboundScopeOwner`/`OutboundScope` implementation and +private revocable operation roots. The required client `progress` hook supplies provider polling, +not cancellation registration or a second scope registry. Independent adapters share the core +progress bridge; Fastly retains only cooperative SDK scheduling and the existing multiplexer. +There is no `capture_scope` callback interface, compatibility default or alternate transport. + +For early body work, `response_body()` creates a provisional owner, `with`/`run` starts work in +that owner, and consuming `attach(response)` carries it in local body metadata. Attachment alone +cannot escape callback cleanup. Canonical egress accepts the exact owner before policy, framing +or suppression; successful outbound streamed collection likewise accepts its body lifetime before +handoff. Provider progress is shared through a request-family cache while callback and body roots +remain independently revocable. EOF is not terminal egress. Failed application body work closes +before bounded fallback delivery. Buffered representation and payload bytes are unchanged. + +`Body` becomes a private carrier with a separate `BodyPayload` transport representation; all four +adapters, supplied clients, demo and templates hard-migrate together. Local ownership does not +enter HTTP extensions, shared clients or App state. Original deadlines, timeout causes, partial +batch/receipt semantics, coding/provenance, independent limits and native Image Optimizer behavior +remain mandatory. This is the requester's scoped architecture authorization, not a claim of a new +exact-digest approval or hosted evidence. + +The same core revocation protocol backs the fallible `OutboundTask` runner/owner pair consumed +by Axum, Cloudflare and Spin. Executor drop cannot retain producer futures through another public +handle, and failed registration returns before provider spawning. One Body source-binding path +serves outbound and downstream egress; one core predicate handles earlier-own-deadline cutoff +classification. Nonblocking completion observation does not drive SDK progress. These are +consolidations of the selected architecture, not independent compatibility implementations. + +| Scope Amendment Identity (2026-10-27) | Value | +| ---------------------------------------- | ------------------------------------------------------------------ | +| Interfaces SHA-256 | `b69a75ba96b69c0979c05b8711f9cacbaad0abef6a6caa053ed77d21916bbba4` | +| Complete plan SHA-256 | `10776c6db1bfacaecca91a32c9ad955ad13f696ef8d078312e5b566b43f6205b` | +| Interfaces size | 856 LF-terminated lines, 53,795 UTF-8 bytes | +| Unchanged normative requirements SHA-256 | `a3d58bcabe7e9e556d21227de973c660794d25606b7ee3365bd7808d3d101c70` | + +These identify the implemented scope proposal; they do not overwrite the original AG approval +subject or convert scoped conversation authorization into a new exact-digest review. + +### Approved Dispatch-Slack Retirement (2026-10-27) + +Code inspection found that Fastly's retained dispatch-slack guard measures from the invocation +snapshot, including bounded GET/HEAD settlement. A body that settles after 25 ms can therefore +encounter a dispatch Internal error even while its original logical deadline remains live. +Resetting the budget or silently changing the guard's anchor would not satisfy the approved +contract. The requester explicitly answered `Approve dispatch-slack retirement` to the scoped +question authorizing removal while preserving original absolute deadlines, B31, typed timeout +causes and resource limits. + +The implementation removes the elapsed-time rejection, its public constant and test-injection +hook instead of retaining aliases. Absolute-expiry and backwards-clock checks remain at dispatch +and registration boundaries. Regression and target verification are required before completing +this checkpoint. This approval neither authorizes a runtime-pin change nor supplies hosted +qualification, a hard interruption guarantee or a capability promotion. + +### Frozen Original Subject + +Review the complete [Interfaces: Proposed Hard-Cut Contract](https://github.com/IABTechLab/edgezero/blob/7cccaf10734914161803317aec6c55c22c2bccc9/docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md#interfaces-proposed-hard-cut-contract), +not just its opening trait declaration or the summaries in this packet. The subject includes every +signature, supporting type, ownership/default rule, migration table and acceptance sentinel in +that section, ending immediately before `## Task P1:`. It is referenced once rather than copied +into a second potentially conflicting contract. +These source links use the immutable submission revision because internal plans are excluded from +the published documentation site. The identities below are historical, not the amended local file. + +| Property | Submitted Value | +| ------------------------------ | -------------------------------------------------------------------------- | +| Source | `docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md` | +| Section | Lines 18-696 inclusive, 679 LF-terminated lines, 42,166 UTF-8 bytes | +| Section SHA-256 | `c623e7fc5b189e2d9122756f29297a92031a9936b13c6cc475484d4e028a4a7a` | +| Complete source-file SHA-256 | `c52b4aea6b8d2793f30784e88016a3676088d344c0a5607774a5d9f1a7ffae80` | +| Normative requirements SHA-256 | `5d9833ebb13bf191377eeec6f9ada89d236044546701d01478c7f12f65b175ec` | + +Run from the repository/worktree root to verify the original subject and historical region amendment. The extraction includes +the trailing blank lines before the next task; it does not normalize whitespace or line endings. +Any changed Interfaces byte needs a new digest and review. Changes elsewhere in the plan or in +the requirements also require reviewing their effect, not silently refreshing these identities. + +```sh +node --input-type=module <<'JS' +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; + +const path = 'docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md'; +const revision = '7cccaf10734914161803317aec6c55c22c2bccc9'; +const source = execFileSync('git', ['show', `${revision}:${path}`], { encoding: 'utf8' }); +const start = source.indexOf('## Interfaces: Proposed Hard-Cut Contract\n'); +assert.notEqual(start, -1); +const end = source.indexOf('## Task P1:', start); +assert.ok(end > start); +const section = source.slice(start, end); +const digest = value => createHash('sha256').update(value).digest('hex'); +assert.equal(Buffer.byteLength(section), 42166); +assert.equal(section.split('\n').length - 1, 679); +assert.equal(digest(section), 'c623e7fc5b189e2d9122756f29297a92031a9936b13c6cc475484d4e028a4a7a'); +assert.equal(digest(source), 'c52b4aea6b8d2793f30784e88016a3676088d344c0a5607774a5d9f1a7ffae80'); +const requirementsPath = 'docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md'; +const requirements = execFileSync('git', ['show', `${revision}:${requirementsPath}`]); +assert.equal(digest(requirements), '5d9833ebb13bf191377eeec6f9ada89d236044546701d01478c7f12f65b175ec'); +const regional = execFileSync('git', ['show', `f4d660b1054a195ec923d78c1375358dc7b8b9fe:${path}`], { encoding: 'utf8' }); +const regionalStart = regional.indexOf('## Interfaces: Proposed Hard-Cut Contract\n'); +assert.notEqual(regionalStart, -1); +const regionalEnd = regional.indexOf('## Task P1:', regionalStart); +assert.ok(regionalEnd > regionalStart); +assert.equal(digest(regional.slice(regionalStart, regionalEnd)), '0b1b2e132dc3e294e117a1a5d2bdb65c49693eab47b93e7926220612fd595404'); +assert.equal(digest(regional), '39c2ae866be8775b6b34879cbabff92a97f1c9f7010d76f275027690c0b6dd51'); +const current = readFileSync(path, 'utf8'); +const currentStart = current.indexOf('## Interfaces: Proposed Hard-Cut Contract\n'); +const currentEnd = current.indexOf('## Task P1:', currentStart); +assert.ok(currentStart >= 0 && currentEnd > currentStart); +assert.equal(digest(current.slice(currentStart, currentEnd)), 'b69a75ba96b69c0979c05b8711f9cacbaad0abef6a6caa053ed77d21916bbba4'); +assert.equal(digest(current), '10776c6db1bfacaecca91a32c9ad955ad13f696ef8d078312e5b566b43f6205b'); +assert.equal(digest(readFileSync(requirementsPath)), 'a3d58bcabe7e9e556d21227de973c660794d25606b7ee3365bd7808d3d101c70'); +console.log('Historical and scope-amendment identities match; authorization remains separately scoped.'); +JS +``` + +### Scoped Region Enum Amendment + +On 2026-10-24 the requester explicitly asked for an enum and replied `ok` to the proposal for a +public, core-owned seven-variant region enum, typed `parse`/`as_str` conversions, corresponding +spec/tests and no compatibility wrapper. This is approval of that described API change, not a +claim that the requester reviewed the new whole-section digest computed afterward. + +The regional [Interfaces contract](https://github.com/IABTechLab/edgezero/blob/f4d660b1054a195ec923d78c1375358dc7b8b9fe/docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md#interfaces-proposed-hard-cut-contract) +differs from the original only by declaring `OutboundImageOptimizerRegion::{Asia, Australia, +EuCentral, EuWest, UsCentral, UsEast, UsWest}` and specifying its Copy/Clone/Debug/PartialEq/Eq, +direct construction and exhaustive matching semantics. Options alone retain private fields; +existing getters and canonical token/error behavior are unchanged. No other interface or gate +is amended. + +| Historical Regional Identity | Value | +| ---------------------------- | ------------------------------------------------------------------ | +| Interfaces SHA-256 | `0b1b2e132dc3e294e117a1a5d2bdb65c49693eab47b93e7926220612fd595404` | +| Complete plan SHA-256 | `39c2ae866be8775b6b34879cbabff92a97f1c9f7010d76f275027690c0b6dd51` | +| Interfaces size | 683 LF-terminated lines, 42,364 UTF-8 bytes | + +These are verification identities, not fresh exact-digest approval evidence. The original +AG approval remains attributed as recorded above; this amendment is attributed to the requester's +explicit conversation approval. The requirements file had already changed since the original +submission; the historical requirements digest above is not asserted against today's file. +This amendment does not change requirements or approve their intervening changes. Fastly policy, +optimizer horizon, applicability, measurement and hosted-qualification decisions stay open. + +The current complete-plan identity also includes the 2026-10-24 Task P2 sequencing +clarification outside Interfaces: Axum can preserve its existing transport-certified empty +body during ingress conversion independently of outbound settlement. This implements an +existing subtask without changing public signatures, core deadline precedence or any provider +policy. Interfaces and normative requirements are unchanged by this checkpoint. This maintenance +identity is not new API, feasibility or policy approval; the original approval identities above +remain historical. + +### Scoped Native Optimizer Parity Instruction + +On 2026-10-24 the requester approved generic typed request options mapped directly to the native +Fastly optimizer, retaining streaming, cache/backend reuse, exact targets/signature-bearing headers, +all outbound limits and cleanup, without an extra fetch or representation layer. Profiles, SVG +selection, query stripping and S3 signing/preflight remain application policy. The instruction also +requires a measured performance comparison against the native application path and an SDK-only +control rather than inferring parity from API shape. + +This scope is recorded in requirements EZ-8.12 and the existing EZ-8 hosted criterion, plan D1d, +and qualification Q4b/runbook. It does not change the approved Interfaces or supply a numerical +B/H(K), performance thresholds, a staging service/entitlement, deployment authorization or passing +evidence. G2 and the corresponding implementation/qualification gates remain open. No production +optimizer path was implemented under this instruction while those prerequisites are missing. + +### Included Contract Decisions + +The full migration decisions below remain the target contract. The +[scoped amendment](#scoped-timing-and-native-integration-amendment) permits a Send-first checkpoint +on the current trait before that atomic migration; it supersedes only the stated implementation +prerequisites, not these signatures or the remaining acceptance requirements. + +- One required `dispatch_head` network primitive, synchronous support validation and paired + `time()`, plus mandatory eager `begin`/batch scheduling. Supplied implementations use the same + protocol. `OutboundHttpClient` stays object-safe and `Send + Sync`; owned futures, heads and + cleanup owners may be local and capture `Rc`. No unsafe `Send` wrapper or core runtime is added. +- Facade invocation captures the original entry time. Unsupported options reject before budget + rejection, timer registration, inbound settlement or network activity. `OutboundTime` pairs the + application clock with the adapter's real wakeup factory; clock-only constructors hard-migrate. + Early/frozen-clock wakeups recheck time and yield/rearm without extending the captured deadline. +- Request builders and readbacks cover connect timeout, decode/original response delivery, + marker override and the complete typed Image Optimizer subset. Marker default remains **on**; + client off and per-request Inherit/On/Off resolve into one effective policy. Decode stays default; + original identity responses still enforce their decoded limit. Existing independent caps, + pre-normalization header accounting, Content-Type provenance and typed errors remain intact. +- Heads own encoded bodies and one SDK cleanup owner. Header limits and immutable Location + evidence precede filtering; body disposition precedes length checks, decoding and collection. + Discard never polls payloads. Collected streams retain ownership instead of being cancelled + when their pending handle completes. No SDK resource receives two independent release callbacks. +- Prepared batches and their contexts retain original indices, start, paired time, cutoff, + selected budgets and exact preflight outcomes. External producers receive these through the + facade, not unchecked public constructors. Drivers emit raw timestamped observations; only core + creates immutable consumer receipts and derives elapsed/cutoff eligibility. +- Mixed collection uses `AlreadyDelivered`, `Resolved` and `Unresolved`, not ambiguous `Option` + slots. Receipts and collections have read-only access and consuming decomposition. A batch-bound + accumulator preserves ownership on rejected insert/merge, including the distinct original + driver error. EOF/trailing validation, poisoning, exact-once release and the extra poll for + `Finished(Completed)` remain. Cancel returns all never-delivered indices, including recorded + but unconsumed results; consumers check first to retain those results. +- Explicit follow uses the same client's head pipeline. The complete redirect limit/context, + prepared-request transfer, actions, generic non-Clone/non-Send policy error, stop reasons and + success/error readbacks are part of this submission. The initial absolute chain deadline survives + replacements; each hop captures its own entry and uses its selected request's timeout. Followed + heads are discarded without payload reads. +- `BudgetSource::ConnectTimeout` and `IdleTimeout` extend the existing non-exhaustive enum; + aggregate budget selection and public wire text do not gain a second cause field. Governing + expiry wins connect ties; expired application/chain absolute deadlines win idle ties and late + observation, as the requirements specify. +- Fastly's request-scoped observation handle, immutable snapshot/getters, extension access and + `current()` scope are included in the API subject. Failed/late registration attempts count; + retained snapshots do not retain work or attach to the next request. This is not approval of + an unproved Fastly scheduling/backend implementation. + +Arrival recording is subject to EZ-6.11's actual scheduling opportunities: Fastly observes during +EdgeZero operations; the other adapters also observe when the application yields to their +executor. CPU-only work or synchronously ready awaits do not promise background observation or +allow backdating a terminal instant. No summary here expands that normative contract. + +The optimizer mode matrix is also fixed: Fastly Send/Follow only, subject to entitlement and +horizon gates; Fastly Begin/Batch and all modes on other adapters reject it synchronously. + +### Scoped Timing and Native Integration Amendment + +**Decision G4-TIMING-INTEGRATION: approved by the requester in the conversation on 2026-10-24.** +The requester answered `Approve scoped change and continue implementation` to this question: + +> May I make a scoped requirements change to unblock implementation: preserve original logical +> deadlines and late-result rejection, treat +5ms as a measured qualification target rather than +> a hard scheduler guarantee, and allow native Image Optimizer integration with its blocking +> horizon explicitly unknown until provider evidence exists? Hosted qualification and performance +> claims would remain blocked. + +This is an attributed scoped authorization, not numerical H(K)/B approval, approval of different +signatures, or a claim that a new whole-contract digest was reviewed. The original API approval, +historical submitted digests and region-enum amendment retain their separate scopes. The current +Interfaces bytes are unchanged; a refreshed complete-plan digest verifies only edits outside them. + +This section takes precedence over earlier implementation-blocking G2 timing and optimizer-horizon +wording in this packet, the requirements, plans, feasibility index and fixture documentation: + +- Preserve original entry `t0`, absolute deadline `D` and `BudgetSource`. Do not extend or round + the logical deadline, restart the budget after preparation, accept a late result, or send after + observed expiry. A synchronous optimizer return at or after `D`, success or failure, becomes the + original attributed 504. Before expiry, missing optimizer entitlement remains a typed 502 with + the unchanged message `Fastly outbound request failed`; its typed reason/provider detail carry + the distinction, not new display text. +- Keep +5 ms as the measured qualification target and retain its tests, raw timings, cleanup + endpoints and negative results. It is not a deterministic scheduler-resumption guarantee. + Timing-only G2 failures no longer prohibit the scoped integration; they are not relabeled passes. +- The synchronous optimizer blocking horizon is explicitly **UNKNOWN** until provider evidence. + Finite numerical H(K) is no longer a preimplementation prerequisite. Do not infer it from SDK + timers, observed maxima, runtime termination or a fabricated policy. Horizon approval and hosted + evidence still gate qualification; an unknown horizon cannot pass a finite-bound assertion. +- Under this 2026-10-24 amendment, backend B/domain/cardinality and resource-policy approval + remained pending. The subsequent scoped B31 approval above supersedes that implementation + restriction only for aggregate backend identity/timers. No dispatch slack, resource limit or + SDK/runtime pin changes are authorized. + Typed body-error preservation and fresh-readiness/subsequent-read safety remain requirements; + no blocking-body fallback or lost-error-to-EOF conversion is permitted. + +The [D1d Send-first checkpoint](https://github.com/IABTechLab/edgezero/blob/docs/outbound-http-spec/docs/superpowers/plans/2026-10-18-trusted-server-outbound-03-drivers.md#send-first-checkpoint-2026-10-24) +is an implementation sequencing decision under this integration authorization. Attach typed +options to requests/parts, validate support first on all four adapters, and map Fastly single +Send directly to SDK options inside existing `execute` and `process_response`. Keep the ordinary +SDK send branch and current `OutboundHttpClient` trait unchanged. No separate transport, +compatibility helper, default implementation or alias is introduced. This does not claim the +requester explicitly approved alternative signatures. + +For this checkpoint, budget selection remains at existing adapter async `send` method entry +on first poll, before SDK preparation/registration. Preserve that selected budget across the native +call; this is not a new facade-invocation snapshot or completion of P1. Invocation-time capture +remains pending with the later atomic head-first/full-trait migration. The full contract's +invocation-time target is unchanged, not asserted as implemented by this subset. + +Full EZ-1 settlement, Follow, new-mode backend integration, the new engine and the head-first/full-trait +migration remain open; the latter still migrates all implementers/callers atomically. The +checkpoint does not complete D1d or any full criterion. No deployment, Native capability promotion, +hosted qualification or performance-parity claim is authorized. Existing failed evidence, +255 acceptance criteria and 511 legs retain their IDs, statuses, applicability and comparison gates. + +### Paired-Time Sequencing Approval (2026-10-26) + +The requester answered `Approve this sequencing change` in this conversation to: + +> May I move paired clock/timer integration ahead of the full trait migration? This would +> hard-migrate all four adapters to the already-approved `with_time`/`try_with_time` +> constructors, wire `OutboundTime` into live deadline handling, and migrate every caller +> without aliases. Public trait, batch-receipt and head-finalizer changes would remain one +> later atomic cut. + +This supersedes only the earlier constructor/paired-time sequencing dependency. Use the +existing approved names and one live paired time source per client, including request injection +and real wakeups. Remove clock-only constructors; add no compatibility aliases, inert timers, +public factory helpers or standalone trait `time()` transition. Preserve support-first checks, +original deadlines, typed timer failures, source/transport cleanup and provider diagnostics. +Fastly timers participate in its cooperative multiplexer rather than blocking before source +polling. Keep application-clock/frozen-clock checks and host-event yields on the other targets. + +The public trait, prepared contexts, batch identity/receipts, head finalization and caller +protocol still migrate together later. The frozen Interfaces and requirements are unchanged. +No SDK/runtime pin, B31/slack policy, capability level or hosted qualification is approved here. + +#### Paired-Time Verification + +The live checkpoint removes clock-only constructors from all four adapters and every caller; +standard injectors retain the exact application clock and pair it with a crate-private runtime +factory. The default constructors use real clocks and timers, not inert waits. Timer setup +precedes source/network polling, supplied failures retain their type and detail, and clock +high-water observations persist across chunks and cooperative yields. Terminal streams release +their source, timer and factory capture before yielding an error, even if the wrapper is retained. + +Fastly's existing progress engine owns its timer registrations and original operation boundary. +Nested engines respect all ancestor deadlines, including custom-pair uploads without pending +handles; child progress causes parent repoll before another host wait. Expired/unconsumed +mailboxes do not block live siblings. Wakes and removed timer state are released outside state +borrows. No extra transport, per-slot blocking timer or public factory API was added. + +Two independent review passes found cross-chunk clock-regression, timer-diagnostic and nested +progress/cancellation defects; deterministic regressions reproduced them and pass after repair. +Fresh local verification passes all five required Rust gates: formatting, workspace strict +Clippy, 2,293 workspace tests (one existing ignored test), all-target adapter feature compilation +and the Spin WASM check. Strict Clippy also passes for the three provider WASM targets. Runtime +tests pass: Cloudflare 53 library/29 contract cases, Fastly 238 library/36 contract cases and +Spin 15 SDK-resource cases on Wasmtime 48.0.2. The deterministic Spin helper uses its controlled +wakeable pair rather than polling a WASI 0.3 host timer under a non-WASI test executor. + +Hosted CI additionally exposed test-helper declaration order in the isolated `cloudflare` +feature build, masked in `cloudflare,test-utils` by the existing module grouping expectation. +Move the helper after its fixtures rather than adding a suppression. Both isolated feature +combinations now pass strict WASM Clippy; the bare-feature contract also passes its 12 cases. + +Demo workspace tests, reusable fixture tests, dependency/legacy/documentation guards, the +evidence checker tests and docs format/lint/build pass. The final actual-client Fastly probe +suite passes its receive cases on the tracked repaired Viceroy; Spin's local component smoke +passes functional cases. Both correctly exit 2 with qualification still open. They do not prove +hosted lifecycle timing, provider allocation bounds, callback-wide progress or end-client receipt. +No deployment occurred. The requirements and frozen Plan 02 hashes remain unchanged. + +This completes only the approved paired-time sequencing checkpoint. Shared support-first +settlement/context issuance, the public trait and batch-receipt hard cut, eager Begin, +head finalization integration, redirects and hosted qualification remain separate work. + +## Hard-Migration Inventory + +This is the submission-time worktree inventory, including untracked G3 callers, not a claim that the +migration is implemented. Repeat the discovery commands before editing; new callers must join +the same cut. A separately approved portable API does not authorize a broken Fastly build, +stubbed transport, compatibility trait or removed baseline regression test. + +| Existing Surface | Submitted Replacement | +| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| Implementer `async send(OutboundRequest)` | Required `dispatch_head(OutboundDispatchRequest)`, `time()` and synchronous support validation | +| Implementer raw request-vector batch | `start_batch_until(OutboundPreparedBatch)` and required eager `begin(OutboundDispatchRequest)` | +| Borrowing facade send/collection futures | Owned local `'static` futures with invocation-time capture; `new`/`with_client` keep their signatures | +| Axum `try_with_clock` / internal `with_transport_and_clock` | `try_with_time` / `with_transport_and_time`; `try_new` stays fallible | +| Fastly/Cloudflare/Spin `with_clock` | `with_time(OutboundTime)`; default constructors pair real clocks and timers | +| Raw-count batch construction, `from_futures`, `finish_batch_item` | Context-consuming `cutoff`/`from_driver`, raw driver observations and a group cancellation owner; remove superseded paths | +| Public item/result/failure fields and receipt constructor | Private identity with inspection/decomposition and lossless accumulator recovery; no reconstruction or mutable compatibility accessor | +| Direct adapter behavioral calls in tests | Facade calls through `HttpClient::with_client`; generic implementer assertions remain trait assertions | + +### Eight Implementations + +| Current File | Implementation / Migration Surface | +| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `crates/edgezero-adapter-axum/src/outbound.rs:174` | `AxumOutboundClient`; constructors 161/165, future factory 212, clock tests 521/544 | +| `crates/edgezero-adapter-fastly/src/outbound.rs:562` | `FastlyOutboundClient`; constructors 300/307, completion helper 170, raw driver 653 | +| `crates/edgezero-adapter-cloudflare/src/outbound.rs:363` | `CloudflareOutboundClient`; constructors 331/338, future factory 401, clock tests 1097/1126 | +| `crates/edgezero-adapter-spin/src/outbound.rs:408` | `SpinOutboundClient`; constructors 296/303, future factory 446, clock tests 1049/1079 | +| `crates/edgezero-core/src/outbound.rs:1982` | `MockClient`; trait 419, facade 567, batch fields 124, factories 254/278 and helper 1307 | +| `crates/edgezero-core/src/context.rs:768` | `DummyOutboundClient`; raw driver 786 and context-client injection 1165; accessor 372 stays | +| `examples/app-demo/crates/app-demo-core/src/handlers.rs:648` | `TestOutboundClient`; application fields 196-199, item 683, driver 691 and injections 1300/1425 | +| `crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs:338` | Template client; application fields 156-159, item 368, driver 376 and injections 595/643 | + +### Callers, Injectors and Guards + +| Files | Migration Responsibility | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `crates/edgezero-adapter-axum/src/request.rs:69` | Clock-only constructor branches 69/70 and injection 75; preserve supplied-client precedence | +| `crates/edgezero-adapter-fastly/src/request.rs:732` | Clock-only constructor/injector 732/733, helper paths 727/826/861/889 and batch test 1068 | +| `crates/edgezero-adapter-cloudflare/src/request.rs:351` | Clock-only constructor/injector 351/352, paths 347/498 and batch test 982 | +| `crates/edgezero-adapter-spin/src/request.rs:152` | Clock-only constructor/injector 152/153, paths 143/535 and batch test 907 | +| `crates/edgezero-adapter-axum/src/service.rs`, `dev_server.rs`, `response.rs` | Preserve shared transport and installed LocalSet; integrate independent callback/body owners under D2 | +| Cloudflare and Spin `src/response.rs`; Fastly `src/context.rs` and serving scope | Adapter-specific lifecycle/scope integration under D1/D3/D4; retain the current Spin repair | +| `crates/edgezero-adapter-axum/tests/contract.rs:67` | Direct sends/batches at 67/98/256 and collection/item fields; runtime evidence remains D2/D5 work | +| `crates/edgezero-adapter-fastly/tests/contract.rs:1303` | Direct batches 1303/1332 and clock constructor 1327; runtime evidence remains D1/D5 work | +| `crates/edgezero-adapter-cloudflare/tests/contract.rs:482` | Clock constructors 482/517/547/566, direct batches 492/522/551/578 and collection 632 | +| `crates/edgezero-adapter-spin/tests/contract.rs:662` | Clock constructors 662/695/723/743, direct batches 673/701/728/756 and collection 822 | +| `crates/edgezero-adapter-spin/tests/sdk_resources.rs:24` | Retain trait assertion 24; migrate clock constructor 43, batch 56 and slot access 61 | +| `crates/edgezero-cli/src/generator.rs:1783` | Literal assertions 1783/1786-1791 for `failure.error`, elapsed, clocks/imports and driver construction change with the template, not later | +| `tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs:251` | Migrate manually constructed clock-only client 251; facade send 253 remains | +| `tests/fixtures/reusable-app/crates/fixture-core/src/lib.rs:208` | Preserve context-client send behavior and validate changed preparation | +| `tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs:157` | Untracked G3 caller: facade send 176, batch 181 and Option-slot consumption 184-188; do not rewrite historical evidence | +| `crates/edgezero-core/src/lib.rs`, `src/error.rs`, `src/time.rs` | Export reviewed supporting types/causes; preserve aggregate budget selection and original clock domain | +| New `crates/edgezero-core/src/outbound/exchange.rs`, `image_optimizer.rs`, `progress.rs`, `redirect.rs` | P1-P5 implementation modules, not alternative transports | +| New `crates/edgezero-core/tests/outbound_public_api.rs` | External object-safety/local ownership, option/follow, and context-backed producer compile contracts | +| `docs/guide/proxying.md`, `streaming.md`, `capabilities.md`, `handlers.md`, `architecture.md`, `adapters/overview.md`, `adapters/axum.md`, `adapters/cloudflare.md`, `adapters/spin.md`, `adapters/fastly.md` | All paths use the `docs/guide/` prefix. Update affected contracts/examples; Fastly's current backend section at line 194 also needs the reviewed observation/getter/counting/frozen-handle contract | +| `scripts/check_outbound_docs_contract.mjs`, `check_outbound_legacy_api.sh`, `CHANGELOG.md` | Replace superseded active-signature assertions with equally strong reviewed contracts; retain retired-API guards and record the hard cut | + +The historical `docs/superpowers/specs/2026-05-21-outbound-http-design.md` describes the implemented +baseline. It must not be rewritten to make a failed cutover pass. Explain supersession in active +guides and CHANGELOG as directed by Q1; the new requirements and approved Interfaces govern the cut. +Demo, generated projects and the excluded fixture workspace must build in the same series. +Source locations above are submission-time review anchors, not stable line-number APIs. Axum's +`src/service.rs:73/282` owns shared transport setup and `:607/614` its injection assertions. +Active guide consumers include `docs/guide/proxying.md:120/144/158` and +`docs/guide/adapters/overview.md:69`; active-source signature guards include +`scripts/check_outbound_docs_contract.mjs:744/1735`. Its `:1216` producer-signature check reads +the historical spec, not active source; review those assertions independently when implementing +the cut. + +```sh +rg -n 'impl OutboundHttpClient|OutboundHttpClient|start_batch_until' crates examples tests +rg -n 'OutboundBatch(Item|Results|Failure)|finish_batch_item|OutboundBatch::(cutoff|from_driver|from_futures)' crates examples tests +rg -n 'with_clock|try_with_clock|with_transport_and_clock|OutboundResponse::new' crates examples tests +rg -n 'failure\.error|slot\.elapsed|handlers\.contains' crates/edgezero-cli/src/generator.rs +``` + +## Evidence Inputs and Outstanding Decisions + +The [feasibility report](2026-10-18-outbound-feasibility.md) separates local results, provider +unknowns and cancelled/deferred work. These hashes identify submitted inputs, not passing gates: + +| Input | SHA-256 | Disposition | +| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------- | +| [G1 draft inventory](../../tests/fixtures/reusable-app/outbound-cutover-cases.json) | `232798a4a7e12a727e937d4ef5064c2c46c7420ded02cbbe6ce3534259f59819` | 255 acceptance criteria, 511 planned legs; scope/owner approval pending | +| [G2 readiness evidence](evidence/2026-10-19-fastly-readiness.json) | `bf0911474b9f2dd50a5c0b1d1927edb1eafc865d7ff816e39bc686c379a88ac3` | Failed error preservation; hosted PendingExternal | +| [Initial G3 evidence](evidence/2026-10-19-outbound-progress.json) | `46effcfd323756d861efef16f46d4652741692db44df595dfcdc5cf8e93a9a9c` | Historical Spin failure, Cloudflare unavailable | +| [Repaired Spin/G3 evidence](evidence/2026-10-19-spin-response-egress.json) | `59006383b01abbde4ad5a97a04841ccfe2cc4cb4627d14b5e13ad80febcc20f6` | Native/Viceroy/Spin exercised cases pass; smoke exit 2, not qualification | +| [G2/G4 policy/readiness proposal](2026-10-25-fastly-backend-policy-proposal.md) | `011b090e21c7e8e6424b53303c55b1b2eacb7c896e51a81b6492c13fa65ce3e1` | B31 selected for review; resource/primitive/read-cap approvals pending | + +G3 does not yet prove Cloudflare execution, every protocol, missing active scheduler detection, +callback/export-return lifetime, all cancellation interleavings or the future public begin API. +Fastly's empty-response progress controls do not repair G2's body-readiness error loss. Spin's +header-derived zero-length hints are not raw framing evidence; no known-empty fast path is approved. + +### Independent Decision Requirements + +Apply the [scoped amendment](#scoped-timing-and-native-integration-amendment) to the original +task effects below: H(K) and timing-only G2 proof no longer block the Send-first checkpoint. +G4-FASTLY-POLICY still gates the full backend/readiness engine and resource qualification; the +separate scoped B31 approval authorizes the existing aggregate backend implementation only. +G4-OPTIMIZER-HORIZON remains pending for numerical horizon approval and qualification, not for +the permitted Send-first integration. + +| Decision | Required Attributed Review | Task Effect | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| G4-API | EdgeZero maintainer reviews the entire submitted Interfaces digest, migration/readback/recovery rules, defaults, causes, timer pairing and external producer contract | Required before P1; does not approve Fastly feasibility, runtime promotion or production adoption | +| G4-FASTLY-POLICY | EdgeZero and TS maintainers review a new preserving primitive/report plus literal positive B, fixed origin/authority/TLS/connect-settings scope, complete budget domain, rounding/cardinality proof and boundary evidence | D1 needs passing local G2 plus this approval; the currently tested primitive remains failed, not waived | +| G4-OPTIMIZER-HORIZON | Both maintainers separately approve original t0/D/source, actual backend-policy digest, finite numerical H(K) rule, literal finite maximum/domain and proposed 5 ms observation tolerance, accounting for every phase/retry/provider step | Blocks optimizer integration until approved and production qualification until hosted proof; never widens ordinary body/head deadlines | +| G4-APPLICABILITY | EdgeZero maintainer approves exact part/target/leg adoption decisions; changes to TS's blocking/applicability set also require TS approval. Record basis, implementation/execution/CI owners and evidence; confirmed-present parts need CHANGELOG and TS confirmation | Locks G1 scope; EZ-24 remains Follow-up unless explicitly promoted; missing Image Optimizer entitlement does not demote blocking EZ-8 | +| G4-MEASUREMENTS | Before replay, confirm M1/M2/M5/M7 samples, warm-up, settings, thresholds, zero-baseline and negative/inconclusive-control rules against identified builds | Enables those comparison runs, not an automatic passing result; M3's count invariant remains pass/fail, its timing distribution report-only; M4 reuse stays deferred | + +The submitted product thresholds are unchanged from EZ-2/EZ-3 and still require confirmation +before replay. Sample counts, runner/build identities and zero-baseline/control rules are not +approved by listing these proposed values: + +| Measurement | Submitted Bound | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| M1 | After five-minute warm-up, new handshakes per request to the origin and provider are at most 10 percent above baseline | +| M2 | Each bidder's p95 slot and response-head latency is at most 10 ms above baseline | +| M3 | Registrations per inbound request do not exceed its distinct origin configurations; duration distribution is report-only | +| M5 | Gateway-failure share is at most 10 percent above baseline share or 0.1 percentage points above it, whichever is larger; apply the connection-limit subcategory if EZ-16 is present | +| M7 | Zero pre-send internal-error slots/sends, reported per 1000 auctions by bidder count; per-slot 504 rate at most one percentage point above baseline, both for a 12-bidder profile | + +M4 reuse-mode capacity characterization remains deferred. Fixed deadline acceptance windows and +required negative/inconclusive-control handling do not become optional product thresholds. + +Applicability decisions must name EZ-5.9 and EZ-5.10 separately; EZ-24 host/WASI promotion, +EZ-24.11 abandonment, EZ-24.12 demo/template replacement and the JavaScript target separately; +and EZ-25.42/.43 separately. Preserve all other individually scoped pending decisions in the G1 +inventory. Approving one sibling does not adopt a range. Scoped approval of portable work does +not cancel the blocking Fastly requirements or permit an incomplete adapter migration. + +For each completed decision, record the exact requirement/target/leg scope; approved subject +digest; actual approver(s)/date; implementation, execution and CI owners; source/SDK/runtime/settings +identities and evidence hashes; unresolved conditions; and explicitly blocked/unblocked tasks. +Keep unknown B, H(K), owner assignments and pending approvals unset rather than manufacturing values. + +## Review Exit + +- [x] AG approves the exact full Interfaces digest for API implementation only, on 2026-10-19. +- [ ] G1's required applicability/owner decisions and G3's remaining prerequisites are resolved + for the work selected to proceed; portable progress is not mistaken for production readiness. +- [ ] Fastly primitive/B and optimizer horizon decisions have their own attributed dispositions; + a negative or unavailable result remains a visible block for dependent work. + +No checkbox is satisfied merely by creating this packet, passing a unit test or receiving an +unscoped instruction to continue. The scoped exact-digest reply and supplied maintainer name close +the API approval entry only; the remaining decisions still need their own approvals. + +**Downstream P1 obligation, not a G4 review exit:** after approval, P1 rereads the decisions, +verifies their digests and hard-migrates every affected implementation and caller with nonzero +red/green sentinels and the index's test/fmt/clippy card. Closing API review does not complete P1. + +## P1 Foundation Checkpoint + +The scoped reply starts the approved clock/timer foundation only: `OutboundFuture` and +`OutboundTime` are declared in core outbound, re-exported at the crate root, and implemented in +private `outbound/exchange.rs`. Named generic syntax for the factory uses the same approved +`Fn(Deadline, MonotonicClock) -> OutboundFuture<()> + Send + Sync + 'static` bounds. +No new executor, default/inert timer, adapter scheduling fallback or transport was added. + +Eleven co-located deterministic tests cover the foundation. Initial compilation failed on the +missing public types/exports; a later ready-error-at-expiry regression failed before its fix. +They cover paired-clock wakeups and waker replacement, retaining pending registrations, +early/frozen-ready rearming with the original deadline, typed original errors, equality/expiry, +clock regression, clone/local-future contracts, and exact release on cancellation and terminal +polls. A terminal wait releases captured factory state even if its completed boxed future remains +allocated. Ready factories under frozen time yield once per poll; real pending wakeups remain the +provider factory's obligation, not a core scheduling guarantee. + +A retained timer is polled once even when a later outer poll arrives at/past expiry, preserving +its original ready error instead of discarding it. A still-pending timer is cancelled when the +paired clock expires; no new timer is constructed for an already-expired wait. Error-path probes +remain fields of the timer after `Ready`, proving actual timer destruction rather than an +explicit test-local cleanup. Fake time is seeded ahead of process time so it is representable +on the JavaScript target as well as native/WASI. + +This does not establish settlement's timer-before-body ordering or 408/504 precedence, eager +public begin/batches, nor any adapter/runtime qualification. Existing client traits/constructors, +request options, demo and generator still use the current implemented contract. They must migrate +together in the later atomic dispatch cut; no compatibility alias was introduced to defer it. +Historical G2/G3 source/artifact records have not been relabeled as evidence for this later work. + +That constructor status describes the initial foundation only. The subsequently approved +2026-10-26 paired-time sequencing checkpoint hard-migrates constructors and live timer races; +the dispatch trait and head/batch protocol remain reserved for their later atomic cut. + +Verification for this foundation passed all five workspace Rust CI commands, 761 core unit +tests (including the eleven time tests), the nonzero timer sentinel, core compilation for +`wasm32-unknown-unknown` and `wasm32-wasip1`, and the Spin `wasm32-wasip2` check. The full workspace +suite had no failures and one existing ignored generated-project test. Strict clippy, decoder/map +dependency audits, outbound docs/legacy guards, the 18 evidence-checker tests and docs format, +lint and build passed. These are local checks, not hosted qualification or acceptance of G1's +511 still-planned legs. + +## Scoped Live-Validation Helper Approval (2026-10-25) + +The requester approved the exact checkpoint question in this conversation with +**"Approve hard-cut helper change"**: change the existing public helper to +`validate_for_dispatch(&request, OutboundSendMode)` and migrate every caller without a +compatibility wrapper. Consolidate live Send/Batch validation and preserve existing outbound +requirements. This is a separate helper-signature approval, not an interpretation of the +original exact Interfaces digest as covering a signature it did not declare. + +The frozen head-first Interfaces and their identities remain unchanged. This approval does not +authorize alternative implementer traits, new transport paths, a Fastly backend policy/bound, +runtime or SDK repins, weaker timer/slack/read-safety requirements, deployment or qualification. + +## Scoped Fastly Current-Operation Multiplexer Approval (2026-10-25) + +The requester approved the exact checkpoint question in this conversation with +**"Approve scoped implementation"**: one cooperative Fastly multiplexer with fresh, +error-preserving readiness, at most 16 KiB per raw read, and rotating slot service, preserving +B31, original logical deadlines, observation cutoffs and limits. Local/CI validation explicitly +uses the tracked Viceroy repair; deployment, hosted runtime promotion and resource qualification +remain separately gated. No dependency or installed runtime pin changes. + +This implementation slice shares one private engine across the existing Send and Batch receive +paths. It replaces blocking selected-body drains, not the frozen public implementer protocol. +SDK-owned cached Send heads remain opaque and use documented nonblocking polling with one shared +positive 1 ms pulse; raw batch heads and response bodies use borrowed-handle timed selection. +Native Image Optimizer remains the direct synchronous SDK operation. Registration, upload writes, +blocked inbound source pulls and SDK cleanup retain their disclosed limitations. + +Callback-wide scheduling/ownership, unpolled batch progress during unrelated operations, eager +Begin, the atomic four-adapter public API migration, dispatch-slack retirement, and hosted +qualification are not completed or waived by this scoped approval. The full Interfaces digest, +requirements digest, historical policy proposal and negative evidence remain unchanged. + +## Functional CI Validation Decision (2026-10-26) + +After the recommendation to gate functional client cases separately from the strict SDK timing +diagnostic, the requester directed **"continue"**. Implement that CI split only: the explicit +`outbound-client-progress` harness suite reuses the actual client cases, artifact, origin bounds +and validators. CI requires exactly exit 2, the complete passing twelve-probe/eighteen-request +inventory, no failures and an explicit unverified disposition. It is not a qualification pass. + +Keep `outbound-readiness` and its full SDK matrix, strict timing assertions, exit semantics and +historical failures intact. Excluding those broader diagnostic cases from ordinary CI is a +coverage choice, not proof that every observed failure is environmental or permission to waive +candidate-specific qualification. The original +5 ms target, logical deadlines, typed-error/read +safety requirements, public protocol and frozen requirements/Interfaces remain unchanged. diff --git a/docs/audits/2026-10-18-outbound-feasibility.md b/docs/audits/2026-10-18-outbound-feasibility.md new file mode 100644 index 000000000..d41708c4f --- /dev/null +++ b/docs/audits/2026-10-18-outbound-feasibility.md @@ -0,0 +1,655 @@ +# Outbound Feasibility Gate + +## Status + +### Dispatch-Slack Retirement (2026-10-27) + +The requester authorized removing the fixed 25 ms dispatch-slack guard and its test injection +helper. This supersedes older slack-pending and slack-unchanged statements below. Original +absolute deadlines, timeout causes, late-result rejection, B31 backend/timer policy and resource +limits remain unchanged. Synchronous SDK host calls remain non-preemptible; this does not establish +a finite blocking horizon, promote capabilities or qualify hosted behavior. Historical hashes, +measurements and failed probes remain intact. EZ-3 legs remain planned until their real commands +and sentinels are bound and verified. + +### Current Scoped Disposition (2026-10-24) + +The requester's [scoped timing and native integration approval](2026-10-18-outbound-api-approval.md#scoped-timing-and-native-integration-amendment) +supersedes this index's older blanket implementation stops for timing-only G2 failures and missing +finite H(K). The [D1d Send-first checkpoint](https://github.com/IABTechLab/edgezero/blob/docs/outbound-http-spec/docs/superpowers/plans/2026-10-18-trusted-server-outbound-03-drivers.md#send-first-checkpoint-2026-10-24) +may use the current trait and existing `execute`/`process_response`, with the ordinary SDK send +branch unchanged. Original deadlines, sources, late-result rejection and +5 ms measured targets/ +tests remain. The synchronous optimizer blocking horizon is explicitly **UNKNOWN** until provider +evidence; no finite horizon or optimizer/performance qualification is established by these probes. + +G2's retained failures remain failures. Typed body-error preservation and fresh-readiness/read +safety are still required; the stock runtime's lost errors are not waived. The later scoped B31 +approval below changes only the existing aggregate backend identity/timer policy; readiness, +resource approval and slack retain their separate gates. +Full EZ-1 settlement, Follow, engine and atomic head-first/full-trait migration remain open. +Hosted/performance qualification and deployment remain blocked. The experiment accounts, exact +measurements, commands, exits, source identities and linked evidence below are historical and +unchanged; this additive disposition does not reclassify them as passing results. + +### B31 Existing-Path Implementation (2026-10-25) + +The [scoped approval](2026-10-18-outbound-api-approval.md#scoped-b31-implementation-approval) +authorizes B31 timers and stable configuration-derived names on the existing aggregate-policy +Send/Batch path. Current production code replaces client namespaces/exact-budget identity and the +rigid phase split with a shared verified registry and the selected 31-member ladder. Configured +timeouts anchor the timer independently of shorter deadlines/cutoffs; otherwise the originally +selected duration anchors it. The original absolute logical budget is never rounded or restarted. + +Four behavioral controls failed against the old code and then pass through the real pinned SDK/ +Viceroy runner. Eleven scoped tests include the retained registration probe, full boundary/cardinality +coverage, clamped timeout/cutoff controls, actual cross-client preparation, untouched request parts, +and typed registry collision/retry/late-success behavior. Actual SDK readbacks also preserve +origin host/port, authority and TLS for DNS, IPv4 and IPv6 targets. The complete 202-test Fastly WASI unit +suite passes. The live shared helper serializes lookup/registration/insertion and checks expiry +after lock acquisition; successful late registrations are trusted before returning their timeout. +No unknown `NameInUse` is converted into a trusted backend. + +This is local working-tree implementation verification on base +`c70e2b05e1049f854c24ad75ee2333dbd4f10804`, not a frozen-candidate qualification. The registry +stores only configuration metadata and SDK backend names. It does not establish a total origin/ +registration/memory ceiling, provider cancellation or hosted pooling. Long configured timeouts can +leave a large provider timer behind a much earlier logical deadline. Original method-entry capture, +dispatch guards, selection/body-read behavior, runtime pins and capability classifications remain +unchanged. G2's failed readiness evidence and the broader G4/hosted decisions remain open. + +### Mixed-Phase And Full-Buffer Diagnostic (2026-10-25) + +The [new local record](evidence/2026-10-25-fastly-mixed-readiness.json) appends eighteen probes to +the existing suite, separately validated from the unchanged 135-probe baseline. Two-slot cases +complete a ready 32769-byte body while another head remains silent, and a delayed head while +another body remains pending. A third case closes after 32769 of 32770 declared bytes. Both +consumers run three repetitions at capacity 16384; every new observation includes a full-capacity +read. Mixed selection domains, per-read readiness, exact payload/checksum evidence, source errors, +head order and the origin's 50 ms delay are checked rather than inferred from case names. + +Each run executes 153 body probes, 177 bounded loopback origin requests and 72 timer controls. +The [retained canonical observations](https://github.com/IABTechLab/edgezero/blob/docs/outbound-http-spec/docs/audits/evidence/2026-10-25-fastly-mixed-readiness-probes.jsonl) +contain all three runs, including the first repaired failure. Replay tests verify both inventories +and retain the exact failure counts; mutations reject head-only/body-only selection, missing +readiness for later payload/error reads, consistently collapsed EOF, absent full-buffer evidence +and missing/reordered origin writes. + +| Run | Exit | Baseline / Supplement Failures | Source Errors Lost | Maximum Completion | +| -------------------------- | ---- | ------------------------------ | ------------------ | ------------------ | +| Repaired before formatting | 1 | 0 / 2, timing | 0 | 518.771 ms | +| Stock, formatted guest | 1 | 25 / 7, errors and timing | 30 | 593.907 ms | +| Repaired, formatted guest | 2 | 0 / 0 | 0 | 504.369 ms | + +The formatted pair uses identical guest SHA-256 +`45f4f541a2338bd923ad33afcf4677a0619b5889778f602f8817cc7d2f97f392`; +the earlier guest has its own identity, not a claimed identical-byte comparison. Stock loses the +six new truncation errors as well as the 24 existing ones. Repaired preserves all of them, and +the ready sibling completes before the unresolved sibling expires. The final local pass exits +**2, unverified**. The earlier overrun is not overwritten or excused by that pass; +5 ms remains +the measured target, not a hard scheduler guarantee. + +These experiments use the explicit isolated repair, not a changed installed runtime or production +transport. Registration remains outside the observed interval with fixed two-second phase timers; +this is not B31 timer enforcement. Full-sized candidate reads and two-slot mixed progress do not +prove arbitrary continuously-ready fairness, approve a production read maximum, establish resource +or hosted teardown bounds, or clear G2/G4 and the atomic migration. No hosted deployment occurs. + +### Backend Policy Review Packet (2026-10-25) + +Historical proposal checkpoint. Its separate scoped implementation authorization is recorded above; +the immutable packet is not rewritten as production or qualification evidence. + +The [new G2/G4 packet](2026-10-25-fastly-backend-policy-proposal.md) supplies a numerical candidate: +B31 with upward-rounded coupled timers, compared with a B1 fixed-ceiling alternative. Its model +passes 93 boundary samples and 16 timeout-anchor controls over the full seven-day clamped domain. +This is arithmetic-only proposal evidence. The requester selected B31 for final review on +2026-10-25; resource/primitive approval and implementation remain pending. The older +"not proposed" descriptions below are historical. SDK representability is not provider acceptance. + +The packet also records the exact repaired readiness sequence, tested capacities versus unproved +full-sized-copy/fairness domains, stock error loss, retained timing failures and unavailable runtime +getters. It does not approve the preserving primitive, a read cap, idle mapping or a runtime change. +G2 remains failed and its dependent work remains gated; no new host run or capability claim is made. + +### B31 SDK Registration Checkpoint (2026-10-25) + +The existing Fastly WASI unit suite now includes `proposed_b31_timer_vectors_register`. It passes +all 31 proposal vectors through the pinned SDK builder and unmodified Viceroy registration ABI, +once with Connect coupled to the phase value and once with Connect fixed at 200 ms: 62 successful +registrations across two fixed-setting scopes. Each registered name is checked for existence; +no request is sent and no timing getter is called. The test uses no production backend-policy +helper and does not implement quantization, identity, registration reuse or the proposed policy. + +The same test-local builder rejects `u32::MAX + 1` milliseconds independently for Connect, +first-byte and between-bytes with the exact SDK error variant and original duration. Removing +each setter separately makes its corresponding assertion fail under Viceroy (exit 134); all +three setters are restored and the guarded test passes again. Before adding the test, the +nonzero guard rejected the empty selection. The existing Fastly WASI unit-suite CI step runs +the new test without another CI command, export or runner. + +Run from the worktree root: + +```sh +SSL_CERT_FILE=/etc/ssl/cert.pem CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh proposed_b31_timer_vectors_register cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --lib proposed_b31_timer_vectors_register +``` + +This is working-tree characterization on base `0166998f8bfb7759e5d661e629d1849d9483e3a2`, not +frozen-candidate qualification. The successful run used Rust 1.98.1 (`48a229cea`), SDK/sys 0.13.1, +and stock Viceroy 0.21.0. Initial execution without `SSL_CERT_FILE` failed during local certificate +initialization, not registration; the explicit system bundle above fixes that runner setup. + +| Input | Successful-Run SHA-256 | +| ---------------------------------------------- | ------------------------------------------------------------------ | +| Fastly outbound source, including the new test | `ad1097270a6c4c319e3d51f82ff73f9c9a700ed8a7908e1b69083e5a7b1e5dcc` | +| WASI unit-test guest | `a3f52b51f101b3c1aed1986d1c084a4dbcd7407449a89307a11aa84d775e7c5c` | +| Stock Viceroy executable | `751ff2c1a83063b109bc212b95be5f52e5ab6284b9027fa03aa229087eba3f30` | + +These identities describe the local run, not retained release binaries. The result establishes +SDK serialization/local registration acceptance for the supplied vectors only. Viceroy does not +enforce Connect and its timing getters remain unavailable. Stored-value readback, hosted acceptance, +timer enforcement, pooling, resource accounting/retirement, cancellation and G2 readiness are not +proved. The proposal's arithmetic, approval requirements and failed readiness evidence remain +unchanged; no production backend/timer/slack policy, runtime pin, capability or acceptance leg changes. + +### Historical Execution Status + +Executed locally on 2026-10-19. **G2: Failed.** The pinned Viceroy body-readiness +primitive discards a source error, confirmed by three independently dispatched paired controls. +Hosted Fastly remains **PendingExternal**. No capability is promoted. G2 itself did not change a +production transport; the later G3 investigation fixed an internal Spin response-egress defect +described below. This is characterization, not an EZ-Q1 passing qualification record. + +The base checkout was `7cccaf10734914161803317aec6c55c22c2bccc9`; the probes were uncommitted +working-tree changes. They do not qualify that base commit. The +[retained evidence](evidence/2026-10-19-fastly-readiness.json) contains source-file SHA-256 hashes, +the compiled binary hash, the original JSONL event hash and all event objects. Reconstruct the +JSONL as `events.map(JSON.stringify).join("\n") + "\n"` to check the event hash. The binary is +identified, not retained as a release asset. Future positive qualification must run a clean, +committed candidate and keep its own durable build and runtime evidence. + +## Pinned Boundary + +| Component | Identity | +| ------------------ | ------------------------------------------------------------------------ | +| Compiler | Rust 1.98.1, `48a229cea`, 2026-09-01 | +| Target | `wasm32-wasip1` | +| SDK / sys / shared | Fastly 0.13.1 | +| Runtime | Standalone Viceroy 0.21.0, not the Fastly CLI's bundled 0.21.1 | +| Origin | Local HTTP/1.1 TCP fixture, no credentials or external service | +| Source identities | Lockfile, fixture sources and Viceroy source hashes in retained evidence | + +The experiment calls the pinned `fastly_sys::fastly_async_io::select(handles, len, +timeout_ms, selected_out)` ABI through a fixture-local scoped unsafe wrapper. Zero would wait +indefinitely, so every call passes at least 1 ms. The wrapper borrows live handles; it neither +closes nor transfers them. This is not a new public EdgeZero interface or production client. + +Response-head readiness is followed by `PendingRequestHandle::wait`. The normal-body path calls +timed readiness before **each** `BodyHandle::read` with a one-byte output buffer. A generic ready +signal is not by itself proof that this read cannot block: Viceroy skips empty data chunks, and +the guest cannot inspect the next retained chunk's length. Head readiness retains its completed +`Result` in `PeekableTask`; body readiness has different error semantics. + +In Viceroy 0.21.0, `src/body.rs`'s `Body::await_ready` matches only +`Some(Ok(chunk))` from `self.data().await`. It puts that successful chunk back, but consumes +`Some(Err(_))` without retaining it. `src/sandbox/async_item.rs` delegates normal bodies to this +method. `src/async_io.rs` then reports readiness. A later body read cannot recover the discarded +error through this primitive. The exact inspected source files are hashed with the evidence. + +## Executed Control + +```sh +scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-readiness +``` + +Expected diagnostic exit: **1**, because the source-error preservation prerequisite fails. +The suite is opt-in and does not turn the normal unit or smoke gates red. + +Each origin response declares three bytes, sends one byte, then closes after 50 ms. The direct +control performs one-byte reads without body readiness. The selected control reacquires body +readiness before every one-byte read. Both use a 500 ms observation budget and a timed response-head +wait. The direct read is deliberately blocking against this finite controlled source; it is not +an implementation proposal or a production fallback. + +| Repetition | Direct Control | Readiness Control | Preservation | +| ---------- | ------------------------------ | -------------------------------------------- | ------------ | +| 0 | One byte, then `UnexpectedEof` | One byte, successful readiness, then `Ok(0)` | Failed | +| 1 | One byte, then `UnexpectedEof` | One byte, successful readiness, then `Ok(0)` | Failed | +| 2 | One byte, then `UnexpectedEof` | One byte, successful readiness, then `Ok(0)` | Failed | + +Six real origin receipts were required. Each read and readiness operation has guest-relative +start/end microseconds in the raw record. All completed before the 500 ms budget, well before +the experiment's two-second backend timers. An incomplete selected trace or a select timeout is +rejected separately; neither is counted as clean EOF or evidence of error consumption. + +The harness guard `outbound_readiness_rejects_empty_or_unbounded_measurements` rejects absent +identity, zero/missing samples, zero/unbounded readiness waits, late operations, missing terminal +reads and selected reads without immediately preceding successful readiness. It is a validator +unit test, not proof of hosted behavior. The parent uses a finite socket timeout and process-group +teardown to contain a stuck diagnostic. That does not prove prompt guest cancellation, sibling +progress or a 500 ms hostcall interruption guarantee. + +## Unproved Work + +| Leg | Disposition | +| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Truncated body typed-error preservation, local | **Failed**, six measured controls | +| Silent head, permanently pending body, 300 ms trickle | Pending; full deadline +5 ms proof not run | +| Sibling readiness and ready-then-exhausted body | Pending; no sibling-progress claim | +| Malformed framing and other transport failures | Pending; truncated body alone does not cover them | +| Buffered and streamed production paths | Pending; this probe tests the host primitive only | +| Hosted Fastly and Image Optimizer entitlement | **PendingExternal**; no deployment attempted | +| Literal backend cardinality bound B | Not proposed or approved; primitive gate failed | +| Numerical synchronous-call horizon H(K) | Unknown; no finite rule or maximum proved or approved | +| G4 public API approval | [API-only approval by AG](2026-10-18-outbound-api-approval.md) recorded later; Fastly policy decisions remain open | + +Milliseconds are the ABI wait unit; the experiment floors positive remaining milliseconds and +uses at least one. This is a diagnostic observation policy, not an approved production rounding +rule, minimum budget or backend discriminator. A successful head/read timing on this finite +origin cannot establish the remaining deadline domain, B, H(K) or a provider processing horizon. + +## Task Disposition + +- **Cancelled pending a new reviewed primitive/report:** D1a's proposed Fastly multiplexer and + D1b's dependent stable-backend/slack removal; D1c and D1d cannot proceed through those prerequisites. +- **Deferred:** dependent Fastly D5/D6 qualification and any hosted capability promotion. Required + acceptance legs remain required and visibly unpassed, not deleted from the inventory. +- **May proceed independently after their approvals:** portable P1-P5 and Axum/Cloudflare/Spin + D2-D4. This failed local primitive does not prohibit that work or approve it automatically. +- **G3:** local characterization and the subsequent Spin repair are recorded below. The initial + Spin failure is fixed; the later Cloudflare checkpoint now passes the exercised local cases. + The incomplete gate does not approve a new scheduling primitive or complete G4. + +Do not repin an SDK/runtime, add a blocking-read fallback, restore an old client, widen the +cutoff/tolerance, or waive the failed gate as BestEffort. The next Fastly step is maintainer review +of a provider/runtime primitive that preserves body errors, followed by a new exact-version +report and the remaining local and hosted probes. Production adoption remains blocked meanwhile. + +## Local Runtime Repair (2026-10-23) + +PR #275 now carries an [exact-version Viceroy source patch and build recipe](https://github.com/IABTechLab/edgezero/blob/docs/outbound-http-spec/patches/viceroy/README.md). +It retains original body errors in the chunk queue, skips empty frames before reporting readiness, +and yields when decompression produces no bytes. It does not add an SDK-side blocking fallback, +change dependency/runtime version pins or replace the installed runtime. A repaired executable +must be selected explicitly with `VICEROY_BIN` and identified by its source/patch/binary hashes. + +The [separate repair record](evidence/2026-10-23-viceroy-readiness-repair.json) preserves a fresh +stock failure and repaired success using the same guest binary at `ff60e5ac9d6d673648ca5e06b1bdddfaab68083a`. +All three stock pairs lose `UnexpectedEof`; all three repaired pairs preserve it. The repaired +suite intentionally exits 2, not a G2 pass. Ten new explicit-poll regressions went red (nine fail) +before the repair and green afterward; all 135 runtime library tests pass. Strict upstream clippy +has the same diagnostics on stock and repaired source under Rust 1.98.1; those are not suppressed. + +This fixes the measured error-loss/empty-frame defects in the locally repaired runtime only. +The unmodified pinned runtime remains a failed control. The remaining G2 stalled/trickle/sibling/ +framing/buffered/streamed ABI cases, exact-version review, B/H(K) approvals and hosted evidence are +still open. The earlier failed evidence is unchanged; this report does not approve the atomic +production migration or promote any capability. + +## G2 Expanded Primitive Matrix (2026-10-23) + +The [separate matrix record](evidence/2026-10-23-fastly-readiness-matrix.json) extends the +diagnostic, not the production client. Eleven cases cover silent heads, pending bodies, trickle, +two siblings with only the second body ready, ready-then-exhausted, ordinary/empty EOF, truncated +bodies, malformed heads/chunks and pre-head connection closure. Each selected case runs buffered +and pull-streamed, with capacities 1/16384 and three repetitions. Three finite direct truncation +controls bring each full run to 135 probes and 147 correlated origin requests. + +The guest borrows live handle identities for timed ABI select, then consumes a ready pending head +through nonblocking `PendingRequestHandle::poll`, not `wait`. Body reads consume one fresh +same-slot readiness result each. Positive millisecond waits are recomputed from one 500 ms absolute +deadline, floored with a one-millisecond minimum; early timer wakes rearm against that same instant. +Expiry wins over late readiness/read outcomes. Stream ownership transfers before any body read, +without resetting the clock; its consumer discards chunks instead of retaining the buffered body. +The two capacities are diagnostic variations, not an approved production maximum. Backend +registration is outside this window and phase timers are fixed at two seconds, so this is not a +backend-policy or whole-entry synchronous-call bound. + +The validator requires complete unique identities, origin receipts/cleanup, monotonic raw samples, +fresh selection (including a final matching wait for each timeout), actual read/EOF/error outcomes, +matching terminal summaries and all completion +times within 505 ms. FNV is only a tiny known-payload checksum, not a provenance hash. Source, +guest, runtime and retained trace identities use SHA-256. Completed samples are also logged +incrementally, including failed ABI outcomes, so a failed guest does not erase its preceding trace. + +The first complete repaired run failed: pending-body/streamed/1/repetition-0 completed at +507.794 ms (the timed select itself returned at 507.153 ms). Its source errors were preserved. +The same guest bytes on stock Viceroy lost truncated/malformed-body errors and had late samples. +After the trace-retention/deadline-check repairs, a fresh identical-guest stock/repaired pair +still failed: stock lost 24 selected source errors; repaired preserved them but completed +siblings/selected/streamed/1/repetition-1 at 507.233 ms. All four runs (540 probes) are retained +and replayed by the validator's regression test; the earlier failures are not replaced by later runs. +The lateness is observed at the host-call boundary; these traces do not isolate an OS/runtime +scheduling cause or establish a deterministic interruption bound. The +5 ms limit is unchanged. + +Each origin owns at most two workers, a 4096-byte request-head cap and a three-second watchdog; +finish/drop closes sockets, wakes timers and joins workers. Harness-forced shutdown is labelled, +not SDK cancellation. Guest handle release is not proof of hosted transport teardown. All results +remain working-tree characterization. Full G2 is **Failed**, hosted remains **PendingExternal**, +and B/H(K), exact-version review, production migration and candidate freeze remain separate gates. + +## G2 Timer-Control Investigation (2026-10-23) + +The [timer-control record](evidence/2026-10-23-fastly-timer-controls.json) retains a separately +identified stock/repaired comparison with identical new guest bytes. Eight repetitions at +1/100/500 ms compare native thread sleep, guest WASI sleep and empty-handle timed ABI select: +72 controls per runtime, plus three excluded warm-ups. The existing 135-probe / 147-origin-request +matrix follows unchanged. Raw ABI entry/return timestamps are captured before status formatting; +outer operation and exchange timing still include guest overhead and enforce the same limit. +Earlier failed traces included status formatting inside the observed selection interval; +they lack raw ABI timestamps, so this instrumentation cannot retroactively isolate that overhead. + +| Runtime / Phase | Native Sleep Late / Maximum Overrun | Empty Select Late / Maximum Overrun | WASI Sleep Late / Maximum Overrun | +| ----------------------- | ----------------------------------- | ----------------------------------- | --------------------------------- | +| Repaired, before review | 5/24; 5015 us | 0/24; 2229 us | 0/24; 2242 us | +| Stock, before review | 7/24; 10012 us | 0/24; 3533 us | 0/24; 3381 us | +| Repaired, reviewed | 0/24; 4557 us | 0/24; 2421 us | 0/24; 2138 us | +| Stock, reviewed | 0/24; 4751 us | 0/24; 2281 us | 1/24; 6952 us | + +Late means elapsed time exceeds the requested duration plus 5 ms. Controls retain false +`within_five_ms` flags rather than rejecting or relabelling those observations. They are not a +replacement G2 acceptance rule. Native controls are collected independently in each comparison, +not shared observations. The two guest controls share Viceroy's async execution/timer machinery; +native sleep does not exercise its reactor. Native lateness proves the window can be exceeded +without HTTP-body processing, not that it caused the earlier select failure. + +Review corrected the timer-only endpoint to captured primitive return, excluding status +formatting, and rejected raw ABI waits shorter than their positive timeout. The G2 outer +operation and exchange checks did not change. Both matched pairs are retained with distinct +guest hashes. The reviewed stock's late WASI control requested 100 ms and returned at 106.952 ms, +1.952 ms beyond the +5 ms window, with no HTTP body. This narrows the issue beyond body readiness +but does not establish the original cause. Earlier observations are not relabelled or overwritten. + +Pinned source inspection shows guest monotonic time delegates to a host monotonic clock; +Tokio uses host monotonic elapsed time too. No scale mismatch was found. Viceroy starts a +relative Tokio timeout after decoding guest handles. Tokio's millisecond rounding, readiness-first +poll order, driver service and task resumption expose possible extra latency, but rounding alone +does not explain the earlier 7.297 ms overrun. Source provides no deterministic +5 ms resumption +bound. WASI sleep is also asynchronous here, not an independent native-clock shortcut. Exact +dependency source identities and the limits of these inferences are retained with the record. + +The initial repaired body matrix met the window in all 135 observations (maximum 502.235 ms); +the reviewed repaired matrix also met it (maximum 502.553 ms). Each repaired run +exits **2, unverified**. Stock still loses 24 source errors and exits **1**. The earlier repaired +timing failures remain retained and replayed. These new observations do not establish a tail +bound, isolate the original scheduling cause, repair a runtime defect or qualify hosted behavior. +No runtime code, pin, deadline, margin, policy approval or capability changes. G2 remains **Failed**; +dependent Fastly engine work stays gated while approved portable work may proceed independently. + +## Local-Only Recheck (2026-10-24) + +The [new recheck record](evidence/2026-10-24-fastly-local-recheck.json) retains identical guest +bytes built from clean commit `98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8` on the explicitly +selected stock and repaired binaries, with unchanged runtime hashes/pins. Each run executes +135 body probes, 147 bounded loopback origin requests and 72 timer controls. No deployment or +external origin is used; this is local characterization, not a frozen qualification build. + +Both runs exit 1. Repaired retains all selected body errors but the +`trickle/selected/buffered/16384/1` probe completes at 505.158 ms. Its last select returns at +502.614 ms; the terminal cleanup window runs from 502.707 to 505.117 ms. That window includes +SDK-handle release and ends before JSON/checksum formatting. The anchors do not distinguish +release cost from descheduling or clock-sampling latency, so no specific cause or repair is +claimed. Mandatory cleanup and completion exceed the unchanged 505 ms bound; neither may be +backdated or excluded. Two native 100 ms sleep controls also exceed +5 ms. + +Stock loses the same 24 selected source errors, with a maximum body completion of 502.445 ms. +It records three late timer controls: two native sleeps and a 500 ms empty select returning at +506.343 ms. The separate retained JSONL contains all 270 body probes and 144 controls; a fixture +regression replays both failures and the repaired cleanup boundary without rewriting earlier +evidence. These observations do not qualify Image Optimizer or prove performance parity. +G2 remains Failed; numerical B/H(K), production integration and hosted qualification stay open. + +## G3 Local Characterization + +The initial default-off `progress` fixture feature uses direct SDK operations, not a second production +client. The standalone `outbound-progress` suite runs the probes; ordinary `smoke` also builds and +runs them in separate artifact directories after its existing lifecycle cases. The all-adapter +smoke command was executed with the explicit local tool paths in the +[retained G3 record](evidence/2026-10-19-outbound-progress.json). It exited **1**, preserving the Spin +failure and the unavailable Cloudflare leg. This is not a passing release gate. + +The record retains 41 G3 event objects, runtime logs, source-file and binary SHA-256 identities. +Reconstruct its retained event JSONL using `events.map(JSON.stringify).join("\n") + "\n"` to +verify `retained_events_sha256`. The full unrelated smoke history has a separate identity-only +hash. Binaries are identified, not retained release assets. Source hashes describe uncommitted +fixtures on the base revision above, not qualification of that base commit. The earlier G2 record +is historical and has not been rewritten to match these later source changes. + +| Adapter / Local Runtime | Progress Cases | HTTP/1 Framing Cases | Disposition | +| ------------------------------ | ----------------------- | -------------------- | ------------------------------------------------------- | +| Axum / Tokio native | 5/5, 18 origin receipts | 5/5 | Passed for these local observations only | +| Fastly 0.13.1 / Viceroy 0.21.0 | 5/5, 18 origin receipts | 5/5 | Passed for these local observations; G2 still Failed | +| Spin SDK 7.0.0 / Spin 4.1.0 | 4/5, 15 origin receipts | 5/5 | **Failed** response-owned execution | +| worker 0.8.5 / workerd | Not executed | Not executed | Unverified: locked wasm-bindgen 0.2.129 CLI unavailable | + +### Scheduling Method + +The five cases are direct-SDK eager single and batch starts, the existing lazy public single +send, the existing never-polled public batch, and SDK jobs retained by a response stream. The +consumer job wrapper counts polls before the window closes. A separate same-guest monotonic +sample must show an unrelated wait of at least 100 ms; origin-local timestamps independently +prove when each request arrived. Guest and origin clock epochs are never compared. Unique case, +token, adapter and build identities, exact receipt counts and duplicate rejection are required. + +Direct SDK starts reached the origin during the window on the three executed targets. The +existing single send was lazy on all three. Existing batches were lazy on native and Spin but +dispatched immediately on Fastly. Fastly's fixture captures pending handles synchronously and +sleeps the guest for the window: this establishes host-side request progress, not background +guest scheduling or an approved cooperative timed-body multiplexer. Its completion probe only +polls response heads from the controlled `Content-Length: 0` origin, avoiding the failed G2 path. + +The response-owned stream yields one whitespace byte before awaiting its jobs. The origin cannot +complete those jobs until the client observes that byte and releases a finite response gate. +This rejects fully buffered substitutes. Native and Fastly passed this initial observation. It proves +handler return followed by response consumption, not return of the complete host export/callback. +The existing native entrypoint installs its `LocalSet`; the negative unit control proves only +that an absent Tokio runtime is rejected before SDK dispatch. It does not establish LocalSet +presence/liveness detection. The subsequent repair added a focused native unpolled-job cleanup +regression without claiming provider cancellation guarantees. + +Spin's four handler-held cases passed before its response-owned case failed in repeated runs. +The retained run saw response-head EOF; other runs saw a 200 response interrupted after zero or +one byte. Runtime logs report `body writer dropped`, then outbound resets and `worker panicked`. +These initial observations did not prove provider impossibility. The isolated controls described +below subsequently identified an adapter bug independently of SDK `get` ownership. +No exception is caught or reclassified as a pass, and independent framing probes still run. + +### Empty-Body Observations + +Actual TCP ingress distinguishes absent framing, explicit zero length, a terminating chunk, +and nonempty fixed/chunked controls. All executed targets returned zero bytes for the first +three and four bytes for both controls. These are byte-count observations, not a payload-fidelity +test. No request body is cloned, consumed or +replaced when its host signal is sampled before normal adapter conversion. + +Fastly reported a body handle even for absent framing; its SDK known length was zero for absent +and explicit-zero requests and unknown for chunked requests. Native's existing entrypoint does +not expose the original host object to this fixture, so its raw signal is explicitly unavailable. +Spin's SDK `is_end_stream` was false even for explicit zero length; only its header-derived hint +reported zero. That is not independent framing evidence and does not justify a known-empty fast +path. Cloudflare's non-consuming raw-object snapshot compiles, but has not run. The production +absent-body fallback protecting browser-created requests is unchanged. + +### Spin Repair and Rerun + +Four additional controls compared whitespace-first ready/timer-only streams through the adapter +and direct SDK `BodyWriter`, with no outbound calls. Before the fix, both SDK controls passed while +both adapter controls failed. Phase tracing then observed `Ready(Ok(()))` on the host response-result +future during pending body-source or write work. The adapter classified every ready response result +as a transport failure and dropped the writers. A guest panic hook did not fire; the originating +host worker-panic message was not itself the proof of this logic defect. + +The adapter now retires successful observation exactly once and resumes the same pinned operation. +Host errors still preempt, deadlines remain active, and source/write/body-result failures remain +errors. Finish closes the byte stream and successfully publishes the body result before accepting +latched success, or awaits the remaining observer under the deadline. Completed observers are never +cancelled or repolled. The report remains `HostHandoff`, not confirmed client delivery. + +The regression `successful_early_host_handoff_does_not_abort_pending_body_work` failed against the +old classification, then passed. Separate tests cover the same pending operation resuming, later +deadline/operation failure, host failure precedence and timer priority. Additional fixture tests +prove unpolled native worker cleanup while its runtime stays alive and exactly-once release of +the two jobs held by an unconsumed response. SDK-specific final-await cancellation/error interleavings +still need broader qualification; generic helper tests are not a substitute for them. + +The [repair evidence](evidence/2026-10-19-spin-response-egress.json) retains source and artifact hashes, +49 selected current events, component stderr, runtime logs and intermediate before-fix control +observations. It is a working-tree characterization, not a clean-candidate qualification. The +earlier G2 and G3 evidence records remain unchanged. Its intermediate before-fix snapshot identifies +the compiled binary but does not claim a complete retained source snapshot. + +Spin now passes 5/5 progress cases, 5/5 HTTP/1 framing cases, four comparison controls and 18 origin +receipts. Exactly two successful token-correlated SDK terminals are required from canonical component +stderr, with failure checks repeated after the finite observation hold. No worker panic was observed +while serving; the message appeared only after controlled SIGTERM. This separates the observation +phases; it does not establish the cause or safety of host shutdown. The latest all-adapter smoke run +exits **2**, preserving Cloudflare's tool gap and unverified same-guest overlap observations. + +### Remaining G3 Work + +Cloudflare requires a matching, explicitly provided `WASM_BINDGEN_BIN` plus absolute executable +`ESBUILD_BIN` and `WASM_OPT_BIN` overrides to avoid worker-build's automatic installations. The +available wasm-bindgen executables are older than 0.2.129; no download or repin was attempted. +All three WASM probe targets compile and pass clippy, which does not supply missing runtime proof. + +HTTP/2 and HTTP/3 were not exercised. Hosted executions remain PendingExternal. Missing active +request-context detection on provider schedulers, host-export/callback completion, cancellation, +and integration through the future public begin API remain unproved. G3 stays open and G4 stays +incomplete; later scoped API approval by AG is recorded in +the G4 packet. These observations themselves neither approve an API, promote capabilities nor +waive a required acceptance leg. + +### Verification + +The five workspace Rust CI commands passed, including the full workspace tests. After the repair, +the fixture's 34 harness tests, 12 core tests and two native controls passed, as did the Spin +adapter's 63 feature-enabled tests and ten bounded-secret integration tests. +Fixture formatting and strict clippy passed for native and all three WASM probe targets. The +18 evidence-checker tests, inventory validation, outbound docs/legacy checks, decoder/map +dependency audits, and docs format/lint/build passed. Retained event and source hashes were +recomputed successfully. The initial all-adapter smoke failed; the repaired run is unverified as +recorded above. Green compilation/unit checks do not turn unavailable runtime legs into passes. + +## Original-Coding Local Checkpoint + +The [coding record](evidence/2026-10-19-outbound-coding.json) covers the separately implemented +Decode/Original request option and mandatory 206 preservation. Pinned Viceroy 0.21.0 and Spin 4.1 +each passed 24 controlled-origin checks: gzip/deflate/Brotli, buffered/streamed, Original/200, +Original/206, Decode/206 and Decode/200. The first three combinations use decoder budget zero +and decoded cap one; controls verify that the fixtures actually decode to the expected plain bytes. +The record retains case results, delivered-byte hashes, original coding/length observations, +source hashes, binary identities and raw event-file identities. Both runner commands exited zero. + +These are uncommitted working-tree characterizations, not qualification of a frozen candidate or +hosted runtime. Reports contain delivered bytes as JSON; they do not prove encoded downstream +relay framing. Fastly mixed-choice batch host execution, the full normalization/cap adapter matrix, +Cloudflare workerd/relay execution and hosted cases remain open. No new tool was downloaded, +dependency repinned, capability promoted or G2 failure waived. Optional HEAD/304 and representation +metadata changes remain unadopted. + +## Proxy-Marker Local Checkpoint + +The [marker record](evidence/2026-10-20-outbound-marker.json) retains 46 checks each through native +Axum, Fastly SDK 0.13.1 / Viceroy 0.21.0 and Spin SDK 7.0.0 / Spin 4.1.0. Each runner exited zero. +The matrix crosses inherited-off, explicit-off and explicit-on settings with absent, upstream +and Connection-nominated markers on GET/200, GET/204, HEAD/200, GET/304 and GET/302 responses, +plus one mixed-policy batch. The client inspector and returned fields are checked together. +The same runs passed 24 original/206/decode coding controls each. Source hashes, compiler/runtime +identities, binary hashes, case observations and raw event-file identities are retained. + +### Spin SDK Header Visibility + +The initial Spin run failed the nominated-marker check: with insertion disabled, the upstream +marker survived. A direct SDK control now snapshots `get_headers().copy_all()` before EdgeZero +conversion. Both ordinary and nominated upstream shapes expose the marker but no Connection field. +Two correlated controlled-origin requests establish this at the SDK boundary; EdgeZero cannot +recover the lost nomination. The failed observation is retained separately, without inventing an +initial source snapshot or asserting source equivalence with the rerun. + +Fifteen Spin nominated-shape rows therefore record `provider_hidden_nomination: true`. Their passes +establish policy on adapter-visible fields only, not removal of raw nominated fields. Off preserves +an ordinary upstream marker; it must not guess that the provider hid a nomination. Spin's +OutboundHeaderFidelity assignment is corrected from Native to BestEffort in code, public guides +and the baseline capability notes. The strong fidelity definition itself is unchanged. Axum and +Viceroy remove the nominated field before marker policy in all exercised cases. + +These are local working-tree characterizations, not hosted or frozen-candidate qualification. +Cloudflare runtime remains blocked on the matching wasm-bindgen 0.2.129 CLI; target compilation and +clippy do not replace that evidence. Head-first/eager/follow integration remains open. No tool was +downloaded, dependency repinned, capability promoted or Fastly G2 failure waived. Exact marker-off +header byte/count budgets, duplicate upstream fields, non-polling inspection and clone isolation +have separate deterministic core tests and Axum transport coverage; the 46 runtime cases do not +claim those resource-bound proofs for every host. + +## Cloudflare Local Runtime Checkpoint + +The [Cloudflare record](evidence/2026-10-20-cloudflare-outbound.json) supersedes the tool-unavailable +status above for this new local run only. Historical records remain unchanged. An explicitly +authorized temporary install supplied wasm-bindgen CLI 0.2.129, matching the locked crate; no +project dependency, SDK, compatibility setting or global tool was replaced. The first attempt +built successfully but the sandbox denied loopback sockets before any request ran. Authorized +loopback-only reruns used Wrangler 4.131.0 and its bundled workerd 2026-09-10 locally, without +deployment, remote bindings or metrics. + +The strengthened harness passed all **80 checks**: 24 coding, 46 marker, five scheduling and five +HTTP/1 framing cases. It asserted **89 origin receipts**: 24 coding, 47 marker and 18 scheduling. +The scheduling receipts are individually retained; coding/marker counts are enforced by the +runner but their individual receipts were not emitted. Coding includes 18 original/206 preservation +cases and six decode controls across gzip, deflate and Brotli, in both response modes. Marker +checks include bodyless dispositions, manual redirects and a mixed-policy batch. Unlike Spin's +retained SDK control, these exercised Cloudflare nominated fields were removed before marker policy. +This does not establish raw-header fidelity for other field shapes or protocols. + +Pre-conversion Workers `body_absent` was true for absent framing and explicit zero length, false +for chunked-empty and both nonempty controls. The latter preserved all four bytes. Missing, +null, mistyped or contradictory signals now fail the harness; regression tests preserve other +adapters' existing byte checks. All eight group-failure combinations prove coding/marker failures +cannot skip scheduling/framing and cannot turn the overall result into success. Phase-specific +errors are retained. Spin's terminal/panic checks and fatal evidence-write behavior are unchanged. + +The record separates the original unchanged-harness run from the strengthened run and retains +source/lockfile SHA-256s, exact tool/runtime identities, effective Wrangler configuration, served +JS/WASM fingerprints, raw events, summary and runtime log. Compatibility date remains +`2026-04-22`, with `enable_request_signal` and `enable_weak_ref`. The built WASM and JS artifacts +did not change when only harness validation was strengthened. + +These are **working-tree characterizations, not frozen-candidate or hosted qualification**. +SDK-native eager probes still contrast with existing lazy public APIs; they do not implement +`HttpClient::begin`, the head-first boundary, or the full adapter hard cut. Response-owned checks +prove handler/adapter dispatch return and gated client progress, not exported callback return or +confirmed delivery. Encoded downstream relay framing, cancellation/deadline qualification, +HTTP/2/3, scheduler-context rejection and hosted execution remain open. Cloudflare capability +assignments are unchanged. Fastly G2 remains failed; no B, H(K), slack removal or readiness waiver +is approved. The frozen API-only approval digest is unchanged. + +## Actual Downstream Relay Checkpoint + +The [relay record](evidence/2026-10-20-outbound-relay.json) supersedes the earlier local downstream +relay-proof gap, not the hosted qualification gap. The final all-runtime command exited zero on +Axum, workerd 2026-09-10, Viceroy 0.21.0 and Spin 4.1.0. Each adapter passed 24 actual downstream +relay cases and the existing 80 base checks, with 113 asserted base origin receipts. Spin also +retained its six SDK/header/stream controls. All 96 relay observations and 96 ordered relay origin +receipts are embedded, together with the 72 scheduling receipts; full raw-event files and binaries +are identified by hash but remain local, not release assets. + +The relay shares coding target validation, caps and the existing client. It returns +`OutboundResponse::into_response()` directly, without collecting or reporting JSON. A shared TCP +reader captures at most 256 binary body bytes and performs no content decoding. Boundary tests +cover fixed, chunked and close-delimited overflow and exact-cap acceptance, plus fixed/chunked truncation. Reader +failures cannot pass the suite; their remaining diagnostic limitation is that they can occur before +a per-case record, although the group still records failure. + +For each target, 18 Original/206 cases preserved the deterministic gzip/deflate/Brotli bytes and +surviving Content-Type, ETag, Last-Modified, Vary, Accept-Ranges, Cache-Control, Content-Digest and +206 Content-Range fields. The fixtures' digest values match their encoded bytes. Six Decode/200 +controls delivered the expected plain bytes without Content-Encoding. Wire Content-Length, when +present, matched delivered bytes; the validator allows host-selected chunked or close-delimited +framing and rejects conflicting framing. This does not adopt optional decoded-metadata SHOULDs or +promise universal wire Content-Length retention. Cloudflare's outbound manual fetch and downstream +manual encoding have now been exercised together on local workerd. + +Source snapshots identify base `0250edc9` plus the three changed fixture files. No production adapter, +public API, pin, capability, approved Interfaces digest or Fastly engine changed. Core fixture tests +prove streamed handoff does not poll the payload and relay marker-off does not mutate the original +coding client. These are constructed outbound GETs, not actual ingress GET/HEAD forwarding. Full +head-first/eager/follow migration, scheduler-context and cancellation/deadline cases, HTTP/2/3 and +hosted frozen-candidate qualification remain open. Fastly G2 remains failed without waiver. diff --git a/docs/audits/2026-10-25-fastly-backend-policy-proposal.md b/docs/audits/2026-10-25-fastly-backend-policy-proposal.md new file mode 100644 index 000000000..d84c90ddf --- /dev/null +++ b/docs/audits/2026-10-25-fastly-backend-policy-proposal.md @@ -0,0 +1,230 @@ +# Fastly Backend Policy and Readiness Review Packet + +## Decision Status + +**Proposal only, 2026-10-25.** Prepared for G2/G4 against PR #275 source +`b33342d600765e5e17928f3f2164e11941a999b4`. No policy, runtime change, read cap or +production integration is approved by this packet. On 2026-10-25 the requester selected +`B = 31 bounded ladder (Recommended)` in response to the question asking which proposal to +finalize for review, explicitly excluding production integration, runtime changes and hosted +qualification. This records candidate selection, not full G4 resource/primitive approval. +Existing API-only approval remains separate. + +The selected proposal is a ceiling-rounded ladder with literal **B = 31**, compared with +a fixed-ceiling **B = 1** alternative. Neither is an implemented backend policy. Arithmetic checks +below establish only properties of the proposed model, not provider acceptance or performance. +The [original feasibility record](2026-10-18-outbound-feasibility.md) and its negative evidence +remain authoritative: stock readiness loses body errors; local repaired timing has failed samples; +hosted qualification and the full engine migration remain open. + +## Policy Alternatives + +Fix one origin scheme/host/port, wire authority, TLS configuration and effective explicit connect +setting. Count complete provider timer vectors for that tuple, not individual timer values. +Other explicit transport settings, when implemented, also belong to this fixed scope. + +| Candidate | Proposed Deadline-Only B | Tradeoff | +| ------------------------------ | ------------------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| One fixed seven-day ceiling | 1 | Fewest configurations and no budget-jitter boundaries; potentially much longer abandoned host work. | +| Upward-rounded ladder | 31 | More configurations and documented jitter boundaries; smaller timer settings for many shorter anchors, not a measured resource or latency improvement. | +| Existing exact-budget identity | Not an acceptable stable policy | Up to 604,800,000 millisecond budget values within one client namespace, plus client-specific names; retains the fixed phase split. | + +B is not a bound on arbitrary origins, TLS configurations, explicit-setting diversity, sandbox +registrations, concurrent requests, allocations or provider work. Pool/resource limits and retirement +remain their own requirements. The SDK's default maximum of 200 pooled connections per backend is +not a service-wide or memory bound. Both candidates still need resource-policy approval. + +## Recommended Arithmetic Candidate + +Let `U = 604800000 ms`, EdgeZero's existing seven-day aggregate-budget clamp. The full positive +selected-budget domain is `0 < R <= U`, including sub-millisecond values. Expired/zero budgets +still fail without registration. Do not restrict the API to Fastly's published execution duration +or introduce a minimum logical budget to make the policy work. + +At original invocation capture, choose anchor `A` as follows: + +- With a configured per-call timeout, `A = min(configured timeout, U)`, even when a caller + deadline or observation cutoff selects a much shorter `R`. Retain the original request setting; + this is the same effective clamp already applied by core, not a new request-field mutation. +- Otherwise, `A = R`, the selected duration captured at original invocation, including the default + budget where applicable. Never substitute the remainder after preparation or registration. + +Define `n = ceil(A / 1 ms)` and: + +```text +S = {2^k ms | k = 0..29} union {604800000 ms} +Q(A) = the smallest member of S greater than or equal to n milliseconds +``` + +The final member is exactly U, not `2^30 ms`. There are 31 members. For the existing aggregate +body policy, first-byte and between-bytes timers use the **same Q(A)**. An unset connect timer +uses that same value; an explicit connect setting is independently rounded upward to milliseconds, +not through this ladder. In particular, an explicit 200 ms connect setting must not become 256 ms. +Its validated effective setting stays fixed within the tuple being counted; do not make it or +its identity depend on the later remainder. Zero/oversized connect input handling and exact +provider mapping still require EZ-4's implementation and tests; this arithmetic packet does not +silently define another public range or error policy. + +For each fixed tuple, every candidate timer vector is determined by one member of S. Thus the +deadline-only cardinality is at most 31, not `31 * 31`. Equal configured timeouts produce the same +vector under different deadlines/cutoffs or preparation delays. Deadline-only jitter crosses a +bucket only immediately above a listed boundary. Positive sub-millisecond anchors get a 1 ms +provider timer, **not** a rounded-up logical deadline or a minimum accepted request budget. + +Since `Q(A) >= A >= R`, an ordinary provider timer armed no earlier than original entry cannot +expire before the logical deadline solely because of this rounding. This is a timer-arithmetic +argument, conditional on provider semantics, not an interruption or scheduler bound. For integer +`n > 0`, `Q(A) < 2n ms`; B31's largest setting is nevertheless still seven days. Long configured +timeouts with earlier deadlines keep that large setting to preserve eligibility. Native optimizer +blocking horizons remain UNKNOWN; timer arithmetic does not establish H(K). + +Logical entry time, absolute deadline, source, cutoff semantics, caps, typed errors and native +optimizer dispatch remain unchanged requirements. Candidate names must encode a reviewed policy +version and all transport settings, not client/sandbox identity, exact remainder, path, query, +method, nontransport headers/body, response mode, marker, decoding choice or optimizer options. +This packet does not implement naming, collision checks, registration caching or slack removal. + +### Unclosed Policy Evidence + +- The pinned SDK serializes durations with `Duration::as_millis()` followed by checked `u32` + conversion. Supply already-rounded integer milliseconds; otherwise sub-millisecond precision + is truncated. All members of S fit. SDK representation is not verified hosted acceptance. + [Fastly 0.13.1 builder source](https://docs.rs/fastly/0.13.1/src/fastly/backend/builder.rs.html) +- Viceroy 0.21.0 stores first-byte/between-bytes values but does not retain/enforce Connect. Its + three timing getters return `NotAvailable`. Do not call SDK getters and label their result + connect enforcement, or infer hosted maxima from local source alone. +- SDK/provider acceptance across the emitted range, startup/rolling-deploy identity, registration + accounting, host-resource lifetime and actual cancellation remain unproved for this policy. +- Future idle-mode mapping requires separate slow-reader/backpressure evidence. Do not blindly + apply this aggregate between-bytes bucket to idle streams: application pauses cannot consume + their pending-read idle allowance. No complete idle timer vector or its resource bound is + approved here; F4 and its capability/evidence gates remain open. + +## Readiness Review + +The candidate local sequence is supported only on explicitly repaired Viceroy 0.21.0: + +1. Borrow live handle identities; recompute a positive ABI wait from the original deadline, + flooring remaining milliseconds with a 1 ms minimum. Recheck expiry before and after selection; + rearm an early timeout against the same absolute instant. Equality is expired. +2. Consume a selected pending head with nonblocking `PendingRequestHandle::poll`, not `wait`. + Preserve slot/handle identity and the original typed outcome. +3. Consume one fresh, same-slot readiness result per raw body read, including EOF/error reads. + Recheck expiry immediately before and after that read. Never use one readiness observation + to authorize `read_exact`, repeated reads or a whole-body drain. +4. Release guest-owned handles before recording terminal completion. Cleanup is inside timing + accounting, and guest release does not prove prompt host teardown. + +The repair retains a nonempty chunk or original owned error, skips empty frames cooperatively +and yields on decoder no-output. The retained 135-probe/147-origin matrix exercised destination +capacities 1 and 16,384 bytes, tiny payloads, one 500 ms deadline and fixed two-second provider +timers. **16 KiB is a tested capacity, not an approved production read cap or full-sized-copy +cost measurement.** Registration was outside that window. The sibling probe waits for both heads +before body selection; it does not prove arbitrary mixed-head/body fairness, heterogeneous +deadlines or the production multiplexer. + +Stock loses 24 selected body errors in the latest retained comparison. The repaired run preserves +those errors but includes a 505.158 ms terminal sample against the 505 ms target; cleanup ends at +505.117 ms. These failures are not rewritten as passes. The timing amendment allows the existing +Send-first subset, not automatic adoption of this primitive, a new runtime pin or hosted approval. + +### Reviewed Input Identities + +| Input | Identity | +| ---------------------------- | ------------------------------------------------------------------ | +| EdgeZero source | `b33342d600765e5e17928f3f2164e11941a999b4` | +| Core time source SHA-256 | `bcd7047587f656b6547c860986f0c8f6459a5554850cbac4c1bfd9c480ec7435` | +| Fastly SDK/sys | `0.13.1` | +| SDK builder source SHA-256 | `35cf9287819c6a07ed517500a3a01465fa2fe61e23893450080e79986a75004b` | +| Viceroy upstream | `v0.21.0`, `a69d982d1d95e13c0fc9f92683b9118639bb4b04` | +| Tracked repair patch SHA-256 | `8693ce34f82cc98dc272bf39379007b17a40dfc19138c55b445378fd9a163759` | + +The [repair recipe](https://github.com/IABTechLab/edgezero/blob/b33342d600765e5e17928f3f2164e11941a999b4/patches/viceroy/README.md) and +[latest retained comparison](evidence/2026-10-24-fastly-local-recheck.json) identify separate +stock/repaired executables, source/guest hashes and traces. This source review adds no new host +execution. It cannot turn the unmodified runtime into a supported primitive. + +## Executable Arithmetic Check + +Run from the worktree root. This is a proposal model, not a test of live adapter dispatch, +provider registration, decoding, cancellation or native optimizer behavior. The checks cover every +ladder boundary at -1/exact/+1 ns within the domain, 1 ns, U, configured timeouts above U, +equal-timeout requests straddling a deadline-only bucket, explicit Connect quantization and zero +selected-budget rejection. Algebra supplies the interval coverage; samples do not exhaust time. + +```sh +node --input-type=module <<'JS' +import assert from 'node:assert/strict'; +const maxMs = 604800000n; +const unit = 1000000n; +const maxNs = maxMs * unit; +const ceilings = Array.from({length: 30}, (_, i) => 1n << BigInt(i)).concat(maxMs); +const clamp = ns => ns > maxNs ? maxNs : ns; +const ceilMs = ns => (ns + unit - 1n) / unit; +function phase(timeoutNs, selectedNs) { + assert(selectedNs > 0n && selectedNs <= maxNs); + const anchor = timeoutNs === undefined ? selectedNs : clamp(timeoutNs); + assert(anchor >= selectedNs); + const timer = ceilings.find(ms => ms >= ceilMs(anchor)); + assert(timer !== undefined); + return timer; +} +assert.equal(ceilings.length, 31); +const samples = new Set([1n, maxNs]); +for (const ms of ceilings) { + for (const delta of [-1n, 0n, 1n]) { + const ns = ms * unit + delta; + if (ns > 0n && ns <= maxNs) samples.add(ns); + } +} +const configurations = new Set(); +for (const ns of samples) { + const timer = phase(undefined, ns); + configurations.add(timer); + assert(timer * unit >= ns); + assert(timer < 2n * ceilMs(ns)); + assert(timer <= 4294967295n); + assert(!ceilings.some(ms => ms >= ceilMs(ns) && ms < timer)); +} +assert.equal(configurations.size, 31); +let timeoutControls = 0; +for (const rawTimeout of [1n, 800000000n, 1000000000n, 15000000000n, maxNs, maxNs * 100n]) { + const timeout = clamp(rawTimeout); + const selected = new Set([1n, timeout, timeout / 2n || 1n]); + const expected = phase(rawTimeout, timeout); + for (const ns of selected) { + assert.equal(phase(rawTimeout, ns), expected); + timeoutControls++; + } +} +for (const ms of [511n, 513n, 640n, 655n]) { + assert.equal(phase(800n * unit, ms * unit), 1024n); +} +assert.equal(phase(undefined, 511n * unit), 512n); +assert.equal(phase(undefined, 513n * unit), 1024n); +assert.equal(ceilMs(200n * unit), 200n); +assert.equal(phase(undefined, 1n), 1n); +assert.equal(phase(maxNs * 100n, maxNs), maxMs); +assert.throws(() => phase(undefined, 0n)); +console.log(JSON.stringify({proposal_B: 31, boundary_samples: samples.size, + timeout_controls: timeoutControls, qualification: false, production_integrated: false})); +JS +``` + +Observed: **93 boundary samples, 16 timeout-anchor controls**, plus the explicit crossed-bucket +controls. No adapter test, capability or acceptance leg is promoted by that result. + +## Required Decisions + +| Decision | State | +| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| Proposal to finalize | B31 selected by the requester on 2026-10-25; B1 retained as the comparison. | +| Anchor domain, rounding and fixed-tuple resource-policy approval | Pending attributed maintainer review; candidate selection does not approve implementation. | +| Repaired-runtime primitive / unchanged stock-runtime disposition | Pending; unchanged stock is not error-preserving. | +| Production maximum read size and bounded-work/fairness implementation | Pending; exercised buffer capacities alone do not approve these. | +| Hosted acceptance, cancellation, slow-reader behavior and H(K) | Pending external evidence; no deployment authorized. | + +Approve those subjects separately in the existing G4 record. A choice of arithmetic candidate, +green local checks, API approval or a generic instruction to continue does not approve the others. +No production timer, backend identity, dispatch slack, runtime pin, public protocol or resource +limit is changed by preparing this packet. The implemented outbound baseline remains protected. diff --git a/docs/audits/2026-10-27-spin-sdk-eager-progress.md b/docs/audits/2026-10-27-spin-sdk-eager-progress.md new file mode 100644 index 000000000..6387931fa --- /dev/null +++ b/docs/audits/2026-10-27-spin-sdk-eager-progress.md @@ -0,0 +1,61 @@ +# Spin SDK Eager Progress Dependency Audit + +## Source And Scope + +The SDK is pinned to `https://github.com/stackpop/spin-rust-sdk.git` at +`e65ff90dadf4173b08b14194e878ad3acd555337`, based on upstream SDK 7 commit +`2dfd93d2e76e89d7cf74ca43140be7730e0746eb`. The proposed upstream change is +[spinframework/spin-rust-sdk#169](https://github.com/spinframework/spin-rust-sdk/pull/169). +It upgrades WASIp3 from 0.7.1 to 0.9.0 while retaining WASI 0.3.0 interfaces and uses +wit-bindgen 0.62.0, whose executor includes the eager task-queue correction. + +The root workspace, demo, reusable fixture and generator all declare the same SDK version, +Git URL and full revision. Their lockfiles select the SDK and macro from that source. +Applications using the SDK directly must use the same source rather than mixing registry +and Git SDK instances. Dependency guards compare source/revision as well as version. + +The deliberate API migration is `wasip3::spawn` to `wasip3::spawn_local`. There is no alias, +vendored SDK, observer-driven producer, second executor or transport workaround. The private +task-context probe queries and immediately restores the opaque pointer through `wasip3_task_set`; +it never dereferences the executor's private task-set layout. Non-null context alone does not prove +executor identity; one coordinated SDK dependency and runtime tests provide that proof. + +## Reproduction And SDK Verification + +With wit-bindgen 0.57.1, newly spawned tasks are added after the current task poll returns +Pending, without polling the new tasks before waiting on an older host event. The SDK +regression waits for a short host timer, spawns a nested child and awaits its local result +while a thirty-second host timer remains pending. A five-second host timeout prevents the +long timer from rescuing the old executor. The exact regression times out on the old chain +and passes with this revision. + +Local SDK validation used Rust 1.98.1 on macOS: 32 SDK tests, 33 SDK doctests, all 21 WASM +workspace packages, SDK-only strict Clippy, formatting and minimal/json/postgres4-types +feature checks passed. Existing ignored doctests remain ignored. Full-workspace strict Clippy +reports generated gRPC `result_large_err` warnings on this compiler; the same failure was +reproduced on unchanged upstream. Upstream's Rust 1.94 CI remains a separate check. + +## Qualification Boundary + +Final EdgeZero adoption checks pass all five root Rust gates, strict Spin-only and +Spin/test-utils WASM Clippy, 35 WASM contracts, 21 SDK-resource tests and the eager ownership +component on exact Wasmtime 48.0.2. The demo's 60 core tests and Spin build, fixture's 42 core +tests, generator contracts, fresh scaffold's 33 core tests and Spin build/strict Clippy pass. +The expensive ignored full generated-workspace gate was not rerun locally; these scoped +scaffold checks are not a claim that it ran. Decoder/map, documentation and evidence guards +retain their existing contracts and the inventory remains unqualified. + +The signed SDK candidate removes the observed slow-origin batch starvation in local Spin +4.1.0, passes 68 HTTP wire exchanges with 84 origin receipts, and passes the existing eager +ownership component with the explicitly selected Wasmtime 48.0.2. These are local observations, +not hosted qualification. Fixed timeline probes still miss their strict timing targets; they +remain failures. No deadline is extended, tolerance widened, capability promoted, acceptance +leg activated or candidate frozen by this dependency decision. The original logical deadlines, +observation cutoffs, typed errors, caps and exactly-once ownership requirements are unchanged. + +The local Spin runs retain a trailing `worker failed: worker panicked` host diagnostic, also +present before this SDK upgrade. Repeated timestamped shutdown traces with identical guest +artifacts place that message after requested shutdown and the last response. This distinguishes +the observed host cancellation diagnostic from an active-exchange failure; it does not prove +clean hosted shutdown, transport teardown or confirmed client delivery. Preserve the diagnostic +and the hosted lifecycle gate rather than suppressing the log or promoting capabilities. diff --git a/docs/audits/evidence/2026-10-19-fastly-readiness.json b/docs/audits/evidence/2026-10-19-fastly-readiness.json new file mode 100644 index 000000000..7aca2c8e1 --- /dev/null +++ b/docs/audits/evidence/2026-10-19-fastly-readiness.json @@ -0,0 +1,332 @@ +{ + "schema": 1, + "execution_date": "2026-10-19", + "status": "Failed", + "qualification": false, + "base_revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "uncommitted working-tree characterization, not qualification for base_revision", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-readiness", + "exit_code": 1, + "source_sha256": { + "tests/fixtures/reusable-app/Cargo.lock": "da27aa5e3c6c062b18ddabc45a2ed4f86196cc7bbf58b46da69bdfbd047d3f6f", + "tests/fixtures/reusable-app/crates/fixture-fastly/Cargo.toml": "c677d3eae023a662a33452685bf1668feb6a2beb6a885744372e5ddf4b879276", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs": "90b959abd5250a230c04409518f6416436db5485c0a174deb9a6a0a5e51bad4e", + "tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs": "640c7e1ff4aaabcca9ba09cdcd26800bf4aef3b4fc00f464532ebf6e89f894bd", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs": "5dd4463d89ee25b6a5db134361786006d24e83f5f62037f255699a4bb2cde3b2", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs": "29e5caf2139519cf3a135f497905de1e2ea2d449de32355be421eb1ecee13b43", + "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs": "b0b1697f06a84db147b2581085ea1472126b5e91dc9a05715501736580e6afd4" + }, + "viceroy_source_sha256": { + "body.rs": "df2134af7ebf5d404b1ad41fcc744737325c7d0d929908f8d8742f50a93f4ee5", + "async_io.rs": "49a73f24b6c495e5240768b4e3af47af21252ae6e4c891df80c635395e3025c3", + "sandbox/async_item.rs": "5ea625789320ecf07988c5fff9db860f7c0b11a69f22779d7cf3b76b64b1f757" + }, + "binary_sha256": "908f69f5224f5cab40efaba8c9856473c45cab260102ce3c251ae548b9d969e3", + "raw_events_sha256": "7c0731f08a53ae4c9d576e2b3f1247e79b9314190cfe16e0b5f1e3d24fc52bad", + "events": [ + { + "construction_rounds": 0, + "event": "run", + "max_requests": 10, + "seed": 856, + "suite": "outbound-readiness" + }, + { + "event": "build_environment", + "host_arch": "aarch64", + "host_os": "macos", + "lockfile": { + "bytes": 84052, + "fnv1a64": "f1ec2624d284e86b", + "path": "/private/tmp/edgezero-outbound-readiness-final/Cargo.lock", + "purpose": "non-security build identity" + }, + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)" + }, + { + "adapter": "fastly", + "event": "runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "expected": "0.21.0", + "version": "viceroy 0.21.0" + }, + { + "artifact": { + "bytes": 737829, + "fnv1a64": "b61715b6c7c8d1a1", + "path": "/private/tmp/edgezero-outbound-readiness-final/artifacts/fixture-fastly-outbound-readiness.wasm", + "purpose": "non-security build identity" + }, + "event": "readiness-build", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sdk": "0.13.1", + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "direct", + "qualification": false, + "read_limit": 1, + "repetition": 0, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "samples": [ + { + "end_us": 12524, + "operation": "head-select", + "selected": 0, + "start_us": 1039, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "bytes": 1, + "end_us": 13902, + "operation": "read", + "start_us": 12831 + }, + { + "end_us": 64129, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 13904 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "selected", + "qualification": false, + "read_limit": 1, + "repetition": 0, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "samples": [ + { + "end_us": 3983, + "operation": "head-select", + "selected": 0, + "start_us": 27, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "end_us": 3994, + "operation": "body-select", + "selected": 0, + "start_us": 3989, + "status": "FastlyStatus::OK", + "timeout_ms": 496 + }, + { + "bytes": 1, + "end_us": 4017, + "operation": "read", + "start_us": 3995 + }, + { + "end_us": 56289, + "operation": "body-select", + "selected": 0, + "start_us": 4018, + "status": "FastlyStatus::OK", + "timeout_ms": 495 + }, + { + "bytes": 0, + "end_us": 56339, + "operation": "read", + "start_us": 56291 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "direct", + "qualification": false, + "read_limit": 1, + "repetition": 1, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "samples": [ + { + "end_us": 4782, + "operation": "head-select", + "selected": 0, + "start_us": 23, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "bytes": 1, + "end_us": 4809, + "operation": "read", + "start_us": 4787 + }, + { + "end_us": 55070, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 4813 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "selected", + "qualification": false, + "read_limit": 1, + "repetition": 1, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "samples": [ + { + "end_us": 3320, + "operation": "head-select", + "selected": 0, + "start_us": 37, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "end_us": 3331, + "operation": "body-select", + "selected": 0, + "start_us": 3326, + "status": "FastlyStatus::OK", + "timeout_ms": 496 + }, + { + "bytes": 1, + "end_us": 3374, + "operation": "read", + "start_us": 3332 + }, + { + "end_us": 56533, + "operation": "body-select", + "selected": 0, + "start_us": 3375, + "status": "FastlyStatus::OK", + "timeout_ms": 496 + }, + { + "bytes": 0, + "end_us": 56586, + "operation": "read", + "start_us": 56535 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "direct", + "qualification": false, + "read_limit": 1, + "repetition": 2, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "samples": [ + { + "end_us": 744, + "operation": "head-select", + "selected": 0, + "start_us": 21, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "bytes": 1, + "end_us": 771, + "operation": "read", + "start_us": 749 + }, + { + "end_us": 50945, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 772 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "selected", + "qualification": false, + "read_limit": 1, + "repetition": 2, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "samples": [ + { + "end_us": 7289, + "operation": "head-select", + "selected": 0, + "start_us": 17, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "end_us": 7299, + "operation": "body-select", + "selected": 0, + "start_us": 7294, + "status": "FastlyStatus::OK", + "timeout_ms": 492 + }, + { + "bytes": 1, + "end_us": 7339, + "operation": "read", + "start_us": 7300 + }, + { + "end_us": 57681, + "operation": "body-select", + "selected": 0, + "start_us": 7340, + "status": "FastlyStatus::OK", + "timeout_ms": 492 + }, + { + "bytes": 0, + "end_us": 57709, + "operation": "read", + "start_us": 57683 + } + ], + "source_state": "working-tree-characterization" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "selected": null, + "status": "fail" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "selected": null, + "status": "fail" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "selected": null, + "status": "fail" + }, + { + "adapter": "fastly", + "error": "pinned Viceroy body readiness consumed the source error; Fastly G2 fails", + "status": "fail" + } + ] +} diff --git a/docs/audits/evidence/2026-10-19-outbound-coding.json b/docs/audits/evidence/2026-10-19-outbound-coding.json new file mode 100644 index 000000000..baa575c3f --- /dev/null +++ b/docs/audits/evidence/2026-10-19-outbound-coding.json @@ -0,0 +1,515 @@ +{ + "schema": 1, + "recorded_date": "2026-10-19", + "base_revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "scope": "outbound original/206 coding only; JSON report, not downstream encoded relay", + "source_sha256": { + "Cargo.toml": "ee73d1318af74a997ecc12bac0f80cd7eefbe8ead34a35b1432c8d6f49fc42e1", + "Cargo.lock": "1afb33fe3067419ad5a2a948b330ac1df123d53214e49dc4005a92f7bc69e043", + "crates/edgezero-core/src/outbound.rs": "a2ae10cc7ac9c4221cb775ccd689f398527e08b95a7969f551edc219302f788b", + "crates/edgezero-core/src/outbound/exchange.rs": "f8ca9867b572b2e40e0c08ae55fefefaefbf82355c83aa8b28f74870d6e2a879", + "crates/edgezero-core/src/lib.rs": "bb75c80ecb47152603d75dd3c7c0e51aed9229b082ea983a222c445ef6f44101", + "crates/edgezero-core/src/compression.rs": "4acccc0c01009894ebe4f77f57ce8ce501d3f3f2ccb565c6b3d3871d826037d4", + "crates/edgezero-core/src/body.rs": "50a8bc65fcbc7df3d9afd3ddc520889f42da46af1c6419e23c19625c33d5bf9e", + "crates/edgezero-core/src/time.rs": "bcd7047587f656b6547c860986f0c8f6459a5554850cbac4c1bfd9c480ec7435", + "crates/edgezero-adapter-fastly/src/outbound.rs": "5e12fe9409efd446fdc1fdcc770a10bce7a55b79e5630c2bc7797d50dcaf058a", + "crates/edgezero-adapter-spin/src/outbound.rs": "3fcfbaed6a51e161023616bf9ef3b1aa60eb6b8830d0386ce2924df43cb44194", + "crates/edgezero-adapter-spin/src/response.rs": "03e8b98de3e4854810b4bdabba7783d3a66cacb5e91402e941609d7dfe567075", + "tests/fixtures/reusable-app/Cargo.toml": "7665200757e4e565d11fa7eb036215aa583d21fd543bca39fdbdce470fee11ae", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs": "a2e03f9201cbc97150b9453e281f20be41fdbb06d9a0451864d875f7d226bce2", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/progress.rs": "b806602abe3aa03a66c68b120906c97436325344ad9512159daf1d1bfa2b5a95", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs": "b4b57e2dc0ef627078f5e8d7677b4487c4e0a8101a6993f1eaf201226c6d2d92", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs": "0b4a81e3678459801b9f5a9379de70160452317929936d1b6c85393c1249c704", + "tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs": "58fec2e1968f40d38a40a8e38642d27be22d4bf520bb29fd8d58fffa688f2414" + }, + "limitations": [ + "not an authoritative frozen candidate", + "hosted execution pending", + "Cloudflare runtime blocked on exact build tool", + "Fastly mixed-choice batch host proof pending", + "Fastly readiness gate remains failed" + ], + "runs": [ + { + "adapter": "fastly", + "command": "cargo run --locked --offline --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -- --adapter fastly --suite outbound-progress --output /private/tmp/edgezero-coding-fastly-final", + "exit_code": 0, + "runtime": { + "adapter": "fastly", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "viceroy 0.21.0" + }, + "artifact": { + "bytes": 2123914, + "fnv1a64": "4a5b4a54734e4303", + "path": "/private/tmp/edgezero-coding-fastly-final/progress-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "purpose": "non-security build identity", + "sha256": "03ef67a8004a55ae5e989b6587e0c81497cf749eef5648075f7a1b37bea6cfa4" + }, + "raw_events_sha256": "0c84e5fc2f2900c8d7b7bbfbbc3e7df242f8b50bdd801c24a99130173814935e", + "checks": [ + { + "case": "coding-gzip-buffered-200-original", + "result": "pass", + "status": 200, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-buffered-206-original", + "result": "pass", + "status": 206, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-buffered-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-buffered-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + }, + { + "case": "coding-gzip-streamed-200-original", + "result": "pass", + "status": 200, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-streamed-206-original", + "result": "pass", + "status": 206, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-streamed-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-streamed-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + }, + { + "case": "coding-deflate-buffered-200-original", + "result": "pass", + "status": 200, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-buffered-206-original", + "result": "pass", + "status": 206, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-buffered-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-buffered-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + }, + { + "case": "coding-deflate-streamed-200-original", + "result": "pass", + "status": 200, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-streamed-206-original", + "result": "pass", + "status": 206, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-streamed-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-streamed-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + }, + { + "case": "coding-br-buffered-200-original", + "result": "pass", + "status": 200, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-buffered-206-original", + "result": "pass", + "status": 206, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-buffered-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-buffered-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + }, + { + "case": "coding-br-streamed-200-original", + "result": "pass", + "status": 200, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-streamed-206-original", + "result": "pass", + "status": 206, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-streamed-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-streamed-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + } + ] + }, + { + "adapter": "spin", + "command": "cargo run --locked --offline --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -- --adapter spin --suite outbound-progress --output /private/tmp/edgezero-coding-spin-final", + "exit_code": 0, + "runtime": { + "adapter": "spin", + "event": "progress-runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + }, + "artifact": { + "bytes": 2273342, + "fnv1a64": "efa8f7778511c0dc", + "path": "/private/tmp/edgezero-coding-spin-final/progress-spin/artifacts/fixture_spin.wasm", + "purpose": "non-security build identity", + "sha256": "affa72b2119189bc818af46957634f83af3bc2f5378bb91206783207df47c2b5" + }, + "raw_events_sha256": "f2df8a2817625d77f4d49dfb4404a7da881cf47cbfd7ce6c053eaa2f3f022a58", + "checks": [ + { + "case": "coding-gzip-buffered-200-original", + "result": "pass", + "status": 200, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-buffered-206-original", + "result": "pass", + "status": 206, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-buffered-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-buffered-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + }, + { + "case": "coding-gzip-streamed-200-original", + "result": "pass", + "status": 200, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-streamed-206-original", + "result": "pass", + "status": 206, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-streamed-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "gzip", + "content_length": "37", + "bytes": 37, + "bytes_sha256": "064cdf94732ad7431208f0c1eda006adbdf314eaeb0933a9e38ff945d295a27e" + }, + { + "case": "coding-gzip-streamed-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + }, + { + "case": "coding-deflate-buffered-200-original", + "result": "pass", + "status": 200, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-buffered-206-original", + "result": "pass", + "status": 206, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-buffered-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-buffered-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + }, + { + "case": "coding-deflate-streamed-200-original", + "result": "pass", + "status": 200, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-streamed-206-original", + "result": "pass", + "status": 206, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-streamed-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "deflate", + "content_length": "25", + "bytes": 25, + "bytes_sha256": "f1562246ce7b1c632b2b11380c3afb28296ffae09ef1af5874fb17cc488adead" + }, + { + "case": "coding-deflate-streamed-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + }, + { + "case": "coding-br-buffered-200-original", + "result": "pass", + "status": 200, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-buffered-206-original", + "result": "pass", + "status": 206, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-buffered-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-buffered-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + }, + { + "case": "coding-br-streamed-200-original", + "result": "pass", + "status": 200, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-streamed-206-original", + "result": "pass", + "status": 206, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-streamed-206-decode", + "result": "pass", + "status": 206, + "content_encoding": "br", + "content_length": "21", + "bytes": 21, + "bytes_sha256": "9c8e6d2d051677e038339ab1ebfe2008807a74b79633f11c7f0020d06a909c14" + }, + { + "case": "coding-br-streamed-200-decode", + "result": "pass", + "status": 200, + "content_encoding": null, + "content_length": null, + "bytes": 17, + "bytes_sha256": "9db9e64f7fdd4d21f9d843b34b1c7f64c93339f6e713d827522677efed13659b" + } + ] + } + ] +} diff --git a/docs/audits/evidence/2026-10-19-outbound-progress.json b/docs/audits/evidence/2026-10-19-outbound-progress.json new file mode 100644 index 000000000..daa0b3061 --- /dev/null +++ b/docs/audits/evidence/2026-10-19-outbound-progress.json @@ -0,0 +1,1501 @@ +{ + "schema": 1, + "recorded_date": "2026-10-19", + "base_revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "command": "scripts/smoke_test_reusable_app.sh --adapter all --suite smoke", + "exit_code": 1, + "tool_overrides": { + "SPIN_BIN": "/opt/homebrew/bin/spin", + "WRANGLER_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler", + "WORKER_BUILD_BIN": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build" + }, + "sdk": { + "fastly": "0.13.1", + "worker": "0.8.5", + "spin_sdk": "7.0.0", + "wasip3": "0.7.1+wasi-0.3.0", + "reqwest": "0.13.4" + }, + "source_sha256": { + "tests/fixtures/reusable-app/crates/fixture-axum/Cargo.toml": "1c1421c7497a7aff305d31ecd4c3ee323eb646acd4d56f23cab765ab6a5e377c", + "tests/fixtures/reusable-app/crates/fixture-axum/src/bin/outbound_progress.rs": "35594150be188ed8598ea1aea6e291f9b00c6aeb558bc4a65de8ac0f9922b69f", + "tests/fixtures/reusable-app/crates/fixture-axum/src/main.rs": "43cbebe047abdf77b4d48f55fd4098eb9c57ac17bedd9e6621335104f1710474", + "tests/fixtures/reusable-app/crates/fixture-axum/src/progress.rs": "f4f1489d4c36bc0814f29811babce7d785b7816528bdbbe3d1278bef05e2ca41", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/Cargo.toml": "96501fc975013ba68411cd6e0f77b6b0fe505dfe4ed8cef16620ef19b4c9784d", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs": "606245131f0c594154ca0a2eb63e5341999325bfb2de55887ab3264fa4b89559", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs": "305f70039af59501639916bd8cfb8db696613c73af670e953dd329a3eed14498", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml": "6c2a097879b98f4d4af3e2fea139215abc9fa0bfdc058500f28fd9f7d757aab3", + "tests/fixtures/reusable-app/crates/fixture-core/Cargo.toml": "f9c68c281c9b57269ee8065f46e9e1c3fcc9c58a562259e480f2125877a8f6c8", + "tests/fixtures/reusable-app/crates/fixture-core/src/lib.rs": "72cd88d5d7d274fa52a4af249a6723e7b842680f08049961cd85d94ed77c2375", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs": "d866a8f17de6544b6963c0a4f383fe7666b81363d6797785bb1e2d7f7ee46023", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs": "321cda3626dbe5e68da4ea1e9b5f71f531c0630f2763196fff8b0e7b64f301db", + "tests/fixtures/reusable-app/crates/fixture-fastly/Cargo.toml": "e82ea4713f86db533796367cee0c3f91191f33e7e1f0510d0cf1bdafd509a7e1", + "tests/fixtures/reusable-app/crates/fixture-fastly/fastly.toml": "4444af855e1f5db54da213ddfc6a84ea34aa52f9fd868d047521b0ab78a3fd18", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/custom_rebuild_per_request.rs": "bb55c45b46bf7afa90546982f90093f1bd5dd10a1f2324c985aba49a64650ea3", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/custom_retained_app.rs": "229b248e9b52a0b433d17e64706569fcb3285818e9975337291107c4b2219d2b", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/custom_single_request.rs": "aeeeb95a1e0a994ea6c62b62977d45e13dc9cce45b6a83fa9135584ab63edff9", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/faults.rs": "c4771da94ddb7ca45e466f4872f1fb5ee9d49ea699b42ac9f01fdf9eeef32112", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_lifetime.rs": "a71e6a90a3710d4a1651f2a4fb76bbe09fad89e9f4be57caa860b8d3cdfd7749", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_memory.rs": "f608680fad1b9eb0f2d6c7e46ab81934ec05d9f0336a92234b550b28ce1b5e97", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_requests.rs": "479f9c6f0787fb25e9a7f27e007ef5b9ee8ceada64b118688456dafd3b92e191", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/logger_negative.rs": "3474b8e2902a7cbddf84fdd98444af17f10898c16cae52588f82d64abe1a5227", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs": "0b4a81e3678459801b9f5a9379de70160452317929936d1b6c85393c1249c704", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs": "90b959abd5250a230c04409518f6416436db5485c0a174deb9a6a0a5e51bad4e", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/qualification.rs": "8a0d6c5d0d29882ec48829a76aa20b5000aa76f87f07265208bc8f75f4268f84", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/rebuild_per_request.rs": "19ec8d044ec2841309c0d6da6e91ffc08343653235312214cc00571386794b87", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/retained_app.rs": "629635c72fe281cda10cb86c75329df67aa3ba53e9d6d93ffe0019eda951d27d", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/single_request.rs": "408f4a4cbc84e85d0d20b4a1d40c8404c6adaa2c02a059da30d52bbb12e4d4ae", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs": "0d727361a788ff8b10aa5d48b80b43bd43411a53ac32fbfb3fb605f9d6bc4768", + "tests/fixtures/reusable-app/crates/fixture-harness/Cargo.toml": "93580bf7575878ce3cb5d82ef3e7aea6d5c26baae6f467f469339858992fd141", + "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs": "22f5c8ff413357b5684910249c635a4993d0cb912b3cc584c8f14cc2258efb18", + "tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs": "1344e6fb8e4e22426ec5ceaacba210c034121970d8b49ece79e6ef3a871776b8", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs": "a4181724068841ef44f112a107566180b58c3a4618921c14eca3ceb823846a05", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/progress.rs": "48ea94cd46c9491a3f6e3a1d8cd7166c3d6d2b8765b0f87fffc61041809a1f0f", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs": "f07fd19c3937780e5a7973513ccd29dd4708c852f62f4b2dd3e14929181605f9", + "tests/fixtures/reusable-app/crates/fixture-spin/Cargo.toml": "7256c3384d5e3c675b8b9e7026d10630bc5fec9c3aaf3900bc740fcb4e1526b4", + "tests/fixtures/reusable-app/crates/fixture-spin/runtime-config.toml": "716d293f350f0e1cb00e4b20b3d428db1cb06f6fe6e0590815147839368916b8", + "tests/fixtures/reusable-app/crates/fixture-spin/spin.toml": "dfed653d283de300aea36feb4841ee731e20643dd60df83dc0907047fdb78d9c", + "tests/fixtures/reusable-app/crates/fixture-spin/src/lib.rs": "a08d5b6c4bfbfe067ffafb8d4996c67a1105c104676f3d9aed5ff66831ac794a", + "tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs": "cb3a8a9c02bb23210e27fa291f8a5a99c6d72fb91fe86eb350205eb1384535b7", + "tests/fixtures/reusable-app/Cargo.toml": "7665200757e4e565d11fa7eb036215aa583d21fd543bca39fdbdce470fee11ae", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "scripts/smoke_test_reusable_app.sh": "5ffedd5e84ae8f8f1e8f30e0ba4b7b4032ffdea48776844619f1e354197a7c1b" + }, + "compiled_artifacts": [ + { + "adapter": "fastly", + "path": "/private/tmp/edgezero-g3-smoke-all/progress-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "sha256": "a5d7f031dc8b715779c29f261c651d541b80dee498ef159210fb8b27eade784c" + }, + { + "adapter": "spin", + "path": "/private/tmp/edgezero-g3-smoke-all/progress-spin/artifacts/fixture_spin.wasm", + "sha256": "5245e493c6fcf95091fd4d5b3b4353e6e390dd2015d2fd451f0fc6e61ab649ce" + }, + { + "adapter": "axum", + "path": "/private/tmp/edgezero-g3-smoke-all/progress-axum/artifacts/fixture-axum-outbound-progress", + "sha256": "915d01378fbc9b84f2fbd8a7b6d7a8ba3f27d4f192e7e8f3d892cc700c4b6421" + } + ], + "retention": "This repository record retains G3 events and runtime logs, not binaries or the complete unrelated smoke history. No release qualification or exact-clean-commit claim.", + "retained_events_sha256": "fc55a59bf91e8f7b1a88f67040ba234becc903b1b7a1504917adad829e88d55c", + "original_smoke_jsonl_sha256": "c95ca4e29fae0628d0fba1d4e2ad0e7d827831436a0ee6e7e2e8ad4430c9d722", + "events": [ + { + "adapter": "fastly", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "viceroy 0.21.0" + }, + { + "adapter": "fastly", + "artifact": { + "bytes": 2092752, + "fnv1a64": "e0a70c916e9e5cec", + "path": "/private/tmp/edgezero-g3-smoke-all/progress-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "purpose": "non-security build identity" + }, + "event": "build", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "variant": "fastly-outbound-progress" + }, + { + "adapter": "fastly", + "case": "eager-single", + "client": { + "body": "{\"adapter\":\"fastly\",\"case\":\"eager-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":103463333,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e00c628a2d72a0-3939-b7\"}", + "complete_ns": 330234750, + "event": "client_completed", + "first_byte_ns": 330221084, + "header_ns": 330218500, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:29 GMT"] + ], + "status": 200, + "token": "18e00c628a2d72a0-3939-b7" + }, + "event": "outbound-progress", + "probe": { + "adapter": "fastly", + "case": "eager-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 103463333, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e00c628a2d72a0-3939-b7" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c628a2d72a0-3939-b7/start", + "received_ns": 15141625 + }, + { + "path": "/g3/18e00c628a2d72a0-3939-b7/slot-0", + "received_ns": 28375333 + }, + { + "path": "/g3/18e00c628a2d72a0-3939-b7/end", + "received_ns": 121140833 + } + ], + "status": "pass" + }, + { + "adapter": "fastly", + "case": "lazy-single", + "client": { + "body": "{\"adapter\":\"fastly\",\"case\":\"lazy-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":100707666,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e00c629e535c68-3939-b8\"}", + "complete_ns": 444165834, + "event": "client_completed", + "first_byte_ns": 444154834, + "header_ns": 444152167, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:29 GMT"] + ], + "status": 200, + "token": "18e00c629e535c68-3939-b8" + }, + "event": "outbound-progress", + "probe": { + "adapter": "fastly", + "case": "lazy-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 100707666, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e00c629e535c68-3939-b8" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c629e535c68-3939-b8/start", + "received_ns": 10362167 + }, + { + "path": "/g3/18e00c629e535c68-3939-b8/end", + "received_ns": 127541375 + }, + { + "path": "/g3/18e00c629e535c68-3939-b8/slot-0", + "received_ns": 137642792 + } + ], + "status": "pass" + }, + { + "adapter": "fastly", + "case": "eager-batch", + "client": { + "body": "{\"adapter\":\"fastly\",\"case\":\"eager-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":102384000,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e00c62b900c870-3939-b9\"}", + "complete_ns": 337059375, + "event": "client_completed", + "first_byte_ns": 337048375, + "header_ns": 337045458, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:29 GMT"] + ], + "status": 200, + "token": "18e00c62b900c870-3939-b9" + }, + "event": "outbound-progress", + "probe": { + "adapter": "fastly", + "case": "eager-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 102384000, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0, 0], + "slots": 2, + "token": "18e00c62b900c870-3939-b9" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c62b900c870-3939-b9/start", + "received_ns": 10723917 + }, + { + "path": "/g3/18e00c62b900c870-3939-b9/slot-1", + "received_ns": 25786625 + }, + { + "path": "/g3/18e00c62b900c870-3939-b9/slot-0", + "received_ns": 25794875 + }, + { + "path": "/g3/18e00c62b900c870-3939-b9/end", + "received_ns": 118442042 + } + ], + "status": "pass" + }, + { + "adapter": "fastly", + "case": "baseline-batch", + "client": { + "body": "{\"adapter\":\"fastly\",\"case\":\"baseline-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":100781959,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":2,\"token\":\"18e00c62cdbbb6a8-3939-ba\"}", + "complete_ns": 333829334, + "event": "client_completed", + "first_byte_ns": 333818209, + "header_ns": 333815417, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:29 GMT"] + ], + "status": 200, + "token": "18e00c62cdbbb6a8-3939-ba" + }, + "event": "outbound-progress", + "probe": { + "adapter": "fastly", + "case": "baseline-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 100781959, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 2, + "token": "18e00c62cdbbb6a8-3939-ba" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c62cdbbb6a8-3939-ba/start", + "received_ns": 12472292 + }, + { + "path": "/g3/18e00c62cdbbb6a8-3939-ba/slot-0", + "received_ns": 24413125 + }, + { + "path": "/g3/18e00c62cdbbb6a8-3939-ba/slot-1", + "received_ns": 24419000 + }, + { + "path": "/g3/18e00c62cdbbb6a8-3939-ba/end", + "received_ns": 126442875 + } + ], + "status": "pass" + }, + { + "adapter": "fastly", + "case": "response-owned", + "client": { + "body": " {\"adapter\":\"fastly\",\"case\":\"response-owned\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":true,\"pause_ns\":102190750,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e00c62e214aec0-3939-bb\"}", + "complete_ns": 335220417, + "event": "client_completed", + "first_byte_ns": 17474542, + "header_ns": 17471917, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:29 GMT"] + ], + "status": 200, + "token": "18e00c62e214aec0-3939-bb" + }, + "event": "outbound-progress", + "probe": { + "adapter": "fastly", + "case": "response-owned", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": true, + "pause_ns": 102190750, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0, 0], + "slots": 2, + "token": "18e00c62e214aec0-3939-bb" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c62e214aec0-3939-bb/start", + "received_ns": 15099500 + }, + { + "path": "/g3/18e00c62e214aec0-3939-bb/slot-0", + "received_ns": 25476583 + }, + { + "path": "/g3/18e00c62e214aec0-3939-bb/slot-1", + "received_ns": 25482958 + }, + { + "path": "/g3/18e00c62e214aec0-3939-bb/end", + "received_ns": 127089667 + } + ], + "status": "pass" + }, + { + "adapter": "fastly", + "client": { + "body": "{\"adapter\":\"fastly\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"has_body\":true,\"known_length\":0,\"source\":\"fastly-sdk-before-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 485709, + "event": "client_completed", + "first_byte_ns": 480000, + "header_ns": 478750, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:30 GMT"] + ], + "status": 200, + "token": "18e00c62f6920f28-3939-bc" + }, + "event": "inbound-empty-signal", + "framing": "Absent", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": 0, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "fastly", + "client": { + "body": "{\"adapter\":\"fastly\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"has_body\":true,\"known_length\":0,\"source\":\"fastly-sdk-before-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 313834, + "event": "client_completed", + "first_byte_ns": 307292, + "header_ns": 306750, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:30 GMT"] + ], + "status": 200, + "token": "18e00c62f6abe470-3939-bd" + }, + "event": "inbound-empty-signal", + "framing": "LengthZero", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": 0, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "fastly", + "client": { + "body": "{\"adapter\":\"fastly\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"has_body\":true,\"known_length\":null,\"source\":\"fastly-sdk-before-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 354208, + "event": "client_completed", + "first_byte_ns": 345833, + "header_ns": 343792, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:30 GMT"] + ], + "status": 200, + "token": "18e00c62f6c29120-3939-be" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedEmpty", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": null, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "fastly", + "client": { + "body": "{\"adapter\":\"fastly\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"has_body\":true,\"known_length\":4,\"source\":\"fastly-sdk-before-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 262667, + "event": "client_completed", + "first_byte_ns": 258458, + "header_ns": 257833, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:30 GMT"] + ], + "status": 200, + "token": "18e00c62f6d41d50-3939-bf" + }, + "event": "inbound-empty-signal", + "framing": "FixedBody", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": 4, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "fastly", + "client": { + "body": "{\"adapter\":\"fastly\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"has_body\":true,\"known_length\":null,\"source\":\"fastly-sdk-before-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 218000, + "event": "client_completed", + "first_byte_ns": 214875, + "header_ns": 214417, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:30 GMT"] + ], + "status": 200, + "token": "18e00c62f6ead5b8-3939-c0" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedBody", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": null, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "fastly", + "attempted_empty_body_cases": 5, + "attempted_progress_cases": 5, + "empty_body_passes": 5, + "event": "progress-coverage", + "failures": [], + "limitations": [ + "HTTP/2 and HTTP/3 not exercised", + "hosted execution pending", + "response-owned tests handler/dispatch return, not host callback return", + "missing-context detection unproved except native absent-runtime control", + "no cancellation or G2 readiness qualification" + ], + "origin_requests": 18, + "progress_passes": 5, + "qualification": false + }, + { + "adapter": "cloudflare", + "observed": "wasm-bindgen 0.2.121", + "phase": "build-tooling", + "qualification": false, + "reason": "matching wasm-bindgen CLI unavailable; no download or dependency repin attempted", + "required_wasm_bindgen": "0.2.129", + "status": "unsupported" + }, + { + "adapter": "spin", + "event": "progress-runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + }, + { + "adapter": "spin", + "artifact": { + "bytes": 2202602, + "fnv1a64": "030270cf51331cb1", + "path": "/private/tmp/edgezero-g3-smoke-all/progress-spin/artifacts/fixture_spin.wasm", + "purpose": "non-security build identity" + }, + "event": "build", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "variant": "spin-outbound-progress" + }, + { + "adapter": "spin", + "case": "eager-single", + "client": { + "body": "{\"adapter\":\"spin\",\"case\":\"eager-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":103377583,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e00c6471c76020-3939-e1\"}", + "complete_ns": 335456667, + "event": "client_completed", + "first_byte_ns": 335436917, + "header_ns": 335434209, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:37 GMT"] + ], + "status": 200, + "token": "18e00c6471c76020-3939-e1" + }, + "event": "outbound-progress", + "probe": { + "adapter": "spin", + "case": "eager-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 103377583, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e00c6471c76020-3939-e1" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c6471c76020-3939-e1/start", + "received_ns": 15096750 + }, + { + "path": "/g3/18e00c6471c76020-3939-e1/slot-0", + "received_ns": 30137875 + }, + { + "path": "/g3/18e00c6471c76020-3939-e1/end", + "received_ns": 127949042 + } + ], + "status": "pass" + }, + { + "adapter": "spin", + "case": "lazy-single", + "client": { + "body": "{\"adapter\":\"spin\",\"case\":\"lazy-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":103084125,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e00c64860f6460-3939-e2\"}", + "complete_ns": 449212625, + "event": "client_completed", + "first_byte_ns": 449200458, + "header_ns": 449197500, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:38 GMT"] + ], + "status": 200, + "token": "18e00c64860f6460-3939-e2" + }, + "event": "outbound-progress", + "probe": { + "adapter": "spin", + "case": "lazy-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 103084125, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e00c64860f6460-3939-e2" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c64860f6460-3939-e2/start", + "received_ns": 15009292 + }, + { + "path": "/g3/18e00c64860f6460-3939-e2/end", + "received_ns": 126419584 + }, + { + "path": "/g3/18e00c64860f6460-3939-e2/slot-0", + "received_ns": 141463042 + } + ], + "status": "pass" + }, + { + "adapter": "spin", + "case": "eager-batch", + "client": { + "body": "{\"adapter\":\"spin\",\"case\":\"eager-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":103338208,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e00c64a1d16900-3939-e3\"}", + "complete_ns": 340274708, + "event": "client_completed", + "first_byte_ns": 340263917, + "header_ns": 340260875, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:38 GMT"] + ], + "status": 200, + "token": "18e00c64a1d16900-3939-e3" + }, + "event": "outbound-progress", + "probe": { + "adapter": "spin", + "case": "eager-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 103338208, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0, 0], + "slots": 2, + "token": "18e00c64a1d16900-3939-e3" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c64a1d16900-3939-e3/start", + "received_ns": 14981125 + }, + { + "path": "/g3/18e00c64a1d16900-3939-e3/slot-1", + "received_ns": 30028917 + }, + { + "path": "/g3/18e00c64a1d16900-3939-e3/slot-0", + "received_ns": 30045584 + }, + { + "path": "/g3/18e00c64a1d16900-3939-e3/end", + "received_ns": 123511750 + } + ], + "status": "pass" + }, + { + "adapter": "spin", + "case": "baseline-batch", + "client": { + "body": "{\"adapter\":\"spin\",\"case\":\"baseline-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":103102333,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":2,\"token\":\"18e00c64b6996310-3939-e4\"}", + "complete_ns": 444776542, + "event": "client_completed", + "first_byte_ns": 444765542, + "header_ns": 444762958, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:38 GMT"] + ], + "status": 200, + "token": "18e00c64b6996310-3939-e4" + }, + "event": "outbound-progress", + "probe": { + "adapter": "spin", + "case": "baseline-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 103102333, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 2, + "token": "18e00c64b6996310-3939-e4" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c64b6996310-3939-e4/start", + "received_ns": 12214208 + }, + { + "path": "/g3/18e00c64b6996310-3939-e4/end", + "received_ns": 126580958 + }, + { + "path": "/g3/18e00c64b6996310-3939-e4/slot-0", + "received_ns": 136694041 + }, + { + "path": "/g3/18e00c64b6996310-3939-e4/slot-1", + "received_ns": 136709833 + } + ], + "status": "pass" + }, + { + "adapter": "spin", + "case": "response-owned", + "error": "response ended before its status: UnexpectedEof", + "event": "outbound-progress-failure", + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c64d1fcf018-3939-e5/start", + "received_ns": 13549500 + } + ], + "status": "fail" + }, + { + "adapter": "spin", + "client": { + "body": "{\"adapter\":\"spin\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"end_stream\":false,\"raw_framing_evidence\":false,\"size_hint_lower\":0,\"size_hint_upper\":null,\"source\":\"spin-sdk-after-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 716375, + "event": "client_completed", + "first_byte_ns": 706250, + "header_ns": 704000, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:38 GMT"] + ], + "status": 200, + "token": "18e00c64d39dabb0-3939-e6" + }, + "event": "inbound-empty-signal", + "framing": "Absent", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": null, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "spin", + "client": { + "body": "{\"adapter\":\"spin\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"end_stream\":false,\"raw_framing_evidence\":false,\"size_hint_lower\":0,\"size_hint_upper\":0,\"source\":\"spin-sdk-after-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 401667, + "event": "client_completed", + "first_byte_ns": 396500, + "header_ns": 395709, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:38 GMT"] + ], + "status": 200, + "token": "18e00c64d3b7bf78-3939-e7" + }, + "event": "inbound-empty-signal", + "framing": "LengthZero", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": 0, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "spin", + "client": { + "body": "{\"adapter\":\"spin\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"end_stream\":false,\"raw_framing_evidence\":false,\"size_hint_lower\":0,\"size_hint_upper\":null,\"source\":\"spin-sdk-after-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 323167, + "event": "client_completed", + "first_byte_ns": 319333, + "header_ns": 318750, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:38 GMT"] + ], + "status": 200, + "token": "18e00c64d3cb8210-3939-e8" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedEmpty", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": null, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "spin", + "client": { + "body": "{\"adapter\":\"spin\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"end_stream\":false,\"raw_framing_evidence\":false,\"size_hint_lower\":0,\"size_hint_upper\":4,\"source\":\"spin-sdk-after-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 320042, + "event": "client_completed", + "first_byte_ns": 316333, + "header_ns": 315750, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:38 GMT"] + ], + "status": 200, + "token": "18e00c64d3de6218-3939-e9" + }, + "event": "inbound-empty-signal", + "framing": "FixedBody", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": 4, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "spin", + "client": { + "body": "{\"adapter\":\"spin\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"end_stream\":false,\"raw_framing_evidence\":false,\"size_hint_lower\":0,\"size_hint_upper\":null,\"source\":\"spin-sdk-after-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 289667, + "event": "client_completed", + "first_byte_ns": 285875, + "header_ns": 285375, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:38 GMT"] + ], + "status": 200, + "token": "18e00c64d3f17100-3939-ea" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedBody", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": null, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "spin", + "attempted_empty_body_cases": 5, + "attempted_progress_cases": 5, + "empty_body_passes": 5, + "event": "progress-coverage", + "failures": ["response-owned"], + "limitations": [ + "HTTP/2 and HTTP/3 not exercised", + "hosted execution pending", + "response-owned tests handler/dispatch return, not host callback return", + "missing-context detection unproved except native absent-runtime control", + "no cancellation or G2 readiness qualification" + ], + "origin_requests": 15, + "progress_passes": 4, + "qualification": false + }, + { + "adapter": "spin", + "error": "SDK progress or empty-body characterization has failed cases; see recorded observations", + "phase": "outbound-progress", + "status": "fail", + "variant": "spin-outbound-progress" + }, + { + "adapter": "axum", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "cargo 1.98.1 (797e8a9bc 2026-08-05)" + }, + { + "adapter": "axum", + "artifact": { + "bytes": 8811552, + "fnv1a64": "caa2a097aeeafa87", + "path": "/private/tmp/edgezero-g3-smoke-all/progress-axum/artifacts/fixture-axum-outbound-progress", + "purpose": "non-security build identity" + }, + "event": "build", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "variant": "axum-outbound-progress" + }, + { + "adapter": "axum", + "case": "eager-single", + "client": { + "body": "{\"adapter\":\"axum\",\"case\":\"eager-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":101842583,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e00c665963b2e8-3939-f5\"}", + "complete_ns": 326355250, + "event": "client_completed", + "first_byte_ns": 326327250, + "header_ns": 326326000, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "281"], + ["date", "Mon, 19 Oct 2026 21:44:45 GMT"] + ], + "status": 200, + "token": "18e00c665963b2e8-3939-f5" + }, + "event": "outbound-progress", + "probe": { + "adapter": "axum", + "case": "eager-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 101842583, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e00c665963b2e8-3939-f5" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c665963b2e8-3939-f5/start", + "received_ns": 14996083 + }, + { + "path": "/g3/18e00c665963b2e8-3939-f5/slot-0", + "received_ns": 25202541 + }, + { + "path": "/g3/18e00c665963b2e8-3939-f5/end", + "received_ns": 128602625 + } + ], + "status": "pass" + }, + { + "adapter": "axum", + "case": "lazy-single", + "client": { + "body": "{\"adapter\":\"axum\",\"case\":\"lazy-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":101173875,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e00c666dc647f0-3939-f6\"}", + "complete_ns": 440952750, + "event": "client_completed", + "first_byte_ns": 440941500, + "header_ns": 440939208, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "280"], + ["date", "Mon, 19 Oct 2026 21:44:46 GMT"] + ], + "status": 200, + "token": "18e00c666dc647f0-3939-f6" + }, + "event": "outbound-progress", + "probe": { + "adapter": "axum", + "case": "lazy-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 101173875, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e00c666dc647f0-3939-f6" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c666dc647f0-3939-f6/start", + "received_ns": 12590542 + }, + { + "path": "/g3/18e00c666dc647f0-3939-f6/end", + "received_ns": 119583250 + }, + { + "path": "/g3/18e00c666dc647f0-3939-f6/slot-0", + "received_ns": 134637167 + } + ], + "status": "pass" + }, + { + "adapter": "axum", + "case": "eager-batch", + "client": { + "body": "{\"adapter\":\"axum\",\"case\":\"eager-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":103231959,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e00c668842f380-3939-f7\"}", + "complete_ns": 334869708, + "event": "client_completed", + "first_byte_ns": 334859291, + "header_ns": 334858208, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "282"], + ["date", "Mon, 19 Oct 2026 21:44:46 GMT"] + ], + "status": 200, + "token": "18e00c668842f380-3939-f7" + }, + "event": "outbound-progress", + "probe": { + "adapter": "axum", + "case": "eager-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 103231959, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0, 0], + "slots": 2, + "token": "18e00c668842f380-3939-f7" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c668842f380-3939-f7/start", + "received_ns": 10123833 + }, + { + "path": "/g3/18e00c668842f380-3939-f7/slot-1", + "received_ns": 24620833 + }, + { + "path": "/g3/18e00c668842f380-3939-f7/slot-0", + "received_ns": 24627083 + }, + { + "path": "/g3/18e00c668842f380-3939-f7/end", + "received_ns": 124412500 + } + ], + "status": "pass" + }, + { + "adapter": "axum", + "case": "baseline-batch", + "client": { + "body": "{\"adapter\":\"axum\",\"case\":\"baseline-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":103038792,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":2,\"token\":\"18e00c669d20b7b0-3939-f8\"}", + "complete_ns": 443930333, + "event": "client_completed", + "first_byte_ns": 443919792, + "header_ns": 443918792, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "283"], + ["date", "Mon, 19 Oct 2026 21:44:46 GMT"] + ], + "status": 200, + "token": "18e00c669d20b7b0-3939-f8" + }, + "event": "outbound-progress", + "probe": { + "adapter": "axum", + "case": "baseline-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 103038792, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 2, + "token": "18e00c669d20b7b0-3939-f8" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c669d20b7b0-3939-f8/start", + "received_ns": 15108792 + }, + { + "path": "/g3/18e00c669d20b7b0-3939-f8/end", + "received_ns": 125239875 + }, + { + "path": "/g3/18e00c669d20b7b0-3939-f8/slot-0", + "received_ns": 138175833 + }, + { + "path": "/g3/18e00c669d20b7b0-3939-f8/slot-1", + "received_ns": 138186125 + } + ], + "status": "pass" + }, + { + "adapter": "axum", + "case": "response-owned", + "client": { + "body": " {\"adapter\":\"axum\",\"case\":\"response-owned\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":true,\"pause_ns\":102632417,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e00c66b80db140-3939-f9\"}", + "complete_ns": 330490000, + "event": "client_completed", + "first_byte_ns": 14148709, + "header_ns": 14146834, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Mon, 19 Oct 2026 21:44:47 GMT"] + ], + "status": 200, + "token": "18e00c66b80db140-3939-f9" + }, + "event": "outbound-progress", + "probe": { + "adapter": "axum", + "case": "response-owned", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": true, + "pause_ns": 102632417, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0, 0], + "slots": 2, + "token": "18e00c66b80db140-3939-f9" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e00c66b80db140-3939-f9/start", + "received_ns": 14004625 + }, + { + "path": "/g3/18e00c66b80db140-3939-f9/slot-1", + "received_ns": 27883000 + }, + { + "path": "/g3/18e00c66b80db140-3939-f9/slot-0", + "received_ns": 27897875 + }, + { + "path": "/g3/18e00c66b80db140-3939-f9/end", + "received_ns": 119965833 + } + ], + "status": "pass" + }, + { + "adapter": "axum", + "client": { + "body": "{\"adapter\":\"axum\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"known_empty\":null,\"source\":\"not-exposed-by-native-entrypoint\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 285791, + "event": "client_completed", + "first_byte_ns": 269500, + "header_ns": 268833, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "193"], + ["date", "Mon, 19 Oct 2026 21:44:47 GMT"] + ], + "status": 200, + "token": "18e00c66cca1a6e8-3939-fa" + }, + "event": "inbound-empty-signal", + "framing": "Absent", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "axum", + "client": { + "body": "{\"adapter\":\"axum\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"known_empty\":null,\"source\":\"not-exposed-by-native-entrypoint\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 187292, + "event": "client_completed", + "first_byte_ns": 182334, + "header_ns": 181709, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "193"], + ["date", "Mon, 19 Oct 2026 21:44:47 GMT"] + ], + "status": 200, + "token": "18e00c66ccbba728-3939-fb" + }, + "event": "inbound-empty-signal", + "framing": "LengthZero", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "axum", + "client": { + "body": "{\"adapter\":\"axum\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"known_empty\":null,\"source\":\"not-exposed-by-native-entrypoint\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 163958, + "event": "client_completed", + "first_byte_ns": 159166, + "header_ns": 158666, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "193"], + ["date", "Mon, 19 Oct 2026 21:44:47 GMT"] + ], + "status": 200, + "token": "18e00c66cccba100-3939-fc" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedEmpty", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "axum", + "client": { + "body": "{\"adapter\":\"axum\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"known_empty\":null,\"source\":\"not-exposed-by-native-entrypoint\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 182375, + "event": "client_completed", + "first_byte_ns": 177042, + "header_ns": 176417, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "193"], + ["date", "Mon, 19 Oct 2026 21:44:47 GMT"] + ], + "status": 200, + "token": "18e00c66cce2d668-3939-fd" + }, + "event": "inbound-empty-signal", + "framing": "FixedBody", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "axum", + "client": { + "body": "{\"adapter\":\"axum\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"known_empty\":null,\"source\":\"not-exposed-by-native-entrypoint\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 176458, + "event": "client_completed", + "first_byte_ns": 162292, + "header_ns": 161917, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "193"], + ["date", "Mon, 19 Oct 2026 21:44:47 GMT"] + ], + "status": 200, + "token": "18e00c66ccf63b40-3939-fe" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedBody", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "axum", + "attempted_empty_body_cases": 5, + "attempted_progress_cases": 5, + "empty_body_passes": 5, + "event": "progress-coverage", + "failures": [], + "limitations": [ + "HTTP/2 and HTTP/3 not exercised", + "hosted execution pending", + "response-owned tests handler/dispatch return, not host callback return", + "missing-context detection unproved except native absent-runtime control", + "no cancellation or G2 readiness qualification" + ], + "origin_requests": 18, + "progress_passes": 5, + "qualification": false + } + ], + "runtime_logs": { + "fastly": "2026-10-19T21:44:29.037540Z WARN no backend definitions found in /private/tmp/edgezero-g3-smoke-all/progress-fastly/fastly.toml\n", + "spin": "Using runtime config [key_value_store.fixture_config: spin], [key_value_store.fixture_kv: spin] from \"/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/crates/fixture-spin/runtime-config.toml\"\nLogging component stdio to \"/private/tmp/edgezero-g3-smoke-all/progress-spin/.spin/logs/\"\n\nServing http://127.0.0.1:60671\nAvailable Routes:\n fixture: http://127.0.0.1:60671 (wildcard)\n2026-10-19T21:44:38.866985Z WARN spin_trigger_http::server: Error serving HTTP connection: hyper::Error(User(Body), InternalError(Some(\"body writer dropped\")))\n2026-10-19T21:44:38.880004Z WARN spin_factor_outbound_http::wasi: hyper request error: hyper_util::client::legacy::Error(SendRequest, hyper::Error(Io, Os { code: 54, kind: ConnectionReset, message: \"Connection reset by peer\" }))\n2026-10-19T21:44:38.880348Z WARN spin_factor_outbound_http::wasi: hyper request error: hyper_util::client::legacy::Error(SendRequest, hyper::Error(Io, Os { code: 54, kind: ConnectionReset, message: \"Connection reset by peer\" }))\nworker failed: worker panicked\n", + "axum": "2026-10-19T21:44:45.408Z INFO [edgezero_adapter_axum::dev_server] [edgezero] axum HTTP/1 ingress limits: AxumIngressConfig { head_read_timeout: 10s, max_connections: 256, max_header_count: 100, max_raw_head_bytes: 65536 }\n2026-10-19T21:44:45.408Z INFO [edgezero_adapter_axum::dev_server] [edgezero] axum config snapshot limits: ConfigStoreLimits { entries: 1024, file_bytes: 16777216, key_bytes: 1024, resident_bytes: 33554432, startup_bytes: 117440512, value_bytes: 8388608 }\n2026-10-19T21:44:45.408Z INFO [edgezero_adapter_axum::dev_server] [edgezero] starting axum server on http://127.0.0.1:60956\n" + }, + "unproved": [ + "Cloudflare runtime blocked by wasm-bindgen CLI mismatch", + "Spin response-owned case failed; exact origin of worker panic unproved", + "HTTP/2 and HTTP/3", + "hosted behavior", + "active request-context rejection on provider schedulers", + "native LocalSet presence/liveness detection", + "native unpolled-job cancellation regression", + "new public begin API integration", + "G2 readiness qualification", + "G4 approval" + ] +} diff --git a/docs/audits/evidence/2026-10-19-spin-response-egress.json b/docs/audits/evidence/2026-10-19-spin-response-egress.json new file mode 100644 index 000000000..17edca644 --- /dev/null +++ b/docs/audits/evidence/2026-10-19-spin-response-egress.json @@ -0,0 +1,1829 @@ +{ + "schema": 1, + "recorded_date": "2026-10-19 America/Los_Angeles", + "base_revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "command": "scripts/smoke_test_reusable_app.sh --adapter all --suite smoke", + "exit_code": 2, + "tool_overrides": { + "SPIN_BIN": "/opt/homebrew/bin/spin", + "WRANGLER_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler", + "WORKER_BUILD_BIN": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build" + }, + "sdk": { + "fastly": "0.13.1", + "worker": "0.8.5", + "spin_sdk": "7.0.0", + "wasip3": "0.7.1+wasi-0.3.0", + "reqwest": "0.13.4" + }, + "source_sha256": { + "tests/fixtures/reusable-app/crates/fixture-axum/Cargo.toml": "1c1421c7497a7aff305d31ecd4c3ee323eb646acd4d56f23cab765ab6a5e377c", + "tests/fixtures/reusable-app/crates/fixture-axum/src/bin/outbound_progress.rs": "35594150be188ed8598ea1aea6e291f9b00c6aeb558bc4a65de8ac0f9922b69f", + "tests/fixtures/reusable-app/crates/fixture-axum/src/main.rs": "43cbebe047abdf77b4d48f55fd4098eb9c57ac17bedd9e6621335104f1710474", + "tests/fixtures/reusable-app/crates/fixture-axum/src/progress.rs": "0c41390031d857f0589667b88bd27fc8e185b483cedc57ec2ee73c99d36a9e4a", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/Cargo.toml": "96501fc975013ba68411cd6e0f77b6b0fe505dfe4ed8cef16620ef19b4c9784d", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs": "606245131f0c594154ca0a2eb63e5341999325bfb2de55887ab3264fa4b89559", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs": "305f70039af59501639916bd8cfb8db696613c73af670e953dd329a3eed14498", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml": "6c2a097879b98f4d4af3e2fea139215abc9fa0bfdc058500f28fd9f7d757aab3", + "tests/fixtures/reusable-app/crates/fixture-core/Cargo.toml": "f9c68c281c9b57269ee8065f46e9e1c3fcc9c58a562259e480f2125877a8f6c8", + "tests/fixtures/reusable-app/crates/fixture-core/src/lib.rs": "72cd88d5d7d274fa52a4af249a6723e7b842680f08049961cd85d94ed77c2375", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs": "c186ffe225ec39483328988e66cb2cb7b10821b622a98bdc4e37a9a4ada627b9", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs": "321cda3626dbe5e68da4ea1e9b5f71f531c0630f2763196fff8b0e7b64f301db", + "tests/fixtures/reusable-app/crates/fixture-fastly/Cargo.toml": "e82ea4713f86db533796367cee0c3f91191f33e7e1f0510d0cf1bdafd509a7e1", + "tests/fixtures/reusable-app/crates/fixture-fastly/fastly.toml": "4444af855e1f5db54da213ddfc6a84ea34aa52f9fd868d047521b0ab78a3fd18", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/custom_rebuild_per_request.rs": "bb55c45b46bf7afa90546982f90093f1bd5dd10a1f2324c985aba49a64650ea3", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/custom_retained_app.rs": "229b248e9b52a0b433d17e64706569fcb3285818e9975337291107c4b2219d2b", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/custom_single_request.rs": "aeeeb95a1e0a994ea6c62b62977d45e13dc9cce45b6a83fa9135584ab63edff9", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/faults.rs": "c4771da94ddb7ca45e466f4872f1fb5ee9d49ea699b42ac9f01fdf9eeef32112", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_lifetime.rs": "a71e6a90a3710d4a1651f2a4fb76bbe09fad89e9f4be57caa860b8d3cdfd7749", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_memory.rs": "f608680fad1b9eb0f2d6c7e46ab81934ec05d9f0336a92234b550b28ce1b5e97", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_requests.rs": "479f9c6f0787fb25e9a7f27e007ef5b9ee8ceada64b118688456dafd3b92e191", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/logger_negative.rs": "3474b8e2902a7cbddf84fdd98444af17f10898c16cae52588f82d64abe1a5227", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs": "0b4a81e3678459801b9f5a9379de70160452317929936d1b6c85393c1249c704", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs": "90b959abd5250a230c04409518f6416436db5485c0a174deb9a6a0a5e51bad4e", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/qualification.rs": "8a0d6c5d0d29882ec48829a76aa20b5000aa76f87f07265208bc8f75f4268f84", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/rebuild_per_request.rs": "19ec8d044ec2841309c0d6da6e91ffc08343653235312214cc00571386794b87", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/retained_app.rs": "629635c72fe281cda10cb86c75329df67aa3ba53e9d6d93ffe0019eda951d27d", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/single_request.rs": "408f4a4cbc84e85d0d20b4a1d40c8404c6adaa2c02a059da30d52bbb12e4d4ae", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs": "0d727361a788ff8b10aa5d48b80b43bd43411a53ac32fbfb3fb605f9d6bc4768", + "tests/fixtures/reusable-app/crates/fixture-harness/Cargo.toml": "93580bf7575878ce3cb5d82ef3e7aea6d5c26baae6f467f469339858992fd141", + "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs": "22f5c8ff413357b5684910249c635a4993d0cb912b3cc584c8f14cc2258efb18", + "tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs": "1344e6fb8e4e22426ec5ceaacba210c034121970d8b49ece79e6ef3a871776b8", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs": "a4181724068841ef44f112a107566180b58c3a4618921c14eca3ceb823846a05", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/progress.rs": "6f257eb1816d2c693cfb3744409e3c75a5f17ef7a8623df72a7c70753cf42cd5", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs": "f07fd19c3937780e5a7973513ccd29dd4708c852f62f4b2dd3e14929181605f9", + "tests/fixtures/reusable-app/crates/fixture-spin/Cargo.toml": "7256c3384d5e3c675b8b9e7026d10630bc5fec9c3aaf3900bc740fcb4e1526b4", + "tests/fixtures/reusable-app/crates/fixture-spin/runtime-config.toml": "716d293f350f0e1cb00e4b20b3d428db1cb06f6fe6e0590815147839368916b8", + "tests/fixtures/reusable-app/crates/fixture-spin/spin.toml": "dfed653d283de300aea36feb4841ee731e20643dd60df83dc0907047fdb78d9c", + "tests/fixtures/reusable-app/crates/fixture-spin/src/lib.rs": "a08d5b6c4bfbfe067ffafb8d4996c67a1105c104676f3d9aed5ff66831ac794a", + "tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs": "58fec2e1968f40d38a40a8e38642d27be22d4bf520bb29fd8d58fffa688f2414", + "tests/fixtures/reusable-app/Cargo.toml": "7665200757e4e565d11fa7eb036215aa583d21fd543bca39fdbdce470fee11ae", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "scripts/smoke_test_reusable_app.sh": "5ffedd5e84ae8f8f1e8f30e0ba4b7b4032ffdea48776844619f1e354197a7c1b", + "crates/edgezero-adapter-spin/src/response.rs": "03e8b98de3e4854810b4bdabba7783d3a66cacb5e91402e941609d7dfe567075" + }, + "compiled_artifacts": [ + { + "adapter": "fastly", + "path": "/private/tmp/edgezero-g3-smoke-reviewed/progress-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "sha256": "55adc3435a3669bfd51f8ca71d59438a506c7bbf26d6c1f5cba58c1df2db3c4d" + }, + { + "adapter": "spin", + "path": "/private/tmp/edgezero-g3-smoke-reviewed/progress-spin/artifacts/fixture_spin.wasm", + "sha256": "a64232085f08c12227867bda98bc1b3e675dfbe3d4c0b47654e7f214ed782c1d" + }, + { + "adapter": "axum", + "path": "/private/tmp/edgezero-g3-smoke-reviewed/progress-axum/artifacts/fixture-axum-outbound-progress", + "sha256": "177e42aa127c58093650ed09c34c721a7620535306d75d00d8b3905b9d29cc59" + } + ], + "retained_events_sha256": "7b3dac8381bc22d935666ade32d98aed1fa4b336f4786017e94a900c0522b41c", + "original_smoke_jsonl_sha256": "3530ea6e0373b19d2bd7706dc8244f13a9ebf9223b8a5c8187e2a2984c7c3eb9", + "events": [ + { + "adapter": "fastly", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "viceroy 0.21.0" + }, + { + "adapter": "fastly", + "artifact": { + "bytes": 2102618, + "fnv1a64": "37f3f1d7020b98cd", + "path": "/private/tmp/edgezero-g3-smoke-reviewed/progress-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "purpose": "non-security build identity" + }, + "event": "build", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "variant": "fastly-outbound-progress" + }, + { + "adapter": "fastly", + "case": "eager-single", + "client": { + "body": "{\"adapter\":\"fastly\",\"case\":\"eager-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":101204500,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e01eabad7fa510-4020-b7\"}", + "complete_ns": 325023500, + "event": "client_completed", + "first_byte_ns": 325012166, + "header_ns": 325009708, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:34 GMT"] + ], + "status": 200, + "token": "18e01eabad7fa510-4020-b7" + }, + "event": "outbound-progress", + "probe": { + "adapter": "fastly", + "case": "eager-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 101204500, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e01eabad7fa510-4020-b7" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01eabad7fa510-4020-b7/start", + "received_ns": 10122042 + }, + { + "path": "/g3/18e01eabad7fa510-4020-b7/slot-0", + "received_ns": 20145917 + }, + { + "path": "/g3/18e01eabad7fa510-4020-b7/end", + "received_ns": 121688208 + } + ], + "status": "pass" + }, + { + "adapter": "fastly", + "case": "lazy-single", + "client": { + "body": "{\"adapter\":\"fastly\",\"case\":\"lazy-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":102224750,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e01eabc1a01868-4020-b8\"}", + "complete_ns": 444747125, + "event": "client_completed", + "first_byte_ns": 444735708, + "header_ns": 444733208, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:35 GMT"] + ], + "status": 200, + "token": "18e01eabc1a01868-4020-b8" + }, + "event": "outbound-progress", + "probe": { + "adapter": "fastly", + "case": "lazy-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 102224750, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e01eabc1a01868-4020-b8" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01eabc1a01868-4020-b8/start", + "received_ns": 12031042 + }, + { + "path": "/g3/18e01eabc1a01868-4020-b8/end", + "received_ns": 123043417 + }, + { + "path": "/g3/18e01eabc1a01868-4020-b8/slot-0", + "received_ns": 137460542 + } + ], + "status": "pass" + }, + { + "adapter": "fastly", + "case": "eager-batch", + "client": { + "body": "{\"adapter\":\"fastly\",\"case\":\"eager-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":100725958,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e01eabdc652350-4020-b9\"}", + "complete_ns": 338682709, + "event": "client_completed", + "first_byte_ns": 338671750, + "header_ns": 338669250, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:34 GMT"] + ], + "status": 200, + "token": "18e01eabdc652350-4020-b9" + }, + "event": "outbound-progress", + "probe": { + "adapter": "fastly", + "case": "eager-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 100725958, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0, 0], + "slots": 2, + "token": "18e01eabdc652350-4020-b9" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01eabdc652350-4020-b9/start", + "received_ns": 11074750 + }, + { + "path": "/g3/18e01eabdc652350-4020-b9/slot-0", + "received_ns": 25963333 + }, + { + "path": "/g3/18e01eabdc652350-4020-b9/slot-1", + "received_ns": 25974708 + }, + { + "path": "/g3/18e01eabdc652350-4020-b9/end", + "received_ns": 123449125 + } + ], + "status": "pass" + }, + { + "adapter": "fastly", + "case": "baseline-batch", + "client": { + "body": "{\"adapter\":\"fastly\",\"case\":\"baseline-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":103221292,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":2,\"token\":\"18e01eabf14abf50-4020-ba\"}", + "complete_ns": 329571959, + "event": "client_completed", + "first_byte_ns": 329557375, + "header_ns": 329554875, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:35 GMT"] + ], + "status": 200, + "token": "18e01eabf14abf50-4020-ba" + }, + "event": "outbound-progress", + "probe": { + "adapter": "fastly", + "case": "baseline-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 103221292, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 2, + "token": "18e01eabf14abf50-4020-ba" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01eabf14abf50-4020-ba/start", + "received_ns": 13548334 + }, + { + "path": "/g3/18e01eabf14abf50-4020-ba/slot-1", + "received_ns": 23572667 + }, + { + "path": "/g3/18e01eabf14abf50-4020-ba/slot-0", + "received_ns": 23579000 + }, + { + "path": "/g3/18e01eabf14abf50-4020-ba/end", + "received_ns": 131931750 + } + ], + "status": "pass" + }, + { + "adapter": "fastly", + "case": "response-owned", + "client": { + "body": " {\"adapter\":\"fastly\",\"case\":\"response-owned\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":true,\"pause_ns\":101245250,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e01eac055a13d8-4020-bb\"}", + "complete_ns": 340075750, + "event": "client_completed", + "first_byte_ns": 18052541, + "header_ns": 18050083, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:35 GMT"] + ], + "status": 200, + "token": "18e01eac055a13d8-4020-bb" + }, + "event": "outbound-progress", + "probe": { + "adapter": "fastly", + "case": "response-owned", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": true, + "pause_ns": 101245250, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0, 0], + "slots": 2, + "token": "18e01eac055a13d8-4020-bb" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01eac055a13d8-4020-bb/start", + "received_ns": 15133792 + }, + { + "path": "/g3/18e01eac055a13d8-4020-bb/slot-0", + "received_ns": 28608000 + }, + { + "path": "/g3/18e01eac055a13d8-4020-bb/slot-1", + "received_ns": 28619125 + }, + { + "path": "/g3/18e01eac055a13d8-4020-bb/end", + "received_ns": 128774167 + } + ], + "status": "pass" + }, + { + "adapter": "fastly", + "client": { + "body": "{\"adapter\":\"fastly\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"has_body\":true,\"known_length\":0,\"source\":\"fastly-sdk-before-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 678541, + "event": "client_completed", + "first_byte_ns": 659583, + "header_ns": 658041, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:36 GMT"] + ], + "status": 200, + "token": "18e01eac1a02be48-4020-bc" + }, + "event": "inbound-empty-signal", + "framing": "Absent", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": 0, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "fastly", + "client": { + "body": "{\"adapter\":\"fastly\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"has_body\":true,\"known_length\":0,\"source\":\"fastly-sdk-before-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 484084, + "event": "client_completed", + "first_byte_ns": 476292, + "header_ns": 474584, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:36 GMT"] + ], + "status": 200, + "token": "18e01eac1a16e288-4020-bd" + }, + "event": "inbound-empty-signal", + "framing": "LengthZero", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": 0, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "fastly", + "client": { + "body": "{\"adapter\":\"fastly\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"has_body\":true,\"known_length\":null,\"source\":\"fastly-sdk-before-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 489958, + "event": "client_completed", + "first_byte_ns": 485292, + "header_ns": 484542, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:35 GMT"] + ], + "status": 200, + "token": "18e01eac1a275190-4020-be" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedEmpty", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": null, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "fastly", + "client": { + "body": "{\"adapter\":\"fastly\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"has_body\":true,\"known_length\":4,\"source\":\"fastly-sdk-before-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 456083, + "event": "client_completed", + "first_byte_ns": 449458, + "header_ns": 447916, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:35 GMT"] + ], + "status": 200, + "token": "18e01eac1a374780-4020-bf" + }, + "event": "inbound-empty-signal", + "framing": "FixedBody", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": 4, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "fastly", + "client": { + "body": "{\"adapter\":\"fastly\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"has_body\":true,\"known_length\":null,\"source\":\"fastly-sdk-before-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 340000, + "event": "client_completed", + "first_byte_ns": 334917, + "header_ns": 334209, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:36 GMT"] + ], + "status": 200, + "token": "18e01eac1a47ed38-4020-c0" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedBody", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": null, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "fastly", + "attempted_empty_body_cases": 5, + "attempted_progress_cases": 5, + "empty_body_passes": 5, + "event": "progress-coverage", + "failures": [], + "limitations": [ + "HTTP/2 and HTTP/3 not exercised", + "hosted execution pending", + "response-owned tests handler/dispatch return, not host callback return", + "missing-context detection unproved except native absent-runtime control", + "no cancellation or G2 readiness qualification" + ], + "origin_requests": 18, + "progress_passes": 5, + "qualification": false + }, + { + "adapter": "cloudflare", + "observed": "wasm-bindgen 0.2.121", + "phase": "build-tooling", + "qualification": false, + "reason": "matching wasm-bindgen CLI unavailable; no download or dependency repin attempted", + "required_wasm_bindgen": "0.2.129", + "status": "unsupported" + }, + { + "adapter": "spin", + "event": "progress-runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + }, + { + "adapter": "spin", + "artifact": { + "bytes": 2252246, + "fnv1a64": "40d6c398386ba6a9", + "path": "/private/tmp/edgezero-g3-smoke-reviewed/progress-spin/artifacts/fixture_spin.wasm", + "purpose": "non-security build identity" + }, + "event": "build", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "variant": "spin-outbound-progress" + }, + { + "adapter": "spin", + "case": "response-owned-ready", + "event": "response-stream-control", + "observation": { + "client": { + "body": " {\"adapter\":\"spin\",\"case\":\"response-owned-ready\",\"dispatch_returned\":true,\"dispatch_returned_at_first_poll\":true,\"handler_returned\":true,\"handler_returned_at_first_poll\":true,\"pause_ns\":458,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"token\":\"18e01ead1a4a5d10-4020-e1\"}", + "complete_ns": 2583125, + "event": "client_completed", + "first_byte_ns": 2568000, + "header_ns": 2561792, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:40 GMT"] + ], + "status": 200, + "token": "18e01ead1a4a5d10-4020-e1" + }, + "probe": { + "adapter": "spin", + "case": "response-owned-ready", + "dispatch_returned": true, + "dispatch_returned_at_first_poll": true, + "handler_returned": true, + "handler_returned_at_first_poll": true, + "pause_ns": 458, + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "token": "18e01ead1a4a5d10-4020-e1" + } + }, + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "response-owned-timer-only", + "event": "response-stream-control", + "observation": { + "client": { + "body": " {\"adapter\":\"spin\",\"case\":\"response-owned-timer-only\",\"dispatch_returned\":true,\"dispatch_returned_at_first_poll\":true,\"handler_returned\":true,\"handler_returned_at_first_poll\":true,\"pause_ns\":103036834,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"token\":\"18e01ead1a7bd908-4020-e2\"}", + "complete_ns": 103701584, + "event": "client_completed", + "first_byte_ns": 568709, + "header_ns": 567250, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:40 GMT"] + ], + "status": 200, + "token": "18e01ead1a7bd908-4020-e2" + }, + "probe": { + "adapter": "spin", + "case": "response-owned-timer-only", + "dispatch_returned": true, + "dispatch_returned_at_first_poll": true, + "handler_returned": true, + "handler_returned_at_first_poll": true, + "pause_ns": 103036834, + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "token": "18e01ead1a7bd908-4020-e2" + } + }, + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "sdk-stream-ready", + "event": "response-stream-control", + "observation": { + "client": { + "body": " {\"adapter\":\"spin\",\"case\":\"sdk-stream-ready\",\"pause_ns\":708,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sdk_native\":true,\"token\":\"18e01ead20b60a50-4020-e3\"}", + "complete_ns": 521875, + "event": "client_completed", + "first_byte_ns": 474625, + "header_ns": 473208, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:40 GMT"] + ], + "status": 200, + "token": "18e01ead20b60a50-4020-e3" + }, + "probe": { + "adapter": "spin", + "case": "sdk-stream-ready", + "pause_ns": 708, + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sdk_native": true, + "token": "18e01ead20b60a50-4020-e3" + } + }, + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "sdk-stream-timer-only", + "event": "response-stream-control", + "observation": { + "client": { + "body": " {\"adapter\":\"spin\",\"case\":\"sdk-stream-timer-only\",\"pause_ns\":103101000,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sdk_native\":true,\"token\":\"18e01ead20cdb4e8-4020-e4\"}", + "complete_ns": 103506000, + "event": "client_completed", + "first_byte_ns": 321833, + "header_ns": 320791, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:40 GMT"] + ], + "status": 200, + "token": "18e01ead20cdb4e8-4020-e4" + }, + "probe": { + "adapter": "spin", + "case": "sdk-stream-timer-only", + "pause_ns": 103101000, + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sdk_native": true, + "token": "18e01ead20cdb4e8-4020-e4" + } + }, + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "attempted": 4, + "event": "response-stream-control-coverage", + "failures": [], + "passes": 4, + "qualification": false + }, + { + "adapter": "spin", + "case": "eager-single", + "client": { + "body": "{\"adapter\":\"spin\",\"case\":\"eager-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":101976875,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e01ead27058ca0-4020-e5\"}", + "complete_ns": 340028917, + "event": "client_completed", + "first_byte_ns": 340016750, + "header_ns": 340014584, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:41 GMT"] + ], + "status": 200, + "token": "18e01ead27058ca0-4020-e5" + }, + "event": "outbound-progress", + "probe": { + "adapter": "spin", + "case": "eager-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 101976875, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e01ead27058ca0-4020-e5" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01ead27058ca0-4020-e5/start", + "received_ns": 14436333 + }, + { + "path": "/g3/18e01ead27058ca0-4020-e5/slot-0", + "received_ns": 29487875 + }, + { + "path": "/g3/18e01ead27058ca0-4020-e5/end", + "received_ns": 131721291 + } + ], + "status": "pass" + }, + { + "adapter": "spin", + "case": "lazy-single", + "client": { + "body": "{\"adapter\":\"spin\",\"case\":\"lazy-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":103130542,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e01ead3c291fe8-4020-e6\"}", + "complete_ns": 443248792, + "event": "client_completed", + "first_byte_ns": 443231625, + "header_ns": 443227917, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:41 GMT"] + ], + "status": 200, + "token": "18e01ead3c291fe8-4020-e6" + }, + "event": "outbound-progress", + "probe": { + "adapter": "spin", + "case": "lazy-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 103130542, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e01ead3c291fe8-4020-e6" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01ead3c291fe8-4020-e6/start", + "received_ns": 15117333 + }, + { + "path": "/g3/18e01ead3c291fe8-4020-e6/end", + "received_ns": 127862917 + }, + { + "path": "/g3/18e01ead3c291fe8-4020-e6/slot-0", + "received_ns": 140740875 + } + ], + "status": "pass" + }, + { + "adapter": "spin", + "case": "eager-batch", + "client": { + "body": "{\"adapter\":\"spin\",\"case\":\"eager-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":103500208,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e01ead56dca670-4020-e7\"}", + "complete_ns": 328527958, + "event": "client_completed", + "first_byte_ns": 328517208, + "header_ns": 328515000, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:41 GMT"] + ], + "status": 200, + "token": "18e01ead56dca670-4020-e7" + }, + "event": "outbound-progress", + "probe": { + "adapter": "spin", + "case": "eager-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 103500208, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0, 0], + "slots": 2, + "token": "18e01ead56dca670-4020-e7" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01ead56dca670-4020-e7/start", + "received_ns": 15305375 + }, + { + "path": "/g3/18e01ead56dca670-4020-e7/slot-0", + "received_ns": 27055416 + }, + { + "path": "/g3/18e01ead56dca670-4020-e7/slot-1", + "received_ns": 27078333 + }, + { + "path": "/g3/18e01ead56dca670-4020-e7/end", + "received_ns": 126359666 + } + ], + "status": "pass" + }, + { + "adapter": "spin", + "case": "baseline-batch", + "client": { + "body": "{\"adapter\":\"spin\",\"case\":\"baseline-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":103313083,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":2,\"token\":\"18e01ead6acbb158-4020-e8\"}", + "complete_ns": 443338792, + "event": "client_completed", + "first_byte_ns": 443327792, + "header_ns": 443325084, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:42 GMT"] + ], + "status": 200, + "token": "18e01ead6acbb158-4020-e8" + }, + "event": "outbound-progress", + "probe": { + "adapter": "spin", + "case": "baseline-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 103313083, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 2, + "token": "18e01ead6acbb158-4020-e8" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01ead6acbb158-4020-e8/start", + "received_ns": 15133334 + }, + { + "path": "/g3/18e01ead6acbb158-4020-e8/end", + "received_ns": 123815667 + }, + { + "path": "/g3/18e01ead6acbb158-4020-e8/slot-1", + "received_ns": 136038084 + }, + { + "path": "/g3/18e01ead6acbb158-4020-e8/slot-0", + "received_ns": 136050459 + } + ], + "status": "pass" + }, + { + "adapter": "spin", + "case": "response-owned", + "client": { + "body": " {\"adapter\":\"spin\",\"case\":\"response-owned\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":true,\"handler_returned\":true,\"pause_ns\":102931250,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e01ead85ec3638-4020-e9\"}", + "complete_ns": 336475542, + "event": "client_completed", + "first_byte_ns": 15576750, + "header_ns": 15574625, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:42 GMT"] + ], + "status": 200, + "token": "18e01ead85ec3638-4020-e9" + }, + "event": "outbound-progress", + "probe": { + "adapter": "spin", + "case": "response-owned", + "consumer_polls_before_window_end": 0, + "dispatch_returned": true, + "handler_returned": true, + "pause_ns": 102931250, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0, 0], + "slots": 2, + "token": "18e01ead85ec3638-4020-e9" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01ead85ec3638-4020-e9/start", + "received_ns": 15093292 + }, + { + "path": "/g3/18e01ead85ec3638-4020-e9/slot-0", + "received_ns": 26636792 + }, + { + "path": "/g3/18e01ead85ec3638-4020-e9/slot-1", + "received_ns": 26643625 + }, + { + "path": "/g3/18e01ead85ec3638-4020-e9/end", + "received_ns": 127325625 + } + ], + "status": "pass" + }, + { + "adapter": "spin", + "client": { + "body": "{\"adapter\":\"spin\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"end_stream\":false,\"raw_framing_evidence\":false,\"size_hint_lower\":0,\"size_hint_upper\":null,\"source\":\"spin-sdk-after-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 699709, + "event": "client_completed", + "first_byte_ns": 691375, + "header_ns": 689875, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:42 GMT"] + ], + "status": 200, + "token": "18e01ead9ad2dbd8-4020-ea" + }, + "event": "inbound-empty-signal", + "framing": "Absent", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": null, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "spin", + "client": { + "body": "{\"adapter\":\"spin\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"end_stream\":false,\"raw_framing_evidence\":false,\"size_hint_lower\":0,\"size_hint_upper\":0,\"source\":\"spin-sdk-after-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 562583, + "event": "client_completed", + "first_byte_ns": 558333, + "header_ns": 557667, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:42 GMT"] + ], + "status": 200, + "token": "18e01ead9ae6ae10-4020-eb" + }, + "event": "inbound-empty-signal", + "framing": "LengthZero", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": 0, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "spin", + "client": { + "body": "{\"adapter\":\"spin\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"end_stream\":false,\"raw_framing_evidence\":false,\"size_hint_lower\":0,\"size_hint_upper\":null,\"source\":\"spin-sdk-after-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 423584, + "event": "client_completed", + "first_byte_ns": 418792, + "header_ns": 417959, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:42 GMT"] + ], + "status": 200, + "token": "18e01ead9af87ca8-4020-ec" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedEmpty", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": null, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "spin", + "client": { + "body": "{\"adapter\":\"spin\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"end_stream\":false,\"raw_framing_evidence\":false,\"size_hint_lower\":0,\"size_hint_upper\":4,\"source\":\"spin-sdk-after-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 525708, + "event": "client_completed", + "first_byte_ns": 517625, + "header_ns": 516125, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:42 GMT"] + ], + "status": 200, + "token": "18e01ead9b08a178-4020-ed" + }, + "event": "inbound-empty-signal", + "framing": "FixedBody", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": 4, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "spin", + "client": { + "body": "{\"adapter\":\"spin\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"end_stream\":false,\"raw_framing_evidence\":false,\"size_hint_lower\":0,\"size_hint_upper\":null,\"source\":\"spin-sdk-after-conversion\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 530875, + "event": "client_completed", + "first_byte_ns": 525916, + "header_ns": 524833, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:42 GMT"] + ], + "status": 200, + "token": "18e01ead9b1a0a80-4020-ee" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedBody", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": null, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "spin", + "attempted_empty_body_cases": 5, + "attempted_progress_cases": 5, + "empty_body_passes": 5, + "event": "progress-coverage", + "failures": [], + "limitations": [ + "HTTP/2 and HTTP/3 not exercised", + "hosted execution pending", + "response-owned tests handler/dispatch return, not host callback return", + "missing-context detection unproved except native absent-runtime control", + "no cancellation or G2 readiness qualification" + ], + "origin_requests": 18, + "progress_passes": 5, + "qualification": false + }, + { + "error": null, + "event": "spin-before-shutdown", + "qualification": false, + "sdk_terminal_evidence": "pass", + "worker_panic": false + }, + { + "event": "spin-shutdown-observation", + "qualification": false, + "worker_panic_after_controlled_shutdown": true, + "worker_panic_before_shutdown": false + }, + { + "adapter": "spin", + "case": "sdk-stream-ready", + "coordinator_ok": true, + "diagnostic_source": "fixture_stderr.txt", + "event": "sdk-stream-coordinator-terminal", + "qualification": false, + "token": "18e01ead20b60a50-4020-e3" + }, + { + "adapter": "spin", + "case": "sdk-stream-timer-only", + "coordinator_ok": true, + "diagnostic_source": "fixture_stderr.txt", + "event": "sdk-stream-coordinator-terminal", + "qualification": false, + "token": "18e01ead20cdb4e8-4020-e4" + }, + { + "adapter": "axum", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "cargo 1.98.1 (797e8a9bc 2026-08-05)" + }, + { + "adapter": "axum", + "artifact": { + "bytes": 8830832, + "fnv1a64": "82759be726f58616", + "path": "/private/tmp/edgezero-g3-smoke-reviewed/progress-axum/artifacts/fixture-axum-outbound-progress", + "purpose": "non-security build identity" + }, + "event": "build", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "variant": "axum-outbound-progress" + }, + { + "adapter": "axum", + "case": "eager-single", + "client": { + "body": "{\"adapter\":\"axum\",\"case\":\"eager-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":103148292,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e01eadfe9cf0b8-4020-f9\"}", + "complete_ns": 338707875, + "event": "client_completed", + "first_byte_ns": 338696708, + "header_ns": 338685667, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "281"], + ["date", "Tue, 20 Oct 2026 03:19:44 GMT"] + ], + "status": 200, + "token": "18e01eadfe9cf0b8-4020-f9" + }, + "event": "outbound-progress", + "probe": { + "adapter": "axum", + "case": "eager-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 103148292, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e01eadfe9cf0b8-4020-f9" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01eadfe9cf0b8-4020-f9/start", + "received_ns": 15085709 + }, + { + "path": "/g3/18e01eadfe9cf0b8-4020-f9/slot-0", + "received_ns": 30101250 + }, + { + "path": "/g3/18e01eadfe9cf0b8-4020-f9/end", + "received_ns": 126935584 + } + ], + "status": "pass" + }, + { + "adapter": "axum", + "case": "lazy-single", + "client": { + "body": "{\"adapter\":\"axum\",\"case\":\"lazy-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":101493625,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e01eae13597148-4020-fa\"}", + "complete_ns": 453004459, + "event": "client_completed", + "first_byte_ns": 452991459, + "header_ns": 452990500, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "280"], + ["date", "Tue, 20 Oct 2026 03:19:45 GMT"] + ], + "status": 200, + "token": "18e01eae13597148-4020-fa" + }, + "event": "outbound-progress", + "probe": { + "adapter": "axum", + "case": "lazy-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 101493625, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 1, + "token": "18e01eae13597148-4020-fa" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01eae13597148-4020-fa/start", + "received_ns": 14493917 + }, + { + "path": "/g3/18e01eae13597148-4020-fa/end", + "received_ns": 128970833 + }, + { + "path": "/g3/18e01eae13597148-4020-fa/slot-0", + "received_ns": 142713500 + } + ], + "status": "pass" + }, + { + "adapter": "axum", + "case": "eager-batch", + "client": { + "body": "{\"adapter\":\"axum\",\"case\":\"eager-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":102747416,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e01eae2e83aa88-4020-fb\"}", + "complete_ns": 333218625, + "event": "client_completed", + "first_byte_ns": 333205333, + "header_ns": 333204458, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "282"], + ["date", "Tue, 20 Oct 2026 03:19:45 GMT"] + ], + "status": 200, + "token": "18e01eae2e83aa88-4020-fb" + }, + "event": "outbound-progress", + "probe": { + "adapter": "axum", + "case": "eager-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 102747416, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0, 0], + "slots": 2, + "token": "18e01eae2e83aa88-4020-fb" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01eae2e83aa88-4020-fb/start", + "received_ns": 12998708 + }, + { + "path": "/g3/18e01eae2e83aa88-4020-fb/slot-0", + "received_ns": 26726875 + }, + { + "path": "/g3/18e01eae2e83aa88-4020-fb/slot-1", + "received_ns": 26742375 + }, + { + "path": "/g3/18e01eae2e83aa88-4020-fb/end", + "received_ns": 130401000 + } + ], + "status": "pass" + }, + { + "adapter": "axum", + "case": "baseline-batch", + "client": { + "body": "{\"adapter\":\"axum\",\"case\":\"baseline-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":false,\"pause_ns\":105429125,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":2,\"token\":\"18e01eae42add628-4020-fc\"}", + "complete_ns": 442574083, + "event": "client_completed", + "first_byte_ns": 442563958, + "header_ns": 442563375, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "283"], + ["date", "Tue, 20 Oct 2026 03:19:45 GMT"] + ], + "status": 200, + "token": "18e01eae42add628-4020-fc" + }, + "event": "outbound-progress", + "probe": { + "adapter": "axum", + "case": "baseline-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 105429125, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0], + "slots": 2, + "token": "18e01eae42add628-4020-fc" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01eae42add628-4020-fc/start", + "received_ns": 14984125 + }, + { + "path": "/g3/18e01eae42add628-4020-fc/end", + "received_ns": 123351750 + }, + { + "path": "/g3/18e01eae42add628-4020-fc/slot-1", + "received_ns": 134712375 + }, + { + "path": "/g3/18e01eae42add628-4020-fc/slot-0", + "received_ns": 134718542 + } + ], + "status": "pass" + }, + { + "adapter": "axum", + "case": "response-owned", + "client": { + "body": " {\"adapter\":\"axum\",\"case\":\"response-owned\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":null,\"handler_returned\":true,\"pause_ns\":103145166,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e01eae5dcf2628-4020-fd\"}", + "complete_ns": 338570792, + "event": "client_completed", + "first_byte_ns": 13550917, + "header_ns": 13549667, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:19:45 GMT"] + ], + "status": 200, + "token": "18e01eae5dcf2628-4020-fd" + }, + "event": "outbound-progress", + "probe": { + "adapter": "axum", + "case": "response-owned", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": true, + "pause_ns": 103145166, + "protocol": "http1", + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sizes": [0, 0], + "slots": 2, + "token": "18e01eae5dcf2628-4020-fd" + }, + "qualification": false, + "receipts": [ + { + "path": "/g3/18e01eae5dcf2628-4020-fd/start", + "received_ns": 13471833 + }, + { + "path": "/g3/18e01eae5dcf2628-4020-fd/slot-0", + "received_ns": 28462125 + }, + { + "path": "/g3/18e01eae5dcf2628-4020-fd/slot-1", + "received_ns": 28471500 + }, + { + "path": "/g3/18e01eae5dcf2628-4020-fd/end", + "received_ns": 123999500 + } + ], + "status": "pass" + }, + { + "adapter": "axum", + "client": { + "body": "{\"adapter\":\"axum\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"known_empty\":null,\"source\":\"not-exposed-by-native-entrypoint\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 166334, + "event": "client_completed", + "first_byte_ns": 157625, + "header_ns": 157209, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "193"], + ["date", "Tue, 20 Oct 2026 03:19:46 GMT"] + ], + "status": 200, + "token": "18e01eae72aed688-4020-fe" + }, + "event": "inbound-empty-signal", + "framing": "Absent", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "axum", + "client": { + "body": "{\"adapter\":\"axum\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"known_empty\":null,\"source\":\"not-exposed-by-native-entrypoint\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 112250, + "event": "client_completed", + "first_byte_ns": 108583, + "header_ns": 108417, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "193"], + ["date", "Tue, 20 Oct 2026 03:19:46 GMT"] + ], + "status": 200, + "token": "18e01eae72b984e8-4020-ff" + }, + "event": "inbound-empty-signal", + "framing": "LengthZero", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "axum", + "client": { + "body": "{\"adapter\":\"axum\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"known_empty\":null,\"source\":\"not-exposed-by-native-entrypoint\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 118583, + "event": "client_completed", + "first_byte_ns": 113583, + "header_ns": 113458, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "193"], + ["date", "Tue, 20 Oct 2026 03:19:46 GMT"] + ], + "status": 200, + "token": "18e01eae72c2c800-4020-100" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedEmpty", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "axum", + "client": { + "body": "{\"adapter\":\"axum\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"known_empty\":null,\"source\":\"not-exposed-by-native-entrypoint\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 103375, + "event": "client_completed", + "first_byte_ns": 99625, + "header_ns": 99417, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "193"], + ["date", "Tue, 20 Oct 2026 03:19:46 GMT"] + ], + "status": 200, + "token": "18e01eae72cc8048-4020-101" + }, + "event": "inbound-empty-signal", + "framing": "FixedBody", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "axum", + "client": { + "body": "{\"adapter\":\"axum\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"known_empty\":null,\"source\":\"not-exposed-by-native-entrypoint\"},\"method\":\"GET\",\"qualification\":false}", + "complete_ns": 99625, + "event": "client_completed", + "first_byte_ns": 95417, + "header_ns": 95292, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "193"], + ["date", "Tue, 20 Oct 2026 03:19:46 GMT"] + ], + "status": 200, + "token": "18e01eae72d56988-4020-102" + }, + "event": "inbound-empty-signal", + "framing": "ChunkedBody", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + }, + "protocol": "http1", + "qualification": false + }, + { + "adapter": "axum", + "attempted_empty_body_cases": 5, + "attempted_progress_cases": 5, + "empty_body_passes": 5, + "event": "progress-coverage", + "failures": [], + "limitations": [ + "HTTP/2 and HTTP/3 not exercised", + "hosted execution pending", + "response-owned tests handler/dispatch return, not host callback return", + "missing-context detection unproved except native absent-runtime control", + "no cancellation or G2 readiness qualification" + ], + "origin_requests": 18, + "progress_passes": 5, + "qualification": false + } + ], + "runtime_logs": { + "fastly": "2026-10-20T03:19:34.357723Z WARN no backend definitions found in /private/tmp/edgezero-g3-smoke-reviewed/progress-fastly/fastly.toml\n", + "spin": "Using runtime config [key_value_store.fixture_config: spin], [key_value_store.fixture_kv: spin] from \"/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/crates/fixture-spin/runtime-config.toml\"\nLogging component stdio to \"/private/tmp/edgezero-g3-smoke-reviewed/progress-spin/.spin/logs/\"\n\nServing http://127.0.0.1:52760\nAvailable Routes:\n fixture: http://127.0.0.1:52760 (wildcard)\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"case\":\"sdk-stream-ready\",\"coordinator_ok\":true,\"event\":\"sdk-stream-coordinator-terminal\",\"qualification\":false,\"token\":\"18e01ead20b60a50-4020-e3\"}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"case\":\"sdk-stream-timer-only\",\"coordinator_ok\":true,\"event\":\"sdk-stream-coordinator-terminal\",\"qualification\":false,\"token\":\"18e01ead20cdb4e8-4020-e4\"}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\nworker failed: worker panicked\n", + "axum": "2026-10-20T03:19:44.290Z INFO [edgezero_adapter_axum::dev_server] [edgezero] axum HTTP/1 ingress limits: AxumIngressConfig { head_read_timeout: 10s, max_connections: 256, max_header_count: 100, max_raw_head_bytes: 65536 }\n2026-10-20T03:19:44.290Z INFO [edgezero_adapter_axum::dev_server] [edgezero] axum config snapshot limits: ConfigStoreLimits { entries: 1024, file_bytes: 16777216, key_bytes: 1024, resident_bytes: 33554432, startup_bytes: 117440512, value_bytes: 8388608 }\n2026-10-20T03:19:44.290Z INFO [edgezero_adapter_axum::dev_server] [edgezero] starting axum server on http://127.0.0.1:52827\n" + }, + "spin_component_stderr": "{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"case\":\"sdk-stream-ready\",\"coordinator_ok\":true,\"event\":\"sdk-stream-coordinator-terminal\",\"qualification\":false,\"token\":\"18e01ead20b60a50-4020-e3\"}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"case\":\"sdk-stream-timer-only\",\"coordinator_ok\":true,\"event\":\"sdk-stream-coordinator-terminal\",\"qualification\":false,\"token\":\"18e01ead20cdb4e8-4020-e4\"}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n", + "before_fix": { + "source_state": "intermediate uncommitted diagnostic build; full source snapshot not retained", + "compiled_binary_sha256": "bd4407f60bb52c0c967e29ca5244ea6ac358d6c46a96e10f5728a38694587ef9", + "production_response_at_base_sha256": "aab7390880450cb6b603dc579d479a54289d4299c8c221138dbbac425634c8e1", + "retained_events_sha256": "fb80d5278b97c8cda87d4d401f2679a403ebb5165a9a641ce22c6433be021448", + "events": [ + { + "adapter": "spin", + "case": "response-owned-ready", + "error": "response ended before its status: UnexpectedEof", + "event": "response-stream-control", + "partial": null, + "qualification": false, + "status": "fail" + }, + { + "adapter": "spin", + "case": "response-owned-timer-only", + "error": "response ended before its status: UnexpectedEof", + "event": "response-stream-control", + "partial": null, + "qualification": false, + "status": "fail" + }, + { + "adapter": "spin", + "case": "sdk-stream-ready", + "event": "response-stream-control", + "observation": { + "client": { + "body": " {\"adapter\":\"spin\",\"case\":\"sdk-stream-ready\",\"pause_ns\":1083,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sdk_native\":true,\"token\":\"18e01dde2a0ed228-10994-2\"}", + "complete_ns": 1209792, + "event": "client_completed", + "first_byte_ns": 1198750, + "header_ns": 1192000, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:04:51 GMT"] + ], + "status": 200, + "token": "18e01dde2a0ed228-10994-2" + }, + "probe": { + "adapter": "spin", + "case": "sdk-stream-ready", + "pause_ns": 1083, + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sdk_native": true, + "token": "18e01dde2a0ed228-10994-2" + } + }, + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "sdk-stream-timer-only", + "event": "response-stream-control", + "observation": { + "client": { + "body": " {\"adapter\":\"spin\",\"case\":\"sdk-stream-timer-only\",\"pause_ns\":101280292,\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sdk_native\":true,\"token\":\"18e01dde2a325400-10994-3\"}", + "complete_ns": 102082208, + "event": "client_completed", + "first_byte_ns": 718125, + "header_ns": 715125, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 03:04:51 GMT"] + ], + "status": 200, + "token": "18e01dde2a325400-10994-3" + }, + "probe": { + "adapter": "spin", + "case": "sdk-stream-timer-only", + "pause_ns": 101280292, + "qualification": false, + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "sdk_native": true, + "token": "18e01dde2a325400-10994-3" + } + }, + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "attempted": 4, + "event": "response-stream-control-coverage", + "failures": ["response-owned-ready", "response-owned-timer-only"], + "passes": 2, + "qualification": false + }, + { + "adapter": "spin", + "attempted_empty_body_cases": 5, + "attempted_progress_cases": 5, + "empty_body_passes": 5, + "event": "progress-coverage", + "failures": ["response-owned"], + "limitations": [ + "HTTP/2 and HTTP/3 not exercised", + "hosted execution pending", + "response-owned tests handler/dispatch return, not host callback return", + "missing-context detection unproved except native absent-runtime control", + "no cancellation or G2 readiness qualification" + ], + "origin_requests": 15, + "progress_passes": 4, + "qualification": false + } + ] + }, + "retention": "Repository-retained local characterization, event and source hashes; not clean-candidate or hosted qualification. Compiled binaries and unrelated smoke history are identity-only.", + "remaining": [ + "G2 Fastly readiness source-error preservation failed", + "Cloudflare matching CLI installation not approved", + "HTTP/2 and HTTP/3", + "provider scheduler-context rejection", + "hosted lifecycle evidence", + "native LocalSet presence/liveness detection", + "future public begin API integration", + "G4 API approval" + ] +} diff --git a/docs/audits/evidence/2026-10-20-cloudflare-outbound.json b/docs/audits/evidence/2026-10-20-cloudflare-outbound.json new file mode 100644 index 000000000..514937c2e --- /dev/null +++ b/docs/audits/evidence/2026-10-20-cloudflare-outbound.json @@ -0,0 +1,400 @@ +{ + "schema": 1, + "recorded_date": "2026-10-20", + "base_revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "scope": "local Cloudflare outbound coding/marker behavior, SDK/public scheduling and original HTTP/1 empty-body signals; not hosted qualification", + "source_snapshot_scope": "selected implementation and fixture inputs, not a frozen full-source-tree attestation", + "environment": { + "WASM_BINDGEN_BIN": "/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen", + "WORKER_BUILD_BIN": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build", + "ESBUILD_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild", + "WASM_OPT_BIN": "/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt", + "WRANGLER_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler", + "WRANGLER_LOG_PATH": "/private/tmp/edgezero-pr275-wrangler-logs", + "WRANGLER_SEND_METRICS": "false", + "MINIFLARE_WORKERD_PATH": null, + "node": "v24.20.0", + "mode": "local loopback-only workerd, no deployment, local synthetic bindings", + "tool_install": "authorized locked wasm-bindgen-cli =0.2.129 installed under /private/tmp only; no project dependency repin or global PATH change" + }, + "tools": { + "WASM_BINDGEN_BIN": { + "path": "/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen", + "bytes": 6938208, + "sha256": "b86e33b3c64362c44d8807ed35797e6b8220dd0232b6b19743caf7c2c68a2284", + "version": "wasm-bindgen 0.2.129" + }, + "WORKER_BUILD_BIN": { + "path": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build", + "bytes": 6149664, + "sha256": "dd7716c2d74637b5d8d2d3e4887252c7c3b8619f549cfb79e5715159bc86c2b0", + "version": "0.8.5" + }, + "ESBUILD_BIN": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild", + "bytes": 10573778, + "sha256": "e2dc9a52440a2a34f09434a2f4843cb1e30f84e40dcf238976ec61ef8cd7f36a", + "version": "0.28.1" + }, + "WASM_OPT_BIN": { + "path": "/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt", + "bytes": 1676584, + "sha256": "fa49ec32a92b72b368b66f8f19d84e9adffeef002449e6a07cd95a3ed2612b41", + "version": "wasm-opt version 130 (version_130)" + }, + "WRANGLER_BIN": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "bytes": 3088, + "sha256": "780661a508810f3b65786895b1ca9aacbc4f55d329ae6b8c1e49ec8433569f77", + "version": "4.131.0" + }, + "workerd": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64/bin/workerd", + "bytes": 156374824, + "sha256": "eb132e3e489b47d804cc70b510a6da8f82dabec4748cdcdaba32a1a1c5c2e285", + "version": "workerd 2026-09-10", + "package_version": "1.20260910.1", + "identification_basis": "Wrangler's bundled workerd package default executable; MINIFLARE_WORKERD_PATH unset; package resolution and executable version verified, not an independent per-process attestation" + } + }, + "worker_sdk": "0.8.5", + "compatibility": { + "date": "2026-04-22", + "flags": ["enable_request_signal", "enable_weak_ref"], + "fixture_mode": "retained" + }, + "invariants": { + "project_pins_unchanged": true, + "production_adapter_source_unchanged": true, + "served_artifacts_identical_between_runs": true, + "fastly_G2": "Failed", + "G4": "API-only AG approval, digest unchanged", + "capabilities_promoted": false + }, + "limitations": [ + "working-tree local characterization, not frozen-candidate/release evidence", + "no hosted, HTTP/2 or HTTP/3 qualification", + "no downstream encoded relay framing proof: coding bytes are returned as JSON", + "SDK-native eager probes do not implement the new public begin/head-first/follow contract; existing public send/batch remain lazy", + "response-owned proves handler/adapter dispatch return and client-byte gate, not exported callback return or confirmed client delivery", + "no cancellation, exact deadline or scheduler-context rejection qualification", + "only exercised adapter-visible fields; no universal raw-header fidelity proof", + "individual coding/marker receipts were asserted but not retained", + "JS/WASM content hashes identify served artifacts; only JS content is embedded, WASM binary remains in the local run directory", + "historical failure records remain unchanged; Fastly G2 still failed and B/H(K) unapproved" + ], + "diagnostic_history": { + "initial_sandbox_attempt": { + "directory": "/private/tmp/edgezero-cloudflare-progress-20261020-first", + "exit_code": 1, + "tests_run": 0, + "reason": "build succeeded; sandbox denied loopback socket creation before tests", + "events": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-first/events.jsonl", + "bytes": 1670, + "sha256": "11c7c461d21c6d7e133d52093f18999fd1c4d737e65cb18eb24025510b838d1e", + "text": "{\"construction_rounds\":0,\"event\":\"run\",\"max_requests\":10,\"seed\":856,\"suite\":\"outbound-progress\"}\n{\"event\":\"build_environment\",\"host_arch\":\"aarch64\",\"host_os\":\"macos\",\"lockfile\":{\"bytes\":84127,\"fnv1a64\":\"10d93fc12c84db43\",\"path\":\"/private/tmp/edgezero-cloudflare-progress-20261020-first/Cargo.lock\",\"purpose\":\"non-security build identity\"},\"rustc\":\"rustc 1.98.1 (48a229cea 2026-09-01)\"}\n{\"adapter\":\"cloudflare\",\"event\":\"progress-runtime\",\"executable\":\"/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js\",\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"source_state\":\"working-tree-characterization\",\"version\":\"4.131.0\"}\n{\"event\":\"worker-build-tool\",\"executable\":\"/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen\",\"variable\":\"WASM_BINDGEN_BIN\",\"version\":\"wasm-bindgen 0.2.129\"}\n{\"event\":\"worker-build-tool\",\"executable\":\"/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild\",\"variable\":\"ESBUILD_BIN\",\"version\":\"0.28.1\"}\n{\"event\":\"worker-build-tool\",\"executable\":\"/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt\",\"variable\":\"WASM_OPT_BIN\",\"version\":\"wasm-opt version 130 (version_130)\"}\n{\"adapter\":\"cloudflare\",\"artifact\":{\"bytes\":1691011,\"fnv1a64\":\"559ae66b5e1e3cc2\",\"path\":\"/private/tmp/edgezero-cloudflare-progress-20261020-first/progress-cloudflare/artifacts/build/index_bg.wasm\",\"purpose\":\"non-security build identity\"},\"event\":\"build\",\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"error\":\"Operation not permitted (os error 1)\",\"status\":\"fail\"}\n" + }, + "summary": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-first/summary.json", + "bytes": 302, + "sha256": "fdad4ea9024508071acff823ee80f31b68f6c2e1980faefaaff86076ffc842e3", + "text": "{\n \"build_identity\": {\n \"status\": \"consistent\"\n },\n \"cpu_comparison\": \"SDK phase readings only; cross-run performance unverified\",\n \"guests\": [],\n \"memory_snapshots\": {},\n \"quantile_method\": \"nearest rank\",\n \"recorded_client_completions\": 0,\n \"recorded_request_starts\": 0,\n \"variants\": {}\n}" + } + }, + "intermediate_strengthened_run": { + "directory": "/private/tmp/edgezero-cloudflare-progress-20261020-validated", + "exit_code": 0, + "events": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-validated/events.jsonl", + "bytes": 90235, + "sha256": "4c283ca504647ea9ba998743c9bb86730780f3797f3c135d97bfba60e83b186c" + }, + "source_equivalence": "after behavior hardening, before final test-only matrix expansion; not used as the final source snapshot" + } + }, + "runs": [ + { + "directory": "/private/tmp/edgezero-cloudflare-progress-20261020-loopback", + "kind": "pre-harness-hardening", + "command": "WASM_BINDGEN_BIN=/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen WORKER_BUILD_BIN=/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build ESBUILD_BIN=/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild WASM_OPT_BIN=/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt WRANGLER_BIN=/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler WRANGLER_LOG_PATH=/private/tmp/edgezero-pr275-wrangler-logs WRANGLER_SEND_METRICS=false cargo run --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -- --adapter cloudflare --suite outbound-progress --output /private/tmp/edgezero-cloudflare-progress-20261020-loopback", + "exit_code": 0, + "source_sha256": { + "Cargo.toml": "ee73d1318af74a997ecc12bac0f80cd7eefbe8ead34a35b1432c8d6f49fc42e1", + "Cargo.lock": "1afb33fe3067419ad5a2a948b330ac1df123d53214e49dc4005a92f7bc69e043", + "crates/edgezero-core/src/outbound.rs": "f1577756a5375dbd9a780b611d50bc56d629b8a50b8c13cc40864d0f1ef5ba8d", + "crates/edgezero-core/src/outbound/exchange.rs": "f8ca9867b572b2e40e0c08ae55fefefaefbf82355c83aa8b28f74870d6e2a879", + "crates/edgezero-core/src/outbound/progress.rs": "90eec32101d09c4ee40409b1fae6f06b97c83d1af8a03baf39d07820da9a0a08", + "crates/edgezero-core/src/lib.rs": "0de5ff9b503fbf86a27b0f0abce7dc47f9f85c7d0d596e0fce4b6589a96d133d", + "crates/edgezero-core/src/compression.rs": "4acccc0c01009894ebe4f77f57ce8ce501d3f3f2ccb565c6b3d3871d826037d4", + "crates/edgezero-core/src/body.rs": "50a8bc65fcbc7df3d9afd3ddc520889f42da46af1c6419e23c19625c33d5bf9e", + "crates/edgezero-core/src/time.rs": "bcd7047587f656b6547c860986f0c8f6459a5554850cbac4c1bfd9c480ec7435", + "crates/edgezero-adapter-cloudflare/src/outbound.rs": "25dbde10a5502c1644a791ce188d39531689d0f1c138594ba17cc23c5854072c", + "crates/edgezero-adapter-cloudflare/src/request.rs": "a755088e4e65e5af813335283ec3095c35b5982d1e9057ce58dd748aa03e6200", + "crates/edgezero-adapter-cloudflare/src/response.rs": "15a5b643713d3fdd2bd965fd00d9b606ca5cddf754678c3b0a0b9a65bba21adb", + "tests/fixtures/reusable-app/Cargo.toml": "7665200757e4e565d11fa7eb036215aa583d21fd543bca39fdbdce470fee11ae", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs": "295b98872ab59c185f6907189dad13c5c4c56699538d9756872d1c7f82cb6812", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/progress.rs": "b29022de35c338efd6969b4f0f254a3c3dbf1036600364f593d7eef0288a3758", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs": "f07fd19c3937780e5a7973513ccd29dd4708c852f62f4b2dd3e14929181605f9", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs": "8719b6eb05c39b514f2f571166628a5d919935f3ebbb9e69be9b854bb416d4a3", + "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs": "22f5c8ff413357b5684910249c635a4993d0cb912b3cc584c8f14cc2258efb18", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/Cargo.toml": "96501fc975013ba68411cd6e0f77b6b0fe505dfe4ed8cef16620ef19b4c9784d", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml": "6c2a097879b98f4d4af3e2fea139215abc9fa0bfdc058500f28fd9f7d757aab3", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs": "606245131f0c594154ca0a2eb63e5341999325bfb2de55887ab3264fa4b89559", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs": "305f70039af59501639916bd8cfb8db696613c73af670e953dd329a3eed14498" + }, + "counts": { + "outbound-coding": 24, + "outbound-marker": 46, + "outbound-progress": 5, + "inbound-empty-signal": 5, + "progress-group": 0, + "client_completed": 80 + }, + "asserted_origin_receipts": { + "coding": 24, + "marker": 47, + "scheduling": 18, + "total": 89, + "retention": "only scheduling receipts are emitted individually; coding/marker counts are enforced by successful runner guards" + }, + "retained_scheduling_receipts": 18, + "build_environment": { + "event": "build_environment", + "host_arch": "aarch64", + "host_os": "macos", + "lockfile": { + "bytes": 84127, + "fnv1a64": "10d93fc12c84db43", + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-loopback/Cargo.lock", + "purpose": "non-security build identity" + }, + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)" + }, + "runtime": { + "adapter": "cloudflare", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "4.131.0" + }, + "coverage": { + "adapter": "cloudflare", + "attempted_empty_body_cases": 5, + "attempted_progress_cases": 5, + "empty_body_passes": 5, + "event": "progress-coverage", + "failures": [], + "limitations": [ + "HTTP/2 and HTTP/3 not exercised", + "hosted execution pending", + "response-owned tests handler/dispatch return, not host callback return", + "missing-context detection unproved except native absent-runtime control", + "no cancellation or G2 readiness qualification" + ], + "origin_requests": 18, + "progress_passes": 5, + "qualification": false + }, + "effective_config": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-loopback/progress-cloudflare/wrangler.toml", + "bytes": 490, + "sha256": "2c37aa9173474494dd0acc76660959c9b65c43b329e9433b1155e8dc1f5aa016", + "text": "name = \"reusable-app-fixture\"\nmain = \"/private/tmp/edgezero-cloudflare-progress-20261020-loopback/progress-cloudflare/artifacts/build/worker/shim.mjs\"\ncompatibility_date = \"2026-04-22\"\ncompatibility_flags = [\"enable_request_signal\", \"enable_weak_ref\"]\n[vars]\nFIXTURE_MODE = \"retained\"\nfixture_marker = \"fixture-only-value\"\n\n[[kv_namespaces]]\nbinding = \"fixture_config\"\nid = \"00000000000000000000000000000001\"\n[[kv_namespaces]]\nbinding = \"fixture_kv\"\nid = \"00000000000000000000000000000002\"\n" + }, + "lockfile": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-loopback/Cargo.lock", + "bytes": 84127, + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + "artifacts": { + "index_bg.wasm": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-loopback/progress-cloudflare/artifacts/build/index_bg.wasm", + "bytes": 1691011, + "sha256": "95ece35b326d6da9c0a312369051299bca179ad46bde33d9958cd47a3c524744" + }, + "index.js": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-loopback/progress-cloudflare/artifacts/build/index.js", + "bytes": 28684, + "sha256": "e3fbc6a80de31ae583f5808512cdaa34dfdfb1a8f9e47dc7c0a339f1ee8842a8", + "text": "import{WorkerEntrypoint as xe}from\"cloudflare:workers\";var G=globalThis.__worker_init_state={criticalError:!1,instanceId:0};import X from\"./index_bg.wasm\";var x=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,be.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_containerstartupoptions_free(e,0)}catch(t){o(t)}}get enableInternet(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_containerstartupoptions_enableInternet(this.__wbg_ptr)}catch(t){o(t)}return e===16777215?void 0:e!==0}get entrypoint(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_containerstartupoptions_entrypoint(this.__wbg_ptr)}catch(n){o(n)}var t=ye(e[0],e[1]);return i.__wbindgen_free(e[0],e[1]*4,4),t}get env(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_containerstartupoptions_env(this.__wbg_ptr)}catch(t){o(t)}return e}set enableInternet(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_containerstartupoptions_enableInternet(this.__wbg_ptr,u(e)?16777215:e?1:0)}catch(t){o(t)}}set entrypoint(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t=he(e,i.__wbindgen_malloc),n=y;c();try{i.__wbg_set_containerstartupoptions_entrypoint(this.__wbg_ptr,t,n)}catch(r){o(r)}}set env(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_containerstartupoptions_env(this.__wbg_ptr,e)}catch(t){o(t)}}};Symbol.dispose&&(x.prototype[Symbol.dispose]=x.prototype.free);var I=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,de.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_intounderlyingbytesource_free(e,0)}catch(t){o(t)}}get autoAllocateChunkSize(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.intounderlyingbytesource_autoAllocateChunkSize(this.__wbg_ptr)}catch(t){o(t)}return e>>>0}cancel(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e=this.__destroy_into_raw();c();try{i.intounderlyingbytesource_cancel(e)}catch(t){o(t)}}pull(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t;c();try{t=i.intounderlyingbytesource_pull(this.__wbg_ptr,e)}catch(n){o(n)}return t}start(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.intounderlyingbytesource_start(this.__wbg_ptr,e)}catch(t){o(t)}}get type(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.intounderlyingbytesource_type(this.__wbg_ptr)}catch(t){o(t)}return fe[e]}};Symbol.dispose&&(I.prototype[Symbol.dispose]=I.prototype.free);var E=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,ge.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_intounderlyingsink_free(e,0)}catch(t){o(t)}}abort(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t=this.__destroy_into_raw(),n;c();try{n=i.intounderlyingsink_abort(t,e)}catch(r){o(r)}return n}close(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e=this.__destroy_into_raw(),t;c();try{t=i.intounderlyingsink_close(e)}catch(n){o(n)}return t}write(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t;c();try{t=i.intounderlyingsink_write(this.__wbg_ptr,e)}catch(n){o(n)}return t}};Symbol.dispose&&(E.prototype[Symbol.dispose]=E.prototype.free);var R=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,we.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_intounderlyingsource_free(e,0)}catch(t){o(t)}}cancel(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e=this.__destroy_into_raw();c();try{i.intounderlyingsource_cancel(e)}catch(t){o(t)}}pull(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t;c();try{t=i.intounderlyingsource_pull(this.__wbg_ptr,e)}catch(n){o(n)}return t}};Symbol.dispose&&(R.prototype[Symbol.dispose]=R.prototype.free);var m=class _{static __wrap(e){let t=Object.create(_.prototype);return t.__wbg_ptr=e,Object.defineProperty(t,\"__wbg_inst\",{value:s,writable:!0}),K.register(t,{ptr:e,instance:s},t),t}__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,K.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_minifyconfig_free(e,0)}catch(t){o(t)}}get css(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_minifyconfig_css(this.__wbg_ptr)}catch(t){o(t)}return e!==0}get html(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_minifyconfig_html(this.__wbg_ptr)}catch(t){o(t)}return e!==0}get js(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_minifyconfig_js(this.__wbg_ptr)}catch(t){o(t)}return e!==0}set css(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_minifyconfig_css(this.__wbg_ptr,e)}catch(t){o(t)}}set html(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_minifyconfig_html(this.__wbg_ptr,e)}catch(t){o(t)}}set js(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_minifyconfig_js(this.__wbg_ptr,e)}catch(t){o(t)}}};Symbol.dispose&&(m.prototype[Symbol.dispose]=m.prototype.free);var F=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,le.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_r2range_free(e,0)}catch(t){o(t)}}get length(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_r2range_length(this.__wbg_ptr)}catch(t){o(t)}return e[0]===0?void 0:e[1]}get offset(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_r2range_offset(this.__wbg_ptr)}catch(t){o(t)}return e[0]===0?void 0:e[1]}get suffix(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_r2range_suffix(this.__wbg_ptr)}catch(t){o(t)}return e[0]===0?void 0:e[1]}set length(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_r2range_length(this.__wbg_ptr,!u(e),u(e)?0:e)}catch(t){o(t)}}set offset(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_r2range_offset(this.__wbg_ptr,!u(e),u(e)?0:e)}catch(t){o(t)}}set suffix(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_r2range_suffix(this.__wbg_ptr,!u(e),u(e)?0:e)}catch(t){o(t)}}};Symbol.dispose&&(F.prototype[Symbol.dispose]=F.prototype.free);function U(){s++,p=null,k=null,W=null,typeof numBytesDecoded<\"u\"&&(numBytesDecoded=0),typeof y<\"u\"&&(y=0),D=!1,O=!1,q=new WebAssembly.Instance(X,te()),i=q.exports,i.__wbindgen_start()}function Y(){let _;c();try{_=i.__worker_init_state()}catch(e){o(e)}return _}function Z(_,e,t){let n;c();try{n=i.fetch(_,e,t)}catch(r){o(r)}return n}function ee(){c();try{i.init()}catch(_){o(_)}}function te(){return{__proto__:null,\"./index_bg.js\":{__proto__:null,__wbg_Error_30c8987f7c2ed4e2:function(e,t){return Error(l(e,t))},__wbg_Number_14af1003b8dd5ead:function(e){return Number(e)},__wbg_String_8564e559799eccda:function(e,t){let n=String(t),r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg___wbindgen_bigint_get_as_i64_a2383202b9353e4c:function(e,t){let n=t,r=typeof n==\"bigint\"?n:void 0;d().setBigInt64(e+8,u(r)?BigInt(0):r,!0),d().setInt32(e+0,!u(r),!0)},__wbg___wbindgen_boolean_get_5b446f51afd21013:function(e){let t=e,n=typeof t==\"boolean\"?t:void 0;return u(n)?16777215:n?1:0},__wbg___wbindgen_debug_string_4687d8d8c2017d52:function(e,t){let n=V(t),r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg___wbindgen_in_92f62ee1427d9e49:function(e,t){return e in t},__wbg___wbindgen_is_bigint_b123553bed3bb382:function(e){return typeof e==\"bigint\"},__wbg___wbindgen_is_function_1f9d30630b8b1d3d:function(e){return typeof e==\"function\"},__wbg___wbindgen_is_null_e343b7d08827ba72:function(e){return e===null},__wbg___wbindgen_is_null_or_undefined_f9435d56bdcc06df:function(e){return e==null},__wbg___wbindgen_is_object_3c45d4f2dde4e749:function(e){let t=e;return typeof t==\"object\"&&t!==null},__wbg___wbindgen_is_string_90b56bc79aad6f6c:function(e){return typeof e==\"string\"},__wbg___wbindgen_is_undefined_8865fb403f8fe9d8:function(e){return e===void 0},__wbg___wbindgen_jsval_eq_02babf21faa37971:function(e,t){return e===t},__wbg___wbindgen_jsval_loose_eq_677f21e468d6b461:function(e,t){return e==t},__wbg___wbindgen_number_get_2e0e7dee9f701a71:function(e,t){let n=t,r=typeof n==\"number\"?n:void 0;d().setFloat64(e+8,u(r)?0:r,!0),d().setInt32(e+0,!u(r),!0)},__wbg___wbindgen_reinit_3f79fdb34dee3ea9:function(){O=!0},__wbg___wbindgen_string_get_0380ccaa2f57f0d9:function(e,t){let n=t,r=typeof n==\"string\"?n:void 0;var f=u(r)?0:S(r,i.__wbindgen_malloc,i.__wbindgen_realloc),a=y;d().setInt32(e+4,a,!0),d().setInt32(e+0,f,!0)},__wbg___wbindgen_throw_41e9ee4f547fc59a:function(e,t){throw new WebAssembly.Exception(B,[new Error(l(e,t))])},__wbg__wbg_cb_unref_dcc1a90847f04c41:function(e){e._wbg_cb_unref()},__wbg_abort_d5fa4a667a578774:function(e){return e.abort()},__wbg_abort_f78b99d80248bacb:function(e){e.abort()},__wbg_aborted_5b2b5f06b5e59fde:function(e){return e.aborted},__wbg_append_1938ed58fb5613b8:function(e,t,n,r,f){e.append(l(t,n),l(r,f))},__wbg_arrayBuffer_aa7cad37be6e6b29:function(e){return e.arrayBuffer()},__wbg_body_4792166cb351f162:function(e){let t=e.body;return u(t)?0:g(t)},__wbg_body_4e088733babc630d:function(e){let t=e.body;return u(t)?0:g(t)},__wbg_buffer_56ec2905a66f58b9:function(e){return e.buffer},__wbg_byobRequest_54f7ce41d584549a:function(e){let t=e.byobRequest;return u(t)?0:g(t)},__wbg_byteLength_9f985b8373f1f3fe:function(e){return e.byteLength},__wbg_byteOffset_0bd5a6a03553639c:function(e){return e.byteOffset},__wbg_call_1875a20c43a36133:function(e,t,n,r){return e.call(t,n,r)},__wbg_call_187d372bd5fdd4aa:function(e,t,n){return e.call(t,n)},__wbg_call_6137034ef55c9d0f:function(e,t){return e.call(t)},__wbg_call_939a2607c4484b0b:function(e,t,n,r,f){return e.call(t,n,r,f)},__wbg_cancel_3b323af6bb094e47:function(e){return e.cancel()},__wbg_cancel_5b5c53c5bbb13318:function(e){return e.cancel()},__wbg_cancel_7c2a08c8db4c73f3:function(e){return e.cancel()},__wbg_catch_92594c2d17f55f12:function(e,t){return e.catch(t)},__wbg_cause_19276d032a32d4bd:function(e){return e.cause},__wbg_cf_909bcfb212a8822c:function(e){let t=e.cf;return u(t)?0:g(t)},__wbg_cf_b47fe5ff6d015a0d:function(e){let t=e.cf;return u(t)?0:g(t)},__wbg_clearTimeout_d49a4afcd9b218b7:function(e,t){e.clearTimeout(t)},__wbg_close_185286988f30f252:function(e){e.close()},__wbg_close_748f26698dabe269:function(e){e.close()},__wbg_close_fd7fd8eaf125c9c1:function(e){return e.close()},__wbg_constructor_076176ce0f386e16:function(e){return e.constructor},__wbg_done_b41a1d26cdb37fb6:function(e){return e.done},__wbg_enqueue_4a01c128f9a0de16:function(e,t){e.enqueue(t)},__wbg_entries_b55ec7e0dc443355:function(e){return e.entries()},__wbg_entries_fb6397112b1de25f:function(e){return Object.entries(e)},__wbg_error_a30e98d44009f1bb:function(e,t){console.error(e,t)},__wbg_error_c9cf3fc2064683a9:function(e){console.error(e)},__wbg_fetch_0e46da8cf7c730e4:function(e,t,n,r){return e.fetch(l(t,n),r)},__wbg_fetch_7b5de4f88137a4dc:function(e,t,n){return e.fetch(t,n)},__wbg_getReader_1ef752ebe7bb7900:function(e,t){return e.getReader(t)},__wbg_getReader_bb0230851fbf986b:function(e){return e.getReader()},__wbg_getWriter_7994a8c7d4b75c01:function(e){return e.getWriter()},__wbg_get_31af05bd4842a84f:function(e,t){return Reflect.get(e,t)},__wbg_get_658f6698067d9515:function(e,t){return Reflect.get(e,t)},__wbg_get_6c896e0571ddae51:function(e,t){return e[t>>>0]},__wbg_get_88983e33467eae60:function(e,t){let n=Reflect.get(e,t);return u(n)?0:g(n)},__wbg_get_done_7020b91c987365c7:function(e){let t=e.done;return u(t)?16777215:t?1:0},__wbg_get_unchecked_288889d017702237:function(e,t){return e[t>>>0]},__wbg_get_value_87fbd6ad1283f2e7:function(e){return e.value},__wbg_get_with_ref_key_6412cf3094599694:function(e,t){return e[t]},__wbg_headers_6930a8bd09b630de:function(e){return e.headers},__wbg_headers_eba93595f8944c2f:function(e){return e.headers},__wbg_instanceId_27a130234331eb08:function(e){return e.instanceId},__wbg_instanceof_ArrayBuffer_a99f175873e5d9b8:function(e){let t;try{t=e instanceof ArrayBuffer}catch{t=!1}return t},__wbg_instanceof_Error_80a725f81f2e102d:function(e){let t;try{t=e instanceof Error}catch{t=!1}return t},__wbg_instanceof_Map_b2611749102d7ba3:function(e){let t;try{t=e instanceof Map}catch{t=!1}return t},__wbg_instanceof_Promise_e1dc6893aa1ece28:function(e){let t;try{t=e instanceof Promise}catch{t=!1}return t},__wbg_instanceof_ReadableStream_e2d77912575119ab:function(e){let t;try{t=e instanceof ReadableStream}catch{t=!1}return t},__wbg_instanceof_Response_b8758567269c30b2:function(e){let t;try{t=e instanceof Response}catch{t=!1}return t},__wbg_instanceof_Uint8Array_828cef2aaacafc31:function(e){let t;try{t=e instanceof Uint8Array}catch{t=!1}return t},__wbg_isArray_e15a2ff68ffdbef2:function(e){return Array.isArray(e)},__wbg_isSafeInteger_717808ad6a54bd9e:function(e){return Number.isSafeInteger(e)},__wbg_iterator_e3c31c892080e444:function(){return Symbol.iterator},__wbg_keys_bac434c8972731d5:function(e){return e.keys()},__wbg_length_7f3c00c40364105e:function(e){return e.length},__wbg_length_d4bdea10311bd9cf:function(e){return e.length},__wbg_message_5f8387f0c32b90a7:function(e){return e.message},__wbg_method_6d02ee0808a7de31:function(e,t){let n=t.method,r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg_minifyconfig_new:function(e){return m.__wrap(e)},__wbg_name_e2eac7cdfa054f65:function(e){return e.name},__wbg_name_f9b814d78e8e299f:function(e){return e.name},__wbg_new_1dbf7428bba60a42:function(e){return new Uint8Array(e)},__wbg_new_28744009d011f847:function(){return new Map},__wbg_new_2e41357094e99294:function(){return new AbortController},__wbg_new_343a093a3c2ffb4e:function(e,t){return new Error(l(e,t))},__wbg_new_617a8cdb8bb1130e:function(){return new Object},__wbg_new_661230f001cf2087:function(){return new TransformStream},__wbg_new_83ca405eae412a62:function(){return new Headers},__wbg_new_big_int_84cdc69174f81f5b:function(e){return new FixedLengthStream(e)},__wbg_new_ee2291f50781bf1d:function(){return new Array},__wbg_new_f569a2e335eed0bb:function(e){return new FixedLengthStream(e>>>0)},__wbg_new_from_slice_9a868026ffa4208a:function(e,t){return new Uint8Array(L(e,t))},__wbg_new_typed_b01cb72a8af741a3:function(e,t){try{var n={a:e,b:t},r=(a,b)=>{let w=n.a;n.a=0;try{return ce(w,n.b,a,b)}finally{n.a=w}};return new Promise(r)}finally{n.a=0}},__wbg_new_with_byte_offset_and_length_2f5d7fc2a828b74d:function(e,t,n){return new Uint8Array(e,t>>>0,n>>>0)},__wbg_new_with_length_3da0ad195f6f63ba:function(e){return new Uint8Array(e>>>0)},__wbg_new_with_opt_buffer_source_and_init_c1b859a23c80afca:function(e,t){return new Response(e,t)},__wbg_new_with_opt_readable_stream_and_init_df114f2d364f216d:function(e,t){return new Response(e,t)},__wbg_new_with_opt_str_and_init_33bef1b272950372:function(e,t,n){return new Response(e===0?void 0:l(e,t),n)},__wbg_new_with_str_and_init_4618dee4e950224f:function(e,t,n){return new Request(l(e,t),n)},__wbg_next_33784799010f1bbe:function(e){return e.next},__wbg_next_f4aac29c42af995c:function(e){return e.next()},__wbg_now_e7c6795a7f81e10f:function(e){return e.now()},__wbg_performance_3fcf6e32a7e1ed0a:function(e){return e.performance},__wbg_prototypesetcall_bc27214492979395:function(e,t,n){Uint8Array.prototype.set.call(L(e,t),n)},__wbg_queueMicrotask_9833f9a49df95a49:function(e){return e.queueMicrotask},__wbg_queueMicrotask_a72f977e97f23c5f:function(e){queueMicrotask(e)},__wbg_read_2a943774344c9728:function(e){return e.read()},__wbg_read_8cba94edf8011d59:function(e,t){return e.read(t)},__wbg_readable_4d43535945ae9ca0:function(e){return e.readable},__wbg_releaseLock_0948949015793a6d:function(e){e.releaseLock()},__wbg_releaseLock_eb64cd83aa9fcfe4:function(e){e.releaseLock()},__wbg_releaseLock_fe24bdfe19e75e0b:function(e){e.releaseLock()},__wbg_resolve_0076e10020304ede:function(e){return Promise.resolve(e)},__wbg_respond_c102fabcc79e5ef0:function(e,t){e.respond(t>>>0)},__wbg_setTimeout_7fb1e1fe294b6ad9:function(e,t,n){return e.setTimeout(t,n)},__wbg_set_145a351398b48c65:function(e,t,n){return Reflect.set(e,t,n)},__wbg_set_575d3ddb70fe831d:function(e,t,n){e.set(L(t,n))},__wbg_set_6ae97e73113c4f0b:function(e,t,n){return e.set(t,n)},__wbg_set_6be42768c690e380:function(e,t,n){e[t]=n},__wbg_set_af5932e3f78b5648:function(e,t,n,r,f){e.set(l(t,n),l(r,f))},__wbg_set_bea140a88be9b277:function(e,t,n){e[t>>>0]=n},__wbg_set_body_1fb0f1008bfc7df6:function(e,t){e.body=t},__wbg_set_cache_7507c4cc5938ccc7:function(e,t){e.cache=ae[t]},__wbg_set_criticalError_0d391f2a45c3bc42:function(e,t){e.criticalError=t!==0},__wbg_set_headers_40ad33dcf016613f:function(e,t){e.headers=t},__wbg_set_headers_69ef7c46a2e6c742:function(e,t){e.headers=t},__wbg_set_instanceId_9b3420954adec865:function(e,t){e.instanceId=t>>>0},__wbg_set_method_dcb32343247ec427:function(e,t,n){e.method=l(t,n)},__wbg_set_mode_6d06239cb5535f25:function(e,t){e.mode=se[t]},__wbg_set_redirect_cc128ab2411da334:function(e,t){e.redirect=ue[t]},__wbg_set_signal_2dbd377604627f26:function(e,t){e.signal=t},__wbg_set_status_678f122cbe435d95:function(e,t){e.status=t},__wbg_signal_26a6bdfa17b46df6:function(e){return e.signal},__wbg_signal_e5a49a2d9c65b7f6:function(e){return e.signal},__wbg_static_accessor_GLOBAL_266715b9d96ba635:function(){let e=typeof global>\"u\"?null:global;return u(e)?0:g(e)},__wbg_static_accessor_GLOBAL_THIS_10fb7dc1ae063179:function(){let e=typeof globalThis>\"u\"?null:globalThis;return u(e)?0:g(e)},__wbg_static_accessor_INIT_STATE_35966a05809b7176:function(){return G},__wbg_static_accessor_SELF_0b583911f537483a:function(){let e=typeof self>\"u\"?null:self;return u(e)?0:g(e)},__wbg_static_accessor_WINDOW_d7f903d1508cbdc4:function(){let e=typeof window>\"u\"?null:window;return u(e)?0:g(e)},__wbg_status_ce0a98d3c57125f3:function(e){return e.status},__wbg_subarray_002b94d5e13d1411:function(e,t,n){return e.subarray(t>>>0,n>>>0)},__wbg_then_4e48da6c0a644c56:function(e,t,n){return e.then(t,n)},__wbg_then_c8a35d4ad59c6b8e:function(e,t){return e.then(t)},__wbg_then_c949d5a25a4e78f8:function(e,t,n){return e.then(t,n)},__wbg_then_e71170d78fcf8954:function(e,t){return e.then(t)},__wbg_url_7bfada5c2297bc70:function(e,t){let n=t.url,r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg_value_f3c585ee8f5ba40c:function(e){return e.value},__wbg_view_9c570f33e8d6ab96:function(e){let t=e.view;return u(t)?0:g(t)},__wbg_waitUntil_c51e37673c596e00:function(e,t){e.waitUntil(t)},__wbg_webSocket_2b7e8e9549bed673:function(e){let t=e.webSocket;return u(t)?0:g(t)},__wbg_writable_44735d15a1cee85d:function(e){return e.writable},__wbg_write_bf07aa79dd36a472:function(e,t){return e.write(t)},__wbindgen_generic_0000000000000001:function(e,t){return C(e,t,oe)},__wbindgen_generic_0000000000000002:function(e,t){return C(e,t,ie)},__wbindgen_generic_0000000000000003:function(e,t){return C(e,t,re)},__wbindgen_generic_0000000000000004:function(e){return e},__wbindgen_generic_0000000000000005:function(e){return e},__wbindgen_generic_0000000000000006:function(e,t){return l(e,t)},__wbindgen_generic_0000000000000007:function(e){return BigInt.asUintN(64,e)},__wbindgen_init_externref_table:function(){let e=i.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)},__wbindgen_jstag:WebAssembly.JSTag,__wbindgen_rethrow_critical:function(e){throw new Error(\"Critical error\",{cause:e})}}}}var B=new WebAssembly.Tag({parameters:[\"externref\"]}),J,D=!1;function ne(){D=!0;try{let _=N()[i.__abort_handler.value/4];_&&i.__wbindgen_export.get(_)()}catch{}}function o(_){throw _ instanceof WebAssembly.Exception&&_.is(B)?_.getArg(B,0):(N()[J]=1,ne(),_)}function c(){if(J??=i.__instance_terminated.value/4,N()[J]){if(D||ne(),O){U();return}throw new Error(\"Module terminated\")}else O&&U()}function re(_,e){c();try{i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke_______true_(_,e)}catch(t){o(t)}}function ie(_,e,t){c();try{i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke___wasm_bindgen_c7f02901330ffbdc___JsValue______true_(_,e,t)}catch(n){o(n)}}function oe(_,e,t){let n;c();try{n=i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke___wasm_bindgen_c7f02901330ffbdc___JsValue__core_ed718c3d60ebd546___result__Result_____wasm_bindgen_c7f02901330ffbdc___JsError___true_(_,e,t)}catch(r){o(r)}if(n[1])throw pe(n[0])}function ce(_,e,t,n){c();try{i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke___js_sys_b2f584a2e795ca61___Function_fn_wasm_bindgen_c7f02901330ffbdc___JsValue_____wasm_bindgen_c7f02901330ffbdc___sys__Undefined___js_sys_b2f584a2e795ca61___Function_fn_wasm_bindgen_c7f02901330ffbdc___JsValue_____wasm_bindgen_c7f02901330ffbdc___sys__Undefined_______true_(_,e,t,n)}catch(r){o(r)}}var se=[\"byob\"],fe=[\"bytes\"],ae=[\"default\",\"no-store\",\"reload\",\"no-cache\",\"force-cache\",\"only-if-cached\"],ue=[\"follow\",\"error\",\"manual\"],s=0,be=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_containerstartupoptions_free(_,1)}),de=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_intounderlyingbytesource_free(_,1)}),ge=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_intounderlyingsink_free(_,1)}),we=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_intounderlyingsource_free(_,1)}),K=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_minifyconfig_free(_,1)}),le=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_r2range_free(_,1)});function g(_){let e=i.__externref_table_alloc();return i.__wbindgen_externrefs.set(e,_),e}var Q=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(_=>{_.instance===s&&i.__wbindgen_destroy_closure(_.a,_.b)});function V(_){let e=typeof _;if(e==\"number\"||e==\"boolean\"||_==null)return`${_}`;if(e==\"string\")return`\"${_}\"`;if(e==\"symbol\"){let r=_.description;return r==null?\"Symbol\":`Symbol(${r})`}if(e==\"function\"){let r=_.name;return typeof r==\"string\"&&r.length>0?`Function(${r})`:\"Function\"}if(Array.isArray(_)){let r=_.length,f=\"[\";r>0&&(f+=V(_[0]));for(let a=1;a1)n=t[1];else return toString.call(_);if(n==\"Object\")try{return\"Object(\"+JSON.stringify(_)+\")\"}catch{return\"Object\"}return _ instanceof Error?`${_.name}: ${_.message}\n${_.stack}`:n}function ye(_,e){_=_>>>0;let t=d(),n=[];for(let r=_;r<_+4*e;r+=4)n.push(i.__wbindgen_externrefs.get(t.getUint32(r,!0)));return i.__externref_drop_slice(_,e),n}function L(_,e){return _=_>>>0,A().subarray(_/1,_/1+e)}var p=null;function d(){return(p===null||p.buffer.detached===!0||p.buffer.detached===void 0&&p.buffer!==i.memory.buffer)&&(p=new DataView(i.memory.buffer)),p}var k=null;function N(){return(k===null||k.byteLength===0)&&(k=new Int32Array(i.memory.buffer)),k}function l(_,e){return me(_>>>0,e)}var W=null;function A(){return(W===null||W.byteLength===0)&&(W=new Uint8Array(i.memory.buffer)),W}function u(_){return _==null}function C(_,e,t){let n={a:_,b:e,cnt:1,instance:s},r=(...f)=>{if(n.instance!==s)throw new Error(\"Cannot invoke closure from previous WASM instance\");n.cnt++;let a=n.a;n.a=0;try{return t(a,n.b,...f)}finally{n.a=a,r._wbg_cb_unref()}};return r._wbg_cb_unref=()=>{--n.cnt===0&&(i.__wbindgen_destroy_closure(n.a,n.b),n.a=0,Q.unregister(n))},Q.register(r,n,n),r}function he(_,e){let t=e(_.length*4,4)>>>0;for(let n=0;n<_.length;n++){let r=g(_[n]);d().setUint32(t+4*n,r,!0)}return y=_.length,t}function S(_,e,t){if(t===void 0){let b=T.encode(_),w=e(b.length,1)>>>0;return A().subarray(w,w+b.length).set(b),y=b.length,w}let n=_.length,r=e(n,1)>>>0,f=A(),a=0;for(;a127)break;f[r+a]=b}if(a!==n){a!==0&&(_=_.slice(a)),r=t(r,n,n=a+_.length*3,1)>>>0;let b=A().subarray(r+a,r+n),w=T.encodeInto(_,b);a+=w.written,r=t(r,n,a,1)>>>0}return y=a,r}var O=!1;function pe(_){let e=i.__wbindgen_externrefs.get(_);return i.__externref_table_dealloc(_),e}var _e=new TextDecoder(\"utf-8\",{ignoreBOM:!0,fatal:!0});_e.decode();function me(_,e){return _e.decode(A().subarray(_,_+e))}var T=new TextEncoder;\"encodeInto\"in T||(T.encodeInto=function(_,e){let t=T.encode(_);return e.set(t),{read:_.length,written:t.length}});var y=0,q=new WebAssembly.Instance(X,te()),i=q.exports;i.__wbindgen_start();Error.stackTraceLimit=100;var h=Y();function $(){h.criticalError&&(console.log(\"Reinitializing Wasm application\"),U(),h.criticalError=!1,h.instanceId++)}addEventListener(\"error\",_=>{H(_.error)});function H(_){_ instanceof WebAssembly.RuntimeError&&(console.error(\"Critical\",_),h.criticalError=!0)}var P=class extends xe{};P.prototype.fetch=function(e){return Z.call(this,e,this.env,this.ctx)};P.prototype.init=ee;var Ie={set:(_,e,t,n)=>Reflect.set(_.instance,e,t,n),has:(_,e)=>Reflect.has(_.instance,e),deleteProperty:(_,e)=>Reflect.deleteProperty(_.instance,e),apply:(_,e,t)=>Reflect.apply(_.instance,e,t),construct:(_,e,t)=>Reflect.construct(_.instance,e,t),getPrototypeOf:_=>Reflect.getPrototypeOf(_.instance),setPrototypeOf:(_,e)=>Reflect.setPrototypeOf(_.instance,e),isExtensible:_=>Reflect.isExtensible(_.instance),preventExtensions:_=>Reflect.preventExtensions(_.instance),getOwnPropertyDescriptor:(_,e)=>Reflect.getOwnPropertyDescriptor(_.instance,e),defineProperty:(_,e,t)=>Reflect.defineProperty(_.instance,e,t),ownKeys:_=>Reflect.ownKeys(_.instance)},v={construct(_,e,t){try{$();let n={instance:Reflect.construct(_,e,t),instanceId:h.instanceId,ctor:_,args:e,newTarget:t};return new Proxy(n,{...Ie,get(r,f,a){r.instanceId!==h.instanceId&&(r.instance=Reflect.construct(r.ctor,r.args,r.newTarget),r.instanceId=h.instanceId);let b=Reflect.get(r.instance,f,a);return typeof b!=\"function\"?b:b.constructor===Function?new Proxy(b,{apply(w,z,M){$();try{return w.apply(z,M)}catch(j){throw H(j),j}}}):new Proxy(b,{async apply(w,z,M){$();try{return await w.apply(z,M)}catch(j){throw H(j),j}}})}})}catch(n){throw h.criticalError=!0,n}}},Se=new Proxy(P,v),ke=new Proxy(x,v),We=new Proxy(I,v),Ae=new Proxy(E,v),Te=new Proxy(R,v),Pe=new Proxy(m,v),Ue=new Proxy(F,v);export{ke as ContainerStartupOptions,We as IntoUnderlyingByteSource,Ae as IntoUnderlyingSink,Te as IntoUnderlyingSource,Pe as MinifyConfig,Ue as R2Range,Se as default};\n" + }, + "worker/shim.mjs": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-loopback/progress-cloudflare/artifacts/build/worker/shim.mjs", + "bytes": 164, + "sha256": "ac99177861405a6046923cbacabbd6adfa1853a56d0c679b9f5e09097c2f4728", + "text": "// Use index.js directly, this file provided for backwards compat\n// with former shim.mjs only.\nexport * from '../index.js';\nexport { default } from '../index.js';\n" + }, + "package.json": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-loopback/progress-cloudflare/artifacts/build/package.json", + "bytes": 211, + "sha256": "d7f0cd6dd39927f703e7ef907b4a12b98f5d7bcca71ef29c4a00b5ca61411a15", + "text": "{\n \"files\": [\n \"index_bg.wasm\",\n \"index.js\",\n \"index.d.ts\"\n ],\n \"main\": \"index.js\",\n \"name\": \"fixture-cloudflare\",\n \"sideEffects\": [\n \"./index.js\"\n ],\n \"type\": \"module\",\n \"version\": \"0.1.0\"\n}" + } + }, + "events": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-loopback/events.jsonl", + "bytes": 89867, + "sha256": "22c46bca697cdefaba9a4add20bbe552d16f4731a2aa9e2ad53cadb6181dc950", + "records": 168, + "jsonl": "{\"construction_rounds\":0,\"event\":\"run\",\"max_requests\":10,\"seed\":856,\"suite\":\"outbound-progress\"}\n{\"event\":\"build_environment\",\"host_arch\":\"aarch64\",\"host_os\":\"macos\",\"lockfile\":{\"bytes\":84127,\"fnv1a64\":\"10d93fc12c84db43\",\"path\":\"/private/tmp/edgezero-cloudflare-progress-20261020-loopback/Cargo.lock\",\"purpose\":\"non-security build identity\"},\"rustc\":\"rustc 1.98.1 (48a229cea 2026-09-01)\"}\n{\"adapter\":\"cloudflare\",\"event\":\"progress-runtime\",\"executable\":\"/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js\",\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"source_state\":\"working-tree-characterization\",\"version\":\"4.131.0\"}\n{\"event\":\"worker-build-tool\",\"executable\":\"/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen\",\"variable\":\"WASM_BINDGEN_BIN\",\"version\":\"wasm-bindgen 0.2.129\"}\n{\"event\":\"worker-build-tool\",\"executable\":\"/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild\",\"variable\":\"ESBUILD_BIN\",\"version\":\"0.28.1\"}\n{\"event\":\"worker-build-tool\",\"executable\":\"/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt\",\"variable\":\"WASM_OPT_BIN\",\"version\":\"wasm-opt version 130 (version_130)\"}\n{\"adapter\":\"cloudflare\",\"artifact\":{\"bytes\":1691011,\"fnv1a64\":\"559ae66b5e1e3cc2\",\"path\":\"/private/tmp/edgezero-cloudflare-progress-20261020-loopback/progress-cloudflare/artifacts/build/index_bg.wasm\",\"purpose\":\"non-security build identity\"},\"event\":\"build\",\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-buffered-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-200-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":102338625,\"event\":\"client_completed\",\"first_byte_ns\":102077250,\"header_ns\":102055875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35cc1bf9e8-462f-0\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-buffered-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-206-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":5242458,\"event\":\"client_completed\",\"first_byte_ns\":5206375,\"header_ns\":5201375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d23ecad0-462f-1\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-buffered-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12105500,\"event\":\"client_completed\",\"first_byte_ns\":12073125,\"header_ns\":12054417,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d2952060-462f-2\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-buffered-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-gzip-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12900375,\"event\":\"client_completed\",\"first_byte_ns\":12875500,\"header_ns\":12872167,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d353f008-462f-3\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-streamed-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-200-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11472125,\"event\":\"client_completed\",\"first_byte_ns\":11432583,\"header_ns\":11428542,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d41f0450-462f-4\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-streamed-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-206-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11460458,\"event\":\"client_completed\",\"first_byte_ns\":11435041,\"header_ns\":11432750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d4d69c50-462f-5\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-streamed-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":10794625,\"event\":\"client_completed\",\"first_byte_ns\":10761208,\"header_ns\":10757333,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d58be678-462f-6\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-streamed-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-gzip-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12008583,\"event\":\"client_completed\",\"first_byte_ns\":11971750,\"header_ns\":11970583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d636e000-462f-7\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-buffered-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-200-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10370250,\"event\":\"client_completed\",\"first_byte_ns\":10340125,\"header_ns\":10338583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d6f3f640-462f-8\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-buffered-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-206-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11579792,\"event\":\"client_completed\",\"first_byte_ns\":11543959,\"header_ns\":11539667,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d7988ef8-462f-9\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-buffered-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12263041,\"event\":\"client_completed\",\"first_byte_ns\":12250083,\"header_ns\":12248125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d8501758-462f-a\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-buffered-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-deflate-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12359417,\"event\":\"client_completed\",\"first_byte_ns\":12321334,\"header_ns\":12319334,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d910ee88-462f-b\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-streamed-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-200-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12498208,\"event\":\"client_completed\",\"first_byte_ns\":12461958,\"header_ns\":12457958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d9d3c188-462f-c\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-streamed-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-206-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":10078625,\"event\":\"client_completed\",\"first_byte_ns\":10041458,\"header_ns\":10036958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35da994020-462f-d\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-streamed-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11920167,\"event\":\"client_completed\",\"first_byte_ns\":11886833,\"header_ns\":11884042,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35db3a42e0-462f-e\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-streamed-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-deflate-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12000583,\"event\":\"client_completed\",\"first_byte_ns\":11975583,\"header_ns\":11973291,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35dbf87e18-462f-f\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-buffered-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-200-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12327667,\"event\":\"client_completed\",\"first_byte_ns\":12107667,\"header_ns\":12104625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35dcb5d6c0-462f-10\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-buffered-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-206-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11637833,\"event\":\"client_completed\",\"first_byte_ns\":11610291,\"header_ns\":11606875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35dd7aad60-462f-11\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-buffered-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11941708,\"event\":\"client_completed\",\"first_byte_ns\":11931125,\"header_ns\":11927625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35de3231d8-462f-12\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-buffered-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-br-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12098875,\"event\":\"client_completed\",\"first_byte_ns\":12086875,\"header_ns\":12081250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35deef3490-462f-13\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-streamed-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-200-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10647666,\"event\":\"client_completed\",\"first_byte_ns\":10638500,\"header_ns\":10635625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35dfafd128-462f-14\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-streamed-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-206-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12742958,\"event\":\"client_completed\",\"first_byte_ns\":12620875,\"header_ns\":12268125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e0598a60-462f-15\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-streamed-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11734708,\"event\":\"client_completed\",\"first_byte_ns\":11723333,\"header_ns\":11717208,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e1227fb0-462f-16\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-streamed-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-br-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10933167,\"event\":\"client_completed\",\"first_byte_ns\":10907583,\"header_ns\":10903625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e1dc59d0-462f-17\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-absent-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12013167,\"event\":\"client_completed\",\"first_byte_ns\":11987625,\"header_ns\":11984042,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e3176df8-462f-18\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-absent-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10598833,\"event\":\"client_completed\",\"first_byte_ns\":10574250,\"header_ns\":10572750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e3d4ca88-462f-19\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-absent-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11644459,\"event\":\"client_completed\",\"first_byte_ns\":11611042,\"header_ns\":11609625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e47cf938-462f-1a\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-upstream-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10036958,\"event\":\"client_completed\",\"first_byte_ns\":10007167,\"header_ns\":10005667,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e5396f50-462f-1b\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-upstream-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12256417,\"event\":\"client_completed\",\"first_byte_ns\":12167583,\"header_ns\":12163833,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e5da6e28-462f-1c\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-upstream-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11784916,\"event\":\"client_completed\",\"first_byte_ns\":11771458,\"header_ns\":11769416,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e69e8d30-462f-1d\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-nominated-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11820250,\"event\":\"client_completed\",\"first_byte_ns\":11784792,\"header_ns\":11781292,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e75abcf8-462f-1e\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-nominated-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":9712750,\"event\":\"client_completed\",\"first_byte_ns\":9676084,\"header_ns\":9673084,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e81798a0-462f-1f\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-nominated-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11948583,\"event\":\"client_completed\",\"first_byte_ns\":11904208,\"header_ns\":11900625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e8b426c0-462f-20\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-absent-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11745000,\"event\":\"client_completed\",\"first_byte_ns\":11697958,\"header_ns\":11694833,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e9745dc8-462f-21\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-absent-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11957875,\"event\":\"client_completed\",\"first_byte_ns\":11916542,\"header_ns\":11912834,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ea2fe980-462f-22\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-absent-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12956208,\"event\":\"client_completed\",\"first_byte_ns\":12793958,\"header_ns\":11875208,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35eaeef3c0-462f-23\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-upstream-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11183917,\"event\":\"client_completed\",\"first_byte_ns\":11102375,\"header_ns\":11101083,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ebbe22d0-462f-24\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-upstream-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":9119291,\"event\":\"client_completed\",\"first_byte_ns\":9029666,\"header_ns\":9024583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ec724be8-462f-25\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-upstream-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11956792,\"event\":\"client_completed\",\"first_byte_ns\":11928750,\"header_ns\":11925750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ed06bfd0-462f-26\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-nominated-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11448667,\"event\":\"client_completed\",\"first_byte_ns\":11367042,\"header_ns\":11365584,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35edc83b10-462f-27\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-nominated-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":10996500,\"event\":\"client_completed\",\"first_byte_ns\":10973667,\"header_ns\":10969667,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ee806398-462f-28\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-nominated-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":9577042,\"event\":\"client_completed\",\"first_byte_ns\":9493292,\"header_ns\":9489833,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ef371cf0-462f-29\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-absent-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11995916,\"event\":\"client_completed\",\"first_byte_ns\":11958583,\"header_ns\":11954541,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35efd0d480-462f-2a\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-absent-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11774791,\"event\":\"client_completed\",\"first_byte_ns\":11690250,\"header_ns\":11688750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f08f3e98-462f-2b\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-absent-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11466500,\"event\":\"client_completed\",\"first_byte_ns\":11406792,\"header_ns\":11403875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f14a0318-462f-2c\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-upstream-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12222709,\"event\":\"client_completed\",\"first_byte_ns\":12192709,\"header_ns\":12191250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f1ff7068-462f-2d\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-upstream-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12338542,\"event\":\"client_completed\",\"first_byte_ns\":11901584,\"header_ns\":11900667,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f2c0c0b0-462f-2e\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-upstream-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11683500,\"event\":\"client_completed\",\"first_byte_ns\":11635166,\"header_ns\":11634125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f3834d60-462f-2f\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-nominated-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11890250,\"event\":\"client_completed\",\"first_byte_ns\":11835834,\"header_ns\":11832875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f43bf6d0-462f-30\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-nominated-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":9803500,\"event\":\"client_completed\",\"first_byte_ns\":9736334,\"header_ns\":9734875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f4f82e68-462f-31\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-nominated-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12186458,\"event\":\"client_completed\",\"first_byte_ns\":12021625,\"header_ns\":12020208,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f5954d10-462f-32\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-absent-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11718541,\"event\":\"client_completed\",\"first_byte_ns\":11655791,\"header_ns\":11649625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f6581070-462f-33\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-absent-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":9704750,\"event\":\"client_completed\",\"first_byte_ns\":9662208,\"header_ns\":9657750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f7121d58-462f-34\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-absent-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11285375,\"event\":\"client_completed\",\"first_byte_ns\":11247750,\"header_ns\":11245375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f7ad3c48-462f-35\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-upstream-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10217208,\"event\":\"client_completed\",\"first_byte_ns\":10130375,\"header_ns\":10126000,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f860adc8-462f-36\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-upstream-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10717083,\"event\":\"client_completed\",\"first_byte_ns\":10650667,\"header_ns\":10647292,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f903cf80-462f-37\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-upstream-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10185750,\"event\":\"client_completed\",\"first_byte_ns\":10143917,\"header_ns\":10141792,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f9b018f8-462f-38\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-nominated-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11891583,\"event\":\"client_completed\",\"first_byte_ns\":11820416,\"header_ns\":11817666,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fa52b9c8-462f-39\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-nominated-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":12117583,\"event\":\"client_completed\",\"first_byte_ns\":12095833,\"header_ns\":12091583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fb0ee5a8-462f-3a\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-nominated-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11083542,\"event\":\"client_completed\",\"first_byte_ns\":11026750,\"header_ns\":11021208,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fbcfc0c0-462f-3b\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-absent-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12011833,\"event\":\"client_completed\",\"first_byte_ns\":11956958,\"header_ns\":11953250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fc7ef838-462f-3c\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-absent-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12096208,\"event\":\"client_completed\",\"first_byte_ns\":12064958,\"header_ns\":12061375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fd3c83a8-462f-3d\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-absent-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12182625,\"event\":\"client_completed\",\"first_byte_ns\":12148417,\"header_ns\":12147250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fdfb9d88-462f-3e\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-upstream-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12189792,\"event\":\"client_completed\",\"first_byte_ns\":12100625,\"header_ns\":12096917,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35febbfba0-462f-3f\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-upstream-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12116250,\"event\":\"client_completed\",\"first_byte_ns\":12077625,\"header_ns\":12074583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ff7ce658-462f-40\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-upstream-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":11716625,\"event\":\"client_completed\",\"first_byte_ns\":11691250,\"header_ns\":11689667,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3600440eb8-462f-41\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-nominated-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":11946625,\"event\":\"client_completed\",\"first_byte_ns\":11905542,\"header_ns\":11901959,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3600fe7d48-462f-42\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-nominated-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12018458,\"event\":\"client_completed\",\"first_byte_ns\":11965916,\"header_ns\":11962791,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3601bc1c40-462f-43\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-nominated-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":10978291,\"event\":\"client_completed\",\"first_byte_ns\":10931250,\"header_ns\":10929708,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c36027a3c20-462f-44\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-batch\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-batch\\\",\\\"markers\\\":[null,\\\"cloudflare\\\"],\\\"qualification\\\":false}\",\"complete_ns\":12337792,\"event\":\"client_completed\",\"first_byte_ns\":12308084,\"header_ns\":12307167,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3603289108-462f-45\"},\"error\":null,\"event\":\"outbound-marker\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"eager-single\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"eager-single\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":1,\\\"token\\\":\\\"18e05c3604927978-462f-46\\\"}\",\"complete_ns\":332302375,\"event\":\"client_completed\",\"first_byte_ns\":331987250,\"header_ns\":330006125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3604927978-462f-46\"},\"event\":\"outbound-progress\",\"probe\":{\"adapter\":\"cloudflare\",\"case\":\"eager-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":101000000,\"protocol\":\"http1\",\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e05c3604927978-462f-46\"},\"qualification\":false,\"receipts\":[{\"path\":\"/g3/18e05c3604927978-462f-46/start\",\"received_ns\":12616333},{\"path\":\"/g3/18e05c3604927978-462f-46/slot-0\",\"received_ns\":25187375},{\"path\":\"/g3/18e05c3604927978-462f-46/end\",\"received_ns\":119261416}],\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"lazy-single\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"lazy-single\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":102000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":1,\\\"token\\\":\\\"18e05c3618960660-462f-47\\\"}\",\"complete_ns\":439124750,\"event\":\"client_completed\",\"first_byte_ns\":439105958,\"header_ns\":439102375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3618960660-462f-47\"},\"event\":\"outbound-progress\",\"probe\":{\"adapter\":\"cloudflare\",\"case\":\"lazy-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":102000000,\"protocol\":\"http1\",\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e05c3618960660-462f-47\"},\"qualification\":false,\"receipts\":[{\"path\":\"/g3/18e05c3618960660-462f-47/start\",\"received_ns\":12510167},{\"path\":\"/g3/18e05c3618960660-462f-47/end\",\"received_ns\":119342459},{\"path\":\"/g3/18e05c3618960660-462f-47/slot-0\",\"received_ns\":131442042}],\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"eager-batch\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"eager-batch\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0,0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c363343f350-462f-48\\\"}\",\"complete_ns\":331695917,\"event\":\"client_completed\",\"first_byte_ns\":331512458,\"header_ns\":331510167,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c363343f350-462f-48\"},\"event\":\"outbound-progress\",\"probe\":{\"adapter\":\"cloudflare\",\"case\":\"eager-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":101000000,\"protocol\":\"http1\",\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e05c363343f350-462f-48\"},\"qualification\":false,\"receipts\":[{\"path\":\"/g3/18e05c363343f350-462f-48/start\",\"received_ns\":13333917},{\"path\":\"/g3/18e05c363343f350-462f-48/slot-1\",\"received_ns\":25651000},{\"path\":\"/g3/18e05c363343f350-462f-48/slot-0\",\"received_ns\":25659875},{\"path\":\"/g3/18e05c363343f350-462f-48/end\",\"received_ns\":122348250}],\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"baseline-batch\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"baseline-batch\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c3647d8b030-462f-49\\\"}\",\"complete_ns\":435106792,\"event\":\"client_completed\",\"first_byte_ns\":434598667,\"header_ns\":434596042,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3647d8b030-462f-49\"},\"event\":\"outbound-progress\",\"probe\":{\"adapter\":\"cloudflare\",\"case\":\"baseline-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":101000000,\"protocol\":\"http1\",\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":2,\"token\":\"18e05c3647d8b030-462f-49\"},\"qualification\":false,\"receipts\":[{\"path\":\"/g3/18e05c3647d8b030-462f-49/start\",\"received_ns\":10529250},{\"path\":\"/g3/18e05c3647d8b030-462f-49/end\",\"received_ns\":115432916},{\"path\":\"/g3/18e05c3647d8b030-462f-49/slot-0\",\"received_ns\":127248833},{\"path\":\"/g3/18e05c3647d8b030-462f-49/slot-1\",\"received_ns\":127257208}],\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"response-owned\",\"client\":{\"body\":\" {\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"response-owned\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":true,\\\"handler_returned\\\":true,\\\"pause_ns\\\":100000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0,0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c36622bdac0-462f-4a\\\"}\",\"complete_ns\":330084416,\"event\":\"client_completed\",\"first_byte_ns\":13768916,\"header_ns\":13069291,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c36622bdac0-462f-4a\"},\"event\":\"outbound-progress\",\"probe\":{\"adapter\":\"cloudflare\",\"case\":\"response-owned\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":true,\"handler_returned\":true,\"pause_ns\":100000000,\"protocol\":\"http1\",\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e05c36622bdac0-462f-4a\"},\"qualification\":false,\"receipts\":[{\"path\":\"/g3/18e05c36622bdac0-462f-4a/start\",\"received_ns\":11318458},{\"path\":\"/g3/18e05c36622bdac0-462f-4a/slot-0\",\"received_ns\":23973750},{\"path\":\"/g3/18e05c36622bdac0-462f-4a/slot-1\",\"received_ns\":24005417},{\"path\":\"/g3/18e05c36622bdac0-462f-4a/end\",\"received_ns\":118031708}],\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":true,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":5623834,\"event\":\"client_completed\",\"first_byte_ns\":5485750,\"header_ns\":5482334,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c367677c318-462f-4b\"},\"event\":\"inbound-empty-signal\",\"framing\":\"Absent\",\"probe\":{\"adapter\":\"cloudflare\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"body_absent\":true,\"source\":\"workerd-before-conversion\"},\"method\":\"GET\",\"qualification\":false},\"protocol\":\"http1\",\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":true,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":5071458,\"event\":\"client_completed\",\"first_byte_ns\":4758542,\"header_ns\":4755875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3676e73270-462f-4c\"},\"event\":\"inbound-empty-signal\",\"framing\":\"LengthZero\",\"probe\":{\"adapter\":\"cloudflare\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"body_absent\":true,\"source\":\"workerd-before-conversion\"},\"method\":\"GET\",\"qualification\":false},\"protocol\":\"http1\",\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":3426416,\"event\":\"client_completed\",\"first_byte_ns\":3405208,\"header_ns\":3402458,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c36774c6898-462f-4d\"},\"event\":\"inbound-empty-signal\",\"framing\":\"ChunkedEmpty\",\"probe\":{\"adapter\":\"cloudflare\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"body_absent\":false,\"source\":\"workerd-before-conversion\"},\"method\":\"GET\",\"qualification\":false},\"protocol\":\"http1\",\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":4,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":3681542,\"event\":\"client_completed\",\"first_byte_ns\":3453583,\"header_ns\":3147708,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c36778f3240-462f-4e\"},\"event\":\"inbound-empty-signal\",\"framing\":\"FixedBody\",\"probe\":{\"adapter\":\"cloudflare\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"body_absent\":false,\"source\":\"workerd-before-conversion\"},\"method\":\"GET\",\"qualification\":false},\"protocol\":\"http1\",\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":4,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":2460500,\"event\":\"client_completed\",\"first_byte_ns\":2447292,\"header_ns\":2445000,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3677dbf2b0-462f-4f\"},\"event\":\"inbound-empty-signal\",\"framing\":\"ChunkedBody\",\"probe\":{\"adapter\":\"cloudflare\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"body_absent\":false,\"source\":\"workerd-before-conversion\"},\"method\":\"GET\",\"qualification\":false},\"protocol\":\"http1\",\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"attempted_empty_body_cases\":5,\"attempted_progress_cases\":5,\"empty_body_passes\":5,\"event\":\"progress-coverage\",\"failures\":[],\"limitations\":[\"HTTP/2 and HTTP/3 not exercised\",\"hosted execution pending\",\"response-owned tests handler/dispatch return, not host callback return\",\"missing-context detection unproved except native absent-runtime control\",\"no cancellation or G2 readiness qualification\"],\"origin_requests\":18,\"progress_passes\":5,\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-200-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":102338625,\"event\":\"client_completed\",\"first_byte_ns\":102077250,\"header_ns\":102055875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35cc1bf9e8-462f-0\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-206-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":5242458,\"event\":\"client_completed\",\"first_byte_ns\":5206375,\"header_ns\":5201375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d23ecad0-462f-1\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12105500,\"event\":\"client_completed\",\"first_byte_ns\":12073125,\"header_ns\":12054417,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d2952060-462f-2\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-gzip-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12900375,\"event\":\"client_completed\",\"first_byte_ns\":12875500,\"header_ns\":12872167,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d353f008-462f-3\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-200-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11472125,\"event\":\"client_completed\",\"first_byte_ns\":11432583,\"header_ns\":11428542,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d41f0450-462f-4\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-206-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11460458,\"event\":\"client_completed\",\"first_byte_ns\":11435041,\"header_ns\":11432750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d4d69c50-462f-5\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":10794625,\"event\":\"client_completed\",\"first_byte_ns\":10761208,\"header_ns\":10757333,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d58be678-462f-6\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-gzip-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12008583,\"event\":\"client_completed\",\"first_byte_ns\":11971750,\"header_ns\":11970583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d636e000-462f-7\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-200-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10370250,\"event\":\"client_completed\",\"first_byte_ns\":10340125,\"header_ns\":10338583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d6f3f640-462f-8\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-206-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11579792,\"event\":\"client_completed\",\"first_byte_ns\":11543959,\"header_ns\":11539667,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d7988ef8-462f-9\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12263041,\"event\":\"client_completed\",\"first_byte_ns\":12250083,\"header_ns\":12248125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d8501758-462f-a\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-deflate-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12359417,\"event\":\"client_completed\",\"first_byte_ns\":12321334,\"header_ns\":12319334,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d910ee88-462f-b\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-200-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12498208,\"event\":\"client_completed\",\"first_byte_ns\":12461958,\"header_ns\":12457958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35d9d3c188-462f-c\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-206-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":10078625,\"event\":\"client_completed\",\"first_byte_ns\":10041458,\"header_ns\":10036958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35da994020-462f-d\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11920167,\"event\":\"client_completed\",\"first_byte_ns\":11886833,\"header_ns\":11884042,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35db3a42e0-462f-e\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-deflate-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12000583,\"event\":\"client_completed\",\"first_byte_ns\":11975583,\"header_ns\":11973291,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35dbf87e18-462f-f\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-200-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12327667,\"event\":\"client_completed\",\"first_byte_ns\":12107667,\"header_ns\":12104625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35dcb5d6c0-462f-10\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-206-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11637833,\"event\":\"client_completed\",\"first_byte_ns\":11610291,\"header_ns\":11606875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35dd7aad60-462f-11\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11941708,\"event\":\"client_completed\",\"first_byte_ns\":11931125,\"header_ns\":11927625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35de3231d8-462f-12\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-br-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12098875,\"event\":\"client_completed\",\"first_byte_ns\":12086875,\"header_ns\":12081250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35deef3490-462f-13\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-200-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10647666,\"event\":\"client_completed\",\"first_byte_ns\":10638500,\"header_ns\":10635625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35dfafd128-462f-14\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-206-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12742958,\"event\":\"client_completed\",\"first_byte_ns\":12620875,\"header_ns\":12268125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e0598a60-462f-15\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11734708,\"event\":\"client_completed\",\"first_byte_ns\":11723333,\"header_ns\":11717208,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e1227fb0-462f-16\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-br-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10933167,\"event\":\"client_completed\",\"first_byte_ns\":10907583,\"header_ns\":10903625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e1dc59d0-462f-17\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12013167,\"event\":\"client_completed\",\"first_byte_ns\":11987625,\"header_ns\":11984042,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e3176df8-462f-18\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10598833,\"event\":\"client_completed\",\"first_byte_ns\":10574250,\"header_ns\":10572750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e3d4ca88-462f-19\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11644459,\"event\":\"client_completed\",\"first_byte_ns\":11611042,\"header_ns\":11609625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e47cf938-462f-1a\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10036958,\"event\":\"client_completed\",\"first_byte_ns\":10007167,\"header_ns\":10005667,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e5396f50-462f-1b\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12256417,\"event\":\"client_completed\",\"first_byte_ns\":12167583,\"header_ns\":12163833,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e5da6e28-462f-1c\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11784916,\"event\":\"client_completed\",\"first_byte_ns\":11771458,\"header_ns\":11769416,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e69e8d30-462f-1d\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11820250,\"event\":\"client_completed\",\"first_byte_ns\":11784792,\"header_ns\":11781292,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e75abcf8-462f-1e\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":9712750,\"event\":\"client_completed\",\"first_byte_ns\":9676084,\"header_ns\":9673084,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e81798a0-462f-1f\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11948583,\"event\":\"client_completed\",\"first_byte_ns\":11904208,\"header_ns\":11900625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e8b426c0-462f-20\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11745000,\"event\":\"client_completed\",\"first_byte_ns\":11697958,\"header_ns\":11694833,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35e9745dc8-462f-21\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11957875,\"event\":\"client_completed\",\"first_byte_ns\":11916542,\"header_ns\":11912834,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ea2fe980-462f-22\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12956208,\"event\":\"client_completed\",\"first_byte_ns\":12793958,\"header_ns\":11875208,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35eaeef3c0-462f-23\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11183917,\"event\":\"client_completed\",\"first_byte_ns\":11102375,\"header_ns\":11101083,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ebbe22d0-462f-24\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":9119291,\"event\":\"client_completed\",\"first_byte_ns\":9029666,\"header_ns\":9024583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ec724be8-462f-25\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11956792,\"event\":\"client_completed\",\"first_byte_ns\":11928750,\"header_ns\":11925750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ed06bfd0-462f-26\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11448667,\"event\":\"client_completed\",\"first_byte_ns\":11367042,\"header_ns\":11365584,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35edc83b10-462f-27\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":10996500,\"event\":\"client_completed\",\"first_byte_ns\":10973667,\"header_ns\":10969667,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ee806398-462f-28\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":9577042,\"event\":\"client_completed\",\"first_byte_ns\":9493292,\"header_ns\":9489833,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ef371cf0-462f-29\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11995916,\"event\":\"client_completed\",\"first_byte_ns\":11958583,\"header_ns\":11954541,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35efd0d480-462f-2a\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11774791,\"event\":\"client_completed\",\"first_byte_ns\":11690250,\"header_ns\":11688750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f08f3e98-462f-2b\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11466500,\"event\":\"client_completed\",\"first_byte_ns\":11406792,\"header_ns\":11403875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f14a0318-462f-2c\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12222709,\"event\":\"client_completed\",\"first_byte_ns\":12192709,\"header_ns\":12191250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f1ff7068-462f-2d\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12338542,\"event\":\"client_completed\",\"first_byte_ns\":11901584,\"header_ns\":11900667,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f2c0c0b0-462f-2e\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11683500,\"event\":\"client_completed\",\"first_byte_ns\":11635166,\"header_ns\":11634125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f3834d60-462f-2f\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11890250,\"event\":\"client_completed\",\"first_byte_ns\":11835834,\"header_ns\":11832875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f43bf6d0-462f-30\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":9803500,\"event\":\"client_completed\",\"first_byte_ns\":9736334,\"header_ns\":9734875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f4f82e68-462f-31\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12186458,\"event\":\"client_completed\",\"first_byte_ns\":12021625,\"header_ns\":12020208,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f5954d10-462f-32\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11718541,\"event\":\"client_completed\",\"first_byte_ns\":11655791,\"header_ns\":11649625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f6581070-462f-33\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":9704750,\"event\":\"client_completed\",\"first_byte_ns\":9662208,\"header_ns\":9657750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f7121d58-462f-34\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11285375,\"event\":\"client_completed\",\"first_byte_ns\":11247750,\"header_ns\":11245375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f7ad3c48-462f-35\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10217208,\"event\":\"client_completed\",\"first_byte_ns\":10130375,\"header_ns\":10126000,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f860adc8-462f-36\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10717083,\"event\":\"client_completed\",\"first_byte_ns\":10650667,\"header_ns\":10647292,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f903cf80-462f-37\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10185750,\"event\":\"client_completed\",\"first_byte_ns\":10143917,\"header_ns\":10141792,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35f9b018f8-462f-38\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11891583,\"event\":\"client_completed\",\"first_byte_ns\":11820416,\"header_ns\":11817666,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fa52b9c8-462f-39\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":12117583,\"event\":\"client_completed\",\"first_byte_ns\":12095833,\"header_ns\":12091583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fb0ee5a8-462f-3a\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11083542,\"event\":\"client_completed\",\"first_byte_ns\":11026750,\"header_ns\":11021208,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fbcfc0c0-462f-3b\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12011833,\"event\":\"client_completed\",\"first_byte_ns\":11956958,\"header_ns\":11953250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fc7ef838-462f-3c\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12096208,\"event\":\"client_completed\",\"first_byte_ns\":12064958,\"header_ns\":12061375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fd3c83a8-462f-3d\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12182625,\"event\":\"client_completed\",\"first_byte_ns\":12148417,\"header_ns\":12147250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35fdfb9d88-462f-3e\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12189792,\"event\":\"client_completed\",\"first_byte_ns\":12100625,\"header_ns\":12096917,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35febbfba0-462f-3f\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12116250,\"event\":\"client_completed\",\"first_byte_ns\":12077625,\"header_ns\":12074583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c35ff7ce658-462f-40\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":11716625,\"event\":\"client_completed\",\"first_byte_ns\":11691250,\"header_ns\":11689667,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3600440eb8-462f-41\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":11946625,\"event\":\"client_completed\",\"first_byte_ns\":11905542,\"header_ns\":11901959,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3600fe7d48-462f-42\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12018458,\"event\":\"client_completed\",\"first_byte_ns\":11965916,\"header_ns\":11962791,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3601bc1c40-462f-43\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":10978291,\"event\":\"client_completed\",\"first_byte_ns\":10931250,\"header_ns\":10929708,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c36027a3c20-462f-44\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-batch\\\",\\\"markers\\\":[null,\\\"cloudflare\\\"],\\\"qualification\\\":false}\",\"complete_ns\":12337792,\"event\":\"client_completed\",\"first_byte_ns\":12308084,\"header_ns\":12307167,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3603289108-462f-45\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"eager-single\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":1,\\\"token\\\":\\\"18e05c3604927978-462f-46\\\"}\",\"complete_ns\":332302375,\"event\":\"client_completed\",\"first_byte_ns\":331987250,\"header_ns\":330006125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3604927978-462f-46\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"lazy-single\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":102000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":1,\\\"token\\\":\\\"18e05c3618960660-462f-47\\\"}\",\"complete_ns\":439124750,\"event\":\"client_completed\",\"first_byte_ns\":439105958,\"header_ns\":439102375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3618960660-462f-47\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"eager-batch\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0,0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c363343f350-462f-48\\\"}\",\"complete_ns\":331695917,\"event\":\"client_completed\",\"first_byte_ns\":331512458,\"header_ns\":331510167,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c363343f350-462f-48\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"baseline-batch\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c3647d8b030-462f-49\\\"}\",\"complete_ns\":435106792,\"event\":\"client_completed\",\"first_byte_ns\":434598667,\"header_ns\":434596042,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3647d8b030-462f-49\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\" {\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"response-owned\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":true,\\\"handler_returned\\\":true,\\\"pause_ns\\\":100000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0,0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c36622bdac0-462f-4a\\\"}\",\"complete_ns\":330084416,\"event\":\"client_completed\",\"first_byte_ns\":13768916,\"header_ns\":13069291,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c36622bdac0-462f-4a\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":true,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":5623834,\"event\":\"client_completed\",\"first_byte_ns\":5485750,\"header_ns\":5482334,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c367677c318-462f-4b\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":true,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":5071458,\"event\":\"client_completed\",\"first_byte_ns\":4758542,\"header_ns\":4755875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3676e73270-462f-4c\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":3426416,\"event\":\"client_completed\",\"first_byte_ns\":3405208,\"header_ns\":3402458,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c36774c6898-462f-4d\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":4,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":3681542,\"event\":\"client_completed\",\"first_byte_ns\":3453583,\"header_ns\":3147708,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c36778f3240-462f-4e\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":4,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":2460500,\"event\":\"client_completed\",\"first_byte_ns\":2447292,\"header_ns\":2445000,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c3677dbf2b0-462f-4f\",\"variant\":\"cloudflare-outbound-progress\"}\n" + }, + "summary": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-loopback/summary.json", + "bytes": 303, + "sha256": "5d0a157d796b357d15561997480a12cc58f9dede469570dfbe91f016cab2ab9f", + "text": "{\n \"build_identity\": {\n \"status\": \"consistent\"\n },\n \"cpu_comparison\": \"SDK phase readings only; cross-run performance unverified\",\n \"guests\": [],\n \"memory_snapshots\": {},\n \"quantile_method\": \"nearest rank\",\n \"recorded_client_completions\": 80,\n \"recorded_request_starts\": 0,\n \"variants\": {}\n}" + }, + "runtime_log": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-loopback/progress-cloudflare/runtime.log", + "bytes": 8265, + "sha256": "6331616b033cd2061a6cb0a4e2ad56a64a840e978648b951259e4430aeec82cc", + "text": "\n \u26c5\ufe0f wrangler 4.131.0 (update available 4.151.3)\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCloudflare collects anonymous telemetry about your usage of Wrangler. Learn more at https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler/telemetry.md\nYour Worker has access to the following bindings:\nBinding Resource Mode\nenv.fixture_config (00000000000000000000000000000001) KV Namespace local\nenv.fixture_kv (00000000000000000000000000000002) KV Namespace local\nenv.FIXTURE_MODE (\"(hidden)\") Environment Variable local\nenv.fixture_marker (\"fixture-only-value\") Environment Variable local\n\n\u2394 Starting local server...\nWrangler detected this dev session is running in an AI agent.\nThe Local Explorer API is available at http://127.0.0.1:59785/cdn-cgi/local/explorer/api\nUseful routes:\n GET http://127.0.0.1:59785/cdn-cgi/local/explorer/api/local/workers - local Workers and bindings\n GET http://127.0.0.1:59785/cdn-cgi/local/explorer/api/storage/kv/namespaces - KV namespaces\n GET http://127.0.0.1:59785/cdn-cgi/local/explorer/api/d1/database - D1 databases\n GET http://127.0.0.1:59785/cdn-cgi/local/explorer/api/r2/buckets - R2 buckets\n GET http://127.0.0.1:59785/cdn-cgi/local/explorer/api/workers/durable_objects/namespaces - Durable Object namespaces\n GET http://127.0.0.1:59785/cdn-cgi/local/explorer/api/workflows - Workflows\n POST http://127.0.0.1:59785/cdn-cgi/local/explorer/api/local/observability/query - run a read-only SQL query (SELECT/WITH only) over captured request traces and console logs. Tables: spans, logs (read attributes via json(attributes)). Example:\n curl -X POST http://127.0.0.1:59785/cdn-cgi/local/explorer/api/local/observability/query -H 'Content-Type: application/json' -d '{\"sql\":\"SELECT service, name, outcome, duration_ms FROM spans WHERE parent_id IS NULL LIMIT 20\"}'\nIf the routes above don't cover what you need, fetch the full OpenAPI schema (large - use only as a last resort):\n GET http://127.0.0.1:59785/cdn-cgi/local/explorer/api - OpenAPI schema\n\u001b[33m\u25b2 \u001b[43;33m[\u001b[43;30mWARNING\u001b[43;33m]\u001b[0m \u001b[1mservice core:user:reusable-app-fixture: The compatibility flag enable_weak_ref became the default as of 2025-05-05 so does not need to be specified anymore.\u001b[0m\n\n\n[wrangler:info] Ready on http://127.0.0.1:59785\n[wrangler:info] GET /progress/coding-gzip-buffered-200-original 200 OK (102ms)\n[wrangler:info] GET /progress/coding-gzip-buffered-206-original 200 OK (4ms)\n[wrangler:info] GET /progress/coding-gzip-buffered-206-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-gzip-buffered-200-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-200-original 200 OK (11ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-206-original 200 OK (11ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-206-decode 200 OK (10ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-200-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-200-original 200 OK (11ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-206-original 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-206-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-200-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-200-original 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-206-original 200 OK (9ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-206-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-200-decode 200 OK (11ms)\n[wrangler:info] GET /progress/coding-br-buffered-200-original 200 OK (12ms)\n[wrangler:info] GET /progress/coding-br-buffered-206-original 200 OK (11ms)\n[wrangler:info] GET /progress/coding-br-buffered-206-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-br-buffered-200-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-br-streamed-200-original 200 OK (10ms)\n[wrangler:info] GET /progress/coding-br-streamed-206-original 200 OK (12ms)\n[wrangler:info] GET /progress/coding-br-streamed-206-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-br-streamed-200-decode 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-200-absent-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-200-absent-on 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-200-absent-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-200-upstream-inherit 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-200-upstream-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-200-upstream-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-200-nominated-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-200-nominated-on 200 OK (9ms)\n[wrangler:info] GET /progress/marker-get-200-nominated-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-204-absent-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-204-absent-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-204-absent-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-204-upstream-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-204-upstream-on 200 OK (9ms)\n[wrangler:info] GET /progress/marker-get-204-upstream-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-204-nominated-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-204-nominated-on 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-204-nominated-off 200 OK (9ms)\n[wrangler:info] GET /progress/marker-head-200-absent-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-head-200-absent-on 200 OK (11ms)\n[wrangler:info] GET /progress/marker-head-200-absent-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-head-200-upstream-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-head-200-upstream-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-head-200-upstream-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-head-200-nominated-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-head-200-nominated-on 200 OK (9ms)\n[wrangler:info] GET /progress/marker-head-200-nominated-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-304-absent-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-304-absent-on 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-304-absent-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-304-upstream-inherit 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-304-upstream-on 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-304-upstream-off 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-304-nominated-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-304-nominated-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-304-nominated-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-302-absent-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-302-absent-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-302-absent-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-302-upstream-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-302-upstream-on 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-302-upstream-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-302-nominated-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-302-nominated-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-302-nominated-off 200 OK (10ms)\n[wrangler:info] GET /progress/marker-batch 200 OK (12ms)\n[wrangler:info] GET /progress/eager-single 200 OK (329ms)\n[wrangler:info] GET /progress/lazy-single 200 OK (438ms)\n[wrangler:info] GET /progress/eager-batch 200 OK (331ms)\n[wrangler:info] GET /progress/baseline-batch 200 OK (434ms)\n[wrangler:info] GET /progress/response-owned 200 OK (13ms)\n[wrangler:info] GET /progress/empty 200 OK (5ms)\n[wrangler:info] GET /progress/empty 200 OK (4ms)\n[wrangler:info] GET /progress/empty 200 OK (3ms)\n[wrangler:info] GET /progress/empty 200 OK (3ms)\n[wrangler:info] GET /progress/empty 200 OK (2ms)\n" + } + }, + { + "directory": "/private/tmp/edgezero-cloudflare-progress-20261020-final", + "kind": "strict-framing-and-independent-groups", + "command": "WASM_BINDGEN_BIN=/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen WORKER_BUILD_BIN=/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build ESBUILD_BIN=/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild WASM_OPT_BIN=/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt WRANGLER_BIN=/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler WRANGLER_LOG_PATH=/private/tmp/edgezero-pr275-wrangler-logs WRANGLER_SEND_METRICS=false cargo run --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -- --adapter cloudflare --suite outbound-progress --output /private/tmp/edgezero-cloudflare-progress-20261020-final", + "exit_code": 0, + "source_sha256": { + "Cargo.toml": "ee73d1318af74a997ecc12bac0f80cd7eefbe8ead34a35b1432c8d6f49fc42e1", + "Cargo.lock": "1afb33fe3067419ad5a2a948b330ac1df123d53214e49dc4005a92f7bc69e043", + "crates/edgezero-core/src/outbound.rs": "f1577756a5375dbd9a780b611d50bc56d629b8a50b8c13cc40864d0f1ef5ba8d", + "crates/edgezero-core/src/outbound/exchange.rs": "f8ca9867b572b2e40e0c08ae55fefefaefbf82355c83aa8b28f74870d6e2a879", + "crates/edgezero-core/src/outbound/progress.rs": "90eec32101d09c4ee40409b1fae6f06b97c83d1af8a03baf39d07820da9a0a08", + "crates/edgezero-core/src/lib.rs": "0de5ff9b503fbf86a27b0f0abce7dc47f9f85c7d0d596e0fce4b6589a96d133d", + "crates/edgezero-core/src/compression.rs": "4acccc0c01009894ebe4f77f57ce8ce501d3f3f2ccb565c6b3d3871d826037d4", + "crates/edgezero-core/src/body.rs": "50a8bc65fcbc7df3d9afd3ddc520889f42da46af1c6419e23c19625c33d5bf9e", + "crates/edgezero-core/src/time.rs": "bcd7047587f656b6547c860986f0c8f6459a5554850cbac4c1bfd9c480ec7435", + "crates/edgezero-adapter-cloudflare/src/outbound.rs": "25dbde10a5502c1644a791ce188d39531689d0f1c138594ba17cc23c5854072c", + "crates/edgezero-adapter-cloudflare/src/request.rs": "a755088e4e65e5af813335283ec3095c35b5982d1e9057ce58dd748aa03e6200", + "crates/edgezero-adapter-cloudflare/src/response.rs": "15a5b643713d3fdd2bd965fd00d9b606ca5cddf754678c3b0a0b9a65bba21adb", + "tests/fixtures/reusable-app/Cargo.toml": "7665200757e4e565d11fa7eb036215aa583d21fd543bca39fdbdce470fee11ae", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs": "295b98872ab59c185f6907189dad13c5c4c56699538d9756872d1c7f82cb6812", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/progress.rs": "d144e3139edc6caff17613a28642bf4126fdc3c416bc4b72302171d63162512d", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs": "f07fd19c3937780e5a7973513ccd29dd4708c852f62f4b2dd3e14929181605f9", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs": "8719b6eb05c39b514f2f571166628a5d919935f3ebbb9e69be9b854bb416d4a3", + "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs": "22f5c8ff413357b5684910249c635a4993d0cb912b3cc584c8f14cc2258efb18", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/Cargo.toml": "96501fc975013ba68411cd6e0f77b6b0fe505dfe4ed8cef16620ef19b4c9784d", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml": "6c2a097879b98f4d4af3e2fea139215abc9fa0bfdc058500f28fd9f7d757aab3", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs": "606245131f0c594154ca0a2eb63e5341999325bfb2de55887ab3264fa4b89559", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs": "305f70039af59501639916bd8cfb8db696613c73af670e953dd329a3eed14498" + }, + "counts": { + "outbound-coding": 24, + "outbound-marker": 46, + "outbound-progress": 5, + "inbound-empty-signal": 5, + "progress-group": 3, + "client_completed": 80 + }, + "asserted_origin_receipts": { + "coding": 24, + "marker": 47, + "scheduling": 18, + "total": 89, + "retention": "only scheduling receipts are emitted individually; coding/marker counts are enforced by successful runner guards" + }, + "retained_scheduling_receipts": 18, + "build_environment": { + "event": "build_environment", + "host_arch": "aarch64", + "host_os": "macos", + "lockfile": { + "bytes": 84127, + "fnv1a64": "10d93fc12c84db43", + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-final/Cargo.lock", + "purpose": "non-security build identity" + }, + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)" + }, + "runtime": { + "adapter": "cloudflare", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "4.131.0" + }, + "coverage": { + "adapter": "cloudflare", + "attempted_empty_body_cases": 5, + "attempted_progress_cases": 5, + "empty_body_passes": 5, + "event": "progress-coverage", + "failures": [], + "limitations": [ + "HTTP/2 and HTTP/3 not exercised", + "hosted execution pending", + "response-owned tests handler/dispatch return, not host callback return", + "missing-context detection unproved except native absent-runtime control", + "no cancellation or G2 readiness qualification" + ], + "origin_requests": 18, + "progress_passes": 5, + "qualification": false + }, + "effective_config": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-final/progress-cloudflare/wrangler.toml", + "bytes": 487, + "sha256": "796f296a58f2226986fe71873f04908727d99ea0d6460c628a540580b0fa8d6e", + "text": "name = \"reusable-app-fixture\"\nmain = \"/private/tmp/edgezero-cloudflare-progress-20261020-final/progress-cloudflare/artifacts/build/worker/shim.mjs\"\ncompatibility_date = \"2026-04-22\"\ncompatibility_flags = [\"enable_request_signal\", \"enable_weak_ref\"]\n[vars]\nFIXTURE_MODE = \"retained\"\nfixture_marker = \"fixture-only-value\"\n\n[[kv_namespaces]]\nbinding = \"fixture_config\"\nid = \"00000000000000000000000000000001\"\n[[kv_namespaces]]\nbinding = \"fixture_kv\"\nid = \"00000000000000000000000000000002\"\n" + }, + "lockfile": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-final/Cargo.lock", + "bytes": 84127, + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + "artifacts": { + "index_bg.wasm": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-final/progress-cloudflare/artifacts/build/index_bg.wasm", + "bytes": 1691011, + "sha256": "95ece35b326d6da9c0a312369051299bca179ad46bde33d9958cd47a3c524744" + }, + "index.js": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-final/progress-cloudflare/artifacts/build/index.js", + "bytes": 28684, + "sha256": "e3fbc6a80de31ae583f5808512cdaa34dfdfb1a8f9e47dc7c0a339f1ee8842a8", + "text": "import{WorkerEntrypoint as xe}from\"cloudflare:workers\";var G=globalThis.__worker_init_state={criticalError:!1,instanceId:0};import X from\"./index_bg.wasm\";var x=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,be.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_containerstartupoptions_free(e,0)}catch(t){o(t)}}get enableInternet(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_containerstartupoptions_enableInternet(this.__wbg_ptr)}catch(t){o(t)}return e===16777215?void 0:e!==0}get entrypoint(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_containerstartupoptions_entrypoint(this.__wbg_ptr)}catch(n){o(n)}var t=ye(e[0],e[1]);return i.__wbindgen_free(e[0],e[1]*4,4),t}get env(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_containerstartupoptions_env(this.__wbg_ptr)}catch(t){o(t)}return e}set enableInternet(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_containerstartupoptions_enableInternet(this.__wbg_ptr,u(e)?16777215:e?1:0)}catch(t){o(t)}}set entrypoint(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t=he(e,i.__wbindgen_malloc),n=y;c();try{i.__wbg_set_containerstartupoptions_entrypoint(this.__wbg_ptr,t,n)}catch(r){o(r)}}set env(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_containerstartupoptions_env(this.__wbg_ptr,e)}catch(t){o(t)}}};Symbol.dispose&&(x.prototype[Symbol.dispose]=x.prototype.free);var I=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,de.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_intounderlyingbytesource_free(e,0)}catch(t){o(t)}}get autoAllocateChunkSize(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.intounderlyingbytesource_autoAllocateChunkSize(this.__wbg_ptr)}catch(t){o(t)}return e>>>0}cancel(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e=this.__destroy_into_raw();c();try{i.intounderlyingbytesource_cancel(e)}catch(t){o(t)}}pull(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t;c();try{t=i.intounderlyingbytesource_pull(this.__wbg_ptr,e)}catch(n){o(n)}return t}start(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.intounderlyingbytesource_start(this.__wbg_ptr,e)}catch(t){o(t)}}get type(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.intounderlyingbytesource_type(this.__wbg_ptr)}catch(t){o(t)}return fe[e]}};Symbol.dispose&&(I.prototype[Symbol.dispose]=I.prototype.free);var E=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,ge.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_intounderlyingsink_free(e,0)}catch(t){o(t)}}abort(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t=this.__destroy_into_raw(),n;c();try{n=i.intounderlyingsink_abort(t,e)}catch(r){o(r)}return n}close(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e=this.__destroy_into_raw(),t;c();try{t=i.intounderlyingsink_close(e)}catch(n){o(n)}return t}write(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t;c();try{t=i.intounderlyingsink_write(this.__wbg_ptr,e)}catch(n){o(n)}return t}};Symbol.dispose&&(E.prototype[Symbol.dispose]=E.prototype.free);var R=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,we.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_intounderlyingsource_free(e,0)}catch(t){o(t)}}cancel(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e=this.__destroy_into_raw();c();try{i.intounderlyingsource_cancel(e)}catch(t){o(t)}}pull(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t;c();try{t=i.intounderlyingsource_pull(this.__wbg_ptr,e)}catch(n){o(n)}return t}};Symbol.dispose&&(R.prototype[Symbol.dispose]=R.prototype.free);var m=class _{static __wrap(e){let t=Object.create(_.prototype);return t.__wbg_ptr=e,Object.defineProperty(t,\"__wbg_inst\",{value:s,writable:!0}),K.register(t,{ptr:e,instance:s},t),t}__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,K.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_minifyconfig_free(e,0)}catch(t){o(t)}}get css(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_minifyconfig_css(this.__wbg_ptr)}catch(t){o(t)}return e!==0}get html(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_minifyconfig_html(this.__wbg_ptr)}catch(t){o(t)}return e!==0}get js(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_minifyconfig_js(this.__wbg_ptr)}catch(t){o(t)}return e!==0}set css(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_minifyconfig_css(this.__wbg_ptr,e)}catch(t){o(t)}}set html(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_minifyconfig_html(this.__wbg_ptr,e)}catch(t){o(t)}}set js(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_minifyconfig_js(this.__wbg_ptr,e)}catch(t){o(t)}}};Symbol.dispose&&(m.prototype[Symbol.dispose]=m.prototype.free);var F=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,le.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_r2range_free(e,0)}catch(t){o(t)}}get length(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_r2range_length(this.__wbg_ptr)}catch(t){o(t)}return e[0]===0?void 0:e[1]}get offset(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_r2range_offset(this.__wbg_ptr)}catch(t){o(t)}return e[0]===0?void 0:e[1]}get suffix(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_r2range_suffix(this.__wbg_ptr)}catch(t){o(t)}return e[0]===0?void 0:e[1]}set length(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_r2range_length(this.__wbg_ptr,!u(e),u(e)?0:e)}catch(t){o(t)}}set offset(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_r2range_offset(this.__wbg_ptr,!u(e),u(e)?0:e)}catch(t){o(t)}}set suffix(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_r2range_suffix(this.__wbg_ptr,!u(e),u(e)?0:e)}catch(t){o(t)}}};Symbol.dispose&&(F.prototype[Symbol.dispose]=F.prototype.free);function U(){s++,p=null,k=null,W=null,typeof numBytesDecoded<\"u\"&&(numBytesDecoded=0),typeof y<\"u\"&&(y=0),D=!1,O=!1,q=new WebAssembly.Instance(X,te()),i=q.exports,i.__wbindgen_start()}function Y(){let _;c();try{_=i.__worker_init_state()}catch(e){o(e)}return _}function Z(_,e,t){let n;c();try{n=i.fetch(_,e,t)}catch(r){o(r)}return n}function ee(){c();try{i.init()}catch(_){o(_)}}function te(){return{__proto__:null,\"./index_bg.js\":{__proto__:null,__wbg_Error_30c8987f7c2ed4e2:function(e,t){return Error(l(e,t))},__wbg_Number_14af1003b8dd5ead:function(e){return Number(e)},__wbg_String_8564e559799eccda:function(e,t){let n=String(t),r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg___wbindgen_bigint_get_as_i64_a2383202b9353e4c:function(e,t){let n=t,r=typeof n==\"bigint\"?n:void 0;d().setBigInt64(e+8,u(r)?BigInt(0):r,!0),d().setInt32(e+0,!u(r),!0)},__wbg___wbindgen_boolean_get_5b446f51afd21013:function(e){let t=e,n=typeof t==\"boolean\"?t:void 0;return u(n)?16777215:n?1:0},__wbg___wbindgen_debug_string_4687d8d8c2017d52:function(e,t){let n=V(t),r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg___wbindgen_in_92f62ee1427d9e49:function(e,t){return e in t},__wbg___wbindgen_is_bigint_b123553bed3bb382:function(e){return typeof e==\"bigint\"},__wbg___wbindgen_is_function_1f9d30630b8b1d3d:function(e){return typeof e==\"function\"},__wbg___wbindgen_is_null_e343b7d08827ba72:function(e){return e===null},__wbg___wbindgen_is_null_or_undefined_f9435d56bdcc06df:function(e){return e==null},__wbg___wbindgen_is_object_3c45d4f2dde4e749:function(e){let t=e;return typeof t==\"object\"&&t!==null},__wbg___wbindgen_is_string_90b56bc79aad6f6c:function(e){return typeof e==\"string\"},__wbg___wbindgen_is_undefined_8865fb403f8fe9d8:function(e){return e===void 0},__wbg___wbindgen_jsval_eq_02babf21faa37971:function(e,t){return e===t},__wbg___wbindgen_jsval_loose_eq_677f21e468d6b461:function(e,t){return e==t},__wbg___wbindgen_number_get_2e0e7dee9f701a71:function(e,t){let n=t,r=typeof n==\"number\"?n:void 0;d().setFloat64(e+8,u(r)?0:r,!0),d().setInt32(e+0,!u(r),!0)},__wbg___wbindgen_reinit_3f79fdb34dee3ea9:function(){O=!0},__wbg___wbindgen_string_get_0380ccaa2f57f0d9:function(e,t){let n=t,r=typeof n==\"string\"?n:void 0;var f=u(r)?0:S(r,i.__wbindgen_malloc,i.__wbindgen_realloc),a=y;d().setInt32(e+4,a,!0),d().setInt32(e+0,f,!0)},__wbg___wbindgen_throw_41e9ee4f547fc59a:function(e,t){throw new WebAssembly.Exception(B,[new Error(l(e,t))])},__wbg__wbg_cb_unref_dcc1a90847f04c41:function(e){e._wbg_cb_unref()},__wbg_abort_d5fa4a667a578774:function(e){return e.abort()},__wbg_abort_f78b99d80248bacb:function(e){e.abort()},__wbg_aborted_5b2b5f06b5e59fde:function(e){return e.aborted},__wbg_append_1938ed58fb5613b8:function(e,t,n,r,f){e.append(l(t,n),l(r,f))},__wbg_arrayBuffer_aa7cad37be6e6b29:function(e){return e.arrayBuffer()},__wbg_body_4792166cb351f162:function(e){let t=e.body;return u(t)?0:g(t)},__wbg_body_4e088733babc630d:function(e){let t=e.body;return u(t)?0:g(t)},__wbg_buffer_56ec2905a66f58b9:function(e){return e.buffer},__wbg_byobRequest_54f7ce41d584549a:function(e){let t=e.byobRequest;return u(t)?0:g(t)},__wbg_byteLength_9f985b8373f1f3fe:function(e){return e.byteLength},__wbg_byteOffset_0bd5a6a03553639c:function(e){return e.byteOffset},__wbg_call_1875a20c43a36133:function(e,t,n,r){return e.call(t,n,r)},__wbg_call_187d372bd5fdd4aa:function(e,t,n){return e.call(t,n)},__wbg_call_6137034ef55c9d0f:function(e,t){return e.call(t)},__wbg_call_939a2607c4484b0b:function(e,t,n,r,f){return e.call(t,n,r,f)},__wbg_cancel_3b323af6bb094e47:function(e){return e.cancel()},__wbg_cancel_5b5c53c5bbb13318:function(e){return e.cancel()},__wbg_cancel_7c2a08c8db4c73f3:function(e){return e.cancel()},__wbg_catch_92594c2d17f55f12:function(e,t){return e.catch(t)},__wbg_cause_19276d032a32d4bd:function(e){return e.cause},__wbg_cf_909bcfb212a8822c:function(e){let t=e.cf;return u(t)?0:g(t)},__wbg_cf_b47fe5ff6d015a0d:function(e){let t=e.cf;return u(t)?0:g(t)},__wbg_clearTimeout_d49a4afcd9b218b7:function(e,t){e.clearTimeout(t)},__wbg_close_185286988f30f252:function(e){e.close()},__wbg_close_748f26698dabe269:function(e){e.close()},__wbg_close_fd7fd8eaf125c9c1:function(e){return e.close()},__wbg_constructor_076176ce0f386e16:function(e){return e.constructor},__wbg_done_b41a1d26cdb37fb6:function(e){return e.done},__wbg_enqueue_4a01c128f9a0de16:function(e,t){e.enqueue(t)},__wbg_entries_b55ec7e0dc443355:function(e){return e.entries()},__wbg_entries_fb6397112b1de25f:function(e){return Object.entries(e)},__wbg_error_a30e98d44009f1bb:function(e,t){console.error(e,t)},__wbg_error_c9cf3fc2064683a9:function(e){console.error(e)},__wbg_fetch_0e46da8cf7c730e4:function(e,t,n,r){return e.fetch(l(t,n),r)},__wbg_fetch_7b5de4f88137a4dc:function(e,t,n){return e.fetch(t,n)},__wbg_getReader_1ef752ebe7bb7900:function(e,t){return e.getReader(t)},__wbg_getReader_bb0230851fbf986b:function(e){return e.getReader()},__wbg_getWriter_7994a8c7d4b75c01:function(e){return e.getWriter()},__wbg_get_31af05bd4842a84f:function(e,t){return Reflect.get(e,t)},__wbg_get_658f6698067d9515:function(e,t){return Reflect.get(e,t)},__wbg_get_6c896e0571ddae51:function(e,t){return e[t>>>0]},__wbg_get_88983e33467eae60:function(e,t){let n=Reflect.get(e,t);return u(n)?0:g(n)},__wbg_get_done_7020b91c987365c7:function(e){let t=e.done;return u(t)?16777215:t?1:0},__wbg_get_unchecked_288889d017702237:function(e,t){return e[t>>>0]},__wbg_get_value_87fbd6ad1283f2e7:function(e){return e.value},__wbg_get_with_ref_key_6412cf3094599694:function(e,t){return e[t]},__wbg_headers_6930a8bd09b630de:function(e){return e.headers},__wbg_headers_eba93595f8944c2f:function(e){return e.headers},__wbg_instanceId_27a130234331eb08:function(e){return e.instanceId},__wbg_instanceof_ArrayBuffer_a99f175873e5d9b8:function(e){let t;try{t=e instanceof ArrayBuffer}catch{t=!1}return t},__wbg_instanceof_Error_80a725f81f2e102d:function(e){let t;try{t=e instanceof Error}catch{t=!1}return t},__wbg_instanceof_Map_b2611749102d7ba3:function(e){let t;try{t=e instanceof Map}catch{t=!1}return t},__wbg_instanceof_Promise_e1dc6893aa1ece28:function(e){let t;try{t=e instanceof Promise}catch{t=!1}return t},__wbg_instanceof_ReadableStream_e2d77912575119ab:function(e){let t;try{t=e instanceof ReadableStream}catch{t=!1}return t},__wbg_instanceof_Response_b8758567269c30b2:function(e){let t;try{t=e instanceof Response}catch{t=!1}return t},__wbg_instanceof_Uint8Array_828cef2aaacafc31:function(e){let t;try{t=e instanceof Uint8Array}catch{t=!1}return t},__wbg_isArray_e15a2ff68ffdbef2:function(e){return Array.isArray(e)},__wbg_isSafeInteger_717808ad6a54bd9e:function(e){return Number.isSafeInteger(e)},__wbg_iterator_e3c31c892080e444:function(){return Symbol.iterator},__wbg_keys_bac434c8972731d5:function(e){return e.keys()},__wbg_length_7f3c00c40364105e:function(e){return e.length},__wbg_length_d4bdea10311bd9cf:function(e){return e.length},__wbg_message_5f8387f0c32b90a7:function(e){return e.message},__wbg_method_6d02ee0808a7de31:function(e,t){let n=t.method,r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg_minifyconfig_new:function(e){return m.__wrap(e)},__wbg_name_e2eac7cdfa054f65:function(e){return e.name},__wbg_name_f9b814d78e8e299f:function(e){return e.name},__wbg_new_1dbf7428bba60a42:function(e){return new Uint8Array(e)},__wbg_new_28744009d011f847:function(){return new Map},__wbg_new_2e41357094e99294:function(){return new AbortController},__wbg_new_343a093a3c2ffb4e:function(e,t){return new Error(l(e,t))},__wbg_new_617a8cdb8bb1130e:function(){return new Object},__wbg_new_661230f001cf2087:function(){return new TransformStream},__wbg_new_83ca405eae412a62:function(){return new Headers},__wbg_new_big_int_84cdc69174f81f5b:function(e){return new FixedLengthStream(e)},__wbg_new_ee2291f50781bf1d:function(){return new Array},__wbg_new_f569a2e335eed0bb:function(e){return new FixedLengthStream(e>>>0)},__wbg_new_from_slice_9a868026ffa4208a:function(e,t){return new Uint8Array(L(e,t))},__wbg_new_typed_b01cb72a8af741a3:function(e,t){try{var n={a:e,b:t},r=(a,b)=>{let w=n.a;n.a=0;try{return ce(w,n.b,a,b)}finally{n.a=w}};return new Promise(r)}finally{n.a=0}},__wbg_new_with_byte_offset_and_length_2f5d7fc2a828b74d:function(e,t,n){return new Uint8Array(e,t>>>0,n>>>0)},__wbg_new_with_length_3da0ad195f6f63ba:function(e){return new Uint8Array(e>>>0)},__wbg_new_with_opt_buffer_source_and_init_c1b859a23c80afca:function(e,t){return new Response(e,t)},__wbg_new_with_opt_readable_stream_and_init_df114f2d364f216d:function(e,t){return new Response(e,t)},__wbg_new_with_opt_str_and_init_33bef1b272950372:function(e,t,n){return new Response(e===0?void 0:l(e,t),n)},__wbg_new_with_str_and_init_4618dee4e950224f:function(e,t,n){return new Request(l(e,t),n)},__wbg_next_33784799010f1bbe:function(e){return e.next},__wbg_next_f4aac29c42af995c:function(e){return e.next()},__wbg_now_e7c6795a7f81e10f:function(e){return e.now()},__wbg_performance_3fcf6e32a7e1ed0a:function(e){return e.performance},__wbg_prototypesetcall_bc27214492979395:function(e,t,n){Uint8Array.prototype.set.call(L(e,t),n)},__wbg_queueMicrotask_9833f9a49df95a49:function(e){return e.queueMicrotask},__wbg_queueMicrotask_a72f977e97f23c5f:function(e){queueMicrotask(e)},__wbg_read_2a943774344c9728:function(e){return e.read()},__wbg_read_8cba94edf8011d59:function(e,t){return e.read(t)},__wbg_readable_4d43535945ae9ca0:function(e){return e.readable},__wbg_releaseLock_0948949015793a6d:function(e){e.releaseLock()},__wbg_releaseLock_eb64cd83aa9fcfe4:function(e){e.releaseLock()},__wbg_releaseLock_fe24bdfe19e75e0b:function(e){e.releaseLock()},__wbg_resolve_0076e10020304ede:function(e){return Promise.resolve(e)},__wbg_respond_c102fabcc79e5ef0:function(e,t){e.respond(t>>>0)},__wbg_setTimeout_7fb1e1fe294b6ad9:function(e,t,n){return e.setTimeout(t,n)},__wbg_set_145a351398b48c65:function(e,t,n){return Reflect.set(e,t,n)},__wbg_set_575d3ddb70fe831d:function(e,t,n){e.set(L(t,n))},__wbg_set_6ae97e73113c4f0b:function(e,t,n){return e.set(t,n)},__wbg_set_6be42768c690e380:function(e,t,n){e[t]=n},__wbg_set_af5932e3f78b5648:function(e,t,n,r,f){e.set(l(t,n),l(r,f))},__wbg_set_bea140a88be9b277:function(e,t,n){e[t>>>0]=n},__wbg_set_body_1fb0f1008bfc7df6:function(e,t){e.body=t},__wbg_set_cache_7507c4cc5938ccc7:function(e,t){e.cache=ae[t]},__wbg_set_criticalError_0d391f2a45c3bc42:function(e,t){e.criticalError=t!==0},__wbg_set_headers_40ad33dcf016613f:function(e,t){e.headers=t},__wbg_set_headers_69ef7c46a2e6c742:function(e,t){e.headers=t},__wbg_set_instanceId_9b3420954adec865:function(e,t){e.instanceId=t>>>0},__wbg_set_method_dcb32343247ec427:function(e,t,n){e.method=l(t,n)},__wbg_set_mode_6d06239cb5535f25:function(e,t){e.mode=se[t]},__wbg_set_redirect_cc128ab2411da334:function(e,t){e.redirect=ue[t]},__wbg_set_signal_2dbd377604627f26:function(e,t){e.signal=t},__wbg_set_status_678f122cbe435d95:function(e,t){e.status=t},__wbg_signal_26a6bdfa17b46df6:function(e){return e.signal},__wbg_signal_e5a49a2d9c65b7f6:function(e){return e.signal},__wbg_static_accessor_GLOBAL_266715b9d96ba635:function(){let e=typeof global>\"u\"?null:global;return u(e)?0:g(e)},__wbg_static_accessor_GLOBAL_THIS_10fb7dc1ae063179:function(){let e=typeof globalThis>\"u\"?null:globalThis;return u(e)?0:g(e)},__wbg_static_accessor_INIT_STATE_35966a05809b7176:function(){return G},__wbg_static_accessor_SELF_0b583911f537483a:function(){let e=typeof self>\"u\"?null:self;return u(e)?0:g(e)},__wbg_static_accessor_WINDOW_d7f903d1508cbdc4:function(){let e=typeof window>\"u\"?null:window;return u(e)?0:g(e)},__wbg_status_ce0a98d3c57125f3:function(e){return e.status},__wbg_subarray_002b94d5e13d1411:function(e,t,n){return e.subarray(t>>>0,n>>>0)},__wbg_then_4e48da6c0a644c56:function(e,t,n){return e.then(t,n)},__wbg_then_c8a35d4ad59c6b8e:function(e,t){return e.then(t)},__wbg_then_c949d5a25a4e78f8:function(e,t,n){return e.then(t,n)},__wbg_then_e71170d78fcf8954:function(e,t){return e.then(t)},__wbg_url_7bfada5c2297bc70:function(e,t){let n=t.url,r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg_value_f3c585ee8f5ba40c:function(e){return e.value},__wbg_view_9c570f33e8d6ab96:function(e){let t=e.view;return u(t)?0:g(t)},__wbg_waitUntil_c51e37673c596e00:function(e,t){e.waitUntil(t)},__wbg_webSocket_2b7e8e9549bed673:function(e){let t=e.webSocket;return u(t)?0:g(t)},__wbg_writable_44735d15a1cee85d:function(e){return e.writable},__wbg_write_bf07aa79dd36a472:function(e,t){return e.write(t)},__wbindgen_generic_0000000000000001:function(e,t){return C(e,t,oe)},__wbindgen_generic_0000000000000002:function(e,t){return C(e,t,ie)},__wbindgen_generic_0000000000000003:function(e,t){return C(e,t,re)},__wbindgen_generic_0000000000000004:function(e){return e},__wbindgen_generic_0000000000000005:function(e){return e},__wbindgen_generic_0000000000000006:function(e,t){return l(e,t)},__wbindgen_generic_0000000000000007:function(e){return BigInt.asUintN(64,e)},__wbindgen_init_externref_table:function(){let e=i.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)},__wbindgen_jstag:WebAssembly.JSTag,__wbindgen_rethrow_critical:function(e){throw new Error(\"Critical error\",{cause:e})}}}}var B=new WebAssembly.Tag({parameters:[\"externref\"]}),J,D=!1;function ne(){D=!0;try{let _=N()[i.__abort_handler.value/4];_&&i.__wbindgen_export.get(_)()}catch{}}function o(_){throw _ instanceof WebAssembly.Exception&&_.is(B)?_.getArg(B,0):(N()[J]=1,ne(),_)}function c(){if(J??=i.__instance_terminated.value/4,N()[J]){if(D||ne(),O){U();return}throw new Error(\"Module terminated\")}else O&&U()}function re(_,e){c();try{i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke_______true_(_,e)}catch(t){o(t)}}function ie(_,e,t){c();try{i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke___wasm_bindgen_c7f02901330ffbdc___JsValue______true_(_,e,t)}catch(n){o(n)}}function oe(_,e,t){let n;c();try{n=i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke___wasm_bindgen_c7f02901330ffbdc___JsValue__core_ed718c3d60ebd546___result__Result_____wasm_bindgen_c7f02901330ffbdc___JsError___true_(_,e,t)}catch(r){o(r)}if(n[1])throw pe(n[0])}function ce(_,e,t,n){c();try{i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke___js_sys_b2f584a2e795ca61___Function_fn_wasm_bindgen_c7f02901330ffbdc___JsValue_____wasm_bindgen_c7f02901330ffbdc___sys__Undefined___js_sys_b2f584a2e795ca61___Function_fn_wasm_bindgen_c7f02901330ffbdc___JsValue_____wasm_bindgen_c7f02901330ffbdc___sys__Undefined_______true_(_,e,t,n)}catch(r){o(r)}}var se=[\"byob\"],fe=[\"bytes\"],ae=[\"default\",\"no-store\",\"reload\",\"no-cache\",\"force-cache\",\"only-if-cached\"],ue=[\"follow\",\"error\",\"manual\"],s=0,be=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_containerstartupoptions_free(_,1)}),de=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_intounderlyingbytesource_free(_,1)}),ge=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_intounderlyingsink_free(_,1)}),we=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_intounderlyingsource_free(_,1)}),K=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_minifyconfig_free(_,1)}),le=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_r2range_free(_,1)});function g(_){let e=i.__externref_table_alloc();return i.__wbindgen_externrefs.set(e,_),e}var Q=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(_=>{_.instance===s&&i.__wbindgen_destroy_closure(_.a,_.b)});function V(_){let e=typeof _;if(e==\"number\"||e==\"boolean\"||_==null)return`${_}`;if(e==\"string\")return`\"${_}\"`;if(e==\"symbol\"){let r=_.description;return r==null?\"Symbol\":`Symbol(${r})`}if(e==\"function\"){let r=_.name;return typeof r==\"string\"&&r.length>0?`Function(${r})`:\"Function\"}if(Array.isArray(_)){let r=_.length,f=\"[\";r>0&&(f+=V(_[0]));for(let a=1;a1)n=t[1];else return toString.call(_);if(n==\"Object\")try{return\"Object(\"+JSON.stringify(_)+\")\"}catch{return\"Object\"}return _ instanceof Error?`${_.name}: ${_.message}\n${_.stack}`:n}function ye(_,e){_=_>>>0;let t=d(),n=[];for(let r=_;r<_+4*e;r+=4)n.push(i.__wbindgen_externrefs.get(t.getUint32(r,!0)));return i.__externref_drop_slice(_,e),n}function L(_,e){return _=_>>>0,A().subarray(_/1,_/1+e)}var p=null;function d(){return(p===null||p.buffer.detached===!0||p.buffer.detached===void 0&&p.buffer!==i.memory.buffer)&&(p=new DataView(i.memory.buffer)),p}var k=null;function N(){return(k===null||k.byteLength===0)&&(k=new Int32Array(i.memory.buffer)),k}function l(_,e){return me(_>>>0,e)}var W=null;function A(){return(W===null||W.byteLength===0)&&(W=new Uint8Array(i.memory.buffer)),W}function u(_){return _==null}function C(_,e,t){let n={a:_,b:e,cnt:1,instance:s},r=(...f)=>{if(n.instance!==s)throw new Error(\"Cannot invoke closure from previous WASM instance\");n.cnt++;let a=n.a;n.a=0;try{return t(a,n.b,...f)}finally{n.a=a,r._wbg_cb_unref()}};return r._wbg_cb_unref=()=>{--n.cnt===0&&(i.__wbindgen_destroy_closure(n.a,n.b),n.a=0,Q.unregister(n))},Q.register(r,n,n),r}function he(_,e){let t=e(_.length*4,4)>>>0;for(let n=0;n<_.length;n++){let r=g(_[n]);d().setUint32(t+4*n,r,!0)}return y=_.length,t}function S(_,e,t){if(t===void 0){let b=T.encode(_),w=e(b.length,1)>>>0;return A().subarray(w,w+b.length).set(b),y=b.length,w}let n=_.length,r=e(n,1)>>>0,f=A(),a=0;for(;a127)break;f[r+a]=b}if(a!==n){a!==0&&(_=_.slice(a)),r=t(r,n,n=a+_.length*3,1)>>>0;let b=A().subarray(r+a,r+n),w=T.encodeInto(_,b);a+=w.written,r=t(r,n,a,1)>>>0}return y=a,r}var O=!1;function pe(_){let e=i.__wbindgen_externrefs.get(_);return i.__externref_table_dealloc(_),e}var _e=new TextDecoder(\"utf-8\",{ignoreBOM:!0,fatal:!0});_e.decode();function me(_,e){return _e.decode(A().subarray(_,_+e))}var T=new TextEncoder;\"encodeInto\"in T||(T.encodeInto=function(_,e){let t=T.encode(_);return e.set(t),{read:_.length,written:t.length}});var y=0,q=new WebAssembly.Instance(X,te()),i=q.exports;i.__wbindgen_start();Error.stackTraceLimit=100;var h=Y();function $(){h.criticalError&&(console.log(\"Reinitializing Wasm application\"),U(),h.criticalError=!1,h.instanceId++)}addEventListener(\"error\",_=>{H(_.error)});function H(_){_ instanceof WebAssembly.RuntimeError&&(console.error(\"Critical\",_),h.criticalError=!0)}var P=class extends xe{};P.prototype.fetch=function(e){return Z.call(this,e,this.env,this.ctx)};P.prototype.init=ee;var Ie={set:(_,e,t,n)=>Reflect.set(_.instance,e,t,n),has:(_,e)=>Reflect.has(_.instance,e),deleteProperty:(_,e)=>Reflect.deleteProperty(_.instance,e),apply:(_,e,t)=>Reflect.apply(_.instance,e,t),construct:(_,e,t)=>Reflect.construct(_.instance,e,t),getPrototypeOf:_=>Reflect.getPrototypeOf(_.instance),setPrototypeOf:(_,e)=>Reflect.setPrototypeOf(_.instance,e),isExtensible:_=>Reflect.isExtensible(_.instance),preventExtensions:_=>Reflect.preventExtensions(_.instance),getOwnPropertyDescriptor:(_,e)=>Reflect.getOwnPropertyDescriptor(_.instance,e),defineProperty:(_,e,t)=>Reflect.defineProperty(_.instance,e,t),ownKeys:_=>Reflect.ownKeys(_.instance)},v={construct(_,e,t){try{$();let n={instance:Reflect.construct(_,e,t),instanceId:h.instanceId,ctor:_,args:e,newTarget:t};return new Proxy(n,{...Ie,get(r,f,a){r.instanceId!==h.instanceId&&(r.instance=Reflect.construct(r.ctor,r.args,r.newTarget),r.instanceId=h.instanceId);let b=Reflect.get(r.instance,f,a);return typeof b!=\"function\"?b:b.constructor===Function?new Proxy(b,{apply(w,z,M){$();try{return w.apply(z,M)}catch(j){throw H(j),j}}}):new Proxy(b,{async apply(w,z,M){$();try{return await w.apply(z,M)}catch(j){throw H(j),j}}})}})}catch(n){throw h.criticalError=!0,n}}},Se=new Proxy(P,v),ke=new Proxy(x,v),We=new Proxy(I,v),Ae=new Proxy(E,v),Te=new Proxy(R,v),Pe=new Proxy(m,v),Ue=new Proxy(F,v);export{ke as ContainerStartupOptions,We as IntoUnderlyingByteSource,Ae as IntoUnderlyingSink,Te as IntoUnderlyingSource,Pe as MinifyConfig,Ue as R2Range,Se as default};\n" + }, + "worker/shim.mjs": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-final/progress-cloudflare/artifacts/build/worker/shim.mjs", + "bytes": 164, + "sha256": "ac99177861405a6046923cbacabbd6adfa1853a56d0c679b9f5e09097c2f4728", + "text": "// Use index.js directly, this file provided for backwards compat\n// with former shim.mjs only.\nexport * from '../index.js';\nexport { default } from '../index.js';\n" + }, + "package.json": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-final/progress-cloudflare/artifacts/build/package.json", + "bytes": 211, + "sha256": "d7f0cd6dd39927f703e7ef907b4a12b98f5d7bcca71ef29c4a00b5ca61411a15", + "text": "{\n \"files\": [\n \"index_bg.wasm\",\n \"index.js\",\n \"index.d.ts\"\n ],\n \"main\": \"index.js\",\n \"name\": \"fixture-cloudflare\",\n \"sideEffects\": [\n \"./index.js\"\n ],\n \"type\": \"module\",\n \"version\": \"0.1.0\"\n}" + } + }, + "events": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-final/events.jsonl", + "bytes": 90243, + "sha256": "2db797446dcaa1ac86ab7a4b8d57fc143519a8743bce7440bc406cd45c4ca60d", + "records": 171, + "jsonl": "{\"construction_rounds\":0,\"event\":\"run\",\"max_requests\":10,\"seed\":856,\"suite\":\"outbound-progress\"}\n{\"event\":\"build_environment\",\"host_arch\":\"aarch64\",\"host_os\":\"macos\",\"lockfile\":{\"bytes\":84127,\"fnv1a64\":\"10d93fc12c84db43\",\"path\":\"/private/tmp/edgezero-cloudflare-progress-20261020-final/Cargo.lock\",\"purpose\":\"non-security build identity\"},\"rustc\":\"rustc 1.98.1 (48a229cea 2026-09-01)\"}\n{\"adapter\":\"cloudflare\",\"event\":\"progress-runtime\",\"executable\":\"/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js\",\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"source_state\":\"working-tree-characterization\",\"version\":\"4.131.0\"}\n{\"event\":\"worker-build-tool\",\"executable\":\"/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen\",\"variable\":\"WASM_BINDGEN_BIN\",\"version\":\"wasm-bindgen 0.2.129\"}\n{\"event\":\"worker-build-tool\",\"executable\":\"/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild\",\"variable\":\"ESBUILD_BIN\",\"version\":\"0.28.1\"}\n{\"event\":\"worker-build-tool\",\"executable\":\"/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt\",\"variable\":\"WASM_OPT_BIN\",\"version\":\"wasm-opt version 130 (version_130)\"}\n{\"adapter\":\"cloudflare\",\"artifact\":{\"bytes\":1691011,\"fnv1a64\":\"559ae66b5e1e3cc2\",\"path\":\"/private/tmp/edgezero-cloudflare-progress-20261020-final/progress-cloudflare/artifacts/build/index_bg.wasm\",\"purpose\":\"non-security build identity\"},\"event\":\"build\",\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-buffered-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-200-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":113197375,\"event\":\"client_completed\",\"first_byte_ns\":112558833,\"header_ns\":112212958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c965ad5c7f8-840c-0\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-buffered-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-206-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":4886500,\"event\":\"client_completed\",\"first_byte_ns\":4843708,\"header_ns\":4823000,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96619d77e8-840c-1\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-buffered-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":10188166,\"event\":\"client_completed\",\"first_byte_ns\":10039375,\"header_ns\":10037625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9661ef2de0-840c-2\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-buffered-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-gzip-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11081000,\"event\":\"client_completed\",\"first_byte_ns\":11041875,\"header_ns\":11038791,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9662910f48-840c-3\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-streamed-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-200-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11517000,\"event\":\"client_completed\",\"first_byte_ns\":11494292,\"header_ns\":11492708,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9663400840-840c-4\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-streamed-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-206-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11051500,\"event\":\"client_completed\",\"first_byte_ns\":11015125,\"header_ns\":10988000,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9663f640b0-840c-5\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-streamed-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11921042,\"event\":\"client_completed\",\"first_byte_ns\":11879042,\"header_ns\":11878084,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9664a62fc0-840c-6\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-gzip-streamed-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-gzip-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12228000,\"event\":\"client_completed\",\"first_byte_ns\":12199292,\"header_ns\":12195958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9665631720-840c-7\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-buffered-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-200-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12330125,\"event\":\"client_completed\",\"first_byte_ns\":12300833,\"header_ns\":12297250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9666239860-840c-8\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-buffered-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-206-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":10552750,\"event\":\"client_completed\",\"first_byte_ns\":10535833,\"header_ns\":10529041,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9666e5fa18-840c-9\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-buffered-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12100958,\"event\":\"client_completed\",\"first_byte_ns\":12070917,\"header_ns\":12065125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96678dcb08-840c-a\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-buffered-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-deflate-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10064292,\"event\":\"client_completed\",\"first_byte_ns\":10034792,\"header_ns\":10032417,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96684dfa40-840c-b\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-streamed-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-200-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11630750,\"event\":\"client_completed\",\"first_byte_ns\":11603333,\"header_ns\":11599583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9668eda540-840c-c\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-streamed-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-206-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12015959,\"event\":\"client_completed\",\"first_byte_ns\":11971750,\"header_ns\":11970292,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9669a59ee8-840c-d\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-streamed-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12200625,\"event\":\"client_completed\",\"first_byte_ns\":12184625,\"header_ns\":12182625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966a63a758-840c-e\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-deflate-streamed-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-deflate-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12133583,\"event\":\"client_completed\",\"first_byte_ns\":12099208,\"header_ns\":12098166,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966b243c20-840c-f\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-buffered-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-200-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11525709,\"event\":\"client_completed\",\"first_byte_ns\":11486875,\"header_ns\":11484209,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966be34e30-840c-10\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-buffered-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-206-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12077750,\"event\":\"client_completed\",\"first_byte_ns\":12042917,\"header_ns\":12041792,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966c98fa00-840c-11\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-buffered-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12475375,\"event\":\"client_completed\",\"first_byte_ns\":12444792,\"header_ns\":12441458,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966d565a78-840c-12\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-buffered-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-br-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10961083,\"event\":\"client_completed\",\"first_byte_ns\":10933000,\"header_ns\":10931125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966e1b19a8-840c-13\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-streamed-200-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-200-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10814791,\"event\":\"client_completed\",\"first_byte_ns\":10682333,\"header_ns\":10677666,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966ec89f88-840c-14\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-streamed-206-original\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-206-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11416125,\"event\":\"client_completed\",\"first_byte_ns\":11316542,\"header_ns\":10985833,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966f75b420-840c-15\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-streamed-206-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11727000,\"event\":\"client_completed\",\"first_byte_ns\":11694083,\"header_ns\":11692791,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967029ecd8-840c-16\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"coding-br-streamed-200-decode\",\"client\":{\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-br-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11244833,\"event\":\"client_completed\",\"first_byte_ns\":11204375,\"header_ns\":11202958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9670e2e080-840c-17\"},\"error\":null,\"event\":\"outbound-coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-absent-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":13956709,\"event\":\"client_completed\",\"first_byte_ns\":13913459,\"header_ns\":13911625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96722eb5b8-840c-18\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-absent-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11548500,\"event\":\"client_completed\",\"first_byte_ns\":11509000,\"header_ns\":11505542,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96730a8b10-840c-19\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-absent-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11497667,\"event\":\"client_completed\",\"first_byte_ns\":11468542,\"header_ns\":11467334,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9673c1b5b0-840c-1a\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-upstream-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12147375,\"event\":\"client_completed\",\"first_byte_ns\":12115084,\"header_ns\":12113709,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9674783088-840c-1b\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-upstream-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11278500,\"event\":\"client_completed\",\"first_byte_ns\":11135875,\"header_ns\":11132541,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9675382910-840c-1c\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-upstream-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":9667292,\"event\":\"client_completed\",\"first_byte_ns\":9630625,\"header_ns\":9629042,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9675ecea80-840c-1d\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-nominated-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12305542,\"event\":\"client_completed\",\"first_byte_ns\":12262417,\"header_ns\":12259375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967686c150-840c-1e\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-nominated-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11818792,\"event\":\"client_completed\",\"first_byte_ns\":11777750,\"header_ns\":11776333,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967748a9f0-840c-1f\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-200-nominated-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11702750,\"event\":\"client_completed\",\"first_byte_ns\":11664000,\"header_ns\":11663083,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96780385e0-840c-20\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-absent-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11873375,\"event\":\"client_completed\",\"first_byte_ns\":11792833,\"header_ns\":11790041,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9678bd3120-840c-21\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-absent-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12077792,\"event\":\"client_completed\",\"first_byte_ns\":11998542,\"header_ns\":11995375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96797987f8-840c-22\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-absent-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":13296917,\"event\":\"client_completed\",\"first_byte_ns\":13137417,\"header_ns\":12016208,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967a384be8-840c-23\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-upstream-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":10200750,\"event\":\"client_completed\",\"first_byte_ns\":10119833,\"header_ns\":10114917,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967b0b3800-840c-24\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-upstream-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12345958,\"event\":\"client_completed\",\"first_byte_ns\":12218750,\"header_ns\":12215583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967bad8e98-840c-25\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-upstream-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11872125,\"event\":\"client_completed\",\"first_byte_ns\":11794417,\"header_ns\":11790917,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967c712100-840c-26\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-nominated-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12041875,\"event\":\"client_completed\",\"first_byte_ns\":11952083,\"header_ns\":11950708,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967d2d6450-840c-27\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-nominated-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12031750,\"event\":\"client_completed\",\"first_byte_ns\":11999708,\"header_ns\":11994875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967deb1ab8-840c-28\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-204-nominated-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12356250,\"event\":\"client_completed\",\"first_byte_ns\":12301250,\"header_ns\":12299750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967ea87b30-840c-29\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-absent-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12146833,\"event\":\"client_completed\",\"first_byte_ns\":12091083,\"header_ns\":12086875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967f6b3e90-840c-2a\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-absent-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11424208,\"event\":\"client_completed\",\"first_byte_ns\":11385917,\"header_ns\":11382292,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96802af898-840c-2b\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-absent-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10412084,\"event\":\"client_completed\",\"first_byte_ns\":10329042,\"header_ns\":10325292,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9680dfe500-840c-2c\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-upstream-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11726167,\"event\":\"client_completed\",\"first_byte_ns\":11693208,\"header_ns\":11688625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9681853550-840c-2d\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-upstream-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12188417,\"event\":\"client_completed\",\"first_byte_ns\":11805750,\"header_ns\":11801333,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9682409228-840c-2e\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-upstream-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10038250,\"event\":\"client_completed\",\"first_byte_ns\":10006000,\"header_ns\":10004583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96830103c8-840c-2f\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-nominated-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11895583,\"event\":\"client_completed\",\"first_byte_ns\":11847000,\"header_ns\":11845291,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9683a02228-840c-30\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-nominated-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":9661291,\"event\":\"client_completed\",\"first_byte_ns\":9589625,\"header_ns\":9586958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96845c2ae0-840c-31\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-head-200-nominated-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12159375,\"event\":\"client_completed\",\"first_byte_ns\":12121541,\"header_ns\":12118583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9684f5abc0-840c-32\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-absent-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10497375,\"event\":\"client_completed\",\"first_byte_ns\":10401583,\"header_ns\":10398083,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9685b52360-840c-33\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-absent-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10024292,\"event\":\"client_completed\",\"first_byte_ns\":9991125,\"header_ns\":9987959,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96865b0ff0-840c-34\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-absent-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11061584,\"event\":\"client_completed\",\"first_byte_ns\":10966000,\"header_ns\":10959084,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9686fa16e0-840c-35\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-upstream-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11344167,\"event\":\"client_completed\",\"first_byte_ns\":11217292,\"header_ns\":11215834,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9687aa5410-840c-36\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-upstream-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11691833,\"event\":\"client_completed\",\"first_byte_ns\":11611542,\"header_ns\":11610542,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96885d4c78-840c-37\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-upstream-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10943375,\"event\":\"client_completed\",\"first_byte_ns\":10907625,\"header_ns\":10901791,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968915e260-840c-38\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-nominated-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":12060417,\"event\":\"client_completed\",\"first_byte_ns\":12027125,\"header_ns\":12024750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9689c2c048-840c-39\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-nominated-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10078833,\"event\":\"client_completed\",\"first_byte_ns\":10006166,\"header_ns\":10005291,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968a80b530-840c-3a\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-304-nominated-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":12399500,\"event\":\"client_completed\",\"first_byte_ns\":12361667,\"header_ns\":12358250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968b1fe330-840c-3b\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-absent-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":10922417,\"event\":\"client_completed\",\"first_byte_ns\":10898125,\"header_ns\":10897125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968be32778-840c-3c\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-absent-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12238708,\"event\":\"client_completed\",\"first_byte_ns\":12135750,\"header_ns\":12131083,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968c8f70f0-840c-3d\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-absent-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":10384708,\"event\":\"client_completed\",\"first_byte_ns\":10344625,\"header_ns\":10340916,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968d50b968-840c-3e\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-upstream-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12016875,\"event\":\"client_completed\",\"first_byte_ns\":11969708,\"header_ns\":11964333,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968df6c150-840c-3f\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-upstream-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":10220541,\"event\":\"client_completed\",\"first_byte_ns\":10150125,\"header_ns\":10149125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968eb496f8-840c-40\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-upstream-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":11984208,\"event\":\"client_completed\",\"first_byte_ns\":11942792,\"header_ns\":11924583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968f570500-840c-41\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-nominated-inherit\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":10469375,\"event\":\"client_completed\",\"first_byte_ns\":10436375,\"header_ns\":10435416,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9690149070-840c-42\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-nominated-on\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12116584,\"event\":\"client_completed\",\"first_byte_ns\":12076084,\"header_ns\":12070834,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9690bb9258-840c-43\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-get-302-nominated-off\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":11951333,\"event\":\"client_completed\",\"first_byte_ns\":11918125,\"header_ns\":11917083,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96917b7758-840c-44\"},\"error\":null,\"event\":\"outbound-marker\",\"provider_hidden_nomination\":false,\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"marker-batch\",\"client\":{\"body\":\"{\\\"case\\\":\\\"marker-batch\\\",\\\"markers\\\":[null,\\\"cloudflare\\\"],\\\"qualification\\\":false}\",\"complete_ns\":10088792,\"event\":\"client_completed\",\"first_byte_ns\":10049375,\"header_ns\":10046625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c969238eb58-840c-45\"},\"error\":null,\"event\":\"outbound-marker\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"eager-single\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"eager-single\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":1,\\\"token\\\":\\\"18e05c96937aba28-840c-46\\\"}\",\"complete_ns\":330029000,\"event\":\"client_completed\",\"first_byte_ns\":329547208,\"header_ns\":328965125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96937aba28-840c-46\"},\"event\":\"outbound-progress\",\"probe\":{\"adapter\":\"cloudflare\",\"case\":\"eager-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":101000000,\"protocol\":\"http1\",\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e05c96937aba28-840c-46\"},\"qualification\":false,\"receipts\":[{\"path\":\"/g3/18e05c96937aba28-840c-46/start\",\"received_ns\":10825875},{\"path\":\"/g3/18e05c96937aba28-840c-46/slot-0\",\"received_ns\":23378709},{\"path\":\"/g3/18e05c96937aba28-840c-46/end\",\"received_ns\":118387750}],\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"lazy-single\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"lazy-single\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":102000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":1,\\\"token\\\":\\\"18e05c96a7b4dbb8-840c-47\\\"}\",\"complete_ns\":438006708,\"event\":\"client_completed\",\"first_byte_ns\":437985125,\"header_ns\":437982833,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96a7b4dbb8-840c-47\"},\"event\":\"outbound-progress\",\"probe\":{\"adapter\":\"cloudflare\",\"case\":\"lazy-single\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":102000000,\"protocol\":\"http1\",\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":1,\"token\":\"18e05c96a7b4dbb8-840c-47\"},\"qualification\":false,\"receipts\":[{\"path\":\"/g3/18e05c96a7b4dbb8-840c-47/start\",\"received_ns\":12482917},{\"path\":\"/g3/18e05c96a7b4dbb8-840c-47/end\",\"received_ns\":120101750},{\"path\":\"/g3/18e05c96a7b4dbb8-840c-47/slot-0\",\"received_ns\":132641833}],\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"eager-batch\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"eager-batch\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0,0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c96c25dfe18-840c-48\\\"}\",\"complete_ns\":329320292,\"event\":\"client_completed\",\"first_byte_ns\":329269042,\"header_ns\":329263000,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96c25dfe18-840c-48\"},\"event\":\"outbound-progress\",\"probe\":{\"adapter\":\"cloudflare\",\"case\":\"eager-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":101000000,\"protocol\":\"http1\",\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e05c96c25dfe18-840c-48\"},\"qualification\":false,\"receipts\":[{\"path\":\"/g3/18e05c96c25dfe18-840c-48/start\",\"received_ns\":12586208},{\"path\":\"/g3/18e05c96c25dfe18-840c-48/slot-0\",\"received_ns\":25027416},{\"path\":\"/g3/18e05c96c25dfe18-840c-48/slot-1\",\"received_ns\":25033708},{\"path\":\"/g3/18e05c96c25dfe18-840c-48/end\",\"received_ns\":119208375}],\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"baseline-batch\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"baseline-batch\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c96d62db190-840c-49\\\"}\",\"complete_ns\":440815375,\"event\":\"client_completed\",\"first_byte_ns\":440769292,\"header_ns\":440764333,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96d62db190-840c-49\"},\"event\":\"outbound-progress\",\"probe\":{\"adapter\":\"cloudflare\",\"case\":\"baseline-batch\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":false,\"handler_returned\":false,\"pause_ns\":101000000,\"protocol\":\"http1\",\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0],\"slots\":2,\"token\":\"18e05c96d62db190-840c-49\"},\"qualification\":false,\"receipts\":[{\"path\":\"/g3/18e05c96d62db190-840c-49/start\",\"received_ns\":12517167},{\"path\":\"/g3/18e05c96d62db190-840c-49/end\",\"received_ns\":123341583},{\"path\":\"/g3/18e05c96d62db190-840c-49/slot-0\",\"received_ns\":135888333},{\"path\":\"/g3/18e05c96d62db190-840c-49/slot-1\",\"received_ns\":135896167}],\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"case\":\"response-owned\",\"client\":{\"body\":\" {\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"response-owned\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":true,\\\"handler_returned\\\":true,\\\"pause_ns\\\":100000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0,0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c96f0f42f90-840c-4a\\\"}\",\"complete_ns\":325408292,\"event\":\"client_completed\",\"first_byte_ns\":11936292,\"header_ns\":11741958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96f0f42f90-840c-4a\"},\"event\":\"outbound-progress\",\"probe\":{\"adapter\":\"cloudflare\",\"case\":\"response-owned\",\"consumer_polls_before_window_end\":0,\"dispatch_returned\":true,\"handler_returned\":true,\"pause_ns\":100000000,\"protocol\":\"http1\",\"qualification\":false,\"revision\":\"7cccaf10734914161803317aec6c55c22c2bccc9\",\"sizes\":[0,0],\"slots\":2,\"token\":\"18e05c96f0f42f90-840c-4a\"},\"qualification\":false,\"receipts\":[{\"path\":\"/g3/18e05c96f0f42f90-840c-4a/start\",\"received_ns\":10339542},{\"path\":\"/g3/18e05c96f0f42f90-840c-4a/slot-0\",\"received_ns\":22907167},{\"path\":\"/g3/18e05c96f0f42f90-840c-4a/slot-1\",\"received_ns\":22916292},{\"path\":\"/g3/18e05c96f0f42f90-840c-4a/end\",\"received_ns\":119528375}],\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":true,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":2557500,\"event\":\"client_completed\",\"first_byte_ns\":2516709,\"header_ns\":2513625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9704ebc210-840c-4b\"},\"event\":\"inbound-empty-signal\",\"framing\":\"Absent\",\"probe\":{\"adapter\":\"cloudflare\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"body_absent\":true,\"source\":\"workerd-before-conversion\"},\"method\":\"GET\",\"qualification\":false},\"protocol\":\"http1\",\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":true,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":1365208,\"event\":\"client_completed\",\"first_byte_ns\":1320291,\"header_ns\":1316250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c97051aa5f8-840c-4c\"},\"event\":\"inbound-empty-signal\",\"framing\":\"LengthZero\",\"probe\":{\"adapter\":\"cloudflare\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"body_absent\":true,\"source\":\"workerd-before-conversion\"},\"method\":\"GET\",\"qualification\":false},\"protocol\":\"http1\",\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":1323292,\"event\":\"client_completed\",\"first_byte_ns\":1285917,\"header_ns\":1283875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c97053820d8-840c-4d\"},\"event\":\"inbound-empty-signal\",\"framing\":\"ChunkedEmpty\",\"probe\":{\"adapter\":\"cloudflare\",\"body_kind_before\":\"Initial\",\"bytes_read\":0,\"case\":\"empty\",\"host_signal\":{\"body_absent\":false,\"source\":\"workerd-before-conversion\"},\"method\":\"GET\",\"qualification\":false},\"protocol\":\"http1\",\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":4,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":1248292,\"event\":\"client_completed\",\"first_byte_ns\":1197208,\"header_ns\":1194708,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c970553d2b0-840c-4e\"},\"event\":\"inbound-empty-signal\",\"framing\":\"FixedBody\",\"probe\":{\"adapter\":\"cloudflare\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"body_absent\":false,\"source\":\"workerd-before-conversion\"},\"method\":\"GET\",\"qualification\":false},\"protocol\":\"http1\",\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"client\":{\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":4,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":1060667,\"event\":\"client_completed\",\"first_byte_ns\":1013042,\"header_ns\":1010625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c97056e4c08-840c-4f\"},\"event\":\"inbound-empty-signal\",\"framing\":\"ChunkedBody\",\"probe\":{\"adapter\":\"cloudflare\",\"body_kind_before\":\"Initial\",\"bytes_read\":4,\"case\":\"empty\",\"host_signal\":{\"body_absent\":false,\"source\":\"workerd-before-conversion\"},\"method\":\"GET\",\"qualification\":false},\"protocol\":\"http1\",\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"attempted_empty_body_cases\":5,\"attempted_progress_cases\":5,\"empty_body_passes\":5,\"event\":\"progress-coverage\",\"failures\":[],\"limitations\":[\"HTTP/2 and HTTP/3 not exercised\",\"hosted execution pending\",\"response-owned tests handler/dispatch return, not host callback return\",\"missing-context detection unproved except native absent-runtime control\",\"no cancellation or G2 readiness qualification\"],\"origin_requests\":18,\"progress_passes\":5,\"qualification\":false}\n{\"adapter\":\"cloudflare\",\"error\":null,\"event\":\"progress-group\",\"group\":\"coding\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"error\":null,\"event\":\"progress-group\",\"group\":\"marker\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"error\":null,\"event\":\"progress-group\",\"group\":\"progress-and-framing\",\"qualification\":false,\"status\":\"pass\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-200-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":113197375,\"event\":\"client_completed\",\"first_byte_ns\":112558833,\"header_ns\":112212958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c965ad5c7f8-840c-0\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-206-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":4886500,\"event\":\"client_completed\",\"first_byte_ns\":4843708,\"header_ns\":4823000,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96619d77e8-840c-1\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":10188166,\"event\":\"client_completed\",\"first_byte_ns\":10039375,\"header_ns\":10037625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9661ef2de0-840c-2\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-gzip-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11081000,\"event\":\"client_completed\",\"first_byte_ns\":11041875,\"header_ns\":11038791,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9662910f48-840c-3\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-200-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11517000,\"event\":\"client_completed\",\"first_byte_ns\":11494292,\"header_ns\":11492708,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9663400840-840c-4\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-206-original\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11051500,\"event\":\"client_completed\",\"first_byte_ns\":11015125,\"header_ns\":10988000,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9663f640b0-840c-5\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[31,139,8,0,0,0,0,0,0,19,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,25,107,60,67,17,0,0,0],\\\"case\\\":\\\"coding-gzip-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"gzip\\\",\\\"content_length\\\":\\\"37\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11921042,\"event\":\"client_completed\",\"first_byte_ns\":11879042,\"header_ns\":11878084,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9664a62fc0-840c-6\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-gzip-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12228000,\"event\":\"client_completed\",\"first_byte_ns\":12199292,\"header_ns\":12195958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9665631720-840c-7\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-200-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12330125,\"event\":\"client_completed\",\"first_byte_ns\":12300833,\"header_ns\":12297250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9666239860-840c-8\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-206-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":10552750,\"event\":\"client_completed\",\"first_byte_ns\":10535833,\"header_ns\":10529041,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9666e5fa18-840c-9\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12100958,\"event\":\"client_completed\",\"first_byte_ns\":12070917,\"header_ns\":12065125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96678dcb08-840c-a\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-deflate-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10064292,\"event\":\"client_completed\",\"first_byte_ns\":10034792,\"header_ns\":10032417,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96684dfa40-840c-b\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-200-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11630750,\"event\":\"client_completed\",\"first_byte_ns\":11603333,\"header_ns\":11599583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9668eda540-840c-c\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-206-original\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12015959,\"event\":\"client_completed\",\"first_byte_ns\":11971750,\"header_ns\":11970292,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9669a59ee8-840c-d\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[120,156,43,74,44,87,72,206,79,201,204,75,87,40,40,202,79,74,229,2,0,57,147,6,33],\\\"case\\\":\\\"coding-deflate-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"deflate\\\",\\\"content_length\\\":\\\"25\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12200625,\"event\":\"client_completed\",\"first_byte_ns\":12184625,\"header_ns\":12182625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966a63a758-840c-e\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-deflate-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12133583,\"event\":\"client_completed\",\"first_byte_ns\":12099208,\"header_ns\":12098166,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966b243c20-840c-f\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-200-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11525709,\"event\":\"client_completed\",\"first_byte_ns\":11486875,\"header_ns\":11484209,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966be34e30-840c-10\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-206-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12077750,\"event\":\"client_completed\",\"first_byte_ns\":12042917,\"header_ns\":12041792,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966c98fa00-840c-11\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-buffered-206-decode\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":12475375,\"event\":\"client_completed\",\"first_byte_ns\":12444792,\"header_ns\":12441458,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966d565a78-840c-12\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-br-buffered-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10961083,\"event\":\"client_completed\",\"first_byte_ns\":10933000,\"header_ns\":10931125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966e1b19a8-840c-13\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-200-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10814791,\"event\":\"client_completed\",\"first_byte_ns\":10682333,\"header_ns\":10677666,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966ec89f88-840c-14\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-206-original\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11416125,\"event\":\"client_completed\",\"first_byte_ns\":11316542,\"header_ns\":10985833,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c966f75b420-840c-15\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[11,8,128,114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10,3],\\\"case\\\":\\\"coding-br-streamed-206-decode\\\",\\\"content_encoding\\\":\\\"br\\\",\\\"content_length\\\":\\\"21\\\",\\\"qualification\\\":false,\\\"status\\\":206}\",\"complete_ns\":11727000,\"event\":\"client_completed\",\"first_byte_ns\":11694083,\"header_ns\":11692791,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967029ecd8-840c-16\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"bytes\\\":[114,97,119,32,99,111,100,105,110,103,32,112,114,111,98,101,10],\\\"case\\\":\\\"coding-br-streamed-200-decode\\\",\\\"content_encoding\\\":null,\\\"content_length\\\":null,\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11244833,\"event\":\"client_completed\",\"first_byte_ns\":11204375,\"header_ns\":11202958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9670e2e080-840c-17\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":13956709,\"event\":\"client_completed\",\"first_byte_ns\":13913459,\"header_ns\":13911625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96722eb5b8-840c-18\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11548500,\"event\":\"client_completed\",\"first_byte_ns\":11509000,\"header_ns\":11505542,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96730a8b10-840c-19\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11497667,\"event\":\"client_completed\",\"first_byte_ns\":11468542,\"header_ns\":11467334,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9673c1b5b0-840c-1a\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12147375,\"event\":\"client_completed\",\"first_byte_ns\":12115084,\"header_ns\":12113709,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9674783088-840c-1b\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11278500,\"event\":\"client_completed\",\"first_byte_ns\":11135875,\"header_ns\":11132541,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9675382910-840c-1c\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":9667292,\"event\":\"client_completed\",\"first_byte_ns\":9630625,\"header_ns\":9629042,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9675ecea80-840c-1d\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12305542,\"event\":\"client_completed\",\"first_byte_ns\":12262417,\"header_ns\":12259375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967686c150-840c-1e\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11818792,\"event\":\"client_completed\",\"first_byte_ns\":11777750,\"header_ns\":11776333,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967748a9f0-840c-1f\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-200-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11702750,\"event\":\"client_completed\",\"first_byte_ns\":11664000,\"header_ns\":11663083,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96780385e0-840c-20\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11873375,\"event\":\"client_completed\",\"first_byte_ns\":11792833,\"header_ns\":11790041,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9678bd3120-840c-21\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12077792,\"event\":\"client_completed\",\"first_byte_ns\":11998542,\"header_ns\":11995375,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96797987f8-840c-22\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":13296917,\"event\":\"client_completed\",\"first_byte_ns\":13137417,\"header_ns\":12016208,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967a384be8-840c-23\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":10200750,\"event\":\"client_completed\",\"first_byte_ns\":10119833,\"header_ns\":10114917,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967b0b3800-840c-24\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12345958,\"event\":\"client_completed\",\"first_byte_ns\":12218750,\"header_ns\":12215583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967bad8e98-840c-25\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":11872125,\"event\":\"client_completed\",\"first_byte_ns\":11794417,\"header_ns\":11790917,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967c712100-840c-26\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12041875,\"event\":\"client_completed\",\"first_byte_ns\":11952083,\"header_ns\":11950708,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967d2d6450-840c-27\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12031750,\"event\":\"client_completed\",\"first_byte_ns\":11999708,\"header_ns\":11994875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967deb1ab8-840c-28\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-204-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":204}\",\"complete_ns\":12356250,\"event\":\"client_completed\",\"first_byte_ns\":12301250,\"header_ns\":12299750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967ea87b30-840c-29\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12146833,\"event\":\"client_completed\",\"first_byte_ns\":12091083,\"header_ns\":12086875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c967f6b3e90-840c-2a\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11424208,\"event\":\"client_completed\",\"first_byte_ns\":11385917,\"header_ns\":11382292,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96802af898-840c-2b\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10412084,\"event\":\"client_completed\",\"first_byte_ns\":10329042,\"header_ns\":10325292,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9680dfe500-840c-2c\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11726167,\"event\":\"client_completed\",\"first_byte_ns\":11693208,\"header_ns\":11688625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9681853550-840c-2d\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12188417,\"event\":\"client_completed\",\"first_byte_ns\":11805750,\"header_ns\":11801333,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9682409228-840c-2e\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":10038250,\"event\":\"client_completed\",\"first_byte_ns\":10006000,\"header_ns\":10004583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96830103c8-840c-2f\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":11895583,\"event\":\"client_completed\",\"first_byte_ns\":11847000,\"header_ns\":11845291,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9683a02228-840c-30\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":9661291,\"event\":\"client_completed\",\"first_byte_ns\":9589625,\"header_ns\":9586958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96845c2ae0-840c-31\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-head-200-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":200}\",\"complete_ns\":12159375,\"event\":\"client_completed\",\"first_byte_ns\":12121541,\"header_ns\":12118583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9684f5abc0-840c-32\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10497375,\"event\":\"client_completed\",\"first_byte_ns\":10401583,\"header_ns\":10398083,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9685b52360-840c-33\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10024292,\"event\":\"client_completed\",\"first_byte_ns\":9991125,\"header_ns\":9987959,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96865b0ff0-840c-34\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11061584,\"event\":\"client_completed\",\"first_byte_ns\":10966000,\"header_ns\":10959084,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9686fa16e0-840c-35\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11344167,\"event\":\"client_completed\",\"first_byte_ns\":11217292,\"header_ns\":11215834,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9687aa5410-840c-36\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":11691833,\"event\":\"client_completed\",\"first_byte_ns\":11611542,\"header_ns\":11610542,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96885d4c78-840c-37\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10943375,\"event\":\"client_completed\",\"first_byte_ns\":10907625,\"header_ns\":10901791,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968915e260-840c-38\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":12060417,\"event\":\"client_completed\",\"first_byte_ns\":12027125,\"header_ns\":12024750,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9689c2c048-840c-39\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":10078833,\"event\":\"client_completed\",\"first_byte_ns\":10006166,\"header_ns\":10005291,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968a80b530-840c-3a\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-304-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":304}\",\"complete_ns\":12399500,\"event\":\"client_completed\",\"first_byte_ns\":12361667,\"header_ns\":12358250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968b1fe330-840c-3b\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":10922417,\"event\":\"client_completed\",\"first_byte_ns\":10898125,\"header_ns\":10897125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968be32778-840c-3c\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12238708,\"event\":\"client_completed\",\"first_byte_ns\":12135750,\"header_ns\":12131083,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968c8f70f0-840c-3d\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-absent-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":10384708,\"event\":\"client_completed\",\"first_byte_ns\":10344625,\"header_ns\":10340916,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968d50b968-840c-3e\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12016875,\"event\":\"client_completed\",\"first_byte_ns\":11969708,\"header_ns\":11964333,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968df6c150-840c-3f\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":10220541,\"event\":\"client_completed\",\"first_byte_ns\":10150125,\"header_ns\":10149125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968eb496f8-840c-40\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-upstream-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[\\\"upstream\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":11984208,\"event\":\"client_completed\",\"first_byte_ns\":11942792,\"header_ns\":11924583,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c968f570500-840c-41\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-inherit\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":10469375,\"event\":\"client_completed\",\"first_byte_ns\":10436375,\"header_ns\":10435416,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9690149070-840c-42\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-on\\\",\\\"enabled\\\":true,\\\"markers\\\":[\\\"cloudflare\\\"],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":12116584,\"event\":\"client_completed\",\"first_byte_ns\":12076084,\"header_ns\":12070834,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9690bb9258-840c-43\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-get-302-nominated-off\\\",\\\"enabled\\\":false,\\\"markers\\\":[],\\\"qualification\\\":false,\\\"status\\\":302}\",\"complete_ns\":11951333,\"event\":\"client_completed\",\"first_byte_ns\":11918125,\"header_ns\":11917083,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96917b7758-840c-44\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"case\\\":\\\"marker-batch\\\",\\\"markers\\\":[null,\\\"cloudflare\\\"],\\\"qualification\\\":false}\",\"complete_ns\":10088792,\"event\":\"client_completed\",\"first_byte_ns\":10049375,\"header_ns\":10046625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c969238eb58-840c-45\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"eager-single\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":1,\\\"token\\\":\\\"18e05c96937aba28-840c-46\\\"}\",\"complete_ns\":330029000,\"event\":\"client_completed\",\"first_byte_ns\":329547208,\"header_ns\":328965125,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96937aba28-840c-46\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"lazy-single\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":102000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":1,\\\"token\\\":\\\"18e05c96a7b4dbb8-840c-47\\\"}\",\"complete_ns\":438006708,\"event\":\"client_completed\",\"first_byte_ns\":437985125,\"header_ns\":437982833,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96a7b4dbb8-840c-47\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"eager-batch\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0,0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c96c25dfe18-840c-48\\\"}\",\"complete_ns\":329320292,\"event\":\"client_completed\",\"first_byte_ns\":329269042,\"header_ns\":329263000,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96c25dfe18-840c-48\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"baseline-batch\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":false,\\\"handler_returned\\\":false,\\\"pause_ns\\\":101000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c96d62db190-840c-49\\\"}\",\"complete_ns\":440815375,\"event\":\"client_completed\",\"first_byte_ns\":440769292,\"header_ns\":440764333,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96d62db190-840c-49\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\" {\\\"adapter\\\":\\\"cloudflare\\\",\\\"case\\\":\\\"response-owned\\\",\\\"consumer_polls_before_window_end\\\":0,\\\"dispatch_returned\\\":true,\\\"handler_returned\\\":true,\\\"pause_ns\\\":100000000,\\\"qualification\\\":false,\\\"revision\\\":\\\"7cccaf10734914161803317aec6c55c22c2bccc9\\\",\\\"sizes\\\":[0,0],\\\"slots\\\":2,\\\"token\\\":\\\"18e05c96f0f42f90-840c-4a\\\"}\",\"complete_ns\":325408292,\"event\":\"client_completed\",\"first_byte_ns\":11936292,\"header_ns\":11741958,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c96f0f42f90-840c-4a\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":true,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":2557500,\"event\":\"client_completed\",\"first_byte_ns\":2516709,\"header_ns\":2513625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c9704ebc210-840c-4b\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":true,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":1365208,\"event\":\"client_completed\",\"first_byte_ns\":1320291,\"header_ns\":1316250,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c97051aa5f8-840c-4c\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":0,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":1323292,\"event\":\"client_completed\",\"first_byte_ns\":1285917,\"header_ns\":1283875,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c97053820d8-840c-4d\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":4,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":1248292,\"event\":\"client_completed\",\"first_byte_ns\":1197208,\"header_ns\":1194708,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c970553d2b0-840c-4e\",\"variant\":\"cloudflare-outbound-progress\"}\n{\"adapter\":\"cloudflare\",\"body\":\"{\\\"adapter\\\":\\\"cloudflare\\\",\\\"body_kind_before\\\":\\\"Initial\\\",\\\"bytes_read\\\":4,\\\"case\\\":\\\"empty\\\",\\\"host_signal\\\":{\\\"body_absent\\\":false,\\\"source\\\":\\\"workerd-before-conversion\\\"},\\\"method\\\":\\\"GET\\\",\\\"qualification\\\":false}\",\"complete_ns\":1060667,\"event\":\"client_completed\",\"first_byte_ns\":1013042,\"header_ns\":1010625,\"headers\":[[\"Transfer-Encoding\",\"chunked\"],[\"Content-Type\",\"application/json\"]],\"status\":200,\"token\":\"18e05c97056e4c08-840c-4f\",\"variant\":\"cloudflare-outbound-progress\"}\n" + }, + "summary": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-final/summary.json", + "bytes": 303, + "sha256": "5d0a157d796b357d15561997480a12cc58f9dede469570dfbe91f016cab2ab9f", + "text": "{\n \"build_identity\": {\n \"status\": \"consistent\"\n },\n \"cpu_comparison\": \"SDK phase readings only; cross-run performance unverified\",\n \"guests\": [],\n \"memory_snapshots\": {},\n \"quantile_method\": \"nearest rank\",\n \"recorded_client_completions\": 80,\n \"recorded_request_starts\": 0,\n \"variants\": {}\n}" + }, + "runtime_log": { + "path": "/private/tmp/edgezero-cloudflare-progress-20261020-final/progress-cloudflare/runtime.log", + "bytes": 8100, + "sha256": "4eb9ef684f8ac8577e47dd10db9068a0377abc75b5d554287e3911e4808d5e49", + "text": "\n \u26c5\ufe0f wrangler 4.131.0 (update available 4.151.3)\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nYour Worker has access to the following bindings:\nBinding Resource Mode\nenv.fixture_config (00000000000000000000000000000001) KV Namespace local\nenv.fixture_kv (00000000000000000000000000000002) KV Namespace local\nenv.FIXTURE_MODE (\"(hidden)\") Environment Variable local\nenv.fixture_marker (\"fixture-only-value\") Environment Variable local\n\n\u2394 Starting local server...\nWrangler detected this dev session is running in an AI agent.\nThe Local Explorer API is available at http://127.0.0.1:60763/cdn-cgi/local/explorer/api\nUseful routes:\n GET http://127.0.0.1:60763/cdn-cgi/local/explorer/api/local/workers - local Workers and bindings\n GET http://127.0.0.1:60763/cdn-cgi/local/explorer/api/storage/kv/namespaces - KV namespaces\n GET http://127.0.0.1:60763/cdn-cgi/local/explorer/api/d1/database - D1 databases\n GET http://127.0.0.1:60763/cdn-cgi/local/explorer/api/r2/buckets - R2 buckets\n GET http://127.0.0.1:60763/cdn-cgi/local/explorer/api/workers/durable_objects/namespaces - Durable Object namespaces\n GET http://127.0.0.1:60763/cdn-cgi/local/explorer/api/workflows - Workflows\n POST http://127.0.0.1:60763/cdn-cgi/local/explorer/api/local/observability/query - run a read-only SQL query (SELECT/WITH only) over captured request traces and console logs. Tables: spans, logs (read attributes via json(attributes)). Example:\n curl -X POST http://127.0.0.1:60763/cdn-cgi/local/explorer/api/local/observability/query -H 'Content-Type: application/json' -d '{\"sql\":\"SELECT service, name, outcome, duration_ms FROM spans WHERE parent_id IS NULL LIMIT 20\"}'\nIf the routes above don't cover what you need, fetch the full OpenAPI schema (large - use only as a last resort):\n GET http://127.0.0.1:60763/cdn-cgi/local/explorer/api - OpenAPI schema\n\u001b[33m\u25b2 \u001b[43;33m[\u001b[43;30mWARNING\u001b[43;33m]\u001b[0m \u001b[1mservice core:user:reusable-app-fixture: The compatibility flag enable_weak_ref became the default as of 2025-05-05 so does not need to be specified anymore.\u001b[0m\n\n\n[wrangler:info] Ready on http://127.0.0.1:60763\n[wrangler:info] GET /progress/coding-gzip-buffered-200-original 200 OK (112ms)\n[wrangler:info] GET /progress/coding-gzip-buffered-206-original 200 OK (4ms)\n[wrangler:info] GET /progress/coding-gzip-buffered-206-decode 200 OK (10ms)\n[wrangler:info] GET /progress/coding-gzip-buffered-200-decode 200 OK (10ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-200-original 200 OK (11ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-206-original 200 OK (11ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-206-decode 200 OK (11ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-200-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-200-original 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-206-original 200 OK (11ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-206-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-200-decode 200 OK (10ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-200-original 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-206-original 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-206-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-200-decode 200 OK (11ms)\n[wrangler:info] GET /progress/coding-br-buffered-200-original 200 OK (11ms)\n[wrangler:info] GET /progress/coding-br-buffered-206-original 200 OK (12ms)\n[wrangler:info] GET /progress/coding-br-buffered-206-decode 200 OK (13ms)\n[wrangler:info] GET /progress/coding-br-buffered-200-decode 200 OK (11ms)\n[wrangler:info] GET /progress/coding-br-streamed-200-original 200 OK (10ms)\n[wrangler:info] GET /progress/coding-br-streamed-206-original 200 OK (11ms)\n[wrangler:info] GET /progress/coding-br-streamed-206-decode 200 OK (11ms)\n[wrangler:info] GET /progress/coding-br-streamed-200-decode 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-200-absent-inherit 200 OK (13ms)\n[wrangler:info] GET /progress/marker-get-200-absent-on 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-200-absent-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-200-upstream-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-200-upstream-on 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-200-upstream-off 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-200-nominated-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-200-nominated-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-200-nominated-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-204-absent-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-204-absent-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-204-absent-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-204-upstream-inherit 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-204-upstream-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-204-upstream-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-204-nominated-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-204-nominated-on 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-204-nominated-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-head-200-absent-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-head-200-absent-on 200 OK (11ms)\n[wrangler:info] GET /progress/marker-head-200-absent-off 200 OK (10ms)\n[wrangler:info] GET /progress/marker-head-200-upstream-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-head-200-upstream-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-head-200-upstream-off 200 OK (10ms)\n[wrangler:info] GET /progress/marker-head-200-nominated-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-head-200-nominated-on 200 OK (9ms)\n[wrangler:info] GET /progress/marker-head-200-nominated-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-304-absent-inherit 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-304-absent-on 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-304-absent-off 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-304-upstream-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-304-upstream-on 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-304-upstream-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-304-nominated-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-304-nominated-on 200 OK (9ms)\n[wrangler:info] GET /progress/marker-get-304-nominated-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-302-absent-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-302-absent-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-302-absent-off 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-302-upstream-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-302-upstream-on 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-302-upstream-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-302-nominated-inherit 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-302-nominated-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-302-nominated-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-batch 200 OK (10ms)\n[wrangler:info] GET /progress/eager-single 200 OK (329ms)\n[wrangler:info] GET /progress/lazy-single 200 OK (438ms)\n[wrangler:info] GET /progress/eager-batch 200 OK (329ms)\n[wrangler:info] GET /progress/baseline-batch 200 OK (441ms)\n[wrangler:info] GET /progress/response-owned 200 OK (11ms)\n[wrangler:info] GET /progress/empty 200 OK (2ms)\n[wrangler:info] GET /progress/empty 200 OK (0ms)\n[wrangler:info] GET /progress/empty 200 OK (2ms)\n[wrangler:info] GET /progress/empty 200 OK (1ms)\n[wrangler:info] GET /progress/empty 200 OK (1ms)\n" + } + } + ] +} diff --git a/docs/audits/evidence/2026-10-20-outbound-marker.json b/docs/audits/evidence/2026-10-20-outbound-marker.json new file mode 100644 index 000000000..f20b5e8a4 --- /dev/null +++ b/docs/audits/evidence/2026-10-20-outbound-marker.json @@ -0,0 +1,1865 @@ +{ + "schema": 1, + "recorded_date": "2026-10-20", + "base_revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "scope": "client/request proxy marker on adapter-visible headers; not raw-header fidelity or hosted qualification", + "source_sha256": { + "Cargo.toml": "ee73d1318af74a997ecc12bac0f80cd7eefbe8ead34a35b1432c8d6f49fc42e1", + "Cargo.lock": "1afb33fe3067419ad5a2a948b330ac1df123d53214e49dc4005a92f7bc69e043", + "crates/edgezero-core/src/outbound.rs": "878081903443f72ee3bbd23976390bc426af09a213121bb7394218edf2224420", + "crates/edgezero-core/src/outbound/exchange.rs": "f8ca9867b572b2e40e0c08ae55fefefaefbf82355c83aa8b28f74870d6e2a879", + "crates/edgezero-core/src/lib.rs": "88fd8859f734b1c3f750f48a38e6c3eb69584128243acc0243eca6f03f923b31", + "crates/edgezero-core/src/compression.rs": "4acccc0c01009894ebe4f77f57ce8ce501d3f3f2ccb565c6b3d3871d826037d4", + "crates/edgezero-core/src/body.rs": "50a8bc65fcbc7df3d9afd3ddc520889f42da46af1c6419e23c19625c33d5bf9e", + "crates/edgezero-core/src/time.rs": "bcd7047587f656b6547c860986f0c8f6459a5554850cbac4c1bfd9c480ec7435", + "crates/edgezero-adapter-axum/src/outbound.rs": "cc6e1f50eb1d85c42100f73ad850109433d56fe43e249f3cdb7203553f0a23a2", + "crates/edgezero-adapter-cloudflare/src/outbound.rs": "25dbde10a5502c1644a791ce188d39531689d0f1c138594ba17cc23c5854072c", + "crates/edgezero-adapter-fastly/src/outbound.rs": "4284cc2af486e2bed63beb6434332965832dbc15a6fc40a0b538bcc54b5a3f31", + "crates/edgezero-adapter-spin/src/outbound.rs": "be9f5ff721bcc47ed530deab39dbdff4058b1ec4e97865c2b8bbb65ae08c1ad5", + "crates/edgezero-adapter-spin/src/cli.rs": "03e5772b7f0d55e56492547fb13791cc2c033338f5ed3eeb2b8f5c14a8c81364", + "crates/edgezero-adapter-spin/src/response.rs": "03e8b98de3e4854810b4bdabba7783d3a66cacb5e91402e941609d7dfe567075", + "tests/fixtures/reusable-app/Cargo.toml": "7665200757e4e565d11fa7eb036215aa583d21fd543bca39fdbdce470fee11ae", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs": "295b98872ab59c185f6907189dad13c5c4c56699538d9756872d1c7f82cb6812", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/progress.rs": "b29022de35c338efd6969b4f0f254a3c3dbf1036600364f593d7eef0288a3758", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs": "8719b6eb05c39b514f2f571166628a5d919935f3ebbb9e69be9b854bb416d4a3", + "tests/fixtures/reusable-app/crates/fixture-axum/src/progress.rs": "0c41390031d857f0589667b88bd27fc8e185b483cedc57ec2ee73c99d36a9e4a", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs": "0b4a81e3678459801b9f5a9379de70160452317929936d1b6c85393c1249c704", + "tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs": "0a20af88ab9a94fe8ef806b15e38994c7790ec0f9040add4c3dec4ead9f022db", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs": "305f70039af59501639916bd8cfb8db696613c73af670e953dd329a3eed14498" + }, + "limitations": [ + "not an authoritative frozen candidate; these selected source hashes characterize a working tree", + "hosted execution pending", + "Cloudflare runtime blocked on locked wasm-bindgen 0.2.129 CLI availability; no replacement tool installed or dependency repinned", + "Spin hides Connection before EdgeZero receives SDK-native response fields; 15 nominated-shape cases pass only against adapter-visible fields", + "Spin OutboundHeaderFidelity is BestEffort; marker-off cannot remove a nomination the host hides", + "head-first, eager begin and redirect-follow migration remain unimplemented", + "Fastly readiness gate remains failed; current dispatch-slack guard and body-read engine retained" + ], + "diagnostic_history": { + "adapter": "spin", + "command_exit_code": 1, + "raw_events_path": "/private/tmp/edgezero-marker-spin-20261019/events.jsonl", + "raw_events_sha256": "f17cdf68ebe9ec19e713b709cc940fe6b4d9f2034fc03db513515d0ffa921225", + "initial_failed_case": { + "adapter": "spin", + "case": "marker-get-200-nominated-inherit", + "client": { + "body": "{\"case\":\"marker-get-200-nominated-inherit\",\"enabled\":false,\"markers\":[\"upstream\"],\"qualification\":false,\"status\":200}", + "complete_ns": 10866125, + "event": "client_completed", + "first_byte_ns": 10853875, + "header_ns": 10852083, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 06:46:33 GMT"] + ], + "status": 200, + "token": "18e029f751d5fde8-115d6-1e" + }, + "error": "marker policy/status mismatch", + "event": "outbound-marker", + "qualification": false, + "status": "fail" + }, + "source_snapshot": "initial source snapshot not retained; no source equivalence to the rerun asserted", + "resolution": "SDK-native controls independently show absent Connection and retained upstream marker for ordinary and nominated fields; retain ordinary fields in Off mode, record hidden nomination and demote header fidelity rather than claim raw normalization" + }, + "runs": [ + { + "adapter": "axum", + "command": "cargo run --locked --offline --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -- --adapter axum --suite outbound-progress --output /private/tmp/edgezero-marker-axum-20261020", + "exit_code": 0, + "environment": { + "event": "build_environment", + "host_arch": "aarch64", + "host_os": "macos", + "lockfile": { + "bytes": 84127, + "fnv1a64": "10d93fc12c84db43", + "path": "/private/tmp/edgezero-marker-axum-20261020/Cargo.lock", + "purpose": "non-security build identity" + }, + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)" + }, + "runtime": { + "adapter": "axum", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "cargo 1.98.1 (797e8a9bc 2026-08-05)" + }, + "artifact": { + "bytes": 8871776, + "fnv1a64": "d6efca5a500547a8", + "path": "/private/tmp/edgezero-marker-axum-20261020/progress-axum/artifacts/fixture-axum-outbound-progress", + "purpose": "non-security build identity", + "sha256": "fe02353170d2ecae021113d6f1fd90a7518056f2d26c4a2939d17c2884e1150e" + }, + "raw_events_path": "/private/tmp/edgezero-marker-axum-20261020/events.jsonl", + "raw_events_sha256": "a1b6d4f96fcdf1041c9634f65ef9509d5aa79414e2d3a53a70726a4bd890c039", + "coding_regression_controls_passed": 24, + "sdk_header_visibility_controls": [], + "provider_hidden_nomination_cases": 0, + "checks": [ + { + "case": "marker-get-200-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-absent-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-upstream-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-nominated-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-nominated-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-nominated-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-nominated-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-nominated-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-204-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-absent-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-upstream-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-nominated-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-nominated-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-nominated-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-nominated-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-nominated-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-head-200-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-absent-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-upstream-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-nominated-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-nominated-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-nominated-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-nominated-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-nominated-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-304-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-absent-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-upstream-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-nominated-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-nominated-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-nominated-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-nominated-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-nominated-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-302-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-absent-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-upstream-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-nominated-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-nominated-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-nominated-on", + "enabled": true, + "markers": ["axum"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-nominated-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-nominated-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-batch", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-batch", + "markers": [null, "axum"], + "qualification": false + } + } + ] + }, + { + "adapter": "fastly", + "command": "cargo run --locked --offline --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -- --adapter fastly --suite outbound-progress --output /private/tmp/edgezero-marker-fastly-final", + "exit_code": 0, + "environment": { + "event": "build_environment", + "host_arch": "aarch64", + "host_os": "macos", + "lockfile": { + "bytes": 84127, + "fnv1a64": "10d93fc12c84db43", + "path": "/private/tmp/edgezero-marker-fastly-final/Cargo.lock", + "purpose": "non-security build identity" + }, + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)" + }, + "runtime": { + "adapter": "fastly", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "viceroy 0.21.0" + }, + "artifact": { + "bytes": 2141949, + "fnv1a64": "44648754657ea5e8", + "path": "/private/tmp/edgezero-marker-fastly-final/progress-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "purpose": "non-security build identity", + "sha256": "3f5ca3689ddfefeb245087a77bb952bd139e166ac493d9e6b5739b8beff4832f" + }, + "raw_events_path": "/private/tmp/edgezero-marker-fastly-final/events.jsonl", + "raw_events_sha256": "824f05e5479f4e854bb407e527553b9fe52d17b5d82834cdf82a973f922c5377", + "coding_regression_controls_passed": 24, + "sdk_header_visibility_controls": [], + "provider_hidden_nomination_cases": 0, + "checks": [ + { + "case": "marker-get-200-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-absent-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-upstream-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-nominated-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-nominated-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-nominated-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-nominated-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-nominated-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-204-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-absent-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-upstream-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-nominated-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-nominated-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-nominated-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-nominated-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-nominated-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-head-200-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-absent-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-upstream-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-nominated-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-nominated-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-nominated-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-nominated-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-nominated-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-304-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-absent-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-upstream-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-nominated-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-nominated-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-nominated-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-nominated-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-nominated-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-302-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-absent-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-upstream-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-nominated-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-nominated-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-nominated-on", + "enabled": true, + "markers": ["fastly"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-nominated-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-nominated-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-batch", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-batch", + "markers": [null, "fastly"], + "qualification": false + } + } + ] + }, + { + "adapter": "spin", + "command": "cargo run --locked --offline --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -- --adapter spin --suite outbound-progress --output /private/tmp/edgezero-marker-spin-visibility2", + "exit_code": 0, + "environment": { + "event": "build_environment", + "host_arch": "aarch64", + "host_os": "macos", + "lockfile": { + "bytes": 84127, + "fnv1a64": "10d93fc12c84db43", + "path": "/private/tmp/edgezero-marker-spin-visibility2/Cargo.lock", + "purpose": "non-security build identity" + }, + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)" + }, + "runtime": { + "adapter": "spin", + "event": "progress-runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "revision": "7cccaf10734914161803317aec6c55c22c2bccc9", + "source_state": "working-tree-characterization", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + }, + "artifact": { + "bytes": 2308687, + "fnv1a64": "cf46233d56aa1321", + "path": "/private/tmp/edgezero-marker-spin-visibility2/progress-spin/artifacts/fixture_spin.wasm", + "purpose": "non-security build identity", + "sha256": "54fb2da731641ee0cd0b4e1f2e91a187e3284b9df9d67e29e9ae1611bfc52035" + }, + "raw_events_path": "/private/tmp/edgezero-marker-spin-visibility2/events.jsonl", + "raw_events_sha256": "63d1bdae2c037f6bb18984b9e68a3a532a77609b0d84656dd4a3d5e23461e449", + "coding_regression_controls_passed": 24, + "sdk_header_visibility_controls": [ + { + "shape": "upstream", + "probe": { + "case": "sdk-marker-headers", + "connection": [], + "markers": ["upstream"], + "qualification": false, + "sdk_native": true + }, + "client_status": 200 + }, + { + "shape": "nominated", + "probe": { + "case": "sdk-marker-headers", + "connection": [], + "markers": ["upstream"], + "qualification": false, + "sdk_native": true + }, + "client_status": 200 + } + ], + "provider_hidden_nomination_cases": 15, + "checks": [ + { + "case": "marker-get-200-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-absent-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-upstream-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-200-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-200-nominated-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-nominated-on", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-200-nominated-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-200-nominated-off", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-200-nominated-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-204-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-absent-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-upstream-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-204-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-204-nominated-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-nominated-on", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-204-nominated-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-get-204-nominated-off", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-204-nominated-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 204 + } + }, + { + "case": "marker-head-200-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-absent-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-upstream-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-head-200-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-head-200-nominated-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-nominated-on", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-head-200-nominated-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-head-200-nominated-off", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-head-200-nominated-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 200 + } + }, + { + "case": "marker-get-304-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-absent-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-upstream-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-304-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-304-nominated-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-nominated-on", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-304-nominated-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-304-nominated-off", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-304-nominated-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 304 + } + }, + { + "case": "marker-get-302-absent-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-absent-inherit", + "enabled": false, + "markers": [], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-absent-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-absent-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-absent-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-absent-off", + "enabled": false, + "markers": [], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-upstream-inherit", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-upstream-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-upstream-on", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-upstream-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-upstream-off", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-get-302-upstream-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-nominated-inherit", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-302-nominated-inherit", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-nominated-on", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-302-nominated-on", + "enabled": true, + "markers": ["spin"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-get-302-nominated-off", + "result": "pass", + "provider_hidden_nomination": true, + "observed": { + "case": "marker-get-302-nominated-off", + "enabled": false, + "markers": ["upstream"], + "qualification": false, + "status": 302 + } + }, + { + "case": "marker-batch", + "result": "pass", + "provider_hidden_nomination": false, + "observed": { + "case": "marker-batch", + "markers": [null, "spin"], + "qualification": false + } + } + ] + } + ] +} diff --git a/docs/audits/evidence/2026-10-20-outbound-relay.json b/docs/audits/evidence/2026-10-20-outbound-relay.json new file mode 100644 index 000000000..f34eb38ae --- /dev/null +++ b/docs/audits/evidence/2026-10-20-outbound-relay.json @@ -0,0 +1,5377 @@ +{ + "schema": 1, + "recorded_date": "2026-10-20", + "base_revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "scope": "actual HTTP/1 downstream relay through standard entrypoints and existing into_response converters on four local runtimes", + "command": "SSL_CERT_FILE=/etc/ssl/cert.pem WASM_BINDGEN_BIN=/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen WORKER_BUILD_BIN=/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build ESBUILD_BIN=/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild WASM_OPT_BIN=/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt WRANGLER_BIN=/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler WRANGLER_LOG_PATH=/private/tmp/edgezero-pr275-wrangler-logs WRANGLER_SEND_METRICS=false cargo run --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -- --adapter all --suite outbound-progress --output /private/tmp/edgezero-relay-all-20261020-final", + "exit_code": 0, + "environment": { + "WASM_BINDGEN_BIN": "/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen", + "WORKER_BUILD_BIN": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build", + "ESBUILD_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild", + "WASM_OPT_BIN": "/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt", + "WRANGLER_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler", + "WRANGLER_LOG_PATH": "/private/tmp/edgezero-pr275-wrangler-logs", + "WRANGLER_SEND_METRICS": "false", + "MINIFLARE_WORKERD_PATH": null, + "node": "v24.20.0", + "mode": "four local runtimes with loopback-only origins; local synthetic workerd bindings, no deployment", + "tool_install": "authorized locked wasm-bindgen-cli =0.2.129 installed under /private/tmp only; no project dependency repin or global PATH change", + "SSL_CERT_FILE": "/etc/ssl/cert.pem" + }, + "source_snapshot_scope": "selected implementation/fixture inputs; not an authoritative full-tree candidate", + "source_sha256": { + "Cargo.lock": "1afb33fe3067419ad5a2a948b330ac1df123d53214e49dc4005a92f7bc69e043", + "Cargo.toml": "ee73d1318af74a997ecc12bac0f80cd7eefbe8ead34a35b1432c8d6f49fc42e1", + "crates/edgezero-adapter-axum/src/outbound.rs": "cc6e1f50eb1d85c42100f73ad850109433d56fe43e249f3cdb7203553f0a23a2", + "crates/edgezero-adapter-axum/src/request.rs": "0c61c652c7e3455d35b9802b26b6118b68c678bdfe01cc1215c7a9ec931cddef", + "crates/edgezero-adapter-axum/src/response.rs": "f35df64dcb1c90a19720739b10dd72903bc73d92a43734d80d1f38cdab29ac51", + "crates/edgezero-adapter-cloudflare/src/outbound.rs": "25dbde10a5502c1644a791ce188d39531689d0f1c138594ba17cc23c5854072c", + "crates/edgezero-adapter-cloudflare/src/request.rs": "a755088e4e65e5af813335283ec3095c35b5982d1e9057ce58dd748aa03e6200", + "crates/edgezero-adapter-cloudflare/src/response.rs": "15a5b643713d3fdd2bd965fd00d9b606ca5cddf754678c3b0a0b9a65bba21adb", + "crates/edgezero-adapter-fastly/src/outbound.rs": "4284cc2af486e2bed63beb6434332965832dbc15a6fc40a0b538bcc54b5a3f31", + "crates/edgezero-adapter-fastly/src/request.rs": "c49b75fd52abacc1b2fb012cc357b3a0ce11f9b6a76397bef0f40114ab79d684", + "crates/edgezero-adapter-fastly/src/response.rs": "5015be8eb6b0f55f2d7e79189a88233fc2142bb06d4820b586a2529fae9703ea", + "crates/edgezero-adapter-spin/src/outbound.rs": "be9f5ff721bcc47ed530deab39dbdff4058b1ec4e97865c2b8bbb65ae08c1ad5", + "crates/edgezero-adapter-spin/src/request.rs": "962e413a2009c82259405628b39976e4040dde6d1425ff5adfaf6ab0865b9d67", + "crates/edgezero-adapter-spin/src/response.rs": "03e8b98de3e4854810b4bdabba7783d3a66cacb5e91402e941609d7dfe567075", + "crates/edgezero-core/src/body.rs": "50a8bc65fcbc7df3d9afd3ddc520889f42da46af1c6419e23c19625c33d5bf9e", + "crates/edgezero-core/src/compression.rs": "4acccc0c01009894ebe4f77f57ce8ce501d3f3f2ccb565c6b3d3871d826037d4", + "crates/edgezero-core/src/lib.rs": "0de5ff9b503fbf86a27b0f0abce7dc47f9f85c7d0d596e0fce4b6589a96d133d", + "crates/edgezero-core/src/outbound.rs": "f1577756a5375dbd9a780b611d50bc56d629b8a50b8c13cc40864d0f1ef5ba8d", + "crates/edgezero-core/src/outbound/exchange.rs": "f8ca9867b572b2e40e0c08ae55fefefaefbf82355c83aa8b28f74870d6e2a879", + "crates/edgezero-core/src/outbound/progress.rs": "90eec32101d09c4ee40409b1fae6f06b97c83d1af8a03baf39d07820da9a0a08", + "crates/edgezero-core/src/time.rs": "bcd7047587f656b6547c860986f0c8f6459a5554850cbac4c1bfd9c480ec7435", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "tests/fixtures/reusable-app/Cargo.toml": "7665200757e4e565d11fa7eb036215aa583d21fd543bca39fdbdce470fee11ae", + "tests/fixtures/reusable-app/crates/fixture-axum/Cargo.toml": "1c1421c7497a7aff305d31ecd4c3ee323eb646acd4d56f23cab765ab6a5e377c", + "tests/fixtures/reusable-app/crates/fixture-axum/src/progress.rs": "0c41390031d857f0589667b88bd27fc8e185b483cedc57ec2ee73c99d36a9e4a", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/Cargo.toml": "96501fc975013ba68411cd6e0f77b6b0fe505dfe4ed8cef16620ef19b4c9784d", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs": "606245131f0c594154ca0a2eb63e5341999325bfb2de55887ab3264fa4b89559", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs": "305f70039af59501639916bd8cfb8db696613c73af670e953dd329a3eed14498", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml": "6c2a097879b98f4d4af3e2fea139215abc9fa0bfdc058500f28fd9f7d757aab3", + "tests/fixtures/reusable-app/crates/fixture-core/Cargo.toml": "f9c68c281c9b57269ee8065f46e9e1c3fcc9c58a562259e480f2125877a8f6c8", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs": "321cda3626dbe5e68da4ea1e9b5f71f531c0630f2763196fff8b0e7b64f301db", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs": "998660abfaeaa29ae9fc87b53fd055da2c63eebd2adaeb50698383475629b0e8", + "tests/fixtures/reusable-app/crates/fixture-fastly/Cargo.toml": "e82ea4713f86db533796367cee0c3f91191f33e7e1f0510d0cf1bdafd509a7e1", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs": "0b4a81e3678459801b9f5a9379de70160452317929936d1b6c85393c1249c704", + "tests/fixtures/reusable-app/crates/fixture-harness/Cargo.toml": "93580bf7575878ce3cb5d82ef3e7aea6d5c26baae6f467f469339858992fd141", + "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs": "22f5c8ff413357b5684910249c635a4993d0cb912b3cc584c8f14cc2258efb18", + "tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs": "1344e6fb8e4e22426ec5ceaacba210c034121970d8b49ece79e6ef3a871776b8", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs": "9cb580666fa2882bef9a87648469fa8170366526463163aabe9d959bda04d2af", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs": "f07fd19c3937780e5a7973513ccd29dd4708c852f62f4b2dd3e14929181605f9", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/progress.rs": "617062572517920d18fb7c8e938d690b38bc0845a036235f217659b63c3a4ab4", + "tests/fixtures/reusable-app/crates/fixture-spin/Cargo.toml": "7256c3384d5e3c675b8b9e7026d10630bc5fec9c3aaf3900bc740fcb4e1526b4", + "tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs": "0a20af88ab9a94fe8ef806b15e38994c7790ec0f9040add4c3dec4ead9f022db" + }, + "tools": { + "WASM_BINDGEN_BIN": { + "path": "/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen", + "bytes": 6938208, + "sha256": "b86e33b3c64362c44d8807ed35797e6b8220dd0232b6b19743caf7c2c68a2284", + "version": "wasm-bindgen 0.2.129" + }, + "WORKER_BUILD_BIN": { + "path": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build", + "bytes": 6149664, + "sha256": "dd7716c2d74637b5d8d2d3e4887252c7c3b8619f549cfb79e5715159bc86c2b0", + "version": "0.8.5" + }, + "ESBUILD_BIN": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild", + "bytes": 10573778, + "sha256": "e2dc9a52440a2a34f09434a2f4843cb1e30f84e40dcf238976ec61ef8cd7f36a", + "version": "0.28.1" + }, + "WASM_OPT_BIN": { + "path": "/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt", + "bytes": 1676584, + "sha256": "fa49ec32a92b72b368b66f8f19d84e9adffeef002449e6a07cd95a3ed2612b41", + "version": "wasm-opt version 130 (version_130)" + }, + "WRANGLER_BIN": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "bytes": 3088, + "sha256": "780661a508810f3b65786895b1ca9aacbc4f55d329ae6b8c1e49ec8433569f77", + "version": "4.131.0" + }, + "workerd": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64/bin/workerd", + "bytes": 156374824, + "sha256": "eb132e3e489b47d804cc70b510a6da8f82dabec4748cdcdaba32a1a1c5c2e285", + "version": "workerd 2026-09-10", + "package_version": "1.20260910.1", + "identification_basis": "Wrangler's bundled workerd package default executable; MINIFLARE_WORKERD_PATH unset; package resolution and executable version verified, not an independent per-process attestation" + } + }, + "sdks": { + "fastly": "0.13.1", + "worker": "0.8.5", + "spin_sdk": "7.0.0", + "reqwest": "0.13.4" + }, + "compatibility": { + "date": "2026-04-22", + "flags": ["enable_request_signal", "enable_weak_ref"], + "fixture_mode": "retained" + }, + "build_environment": { + "event": "build_environment", + "host_arch": "aarch64", + "host_os": "macos", + "lockfile": { + "bytes": 84127, + "fnv1a64": "10d93fc12c84db43", + "path": "/private/tmp/edgezero-relay-all-20261020-final/Cargo.lock", + "purpose": "non-security build identity" + }, + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)" + }, + "raw_events": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/events.jsonl", + "bytes": 585825, + "sha256": "5ef9c4b0aa976a57172740b5a9b4e7f0775e1f72c79b84c9962e43b348c10e17" + }, + "summary": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/summary.json", + "bytes": 304, + "sha256": "4df1030a8ab7098a6c336928ce12781ae269115a486bdd5da45900b8b6e30c8b", + "text": "{\n \"build_identity\": {\n \"status\": \"consistent\"\n },\n \"cpu_comparison\": \"SDK phase readings only; cross-run performance unverified\",\n \"guests\": [],\n \"memory_snapshots\": {},\n \"quantile_method\": \"nearest rank\",\n \"recorded_client_completions\": 422,\n \"recorded_request_starts\": 0,\n \"variants\": {}\n}" + }, + "retention": "all 96 relay observations and their 96 ordered origin receipts, 72 scheduling receipts, runtime logs and Cloudflare JS/config are embedded; full JSONL and binaries are identified by hash and remain local, not published release assets", + "limitations": [ + "local working-tree characterization, not frozen candidate or hosted qualification", + "the constructed GET relay does not prove forwarding actual inbound GET/HEAD bodies", + "head-first/eager/follow hard migration remains open; no production API or adapter source changed", + "no exact deadline/cancellation, scheduler-context rejection, HTTP/2 or HTTP/3 qualification", + "only surviving adapter-visible representation fields; no raw-header-fidelity promotion", + "host may select chunked or close-delimited framing; any observed Content-Length must match captured payload bytes, not a universal wire-header guarantee", + "Decode/200 metadata SHOULD policies remain unadopted; controls check decoded bytes and coding/length, not rewritten validators", + "coding/marker individual receipts are not retained, only successful receipt-count guards", + "reader failures are fatal and group-labelled but may occur before per-case relay records", + "Fastly G2 remains Failed; no B/H(K), new timed reads, slack removal or provider-policy waiver", + "workerd identity uses verified bundled package-default resolution, not independent per-process attestation", + "no capabilities promoted" + ], + "runs": [ + { + "adapter": "axum", + "counts": { + "outbound-coding": 24, + "outbound-marker": 46, + "outbound-relay": 24, + "outbound-progress": 5, + "inbound-empty-signal": 5, + "progress-group": 4, + "client_completed": 104 + }, + "runtime": { + "adapter": "axum", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo", + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "source_state": "working-tree-characterization", + "version": "cargo 1.98.1 (797e8a9bc 2026-08-05)", + "identity": { + "path": "/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo", + "bytes": 31960040, + "sha256": "6e17e865f3a20dd55a1d212f849f58b77124179f0de7c52973096d84ba34118d" + } + }, + "artifact": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/progress-axum/artifacts/fixture-axum-outbound-progress", + "bytes": 8907728, + "sha256": "afe9d2cbcb6d6cf94fe36a23557bc2b055c86d30e032185d6cbeee728ad4103d" + }, + "runtime_log": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/progress-axum/runtime.log", + "bytes": 604, + "sha256": "c7eb6e6746e10e5e6d0df62371e4454e86898c066d13a6f19ebfa68dae72b6a3", + "text": "2026-10-20T22:59:15.450Z INFO [edgezero_adapter_axum::dev_server] [edgezero] axum HTTP/1 ingress limits: AxumIngressConfig { head_read_timeout: 10s, max_connections: 256, max_header_count: 100, max_raw_head_bytes: 65536 }\n2026-10-20T22:59:15.450Z INFO [edgezero_adapter_axum::dev_server] [edgezero] axum config snapshot limits: ConfigStoreLimits { entries: 1024, file_bytes: 16777216, key_bytes: 1024, resident_bytes: 33554432, startup_bytes: 117440512, value_bytes: 8388608 }\n2026-10-20T22:59:15.450Z INFO [edgezero_adapter_axum::dev_server] [edgezero] starting axum server on http://127.0.0.1:50143\n" + }, + "relay": [ + { + "adapter": "axum", + "case": "relay-gzip-buffered-200-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 14232375, + "event": "client_completed", + "first_byte_ns": 14114250, + "header_ns": 14114125, + "headers": [ + ["content-encoding", "gzip"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0be6b3f610-b166-184" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-gzip-buffered-206-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 14302917, + "event": "client_completed", + "first_byte_ns": 14298167, + "header_ns": 14298042, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0be7964790-b166-185" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-gzip-buffered-206-decode", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 10506625, + "event": "client_completed", + "first_byte_ns": 10502417, + "header_ns": 10502292, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0be87a17e0-b166-186" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-gzip-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 13709000, + "event": "client_completed", + "first_byte_ns": 13703333, + "header_ns": 13703208, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["content-length", "17"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0be923fc10-b166-187" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-gzip-streamed-200-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 10648666, + "event": "client_completed", + "first_byte_ns": 10644666, + "header_ns": 10644541, + "headers": [ + ["content-encoding", "gzip"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0be9fccbf8-b166-188" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-gzip-streamed-206-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 12439792, + "event": "client_completed", + "first_byte_ns": 12435042, + "header_ns": 12434958, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0beaa6db20-b166-189" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-gzip-streamed-206-decode", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 14287708, + "event": "client_completed", + "first_byte_ns": 14283333, + "header_ns": 14283250, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0beb6ee610-b166-18a" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-gzip-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 10039291, + "event": "client_completed", + "first_byte_ns": 10034083, + "header_ns": 10033041, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0bec5258a0-b166-18b" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-deflate-buffered-200-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 12360333, + "event": "client_completed", + "first_byte_ns": 12356291, + "header_ns": 12356250, + "headers": [ + ["content-encoding", "deflate"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0becf2d2a8-b166-18c" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-deflate-buffered-206-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 11626875, + "event": "client_completed", + "first_byte_ns": 11619792, + "header_ns": 11619625, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0bedb6edc8-b166-18d" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-deflate-buffered-206-decode", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 11320458, + "event": "client_completed", + "first_byte_ns": 11316208, + "header_ns": 11316083, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0bee724e88-b166-18e" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-deflate-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 11405125, + "event": "client_completed", + "first_byte_ns": 11400125, + "header_ns": 11400000, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["content-length", "17"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0bef2ed828-b166-18f" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-deflate-streamed-200-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 14519375, + "event": "client_completed", + "first_byte_ns": 14514833, + "header_ns": 14510250, + "headers": [ + ["content-encoding", "deflate"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0befe41a80-b166-190" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-deflate-streamed-206-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 14428208, + "event": "client_completed", + "first_byte_ns": 14421125, + "header_ns": 14420875, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0bf0cb4630-b166-191" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-deflate-streamed-206-decode", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 14526500, + "event": "client_completed", + "first_byte_ns": 14518125, + "header_ns": 14517708, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0bf1b04730-b166-192" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-deflate-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 14389625, + "event": "client_completed", + "first_byte_ns": 14383750, + "header_ns": 14372375, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0bf2961f08-b166-193" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-br-buffered-200-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 9980750, + "event": "client_completed", + "first_byte_ns": 9975875, + "header_ns": 9975625, + "headers": [ + ["content-encoding", "br"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0bf3798db0-b166-194" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-br-buffered-206-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 10662333, + "event": "client_completed", + "first_byte_ns": 10657833, + "header_ns": 10657708, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0bf419ffe8-b166-195" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-br-buffered-206-decode", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 10108500, + "event": "client_completed", + "first_byte_ns": 10104291, + "header_ns": 10104166, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0bf4c50910-b166-196" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-br-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 13406625, + "event": "client_completed", + "first_byte_ns": 13402000, + "header_ns": 13401875, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["content-length", "17"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0bf5677ee8-b166-197" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-br-streamed-200-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 11816084, + "event": "client_completed", + "first_byte_ns": 11812125, + "header_ns": 11811917, + "headers": [ + ["content-encoding", "br"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0bf63b6cd0-b166-198" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-br-streamed-206-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 14482209, + "event": "client_completed", + "first_byte_ns": 14478167, + "header_ns": 14478084, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0bf6f7cb78-b166-199" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-br-streamed-206-decode", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 14451500, + "event": "client_completed", + "first_byte_ns": 14447333, + "header_ns": 14447166, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 206, + "token": "18e05f0bf7dd1e80-b166-19a" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "case": "relay-br-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 10927500, + "event": "client_completed", + "first_byte_ns": 10922917, + "header_ns": 10921750, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:16 GMT"] + ], + "status": 200, + "token": "18e05f0bf8c1ae38-b166-19b" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + } + ], + "relay_coverage": { + "adapter": "axum", + "attempted": 24, + "event": "outbound-relay-coverage", + "qualification": false, + "receipts": [ + "/coding/gzip/200", + "/coding/gzip/206", + "/coding/gzip/206", + "/coding/gzip/200", + "/coding/gzip/200", + "/coding/gzip/206", + "/coding/gzip/206", + "/coding/gzip/200", + "/coding/deflate/200", + "/coding/deflate/206", + "/coding/deflate/206", + "/coding/deflate/200", + "/coding/deflate/200", + "/coding/deflate/206", + "/coding/deflate/206", + "/coding/deflate/200", + "/coding/br/200", + "/coding/br/206", + "/coding/br/206", + "/coding/br/200", + "/coding/br/200", + "/coding/br/206", + "/coding/br/206", + "/coding/br/200" + ], + "status": "pass" + }, + "scheduling": [ + { + "case": "eager-single", + "status": "pass", + "probe": { + "adapter": "axum", + "case": "eager-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 103084958, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 1, + "token": "18e05f0bfa281438-b166-19c" + }, + "receipts": [ + { + "path": "/g3/18e05f0bfa281438-b166-19c/start", + "received_ns": 14964000 + }, + { + "path": "/g3/18e05f0bfa281438-b166-19c/slot-0", + "received_ns": 26058750 + }, + { + "path": "/g3/18e05f0bfa281438-b166-19c/end", + "received_ns": 124291584 + } + ] + }, + { + "case": "lazy-single", + "status": "pass", + "probe": { + "adapter": "axum", + "case": "lazy-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 103039958, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 1, + "token": "18e05f0c0e7c4990-b166-19d" + }, + "receipts": [ + { + "path": "/g3/18e05f0c0e7c4990-b166-19d/start", + "received_ns": 10981417 + }, + { + "path": "/g3/18e05f0c0e7c4990-b166-19d/end", + "received_ns": 125843833 + }, + { + "path": "/g3/18e05f0c0e7c4990-b166-19d/slot-0", + "received_ns": 137773375 + } + ] + }, + { + "case": "eager-batch", + "status": "pass", + "probe": { + "adapter": "axum", + "case": "eager-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 103137625, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0, 0], + "slots": 2, + "token": "18e05f0c29059f50-b166-19e" + }, + "receipts": [ + { + "path": "/g3/18e05f0c29059f50-b166-19e/start", + "received_ns": 14158792 + }, + { + "path": "/g3/18e05f0c29059f50-b166-19e/slot-0", + "received_ns": 26638958 + }, + { + "path": "/g3/18e05f0c29059f50-b166-19e/slot-1", + "received_ns": 26643917 + }, + { + "path": "/g3/18e05f0c29059f50-b166-19e/end", + "received_ns": 123010292 + } + ] + }, + { + "case": "baseline-batch", + "status": "pass", + "probe": { + "adapter": "axum", + "case": "baseline-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 103041083, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 2, + "token": "18e05f0c3d519f18-b166-19f" + }, + "receipts": [ + { + "path": "/g3/18e05f0c3d519f18-b166-19f/start", + "received_ns": 13400041 + }, + { + "path": "/g3/18e05f0c3d519f18-b166-19f/end", + "received_ns": 129621666 + }, + { + "path": "/g3/18e05f0c3d519f18-b166-19f/slot-0", + "received_ns": 141501375 + }, + { + "path": "/g3/18e05f0c3d519f18-b166-19f/slot-1", + "received_ns": 141507250 + } + ] + }, + { + "case": "response-owned", + "status": "pass", + "probe": { + "adapter": "axum", + "case": "response-owned", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": true, + "pause_ns": 103128084, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0, 0], + "slots": 2, + "token": "18e05f0c58ed89d0-b166-1a0" + }, + "receipts": [ + { + "path": "/g3/18e05f0c58ed89d0-b166-1a0/start", + "received_ns": 11125083 + }, + { + "path": "/g3/18e05f0c58ed89d0-b166-1a0/slot-0", + "received_ns": 25816333 + }, + { + "path": "/g3/18e05f0c58ed89d0-b166-1a0/slot-1", + "received_ns": 25822416 + }, + { + "path": "/g3/18e05f0c58ed89d0-b166-1a0/end", + "received_ns": 122614125 + } + ] + } + ], + "group_results": [ + { + "adapter": "axum", + "error": null, + "event": "progress-group", + "group": "coding", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "error": null, + "event": "progress-group", + "group": "marker", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "error": null, + "event": "progress-group", + "group": "relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "error": null, + "event": "progress-group", + "group": "progress-and-framing", + "qualification": false, + "status": "pass" + } + ], + "framing": [ + { + "framing": "Absent", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "LengthZero", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "ChunkedEmpty", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "FixedBody", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "ChunkedBody", + "probe": { + "adapter": "axum", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "known_empty": null, + "source": "not-exposed-by-native-entrypoint" + }, + "method": "GET", + "qualification": false + } + } + ], + "receipts": { + "relay": 24, + "coding": 24, + "marker": 47, + "scheduling": 18, + "total_base": 113, + "retained_relay_and_scheduling": 42, + "note": "coding and marker receipts are asserted by successful runner guards; Spin additionally has two SDK header-control receipts" + } + }, + { + "adapter": "cloudflare", + "counts": { + "outbound-coding": 24, + "outbound-marker": 46, + "outbound-relay": 24, + "outbound-progress": 5, + "inbound-empty-signal": 5, + "progress-group": 4, + "client_completed": 104 + }, + "runtime": { + "adapter": "cloudflare", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "source_state": "working-tree-characterization", + "version": "4.131.0", + "identity": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "bytes": 3088, + "sha256": "780661a508810f3b65786895b1ca9aacbc4f55d329ae6b8c1e49ec8433569f77" + } + }, + "artifact": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/progress-cloudflare/artifacts/build/index_bg.wasm", + "bytes": 1691827, + "sha256": "956fa88fab457103f71ccbea48bb5014b4ef2700188328aca083c983c31b41b3" + }, + "runtime_log": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/progress-cloudflare/runtime.log", + "bytes": 10087, + "sha256": "af70c267f4bb128b90c48ddf4818a48f60f465a4ea5114a44a7c68ac37d18bcb", + "text": "\n \u26c5\ufe0f wrangler 4.131.0 (update available 4.151.3)\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nYour Worker has access to the following bindings:\nBinding Resource Mode\nenv.fixture_config (00000000000000000000000000000001) KV Namespace local\nenv.fixture_kv (00000000000000000000000000000002) KV Namespace local\nenv.FIXTURE_MODE (\"(hidden)\") Environment Variable local\nenv.fixture_marker (\"fixture-only-value\") Environment Variable local\n\n\u2394 Starting local server...\nWrangler detected this dev session is running in an AI agent.\nThe Local Explorer API is available at http://127.0.0.1:49579/cdn-cgi/local/explorer/api\nUseful routes:\n GET http://127.0.0.1:49579/cdn-cgi/local/explorer/api/local/workers - local Workers and bindings\n GET http://127.0.0.1:49579/cdn-cgi/local/explorer/api/storage/kv/namespaces - KV namespaces\n GET http://127.0.0.1:49579/cdn-cgi/local/explorer/api/d1/database - D1 databases\n GET http://127.0.0.1:49579/cdn-cgi/local/explorer/api/r2/buckets - R2 buckets\n GET http://127.0.0.1:49579/cdn-cgi/local/explorer/api/workers/durable_objects/namespaces - Durable Object namespaces\n GET http://127.0.0.1:49579/cdn-cgi/local/explorer/api/workflows - Workflows\n POST http://127.0.0.1:49579/cdn-cgi/local/explorer/api/local/observability/query - run a read-only SQL query (SELECT/WITH only) over captured request traces and console logs. Tables: spans, logs (read attributes via json(attributes)). Example:\n curl -X POST http://127.0.0.1:49579/cdn-cgi/local/explorer/api/local/observability/query -H 'Content-Type: application/json' -d '{\"sql\":\"SELECT service, name, outcome, duration_ms FROM spans WHERE parent_id IS NULL LIMIT 20\"}'\nIf the routes above don't cover what you need, fetch the full OpenAPI schema (large - use only as a last resort):\n GET http://127.0.0.1:49579/cdn-cgi/local/explorer/api - OpenAPI schema\n\u001b[33m\u25b2 \u001b[43;33m[\u001b[43;30mWARNING\u001b[43;33m]\u001b[0m \u001b[1mservice core:user:reusable-app-fixture: The compatibility flag enable_weak_ref became the default as of 2025-05-05 so does not need to be specified anymore.\u001b[0m\n\n\n[wrangler:info] Ready on http://127.0.0.1:49579\n[wrangler:info] GET /progress/coding-gzip-buffered-200-original 200 OK (181ms)\n[wrangler:info] GET /progress/coding-gzip-buffered-206-original 200 OK (7ms)\n[wrangler:info] GET /progress/coding-gzip-buffered-206-decode 200 OK (14ms)\n[wrangler:info] GET /progress/coding-gzip-buffered-200-decode 200 OK (11ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-200-original 200 OK (10ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-206-original 200 OK (12ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-206-decode 200 OK (13ms)\n[wrangler:info] GET /progress/coding-gzip-streamed-200-decode 200 OK (15ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-200-original 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-206-original 200 OK (10ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-206-decode 200 OK (11ms)\n[wrangler:info] GET /progress/coding-deflate-buffered-200-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-200-original 200 OK (14ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-206-original 200 OK (9ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-206-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-deflate-streamed-200-decode 200 OK (11ms)\n[wrangler:info] GET /progress/coding-br-buffered-200-original 200 OK (14ms)\n[wrangler:info] GET /progress/coding-br-buffered-206-original 200 OK (13ms)\n[wrangler:info] GET /progress/coding-br-buffered-206-decode 200 OK (12ms)\n[wrangler:info] GET /progress/coding-br-buffered-200-decode 200 OK (15ms)\n[wrangler:info] GET /progress/coding-br-streamed-200-original 200 OK (13ms)\n[wrangler:info] GET /progress/coding-br-streamed-206-original 200 OK (14ms)\n[wrangler:info] GET /progress/coding-br-streamed-206-decode 200 OK (13ms)\n[wrangler:info] GET /progress/coding-br-streamed-200-decode 200 OK (15ms)\n[wrangler:info] GET /progress/marker-get-200-absent-inherit 200 OK (16ms)\n[wrangler:info] GET /progress/marker-get-200-absent-on 200 OK (13ms)\n[wrangler:info] GET /progress/marker-get-200-absent-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-200-upstream-inherit 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-200-upstream-on 200 OK (13ms)\n[wrangler:info] GET /progress/marker-get-200-upstream-off 200 OK (10ms)\n[wrangler:info] GET /progress/marker-get-200-nominated-inherit 200 OK (15ms)\n[wrangler:info] GET /progress/marker-get-200-nominated-on 200 OK (9ms)\n[wrangler:info] GET /progress/marker-get-200-nominated-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-204-absent-inherit 200 OK (14ms)\n[wrangler:info] GET /progress/marker-get-204-absent-on 200 OK (14ms)\n[wrangler:info] GET /progress/marker-get-204-absent-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-204-upstream-inherit 200 OK (51ms)\n[wrangler:info] GET /progress/marker-get-204-upstream-on 200 OK (6ms)\n[wrangler:info] GET /progress/marker-get-204-upstream-off 200 OK (15ms)\n[wrangler:info] GET /progress/marker-get-204-nominated-inherit 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-204-nominated-on 200 OK (15ms)\n[wrangler:info] GET /progress/marker-get-204-nominated-off 200 OK (14ms)\n[wrangler:info] GET /progress/marker-head-200-absent-inherit 200 OK (13ms)\n[wrangler:info] GET /progress/marker-head-200-absent-on 200 OK (10ms)\n[wrangler:info] GET /progress/marker-head-200-absent-off 200 OK (14ms)\n[wrangler:info] GET /progress/marker-head-200-upstream-inherit 200 OK (14ms)\n[wrangler:info] GET /progress/marker-head-200-upstream-on 200 OK (14ms)\n[wrangler:info] GET /progress/marker-head-200-upstream-off 200 OK (14ms)\n[wrangler:info] GET /progress/marker-head-200-nominated-inherit 200 OK (15ms)\n[wrangler:info] GET /progress/marker-head-200-nominated-on 200 OK (10ms)\n[wrangler:info] GET /progress/marker-head-200-nominated-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-304-absent-inherit 200 OK (13ms)\n[wrangler:info] GET /progress/marker-get-304-absent-on 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-304-absent-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-304-upstream-inherit 200 OK (13ms)\n[wrangler:info] GET /progress/marker-get-304-upstream-on 200 OK (14ms)\n[wrangler:info] GET /progress/marker-get-304-upstream-off 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-304-nominated-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-304-nominated-on 200 OK (14ms)\n[wrangler:info] GET /progress/marker-get-304-nominated-off 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-302-absent-inherit 200 OK (14ms)\n[wrangler:info] GET /progress/marker-get-302-absent-on 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-302-absent-off 200 OK (14ms)\n[wrangler:info] GET /progress/marker-get-302-upstream-inherit 200 OK (15ms)\n[wrangler:info] GET /progress/marker-get-302-upstream-on 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-302-upstream-off 200 OK (14ms)\n[wrangler:info] GET /progress/marker-get-302-nominated-inherit 200 OK (11ms)\n[wrangler:info] GET /progress/marker-get-302-nominated-on 200 OK (12ms)\n[wrangler:info] GET /progress/marker-get-302-nominated-off 200 OK (14ms)\n[wrangler:info] GET /progress/marker-batch 200 OK (10ms)\n[wrangler:info] GET /progress/relay-gzip-buffered-200-original 200 OK (16ms)\n[wrangler:info] GET /progress/relay-gzip-buffered-206-original 206 Partial Content (13ms)\n[wrangler:info] GET /progress/relay-gzip-buffered-206-decode 206 Partial Content (10ms)\n[wrangler:info] GET /progress/relay-gzip-buffered-200-decode 200 OK (14ms)\n[wrangler:info] GET /progress/relay-gzip-streamed-200-original 200 OK (14ms)\n[wrangler:info] GET /progress/relay-gzip-streamed-206-original 206 Partial Content (12ms)\n[wrangler:info] GET /progress/relay-gzip-streamed-206-decode 206 Partial Content (12ms)\n[wrangler:info] GET /progress/relay-gzip-streamed-200-decode 200 OK (15ms)\n[wrangler:info] GET /progress/relay-deflate-buffered-200-original 200 OK (14ms)\n[wrangler:info] GET /progress/relay-deflate-buffered-206-original 206 Partial Content (14ms)\n[wrangler:info] GET /progress/relay-deflate-buffered-206-decode 206 Partial Content (15ms)\n[wrangler:info] GET /progress/relay-deflate-buffered-200-decode 200 OK (15ms)\n[wrangler:info] GET /progress/relay-deflate-streamed-200-original 200 OK (11ms)\n[wrangler:info] GET /progress/relay-deflate-streamed-206-original 206 Partial Content (14ms)\n[wrangler:info] GET /progress/relay-deflate-streamed-206-decode 206 Partial Content (13ms)\n[wrangler:info] GET /progress/relay-deflate-streamed-200-decode 200 OK (14ms)\n[wrangler:info] GET /progress/relay-br-buffered-200-original 200 OK (14ms)\n[wrangler:info] GET /progress/relay-br-buffered-206-original 206 Partial Content (14ms)\n[wrangler:info] GET /progress/relay-br-buffered-206-decode 206 Partial Content (11ms)\n[wrangler:info] GET /progress/relay-br-buffered-200-decode 200 OK (10ms)\n[wrangler:info] GET /progress/relay-br-streamed-200-original 200 OK (14ms)\n[wrangler:info] GET /progress/relay-br-streamed-206-original 206 Partial Content (14ms)\n[wrangler:info] GET /progress/relay-br-streamed-206-decode 206 Partial Content (11ms)\n[wrangler:info] GET /progress/relay-br-streamed-200-decode 200 OK (10ms)\n[wrangler:info] GET /progress/eager-single 200 OK (330ms)\n[wrangler:info] GET /progress/lazy-single 200 OK (440ms)\n[wrangler:info] GET /progress/eager-batch 200 OK (339ms)\n[wrangler:info] GET /progress/baseline-batch 200 OK (442ms)\n[wrangler:info] GET /progress/response-owned 200 OK (13ms)\n[wrangler:info] GET /progress/empty 200 OK (2ms)\n[wrangler:info] GET /progress/empty 200 OK (1ms)\n[wrangler:info] GET /progress/empty 200 OK (1ms)\n[wrangler:info] GET /progress/empty 200 OK (1ms)\n[wrangler:info] GET /progress/empty 200 OK (1ms)\n" + }, + "relay": [ + { + "adapter": "cloudflare", + "case": "relay-gzip-buffered-200-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 16732334, + "event": "client_completed", + "first_byte_ns": 16716000, + "header_ns": 16713542, + "headers": [ + ["Content-Length", "37"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "gzip"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ] + ], + "status": 200, + "token": "18e05f08c755f5f0-b166-ae" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-gzip-buffered-206-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 13642916, + "event": "client_completed", + "first_byte_ns": 13634875, + "header_ns": 13632208, + "headers": [ + ["Content-Length", "37"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-36/37"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "gzip"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ] + ], + "status": 206, + "token": "18e05f08c85e26c0-b166-af" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-gzip-buffered-206-decode", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 9490625, + "event": "client_completed", + "first_byte_ns": 9482708, + "header_ns": 9480375, + "headers": [ + ["Content-Length", "37"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-36/37"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "gzip"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ] + ], + "status": 206, + "token": "18e05f08c937c5b0-b166-b0" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-gzip-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 13975792, + "event": "client_completed", + "first_byte_ns": 13870750, + "header_ns": 13867292, + "headers": [ + ["Transfer-Encoding", "chunked"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ] + ], + "status": 200, + "token": "18e05f08c9d19c80-b166-b1" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-gzip-streamed-200-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 13978709, + "event": "client_completed", + "first_byte_ns": 13969917, + "header_ns": 13862959, + "headers": [ + ["Content-Length", "37"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "gzip"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ] + ], + "status": 200, + "token": "18e05f08cab16590-b166-b2" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-gzip-streamed-206-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 12650417, + "event": "client_completed", + "first_byte_ns": 12642459, + "header_ns": 12639834, + "headers": [ + ["Content-Length", "37"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-36/37"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "gzip"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ] + ], + "status": 206, + "token": "18e05f08cb8fea68-b166-b3" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-gzip-streamed-206-decode", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 11675666, + "event": "client_completed", + "first_byte_ns": 11667375, + "header_ns": 11665041, + "headers": [ + ["Content-Length", "37"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-36/37"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "gzip"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ] + ], + "status": 206, + "token": "18e05f08cc5d2578-b166-b4" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-gzip-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 15330959, + "event": "client_completed", + "first_byte_ns": 14710000, + "header_ns": 14707709, + "headers": [ + ["Transfer-Encoding", "chunked"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ] + ], + "status": 200, + "token": "18e05f08cd187e68-b166-b5" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-deflate-buffered-200-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 13573875, + "event": "client_completed", + "first_byte_ns": 13566083, + "header_ns": 13565625, + "headers": [ + ["Content-Length", "25"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "deflate"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ] + ], + "status": 200, + "token": "18e05f08ce0a25b0-b166-b6" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-deflate-buffered-206-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 14296875, + "event": "client_completed", + "first_byte_ns": 14289750, + "header_ns": 14289541, + "headers": [ + ["Content-Length", "25"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-24/25"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "deflate"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ] + ], + "status": 206, + "token": "18e05f08cee1c4e8-b166-b7" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-deflate-buffered-206-decode", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 14586708, + "event": "client_completed", + "first_byte_ns": 14578875, + "header_ns": 14575708, + "headers": [ + ["Content-Length", "25"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-24/25"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "deflate"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ] + ], + "status": 206, + "token": "18e05f08cfc3fb10-b166-b8" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-deflate-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 14646333, + "event": "client_completed", + "first_byte_ns": 14633750, + "header_ns": 14631458, + "headers": [ + ["Transfer-Encoding", "chunked"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ] + ], + "status": 200, + "token": "18e05f08d0aae070-b166-b9" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-deflate-streamed-200-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 11684375, + "event": "client_completed", + "first_byte_ns": 11674959, + "header_ns": 11531959, + "headers": [ + ["Content-Length", "25"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "deflate"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ] + ], + "status": 200, + "token": "18e05f08d1966950-b166-ba" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-deflate-streamed-206-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 13982958, + "event": "client_completed", + "first_byte_ns": 13976333, + "header_ns": 13974208, + "headers": [ + ["Content-Length", "25"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-24/25"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "deflate"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ] + ], + "status": 206, + "token": "18e05f08d2536c08-b166-bb" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-deflate-streamed-206-decode", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 13678875, + "event": "client_completed", + "first_byte_ns": 13671875, + "header_ns": 13669083, + "headers": [ + ["Content-Length", "25"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-24/25"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "deflate"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ] + ], + "status": 206, + "token": "18e05f08d332b048-b166-bc" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-deflate-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 14539375, + "event": "client_completed", + "first_byte_ns": 14349791, + "header_ns": 14346583, + "headers": [ + ["Transfer-Encoding", "chunked"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ] + ], + "status": 200, + "token": "18e05f08d40c3f98-b166-bd" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-br-buffered-200-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 14543208, + "event": "client_completed", + "first_byte_ns": 14504958, + "header_ns": 14502250, + "headers": [ + ["Content-Length", "21"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "br"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ] + ], + "status": 200, + "token": "18e05f08d4f1c568-b166-be" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-br-buffered-206-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 14535166, + "event": "client_completed", + "first_byte_ns": 14527250, + "header_ns": 14524625, + "headers": [ + ["Content-Length", "21"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-20/21"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "br"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ] + ], + "status": 206, + "token": "18e05f08d5d6f548-b166-bf" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-br-buffered-206-decode", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 10962375, + "event": "client_completed", + "first_byte_ns": 10953791, + "header_ns": 10953375, + "headers": [ + ["Content-Length", "21"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-20/21"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "br"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ] + ], + "status": 206, + "token": "18e05f08d6bce878-b166-c0" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-br-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 10253250, + "event": "client_completed", + "first_byte_ns": 10240625, + "header_ns": 10238125, + "headers": [ + ["Transfer-Encoding", "chunked"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ] + ], + "status": 200, + "token": "18e05f08d77088d8-b166-c1" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-br-streamed-200-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 14216458, + "event": "client_completed", + "first_byte_ns": 14210500, + "header_ns": 14208750, + "headers": [ + ["Content-Length", "21"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "br"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ] + ], + "status": 200, + "token": "18e05f08d81642a0-b166-c2" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-br-streamed-206-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 14582000, + "event": "client_completed", + "first_byte_ns": 14572833, + "header_ns": 14569625, + "headers": [ + ["Content-Length", "21"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-20/21"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "br"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ] + ], + "status": 206, + "token": "18e05f08d8f78698-b166-c3" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-br-streamed-206-decode", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 11686417, + "event": "client_completed", + "first_byte_ns": 11676000, + "header_ns": 11675542, + "headers": [ + ["Content-Length", "21"], + ["Content-Type", "application/octet-stream"], + ["Content-Range", "bytes 0-20/21"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["Content-Encoding", "br"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ] + ], + "status": 206, + "token": "18e05f08d9dfb418-b166-c4" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "case": "relay-br-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 9822375, + "event": "client_completed", + "first_byte_ns": 9605125, + "header_ns": 9603292, + "headers": [ + ["Transfer-Encoding", "chunked"], + ["Content-Type", "application/octet-stream"], + ["Accept-Ranges", "bytes"], + ["Cache-Control", "max-age=60"], + ["ETag", "\"coding-probe\""], + ["Last-Modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["Vary", "Accept-Encoding"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ] + ], + "status": 200, + "token": "18e05f08daa077c0-b166-c5" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + } + ], + "relay_coverage": { + "adapter": "cloudflare", + "attempted": 24, + "event": "outbound-relay-coverage", + "qualification": false, + "receipts": [ + "/coding/gzip/200", + "/coding/gzip/206", + "/coding/gzip/206", + "/coding/gzip/200", + "/coding/gzip/200", + "/coding/gzip/206", + "/coding/gzip/206", + "/coding/gzip/200", + "/coding/deflate/200", + "/coding/deflate/206", + "/coding/deflate/206", + "/coding/deflate/200", + "/coding/deflate/200", + "/coding/deflate/206", + "/coding/deflate/206", + "/coding/deflate/200", + "/coding/br/200", + "/coding/br/206", + "/coding/br/206", + "/coding/br/200", + "/coding/br/200", + "/coding/br/206", + "/coding/br/206", + "/coding/br/200" + ], + "status": "pass" + }, + "scheduling": [ + { + "case": "eager-single", + "status": "pass", + "probe": { + "adapter": "cloudflare", + "case": "eager-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 100000000, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 1, + "token": "18e05f08dbe9dfe0-b166-c6" + }, + "receipts": [ + { + "path": "/g3/18e05f08dbe9dfe0-b166-c6/start", + "received_ns": 13814166 + }, + { + "path": "/g3/18e05f08dbe9dfe0-b166-c6/slot-0", + "received_ns": 25082958 + }, + { + "path": "/g3/18e05f08dbe9dfe0-b166-c6/end", + "received_ns": 122541833 + } + ] + }, + { + "case": "lazy-single", + "status": "pass", + "probe": { + "adapter": "cloudflare", + "case": "lazy-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 102000000, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 1, + "token": "18e05f08efdfd450-b166-c7" + }, + "receipts": [ + { + "path": "/g3/18e05f08efdfd450-b166-c7/start", + "received_ns": 15003542 + }, + { + "path": "/g3/18e05f08efdfd450-b166-c7/end", + "received_ns": 117962209 + }, + { + "path": "/g3/18e05f08efdfd450-b166-c7/slot-0", + "received_ns": 132239417 + } + ] + }, + { + "case": "eager-batch", + "status": "pass", + "probe": { + "adapter": "cloudflare", + "case": "eager-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 102000000, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0, 0], + "slots": 2, + "token": "18e05f090a854948-b166-c8" + }, + "receipts": [ + { + "path": "/g3/18e05f090a854948-b166-c8/start", + "received_ns": 13156917 + }, + { + "path": "/g3/18e05f090a854948-b166-c8/slot-0", + "received_ns": 27308959 + }, + { + "path": "/g3/18e05f090a854948-b166-c8/slot-1", + "received_ns": 27312209 + }, + { + "path": "/g3/18e05f090a854948-b166-c8/end", + "received_ns": 123119917 + } + ] + }, + { + "case": "baseline-batch", + "status": "pass", + "probe": { + "adapter": "cloudflare", + "case": "baseline-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 102000000, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 2, + "token": "18e05f091f1e6358-b166-c9" + }, + "receipts": [ + { + "path": "/g3/18e05f091f1e6358-b166-c9/start", + "received_ns": 13135500 + }, + { + "path": "/g3/18e05f091f1e6358-b166-c9/end", + "received_ns": 123241000 + }, + { + "path": "/g3/18e05f091f1e6358-b166-c9/slot-0", + "received_ns": 136303667 + }, + { + "path": "/g3/18e05f091f1e6358-b166-c9/slot-1", + "received_ns": 136315167 + } + ] + }, + { + "case": "response-owned", + "status": "pass", + "probe": { + "adapter": "cloudflare", + "case": "response-owned", + "consumer_polls_before_window_end": 0, + "dispatch_returned": true, + "handler_returned": true, + "pause_ns": 100000000, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0, 0], + "slots": 2, + "token": "18e05f0939dd24e0-b166-ca" + }, + "receipts": [ + { + "path": "/g3/18e05f0939dd24e0-b166-ca/start", + "received_ns": 12307292 + }, + { + "path": "/g3/18e05f0939dd24e0-b166-ca/slot-0", + "received_ns": 25445917 + }, + { + "path": "/g3/18e05f0939dd24e0-b166-ca/slot-1", + "received_ns": 25450875 + }, + { + "path": "/g3/18e05f0939dd24e0-b166-ca/end", + "received_ns": 123559042 + } + ] + } + ], + "group_results": [ + { + "adapter": "cloudflare", + "error": null, + "event": "progress-group", + "group": "coding", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "error": null, + "event": "progress-group", + "group": "marker", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "error": null, + "event": "progress-group", + "group": "relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "error": null, + "event": "progress-group", + "group": "progress-and-framing", + "qualification": false, + "status": "pass" + } + ], + "framing": [ + { + "framing": "Absent", + "probe": { + "adapter": "cloudflare", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "body_absent": true, + "source": "workerd-before-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "LengthZero", + "probe": { + "adapter": "cloudflare", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "body_absent": true, + "source": "workerd-before-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "ChunkedEmpty", + "probe": { + "adapter": "cloudflare", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "body_absent": false, + "source": "workerd-before-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "FixedBody", + "probe": { + "adapter": "cloudflare", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "body_absent": false, + "source": "workerd-before-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "ChunkedBody", + "probe": { + "adapter": "cloudflare", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "body_absent": false, + "source": "workerd-before-conversion" + }, + "method": "GET", + "qualification": false + } + } + ], + "receipts": { + "relay": 24, + "coding": 24, + "marker": 47, + "scheduling": 18, + "total_base": 113, + "retained_relay_and_scheduling": 42, + "note": "coding and marker receipts are asserted by successful runner guards; Spin additionally has two SDK header-control receipts" + }, + "effective_config": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/progress-cloudflare/wrangler.toml", + "bytes": 477, + "sha256": "b00895f8b722519edd19d5bfcd913c4f9c0d8daef4854edf9de795b16c57e6e4", + "text": "name = \"reusable-app-fixture\"\nmain = \"/private/tmp/edgezero-relay-all-20261020-final/progress-cloudflare/artifacts/build/worker/shim.mjs\"\ncompatibility_date = \"2026-04-22\"\ncompatibility_flags = [\"enable_request_signal\", \"enable_weak_ref\"]\n[vars]\nFIXTURE_MODE = \"retained\"\nfixture_marker = \"fixture-only-value\"\n\n[[kv_namespaces]]\nbinding = \"fixture_config\"\nid = \"00000000000000000000000000000001\"\n[[kv_namespaces]]\nbinding = \"fixture_kv\"\nid = \"00000000000000000000000000000002\"\n" + }, + "js": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/progress-cloudflare/artifacts/build/index.js", + "bytes": 28684, + "sha256": "e3fbc6a80de31ae583f5808512cdaa34dfdfb1a8f9e47dc7c0a339f1ee8842a8", + "text": "import{WorkerEntrypoint as xe}from\"cloudflare:workers\";var G=globalThis.__worker_init_state={criticalError:!1,instanceId:0};import X from\"./index_bg.wasm\";var x=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,be.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_containerstartupoptions_free(e,0)}catch(t){o(t)}}get enableInternet(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_containerstartupoptions_enableInternet(this.__wbg_ptr)}catch(t){o(t)}return e===16777215?void 0:e!==0}get entrypoint(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_containerstartupoptions_entrypoint(this.__wbg_ptr)}catch(n){o(n)}var t=ye(e[0],e[1]);return i.__wbindgen_free(e[0],e[1]*4,4),t}get env(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_containerstartupoptions_env(this.__wbg_ptr)}catch(t){o(t)}return e}set enableInternet(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_containerstartupoptions_enableInternet(this.__wbg_ptr,u(e)?16777215:e?1:0)}catch(t){o(t)}}set entrypoint(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t=he(e,i.__wbindgen_malloc),n=y;c();try{i.__wbg_set_containerstartupoptions_entrypoint(this.__wbg_ptr,t,n)}catch(r){o(r)}}set env(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_containerstartupoptions_env(this.__wbg_ptr,e)}catch(t){o(t)}}};Symbol.dispose&&(x.prototype[Symbol.dispose]=x.prototype.free);var I=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,de.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_intounderlyingbytesource_free(e,0)}catch(t){o(t)}}get autoAllocateChunkSize(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.intounderlyingbytesource_autoAllocateChunkSize(this.__wbg_ptr)}catch(t){o(t)}return e>>>0}cancel(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e=this.__destroy_into_raw();c();try{i.intounderlyingbytesource_cancel(e)}catch(t){o(t)}}pull(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t;c();try{t=i.intounderlyingbytesource_pull(this.__wbg_ptr,e)}catch(n){o(n)}return t}start(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.intounderlyingbytesource_start(this.__wbg_ptr,e)}catch(t){o(t)}}get type(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.intounderlyingbytesource_type(this.__wbg_ptr)}catch(t){o(t)}return fe[e]}};Symbol.dispose&&(I.prototype[Symbol.dispose]=I.prototype.free);var E=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,ge.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_intounderlyingsink_free(e,0)}catch(t){o(t)}}abort(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t=this.__destroy_into_raw(),n;c();try{n=i.intounderlyingsink_abort(t,e)}catch(r){o(r)}return n}close(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e=this.__destroy_into_raw(),t;c();try{t=i.intounderlyingsink_close(e)}catch(n){o(n)}return t}write(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t;c();try{t=i.intounderlyingsink_write(this.__wbg_ptr,e)}catch(n){o(n)}return t}};Symbol.dispose&&(E.prototype[Symbol.dispose]=E.prototype.free);var R=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,we.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_intounderlyingsource_free(e,0)}catch(t){o(t)}}cancel(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e=this.__destroy_into_raw();c();try{i.intounderlyingsource_cancel(e)}catch(t){o(t)}}pull(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let t;c();try{t=i.intounderlyingsource_pull(this.__wbg_ptr,e)}catch(n){o(n)}return t}};Symbol.dispose&&(R.prototype[Symbol.dispose]=R.prototype.free);var m=class _{static __wrap(e){let t=Object.create(_.prototype);return t.__wbg_ptr=e,Object.defineProperty(t,\"__wbg_inst\",{value:s,writable:!0}),K.register(t,{ptr:e,instance:s},t),t}__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,K.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_minifyconfig_free(e,0)}catch(t){o(t)}}get css(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_minifyconfig_css(this.__wbg_ptr)}catch(t){o(t)}return e!==0}get html(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_minifyconfig_html(this.__wbg_ptr)}catch(t){o(t)}return e!==0}get js(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_minifyconfig_js(this.__wbg_ptr)}catch(t){o(t)}return e!==0}set css(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_minifyconfig_css(this.__wbg_ptr,e)}catch(t){o(t)}}set html(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_minifyconfig_html(this.__wbg_ptr,e)}catch(t){o(t)}}set js(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_minifyconfig_js(this.__wbg_ptr,e)}catch(t){o(t)}}};Symbol.dispose&&(m.prototype[Symbol.dispose]=m.prototype.free);var F=class{__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,le.unregister(this),e}free(){let e=this.__destroy_into_raw();c();try{i.__wbg_r2range_free(e,0)}catch(t){o(t)}}get length(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_r2range_length(this.__wbg_ptr)}catch(t){o(t)}return e[0]===0?void 0:e[1]}get offset(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_r2range_offset(this.__wbg_ptr)}catch(t){o(t)}return e[0]===0?void 0:e[1]}get suffix(){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");let e;c();try{e=i.__wbg_get_r2range_suffix(this.__wbg_ptr)}catch(t){o(t)}return e[0]===0?void 0:e[1]}set length(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_r2range_length(this.__wbg_ptr,!u(e),u(e)?0:e)}catch(t){o(t)}}set offset(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_r2range_offset(this.__wbg_ptr,!u(e),u(e)?0:e)}catch(t){o(t)}}set suffix(e){if(this.__wbg_inst!==void 0&&this.__wbg_inst!==s)throw new Error(\"Invalid stale object from previous Wasm instance\");c();try{i.__wbg_set_r2range_suffix(this.__wbg_ptr,!u(e),u(e)?0:e)}catch(t){o(t)}}};Symbol.dispose&&(F.prototype[Symbol.dispose]=F.prototype.free);function U(){s++,p=null,k=null,W=null,typeof numBytesDecoded<\"u\"&&(numBytesDecoded=0),typeof y<\"u\"&&(y=0),D=!1,O=!1,q=new WebAssembly.Instance(X,te()),i=q.exports,i.__wbindgen_start()}function Y(){let _;c();try{_=i.__worker_init_state()}catch(e){o(e)}return _}function Z(_,e,t){let n;c();try{n=i.fetch(_,e,t)}catch(r){o(r)}return n}function ee(){c();try{i.init()}catch(_){o(_)}}function te(){return{__proto__:null,\"./index_bg.js\":{__proto__:null,__wbg_Error_30c8987f7c2ed4e2:function(e,t){return Error(l(e,t))},__wbg_Number_14af1003b8dd5ead:function(e){return Number(e)},__wbg_String_8564e559799eccda:function(e,t){let n=String(t),r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg___wbindgen_bigint_get_as_i64_a2383202b9353e4c:function(e,t){let n=t,r=typeof n==\"bigint\"?n:void 0;d().setBigInt64(e+8,u(r)?BigInt(0):r,!0),d().setInt32(e+0,!u(r),!0)},__wbg___wbindgen_boolean_get_5b446f51afd21013:function(e){let t=e,n=typeof t==\"boolean\"?t:void 0;return u(n)?16777215:n?1:0},__wbg___wbindgen_debug_string_4687d8d8c2017d52:function(e,t){let n=V(t),r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg___wbindgen_in_92f62ee1427d9e49:function(e,t){return e in t},__wbg___wbindgen_is_bigint_b123553bed3bb382:function(e){return typeof e==\"bigint\"},__wbg___wbindgen_is_function_1f9d30630b8b1d3d:function(e){return typeof e==\"function\"},__wbg___wbindgen_is_null_e343b7d08827ba72:function(e){return e===null},__wbg___wbindgen_is_null_or_undefined_f9435d56bdcc06df:function(e){return e==null},__wbg___wbindgen_is_object_3c45d4f2dde4e749:function(e){let t=e;return typeof t==\"object\"&&t!==null},__wbg___wbindgen_is_string_90b56bc79aad6f6c:function(e){return typeof e==\"string\"},__wbg___wbindgen_is_undefined_8865fb403f8fe9d8:function(e){return e===void 0},__wbg___wbindgen_jsval_eq_02babf21faa37971:function(e,t){return e===t},__wbg___wbindgen_jsval_loose_eq_677f21e468d6b461:function(e,t){return e==t},__wbg___wbindgen_number_get_2e0e7dee9f701a71:function(e,t){let n=t,r=typeof n==\"number\"?n:void 0;d().setFloat64(e+8,u(r)?0:r,!0),d().setInt32(e+0,!u(r),!0)},__wbg___wbindgen_reinit_3f79fdb34dee3ea9:function(){O=!0},__wbg___wbindgen_string_get_0380ccaa2f57f0d9:function(e,t){let n=t,r=typeof n==\"string\"?n:void 0;var f=u(r)?0:S(r,i.__wbindgen_malloc,i.__wbindgen_realloc),a=y;d().setInt32(e+4,a,!0),d().setInt32(e+0,f,!0)},__wbg___wbindgen_throw_41e9ee4f547fc59a:function(e,t){throw new WebAssembly.Exception(B,[new Error(l(e,t))])},__wbg__wbg_cb_unref_dcc1a90847f04c41:function(e){e._wbg_cb_unref()},__wbg_abort_d5fa4a667a578774:function(e){return e.abort()},__wbg_abort_f78b99d80248bacb:function(e){e.abort()},__wbg_aborted_5b2b5f06b5e59fde:function(e){return e.aborted},__wbg_append_1938ed58fb5613b8:function(e,t,n,r,f){e.append(l(t,n),l(r,f))},__wbg_arrayBuffer_aa7cad37be6e6b29:function(e){return e.arrayBuffer()},__wbg_body_4792166cb351f162:function(e){let t=e.body;return u(t)?0:g(t)},__wbg_body_4e088733babc630d:function(e){let t=e.body;return u(t)?0:g(t)},__wbg_buffer_56ec2905a66f58b9:function(e){return e.buffer},__wbg_byobRequest_54f7ce41d584549a:function(e){let t=e.byobRequest;return u(t)?0:g(t)},__wbg_byteLength_9f985b8373f1f3fe:function(e){return e.byteLength},__wbg_byteOffset_0bd5a6a03553639c:function(e){return e.byteOffset},__wbg_call_1875a20c43a36133:function(e,t,n,r){return e.call(t,n,r)},__wbg_call_187d372bd5fdd4aa:function(e,t,n){return e.call(t,n)},__wbg_call_6137034ef55c9d0f:function(e,t){return e.call(t)},__wbg_call_939a2607c4484b0b:function(e,t,n,r,f){return e.call(t,n,r,f)},__wbg_cancel_3b323af6bb094e47:function(e){return e.cancel()},__wbg_cancel_5b5c53c5bbb13318:function(e){return e.cancel()},__wbg_cancel_7c2a08c8db4c73f3:function(e){return e.cancel()},__wbg_catch_92594c2d17f55f12:function(e,t){return e.catch(t)},__wbg_cause_19276d032a32d4bd:function(e){return e.cause},__wbg_cf_909bcfb212a8822c:function(e){let t=e.cf;return u(t)?0:g(t)},__wbg_cf_b47fe5ff6d015a0d:function(e){let t=e.cf;return u(t)?0:g(t)},__wbg_clearTimeout_d49a4afcd9b218b7:function(e,t){e.clearTimeout(t)},__wbg_close_185286988f30f252:function(e){e.close()},__wbg_close_748f26698dabe269:function(e){e.close()},__wbg_close_fd7fd8eaf125c9c1:function(e){return e.close()},__wbg_constructor_076176ce0f386e16:function(e){return e.constructor},__wbg_done_b41a1d26cdb37fb6:function(e){return e.done},__wbg_enqueue_4a01c128f9a0de16:function(e,t){e.enqueue(t)},__wbg_entries_b55ec7e0dc443355:function(e){return e.entries()},__wbg_entries_fb6397112b1de25f:function(e){return Object.entries(e)},__wbg_error_a30e98d44009f1bb:function(e,t){console.error(e,t)},__wbg_error_c9cf3fc2064683a9:function(e){console.error(e)},__wbg_fetch_0e46da8cf7c730e4:function(e,t,n,r){return e.fetch(l(t,n),r)},__wbg_fetch_7b5de4f88137a4dc:function(e,t,n){return e.fetch(t,n)},__wbg_getReader_1ef752ebe7bb7900:function(e,t){return e.getReader(t)},__wbg_getReader_bb0230851fbf986b:function(e){return e.getReader()},__wbg_getWriter_7994a8c7d4b75c01:function(e){return e.getWriter()},__wbg_get_31af05bd4842a84f:function(e,t){return Reflect.get(e,t)},__wbg_get_658f6698067d9515:function(e,t){return Reflect.get(e,t)},__wbg_get_6c896e0571ddae51:function(e,t){return e[t>>>0]},__wbg_get_88983e33467eae60:function(e,t){let n=Reflect.get(e,t);return u(n)?0:g(n)},__wbg_get_done_7020b91c987365c7:function(e){let t=e.done;return u(t)?16777215:t?1:0},__wbg_get_unchecked_288889d017702237:function(e,t){return e[t>>>0]},__wbg_get_value_87fbd6ad1283f2e7:function(e){return e.value},__wbg_get_with_ref_key_6412cf3094599694:function(e,t){return e[t]},__wbg_headers_6930a8bd09b630de:function(e){return e.headers},__wbg_headers_eba93595f8944c2f:function(e){return e.headers},__wbg_instanceId_27a130234331eb08:function(e){return e.instanceId},__wbg_instanceof_ArrayBuffer_a99f175873e5d9b8:function(e){let t;try{t=e instanceof ArrayBuffer}catch{t=!1}return t},__wbg_instanceof_Error_80a725f81f2e102d:function(e){let t;try{t=e instanceof Error}catch{t=!1}return t},__wbg_instanceof_Map_b2611749102d7ba3:function(e){let t;try{t=e instanceof Map}catch{t=!1}return t},__wbg_instanceof_Promise_e1dc6893aa1ece28:function(e){let t;try{t=e instanceof Promise}catch{t=!1}return t},__wbg_instanceof_ReadableStream_e2d77912575119ab:function(e){let t;try{t=e instanceof ReadableStream}catch{t=!1}return t},__wbg_instanceof_Response_b8758567269c30b2:function(e){let t;try{t=e instanceof Response}catch{t=!1}return t},__wbg_instanceof_Uint8Array_828cef2aaacafc31:function(e){let t;try{t=e instanceof Uint8Array}catch{t=!1}return t},__wbg_isArray_e15a2ff68ffdbef2:function(e){return Array.isArray(e)},__wbg_isSafeInteger_717808ad6a54bd9e:function(e){return Number.isSafeInteger(e)},__wbg_iterator_e3c31c892080e444:function(){return Symbol.iterator},__wbg_keys_bac434c8972731d5:function(e){return e.keys()},__wbg_length_7f3c00c40364105e:function(e){return e.length},__wbg_length_d4bdea10311bd9cf:function(e){return e.length},__wbg_message_5f8387f0c32b90a7:function(e){return e.message},__wbg_method_6d02ee0808a7de31:function(e,t){let n=t.method,r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg_minifyconfig_new:function(e){return m.__wrap(e)},__wbg_name_e2eac7cdfa054f65:function(e){return e.name},__wbg_name_f9b814d78e8e299f:function(e){return e.name},__wbg_new_1dbf7428bba60a42:function(e){return new Uint8Array(e)},__wbg_new_28744009d011f847:function(){return new Map},__wbg_new_2e41357094e99294:function(){return new AbortController},__wbg_new_343a093a3c2ffb4e:function(e,t){return new Error(l(e,t))},__wbg_new_617a8cdb8bb1130e:function(){return new Object},__wbg_new_661230f001cf2087:function(){return new TransformStream},__wbg_new_83ca405eae412a62:function(){return new Headers},__wbg_new_big_int_84cdc69174f81f5b:function(e){return new FixedLengthStream(e)},__wbg_new_ee2291f50781bf1d:function(){return new Array},__wbg_new_f569a2e335eed0bb:function(e){return new FixedLengthStream(e>>>0)},__wbg_new_from_slice_9a868026ffa4208a:function(e,t){return new Uint8Array(L(e,t))},__wbg_new_typed_b01cb72a8af741a3:function(e,t){try{var n={a:e,b:t},r=(a,b)=>{let w=n.a;n.a=0;try{return ce(w,n.b,a,b)}finally{n.a=w}};return new Promise(r)}finally{n.a=0}},__wbg_new_with_byte_offset_and_length_2f5d7fc2a828b74d:function(e,t,n){return new Uint8Array(e,t>>>0,n>>>0)},__wbg_new_with_length_3da0ad195f6f63ba:function(e){return new Uint8Array(e>>>0)},__wbg_new_with_opt_buffer_source_and_init_c1b859a23c80afca:function(e,t){return new Response(e,t)},__wbg_new_with_opt_readable_stream_and_init_df114f2d364f216d:function(e,t){return new Response(e,t)},__wbg_new_with_opt_str_and_init_33bef1b272950372:function(e,t,n){return new Response(e===0?void 0:l(e,t),n)},__wbg_new_with_str_and_init_4618dee4e950224f:function(e,t,n){return new Request(l(e,t),n)},__wbg_next_33784799010f1bbe:function(e){return e.next},__wbg_next_f4aac29c42af995c:function(e){return e.next()},__wbg_now_e7c6795a7f81e10f:function(e){return e.now()},__wbg_performance_3fcf6e32a7e1ed0a:function(e){return e.performance},__wbg_prototypesetcall_bc27214492979395:function(e,t,n){Uint8Array.prototype.set.call(L(e,t),n)},__wbg_queueMicrotask_9833f9a49df95a49:function(e){return e.queueMicrotask},__wbg_queueMicrotask_a72f977e97f23c5f:function(e){queueMicrotask(e)},__wbg_read_2a943774344c9728:function(e){return e.read()},__wbg_read_8cba94edf8011d59:function(e,t){return e.read(t)},__wbg_readable_4d43535945ae9ca0:function(e){return e.readable},__wbg_releaseLock_0948949015793a6d:function(e){e.releaseLock()},__wbg_releaseLock_eb64cd83aa9fcfe4:function(e){e.releaseLock()},__wbg_releaseLock_fe24bdfe19e75e0b:function(e){e.releaseLock()},__wbg_resolve_0076e10020304ede:function(e){return Promise.resolve(e)},__wbg_respond_c102fabcc79e5ef0:function(e,t){e.respond(t>>>0)},__wbg_setTimeout_7fb1e1fe294b6ad9:function(e,t,n){return e.setTimeout(t,n)},__wbg_set_145a351398b48c65:function(e,t,n){return Reflect.set(e,t,n)},__wbg_set_575d3ddb70fe831d:function(e,t,n){e.set(L(t,n))},__wbg_set_6ae97e73113c4f0b:function(e,t,n){return e.set(t,n)},__wbg_set_6be42768c690e380:function(e,t,n){e[t]=n},__wbg_set_af5932e3f78b5648:function(e,t,n,r,f){e.set(l(t,n),l(r,f))},__wbg_set_bea140a88be9b277:function(e,t,n){e[t>>>0]=n},__wbg_set_body_1fb0f1008bfc7df6:function(e,t){e.body=t},__wbg_set_cache_7507c4cc5938ccc7:function(e,t){e.cache=ae[t]},__wbg_set_criticalError_0d391f2a45c3bc42:function(e,t){e.criticalError=t!==0},__wbg_set_headers_40ad33dcf016613f:function(e,t){e.headers=t},__wbg_set_headers_69ef7c46a2e6c742:function(e,t){e.headers=t},__wbg_set_instanceId_9b3420954adec865:function(e,t){e.instanceId=t>>>0},__wbg_set_method_dcb32343247ec427:function(e,t,n){e.method=l(t,n)},__wbg_set_mode_6d06239cb5535f25:function(e,t){e.mode=se[t]},__wbg_set_redirect_cc128ab2411da334:function(e,t){e.redirect=ue[t]},__wbg_set_signal_2dbd377604627f26:function(e,t){e.signal=t},__wbg_set_status_678f122cbe435d95:function(e,t){e.status=t},__wbg_signal_26a6bdfa17b46df6:function(e){return e.signal},__wbg_signal_e5a49a2d9c65b7f6:function(e){return e.signal},__wbg_static_accessor_GLOBAL_266715b9d96ba635:function(){let e=typeof global>\"u\"?null:global;return u(e)?0:g(e)},__wbg_static_accessor_GLOBAL_THIS_10fb7dc1ae063179:function(){let e=typeof globalThis>\"u\"?null:globalThis;return u(e)?0:g(e)},__wbg_static_accessor_INIT_STATE_35966a05809b7176:function(){return G},__wbg_static_accessor_SELF_0b583911f537483a:function(){let e=typeof self>\"u\"?null:self;return u(e)?0:g(e)},__wbg_static_accessor_WINDOW_d7f903d1508cbdc4:function(){let e=typeof window>\"u\"?null:window;return u(e)?0:g(e)},__wbg_status_ce0a98d3c57125f3:function(e){return e.status},__wbg_subarray_002b94d5e13d1411:function(e,t,n){return e.subarray(t>>>0,n>>>0)},__wbg_then_4e48da6c0a644c56:function(e,t,n){return e.then(t,n)},__wbg_then_c8a35d4ad59c6b8e:function(e,t){return e.then(t)},__wbg_then_c949d5a25a4e78f8:function(e,t,n){return e.then(t,n)},__wbg_then_e71170d78fcf8954:function(e,t){return e.then(t)},__wbg_url_7bfada5c2297bc70:function(e,t){let n=t.url,r=S(n,i.__wbindgen_malloc,i.__wbindgen_realloc),f=y;d().setInt32(e+4,f,!0),d().setInt32(e+0,r,!0)},__wbg_value_f3c585ee8f5ba40c:function(e){return e.value},__wbg_view_9c570f33e8d6ab96:function(e){let t=e.view;return u(t)?0:g(t)},__wbg_waitUntil_c51e37673c596e00:function(e,t){e.waitUntil(t)},__wbg_webSocket_2b7e8e9549bed673:function(e){let t=e.webSocket;return u(t)?0:g(t)},__wbg_writable_44735d15a1cee85d:function(e){return e.writable},__wbg_write_bf07aa79dd36a472:function(e,t){return e.write(t)},__wbindgen_generic_0000000000000001:function(e,t){return C(e,t,oe)},__wbindgen_generic_0000000000000002:function(e,t){return C(e,t,ie)},__wbindgen_generic_0000000000000003:function(e,t){return C(e,t,re)},__wbindgen_generic_0000000000000004:function(e){return e},__wbindgen_generic_0000000000000005:function(e){return e},__wbindgen_generic_0000000000000006:function(e,t){return l(e,t)},__wbindgen_generic_0000000000000007:function(e){return BigInt.asUintN(64,e)},__wbindgen_init_externref_table:function(){let e=i.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)},__wbindgen_jstag:WebAssembly.JSTag,__wbindgen_rethrow_critical:function(e){throw new Error(\"Critical error\",{cause:e})}}}}var B=new WebAssembly.Tag({parameters:[\"externref\"]}),J,D=!1;function ne(){D=!0;try{let _=N()[i.__abort_handler.value/4];_&&i.__wbindgen_export.get(_)()}catch{}}function o(_){throw _ instanceof WebAssembly.Exception&&_.is(B)?_.getArg(B,0):(N()[J]=1,ne(),_)}function c(){if(J??=i.__instance_terminated.value/4,N()[J]){if(D||ne(),O){U();return}throw new Error(\"Module terminated\")}else O&&U()}function re(_,e){c();try{i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke_______true_(_,e)}catch(t){o(t)}}function ie(_,e,t){c();try{i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke___wasm_bindgen_c7f02901330ffbdc___JsValue______true_(_,e,t)}catch(n){o(n)}}function oe(_,e,t){let n;c();try{n=i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke___wasm_bindgen_c7f02901330ffbdc___JsValue__core_ed718c3d60ebd546___result__Result_____wasm_bindgen_c7f02901330ffbdc___JsError___true_(_,e,t)}catch(r){o(r)}if(n[1])throw pe(n[0])}function ce(_,e,t,n){c();try{i.wasm_bindgen_c7f02901330ffbdc___convert__closures_____invoke___js_sys_b2f584a2e795ca61___Function_fn_wasm_bindgen_c7f02901330ffbdc___JsValue_____wasm_bindgen_c7f02901330ffbdc___sys__Undefined___js_sys_b2f584a2e795ca61___Function_fn_wasm_bindgen_c7f02901330ffbdc___JsValue_____wasm_bindgen_c7f02901330ffbdc___sys__Undefined_______true_(_,e,t,n)}catch(r){o(r)}}var se=[\"byob\"],fe=[\"bytes\"],ae=[\"default\",\"no-store\",\"reload\",\"no-cache\",\"force-cache\",\"only-if-cached\"],ue=[\"follow\",\"error\",\"manual\"],s=0,be=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_containerstartupoptions_free(_,1)}),de=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_intounderlyingbytesource_free(_,1)}),ge=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_intounderlyingsink_free(_,1)}),we=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_intounderlyingsource_free(_,1)}),K=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_minifyconfig_free(_,1)}),le=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(({ptr:_,instance:e})=>{e===s&&i.__wbg_r2range_free(_,1)});function g(_){let e=i.__externref_table_alloc();return i.__wbindgen_externrefs.set(e,_),e}var Q=typeof FinalizationRegistry>\"u\"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(_=>{_.instance===s&&i.__wbindgen_destroy_closure(_.a,_.b)});function V(_){let e=typeof _;if(e==\"number\"||e==\"boolean\"||_==null)return`${_}`;if(e==\"string\")return`\"${_}\"`;if(e==\"symbol\"){let r=_.description;return r==null?\"Symbol\":`Symbol(${r})`}if(e==\"function\"){let r=_.name;return typeof r==\"string\"&&r.length>0?`Function(${r})`:\"Function\"}if(Array.isArray(_)){let r=_.length,f=\"[\";r>0&&(f+=V(_[0]));for(let a=1;a1)n=t[1];else return toString.call(_);if(n==\"Object\")try{return\"Object(\"+JSON.stringify(_)+\")\"}catch{return\"Object\"}return _ instanceof Error?`${_.name}: ${_.message}\n${_.stack}`:n}function ye(_,e){_=_>>>0;let t=d(),n=[];for(let r=_;r<_+4*e;r+=4)n.push(i.__wbindgen_externrefs.get(t.getUint32(r,!0)));return i.__externref_drop_slice(_,e),n}function L(_,e){return _=_>>>0,A().subarray(_/1,_/1+e)}var p=null;function d(){return(p===null||p.buffer.detached===!0||p.buffer.detached===void 0&&p.buffer!==i.memory.buffer)&&(p=new DataView(i.memory.buffer)),p}var k=null;function N(){return(k===null||k.byteLength===0)&&(k=new Int32Array(i.memory.buffer)),k}function l(_,e){return me(_>>>0,e)}var W=null;function A(){return(W===null||W.byteLength===0)&&(W=new Uint8Array(i.memory.buffer)),W}function u(_){return _==null}function C(_,e,t){let n={a:_,b:e,cnt:1,instance:s},r=(...f)=>{if(n.instance!==s)throw new Error(\"Cannot invoke closure from previous WASM instance\");n.cnt++;let a=n.a;n.a=0;try{return t(a,n.b,...f)}finally{n.a=a,r._wbg_cb_unref()}};return r._wbg_cb_unref=()=>{--n.cnt===0&&(i.__wbindgen_destroy_closure(n.a,n.b),n.a=0,Q.unregister(n))},Q.register(r,n,n),r}function he(_,e){let t=e(_.length*4,4)>>>0;for(let n=0;n<_.length;n++){let r=g(_[n]);d().setUint32(t+4*n,r,!0)}return y=_.length,t}function S(_,e,t){if(t===void 0){let b=T.encode(_),w=e(b.length,1)>>>0;return A().subarray(w,w+b.length).set(b),y=b.length,w}let n=_.length,r=e(n,1)>>>0,f=A(),a=0;for(;a127)break;f[r+a]=b}if(a!==n){a!==0&&(_=_.slice(a)),r=t(r,n,n=a+_.length*3,1)>>>0;let b=A().subarray(r+a,r+n),w=T.encodeInto(_,b);a+=w.written,r=t(r,n,a,1)>>>0}return y=a,r}var O=!1;function pe(_){let e=i.__wbindgen_externrefs.get(_);return i.__externref_table_dealloc(_),e}var _e=new TextDecoder(\"utf-8\",{ignoreBOM:!0,fatal:!0});_e.decode();function me(_,e){return _e.decode(A().subarray(_,_+e))}var T=new TextEncoder;\"encodeInto\"in T||(T.encodeInto=function(_,e){let t=T.encode(_);return e.set(t),{read:_.length,written:t.length}});var y=0,q=new WebAssembly.Instance(X,te()),i=q.exports;i.__wbindgen_start();Error.stackTraceLimit=100;var h=Y();function $(){h.criticalError&&(console.log(\"Reinitializing Wasm application\"),U(),h.criticalError=!1,h.instanceId++)}addEventListener(\"error\",_=>{H(_.error)});function H(_){_ instanceof WebAssembly.RuntimeError&&(console.error(\"Critical\",_),h.criticalError=!0)}var P=class extends xe{};P.prototype.fetch=function(e){return Z.call(this,e,this.env,this.ctx)};P.prototype.init=ee;var Ie={set:(_,e,t,n)=>Reflect.set(_.instance,e,t,n),has:(_,e)=>Reflect.has(_.instance,e),deleteProperty:(_,e)=>Reflect.deleteProperty(_.instance,e),apply:(_,e,t)=>Reflect.apply(_.instance,e,t),construct:(_,e,t)=>Reflect.construct(_.instance,e,t),getPrototypeOf:_=>Reflect.getPrototypeOf(_.instance),setPrototypeOf:(_,e)=>Reflect.setPrototypeOf(_.instance,e),isExtensible:_=>Reflect.isExtensible(_.instance),preventExtensions:_=>Reflect.preventExtensions(_.instance),getOwnPropertyDescriptor:(_,e)=>Reflect.getOwnPropertyDescriptor(_.instance,e),defineProperty:(_,e,t)=>Reflect.defineProperty(_.instance,e,t),ownKeys:_=>Reflect.ownKeys(_.instance)},v={construct(_,e,t){try{$();let n={instance:Reflect.construct(_,e,t),instanceId:h.instanceId,ctor:_,args:e,newTarget:t};return new Proxy(n,{...Ie,get(r,f,a){r.instanceId!==h.instanceId&&(r.instance=Reflect.construct(r.ctor,r.args,r.newTarget),r.instanceId=h.instanceId);let b=Reflect.get(r.instance,f,a);return typeof b!=\"function\"?b:b.constructor===Function?new Proxy(b,{apply(w,z,M){$();try{return w.apply(z,M)}catch(j){throw H(j),j}}}):new Proxy(b,{async apply(w,z,M){$();try{return await w.apply(z,M)}catch(j){throw H(j),j}}})}})}catch(n){throw h.criticalError=!0,n}}},Se=new Proxy(P,v),ke=new Proxy(x,v),We=new Proxy(I,v),Ae=new Proxy(E,v),Te=new Proxy(R,v),Pe=new Proxy(m,v),Ue=new Proxy(F,v);export{ke as ContainerStartupOptions,We as IntoUnderlyingByteSource,Ae as IntoUnderlyingSink,Te as IntoUnderlyingSource,Pe as MinifyConfig,Ue as R2Range,Se as default};\n" + }, + "shim": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/progress-cloudflare/artifacts/build/worker/shim.mjs", + "bytes": 164, + "sha256": "ac99177861405a6046923cbacabbd6adfa1853a56d0c679b9f5e09097c2f4728", + "text": "// Use index.js directly, this file provided for backwards compat\n// with former shim.mjs only.\nexport * from '../index.js';\nexport { default } from '../index.js';\n" + } + }, + { + "adapter": "fastly", + "counts": { + "outbound-coding": 24, + "outbound-marker": 46, + "outbound-relay": 24, + "outbound-progress": 5, + "inbound-empty-signal": 5, + "progress-group": 4, + "client_completed": 104 + }, + "runtime": { + "adapter": "fastly", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "source_state": "working-tree-characterization", + "version": "viceroy 0.21.0", + "identity": { + "path": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "bytes": 53317104, + "sha256": "751ff2c1a83063b109bc212b95be5f52e5ab6284b9027fa03aa229087eba3f30" + } + }, + "artifact": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/progress-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "bytes": 2153968, + "sha256": "b50e53dd7d1681c4bb5fd02e857e09f6ad977dba5145f656041eb5d73d0d4eab" + }, + "runtime_log": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/progress-fastly/runtime.log", + "bytes": 141, + "sha256": "d91a24e5fbf83806b45828a4bcda379c828ac946aa322bb73164b45ee3244c39", + "text": "2026-10-20T22:58:51.515472Z WARN no backend definitions found in /private/tmp/edgezero-relay-all-20261020-final/progress-fastly/fastly.toml\n" + }, + "relay": [ + { + "adapter": "fastly", + "case": "relay-gzip-buffered-200-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 10347334, + "event": "client_completed", + "first_byte_ns": 10342417, + "header_ns": 10342167, + "headers": [ + ["content-encoding", "gzip"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "37"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 200, + "token": "18e05f064fefea18-b166-46" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-gzip-buffered-206-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 9595750, + "event": "client_completed", + "first_byte_ns": 9589916, + "header_ns": 9589541, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["date", "Tue, 20 Oct 2026 22:58:51 GMT"] + ], + "status": 206, + "token": "18e05f0650964020-b166-47" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-gzip-buffered-206-decode", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 9449916, + "event": "client_completed", + "first_byte_ns": 9444916, + "header_ns": 9444666, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["date", "Tue, 20 Oct 2026 22:58:51 GMT"] + ], + "status": 206, + "token": "18e05f06513191d8-b166-48" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-gzip-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 12199584, + "event": "client_completed", + "first_byte_ns": 12193959, + "header_ns": 12192959, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:58:51 GMT"] + ], + "status": 200, + "token": "18e05f0651c92e58-b166-49" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-gzip-streamed-200-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 9553333, + "event": "client_completed", + "first_byte_ns": 9547458, + "header_ns": 9547167, + "headers": [ + ["content-encoding", "gzip"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "37"], + ["date", "Tue, 20 Oct 2026 22:58:51 GMT"] + ], + "status": 200, + "token": "18e05f06528a9610-b166-4a" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-gzip-streamed-206-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 9468750, + "event": "client_completed", + "first_byte_ns": 9463583, + "header_ns": 9463333, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["date", "Tue, 20 Oct 2026 22:58:51 GMT"] + ], + "status": 206, + "token": "18e05f06532547a0-b166-4b" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-gzip-streamed-206-decode", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 11477250, + "event": "client_completed", + "first_byte_ns": 11466834, + "header_ns": 11466500, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["date", "Tue, 20 Oct 2026 22:58:51 GMT"] + ], + "status": 206, + "token": "18e05f0653beeba8-b166-4c" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-gzip-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 13949833, + "event": "client_completed", + "first_byte_ns": 13944541, + "header_ns": 13943750, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:58:51 GMT"] + ], + "status": 200, + "token": "18e05f065477c010-b166-4d" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-deflate-buffered-200-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 9638250, + "event": "client_completed", + "first_byte_ns": 9630250, + "header_ns": 9630083, + "headers": [ + ["content-encoding", "deflate"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "25"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 200, + "token": "18e05f06555300f8-b166-4e" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-deflate-buffered-206-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 13989000, + "event": "client_completed", + "first_byte_ns": 13984333, + "header_ns": 13984083, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["date", "Tue, 20 Oct 2026 22:58:51 GMT"] + ], + "status": 206, + "token": "18e05f0655ee00a8-b166-4f" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-deflate-buffered-206-decode", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 13926834, + "event": "client_completed", + "first_byte_ns": 13922042, + "header_ns": 13921792, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 206, + "token": "18e05f0656cb7fc8-b166-50" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-deflate-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 9588625, + "event": "client_completed", + "first_byte_ns": 9581583, + "header_ns": 9580125, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:58:51 GMT"] + ], + "status": 200, + "token": "18e05f0657a7ed78-b166-51" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-deflate-streamed-200-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 14576667, + "event": "client_completed", + "first_byte_ns": 14569834, + "header_ns": 14520709, + "headers": [ + ["content-encoding", "deflate"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "25"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 200, + "token": "18e05f065841ef40-b166-52" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-deflate-streamed-206-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 10781583, + "event": "client_completed", + "first_byte_ns": 10776791, + "header_ns": 10776541, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 206, + "token": "18e05f065927d6b8-b166-53" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-deflate-streamed-206-decode", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 11154167, + "event": "client_completed", + "first_byte_ns": 11148000, + "header_ns": 11147750, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 206, + "token": "18e05f0659d452f8-b166-54" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-deflate-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 10956750, + "event": "client_completed", + "first_byte_ns": 10950750, + "header_ns": 10913625, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 200, + "token": "18e05f065a86a368-b166-55" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-br-buffered-200-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 10262625, + "event": "client_completed", + "first_byte_ns": 10258041, + "header_ns": 10257875, + "headers": [ + ["content-encoding", "br"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "21"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 200, + "token": "18e05f065b34e0e0-b166-56" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-br-buffered-206-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 13770209, + "event": "client_completed", + "first_byte_ns": 13765125, + "header_ns": 13764875, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 206, + "token": "18e05f065bd95288-b166-57" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-br-buffered-206-decode", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 9533000, + "event": "client_completed", + "first_byte_ns": 9528542, + "header_ns": 9528375, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 206, + "token": "18e05f065cb35708-b166-58" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-br-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 14503250, + "event": "client_completed", + "first_byte_ns": 14496708, + "header_ns": 14495416, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 200, + "token": "18e05f065d4c33d8-b166-59" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-br-streamed-200-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 9478375, + "event": "client_completed", + "first_byte_ns": 9472792, + "header_ns": 9472500, + "headers": [ + ["content-encoding", "br"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "21"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 200, + "token": "18e05f065e30fa40-b166-5a" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-br-streamed-206-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 13724291, + "event": "client_completed", + "first_byte_ns": 13716875, + "header_ns": 13716291, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 206, + "token": "18e05f065ec95240-b166-5b" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-br-streamed-206-decode", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 9533750, + "event": "client_completed", + "first_byte_ns": 9526416, + "header_ns": 9526041, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 206, + "token": "18e05f065fa36278-b166-5c" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "case": "relay-br-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 9473458, + "event": "client_completed", + "first_byte_ns": 9466458, + "header_ns": 9464791, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:58:52 GMT"] + ], + "status": 200, + "token": "18e05f06603de140-b166-5d" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + } + ], + "relay_coverage": { + "adapter": "fastly", + "attempted": 24, + "event": "outbound-relay-coverage", + "qualification": false, + "receipts": [ + "/coding/gzip/200", + "/coding/gzip/206", + "/coding/gzip/206", + "/coding/gzip/200", + "/coding/gzip/200", + "/coding/gzip/206", + "/coding/gzip/206", + "/coding/gzip/200", + "/coding/deflate/200", + "/coding/deflate/206", + "/coding/deflate/206", + "/coding/deflate/200", + "/coding/deflate/200", + "/coding/deflate/206", + "/coding/deflate/206", + "/coding/deflate/200", + "/coding/br/200", + "/coding/br/206", + "/coding/br/206", + "/coding/br/200", + "/coding/br/200", + "/coding/br/206", + "/coding/br/206", + "/coding/br/200" + ], + "status": "pass" + }, + "scheduling": [ + { + "case": "eager-single", + "status": "pass", + "probe": { + "adapter": "fastly", + "case": "eager-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 101801917, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 1, + "token": "18e05f066164d128-b166-5e" + }, + "receipts": [ + { + "path": "/g3/18e05f066164d128-b166-5e/start", + "received_ns": 12636083 + }, + { + "path": "/g3/18e05f066164d128-b166-5e/slot-0", + "received_ns": 25514542 + }, + { + "path": "/g3/18e05f066164d128-b166-5e/end", + "received_ns": 127463542 + } + ] + }, + { + "case": "lazy-single", + "status": "pass", + "probe": { + "adapter": "fastly", + "case": "lazy-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 102438208, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 1, + "token": "18e05f0676097070-b166-5f" + }, + "receipts": [ + { + "path": "/g3/18e05f0676097070-b166-5f/start", + "received_ns": 10088583 + }, + { + "path": "/g3/18e05f0676097070-b166-5f/end", + "received_ns": 123208417 + }, + { + "path": "/g3/18e05f0676097070-b166-5f/slot-0", + "received_ns": 133278875 + } + ] + }, + { + "case": "eager-batch", + "status": "pass", + "probe": { + "adapter": "fastly", + "case": "eager-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 101608959, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0, 0], + "slots": 2, + "token": "18e05f0690805770-b166-60" + }, + "receipts": [ + { + "path": "/g3/18e05f0690805770-b166-60/start", + "received_ns": 10145500 + }, + { + "path": "/g3/18e05f0690805770-b166-60/slot-1", + "received_ns": 22297916 + }, + { + "path": "/g3/18e05f0690805770-b166-60/slot-0", + "received_ns": 22304083 + }, + { + "path": "/g3/18e05f0690805770-b166-60/end", + "received_ns": 124298458 + } + ] + }, + { + "case": "baseline-batch", + "status": "pass", + "probe": { + "adapter": "fastly", + "case": "baseline-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": false, + "pause_ns": 103193000, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 2, + "token": "18e05f06a4c1dba0-b166-61" + }, + "receipts": [ + { + "path": "/g3/18e05f06a4c1dba0-b166-61/start", + "received_ns": 12270917 + }, + { + "path": "/g3/18e05f06a4c1dba0-b166-61/slot-1", + "received_ns": 27290542 + }, + { + "path": "/g3/18e05f06a4c1dba0-b166-61/slot-0", + "received_ns": 27295792 + }, + { + "path": "/g3/18e05f06a4c1dba0-b166-61/end", + "received_ns": 128010959 + } + ] + }, + { + "case": "response-owned", + "status": "pass", + "probe": { + "adapter": "fastly", + "case": "response-owned", + "consumer_polls_before_window_end": 0, + "dispatch_returned": null, + "handler_returned": true, + "pause_ns": 101436500, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0, 0], + "slots": 2, + "token": "18e05f06b9407c58-b166-62" + }, + "receipts": [ + { + "path": "/g3/18e05f06b9407c58-b166-62/start", + "received_ns": 12567375 + }, + { + "path": "/g3/18e05f06b9407c58-b166-62/slot-1", + "received_ns": 25854750 + }, + { + "path": "/g3/18e05f06b9407c58-b166-62/slot-0", + "received_ns": 25858125 + }, + { + "path": "/g3/18e05f06b9407c58-b166-62/end", + "received_ns": 120026667 + } + ] + } + ], + "group_results": [ + { + "adapter": "fastly", + "error": null, + "event": "progress-group", + "group": "coding", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "error": null, + "event": "progress-group", + "group": "marker", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "error": null, + "event": "progress-group", + "group": "relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "error": null, + "event": "progress-group", + "group": "progress-and-framing", + "qualification": false, + "status": "pass" + } + ], + "framing": [ + { + "framing": "Absent", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": 0, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "LengthZero", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": 0, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "ChunkedEmpty", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": null, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "FixedBody", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": 4, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "ChunkedBody", + "probe": { + "adapter": "fastly", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "has_body": true, + "known_length": null, + "source": "fastly-sdk-before-conversion" + }, + "method": "GET", + "qualification": false + } + } + ], + "receipts": { + "relay": 24, + "coding": 24, + "marker": 47, + "scheduling": 18, + "total_base": 113, + "retained_relay_and_scheduling": 42, + "note": "coding and marker receipts are asserted by successful runner guards; Spin additionally has two SDK header-control receipts" + } + }, + { + "adapter": "spin", + "counts": { + "outbound-coding": 24, + "outbound-marker": 46, + "outbound-relay": 24, + "outbound-progress": 5, + "inbound-empty-signal": 5, + "progress-group": 4, + "client_completed": 110 + }, + "runtime": { + "adapter": "spin", + "event": "progress-runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "source_state": "working-tree-characterization", + "version": "spin 4.1.0 (c0b3726 2026-08-25)", + "identity": { + "path": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "bytes": 116793168, + "sha256": "0efe71a81495a75e81624186d457afa5398117e685567ba2cf90bd01a269128a" + } + }, + "artifact": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/progress-spin/artifacts/fixture_spin.wasm", + "bytes": 2310390, + "sha256": "e8973279d619a69d14c8b3b98aa3734ac62658fcdb4befa06065d41138f90c38" + }, + "runtime_log": { + "path": "/private/tmp/edgezero-relay-all-20261020-final/progress-spin/runtime.log", + "bytes": 13633, + "sha256": "700a17d5136b2cf5b2235fc5845ed8bbc00adb6dae4f409ae175616cb9008191", + "text": "Using runtime config [key_value_store.fixture_config: spin], [key_value_store.fixture_kv: spin] from \"/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/crates/fixture-spin/runtime-config.toml\"\nLogging component stdio to \"/private/tmp/edgezero-relay-all-20261020-final/progress-spin/.spin/logs/\"\n\nServing http://127.0.0.1:49876\nAvailable Routes:\n fixture: http://127.0.0.1:49876 (wildcard)\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"case\":\"sdk-stream-ready\",\"coordinator_ok\":true,\"event\":\"sdk-stream-coordinator-terminal\",\"qualification\":false,\"token\":\"18e05f0a64043758-b166-132\"}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"case\":\"sdk-stream-timer-only\",\"coordinator_ok\":true,\"event\":\"sdk-stream-coordinator-terminal\",\"qualification\":false,\"token\":\"18e05f0a64128380-b166-133\"}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\n{\"event\":\"progress-dispatch-start\",\"qualification\":false}\n{\"event\":\"progress-dispatch-returned\",\"qualification\":false}\nworker failed: worker panicked\n" + }, + "relay": [ + { + "adapter": "spin", + "case": "relay-gzip-buffered-200-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 12357791, + "event": "client_completed", + "first_byte_ns": 12342791, + "header_ns": 12342291, + "headers": [ + ["content-encoding", "gzip"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a49335530-b166-118" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-gzip-buffered-206-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 14382458, + "event": "client_completed", + "first_byte_ns": 14373666, + "header_ns": 14372958, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a49f929b8-b166-119" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-gzip-buffered-206-decode", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 9898500, + "event": "client_completed", + "first_byte_ns": 9887458, + "header_ns": 9886750, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a4ae05180-b166-11a" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-gzip-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 9901042, + "event": "client_completed", + "first_byte_ns": 9877500, + "header_ns": 9875459, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a4b81d910-b166-11b" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-gzip-streamed-200-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 14346166, + "event": "client_completed", + "first_byte_ns": 14338208, + "header_ns": 14337458, + "headers": [ + ["content-encoding", "gzip"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a4c2210b0-b166-11c" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-gzip-streamed-206-original", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 13725417, + "event": "client_completed", + "first_byte_ns": 13716000, + "header_ns": 13715500, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a4d07fff8-b166-11d" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-gzip-streamed-206-decode", + "client": { + "body_bytes": [ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 43, 74, 44, 87, 72, 206, 79, + 201, 204, 75, 87, 40, 40, 202, 79, 74, 229, 2, 0, 25, 107, 60, 67, + 17, 0, 0, 0 + ], + "complete_ns": 13354208, + "event": "client_completed", + "first_byte_ns": 13342875, + "header_ns": 13342417, + "headers": [ + ["content-encoding", "gzip"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-range", "bytes 0-36/37"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "37"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a4de38f00-b166-11e" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-gzip-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 14011541, + "event": "client_completed", + "first_byte_ns": 14002166, + "header_ns": 14000125, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:BkzflHMq10MSCPDB7aAGrb3zFOrrCTOp44/5RdKVon4=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a4ebfbe30-b166-11f" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-deflate-buffered-200-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 10324000, + "event": "client_completed", + "first_byte_ns": 10316125, + "header_ns": 10315666, + "headers": [ + ["content-encoding", "deflate"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a4f9e4ec0-b166-120" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-deflate-buffered-206-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 14404917, + "event": "client_completed", + "first_byte_ns": 14396042, + "header_ns": 14395375, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a5045b250-b166-121" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-deflate-buffered-206-decode", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 14452959, + "event": "client_completed", + "first_byte_ns": 14442625, + "header_ns": 14441959, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a512c0340-b166-122" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-deflate-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 10849709, + "event": "client_completed", + "first_byte_ns": 10836959, + "header_ns": 10834542, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a52129a80-b166-123" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-deflate-streamed-200-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 12946625, + "event": "client_completed", + "first_byte_ns": 12940042, + "header_ns": 12939625, + "headers": [ + ["content-encoding", "deflate"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a52c65250-b166-124" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-deflate-streamed-206-original", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 14578041, + "event": "client_completed", + "first_byte_ns": 14570916, + "header_ns": 14570458, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a53945498-b166-125" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-deflate-streamed-206-decode", + "client": { + "body_bytes": [ + 120, 156, 43, 74, 44, 87, 72, 206, 79, 201, 204, 75, 87, 40, 40, + 202, 79, 74, 229, 2, 0, 57, 147, 6, 33 + ], + "complete_ns": 14331542, + "event": "client_completed", + "first_byte_ns": 14316709, + "header_ns": 14316292, + "headers": [ + ["content-encoding", "deflate"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-range", "bytes 0-24/25"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "25"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a547c3bc8-b166-126" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-deflate-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 12894208, + "event": "client_completed", + "first_byte_ns": 12787583, + "header_ns": 12785833, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:8VYiRs57HGMrKxE4DDr7KClv+uCe8a9YdPsXzEiK3q0=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a555eeef0-b166-127" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-br-buffered-200-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 12509625, + "event": "client_completed", + "first_byte_ns": 12500541, + "header_ns": 12499541, + "headers": [ + ["content-encoding", "br"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a562c60b0-b166-128" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-br-buffered-206-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 10802500, + "event": "client_completed", + "first_byte_ns": 10793792, + "header_ns": 10793208, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a56f3bfc0-b166-129" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-br-buffered-206-decode", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 13882375, + "event": "client_completed", + "first_byte_ns": 13874584, + "header_ns": 13874250, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a57a19f78-b166-12a" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-br-buffered-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 11843792, + "event": "client_completed", + "first_byte_ns": 11835875, + "header_ns": 11833833, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a587dd678-b166-12b" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-br-streamed-200-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 14544167, + "event": "client_completed", + "first_byte_ns": 14534500, + "header_ns": 14533750, + "headers": [ + ["content-encoding", "br"], + ["cache-control", "max-age=60"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a593ac1c0-b166-12c" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-br-streamed-206-original", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 14415292, + "event": "client_completed", + "first_byte_ns": 14405834, + "header_ns": 14405209, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a5a212e08-b166-12d" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-br-streamed-206-decode", + "client": { + "body_bytes": [ + 11, 8, 128, 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, + 112, 114, 111, 98, 101, 10, 3 + ], + "complete_ns": 14371083, + "event": "client_completed", + "first_byte_ns": 14362083, + "header_ns": 14361500, + "headers": [ + ["content-encoding", "br"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-range", "bytes 0-20/21"], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["accept-ranges", "bytes"], + ["cache-control", "max-age=60"], + ["content-length", "21"], + ["connection", "close"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 206, + "token": "18e05f0a5b071580-b166-12e" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "relay-br-streamed-200-decode", + "client": { + "body_bytes": [ + 114, 97, 119, 32, 99, 111, 100, 105, 110, 103, 32, 112, 114, 111, + 98, 101, 10 + ], + "complete_ns": 14519250, + "event": "client_completed", + "first_byte_ns": 14496125, + "header_ns": 14493625, + "headers": [ + ["cache-control", "max-age=60"], + ["accept-ranges", "bytes"], + [ + "content-digest", + "sha-256=:nI5tLQUWd+A4M5qx6/4gCIB6dLeWM/EcfwAg0GqQnBQ=:" + ], + ["content-type", "application/octet-stream"], + ["etag", "\"coding-probe\""], + ["last-modified", "Tue, 20 Oct 2026 00:00:00 GMT"], + ["vary", "Accept-Encoding"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a5beb2838-b166-12f" + }, + "error": null, + "event": "outbound-relay", + "qualification": false, + "status": "pass" + } + ], + "relay_coverage": { + "adapter": "spin", + "attempted": 24, + "event": "outbound-relay-coverage", + "qualification": false, + "receipts": [ + "/coding/gzip/200", + "/coding/gzip/206", + "/coding/gzip/206", + "/coding/gzip/200", + "/coding/gzip/200", + "/coding/gzip/206", + "/coding/gzip/206", + "/coding/gzip/200", + "/coding/deflate/200", + "/coding/deflate/206", + "/coding/deflate/206", + "/coding/deflate/200", + "/coding/deflate/200", + "/coding/deflate/206", + "/coding/deflate/206", + "/coding/deflate/200", + "/coding/br/200", + "/coding/br/206", + "/coding/br/206", + "/coding/br/200", + "/coding/br/200", + "/coding/br/206", + "/coding/br/206", + "/coding/br/200" + ], + "status": "pass" + }, + "scheduling": [ + { + "case": "eager-single", + "status": "pass", + "probe": { + "adapter": "spin", + "case": "eager-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 102479583, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 1, + "token": "18e05f0a6a485f68-b166-134" + }, + "receipts": [ + { + "path": "/g3/18e05f0a6a485f68-b166-134/start", + "received_ns": 15120625 + }, + { + "path": "/g3/18e05f0a6a485f68-b166-134/slot-0", + "received_ns": 25226542 + }, + { + "path": "/g3/18e05f0a6a485f68-b166-134/end", + "received_ns": 128730542 + } + ] + }, + { + "case": "lazy-single", + "status": "pass", + "probe": { + "adapter": "spin", + "case": "lazy-single", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 103077500, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 1, + "token": "18e05f0a7e6cf170-b166-135" + }, + "receipts": [ + { + "path": "/g3/18e05f0a7e6cf170-b166-135/start", + "received_ns": 11094542 + }, + { + "path": "/g3/18e05f0a7e6cf170-b166-135/end", + "received_ns": 117558250 + }, + { + "path": "/g3/18e05f0a7e6cf170-b166-135/slot-0", + "received_ns": 130191459 + } + ] + }, + { + "case": "eager-batch", + "status": "pass", + "probe": { + "adapter": "spin", + "case": "eager-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 103061500, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0, 0], + "slots": 2, + "token": "18e05f0a9933fc10-b166-136" + }, + "receipts": [ + { + "path": "/g3/18e05f0a9933fc10-b166-136/start", + "received_ns": 11361000 + }, + { + "path": "/g3/18e05f0a9933fc10-b166-136/slot-0", + "received_ns": 24901458 + }, + { + "path": "/g3/18e05f0a9933fc10-b166-136/slot-1", + "received_ns": 24908458 + }, + { + "path": "/g3/18e05f0a9933fc10-b166-136/end", + "received_ns": 123262333 + } + ] + }, + { + "case": "baseline-batch", + "status": "pass", + "probe": { + "adapter": "spin", + "case": "baseline-batch", + "consumer_polls_before_window_end": 0, + "dispatch_returned": false, + "handler_returned": false, + "pause_ns": 102698375, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0], + "slots": 2, + "token": "18e05f0aad40a500-b166-137" + }, + "receipts": [ + { + "path": "/g3/18e05f0aad40a500-b166-137/start", + "received_ns": 15065834 + }, + { + "path": "/g3/18e05f0aad40a500-b166-137/end", + "received_ns": 132436292 + }, + { + "path": "/g3/18e05f0aad40a500-b166-137/slot-0", + "received_ns": 142872875 + }, + { + "path": "/g3/18e05f0aad40a500-b166-137/slot-1", + "received_ns": 142876084 + } + ] + }, + { + "case": "response-owned", + "status": "pass", + "probe": { + "adapter": "spin", + "case": "response-owned", + "consumer_polls_before_window_end": 0, + "dispatch_returned": true, + "handler_returned": true, + "pause_ns": 103022209, + "protocol": "http1", + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sizes": [0, 0], + "slots": 2, + "token": "18e05f0ac8f52898-b166-138" + }, + "receipts": [ + { + "path": "/g3/18e05f0ac8f52898-b166-138/start", + "received_ns": 14940375 + }, + { + "path": "/g3/18e05f0ac8f52898-b166-138/slot-0", + "received_ns": 27896959 + }, + { + "path": "/g3/18e05f0ac8f52898-b166-138/slot-1", + "received_ns": 27903084 + }, + { + "path": "/g3/18e05f0ac8f52898-b166-138/end", + "received_ns": 124143709 + } + ] + } + ], + "group_results": [ + { + "adapter": "spin", + "error": null, + "event": "progress-group", + "group": "coding", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "error": null, + "event": "progress-group", + "group": "marker", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "error": null, + "event": "progress-group", + "group": "relay", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "error": null, + "event": "progress-group", + "group": "progress-and-framing", + "qualification": false, + "status": "pass" + } + ], + "framing": [ + { + "framing": "Absent", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": null, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "LengthZero", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": 0, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "ChunkedEmpty", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 0, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": null, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "FixedBody", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": 4, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + } + }, + { + "framing": "ChunkedBody", + "probe": { + "adapter": "spin", + "body_kind_before": "Initial", + "bytes_read": 4, + "case": "empty", + "host_signal": { + "end_stream": false, + "raw_framing_evidence": false, + "size_hint_lower": 0, + "size_hint_upper": null, + "source": "spin-sdk-after-conversion" + }, + "method": "GET", + "qualification": false + } + } + ], + "receipts": { + "relay": 24, + "coding": 24, + "marker": 47, + "scheduling": 18, + "total_base": 113, + "retained_relay_and_scheduling": 42, + "note": "coding and marker receipts are asserted by successful runner guards; Spin additionally has two SDK header-control receipts" + }, + "diagnostics": [ + { + "adapter": "spin", + "client": { + "body": "{\"case\":\"sdk-marker-headers\",\"connection\":[],\"markers\":[\"upstream\"],\"qualification\":false,\"sdk_native\":true}", + "complete_ns": 15408666, + "event": "client_completed", + "first_byte_ns": 15397583, + "header_ns": 15396041, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:08 GMT"] + ], + "status": 200, + "token": "18e05f0a21ea20a8-b166-e8" + }, + "event": "marker-sdk-header-visibility", + "probe": { + "case": "sdk-marker-headers", + "connection": [], + "markers": ["upstream"], + "qualification": false, + "sdk_native": true + }, + "qualification": false, + "shape": "upstream" + }, + { + "adapter": "spin", + "client": { + "body": "{\"case\":\"sdk-marker-headers\",\"connection\":[],\"markers\":[\"upstream\"],\"qualification\":false,\"sdk_native\":true}", + "complete_ns": 12726917, + "event": "client_completed", + "first_byte_ns": 12717875, + "header_ns": 12716125, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:08 GMT"] + ], + "status": 200, + "token": "18e05f0a22dd94e0-b166-e9" + }, + "event": "marker-sdk-header-visibility", + "probe": { + "case": "sdk-marker-headers", + "connection": [], + "markers": ["upstream"], + "qualification": false, + "sdk_native": true + }, + "qualification": false, + "shape": "nominated" + }, + { + "adapter": "spin", + "case": "response-owned-ready", + "event": "response-stream-control", + "observation": { + "client": { + "body": " {\"adapter\":\"spin\",\"case\":\"response-owned-ready\",\"dispatch_returned\":true,\"dispatch_returned_at_first_poll\":true,\"handler_returned\":true,\"handler_returned_at_first_poll\":true,\"pause_ns\":375,\"qualification\":false,\"revision\":\"0250edc9e3bffc797304b539eb69edf5be7a8bc3\",\"token\":\"18e05f0a5d8efcc8-b166-130\"}", + "complete_ns": 2851625, + "event": "client_completed", + "first_byte_ns": 2791583, + "header_ns": 944541, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a5d8efcc8-b166-130" + }, + "probe": { + "adapter": "spin", + "case": "response-owned-ready", + "dispatch_returned": true, + "dispatch_returned_at_first_poll": true, + "handler_returned": true, + "handler_returned_at_first_poll": true, + "pause_ns": 375, + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "token": "18e05f0a5d8efcc8-b166-130" + } + }, + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "response-owned-timer-only", + "event": "response-stream-control", + "observation": { + "client": { + "body": " {\"adapter\":\"spin\",\"case\":\"response-owned-timer-only\",\"dispatch_returned\":true,\"dispatch_returned_at_first_poll\":true,\"handler_returned\":true,\"handler_returned_at_first_poll\":true,\"pause_ns\":103481458,\"qualification\":false,\"revision\":\"0250edc9e3bffc797304b539eb69edf5be7a8bc3\",\"token\":\"18e05f0a5dc650d8-b166-131\"}", + "complete_ns": 104082125, + "event": "client_completed", + "first_byte_ns": 1377666, + "header_ns": 1373625, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a5dc650d8-b166-131" + }, + "probe": { + "adapter": "spin", + "case": "response-owned-timer-only", + "dispatch_returned": true, + "dispatch_returned_at_first_poll": true, + "handler_returned": true, + "handler_returned_at_first_poll": true, + "pause_ns": 103481458, + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "token": "18e05f0a5dc650d8-b166-131" + } + }, + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "sdk-stream-ready", + "event": "response-stream-control", + "observation": { + "client": { + "body": " {\"adapter\":\"spin\",\"case\":\"sdk-stream-ready\",\"pause_ns\":458,\"qualification\":false,\"revision\":\"0250edc9e3bffc797304b539eb69edf5be7a8bc3\",\"sdk_native\":true,\"token\":\"18e05f0a64043758-b166-132\"}", + "complete_ns": 355625, + "event": "client_completed", + "first_byte_ns": 344292, + "header_ns": 343334, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a64043758-b166-132" + }, + "probe": { + "adapter": "spin", + "case": "sdk-stream-ready", + "pause_ns": 458, + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sdk_native": true, + "token": "18e05f0a64043758-b166-132" + } + }, + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "sdk-stream-timer-only", + "event": "response-stream-control", + "observation": { + "client": { + "body": " {\"adapter\":\"spin\",\"case\":\"sdk-stream-timer-only\",\"pause_ns\":103102167,\"qualification\":false,\"revision\":\"0250edc9e3bffc797304b539eb69edf5be7a8bc3\",\"sdk_native\":true,\"token\":\"18e05f0a64128380-b166-133\"}", + "complete_ns": 103412417, + "event": "client_completed", + "first_byte_ns": 224792, + "header_ns": 224042, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Tue, 20 Oct 2026 22:59:09 GMT"] + ], + "status": 200, + "token": "18e05f0a64128380-b166-133" + }, + "probe": { + "adapter": "spin", + "case": "sdk-stream-timer-only", + "pause_ns": 103102167, + "qualification": false, + "revision": "0250edc9e3bffc797304b539eb69edf5be7a8bc3", + "sdk_native": true, + "token": "18e05f0a64128380-b166-133" + } + }, + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "case": "sdk-stream-ready", + "coordinator_ok": true, + "diagnostic_source": "fixture_stderr.txt", + "event": "sdk-stream-coordinator-terminal", + "qualification": false, + "token": "18e05f0a64043758-b166-132" + }, + { + "adapter": "spin", + "case": "sdk-stream-timer-only", + "coordinator_ok": true, + "diagnostic_source": "fixture_stderr.txt", + "event": "sdk-stream-coordinator-terminal", + "qualification": false, + "token": "18e05f0a64128380-b166-133" + }, + { + "adapter": "spin", + "case": "sdk-stream-ready", + "coordinator_ok": true, + "event": "sdk-stream-coordinator-terminal", + "qualification": false, + "token": "18e05f0a64043758-b166-132", + "variant": "spin-outbound-progress" + }, + { + "adapter": "spin", + "case": "sdk-stream-timer-only", + "coordinator_ok": true, + "event": "sdk-stream-coordinator-terminal", + "qualification": false, + "token": "18e05f0a64128380-b166-133", + "variant": "spin-outbound-progress" + } + ] + } + ], + "diagnostic_history": { + "before_test_parameter_rename": { + "exit_code": 0, + "raw_events": { + "path": "/private/tmp/edgezero-relay-all-20261020/events.jsonl", + "bytes": 585711, + "sha256": "04ca7afaacd46038a4c5e41d56d6af4d53c5d32a51fcec4d558ab63da2c19103" + }, + "source_sha256": { + "Cargo.lock": "1afb33fe3067419ad5a2a948b330ac1df123d53214e49dc4005a92f7bc69e043", + "Cargo.toml": "ee73d1318af74a997ecc12bac0f80cd7eefbe8ead34a35b1432c8d6f49fc42e1", + "crates/edgezero-adapter-axum/src/outbound.rs": "cc6e1f50eb1d85c42100f73ad850109433d56fe43e249f3cdb7203553f0a23a2", + "crates/edgezero-adapter-axum/src/request.rs": "0c61c652c7e3455d35b9802b26b6118b68c678bdfe01cc1215c7a9ec931cddef", + "crates/edgezero-adapter-axum/src/response.rs": "f35df64dcb1c90a19720739b10dd72903bc73d92a43734d80d1f38cdab29ac51", + "crates/edgezero-adapter-cloudflare/src/outbound.rs": "25dbde10a5502c1644a791ce188d39531689d0f1c138594ba17cc23c5854072c", + "crates/edgezero-adapter-cloudflare/src/request.rs": "a755088e4e65e5af813335283ec3095c35b5982d1e9057ce58dd748aa03e6200", + "crates/edgezero-adapter-cloudflare/src/response.rs": "15a5b643713d3fdd2bd965fd00d9b606ca5cddf754678c3b0a0b9a65bba21adb", + "crates/edgezero-adapter-fastly/src/outbound.rs": "4284cc2af486e2bed63beb6434332965832dbc15a6fc40a0b538bcc54b5a3f31", + "crates/edgezero-adapter-fastly/src/request.rs": "c49b75fd52abacc1b2fb012cc357b3a0ce11f9b6a76397bef0f40114ab79d684", + "crates/edgezero-adapter-fastly/src/response.rs": "5015be8eb6b0f55f2d7e79189a88233fc2142bb06d4820b586a2529fae9703ea", + "crates/edgezero-adapter-spin/src/outbound.rs": "be9f5ff721bcc47ed530deab39dbdff4058b1ec4e97865c2b8bbb65ae08c1ad5", + "crates/edgezero-adapter-spin/src/request.rs": "962e413a2009c82259405628b39976e4040dde6d1425ff5adfaf6ab0865b9d67", + "crates/edgezero-adapter-spin/src/response.rs": "03e8b98de3e4854810b4bdabba7783d3a66cacb5e91402e941609d7dfe567075", + "crates/edgezero-core/src/body.rs": "50a8bc65fcbc7df3d9afd3ddc520889f42da46af1c6419e23c19625c33d5bf9e", + "crates/edgezero-core/src/compression.rs": "4acccc0c01009894ebe4f77f57ce8ce501d3f3f2ccb565c6b3d3871d826037d4", + "crates/edgezero-core/src/lib.rs": "0de5ff9b503fbf86a27b0f0abce7dc47f9f85c7d0d596e0fce4b6589a96d133d", + "crates/edgezero-core/src/outbound.rs": "f1577756a5375dbd9a780b611d50bc56d629b8a50b8c13cc40864d0f1ef5ba8d", + "crates/edgezero-core/src/outbound/exchange.rs": "f8ca9867b572b2e40e0c08ae55fefefaefbf82355c83aa8b28f74870d6e2a879", + "crates/edgezero-core/src/outbound/progress.rs": "90eec32101d09c4ee40409b1fae6f06b97c83d1af8a03baf39d07820da9a0a08", + "crates/edgezero-core/src/time.rs": "bcd7047587f656b6547c860986f0c8f6459a5554850cbac4c1bfd9c480ec7435", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "tests/fixtures/reusable-app/Cargo.toml": "7665200757e4e565d11fa7eb036215aa583d21fd543bca39fdbdce470fee11ae", + "tests/fixtures/reusable-app/crates/fixture-axum/Cargo.toml": "1c1421c7497a7aff305d31ecd4c3ee323eb646acd4d56f23cab765ab6a5e377c", + "tests/fixtures/reusable-app/crates/fixture-axum/src/progress.rs": "0c41390031d857f0589667b88bd27fc8e185b483cedc57ec2ee73c99d36a9e4a", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/Cargo.toml": "96501fc975013ba68411cd6e0f77b6b0fe505dfe4ed8cef16620ef19b4c9784d", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs": "606245131f0c594154ca0a2eb63e5341999325bfb2de55887ab3264fa4b89559", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs": "305f70039af59501639916bd8cfb8db696613c73af670e953dd329a3eed14498", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml": "6c2a097879b98f4d4af3e2fea139215abc9fa0bfdc058500f28fd9f7d757aab3", + "tests/fixtures/reusable-app/crates/fixture-core/Cargo.toml": "f9c68c281c9b57269ee8065f46e9e1c3fcc9c58a562259e480f2125877a8f6c8", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs": "321cda3626dbe5e68da4ea1e9b5f71f531c0630f2763196fff8b0e7b64f301db", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs": "73b9f6a2822c45ddbecb09c876b035800017e29c16117edf5fbb8040f177bb73", + "tests/fixtures/reusable-app/crates/fixture-fastly/Cargo.toml": "e82ea4713f86db533796367cee0c3f91191f33e7e1f0510d0cf1bdafd509a7e1", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs": "0b4a81e3678459801b9f5a9379de70160452317929936d1b6c85393c1249c704", + "tests/fixtures/reusable-app/crates/fixture-harness/Cargo.toml": "93580bf7575878ce3cb5d82ef3e7aea6d5c26baae6f467f469339858992fd141", + "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs": "22f5c8ff413357b5684910249c635a4993d0cb912b3cc584c8f14cc2258efb18", + "tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs": "1344e6fb8e4e22426ec5ceaacba210c034121970d8b49ece79e6ef3a871776b8", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs": "9cb580666fa2882bef9a87648469fa8170366526463163aabe9d959bda04d2af", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs": "f07fd19c3937780e5a7973513ccd29dd4708c852f62f4b2dd3e14929181605f9", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/progress.rs": "617062572517920d18fb7c8e938d690b38bc0845a036235f217659b63c3a4ab4", + "tests/fixtures/reusable-app/crates/fixture-spin/Cargo.toml": "7256c3384d5e3c675b8b9e7026d10630bc5fec9c3aaf3900bc740fcb4e1526b4", + "tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs": "0a20af88ab9a94fe8ef806b15e38994c7790ec0f9040add4c3dec4ead9f022db" + }, + "note": "all runtime cases passed before the legacy guard caught the test-double cutoff parameter name; final record uses the corrected source and a complete rerun" + } + } +} diff --git a/docs/audits/evidence/2026-10-21-declared-kv.json b/docs/audits/evidence/2026-10-21-declared-kv.json new file mode 100644 index 000000000..8413d7a20 --- /dev/null +++ b/docs/audits/evidence/2026-10-21-declared-kv.json @@ -0,0 +1,1566 @@ +{ + "schema": 1, + "recorded_date": "2026-10-21", + "base_revision": "0ee562529aaac450f313bc0a4dc5c18a214539ff", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "status": "local-declared-kv-cases-pass-independent-spin-overlap-unverified", + "scope": "F8 / EZ-20 local implementation and adopted Cloudflare/Spin behavior; not release cutover certification", + "adoption": { + "decision": "Include all three serverless adapters", + "source": "Requester reply in this conversation on 2026-10-21", + "scope": "Implementation adoption only; formal release approval fields remain pending." + }, + "source_snapshot_scope": "Selected final checkout inputs, not a frozen candidate or full build-input attestation. Final C uses final runtime and harness logic; later production edits clarify Rustdoc only, with source-path tests rerun. No outbound transport/API, SDK/decoder pin or capability change.", + "frozen_api_plan": { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md", + "bytes": 65279, + "sha256": "c52b4aea6b8d2793f30784e88016a3676088d344c0a5607774a5d9f1a7ffae80" + }, + "approved_interfaces_sha256": "c623e7fc5b189e2d9122756f29297a92031a9936b13c6cc475484d4e028a4a7a", + "requirements": { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md", + "bytes": 390165, + "sha256": "86691fe7f0dfae0e489678f01eeb4385c9acdc238778801d700c7ef8585f2fd6", + "baseline_sha256": "5d9833ebb13bf191377eeec6f9ada89d236044546701d01478c7f12f65b175ec", + "acceptance_criteria": 255, + "acceptance_criteria_identical": true, + "reviewed_legs_unchanged": 511 + }, + "source_files": [ + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/crates/edgezero-adapter-cloudflare/src/key_value_store.rs", + "bytes": 10745, + "sha256": "1ca3d1a60ac6dfdd32e0016140929d489b82b85318d609c59cf15d4ff6101407" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/crates/edgezero-adapter-cloudflare/src/request.rs", + "bytes": 41781, + "sha256": "0d6b5e888c0b352f5372e06d5fa04c06ac21b2d3177f4d23313a0058889a267b" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/crates/edgezero-adapter-fastly/src/key_value_store.rs", + "bytes": 12677, + "sha256": "dc55c32c9d7d143275dd0e025fb96b5020a4a30bfd4547f311406102993b58e4" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/crates/edgezero-adapter-fastly/src/request.rs", + "bytes": 46822, + "sha256": "3bdcabc9c5f18e9cd7d76695f0aacea81e24f135b51b0bc2ada7c67b13eb1dcf" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/crates/edgezero-adapter-spin/src/key_value_store.rs", + "bytes": 7543, + "sha256": "19941a845fdd46b7221c7ce89f8f86c6d73e17fe96897e4ba4c995aceb9ca0ed" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/crates/edgezero-adapter-spin/src/request.rs", + "bytes": 34214, + "sha256": "1ba2cff36a9a32ffcf17686fcab1fe2baf23537e5264eb049dc2b8f4f7b943fa" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/crates/edgezero-core/src/key_value_store.rs", + "bytes": 62623, + "sha256": "37edf3f5491193ee120aafeab74d31afc419a93dfe9e959aec5383246b8717c4" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs", + "bytes": 2794, + "sha256": "ed27e95ad3783767832a974e152c762628fef6316c4944a05a5a831cb5702161" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/crates/fixture-core/src/lib.rs", + "bytes": 19614, + "sha256": "576eb54ed6c4213dbc027a001929177de13ef42899b21ba182803a1784d37ef3" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs", + "bytes": 19398, + "sha256": "68445dccf097c92906fbe409477c5d7ae616137f3f1d2f39c6a4ee3751ab67d2" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs", + "bytes": 61859, + "sha256": "89f5275cb58fa639b845f7a65d40cb868f0455934f4cfbf899e7238540d9c66d" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/crates/fixture-spin/src/lib.rs", + "bytes": 3113, + "sha256": "ebcea9b59e905a2bc0670f561463b5c74d5512795a4e63674db452db6f2c8668" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/outbound-cutover-cases.json", + "bytes": 754487, + "sha256": "8ecbc115bbb5d61ad3273386932fe8087ca2d15adcc48c1a922176762eca4333" + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/crates/edgezero-adapter-spin/tests/key_value_store_lazy.rs", + "bytes": 6017, + "sha256": "7db17ee83225da25eed2e7e8209011d1c013a9330cd8f17338e14380636fda33" + } + ], + "locks": [ + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/Cargo.lock", + "bytes": 96111, + "sha256": "7f65719e408c31d4e6c32f2e168b00ace4f73de412ca6b5469b831bd151c04ac", + "unchanged_from_base": true + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/Cargo.lock", + "bytes": 84127, + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "unchanged_from_base": true + }, + { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/examples/app-demo/Cargo.lock", + "bytes": 87596, + "sha256": "d1dad662166f73b278b4ba0c33a017f3d30f4749f6e1a6a9a43fc1287c31f423", + "unchanged_from_base": true + } + ], + "sdk_versions": { + "fastly": "0.13.1", + "worker": "0.8.5", + "spin_sdk": "7.0.0" + }, + "runtime_observations": [ + { + "adapter": "fastly", + "event": "runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "expected": "0.21.0", + "version": "viceroy 0.21.0" + }, + { + "adapter": "cloudflare", + "event": "runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "version": "4.131.0" + }, + { + "adapter": "spin", + "event": "runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + } + ], + "tool_identities": [ + { + "path": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "bytes": 53317104, + "sha256": "751ff2c1a83063b109bc212b95be5f52e5ab6284b9027fa03aa229087eba3f30" + }, + { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "bytes": 3088, + "sha256": "780661a508810f3b65786895b1ca9aacbc4f55d329ae6b8c1e49ec8433569f77" + }, + { + "path": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "bytes": 116793168, + "sha256": "0efe71a81495a75e81624186d457afa5398117e685567ba2cf90bd01a269128a" + }, + { + "path": "/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen", + "bytes": 6938208, + "sha256": "b86e33b3c64362c44d8807ed35797e6b8220dd0232b6b19743caf7c2c68a2284" + }, + { + "path": "/private/tmp/mocktioneer-pr275-20261021.YgmPJO/tools/bin/wasm-bindgen-test-runner", + "bytes": 10179856, + "sha256": "95966cd7a78035f433b367303e74df4784db2b7a50f9bcfa5b421535fe33d1b3" + }, + { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64/bin/workerd", + "bytes": 156374824, + "sha256": "eb132e3e489b47d804cc70b510a6da8f82dabec4748cdcdaba32a1a1c5c2e285" + } + ], + "compiler": "rustc 1.98.1 (48a229cea 2026-09-01)", + "cloudflare": { + "compatibility_date": "2026-04-22", + "compatibility_flags": ["enable_request_signal", "enable_weak_ref"], + "workerd_package_version": "1.20260910.1", + "workerd_identity_basis": "Wrangler bundled workerd default executable, not independent per-process attestation." + }, + "final_run": { + "environment": { + "SSL_CERT_FILE": "/etc/ssl/cert.pem", + "WASM_BINDGEN_BIN": "/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen", + "WORKER_BUILD_BIN": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build", + "ESBUILD_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild", + "WASM_OPT_BIN": "/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt", + "WRANGLER_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler", + "WRANGLER_LOG_PATH": "/private/tmp/edgezero-pr275-wrangler-logs", + "WRANGLER_SEND_METRICS": "false", + "CARGO_NET_OFFLINE": "true" + }, + "command": "scripts/smoke_test_reusable_app.sh --adapter all --suite smoke --output /private/tmp/edgezero-f8-all-final-c", + "exit_code": 2, + "events": { + "path": "/private/tmp/edgezero-f8-all-final-c/events.jsonl", + "bytes": 1785349, + "sha256": "88b9c651ab9f5b3584abc0aa2746c8bc4fe33162117746c0239bd963540395b4" + }, + "summary": { + "path": "/private/tmp/edgezero-f8-all-final-c/summary.json", + "bytes": 469058, + "sha256": "0d5303b0fc309b26efbd4e926bba24dc5987a6b52a1e7de229f64cbdcdf721f1" + }, + "f8_request_cases": [ + { + "event": "declared-kv-case", + "adapter": "fastly", + "path": "/declared-kv/free", + "token": "18e0a73d57e3c918-13ab6-82", + "status": "pass", + "http_status": 200, + "warnings": 0, + "instance": "18e0a73d57522bc0-13ab6-81", + "body_sha256": "114095efdb9546df42494cd1abbe7b475312fac70784b82a3dc8bddc43702949", + "body_bytes": 283, + "request_start": { + "correlation": "00000000000000000000000000000006", + "cpu_ms": 5, + "event": "request_start", + "heap_mib": 2, + "instance": "18e0a73d57522bc0-13ab6-81", + "native_id": true, + "ordinal": 2, + "repetition": 0, + "service_id": "0000000000000000000000", + "token": "18e0a73d57e3c918-13ab6-82", + "variant": "c" + }, + "egress_terminal": { + "body_kind": "Application", + "bytes_written": 283, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a73d57522bc0-13ab6-81", + "outcome": "Completed", + "repetition": 0, + "token": "18e0a73d57e3c918-13ab6-82", + "variant": "c" + } + }, + { + "event": "declared-kv-case", + "adapter": "fastly", + "path": "/declared-kv/missing/read", + "token": "18e0a73d57f30770-13ab6-83", + "status": "pass", + "http_status": 500, + "warnings": 1, + "instance": "18e0a73d57522bc0-13ab6-81", + "body_sha256": "f7cd249a93d558f2efcfd085d1f107068820182465b1d7154d1eb2c4463aec87", + "body_bytes": 76, + "request_start": { + "correlation": "00000000000000000000000000000007", + "cpu_ms": 5, + "event": "request_start", + "heap_mib": 2, + "instance": "18e0a73d57522bc0-13ab6-81", + "native_id": true, + "ordinal": 3, + "repetition": 0, + "service_id": "0000000000000000000000", + "token": "18e0a73d57f30770-13ab6-83", + "variant": "c" + }, + "egress_terminal": { + "body_kind": "Application", + "bytes_written": 76, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a73d57522bc0-13ab6-81", + "outcome": "Completed", + "repetition": 0, + "token": "18e0a73d57f30770-13ab6-83", + "variant": "c" + } + }, + { + "event": "declared-kv-case", + "adapter": "fastly", + "path": "/declared-kv/missing/read", + "token": "18e0a73d580c4078-13ab6-84", + "status": "pass", + "http_status": 500, + "warnings": 1, + "instance": "18e0a73d57522bc0-13ab6-81", + "body_sha256": "f7cd249a93d558f2efcfd085d1f107068820182465b1d7154d1eb2c4463aec87", + "body_bytes": 76, + "request_start": { + "correlation": "00000000000000000000000000000008", + "cpu_ms": 6, + "event": "request_start", + "heap_mib": 2, + "instance": "18e0a73d57522bc0-13ab6-81", + "native_id": true, + "ordinal": 4, + "repetition": 0, + "service_id": "0000000000000000000000", + "token": "18e0a73d580c4078-13ab6-84", + "variant": "c" + }, + "egress_terminal": { + "body_kind": "Application", + "bytes_written": 76, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a73d57522bc0-13ab6-81", + "outcome": "Completed", + "repetition": 0, + "token": "18e0a73d580c4078-13ab6-84", + "variant": "c" + } + }, + { + "event": "declared-kv-case", + "adapter": "fastly", + "path": "/declared-kv/free", + "token": "18e0a73d581ece78-13ab6-85", + "status": "pass", + "http_status": 200, + "warnings": 1, + "instance": "18e0a73d57522bc0-13ab6-81", + "body_sha256": "cf6f203005d6190a40a4f1257ac972faa85666a964d977684a5c9de1fdf82157", + "body_bytes": 283, + "request_start": { + "correlation": "00000000000000000000000000000009", + "cpu_ms": 6, + "event": "request_start", + "heap_mib": 2, + "instance": "18e0a73d57522bc0-13ab6-81", + "native_id": true, + "ordinal": 5, + "repetition": 0, + "service_id": "0000000000000000000000", + "token": "18e0a73d581ece78-13ab6-85", + "variant": "c" + }, + "egress_terminal": { + "body_kind": "Application", + "bytes_written": 283, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a73d57522bc0-13ab6-81", + "outcome": "Completed", + "repetition": 0, + "token": "18e0a73d581ece78-13ab6-85", + "variant": "c" + } + }, + { + "event": "declared-kv-linked", + "adapter": "fastly", + "operation": "write", + "token": "18e0a73d583465d0-13ab6-86", + "status": "pass", + "http_status": 200, + "instance": "18e0a73d57522bc0-13ab6-81", + "body_sha256": "4931e42d4d10b497efe95243d1044f28a089462424ff5c01deaa85f257a0b4d4", + "body_bytes": 250, + "request_start": { + "correlation": "0000000000000000000000000000000a", + "cpu_ms": 7, + "event": "request_start", + "heap_mib": 2, + "instance": "18e0a73d57522bc0-13ab6-81", + "native_id": true, + "ordinal": 6, + "repetition": 0, + "service_id": "0000000000000000000000", + "token": "18e0a73d583465d0-13ab6-86", + "variant": "c" + }, + "egress_terminal": { + "body_kind": "Application", + "bytes_written": 250, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a73d57522bc0-13ab6-81", + "outcome": "Completed", + "repetition": 0, + "token": "18e0a73d583465d0-13ab6-86", + "variant": "c" + } + }, + { + "event": "declared-kv-linked", + "adapter": "fastly", + "operation": "read", + "token": "18e0a73d5854ea08-13ab6-87", + "status": "pass", + "http_status": 200, + "instance": "18e0a73d5854ea08-13ab6-87", + "body_sha256": "91fa2b6c6292d81ea6cb05b7f2f23f6d8254191c6e2e40a4d798000819282b47", + "body_bytes": 251, + "request_start": { + "correlation": "0000000000000000000000000000000b", + "cpu_ms": 0, + "event": "request_start", + "heap_mib": 2, + "instance": "18e0a73d5854ea08-13ab6-87", + "native_id": true, + "ordinal": 1, + "repetition": 0, + "service_id": "0000000000000000000000", + "token": "18e0a73d5854ea08-13ab6-87", + "variant": "c" + }, + "egress_terminal": { + "body_kind": "Application", + "bytes_written": 251, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a73d5854ea08-13ab6-87", + "outcome": "Completed", + "repetition": 0, + "token": "18e0a73d5854ea08-13ab6-87", + "variant": "c" + } + }, + { + "event": "declared-kv-linked", + "adapter": "fastly", + "operation": "list", + "token": "18e0a73d586b1da0-13ab6-88", + "status": "pass", + "http_status": 200, + "instance": "18e0a73d5854ea08-13ab6-87", + "body_sha256": "209a3acf86f251dbd21098dda091b7dd83189918c49d98c1ae7a8bf6ecfbdc41", + "body_bytes": 251, + "request_start": { + "correlation": "0000000000000000000000000000000c", + "cpu_ms": 0, + "event": "request_start", + "heap_mib": 2, + "instance": "18e0a73d5854ea08-13ab6-87", + "native_id": true, + "ordinal": 2, + "repetition": 0, + "service_id": "0000000000000000000000", + "token": "18e0a73d586b1da0-13ab6-88", + "variant": "c" + }, + "egress_terminal": { + "body_kind": "Application", + "bytes_written": 251, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a73d5854ea08-13ab6-87", + "outcome": "Completed", + "repetition": 0, + "token": "18e0a73d586b1da0-13ab6-88", + "variant": "c" + } + }, + { + "event": "declared-kv-linked", + "adapter": "fastly", + "operation": "delete", + "token": "18e0a73d588df398-13ab6-89", + "status": "pass", + "http_status": 200, + "instance": "18e0a73d5854ea08-13ab6-87", + "body_sha256": "52deb0ecf9577e6fa4549821ed3927e6a86ae3413a9b913923836d97ee87eb8a", + "body_bytes": 250, + "request_start": { + "correlation": "0000000000000000000000000000000d", + "cpu_ms": 1, + "event": "request_start", + "heap_mib": 2, + "instance": "18e0a73d5854ea08-13ab6-87", + "native_id": true, + "ordinal": 3, + "repetition": 0, + "service_id": "0000000000000000000000", + "token": "18e0a73d588df398-13ab6-89", + "variant": "c" + }, + "egress_terminal": { + "body_kind": "Application", + "bytes_written": 250, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a73d5854ea08-13ab6-87", + "outcome": "Completed", + "repetition": 0, + "token": "18e0a73d588df398-13ab6-89", + "variant": "c" + } + }, + { + "event": "declared-kv-case", + "adapter": "cloudflare", + "path": "/declared-kv/free", + "token": "18e0a743d811f3c0-13ab6-132", + "status": "pass", + "http_status": 200, + "warnings": 0, + "instance": "18e0a743d811f3c0-13ab6-132", + "body_sha256": "cdbb54dbb500fb9b7c818c88fe00e2a4198aed55a4865543b1738a568c8e56f8", + "body_bytes": 280, + "request_start": { + "adapter": "cloudflare", + "correlation": "18e0a743d811f3c0-13ab6-132", + "event": "request_start", + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "native_id": false, + "ordinal": 1, + "token": "18e0a743d811f3c0-13ab6-132" + }, + "egress_terminal": { + "adapter": "cloudflare", + "body_kind": "Application", + "bytes_written": 280, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a743d811f3c0-13ab6-132" + } + }, + { + "event": "declared-kv-case", + "adapter": "cloudflare", + "path": "/declared-kv/missing/read", + "token": "18e0a743e2ed10e0-13ab6-133", + "status": "pass", + "http_status": 500, + "warnings": 1, + "instance": "18e0a743d811f3c0-13ab6-132", + "body_sha256": "f7cd249a93d558f2efcfd085d1f107068820182465b1d7154d1eb2c4463aec87", + "body_bytes": 76, + "request_start": { + "adapter": "cloudflare", + "correlation": "18e0a743e2ed10e0-13ab6-133", + "event": "request_start", + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "native_id": false, + "ordinal": 2, + "token": "18e0a743e2ed10e0-13ab6-133" + }, + "egress_terminal": { + "adapter": "cloudflare", + "body_kind": "Application", + "bytes_written": 76, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a743e2ed10e0-13ab6-133" + } + }, + { + "event": "declared-kv-case", + "adapter": "cloudflare", + "path": "/declared-kv/missing/read", + "token": "18e0a743e68dea08-13ab6-134", + "status": "pass", + "http_status": 500, + "warnings": 1, + "instance": "18e0a743d811f3c0-13ab6-132", + "body_sha256": "f7cd249a93d558f2efcfd085d1f107068820182465b1d7154d1eb2c4463aec87", + "body_bytes": 76, + "request_start": { + "adapter": "cloudflare", + "correlation": "18e0a743e68dea08-13ab6-134", + "event": "request_start", + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "native_id": false, + "ordinal": 3, + "token": "18e0a743e68dea08-13ab6-134" + }, + "egress_terminal": { + "adapter": "cloudflare", + "body_kind": "Application", + "bytes_written": 76, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a743e68dea08-13ab6-134" + } + }, + { + "event": "declared-kv-case", + "adapter": "cloudflare", + "path": "/declared-kv/free", + "token": "18e0a743e6d33450-13ab6-135", + "status": "pass", + "http_status": 200, + "warnings": 1, + "instance": "18e0a743d811f3c0-13ab6-132", + "body_sha256": "c3127e0969fc3c595a2d2b3bf1b95204b72c846cb0f4e657817f5c9dd63bf3ca", + "body_bytes": 280, + "request_start": { + "adapter": "cloudflare", + "correlation": "18e0a743e6d33450-13ab6-135", + "event": "request_start", + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "native_id": false, + "ordinal": 4, + "token": "18e0a743e6d33450-13ab6-135" + }, + "egress_terminal": { + "adapter": "cloudflare", + "body_kind": "Application", + "bytes_written": 280, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a743e6d33450-13ab6-135" + } + }, + { + "event": "declared-kv-linked", + "adapter": "cloudflare", + "operation": "write", + "token": "18e0a743ea513b68-13ab6-136", + "status": "pass", + "http_status": 200, + "instance": "18e0a743d811f3c0-13ab6-132", + "body_sha256": "d2a2d355646b99dff6d620e5da4be67fe69372290a955c1f7ca483a2b91feb77", + "body_bytes": 247, + "request_start": { + "adapter": "cloudflare", + "correlation": "18e0a743ea513b68-13ab6-136", + "event": "request_start", + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "native_id": false, + "ordinal": 5, + "token": "18e0a743ea513b68-13ab6-136" + }, + "egress_terminal": { + "adapter": "cloudflare", + "body_kind": "Application", + "bytes_written": 247, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a743ea513b68-13ab6-136" + } + }, + { + "event": "declared-kv-linked", + "adapter": "cloudflare", + "operation": "read", + "token": "18e0a743edba89d0-13ab6-137", + "status": "pass", + "http_status": 200, + "instance": "18e0a743d811f3c0-13ab6-132", + "body_sha256": "0b7017140152d50a78ebd4539c4fabea534676fafad8822fac66a0e54636a9a1", + "body_bytes": 248, + "request_start": { + "adapter": "cloudflare", + "correlation": "18e0a743edba89d0-13ab6-137", + "event": "request_start", + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "native_id": false, + "ordinal": 6, + "token": "18e0a743edba89d0-13ab6-137" + }, + "egress_terminal": { + "adapter": "cloudflare", + "body_kind": "Application", + "bytes_written": 248, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a743edba89d0-13ab6-137" + } + }, + { + "event": "declared-kv-linked", + "adapter": "cloudflare", + "operation": "list", + "token": "18e0a743f152bc20-13ab6-138", + "status": "pass", + "http_status": 200, + "instance": "18e0a743d811f3c0-13ab6-132", + "body_sha256": "d16763e60064382e839ccd5b0fea7fbc639ca2420d7db6407d79fadd482b9ceb", + "body_bytes": 248, + "request_start": { + "adapter": "cloudflare", + "correlation": "18e0a743f152bc20-13ab6-138", + "event": "request_start", + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "native_id": false, + "ordinal": 7, + "token": "18e0a743f152bc20-13ab6-138" + }, + "egress_terminal": { + "adapter": "cloudflare", + "body_kind": "Application", + "bytes_written": 248, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a743f152bc20-13ab6-138" + } + }, + { + "event": "declared-kv-linked", + "adapter": "cloudflare", + "operation": "delete", + "token": "18e0a743f4c72260-13ab6-139", + "status": "pass", + "http_status": 200, + "instance": "18e0a743d811f3c0-13ab6-132", + "body_sha256": "a9e53031b4cb61678f190b24dbefbacf65c1ce8f3c0431cb31dada65f5704c20", + "body_bytes": 247, + "request_start": { + "adapter": "cloudflare", + "correlation": "18e0a743f4c72260-13ab6-139", + "event": "request_start", + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "native_id": false, + "ordinal": 8, + "token": "18e0a743f4c72260-13ab6-139" + }, + "egress_terminal": { + "adapter": "cloudflare", + "body_kind": "Application", + "bytes_written": 247, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a743d811f3c0-13ab6-132", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a743f4c72260-13ab6-139" + } + }, + { + "event": "declared-kv-case", + "adapter": "spin", + "path": "/declared-kv/free", + "token": "18e0a746d4d0e290-13ab6-1be", + "status": "pass", + "http_status": 200, + "warnings": 0, + "instance": "18e0a746d4d0e290-13ab6-1be", + "body_sha256": "2cdbcdea5274672c290584568be8efd05539bfd4d23b980055bbc819a8303151", + "body_bytes": 280, + "request_start": { + "adapter": "spin", + "correlation": "18e0a746d4d0e290-13ab6-1be", + "event": "request_start", + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "native_id": false, + "ordinal": 1, + "token": "18e0a746d4d0e290-13ab6-1be" + }, + "egress_terminal": { + "adapter": "spin", + "body_kind": "Application", + "bytes_written": 280, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a746d4d0e290-13ab6-1be" + } + }, + { + "event": "declared-kv-case", + "adapter": "spin", + "path": "/declared-kv/missing/read", + "token": "18e0a746d4f5f2d8-13ab6-1bf", + "status": "pass", + "http_status": 500, + "warnings": 1, + "instance": "18e0a746d4d0e290-13ab6-1be", + "body_sha256": "f7cd249a93d558f2efcfd085d1f107068820182465b1d7154d1eb2c4463aec87", + "body_bytes": 76, + "request_start": { + "adapter": "spin", + "correlation": "18e0a746d4f5f2d8-13ab6-1bf", + "event": "request_start", + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "native_id": false, + "ordinal": 2, + "token": "18e0a746d4f5f2d8-13ab6-1bf" + }, + "egress_terminal": { + "adapter": "spin", + "body_kind": "Application", + "bytes_written": 76, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a746d4f5f2d8-13ab6-1bf" + } + }, + { + "event": "declared-kv-case", + "adapter": "spin", + "path": "/declared-kv/missing/read", + "token": "18e0a746d5071978-13ab6-1c0", + "status": "pass", + "http_status": 500, + "warnings": 1, + "instance": "18e0a746d4d0e290-13ab6-1be", + "body_sha256": "f7cd249a93d558f2efcfd085d1f107068820182465b1d7154d1eb2c4463aec87", + "body_bytes": 76, + "request_start": { + "adapter": "spin", + "correlation": "18e0a746d5071978-13ab6-1c0", + "event": "request_start", + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "native_id": false, + "ordinal": 3, + "token": "18e0a746d5071978-13ab6-1c0" + }, + "egress_terminal": { + "adapter": "spin", + "body_kind": "Application", + "bytes_written": 76, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a746d5071978-13ab6-1c0" + } + }, + { + "event": "declared-kv-case", + "adapter": "spin", + "path": "/declared-kv/free", + "token": "18e0a746d5178498-13ab6-1c1", + "status": "pass", + "http_status": 200, + "warnings": 1, + "instance": "18e0a746d4d0e290-13ab6-1be", + "body_sha256": "8f5ff82549486530c1f26565e746b4dba0d4b65f25858988172bd7311fa48885", + "body_bytes": 280, + "request_start": { + "adapter": "spin", + "correlation": "18e0a746d5178498-13ab6-1c1", + "event": "request_start", + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "native_id": false, + "ordinal": 4, + "token": "18e0a746d5178498-13ab6-1c1" + }, + "egress_terminal": { + "adapter": "spin", + "body_kind": "Application", + "bytes_written": 280, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a746d5178498-13ab6-1c1" + } + }, + { + "event": "declared-kv-linked", + "adapter": "spin", + "operation": "write", + "token": "18e0a746d526c6d8-13ab6-1c2", + "status": "pass", + "http_status": 200, + "instance": "18e0a746d4d0e290-13ab6-1be", + "body_sha256": "7b0a6b006fc4dedec483a5ff809774104d7250dac6ad7b195f4142e48b2882e4", + "body_bytes": 247, + "request_start": { + "adapter": "spin", + "correlation": "18e0a746d526c6d8-13ab6-1c2", + "event": "request_start", + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "native_id": false, + "ordinal": 5, + "token": "18e0a746d526c6d8-13ab6-1c2" + }, + "egress_terminal": { + "adapter": "spin", + "body_kind": "Application", + "bytes_written": 247, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a746d526c6d8-13ab6-1c2" + } + }, + { + "event": "declared-kv-linked", + "adapter": "spin", + "operation": "read", + "token": "18e0a746d5396090-13ab6-1c3", + "status": "pass", + "http_status": 200, + "instance": "18e0a746d4d0e290-13ab6-1be", + "body_sha256": "3de25875a8b682c899f60474b078541bbc8b70a15ca466ea2181774fe9e28d6b", + "body_bytes": 248, + "request_start": { + "adapter": "spin", + "correlation": "18e0a746d5396090-13ab6-1c3", + "event": "request_start", + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "native_id": false, + "ordinal": 6, + "token": "18e0a746d5396090-13ab6-1c3" + }, + "egress_terminal": { + "adapter": "spin", + "body_kind": "Application", + "bytes_written": 248, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a746d5396090-13ab6-1c3" + } + }, + { + "event": "declared-kv-linked", + "adapter": "spin", + "operation": "list", + "token": "18e0a746d5493f10-13ab6-1c4", + "status": "pass", + "http_status": 200, + "instance": "18e0a746d4d0e290-13ab6-1be", + "body_sha256": "8a40e3f37434e9ad8f570c769a7e2120050d307deef5cf53fbbf8d2d02f75b79", + "body_bytes": 248, + "request_start": { + "adapter": "spin", + "correlation": "18e0a746d5493f10-13ab6-1c4", + "event": "request_start", + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "native_id": false, + "ordinal": 7, + "token": "18e0a746d5493f10-13ab6-1c4" + }, + "egress_terminal": { + "adapter": "spin", + "body_kind": "Application", + "bytes_written": 248, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a746d5493f10-13ab6-1c4" + } + }, + { + "event": "declared-kv-linked", + "adapter": "spin", + "operation": "delete", + "token": "18e0a746d5943290-13ab6-1c5", + "status": "pass", + "http_status": 200, + "instance": "18e0a746d4d0e290-13ab6-1be", + "body_sha256": "efb619e74f318739c03c3a30eafb0b026762520f289c34e70802cbe493669647", + "body_bytes": 247, + "request_start": { + "adapter": "spin", + "correlation": "18e0a746d5943290-13ab6-1c5", + "event": "request_start", + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "native_id": false, + "ordinal": 8, + "token": "18e0a746d5943290-13ab6-1c5" + }, + "egress_terminal": { + "adapter": "spin", + "body_kind": "Application", + "bytes_written": 247, + "event": "egress_terminal", + "fallback": null, + "instance": "18e0a746d4d0e290-13ab6-1be", + "mode": "retained", + "outcome": "HostHandoff", + "token": "18e0a746d5943290-13ab6-1c5" + } + } + ], + "post_shutdown_warning_checks": [ + { + "adapter": "fastly", + "event": "declared-kv-after-shutdown", + "qualification": false, + "warnings": 1 + }, + { + "adapter": "cloudflare", + "event": "declared-kv-after-shutdown", + "instance": "18e0a743d811f3c0-13ab6-132", + "qualification": false, + "status": "pass", + "warnings": 1 + }, + { + "adapter": "spin", + "event": "declared-kv-after-shutdown", + "instance": "18e0a746d4d0e290-13ab6-1be", + "qualification": false, + "status": "pass", + "warnings": 1 + } + ], + "independent_unverified": [ + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "per-request", + "status": "unverified" + }, + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "retained", + "status": "unverified" + } + ], + "failed_records": [], + "raw_logs": [ + { + "path": "/private/tmp/edgezero-f8-all-final-c/c-0.log", + "bytes": 19446, + "sha256": "d49d1e81cd49d19e35f0c8ad945524a57e8261aec40331f58a5d77ded6ac5e02" + }, + { + "path": "/private/tmp/edgezero-f8-all-final-c/cloudflare-retained.log", + "bytes": 10838, + "sha256": "c19b3387ff0fd73b9c118d56b6b3b46481daa7919d19fa5ac67a62db15fdeb46" + }, + { + "path": "/private/tmp/edgezero-f8-all-final-c/spin-retained.log", + "bytes": 9142, + "sha256": "6186c80ad38575a3e22ff0a70dec6cd99eecbb3a347202eacf29c6d96768695a" + }, + { + "path": "/private/tmp/edgezero-f8-all-final-c/spin-per-request.log", + "bytes": 5625, + "sha256": "6a5d7adc33b2676b614b02595cad33885fc6d9aa606c44b92700746267689f5c" + } + ], + "artifacts": [ + { + "path": "/private/tmp/edgezero-f8-all-final-c/artifacts/fixture-fastly-retained-app.wasm", + "bytes": 3573569, + "sha256": "0a74ce9574e118814335f217e870ea387fb1a4a1ec1492219af88da449dfb298" + }, + { + "path": "/private/tmp/edgezero-f8-all-final-c/artifacts/fixture_spin.wasm", + "bytes": 2150292, + "sha256": "9ee055bc2f014866b7c766bed0b85f640de39dde59ee6616a9cb0f6961ba2b2c" + }, + { + "path": "/private/tmp/edgezero-f8-all-final-c/artifacts/build/index_bg.wasm", + "bytes": 1626105, + "sha256": "8a959911b8e8350f4ccbe7ba40e3ecd9d7a49da20dfd221bddb81460c69fb48e" + }, + { + "path": "/private/tmp/edgezero-f8-all-final-c/artifacts/build/worker/shim.mjs", + "bytes": 164, + "sha256": "ac99177861405a6046923cbacabbd6adfa1853a56d0c679b9f5e09097c2f4728" + }, + { + "path": "/private/tmp/edgezero-f8-all-final-c/wrangler.toml", + "bytes": 447, + "sha256": "6d09e29b802287dbfffcbfa8111f4ed1a2aa4397bc80396a035cd7a66c2b2923" + }, + { + "path": "/private/tmp/edgezero-f8-all-final-c/spin-retained.toml", + "bytes": 567, + "sha256": "07a7991f4d6eec98e4f0521812dd05871037196e331e1566a7e2cec33d931b0a" + }, + { + "path": "/private/tmp/edgezero-f8-all-final-c/fastly.toml", + "bytes": 463, + "sha256": "4444af855e1f5db54da213ddfc6a84ea34aa52f9fd868d047521b0ab78a3fd18" + } + ] + }, + "verification": { + "workspace_core_tests": 794, + "fixture_core_qualification_tests": 17, + "fixture_harness_tests": 56, + "spin_source_harness_tests": 12, + "cloudflare_js_tests": 2, + "fastly_kv_wasi_tests": 3, + "fastly_request_wasi_tests": 12, + "commands": { + "workspace_tests": "cargo test --offline --locked --workspace --all-targets", + "workspace_clippy": "cargo clippy --offline --locked --workspace --all-targets --all-features -- -D warnings", + "feature_check": "cargo check --offline --locked --workspace --all-targets --features \"fastly cloudflare spin\"", + "spin_target": "cargo check --offline --locked -p edgezero-adapter-spin --target wasm32-wasip2 --features spin", + "cf_js": "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=/private/tmp/mocktioneer-pr275-20261021.YgmPJO/tools/bin/wasm-bindgen-test-runner cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --lib key_value_store::tests::", + "fastly_wasi_kv": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run --' cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --lib key_value_store::tests:: -- --nocapture", + "fastly_wasi_requests": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run --' cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --lib request::synthesis_tests:: -- --nocapture" + }, + "logs": [ + { + "path": "/private/tmp/edgezero-f8-workspace-tests-final.log", + "bytes": 175986, + "sha256": "6a37222f82d29ce40c2fed2b749d848d6c88876707801e2a1c79607d58114bea", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-workspace-clippy-final.log", + "bytes": 836, + "sha256": "5106046a86a13e5e095c93df308ba725a6f8e219fd310bc6405dc5629a43987e", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-feature-check-final.log", + "bytes": 836, + "sha256": "0facd333d569f4c2b2fa334c37e0961c94ca74fc18a6267b9db1c85e774fae42", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-spin-check-final.log", + "bytes": 483, + "sha256": "993e72af1e919eeddd8c6d4053122cc9e0a02171a83f0823e184a3d3542286e4", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-fixture-final-d.log", + "bytes": 6196, + "sha256": "7b5a8854a5a110c660cda00f9858c40669d37c54372e57c46953d065cb9c4d8c", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-fixture-core-qualification.log", + "bytes": 2458, + "sha256": "c8fd7e0cca00baa104d5c41c6b285504b2dd2d3da9c400e9f2209ce144939f75", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-fixture-clippy-final-c.log", + "bytes": 553, + "sha256": "8d8f0215ec66f5621f866d9fd4020443b4cc7656a2150dc6c11d557f4691a236", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-spin-unit-final.log", + "bytes": 1514, + "sha256": "4fea553169eaba1f7c31b9278fb4d5b49e945f38cded60535bbeddf74ef59a5e", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-cf-js-final.log", + "bytes": 962, + "sha256": "cafe873cb0685fad97be98a72de97a22937771214902bd66f2bff062eb7b7445", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-cf-final-review-clippy.log", + "bytes": 495, + "sha256": "17d00f02f2fc8f4b5c7c323aa18e781cbadd22004bd1d602358a9dde95fcb280", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-final-review-fastly-kv-wasi-host.log", + "bytes": 956, + "sha256": "655a53dbc09255ff62357fcdad8e1831271056eb0cb699b90a23b2feea75ecf0", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-final-review-fastly-requests-wasi-host.log", + "bytes": 1873, + "sha256": "8823576d430934a331094a805c4075889a69aada3d1835330648db386ba71dcd", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-final-review-spin-wasm-clippy.log", + "bytes": 538, + "sha256": "8dc34de8e64c67104b11a9a12fbaf5632b6e17344d0baffd449e9f43e3a8d5da", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-script-tests-final.log", + "bytes": 2048, + "sha256": "6f8bd4776b81f7b87a63df58d08f4411e4a053fd74724fde4acfc63e0d18974d", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-inventory-check-final.log", + "bytes": 87, + "sha256": "5ff471df1974e9c78508fec0a63e32cc491a25dc31c9510b89b889ee5be21b40", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-docs-format-final.log", + "bytes": 118, + "sha256": "43ba3f26abc1bc66ab5eaa42bcc64ba4c70edea76a4a621144b929d0812736f1", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-docs-lint.log", + "bytes": 40, + "sha256": "81521cc702a8a0674ad78ee506a2ec936217876334be26daf9c738d03779be16", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-docs-build.log", + "bytes": 234, + "sha256": "f71693d704917f0610e36d65b5cad8682a25b5ab88cb597d3eb9ae92de22cb3e", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-core-red.log", + "bytes": 4519, + "sha256": "4d3b5cd9805d3289cf46fb9ecc672e45a4626528cf9b61a5dae3688409189b1a", + "exit_code": 101 + }, + { + "path": "/private/tmp/edgezero-f8-fastly-red-detail.log", + "bytes": 12731, + "sha256": "18580bf01b6d9e0889e324a66dddb622e3d792cb5ebc214c493788480fbea544", + "exit_code": 134 + }, + { + "path": "/private/tmp/edgezero-f8-spin-unit-red.log", + "bytes": 3511, + "sha256": "fbb159e35f16b4701d5e051e3dbac427fff045323855f98cda15af7f28b6e3ac", + "exit_code": 101 + }, + { + "path": "/private/tmp/edgezero-f8-fastly-native-tests.log", + "bytes": 30520, + "sha256": "4722a168cdf6fd1908f72a79256060a05b3d520dbae29e8c3a06c627015c9609", + "exit_code": 101 + }, + { + "path": "/private/tmp/edgezero-f8-final-review-fastly-kv-wasi.log", + "bytes": 1088, + "sha256": "a5878264ea67f8a249d31f722e5f0647752ad337c843725a3820ec981e959392", + "exit_code": 1 + } + ], + "native_fastly_feature_linking": "Native SDK-feature test linking is unsupported by the Fastly host ABI; real Fastly tests execute on WASI/Viceroy. A sandbox keychain failure was followed by the already-configured host runner; no code or SDK workaround." + }, + "retained_attempts": [ + { + "directory": "/private/tmp/edgezero-f8-fastly-smoke-a", + "exit_code": 1, + "scope": "Fixture path-param type mismatch; no runtime verdict.", + "log": { + "path": "/private/tmp/edgezero-f8-fastly-smoke-a.log", + "bytes": 4468, + "sha256": "bf95fec75bde9a4ebcd90b2ee511c619a8917e94f28931f7c4872560ac3a6710" + }, + "events": { + "path": "/private/tmp/edgezero-f8-fastly-smoke-a/events.jsonl", + "bytes": 1097, + "sha256": "9c947795bfa0b54574662f2b787ccbdd4b5174b03fcfb9473dc6825698ab098e" + }, + "diagnostics": [ + { + "adapter": "fastly", + "error": "command cd \"/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app\" && CARGO_TARGET_DIR=\"/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/target\" FIXTURE_BUILD_ROUNDS=\"0\" FIXTURE_MAX_REQUESTS=\"10\" \"/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo\" \"build\" \"--locked\" \"--release\" \"-p\" \"fixture-fastly\" \"--bins\" \"--target\" \"wasm32-wasip1\" failed: exit status: 101", + "status": "fail" + } + ] + }, + { + "directory": "/private/tmp/edgezero-f8-fastly-smoke-b", + "exit_code": 1, + "scope": "Harness demanded HostHandoff; actual Fastly contract reports Completed.", + "log": { + "path": "/private/tmp/edgezero-f8-fastly-smoke-b.log", + "bytes": 1688, + "sha256": "4931bf4382d2a2c8610c9ef8609aa332a4dcab5d381e9b572512c0368366d4ab" + }, + "events": { + "path": "/private/tmp/edgezero-f8-fastly-smoke-b/events.jsonl", + "bytes": 1010340, + "sha256": "c33725b898d0a9621eaf225de482ef562b406e6280b4b951cc8bdfc4187e2d20" + }, + "diagnostics": [ + { + "adapter": "fastly", + "error": "declared KV terminal disposition mismatch", + "repetition": 0, + "status": "fail", + "variant": "c" + } + ] + }, + { + "directory": "/private/tmp/edgezero-f8-fastly-smoke-c", + "exit_code": 1, + "scope": "Overlong missing-operation sequence crossed sandbox identities; retirement cause unproved. Exact four-request acceptance prefix retained instead; all six operations tested in WASI.", + "log": { + "path": "/private/tmp/edgezero-f8-fastly-smoke-c.log", + "bytes": 1534, + "sha256": "766be467723368a8e0658aec8ae472859e1f50375e657cc68495586093e3618e" + }, + "events": { + "path": "/private/tmp/edgezero-f8-fastly-smoke-c/events.jsonl", + "bytes": 1018133, + "sha256": "2422d5e3104fcdfa7210233f6368364a87684d426ad983ec9cb90cdbbdec6e88" + }, + "diagnostics": [ + { + "adapter": "fastly", + "error": "declared KV sandbox turnover: deduplication unverified", + "repetition": 0, + "status": "fail", + "variant": "c" + } + ] + }, + { + "directory": "/private/tmp/edgezero-f8-fastly-smoke-e", + "exit_code": 0, + "scope": "Earlier Fastly-only complete smoke; superseded by final C for final fixture code.", + "log": { + "path": "/private/tmp/edgezero-f8-fastly-smoke-e.log", + "bytes": 1922, + "sha256": "dec98ea5d48bfe3436b0f878cc2f2507887a2de74f6947abd64ab5226fab7520" + }, + "events": { + "path": "/private/tmp/edgezero-f8-fastly-smoke-e/events.jsonl", + "bytes": 1226090, + "sha256": "de1c4faab1c668376a7b70b6b10a39737ca8db98ec2d66fc06e800016d910b47" + }, + "diagnostics": [] + }, + { + "directory": "/private/tmp/edgezero-f8-spin-red", + "exit_code": 1, + "scope": "Unused missing declaration failed provider-free route before handler.", + "log": { + "path": "/private/tmp/edgezero-f8-spin-red.log", + "bytes": 1431, + "sha256": "b8734345c5d9f2a02f50cf0eac4f1b05fc9eb5893f365c3f457e9c1432f19bb4" + }, + "events": { + "path": "/private/tmp/edgezero-f8-spin-red/events.jsonl", + "bytes": 1491, + "sha256": "80f4654180df4c6c7245ca277bd1916d4a245cf2dab79f8dec746616617ff6e8" + }, + "diagnostics": [ + { + "adapter": "spin", + "error": "unexpected response: {\"body\":\"\",\"complete_ns\":42570250,\"event\":\"client_completed\",\"first_byte_ns\":42558084,\"header_ns\":42558084,\"headers\":[[\"connection\",\"close\"],[\"content-length\",\"0\"],[\"date\",\"Wed, 21 Oct 2026 20:43:01 GMT\"]],\"status\":500,\"token\":\"18e0a63101faa830-2dbc-0\"}", + "mode": "per-request", + "phase": "probe", + "repetition": 0, + "status": "fail" + } + ] + }, + { + "directory": "/private/tmp/edgezero-f8-cloudflare-red-b", + "exit_code": 1, + "scope": "Fixture service import typo; no runtime verdict.", + "log": { + "path": "/private/tmp/edgezero-f8-cloudflare-red-b.log", + "bytes": 2178, + "sha256": "b58b88f77b4ace2143f8ec63ce43200c84038b42006986cdbd4c8f3835de53cb" + }, + "events": { + "path": "/private/tmp/edgezero-f8-cloudflare-red-b/events.jsonl", + "bytes": 1561, + "sha256": "8a9b21e519bc2fb54e0c7439a6297bf98a2b15e1b50d6718411bbea0c0534b3d" + }, + "diagnostics": [ + { + "adapter": "cloudflare", + "error": "command cd \"/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/crates/fixture-cloudflare\" && CARGO_TARGET_DIR=\"/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/tests/fixtures/reusable-app/target\" \"/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build\" \"--release\" \".\" \"--\" \"--locked\" failed: exit status: 1", + "status": "fail" + } + ] + }, + { + "directory": "/private/tmp/edgezero-f8-cloudflare-red-c", + "exit_code": 1, + "scope": "Unused missing declaration failed provider-free route before handler.", + "log": { + "path": "/private/tmp/edgezero-f8-cloudflare-red-c.log", + "bytes": 1950, + "sha256": "291d915e037fcc25d1b08e4a32bc975bde4fe64aa951a445593c9c801ae791f5" + }, + "events": { + "path": "/private/tmp/edgezero-f8-cloudflare-red-c/events.jsonl", + "bytes": 1939, + "sha256": "c724c7963d9efc4ae6d2c4ede123a234754f37bcbd145fcccdd3f7ee3bff79f9" + }, + "diagnostics": [ + { + "adapter": "cloudflare", + "error": "unexpected response: {\"body\":\"INTERNAL SERVER ERROR\",\"complete_ns\":138536458,\"event\":\"client_completed\",\"first_byte_ns\":138527083,\"header_ns\":138523166,\"headers\":[[\"Content-Length\",\"21\"]],\"status\":500,\"token\":\"18e0a6587cc34fc8-5df8-0\"}", + "mode": "per-request", + "phase": "probe", + "repetition": 0, + "status": "fail" + } + ] + }, + { + "directory": "/private/tmp/edgezero-f8-cloudflare-green-a", + "exit_code": 1, + "scope": "Explicit fault constructed another measured app and changed build count; fixture now reuses retained app.", + "log": { + "path": "/private/tmp/edgezero-f8-cloudflare-green-a.log", + "bytes": 3222, + "sha256": "2374d5401e311090b44bda4e851e9962ae4581bd5b002f17efbeba12159d71db" + }, + "events": { + "path": "/private/tmp/edgezero-f8-cloudflare-green-a/events.jsonl", + "bytes": 21231, + "sha256": "67d7862e447e88f51be70bd10a762e3358d4c089918f4a8857f41b3caccee4b7" + }, + "diagnostics": [ + { + "adapter": "cloudflare", + "error": "app rebuilt after binding failure", + "mode": "retained", + "phase": "binding_failure", + "status": "fail" + } + ] + }, + { + "directory": "/private/tmp/edgezero-f8-all-final-a", + "exit_code": 1, + "scope": "After overlap created two Spin instances, the sequential dedup prefix crossed instances and failed. No identity check was relaxed.", + "log": { + "path": "/private/tmp/edgezero-f8-all-final-a.log", + "bytes": 9817, + "sha256": "2467cd2a7ee2aa1070a24de1fec2477e3083d9157d351c7608b8b23c2953c09a" + }, + "events": { + "path": "/private/tmp/edgezero-f8-all-final-a/events.jsonl", + "bytes": 1575597, + "sha256": "2af1ca388238d8927201ae166bdee4a49ce1a819bbc3cb49719612105e483e45" + }, + "diagnostics": [ + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "per-request", + "status": "unverified" + }, + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "retained", + "status": "unverified" + }, + { + "adapter": "spin", + "error": "declared KV sandbox turnover: deduplication unverified", + "mode": "retained", + "phase": "declared_kv", + "status": "fail" + } + ] + }, + { + "directory": "/private/tmp/edgezero-f8-all-final-b", + "exit_code": 2, + "scope": "All 24 F8 request cases and three shutdown checks pass; Spin per-request overlap remains unverified.", + "log": { + "path": "/private/tmp/edgezero-f8-all-final-b.log", + "bytes": 7811, + "sha256": "5f5da1b36620c80b07afd2f30e534712669b9d0dfdb7a9a1084c6c3ffa4b7c42" + }, + "events": { + "path": "/private/tmp/edgezero-f8-all-final-b/events.jsonl", + "bytes": 1775142, + "sha256": "9539a7812287e041e0ef82a95f19897ba920c3f09165923e0d9c77979c3d50d9" + }, + "diagnostics": [ + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "per-request", + "status": "unverified" + } + ] + }, + { + "directory": "/private/tmp/edgezero-f8-all-final-c", + "exit_code": 2, + "scope": "Final harness source after warning snapshot fix. All 24 F8 cases and three shutdown checks pass; independent Spin per-request and retained overlap remain unverified.", + "log": { + "path": "/private/tmp/edgezero-f8-all-final-c.log", + "bytes": 7438, + "sha256": "8406d6c9e853ceca037ec0242c0fbe2cd6219418dd4694c14d66cd3cbdf0bb7d" + }, + "events": { + "path": "/private/tmp/edgezero-f8-all-final-c/events.jsonl", + "bytes": 1785349, + "sha256": "88b9c651ab9f5b3584abc0aa2746c8bc4fe33162117746c0239bd963540395b4" + }, + "diagnostics": [ + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "per-request", + "status": "unverified" + }, + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "retained", + "status": "unverified" + } + ] + } + ], + "limits": [ + "No hosted provider outage/restoration evidence; fresh-cell recovery is scripted unit/source-path proof, not backend repair.", + "Healthy linked-store CRUD proves continuity, not restoration of the missing binding.", + "Fastly Completed and Cloudflare/Spin HostHandoff do not certify end-client delivery.", + "Final C retains two independent Spin overlap observations as unverified; no whole-suite pass claimed.", + "No request-affinity guarantee after overlap; strict sequential dedup prefix still fails on identity turnover.", + "No KV allocation/deadline or host cleanup certification.", + "Consumer EZ-20 acceptance 5 and canonical cutover activation remain open.", + "G2 readiness failure, Cloudflare post-overflow front-proxy recovery failure, and full P1 hard migration remain unchanged.", + "Raw logs and binaries are local ephemeral artifacts, not immutable release assets.", + "No tests required credentials, remote deployments, downloads, dependency repins or failure waivers." + ] +} diff --git a/docs/audits/evidence/2026-10-21-generated-fallback.json b/docs/audits/evidence/2026-10-21-generated-fallback.json new file mode 100644 index 000000000..19595bd77 --- /dev/null +++ b/docs/audits/evidence/2026-10-21-generated-fallback.json @@ -0,0 +1,693 @@ +{ + "schema": 1, + "recorded_date": "2026-10-21", + "base_revision": "619c69bd2b2b24f35210c482b560cb46b1b96888", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "status": "pass-local-generated-demo-fallback-leg-only", + "requirement": "EZ-10.9 and EZ-10 acceptance criterion8; F2 local generated/demo Axum fallback leg", + "scope": "Existing edgezero new scaffold and bundled edgezero demo, through unchanged run_app/run_app_with_preflight Axum entrypoints over loopback HTTP/1.1; no replacement listener, router or application policy", + "commands": { + "generated": "CARGO_NET_OFFLINE=true CARGO_TARGET_DIR=$PWD/target cargo test --offline --locked -p edgezero-cli --test generated_project_builds -- --ignored --nocapture", + "demo": "cargo test --offline --locked -p edgezero-cli --features demo-example --test demo_logging -- --nocapture" + }, + "toolchain": { + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)\nbinary: rustc\ncommit-hash: 48a229ceaefd4985c50990b14116b6d856af0985\ncommit-date: 2026-09-01\nhost: aarch64-apple-darwin\nrelease: 1.98.1\nLLVM version: 22.1.8", + "cargo": "cargo 1.98.1 (797e8a9bc 2026-08-05)", + "installed_targets": [ + "aarch64-apple-darwin", + "wasm32-unknown-unknown", + "wasm32-wasip1", + "wasm32-wasip2" + ] + }, + "environment": { + "mode": "isolated temporary working directories, fresh loopback ports and local file stores; no hosted deployment, credentials or external origins", + "offline_generated": true, + "adapter_override_policy": "Clear inherited EDGEZERO__ overrides in server children only; explicitly select 127.0.0.1, reserved port and logging off", + "normal_dependency_changes": false, + "dev_dependency": "httparse =1.10.1, already in lockfile; add only the CLI dev-dependency edge" + }, + "source_snapshot_scope": "Selected implementation, generator, fixture and CI inputs, not an authoritative complete candidate tree", + "source_sha256": { + "Cargo.toml": "527a6f29fd231afb35dea70ba3ec41b9c9e78ce52324cb20fe9ed6c75afb415b", + "Cargo.lock": "7f65719e408c31d4e6c32f2e168b00ace4f73de412ca6b5469b831bd151c04ac", + "crates/edgezero-cli/Cargo.toml": "ac6d584995247d98ea4250eb9f616c39c35b9806766479020b6428f6915eef16", + "crates/edgezero-cli/src/generator.rs": "9fd5b374b748e91319d673bdd76abf9b40679a7b7cb5a611ee3bfba5bc0d4e57", + "crates/edgezero-cli/src/scaffold.rs": "56521292effeab33f1d37cd1e379eac6ab494d357de226e9ac7c6a08bd4f56b8", + "crates/edgezero-cli/src/demo_server.rs": "40e751ba3a436bd1a8d558d1f2fa0a90c3d04685dc3ab3334c191fed14e99ea5", + "crates/edgezero-cli/src/templates/core/src/lib.rs.hbs": "19ae8ce1d6a47945d7b8676d826e54671007358903deb26bb353b2083030a93a", + "crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs": "d457edd4d91435af010014b5fcc4f0b454aff1c2d528f17edc8cb8a4993dc362", + "crates/edgezero-cli/src/templates/root/edgezero.toml.hbs": "c074eff938039b2115a348ec127df45658343a91f9fe1647cd9365bdc66a5024", + "crates/edgezero-cli/tests/demo_logging.rs": "30599b60e7b6bdf323d489e0bae6b4a4f2d442af9539a981f6919d8a24b6d967", + "crates/edgezero-cli/tests/generated_project_builds.rs": "d41224e0eb1c95f3faffdfe01f93fd656cb53a24e2e94a253d545e9b05c307bf", + "crates/edgezero-cli/tests/support/fallback_tcp.rs": "464cecfc7d0108cfcd434450b99f13896eb4ee1a6fca287ba4fc19c56860c3e1", + "crates/edgezero-adapter-axum/src/templates/src/main.rs.hbs": "fc12412cb495e781a9368b9a0d8624e6509b9739ae348174fbe78bb75ec70726", + "crates/edgezero-adapter-axum/src/dev_server.rs": "8563f4b7fc83e6b163b3df3f561ba4c46b5189aed9a1956ecaec818a0ab5377e", + "crates/edgezero-adapter-axum/src/connection.rs": "ae594bdb9d745f953d394883a2fe87a6a231dd2aba5b18c15b8341e745eb9bbd", + "crates/edgezero-adapter-axum/src/request.rs": "0c61c652c7e3455d35b9802b26b6118b68c678bdfe01cc1215c7a9ec931cddef", + "crates/edgezero-adapter-axum/src/response.rs": "f35df64dcb1c90a19720739b10dd72903bc73d92a43734d80d1f38cdab29ac51", + "crates/edgezero-adapter-axum/src/service.rs": "642b85143cc3c0fa9b56154166791cbdb35146838fea6fd10776e85e32fb331a", + "crates/edgezero-core/src/app.rs": "96191a2045bd93b4f0db0aaa18b8a27a47683171beeb8b5e2c4180da144aee8e", + "crates/edgezero-core/src/body.rs": "304908d06d0cd7a1d846e3a6d36dc08e4e498728e06ae7c36050b8389f8fbb39", + "crates/edgezero-core/src/context.rs": "c9b9cca8120b1f81ba5e101272b9df654f459edbf10ea33759cfffd558b347ed", + "crates/edgezero-core/src/error.rs": "81d3b708862a18571235baa4dd03cd580b52af6807d092ba22f780ed28c22534", + "crates/edgezero-core/src/ingress.rs": "479c2a7dac88822ce5991c502852975d07092e4c690e98be909ef0062c944da5", + "crates/edgezero-core/src/router.rs": "43a0aa4239463670c967d324db28732c7f8341d60ceee2753a616936c64755a5", + "crates/edgezero-core/src/response_egress.rs": "e00790c1ecfc46a9a04893ceec2a946b434855816e5338592366f784f246aee2", + "examples/app-demo/crates/app-demo-core/src/lib.rs": "eed58298aa757f26c0d2bbfe20552f07725c873b691e5c9d3a0d8f6ac00190f0", + "examples/app-demo/edgezero.toml": "68890ea7e4bf1001877419e641c90c4a4fd6f66dfbe23c0bd551c3e75438d73a", + ".github/workflows/test.yml": "176fe8db29f7bcccccd576b81218a1fbd4084a53da036a937c94182ad351ba4b", + "scripts/run_test_nonzero.sh": "f4854db904339ab9197591be6dd4eccfb186fc602c79f8142affe0453a75ccf6", + "scripts/run_test_nonzero.test.mjs": "1c80689f6830b671822082449a997ae0f6a210ac6a2b4cb9ebf36a7ebf6ba18e" + }, + "retained_artifacts": [ + { + "path": "/private/tmp/edgezero-fallback-20261021-artifacts/generated-source.tar.gz", + "bytes": 49603, + "sha256": "986f50b434358a4f4bdd58572110d8b97cf057511f117c89414bfe9bb3e8d18a" + }, + { + "path": "/private/tmp/edgezero-fallback-20261021-artifacts/generated-axum", + "bytes": 30022912, + "sha256": "095cb12b15550bfc8135a2a847d0bd6e39ed7e83522d3ac3a9958c371fc26e35" + }, + { + "path": "/private/tmp/edgezero-fallback-20261021-artifacts/edgezero-demo", + "bytes": 53042936, + "sha256": "00fdb45604659b9e2fda7e0e5274da9ba43312e0d9fe4ee80a6de24795e1f5c3" + } + ], + "successful_runs": [ + { + "app": "generated", + "exit_code": 0, + "raw_log": { + "path": "/private/tmp/edgezero-generated-fallback-20261021-b.log", + "bytes": 51829, + "sha256": "0a4408658580f08e1ee2a6843e0e4fd348e1238c3921aeecff5e983aec8daca6" + }, + "test_summary": "test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 783.15s" + }, + { + "app": "demo", + "exit_code": 0, + "raw_log": { + "path": "/private/tmp/edgezero-demo-fallback-20261021-b.log", + "bytes": 5461, + "sha256": "7bf82e9b9be9ec6e844a5010a5f84467ac1fd58410ecc9f392fde06d787dd8b3" + }, + "test_summary": "test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.01s" + } + ], + "initial_failed_run": { + "exit_code": 101, + "raw_log": { + "path": "/private/tmp/edgezero-generated-fallback-20261021-a.log", + "bytes": 41046, + "sha256": "889b9e9191a172c5cf42508e5d2e0e364595d0c6ce0e5b1a5f56406675fc74ac" + }, + "finding": "Native build succeeded but lexical manifest matching returned zero executables for a macOS /var versus /private/var alias; fixed with canonical path comparison", + "source_identity": "Earlier uncommitted harness state, not the successful final snapshot; no complete earlier source snapshot retained" + }, + "coverage": { + "applications": 2, + "cases_per_application": 16, + "total": 32, + "boundary_cases_per_application": 12, + "framing": ["Content-Length", "chunked"], + "delivered_body_bytes": [4095, 4096, 4097], + "cap_bytes": 4096, + "fallbacks": { + "POST /missing": "404 under/exact cap, 413 above cap", + "POST /": "405 with Allow GET under/exact cap, 413 above cap" + }, + "overflow_body": "request body too large\n", + "overflow_content_type_assertion": "text/plain; charset=utf-8", + "pending_timeout": "No body data or EOF; full408 response must arrive after4.5s and before8s, with exact request timeout newline body", + "pipeline": "Send the first4097-byte chunk without the terminal chunk; wait for complete413, then send remaining request-looking payload, terminal chunk and genuine health request. Require safe close or exactly the expected200 health response; fresh health succeeds" + }, + "records": [ + { + "event": "fallback-tcp", + "app": "generated", + "case": "initial-health", + "responses": [ + { + "status": 200, + "body": "scaffold-probe app", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/missing-fixed-4095", + "responses": [ + { + "status": 404, + "body": "{\"error\":{\"kind\":\"not_found\",\"message\":\"no route matched path: /missing\",\"status\":404}}", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/missing-fixed-4096", + "responses": [ + { + "status": 404, + "body": "{\"error\":{\"kind\":\"not_found\",\"message\":\"no route matched path: /missing\",\"status\":404}}", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/missing-fixed-4097", + "responses": [ + { + "status": 413, + "body": "request body too large\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/missing-chunked-4095", + "responses": [ + { + "status": 404, + "body": "{\"error\":{\"kind\":\"not_found\",\"message\":\"no route matched path: /missing\",\"status\":404}}", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/missing-chunked-4096", + "responses": [ + { + "status": 404, + "body": "{\"error\":{\"kind\":\"not_found\",\"message\":\"no route matched path: /missing\",\"status\":404}}", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/missing-chunked-4097", + "responses": [ + { + "status": 413, + "body": "request body too large\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/-fixed-4095", + "responses": [ + { + "status": 405, + "body": "{\"error\":{\"kind\":\"method_not_allowed\",\"message\":\"method POST not allowed; allowed: GET\",\"status\":405}}", + "allow": ["GET"], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/-fixed-4096", + "responses": [ + { + "status": 405, + "body": "{\"error\":{\"kind\":\"method_not_allowed\",\"message\":\"method POST not allowed; allowed: GET\",\"status\":405}}", + "allow": ["GET"], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/-fixed-4097", + "responses": [ + { + "status": 413, + "body": "request body too large\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/-chunked-4095", + "responses": [ + { + "status": 405, + "body": "{\"error\":{\"kind\":\"method_not_allowed\",\"message\":\"method POST not allowed; allowed: GET\",\"status\":405}}", + "allow": ["GET"], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/-chunked-4096", + "responses": [ + { + "status": 405, + "body": "{\"error\":{\"kind\":\"method_not_allowed\",\"message\":\"method POST not allowed; allowed: GET\",\"status\":405}}", + "allow": ["GET"], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "/-chunked-4097", + "responses": [ + { + "status": 413, + "body": "request body too large\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 5, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "pending-fallback-deadline", + "responses": [ + { + "status": 408, + "body": "request timeout\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 5002, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "overflow-deferred-tail-close-or-health", + "responses": [ + { + "status": 413, + "body": "request body too large\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "generated", + "case": "recovery-health", + "responses": [ + { + "status": 200, + "body": "scaffold-probe app", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "initial-health", + "responses": [ + { + "status": 200, + "body": "app-demo app", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 1, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/missing-fixed-4095", + "responses": [ + { + "status": 404, + "body": "{\"error\":{\"status\":404,\"kind\":\"not_found\",\"message\":\"no route matched path: /missing\"}}", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/missing-fixed-4096", + "responses": [ + { + "status": 404, + "body": "{\"error\":{\"status\":404,\"kind\":\"not_found\",\"message\":\"no route matched path: /missing\"}}", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/missing-fixed-4097", + "responses": [ + { + "status": 413, + "body": "request body too large\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/missing-chunked-4095", + "responses": [ + { + "status": 404, + "body": "{\"error\":{\"status\":404,\"kind\":\"not_found\",\"message\":\"no route matched path: /missing\"}}", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/missing-chunked-4096", + "responses": [ + { + "status": 404, + "body": "{\"error\":{\"status\":404,\"kind\":\"not_found\",\"message\":\"no route matched path: /missing\"}}", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/missing-chunked-4097", + "responses": [ + { + "status": 413, + "body": "request body too large\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/-fixed-4095", + "responses": [ + { + "status": 405, + "body": "{\"error\":{\"status\":405,\"kind\":\"method_not_allowed\",\"message\":\"method POST not allowed; allowed: GET\"}}", + "allow": ["GET"], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/-fixed-4096", + "responses": [ + { + "status": 405, + "body": "{\"error\":{\"status\":405,\"kind\":\"method_not_allowed\",\"message\":\"method POST not allowed; allowed: GET\"}}", + "allow": ["GET"], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/-fixed-4097", + "responses": [ + { + "status": 413, + "body": "request body too large\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/-chunked-4095", + "responses": [ + { + "status": 405, + "body": "{\"error\":{\"status\":405,\"kind\":\"method_not_allowed\",\"message\":\"method POST not allowed; allowed: GET\"}}", + "allow": ["GET"], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/-chunked-4096", + "responses": [ + { + "status": 405, + "body": "{\"error\":{\"status\":405,\"kind\":\"method_not_allowed\",\"message\":\"method POST not allowed; allowed: GET\"}}", + "allow": ["GET"], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "/-chunked-4097", + "responses": [ + { + "status": 413, + "body": "request body too large\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "pending-fallback-deadline", + "responses": [ + { + "status": 408, + "body": "request timeout\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 5004, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "overflow-deferred-tail-close-or-health", + "responses": [ + { + "status": 413, + "body": "request body too large\n", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + }, + { + "event": "fallback-tcp", + "app": "demo", + "case": "recovery-health", + "responses": [ + { + "status": 200, + "body": "app-demo app", + "allow": [], + "retry_after_count": 0 + } + ], + "response_complete_ms": 0, + "status": "pass", + "qualification": false + } + ], + "bounds": { + "wire_capture_bytes": 8192, + "overflow_witness_bytes": 1, + "max_response_heads": 2, + "response_header_count": 32, + "max_buffered_response_body_bytes": 1024, + "client_connect_timeout_ms": 1000, + "client_absolute_read_budget_ms": 8000, + "initial_write_timeout_ms": 2000, + "deferred_write_timeout": "min(2 seconds, remaining read budget); per-write timeout, not a whole-exchange wall-clock guarantee", + "startup_watchdog_ms": 15000, + "startup_connect_probe_timeout_ms": 100, + "diagnostic_read_bytes": 65536, + "response_reader": "httparse header parsing; exactly one valid bounded Content-Length; reject TE, ambiguous lengths, missing/truncated heads/bodies and extra/trailing replies", + "cleanup": "Child liveness checked before/after probes and readiness; expected health body identifies application; direct child killed/reaped on guard drop including failure" + }, + "verification": { + "root_fmt": "pass", + "root_clippy": "pass; workspace all-targets all-features -Dwarnings", + "root_tests": "pass; 793coretests, all native workspace targets", + "root_feature_check": "pass", + "root_spin_check": "pass;wasm32-wasip2", + "demo_contracts": "5passed", + "generated_workspace": "pass;22generatedcoretests, hoststrictclippy, nativebuild/TCP and threeWASMcompilationchecks", + "guards": [ + "outbound docs contract", + "legacy API", + "Brotli dependency contract", + "7 sentinel-selection regressions; 25 combined with cutover checker tests" + ] + }, + "limitations": [ + "Local HTTP/1 evidence only, not hosted lifecycle or full allocation certification.", + "No capability promotion, changed production transport or application policy.", + "Cloudflare post-overflow proxy failure and Fastly failed G2 readiness gate remain open and unchanged.", + "Hosted FastlyHTTP2, TrustedServer end-to-end and later F7b composition qualification remain open.", + "Generated dependencies resolve from the offline cache with existing audited exact requirements; other compatible versions may differ from the root lockfile. The retained generated source archive includes its own lockfile." + ], + "ci_guard_verification": { + "command": "scripts/run_test_nonzero.sh --include-ignored demo_fallbacks_enforce_body_cap_over_tcp cargo test --offline --locked -p edgezero-cli --features demo-example --test demo_logging", + "exit_code": 0, + "raw_log": { + "path": "/private/tmp/edgezero-demo-fallback-20261021-guard.log", + "bytes": 1330, + "sha256": "f9fb425d2a2b3afcebee27473e1731b9ff98c9912019185adde6b911843f8fe2" + }, + "result": "5 passed, including the required fallback test and original logging tests; --include-ignored forces future ignored tests to execute", + "node_regressions": "7 passed, covering all selection modes, both phases, missing/zero tests and failure propagation" + } +} diff --git a/docs/audits/evidence/2026-10-21-outbound-overflow.json b/docs/audits/evidence/2026-10-21-outbound-overflow.json new file mode 100644 index 000000000..44332b585 --- /dev/null +++ b/docs/audits/evidence/2026-10-21-outbound-overflow.json @@ -0,0 +1,2437 @@ +{ + "schema": 1, + "recorded_date": "2026-10-21", + "base_revision": "852ec74bbb72139dca2809f3537689bb1ee5f0c6", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "scope": "HTTP/1 typed request overflow and upload/head observations through four existing local entrypoints; not body relay or hosted qualification", + "status": "failed-cloudflare-recovery-open", + "command": "SSL_CERT_FILE=/etc/ssl/cert.pem WASM_BINDGEN_BIN=/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen WORKER_BUILD_BIN=/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build ESBUILD_BIN=/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild WASM_OPT_BIN=/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt WRANGLER_BIN=/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler WRANGLER_LOG_PATH=/private/tmp/edgezero-pr275-wrangler-logs WRANGLER_SEND_METRICS=false cargo run --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -- --adapter all --suite outbound-overflow --output /private/tmp/edgezero-overflow-all-20261021-c", + "environment": { + "WASM_BINDGEN_BIN": "/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen", + "WORKER_BUILD_BIN": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build", + "ESBUILD_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild", + "WASM_OPT_BIN": "/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt", + "WRANGLER_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler", + "WRANGLER_LOG_PATH": "/private/tmp/edgezero-pr275-wrangler-logs", + "WRANGLER_SEND_METRICS": "false", + "MINIFLARE_WORKERD_PATH": null, + "node": "v24.20.0", + "mode": "four local runtimes with loopback-only origins; local synthetic workerd bindings, no deployment", + "tool_install": "Existing pinned executables reused; no install, download, SDK or dependency repin in this checkpoint.", + "SSL_CERT_FILE": "/etc/ssl/cert.pem" + }, + "runtime_versions": [ + { + "adapter": "fastly", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "revision": "852ec74bbb72139dca2809f3537689bb1ee5f0c6", + "source_state": "working-tree-characterization", + "version": "viceroy 0.21.0" + }, + { + "adapter": "cloudflare", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "revision": "852ec74bbb72139dca2809f3537689bb1ee5f0c6", + "source_state": "working-tree-characterization", + "version": "4.131.0" + }, + { + "adapter": "spin", + "event": "progress-runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "revision": "852ec74bbb72139dca2809f3537689bb1ee5f0c6", + "source_state": "working-tree-characterization", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + }, + { + "adapter": "axum", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo", + "revision": "852ec74bbb72139dca2809f3537689bb1ee5f0c6", + "source_state": "working-tree-characterization", + "version": "cargo 1.98.1 (797e8a9bc 2026-08-05)" + } + ], + "tools": { + "WASM_BINDGEN_BIN": { + "path": "/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen", + "bytes": 6938208, + "sha256": "b86e33b3c64362c44d8807ed35797e6b8220dd0232b6b19743caf7c2c68a2284", + "version": "wasm-bindgen 0.2.129" + }, + "WORKER_BUILD_BIN": { + "path": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build", + "bytes": 6149664, + "sha256": "dd7716c2d74637b5d8d2d3e4887252c7c3b8619f549cfb79e5715159bc86c2b0", + "version": "0.8.5" + }, + "ESBUILD_BIN": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild", + "bytes": 10573778, + "sha256": "e2dc9a52440a2a34f09434a2f4843cb1e30f84e40dcf238976ec61ef8cd7f36a", + "version": "0.28.1" + }, + "WASM_OPT_BIN": { + "path": "/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt", + "bytes": 1676584, + "sha256": "fa49ec32a92b72b368b66f8f19d84e9adffeef002449e6a07cd95a3ed2612b41", + "version": "wasm-opt version 130 (version_130)" + }, + "WRANGLER_BIN": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "bytes": 3088, + "sha256": "780661a508810f3b65786895b1ca9aacbc4f55d329ae6b8c1e49ec8433569f77", + "version": "4.131.0" + }, + "workerd": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64/bin/workerd", + "bytes": 156374824, + "sha256": "eb132e3e489b47d804cc70b510a6da8f82dabec4748cdcdaba32a1a1c5c2e285", + "version": "workerd 2026-09-10", + "package_version": "1.20260910.1", + "identification_basis": "Wrangler's bundled workerd package default executable; MINIFLARE_WORKERD_PATH unset; package resolution and executable version verified, not an independent per-process attestation" + } + }, + "sdk_versions": { + "fastly": "0.13.1", + "worker": "0.8.5", + "spin_sdk": "7.0.0", + "reqwest": "0.13.4", + "miniflare": "5.20260910.0-alpha", + "workerd": "1.20260910.1" + }, + "cloudflare_compatibility": { + "date": "2026-04-22", + "flags": ["enable_request_signal", "enable_weak_ref"] + }, + "source_snapshot_scope": "selected production and fixture inputs, not a complete candidate tree or an exact-commit release qualification", + "source_sha256": { + "Cargo.lock": "1afb33fe3067419ad5a2a948b330ac1df123d53214e49dc4005a92f7bc69e043", + "Cargo.toml": "ee73d1318af74a997ecc12bac0f80cd7eefbe8ead34a35b1432c8d6f49fc42e1", + "crates/edgezero-adapter-axum/src/outbound.rs": "5815144bd9720706379f90d2aa8aa2988af393e6e84360098e568f1c2079eb30", + "crates/edgezero-adapter-axum/src/request.rs": "0c61c652c7e3455d35b9802b26b6118b68c678bdfe01cc1215c7a9ec931cddef", + "crates/edgezero-adapter-axum/src/response.rs": "f35df64dcb1c90a19720739b10dd72903bc73d92a43734d80d1f38cdab29ac51", + "crates/edgezero-adapter-cloudflare/src/outbound.rs": "eedcf2bfdab0cd8d7d4aaf7ca6a7a41dfd10309566b9c0239e9e6ce0f6ac3392", + "crates/edgezero-adapter-cloudflare/src/request.rs": "a755088e4e65e5af813335283ec3095c35b5982d1e9057ce58dd748aa03e6200", + "crates/edgezero-adapter-cloudflare/src/response.rs": "15a5b643713d3fdd2bd965fd00d9b606ca5cddf754678c3b0a0b9a65bba21adb", + "crates/edgezero-adapter-fastly/src/outbound.rs": "17edf76e5ca1dadc05f4013f282e443ee1309c4530c54f65412afa01d8463f40", + "crates/edgezero-adapter-fastly/src/request.rs": "c49b75fd52abacc1b2fb012cc357b3a0ce11f9b6a76397bef0f40114ab79d684", + "crates/edgezero-adapter-fastly/src/response.rs": "5015be8eb6b0f55f2d7e79189a88233fc2142bb06d4820b586a2529fae9703ea", + "crates/edgezero-adapter-spin/src/outbound.rs": "8d98cff3dc724bf6a6c4c3b6b805b3bfb7631e3ff52cd3716745682170e19a61", + "crates/edgezero-adapter-spin/src/request.rs": "962e413a2009c82259405628b39976e4040dde6d1425ff5adfaf6ab0865b9d67", + "crates/edgezero-adapter-spin/src/response.rs": "03e8b98de3e4854810b4bdabba7783d3a66cacb5e91402e941609d7dfe567075", + "crates/edgezero-core/src/app.rs": "96191a2045bd93b4f0db0aaa18b8a27a47683171beeb8b5e2c4180da144aee8e", + "crates/edgezero-core/src/body.rs": "304908d06d0cd7a1d846e3a6d36dc08e4e498728e06ae7c36050b8389f8fbb39", + "crates/edgezero-core/src/compression.rs": "4acccc0c01009894ebe4f77f57ce8ce501d3f3f2ccb565c6b3d3871d826037d4", + "crates/edgezero-core/src/context.rs": "c9b9cca8120b1f81ba5e101272b9df654f459edbf10ea33759cfffd558b347ed", + "crates/edgezero-core/src/error.rs": "81d3b708862a18571235baa4dd03cd580b52af6807d092ba22f780ed28c22534", + "crates/edgezero-core/src/lib.rs": "3b7cc874b16ebb992cfaf7a012c1360536947203a734e9bef5f3742e5d3f5ef3", + "crates/edgezero-core/src/outbound.rs": "edcecdefd41284453cf2960943ece4d4424c0ad5fca37169d65144933f50237e", + "crates/edgezero-core/src/outbound/exchange.rs": "f8ca9867b572b2e40e0c08ae55fefefaefbf82355c83aa8b28f74870d6e2a879", + "crates/edgezero-core/src/outbound/progress.rs": "90eec32101d09c4ee40409b1fae6f06b97c83d1af8a03baf39d07820da9a0a08", + "crates/edgezero-core/src/time.rs": "bcd7047587f656b6547c860986f0c8f6459a5554850cbac4c1bfd9c480ec7435", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "tests/fixtures/reusable-app/Cargo.toml": "7665200757e4e565d11fa7eb036215aa583d21fd543bca39fdbdce470fee11ae", + "tests/fixtures/reusable-app/crates/fixture-axum/Cargo.toml": "1c1421c7497a7aff305d31ecd4c3ee323eb646acd4d56f23cab765ab6a5e377c", + "tests/fixtures/reusable-app/crates/fixture-axum/src/progress.rs": "0c41390031d857f0589667b88bd27fc8e185b483cedc57ec2ee73c99d36a9e4a", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/Cargo.toml": "96501fc975013ba68411cd6e0f77b6b0fe505dfe4ed8cef16620ef19b4c9784d", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs": "606245131f0c594154ca0a2eb63e5341999325bfb2de55887ab3264fa4b89559", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs": "0ec7229f6a64b08b293a8138eda4bcb3ed711abd77dd2ab65227fb9ee3ed5cb0", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml": "6c2a097879b98f4d4af3e2fea139215abc9fa0bfdc058500f28fd9f7d757aab3", + "tests/fixtures/reusable-app/crates/fixture-core/Cargo.toml": "f9c68c281c9b57269ee8065f46e9e1c3fcc9c58a562259e480f2125877a8f6c8", + "tests/fixtures/reusable-app/crates/fixture-core/src/lib.rs": "9d983ca4169963ccc23edf9fc8b1cec9f38a56e545f58d84883fbf9cda09d08f", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs": "2afff35c0a0b01dd70b959d621613dfa5c5d2871b3cab8a5972b2d7d8dfe5026", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/overflow.rs": "f9fa0f6b9417af2a59bf5ed60fbf4799c81d86b3457343620ebd2e8874f143ee", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs": "b119facad71efc57eb349131d5a9044d1a956843559852cc208e79df05a6013a", + "tests/fixtures/reusable-app/crates/fixture-fastly/Cargo.toml": "e82ea4713f86db533796367cee0c3f91191f33e7e1f0510d0cf1bdafd509a7e1", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs": "0b4a81e3678459801b9f5a9379de70160452317929936d1b6c85393c1249c704", + "tests/fixtures/reusable-app/crates/fixture-harness/Cargo.toml": "93580bf7575878ce3cb5d82ef3e7aea6d5c26baae6f467f469339858992fd141", + "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs": "22f5c8ff413357b5684910249c635a4993d0cb912b3cc584c8f14cc2258efb18", + "tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs": "ee806666ac7e5fe0f22791981bc67fa7eb016cfa57ca3b74d4ed889c15f3eb1e", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs": "723445e8bf33584832cbfc36dc04c049280bb4754c70b1f8ac22d9fa7fee6a75", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net/overflow.rs": "ad7bdd96a37b50d7c88bc5a8a6b0682fbe883c51a39adc40ed04dbd6436c49ef", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs": "246bcfe51176f38de6e4d8d84b6d27ce2dec03f676296b1f3353b7de15ae3b16", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/overflow.rs": "ce305aa14bc45fca96d809b41bb668a2fafaef30b48bd5c58ebc5d574b7df5dd", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/progress.rs": "7999274f23072a9b7499b638c1e63798d662152250674f4fda3fb9cec2765f88", + "tests/fixtures/reusable-app/crates/fixture-spin/Cargo.toml": "7256c3384d5e3c675b8b9e7026d10630bc5fec9c3aaf3900bc740fcb4e1526b4", + "tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs": "0a20af88ab9a94fe8ef806b15e38994c7790ec0f9040add4c3dec4ead9f022db" + }, + "artifacts": [ + { + "adapter": "fastly", + "path": "/private/tmp/edgezero-overflow-all-20261021-c/overflow-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "bytes": 2190854, + "sha256": "b39dab46b19543b5cc2086010d1ee40fa514efd59be575aabedd60dd1c02bc71" + }, + { + "adapter": "cloudflare", + "path": "/private/tmp/edgezero-overflow-all-20261021-c/overflow-cloudflare/artifacts/build/index_bg.wasm", + "bytes": 1718831, + "sha256": "f57993e2e8003712b6f15f8f75f54caabf66f6f1d218bfe5b4f8cacb7fff11c2" + }, + { + "adapter": "spin", + "path": "/private/tmp/edgezero-overflow-all-20261021-c/overflow-spin/artifacts/fixture_spin.wasm", + "bytes": 2341413, + "sha256": "a4724914bb5392e01a726474db56cdb2c2f8b2d69ee2eb9518f4e323695e8e69" + }, + { + "adapter": "axum", + "path": "/private/tmp/edgezero-overflow-all-20261021-c/overflow-axum/artifacts/fixture-axum-outbound-progress", + "bytes": 8933824, + "sha256": "6857c629d62463ff8554340299db6e27a4bde79adf7022a700dc8e56a5d406a0" + } + ], + "runs": [ + { + "id": "a", + "directory": "/private/tmp/edgezero-overflow-all-20261021-a", + "exit_code": 1, + "events": { + "path": "/private/tmp/edgezero-overflow-all-20261021-a/events.jsonl", + "bytes": 36596, + "sha256": "7e62f1ed68b080242f0ad453adc91f49971112b9c6ab00b957056e64205a05fd" + }, + "summary": { + "path": "/private/tmp/edgezero-overflow-all-20261021-a/summary.json", + "bytes": 302, + "sha256": "fdad4ea9024508071acff823ee80f31b68f6c2e1980faefaaff86076ffc842e3" + }, + "coverage": [ + { + "adapter": "fastly", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "fail" + }, + { + "adapter": "cloudflare", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "fail" + }, + { + "adapter": "spin", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "fail" + }, + { + "adapter": "axum", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + } + ], + "failed_cases": [ + { + "adapter": "fastly", + "case": "streamed-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["date", "Wed, 21 Oct 2026 17:32:26 GMT"] + ], + "status": 413 + }, + "error": "foreign or headless upload origin receipt", + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 0, + "body_prefix": [], + "head": null, + "terminal": "eof" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "fail", + "token": "18e09bcae7daf920-15cac-8" + }, + { + "adapter": "cloudflare", + "case": "buffered-7", + "client": { + "body": "Error: Network connection lost.\n at async Object.fetch (file:///Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/miniflare/dist/src/workers/core/entry.worker.js:5216:22)", + "headers": [ + ["content-length", "201"], + ["content-type", "text/plain;charset=UTF-8"] + ], + "status": 500 + }, + "error": "overflow probe status mismatch", + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "fail", + "token": "18e09bce7fa75390-15cac-d" + }, + { + "adapter": "spin", + "case": "streamed-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 17:32:52 GMT"] + ], + "status": 413 + }, + "error": "foreign or headless upload origin receipt", + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 0, + "body_prefix": [], + "head": null, + "terminal": "eof" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "fail", + "token": "18e09bd0c1499040-15cac-1c" + } + ], + "runtime_logs": [ + { + "path": "/private/tmp/edgezero-overflow-all-20261021-a/overflow-axum/runtime.log", + "bytes": 604, + "sha256": "239827d3620c68ce6723209762a90d2a1712f2f612d2450f342f68a931f766b4" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-a/overflow-cloudflare/runtime.log", + "bytes": 3344, + "sha256": "3cdaeb68531df85ed302bf678328381091e1fca21b619db328f21c5a12e99cb3" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-a/overflow-fastly/runtime.log", + "bytes": 331, + "sha256": "42ef08f64fc6897a33dfcc2d86aa742b2ff46b0e735a8da4f3a6a7d70f28752a" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-a/overflow-spin/runtime.log", + "bytes": 1671, + "sha256": "1f07fbfe976143f2279bf834d260605e1282c331bbbdcdf482434a837493c25f" + } + ] + }, + { + "id": "b", + "directory": "/private/tmp/edgezero-overflow-all-20261021-b", + "exit_code": 1, + "events": { + "path": "/private/tmp/edgezero-overflow-all-20261021-b/events.jsonl", + "bytes": 64992, + "sha256": "40b1742ad55f35debdb6774854db53c949d777224f8df700792d80140c67e788" + }, + "summary": { + "path": "/private/tmp/edgezero-overflow-all-20261021-b/summary.json", + "bytes": 303, + "sha256": "753ef759c786b7b12594f4b374b95d1c65c0cecac9ba9110343bb3f0aa49c476" + }, + "coverage": [ + { + "adapter": "fastly", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "fail" + }, + { + "adapter": "spin", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + } + ], + "failed_cases": [ + { + "adapter": "cloudflare", + "case": "buffered-7", + "client": { + "body": "Error: Network connection lost.\n at async Object.fetch (file:///Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/miniflare/dist/src/workers/core/entry.worker.js:5216:22)", + "complete_ns": 6451334, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6431625, + "headers": [ + ["content-length", "201"], + ["content-type", "text/plain;charset=UTF-8"] + ], + "status": 500, + "token": "18e09c21dc350b18-31aa-d" + }, + "error": "overflow probe status mismatch", + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "fail", + "token": "18e09c21dc350b18-31aa-d" + } + ], + "runtime_logs": [ + { + "path": "/private/tmp/edgezero-overflow-all-20261021-b/overflow-axum/runtime.log", + "bytes": 604, + "sha256": "04f170c9b686f0d2dab4b4b8167d26d594c6ce0b0ef416badb8e41e185bd93b0" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-b/overflow-cloudflare/runtime.log", + "bytes": 5848, + "sha256": "7cb2696198c0873c12878cf3c4e79389fbc56443dc2d9511199cc21eb5005db0" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-b/overflow-fastly/runtime.log", + "bytes": 331, + "sha256": "78bd74bd32a7a5fc4809c5126791d23ada8f8110806b0ffeaf33ea22b23e16a8" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-b/overflow-spin/runtime.log", + "bytes": 1671, + "sha256": "3c7c50ced385595dad9ec7a0d048a9a8f0838c0e092722a2c12728b29b94e0e0" + } + ] + }, + { + "id": "c", + "directory": "/private/tmp/edgezero-overflow-all-20261021-c", + "exit_code": 1, + "events": { + "path": "/private/tmp/edgezero-overflow-all-20261021-c/events.jsonl", + "bytes": 65020, + "sha256": "0ebb651aacb89c63b67f472bfb92e3d468e12bed5c5808b8d630077f0a861ca1" + }, + "summary": { + "path": "/private/tmp/edgezero-overflow-all-20261021-c/summary.json", + "bytes": 303, + "sha256": "753ef759c786b7b12594f4b374b95d1c65c0cecac9ba9110343bb3f0aa49c476" + }, + "coverage": [ + { + "adapter": "fastly", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "fail" + }, + { + "adapter": "spin", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "attempted": 10, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + } + ], + "failed_cases": [ + { + "adapter": "cloudflare", + "case": "buffered-7", + "client": { + "body": "Error: Network connection lost.\n at async Object.fetch (file:///Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/miniflare/dist/src/workers/core/entry.worker.js:5216:22)", + "complete_ns": 6696458, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6655500, + "headers": [ + ["content-length", "201"], + ["content-type", "text/plain;charset=UTF-8"] + ], + "status": 500, + "token": "18e09c8b97d97ae8-8bd6-d" + }, + "error": "overflow probe status mismatch", + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "fail", + "token": "18e09c8b97d97ae8-8bd6-d" + } + ], + "runtime_logs": [ + { + "path": "/private/tmp/edgezero-overflow-all-20261021-c/overflow-axum/runtime.log", + "bytes": 604, + "sha256": "dd57d7fd71e6a663853bcc48b1569a0c1058d99c14666d0e4972f914ff541b95" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-c/overflow-cloudflare/runtime.log", + "bytes": 5851, + "sha256": "ee187e21f95fe9436bcf21e6cc2923caa9c6dbf9806b7ec1c65a849a4c2cc089" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-c/overflow-fastly/runtime.log", + "bytes": 331, + "sha256": "70dd133648c7c259c14388e5a1d7b48070c49c0adbb7badcbbbf9a4a91281d2a" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-c/overflow-spin/runtime.log", + "bytes": 1671, + "sha256": "aa93e972ce20ec8e9e9795342853908bc686079e8744a76bed16d25fbb0c4627" + } + ] + } + ], + "final_cases": [ + { + "adapter": "fastly", + "case": "inbound-1023", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1023}", + "complete_ns": 32801500, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 32768833, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:05 GMT"] + ], + "status": 200, + "token": "18e09c8945cf8d20-8bd6-0" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8945cf8d20-8bd6-0" + }, + { + "adapter": "fastly", + "case": "inbound-1024", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1024}", + "complete_ns": 555250, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 547625, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:05 GMT"] + ], + "status": 200, + "token": "18e09c8947cc8600-8bd6-1" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8947cc8600-8bd6-1" + }, + { + "adapter": "fastly", + "case": "inbound-1025", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 590291, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 579500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["date", "Wed, 21 Oct 2026 17:46:05 GMT"] + ], + "status": 413, + "token": "18e09c8947de4cc8-8bd6-2" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8947de4cc8-8bd6-2" + }, + { + "adapter": "fastly", + "case": "buffered-7", + "client": { + "body": "{\"case\":\"buffered-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7417958, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7395125, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:05 GMT"] + ], + "status": 200, + "token": "18e09c8947ee7580-8bd6-3" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:56234"], + ["content-length", "7"] + ], + "method": "POST", + "target": "/overflow/18e09c8947ee7580-8bd6-3/buffered-7", + "version": "HTTP/1.1" + }, + "head_bytes": 131, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8947ee7580-8bd6-3" + }, + { + "adapter": "fastly", + "case": "buffered-8", + "client": { + "body": "{\"case\":\"buffered-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 5534292, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 5525292, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:05 GMT"] + ], + "status": 200, + "token": "18e09c894f2bde28-8bd6-4" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:56237"], + ["content-length", "8"] + ], + "method": "POST", + "target": "/overflow/18e09c894f2bde28-8bd6-4/buffered-8", + "version": "HTTP/1.1" + }, + "head_bytes": 131, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c894f2bde28-8bd6-4" + }, + { + "adapter": "fastly", + "case": "buffered-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 956041, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 937333, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["date", "Wed, 21 Oct 2026 17:46:05 GMT"] + ], + "status": 413, + "token": "18e09c895637a3c8-8bd6-5" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c895637a3c8-8bd6-5" + }, + { + "adapter": "fastly", + "case": "streamed-7", + "client": { + "body": "{\"case\":\"streamed-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7818083, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7774750, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:05 GMT"] + ], + "status": 200, + "token": "18e09c895c78e5a8-8bd6-6" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:56242"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e09c895c78e5a8-8bd6-6/streamed-7", + "version": "HTTP/1.1" + }, + "head_bytes": 140, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c895c78e5a8-8bd6-6" + }, + { + "adapter": "fastly", + "case": "streamed-8", + "client": { + "body": "{\"case\":\"streamed-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7252583, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7243833, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:05 GMT"] + ], + "status": 200, + "token": "18e09c8963b037b8-8bd6-7" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:56245"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e09c8963b037b8-8bd6-7/streamed-8", + "version": "HTTP/1.1" + }, + "head_bytes": 140, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8963b037b8-8bd6-7" + }, + { + "adapter": "fastly", + "case": "streamed-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 775791, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 749333, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["date", "Wed, 21 Oct 2026 17:46:05 GMT"] + ], + "status": 413, + "token": "18e09c896a970c50-8bd6-8" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 0, + "body_prefix": [], + "head": null, + "head_bytes": 0, + "head_state": "absent", + "terminal": "eof" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c896a970c50-8bd6-8" + }, + { + "adapter": "fastly", + "case": "upstream-413", + "client": { + "body": "{\"case\":\"upstream-413\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":\"120\",\"upstream_status\":413}", + "complete_ns": 6267042, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6258375, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:05 GMT"] + ], + "status": 200, + "token": "18e09c8971527f70-8bd6-9" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:56251"], + ["content-length", "8"] + ], + "method": "POST", + "target": "/overflow/18e09c8971527f70-8bd6-9/upstream-413", + "version": "HTTP/1.1" + }, + "head_bytes": 133, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8971527f70-8bd6-9" + }, + { + "adapter": "cloudflare", + "case": "inbound-1023", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1023}", + "complete_ns": 378536166, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 378276416, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e09c8b80167cd0-8bd6-a" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8b80167cd0-8bd6-a" + }, + { + "adapter": "cloudflare", + "case": "inbound-1024", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1024}", + "complete_ns": 5191334, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 5174542, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e09c8b96b6c508-8bd6-b" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8b96b6c508-8bd6-b" + }, + { + "adapter": "cloudflare", + "case": "inbound-1025", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 11581000, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 11299917, + "headers": [ + ["content-length", "86"], + ["content-type", "application/json"] + ], + "status": 413, + "token": "18e09c8b971a8c00-8bd6-c" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8b971a8c00-8bd6-c" + }, + { + "adapter": "cloudflare", + "case": "buffered-7", + "client": { + "body": "Error: Network connection lost.\n at async Object.fetch (file:///Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/miniflare/dist/src/workers/core/entry.worker.js:5216:22)", + "complete_ns": 6696458, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6655500, + "headers": [ + ["content-length", "201"], + ["content-type", "text/plain;charset=UTF-8"] + ], + "status": 500, + "token": "18e09c8b97d97ae8-8bd6-d" + }, + "error": "overflow probe status mismatch", + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "fail", + "token": "18e09c8b97d97ae8-8bd6-d" + }, + { + "adapter": "cloudflare", + "case": "buffered-8", + "client": { + "body": "{\"case\":\"buffered-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 12624833, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 10798250, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e09c8b9f068fe0-8bd6-e" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["content-length", "8"], + ["host", "127.0.0.1:56522"], + ["accept-encoding", "identity"], + ["cache-control", "no-cache"], + ["pragma", "no-cache"], + ["cf-worker", "reusable-app-fixture.example.com"] + ], + "method": "POST", + "target": "/overflow/18e09c8b9f068fe0-8bd6-e/buffered-8", + "version": "HTTP/1.1" + }, + "head_bytes": 219, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8b9f068fe0-8bd6-e" + }, + { + "adapter": "cloudflare", + "case": "buffered-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 4999500, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 4022000, + "headers": [ + ["content-length", "86"], + ["content-type", "application/json"] + ], + "status": 413, + "token": "18e09c8ba620ff68-8bd6-f" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8ba620ff68-8bd6-f" + }, + { + "adapter": "cloudflare", + "case": "streamed-7", + "client": { + "body": "{\"case\":\"streamed-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 9615208, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 9066833, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e09c8bac85f9d0-8bd6-10" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["content-length", "7"], + ["host", "127.0.0.1:56528"], + ["accept-encoding", "identity"], + ["cache-control", "no-cache"], + ["pragma", "no-cache"], + ["cf-worker", "reusable-app-fixture.example.com"] + ], + "method": "POST", + "target": "/overflow/18e09c8bac85f9d0-8bd6-10/streamed-7", + "version": "HTTP/1.1" + }, + "head_bytes": 220, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8bac85f9d0-8bd6-10" + }, + { + "adapter": "cloudflare", + "case": "streamed-8", + "client": { + "body": "{\"case\":\"streamed-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 9087375, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7671250, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e09c8bb3886cb8-8bd6-11" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["content-length", "8"], + ["host", "127.0.0.1:56531"], + ["accept-encoding", "identity"], + ["cache-control", "no-cache"], + ["pragma", "no-cache"], + ["cf-worker", "reusable-app-fixture.example.com"] + ], + "method": "POST", + "target": "/overflow/18e09c8bb3886cb8-8bd6-11/streamed-8", + "version": "HTTP/1.1" + }, + "head_bytes": 220, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8bb3886cb8-8bd6-11" + }, + { + "adapter": "cloudflare", + "case": "streamed-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 4487917, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 4461625, + "headers": [ + ["content-length", "86"], + ["content-type", "application/json"] + ], + "status": 413, + "token": "18e09c8bba640268-8bd6-12" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8bba640268-8bd6-12" + }, + { + "adapter": "cloudflare", + "case": "upstream-413", + "client": { + "body": "{\"case\":\"upstream-413\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":\"120\",\"upstream_status\":413}", + "complete_ns": 11226166, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 10968166, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e09c8bc12d0e00-8bd6-13" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["content-length", "8"], + ["host", "127.0.0.1:56536"], + ["accept-encoding", "identity"], + ["cache-control", "no-cache"], + ["pragma", "no-cache"], + ["cf-worker", "reusable-app-fixture.example.com"] + ], + "method": "POST", + "target": "/overflow/18e09c8bc12d0e00-8bd6-13/upstream-413", + "version": "HTTP/1.1" + }, + "head_bytes": 222, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8bc12d0e00-8bd6-13" + }, + { + "adapter": "spin", + "case": "inbound-1023", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1023}", + "complete_ns": 45925166, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 45904500, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:16 GMT"] + ], + "status": 200, + "token": "18e09c8bf3146e18-8bd6-14" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8bf3146e18-8bd6-14" + }, + { + "adapter": "spin", + "case": "inbound-1024", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1024}", + "complete_ns": 1280000, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 1271084, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:16 GMT"] + ], + "status": 200, + "token": "18e09c8bf5d92f80-8bd6-15" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8bf5d92f80-8bd6-15" + }, + { + "adapter": "spin", + "case": "inbound-1025", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 635500, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 624375, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 17:46:16 GMT"] + ], + "status": 413, + "token": "18e09c8bf5f60268-8bd6-16" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8bf5f60268-8bd6-16" + }, + { + "adapter": "spin", + "case": "buffered-7", + "client": { + "body": "{\"case\":\"buffered-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 13886166, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 13863916, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:16 GMT"] + ], + "status": 200, + "token": "18e09c8bf6075018-8bd6-17" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:56547"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e09c8bf6075018-8bd6-17/buffered-7", + "version": "HTTP/1.1" + }, + "head_bytes": 141, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8bf6075018-8bd6-17" + }, + { + "adapter": "spin", + "case": "buffered-8", + "client": { + "body": "{\"case\":\"buffered-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 6415417, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6400917, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:16 GMT"] + ], + "status": 200, + "token": "18e09c8bfdd7b1c0-8bd6-18" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:56550"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e09c8bfdd7b1c0-8bd6-18/buffered-8", + "version": "HTTP/1.1" + }, + "head_bytes": 141, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8bfdd7b1c0-8bd6-18" + }, + { + "adapter": "spin", + "case": "buffered-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 1902667, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 1883500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 17:46:16 GMT"] + ], + "status": 413, + "token": "18e09c8c04e1b628-8bd6-19" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c04e1b628-8bd6-19" + }, + { + "adapter": "spin", + "case": "streamed-7", + "client": { + "body": "{\"case\":\"streamed-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 8196208, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 8184208, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:17 GMT"] + ], + "status": 200, + "token": "18e09c8c0bba9c58-8bd6-1a" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:56555"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e09c8c0bba9c58-8bd6-1a/streamed-7", + "version": "HTTP/1.1" + }, + "head_bytes": 141, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c0bba9c58-8bd6-1a" + }, + { + "adapter": "spin", + "case": "streamed-8", + "client": { + "body": "{\"case\":\"streamed-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 6568667, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6534708, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:17 GMT"] + ], + "status": 200, + "token": "18e09c8c12df3958-8bd6-1b" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:56558"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e09c8c12df3958-8bd6-1b/streamed-8", + "version": "HTTP/1.1" + }, + "head_bytes": 141, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c12df3958-8bd6-1b" + }, + { + "adapter": "spin", + "case": "streamed-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 10664709, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 10646875, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 17:46:17 GMT"] + ], + "status": 413, + "token": "18e09c8c1aaf3958-8bd6-1c" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 0, + "body_prefix": [], + "head": null, + "head_bytes": 0, + "head_state": "absent", + "terminal": "eof" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c1aaf3958-8bd6-1c" + }, + { + "adapter": "spin", + "case": "upstream-413", + "client": { + "body": "{\"case\":\"upstream-413\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":\"120\",\"upstream_status\":413}", + "complete_ns": 8903917, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 8879917, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 17:46:17 GMT"] + ], + "status": 200, + "token": "18e09c8c22af9058-8bd6-1d" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:56564"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e09c8c22af9058-8bd6-1d/upstream-413", + "version": "HTTP/1.1" + }, + "head_bytes": 143, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c22af9058-8bd6-1d" + }, + { + "adapter": "axum", + "case": "inbound-1023", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1023}", + "complete_ns": 806709, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 781875, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "56"], + ["date", "Wed, 21 Oct 2026 17:46:18 GMT"] + ], + "status": 200, + "token": "18e09c8c61844008-8bd6-1e" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8c61844008-8bd6-1e" + }, + { + "adapter": "axum", + "case": "inbound-1024", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1024}", + "complete_ns": 292125, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 281500, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "56"], + ["date", "Wed, 21 Oct 2026 17:46:18 GMT"] + ], + "status": 200, + "token": "18e09c8c61a0f798-8bd6-1f" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8c61a0f798-8bd6-1f" + }, + { + "adapter": "axum", + "case": "inbound-1025", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 277625, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 254583, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 17:46:18 GMT"] + ], + "status": 413, + "token": "18e09c8c61ac1f10-8bd6-20" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e09c8c61ac1f10-8bd6-20" + }, + { + "adapter": "axum", + "case": "buffered-7", + "client": { + "body": "{\"case\":\"buffered-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 6021000, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6012584, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "105"], + ["date", "Wed, 21 Oct 2026 17:46:18 GMT"] + ], + "status": 200, + "token": "18e09c8c61c34ca8-8bd6-21" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["accept", "*/*"], + ["host", "127.0.0.1:56579"], + ["content-length", "7"] + ], + "method": "POST", + "target": "/overflow/18e09c8c61c34ca8-8bd6-21/buffered-7", + "version": "HTTP/1.1" + }, + "head_bytes": 145, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c61c34ca8-8bd6-21" + }, + { + "adapter": "axum", + "case": "buffered-8", + "client": { + "body": "{\"case\":\"buffered-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 8071084, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 8058792, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "105"], + ["date", "Wed, 21 Oct 2026 17:46:18 GMT"] + ], + "status": 200, + "token": "18e09c8c68e10408-8bd6-22" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["accept", "*/*"], + ["host", "127.0.0.1:56582"], + ["content-length", "8"] + ], + "method": "POST", + "target": "/overflow/18e09c8c68e10408-8bd6-22/buffered-8", + "version": "HTTP/1.1" + }, + "head_bytes": 145, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c68e10408-8bd6-22" + }, + { + "adapter": "axum", + "case": "buffered-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 357625, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 330375, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 17:46:18 GMT"] + ], + "status": 413, + "token": "18e09c8c701d0550-8bd6-23" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c701d0550-8bd6-23" + }, + { + "adapter": "axum", + "case": "streamed-7", + "client": { + "body": "{\"case\":\"streamed-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7623625, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7613333, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "105"], + ["date", "Wed, 21 Oct 2026 17:46:18 GMT"] + ], + "status": 200, + "token": "18e09c8c7724bfc8-8bd6-24" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["accept", "*/*"], + ["host", "127.0.0.1:56587"], + ["content-length", "7"] + ], + "method": "POST", + "target": "/overflow/18e09c8c7724bfc8-8bd6-24/streamed-7", + "version": "HTTP/1.1" + }, + "head_bytes": 145, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c7724bfc8-8bd6-24" + }, + { + "adapter": "axum", + "case": "streamed-8", + "client": { + "body": "{\"case\":\"streamed-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 6355042, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6346167, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "105"], + ["date", "Wed, 21 Oct 2026 17:46:18 GMT"] + ], + "status": 200, + "token": "18e09c8c7e37b928-8bd6-25" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["accept", "*/*"], + ["host", "127.0.0.1:56590"], + ["content-length", "8"] + ], + "method": "POST", + "target": "/overflow/18e09c8c7e37b928-8bd6-25/streamed-8", + "version": "HTTP/1.1" + }, + "head_bytes": 145, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c7e37b928-8bd6-25" + }, + { + "adapter": "axum", + "case": "streamed-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 359542, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 338917, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 17:46:19 GMT"] + ], + "status": 413, + "token": "18e09c8c856700a0-8bd6-26" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c856700a0-8bd6-26" + }, + { + "adapter": "axum", + "case": "upstream-413", + "client": { + "body": "{\"case\":\"upstream-413\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":\"120\",\"upstream_status\":413}", + "complete_ns": 7452750, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7412333, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "108"], + ["date", "Wed, 21 Oct 2026 17:46:19 GMT"] + ], + "status": 200, + "token": "18e09c8c8c1bfb80-8bd6-27" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["accept", "*/*"], + ["host", "127.0.0.1:56595"], + ["content-length", "8"] + ], + "method": "POST", + "target": "/overflow/18e09c8c8c1bfb80-8bd6-27/upstream-413", + "version": "HTTP/1.1" + }, + "head_bytes": 147, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e09c8c8c1bfb80-8bd6-27" + } + ], + "final_client_completions": 40, + "cloudflare_diagnostics": { + "source": { + "path": "/private/tmp/edgezero-overflow-all-20261021-c/overflow-cloudflare/runtime.log", + "bytes": 5851, + "sha256": "ee187e21f95fe9436bcf21e6cc2923caa9c6dbf9806b7ec1c65a849a4c2cc089" + }, + "dispatch_records": [ + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "18e09c8b80167cd0-8bd6-a" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "18e09c8b80167cd0-8bd6-a" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "18e09c8b96b6c508-8bd6-b" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "18e09c8b96b6c508-8bd6-b" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "18e09c8b971a8c00-8bd6-c" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 413, + "token": "18e09c8b971a8c00-8bd6-c" + }, + { + "case": "buffered-8", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "18e09c8b9f068fe0-8bd6-e" + }, + { + "case": "buffered-8", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "18e09c8b9f068fe0-8bd6-e" + }, + { + "case": "buffered-9", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "18e09c8ba620ff68-8bd6-f" + }, + { + "case": "buffered-9", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 413, + "token": "18e09c8ba620ff68-8bd6-f" + }, + { + "case": "streamed-7", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "18e09c8bac85f9d0-8bd6-10" + }, + { + "case": "streamed-7", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "18e09c8bac85f9d0-8bd6-10" + }, + { + "case": "streamed-8", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "18e09c8bb3886cb8-8bd6-11" + }, + { + "case": "streamed-8", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "18e09c8bb3886cb8-8bd6-11" + }, + { + "case": "streamed-9", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "18e09c8bba640268-8bd6-12" + }, + { + "case": "streamed-9", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 413, + "token": "18e09c8bba640268-8bd6-12" + }, + { + "case": "upstream-413", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "18e09c8bc12d0e00-8bd6-13" + }, + { + "case": "upstream-413", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "18e09c8bc12d0e00-8bd6-13" + } + ], + "finding": "Ingress1025 returns413; first following validPOST receives proxy500 with no fixture entry or origin contact. Succeeding requests enter normally. Native connection-pool mechanism unproven." + }, + "control_inputs": [ + { + "path": "/private/tmp/edgezero-overflow-cf-control.mjs", + "bytes": 2972, + "sha256": "1a8ca5da37d0562e52fb692822b568c25c53e8c1933ee806f131daffea5fd450", + "source": "import { spawn } from 'node:child_process';\nimport net from 'node:net';\nimport { setTimeout as delay } from 'node:timers/promises';\n\nconst port = 55881;\nconst proc = spawn('/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler', [\n 'dev', ...(process.env.CONTROL_SCRIPT ? [process.env.CONTROL_SCRIPT] : []), '--config', '/private/tmp/edgezero-overflow-all-20261021-b/overflow-cloudflare/wrangler.toml',\n '--port', String(port), '--ip', '127.0.0.1', '--local', '--persist-to', '/private/tmp/edgezero-overflow-cf-control-state'\n], { env: { ...process.env, WRANGLER_SEND_METRICS: 'false', WRANGLER_LOG_PATH: '/private/tmp/edgezero-pr275-wrangler-logs' }});\nlet logs = '';\nfor (const stream of [proc.stdout, proc.stderr]) stream.on('data', bytes => {\n if (logs.length < 65536) logs += bytes.toString();\n});\nasync function request(label, length) {\n const payload = length === null ? '' : `${length.toString(16)}\\r\\n${'x'.repeat(length)}\\r\\n0\\r\\n\\r\\n`;\n const wire = `POST /progress/overflow/inbound HTTP/1.1\\r\\nHost: 127.0.0.1:${port}\\r\\nx-request-token: ${label}\\r\\nConnection: close\\r\\n${length === null ? 'Content-Length: 0' : 'Transfer-Encoding: chunked'}\\r\\n\\r\\n${payload}`;\n const result = await new Promise((resolve, reject) => {\n const socket = net.connect({ host: '127.0.0.1', port });\n let reply = '';\n socket.setTimeout(6000, () => socket.destroy(new Error('diagnostic timeout')));\n socket.on('connect', () => socket.write(wire));\n socket.on('data', bytes => {\n reply += bytes.toString();\n if (reply.length > 8192) socket.destroy(new Error('diagnostic capture bound'));\n const boundary = reply.indexOf('\\r\\n\\r\\n');\n if (boundary < 0) return;\n const head = reply.slice(0, boundary);\n const body = reply.slice(boundary + 4);\n const length = /content-length: (\\d+)/i.exec(head);\n if ((length && Buffer.byteLength(body) >= Number(length[1])) || (!length && body.endsWith('\\r\\n0\\r\\n\\r\\n'))) {\n resolve(reply);\n socket.destroy();\n }\n });\n socket.on('error', reject);\n socket.on('end', () => resolve(reply));\n });\n console.log(JSON.stringify({ label, length, reply: result }));\n}\ntry {\n for (let index = 0; index < 150 && !logs.includes('Ready on'); index++) {\n if (proc.exitCode !== null) throw new Error('runtime exited');\n await delay(100);\n }\n if (!logs.includes('Ready on')) throw new Error('runtime startup timeout');\n for (const [label, length] of [\n ['baseline', 1024], ['overflow-a', 1025], ['after-a', null], ['recover-a', null],\n ['overflow-b', 1025], ['after-b', 1], ['recover-b', 1],\n ['overflow-c', 1025], ['after-c', 1024], ['recover-c', 1024]\n ]) await request(label, length);\n} finally {\n proc.kill('SIGTERM');\n await Promise.race([new Promise(resolve => proc.once('exit', resolve)), delay(5000)]);\n if (proc.exitCode === null && proc.signalCode === null) proc.kill('SIGKILL');\n console.log(JSON.stringify({ event: 'runtime-log', logs }));\n}\n" + }, + { + "path": "/private/tmp/edgezero-overflow-cf-sdkless.mjs", + "bytes": 928, + "sha256": "54d218caa546f4b6e80fc355e053fde5708afebf79cb2499a3d5bb8cd4575b94", + "source": "export default {\n async fetch(request) {\n const token = request.headers.get('x-request-token');\n console.log(JSON.stringify({ event: 'control-entry', token }));\n let size = 0;\n if (request.body) {\n const reader = request.body.getReader();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > 1024) {\n reader.cancel().catch(() => {});\n console.log(JSON.stringify({ event: 'control-return', token, status: 413 }));\n return new Response(JSON.stringify({ error: { kind: 'payload_too_large', message: 'request body too large', status: 413 } }), { status: 413 });\n }\n }\n reader.releaseLock();\n }\n console.log(JSON.stringify({ event: 'control-return', token, status: 200 }));\n return Response.json({ case: 'inbound', received: size, qualification: false });\n }\n};\n" + } + ], + "bounds": { + "origin_workers": 1, + "retained_connections": 2, + "extra_connection_witness": 1, + "metadata_bytes": 16384, + "head_field_count": 100, + "origin_body_capture_bytes": 256, + "origin_body_witness_bytes": 1, + "socket_absolute_budget_ms": 2000, + "finish_observation_ms": 100, + "client_body_capture_bytes": 4096, + "client_read_deadline_ms": 6000, + "finish": "After grace, stop accepts at collector barrier and join the one worker; two already accepted socket budgets may remain. Third contact fails the run.", + "headless": "Only one explicitly recorded zero-head-byte/zero-body-byte EOF or reset is a pre-head abort witness for Fastly/Spin streamed overflow. It is TCP contact, not no-contact.", + "timing": "Client header/completion times only; first-byte time deliberately null." + }, + "limitations": [ + "Cloudflare matrix fails and remains open; no retries, recovery sleep, drain or capability promotion.", + "Streamed response heads only; bodies dropped unpolled, so Fastly G2 remains failed and unchanged.", + "The finite origin observation does not certify permanent host cancellation or whole-runtime allocations.", + "Generated-app fallback TCP, hosted FastlyHTTP2, TS end-to-end and later composed headers remain open.", + "JS-only control is diagnostic, not an F2 acceptance substitute. No EdgeZero/Rust/WASM worker is used in that control." + ], + "verification": { + "root_fmt": "pass", + "root_clippy": "pass", + "root_tests": "pass; 793 core tests", + "root_feature_check": "pass", + "root_spin_check": "pass", + "fixture_core_tests": 17, + "fixture_harness_tests": 51, + "fixture_clippy": "pass; core/harness and three real WASM targets" + }, + "control_observation": { + "command": "CONTROL_SCRIPT=/private/tmp/edgezero-overflow-cf-sdkless.mjs node /private/tmp/edgezero-overflow-cf-control.mjs", + "qualification": false, + "status": "diagnostic-reproduced", + "observations": [ + { + "label": "baseline", + "length": 1024, + "reply": "HTTP/1.1 200 OK\r\nContent-Length: 56\r\nContent-Type: application/json\r\n\r\n{\"case\":\"inbound\",\"received\":1024,\"qualification\":false}" + }, + { + "label": "overflow-a", + "length": 1025, + "reply": "HTTP/1.1 413 Payload Too Large\r\nContent-Length: 86\r\nContent-Type: text/plain;charset=UTF-8\r\n\r\n{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}" + }, + { + "label": "after-a", + "length": null, + "reply": "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 201\r\nContent-Type: text/plain;charset=UTF-8\r\n\r\nError: Network connection lost.\n at async Object.fetch (file:///Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/miniflare/dist/src/workers/core/entry.worker.js:5216:22)" + }, + { + "label": "recover-a", + "length": null, + "reply": "HTTP/1.1 200 OK\r\nContent-Length: 53\r\nContent-Type: application/json\r\n\r\n{\"case\":\"inbound\",\"received\":0,\"qualification\":false}" + }, + { + "label": "overflow-b", + "length": 1025, + "reply": "HTTP/1.1 413 Payload Too Large\r\nContent-Length: 86\r\nContent-Type: text/plain;charset=UTF-8\r\n\r\n{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}" + }, + { + "label": "after-b", + "length": 1, + "reply": "HTTP/1.1 200 OK\r\nContent-Length: 53\r\nContent-Type: application/json\r\n\r\n{\"case\":\"inbound\",\"received\":1,\"qualification\":false}" + }, + { + "label": "recover-b", + "length": 1, + "reply": "HTTP/1.1 200 OK\r\nContent-Length: 53\r\nContent-Type: application/json\r\n\r\n{\"case\":\"inbound\",\"received\":1,\"qualification\":false}" + }, + { + "label": "overflow-c", + "length": 1025, + "reply": "HTTP/1.1 413 Payload Too Large\r\nContent-Length: 86\r\nContent-Type: text/plain;charset=UTF-8\r\n\r\n{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}" + }, + { + "label": "after-c", + "length": 1024, + "reply": "HTTP/1.1 200 OK\r\nContent-Length: 56\r\nContent-Type: application/json\r\n\r\n{\"case\":\"inbound\",\"received\":1024,\"qualification\":false}" + }, + { + "label": "recover-c", + "length": 1024, + "reply": "HTTP/1.1 200 OK\r\nContent-Length: 56\r\nContent-Type: application/json\r\n\r\n{\"case\":\"inbound\",\"received\":1024,\"qualification\":false}" + } + ], + "runtime_log": "\n ⛅️ wrangler 4.131.0 (update available 4.151.3)\n───────────────────────────────────────────────\nYour Worker has access to the following bindings:\nBinding Resource Mode\nenv.fixture_config (00000000000000000000000000000001) KV Namespace local\nenv.fixture_kv (00000000000000000000000000000002) KV Namespace local\nenv.FIXTURE_MODE (\"retained\") Environment Variable local\nenv.fixture_marker (\"fixture-only-value\") Environment Variable local\n\n⎔ Starting local server...\n\u001b[33m▲ \u001b[43;33m[\u001b[43;30mWARNING\u001b[43;33m]\u001b[0m \u001b[1mservice core:user:reusable-app-fixture: The compatibility flag enable_weak_ref became the default as of 2025-05-05 so does not need to be specified anymore.\u001b[0m\n\n\nWrangler detected this dev session is running in an AI agent.\nThe Local Explorer API is available at http://127.0.0.1:55881/cdn-cgi/local/explorer/api\nUseful routes:\n GET http://127.0.0.1:55881/cdn-cgi/local/explorer/api/local/workers - local Workers and bindings\n GET http://127.0.0.1:55881/cdn-cgi/local/explorer/api/storage/kv/namespaces - KV namespaces\n GET http://127.0.0.1:55881/cdn-cgi/local/explorer/api/d1/database - D1 databases\n GET http://127.0.0.1:55881/cdn-cgi/local/explorer/api/r2/buckets - R2 buckets\n GET http://127.0.0.1:55881/cdn-cgi/local/explorer/api/workers/durable_objects/namespaces - Durable Object namespaces\n GET http://127.0.0.1:55881/cdn-cgi/local/explorer/api/workflows - Workflows\n POST http://127.0.0.1:55881/cdn-cgi/local/explorer/api/local/observability/query - run a read-only SQL query (SELECT/WITH only) over captured request traces and console logs. Tables: spans, logs (read attributes via json(attributes)). Example:\n curl -X POST http://127.0.0.1:55881/cdn-cgi/local/explorer/api/local/observability/query -H 'Content-Type: application/json' -d '{\"sql\":\"SELECT service, name, outcome, duration_ms FROM spans WHERE parent_id IS NULL LIMIT 20\"}'\nIf the routes above don't cover what you need, fetch the full OpenAPI schema (large - use only as a last resort):\n GET http://127.0.0.1:55881/cdn-cgi/local/explorer/api - OpenAPI schema\n[wrangler:info] Ready on http://127.0.0.1:55881\n{\"event\":\"control-entry\",\"token\":\"baseline\"}\n{\"event\":\"control-return\",\"token\":\"baseline\",\"status\":200}\n[wrangler:info] POST /progress/overflow/inbound 200 OK (103ms)\n{\"event\":\"control-entry\",\"token\":\"overflow-a\"}\n{\"event\":\"control-return\",\"token\":\"overflow-a\",\"status\":413}\n[wrangler:info] POST /progress/overflow/inbound 413 Payload Too Large (8ms)\n\u001b[31m✘ \u001b[41;31m[\u001b[41;97mERROR\u001b[41;31m]\u001b[0m \u001b[1mError inside ProxyWorker (the affected request failed; the dev server continues): POST http://127.0.0.1:55881/progress/overflow/inbound (failed after 1 attempt): Network connection lost.\u001b[0m\n\n\n{\"event\":\"control-entry\",\"token\":\"recover-a\"}\n{\"event\":\"control-return\",\"token\":\"recover-a\",\"status\":200}\n[wrangler:info] POST /progress/overflow/inbound 200 OK (8ms)\n{\"event\":\"control-entry\",\"token\":\"overflow-b\"}\n{\"event\":\"control-return\",\"token\":\"overflow-b\",\"status\":413}\n[wrangler:info] POST /progress/overflow/inbound 413 Payload Too Large (6ms)\n{\"event\":\"control-entry\",\"token\":\"after-b\"}\n{\"event\":\"control-return\",\"token\":\"after-b\",\"status\":200}\n[wrangler:info] POST /progress/overflow/inbound 200 OK (8ms)\n{\"event\":\"control-entry\",\"token\":\"recover-b\"}\n{\"event\":\"control-return\",\"token\":\"recover-b\",\"status\":200}\n[wrangler:info] POST /progress/overflow/inbound 200 OK (10ms)\n{\"event\":\"control-entry\",\"token\":\"overflow-c\"}\n{\"event\":\"control-return\",\"token\":\"overflow-c\",\"status\":413}\n[wrangler:info] POST /progress/overflow/inbound 413 Payload Too Large (5ms)\n{\"event\":\"control-entry\",\"token\":\"after-c\"}\n{\"event\":\"control-return\",\"token\":\"after-c\",\"status\":200}\n[wrangler:info] POST /progress/overflow/inbound 200 OK (6ms)\n{\"event\":\"control-entry\",\"token\":\"recover-c\"}\n{\"event\":\"control-return\",\"token\":\"recover-c\",\"status\":200}\n[wrangler:info] POST /progress/overflow/inbound 200 OK (6ms)\n🪵 Logs were written to \"/private/tmp/edgezero-pr275-wrangler-logs/wrangler-2026-10-21_17-46-28_512.log\"\n", + "finding": "Three oversized requests returned413; one following validPOST failed500 without control-entry; two following validPOSTs passed. Every recovery probe returned200. Intermittency is retained, not retried or waived." + } +} diff --git a/docs/audits/evidence/2026-10-21-overflow-boundaries.json b/docs/audits/evidence/2026-10-21-overflow-boundaries.json new file mode 100644 index 000000000..d28199ee7 --- /dev/null +++ b/docs/audits/evidence/2026-10-21-overflow-boundaries.json @@ -0,0 +1,2902 @@ +{ + "schema": 1, + "recorded_date": "2026-10-21", + "base_revision": "cda324d433877e5b46599aee0cd685df72372c68", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "status": "exact-local-boundaries-pass-cloudflare-recovery-open", + "scope": "EZ-10 acceptance 3 exact 2 KiB/1 KiB and Axum pipelining; acceptance 7 built-in JSON extractor/custom rendering; bounded collector Drop proof; local Cloudflare topology diagnosis only", + "production_runtime_logic_changed": false, + "original_record": { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/docs/audits/evidence/2026-10-21-outbound-overflow.json", + "bytes": 95534, + "sha256": "54d6d9919579b0d26f10776196238414e3f9c5b0e6a92b600c4a668de32787e1" + }, + "frozen_api_plan": { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md", + "bytes": 65279, + "sha256": "c52b4aea6b8d2793f30784e88016a3676088d344c0a5607774a5d9f1a7ffae80" + }, + "requirements": { + "path": "/Users/ag/projects/stackpop/prebid/edgezero/.claude/worktrees/pr-275-outbound-http/docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md", + "bytes": 389929, + "sha256": "5d9833ebb13bf191377eeec6f9ada89d236044546701d01478c7f12f65b175ec" + }, + "source_snapshot_scope": "Selected checkout inputs at record creation, not a full frozen candidate. Runtime-relevant fixture sources match final E; subsequent Axum test-only extensions are verified separately, not included in fixture release builds.", + "source_sha256": { + "Cargo.toml": "527a6f29fd231afb35dea70ba3ec41b9c9e78ce52324cb20fe9ed6c75afb415b", + "Cargo.lock": "7f65719e408c31d4e6c32f2e168b00ace4f73de412ca6b5469b831bd151c04ac", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net/overflow.rs": "85fd43259be523fa4eeb063aece98ad2bba641b0bf59516138abca88f63bd4bc", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/overflow.rs": "9fb78e32a856905e2f5958b50f1e33a129778c7c2c0aab6321fc5a9fbe6e3e8e", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/overflow.rs": "f9fa0f6b9417af2a59bf5ed60fbf4799c81d86b3457343620ebd2e8874f143ee", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs": "0ec7229f6a64b08b293a8138eda4bcb3ed711abd77dd2ab65227fb9ee3ed5cb0", + "crates/edgezero-core/src/body.rs": "304908d06d0cd7a1d846e3a6d36dc08e4e498728e06ae7c36050b8389f8fbb39", + "crates/edgezero-core/src/context.rs": "c9b9cca8120b1f81ba5e101272b9df654f459edbf10ea33759cfffd558b347ed", + "crates/edgezero-core/src/error.rs": "81d3b708862a18571235baa4dd03cd580b52af6807d092ba22f780ed28c22534", + "crates/edgezero-adapter-axum/src/connection.rs": "57553dc5c72d82cfc2d63fdbb2cef98b863833216be0b6dc32f19ab1ef706f18", + "crates/edgezero-adapter-axum/src/request.rs": "0c61c652c7e3455d35b9802b26b6118b68c678bdfe01cc1215c7a9ec931cddef", + "crates/edgezero-adapter-axum/src/outbound.rs": "5815144bd9720706379f90d2aa8aa2988af393e6e84360098e568f1c2079eb30", + "crates/edgezero-adapter-cloudflare/src/request.rs": "a755088e4e65e5af813335283ec3095c35b5982d1e9057ce58dd748aa03e6200", + "crates/edgezero-adapter-cloudflare/src/outbound.rs": "eedcf2bfdab0cd8d7d4aaf7ca6a7a41dfd10309566b9c0239e9e6ce0f6ac3392", + "crates/edgezero-adapter-fastly/src/request.rs": "c49b75fd52abacc1b2fb012cc357b3a0ce11f9b6a76397bef0f40114ab79d684", + "crates/edgezero-adapter-fastly/src/outbound.rs": "17edf76e5ca1dadc05f4013f282e443ee1309c4530c54f65412afa01d8463f40", + "crates/edgezero-adapter-spin/src/request.rs": "962e413a2009c82259405628b39976e4040dde6d1425ff5adfaf6ab0865b9d67", + "crates/edgezero-adapter-spin/src/outbound.rs": "8d98cff3dc724bf6a6c4c3b6b805b3bfb7631e3ff52cd3716745682170e19a61" + }, + "sdk_versions": { + "fastly": "0.13.1", + "worker": "0.8.5", + "spin_sdk": "7.0.0", + "reqwest": "0.13.4", + "miniflare": "5.20260910.0-alpha", + "workerd": "1.20260910.1" + }, + "tools": { + "WASM_BINDGEN_BIN": { + "path": "/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen", + "bytes": 6938208, + "sha256": "b86e33b3c64362c44d8807ed35797e6b8220dd0232b6b19743caf7c2c68a2284", + "version": "wasm-bindgen 0.2.129" + }, + "WORKER_BUILD_BIN": { + "path": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build", + "bytes": 6149664, + "sha256": "dd7716c2d74637b5d8d2d3e4887252c7c3b8619f549cfb79e5715159bc86c2b0", + "version": "0.8.5" + }, + "ESBUILD_BIN": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild", + "bytes": 10573778, + "sha256": "e2dc9a52440a2a34f09434a2f4843cb1e30f84e40dcf238976ec61ef8cd7f36a", + "version": "0.28.1" + }, + "WASM_OPT_BIN": { + "path": "/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt", + "bytes": 1676584, + "sha256": "fa49ec32a92b72b368b66f8f19d84e9adffeef002449e6a07cd95a3ed2612b41", + "version": "wasm-opt version 130 (version_130)" + }, + "WRANGLER_BIN": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "bytes": 3088, + "sha256": "780661a508810f3b65786895b1ca9aacbc4f55d329ae6b8c1e49ec8433569f77", + "version": "4.131.0" + }, + "workerd": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64/bin/workerd", + "bytes": 156374824, + "sha256": "eb132e3e489b47d804cc70b510a6da8f82dabec4748cdcdaba32a1a1c5c2e285", + "version": "workerd 2026-09-10", + "package_version": "1.20260910.1", + "identification_basis": "Wrangler's bundled workerd package default executable; MINIFLARE_WORKERD_PATH unset; package resolution and executable version verified, not an independent per-process attestation" + } + }, + "cloudflare_compatibility": { + "date": "2026-04-22", + "flags": ["enable_request_signal", "enable_weak_ref"] + }, + "matrix_command": "env SSL_CERT_FILE=/etc/ssl/cert.pem WASM_BINDGEN_BIN=/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen WORKER_BUILD_BIN=/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build ESBUILD_BIN=/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild WASM_OPT_BIN=/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt WRANGLER_BIN=/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler WRANGLER_LOG_PATH=/private/tmp/edgezero-pr275-wrangler-logs WRANGLER_SEND_METRICS=false CARGO_NET_OFFLINE=true scripts/smoke_test_reusable_app.sh --adapter all --suite outbound-overflow --output /private/tmp/edgezero-overflow-all-20261021-e", + "matrix": { + "output": "/private/tmp/edgezero-overflow-all-20261021-e", + "exit_code": 1, + "status": "failed-cloudflare-recovery-open", + "qualification": false, + "runtime_versions": [ + { + "adapter": "fastly", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "revision": "cda324d433877e5b46599aee0cd685df72372c68", + "source_state": "working-tree-characterization", + "version": "viceroy 0.21.0" + }, + { + "adapter": "cloudflare", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "revision": "cda324d433877e5b46599aee0cd685df72372c68", + "source_state": "working-tree-characterization", + "version": "4.131.0" + }, + { + "adapter": "spin", + "event": "progress-runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "revision": "cda324d433877e5b46599aee0cd685df72372c68", + "source_state": "working-tree-characterization", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + }, + { + "adapter": "axum", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo", + "revision": "cda324d433877e5b46599aee0cd685df72372c68", + "source_state": "working-tree-characterization", + "version": "cargo 1.98.1 (797e8a9bc 2026-08-05)" + } + ], + "raw": [ + { + "path": "/private/tmp/edgezero-overflow-all-20261021-e/events.jsonl", + "bytes": 69817, + "sha256": "f181ba0d596b9278a55642e35d606bf6ad8ee0711b6a013469209dc9b2821565" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-e/summary.json", + "bytes": 303, + "sha256": "9eabb63004810306629cff689f20a92743b4784c11fe0bdac11418150747c002" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-e/Cargo.lock", + "bytes": 84127, + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-e.log", + "bytes": 3894, + "sha256": "1a978a7f8868f9429819fe52dfc2d57d0b3a868373c6123885ac5a764bf0a051" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-e/overflow-axum/runtime.log", + "bytes": 604, + "sha256": "d0ad2b9d072146f1ba29fa30737144b642f84b0ae3a5fd8b39676e3f8b3948bb" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-e/overflow-cloudflare/runtime.log", + "bytes": 6201, + "sha256": "3e4ee510c36fa5c945cade8d3505775afd6ceb2ed67d1d9415f239fc055bf091" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-e/overflow-fastly/runtime.log", + "bytes": 331, + "sha256": "d9f7110a48a874bb23878548f32feab0b80b40fdaca57fa93cead170ff2ee0fc" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-e/overflow-spin/runtime.log", + "bytes": 1790, + "sha256": "27a9b071cfd01e2d89b9441bba8298681f02210132c4c0e3596f1da443fb6393" + } + ], + "artifacts": [ + { + "adapter": "fastly", + "path": "/private/tmp/edgezero-overflow-all-20261021-e/overflow-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "bytes": 2190860, + "sha256": "66308adb388c4fa8c56de94cd6a0ab599cbe25146f33cba8276e181dc93cac7f" + }, + { + "adapter": "cloudflare", + "path": "/private/tmp/edgezero-overflow-all-20261021-e/overflow-cloudflare/artifacts/build/index_bg.wasm", + "bytes": 1718831, + "sha256": "53ddbfa80f8e6559f5db930f591db0ae0cf16b9c8bfb890bc773bf23254f4aa3" + }, + { + "adapter": "spin", + "path": "/private/tmp/edgezero-overflow-all-20261021-e/overflow-spin/artifacts/fixture_spin.wasm", + "bytes": 2341405, + "sha256": "6a6f7a511755c2dc2230c810c9c7202326d3e29ebcfaadf830098c3b512873a6" + }, + { + "adapter": "axum", + "path": "/private/tmp/edgezero-overflow-all-20261021-e/overflow-axum/artifacts/fixture-axum-outbound-progress", + "bytes": 8933824, + "sha256": "88ed8eaa05919d7e557b216315a9cdefe0c5ad345911d791239a7cd8e97c1a22" + } + ], + "coverage": [ + { + "adapter": "fastly", + "attempted": 11, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "attempted": 11, + "event": "overflow-coverage", + "qualification": false, + "status": "fail" + }, + { + "adapter": "spin", + "attempted": 11, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "attempted": 11, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + } + ], + "cases": [ + { + "adapter": "fastly", + "case": "inbound-1023", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1023}", + "complete_ns": 8094333, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 8075083, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:21 GMT"] + ], + "status": 200, + "token": "18e0a114933deb80-343f-0" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a114933deb80-343f-0" + }, + { + "adapter": "fastly", + "case": "inbound-1024", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1024}", + "complete_ns": 273916, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 266958, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:21 GMT"] + ], + "status": 200, + "token": "18e0a11493c07578-343f-1" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a11493c07578-343f-1" + }, + { + "adapter": "fastly", + "case": "inbound-1025", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 262583, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 255417, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["date", "Wed, 21 Oct 2026 19:09:21 GMT"] + ], + "status": 413, + "token": "18e0a11493cb0498-343f-2" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a11493cb0498-343f-2" + }, + { + "adapter": "fastly", + "case": "buffered-7", + "client": { + "body": "{\"case\":\"buffered-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 5664959, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 5658875, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:21 GMT"] + ], + "status": 200, + "token": "18e0a11493d597a0-343f-3" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:52077"], + ["content-length", "7"] + ], + "method": "POST", + "target": "/overflow/18e0a11493d597a0-343f-3/buffered-7", + "version": "HTTP/1.1" + }, + "head_bytes": 131, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a11493d597a0-343f-3" + }, + { + "adapter": "fastly", + "case": "buffered-8", + "client": { + "body": "{\"case\":\"buffered-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7810792, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7718750, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:21 GMT"] + ], + "status": 200, + "token": "18e0a1149ae38020-343f-4" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:52080"], + ["content-length", "8"] + ], + "method": "POST", + "target": "/overflow/18e0a1149ae38020-343f-4/buffered-8", + "version": "HTTP/1.1" + }, + "head_bytes": 131, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a1149ae38020-343f-4" + }, + { + "adapter": "fastly", + "case": "buffered-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 470959, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 459459, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["date", "Wed, 21 Oct 2026 19:09:21 GMT"] + ], + "status": 413, + "token": "18e0a114a1d0dc98-343f-5" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a114a1d0dc98-343f-5" + }, + { + "adapter": "fastly", + "case": "streamed-7", + "client": { + "body": "{\"case\":\"streamed-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7869250, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7861500, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:21 GMT"] + ], + "status": 200, + "token": "18e0a114a85c8558-343f-6" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:52085"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e0a114a85c8558-343f-6/streamed-7", + "version": "HTTP/1.1" + }, + "head_bytes": 140, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a114a85c8558-343f-6" + }, + { + "adapter": "fastly", + "case": "streamed-8", + "client": { + "body": "{\"case\":\"streamed-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7887625, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7876875, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:21 GMT"] + ], + "status": 200, + "token": "18e0a114afa0b460-343f-7" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:52088"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e0a114afa0b460-343f-7/streamed-8", + "version": "HTTP/1.1" + }, + "head_bytes": 140, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a114afa0b460-343f-7" + }, + { + "adapter": "fastly", + "case": "streamed-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 645750, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 571583, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["date", "Wed, 21 Oct 2026 19:09:21 GMT"] + ], + "status": 413, + "token": "18e0a114b6d61a40-343f-8" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 0, + "body_prefix": [], + "head": null, + "head_bytes": 0, + "head_state": "absent", + "terminal": "eof" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a114b6d61a40-343f-8" + }, + { + "adapter": "fastly", + "case": "upstream-413", + "client": { + "body": "{\"case\":\"upstream-413\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":\"120\",\"upstream_status\":413}", + "complete_ns": 5310166, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 5254166, + "headers": [ + ["content-type", "application/json"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:21 GMT"] + ], + "status": 200, + "token": "18e0a114bd4af328-343f-9" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:52094"], + ["content-length", "8"] + ], + "method": "POST", + "target": "/overflow/18e0a114bd4af328-343f-9/upstream-413", + "version": "HTTP/1.1" + }, + "head_bytes": 133, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a114bd4af328-343f-9" + }, + { + "adapter": "fastly", + "case": "inbound-2048", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 528500, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 517750, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["date", "Wed, 21 Oct 2026 19:09:22 GMT"] + ], + "status": 413, + "token": "18e0a114c47d3458-343f-a" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a114c47d3458-343f-a" + }, + { + "adapter": "cloudflare", + "case": "inbound-1023", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1023}", + "complete_ns": 106547875, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 105807834, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e0a115baff0630-343f-b" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a115baff0630-343f-b" + }, + { + "adapter": "cloudflare", + "case": "inbound-1024", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1024}", + "complete_ns": 2386917, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 2187042, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e0a115c161a708-343f-c" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a115c161a708-343f-c" + }, + { + "adapter": "cloudflare", + "case": "inbound-1025", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 2836250, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 2748166, + "headers": [ + ["content-length", "86"], + ["content-type", "application/json"] + ], + "status": 413, + "token": "18e0a115c18ce940-343f-d" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a115c18ce940-343f-d" + }, + { + "adapter": "cloudflare", + "case": "buffered-7", + "client": { + "body": "Error: Network connection lost.\n at async Object.fetch (file:///Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/miniflare/dist/src/workers/core/entry.worker.js:5216:22)", + "complete_ns": 6118625, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6104250, + "headers": [ + ["content-length", "201"], + ["content-type", "text/plain;charset=UTF-8"] + ], + "status": 500, + "token": "18e0a115c1bf2888-343f-e" + }, + "error": "overflow probe status mismatch", + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "fail", + "token": "18e0a115c1bf2888-343f-e" + }, + { + "adapter": "cloudflare", + "case": "buffered-8", + "client": { + "body": "{\"case\":\"buffered-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7870500, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7462333, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e0a115c8f60568-343f-f" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["content-length", "8"], + ["host", "127.0.0.1:52137"], + ["accept-encoding", "identity"], + ["cache-control", "no-cache"], + ["pragma", "no-cache"], + ["cf-worker", "reusable-app-fixture.example.com"] + ], + "method": "POST", + "target": "/overflow/18e0a115c8f60568-343f-f/buffered-8", + "version": "HTTP/1.1" + }, + "head_bytes": 219, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a115c8f60568-343f-f" + }, + { + "adapter": "cloudflare", + "case": "buffered-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 3031417, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 2433375, + "headers": [ + ["content-length", "86"], + ["content-type", "application/json"] + ], + "status": 413, + "token": "18e0a115cf89d030-343f-10" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a115cf89d030-343f-10" + }, + { + "adapter": "cloudflare", + "case": "streamed-7", + "client": { + "body": "{\"case\":\"streamed-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7929416, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7549750, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e0a115d68c5a88-343f-11" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["content-length", "7"], + ["host", "127.0.0.1:52143"], + ["accept-encoding", "identity"], + ["cache-control", "no-cache"], + ["pragma", "no-cache"], + ["cf-worker", "reusable-app-fixture.example.com"] + ], + "method": "POST", + "target": "/overflow/18e0a115d68c5a88-343f-11/streamed-7", + "version": "HTTP/1.1" + }, + "head_bytes": 220, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a115d68c5a88-343f-11" + }, + { + "adapter": "cloudflare", + "case": "streamed-8", + "client": { + "body": "{\"case\":\"streamed-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 8446459, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 8045042, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e0a115ddd62710-343f-12" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["content-length", "8"], + ["host", "127.0.0.1:52146"], + ["accept-encoding", "identity"], + ["cache-control", "no-cache"], + ["pragma", "no-cache"], + ["cf-worker", "reusable-app-fixture.example.com"] + ], + "method": "POST", + "target": "/overflow/18e0a115ddd62710-343f-12/streamed-8", + "version": "HTTP/1.1" + }, + "head_bytes": 220, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a115ddd62710-343f-12" + }, + { + "adapter": "cloudflare", + "case": "streamed-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 4119833, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 3951250, + "headers": [ + ["content-length", "86"], + ["content-type", "application/json"] + ], + "status": 413, + "token": "18e0a115e5282928-343f-13" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a115e5282928-343f-13" + }, + { + "adapter": "cloudflare", + "case": "upstream-413", + "client": { + "body": "{\"case\":\"upstream-413\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":\"120\",\"upstream_status\":413}", + "complete_ns": 6682083, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6406708, + "headers": [ + ["transfer-encoding", "chunked"], + ["content-type", "application/json"] + ], + "status": 200, + "token": "18e0a115ec1d49e8-343f-14" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["content-length", "8"], + ["host", "127.0.0.1:52158"], + ["accept-encoding", "identity"], + ["cache-control", "no-cache"], + ["pragma", "no-cache"], + ["cf-worker", "reusable-app-fixture.example.com"] + ], + "method": "POST", + "target": "/overflow/18e0a115ec1d49e8-343f-14/upstream-413", + "version": "HTTP/1.1" + }, + "head_bytes": 222, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a115ec1d49e8-343f-14" + }, + { + "adapter": "cloudflare", + "case": "inbound-2048", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 3202167, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 3033458, + "headers": [ + ["content-length", "86"], + ["content-type", "application/json"] + ], + "status": 413, + "token": "18e0a115f326c598-343f-15" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a115f326c598-343f-15" + }, + { + "adapter": "spin", + "case": "inbound-1023", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1023}", + "complete_ns": 27063417, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 27048125, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:27 GMT"] + ], + "status": 200, + "token": "18e0a1160a6418f0-343f-16" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a1160a6418f0-343f-16" + }, + { + "adapter": "spin", + "case": "inbound-1024", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1024}", + "complete_ns": 527042, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 520250, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:27 GMT"] + ], + "status": 200, + "token": "18e0a1160c09ed38-343f-17" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a1160c09ed38-343f-17" + }, + { + "adapter": "spin", + "case": "inbound-1025", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 510625, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 501625, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 19:09:27 GMT"] + ], + "status": 413, + "token": "18e0a1160c188f50-343f-18" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a1160c188f50-343f-18" + }, + { + "adapter": "spin", + "case": "buffered-7", + "client": { + "body": "{\"case\":\"buffered-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 8639541, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 8633833, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:27 GMT"] + ], + "status": 200, + "token": "18e0a1160c282398-343f-19" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:52178"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e0a1160c282398-343f-19/buffered-7", + "version": "HTTP/1.1" + }, + "head_bytes": 141, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a1160c282398-343f-19" + }, + { + "adapter": "spin", + "case": "buffered-8", + "client": { + "body": "{\"case\":\"buffered-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 5699166, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 5692125, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:27 GMT"] + ], + "status": 200, + "token": "18e0a116135724c0-343f-1a" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:52181"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e0a116135724c0-343f-1a/buffered-8", + "version": "HTTP/1.1" + }, + "head_bytes": 141, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a116135724c0-343f-1a" + }, + { + "adapter": "spin", + "case": "buffered-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 742791, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 730041, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 19:09:27 GMT"] + ], + "status": 413, + "token": "18e0a1161a2a84e0-343f-1b" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a1161a2a84e0-343f-1b" + }, + { + "adapter": "spin", + "case": "streamed-7", + "client": { + "body": "{\"case\":\"streamed-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7397000, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7383375, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:28 GMT"] + ], + "status": 200, + "token": "18e0a11620f05c28-343f-1c" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:52190"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e0a11620f05c28-343f-1c/streamed-7", + "version": "HTTP/1.1" + }, + "head_bytes": 141, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a11620f05c28-343f-1c" + }, + { + "adapter": "spin", + "case": "streamed-8", + "client": { + "body": "{\"case\":\"streamed-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 6627542, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6612958, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:28 GMT"] + ], + "status": 200, + "token": "18e0a11628462ae8-343f-1d" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:52193"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e0a11628462ae8-343f-1d/streamed-8", + "version": "HTTP/1.1" + }, + "head_bytes": 141, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a11628462ae8-343f-1d" + }, + { + "adapter": "spin", + "case": "streamed-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 1663041, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 1639750, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 19:09:28 GMT"] + ], + "status": 413, + "token": "18e0a1162f4682c0-343f-1e" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 0, + "body_prefix": [], + "head": null, + "head_bytes": 0, + "head_state": "absent", + "terminal": "eof" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a1162f4682c0-343f-1e" + }, + { + "adapter": "spin", + "case": "upstream-413", + "client": { + "body": "{\"case\":\"upstream-413\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":\"120\",\"upstream_status\":413}", + "complete_ns": 6954792, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6939875, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["transfer-encoding", "chunked"], + ["date", "Wed, 21 Oct 2026 19:09:28 GMT"] + ], + "status": 200, + "token": "18e0a11636192760-343f-1f" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["host", "127.0.0.1:52203"], + ["transfer-encoding", "chunked"] + ], + "method": "POST", + "target": "/overflow/18e0a11636192760-343f-1f/upstream-413", + "version": "HTTP/1.1" + }, + "head_bytes": 143, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a11636192760-343f-1f" + }, + { + "adapter": "spin", + "case": "inbound-2048", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 1944458, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 1908917, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 19:09:28 GMT"] + ], + "status": 413, + "token": "18e0a1163d23f6e8-343f-20" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a1163d23f6e8-343f-20" + }, + { + "adapter": "axum", + "case": "inbound-1023", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1023}", + "complete_ns": 887083, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 853833, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "56"], + ["date", "Wed, 21 Oct 2026 19:09:33 GMT"] + ], + "status": 200, + "token": "18e0a11756bc4c30-343f-21" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a11756bc4c30-343f-21" + }, + { + "adapter": "axum", + "case": "inbound-1024", + "client": { + "body": "{\"case\":\"inbound\",\"qualification\":false,\"received\":1024}", + "complete_ns": 226792, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 210834, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "56"], + ["date", "Wed, 21 Oct 2026 19:09:33 GMT"] + ], + "status": 200, + "token": "18e0a11756e89fd8-343f-22" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a11756e89fd8-343f-22" + }, + { + "adapter": "axum", + "case": "inbound-1025", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 192209, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 176209, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 19:09:33 GMT"] + ], + "status": 413, + "token": "18e0a1175704f9a8-343f-23" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a1175704f9a8-343f-23" + }, + { + "adapter": "axum", + "case": "buffered-7", + "client": { + "body": "{\"case\":\"buffered-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7784875, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7774625, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "105"], + ["date", "Wed, 21 Oct 2026 19:09:33 GMT"] + ], + "status": 200, + "token": "18e0a117572114f8-343f-24" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["accept", "*/*"], + ["host", "127.0.0.1:52251"], + ["content-length", "7"] + ], + "method": "POST", + "target": "/overflow/18e0a117572114f8-343f-24/buffered-7", + "version": "HTTP/1.1" + }, + "head_bytes": 145, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a117572114f8-343f-24" + }, + { + "adapter": "axum", + "case": "buffered-8", + "client": { + "body": "{\"case\":\"buffered-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 5631125, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 5612000, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "105"], + ["date", "Wed, 21 Oct 2026 19:09:33 GMT"] + ], + "status": 200, + "token": "18e0a1175e4033b8-343f-25" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["accept", "*/*"], + ["host", "127.0.0.1:52254"], + ["content-length", "8"] + ], + "method": "POST", + "target": "/overflow/18e0a1175e4033b8-343f-25/buffered-8", + "version": "HTTP/1.1" + }, + "head_bytes": 145, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a1175e4033b8-343f-25" + }, + { + "adapter": "axum", + "case": "buffered-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 256417, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 243959, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 19:09:33 GMT"] + ], + "status": 413, + "token": "18e0a11764b30ce8-343f-26" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a11764b30ce8-343f-26" + }, + { + "adapter": "axum", + "case": "streamed-7", + "client": { + "body": "{\"case\":\"streamed-7\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 7151167, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 7136708, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "105"], + ["date", "Wed, 21 Oct 2026 19:09:33 GMT"] + ], + "status": 200, + "token": "18e0a1176bc162c8-343f-27" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 7, + "body_prefix": [120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["accept", "*/*"], + ["host", "127.0.0.1:52261"], + ["content-length", "7"] + ], + "method": "POST", + "target": "/overflow/18e0a1176bc162c8-343f-27/streamed-7", + "version": "HTTP/1.1" + }, + "head_bytes": 145, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a1176bc162c8-343f-27" + }, + { + "adapter": "axum", + "case": "streamed-8", + "client": { + "body": "{\"case\":\"streamed-8\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":null,\"upstream_status\":200}", + "complete_ns": 6656125, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6642333, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "105"], + ["date", "Wed, 21 Oct 2026 19:09:33 GMT"] + ], + "status": 200, + "token": "18e0a11772e42ef0-343f-28" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["accept", "*/*"], + ["host", "127.0.0.1:52264"], + ["content-length", "8"] + ], + "method": "POST", + "target": "/overflow/18e0a11772e42ef0-343f-28/streamed-8", + "version": "HTTP/1.1" + }, + "head_bytes": 145, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a11772e42ef0-343f-28" + }, + { + "adapter": "axum", + "case": "streamed-9", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 305333, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 285083, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 19:09:33 GMT"] + ], + "status": 413, + "token": "18e0a11779fce128-343f-29" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 0, + "capture_bytes": 256, + "collection_complete": true, + "connections": [], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a11779fce128-343f-29" + }, + { + "adapter": "axum", + "case": "upstream-413", + "client": { + "body": "{\"case\":\"upstream-413\",\"outcome\":\"response\",\"qualification\":false,\"retry_after\":\"120\",\"upstream_status\":413}", + "complete_ns": 6640333, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 6631958, + "headers": [ + ["content-type", "application/json"], + ["connection", "close"], + ["content-length", "108"], + ["date", "Wed, 21 Oct 2026 19:09:34 GMT"] + ], + "status": 200, + "token": "18e0a11780427480-343f-2a" + }, + "error": null, + "event": "overflow-case", + "origin": { + "accepted": 1, + "capture_bytes": 256, + "collection_complete": true, + "connections": [ + { + "body_bytes": 8, + "body_prefix": [120, 120, 120, 120, 120, 120, 120, 120], + "head": { + "headers": [ + ["accept-encoding", "identity"], + ["accept", "*/*"], + ["host", "127.0.0.1:52269"], + ["content-length", "8"] + ], + "method": "POST", + "target": "/overflow/18e0a11780427480-343f-2a/upstream-413", + "version": "HTTP/1.1" + }, + "head_bytes": 147, + "head_state": "complete", + "reply_written": true, + "terminal": "complete" + } + ], + "origin_deadline_ms": 2000, + "overflow": false + }, + "origin_collection_error": null, + "qualification": false, + "status": "pass", + "token": "18e0a11780427480-343f-2a" + }, + { + "adapter": "axum", + "case": "inbound-2048", + "client": { + "body": "{\"error\":{\"kind\":\"payload_too_large\",\"message\":\"request body too large\",\"status\":413}}", + "complete_ns": 318542, + "event": "client_completed", + "first_byte_ns": null, + "first_byte_timing": "not-measured", + "header_ns": 299000, + "headers": [ + ["content-type", "application/json"], + ["content-length", "86"], + ["connection", "close"], + ["date", "Wed, 21 Oct 2026 19:09:34 GMT"] + ], + "status": 413, + "token": "18e0a11787b79ab0-343f-2b" + }, + "error": null, + "event": "overflow-case", + "qualification": false, + "status": "pass", + "token": "18e0a11787b79ab0-343f-2b" + } + ] + }, + "earlier_expanded_run": { + "output": "/private/tmp/edgezero-overflow-all-20261021-d", + "exit_code": 1, + "status": "failed-cloudflare-recovery-open", + "qualification": false, + "runtime_versions": [ + { + "adapter": "fastly", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "revision": "cda324d433877e5b46599aee0cd685df72372c68", + "source_state": "working-tree-characterization", + "version": "viceroy 0.21.0" + }, + { + "adapter": "cloudflare", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "revision": "cda324d433877e5b46599aee0cd685df72372c68", + "source_state": "working-tree-characterization", + "version": "4.131.0" + }, + { + "adapter": "spin", + "event": "progress-runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "revision": "cda324d433877e5b46599aee0cd685df72372c68", + "source_state": "working-tree-characterization", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + }, + { + "adapter": "axum", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo", + "revision": "cda324d433877e5b46599aee0cd685df72372c68", + "source_state": "working-tree-characterization", + "version": "cargo 1.98.1 (797e8a9bc 2026-08-05)" + } + ], + "raw": [ + { + "path": "/private/tmp/edgezero-overflow-all-20261021-d/events.jsonl", + "bytes": 69823, + "sha256": "bd4dc40f2c6c91008fe1f11f3a91a29e99edaf9eea6fef9067d1a90fd1b8d0ba" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-d/summary.json", + "bytes": 303, + "sha256": "9eabb63004810306629cff689f20a92743b4784c11fe0bdac11418150747c002" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-d/Cargo.lock", + "bytes": 84127, + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-d.log", + "bytes": 4964, + "sha256": "b940f4757a7797cc36aaa03bd183f66a4ce8a001974b1a692581e105551f1ad6" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-d/overflow-axum/runtime.log", + "bytes": 604, + "sha256": "f2b54fbc853c97531161a1ac6db42ce22420034a80982ed99679f2f005adb09e" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-d/overflow-cloudflare/runtime.log", + "bytes": 6201, + "sha256": "a631338d3b784e24924f7e45a3eb591465df23278fe3d4b5323504d227bf4c6b" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-d/overflow-fastly/runtime.log", + "bytes": 331, + "sha256": "d2ca7ca2174b83f362cd2442cae4fe3bcf433ddd1915a96a8a6ec948abc75fc0" + }, + { + "path": "/private/tmp/edgezero-overflow-all-20261021-d/overflow-spin/runtime.log", + "bytes": 1790, + "sha256": "dc661bcb239d42fe544fe62e072ea55c0cfb7b63d73e03583da1d8055a4487a8" + } + ], + "coverage": [ + { + "adapter": "fastly", + "attempted": 11, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "attempted": 11, + "event": "overflow-coverage", + "qualification": false, + "status": "fail" + }, + { + "adapter": "spin", + "attempted": 11, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "attempted": 11, + "event": "overflow-coverage", + "qualification": false, + "status": "pass" + } + ] + }, + "controls": { + "source_inputs": [ + { + "path": "/private/tmp/edgezero-overflow-cf-isolate.mjs", + "bytes": 6887, + "sha256": "f4d630f6e72fe7d299985d7b753b46a169aa0e6069aa4d7602ccba9afdfa28d8", + "text": "import { spawn } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport net from 'node:net';\nimport { readdir, open } from 'node:fs/promises';\nimport { setTimeout as delay } from 'node:timers/promises';\n\nconst mode = process.env.CONTROL_MODE ?? 'wrangler';\nconst connection = process.env.CONTROL_CONNECTION ?? 'close';\nif (!['wrangler', 'wrangler-user', 'miniflare', 'direct'].includes(mode) || !['close', 'omit'].includes(connection)) {\n throw new Error('invalid diagnostic mode');\n}\nconst script = process.env.CONTROL_SCRIPT ?? '/private/tmp/edgezero-overflow-cf-sdkless.mjs';\nlet port = 55881;\nlet child;\nlet miniflare;\nlet logs = '';\nfunction append(bytes) { logs += String(bytes).slice(0, Math.max(0, 65536 - logs.length)); }\n\nasync function start() {\n if (!mode.startsWith('wrangler')) {\n const require = createRequire('/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/package.json');\n const { Miniflare, Log, LogLevel, convertV4MiniflareOptions } = require('miniflare');\n class CaptureLog extends Log {\n log(message) { append(`${message}\\n`); }\n }\n miniflare = new Miniflare(convertV4MiniflareOptions({\n name: 'cancellation-control', modules: true, scriptPath: script,\n compatibilityDate: '2026-04-22', compatibilityFlags: ['enable_request_signal', 'enable_weak_ref'],\n host: '127.0.0.1', port: mode === 'direct' ? 55882 : port,\n unsafeDirectSockets: mode === 'direct' ? [{ host: '127.0.0.1', port }] : undefined,\n cf: false, log: new CaptureLog(LogLevel.DEBUG),\n }));\n await miniflare.ready;\n return;\n }\n const directory = '/private/tmp/edgezero-pr275-wrangler-logs';\n const before = new Set(await readdir(directory));\n child = spawn('/Users/ag/.asdf/installs/nodejs/24.12.0/bin/wrangler', [\n 'dev', script, '--config', '/private/tmp/edgezero-overflow-all-20261021-b/overflow-cloudflare/wrangler.toml',\n '--port', String(port), '--ip', '127.0.0.1', '--local',\n '--persist-to', '/private/tmp/edgezero-overflow-cf-control-state',\n ], { env: { ...process.env, WRANGLER_SEND_METRICS: 'false', WRANGLER_LOG_PATH: '/private/tmp/edgezero-pr275-wrangler-logs' } });\n child.stdout.on('data', append);\n child.stderr.on('data', append);\n for (let index = 0; index < 150 && !logs.includes('Ready on'); index++) {\n if (child.exitCode !== null) throw new Error('runtime exited');\n await delay(100);\n }\n if (!logs.includes('Ready on')) throw new Error('runtime startup timeout');\n if (mode === 'wrangler-user') {\n const files = (await readdir(directory)).filter(file => !before.has(file) && /^wrangler-.*\\.log$/.test(file));\n if (files.length !== 1) throw new Error('ambiguous runtime log');\n const handle = await open(`${directory}/${files[0]}`, 'r');\n let text;\n try {\n const buffer = Buffer.alloc(65536);\n const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);\n text = buffer.subarray(0, bytesRead).toString();\n } finally { await handle.close(); }\n const matches = [...text.matchAll(/\"userWorkerUrl\":\\{\"protocol\":\"http:\",\"hostname\":\"127\\.0\\.0\\.1\",\"port\":\"(\\d+)\"\\}/g)];\n if (matches.length !== 1) throw new Error('ambiguous user worker address');\n port = Number(matches[0][1]);\n console.log(JSON.stringify({ event: 'diagnostic-boundary', mode, port, qualification: false }));\n }\n}\n\nfunction complete(bytes) {\n const boundary = bytes.indexOf('\\r\\n\\r\\n');\n if (boundary < 0) return false;\n const head = bytes.subarray(0, boundary).toString('latin1');\n const body = bytes.subarray(boundary + 4);\n const length = /^content-length: (\\d+)\\r?$/im.exec(head);\n if (length) return body.length === Number(length[1]);\n if (!/^transfer-encoding: chunked\\r?$/im.test(head)) throw new Error('unframed diagnostic response');\n let position = 0;\n while (true) {\n const end = body.indexOf('\\r\\n', position);\n if (end < 0) return false;\n const line = body.subarray(position, end).toString('ascii');\n if (!/^[0-9a-f]+$/i.test(line)) throw new Error('invalid diagnostic chunk');\n const length = Number.parseInt(line, 16);\n position = end + 2;\n if (body.length < position + length + 2) return false;\n if (body.subarray(position + length, position + length + 2).toString() !== '\\r\\n') throw new Error('invalid diagnostic framing');\n position += length + 2;\n if (length === 0) return body.length === position;\n }\n}\n\nasync function request(label, length) {\n const payload = length === null ? '' : `${length.toString(16)}\\r\\n${'x'.repeat(length)}\\r\\n0\\r\\n\\r\\n`;\n const wire = `POST /progress/overflow/inbound HTTP/1.1\\r\\nHost: 127.0.0.1:${port}\\r\\nx-request-token: ${label}\\r\\n${connection === 'close' ? 'Connection: close\\r\\n' : ''}${length === null ? 'Content-Length: 0' : 'Transfer-Encoding: chunked'}\\r\\n\\r\\n${payload}`;\n const started = performance.now();\n try {\n const reply = await new Promise((resolve, reject) => {\n const socket = net.connect({ host: '127.0.0.1', port });\n let bytes = Buffer.alloc(0);\n const timer = setTimeout(() => socket.destroy(new Error('absolute diagnostic deadline')), 6000);\n socket.once('close', () => clearTimeout(timer));\n socket.once('connect', () => socket.write(wire));\n socket.on('data', chunk => {\n if (bytes.length + chunk.length > 8192) return socket.destroy(new Error('diagnostic capture bound'));\n bytes = Buffer.concat([bytes, chunk]);\n try {\n if (complete(bytes)) { resolve(bytes.toString()); socket.destroy(); }\n } catch (error) { socket.destroy(error); }\n });\n socket.once('error', reject);\n socket.once('end', () => {\n if (complete(bytes)) resolve(bytes.toString());\n else reject(new Error('incomplete diagnostic response'));\n });\n });\n console.log(JSON.stringify({ event: 'control-request', mode, connection, label, length, elapsed_ms: performance.now() - started, reply }));\n } catch (error) {\n console.log(JSON.stringify({ event: 'control-request', mode, connection, label, length, error: String(error) }));\n }\n}\n\ntry {\n await start();\n for (const [label, length] of [\n ['baseline', 1024], ['overflow-a', 1025], ['after-a', null], ['recover-a', null],\n ['overflow-b', 1025], ['after-b', 1], ['recover-b', 1],\n ['overflow-c', 1025], ['after-c', 1024], ['recover-c', 1024],\n ]) await request(label, length);\n} finally {\n if (miniflare) await miniflare.dispose();\n if (child) {\n const stopped = new Promise(resolve => child.once('close', resolve));\n child.kill('SIGTERM');\n let timer;\n await Promise.race([stopped, new Promise(resolve => { timer = setTimeout(resolve, 5000); })]);\n clearTimeout(timer);\n if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');\n await stopped;\n }\n console.log(JSON.stringify({ event: 'runtime-log', mode, connection, logs }));\n}\n" + }, + { + "path": "/private/tmp/edgezero-overflow-cf-sdkless.mjs", + "bytes": 928, + "sha256": "54d218caa546f4b6e80fc355e053fde5708afebf79cb2499a3d5bb8cd4575b94", + "text": "export default {\n async fetch(request) {\n const token = request.headers.get('x-request-token');\n console.log(JSON.stringify({ event: 'control-entry', token }));\n let size = 0;\n if (request.body) {\n const reader = request.body.getReader();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > 1024) {\n reader.cancel().catch(() => {});\n console.log(JSON.stringify({ event: 'control-return', token, status: 413 }));\n return new Response(JSON.stringify({ error: { kind: 'payload_too_large', message: 'request body too large', status: 413 } }), { status: 413 });\n }\n }\n reader.releaseLock();\n }\n console.log(JSON.stringify({ event: 'control-return', token, status: 200 }));\n return Response.json({ case: 'inbound', received: size, qualification: false });\n }\n};\n" + } + ], + "results": [ + { + "mode": "wrangler", + "connection": "close", + "script": "sdkless", + "exit_code": 0, + "qualification": false, + "command": "node /private/tmp/edgezero-overflow-cf-isolate.mjs", + "raw": { + "path": "/private/tmp/edgezero-overflow-cf-boundary-wrangler-close-final.jsonl", + "bytes": 8356, + "sha256": "17948227b7d3d632c7ad3b82685e9614467ee981cbf3c9c6fb3190ec5c104380" + }, + "requests": [ + { + "label": "baseline", + "length": 1024, + "status": 200, + "elapsed_ms": 17.977374999999938 + }, + { + "label": "overflow-a", + "length": 1025, + "status": 413, + "elapsed_ms": 1.9704169999999976 + }, + { + "label": "after-a", + "length": null, + "status": 500, + "elapsed_ms": 8.994374999999991 + }, + { + "label": "recover-a", + "length": null, + "status": 200, + "elapsed_ms": 1.244374999999991 + }, + { + "label": "overflow-b", + "length": 1025, + "status": 413, + "elapsed_ms": 1.6367079999999987 + }, + { + "label": "after-b", + "length": 1, + "status": 500, + "elapsed_ms": 1.0415840000000571 + }, + { + "label": "recover-b", + "length": 1, + "status": 200, + "elapsed_ms": 1.5214590000000499 + }, + { + "label": "overflow-c", + "length": 1025, + "status": 413, + "elapsed_ms": 2.1339159999999993 + }, + { + "label": "after-c", + "length": 1024, + "status": 500, + "elapsed_ms": 0.9695000000000391 + }, + { + "label": "recover-c", + "length": 1024, + "status": 200, + "elapsed_ms": 1.4685420000000704 + } + ], + "runtime_events": [ + { + "event": "control-entry", + "token": "baseline" + }, + { + "event": "control-return", + "token": "baseline", + "status": 200 + }, + { + "event": "control-entry", + "token": "overflow-a" + }, + { + "event": "control-return", + "token": "overflow-a", + "status": 413 + }, + { + "event": "control-entry", + "token": "recover-a" + }, + { + "event": "control-return", + "token": "recover-a", + "status": 200 + }, + { + "event": "control-entry", + "token": "overflow-b" + }, + { + "event": "control-return", + "token": "overflow-b", + "status": 413 + }, + { + "event": "control-entry", + "token": "recover-b" + }, + { + "event": "control-return", + "token": "recover-b", + "status": 200 + }, + { + "event": "control-entry", + "token": "overflow-c" + }, + { + "event": "control-return", + "token": "overflow-c", + "status": 413 + }, + { + "event": "control-entry", + "token": "recover-c" + }, + { + "event": "control-return", + "token": "recover-c", + "status": 200 + } + ], + "post_overflow_failures": 3, + "boundary": null + }, + { + "mode": "wrangler", + "connection": "omit", + "script": "sdkless", + "exit_code": 0, + "qualification": false, + "command": "CONTROL_CONNECTION=omit node /private/tmp/edgezero-overflow-cf-isolate.mjs", + "raw": { + "path": "/private/tmp/edgezero-overflow-cf-boundary-wrangler-omit-final.jsonl", + "bytes": 8342, + "sha256": "57b2941fafb105710df5d4f257d770395d0f18b0a3d61e9be655f80c51ff13fd" + }, + "requests": [ + { + "label": "baseline", + "length": 1024, + "status": 200, + "elapsed_ms": 8.012499999999932 + }, + { + "label": "overflow-a", + "length": 1025, + "status": 413, + "elapsed_ms": 1.8470829999999978 + }, + { + "label": "after-a", + "length": null, + "status": 500, + "elapsed_ms": 1.5463750000000118 + }, + { + "label": "recover-a", + "length": null, + "status": 200, + "elapsed_ms": 1.329291000000012 + }, + { + "label": "overflow-b", + "length": 1025, + "status": 413, + "elapsed_ms": 2.0001250000000255 + }, + { + "label": "after-b", + "length": 1, + "status": 500, + "elapsed_ms": 0.9927500000000009 + }, + { + "label": "recover-b", + "length": 1, + "status": 200, + "elapsed_ms": 1.7725840000000517 + }, + { + "label": "overflow-c", + "length": 1025, + "status": 413, + "elapsed_ms": 2.856208000000038 + }, + { + "label": "after-c", + "length": 1024, + "status": 500, + "elapsed_ms": 0.994792000000075 + }, + { + "label": "recover-c", + "length": 1024, + "status": 200, + "elapsed_ms": 1.4197499999999081 + } + ], + "runtime_events": [ + { + "event": "control-entry", + "token": "baseline" + }, + { + "event": "control-return", + "token": "baseline", + "status": 200 + }, + { + "event": "control-entry", + "token": "overflow-a" + }, + { + "event": "control-return", + "token": "overflow-a", + "status": 413 + }, + { + "event": "control-entry", + "token": "recover-a" + }, + { + "event": "control-return", + "token": "recover-a", + "status": 200 + }, + { + "event": "control-entry", + "token": "overflow-b" + }, + { + "event": "control-return", + "token": "overflow-b", + "status": 413 + }, + { + "event": "control-entry", + "token": "recover-b" + }, + { + "event": "control-return", + "token": "recover-b", + "status": 200 + }, + { + "event": "control-entry", + "token": "overflow-c" + }, + { + "event": "control-return", + "token": "overflow-c", + "status": 413 + }, + { + "event": "control-entry", + "token": "recover-c" + }, + { + "event": "control-return", + "token": "recover-c", + "status": 200 + } + ], + "post_overflow_failures": 3, + "boundary": null + }, + { + "mode": "wrangler-user", + "connection": "close", + "script": "sdkless", + "exit_code": 0, + "qualification": false, + "command": "CONTROL_MODE=wrangler-user node /private/tmp/edgezero-overflow-cf-isolate.mjs", + "raw": { + "path": "/private/tmp/edgezero-overflow-cf-boundary-user-sdkless-final.jsonl", + "bytes": 6934, + "sha256": "443d241b5a7a59d9531770344d2e36ce7615daf07adb346a8317c047227869d1" + }, + "requests": [ + { + "label": "baseline", + "length": 1024, + "status": 200, + "elapsed_ms": 22.604499999999916 + }, + { + "label": "overflow-a", + "length": 1025, + "status": 413, + "elapsed_ms": 1.9077499999999645 + }, + { + "label": "after-a", + "length": null, + "status": 200, + "elapsed_ms": 3.1105830000000196 + }, + { + "label": "recover-a", + "length": null, + "status": 200, + "elapsed_ms": 1.2927080000000615 + }, + { + "label": "overflow-b", + "length": 1025, + "status": 413, + "elapsed_ms": 1.9069159999999101 + }, + { + "label": "after-b", + "length": 1, + "status": 200, + "elapsed_ms": 1.6937920000000304 + }, + { + "label": "recover-b", + "length": 1, + "status": 200, + "elapsed_ms": 1.3045000000000755 + }, + { + "label": "overflow-c", + "length": 1025, + "status": 413, + "elapsed_ms": 1.1548329999999396 + }, + { + "label": "after-c", + "length": 1024, + "status": 200, + "elapsed_ms": 1.6749999999999545 + }, + { + "label": "recover-c", + "length": 1024, + "status": 200, + "elapsed_ms": 1.1340420000000222 + } + ], + "runtime_events": [ + { + "event": "control-entry", + "token": "baseline" + }, + { + "event": "control-return", + "token": "baseline", + "status": 200 + }, + { + "event": "control-entry", + "token": "overflow-a" + }, + { + "event": "control-return", + "token": "overflow-a", + "status": 413 + }, + { + "event": "control-entry", + "token": "after-a" + }, + { + "event": "control-return", + "token": "after-a", + "status": 200 + }, + { + "event": "control-entry", + "token": "recover-a" + }, + { + "event": "control-return", + "token": "recover-a", + "status": 200 + }, + { + "event": "control-entry", + "token": "overflow-b" + }, + { + "event": "control-return", + "token": "overflow-b", + "status": 413 + }, + { + "event": "control-entry", + "token": "after-b" + }, + { + "event": "control-return", + "token": "after-b", + "status": 200 + }, + { + "event": "control-entry", + "token": "recover-b" + }, + { + "event": "control-return", + "token": "recover-b", + "status": 200 + }, + { + "event": "control-entry", + "token": "overflow-c" + }, + { + "event": "control-return", + "token": "overflow-c", + "status": 413 + }, + { + "event": "control-entry", + "token": "after-c" + }, + { + "event": "control-return", + "token": "after-c", + "status": 200 + }, + { + "event": "control-entry", + "token": "recover-c" + }, + { + "event": "control-return", + "token": "recover-c", + "status": 200 + } + ], + "post_overflow_failures": 0, + "boundary": { + "event": "diagnostic-boundary", + "mode": "wrangler-user", + "port": 52719, + "qualification": false + } + }, + { + "mode": "wrangler-user", + "connection": "close", + "script": "edgezero", + "exit_code": 0, + "qualification": false, + "command": "CONTROL_MODE=wrangler-user CONTROL_SCRIPT=/private/tmp/edgezero-overflow-all-20261021-e/overflow-cloudflare/artifacts/build/worker/shim.mjs node /private/tmp/edgezero-overflow-cf-isolate.mjs", + "raw": { + "path": "/private/tmp/edgezero-overflow-cf-boundary-user-edgezero-final.jsonl", + "bytes": 8725, + "sha256": "e28205350a26adf77544f80dde0df0adf7064271ef6cb4f408f2e4046d5d049d" + }, + "requests": [ + { + "label": "baseline", + "length": 1024, + "status": 200, + "elapsed_ms": 39.223499999999945 + }, + { + "label": "overflow-a", + "length": 1025, + "status": 413, + "elapsed_ms": 3.5598340000000235 + }, + { + "label": "after-a", + "length": null, + "status": 200, + "elapsed_ms": 6.690249999999992 + }, + { + "label": "recover-a", + "length": null, + "status": 200, + "elapsed_ms": 1.7121670000000222 + }, + { + "label": "overflow-b", + "length": 1025, + "status": 413, + "elapsed_ms": 1.6609589999999343 + }, + { + "label": "after-b", + "length": 1, + "status": 200, + "elapsed_ms": 2.093958999999927 + }, + { + "label": "recover-b", + "length": 1, + "status": 200, + "elapsed_ms": 2.251207999999906 + }, + { + "label": "overflow-c", + "length": 1025, + "status": 413, + "elapsed_ms": 3.2323750000000473 + }, + { + "label": "after-c", + "length": 1024, + "status": 200, + "elapsed_ms": 2.422584000000029 + }, + { + "label": "recover-c", + "length": 1024, + "status": 200, + "elapsed_ms": 3.5342500000000427 + } + ], + "runtime_events": [ + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "baseline" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "baseline" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "overflow-a" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 413, + "token": "overflow-a" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "after-a" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "after-a" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "recover-a" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "recover-a" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "overflow-b" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 413, + "token": "overflow-b" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "after-b" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "after-b" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "recover-b" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "recover-b" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "overflow-c" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 413, + "token": "overflow-c" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "after-c" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "after-c" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "phase": "entry", + "qualification": false, + "token": "recover-c" + }, + { + "case": "inbound", + "event": "overflow-dispatch", + "outcome": "response", + "phase": "return", + "qualification": false, + "status": 200, + "token": "recover-c" + } + ], + "post_overflow_failures": 0, + "boundary": { + "event": "diagnostic-boundary", + "mode": "wrangler-user", + "port": 52749, + "qualification": false + } + } + ], + "earlier_observations": [ + { + "raw": { + "path": "/private/tmp/edgezero-overflow-cf-isolate-wrangler-omit.jsonl", + "bytes": 7708, + "sha256": "fdeedc69ed9677ac0cbc6172260d577eb54fd22dae884ba17a505aeec30bbf1c" + }, + "post_overflow_failures": 1, + "cycles": 3, + "note": "Earlier driver revision; retained to show intermittency, not replaced by final 3/3 failures." + }, + { + "raw": { + "path": "/private/tmp/edgezero-overflow-cf-isolate-miniflare-close.jsonl", + "bytes": 929, + "sha256": "417f0ed0d22ba23fcf2b395a399d9f127570e854a50d6e1c18e1505db9b84374" + }, + "exit_code": 1, + "status": "setup-failed-before-requests", + "qualification": false + }, + { + "raw": { + "path": "/private/tmp/edgezero-overflow-cf-isolate-miniflare-close-b.jsonl", + "bytes": 2304, + "sha256": "ecee5eaae84743f78c2482379a83a264569ffbb2e58f87381fec927a514eca22" + }, + "exit_code": 1, + "status": "setup-failed-before-requests", + "qualification": false + } + ], + "conclusion": "Observed failure narrows to the Wrangler front-proxy path: both header variants fail before worker entry, while SDKless and actual-artifact internal-listener controls recover. Native pooling/reuse mechanism unproven; direct listener is not the standard acceptance path and is not adopted as a repair." + }, + "verification": { + "fixture_test_command": "cargo test --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -p fixture-core --all-features", + "logs": [ + { + "path": "/private/tmp/edgezero-overflow-boundaries-fixture-tests-all.log", + "bytes": 6965, + "sha256": "ceef89bdb74a5aaa9d7bdb23598ea509e032659d323f56afe947f932df79c40f" + }, + { + "path": "/private/tmp/edgezero-overflow-boundaries-fixture-tests.log", + "bytes": 6073, + "sha256": "5296612d38967ed230a65dbf63c48b0c6d6f061be42dce7f00ac739891c59265" + }, + { + "path": "/private/tmp/edgezero-overflow-boundaries-fixture-clippy.log", + "bytes": 553, + "sha256": "2f7c0eadc991756a3a7cca02e0a00c31871ecd2ea7eb65002c0546452841f9b7" + }, + { + "path": "/private/tmp/edgezero-overflow-axum-boundaries-unit-c.log", + "bytes": 2005, + "sha256": "30dbf291f6f555838e1f1a12b19dd99695f49d10fcb1c740583d966246a46a47" + }, + { + "path": "/private/tmp/edgezero-overflow-boundaries-workspace-test-b.log", + "bytes": 175005, + "sha256": "39c57e2d67bced37c59c33a57fb24454a273b999b5ae4c6c48233ce2cc1530b2" + }, + { + "path": "/private/tmp/edgezero-overflow-boundaries-clippy-c.log", + "bytes": 621, + "sha256": "cb70face2abafb07e11cd901497ec844e9cf0e1db9e4cd4dec7dd5c0a3f06600" + }, + { + "path": "/private/tmp/edgezero-overflow-boundaries-feature-check-b.log", + "bytes": 621, + "sha256": "4fd9fdc56b449d306fd6ed89ecfd9fa70455826303f8e4c0806aab07ed676fb6" + }, + { + "path": "/private/tmp/edgezero-overflow-boundaries-spin-check.log", + "bytes": 382, + "sha256": "d0a142b8e9ccf91da4098e6e2f57d55c4d880a808977aed1bb4e8f385d783195" + }, + { + "path": "/private/tmp/edgezero-overflow-drop-mutation.log", + "bytes": 1211, + "sha256": "438dcd937402d030d7ce0622c021953733ff987721024c62e8f2f6dd30a686c4" + } + ], + "rust_gates": [ + "cargo fmt --all -- --check", + "cargo clippy --offline --locked --workspace --all-targets --all-features -- -D warnings", + "cargo test --offline --locked --workspace --all-targets", + "cargo check --offline --locked --workspace --all-targets --features \"fastly cloudflare spin\"", + "cargo check --offline --locked -p edgezero-adapter-spin --target wasm32-wasip2 --features spin" + ], + "cases": [ + "chunked_overflow_keeps_pipelined_request_boundaries_and_error_policy: original 9/8 and exact 2048/1024 over TCP", + "json_extractor_overflow_reaches_custom_renderer_without_retry_after: existing 8 MiB default cap plus one, valid json! string, typed inbound origin and custom wire body", + "dropping_origin_signals_stop_and_joins_the_worker_before_returning: stop signal, gated join, release and exit acknowledgement before assertions", + "two_kib_ingress_requires_fixed_overflow_without_retry_after: wrong status, case-insensitive retry header and changed public message rejected" + ], + "mutation": { + "change": "Temporarily remove UploadOrigin Drop join only; restored before final tests", + "exit_code": 101, + "expected_failure": "while_gated must be RecvTimeoutError::Timeout", + "restored_tests_exit_code": 0 + }, + "fixture_harness_tests": 53, + "fixture_core_tests": 17, + "axum_connection_tests": 14, + "core_tests": 793 + }, + "limitations": [ + "No hosted/protocol/context qualification or exact-commit release evidence. Temporary artifacts/logs are local, not durable release assets.", + "Cloudflare standard-entrypoint recovery remains failed. No POST retry, recovery sleep, body drain, SDK upgrade or topology workaround was adopted.", + "The SDKless control cancels but does not release the overflow reader lock; it is not a lifecycle-equivalent Rust control. The separate actual EdgeZero artifact differential retains its real cancel-on-drop and lock release.", + "The 50 ms gated negative Drop assertion remains scheduler-sensitive; workers are explicitly released and joined before assertions. Client write operations remain individually bounded, not one total exchange wall-clock guarantee.", + "Collector receipts cover a finite observation window, not indefinite provider cancellation. Existing source/capture/deadline bounds are unchanged.", + "Fastly G2 still fails on the pinned readiness primitive. This checkpoint does not implement the full head-first/eager/follow hard cut, alter approved APIs, remove dispatch slack or promote capabilities.", + "Hosted Fastly HTTP/2, consumer end-to-end and later synthesized-header composition legs remain open." + ] +} diff --git a/docs/audits/evidence/2026-10-23-assembly-response-headers.json b/docs/audits/evidence/2026-10-23-assembly-response-headers.json new file mode 100644 index 000000000..fd8edb401 --- /dev/null +++ b/docs/audits/evidence/2026-10-23-assembly-response-headers.json @@ -0,0 +1,1987 @@ +{ + "schema": 1, + "recorded_date": "2026-10-23", + "base_revision": "7fdef8b0a31f386f3c7e304ccf807f7ab500b19a", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "status": "local-assembly-exclusion-and-header-regression-pass", + "scope": "EZ-19.7 partial-configuration exclusion and successful recovered-policy delivery, alongside the repeated full F7b local HTTP1 matrix", + "production_runtime_delta_this_slice": false, + "source_snapshot_scope": "Selected local inputs; not a full frozen candidate or hosted qualification", + "source_sha256": { + "Cargo.lock": "ded1da41b9bd6fae689e16d277d076e975c8f3ccfa2842cd53addf30a5c3e27e", + "Cargo.toml": "527a6f29fd231afb35dea70ba3ec41b9c9e78ce52324cb20fe9ed6c75afb415b", + "crates/edgezero-adapter-axum/src/response.rs": "b5068969a307261af438a8cf5fcd15a43fe03a045b57a7231f88de1df84b1ae8", + "crates/edgezero-adapter-cloudflare/src/response.rs": "bd7980085cd771ed3940591f8501aa84facb35d4281e9177fe6a71bbaa20c57a", + "crates/edgezero-adapter-fastly/src/request.rs": "279751032c2b7fdfb6e1aeec20cd9f2853f46095865ce4504a88b0d4152eb06b", + "crates/edgezero-adapter-fastly/src/response.rs": "e1ddc3774504da3ffbc5b2ed18ffd732a486ac17e9dca96dc47d0083da4f0a0c", + "crates/edgezero-adapter-fastly/tests/contract.rs": "bae55aab4b21dc048ee4defd5d276ef43ff9517019e2cbfa9b0ddc64f88c74e5", + "crates/edgezero-adapter-spin/src/response.rs": "43be730fb6244ce244b6adce80da37731b741ff8475c0ecdc693fc64c13d4f2d", + "crates/edgezero-cli/src/templates/core/src/lib.rs.hbs": "ea0fc1570cd1a54e76c8918f79e1f198d457f6aa6318fbabe407125b581e0708", + "crates/edgezero-core/src/app.rs": "30082b21f9a8c32dcd337fd353ea6ead3957be077b821bc0cfcb252d40c4e25f", + "crates/edgezero-core/src/lib.rs": "9e8570286ab192744e8089a656163ccbaa4c1720a36c1110f49679c50f77f2a1", + "crates/edgezero-core/src/response_egress.rs": "665f5363ff4c003eac513f3262dd1797d9b269cd20189eadcfefac50624db452", + "crates/edgezero-core/src/response_egress_framing_test_support.rs": "9eb3a368d428ff7a8463613f2738e0281c34ebcdb6f02d364e6577188a3695d6", + "crates/edgezero-core/src/response_headers.rs": "6ce849b0c1e64cb34d1a4468f031b753d612f2f6554c9681088327c96f2e5f75", + "examples/app-demo/Cargo.lock": "d1dad662166f73b278b4ba0c33a017f3d30f4749f6e1a6a9a43fc1287c31f423", + "examples/app-demo/crates/app-demo-core/src/lib.rs": "ec9aa401fd348c35ccb76999b648f4bf97a35f32560a64abcad448d7e0eccb3a", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "tests/fixtures/reusable-app/Cargo.toml": "7665200757e4e565d11fa7eb036215aa583d21fd543bca39fdbdce470fee11ae", + "tests/fixtures/reusable-app/crates/fixture-axum/Cargo.toml": "23e14fd20cba550df413488ff746e9380ff6f09cc823e6d8bffcbca5e1b05695", + "tests/fixtures/reusable-app/crates/fixture-axum/src/bin/assembly.rs": "ca3c088d282d4d8ac37cd07f63a6515efd2969506be69a6194c52584bb3b7892", + "tests/fixtures/reusable-app/crates/fixture-axum/src/progress.rs": "1513e29d6b8fb8a16dbcbcec6e8119e73c764eb86f0635a97ddb86b4fa647d17", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs": "ae744bde0abe850ddbe18764e4d366ec11eb5df39c757d5ea816027b010c322a", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs": "a67f65fe9990ce4adba50ffe5e01b5bf50809492a2f222a3cbc5c726ac4ace96", + "tests/fixtures/reusable-app/crates/fixture-core/src/assembly.rs": "e542fa79c73c0a7598a90098d255e3bd1bb47052f89a569630545014c56bc03b", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs": "b8ce27cccb0949eab4a03b7694cbe528e028fc42ae4b14880f13bd68bc85f1b8", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs": "3ccea7b618f7ee3b9103bd5fd6ce597d65fa62fb2f118aabdb30c2544628c983", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/response_headers.rs": "e33aa3ca2375ac746900b75eecfbde33cf853e4777bd92f5b18300519a6bec9a", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/assembly.rs": "7125798a9976c44d433ce1f709325b5525a91880b516b83cbb031c964dee006c", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs": "057cd67c78725c19e24f2ce968138e3c38001c43ec656c015863834a0b87e706", + "tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs": "b8818ae4ebd1587674a17af9d642655304543e473275117d5a672c64c115ca06", + "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs": "723445e8bf33584832cbfc36dc04c049280bb4754c70b1f8ac22d9fa7fee6a75", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs": "734e56e9fa1beda4a4dbca09c7ac3914bd56c8bd33e4901e2db1c3626b99b9d0", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/assembly.rs": "b4efaafe9ea54a1ea7e26788b115774c0987bd7cfa7f44075f0c02b5e88bf117", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/framing.rs": "3cf08dbf5d6bcf45b7eb9f4793d6020bd3887d1af8f57a06353d2b3cfd9fa939", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/overflow.rs": "5597383d9e912ea20beda249e0b03c115055d31e459d6d9aa1aa48e3162761ea", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/progress.rs": "acfd1f42e71fbadc499df8db45f5852c1e7ef71944527a3c8479aecd06e5db36", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/response_headers.rs": "20cb1dfac7897ce11da86f22b67c3657b6a0fd3cb6ce5b3e980646f4ad2467ba", + "tests/fixtures/reusable-app/crates/fixture-spin/src/lib.rs": "6f5cfb313ec654d68455550dab6a439c946a29935a1798d1d0f233fafdcd9b8b", + "tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs": "47e765274ca5175ab5e80f6658e0c53fdfaf256edebb6a3c5337de9f2e138237" + }, + "predecessor_record": { + "path": "docs/audits/evidence/2026-10-23-synthesized-response-headers.json", + "bytes": 58788, + "sha256": "43d3e34fdc9a2301dc90657bf66b2eccbf038d1c5d32a395bb8a43290ea66e6f" + }, + "commands": [ + "CARGO_NET_OFFLINE=true scripts/smoke_test_reusable_app.sh --adapter all --suite assembly --output /private/tmp/edgezero-f7b-assembly-final", + "CARGO_NET_OFFLINE=true scripts/smoke_test_reusable_app.sh --adapter all --suite synthesized-headers --output /private/tmp/edgezero-f7b-headers-publish" + ], + "tool_overrides": { + "WORKER_BUILD_BIN": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build", + "WASM_BINDGEN_BIN": "/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen", + "ESBUILD_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild", + "WASM_OPT_BIN": "/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt", + "WRANGLER_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "WRANGLER_SEND_METRICS": "false", + "WRANGLER_LOG_PATH": "/private/tmp/edgezero-pr275-wrangler-logs", + "SSL_CERT_FILE": "/etc/ssl/cert.pem" + }, + "sdk_versions": { + "fastly": "0.13.1", + "worker": "0.8.5", + "spin_sdk": "7.0.0" + }, + "cloudflare_compatibility": { + "date": "2026-04-22", + "flags": ["enable_request_signal", "enable_weak_ref"] + }, + "runtime_versions": [ + { + "adapter": "fastly", + "event": "runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "expected": "0.21.0", + "version": "viceroy 0.21.0" + }, + { + "adapter": "cloudflare", + "event": "runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "version": "4.131.0" + }, + { + "adapter": "spin", + "event": "runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + }, + { + "adapter": "fastly", + "event": "runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "expected": "0.21.0", + "version": "viceroy 0.21.0" + }, + { + "adapter": "fastly", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "revision": "7fdef8b0a31f386f3c7e304ccf807f7ab500b19a", + "source_state": "working-tree-characterization", + "version": "viceroy 0.21.0" + }, + { + "adapter": "cloudflare", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "revision": "7fdef8b0a31f386f3c7e304ccf807f7ab500b19a", + "source_state": "working-tree-characterization", + "version": "4.131.0" + }, + { + "adapter": "spin", + "event": "progress-runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "revision": "7fdef8b0a31f386f3c7e304ccf807f7ab500b19a", + "source_state": "working-tree-characterization", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + }, + { + "adapter": "axum", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo", + "revision": "7fdef8b0a31f386f3c7e304ccf807f7ab500b19a", + "source_state": "working-tree-characterization", + "version": "cargo 1.98.1 (797e8a9bc 2026-08-05)" + } + ], + "artifacts": [ + { + "adapter": "fastly", + "variant": "assembly", + "path": "/private/tmp/edgezero-f7b-assembly-final/artifacts/fixture-fastly-assembly.wasm", + "bytes": 2152036, + "sha256": "b4c33fa10c069e4344003cf1bb70cd0a2a9054b94d265227962a5a214a6a19ac" + }, + { + "adapter": "cloudflare", + "path": "/private/tmp/edgezero-f7b-assembly-final/artifacts/build/index_bg.wasm", + "bytes": 1773375, + "sha256": "ca1d0753cba18579e83dfd8d5ebad2dab233e61569ffeab285710655123bda23" + }, + { + "adapter": "spin", + "path": "/private/tmp/edgezero-f7b-assembly-final/artifacts/fixture_spin.wasm", + "bytes": 2298315, + "sha256": "df1110c3dbf0c1ea5e54410640ace5017f0e0b424b92259406e5d8db7fcbb456" + }, + { + "adapter": "axum", + "variant": "assembly-startup", + "path": "/private/tmp/edgezero-f7b-assembly-final/artifacts/fixture-axum-assembly", + "bytes": 8539824, + "sha256": "0fd93d7a93c33acd32a92330f748168a965810103d687e251d47be30df1b231b" + }, + { + "adapter": "fastly", + "variant": "fastly-outbound-progress", + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "bytes": 2373005, + "sha256": "ccef13158ff5e4c5db87905251ba0b1b4089be8c58005f526a54d855f786e498" + }, + { + "adapter": "cloudflare", + "variant": "cloudflare-outbound-progress", + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/artifacts/build/index_bg.wasm", + "bytes": 1956518, + "sha256": "ae651cea9ab1afa01bd348d9399089a2174c924dec4663ed6a374fedf4c84847" + }, + { + "adapter": "spin", + "variant": "spin-outbound-progress", + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/artifacts/fixture_spin.wasm", + "bytes": 2618045, + "sha256": "c7bd5c59ec0e918a4c7e6f50b93b9c8624d99735bab357209937d1ffcdc3e0d0" + }, + { + "adapter": "axum", + "variant": "axum-outbound-progress", + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/artifacts/fixture-axum-outbound-progress", + "bytes": 9078304, + "sha256": "ecc403cdd0183ec2645ebf86934d093f51d828aac7a410d70c0222f602b1a2a5" + } + ], + "raw": [ + { + "path": "/private/tmp/edgezero-f7b-assembly-final/events.jsonl", + "bytes": 75530, + "sha256": "aac3350a27ea6b4e3b7b42728ae5b02043a6afe4d5000dfef00048f1c230ab52" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/summary.json", + "bytes": 31707, + "sha256": "c52b3d964eba611b018683574dc176421795f0c47379007bdfb12814074f47b5" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/Cargo.lock", + "bytes": 84127, + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final.log", + "bytes": 3027, + "sha256": "2f29456f518a2a09b2ad4ece8b66245ce76292681fdfb46dbfb67ed245baefa3" + }, + { + "path": "/private/tmp/edgezero-f7b-headers-publish/events.jsonl", + "bytes": 202279, + "sha256": "8691e70d81aa9892e43cea1c02ec0617880effb91c69682b6413d69fa3564ce8" + }, + { + "path": "/private/tmp/edgezero-f7b-headers-publish/summary.json", + "bytes": 302, + "sha256": "fdad4ea9024508071acff823ee80f31b68f6c2e1980faefaaff86076ffc842e3" + }, + { + "path": "/private/tmp/edgezero-f7b-headers-publish/Cargo.lock", + "bytes": 84127, + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + { + "path": "/private/tmp/edgezero-f7b-headers-publish.log", + "bytes": 3793, + "sha256": "46a3c1348ba23e692204fcf49bf14fd6a562bb9ef0a86d315fb9acccba425f19" + } + ], + "assembly_cases": [ + { + "adapter": "fastly", + "case": "standard-retained", + "method": "GET", + "token": "18e13c9ff9e16d18-142ba-0", + "http_status": 503, + "body_bytes": 83, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13c9ff9e16d18-142ba-0.http1", + "bytes": 208, + "sha256": "6780bab9b06582e9eb639732c41d68213ed9962638deaf404d3234f8e3dc05f2" + } + }, + { + "adapter": "fastly", + "case": "standard-retained", + "method": "GET", + "token": "18e13c9ffaa50f20-142ba-1", + "http_status": 200, + "body_bytes": 121, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13c9ffaa50f20-142ba-1.http1", + "bytes": 259, + "sha256": "593d254de5033ca892efeec42384d4ce2a544fa253993239c5279394838f2c04" + } + }, + { + "adapter": "fastly", + "case": "standard-retained", + "method": "GET", + "token": "18e13c9ffad21290-142ba-2", + "http_status": 200, + "body_bytes": 121, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13c9ffad21290-142ba-2.http1", + "bytes": 259, + "sha256": "ad92a0752b61b56194755d9006e00054790ccb537de321ee4d3d5fb3f2b44695" + } + }, + { + "adapter": "fastly", + "case": "standard-retained", + "method": "GET", + "token": "18e13c9ffafa9990-142ba-3", + "http_status": 404, + "body_bytes": 102, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [ + ["set-cookie", "assembly=retry-2; Path=/"], + ["x-assembly-synthesized", "retry-2"] + ], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13c9ffafa9990-142ba-3.http1", + "bytes": 289, + "sha256": "4b863d628ec6941a47d654432066ee680a55f2b2c6ac0b912e306ac4ff956cdb" + } + }, + { + "adapter": "fastly", + "case": "custom-retained", + "method": "GET", + "token": "18e13ca0093a4a28-142ba-4", + "http_status": 503, + "body_bytes": 91, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca0093a4a28-142ba-4.http1", + "bytes": 216, + "sha256": "e2e99c7b14e72bcc860df62095a10e2f317092ff162e3aebfef4231d383aad51" + } + }, + { + "adapter": "fastly", + "case": "custom-retained", + "method": "GET", + "token": "18e13ca0096ea480-142ba-5", + "http_status": 200, + "body_bytes": 120, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca0096ea480-142ba-5.http1", + "bytes": 258, + "sha256": "565d532f20dce9d4972cde935a4425607f89d16d9b29afd2c13497b33cba7c1f" + } + }, + { + "adapter": "fastly", + "case": "custom-retained", + "method": "GET", + "token": "18e13ca009a1f150-142ba-6", + "http_status": 200, + "body_bytes": 120, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca009a1f150-142ba-6.http1", + "bytes": 258, + "sha256": "1fcbd1b8806009404ab1dd9f26ff707428ffaa968a7467b03b2798c7f008f837" + } + }, + { + "adapter": "fastly", + "case": "custom-retained", + "method": "GET", + "token": "18e13ca009d2c938-142ba-7", + "http_status": 404, + "body_bytes": 102, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [ + ["set-cookie", "assembly=retry-2; Path=/"], + ["x-assembly-synthesized", "retry-2"] + ], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca009d2c938-142ba-7.http1", + "bytes": 289, + "sha256": "4b863d628ec6941a47d654432066ee680a55f2b2c6ac0b912e306ac4ff956cdb" + } + }, + { + "adapter": "fastly", + "case": "single-routes", + "method": "GET", + "token": "18e13ca0171393e8-142ba-8", + "http_status": 503, + "body_bytes": 83, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca0171393e8-142ba-8.http1", + "bytes": 208, + "sha256": "6780bab9b06582e9eb639732c41d68213ed9962638deaf404d3234f8e3dc05f2" + } + }, + { + "adapter": "fastly", + "case": "single-routes", + "method": "HEAD", + "token": "18e13ca0174594b0-142ba-9", + "http_status": 503, + "body_bytes": 0, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca0174594b0-142ba-9.http1", + "bytes": 105, + "sha256": "f1c8782b172376d93944809731f394d77f50491741a5c38b9f3fbd0542f0d492" + } + }, + { + "adapter": "fastly", + "case": "single-config", + "method": "GET", + "token": "18e13ca024c3be50-142ba-a", + "http_status": 503, + "body_bytes": 90, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca024c3be50-142ba-a.http1", + "bytes": 232, + "sha256": "38a35c254ce5046d518c763cf06f4398a99f92c840c67a95f54af9c08589deaa" + } + }, + { + "adapter": "fastly", + "case": "single-config", + "method": "HEAD", + "token": "18e13ca024f80138-142ba-b", + "http_status": 503, + "body_bytes": 0, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca024f80138-142ba-b.http1", + "bytes": 122, + "sha256": "4d089be143d4ae39e6ba48b5ce19d27673248debf4651d3df518bfa3d26c6583" + } + }, + { + "adapter": "fastly", + "case": "single-internal", + "method": "GET", + "token": "18e13ca033927818-142ba-c", + "http_status": 500, + "body_bytes": 76, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca033927818-142ba-c.http1", + "bytes": 203, + "sha256": "6eddf3b81daf490d85690746eb7762b420d3a5d02f383172e8e2f84716abe5ec" + } + }, + { + "adapter": "fastly", + "case": "single-internal", + "method": "HEAD", + "token": "18e13ca033be1fe0-142ba-d", + "http_status": 500, + "body_bytes": 0, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca033be1fe0-142ba-d.http1", + "bytes": 107, + "sha256": "f6d612037066b8843003325e607599ca864110092381cdbced492c25071ead5a" + } + }, + { + "adapter": "fastly", + "case": "single-hooks", + "method": "GET", + "token": "18e13ca041c26970-142ba-e", + "http_status": 503, + "body_bytes": 90, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca041c26970-142ba-e.http1", + "bytes": 232, + "sha256": "38a35c254ce5046d518c763cf06f4398a99f92c840c67a95f54af9c08589deaa" + } + }, + { + "adapter": "fastly", + "case": "single-hooks", + "method": "HEAD", + "token": "18e13ca041f5a6a0-142ba-f", + "http_status": 503, + "body_bytes": 0, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca041f5a6a0-142ba-f.http1", + "bytes": 122, + "sha256": "4d089be143d4ae39e6ba48b5ce19d27673248debf4651d3df518bfa3d26c6583" + } + }, + { + "adapter": "fastly", + "case": "single-config-explicit", + "method": "GET", + "token": "18e13ca04fbb4538-142ba-10", + "http_status": 503, + "body_bytes": 90, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca04fbb4538-142ba-10.http1", + "bytes": 232, + "sha256": "38a35c254ce5046d518c763cf06f4398a99f92c840c67a95f54af9c08589deaa" + } + }, + { + "adapter": "fastly", + "case": "single-config-explicit", + "method": "HEAD", + "token": "18e13ca04feeefc8-142ba-11", + "http_status": 503, + "body_bytes": 0, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca04feeefc8-142ba-11.http1", + "bytes": 122, + "sha256": "4d089be143d4ae39e6ba48b5ce19d27673248debf4651d3df518bfa3d26c6583" + } + }, + { + "adapter": "cloudflare", + "case": "single-config", + "method": "GET", + "token": "18e13ca155a9fd08-142ba-12", + "http_status": 503, + "body_bytes": 90, + "eof": true, + "capture_error": null, + "request_shutdown": "write-half-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca155a9fd08-142ba-12.http1", + "bytes": 195, + "sha256": "390ba2d501a43e70b5383ed9021b15db8678c5cd3d2de34986d38ffb923d85f6" + } + }, + { + "adapter": "cloudflare", + "case": "single-internal", + "method": "GET", + "token": "18e13ca15f2b1fd8-142ba-13", + "http_status": 500, + "body_bytes": 76, + "eof": true, + "capture_error": null, + "request_shutdown": "write-half-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca15f2b1fd8-142ba-13.http1", + "bytes": 166, + "sha256": "0146b8543437cbfddfee76618691afbc68ae72f549aad19f71ecd2ad588e8724" + } + }, + { + "adapter": "cloudflare", + "case": "single-config", + "method": "HEAD", + "token": "18e13ca15fb14798-142ba-14", + "http_status": 503, + "body_bytes": 0, + "eof": true, + "capture_error": null, + "request_shutdown": "write-half-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca15fb14798-142ba-14.http1", + "bytes": 105, + "sha256": "8374b6733b47d93d07aa78b9fdf1ab3b8911e491d36b2b9486fb6a95dd77c21f" + } + }, + { + "adapter": "cloudflare", + "case": "standard-retained", + "method": "GET", + "token": "18e13ca1600d07e0-142ba-15", + "http_status": 503, + "body_bytes": 83, + "eof": true, + "capture_error": null, + "request_shutdown": "write-half-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1600d07e0-142ba-15.http1", + "bytes": 171, + "sha256": "2b9f0fd508703306e11268582510144d4b1c59f5b11966c9ac422ee5d60f15c8" + } + }, + { + "adapter": "cloudflare", + "case": "single-config", + "method": "GET", + "token": "18e13ca1aadcb7e8-142ba-16", + "http_status": 503, + "body_bytes": 90, + "eof": true, + "capture_error": null, + "request_shutdown": "write-half-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1aadcb7e8-142ba-16.http1", + "bytes": 195, + "sha256": "390ba2d501a43e70b5383ed9021b15db8678c5cd3d2de34986d38ffb923d85f6" + } + }, + { + "adapter": "cloudflare", + "case": "single-internal", + "method": "GET", + "token": "18e13ca1b14f71d8-142ba-17", + "http_status": 500, + "body_bytes": 76, + "eof": true, + "capture_error": null, + "request_shutdown": "write-half-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1b14f71d8-142ba-17.http1", + "bytes": 166, + "sha256": "0146b8543437cbfddfee76618691afbc68ae72f549aad19f71ecd2ad588e8724" + } + }, + { + "adapter": "cloudflare", + "case": "single-config", + "method": "HEAD", + "token": "18e13ca1b1a51f70-142ba-18", + "http_status": 503, + "body_bytes": 0, + "eof": true, + "capture_error": null, + "request_shutdown": "write-half-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1b1a51f70-142ba-18.http1", + "bytes": 105, + "sha256": "8374b6733b47d93d07aa78b9fdf1ab3b8911e491d36b2b9486fb6a95dd77c21f" + } + }, + { + "adapter": "cloudflare", + "case": "standard-retained", + "method": "GET", + "token": "18e13ca1b1da0280-142ba-19", + "http_status": 503, + "body_bytes": 83, + "eof": true, + "capture_error": null, + "request_shutdown": "write-half-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1b1da0280-142ba-19.http1", + "bytes": 171, + "sha256": "2b9f0fd508703306e11268582510144d4b1c59f5b11966c9ac422ee5d60f15c8" + } + }, + { + "adapter": "cloudflare", + "case": "standard-retained", + "method": "GET", + "token": "18e13ca1b23322e8-142ba-1a", + "http_status": 200, + "body_bytes": 106, + "eof": true, + "capture_error": null, + "request_shutdown": "write-half-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1b23322e8-142ba-1a.http1", + "bytes": 207, + "sha256": "fbbfc2b1af9b7686b916bbcb51d1902876c51ea39f8e4dca398c9b756a019861" + } + }, + { + "adapter": "cloudflare", + "case": "standard-retained", + "method": "GET", + "token": "18e13ca1b2d6b9d0-142ba-1b", + "http_status": 200, + "body_bytes": 106, + "eof": true, + "capture_error": null, + "request_shutdown": "write-half-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1b2d6b9d0-142ba-1b.http1", + "bytes": 207, + "sha256": "6418420c3c62438d2c7d721a68a2eac101d20fd17d866f6122534984e7eff1f1" + } + }, + { + "adapter": "cloudflare", + "case": "standard-retained", + "method": "GET", + "token": "18e13ca1b3096290-142ba-1c", + "http_status": 404, + "body_bytes": 102, + "eof": true, + "capture_error": null, + "request_shutdown": "write-half-close", + "assembly_headers": [ + ["set-cookie", "assembly=retry-2; Path=/"], + ["x-assembly-synthesized", "retry-2"] + ], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1b3096290-142ba-1c.http1", + "bytes": 252, + "sha256": "526f1715bfd31326cea750b648bc750bb359881d5757de0bfbee3dfc0476ff6a" + } + }, + { + "adapter": "spin", + "case": "single-config", + "method": "GET", + "token": "18e13ca1e4c2c920-142ba-1d", + "http_status": 503, + "body_bytes": 90, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1e4c2c920-142ba-1d.http1", + "bytes": 251, + "sha256": "d415534544ed7b2724f6243060c89ec00a95f6d85e64dea8fbf5eaab79cebbc4" + } + }, + { + "adapter": "spin", + "case": "single-internal", + "method": "GET", + "token": "18e13ca1e9954090-142ba-1e", + "http_status": 500, + "body_bytes": 76, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1e9954090-142ba-1e.http1", + "bytes": 222, + "sha256": "ffc413d86703b96e6fa3e4b81be048b43a29ad8d874dbf9d38673e319086e7c1" + } + }, + { + "adapter": "spin", + "case": "single-config", + "method": "HEAD", + "token": "18e13ca1e9cb19b8-142ba-1f", + "http_status": 503, + "body_bytes": 0, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1e9cb19b8-142ba-1f.http1", + "bytes": 161, + "sha256": "3f681564e4a3b1188be848bcf33906f4670498dce81d24f359db6000b32fb9d5" + } + }, + { + "adapter": "spin", + "case": "standard-retained", + "method": "GET", + "token": "18e13ca1ea034888-142ba-20", + "http_status": 503, + "body_bytes": 83, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1ea034888-142ba-20.http1", + "bytes": 227, + "sha256": "a893fa791a5dcd01752e0569edfcd163e410f579359f92c1e4ea12f90eaeca14" + } + }, + { + "adapter": "spin", + "case": "single-config", + "method": "GET", + "token": "18e13ca1f0d6b078-142ba-21", + "http_status": 503, + "body_bytes": 90, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1f0d6b078-142ba-21.http1", + "bytes": 251, + "sha256": "d415534544ed7b2724f6243060c89ec00a95f6d85e64dea8fbf5eaab79cebbc4" + } + }, + { + "adapter": "spin", + "case": "single-internal", + "method": "GET", + "token": "18e13ca1f10d2db0-142ba-22", + "http_status": 500, + "body_bytes": 76, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1f10d2db0-142ba-22.http1", + "bytes": 222, + "sha256": "ffc413d86703b96e6fa3e4b81be048b43a29ad8d874dbf9d38673e319086e7c1" + } + }, + { + "adapter": "spin", + "case": "single-config", + "method": "HEAD", + "token": "18e13ca1f147d938-142ba-23", + "http_status": 503, + "body_bytes": 0, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1f147d938-142ba-23.http1", + "bytes": 161, + "sha256": "3f681564e4a3b1188be848bcf33906f4670498dce81d24f359db6000b32fb9d5" + } + }, + { + "adapter": "spin", + "case": "standard-retained", + "method": "GET", + "token": "18e13ca1f180ea98-142ba-24", + "http_status": 503, + "body_bytes": 83, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1f180ea98-142ba-24.http1", + "bytes": 227, + "sha256": "a893fa791a5dcd01752e0569edfcd163e410f579359f92c1e4ea12f90eaeca14" + } + }, + { + "adapter": "spin", + "case": "standard-retained", + "method": "GET", + "token": "18e13ca1f1ad5d80-142ba-25", + "http_status": 200, + "body_bytes": 120, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1f1ad5d80-142ba-25.http1", + "bytes": 277, + "sha256": "af2b4d9b02a0a3d649502344bf8ece60e0e24e712cefc0bfdf9a3e5524c17987" + } + }, + { + "adapter": "spin", + "case": "standard-retained", + "method": "GET", + "token": "18e13ca1f1e31f38-142ba-26", + "http_status": 200, + "body_bytes": 120, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1f1e31f38-142ba-26.http1", + "bytes": 277, + "sha256": "237e6477fe54fc14931e7126e6bc0ff0fa38fe05c9cf2add888d0cadde8a2404" + } + }, + { + "adapter": "spin", + "case": "standard-retained", + "method": "GET", + "token": "18e13ca1f21b7518-142ba-27", + "http_status": 404, + "body_bytes": 102, + "eof": true, + "capture_error": null, + "request_shutdown": "connection-close", + "assembly_headers": [ + ["set-cookie", "assembly=retry-2; Path=/"], + ["x-assembly-synthesized", "retry-2"] + ], + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-assembly-final/18e13ca1f21b7518-142ba-27.http1", + "bytes": 308, + "sha256": "bebbe8986a0ae237c6a77b39ef3863ec57f6deb68ccf5f22dac595815873b748" + } + } + ], + "assembly_assertions": [ + { + "adapter": "fastly", + "assertion": "canonical-assembly-delivery", + "case": "standard-retained", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "assertion": "failed-state-released-before-retry", + "qualification": false, + "status": "pass", + "token": "18e13ca0093a4a28-142ba-4" + }, + { + "adapter": "fastly", + "assertion": "canonical-assembly-delivery", + "case": "custom-retained", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "assertion": "canonical-assembly-delivery", + "case": "single-routes", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "assertion": "canonical-assembly-delivery", + "case": "single-config", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "assertion": "canonical-assembly-delivery", + "case": "single-internal", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "assertion": "canonical-assembly-delivery", + "case": "single-hooks", + "qualification": false, + "status": "pass" + }, + { + "adapter": "fastly", + "assertion": "canonical-assembly-delivery", + "case": "single-config-explicit", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "assertion": "canonical-assembly-delivery", + "mode": "per-request", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "assertion": "canonical-assembly-delivery", + "mode": "retained", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "assertion": "canonical-assembly-delivery", + "mode": "per-request", + "qualification": false, + "status": "pass" + }, + { + "adapter": "spin", + "assertion": "canonical-assembly-delivery", + "mode": "retained", + "qualification": false, + "status": "pass" + }, + { + "adapter": "axum", + "assertion": "assembly-before-bind", + "exit_code": 1, + "http_response_attempted": false, + "occupied_port_guard": true, + "qualification": false, + "status": "pass" + } + ], + "header_regression_coverage": [ + { + "adapter": "fastly", + "attempted": 22, + "event": "synthesized-header-coverage", + "failed": 0, + "protocol": "HTTP/1.1", + "qualification": false + }, + { + "adapter": "fastly", + "checked": 22, + "error": null, + "event": "synthesized-final-evidence", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "attempted": 11, + "event": "synthesized-header-coverage", + "failed": 0, + "protocol": "HTTP/1.1", + "qualification": false + }, + { + "adapter": "spin", + "attempted": 11, + "event": "synthesized-header-coverage", + "failed": 0, + "protocol": "HTTP/1.1", + "qualification": false + }, + { + "adapter": "axum", + "attempted": 11, + "event": "synthesized-header-coverage", + "failed": 0, + "protocol": "HTTP/1.1", + "qualification": false + } + ], + "lifecycle_logs": [ + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-axum-startup.log", + "bytes": 464, + "sha256": "994911528e2515b403de742ebb3a380edda949bc19315b1e223d9cb7b6a8ed04" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-cloudflare-per-request.log", + "bytes": 3801, + "sha256": "db1401305bd5aedf98ec1b523a615c6d2f75e1d9d92dbcceab243a95174a18f7" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-cloudflare-retained.log", + "bytes": 4186, + "sha256": "cc3a18d0f072fb3a12addd796adb611e1cbf2c8fa0a0ae3cc0d7f7f9578d1bbb" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-custom-retained.log", + "bytes": 1960, + "sha256": "d16103ea0e33e9e0c455428ca6774b5b42c5464b115126991d9282d76daad0d1" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-single-config-explicit.log", + "bytes": 852, + "sha256": "abfb9f4b48ff6fedebe9d65fff6caad9eb7ba71d7061cc01f55f3ceeb517f163" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-single-config.log", + "bytes": 843, + "sha256": "748b9b96fc814d18120091becde61c236ce1432f8ce001aeebb46e230179519b" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-single-hooks.log", + "bytes": 841, + "sha256": "f9d8575d197054a77a1782edafb38545d7b914ba3578c9a9d2b70f58573324fb" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-single-internal.log", + "bytes": 531, + "sha256": "bab19fb47a27c517098d25de4eeb43274c681fb2fec93adae6ec7de850d333af" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-single-routes.log", + "bytes": 693, + "sha256": "57f356493b7ed849862ad2f897091dd03080c5244b65aaf00c411de9eef8c8e9" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-spin-per-request.log", + "bytes": 1652, + "sha256": "881dd3fe5efe20301fa90a34d0dda0a07b088a5229d33f857ab1a0409fe64388" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-spin-retained.log", + "bytes": 1894, + "sha256": "e2a07e2bcf1ba8a5e55ab2c2cca68250918d922fedcb9b63db7a4e995375aa56" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-final/assembly-standard-retained.log", + "bytes": 881, + "sha256": "7cd5c52af3c87e2c99002d4e5769cf925c4f698b0b517a86a766f3f5ca39f845" + } + ], + "retained_prior_run": { + "run": "/private/tmp/edgezero-f7b-assembly-01", + "status": "pass-before-evidence-strengthening", + "events": { + "path": "/private/tmp/edgezero-f7b-assembly-01/events.jsonl", + "bytes": 74844, + "sha256": "c5f635e846d0b56a1e08b2174086db584e07b8c1614e533621340fde21c2dd6f" + }, + "log": { + "path": "/private/tmp/edgezero-f7b-assembly-01.log", + "bytes": 4842, + "sha256": "88b9376ce144dd0b2d8135819a8451a576db316ca7d7f79f88f115bd46efb7a2" + } + }, + "verification": { + "workspace": { + "passed": 2193, + "failed": 0, + "ignored": 1, + "binaries": 28 + }, + "fixture": { + "core": 20, + "harness": 79, + "failed": 0 + }, + "wire": { + "assembly_http": 40, + "assembly_head": 9, + "native_startup": 1, + "header_regression_http": 55 + }, + "red_controls": [ + "Partial-app synthesized-field leak passed old validator; new test failed before the validator change.", + "Actual TCP Content-Length: 0 plus a nonempty payload passed old capture checks; the new negative failed before the framing change." + ], + "logs": [ + { + "path": "/private/tmp/edgezero-f7b-assembly-red.log", + "bytes": 1166, + "sha256": "ca223e35734109918de5d4d86b91e078f15519056efb0e55b4b672831ab4ca21" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-framing-red.log", + "bytes": 1101, + "sha256": "38dc7f556ac5253f05a8ed54d277fcc24edc2079f1f4895832d148c21585d2e2" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-fixture-final2.log", + "bytes": 10073, + "sha256": "7c5d3be82007a5590911e51d82070eb107246c56535916c9108e72a23e278dac" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-fixture-clippy-publish.log", + "bytes": 499, + "sha256": "4da162534388582e22cc49eb4afa911ff37d7e05fdb06a779a084032e543459f" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-workspace-tests.log", + "bytes": 184442, + "sha256": "6302fc65cb4fd949f125c0c90409ef09983d3c998c1b46b28dc2ff08c7ac29d5" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-workspace-clippy.log", + "bytes": 327, + "sha256": "39cca61afd9d42e45e219e95d14205db70668acd9985092e697acdd8c75093b0" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-features.log", + "bytes": 327, + "sha256": "de1c8a9e09c06a52712570d9b95bb3fa4fc0ec18ad92d53b84570e03050af5ad" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-spin-check.log", + "bytes": 327, + "sha256": "1b5941eb11eea54d925f1b98e6399d80f77967f4163188ebd7e165243916f1db" + } + ], + "initial_lint_failure": { + "reason": "Migrating actual HEAD callers left the old helper unused; obsolete path removed and protocol regression migrated without lint suppression", + "log": { + "path": "/private/tmp/edgezero-f7b-assembly-fixture-clippy.log", + "bytes": 1201, + "sha256": "7ccbfe1b1e8d26689bf10f28963945ea0c4877b3dea33a8e454f000d23d19c22" + } + }, + "target_clippy": [ + { + "path": "/private/tmp/edgezero-f7b-assembly-validator-fastly-final.log", + "bytes": 497, + "sha256": "5843a11e70e3450c933465e39e5a76e6a4af3a9e83eeece65cc0b2312802929a" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-validator-cloudflare.log", + "bytes": 671, + "sha256": "d340e55305849b7d811259b27d0937f4dd828389bd81f3873ab3962b1fbfd453" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-validator-spin.log", + "bytes": 815, + "sha256": "1e3c207442ffd167b80cfbb596b05438e51c16b612686f279421d375c055c43d" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-validator-axum.log", + "bytes": 659, + "sha256": "f590958506fa96970829091a02945b3a0617517be7c30706b79c27d3d05a5812" + } + ], + "documentation": [ + { + "path": "/private/tmp/edgezero-f7b-assembly-docs-format.log", + "bytes": 118, + "sha256": "43ba3f26abc1bc66ab5eaa42bcc64ba4c70edea76a4a621144b929d0812736f1" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-docs-lint.log", + "bytes": 40, + "sha256": "81521cc702a8a0674ad78ee506a2ec936217876334be26daf9c738d03779be16" + }, + { + "path": "/private/tmp/edgezero-f7b-assembly-docs-build.log", + "bytes": 234, + "sha256": "5821876c9463a112dabc767be0869a4b33a32ae4348d07931ba3ac40a43e7db8" + } + ] + }, + "limitations": [ + "No hosted, HTTP2/HTTP3, consumer-owned integration or frozen-candidate qualification.", + "Native evidence is configuration-error precedence over an occupied bind target, with installation witness and nonzero exit, not HTTP response delivery.", + "Temporary captures and binaries are local artifacts, not durable release assets.", + "Fastly G2, its blocked driver migration and all nine failed framing SDK parity controls remain open.", + "No capability promotion or claim that Cloudflare overflow recovery is fixed.", + "Provider-owned resource accounting and complete response-lifetime certification remain outside this local header proof." + ], + "header_regression_captures": [ + { + "adapter": "fastly", + "case": "success-GET", + "token": "18e13cb89ccb9dd8-15246-0", + "status": "pass", + "eof": true, + "body_bytes": 14, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb89ccb9dd8-15246-0.http1", + "bytes": 90, + "sha256": "b1088cfbfa53d6c25c399b68143b5b40fd8aadfa4430fcce707a9d623d70bdc4" + } + }, + { + "adapter": "fastly", + "case": "handler-error-GET", + "token": "18e13cb89daa22b0-15246-1", + "status": "pass", + "eof": true, + "body_bytes": 82, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb89daa22b0-15246-1.http1", + "bytes": 199, + "sha256": "71dc1b9f34151e81b6ea4482af3f391ba1b31a23b99013ac6718f7e8052df3ab" + } + }, + { + "adapter": "fastly", + "case": "refused-GET", + "token": "18e13cb89dde1b60-15246-2", + "status": "pass", + "eof": true, + "body_bytes": 16, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb89dde1b60-15246-2.http1", + "bytes": 109, + "sha256": "0a72415e3f91beafd950fb1181ebc71dfb1e06998490887fb2bdd3ac816d46ba" + } + }, + { + "adapter": "fastly", + "case": "missing/path-GET", + "token": "18e13cb89e084840-15246-3", + "status": "pass", + "eof": true, + "body_bytes": 113, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb89e084840-15246-3.http1", + "bytes": 4323, + "sha256": "4511d07434c046e26628b5735c53270f135f7f4d8f6f99e4bcf67062b9fdaf4f" + } + }, + { + "adapter": "fastly", + "case": "fallback-GET", + "token": "18e13cb89e48cfc8-15246-4", + "status": "pass", + "eof": true, + "body_bytes": 21, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb89e48cfc8-15246-4.http1", + "bytes": 4210, + "sha256": "2280d5c1b14eff939f0ba26be85265f8d60f0fd54a4425284421ee3478077959" + } + }, + { + "adapter": "fastly", + "case": "success-HEAD", + "token": "18e13cb8a1c19b08-15246-5", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a1c19b08-15246-5.http1", + "bytes": 56, + "sha256": "4663be53814bf473b3ce7be11b442b318542d5a935038c8cf8c557ad61e5daa6" + } + }, + { + "adapter": "fastly", + "case": "handler-error-HEAD", + "token": "18e13cb8a1f4ff48-15246-6", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a1f4ff48-15246-6.http1", + "bytes": 97, + "sha256": "9a9b5387eee4f29e0fcec3ebe7de1ace6c1b72b1181e6d678fba2bfd8c2227dd" + } + }, + { + "adapter": "fastly", + "case": "refused-HEAD", + "token": "18e13cb8a2486ac0-15246-7", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a2486ac0-15246-7.http1", + "bytes": 73, + "sha256": "6322c666770267e09d6991b58380c63af72e638d6df30711345c879f1c3c7081" + } + }, + { + "adapter": "fastly", + "case": "missing/path-HEAD", + "token": "18e13cb8a2849ce8-15246-8", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a2849ce8-15246-8.http1", + "bytes": 4189, + "sha256": "3d9473f505b65e73c761dc6a5961828d99bc1027b74c78282f70bb654f5d3fc8" + } + }, + { + "adapter": "fastly", + "case": "fallback-HEAD", + "token": "18e13cb8a2c4a388-15246-9", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a2c4a388-15246-9.http1", + "bytes": 4169, + "sha256": "5d9ee73e3c73e97276e86c09b8ebfd74cba53c6d3f982ab06288b085ce98239a" + } + }, + { + "adapter": "fastly", + "case": "wrong-method", + "token": "18e13cb8a30601e8-15246-a", + "status": "pass", + "eof": true, + "body_bytes": 108, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a30601e8-15246-a.http1", + "bytes": 4345, + "sha256": "95a8b115edd6ab863ebe5b69eb6217784aadf96c59ab5abdf39dd141cd191848" + } + }, + { + "adapter": "fastly", + "case": "normalized-header-overflow", + "token": "18e13cb8a352fcf0-15246-b", + "status": "pass", + "eof": true, + "body_bytes": 128, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a352fcf0-15246-b.http1", + "bytes": 4360, + "sha256": "2d750335f8324cf01176c11d523e1e9ba008f52e0398a0f3af9630f0bb0854c4" + } + }, + { + "adapter": "fastly", + "case": "normalized-target-overflow-app", + "token": "18e13cb8a6b33ef0-15246-c", + "status": "pass", + "eof": true, + "body_bytes": 109, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a6b33ef0-15246-c.http1", + "bytes": 4322, + "sha256": "9cc0c810f136c1ecc3ec4d5cace1deb595610c189b560e0b6b529a44877cde6a" + } + }, + { + "adapter": "fastly", + "case": "normalized-target-overflow-replace", + "token": "18e13cb8a6f45ed0-15246-d", + "status": "pass", + "eof": true, + "body_bytes": 109, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a6f45ed0-15246-d.http1", + "bytes": 4322, + "sha256": "374f14ca366fa45384572ec5e456bcd45e1633ff32bce4df1e4e090153f8a507" + } + }, + { + "adapter": "fastly", + "case": "fallback-replace-GET", + "token": "18e13cb8a72e9140-15246-e", + "status": "pass", + "eof": true, + "body_bytes": 21, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a72e9140-15246-e.http1", + "bytes": 4210, + "sha256": "d34382b20391ae012946687a4b711034594ab16c1953ea74fdc357e18aaf173f" + } + }, + { + "adapter": "fastly", + "case": "fallback-replace-HEAD", + "token": "18e13cb8a76f6300-15246-f", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a76f6300-15246-f.http1", + "bytes": 4169, + "sha256": "8080b07a0da14a87abd575ba43f48ede879af3065b3a83b4c8fce7dee90e7110" + } + }, + { + "adapter": "fastly", + "case": "normalized-target-overflow-invalid", + "token": "18e13cb8a7b005e0-15246-10", + "status": "pass", + "eof": true, + "body_bytes": 109, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a7b005e0-15246-10.http1", + "bytes": 4322, + "sha256": "9cc0c810f136c1ecc3ec4d5cace1deb595610c189b560e0b6b529a44877cde6a" + } + }, + { + "adapter": "fastly", + "case": "fallback-invalid-GET", + "token": "18e13cb8a7eded88-15246-11", + "status": "pass", + "eof": true, + "body_bytes": 21, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a7eded88-15246-11.http1", + "bytes": 4210, + "sha256": "2280d5c1b14eff939f0ba26be85265f8d60f0fd54a4425284421ee3478077959" + } + }, + { + "adapter": "fastly", + "case": "fallback-invalid-HEAD", + "token": "18e13cb8a8313fe8-15246-12", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a8313fe8-15246-12.http1", + "bytes": 4169, + "sha256": "5d9ee73e3c73e97276e86c09b8ebfd74cba53c6d3f982ab06288b085ce98239a" + } + }, + { + "adapter": "fastly", + "case": "normalized-target-overflow-empty", + "token": "18e13cb8a86db478-15246-13", + "status": "pass", + "eof": true, + "body_bytes": 109, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a86db478-15246-13.http1", + "bytes": 228, + "sha256": "bc3e28483e18ff3dd2dbb7b50b11ec8021eebb9d253c7b458a99fcc727bb075b" + } + }, + { + "adapter": "fastly", + "case": "fallback-empty-GET", + "token": "18e13cb8a8a22258-15246-14", + "status": "pass", + "eof": true, + "body_bytes": 21, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a8a22258-15246-14.http1", + "bytes": 116, + "sha256": "a615004a2c10bde02716e12f41641da4d122ebe7f3e978b5aaf5e76660d4f4a0" + } + }, + { + "adapter": "fastly", + "case": "fallback-empty-HEAD", + "token": "18e13cb8a8d22f20-15246-15", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-fastly/18e13cb8a8d22f20-15246-15.http1", + "bytes": 75, + "sha256": "65b1b32404490ac266c12875fc1aaddfd72c0c3a10a1545d88572d245bed8d25" + } + }, + { + "adapter": "cloudflare", + "case": "success-GET", + "token": "18e13cbaf63fdc60-15246-16", + "status": "pass", + "eof": true, + "body_bytes": 14, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/18e13cbaf63fdc60-15246-16.http1", + "bytes": 53, + "sha256": "605394fa5e0e59c7aeef09952395af12aae160f4a7b2cca9895829a90c3296ab" + } + }, + { + "adapter": "cloudflare", + "case": "handler-error-GET", + "token": "18e13cbb0030ad08-15246-17", + "status": "pass", + "eof": true, + "body_bytes": 82, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/18e13cbb0030ad08-15246-17.http1", + "bytes": 162, + "sha256": "30620340332cfed06ce35d204a0754a03ebcab2bcebce5800dea6a41f8bcd10e" + } + }, + { + "adapter": "cloudflare", + "case": "refused-GET", + "token": "18e13cbb008d01c0-15246-18", + "status": "pass", + "eof": true, + "body_bytes": 16, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/18e13cbb008d01c0-15246-18.http1", + "bytes": 72, + "sha256": "e8720a13e727055a818f91d872efa4fb3fc3b2ee224c48797a8ce9b78491c270" + } + }, + { + "adapter": "cloudflare", + "case": "missing/path-GET", + "token": "18e13cbb00c20410-15246-19", + "status": "pass", + "eof": true, + "body_bytes": 113, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/18e13cbb00c20410-15246-19.http1", + "bytes": 4286, + "sha256": "d82ca802ce4cc925943ea796ba5afa21a5308e7e514158d646ca440c00d5a459" + } + }, + { + "adapter": "cloudflare", + "case": "fallback-GET", + "token": "18e13cbb0105eae0-15246-1a", + "status": "pass", + "eof": true, + "body_bytes": 21, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/18e13cbb0105eae0-15246-1a.http1", + "bytes": 4173, + "sha256": "bc97d96348cb5042d7a727c4dc6bc1b6d2dfbbb58eb0371388ed955880978166" + } + }, + { + "adapter": "cloudflare", + "case": "success-HEAD", + "token": "18e13cbb014e3e80-15246-1b", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/18e13cbb014e3e80-15246-1b.http1", + "bytes": 19, + "sha256": "5212f7a67551c4744fd1099622fe454a56221fcb231c76d1ab15efbdf44f1057" + } + }, + { + "adapter": "cloudflare", + "case": "handler-error-HEAD", + "token": "18e13cbb017b93f8-15246-1c", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/18e13cbb017b93f8-15246-1c.http1", + "bytes": 80, + "sha256": "ffd8ea887297580af13c74461a1a4d33420e59f894da129bb4404bf57c5c00da" + } + }, + { + "adapter": "cloudflare", + "case": "refused-HEAD", + "token": "18e13cbb01ac92f0-15246-1d", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/18e13cbb01ac92f0-15246-1d.http1", + "bytes": 36, + "sha256": "5ae6460b33a4847809ee7828d0d10942f4a74a21c1f0c13be39f9a25c8aec7f9" + } + }, + { + "adapter": "cloudflare", + "case": "missing/path-HEAD", + "token": "18e13cbb01e1f300-15246-1e", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/18e13cbb01e1f300-15246-1e.http1", + "bytes": 4173, + "sha256": "6fae2fa48eab5cc1efb2bc918e2489d85b08b7e4bdb065a95e6870660fcfe5f7" + } + }, + { + "adapter": "cloudflare", + "case": "fallback-HEAD", + "token": "18e13cbb02332fe0-15246-1f", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/18e13cbb02332fe0-15246-1f.http1", + "bytes": 4132, + "sha256": "26a13e011c7d6593aa681d8da047ebf43830f412a7128747bba7fdd46b19aa03" + } + }, + { + "adapter": "cloudflare", + "case": "wrong-method", + "token": "18e13cbb02753250-15246-20", + "status": "pass", + "eof": true, + "body_bytes": 108, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-cloudflare/18e13cbb02753250-15246-20.http1", + "bytes": 4308, + "sha256": "6f2be0f5fcdb5b5adae267667dc659600b0bc6c9d9eb7dacd2adfe79df6e6fd8" + } + }, + { + "adapter": "spin", + "case": "success-GET", + "token": "18e13cbc107d66a0-15246-21", + "status": "pass", + "eof": true, + "body_bytes": 14, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/18e13cbc107d66a0-15246-21.http1", + "bytes": 109, + "sha256": "e54fdbfc58deae34cb6e51b126b1c915989593dfba5f094a6ce84a8f12d8659b" + } + }, + { + "adapter": "spin", + "case": "handler-error-GET", + "token": "18e13cbc12385158-15246-22", + "status": "pass", + "eof": true, + "body_bytes": 82, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/18e13cbc12385158-15246-22.http1", + "bytes": 218, + "sha256": "c87505075138e8f97dbf0665a6f3beb7b2e297bebd628a47851e8207513c459a" + } + }, + { + "adapter": "spin", + "case": "refused-GET", + "token": "18e13cbc12881f08-15246-23", + "status": "pass", + "eof": true, + "body_bytes": 16, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/18e13cbc12881f08-15246-23.http1", + "bytes": 128, + "sha256": "030c0890945236d4761b17105b932da185471b347a268d7a2770f9eec4802e02" + } + }, + { + "adapter": "spin", + "case": "missing/path-GET", + "token": "18e13cbc12c28c10-15246-24", + "status": "pass", + "eof": true, + "body_bytes": 113, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/18e13cbc12c28c10-15246-24.http1", + "bytes": 4342, + "sha256": "67cf23b01a041824056779ff8618bc5587903b116ec16473e297b384317109e6" + } + }, + { + "adapter": "spin", + "case": "fallback-GET", + "token": "18e13cbc1310ded8-15246-25", + "status": "pass", + "eof": true, + "body_bytes": 21, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/18e13cbc1310ded8-15246-25.http1", + "bytes": 4229, + "sha256": "9abc572a0d9e1970a5796c9e7775d7f230a3559dbd9931491c7c2768fbef996a" + } + }, + { + "adapter": "spin", + "case": "success-HEAD", + "token": "18e13cbc13a581a0-15246-26", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/18e13cbc13a581a0-15246-26.http1", + "bytes": 75, + "sha256": "2f16b2f1de2f141e95944f13435ed181762cdf8ea2bc328cd4f281ece30aee7a" + } + }, + { + "adapter": "spin", + "case": "handler-error-HEAD", + "token": "18e13cbc13db4f10-15246-27", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/18e13cbc13db4f10-15246-27.http1", + "bytes": 136, + "sha256": "c5166bad7ca407d61a74be101d8f03b988648f3a4f34e52443ca4e53744d0466" + } + }, + { + "adapter": "spin", + "case": "refused-HEAD", + "token": "18e13cbc140c26f8-15246-28", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/18e13cbc140c26f8-15246-28.http1", + "bytes": 92, + "sha256": "915514fba513f30c8791a70cf09c199cd8146dac127733102292bd69e584cf24" + } + }, + { + "adapter": "spin", + "case": "missing/path-HEAD", + "token": "18e13cbc143e6640-15246-29", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/18e13cbc143e6640-15246-29.http1", + "bytes": 4229, + "sha256": "d541e790287a43acc95e323aeef27dbcb8c99480c1bdd866d92940f6c852e298" + } + }, + { + "adapter": "spin", + "case": "fallback-HEAD", + "token": "18e13cbc147fc0b8-15246-2a", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/18e13cbc147fc0b8-15246-2a.http1", + "bytes": 4188, + "sha256": "6438b69211dba906fd20e542f71231b41b688a36b1d966d45f35ae8a54fd9309" + } + }, + { + "adapter": "spin", + "case": "wrong-method", + "token": "18e13cbc14cf28d8-15246-2b", + "status": "pass", + "eof": true, + "body_bytes": 108, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-spin/18e13cbc14cf28d8-15246-2b.http1", + "bytes": 4364, + "sha256": "dafbe4c4c362353d619e2e55aa53c440a559c59769c49acffd12ed6a76299267" + } + }, + { + "adapter": "axum", + "case": "success-GET", + "token": "18e13cbd6b0dfb60-15246-2c", + "status": "pass", + "eof": true, + "body_bytes": 14, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/18e13cbd6b0dfb60-15246-2c.http1", + "bytes": 109, + "sha256": "77f6b6bc14d284ccd492a123d00b2eca049214da918468514afb85085d29cabe" + } + }, + { + "adapter": "axum", + "case": "handler-error-GET", + "token": "18e13cbd6b50a5c8-15246-2d", + "status": "pass", + "eof": true, + "body_bytes": 82, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/18e13cbd6b50a5c8-15246-2d.http1", + "bytes": 218, + "sha256": "811fe171f41d9366ff2a92f07dfc2ccc208b93ae3d1d39a58728147e5f4266ca" + } + }, + { + "adapter": "axum", + "case": "refused-GET", + "token": "18e13cbd6b959a20-15246-2e", + "status": "pass", + "eof": true, + "body_bytes": 16, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/18e13cbd6b959a20-15246-2e.http1", + "bytes": 128, + "sha256": "d1df854802ecc4ffa7b38e7da5ca89be4f92f6a2d2abb984c45ccc6472b2a2a8" + } + }, + { + "adapter": "axum", + "case": "missing/path-GET", + "token": "18e13cbd6bcdb568-15246-2f", + "status": "pass", + "eof": true, + "body_bytes": 113, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/18e13cbd6bcdb568-15246-2f.http1", + "bytes": 4342, + "sha256": "383526ee0c8730683460cac356f8099dd34c0bf79b3d82a6ecc292f122084940" + } + }, + { + "adapter": "axum", + "case": "fallback-GET", + "token": "18e13cbd6c2c6b80-15246-30", + "status": "pass", + "eof": true, + "body_bytes": 21, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/18e13cbd6c2c6b80-15246-30.http1", + "bytes": 4229, + "sha256": "4d6d0ea76faca5ae1c59cda4e10bf125f5198fba860a7b3b77d36486005d025f" + } + }, + { + "adapter": "axum", + "case": "success-HEAD", + "token": "18e13cbd6ca715d8-15246-31", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/18e13cbd6ca715d8-15246-31.http1", + "bytes": 75, + "sha256": "bceaf229c032f01baa30446bd269f2b6342bf0eeba4d92b939baae7094531cd3" + } + }, + { + "adapter": "axum", + "case": "handler-error-HEAD", + "token": "18e13cbd6cf1cad8-15246-32", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/18e13cbd6cf1cad8-15246-32.http1", + "bytes": 136, + "sha256": "a0e15120932974314282f19cdd51607005074ab9c3791215a7d54c6d30ab52af" + } + }, + { + "adapter": "axum", + "case": "refused-HEAD", + "token": "18e13cbd6d310e28-15246-33", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/18e13cbd6d310e28-15246-33.http1", + "bytes": 92, + "sha256": "f664f3515063882ad2237852484d8aa9fdffbf3484c0d08ab6739a20a1dcc6f2" + } + }, + { + "adapter": "axum", + "case": "missing/path-HEAD", + "token": "18e13cbd6d6ab7e0-15246-34", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/18e13cbd6d6ab7e0-15246-34.http1", + "bytes": 4229, + "sha256": "c2e670a9605ba5d28ab3b6f9272d5d7af339ce10d1b869a3fb4997602fcb1acd" + } + }, + { + "adapter": "axum", + "case": "fallback-HEAD", + "token": "18e13cbd6dcc5040-15246-35", + "status": "pass", + "eof": true, + "body_bytes": 0, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/18e13cbd6dcc5040-15246-35.http1", + "bytes": 4188, + "sha256": "b0eaa7722f05811eb5b41c69591768bd29c2252ef19ba48a49e17b6aa0618436" + } + }, + { + "adapter": "axum", + "case": "wrong-method", + "token": "18e13cbd6e1fbfa0-15246-36", + "status": "pass", + "eof": true, + "body_bytes": 108, + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-headers-publish/synthesized-headers-axum/18e13cbd6e1fbfa0-15246-36.http1", + "bytes": 4364, + "sha256": "645fa5d05f5c3671386a4a18e848a341f2271cf11e0c122d448c32e7a06890d3" + } + } + ], + "frozen_policy": { + "path": "docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md", + "bytes": 65279, + "sha256": "c52b4aea6b8d2793f30784e88016a3676088d344c0a5607774a5d9f1a7ffae80" + } +} diff --git a/docs/audits/evidence/2026-10-23-buffered-response-framing.json b/docs/audits/evidence/2026-10-23-buffered-response-framing.json new file mode 100644 index 000000000..b43049504 --- /dev/null +++ b/docs/audits/evidence/2026-10-23-buffered-response-framing.json @@ -0,0 +1,443 @@ +{ + "checkpoint": "F7a buffered response framing", + "recorded_on": "2026-10-23", + "base_revision": "fe0fd4c968c0a1024e49ce9bb1625a106b2c4303", + "source_state": "working-tree characterization", + "qualification": false, + "command": "scripts/smoke_test_reusable_app.sh --adapter all --suite response-framing", + "exit_code": 1, + "raw_evidence": { + "path": "/private/tmp/edgezero-f7a-framing-verified/events.jsonl", + "sha256": "2119d10957cf4ddebc8765d30c4372e04e5587cd3bb7385bea301a5e98a80c37" + }, + "production": { + "passed": 80, + "failed": 0, + "adapters": [ + { + "adapter": "fastly", + "passed": 32, + "failed": 0 + }, + { + "adapter": "cloudflare", + "passed": 16, + "failed": 0 + }, + { + "adapter": "spin", + "passed": 16, + "failed": 0 + }, + { + "adapter": "axum", + "passed": 16, + "failed": 0 + } + ] + }, + "sdk_parity": { + "passed": 9, + "failed": 9, + "failed_cases": [ + { + "case": "head", + "http2": false, + "error": "Fastly SDK automatic framing parity mismatch", + "reference_curl_exit": 0, + "reference_length": 6, + "diagnostic_artifact": { + "bytes": 0, + "fnv1a64": "cbf29ce484222325", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-fastly/18e11845e12fcb80-109b4-e.curl-stderr", + "purpose": "non-security build identity" + } + }, + { + "case": "head-explicit", + "http2": false, + "error": "Fastly SDK automatic framing parity mismatch", + "reference_curl_exit": 0, + "reference_length": 6, + "diagnostic_artifact": { + "bytes": 0, + "fnv1a64": "cbf29ce484222325", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-fastly/18e11845e25ca320-109b4-10.curl-stderr", + "purpose": "non-security build identity" + } + }, + { + "case": "reset-content", + "http2": false, + "error": "Fastly SDK automatic framing parity mismatch", + "reference_curl_exit": 0, + "reference_length": 6, + "diagnostic_artifact": { + "bytes": 0, + "fnv1a64": "cbf29ce484222325", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-fastly/18e11845e6f8c850-109b4-18.curl-stderr", + "purpose": "non-security build identity" + } + }, + { + "case": "head", + "http2": true, + "error": "Fastly SDK automatic framing parity mismatch", + "reference_curl_exit": 16, + "reference_length": 6, + "diagnostic_artifact": { + "bytes": 44, + "fnv1a64": "a98ff98535bff32c", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-fastly/18e11845ef6d4d80-109b4-27.curl-stderr", + "purpose": "non-security build identity" + } + }, + { + "case": "head-explicit", + "http2": true, + "error": "Fastly SDK automatic framing parity mismatch", + "reference_curl_exit": 16, + "reference_length": 6, + "diagnostic_artifact": { + "bytes": 44, + "fnv1a64": "a98ff98535bff32c", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-fastly/18e11845f0769f60-109b4-29.curl-stderr", + "purpose": "non-security build identity" + } + }, + { + "case": "not-modified", + "http2": true, + "error": "Fastly SDK automatic framing parity mismatch", + "reference_curl_exit": 16, + "reference_length": 6, + "diagnostic_artifact": { + "bytes": 44, + "fnv1a64": "a98ff98535bff32c", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-fastly/18e11845f3e93c98-109b4-2b.curl-stderr", + "purpose": "non-security build identity" + } + }, + { + "case": "not-modified-explicit", + "http2": true, + "error": "Fastly SDK automatic framing parity mismatch", + "reference_curl_exit": 16, + "reference_length": 6, + "diagnostic_artifact": { + "bytes": 44, + "fnv1a64": "a98ff98535bff32c", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-fastly/18e11845f4e0d750-109b4-2d.curl-stderr", + "purpose": "non-security build identity" + } + }, + { + "case": "no-content", + "http2": true, + "error": "Fastly SDK automatic framing parity mismatch", + "reference_curl_exit": 16, + "diagnostic_artifact": { + "bytes": 112, + "fnv1a64": "4c3a290fc2778fae", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-fastly/18e11845f5d2c8d0-109b4-2f.curl-stderr", + "purpose": "non-security build identity" + } + }, + { + "case": "reset-content", + "http2": true, + "error": "Fastly SDK automatic framing parity mismatch", + "reference_curl_exit": 0, + "reference_length": 6, + "diagnostic_artifact": { + "bytes": 0, + "fnv1a64": "cbf29ce484222325", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-fastly/18e11845f6b83b18-109b4-31.curl-stderr", + "purpose": "non-security build identity" + } + } + ] + }, + "runtime": [ + { + "adapter": "fastly", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "revision": "fe0fd4c968c0a1024e49ce9bb1625a106b2c4303", + "source_state": "working-tree-characterization", + "version": "viceroy 0.21.0" + }, + { + "adapter": "cloudflare", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "revision": "fe0fd4c968c0a1024e49ce9bb1625a106b2c4303", + "source_state": "working-tree-characterization", + "version": "4.131.0" + }, + { + "adapter": "spin", + "event": "progress-runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "revision": "fe0fd4c968c0a1024e49ce9bb1625a106b2c4303", + "source_state": "working-tree-characterization", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + }, + { + "adapter": "axum", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo", + "revision": "fe0fd4c968c0a1024e49ce9bb1625a106b2c4303", + "source_state": "working-tree-characterization", + "version": "cargo 1.98.1 (797e8a9bc 2026-08-05)" + } + ], + "artifacts": [ + { + "adapter": "fastly", + "variant": "fastly-outbound-progress", + "artifact": { + "bytes": 2311917, + "fnv1a64": "27b82af5967f419d", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "purpose": "non-security build identity" + }, + "sha256": "83c92f15047d2da4edd84767da9a90b88aed84acb8c34a7f32676aea2b31ce40" + }, + { + "adapter": "cloudflare", + "variant": "cloudflare-outbound-progress", + "artifact": { + "bytes": 1917775, + "fnv1a64": "f8ea89ea510aaa52", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-cloudflare/artifacts/build/index_bg.wasm", + "purpose": "non-security build identity" + }, + "sha256": "9e48c4b0e3f22884053073e4ed81d4e2b7556d03b21e71a71d2b76065827b89b" + }, + { + "adapter": "spin", + "variant": "spin-outbound-progress", + "artifact": { + "bytes": 2576158, + "fnv1a64": "3879a9ee0b36da50", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-spin/artifacts/fixture_spin.wasm", + "purpose": "non-security build identity" + }, + "sha256": "259535839d96427815f1f03e18684d75f21fa58855667befa2f272c490eed6d2" + }, + { + "adapter": "axum", + "variant": "axum-outbound-progress", + "artifact": { + "bytes": 9021728, + "fnv1a64": "bb168795de791e9e", + "path": "/private/tmp/edgezero-f7a-framing-verified/framing-axum/artifacts/fixture-axum-outbound-progress", + "purpose": "non-security build identity" + }, + "sha256": "cc88df47be29736979f96d380d4e931f457f9a4294612a7490e7d31732cd0ab4" + } + ], + "decision": { + "preserve_strict_suppression": true, + "restore_head_payload": false, + "qualification_waiver": false + }, + "open_gates": [ + "Fastly bodyless SDK parity", + "Hosted Fastly HTTP/1.1 and HTTP/2 buffered and declared-stream compression", + "Deployed Workers header delivery", + "Frozen pinned-runtime Spin evidence", + "Consumer response-diff acceptance", + "Fastly G2 remains FAILED and full P1 is not complete" + ], + "source_fingerprints": [ + { + "path": "crates/edgezero-adapter-axum/src/cli.rs", + "sha256": "dae042013c33bad529cef28c85832a9b66c976b864e94245e4596215e8ef5015" + }, + { + "path": "crates/edgezero-adapter-axum/src/response.rs", + "sha256": "4c8ed4c2f8690c35156f4aaeb7575bf5d4099ed1d560682b3f3b10c7692dd608" + }, + { + "path": "crates/edgezero-adapter-axum/src/service.rs", + "sha256": "865c46136c92fe5e09d32604c38b2ee485b47ad88ee4dd292a39d04f21ba0814" + }, + { + "path": "crates/edgezero-adapter-cloudflare/src/cli.rs", + "sha256": "2beeae2c258941e0495c0da1543ee0706027eb8de6d9bb3cdcc48f783025ff97" + }, + { + "path": "crates/edgezero-adapter-cloudflare/src/response.rs", + "sha256": "8ff523e39363b924d4cc26f4dcbc65f9bfdda5808e09ea5ed4c28d64cd9901ed" + }, + { + "path": "crates/edgezero-adapter-cloudflare/tests/contract.rs", + "sha256": "fd08f8aed3ed4bd17c5b772c175096443d1994a326136fe01e3247e0a662b4d5" + }, + { + "path": "crates/edgezero-adapter-fastly/src/cli.rs", + "sha256": "87bb75efa4bc557df6c2bf66978b030eff7f54d39e9b5e45678e430f31eabf4d" + }, + { + "path": "crates/edgezero-adapter-fastly/src/fastly_response_abi.rs", + "sha256": "e3689aac15e16fc055907f358db5ba707563115bd9d7d07e245a73c3b1b7f1b2" + }, + { + "path": "crates/edgezero-adapter-fastly/src/request.rs", + "sha256": "e427f8a0ca339b5a14a87c49da4d6c47d91751a57cbe0da7b3d445b819100df8" + }, + { + "path": "crates/edgezero-adapter-fastly/src/response.rs", + "sha256": "2ccfe4bf52996ac192f0c61857677caf4e0dd84e5868c773b834f6a8964a7696" + }, + { + "path": "crates/edgezero-adapter-fastly/tests/contract.rs", + "sha256": "bab3d2aa4de9fd87e7729e081c6eab3400305dcd868cc504b26e7c8a79e947b2" + }, + { + "path": "crates/edgezero-adapter-spin/src/cli.rs", + "sha256": "525e7072fdd9249e88791dd1c51a26bfd0319aee0ecc8f649373891bbd1dcc77" + }, + { + "path": "crates/edgezero-adapter-spin/src/response.rs", + "sha256": "14aa933a57554808233d0d2b6ced976a99f7bca9a5afd984bedc28394d5d800f" + }, + { + "path": "crates/edgezero-adapter-spin/tests/contract.rs", + "sha256": "cbe54b7776a73f80a6912155bb3392e5a10f196762fac6604647c400a87527ec" + }, + { + "path": "crates/edgezero-cli/src/templates/core/src/lib.rs.hbs", + "sha256": "9c7389870a1b284bd84ada1195dcf492c5cb1aa88c1df0fff1afcacc71128fb3" + }, + { + "path": "crates/edgezero-core/src/app.rs", + "sha256": "4cc0548b2ae25b46db0ae6f795df91d51b0de3195952c2476f44c82c7d00549a" + }, + { + "path": "crates/edgezero-core/src/manifest.rs", + "sha256": "389432c2539f9c32070f7d88cc991738ebfaa660777d70526ece56971aadb52c" + }, + { + "path": "crates/edgezero-core/src/response_egress.rs", + "sha256": "e10d4b2688760717dafa0508911f5e8d07979ac1ae2cf6b8e60ae553dd1b531f" + }, + { + "path": "crates/edgezero-core/src/response_egress_framing.rs", + "sha256": "caf36acc8e69999f138c48e1aad607a74e0cbee09c51c83ab57a1532a4afada0" + }, + { + "path": "crates/edgezero-core/tests/assembly_failure_logging.rs", + "sha256": "e2b4f29fae7e44dbd24ec308460e31e6818840e7ac67a040e096afe125c40351" + }, + { + "path": "crates/edgezero-macros/tests/app_macro.rs", + "sha256": "e7982630de7e7d73254dd84b128622aca631d7dd4ca561f217d02fa8f47c54f2" + }, + { + "path": "examples/app-demo/crates/app-demo-core/src/lib.rs", + "sha256": "67c8a9a1f6b42a0389c62271d109b48f34cc13c8925219a067abe33fe7d72f85" + }, + { + "path": "scripts/check_outbound_docs_contract.mjs", + "sha256": "d6c0284081e738cdc49a336302afb9f4bdb5d75b4e89c7d1bf0e07f58ef3baef" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs", + "sha256": "96370f686597c5d7e6d2ce47cc5c8e142d108f9638ca3d3d1eb0689dac3c54bc" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/framing.rs", + "sha256": "92201a7f6667c54967b47e2aa972cdf0a5f8ac4828db52d119f3fdfa6a3139d1" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs", + "sha256": "e808041e3019594d69300d4ee85da041ee46d975541d238f17b2f546c303352f" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs", + "sha256": "c19350247d13fd52db74db8c3235f5eb71a4dd0f1323a790f8220f2720cfadd5" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs", + "sha256": "576c6eb279c8934c3f60ba770cf27c4c58a606dbe9430625494d9e57775cdc43" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs", + "sha256": "d1d96ccbf12c5c42ad01787f8c6c89d0361cc879ea96de2bf2424a0b1bd6db7a" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/framing.rs", + "sha256": "56c570c8b206c51f6aa9fe282cf6a0adcdd28f19836dd52dd12d4c1bfc47418d" + } + ], + "runtime_settings": { + "cloudflare": { + "worker_sdk": "0.8.5", + "workerd": "2026-09-10", + "compatibility_date": "2026-04-22", + "compatibility_flags": ["enable_request_signal", "enable_weak_ref"], + "deployment": "local" + }, + "fastly": { + "sdk": "0.13.1", + "viceroy": "0.21.0", + "deployment": "local" + }, + "spin": { + "sdk": "7.0.0", + "runtime": "4.1.0", + "deployment": "local" + } + }, + "verification": { + "required_rust_gates": { + "fmt": 0, + "clippy": 0, + "workspace_tests": 0, + "workspace_features": 0, + "spin_wasm_check": 0 + }, + "workspace_tests": { + "passed": 2181, + "failed": 0, + "binaries": 28, + "core": 823, + "ignored": 1, + "ignored_generated_project_test_run_separately": true + }, + "feature_compilation": { + "passed": 56, + "failed": 0 + }, + "wasm_clippy_variants": { + "passed": 8, + "failed": 0 + }, + "demo_and_generated_apps": "passed", + "docs_format_lint_build": "passed", + "additional_build_review": { + "default_native": 0, + "fastly_wasm": 0, + "cloudflare_wasm": 0, + "spin_wasm": 0, + "native_all_features": 101, + "baseline_native_all_features": { + "revision": "fe0fd4c968c0a1024e49ce9bb1625a106b2c4303", + "exit_code": 101, + "log": "/private/tmp/edgezero-f7a-native-baseline-build.log", + "sha256": "5d1e61caa81e39d7e41b00e26e14a021f18218d55883f1db71f5ca8e190a751d" + }, + "current_native_all_features": { + "exit_code": 101, + "log": "/private/tmp/edgezero-f7a-build-validator-native-all-features.log", + "sha256": "78ef251ea1bc7d33c898191f8cfd7c1de9867981729711bee3221301c435327a" + }, + "undefined_symbols": 83, + "identical_to_baseline": true, + "classification": "pre-existing Fastly hostcall native-link failure; not a passing check" + } + } +} diff --git a/docs/audits/evidence/2026-10-23-declared-kv-operations.json b/docs/audits/evidence/2026-10-23-declared-kv-operations.json new file mode 100644 index 000000000..c524f5cdd --- /dev/null +++ b/docs/audits/evidence/2026-10-23-declared-kv-operations.json @@ -0,0 +1,2056 @@ +{ + "schema": 1, + "recorded_date": "2026-10-23", + "base_revision": "d47c8026a941a2361182c9e2042bebdcb8be4d04", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "status": "local-f8-operation-delivery-pass-whole-smoke-spin-retained-overlap-unverified", + "scope": "F8/EZ-20 local canonical operation delivery and exactly-once guest terminal observations; no production implementation or capability change", + "source_snapshot_scope": "Selected final checkout inputs, not frozen-candidate or full build-input attestation. Temporary raw artifacts are local, not durable release evidence.", + "frozen_api_plan": { + "path": "docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md", + "bytes": 65279, + "sha256": "c52b4aea6b8d2793f30784e88016a3676088d344c0a5607774a5d9f1a7ffae80" + }, + "approved_interfaces_sha256": "c623e7fc5b189e2d9122756f29297a92031a9936b13c6cc475484d4e028a4a7a", + "requirements": { + "path": "docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md", + "bytes": 390165, + "sha256": "86691fe7f0dfae0e489678f01eeb4385c9acdc238778801d700c7ef8585f2fd6" + }, + "source_files": [ + { + "path": "tests/fixtures/reusable-app/crates/fixture-core/src/lib.rs", + "bytes": 20997, + "sha256": "17e94e8af8ef6b49554a858308727aa34c4bc0676359b80642b994abad2c50be" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs", + "bytes": 86933, + "sha256": "e2e3f032d4d707296e4b61bd98bdfc19de18a78db2444357aaa23b9d7bba9209" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs", + "bytes": 13357, + "sha256": "b8818ae4ebd1587674a17af9d642655304543e473275117d5a672c64c115ca06" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs", + "bytes": 30038, + "sha256": "723445e8bf33584832cbfc36dc04c049280bb4754c70b1f8ac22d9fa7fee6a75" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs", + "bytes": 23839, + "sha256": "008c267b17b08cba63ea69a284cd9b27e9c7ec9780342b96829f4402653fada7" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/retained_app.rs", + "bytes": 294, + "sha256": "629635c72fe281cda10cb86c75329df67aa3ba53e9d6d93ffe0019eda951d27d" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-fastly/fastly.toml", + "bytes": 463, + "sha256": "4444af855e1f5db54da213ddfc6a84ea34aa52f9fd868d047521b0ab78a3fd18" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs", + "bytes": 3848, + "sha256": "ae744bde0abe850ddbe18764e4d366ec11eb5df39c757d5ea816027b010c322a" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml", + "bytes": 457, + "sha256": "6c2a097879b98f4d4af3e2fea139215abc9fa0bfdc058500f28fd9f7d757aab3" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-spin/src/lib.rs", + "bytes": 3743, + "sha256": "6f5cfb313ec654d68455550dab6a439c946a29935a1798d1d0f233fafdcd9b8b" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-spin/spin.toml", + "bytes": 555, + "sha256": "dfed653d283de300aea36feb4841ee731e20643dd60df83dc0907047fdb78d9c" + }, + { + "path": "scripts/smoke_test_reusable_app.sh", + "bytes": 236, + "sha256": "5ffedd5e84ae8f8f1e8f30e0ba4b7b4032ffdea48776844619f1e354197a7c1b" + }, + { + "path": "crates/edgezero-core/src/key_value_store.rs", + "bytes": 62623, + "sha256": "37edf3f5491193ee120aafeab74d31afc419a93dfe9e959aec5383246b8717c4" + }, + { + "path": "crates/edgezero-adapter-fastly/src/key_value_store.rs", + "bytes": 12677, + "sha256": "dc55c32c9d7d143275dd0e025fb96b5020a4a30bfd4547f311406102993b58e4" + }, + { + "path": "crates/edgezero-adapter-cloudflare/src/key_value_store.rs", + "bytes": 10745, + "sha256": "1ca3d1a60ac6dfdd32e0016140929d489b82b85318d609c59cf15d4ff6101407" + }, + { + "path": "crates/edgezero-adapter-spin/src/key_value_store.rs", + "bytes": 7543, + "sha256": "19941a845fdd46b7221c7ce89f8f86c6d73e17fe96897e4ba4c995aceb9ca0ed" + } + ], + "locks": [ + { + "path": "Cargo.lock", + "bytes": 96143, + "sha256": "ded1da41b9bd6fae689e16d277d076e975c8f3ccfa2842cd53addf30a5c3e27e", + "unchanged_from_base": true + }, + { + "path": "tests/fixtures/reusable-app/Cargo.lock", + "bytes": 84127, + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "unchanged_from_base": true + }, + { + "path": "examples/app-demo/Cargo.lock", + "bytes": 87596, + "sha256": "d1dad662166f73b278b4ba0c33a017f3d30f4749f6e1a6a9a43fc1287c31f423", + "unchanged_from_base": true + } + ], + "sdk_versions": { + "fastly": "0.13.1", + "worker": "0.8.5", + "spin_sdk": "7.0.0" + }, + "runtime_identity": [ + { + "adapter": "fastly", + "event": "runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "expected": "0.21.0", + "version": "viceroy 0.21.0" + }, + { + "adapter": "cloudflare", + "event": "runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "version": "4.131.0" + }, + { + "adapter": "spin", + "event": "runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + } + ], + "cloudflare_compatibility": { + "date": "2026-04-22", + "flags": ["enable_request_signal", "enable_weak_ref"], + "environment": "Wrangler local/workerd, not deployed" + }, + "local_host_bound": { + "provider": "Viceroy", + "version": "0.21.0", + "max_followup_requests": 5, + "max_total_requests_per_guest": 6, + "source": { + "path": "/private/tmp/claude-501/-Users-ag-projects-iab-trusted-server/1768e0ae-8449-4497-9bcc-6cb96930a4f3/scratchpad/edgezero-analysis/dl-viceroy/viceroy-lib-0.21.0/src/sandbox.rs", + "bytes": 61057, + "sha256": "e78e94daa329ce51a2cf43b9f67eef6454cc83a32f8e1267297c3a0482c4821f" + }, + "interpretation": "Pinned local host limit, not a hosted reuse guarantee. Original four-request identity proof remains mandatory; later operations allow observed natural turnover." + }, + "final_run": { + "directory": "/private/tmp/edgezero-f8-operations-correlated", + "command": "scripts/smoke_test_reusable_app.sh --adapter all --suite smoke --output /private/tmp/edgezero-f8-operations-correlated", + "environment": "CARGO_NET_OFFLINE=true and pinned tool overrides; loopback access enabled", + "exit_code": 2, + "events": { + "path": "/private/tmp/edgezero-f8-operations-correlated/events.jsonl", + "bytes": 1843639, + "sha256": "334ffa74be2e06dec62e9c27904e53e1e4e9df212c27ff36b6fb8f9d961ab242" + }, + "summary": { + "path": "/private/tmp/edgezero-f8-operations-correlated/summary.json", + "bytes": 483003, + "sha256": "f485a9a8284a3a14c4ca5a5955ff0b608acf0b40f281a99d4d2efb6305c96726" + }, + "log": { + "path": "/private/tmp/edgezero-f8-operations-correlated.log", + "bytes": 7316, + "sha256": "8b8e5b389115c4a375f406db9107f293c8677f6b27859470cc405975acae205b" + }, + "f8_request_cases": 39, + "failed_open_cases": 15, + "handler_free_cases": 6, + "healthy_cases": 18, + "operation_receipt_pairs": 33, + "terminal_reports": 39, + "post_shutdown": [ + { + "adapter": "fastly", + "error": null, + "event": "declared-kv-after-shutdown", + "failed_instances": [ + "18e13f67e1d94108-86fe-84", + "18e13f67e24b5040-86fe-8a" + ], + "instance": "18e13f67e1d94108-86fe-84", + "qualification": false, + "requests": 13, + "status": "pass", + "warnings": 2 + }, + { + "adapter": "cloudflare", + "error": null, + "event": "declared-kv-after-shutdown", + "failed_instances": ["18e13f6b24482b00-86fe-13b"], + "instance": "18e13f6b24482b00-86fe-13b", + "qualification": false, + "requests": 13, + "status": "pass", + "warnings": 1 + }, + { + "adapter": "spin", + "error": null, + "event": "declared-kv-after-shutdown", + "failed_instances": ["18e13f6dce9432a0-86fe-1c9"], + "instance": "18e13f6dce9432a0-86fe-1c9", + "qualification": false, + "requests": 13, + "status": "pass", + "warnings": 1 + } + ], + "overlap_observations": [ + { + "adapter": "cloudflare", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "per-request", + "status": "pass" + }, + { + "adapter": "cloudflare", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "retained", + "status": "pass" + }, + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "per-request", + "status": "pass" + }, + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "retained", + "status": "unverified" + }, + { + "adapter": "axum", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "retained", + "status": "pass" + } + ], + "unverified": [ + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "retained", + "status": "unverified" + } + ], + "failures": [], + "cases": [ + { + "adapter": "fastly", + "path": "/declared-kv/free", + "token": "18e13f67e1f302c8-86fe-85", + "instance": "18e13f67e1d94108-86fe-84", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "281"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"00000000000000000000000000000006\",\"declared_kv\":true,\"default_kv\":true,\"inflight\":0,\"instance\":\"18e13f67e1d94108-86fe-84\",\"mutated\":false,\"native_id\":true,\"ordinal\":2,\"path\":null,\"shared\":2,\"token\":\"18e13f67e1f302c8-86fe-85\",\"unknown_kv\":false}" + }, + "warnings_after_request": 0, + "lifecycle": { + "ordered_events": ["request_start", "egress_terminal"], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 281, + "fallback": null + }, + "operation": null + }, + { + "adapter": "fastly", + "path": "/declared-kv/missing/read", + "token": "18e13f67e2052f20-86fe-86", + "instance": "18e13f67e1d94108-86fe-84", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "76"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "read", + "outcome": "open-failed", + "reason": "MissingBinding" + } + }, + { + "adapter": "fastly", + "path": "/declared-kv/missing/read", + "token": "18e13f67e218a398-86fe-87", + "instance": "18e13f67e1d94108-86fe-84", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "76"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "read", + "outcome": "open-failed", + "reason": "MissingBinding" + } + }, + { + "adapter": "fastly", + "path": "/declared-kv/free", + "token": "18e13f67e227bec8-86fe-88", + "instance": "18e13f67e1d94108-86fe-84", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "281"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"00000000000000000000000000000009\",\"declared_kv\":true,\"default_kv\":true,\"inflight\":0,\"instance\":\"18e13f67e1d94108-86fe-84\",\"mutated\":false,\"native_id\":true,\"ordinal\":5,\"path\":null,\"shared\":5,\"token\":\"18e13f67e227bec8-86fe-88\",\"unknown_kv\":false}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": ["request_start", "egress_terminal"], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 281, + "fallback": null + }, + "operation": null + }, + { + "adapter": "fastly", + "path": "/declared-kv/missing/write", + "token": "18e13f67e23923e8-86fe-89", + "instance": "18e13f67e1d94108-86fe-84", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "76"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "write", + "outcome": "open-failed", + "reason": "MissingBinding" + } + }, + { + "adapter": "fastly", + "path": "/declared-kv/missing/delete", + "token": "18e13f67e24b5040-86fe-8a", + "instance": "18e13f67e24b5040-86fe-8a", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "76"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 2, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "delete", + "outcome": "open-failed", + "reason": "MissingBinding" + } + }, + { + "adapter": "fastly", + "path": "/declared-kv/missing/list", + "token": "18e13f67e261d5e0-86fe-8b", + "instance": "18e13f67e24b5040-86fe-8a", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "76"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 2, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "list", + "outcome": "open-failed", + "reason": "MissingBinding" + } + }, + { + "adapter": "fastly", + "path": "/declared-kv/linked/write", + "token": "18e13f67e275c370-86fe-8c", + "instance": "18e13f67e24b5040-86fe-8a", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "248"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"0000000000000000000000000000000d\",\"inflight\":0,\"instance\":\"18e13f67e24b5040-86fe-8a\",\"kv_outcome\":\"written\",\"mutated\":false,\"native_id\":true,\"ordinal\":3,\"path\":null,\"shared\":3,\"token\":\"18e13f67e275c370-86fe-8c\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 248, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "write", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "fastly", + "path": "/declared-kv/linked/read", + "token": "18e13f67e28af538-86fe-8d", + "instance": "18e13f67e24b5040-86fe-8a", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "249"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"0000000000000000000000000000000e\",\"inflight\":0,\"instance\":\"18e13f67e24b5040-86fe-8a\",\"kv_outcome\":\"f8-value\",\"mutated\":false,\"native_id\":true,\"ordinal\":4,\"path\":null,\"shared\":4,\"token\":\"18e13f67e28af538-86fe-8d\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 249, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "read", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "fastly", + "path": "/declared-kv/linked/list", + "token": "18e13f67e29ea060-86fe-8e", + "instance": "18e13f67e24b5040-86fe-8a", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "249"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"0000000000000000000000000000000f\",\"inflight\":0,\"instance\":\"18e13f67e24b5040-86fe-8a\",\"kv_outcome\":[\"f8-key\"],\"mutated\":false,\"native_id\":true,\"ordinal\":5,\"path\":null,\"shared\":5,\"token\":\"18e13f67e29ea060-86fe-8e\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 249, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "list", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "fastly", + "path": "/declared-kv/linked/delete", + "token": "18e13f67e2b64328-86fe-8f", + "instance": "18e13f67e24b5040-86fe-8a", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "248"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"00000000000000000000000000000010\",\"inflight\":0,\"instance\":\"18e13f67e24b5040-86fe-8a\",\"kv_outcome\":\"deleted\",\"mutated\":false,\"native_id\":true,\"ordinal\":6,\"path\":null,\"shared\":6,\"token\":\"18e13f67e2b64328-86fe-8f\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 248, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "delete", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "fastly", + "path": "/declared-kv/linked/read", + "token": "18e13f67e2cc8278-86fe-90", + "instance": "18e13f67e2cc8278-86fe-90", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "243"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"00000000000000000000000000000011\",\"inflight\":0,\"instance\":\"18e13f67e2cc8278-86fe-90\",\"kv_outcome\":null,\"mutated\":false,\"native_id\":true,\"ordinal\":1,\"path\":null,\"shared\":1,\"token\":\"18e13f67e2cc8278-86fe-90\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 243, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "read", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "fastly", + "path": "/declared-kv/linked/list", + "token": "18e13f67e2e57530-86fe-91", + "instance": "18e13f67e2cc8278-86fe-90", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "241"], + ["date", "Fri, 23 Oct 2026 19:30:42 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"00000000000000000000000000000012\",\"inflight\":0,\"instance\":\"18e13f67e2cc8278-86fe-90\",\"kv_outcome\":[],\"mutated\":false,\"native_id\":true,\"ordinal\":2,\"path\":null,\"shared\":2,\"token\":\"18e13f67e2e57530-86fe-91\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "Completed", + "body_kind": "Application", + "bytes_written": 241, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "list", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/free", + "token": "18e13f6b24482b00-86fe-13b", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["Content-Length", "277"], + ["Content-Type", "text/plain; charset=utf-8"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6b24482b00-86fe-13b\",\"declared_kv\":true,\"default_kv\":true,\"inflight\":0,\"instance\":\"18e13f6b24482b00-86fe-13b\",\"mutated\":false,\"native_id\":false,\"ordinal\":1,\"path\":null,\"shared\":1,\"token\":\"18e13f6b24482b00-86fe-13b\",\"unknown_kv\":false}" + }, + "warnings_after_request": 0, + "lifecycle": { + "ordered_events": ["request_start", "egress_terminal"], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 277, + "fallback": null + }, + "operation": null + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/missing/read", + "token": "18e13f6b391f01c0-86fe-13c", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["Content-Length", "76"], + ["Content-Type", "application/json"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "read", + "outcome": "open-failed", + "reason": "MissingBinding" + } + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/missing/read", + "token": "18e13f6b3d51be90-86fe-13d", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["Content-Length", "76"], + ["Content-Type", "application/json"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "read", + "outcome": "open-failed", + "reason": "MissingBinding" + } + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/free", + "token": "18e13f6b411cc498-86fe-13e", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["Content-Length", "277"], + ["Content-Type", "text/plain; charset=utf-8"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6b411cc498-86fe-13e\",\"declared_kv\":true,\"default_kv\":true,\"inflight\":0,\"instance\":\"18e13f6b24482b00-86fe-13b\",\"mutated\":false,\"native_id\":false,\"ordinal\":4,\"path\":null,\"shared\":4,\"token\":\"18e13f6b411cc498-86fe-13e\",\"unknown_kv\":false}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": ["request_start", "egress_terminal"], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 277, + "fallback": null + }, + "operation": null + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/missing/write", + "token": "18e13f6b449787c0-86fe-13f", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["Content-Length", "76"], + ["Content-Type", "application/json"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "write", + "outcome": "open-failed", + "reason": "MissingBinding" + } + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/missing/delete", + "token": "18e13f6b4830eea8-86fe-140", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["Content-Length", "76"], + ["Content-Type", "application/json"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "delete", + "outcome": "open-failed", + "reason": "MissingBinding" + } + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/missing/list", + "token": "18e13f6b4bc21448-86fe-141", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["Content-Length", "76"], + ["Content-Type", "application/json"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "list", + "outcome": "open-failed", + "reason": "MissingBinding" + } + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/linked/write", + "token": "18e13f6b4f566a50-86fe-142", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["Content-Length", "244"], + ["Content-Type", "text/plain; charset=utf-8"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6b4f566a50-86fe-142\",\"inflight\":0,\"instance\":\"18e13f6b24482b00-86fe-13b\",\"kv_outcome\":\"written\",\"mutated\":false,\"native_id\":false,\"ordinal\":8,\"path\":null,\"shared\":8,\"token\":\"18e13f6b4f566a50-86fe-142\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 244, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "write", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/linked/read", + "token": "18e13f6b53231638-86fe-143", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["Content-Length", "245"], + ["Content-Type", "text/plain; charset=utf-8"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6b53231638-86fe-143\",\"inflight\":0,\"instance\":\"18e13f6b24482b00-86fe-13b\",\"kv_outcome\":\"f8-value\",\"mutated\":false,\"native_id\":false,\"ordinal\":9,\"path\":null,\"shared\":9,\"token\":\"18e13f6b53231638-86fe-143\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 245, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "read", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/linked/list", + "token": "18e13f6b56e6d110-86fe-144", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["Content-Length", "247"], + ["Content-Type", "text/plain; charset=utf-8"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6b56e6d110-86fe-144\",\"inflight\":0,\"instance\":\"18e13f6b24482b00-86fe-13b\",\"kv_outcome\":[\"f8-key\"],\"mutated\":false,\"native_id\":false,\"ordinal\":10,\"path\":null,\"shared\":10,\"token\":\"18e13f6b56e6d110-86fe-144\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 247, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "list", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/linked/delete", + "token": "18e13f6b5a629dd8-86fe-145", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["Content-Length", "246"], + ["Content-Type", "text/plain; charset=utf-8"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6b5a629dd8-86fe-145\",\"inflight\":0,\"instance\":\"18e13f6b24482b00-86fe-13b\",\"kv_outcome\":\"deleted\",\"mutated\":false,\"native_id\":false,\"ordinal\":11,\"path\":null,\"shared\":11,\"token\":\"18e13f6b5a629dd8-86fe-145\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 246, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "delete", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/linked/read", + "token": "18e13f6b5e03dc90-86fe-146", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["Content-Length", "241"], + ["Content-Type", "text/plain; charset=utf-8"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6b5e03dc90-86fe-146\",\"inflight\":0,\"instance\":\"18e13f6b24482b00-86fe-13b\",\"kv_outcome\":null,\"mutated\":false,\"native_id\":false,\"ordinal\":12,\"path\":null,\"shared\":12,\"token\":\"18e13f6b5e03dc90-86fe-146\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 241, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "read", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "cloudflare", + "path": "/declared-kv/linked/list", + "token": "18e13f6b615bcda8-86fe-147", + "instance": "18e13f6b24482b00-86fe-13b", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["Content-Length", "239"], + ["Content-Type", "text/plain; charset=utf-8"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6b615bcda8-86fe-147\",\"inflight\":0,\"instance\":\"18e13f6b24482b00-86fe-13b\",\"kv_outcome\":[],\"mutated\":false,\"native_id\":false,\"ordinal\":13,\"path\":null,\"shared\":13,\"token\":\"18e13f6b615bcda8-86fe-147\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 239, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "list", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "spin", + "path": "/declared-kv/free", + "token": "18e13f6dce9432a0-86fe-1c9", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "277"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6dce9432a0-86fe-1c9\",\"declared_kv\":true,\"default_kv\":true,\"inflight\":0,\"instance\":\"18e13f6dce9432a0-86fe-1c9\",\"mutated\":false,\"native_id\":false,\"ordinal\":1,\"path\":null,\"shared\":1,\"token\":\"18e13f6dce9432a0-86fe-1c9\",\"unknown_kv\":false}" + }, + "warnings_after_request": 0, + "lifecycle": { + "ordered_events": ["request_start", "egress_terminal"], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 277, + "fallback": null + }, + "operation": null + }, + { + "adapter": "spin", + "path": "/declared-kv/missing/read", + "token": "18e13f6dcebd9078-86fe-1ca", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "76"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "read", + "outcome": "open-failed", + "reason": "InvalidConfiguration" + } + }, + { + "adapter": "spin", + "path": "/declared-kv/missing/read", + "token": "18e13f6dcecf8620-86fe-1cb", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "76"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "read", + "outcome": "open-failed", + "reason": "InvalidConfiguration" + } + }, + { + "adapter": "spin", + "path": "/declared-kv/free", + "token": "18e13f6dcedf4948-86fe-1cc", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "277"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6dcedf4948-86fe-1cc\",\"declared_kv\":true,\"default_kv\":true,\"inflight\":0,\"instance\":\"18e13f6dce9432a0-86fe-1c9\",\"mutated\":false,\"native_id\":false,\"ordinal\":4,\"path\":null,\"shared\":4,\"token\":\"18e13f6dcedf4948-86fe-1cc\",\"unknown_kv\":false}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": ["request_start", "egress_terminal"], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 277, + "fallback": null + }, + "operation": null + }, + { + "adapter": "spin", + "path": "/declared-kv/missing/write", + "token": "18e13f6dceef6260-86fe-1cd", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "76"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "write", + "outcome": "open-failed", + "reason": "InvalidConfiguration" + } + }, + { + "adapter": "spin", + "path": "/declared-kv/missing/delete", + "token": "18e13f6dcefe5298-86fe-1ce", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "76"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "delete", + "outcome": "open-failed", + "reason": "InvalidConfiguration" + } + }, + { + "adapter": "spin", + "path": "/declared-kv/missing/list", + "token": "18e13f6dcf0f0fc0-86fe-1cf", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 500, + "headers": [ + ["content-type", "application/json"], + ["content-length", "76"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"error\":{\"kind\":\"internal\",\"message\":\"internal server error\",\"status\":500}}" + }, + "warnings_after_request": 1, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 76, + "fallback": null + }, + "operation": { + "store": "missing", + "operation": "list", + "outcome": "open-failed", + "reason": "InvalidConfiguration" + } + }, + { + "adapter": "spin", + "path": "/declared-kv/linked/write", + "token": "18e13f6dcf200398-86fe-1d0", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "244"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6dcf200398-86fe-1d0\",\"inflight\":0,\"instance\":\"18e13f6dce9432a0-86fe-1c9\",\"kv_outcome\":\"written\",\"mutated\":false,\"native_id\":false,\"ordinal\":8,\"path\":null,\"shared\":8,\"token\":\"18e13f6dcf200398-86fe-1d0\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 244, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "write", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "spin", + "path": "/declared-kv/linked/read", + "token": "18e13f6dcf361408-86fe-1d1", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "245"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6dcf361408-86fe-1d1\",\"inflight\":0,\"instance\":\"18e13f6dce9432a0-86fe-1c9\",\"kv_outcome\":\"f8-value\",\"mutated\":false,\"native_id\":false,\"ordinal\":9,\"path\":null,\"shared\":9,\"token\":\"18e13f6dcf361408-86fe-1d1\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 245, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "read", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "spin", + "path": "/declared-kv/linked/list", + "token": "18e13f6dcf46bda8-86fe-1d2", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "247"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6dcf46bda8-86fe-1d2\",\"inflight\":0,\"instance\":\"18e13f6dce9432a0-86fe-1c9\",\"kv_outcome\":[\"f8-key\"],\"mutated\":false,\"native_id\":false,\"ordinal\":10,\"path\":null,\"shared\":10,\"token\":\"18e13f6dcf46bda8-86fe-1d2\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 247, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "list", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "spin", + "path": "/declared-kv/linked/delete", + "token": "18e13f6dcf62a248-86fe-1d3", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "246"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6dcf62a248-86fe-1d3\",\"inflight\":0,\"instance\":\"18e13f6dce9432a0-86fe-1c9\",\"kv_outcome\":\"deleted\",\"mutated\":false,\"native_id\":false,\"ordinal\":11,\"path\":null,\"shared\":11,\"token\":\"18e13f6dcf62a248-86fe-1d3\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 246, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "delete", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "spin", + "path": "/declared-kv/linked/read", + "token": "18e13f6dcf742e78-86fe-1d4", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "241"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6dcf742e78-86fe-1d4\",\"inflight\":0,\"instance\":\"18e13f6dce9432a0-86fe-1c9\",\"kv_outcome\":null,\"mutated\":false,\"native_id\":false,\"ordinal\":12,\"path\":null,\"shared\":12,\"token\":\"18e13f6dcf742e78-86fe-1d4\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 241, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "read", + "outcome": "success", + "reason": null + } + }, + { + "adapter": "spin", + "path": "/declared-kv/linked/list", + "token": "18e13f6dcf869568-86fe-1d5", + "instance": "18e13f6dce9432a0-86fe-1c9", + "status": "pass", + "wire": { + "status": 200, + "headers": [ + ["content-type", "text/plain; charset=utf-8"], + ["content-length", "239"], + ["connection", "close"], + ["date", "Fri, 23 Oct 2026 19:31:07 GMT"] + ], + "body": "{\"body\":\"\",\"builds\":1,\"correlation\":\"18e13f6dcf869568-86fe-1d5\",\"inflight\":0,\"instance\":\"18e13f6dce9432a0-86fe-1c9\",\"kv_outcome\":[],\"mutated\":false,\"native_id\":false,\"ordinal\":13,\"path\":null,\"shared\":13,\"token\":\"18e13f6dcf869568-86fe-1d5\"}" + }, + "warnings_after_request": null, + "lifecycle": { + "ordered_events": [ + "request_start", + "declared_kv_enter", + "declared_kv_result", + "egress_terminal" + ], + "outcome": "HostHandoff", + "body_kind": "Application", + "bytes_written": 239, + "fallback": null + }, + "operation": { + "store": "linked", + "operation": "list", + "outcome": "success", + "reason": null + } + } + ], + "build_records": [ + { + "artifact": { + "bytes": 3632585, + "fnv1a64": "83bce60c991e159b", + "path": "/private/tmp/edgezero-f8-operations-correlated/artifacts/fixture-fastly-retained-app.wasm", + "purpose": "non-security build identity" + }, + "config": { + "bytes": 463, + "fnv1a64": "08fc48ea1ca6c072", + "path": "/private/tmp/edgezero-f8-operations-correlated/fastly.toml", + "purpose": "non-security build identity" + }, + "event": "build", + "repetition": 0, + "target": "wasm32-wasip1", + "variant": "c" + }, + { + "adapter": "cloudflare", + "artifact": { + "bytes": 1780906, + "fnv1a64": "f260b43b08b2233c", + "path": "/private/tmp/edgezero-f8-operations-correlated/artifacts/build/index_bg.wasm", + "purpose": "non-security build identity" + }, + "event": "build" + }, + { + "adapter": "spin", + "artifact": { + "bytes": 2312993, + "fnv1a64": "eb35d80e88702611", + "path": "/private/tmp/edgezero-f8-operations-correlated/artifacts/fixture_spin.wasm", + "purpose": "non-security build identity" + }, + "event": "build" + } + ], + "selected_artifacts": [ + { + "path": "/private/tmp/edgezero-f8-operations-correlated/artifacts/fixture-fastly-retained-app.wasm", + "bytes": 3632585, + "sha256": "404a57f63f2103641fb1d07551d63a7e55009fe0959316ed9f65ed2f16a58eae" + }, + { + "path": "/private/tmp/edgezero-f8-operations-correlated/fastly.toml", + "bytes": 463, + "sha256": "4444af855e1f5db54da213ddfc6a84ea34aa52f9fd868d047521b0ab78a3fd18" + }, + { + "path": "/private/tmp/edgezero-f8-operations-correlated/artifacts/build/index_bg.wasm", + "bytes": 1780906, + "sha256": "1eb166a3a7534e3b457e04024962555bcb0c06b4bda0cab7edb83d764e1f1af5" + }, + { + "path": "/private/tmp/edgezero-f8-operations-correlated/artifacts/fixture_spin.wasm", + "bytes": 2312993, + "sha256": "f5d5cdff5437dcd1dc37706807c2b314e33603e4afefd0935d0a08b2dad52456" + } + ] + }, + "warning_attribution": { + "original_prefix": "One observed instance, exact 0/1/1/1 counts; not restarted or retried.", + "fastly": "Named-endpoint aggregate count after each request and shutdown equals distinct observed missing-operation instances. Lines lack instance tags, so late cross-instance warnings are not independently attributable.", + "cloudflare_spin": "Exact per-instance counts: one for observed missing-operation instances, zero for healthy/free-only instances." + }, + "earlier_runs": [ + { + "directory": "/private/tmp/edgezero-f8-operations-smoke", + "exit_code": 1, + "events": { + "path": "/private/tmp/edgezero-f8-operations-smoke/events.jsonl", + "bytes": 1631214, + "sha256": "beb99125c9ca9c07c1eed9cbe5773842e5393f28044eee3c88c8c6ff410d95ca" + }, + "summary": { + "path": "/private/tmp/edgezero-f8-operations-smoke/summary.json", + "bytes": 369916, + "sha256": "b8e00e54495f583fd10f650b78e7a93ebbd0b1aba9c9274e39b5d9548a646b2f" + }, + "log": { + "path": "/private/tmp/edgezero-f8-operations-smoke.log", + "bytes": 9564, + "sha256": "324e6370b2a0383b7d5b779ed4b7bfee1cca84b4065d9fd94d999e6d9e3c0384" + }, + "f8_cases": 31, + "post_shutdown": [ + { + "adapter": "fastly", + "error": "declared KV matrix did not execute every request", + "event": "declared-kv-after-shutdown", + "instance": "18e13e9b4c7a6618-17359-84", + "qualification": false, + "requests": 6, + "status": "fail", + "warnings": 2 + }, + { + "adapter": "cloudflare", + "error": null, + "event": "declared-kv-after-shutdown", + "instance": "18e13e9e993ebaa0-17359-96", + "qualification": false, + "requests": 13, + "status": "pass", + "warnings": 1 + }, + { + "adapter": "spin", + "error": null, + "event": "declared-kv-after-shutdown", + "instance": "18e13ea24b7722b8-17359-128", + "qualification": false, + "requests": 13, + "status": "pass", + "warnings": 1 + } + ], + "failures": [ + { + "adapter": "fastly", + "error": "declared KV matrix did not execute every request", + "event": "declared-kv-after-shutdown", + "instance": "18e13e9b4c7a6618-17359-84", + "qualification": false, + "requests": 6, + "status": "fail", + "warnings": 2 + }, + { + "adapter": "fastly", + "error": "declared KV sandbox turnover: deduplication unverified", + "repetition": 0, + "status": "fail", + "variant": "c" + } + ], + "unverified": [ + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "per-request", + "status": "unverified" + }, + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "retained", + "status": "unverified" + } + ], + "interpretation": "New seven-request same-instance assertion failed at the pinned host limit. Original four-request proof passed; no retry or provider-setting change waived it." + }, + { + "directory": "/private/tmp/edgezero-f8-operations-final", + "exit_code": 2, + "events": { + "path": "/private/tmp/edgezero-f8-operations-final/events.jsonl", + "bytes": 1851643, + "sha256": "1fb9af2b663ad04e982d72451a26334e6683153886c4228045736c01bec0a9ba" + }, + "summary": { + "path": "/private/tmp/edgezero-f8-operations-final/summary.json", + "bytes": 486324, + "sha256": "03697a119eae4cb9db847108d686ed963cd14fa75086024a51797e26877b7a4f" + }, + "log": { + "path": "/private/tmp/edgezero-f8-operations-final.log", + "bytes": 7777, + "sha256": "74c182281b92493d9fe04e46b4ae13dea67bc965792e00a4358af08d04ce6908" + }, + "f8_cases": 39, + "post_shutdown": [ + { + "adapter": "fastly", + "error": null, + "event": "declared-kv-after-shutdown", + "failed_instances": [ + "18e13ee4679143d8-22d2-84", + "18e13ee4681498f0-22d2-8a" + ], + "instance": "18e13ee4679143d8-22d2-84", + "qualification": false, + "requests": 13, + "status": "pass", + "warnings": 2 + }, + { + "adapter": "cloudflare", + "error": null, + "event": "declared-kv-after-shutdown", + "failed_instances": ["18e13ee853536c58-22d2-13b"], + "instance": "18e13ee853536c58-22d2-13b", + "qualification": false, + "requests": 13, + "status": "pass", + "warnings": 1 + }, + { + "adapter": "spin", + "error": null, + "event": "declared-kv-after-shutdown", + "failed_instances": ["18e13eeb4f383b50-22d2-1cd"], + "instance": "18e13eeb4f383b50-22d2-1cd", + "qualification": false, + "requests": 13, + "status": "pass", + "warnings": 1 + } + ], + "failures": [], + "unverified": [ + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "per-request", + "status": "unverified" + }, + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "retained", + "status": "unverified" + } + ], + "interpretation": "Intermediate local pass before final receipt-order, nullable-field and success-body correlation assertions." + }, + { + "directory": "/private/tmp/edgezero-f8-operations-publish", + "exit_code": 2, + "events": { + "path": "/private/tmp/edgezero-f8-operations-publish/events.jsonl", + "bytes": 1850127, + "sha256": "639b85f0c342c3bb5b2c214a5fbc3a3d33ec1fdcb4355823e1d9d5865c369b10" + }, + "summary": { + "path": "/private/tmp/edgezero-f8-operations-publish/summary.json", + "bytes": 485078, + "sha256": "2cc0e7fc2c49bf3222a29def2ed0b88dc02f2ba8a07579f1ea41d46b3e125b16" + }, + "log": { + "path": "/private/tmp/edgezero-f8-operations-publish.log", + "bytes": 7443, + "sha256": "79f449b8a4eb0dd264f2bf892357801328966a5a466ec1866ed24ccfb908f30e" + }, + "f8_cases": 39, + "post_shutdown": [ + { + "adapter": "fastly", + "error": null, + "event": "declared-kv-after-shutdown", + "failed_instances": [ + "18e13f4e1d558170-753f-84", + "18e13f4e1de79fb0-753f-8a" + ], + "instance": "18e13f4e1d558170-753f-84", + "qualification": false, + "requests": 13, + "status": "pass", + "warnings": 2 + }, + { + "adapter": "cloudflare", + "error": null, + "event": "declared-kv-after-shutdown", + "failed_instances": ["18e13f514405aea0-753f-13b"], + "instance": "18e13f514405aea0-753f-13b", + "qualification": false, + "requests": 13, + "status": "pass", + "warnings": 1 + }, + { + "adapter": "spin", + "error": null, + "event": "declared-kv-after-shutdown", + "failed_instances": ["18e13f53ddc4ba48-753f-1cd"], + "instance": "18e13f53ddc4ba48-753f-1cd", + "qualification": false, + "requests": 13, + "status": "pass", + "warnings": 1 + } + ], + "failures": [], + "unverified": [ + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "per-request", + "status": "unverified" + }, + { + "adapter": "spin", + "assertion": "same_guest_overlap", + "attempt_limit": 3, + "mode": "retained", + "status": "unverified" + } + ], + "interpretation": "Intermediate local pass before final receipt-order, nullable-field and success-body correlation assertions." + } + ], + "verification": { + "passed": [ + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T192245Z-root-fmt.log", + "bytes": 112, + "sha256": "42b9bdc49770be13a86d5573efca2f6e61a4768082527791cdc92d356bdfcf24", + "command": "cargo --offline --locked fmt --all -- --check", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T192245Z-root-clippy.log", + "bytes": 481, + "sha256": "b4c27f6d839eb6951bc680527551cf9fdd476a772d69249663dd583b5877835a", + "command": "cargo clippy --offline --locked --workspace --all-targets --all-features -- -D warnings", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T192245Z-root-test-unsandboxed.log", + "bytes": 184563, + "sha256": "b5813afb9729bbe0cb5d2b43e9b2c7ee550e2f540934500c92ce94a6ead7fd6b", + "command": "cargo test --offline --locked --workspace --all-targets", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T192245Z-root-check-features.log", + "bytes": 486, + "sha256": "516aacfb12291e2d23385f570688a700df65f13b6cf3e6660b58dcb963448d9f", + "command": "cargo check --offline --locked --workspace --all-targets --features \"fastly cloudflare spin\"", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T192245Z-root-check-spin.log", + "bytes": 488, + "sha256": "62620caa0bfd56d6c02916fcf13da20f1254ef74e3d86252e5bc5f48febe7793", + "command": "cargo check --offline --locked -p edgezero-adapter-spin --target wasm32-wasip2 --features spin", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T192245Z-fixture-core-test.log", + "bytes": 2858, + "sha256": "fdcada5442b128c7549c1082c5c97bf665b5eaf17d77b49ff10bbc7cf087bf90", + "command": "cargo test --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-core --all-targets --all-features", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T192245Z-fixture-core-clippy.log", + "bytes": 846, + "sha256": "4586db0141308c7fc61ff64602c803e73a05d2a6f10239e1b21b25da4058b213", + "command": "cargo clippy --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-core --all-targets --all-features -- -D warnings", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T192245Z-fixture-fastly-clippy.log", + "bytes": 906, + "sha256": "8e7275b1ec64632224eec0e3eb56c72c84d4ac666d74fd343f1e35e000cf3966", + "command": "cargo clippy --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-fastly --target wasm32-wasip1 --all-targets --features progress -- -D warnings", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T192245Z-fixture-cloudflare-clippy.log", + "bytes": 932, + "sha256": "c8cd86475b06fa3fa3872b1327605adab759906e920c3b60320a6af02e416675", + "command": "cargo clippy --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-cloudflare --target wasm32-unknown-unknown --all-targets --features qualification -- -D warnings", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T192245Z-fixture-spin-clippy.log", + "bytes": 905, + "sha256": "2e9a5e82b837a9f350322f8bf38b6417e2d1bc398201a5fe6b8118c60188d102", + "command": "cargo clippy --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-spin --target wasm32-wasip2 --all-targets --features qualification -- -D warnings", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T193047Z-fixture-harness-test-final84.log", + "bytes": 8165, + "sha256": "6795f4720eb394c35220c850969cd7697fff6f444f5d633223ec7c966d12cd28", + "command": "cargo test --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness --all-targets --all-features", + "exit_code": 0 + }, + { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T193047Z-fixture-harness-clippy-final84.log", + "bytes": 460, + "sha256": "b4f097d836d9fb9004cd8b4b255ef2ac3b1ad9f74c19d790afe7541644a24ad5", + "command": "cargo clippy --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness --all-targets --all-features -- -D warnings", + "exit_code": 0 + } + ], + "workspace_tests": { + "passed": 2193, + "failed": 0, + "ignored": 1, + "binaries": 28 + }, + "fixture_tests": { + "core": 20, + "harness": 84, + "failed": 0 + }, + "red_green": [ + { + "sentinel": "declared_kv_final_validation_rejects_late_duplicates_and_missing_cases", + "red": { + "path": "/private/tmp/edgezero-f8-operation-order-red.log", + "bytes": 1376, + "sha256": "9ecc89c3a55925e49fa1a132ec739eb5dd50c16436ac83c0f994e94824174adb" + }, + "red_exit_code": 101, + "green": { + "path": "/private/tmp/edgezero-f8-operation-order-green.log", + "bytes": 1136, + "sha256": "78fd3ea05979b19ee2055e32b45746f7386b89917a6135f3850e90da1c77406d" + }, + "green_exit_code": 0 + }, + { + "sentinel": "declared_kv_linked_result_requires_a_present_field_even_when_null", + "red": { + "path": "/private/tmp/edgezero-f8-operation-null-red.log", + "bytes": 5815, + "sha256": "e2624907de74cd58ed6cbc1768e6d8f2fda4c8e769ce3cf9e147178c7f191132" + }, + "red_exit_code": 101, + "green": { + "path": "/private/tmp/edgezero-f8-operation-null-green.log", + "bytes": 1230, + "sha256": "6246a88fefea8563e5a7affae5cf6293a9f8ca99f17fdc4a77baaea6e9292f3e" + }, + "green_exit_code": 0 + }, + { + "sentinel": "declared_kv_success_body_requires_request_and_instance_correlation", + "red": { + "path": "/private/tmp/edgezero-f8-operation-correlation-red.log", + "bytes": 2801, + "sha256": "d51726198622532a507d579ad756834bae9b2e420e7581373f954c7721bf0692" + }, + "red_exit_code": 101, + "green": { + "path": "/private/tmp/edgezero-f8-operation-correlation-green.log", + "bytes": 1325, + "sha256": "74bc42dda523df892df0e055d6a079a73a734a49ca733775e8f793c0a5615d10" + }, + "green_exit_code": 0 + } + ], + "restricted_attempt": { + "log": { + "path": "/private/tmp/edgezero-f8-operations-validator-20261023T192245Z-root-test.log", + "bytes": 53150, + "sha256": "9ef6d21137f322e28dbc240a3cffed97072d23f51da094a16b065df1211279f2" + }, + "exit_code": 101, + "interpretation": "Socket operations denied by sandbox. Unchanged escalated run passed; not a product regression or passing evidence." + } + }, + "remaining": [ + "Consumer-owned route acceptance and actual outage/restoration of the same declaration on a later request.", + "Hosted and frozen-candidate qualification; no capability promotion.", + "Fastly G2 and dependent head-first/eager driver migration remain open.", + "Fastly SDK framing parity and Cloudflare front-proxy overflow recovery remain separate failed/open gates.", + "Spin retained-mode overlap remains unverified. Per-request overlap observed once locally does not qualify hosted behavior." + ] +} diff --git a/docs/audits/evidence/2026-10-23-fastly-readiness-matrix-probes.jsonl b/docs/audits/evidence/2026-10-23-fastly-readiness-matrix-probes.jsonl new file mode 100644 index 000000000..7cf9fc6c7 --- /dev/null +++ b/docs/audits/evidence/2026-10-23-fastly-readiness-matrix-probes.jsonl @@ -0,0 +1,540 @@ +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":63933,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":36323,"slot":0,"token":"18e15c0d9a665cd0-b8b0-0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c0d9a665cd0-b8b0-0","watchdog":false,"workers_joined":1,"writes":[{"at_us":36370,"bytes":57,"kind":"head","slot":0},{"at_us":36569,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":6490,"operation":"dispatch","outcome":"pending","slot":0,"start_us":2814},{"end_us":12309,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6555,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":13190,"operation":"head-poll","outcome":"ready","slot":0,"start_us":12320},{"bytes":1,"capacity":1,"end_us":17524,"operation":"read","slot":0,"start_us":13193},{"capacity":1,"end_us":62969,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":17530},{"end_us":63926,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":62982}],"schema":2,"slots":[{"at_us":63926,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0d9a665cd0-b8b0-0","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":58771,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5260,"slot":0,"token":"18e15c0d9ff40238-b8b0-1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c0d9ff40238-b8b0-1","watchdog":false,"workers_joined":1,"writes":[{"at_us":5283,"bytes":57,"kind":"head","slot":0},{"at_us":5289,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5001,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5011,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5005},{"end_us":5022,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5013,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5072,"operation":"read","slot":0,"start_us":5023},{"end_us":58692,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5075,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":58756,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58697},{"end_us":58768,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58761}],"schema":2,"slots":[{"at_us":58768,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0d9ff40238-b8b0-1","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":55897,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5369,"slot":0,"token":"18e15c0da3abc690-b8b0-2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c0da3abc690-b8b0-2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5380,"bytes":57,"kind":"head","slot":0},{"at_us":5386,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5069,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5078,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5072},{"end_us":5091,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5079,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5116,"operation":"read","slot":0,"start_us":5092},{"end_us":55819,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5118,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":55881,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":55825},{"end_us":55894,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55887}],"schema":2,"slots":[{"at_us":55894,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0da3abc690-b8b0-2","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":59917,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5882,"slot":0,"token":"18e15c0da7449138-b8b0-3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c0da7449138-b8b0-3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5897,"bytes":57,"kind":"head","slot":0},{"at_us":5907,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":48,"operation":"handoff","start_us":48},{"end_us":5641,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5650,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5645},{"end_us":5657,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5651,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5701,"operation":"read","slot":0,"start_us":5658},{"end_us":59854,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5704,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":59899,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59859},{"end_us":59913,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59904}],"schema":2,"slots":[{"at_us":59913,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0da7449138-b8b0-3","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":58750,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5428,"slot":0,"token":"18e15c0dab05def8-b8b0-4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c0dab05def8-b8b0-4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5441,"bytes":57,"kind":"head","slot":0},{"at_us":5447,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":44,"operation":"handoff","start_us":44},{"end_us":5144,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5158,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5148},{"end_us":5170,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5159,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5211,"operation":"read","slot":0,"start_us":5171},{"end_us":58678,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5213,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":58732,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58683},{"end_us":58746,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58739}],"schema":2,"slots":[{"at_us":58746,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0dab05def8-b8b0-4","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":500321,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5747,"slot":0,"token":"18e15c0daecf1fe0-b8b0-5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c0daecf1fe0-b8b0-5","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":500307,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500317,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500317}],"schema":2,"slots":[{"at_us":500317,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c0daecf1fe0-b8b0-5","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":501707,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6517,"slot":0,"token":"18e15c0dccde6fe0-b8b0-6"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c0dccde6fe0-b8b0-6","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":78,"operation":"dispatch","outcome":"pending","slot":0,"start_us":32},{"end_us":501506,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501593,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501593}],"schema":2,"slots":[{"at_us":501593,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c0dccde6fe0-b8b0-6","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":501000,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5478,"slot":0,"token":"18e15c0deb4025a0-b8b0-7"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c0deb4025a0-b8b0-7","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":270,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"body_reads":0,"deadline_us":500000,"end_us":278,"operation":"handoff","start_us":278},{"end_us":500983,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":281,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500996,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500996}],"schema":2,"slots":[{"at_us":500996,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c0deb4025a0-b8b0-7","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":502062,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5839,"slot":0,"token":"18e15c0e0951ded0-b8b0-8"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c0e0951ded0-b8b0-8","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"end_us":499662,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502048,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499742,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502052,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502052}],"schema":2,"slots":[{"at_us":502052,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c0e0951ded0-b8b0-8","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":500644,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5434,"slot":0,"token":"18e15c0e27736ac8-b8b0-9"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c0e27736ac8-b8b0-9","watchdog":false,"workers_joined":1,"writes":[{"at_us":5451,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5091,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":42,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5114,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5094},{"end_us":500624,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5115,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500638,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500629}],"schema":2,"slots":[{"at_us":500638,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0e27736ac8-b8b0-9","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":500399,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5232,"slot":0,"token":"18e15c0e457cd6f8-b8b0-a"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c0e457cd6f8-b8b0-a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5250,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":4992,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5001,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4996},{"end_us":500376,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5003,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500391,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500382}],"schema":2,"slots":[{"at_us":500391,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0e457cd6f8-b8b0-a","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":507794,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":7442,"slot":0,"token":"18e15c0e63994e28-b8b0-b"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c0e63994e28-b8b0-b","watchdog":false,"workers_joined":1,"writes":[{"at_us":7552,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":230,"operation":"dispatch","outcome":"pending","slot":0,"start_us":47},{"body_reads":0,"deadline_us":500000,"end_us":235,"operation":"handoff","start_us":235},{"end_us":5317,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":236,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5330,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5322},{"end_us":507153,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5332,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":507596,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":507362}],"schema":2,"slots":[{"at_us":507596,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0e63994e28-b8b0-b","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":500040,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6649,"slot":0,"token":"18e15c0e8248d5c8-b8b0-c"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c0e8248d5c8-b8b0-c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6685,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":396,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"body_reads":0,"deadline_us":500000,"end_us":399,"operation":"handoff","start_us":399},{"end_us":3209,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":400,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":3239,"operation":"head-poll","outcome":"ready","slot":0,"start_us":3220},{"end_us":500015,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":3240,"status":"FastlyStatus::OK","timeout_ms":496},{"end_us":500034,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500027}],"schema":2,"slots":[{"at_us":500034,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0e8248d5c8-b8b0-c","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":500549,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5476,"slot":0,"token":"18e15c0ea0837f70-b8b0-d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c0ea0837f70-b8b0-d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5494,"bytes":57,"kind":"head","slot":0},{"at_us":5501,"bytes":1,"kind":"body","slot":0},{"at_us":310553,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":33},{"end_us":5235,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5246,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5240},{"end_us":5255,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5247,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5297,"operation":"read","slot":0,"start_us":5256},{"end_us":310291,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5300,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":310380,"operation":"read","slot":0,"start_us":310303},{"end_us":500502,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310384,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":500526,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500507}],"schema":2,"slots":[{"at_us":500526,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0ea0837f70-b8b0-d","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":500215,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6478,"slot":0,"token":"18e15c0ebea5bb30-b8b0-e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c0ebea5bb30-b8b0-e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6497,"bytes":57,"kind":"head","slot":0},{"at_us":6506,"bytes":1,"kind":"body","slot":0},{"at_us":309433,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":64,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":6151,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6171,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6155},{"end_us":6178,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6172,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6223,"operation":"read","slot":0,"start_us":6178},{"end_us":309037,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6230,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309113,"operation":"read","slot":0,"start_us":309044},{"end_us":500196,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309118,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500208,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500201}],"schema":2,"slots":[{"at_us":500208,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0ebea5bb30-b8b0-e","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":501581,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5551,"slot":0,"token":"18e15c0edca0cf80-b8b0-f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c0edca0cf80-b8b0-f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5573,"bytes":57,"kind":"head","slot":0},{"at_us":5580,"bytes":1,"kind":"body","slot":0},{"at_us":309723,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":44,"operation":"handoff","start_us":44},{"end_us":5318,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5330,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5324},{"end_us":5336,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5332,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5384,"operation":"read","slot":0,"start_us":5337},{"end_us":309408,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5386,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309459,"operation":"read","slot":0,"start_us":309414},{"end_us":501555,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309462,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501575,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501565}],"schema":2,"slots":[{"at_us":501575,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0edca0cf80-b8b0-f","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":500550,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6434,"slot":0,"token":"18e15c0efab30998-b8b0-10"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c0efab30998-b8b0-10","watchdog":false,"workers_joined":1,"writes":[{"at_us":6451,"bytes":57,"kind":"head","slot":0},{"at_us":6457,"bytes":1,"kind":"body","slot":0},{"at_us":307245,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"end_us":6194,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":42,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6203,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6198},{"end_us":6210,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6204,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6259,"operation":"read","slot":0,"start_us":6211},{"end_us":306956,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6262,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":307030,"operation":"read","slot":0,"start_us":306967},{"end_us":500532,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307034,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500543,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500537}],"schema":2,"slots":[{"at_us":500543,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0efab30998-b8b0-10","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":500374,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6364,"slot":0,"token":"18e15c0f18c9d790-b8b0-11"},{"path":"/readiness/siblings/1","received_us":6370,"slot":1,"token":"18e15c0f18c9d790-b8b0-11"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c0f18c9d790-b8b0-11","watchdog":false,"workers_joined":2,"writes":[{"at_us":6384,"bytes":57,"kind":"head","slot":0},{"at_us":6392,"bytes":57,"kind":"head","slot":1},{"at_us":6399,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":69,"operation":"dispatch","outcome":"pending","slot":1,"start_us":55},{"end_us":6139,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6150,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6144},{"end_us":6154,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6151,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6156,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6155},{"end_us":6159,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6156,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6200,"operation":"read","slot":1,"start_us":6160},{"end_us":6207,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6205,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6217,"operation":"read","slot":1,"start_us":6208},{"end_us":6219,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6218,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6225,"operation":"read","slot":1,"start_us":6220},{"end_us":6228,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6226,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6237,"operation":"read","slot":1,"start_us":6231},{"end_us":6240,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6237},{"end_us":500261,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6243,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500353,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500274}],"schema":2,"slots":[{"at_us":500353,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6240,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0f18c9d790-b8b0-11","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":500919,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5514,"slot":0,"token":"18e15c0f36f45880-b8b0-12"},{"path":"/readiness/siblings/1","received_us":5580,"slot":1,"token":"18e15c0f36f45880-b8b0-12"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c0f36f45880-b8b0-12","watchdog":false,"workers_joined":2,"writes":[{"at_us":5541,"bytes":57,"kind":"head","slot":0},{"at_us":5588,"bytes":57,"kind":"head","slot":1},{"at_us":5594,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":89,"operation":"dispatch","outcome":"pending","slot":0,"start_us":48},{"end_us":186,"operation":"dispatch","outcome":"pending","slot":1,"start_us":122},{"end_us":5169,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":189,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5190,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5176},{"end_us":5204,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5192,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5207,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5206},{"end_us":5211,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5208,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5264,"operation":"read","slot":1,"start_us":5212},{"end_us":5271,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5267,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5328,"operation":"read","slot":1,"start_us":5272},{"end_us":5333,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5329},{"end_us":500902,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5336,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500916,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500908}],"schema":2,"slots":[{"at_us":500916,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5333,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0f36f45880-b8b0-12","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":500439,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6547,"slot":0,"token":"18e15c0f5528d420-b8b0-13"},{"path":"/readiness/siblings/1","received_us":6593,"slot":1,"token":"18e15c0f5528d420-b8b0-13"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c0f5528d420-b8b0-13","watchdog":false,"workers_joined":2,"writes":[{"at_us":6559,"bytes":57,"kind":"head","slot":0},{"at_us":6599,"bytes":57,"kind":"head","slot":1},{"at_us":6605,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":61,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":124,"operation":"dispatch","outcome":"pending","slot":1,"start_us":97},{"body_reads":0,"deadline_us":500000,"end_us":126,"operation":"handoff","start_us":126},{"end_us":6276,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":127,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6291,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6281},{"end_us":6310,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6293,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6316,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6312},{"end_us":6324,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6317,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6351,"operation":"read","slot":1,"start_us":6325},{"end_us":6358,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6353,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6368,"operation":"read","slot":1,"start_us":6359},{"end_us":6370,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6368,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6379,"operation":"read","slot":1,"start_us":6371},{"end_us":6382,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6380,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6388,"operation":"read","slot":1,"start_us":6383},{"end_us":6391,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6389},{"end_us":500402,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6394,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500436,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500412}],"schema":2,"slots":[{"at_us":500436,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6391,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0f5528d420-b8b0-13","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":501058,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5927,"slot":1,"token":"18e15c0f734bf270-b8b0-14"},{"path":"/readiness/siblings/0","received_us":5938,"slot":0,"token":"18e15c0f734bf270-b8b0-14"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c0f734bf270-b8b0-14","watchdog":false,"workers_joined":2,"writes":[{"at_us":5951,"bytes":57,"kind":"head","slot":1},{"at_us":5959,"bytes":57,"kind":"head","slot":0},{"at_us":11314,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":82,"operation":"dispatch","outcome":"pending","slot":1,"start_us":61},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"end_us":5639,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5650,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5643},{"end_us":5661,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5651,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5665,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5663},{"end_us":10995,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5666,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11054,"operation":"read","slot":1,"start_us":11000},{"end_us":11063,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11057,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11075,"operation":"read","slot":1,"start_us":11064},{"end_us":11080,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11076},{"end_us":500828,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11086,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501048,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500880}],"schema":2,"slots":[{"at_us":501048,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11080,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0f734bf270-b8b0-14","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":501944,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":16374,"slot":0,"token":"18e15c0f91819ad8-b8b0-15"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c0f91819ad8-b8b0-15","watchdog":false,"workers_joined":1,"writes":[{"at_us":16390,"bytes":57,"kind":"head","slot":0},{"at_us":16396,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":8873,"operation":"dispatch","outcome":"pending","slot":0,"start_us":30},{"end_us":15669,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":8887,"status":"FastlyStatus::OK","timeout_ms":491},{"end_us":15754,"operation":"head-poll","outcome":"ready","slot":0,"start_us":15674},{"end_us":15771,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":15757,"status":"FastlyStatus::OK","timeout_ms":484},{"bytes":1,"capacity":1,"end_us":15843,"operation":"read","slot":0,"start_us":15772},{"end_us":501923,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":15846,"status":"FastlyStatus::OK","timeout_ms":484},{"end_us":501938,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501929}],"schema":2,"slots":[{"at_us":501938,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0f91819ad8-b8b0-15","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":502793,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5224,"slot":0,"token":"18e15c0fafb4e5c8-b8b0-16"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c0fafb4e5c8-b8b0-16","watchdog":false,"workers_joined":1,"writes":[{"at_us":5236,"bytes":57,"kind":"head","slot":0},{"at_us":5243,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":4910,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4923,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4914},{"end_us":4930,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4924,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4994,"operation":"read","slot":0,"start_us":4931},{"end_us":502766,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4997,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":502786,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502773}],"schema":2,"slots":[{"at_us":502786,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0fafb4e5c8-b8b0-16","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":501302,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6470,"slot":0,"token":"18e15c0fcde4dd28-b8b0-17"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c0fcde4dd28-b8b0-17","watchdog":false,"workers_joined":1,"writes":[{"at_us":6493,"bytes":57,"kind":"head","slot":0},{"at_us":6500,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"end_us":6112,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6123,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6116},{"end_us":6132,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6125,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6188,"operation":"read","slot":0,"start_us":6133},{"end_us":501280,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6191,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501296,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501285}],"schema":2,"slots":[{"at_us":501296,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0fcde4dd28-b8b0-17","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":500339,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6429,"slot":0,"token":"18e15c0fec033ca0-b8b0-18"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c0fec033ca0-b8b0-18","watchdog":false,"workers_joined":1,"writes":[{"at_us":6445,"bytes":57,"kind":"head","slot":0},{"at_us":6454,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":44,"operation":"handoff","start_us":44},{"end_us":6176,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6185,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6179},{"end_us":6191,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6186,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6227,"operation":"read","slot":0,"start_us":6192},{"end_us":500316,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6230,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500331,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500324}],"schema":2,"slots":[{"at_us":500331,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c0fec033ca0-b8b0-18","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":5280,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5566,"slot":0,"token":"18e15c100a4ad2e0-b8b0-19"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c100a4ad2e0-b8b0-19","watchdog":false,"workers_joined":1,"writes":[{"at_us":5584,"bytes":57,"kind":"head","slot":0},{"at_us":5590,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":5137,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5154,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5141},{"end_us":5163,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5155,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5218,"operation":"read","slot":0,"start_us":5164},{"end_us":5223,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5221,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5233,"operation":"read","slot":0,"start_us":5224},{"end_us":5235,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5234,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5247,"operation":"read","slot":0,"start_us":5236},{"end_us":5250,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5247,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5272,"operation":"read","slot":0,"start_us":5251},{"end_us":5277,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5273}],"schema":2,"slots":[{"at_us":5277,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c100a4ad2e0-b8b0-19","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":5889,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6153,"slot":0,"token":"18e15c100ae8ff10-b8b0-1a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c100ae8ff10-b8b0-1a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6171,"bytes":57,"kind":"head","slot":0},{"at_us":6177,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":65,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":5791,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5805,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5798},{"end_us":5813,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5806,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5859,"operation":"read","slot":0,"start_us":5814},{"end_us":5865,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5862,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5881,"operation":"read","slot":0,"start_us":5866},{"end_us":5886,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5882}],"schema":2,"slots":[{"at_us":5886,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c100ae8ff10-b8b0-1a","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":5009,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5602,"slot":0,"token":"18e15c100b9ac2e0-b8b0-1b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c100b9ac2e0-b8b0-1b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5619,"bytes":57,"kind":"head","slot":0},{"at_us":5627,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":90,"operation":"dispatch","outcome":"pending","slot":0,"start_us":49},{"body_reads":0,"deadline_us":500000,"end_us":94,"operation":"handoff","start_us":94},{"end_us":4899,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4910,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4903},{"end_us":4918,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4911,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4962,"operation":"read","slot":0,"start_us":4919},{"end_us":4966,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4965,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4976,"operation":"read","slot":0,"start_us":4967},{"end_us":4978,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4977,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4985,"operation":"read","slot":0,"start_us":4979},{"end_us":4989,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4986,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":1,"end_us":4998,"operation":"read","slot":0,"start_us":4989},{"end_us":5003,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":4999}],"schema":2,"slots":[{"at_us":5003,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c100b9ac2e0-b8b0-1b","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":5365,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5559,"slot":0,"token":"18e15c100c2b4ae0-b8b0-1c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c100c2b4ae0-b8b0-1c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5573,"bytes":57,"kind":"head","slot":0},{"at_us":5579,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":43,"operation":"handoff","start_us":43},{"end_us":5283,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5293,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5287},{"end_us":5300,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5294,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5337,"operation":"read","slot":0,"start_us":5302},{"end_us":5346,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5340,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5358,"operation":"read","slot":0,"start_us":5348},{"end_us":5362,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5359}],"schema":2,"slots":[{"at_us":5362,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c100c2b4ae0-b8b0-1c","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5531,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5643,"slot":0,"token":"18e15c100cd342e0-b8b0-1d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c100cd342e0-b8b0-1d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5659,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5460,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":40,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5476,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5465},{"end_us":5483,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5477,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5521,"operation":"read","slot":0,"start_us":5484},{"end_us":5528,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5524}],"schema":2,"slots":[{"at_us":5528,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c100cd342e0-b8b0-1d","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5076,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5220,"slot":0,"token":"18e15c100d5e6410-b8b0-1e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c100d5e6410-b8b0-1e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5228,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5015,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5025,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5018},{"end_us":5029,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5026,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5067,"operation":"read","slot":0,"start_us":5030},{"end_us":5073,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5069}],"schema":2,"slots":[{"at_us":5073,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c100d5e6410-b8b0-1e","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":6100,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6238,"slot":0,"token":"18e15c100debac08-b8b0-1f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c100debac08-b8b0-1f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6249,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":43,"operation":"handoff","start_us":43},{"end_us":6040,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6049,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6043},{"end_us":6054,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6050,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6091,"operation":"read","slot":0,"start_us":6055},{"end_us":6097,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6093}],"schema":2,"slots":[{"at_us":6097,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c100debac08-b8b0-1f","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5406,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5517,"slot":0,"token":"18e15c100e757d48-b8b0-20"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c100e757d48-b8b0-20","watchdog":false,"workers_joined":1,"writes":[{"at_us":5526,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":36,"operation":"handoff","start_us":36},{"end_us":5357,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":37,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5367,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5362},{"end_us":5372,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5368,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5399,"operation":"read","slot":0,"start_us":5373},{"end_us":5403,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5401}],"schema":2,"slots":[{"at_us":5403,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c100e757d48-b8b0-20","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":6370,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6473,"slot":0,"token":"18e15c100ef98228-b8b0-21"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c100ef98228-b8b0-21","watchdog":false,"workers_joined":1,"writes":[{"at_us":6492,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":6355,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6365,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6359},{"end_us":6367,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6367}],"schema":2,"slots":[{"at_us":6367,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c100ef98228-b8b0-21","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5246,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5305,"slot":0,"token":"18e15c100f942030-b8b0-22"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c100f942030-b8b0-22","watchdog":false,"workers_joined":1,"writes":[{"at_us":5323,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5235,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5241,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5237},{"end_us":5243,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5242}],"schema":2,"slots":[{"at_us":5243,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c100f942030-b8b0-22","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":4482,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5231,"slot":0,"token":"18e15c10101d8410-b8b0-23"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c10101d8410-b8b0-23","watchdog":false,"workers_joined":1,"writes":[{"at_us":5239,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":42,"operation":"handoff","start_us":42},{"end_us":4457,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4471,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":4464},{"end_us":4475,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":4475}],"schema":2,"slots":[{"at_us":4475,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c10101d8410-b8b0-23","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5868,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6021,"slot":0,"token":"18e15c1010adb620-b8b0-24"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1010adb620-b8b0-24","watchdog":false,"workers_joined":1,"writes":[{"at_us":6027,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"end_us":5853,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5861,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5857},{"end_us":5863,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5863}],"schema":2,"slots":[{"at_us":5863,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c1010adb620-b8b0-24","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":58160,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5318,"slot":0,"token":"18e15c101138ead8-b8b0-25"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c101138ead8-b8b0-25","watchdog":false,"workers_joined":1,"writes":[{"at_us":5331,"bytes":66,"kind":"head","slot":0},{"at_us":5337,"bytes":6,"kind":"body","slot":0},{"at_us":58255,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":5074,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5082,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5076},{"end_us":5088,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5083,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5116,"operation":"read","slot":0,"start_us":5088},{"end_us":58011,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5122,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":58137,"error":"Other","operation":"read","slot":0,"start_us":58016},{"end_us":58155,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58144}],"schema":2,"slots":[{"at_us":58155,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c101138ead8-b8b0-25","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":56831,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5351,"slot":0,"token":"18e15c1015069890-b8b0-26"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1015069890-b8b0-26","watchdog":false,"workers_joined":1,"writes":[{"at_us":5366,"bytes":66,"kind":"head","slot":0},{"at_us":5373,"bytes":6,"kind":"body","slot":0},{"at_us":57344,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":4760,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4768,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4763},{"end_us":4781,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4769,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4823,"operation":"read","slot":0,"start_us":4781},{"end_us":56764,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4827,"status":"FastlyStatus::OK","timeout_ms":495},{"capacity":16384,"end_us":56814,"error":"Other","operation":"read","slot":0,"start_us":56772},{"end_us":56827,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56821}],"schema":2,"slots":[{"at_us":56827,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1015069890-b8b0-26","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":57115,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6450,"slot":0,"token":"18e15c1018b8bb80-b8b0-27"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1018b8bb80-b8b0-27","watchdog":false,"workers_joined":1,"writes":[{"at_us":6471,"bytes":66,"kind":"head","slot":0},{"at_us":6479,"bytes":6,"kind":"body","slot":0},{"at_us":57352,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":44,"operation":"handoff","start_us":44},{"end_us":6177,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6186,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6181},{"end_us":6194,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6187,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6240,"operation":"read","slot":0,"start_us":6195},{"end_us":57040,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6243,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":57096,"error":"Other","operation":"read","slot":0,"start_us":57045},{"end_us":57110,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57103}],"schema":2,"slots":[{"at_us":57110,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1018b8bb80-b8b0-27","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":56790,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6428,"slot":0,"token":"18e15c101c541668-b8b0-28"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c101c541668-b8b0-28","watchdog":false,"workers_joined":1,"writes":[{"at_us":6444,"bytes":66,"kind":"head","slot":0},{"at_us":6449,"bytes":6,"kind":"body","slot":0},{"at_us":56994,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":43,"operation":"handoff","start_us":43},{"end_us":6157,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6166,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6161},{"end_us":6173,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6168,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6213,"operation":"read","slot":0,"start_us":6174},{"end_us":56721,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6216,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":56775,"error":"Other","operation":"read","slot":0,"start_us":56726},{"end_us":56786,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56780}],"schema":2,"slots":[{"at_us":56786,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c101c541668-b8b0-28","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":6117,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6468,"slot":0,"token":"18e15c101ffbd918-b8b0-29"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c101ffbd918-b8b0-29","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":6101,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6111,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6105},{"end_us":6113,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6113}],"schema":2,"slots":[{"at_us":6113,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c101ffbd918-b8b0-29","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":6319,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6472,"slot":0,"token":"18e15c10207dcab8-b8b0-2a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c10207dcab8-b8b0-2a","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":6304,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":38,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6313,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6308},{"end_us":6315,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6315}],"schema":2,"slots":[{"at_us":6315,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c10207dcab8-b8b0-2a","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":6017,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6187,"slot":0,"token":"18e15c10210609a0-b8b0-2b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c10210609a0-b8b0-2b","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":43,"operation":"handoff","start_us":43},{"end_us":6002,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6011,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6006},{"end_us":6013,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6013}],"schema":2,"slots":[{"at_us":6013,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c10210609a0-b8b0-2b","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":6261,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6461,"slot":0,"token":"18e15c10218480a0-b8b0-2c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c10218480a0-b8b0-2c","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":68,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"end_us":6243,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6254,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6246},{"end_us":6257,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6257}],"schema":2,"slots":[{"at_us":6257,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c10218480a0-b8b0-2c","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":60246,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5385,"slot":0,"token":"18e15c102211d068-b8b0-2d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c102211d068-b8b0-2d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5396,"bytes":57,"kind":"head","slot":0},{"at_us":5403,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5189,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":37,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5204,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5195},{"bytes":1,"capacity":1,"end_us":5239,"operation":"read","slot":0,"start_us":5205},{"capacity":1,"end_us":60224,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5242},{"end_us":60242,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60236}],"schema":2,"slots":[{"at_us":60242,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c102211d068-b8b0-2d","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":58475,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5234,"slot":0,"token":"18e15c1025ccb970-b8b0-2e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1025ccb970-b8b0-2e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5250,"bytes":57,"kind":"head","slot":0},{"at_us":5255,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":5005,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5015,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5009},{"end_us":5022,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5016,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5065,"operation":"read","slot":0,"start_us":5022},{"end_us":58405,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5067,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":58456,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58410},{"end_us":58470,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58464}],"schema":2,"slots":[{"at_us":58470,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1025ccb970-b8b0-2e","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":59803,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5641,"slot":0,"token":"18e15c1029962938-b8b0-2f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1029962938-b8b0-2f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5655,"bytes":57,"kind":"head","slot":0},{"at_us":5662,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":59,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5447,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5457,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5450},{"end_us":5465,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5458,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5505,"operation":"read","slot":0,"start_us":5466},{"end_us":59736,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5508,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":59788,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59740},{"end_us":59800,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59794}],"schema":2,"slots":[{"at_us":59800,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1029962938-b8b0-2f","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":60053,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5553,"slot":0,"token":"18e15c102d54f658-b8b0-30"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c102d54f658-b8b0-30","watchdog":false,"workers_joined":1,"writes":[{"at_us":5568,"bytes":57,"kind":"head","slot":0},{"at_us":5573,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"end_us":5205,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5214,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5208},{"end_us":5222,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5215,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5263,"operation":"read","slot":0,"start_us":5223},{"end_us":59992,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5266,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":60037,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59996},{"end_us":60049,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60043}],"schema":2,"slots":[{"at_us":60049,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c102d54f658-b8b0-30","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":58536,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6429,"slot":0,"token":"18e15c10312b8580-b8b0-31"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c10312b8580-b8b0-31","watchdog":false,"workers_joined":1,"writes":[{"at_us":6443,"bytes":57,"kind":"head","slot":0},{"at_us":6449,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":43,"operation":"handoff","start_us":43},{"end_us":6163,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6171,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6166},{"end_us":6177,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6172,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6210,"operation":"read","slot":0,"start_us":6178},{"end_us":58464,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6213,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":58521,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58469},{"end_us":58532,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58526}],"schema":2,"slots":[{"at_us":58532,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c10312b8580-b8b0-31","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":500481,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5248,"slot":0,"token":"18e15c1034f36ac0-b8b0-32"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c1034f36ac0-b8b0-32","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":500473,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500478,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500478}],"schema":2,"slots":[{"at_us":500478,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c1034f36ac0-b8b0-32","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":501677,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6465,"slot":0,"token":"18e15c1052e70118-b8b0-33"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c1052e70118-b8b0-33","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":501668,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501673,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501673}],"schema":2,"slots":[{"at_us":501673,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c1052e70118-b8b0-33","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":501647,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6458,"slot":0,"token":"18e15c1070f24dc0-b8b0-34"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c1070f24dc0-b8b0-34","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"end_us":501583,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501628,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501628}],"schema":2,"slots":[{"at_us":501628,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c1070f24dc0-b8b0-34","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":500501,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6433,"slot":0,"token":"18e15c108f0431e8-b8b0-35"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c108f0431e8-b8b0-35","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":68,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"end_us":500492,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500498,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500497}],"schema":2,"slots":[{"at_us":500498,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c108f0431e8-b8b0-35","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":500853,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5542,"slot":0,"token":"18e15c10ad049d68-b8b0-36"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c10ad049d68-b8b0-36","watchdog":false,"workers_joined":1,"writes":[{"at_us":5565,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5155,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5180,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5162},{"end_us":500836,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5181,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500848,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500840}],"schema":2,"slots":[{"at_us":500848,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c10ad049d68-b8b0-36","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":500774,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5885,"slot":0,"token":"18e15c10cb077218-b8b0-37"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c10cb077218-b8b0-37","watchdog":false,"workers_joined":1,"writes":[{"at_us":5898,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5599,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":40,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5610,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5603},{"end_us":500758,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5611,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500769,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500762}],"schema":2,"slots":[{"at_us":500769,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c10cb077218-b8b0-37","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":500642,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6507,"slot":0,"token":"18e15c10e90783c0-b8b0-38"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c10e90783c0-b8b0-38","watchdog":false,"workers_joined":1,"writes":[{"at_us":6525,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"end_us":6264,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6278,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6272},{"end_us":500579,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6280,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500618,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500593}],"schema":2,"slots":[{"at_us":500618,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c10e90783c0-b8b0-38","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":500585,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5346,"slot":0,"token":"18e15c11070dc758-b8b0-39"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c11070dc758-b8b0-39","watchdog":false,"workers_joined":1,"writes":[{"at_us":5364,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":68,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"end_us":5035,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5045,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5038},{"end_us":500566,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5046,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500579,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500572}],"schema":2,"slots":[{"at_us":500579,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c11070dc758-b8b0-39","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":501347,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6283,"slot":0,"token":"18e15c11250fb978-b8b0-3a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c11250fb978-b8b0-3a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6293,"bytes":57,"kind":"head","slot":0},{"at_us":6299,"bytes":1,"kind":"body","slot":0},{"at_us":311345,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6053,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6061,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6056},{"end_us":6068,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6062,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6099,"operation":"read","slot":0,"start_us":6069},{"end_us":311101,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6105,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":311217,"operation":"read","slot":0,"start_us":311105},{"end_us":501327,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":311220,"status":"FastlyStatus::OK","timeout_ms":188},{"end_us":501340,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501332}],"schema":2,"slots":[{"at_us":501340,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c11250fb978-b8b0-3a","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":501142,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6151,"slot":0,"token":"18e15c11434569b0-b8b0-3b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c11434569b0-b8b0-3b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6166,"bytes":57,"kind":"head","slot":0},{"at_us":6173,"bytes":1,"kind":"body","slot":0},{"at_us":308109,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5898,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5907,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5901},{"end_us":5915,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5909,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5966,"operation":"read","slot":0,"start_us":5916},{"end_us":307801,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5969,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":307858,"operation":"read","slot":0,"start_us":307806},{"end_us":501121,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307861,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501134,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501127}],"schema":2,"slots":[{"at_us":501134,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c11434569b0-b8b0-3b","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":501883,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6478,"slot":0,"token":"18e15c116177ab00-b8b0-3c"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c116177ab00-b8b0-3c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6498,"bytes":57,"kind":"head","slot":0},{"at_us":6505,"bytes":1,"kind":"body","slot":0},{"at_us":311076,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":49,"operation":"handoff","start_us":49},{"end_us":6211,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6223,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6215},{"end_us":6229,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6224,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6285,"operation":"read","slot":0,"start_us":6230},{"end_us":310732,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6288,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":310792,"operation":"read","slot":0,"start_us":310737},{"end_us":501862,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310794,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":501875,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501867}],"schema":2,"slots":[{"at_us":501875,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c116177ab00-b8b0-3c","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":501010,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5544,"slot":0,"token":"18e15c117fb88698-b8b0-3d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c117fb88698-b8b0-3d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5564,"bytes":57,"kind":"head","slot":0},{"at_us":5572,"bytes":1,"kind":"body","slot":0},{"at_us":307683,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":49,"operation":"handoff","start_us":49},{"end_us":5258,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5268,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5262},{"end_us":5276,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5269,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5331,"operation":"read","slot":0,"start_us":5277},{"end_us":307357,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5334,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":307424,"operation":"read","slot":0,"start_us":307368},{"end_us":500991,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307427,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501003,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500996}],"schema":2,"slots":[{"at_us":501003,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c117fb88698-b8b0-3d","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":501414,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5349,"slot":0,"token":"18e15c119dcded30-b8b0-3e"},{"path":"/readiness/siblings/1","received_us":5353,"slot":1,"token":"18e15c119dcded30-b8b0-3e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c119dcded30-b8b0-3e","watchdog":false,"workers_joined":2,"writes":[{"at_us":5363,"bytes":57,"kind":"head","slot":1},{"at_us":5372,"bytes":57,"kind":"head","slot":0},{"at_us":11641,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":88,"operation":"dispatch","outcome":"pending","slot":1,"start_us":63},{"end_us":5076,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5085,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5080},{"end_us":5088,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5086,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5090,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5089},{"end_us":11340,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5090,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":11389,"operation":"read","slot":1,"start_us":11343},{"end_us":11396,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11394,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11408,"operation":"read","slot":1,"start_us":11397},{"end_us":11410,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11409,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11417,"operation":"read","slot":1,"start_us":11411},{"end_us":11421,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11418,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11431,"operation":"read","slot":1,"start_us":11424},{"end_us":11435,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11431},{"end_us":501396,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11438,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501411,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501402}],"schema":2,"slots":[{"at_us":501411,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11435,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c119dcded30-b8b0-3e","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":500921,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6091,"slot":1,"token":"18e15c11bbefa808-b8b0-3f"},{"path":"/readiness/siblings/0","received_us":6097,"slot":0,"token":"18e15c11bbefa808-b8b0-3f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c11bbefa808-b8b0-3f","watchdog":false,"workers_joined":2,"writes":[{"at_us":6105,"bytes":57,"kind":"head","slot":1},{"at_us":6112,"bytes":57,"kind":"head","slot":0},{"at_us":12403,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":67,"operation":"dispatch","outcome":"pending","slot":1,"start_us":52},{"end_us":5832,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5840,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5835},{"end_us":5848,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5842,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5850,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5849},{"end_us":12152,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5851,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":12218,"operation":"read","slot":1,"start_us":12157},{"end_us":12228,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12221,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":16384,"end_us":12240,"operation":"read","slot":1,"start_us":12230},{"end_us":12247,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12241},{"end_us":500878,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12250,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":500914,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500891}],"schema":2,"slots":[{"at_us":500914,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12247,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c11bbefa808-b8b0-3f","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":500940,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5342,"slot":0,"token":"18e15c11da0b1d68-b8b0-40"},{"path":"/readiness/siblings/1","received_us":5353,"slot":1,"token":"18e15c11da0b1d68-b8b0-40"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e15c11da0b1d68-b8b0-40","watchdog":false,"workers_joined":2,"writes":[{"at_us":5356,"bytes":57,"kind":"head","slot":0},{"at_us":5386,"bytes":57,"kind":"head","slot":1},{"at_us":5392,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":96,"operation":"dispatch","outcome":"pending","slot":1,"start_us":69},{"body_reads":0,"deadline_us":500000,"end_us":97,"operation":"handoff","start_us":97},{"end_us":5030,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5039,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5034},{"end_us":5090,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5041,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5096,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5093},{"end_us":5104,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5097,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5148,"operation":"read","slot":1,"start_us":5105},{"end_us":5154,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5151,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5165,"operation":"read","slot":1,"start_us":5155},{"end_us":5167,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5166,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5178,"operation":"read","slot":1,"start_us":5168},{"end_us":5182,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5179,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5189,"operation":"read","slot":1,"start_us":5182},{"end_us":5192,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5190},{"end_us":500921,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5195,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500937,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500929}],"schema":2,"slots":[{"at_us":500937,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5192,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c11da0b1d68-b8b0-40","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":502068,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6501,"slot":1,"token":"18e15c11f83538c8-b8b0-41"},{"path":"/readiness/siblings/0","received_us":6507,"slot":0,"token":"18e15c11f83538c8-b8b0-41"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e15c11f83538c8-b8b0-41","watchdog":false,"workers_joined":2,"writes":[{"at_us":6520,"bytes":57,"kind":"head","slot":1},{"at_us":6532,"bytes":57,"kind":"head","slot":0},{"at_us":12060,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":136,"operation":"dispatch","outcome":"pending","slot":1,"start_us":94},{"body_reads":0,"deadline_us":500000,"end_us":137,"operation":"handoff","start_us":137},{"end_us":6115,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":138,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6126,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6119},{"end_us":6148,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6127,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6152,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6150},{"end_us":11621,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6153,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":11677,"operation":"read","slot":1,"start_us":11624},{"end_us":11684,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11680,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11696,"operation":"read","slot":1,"start_us":11685},{"end_us":11701,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11696},{"end_us":502050,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11704,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":502065,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502057}],"schema":2,"slots":[{"at_us":502065,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11701,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c11f83538c8-b8b0-41","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":504450,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5664,"slot":0,"token":"18e15c121663c4e0-b8b0-42"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c121663c4e0-b8b0-42","watchdog":false,"workers_joined":1,"writes":[{"at_us":5682,"bytes":57,"kind":"head","slot":0},{"at_us":5688,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5385,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5395,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5390},{"end_us":5401,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5397,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5453,"operation":"read","slot":0,"start_us":5402},{"end_us":504419,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5456,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":504440,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":504427}],"schema":2,"slots":[{"at_us":504440,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c121663c4e0-b8b0-42","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":503154,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":19497,"slot":0,"token":"18e15c12359dfc68-b8b0-43"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c12359dfc68-b8b0-43","watchdog":false,"workers_joined":1,"writes":[{"at_us":19520,"bytes":57,"kind":"head","slot":0},{"at_us":19527,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":4969,"operation":"dispatch","outcome":"pending","slot":0,"start_us":35},{"end_us":15762,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":4978,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":15778,"operation":"head-poll","outcome":"ready","slot":0,"start_us":15769},{"end_us":15789,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":15780,"status":"FastlyStatus::OK","timeout_ms":484},{"bytes":1,"capacity":16384,"end_us":15871,"operation":"read","slot":0,"start_us":15790},{"end_us":503068,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":15876,"status":"FastlyStatus::OK","timeout_ms":484},{"end_us":503129,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":503090}],"schema":2,"slots":[{"at_us":503129,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c12359dfc68-b8b0-43","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":501725,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5961,"slot":0,"token":"18e15c1254273758-b8b0-44"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c1254273758-b8b0-44","watchdog":false,"workers_joined":1,"writes":[{"at_us":5984,"bytes":57,"kind":"head","slot":0},{"at_us":5990,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"end_us":5670,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5705,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5674},{"end_us":5720,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5708,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5781,"operation":"read","slot":0,"start_us":5722},{"end_us":501701,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5783,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501715,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501707}],"schema":2,"slots":[{"at_us":501715,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1254273758-b8b0-44","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":501789,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6201,"slot":0,"token":"18e15c1272ae7678-b8b0-45"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c1272ae7678-b8b0-45","watchdog":false,"workers_joined":1,"writes":[{"at_us":6218,"bytes":57,"kind":"head","slot":0},{"at_us":6224,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"end_us":5884,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5900,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5888},{"end_us":5907,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5901,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5960,"operation":"read","slot":0,"start_us":5908},{"end_us":501765,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5963,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501784,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501777}],"schema":2,"slots":[{"at_us":501784,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1272ae7678-b8b0-45","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":6305,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6464,"slot":0,"token":"18e15c1290df63f0-b8b0-46"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1290df63f0-b8b0-46","watchdog":false,"workers_joined":1,"writes":[{"at_us":6482,"bytes":57,"kind":"head","slot":0},{"at_us":6486,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":6198,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6207,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6202},{"end_us":6214,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6208,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6265,"operation":"read","slot":0,"start_us":6215},{"end_us":6269,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6268,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6279,"operation":"read","slot":0,"start_us":6270},{"end_us":6281,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6280,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6288,"operation":"read","slot":0,"start_us":6282},{"end_us":6291,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6289,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6298,"operation":"read","slot":0,"start_us":6292},{"end_us":6301,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6298}],"schema":2,"slots":[{"at_us":6301,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1290df63f0-b8b0-46","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":5838,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5880,"slot":0,"token":"18e15c12916b6f80-b8b0-47"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12916b6f80-b8b0-47","watchdog":false,"workers_joined":1,"writes":[{"at_us":5895,"bytes":57,"kind":"head","slot":0},{"at_us":5900,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":5738,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":42,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5751,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5742},{"end_us":5758,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5752,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5809,"operation":"read","slot":0,"start_us":5759},{"end_us":5818,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5814,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5830,"operation":"read","slot":0,"start_us":5819},{"end_us":5834,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5831}],"schema":2,"slots":[{"at_us":5834,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c12916b6f80-b8b0-47","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":5189,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5261,"slot":0,"token":"18e15c1291e8d510-b8b0-48"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1291e8d510-b8b0-48","watchdog":false,"workers_joined":1,"writes":[{"at_us":5277,"bytes":57,"kind":"head","slot":0},{"at_us":5282,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":48,"operation":"handoff","start_us":48},{"end_us":5063,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5075,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5069},{"end_us":5084,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5077,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5143,"operation":"read","slot":0,"start_us":5085},{"end_us":5148,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5146,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5159,"operation":"read","slot":0,"start_us":5149},{"end_us":5161,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5160,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5169,"operation":"read","slot":0,"start_us":5162},{"end_us":5172,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5170,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5179,"operation":"read","slot":0,"start_us":5173},{"end_us":5183,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5179}],"schema":2,"slots":[{"at_us":5183,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1291e8d510-b8b0-48","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":6275,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6439,"slot":0,"token":"18e15c12926494c0-b8b0-49"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12926494c0-b8b0-49","watchdog":false,"workers_joined":1,"writes":[{"at_us":6456,"bytes":57,"kind":"head","slot":0},{"at_us":6462,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":50,"operation":"handoff","start_us":50},{"end_us":6203,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6212,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6206},{"end_us":6218,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6213,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6254,"operation":"read","slot":0,"start_us":6219},{"end_us":6258,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6256,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6268,"operation":"read","slot":0,"start_us":6259},{"end_us":6273,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6268}],"schema":2,"slots":[{"at_us":6273,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c12926494c0-b8b0-49","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":6347,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6401,"slot":0,"token":"18e15c1292e91a88-b8b0-4a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1292e91a88-b8b0-4a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6419,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":6263,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6274,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6267},{"end_us":6282,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6275,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6335,"operation":"read","slot":0,"start_us":6283},{"end_us":6341,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6337}],"schema":2,"slots":[{"at_us":6341,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1292e91a88-b8b0-4a","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5819,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5966,"slot":0,"token":"18e15c1293696260-b8b0-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1293696260-b8b0-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5987,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5749,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5760,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5753},{"end_us":5765,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5761,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5810,"operation":"read","slot":0,"start_us":5766},{"end_us":5816,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5813}],"schema":2,"slots":[{"at_us":5816,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1293696260-b8b0-4b","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5988,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6098,"slot":0,"token":"18e15c1293ffca48-b8b0-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1293ffca48-b8b0-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6118,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"end_us":5919,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5929,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5923},{"end_us":5933,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5930,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5979,"operation":"read","slot":0,"start_us":5934},{"end_us":5984,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5981}],"schema":2,"slots":[{"at_us":5984,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1293ffca48-b8b0-4c","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5714,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5890,"slot":0,"token":"18e15c129499d3e0-b8b0-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c129499d3e0-b8b0-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5903,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":68,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"end_us":5631,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5642,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5635},{"end_us":5648,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5644,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5704,"operation":"read","slot":0,"start_us":5649},{"end_us":5710,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5707}],"schema":2,"slots":[{"at_us":5710,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c129499d3e0-b8b0-4d","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5075,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5278,"slot":0,"token":"18e15c12951bf848-b8b0-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12951bf848-b8b0-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5286,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5061,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5069,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5064},{"end_us":5071,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5071}],"schema":2,"slots":[{"at_us":5071,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c12951bf848-b8b0-4e","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5733,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5960,"slot":0,"token":"18e15c12959a30c8-b8b0-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12959a30c8-b8b0-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5976,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":59,"operation":"dispatch","outcome":"pending","slot":0,"start_us":31},{"end_us":5704,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5725,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5717},{"end_us":5728,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5728}],"schema":2,"slots":[{"at_us":5728,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c12959a30c8-b8b0-4f","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":6072,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6277,"slot":0,"token":"18e15c12961ec248-b8b0-50"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12961ec248-b8b0-50","watchdog":false,"workers_joined":1,"writes":[{"at_us":6285,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"end_us":6055,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6065,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6060},{"end_us":6067,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6067}],"schema":2,"slots":[{"at_us":6067,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c12961ec248-b8b0-50","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":6403,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6537,"slot":0,"token":"18e15c1296ae7f28-b8b0-51"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1296ae7f28-b8b0-51","watchdog":false,"workers_joined":1,"writes":[{"at_us":6587,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"end_us":6386,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6396,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6390},{"end_us":6398,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6398}],"schema":2,"slots":[{"at_us":6398,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c1296ae7f28-b8b0-51","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":55723,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5183,"slot":0,"token":"18e15c1297375280-b8b0-52"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1297375280-b8b0-52","watchdog":false,"workers_joined":1,"writes":[{"at_us":5194,"bytes":66,"kind":"head","slot":0},{"at_us":5200,"bytes":6,"kind":"body","slot":0},{"at_us":55819,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":4936,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4944,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4939},{"end_us":4950,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4945,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4997,"operation":"read","slot":0,"start_us":4951},{"end_us":55570,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5000,"status":"FastlyStatus::OK","timeout_ms":495},{"capacity":1,"end_us":55701,"error":"Other","operation":"read","slot":0,"start_us":55595},{"end_us":55720,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55711}],"schema":2,"slots":[{"at_us":55720,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1297375280-b8b0-52","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":58649,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5611,"slot":0,"token":"18e15c129ac87c08-b8b0-53"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c129ac87c08-b8b0-53","watchdog":false,"workers_joined":1,"writes":[{"at_us":5622,"bytes":66,"kind":"head","slot":0},{"at_us":5633,"bytes":6,"kind":"body","slot":0},{"at_us":58824,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5333,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5341,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5336},{"end_us":5349,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5343,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5398,"operation":"read","slot":0,"start_us":5349},{"end_us":58559,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5401,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":58632,"error":"Other","operation":"read","slot":0,"start_us":58565},{"end_us":58645,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58638}],"schema":2,"slots":[{"at_us":58645,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c129ac87c08-b8b0-53","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":58001,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5315,"slot":0,"token":"18e15c129e908088-b8b0-54"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c129e908088-b8b0-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":5333,"bytes":66,"kind":"head","slot":0},{"at_us":5338,"bytes":6,"kind":"body","slot":0},{"at_us":58231,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"end_us":4998,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5012,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5002},{"end_us":5021,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5013,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5067,"operation":"read","slot":0,"start_us":5022},{"end_us":57923,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5069,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":57979,"error":"Other","operation":"read","slot":0,"start_us":57927},{"end_us":57997,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57986}],"schema":2,"slots":[{"at_us":57997,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c129e908088-b8b0-54","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":56316,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6328,"slot":0,"token":"18e15c12a258a448-b8b0-55"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12a258a448-b8b0-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":6344,"bytes":66,"kind":"head","slot":0},{"at_us":6349,"bytes":6,"kind":"body","slot":0},{"at_us":56562,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"end_us":6004,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6014,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6008},{"end_us":6025,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6015,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6075,"operation":"read","slot":0,"start_us":6026},{"end_us":56232,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6078,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":56297,"error":"Other","operation":"read","slot":0,"start_us":56238},{"end_us":56312,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56304}],"schema":2,"slots":[{"at_us":56312,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c12a258a448-b8b0-55","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":6217,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6495,"slot":0,"token":"18e15c12a5fc1198-b8b0-56"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12a5fc1198-b8b0-56","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":6200,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6210,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6205},{"end_us":6213,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6212}],"schema":2,"slots":[{"at_us":6213,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c12a5fc1198-b8b0-56","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":6081,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6294,"slot":0,"token":"18e15c12a68e6e58-b8b0-57"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12a68e6e58-b8b0-57","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":6066,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":48,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6076,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6070},{"end_us":6078,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6077}],"schema":2,"slots":[{"at_us":6078,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c12a68e6e58-b8b0-57","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":5430,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5718,"slot":0,"token":"18e15c12a71570d8-b8b0-58"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12a71570d8-b8b0-58","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":54,"operation":"handoff","start_us":54},{"end_us":5410,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5420,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5414},{"end_us":5423,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5422}],"schema":2,"slots":[{"at_us":5423,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c12a71570d8-b8b0-58","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":6197,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6463,"slot":0,"token":"18e15c12a7a2b4e8-b8b0-59"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12a7a2b4e8-b8b0-59","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"end_us":6179,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6188,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6184},{"end_us":6190,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6190}],"schema":2,"slots":[{"at_us":6190,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c12a7a2b4e8-b8b0-59","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":57572,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5772,"slot":0,"token":"18e15c12a838af70-b8b0-5a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12a838af70-b8b0-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5790,"bytes":57,"kind":"head","slot":0},{"at_us":5799,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":5519,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5532,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5523},{"bytes":1,"capacity":1,"end_us":5581,"operation":"read","slot":0,"start_us":5533},{"capacity":1,"end_us":57550,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5583},{"end_us":57568,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57561}],"schema":2,"slots":[{"at_us":57568,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c12a838af70-b8b0-5a","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":55304,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5365,"slot":0,"token":"18e15c12abcf95b8-b8b0-5b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12abcf95b8-b8b0-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5385,"bytes":57,"kind":"head","slot":0},{"at_us":5409,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":5162,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":48,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5174,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5166},{"end_us":5188,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5175,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5269,"operation":"read","slot":0,"start_us":5189},{"end_us":55237,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5272,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":55288,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":55242},{"end_us":55301,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55295}],"schema":2,"slots":[{"at_us":55301,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c12abcf95b8-b8b0-5b","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":60615,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5847,"slot":0,"token":"18e15c12af620b48-b8b0-5c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12af620b48-b8b0-5c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5859,"bytes":57,"kind":"head","slot":0},{"at_us":5866,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":5516,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5524,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5519},{"end_us":5530,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5525,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5578,"operation":"read","slot":0,"start_us":5530},{"end_us":60535,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5581,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":60596,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":60541},{"end_us":60611,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60604}],"schema":2,"slots":[{"at_us":60611,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c12af620b48-b8b0-5c","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":59919,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5826,"slot":0,"token":"18e15c12b34bc898-b8b0-5d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12b34bc898-b8b0-5d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5843,"bytes":57,"kind":"head","slot":0},{"at_us":5849,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":46,"operation":"handoff","start_us":46},{"end_us":5510,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5525,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5517},{"end_us":5535,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5526,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5585,"operation":"read","slot":0,"start_us":5537},{"end_us":59569,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5588,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":59795,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59586},{"end_us":59911,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59893}],"schema":2,"slots":[{"at_us":59911,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c12b34bc898-b8b0-5d","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"truncated","completed_us":57974,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5286,"slot":0,"token":"18e15c12b7209e58-b8b0-5e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c12b7209e58-b8b0-5e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5305,"bytes":57,"kind":"head","slot":0},{"at_us":5316,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":72,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":75,"operation":"handoff","start_us":75},{"end_us":4976,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4989,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4982},{"end_us":5006,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4990,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5078,"operation":"read","slot":0,"start_us":5008},{"end_us":57898,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5081,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57957,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57905},{"end_us":57970,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57963}],"schema":2,"slots":[{"at_us":57970,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c12b7209e58-b8b0-5e","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":502174,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5617,"slot":0,"token":"18e15c12bac75f38-b8b0-5f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c12bac75f38-b8b0-5f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":499955,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502163,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499960,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502169,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502168}],"schema":2,"slots":[{"at_us":502169,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c12bac75f38-b8b0-5f","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":500711,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6462,"slot":0,"token":"18e15c12d8e8e748-b8b0-60"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c12d8e8e748-b8b0-60","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":98,"operation":"dispatch","outcome":"pending","slot":0,"start_us":36},{"end_us":500702,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500708,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500708}],"schema":2,"slots":[{"at_us":500708,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c12d8e8e748-b8b0-60","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":501157,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5242,"slot":0,"token":"18e15c12f6f36cb8-b8b0-61"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c12f6f36cb8-b8b0-61","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"end_us":501141,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501152,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501152}],"schema":2,"slots":[{"at_us":501152,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c12f6f36cb8-b8b0-61","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"silent-head","completed_us":501348,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5691,"slot":0,"token":"18e15c1314f91410-b8b0-62"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c1314f91410-b8b0-62","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":68,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"end_us":501338,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501344,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501344}],"schema":2,"slots":[{"at_us":501344,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c1314f91410-b8b0-62","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":500506,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6442,"slot":0,"token":"18e15c133301a968-b8b0-63"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c133301a968-b8b0-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":6466,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":6185,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6200,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6188},{"end_us":500487,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6202,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500500,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500493}],"schema":2,"slots":[{"at_us":500500,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c133301a968-b8b0-63","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":500594,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5524,"slot":0,"token":"18e15c1350fd5de0-b8b0-64"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c1350fd5de0-b8b0-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":5542,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":5231,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5241,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5235},{"end_us":500575,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5243,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500588,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500581}],"schema":2,"slots":[{"at_us":500588,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1350fd5de0-b8b0-64","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":500507,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5328,"slot":0,"token":"18e15c136f00bb48-b8b0-65"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c136f00bb48-b8b0-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":5348,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"end_us":5014,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5025,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5018},{"end_us":500443,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5028,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500478,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500456}],"schema":2,"slots":[{"at_us":500478,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c136f00bb48-b8b0-65","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"pending-body","completed_us":500546,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5527,"slot":0,"token":"18e15c138d025f48-b8b0-66"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c138d025f48-b8b0-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":5555,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":87,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":90,"operation":"handoff","start_us":90},{"end_us":5096,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5106,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5100},{"end_us":500517,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5107,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500530,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500523}],"schema":2,"slots":[{"at_us":500530,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c138d025f48-b8b0-66","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":500837,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6459,"slot":0,"token":"18e15c13ab032888-b8b0-67"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c13ab032888-b8b0-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":6477,"bytes":57,"kind":"head","slot":0},{"at_us":6484,"bytes":1,"kind":"body","slot":0},{"at_us":310912,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":68,"operation":"dispatch","outcome":"pending","slot":0,"start_us":44},{"end_us":6179,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6188,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6183},{"end_us":6192,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6189,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6237,"operation":"read","slot":0,"start_us":6193},{"end_us":310613,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6240,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":310734,"operation":"read","slot":0,"start_us":310618},{"end_us":500818,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310738,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":500830,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500823}],"schema":2,"slots":[{"at_us":500830,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c13ab032888-b8b0-67","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":501420,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6440,"slot":0,"token":"18e15c13c90e2af8-b8b0-68"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c13c90e2af8-b8b0-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":6455,"bytes":57,"kind":"head","slot":0},{"at_us":6461,"bytes":1,"kind":"body","slot":0},{"at_us":308026,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":6194,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6203,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6197},{"end_us":6209,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6204,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6255,"operation":"read","slot":0,"start_us":6210},{"end_us":308095,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6258,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":308224,"operation":"read","slot":0,"start_us":308103},{"end_us":501398,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308228,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501412,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501404}],"schema":2,"slots":[{"at_us":501412,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c13c90e2af8-b8b0-68","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":501795,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6022,"slot":0,"token":"18e15c13e71d05c8-b8b0-69"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c13e71d05c8-b8b0-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":6044,"bytes":57,"kind":"head","slot":0},{"at_us":6051,"bytes":1,"kind":"body","slot":0},{"at_us":309795,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":54,"operation":"handoff","start_us":54},{"end_us":5742,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5751,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5746},{"end_us":5756,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5752,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5804,"operation":"read","slot":0,"start_us":5757},{"end_us":309504,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5806,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309568,"operation":"read","slot":0,"start_us":309510},{"end_us":501776,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309571,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501789,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501781}],"schema":2,"slots":[{"at_us":501789,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c13e71d05c8-b8b0-69","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"trickle","completed_us":501273,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5805,"slot":0,"token":"18e15c14053a0d80-b8b0-6a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c14053a0d80-b8b0-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5827,"bytes":57,"kind":"head","slot":0},{"at_us":5836,"bytes":1,"kind":"body","slot":0},{"at_us":307790,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":54,"operation":"handoff","start_us":54},{"end_us":5528,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5540,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5532},{"end_us":5545,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5541,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5606,"operation":"read","slot":0,"start_us":5546},{"end_us":307425,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5609,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":307486,"operation":"read","slot":0,"start_us":307431},{"end_us":501251,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307489,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501266,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501256}],"schema":2,"slots":[{"at_us":501266,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c14053a0d80-b8b0-6a","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":500539,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6446,"slot":0,"token":"18e15c1423505e78-b8b0-6b"},{"path":"/readiness/siblings/1","received_us":6452,"slot":1,"token":"18e15c1423505e78-b8b0-6b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c1423505e78-b8b0-6b","watchdog":false,"workers_joined":2,"writes":[{"at_us":6456,"bytes":57,"kind":"head","slot":0},{"at_us":6464,"bytes":57,"kind":"head","slot":1},{"at_us":6469,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":66,"operation":"dispatch","outcome":"pending","slot":1,"start_us":51},{"end_us":6166,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6173,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6168},{"end_us":6176,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6174,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6179,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6176},{"end_us":6184,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6179,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6217,"operation":"read","slot":1,"start_us":6184},{"end_us":6221,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6220,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6230,"operation":"read","slot":1,"start_us":6222},{"end_us":6232,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6231,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6241,"operation":"read","slot":1,"start_us":6233},{"end_us":6246,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6242,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6254,"operation":"read","slot":1,"start_us":6248},{"end_us":6257,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6255},{"end_us":500411,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6260,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500481,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500453}],"schema":2,"slots":[{"at_us":500481,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6257,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1423505e78-b8b0-6b","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":501094,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5265,"slot":0,"token":"18e15c1441a32018-b8b0-6c"},{"path":"/readiness/siblings/1","received_us":5271,"slot":1,"token":"18e15c1441a32018-b8b0-6c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c1441a32018-b8b0-6c","watchdog":false,"workers_joined":2,"writes":[{"at_us":5284,"bytes":57,"kind":"head","slot":0},{"at_us":5292,"bytes":57,"kind":"head","slot":1},{"at_us":5299,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":112,"operation":"dispatch","outcome":"pending","slot":1,"start_us":57},{"end_us":4929,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":114,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4939,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4933},{"end_us":4942,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":4940,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":4944,"operation":"head-poll","outcome":"ready","slot":1,"start_us":4943},{"end_us":4948,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":4945,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":3,"capacity":16384,"end_us":5001,"operation":"read","slot":1,"start_us":4948},{"end_us":5007,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5004,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5019,"operation":"read","slot":1,"start_us":5008},{"end_us":5023,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5020},{"end_us":501039,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5026,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501070,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501050}],"schema":2,"slots":[{"at_us":501070,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5023,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1441a32018-b8b0-6c","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":501883,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5639,"slot":0,"token":"18e15c145fb8d0e8-b8b0-6d"},{"path":"/readiness/siblings/1","received_us":5649,"slot":1,"token":"18e15c145fb8d0e8-b8b0-6d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c145fb8d0e8-b8b0-6d","watchdog":false,"workers_joined":2,"writes":[{"at_us":5659,"bytes":57,"kind":"head","slot":0},{"at_us":5675,"bytes":57,"kind":"head","slot":1},{"at_us":5680,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"end_us":107,"operation":"dispatch","outcome":"pending","slot":1,"start_us":73},{"body_reads":0,"deadline_us":500000,"end_us":109,"operation":"handoff","start_us":109},{"end_us":5189,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5199,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5193},{"end_us":5222,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5203,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5225,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5223},{"end_us":5231,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5226,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5283,"operation":"read","slot":1,"start_us":5232},{"end_us":5287,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5285,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5297,"operation":"read","slot":1,"start_us":5288},{"end_us":5299,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5298,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5309,"operation":"read","slot":1,"start_us":5300},{"end_us":5313,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5310,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5319,"operation":"read","slot":1,"start_us":5313},{"end_us":5323,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5320},{"end_us":499763,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5326,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501868,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":499770,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501879,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501872}],"schema":2,"slots":[{"at_us":501879,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5323,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c145fb8d0e8-b8b0-6d","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"siblings","completed_us":500526,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6301,"slot":1,"token":"18e15c147e265a00-b8b0-6e"},{"path":"/readiness/siblings/0","received_us":6307,"slot":0,"token":"18e15c147e265a00-b8b0-6e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e15c147e265a00-b8b0-6e","watchdog":false,"workers_joined":2,"writes":[{"at_us":6321,"bytes":57,"kind":"head","slot":1},{"at_us":6330,"bytes":57,"kind":"head","slot":0},{"at_us":11435,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":73,"operation":"dispatch","outcome":"pending","slot":1,"start_us":57},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"end_us":5977,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5988,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5982},{"end_us":5992,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5990,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5994,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5993},{"end_us":11089,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5994,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11157,"operation":"read","slot":1,"start_us":11094},{"end_us":11166,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11160,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11179,"operation":"read","slot":1,"start_us":11167},{"end_us":11184,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11180},{"end_us":500505,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11187,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500523,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500511}],"schema":2,"slots":[{"at_us":500523,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11184,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c147e265a00-b8b0-6e","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":501642,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5911,"slot":0,"token":"18e15c149c5c8738-b8b0-6f"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c149c5c8738-b8b0-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5933,"bytes":57,"kind":"head","slot":0},{"at_us":5940,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5606,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5615,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5609},{"end_us":5621,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5616,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5675,"operation":"read","slot":0,"start_us":5622},{"end_us":501618,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5678,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501636,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501627}],"schema":2,"slots":[{"at_us":501636,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c149c5c8738-b8b0-6f","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":501955,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6084,"slot":0,"token":"18e15c14ba877d58-b8b0-70"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c14ba877d58-b8b0-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":6102,"bytes":57,"kind":"head","slot":0},{"at_us":6110,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"end_us":5767,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5780,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5771},{"end_us":5789,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5781,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5847,"operation":"read","slot":0,"start_us":5790},{"end_us":501930,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5850,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501948,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501940}],"schema":2,"slots":[{"at_us":501948,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c14ba877d58-b8b0-70","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":500506,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5246,"slot":0,"token":"18e15c14d8a5adf0-b8b0-71"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c14d8a5adf0-b8b0-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":5271,"bytes":57,"kind":"head","slot":0},{"at_us":5278,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":30},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"end_us":4951,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4964,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4957},{"end_us":4973,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4965,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5033,"operation":"read","slot":0,"start_us":4974},{"end_us":500452,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5036,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500492,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500466}],"schema":2,"slots":[{"at_us":500492,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c14d8a5adf0-b8b0-71","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"exhausted","completed_us":501309,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5568,"slot":0,"token":"18e15c14f6afe540-b8b0-72"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c14f6afe540-b8b0-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":5579,"bytes":57,"kind":"head","slot":0},{"at_us":5584,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"end_us":5214,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5222,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5217},{"end_us":5228,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5223,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5273,"operation":"read","slot":0,"start_us":5229},{"end_us":501289,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5275,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501303,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501296}],"schema":2,"slots":[{"at_us":501303,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c14f6afe540-b8b0-72","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":6330,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6444,"slot":0,"token":"18e15c1514c35008-b8b0-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1514c35008-b8b0-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":6461,"bytes":57,"kind":"head","slot":0},{"at_us":6466,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":113,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":6218,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":115,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6228,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6222},{"end_us":6235,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6229,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6286,"operation":"read","slot":0,"start_us":6238},{"end_us":6290,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6288,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6300,"operation":"read","slot":0,"start_us":6291},{"end_us":6304,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6303,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6312,"operation":"read","slot":0,"start_us":6305},{"end_us":6316,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6313,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6323,"operation":"read","slot":0,"start_us":6316},{"end_us":6327,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6323}],"schema":2,"slots":[{"at_us":6327,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1514c35008-b8b0-73","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":6375,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6429,"slot":0,"token":"18e15c151556e548-b8b0-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c151556e548-b8b0-74","watchdog":false,"workers_joined":1,"writes":[{"at_us":6446,"bytes":57,"kind":"head","slot":0},{"at_us":6454,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":6276,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6288,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6282},{"end_us":6295,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6289,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6349,"operation":"read","slot":0,"start_us":6296},{"end_us":6355,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6352,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6368,"operation":"read","slot":0,"start_us":6356},{"end_us":6372,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6369}],"schema":2,"slots":[{"at_us":6372,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c151556e548-b8b0-74","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":6199,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6288,"slot":0,"token":"18e15c1515eea4f0-b8b0-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1515eea4f0-b8b0-75","watchdog":false,"workers_joined":1,"writes":[{"at_us":6303,"bytes":57,"kind":"head","slot":0},{"at_us":6308,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"end_us":6083,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6097,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6091},{"end_us":6102,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6098,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6151,"operation":"read","slot":0,"start_us":6103},{"end_us":6155,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6154,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6167,"operation":"read","slot":0,"start_us":6156},{"end_us":6169,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6168,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6177,"operation":"read","slot":0,"start_us":6170},{"end_us":6180,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6178,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6187,"operation":"read","slot":0,"start_us":6181},{"end_us":6191,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6188}],"schema":2,"slots":[{"at_us":6191,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1515eea4f0-b8b0-75","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"eof","completed_us":6373,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6456,"slot":0,"token":"18e15c15169d8e48-b8b0-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c15169d8e48-b8b0-76","watchdog":false,"workers_joined":1,"writes":[{"at_us":6469,"bytes":57,"kind":"head","slot":0},{"at_us":6476,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"end_us":6281,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6290,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6285},{"end_us":6297,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6291,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6348,"operation":"read","slot":0,"start_us":6300},{"end_us":6353,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6350,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6366,"operation":"read","slot":0,"start_us":6354},{"end_us":6370,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6367}],"schema":2,"slots":[{"at_us":6370,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c15169d8e48-b8b0-76","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5239,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5314,"slot":0,"token":"18e15c1517478218-b8b0-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1517478218-b8b0-77","watchdog":false,"workers_joined":1,"writes":[{"at_us":5323,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5170,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5180,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5174},{"end_us":5185,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5181,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5229,"operation":"read","slot":0,"start_us":5186},{"end_us":5235,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5232}],"schema":2,"slots":[{"at_us":5235,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1517478218-b8b0-77","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5093,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5191,"slot":0,"token":"18e15c1517bd9c78-b8b0-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1517bd9c78-b8b0-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":5213,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":65,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5013,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5030,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5023},{"end_us":5038,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5032,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5081,"operation":"read","slot":0,"start_us":5039},{"end_us":5090,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5087}],"schema":2,"slots":[{"at_us":5090,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1517bd9c78-b8b0-78","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5184,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5303,"slot":0,"token":"18e15c1518315d48-b8b0-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1518315d48-b8b0-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":5324,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":45,"operation":"handoff","start_us":45},{"end_us":5121,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5130,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5125},{"end_us":5135,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5131,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5176,"operation":"read","slot":0,"start_us":5136},{"end_us":5181,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5178}],"schema":2,"slots":[{"at_us":5181,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1518315d48-b8b0-79","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5191,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5292,"slot":0,"token":"18e15c1518b8cd28-b8b0-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1518b8cd28-b8b0-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5311,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":49,"operation":"handoff","start_us":49},{"end_us":5117,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5126,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5120},{"end_us":5130,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5127,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5181,"operation":"read","slot":0,"start_us":5131},{"end_us":5187,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5183}],"schema":2,"slots":[{"at_us":5187,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1518b8cd28-b8b0-7a","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5653,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5805,"slot":0,"token":"18e15c1519483fd0-b8b0-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1519483fd0-b8b0-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5823,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":5637,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5646,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5641},{"end_us":5649,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5649}],"schema":2,"slots":[{"at_us":5649,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c1519483fd0-b8b0-7b","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":6017,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6153,"slot":0,"token":"18e15c1519c27cc8-b8b0-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1519c27cc8-b8b0-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6168,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5999,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":42,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6010,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6005},{"end_us":6012,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6012}],"schema":2,"slots":[{"at_us":6012,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c1519c27cc8-b8b0-7c","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5205,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5258,"slot":0,"token":"18e15c151a40ebf8-b8b0-7d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c151a40ebf8-b8b0-7d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5275,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":45,"operation":"handoff","start_us":45},{"end_us":5189,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5198,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5193},{"end_us":5201,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5200}],"schema":2,"slots":[{"at_us":5201,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c151a40ebf8-b8b0-7d","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5339,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5243,"slot":0,"token":"18e15c151aca01b8-b8b0-7e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c151aca01b8-b8b0-7e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5256,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"end_us":5318,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5332,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5322},{"end_us":5334,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5334}],"schema":2,"slots":[{"at_us":5334,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c151aca01b8-b8b0-7e","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":56985,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5641,"slot":0,"token":"18e15c151b5f7f40-b8b0-7f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c151b5f7f40-b8b0-7f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5652,"bytes":66,"kind":"head","slot":0},{"at_us":5658,"bytes":6,"kind":"body","slot":0},{"at_us":57258,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5338,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5350,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5341},{"end_us":5358,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5351,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5396,"operation":"read","slot":0,"start_us":5359},{"end_us":56903,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5399,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":56969,"error":"Other","operation":"read","slot":0,"start_us":56908},{"end_us":56982,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56975}],"schema":2,"slots":[{"at_us":56982,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c151b5f7f40-b8b0-7f","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":57250,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6109,"slot":0,"token":"18e15c151f14edf0-b8b0-80"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c151f14edf0-b8b0-80","watchdog":false,"workers_joined":1,"writes":[{"at_us":6117,"bytes":66,"kind":"head","slot":0},{"at_us":6122,"bytes":6,"kind":"body","slot":0},{"at_us":57370,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5896,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5906,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5900},{"end_us":5913,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5907,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5962,"operation":"read","slot":0,"start_us":5914},{"end_us":57140,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5965,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57228,"error":"Other","operation":"read","slot":0,"start_us":57145},{"end_us":57246,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57235}],"schema":2,"slots":[{"at_us":57246,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c151f14edf0-b8b0-80","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":56660,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6069,"slot":0,"token":"18e15c1522b09ec8-b8b0-81"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1522b09ec8-b8b0-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":6087,"bytes":66,"kind":"head","slot":0},{"at_us":6093,"bytes":6,"kind":"body","slot":0},{"at_us":56845,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":49,"operation":"handoff","start_us":49},{"end_us":5837,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5847,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5841},{"end_us":5854,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5849,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5908,"operation":"read","slot":0,"start_us":5855},{"end_us":56591,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5911,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":56645,"error":"Other","operation":"read","slot":0,"start_us":56595},{"end_us":56657,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56650}],"schema":2,"slots":[{"at_us":56657,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1522b09ec8-b8b0-81","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"malformed-body","completed_us":57137,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5538,"slot":0,"token":"18e15c1526418200-b8b0-82"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1526418200-b8b0-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":5556,"bytes":66,"kind":"head","slot":0},{"at_us":5562,"bytes":6,"kind":"body","slot":0},{"at_us":57382,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":65,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"end_us":5235,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5245,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5239},{"end_us":5253,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5246,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5313,"operation":"read","slot":0,"start_us":5254},{"end_us":57050,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5315,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57119,"error":"Other","operation":"read","slot":0,"start_us":57057},{"end_us":57133,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57126}],"schema":2,"slots":[{"at_us":57133,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c1526418200-b8b0-82","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":5211,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5455,"slot":0,"token":"18e15c1529e18450-b8b0-83"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c1529e18450-b8b0-83","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"end_us":5194,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5205,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5199},{"end_us":5207,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5207}],"schema":2,"slots":[{"at_us":5207,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c1529e18450-b8b0-83","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":5475,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5730,"slot":0,"token":"18e15c152a581bb0-b8b0-84"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c152a581bb0-b8b0-84","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"end_us":5461,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5468,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5465},{"end_us":5472,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5471}],"schema":2,"slots":[{"at_us":5472,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c152a581bb0-b8b0-84","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":5789,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6027,"slot":0,"token":"18e15c152ae2b810-b8b0-85"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c152ae2b810-b8b0-85","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":54,"operation":"handoff","start_us":54},{"end_us":5774,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5783,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5778},{"end_us":5785,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5784}],"schema":2,"slots":[{"at_us":5785,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c152ae2b810-b8b0-85","tolerance_us":5000}} +{"run":"repaired-before-trace-hardening","probe":{"case":"head-error","completed_us":6251,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6386,"slot":0,"token":"18e15c152b71b970-b8b0-86"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c152b71b970-b8b0-86","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":45,"operation":"handoff","start_us":45},{"end_us":6234,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6243,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6238},{"end_us":6247,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6247}],"schema":2,"slots":[{"at_us":6247,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c152b71b970-b8b0-86","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":60602,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":22982,"slot":0,"token":"18e15c23790514f8-ca5e-0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c23790514f8-ca5e-0","watchdog":false,"workers_joined":1,"writes":[{"at_us":23006,"bytes":57,"kind":"head","slot":0},{"at_us":23013,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":3292,"operation":"dispatch","outcome":"pending","slot":0,"start_us":914},{"end_us":10256,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":3315,"status":"FastlyStatus::OK","timeout_ms":496},{"end_us":10567,"operation":"head-poll","outcome":"ready","slot":0,"start_us":10272},{"bytes":1,"capacity":1,"end_us":11314,"operation":"read","slot":0,"start_us":10569},{"capacity":1,"end_us":60355,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":11318},{"end_us":60594,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60365}],"schema":2,"slots":[{"at_us":60594,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c23790514f8-ca5e-0","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":56499,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5891,"slot":0,"token":"18e15c237db5b840-ca5e-1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c237db5b840-ca5e-1","watchdog":false,"workers_joined":1,"writes":[{"at_us":5911,"bytes":57,"kind":"head","slot":0},{"at_us":5918,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5557,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":48,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5566,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5562},{"end_us":5575,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5568,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5621,"operation":"read","slot":0,"start_us":5576},{"end_us":56432,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5624,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":56487,"operation":"read","slot":0,"start_us":56437},{"end_us":56493,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56489}],"schema":2,"slots":[{"at_us":56493,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c237db5b840-ca5e-1","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":56689,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5745,"slot":0,"token":"18e15c2381443248-ca5e-2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2381443248-ca5e-2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5771,"bytes":57,"kind":"head","slot":0},{"at_us":5797,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5480,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5491,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5484},{"end_us":5498,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5493,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5565,"operation":"read","slot":0,"start_us":5499},{"end_us":56592,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5568,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":56676,"operation":"read","slot":0,"start_us":56597},{"end_us":56683,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56679}],"schema":2,"slots":[{"at_us":56683,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2381443248-ca5e-2","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":58424,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5423,"slot":0,"token":"18e15c2384d469a0-ca5e-3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2384d469a0-ca5e-3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5438,"bytes":57,"kind":"head","slot":0},{"at_us":5444,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":48,"operation":"handoff","start_us":48},{"end_us":5012,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5021,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5017},{"end_us":5028,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5023,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5084,"operation":"read","slot":0,"start_us":5029},{"end_us":58245,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5086,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58402,"operation":"read","slot":0,"start_us":58249},{"end_us":58411,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58405}],"schema":2,"slots":[{"at_us":58411,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2384d469a0-ca5e-3","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":60740,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6133,"slot":0,"token":"18e15c23887980b8-ca5e-4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c23887980b8-ca5e-4","watchdog":false,"workers_joined":1,"writes":[{"at_us":6151,"bytes":57,"kind":"head","slot":0},{"at_us":6157,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"end_us":5858,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5868,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5862},{"end_us":5880,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5869,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5935,"operation":"read","slot":0,"start_us":5882},{"end_us":60669,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5937,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":60729,"operation":"read","slot":0,"start_us":60676},{"end_us":60735,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60731}],"schema":2,"slots":[{"at_us":60735,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c23887980b8-ca5e-4","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":501251,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6476,"slot":0,"token":"18e15c238c41cf70-ca5e-5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c238c41cf70-ca5e-5","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":501242,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501247,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501247}],"schema":2,"slots":[{"at_us":501247,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c238c41cf70-ca5e-5","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":501847,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6239,"slot":0,"token":"18e15c23aa481ec0-ca5e-6"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c23aa481ec0-ca5e-6","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":501838,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501844,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501844}],"schema":2,"slots":[{"at_us":501844,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c23aa481ec0-ca5e-6","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":501555,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5277,"slot":0,"token":"18e15c23c8693588-ca5e-7"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c23c8693588-ca5e-7","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"end_us":499436,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501546,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499442,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501550,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501550}],"schema":2,"slots":[{"at_us":501550,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c23c8693588-ca5e-7","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":501507,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6508,"slot":0,"token":"18e15c23e6715998-ca5e-8"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c23e6715998-ca5e-8","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"end_us":501499,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501504,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501504}],"schema":2,"slots":[{"at_us":501504,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c23e6715998-ca5e-8","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":501445,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6467,"slot":0,"token":"18e15c24048bf438-ca5e-9"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c24048bf438-ca5e-9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6490,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":6278,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6288,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6283},{"end_us":501395,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6289,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501418,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501401}],"schema":2,"slots":[{"at_us":501418,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c24048bf438-ca5e-9","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":501372,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5205,"slot":0,"token":"18e15c2422ae2828-ca5e-a"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c2422ae2828-ca5e-a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5219,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":4891,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4898,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4893},{"end_us":501333,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4899,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501360,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501345}],"schema":2,"slots":[{"at_us":501360,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2422ae2828-ca5e-a","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":501986,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6472,"slot":0,"token":"18e15c2440bfd1b8-ca5e-b"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c2440bfd1b8-ca5e-b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6492,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":50,"operation":"handoff","start_us":50},{"end_us":6184,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6193,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6188},{"end_us":499636,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6194,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501960,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":499642,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501979,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501968}],"schema":2,"slots":[{"at_us":501979,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2440bfd1b8-ca5e-b","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":500711,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5478,"slot":0,"token":"18e15c245ecf3540-ca5e-c"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c245ecf3540-ca5e-c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5499,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":39,"operation":"handoff","start_us":39},{"end_us":5252,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5260,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5256},{"end_us":500684,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5262,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500705,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500694}],"schema":2,"slots":[{"at_us":500705,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c245ecf3540-ca5e-c","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":500512,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5537,"slot":0,"token":"18e15c247cdf2d38-ca5e-d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c247cdf2d38-ca5e-d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5554,"bytes":57,"kind":"head","slot":0},{"at_us":5561,"bytes":1,"kind":"body","slot":0},{"at_us":306601,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"end_us":5265,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5273,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5269},{"end_us":5280,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5275,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5308,"operation":"read","slot":0,"start_us":5281},{"end_us":306299,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5310,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":306351,"operation":"read","slot":0,"start_us":306307},{"end_us":500495,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306354,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":500506,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500500}],"schema":2,"slots":[{"at_us":500506,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c247cdf2d38-ca5e-d","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":500759,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5661,"slot":0,"token":"18e15c249ae66ad0-ca5e-e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c249ae66ad0-ca5e-e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5681,"bytes":57,"kind":"head","slot":0},{"at_us":5689,"bytes":1,"kind":"body","slot":0},{"at_us":308423,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":5474,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":36,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5482,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5477},{"end_us":5487,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5483,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5540,"operation":"read","slot":0,"start_us":5487},{"end_us":308225,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5543,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":308411,"operation":"read","slot":0,"start_us":308231},{"end_us":500739,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308416,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500753,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500744}],"schema":2,"slots":[{"at_us":500753,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c249ae66ad0-ca5e-e","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":502060,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6588,"slot":0,"token":"18e15c24b93d00e8-ca5e-f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c24b93d00e8-ca5e-f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6611,"bytes":57,"kind":"head","slot":0},{"at_us":6620,"bytes":1,"kind":"body","slot":0},{"at_us":314844,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":89,"operation":"dispatch","outcome":"pending","slot":0,"start_us":54},{"body_reads":0,"deadline_us":500000,"end_us":91,"operation":"handoff","start_us":91},{"end_us":6110,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6120,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6115},{"end_us":6129,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6122,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6182,"operation":"read","slot":0,"start_us":6130},{"end_us":314771,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6185,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":314939,"operation":"read","slot":0,"start_us":314791},{"end_us":502029,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":314968,"status":"FastlyStatus::OK","timeout_ms":185},{"end_us":502048,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502034}],"schema":2,"slots":[{"at_us":502048,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c24b93d00e8-ca5e-f","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":501910,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5228,"slot":0,"token":"18e15c24d75e4a78-ca5e-10"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c24d75e4a78-ca5e-10","watchdog":false,"workers_joined":1,"writes":[{"at_us":5249,"bytes":57,"kind":"head","slot":0},{"at_us":5256,"bytes":1,"kind":"body","slot":0},{"at_us":307061,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"end_us":4910,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4918,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4913},{"end_us":4925,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4919,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4970,"operation":"read","slot":0,"start_us":4926},{"end_us":306724,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4975,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":306786,"operation":"read","slot":0,"start_us":306730},{"end_us":501893,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306789,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":501905,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501898}],"schema":2,"slots":[{"at_us":501905,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c24d75e4a78-ca5e-10","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":502387,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6495,"slot":1,"token":"18e15c24f57a8af8-ca5e-11"},{"path":"/readiness/siblings/0","received_us":6502,"slot":0,"token":"18e15c24f57a8af8-ca5e-11"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c24f57a8af8-ca5e-11","watchdog":false,"workers_joined":2,"writes":[{"at_us":6514,"bytes":57,"kind":"head","slot":1},{"at_us":6522,"bytes":57,"kind":"head","slot":0},{"at_us":6536,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":72,"operation":"dispatch","outcome":"pending","slot":1,"start_us":46},{"end_us":6307,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6316,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6311},{"end_us":6324,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6317,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6325,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6325},{"end_us":6329,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6326,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6367,"operation":"read","slot":1,"start_us":6329},{"end_us":6371,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6370,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6385,"operation":"read","slot":1,"start_us":6372},{"end_us":6387,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6386,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6394,"operation":"read","slot":1,"start_us":6388},{"end_us":6397,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6395,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6408,"operation":"read","slot":1,"start_us":6400},{"end_us":6411,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6408},{"end_us":502370,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6414,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":502383,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502377}],"schema":2,"slots":[{"at_us":502383,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6411,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c24f57a8af8-ca5e-11","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":500483,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5799,"slot":1,"token":"18e15c2513b15088-ca5e-12"},{"path":"/readiness/siblings/0","received_us":5805,"slot":0,"token":"18e15c2513b15088-ca5e-12"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e15c2513b15088-ca5e-12","watchdog":false,"workers_joined":2,"writes":[{"at_us":5813,"bytes":57,"kind":"head","slot":1},{"at_us":5821,"bytes":57,"kind":"head","slot":0},{"at_us":11336,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":80,"operation":"dispatch","outcome":"pending","slot":1,"start_us":56},{"end_us":5551,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5559,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5554},{"end_us":5566,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5560,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5569,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5568},{"end_us":11060,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5570,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11108,"operation":"read","slot":1,"start_us":11061},{"end_us":11115,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11111,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11126,"operation":"read","slot":1,"start_us":11116},{"end_us":11129,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11126},{"end_us":500380,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11133,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500464,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500406}],"schema":2,"slots":[{"at_us":500464,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11129,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2513b15088-ca5e-12","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":501150,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6307,"slot":0,"token":"18e15c2531d5a758-ca5e-13"},{"path":"/readiness/siblings/1","received_us":6481,"slot":1,"token":"18e15c2531d5a758-ca5e-13"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c2531d5a758-ca5e-13","watchdog":false,"workers_joined":2,"writes":[{"at_us":6332,"bytes":57,"kind":"head","slot":0},{"at_us":6490,"bytes":57,"kind":"head","slot":1},{"at_us":6495,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":108,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":141,"operation":"dispatch","outcome":"pending","slot":1,"start_us":123},{"body_reads":0,"deadline_us":500000,"end_us":142,"operation":"handoff","start_us":142},{"end_us":6056,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":144,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6066,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6060},{"end_us":6118,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6067,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6120,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6119},{"end_us":6178,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6121,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6219,"operation":"read","slot":1,"start_us":6180},{"end_us":6223,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6222,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6232,"operation":"read","slot":1,"start_us":6224},{"end_us":6233,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6233,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6244,"operation":"read","slot":1,"start_us":6234},{"end_us":6249,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6244,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6256,"operation":"read","slot":1,"start_us":6250},{"end_us":6260,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6256},{"end_us":501132,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6263,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501147,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501139}],"schema":2,"slots":[{"at_us":501147,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6260,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2531d5a758-ca5e-13","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":501902,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6582,"slot":1,"token":"18e15c2550140e08-ca5e-14"},{"path":"/readiness/siblings/0","received_us":6589,"slot":0,"token":"18e15c2550140e08-ca5e-14"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e15c2550140e08-ca5e-14","watchdog":false,"workers_joined":2,"writes":[{"at_us":6604,"bytes":57,"kind":"head","slot":1},{"at_us":6613,"bytes":57,"kind":"head","slot":0},{"at_us":6622,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":111,"operation":"dispatch","outcome":"pending","slot":1,"start_us":83},{"body_reads":0,"deadline_us":500000,"end_us":113,"operation":"handoff","start_us":113},{"end_us":5776,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":114,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5785,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5779},{"end_us":5789,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5787,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5790,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5790},{"end_us":5794,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5791,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5835,"operation":"read","slot":1,"start_us":5794},{"end_us":5840,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5837,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5852,"operation":"read","slot":1,"start_us":5841},{"end_us":5856,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5853},{"end_us":501774,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5859,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501892,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501806}],"schema":2,"slots":[{"at_us":501892,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5856,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2550140e08-ca5e-14","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":501908,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":7992,"slot":0,"token":"18e15c256e751fb8-ca5e-15"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c256e751fb8-ca5e-15","watchdog":false,"workers_joined":1,"writes":[{"at_us":8013,"bytes":57,"kind":"head","slot":0},{"at_us":8022,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":104,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":7459,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":131,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7473,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7465},{"end_us":7482,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7475,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":8058,"operation":"read","slot":0,"start_us":7484},{"end_us":501880,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":8064,"status":"FastlyStatus::OK","timeout_ms":491},{"end_us":501901,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501884}],"schema":2,"slots":[{"at_us":501901,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c256e751fb8-ca5e-15","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":503470,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5612,"slot":0,"token":"18e15c258ca86aa8-ca5e-16"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c258ca86aa8-ca5e-16","watchdog":false,"workers_joined":1,"writes":[{"at_us":5628,"bytes":57,"kind":"head","slot":0},{"at_us":5635,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5178,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5188,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5183},{"end_us":5197,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5189,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5248,"operation":"read","slot":0,"start_us":5198},{"end_us":503318,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5252,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":503395,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":503367}],"schema":2,"slots":[{"at_us":503395,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c258ca86aa8-ca5e-16","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":501762,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":10667,"slot":0,"token":"18e15c25aba7e708-ca5e-17"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c25aba7e708-ca5e-17","watchdog":false,"workers_joined":1,"writes":[{"at_us":10741,"bytes":57,"kind":"head","slot":0},{"at_us":10792,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":228,"operation":"dispatch","outcome":"pending","slot":0,"start_us":128},{"body_reads":0,"deadline_us":500000,"end_us":232,"operation":"handoff","start_us":232},{"end_us":2482,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":233,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":2632,"operation":"head-poll","outcome":"ready","slot":0,"start_us":2562},{"end_us":3026,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":2652,"status":"FastlyStatus::OK","timeout_ms":497},{"bytes":1,"capacity":1,"end_us":3073,"operation":"read","slot":0,"start_us":3028},{"end_us":501716,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":3076,"status":"FastlyStatus::OK","timeout_ms":496},{"end_us":501755,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501723}],"schema":2,"slots":[{"at_us":501755,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25aba7e708-ca5e-17","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":501648,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6379,"slot":0,"token":"18e15c25ca877788-ca5e-18"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c25ca877788-ca5e-18","watchdog":false,"workers_joined":1,"writes":[{"at_us":6470,"bytes":57,"kind":"head","slot":0},{"at_us":6494,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":1696,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"body_reads":0,"deadline_us":500000,"end_us":1700,"operation":"handoff","start_us":1700},{"end_us":5514,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1702,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":5524,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5519},{"end_us":5529,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5525,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5566,"operation":"read","slot":0,"start_us":5530},{"end_us":501626,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5569,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501643,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501633}],"schema":2,"slots":[{"at_us":501643,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25ca877788-ca5e-18","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":5844,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5979,"slot":0,"token":"18e15c25e8c29278-ca5e-19"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25e8c29278-ca5e-19","watchdog":false,"workers_joined":1,"writes":[{"at_us":5991,"bytes":57,"kind":"head","slot":0},{"at_us":5997,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5731,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5740,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5735},{"end_us":5747,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5741,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5794,"operation":"read","slot":0,"start_us":5748},{"end_us":5798,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5797,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5817,"operation":"read","slot":0,"start_us":5799},{"end_us":5819,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5818,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5828,"operation":"read","slot":0,"start_us":5819},{"end_us":5831,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5829,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5837,"operation":"read","slot":0,"start_us":5832},{"end_us":5841,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5838}],"schema":2,"slots":[{"at_us":5841,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25e8c29278-ca5e-19","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":5908,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6453,"slot":0,"token":"18e15c25e95d0588-ca5e-1a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25e95d0588-ca5e-1a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6464,"bytes":57,"kind":"head","slot":0},{"at_us":6469,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":5821,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5831,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5825},{"end_us":5837,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5832,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5884,"operation":"read","slot":0,"start_us":5838},{"end_us":5890,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5887,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5900,"operation":"read","slot":0,"start_us":5891},{"end_us":5904,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5901}],"schema":2,"slots":[{"at_us":5904,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25e95d0588-ca5e-1a","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":5422,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5643,"slot":0,"token":"18e15c25ea042a98-ca5e-1b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25ea042a98-ca5e-1b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5659,"bytes":57,"kind":"head","slot":0},{"at_us":5669,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"end_us":5327,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5337,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5332},{"end_us":5343,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5338,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5384,"operation":"read","slot":0,"start_us":5344},{"end_us":5388,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5387,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5397,"operation":"read","slot":0,"start_us":5389},{"end_us":5399,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5398,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5405,"operation":"read","slot":0,"start_us":5399},{"end_us":5408,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5405,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5413,"operation":"read","slot":0,"start_us":5408},{"end_us":5416,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5413}],"schema":2,"slots":[{"at_us":5416,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25ea042a98-ca5e-1b","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":5338,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5508,"slot":0,"token":"18e15c25ea94cdf0-ca5e-1c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25ea94cdf0-ca5e-1c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5525,"bytes":57,"kind":"head","slot":0},{"at_us":5532,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":36,"operation":"handoff","start_us":36},{"end_us":5252,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":37,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5265,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5256},{"end_us":5275,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5266,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5315,"operation":"read","slot":0,"start_us":5276},{"end_us":5320,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5317,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5331,"operation":"read","slot":0,"start_us":5321},{"end_us":5336,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5332}],"schema":2,"slots":[{"at_us":5336,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25ea94cdf0-ca5e-1c","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":6347,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6540,"slot":0,"token":"18e15c25eb1ef138-ca5e-1d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25eb1ef138-ca5e-1d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6553,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"end_us":6299,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6306,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6302},{"end_us":6310,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6307,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6340,"operation":"read","slot":0,"start_us":6311},{"end_us":6345,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6342}],"schema":2,"slots":[{"at_us":6345,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25eb1ef138-ca5e-1d","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":6081,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6237,"slot":0,"token":"18e15c25ebba5a60-ca5e-1e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25ebba5a60-ca5e-1e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6258,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":6017,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":36,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6025,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6021},{"end_us":6030,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6027,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6072,"operation":"read","slot":0,"start_us":6031},{"end_us":6078,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6075}],"schema":2,"slots":[{"at_us":6078,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25ebba5a60-ca5e-1e","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5851,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5997,"slot":0,"token":"18e15c25ec3cc518-ca5e-1f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25ec3cc518-ca5e-1f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6015,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":36,"operation":"handoff","start_us":36},{"end_us":5803,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":37,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5810,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5806},{"end_us":5813,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5811,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5845,"operation":"read","slot":0,"start_us":5814},{"end_us":5849,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5847}],"schema":2,"slots":[{"at_us":5849,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25ec3cc518-ca5e-1f","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":6216,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6359,"slot":0,"token":"18e15c25ecbe0ec0-ca5e-20"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25ecbe0ec0-ca5e-20","watchdog":false,"workers_joined":1,"writes":[{"at_us":6374,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":42,"operation":"handoff","start_us":42},{"end_us":6164,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6171,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6167},{"end_us":6174,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6172,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6208,"operation":"read","slot":0,"start_us":6175},{"end_us":6213,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6210}],"schema":2,"slots":[{"at_us":6213,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25ecbe0ec0-ca5e-20","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5107,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5240,"slot":0,"token":"18e15c25ed438e88-ca5e-21"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25ed438e88-ca5e-21","watchdog":false,"workers_joined":1,"writes":[{"at_us":5254,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"end_us":5092,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5101,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5096},{"end_us":5103,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5103}],"schema":2,"slots":[{"at_us":5103,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c25ed438e88-ca5e-21","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":6152,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6845,"slot":0,"token":"18e15c25edb3ec28-ca5e-22"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25edb3ec28-ca5e-22","watchdog":false,"workers_joined":1,"writes":[{"at_us":6858,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":104,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6138,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6146,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6141},{"end_us":6148,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6148}],"schema":2,"slots":[{"at_us":6148,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c25edb3ec28-ca5e-22","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5023,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5204,"slot":0,"token":"18e15c25ee560c10-ca5e-23"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25ee560c10-ca5e-23","watchdog":false,"workers_joined":1,"writes":[{"at_us":5215,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":35,"operation":"handoff","start_us":35},{"end_us":5011,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":36,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5017,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5014},{"end_us":5019,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5019}],"schema":2,"slots":[{"at_us":5019,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c25ee560c10-ca5e-23","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5044,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5247,"slot":0,"token":"18e15c25eeddcdf8-ca5e-24"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25eeddcdf8-ca5e-24","watchdog":false,"workers_joined":1,"writes":[{"at_us":5261,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":34,"operation":"handoff","start_us":34},{"end_us":5030,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":35,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5038,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5034},{"end_us":5040,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5040}],"schema":2,"slots":[{"at_us":5040,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c25eeddcdf8-ca5e-24","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":55819,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5434,"slot":0,"token":"18e15c25ef64a580-ca5e-25"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25ef64a580-ca5e-25","watchdog":false,"workers_joined":1,"writes":[{"at_us":5491,"bytes":66,"kind":"head","slot":0},{"at_us":5497,"bytes":6,"kind":"body","slot":0},{"at_us":56094,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5270,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5278,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5273},{"end_us":5286,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5279,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5315,"operation":"read","slot":0,"start_us":5287},{"end_us":55764,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5317,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":55806,"operation":"read","slot":0,"start_us":55768},{"end_us":55813,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55808}],"schema":2,"slots":[{"at_us":55813,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25ef64a580-ca5e-25","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":60024,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5524,"slot":0,"token":"18e15c25f328e910-ca5e-26"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25f328e910-ca5e-26","watchdog":false,"workers_joined":1,"writes":[{"at_us":5548,"bytes":66,"kind":"head","slot":0},{"at_us":5556,"bytes":6,"kind":"body","slot":0},{"at_us":60359,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":179,"operation":"dispatch","outcome":"pending","slot":0,"start_us":30},{"end_us":5073,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":183,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5101,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5078},{"end_us":5144,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5103,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5202,"operation":"read","slot":0,"start_us":5148},{"end_us":59937,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5205,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":60000,"operation":"read","slot":0,"start_us":59947},{"end_us":60012,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60003}],"schema":2,"slots":[{"at_us":60012,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25f328e910-ca5e-26","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":59898,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5410,"slot":0,"token":"18e15c25f7091cf8-ca5e-27"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25f7091cf8-ca5e-27","watchdog":false,"workers_joined":1,"writes":[{"at_us":5424,"bytes":66,"kind":"head","slot":0},{"at_us":5430,"bytes":6,"kind":"body","slot":0},{"at_us":60171,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":45,"operation":"handoff","start_us":45},{"end_us":5098,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5108,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5103},{"end_us":5115,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5109,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5153,"operation":"read","slot":0,"start_us":5123},{"end_us":59839,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5156,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":59878,"operation":"read","slot":0,"start_us":59843},{"end_us":59889,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59879}],"schema":2,"slots":[{"at_us":59889,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25f7091cf8-ca5e-27","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":56794,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6525,"slot":0,"token":"18e15c25fb024f50-ca5e-28"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25fb024f50-ca5e-28","watchdog":false,"workers_joined":1,"writes":[{"at_us":6595,"bytes":66,"kind":"head","slot":0},{"at_us":6606,"bytes":6,"kind":"body","slot":0},{"at_us":58081,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"end_us":5229,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5238,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5233},{"end_us":5244,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5239,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5291,"operation":"read","slot":0,"start_us":5245},{"end_us":56733,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5294,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":56781,"operation":"read","slot":0,"start_us":56738},{"end_us":56788,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56783}],"schema":2,"slots":[{"at_us":56788,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c25fb024f50-ca5e-28","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":5611,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6740,"slot":0,"token":"18e15c25feb2b108-ca5e-29"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25feb2b108-ca5e-29","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5595,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5604,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5599},{"end_us":5607,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5607}],"schema":2,"slots":[{"at_us":5607,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c25feb2b108-ca5e-29","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":5694,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5836,"slot":0,"token":"18e15c25ff3834b8-ca5e-2a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25ff3834b8-ca5e-2a","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5679,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":39,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5688,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5683},{"end_us":5690,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5690}],"schema":2,"slots":[{"at_us":5690,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c25ff3834b8-ca5e-2a","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":5824,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6099,"slot":0,"token":"18e15c25ffb3a648-ca5e-2b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c25ffb3a648-ca5e-2b","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"end_us":5806,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5817,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5811},{"end_us":5819,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5819}],"schema":2,"slots":[{"at_us":5819,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c25ffb3a648-ca5e-2b","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":4837,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5292,"slot":0,"token":"18e15c2600309a90-ca5e-2c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2600309a90-ca5e-2c","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":239,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":244,"operation":"handoff","start_us":244},{"end_us":4821,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":246,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4831,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":4825},{"end_us":4833,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":4833}],"schema":2,"slots":[{"at_us":4833,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2600309a90-ca5e-2c","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":59627,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6568,"slot":0,"token":"18e15c2600c934f8-ca5e-2d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2600c934f8-ca5e-2d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6588,"bytes":57,"kind":"head","slot":0},{"at_us":6595,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6115,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6124,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6119},{"bytes":1,"capacity":1,"end_us":6159,"operation":"read","slot":0,"start_us":6125},{"capacity":1,"end_us":59598,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":6162},{"end_us":59621,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59612}],"schema":2,"slots":[{"at_us":59621,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2600c934f8-ca5e-2d","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":59898,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":7844,"slot":0,"token":"18e15c26049dd020-ca5e-2e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c26049dd020-ca5e-2e","watchdog":false,"workers_joined":1,"writes":[{"at_us":7866,"bytes":57,"kind":"head","slot":0},{"at_us":7874,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5302,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5315,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5308},{"end_us":5330,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5317,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5404,"operation":"read","slot":0,"start_us":5332},{"end_us":59847,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5409,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":59886,"operation":"read","slot":0,"start_us":59851},{"end_us":59892,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59888}],"schema":2,"slots":[{"at_us":59892,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c26049dd020-ca5e-2e","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":60324,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6585,"slot":0,"token":"18e15c260897d950-ca5e-2f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c260897d950-ca5e-2f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6611,"bytes":57,"kind":"head","slot":0},{"at_us":6618,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6332,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6342,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6337},{"end_us":6347,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6343,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6381,"operation":"read","slot":0,"start_us":6348},{"end_us":60268,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6389,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":60314,"operation":"read","slot":0,"start_us":60273},{"end_us":60319,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60316}],"schema":2,"slots":[{"at_us":60319,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c260897d950-ca5e-2f","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":56962,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5419,"slot":0,"token":"18e15c260c7cc058-ca5e-30"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c260c7cc058-ca5e-30","watchdog":false,"workers_joined":1,"writes":[{"at_us":5440,"bytes":57,"kind":"head","slot":0},{"at_us":5446,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"end_us":5031,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":42,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5041,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5036},{"end_us":5046,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5042,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5092,"operation":"read","slot":0,"start_us":5047},{"end_us":56901,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5095,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":56950,"operation":"read","slot":0,"start_us":56906},{"end_us":56956,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56952}],"schema":2,"slots":[{"at_us":56956,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c260c7cc058-ca5e-30","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":60046,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5572,"slot":0,"token":"18e15c26102cfb00-ca5e-31"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c26102cfb00-ca5e-31","watchdog":false,"workers_joined":1,"writes":[{"at_us":5594,"bytes":57,"kind":"head","slot":0},{"at_us":5601,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":46,"operation":"handoff","start_us":46},{"end_us":5353,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5363,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5358},{"end_us":5371,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5364,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5418,"operation":"read","slot":0,"start_us":5372},{"end_us":59981,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5421,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":60033,"operation":"read","slot":0,"start_us":59987},{"end_us":60040,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60035}],"schema":2,"slots":[{"at_us":60040,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c26102cfb00-ca5e-31","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":501154,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5346,"slot":0,"token":"18e15c26140a0a38-ca5e-32"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c26140a0a38-ca5e-32","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":501145,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501150,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501150}],"schema":2,"slots":[{"at_us":501150,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c26140a0a38-ca5e-32","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":500458,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6512,"slot":0,"token":"18e15c263209d1a8-ca5e-33"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c263209d1a8-ca5e-33","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":500450,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500455,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500455}],"schema":2,"slots":[{"at_us":500455,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c263209d1a8-ca5e-33","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":502125,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5326,"slot":0,"token":"18e15c26500c7b60-ca5e-34"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c26500c7b60-ca5e-34","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"end_us":501786,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":42,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502116,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502116}],"schema":2,"slots":[{"at_us":502116,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c26500c7b60-ca5e-34","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":500028,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6446,"slot":0,"token":"18e15c266e511400-ca5e-35"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c266e511400-ca5e-35","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":100,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":106,"operation":"handoff","start_us":106},{"end_us":500020,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500025,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500025}],"schema":2,"slots":[{"at_us":500025,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c266e511400-ca5e-35","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":500356,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5274,"slot":0,"token":"18e15c268c654600-ca5e-36"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c268c654600-ca5e-36","watchdog":false,"workers_joined":1,"writes":[{"at_us":5292,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":5030,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5038,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5034},{"end_us":500339,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5039,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500351,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500344}],"schema":2,"slots":[{"at_us":500351,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c268c654600-ca5e-36","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":515212,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6465,"slot":0,"token":"18e15c26aa704488-ca5e-37"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c26aa704488-ca5e-37","watchdog":false,"workers_joined":1,"writes":[{"at_us":6486,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":6170,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6178,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6173},{"end_us":515190,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6179,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":515206,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":515195}],"schema":2,"slots":[{"at_us":515206,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c26aa704488-ca5e-37","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":501316,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":46234,"slot":0,"token":"18e15c26cb32dd48-ca5e-38"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c26cb32dd48-ca5e-38","watchdog":false,"workers_joined":1,"writes":[{"at_us":48904,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":14544,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8677},{"body_reads":0,"deadline_us":500000,"end_us":14577,"operation":"handoff","start_us":14577},{"end_us":33562,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":14579,"status":"FastlyStatus::OK","timeout_ms":485},{"end_us":33578,"operation":"head-poll","outcome":"ready","slot":0,"start_us":33569},{"end_us":501267,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":33579,"status":"FastlyStatus::OK","timeout_ms":466},{"end_us":501307,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501272}],"schema":2,"slots":[{"at_us":501307,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c26cb32dd48-ca5e-38","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":500995,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5431,"slot":0,"token":"18e15c26ea614d30-ca5e-39"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c26ea614d30-ca5e-39","watchdog":false,"workers_joined":1,"writes":[{"at_us":5450,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"end_us":5085,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5095,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5089},{"end_us":500969,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5096,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500989,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500983}],"schema":2,"slots":[{"at_us":500989,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c26ea614d30-ca5e-39","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":500384,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5819,"slot":0,"token":"18e15c270867c778-ca5e-3a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c270867c778-ca5e-3a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5838,"bytes":57,"kind":"head","slot":0},{"at_us":5844,"bytes":1,"kind":"body","slot":0},{"at_us":309387,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5609,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5618,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5613},{"end_us":5625,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5619,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5669,"operation":"read","slot":0,"start_us":5626},{"end_us":309147,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5671,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309283,"operation":"read","slot":0,"start_us":309152},{"end_us":500367,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309286,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500378,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500372}],"schema":2,"slots":[{"at_us":500378,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c270867c778-ca5e-3a","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":500276,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6447,"slot":0,"token":"18e15c27266ade90-ca5e-3b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c27266ade90-ca5e-3b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6465,"bytes":57,"kind":"head","slot":0},{"at_us":6471,"bytes":1,"kind":"body","slot":0},{"at_us":309342,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":6163,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6172,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6167},{"end_us":6179,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6173,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6225,"operation":"read","slot":0,"start_us":6180},{"end_us":309027,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6228,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309069,"operation":"read","slot":0,"start_us":309037},{"end_us":500214,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309072,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500258,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500230}],"schema":2,"slots":[{"at_us":500258,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c27266ade90-ca5e-3b","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":501433,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6511,"slot":0,"token":"18e15c27447b1cd8-ca5e-3c"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c27447b1cd8-ca5e-3c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6528,"bytes":57,"kind":"head","slot":0},{"at_us":6535,"bytes":1,"kind":"body","slot":0},{"at_us":308473,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":273,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":276,"operation":"handoff","start_us":276},{"end_us":5720,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":278,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5728,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5723},{"end_us":5735,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5729,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5787,"operation":"read","slot":0,"start_us":5735},{"end_us":307636,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5789,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307701,"operation":"read","slot":0,"start_us":307641},{"end_us":501413,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307704,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501426,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501418}],"schema":2,"slots":[{"at_us":501426,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c27447b1cd8-ca5e-3c","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":505014,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6677,"slot":0,"token":"18e15c2762e379b8-ca5e-3d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c2762e379b8-ca5e-3d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6697,"bytes":57,"kind":"head","slot":0},{"at_us":6707,"bytes":1,"kind":"body","slot":0},{"at_us":307035,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":208,"operation":"dispatch","outcome":"pending","slot":0,"start_us":35},{"body_reads":0,"deadline_us":500000,"end_us":212,"operation":"handoff","start_us":212},{"end_us":5713,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":214,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5722,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5716},{"end_us":5732,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5723,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5785,"operation":"read","slot":0,"start_us":5733},{"end_us":306082,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5787,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":306184,"operation":"read","slot":0,"start_us":306094},{"end_us":504994,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306188,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":505008,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":505000}],"schema":2,"slots":[{"at_us":505008,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2762e379b8-ca5e-3d","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":501798,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6979,"slot":1,"token":"18e15c2782110710-ca5e-3e"},{"path":"/readiness/siblings/0","received_us":7083,"slot":0,"token":"18e15c2782110710-ca5e-3e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c2782110710-ca5e-3e","watchdog":false,"workers_joined":2,"writes":[{"at_us":7094,"bytes":57,"kind":"head","slot":0},{"at_us":7108,"bytes":57,"kind":"head","slot":1},{"at_us":7118,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":84,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"end_us":123,"operation":"dispatch","outcome":"pending","slot":1,"start_us":96},{"end_us":6348,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":125,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6361,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6354},{"end_us":6369,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6363,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6372,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6371},{"end_us":6384,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6373,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6472,"operation":"read","slot":1,"start_us":6391},{"end_us":6478,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6476,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6511,"operation":"read","slot":1,"start_us":6480},{"end_us":6515,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6513,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6530,"operation":"read","slot":1,"start_us":6517},{"end_us":6535,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6531,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6554,"operation":"read","slot":1,"start_us":6541},{"end_us":6559,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6555},{"end_us":501784,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6565,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501795,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501789}],"schema":2,"slots":[{"at_us":501795,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6559,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2782110710-ca5e-3e","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":501262,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6509,"slot":0,"token":"18e15c27a0521d40-ca5e-3f"},{"path":"/readiness/siblings/1","received_us":6515,"slot":1,"token":"18e15c27a0521d40-ca5e-3f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c27a0521d40-ca5e-3f","watchdog":false,"workers_joined":2,"writes":[{"at_us":6527,"bytes":57,"kind":"head","slot":0},{"at_us":6536,"bytes":57,"kind":"head","slot":1},{"at_us":6546,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":86,"operation":"dispatch","outcome":"pending","slot":1,"start_us":64},{"end_us":6200,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6210,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6205},{"end_us":6213,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6211,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6215,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6215},{"end_us":6218,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6216,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6268,"operation":"read","slot":1,"start_us":6219},{"end_us":6274,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6271,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6285,"operation":"read","slot":1,"start_us":6275},{"end_us":6289,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6286},{"end_us":501190,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6292,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501254,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501205}],"schema":2,"slots":[{"at_us":501254,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6289,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c27a0521d40-ca5e-3f","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":500807,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5518,"slot":0,"token":"18e15c27be846278-ca5e-40"},{"path":"/readiness/siblings/1","received_us":5524,"slot":1,"token":"18e15c27be846278-ca5e-40"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e15c27be846278-ca5e-40","watchdog":false,"workers_joined":2,"writes":[{"at_us":5536,"bytes":57,"kind":"head","slot":0},{"at_us":5543,"bytes":57,"kind":"head","slot":1},{"at_us":5548,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"end_us":93,"operation":"dispatch","outcome":"pending","slot":1,"start_us":67},{"body_reads":0,"deadline_us":500000,"end_us":94,"operation":"handoff","start_us":94},{"end_us":5212,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5220,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5215},{"end_us":5224,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5222,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5226,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5225},{"end_us":5230,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5226,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5286,"operation":"read","slot":1,"start_us":5230},{"end_us":5290,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5288,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5303,"operation":"read","slot":1,"start_us":5291},{"end_us":5305,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5304,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5319,"operation":"read","slot":1,"start_us":5306},{"end_us":5322,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5319,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5331,"operation":"read","slot":1,"start_us":5322},{"end_us":5334,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5331},{"end_us":500725,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5339,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500803,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500795}],"schema":2,"slots":[{"at_us":500803,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5334,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c27be846278-ca5e-40","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":500287,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6438,"slot":0,"token":"18e15c27dcdadd38-ca5e-41"},{"path":"/readiness/siblings/1","received_us":6501,"slot":1,"token":"18e15c27dcdadd38-ca5e-41"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c27dcdadd38-ca5e-41","watchdog":false,"workers_joined":2,"writes":[{"at_us":6520,"bytes":57,"kind":"head","slot":0},{"at_us":6564,"bytes":57,"kind":"head","slot":1},{"at_us":6577,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":275,"operation":"dispatch","outcome":"pending","slot":1,"start_us":102},{"body_reads":0,"deadline_us":500000,"end_us":282,"operation":"handoff","start_us":282},{"end_us":6073,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":284,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6087,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6082},{"end_us":6116,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6088,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6120,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6118},{"end_us":6124,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6121,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6176,"operation":"read","slot":1,"start_us":6125},{"end_us":6182,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6179,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6194,"operation":"read","slot":1,"start_us":6183},{"end_us":6197,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6194},{"end_us":500272,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6200,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500284,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500277}],"schema":2,"slots":[{"at_us":500284,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6197,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c27dcdadd38-ca5e-41","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":500827,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5556,"slot":0,"token":"18e15c27fb116060-ca5e-42"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c27fb116060-ca5e-42","watchdog":false,"workers_joined":1,"writes":[{"at_us":5574,"bytes":57,"kind":"head","slot":0},{"at_us":5581,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":5292,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":42,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5300,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5295},{"end_us":5308,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5301,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5354,"operation":"read","slot":0,"start_us":5309},{"end_us":500810,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5357,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500822,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500815}],"schema":2,"slots":[{"at_us":500822,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c27fb116060-ca5e-42","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":501358,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5280,"slot":0,"token":"18e15c281927ad70-ca5e-43"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c281927ad70-ca5e-43","watchdog":false,"workers_joined":1,"writes":[{"at_us":5297,"bytes":57,"kind":"head","slot":0},{"at_us":5302,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5056,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":48,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5065,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5060},{"end_us":5070,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5066,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5113,"operation":"read","slot":0,"start_us":5071},{"end_us":501209,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5116,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501317,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501248}],"schema":2,"slots":[{"at_us":501317,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c281927ad70-ca5e-43","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":533355,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6487,"slot":0,"token":"18e15c2837806a50-ca5e-44"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c2837806a50-ca5e-44","watchdog":false,"workers_joined":1,"writes":[{"at_us":6497,"bytes":57,"kind":"head","slot":0},{"at_us":6503,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"end_us":6004,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6012,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6007},{"end_us":6016,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6013,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6093,"operation":"read","slot":0,"start_us":6017},{"end_us":529716,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6095,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":533344,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":529754}],"schema":2,"slots":[{"at_us":533344,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2837806a50-ca5e-44","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":507039,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":11697,"slot":0,"token":"18e15c285d179590-ca5e-45"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c285d179590-ca5e-45","watchdog":false,"workers_joined":1,"writes":[{"at_us":11713,"bytes":57,"kind":"head","slot":0},{"at_us":11719,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":1844,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":1849,"operation":"handoff","start_us":1849},{"end_us":9712,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1850,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":9721,"operation":"head-poll","outcome":"ready","slot":0,"start_us":9716},{"end_us":9731,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":9723,"status":"FastlyStatus::OK","timeout_ms":490},{"bytes":1,"capacity":16384,"end_us":9786,"operation":"read","slot":0,"start_us":9732},{"end_us":506010,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":9789,"status":"FastlyStatus::OK","timeout_ms":490},{"end_us":506921,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":506177}],"schema":2,"slots":[{"at_us":506921,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c285d179590-ca5e-45","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":2711,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5674,"slot":0,"token":"18e15c287cf48d28-ca5e-46"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c287cf48d28-ca5e-46","watchdog":false,"workers_joined":1,"writes":[{"at_us":5714,"bytes":57,"kind":"head","slot":0},{"at_us":5727,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":742,"operation":"dispatch","outcome":"pending","slot":0,"start_us":102},{"end_us":2571,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":745,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":2580,"operation":"head-poll","outcome":"ready","slot":0,"start_us":2575},{"end_us":2586,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":2581,"status":"FastlyStatus::OK","timeout_ms":497},{"bytes":1,"capacity":1,"end_us":2643,"operation":"read","slot":0,"start_us":2587},{"end_us":2647,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":2646,"status":"FastlyStatus::OK","timeout_ms":497},{"bytes":1,"capacity":1,"end_us":2684,"operation":"read","slot":0,"start_us":2648},{"end_us":2686,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":2685,"status":"FastlyStatus::OK","timeout_ms":497},{"bytes":1,"capacity":1,"end_us":2694,"operation":"read","slot":0,"start_us":2687},{"end_us":2698,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":2695,"status":"FastlyStatus::OK","timeout_ms":497},{"bytes":0,"capacity":1,"end_us":2704,"operation":"read","slot":0,"start_us":2699},{"end_us":2708,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":2705}],"schema":2,"slots":[{"at_us":2708,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c287cf48d28-ca5e-46","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":7147,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":7729,"slot":0,"token":"18e15c287e729dc0-ca5e-47"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c287e729dc0-ca5e-47","watchdog":false,"workers_joined":1,"writes":[{"at_us":7755,"bytes":57,"kind":"head","slot":0},{"at_us":7817,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":78,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"end_us":6836,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7016,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6840},{"end_us":7025,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7018,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":3,"capacity":16384,"end_us":7116,"operation":"read","slot":0,"start_us":7026},{"end_us":7123,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7119,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":7137,"operation":"read","slot":0,"start_us":7125},{"end_us":7142,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":7138}],"schema":2,"slots":[{"at_us":7142,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c287e729dc0-ca5e-47","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":5056,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5245,"slot":0,"token":"18e15c287f79e048-ca5e-48"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c287f79e048-ca5e-48","watchdog":false,"workers_joined":1,"writes":[{"at_us":5265,"bytes":57,"kind":"head","slot":0},{"at_us":5272,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":47,"operation":"handoff","start_us":47},{"end_us":4949,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4960,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4954},{"end_us":4965,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4961,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5011,"operation":"read","slot":0,"start_us":4966},{"end_us":5018,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5013,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5028,"operation":"read","slot":0,"start_us":5018},{"end_us":5029,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5029,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5036,"operation":"read","slot":0,"start_us":5030},{"end_us":5040,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5037,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5046,"operation":"read","slot":0,"start_us":5040},{"end_us":5050,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5046}],"schema":2,"slots":[{"at_us":5050,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c287f79e048-ca5e-48","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":6315,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6478,"slot":0,"token":"18e15c287ff7d278-ca5e-49"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c287ff7d278-ca5e-49","watchdog":false,"workers_joined":1,"writes":[{"at_us":6496,"bytes":57,"kind":"head","slot":0},{"at_us":6501,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":64,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"end_us":6217,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6226,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6221},{"end_us":6233,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6227,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6289,"operation":"read","slot":0,"start_us":6234},{"end_us":6294,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6291,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6306,"operation":"read","slot":0,"start_us":6295},{"end_us":6312,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6307}],"schema":2,"slots":[{"at_us":6312,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c287ff7d278-ca5e-49","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5695,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5834,"slot":0,"token":"18e15c288085d9d8-ca5e-4a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c288085d9d8-ca5e-4a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5853,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5624,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5634,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5628},{"end_us":5640,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5635,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5686,"operation":"read","slot":0,"start_us":5641},{"end_us":5691,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5688}],"schema":2,"slots":[{"at_us":5691,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c288085d9d8-ca5e-4a","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5484,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5726,"slot":0,"token":"18e15c28810162d8-ca5e-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c28810162d8-ca5e-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5743,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5418,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5427,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5422},{"end_us":5431,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5428,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5476,"operation":"read","slot":0,"start_us":5435},{"end_us":5481,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5478}],"schema":2,"slots":[{"at_us":5481,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c28810162d8-ca5e-4b","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":6377,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6482,"slot":0,"token":"18e15c28817e6e90-ca5e-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c28817e6e90-ca5e-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6500,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":54,"operation":"handoff","start_us":54},{"end_us":6306,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6313,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6309},{"end_us":6321,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6317,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6369,"operation":"read","slot":0,"start_us":6323},{"end_us":6374,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6371}],"schema":2,"slots":[{"at_us":6374,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c28817e6e90-ca5e-4c","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5764,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5845,"slot":0,"token":"18e15c2882022d20-ca5e-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2882022d20-ca5e-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5865,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":39,"operation":"handoff","start_us":39},{"end_us":5621,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5630,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5624},{"end_us":5634,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5631,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5755,"operation":"read","slot":0,"start_us":5635},{"end_us":5760,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5757}],"schema":2,"slots":[{"at_us":5760,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2882022d20-ca5e-4d","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":6268,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6520,"slot":0,"token":"18e15c28827f9698-ca5e-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c28827f9698-ca5e-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6539,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":6252,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6261,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6256},{"end_us":6263,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6263}],"schema":2,"slots":[{"at_us":6263,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c28827f9698-ca5e-4e","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5131,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5446,"slot":0,"token":"18e15c28830d4bf0-ca5e-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c28830d4bf0-ca5e-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5459,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":149,"operation":"dispatch","outcome":"pending","slot":0,"start_us":33},{"end_us":5115,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":152,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5124,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5119},{"end_us":5127,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5126}],"schema":2,"slots":[{"at_us":5127,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c28830d4bf0-ca5e-4f","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":5250,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5402,"slot":0,"token":"18e15c2883822218-ca5e-50"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2883822218-ca5e-50","watchdog":false,"workers_joined":1,"writes":[{"at_us":5422,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"end_us":5233,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5243,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5238},{"end_us":5245,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5245}],"schema":2,"slots":[{"at_us":5245,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2883822218-ca5e-50","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":6026,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6179,"slot":0,"token":"18e15c2883f5fe40-ca5e-51"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2883f5fe40-ca5e-51","watchdog":false,"workers_joined":1,"writes":[{"at_us":6193,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":64,"operation":"dispatch","outcome":"pending","slot":0,"start_us":30},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"end_us":6009,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6019,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6013},{"end_us":6022,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6022}],"schema":2,"slots":[{"at_us":6022,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2883f5fe40-ca5e-51","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":59439,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6481,"slot":0,"token":"18e15c28847bc070-ca5e-52"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c28847bc070-ca5e-52","watchdog":false,"workers_joined":1,"writes":[{"at_us":6508,"bytes":66,"kind":"head","slot":0},{"at_us":6515,"bytes":6,"kind":"body","slot":0},{"at_us":59609,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":6262,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6275,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6266},{"end_us":6284,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6277,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6331,"operation":"read","slot":0,"start_us":6285},{"end_us":59355,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6333,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":59425,"operation":"read","slot":0,"start_us":59360},{"end_us":59434,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59428}],"schema":2,"slots":[{"at_us":59434,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c28847bc070-ca5e-52","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":57196,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5622,"slot":0,"token":"18e15c28885224a0-ca5e-53"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c28885224a0-ca5e-53","watchdog":false,"workers_joined":1,"writes":[{"at_us":5641,"bytes":66,"kind":"head","slot":0},{"at_us":5647,"bytes":6,"kind":"body","slot":0},{"at_us":57409,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5344,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5351,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5347},{"end_us":5356,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5352,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5400,"operation":"read","slot":0,"start_us":5357},{"end_us":57119,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5403,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":57185,"operation":"read","slot":0,"start_us":57125},{"end_us":57191,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57187}],"schema":2,"slots":[{"at_us":57191,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c28885224a0-ca5e-53","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":58279,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5234,"slot":0,"token":"18e15c288c0754d0-ca5e-54"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c288c0754d0-ca5e-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":5248,"bytes":66,"kind":"head","slot":0},{"at_us":5255,"bytes":6,"kind":"body","slot":0},{"at_us":58498,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":44,"operation":"handoff","start_us":44},{"end_us":4962,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4970,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4965},{"end_us":4977,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4971,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5016,"operation":"read","slot":0,"start_us":4978},{"end_us":58217,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5018,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58268,"operation":"read","slot":0,"start_us":58222},{"end_us":58274,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58270}],"schema":2,"slots":[{"at_us":58274,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c288c0754d0-ca5e-54","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":59917,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5785,"slot":0,"token":"18e15c288faffa10-ca5e-55"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c288faffa10-ca5e-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":5794,"bytes":66,"kind":"head","slot":0},{"at_us":5799,"bytes":6,"kind":"body","slot":0},{"at_us":60039,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":43,"operation":"handoff","start_us":43},{"end_us":5598,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5606,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5601},{"end_us":5611,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5607,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5647,"operation":"read","slot":0,"start_us":5612},{"end_us":59849,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5649,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":59905,"operation":"read","slot":0,"start_us":59857},{"end_us":59912,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59907}],"schema":2,"slots":[{"at_us":59912,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c288faffa10-ca5e-55","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":6280,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6532,"slot":0,"token":"18e15c2893835cb8-ca5e-56"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2893835cb8-ca5e-56","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":6246,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6253,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6249},{"end_us":6256,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6255}],"schema":2,"slots":[{"at_us":6256,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2893835cb8-ca5e-56","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":5201,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5381,"slot":0,"token":"18e15c289411b620-ca5e-57"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c289411b620-ca5e-57","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":5189,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":38,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5196,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5192},{"end_us":5198,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5198}],"schema":2,"slots":[{"at_us":5198,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c289411b620-ca5e-57","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":6302,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6457,"slot":0,"token":"18e15c28948d71e8-ca5e-58"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c28948d71e8-ca5e-58","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":70,"operation":"dispatch","outcome":"pending","slot":0,"start_us":30},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"end_us":6276,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6294,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6280},{"end_us":6297,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6297}],"schema":2,"slots":[{"at_us":6297,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c28948d71e8-ca5e-58","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":5939,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6093,"slot":0,"token":"18e15c289522cc48-ca5e-59"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c289522cc48-ca5e-59","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":36,"operation":"handoff","start_us":36},{"end_us":5923,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":37,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5932,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5928},{"end_us":5934,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5934}],"schema":2,"slots":[{"at_us":5934,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c289522cc48-ca5e-59","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":58000,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5748,"slot":0,"token":"18e15c28959bc8f0-ca5e-5a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c28959bc8f0-ca5e-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5767,"bytes":57,"kind":"head","slot":0},{"at_us":5773,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":5570,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":40,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5578,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5573},{"bytes":1,"capacity":1,"end_us":5626,"operation":"read","slot":0,"start_us":5580},{"capacity":1,"end_us":57980,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5629},{"end_us":57996,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57989}],"schema":2,"slots":[{"at_us":57996,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c28959bc8f0-ca5e-5a","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":56961,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5714,"slot":0,"token":"18e15c28993ed498-ca5e-5b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c28993ed498-ca5e-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5729,"bytes":57,"kind":"head","slot":0},{"at_us":5735,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5438,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5447,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5443},{"end_us":5455,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5449,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5514,"operation":"read","slot":0,"start_us":5456},{"end_us":56899,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5517,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":56950,"operation":"read","slot":0,"start_us":56903},{"end_us":56956,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56952}],"schema":2,"slots":[{"at_us":56956,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c28993ed498-ca5e-5b","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":57865,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6185,"slot":0,"token":"18e15c289ce3e3e0-ca5e-5c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c289ce3e3e0-ca5e-5c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6220,"bytes":57,"kind":"head","slot":0},{"at_us":6233,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":7615,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7636,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7622},{"end_us":7700,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7661,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7801,"operation":"read","slot":0,"start_us":7703},{"end_us":57800,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7806,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":57854,"operation":"read","slot":0,"start_us":57806},{"end_us":57860,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57856}],"schema":2,"slots":[{"at_us":57860,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c289ce3e3e0-ca5e-5c","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":57293,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5293,"slot":0,"token":"18e15c28a09f9808-ca5e-5d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c28a09f9808-ca5e-5d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5301,"bytes":57,"kind":"head","slot":0},{"at_us":5307,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"end_us":5047,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5055,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5050},{"end_us":5064,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5056,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5123,"operation":"read","slot":0,"start_us":5065},{"end_us":57225,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5127,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":57282,"operation":"read","slot":0,"start_us":57235},{"end_us":57288,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57284}],"schema":2,"slots":[{"at_us":57288,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c28a09f9808-ca5e-5d","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"truncated","completed_us":61373,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6513,"slot":0,"token":"18e15c28a45312b8-ca5e-5e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c28a45312b8-ca5e-5e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6529,"bytes":57,"kind":"head","slot":0},{"at_us":6539,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"end_us":6248,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6257,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6252},{"end_us":6270,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6258,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6321,"operation":"read","slot":0,"start_us":6272},{"end_us":61304,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6324,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":61362,"operation":"read","slot":0,"start_us":61310},{"end_us":61367,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":61363}],"schema":2,"slots":[{"at_us":61367,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c28a45312b8-ca5e-5e","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":500736,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6506,"slot":0,"token":"18e15c28a8399bb8-ca5e-5f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c28a8399bb8-ca5e-5f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":500727,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500733,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500733}],"schema":2,"slots":[{"at_us":500733,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c28a8399bb8-ca5e-5f","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":501605,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5729,"slot":0,"token":"18e15c28c6382e90-ca5e-60"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c28c6382e90-ca5e-60","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":499505,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501593,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499516,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501596,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501596}],"schema":2,"slots":[{"at_us":501596,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c28c6382e90-ca5e-60","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":500400,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5403,"slot":0,"token":"18e15c28e455d670-ca5e-61"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c28e455d670-ca5e-61","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"end_us":500389,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500396,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500396}],"schema":2,"slots":[{"at_us":500396,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c28e455d670-ca5e-61","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"silent-head","completed_us":501517,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5244,"slot":0,"token":"18e15c29024ea8a0-ca5e-62"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c29024ea8a0-ca5e-62","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":48,"operation":"handoff","start_us":48},{"end_us":499415,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501508,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499421,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501512,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501512}],"schema":2,"slots":[{"at_us":501512,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c29024ea8a0-ca5e-62","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":500171,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5268,"slot":0,"token":"18e15c29206d7190-ca5e-63"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c29206d7190-ca5e-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":5292,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5053,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5064,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5058},{"end_us":500151,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5065,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500165,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500158}],"schema":2,"slots":[{"at_us":500165,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c29206d7190-ca5e-63","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":501233,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5211,"slot":0,"token":"18e15c293e793f20-ca5e-64"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c293e793f20-ca5e-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":5228,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":4953,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4960,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4956},{"end_us":501217,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4961,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501228,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501222}],"schema":2,"slots":[{"at_us":501228,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c293e793f20-ca5e-64","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":500650,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5328,"slot":0,"token":"18e15c295c828ff8-ca5e-65"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c295c828ff8-ca5e-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":5349,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":55,"operation":"handoff","start_us":55},{"end_us":5089,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5097,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5092},{"end_us":500588,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5098,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500636,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500603}],"schema":2,"slots":[{"at_us":500636,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c295c828ff8-ca5e-65","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"pending-body","completed_us":501958,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5243,"slot":0,"token":"18e15c297a8c9098-ca5e-66"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c297a8c9098-ca5e-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":5255,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":77,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"end_us":4925,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4934,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4928},{"end_us":501937,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4935,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501952,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501946}],"schema":2,"slots":[{"at_us":501952,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c297a8c9098-ca5e-66","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":501523,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5611,"slot":0,"token":"18e15c2998ac47d0-ca5e-67"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c2998ac47d0-ca5e-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":5628,"bytes":57,"kind":"head","slot":0},{"at_us":5635,"bytes":1,"kind":"body","slot":0},{"at_us":305798,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":5332,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5340,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5335},{"end_us":5347,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5341,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5390,"operation":"read","slot":0,"start_us":5347},{"end_us":305485,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5393,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":305547,"operation":"read","slot":0,"start_us":305490},{"end_us":501496,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":305550,"status":"FastlyStatus::OK","timeout_ms":194},{"end_us":501516,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501503}],"schema":2,"slots":[{"at_us":501516,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2998ac47d0-ca5e-67","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":500789,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6502,"slot":0,"token":"18e15c29b6cc2230-ca5e-68"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c29b6cc2230-ca5e-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":6512,"bytes":57,"kind":"head","slot":0},{"at_us":6520,"bytes":1,"kind":"body","slot":0},{"at_us":309263,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":6273,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":39,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6281,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6276},{"end_us":6288,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6282,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6344,"operation":"read","slot":0,"start_us":6289},{"end_us":309014,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6347,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309082,"operation":"read","slot":0,"start_us":309020},{"end_us":500773,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309085,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500784,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500778}],"schema":2,"slots":[{"at_us":500784,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c29b6cc2230-ca5e-68","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":502496,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6452,"slot":0,"token":"18e15c29d4d87490-ca5e-69"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c29d4d87490-ca5e-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":6471,"bytes":57,"kind":"head","slot":0},{"at_us":6479,"bytes":1,"kind":"body","slot":0},{"at_us":308167,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"end_us":6148,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6156,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6152},{"end_us":6161,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6157,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6211,"operation":"read","slot":0,"start_us":6162},{"end_us":307825,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6213,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":307895,"operation":"read","slot":0,"start_us":307831},{"end_us":502467,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307898,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":502488,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502475}],"schema":2,"slots":[{"at_us":502488,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c29d4d87490-ca5e-69","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"trickle","completed_us":500774,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":7500,"slot":0,"token":"18e15c29f34c3b50-ca5e-6a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c29f34c3b50-ca5e-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":7521,"bytes":57,"kind":"head","slot":0},{"at_us":7528,"bytes":1,"kind":"body","slot":0},{"at_us":310060,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":93,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":99,"operation":"handoff","start_us":99},{"end_us":5802,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":101,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5811,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5806},{"end_us":5817,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5812,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5871,"operation":"read","slot":0,"start_us":5818},{"end_us":308426,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5874,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":308507,"operation":"read","slot":0,"start_us":308437},{"end_us":500709,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308509,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500760,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500714}],"schema":2,"slots":[{"at_us":500760,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c29f34c3b50-ca5e-6a","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":501761,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6049,"slot":1,"token":"18e15c2a118ea558-ca5e-6b"},{"path":"/readiness/siblings/0","received_us":6056,"slot":0,"token":"18e15c2a118ea558-ca5e-6b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e15c2a118ea558-ca5e-6b","watchdog":false,"workers_joined":2,"writes":[{"at_us":6064,"bytes":57,"kind":"head","slot":1},{"at_us":6070,"bytes":57,"kind":"head","slot":0},{"at_us":11110,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":93,"operation":"dispatch","outcome":"pending","slot":1,"start_us":61},{"end_us":5746,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5756,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5751},{"end_us":5766,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5757,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5770,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5768},{"end_us":10801,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5771,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":10860,"operation":"read","slot":1,"start_us":10804},{"end_us":10865,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10863,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10882,"operation":"read","slot":1,"start_us":10867},{"end_us":10887,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10886,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10896,"operation":"read","slot":1,"start_us":10888},{"end_us":10899,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10896,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":1,"end_us":10908,"operation":"read","slot":1,"start_us":10902},{"end_us":10911,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10909},{"end_us":501720,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":10915,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":501754,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501728}],"schema":2,"slots":[{"at_us":501754,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":10911,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2a118ea558-ca5e-6b","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":501556,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5283,"slot":1,"token":"18e15c2a2fbcdb80-ca5e-6c"},{"path":"/readiness/siblings/0","received_us":5294,"slot":0,"token":"18e15c2a2fbcdb80-ca5e-6c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e15c2a2fbcdb80-ca5e-6c","watchdog":false,"workers_joined":2,"writes":[{"at_us":5298,"bytes":57,"kind":"head","slot":1},{"at_us":5312,"bytes":57,"kind":"head","slot":0},{"at_us":11575,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":62,"operation":"dispatch","outcome":"pending","slot":1,"start_us":47},{"end_us":5057,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5064,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5059},{"end_us":5066,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5065,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5068,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5067},{"end_us":11307,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5068,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11359,"operation":"read","slot":1,"start_us":11309},{"end_us":11366,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11362,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11377,"operation":"read","slot":1,"start_us":11367},{"end_us":11381,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11378},{"end_us":501540,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11384,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501553,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501546}],"schema":2,"slots":[{"at_us":501553,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11381,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2a2fbcdb80-ca5e-6c","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":500532,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6381,"slot":0,"token":"18e15c2a4dfca1c0-ca5e-6d"},{"path":"/readiness/siblings/1","received_us":6396,"slot":1,"token":"18e15c2a4dfca1c0-ca5e-6d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c2a4dfca1c0-ca5e-6d","watchdog":false,"workers_joined":2,"writes":[{"at_us":6406,"bytes":57,"kind":"head","slot":0},{"at_us":6412,"bytes":57,"kind":"head","slot":1},{"at_us":6436,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":91,"operation":"dispatch","outcome":"pending","slot":1,"start_us":56},{"body_reads":0,"deadline_us":500000,"end_us":92,"operation":"handoff","start_us":92},{"end_us":6149,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6159,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6153},{"end_us":6163,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6160,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6165,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6164},{"end_us":6168,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6165,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6216,"operation":"read","slot":1,"start_us":6169},{"end_us":6220,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6219,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6231,"operation":"read","slot":1,"start_us":6221},{"end_us":6232,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6231,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6243,"operation":"read","slot":1,"start_us":6233},{"end_us":6246,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6244,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6257,"operation":"read","slot":1,"start_us":6247},{"end_us":6260,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6257},{"end_us":500516,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6263,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500529,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500523}],"schema":2,"slots":[{"at_us":500529,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6260,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2a4dfca1c0-ca5e-6d","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"siblings","completed_us":501595,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6135,"slot":1,"token":"18e15c2a6c434da0-ca5e-6e"},{"path":"/readiness/siblings/0","received_us":6217,"slot":0,"token":"18e15c2a6c434da0-ca5e-6e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c2a6c434da0-ca5e-6e","watchdog":false,"workers_joined":2,"writes":[{"at_us":6186,"bytes":57,"kind":"head","slot":1},{"at_us":6239,"bytes":57,"kind":"head","slot":0},{"at_us":12052,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":105,"operation":"dispatch","outcome":"pending","slot":1,"start_us":83},{"body_reads":0,"deadline_us":500000,"end_us":106,"operation":"handoff","start_us":106},{"end_us":5863,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5873,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5868},{"end_us":6076,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5874,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":6082,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6079},{"end_us":11722,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6100,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":11789,"operation":"read","slot":1,"start_us":11726},{"end_us":11798,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11793,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11816,"operation":"read","slot":1,"start_us":11799},{"end_us":11821,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11817},{"end_us":501577,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11824,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501592,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501583}],"schema":2,"slots":[{"at_us":501592,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11821,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2a6c434da0-ca5e-6e","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":501140,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6276,"slot":0,"token":"18e15c2a8a8efac0-ca5e-6f"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c2a8a8efac0-ca5e-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6293,"bytes":57,"kind":"head","slot":0},{"at_us":6298,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5958,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5967,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5962},{"end_us":5972,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5968,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":6022,"operation":"read","slot":0,"start_us":5973},{"end_us":501105,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6024,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501117,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501110}],"schema":2,"slots":[{"at_us":501117,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2a8a8efac0-ca5e-6f","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":501116,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5683,"slot":0,"token":"18e15c2aa89a3bb0-ca5e-70"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c2aa89a3bb0-ca5e-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":5701,"bytes":57,"kind":"head","slot":0},{"at_us":5707,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":5471,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5480,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5475},{"end_us":5487,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5481,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5540,"operation":"read","slot":0,"start_us":5488},{"end_us":501098,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5543,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501111,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501104}],"schema":2,"slots":[{"at_us":501111,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2aa89a3bb0-ca5e-70","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":500425,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5222,"slot":0,"token":"18e15c2ac6c23048-ca5e-71"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c2ac6c23048-ca5e-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":5238,"bytes":57,"kind":"head","slot":0},{"at_us":5243,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":85,"operation":"dispatch","outcome":"pending","slot":0,"start_us":34},{"body_reads":0,"deadline_us":500000,"end_us":88,"operation":"handoff","start_us":88},{"end_us":4919,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4927,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4923},{"end_us":4933,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4928,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4979,"operation":"read","slot":0,"start_us":4934},{"end_us":500379,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4982,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":500411,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500391}],"schema":2,"slots":[{"at_us":500411,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2ac6c23048-ca5e-71","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"exhausted","completed_us":501865,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6103,"slot":0,"token":"18e15c2ae4c366e8-ca5e-72"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c2ae4c366e8-ca5e-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":6119,"bytes":57,"kind":"head","slot":0},{"at_us":6125,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":42,"operation":"handoff","start_us":42},{"end_us":5727,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5737,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5732},{"end_us":5744,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5739,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5777,"operation":"read","slot":0,"start_us":5745},{"end_us":501844,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5782,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501859,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501853}],"schema":2,"slots":[{"at_us":501859,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2ae4c366e8-ca5e-72","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":5506,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5684,"slot":0,"token":"18e15c2b02e57f80-ca5e-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b02e57f80-ca5e-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":5700,"bytes":57,"kind":"head","slot":0},{"at_us":5705,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5409,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5420,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5412},{"end_us":5426,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5421,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5461,"operation":"read","slot":0,"start_us":5427},{"end_us":5465,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5464,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5479,"operation":"read","slot":0,"start_us":5466},{"end_us":5483,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5480,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5491,"operation":"read","slot":0,"start_us":5484},{"end_us":5494,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5492,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5500,"operation":"read","slot":0,"start_us":5494},{"end_us":5503,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5500}],"schema":2,"slots":[{"at_us":5503,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b02e57f80-ca5e-73","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":6356,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6441,"slot":0,"token":"18e15c2b0387fd28-ca5e-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b0387fd28-ca5e-74","watchdog":false,"workers_joined":1,"writes":[{"at_us":6454,"bytes":57,"kind":"head","slot":0},{"at_us":6461,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":6272,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6280,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6275},{"end_us":6290,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6282,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6335,"operation":"read","slot":0,"start_us":6291},{"end_us":6340,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6338,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6351,"operation":"read","slot":0,"start_us":6341},{"end_us":6354,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6351}],"schema":2,"slots":[{"at_us":6354,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b0387fd28-ca5e-74","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":6368,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6466,"slot":0,"token":"18e15c2b04120ce8-ca5e-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b04120ce8-ca5e-75","watchdog":false,"workers_joined":1,"writes":[{"at_us":6477,"bytes":57,"kind":"head","slot":0},{"at_us":6482,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":33,"operation":"handoff","start_us":33},{"end_us":6261,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":34,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6268,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6264},{"end_us":6275,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6270,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6317,"operation":"read","slot":0,"start_us":6276},{"end_us":6321,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6319,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6333,"operation":"read","slot":0,"start_us":6321},{"end_us":6334,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6333,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6344,"operation":"read","slot":0,"start_us":6335},{"end_us":6346,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6345,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6359,"operation":"read","slot":0,"start_us":6347},{"end_us":6363,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6360}],"schema":2,"slots":[{"at_us":6363,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b04120ce8-ca5e-75","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"eof","completed_us":5391,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5562,"slot":0,"token":"18e15c2b04a121d0-ca5e-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b04a121d0-ca5e-76","watchdog":false,"workers_joined":1,"writes":[{"at_us":5579,"bytes":57,"kind":"head","slot":0},{"at_us":5597,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":38,"operation":"handoff","start_us":38},{"end_us":5345,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":39,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5348,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5346},{"end_us":5354,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5349,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5374,"operation":"read","slot":0,"start_us":5355},{"end_us":5376,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5375,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5388,"operation":"read","slot":0,"start_us":5377},{"end_us":5389,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5388}],"schema":2,"slots":[{"at_us":5389,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b04a121d0-ca5e-76","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5680,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5843,"slot":0,"token":"18e15c2b0541a3a8-ca5e-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b0541a3a8-ca5e-77","watchdog":false,"workers_joined":1,"writes":[{"at_us":5857,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5623,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":39,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5630,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5626},{"end_us":5633,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5631,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5673,"operation":"read","slot":0,"start_us":5634},{"end_us":5678,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5675}],"schema":2,"slots":[{"at_us":5678,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b0541a3a8-ca5e-77","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":6302,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6438,"slot":0,"token":"18e15c2b05bdc118-ca5e-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b05bdc118-ca5e-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":6453,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"end_us":6236,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":39,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6245,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6240},{"end_us":6249,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6246,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6293,"operation":"read","slot":0,"start_us":6249},{"end_us":6299,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6296}],"schema":2,"slots":[{"at_us":6299,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b05bdc118-ca5e-78","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5161,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5267,"slot":0,"token":"18e15c2b0647adb0-ca5e-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b0647adb0-ca5e-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":5278,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":38,"operation":"handoff","start_us":38},{"end_us":5095,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":39,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5104,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5099},{"end_us":5109,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5106,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5153,"operation":"read","slot":0,"start_us":5110},{"end_us":5158,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5155}],"schema":2,"slots":[{"at_us":5158,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b0647adb0-ca5e-79","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"empty-eof","completed_us":5033,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5236,"slot":0,"token":"18e15c2b06d5b8f8-ca5e-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b06d5b8f8-ca5e-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5245,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":44,"operation":"handoff","start_us":44},{"end_us":4986,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4991,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4987},{"end_us":4994,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4992,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":16384,"end_us":5026,"operation":"read","slot":0,"start_us":4994},{"end_us":5030,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5028}],"schema":2,"slots":[{"at_us":5030,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b06d5b8f8-ca5e-7a","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":6320,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6451,"slot":0,"token":"18e15c2b0749b848-ca5e-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b0749b848-ca5e-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6467,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"end_us":6306,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":39,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6314,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6310},{"end_us":6316,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6316}],"schema":2,"slots":[{"at_us":6316,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2b0749b848-ca5e-7b","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":6172,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6429,"slot":0,"token":"18e15c2b07e42388-ca5e-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b07e42388-ca5e-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6437,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":6161,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":35,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6167,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6163},{"end_us":6168,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6168}],"schema":2,"slots":[{"at_us":6168,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2b07e42388-ca5e-7c","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":6239,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6404,"slot":0,"token":"18e15c2b0868f388-ca5e-7d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b0868f388-ca5e-7d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6417,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":32,"operation":"handoff","start_us":32},{"end_us":6225,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":33,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6233,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6228},{"end_us":6235,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6235}],"schema":2,"slots":[{"at_us":6235,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2b0868f388-ca5e-7d","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-head","completed_us":6333,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6473,"slot":0,"token":"18e15c2b08f0b958-ca5e-7e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b08f0b958-ca5e-7e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6486,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":43,"operation":"handoff","start_us":43},{"end_us":6316,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6326,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6321},{"end_us":6329,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6329}],"schema":2,"slots":[{"at_us":6329,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2b08f0b958-ca5e-7e","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":57385,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6447,"slot":0,"token":"18e15c2b099503f0-ca5e-7f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b099503f0-ca5e-7f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6529,"bytes":66,"kind":"head","slot":0},{"at_us":6533,"bytes":6,"kind":"body","slot":0},{"at_us":57611,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":6241,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6249,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6244},{"end_us":6254,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6251,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6295,"operation":"read","slot":0,"start_us":6255},{"end_us":57327,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6298,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":57370,"operation":"read","slot":0,"start_us":57331},{"end_us":57379,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57373}],"schema":2,"slots":[{"at_us":57379,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b099503f0-ca5e-7f","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":57367,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6251,"slot":0,"token":"18e15c2b0d2ce438-ca5e-80"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b0d2ce438-ca5e-80","watchdog":false,"workers_joined":1,"writes":[{"at_us":6269,"bytes":66,"kind":"head","slot":0},{"at_us":6277,"bytes":6,"kind":"body","slot":0},{"at_us":57524,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":6044,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6054,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6049},{"end_us":6064,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6056,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6127,"operation":"read","slot":0,"start_us":6065},{"end_us":57286,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6130,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":57355,"operation":"read","slot":0,"start_us":57293},{"end_us":57362,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57358}],"schema":2,"slots":[{"at_us":57362,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b0d2ce438-ca5e-80","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":58543,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6030,"slot":0,"token":"18e15c2b10ea2ab8-ca5e-81"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b10ea2ab8-ca5e-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":6045,"bytes":66,"kind":"head","slot":0},{"at_us":6051,"bytes":6,"kind":"body","slot":0},{"at_us":58739,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":39,"operation":"handoff","start_us":39},{"end_us":5808,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":40,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5820,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5815},{"end_us":5826,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5822,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5866,"operation":"read","slot":0,"start_us":5828},{"end_us":58469,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5868,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58527,"operation":"read","slot":0,"start_us":58474},{"end_us":58537,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58533}],"schema":2,"slots":[{"at_us":58537,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b10ea2ab8-ca5e-81","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"malformed-body","completed_us":59633,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5566,"slot":0,"token":"18e15c2b149f2c08-ca5e-82"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b149f2c08-ca5e-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":5581,"bytes":66,"kind":"head","slot":0},{"at_us":5586,"bytes":6,"kind":"body","slot":0},{"at_us":59723,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":45,"operation":"handoff","start_us":45},{"end_us":5296,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5304,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5300},{"end_us":5311,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5306,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5357,"operation":"read","slot":0,"start_us":5312},{"end_us":59513,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5359,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":59611,"operation":"read","slot":0,"start_us":59520},{"end_us":59620,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59614}],"schema":2,"slots":[{"at_us":59620,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c2b149f2c08-ca5e-82","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":5632,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5896,"slot":0,"token":"18e15c2b1872fc10-ca5e-83"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b1872fc10-ca5e-83","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":65,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"end_us":5619,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5627,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5622},{"end_us":5629,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5628}],"schema":2,"slots":[{"at_us":5629,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2b1872fc10-ca5e-83","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":5937,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6092,"slot":0,"token":"18e15c2b18eb1df8-ca5e-84"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b18eb1df8-ca5e-84","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":5922,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":32,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5931,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5926},{"end_us":5933,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5933}],"schema":2,"slots":[{"at_us":5933,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2b18eb1df8-ca5e-84","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":5795,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5987,"slot":0,"token":"18e15c2b19786208-ca5e-85"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b19786208-ca5e-85","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":42,"operation":"handoff","start_us":42},{"end_us":5781,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5789,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5785},{"end_us":5791,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5791}],"schema":2,"slots":[{"at_us":5791,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2b19786208-ca5e-85","tolerance_us":5000}} +{"run":"stock-before-trace-hardening","probe":{"case":"head-error","completed_us":5031,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5254,"slot":0,"token":"18e15c2b19f6cd50-ca5e-86"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c2b19f6cd50-ca5e-86","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"end_us":5016,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5025,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5020},{"end_us":5027,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5027}],"schema":2,"slots":[{"at_us":5027,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c2b19f6cd50-ca5e-86","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":54661,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":10905,"slot":0,"token":"18e15c520f8ba238-f107-0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c520f8ba238-f107-0","watchdog":false,"workers_joined":1,"writes":[{"at_us":10916,"bytes":57,"kind":"head","slot":0},{"at_us":10923,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":1617,"operation":"dispatch","outcome":"pending","slot":0,"start_us":181},{"end_us":4025,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":2379,"status":"FastlyStatus::OK","timeout_ms":497},{"end_us":4122,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4060},{"bytes":1,"capacity":1,"end_us":4512,"operation":"read","slot":0,"start_us":4159},{"capacity":1,"end_us":54276,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":4537},{"end_us":54628,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":54340}],"schema":2,"slots":[{"at_us":54628,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c520f8ba238-f107-0","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":59913,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6029,"slot":0,"token":"18e15c52136e08a0-f107-1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c52136e08a0-f107-1","watchdog":false,"workers_joined":1,"writes":[{"at_us":6048,"bytes":57,"kind":"head","slot":0},{"at_us":6054,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5813,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5859,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5854},{"end_us":5881,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5875,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5922,"operation":"read","slot":0,"start_us":5920},{"end_us":59805,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5936,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":59872,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59867},{"end_us":59896,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59890}],"schema":2,"slots":[{"at_us":59896,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c52136e08a0-f107-1","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":59639,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5179,"slot":0,"token":"18e15c521723c958-f107-2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c521723c958-f107-2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5188,"bytes":57,"kind":"head","slot":0},{"at_us":5194,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":4915,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4946,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4942},{"end_us":4965,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4960,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4996,"operation":"read","slot":0,"start_us":4994},{"end_us":59522,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5007,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":59596,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59591},{"end_us":59624,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59617}],"schema":2,"slots":[{"at_us":59624,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c521723c958-f107-2","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":56864,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5616,"slot":0,"token":"18e15c521ad75f60-f107-3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c521ad75f60-f107-3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5634,"bytes":57,"kind":"head","slot":0},{"at_us":5639,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"end_us":5355,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5411,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5405},{"end_us":5469,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5463,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5503,"operation":"read","slot":0,"start_us":5501},{"end_us":56752,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5518,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":56818,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56813},{"end_us":56847,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56841}],"schema":2,"slots":[{"at_us":56847,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c521ad75f60-f107-3","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":57723,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5566,"slot":0,"token":"18e15c521e5ce858-f107-4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c521e5ce858-f107-4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5574,"bytes":57,"kind":"head","slot":0},{"at_us":5579,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"end_us":5328,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5353,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5347},{"end_us":5369,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5365,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5406,"operation":"read","slot":0,"start_us":5405},{"end_us":57583,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5417,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57670,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57665},{"end_us":57704,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57693}],"schema":2,"slots":[{"at_us":57704,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c521e5ce858-f107-4","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":502116,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5195,"slot":0,"token":"18e15c5221f4c4b8-f107-5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5221f4c4b8-f107-5","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":499705,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502039,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499765,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502086,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502086}],"schema":2,"slots":[{"at_us":502086,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5221f4c4b8-f107-5","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":500392,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5433,"slot":0,"token":"18e15c52400eaf90-f107-6"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c52400eaf90-f107-6","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":500313,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500374,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500374}],"schema":2,"slots":[{"at_us":500374,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c52400eaf90-f107-6","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":501468,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5408,"slot":0,"token":"18e15c525e00cee8-f107-7"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c525e00cee8-f107-7","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":73,"operation":"handoff","start_us":73},{"end_us":501388,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501448,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501447}],"schema":2,"slots":[{"at_us":501448,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c525e00cee8-f107-7","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":502049,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6448,"slot":0,"token":"18e15c527c03d278-f107-8"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c527c03d278-f107-8","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":67,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":110,"operation":"handoff","start_us":110},{"end_us":499624,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":165,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501987,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499685,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502030,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502030}],"schema":2,"slots":[{"at_us":502030,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c527c03d278-f107-8","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":500708,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5613,"slot":0,"token":"18e15c529a109620-f107-9"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c529a109620-f107-9","watchdog":false,"workers_joined":1,"writes":[{"at_us":5627,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5386,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5431,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5421},{"end_us":500610,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5456,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500681,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500672}],"schema":2,"slots":[{"at_us":500681,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c529a109620-f107-9","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":500233,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5233,"slot":0,"token":"18e15c52b80b5480-f107-a"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c52b80b5480-f107-a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5250,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5044,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5100,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5094},{"end_us":500140,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5114,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500214,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500207}],"schema":2,"slots":[{"at_us":500214,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c52b80b5480-f107-a","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":502592,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6032,"slot":0,"token":"18e15c52d5ffc1b0-f107-b"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c52d5ffc1b0-f107-b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6047,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":62,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":109,"operation":"handoff","start_us":109},{"end_us":5729,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":126,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5784,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5778},{"end_us":502426,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5799,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502542,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502516}],"schema":2,"slots":[{"at_us":502542,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c52d5ffc1b0-f107-b","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":500575,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6545,"slot":0,"token":"18e15c52f42eb740-f107-c"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c52f42eb740-f107-c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6567,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":95,"operation":"dispatch","outcome":"pending","slot":0,"start_us":47},{"body_reads":0,"deadline_us":500000,"end_us":141,"operation":"handoff","start_us":141},{"end_us":6116,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":156,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6182,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6172},{"end_us":500469,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6197,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500551,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500542}],"schema":2,"slots":[{"at_us":500551,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c52f42eb740-f107-c","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":501849,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5736,"slot":0,"token":"18e15c53122f0768-f107-d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c53122f0768-f107-d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5755,"bytes":57,"kind":"head","slot":0},{"at_us":5763,"bytes":1,"kind":"body","slot":0},{"at_us":309791,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5509,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5566,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5558},{"end_us":5588,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5581,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5613,"operation":"read","slot":0,"start_us":5610},{"end_us":309514,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5628,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309591,"operation":"read","slot":0,"start_us":309586},{"end_us":501718,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309609,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501799,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501790}],"schema":2,"slots":[{"at_us":501799,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c53122f0768-f107-d","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":500958,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6475,"slot":0,"token":"18e15c533042dba8-f107-e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c533042dba8-f107-e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6498,"bytes":57,"kind":"head","slot":0},{"at_us":6506,"bytes":1,"kind":"body","slot":0},{"at_us":307628,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":6121,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6182,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6176},{"end_us":6202,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6197,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6231,"operation":"read","slot":0,"start_us":6228},{"end_us":307007,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6247,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":307194,"operation":"read","slot":0,"start_us":307188},{"end_us":500819,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307215,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500906,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500889}],"schema":2,"slots":[{"at_us":500906,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c533042dba8-f107-e","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":501583,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5249,"slot":0,"token":"18e15c534e4b6548-f107-f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c534e4b6548-f107-f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5264,"bytes":57,"kind":"head","slot":0},{"at_us":5270,"bytes":1,"kind":"body","slot":0},{"at_us":309592,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":90,"operation":"handoff","start_us":90},{"end_us":4960,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5006,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4999},{"end_us":5025,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5019,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5124,"operation":"read","slot":0,"start_us":5110},{"end_us":309280,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5145,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309357,"operation":"read","slot":0,"start_us":309353},{"end_us":501441,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309386,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501521,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501513}],"schema":2,"slots":[{"at_us":501521,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c534e4b6548-f107-f","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":501148,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6539,"slot":0,"token":"18e15c536c72fc20-f107-10"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c536c72fc20-f107-10","watchdog":false,"workers_joined":1,"writes":[{"at_us":6564,"bytes":57,"kind":"head","slot":0},{"at_us":6570,"bytes":1,"kind":"body","slot":0},{"at_us":308743,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":148,"operation":"handoff","start_us":148},{"end_us":6013,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":234,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6105,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6098},{"end_us":6135,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6126,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6167,"operation":"read","slot":0,"start_us":6164},{"end_us":308136,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6179,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":308226,"operation":"read","slot":0,"start_us":308222},{"end_us":500827,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308239,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501015,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501001}],"schema":2,"slots":[{"at_us":501015,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c536c72fc20-f107-10","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":501125,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5247,"slot":0,"token":"18e15c538b192550-f107-11"},{"path":"/readiness/siblings/1","received_us":5821,"slot":1,"token":"18e15c538b192550-f107-11"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c538b192550-f107-11","watchdog":false,"workers_joined":2,"writes":[{"at_us":5268,"bytes":57,"kind":"head","slot":0},{"at_us":5832,"bytes":57,"kind":"head","slot":1},{"at_us":5838,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":1027,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":1111,"operation":"dispatch","outcome":"pending","slot":1,"start_us":1093},{"end_us":4274,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":1127,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":4328,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4322},{"end_us":4930,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":4343,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":4971,"operation":"head-poll","outcome":"ready","slot":1,"start_us":4967},{"end_us":5000,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":4994,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5026,"operation":"read","slot":1,"start_us":5022},{"end_us":5039,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5037,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5060,"operation":"read","slot":1,"start_us":5058},{"end_us":5071,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5070,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5111,"operation":"read","slot":1,"start_us":5109},{"end_us":5128,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5122,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5151,"operation":"read","slot":1,"start_us":5150},{"end_us":5168,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5164},{"end_us":501026,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5181,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501097,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501091}],"schema":2,"slots":[{"at_us":501097,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5168,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c538b192550-f107-11","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":500782,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5274,"slot":0,"token":"18e15c53a93951b8-f107-12"},{"path":"/readiness/siblings/1","received_us":5292,"slot":1,"token":"18e15c53a93951b8-f107-12"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c53a93951b8-f107-12","watchdog":false,"workers_joined":2,"writes":[{"at_us":5283,"bytes":57,"kind":"head","slot":0},{"at_us":5296,"bytes":57,"kind":"head","slot":1},{"at_us":5300,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":101,"operation":"dispatch","outcome":"pending","slot":1,"start_us":75},{"end_us":4993,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":131,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5015,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5010},{"end_us":5028,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5026,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5040,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5039},{"end_us":5054,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5052,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5082,"operation":"read","slot":1,"start_us":5080},{"end_us":5095,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5093,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5116,"operation":"read","slot":1,"start_us":5116},{"end_us":5127,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5125},{"end_us":500680,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5137,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500765,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500757}],"schema":2,"slots":[{"at_us":500765,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5127,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c53a93951b8-f107-12","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":501548,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5337,"slot":0,"token":"18e15c53c7432f30-f107-13"},{"path":"/readiness/siblings/1","received_us":5396,"slot":1,"token":"18e15c53c7432f30-f107-13"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c53c7432f30-f107-13","watchdog":false,"workers_joined":2,"writes":[{"at_us":5361,"bytes":57,"kind":"head","slot":0},{"at_us":5409,"bytes":57,"kind":"head","slot":1},{"at_us":5419,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":109,"operation":"dispatch","outcome":"pending","slot":1,"start_us":81},{"body_reads":0,"deadline_us":500000,"end_us":128,"operation":"handoff","start_us":128},{"end_us":5101,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":158,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5141,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5136},{"end_us":5158,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5155,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5171,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5170},{"end_us":5186,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5181,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5213,"operation":"read","slot":1,"start_us":5210},{"end_us":5225,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5224,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5244,"operation":"read","slot":1,"start_us":5243},{"end_us":5255,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5253,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5274,"operation":"read","slot":1,"start_us":5273},{"end_us":5285,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5282,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5305,"operation":"read","slot":1,"start_us":5305},{"end_us":5316,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5313},{"end_us":501467,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5328,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501530,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501521}],"schema":2,"slots":[{"at_us":501530,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5316,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c53c7432f30-f107-13","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":501514,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6494,"slot":1,"token":"18e15c53e559ca60-f107-14"},{"path":"/readiness/siblings/0","received_us":6501,"slot":0,"token":"18e15c53e559ca60-f107-14"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c53e559ca60-f107-14","watchdog":false,"workers_joined":2,"writes":[{"at_us":6513,"bytes":57,"kind":"head","slot":1},{"at_us":6521,"bytes":57,"kind":"head","slot":0},{"at_us":12794,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":99,"operation":"dispatch","outcome":"pending","slot":1,"start_us":83},{"body_reads":0,"deadline_us":500000,"end_us":130,"operation":"handoff","start_us":130},{"end_us":6267,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":144,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6318,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6312},{"end_us":6337,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6333,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6350,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6349},{"end_us":12515,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6359,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":12587,"operation":"read","slot":1,"start_us":12582},{"end_us":12605,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12600,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":16384,"end_us":12628,"operation":"read","slot":1,"start_us":12627},{"end_us":12642,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12638},{"end_us":501379,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12658,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":501491,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501467}],"schema":2,"slots":[{"at_us":501491,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12642,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c53e559ca60-f107-14","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":501941,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5636,"slot":0,"token":"18e15c54036b5898-f107-15"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c54036b5898-f107-15","watchdog":false,"workers_joined":1,"writes":[{"at_us":5727,"bytes":57,"kind":"head","slot":0},{"at_us":5745,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5441,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5500,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5493},{"end_us":5556,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5551,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5582,"operation":"read","slot":0,"start_us":5579},{"end_us":501811,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5596,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501898,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501889}],"schema":2,"slots":[{"at_us":501898,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c54036b5898-f107-15","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":500927,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6467,"slot":0,"token":"18e15c54217d63d0-f107-16"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c54217d63d0-f107-16","watchdog":false,"workers_joined":1,"writes":[{"at_us":6488,"bytes":57,"kind":"head","slot":0},{"at_us":6495,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":73,"operation":"dispatch","outcome":"pending","slot":0,"start_us":32},{"end_us":6001,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":121,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6052,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6044},{"end_us":6077,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6065,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6118,"operation":"read","slot":0,"start_us":6115},{"end_us":500846,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6130,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500909,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500900}],"schema":2,"slots":[{"at_us":500909,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c54217d63d0-f107-16","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":501445,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5208,"slot":0,"token":"18e15c543f807ae8-f107-17"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c543f807ae8-f107-17","watchdog":false,"workers_joined":1,"writes":[{"at_us":5217,"bytes":57,"kind":"head","slot":0},{"at_us":5221,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"end_us":4981,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5027,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5021},{"end_us":5046,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5041,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5071,"operation":"read","slot":0,"start_us":5068},{"end_us":501352,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5099,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501417,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501410}],"schema":2,"slots":[{"at_us":501417,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c543f807ae8-f107-17","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":501228,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6422,"slot":0,"token":"18e15c545d8c8ec8-f107-18"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c545d8c8ec8-f107-18","watchdog":false,"workers_joined":1,"writes":[{"at_us":6431,"bytes":57,"kind":"head","slot":0},{"at_us":6437,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"end_us":6187,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6225,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6219},{"end_us":6241,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6237,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6264,"operation":"read","slot":0,"start_us":6262},{"end_us":501121,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6275,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501186,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501178}],"schema":2,"slots":[{"at_us":501186,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c545d8c8ec8-f107-18","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5165,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5175,"slot":0,"token":"18e15c547b9120c8-f107-19"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c547b9120c8-f107-19","watchdog":false,"workers_joined":1,"writes":[{"at_us":5186,"bytes":57,"kind":"head","slot":0},{"at_us":5191,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":4958,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5001,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4996},{"end_us":5023,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5018,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5049,"operation":"read","slot":0,"start_us":5046},{"end_us":5062,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5061,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5085,"operation":"read","slot":0,"start_us":5084},{"end_us":5095,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5094,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5112,"operation":"read","slot":0,"start_us":5112},{"end_us":5124,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5121,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5143,"operation":"read","slot":0,"start_us":5142},{"end_us":5153,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5151}],"schema":2,"slots":[{"at_us":5153,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c547b9120c8-f107-19","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5132,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5165,"slot":0,"token":"18e15c547c059d18-f107-1a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c547c059d18-f107-1a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5179,"bytes":57,"kind":"head","slot":0},{"at_us":5185,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"end_us":5010,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5039,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5034},{"end_us":5054,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5050,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5076,"operation":"read","slot":0,"start_us":5075},{"end_us":5089,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5087,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5110,"operation":"read","slot":0,"start_us":5110},{"end_us":5121,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5119}],"schema":2,"slots":[{"at_us":5121,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c547c059d18-f107-1a","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5436,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5437,"slot":0,"token":"18e15c547c7650a8-f107-1b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c547c7650a8-f107-1b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5444,"bytes":57,"kind":"head","slot":0},{"at_us":5451,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":46,"operation":"handoff","start_us":46},{"end_us":5256,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5283,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5279},{"end_us":5298,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5294,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5320,"operation":"read","slot":0,"start_us":5318},{"end_us":5332,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5331,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5353,"operation":"read","slot":0,"start_us":5352},{"end_us":5364,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5363,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5381,"operation":"read","slot":0,"start_us":5380},{"end_us":5392,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5390,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5412,"operation":"read","slot":0,"start_us":5411},{"end_us":5424,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5421}],"schema":2,"slots":[{"at_us":5424,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c547c7650a8-f107-1b","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5688,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5598,"slot":0,"token":"18e15c547ceaec38-f107-1c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c547ceaec38-f107-1c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5621,"bytes":57,"kind":"head","slot":0},{"at_us":5629,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"end_us":5328,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5523,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5490},{"end_us":5575,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5564,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5614,"operation":"read","slot":0,"start_us":5610},{"end_us":5630,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5626,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5655,"operation":"read","slot":0,"start_us":5655},{"end_us":5672,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5667}],"schema":2,"slots":[{"at_us":5672,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c547ceaec38-f107-1c","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":4984,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6289,"slot":0,"token":"18e15c547d733ea8-f107-1d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c547d733ea8-f107-1d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6309,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":4837,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":101,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4902,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4895},{"end_us":4922,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4918,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":1,"end_us":4951,"operation":"read","slot":0,"start_us":4949},{"end_us":4970,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":4967}],"schema":2,"slots":[{"at_us":4970,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c547d733ea8-f107-1d","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":4444,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5271,"slot":0,"token":"18e15c547dfc0a30-f107-1e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c547dfc0a30-f107-1e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5289,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":4305,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4362,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4356},{"end_us":4383,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4377,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":16384,"end_us":4414,"operation":"read","slot":0,"start_us":4412},{"end_us":4430,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":4427}],"schema":2,"slots":[{"at_us":4430,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c547dfc0a30-f107-1e","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":4692,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5732,"slot":0,"token":"18e15c547e7b5038-f107-1f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c547e7b5038-f107-1f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5751,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":99,"operation":"handoff","start_us":99},{"end_us":4559,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":120,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4612,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4607},{"end_us":4631,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4627,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":1,"end_us":4663,"operation":"read","slot":0,"start_us":4660},{"end_us":4678,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":4675}],"schema":2,"slots":[{"at_us":4678,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c547e7b5038-f107-1f","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":4452,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5341,"slot":0,"token":"18e15c547f0e58d8-f107-20"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c547f0e58d8-f107-20","watchdog":false,"workers_joined":1,"writes":[{"at_us":5357,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":378,"operation":"dispatch","outcome":"pending","slot":0,"start_us":240},{"body_reads":0,"deadline_us":500000,"end_us":420,"operation":"handoff","start_us":420},{"end_us":4319,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":433,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4370,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4365},{"end_us":4389,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4385,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":16384,"end_us":4422,"operation":"read","slot":0,"start_us":4420},{"end_us":4438,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":4434}],"schema":2,"slots":[{"at_us":4438,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c547f0e58d8-f107-20","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":5589,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5446,"slot":0,"token":"18e15c547f82d140-f107-21"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c547f82d140-f107-21","watchdog":false,"workers_joined":1,"writes":[{"at_us":5462,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5436,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5541,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5535},{"end_us":5569,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5569}],"schema":2,"slots":[{"at_us":5569,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c547f82d140-f107-21","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":5423,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5575,"slot":0,"token":"18e15c547ffc2ba8-f107-22"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c547ffc2ba8-f107-22","watchdog":false,"workers_joined":1,"writes":[{"at_us":5589,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":5365,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5392,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5388},{"end_us":5406,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5406}],"schema":2,"slots":[{"at_us":5406,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c547ffc2ba8-f107-22","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":6292,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6405,"slot":0,"token":"18e15c54806e1f88-f107-23"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c54806e1f88-f107-23","watchdog":false,"workers_joined":1,"writes":[{"at_us":6413,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"end_us":6226,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6250,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6247},{"end_us":6267,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6266}],"schema":2,"slots":[{"at_us":6267,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c54806e1f88-f107-23","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":6316,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6430,"slot":0,"token":"18e15c5480ea7b78-f107-24"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5480ea7b78-f107-24","watchdog":false,"workers_joined":1,"writes":[{"at_us":6443,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"end_us":6263,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6291,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6287},{"end_us":6303,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6303}],"schema":2,"slots":[{"at_us":6303,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5480ea7b78-f107-24","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":58260,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5213,"slot":0,"token":"18e15c5481670e18-f107-25"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5481670e18-f107-25","watchdog":false,"workers_joined":1,"writes":[{"at_us":5222,"bytes":66,"kind":"head","slot":0},{"at_us":5226,"bytes":6,"kind":"body","slot":0},{"at_us":58311,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":5011,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5037,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5034},{"end_us":5057,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5052,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5078,"operation":"read","slot":0,"start_us":5075},{"end_us":58137,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5089,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":58218,"error":"Other","operation":"read","slot":0,"start_us":58199},{"end_us":58243,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58237}],"schema":2,"slots":[{"at_us":58243,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5481670e18-f107-25","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":58530,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5735,"slot":0,"token":"18e15c5485072bc0-f107-26"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5485072bc0-f107-26","watchdog":false,"workers_joined":1,"writes":[{"at_us":5751,"bytes":66,"kind":"head","slot":0},{"at_us":5756,"bytes":6,"kind":"body","slot":0},{"at_us":58658,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5505,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5549,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5543},{"end_us":5570,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5565,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5600,"operation":"read","slot":0,"start_us":5598},{"end_us":58420,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5612,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":58497,"error":"Other","operation":"read","slot":0,"start_us":58491},{"end_us":58517,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58512}],"schema":2,"slots":[{"at_us":58517,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5485072bc0-f107-26","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":58885,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6430,"slot":0,"token":"18e15c5488aa52c0-f107-27"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5488aa52c0-f107-27","watchdog":false,"workers_joined":1,"writes":[{"at_us":6443,"bytes":66,"kind":"head","slot":0},{"at_us":6448,"bytes":6,"kind":"body","slot":0},{"at_us":59025,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"end_us":6159,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6198,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6193},{"end_us":6216,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6210,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6242,"operation":"read","slot":0,"start_us":6240},{"end_us":58762,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6254,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":58837,"error":"Other","operation":"read","slot":0,"start_us":58832},{"end_us":58865,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58858}],"schema":2,"slots":[{"at_us":58865,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5488aa52c0-f107-27","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":57914,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5423,"slot":0,"token":"18e15c548c56cc78-f107-28"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c548c56cc78-f107-28","watchdog":false,"workers_joined":1,"writes":[{"at_us":5433,"bytes":66,"kind":"head","slot":0},{"at_us":5438,"bytes":6,"kind":"body","slot":0},{"at_us":57985,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"end_us":5141,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5175,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5171},{"end_us":5194,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5189,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5225,"operation":"read","slot":0,"start_us":5223},{"end_us":57802,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5237,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57877,"error":"Other","operation":"read","slot":0,"start_us":57871},{"end_us":57898,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57892}],"schema":2,"slots":[{"at_us":57898,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c548c56cc78-f107-28","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":5709,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5847,"slot":0,"token":"18e15c548ff557c8-f107-29"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c548ff557c8-f107-29","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5635,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5679,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5673},{"end_us":5692,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5692}],"schema":2,"slots":[{"at_us":5692,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c548ff557c8-f107-29","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":5113,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5228,"slot":0,"token":"18e15c5490683dd8-f107-2a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5490683dd8-f107-2a","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5053,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5085,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5080},{"end_us":5099,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5098}],"schema":2,"slots":[{"at_us":5099,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5490683dd8-f107-2a","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":6284,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6448,"slot":0,"token":"18e15c5490d17f28-f107-2b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5490d17f28-f107-2b","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":91,"operation":"handoff","start_us":91},{"end_us":6230,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6257,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6253},{"end_us":6269,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6269}],"schema":2,"slots":[{"at_us":6269,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5490d17f28-f107-2b","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":6219,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6397,"slot":0,"token":"18e15c54914e34f0-f107-2c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c54914e34f0-f107-2c","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"end_us":6172,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6192,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6189},{"end_us":6205,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6204}],"schema":2,"slots":[{"at_us":6205,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c54914e34f0-f107-2c","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":60752,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6421,"slot":0,"token":"18e15c5491c7e930-f107-2d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5491c7e930-f107-2d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6437,"bytes":57,"kind":"head","slot":0},{"at_us":6442,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":6264,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6299,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6293},{"bytes":1,"capacity":1,"end_us":6323,"operation":"read","slot":0,"start_us":6319},{"capacity":1,"end_us":60455,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":6340},{"end_us":60716,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60518}],"schema":2,"slots":[{"at_us":60716,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5491c7e930-f107-2d","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":61197,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6470,"slot":0,"token":"18e15c549589bbc0-f107-2e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c549589bbc0-f107-2e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6494,"bytes":57,"kind":"head","slot":0},{"at_us":6501,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":6216,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6272,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6265},{"end_us":6290,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6285,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6314,"operation":"read","slot":0,"start_us":6312},{"end_us":61088,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6330,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":61155,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":61150},{"end_us":61180,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61174}],"schema":2,"slots":[{"at_us":61180,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c549589bbc0-f107-2e","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":55560,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5582,"slot":0,"token":"18e15c54995ad090-f107-2f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c54995ad090-f107-2f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5595,"bytes":57,"kind":"head","slot":0},{"at_us":5601,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5373,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5411,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5406},{"end_us":5429,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5424,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5458,"operation":"read","slot":0,"start_us":5456},{"end_us":55451,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5472,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":55516,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":55511},{"end_us":55543,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55537}],"schema":2,"slots":[{"at_us":55543,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c54995ad090-f107-2f","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":57719,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5773,"slot":0,"token":"18e15c549cd2bd28-f107-30"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c549cd2bd28-f107-30","watchdog":false,"workers_joined":1,"writes":[{"at_us":5789,"bytes":57,"kind":"head","slot":0},{"at_us":5865,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"end_us":5544,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5586,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5580},{"end_us":5604,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5599,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5627,"operation":"read","slot":0,"start_us":5625},{"end_us":57614,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5642,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":57682,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57676},{"end_us":57705,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57699}],"schema":2,"slots":[{"at_us":57705,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c549cd2bd28-f107-30","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":57925,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5247,"slot":0,"token":"18e15c54a06cef30-f107-31"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c54a06cef30-f107-31","watchdog":false,"workers_joined":1,"writes":[{"at_us":5262,"bytes":57,"kind":"head","slot":0},{"at_us":5269,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":82,"operation":"handoff","start_us":82},{"end_us":4979,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5024,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5017},{"end_us":5044,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5037,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5078,"operation":"read","slot":0,"start_us":5076},{"end_us":57803,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5089,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57883,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57878},{"end_us":57909,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57901}],"schema":2,"slots":[{"at_us":57909,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c54a06cef30-f107-31","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":501423,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5354,"slot":0,"token":"18e15c54a40b1108-f107-32"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c54a40b1108-f107-32","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":501344,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501404,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501404}],"schema":2,"slots":[{"at_us":501404,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c54a40b1108-f107-32","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":500793,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5245,"slot":0,"token":"18e15c54c20e7e10-f107-33"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c54c20e7e10-f107-33","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":500704,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500771,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500771}],"schema":2,"slots":[{"at_us":500771,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c54c20e7e10-f107-33","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":500521,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5552,"slot":0,"token":"18e15c54e003ae90-f107-34"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c54e003ae90-f107-34","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":89,"operation":"handoff","start_us":89},{"end_us":500438,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500504,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500503}],"schema":2,"slots":[{"at_us":500504,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c54e003ae90-f107-34","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":500635,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5817,"slot":0,"token":"18e15c54fdf5f4f8-f107-35"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c54fdf5f4f8-f107-35","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"end_us":500503,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500609,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500609}],"schema":2,"slots":[{"at_us":500609,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c54fdf5f4f8-f107-35","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":502377,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6448,"slot":0,"token":"18e15c551bef3488-f107-36"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c551bef3488-f107-36","watchdog":false,"workers_joined":1,"writes":[{"at_us":6464,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":6138,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6228,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6212},{"end_us":502275,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6251,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":502350,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502341}],"schema":2,"slots":[{"at_us":502350,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c551bef3488-f107-36","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":500369,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6442,"slot":0,"token":"18e15c553b386fd0-f107-37"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c553b386fd0-f107-37","watchdog":false,"workers_joined":1,"writes":[{"at_us":6451,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":64,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":6076,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1561,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":6115,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6109},{"end_us":500261,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6128,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500330,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500323}],"schema":2,"slots":[{"at_us":500330,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c553b386fd0-f107-37","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":501551,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5262,"slot":0,"token":"18e15c55594a1190-f107-38"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c55594a1190-f107-38","watchdog":false,"workers_joined":1,"writes":[{"at_us":5280,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":106,"operation":"handoff","start_us":106},{"end_us":4983,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":121,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5036,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5030},{"end_us":501443,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5050,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501526,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501517}],"schema":2,"slots":[{"at_us":501526,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c55594a1190-f107-38","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":500696,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5580,"slot":0,"token":"18e15c557752d9b0-f107-39"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c557752d9b0-f107-39","watchdog":false,"workers_joined":1,"writes":[{"at_us":5602,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":104,"operation":"handoff","start_us":104},{"end_us":5341,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":129,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5393,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5387},{"end_us":500587,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5408,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500674,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500666}],"schema":2,"slots":[{"at_us":500674,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c557752d9b0-f107-39","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":500806,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5718,"slot":0,"token":"18e15c55954c09a0-f107-3a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c55954c09a0-f107-3a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5738,"bytes":57,"kind":"head","slot":0},{"at_us":5745,"bytes":1,"kind":"body","slot":0},{"at_us":308575,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5479,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5524,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5517},{"end_us":5543,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5537,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5566,"operation":"read","slot":0,"start_us":5564},{"end_us":308301,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5577,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":308380,"operation":"read","slot":0,"start_us":308376},{"end_us":500723,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308396,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500785,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500777}],"schema":2,"slots":[{"at_us":500785,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c55954c09a0-f107-3a","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":501461,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6432,"slot":0,"token":"18e15c55b34ee238-f107-3b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c55b34ee238-f107-3b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6445,"bytes":57,"kind":"head","slot":0},{"at_us":6462,"bytes":1,"kind":"body","slot":0},{"at_us":309038,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":6216,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6258,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6250},{"end_us":6372,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6272,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6413,"operation":"read","slot":0,"start_us":6411},{"end_us":308731,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6426,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":308907,"operation":"read","slot":0,"start_us":308903},{"end_us":501321,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308924,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501397,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501390}],"schema":2,"slots":[{"at_us":501397,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c55b34ee238-f107-3b","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":502556,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5204,"slot":0,"token":"18e15c55d15cea18-f107-3c"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c55d15cea18-f107-3c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5212,"bytes":57,"kind":"head","slot":0},{"at_us":5218,"bytes":1,"kind":"body","slot":0},{"at_us":308761,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"end_us":4959,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4983,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4978},{"end_us":4999,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4995,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5026,"operation":"read","slot":0,"start_us":5023},{"end_us":308591,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5037,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":308716,"operation":"read","slot":0,"start_us":308710},{"end_us":502380,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308738,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":502477,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502436}],"schema":2,"slots":[{"at_us":502477,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c55d15cea18-f107-3c","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":500679,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5810,"slot":0,"token":"18e15c55ef7d7c10-f107-3d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c55ef7d7c10-f107-3d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5820,"bytes":57,"kind":"head","slot":0},{"at_us":5826,"bytes":1,"kind":"body","slot":0},{"at_us":309407,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"end_us":5497,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5546,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5538},{"end_us":5567,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5561,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5599,"operation":"read","slot":0,"start_us":5596},{"end_us":309054,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5611,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":309112,"operation":"read","slot":0,"start_us":309108},{"end_us":500591,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309125,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500658,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500650}],"schema":2,"slots":[{"at_us":500658,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c55ef7d7c10-f107-3d","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":500621,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5391,"slot":0,"token":"18e15c560d801a10-f107-3e"},{"path":"/readiness/siblings/1","received_us":5410,"slot":1,"token":"18e15c560d801a10-f107-3e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c560d801a10-f107-3e","watchdog":false,"workers_joined":2,"writes":[{"at_us":5414,"bytes":57,"kind":"head","slot":0},{"at_us":5430,"bytes":57,"kind":"head","slot":1},{"at_us":5438,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":115,"operation":"dispatch","outcome":"pending","slot":1,"start_us":90},{"end_us":5183,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":140,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5249,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5240},{"end_us":5270,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5266,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5285,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5284},{"end_us":5299,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5295,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5326,"operation":"read","slot":1,"start_us":5323},{"end_us":5340,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5338,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5360,"operation":"read","slot":1,"start_us":5359},{"end_us":5375,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5370,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5394,"operation":"read","slot":1,"start_us":5394},{"end_us":5407,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5404,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5425,"operation":"read","slot":1,"start_us":5425},{"end_us":5446,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5438},{"end_us":500504,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5459,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500584,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500577}],"schema":2,"slots":[{"at_us":500584,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5446,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c560d801a10-f107-3e","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":500457,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5232,"slot":1,"token":"18e15c562b915a28-f107-3f"},{"path":"/readiness/siblings/0","received_us":5235,"slot":0,"token":"18e15c562b915a28-f107-3f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e15c562b915a28-f107-3f","watchdog":false,"workers_joined":2,"writes":[{"at_us":5239,"bytes":57,"kind":"head","slot":1},{"at_us":5247,"bytes":57,"kind":"head","slot":0},{"at_us":11424,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":99,"operation":"dispatch","outcome":"pending","slot":1,"start_us":77},{"end_us":4947,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":117,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4974,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4969},{"end_us":4988,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":4986,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":5002,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5001},{"end_us":11129,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5013,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11175,"operation":"read","slot":1,"start_us":11172},{"end_us":11191,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11188,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11215,"operation":"read","slot":1,"start_us":11214},{"end_us":11231,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11227},{"end_us":500318,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11244,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500396,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500389}],"schema":2,"slots":[{"at_us":500396,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11231,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c562b915a28-f107-3f","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":507233,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6460,"slot":0,"token":"18e15c564997b530-f107-40"},{"path":"/readiness/siblings/1","received_us":6466,"slot":1,"token":"18e15c564997b530-f107-40"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c564997b530-f107-40","watchdog":false,"workers_joined":2,"writes":[{"at_us":6472,"bytes":57,"kind":"head","slot":0},{"at_us":6478,"bytes":57,"kind":"head","slot":1},{"at_us":6483,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":120,"operation":"dispatch","outcome":"pending","slot":1,"start_us":96},{"body_reads":0,"deadline_us":500000,"end_us":154,"operation":"handoff","start_us":154},{"end_us":6183,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":183,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6224,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6219},{"end_us":6242,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6239,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6257,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6256},{"end_us":6272,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6267,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6297,"operation":"read","slot":1,"start_us":6295},{"end_us":6310,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6309,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6330,"operation":"read","slot":1,"start_us":6329},{"end_us":6341,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6340,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6362,"operation":"read","slot":1,"start_us":6361},{"end_us":6373,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6371,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6394,"operation":"read","slot":1,"start_us":6393},{"end_us":6405,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6402},{"end_us":506716,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6419,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":507211,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":507203}],"schema":2,"slots":[{"at_us":507211,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6405,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c564997b530-f107-40","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":502147,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":8475,"slot":1,"token":"18e15c56691984d8-f107-41"},{"path":"/readiness/siblings/0","received_us":9364,"slot":0,"token":"18e15c56691984d8-f107-41"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c56691984d8-f107-41","watchdog":false,"workers_joined":2,"writes":[{"at_us":8611,"bytes":57,"kind":"head","slot":1},{"at_us":9395,"bytes":57,"kind":"head","slot":0},{"at_us":13955,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":193,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":287,"operation":"dispatch","outcome":"pending","slot":1,"start_us":266},{"body_reads":0,"deadline_us":500000,"end_us":305,"operation":"handoff","start_us":305},{"end_us":3058,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":317,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":3104,"operation":"head-poll","outcome":"ready","slot":1,"start_us":3098},{"end_us":4071,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":3118,"status":"FastlyStatus::OK","timeout_ms":496},{"end_us":4111,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4107},{"end_us":8561,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":4124,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":3,"capacity":16384,"end_us":8808,"operation":"read","slot":1,"start_us":8803},{"end_us":8890,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":8878,"status":"FastlyStatus::OK","timeout_ms":491},{"bytes":0,"capacity":16384,"end_us":8947,"operation":"read","slot":1,"start_us":8945},{"end_us":8965,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":8960},{"end_us":502056,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":8982,"status":"FastlyStatus::OK","timeout_ms":491},{"end_us":502127,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502120}],"schema":2,"slots":[{"at_us":502127,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":8965,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c56691984d8-f107-41","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":500902,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6024,"slot":0,"token":"18e15c56878af9d8-f107-42"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c56878af9d8-f107-42","watchdog":false,"workers_joined":1,"writes":[{"at_us":6040,"bytes":57,"kind":"head","slot":0},{"at_us":6046,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5757,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5814,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5809},{"end_us":5835,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5831,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5859,"operation":"read","slot":0,"start_us":5857},{"end_us":500789,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5871,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500867,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500860}],"schema":2,"slots":[{"at_us":500867,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c56878af9d8-f107-42","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":501347,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5214,"slot":0,"token":"18e15c56a589deb8-f107-43"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c56a589deb8-f107-43","watchdog":false,"workers_joined":1,"writes":[{"at_us":5230,"bytes":57,"kind":"head","slot":0},{"at_us":5236,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":4955,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4994,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4990},{"end_us":5012,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5007,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5042,"operation":"read","slot":0,"start_us":5039},{"end_us":501229,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5054,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501305,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501298}],"schema":2,"slots":[{"at_us":501305,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c56a589deb8-f107-43","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":500293,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6215,"slot":0,"token":"18e15c56c3934ed0-f107-44"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c56c3934ed0-f107-44","watchdog":false,"workers_joined":1,"writes":[{"at_us":6225,"bytes":57,"kind":"head","slot":0},{"at_us":6230,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"end_us":5966,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6003,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5996},{"end_us":6024,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6020,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6049,"operation":"read","slot":0,"start_us":6047},{"end_us":500172,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6059,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500253,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500245}],"schema":2,"slots":[{"at_us":500253,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c56c3934ed0-f107-44","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":501490,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5211,"slot":0,"token":"18e15c56e18b4258-f107-45"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c56e18b4258-f107-45","watchdog":false,"workers_joined":1,"writes":[{"at_us":5221,"bytes":57,"kind":"head","slot":0},{"at_us":5226,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"end_us":4950,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4993,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4986},{"end_us":5013,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5008,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5042,"operation":"read","slot":0,"start_us":5040},{"end_us":501258,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5057,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501365,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501353}],"schema":2,"slots":[{"at_us":501365,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c56e18b4258-f107-45","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5403,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5361,"slot":0,"token":"18e15c56ff968348-f107-46"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c56ff968348-f107-46","watchdog":false,"workers_joined":1,"writes":[{"at_us":5383,"bytes":57,"kind":"head","slot":0},{"at_us":5393,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5151,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":248,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5220,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5214},{"end_us":5244,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5238,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5275,"operation":"read","slot":0,"start_us":5272},{"end_us":5289,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5288,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5310,"operation":"read","slot":0,"start_us":5308},{"end_us":5321,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5320,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5343,"operation":"read","slot":0,"start_us":5343},{"end_us":5356,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5353,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5374,"operation":"read","slot":0,"start_us":5374},{"end_us":5387,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5383}],"schema":2,"slots":[{"at_us":5387,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c56ff968348-f107-46","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5441,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5426,"slot":0,"token":"18e15c57001874e8-f107-47"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c57001874e8-f107-47","watchdog":false,"workers_joined":1,"writes":[{"at_us":5450,"bytes":57,"kind":"head","slot":0},{"at_us":5458,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":5260,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5322,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5316},{"end_us":5345,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5338,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5376,"operation":"read","slot":0,"start_us":5373},{"end_us":5391,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5389,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5412,"operation":"read","slot":0,"start_us":5412},{"end_us":5428,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5422}],"schema":2,"slots":[{"at_us":5428,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c57001874e8-f107-47","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5329,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5369,"slot":0,"token":"18e15c57009359d8-f107-48"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c57009359d8-f107-48","watchdog":false,"workers_joined":1,"writes":[{"at_us":5396,"bytes":57,"kind":"head","slot":0},{"at_us":5408,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"body_reads":0,"deadline_us":500000,"end_us":188,"operation":"handoff","start_us":188},{"end_us":5088,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":213,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5152,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5146},{"end_us":5174,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5167,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5205,"operation":"read","slot":0,"start_us":5202},{"end_us":5219,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5217,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5238,"operation":"read","slot":0,"start_us":5237},{"end_us":5252,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5251,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5271,"operation":"read","slot":0,"start_us":5270},{"end_us":5283,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5280,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5300,"operation":"read","slot":0,"start_us":5300},{"end_us":5313,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5309}],"schema":2,"slots":[{"at_us":5313,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c57009359d8-f107-48","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5066,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5271,"slot":0,"token":"18e15c570116b2d8-f107-49"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c570116b2d8-f107-49","watchdog":false,"workers_joined":1,"writes":[{"at_us":5295,"bytes":57,"kind":"head","slot":0},{"at_us":5317,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":134,"operation":"handoff","start_us":134},{"end_us":4876,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":146,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4936,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4929},{"end_us":4964,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4953,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":3,"capacity":16384,"end_us":4999,"operation":"read","slot":0,"start_us":4996},{"end_us":5014,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5011,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5039,"operation":"read","slot":0,"start_us":5039},{"end_us":5053,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5049}],"schema":2,"slots":[{"at_us":5053,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c570116b2d8-f107-49","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":6337,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6459,"slot":0,"token":"18e15c5701909210-f107-4a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5701909210-f107-4a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6476,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":6238,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6273,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6268},{"end_us":6287,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6284,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6309,"operation":"read","slot":0,"start_us":6306},{"end_us":6326,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6323}],"schema":2,"slots":[{"at_us":6326,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5701909210-f107-4a","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":6426,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6516,"slot":0,"token":"18e15c5702129b20-f107-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5702129b20-f107-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6538,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":6305,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6348,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6343},{"end_us":6369,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6366,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6397,"operation":"read","slot":0,"start_us":6395},{"end_us":6410,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6408}],"schema":2,"slots":[{"at_us":6410,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5702129b20-f107-4b","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":5862,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5919,"slot":0,"token":"18e15c5702947d20-f107-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5702947d20-f107-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5941,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":88,"operation":"handoff","start_us":88},{"end_us":5726,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":105,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5786,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5779},{"end_us":5808,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5804,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5832,"operation":"read","slot":0,"start_us":5830},{"end_us":5847,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5843}],"schema":2,"slots":[{"at_us":5847,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5702947d20-f107-4c","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":6405,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6476,"slot":0,"token":"18e15c57030f6dc8-f107-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c57030f6dc8-f107-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6492,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":94,"operation":"handoff","start_us":94},{"end_us":6287,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6334,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6327},{"end_us":6349,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6345,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6378,"operation":"read","slot":0,"start_us":6376},{"end_us":6392,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6389}],"schema":2,"slots":[{"at_us":6392,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c57030f6dc8-f107-4d","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":5295,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5483,"slot":0,"token":"18e15c5703925198-f107-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5703925198-f107-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5499,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5239,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5268,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5264},{"end_us":5281,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5281}],"schema":2,"slots":[{"at_us":5281,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5703925198-f107-4e","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":6309,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6422,"slot":0,"token":"18e15c5704014008-f107-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5704014008-f107-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6432,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":6253,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6282,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6278},{"end_us":6295,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6295}],"schema":2,"slots":[{"at_us":6295,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5704014008-f107-4f","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":5478,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5595,"slot":0,"token":"18e15c57047e18f8-f107-50"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c57047e18f8-f107-50","watchdog":false,"workers_joined":1,"writes":[{"at_us":5612,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":55,"operation":"handoff","start_us":55},{"end_us":5427,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5450,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5446},{"end_us":5462,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5462}],"schema":2,"slots":[{"at_us":5462,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c57047e18f8-f107-50","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":6343,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6477,"slot":0,"token":"18e15c5704ee5758-f107-51"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5704ee5758-f107-51","watchdog":false,"workers_joined":1,"writes":[{"at_us":6490,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"end_us":6289,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6313,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6309},{"end_us":6326,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6326}],"schema":2,"slots":[{"at_us":6326,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5704ee5758-f107-51","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":59021,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6167,"slot":0,"token":"18e15c57056b18d8-f107-52"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c57056b18d8-f107-52","watchdog":false,"workers_joined":1,"writes":[{"at_us":6179,"bytes":66,"kind":"head","slot":0},{"at_us":6184,"bytes":6,"kind":"body","slot":0},{"at_us":59026,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5983,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":48,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6012,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6009},{"end_us":6027,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6024,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6048,"operation":"read","slot":0,"start_us":6046},{"end_us":58872,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6058,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":58970,"error":"Other","operation":"read","slot":0,"start_us":58958},{"end_us":59000,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58994}],"schema":2,"slots":[{"at_us":59000,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c57056b18d8-f107-52","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":61197,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6413,"slot":0,"token":"18e15c57091953c8-f107-53"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c57091953c8-f107-53","watchdog":false,"workers_joined":1,"writes":[{"at_us":6419,"bytes":66,"kind":"head","slot":0},{"at_us":6424,"bytes":6,"kind":"body","slot":0},{"at_us":61387,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6117,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6149,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6144},{"end_us":6166,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6162,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6199,"operation":"read","slot":0,"start_us":6197},{"end_us":61094,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6212,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":61160,"error":"Other","operation":"read","slot":0,"start_us":61155},{"end_us":61183,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61176}],"schema":2,"slots":[{"at_us":61183,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c57091953c8-f107-53","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":57300,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5735,"slot":0,"token":"18e15c570ce63278-f107-54"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c570ce63278-f107-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":5755,"bytes":66,"kind":"head","slot":0},{"at_us":5761,"bytes":6,"kind":"body","slot":0},{"at_us":57435,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"end_us":5519,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5571,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5566},{"end_us":5597,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5586,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5624,"operation":"read","slot":0,"start_us":5621},{"end_us":57192,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5636,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":57257,"error":"Other","operation":"read","slot":0,"start_us":57253},{"end_us":57283,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57277}],"schema":2,"slots":[{"at_us":57283,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c570ce63278-f107-54","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":56140,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5758,"slot":0,"token":"18e15c5710774490-f107-55"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5710774490-f107-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":5775,"bytes":66,"kind":"head","slot":0},{"at_us":5781,"bytes":6,"kind":"body","slot":0},{"at_us":56300,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"end_us":5494,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5547,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5537},{"end_us":5570,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5563,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5597,"operation":"read","slot":0,"start_us":5595},{"end_us":56016,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5608,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56096,"error":"Other","operation":"read","slot":0,"start_us":56090},{"end_us":56122,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56116}],"schema":2,"slots":[{"at_us":56122,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5710774490-f107-55","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":6231,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6406,"slot":0,"token":"18e15c5713f93790-f107-56"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5713f93790-f107-56","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6180,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6205,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6201},{"end_us":6218,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6218}],"schema":2,"slots":[{"at_us":6218,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5713f93790-f107-56","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":6326,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6419,"slot":0,"token":"18e15c571470e448-f107-57"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c571470e448-f107-57","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":6277,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6300,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6297},{"end_us":6312,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6312}],"schema":2,"slots":[{"at_us":6312,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c571470e448-f107-57","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":6156,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6345,"slot":0,"token":"18e15c5714ebe878-f107-58"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5714ebe878-f107-58","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":46,"operation":"handoff","start_us":46},{"end_us":6111,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6131,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6128},{"end_us":6143,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6143}],"schema":2,"slots":[{"at_us":6143,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5714ebe878-f107-58","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":6339,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6420,"slot":0,"token":"18e15c5715647ba8-f107-59"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5715647ba8-f107-59","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":45,"operation":"handoff","start_us":45},{"end_us":6274,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6302,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6298},{"end_us":6317,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6317}],"schema":2,"slots":[{"at_us":6317,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5715647ba8-f107-59","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":59253,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5396,"slot":0,"token":"18e15c5715df1a48-f107-5a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5715df1a48-f107-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5405,"bytes":57,"kind":"head","slot":0},{"at_us":5413,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5159,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5189,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5185},{"bytes":1,"capacity":1,"end_us":5214,"operation":"read","slot":0,"start_us":5210},{"capacity":1,"end_us":59189,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5233},{"end_us":59239,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59233}],"schema":2,"slots":[{"at_us":59239,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5715df1a48-f107-5a","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":56612,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6002,"slot":0,"token":"18e15c57198b6138-f107-5b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c57198b6138-f107-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6011,"bytes":57,"kind":"head","slot":0},{"at_us":6016,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5787,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5825,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5818},{"end_us":5842,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5837,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5864,"operation":"read","slot":0,"start_us":5862},{"end_us":56515,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5875,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":56576,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56571},{"end_us":56597,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56592}],"schema":2,"slots":[{"at_us":56597,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c57198b6138-f107-5b","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":56509,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6400,"slot":0,"token":"18e15c571d11ccc0-f107-5c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c571d11ccc0-f107-5c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6406,"bytes":57,"kind":"head","slot":0},{"at_us":6411,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6209,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6320,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6311},{"end_us":6368,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6356,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6421,"operation":"read","slot":0,"start_us":6419},{"end_us":56416,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6432,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":56471,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56467},{"end_us":56495,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56486}],"schema":2,"slots":[{"at_us":56495,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c571d11ccc0-f107-5c","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":56089,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5209,"slot":0,"token":"18e15c572093bbd8-f107-5d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c572093bbd8-f107-5d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5222,"bytes":57,"kind":"head","slot":0},{"at_us":5243,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"end_us":4971,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5023,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5017},{"end_us":5044,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5038,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5069,"operation":"read","slot":0,"start_us":5067},{"end_us":55974,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5080,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":56046,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56042},{"end_us":56073,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56067}],"schema":2,"slots":[{"at_us":56073,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c572093bbd8-f107-5d","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"truncated","completed_us":59716,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6430,"slot":0,"token":"18e15c5724128258-f107-5e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5724128258-f107-5e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6447,"bytes":57,"kind":"head","slot":0},{"at_us":6453,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"end_us":6222,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6262,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6257},{"end_us":6279,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6274,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6305,"operation":"read","slot":0,"start_us":6303},{"end_us":59613,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6316,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":59676,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59671},{"end_us":59700,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59693}],"schema":2,"slots":[{"at_us":59700,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5724128258-f107-5e","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":500602,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6406,"slot":0,"token":"18e15c5727c93158-f107-5f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5727c93158-f107-5f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":500512,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500584,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500584}],"schema":2,"slots":[{"at_us":500584,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5727c93158-f107-5f","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":501881,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6406,"slot":0,"token":"18e15c5745bc0c30-f107-60"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5745bc0c30-f107-60","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":499685,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501825,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499787,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501855,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501855}],"schema":2,"slots":[{"at_us":501855,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5745bc0c30-f107-60","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":502155,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6439,"slot":0,"token":"18e15c5763c54d68-f107-61"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5763c54d68-f107-61","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":87,"operation":"handoff","start_us":87},{"end_us":502057,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":108,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502132,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502132}],"schema":2,"slots":[{"at_us":502132,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5763c54d68-f107-61","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"silent-head","completed_us":501651,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5209,"slot":0,"token":"18e15c5781d5fcf8-f107-62"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5781d5fcf8-f107-62","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":87,"operation":"handoff","start_us":87},{"end_us":501518,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501594,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501594}],"schema":2,"slots":[{"at_us":501594,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5781d5fcf8-f107-62","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":500834,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":12568,"slot":0,"token":"18e15c57a01fe4f8-f107-63"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c57a01fe4f8-f107-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":12595,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":451,"operation":"dispatch","outcome":"pending","slot":0,"start_us":255},{"end_us":5063,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1063,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":5124,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5115},{"end_us":500713,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5152,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500793,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500784}],"schema":2,"slots":[{"at_us":500793,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c57a01fe4f8-f107-63","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":500563,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6208,"slot":0,"token":"18e15c57be885178-f107-64"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c57be885178-f107-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":6228,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5993,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6057,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6050},{"end_us":500465,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6075,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500542,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500535}],"schema":2,"slots":[{"at_us":500542,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c57be885178-f107-64","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":502157,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5235,"slot":0,"token":"18e15c57dc8169f8-f107-65"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c57dc8169f8-f107-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":5245,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":85,"operation":"handoff","start_us":85},{"end_us":4947,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4974,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4970},{"end_us":502051,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4988,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":502131,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502123}],"schema":2,"slots":[{"at_us":502131,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c57dc8169f8-f107-65","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"pending-body","completed_us":501181,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6225,"slot":0,"token":"18e15c57fa93f618-f107-66"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c57fa93f618-f107-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":6243,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":80,"operation":"handoff","start_us":80},{"end_us":5976,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6024,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6019},{"end_us":501085,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6039,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501159,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501150}],"schema":2,"slots":[{"at_us":501159,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c57fa93f618-f107-66","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":502001,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5687,"slot":0,"token":"18e15c5818957308-f107-67"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5818957308-f107-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":5698,"bytes":57,"kind":"head","slot":0},{"at_us":5705,"bytes":1,"kind":"body","slot":0},{"at_us":307954,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":5410,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5454,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5449},{"end_us":5473,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5468,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5511,"operation":"read","slot":0,"start_us":5509},{"end_us":307687,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5525,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307770,"operation":"read","slot":0,"start_us":307766},{"end_us":501902,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307783,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501975,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501968}],"schema":2,"slots":[{"at_us":501975,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5818957308-f107-67","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":500667,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5235,"slot":0,"token":"18e15c5836b08aa8-f107-68"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5836b08aa8-f107-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":5249,"bytes":57,"kind":"head","slot":0},{"at_us":5257,"bytes":1,"kind":"body","slot":0},{"at_us":308568,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":4976,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5024,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5019},{"end_us":5044,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5039,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5077,"operation":"read","slot":0,"start_us":5075},{"end_us":308263,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5090,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":308359,"operation":"read","slot":0,"start_us":308353},{"end_us":500564,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308376,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500642,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500633}],"schema":2,"slots":[{"at_us":500642,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5836b08aa8-f107-68","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":500891,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6438,"slot":0,"token":"18e15c5854b22ea8-f107-69"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5854b22ea8-f107-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":6452,"bytes":57,"kind":"head","slot":0},{"at_us":6459,"bytes":1,"kind":"body","slot":0},{"at_us":306543,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"end_us":6139,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":99,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6184,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6179},{"end_us":6206,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6197,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6238,"operation":"read","slot":0,"start_us":6235},{"end_us":306208,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6253,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":306291,"operation":"read","slot":0,"start_us":306288},{"end_us":500782,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306309,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":500865,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500858}],"schema":2,"slots":[{"at_us":500865,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5854b22ea8-f107-69","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"trickle","completed_us":501038,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6429,"slot":0,"token":"18e15c5872bc4e88-f107-6a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5872bc4e88-f107-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6454,"bytes":57,"kind":"head","slot":0},{"at_us":6462,"bytes":1,"kind":"body","slot":0},{"at_us":307724,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":76,"operation":"handoff","start_us":76},{"end_us":6245,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6312,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6306},{"end_us":6339,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6328,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6374,"operation":"read","slot":0,"start_us":6371},{"end_us":307402,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6387,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":307563,"operation":"read","slot":0,"start_us":307556},{"end_us":500923,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307579,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500995,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500986}],"schema":2,"slots":[{"at_us":500995,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5872bc4e88-f107-6a","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":500557,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5649,"slot":1,"token":"18e15c5890c8cbe0-f107-6b"},{"path":"/readiness/siblings/0","received_us":5720,"slot":0,"token":"18e15c5890c8cbe0-f107-6b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c5890c8cbe0-f107-6b","watchdog":false,"workers_joined":2,"writes":[{"at_us":5665,"bytes":57,"kind":"head","slot":1},{"at_us":5727,"bytes":57,"kind":"head","slot":0},{"at_us":10826,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"end_us":173,"operation":"dispatch","outcome":"pending","slot":1,"start_us":131},{"end_us":5059,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":197,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5119,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5112},{"end_us":5166,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5135,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5185,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5184},{"end_us":10068,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5196,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":10137,"operation":"read","slot":1,"start_us":10133},{"end_us":10156,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10152,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10180,"operation":"read","slot":1,"start_us":10178},{"end_us":10192,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10191,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10218,"operation":"read","slot":1,"start_us":10218},{"end_us":10232,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10229,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":1,"end_us":10254,"operation":"read","slot":1,"start_us":10254},{"end_us":10277,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10269},{"end_us":500239,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":10294,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":500436,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500413}],"schema":2,"slots":[{"at_us":500436,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":10277,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5890c8cbe0-f107-6b","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":500890,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5894,"slot":0,"token":"18e15c58aefc1ea0-f107-6c"},{"path":"/readiness/siblings/1","received_us":5941,"slot":1,"token":"18e15c58aefc1ea0-f107-6c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c58aefc1ea0-f107-6c","watchdog":false,"workers_joined":2,"writes":[{"at_us":5913,"bytes":57,"kind":"head","slot":0},{"at_us":5957,"bytes":57,"kind":"head","slot":1},{"at_us":5964,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":161,"operation":"dispatch","outcome":"pending","slot":1,"start_us":111},{"end_us":5562,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":186,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5624,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5615},{"end_us":5646,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5642,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5663,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5662},{"end_us":5677,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5674,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5707,"operation":"read","slot":1,"start_us":5704},{"end_us":5722,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5719,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5744,"operation":"read","slot":1,"start_us":5743},{"end_us":5757,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5753},{"end_us":500781,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5773,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500863,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500844}],"schema":2,"slots":[{"at_us":500863,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5757,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c58aefc1ea0-f107-6c","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":500841,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6460,"slot":1,"token":"18e15c58cd0bbcc0-f107-6d"},{"path":"/readiness/siblings/0","received_us":6470,"slot":0,"token":"18e15c58cd0bbcc0-f107-6d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c58cd0bbcc0-f107-6d","watchdog":false,"workers_joined":2,"writes":[{"at_us":6477,"bytes":57,"kind":"head","slot":1},{"at_us":6486,"bytes":57,"kind":"head","slot":0},{"at_us":12753,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":113,"operation":"dispatch","outcome":"pending","slot":1,"start_us":87},{"body_reads":0,"deadline_us":500000,"end_us":133,"operation":"handoff","start_us":133},{"end_us":6236,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":155,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6272,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6266},{"end_us":6287,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6285,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6301,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6300},{"end_us":12484,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6312,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":12531,"operation":"read","slot":1,"start_us":12527},{"end_us":12547,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12543,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12567,"operation":"read","slot":1,"start_us":12565},{"end_us":12579,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12578,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12598,"operation":"read","slot":1,"start_us":12597},{"end_us":12609,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12606,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":1,"end_us":12630,"operation":"read","slot":1,"start_us":12629},{"end_us":12644,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12638},{"end_us":500749,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12657,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":500824,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500817}],"schema":2,"slots":[{"at_us":500824,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12644,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c58cd0bbcc0-f107-6d","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"siblings","completed_us":501043,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6324,"slot":1,"token":"18e15c58eb28f740-f107-6e"},{"path":"/readiness/siblings/0","received_us":6330,"slot":0,"token":"18e15c58eb28f740-f107-6e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c58eb28f740-f107-6e","watchdog":false,"workers_joined":2,"writes":[{"at_us":6343,"bytes":57,"kind":"head","slot":0},{"at_us":6352,"bytes":57,"kind":"head","slot":1},{"at_us":6361,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":170,"operation":"dispatch","outcome":"pending","slot":1,"start_us":131},{"body_reads":0,"deadline_us":500000,"end_us":194,"operation":"handoff","start_us":194},{"end_us":6082,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":207,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6129,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6124},{"end_us":6148,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6145,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6163,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6162},{"end_us":6177,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6172,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6205,"operation":"read","slot":1,"start_us":6203},{"end_us":6219,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6216,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6240,"operation":"read","slot":1,"start_us":6239},{"end_us":6253,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6250},{"end_us":500960,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6269,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501026,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501019}],"schema":2,"slots":[{"at_us":501026,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6253,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c58eb28f740-f107-6e","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":500562,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6488,"slot":0,"token":"18e15c5909407118-f107-6f"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c5909407118-f107-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6535,"bytes":57,"kind":"head","slot":0},{"at_us":6543,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":6336,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6388,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6382},{"end_us":6410,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6401,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6435,"operation":"read","slot":0,"start_us":6432},{"end_us":500481,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6446,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500543,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500537}],"schema":2,"slots":[{"at_us":500543,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5909407118-f107-6f","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":501530,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6436,"slot":0,"token":"18e15c592745e1c0-f107-70"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c592745e1c0-f107-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":6456,"bytes":57,"kind":"head","slot":0},{"at_us":6464,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":6194,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6230,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6224},{"end_us":6249,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6242,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6275,"operation":"read","slot":0,"start_us":6272},{"end_us":501327,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6286,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501434,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501423}],"schema":2,"slots":[{"at_us":501434,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c592745e1c0-f107-70","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":501129,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5338,"slot":0,"token":"18e15c59455c9460-f107-71"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c59455c9460-f107-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":5357,"bytes":57,"kind":"head","slot":0},{"at_us":5364,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":221,"operation":"handoff","start_us":221},{"end_us":4397,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":236,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4452,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4446},{"end_us":4475,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4467,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4501,"operation":"read","slot":0,"start_us":4498},{"end_us":501043,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4518,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501109,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501102}],"schema":2,"slots":[{"at_us":501109,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c59455c9460-f107-71","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"exhausted","completed_us":501888,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5972,"slot":0,"token":"18e15c59636d6b00-f107-72"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c59636d6b00-f107-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":5990,"bytes":57,"kind":"head","slot":0},{"at_us":5995,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"end_us":5697,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5738,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5730},{"end_us":5756,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5751,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5785,"operation":"read","slot":0,"start_us":5782},{"end_us":501807,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5797,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501869,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501861}],"schema":2,"slots":[{"at_us":501869,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c59636d6b00-f107-72","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5171,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5201,"slot":0,"token":"18e15c59817c9fa8-f107-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c59817c9fa8-f107-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":5208,"bytes":57,"kind":"head","slot":0},{"at_us":5212,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":4997,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5022,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5017},{"end_us":5041,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5034,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5063,"operation":"read","slot":0,"start_us":5061},{"end_us":5075,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5073,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5093,"operation":"read","slot":0,"start_us":5092},{"end_us":5102,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5101,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5121,"operation":"read","slot":0,"start_us":5120},{"end_us":5132,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5130,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5148,"operation":"read","slot":0,"start_us":5147},{"end_us":5158,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5156}],"schema":2,"slots":[{"at_us":5158,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c59817c9fa8-f107-73","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5103,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5168,"slot":0,"token":"18e15c5981efcc08-f107-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5981efcc08-f107-74","watchdog":false,"workers_joined":1,"writes":[{"at_us":5177,"bytes":57,"kind":"head","slot":0},{"at_us":5181,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":4990,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5012,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5010},{"end_us":5026,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5023,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5051,"operation":"read","slot":0,"start_us":5049},{"end_us":5063,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5061,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5083,"operation":"read","slot":0,"start_us":5082},{"end_us":5093,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5091}],"schema":2,"slots":[{"at_us":5093,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5981efcc08-f107-74","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5174,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5228,"slot":0,"token":"18e15c59825f81b0-f107-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c59825f81b0-f107-75","watchdog":false,"workers_joined":1,"writes":[{"at_us":5237,"bytes":57,"kind":"head","slot":0},{"at_us":5242,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"end_us":5003,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5028,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5024},{"end_us":5042,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5038,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5065,"operation":"read","slot":0,"start_us":5064},{"end_us":5077,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5076,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5095,"operation":"read","slot":0,"start_us":5094},{"end_us":5105,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5104,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5122,"operation":"read","slot":0,"start_us":5121},{"end_us":5135,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5133,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5152,"operation":"read","slot":0,"start_us":5151},{"end_us":5162,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5160}],"schema":2,"slots":[{"at_us":5162,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c59825f81b0-f107-75","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"eof","completed_us":5685,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5790,"slot":0,"token":"18e15c5982e01b90-f107-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5982e01b90-f107-76","watchdog":false,"workers_joined":1,"writes":[{"at_us":5796,"bytes":57,"kind":"head","slot":0},{"at_us":5801,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":57,"operation":"handoff","start_us":57},{"end_us":5550,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5582,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5578},{"end_us":5600,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5595,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5625,"operation":"read","slot":0,"start_us":5623},{"end_us":5641,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5639,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5662,"operation":"read","slot":0,"start_us":5661},{"end_us":5673,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5671}],"schema":2,"slots":[{"at_us":5673,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5982e01b90-f107-76","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":5614,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5657,"slot":0,"token":"18e15c598359df70-f107-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c598359df70-f107-77","watchdog":false,"workers_joined":1,"writes":[{"at_us":5671,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":21,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"end_us":5534,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":35,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5558,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5556},{"end_us":5570,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5569,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5589,"operation":"read","slot":0,"start_us":5588},{"end_us":5603,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5601}],"schema":2,"slots":[{"at_us":5603,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c598359df70-f107-77","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":5203,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5281,"slot":0,"token":"18e15c5983cc5438-f107-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5983cc5438-f107-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":5291,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":5098,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5133,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5130},{"end_us":5150,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5147,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5174,"operation":"read","slot":0,"start_us":5173},{"end_us":5187,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5185}],"schema":2,"slots":[{"at_us":5187,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5983cc5438-f107-78","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":5155,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5213,"slot":0,"token":"18e15c598437e748-f107-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c598437e748-f107-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":5227,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":44,"operation":"handoff","start_us":44},{"end_us":5075,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5099,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5096},{"end_us":5111,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5110,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5131,"operation":"read","slot":0,"start_us":5130},{"end_us":5145,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5143}],"schema":2,"slots":[{"at_us":5145,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c598437e748-f107-79","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"empty-eof","completed_us":5060,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5185,"slot":0,"token":"18e15c5984a37288-f107-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5984a37288-f107-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5199,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":55,"operation":"handoff","start_us":55},{"end_us":4951,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4986,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4981},{"end_us":5003,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5000,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":16384,"end_us":5029,"operation":"read","slot":0,"start_us":5027},{"end_us":5042,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5039}],"schema":2,"slots":[{"at_us":5042,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5984a37288-f107-7a","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":6349,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6404,"slot":0,"token":"18e15c59850f9a08-f107-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c59850f9a08-f107-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6414,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":6302,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6322,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6319},{"end_us":6336,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6336}],"schema":2,"slots":[{"at_us":6336,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c59850f9a08-f107-7b","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":5036,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5153,"slot":0,"token":"18e15c598589c378-f107-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c598589c378-f107-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5160,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"end_us":4997,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5013,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5011},{"end_us":5024,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5024}],"schema":2,"slots":[{"at_us":5024,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c598589c378-f107-7c","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":6172,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6261,"slot":0,"token":"18e15c5985f24560-f107-7d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5985f24560-f107-7d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6284,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":25,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":46,"operation":"handoff","start_us":46},{"end_us":6128,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6148,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6145},{"end_us":6160,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6159}],"schema":2,"slots":[{"at_us":6160,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5985f24560-f107-7d","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-head","completed_us":6293,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6410,"slot":0,"token":"18e15c59866bbf08-f107-7e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c59866bbf08-f107-7e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6422,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":23,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":38,"operation":"handoff","start_us":38},{"end_us":6247,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6267,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6264},{"end_us":6280,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6280}],"schema":2,"slots":[{"at_us":6280,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c59866bbf08-f107-7e","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":60925,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6451,"slot":0,"token":"18e15c5986e8a798-f107-7f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5986e8a798-f107-7f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6473,"bytes":66,"kind":"head","slot":0},{"at_us":6477,"bytes":6,"kind":"body","slot":0},{"at_us":61027,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":6258,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6290,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6286},{"end_us":6307,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6302,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6346,"operation":"read","slot":0,"start_us":6343},{"end_us":60817,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6359,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":60887,"error":"Other","operation":"read","slot":0,"start_us":60875},{"end_us":60910,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60904}],"schema":2,"slots":[{"at_us":60910,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5986e8a798-f107-7f","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":56936,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5442,"slot":0,"token":"18e15c598ab01b90-f107-80"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c598ab01b90-f107-80","watchdog":false,"workers_joined":1,"writes":[{"at_us":5450,"bytes":66,"kind":"head","slot":0},{"at_us":5454,"bytes":6,"kind":"body","slot":0},{"at_us":57105,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5146,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5168,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5164},{"end_us":5183,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5179,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5208,"operation":"read","slot":0,"start_us":5206},{"end_us":56837,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5219,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56899,"error":"Other","operation":"read","slot":0,"start_us":56893},{"end_us":56922,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56916}],"schema":2,"slots":[{"at_us":56922,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c598ab01b90-f107-80","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":58635,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5316,"slot":0,"token":"18e15c598e3aec18-f107-81"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c598e3aec18-f107-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":5332,"bytes":66,"kind":"head","slot":0},{"at_us":5337,"bytes":6,"kind":"body","slot":0},{"at_us":58761,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"end_us":5103,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5134,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5129},{"end_us":5152,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5146,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5178,"operation":"read","slot":0,"start_us":5176},{"end_us":58542,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5190,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":58597,"error":"Other","operation":"read","slot":0,"start_us":58592},{"end_us":58621,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58616}],"schema":2,"slots":[{"at_us":58621,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c598e3aec18-f107-81","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"malformed-body","completed_us":60142,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6398,"slot":0,"token":"18e15c5991e1b0e0-f107-82"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5991e1b0e0-f107-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":6406,"bytes":66,"kind":"head","slot":0},{"at_us":6410,"bytes":6,"kind":"body","slot":0},{"at_us":60267,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":57,"operation":"handoff","start_us":57},{"end_us":6154,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6174,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6170},{"end_us":6191,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6187,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6212,"operation":"read","slot":0,"start_us":6211},{"end_us":60045,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6222,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":60104,"error":"Other","operation":"read","slot":0,"start_us":60099},{"end_us":60128,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60122}],"schema":2,"slots":[{"at_us":60128,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5991e1b0e0-f107-82","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":5519,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5708,"slot":0,"token":"18e15c59959d7890-f107-83"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c59959d7890-f107-83","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":5478,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5496,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5492},{"end_us":5507,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5507}],"schema":2,"slots":[{"at_us":5507,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c59959d7890-f107-83","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":6304,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6399,"slot":0,"token":"18e15c59960ae830-f107-84"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c59960ae830-f107-84","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"end_us":6260,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6278,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6276},{"end_us":6290,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6289}],"schema":2,"slots":[{"at_us":6290,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c59960ae830-f107-84","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":5526,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5620,"slot":0,"token":"18e15c5996834898-f107-85"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5996834898-f107-85","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":40,"operation":"handoff","start_us":40},{"end_us":5475,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5499,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5496},{"end_us":5512,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5512}],"schema":2,"slots":[{"at_us":5512,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5996834898-f107-85","tolerance_us":5000}} +{"run":"repaired-current","probe":{"case":"head-error","completed_us":6251,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6388,"slot":0,"token":"18e15c5996f1f888-f107-86"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5996f1f888-f107-86","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"end_us":6200,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6224,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6222},{"end_us":6237,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6237}],"schema":2,"slots":[{"at_us":6237,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5996f1f888-f107-86","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":58252,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":16064,"slot":0,"token":"18e15c5f701d02b0-fca7-0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5f701d02b0-fca7-0","watchdog":false,"workers_joined":1,"writes":[{"at_us":16078,"bytes":57,"kind":"head","slot":0},{"at_us":16087,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":1617,"operation":"dispatch","outcome":"pending","slot":0,"start_us":396},{"end_us":6835,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1995,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":6935,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6882},{"bytes":1,"capacity":1,"end_us":7176,"operation":"read","slot":0,"start_us":6967},{"capacity":1,"end_us":58132,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":7197},{"end_us":58228,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58193}],"schema":2,"slots":[{"at_us":58228,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5f701d02b0-fca7-0","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":58182,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5502,"slot":0,"token":"18e15c5f744abe40-fca7-1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5f744abe40-fca7-1","watchdog":false,"workers_joined":1,"writes":[{"at_us":5516,"bytes":57,"kind":"head","slot":0},{"at_us":5521,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":5254,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5302,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5298},{"end_us":5319,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5315,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5353,"operation":"read","slot":0,"start_us":5352},{"end_us":58082,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5370,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58144,"operation":"read","slot":0,"start_us":58140},{"end_us":58162,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58157}],"schema":2,"slots":[{"at_us":58162,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5f744abe40-fca7-1","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":59353,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6433,"slot":0,"token":"18e15c5f77e5d6c0-fca7-2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5f77e5d6c0-fca7-2","watchdog":false,"workers_joined":1,"writes":[{"at_us":6450,"bytes":57,"kind":"head","slot":0},{"at_us":6456,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":6235,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6266,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6263},{"end_us":6283,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6279,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6310,"operation":"read","slot":0,"start_us":6308},{"end_us":59253,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6329,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":59319,"operation":"read","slot":0,"start_us":59315},{"end_us":59337,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59332}],"schema":2,"slots":[{"at_us":59337,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5f77e5d6c0-fca7-2","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":58906,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6463,"slot":0,"token":"18e15c5f7b91e318-fca7-3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5f7b91e318-fca7-3","watchdog":false,"workers_joined":1,"writes":[{"at_us":6475,"bytes":57,"kind":"head","slot":0},{"at_us":6479,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"end_us":6220,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6258,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6254},{"end_us":6278,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6274,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6301,"operation":"read","slot":0,"start_us":6299},{"end_us":58821,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6319,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":58877,"operation":"read","slot":0,"start_us":58874},{"end_us":58892,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58887}],"schema":2,"slots":[{"at_us":58892,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5f7b91e318-fca7-3","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":59855,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6442,"slot":0,"token":"18e15c5f7f37ffe8-fca7-4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c5f7f37ffe8-fca7-4","watchdog":false,"workers_joined":1,"writes":[{"at_us":6461,"bytes":57,"kind":"head","slot":0},{"at_us":6473,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"end_us":6193,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6227,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6223},{"end_us":6239,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6236,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6268,"operation":"read","slot":0,"start_us":6267},{"end_us":59760,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6291,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":59821,"operation":"read","slot":0,"start_us":59818},{"end_us":59838,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59833}],"schema":2,"slots":[{"at_us":59838,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5f7f37ffe8-fca7-4","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":501395,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5753,"slot":0,"token":"18e15c5f82f38148-fca7-5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5f82f38148-fca7-5","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":501314,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501379,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501379}],"schema":2,"slots":[{"at_us":501379,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5f82f38148-fca7-5","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":501342,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5367,"slot":0,"token":"18e15c5fa0f5a630-fca7-6"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5fa0f5a630-fca7-6","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":501262,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501325,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501325}],"schema":2,"slots":[{"at_us":501325,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5fa0f5a630-fca7-6","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":501395,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5208,"slot":0,"token":"18e15c5fbef35a60-fca7-7"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5fbef35a60-fca7-7","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"end_us":501308,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501374,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501374}],"schema":2,"slots":[{"at_us":501374,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5fbef35a60-fca7-7","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":501371,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5212,"slot":0,"token":"18e15c5fdcf0b4b8-fca7-8"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5fdcf0b4b8-fca7-8","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"end_us":501297,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501353,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501352}],"schema":2,"slots":[{"at_us":501353,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c5fdcf0b4b8-fca7-8","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":500383,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6432,"slot":0,"token":"18e15c5ffaf611d8-fca7-9"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c5ffaf611d8-fca7-9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6442,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6167,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6201,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6198},{"end_us":500293,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6214,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500361,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500354}],"schema":2,"slots":[{"at_us":500361,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c5ffaf611d8-fca7-9","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":502371,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6469,"slot":0,"token":"18e15c6018ea1590-fca7-a"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c6018ea1590-fca7-a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6492,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":6238,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6293,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6288},{"end_us":502274,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6308,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":502343,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502337}],"schema":2,"slots":[{"at_us":502343,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6018ea1590-fca7-a","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":501470,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5187,"slot":0,"token":"18e15c6036fc18f8-fca7-b"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c6036fc18f8-fca7-b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5209,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"end_us":4957,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4997,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4992},{"end_us":501363,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5009,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501442,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501430}],"schema":2,"slots":[{"at_us":501442,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6036fc18f8-fca7-b","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":500354,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5223,"slot":0,"token":"18e15c6054ff4f50-fca7-c"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c6054ff4f50-fca7-c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5245,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"end_us":5009,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5066,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5062},{"end_us":500265,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5081,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500335,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500328}],"schema":2,"slots":[{"at_us":500335,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6054ff4f50-fca7-c","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":501771,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6236,"slot":0,"token":"18e15c6072ef43f8-fca7-d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c6072ef43f8-fca7-d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6252,"bytes":57,"kind":"head","slot":0},{"at_us":6257,"bytes":1,"kind":"body","slot":0},{"at_us":308726,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":6046,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6095,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6088},{"end_us":6114,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6109,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6137,"operation":"read","slot":0,"start_us":6135},{"end_us":308505,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6148,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":308581,"operation":"read","slot":0,"start_us":308577},{"end_us":501673,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308598,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501732,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501726}],"schema":2,"slots":[{"at_us":501732,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6072ef43f8-fca7-d","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":500767,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6492,"slot":0,"token":"18e15c609100af08-fca7-e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c609100af08-fca7-e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6515,"bytes":57,"kind":"head","slot":0},{"at_us":6522,"bytes":1,"kind":"body","slot":0},{"at_us":309903,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":6084,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6138,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6133},{"end_us":6155,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6151,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6184,"operation":"read","slot":0,"start_us":6182},{"end_us":309459,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6199,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309550,"operation":"read","slot":0,"start_us":309547},{"end_us":500685,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309565,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500747,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500741}],"schema":2,"slots":[{"at_us":500747,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c609100af08-fca7-e","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":500862,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6474,"slot":0,"token":"18e15c60af097ef8-fca7-f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c60af097ef8-fca7-f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6488,"bytes":57,"kind":"head","slot":0},{"at_us":6495,"bytes":1,"kind":"body","slot":0},{"at_us":310575,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"end_us":6251,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6295,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6290},{"end_us":6313,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6309,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6340,"operation":"read","slot":0,"start_us":6338},{"end_us":310332,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6354,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":310534,"operation":"read","slot":0,"start_us":310531},{"end_us":500682,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310549,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":500760,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500753}],"schema":2,"slots":[{"at_us":500760,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c60af097ef8-fca7-f","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":501071,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5252,"slot":0,"token":"18e15c60cd0bea30-fca7-10"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c60cd0bea30-fca7-10","watchdog":false,"workers_joined":1,"writes":[{"at_us":5264,"bytes":57,"kind":"head","slot":0},{"at_us":5271,"bytes":1,"kind":"body","slot":0},{"at_us":306057,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":85,"operation":"handoff","start_us":85},{"end_us":4968,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5000,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4996},{"end_us":5016,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5012,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5045,"operation":"read","slot":0,"start_us":5043},{"end_us":305773,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5055,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":305858,"operation":"read","slot":0,"start_us":305855},{"end_us":500996,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":305874,"status":"FastlyStatus::OK","timeout_ms":194},{"end_us":501052,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501046}],"schema":2,"slots":[{"at_us":501052,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c60cd0bea30-fca7-10","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":500530,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5263,"slot":0,"token":"18e15c60eb21a2d0-fca7-11"},{"path":"/readiness/siblings/1","received_us":5270,"slot":1,"token":"18e15c60eb21a2d0-fca7-11"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c60eb21a2d0-fca7-11","watchdog":false,"workers_joined":2,"writes":[{"at_us":5272,"bytes":57,"kind":"head","slot":0},{"at_us":5276,"bytes":57,"kind":"head","slot":1},{"at_us":5289,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":109,"operation":"dispatch","outcome":"pending","slot":1,"start_us":90},{"end_us":5050,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":129,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5076,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5072},{"end_us":5091,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5087,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5104,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5103},{"end_us":5116,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5113,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5136,"operation":"read","slot":1,"start_us":5134},{"end_us":5148,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5147,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5176,"operation":"read","slot":1,"start_us":5175},{"end_us":5186,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5185,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5203,"operation":"read","slot":1,"start_us":5202},{"end_us":5213,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5211,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5231,"operation":"read","slot":1,"start_us":5231},{"end_us":5243,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5241},{"end_us":500427,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5254,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500513,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500501}],"schema":2,"slots":[{"at_us":500513,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5243,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c60eb21a2d0-fca7-11","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":500440,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6250,"slot":0,"token":"18e15c61093484e0-fca7-12"},{"path":"/readiness/siblings/1","received_us":6295,"slot":1,"token":"18e15c61093484e0-fca7-12"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e15c61093484e0-fca7-12","watchdog":false,"workers_joined":2,"writes":[{"at_us":6273,"bytes":57,"kind":"head","slot":0},{"at_us":6303,"bytes":57,"kind":"head","slot":1},{"at_us":6311,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":141,"operation":"dispatch","outcome":"pending","slot":1,"start_us":115},{"end_us":5912,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":164,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5962,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5958},{"end_us":5982,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5978,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":6000,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5999},{"end_us":6014,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6010,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6039,"operation":"read","slot":1,"start_us":6037},{"end_us":6053,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6050,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6077,"operation":"read","slot":1,"start_us":6076},{"end_us":6089,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6086},{"end_us":500330,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6665,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500404,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500397}],"schema":2,"slots":[{"at_us":500404,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6089,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61093484e0-fca7-12","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":500484,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5233,"slot":1,"token":"18e15c6127377cb8-fca7-13"},{"path":"/readiness/siblings/0","received_us":5249,"slot":0,"token":"18e15c6127377cb8-fca7-13"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c6127377cb8-fca7-13","watchdog":false,"workers_joined":2,"writes":[{"at_us":5259,"bytes":57,"kind":"head","slot":1},{"at_us":5268,"bytes":57,"kind":"head","slot":0},{"at_us":10313,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":115,"operation":"dispatch","outcome":"pending","slot":1,"start_us":96},{"body_reads":0,"deadline_us":500000,"end_us":130,"operation":"handoff","start_us":130},{"end_us":5002,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":153,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5048,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5041},{"end_us":5066,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5064,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5094,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5093},{"end_us":10034,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5106,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":10085,"operation":"read","slot":1,"start_us":10079},{"end_us":10101,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10099,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10123,"operation":"read","slot":1,"start_us":10122},{"end_us":10135,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10134,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10158,"operation":"read","slot":1,"start_us":10158},{"end_us":10169,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10167,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":1,"end_us":10188,"operation":"read","slot":1,"start_us":10187},{"end_us":10197,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10195},{"end_us":500396,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":10208,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":500466,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500460}],"schema":2,"slots":[{"at_us":500466,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":10197,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6127377cb8-fca7-13","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":500510,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6467,"slot":0,"token":"18e15c614540a680-fca7-14"},{"path":"/readiness/siblings/1","received_us":6475,"slot":1,"token":"18e15c614540a680-fca7-14"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c614540a680-fca7-14","watchdog":false,"workers_joined":2,"writes":[{"at_us":6483,"bytes":57,"kind":"head","slot":0},{"at_us":6491,"bytes":57,"kind":"head","slot":1},{"at_us":6499,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":98,"operation":"dispatch","outcome":"pending","slot":1,"start_us":75},{"body_reads":0,"deadline_us":500000,"end_us":124,"operation":"handoff","start_us":124},{"end_us":6243,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":139,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6274,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6271},{"end_us":6290,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6289,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6304,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6303},{"end_us":6317,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6313,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6341,"operation":"read","slot":1,"start_us":6339},{"end_us":6355,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6353,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6375,"operation":"read","slot":1,"start_us":6375},{"end_us":6386,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6384},{"end_us":500428,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6396,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500490,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500483}],"schema":2,"slots":[{"at_us":500490,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6386,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c614540a680-fca7-14","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":500235,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5923,"slot":0,"token":"18e15c61634582b8-fca7-15"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c61634582b8-fca7-15","watchdog":false,"workers_joined":1,"writes":[{"at_us":5945,"bytes":57,"kind":"head","slot":0},{"at_us":5965,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":5710,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5750,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5745},{"end_us":5768,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5762,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5793,"operation":"read","slot":0,"start_us":5788},{"end_us":500153,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5805,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500217,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500208}],"schema":2,"slots":[{"at_us":500217,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61634582b8-fca7-15","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":500734,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5640,"slot":0,"token":"18e15c618139ccc0-fca7-16"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c618139ccc0-fca7-16","watchdog":false,"workers_joined":1,"writes":[{"at_us":5660,"bytes":57,"kind":"head","slot":0},{"at_us":5669,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":5360,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5409,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5406},{"end_us":5431,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5423,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5462,"operation":"read","slot":0,"start_us":5460},{"end_us":500585,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5473,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500694,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500675}],"schema":2,"slots":[{"at_us":500694,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c618139ccc0-fca7-16","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":500742,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6417,"slot":0,"token":"18e15c619f339120-fca7-17"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c619f339120-fca7-17","watchdog":false,"workers_joined":1,"writes":[{"at_us":6435,"bytes":57,"kind":"head","slot":0},{"at_us":6440,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":104,"operation":"handoff","start_us":104},{"end_us":6085,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":127,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6126,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6121},{"end_us":6146,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6138,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6187,"operation":"read","slot":0,"start_us":6184},{"end_us":500656,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6201,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500720,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500711}],"schema":2,"slots":[{"at_us":500720,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c619f339120-fca7-17","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":501152,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5224,"slot":0,"token":"18e15c61bd331628-fca7-18"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c61bd331628-fca7-18","watchdog":false,"workers_joined":1,"writes":[{"at_us":5231,"bytes":57,"kind":"head","slot":0},{"at_us":5236,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"end_us":4940,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4967,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4963},{"end_us":4984,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4980,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5011,"operation":"read","slot":0,"start_us":5009},{"end_us":501077,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5022,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501135,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501129}],"schema":2,"slots":[{"at_us":501135,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61bd331628-fca7-18","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":5158,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5192,"slot":0,"token":"18e15c61db35f690-fca7-19"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61db35f690-fca7-19","watchdog":false,"workers_joined":1,"writes":[{"at_us":5207,"bytes":57,"kind":"head","slot":0},{"at_us":5212,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":4973,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5004,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5000},{"end_us":5020,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5015,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5041,"operation":"read","slot":0,"start_us":5039},{"end_us":5055,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5052,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5079,"operation":"read","slot":0,"start_us":5073},{"end_us":5089,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5089,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5106,"operation":"read","slot":0,"start_us":5106},{"end_us":5119,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5115,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5137,"operation":"read","slot":0,"start_us":5137},{"end_us":5147,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5145}],"schema":2,"slots":[{"at_us":5147,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61db35f690-fca7-19","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":5574,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5602,"slot":0,"token":"18e15c61dbabe5f8-fca7-1a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61dbabe5f8-fca7-1a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5619,"bytes":57,"kind":"head","slot":0},{"at_us":5624,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"end_us":5434,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5475,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5468},{"end_us":5492,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5487,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5516,"operation":"read","slot":0,"start_us":5514},{"end_us":5530,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5528,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5551,"operation":"read","slot":0,"start_us":5550},{"end_us":5563,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5559}],"schema":2,"slots":[{"at_us":5563,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61dbabe5f8-fca7-1a","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":6444,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6415,"slot":0,"token":"18e15c61dc22f670-fca7-1b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61dc22f670-fca7-1b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6429,"bytes":57,"kind":"head","slot":0},{"at_us":6433,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"end_us":6256,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6297,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6293},{"end_us":6314,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6310,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6336,"operation":"read","slot":0,"start_us":6334},{"end_us":6350,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6349,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6367,"operation":"read","slot":0,"start_us":6366},{"end_us":6377,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6377,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6394,"operation":"read","slot":0,"start_us":6393},{"end_us":6404,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6402,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6422,"operation":"read","slot":0,"start_us":6422},{"end_us":6433,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6430}],"schema":2,"slots":[{"at_us":6433,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61dc22f670-fca7-1b","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":6323,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6414,"slot":0,"token":"18e15c61dcaa1c18-fca7-1c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61dcaa1c18-fca7-1c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6425,"bytes":57,"kind":"head","slot":0},{"at_us":6431,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":42,"operation":"handoff","start_us":42},{"end_us":6215,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6238,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6236},{"end_us":6251,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6248,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6274,"operation":"read","slot":0,"start_us":6274},{"end_us":6285,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6284,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6304,"operation":"read","slot":0,"start_us":6303},{"end_us":6313,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6312}],"schema":2,"slots":[{"at_us":6313,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61dcaa1c18-fca7-1c","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":6261,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6400,"slot":0,"token":"18e15c61dd2cd8d8-fca7-1d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61dd2cd8d8-fca7-1d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6407,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":25,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"end_us":6196,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":39,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6215,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6213},{"end_us":6225,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6224,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6242,"operation":"read","slot":0,"start_us":6241},{"end_us":6251,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6251}],"schema":2,"slots":[{"at_us":6251,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61dd2cd8d8-fca7-1d","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":6384,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6408,"slot":0,"token":"18e15c61dda51de8-fca7-1e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61dda51de8-fca7-1e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6423,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":19,"operation":"dispatch","outcome":"pending","slot":0,"start_us":7},{"end_us":6303,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6329,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6326},{"end_us":6342,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6340,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6364,"operation":"read","slot":0,"start_us":6362},{"end_us":6374,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6372}],"schema":2,"slots":[{"at_us":6374,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61dda51de8-fca7-1e","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":5182,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5229,"slot":0,"token":"18e15c61de22c9c8-fca7-1f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61de22c9c8-fca7-1f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5247,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":25,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"end_us":5089,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5122,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5119},{"end_us":5135,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5133,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5155,"operation":"read","slot":0,"start_us":5154},{"end_us":5167,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5164}],"schema":2,"slots":[{"at_us":5167,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61de22c9c8-fca7-1f","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":5123,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5263,"slot":0,"token":"18e15c61de91f6b8-fca7-20"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61de91f6b8-fca7-20","watchdog":false,"workers_joined":1,"writes":[{"at_us":5279,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"end_us":5024,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5059,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5054},{"end_us":5072,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5070,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5097,"operation":"read","slot":0,"start_us":5095},{"end_us":5111,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5108}],"schema":2,"slots":[{"at_us":5111,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61de91f6b8-fca7-20","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":6373,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6460,"slot":0,"token":"18e15c61df00c9d0-fca7-21"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61df00c9d0-fca7-21","watchdog":false,"workers_joined":1,"writes":[{"at_us":6476,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"end_us":6324,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6351,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6346},{"end_us":6361,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6361}],"schema":2,"slots":[{"at_us":6361,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c61df00c9d0-fca7-21","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":5065,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5217,"slot":0,"token":"18e15c61df7dc5e8-fca7-22"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61df7dc5e8-fca7-22","watchdog":false,"workers_joined":1,"writes":[{"at_us":5224,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5021,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5038,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5036},{"end_us":5052,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5052}],"schema":2,"slots":[{"at_us":5052,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c61df7dc5e8-fca7-22","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":5317,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5376,"slot":0,"token":"18e15c61dfe5f9b0-fca7-23"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61dfe5f9b0-fca7-23","watchdog":false,"workers_joined":1,"writes":[{"at_us":5387,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":23,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"body_reads":0,"deadline_us":500000,"end_us":37,"operation":"handoff","start_us":37},{"end_us":5275,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5294,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5292},{"end_us":5305,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5305}],"schema":2,"slots":[{"at_us":5305,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c61dfe5f9b0-fca7-23","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":5487,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5583,"slot":0,"token":"18e15c61e0538c78-fca7-24"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61e0538c78-fca7-24","watchdog":false,"workers_joined":1,"writes":[{"at_us":5596,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":42,"operation":"handoff","start_us":42},{"end_us":5436,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5460,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5457},{"end_us":5473,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5473}],"schema":2,"slots":[{"at_us":5473,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c61e0538c78-fca7-24","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":59536,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6405,"slot":0,"token":"18e15c61e0c40570-fca7-25"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61e0c40570-fca7-25","watchdog":false,"workers_joined":1,"writes":[{"at_us":6421,"bytes":66,"kind":"head","slot":0},{"at_us":6425,"bytes":6,"kind":"body","slot":0},{"at_us":59605,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"end_us":6240,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6266,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6263},{"end_us":6281,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6277,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6303,"operation":"read","slot":0,"start_us":6302},{"end_us":59453,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6314,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":59506,"operation":"read","slot":0,"start_us":59502},{"end_us":59520,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59516}],"schema":2,"slots":[{"at_us":59520,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61e0c40570-fca7-25","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":58663,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5490,"slot":0,"token":"18e15c61e479aeb8-fca7-26"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61e479aeb8-fca7-26","watchdog":false,"workers_joined":1,"writes":[{"at_us":5505,"bytes":66,"kind":"head","slot":0},{"at_us":5511,"bytes":6,"kind":"body","slot":0},{"at_us":58786,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":5276,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5314,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5310},{"end_us":5332,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5328,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5357,"operation":"read","slot":0,"start_us":5356},{"end_us":58577,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5369,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":58631,"operation":"read","slot":0,"start_us":58628},{"end_us":58649,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58645}],"schema":2,"slots":[{"at_us":58649,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61e479aeb8-fca7-26","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":55561,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5223,"slot":0,"token":"18e15c61e81f90f0-fca7-27"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61e81f90f0-fca7-27","watchdog":false,"workers_joined":1,"writes":[{"at_us":5234,"bytes":66,"kind":"head","slot":0},{"at_us":5240,"bytes":6,"kind":"body","slot":0},{"at_us":55922,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":585,"operation":"handoff","start_us":585},{"end_us":4760,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":598,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4799,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4794},{"end_us":4816,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4812,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4842,"operation":"read","slot":0,"start_us":4838},{"end_us":55455,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4854,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":1,"end_us":55528,"operation":"read","slot":0,"start_us":55525},{"end_us":55544,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55541}],"schema":2,"slots":[{"at_us":55544,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61e81f90f0-fca7-27","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":61533,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6453,"slot":0,"token":"18e15c61eb990810-fca7-28"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61eb990810-fca7-28","watchdog":false,"workers_joined":1,"writes":[{"at_us":6467,"bytes":66,"kind":"head","slot":0},{"at_us":6473,"bytes":6,"kind":"body","slot":0},{"at_us":61517,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"end_us":6258,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6294,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6290},{"end_us":6310,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6306,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6335,"operation":"read","slot":0,"start_us":6334},{"end_us":61317,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6346,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":61486,"operation":"read","slot":0,"start_us":61481},{"end_us":61505,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":61501}],"schema":2,"slots":[{"at_us":61505,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61eb990810-fca7-28","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":5073,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5259,"slot":0,"token":"18e15c61ef7f5a60-fca7-29"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61ef7f5a60-fca7-29","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":5015,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5044,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5040},{"end_us":5059,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5059}],"schema":2,"slots":[{"at_us":5059,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c61ef7f5a60-fca7-29","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":5182,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5289,"slot":0,"token":"18e15c61efe87c70-fca7-2a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61efe87c70-fca7-2a","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":5120,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5154,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5149},{"end_us":5167,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5167}],"schema":2,"slots":[{"at_us":5167,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c61efe87c70-fca7-2a","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":5155,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5279,"slot":0,"token":"18e15c61f0709830-fca7-2b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61f0709830-fca7-2b","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":88,"operation":"handoff","start_us":88},{"end_us":4966,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5095,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5089},{"end_us":5135,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5135}],"schema":2,"slots":[{"at_us":5135,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c61f0709830-fca7-2b","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":5659,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5776,"slot":0,"token":"18e15c61f0e21ac8-fca7-2c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61f0e21ac8-fca7-2c","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":100,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"body_reads":0,"deadline_us":500000,"end_us":149,"operation":"handoff","start_us":149},{"end_us":5573,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":167,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5611,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5604},{"end_us":5626,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5626}],"schema":2,"slots":[{"at_us":5626,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c61f0e21ac8-fca7-2c","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":59620,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6461,"slot":0,"token":"18e15c61f17b9f90-fca7-2d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61f17b9f90-fca7-2d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6479,"bytes":57,"kind":"head","slot":0},{"at_us":6486,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":6210,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6266,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6262},{"bytes":1,"capacity":1,"end_us":6300,"operation":"read","slot":0,"start_us":6296},{"capacity":1,"end_us":59528,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":6319},{"end_us":59601,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59594}],"schema":2,"slots":[{"at_us":59601,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61f17b9f90-fca7-2d","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":57727,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6432,"slot":0,"token":"18e15c61f52fdd90-fca7-2e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61f52fdd90-fca7-2e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6447,"bytes":57,"kind":"head","slot":0},{"at_us":6457,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":6243,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6279,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6276},{"end_us":6301,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6297,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6324,"operation":"read","slot":0,"start_us":6322},{"end_us":57618,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6335,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":57694,"operation":"read","slot":0,"start_us":57690},{"end_us":57711,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57707}],"schema":2,"slots":[{"at_us":57711,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61f52fdd90-fca7-2e","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":65063,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6452,"slot":0,"token":"18e15c61f8c7fc58-fca7-2f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61f8c7fc58-fca7-2f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6469,"bytes":57,"kind":"head","slot":0},{"at_us":6474,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":6253,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6287,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6283},{"end_us":6337,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6328,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6378,"operation":"read","slot":0,"start_us":6375},{"end_us":64920,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6391,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":65024,"operation":"read","slot":0,"start_us":65019},{"end_us":65044,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":65039}],"schema":2,"slots":[{"at_us":65044,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61f8c7fc58-fca7-2f","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":58840,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6589,"slot":0,"token":"18e15c61fd2c6bd0-fca7-30"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c61fd2c6bd0-fca7-30","watchdog":false,"workers_joined":1,"writes":[{"at_us":6611,"bytes":57,"kind":"head","slot":0},{"at_us":6620,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":105,"operation":"handoff","start_us":105},{"end_us":6133,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":232,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6341,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6328},{"end_us":6389,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6376,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6454,"operation":"read","slot":0,"start_us":6451},{"end_us":58732,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6479,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":58802,"operation":"read","slot":0,"start_us":58798},{"end_us":58820,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58817}],"schema":2,"slots":[{"at_us":58820,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c61fd2c6bd0-fca7-30","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":55488,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5532,"slot":0,"token":"18e15c6200e224b8-fca7-31"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6200e224b8-fca7-31","watchdog":false,"workers_joined":1,"writes":[{"at_us":5542,"bytes":57,"kind":"head","slot":0},{"at_us":5547,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"end_us":5326,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5368,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5364},{"end_us":5390,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5386,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5419,"operation":"read","slot":0,"start_us":5417},{"end_us":55385,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5431,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":55451,"operation":"read","slot":0,"start_us":55448},{"end_us":55471,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55468}],"schema":2,"slots":[{"at_us":55471,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6200e224b8-fca7-31","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":501463,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5420,"slot":0,"token":"18e15c62045e3bb8-fca7-32"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c62045e3bb8-fca7-32","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":501333,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501443,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501443}],"schema":2,"slots":[{"at_us":501443,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c62045e3bb8-fca7-32","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":500854,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5567,"slot":0,"token":"18e15c62226adc38-fca7-33"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c62226adc38-fca7-33","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":500773,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":129,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500839,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500839}],"schema":2,"slots":[{"at_us":500839,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c62226adc38-fca7-33","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":500395,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5554,"slot":0,"token":"18e15c6240769640-fca7-34"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c6240769640-fca7-34","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":89,"operation":"handoff","start_us":89},{"end_us":500320,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":113,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500378,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500378}],"schema":2,"slots":[{"at_us":500378,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c6240769640-fca7-34","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":500791,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5206,"slot":0,"token":"18e15c625e690b88-fca7-35"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c625e690b88-fca7-35","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"end_us":500716,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500773,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500773}],"schema":2,"slots":[{"at_us":500773,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c625e690b88-fca7-35","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":501738,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5775,"slot":0,"token":"18e15c627c6ea728-fca7-36"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c627c6ea728-fca7-36","watchdog":false,"workers_joined":1,"writes":[{"at_us":5792,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":5569,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5603,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5599},{"end_us":501657,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5616,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501719,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501714}],"schema":2,"slots":[{"at_us":501719,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c627c6ea728-fca7-36","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":500517,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6317,"slot":0,"token":"18e15c629a83ea98-fca7-37"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c629a83ea98-fca7-37","watchdog":false,"workers_joined":1,"writes":[{"at_us":6326,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":6065,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6096,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6093},{"end_us":500397,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6109,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500486,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500473}],"schema":2,"slots":[{"at_us":500486,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c629a83ea98-fca7-37","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":500645,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6445,"slot":0,"token":"18e15c62b8849880-fca7-38"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c62b8849880-fca7-38","watchdog":false,"workers_joined":1,"writes":[{"at_us":6464,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"end_us":6110,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6148,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6144},{"end_us":500547,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6161,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500621,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500614}],"schema":2,"slots":[{"at_us":500621,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c62b8849880-fca7-38","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":501644,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5408,"slot":0,"token":"18e15c62d68605d0-fca7-39"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c62d68605d0-fca7-39","watchdog":false,"workers_joined":1,"writes":[{"at_us":5427,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"end_us":5186,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5238,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5233},{"end_us":501537,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5255,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501616,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501603}],"schema":2,"slots":[{"at_us":501616,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c62d68605d0-fca7-39","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":500856,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6470,"slot":0,"token":"18e15c62f49cb488-fca7-3a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c62f49cb488-fca7-3a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6487,"bytes":57,"kind":"head","slot":0},{"at_us":6498,"bytes":1,"kind":"body","slot":0},{"at_us":306548,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":6261,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6310,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6306},{"end_us":6362,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6349,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6386,"operation":"read","slot":0,"start_us":6384},{"end_us":306306,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6399,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":306379,"operation":"read","slot":0,"start_us":306376},{"end_us":500766,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306393,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":500833,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500828}],"schema":2,"slots":[{"at_us":500833,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c62f49cb488-fca7-3a","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":501139,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6519,"slot":0,"token":"18e15c6312a45b98-fca7-3b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c6312a45b98-fca7-3b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6558,"bytes":57,"kind":"head","slot":0},{"at_us":6578,"bytes":1,"kind":"body","slot":0},{"at_us":309337,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6283,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6331,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6326},{"end_us":6349,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6343,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6383,"operation":"read","slot":0,"start_us":6380},{"end_us":309028,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6397,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309098,"operation":"read","slot":0,"start_us":309095},{"end_us":500981,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309111,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501104,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501098}],"schema":2,"slots":[{"at_us":501104,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6312a45b98-fca7-3b","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":500914,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5218,"slot":0,"token":"18e15c6330c08890-fca7-3c"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c6330c08890-fca7-3c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5237,"bytes":57,"kind":"head","slot":0},{"at_us":5244,"bytes":1,"kind":"body","slot":0},{"at_us":307411,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":80,"operation":"handoff","start_us":80},{"end_us":4977,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":101,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5027,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5023},{"end_us":5051,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5042,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5078,"operation":"read","slot":0,"start_us":5076},{"end_us":307158,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5090,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307235,"operation":"read","slot":0,"start_us":307231},{"end_us":500820,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307250,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500894,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500886}],"schema":2,"slots":[{"at_us":500894,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6330c08890-fca7-3c","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":502179,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6465,"slot":0,"token":"18e15c634ed5cc00-fca7-3d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c634ed5cc00-fca7-3d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6475,"bytes":57,"kind":"head","slot":0},{"at_us":6483,"bytes":1,"kind":"body","slot":0},{"at_us":311106,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":92,"operation":"handoff","start_us":92},{"end_us":6162,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":118,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6213,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6208},{"end_us":6246,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6236,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6281,"operation":"read","slot":0,"start_us":6278},{"end_us":310766,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6293,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":310881,"operation":"read","slot":0,"start_us":310878},{"end_us":502046,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310896,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":502151,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502131}],"schema":2,"slots":[{"at_us":502151,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c634ed5cc00-fca7-3d","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":500866,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6124,"slot":0,"token":"18e15c636cf36828-fca7-3e"},{"path":"/readiness/siblings/1","received_us":6131,"slot":1,"token":"18e15c636cf36828-fca7-3e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c636cf36828-fca7-3e","watchdog":false,"workers_joined":2,"writes":[{"at_us":6142,"bytes":57,"kind":"head","slot":1},{"at_us":6150,"bytes":57,"kind":"head","slot":0},{"at_us":12420,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":122,"operation":"dispatch","outcome":"pending","slot":1,"start_us":108},{"end_us":5791,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":138,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5846,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5842},{"end_us":5863,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5860,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5878,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5877},{"end_us":12058,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5888,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":12097,"operation":"read","slot":1,"start_us":12095},{"end_us":12111,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12110,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12138,"operation":"read","slot":1,"start_us":12130},{"end_us":12150,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12149,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12171,"operation":"read","slot":1,"start_us":12170},{"end_us":12182,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12180,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":1,"end_us":12199,"operation":"read","slot":1,"start_us":12199},{"end_us":12214,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12211},{"end_us":500777,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12228,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":500848,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500842}],"schema":2,"slots":[{"at_us":500848,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12214,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c636cf36828-fca7-3e","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":501042,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6484,"slot":0,"token":"18e15c638b071d28-fca7-3f"},{"path":"/readiness/siblings/1","received_us":6490,"slot":1,"token":"18e15c638b071d28-fca7-3f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e15c638b071d28-fca7-3f","watchdog":false,"workers_joined":2,"writes":[{"at_us":6499,"bytes":57,"kind":"head","slot":0},{"at_us":6509,"bytes":57,"kind":"head","slot":1},{"at_us":6521,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":115,"operation":"dispatch","outcome":"pending","slot":1,"start_us":90},{"end_us":6171,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":137,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6218,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6213},{"end_us":6235,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6233,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6249,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6249},{"end_us":6263,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6259,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6292,"operation":"read","slot":1,"start_us":6290},{"end_us":6306,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6304,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6327,"operation":"read","slot":1,"start_us":6327},{"end_us":6341,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6338},{"end_us":500958,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6353,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501023,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501017}],"schema":2,"slots":[{"at_us":501023,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6341,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c638b071d28-fca7-3f","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":501037,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5780,"slot":1,"token":"18e15c63a91c0e90-fca7-40"},{"path":"/readiness/siblings/0","received_us":5786,"slot":0,"token":"18e15c63a91c0e90-fca7-40"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c63a91c0e90-fca7-40","watchdog":false,"workers_joined":2,"writes":[{"at_us":5797,"bytes":57,"kind":"head","slot":1},{"at_us":5804,"bytes":57,"kind":"head","slot":0},{"at_us":11130,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":110,"operation":"dispatch","outcome":"pending","slot":1,"start_us":96},{"body_reads":0,"deadline_us":500000,"end_us":126,"operation":"handoff","start_us":126},{"end_us":5543,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":138,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5588,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5585},{"end_us":5605,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5602,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5618,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5617},{"end_us":10863,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5627,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":10932,"operation":"read","slot":1,"start_us":10929},{"end_us":10953,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10950,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10976,"operation":"read","slot":1,"start_us":10975},{"end_us":10988,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10986,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":11009,"operation":"read","slot":1,"start_us":11008},{"end_us":11022,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11019,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11048,"operation":"read","slot":1,"start_us":11047},{"end_us":11061,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11056},{"end_us":500900,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11078,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500976,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500969}],"schema":2,"slots":[{"at_us":500976,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11061,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c63a91c0e90-fca7-40","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":500976,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5852,"slot":1,"token":"18e15c63c738cff8-fca7-41"},{"path":"/readiness/siblings/0","received_us":5859,"slot":0,"token":"18e15c63c738cff8-fca7-41"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c63c738cff8-fca7-41","watchdog":false,"workers_joined":2,"writes":[{"at_us":5871,"bytes":57,"kind":"head","slot":0},{"at_us":5882,"bytes":57,"kind":"head","slot":1},{"at_us":5894,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":123,"operation":"dispatch","outcome":"pending","slot":1,"start_us":101},{"body_reads":0,"deadline_us":500000,"end_us":155,"operation":"handoff","start_us":155},{"end_us":5525,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":185,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5583,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5576},{"end_us":5600,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5598,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5615,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5614},{"end_us":5628,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5625,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5657,"operation":"read","slot":1,"start_us":5655},{"end_us":5671,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5669,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5695,"operation":"read","slot":1,"start_us":5694},{"end_us":5707,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5704},{"end_us":500890,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5720,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500959,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500953}],"schema":2,"slots":[{"at_us":500959,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5707,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c63c738cff8-fca7-41","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":501458,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6463,"slot":0,"token":"18e15c63e549e900-fca7-42"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c63e549e900-fca7-42","watchdog":false,"workers_joined":1,"writes":[{"at_us":6481,"bytes":57,"kind":"head","slot":0},{"at_us":6486,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":6192,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6234,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6230},{"end_us":6251,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6246,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6274,"operation":"read","slot":0,"start_us":6271},{"end_us":501349,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6288,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501417,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501411}],"schema":2,"slots":[{"at_us":501417,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c63e549e900-fca7-42","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":500397,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5241,"slot":0,"token":"18e15c6403542050-fca7-43"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c6403542050-fca7-43","watchdog":false,"workers_joined":1,"writes":[{"at_us":5258,"bytes":57,"kind":"head","slot":0},{"at_us":5269,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":5002,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5058,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5046},{"end_us":5109,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5103,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5140,"operation":"read","slot":0,"start_us":5137},{"end_us":500307,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5153,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500378,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500372}],"schema":2,"slots":[{"at_us":500378,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6403542050-fca7-43","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":501299,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5296,"slot":0,"token":"18e15c6421501348-fca7-44"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c6421501348-fca7-44","watchdog":false,"workers_joined":1,"writes":[{"at_us":5316,"bytes":57,"kind":"head","slot":0},{"at_us":5323,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"end_us":5041,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5097,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5092},{"end_us":5118,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5111,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5149,"operation":"read","slot":0,"start_us":5146},{"end_us":501205,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5161,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501278,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501272}],"schema":2,"slots":[{"at_us":501278,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6421501348-fca7-44","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":500386,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6470,"slot":0,"token":"18e15c643f5b40b0-fca7-45"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c643f5b40b0-fca7-45","watchdog":false,"workers_joined":1,"writes":[{"at_us":6492,"bytes":57,"kind":"head","slot":0},{"at_us":6497,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"end_us":6224,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6280,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6276},{"end_us":6300,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6296,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6328,"operation":"read","slot":0,"start_us":6326},{"end_us":500229,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6344,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500361,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500349}],"schema":2,"slots":[{"at_us":500361,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c643f5b40b0-fca7-45","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":5617,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5617,"slot":0,"token":"18e15c645d79f230-fca7-46"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c645d79f230-fca7-46","watchdog":false,"workers_joined":1,"writes":[{"at_us":5629,"bytes":57,"kind":"head","slot":0},{"at_us":5634,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5403,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5444,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5438},{"end_us":5463,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5458,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5487,"operation":"read","slot":0,"start_us":5485},{"end_us":5501,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5500,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5529,"operation":"read","slot":0,"start_us":5523},{"end_us":5542,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5541,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5561,"operation":"read","slot":0,"start_us":5561},{"end_us":5575,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5572,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5593,"operation":"read","slot":0,"start_us":5593},{"end_us":5605,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5602}],"schema":2,"slots":[{"at_us":5605,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c645d79f230-fca7-46","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":5396,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5359,"slot":0,"token":"18e15c645e0044e8-fca7-47"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c645e0044e8-fca7-47","watchdog":false,"workers_joined":1,"writes":[{"at_us":5379,"bytes":57,"kind":"head","slot":0},{"at_us":5384,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"end_us":5208,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5260,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5256},{"end_us":5305,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5300,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5336,"operation":"read","slot":0,"start_us":5335},{"end_us":5351,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5349,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5372,"operation":"read","slot":0,"start_us":5372},{"end_us":5384,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5381}],"schema":2,"slots":[{"at_us":5384,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c645e0044e8-fca7-47","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":5195,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5211,"slot":0,"token":"18e15c645e968d90-fca7-48"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c645e968d90-fca7-48","watchdog":false,"workers_joined":1,"writes":[{"at_us":5220,"bytes":57,"kind":"head","slot":0},{"at_us":5226,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"end_us":5021,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5051,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5046},{"end_us":5066,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5062,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5087,"operation":"read","slot":0,"start_us":5085},{"end_us":5098,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5097,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5116,"operation":"read","slot":0,"start_us":5115},{"end_us":5128,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5127,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5144,"operation":"read","slot":0,"start_us":5144},{"end_us":5155,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5153,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5171,"operation":"read","slot":0,"start_us":5171},{"end_us":5184,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5180}],"schema":2,"slots":[{"at_us":5184,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c645e968d90-fca7-48","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":6375,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6459,"slot":0,"token":"18e15c64609caa48-fca7-49"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c64609caa48-fca7-49","watchdog":false,"workers_joined":1,"writes":[{"at_us":6473,"bytes":57,"kind":"head","slot":0},{"at_us":6479,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":435,"operation":"handoff","start_us":435},{"end_us":6180,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":450,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6220,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6216},{"end_us":6238,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6233,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6268,"operation":"read","slot":0,"start_us":6266},{"end_us":6315,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6312,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6347,"operation":"read","slot":0,"start_us":6346},{"end_us":6360,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6357}],"schema":2,"slots":[{"at_us":6360,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c64609caa48-fca7-49","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":6412,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6459,"slot":0,"token":"18e15c6461244908-fca7-4a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6461244908-fca7-4a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6477,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"end_us":6303,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6346,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6341},{"end_us":6361,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6358,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6383,"operation":"read","slot":0,"start_us":6381},{"end_us":6399,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6396}],"schema":2,"slots":[{"at_us":6399,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6461244908-fca7-4a","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":6106,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6166,"slot":0,"token":"18e15c6461a55430-fca7-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6461a55430-fca7-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6183,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":5995,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6036,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6030},{"end_us":6053,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6050,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6078,"operation":"read","slot":0,"start_us":6076},{"end_us":6092,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6089}],"schema":2,"slots":[{"at_us":6092,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6461a55430-fca7-4b","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":6439,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6451,"slot":0,"token":"18e15c646220bdf0-fca7-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c646220bdf0-fca7-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6468,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"end_us":6314,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6366,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6362},{"end_us":6383,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6380,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6413,"operation":"read","slot":0,"start_us":6411},{"end_us":6427,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6424}],"schema":2,"slots":[{"at_us":6427,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c646220bdf0-fca7-4c","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":6342,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6411,"slot":0,"token":"18e15c6462a3c8d0-fca7-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6462a3c8d0-fca7-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6422,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"end_us":6252,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6280,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6275},{"end_us":6294,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6291,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6318,"operation":"read","slot":0,"start_us":6316},{"end_us":6331,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6328}],"schema":2,"slots":[{"at_us":6331,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6462a3c8d0-fca7-4d","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":5086,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5240,"slot":0,"token":"18e15c646322ef98-fca7-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c646322ef98-fca7-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5255,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":5022,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":134,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5057,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5053},{"end_us":5070,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5070}],"schema":2,"slots":[{"at_us":5070,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c646322ef98-fca7-4e","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":6220,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6451,"slot":0,"token":"18e15c646390d850-fca7-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c646390d850-fca7-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6466,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6157,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6191,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6187},{"end_us":6204,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6204}],"schema":2,"slots":[{"at_us":6204,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c646390d850-fca7-4f","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":6379,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6359,"slot":0,"token":"18e15c64640f8600-fca7-50"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c64640f8600-fca7-50","watchdog":false,"workers_joined":1,"writes":[{"at_us":6375,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":46,"operation":"handoff","start_us":46},{"end_us":6312,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6349,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6344},{"end_us":6362,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6362}],"schema":2,"slots":[{"at_us":6362,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c64640f8600-fca7-50","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":5040,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5201,"slot":0,"token":"18e15c64648dca38-fca7-51"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c64648dca38-fca7-51","watchdog":false,"workers_joined":1,"writes":[{"at_us":5215,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"end_us":4988,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5012,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5009},{"end_us":5024,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5024}],"schema":2,"slots":[{"at_us":5024,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c64648dca38-fca7-51","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":58862,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5220,"slot":0,"token":"18e15c6464f8f7b8-fca7-52"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6464f8f7b8-fca7-52","watchdog":false,"workers_joined":1,"writes":[{"at_us":5238,"bytes":66,"kind":"head","slot":0},{"at_us":5243,"bytes":6,"kind":"body","slot":0},{"at_us":58937,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"end_us":5009,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5041,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5038},{"end_us":5057,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5052,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5083,"operation":"read","slot":0,"start_us":5081},{"end_us":58755,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5093,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58824,"operation":"read","slot":0,"start_us":58820},{"end_us":58841,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58837}],"schema":2,"slots":[{"at_us":58841,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6464f8f7b8-fca7-52","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":61398,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6443,"slot":0,"token":"18e15c6468a44c78-fca7-53"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6468a44c78-fca7-53","watchdog":false,"workers_joined":1,"writes":[{"at_us":6457,"bytes":66,"kind":"head","slot":0},{"at_us":6464,"bytes":6,"kind":"body","slot":0},{"at_us":61506,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":6239,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6280,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6276},{"end_us":6297,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6292,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6326,"operation":"read","slot":0,"start_us":6324},{"end_us":61302,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6337,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":61361,"operation":"read","slot":0,"start_us":61358},{"end_us":61384,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":61379}],"schema":2,"slots":[{"at_us":61384,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6468a44c78-fca7-53","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":55683,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5548,"slot":0,"token":"18e15c646c7282e8-fca7-54"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c646c7282e8-fca7-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":5568,"bytes":66,"kind":"head","slot":0},{"at_us":5578,"bytes":6,"kind":"body","slot":0},{"at_us":55877,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"end_us":5270,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5308,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5302},{"end_us":5326,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5321,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5347,"operation":"read","slot":0,"start_us":5345},{"end_us":55584,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5358,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":55648,"operation":"read","slot":0,"start_us":55645},{"end_us":55666,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55662}],"schema":2,"slots":[{"at_us":55666,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c646c7282e8-fca7-54","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":55385,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5267,"slot":0,"token":"18e15c646fecc140-fca7-55"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c646fecc140-fca7-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":5286,"bytes":66,"kind":"head","slot":0},{"at_us":5291,"bytes":6,"kind":"body","slot":0},{"at_us":55514,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":91,"operation":"handoff","start_us":91},{"end_us":5050,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":163,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5097,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5091},{"end_us":5114,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5110,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5140,"operation":"read","slot":0,"start_us":5138},{"end_us":55292,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5151,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":55356,"operation":"read","slot":0,"start_us":55352},{"end_us":55371,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55367}],"schema":2,"slots":[{"at_us":55371,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c646fecc140-fca7-55","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":4984,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5187,"slot":0,"token":"18e15c6473605490-fca7-56"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6473605490-fca7-56","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":4932,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4957,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":4954},{"end_us":4971,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":4970}],"schema":2,"slots":[{"at_us":4971,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c6473605490-fca7-56","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":6204,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6413,"slot":0,"token":"18e15c6473c7c508-fca7-57"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6473c7c508-fca7-57","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"end_us":6162,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6180,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6177},{"end_us":6192,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6192}],"schema":2,"slots":[{"at_us":6192,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c6473c7c508-fca7-57","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":6361,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6434,"slot":0,"token":"18e15c647440e4d8-fca7-58"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c647440e4d8-fca7-58","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"end_us":6315,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6339,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6335},{"end_us":6349,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6349}],"schema":2,"slots":[{"at_us":6349,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c647440e4d8-fca7-58","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":5016,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5178,"slot":0,"token":"18e15c6474bbecf0-fca7-59"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6474bbecf0-fca7-59","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"end_us":4981,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4995,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":4993},{"end_us":5005,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5005}],"schema":2,"slots":[{"at_us":5005,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c6474bbecf0-fca7-59","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":58185,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5193,"slot":0,"token":"18e15c647521a018-fca7-5a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c647521a018-fca7-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5208,"bytes":57,"kind":"head","slot":0},{"at_us":5214,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"end_us":5055,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":38,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5087,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5082},{"bytes":1,"capacity":1,"end_us":5111,"operation":"read","slot":0,"start_us":5105},{"capacity":1,"end_us":58111,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5135},{"end_us":58167,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58158}],"schema":2,"slots":[{"at_us":58167,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c647521a018-fca7-5a","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":59419,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6438,"slot":0,"token":"18e15c6478bacc68-fca7-5b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6478bacc68-fca7-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6450,"bytes":57,"kind":"head","slot":0},{"at_us":6456,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6150,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6187,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6183},{"end_us":6208,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6203,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6234,"operation":"read","slot":0,"start_us":6232},{"end_us":59302,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6250,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":59377,"operation":"read","slot":0,"start_us":59373},{"end_us":59404,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59400}],"schema":2,"slots":[{"at_us":59404,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6478bacc68-fca7-5b","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":61106,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6442,"slot":0,"token":"18e15c647c6949c0-fca7-5c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c647c6949c0-fca7-5c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6460,"bytes":57,"kind":"head","slot":0},{"at_us":6464,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":6189,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6222,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6218},{"end_us":6238,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6234,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6261,"operation":"read","slot":0,"start_us":6259},{"end_us":60999,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6271,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":61076,"operation":"read","slot":0,"start_us":61071},{"end_us":61092,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":61088}],"schema":2,"slots":[{"at_us":61092,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c647c6949c0-fca7-5c","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":59940,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6452,"slot":0,"token":"18e15c6480348e48-fca7-5d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6480348e48-fca7-5d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6469,"bytes":57,"kind":"head","slot":0},{"at_us":6475,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"end_us":6264,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6309,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6304},{"end_us":6326,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6320,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6351,"operation":"read","slot":0,"start_us":6349},{"end_us":59834,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6362,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":59905,"operation":"read","slot":0,"start_us":59900},{"end_us":59925,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59920}],"schema":2,"slots":[{"at_us":59925,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6480348e48-fca7-5d","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"truncated","completed_us":58813,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6486,"slot":0,"token":"18e15c6483ecb830-fca7-5e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c6483ecb830-fca7-5e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6501,"bytes":57,"kind":"head","slot":0},{"at_us":6507,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"end_us":6241,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6281,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6277},{"end_us":6297,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6293,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6326,"operation":"read","slot":0,"start_us":6324},{"end_us":58692,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6337,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":58774,"operation":"read","slot":0,"start_us":58770},{"end_us":58793,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58786}],"schema":2,"slots":[{"at_us":58793,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6483ecb830-fca7-5e","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":501394,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6467,"slot":0,"token":"18e15c648794ffb0-fca7-5f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c648794ffb0-fca7-5f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":501307,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501374,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501374}],"schema":2,"slots":[{"at_us":501374,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c648794ffb0-fca7-5f","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":501377,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5181,"slot":0,"token":"18e15c64a592f648-fca7-60"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c64a592f648-fca7-60","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":501277,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501349,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501349}],"schema":2,"slots":[{"at_us":501349,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c64a592f648-fca7-60","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":500416,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5206,"slot":0,"token":"18e15c64c39096f0-fca7-61"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c64c39096f0-fca7-61","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"end_us":500345,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500400,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500400}],"schema":2,"slots":[{"at_us":500400,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c64c39096f0-fca7-61","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"silent-head","completed_us":500655,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6430,"slot":0,"token":"18e15c64e17ffb10-fca7-62"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c64e17ffb10-fca7-62","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"end_us":500583,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500636,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500636}],"schema":2,"slots":[{"at_us":500636,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c64e17ffb10-fca7-62","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":500989,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6005,"slot":0,"token":"18e15c64ff717e28-fca7-63"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c64ff717e28-fca7-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":6014,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":5778,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5810,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5806},{"end_us":500905,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5822,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500972,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500966}],"schema":2,"slots":[{"at_us":500972,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c64ff717e28-fca7-63","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":500695,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5515,"slot":0,"token":"18e15c651d6d7cd8-fca7-64"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c651d6d7cd8-fca7-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":5523,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":5221,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5244,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5241},{"end_us":500569,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5256,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500656,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500650}],"schema":2,"slots":[{"at_us":500656,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c651d6d7cd8-fca7-64","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":500776,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6442,"slot":0,"token":"18e15c653b64ff18-fca7-65"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c653b64ff18-fca7-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":6460,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"end_us":6188,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6226,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6222},{"end_us":500687,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6237,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500757,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500751}],"schema":2,"slots":[{"at_us":500757,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c653b64ff18-fca7-65","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"pending-body","completed_us":501438,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6419,"slot":0,"token":"18e15c6559607ce0-fca7-66"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c6559607ce0-fca7-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":6430,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"end_us":6125,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6147,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6144},{"end_us":501226,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6158,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501398,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501392}],"schema":2,"slots":[{"at_us":501398,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6559607ce0-fca7-66","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":500600,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6449,"slot":0,"token":"18e15c6577660cc8-fca7-67"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c6577660cc8-fca7-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":6463,"bytes":57,"kind":"head","slot":0},{"at_us":6470,"bytes":1,"kind":"body","slot":0},{"at_us":309541,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6253,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6296,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6292},{"end_us":6312,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6308,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6343,"operation":"read","slot":0,"start_us":6341},{"end_us":309336,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6357,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":309415,"operation":"read","slot":0,"start_us":309412},{"end_us":500512,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309429,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500579,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500571}],"schema":2,"slots":[{"at_us":500579,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6577660cc8-fca7-67","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":501834,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5416,"slot":0,"token":"18e15c659566be98-fca7-68"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c659566be98-fca7-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":5434,"bytes":57,"kind":"head","slot":0},{"at_us":5441,"bytes":1,"kind":"body","slot":0},{"at_us":309392,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5249,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5335,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5330},{"end_us":5354,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5350,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5382,"operation":"read","slot":0,"start_us":5380},{"end_us":309239,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5395,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":309439,"operation":"read","slot":0,"start_us":309433},{"end_us":501672,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309462,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501771,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501734}],"schema":2,"slots":[{"at_us":501771,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c659566be98-fca7-68","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":501714,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5811,"slot":0,"token":"18e15c65b37b5628-fca7-69"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c65b37b5628-fca7-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":5828,"bytes":57,"kind":"head","slot":0},{"at_us":5834,"bytes":1,"kind":"body","slot":0},{"at_us":308867,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":76,"operation":"handoff","start_us":76},{"end_us":5393,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5433,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5429},{"end_us":5449,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5446,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5472,"operation":"read","slot":0,"start_us":5469},{"end_us":308425,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5485,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":308532,"operation":"read","slot":0,"start_us":308529},{"end_us":501601,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308546,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501674,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501668}],"schema":2,"slots":[{"at_us":501674,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c65b37b5628-fca7-69","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"trickle","completed_us":501323,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6367,"slot":0,"token":"18e15c65d18df1e8-fca7-6a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c65d18df1e8-fca7-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6382,"bytes":57,"kind":"head","slot":0},{"at_us":6388,"bytes":1,"kind":"body","slot":0},{"at_us":309310,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":92,"operation":"handoff","start_us":92},{"end_us":6132,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":113,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6173,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6170},{"end_us":6192,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6187,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6224,"operation":"read","slot":0,"start_us":6222},{"end_us":309053,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6241,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309144,"operation":"read","slot":0,"start_us":309141},{"end_us":501216,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309159,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501288,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501282}],"schema":2,"slots":[{"at_us":501288,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c65d18df1e8-fca7-6a","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":501385,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6033,"slot":1,"token":"18e15c65ef9aec40-fca7-6b"},{"path":"/readiness/siblings/0","received_us":6045,"slot":0,"token":"18e15c65ef9aec40-fca7-6b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c65ef9aec40-fca7-6b","watchdog":false,"workers_joined":2,"writes":[{"at_us":6051,"bytes":57,"kind":"head","slot":1},{"at_us":6062,"bytes":57,"kind":"head","slot":0},{"at_us":11133,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":105,"operation":"dispatch","outcome":"pending","slot":1,"start_us":80},{"end_us":5847,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":122,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5943,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5938},{"end_us":5976,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5972,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5996,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5995},{"end_us":10898,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6010,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":10931,"operation":"read","slot":1,"start_us":10928},{"end_us":10950,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10946,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10977,"operation":"read","slot":1,"start_us":10973},{"end_us":10990,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10989,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":11008,"operation":"read","slot":1,"start_us":11008},{"end_us":11020,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11018,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11039,"operation":"read","slot":1,"start_us":11039},{"end_us":11054,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11051},{"end_us":501294,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11065,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501364,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501358}],"schema":2,"slots":[{"at_us":501364,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11054,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c65ef9aec40-fca7-6b","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":500654,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6436,"slot":0,"token":"18e15c660db3a668-fca7-6c"},{"path":"/readiness/siblings/1","received_us":6439,"slot":1,"token":"18e15c660db3a668-fca7-6c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c660db3a668-fca7-6c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6448,"bytes":57,"kind":"head","slot":1},{"at_us":6456,"bytes":57,"kind":"head","slot":0},{"at_us":11489,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":105,"operation":"dispatch","outcome":"pending","slot":1,"start_us":82},{"end_us":6212,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":121,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6237,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6234},{"end_us":6251,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6249,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6264,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6263},{"end_us":11256,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6273,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":11304,"operation":"read","slot":1,"start_us":11301},{"end_us":11322,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11318,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11348,"operation":"read","slot":1,"start_us":11347},{"end_us":11359,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11357},{"end_us":500540,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11374,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500621,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500615}],"schema":2,"slots":[{"at_us":500621,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11359,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c660db3a668-fca7-6c","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":500825,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5594,"slot":0,"token":"18e15c662bc04ad0-fca7-6d"},{"path":"/readiness/siblings/1","received_us":5617,"slot":1,"token":"18e15c662bc04ad0-fca7-6d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c662bc04ad0-fca7-6d","watchdog":false,"workers_joined":2,"writes":[{"at_us":5627,"bytes":57,"kind":"head","slot":0},{"at_us":5634,"bytes":57,"kind":"head","slot":1},{"at_us":5648,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":73,"operation":"dispatch","outcome":"pending","slot":1,"start_us":61},{"body_reads":0,"deadline_us":500000,"end_us":112,"operation":"handoff","start_us":112},{"end_us":5326,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":126,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5379,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5374},{"end_us":5398,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5396,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5413,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5413},{"end_us":5428,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5423,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5450,"operation":"read","slot":1,"start_us":5448},{"end_us":5476,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5475,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5496,"operation":"read","slot":1,"start_us":5495},{"end_us":5506,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5505,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5524,"operation":"read","slot":1,"start_us":5522},{"end_us":5535,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5532,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5555,"operation":"read","slot":1,"start_us":5555},{"end_us":5566,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5563},{"end_us":500740,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5577,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500808,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500801}],"schema":2,"slots":[{"at_us":500808,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5566,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c662bc04ad0-fca7-6d","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"siblings","completed_us":502580,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":7369,"slot":0,"token":"18e15c6649d6f988-fca7-6e"},{"path":"/readiness/siblings/1","received_us":7374,"slot":1,"token":"18e15c6649d6f988-fca7-6e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e15c6649d6f988-fca7-6e","watchdog":false,"workers_joined":2,"writes":[{"at_us":7384,"bytes":57,"kind":"head","slot":1},{"at_us":7397,"bytes":57,"kind":"head","slot":0},{"at_us":12730,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":113,"operation":"dispatch","outcome":"pending","slot":1,"start_us":87},{"body_reads":0,"deadline_us":500000,"end_us":141,"operation":"handoff","start_us":141},{"end_us":7132,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":154,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7168,"operation":"head-poll","outcome":"ready","slot":1,"start_us":7163},{"end_us":7181,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":7179,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":7193,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7193},{"end_us":12472,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7202,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":3,"capacity":16384,"end_us":12529,"operation":"read","slot":1,"start_us":12526},{"end_us":12549,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12545,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":16384,"end_us":12571,"operation":"read","slot":1,"start_us":12570},{"end_us":12583,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12580},{"end_us":502495,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12597,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":502560,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502554}],"schema":2,"slots":[{"at_us":502560,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12583,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6649d6f988-fca7-6e","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":501405,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5266,"slot":0,"token":"18e15c6667fff3d8-fca7-6f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c6667fff3d8-fca7-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5284,"bytes":57,"kind":"head","slot":0},{"at_us":5292,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":5041,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5092,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5088},{"end_us":5115,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5109,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5142,"operation":"read","slot":0,"start_us":5140},{"end_us":501314,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5157,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501383,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501376}],"schema":2,"slots":[{"at_us":501383,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c6667fff3d8-fca7-6f","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":501325,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5232,"slot":0,"token":"18e15c668610af20-fca7-70"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c668610af20-fca7-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":5244,"bytes":57,"kind":"head","slot":0},{"at_us":5250,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":4987,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5027,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5022},{"end_us":5045,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5040,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5074,"operation":"read","slot":0,"start_us":5072},{"end_us":501207,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5092,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501298,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501283}],"schema":2,"slots":[{"at_us":501298,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c668610af20-fca7-70","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":501936,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5584,"slot":0,"token":"18e15c66a418b7d8-fca7-71"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e15c66a418b7d8-fca7-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":5604,"bytes":57,"kind":"head","slot":0},{"at_us":5609,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"end_us":5185,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5235,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5230},{"end_us":5253,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5249,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5275,"operation":"read","slot":0,"start_us":5273},{"end_us":501809,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5290,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501889,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501879}],"schema":2,"slots":[{"at_us":501889,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66a418b7d8-fca7-71","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"exhausted","completed_us":501265,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6281,"slot":0,"token":"18e15c66c2434868-fca7-72"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e15c66c2434868-fca7-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":6297,"bytes":57,"kind":"head","slot":0},{"at_us":6303,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":134,"operation":"handoff","start_us":134},{"end_us":5991,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":159,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6031,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6026},{"end_us":6050,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6046,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6078,"operation":"read","slot":0,"start_us":6075},{"end_us":501139,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6090,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501222,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501216}],"schema":2,"slots":[{"at_us":501222,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66c2434868-fca7-72","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":6371,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6430,"slot":0,"token":"18e15c66e04ccff0-fca7-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e04ccff0-fca7-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":6439,"bytes":57,"kind":"head","slot":0},{"at_us":6444,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":6166,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6206,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6202},{"end_us":6223,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6220,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6244,"operation":"read","slot":0,"start_us":6242},{"end_us":6255,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6254,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6285,"operation":"read","slot":0,"start_us":6277},{"end_us":6298,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6297,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6318,"operation":"read","slot":0,"start_us":6317},{"end_us":6331,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6328,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6350,"operation":"read","slot":0,"start_us":6349},{"end_us":6361,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6358}],"schema":2,"slots":[{"at_us":6361,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66e04ccff0-fca7-73","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":5333,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5397,"slot":0,"token":"18e15c66e0d67e08-fca7-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e0d67e08-fca7-74","watchdog":false,"workers_joined":1,"writes":[{"at_us":5409,"bytes":57,"kind":"head","slot":0},{"at_us":5414,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"end_us":5208,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5242,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5240},{"end_us":5257,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5254,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5279,"operation":"read","slot":0,"start_us":5279},{"end_us":5291,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5290,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5310,"operation":"read","slot":0,"start_us":5310},{"end_us":5323,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5321}],"schema":2,"slots":[{"at_us":5323,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66e0d67e08-fca7-74","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":6001,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6015,"slot":0,"token":"18e15c66e148eb00-fca7-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e148eb00-fca7-75","watchdog":false,"workers_joined":1,"writes":[{"at_us":6025,"bytes":57,"kind":"head","slot":0},{"at_us":6030,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"end_us":5830,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5858,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5855},{"end_us":5873,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5869,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5893,"operation":"read","slot":0,"start_us":5892},{"end_us":5906,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5905,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5925,"operation":"read","slot":0,"start_us":5924},{"end_us":5935,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5934,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5952,"operation":"read","slot":0,"start_us":5952},{"end_us":5962,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5961,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5980,"operation":"read","slot":0,"start_us":5980},{"end_us":5990,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5989}],"schema":2,"slots":[{"at_us":5990,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66e148eb00-fca7-75","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"eof","completed_us":6011,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6057,"slot":0,"token":"18e15c66e1ca01e0-fca7-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e1ca01e0-fca7-76","watchdog":false,"workers_joined":1,"writes":[{"at_us":6069,"bytes":57,"kind":"head","slot":0},{"at_us":6074,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"end_us":5862,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5902,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5897},{"end_us":5923,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5919,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5949,"operation":"read","slot":0,"start_us":5948},{"end_us":5963,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5961,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5987,"operation":"read","slot":0,"start_us":5986},{"end_us":5999,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5996}],"schema":2,"slots":[{"at_us":5999,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66e1ca01e0-fca7-76","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":5575,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5646,"slot":0,"token":"18e15c66e247d0e8-fca7-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e247d0e8-fca7-77","watchdog":false,"workers_joined":1,"writes":[{"at_us":5656,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"end_us":5482,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":42,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5514,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5512},{"end_us":5529,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5527,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5550,"operation":"read","slot":0,"start_us":5549},{"end_us":5562,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5561}],"schema":2,"slots":[{"at_us":5562,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66e247d0e8-fca7-77","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":5211,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5268,"slot":0,"token":"18e15c66e2b7e450-fca7-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e2b7e450-fca7-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":5275,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":23,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"end_us":5127,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":38,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5150,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5148},{"end_us":5162,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5161,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5185,"operation":"read","slot":0,"start_us":5184},{"end_us":5199,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5197}],"schema":2,"slots":[{"at_us":5199,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66e2b7e450-fca7-78","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":6281,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6376,"slot":0,"token":"18e15c66e3210a48-fca7-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e3210a48-fca7-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":6382,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":19,"operation":"dispatch","outcome":"pending","slot":0,"start_us":7},{"body_reads":0,"deadline_us":500000,"end_us":34,"operation":"handoff","start_us":34},{"end_us":6214,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6229,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6228},{"end_us":6243,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6240,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6261,"operation":"read","slot":0,"start_us":6261},{"end_us":6271,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6270}],"schema":2,"slots":[{"at_us":6271,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66e3210a48-fca7-79","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"empty-eof","completed_us":5495,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5534,"slot":0,"token":"18e15c66e39b89a8-fca7-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e39b89a8-fca7-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5544,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":15,"operation":"dispatch","outcome":"pending","slot":0,"start_us":5},{"body_reads":0,"deadline_us":500000,"end_us":34,"operation":"handoff","start_us":34},{"end_us":5415,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5438,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5436},{"end_us":5451,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5450,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5473,"operation":"read","slot":0,"start_us":5473},{"end_us":5484,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5483}],"schema":2,"slots":[{"at_us":5484,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66e39b89a8-fca7-7a","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":5593,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5690,"slot":0,"token":"18e15c66e40a7818-fca7-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e40a7818-fca7-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5703,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"end_us":5544,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5566,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5563},{"end_us":5578,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5578}],"schema":2,"slots":[{"at_us":5578,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c66e40a7818-fca7-7b","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":5041,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5150,"slot":0,"token":"18e15c66e47a1e20-fca7-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e47a1e20-fca7-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5157,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"end_us":5003,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":36,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5019,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5017},{"end_us":5030,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5030}],"schema":2,"slots":[{"at_us":5030,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c66e47a1e20-fca7-7c","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":5169,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5288,"slot":0,"token":"18e15c66e4e09880-fca7-7d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e4e09880-fca7-7d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5296,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":42,"operation":"handoff","start_us":42},{"end_us":5124,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5144,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5142},{"end_us":5156,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5156}],"schema":2,"slots":[{"at_us":5156,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c66e4e09880-fca7-7d","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-head","completed_us":6280,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6387,"slot":0,"token":"18e15c66e5498bb0-fca7-7e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e5498bb0-fca7-7e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6394,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":21,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"body_reads":0,"deadline_us":500000,"end_us":35,"operation":"handoff","start_us":35},{"end_us":6232,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6246,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6245},{"end_us":6257,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6257}],"schema":2,"slots":[{"at_us":6257,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c66e5498bb0-fca7-7e","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":58395,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5619,"slot":0,"token":"18e15c66e5c29028-fca7-7f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e5c29028-fca7-7f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5632,"bytes":66,"kind":"head","slot":0},{"at_us":5637,"bytes":6,"kind":"body","slot":0},{"at_us":58435,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":18,"operation":"dispatch","outcome":"pending","slot":0,"start_us":7},{"end_us":5501,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":30,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5527,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5525},{"end_us":5543,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5537,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5564,"operation":"read","slot":0,"start_us":5562},{"end_us":58318,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5575,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58366,"operation":"read","slot":0,"start_us":58363},{"end_us":58381,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58378}],"schema":2,"slots":[{"at_us":58381,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66e5c29028-fca7-7f","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":57468,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6415,"slot":0,"token":"18e15c66e9614670-fca7-80"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66e9614670-fca7-80","watchdog":false,"workers_joined":1,"writes":[{"at_us":6422,"bytes":66,"kind":"head","slot":0},{"at_us":6427,"bytes":6,"kind":"body","slot":0},{"at_us":57565,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":6201,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6223,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6220},{"end_us":6237,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6234,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6259,"operation":"read","slot":0,"start_us":6257},{"end_us":57369,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6271,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":57436,"operation":"read","slot":0,"start_us":57431},{"end_us":57453,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57449}],"schema":2,"slots":[{"at_us":57453,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66e9614670-fca7-80","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":56602,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6273,"slot":0,"token":"18e15c66ecf4e0f8-fca7-81"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66ecf4e0f8-fca7-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":6281,"bytes":66,"kind":"head","slot":0},{"at_us":6284,"bytes":6,"kind":"body","slot":0},{"at_us":56724,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"end_us":6053,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6084,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6081},{"end_us":6100,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6096,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6140,"operation":"read","slot":0,"start_us":6138},{"end_us":56513,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6155,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":56571,"operation":"read","slot":0,"start_us":56568},{"end_us":56587,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56583}],"schema":2,"slots":[{"at_us":56587,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66ecf4e0f8-fca7-81","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"malformed-body","completed_us":55886,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5848,"slot":0,"token":"18e15c66f07c8500-fca7-82"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66f07c8500-fca7-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":5857,"bytes":66,"kind":"head","slot":0},{"at_us":5863,"bytes":6,"kind":"body","slot":0},{"at_us":56031,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":54,"operation":"handoff","start_us":54},{"end_us":5587,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5608,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5604},{"end_us":5621,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5618,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5650,"operation":"read","slot":0,"start_us":5649},{"end_us":55784,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5661,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":55850,"operation":"read","slot":0,"start_us":55846},{"end_us":55869,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55865}],"schema":2,"slots":[{"at_us":55869,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e15c66f07c8500-fca7-82","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":5177,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5250,"slot":0,"token":"18e15c66f3f7c140-fca7-83"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66f3f7c140-fca7-83","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":5074,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5101,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5096},{"end_us":5114,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5113}],"schema":2,"slots":[{"at_us":5114,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c66f3f7c140-fca7-83","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":5077,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5175,"slot":0,"token":"18e15c66f462fa78-fca7-84"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66f462fa78-fca7-84","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":5020,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5048,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5045},{"end_us":5061,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5061}],"schema":2,"slots":[{"at_us":5061,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c66f462fa78-fca7-84","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":6206,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6256,"slot":0,"token":"18e15c66f4cc91b8-fca7-85"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66f4cc91b8-fca7-85","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":50,"operation":"handoff","start_us":50},{"end_us":6122,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6157,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6153},{"end_us":6191,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6191}],"schema":2,"slots":[{"at_us":6191,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c66f4cc91b8-fca7-85","tolerance_us":5000}} +{"run":"stock-current","probe":{"case":"head-error","completed_us":5059,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5206,"slot":0,"token":"18e15c66f545c8f8-fca7-86"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e15c66f545c8f8-fca7-86","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"d12730fda2704ba4c95e878dccd50aef8c4ec22b","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"end_us":5002,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5026,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5023},{"end_us":5042,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5042}],"schema":2,"slots":[{"at_us":5042,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e15c66f545c8f8-fca7-86","tolerance_us":5000}} diff --git a/docs/audits/evidence/2026-10-23-fastly-readiness-matrix.json b/docs/audits/evidence/2026-10-23-fastly-readiness-matrix.json new file mode 100644 index 000000000..141b3fb34 --- /dev/null +++ b/docs/audits/evidence/2026-10-23-fastly-readiness-matrix.json @@ -0,0 +1,557 @@ +{ + "schema": 1, + "execution_date": "2026-10-23", + "status": "Failed local timing acceptance; repaired runtime preserves source errors", + "qualification": false, + "edgezero_base_revision": "d12730fda2704ba4c95e878dccd50aef8c4ec22b", + "source_state": "working-tree characterization; not a frozen qualification record", + "command": "VICEROY_BIN= scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-readiness --output ", + "matrix": { + "cases": 11, + "consumption": ["buffered", "streamed"], + "read_capacities": [1, 16384], + "repetitions": 3, + "finite_direct_controls": 3, + "probes_per_run": 135, + "origin_requests_per_run": 147, + "deadline_us": 500000, + "maximum_completion_us": 505000, + "backend_phase_timeout_ms": 2000, + "backend_registration_in_timing_window": false, + "production_read_maximum_approved": false + }, + "runtime": { + "sdk": "fastly 0.13.1", + "upstream_tag": "v0.21.0", + "upstream_revision": "a69d982d1d95e13c0fc9f92683b9118639bb4b04", + "stock_binary_sha256": "751ff2c1a83063b109bc212b95be5f52e5ab6284b9027fa03aa229087eba3f30", + "repaired_binary_sha256": "98ea0536478ce2e641f093b2619b7ac7c6c237402f140e5792d854575abc7100", + "patch_sha256": "8693ce34f82cc98dc272bf39379007b17a40dfc19138c55b445378fd9a163759", + "build_details": "2026-10-23-viceroy-readiness-repair.json", + "pins_changed": false, + "installed_runtime_replaced": false + }, + "containment": { + "origin_maximum_workers": 2, + "origin_request_head_bytes": 4096, + "origin_watchdog_ms": 3000, + "client_timeout_ms": 12000, + "guest_trace_samples_maximum": 512, + "guest_consumed_bytes_per_slot_maximum": 65536, + "forced_cleanup_is_cancellation_evidence": false + }, + "interpretation": [ + "The pre-hardening pair uses identical guest bytes; the current pair uses identical revised guest bytes.", + "Guest revisions identify the base only; source hashes describe the current uncommitted fixture inputs.", + "The current fixture persists completed samples incrementally and rechecks expiry immediately before body reads.", + "First repaired matrix: pending-body/selected/streamed/1/0 completed at 507794 us; select returned at 507153 us.", + "Current repaired matrix: siblings/selected/streamed/1/1 completed at 507233 us, outside the unchanged 505000 us boundary.", + "Traces observe lateness at the host-call boundary; they do not isolate the OS/runtime scheduling cause.", + "Stock loses errors in all 24 selected truncation/malformed-body probes. Repaired runs retain those source errors.", + "No retry result erases the earlier failures. No timing tolerance, policy approval or capability is changed.", + "Raw guest/origin probes are retained separately; full event/log files and binaries are identified by hash, not retained release assets.", + "B, H(K), production-path migration, exact-version review, hosted evidence, candidate freeze and all qualification legs remain open." + ], + "source_files": [ + { + "path": "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs", + "sha256": "04fd787c3046d8867093915d43043037dbe95f229b5d45286426162e10df5c49" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs", + "sha256": "4f635687f6cbefc458c90acda71e03378825e94bb5d8fa47435df5c55eaeddb8" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence/readiness.rs", + "sha256": "1e81a86aedf33d624b1c4b417e2be77d1ec9ec7473eee1fd65645e887dfe3113" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs", + "sha256": "26a64dd6a2814031fed30cbd77d57e75f2cdc00540a5de1e0fac707390a4cf5d" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/net/readiness.rs", + "sha256": "c4a3cbe13e0bfb1fc0134c920cb78792e2e3bb4b352bbdc68cf18c4c0995fd67" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs", + "sha256": "54e4c2974dd0233419875a8c9d8c90a6b8a573552b20786d1cff0c432355260f" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/readiness.rs", + "sha256": "91b90dc9933a48a92567df79aca08937e637d2a96525fbdb5e0f81f7e1d3815f" + }, + { + "path": "tests/fixtures/reusable-app/Cargo.lock", + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + { + "path": "patches/viceroy/0.21.0-body-readiness.patch", + "sha256": "8693ce34f82cc98dc272bf39379007b17a40dfc19138c55b445378fd9a163759" + } + ], + "runs": [ + { + "id": "repaired-before-trace-hardening", + "exit_code": 1, + "directory": "/private/tmp/edgezero-fastly-readiness-matrix-20261023-02", + "events_sha256": "6346fed93b8dd4a4746ca14609a067198e56c9c4e5a3167a14b2df8d2062e32c", + "log_sha256": "17638b9f2800526fb2d1105c97795e03f1b8e9a507c90cd5b29ae37a0c6f90e4", + "guest_sha256": "b7ac011327c2335bfff83d0ac3c369bf075497277f9162e73f39bd8206d516c6", + "probes": 135, + "origin_requests": 147, + "max_completion_us": 507794, + "all_workers_joined": true, + "origin_errors": [], + "watchdogs": 0, + "source_error_losses": [], + "late_probes": [ + { + "leg": "pending-body/selected/streamed/1/0", + "completed_us": 507794, + "late_operations": [ + { + "end_us": 507153, + "operation": "select", + "phases": ["body"], + "selected": null, + "slots": [0], + "start_us": 5332, + "status": "FastlyStatus::OK", + "timeout_ms": 494 + }, + { + "end_us": 507596, + "operation": "finish", + "outcome": "timeout", + "phase": "body", + "slot": 0, + "start_us": 507362 + } + ] + } + ], + "known_failed_legs": ["pending-body/selected/streamed/1/0"], + "error_comparisons": [ + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 0, + "selected": "UnexpectedEof", + "status": "pass" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 1, + "selected": "UnexpectedEof", + "status": "pass" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 2, + "selected": "UnexpectedEof", + "status": "pass" + } + ], + "validation_failures": ["reordered or late operation"] + }, + { + "id": "stock-before-trace-hardening", + "exit_code": 1, + "directory": "/private/tmp/edgezero-fastly-readiness-matrix-stock-20261023-01", + "events_sha256": "1a41cc6b24494915a79715f20fdf1c5c2a9443257cf7a8592d4074d9e6fb027f", + "log_sha256": "57d330bc7783cfdf7f8a313581fa103f31e6a057faba3fecf30622e929a295f6", + "guest_sha256": "b7ac011327c2335bfff83d0ac3c369bf075497277f9162e73f39bd8206d516c6", + "probes": 135, + "origin_requests": 147, + "max_completion_us": 533355, + "all_workers_joined": true, + "origin_errors": [], + "watchdogs": 0, + "source_error_losses": [ + "truncated/selected/buffered/1/0", + "truncated/selected/buffered/16384/0", + "truncated/selected/streamed/1/0", + "truncated/selected/streamed/16384/0", + "malformed-body/selected/buffered/1/0", + "malformed-body/selected/buffered/16384/0", + "malformed-body/selected/streamed/1/0", + "malformed-body/selected/streamed/16384/0", + "truncated/selected/buffered/1/1", + "truncated/selected/buffered/16384/1", + "truncated/selected/streamed/1/1", + "truncated/selected/streamed/16384/1", + "malformed-body/selected/buffered/1/1", + "malformed-body/selected/buffered/16384/1", + "malformed-body/selected/streamed/1/1", + "malformed-body/selected/streamed/16384/1", + "truncated/selected/buffered/1/2", + "truncated/selected/buffered/16384/2", + "truncated/selected/streamed/1/2", + "truncated/selected/streamed/16384/2", + "malformed-body/selected/buffered/1/2", + "malformed-body/selected/buffered/16384/2", + "malformed-body/selected/streamed/1/2", + "malformed-body/selected/streamed/16384/2" + ], + "late_probes": [ + { + "leg": "pending-body/selected/buffered/16384/1", + "completed_us": 515212, + "late_operations": [ + { + "end_us": 515190, + "operation": "select", + "phases": ["body"], + "selected": null, + "slots": [0], + "start_us": 6179, + "status": "FastlyStatus::OK", + "timeout_ms": 493 + }, + { + "end_us": 515206, + "operation": "finish", + "outcome": "timeout", + "phase": "body", + "slot": 0, + "start_us": 515195 + } + ] + }, + { + "leg": "trickle/selected/streamed/16384/1", + "completed_us": 505014, + "late_operations": [ + { + "end_us": 505008, + "operation": "finish", + "outcome": "timeout", + "phase": "body", + "slot": 0, + "start_us": 505000 + } + ] + }, + { + "leg": "exhausted/selected/streamed/1/1", + "completed_us": 533355, + "late_operations": [ + { + "end_us": 529716, + "operation": "select", + "phases": ["body"], + "selected": null, + "slots": [0], + "start_us": 6095, + "status": "FastlyStatus::OK", + "timeout_ms": 493 + }, + { + "end_us": 533344, + "operation": "finish", + "outcome": "timeout", + "phase": "body", + "slot": 0, + "start_us": 529754 + } + ] + }, + { + "leg": "exhausted/selected/streamed/16384/1", + "completed_us": 507039, + "late_operations": [ + { + "end_us": 506010, + "operation": "select", + "phases": ["body"], + "selected": null, + "slots": [0], + "start_us": 9789, + "status": "FastlyStatus::OK", + "timeout_ms": 490 + }, + { + "end_us": 506921, + "operation": "finish", + "outcome": "timeout", + "phase": "body", + "slot": 0, + "start_us": 506177 + } + ] + } + ], + "known_failed_legs": [ + "pending-body/selected/buffered/16384/1", + "trickle/selected/streamed/16384/1", + "exhausted/selected/streamed/1/1", + "exhausted/selected/streamed/16384/1", + "truncated/selected/buffered/1/0", + "truncated/selected/buffered/16384/0", + "truncated/selected/streamed/1/0", + "truncated/selected/streamed/16384/0", + "malformed-body/selected/buffered/1/0", + "malformed-body/selected/buffered/16384/0", + "malformed-body/selected/streamed/1/0", + "malformed-body/selected/streamed/16384/0", + "truncated/selected/buffered/1/1", + "truncated/selected/buffered/16384/1", + "truncated/selected/streamed/1/1", + "truncated/selected/streamed/16384/1", + "malformed-body/selected/buffered/1/1", + "malformed-body/selected/buffered/16384/1", + "malformed-body/selected/streamed/1/1", + "malformed-body/selected/streamed/16384/1", + "truncated/selected/buffered/1/2", + "truncated/selected/buffered/16384/2", + "truncated/selected/streamed/1/2", + "truncated/selected/streamed/16384/2", + "malformed-body/selected/buffered/1/2", + "malformed-body/selected/buffered/16384/2", + "malformed-body/selected/streamed/1/2", + "malformed-body/selected/streamed/16384/2" + ], + "error_comparisons": [ + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 0, + "selected": null, + "status": "fail" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 1, + "selected": null, + "status": "fail" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 2, + "selected": null, + "status": "fail" + } + ], + "validation_failures": ["wrong case terminal"] + }, + { + "id": "repaired-current", + "exit_code": 1, + "directory": "/private/tmp/edgezero-fastly-readiness-matrix-20261023-03", + "events_sha256": "5a59175ec845a02d0e2fe9669c6859bd9c1024fe865d1a10212fef4a3bae4495", + "log_sha256": "e5434780fb66267ac0a879907fb7e6f4a3860cdc012a55a3e1aa61bb1f140142", + "guest_sha256": "1495a3bd629481f81d3adc5709347e3bd672ae92049e13d3de134b0f2f01a3c5", + "probes": 135, + "origin_requests": 147, + "max_completion_us": 507233, + "all_workers_joined": true, + "origin_errors": [], + "watchdogs": 0, + "source_error_losses": [], + "late_probes": [ + { + "leg": "siblings/selected/streamed/1/1", + "completed_us": 507233, + "late_operations": [ + { + "end_us": 506716, + "operation": "select", + "phases": ["body"], + "selected": null, + "slots": [0], + "start_us": 6419, + "status": "FastlyStatus::OK", + "timeout_ms": 493 + }, + { + "end_us": 507211, + "operation": "finish", + "outcome": "timeout", + "phase": "body", + "slot": 0, + "start_us": 507203 + } + ] + } + ], + "known_failed_legs": ["siblings/selected/streamed/1/1"], + "error_comparisons": [ + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 0, + "selected": "UnexpectedEof", + "status": "pass" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 1, + "selected": "UnexpectedEof", + "status": "pass" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 2, + "selected": "UnexpectedEof", + "status": "pass" + } + ], + "validation_failures": [ + "1 readiness leg(s) failed:\nsiblings/selected/streamed/1/1: reordered or late operation" + ] + }, + { + "id": "stock-current", + "exit_code": 1, + "directory": "/private/tmp/edgezero-fastly-readiness-matrix-stock-20261023-02", + "events_sha256": "fc0efd5096a8cea8cc664484e765887d7c93a5566348031e1556edd229be2398", + "log_sha256": "c4b31684d2d4cacb329989be09249b614e986b2ffd396fa5048c2575a0797565", + "guest_sha256": "1495a3bd629481f81d3adc5709347e3bd672ae92049e13d3de134b0f2f01a3c5", + "probes": 135, + "origin_requests": 147, + "max_completion_us": 502580, + "all_workers_joined": true, + "origin_errors": [], + "watchdogs": 0, + "source_error_losses": [ + "truncated/selected/buffered/1/0", + "truncated/selected/buffered/16384/0", + "truncated/selected/streamed/1/0", + "truncated/selected/streamed/16384/0", + "malformed-body/selected/buffered/1/0", + "malformed-body/selected/buffered/16384/0", + "malformed-body/selected/streamed/1/0", + "malformed-body/selected/streamed/16384/0", + "truncated/selected/buffered/1/1", + "truncated/selected/buffered/16384/1", + "truncated/selected/streamed/1/1", + "truncated/selected/streamed/16384/1", + "malformed-body/selected/buffered/1/1", + "malformed-body/selected/buffered/16384/1", + "malformed-body/selected/streamed/1/1", + "malformed-body/selected/streamed/16384/1", + "truncated/selected/buffered/1/2", + "truncated/selected/buffered/16384/2", + "truncated/selected/streamed/1/2", + "truncated/selected/streamed/16384/2", + "malformed-body/selected/buffered/1/2", + "malformed-body/selected/buffered/16384/2", + "malformed-body/selected/streamed/1/2", + "malformed-body/selected/streamed/16384/2" + ], + "late_probes": [], + "known_failed_legs": [ + "truncated/selected/buffered/1/0", + "truncated/selected/buffered/16384/0", + "truncated/selected/streamed/1/0", + "truncated/selected/streamed/16384/0", + "malformed-body/selected/buffered/1/0", + "malformed-body/selected/buffered/16384/0", + "malformed-body/selected/streamed/1/0", + "malformed-body/selected/streamed/16384/0", + "truncated/selected/buffered/1/1", + "truncated/selected/buffered/16384/1", + "truncated/selected/streamed/1/1", + "truncated/selected/streamed/16384/1", + "malformed-body/selected/buffered/1/1", + "malformed-body/selected/buffered/16384/1", + "malformed-body/selected/streamed/1/1", + "malformed-body/selected/streamed/16384/1", + "truncated/selected/buffered/1/2", + "truncated/selected/buffered/16384/2", + "truncated/selected/streamed/1/2", + "truncated/selected/streamed/16384/2", + "malformed-body/selected/buffered/1/2", + "malformed-body/selected/buffered/16384/2", + "malformed-body/selected/streamed/1/2", + "malformed-body/selected/streamed/16384/2" + ], + "error_comparisons": [ + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 0, + "selected": null, + "status": "fail" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 1, + "selected": null, + "status": "fail" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "repetition": 2, + "selected": null, + "status": "fail" + } + ], + "validation_failures": [ + "24 readiness leg(s) failed:\ntruncated/selected/buffered/1/0: wrong case terminal\ntruncated/selected/buffered/16384/0: wrong case terminal\ntruncated/selected/streamed/1/0: wrong case terminal\ntruncated/selected/streamed/16384/0: wrong case terminal\nmalformed-body/selected/buffered/1/0: wrong case terminal\nmalformed-body/selected/buffered/16384/0: wrong case terminal\nmalformed-body/selected/streamed/1/0: wrong case terminal\nmalformed-body/selected/streamed/16384/0: wrong case terminal\ntruncated/selected/buffered/1/1: wrong case terminal\ntruncated/selected/buffered/16384/1: wrong case terminal\ntruncated/selected/streamed/1/1: wrong case terminal\ntruncated/selected/streamed/16384/1: wrong case terminal\nmalformed-body/selected/buffered/1/1: wrong case terminal\nmalformed-body/selected/buffered/16384/1: wrong case terminal\nmalformed-body/selected/streamed/1/1: wrong case terminal\nmalformed-body/selected/streamed/16384/1: wrong case terminal\ntruncated/selected/buffered/1/2: wrong case terminal\ntruncated/selected/buffered/16384/2: wrong case terminal\ntruncated/selected/streamed/1/2: wrong case terminal\ntruncated/selected/streamed/16384/2: wrong case terminal\nmalformed-body/selected/buffered/1/2: wrong case terminal\nmalformed-body/selected/buffered/16384/2: wrong case terminal\nmalformed-body/selected/streamed/1/2: wrong case terminal\nmalformed-body/selected/streamed/16384/2: wrong case terminal" + ] + } + ], + "retained_probes": { + "path": "docs/audits/evidence/2026-10-23-fastly-readiness-matrix-probes.jsonl", + "sha256": "7499118d2754abfbfdd92676867c53f5d850d2f89f8f6220f498b0bdc11664b6", + "records": 540 + }, + "verification": { + "fixture_core_tests": 6, + "fixture_harness_tests": 103, + "retained_probe_replay_tests": 1, + "readiness_tests": 19, + "strict_fixture_clippy": "passed for native harness and wasm32-wasip1 diagnostic guest", + "fixture_format": "passed", + "qualification_inventory": { + "criteria": 255, + "legs": 511, + "planned": 511, + "qualified": false + }, + "root_gates": { + "format": "passed", + "strict_workspace_clippy": "passed", + "workspace_tests": { + "result_suites": 28, + "nonempty_suites": 21, + "passed": 2193, + "failed": 0, + "ignored": 1, + "measured": 0, + "filtered_out": 0, + "ui_compile_cases": 22 + }, + "feature_check": "passed", + "spin_wasip2_check": "passed", + "test_execution": "loopback-authorized rerun; initial sandbox permission failure retained", + "status_log_sha256": "d61b8895a879bf36f597e0ca11666a8c4c2a30a2e9cc18a56f434ce90613da63" + }, + "timeout_wait_review_regressions": { + "red_missing_wait_mutations": 3, + "green": "all mutations rejected; 540 actual probes replayed with unchanged failed dispositions" + } + } +} diff --git a/docs/audits/evidence/2026-10-23-fastly-timer-controls-probes.jsonl b/docs/audits/evidence/2026-10-23-fastly-timer-controls-probes.jsonl new file mode 100644 index 000000000..cf43c3593 --- /dev/null +++ b/docs/audits/evidence/2026-10-23-fastly-timer-controls-probes.jsonl @@ -0,0 +1,828 @@ +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1031,"event":"readiness-timer-control","overrun_us":31,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160dcd73e73d0-5dc4-3","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2173,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2174,"event":"readiness-timer-control","overrun_us":1174,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160dcd750f618-5dc4-4","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2263,"event":"readiness-timer-control","overrun_us":1263,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160dcd77b8c70-5dc4-5","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102473,"event":"readiness-timer-control","overrun_us":2473,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160dcd7a54fd8-5dc4-6","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102073,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102081,"event":"readiness-timer-control","overrun_us":2081,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160dcddc4f440-5dc4-7","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101852,"event":"readiness-timer-control","overrun_us":1852,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160dce3eaf590-5dc4-8","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":505015,"event":"readiness-timer-control","overrun_us":5015,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160dcea0ad878-5dc4-9","within_five_ms":false}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501098,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501105,"event":"readiness-timer-control","overrun_us":1105,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160dd08287888-5dc4-a","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502056,"event":"readiness-timer-control","overrun_us":2056,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160dd26181740-5dc4-b","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1254,"event":"readiness-timer-control","overrun_us":254,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160dd441258a0-5dc4-c","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2023,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2025,"event":"readiness-timer-control","overrun_us":1025,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160dd4427ec10-5dc4-d","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2042,"event":"readiness-timer-control","overrun_us":1042,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160dd44524bb8-5dc4-e","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":105010,"event":"readiness-timer-control","overrun_us":5010,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160dd447a0b80-5dc4-f","within_five_ms":false}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101111,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101117,"event":"readiness-timer-control","overrun_us":1117,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160dd4abff0e0-5dc4-10","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101635,"event":"readiness-timer-control","overrun_us":1635,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160dd50d8bf48-5dc4-11","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504060,"event":"readiness-timer-control","overrun_us":4060,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160dd56f58d20-5dc4-12","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502056,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502066,"event":"readiness-timer-control","overrun_us":2066,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160dd7504aa58-5dc4-13","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501358,"event":"readiness-timer-control","overrun_us":1358,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160dd930135a8-5dc4-14","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1256,"event":"readiness-timer-control","overrun_us":256,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ddb0f00558-5dc4-15","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2267,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2270,"event":"readiness-timer-control","overrun_us":1270,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ddb1059cb0-5dc4-16","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2042,"event":"readiness-timer-control","overrun_us":1042,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ddb133d4b8-5dc4-17","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":105008,"event":"readiness-timer-control","overrun_us":5008,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ddb15b1398-5dc4-18","within_five_ms":false}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101063,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101072,"event":"readiness-timer-control","overrun_us":1072,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ddb7a14718-5dc4-19","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101159,"event":"readiness-timer-control","overrun_us":1159,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ddbdba1968-5dc4-1a","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":505009,"event":"readiness-timer-control","overrun_us":5009,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ddc3d0c8d8-5dc4-1b","within_five_ms":false}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502063,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502073,"event":"readiness-timer-control","overrun_us":2073,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160dde1ee2a68-5dc4-1c","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502242,"event":"readiness-timer-control","overrun_us":2242,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ddffedd680-5dc4-1d","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1255,"event":"readiness-timer-control","overrun_us":255,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160de1df0f568-5dc4-1e","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2049,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2052,"event":"readiness-timer-control","overrun_us":1052,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160de1e069878-5dc4-1f","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2037,"event":"readiness-timer-control","overrun_us":1037,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160de1e3253c8-5dc4-20","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103946,"event":"readiness-timer-control","overrun_us":3946,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160de1e59e4b0-5dc4-21","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101092,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101099,"event":"readiness-timer-control","overrun_us":1099,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160de248f7a48-5dc4-22","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102051,"event":"readiness-timer-control","overrun_us":2051,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160de2aa5a8d0-5dc4-23","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500876,"event":"readiness-timer-control","overrun_us":876,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160de30c80488-5dc4-24","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501419,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501430,"event":"readiness-timer-control","overrun_us":1430,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160de4ea72498-5dc4-25","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501095,"event":"readiness-timer-control","overrun_us":1095,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160de6c9f0498-5dc4-26","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1257,"event":"readiness-timer-control","overrun_us":257,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160de8a8bd490-5dc4-27","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2070,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2072,"event":"readiness-timer-control","overrun_us":1072,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160de8aa1aa68-5dc4-28","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2267,"event":"readiness-timer-control","overrun_us":1267,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160de8acc38f0-5dc4-29","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":105007,"event":"readiness-timer-control","overrun_us":5007,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160de8af69c80-5dc4-2a","within_five_ms":false}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101133,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101140,"event":"readiness-timer-control","overrun_us":1140,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160de913cfee0-5dc4-2b","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101060,"event":"readiness-timer-control","overrun_us":1060,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160de9754e2e8-5dc4-2c","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504496,"event":"readiness-timer-control","overrun_us":4496,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160de9d677b78-5dc4-2d","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502045,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502058,"event":"readiness-timer-control","overrun_us":2058,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160debb7dc4a0-5dc4-2e","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501131,"event":"readiness-timer-control","overrun_us":1131,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ded97c7aa0-5dc4-2f","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1254,"event":"readiness-timer-control","overrun_us":254,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160def7686808-5dc4-30","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2266,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2268,"event":"readiness-timer-control","overrun_us":1268,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160def77df3a8-5dc4-31","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2266,"event":"readiness-timer-control","overrun_us":1266,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160def7abd5c0-5dc4-32","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103052,"event":"readiness-timer-control","overrun_us":3052,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160def7d68b58-5dc4-33","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102082,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102101,"event":"readiness-timer-control","overrun_us":2101,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160defdffe808-5dc4-34","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102067,"event":"readiness-timer-control","overrun_us":2067,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160df04275c70-5dc4-35","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504205,"event":"readiness-timer-control","overrun_us":4205,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160df0a4c8eb8-5dc4-36","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501091,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501101,"event":"readiness-timer-control","overrun_us":1101,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160df285e76c8-5dc4-37","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502078,"event":"readiness-timer-control","overrun_us":2078,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160df464cbdc0-5dc4-38","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1254,"event":"readiness-timer-control","overrun_us":254,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160df644783f0-5dc4-39","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2047,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2049,"event":"readiness-timer-control","overrun_us":1049,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160df645d1760-5dc4-3a","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2041,"event":"readiness-timer-control","overrun_us":1041,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160df64878a90-5dc4-3b","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102480,"event":"readiness-timer-control","overrun_us":2480,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160df64afa818-5dc4-3c","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102058,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102063,"event":"readiness-timer-control","overrun_us":2063,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160df6ad328c8-5dc4-3d","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102079,"event":"readiness-timer-control","overrun_us":2079,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160df70f92a18-5dc4-3e","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500418,"event":"readiness-timer-control","overrun_us":418,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160df771c25f8-5dc4-3f","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501653,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502032,"event":"readiness-timer-control","overrun_us":2032,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160df94f3fec0-5dc4-40","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502152,"event":"readiness-timer-control","overrun_us":2152,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160dfb2f49920-5dc4-41","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1256,"event":"readiness-timer-control","overrun_us":256,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160dfd0f28018-5dc4-42","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2076,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2082,"event":"readiness-timer-control","overrun_us":1082,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160dfd1087d00-5dc4-43","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2355,"event":"readiness-timer-control","overrun_us":1355,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160dfd1380110-5dc4-44","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101373,"event":"readiness-timer-control","overrun_us":1373,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160dfd16d9bb8-5dc4-45","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102219,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102229,"event":"readiness-timer-control","overrun_us":2229,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160dfd7806328-5dc4-46","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102057,"event":"readiness-timer-control","overrun_us":2057,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160dfde466950-5dc4-47","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502061,"event":"readiness-timer-control","overrun_us":2061,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160dfe473fc20-5dc4-48","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502062,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502083,"event":"readiness-timer-control","overrun_us":2083,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160e002642778-5dc4-49","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501948,"event":"readiness-timer-control","overrun_us":1948,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160e020627018-5dc4-4a","within_five_ms":true}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":57405,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5336,"slot":0,"token":"18e160e03e5f8fd8-5dc4-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e03e5f8fd8-5dc4-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5349,"bytes":57,"kind":"head","slot":0},{"at_us":5356,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":1407,"operation":"dispatch","outcome":"pending","slot":0,"start_us":622},{"abi_enter_us":1453,"abi_return_us":4347,"end_us":4350,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1452,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":5000,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4389},{"bytes":1,"capacity":1,"end_us":5033,"operation":"read","slot":0,"start_us":5026},{"capacity":1,"end_us":57329,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5054},{"end_us":57387,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57379}],"schema":2,"slots":[{"at_us":57387,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e03e5f8fd8-5dc4-4b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":55364,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5255,"slot":0,"token":"18e160e0435d64d8-5dc4-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e0435d64d8-5dc4-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5278,"bytes":57,"kind":"head","slot":0},{"at_us":5285,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":105,"abi_return_us":4985,"end_us":4995,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":105,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5052,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5045},{"abi_enter_us":5069,"abi_return_us":5077,"end_us":5077,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5069,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5112,"operation":"read","slot":0,"start_us":5109},{"abi_enter_us":5126,"abi_return_us":55260,"end_us":55265,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5126,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":55325,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":55320},{"end_us":55349,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55344}],"schema":2,"slots":[{"at_us":55349,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e0435d64d8-5dc4-4c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":57422,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5454,"slot":0,"token":"18e160e046d30b68-5dc4-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e046d30b68-5dc4-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5468,"bytes":57,"kind":"head","slot":0},{"at_us":5474,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":99,"abi_return_us":5198,"end_us":5202,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":99,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5247,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5238},{"abi_enter_us":5263,"abi_return_us":5269,"end_us":5270,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5262,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5299,"operation":"read","slot":0,"start_us":5297},{"abi_enter_us":5317,"abi_return_us":57316,"end_us":57319,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5317,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57383,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57378},{"end_us":57408,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57402}],"schema":2,"slots":[{"at_us":57408,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e046d30b68-5dc4-4d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":56511,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5249,"slot":0,"token":"18e160e04a6c2430-5dc4-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e04a6c2430-5dc4-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5261,"bytes":57,"kind":"head","slot":0},{"at_us":5267,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":102,"abi_return_us":4998,"end_us":5001,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5039,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5033},{"abi_enter_us":5050,"abi_return_us":5056,"end_us":5056,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5050,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5083,"operation":"read","slot":0,"start_us":5081},{"abi_enter_us":5096,"abi_return_us":56388,"end_us":56395,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5095,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":56470,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56465},{"end_us":56496,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56489}],"schema":2,"slots":[{"at_us":56496,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e04a6c2430-5dc4-4e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":57566,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6461,"slot":0,"token":"18e160e04df8ae20-5dc4-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e04df8ae20-5dc4-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6480,"bytes":57,"kind":"head","slot":0},{"at_us":6486,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":104,"operation":"handoff","start_us":104},{"abi_enter_us":122,"abi_return_us":5938,"end_us":5947,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":122,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6004,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5998},{"abi_enter_us":6020,"abi_return_us":6028,"end_us":6029,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6020,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6062,"operation":"read","slot":0,"start_us":6060},{"abi_enter_us":6074,"abi_return_us":57386,"end_us":57400,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6074,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":57501,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57493},{"end_us":57543,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57532}],"schema":2,"slots":[{"at_us":57543,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e04df8ae20-5dc4-4f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":500546,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5446,"slot":0,"token":"18e160e05194eb98-5dc4-50"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e05194eb98-5dc4-50","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":78,"abi_return_us":500467,"end_us":500478,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500527,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500527}],"schema":2,"slots":[{"at_us":500527,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e05194eb98-5dc4-50","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501403,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5435,"slot":0,"token":"18e160e06f8b8b48-5dc4-51"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e06f8b8b48-5dc4-51","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":71,"abi_return_us":501308,"end_us":501318,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501383,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501383}],"schema":2,"slots":[{"at_us":501383,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e06f8b8b48-5dc4-51","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501234,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5212,"slot":0,"token":"18e160e08d8ad5b8-5dc4-52"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e08d8ad5b8-5dc4-52","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":104,"abi_return_us":501128,"end_us":501134,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501216,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501216}],"schema":2,"slots":[{"at_us":501216,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e08d8ad5b8-5dc4-52","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501591,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6402,"slot":0,"token":"18e160e0ab87c698-5dc4-53"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e0ab87c698-5dc4-53","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":87,"abi_return_us":499367,"end_us":499375,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"abi_enter_us":499440,"abi_return_us":501524,"end_us":501525,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499440,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501553,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501553}],"schema":2,"slots":[{"at_us":501553,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e0ab87c698-5dc4-53","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501110,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5154,"slot":0,"token":"18e160e0c98dbff8-5dc4-54"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e0c98dbff8-5dc4-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":5162,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":65,"abi_return_us":4897,"end_us":4898,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4923,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4919},{"abi_enter_us":4934,"abi_return_us":501029,"end_us":501034,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4934,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501091,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501084}],"schema":2,"slots":[{"at_us":501091,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e0c98dbff8-5dc4-54","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501273,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6403,"slot":0,"token":"18e160e0e78cdf70-5dc4-55"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e0e78cdf70-5dc4-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":6411,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":73,"abi_return_us":6114,"end_us":6116,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6146,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6141},{"abi_enter_us":6160,"abi_return_us":501199,"end_us":501202,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6159,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501255,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501249}],"schema":2,"slots":[{"at_us":501255,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e0e78cdf70-5dc4-55","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":500696,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6394,"slot":0,"token":"18e160e10590d918-5dc4-56"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e10590d918-5dc4-56","watchdog":false,"workers_joined":1,"writes":[{"at_us":6404,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":72,"abi_return_us":6143,"end_us":6144,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6170,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6165},{"abi_enter_us":6180,"abi_return_us":500609,"end_us":500614,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6180,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500677,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500671}],"schema":2,"slots":[{"at_us":500677,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e10590d918-5dc4-56","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501059,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6144,"slot":0,"token":"18e160e12388a978-5dc4-57"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e12388a978-5dc4-57","watchdog":false,"workers_joined":1,"writes":[{"at_us":6152,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":88,"abi_return_us":5860,"end_us":5861,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5886,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5881},{"abi_enter_us":5898,"abi_return_us":500964,"end_us":500969,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5898,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501037,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501029}],"schema":2,"slots":[{"at_us":501037,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e12388a978-5dc4-57","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":501360,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5354,"slot":0,"token":"18e160e1418a6cb8-5dc4-58"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e1418a6cb8-5dc4-58","watchdog":false,"workers_joined":1,"writes":[{"at_us":5363,"bytes":57,"kind":"head","slot":0},{"at_us":5370,"bytes":1,"kind":"body","slot":0},{"at_us":310404,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":91,"abi_return_us":5089,"end_us":5092,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5133,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5128},{"abi_enter_us":5146,"abi_return_us":5149,"end_us":5150,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5146,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5177,"operation":"read","slot":0,"start_us":5174},{"abi_enter_us":5189,"abi_return_us":310147,"end_us":310150,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5189,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":310211,"operation":"read","slot":0,"start_us":310207},{"abi_enter_us":310223,"abi_return_us":501277,"end_us":501281,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310223,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":501340,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501333}],"schema":2,"slots":[{"at_us":501340,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e1418a6cb8-5dc4-58","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":500993,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5190,"slot":0,"token":"18e160e15f9330f0-5dc4-59"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e15f9330f0-5dc4-59","watchdog":false,"workers_joined":1,"writes":[{"at_us":5197,"bytes":57,"kind":"head","slot":0},{"at_us":5202,"bytes":1,"kind":"body","slot":0},{"at_us":310212,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":69,"abi_return_us":4892,"end_us":4893,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4918,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4913},{"abi_enter_us":4929,"abi_return_us":4932,"end_us":4933,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4929,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4956,"operation":"read","slot":0,"start_us":4955},{"abi_enter_us":4967,"abi_return_us":309913,"end_us":309916,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4967,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":309987,"operation":"read","slot":0,"start_us":309984},{"abi_enter_us":310003,"abi_return_us":500903,"end_us":500907,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310003,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":500972,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500965}],"schema":2,"slots":[{"at_us":500972,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e15f9330f0-5dc4-59","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":501165,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5453,"slot":0,"token":"18e160e17d98fb70-5dc4-5a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e17d98fb70-5dc4-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5462,"bytes":57,"kind":"head","slot":0},{"at_us":5468,"bytes":1,"kind":"body","slot":0},{"at_us":309326,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"abi_enter_us":87,"abi_return_us":5008,"end_us":5009,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5030,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5026},{"abi_enter_us":5041,"abi_return_us":5044,"end_us":5044,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5041,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5070,"operation":"read","slot":0,"start_us":5068},{"abi_enter_us":5080,"abi_return_us":308900,"end_us":308906,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5080,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":308968,"operation":"read","slot":0,"start_us":308965},{"abi_enter_us":308983,"abi_return_us":501071,"end_us":501075,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308983,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501132,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501125}],"schema":2,"slots":[{"at_us":501132,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e17d98fb70-5dc4-5a","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":502049,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5189,"slot":0,"token":"18e160e19ba87e38-5dc4-5b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e19ba87e38-5dc4-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5199,"bytes":57,"kind":"head","slot":0},{"at_us":5205,"bytes":1,"kind":"body","slot":0},{"at_us":306753,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"abi_enter_us":96,"abi_return_us":4926,"end_us":4927,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4954,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4946},{"abi_enter_us":4965,"abi_return_us":4968,"end_us":4969,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4965,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4993,"operation":"read","slot":0,"start_us":4991},{"abi_enter_us":5003,"abi_return_us":306504,"end_us":306513,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5003,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":306593,"operation":"read","slot":0,"start_us":306588},{"abi_enter_us":306609,"abi_return_us":501963,"end_us":501967,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306608,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":502028,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502021}],"schema":2,"slots":[{"at_us":502028,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e19ba87e38-5dc4-5b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":500797,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5272,"slot":0,"token":"18e160e1b9be7170-5dc4-5c"},{"path":"/readiness/siblings/1","received_us":5312,"slot":1,"token":"18e160e1b9be7170-5dc4-5c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e160e1b9be7170-5dc4-5c","watchdog":false,"workers_joined":2,"writes":[{"at_us":5285,"bytes":57,"kind":"head","slot":0},{"at_us":5320,"bytes":57,"kind":"head","slot":1},{"at_us":5325,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":102,"operation":"dispatch","outcome":"pending","slot":1,"start_us":81},{"abi_enter_us":127,"abi_return_us":5071,"end_us":5075,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":127,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5118,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5113},{"abi_enter_us":5132,"abi_return_us":5135,"end_us":5135,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5132,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5149,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5148},{"abi_enter_us":5158,"abi_return_us":5160,"end_us":5161,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5158,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5186,"operation":"read","slot":1,"start_us":5184},{"abi_enter_us":5197,"abi_return_us":5198,"end_us":5199,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5197,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5219,"operation":"read","slot":1,"start_us":5217},{"abi_enter_us":5228,"abi_return_us":5232,"end_us":5232,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5228,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5251,"operation":"read","slot":1,"start_us":5250},{"abi_enter_us":5260,"abi_return_us":5262,"end_us":5262,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5260,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5282,"operation":"read","slot":1,"start_us":5282},{"end_us":5294,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5291},{"abi_enter_us":5308,"abi_return_us":500672,"end_us":500678,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5308,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500755,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500746}],"schema":2,"slots":[{"at_us":500755,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5294,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e1b9be7170-5dc4-5c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":501820,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5284,"slot":1,"token":"18e160e1d7d172c0-5dc4-5d"},{"path":"/readiness/siblings/0","received_us":5291,"slot":0,"token":"18e160e1d7d172c0-5dc4-5d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e160e1d7d172c0-5dc4-5d","watchdog":false,"workers_joined":2,"writes":[{"at_us":5296,"bytes":57,"kind":"head","slot":1},{"at_us":5307,"bytes":57,"kind":"head","slot":0},{"at_us":11579,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":126,"operation":"dispatch","outcome":"pending","slot":1,"start_us":100},{"abi_enter_us":149,"abi_return_us":5005,"end_us":5008,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":149,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5047,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5042},{"abi_enter_us":5062,"abi_return_us":5064,"end_us":5065,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5062,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5080,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5079},{"abi_enter_us":5090,"abi_return_us":11273,"end_us":11274,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5090,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11324,"operation":"read","slot":1,"start_us":11320},{"abi_enter_us":11338,"abi_return_us":11342,"end_us":11342,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11338,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11366,"operation":"read","slot":1,"start_us":11365},{"end_us":11379,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11375},{"abi_enter_us":11397,"abi_return_us":501504,"end_us":501517,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11397,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501791,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501775}],"schema":2,"slots":[{"at_us":501791,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11379,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e1d7d172c0-5dc4-5d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":500374,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5469,"slot":1,"token":"18e160e1f5ead4e0-5dc4-5e"},{"path":"/readiness/siblings/0","received_us":5476,"slot":0,"token":"18e160e1f5ead4e0-5dc4-5e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160e1f5ead4e0-5dc4-5e","watchdog":false,"workers_joined":2,"writes":[{"at_us":5483,"bytes":57,"kind":"head","slot":1},{"at_us":5492,"bytes":57,"kind":"head","slot":0},{"at_us":10525,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":101,"operation":"dispatch","outcome":"pending","slot":1,"start_us":78},{"body_reads":0,"deadline_us":500000,"end_us":117,"operation":"handoff","start_us":117},{"abi_enter_us":129,"abi_return_us":5137,"end_us":5141,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":129,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5189,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5183},{"abi_enter_us":5203,"abi_return_us":5206,"end_us":5206,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5203,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5223,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5220},{"abi_enter_us":5235,"abi_return_us":10151,"end_us":10153,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5235,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":10192,"operation":"read","slot":1,"start_us":10189},{"abi_enter_us":10204,"abi_return_us":10206,"end_us":10207,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10204,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10228,"operation":"read","slot":1,"start_us":10227},{"abi_enter_us":10238,"abi_return_us":10239,"end_us":10239,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10238,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10261,"operation":"read","slot":1,"start_us":10261},{"abi_enter_us":10271,"abi_return_us":10273,"end_us":10273,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10271,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":1,"end_us":10296,"operation":"read","slot":1,"start_us":10296},{"end_us":10311,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10307},{"abi_enter_us":10325,"abi_return_us":500288,"end_us":500294,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":10325,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":500356,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500350}],"schema":2,"slots":[{"at_us":500356,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":10311,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e1f5ead4e0-5dc4-5e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":501643,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6433,"slot":1,"token":"18e160e214076b50-5dc4-5f"},{"path":"/readiness/siblings/0","received_us":6439,"slot":0,"token":"18e160e214076b50-5dc4-5f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160e214076b50-5dc4-5f","watchdog":false,"workers_joined":2,"writes":[{"at_us":6442,"bytes":57,"kind":"head","slot":1},{"at_us":6448,"bytes":57,"kind":"head","slot":0},{"at_us":12708,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":89,"operation":"dispatch","outcome":"pending","slot":1,"start_us":66},{"body_reads":0,"deadline_us":500000,"end_us":104,"operation":"handoff","start_us":104},{"abi_enter_us":116,"abi_return_us":6175,"end_us":6177,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6201,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6195},{"abi_enter_us":6213,"abi_return_us":6215,"end_us":6215,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6213,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6229,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6229},{"abi_enter_us":6240,"abi_return_us":12432,"end_us":12433,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6240,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":12464,"operation":"read","slot":1,"start_us":12462},{"abi_enter_us":12477,"abi_return_us":12480,"end_us":12480,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12477,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":16384,"end_us":12508,"operation":"read","slot":1,"start_us":12507},{"end_us":12520,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12518},{"abi_enter_us":12533,"abi_return_us":501540,"end_us":501547,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12533,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":501624,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501616}],"schema":2,"slots":[{"at_us":501624,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12520,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e214076b50-5dc4-5f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":501192,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5364,"slot":0,"token":"18e160e23233a5a8-5dc4-60"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e23233a5a8-5dc4-60","watchdog":false,"workers_joined":1,"writes":[{"at_us":5382,"bytes":57,"kind":"head","slot":0},{"at_us":5387,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":74,"abi_return_us":5071,"end_us":5074,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5114,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5109},{"abi_enter_us":5128,"abi_return_us":5135,"end_us":5135,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5127,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5161,"operation":"read","slot":0,"start_us":5159},{"abi_enter_us":5176,"abi_return_us":501102,"end_us":501110,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5176,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501171,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501163}],"schema":2,"slots":[{"at_us":501171,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e23233a5a8-5dc4-60","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":500271,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5281,"slot":0,"token":"18e160e2503e7550-5dc4-61"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e2503e7550-5dc4-61","watchdog":false,"workers_joined":1,"writes":[{"at_us":5295,"bytes":57,"kind":"head","slot":0},{"at_us":5301,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":77,"abi_return_us":4990,"end_us":4993,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5025,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5020},{"abi_enter_us":5040,"abi_return_us":5045,"end_us":5045,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5039,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5080,"operation":"read","slot":0,"start_us":5078},{"abi_enter_us":5094,"abi_return_us":500180,"end_us":500186,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5093,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500250,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500243}],"schema":2,"slots":[{"at_us":500250,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2503e7550-5dc4-61","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":500365,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6396,"slot":0,"token":"18e160e26e3b3750-5dc4-62"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e26e3b3750-5dc4-62","watchdog":false,"workers_joined":1,"writes":[{"at_us":6405,"bytes":57,"kind":"head","slot":0},{"at_us":6409,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":76,"operation":"handoff","start_us":76},{"abi_enter_us":91,"abi_return_us":6144,"end_us":6145,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6171,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6166},{"abi_enter_us":6185,"abi_return_us":6189,"end_us":6190,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6184,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6218,"operation":"read","slot":0,"start_us":6216},{"abi_enter_us":6231,"abi_return_us":500274,"end_us":500279,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6231,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500344,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500338}],"schema":2,"slots":[{"at_us":500344,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e26e3b3750-5dc4-62","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":501377,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5805,"slot":0,"token":"18e160e28c3fd508-5dc4-63"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e28c3fd508-5dc4-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":5813,"bytes":57,"kind":"head","slot":0},{"at_us":5819,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"abi_enter_us":102,"abi_return_us":5525,"end_us":5527,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5551,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5546},{"abi_enter_us":5564,"abi_return_us":5570,"end_us":5570,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5564,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5598,"operation":"read","slot":0,"start_us":5596},{"abi_enter_us":5610,"abi_return_us":501285,"end_us":501291,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5610,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501356,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501349}],"schema":2,"slots":[{"at_us":501356,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e28c3fd508-5dc4-63","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":5799,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5783,"slot":0,"token":"18e160e2aa50c318-5dc4-64"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2aa50c318-5dc4-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":5796,"bytes":57,"kind":"head","slot":0},{"at_us":5803,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":69,"abi_return_us":5561,"end_us":5565,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5617,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5611},{"abi_enter_us":5633,"abi_return_us":5638,"end_us":5638,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5633,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5669,"operation":"read","slot":0,"start_us":5666},{"abi_enter_us":5682,"abi_return_us":5683,"end_us":5683,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5682,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5705,"operation":"read","slot":0,"start_us":5704},{"abi_enter_us":5718,"abi_return_us":5719,"end_us":5719,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5718,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5740,"operation":"read","slot":0,"start_us":5739},{"abi_enter_us":5750,"abi_return_us":5752,"end_us":5752,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5750,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5772,"operation":"read","slot":0,"start_us":5772},{"end_us":5786,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5781}],"schema":2,"slots":[{"at_us":5786,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2aa50c318-5dc4-64","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":5747,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5797,"slot":0,"token":"18e160e2aadb9240-5dc4-65"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2aadb9240-5dc4-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":5805,"bytes":57,"kind":"head","slot":0},{"at_us":5810,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":57,"abi_return_us":5601,"end_us":5604,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5633,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5629},{"abi_enter_us":5646,"abi_return_us":5653,"end_us":5653,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5646,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5681,"operation":"read","slot":0,"start_us":5678},{"abi_enter_us":5694,"abi_return_us":5696,"end_us":5696,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5694,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5722,"operation":"read","slot":0,"start_us":5722},{"end_us":5735,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5733}],"schema":2,"slots":[{"at_us":5735,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2aadb9240-5dc4-65","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":5163,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5175,"slot":0,"token":"18e160e2ab5d2238-5dc4-66"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2ab5d2238-5dc4-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":5186,"bytes":57,"kind":"head","slot":0},{"at_us":5191,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"abi_enter_us":65,"abi_return_us":4932,"end_us":4934,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4961,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4957},{"abi_enter_us":4976,"abi_return_us":4979,"end_us":4980,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4975,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5008,"operation":"read","slot":0,"start_us":5007},{"abi_enter_us":5020,"abi_return_us":5021,"end_us":5021,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5020,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5047,"operation":"read","slot":0,"start_us":5046},{"abi_enter_us":5059,"abi_return_us":5060,"end_us":5060,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5059,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5081,"operation":"read","slot":0,"start_us":5080},{"abi_enter_us":5092,"abi_return_us":5098,"end_us":5098,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5092,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5135,"operation":"read","slot":0,"start_us":5135},{"end_us":5147,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5145}],"schema":2,"slots":[{"at_us":5147,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2ab5d2238-5dc4-66","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":6057,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6115,"slot":0,"token":"18e160e2abe3b370-5dc4-67"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2abe3b370-5dc4-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":6126,"bytes":57,"kind":"head","slot":0},{"at_us":6132,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"abi_enter_us":78,"abi_return_us":5911,"end_us":5914,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5950,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5945},{"abi_enter_us":5963,"abi_return_us":5968,"end_us":5968,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5963,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5996,"operation":"read","slot":0,"start_us":5994},{"abi_enter_us":6009,"abi_return_us":6011,"end_us":6011,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6009,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6032,"operation":"read","slot":0,"start_us":6032},{"end_us":6044,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6041}],"schema":2,"slots":[{"at_us":6044,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2abe3b370-5dc4-67","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6266,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6396,"slot":0,"token":"18e160e2ac777790-5dc4-68"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2ac777790-5dc4-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":6404,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":61,"abi_return_us":6183,"end_us":6185,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6206,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6203},{"abi_enter_us":6217,"abi_return_us":6219,"end_us":6219,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6217,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6242,"operation":"read","slot":0,"start_us":6241},{"end_us":6254,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6252}],"schema":2,"slots":[{"at_us":6254,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2ac777790-5dc4-68","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6378,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6414,"slot":0,"token":"18e160e2acfaa980-5dc4-69"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2acfaa980-5dc4-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":6425,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":51,"abi_return_us":6256,"end_us":6259,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6304,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6299},{"abi_enter_us":6318,"abi_return_us":6321,"end_us":6322,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6318,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6351,"operation":"read","slot":0,"start_us":6347},{"end_us":6365,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6362}],"schema":2,"slots":[{"at_us":6365,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2acfaa980-5dc4-69","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5060,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5199,"slot":0,"token":"18e160e2ad7ec1e8-5dc4-6a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2ad7ec1e8-5dc4-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5206,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":54,"operation":"handoff","start_us":54},{"abi_enter_us":71,"abi_return_us":4982,"end_us":4983,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5005,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5002},{"abi_enter_us":5016,"abi_return_us":5017,"end_us":5017,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5015,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5038,"operation":"read","slot":0,"start_us":5037},{"end_us":5049,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5047}],"schema":2,"slots":[{"at_us":5049,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2ad7ec1e8-5dc4-6a","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5421,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5461,"slot":0,"token":"18e160e2adef2b40-5dc4-6b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2adef2b40-5dc4-6b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5480,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":47,"operation":"handoff","start_us":47},{"abi_enter_us":64,"abi_return_us":5322,"end_us":5326,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5355,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5352},{"abi_enter_us":5367,"abi_return_us":5369,"end_us":5370,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5367,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5396,"operation":"read","slot":0,"start_us":5394},{"end_us":5409,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5407}],"schema":2,"slots":[{"at_us":5409,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2adef2b40-5dc4-6b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5300,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5355,"slot":0,"token":"18e160e2ae63c6d0-5dc4-6c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2ae63c6d0-5dc4-6c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5365,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":53,"abi_return_us":5244,"end_us":5247,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5274,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5270},{"end_us":5286,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5286}],"schema":2,"slots":[{"at_us":5286,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e2ae63c6d0-5dc4-6c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5596,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5702,"slot":0,"token":"18e160e2aed43410-5dc4-6d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2aed43410-5dc4-6d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5720,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":43,"abi_return_us":5546,"end_us":5548,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5569,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5566},{"end_us":5583,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5583}],"schema":2,"slots":[{"at_us":5583,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e2aed43410-5dc4-6d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5549,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5662,"slot":0,"token":"18e160e2af47f8c8-5dc4-6e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2af47f8c8-5dc4-6e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5670,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"abi_enter_us":61,"abi_return_us":5496,"end_us":5498,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5520,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5518},{"end_us":5536,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5536}],"schema":2,"slots":[{"at_us":5536,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e2af47f8c8-5dc4-6e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5366,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5385,"slot":0,"token":"18e160e2afbd7eb8-5dc4-6f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2afbd7eb8-5dc4-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5401,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"body_reads":0,"deadline_us":500000,"end_us":46,"operation":"handoff","start_us":46},{"abi_enter_us":56,"abi_return_us":5296,"end_us":5300,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5333,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5328},{"end_us":5351,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5351}],"schema":2,"slots":[{"at_us":5351,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e2afbd7eb8-5dc4-6f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":61387,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6425,"slot":0,"token":"18e160e2b0317638-5dc4-70"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2b0317638-5dc4-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":6435,"bytes":66,"kind":"head","slot":0},{"at_us":6440,"bytes":6,"kind":"body","slot":0},{"at_us":61490,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":62,"abi_return_us":6190,"end_us":6192,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6226,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6221},{"abi_enter_us":6242,"abi_return_us":6245,"end_us":6246,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6242,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6272,"operation":"read","slot":0,"start_us":6270},{"abi_enter_us":6284,"abi_return_us":61268,"end_us":61283,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6284,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":61347,"error":"Other","operation":"read","slot":0,"start_us":61334},{"end_us":61372,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61365}],"schema":2,"slots":[{"at_us":61372,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2b0317638-5dc4-70","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58565,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6428,"slot":0,"token":"18e160e2b407bb28-5dc4-71"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2b407bb28-5dc4-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":6441,"bytes":66,"kind":"head","slot":0},{"at_us":6446,"bytes":6,"kind":"body","slot":0},{"at_us":58685,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":58,"abi_return_us":6209,"end_us":6213,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6247,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6242},{"abi_enter_us":6258,"abi_return_us":6264,"end_us":6264,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6258,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6294,"operation":"read","slot":0,"start_us":6291},{"abi_enter_us":6306,"abi_return_us":58451,"end_us":58454,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6306,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":58521,"error":"Other","operation":"read","slot":0,"start_us":58516},{"end_us":58548,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58542}],"schema":2,"slots":[{"at_us":58548,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2b407bb28-5dc4-71","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57845,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5359,"slot":0,"token":"18e160e2b7d17528-5dc4-72"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2b7d17528-5dc4-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":5367,"bytes":66,"kind":"head","slot":0},{"at_us":5373,"bytes":6,"kind":"body","slot":0},{"at_us":57940,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"abi_enter_us":96,"abi_return_us":5106,"end_us":5109,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5144,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5138},{"abi_enter_us":5161,"abi_return_us":5167,"end_us":5167,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5161,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5194,"operation":"read","slot":0,"start_us":5191},{"abi_enter_us":5206,"abi_return_us":57712,"end_us":57716,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5206,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":57795,"error":"Other","operation":"read","slot":0,"start_us":57790},{"end_us":57823,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57816}],"schema":2,"slots":[{"at_us":57823,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2b7d17528-5dc4-72","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":60852,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5916,"slot":0,"token":"18e160e2bb95a918-5dc4-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2bb95a918-5dc4-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":5923,"bytes":66,"kind":"head","slot":0},{"at_us":5929,"bytes":6,"kind":"body","slot":0},{"at_us":60962,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"abi_enter_us":81,"abi_return_us":5692,"end_us":5693,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5729,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5725},{"abi_enter_us":5745,"abi_return_us":5750,"end_us":5751,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5745,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5785,"operation":"read","slot":0,"start_us":5783},{"abi_enter_us":5797,"abi_return_us":60736,"end_us":60745,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5797,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":60809,"error":"Other","operation":"read","slot":0,"start_us":60802},{"end_us":60831,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60826}],"schema":2,"slots":[{"at_us":60831,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2bb95a918-5dc4-73","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":6260,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6421,"slot":0,"token":"18e160e2bf68e0c8-5dc4-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2bf68e0c8-5dc4-74","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":69,"abi_return_us":6204,"end_us":6206,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6230,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6225},{"end_us":6243,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6243}],"schema":2,"slots":[{"at_us":6243,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e2bf68e0c8-5dc4-74","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":6434,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6436,"slot":0,"token":"18e160e2bfe8f9c0-5dc4-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2bfe8f9c0-5dc4-75","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":55,"abi_return_us":6305,"end_us":6310,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6399,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6387},{"end_us":6421,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6421}],"schema":2,"slots":[{"at_us":6421,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e2bfe8f9c0-5dc4-75","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5249,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5356,"slot":0,"token":"18e160e2c069ca50-5dc4-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2c069ca50-5dc4-76","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":92,"abi_return_us":5179,"end_us":5185,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5221,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5216},{"end_us":5235,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5234}],"schema":2,"slots":[{"at_us":5235,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e2c069ca50-5dc4-76","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5035,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5234,"slot":0,"token":"18e160e2c0ec5448-5dc4-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2c0ec5448-5dc4-77","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":98,"operation":"handoff","start_us":98},{"abi_enter_us":110,"abi_return_us":4975,"end_us":4978,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5007,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5002},{"end_us":5020,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5020}],"schema":2,"slots":[{"at_us":5020,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e2c0ec5448-5dc4-77","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":58965,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5225,"slot":0,"token":"18e160e2c1653980-5dc4-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2c1653980-5dc4-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":5237,"bytes":57,"kind":"head","slot":0},{"at_us":5242,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":75,"abi_return_us":4932,"end_us":4935,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4980,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4961},{"bytes":1,"capacity":1,"end_us":5029,"operation":"read","slot":0,"start_us":5021},{"capacity":1,"end_us":58900,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5057},{"end_us":58946,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58940}],"schema":2,"slots":[{"at_us":58946,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2c1653980-5dc4-78","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":58291,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6114,"slot":0,"token":"18e160e2c52d5958-5dc4-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2c52d5958-5dc4-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":6125,"bytes":57,"kind":"head","slot":0},{"at_us":6130,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":74,"abi_return_us":5899,"end_us":5903,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5940,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5934},{"abi_enter_us":5956,"abi_return_us":5959,"end_us":5960,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5955,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5985,"operation":"read","slot":0,"start_us":5983},{"abi_enter_us":5997,"abi_return_us":58196,"end_us":58199,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5997,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":58256,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58251},{"end_us":58275,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58270}],"schema":2,"slots":[{"at_us":58275,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2c52d5958-5dc4-79","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":58791,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6440,"slot":0,"token":"18e160e2c8e43f08-5dc4-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2c8e43f08-5dc4-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6455,"bytes":57,"kind":"head","slot":0},{"at_us":6460,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":66,"abi_return_us":6216,"end_us":6219,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6255,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6249},{"abi_enter_us":6267,"abi_return_us":6272,"end_us":6272,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6267,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6298,"operation":"read","slot":0,"start_us":6296},{"abi_enter_us":6321,"abi_return_us":58684,"end_us":58693,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6320,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":58756,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58751},{"end_us":58778,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58773}],"schema":2,"slots":[{"at_us":58778,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2c8e43f08-5dc4-7a","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":56625,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5187,"slot":0,"token":"18e160e2cc914948-5dc4-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2cc914948-5dc4-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5202,"bytes":57,"kind":"head","slot":0},{"at_us":5208,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":84,"abi_return_us":4991,"end_us":4996,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5033,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5028},{"abi_enter_us":5046,"abi_return_us":5050,"end_us":5051,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5046,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5078,"operation":"read","slot":0,"start_us":5076},{"abi_enter_us":5090,"abi_return_us":56510,"end_us":56514,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5090,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":56585,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56580},{"end_us":56609,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56602}],"schema":2,"slots":[{"at_us":56609,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2cc914948-5dc4-7b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":57356,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5240,"slot":0,"token":"18e160e2d02cc370-5dc4-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e2d02cc370-5dc4-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5252,"bytes":57,"kind":"head","slot":0},{"at_us":5257,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":90,"operation":"handoff","start_us":90},{"abi_enter_us":106,"abi_return_us":4922,"end_us":4926,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4971,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4964},{"abi_enter_us":4985,"abi_return_us":4990,"end_us":4991,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4985,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5021,"operation":"read","slot":0,"start_us":5018},{"abi_enter_us":5033,"abi_return_us":57256,"end_us":57259,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5033,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57317,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57312},{"end_us":57341,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57333}],"schema":2,"slots":[{"at_us":57341,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e2d02cc370-5dc4-7c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":500530,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5208,"slot":0,"token":"18e160e2d3c10dc0-5dc4-7d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e2d3c10dc0-5dc4-7d","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":62,"abi_return_us":500424,"end_us":500433,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500505,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500505}],"schema":2,"slots":[{"at_us":500505,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e2d3c10dc0-5dc4-7d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501136,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6494,"slot":0,"token":"18e160e2f1c00df8-5dc4-7e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e2f1c00df8-5dc4-7e","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"abi_enter_us":106,"abi_return_us":501053,"end_us":501063,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501119,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501119}],"schema":2,"slots":[{"at_us":501119,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e2f1c00df8-5dc4-7e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":500602,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6393,"slot":0,"token":"18e160e30fbd1648-5dc4-7f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e30fbd1648-5dc4-7f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"abi_enter_us":89,"abi_return_us":500375,"end_us":500386,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500556,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500556}],"schema":2,"slots":[{"at_us":500556,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e30fbd1648-5dc4-7f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":500458,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5172,"slot":0,"token":"18e160e32db22b70-5dc4-80"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e32db22b70-5dc4-80","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"abi_enter_us":102,"abi_return_us":500378,"end_us":500386,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500437,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500437}],"schema":2,"slots":[{"at_us":500437,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e32db22b70-5dc4-80","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":500960,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5602,"slot":0,"token":"18e160e34ba81770-5dc4-81"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e34ba81770-5dc4-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":5615,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":68,"abi_return_us":5361,"end_us":5363,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5400,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5389},{"abi_enter_us":5418,"abi_return_us":500855,"end_us":500864,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5418,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500938,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500930}],"schema":2,"slots":[{"at_us":500938,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e34ba81770-5dc4-81","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":500693,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6016,"slot":0,"token":"18e160e369abc2f8-5dc4-82"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e369abc2f8-5dc4-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":6030,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":81,"abi_return_us":5712,"end_us":5714,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5751,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5746},{"abi_enter_us":5767,"abi_return_us":500602,"end_us":500613,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5767,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500672,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500664}],"schema":2,"slots":[{"at_us":500672,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e369abc2f8-5dc4-82","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501427,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6432,"slot":0,"token":"18e160e387a8fa28-5dc4-83"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e387a8fa28-5dc4-83","watchdog":false,"workers_joined":1,"writes":[{"at_us":6449,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":98,"abi_return_us":6199,"end_us":6203,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6250,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6244},{"abi_enter_us":6266,"abi_return_us":501326,"end_us":501333,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6266,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501403,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501396}],"schema":2,"slots":[{"at_us":501403,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e387a8fa28-5dc4-83","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":500579,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5300,"slot":0,"token":"18e160e3a5b24b00-5dc4-84"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e3a5b24b00-5dc4-84","watchdog":false,"workers_joined":1,"writes":[{"at_us":5317,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"abi_enter_us":95,"abi_return_us":5101,"end_us":5106,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5154,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5148},{"abi_enter_us":5173,"abi_return_us":500472,"end_us":500479,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5173,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500553,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500545}],"schema":2,"slots":[{"at_us":500553,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e3a5b24b00-5dc4-84","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":501045,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5337,"slot":0,"token":"18e160e3c3b08fb8-5dc4-85"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e3c3b08fb8-5dc4-85","watchdog":false,"workers_joined":1,"writes":[{"at_us":5345,"bytes":57,"kind":"head","slot":0},{"at_us":5351,"bytes":1,"kind":"body","slot":0},{"at_us":308805,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":75,"abi_return_us":5111,"end_us":5113,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5141,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5137},{"abi_enter_us":5154,"abi_return_us":5158,"end_us":5158,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5154,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5185,"operation":"read","slot":0,"start_us":5181},{"abi_enter_us":5198,"abi_return_us":308578,"end_us":308584,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5198,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":308656,"operation":"read","slot":0,"start_us":308653},{"abi_enter_us":308676,"abi_return_us":500950,"end_us":500954,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308676,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501020,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501013}],"schema":2,"slots":[{"at_us":501020,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e3c3b08fb8-5dc4-85","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":501366,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6237,"slot":0,"token":"18e160e3e1c30468-5dc4-86"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e3e1c30468-5dc4-86","watchdog":false,"workers_joined":1,"writes":[{"at_us":6250,"bytes":57,"kind":"head","slot":0},{"at_us":6256,"bytes":1,"kind":"body","slot":0},{"at_us":307195,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":69,"abi_return_us":5923,"end_us":5927,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5968,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5962},{"abi_enter_us":5982,"abi_return_us":5986,"end_us":5987,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5982,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":6017,"operation":"read","slot":0,"start_us":6015},{"abi_enter_us":6030,"abi_return_us":306846,"end_us":306852,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6030,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":306941,"operation":"read","slot":0,"start_us":306934},{"abi_enter_us":306956,"abi_return_us":501231,"end_us":501247,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306956,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":501337,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501325}],"schema":2,"slots":[{"at_us":501337,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e3e1c30468-5dc4-86","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":501192,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6433,"slot":0,"token":"18e160e3ffd7ab98-5dc4-87"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e3ffd7ab98-5dc4-87","watchdog":false,"workers_joined":1,"writes":[{"at_us":6448,"bytes":57,"kind":"head","slot":0},{"at_us":6454,"bytes":1,"kind":"body","slot":0},{"at_us":308377,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":75,"operation":"handoff","start_us":75},{"abi_enter_us":90,"abi_return_us":5977,"end_us":5981,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6027,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6022},{"abi_enter_us":6067,"abi_return_us":6073,"end_us":6074,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6067,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6105,"operation":"read","slot":0,"start_us":6103},{"abi_enter_us":6117,"abi_return_us":307902,"end_us":307909,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6117,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":308000,"operation":"read","slot":0,"start_us":307997},{"abi_enter_us":308020,"abi_return_us":501077,"end_us":501080,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308020,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501154,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501143}],"schema":2,"slots":[{"at_us":501154,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e3ffd7ab98-5dc4-87","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":501450,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6416,"slot":0,"token":"18e160e41e0b7f40-5dc4-88"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e41e0b7f40-5dc4-88","watchdog":false,"workers_joined":1,"writes":[{"at_us":6428,"bytes":57,"kind":"head","slot":0},{"at_us":6440,"bytes":1,"kind":"body","slot":0},{"at_us":306500,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":117,"abi_return_us":6134,"end_us":6140,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":117,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6184,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6179},{"abi_enter_us":6199,"abi_return_us":6209,"end_us":6209,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6199,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6240,"operation":"read","slot":0,"start_us":6238},{"abi_enter_us":6253,"abi_return_us":306174,"end_us":306181,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6253,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":306265,"operation":"read","slot":0,"start_us":306261},{"abi_enter_us":306288,"abi_return_us":501346,"end_us":501350,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306288,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":501417,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501410}],"schema":2,"slots":[{"at_us":501417,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e41e0b7f40-5dc4-88","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":501591,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6214,"slot":1,"token":"18e160e43c333170-5dc4-89"},{"path":"/readiness/siblings/0","received_us":6221,"slot":0,"token":"18e160e43c333170-5dc4-89"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160e43c333170-5dc4-89","watchdog":false,"workers_joined":2,"writes":[{"at_us":6237,"bytes":57,"kind":"head","slot":0},{"at_us":6247,"bytes":57,"kind":"head","slot":1},{"at_us":6256,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":788,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":987,"operation":"dispatch","outcome":"pending","slot":1,"start_us":940},{"abi_enter_us":1018,"abi_return_us":5642,"end_us":5647,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":1018,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":5692,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5686},{"abi_enter_us":5705,"abi_return_us":5707,"end_us":5708,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5705,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5725,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5724},{"abi_enter_us":5736,"abi_return_us":5740,"end_us":5740,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5736,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5766,"operation":"read","slot":1,"start_us":5763},{"abi_enter_us":5778,"abi_return_us":5779,"end_us":5779,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5778,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5804,"operation":"read","slot":1,"start_us":5803},{"abi_enter_us":5814,"abi_return_us":5815,"end_us":5816,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5814,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5836,"operation":"read","slot":1,"start_us":5836},{"abi_enter_us":5848,"abi_return_us":5851,"end_us":5851,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5848,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5875,"operation":"read","slot":1,"start_us":5874},{"end_us":5888,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5885},{"abi_enter_us":5901,"abi_return_us":501409,"end_us":501430,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5901,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501551,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501528}],"schema":2,"slots":[{"at_us":501551,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5888,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e43c333170-5dc4-89","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":502004,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5560,"slot":0,"token":"18e160e45a907290-5dc4-8a"},{"path":"/readiness/siblings/1","received_us":5602,"slot":1,"token":"18e160e45a907290-5dc4-8a"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e160e45a907290-5dc4-8a","watchdog":false,"workers_joined":2,"writes":[{"at_us":5575,"bytes":57,"kind":"head","slot":0},{"at_us":5615,"bytes":57,"kind":"head","slot":1},{"at_us":5628,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":89,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"end_us":174,"operation":"dispatch","outcome":"pending","slot":1,"start_us":157},{"abi_enter_us":191,"abi_return_us":5268,"end_us":5272,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":191,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5317,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5310},{"abi_enter_us":5332,"abi_return_us":5334,"end_us":5335,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5332,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5354,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5353},{"abi_enter_us":5365,"abi_return_us":5368,"end_us":5369,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5365,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5396,"operation":"read","slot":1,"start_us":5394},{"abi_enter_us":5411,"abi_return_us":5414,"end_us":5414,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5411,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5438,"operation":"read","slot":1,"start_us":5438},{"end_us":5452,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5448},{"abi_enter_us":7793,"abi_return_us":501082,"end_us":501096,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":7793,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":501952,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501939}],"schema":2,"slots":[{"at_us":501952,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5452,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e45a907290-5dc4-8a","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":501361,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6480,"slot":0,"token":"18e160e478bd9f18-5dc4-8b"},{"path":"/readiness/siblings/1","received_us":6486,"slot":1,"token":"18e160e478bd9f18-5dc4-8b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160e478bd9f18-5dc4-8b","watchdog":false,"workers_joined":2,"writes":[{"at_us":6498,"bytes":57,"kind":"head","slot":1},{"at_us":6506,"bytes":57,"kind":"head","slot":0},{"at_us":12238,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":139,"operation":"dispatch","outcome":"pending","slot":1,"start_us":112},{"body_reads":0,"deadline_us":500000,"end_us":167,"operation":"handoff","start_us":167},{"abi_enter_us":181,"abi_return_us":6204,"end_us":6210,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":181,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6265,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6260},{"abi_enter_us":6294,"abi_return_us":6296,"end_us":6297,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6294,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6314,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6311},{"abi_enter_us":6325,"abi_return_us":11920,"end_us":11921,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6325,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":11959,"operation":"read","slot":1,"start_us":11953},{"abi_enter_us":11974,"abi_return_us":11977,"end_us":11977,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11974,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":12004,"operation":"read","slot":1,"start_us":12002},{"abi_enter_us":12016,"abi_return_us":12018,"end_us":12018,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12016,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12045,"operation":"read","slot":1,"start_us":12044},{"abi_enter_us":12055,"abi_return_us":12058,"end_us":12058,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12055,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":1,"end_us":12079,"operation":"read","slot":1,"start_us":12079},{"end_us":12094,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12088},{"abi_enter_us":12108,"abi_return_us":501237,"end_us":501243,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12107,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":501318,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501311}],"schema":2,"slots":[{"at_us":501318,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12094,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e478bd9f18-5dc4-8b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":500375,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6214,"slot":0,"token":"18e160e496e99af0-5dc4-8c"},{"path":"/readiness/siblings/1","received_us":6221,"slot":1,"token":"18e160e496e99af0-5dc4-8c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160e496e99af0-5dc4-8c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6225,"bytes":57,"kind":"head","slot":0},{"at_us":6231,"bytes":57,"kind":"head","slot":1},{"at_us":6236,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":118,"operation":"dispatch","outcome":"pending","slot":1,"start_us":92},{"body_reads":0,"deadline_us":500000,"end_us":139,"operation":"handoff","start_us":139},{"abi_enter_us":153,"abi_return_us":5880,"end_us":5884,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":153,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5913,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5908},{"abi_enter_us":5926,"abi_return_us":5928,"end_us":5929,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5926,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5943,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5942},{"abi_enter_us":5957,"abi_return_us":5959,"end_us":5960,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5956,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5984,"operation":"read","slot":1,"start_us":5982},{"abi_enter_us":5995,"abi_return_us":5997,"end_us":5998,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5995,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":6022,"operation":"read","slot":1,"start_us":6021},{"end_us":6034,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6031},{"abi_enter_us":6046,"abi_return_us":500209,"end_us":500232,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6046,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500350,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500333}],"schema":2,"slots":[{"at_us":500350,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6034,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e496e99af0-5dc4-8c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":501365,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5635,"slot":0,"token":"18e160e4b512a8c8-5dc4-8d"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e4b512a8c8-5dc4-8d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5652,"bytes":57,"kind":"head","slot":0},{"at_us":5657,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"abi_enter_us":114,"abi_return_us":5229,"end_us":5235,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":113,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5293,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5287},{"abi_enter_us":5309,"abi_return_us":5313,"end_us":5314,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5308,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5344,"operation":"read","slot":0,"start_us":5341},{"abi_enter_us":5357,"abi_return_us":501273,"end_us":501281,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5357,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501345,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501338}],"schema":2,"slots":[{"at_us":501345,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e4b512a8c8-5dc4-8d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":501446,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6435,"slot":0,"token":"18e160e4d32f7db8-5dc4-8e"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e4d32f7db8-5dc4-8e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6451,"bytes":57,"kind":"head","slot":0},{"at_us":6455,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":74,"abi_return_us":6187,"end_us":6192,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6247,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6237},{"abi_enter_us":6263,"abi_return_us":6269,"end_us":6269,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6263,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6305,"operation":"read","slot":0,"start_us":6302},{"abi_enter_us":6319,"abi_return_us":501347,"end_us":501356,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6319,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501423,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501412}],"schema":2,"slots":[{"at_us":501423,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e4d32f7db8-5dc4-8e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":500705,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5326,"slot":0,"token":"18e160e4f1647270-5dc4-8f"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e4f1647270-5dc4-8f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5341,"bytes":57,"kind":"head","slot":0},{"at_us":5347,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":92,"operation":"handoff","start_us":92},{"abi_enter_us":105,"abi_return_us":5065,"end_us":5070,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":105,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5122,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5115},{"abi_enter_us":5168,"abi_return_us":5186,"end_us":5188,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5168,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5243,"operation":"read","slot":0,"start_us":5240},{"abi_enter_us":5257,"abi_return_us":500604,"end_us":500611,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5257,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500676,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500667}],"schema":2,"slots":[{"at_us":500676,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e4f1647270-5dc4-8f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":500996,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6428,"slot":0,"token":"18e160e50f75e550-5dc4-90"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e50f75e550-5dc4-90","watchdog":false,"workers_joined":1,"writes":[{"at_us":6442,"bytes":57,"kind":"head","slot":0},{"at_us":6454,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":81,"operation":"handoff","start_us":81},{"abi_enter_us":94,"abi_return_us":6117,"end_us":6121,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6159,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6153},{"abi_enter_us":6175,"abi_return_us":6180,"end_us":6180,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6175,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6211,"operation":"read","slot":0,"start_us":6208},{"abi_enter_us":6223,"abi_return_us":500904,"end_us":500910,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6223,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500975,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500968}],"schema":2,"slots":[{"at_us":500975,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e50f75e550-5dc4-90","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":6024,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6048,"slot":0,"token":"18e160e52d8c5970-5dc4-91"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e52d8c5970-5dc4-91","watchdog":false,"workers_joined":1,"writes":[{"at_us":6060,"bytes":57,"kind":"head","slot":0},{"at_us":6065,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":68,"abi_return_us":5799,"end_us":5804,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5848,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5842},{"abi_enter_us":5862,"abi_return_us":5867,"end_us":5868,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5862,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5895,"operation":"read","slot":0,"start_us":5893},{"abi_enter_us":5911,"abi_return_us":5911,"end_us":5912,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5910,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5934,"operation":"read","slot":0,"start_us":5933},{"abi_enter_us":5944,"abi_return_us":5945,"end_us":5945,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5944,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5967,"operation":"read","slot":0,"start_us":5966},{"abi_enter_us":5977,"abi_return_us":5979,"end_us":5980,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5977,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5999,"operation":"read","slot":0,"start_us":5999},{"end_us":6012,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6009}],"schema":2,"slots":[{"at_us":6012,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e52d8c5970-5dc4-91","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":5585,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5746,"slot":0,"token":"18e160e52e2453b0-5dc4-92"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e52e2453b0-5dc4-92","watchdog":false,"workers_joined":1,"writes":[{"at_us":5760,"bytes":57,"kind":"head","slot":0},{"at_us":5766,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":86,"abi_return_us":5438,"end_us":5442,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5479,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5473},{"abi_enter_us":5491,"abi_return_us":5495,"end_us":5495,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5491,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5522,"operation":"read","slot":0,"start_us":5520},{"abi_enter_us":5534,"abi_return_us":5536,"end_us":5536,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5534,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5558,"operation":"read","slot":0,"start_us":5558},{"end_us":5571,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5568}],"schema":2,"slots":[{"at_us":5571,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e52e2453b0-5dc4-92","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":6498,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6429,"slot":0,"token":"18e160e52eb00568-5dc4-93"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e52eb00568-5dc4-93","watchdog":false,"workers_joined":1,"writes":[{"at_us":6449,"bytes":57,"kind":"head","slot":0},{"at_us":6460,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":78,"abi_return_us":6278,"end_us":6283,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6324,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6318},{"abi_enter_us":6340,"abi_return_us":6344,"end_us":6345,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6340,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6371,"operation":"read","slot":0,"start_us":6368},{"abi_enter_us":6384,"abi_return_us":6385,"end_us":6385,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6384,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6408,"operation":"read","slot":0,"start_us":6407},{"abi_enter_us":6418,"abi_return_us":6419,"end_us":6419,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6418,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6439,"operation":"read","slot":0,"start_us":6438},{"abi_enter_us":6448,"abi_return_us":6450,"end_us":6451,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6448,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6471,"operation":"read","slot":0,"start_us":6470},{"end_us":6483,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6479}],"schema":2,"slots":[{"at_us":6483,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e52eb00568-5dc4-93","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":6328,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6413,"slot":0,"token":"18e160e52f4cc650-5dc4-94"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e52f4cc650-5dc4-94","watchdog":false,"workers_joined":1,"writes":[{"at_us":6422,"bytes":57,"kind":"head","slot":0},{"at_us":6426,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":72,"abi_return_us":6204,"end_us":6206,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6230,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6226},{"abi_enter_us":6241,"abi_return_us":6245,"end_us":6245,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6241,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6269,"operation":"read","slot":0,"start_us":6268},{"abi_enter_us":6279,"abi_return_us":6281,"end_us":6281,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6279,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6303,"operation":"read","slot":0,"start_us":6302},{"end_us":6314,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6312}],"schema":2,"slots":[{"at_us":6314,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e52f4cc650-5dc4-94","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6331,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6404,"slot":0,"token":"18e160e52fe53d90-5dc4-95"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e52fe53d90-5dc4-95","watchdog":false,"workers_joined":1,"writes":[{"at_us":6413,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":55,"abi_return_us":6224,"end_us":6226,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6261,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6256},{"abi_enter_us":6274,"abi_return_us":6276,"end_us":6277,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6274,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6301,"operation":"read","slot":0,"start_us":6299},{"end_us":6318,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6313}],"schema":2,"slots":[{"at_us":6318,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e52fe53d90-5dc4-95","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6341,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6402,"slot":0,"token":"18e160e5307373d0-5dc4-96"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e5307373d0-5dc4-96","watchdog":false,"workers_joined":1,"writes":[{"at_us":6412,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":44,"abi_return_us":6242,"end_us":6245,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6274,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6269},{"abi_enter_us":6285,"abi_return_us":6288,"end_us":6288,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6285,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6315,"operation":"read","slot":0,"start_us":6314},{"end_us":6328,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6326}],"schema":2,"slots":[{"at_us":6328,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e5307373d0-5dc4-96","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5144,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5174,"slot":0,"token":"18e160e5310098a0-5dc4-97"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e5310098a0-5dc4-97","watchdog":false,"workers_joined":1,"writes":[{"at_us":5185,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"abi_enter_us":70,"abi_return_us":5019,"end_us":5023,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5069,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5064},{"abi_enter_us":5084,"abi_return_us":5086,"end_us":5087,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5084,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5116,"operation":"read","slot":0,"start_us":5114},{"end_us":5131,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5128}],"schema":2,"slots":[{"at_us":5131,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e5310098a0-5dc4-97","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6015,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6081,"slot":0,"token":"18e160e5317db3f8-5dc4-98"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e5317db3f8-5dc4-98","watchdog":false,"workers_joined":1,"writes":[{"at_us":6091,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":49,"operation":"handoff","start_us":49},{"abi_enter_us":59,"abi_return_us":5923,"end_us":5925,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5951,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5947},{"abi_enter_us":5964,"abi_return_us":5966,"end_us":5966,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5964,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5990,"operation":"read","slot":0,"start_us":5989},{"end_us":6003,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6001}],"schema":2,"slots":[{"at_us":6003,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e5317db3f8-5dc4-98","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5055,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5199,"slot":0,"token":"18e160e532062990-5dc4-99"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e532062990-5dc4-99","watchdog":false,"workers_joined":1,"writes":[{"at_us":5206,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":57,"abi_return_us":5010,"end_us":5011,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5031,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5027},{"end_us":5042,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5042}],"schema":2,"slots":[{"at_us":5042,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e532062990-5dc4-99","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6362,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6406,"slot":0,"token":"18e160e5327a1940-5dc4-9a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e5327a1940-5dc4-9a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6416,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"abi_enter_us":45,"abi_return_us":6295,"end_us":6298,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6326,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6319},{"end_us":6349,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6349}],"schema":2,"slots":[{"at_us":6349,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e5327a1940-5dc4-9a","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":4956,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5152,"slot":0,"token":"18e160e533040da8-5dc4-9b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e533040da8-5dc4-9b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5159,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"abi_enter_us":67,"abi_return_us":4908,"end_us":4909,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4928,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":4926},{"end_us":4942,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":4942}],"schema":2,"slots":[{"at_us":4942,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e533040da8-5dc4-9b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5264,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5378,"slot":0,"token":"18e160e5337b8f68-5dc4-9c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e5337b8f68-5dc4-9c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5391,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":42,"operation":"handoff","start_us":42},{"abi_enter_us":53,"abi_return_us":5219,"end_us":5221,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5240,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5237},{"end_us":5251,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5251}],"schema":2,"slots":[{"at_us":5251,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e5337b8f68-5dc4-9c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58422,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6408,"slot":0,"token":"18e160e533f543a8-5dc4-9d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e533f543a8-5dc4-9d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6421,"bytes":66,"kind":"head","slot":0},{"at_us":6426,"bytes":6,"kind":"body","slot":0},{"at_us":58429,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":25,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"abi_enter_us":49,"abi_return_us":6262,"end_us":6264,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6293,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6288},{"abi_enter_us":6305,"abi_return_us":6309,"end_us":6310,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6305,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6334,"operation":"read","slot":0,"start_us":6331},{"abi_enter_us":6347,"abi_return_us":58310,"end_us":58314,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6347,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":58383,"error":"Other","operation":"read","slot":0,"start_us":58370},{"end_us":58407,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58401}],"schema":2,"slots":[{"at_us":58407,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e533f543a8-5dc4-9d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58241,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6423,"slot":0,"token":"18e160e537a84540-5dc4-9e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e537a84540-5dc4-9e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6436,"bytes":66,"kind":"head","slot":0},{"at_us":6441,"bytes":6,"kind":"body","slot":0},{"at_us":58376,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":66,"abi_return_us":6192,"end_us":6195,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6226,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6221},{"abi_enter_us":6239,"abi_return_us":6244,"end_us":6244,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6239,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6274,"operation":"read","slot":0,"start_us":6271},{"abi_enter_us":6288,"abi_return_us":58129,"end_us":58133,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6288,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":58197,"error":"Other","operation":"read","slot":0,"start_us":58192},{"end_us":58221,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58215}],"schema":2,"slots":[{"at_us":58221,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e537a84540-5dc4-9e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":55331,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5245,"slot":0,"token":"18e160e53b589b40-5dc4-9f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e53b589b40-5dc4-9f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5260,"bytes":66,"kind":"head","slot":0},{"at_us":5268,"bytes":6,"kind":"body","slot":0},{"at_us":55501,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"abi_enter_us":86,"abi_return_us":4988,"end_us":4991,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5024,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5019},{"abi_enter_us":5037,"abi_return_us":5042,"end_us":5042,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5037,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5068,"operation":"read","slot":0,"start_us":5066},{"abi_enter_us":5083,"abi_return_us":55239,"end_us":55243,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5083,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":55296,"error":"Other","operation":"read","slot":0,"start_us":55291},{"end_us":55317,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55312}],"schema":2,"slots":[{"at_us":55317,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e53b589b40-5dc4-9f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58350,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6431,"slot":0,"token":"18e160e53ee11238-5dc4-a0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e53ee11238-5dc4-a0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6445,"bytes":66,"kind":"head","slot":0},{"at_us":6451,"bytes":6,"kind":"body","slot":0},{"at_us":58520,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":90,"abi_return_us":6166,"end_us":6170,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6207,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6202},{"abi_enter_us":6223,"abi_return_us":6228,"end_us":6228,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6223,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6256,"operation":"read","slot":0,"start_us":6254},{"abi_enter_us":6269,"abi_return_us":58245,"end_us":58250,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6268,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":58311,"error":"Other","operation":"read","slot":0,"start_us":58307},{"end_us":58335,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58329}],"schema":2,"slots":[{"at_us":58335,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e53ee11238-5dc4-a0","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5323,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5509,"slot":0,"token":"18e160e5429ade18-5dc4-a1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e5429ade18-5dc4-a1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":63,"abi_return_us":5265,"end_us":5267,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5295,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5287},{"end_us":5308,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5308}],"schema":2,"slots":[{"at_us":5308,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e5429ade18-5dc4-a1","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":6314,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6416,"slot":0,"token":"18e160e5431486a0-5dc4-a2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e5431486a0-5dc4-a2","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":49,"abi_return_us":6258,"end_us":6261,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6286,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6282},{"end_us":6299,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6299}],"schema":2,"slots":[{"at_us":6299,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e5431486a0-5dc4-a2","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5085,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5213,"slot":0,"token":"18e160e543a0d0b0-5dc4-a3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e543a0d0b0-5dc4-a3","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"abi_enter_us":61,"abi_return_us":5028,"end_us":5031,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5059,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5052},{"end_us":5071,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5071}],"schema":2,"slots":[{"at_us":5071,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e543a0d0b0-5dc4-a3","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5321,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5439,"slot":0,"token":"18e160e54417ece0-5dc4-a4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e54417ece0-5dc4-a4","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":44,"operation":"handoff","start_us":44},{"abi_enter_us":54,"abi_return_us":5274,"end_us":5276,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5296,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5293},{"end_us":5308,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5308}],"schema":2,"slots":[{"at_us":5308,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e54417ece0-5dc4-a4","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":60528,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6054,"slot":0,"token":"18e160e5448fe7b8-5dc4-a5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e5448fe7b8-5dc4-a5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6068,"bytes":57,"kind":"head","slot":0},{"at_us":6074,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":47,"abi_return_us":5906,"end_us":5909,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5941,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5936},{"bytes":1,"capacity":1,"end_us":5971,"operation":"read","slot":0,"start_us":5965},{"capacity":1,"end_us":60445,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5991},{"end_us":60508,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60501}],"schema":2,"slots":[{"at_us":60508,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e5448fe7b8-5dc4-a5","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":55807,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5302,"slot":0,"token":"18e160e54859fb90-5dc4-a6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e54859fb90-5dc4-a6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5314,"bytes":57,"kind":"head","slot":0},{"at_us":5319,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":56,"abi_return_us":5051,"end_us":5054,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5087,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5082},{"abi_enter_us":5099,"abi_return_us":5104,"end_us":5105,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5099,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5129,"operation":"read","slot":0,"start_us":5127},{"abi_enter_us":5140,"abi_return_us":55678,"end_us":55682,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5140,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":55759,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":55754},{"end_us":55786,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55779}],"schema":2,"slots":[{"at_us":55786,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e54859fb90-5dc4-a6","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":59214,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6474,"slot":0,"token":"18e160e54be90238-5dc4-a7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e54be90238-5dc4-a7","watchdog":false,"workers_joined":1,"writes":[{"at_us":6491,"bytes":57,"kind":"head","slot":0},{"at_us":6499,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":62,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":100,"abi_return_us":6119,"end_us":6126,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":100,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6187,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6180},{"abi_enter_us":6203,"abi_return_us":6209,"end_us":6209,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6203,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6247,"operation":"read","slot":0,"start_us":6244},{"abi_enter_us":6260,"abi_return_us":59080,"end_us":59087,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6260,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":59169,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59164},{"end_us":59193,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59186}],"schema":2,"slots":[{"at_us":59193,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e54be90238-5dc4-a7","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":59528,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5244,"slot":0,"token":"18e160e54fae5350-5dc4-a8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e54fae5350-5dc4-a8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5263,"bytes":57,"kind":"head","slot":0},{"at_us":5269,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":102,"abi_return_us":4977,"end_us":4981,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5015,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5009},{"abi_enter_us":5031,"abi_return_us":5036,"end_us":5037,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5031,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5062,"operation":"read","slot":0,"start_us":5060},{"abi_enter_us":5075,"abi_return_us":59405,"end_us":59412,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5075,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":59486,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59481},{"end_us":59512,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59506}],"schema":2,"slots":[{"at_us":59512,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e54fae5350-5dc4-a8","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":59854,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6450,"slot":0,"token":"18e160e5537ca518-5dc4-a9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e5537ca518-5dc4-a9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6468,"bytes":57,"kind":"head","slot":0},{"at_us":6474,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"abi_enter_us":93,"abi_return_us":6179,"end_us":6185,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6237,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6231},{"abi_enter_us":6252,"abi_return_us":6256,"end_us":6256,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6252,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6286,"operation":"read","slot":0,"start_us":6284},{"abi_enter_us":6301,"abi_return_us":59724,"end_us":59730,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6301,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":59810,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59806},{"end_us":59838,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59829}],"schema":2,"slots":[{"at_us":59838,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e5537ca518-5dc4-a9","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":500887,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6429,"slot":0,"token":"18e160e5573868e0-5dc4-aa"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e5573868e0-5dc4-aa","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":68,"abi_return_us":500795,"end_us":500805,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500866,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500866}],"schema":2,"slots":[{"at_us":500866,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e5573868e0-5dc4-aa","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501471,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6432,"slot":0,"token":"18e160e57530cdb0-5dc4-ab"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e57530cdb0-5dc4-ab","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":75,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":164,"abi_return_us":501350,"end_us":501372,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":164,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501446,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501446}],"schema":2,"slots":[{"at_us":501446,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e57530cdb0-5dc4-ab","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501156,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5210,"slot":0,"token":"18e160e59332bbe8-5dc4-ac"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e59332bbe8-5dc4-ac","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":84,"abi_return_us":500851,"end_us":500864,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501125,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501125}],"schema":2,"slots":[{"at_us":501125,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e59332bbe8-5dc4-ac","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":500520,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6442,"slot":0,"token":"18e160e5b12f1c40-5dc4-ad"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e5b12f1c40-5dc4-ad","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":123,"abi_return_us":500431,"end_us":500440,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":123,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500502,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500502}],"schema":2,"slots":[{"at_us":500502,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e5b12f1c40-5dc4-ad","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":502235,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5176,"slot":0,"token":"18e160e5cf21dbc0-5dc4-ae"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e5cf21dbc0-5dc4-ae","watchdog":false,"workers_joined":1,"writes":[{"at_us":5189,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":68,"abi_return_us":4906,"end_us":4908,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4945,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4940},{"abi_enter_us":4961,"abi_return_us":502115,"end_us":502124,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4961,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":502209,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502201}],"schema":2,"slots":[{"at_us":502209,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e5cf21dbc0-5dc4-ae","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":500708,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6055,"slot":0,"token":"18e160e5ed35b3e8-5dc4-af"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e5ed35b3e8-5dc4-af","watchdog":false,"workers_joined":1,"writes":[{"at_us":6073,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":88,"abi_return_us":5808,"end_us":5813,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5868,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5861},{"abi_enter_us":5890,"abi_return_us":500611,"end_us":500617,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5890,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500689,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500679}],"schema":2,"slots":[{"at_us":500689,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e5ed35b3e8-5dc4-af","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501070,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6445,"slot":0,"token":"18e160e60b326a30-5dc4-b0"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e60b326a30-5dc4-b0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6462,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":81,"operation":"handoff","start_us":81},{"abi_enter_us":95,"abi_return_us":6211,"end_us":6214,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6253,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6247},{"abi_enter_us":6265,"abi_return_us":500948,"end_us":500954,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6265,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501039,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501020}],"schema":2,"slots":[{"at_us":501039,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e60b326a30-5dc4-b0","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501560,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5538,"slot":0,"token":"18e160e629311090-5dc4-b1"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e629311090-5dc4-b1","watchdog":false,"workers_joined":1,"writes":[{"at_us":5551,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"abi_enter_us":82,"abi_return_us":5281,"end_us":5285,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5325,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5317},{"abi_enter_us":5337,"abi_return_us":501453,"end_us":501457,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5337,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501535,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501520}],"schema":2,"slots":[{"at_us":501535,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e629311090-5dc4-b1","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":501586,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5753,"slot":0,"token":"18e160e647449e80-5dc4-b2"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e647449e80-5dc4-b2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5770,"bytes":57,"kind":"head","slot":0},{"at_us":5778,"bytes":1,"kind":"body","slot":0},{"at_us":310635,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":87,"abi_return_us":5486,"end_us":5491,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5538,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5532},{"abi_enter_us":5556,"abi_return_us":5563,"end_us":5563,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5556,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5589,"operation":"read","slot":0,"start_us":5586},{"abi_enter_us":5601,"abi_return_us":310332,"end_us":310337,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5601,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":310410,"operation":"read","slot":0,"start_us":310406},{"abi_enter_us":310425,"abi_return_us":501498,"end_us":501502,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310425,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":501566,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501558}],"schema":2,"slots":[{"at_us":501566,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e647449e80-5dc4-b2","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":501239,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5243,"slot":0,"token":"18e160e66563dca0-5dc4-b3"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e66563dca0-5dc4-b3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5252,"bytes":57,"kind":"head","slot":0},{"at_us":5258,"bytes":1,"kind":"body","slot":0},{"at_us":307248,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":129,"abi_return_us":4952,"end_us":4954,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":128,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4990,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4984},{"abi_enter_us":5005,"abi_return_us":5010,"end_us":5010,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5005,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5040,"operation":"read","slot":0,"start_us":5037},{"abi_enter_us":5053,"abi_return_us":306960,"end_us":306964,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5053,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":307039,"operation":"read","slot":0,"start_us":307035},{"abi_enter_us":307055,"abi_return_us":501131,"end_us":501141,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307055,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501216,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501201}],"schema":2,"slots":[{"at_us":501216,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e66563dca0-5dc4-b3","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":500326,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5240,"slot":0,"token":"18e160e6836f7b50-5dc4-b4"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e6836f7b50-5dc4-b4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5258,"bytes":57,"kind":"head","slot":0},{"at_us":5265,"bytes":1,"kind":"body","slot":0},{"at_us":308042,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":92,"operation":"handoff","start_us":92},{"abi_enter_us":106,"abi_return_us":4942,"end_us":4948,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4998,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4992},{"abi_enter_us":5012,"abi_return_us":5016,"end_us":5017,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5012,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5046,"operation":"read","slot":0,"start_us":5043},{"abi_enter_us":5058,"abi_return_us":307725,"end_us":307731,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5058,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307803,"operation":"read","slot":0,"start_us":307799},{"abi_enter_us":307816,"abi_return_us":500241,"end_us":500245,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307816,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500305,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500298}],"schema":2,"slots":[{"at_us":500305,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e6836f7b50-5dc4-b4","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":500274,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6408,"slot":0,"token":"18e160e6a17950f8-5dc4-b5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e6a17950f8-5dc4-b5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6417,"bytes":57,"kind":"head","slot":0},{"at_us":6423,"bytes":1,"kind":"body","slot":0},{"at_us":309131,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":91,"abi_return_us":6162,"end_us":6164,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6200,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6195},{"abi_enter_us":6213,"abi_return_us":6221,"end_us":6221,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6213,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6253,"operation":"read","slot":0,"start_us":6251},{"abi_enter_us":6265,"abi_return_us":309012,"end_us":309021,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6265,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309096,"operation":"read","slot":0,"start_us":309092},{"abi_enter_us":309110,"abi_return_us":500196,"end_us":500200,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309110,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500255,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500248}],"schema":2,"slots":[{"at_us":500255,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e6a17950f8-5dc4-b5","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":501197,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6431,"slot":0,"token":"18e160e6bf7d2b60-5dc4-b6"},{"path":"/readiness/siblings/1","received_us":6435,"slot":1,"token":"18e160e6bf7d2b60-5dc4-b6"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160e6bf7d2b60-5dc4-b6","watchdog":false,"workers_joined":2,"writes":[{"at_us":6440,"bytes":57,"kind":"head","slot":0},{"at_us":6448,"bytes":57,"kind":"head","slot":1},{"at_us":6452,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":104,"operation":"dispatch","outcome":"pending","slot":1,"start_us":89},{"abi_enter_us":140,"abi_return_us":6156,"end_us":6158,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":140,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6182,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6177},{"abi_enter_us":6194,"abi_return_us":6195,"end_us":6196,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6193,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6212,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6211},{"abi_enter_us":6222,"abi_return_us":6224,"end_us":6224,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6222,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6249,"operation":"read","slot":1,"start_us":6247},{"abi_enter_us":6260,"abi_return_us":6261,"end_us":6261,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6260,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6284,"operation":"read","slot":1,"start_us":6283},{"abi_enter_us":6293,"abi_return_us":6294,"end_us":6295,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6293,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6313,"operation":"read","slot":1,"start_us":6313},{"abi_enter_us":6323,"abi_return_us":6324,"end_us":6325,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6322,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6347,"operation":"read","slot":1,"start_us":6346},{"end_us":6357,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6356},{"abi_enter_us":6369,"abi_return_us":500995,"end_us":501027,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6369,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501159,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501147}],"schema":2,"slots":[{"at_us":501159,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6357,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e6bf7d2b60-5dc4-b6","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":500883,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5504,"slot":0,"token":"18e160e6ddbcb320-5dc4-b7"},{"path":"/readiness/siblings/1","received_us":5524,"slot":1,"token":"18e160e6ddbcb320-5dc4-b7"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160e6ddbcb320-5dc4-b7","watchdog":false,"workers_joined":2,"writes":[{"at_us":5533,"bytes":57,"kind":"head","slot":0},{"at_us":5545,"bytes":57,"kind":"head","slot":1},{"at_us":5554,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":130,"operation":"dispatch","outcome":"pending","slot":1,"start_us":103},{"abi_enter_us":153,"abi_return_us":5209,"end_us":5215,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":152,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5260,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5255},{"abi_enter_us":5277,"abi_return_us":5280,"end_us":5280,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5277,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5295,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5294},{"abi_enter_us":5305,"abi_return_us":5309,"end_us":5309,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5305,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5340,"operation":"read","slot":1,"start_us":5337},{"abi_enter_us":5352,"abi_return_us":5355,"end_us":5355,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5352,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5377,"operation":"read","slot":1,"start_us":5377},{"end_us":5390,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5387},{"abi_enter_us":5406,"abi_return_us":500701,"end_us":500711,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5405,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500862,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500853}],"schema":2,"slots":[{"at_us":500862,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5390,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e6ddbcb320-5dc4-b7","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":501644,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5331,"slot":1,"token":"18e160e6fbd6a1e0-5dc4-b8"},{"path":"/readiness/siblings/0","received_us":5383,"slot":0,"token":"18e160e6fbd6a1e0-5dc4-b8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160e6fbd6a1e0-5dc4-b8","watchdog":false,"workers_joined":2,"writes":[{"at_us":5351,"bytes":57,"kind":"head","slot":1},{"at_us":5399,"bytes":57,"kind":"head","slot":0},{"at_us":10791,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":127,"operation":"dispatch","outcome":"pending","slot":1,"start_us":91},{"body_reads":0,"deadline_us":500000,"end_us":162,"operation":"handoff","start_us":162},{"abi_enter_us":181,"abi_return_us":5070,"end_us":5075,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":181,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5120,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5114},{"abi_enter_us":5135,"abi_return_us":5138,"end_us":5138,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5135,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5153,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5152},{"abi_enter_us":5167,"abi_return_us":10506,"end_us":10508,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5167,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":10556,"operation":"read","slot":1,"start_us":10553},{"abi_enter_us":10569,"abi_return_us":10571,"end_us":10572,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10569,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10598,"operation":"read","slot":1,"start_us":10596},{"abi_enter_us":10608,"abi_return_us":10609,"end_us":10609,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10608,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10633,"operation":"read","slot":1,"start_us":10632},{"abi_enter_us":10643,"abi_return_us":10647,"end_us":10647,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10643,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":1,"end_us":10671,"operation":"read","slot":1,"start_us":10671},{"end_us":10685,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10681},{"abi_enter_us":10700,"abi_return_us":501553,"end_us":501559,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":10700,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":501626,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501618}],"schema":2,"slots":[{"at_us":501626,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":10685,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e6fbd6a1e0-5dc4-b8","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":500550,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5350,"slot":1,"token":"18e160e71a0466c0-5dc4-b9"},{"path":"/readiness/siblings/0","received_us":5404,"slot":0,"token":"18e160e71a0466c0-5dc4-b9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160e71a0466c0-5dc4-b9","watchdog":false,"workers_joined":2,"writes":[{"at_us":5369,"bytes":57,"kind":"head","slot":1},{"at_us":5411,"bytes":57,"kind":"head","slot":0},{"at_us":11498,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":133,"operation":"dispatch","outcome":"pending","slot":1,"start_us":100},{"body_reads":0,"deadline_us":500000,"end_us":155,"operation":"handoff","start_us":155},{"abi_enter_us":170,"abi_return_us":5106,"end_us":5111,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":170,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5172,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5166},{"abi_enter_us":5189,"abi_return_us":5195,"end_us":5196,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5189,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5213,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5212},{"abi_enter_us":5227,"abi_return_us":11205,"end_us":11207,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5227,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11267,"operation":"read","slot":1,"start_us":11264},{"abi_enter_us":11282,"abi_return_us":11286,"end_us":11287,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11282,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11321,"operation":"read","slot":1,"start_us":11320},{"end_us":11337,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11332},{"abi_enter_us":11354,"abi_return_us":500455,"end_us":500461,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11354,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500530,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500523}],"schema":2,"slots":[{"at_us":500530,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11337,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e71a0466c0-5dc4-b9","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":501456,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5451,"slot":0,"token":"18e160e738191d90-5dc4-ba"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e738191d90-5dc4-ba","watchdog":false,"workers_joined":1,"writes":[{"at_us":5458,"bytes":57,"kind":"head","slot":0},{"at_us":5463,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":71,"abi_return_us":5147,"end_us":5149,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5181,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5176},{"abi_enter_us":5196,"abi_return_us":5203,"end_us":5203,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5196,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5235,"operation":"read","slot":0,"start_us":5232},{"abi_enter_us":5247,"abi_return_us":501348,"end_us":501356,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5247,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501426,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501418}],"schema":2,"slots":[{"at_us":501426,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e738191d90-5dc4-ba","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":500951,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6439,"slot":0,"token":"18e160e756234928-5dc4-bb"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160e756234928-5dc4-bb","watchdog":false,"workers_joined":1,"writes":[{"at_us":6455,"bytes":57,"kind":"head","slot":0},{"at_us":6460,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":101,"abi_return_us":6108,"end_us":6113,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":101,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6154,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6148},{"abi_enter_us":6168,"abi_return_us":6173,"end_us":6174,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6168,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6206,"operation":"read","slot":0,"start_us":6203},{"abi_enter_us":6220,"abi_return_us":500838,"end_us":500845,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6219,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500916,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500908}],"schema":2,"slots":[{"at_us":500916,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e756234928-5dc4-bb","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":501761,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5782,"slot":0,"token":"18e160e7742dd280-5dc4-bc"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e7742dd280-5dc4-bc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5789,"bytes":57,"kind":"head","slot":0},{"at_us":5794,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":104,"abi_return_us":5528,"end_us":5530,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5556,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5551},{"abi_enter_us":5569,"abi_return_us":5573,"end_us":5573,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5569,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5613,"operation":"read","slot":0,"start_us":5611},{"abi_enter_us":5624,"abi_return_us":501676,"end_us":501680,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5624,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501741,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501734}],"schema":2,"slots":[{"at_us":501741,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7742dd280-5dc4-bc","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":500435,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6274,"slot":0,"token":"18e160e79244ac30-5dc4-bd"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160e79244ac30-5dc4-bd","watchdog":false,"workers_joined":1,"writes":[{"at_us":6282,"bytes":57,"kind":"head","slot":0},{"at_us":6288,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":89,"abi_return_us":6029,"end_us":6030,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6054,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6047},{"abi_enter_us":6065,"abi_return_us":6068,"end_us":6069,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6065,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6093,"operation":"read","slot":0,"start_us":6091},{"abi_enter_us":6104,"abi_return_us":500343,"end_us":500350,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6104,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500415,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500408}],"schema":2,"slots":[{"at_us":500415,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e79244ac30-5dc4-bd","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":5764,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5840,"slot":0,"token":"18e160e7b045fa40-5dc4-be"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b045fa40-5dc4-be","watchdog":false,"workers_joined":1,"writes":[{"at_us":5856,"bytes":57,"kind":"head","slot":0},{"at_us":5862,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":68,"abi_return_us":5583,"end_us":5584,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5608,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5603},{"abi_enter_us":5619,"abi_return_us":5625,"end_us":5625,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5619,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5649,"operation":"read","slot":0,"start_us":5647},{"abi_enter_us":5660,"abi_return_us":5661,"end_us":5661,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5660,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5681,"operation":"read","slot":0,"start_us":5680},{"abi_enter_us":5692,"abi_return_us":5693,"end_us":5693,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5692,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5712,"operation":"read","slot":0,"start_us":5711},{"abi_enter_us":5721,"abi_return_us":5723,"end_us":5723,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5721,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5741,"operation":"read","slot":0,"start_us":5741},{"end_us":5751,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5750}],"schema":2,"slots":[{"at_us":5751,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7b045fa40-5dc4-be","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":6369,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6390,"slot":0,"token":"18e160e7b0d24068-5dc4-bf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b0d24068-5dc4-bf","watchdog":false,"workers_joined":1,"writes":[{"at_us":6400,"bytes":57,"kind":"head","slot":0},{"at_us":6405,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":25,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"abi_enter_us":43,"abi_return_us":6233,"end_us":6237,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":43,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6264,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6261},{"abi_enter_us":6277,"abi_return_us":6281,"end_us":6281,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6277,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6309,"operation":"read","slot":0,"start_us":6308},{"abi_enter_us":6320,"abi_return_us":6323,"end_us":6323,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6320,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6345,"operation":"read","slot":0,"start_us":6344},{"end_us":6356,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6354}],"schema":2,"slots":[{"at_us":6356,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7b0d24068-5dc4-bf","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":6379,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6369,"slot":0,"token":"18e160e7b15c7f08-5dc4-c0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b15c7f08-5dc4-c0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6378,"bytes":57,"kind":"head","slot":0},{"at_us":6382,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"abi_enter_us":64,"abi_return_us":6196,"end_us":6198,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6223,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6221},{"abi_enter_us":6236,"abi_return_us":6239,"end_us":6240,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6235,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6263,"operation":"read","slot":0,"start_us":6261},{"abi_enter_us":6275,"abi_return_us":6276,"end_us":6277,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6275,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6297,"operation":"read","slot":0,"start_us":6296},{"abi_enter_us":6306,"abi_return_us":6307,"end_us":6307,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6306,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6325,"operation":"read","slot":0,"start_us":6325},{"abi_enter_us":6334,"abi_return_us":6336,"end_us":6336,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6334,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6356,"operation":"read","slot":0,"start_us":6356},{"end_us":6366,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6365}],"schema":2,"slots":[{"at_us":6366,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7b15c7f08-5dc4-c0","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":6324,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6392,"slot":0,"token":"18e160e7b1f0d3b0-5dc4-c1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b1f0d3b0-5dc4-c1","watchdog":false,"workers_joined":1,"writes":[{"at_us":6399,"bytes":57,"kind":"head","slot":0},{"at_us":6404,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":54,"operation":"handoff","start_us":54},{"abi_enter_us":64,"abi_return_us":6196,"end_us":6198,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6221,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6217},{"abi_enter_us":6235,"abi_return_us":6239,"end_us":6239,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6235,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6263,"operation":"read","slot":0,"start_us":6262},{"abi_enter_us":6274,"abi_return_us":6276,"end_us":6277,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6274,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6301,"operation":"read","slot":0,"start_us":6300},{"end_us":6312,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6310}],"schema":2,"slots":[{"at_us":6312,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7b1f0d3b0-5dc4-c1","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5024,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5163,"slot":0,"token":"18e160e7b280abe8-5dc4-c2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b280abe8-5dc4-c2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5175,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":51,"abi_return_us":4927,"end_us":4930,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4962,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4955},{"abi_enter_us":4974,"abi_return_us":4977,"end_us":4977,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4974,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":1,"end_us":4997,"operation":"read","slot":0,"start_us":4996},{"end_us":5009,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5007}],"schema":2,"slots":[{"at_us":5009,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7b280abe8-5dc4-c2","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5016,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5166,"slot":0,"token":"18e160e7b2f20b58-5dc4-c3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b2f20b58-5dc4-c3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5174,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":50,"abi_return_us":4931,"end_us":4932,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4954,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4951},{"abi_enter_us":4967,"abi_return_us":4969,"end_us":4969,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4967,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":16384,"end_us":4992,"operation":"read","slot":0,"start_us":4991},{"end_us":5004,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5003}],"schema":2,"slots":[{"at_us":5004,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7b2f20b58-5dc4-c3","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5103,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5224,"slot":0,"token":"18e160e7b36fc2f0-5dc4-c4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b36fc2f0-5dc4-c4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5238,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"abi_enter_us":79,"abi_return_us":5000,"end_us":5004,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5039,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5034},{"abi_enter_us":5051,"abi_return_us":5054,"end_us":5054,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5051,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5078,"operation":"read","slot":0,"start_us":5075},{"end_us":5091,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5088}],"schema":2,"slots":[{"at_us":5091,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7b36fc2f0-5dc4-c4","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5226,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5373,"slot":0,"token":"18e160e7b3e41830-5dc4-c5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b3e41830-5dc4-c5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5378,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":57,"operation":"handoff","start_us":57},{"abi_enter_us":68,"abi_return_us":5152,"end_us":5153,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5170,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5167},{"abi_enter_us":5180,"abi_return_us":5181,"end_us":5181,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5180,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5205,"operation":"read","slot":0,"start_us":5204},{"end_us":5215,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5214}],"schema":2,"slots":[{"at_us":5215,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7b3e41830-5dc4-c5","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5032,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5155,"slot":0,"token":"18e160e7b458a808-5dc4-c6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b458a808-5dc4-c6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5163,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":46,"abi_return_us":4988,"end_us":4989,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5006,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5004},{"end_us":5019,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5019}],"schema":2,"slots":[{"at_us":5019,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e7b458a808-5dc4-c6","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5064,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5183,"slot":0,"token":"18e160e7b4c52190-5dc4-c7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b4c52190-5dc4-c7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5189,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":20,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"abi_enter_us":44,"abi_return_us":5019,"end_us":5020,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5039,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5038},{"end_us":5051,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5051}],"schema":2,"slots":[{"at_us":5051,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e7b4c52190-5dc4-c7","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6307,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6381,"slot":0,"token":"18e160e7b5329ce8-5dc4-c8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b5329ce8-5dc4-c8","watchdog":false,"workers_joined":1,"writes":[{"at_us":6386,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":21,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"body_reads":0,"deadline_us":500000,"end_us":42,"operation":"handoff","start_us":42},{"abi_enter_us":54,"abi_return_us":6263,"end_us":6264,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6284,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6282},{"end_us":6295,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6295}],"schema":2,"slots":[{"at_us":6295,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e7b5329ce8-5dc4-c8","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6273,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6380,"slot":0,"token":"18e160e7b5b361c0-5dc4-c9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b5b361c0-5dc4-c9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6386,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":47,"operation":"handoff","start_us":47},{"abi_enter_us":62,"abi_return_us":6227,"end_us":6228,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6246,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6244},{"end_us":6259,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6258}],"schema":2,"slots":[{"at_us":6259,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e7b5b361c0-5dc4-c9","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57389,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6383,"slot":0,"token":"18e160e7b6346900-5dc4-ca"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b6346900-5dc4-ca","watchdog":false,"workers_joined":1,"writes":[{"at_us":6395,"bytes":66,"kind":"head","slot":0},{"at_us":6403,"bytes":6,"kind":"body","slot":0},{"at_us":57414,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"abi_enter_us":40,"abi_return_us":6233,"end_us":6234,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":40,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6257,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6255},{"abi_enter_us":6268,"abi_return_us":6273,"end_us":6273,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6268,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6295,"operation":"read","slot":0,"start_us":6293},{"abi_enter_us":6305,"abi_return_us":57288,"end_us":57291,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6305,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":57351,"error":"Other","operation":"read","slot":0,"start_us":57337},{"end_us":57374,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57369}],"schema":2,"slots":[{"at_us":57374,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7b6346900-5dc4-ca","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58161,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5169,"slot":0,"token":"18e160e7b9cdf310-5dc4-cb"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7b9cdf310-5dc4-cb","watchdog":false,"workers_joined":1,"writes":[{"at_us":5179,"bytes":66,"kind":"head","slot":0},{"at_us":5183,"bytes":6,"kind":"body","slot":0},{"at_us":58308,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":58,"abi_return_us":4901,"end_us":4902,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4927,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4922},{"abi_enter_us":4939,"abi_return_us":4943,"end_us":4943,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4939,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4971,"operation":"read","slot":0,"start_us":4969},{"abi_enter_us":4985,"abi_return_us":58064,"end_us":58067,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4985,"status":"FastlyStatus::OK","timeout_ms":495},{"capacity":16384,"end_us":58124,"error":"Other","operation":"read","slot":0,"start_us":58117},{"end_us":58146,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58140}],"schema":2,"slots":[{"at_us":58146,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7b9cdf310-5dc4-cb","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":55271,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5169,"slot":0,"token":"18e160e7bd759680-5dc4-cc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7bd759680-5dc4-cc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5177,"bytes":66,"kind":"head","slot":0},{"at_us":5181,"bytes":6,"kind":"body","slot":0},{"at_us":55446,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":73,"operation":"handoff","start_us":73},{"abi_enter_us":85,"abi_return_us":4883,"end_us":4884,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4911,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4907},{"abi_enter_us":4922,"abi_return_us":4925,"end_us":4926,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4922,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4951,"operation":"read","slot":0,"start_us":4949},{"abi_enter_us":4967,"abi_return_us":55177,"end_us":55180,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4967,"status":"FastlyStatus::OK","timeout_ms":495},{"capacity":1,"end_us":55233,"error":"Other","operation":"read","slot":0,"start_us":55228},{"end_us":55257,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55251}],"schema":2,"slots":[{"at_us":55257,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7bd759680-5dc4-cc","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56058,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5181,"slot":0,"token":"18e160e7c0ee6990-5dc4-cd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7c0ee6990-5dc4-cd","watchdog":false,"workers_joined":1,"writes":[{"at_us":5187,"bytes":66,"kind":"head","slot":0},{"at_us":5192,"bytes":6,"kind":"body","slot":0},{"at_us":56198,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"abi_enter_us":75,"abi_return_us":4946,"end_us":4947,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4972,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4968},{"abi_enter_us":4985,"abi_return_us":4988,"end_us":4988,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4985,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5011,"operation":"read","slot":0,"start_us":5010},{"abi_enter_us":5024,"abi_return_us":55970,"end_us":55973,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5024,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56024,"error":"Other","operation":"read","slot":0,"start_us":56019},{"end_us":56044,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56038}],"schema":2,"slots":[{"at_us":56044,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160e7c0ee6990-5dc4-cd","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":6304,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6427,"slot":0,"token":"18e160e7c4746ba0-5dc4-ce"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7c4746ba0-5dc4-ce","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":64,"abi_return_us":6238,"end_us":6242,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6275,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6266},{"end_us":6290,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6289}],"schema":2,"slots":[{"at_us":6290,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e7c4746ba0-5dc4-ce","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5080,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5238,"slot":0,"token":"18e160e7c4f56ef8-5dc4-cf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7c4f56ef8-5dc4-cf","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":60,"abi_return_us":5020,"end_us":5023,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5050,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5046},{"end_us":5063,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5063}],"schema":2,"slots":[{"at_us":5063,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e7c4f56ef8-5dc4-cf","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5433,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5579,"slot":0,"token":"18e160e7c5631930-5dc4-d0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7c5631930-5dc4-d0","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"abi_enter_us":77,"abi_return_us":5373,"end_us":5379,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5407,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5402},{"end_us":5419,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5419}],"schema":2,"slots":[{"at_us":5419,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e7c5631930-5dc4-d0","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5320,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5471,"slot":0,"token":"18e160e7c5d710b0-5dc4-d1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160e7c5d710b0-5dc4-d1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"abi_enter_us":76,"abi_return_us":5264,"end_us":5267,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5294,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5290},{"end_us":5306,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5306}],"schema":2,"slots":[{"at_us":5306,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160e7c5d710b0-5dc4-d1","tolerance_us":5000}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1619,"event":"readiness-timer-control","overrun_us":619,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160edae77de58-6a3d-3","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2568,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2573,"event":"readiness-timer-control","overrun_us":1573,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160edae982be0-6a3d-4","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2292,"event":"readiness-timer-control","overrun_us":1292,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160edaefe4880-6a3d-5","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":106373,"event":"readiness-timer-control","overrun_us":6373,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160edaf2f0510-6a3d-6","within_five_ms":false}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":103522,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":103533,"event":"readiness-timer-control","overrun_us":3533,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160edb58abc60-6a3d-7","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101227,"event":"readiness-timer-control","overrun_us":1227,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160edbbc7f318-6a3d-8","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":510012,"event":"readiness-timer-control","overrun_us":10012,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160edc1e783f8-6a3d-9","within_five_ms":false}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501602,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501614,"event":"readiness-timer-control","overrun_us":1614,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ede051a9e0-6a3d-a","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503086,"event":"readiness-timer-control","overrun_us":3086,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160edfe4ab6a8-6a3d-b","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1506,"event":"readiness-timer-control","overrun_us":506,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ee1c55d858-6a3d-c","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2053,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2056,"event":"readiness-timer-control","overrun_us":1056,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ee1c7022d0-6a3d-d","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2514,"event":"readiness-timer-control","overrun_us":1514,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ee1c9b7c78-6a3d-e","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102318,"event":"readiness-timer-control","overrun_us":2318,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ee1ccb42f0-6a3d-f","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102155,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102164,"event":"readiness-timer-control","overrun_us":2164,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ee22e99f38-6a3d-10","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101857,"event":"readiness-timer-control","overrun_us":1857,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ee2913e648-6a3d-11","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504462,"event":"readiness-timer-control","overrun_us":4462,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ee2f3403c8-6a3d-12","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":503062,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":503072,"event":"readiness-timer-control","overrun_us":3072,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ee4d4b6630-6a3d-13","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501210,"event":"readiness-timer-control","overrun_us":1210,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ee6b59d788-6a3d-14","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1102,"event":"readiness-timer-control","overrun_us":102,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ee894dd758-6a3d-15","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2534,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2538,"event":"readiness-timer-control","overrun_us":1538,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ee8964b2e8-6a3d-16","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2391,"event":"readiness-timer-control","overrun_us":1391,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ee899c1698-6a3d-17","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":109952,"event":"readiness-timer-control","overrun_us":9952,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ee89cd6b80-6a3d-18","within_five_ms":false}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101310,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101318,"event":"readiness-timer-control","overrun_us":1318,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ee90645ee0-6a3d-19","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101086,"event":"readiness-timer-control","overrun_us":1086,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ee96993510-6a3d-1a","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":507007,"event":"readiness-timer-control","overrun_us":7007,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ee9caeafe8-6a3d-1b","within_five_ms":false}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501390,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501398,"event":"readiness-timer-control","overrun_us":1398,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160eebaed2e08-6a3d-1c","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500349,"event":"readiness-timer-control","overrun_us":349,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160eed8dfed88-6a3d-1d","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1021,"event":"readiness-timer-control","overrun_us":21,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160eef6c01f08-6a3d-1e","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2177,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2185,"event":"readiness-timer-control","overrun_us":1185,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160eef6d5cdd0-6a3d-1f","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2050,"event":"readiness-timer-control","overrun_us":1050,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160eef70ad020-6a3d-20","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":105252,"event":"readiness-timer-control","overrun_us":5252,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160eef73971a0-6a3d-21","within_five_ms":false}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101088,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101095,"event":"readiness-timer-control","overrun_us":1095,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160eefd879c30-6a3d-22","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101242,"event":"readiness-timer-control","overrun_us":1242,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ef03ace9d0-6a3d-23","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":509878,"event":"readiness-timer-control","overrun_us":9878,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ef09c8c960-6a3d-24","within_five_ms":false}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":2,"abi_return_us":502308,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502319,"event":"readiness-timer-control","overrun_us":2319,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ef2831f160-6a3d-25","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503381,"event":"readiness-timer-control","overrun_us":3381,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ef4632a330-6a3d-26","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1513,"event":"readiness-timer-control","overrun_us":513,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ef64435a90-6a3d-27","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2058,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2068,"event":"readiness-timer-control","overrun_us":1068,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ef64614aa0-6a3d-28","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2534,"event":"readiness-timer-control","overrun_us":1534,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ef64926108-6a3d-29","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":105118,"event":"readiness-timer-control","overrun_us":5118,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ef64c80b50-6a3d-2a","within_five_ms":false}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101250,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101268,"event":"readiness-timer-control","overrun_us":1268,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ef6b344530-6a3d-2b","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102612,"event":"readiness-timer-control","overrun_us":2612,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160ef715c03d0-6a3d-2c","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501333,"event":"readiness-timer-control","overrun_us":1333,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160ef778b30c8-6a3d-2d","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501516,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501525,"event":"readiness-timer-control","overrun_us":1525,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160ef9574b118-6a3d-2e","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502940,"event":"readiness-timer-control","overrun_us":2940,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160efb36cd768-6a3d-2f","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1082,"event":"readiness-timer-control","overrun_us":82,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160efd17ce2e8-6a3d-30","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2531,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2535,"event":"readiness-timer-control","overrun_us":1535,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160efd19193c8-6a3d-31","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2526,"event":"readiness-timer-control","overrun_us":1526,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160efd1c61148-6a3d-32","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102613,"event":"readiness-timer-control","overrun_us":2613,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160efd1f69340-6a3d-33","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":103295,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":103305,"event":"readiness-timer-control","overrun_us":3305,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160efd8186e10-6a3d-34","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101864,"event":"readiness-timer-control","overrun_us":1864,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160efde53b898-6a3d-35","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501898,"event":"readiness-timer-control","overrun_us":1898,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160efe476cfd0-6a3d-36","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501076,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501084,"event":"readiness-timer-control","overrun_us":1084,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160f00265a750-6a3d-37","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501908,"event":"readiness-timer-control","overrun_us":1908,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160f020546b48-6a3d-38","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1065,"event":"readiness-timer-control","overrun_us":65,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160f03e4e3390-6a3d-39","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2522,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2525,"event":"readiness-timer-control","overrun_us":1525,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160f03e624060-6a3d-3a","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2524,"event":"readiness-timer-control","overrun_us":1524,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160f03e974a80-6a3d-3b","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102055,"event":"readiness-timer-control","overrun_us":2055,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160f03ec795c8-6a3d-3c","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101539,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101556,"event":"readiness-timer-control","overrun_us":1556,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160f044e59838-6a3d-3d","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101075,"event":"readiness-timer-control","overrun_us":1075,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160f04b045df8-6a3d-3e","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504650,"event":"readiness-timer-control","overrun_us":4650,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160f0511ba1d8-6a3d-3f","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502407,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502416,"event":"readiness-timer-control","overrun_us":2416,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160f06f350fb0-6a3d-40","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501544,"event":"readiness-timer-control","overrun_us":1544,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160f08d383e38-6a3d-41","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1055,"event":"readiness-timer-control","overrun_us":55,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160f0ab2cf1b8-6a3d-42","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2547,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2552,"event":"readiness-timer-control","overrun_us":1552,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160f0ab406630-6a3d-43","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2044,"event":"readiness-timer-control","overrun_us":1044,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160f0ab751290-6a3d-44","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103756,"event":"readiness-timer-control","overrun_us":3756,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160f0ab9f00f0-6a3d-45","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101128,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101135,"event":"readiness-timer-control","overrun_us":1135,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160f0b1d4e0c0-6a3d-46","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103110,"event":"readiness-timer-control","overrun_us":3110,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160f0b7ecd468-6a3d-47","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504121,"event":"readiness-timer-control","overrun_us":4121,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e160f0be21d978-6a3d-48","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":3,"abi_return_us":501218,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501256,"event":"readiness-timer-control","overrun_us":1256,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e160f0dc36a7b8-6a3d-49","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501097,"event":"readiness-timer-control","overrun_us":1097,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e160f0fa2d9970-6a3d-4a","within_five_ms":true}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":62578,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":7728,"slot":0,"token":"18e160f118243538-6a3d-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f118243538-6a3d-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":7748,"bytes":57,"kind":"head","slot":0},{"at_us":7753,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":995,"operation":"dispatch","outcome":"pending","slot":0,"start_us":396},{"abi_enter_us":1044,"abi_return_us":6763,"end_us":6769,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1044,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":6989,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6821},{"bytes":1,"capacity":1,"end_us":7022,"operation":"read","slot":0,"start_us":7016},{"capacity":1,"end_us":62507,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":7043},{"end_us":62561,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":62555}],"schema":2,"slots":[{"at_us":62561,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f118243538-6a3d-4b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":63005,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5638,"slot":0,"token":"18e160f11c1604f0-6a3d-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f11c1604f0-6a3d-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5649,"bytes":57,"kind":"head","slot":0},{"at_us":5656,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":66,"abi_return_us":5433,"end_us":5435,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5467,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5461},{"abi_enter_us":5481,"abi_return_us":5485,"end_us":5486,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5480,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5510,"operation":"read","slot":0,"start_us":5508},{"abi_enter_us":5521,"abi_return_us":62900,"end_us":62906,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5521,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":62969,"operation":"read","slot":0,"start_us":62965},{"end_us":62988,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":62984}],"schema":2,"slots":[{"at_us":62988,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f11c1604f0-6a3d-4c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":62521,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":7670,"slot":0,"token":"18e160f11ffaebf8-6a3d-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f11ffaebf8-6a3d-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":7680,"bytes":57,"kind":"head","slot":0},{"at_us":7685,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":55,"abi_return_us":7422,"end_us":7425,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7454,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7450},{"abi_enter_us":7465,"abi_return_us":7469,"end_us":7469,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7465,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7501,"operation":"read","slot":0,"start_us":7500},{"abi_enter_us":7512,"abi_return_us":62424,"end_us":62427,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7512,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":62485,"operation":"read","slot":0,"start_us":62482},{"end_us":62501,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":62498}],"schema":2,"slots":[{"at_us":62501,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f11ffaebf8-6a3d-4d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":55576,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5210,"slot":0,"token":"18e160f123e33a18-6a3d-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f123e33a18-6a3d-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5219,"bytes":57,"kind":"head","slot":0},{"at_us":5225,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"abi_enter_us":74,"abi_return_us":4961,"end_us":4962,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4985,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4981},{"abi_enter_us":4996,"abi_return_us":4999,"end_us":5000,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4995,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5024,"operation":"read","slot":0,"start_us":5022},{"abi_enter_us":5034,"abi_return_us":55493,"end_us":55496,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5034,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":55546,"operation":"read","slot":0,"start_us":55540},{"end_us":55560,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55557}],"schema":2,"slots":[{"at_us":55560,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f123e33a18-6a3d-4e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":65540,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5586,"slot":0,"token":"18e160f1275fa320-6a3d-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f1275fa320-6a3d-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5604,"bytes":57,"kind":"head","slot":0},{"at_us":5611,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"abi_enter_us":79,"abi_return_us":5338,"end_us":5344,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5396,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5391},{"abi_enter_us":5410,"abi_return_us":5414,"end_us":5414,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5409,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5442,"operation":"read","slot":0,"start_us":5439},{"abi_enter_us":5453,"abi_return_us":65427,"end_us":65435,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5453,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":65501,"operation":"read","slot":0,"start_us":65497},{"end_us":65521,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":65516}],"schema":2,"slots":[{"at_us":65521,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f1275fa320-6a3d-4f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":500962,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6118,"slot":0,"token":"18e160f12b79abb8-6a3d-50"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f12b79abb8-6a3d-50","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":94,"abi_return_us":500886,"end_us":500894,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500946,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500946}],"schema":2,"slots":[{"at_us":500946,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f12b79abb8-6a3d-50","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":500475,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5222,"slot":0,"token":"18e160f14977a250-6a3d-51"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f14977a250-6a3d-51","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":54,"abi_return_us":500398,"end_us":500406,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500458,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500458}],"schema":2,"slots":[{"at_us":500458,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f14977a250-6a3d-51","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":501979,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5226,"slot":0,"token":"18e160f1676bdcb8-6a3d-52"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f1676bdcb8-6a3d-52","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"abi_enter_us":83,"abi_return_us":501896,"end_us":501905,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501963,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501962}],"schema":2,"slots":[{"at_us":501963,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f1676bdcb8-6a3d-52","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":501109,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5229,"slot":0,"token":"18e160f1857c0778-6a3d-53"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f1857c0778-6a3d-53","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":73,"operation":"handoff","start_us":73},{"abi_enter_us":86,"abi_return_us":501021,"end_us":501032,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501093,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501093}],"schema":2,"slots":[{"at_us":501093,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f1857c0778-6a3d-53","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":502650,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5162,"slot":0,"token":"18e160f1a37d8080-6a3d-54"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f1a37d8080-6a3d-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":5175,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":68,"abi_return_us":4892,"end_us":4893,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4916,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4913},{"abi_enter_us":4928,"abi_return_us":502562,"end_us":502569,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4928,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":502630,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502624}],"schema":2,"slots":[{"at_us":502630,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f1a37d8080-6a3d-54","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":502592,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":7644,"slot":0,"token":"18e160f1c1945260-6a3d-55"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f1c1945260-6a3d-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":7652,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":64,"abi_return_us":7417,"end_us":7419,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7443,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7440},{"abi_enter_us":7455,"abi_return_us":502506,"end_us":502512,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":7455,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":502573,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502567}],"schema":2,"slots":[{"at_us":502573,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f1c1945260-6a3d-55","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":502216,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":7689,"slot":0,"token":"18e160f1dfa845e0-6a3d-56"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f1dfa845e0-6a3d-56","watchdog":false,"workers_joined":1,"writes":[{"at_us":7705,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":77,"abi_return_us":7461,"end_us":7466,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7511,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7507},{"abi_enter_us":7523,"abi_return_us":502126,"end_us":502133,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":7523,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":502182,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502177}],"schema":2,"slots":[{"at_us":502182,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f1dfa845e0-6a3d-56","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":500538,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5253,"slot":0,"token":"18e160f1fdb9cc48-6a3d-57"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f1fdb9cc48-6a3d-57","watchdog":false,"workers_joined":1,"writes":[{"at_us":5268,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":72,"abi_return_us":5010,"end_us":5015,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5062,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5058},{"abi_enter_us":5077,"abi_return_us":500449,"end_us":500456,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5076,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500514,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500504}],"schema":2,"slots":[{"at_us":500514,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f1fdb9cc48-6a3d-57","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":500483,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":7654,"slot":0,"token":"18e160f21bbda2c8-6a3d-58"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f21bbda2c8-6a3d-58","watchdog":false,"workers_joined":1,"writes":[{"at_us":7665,"bytes":57,"kind":"head","slot":0},{"at_us":7671,"bytes":1,"kind":"body","slot":0},{"at_us":307744,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":59,"abi_return_us":7443,"end_us":7445,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7473,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7469},{"abi_enter_us":7487,"abi_return_us":7490,"end_us":7491,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7487,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7521,"operation":"read","slot":0,"start_us":7519},{"abi_enter_us":7532,"abi_return_us":307520,"end_us":307527,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7532,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":307590,"operation":"read","slot":0,"start_us":307587},{"abi_enter_us":307608,"abi_return_us":500358,"end_us":500362,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307608,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500458,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500452}],"schema":2,"slots":[{"at_us":500458,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f21bbda2c8-6a3d-58","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":501392,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6445,"slot":0,"token":"18e160f239beecf0-6a3d-59"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f239beecf0-6a3d-59","watchdog":false,"workers_joined":1,"writes":[{"at_us":6454,"bytes":57,"kind":"head","slot":0},{"at_us":6460,"bytes":1,"kind":"body","slot":0},{"at_us":313374,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":57,"abi_return_us":6248,"end_us":6250,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6288,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6284},{"abi_enter_us":6299,"abi_return_us":6303,"end_us":6303,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6299,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6327,"operation":"read","slot":0,"start_us":6326},{"abi_enter_us":6339,"abi_return_us":313156,"end_us":313162,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6339,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":313225,"operation":"read","slot":0,"start_us":313222},{"abi_enter_us":313238,"abi_return_us":501306,"end_us":501309,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":313238,"status":"FastlyStatus::OK","timeout_ms":186},{"end_us":501353,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501347}],"schema":2,"slots":[{"at_us":501353,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f239beecf0-6a3d-59","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":502341,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5182,"slot":0,"token":"18e160f257d6ef80-6a3d-5a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f257d6ef80-6a3d-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5190,"bytes":57,"kind":"head","slot":0},{"at_us":5199,"bytes":1,"kind":"body","slot":0},{"at_us":310822,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":55,"operation":"handoff","start_us":55},{"abi_enter_us":78,"abi_return_us":4899,"end_us":4901,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4925,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4921},{"abi_enter_us":4936,"abi_return_us":4940,"end_us":4940,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4936,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4965,"operation":"read","slot":0,"start_us":4963},{"abi_enter_us":4976,"abi_return_us":310544,"end_us":310550,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4976,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":310618,"operation":"read","slot":0,"start_us":310615},{"abi_enter_us":310631,"abi_return_us":502254,"end_us":502258,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310631,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":502321,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502314}],"schema":2,"slots":[{"at_us":502321,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f257d6ef80-6a3d-5a","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":501219,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5302,"slot":0,"token":"18e160f275fb1770-6a3d-5b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f275fb1770-6a3d-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5318,"bytes":57,"kind":"head","slot":0},{"at_us":5325,"bytes":1,"kind":"body","slot":0},{"at_us":312258,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":85,"abi_return_us":5045,"end_us":5048,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5087,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5083},{"abi_enter_us":5101,"abi_return_us":5106,"end_us":5106,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5101,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5140,"operation":"read","slot":0,"start_us":5137},{"abi_enter_us":5151,"abi_return_us":311979,"end_us":311989,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5151,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":312057,"operation":"read","slot":0,"start_us":312054},{"abi_enter_us":312072,"abi_return_us":501124,"end_us":501128,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":312072,"status":"FastlyStatus::OK","timeout_ms":187},{"end_us":501195,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501188}],"schema":2,"slots":[{"at_us":501195,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f275fb1770-6a3d-5b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":502224,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5242,"slot":1,"token":"18e160f294066418-6a3d-5c"},{"path":"/readiness/siblings/0","received_us":5250,"slot":0,"token":"18e160f294066418-6a3d-5c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160f294066418-6a3d-5c","watchdog":false,"workers_joined":2,"writes":[{"at_us":5255,"bytes":57,"kind":"head","slot":1},{"at_us":5264,"bytes":57,"kind":"head","slot":0},{"at_us":12776,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":76,"operation":"dispatch","outcome":"pending","slot":1,"start_us":63},{"abi_enter_us":96,"abi_return_us":4922,"end_us":4924,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4950,"operation":"head-poll","outcome":"ready","slot":1,"start_us":4946},{"abi_enter_us":4961,"abi_return_us":4962,"end_us":4962,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":4960,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":4975,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4975},{"abi_enter_us":4984,"abi_return_us":12479,"end_us":12481,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":4984,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":12532,"operation":"read","slot":1,"start_us":12529},{"abi_enter_us":12545,"abi_return_us":12547,"end_us":12547,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12545,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12578,"operation":"read","slot":1,"start_us":12571},{"abi_enter_us":12589,"abi_return_us":12590,"end_us":12591,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12589,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12613,"operation":"read","slot":1,"start_us":12612},{"abi_enter_us":12622,"abi_return_us":12624,"end_us":12624,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12622,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":1,"end_us":12644,"operation":"read","slot":1,"start_us":12644},{"end_us":12659,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12655},{"abi_enter_us":12673,"abi_return_us":502137,"end_us":502144,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12673,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":502205,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502199}],"schema":2,"slots":[{"at_us":502205,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12659,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f294066418-6a3d-5c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501924,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5214,"slot":0,"token":"18e160f2b24b1fe0-6a3d-5d"},{"path":"/readiness/siblings/1","received_us":5221,"slot":1,"token":"18e160f2b24b1fe0-6a3d-5d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160f2b24b1fe0-6a3d-5d","watchdog":false,"workers_joined":2,"writes":[{"at_us":5226,"bytes":57,"kind":"head","slot":0},{"at_us":5234,"bytes":57,"kind":"head","slot":1},{"at_us":5241,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":90,"operation":"dispatch","outcome":"pending","slot":1,"start_us":74},{"abi_enter_us":103,"abi_return_us":4994,"end_us":4996,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5026,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5022},{"abi_enter_us":5037,"abi_return_us":5038,"end_us":5038,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5036,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5051,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5051},{"abi_enter_us":5063,"abi_return_us":5065,"end_us":5065,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5062,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5098,"operation":"read","slot":1,"start_us":5096},{"abi_enter_us":5109,"abi_return_us":5111,"end_us":5111,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5108,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5131,"operation":"read","slot":1,"start_us":5131},{"end_us":5145,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5143},{"abi_enter_us":5156,"abi_return_us":501834,"end_us":501840,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5156,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501905,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501898}],"schema":2,"slots":[{"at_us":501905,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5145,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f2b24b1fe0-6a3d-5d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":500975,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":7725,"slot":0,"token":"18e160f2d06fcca0-6a3d-5e"},{"path":"/readiness/siblings/1","received_us":7735,"slot":1,"token":"18e160f2d06fcca0-6a3d-5e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160f2d06fcca0-6a3d-5e","watchdog":false,"workers_joined":2,"writes":[{"at_us":7740,"bytes":57,"kind":"head","slot":0},{"at_us":7750,"bytes":57,"kind":"head","slot":1},{"at_us":7756,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"end_us":122,"operation":"dispatch","outcome":"pending","slot":1,"start_us":100},{"body_reads":0,"deadline_us":500000,"end_us":137,"operation":"handoff","start_us":137},{"abi_enter_us":188,"abi_return_us":7527,"end_us":7529,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":188,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7559,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7553},{"abi_enter_us":7570,"abi_return_us":7572,"end_us":7572,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":7570,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":7585,"operation":"head-poll","outcome":"ready","slot":1,"start_us":7584},{"abi_enter_us":7598,"abi_return_us":7603,"end_us":7603,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7598,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7627,"operation":"read","slot":1,"start_us":7625},{"abi_enter_us":7637,"abi_return_us":7638,"end_us":7638,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7637,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7657,"operation":"read","slot":1,"start_us":7656},{"abi_enter_us":7665,"abi_return_us":7666,"end_us":7666,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7665,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7690,"operation":"read","slot":1,"start_us":7690},{"abi_enter_us":7700,"abi_return_us":7702,"end_us":7702,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7700,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":1,"end_us":7723,"operation":"read","slot":1,"start_us":7723},{"end_us":7732,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":7731},{"abi_enter_us":7743,"abi_return_us":500898,"end_us":500904,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":7743,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":500958,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500951}],"schema":2,"slots":[{"at_us":500958,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":7732,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f2d06fcca0-6a3d-5e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501592,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5672,"slot":0,"token":"18e160f2ee975ba8-6a3d-5f"},{"path":"/readiness/siblings/1","received_us":5677,"slot":1,"token":"18e160f2ee975ba8-6a3d-5f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160f2ee975ba8-6a3d-5f","watchdog":false,"workers_joined":2,"writes":[{"at_us":5684,"bytes":57,"kind":"head","slot":0},{"at_us":5695,"bytes":57,"kind":"head","slot":1},{"at_us":5702,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":98,"operation":"dispatch","outcome":"pending","slot":1,"start_us":84},{"body_reads":0,"deadline_us":500000,"end_us":129,"operation":"handoff","start_us":129},{"abi_enter_us":142,"abi_return_us":5404,"end_us":5407,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":141,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5440,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5435},{"abi_enter_us":5456,"abi_return_us":5458,"end_us":5458,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5456,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5473,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5472},{"abi_enter_us":5483,"abi_return_us":5487,"end_us":5487,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5483,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5513,"operation":"read","slot":1,"start_us":5511},{"abi_enter_us":5526,"abi_return_us":5528,"end_us":5529,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5526,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5550,"operation":"read","slot":1,"start_us":5550},{"end_us":5561,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5559},{"abi_enter_us":5573,"abi_return_us":501508,"end_us":501516,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5573,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501576,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501571}],"schema":2,"slots":[{"at_us":501576,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5561,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f2ee975ba8-6a3d-5f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":501833,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5222,"slot":0,"token":"18e160f30cbfc570-6a3d-60"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f30cbfc570-6a3d-60","watchdog":false,"workers_joined":1,"writes":[{"at_us":5232,"bytes":57,"kind":"head","slot":0},{"at_us":5238,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":68,"abi_return_us":4980,"end_us":4982,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5009,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5005},{"abi_enter_us":5022,"abi_return_us":5025,"end_us":5026,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5022,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5051,"operation":"read","slot":0,"start_us":5049},{"abi_enter_us":5061,"abi_return_us":501755,"end_us":501761,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5061,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501815,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501810}],"schema":2,"slots":[{"at_us":501815,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f30cbfc570-6a3d-60","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":502579,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6515,"slot":0,"token":"18e160f32acdfc30-6a3d-61"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f32acdfc30-6a3d-61","watchdog":false,"workers_joined":1,"writes":[{"at_us":6530,"bytes":57,"kind":"head","slot":0},{"at_us":6535,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":73,"abi_return_us":6327,"end_us":6332,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6379,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6374},{"abi_enter_us":6392,"abi_return_us":6398,"end_us":6398,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6392,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6436,"operation":"read","slot":0,"start_us":6433},{"abi_enter_us":6448,"abi_return_us":502494,"end_us":502499,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6448,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":502561,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502554}],"schema":2,"slots":[{"at_us":502561,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f32acdfc30-6a3d-61","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":502611,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5213,"slot":0,"token":"18e160f348eef7a0-6a3d-62"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f348eef7a0-6a3d-62","watchdog":false,"workers_joined":1,"writes":[{"at_us":5226,"bytes":57,"kind":"head","slot":0},{"at_us":5232,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":72,"abi_return_us":4937,"end_us":4941,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4977,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4973},{"abi_enter_us":4991,"abi_return_us":4994,"end_us":4995,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4990,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5018,"operation":"read","slot":0,"start_us":5016},{"abi_enter_us":5029,"abi_return_us":502512,"end_us":502521,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5029,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502590,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502583}],"schema":2,"slots":[{"at_us":502590,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f348eef7a0-6a3d-62","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":500214,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5637,"slot":0,"token":"18e160f36715ee50-6a3d-63"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f36715ee50-6a3d-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":5646,"bytes":57,"kind":"head","slot":0},{"at_us":5650,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":95,"abi_return_us":5316,"end_us":5319,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5355,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5351},{"abi_enter_us":5369,"abi_return_us":5373,"end_us":5374,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5369,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5401,"operation":"read","slot":0,"start_us":5398},{"abi_enter_us":5415,"abi_return_us":500064,"end_us":500075,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5415,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500164,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500152}],"schema":2,"slots":[{"at_us":500164,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f36715ee50-6a3d-63","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":7525,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":7647,"slot":0,"token":"18e160f385141f80-6a3d-64"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f385141f80-6a3d-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":7656,"bytes":57,"kind":"head","slot":0},{"at_us":7661,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":58,"abi_return_us":7349,"end_us":7351,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7376,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7372},{"abi_enter_us":7388,"abi_return_us":7392,"end_us":7393,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7388,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7415,"operation":"read","slot":0,"start_us":7414},{"abi_enter_us":7426,"abi_return_us":7427,"end_us":7427,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7426,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7448,"operation":"read","slot":0,"start_us":7447},{"abi_enter_us":7457,"abi_return_us":7458,"end_us":7458,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7457,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7475,"operation":"read","slot":0,"start_us":7475},{"abi_enter_us":7484,"abi_return_us":7486,"end_us":7486,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7484,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":1,"end_us":7503,"operation":"read","slot":0,"start_us":7502},{"end_us":7512,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":7511}],"schema":2,"slots":[{"at_us":7512,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f385141f80-6a3d-64","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":5982,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6112,"slot":0,"token":"18e160f385cc0988-6a3d-65"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f385cc0988-6a3d-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":6123,"bytes":57,"kind":"head","slot":0},{"at_us":6128,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":59,"abi_return_us":5861,"end_us":5862,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5887,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5884},{"abi_enter_us":5898,"abi_return_us":5902,"end_us":5902,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5898,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5928,"operation":"read","slot":0,"start_us":5927},{"abi_enter_us":5939,"abi_return_us":5941,"end_us":5941,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5939,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5961,"operation":"read","slot":0,"start_us":5961},{"end_us":5972,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5970}],"schema":2,"slots":[{"at_us":5972,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f385cc0988-6a3d-65","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":5264,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5255,"slot":0,"token":"18e160f38651ac78-6a3d-66"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f38651ac78-6a3d-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":5268,"bytes":57,"kind":"head","slot":0},{"at_us":5273,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":37},{"body_reads":0,"deadline_us":500000,"end_us":95,"operation":"handoff","start_us":95},{"abi_enter_us":118,"abi_return_us":5074,"end_us":5076,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":118,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5109,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5106},{"abi_enter_us":5122,"abi_return_us":5127,"end_us":5127,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5121,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5149,"operation":"read","slot":0,"start_us":5148},{"abi_enter_us":5160,"abi_return_us":5160,"end_us":5161,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5159,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5181,"operation":"read","slot":0,"start_us":5180},{"abi_enter_us":5190,"abi_return_us":5191,"end_us":5191,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5190,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5209,"operation":"read","slot":0,"start_us":5208},{"abi_enter_us":5217,"abi_return_us":5219,"end_us":5219,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5217,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5236,"operation":"read","slot":0,"start_us":5235},{"end_us":5250,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5245}],"schema":2,"slots":[{"at_us":5250,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f38651ac78-6a3d-66","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":7605,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":7634,"slot":0,"token":"18e160f386d50960-6a3d-67"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f386d50960-6a3d-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":7642,"bytes":57,"kind":"head","slot":0},{"at_us":7647,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":47,"operation":"handoff","start_us":47},{"abi_enter_us":57,"abi_return_us":7486,"end_us":7488,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7508,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7506},{"abi_enter_us":7518,"abi_return_us":7521,"end_us":7521,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7518,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":3,"capacity":16384,"end_us":7548,"operation":"read","slot":0,"start_us":7547},{"abi_enter_us":7558,"abi_return_us":7560,"end_us":7560,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7558,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":7583,"operation":"read","slot":0,"start_us":7582},{"end_us":7594,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":7591}],"schema":2,"slots":[{"at_us":7594,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f386d50960-6a3d-67","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5185,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5270,"slot":0,"token":"18e160f38772fee0-6a3d-68"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f38772fee0-6a3d-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":5278,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"abi_enter_us":49,"abi_return_us":5111,"end_us":5112,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5130,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5129},{"abi_enter_us":5140,"abi_return_us":5142,"end_us":5142,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5140,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5163,"operation":"read","slot":0,"start_us":5163},{"end_us":5174,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5173}],"schema":2,"slots":[{"at_us":5174,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f38772fee0-6a3d-68","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":7529,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":7629,"slot":0,"token":"18e160f387e479a8-6a3d-69"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f387e479a8-6a3d-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":7635,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"abi_enter_us":45,"abi_return_us":7452,"end_us":7453,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":45,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7476,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7474},{"abi_enter_us":7486,"abi_return_us":7487,"end_us":7487,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7486,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":7508,"operation":"read","slot":0,"start_us":7508},{"end_us":7518,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":7517}],"schema":2,"slots":[{"at_us":7518,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f387e479a8-6a3d-69","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5045,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5150,"slot":0,"token":"18e160f3887a6490-6a3d-6a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f3887a6490-6a3d-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5157,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"abi_enter_us":62,"abi_return_us":4971,"end_us":4972,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4990,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4988},{"abi_enter_us":5000,"abi_return_us":5001,"end_us":5002,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5000,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":1,"end_us":5024,"operation":"read","slot":0,"start_us":5023},{"end_us":5035,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5033}],"schema":2,"slots":[{"at_us":5035,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f3887a6490-6a3d-6a","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5052,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5187,"slot":0,"token":"18e160f388ebdb70-6a3d-6b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f388ebdb70-6a3d-6b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5193,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"body_reads":0,"deadline_us":500000,"end_us":35,"operation":"handoff","start_us":35},{"abi_enter_us":45,"abi_return_us":4984,"end_us":4985,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5000,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4999},{"abi_enter_us":5009,"abi_return_us":5010,"end_us":5011,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5009,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5031,"operation":"read","slot":0,"start_us":5030},{"end_us":5040,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5039}],"schema":2,"slots":[{"at_us":5040,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f388ebdb70-6a3d-6b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6327,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6322,"slot":0,"token":"18e160f3895cf878-6a3d-6c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f3895cf878-6a3d-6c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6328,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":21,"operation":"dispatch","outcome":"pending","slot":0,"start_us":7},{"abi_enter_us":37,"abi_return_us":6286,"end_us":6287,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":37,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6304,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6303},{"end_us":6315,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6315}],"schema":2,"slots":[{"at_us":6315,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f3895cf878-6a3d-6c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5504,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5576,"slot":0,"token":"18e160f389dbd120-6a3d-6d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f389dbd120-6a3d-6d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5584,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":19,"operation":"dispatch","outcome":"pending","slot":0,"start_us":7},{"abi_enter_us":34,"abi_return_us":5455,"end_us":5456,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":34,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5477,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5475},{"end_us":5491,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5491}],"schema":2,"slots":[{"at_us":5491,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f389dbd120-6a3d-6d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6346,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6460,"slot":0,"token":"18e160f38a4ee228-6a3d-6e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f38a4ee228-6a3d-6e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6468,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":21,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"body_reads":0,"deadline_us":500000,"end_us":40,"operation":"handoff","start_us":40},{"abi_enter_us":52,"abi_return_us":6301,"end_us":6302,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6323,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6321},{"end_us":6334,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6334}],"schema":2,"slots":[{"at_us":6334,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f38a4ee228-6a3d-6e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5099,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5196,"slot":0,"token":"18e160f38ada12f8-6a3d-6f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f38ada12f8-6a3d-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5201,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"body_reads":0,"deadline_us":500000,"end_us":37,"operation":"handoff","start_us":37},{"abi_enter_us":46,"abi_return_us":5061,"end_us":5062,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":46,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5077,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5076},{"end_us":5088,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5088}],"schema":2,"slots":[{"at_us":5088,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f38ada12f8-6a3d-6f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":62365,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":7629,"slot":0,"token":"18e160f38b47a5c0-6a3d-70"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f38b47a5c0-6a3d-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":7635,"bytes":66,"kind":"head","slot":0},{"at_us":7639,"bytes":6,"kind":"body","slot":0},{"at_us":62394,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":17,"operation":"dispatch","outcome":"pending","slot":0,"start_us":6},{"abi_enter_us":31,"abi_return_us":7481,"end_us":7482,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":31,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7504,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7502},{"abi_enter_us":7516,"abi_return_us":7519,"end_us":7519,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7516,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7541,"operation":"read","slot":0,"start_us":7539},{"abi_enter_us":7551,"abi_return_us":62288,"end_us":62290,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7551,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":1,"end_us":62337,"operation":"read","slot":0,"start_us":62333},{"end_us":62352,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":62349}],"schema":2,"slots":[{"at_us":62352,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f38b47a5c0-6a3d-70","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":64330,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":7465,"slot":0,"token":"18e160f38f2c6d88-6a3d-71"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f38f2c6d88-6a3d-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":7472,"bytes":66,"kind":"head","slot":0},{"at_us":7475,"bytes":6,"kind":"body","slot":0},{"at_us":64440,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"abi_enter_us":58,"abi_return_us":7246,"end_us":7247,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7264,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7261},{"abi_enter_us":7274,"abi_return_us":7277,"end_us":7277,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7274,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7300,"operation":"read","slot":0,"start_us":7299},{"abi_enter_us":7312,"abi_return_us":64248,"end_us":64251,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7312,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":64302,"operation":"read","slot":0,"start_us":64299},{"end_us":64317,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":64313}],"schema":2,"slots":[{"at_us":64317,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f38f2c6d88-6a3d-71","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":60621,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5179,"slot":0,"token":"18e160f3932dc5d0-6a3d-72"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f3932dc5d0-6a3d-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":5186,"bytes":66,"kind":"head","slot":0},{"at_us":5190,"bytes":6,"kind":"body","slot":0},{"at_us":60725,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":54,"operation":"handoff","start_us":54},{"abi_enter_us":64,"abi_return_us":4971,"end_us":4972,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4990,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4987},{"abi_enter_us":5000,"abi_return_us":5003,"end_us":5003,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5000,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5026,"operation":"read","slot":0,"start_us":5025},{"abi_enter_us":5036,"abi_return_us":60536,"end_us":60541,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5036,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":60592,"operation":"read","slot":0,"start_us":60589},{"end_us":60607,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60603}],"schema":2,"slots":[{"at_us":60607,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f3932dc5d0-6a3d-72","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56062,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5374,"slot":0,"token":"18e160f396f84708-6a3d-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f396f84708-6a3d-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":5381,"bytes":66,"kind":"head","slot":0},{"at_us":5385,"bytes":6,"kind":"body","slot":0},{"at_us":55617,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":50,"operation":"handoff","start_us":50},{"abi_enter_us":59,"abi_return_us":5194,"end_us":5195,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5269,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5266},{"abi_enter_us":5282,"abi_return_us":5285,"end_us":5286,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5282,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5314,"operation":"read","slot":0,"start_us":5313},{"abi_enter_us":5325,"abi_return_us":55930,"end_us":55935,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5325,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":56033,"operation":"read","slot":0,"start_us":56030},{"end_us":56049,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56045}],"schema":2,"slots":[{"at_us":56049,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f396f84708-6a3d-73","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":7599,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":7708,"slot":0,"token":"18e160f39a7680e8-6a3d-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f39a7680e8-6a3d-74","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":48,"abi_return_us":7539,"end_us":7542,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":48,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7569,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":7566},{"end_us":7583,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":7583}],"schema":2,"slots":[{"at_us":7583,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f39a7680e8-6a3d-74","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":5057,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5186,"slot":0,"token":"18e160f39b05d450-6a3d-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f39b05d450-6a3d-75","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":64,"abi_return_us":4996,"end_us":5000,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5030,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5023},{"end_us":5044,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5043}],"schema":2,"slots":[{"at_us":5044,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f39b05d450-6a3d-75","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":5111,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5275,"slot":0,"token":"18e160f39b6e0fe8-6a3d-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f39b6e0fe8-6a3d-76","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":79,"abi_return_us":5052,"end_us":5055,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5084,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5080},{"end_us":5097,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5097}],"schema":2,"slots":[{"at_us":5097,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f39b6e0fe8-6a3d-76","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":6493,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6644,"slot":0,"token":"18e160f39bdf3c90-6a3d-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f39bdf3c90-6a3d-77","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":57,"operation":"handoff","start_us":57},{"abi_enter_us":74,"abi_return_us":6436,"end_us":6440,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6467,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6463},{"end_us":6480,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6480}],"schema":2,"slots":[{"at_us":6480,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f39bdf3c90-6a3d-77","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":59612,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6861,"slot":0,"token":"18e160f39c5f9020-6a3d-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f39c5f9020-6a3d-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":6878,"bytes":57,"kind":"head","slot":0},{"at_us":6883,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":58,"abi_return_us":6652,"end_us":6655,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6699,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6695},{"bytes":1,"capacity":1,"end_us":6726,"operation":"read","slot":0,"start_us":6722},{"capacity":1,"end_us":59519,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":6745},{"end_us":59593,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59585}],"schema":2,"slots":[{"at_us":59593,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f39c5f9020-6a3d-78","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":55845,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5869,"slot":0,"token":"18e160f3a0107a90-6a3d-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f3a0107a90-6a3d-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":5891,"bytes":57,"kind":"head","slot":0},{"at_us":5898,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":67,"abi_return_us":5564,"end_us":5567,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5599,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5596},{"abi_enter_us":5612,"abi_return_us":5615,"end_us":5616,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5612,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5642,"operation":"read","slot":0,"start_us":5640},{"abi_enter_us":5653,"abi_return_us":55716,"end_us":55724,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5653,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":55806,"operation":"read","slot":0,"start_us":55802},{"end_us":55827,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55821}],"schema":2,"slots":[{"at_us":55827,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f3a0107a90-6a3d-79","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":62154,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":7600,"slot":0,"token":"18e160f3a38d2dd0-6a3d-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f3a38d2dd0-6a3d-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":7616,"bytes":57,"kind":"head","slot":0},{"at_us":7623,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":71,"abi_return_us":7369,"end_us":7374,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7413,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7409},{"abi_enter_us":7430,"abi_return_us":7436,"end_us":7437,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7430,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7466,"operation":"read","slot":0,"start_us":7464},{"abi_enter_us":7478,"abi_return_us":62048,"end_us":62054,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7478,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":62123,"operation":"read","slot":0,"start_us":62119},{"end_us":62138,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":62135}],"schema":2,"slots":[{"at_us":62138,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f3a38d2dd0-6a3d-7a","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":60382,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5226,"slot":0,"token":"18e160f3a76cdce8-6a3d-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f3a76cdce8-6a3d-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5236,"bytes":57,"kind":"head","slot":0},{"at_us":5241,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"abi_enter_us":76,"abi_return_us":5009,"end_us":5010,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5029,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5025},{"abi_enter_us":5041,"abi_return_us":5043,"end_us":5044,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5041,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5064,"operation":"read","slot":0,"start_us":5063},{"abi_enter_us":5073,"abi_return_us":60290,"end_us":60296,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5073,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":60346,"operation":"read","slot":0,"start_us":60342},{"end_us":60363,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60358}],"schema":2,"slots":[{"at_us":60363,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f3a76cdce8-6a3d-7b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":67669,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":7742,"slot":0,"token":"18e160f3ab3b4620-6a3d-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f3ab3b4620-6a3d-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":7760,"bytes":57,"kind":"head","slot":0},{"at_us":7766,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"abi_enter_us":85,"abi_return_us":7522,"end_us":7532,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7567,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7562},{"abi_enter_us":7579,"abi_return_us":7595,"end_us":7596,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7579,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7628,"operation":"read","slot":0,"start_us":7627},{"abi_enter_us":7642,"abi_return_us":67573,"end_us":67576,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7642,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":67638,"operation":"read","slot":0,"start_us":67635},{"end_us":67654,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":67650}],"schema":2,"slots":[{"at_us":67654,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f3ab3b4620-6a3d-7c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":501387,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5822,"slot":0,"token":"18e160f3af711418-6a3d-7d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f3af711418-6a3d-7d","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":59,"abi_return_us":501318,"end_us":501327,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501372,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501372}],"schema":2,"slots":[{"at_us":501372,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f3af711418-6a3d-7d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":501870,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5184,"slot":0,"token":"18e160f3cd6fa6f0-6a3d-7e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f3cd6fa6f0-6a3d-7e","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":65,"abi_return_us":501788,"end_us":501800,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501852,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501852}],"schema":2,"slots":[{"at_us":501852,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f3cd6fa6f0-6a3d-7e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":502459,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5561,"slot":0,"token":"18e160f3eb7bc688-6a3d-7f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f3eb7bc688-6a3d-7f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"abi_enter_us":78,"abi_return_us":502378,"end_us":502386,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502443,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502443}],"schema":2,"slots":[{"at_us":502443,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f3eb7bc688-6a3d-7f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":500869,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":7700,"slot":0,"token":"18e160f409a159c0-6a3d-80"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f409a159c0-6a3d-80","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":75,"abi_return_us":500787,"end_us":500797,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500852,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500852}],"schema":2,"slots":[{"at_us":500852,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f409a159c0-6a3d-80","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":502905,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5933,"slot":0,"token":"18e160f42799d600-6a3d-81"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f42799d600-6a3d-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":5943,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":63,"abi_return_us":5681,"end_us":5683,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5712,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5708},{"abi_enter_us":5723,"abi_return_us":502816,"end_us":502821,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5723,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502887,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502882}],"schema":2,"slots":[{"at_us":502887,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f42799d600-6a3d-81","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":501378,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":7697,"slot":0,"token":"18e160f445b32498-6a3d-82"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f445b32498-6a3d-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":7716,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":61,"abi_return_us":7479,"end_us":7484,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7529,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7525},{"abi_enter_us":7546,"abi_return_us":501267,"end_us":501286,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":7546,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":501341,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501335}],"schema":2,"slots":[{"at_us":501341,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f445b32498-6a3d-82","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":501525,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5541,"slot":0,"token":"18e160f463cd0b88-6a3d-83"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f463cd0b88-6a3d-83","watchdog":false,"workers_joined":1,"writes":[{"at_us":5559,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"abi_enter_us":95,"abi_return_us":5304,"end_us":5309,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5356,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5352},{"abi_enter_us":5370,"abi_return_us":501427,"end_us":501434,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5370,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501505,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501499}],"schema":2,"slots":[{"at_us":501505,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f463cd0b88-6a3d-83","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":501713,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5214,"slot":0,"token":"18e160f481d9a050-6a3d-84"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f481d9a050-6a3d-84","watchdog":false,"workers_joined":1,"writes":[{"at_us":5233,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":75,"operation":"handoff","start_us":75},{"abi_enter_us":86,"abi_return_us":4980,"end_us":4984,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5027,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5023},{"abi_enter_us":5039,"abi_return_us":501610,"end_us":501619,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5039,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501693,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501681}],"schema":2,"slots":[{"at_us":501693,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f481d9a050-6a3d-84","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":502848,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5306,"slot":0,"token":"18e160f49fe54ab8-6a3d-85"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f49fe54ab8-6a3d-85","watchdog":false,"workers_joined":1,"writes":[{"at_us":5326,"bytes":57,"kind":"head","slot":0},{"at_us":5334,"bytes":1,"kind":"body","slot":0},{"at_us":305557,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":85,"abi_return_us":5095,"end_us":5101,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5133,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5129},{"abi_enter_us":5144,"abi_return_us":5149,"end_us":5149,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5144,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5189,"operation":"read","slot":0,"start_us":5187},{"abi_enter_us":5204,"abi_return_us":305281,"end_us":305287,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5204,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":305349,"operation":"read","slot":0,"start_us":305347},{"abi_enter_us":305362,"abi_return_us":502765,"end_us":502769,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":305362,"status":"FastlyStatus::OK","timeout_ms":194},{"end_us":502828,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502821}],"schema":2,"slots":[{"at_us":502828,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f49fe54ab8-6a3d-85","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":501107,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":7101,"slot":0,"token":"18e160f4be33ce68-6a3d-86"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f4be33ce68-6a3d-86","watchdog":false,"workers_joined":1,"writes":[{"at_us":7109,"bytes":57,"kind":"head","slot":0},{"at_us":7119,"bytes":1,"kind":"body","slot":0},{"at_us":316846,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":64,"abi_return_us":6864,"end_us":6867,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6899,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6895},{"abi_enter_us":6912,"abi_return_us":6919,"end_us":6919,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6912,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6946,"operation":"read","slot":0,"start_us":6944},{"abi_enter_us":6957,"abi_return_us":316656,"end_us":316669,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6957,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":316830,"operation":"read","slot":0,"start_us":316824},{"abi_enter_us":316847,"abi_return_us":500989,"end_us":500993,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":316847,"status":"FastlyStatus::OK","timeout_ms":183},{"end_us":501075,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501056}],"schema":2,"slots":[{"at_us":501075,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f4be33ce68-6a3d-86","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":500781,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":7254,"slot":0,"token":"18e160f4dc442808-6a3d-87"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f4dc442808-6a3d-87","watchdog":false,"workers_joined":1,"writes":[{"at_us":7275,"bytes":57,"kind":"head","slot":0},{"at_us":7284,"bytes":1,"kind":"body","slot":0},{"at_us":308256,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":93,"operation":"handoff","start_us":93},{"abi_enter_us":110,"abi_return_us":6996,"end_us":7001,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7046,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7041},{"abi_enter_us":7061,"abi_return_us":7067,"end_us":7067,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7061,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7098,"operation":"read","slot":0,"start_us":7095},{"abi_enter_us":7110,"abi_return_us":307941,"end_us":307947,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7110,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":308028,"operation":"read","slot":0,"start_us":308025},{"abi_enter_us":308042,"abi_return_us":500701,"end_us":500705,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308042,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500760,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500753}],"schema":2,"slots":[{"at_us":500760,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f4dc442808-6a3d-87","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":502684,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6672,"slot":0,"token":"18e160f4fa4fb718-6a3d-88"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f4fa4fb718-6a3d-88","watchdog":false,"workers_joined":1,"writes":[{"at_us":6684,"bytes":57,"kind":"head","slot":0},{"at_us":6689,"bytes":1,"kind":"body","slot":0},{"at_us":311606,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"abi_enter_us":76,"abi_return_us":6403,"end_us":6408,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6454,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6450},{"abi_enter_us":6469,"abi_return_us":6475,"end_us":6476,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6469,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6505,"operation":"read","slot":0,"start_us":6503},{"abi_enter_us":6517,"abi_return_us":311301,"end_us":311310,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6517,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":311387,"operation":"read","slot":0,"start_us":311384},{"abi_enter_us":311402,"abi_return_us":502584,"end_us":502588,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":311402,"status":"FastlyStatus::OK","timeout_ms":188},{"end_us":502662,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502654}],"schema":2,"slots":[{"at_us":502662,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f4fa4fb718-6a3d-88","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501479,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5237,"slot":0,"token":"18e160f518789de0-6a3d-89"},{"path":"/readiness/siblings/1","received_us":5246,"slot":1,"token":"18e160f518789de0-6a3d-89"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160f518789de0-6a3d-89","watchdog":false,"workers_joined":2,"writes":[{"at_us":5254,"bytes":57,"kind":"head","slot":0},{"at_us":5262,"bytes":57,"kind":"head","slot":1},{"at_us":5271,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":96,"operation":"dispatch","outcome":"pending","slot":1,"start_us":76},{"abi_enter_us":112,"abi_return_us":4988,"end_us":4992,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5043,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5039},{"abi_enter_us":5055,"abi_return_us":5057,"end_us":5057,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5055,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5071,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5070},{"abi_enter_us":5084,"abi_return_us":5087,"end_us":5087,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5083,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5110,"operation":"read","slot":1,"start_us":5108},{"abi_enter_us":5122,"abi_return_us":5123,"end_us":5123,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5122,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5160,"operation":"read","slot":1,"start_us":5143},{"abi_enter_us":5171,"abi_return_us":5172,"end_us":5173,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5171,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5192,"operation":"read","slot":1,"start_us":5192},{"abi_enter_us":5202,"abi_return_us":5204,"end_us":5204,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5202,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5226,"operation":"read","slot":1,"start_us":5225},{"end_us":5239,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5236},{"abi_enter_us":5251,"abi_return_us":501395,"end_us":501401,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5251,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501462,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501456}],"schema":2,"slots":[{"at_us":501462,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5239,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f518789de0-6a3d-89","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":502369,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6722,"slot":1,"token":"18e160f536a3e9f0-6a3d-8a"},{"path":"/readiness/siblings/0","received_us":6728,"slot":0,"token":"18e160f536a3e9f0-6a3d-8a"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160f536a3e9f0-6a3d-8a","watchdog":false,"workers_joined":2,"writes":[{"at_us":6733,"bytes":57,"kind":"head","slot":1},{"at_us":6740,"bytes":57,"kind":"head","slot":0},{"at_us":14088,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":139,"operation":"dispatch","outcome":"pending","slot":1,"start_us":72},{"abi_enter_us":227,"abi_return_us":6461,"end_us":6463,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":227,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6493,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6490},{"abi_enter_us":6506,"abi_return_us":6508,"end_us":6508,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6506,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6523,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6522},{"abi_enter_us":6532,"abi_return_us":13827,"end_us":13828,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6532,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":13873,"operation":"read","slot":1,"start_us":13870},{"abi_enter_us":13889,"abi_return_us":13892,"end_us":13893,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":13889,"status":"FastlyStatus::OK","timeout_ms":486},{"bytes":0,"capacity":16384,"end_us":13917,"operation":"read","slot":1,"start_us":13917},{"end_us":13930,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":13927},{"abi_enter_us":13945,"abi_return_us":502292,"end_us":502297,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":13945,"status":"FastlyStatus::OK","timeout_ms":486},{"end_us":502351,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502344}],"schema":2,"slots":[{"at_us":502351,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":13930,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f536a3e9f0-6a3d-8a","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501819,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":7734,"slot":0,"token":"18e160f554d41800-6a3d-8b"},{"path":"/readiness/siblings/1","received_us":7746,"slot":1,"token":"18e160f554d41800-6a3d-8b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e160f554d41800-6a3d-8b","watchdog":false,"workers_joined":2,"writes":[{"at_us":7756,"bytes":57,"kind":"head","slot":0},{"at_us":7762,"bytes":57,"kind":"head","slot":1},{"at_us":7769,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":308,"operation":"dispatch","outcome":"pending","slot":1,"start_us":123},{"body_reads":0,"deadline_us":500000,"end_us":353,"operation":"handoff","start_us":353},{"abi_enter_us":374,"abi_return_us":7585,"end_us":7591,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":374,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7644,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7640},{"abi_enter_us":7666,"abi_return_us":7669,"end_us":7669,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":7666,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":7689,"operation":"head-poll","outcome":"ready","slot":1,"start_us":7688},{"abi_enter_us":7703,"abi_return_us":7706,"end_us":7706,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7703,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7743,"operation":"read","slot":1,"start_us":7741},{"abi_enter_us":7759,"abi_return_us":7760,"end_us":7760,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7759,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7801,"operation":"read","slot":1,"start_us":7800},{"abi_enter_us":7810,"abi_return_us":7811,"end_us":7811,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7810,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7833,"operation":"read","slot":1,"start_us":7833},{"abi_enter_us":7842,"abi_return_us":7844,"end_us":7845,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7842,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":1,"end_us":7867,"operation":"read","slot":1,"start_us":7867},{"end_us":7879,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":7876},{"abi_enter_us":7894,"abi_return_us":501730,"end_us":501736,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":7894,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":501797,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501791}],"schema":2,"slots":[{"at_us":501797,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":7879,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f554d41800-6a3d-8b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":502148,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":7684,"slot":0,"token":"18e160f57303fbd8-6a3d-8c"},{"path":"/readiness/siblings/1","received_us":7689,"slot":1,"token":"18e160f57303fbd8-6a3d-8c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160f57303fbd8-6a3d-8c","watchdog":false,"workers_joined":2,"writes":[{"at_us":7691,"bytes":57,"kind":"head","slot":0},{"at_us":7695,"bytes":57,"kind":"head","slot":1},{"at_us":7701,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":80,"operation":"dispatch","outcome":"pending","slot":1,"start_us":63},{"body_reads":0,"deadline_us":500000,"end_us":100,"operation":"handoff","start_us":100},{"abi_enter_us":110,"abi_return_us":7433,"end_us":7434,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7456,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7453},{"abi_enter_us":7467,"abi_return_us":7468,"end_us":7468,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":7467,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":7481,"operation":"head-poll","outcome":"ready","slot":1,"start_us":7480},{"abi_enter_us":7489,"abi_return_us":7494,"end_us":7494,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7489,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":3,"capacity":16384,"end_us":7518,"operation":"read","slot":1,"start_us":7515},{"abi_enter_us":7529,"abi_return_us":7530,"end_us":7531,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7528,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":7551,"operation":"read","slot":1,"start_us":7550},{"end_us":7561,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":7559},{"abi_enter_us":7572,"abi_return_us":502062,"end_us":502069,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":7572,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":502132,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502125}],"schema":2,"slots":[{"at_us":502132,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":7561,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f57303fbd8-6a3d-8c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":502509,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5359,"slot":0,"token":"18e160f591341e30-6a3d-8d"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f591341e30-6a3d-8d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5368,"bytes":57,"kind":"head","slot":0},{"at_us":5373,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":60,"abi_return_us":5093,"end_us":5095,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5128,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5124},{"abi_enter_us":5141,"abi_return_us":5145,"end_us":5145,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5141,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5169,"operation":"read","slot":0,"start_us":5167},{"abi_enter_us":5186,"abi_return_us":499859,"end_us":499865,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5186,"status":"FastlyStatus::OK","timeout_ms":494},{"abi_enter_us":499927,"abi_return_us":502449,"end_us":502451,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":499927,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502491,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502485}],"schema":2,"slots":[{"at_us":502491,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f591341e30-6a3d-8d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":503098,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":7722,"slot":0,"token":"18e160f5af6f96e0-6a3d-8e"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f5af6f96e0-6a3d-8e","watchdog":false,"workers_joined":1,"writes":[{"at_us":7741,"bytes":57,"kind":"head","slot":0},{"at_us":7746,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":78,"abi_return_us":7472,"end_us":7478,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7529,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7525},{"abi_enter_us":7543,"abi_return_us":7549,"end_us":7549,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7543,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7578,"operation":"read","slot":0,"start_us":7576},{"abi_enter_us":7593,"abi_return_us":502930,"end_us":502936,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":7593,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":503035,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502989}],"schema":2,"slots":[{"at_us":503035,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f5af6f96e0-6a3d-8e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":500561,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5224,"slot":0,"token":"18e160f5cd97c610-6a3d-8f"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f5cd97c610-6a3d-8f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5238,"bytes":57,"kind":"head","slot":0},{"at_us":5243,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"abi_enter_us":77,"abi_return_us":5009,"end_us":5014,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5055,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5051},{"abi_enter_us":5068,"abi_return_us":5078,"end_us":5079,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5068,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5106,"operation":"read","slot":0,"start_us":5103},{"abi_enter_us":5118,"abi_return_us":500463,"end_us":500469,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5118,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500540,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500533}],"schema":2,"slots":[{"at_us":500540,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f5cd97c610-6a3d-8f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":502772,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":7649,"slot":0,"token":"18e160f5eb9a67f8-6a3d-90"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f5eb9a67f8-6a3d-90","watchdog":false,"workers_joined":1,"writes":[{"at_us":7660,"bytes":57,"kind":"head","slot":0},{"at_us":7665,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"abi_enter_us":79,"abi_return_us":7392,"end_us":7393,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7417,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7413},{"abi_enter_us":7428,"abi_return_us":7433,"end_us":7433,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7428,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7458,"operation":"read","slot":0,"start_us":7456},{"abi_enter_us":7469,"abi_return_us":502590,"end_us":502596,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":7469,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":502747,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502739}],"schema":2,"slots":[{"at_us":502747,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f5eb9a67f8-6a3d-90","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":6029,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6074,"slot":0,"token":"18e160f609be39f8-6a3d-91"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f609be39f8-6a3d-91","watchdog":false,"workers_joined":1,"writes":[{"at_us":6086,"bytes":57,"kind":"head","slot":0},{"at_us":6100,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":75,"abi_return_us":5842,"end_us":5843,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5867,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5864},{"abi_enter_us":5882,"abi_return_us":5886,"end_us":5887,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5882,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5911,"operation":"read","slot":0,"start_us":5909},{"abi_enter_us":5923,"abi_return_us":5923,"end_us":5924,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5923,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5943,"operation":"read","slot":0,"start_us":5943},{"abi_enter_us":5953,"abi_return_us":5954,"end_us":5954,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5953,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5978,"operation":"read","slot":0,"start_us":5978},{"abi_enter_us":5987,"abi_return_us":5989,"end_us":5989,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5987,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":6008,"operation":"read","slot":0,"start_us":6008},{"end_us":6018,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6017}],"schema":2,"slots":[{"at_us":6018,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f609be39f8-6a3d-91","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":5697,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5766,"slot":0,"token":"18e160f60a708680-6a3d-92"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f60a708680-6a3d-92","watchdog":false,"workers_joined":1,"writes":[{"at_us":5774,"bytes":57,"kind":"head","slot":0},{"at_us":5780,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":25,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"abi_enter_us":50,"abi_return_us":5561,"end_us":5562,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5592,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5586},{"abi_enter_us":5605,"abi_return_us":5608,"end_us":5609,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5605,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5637,"operation":"read","slot":0,"start_us":5635},{"abi_enter_us":5649,"abi_return_us":5650,"end_us":5651,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5649,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5674,"operation":"read","slot":0,"start_us":5674},{"end_us":5685,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5684}],"schema":2,"slots":[{"at_us":5685,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f60a708680-6a3d-92","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":6016,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6000,"slot":0,"token":"18e160f60af045a0-6a3d-93"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f60af045a0-6a3d-93","watchdog":false,"workers_joined":1,"writes":[{"at_us":6007,"bytes":57,"kind":"head","slot":0},{"at_us":6011,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"body_reads":0,"deadline_us":500000,"end_us":38,"operation":"handoff","start_us":38},{"abi_enter_us":48,"abi_return_us":5837,"end_us":5837,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":48,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5857,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5856},{"abi_enter_us":5870,"abi_return_us":5873,"end_us":5873,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5870,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5897,"operation":"read","slot":0,"start_us":5896},{"abi_enter_us":5909,"abi_return_us":5909,"end_us":5910,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5909,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5931,"operation":"read","slot":0,"start_us":5931},{"abi_enter_us":5941,"abi_return_us":5942,"end_us":5942,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5941,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5962,"operation":"read","slot":0,"start_us":5962},{"abi_enter_us":5973,"abi_return_us":5974,"end_us":5975,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5972,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5994,"operation":"read","slot":0,"start_us":5993},{"end_us":6003,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6003}],"schema":2,"slots":[{"at_us":6003,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f60af045a0-6a3d-93","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":5308,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5391,"slot":0,"token":"18e160f60b7b5b18-6a3d-94"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f60b7b5b18-6a3d-94","watchdog":false,"workers_joined":1,"writes":[{"at_us":5399,"bytes":57,"kind":"head","slot":0},{"at_us":5404,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":21,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"body_reads":0,"deadline_us":500000,"end_us":36,"operation":"handoff","start_us":36},{"abi_enter_us":51,"abi_return_us":5199,"end_us":5200,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5216,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5215},{"abi_enter_us":5228,"abi_return_us":5230,"end_us":5230,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5228,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5255,"operation":"read","slot":0,"start_us":5254},{"abi_enter_us":5264,"abi_return_us":5265,"end_us":5265,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5264,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5288,"operation":"read","slot":0,"start_us":5288},{"end_us":5297,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5296}],"schema":2,"slots":[{"at_us":5297,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f60b7b5b18-6a3d-94","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5383,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5443,"slot":0,"token":"18e160f60bf4d4c0-6a3d-95"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f60bf4d4c0-6a3d-95","watchdog":false,"workers_joined":1,"writes":[{"at_us":5450,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"abi_enter_us":44,"abi_return_us":5304,"end_us":5305,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5324,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5322},{"abi_enter_us":5334,"abi_return_us":5336,"end_us":5336,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5334,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5360,"operation":"read","slot":0,"start_us":5360},{"end_us":5372,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5371}],"schema":2,"slots":[{"at_us":5372,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f60bf4d4c0-6a3d-95","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6963,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6973,"slot":0,"token":"18e160f60c683bb8-6a3d-96"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f60c683bb8-6a3d-96","watchdog":false,"workers_joined":1,"writes":[{"at_us":6979,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":17,"operation":"dispatch","outcome":"pending","slot":0,"start_us":6},{"abi_enter_us":45,"abi_return_us":6868,"end_us":6869,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6896,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6894},{"abi_enter_us":6908,"abi_return_us":6910,"end_us":6910,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6908,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6936,"operation":"read","slot":0,"start_us":6935},{"end_us":6949,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6947}],"schema":2,"slots":[{"at_us":6949,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f60c683bb8-6a3d-96","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6220,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6300,"slot":0,"token":"18e160f60cf3ce30-6a3d-97"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f60cf3ce30-6a3d-97","watchdog":false,"workers_joined":1,"writes":[{"at_us":6305,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"body_reads":0,"deadline_us":500000,"end_us":49,"operation":"handoff","start_us":49},{"abi_enter_us":61,"abi_return_us":6150,"end_us":6150,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6166,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6165},{"abi_enter_us":6176,"abi_return_us":6176,"end_us":6177,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6176,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6197,"operation":"read","slot":0,"start_us":6197},{"end_us":6207,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6206}],"schema":2,"slots":[{"at_us":6207,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f60cf3ce30-6a3d-97","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":7545,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":7627,"slot":0,"token":"18e160f60d756210-6a3d-98"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f60d756210-6a3d-98","watchdog":false,"workers_joined":1,"writes":[{"at_us":7633,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":18,"operation":"dispatch","outcome":"pending","slot":0,"start_us":6},{"body_reads":0,"deadline_us":500000,"end_us":37,"operation":"handoff","start_us":37},{"abi_enter_us":47,"abi_return_us":7474,"end_us":7475,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7493,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7492},{"abi_enter_us":7502,"abi_return_us":7503,"end_us":7503,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7502,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":7524,"operation":"read","slot":0,"start_us":7524},{"end_us":7534,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":7534}],"schema":2,"slots":[{"at_us":7534,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f60d756210-6a3d-98","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":7538,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":7634,"slot":0,"token":"18e160f60e0b0a90-6a3d-99"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f60e0b0a90-6a3d-99","watchdog":false,"workers_joined":1,"writes":[{"at_us":7641,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":7},{"abi_enter_us":41,"abi_return_us":7495,"end_us":7496,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7514,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":7511},{"end_us":7526,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":7526}],"schema":2,"slots":[{"at_us":7526,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f60e0b0a90-6a3d-99","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":7537,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":7644,"slot":0,"token":"18e160f60e9df008-6a3d-9a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f60e9df008-6a3d-9a","watchdog":false,"workers_joined":1,"writes":[{"at_us":7651,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":23,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"abi_enter_us":39,"abi_return_us":7497,"end_us":7498,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":39,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7513,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":7512},{"end_us":7524,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":7524}],"schema":2,"slots":[{"at_us":7524,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f60e9df008-6a3d-9a","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":7587,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":7635,"slot":0,"token":"18e160f60f318548-6a3d-9b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f60f318548-6a3d-9b","watchdog":false,"workers_joined":1,"writes":[{"at_us":7642,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":21,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"body_reads":0,"deadline_us":500000,"end_us":35,"operation":"handoff","start_us":35},{"abi_enter_us":44,"abi_return_us":7540,"end_us":7541,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":44,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7562,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":7560},{"end_us":7574,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":7574}],"schema":2,"slots":[{"at_us":7574,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f60f318548-6a3d-9b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5438,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5530,"slot":0,"token":"18e160f60fc4eba8-6a3d-9c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f60fc4eba8-6a3d-9c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5536,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":16,"operation":"dispatch","outcome":"pending","slot":0,"start_us":6},{"body_reads":0,"deadline_us":500000,"end_us":28,"operation":"handoff","start_us":28},{"abi_enter_us":37,"abi_return_us":5399,"end_us":5400,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":37,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5416,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5414},{"end_us":5426,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5426}],"schema":2,"slots":[{"at_us":5426,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f60fc4eba8-6a3d-9c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":62893,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":7631,"slot":0,"token":"18e160f610370a80-6a3d-9d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f610370a80-6a3d-9d","watchdog":false,"workers_joined":1,"writes":[{"at_us":7638,"bytes":66,"kind":"head","slot":0},{"at_us":7642,"bytes":6,"kind":"body","slot":0},{"at_us":62912,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":17,"operation":"dispatch","outcome":"pending","slot":0,"start_us":5},{"abi_enter_us":30,"abi_return_us":7503,"end_us":7504,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":30,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7526,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7524},{"abi_enter_us":7536,"abi_return_us":7539,"end_us":7540,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7536,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7559,"operation":"read","slot":0,"start_us":7558},{"abi_enter_us":7569,"abi_return_us":62810,"end_us":62814,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7569,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":1,"end_us":62863,"operation":"read","slot":0,"start_us":62860},{"end_us":62880,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":62876}],"schema":2,"slots":[{"at_us":62880,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f610370a80-6a3d-9d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59739,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6964,"slot":0,"token":"18e160f61423a248-6a3d-9e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f61423a248-6a3d-9e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6971,"bytes":66,"kind":"head","slot":0},{"at_us":6976,"bytes":6,"kind":"body","slot":0},{"at_us":59843,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":51,"abi_return_us":6773,"end_us":6775,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6801,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6797},{"abi_enter_us":6814,"abi_return_us":6818,"end_us":6818,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6814,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6843,"operation":"read","slot":0,"start_us":6842},{"abi_enter_us":6853,"abi_return_us":59653,"end_us":59656,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6853,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":59711,"operation":"read","slot":0,"start_us":59707},{"end_us":59726,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59723}],"schema":2,"slots":[{"at_us":59726,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f61423a248-6a3d-9e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":65163,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5296,"slot":0,"token":"18e160f617dfbc00-6a3d-9f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f617dfbc00-6a3d-9f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5304,"bytes":66,"kind":"head","slot":0},{"at_us":5311,"bytes":6,"kind":"body","slot":0},{"at_us":65181,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":55,"operation":"handoff","start_us":55},{"abi_enter_us":64,"abi_return_us":5017,"end_us":5018,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5035,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5033},{"abi_enter_us":5045,"abi_return_us":5048,"end_us":5048,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5045,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5068,"operation":"read","slot":0,"start_us":5067},{"abi_enter_us":5079,"abi_return_us":65069,"end_us":65078,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5079,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":65129,"operation":"read","slot":0,"start_us":65125},{"end_us":65147,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":65142}],"schema":2,"slots":[{"at_us":65147,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f617dfbc00-6a3d-9f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":62883,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5234,"slot":0,"token":"18e160f61bf2a078-6a3d-a0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f61bf2a078-6a3d-a0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5248,"bytes":66,"kind":"head","slot":0},{"at_us":5254,"bytes":6,"kind":"body","slot":0},{"at_us":63066,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"abi_enter_us":79,"abi_return_us":4983,"end_us":4987,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5031,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5027},{"abi_enter_us":5045,"abi_return_us":5051,"end_us":5051,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5044,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5083,"operation":"read","slot":0,"start_us":5080},{"abi_enter_us":5095,"abi_return_us":62800,"end_us":62803,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5095,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":62854,"operation":"read","slot":0,"start_us":62851},{"end_us":62869,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":62865}],"schema":2,"slots":[{"at_us":62869,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f61bf2a078-6a3d-a0","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":5044,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5221,"slot":0,"token":"18e160f61fdb38d0-6a3d-a1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f61fdb38d0-6a3d-a1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":68,"abi_return_us":4985,"end_us":4989,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5019,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5014},{"end_us":5031,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5031}],"schema":2,"slots":[{"at_us":5031,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f61fdb38d0-6a3d-a1","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":6210,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6363,"slot":0,"token":"18e160f62044d7e0-6a3d-a2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f62044d7e0-6a3d-a2","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":61,"abi_return_us":6152,"end_us":6156,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6181,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6177},{"end_us":6196,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6196}],"schema":2,"slots":[{"at_us":6196,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f62044d7e0-6a3d-a2","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":5557,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5644,"slot":0,"token":"18e160f620be8838-6a3d-a3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f620be8838-6a3d-a3","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":43,"operation":"handoff","start_us":43},{"abi_enter_us":52,"abi_return_us":5477,"end_us":5480,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5505,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5502},{"end_us":5516,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5516}],"schema":2,"slots":[{"at_us":5516,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f620be8838-6a3d-a3","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":7543,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":7646,"slot":0,"token":"18e160f62130ca38-6a3d-a4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f62130ca38-6a3d-a4","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"body_reads":0,"deadline_us":500000,"end_us":45,"operation":"handoff","start_us":45},{"abi_enter_us":54,"abi_return_us":7493,"end_us":7495,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7518,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":7515},{"end_us":7530,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":7530}],"schema":2,"slots":[{"at_us":7530,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f62130ca38-6a3d-a4","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":61952,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5205,"slot":0,"token":"18e160f621bde350-6a3d-a5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f621bde350-6a3d-a5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5217,"bytes":57,"kind":"head","slot":0},{"at_us":5228,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"abi_enter_us":40,"abi_return_us":5041,"end_us":5042,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":40,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5061,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5058},{"bytes":1,"capacity":1,"end_us":5082,"operation":"read","slot":0,"start_us":5077},{"capacity":1,"end_us":61885,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5101},{"end_us":61937,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61931}],"schema":2,"slots":[{"at_us":61937,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f621bde350-6a3d-a5","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":60948,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5188,"slot":0,"token":"18e160f6259491b8-6a3d-a6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f6259491b8-6a3d-a6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5197,"bytes":57,"kind":"head","slot":0},{"at_us":5202,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":63,"abi_return_us":4923,"end_us":4924,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4949,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4945},{"abi_enter_us":4960,"abi_return_us":4967,"end_us":4967,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4960,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4987,"operation":"read","slot":0,"start_us":4986},{"abi_enter_us":4998,"abi_return_us":60859,"end_us":60861,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4998,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":1,"end_us":60915,"operation":"read","slot":0,"start_us":60911},{"end_us":60930,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60926}],"schema":2,"slots":[{"at_us":60930,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f6259491b8-6a3d-a6","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":59815,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5180,"slot":0,"token":"18e160f629639348-6a3d-a7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f629639348-6a3d-a7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5192,"bytes":57,"kind":"head","slot":0},{"at_us":5199,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":51,"abi_return_us":4911,"end_us":4914,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4942,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4938},{"abi_enter_us":4954,"abi_return_us":4958,"end_us":4959,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4954,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4982,"operation":"read","slot":0,"start_us":4980},{"abi_enter_us":4992,"abi_return_us":59722,"end_us":59724,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4992,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":16384,"end_us":59781,"operation":"read","slot":0,"start_us":59778},{"end_us":59798,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59795}],"schema":2,"slots":[{"at_us":59798,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f629639348-6a3d-a7","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":60649,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5200,"slot":0,"token":"18e160f62d2492e8-6a3d-a8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f62d2492e8-6a3d-a8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5208,"bytes":57,"kind":"head","slot":0},{"at_us":5211,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"abi_enter_us":69,"abi_return_us":4955,"end_us":4957,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4985,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4982},{"abi_enter_us":4998,"abi_return_us":5007,"end_us":5008,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4998,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5043,"operation":"read","slot":0,"start_us":5041},{"abi_enter_us":5054,"abi_return_us":60552,"end_us":60561,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5054,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":60619,"operation":"read","slot":0,"start_us":60616},{"end_us":60634,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60630}],"schema":2,"slots":[{"at_us":60634,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f62d2492e8-6a3d-a8","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":64833,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":7679,"slot":0,"token":"18e160f631000410-6a3d-a9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f631000410-6a3d-a9","watchdog":false,"workers_joined":1,"writes":[{"at_us":7690,"bytes":57,"kind":"head","slot":0},{"at_us":7695,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"abi_enter_us":88,"abi_return_us":7457,"end_us":7459,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7487,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7483},{"abi_enter_us":7498,"abi_return_us":7502,"end_us":7503,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7497,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7529,"operation":"read","slot":0,"start_us":7527},{"abi_enter_us":7539,"abi_return_us":64587,"end_us":64598,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7538,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":64749,"operation":"read","slot":0,"start_us":64743},{"end_us":64805,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":64800}],"schema":2,"slots":[{"at_us":64805,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f631000410-6a3d-a9","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":500693,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":7682,"slot":0,"token":"18e160f6350b4f38-6a3d-aa"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f6350b4f38-6a3d-aa","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":57,"abi_return_us":500622,"end_us":500630,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500675,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500675}],"schema":2,"slots":[{"at_us":500675,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f6350b4f38-6a3d-aa","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":500576,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":7651,"slot":0,"token":"18e160f65304e0d0-6a3d-ab"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f65304e0d0-6a3d-ab","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":50,"abi_return_us":500361,"end_us":500501,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500558,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500558}],"schema":2,"slots":[{"at_us":500558,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f65304e0d0-6a3d-ab","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":502372,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5246,"slot":0,"token":"18e160f671120238-6a3d-ac"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f671120238-6a3d-ac","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"abi_enter_us":64,"abi_return_us":502293,"end_us":502302,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502354,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502354}],"schema":2,"slots":[{"at_us":502354,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f671120238-6a3d-ac","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":502320,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":7036,"slot":0,"token":"18e160f68f2482a0-6a3d-ad"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f68f2482a0-6a3d-ad","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":86,"abi_return_us":502187,"end_us":502212,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502297,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502297}],"schema":2,"slots":[{"at_us":502297,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f68f2482a0-6a3d-ad","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":503628,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5729,"slot":0,"token":"18e160f6ad357880-6a3d-ae"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f6ad357880-6a3d-ae","watchdog":false,"workers_joined":1,"writes":[{"at_us":5746,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":54,"abi_return_us":5503,"end_us":5506,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5547,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5543},{"abi_enter_us":5560,"abi_return_us":503523,"end_us":503530,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5560,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":503607,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":503599}],"schema":2,"slots":[{"at_us":503607,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f6ad357880-6a3d-ae","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":502138,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5251,"slot":0,"token":"18e160f6cb608de0-6a3d-af"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f6cb608de0-6a3d-af","watchdog":false,"workers_joined":1,"writes":[{"at_us":5263,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":80,"abi_return_us":4987,"end_us":4991,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5027,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5022},{"abi_enter_us":5039,"abi_return_us":502031,"end_us":502038,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5039,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502107,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502101}],"schema":2,"slots":[{"at_us":502107,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f6cb608de0-6a3d-af","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":500591,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5718,"slot":0,"token":"18e160f6e972fac0-6a3d-b0"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f6e972fac0-6a3d-b0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5737,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"abi_enter_us":83,"abi_return_us":5547,"end_us":5553,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5607,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5602},{"abi_enter_us":5622,"abi_return_us":500511,"end_us":500517,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5622,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500573,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500568}],"schema":2,"slots":[{"at_us":500573,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f6e972fac0-6a3d-b0","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":500917,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5968,"slot":0,"token":"18e160f7076963c0-6a3d-b1"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f7076963c0-6a3d-b1","watchdog":false,"workers_joined":1,"writes":[{"at_us":5975,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"abi_enter_us":67,"abi_return_us":5707,"end_us":5709,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5737,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5733},{"abi_enter_us":5750,"abi_return_us":500832,"end_us":500836,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5750,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500893,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500881}],"schema":2,"slots":[{"at_us":500893,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f7076963c0-6a3d-b1","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":500410,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6445,"slot":0,"token":"18e160f72564dda0-6a3d-b2"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f72564dda0-6a3d-b2","watchdog":false,"workers_joined":1,"writes":[{"at_us":6452,"bytes":57,"kind":"head","slot":0},{"at_us":6457,"bytes":1,"kind":"body","slot":0},{"at_us":316252,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":63,"abi_return_us":6163,"end_us":6164,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6184,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6181},{"abi_enter_us":6195,"abi_return_us":6199,"end_us":6199,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6195,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6221,"operation":"read","slot":0,"start_us":6219},{"abi_enter_us":6231,"abi_return_us":315979,"end_us":315983,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6230,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":316037,"operation":"read","slot":0,"start_us":316034},{"abi_enter_us":316049,"abi_return_us":500336,"end_us":500340,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":316049,"status":"FastlyStatus::OK","timeout_ms":183},{"end_us":500391,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500386}],"schema":2,"slots":[{"at_us":500391,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f72564dda0-6a3d-b2","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":500468,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":7644,"slot":0,"token":"18e160f743616cd8-6a3d-b3"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f743616cd8-6a3d-b3","watchdog":false,"workers_joined":1,"writes":[{"at_us":7654,"bytes":57,"kind":"head","slot":0},{"at_us":7659,"bytes":1,"kind":"body","slot":0},{"at_us":308363,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":51,"abi_return_us":7412,"end_us":7414,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7439,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7436},{"abi_enter_us":7451,"abi_return_us":7454,"end_us":7455,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7451,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7479,"operation":"read","slot":0,"start_us":7477},{"abi_enter_us":7489,"abi_return_us":308151,"end_us":308158,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7489,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":308246,"operation":"read","slot":0,"start_us":308243},{"abi_enter_us":308261,"abi_return_us":500373,"end_us":500377,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308261,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500448,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500441}],"schema":2,"slots":[{"at_us":500448,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f743616cd8-6a3d-b3","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":500433,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5192,"slot":0,"token":"18e160f7615ae318-6a3d-b4"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f7615ae318-6a3d-b4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5200,"bytes":57,"kind":"head","slot":0},{"at_us":5211,"bytes":1,"kind":"body","slot":0},{"at_us":311537,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":71,"abi_return_us":4899,"end_us":4901,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4930,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4926},{"abi_enter_us":4943,"abi_return_us":4946,"end_us":4947,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4943,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4971,"operation":"read","slot":0,"start_us":4969},{"abi_enter_us":4982,"abi_return_us":311229,"end_us":311235,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4981,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":311300,"operation":"read","slot":0,"start_us":311297},{"abi_enter_us":311315,"abi_return_us":500345,"end_us":500349,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":311315,"status":"FastlyStatus::OK","timeout_ms":188},{"end_us":500412,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500405}],"schema":2,"slots":[{"at_us":500412,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f7615ae318-6a3d-b4","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":502560,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6610,"slot":0,"token":"18e160f77f59c028-6a3d-b5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f77f59c028-6a3d-b5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6630,"bytes":57,"kind":"head","slot":0},{"at_us":6636,"bytes":1,"kind":"body","slot":0},{"at_us":311563,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"abi_enter_us":78,"abi_return_us":6404,"end_us":6408,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6458,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6453},{"abi_enter_us":6473,"abi_return_us":6477,"end_us":6477,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6473,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6508,"operation":"read","slot":0,"start_us":6506},{"abi_enter_us":6520,"abi_return_us":311335,"end_us":311342,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6520,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":311409,"operation":"read","slot":0,"start_us":311406},{"abi_enter_us":311421,"abi_return_us":502471,"end_us":502477,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":311421,"status":"FastlyStatus::OK","timeout_ms":188},{"end_us":502539,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502532}],"schema":2,"slots":[{"at_us":502539,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f77f59c028-6a3d-b5","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":503095,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5360,"slot":1,"token":"18e160f79d7c9ff8-6a3d-b6"},{"path":"/readiness/siblings/0","received_us":5366,"slot":0,"token":"18e160f79d7c9ff8-6a3d-b6"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160f79d7c9ff8-6a3d-b6","watchdog":false,"workers_joined":2,"writes":[{"at_us":5382,"bytes":57,"kind":"head","slot":0},{"at_us":5395,"bytes":57,"kind":"head","slot":1},{"at_us":5407,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":96,"operation":"dispatch","outcome":"pending","slot":1,"start_us":75},{"abi_enter_us":116,"abi_return_us":5089,"end_us":5093,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5144,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5139},{"abi_enter_us":5158,"abi_return_us":5161,"end_us":5161,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5158,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5176,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5175},{"abi_enter_us":5189,"abi_return_us":5192,"end_us":5193,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5189,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5214,"operation":"read","slot":1,"start_us":5212},{"abi_enter_us":5225,"abi_return_us":5226,"end_us":5226,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5225,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5251,"operation":"read","slot":1,"start_us":5244},{"abi_enter_us":5264,"abi_return_us":5265,"end_us":5265,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5264,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5283,"operation":"read","slot":1,"start_us":5282},{"abi_enter_us":5293,"abi_return_us":5295,"end_us":5295,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5293,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5315,"operation":"read","slot":1,"start_us":5315},{"end_us":5327,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5324},{"abi_enter_us":5341,"abi_return_us":503008,"end_us":503014,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5341,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":503064,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":503055}],"schema":2,"slots":[{"at_us":503064,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5327,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f79d7c9ff8-6a3d-b6","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501021,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5458,"slot":0,"token":"18e160f7bbc4f5a0-6a3d-b7"},{"path":"/readiness/siblings/1","received_us":5501,"slot":1,"token":"18e160f7bbc4f5a0-6a3d-b7"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e160f7bbc4f5a0-6a3d-b7","watchdog":false,"workers_joined":2,"writes":[{"at_us":5476,"bytes":57,"kind":"head","slot":0},{"at_us":5509,"bytes":57,"kind":"head","slot":1},{"at_us":5525,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":71,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":155,"operation":"dispatch","outcome":"pending","slot":1,"start_us":133},{"abi_enter_us":169,"abi_return_us":5189,"end_us":5193,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":169,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5231,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5227},{"abi_enter_us":5243,"abi_return_us":5245,"end_us":5245,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5243,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5259,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5259},{"abi_enter_us":5269,"abi_return_us":5271,"end_us":5271,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5269,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5300,"operation":"read","slot":1,"start_us":5298},{"abi_enter_us":5312,"abi_return_us":5314,"end_us":5315,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5312,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5337,"operation":"read","slot":1,"start_us":5336},{"end_us":5349,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5346},{"abi_enter_us":5361,"abi_return_us":500880,"end_us":500899,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5361,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501000,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500982}],"schema":2,"slots":[{"at_us":501000,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5349,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f7bbc4f5a0-6a3d-b7","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":502606,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":7109,"slot":1,"token":"18e160f7d9e44b30-6a3d-b8"},{"path":"/readiness/siblings/0","received_us":7116,"slot":0,"token":"18e160f7d9e44b30-6a3d-b8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e160f7d9e44b30-6a3d-b8","watchdog":false,"workers_joined":2,"writes":[{"at_us":7128,"bytes":57,"kind":"head","slot":0},{"at_us":7137,"bytes":57,"kind":"head","slot":1},{"at_us":7142,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":123,"operation":"dispatch","outcome":"pending","slot":1,"start_us":93},{"body_reads":0,"deadline_us":500000,"end_us":151,"operation":"handoff","start_us":151},{"abi_enter_us":164,"abi_return_us":6761,"end_us":6765,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":164,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6814,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6810},{"abi_enter_us":6828,"abi_return_us":6829,"end_us":6830,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6828,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6844,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6843},{"abi_enter_us":6854,"abi_return_us":6857,"end_us":6857,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6854,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6879,"operation":"read","slot":1,"start_us":6876},{"abi_enter_us":6892,"abi_return_us":6893,"end_us":6893,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6892,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6912,"operation":"read","slot":1,"start_us":6911},{"abi_enter_us":6921,"abi_return_us":6922,"end_us":6922,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6921,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6940,"operation":"read","slot":1,"start_us":6940},{"abi_enter_us":6949,"abi_return_us":6951,"end_us":6951,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6949,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6975,"operation":"read","slot":1,"start_us":6974},{"end_us":6986,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6983},{"abi_enter_us":6997,"abi_return_us":502518,"end_us":502524,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6997,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":502589,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502584}],"schema":2,"slots":[{"at_us":502589,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6986,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f7d9e44b30-6a3d-b8","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501346,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5436,"slot":1,"token":"18e160f7f8247318-6a3d-b9"},{"path":"/readiness/siblings/0","received_us":5439,"slot":0,"token":"18e160f7f8247318-6a3d-b9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e160f7f8247318-6a3d-b9","watchdog":false,"workers_joined":2,"writes":[{"at_us":5450,"bytes":57,"kind":"head","slot":1},{"at_us":5457,"bytes":57,"kind":"head","slot":0},{"at_us":10849,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":79,"operation":"dispatch","outcome":"pending","slot":1,"start_us":65},{"body_reads":0,"deadline_us":500000,"end_us":103,"operation":"handoff","start_us":103},{"abi_enter_us":114,"abi_return_us":5173,"end_us":5176,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":114,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5208,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5205},{"abi_enter_us":5223,"abi_return_us":5225,"end_us":5225,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5223,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5240,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5239},{"abi_enter_us":5250,"abi_return_us":10557,"end_us":10561,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5250,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":10671,"operation":"read","slot":1,"start_us":10668},{"abi_enter_us":10688,"abi_return_us":10692,"end_us":10693,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10688,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":16384,"end_us":10720,"operation":"read","slot":1,"start_us":10719},{"end_us":10733,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10731},{"abi_enter_us":10751,"abi_return_us":501242,"end_us":501247,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":10751,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":501328,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501320}],"schema":2,"slots":[{"at_us":501328,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":10733,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f7f8247318-6a3d-b9","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":502419,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5624,"slot":0,"token":"18e160f8163f8ab8-6a3d-ba"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f8163f8ab8-6a3d-ba","watchdog":false,"workers_joined":1,"writes":[{"at_us":5647,"bytes":57,"kind":"head","slot":0},{"at_us":5656,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":86,"abi_return_us":5356,"end_us":5362,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5423,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5418},{"abi_enter_us":5438,"abi_return_us":5442,"end_us":5442,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5438,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5472,"operation":"read","slot":0,"start_us":5470},{"abi_enter_us":5484,"abi_return_us":502298,"end_us":502308,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5484,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502373,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502367}],"schema":2,"slots":[{"at_us":502373,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f8163f8ab8-6a3d-ba","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":500622,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5226,"slot":0,"token":"18e160f8345fda48-6a3d-bb"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e160f8345fda48-6a3d-bb","watchdog":false,"workers_joined":1,"writes":[{"at_us":5244,"bytes":57,"kind":"head","slot":0},{"at_us":5251,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":56,"abi_return_us":5006,"end_us":5012,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5065,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5061},{"abi_enter_us":5080,"abi_return_us":5083,"end_us":5084,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5080,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5116,"operation":"read","slot":0,"start_us":5113},{"abi_enter_us":5130,"abi_return_us":500512,"end_us":500519,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5129,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500596,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500590}],"schema":2,"slots":[{"at_us":500596,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f8345fda48-6a3d-bb","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":502388,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5484,"slot":0,"token":"18e160f85269fa28-6a3d-bc"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f85269fa28-6a3d-bc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5502,"bytes":57,"kind":"head","slot":0},{"at_us":5509,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":93,"abi_return_us":5207,"end_us":5213,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5258,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5254},{"abi_enter_us":5272,"abi_return_us":5276,"end_us":5276,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5271,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5304,"operation":"read","slot":0,"start_us":5302},{"abi_enter_us":5321,"abi_return_us":502277,"end_us":502283,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5321,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502362,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502354}],"schema":2,"slots":[{"at_us":502362,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f85269fa28-6a3d-bc","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":501928,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6264,"slot":0,"token":"18e160f8708d6e68-6a3d-bd"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e160f8708d6e68-6a3d-bd","watchdog":false,"workers_joined":1,"writes":[{"at_us":6283,"bytes":57,"kind":"head","slot":0},{"at_us":6289,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":113,"abi_return_us":5977,"end_us":5981,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6028,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6024},{"abi_enter_us":6045,"abi_return_us":6054,"end_us":6054,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6045,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6081,"operation":"read","slot":0,"start_us":6079},{"abi_enter_us":6093,"abi_return_us":499559,"end_us":499565,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6093,"status":"FastlyStatus::OK","timeout_ms":493},{"abi_enter_us":499619,"abi_return_us":501872,"end_us":501873,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":499619,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501910,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501902}],"schema":2,"slots":[{"at_us":501910,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f8708d6e68-6a3d-bd","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":6036,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6091,"slot":0,"token":"18e160f88eaf5820-6a3d-be"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f88eaf5820-6a3d-be","watchdog":false,"workers_joined":1,"writes":[{"at_us":6114,"bytes":57,"kind":"head","slot":0},{"at_us":6122,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":121,"abi_return_us":5795,"end_us":5798,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":121,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5841,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5837},{"abi_enter_us":5856,"abi_return_us":5862,"end_us":5862,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5856,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5887,"operation":"read","slot":0,"start_us":5885},{"abi_enter_us":5903,"abi_return_us":5904,"end_us":5904,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5903,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5938,"operation":"read","slot":0,"start_us":5928},{"abi_enter_us":5950,"abi_return_us":5950,"end_us":5951,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5950,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5972,"operation":"read","slot":0,"start_us":5971},{"abi_enter_us":5984,"abi_return_us":5986,"end_us":5987,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5984,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":6008,"operation":"read","slot":0,"start_us":6008},{"end_us":6023,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6020}],"schema":2,"slots":[{"at_us":6023,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f88eaf5820-6a3d-be","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":7563,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":7720,"slot":0,"token":"18e160f88f630438-6a3d-bf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f88f630438-6a3d-bf","watchdog":false,"workers_joined":1,"writes":[{"at_us":7735,"bytes":57,"kind":"head","slot":0},{"at_us":7744,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":78,"abi_return_us":7424,"end_us":7428,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7465,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7461},{"abi_enter_us":7475,"abi_return_us":7479,"end_us":7480,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7475,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":3,"capacity":16384,"end_us":7506,"operation":"read","slot":0,"start_us":7505},{"abi_enter_us":7519,"abi_return_us":7521,"end_us":7521,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7519,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":7541,"operation":"read","slot":0,"start_us":7540},{"end_us":7552,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":7549}],"schema":2,"slots":[{"at_us":7552,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f88f630438-6a3d-bf","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":5494,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5461,"slot":0,"token":"18e160f8900f6138-6a3d-c0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f8900f6138-6a3d-c0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5475,"bytes":57,"kind":"head","slot":0},{"at_us":5482,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":72,"abi_return_us":5248,"end_us":5254,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5313,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5308},{"abi_enter_us":5329,"abi_return_us":5337,"end_us":5337,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5329,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5368,"operation":"read","slot":0,"start_us":5365},{"abi_enter_us":5380,"abi_return_us":5381,"end_us":5381,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5380,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5402,"operation":"read","slot":0,"start_us":5401},{"abi_enter_us":5411,"abi_return_us":5412,"end_us":5412,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5411,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5434,"operation":"read","slot":0,"start_us":5434},{"abi_enter_us":5444,"abi_return_us":5446,"end_us":5446,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5444,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5464,"operation":"read","slot":0,"start_us":5464},{"end_us":5477,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5473}],"schema":2,"slots":[{"at_us":5477,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f8900f6138-6a3d-c0","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":6224,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6363,"slot":0,"token":"18e160f890996158-6a3d-c1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f890996158-6a3d-c1","watchdog":false,"workers_joined":1,"writes":[{"at_us":6378,"bytes":57,"kind":"head","slot":0},{"at_us":6385,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":84,"abi_return_us":6115,"end_us":6116,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6138,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6136},{"abi_enter_us":6147,"abi_return_us":6150,"end_us":6151,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6147,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6173,"operation":"read","slot":0,"start_us":6172},{"abi_enter_us":6182,"abi_return_us":6183,"end_us":6183,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6181,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6204,"operation":"read","slot":0,"start_us":6204},{"end_us":6213,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6212}],"schema":2,"slots":[{"at_us":6213,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f890996158-6a3d-c1","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5260,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5348,"slot":0,"token":"18e160f89124ec00-6a3d-c2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f89124ec00-6a3d-c2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5365,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":66,"abi_return_us":5125,"end_us":5131,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5184,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5180},{"abi_enter_us":5199,"abi_return_us":5203,"end_us":5203,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5199,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5228,"operation":"read","slot":0,"start_us":5226},{"end_us":5243,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5239}],"schema":2,"slots":[{"at_us":5243,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f89124ec00-6a3d-c2","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":7586,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":7697,"slot":0,"token":"18e160f8919ca858-6a3d-c3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f8919ca858-6a3d-c3","watchdog":false,"workers_joined":1,"writes":[{"at_us":7712,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":64,"abi_return_us":7452,"end_us":7458,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7512,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7508},{"abi_enter_us":7527,"abi_return_us":7530,"end_us":7531,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7527,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":7559,"operation":"read","slot":0,"start_us":7557},{"end_us":7573,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":7570}],"schema":2,"slots":[{"at_us":7573,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f8919ca858-6a3d-c3","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6357,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6406,"slot":0,"token":"18e160f89235fa58-6a3d-c4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f89235fa58-6a3d-c4","watchdog":false,"workers_joined":1,"writes":[{"at_us":6423,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"abi_enter_us":68,"abi_return_us":6244,"end_us":6249,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6291,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6287},{"abi_enter_us":6304,"abi_return_us":6309,"end_us":6309,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6304,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6332,"operation":"read","slot":0,"start_us":6330},{"end_us":6345,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6343}],"schema":2,"slots":[{"at_us":6345,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f89235fa58-6a3d-c4","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6076,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6124,"slot":0,"token":"18e160f892bd3388-6a3d-c5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f892bd3388-6a3d-c5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6138,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"abi_enter_us":68,"abi_return_us":5951,"end_us":5956,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6004,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6000},{"abi_enter_us":6020,"abi_return_us":6022,"end_us":6023,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6020,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6050,"operation":"read","slot":0,"start_us":6048},{"end_us":6064,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6061}],"schema":2,"slots":[{"at_us":6064,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f892bd3388-6a3d-c5","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6171,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6303,"slot":0,"token":"18e160f8933f5bd8-6a3d-c6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f8933f5bd8-6a3d-c6","watchdog":false,"workers_joined":1,"writes":[{"at_us":6310,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":62,"abi_return_us":6124,"end_us":6126,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6147,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6142},{"end_us":6159,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6158}],"schema":2,"slots":[{"at_us":6159,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f8933f5bd8-6a3d-c6","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5071,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5150,"slot":0,"token":"18e160f893bda3f8-6a3d-c7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f893bda3f8-6a3d-c7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5162,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":41,"abi_return_us":5023,"end_us":5025,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5047,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5044},{"end_us":5058,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5058}],"schema":2,"slots":[{"at_us":5058,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f893bda3f8-6a3d-c7","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5252,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5386,"slot":0,"token":"18e160f8942b4e30-6a3d-c8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f8942b4e30-6a3d-c8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5396,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"abi_enter_us":50,"abi_return_us":5203,"end_us":5205,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5229,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5226},{"end_us":5240,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5240}],"schema":2,"slots":[{"at_us":5240,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f8942b4e30-6a3d-c8","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":7361,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":7486,"slot":0,"token":"18e160f8949d3a40-6a3d-c9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f8949d3a40-6a3d-c9","watchdog":false,"workers_joined":1,"writes":[{"at_us":7494,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"abi_enter_us":63,"abi_return_us":7313,"end_us":7315,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7336,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":7333},{"end_us":7347,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":7347}],"schema":2,"slots":[{"at_us":7347,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f8949d3a40-6a3d-c9","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59911,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5827,"slot":0,"token":"18e160f8952ef6d8-6a3d-ca"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f8952ef6d8-6a3d-ca","watchdog":false,"workers_joined":1,"writes":[{"at_us":5846,"bytes":66,"kind":"head","slot":0},{"at_us":5850,"bytes":6,"kind":"body","slot":0},{"at_us":60038,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":42,"abi_return_us":5643,"end_us":5646,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":42,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5674,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5671},{"abi_enter_us":5685,"abi_return_us":5690,"end_us":5690,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5685,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5711,"operation":"read","slot":0,"start_us":5709},{"abi_enter_us":5725,"abi_return_us":59834,"end_us":59836,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5725,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":59880,"operation":"read","slot":0,"start_us":59877},{"end_us":59898,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59894}],"schema":2,"slots":[{"at_us":59898,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f8952ef6d8-6a3d-ca","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":61022,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5522,"slot":0,"token":"18e160f898fb2d90-6a3d-cb"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f898fb2d90-6a3d-cb","watchdog":false,"workers_joined":1,"writes":[{"at_us":5537,"bytes":66,"kind":"head","slot":0},{"at_us":5543,"bytes":6,"kind":"body","slot":0},{"at_us":61103,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":78,"abi_return_us":5308,"end_us":5312,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5349,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5346},{"abi_enter_us":5362,"abi_return_us":5369,"end_us":5369,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5362,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5399,"operation":"read","slot":0,"start_us":5398},{"abi_enter_us":5410,"abi_return_us":60874,"end_us":60880,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5410,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":60985,"operation":"read","slot":0,"start_us":60981},{"end_us":61005,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":61001}],"schema":2,"slots":[{"at_us":61005,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f898fb2d90-6a3d-cb","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":64983,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5463,"slot":0,"token":"18e160f89cea8478-6a3d-cc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f89cea8478-6a3d-cc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5485,"bytes":66,"kind":"head","slot":0},{"at_us":5491,"bytes":6,"kind":"body","slot":0},{"at_us":65180,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":73,"operation":"handoff","start_us":73},{"abi_enter_us":87,"abi_return_us":5157,"end_us":5161,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5197,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5194},{"abi_enter_us":5210,"abi_return_us":5217,"end_us":5217,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5210,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5240,"operation":"read","slot":0,"start_us":5239},{"abi_enter_us":5252,"abi_return_us":64883,"end_us":64890,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5252,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":64948,"operation":"read","slot":0,"start_us":64945},{"end_us":64965,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":64961}],"schema":2,"slots":[{"at_us":64965,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f89cea8478-6a3d-cc","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59609,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6944,"slot":0,"token":"18e160f8a11d25f0-6a3d-cd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f8a11d25f0-6a3d-cd","watchdog":false,"workers_joined":1,"writes":[{"at_us":6960,"bytes":66,"kind":"head","slot":0},{"at_us":6968,"bytes":6,"kind":"body","slot":0},{"at_us":59794,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":80,"abi_return_us":6665,"end_us":6670,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6719,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6715},{"abi_enter_us":6732,"abi_return_us":6740,"end_us":6741,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6732,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6774,"operation":"read","slot":0,"start_us":6772},{"abi_enter_us":6786,"abi_return_us":59500,"end_us":59504,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6786,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":59576,"operation":"read","slot":0,"start_us":59572},{"end_us":59593,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59589}],"schema":2,"slots":[{"at_us":59593,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e160f8a11d25f0-6a3d-cd","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":5523,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5638,"slot":0,"token":"18e160f8a4d5bd38-6a3d-ce"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f8a4d5bd38-6a3d-ce","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":65,"abi_return_us":5444,"end_us":5449,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5495,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5491},{"end_us":5509,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5509}],"schema":2,"slots":[{"at_us":5509,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f8a4d5bd38-6a3d-ce","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":7596,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":7699,"slot":0,"token":"18e160f8a54668f8-6a3d-cf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f8a54668f8-6a3d-cf","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":60,"abi_return_us":7536,"end_us":7541,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7570,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":7567},{"end_us":7583,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":7582}],"schema":2,"slots":[{"at_us":7583,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f8a54668f8-6a3d-cf","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":5444,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5557,"slot":0,"token":"18e160f8a5d5dba0-6a3d-d0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f8a5d5dba0-6a3d-d0","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"abi_enter_us":62,"abi_return_us":5390,"end_us":5393,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5418,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5414},{"end_us":5431,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5431}],"schema":2,"slots":[{"at_us":5431,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f8a5d5dba0-6a3d-d0","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":7439,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":7483,"slot":0,"token":"18e160f8a64402d8-6a3d-d1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e160f8a64402d8-6a3d-d1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":73,"abi_return_us":7365,"end_us":7371,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7410,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":7402},{"end_us":7424,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":7424}],"schema":2,"slots":[{"at_us":7424,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e160f8a64402d8-6a3d-d1","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1258,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":258,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a30f5face0-10a77-3","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2111,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2111,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1111,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a30f758a88-10a77-4","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2031,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1031,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a30fa1a398-10a77-5","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103962,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3962,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a30fcbbcf0-10a77-6","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101101,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101101,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1101,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a316026bc8-10a77-7","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101511,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1511,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a31c1a4fd0-10a77-8","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503227,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3227,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a322353d30-10a77-9","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501827,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501827,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1827,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a3403815c8-10a77-a","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501486,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1486,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a35e34a8e8-10a77-b","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1258,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":258,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a37c276480-10a77-c","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2082,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2082,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1082,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a37c3dda80-10a77-d","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2272,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1272,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a37c6991e8-10a77-e","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102072,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2072,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a37c948600-10a77-f","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":2,"abi_return_us":101324,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101324,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1324,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a382ae98a0-10a77-10","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101134,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1134,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a388ca6890-10a77-11","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503074,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3074,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a38edfb0a0-10a77-12","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501407,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501407,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1407,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a3ace177c8-10a77-13","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501129,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1129,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a3cadc16e8-10a77-14","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1256,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":256,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a3e8cb0da8-10a77-15","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2273,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2273,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1273,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a3e8e0c828-10a77-16","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2030,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1030,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a3e9146ed0-10a77-17","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101526,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1526,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a3e93c74e8-10a77-18","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101398,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101398,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1398,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a3ef4f1d18-10a77-19","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102068,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2068,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a3f5694728-10a77-1a","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502054,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2054,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a3fb8c5a78-10a77-1b","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501204,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501204,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1204,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a4197d5ca8-10a77-1c","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502138,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2138,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a4376fd1f0-10a77-1d","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1256,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":256,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a4556ec288-10a77-1e","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2285,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2285,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1285,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a455858e78-10a77-1f","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2051,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1051,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a455b74508-10a77-20","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101979,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1979,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a455e0ba50-10a77-21","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101274,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101274,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1274,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a45bfa05b8-10a77-22","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101625,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1625,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a4621698f8-10a77-23","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500082,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":82,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a46832c6a8-10a77-24","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502051,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502051,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2051,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a48605b1b8-10a77-25","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501613,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1613,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a4a402ecd0-10a77-26","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1257,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":257,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a4c1f60628-10a77-27","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2266,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2266,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1266,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a4c20bf758-10a77-28","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2273,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1273,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a4c239ecf8-10a77-29","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103927,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3927,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a4c2661990-10a77-2a","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102421,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102421,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2421,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a4c89d39b0-10a77-2b","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102021,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2021,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a4cedf94d0-10a77-2c","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504167,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4167,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a4d504cb00-10a77-2d","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501333,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501333,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1333,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a4f3172c28-10a77-2e","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502074,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2074,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a5110a2e10-10a77-2f","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1056,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":56,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a52f073660-10a77-30","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2273,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2273,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1273,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a52f1ae188-10a77-31","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2123,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1123,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a52f4b53e0-10a77-32","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":104557,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4557,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a52f7482d8-10a77-33","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101745,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101745,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1745,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a535b860b0-10a77-34","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102067,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2067,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a53bd96890-10a77-35","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502187,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2187,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a541fd2ba8-10a77-36","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501064,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501064,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1064,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a55ff0e140-10a77-37","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501774,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1774,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a57ddff740-10a77-38","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1256,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":256,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a59bd58580-10a77-39","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2275,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2275,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1275,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a59bec0b20-10a77-3a","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2270,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1270,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a59c1b3170-10a77-3b","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102603,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2603,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a59c4742b0-10a77-3c","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101659,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101659,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1659,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a5a269ec88-10a77-3d","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101689,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1689,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a5a88a0238-10a77-3e","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502903,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2903,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a5aea6d010-10a77-3f","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501636,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501636,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1636,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a5cca493e0-10a77-40","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501059,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1059,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a5eaa691b8-10a77-41","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1209,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":209,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a60895ea20-10a77-42","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2328,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2328,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1328,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a608ad7d48-10a77-43","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2289,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1289,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a608e14330-10a77-44","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102204,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2204,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a6090fae00-10a77-45","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101079,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101079,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1079,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a60f2bacd0-10a77-46","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101121,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1121,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a61542ecc8-10a77-47","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501188,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1188,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161a61b598c98-10a77-48","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502063,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502063,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2063,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161a6393de880-10a77-49","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502062,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2062,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161a6573ac9c0-10a77-4a","within_five_ms":true}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":57740,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5751,"slot":0,"token":"18e161a6753c8148-10a77-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a6753c8148-10a77-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5765,"bytes":57,"kind":"head","slot":0},{"at_us":5771,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":1555,"operation":"dispatch","outcome":"pending","slot":0,"start_us":732},{"abi_enter_us":1601,"abi_return_us":5595,"end_us":5598,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1601,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":6097,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5633},{"bytes":1,"capacity":1,"end_us":6120,"operation":"read","slot":0,"start_us":6117},{"capacity":1,"end_us":57660,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":6151},{"end_us":57723,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57716}],"schema":2,"slots":[{"at_us":57723,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a6753c8148-10a77-4b","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":60557,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6343,"slot":0,"token":"18e161a678dc8780-10a77-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a678dc8780-10a77-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6359,"bytes":57,"kind":"head","slot":0},{"at_us":6364,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":126,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":155,"abi_return_us":6090,"end_us":6093,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":154,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6130,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6125},{"abi_enter_us":6143,"abi_return_us":6148,"end_us":6149,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6143,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6186,"operation":"read","slot":0,"start_us":6184},{"abi_enter_us":6200,"abi_return_us":60336,"end_us":60340,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6200,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":60416,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":60411},{"end_us":60537,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60527}],"schema":2,"slots":[{"at_us":60537,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a678dc8780-10a77-4c","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":61201,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6476,"slot":0,"token":"18e161a67c9fe498-10a77-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a67c9fe498-10a77-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6487,"bytes":57,"kind":"head","slot":0},{"at_us":6493,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":32},{"abi_enter_us":88,"abi_return_us":6214,"end_us":6217,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6255,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6250},{"abi_enter_us":6267,"abi_return_us":6272,"end_us":6273,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6267,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6299,"operation":"read","slot":0,"start_us":6297},{"abi_enter_us":6313,"abi_return_us":61060,"end_us":61065,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6313,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":61160,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":61155},{"end_us":61186,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61180}],"schema":2,"slots":[{"at_us":61186,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a67c9fe498-10a77-4d","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":61035,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6163,"slot":0,"token":"18e161a680707c68-10a77-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a680707c68-10a77-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6193,"bytes":57,"kind":"head","slot":0},{"at_us":6219,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":143,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":180,"operation":"handoff","start_us":180},{"abi_enter_us":193,"abi_return_us":5755,"end_us":5762,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":193,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5824,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5817},{"abi_enter_us":5843,"abi_return_us":5848,"end_us":5848,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5843,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5875,"operation":"read","slot":0,"start_us":5872},{"abi_enter_us":5887,"abi_return_us":60876,"end_us":60884,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5887,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":60988,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":60981},{"end_us":61020,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61013}],"schema":2,"slots":[{"at_us":61020,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a680707c68-10a77-4e","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":56941,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6458,"slot":0,"token":"18e161a6843f7df8-10a77-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a6843f7df8-10a77-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6474,"bytes":57,"kind":"head","slot":0},{"at_us":6479,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":112,"abi_return_us":6155,"end_us":6161,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6209,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6204},{"abi_enter_us":6225,"abi_return_us":6231,"end_us":6231,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6225,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6268,"operation":"read","slot":0,"start_us":6265},{"abi_enter_us":6279,"abi_return_us":56820,"end_us":56825,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6279,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":56901,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56895},{"end_us":56926,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56919}],"schema":2,"slots":[{"at_us":56926,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a6843f7df8-10a77-4f","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":500788,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6047,"slot":0,"token":"18e161a687cc0400-10a77-50"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a687cc0400-10a77-50","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":70,"abi_return_us":500686,"end_us":500700,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500767,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500767}],"schema":2,"slots":[{"at_us":500767,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a687cc0400-10a77-50","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501126,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6446,"slot":0,"token":"18e161a6a5c44d78-10a77-51"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a6a5c44d78-10a77-51","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":86,"abi_return_us":501032,"end_us":501041,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501104,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501104}],"schema":2,"slots":[{"at_us":501104,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a6a5c44d78-10a77-51","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":500950,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5272,"slot":0,"token":"18e161a6c3bea260-10a77-52"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a6c3bea260-10a77-52","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"abi_enter_us":84,"abi_return_us":500862,"end_us":500868,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500929,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500929}],"schema":2,"slots":[{"at_us":500929,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a6c3bea260-10a77-52","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501055,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5804,"slot":0,"token":"18e161a6e1b59030-10a77-53"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a6e1b59030-10a77-53","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":73,"operation":"handoff","start_us":73},{"abi_enter_us":91,"abi_return_us":500954,"end_us":500964,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501035,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501035}],"schema":2,"slots":[{"at_us":501035,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a6e1b59030-10a77-53","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501554,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6428,"slot":0,"token":"18e161a6ffb2ee70-10a77-54"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161a6ffb2ee70-10a77-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":6441,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":66,"abi_return_us":6177,"end_us":6181,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6346,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6340},{"abi_enter_us":6363,"abi_return_us":501433,"end_us":501437,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6363,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501506,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501498}],"schema":2,"slots":[{"at_us":501506,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a6ffb2ee70-10a77-54","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501822,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5549,"slot":0,"token":"18e161a71dbe5e40-10a77-55"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161a71dbe5e40-10a77-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":5565,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":178,"abi_return_us":5274,"end_us":5281,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":178,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5349,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5343},{"abi_enter_us":5364,"abi_return_us":501370,"end_us":501375,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5364,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501780,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501773}],"schema":2,"slots":[{"at_us":501780,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a71dbe5e40-10a77-55","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501866,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5908,"slot":0,"token":"18e161a73bca1c30-10a77-56"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161a73bca1c30-10a77-56","watchdog":false,"workers_joined":1,"writes":[{"at_us":5922,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":103,"operation":"handoff","start_us":103},{"abi_enter_us":131,"abi_return_us":5649,"end_us":5652,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":131,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5687,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5680},{"abi_enter_us":5701,"abi_return_us":501758,"end_us":501762,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5700,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501834,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501827}],"schema":2,"slots":[{"at_us":501834,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a73bca1c30-10a77-56","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501259,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6460,"slot":0,"token":"18e161a759da9ce0-10a77-57"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a759da9ce0-10a77-57","watchdog":false,"workers_joined":1,"writes":[{"at_us":6477,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":122,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":149,"operation":"handoff","start_us":149},{"abi_enter_us":161,"abi_return_us":6103,"end_us":6110,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":161,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6169,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6163},{"abi_enter_us":6186,"abi_return_us":501153,"end_us":501160,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6186,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501232,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501224}],"schema":2,"slots":[{"at_us":501232,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a759da9ce0-10a77-57","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":500851,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6514,"slot":0,"token":"18e161a777e082b8-10a77-58"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a777e082b8-10a77-58","watchdog":false,"workers_joined":1,"writes":[{"at_us":6526,"bytes":57,"kind":"head","slot":0},{"at_us":6534,"bytes":1,"kind":"body","slot":0},{"at_us":310539,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":71,"operation":"dispatch","outcome":"pending","slot":0,"start_us":33},{"abi_enter_us":113,"abi_return_us":6267,"end_us":6271,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6311,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6303},{"abi_enter_us":6329,"abi_return_us":6334,"end_us":6334,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6329,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6359,"operation":"read","slot":0,"start_us":6357},{"abi_enter_us":6391,"abi_return_us":310276,"end_us":310279,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6390,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":310366,"operation":"read","slot":0,"start_us":310362},{"abi_enter_us":310380,"abi_return_us":500761,"end_us":500767,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310379,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":500829,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500822}],"schema":2,"slots":[{"at_us":500829,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a777e082b8-10a77-58","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501506,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6724,"slot":0,"token":"18e161a7960d0f18-10a77-59"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a7960d0f18-10a77-59","watchdog":false,"workers_joined":1,"writes":[{"at_us":6747,"bytes":57,"kind":"head","slot":0},{"at_us":6757,"bytes":1,"kind":"body","slot":0},{"at_us":310198,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":103,"abi_return_us":6445,"end_us":6452,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6517,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6511},{"abi_enter_us":6538,"abi_return_us":6548,"end_us":6548,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6538,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6580,"operation":"read","slot":0,"start_us":6576},{"abi_enter_us":6593,"abi_return_us":309844,"end_us":309854,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6592,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309948,"operation":"read","slot":0,"start_us":309944},{"abi_enter_us":309978,"abi_return_us":501373,"end_us":501377,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309978,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501462,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501440}],"schema":2,"slots":[{"at_us":501462,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a7960d0f18-10a77-59","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501016,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6423,"slot":0,"token":"18e161a7b41f87b0-10a77-5a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a7b41f87b0-10a77-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6435,"bytes":57,"kind":"head","slot":0},{"at_us":6440,"bytes":1,"kind":"body","slot":0},{"at_us":309460,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":82,"operation":"handoff","start_us":82},{"abi_enter_us":101,"abi_return_us":6113,"end_us":6116,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":100,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6156,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6151},{"abi_enter_us":6171,"abi_return_us":6176,"end_us":6176,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6171,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6210,"operation":"read","slot":0,"start_us":6208},{"abi_enter_us":6222,"abi_return_us":309124,"end_us":309128,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6222,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":309204,"operation":"read","slot":0,"start_us":309201},{"abi_enter_us":309230,"abi_return_us":500917,"end_us":500921,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309230,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500988,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500981}],"schema":2,"slots":[{"at_us":500988,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a7b41f87b0-10a77-5a","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":500762,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5389,"slot":0,"token":"18e161a7d224a650-10a77-5b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a7d224a650-10a77-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5402,"bytes":57,"kind":"head","slot":0},{"at_us":5408,"bytes":1,"kind":"body","slot":0},{"at_us":309249,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":87,"abi_return_us":5217,"end_us":5222,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5263,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5255},{"abi_enter_us":5319,"abi_return_us":5324,"end_us":5325,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5319,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5357,"operation":"read","slot":0,"start_us":5354},{"abi_enter_us":5368,"abi_return_us":308988,"end_us":308995,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5368,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":309074,"operation":"read","slot":0,"start_us":309070},{"abi_enter_us":309090,"abi_return_us":500672,"end_us":500680,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309090,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500738,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500730}],"schema":2,"slots":[{"at_us":500738,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a7d224a650-10a77-5b","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":500825,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5261,"slot":0,"token":"18e161a7f028ea30-10a77-5c"},{"path":"/readiness/siblings/1","received_us":5268,"slot":1,"token":"18e161a7f028ea30-10a77-5c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161a7f028ea30-10a77-5c","watchdog":false,"workers_joined":2,"writes":[{"at_us":5277,"bytes":57,"kind":"head","slot":1},{"at_us":5283,"bytes":57,"kind":"head","slot":0},{"at_us":11553,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":139,"operation":"dispatch","outcome":"pending","slot":1,"start_us":88},{"abi_enter_us":159,"abi_return_us":5021,"end_us":5023,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":159,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5055,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5050},{"abi_enter_us":5067,"abi_return_us":5069,"end_us":5069,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5067,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5083,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5082},{"abi_enter_us":5092,"abi_return_us":11290,"end_us":11291,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5092,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":11336,"operation":"read","slot":1,"start_us":11333},{"abi_enter_us":11354,"abi_return_us":11356,"end_us":11357,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11354,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11385,"operation":"read","slot":1,"start_us":11384},{"abi_enter_us":11397,"abi_return_us":11398,"end_us":11399,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11397,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11417,"operation":"read","slot":1,"start_us":11416},{"abi_enter_us":11430,"abi_return_us":11433,"end_us":11433,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11430,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11457,"operation":"read","slot":1,"start_us":11457},{"end_us":11468,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11464},{"abi_enter_us":11481,"abi_return_us":500731,"end_us":500737,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11481,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500804,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500796}],"schema":2,"slots":[{"at_us":500804,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11468,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a7f028ea30-10a77-5c","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":500674,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5219,"slot":0,"token":"18e161a80e3e48f8-10a77-5d"},{"path":"/readiness/siblings/1","received_us":5258,"slot":1,"token":"18e161a80e3e48f8-10a77-5d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161a80e3e48f8-10a77-5d","watchdog":false,"workers_joined":2,"writes":[{"at_us":5233,"bytes":57,"kind":"head","slot":0},{"at_us":5264,"bytes":57,"kind":"head","slot":1},{"at_us":5269,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":128,"operation":"dispatch","outcome":"pending","slot":1,"start_us":91},{"abi_enter_us":160,"abi_return_us":4951,"end_us":4955,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":160,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4990,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4985},{"abi_enter_us":5006,"abi_return_us":5008,"end_us":5008,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5006,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5023,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5022},{"abi_enter_us":5033,"abi_return_us":5037,"end_us":5037,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5033,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5064,"operation":"read","slot":1,"start_us":5062},{"abi_enter_us":5076,"abi_return_us":5078,"end_us":5079,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5076,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5100,"operation":"read","slot":1,"start_us":5100},{"end_us":5112,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5109},{"abi_enter_us":5126,"abi_return_us":500578,"end_us":500582,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5126,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500656,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500648}],"schema":2,"slots":[{"at_us":500656,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5112,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a80e3e48f8-10a77-5d","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":500835,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6216,"slot":1,"token":"18e161a82c4deee8-10a77-5e"},{"path":"/readiness/siblings/0","received_us":6227,"slot":0,"token":"18e161a82c4deee8-10a77-5e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161a82c4deee8-10a77-5e","watchdog":false,"workers_joined":2,"writes":[{"at_us":6232,"bytes":57,"kind":"head","slot":1},{"at_us":6239,"bytes":57,"kind":"head","slot":0},{"at_us":11709,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":127,"operation":"dispatch","outcome":"pending","slot":1,"start_us":108},{"body_reads":0,"deadline_us":500000,"end_us":141,"operation":"handoff","start_us":141},{"abi_enter_us":152,"abi_return_us":5980,"end_us":5984,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":152,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6044,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6039},{"abi_enter_us":6060,"abi_return_us":6063,"end_us":6063,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6060,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6081,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6080},{"abi_enter_us":6094,"abi_return_us":11473,"end_us":11474,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6094,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":11526,"operation":"read","slot":1,"start_us":11515},{"abi_enter_us":11540,"abi_return_us":11542,"end_us":11543,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11540,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11565,"operation":"read","slot":1,"start_us":11561},{"abi_enter_us":11581,"abi_return_us":11582,"end_us":11582,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11581,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11611,"operation":"read","slot":1,"start_us":11611},{"abi_enter_us":11620,"abi_return_us":11625,"end_us":11625,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11620,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11657,"operation":"read","slot":1,"start_us":11656},{"end_us":11669,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11665},{"abi_enter_us":11682,"abi_return_us":500742,"end_us":500746,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11682,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500815,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500808}],"schema":2,"slots":[{"at_us":500815,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11669,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a82c4deee8-10a77-5e","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":500996,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5476,"slot":0,"token":"18e161a84a639400-10a77-5f"},{"path":"/readiness/siblings/1","received_us":5480,"slot":1,"token":"18e161a84a639400-10a77-5f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161a84a639400-10a77-5f","watchdog":false,"workers_joined":2,"writes":[{"at_us":5485,"bytes":57,"kind":"head","slot":0},{"at_us":5493,"bytes":57,"kind":"head","slot":1},{"at_us":5499,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":59,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":161,"operation":"dispatch","outcome":"pending","slot":1,"start_us":120},{"body_reads":0,"deadline_us":500000,"end_us":175,"operation":"handoff","start_us":175},{"abi_enter_us":186,"abi_return_us":5227,"end_us":5230,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":186,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5266,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5261},{"abi_enter_us":5283,"abi_return_us":5285,"end_us":5285,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5283,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5301,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5300},{"abi_enter_us":5312,"abi_return_us":5314,"end_us":5314,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5311,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5340,"operation":"read","slot":1,"start_us":5337},{"abi_enter_us":5352,"abi_return_us":5355,"end_us":5355,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5352,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5377,"operation":"read","slot":1,"start_us":5377},{"end_us":5388,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5386},{"abi_enter_us":5400,"abi_return_us":500756,"end_us":500765,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5400,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500931,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500893}],"schema":2,"slots":[{"at_us":500931,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5388,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a84a639400-10a77-5f","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501582,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6451,"slot":0,"token":"18e161a8687c9860-10a77-60"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161a8687c9860-10a77-60","watchdog":false,"workers_joined":1,"writes":[{"at_us":6467,"bytes":57,"kind":"head","slot":0},{"at_us":6477,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":32},{"abi_enter_us":115,"abi_return_us":6185,"end_us":6190,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":115,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6241,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6233},{"abi_enter_us":6256,"abi_return_us":6261,"end_us":6261,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6255,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6288,"operation":"read","slot":0,"start_us":6285},{"abi_enter_us":6300,"abi_return_us":501333,"end_us":501337,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6300,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501531,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501518}],"schema":2,"slots":[{"at_us":501531,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8687c9860-10a77-60","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":500741,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6451,"slot":0,"token":"18e161a886891d88-10a77-61"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161a886891d88-10a77-61","watchdog":false,"workers_joined":1,"writes":[{"at_us":6471,"bytes":57,"kind":"head","slot":0},{"at_us":6477,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"abi_enter_us":129,"abi_return_us":6064,"end_us":6071,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":128,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6129,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6123},{"abi_enter_us":6145,"abi_return_us":6149,"end_us":6150,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6145,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6183,"operation":"read","slot":0,"start_us":6181},{"abi_enter_us":6196,"abi_return_us":500613,"end_us":500624,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6196,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500699,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500689}],"schema":2,"slots":[{"at_us":500699,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a886891d88-10a77-61","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":500948,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6441,"slot":0,"token":"18e161a8a48aaa18-10a77-62"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161a8a48aaa18-10a77-62","watchdog":false,"workers_joined":1,"writes":[{"at_us":6453,"bytes":57,"kind":"head","slot":0},{"at_us":6458,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":62,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":99,"operation":"handoff","start_us":99},{"abi_enter_us":116,"abi_return_us":6154,"end_us":6159,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6200,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6193},{"abi_enter_us":6214,"abi_return_us":6219,"end_us":6220,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6214,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6251,"operation":"read","slot":0,"start_us":6249},{"abi_enter_us":6264,"abi_return_us":500676,"end_us":500680,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6264,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500844,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500836}],"schema":2,"slots":[{"at_us":500844,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8a48aaa18-10a77-62","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":500631,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6454,"slot":0,"token":"18e161a8c2e1bd30-10a77-63"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161a8c2e1bd30-10a77-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":6470,"bytes":57,"kind":"head","slot":0},{"at_us":6477,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":80,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":119,"operation":"handoff","start_us":119},{"abi_enter_us":133,"abi_return_us":6085,"end_us":6091,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":133,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6139,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6133},{"abi_enter_us":6154,"abi_return_us":6160,"end_us":6160,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6154,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6190,"operation":"read","slot":0,"start_us":6187},{"abi_enter_us":6202,"abi_return_us":500449,"end_us":500466,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6202,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500597,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500578}],"schema":2,"slots":[{"at_us":500597,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8c2e1bd30-10a77-63","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":4582,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5264,"slot":0,"token":"18e161a8e0eecb10-10a77-64"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e0eecb10-10a77-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":5286,"bytes":57,"kind":"head","slot":0},{"at_us":5292,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":61,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"abi_enter_us":138,"abi_return_us":4326,"end_us":4333,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":138,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4393,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4386},{"abi_enter_us":4412,"abi_return_us":4417,"end_us":4417,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4411,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4446,"operation":"read","slot":0,"start_us":4443},{"abi_enter_us":4459,"abi_return_us":4460,"end_us":4460,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4458,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4487,"operation":"read","slot":0,"start_us":4485},{"abi_enter_us":4497,"abi_return_us":4498,"end_us":4498,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4497,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4519,"operation":"read","slot":0,"start_us":4518},{"abi_enter_us":4529,"abi_return_us":4531,"end_us":4531,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4529,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":1,"end_us":4551,"operation":"read","slot":0,"start_us":4551},{"end_us":4568,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":4565}],"schema":2,"slots":[{"at_us":4568,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8e0eecb10-10a77-64","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":5470,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5491,"slot":0,"token":"18e161a8e1730e70-10a77-65"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e1730e70-10a77-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":5503,"bytes":57,"kind":"head","slot":0},{"at_us":5509,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":85,"abi_return_us":5217,"end_us":5226,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5284,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5278},{"abi_enter_us":5299,"abi_return_us":5307,"end_us":5308,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5298,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5339,"operation":"read","slot":0,"start_us":5336},{"abi_enter_us":5351,"abi_return_us":5354,"end_us":5354,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5351,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5443,"operation":"read","slot":0,"start_us":5442},{"end_us":5457,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5453}],"schema":2,"slots":[{"at_us":5457,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8e1730e70-10a77-65","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":6025,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6174,"slot":0,"token":"18e161a8e1f4c960-10a77-66"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e1f4c960-10a77-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":6191,"bytes":57,"kind":"head","slot":0},{"at_us":6197,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":62,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"body_reads":0,"deadline_us":500000,"end_us":101,"operation":"handoff","start_us":101},{"abi_enter_us":116,"abi_return_us":5770,"end_us":5776,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5826,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5820},{"abi_enter_us":5841,"abi_return_us":5846,"end_us":5846,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5841,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5874,"operation":"read","slot":0,"start_us":5871},{"abi_enter_us":5892,"abi_return_us":5893,"end_us":5894,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5892,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5930,"operation":"read","slot":0,"start_us":5929},{"abi_enter_us":5940,"abi_return_us":5940,"end_us":5941,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5940,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5961,"operation":"read","slot":0,"start_us":5961},{"abi_enter_us":5971,"abi_return_us":5976,"end_us":5976,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5970,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5993,"operation":"read","slot":0,"start_us":5993},{"end_us":6006,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6002}],"schema":2,"slots":[{"at_us":6006,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8e1f4c960-10a77-66","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":6313,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6338,"slot":0,"token":"18e161a8e287c648-10a77-67"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e287c648-10a77-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":6351,"bytes":57,"kind":"head","slot":0},{"at_us":6354,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"abi_enter_us":81,"abi_return_us":6153,"end_us":6158,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6197,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6192},{"abi_enter_us":6209,"abi_return_us":6215,"end_us":6215,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6209,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6248,"operation":"read","slot":0,"start_us":6246},{"abi_enter_us":6259,"abi_return_us":6261,"end_us":6262,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6259,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6283,"operation":"read","slot":0,"start_us":6282},{"end_us":6298,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6295}],"schema":2,"slots":[{"at_us":6298,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8e287c648-10a77-67","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5189,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5287,"slot":0,"token":"18e161a8e31475e8-10a77-68"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e31475e8-10a77-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":5303,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":64,"abi_return_us":5089,"end_us":5092,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5124,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5120},{"abi_enter_us":5135,"abi_return_us":5138,"end_us":5138,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5135,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5160,"operation":"read","slot":0,"start_us":5159},{"end_us":5177,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5175}],"schema":2,"slots":[{"at_us":5177,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8e31475e8-10a77-68","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6310,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6442,"slot":0,"token":"18e161a8e38e68a8-10a77-69"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e38e68a8-10a77-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":6456,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":77,"abi_return_us":6189,"end_us":6193,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6236,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6231},{"abi_enter_us":6252,"abi_return_us":6254,"end_us":6255,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6252,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6283,"operation":"read","slot":0,"start_us":6280},{"end_us":6297,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6294}],"schema":2,"slots":[{"at_us":6297,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8e38e68a8-10a77-69","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6318,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6402,"slot":0,"token":"18e161a8e40ec408-10a77-6a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e40ec408-10a77-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6412,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"abi_enter_us":71,"abi_return_us":6222,"end_us":6226,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6257,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6252},{"abi_enter_us":6271,"abi_return_us":6273,"end_us":6273,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6271,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6295,"operation":"read","slot":0,"start_us":6294},{"end_us":6307,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6305}],"schema":2,"slots":[{"at_us":6307,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8e40ec408-10a77-6a","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6386,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6415,"slot":0,"token":"18e161a8e48d8158-10a77-6b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e48d8158-10a77-6b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6430,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"abi_enter_us":72,"abi_return_us":6274,"end_us":6278,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6310,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6306},{"abi_enter_us":6324,"abi_return_us":6329,"end_us":6329,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6324,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6355,"operation":"read","slot":0,"start_us":6354},{"end_us":6368,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6365}],"schema":2,"slots":[{"at_us":6368,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8e48d8158-10a77-6b","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5931,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6017,"slot":0,"token":"18e161a8e50c7d28-10a77-6c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e50c7d28-10a77-6c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6027,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":59,"abi_return_us":5881,"end_us":5883,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5906,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5903},{"end_us":5918,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5918}],"schema":2,"slots":[{"at_us":5918,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a8e50c7d28-10a77-6c","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5085,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5247,"slot":0,"token":"18e161a8e587e300-10a77-6d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e587e300-10a77-6d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5256,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":58,"abi_return_us":5037,"end_us":5039,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5060,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5056},{"end_us":5070,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5070}],"schema":2,"slots":[{"at_us":5070,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a8e587e300-10a77-6d","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5076,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5207,"slot":0,"token":"18e161a8e5f16aa0-10a77-6e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e5f16aa0-10a77-6e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5217,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":47,"operation":"handoff","start_us":47},{"abi_enter_us":58,"abi_return_us":5033,"end_us":5034,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5053,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5051},{"end_us":5063,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5063}],"schema":2,"slots":[{"at_us":5063,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a8e5f16aa0-10a77-6e","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5335,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5415,"slot":0,"token":"18e161a8e65acb30-10a77-6f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e65acb30-10a77-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5422,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":45,"operation":"handoff","start_us":45},{"abi_enter_us":55,"abi_return_us":5291,"end_us":5292,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5311,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5309},{"end_us":5323,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5322}],"schema":2,"slots":[{"at_us":5323,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a8e65acb30-10a77-6f","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58142,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6098,"slot":0,"token":"18e161a8e6c99290-10a77-70"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8e6c99290-10a77-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":6113,"bytes":66,"kind":"head","slot":0},{"at_us":6119,"bytes":6,"kind":"body","slot":0},{"at_us":58230,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":82,"abi_return_us":5887,"end_us":5892,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5935,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5930},{"abi_enter_us":5947,"abi_return_us":5953,"end_us":5953,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5947,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5977,"operation":"read","slot":0,"start_us":5974},{"abi_enter_us":6015,"abi_return_us":58026,"end_us":58030,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6014,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":58101,"error":"Other","operation":"read","slot":0,"start_us":58089},{"end_us":58128,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58121}],"schema":2,"slots":[{"at_us":58128,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8e6c99290-10a77-70","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":55296,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5210,"slot":0,"token":"18e161a8ea67fab8-10a77-71"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8ea67fab8-10a77-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":5226,"bytes":66,"kind":"head","slot":0},{"at_us":5231,"bytes":6,"kind":"body","slot":0},{"at_us":55449,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":74,"abi_return_us":4980,"end_us":4987,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5027,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5021},{"abi_enter_us":5039,"abi_return_us":5043,"end_us":5044,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5039,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5075,"operation":"read","slot":0,"start_us":5073},{"abi_enter_us":5086,"abi_return_us":55176,"end_us":55180,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5086,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":55255,"error":"Other","operation":"read","slot":0,"start_us":55247},{"end_us":55282,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55276}],"schema":2,"slots":[{"at_us":55282,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8ea67fab8-10a77-71","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":54232,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6262,"slot":0,"token":"18e161a8eddb4ba0-10a77-72"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8eddb4ba0-10a77-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":6279,"bytes":66,"kind":"head","slot":0},{"at_us":6285,"bytes":6,"kind":"body","slot":0},{"at_us":56889,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":82,"abi_return_us":3540,"end_us":3546,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":3592,"operation":"head-poll","outcome":"ready","slot":0,"start_us":3586},{"abi_enter_us":3608,"abi_return_us":3614,"end_us":3615,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":3608,"status":"FastlyStatus::OK","timeout_ms":496},{"bytes":1,"capacity":1,"end_us":3643,"operation":"read","slot":0,"start_us":3641},{"abi_enter_us":3658,"abi_return_us":54112,"end_us":54116,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":3658,"status":"FastlyStatus::OK","timeout_ms":496},{"capacity":1,"end_us":54191,"error":"Other","operation":"read","slot":0,"start_us":54185},{"end_us":54217,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":54210}],"schema":2,"slots":[{"at_us":54217,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8eddb4ba0-10a77-72","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":69610,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5867,"slot":0,"token":"18e161a8f1675c78-10a77-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8f1675c78-10a77-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":5887,"bytes":66,"kind":"head","slot":0},{"at_us":5895,"bytes":6,"kind":"body","slot":0},{"at_us":56215,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":93,"operation":"handoff","start_us":93},{"abi_enter_us":107,"abi_return_us":5632,"end_us":5637,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5689,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5682},{"abi_enter_us":5724,"abi_return_us":5731,"end_us":5731,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5724,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5766,"operation":"read","slot":0,"start_us":5763},{"abi_enter_us":5778,"abi_return_us":55991,"end_us":55996,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5778,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":61342,"error":"Other","operation":"read","slot":0,"start_us":61302},{"end_us":66582,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":65844}],"schema":2,"slots":[{"at_us":66582,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a8f1675c78-10a77-73","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":9012,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":23527,"slot":0,"token":"18e161a8f846bae8-10a77-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8f846bae8-10a77-74","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":143,"operation":"dispatch","outcome":"pending","slot":0,"start_us":72},{"abi_enter_us":226,"abi_return_us":8934,"end_us":8942,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":226,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":8982,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":8977},{"end_us":8996,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":8996}],"schema":2,"slots":[{"at_us":8996,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a8f846bae8-10a77-74","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":13660,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":21123,"slot":0,"token":"18e161a8fbca4428-10a77-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8fbca4428-10a77-75","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":187,"operation":"dispatch","outcome":"pending","slot":0,"start_us":30},{"abi_enter_us":620,"abi_return_us":13534,"end_us":13541,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":620,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":13605,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":13596},{"end_us":13643,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":13643}],"schema":2,"slots":[{"at_us":13643,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a8fbca4428-10a77-75","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":24510,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":25585,"slot":0,"token":"18e161a8ff4b8f30-10a77-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a8ff4b8f30-10a77-76","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":71,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"body_reads":0,"deadline_us":500000,"end_us":122,"operation":"handoff","start_us":122},{"abi_enter_us":141,"abi_return_us":23522,"end_us":23530,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":141,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":24469,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":24462},{"end_us":24492,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":24492}],"schema":2,"slots":[{"at_us":24492,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a8ff4b8f30-10a77-76","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":4771,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6210,"slot":0,"token":"18e161a902641db8-10a77-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a902641db8-10a77-77","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":193,"operation":"handoff","start_us":193},{"abi_enter_us":208,"abi_return_us":4508,"end_us":4514,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":208,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4604,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":4598},{"end_us":4620,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":4620}],"schema":2,"slots":[{"at_us":4620,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a902641db8-10a77-77","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":70143,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":21915,"slot":0,"token":"18e161a903b83050-10a77-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a903b83050-10a77-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":22416,"bytes":57,"kind":"head","slot":0},{"at_us":22472,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":655,"operation":"dispatch","outcome":"pending","slot":0,"start_us":171},{"abi_enter_us":775,"abi_return_us":20207,"end_us":20217,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":775,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":20369,"operation":"head-poll","outcome":"ready","slot":0,"start_us":20361},{"bytes":1,"capacity":1,"end_us":20538,"operation":"read","slot":0,"start_us":20529},{"capacity":1,"end_us":70062,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":20647},{"end_us":70119,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":70111}],"schema":2,"slots":[{"at_us":70119,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a903b83050-10a77-78","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":60261,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6916,"slot":0,"token":"18e161a908d397a0-10a77-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a908d397a0-10a77-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":6937,"bytes":57,"kind":"head","slot":0},{"at_us":6945,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":112,"abi_return_us":6402,"end_us":6449,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6879,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6819},{"abi_enter_us":6910,"abi_return_us":6935,"end_us":6938,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6909,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":7291,"operation":"read","slot":0,"start_us":7286},{"abi_enter_us":7321,"abi_return_us":60155,"end_us":60159,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7321,"status":"FastlyStatus::OK","timeout_ms":492},{"capacity":1,"end_us":60221,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":60217},{"end_us":60248,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60242}],"schema":2,"slots":[{"at_us":60248,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a908d397a0-10a77-79","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":56815,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6044,"slot":0,"token":"18e161a90c95eb18-10a77-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a90c95eb18-10a77-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6064,"bytes":57,"kind":"head","slot":0},{"at_us":6070,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"abi_enter_us":106,"abi_return_us":5796,"end_us":5802,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5857,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5849},{"abi_enter_us":5870,"abi_return_us":5875,"end_us":5878,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5870,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5905,"operation":"read","slot":0,"start_us":5903},{"abi_enter_us":5917,"abi_return_us":56690,"end_us":56694,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5917,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56767,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56761},{"end_us":56798,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56792}],"schema":2,"slots":[{"at_us":56798,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a90c95eb18-10a77-7a","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":57472,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6418,"slot":0,"token":"18e161a9102a6830-10a77-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a9102a6830-10a77-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6430,"bytes":57,"kind":"head","slot":0},{"at_us":6436,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":78,"abi_return_us":6202,"end_us":6205,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6235,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6230},{"abi_enter_us":6246,"abi_return_us":6256,"end_us":6257,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6246,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6296,"operation":"read","slot":0,"start_us":6294},{"abi_enter_us":6309,"abi_return_us":57363,"end_us":57367,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6308,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":57430,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57426},{"end_us":57458,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57452}],"schema":2,"slots":[{"at_us":57458,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a9102a6830-10a77-7b","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":57017,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5324,"slot":0,"token":"18e161a913c6ccb8-10a77-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161a913c6ccb8-10a77-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5339,"bytes":57,"kind":"head","slot":0},{"at_us":5349,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":86,"abi_return_us":5069,"end_us":5077,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5130,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5123},{"abi_enter_us":5145,"abi_return_us":5149,"end_us":5150,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5145,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5179,"operation":"read","slot":0,"start_us":5176},{"abi_enter_us":5195,"abi_return_us":56895,"end_us":56902,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5195,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56973,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56965},{"end_us":57002,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56995}],"schema":2,"slots":[{"at_us":57002,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a913c6ccb8-10a77-7c","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":500195,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6474,"slot":0,"token":"18e161a9175ded98-10a77-7d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a9175ded98-10a77-7d","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":70,"abi_return_us":500113,"end_us":500120,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500178,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500178}],"schema":2,"slots":[{"at_us":500178,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a9175ded98-10a77-7d","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501516,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6219,"slot":0,"token":"18e161a9354aa238-10a77-7e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a9354aa238-10a77-7e","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":72,"abi_return_us":501403,"end_us":501438,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501495,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501495}],"schema":2,"slots":[{"at_us":501495,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a9354aa238-10a77-7e","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":500970,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5324,"slot":0,"token":"18e161a9534bc168-10a77-7f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a9534bc168-10a77-7f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":93,"abi_return_us":500791,"end_us":500819,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500941,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500941}],"schema":2,"slots":[{"at_us":500941,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a9534bc168-10a77-7f","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501535,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5263,"slot":0,"token":"18e161a9714e9a00-10a77-80"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a9714e9a00-10a77-80","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":98,"operation":"handoff","start_us":98},{"abi_enter_us":112,"abi_return_us":501429,"end_us":501440,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501514,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501514}],"schema":2,"slots":[{"at_us":501514,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161a9714e9a00-10a77-80","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501115,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6443,"slot":0,"token":"18e161a98f595238-10a77-81"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161a98f595238-10a77-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":6470,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":76,"abi_return_us":6248,"end_us":6254,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6304,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6298},{"abi_enter_us":6320,"abi_return_us":501022,"end_us":501026,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6320,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501093,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501085}],"schema":2,"slots":[{"at_us":501093,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a98f595238-10a77-81","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501410,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6416,"slot":0,"token":"18e161a9ad5f43c8-10a77-82"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161a9ad5f43c8-10a77-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":6426,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":77,"abi_return_us":6146,"end_us":6149,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6188,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6183},{"abi_enter_us":6202,"abi_return_us":501224,"end_us":501229,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6202,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501383,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501375}],"schema":2,"slots":[{"at_us":501383,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a9ad5f43c8-10a77-82","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501095,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5272,"slot":0,"token":"18e161a9cb67c980-10a77-83"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161a9cb67c980-10a77-83","watchdog":false,"workers_joined":1,"writes":[{"at_us":5292,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":87,"operation":"handoff","start_us":87},{"abi_enter_us":100,"abi_return_us":5003,"end_us":5008,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":100,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5062,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5056},{"abi_enter_us":5076,"abi_return_us":500948,"end_us":500955,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5076,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501031,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501021}],"schema":2,"slots":[{"at_us":501031,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a9cb67c980-10a77-83","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501998,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5228,"slot":0,"token":"18e161a9e974cba8-10a77-84"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161a9e974cba8-10a77-84","watchdog":false,"workers_joined":1,"writes":[{"at_us":5250,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":95,"operation":"handoff","start_us":95},{"abi_enter_us":130,"abi_return_us":4899,"end_us":4902,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":130,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4936,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4931},{"abi_enter_us":4954,"abi_return_us":501891,"end_us":501895,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4954,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501975,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501965}],"schema":2,"slots":[{"at_us":501975,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161a9e974cba8-10a77-84","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":500254,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6491,"slot":0,"token":"18e161aa07864a40-10a77-85"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161aa07864a40-10a77-85","watchdog":false,"workers_joined":1,"writes":[{"at_us":6510,"bytes":57,"kind":"head","slot":0},{"at_us":6517,"bytes":1,"kind":"body","slot":0},{"at_us":311293,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":80,"abi_return_us":6231,"end_us":6237,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6293,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6285},{"abi_enter_us":6308,"abi_return_us":6321,"end_us":6322,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6308,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6368,"operation":"read","slot":0,"start_us":6364},{"abi_enter_us":6387,"abi_return_us":310979,"end_us":310983,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6387,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":311076,"operation":"read","slot":0,"start_us":311073},{"abi_enter_us":311091,"abi_return_us":500150,"end_us":500154,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":311091,"status":"FastlyStatus::OK","timeout_ms":188},{"end_us":500229,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500222}],"schema":2,"slots":[{"at_us":500229,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161aa07864a40-10a77-85","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":500813,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6462,"slot":0,"token":"18e161aa25850428-10a77-86"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161aa25850428-10a77-86","watchdog":false,"workers_joined":1,"writes":[{"at_us":6476,"bytes":57,"kind":"head","slot":0},{"at_us":6484,"bytes":1,"kind":"body","slot":0},{"at_us":307517,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":74,"abi_return_us":6294,"end_us":6308,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6401,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6395},{"abi_enter_us":6424,"abi_return_us":6429,"end_us":6430,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6424,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6478,"operation":"read","slot":0,"start_us":6475},{"abi_enter_us":6490,"abi_return_us":307283,"end_us":307287,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6490,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":307391,"operation":"read","slot":0,"start_us":307387},{"abi_enter_us":307409,"abi_return_us":500697,"end_us":500704,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307408,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500777,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500769}],"schema":2,"slots":[{"at_us":500777,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161aa25850428-10a77-86","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":500910,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6436,"slot":0,"token":"18e161aa4388f218-10a77-87"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161aa4388f218-10a77-87","watchdog":false,"workers_joined":1,"writes":[{"at_us":6452,"bytes":57,"kind":"head","slot":0},{"at_us":6458,"bytes":1,"kind":"body","slot":0},{"at_us":308418,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":87,"abi_return_us":6195,"end_us":6200,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6259,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6253},{"abi_enter_us":6276,"abi_return_us":6282,"end_us":6282,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6276,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6314,"operation":"read","slot":0,"start_us":6312},{"abi_enter_us":6327,"abi_return_us":308157,"end_us":308163,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6327,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":308269,"operation":"read","slot":0,"start_us":308265},{"abi_enter_us":308287,"abi_return_us":500796,"end_us":500800,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308286,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500873,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500866}],"schema":2,"slots":[{"at_us":500873,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161aa4388f218-10a77-87","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501876,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6415,"slot":0,"token":"18e161aa6193f870-10a77-88"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161aa6193f870-10a77-88","watchdog":false,"workers_joined":1,"writes":[{"at_us":6431,"bytes":57,"kind":"head","slot":0},{"at_us":6437,"bytes":1,"kind":"body","slot":0},{"at_us":309543,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"abi_enter_us":86,"abi_return_us":6124,"end_us":6128,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6173,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6168},{"abi_enter_us":6192,"abi_return_us":6196,"end_us":6196,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6192,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6233,"operation":"read","slot":0,"start_us":6230},{"abi_enter_us":6246,"abi_return_us":309289,"end_us":309322,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6246,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309624,"operation":"read","slot":0,"start_us":309617},{"abi_enter_us":309645,"abi_return_us":501739,"end_us":501756,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309645,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501845,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501823}],"schema":2,"slots":[{"at_us":501845,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161aa6193f870-10a77-88","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501237,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5910,"slot":0,"token":"18e161aa7fae48d8-10a77-89"},{"path":"/readiness/siblings/1","received_us":5916,"slot":1,"token":"18e161aa7fae48d8-10a77-89"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161aa7fae48d8-10a77-89","watchdog":false,"workers_joined":2,"writes":[{"at_us":5922,"bytes":57,"kind":"head","slot":0},{"at_us":5928,"bytes":57,"kind":"head","slot":1},{"at_us":5933,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":147,"operation":"dispatch","outcome":"pending","slot":1,"start_us":90},{"abi_enter_us":172,"abi_return_us":5571,"end_us":5574,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":172,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5606,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5601},{"abi_enter_us":5621,"abi_return_us":5623,"end_us":5623,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5621,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5638,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5637},{"abi_enter_us":5648,"abi_return_us":5650,"end_us":5651,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5648,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5674,"operation":"read","slot":1,"start_us":5672},{"abi_enter_us":5688,"abi_return_us":5689,"end_us":5689,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5687,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5709,"operation":"read","slot":1,"start_us":5709},{"abi_enter_us":5719,"abi_return_us":5720,"end_us":5720,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5719,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5739,"operation":"read","slot":1,"start_us":5739},{"abi_enter_us":5751,"abi_return_us":5753,"end_us":5753,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5751,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5778,"operation":"read","slot":1,"start_us":5777},{"end_us":5789,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5786},{"abi_enter_us":5800,"abi_return_us":501129,"end_us":501136,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5800,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501214,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501205}],"schema":2,"slots":[{"at_us":501214,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5789,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161aa7fae48d8-10a77-89","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501834,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5216,"slot":0,"token":"18e161aa9dcc6200-10a77-8a"},{"path":"/readiness/siblings/1","received_us":5224,"slot":1,"token":"18e161aa9dcc6200-10a77-8a"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161aa9dcc6200-10a77-8a","watchdog":false,"workers_joined":2,"writes":[{"at_us":5227,"bytes":57,"kind":"head","slot":0},{"at_us":5235,"bytes":57,"kind":"head","slot":1},{"at_us":5239,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":120,"operation":"dispatch","outcome":"pending","slot":1,"start_us":101},{"abi_enter_us":139,"abi_return_us":4927,"end_us":4928,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":139,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4957,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4950},{"abi_enter_us":4970,"abi_return_us":4972,"end_us":4972,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":4970,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":4988,"operation":"head-poll","outcome":"ready","slot":1,"start_us":4987},{"abi_enter_us":4999,"abi_return_us":5003,"end_us":5003,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":4999,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":3,"capacity":16384,"end_us":5031,"operation":"read","slot":1,"start_us":5029},{"abi_enter_us":5043,"abi_return_us":5045,"end_us":5045,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5043,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5068,"operation":"read","slot":1,"start_us":5067},{"end_us":5079,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5077},{"abi_enter_us":5094,"abi_return_us":501620,"end_us":501628,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5094,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501706,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501697}],"schema":2,"slots":[{"at_us":501706,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5079,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161aa9dcc6200-10a77-8a","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501890,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6067,"slot":1,"token":"18e161aabe531e88-10a77-8b"},{"path":"/readiness/siblings/0","received_us":9836,"slot":0,"token":"18e161aabe531e88-10a77-8b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161aabe531e88-10a77-8b","watchdog":false,"workers_joined":2,"writes":[{"at_us":6083,"bytes":57,"kind":"head","slot":1},{"at_us":9902,"bytes":57,"kind":"head","slot":0},{"at_us":11433,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":186,"operation":"dispatch","outcome":"pending","slot":0,"start_us":78},{"end_us":297,"operation":"dispatch","outcome":"pending","slot":1,"start_us":261},{"body_reads":0,"deadline_us":500000,"end_us":344,"operation":"handoff","start_us":344},{"abi_enter_us":364,"abi_return_us":6552,"end_us":6560,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":363,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6672,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6659},{"abi_enter_us":6692,"abi_return_us":9603,"end_us":9608,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6692,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":9782,"operation":"head-poll","outcome":"ready","slot":0,"start_us":9744},{"abi_enter_us":9822,"abi_return_us":11079,"end_us":11082,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":9822,"status":"FastlyStatus::OK","timeout_ms":490},{"bytes":1,"capacity":1,"end_us":11163,"operation":"read","slot":1,"start_us":11159},{"abi_enter_us":11177,"abi_return_us":11184,"end_us":11185,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11177,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11209,"operation":"read","slot":1,"start_us":11208},{"abi_enter_us":11219,"abi_return_us":11221,"end_us":11221,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11219,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11241,"operation":"read","slot":1,"start_us":11241},{"abi_enter_us":11286,"abi_return_us":11290,"end_us":11291,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11286,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11329,"operation":"read","slot":1,"start_us":11328},{"end_us":11345,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11340},{"abi_enter_us":11467,"abi_return_us":501267,"end_us":501325,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11467,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501864,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501707}],"schema":2,"slots":[{"at_us":501864,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11345,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161aabe531e88-10a77-8b","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":500934,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6189,"slot":1,"token":"18e161aadca49808-10a77-8c"},{"path":"/readiness/siblings/0","received_us":6206,"slot":0,"token":"18e161aadca49808-10a77-8c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161aadca49808-10a77-8c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6217,"bytes":57,"kind":"head","slot":1},{"at_us":6234,"bytes":57,"kind":"head","slot":0},{"at_us":12522,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":127,"operation":"dispatch","outcome":"pending","slot":0,"start_us":37},{"end_us":262,"operation":"dispatch","outcome":"pending","slot":1,"start_us":226},{"body_reads":0,"deadline_us":500000,"end_us":313,"operation":"handoff","start_us":313},{"abi_enter_us":340,"abi_return_us":5754,"end_us":5760,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":340,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5827,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5816},{"abi_enter_us":5842,"abi_return_us":5847,"end_us":5848,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5842,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5863,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5862},{"abi_enter_us":5876,"abi_return_us":12064,"end_us":12069,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5876,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":12278,"operation":"read","slot":1,"start_us":12273},{"abi_enter_us":12296,"abi_return_us":12308,"end_us":12309,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12296,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":16384,"end_us":12337,"operation":"read","slot":1,"start_us":12336},{"end_us":12420,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12412},{"abi_enter_us":12454,"abi_return_us":500658,"end_us":500673,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12454,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":500890,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500875}],"schema":2,"slots":[{"at_us":500890,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12420,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161aadca49808-10a77-8c","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501038,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6704,"slot":0,"token":"18e161aafabda820-10a77-8d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161aafabda820-10a77-8d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6721,"bytes":57,"kind":"head","slot":0},{"at_us":6728,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":61,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"abi_enter_us":118,"abi_return_us":5569,"end_us":5575,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":117,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5638,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5631},{"abi_enter_us":5675,"abi_return_us":5687,"end_us":5688,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5675,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5743,"operation":"read","slot":0,"start_us":5739},{"abi_enter_us":5756,"abi_return_us":500857,"end_us":500862,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5756,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501009,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500998}],"schema":2,"slots":[{"at_us":501009,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161aafabda820-10a77-8d","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501521,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5951,"slot":0,"token":"18e161ab18d76418-10a77-8e"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161ab18d76418-10a77-8e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5962,"bytes":57,"kind":"head","slot":0},{"at_us":5967,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":88,"abi_return_us":5716,"end_us":5721,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5772,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5766},{"abi_enter_us":5791,"abi_return_us":5799,"end_us":5800,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5791,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5830,"operation":"read","slot":0,"start_us":5827},{"abi_enter_us":5843,"abi_return_us":501392,"end_us":501399,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5842,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501480,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501471}],"schema":2,"slots":[{"at_us":501480,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab18d76418-10a77-8e","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501525,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5378,"slot":0,"token":"18e161ab36e22fd8-10a77-8f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161ab36e22fd8-10a77-8f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5394,"bytes":57,"kind":"head","slot":0},{"at_us":5399,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"body_reads":0,"deadline_us":500000,"end_us":92,"operation":"handoff","start_us":92},{"abi_enter_us":110,"abi_return_us":5131,"end_us":5135,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5200,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5194},{"abi_enter_us":5222,"abi_return_us":5228,"end_us":5229,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5222,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5264,"operation":"read","slot":0,"start_us":5261},{"abi_enter_us":5277,"abi_return_us":501367,"end_us":501374,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5276,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501496,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501480}],"schema":2,"slots":[{"at_us":501496,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab36e22fd8-10a77-8f","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":502553,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5708,"slot":0,"token":"18e161ab54f79e40-10a77-90"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161ab54f79e40-10a77-90","watchdog":false,"workers_joined":1,"writes":[{"at_us":5729,"bytes":57,"kind":"head","slot":0},{"at_us":5736,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":116,"operation":"handoff","start_us":116},{"abi_enter_us":162,"abi_return_us":5351,"end_us":5359,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":161,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5425,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5409},{"abi_enter_us":5441,"abi_return_us":5445,"end_us":5446,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5441,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5482,"operation":"read","slot":0,"start_us":5479},{"abi_enter_us":5495,"abi_return_us":502396,"end_us":502411,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5495,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502523,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502507}],"schema":2,"slots":[{"at_us":502523,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab54f79e40-10a77-90","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":6413,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6452,"slot":0,"token":"18e161ab73159440-10a77-91"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab73159440-10a77-91","watchdog":false,"workers_joined":1,"writes":[{"at_us":6469,"bytes":57,"kind":"head","slot":0},{"at_us":6474,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":78,"abi_return_us":6166,"end_us":6173,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6231,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6226},{"abi_enter_us":6247,"abi_return_us":6252,"end_us":6253,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6247,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6283,"operation":"read","slot":0,"start_us":6280},{"abi_enter_us":6299,"abi_return_us":6300,"end_us":6300,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6298,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6321,"operation":"read","slot":0,"start_us":6320},{"abi_enter_us":6333,"abi_return_us":6334,"end_us":6335,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6333,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6355,"operation":"read","slot":0,"start_us":6354},{"abi_enter_us":6365,"abi_return_us":6367,"end_us":6367,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6365,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6386,"operation":"read","slot":0,"start_us":6386},{"end_us":6400,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6397}],"schema":2,"slots":[{"at_us":6400,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab73159440-10a77-91","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":5475,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5524,"slot":0,"token":"18e161ab73a28648-10a77-92"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab73a28648-10a77-92","watchdog":false,"workers_joined":1,"writes":[{"at_us":5538,"bytes":57,"kind":"head","slot":0},{"at_us":5543,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":61,"abi_return_us":5302,"end_us":5308,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5355,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5349},{"abi_enter_us":5370,"abi_return_us":5375,"end_us":5376,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5369,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5410,"operation":"read","slot":0,"start_us":5403},{"abi_enter_us":5423,"abi_return_us":5426,"end_us":5426,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5423,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5450,"operation":"read","slot":0,"start_us":5449},{"end_us":5463,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5459}],"schema":2,"slots":[{"at_us":5463,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab73a28648-10a77-92","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":4833,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6535,"slot":0,"token":"18e161ab749e5ef0-10a77-93"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab749e5ef0-10a77-93","watchdog":false,"workers_joined":1,"writes":[{"at_us":6553,"bytes":57,"kind":"head","slot":0},{"at_us":6560,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":399,"operation":"handoff","start_us":399},{"abi_enter_us":444,"abi_return_us":4323,"end_us":4330,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":444,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4386,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4380},{"abi_enter_us":4401,"abi_return_us":4407,"end_us":4408,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4401,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4440,"operation":"read","slot":0,"start_us":4437},{"abi_enter_us":4454,"abi_return_us":4455,"end_us":4455,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4454,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4477,"operation":"read","slot":0,"start_us":4475},{"abi_enter_us":4492,"abi_return_us":4492,"end_us":4493,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4492,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4518,"operation":"read","slot":0,"start_us":4518},{"abi_enter_us":4586,"abi_return_us":4599,"end_us":4610,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4586,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":1,"end_us":4711,"operation":"read","slot":0,"start_us":4708},{"end_us":4776,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":4769}],"schema":2,"slots":[{"at_us":4776,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab749e5ef0-10a77-93","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":5985,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6827,"slot":0,"token":"18e161ab757fd1c8-10a77-94"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab757fd1c8-10a77-94","watchdog":false,"workers_joined":1,"writes":[{"at_us":6841,"bytes":57,"kind":"head","slot":0},{"at_us":6848,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":98,"operation":"dispatch","outcome":"pending","slot":0,"start_us":44},{"body_reads":0,"deadline_us":500000,"end_us":202,"operation":"handoff","start_us":202},{"abi_enter_us":234,"abi_return_us":5783,"end_us":5790,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":233,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5848,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5842},{"abi_enter_us":5863,"abi_return_us":5871,"end_us":5872,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5863,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5909,"operation":"read","slot":0,"start_us":5906},{"abi_enter_us":5923,"abi_return_us":5926,"end_us":5926,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5923,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5951,"operation":"read","slot":0,"start_us":5950},{"end_us":5966,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5962}],"schema":2,"slots":[{"at_us":5966,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab757fd1c8-10a77-94","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6385,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6392,"slot":0,"token":"18e161ab7614bec8-10a77-95"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab7614bec8-10a77-95","watchdog":false,"workers_joined":1,"writes":[{"at_us":6422,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":74,"operation":"dispatch","outcome":"pending","slot":0,"start_us":34},{"abi_enter_us":146,"abi_return_us":6106,"end_us":6115,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":145,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6205,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6197},{"abi_enter_us":6236,"abi_return_us":6241,"end_us":6241,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6235,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6319,"operation":"read","slot":0,"start_us":6316},{"end_us":6349,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6344}],"schema":2,"slots":[{"at_us":6349,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab7614bec8-10a77-95","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5234,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5303,"slot":0,"token":"18e161ab76a790b8-10a77-96"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab76a790b8-10a77-96","watchdog":false,"workers_joined":1,"writes":[{"at_us":5335,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"abi_enter_us":104,"abi_return_us":5085,"end_us":5091,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5151,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5144},{"abi_enter_us":5165,"abi_return_us":5169,"end_us":5170,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5165,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5200,"operation":"read","slot":0,"start_us":5197},{"end_us":5219,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5215}],"schema":2,"slots":[{"at_us":5219,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab76a790b8-10a77-96","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5063,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5195,"slot":0,"token":"18e161ab771bd270-10a77-97"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab771bd270-10a77-97","watchdog":false,"workers_joined":1,"writes":[{"at_us":5206,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":86,"operation":"handoff","start_us":86},{"abi_enter_us":106,"abi_return_us":4945,"end_us":4949,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4992,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4986},{"abi_enter_us":5005,"abi_return_us":5008,"end_us":5008,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5005,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5035,"operation":"read","slot":0,"start_us":5031},{"end_us":5050,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5047}],"schema":2,"slots":[{"at_us":5050,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab771bd270-10a77-97","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6340,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6395,"slot":0,"token":"18e161ab778aad58-10a77-98"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab778aad58-10a77-98","watchdog":false,"workers_joined":1,"writes":[{"at_us":6403,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":48,"operation":"handoff","start_us":48},{"abi_enter_us":59,"abi_return_us":6212,"end_us":6216,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6250,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6246},{"abi_enter_us":6271,"abi_return_us":6274,"end_us":6274,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6271,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6308,"operation":"read","slot":0,"start_us":6306},{"end_us":6324,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6322}],"schema":2,"slots":[{"at_us":6324,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab778aad58-10a77-98","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5220,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5274,"slot":0,"token":"18e161ab7808bec8-10a77-99"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab7808bec8-10a77-99","watchdog":false,"workers_joined":1,"writes":[{"at_us":5287,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":65,"abi_return_us":5147,"end_us":5150,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5192,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5188},{"end_us":5206,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5206}],"schema":2,"slots":[{"at_us":5206,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ab7808bec8-10a77-99","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":12630,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":9069,"slot":0,"token":"18e161ab78799d50-10a77-9a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab78799d50-10a77-9a","watchdog":false,"workers_joined":1,"writes":[{"at_us":9084,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":109,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":131,"abi_return_us":12274,"end_us":12287,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":131,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":12371,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":12363},{"end_us":12551,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":12550}],"schema":2,"slots":[{"at_us":12551,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ab78799d50-10a77-9a","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":4496,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5388,"slot":0,"token":"18e161ab7a037d58-10a77-9b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab7a037d58-10a77-9b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5402,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":342,"operation":"dispatch","outcome":"pending","slot":0,"start_us":201},{"body_reads":0,"deadline_us":500000,"end_us":374,"operation":"handoff","start_us":374},{"abi_enter_us":485,"abi_return_us":4359,"end_us":4364,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":484,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4444,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":4438},{"end_us":4459,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":4459}],"schema":2,"slots":[{"at_us":4459,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ab7a037d58-10a77-9b","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":7640,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":19450,"slot":0,"token":"18e161ab7b3d3da8-10a77-9c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab7b3d3da8-10a77-9c","watchdog":false,"workers_joined":1,"writes":[{"at_us":19465,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":1451,"operation":"dispatch","outcome":"pending","slot":0,"start_us":33},{"body_reads":0,"deadline_us":500000,"end_us":1630,"operation":"handoff","start_us":1630},{"abi_enter_us":1650,"abi_return_us":5178,"end_us":5184,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1649,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":5217,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5211},{"end_us":7599,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":7598}],"schema":2,"slots":[{"at_us":7599,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ab7b3d3da8-10a77-9c","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":61731,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":21889,"slot":0,"token":"18e161ab7d9ac160-10a77-9d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab7d9ac160-10a77-9d","watchdog":false,"workers_joined":1,"writes":[{"at_us":21931,"bytes":66,"kind":"head","slot":0},{"at_us":21943,"bytes":6,"kind":"body","slot":0},{"at_us":76910,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":115,"abi_return_us":6647,"end_us":6658,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":114,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6747,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6738},{"abi_enter_us":6774,"abi_return_us":6788,"end_us":6789,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6773,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6839,"operation":"read","slot":0,"start_us":6835},{"abi_enter_us":6868,"abi_return_us":61610,"end_us":61616,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6867,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":61691,"error":"Other","operation":"read","slot":0,"start_us":61680},{"end_us":61717,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61709}],"schema":2,"slots":[{"at_us":61717,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab7d9ac160-10a77-9d","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59622,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":7350,"slot":0,"token":"18e161ab8258af00-10a77-9e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab8258af00-10a77-9e","watchdog":false,"workers_joined":1,"writes":[{"at_us":7369,"bytes":66,"kind":"head","slot":0},{"at_us":7381,"bytes":6,"kind":"body","slot":0},{"at_us":59141,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":69,"abi_return_us":7160,"end_us":7176,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7263,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7253},{"abi_enter_us":7290,"abi_return_us":7300,"end_us":7301,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7289,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7369,"operation":"read","slot":0,"start_us":7365},{"abi_enter_us":7405,"abi_return_us":59099,"end_us":59103,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7405,"status":"FastlyStatus::OK","timeout_ms":492},{"capacity":16384,"end_us":59538,"error":"Other","operation":"read","slot":0,"start_us":59529},{"end_us":59590,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59577}],"schema":2,"slots":[{"at_us":59590,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab8258af00-10a77-9e","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58551,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6538,"slot":0,"token":"18e161ab86467b60-10a77-9f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab86467b60-10a77-9f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6747,"bytes":66,"kind":"head","slot":0},{"at_us":6781,"bytes":6,"kind":"body","slot":0},{"at_us":59168,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":159,"operation":"handoff","start_us":159},{"abi_enter_us":220,"abi_return_us":6102,"end_us":6112,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":220,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6202,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6192},{"abi_enter_us":6233,"abi_return_us":6246,"end_us":6246,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6233,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6299,"operation":"read","slot":0,"start_us":6296},{"abi_enter_us":6358,"abi_return_us":58423,"end_us":58430,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6358,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":58510,"error":"Other","operation":"read","slot":0,"start_us":58504},{"end_us":58535,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58528}],"schema":2,"slots":[{"at_us":58535,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab86467b60-10a77-9f","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56211,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5694,"slot":0,"token":"18e161ab89f96588-10a77-a0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab89f96588-10a77-a0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5712,"bytes":66,"kind":"head","slot":0},{"at_us":5718,"bytes":6,"kind":"body","slot":0},{"at_us":56310,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"abi_enter_us":102,"abi_return_us":5469,"end_us":5483,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":101,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5534,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5527},{"abi_enter_us":5552,"abi_return_us":5562,"end_us":5562,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5552,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5592,"operation":"read","slot":0,"start_us":5589},{"abi_enter_us":5604,"abi_return_us":56104,"end_us":56108,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5604,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56173,"error":"Other","operation":"read","slot":0,"start_us":56165},{"end_us":56198,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56191}],"schema":2,"slots":[{"at_us":56198,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab89f96588-10a77-a0","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":5089,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5326,"slot":0,"token":"18e161ab8d7fec68-10a77-a1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab8d7fec68-10a77-a1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":75,"abi_return_us":5017,"end_us":5022,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5056,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5051},{"end_us":5071,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5071}],"schema":2,"slots":[{"at_us":5071,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ab8d7fec68-10a77-a1","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":6201,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6454,"slot":0,"token":"18e161ab8dec5650-10a77-a2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab8dec5650-10a77-a2","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":94,"abi_return_us":6122,"end_us":6128,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6166,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6160},{"end_us":6185,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6185}],"schema":2,"slots":[{"at_us":6185,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ab8dec5650-10a77-a2","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":5174,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5284,"slot":0,"token":"18e161ab8e6cb598-10a77-a3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab8e6cb598-10a77-a3","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"abi_enter_us":80,"abi_return_us":5091,"end_us":5095,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5143,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5138},{"end_us":5156,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5156}],"schema":2,"slots":[{"at_us":5156,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ab8e6cb598-10a77-a3","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":5148,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5265,"slot":0,"token":"18e161ab8edba408-10a77-a4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab8edba408-10a77-a4","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":88,"abi_return_us":5078,"end_us":5083,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5116,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5110},{"end_us":5134,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5133}],"schema":2,"slots":[{"at_us":5134,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ab8edba408-10a77-a4","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":59792,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5429,"slot":0,"token":"18e161ab8f46e510-10a77-a5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab8f46e510-10a77-a5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5446,"bytes":57,"kind":"head","slot":0},{"at_us":5453,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":92,"abi_return_us":5215,"end_us":5220,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5264,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5258},{"bytes":1,"capacity":1,"end_us":5293,"operation":"read","slot":0,"start_us":5284},{"capacity":1,"end_us":59717,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5312},{"end_us":59773,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59767}],"schema":2,"slots":[{"at_us":59773,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab8f46e510-10a77-a5","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":59879,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6023,"slot":0,"token":"18e161ab92f87778-10a77-a6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab92f87778-10a77-a6","watchdog":false,"workers_joined":1,"writes":[{"at_us":6037,"bytes":57,"kind":"head","slot":0},{"at_us":6043,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":67,"abi_return_us":5809,"end_us":5815,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5864,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5858},{"abi_enter_us":5880,"abi_return_us":5884,"end_us":5885,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5880,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5909,"operation":"read","slot":0,"start_us":5907},{"abi_enter_us":5920,"abi_return_us":59716,"end_us":59720,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5920,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":59834,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59825},{"end_us":59864,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59856}],"schema":2,"slots":[{"at_us":59864,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab92f87778-10a77-a6","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":56922,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5346,"slot":0,"token":"18e161ab96bab380-10a77-a7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab96bab380-10a77-a7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5364,"bytes":57,"kind":"head","slot":0},{"at_us":5371,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":173,"operation":"dispatch","outcome":"pending","slot":0,"start_us":138},{"abi_enter_us":368,"abi_return_us":4708,"end_us":4715,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":368,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4800,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4764},{"abi_enter_us":4835,"abi_return_us":4841,"end_us":4841,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4835,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4935,"operation":"read","slot":0,"start_us":4930},{"abi_enter_us":4954,"abi_return_us":56722,"end_us":56727,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4954,"status":"FastlyStatus::OK","timeout_ms":495},{"capacity":16384,"end_us":56822,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56817},{"end_us":56870,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56850}],"schema":2,"slots":[{"at_us":56870,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab96bab380-10a77-a7","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":57189,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6498,"slot":0,"token":"18e161ab9a5677e0-10a77-a8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab9a5677e0-10a77-a8","watchdog":false,"workers_joined":1,"writes":[{"at_us":6511,"bytes":57,"kind":"head","slot":0},{"at_us":6516,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":106,"operation":"handoff","start_us":106},{"abi_enter_us":123,"abi_return_us":6172,"end_us":6178,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":123,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6236,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6229},{"abi_enter_us":6252,"abi_return_us":6257,"end_us":6257,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6252,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6292,"operation":"read","slot":0,"start_us":6289},{"abi_enter_us":6312,"abi_return_us":57076,"end_us":57080,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6312,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":57145,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57140},{"end_us":57174,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57164}],"schema":2,"slots":[{"at_us":57174,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab9a5677e0-10a77-a8","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":61297,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6440,"slot":0,"token":"18e161ab9de7dfe8-10a77-a9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ab9de7dfe8-10a77-a9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6456,"bytes":57,"kind":"head","slot":0},{"at_us":6462,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":93,"operation":"handoff","start_us":93},{"abi_enter_us":110,"abi_return_us":6228,"end_us":6236,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":109,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6291,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6285},{"abi_enter_us":6314,"abi_return_us":6320,"end_us":6320,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6313,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6351,"operation":"read","slot":0,"start_us":6349},{"abi_enter_us":6363,"abi_return_us":61180,"end_us":61185,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6363,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":61254,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":61250},{"end_us":61281,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61276}],"schema":2,"slots":[{"at_us":61281,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ab9de7dfe8-10a77-a9","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501535,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5545,"slot":0,"token":"18e161aba1b64150-10a77-aa"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161aba1b64150-10a77-aa","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":83,"abi_return_us":501406,"end_us":501423,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501515,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501514}],"schema":2,"slots":[{"at_us":501515,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161aba1b64150-10a77-aa","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":500619,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5918,"slot":0,"token":"18e161abbfbc0018-10a77-ab"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161abbfbc0018-10a77-ab","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":73,"abi_return_us":499590,"end_us":499875,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500494,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500494}],"schema":2,"slots":[{"at_us":500494,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161abbfbc0018-10a77-ab","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":500541,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6174,"slot":0,"token":"18e161abddc3c668-10a77-ac"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161abddc3c668-10a77-ac","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":93,"operation":"dispatch","outcome":"pending","slot":0,"start_us":40},{"body_reads":0,"deadline_us":500000,"end_us":180,"operation":"handoff","start_us":180},{"abi_enter_us":200,"abi_return_us":500445,"end_us":500455,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":199,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500523,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500523}],"schema":2,"slots":[{"at_us":500523,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161abddc3c668-10a77-ac","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501254,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6445,"slot":0,"token":"18e161abfbc56e50-10a77-ad"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161abfbc56e50-10a77-ad","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":92,"abi_return_us":501026,"end_us":501069,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501230,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501230}],"schema":2,"slots":[{"at_us":501230,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161abfbc56e50-10a77-ad","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501065,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6780,"slot":0,"token":"18e161ac1a2f4a00-10a77-ae"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161ac1a2f4a00-10a77-ae","watchdog":false,"workers_joined":1,"writes":[{"at_us":6805,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":679,"operation":"dispatch","outcome":"pending","slot":0,"start_us":584},{"abi_enter_us":800,"abi_return_us":5607,"end_us":5677,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":800,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6049,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5808},{"abi_enter_us":6069,"abi_return_us":500328,"end_us":500461,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6069,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500887,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500801}],"schema":2,"slots":[{"at_us":500887,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ac1a2f4a00-10a77-ae","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":500780,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6477,"slot":0,"token":"18e161ac38756940-10a77-af"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161ac38756940-10a77-af","watchdog":false,"workers_joined":1,"writes":[{"at_us":6489,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":34},{"abi_enter_us":114,"abi_return_us":6239,"end_us":6245,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":114,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6307,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6301},{"abi_enter_us":6323,"abi_return_us":500630,"end_us":500636,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6322,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500744,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500718}],"schema":2,"slots":[{"at_us":500744,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ac38756940-10a77-af","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":500475,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6403,"slot":0,"token":"18e161ac566e85a8-10a77-b0"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161ac566e85a8-10a77-b0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6412,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":88,"operation":"handoff","start_us":88},{"abi_enter_us":105,"abi_return_us":6140,"end_us":6142,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":105,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6171,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6165},{"abi_enter_us":6184,"abi_return_us":500347,"end_us":500354,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6184,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500450,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500436}],"schema":2,"slots":[{"at_us":500450,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ac566e85a8-10a77-b0","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":500754,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5710,"slot":0,"token":"18e161ac746d2820-10a77-b1"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161ac746d2820-10a77-b1","watchdog":false,"workers_joined":1,"writes":[{"at_us":5724,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":76,"abi_return_us":5432,"end_us":5436,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5484,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5479},{"abi_enter_us":5501,"abi_return_us":500619,"end_us":500623,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5501,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500708,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500701}],"schema":2,"slots":[{"at_us":500708,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ac746d2820-10a77-b1","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":502057,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5199,"slot":0,"token":"18e161ac92671778-10a77-b2"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161ac92671778-10a77-b2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5213,"bytes":57,"kind":"head","slot":0},{"at_us":5219,"bytes":1,"kind":"body","slot":0},{"at_us":309065,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":318,"operation":"dispatch","outcome":"pending","slot":0,"start_us":42},{"abi_enter_us":405,"abi_return_us":4934,"end_us":4937,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":404,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4979,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4973},{"abi_enter_us":4995,"abi_return_us":5001,"end_us":5001,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4995,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5046,"operation":"read","slot":0,"start_us":5043},{"abi_enter_us":5063,"abi_return_us":308764,"end_us":308768,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5063,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":308857,"operation":"read","slot":0,"start_us":308852},{"abi_enter_us":308874,"abi_return_us":501936,"end_us":501940,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308874,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":502023,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502005}],"schema":2,"slots":[{"at_us":502023,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ac92671778-10a77-b2","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501316,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6445,"slot":0,"token":"18e161acb08d3f20-10a77-b3"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161acb08d3f20-10a77-b3","watchdog":false,"workers_joined":1,"writes":[{"at_us":6461,"bytes":57,"kind":"head","slot":0},{"at_us":6467,"bytes":1,"kind":"body","slot":0},{"at_us":308483,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":81,"abi_return_us":6128,"end_us":6132,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6178,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6173},{"abi_enter_us":6192,"abi_return_us":6197,"end_us":6197,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6192,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6230,"operation":"read","slot":0,"start_us":6227},{"abi_enter_us":6242,"abi_return_us":308194,"end_us":308207,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6242,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":308322,"operation":"read","slot":0,"start_us":308318},{"abi_enter_us":308341,"abi_return_us":501204,"end_us":501210,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308341,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501286,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501275}],"schema":2,"slots":[{"at_us":501286,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161acb08d3f20-10a77-b3","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501583,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6432,"slot":0,"token":"18e161accea11f18-10a77-b4"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161accea11f18-10a77-b4","watchdog":false,"workers_joined":1,"writes":[{"at_us":6444,"bytes":57,"kind":"head","slot":0},{"at_us":6451,"bytes":1,"kind":"body","slot":0},{"at_us":306810,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":100,"abi_return_us":6106,"end_us":6110,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":100,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6142,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6136},{"abi_enter_us":6154,"abi_return_us":6159,"end_us":6160,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6154,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6188,"operation":"read","slot":0,"start_us":6185},{"abi_enter_us":6200,"abi_return_us":306528,"end_us":306532,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6200,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":306619,"operation":"read","slot":0,"start_us":306616},{"abi_enter_us":306637,"abi_return_us":501477,"end_us":501481,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306636,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":501545,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501538}],"schema":2,"slots":[{"at_us":501545,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161accea11f18-10a77-b4","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501153,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5619,"slot":0,"token":"18e161acecb247c0-10a77-b5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161acecb247c0-10a77-b5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5627,"bytes":57,"kind":"head","slot":0},{"at_us":5633,"bytes":1,"kind":"body","slot":0},{"at_us":305741,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"abi_enter_us":76,"abi_return_us":5346,"end_us":5347,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5377,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5370},{"abi_enter_us":5389,"abi_return_us":5392,"end_us":5393,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5389,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5418,"operation":"read","slot":0,"start_us":5416},{"abi_enter_us":5429,"abi_return_us":305480,"end_us":305484,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5429,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":305573,"operation":"read","slot":0,"start_us":305569},{"abi_enter_us":305588,"abi_return_us":501023,"end_us":501027,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":305588,"status":"FastlyStatus::OK","timeout_ms":194},{"end_us":501109,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501103}],"schema":2,"slots":[{"at_us":501109,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161acecb247c0-10a77-b5","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":500550,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6497,"slot":1,"token":"18e161ad0abd1380-10a77-b6"},{"path":"/readiness/siblings/0","received_us":6510,"slot":0,"token":"18e161ad0abd1380-10a77-b6"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161ad0abd1380-10a77-b6","watchdog":false,"workers_joined":2,"writes":[{"at_us":6513,"bytes":57,"kind":"head","slot":1},{"at_us":6519,"bytes":57,"kind":"head","slot":0},{"at_us":12112,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":120,"operation":"dispatch","outcome":"pending","slot":1,"start_us":95},{"abi_enter_us":137,"abi_return_us":6237,"end_us":6242,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":136,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6288,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6283},{"abi_enter_us":6307,"abi_return_us":6309,"end_us":6309,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6306,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6327,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6326},{"abi_enter_us":6340,"abi_return_us":11807,"end_us":11810,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6340,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":11858,"operation":"read","slot":1,"start_us":11855},{"abi_enter_us":11869,"abi_return_us":11871,"end_us":11871,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11869,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11891,"operation":"read","slot":1,"start_us":11889},{"abi_enter_us":11899,"abi_return_us":11900,"end_us":11901,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11899,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11921,"operation":"read","slot":1,"start_us":11921},{"abi_enter_us":11932,"abi_return_us":11935,"end_us":11935,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11932,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11956,"operation":"read","slot":1,"start_us":11956},{"end_us":11968,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11964},{"abi_enter_us":11983,"abi_return_us":500444,"end_us":500448,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11983,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500524,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500517}],"schema":2,"slots":[{"at_us":500524,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11968,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ad0abd1380-10a77-b6","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":500455,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5240,"slot":0,"token":"18e161ad28cd3288-10a77-b7"},{"path":"/readiness/siblings/1","received_us":5246,"slot":1,"token":"18e161ad28cd3288-10a77-b7"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161ad28cd3288-10a77-b7","watchdog":false,"workers_joined":2,"writes":[{"at_us":5256,"bytes":57,"kind":"head","slot":1},{"at_us":5265,"bytes":57,"kind":"head","slot":0},{"at_us":5274,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":98,"operation":"dispatch","outcome":"pending","slot":1,"start_us":74},{"abi_enter_us":116,"abi_return_us":4976,"end_us":4979,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5019,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5014},{"abi_enter_us":5034,"abi_return_us":5037,"end_us":5037,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5034,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5056,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5055},{"abi_enter_us":5067,"abi_return_us":5070,"end_us":5070,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5067,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5098,"operation":"read","slot":1,"start_us":5096},{"abi_enter_us":5112,"abi_return_us":5114,"end_us":5115,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5112,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5139,"operation":"read","slot":1,"start_us":5138},{"end_us":5152,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5149},{"abi_enter_us":5165,"abi_return_us":500328,"end_us":500337,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5165,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500433,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500423}],"schema":2,"slots":[{"at_us":500433,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5152,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ad28cd3288-10a77-b7","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501719,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6544,"slot":0,"token":"18e161ad46d74a98-10a77-b8"},{"path":"/readiness/siblings/1","received_us":6584,"slot":1,"token":"18e161ad46d74a98-10a77-b8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161ad46d74a98-10a77-b8","watchdog":false,"workers_joined":2,"writes":[{"at_us":6562,"bytes":57,"kind":"head","slot":0},{"at_us":6594,"bytes":57,"kind":"head","slot":1},{"at_us":6602,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"end_us":153,"operation":"dispatch","outcome":"pending","slot":1,"start_us":106},{"body_reads":0,"deadline_us":500000,"end_us":195,"operation":"handoff","start_us":195},{"abi_enter_us":208,"abi_return_us":6278,"end_us":6285,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":208,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6353,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6345},{"abi_enter_us":6381,"abi_return_us":6385,"end_us":6386,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6381,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6413,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6411},{"abi_enter_us":6427,"abi_return_us":6432,"end_us":6432,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6426,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6463,"operation":"read","slot":1,"start_us":6460},{"abi_enter_us":6475,"abi_return_us":6477,"end_us":6477,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6475,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6499,"operation":"read","slot":1,"start_us":6498},{"abi_enter_us":6512,"abi_return_us":6513,"end_us":6513,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6512,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6533,"operation":"read","slot":1,"start_us":6532},{"abi_enter_us":6542,"abi_return_us":6544,"end_us":6545,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6542,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6567,"operation":"read","slot":1,"start_us":6567},{"end_us":6585,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6580},{"abi_enter_us":6600,"abi_return_us":501632,"end_us":501635,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6600,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501700,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501689}],"schema":2,"slots":[{"at_us":501700,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6585,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ad46d74a98-10a77-b8","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501576,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5309,"slot":1,"token":"18e161ad650019f0-10a77-b9"},{"path":"/readiness/siblings/0","received_us":5332,"slot":0,"token":"18e161ad650019f0-10a77-b9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161ad650019f0-10a77-b9","watchdog":false,"workers_joined":2,"writes":[{"at_us":5341,"bytes":57,"kind":"head","slot":1},{"at_us":5363,"bytes":57,"kind":"head","slot":0},{"at_us":11639,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"end_us":122,"operation":"dispatch","outcome":"pending","slot":1,"start_us":100},{"body_reads":0,"deadline_us":500000,"end_us":151,"operation":"handoff","start_us":151},{"abi_enter_us":171,"abi_return_us":4958,"end_us":4964,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":171,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5014,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5008},{"abi_enter_us":5029,"abi_return_us":5031,"end_us":5032,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5029,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5048,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5046},{"abi_enter_us":5060,"abi_return_us":11255,"end_us":11258,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5060,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11333,"operation":"read","slot":1,"start_us":11329},{"abi_enter_us":11347,"abi_return_us":11353,"end_us":11353,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11347,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11381,"operation":"read","slot":1,"start_us":11380},{"end_us":11396,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11391},{"abi_enter_us":11413,"abi_return_us":501327,"end_us":501334,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11413,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501537,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501527}],"schema":2,"slots":[{"at_us":501537,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11396,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ad650019f0-10a77-b9","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501248,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5269,"slot":0,"token":"18e161ad83223a58-10a77-ba"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161ad83223a58-10a77-ba","watchdog":false,"workers_joined":1,"writes":[{"at_us":5288,"bytes":57,"kind":"head","slot":0},{"at_us":5295,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":96,"abi_return_us":4961,"end_us":4967,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5029,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5023},{"abi_enter_us":5045,"abi_return_us":5053,"end_us":5053,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5045,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5087,"operation":"read","slot":0,"start_us":5084},{"abi_enter_us":5100,"abi_return_us":501158,"end_us":501162,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5100,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501229,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501221}],"schema":2,"slots":[{"at_us":501229,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ad83223a58-10a77-ba","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":502536,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6070,"slot":0,"token":"18e161ada12b40f8-10a77-bb"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161ada12b40f8-10a77-bb","watchdog":false,"workers_joined":1,"writes":[{"at_us":6087,"bytes":57,"kind":"head","slot":0},{"at_us":6094,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":78,"abi_return_us":5803,"end_us":5806,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5857,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5852},{"abi_enter_us":5873,"abi_return_us":5878,"end_us":5879,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5873,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5914,"operation":"read","slot":0,"start_us":5911},{"abi_enter_us":5927,"abi_return_us":501977,"end_us":502003,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5927,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502424,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502405}],"schema":2,"slots":[{"at_us":502424,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ada12b40f8-10a77-bb","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501709,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5914,"slot":0,"token":"18e161adbff889c8-10a77-bc"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161adbff889c8-10a77-bc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5938,"bytes":57,"kind":"head","slot":0},{"at_us":5947,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":132,"operation":"dispatch","outcome":"pending","slot":0,"start_us":52},{"body_reads":0,"deadline_us":500000,"end_us":207,"operation":"handoff","start_us":207},{"abi_enter_us":233,"abi_return_us":5391,"end_us":5398,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":232,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5455,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5446},{"abi_enter_us":5474,"abi_return_us":5482,"end_us":5482,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5474,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5593,"operation":"read","slot":0,"start_us":5590},{"abi_enter_us":5606,"abi_return_us":501556,"end_us":501567,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5606,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501642,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501634}],"schema":2,"slots":[{"at_us":501642,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161adbff889c8-10a77-bc","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501438,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6463,"slot":0,"token":"18e161adde1f3a28-10a77-bd"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161adde1f3a28-10a77-bd","watchdog":false,"workers_joined":1,"writes":[{"at_us":6479,"bytes":57,"kind":"head","slot":0},{"at_us":6486,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":90,"operation":"handoff","start_us":90},{"abi_enter_us":104,"abi_return_us":6183,"end_us":6188,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6228,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6223},{"abi_enter_us":6243,"abi_return_us":6249,"end_us":6249,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6243,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6276,"operation":"read","slot":0,"start_us":6273},{"abi_enter_us":6287,"abi_return_us":501339,"end_us":501344,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6287,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501412,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501405}],"schema":2,"slots":[{"at_us":501412,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161adde1f3a28-10a77-bd","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":5194,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5228,"slot":0,"token":"18e161adfc2b6960-10a77-be"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161adfc2b6960-10a77-be","watchdog":false,"workers_joined":1,"writes":[{"at_us":5242,"bytes":57,"kind":"head","slot":0},{"at_us":5247,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":76,"abi_return_us":4982,"end_us":4986,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5026,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5020},{"abi_enter_us":5039,"abi_return_us":5044,"end_us":5044,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5039,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5071,"operation":"read","slot":0,"start_us":5069},{"abi_enter_us":5087,"abi_return_us":5088,"end_us":5088,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5087,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5108,"operation":"read","slot":0,"start_us":5107},{"abi_enter_us":5117,"abi_return_us":5118,"end_us":5118,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5117,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5138,"operation":"read","slot":0,"start_us":5138},{"abi_enter_us":5149,"abi_return_us":5151,"end_us":5151,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5149,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5169,"operation":"read","slot":0,"start_us":5168},{"end_us":5180,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5177}],"schema":2,"slots":[{"at_us":5180,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161adfc2b6960-10a77-be","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":5972,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5976,"slot":0,"token":"18e161adfca68cd0-10a77-bf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161adfca68cd0-10a77-bf","watchdog":false,"workers_joined":1,"writes":[{"at_us":5994,"bytes":57,"kind":"head","slot":0},{"at_us":5999,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":51,"abi_return_us":5803,"end_us":5808,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5855,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5849},{"abi_enter_us":5870,"abi_return_us":5876,"end_us":5876,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5870,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5909,"operation":"read","slot":0,"start_us":5906},{"abi_enter_us":5921,"abi_return_us":5923,"end_us":5924,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5921,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5945,"operation":"read","slot":0,"start_us":5944},{"end_us":5959,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5954}],"schema":2,"slots":[{"at_us":5959,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161adfca68cd0-10a77-bf","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":5847,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5826,"slot":0,"token":"18e161adfd249288-10a77-c0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161adfd249288-10a77-c0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5839,"bytes":57,"kind":"head","slot":0},{"at_us":5844,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"abi_enter_us":84,"abi_return_us":5636,"end_us":5640,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5681,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5676},{"abi_enter_us":5693,"abi_return_us":5698,"end_us":5698,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5693,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5723,"operation":"read","slot":0,"start_us":5720},{"abi_enter_us":5737,"abi_return_us":5738,"end_us":5738,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5737,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5758,"operation":"read","slot":0,"start_us":5757},{"abi_enter_us":5767,"abi_return_us":5768,"end_us":5768,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5767,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5787,"operation":"read","slot":0,"start_us":5786},{"abi_enter_us":5799,"abi_return_us":5801,"end_us":5802,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5799,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5820,"operation":"read","slot":0,"start_us":5820},{"end_us":5832,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5829}],"schema":2,"slots":[{"at_us":5832,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161adfd249288-10a77-c0","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":6482,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6584,"slot":0,"token":"18e161adfda908b0-10a77-c1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161adfda908b0-10a77-c1","watchdog":false,"workers_joined":1,"writes":[{"at_us":6603,"bytes":57,"kind":"head","slot":0},{"at_us":6625,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":79,"abi_return_us":6327,"end_us":6332,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6376,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6370},{"abi_enter_us":6388,"abi_return_us":6393,"end_us":6393,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6388,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6421,"operation":"read","slot":0,"start_us":6419},{"abi_enter_us":6432,"abi_return_us":6434,"end_us":6434,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6432,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6456,"operation":"read","slot":0,"start_us":6455},{"end_us":6469,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6464}],"schema":2,"slots":[{"at_us":6469,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161adfda908b0-10a77-c1","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6345,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6417,"slot":0,"token":"18e161adfe30fd60-10a77-c2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161adfe30fd60-10a77-c2","watchdog":false,"workers_joined":1,"writes":[{"at_us":6429,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":70,"abi_return_us":6251,"end_us":6254,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6284,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6280},{"abi_enter_us":6296,"abi_return_us":6298,"end_us":6300,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6296,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6321,"operation":"read","slot":0,"start_us":6320},{"end_us":6333,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6331}],"schema":2,"slots":[{"at_us":6333,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161adfe30fd60-10a77-c2","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5478,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5314,"slot":0,"token":"18e161adfeb06690-10a77-c3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161adfeb06690-10a77-c3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5344,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":64,"abi_return_us":5150,"end_us":5151,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5179,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5175},{"abi_enter_us":5189,"abi_return_us":5213,"end_us":5217,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5189,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5396,"operation":"read","slot":0,"start_us":5395},{"end_us":5464,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5462}],"schema":2,"slots":[{"at_us":5464,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161adfeb06690-10a77-c3","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5415,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5488,"slot":0,"token":"18e161adff26abe8-10a77-c4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161adff26abe8-10a77-c4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5500,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"abi_enter_us":64,"abi_return_us":5317,"end_us":5320,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5352,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5348},{"abi_enter_us":5363,"abi_return_us":5366,"end_us":5367,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5363,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5390,"operation":"read","slot":0,"start_us":5389},{"end_us":5403,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5400}],"schema":2,"slots":[{"at_us":5403,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161adff26abe8-10a77-c4","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6055,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6167,"slot":0,"token":"18e161adff9aa750-10a77-c5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161adff9aa750-10a77-c5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6174,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"abi_enter_us":75,"abi_return_us":5966,"end_us":5968,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5991,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5987},{"abi_enter_us":6002,"abi_return_us":6004,"end_us":6004,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6002,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6031,"operation":"read","slot":0,"start_us":6030},{"end_us":6043,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6041}],"schema":2,"slots":[{"at_us":6043,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161adff9aa750-10a77-c5","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5500,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5646,"slot":0,"token":"18e161ae001c8568-10a77-c6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae001c8568-10a77-c6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5653,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":53,"abi_return_us":5454,"end_us":5455,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5476,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5471},{"end_us":5488,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5487}],"schema":2,"slots":[{"at_us":5488,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ae001c8568-10a77-c6","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6313,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6413,"slot":0,"token":"18e161ae008bdd50-10a77-c7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae008bdd50-10a77-c7","watchdog":false,"workers_joined":1,"writes":[{"at_us":6420,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":58,"abi_return_us":6266,"end_us":6268,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6288,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6285},{"end_us":6301,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6300}],"schema":2,"slots":[{"at_us":6301,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ae008bdd50-10a77-c7","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6139,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6242,"slot":0,"token":"18e161ae0108b640-10a77-c8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae0108b640-10a77-c8","watchdog":false,"workers_joined":1,"writes":[{"at_us":6253,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":45,"operation":"handoff","start_us":45},{"abi_enter_us":61,"abi_return_us":6076,"end_us":6079,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6107,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6102},{"end_us":6121,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6121}],"schema":2,"slots":[{"at_us":6121,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ae0108b640-10a77-c8","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6242,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6391,"slot":0,"token":"18e161ae01853170-10a77-c9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae01853170-10a77-c9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6398,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":55,"operation":"handoff","start_us":55},{"abi_enter_us":67,"abi_return_us":6200,"end_us":6201,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6219,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6217},{"end_us":6230,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6230}],"schema":2,"slots":[{"at_us":6230,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ae01853170-10a77-c9","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":61084,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6493,"slot":0,"token":"18e161ae01ffdfb0-10a77-ca"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae01ffdfb0-10a77-ca","watchdog":false,"workers_joined":1,"writes":[{"at_us":6519,"bytes":66,"kind":"head","slot":0},{"at_us":6524,"bytes":6,"kind":"body","slot":0},{"at_us":61236,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":49,"abi_return_us":6244,"end_us":6248,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6282,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6277},{"abi_enter_us":6296,"abi_return_us":6301,"end_us":6301,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6294,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6324,"operation":"read","slot":0,"start_us":6322},{"abi_enter_us":6335,"abi_return_us":60971,"end_us":60974,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6335,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":61041,"error":"Other","operation":"read","slot":0,"start_us":61029},{"end_us":61068,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61062}],"schema":2,"slots":[{"at_us":61068,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ae01ffdfb0-10a77-ca","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56865,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5186,"slot":0,"token":"18e161ae05cd23f0-10a77-cb"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae05cd23f0-10a77-cb","watchdog":false,"workers_joined":1,"writes":[{"at_us":5196,"bytes":66,"kind":"head","slot":0},{"at_us":5200,"bytes":6,"kind":"body","slot":0},{"at_us":56987,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":65,"abi_return_us":4912,"end_us":4914,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4952,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4947},{"abi_enter_us":4964,"abi_return_us":4968,"end_us":4968,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4964,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4999,"operation":"read","slot":0,"start_us":4998},{"abi_enter_us":5011,"abi_return_us":56743,"end_us":56747,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5011,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56820,"error":"Other","operation":"read","slot":0,"start_us":56815},{"end_us":56850,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56844}],"schema":2,"slots":[{"at_us":56850,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ae05cd23f0-10a77-cb","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59529,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5265,"slot":0,"token":"18e161ae0957d920-10a77-cc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae0957d920-10a77-cc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5274,"bytes":66,"kind":"head","slot":0},{"at_us":5279,"bytes":6,"kind":"body","slot":0},{"at_us":59586,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"abi_enter_us":82,"abi_return_us":4991,"end_us":4993,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5026,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5021},{"abi_enter_us":5038,"abi_return_us":5042,"end_us":5043,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5037,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5083,"operation":"read","slot":0,"start_us":5081},{"abi_enter_us":5099,"abi_return_us":59352,"end_us":59356,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5098,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":59484,"error":"Other","operation":"read","slot":0,"start_us":59478},{"end_us":59512,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59504}],"schema":2,"slots":[{"at_us":59512,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ae0957d920-10a77-cc","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56294,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5221,"slot":0,"token":"18e161ae0d0cd2a0-10a77-cd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae0d0cd2a0-10a77-cd","watchdog":false,"workers_joined":1,"writes":[{"at_us":5234,"bytes":66,"kind":"head","slot":0},{"at_us":5238,"bytes":6,"kind":"body","slot":0},{"at_us":56423,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":84,"abi_return_us":4965,"end_us":4967,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4994,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4989},{"abi_enter_us":5007,"abi_return_us":5011,"end_us":5011,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5007,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5035,"operation":"read","slot":0,"start_us":5034},{"abi_enter_us":5046,"abi_return_us":56188,"end_us":56192,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5046,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56256,"error":"Other","operation":"read","slot":0,"start_us":56250},{"end_us":56280,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56273}],"schema":2,"slots":[{"at_us":56280,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161ae0d0cd2a0-10a77-cd","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":6082,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6251,"slot":0,"token":"18e161ae10906798-10a77-ce"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae10906798-10a77-ce","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":75,"abi_return_us":6024,"end_us":6027,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6054,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6050},{"end_us":6068,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6068}],"schema":2,"slots":[{"at_us":6068,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ae10906798-10a77-ce","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":6018,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6122,"slot":0,"token":"18e161ae110cf650-10a77-cf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae110cf650-10a77-cf","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":51,"abi_return_us":5957,"end_us":5960,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5991,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5984},{"end_us":6004,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6004}],"schema":2,"slots":[{"at_us":6004,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ae110cf650-10a77-cf","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":6401,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6426,"slot":0,"token":"18e161ae11832050-10a77-d0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae11832050-10a77-d0","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"abi_enter_us":62,"abi_return_us":6327,"end_us":6332,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6367,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6363},{"end_us":6386,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6386}],"schema":2,"slots":[{"at_us":6386,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ae11832050-10a77-d0","tolerance_us":5000}} +{"run":"repaired-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":5810,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5253,"slot":0,"token":"18e161ae12006a88-10a77-d1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161ae12006a88-10a77-d1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":73,"operation":"handoff","start_us":73},{"abi_enter_us":83,"abi_return_us":5058,"end_us":5063,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5764,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5759},{"end_us":5792,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5792}],"schema":2,"slots":[{"at_us":5792,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161ae12006a88-10a77-d1","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1255,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":255,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b7750d2428-11a68-3","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2263,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2263,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1263,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b7752330b0-11a68-4","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2267,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1267,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b7754fce90-11a68-5","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103863,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3863,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b7757e9b08-11a68-6","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102281,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102281,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2281,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b77bb3d6c8-11a68-7","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102621,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2621,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b7823ad050-11a68-8","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500771,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":771,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b78870b020-11a68-9","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501898,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501898,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1898,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b7a650f140-11a68-a","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502074,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2074,"qualification":false,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b7c451ce08-11a68-b","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1259,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":259,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b7e24f3be8-11a68-c","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2266,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2266,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1266,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b7e2652548-11a68-d","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2046,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1046,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b7e2943fe0-11a68-e","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103353,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3353,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b7e2bcb358-11a68-f","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":100847,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":100847,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":847,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b7e8ea7cd8-11a68-10","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101569,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1569,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b7ef00e210-11a68-11","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500567,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":567,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b7f51c63e0-11a68-12","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502255,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502255,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2255,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b812f74dd0-11a68-13","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502091,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2091,"qualification":false,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b8310e5660-11a68-14","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1257,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":257,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b84f136560-11a68-15","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2268,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2268,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1268,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b84f298d40-11a68-16","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2279,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1279,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b84f6bd600-11a68-17","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102427,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2427,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b84f9ad928-11a68-18","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":2,"abi_return_us":101619,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101619,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1619,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b855ba3740-11a68-19","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101301,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1301,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b85bd84180-11a68-1a","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503032,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3032,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b861ef7d90-11a68-1b","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502104,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502104,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2104,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b87fef9ed8-11a68-1c","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502228,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2228,"qualification":false,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b89e0042b0-11a68-1d","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1258,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":258,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b8bc0810d0-11a68-1e","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2269,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2269,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1269,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b8bc1e4468-11a68-1f","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2270,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1270,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b8bc4c5178-11a68-20","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102773,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2773,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b8bc788db0-11a68-21","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101092,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101092,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1092,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b8c29d6df0-11a68-22","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101883,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1883,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b8c8b30bf0-11a68-23","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501812,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1812,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b8ced40048-11a68-24","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502108,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502108,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2108,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b8ecc237a0-11a68-25","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501274,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1274,"qualification":false,"repetition":3,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b90ac308b0-11a68-26","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1049,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":49,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b928b366d0-11a68-27","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2271,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2271,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1271,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b928c68d28-11a68-28","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2029,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1029,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b928f647e8-11a68-29","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101482,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1482,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b92921dc28-11a68-2a","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101670,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101670,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1670,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b92f342698-11a68-2b","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101129,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1129,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b9355380c8-11a68-2c","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503981,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3981,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b93b6899f8-11a68-2d","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":500503,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":500503,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":503,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b959776528-11a68-2e","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501142,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1142,"qualification":false,"repetition":4,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b9775db8f8-11a68-2f","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1263,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":263,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b9954dd898-11a68-30","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2282,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2282,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1282,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b995658ee8-11a68-31","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2089,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1089,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b995998b80-11a68-32","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103473,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3473,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b995c4f0e0-11a68-33","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101535,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101535,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1535,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b99bf45c58-11a68-34","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102079,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2079,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b9a2156438-11a68-35","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500049,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":49,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161b9a83ae4a0-11a68-36","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501153,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501153,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1153,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161b9c60d9518-11a68-37","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501218,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1218,"qualification":false,"repetition":5,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161b9e3ff6a38-11a68-38","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1264,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":264,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161ba02005e70-11a68-39","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2267,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2267,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1267,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161ba02167a98-11a68-3a","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2288,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1288,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161ba0246cdb0-11a68-3b","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101033,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1033,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161ba0274a028-11a68-3c","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101712,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101712,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1712,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161ba088a9030-11a68-3d","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102059,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2059,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161ba0eb18798-11a68-3e","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501315,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1315,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161ba14d60a18-11a68-3f","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502245,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502245,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2245,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161ba32bc2350-11a68-40","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501783,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1783,"qualification":false,"repetition":6,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161ba50c992d8-11a68-41","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1256,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":256,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161ba6ec5e390-11a68-42","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2273,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2273,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1273,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161ba6eddd090-11a68-43","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2270,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1270,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161ba6f121760-11a68-44","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":104751,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4751,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161ba6f42f330-11a68-45","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101424,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101424,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1424,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161ba75871758-11a68-46","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":106952,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":6952,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161ba7ba2c420-11a68-47","within_five_ms":false}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502684,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2684,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"native-sleep","token":"18e161ba82acdc10-11a68-48","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502217,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502217,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2217,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e161baa0a957c0-11a68-49","within_five_ms":true}} +{"run":"stock-reviewed","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500902,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":902,"qualification":false,"repetition":7,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e161babeb9c4e8-11a68-4a","within_five_ms":true}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":57563,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6506,"slot":0,"token":"18e161badcab9620-11a68-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161badcab9620-11a68-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6522,"bytes":57,"kind":"head","slot":0},{"at_us":6528,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":979,"operation":"dispatch","outcome":"pending","slot":0,"start_us":389},{"abi_enter_us":1020,"abi_return_us":5594,"end_us":5599,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1019,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":5806,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5652},{"bytes":1,"capacity":1,"end_us":5834,"operation":"read","slot":0,"start_us":5831},{"capacity":1,"end_us":57480,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5864},{"end_us":57545,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57538}],"schema":2,"slots":[{"at_us":57545,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161badcab9620-11a68-4b","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":60029,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6142,"slot":0,"token":"18e161bae04ad908-11a68-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bae04ad908-11a68-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6156,"bytes":57,"kind":"head","slot":0},{"at_us":6162,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":91,"abi_return_us":5926,"end_us":5930,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5967,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5962},{"abi_enter_us":5982,"abi_return_us":5992,"end_us":5992,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5982,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":6016,"operation":"read","slot":0,"start_us":6014},{"abi_enter_us":6027,"abi_return_us":59901,"end_us":59910,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6027,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":59984,"operation":"read","slot":0,"start_us":59980},{"end_us":60010,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60005}],"schema":2,"slots":[{"at_us":60010,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bae04ad908-11a68-4c","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":57495,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6462,"slot":0,"token":"18e161bae40767f0-11a68-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bae40767f0-11a68-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6479,"bytes":57,"kind":"head","slot":0},{"at_us":6485,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":90,"abi_return_us":6179,"end_us":6185,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6239,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6234},{"abi_enter_us":6254,"abi_return_us":6258,"end_us":6258,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6253,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6289,"operation":"read","slot":0,"start_us":6286},{"abi_enter_us":6301,"abi_return_us":57372,"end_us":57376,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6301,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":57452,"operation":"read","slot":0,"start_us":57444},{"end_us":57480,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57475}],"schema":2,"slots":[{"at_us":57480,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bae40767f0-11a68-4d","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":60654,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5730,"slot":0,"token":"18e161bae79d9e70-11a68-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bae79d9e70-11a68-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5742,"bytes":57,"kind":"head","slot":0},{"at_us":5749,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":64,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":111,"operation":"handoff","start_us":111},{"abi_enter_us":131,"abi_return_us":5520,"end_us":5527,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":131,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5581,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5576},{"abi_enter_us":5593,"abi_return_us":5603,"end_us":5604,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5593,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5632,"operation":"read","slot":0,"start_us":5630},{"abi_enter_us":5642,"abi_return_us":60534,"end_us":60538,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5642,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":60618,"operation":"read","slot":0,"start_us":60614},{"end_us":60637,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60631}],"schema":2,"slots":[{"at_us":60637,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bae79d9e70-11a68-4e","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":55547,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5211,"slot":0,"token":"18e161baeb5e5f90-11a68-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161baeb5e5f90-11a68-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5224,"bytes":57,"kind":"head","slot":0},{"at_us":5230,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":91,"abi_return_us":4947,"end_us":4952,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4997,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4992},{"abi_enter_us":5010,"abi_return_us":5014,"end_us":5014,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5010,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5049,"operation":"read","slot":0,"start_us":5048},{"abi_enter_us":5060,"abi_return_us":55434,"end_us":55438,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5060,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":55513,"operation":"read","slot":0,"start_us":55510},{"end_us":55533,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55528}],"schema":2,"slots":[{"at_us":55533,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161baeb5e5f90-11a68-4f","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501845,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5266,"slot":0,"token":"18e161baeedac4b0-11a68-50"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161baeedac4b0-11a68-50","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":90,"abi_return_us":501748,"end_us":501758,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501823,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501823}],"schema":2,"slots":[{"at_us":501823,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161baeedac4b0-11a68-50","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501167,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5694,"slot":0,"token":"18e161bb0ce5de90-11a68-51"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bb0ce5de90-11a68-51","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":85,"abi_return_us":501067,"end_us":501074,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501143,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501136}],"schema":2,"slots":[{"at_us":501143,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bb0ce5de90-11a68-51","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501425,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5467,"slot":0,"token":"18e161bb2ae6de80-11a68-52"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bb2ae6de80-11a68-52","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"abi_enter_us":72,"abi_return_us":501337,"end_us":501344,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501405,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501405}],"schema":2,"slots":[{"at_us":501405,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bb2ae6de80-11a68-52","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":500115,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5259,"slot":0,"token":"18e161bb48e59c50-11a68-53"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bb48e59c50-11a68-53","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":86,"abi_return_us":500034,"end_us":500041,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500096,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500095}],"schema":2,"slots":[{"at_us":500096,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bb48e59c50-11a68-53","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":500409,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5429,"slot":0,"token":"18e161bb66d5db30-11a68-54"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bb66d5db30-11a68-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":5442,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":66,"abi_return_us":5173,"end_us":5176,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5214,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5211},{"abi_enter_us":5226,"abi_return_us":500329,"end_us":500332,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5226,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500389,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500383}],"schema":2,"slots":[{"at_us":500389,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bb66d5db30-11a68-54","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501572,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5203,"slot":0,"token":"18e161bb84c7b820-11a68-55"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bb84c7b820-11a68-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":5215,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":104,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":130,"abi_return_us":4979,"end_us":4980,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":130,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5008,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5004},{"abi_enter_us":5020,"abi_return_us":501384,"end_us":501394,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5020,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501505,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501492}],"schema":2,"slots":[{"at_us":501505,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bb84c7b820-11a68-55","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501145,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5217,"slot":0,"token":"18e161bba2cf5378-11a68-56"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bba2cf5378-11a68-56","watchdog":false,"workers_joined":1,"writes":[{"at_us":5232,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":73,"operation":"handoff","start_us":73},{"abi_enter_us":86,"abi_return_us":4898,"end_us":4902,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4977,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4961},{"abi_enter_us":5028,"abi_return_us":500937,"end_us":500945,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5028,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501107,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501101}],"schema":2,"slots":[{"at_us":501107,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bba2cf5378-11a68-56","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":500370,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6444,"slot":0,"token":"18e161bbc0cfe9f0-11a68-57"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bbc0cfe9f0-11a68-57","watchdog":false,"workers_joined":1,"writes":[{"at_us":6464,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":93,"abi_return_us":6196,"end_us":6200,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6238,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6234},{"abi_enter_us":6253,"abi_return_us":500263,"end_us":500267,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6253,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500342,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500330}],"schema":2,"slots":[{"at_us":500342,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bbc0cfe9f0-11a68-57","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501152,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5534,"slot":0,"token":"18e161bbdec5c268-11a68-58"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bbdec5c268-11a68-58","watchdog":false,"workers_joined":1,"writes":[{"at_us":5550,"bytes":57,"kind":"head","slot":0},{"at_us":5557,"bytes":1,"kind":"body","slot":0},{"at_us":308157,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":69,"operation":"dispatch","outcome":"pending","slot":0,"start_us":33},{"abi_enter_us":97,"abi_return_us":5288,"end_us":5290,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":97,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5338,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5329},{"abi_enter_us":5349,"abi_return_us":5353,"end_us":5353,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5349,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5374,"operation":"read","slot":0,"start_us":5372},{"abi_enter_us":5391,"abi_return_us":307915,"end_us":307922,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5391,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307996,"operation":"read","slot":0,"start_us":307993},{"abi_enter_us":308010,"abi_return_us":501069,"end_us":501073,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308010,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501130,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501122}],"schema":2,"slots":[{"at_us":501130,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bbdec5c268-11a68-58","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":500236,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6435,"slot":0,"token":"18e161bbfcce1558-11a68-59"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bbfcce1558-11a68-59","watchdog":false,"workers_joined":1,"writes":[{"at_us":6449,"bytes":57,"kind":"head","slot":0},{"at_us":6456,"bytes":1,"kind":"body","slot":0},{"at_us":311201,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":91,"abi_return_us":6219,"end_us":6224,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6268,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6264},{"abi_enter_us":6280,"abi_return_us":6285,"end_us":6285,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6280,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6311,"operation":"read","slot":0,"start_us":6309},{"abi_enter_us":6325,"abi_return_us":310947,"end_us":310953,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6325,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":311026,"operation":"read","slot":0,"start_us":311023},{"abi_enter_us":311045,"abi_return_us":500139,"end_us":500143,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":311045,"status":"FastlyStatus::OK","timeout_ms":188},{"end_us":500219,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500214}],"schema":2,"slots":[{"at_us":500219,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bbfcce1558-11a68-59","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":500587,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5302,"slot":0,"token":"18e161bc1acaeec8-11a68-5a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bc1acaeec8-11a68-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5319,"bytes":57,"kind":"head","slot":0},{"at_us":5330,"bytes":1,"kind":"body","slot":0},{"at_us":306598,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":79,"abi_return_us":5052,"end_us":5058,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5101,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5097},{"abi_enter_us":5115,"abi_return_us":5120,"end_us":5120,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5115,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5149,"operation":"read","slot":0,"start_us":5147},{"abi_enter_us":5162,"abi_return_us":306340,"end_us":306343,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5162,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":306424,"operation":"read","slot":0,"start_us":306421},{"abi_enter_us":306440,"abi_return_us":500500,"end_us":500504,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306440,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":500568,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500562}],"schema":2,"slots":[{"at_us":500568,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bc1acaeec8-11a68-5a","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501489,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5313,"slot":0,"token":"18e161bc38c83d68-11a68-5b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bc38c83d68-11a68-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5331,"bytes":57,"kind":"head","slot":0},{"at_us":5338,"bytes":1,"kind":"body","slot":0},{"at_us":309324,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":81,"abi_return_us":5086,"end_us":5091,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5135,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5131},{"abi_enter_us":5150,"abi_return_us":5154,"end_us":5155,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5150,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5187,"operation":"read","slot":0,"start_us":5185},{"abi_enter_us":5198,"abi_return_us":309091,"end_us":309095,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5198,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":309171,"operation":"read","slot":0,"start_us":309167},{"abi_enter_us":309188,"abi_return_us":501301,"end_us":501306,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309188,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501450,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501444}],"schema":2,"slots":[{"at_us":501450,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bc38c83d68-11a68-5b","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501009,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6480,"slot":0,"token":"18e161bc56dc82f0-11a68-5c"},{"path":"/readiness/siblings/1","received_us":6487,"slot":1,"token":"18e161bc56dc82f0-11a68-5c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161bc56dc82f0-11a68-5c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6496,"bytes":57,"kind":"head","slot":0},{"at_us":6504,"bytes":57,"kind":"head","slot":1},{"at_us":6511,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":91,"operation":"dispatch","outcome":"pending","slot":1,"start_us":71},{"abi_enter_us":109,"abi_return_us":6218,"end_us":6226,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":108,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6268,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6264},{"abi_enter_us":6279,"abi_return_us":6281,"end_us":6281,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6279,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6297,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6296},{"abi_enter_us":6306,"abi_return_us":6309,"end_us":6309,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6306,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6331,"operation":"read","slot":1,"start_us":6328},{"abi_enter_us":6341,"abi_return_us":6342,"end_us":6342,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6341,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6369,"operation":"read","slot":1,"start_us":6365},{"abi_enter_us":6379,"abi_return_us":6379,"end_us":6380,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6378,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6397,"operation":"read","slot":1,"start_us":6396},{"abi_enter_us":6405,"abi_return_us":6407,"end_us":6407,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6405,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6431,"operation":"read","slot":1,"start_us":6430},{"end_us":6442,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6439},{"abi_enter_us":6454,"abi_return_us":500918,"end_us":500924,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6454,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500990,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500984}],"schema":2,"slots":[{"at_us":500990,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6442,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bc56dc82f0-11a68-5c","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501740,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6456,"slot":0,"token":"18e161bc74f0c878-11a68-5d"},{"path":"/readiness/siblings/1","received_us":6460,"slot":1,"token":"18e161bc74f0c878-11a68-5d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161bc74f0c878-11a68-5d","watchdog":false,"workers_joined":2,"writes":[{"at_us":6472,"bytes":57,"kind":"head","slot":1},{"at_us":6481,"bytes":57,"kind":"head","slot":0},{"at_us":12742,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":91,"operation":"dispatch","outcome":"pending","slot":1,"start_us":63},{"abi_enter_us":110,"abi_return_us":6225,"end_us":6227,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6259,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6256},{"abi_enter_us":6276,"abi_return_us":6278,"end_us":6278,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6276,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6293,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6292},{"abi_enter_us":6302,"abi_return_us":12479,"end_us":12480,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6302,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":12532,"operation":"read","slot":1,"start_us":12530},{"abi_enter_us":12543,"abi_return_us":12546,"end_us":12546,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12543,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":16384,"end_us":12581,"operation":"read","slot":1,"start_us":12581},{"end_us":12595,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12593},{"abi_enter_us":12607,"abi_return_us":501656,"end_us":501660,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12607,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":501721,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501715}],"schema":2,"slots":[{"at_us":501721,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12595,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bc74f0c878-11a68-5d","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501116,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6448,"slot":1,"token":"18e161bc9309b950-11a68-5e"},{"path":"/readiness/siblings/0","received_us":6453,"slot":0,"token":"18e161bc9309b950-11a68-5e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161bc9309b950-11a68-5e","watchdog":false,"workers_joined":2,"writes":[{"at_us":6464,"bytes":57,"kind":"head","slot":1},{"at_us":6471,"bytes":57,"kind":"head","slot":0},{"at_us":11784,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":89,"operation":"dispatch","outcome":"pending","slot":1,"start_us":63},{"body_reads":0,"deadline_us":500000,"end_us":112,"operation":"handoff","start_us":112},{"abi_enter_us":125,"abi_return_us":6196,"end_us":6199,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":125,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6232,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6228},{"abi_enter_us":6243,"abi_return_us":6245,"end_us":6245,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6243,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6258,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6257},{"abi_enter_us":6266,"abi_return_us":11595,"end_us":11597,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6266,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":11655,"operation":"read","slot":1,"start_us":11653},{"abi_enter_us":11666,"abi_return_us":11673,"end_us":11673,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11666,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11692,"operation":"read","slot":1,"start_us":11691},{"abi_enter_us":11700,"abi_return_us":11704,"end_us":11704,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11700,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11720,"operation":"read","slot":1,"start_us":11719},{"abi_enter_us":11727,"abi_return_us":11728,"end_us":11729,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11726,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11748,"operation":"read","slot":1,"start_us":11748},{"end_us":11758,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11755},{"abi_enter_us":11773,"abi_return_us":501025,"end_us":501029,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11773,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501099,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501094}],"schema":2,"slots":[{"at_us":501099,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11758,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bc9309b950-11a68-5e","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501124,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5540,"slot":0,"token":"18e161bcb12c9150-11a68-5f"},{"path":"/readiness/siblings/1","received_us":5622,"slot":1,"token":"18e161bcb12c9150-11a68-5f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e161bcb12c9150-11a68-5f","watchdog":false,"workers_joined":2,"writes":[{"at_us":5556,"bytes":57,"kind":"head","slot":0},{"at_us":5638,"bytes":57,"kind":"head","slot":1},{"at_us":5646,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":128,"operation":"dispatch","outcome":"pending","slot":1,"start_us":95},{"body_reads":0,"deadline_us":500000,"end_us":161,"operation":"handoff","start_us":161},{"abi_enter_us":181,"abi_return_us":5214,"end_us":5218,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":181,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5255,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5251},{"abi_enter_us":5268,"abi_return_us":5277,"end_us":5278,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5268,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5304,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5301},{"abi_enter_us":5317,"abi_return_us":5322,"end_us":5322,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5317,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5353,"operation":"read","slot":1,"start_us":5351},{"abi_enter_us":5365,"abi_return_us":5369,"end_us":5369,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5365,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5390,"operation":"read","slot":1,"start_us":5390},{"end_us":5401,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5399},{"abi_enter_us":5413,"abi_return_us":501042,"end_us":501046,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5413,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501107,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501100}],"schema":2,"slots":[{"at_us":501107,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5401,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bcb12c9150-11a68-5f","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501801,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6171,"slot":0,"token":"18e161bccf4514c8-11a68-60"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bccf4514c8-11a68-60","watchdog":false,"workers_joined":1,"writes":[{"at_us":6186,"bytes":57,"kind":"head","slot":0},{"at_us":6193,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"abi_enter_us":110,"abi_return_us":5789,"end_us":5793,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5828,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5824},{"abi_enter_us":5844,"abi_return_us":5851,"end_us":5852,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5844,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5872,"operation":"read","slot":0,"start_us":5870},{"abi_enter_us":5882,"abi_return_us":501699,"end_us":501705,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5882,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501776,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501769}],"schema":2,"slots":[{"at_us":501776,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bccf4514c8-11a68-60","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":500823,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5755,"slot":0,"token":"18e161bced566480-11a68-61"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bced566480-11a68-61","watchdog":false,"workers_joined":1,"writes":[{"at_us":5767,"bytes":57,"kind":"head","slot":0},{"at_us":5772,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":77,"abi_return_us":5485,"end_us":5490,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5531,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5527},{"abi_enter_us":5544,"abi_return_us":5549,"end_us":5549,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5544,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5578,"operation":"read","slot":0,"start_us":5575},{"abi_enter_us":5590,"abi_return_us":500733,"end_us":500736,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5590,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500801,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500793}],"schema":2,"slots":[{"at_us":500801,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bced566480-11a68-61","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501682,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6406,"slot":0,"token":"18e161bd0b597b98-11a68-62"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bd0b597b98-11a68-62","watchdog":false,"workers_joined":1,"writes":[{"at_us":6418,"bytes":57,"kind":"head","slot":0},{"at_us":6424,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"abi_enter_us":82,"abi_return_us":6160,"end_us":6162,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6191,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6187},{"abi_enter_us":6203,"abi_return_us":6207,"end_us":6207,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6203,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6235,"operation":"read","slot":0,"start_us":6233},{"abi_enter_us":6246,"abi_return_us":501252,"end_us":501255,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6246,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501650,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501642}],"schema":2,"slots":[{"at_us":501650,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd0b597b98-11a68-62","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":500539,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5513,"slot":0,"token":"18e161bd296d3480-11a68-63"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bd296d3480-11a68-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":5535,"bytes":57,"kind":"head","slot":0},{"at_us":5542,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":99,"operation":"handoff","start_us":99},{"abi_enter_us":118,"abi_return_us":5254,"end_us":5259,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":118,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5313,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5309},{"abi_enter_us":5329,"abi_return_us":5334,"end_us":5335,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5329,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5368,"operation":"read","slot":0,"start_us":5366},{"abi_enter_us":5380,"abi_return_us":500439,"end_us":500443,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5380,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500515,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500510}],"schema":2,"slots":[{"at_us":500515,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd296d3480-11a68-63","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":6404,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6432,"slot":0,"token":"18e161bd476ba818-11a68-64"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd476ba818-11a68-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":6448,"bytes":57,"kind":"head","slot":0},{"at_us":6453,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":67,"abi_return_us":6189,"end_us":6192,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6238,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6234},{"abi_enter_us":6251,"abi_return_us":6257,"end_us":6257,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6251,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6284,"operation":"read","slot":0,"start_us":6282},{"abi_enter_us":6296,"abi_return_us":6296,"end_us":6297,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6295,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6317,"operation":"read","slot":0,"start_us":6316},{"abi_enter_us":6329,"abi_return_us":6329,"end_us":6330,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6329,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6348,"operation":"read","slot":0,"start_us":6348},{"abi_enter_us":6358,"abi_return_us":6360,"end_us":6360,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6358,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6378,"operation":"read","slot":0,"start_us":6378},{"end_us":6390,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6387}],"schema":2,"slots":[{"at_us":6390,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd476ba818-11a68-64","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":5110,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5182,"slot":0,"token":"18e161bd47f86f28-11a68-65"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd47f86f28-11a68-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":5196,"bytes":57,"kind":"head","slot":0},{"at_us":5200,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":54,"abi_return_us":4983,"end_us":4986,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5015,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5011},{"abi_enter_us":5025,"abi_return_us":5029,"end_us":5029,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5025,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5052,"operation":"read","slot":0,"start_us":5051},{"abi_enter_us":5062,"abi_return_us":5064,"end_us":5064,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5062,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5086,"operation":"read","slot":0,"start_us":5086},{"end_us":5097,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5095}],"schema":2,"slots":[{"at_us":5097,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd47f86f28-11a68-65","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":5303,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5221,"slot":0,"token":"18e161bd4869f1c0-11a68-66"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd4869f1c0-11a68-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":5238,"bytes":57,"kind":"head","slot":0},{"at_us":5244,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"abi_enter_us":81,"abi_return_us":5046,"end_us":5052,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5113,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5109},{"abi_enter_us":5130,"abi_return_us":5135,"end_us":5136,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5130,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5164,"operation":"read","slot":0,"start_us":5162},{"abi_enter_us":5177,"abi_return_us":5177,"end_us":5178,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5176,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5199,"operation":"read","slot":0,"start_us":5198},{"abi_enter_us":5225,"abi_return_us":5226,"end_us":5226,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5225,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5245,"operation":"read","slot":0,"start_us":5245},{"abi_enter_us":5254,"abi_return_us":5256,"end_us":5256,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5254,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5276,"operation":"read","slot":0,"start_us":5275},{"end_us":5287,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5284}],"schema":2,"slots":[{"at_us":5287,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd4869f1c0-11a68-66","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":5977,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6058,"slot":0,"token":"18e161bd4900ea30-11a68-67"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd4900ea30-11a68-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":6074,"bytes":57,"kind":"head","slot":0},{"at_us":6080,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":77,"abi_return_us":5848,"end_us":5851,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5882,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5879},{"abi_enter_us":5892,"abi_return_us":5896,"end_us":5896,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5892,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5921,"operation":"read","slot":0,"start_us":5920},{"abi_enter_us":5932,"abi_return_us":5933,"end_us":5933,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5931,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5953,"operation":"read","slot":0,"start_us":5953},{"end_us":5964,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5962}],"schema":2,"slots":[{"at_us":5964,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd4900ea30-11a68-67","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5912,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6078,"slot":0,"token":"18e161bd497ebd20-11a68-68"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd497ebd20-11a68-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":6094,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":52,"abi_return_us":5807,"end_us":5810,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5847,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5843},{"abi_enter_us":5861,"abi_return_us":5863,"end_us":5863,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5861,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5887,"operation":"read","slot":0,"start_us":5885},{"end_us":5900,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5897}],"schema":2,"slots":[{"at_us":5900,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd497ebd20-11a68-68","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5588,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5600,"slot":0,"token":"18e161bd49fa26e0-11a68-69"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd49fa26e0-11a68-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":5619,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":50,"abi_return_us":5436,"end_us":5439,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5490,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5486},{"abi_enter_us":5506,"abi_return_us":5508,"end_us":5509,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5506,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5558,"operation":"read","slot":0,"start_us":5556},{"end_us":5571,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5569}],"schema":2,"slots":[{"at_us":5571,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd49fa26e0-11a68-69","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5276,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5462,"slot":0,"token":"18e161bd4a6fb0b8-11a68-6a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd4a6fb0b8-11a68-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5476,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"abi_enter_us":73,"abi_return_us":5178,"end_us":5180,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5214,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5210},{"abi_enter_us":5225,"abi_return_us":5227,"end_us":5228,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5225,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5249,"operation":"read","slot":0,"start_us":5247},{"end_us":5261,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5259}],"schema":2,"slots":[{"at_us":5261,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd4a6fb0b8-11a68-6a","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6476,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6399,"slot":0,"token":"18e161bd4ae4e4a0-11a68-6b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd4ae4e4a0-11a68-6b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6419,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":50,"operation":"handoff","start_us":50},{"abi_enter_us":172,"abi_return_us":6330,"end_us":6335,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":172,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6393,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6388},{"abi_enter_us":6406,"abi_return_us":6409,"end_us":6409,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6406,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6447,"operation":"read","slot":0,"start_us":6445},{"end_us":6464,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6460}],"schema":2,"slots":[{"at_us":6464,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd4ae4e4a0-11a68-6b","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5629,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5732,"slot":0,"token":"18e161bd4b6a4528-11a68-6c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd4b6a4528-11a68-6c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5743,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":104,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":128,"abi_return_us":5526,"end_us":5546,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":128,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5593,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5587},{"end_us":5610,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5610}],"schema":2,"slots":[{"at_us":5610,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bd4b6a4528-11a68-6c","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":4898,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5231,"slot":0,"token":"18e161bd4be0f7e0-11a68-6d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd4be0f7e0-11a68-6d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5243,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":53,"abi_return_us":4851,"end_us":4854,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4874,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":4871},{"end_us":4886,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":4886}],"schema":2,"slots":[{"at_us":4886,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bd4be0f7e0-11a68-6d","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5962,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6434,"slot":0,"token":"18e161bd4c4db7b8-11a68-6e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd4c4db7b8-11a68-6e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6449,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":78,"abi_return_us":5910,"end_us":5914,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5940,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5936},{"end_us":5950,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5950}],"schema":2,"slots":[{"at_us":5950,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bd4c4db7b8-11a68-6e","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5434,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5465,"slot":0,"token":"18e161bd4ccb2900-11a68-6f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd4ccb2900-11a68-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5473,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"abi_enter_us":50,"abi_return_us":5380,"end_us":5382,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5407,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5404},{"end_us":5421,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5420}],"schema":2,"slots":[{"at_us":5421,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bd4ccb2900-11a68-6f","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56480,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5379,"slot":0,"token":"18e161bd4d3c3668-11a68-70"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd4d3c3668-11a68-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":5390,"bytes":66,"kind":"head","slot":0},{"at_us":5430,"bytes":6,"kind":"body","slot":0},{"at_us":56549,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":59,"abi_return_us":5253,"end_us":5257,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5296,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5292},{"abi_enter_us":5308,"abi_return_us":5319,"end_us":5319,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5308,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5398,"operation":"read","slot":0,"start_us":5396},{"abi_enter_us":5410,"abi_return_us":56379,"end_us":56382,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5410,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":56448,"operation":"read","slot":0,"start_us":56445},{"end_us":56464,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56460}],"schema":2,"slots":[{"at_us":56464,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd4d3c3668-11a68-70","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":61303,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6445,"slot":0,"token":"18e161bd50bffe28-11a68-71"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd50bffe28-11a68-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":6461,"bytes":66,"kind":"head","slot":0},{"at_us":6467,"bytes":6,"kind":"body","slot":0},{"at_us":61355,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":55,"abi_return_us":6264,"end_us":6270,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6322,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6317},{"abi_enter_us":6336,"abi_return_us":6340,"end_us":6341,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6335,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6372,"operation":"read","slot":0,"start_us":6370},{"abi_enter_us":6389,"abi_return_us":61185,"end_us":61189,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6389,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":61271,"operation":"read","slot":0,"start_us":61267},{"end_us":61288,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":61284}],"schema":2,"slots":[{"at_us":61288,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd50bffe28-11a68-71","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59316,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5882,"slot":0,"token":"18e161bd54939398-11a68-72"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd54939398-11a68-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":5901,"bytes":66,"kind":"head","slot":0},{"at_us":5911,"bytes":6,"kind":"body","slot":0},{"at_us":59480,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":77,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":131,"operation":"handoff","start_us":131},{"abi_enter_us":146,"abi_return_us":5648,"end_us":5655,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":145,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5705,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5699},{"abi_enter_us":5718,"abi_return_us":5725,"end_us":5725,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5718,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5749,"operation":"read","slot":0,"start_us":5747},{"abi_enter_us":5765,"abi_return_us":59209,"end_us":59212,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5765,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":59280,"operation":"read","slot":0,"start_us":59276},{"end_us":59298,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59294}],"schema":2,"slots":[{"at_us":59298,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd54939398-11a68-72","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59057,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5872,"slot":0,"token":"18e161bd584a5a08-11a68-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd584a5a08-11a68-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":5891,"bytes":66,"kind":"head","slot":0},{"at_us":5905,"bytes":6,"kind":"body","slot":0},{"at_us":59272,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":92,"abi_return_us":5549,"end_us":5553,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5587,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5583},{"abi_enter_us":5602,"abi_return_us":5610,"end_us":5610,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5602,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5657,"operation":"read","slot":0,"start_us":5655},{"abi_enter_us":5673,"abi_return_us":58933,"end_us":58940,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5673,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":59024,"operation":"read","slot":0,"start_us":59019},{"end_us":59041,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59036}],"schema":2,"slots":[{"at_us":59041,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd584a5a08-11a68-73","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":5607,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6033,"slot":0,"token":"18e161bd5bfd7310-11a68-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd5bfd7310-11a68-74","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":82,"abi_return_us":5534,"end_us":5540,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5579,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5575},{"end_us":5593,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5593}],"schema":2,"slots":[{"at_us":5593,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bd5bfd7310-11a68-74","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":6124,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6440,"slot":0,"token":"18e161bd5c801860-11a68-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd5c801860-11a68-75","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"abi_enter_us":74,"abi_return_us":6056,"end_us":6061,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6095,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6091},{"end_us":6108,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6108}],"schema":2,"slots":[{"at_us":6108,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bd5c801860-11a68-75","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":6281,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6416,"slot":0,"token":"18e161bd5cfd00f0-11a68-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd5cfd00f0-11a68-76","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"abi_enter_us":77,"abi_return_us":6227,"end_us":6229,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6253,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6250},{"end_us":6268,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6268}],"schema":2,"slots":[{"at_us":6268,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bd5cfd00f0-11a68-76","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":6256,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6350,"slot":0,"token":"18e161bd5d782c30-11a68-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd5d782c30-11a68-77","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":48,"operation":"handoff","start_us":48},{"abi_enter_us":58,"abi_return_us":6202,"end_us":6204,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6226,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6224},{"end_us":6238,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6238}],"schema":2,"slots":[{"at_us":6238,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bd5d782c30-11a68-77","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":58997,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5885,"slot":0,"token":"18e161bd5df2f1e0-11a68-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd5df2f1e0-11a68-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":5900,"bytes":57,"kind":"head","slot":0},{"at_us":5906,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":70,"abi_return_us":5697,"end_us":5701,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5734,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5730},{"bytes":1,"capacity":1,"end_us":5761,"operation":"read","slot":0,"start_us":5755},{"capacity":1,"end_us":58910,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5779},{"end_us":58975,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58967}],"schema":2,"slots":[{"at_us":58975,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd5df2f1e0-11a68-78","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":58174,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5317,"slot":0,"token":"18e161bd61973dd8-11a68-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd61973dd8-11a68-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":5337,"bytes":57,"kind":"head","slot":0},{"at_us":5343,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":77,"abi_return_us":5105,"end_us":5111,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5165,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5160},{"abi_enter_us":5178,"abi_return_us":5182,"end_us":5183,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5178,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5206,"operation":"read","slot":0,"start_us":5204},{"abi_enter_us":5221,"abi_return_us":58067,"end_us":58072,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5221,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58134,"operation":"read","slot":0,"start_us":58130},{"end_us":58156,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58147}],"schema":2,"slots":[{"at_us":58156,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd61973dd8-11a68-79","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":57372,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5250,"slot":0,"token":"18e161bd653c7818-11a68-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd653c7818-11a68-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5267,"bytes":57,"kind":"head","slot":0},{"at_us":5273,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":77,"abi_return_us":5001,"end_us":5005,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5047,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5042},{"abi_enter_us":5070,"abi_return_us":5076,"end_us":5076,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5070,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5104,"operation":"read","slot":0,"start_us":5102},{"abi_enter_us":5116,"abi_return_us":57262,"end_us":57266,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5115,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":57332,"operation":"read","slot":0,"start_us":57329},{"end_us":57354,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57350}],"schema":2,"slots":[{"at_us":57354,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd653c7818-11a68-7a","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":57518,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6433,"slot":0,"token":"18e161bd68ccc2f8-11a68-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd68ccc2f8-11a68-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6446,"bytes":57,"kind":"head","slot":0},{"at_us":6453,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"abi_enter_us":89,"abi_return_us":6159,"end_us":6167,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6201,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6196},{"abi_enter_us":6213,"abi_return_us":6218,"end_us":6218,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6213,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6245,"operation":"read","slot":0,"start_us":6243},{"abi_enter_us":6256,"abi_return_us":57420,"end_us":57425,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6256,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":57484,"operation":"read","slot":0,"start_us":57481},{"end_us":57502,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57496}],"schema":2,"slots":[{"at_us":57502,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd68ccc2f8-11a68-7b","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":57687,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6055,"slot":0,"token":"18e161bd6c648bd0-11a68-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bd6c648bd0-11a68-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6072,"bytes":57,"kind":"head","slot":0},{"at_us":6078,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":96,"abi_return_us":5789,"end_us":5795,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5832,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5828},{"abi_enter_us":5847,"abi_return_us":5852,"end_us":5852,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5847,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5881,"operation":"read","slot":0,"start_us":5879},{"abi_enter_us":5892,"abi_return_us":57593,"end_us":57596,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5892,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":57657,"operation":"read","slot":0,"start_us":57653},{"end_us":57674,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57667}],"schema":2,"slots":[{"at_us":57674,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bd6c648bd0-11a68-7c","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":500901,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5795,"slot":0,"token":"18e161bd6ffa1288-11a68-7d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bd6ffa1288-11a68-7d","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":68,"abi_return_us":500815,"end_us":500825,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500883,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500883}],"schema":2,"slots":[{"at_us":500883,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bd6ffa1288-11a68-7d","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501496,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6452,"slot":0,"token":"18e161bd8df35dd0-11a68-7e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bd8df35dd0-11a68-7e","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":92,"abi_return_us":501414,"end_us":501424,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501480,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501480}],"schema":2,"slots":[{"at_us":501480,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bd8df35dd0-11a68-7e","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501429,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6457,"slot":0,"token":"18e161bdabfac660-11a68-7f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bdabfac660-11a68-7f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":92,"operation":"handoff","start_us":92},{"abi_enter_us":116,"abi_return_us":501352,"end_us":501360,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501411,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501411}],"schema":2,"slots":[{"at_us":501411,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bdabfac660-11a68-7f","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":502155,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6451,"slot":0,"token":"18e161bdc9fd50d8-11a68-80"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bdc9fd50d8-11a68-80","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":83,"abi_return_us":499780,"end_us":499788,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"abi_enter_us":499847,"abi_return_us":502114,"end_us":502115,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499847,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502140,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502140}],"schema":2,"slots":[{"at_us":502140,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bdc9fd50d8-11a68-80","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501760,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5615,"slot":0,"token":"18e161bde80e69e0-11a68-81"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bde80e69e0-11a68-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":5634,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":71,"abi_return_us":5333,"end_us":5337,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5372,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5367},{"abi_enter_us":5383,"abi_return_us":501628,"end_us":501634,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5383,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501694,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501688}],"schema":2,"slots":[{"at_us":501694,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bde80e69e0-11a68-81","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501239,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5696,"slot":0,"token":"18e161be06198b90-11a68-82"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161be06198b90-11a68-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":5725,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":79,"abi_return_us":5599,"end_us":5606,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5665,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5660},{"abi_enter_us":5680,"abi_return_us":501128,"end_us":501132,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5680,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501195,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501190}],"schema":2,"slots":[{"at_us":501195,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161be06198b90-11a68-82","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":500454,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5300,"slot":0,"token":"18e161be241afcc8-11a68-83"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161be241afcc8-11a68-83","watchdog":false,"workers_joined":1,"writes":[{"at_us":5310,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":88,"abi_return_us":5010,"end_us":5012,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5054,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5050},{"abi_enter_us":5068,"abi_return_us":500350,"end_us":500354,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5068,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500425,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500419}],"schema":2,"slots":[{"at_us":500425,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161be241afcc8-11a68-83","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501539,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5473,"slot":0,"token":"18e161be4210d928-11a68-84"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161be4210d928-11a68-84","watchdog":false,"workers_joined":1,"writes":[{"at_us":5492,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":81,"abi_return_us":5214,"end_us":5216,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5250,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5246},{"abi_enter_us":5263,"abi_return_us":501432,"end_us":501439,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5263,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501502,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501497}],"schema":2,"slots":[{"at_us":501502,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161be4210d928-11a68-84","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501233,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6186,"slot":0,"token":"18e161be601a83d8-11a68-85"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161be601a83d8-11a68-85","watchdog":false,"workers_joined":1,"writes":[{"at_us":6200,"bytes":57,"kind":"head","slot":0},{"at_us":6209,"bytes":1,"kind":"body","slot":0},{"at_us":310031,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":50,"abi_return_us":5937,"end_us":5939,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5975,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5971},{"abi_enter_us":5988,"abi_return_us":5992,"end_us":5992,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5988,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":6025,"operation":"read","slot":0,"start_us":6023},{"abi_enter_us":6038,"abi_return_us":309848,"end_us":309857,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6038,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":310085,"operation":"read","slot":0,"start_us":310079},{"abi_enter_us":310106,"abi_return_us":501089,"end_us":501093,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310106,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":501172,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501157}],"schema":2,"slots":[{"at_us":501172,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161be601a83d8-11a68-85","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":500334,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5223,"slot":0,"token":"18e161be7e2b7da0-11a68-86"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161be7e2b7da0-11a68-86","watchdog":false,"workers_joined":1,"writes":[{"at_us":5232,"bytes":57,"kind":"head","slot":0},{"at_us":5237,"bytes":1,"kind":"body","slot":0},{"at_us":309540,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":88,"abi_return_us":4897,"end_us":4899,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4937,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4933},{"abi_enter_us":4952,"abi_return_us":4956,"end_us":4956,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4952,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4983,"operation":"read","slot":0,"start_us":4981},{"abi_enter_us":4995,"abi_return_us":309207,"end_us":309211,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4995,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":309302,"operation":"read","slot":0,"start_us":309299},{"abi_enter_us":309317,"abi_return_us":500206,"end_us":500212,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309317,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500299,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500289}],"schema":2,"slots":[{"at_us":500299,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161be7e2b7da0-11a68-86","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":500358,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6428,"slot":0,"token":"18e161be9c2b5c80-11a68-87"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161be9c2b5c80-11a68-87","watchdog":false,"workers_joined":1,"writes":[{"at_us":6439,"bytes":57,"kind":"head","slot":0},{"at_us":6445,"bytes":1,"kind":"body","slot":0},{"at_us":309306,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":102,"abi_return_us":6126,"end_us":6129,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6165,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6162},{"abi_enter_us":6180,"abi_return_us":6188,"end_us":6188,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6180,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6220,"operation":"read","slot":0,"start_us":6218},{"abi_enter_us":6231,"abi_return_us":309014,"end_us":309018,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6231,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":309101,"operation":"read","slot":0,"start_us":309098},{"abi_enter_us":309116,"abi_return_us":500271,"end_us":500274,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309116,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500339,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500334}],"schema":2,"slots":[{"at_us":500339,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161be9c2b5c80-11a68-87","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501453,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5173,"slot":0,"token":"18e161beba307350-11a68-88"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161beba307350-11a68-88","watchdog":false,"workers_joined":1,"writes":[{"at_us":5183,"bytes":57,"kind":"head","slot":0},{"at_us":5190,"bytes":1,"kind":"body","slot":0},{"at_us":308078,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":80,"operation":"handoff","start_us":80},{"abi_enter_us":96,"abi_return_us":4924,"end_us":4927,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4964,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4960},{"abi_enter_us":4979,"abi_return_us":4982,"end_us":4982,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4979,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5013,"operation":"read","slot":0,"start_us":5011},{"abi_enter_us":5026,"abi_return_us":307833,"end_us":307840,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5026,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":307928,"operation":"read","slot":0,"start_us":307925},{"abi_enter_us":307945,"abi_return_us":501337,"end_us":501341,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307945,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501416,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501410}],"schema":2,"slots":[{"at_us":501416,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161beba307350-11a68-88","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501444,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5282,"slot":0,"token":"18e161bed84299e0-11a68-89"},{"path":"/readiness/siblings/1","received_us":5297,"slot":1,"token":"18e161bed84299e0-11a68-89"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161bed84299e0-11a68-89","watchdog":false,"workers_joined":2,"writes":[{"at_us":5302,"bytes":57,"kind":"head","slot":0},{"at_us":5309,"bytes":57,"kind":"head","slot":1},{"at_us":5314,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":90,"operation":"dispatch","outcome":"pending","slot":1,"start_us":69},{"abi_enter_us":110,"abi_return_us":5035,"end_us":5039,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5076,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5073},{"abi_enter_us":5090,"abi_return_us":5092,"end_us":5093,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5090,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5109,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5109},{"abi_enter_us":5120,"abi_return_us":5127,"end_us":5128,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5120,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5157,"operation":"read","slot":1,"start_us":5155},{"abi_enter_us":5171,"abi_return_us":5172,"end_us":5172,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5171,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5192,"operation":"read","slot":1,"start_us":5191},{"abi_enter_us":5201,"abi_return_us":5201,"end_us":5201,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5201,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5219,"operation":"read","slot":1,"start_us":5218},{"abi_enter_us":5230,"abi_return_us":5233,"end_us":5233,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5230,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5261,"operation":"read","slot":1,"start_us":5260},{"end_us":5273,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5271},{"abi_enter_us":5284,"abi_return_us":501332,"end_us":501336,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5284,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501409,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501402}],"schema":2,"slots":[{"at_us":501409,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5273,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bed84299e0-11a68-89","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":500782,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5330,"slot":0,"token":"18e161bef664c600-11a68-8a"},{"path":"/readiness/siblings/1","received_us":5374,"slot":1,"token":"18e161bef664c600-11a68-8a"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161bef664c600-11a68-8a","watchdog":false,"workers_joined":2,"writes":[{"at_us":5341,"bytes":57,"kind":"head","slot":0},{"at_us":5382,"bytes":57,"kind":"head","slot":1},{"at_us":5387,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":112,"operation":"dispatch","outcome":"pending","slot":1,"start_us":83},{"abi_enter_us":149,"abi_return_us":5047,"end_us":5050,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":148,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5090,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5086},{"abi_enter_us":5105,"abi_return_us":5107,"end_us":5107,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5105,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5122,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5122},{"abi_enter_us":5133,"abi_return_us":5135,"end_us":5135,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5132,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5166,"operation":"read","slot":1,"start_us":5164},{"abi_enter_us":5177,"abi_return_us":5179,"end_us":5180,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5177,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5202,"operation":"read","slot":1,"start_us":5201},{"end_us":5214,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5210},{"abi_enter_us":5226,"abi_return_us":500668,"end_us":500672,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5226,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500743,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500737}],"schema":2,"slots":[{"at_us":500743,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5214,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bef664c600-11a68-8a","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":502413,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6469,"slot":1,"token":"18e161bf146f0908-11a68-8b"},{"path":"/readiness/siblings/0","received_us":6476,"slot":0,"token":"18e161bf146f0908-11a68-8b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161bf146f0908-11a68-8b","watchdog":false,"workers_joined":2,"writes":[{"at_us":6491,"bytes":57,"kind":"head","slot":1},{"at_us":6499,"bytes":57,"kind":"head","slot":0},{"at_us":12775,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":77,"operation":"dispatch","outcome":"pending","slot":1,"start_us":58},{"body_reads":0,"deadline_us":500000,"end_us":99,"operation":"handoff","start_us":99},{"abi_enter_us":112,"abi_return_us":6268,"end_us":6274,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6325,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6320},{"abi_enter_us":6339,"abi_return_us":6341,"end_us":6341,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6339,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6356,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6355},{"abi_enter_us":6366,"abi_return_us":12538,"end_us":12541,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6366,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":12593,"operation":"read","slot":1,"start_us":12589},{"abi_enter_us":12606,"abi_return_us":12608,"end_us":12609,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12606,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12632,"operation":"read","slot":1,"start_us":12631},{"abi_enter_us":12641,"abi_return_us":12642,"end_us":12642,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12641,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12662,"operation":"read","slot":1,"start_us":12662},{"abi_enter_us":12670,"abi_return_us":12672,"end_us":12672,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12670,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":1,"end_us":12696,"operation":"read","slot":1,"start_us":12695},{"end_us":12708,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12704},{"abi_enter_us":12721,"abi_return_us":502327,"end_us":502331,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12721,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":502393,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502387}],"schema":2,"slots":[{"at_us":502393,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12708,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bf146f0908-11a68-8b","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":500970,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6483,"slot":0,"token":"18e161bf32998de0-11a68-8c"},{"path":"/readiness/siblings/1","received_us":6489,"slot":1,"token":"18e161bf32998de0-11a68-8c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161bf32998de0-11a68-8c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6499,"bytes":57,"kind":"head","slot":1},{"at_us":6516,"bytes":57,"kind":"head","slot":0},{"at_us":11948,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":85,"operation":"dispatch","outcome":"pending","slot":1,"start_us":71},{"body_reads":0,"deadline_us":500000,"end_us":102,"operation":"handoff","start_us":102},{"abi_enter_us":116,"abi_return_us":6254,"end_us":6256,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6292,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6287},{"abi_enter_us":6303,"abi_return_us":6305,"end_us":6305,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6303,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6318,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6318},{"abi_enter_us":6328,"abi_return_us":11693,"end_us":11694,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6327,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":11736,"operation":"read","slot":1,"start_us":11734},{"abi_enter_us":11750,"abi_return_us":11753,"end_us":11754,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11750,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11775,"operation":"read","slot":1,"start_us":11774},{"end_us":11788,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11785},{"abi_enter_us":11800,"abi_return_us":500874,"end_us":500880,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11800,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500950,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500942}],"schema":2,"slots":[{"at_us":500950,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11788,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bf32998de0-11a68-8c","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501706,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5723,"slot":0,"token":"18e161bf50acae70-11a68-8d"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bf50acae70-11a68-8d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5740,"bytes":57,"kind":"head","slot":0},{"at_us":5746,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":158,"abi_return_us":5480,"end_us":5483,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":158,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5524,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5520},{"abi_enter_us":5539,"abi_return_us":5544,"end_us":5544,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5539,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5573,"operation":"read","slot":0,"start_us":5570},{"abi_enter_us":5586,"abi_return_us":501600,"end_us":501604,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5586,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501668,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501663}],"schema":2,"slots":[{"at_us":501668,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bf50acae70-11a68-8d","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501589,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6166,"slot":0,"token":"18e161bf6eba0e58-11a68-8e"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bf6eba0e58-11a68-8e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6181,"bytes":57,"kind":"head","slot":0},{"at_us":6186,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":67,"abi_return_us":5951,"end_us":5953,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5993,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5989},{"abi_enter_us":6007,"abi_return_us":6011,"end_us":6011,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6007,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6042,"operation":"read","slot":0,"start_us":6040},{"abi_enter_us":6058,"abi_return_us":501450,"end_us":501454,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6058,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501548,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501510}],"schema":2,"slots":[{"at_us":501548,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bf6eba0e58-11a68-8e","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501467,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5618,"slot":0,"token":"18e161bf8cc54f48-11a68-8f"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bf8cc54f48-11a68-8f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5630,"bytes":57,"kind":"head","slot":0},{"at_us":5635,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":70,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":187,"operation":"handoff","start_us":187},{"abi_enter_us":206,"abi_return_us":5347,"end_us":5350,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":206,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5388,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5384},{"abi_enter_us":5400,"abi_return_us":5404,"end_us":5405,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5400,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5436,"operation":"read","slot":0,"start_us":5433},{"abi_enter_us":5449,"abi_return_us":501378,"end_us":501382,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5449,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501445,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501439}],"schema":2,"slots":[{"at_us":501445,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bf8cc54f48-11a68-8f","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501304,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5209,"slot":0,"token":"18e161bfaad1e028-11a68-90"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161bfaad1e028-11a68-90","watchdog":false,"workers_joined":1,"writes":[{"at_us":5223,"bytes":57,"kind":"head","slot":0},{"at_us":5229,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"abi_enter_us":77,"abi_return_us":4913,"end_us":4916,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4958,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4954},{"abi_enter_us":4972,"abi_return_us":4980,"end_us":4980,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4972,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5011,"operation":"read","slot":0,"start_us":5008},{"abi_enter_us":5021,"abi_return_us":501125,"end_us":501131,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5021,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501270,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501264}],"schema":2,"slots":[{"at_us":501270,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfaad1e028-11a68-90","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":6397,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6415,"slot":0,"token":"18e161bfc8db9690-11a68-91"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfc8db9690-11a68-91","watchdog":false,"workers_joined":1,"writes":[{"at_us":6424,"bytes":57,"kind":"head","slot":0},{"at_us":6429,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":78,"abi_return_us":6221,"end_us":6222,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6248,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6244},{"abi_enter_us":6259,"abi_return_us":6263,"end_us":6263,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6259,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6288,"operation":"read","slot":0,"start_us":6286},{"abi_enter_us":6299,"abi_return_us":6299,"end_us":6300,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6298,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6318,"operation":"read","slot":0,"start_us":6318},{"abi_enter_us":6328,"abi_return_us":6328,"end_us":6328,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6327,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6348,"operation":"read","slot":0,"start_us":6347},{"abi_enter_us":6356,"abi_return_us":6358,"end_us":6359,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6356,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6376,"operation":"read","slot":0,"start_us":6375},{"end_us":6386,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6384}],"schema":2,"slots":[{"at_us":6386,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfc8db9690-11a68-91","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":6303,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6388,"slot":0,"token":"18e161bfc966d700-11a68-92"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfc966d700-11a68-92","watchdog":false,"workers_joined":1,"writes":[{"at_us":6395,"bytes":57,"kind":"head","slot":0},{"at_us":6399,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":51,"abi_return_us":6182,"end_us":6183,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6201,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6199},{"abi_enter_us":6211,"abi_return_us":6213,"end_us":6214,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6211,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6246,"operation":"read","slot":0,"start_us":6246},{"abi_enter_us":6256,"abi_return_us":6257,"end_us":6257,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6256,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6277,"operation":"read","slot":0,"start_us":6277},{"end_us":6290,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6289}],"schema":2,"slots":[{"at_us":6290,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfc966d700-11a68-92","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":6477,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6392,"slot":0,"token":"18e161bfc9e8a578-11a68-93"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfc9e8a578-11a68-93","watchdog":false,"workers_joined":1,"writes":[{"at_us":6400,"bytes":57,"kind":"head","slot":0},{"at_us":6406,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"abi_enter_us":69,"abi_return_us":6176,"end_us":6181,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6208,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6205},{"abi_enter_us":6221,"abi_return_us":6225,"end_us":6225,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6221,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6260,"operation":"read","slot":0,"start_us":6258},{"abi_enter_us":6271,"abi_return_us":6272,"end_us":6272,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6271,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6395,"operation":"read","slot":0,"start_us":6394},{"abi_enter_us":6405,"abi_return_us":6405,"end_us":6406,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6405,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6425,"operation":"read","slot":0,"start_us":6424},{"abi_enter_us":6434,"abi_return_us":6435,"end_us":6435,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6434,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6455,"operation":"read","slot":0,"start_us":6453},{"end_us":6465,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6463}],"schema":2,"slots":[{"at_us":6465,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfc9e8a578-11a68-93","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":6892,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6412,"slot":0,"token":"18e161bfca745730-11a68-94"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfca745730-11a68-94","watchdog":false,"workers_joined":1,"writes":[{"at_us":6424,"bytes":57,"kind":"head","slot":0},{"at_us":6428,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"abi_enter_us":64,"abi_return_us":6390,"end_us":6397,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6565,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6546},{"abi_enter_us":6623,"abi_return_us":6681,"end_us":6682,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6623,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6742,"operation":"read","slot":0,"start_us":6729},{"abi_enter_us":6755,"abi_return_us":6759,"end_us":6759,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6755,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6858,"operation":"read","slot":0,"start_us":6848},{"end_us":6878,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6874}],"schema":2,"slots":[{"at_us":6878,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfca745730-11a68-94","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5537,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5628,"slot":0,"token":"18e161bfcb0442f0-11a68-95"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfcb0442f0-11a68-95","watchdog":false,"workers_joined":1,"writes":[{"at_us":5639,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":61,"abi_return_us":5421,"end_us":5424,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5467,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5463},{"abi_enter_us":5481,"abi_return_us":5483,"end_us":5483,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5481,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5510,"operation":"read","slot":0,"start_us":5508},{"end_us":5524,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5522}],"schema":2,"slots":[{"at_us":5524,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfcb0442f0-11a68-95","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6180,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6006,"slot":0,"token":"18e161bfcb778aa8-11a68-96"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfcb778aa8-11a68-96","watchdog":false,"workers_joined":1,"writes":[{"at_us":6014,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":51,"abi_return_us":5927,"end_us":5928,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6078,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6075},{"abi_enter_us":6096,"abi_return_us":6105,"end_us":6126,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6096,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6156,"operation":"read","slot":0,"start_us":6155},{"end_us":6168,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6167}],"schema":2,"slots":[{"at_us":6168,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfcb778aa8-11a68-96","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6604,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6504,"slot":0,"token":"18e161bfcbf42ce8-11a68-97"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfcbf42ce8-11a68-97","watchdog":false,"workers_joined":1,"writes":[{"at_us":6512,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"abi_enter_us":62,"abi_return_us":6302,"end_us":6315,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6411,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6408},{"abi_enter_us":6485,"abi_return_us":6509,"end_us":6514,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6484,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6556,"operation":"read","slot":0,"start_us":6554},{"end_us":6570,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6566}],"schema":2,"slots":[{"at_us":6570,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfcbf42ce8-11a68-97","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6165,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6195,"slot":0,"token":"18e161bfcc776e78-11a68-98"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfcc776e78-11a68-98","watchdog":false,"workers_joined":1,"writes":[{"at_us":6207,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":46,"operation":"handoff","start_us":46},{"abi_enter_us":55,"abi_return_us":6052,"end_us":6054,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6093,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6089},{"abi_enter_us":6109,"abi_return_us":6111,"end_us":6112,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6109,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6139,"operation":"read","slot":0,"start_us":6138},{"end_us":6153,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6150}],"schema":2,"slots":[{"at_us":6153,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfcc776e78-11a68-98","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5355,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5500,"slot":0,"token":"18e161bfccf38fd0-11a68-99"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfccf38fd0-11a68-99","watchdog":false,"workers_joined":1,"writes":[{"at_us":5507,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":61,"abi_return_us":5312,"end_us":5312,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5331,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5329},{"end_us":5343,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5342}],"schema":2,"slots":[{"at_us":5343,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bfccf38fd0-11a68-99","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6361,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6424,"slot":0,"token":"18e161bfcd5e9e10-11a68-9a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfcd5e9e10-11a68-9a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6439,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":20,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"abi_enter_us":42,"abi_return_us":6304,"end_us":6308,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":42,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6333,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6329},{"end_us":6348,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6348}],"schema":2,"slots":[{"at_us":6348,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bfcd5e9e10-11a68-9a","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6270,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6408,"slot":0,"token":"18e161bfcdddcca8-11a68-9b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfcdddcca8-11a68-9b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6416,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":48,"operation":"handoff","start_us":48},{"abi_enter_us":67,"abi_return_us":6226,"end_us":6227,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6245,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6243},{"end_us":6258,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6258}],"schema":2,"slots":[{"at_us":6258,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bfcdddcca8-11a68-9b","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5762,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5838,"slot":0,"token":"18e161bfce5b0b28-11a68-9c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfce5b0b28-11a68-9c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5854,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":23,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":70,"abi_return_us":5701,"end_us":5704,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5737,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5727},{"end_us":5749,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5749}],"schema":2,"slots":[{"at_us":5749,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bfce5b0b28-11a68-9c","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58569,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5349,"slot":0,"token":"18e161bfced0a4a0-11a68-9d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfced0a4a0-11a68-9d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5367,"bytes":66,"kind":"head","slot":0},{"at_us":5371,"bytes":6,"kind":"body","slot":0},{"at_us":58288,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":58,"abi_return_us":5175,"end_us":5176,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5202,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5199},{"abi_enter_us":5212,"abi_return_us":5216,"end_us":5216,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5212,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5239,"operation":"read","slot":0,"start_us":5235},{"abi_enter_us":5250,"abi_return_us":58343,"end_us":58346,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5250,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58486,"operation":"read","slot":0,"start_us":58480},{"end_us":58542,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58537}],"schema":2,"slots":[{"at_us":58542,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfced0a4a0-11a68-9d","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57237,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5197,"slot":0,"token":"18e161bfd277c728-11a68-9e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfd277c728-11a68-9e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5213,"bytes":66,"kind":"head","slot":0},{"at_us":5218,"bytes":6,"kind":"body","slot":0},{"at_us":57403,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":60,"abi_return_us":4931,"end_us":4935,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4982,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4978},{"abi_enter_us":4994,"abi_return_us":5001,"end_us":5001,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4994,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5029,"operation":"read","slot":0,"start_us":5027},{"abi_enter_us":5041,"abi_return_us":57142,"end_us":57145,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5041,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":57206,"operation":"read","slot":0,"start_us":57202},{"end_us":57221,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57217}],"schema":2,"slots":[{"at_us":57221,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfd277c728-11a68-9e","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57907,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5214,"slot":0,"token":"18e161bfd6090050-11a68-9f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfd6090050-11a68-9f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5222,"bytes":66,"kind":"head","slot":0},{"at_us":5227,"bytes":6,"kind":"body","slot":0},{"at_us":57991,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"abi_enter_us":78,"abi_return_us":4972,"end_us":4974,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4999,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4996},{"abi_enter_us":5011,"abi_return_us":5014,"end_us":5015,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5011,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5040,"operation":"read","slot":0,"start_us":5038},{"abi_enter_us":5050,"abi_return_us":57798,"end_us":57802,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5050,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":57870,"operation":"read","slot":0,"start_us":57865},{"end_us":57886,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57882}],"schema":2,"slots":[{"at_us":57886,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfd6090050-11a68-9f","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58098,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5437,"slot":0,"token":"18e161bfd9a60118-11a68-a0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfd9a60118-11a68-a0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5454,"bytes":66,"kind":"head","slot":0},{"at_us":5460,"bytes":6,"kind":"body","slot":0},{"at_us":58238,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":88,"operation":"handoff","start_us":88},{"abi_enter_us":105,"abi_return_us":5185,"end_us":5190,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5231,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5226},{"abi_enter_us":5248,"abi_return_us":5255,"end_us":5255,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5247,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5285,"operation":"read","slot":0,"start_us":5283},{"abi_enter_us":5297,"abi_return_us":57987,"end_us":57991,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5296,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":58069,"operation":"read","slot":0,"start_us":58065},{"end_us":58085,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58081}],"schema":2,"slots":[{"at_us":58085,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfd9a60118-11a68-a0","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":4979,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5202,"slot":0,"token":"18e161bfdd4812c0-11a68-a1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfdd4812c0-11a68-a1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":78,"abi_return_us":4933,"end_us":4934,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4956,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":4953},{"end_us":4967,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":4967}],"schema":2,"slots":[{"at_us":4967,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bfdd4812c0-11a68-a1","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":5206,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5322,"slot":0,"token":"18e161bfddaefe68-11a68-a2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfddaefe68-11a68-a2","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":57,"abi_return_us":5156,"end_us":5158,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5181,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5177},{"end_us":5194,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5193}],"schema":2,"slots":[{"at_us":5194,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bfddaefe68-11a68-a2","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":5281,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5440,"slot":0,"token":"18e161bfde18ff20-11a68-a3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfde18ff20-11a68-a3","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":46,"operation":"handoff","start_us":46},{"abi_enter_us":64,"abi_return_us":5233,"end_us":5235,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5258,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5255},{"end_us":5269,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5269}],"schema":2,"slots":[{"at_us":5269,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bfde18ff20-11a68-a3","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":5119,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5239,"slot":0,"token":"18e161bfde847ac0-11a68-a4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfde847ac0-11a68-a4","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":49,"operation":"handoff","start_us":49},{"abi_enter_us":59,"abi_return_us":5072,"end_us":5073,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5092,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5090},{"end_us":5106,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5106}],"schema":2,"slots":[{"at_us":5106,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bfde847ac0-11a68-a4","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":56526,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5353,"slot":0,"token":"18e161bfdef12af8-11a68-a5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfdef12af8-11a68-a5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5368,"bytes":57,"kind":"head","slot":0},{"at_us":5374,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"abi_enter_us":41,"abi_return_us":5163,"end_us":5164,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5186,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5184},{"bytes":1,"capacity":1,"end_us":5207,"operation":"read","slot":0,"start_us":5203},{"capacity":1,"end_us":56452,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5224},{"end_us":56510,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56504}],"schema":2,"slots":[{"at_us":56510,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfdef12af8-11a68-a5","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":56994,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6415,"slot":0,"token":"18e161bfe2701888-11a68-a6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfe2701888-11a68-a6","watchdog":false,"workers_joined":1,"writes":[{"at_us":6428,"bytes":57,"kind":"head","slot":0},{"at_us":6433,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":74,"abi_return_us":6163,"end_us":6166,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6199,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6195},{"abi_enter_us":6215,"abi_return_us":6220,"end_us":6220,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6215,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6248,"operation":"read","slot":0,"start_us":6247},{"abi_enter_us":6259,"abi_return_us":56896,"end_us":56900,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6259,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":56958,"operation":"read","slot":0,"start_us":56954},{"end_us":56976,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56972}],"schema":2,"slots":[{"at_us":56976,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfe2701888-11a68-a6","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":58005,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6098,"slot":0,"token":"18e161bfe5fb5670-11a68-a7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfe5fb5670-11a68-a7","watchdog":false,"workers_joined":1,"writes":[{"at_us":6110,"bytes":57,"kind":"head","slot":0},{"at_us":6115,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":68,"abi_return_us":5841,"end_us":5844,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5880,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5876},{"abi_enter_us":5896,"abi_return_us":5900,"end_us":5901,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5896,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5926,"operation":"read","slot":0,"start_us":5925},{"abi_enter_us":5940,"abi_return_us":57901,"end_us":57905,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5940,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":57968,"operation":"read","slot":0,"start_us":57964},{"end_us":57985,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57982}],"schema":2,"slots":[{"at_us":57985,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfe5fb5670-11a68-a7","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":59017,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5659,"slot":0,"token":"18e161bfe9970b30-11a68-a8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfe9970b30-11a68-a8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5673,"bytes":57,"kind":"head","slot":0},{"at_us":5679,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":91,"abi_return_us":5400,"end_us":5403,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5437,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5433},{"abi_enter_us":5449,"abi_return_us":5455,"end_us":5455,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5449,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5481,"operation":"read","slot":0,"start_us":5479},{"abi_enter_us":5491,"abi_return_us":58876,"end_us":58885,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5491,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58979,"operation":"read","slot":0,"start_us":58974},{"end_us":58996,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58991}],"schema":2,"slots":[{"at_us":58996,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfe9970b30-11a68-a8","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"truncated","completed_us":58528,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5217,"slot":0,"token":"18e161bfed40f4a8-11a68-a9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161bfed40f4a8-11a68-a9","watchdog":false,"workers_joined":1,"writes":[{"at_us":5230,"bytes":57,"kind":"head","slot":0},{"at_us":5236,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":89,"operation":"handoff","start_us":89},{"abi_enter_us":112,"abi_return_us":4949,"end_us":4951,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4985,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4981},{"abi_enter_us":4997,"abi_return_us":5001,"end_us":5001,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4997,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5028,"operation":"read","slot":0,"start_us":5027},{"abi_enter_us":5039,"abi_return_us":58426,"end_us":58430,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5039,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":58495,"operation":"read","slot":0,"start_us":58491},{"end_us":58514,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58510}],"schema":2,"slots":[{"at_us":58514,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161bfed40f4a8-11a68-a9","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":502128,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5679,"slot":0,"token":"18e161bff0e45e10-11a68-aa"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161bff0e45e10-11a68-aa","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":76,"abi_return_us":501921,"end_us":501940,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502094,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502094}],"schema":2,"slots":[{"at_us":502094,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161bff0e45e10-11a68-aa","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501370,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5411,"slot":0,"token":"18e161c00ef0cbc8-11a68-ab"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161c00ef0cbc8-11a68-ab","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":66,"abi_return_us":501269,"end_us":501278,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501346,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501346}],"schema":2,"slots":[{"at_us":501346,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161c00ef0cbc8-11a68-ab","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":500231,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6524,"slot":0,"token":"18e161c02cf2e4f8-11a68-ac"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161c02cf2e4f8-11a68-ac","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":75,"operation":"handoff","start_us":75},{"abi_enter_us":91,"abi_return_us":500143,"end_us":500152,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500210,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500210}],"schema":2,"slots":[{"at_us":500210,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161c02cf2e4f8-11a68-ac","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"silent-head","completed_us":501907,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5602,"slot":0,"token":"18e161c04ae39cf0-11a68-ad"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161c04ae39cf0-11a68-ad","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"abi_enter_us":87,"abi_return_us":499520,"end_us":499526,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"abi_enter_us":499574,"abi_return_us":501848,"end_us":501850,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499574,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501889,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501889}],"schema":2,"slots":[{"at_us":501889,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161c04ae39cf0-11a68-ad","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":500641,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6637,"slot":0,"token":"18e161c068ebd0a0-11a68-ae"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161c068ebd0a0-11a68-ae","watchdog":false,"workers_joined":1,"writes":[{"at_us":6656,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":65,"abi_return_us":6599,"end_us":6603,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6661,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6640},{"abi_enter_us":6717,"abi_return_us":500547,"end_us":500551,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6716,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500621,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500615}],"schema":2,"slots":[{"at_us":500621,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c068ebd0a0-11a68-ae","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501385,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6470,"slot":0,"token":"18e161c086e5d380-11a68-af"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161c086e5d380-11a68-af","watchdog":false,"workers_joined":1,"writes":[{"at_us":6491,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":96,"abi_return_us":5970,"end_us":5974,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6032,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6027},{"abi_enter_us":6049,"abi_return_us":501065,"end_us":501075,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6049,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501313,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501224}],"schema":2,"slots":[{"at_us":501313,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c086e5d380-11a68-af","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":500785,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6273,"slot":0,"token":"18e161c0a5051970-11a68-b0"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161c0a5051970-11a68-b0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6292,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":146,"operation":"dispatch","outcome":"pending","slot":0,"start_us":38},{"body_reads":0,"deadline_us":500000,"end_us":209,"operation":"handoff","start_us":209},{"abi_enter_us":236,"abi_return_us":5952,"end_us":5959,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":235,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6015,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6011},{"abi_enter_us":6030,"abi_return_us":500698,"end_us":500702,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6030,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500766,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500760}],"schema":2,"slots":[{"at_us":500766,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c0a5051970-11a68-b0","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"pending-body","completed_us":501103,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5993,"slot":0,"token":"18e161c0c30703c0-11a68-b1"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161c0c30703c0-11a68-b1","watchdog":false,"workers_joined":1,"writes":[{"at_us":6011,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"abi_enter_us":94,"abi_return_us":5774,"end_us":5777,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5812,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5808},{"abi_enter_us":5825,"abi_return_us":500983,"end_us":500987,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5824,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501061,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501054}],"schema":2,"slots":[{"at_us":501061,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c0c30703c0-11a68-b1","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501548,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6495,"slot":0,"token":"18e161c0e10b3800-11a68-b2"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161c0e10b3800-11a68-b2","watchdog":false,"workers_joined":1,"writes":[{"at_us":6514,"bytes":57,"kind":"head","slot":0},{"at_us":6523,"bytes":1,"kind":"body","slot":0},{"at_us":307587,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":103,"abi_return_us":6166,"end_us":6172,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6221,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6216},{"abi_enter_us":6240,"abi_return_us":6251,"end_us":6251,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6240,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6277,"operation":"read","slot":0,"start_us":6274},{"abi_enter_us":6289,"abi_return_us":307269,"end_us":307281,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6289,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":307365,"operation":"read","slot":0,"start_us":307360},{"abi_enter_us":307380,"abi_return_us":501439,"end_us":501442,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307379,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501506,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501495}],"schema":2,"slots":[{"at_us":501506,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c0e10b3800-11a68-b2","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501703,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6411,"slot":0,"token":"18e161c0ff1df6e8-11a68-b3"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161c0ff1df6e8-11a68-b3","watchdog":false,"workers_joined":1,"writes":[{"at_us":6423,"bytes":57,"kind":"head","slot":0},{"at_us":6430,"bytes":1,"kind":"body","slot":0},{"at_us":310810,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":74,"abi_return_us":6086,"end_us":6088,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6119,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6115},{"abi_enter_us":6131,"abi_return_us":6135,"end_us":6135,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6131,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6163,"operation":"read","slot":0,"start_us":6161},{"abi_enter_us":6176,"abi_return_us":310468,"end_us":310481,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6176,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":310556,"operation":"read","slot":0,"start_us":310553},{"abi_enter_us":310569,"abi_return_us":501598,"end_us":501601,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310569,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":501683,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501669}],"schema":2,"slots":[{"at_us":501683,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c0ff1df6e8-11a68-b3","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501410,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5907,"slot":0,"token":"18e161c11d314658-11a68-b4"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161c11d314658-11a68-b4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5924,"bytes":57,"kind":"head","slot":0},{"at_us":5931,"bytes":1,"kind":"body","slot":0},{"at_us":308489,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"abi_enter_us":76,"abi_return_us":5679,"end_us":5683,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5733,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5729},{"abi_enter_us":5747,"abi_return_us":5753,"end_us":5753,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5747,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5777,"operation":"read","slot":0,"start_us":5775},{"abi_enter_us":5791,"abi_return_us":308235,"end_us":308242,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5791,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":308658,"operation":"read","slot":0,"start_us":308655},{"abi_enter_us":308674,"abi_return_us":501308,"end_us":501313,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308673,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501386,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501379}],"schema":2,"slots":[{"at_us":501386,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c11d314658-11a68-b4","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"trickle","completed_us":501635,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6423,"slot":0,"token":"18e161c13b42ad80-11a68-b5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161c13b42ad80-11a68-b5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6437,"bytes":57,"kind":"head","slot":0},{"at_us":6443,"bytes":1,"kind":"body","slot":0},{"at_us":311485,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"abi_enter_us":89,"abi_return_us":6073,"end_us":6075,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6106,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6102},{"abi_enter_us":6117,"abi_return_us":6122,"end_us":6122,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6117,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6148,"operation":"read","slot":0,"start_us":6146},{"abi_enter_us":6162,"abi_return_us":311134,"end_us":311138,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6162,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":311228,"operation":"read","slot":0,"start_us":311224},{"abi_enter_us":311242,"abi_return_us":500054,"end_us":500058,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":311242,"status":"FastlyStatus::OK","timeout_ms":188},{"end_us":501566,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501513}],"schema":2,"slots":[{"at_us":501566,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c13b42ad80-11a68-b5","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501997,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6968,"slot":1,"token":"18e161c159dde200-11a68-b6"},{"path":"/readiness/siblings/0","received_us":7008,"slot":0,"token":"18e161c159dde200-11a68-b6"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e161c159dde200-11a68-b6","watchdog":false,"workers_joined":2,"writes":[{"at_us":6998,"bytes":57,"kind":"head","slot":1},{"at_us":7017,"bytes":57,"kind":"head","slot":0},{"at_us":12137,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":178,"operation":"dispatch","outcome":"pending","slot":0,"start_us":47},{"end_us":291,"operation":"dispatch","outcome":"pending","slot":1,"start_us":242},{"abi_enter_us":308,"abi_return_us":6477,"end_us":6484,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":308,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6545,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6540},{"abi_enter_us":6560,"abi_return_us":6562,"end_us":6562,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6560,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6579,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6578},{"abi_enter_us":6589,"abi_return_us":11606,"end_us":11610,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6589,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":11682,"operation":"read","slot":1,"start_us":11679},{"abi_enter_us":11698,"abi_return_us":11702,"end_us":11702,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11698,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11725,"operation":"read","slot":1,"start_us":11724},{"abi_enter_us":11735,"abi_return_us":11736,"end_us":11736,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11735,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11755,"operation":"read","slot":1,"start_us":11755},{"abi_enter_us":11767,"abi_return_us":11769,"end_us":11769,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11767,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11791,"operation":"read","slot":1,"start_us":11791},{"end_us":11804,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11800},{"abi_enter_us":11819,"abi_return_us":501626,"end_us":501634,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11819,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501961,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501947}],"schema":2,"slots":[{"at_us":501961,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11804,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c159dde200-11a68-b6","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":500749,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6892,"slot":1,"token":"18e161c17d167818-11a68-b7"},{"path":"/readiness/siblings/0","received_us":7295,"slot":0,"token":"18e161c17d167818-11a68-b7"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161c17d167818-11a68-b7","watchdog":false,"workers_joined":2,"writes":[{"at_us":6929,"bytes":57,"kind":"head","slot":1},{"at_us":7309,"bytes":57,"kind":"head","slot":0},{"at_us":12797,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":112,"operation":"dispatch","outcome":"pending","slot":0,"start_us":52},{"end_us":198,"operation":"dispatch","outcome":"pending","slot":1,"start_us":155},{"abi_enter_us":218,"abi_return_us":6839,"end_us":6846,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":218,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6903,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6894},{"abi_enter_us":6920,"abi_return_us":6935,"end_us":6937,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6919,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":7012,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7009},{"abi_enter_us":7030,"abi_return_us":12397,"end_us":12401,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7030,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":3,"capacity":16384,"end_us":12450,"operation":"read","slot":1,"start_us":12447},{"abi_enter_us":12467,"abi_return_us":12473,"end_us":12473,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12467,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":16384,"end_us":12500,"operation":"read","slot":1,"start_us":12500},{"end_us":12516,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12512},{"abi_enter_us":12535,"abi_return_us":500654,"end_us":500662,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12535,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":500731,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500725}],"schema":2,"slots":[{"at_us":500731,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12516,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c17d167818-11a68-b7","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":501818,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6459,"slot":1,"token":"18e161c19b2a4c58-11a68-b8"},{"path":"/readiness/siblings/0","received_us":6464,"slot":0,"token":"18e161c19b2a4c58-11a68-b8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161c19b2a4c58-11a68-b8","watchdog":false,"workers_joined":2,"writes":[{"at_us":6472,"bytes":57,"kind":"head","slot":1},{"at_us":6479,"bytes":57,"kind":"head","slot":0},{"at_us":11517,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":98,"operation":"dispatch","outcome":"pending","slot":1,"start_us":71},{"body_reads":0,"deadline_us":500000,"end_us":118,"operation":"handoff","start_us":118},{"abi_enter_us":132,"abi_return_us":6217,"end_us":6221,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":132,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6257,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6253},{"abi_enter_us":6271,"abi_return_us":6273,"end_us":6273,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6271,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6290,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6289},{"abi_enter_us":6301,"abi_return_us":11314,"end_us":11318,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6301,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":11378,"operation":"read","slot":1,"start_us":11376},{"abi_enter_us":11390,"abi_return_us":11393,"end_us":11393,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11390,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11423,"operation":"read","slot":1,"start_us":11422},{"abi_enter_us":11438,"abi_return_us":11439,"end_us":11440,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11438,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11460,"operation":"read","slot":1,"start_us":11459},{"abi_enter_us":11467,"abi_return_us":11470,"end_us":11470,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11467,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11502,"operation":"read","slot":1,"start_us":11501},{"end_us":11515,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11510},{"abi_enter_us":11528,"abi_return_us":501660,"end_us":501674,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11528,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501745,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501737}],"schema":2,"slots":[{"at_us":501745,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11515,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c19b2a4c58-11a68-b8","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"siblings","completed_us":500813,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5585,"slot":0,"token":"18e161c1b9507018-11a68-b9"},{"path":"/readiness/siblings/1","received_us":5592,"slot":1,"token":"18e161c1b9507018-11a68-b9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e161c1b9507018-11a68-b9","watchdog":false,"workers_joined":2,"writes":[{"at_us":5602,"bytes":57,"kind":"head","slot":1},{"at_us":5610,"bytes":57,"kind":"head","slot":0},{"at_us":10636,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":101,"operation":"dispatch","outcome":"pending","slot":1,"start_us":80},{"body_reads":0,"deadline_us":500000,"end_us":122,"operation":"handoff","start_us":122},{"abi_enter_us":137,"abi_return_us":5322,"end_us":5327,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":136,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5373,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5368},{"abi_enter_us":5385,"abi_return_us":5387,"end_us":5387,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5385,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5402,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5402},{"abi_enter_us":5413,"abi_return_us":10368,"end_us":10371,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5412,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":10426,"operation":"read","slot":1,"start_us":10423},{"abi_enter_us":10440,"abi_return_us":10444,"end_us":10444,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10440,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":16384,"end_us":10471,"operation":"read","slot":1,"start_us":10470},{"end_us":10484,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10480},{"abi_enter_us":10498,"abi_return_us":500681,"end_us":500694,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":10498,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":500788,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500776}],"schema":2,"slots":[{"at_us":500788,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":10484,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c1b9507018-11a68-b9","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501853,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6423,"slot":0,"token":"18e161c1d763f638-11a68-ba"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161c1d763f638-11a68-ba","watchdog":false,"workers_joined":1,"writes":[{"at_us":6434,"bytes":57,"kind":"head","slot":0},{"at_us":6439,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":74,"abi_return_us":6117,"end_us":6120,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6156,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6152},{"abi_enter_us":6169,"abi_return_us":6173,"end_us":6173,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6168,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6196,"operation":"read","slot":0,"start_us":6194},{"abi_enter_us":6210,"abi_return_us":501743,"end_us":501747,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6210,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501829,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501820}],"schema":2,"slots":[{"at_us":501829,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c1d763f638-11a68-ba","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":501291,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5339,"slot":0,"token":"18e161c1f575d678-11a68-bb"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e161c1f575d678-11a68-bb","watchdog":false,"workers_joined":1,"writes":[{"at_us":5359,"bytes":57,"kind":"head","slot":0},{"at_us":5365,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":82,"abi_return_us":5058,"end_us":5063,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5117,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5113},{"abi_enter_us":5133,"abi_return_us":5137,"end_us":5137,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5133,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5172,"operation":"read","slot":0,"start_us":5170},{"abi_enter_us":5185,"abi_return_us":501148,"end_us":501152,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5185,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501249,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501240}],"schema":2,"slots":[{"at_us":501249,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c1f575d678-11a68-bb","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":500736,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5232,"slot":0,"token":"18e161c21388b0b8-11a68-bc"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161c21388b0b8-11a68-bc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5248,"bytes":57,"kind":"head","slot":0},{"at_us":5254,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":96,"operation":"handoff","start_us":96},{"abi_enter_us":111,"abi_return_us":4929,"end_us":4934,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4987,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4983},{"abi_enter_us":5001,"abi_return_us":5007,"end_us":5008,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5001,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5032,"operation":"read","slot":0,"start_us":5030},{"abi_enter_us":5045,"abi_return_us":500652,"end_us":500656,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5045,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500719,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500713}],"schema":2,"slots":[{"at_us":500719,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c21388b0b8-11a68-bc","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"exhausted","completed_us":500555,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6437,"slot":0,"token":"18e161c2318c19d8-11a68-bd"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e161c2318c19d8-11a68-bd","watchdog":false,"workers_joined":1,"writes":[{"at_us":6450,"bytes":57,"kind":"head","slot":0},{"at_us":6455,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"abi_enter_us":86,"abi_return_us":6180,"end_us":6183,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6222,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6218},{"abi_enter_us":6234,"abi_return_us":6238,"end_us":6238,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6234,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6273,"operation":"read","slot":0,"start_us":6265},{"abi_enter_us":6292,"abi_return_us":500462,"end_us":500468,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6292,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500536,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500530}],"schema":2,"slots":[{"at_us":500536,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c2318c19d8-11a68-bd","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":6451,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6452,"slot":0,"token":"18e161c24f8f6b88-11a68-be"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c24f8f6b88-11a68-be","watchdog":false,"workers_joined":1,"writes":[{"at_us":6474,"bytes":57,"kind":"head","slot":0},{"at_us":6480,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":112,"operation":"dispatch","outcome":"pending","slot":0,"start_us":31},{"abi_enter_us":140,"abi_return_us":6198,"end_us":6204,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":140,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6248,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6243},{"abi_enter_us":6263,"abi_return_us":6270,"end_us":6270,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6263,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6295,"operation":"read","slot":0,"start_us":6293},{"abi_enter_us":6307,"abi_return_us":6308,"end_us":6308,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6307,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6334,"operation":"read","slot":0,"start_us":6332},{"abi_enter_us":6344,"abi_return_us":6345,"end_us":6345,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6344,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6362,"operation":"read","slot":0,"start_us":6361},{"abi_enter_us":6370,"abi_return_us":6372,"end_us":6373,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6370,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6389,"operation":"read","slot":0,"start_us":6389},{"end_us":6405,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6401}],"schema":2,"slots":[{"at_us":6405,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c24f8f6b88-11a68-be","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":6096,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6086,"slot":0,"token":"18e161c25044b1c8-11a68-bf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c25044b1c8-11a68-bf","watchdog":false,"workers_joined":1,"writes":[{"at_us":6103,"bytes":57,"kind":"head","slot":0},{"at_us":6108,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":73,"operation":"dispatch","outcome":"pending","slot":0,"start_us":31},{"abi_enter_us":132,"abi_return_us":5863,"end_us":5870,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":131,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5925,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5921},{"abi_enter_us":5939,"abi_return_us":5947,"end_us":5947,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5938,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5975,"operation":"read","slot":0,"start_us":5972},{"abi_enter_us":5987,"abi_return_us":5989,"end_us":5989,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5987,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":6060,"operation":"read","slot":0,"start_us":6059},{"end_us":6080,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6073}],"schema":2,"slots":[{"at_us":6080,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c25044b1c8-11a68-bf","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":5655,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5260,"slot":0,"token":"18e161c250cd3700-11a68-c0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c250cd3700-11a68-c0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5274,"bytes":57,"kind":"head","slot":0},{"at_us":5281,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":34},{"body_reads":0,"deadline_us":500000,"end_us":89,"operation":"handoff","start_us":89},{"abi_enter_us":99,"abi_return_us":5097,"end_us":5103,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":99,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5248,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5239},{"abi_enter_us":5317,"abi_return_us":5324,"end_us":5325,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5317,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5362,"operation":"read","slot":0,"start_us":5361},{"abi_enter_us":5374,"abi_return_us":5375,"end_us":5375,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5374,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5398,"operation":"read","slot":0,"start_us":5397},{"abi_enter_us":5407,"abi_return_us":5407,"end_us":5408,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5407,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5473,"operation":"read","slot":0,"start_us":5472},{"abi_enter_us":5490,"abi_return_us":5493,"end_us":5494,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5490,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5612,"operation":"read","slot":0,"start_us":5610},{"end_us":5631,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5628}],"schema":2,"slots":[{"at_us":5631,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c250cd3700-11a68-c0","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"eof","completed_us":5333,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6792,"slot":0,"token":"18e161c251775d98-11a68-c1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c251775d98-11a68-c1","watchdog":false,"workers_joined":1,"writes":[{"at_us":6821,"bytes":57,"kind":"head","slot":0},{"at_us":6853,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":98,"operation":"handoff","start_us":98},{"abi_enter_us":112,"abi_return_us":5165,"end_us":5175,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5227,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5222},{"abi_enter_us":5239,"abi_return_us":5246,"end_us":5246,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5238,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5272,"operation":"read","slot":0,"start_us":5270},{"abi_enter_us":5286,"abi_return_us":5288,"end_us":5289,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5286,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5309,"operation":"read","slot":0,"start_us":5309},{"end_us":5322,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5318}],"schema":2,"slots":[{"at_us":5322,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c251775d98-11a68-c1","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5882,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6044,"slot":0,"token":"18e161c252062460-11a68-c2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c252062460-11a68-c2","watchdog":false,"workers_joined":1,"writes":[{"at_us":6062,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":70,"abi_return_us":5757,"end_us":5763,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5812,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5808},{"abi_enter_us":5825,"abi_return_us":5828,"end_us":5829,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5825,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5852,"operation":"read","slot":0,"start_us":5850},{"end_us":5869,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5863}],"schema":2,"slots":[{"at_us":5869,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c252062460-11a68-c2","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5493,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5532,"slot":0,"token":"18e161c252874ae0-11a68-c3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c252874ae0-11a68-c3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5555,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":70,"abi_return_us":5362,"end_us":5371,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5422,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5417},{"abi_enter_us":5436,"abi_return_us":5439,"end_us":5439,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5436,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5467,"operation":"read","slot":0,"start_us":5464},{"end_us":5481,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5478}],"schema":2,"slots":[{"at_us":5481,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c252874ae0-11a68-c3","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6322,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6450,"slot":0,"token":"18e161c25314c1b8-11a68-c4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c25314c1b8-11a68-c4","watchdog":false,"workers_joined":1,"writes":[{"at_us":6468,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":75,"operation":"handoff","start_us":75},{"abi_enter_us":85,"abi_return_us":6196,"end_us":6202,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6250,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6246},{"abi_enter_us":6262,"abi_return_us":6267,"end_us":6268,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6262,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6291,"operation":"read","slot":0,"start_us":6289},{"end_us":6309,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6306}],"schema":2,"slots":[{"at_us":6309,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c25314c1b8-11a68-c4","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6006,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6022,"slot":0,"token":"18e161c253937f08-11a68-c5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c253937f08-11a68-c5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6054,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"abi_enter_us":77,"abi_return_us":5896,"end_us":5900,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5939,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5935},{"abi_enter_us":5954,"abi_return_us":5957,"end_us":5957,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5954,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5982,"operation":"read","slot":0,"start_us":5981},{"end_us":5995,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5992}],"schema":2,"slots":[{"at_us":5995,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c253937f08-11a68-c5","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6336,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6448,"slot":0,"token":"18e161c2540e44b8-11a68-c6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c2540e44b8-11a68-c6","watchdog":false,"workers_joined":1,"writes":[{"at_us":6466,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":72,"abi_return_us":6274,"end_us":6278,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6307,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6304},{"end_us":6320,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6320}],"schema":2,"slots":[{"at_us":6320,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161c2540e44b8-11a68-c6","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5615,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5655,"slot":0,"token":"18e161c2548ad370-11a68-c7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c2548ad370-11a68-c7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5669,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":50,"abi_return_us":5538,"end_us":5545,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5583,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5579},{"end_us":5598,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5598}],"schema":2,"slots":[{"at_us":5598,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161c2548ad370-11a68-c7","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5089,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5248,"slot":0,"token":"18e161c254fe78e8-11a68-c8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c254fe78e8-11a68-c8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5264,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":85,"abi_return_us":5020,"end_us":5024,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5055,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5051},{"end_us":5069,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5069}],"schema":2,"slots":[{"at_us":5069,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161c254fe78e8-11a68-c8","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5058,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5359,"slot":0,"token":"18e161c2556b38c0-11a68-c9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c2556b38c0-11a68-c9","watchdog":false,"workers_joined":1,"writes":[{"at_us":5372,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":74,"abi_return_us":4990,"end_us":4995,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5029,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5026},{"end_us":5044,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5044}],"schema":2,"slots":[{"at_us":5044,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161c2556b38c0-11a68-c9","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57376,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5232,"slot":0,"token":"18e161c255d8cf70-11a68-ca"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c255d8cf70-11a68-ca","watchdog":false,"workers_joined":1,"writes":[{"at_us":5264,"bytes":66,"kind":"head","slot":0},{"at_us":5270,"bytes":6,"kind":"body","slot":0},{"at_us":55348,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":54,"abi_return_us":5060,"end_us":5064,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5105,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5101},{"abi_enter_us":5117,"abi_return_us":5122,"end_us":5122,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5117,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5145,"operation":"read","slot":0,"start_us":5143},{"abi_enter_us":5158,"abi_return_us":55146,"end_us":55153,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5158,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":57327,"operation":"read","slot":0,"start_us":57323},{"end_us":57346,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57341}],"schema":2,"slots":[{"at_us":57346,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c255d8cf70-11a68-ca","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58214,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5407,"slot":0,"token":"18e161c2596d2190-11a68-cb"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c2596d2190-11a68-cb","watchdog":false,"workers_joined":1,"writes":[{"at_us":5421,"bytes":66,"kind":"head","slot":0},{"at_us":5427,"bytes":6,"kind":"body","slot":0},{"at_us":58381,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":102,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":125,"abi_return_us":5125,"end_us":5132,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":125,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5166,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5161},{"abi_enter_us":5177,"abi_return_us":5181,"end_us":5182,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5177,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5218,"operation":"read","slot":0,"start_us":5216},{"abi_enter_us":5229,"abi_return_us":58103,"end_us":58109,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5229,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":58181,"operation":"read","slot":0,"start_us":58177},{"end_us":58199,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58194}],"schema":2,"slots":[{"at_us":58199,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c2596d2190-11a68-cb","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":55645,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5483,"slot":0,"token":"18e161c25d0937f8-11a68-cc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c25d0937f8-11a68-cc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5497,"bytes":66,"kind":"head","slot":0},{"at_us":5504,"bytes":6,"kind":"body","slot":0},{"at_us":55565,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":98,"abi_return_us":5259,"end_us":5273,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5346,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5340},{"abi_enter_us":5374,"abi_return_us":5383,"end_us":5384,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5374,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5413,"operation":"read","slot":0,"start_us":5410},{"abi_enter_us":5427,"abi_return_us":55400,"end_us":55410,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5427,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":55544,"operation":"read","slot":0,"start_us":55538},{"end_us":55612,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55604}],"schema":2,"slots":[{"at_us":55612,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c25d0937f8-11a68-cc","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58525,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6496,"slot":0,"token":"18e161c2608435b8-11a68-cd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c2608435b8-11a68-cd","watchdog":false,"workers_joined":1,"writes":[{"at_us":6510,"bytes":66,"kind":"head","slot":0},{"at_us":6515,"bytes":6,"kind":"body","slot":0},{"at_us":58812,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":80,"abi_return_us":6105,"end_us":6109,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6156,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6152},{"abi_enter_us":6170,"abi_return_us":6174,"end_us":6174,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6170,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6211,"operation":"read","slot":0,"start_us":6206},{"abi_enter_us":6222,"abi_return_us":58408,"end_us":58415,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6222,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":58488,"operation":"read","slot":0,"start_us":58485},{"end_us":58507,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58503}],"schema":2,"slots":[{"at_us":58507,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e161c2608435b8-11a68-cd","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":4998,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5241,"slot":0,"token":"18e161c2642a9cc0-11a68-ce"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c2642a9cc0-11a68-ce","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":79,"abi_return_us":4932,"end_us":4936,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4969,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":4965},{"end_us":4984,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":4984}],"schema":2,"slots":[{"at_us":4984,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161c2642a9cc0-11a68-ce","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":6317,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6450,"slot":0,"token":"18e161c26499ba10-11a68-cf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c26499ba10-11a68-cf","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":72,"abi_return_us":6255,"end_us":6259,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6290,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6286},{"end_us":6303,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6303}],"schema":2,"slots":[{"at_us":6303,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161c26499ba10-11a68-cf","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":5179,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5308,"slot":0,"token":"18e161c26517b410-11a68-d0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c26517b410-11a68-d0","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":74,"abi_return_us":5118,"end_us":5121,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5148,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5144},{"end_us":5160,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5160}],"schema":2,"slots":[{"at_us":5160,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161c26517b410-11a68-d0","tolerance_us":5000}} +{"run":"stock-reviewed","kind":"body-probe","probe":{"case":"head-error","completed_us":5901,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6451,"slot":0,"token":"18e161c26584a6b0-11a68-d1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e161c26584a6b0-11a68-d1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"77819f712ad971ada5494c563dd86a3b64a2afa6","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"abi_enter_us":93,"abi_return_us":5830,"end_us":5836,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5872,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5868},{"end_us":5886,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5886}],"schema":2,"slots":[{"at_us":5886,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e161c26584a6b0-11a68-d1","tolerance_us":5000}} diff --git a/docs/audits/evidence/2026-10-23-fastly-timer-controls.json b/docs/audits/evidence/2026-10-23-fastly-timer-controls.json new file mode 100644 index 000000000..55a06249d --- /dev/null +++ b/docs/audits/evidence/2026-10-23-fastly-timer-controls.json @@ -0,0 +1,574 @@ +{ + "schema": 1, + "execution_date": "2026-10-23", + "timezone": "America/Los_Angeles", + "status": "Non-body guest timer lateness retained; original timing cause and guarantee remain open", + "qualification": false, + "edgezero_base_revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "source_state": "working-tree characterization; not a frozen qualification record", + "command": "VICEROY_BIN= scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-readiness --output ", + "controls": { + "sources": ["native-sleep", "empty-select", "wasi-sleep"], + "budgets_ms": [1, 100, 500], + "repetitions": 8, + "observations_per_run": 72, + "excluded_warmups_per_run": 3, + "warmup_ms": 1, + "new_outbound_requests": 0, + "native_controls": "independent matched observations in each runtime run, not shared samples", + "late_observations": "retained with within_five_ms=false; not a passing G2 assertion", + "raw_intervals": "Reviewed timer controls end at captured primitive return, before formatting; the G2 operation and whole-exchange intervals still include all guest overhead.", + "comparison_phases": ["before-review", "reviewed"], + "runs": 4, + "total_observations": 288 + }, + "unchanged_gate": { + "body_probes_per_run": 135, + "origin_requests_per_run": 147, + "absolute_deadline_us": 500000, + "maximum_completion_us": 505000, + "historical_failure_record": "2026-10-23-fastly-readiness-matrix.json", + "historical_observations_rewritten": false, + "overall_G2": "Failed; later passing samples do not establish a bound or erase failures" + }, + "runtime": { + "sdk": "fastly 0.13.1", + "runtime_version": "Viceroy 0.21.0; repaired executable is distinct from stock", + "upstream_revision": "a69d982d1d95e13c0fc9f92683b9118639bb4b04", + "stock_binary_sha256": "751ff2c1a83063b109bc212b95be5f52e5ab6284b9027fa03aa229087eba3f30", + "repaired_binary_sha256": "98ea0536478ce2e641f093b2619b7ac7c6c237402f140e5792d854575abc7100", + "build_details": "2026-10-23-viceroy-readiness-repair.json", + "tokio": "1.53.1", + "wasmtime_wasi": "39.0.2", + "runtime_code_changed": false, + "pins_changed": false, + "installed_runtime_replaced": false + }, + "source_review": [ + "Viceroy constructs a relative Tokio timeout after decoding the guest handle list; this is not an absolute guest deadline.", + "Tokio rounds timer deadlines and park calculations at millisecond granularity; source arithmetic does not explain the entire observed 7.297 ms overrun.", + "Tokio polls readiness before its timeout; guest code still arbitrates expiry before consuming a late result.", + "Guest WASI and Tokio elapsed clocks are host-monotonic based; source inspection supplies no clock-scale mismatch explanation.", + "Guest WASI sleep uses the shared async timer machinery here; it is not an independent native-sleep control.", + "Source exposes scheduling/driver/resumption opportunities but no deterministic +5 ms resume bound; actual cause of the earlier failure is not isolated." + ], + "interpretation": [ + "Each stock/repaired comparison uses identical guest bytes; before-review and reviewed pairs have distinct byte identities. Requests are sequential without concurrent builds.", + "Native sleep lateness shows the observation window can be exceeded without HTTP bodies or the Viceroy select path; it does not identify the cause of the previous select failure.", + "No guest control exceeded +5 ms in the first pair. Reviewed stock WASI sleep requested 100 ms and returned at 106.952 ms, 6952 us after the request duration and 1952 us beyond its +5 ms window. Eight repetitions do not establish a tail guarantee.", + "This repaired HTTP matrix met the window once and exits 2 unverified; the earlier repaired timing failures remain retained and replayed.", + "Stock still loses all 24 selected truncation/malformed-body errors and exits 1.", + "No margin, shorter budget, blocking-body fallback, source substitution, backend policy approval, capability promotion or production migration is introduced.", + "Full event/log files and binaries are identified by hash, not retained release assets. Raw controls and body probes are retained separately.", + "Dependent Fastly engine work remains gated. Approved portable work may proceed independently; B/H(K), exact-version review, candidate freeze and hosted qualification remain open.", + "Earlier failed selection intervals included status formatting and lack raw ABI timestamps; new measurements cannot retroactively attribute their delay.", + "Review corrected the timer-only endpoint (4330 us versus raw return at 4325 us in the red control), and validation now rejects ABI waits shorter than their positive timeout. Both measurement phases are retained, not overwritten." + ], + "source_files": [ + { + "path": "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs", + "sha256": "47d3f8be81c1531ad732d12ac1d645d70deba7328a0d8ded5be12bc0b7e752eb" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs", + "sha256": "bf497a757f36427120e7deab1e1e70e415977ce88b73b138e750df8032e5b4ca" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence/readiness.rs", + "sha256": "5842b53b024b383ebdc6f9356e6e1cc58d6dbb3f1f08ae133f15698967f55687" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence/timers.rs", + "sha256": "07a4c42569e1403da5847d02b3b2a33380253d54f83ffd595f746ba30d7d8748" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/readiness.rs", + "sha256": "6010f19a22282cca84b675b3d3d271e3006d27a1a463c3162910d7baef73e7f8" + }, + { + "path": "tests/fixtures/reusable-app/Cargo.lock", + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + { + "path": "patches/viceroy/0.21.0-body-readiness.patch", + "sha256": "8693ce34f82cc98dc272bf39379007b17a40dfc19138c55b445378fd9a163759" + } + ], + "runs": [ + { + "id": "repaired", + "exit_code": 2, + "directory": "/private/tmp/edgezero-fastly-timer-controls-repaired-20261023-01", + "events_sha256": "2455df72b9a49ffa7c4c9fac05402816068fa755d5013fed1775f30b8396cb70", + "log_sha256": "7174987ca8cea9c4b6d4519636173e62b855d9df0f139724dad94785ab8f3021", + "guest_sha256": "b8935d601fade70fb030047fdf2b8c2211cbd483c45820f1db6d5dd97fe5b14b", + "controls": 72, + "warmups": [ + { + "budget_ms": 1, + "elapsed_us": 1007, + "event": "readiness-timer-warmup", + "overrun_us": 7, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "source": "native-sleep", + "token": "18e160dcd62fde48-5dc4-0", + "within_five_ms": true + }, + { + "abi_enter_us": 2, + "abi_return_us": 2669, + "abi_status": "FastlyStatus::OK", + "budget_ms": 1, + "elapsed_us": 2671, + "event": "readiness-timer-warmup", + "overrun_us": 1671, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "selected": null, + "source": "empty-select", + "source_state": "working-tree-characterization", + "token": "18e160dcd6426c48-5dc4-1", + "within_five_ms": true + }, + { + "budget_ms": 1, + "elapsed_us": 2916, + "event": "readiness-timer-warmup", + "overrun_us": 1916, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "source": "wasi-sleep", + "source_state": "working-tree-characterization", + "token": "18e160dcd7087b68-5dc4-2", + "within_five_ms": true + } + ], + "summaries": [ + { + "source": "native-sleep", + "count": 24, + "late": 5, + "max_overrun_us": 5015, + "max_raw_select_overrun_us": null + }, + { + "source": "empty-select", + "count": 24, + "late": 0, + "max_overrun_us": 2229, + "max_raw_select_overrun_us": 2219 + }, + { + "source": "wasi-sleep", + "count": 24, + "late": 0, + "max_overrun_us": 2242, + "max_raw_select_overrun_us": null + } + ], + "body_probes": 135, + "origin_requests": 147, + "body_max_completion_us": 502235, + "body_late": 0, + "body_source_error_losses": 0, + "validation_failures": [], + "local_matrix_pass": true, + "qualification": false, + "phase": "before-review" + }, + { + "id": "stock", + "exit_code": 1, + "directory": "/private/tmp/edgezero-fastly-timer-controls-stock-20261023-01", + "events_sha256": "865ac2d7791d355410d073e97b45418b34dd8b9471c868a0e0f301354d4903b7", + "log_sha256": "8aebcff3fb17bc5880ee9afa02a2ccc92f5dd065d01288d19c941d591d014087", + "guest_sha256": "b8935d601fade70fb030047fdf2b8c2211cbd483c45820f1db6d5dd97fe5b14b", + "controls": 72, + "warmups": [ + { + "budget_ms": 1, + "elapsed_us": 1008, + "event": "readiness-timer-warmup", + "overrun_us": 8, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "source": "native-sleep", + "token": "18e160edac7524d0-6a3d-0", + "within_five_ms": true + }, + { + "abi_enter_us": 0, + "abi_return_us": 5016, + "abi_status": "FastlyStatus::OK", + "budget_ms": 1, + "elapsed_us": 5022, + "event": "readiness-timer-warmup", + "overrun_us": 4022, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "selected": null, + "source": "empty-select", + "source_state": "working-tree-characterization", + "token": "18e160edac884358-6a3d-1", + "within_five_ms": true + }, + { + "budget_ms": 1, + "elapsed_us": 4247, + "event": "readiness-timer-warmup", + "overrun_us": 3247, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "source": "wasi-sleep", + "source_state": "working-tree-characterization", + "token": "18e160edadfda548-6a3d-2", + "within_five_ms": true + } + ], + "summaries": [ + { + "source": "native-sleep", + "count": 24, + "late": 7, + "max_overrun_us": 10012, + "max_raw_select_overrun_us": null + }, + { + "source": "empty-select", + "count": 24, + "late": 0, + "max_overrun_us": 3533, + "max_raw_select_overrun_us": 3522 + }, + { + "source": "wasi-sleep", + "count": 24, + "late": 0, + "max_overrun_us": 3381, + "max_raw_select_overrun_us": null + } + ], + "body_probes": 135, + "origin_requests": 147, + "body_max_completion_us": 503628, + "body_late": 0, + "body_source_error_losses": 24, + "validation_failures": [ + "readiness: 24 readiness leg(s) failed:\ntruncated/selected/buffered/1/0: wrong case terminal\ntruncated/selected/buffered/16384/0: wrong case terminal\ntruncated/selected/streamed/1/0: wrong case terminal\ntruncated/selected/streamed/16384/0: wrong case terminal\nmalformed-body/selected/buffered/1/0: wrong case terminal\nmalformed-body/selected/buffered/16384/0: wrong case terminal\nmalformed-body/selected/streamed/1/0: wrong case terminal\nmalformed-body/selected/streamed/16384/0: wrong case terminal\ntruncated/selected/buffered/1/1: wrong case terminal\ntruncated/selected/buffered/16384/1: wrong case terminal\ntruncated/selected/streamed/1/1: wrong case terminal\ntruncated/selected/streamed/16384/1: wrong case terminal\nmalformed-body/selected/buffered/1/1: wrong case terminal\nmalformed-body/selected/buffered/16384/1: wrong case terminal\nmalformed-body/selected/streamed/1/1: wrong case terminal\nmalformed-body/selected/streamed/16384/1: wrong case terminal\ntruncated/selected/buffered/1/2: wrong case terminal\ntruncated/selected/buffered/16384/2: wrong case terminal\ntruncated/selected/streamed/1/2: wrong case terminal\ntruncated/selected/streamed/16384/2: wrong case terminal\nmalformed-body/selected/buffered/1/2: wrong case terminal\nmalformed-body/selected/buffered/16384/2: wrong case terminal\nmalformed-body/selected/streamed/1/2: wrong case terminal\nmalformed-body/selected/streamed/16384/2: wrong case terminal" + ], + "local_matrix_pass": false, + "qualification": false, + "phase": "before-review" + }, + { + "id": "repaired-reviewed", + "runtime": "repaired", + "phase": "reviewed", + "exit_code": 2, + "directory": "/private/tmp/edgezero-fastly-timer-controls-repaired-20261023-02", + "events_sha256": "572a738000b5b087bc7ee1999298b7d17540565ff8fc4c560eab0cfd8e91286e", + "log_sha256": "f0063db3b4059e4c3065117fb8117310fc6bdffa3a9333b5bde6d44f2cc5a6b1", + "guest_sha256": "4d3cf68793b593fe3d527176b2e0d00adfec434f330287bbfebe07f0b2f419e0", + "controls": 72, + "warmups": [ + { + "budget_ms": 1, + "elapsed_us": 1258, + "event": "readiness-timer-warmup", + "measurement_end": "primitive-return", + "overrun_us": 258, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "source": "native-sleep", + "token": "18e161a30df2af60-10a77-0", + "within_five_ms": true + }, + { + "abi_enter_us": 4, + "abi_return_us": 7139, + "abi_status": "FastlyStatus::OK", + "budget_ms": 1, + "elapsed_us": 7139, + "event": "readiness-timer-warmup", + "measurement_end": "primitive-return", + "overrun_us": 6139, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "selected": null, + "source": "empty-select", + "source_state": "working-tree-characterization", + "token": "18e161a30e097b50-10a77-1", + "within_five_ms": false + }, + { + "budget_ms": 1, + "elapsed_us": 3127, + "event": "readiness-timer-warmup", + "measurement_end": "primitive-return", + "overrun_us": 2127, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "source": "wasi-sleep", + "source_state": "working-tree-characterization", + "token": "18e161a30f229828-10a77-2", + "within_five_ms": true + } + ], + "summaries": [ + { + "source": "native-sleep", + "count": 24, + "late": 0, + "max_overrun_us": 4557, + "max_raw_select_overrun_us": null + }, + { + "source": "empty-select", + "count": 24, + "late": 0, + "max_overrun_us": 2421, + "max_raw_select_overrun_us": 2421 + }, + { + "source": "wasi-sleep", + "count": 24, + "late": 0, + "max_overrun_us": 2138, + "max_raw_select_overrun_us": null + } + ], + "body_probes": 135, + "origin_requests": 147, + "body_max_completion_us": 502553, + "body_late": 0, + "body_source_error_losses": 0, + "validation_failures": [], + "local_matrix_pass": true, + "qualification": false + }, + { + "id": "stock-reviewed", + "runtime": "stock", + "phase": "reviewed", + "exit_code": 1, + "directory": "/private/tmp/edgezero-fastly-timer-controls-stock-20261023-02", + "events_sha256": "4f0cf5574d49293d27b03296490f56b48a37c3bdb4ac402028892898403a7232", + "log_sha256": "b85e4aff249a6c9d5194b9521dcb1b18f76d28453b53e0a0f8ab74c4b1bf5f4d", + "guest_sha256": "4d3cf68793b593fe3d527176b2e0d00adfec434f330287bbfebe07f0b2f419e0", + "controls": 72, + "warmups": [ + { + "budget_ms": 1, + "elapsed_us": 1254, + "event": "readiness-timer-warmup", + "measurement_end": "primitive-return", + "overrun_us": 254, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "source": "native-sleep", + "token": "18e161b773b613f0-11a68-0", + "within_five_ms": true + }, + { + "abi_enter_us": 0, + "abi_return_us": 2150, + "abi_status": "FastlyStatus::OK", + "budget_ms": 1, + "elapsed_us": 2150, + "event": "readiness-timer-warmup", + "measurement_end": "primitive-return", + "overrun_us": 1150, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "selected": null, + "source": "empty-select", + "source_state": "working-tree-characterization", + "token": "18e161b773ccf368-11a68-1", + "within_five_ms": true + }, + { + "budget_ms": 1, + "elapsed_us": 2502, + "event": "readiness-timer-warmup", + "measurement_end": "primitive-return", + "overrun_us": 1502, + "qualification": false, + "revision": "77819f712ad971ada5494c563dd86a3b64a2afa6", + "schema": 1, + "source": "wasi-sleep", + "source_state": "working-tree-characterization", + "token": "18e161b774dc7b20-11a68-2", + "within_five_ms": true + } + ], + "summaries": [ + { + "source": "native-sleep", + "count": 24, + "late": 0, + "max_overrun_us": 4751, + "max_raw_select_overrun_us": null + }, + { + "source": "empty-select", + "count": 24, + "late": 0, + "max_overrun_us": 2281, + "max_raw_select_overrun_us": 2281 + }, + { + "source": "wasi-sleep", + "count": 24, + "late": 1, + "max_overrun_us": 6952, + "max_raw_select_overrun_us": null + } + ], + "body_probes": 135, + "origin_requests": 147, + "body_max_completion_us": 502413, + "body_late": 0, + "body_source_error_losses": 24, + "validation_failures": [ + "readiness: 24 readiness leg(s) failed:\ntruncated/selected/buffered/1/0: wrong case terminal\ntruncated/selected/buffered/16384/0: wrong case terminal\ntruncated/selected/streamed/1/0: wrong case terminal\ntruncated/selected/streamed/16384/0: wrong case terminal\nmalformed-body/selected/buffered/1/0: wrong case terminal\nmalformed-body/selected/buffered/16384/0: wrong case terminal\nmalformed-body/selected/streamed/1/0: wrong case terminal\nmalformed-body/selected/streamed/16384/0: wrong case terminal\ntruncated/selected/buffered/1/1: wrong case terminal\ntruncated/selected/buffered/16384/1: wrong case terminal\ntruncated/selected/streamed/1/1: wrong case terminal\ntruncated/selected/streamed/16384/1: wrong case terminal\nmalformed-body/selected/buffered/1/1: wrong case terminal\nmalformed-body/selected/buffered/16384/1: wrong case terminal\nmalformed-body/selected/streamed/1/1: wrong case terminal\nmalformed-body/selected/streamed/16384/1: wrong case terminal\ntruncated/selected/buffered/1/2: wrong case terminal\ntruncated/selected/buffered/16384/2: wrong case terminal\ntruncated/selected/streamed/1/2: wrong case terminal\ntruncated/selected/streamed/16384/2: wrong case terminal\nmalformed-body/selected/buffered/1/2: wrong case terminal\nmalformed-body/selected/buffered/16384/2: wrong case terminal\nmalformed-body/selected/streamed/1/2: wrong case terminal\nmalformed-body/selected/streamed/16384/2: wrong case terminal" + ], + "local_matrix_pass": false, + "qualification": false + } + ], + "retained_probes": { + "path": "docs/audits/evidence/2026-10-23-fastly-timer-controls-probes.jsonl", + "sha256": "bfd2d672c0540b3a63ffbe51ca9a2ea76afff28c541425d3e70f49f001566898", + "records": 828, + "timer_controls": 288, + "body_probes": 540 + }, + "reviewed_dependency_sources": [ + { + "package": "Viceroy a69d982d1d95e13c0fc9f92683b9118639bb4b04", + "path": "src/async_io.rs", + "sha256": "49a73f24b6c495e5240768b4e3af47af21252ae6e4c891df80c635395e3025c3" + }, + { + "package": "Viceroy a69d982d1d95e13c0fc9f92683b9118639bb4b04", + "path": "Cargo.lock", + "sha256": "d642d177081f42aec75d25c0d1124c013d3abcb081886251420a90e9c53c1a2a" + }, + { + "package": "tokio 1.53.1", + "path": "tokio-1.53.1/src/time/timeout.rs", + "sha256": "5b1b20cb77de63fb5071b696ac644706a9749e0aeb6dbea57220834cba6dc953" + }, + { + "package": "tokio 1.53.1", + "path": "tokio-1.53.1/src/runtime/time/source.rs", + "sha256": "02e85457f988c0cffd50c9fe92f4a12b33486803b331a254672dec89d5727443" + }, + { + "package": "tokio 1.53.1", + "path": "tokio-1.53.1/src/runtime/time/mod.rs", + "sha256": "3123856cc9f93bb319c3c428f4e14f1b6bddad4344240150c616079004e41058" + }, + { + "package": "wasmtime-wasi 39.0.2", + "path": "wasmtime-wasi-39.0.2/src/p1.rs", + "sha256": "c14dbcbb3b7c8f2efc8ff4f20429639c46848e98cdf216105d93b8d6bd6915b2" + }, + { + "package": "wasmtime-wasi 39.0.2", + "path": "wasmtime-wasi-39.0.2/src/clocks.rs", + "sha256": "41cba705f75663a6c02344cfbcc4278dc7aa94c3d8ec1b6f00dcd9a13946c43e" + }, + { + "package": "wasmtime-wasi 39.0.2", + "path": "wasmtime-wasi-39.0.2/src/p2/host/clocks.rs", + "sha256": "bfa64ad9a4a24f2062c809c991e92b1dc95f7018744e51dda0707eb746cf2900" + } + ], + "verification": { + "fixture_core_tests": 6, + "fixture_harness_tests": 118, + "retained_replay_tests": 3, + "strict_native_harness_clippy": "passed", + "strict_wasip1_guest_clippy": "passed", + "fixture_format": "passed", + "qualification_inventory_unchanged": true, + "root_gates": { + "format": "passed", + "strict_workspace_clippy": "passed", + "workspace_tests": { + "passed": 2193, + "failed": 0, + "ignored": 1 + }, + "feature_check": "passed", + "spin_wasip2": "passed", + "test_execution": "localhost-authorized rerun after sandbox bind denials; both logs retained", + "status_log_sha256": "2292731c26db03f8b938da30d1a9cb80746d8be08a4c60556efb90a1239fbb6e" + }, + "final_workspace_rerun": { + "passed": 2193, + "failed": 0, + "ignored": 1, + "log_sha256": "8b06944de09d38a8031ac1affb71babf1c26665fd759397bd6bd2c4ac8b54ecb", + "other_four_gates": "passed on final working tree" + } + }, + "prior_retained_prefix": { + "bytes": 564456, + "records": 414, + "sha256": "a3d1badd60cd5cda2486c72a9b615c5fb63bc0596e969384195931af34a65db5" + }, + "source_files_before_review": [ + { + "path": "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs", + "sha256": "3dd9cebd97cf6921157470727f951b1f412136d430aacc93a95b54c2e8ac59b2" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs", + "sha256": "bf497a757f36427120e7deab1e1e70e415977ce88b73b138e750df8032e5b4ca" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence/readiness.rs", + "sha256": "20d40c71bf6f4124126a7e72f53da9897d55da341aae664257697773eb9b14da" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence/timers.rs", + "sha256": "ea4fca7c091c25bad017c6b7132c8075a7d088f90ca59428e5edc55302e91f76" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/readiness.rs", + "sha256": "51341b2d7cfac23d07803c2d72cc227299a21fa409864fabaa0fbfdd840b4ad0" + }, + { + "path": "tests/fixtures/reusable-app/Cargo.lock", + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + { + "path": "patches/viceroy/0.21.0-body-readiness.patch", + "sha256": "8693ce34f82cc98dc272bf39379007b17a40dfc19138c55b445378fd9a163759" + } + ], + "review_regressions": { + "timer_only_endpoint": { + "before_elapsed_us": 4330, + "before_abi_return_us": 4325, + "red_assertion_failed": true, + "reviewed_empty_select_endpoint_equality_enforced": true + }, + "raw_abi_duration": "red and green regression rejects waits shorter than the positive timeout", + "declared_endpoint": "red and green regression rejects post-primitive work in a declared primitive-return measurement" + } +} diff --git a/docs/audits/evidence/2026-10-23-synthesized-response-headers.json b/docs/audits/evidence/2026-10-23-synthesized-response-headers.json new file mode 100644 index 000000000..ee31bf6f9 --- /dev/null +++ b/docs/audits/evidence/2026-10-23-synthesized-response-headers.json @@ -0,0 +1,1567 @@ +{ + "schema": 1, + "recorded_date": "2026-10-23", + "base_revision": "7fdef8b0a31f386f3c7e304ccf807f7ab500b19a", + "source_state": "uncommitted-working-tree-characterization", + "qualification": false, + "status": "local-http1-header-delivery-pass", + "scope": "F7b / EZ-19 local normalized rejection, full-size synthesized fields, GET/HEAD fallback, Fastly prepare replacement, exclusion and terminal controls", + "production_delta_this_runtime_slice": false, + "source_snapshot_scope": "Selected local production/fixture inputs, not a complete frozen candidate; source and executed artifacts are fingerprinted separately.", + "source_sha256": { + "Cargo.lock": "ded1da41b9bd6fae689e16d277d076e975c8f3ccfa2842cd53addf30a5c3e27e", + "Cargo.toml": "527a6f29fd231afb35dea70ba3ec41b9c9e78ce52324cb20fe9ed6c75afb415b", + "crates/edgezero-adapter-axum/src/response.rs": "b5068969a307261af438a8cf5fcd15a43fe03a045b57a7231f88de1df84b1ae8", + "crates/edgezero-adapter-cloudflare/src/response.rs": "bd7980085cd771ed3940591f8501aa84facb35d4281e9177fe6a71bbaa20c57a", + "crates/edgezero-adapter-fastly/src/request.rs": "279751032c2b7fdfb6e1aeec20cd9f2853f46095865ce4504a88b0d4152eb06b", + "crates/edgezero-adapter-fastly/src/response.rs": "e1ddc3774504da3ffbc5b2ed18ffd732a486ac17e9dca96dc47d0083da4f0a0c", + "crates/edgezero-adapter-fastly/tests/contract.rs": "bae55aab4b21dc048ee4defd5d276ef43ff9517019e2cbfa9b0ddc64f88c74e5", + "crates/edgezero-adapter-spin/src/response.rs": "43be730fb6244ce244b6adce80da37731b741ff8475c0ecdc693fc64c13d4f2d", + "crates/edgezero-core/src/app.rs": "30082b21f9a8c32dcd337fd353ea6ead3957be077b821bc0cfcb252d40c4e25f", + "crates/edgezero-core/src/lib.rs": "9e8570286ab192744e8089a656163ccbaa4c1720a36c1110f49679c50f77f2a1", + "crates/edgezero-core/src/response_egress.rs": "665f5363ff4c003eac513f3262dd1797d9b269cd20189eadcfefac50624db452", + "crates/edgezero-core/src/response_egress_framing_test_support.rs": "9eb3a368d428ff7a8463613f2738e0281c34ebcdb6f02d364e6577188a3695d6", + "crates/edgezero-core/src/response_headers.rs": "6ce849b0c1e64cb34d1a4468f031b753d612f2f6554c9681088327c96f2e5f75", + "examples/app-demo/crates/app-demo-core/src/lib.rs": "ec9aa401fd348c35ccb76999b648f4bf97a35f32560a64abcad448d7e0eccb3a", + "tests/fixtures/reusable-app/Cargo.lock": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee", + "tests/fixtures/reusable-app/Cargo.toml": "7665200757e4e565d11fa7eb036215aa583d21fd543bca39fdbdce470fee11ae", + "tests/fixtures/reusable-app/crates/fixture-axum/src/progress.rs": "1513e29d6b8fb8a16dbcbcec6e8119e73c764eb86f0635a97ddb86b4fa647d17", + "tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs": "a67f65fe9990ce4adba50ffe5e01b5bf50809492a2f222a3cbc5c726ac4ace96", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs": "b8ce27cccb0949eab4a03b7694cbe528e028fc42ae4b14880f13bd68bc85f1b8", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs": "3ccea7b618f7ee3b9103bd5fd6ce597d65fa62fb2f118aabdb30c2544628c983", + "tests/fixtures/reusable-app/crates/fixture-core/src/qualification/response_headers.rs": "e33aa3ca2375ac746900b75eecfbde33cf853e4777bd92f5b18300519a6bec9a", + "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs": "057cd67c78725c19e24f2ce968138e3c38001c43ec656c015863834a0b87e706", + "tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs": "605a337afb130d0eddafb58d910eaa1c4f7cbe06b2481648293308bf3aee75fd", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs": "734e56e9fa1beda4a4dbca09c7ac3914bd56c8bd33e4901e2db1c3626b99b9d0", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/framing.rs": "3cf08dbf5d6bcf45b7eb9f4793d6020bd3887d1af8f57a06353d2b3cfd9fa939", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/overflow.rs": "5597383d9e912ea20beda249e0b03c115055d31e459d6d9aa1aa48e3162761ea", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/progress.rs": "acfd1f42e71fbadc499df8db45f5852c1e7ef71944527a3c8479aecd06e5db36", + "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/response_headers.rs": "5a8545019592510864212a811059f1ee3c9c86fed4b59c26ce6ca246ebeaf98d", + "tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs": "47e765274ca5175ab5e80f6658e0c53fdfaf256edebb6a3c5337de9f2e138237" + }, + "frozen_policy": { + "path": "docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md", + "bytes": 65279, + "sha256": "c52b4aea6b8d2793f30784e88016a3676088d344c0a5607774a5d9f1a7ffae80" + }, + "command": "CARGO_NET_OFFLINE=true scripts/smoke_test_reusable_app.sh --adapter all --suite synthesized-headers --output /private/tmp/edgezero-f7b-wire-final3", + "tool_overrides": { + "WORKER_BUILD_BIN": "/Users/ag/.asdf/installs/rust/1.98.1/bin/worker-build", + "WASM_BINDGEN_BIN": "/private/tmp/edgezero-pr275-tools/wasm-bindgen-0.2.129/bin/wasm-bindgen", + "ESBUILD_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@esbuild/darwin-arm64/bin/esbuild", + "WASM_OPT_BIN": "/Users/ag/Library/Caches/worker-build/wasm-opt-arm64-macos-130/bin/wasm-opt", + "WRANGLER_BIN": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "WRANGLER_SEND_METRICS": "false", + "WRANGLER_LOG_PATH": "/private/tmp/edgezero-pr275-wrangler-logs", + "SSL_CERT_FILE": "/etc/ssl/cert.pem" + }, + "sdk_versions": { + "fastly": "0.13.1", + "worker": "0.8.5", + "spin_sdk": "7.0.0" + }, + "cloudflare_compatibility": { + "date": "2026-04-22", + "flags": ["enable_request_signal", "enable_weak_ref"] + }, + "workerd": { + "path": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64/bin/workerd", + "bytes": 156374824, + "sha256": "eb132e3e489b47d804cc70b510a6da8f82dabec4748cdcdaba32a1a1c5c2e285", + "version": "workerd 2026-09-10", + "identification_basis": "Wrangler bundled executable; not independent per-process attestation" + }, + "runtime_versions": [ + { + "adapter": "fastly", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "revision": "7fdef8b0a31f386f3c7e304ccf807f7ab500b19a", + "source_state": "working-tree-characterization", + "version": "viceroy 0.21.0" + }, + { + "adapter": "cloudflare", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/nodejs/24.12.0/lib/node_modules/wrangler/bin/wrangler.js", + "revision": "7fdef8b0a31f386f3c7e304ccf807f7ab500b19a", + "source_state": "working-tree-characterization", + "version": "4.131.0" + }, + { + "adapter": "spin", + "event": "progress-runtime", + "executable": "/opt/homebrew/Cellar/spin/4.1.0/bin/spin", + "revision": "7fdef8b0a31f386f3c7e304ccf807f7ab500b19a", + "source_state": "working-tree-characterization", + "version": "spin 4.1.0 (c0b3726 2026-08-25)" + }, + { + "adapter": "axum", + "event": "progress-runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/toolchains/1.98.1-aarch64-apple-darwin/bin/cargo", + "revision": "7fdef8b0a31f386f3c7e304ccf807f7ab500b19a", + "source_state": "working-tree-characterization", + "version": "cargo 1.98.1 (797e8a9bc 2026-08-05)" + } + ], + "artifacts": [ + { + "adapter": "fastly", + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/artifacts/fixture-fastly-outbound-progress.wasm", + "bytes": 2375095, + "sha256": "7f9275671e38c5f2fcbaff60f235820a45dd37adca3df9f627fb16eec853b7f0" + }, + { + "adapter": "cloudflare", + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/artifacts/build/index_bg.wasm", + "bytes": 1947183, + "sha256": "2f14ca7e7f5ff1d9ecb92fa1f07583328813dc4925644d248dcf111ab3eb17d0" + }, + { + "adapter": "spin", + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/artifacts/fixture_spin.wasm", + "bytes": 2606578, + "sha256": "380730d46315cc42a82146e6b2afaa620998d0105713f7d6204818ae9a52aa37" + }, + { + "adapter": "axum", + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/artifacts/fixture-axum-outbound-progress", + "bytes": 9085536, + "sha256": "819c21681057ae0a92e243313a985b7e15d9d14e04120d083d6a27a66a186dac" + } + ], + "raw": [ + { + "path": "/private/tmp/edgezero-f7b-wire-final3/events.jsonl", + "bytes": 202039, + "sha256": "296c0e9f2db7995f06b44d6414fed3eb02d5df5ec6d50af7688678dd27cc9d2a" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-final3/summary.json", + "bytes": 302, + "sha256": "fdad4ea9024508071acff823ee80f31b68f6c2e1980faefaaff86076ffc842e3" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-final3/Cargo.lock", + "bytes": 84127, + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-final3.log", + "bytes": 2445, + "sha256": "f3f405e88634e245a396ebe6d77666ae4a39da521d2ab06a484b3c9e34d809e1" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/runtime.log", + "bytes": 12981, + "sha256": "0c58d9bb788ab04075ea99483bfc2ac91da46d6554cafb461f2d9064256c295a" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/runtime.log", + "bytes": 3218, + "sha256": "aedd8d5b4213a5bcbb0b365b8f3f64b7fdc0c0bb21a719564d4ab65ba2d82fae" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/runtime.log", + "bytes": 5691, + "sha256": "2be2dae71bb05eadfcc16799f2f26e8809639b587b6e78986619ea2f935a3377" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/runtime.log", + "bytes": 4439, + "sha256": "c58c230010705dfce4c8335e7633e3f2b01d494a3bdd650a6d83eadcb7bb630a" + } + ], + "coverage": [ + { + "adapter": "fastly", + "attempted": 22, + "event": "synthesized-header-coverage", + "failed": 0, + "protocol": "HTTP/1.1", + "qualification": false + }, + { + "adapter": "fastly", + "checked": 22, + "error": null, + "event": "synthesized-final-evidence", + "qualification": false, + "status": "pass" + }, + { + "adapter": "cloudflare", + "attempted": 11, + "event": "synthesized-header-coverage", + "failed": 0, + "protocol": "HTTP/1.1", + "qualification": false + }, + { + "adapter": "spin", + "attempted": 11, + "event": "synthesized-header-coverage", + "failed": 0, + "protocol": "HTTP/1.1", + "qualification": false + }, + { + "adapter": "axum", + "attempted": 11, + "event": "synthesized-header-coverage", + "failed": 0, + "protocol": "HTTP/1.1", + "qualification": false + } + ], + "cases": [ + { + "adapter": "fastly", + "case": "success-GET", + "token": "18e133eff4bc79a8-1205a-0", + "status": "pass", + "http_status": 200, + "eof": true, + "capture_error": null, + "body_bytes": 14, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff4bc79a8-1205a-0.http1", + "bytes": 90, + "sha256": "b00404ddd950a7f2c39447f0b9727ab0320594ff583be016d8b9bce6d3afb3da" + } + }, + { + "adapter": "fastly", + "case": "handler-error-GET", + "token": "18e133eff57457f8-1205a-1", + "status": "pass", + "http_status": 400, + "eof": true, + "capture_error": null, + "body_bytes": 82, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 35, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff57457f8-1205a-1.http1", + "bytes": 199, + "sha256": "9c9a6acea56423833fc818385625baa8025fb7a8321c002defcbce39f57dec5b" + } + }, + { + "adapter": "fastly", + "case": "refused-GET", + "token": "18e133eff5a76260-1205a-2", + "status": "pass", + "http_status": 503, + "eof": true, + "capture_error": null, + "body_bytes": 16, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff5a76260-1205a-2.http1", + "bytes": 109, + "sha256": "47de362c46f948cd70777bf9faa27045a7a990a49f2f0cc8d893f75cf3aded29" + } + }, + { + "adapter": "fastly", + "case": "missing/path-GET", + "token": "18e133eff5d148f0-1205a-3", + "status": "pass", + "http_status": 404, + "eof": true, + "capture_error": null, + "body_bytes": 113, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 34, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff5d148f0-1205a-3.http1", + "bytes": 4323, + "sha256": "064b61d9c8294e6338b9661375175e6437de65aa6261b84570a73466f252071d" + } + }, + { + "adapter": "fastly", + "case": "fallback-GET", + "token": "18e133eff6141a68-1205a-4", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 21, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff6141a68-1205a-4.http1", + "bytes": 4210, + "sha256": "9150bd70c59a61d37f6c08e2a8958c8f5342c287d31cb61e983e478457ea822c" + } + }, + { + "adapter": "fastly", + "case": "success-HEAD", + "token": "18e133eff64c3998-1205a-5", + "status": "pass", + "http_status": 200, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff64c3998-1205a-5.http1", + "bytes": 56, + "sha256": "7555c0531da70f2f00323b450d6011324043b6c6d6fbe2bfb0fa02e75bee85d8" + } + }, + { + "adapter": "fastly", + "case": "handler-error-HEAD", + "token": "18e133eff67fc100-1205a-6", + "status": "pass", + "http_status": 400, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 35, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff67fc100-1205a-6.http1", + "bytes": 97, + "sha256": "4d2e83fc1288b64e43612fb0218d3b0cc2de5f157c2b04651f0f6ac6a36ae3e1" + } + }, + { + "adapter": "fastly", + "case": "refused-HEAD", + "token": "18e133eff6b1ae40-1205a-7", + "status": "pass", + "http_status": 503, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff6b1ae40-1205a-7.http1", + "bytes": 73, + "sha256": "1c19811661ba747d4f7efe22b4c2a660aaa024e471476f5e2d5e5a9d9a699e2a" + } + }, + { + "adapter": "fastly", + "case": "missing/path-HEAD", + "token": "18e133eff6e295c8-1205a-8", + "status": "pass", + "http_status": 404, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 34, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff6e295c8-1205a-8.http1", + "bytes": 4189, + "sha256": "50188f960e528be673f4b0d0627fc84ec8f9429555699fe17b8b96f113544395" + } + }, + { + "adapter": "fastly", + "case": "fallback-HEAD", + "token": "18e133eff71e7200-1205a-9", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff71e7200-1205a-9.http1", + "bytes": 4169, + "sha256": "ff76096f357c1e30e266331af00ad25783bff8974f4393a08b72e8e6f3c19b5b" + } + }, + { + "adapter": "fastly", + "case": "wrong-method", + "token": "18e133eff75dc8d8-1205a-a", + "status": "pass", + "http_status": 405, + "eof": true, + "capture_error": null, + "body_bytes": 108, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff75dc8d8-1205a-a.http1", + "bytes": 4345, + "sha256": "6c4d581626e5572467095907b5af6a11917ba6176b9946b51a154a52666e6a27" + } + }, + { + "adapter": "fastly", + "case": "normalized-header-overflow", + "token": "18e133eff7a26740-1205a-b", + "status": "pass", + "http_status": 431, + "eof": true, + "capture_error": null, + "body_bytes": 128, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 29, + "request_field_values": 65, + "request_field_wire_bytes": 74779, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff7a26740-1205a-b.http1", + "bytes": 4360, + "sha256": "ba835de782397d2cc992f33755dec25ee88678f6615b77c9984b375df913f7d3" + } + }, + { + "adapter": "fastly", + "case": "normalized-target-overflow-app", + "token": "18e133eff7f2dce8-1205a-c", + "status": "pass", + "http_status": 414, + "eof": true, + "capture_error": null, + "body_bytes": 109, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 9254, + "request_field_values": 5, + "request_field_wire_bytes": 139, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff7f2dce8-1205a-c.http1", + "bytes": 4322, + "sha256": "3db55c890160de9d57ff6dfafaa60668de022337e60a286631b38bb72b653533" + } + }, + { + "adapter": "fastly", + "case": "normalized-target-overflow-replace", + "token": "18e133eff83131f0-1205a-d", + "status": "pass", + "http_status": 414, + "eof": true, + "capture_error": null, + "body_bytes": 109, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 9254, + "request_field_values": 5, + "request_field_wire_bytes": 143, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff83131f0-1205a-d.http1", + "bytes": 4322, + "sha256": "b6712282dc92647f40af9414c72312c73d2c2ba2d84904c601e16274278de66e" + } + }, + { + "adapter": "fastly", + "case": "fallback-replace-GET", + "token": "18e133eff8730580-1205a-e", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 21, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 143, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff8730580-1205a-e.http1", + "bytes": 4210, + "sha256": "e1e60e38da33358c6762198d19ea524a60605f5d1750dc7f4f5834d1f86b117c" + } + }, + { + "adapter": "fastly", + "case": "fallback-replace-HEAD", + "token": "18e133eff8b70b90-1205a-f", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 143, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff8b70b90-1205a-f.http1", + "bytes": 4169, + "sha256": "14e7e48804a841c07c6c9136a193d102fc9852612d99f2649716e17317f41da0" + } + }, + { + "adapter": "fastly", + "case": "normalized-target-overflow-invalid", + "token": "18e133eff903eb40-1205a-10", + "status": "pass", + "http_status": 414, + "eof": true, + "capture_error": null, + "body_bytes": 109, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 9254, + "request_field_values": 5, + "request_field_wire_bytes": 144, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff903eb40-1205a-10.http1", + "bytes": 4322, + "sha256": "3db55c890160de9d57ff6dfafaa60668de022337e60a286631b38bb72b653533" + } + }, + { + "adapter": "fastly", + "case": "fallback-invalid-GET", + "token": "18e133eff9532868-1205a-11", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 21, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 144, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff9532868-1205a-11.http1", + "bytes": 4210, + "sha256": "9150bd70c59a61d37f6c08e2a8958c8f5342c287d31cb61e983e478457ea822c" + } + }, + { + "adapter": "fastly", + "case": "fallback-invalid-HEAD", + "token": "18e133eff99a4b58-1205a-12", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 144, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff99a4b58-1205a-12.http1", + "bytes": 4169, + "sha256": "ff76096f357c1e30e266331af00ad25783bff8974f4393a08b72e8e6f3c19b5b" + } + }, + { + "adapter": "fastly", + "case": "normalized-target-overflow-empty", + "token": "18e133eff9e611c8-1205a-13", + "status": "pass", + "http_status": 414, + "eof": true, + "capture_error": null, + "body_bytes": 109, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 9254, + "request_field_values": 5, + "request_field_wire_bytes": 142, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133eff9e611c8-1205a-13.http1", + "bytes": 228, + "sha256": "9386430faef68cdd70c752760db557d26e8cee113caf414ce552d0577c48459a" + } + }, + { + "adapter": "fastly", + "case": "fallback-empty-GET", + "token": "18e133effa213a50-1205a-14", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 21, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 142, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133effa213a50-1205a-14.http1", + "bytes": 116, + "sha256": "4250744989e9a7e679ab74a8700a1d52b845c00942160619cfd9a9c6bef22634" + } + }, + { + "adapter": "fastly", + "case": "fallback-empty-HEAD", + "token": "18e133effa516270-1205a-15", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 142, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-fastly/18e133effa516270-1205a-15.http1", + "bytes": 75, + "sha256": "d71beb69da73d4a72ecd8a9c075ef0c1582e7fdb0445b69892c09ade57695ac1" + } + }, + { + "adapter": "cloudflare", + "case": "success-GET", + "token": "18e133f0fb4ed030-1205a-16", + "status": "pass", + "http_status": 200, + "eof": true, + "capture_error": null, + "body_bytes": 14, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "write-half-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/18e133f0fb4ed030-1205a-16.http1", + "bytes": 53, + "sha256": "605394fa5e0e59c7aeef09952395af12aae160f4a7b2cca9895829a90c3296ab" + } + }, + { + "adapter": "cloudflare", + "case": "handler-error-GET", + "token": "18e133f10151b920-1205a-17", + "status": "pass", + "http_status": 400, + "eof": true, + "capture_error": null, + "body_bytes": 82, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 35, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "write-half-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/18e133f10151b920-1205a-17.http1", + "bytes": 162, + "sha256": "30620340332cfed06ce35d204a0754a03ebcab2bcebce5800dea6a41f8bcd10e" + } + }, + { + "adapter": "cloudflare", + "case": "refused-GET", + "token": "18e133f101ce24b0-1205a-18", + "status": "pass", + "http_status": 503, + "eof": true, + "capture_error": null, + "body_bytes": 16, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "write-half-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/18e133f101ce24b0-1205a-18.http1", + "bytes": 72, + "sha256": "e8720a13e727055a818f91d872efa4fb3fc3b2ee224c48797a8ce9b78491c270" + } + }, + { + "adapter": "cloudflare", + "case": "missing/path-GET", + "token": "18e133f101fdf6e0-1205a-19", + "status": "pass", + "http_status": 404, + "eof": true, + "capture_error": null, + "body_bytes": 113, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 34, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "write-half-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/18e133f101fdf6e0-1205a-19.http1", + "bytes": 4286, + "sha256": "d82ca802ce4cc925943ea796ba5afa21a5308e7e514158d646ca440c00d5a459" + } + }, + { + "adapter": "cloudflare", + "case": "fallback-GET", + "token": "18e133f10239d318-1205a-1a", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 21, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "write-half-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/18e133f10239d318-1205a-1a.http1", + "bytes": 4173, + "sha256": "bc97d96348cb5042d7a727c4dc6bc1b6d2dfbbb58eb0371388ed955880978166" + } + }, + { + "adapter": "cloudflare", + "case": "success-HEAD", + "token": "18e133f10275e600-1205a-1b", + "status": "pass", + "http_status": 200, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "write-half-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/18e133f10275e600-1205a-1b.http1", + "bytes": 19, + "sha256": "5212f7a67551c4744fd1099622fe454a56221fcb231c76d1ab15efbdf44f1057" + } + }, + { + "adapter": "cloudflare", + "case": "handler-error-HEAD", + "token": "18e133f102a4e928-1205a-1c", + "status": "pass", + "http_status": 400, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 35, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "write-half-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/18e133f102a4e928-1205a-1c.http1", + "bytes": 80, + "sha256": "ffd8ea887297580af13c74461a1a4d33420e59f894da129bb4404bf57c5c00da" + } + }, + { + "adapter": "cloudflare", + "case": "refused-HEAD", + "token": "18e133f102d46180-1205a-1d", + "status": "pass", + "http_status": 503, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "write-half-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/18e133f102d46180-1205a-1d.http1", + "bytes": 36, + "sha256": "5ae6460b33a4847809ee7828d0d10942f4a74a21c1f0c13be39f9a25c8aec7f9" + } + }, + { + "adapter": "cloudflare", + "case": "missing/path-HEAD", + "token": "18e133f103056460-1205a-1e", + "status": "pass", + "http_status": 404, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 34, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "write-half-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/18e133f103056460-1205a-1e.http1", + "bytes": 4173, + "sha256": "6fae2fa48eab5cc1efb2bc918e2489d85b08b7e4bdb065a95e6870660fcfe5f7" + } + }, + { + "adapter": "cloudflare", + "case": "fallback-HEAD", + "token": "18e133f103437700-1205a-1f", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "write-half-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/18e133f103437700-1205a-1f.http1", + "bytes": 4132, + "sha256": "26a13e011c7d6593aa681d8da047ebf43830f412a7128747bba7fdd46b19aa03" + } + }, + { + "adapter": "cloudflare", + "case": "wrong-method", + "token": "18e133f10380f148-1205a-20", + "status": "pass", + "http_status": 405, + "eof": true, + "capture_error": null, + "body_bytes": 108, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "write-half-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-cloudflare/18e133f10380f148-1205a-20.http1", + "bytes": 4308, + "sha256": "6f2be0f5fcdb5b5adae267667dc659600b0bc6c9d9eb7dacd2adfe79df6e6fd8" + } + }, + { + "adapter": "spin", + "case": "success-GET", + "token": "18e133f11a766630-1205a-21", + "status": "pass", + "http_status": 200, + "eof": true, + "capture_error": null, + "body_bytes": 14, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/18e133f11a766630-1205a-21.http1", + "bytes": 109, + "sha256": "023e4df486a3ce7b6ed236e15f164df11d5cce3cf33b36ce44e7d204ce94b2a4" + } + }, + { + "adapter": "spin", + "case": "handler-error-GET", + "token": "18e133f11c2c20c8-1205a-22", + "status": "pass", + "http_status": 400, + "eof": true, + "capture_error": null, + "body_bytes": 82, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 35, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/18e133f11c2c20c8-1205a-22.http1", + "bytes": 218, + "sha256": "473817a7b88ce2e86b25b054d370dc5ab32ea04c1452aa36975ac11331139880" + } + }, + { + "adapter": "spin", + "case": "refused-GET", + "token": "18e133f11c621d18-1205a-23", + "status": "pass", + "http_status": 503, + "eof": true, + "capture_error": null, + "body_bytes": 16, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/18e133f11c621d18-1205a-23.http1", + "bytes": 128, + "sha256": "1e29092c680b3d984ee02fe6f95c008d740d22172145094234f5666feb2103b4" + } + }, + { + "adapter": "spin", + "case": "missing/path-GET", + "token": "18e133f11c98f810-1205a-24", + "status": "pass", + "http_status": 404, + "eof": true, + "capture_error": null, + "body_bytes": 113, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 34, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/18e133f11c98f810-1205a-24.http1", + "bytes": 4342, + "sha256": "d911dc2cf6a02cc5b0bd770e38a69c4ffd0db680a6bcdd940cedf08b973ee186" + } + }, + { + "adapter": "spin", + "case": "fallback-GET", + "token": "18e133f11cd06778-1205a-25", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 21, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/18e133f11cd06778-1205a-25.http1", + "bytes": 4229, + "sha256": "1c5b7b4fa164bfbc7e526fd86dcdc3069e3fcb4400b53e7aaec59ab06fe53d2d" + } + }, + { + "adapter": "spin", + "case": "success-HEAD", + "token": "18e133f11d108d58-1205a-26", + "status": "pass", + "http_status": 200, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/18e133f11d108d58-1205a-26.http1", + "bytes": 75, + "sha256": "a785ffdf4ff85f35539851107d6fc0a4b82ee4f342cae326064418724444e808" + } + }, + { + "adapter": "spin", + "case": "handler-error-HEAD", + "token": "18e133f11d3fdea0-1205a-27", + "status": "pass", + "http_status": 400, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 35, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/18e133f11d3fdea0-1205a-27.http1", + "bytes": 136, + "sha256": "6628cbdb103526dbe5afb12865312a885779f634eca8ee9cc70787cfa51ef47b" + } + }, + { + "adapter": "spin", + "case": "refused-HEAD", + "token": "18e133f11d6f6e68-1205a-28", + "status": "pass", + "http_status": 503, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/18e133f11d6f6e68-1205a-28.http1", + "bytes": 92, + "sha256": "2fb9022a0ee2d226b368406b710a7fe06e15a1abe8e9d0b6f3836d2cf50d3a54" + } + }, + { + "adapter": "spin", + "case": "missing/path-HEAD", + "token": "18e133f11d9f4480-1205a-29", + "status": "pass", + "http_status": 404, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 34, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/18e133f11d9f4480-1205a-29.http1", + "bytes": 4229, + "sha256": "64d0841d1582de4bcd0f07483d2aab2b54e547c828479aa468917abf96a38e9e" + } + }, + { + "adapter": "spin", + "case": "fallback-HEAD", + "token": "18e133f11ddef530-1205a-2a", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/18e133f11ddef530-1205a-2a.http1", + "bytes": 4188, + "sha256": "b2dc23cf77b0b5431e8eed1ad5a58a53ca62a97c3da31925c75497fe6464ccba" + } + }, + { + "adapter": "spin", + "case": "wrong-method", + "token": "18e133f11e215948-1205a-2b", + "status": "pass", + "http_status": 405, + "eof": true, + "capture_error": null, + "body_bytes": 108, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-spin/18e133f11e215948-1205a-2b.http1", + "bytes": 4364, + "sha256": "e2237777ff8aabdbf45bf20778e65ae362e63db3ad1a34be2bca1df5fcba9a61" + } + }, + { + "adapter": "axum", + "case": "success-GET", + "token": "18e133f13d28c4e8-1205a-2c", + "status": "pass", + "http_status": 200, + "eof": true, + "capture_error": null, + "body_bytes": 14, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/18e133f13d28c4e8-1205a-2c.http1", + "bytes": 109, + "sha256": "023e4df486a3ce7b6ed236e15f164df11d5cce3cf33b36ce44e7d204ce94b2a4" + } + }, + { + "adapter": "axum", + "case": "handler-error-GET", + "token": "18e133f13d541aa8-1205a-2d", + "status": "pass", + "http_status": 400, + "eof": true, + "capture_error": null, + "body_bytes": 82, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 35, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/18e133f13d541aa8-1205a-2d.http1", + "bytes": 218, + "sha256": "473817a7b88ce2e86b25b054d370dc5ab32ea04c1452aa36975ac11331139880" + } + }, + { + "adapter": "axum", + "case": "refused-GET", + "token": "18e133f13d83fc78-1205a-2e", + "status": "pass", + "http_status": 503, + "eof": true, + "capture_error": null, + "body_bytes": 16, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/18e133f13d83fc78-1205a-2e.http1", + "bytes": 128, + "sha256": "1e29092c680b3d984ee02fe6f95c008d740d22172145094234f5666feb2103b4" + } + }, + { + "adapter": "axum", + "case": "missing/path-GET", + "token": "18e133f13db2ad98-1205a-2f", + "status": "pass", + "http_status": 404, + "eof": true, + "capture_error": null, + "body_bytes": 113, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 34, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/18e133f13db2ad98-1205a-2f.http1", + "bytes": 4342, + "sha256": "d911dc2cf6a02cc5b0bd770e38a69c4ffd0db680a6bcdd940cedf08b973ee186" + } + }, + { + "adapter": "axum", + "case": "fallback-GET", + "token": "18e133f13defaae0-1205a-30", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 21, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/18e133f13defaae0-1205a-30.http1", + "bytes": 4229, + "sha256": "1c5b7b4fa164bfbc7e526fd86dcdc3069e3fcb4400b53e7aaec59ab06fe53d2d" + } + }, + { + "adapter": "axum", + "case": "success-HEAD", + "token": "18e133f13e2cc768-1205a-31", + "status": "pass", + "http_status": 200, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/18e133f13e2cc768-1205a-31.http1", + "bytes": 75, + "sha256": "a785ffdf4ff85f35539851107d6fc0a4b82ee4f342cae326064418724444e808" + } + }, + { + "adapter": "axum", + "case": "handler-error-HEAD", + "token": "18e133f13e598c58-1205a-32", + "status": "pass", + "http_status": 400, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 35, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/18e133f13e598c58-1205a-32.http1", + "bytes": 136, + "sha256": "6628cbdb103526dbe5afb12865312a885779f634eca8ee9cc70787cfa51ef47b" + } + }, + { + "adapter": "axum", + "case": "refused-HEAD", + "token": "18e133f13e88ca18-1205a-33", + "status": "pass", + "http_status": 503, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 0, + "synthesized_field_section_bytes": 0, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/18e133f13e88ca18-1205a-33.http1", + "bytes": 92, + "sha256": "2fb9022a0ee2d226b368406b710a7fe06e15a1abe8e9d0b6f3836d2cf50d3a54" + } + }, + { + "adapter": "axum", + "case": "missing/path-HEAD", + "token": "18e133f13eb75428-1205a-34", + "status": "pass", + "http_status": 404, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 34, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/18e133f13eb75428-1205a-34.http1", + "bytes": 4229, + "sha256": "64d0841d1582de4bcd0f07483d2aab2b54e547c828479aa468917abf96a38e9e" + } + }, + { + "adapter": "axum", + "case": "fallback-HEAD", + "token": "18e133f13ef2edf8-1205a-35", + "status": "pass", + "http_status": 500, + "eof": true, + "capture_error": null, + "body_bytes": 0, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 30, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/18e133f13ef2edf8-1205a-35.http1", + "bytes": 4188, + "sha256": "b2dc23cf77b0b5431e8eed1ad5a58a53ca62a97c3da31925c75497fe6464ccba" + } + }, + { + "adapter": "axum", + "case": "wrong-method", + "token": "18e133f13f2f9938-1205a-36", + "status": "pass", + "http_status": 405, + "eof": true, + "capture_error": null, + "body_bytes": 108, + "synthesized_field_values": 32, + "synthesized_field_section_bytes": 4096, + "request_target_bytes": 29, + "request_field_values": 5, + "request_field_wire_bytes": 140, + "request_shutdown": "connection-close", + "wire_capture": { + "path": "/private/tmp/edgezero-f7b-wire-final3/synthesized-headers-axum/18e133f13f2f9938-1205a-36.http1", + "bytes": 4364, + "sha256": "e2237777ff8aabdbf45bf20778e65ae362e63db3ad1a34be2bca1df5fcba9a61" + } + } + ], + "retained_diagnostics": [ + { + "run": "/private/tmp/edgezero-f7b-wire-01", + "status": "failed", + "reason": "Cloudflare frontend did not close on Connection: close; other three passed", + "events": { + "path": "/private/tmp/edgezero-f7b-wire-01/events.jsonl", + "bytes": 193471, + "sha256": "d23210978c7cf57e503643f475c9a3d6c9ff87fc07942ba8cfa74c7e9ff7de73" + }, + "driver_log": { + "path": "/private/tmp/edgezero-f7b-wire-01.log", + "bytes": 30115, + "sha256": "605669b514be23f4c235b8865f1c4678e3c3796f6def552f11ed594c691a8ba2" + } + }, + { + "run": "/private/tmp/edgezero-f7b-wire-halfclose", + "status": "failed", + "reason": "macOS repeated timeout-option update failed after FIN", + "events": { + "path": "/private/tmp/edgezero-f7b-wire-halfclose/events.jsonl", + "bytes": 9272, + "sha256": "41464648d359f67f12cb37c548a5c0f6ee85ed4df032dab58c0c4a73e8689600" + }, + "driver_log": { + "path": "/private/tmp/edgezero-f7b-wire-halfclose.log", + "bytes": 6570, + "sha256": "163ed5a98d01cfbf1c747c051e70ff9e91fc574dffe7b01c863f3a7f92eecffc" + } + }, + { + "run": "/private/tmp/edgezero-f7b-wire-halfclose-nonblocking", + "status": "pass-cloudflare-only", + "reason": "Nonblocking capture observes clean EOF with client write-half-close", + "events": { + "path": "/private/tmp/edgezero-f7b-wire-halfclose-nonblocking/events.jsonl", + "bytes": 30383, + "sha256": "86ad0cf53378a11809ed145a7e415dca84a161ed06dbdf375c540c16f08e5cd4" + }, + "driver_log": { + "path": "/private/tmp/edgezero-f7b-wire-halfclose-nonblocking.log", + "bytes": 1509, + "sha256": "387bd29c20448b03c3997a0b75cc21a22e28ac758abcbf056aea0b858cf21502" + } + }, + { + "run": "/private/tmp/edgezero-f7b-wire-final", + "status": "failed", + "reason": "Early write FIN cancels requests on the other frontends; capture mode is now explicitly scoped", + "events": { + "path": "/private/tmp/edgezero-f7b-wire-final/events.jsonl", + "bytes": 60832, + "sha256": "64c14eaf9e51064a94717b752aa8b55037d7e299b1b6c38cca44d4de965098ce" + }, + "driver_log": { + "path": "/private/tmp/edgezero-f7b-wire-final.log", + "bytes": 32780, + "sha256": "335c5d346def50a883d2c89d58d17854d71a27aef7f9f9e0cf21628e9ec3e100" + } + }, + { + "run": "/private/tmp/edgezero-f7b-wire-final2", + "status": "pass-before-writer-hardening", + "reason": "Repeated full matrix after extending absolute deadline to each nonblocking write", + "events": { + "path": "/private/tmp/edgezero-f7b-wire-final2/events.jsonl", + "bytes": 201765, + "sha256": "4db37b45f1a5b24a304a984562e479a2b69d8dd4f0e394273b6c4b41fd3f9848" + }, + "driver_log": { + "path": "/private/tmp/edgezero-f7b-wire-final2.log", + "bytes": 3789, + "sha256": "e3cbdd9c311e83d0c3f107273c1467bf3730c85bd40b75feaa6dde9f31b53e05" + } + } + ], + "limitations": [ + "Local HTTP/1 only; no hosted, HTTP2/HTTP3, consumer-owned integration or frozen-candidate qualification.", + "Temporary captures and binaries remain local artifacts, not durable release assets.", + "Assembly-failure exclusion has unit coverage but no new runtime control in this suite.", + "Fastly G2 remains failed; its blocked driver rewrite and all nine failed SDK parity controls remain open.", + "No claim of Cloudflare overflow recovery or capability promotion.", + "Cloudflare uses client write-half-close; the other frontends use Connection: close without early FIN. This changes only controlled probe input, not production serving." + ], + "verification": { + "workspace": { + "passed": 2193, + "binaries": 28, + "failed": 0, + "ignored": 1 + }, + "fixture": { + "core": 20, + "harness": 72, + "failed": 0 + }, + "executed_wire": { + "total": 55, + "head": 23, + "clean_eof": 55, + "failed": 0 + }, + "required_rust_gates": [ + "cargo fmt --all -- --check", + "cargo clippy --offline --locked --workspace --all-targets --all-features -- -D warnings", + "cargo test --offline --locked --workspace --all-targets", + "cargo check --offline --locked --workspace --all-targets --features fastly,cloudflare,spin", + "cargo check --offline --locked -p edgezero-adapter-spin --target wasm32-wasip2 --features spin" + ], + "logs": [ + { + "path": "/private/tmp/edgezero-f7b-wire-workspace-clippy.log", + "bytes": 327, + "sha256": "d62b38e72a387717f5846ee77c85124ad1955606a1e3dc4ca2bbb0200b41052b" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-workspace-tests-sockets.log", + "bytes": 184441, + "sha256": "f64f2d6cf6708f5473e1e08bb07314992a9986329ad69bcfb3553a564f239836" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-features.log", + "bytes": 327, + "sha256": "5403378695e19c3998cdafda6e194cf2c8a71bad57fe6fcf84038171295fc922" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-spin-check.log", + "bytes": 483, + "sha256": "4e0a873c8fcf3410a21f4a5ff328734064f07abc7eeabd5db38b5b912cc6f962" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-fixture-tests-final.log", + "bytes": 9335, + "sha256": "3e20d507ca667c24897f278a2214a03b351e9346caefa4187d11d8fc053638a7" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-fixture-lint-final.log", + "bytes": 665, + "sha256": "c40df9e9fb59c04afa00e292c71441221ea8a19742b9258cb5bb66b8bba54406" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-docs-format.log", + "bytes": 118, + "sha256": "43ba3f26abc1bc66ab5eaa42bcc64ba4c70edea76a4a621144b929d0812736f1" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-docs-lint.log", + "bytes": 40, + "sha256": "81521cc702a8a0674ad78ee506a2ec936217876334be26daf9c738d03779be16" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-docs-build.log", + "bytes": 234, + "sha256": "e95fb17753b9329153dbfe57601f9d6e613fe06f997b0684a9dd7263a837c5d3" + } + ], + "actual_target_clippy": [ + { + "path": "/private/tmp/edgezero-f7b-wire-validator-fastly.log", + "bytes": 1107, + "sha256": "9ad3600056e4c2f1e129112dae0f7cb18c836921000acf98eae60684b7f1d860" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-validator-cloudflare.log", + "bytes": 1123, + "sha256": "bd173500b9dbfa13077926509e165a09d0bb1b1a3f774a5499f1cde3be9e1a38" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-validator-spin.log", + "bytes": 1099, + "sha256": "3d9a5ec8393626dee7496de54ed589373697e34b7720c32df62dd708facb4300" + }, + { + "path": "/private/tmp/edgezero-f7b-wire-validator-axum.log", + "bytes": 955, + "sha256": "f215c51f238ab7a966d97540375ce7a0ab3b2a2ca950236c1841551c9ea1a63a" + } + ], + "restricted_workspace_run": { + "status": "failed-environment", + "reason": "Socket bind denied; all affected tests rerun with loopback access", + "log": { + "path": "/private/tmp/edgezero-f7b-wire-workspace-tests.log", + "bytes": 53026, + "sha256": "f9a74900643a5ca81cc6e62aab81aecbaa1a6f16154d529e14456446b0de89bd" + } + }, + "warnings": [ + "Existing proc-macro-error2 2.0.1 future-incompatibility warning remains; no SDK/toolchain pin changes." + ] + } +} diff --git a/docs/audits/evidence/2026-10-23-viceroy-readiness-repair.json b/docs/audits/evidence/2026-10-23-viceroy-readiness-repair.json new file mode 100644 index 000000000..fd8db5638 --- /dev/null +++ b/docs/audits/evidence/2026-10-23-viceroy-readiness-repair.json @@ -0,0 +1,733 @@ +{ + "schema": 1, + "execution_date": "2026-10-23", + "status": "Local runtime repair verified; full G2 remains open", + "qualification": false, + "edgezero_base_revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "source_state": "working-tree runtime repair characterization; not a frozen qualification record", + "upstream": { + "repository": "https://github.com/fastly/Viceroy", + "revision": "a69d982d1d95e13c0fc9f92683b9118639bb4b04", + "tag": "v0.21.0", + "lockfile_sha256": "d642d177081f42aec75d25c0d1124c013d3abcb081886251420a90e9c53c1a2a", + "original_body_sha256": "df2134af7ebf5d404b1ad41fcc744737325c7d0d929908f8d8742f50a93f4ee5", + "repaired_body_sha256": "1c56533d85a5c26390f9224e102425cbecc0fe4a280513e00095e7117d6722f7" + }, + "patch": { + "path": "patches/viceroy/0.21.0-body-readiness.patch", + "sha256": "8693ce34f82cc98dc272bf39379007b17a40dfc19138c55b445378fd9a163759" + }, + "build": { + "repaired_compiler": "rustc 1.98.1 (48a229cea 2026-09-01)", + "repaired_profile": "dev; debug=0; incremental=0; upstream opt-level=1", + "stock_build": { + "unknown": "prebuilt executable compiler/profile not independently established" + }, + "sdk": "fastly 0.13.1", + "runtime_version": "viceroy 0.21.0; repaired binary is not unmodified 0.21.0", + "stock_host_binary_sha256": "751ff2c1a83063b109bc212b95be5f52e5ab6284b9027fa03aa229087eba3f30", + "repaired_host_binary_sha256": "98ea0536478ce2e641f093b2619b7ac7c6c237402f140e5792d854575abc7100", + "command": "RUSTUP_TOOLCHAIN=1.98.1 ASDF_RUST_VERSION=1.98.1 CARGO_PROFILE_DEV_DEBUG=0 CARGO_INCREMENTAL=0 cargo build --offline --locked -p viceroy" + }, + "tests": { + "unmodified_behavior_with_new_tests": { + "passed": 1, + "failed": 9, + "ignored": 0, + "log_sha256": "d941055af8a1b45ef66fbeb34a6ee60149562c57b29708c2e9704204fed6c450" + }, + "repaired_readiness": { + "passed": 10, + "failed": 0, + "ignored": 0, + "log_sha256": "ff1386a71ba1a0e87c0f75feeb0c9da481778ac1617f7b8ff639e2141b3f5074" + }, + "repaired_body_and_collector": { + "passed": 24, + "failed": 0, + "ignored": 0, + "log_sha256": "e9745d620a7116cafdeced167e320e79a33e99db395ff320a87b68d900b00e10" + }, + "repaired_full_library": { + "passed": 135, + "failed": 0, + "ignored": 0, + "log_sha256": "233359937452a70db23d2d69c9af3faaf454932ddc625b3666be25e9890f214f" + }, + "strict_upstream_clippy": { + "status": "fails identically on stock and repaired source under Rust 1.98.1", + "reported_library_errors": 7, + "reported_test_errors": 25, + "rendered_diagnostic_count": 26, + "stock_log_sha256": "975277256e08f29e43676a1df1f5659c5b3592dffb5437ec78e7c3cd527f8881", + "repaired_log_sha256": "479ea66af1c3a660fb6b25a444d2fe6233cf911df266c4fde609db7f8ac7796a", + "diagnostics": [ + "error: constants have by default a `'static` lifetime", + "error: enclosing `Ok` and `?` operator are unneeded", + "error: explicit call to `.into_iter()` in function argument accepting `IntoIterator`", + "error: large size difference between variants", + "error: length comparison to zero", + "error: redundant closure", + "error: redundant reference in `format!` argument", + "error: redundant reference in `format!` argument", + "error: slow zero-filling initialization", + "error: slow zero-filling initialization", + "error: the `Err`-variant returned from this function is very large", + "error: this `match` expression can be replaced with `?`", + "error: this can be `std::io::Error::other(_)`", + "error: unneeded late initialization", + "error: useless conversion to the same type: `error::Error`", + "error: useless conversion to the same type: `error::Error`", + "error: useless conversion to the same type: `error::Error`", + "error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`", + "error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`", + "error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`", + "error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`", + "error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`", + "error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`", + "error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`", + "error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`", + "error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`" + ] + } + }, + "command": "VICEROY_BIN= scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-readiness --output ", + "stock": { + "exit_code": 1, + "origin_requests_checked": 6, + "comparisons": 3, + "comparison_status": "fail", + "guest_revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "guest_sha256": "ffc3dc8ca82f34433d48b1d9b28a43e73b4494ebbb0cb348b37bfbaf1de0b4dc", + "events_sha256": "3b51fae4552a2e3a62539d7e6b8469a7fed3fd9cd8614d9d438ae1170801dc93", + "events": [ + { + "construction_rounds": 0, + "event": "run", + "max_requests": 10, + "seed": 856, + "suite": "outbound-readiness" + }, + { + "event": "build_environment", + "host_arch": "aarch64", + "host_os": "macos", + "lockfile": { + "bytes": 84127, + "fnv1a64": "10d93fc12c84db43", + "path": "/private/tmp/edgezero-fastly-readiness-stock-20261023-01/Cargo.lock", + "purpose": "non-security build identity" + }, + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)" + }, + { + "adapter": "fastly", + "event": "runtime", + "executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "expected": "0.21.0", + "version": "viceroy 0.21.0" + }, + { + "artifact": { + "bytes": 737829, + "fnv1a64": "dda314302b98af87", + "path": "/private/tmp/edgezero-fastly-readiness-stock-20261023-01/artifacts/fixture-fastly-outbound-readiness.wasm", + "purpose": "non-security build identity" + }, + "event": "readiness-build", + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "sdk": "0.13.1", + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "direct", + "qualification": false, + "read_limit": 1, + "repetition": 0, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 3744, + "operation": "head-select", + "selected": 0, + "start_us": 960, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "bytes": 1, + "end_us": 4091, + "operation": "read", + "start_us": 3769 + }, + { + "end_us": 54182, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 4095 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "selected", + "qualification": false, + "read_limit": 1, + "repetition": 0, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 571, + "operation": "head-select", + "selected": 0, + "start_us": 23, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "end_us": 578, + "operation": "body-select", + "selected": 0, + "start_us": 575, + "status": "FastlyStatus::OK", + "timeout_ms": 499 + }, + { + "bytes": 1, + "end_us": 594, + "operation": "read", + "start_us": 579 + }, + { + "end_us": 53639, + "operation": "body-select", + "selected": 0, + "start_us": 594, + "status": "FastlyStatus::OK", + "timeout_ms": 499 + }, + { + "bytes": 0, + "end_us": 53696, + "operation": "read", + "start_us": 53642 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "direct", + "qualification": false, + "read_limit": 1, + "repetition": 1, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 10186, + "operation": "head-select", + "selected": 0, + "start_us": 26, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "bytes": 1, + "end_us": 10210, + "operation": "read", + "start_us": 10189 + }, + { + "end_us": 64121, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 10211 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "selected", + "qualification": false, + "read_limit": 1, + "repetition": 1, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 580, + "operation": "head-select", + "selected": 0, + "start_us": 18, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "end_us": 589, + "operation": "body-select", + "selected": 0, + "start_us": 586, + "status": "FastlyStatus::OK", + "timeout_ms": 499 + }, + { + "bytes": 1, + "end_us": 608, + "operation": "read", + "start_us": 590 + }, + { + "end_us": 55127, + "operation": "body-select", + "selected": 0, + "start_us": 609, + "status": "FastlyStatus::OK", + "timeout_ms": 499 + }, + { + "bytes": 0, + "end_us": 55185, + "operation": "read", + "start_us": 55129 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "direct", + "qualification": false, + "read_limit": 1, + "repetition": 2, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 1365, + "operation": "head-select", + "selected": 0, + "start_us": 34, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "bytes": 1, + "end_us": 1408, + "operation": "read", + "start_us": 1372 + }, + { + "end_us": 55442, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 1409 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "selected", + "qualification": false, + "read_limit": 1, + "repetition": 2, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 9026, + "operation": "head-select", + "selected": 0, + "start_us": 32, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "end_us": 9037, + "operation": "body-select", + "selected": 0, + "start_us": 9031, + "status": "FastlyStatus::OK", + "timeout_ms": 490 + }, + { + "bytes": 1, + "end_us": 9073, + "operation": "read", + "start_us": 9038 + }, + { + "end_us": 61778, + "operation": "body-select", + "selected": 0, + "start_us": 9075, + "status": "FastlyStatus::OK", + "timeout_ms": 490 + }, + { + "bytes": 0, + "end_us": 61825, + "operation": "read", + "start_us": 61780 + } + ], + "source_state": "working-tree-characterization" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "selected": null, + "status": "fail" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "selected": null, + "status": "fail" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "selected": null, + "status": "fail" + }, + { + "adapter": "fastly", + "error": "pinned Viceroy body readiness consumed the source error; Fastly G2 fails", + "status": "fail" + } + ] + }, + "repaired": { + "exit_code": 2, + "origin_requests_checked": 6, + "comparisons": 3, + "comparison_status": "pass", + "guest_revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "guest_sha256": "ffc3dc8ca82f34433d48b1d9b28a43e73b4494ebbb0cb348b37bfbaf1de0b4dc", + "events_sha256": "708ecb3c0d98535d9bc5df7e682e8e130a238b41311e0fb01add72671c125eb4", + "events": [ + { + "construction_rounds": 0, + "event": "run", + "max_requests": 10, + "seed": 856, + "suite": "outbound-readiness" + }, + { + "event": "build_environment", + "host_arch": "aarch64", + "host_os": "macos", + "lockfile": { + "bytes": 84127, + "fnv1a64": "10d93fc12c84db43", + "path": "/private/tmp/edgezero-fastly-readiness-repaired-20261023-01/Cargo.lock", + "purpose": "non-security build identity" + }, + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)" + }, + { + "adapter": "fastly", + "event": "runtime", + "executable": "/private/tmp/edgezero-viceroy-0.21.0-readiness-20261023/target/debug/viceroy", + "expected": "0.21.0", + "version": "viceroy 0.21.0" + }, + { + "artifact": { + "bytes": 737829, + "fnv1a64": "dda314302b98af87", + "path": "/private/tmp/edgezero-fastly-readiness-repaired-20261023-01/artifacts/fixture-fastly-outbound-readiness.wasm", + "purpose": "non-security build identity" + }, + "event": "readiness-build", + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "sdk": "0.13.1", + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "direct", + "qualification": false, + "read_limit": 1, + "repetition": 0, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 9243, + "operation": "head-select", + "selected": 0, + "start_us": 207, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "bytes": 1, + "end_us": 9486, + "operation": "read", + "start_us": 9300 + }, + { + "end_us": 61230, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 9488 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "selected", + "qualification": false, + "read_limit": 1, + "repetition": 0, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 4697, + "operation": "head-select", + "selected": 0, + "start_us": 66, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "end_us": 4714, + "operation": "body-select", + "selected": 0, + "start_us": 4705, + "status": "FastlyStatus::OK", + "timeout_ms": 495 + }, + { + "bytes": 1, + "end_us": 4757, + "operation": "read", + "start_us": 4715 + }, + { + "end_us": 59163, + "operation": "body-select", + "selected": 0, + "start_us": 4758, + "status": "FastlyStatus::OK", + "timeout_ms": 495 + }, + { + "end_us": 59217, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 59164 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "direct", + "qualification": false, + "read_limit": 1, + "repetition": 1, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 10403, + "operation": "head-select", + "selected": 0, + "start_us": 29, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "bytes": 1, + "end_us": 10456, + "operation": "read", + "start_us": 10410 + }, + { + "end_us": 61740, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 10458 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "selected", + "qualification": false, + "read_limit": 1, + "repetition": 1, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 1209, + "operation": "head-select", + "selected": 0, + "start_us": 30, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "end_us": 1221, + "operation": "body-select", + "selected": 0, + "start_us": 1215, + "status": "FastlyStatus::OK", + "timeout_ms": 498 + }, + { + "bytes": 1, + "end_us": 1258, + "operation": "read", + "start_us": 1223 + }, + { + "end_us": 51397, + "operation": "body-select", + "selected": 0, + "start_us": 1259, + "status": "FastlyStatus::OK", + "timeout_ms": 498 + }, + { + "end_us": 51447, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 51399 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "direct", + "qualification": false, + "read_limit": 1, + "repetition": 2, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 4569, + "operation": "head-select", + "selected": 0, + "start_us": 22, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "bytes": 1, + "end_us": 4618, + "operation": "read", + "start_us": 4579 + }, + { + "end_us": 59585, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 4620 + } + ], + "source_state": "working-tree-characterization" + }, + { + "case": "truncated", + "deadline_us": 500000, + "mode": "selected", + "qualification": false, + "read_limit": 1, + "repetition": 2, + "revision": "ff60e5ac9d6d673648ca5e06b1bdddfaab68083a", + "samples": [ + { + "end_us": 8737, + "operation": "head-select", + "selected": 0, + "start_us": 27, + "status": "FastlyStatus::OK", + "timeout_ms": 500 + }, + { + "end_us": 8760, + "operation": "body-select", + "selected": 0, + "start_us": 8747, + "status": "FastlyStatus::OK", + "timeout_ms": 491 + }, + { + "bytes": 1, + "end_us": 8795, + "operation": "read", + "start_us": 8762 + }, + { + "end_us": 63720, + "operation": "body-select", + "selected": 0, + "start_us": 8796, + "status": "FastlyStatus::OK", + "timeout_ms": 491 + }, + { + "end_us": 63771, + "error": "UnexpectedEof", + "operation": "read", + "start_us": 63722 + } + ], + "source_state": "working-tree-characterization" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "selected": "UnexpectedEof", + "status": "pass" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "selected": "UnexpectedEof", + "status": "pass" + }, + { + "direct": "UnexpectedEof", + "event": "readiness-error-preservation", + "qualification": false, + "selected": "UnexpectedEof", + "status": "pass" + }, + { + "qualification": false, + "reason": "paired error control passed; remaining G2 timed-read scenarios are still pending", + "status": "unverified" + } + ] + }, + "limitations": [ + "Both probe runs use the same guest binary; the host runtime is the changed boundary.", + "Only three paired truncated-body controls ran through the actual guest ABI. Full G2 stalled/trickle/sibling/EOF/framing/buffered/streamed cases remain open.", + "Unit tests complement but do not replace the full G2 ABI or hosted suite.", + "The original pinned unmodified runtime remains a failed control; no SDK/runtime pin or installed executable was replaced.", + "No backend B, optimizer H(K), capability promotion, source freeze, hosted execution or production approval.", + "Raw probe events are retained here; binary and test/build/lint logs are hash-identified, not durable release assets." + ] +} diff --git a/docs/audits/evidence/2026-10-24-fastly-local-recheck-probes.jsonl b/docs/audits/evidence/2026-10-24-fastly-local-recheck-probes.jsonl new file mode 100644 index 000000000..83b506d79 --- /dev/null +++ b/docs/audits/evidence/2026-10-24-fastly-local-recheck-probes.jsonl @@ -0,0 +1,414 @@ +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1256,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":256,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19138ab3caae0-cca9-3","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2277,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2277,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1277,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19138ab536730-cca9-4","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2027,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1027,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19138ab8208b0-cca9-5","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102542,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2542,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19138abaa7840-cca9-6","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102057,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102057,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2057,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19138b1cb1a90-cca9-7","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102070,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2070,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19138b7ee6878-cca9-8","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503492,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3492,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19138be112da8-cca9-9","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":500955,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":500955,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":955,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19138dc180998-cca9-a","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500839,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":839,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19138fa036a60-cca9-b","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1260,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":260,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913917f3fb48-cca9-c","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2296,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2296,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1296,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19139180b7700-cca9-d","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2286,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1286,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913918426968-cca9-e","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102470,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2470,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e191391871e5a8-cca9-f","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101640,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101640,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1640,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e191391e921e80-cca9-10","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101413,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1413,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913924b2bce8-cca9-11","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502276,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2276,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e191392ac96c58-cca9-12","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502067,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502067,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2067,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913948bf8350-cca9-13","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501485,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1485,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913966bf42f0-cca9-14","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1257,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":257,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913984b002b8-cca9-15","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2270,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2270,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1270,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913984c60388-cca9-16","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2061,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1061,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913984f2eba0-cca9-17","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":104844,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4844,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19139851a7c88-cca9-18","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":100771,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":100771,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":771,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e191398b5f7788-cca9-19","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101079,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1079,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19139916ff8f0-cca9-1a","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502679,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2679,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913997838f68-cca9-1b","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":500441,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":500441,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":441,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19139b58389a0-cca9-1c","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501722,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1722,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19139d3725180-cca9-1d","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1256,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":256,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19139f1694ef0-cca9-1e","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":1,"abi_return_us":2057,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2057,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1057,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19139f17f4020-cca9-1f","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2284,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1284,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19139f1aa7a88-cca9-20","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102180,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2180,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19139f1d708c8-cca9-21","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101205,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101205,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1205,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19139f7f27328-cca9-22","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102056,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2056,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19139fe097c70-cca9-23","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500124,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":124,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913a042abee8-cca9-24","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":2,"abi_return_us":502064,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502064,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2064,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913a21ff1d10-cca9-25","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501796,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1796,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913a3ffae8f8-cca9-26","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1015,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":15,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913a5df19848-cca9-27","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2041,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2041,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1041,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913a5e043db8-cca9-28","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2317,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1317,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913a5e2d7480-cca9-29","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103995,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3995,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913a5e59cff8-cca9-2a","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101129,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101129,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1129,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913a64911340-cca9-2b","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102135,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2135,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913a6aa770a8-cca9-2c","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500279,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":279,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913a70cba8f0-cca9-2d","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502046,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502046,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2046,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913a8ea27430-cca9-2e","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501595,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1595,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913aac9c34a8-cca9-2f","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1257,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":257,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913aca8fee28-cca9-30","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2052,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2052,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1052,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913acaa621c0-cca9-31","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2281,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1281,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913acacf9af0-cca9-32","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":105010,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":5010,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913acafbb7e8-cca9-33","within_five_ms":false}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101253,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101253,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1253,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913ad142f120-cca9-34","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101478,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1478,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913ad75a0a08-cca9-35","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500736,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":736,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913add74c888-cca9-36","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501535,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501535,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1535,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913afb51c9a0-cca9-37","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500890,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":890,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913b194d10b8-cca9-38","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1258,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":258,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913b37395410-cca9-39","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2270,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2270,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1270,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913b374f7bf0-cca9-3a","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2281,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1281,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913b377e2158-cca9-3b","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102439,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2439,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913b37aa6178-cca9-3c","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101296,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101296,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1296,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913b3dc9a820-cca9-3d","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101219,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1219,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913b43e0f3d0-cca9-3e","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502017,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2017,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913b49f7ce38-cca9-3f","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501237,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501237,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1237,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913b67e866f0-cca9-40","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501421,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1421,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913b85d81548-cca9-41","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1390,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":390,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913ba3c92718-cca9-42","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2284,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2284,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1284,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913ba3e26020-cca9-43","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2079,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1079,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913ba413cc78-cca9-44","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":105016,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":5016,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913ba43f8f98-cca9-45","within_five_ms":false}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102055,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102055,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2055,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913baa86a5a8-cca9-46","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102242,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2242,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913bb0ab0500-cca9-47","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504636,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4636,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1913bb6d046e8-cca9-48","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502097,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502097,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2097,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1913bd4e8ca60-cca9-49","within_five_ms":true}} +{"run":"repaired","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501633,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1633,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1913bf2e54de0-cca9-4a","within_five_ms":true}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":59303,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6402,"slot":0,"token":"18e1913c10dd7048-cca9-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913c10dd7048-cca9-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6410,"bytes":57,"kind":"head","slot":0},{"at_us":6415,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":1329,"operation":"dispatch","outcome":"pending","slot":0,"start_us":596},{"abi_enter_us":1367,"abi_return_us":6102,"end_us":6107,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1366,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":6662,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6149},{"bytes":1,"capacity":1,"end_us":6694,"operation":"read","slot":0,"start_us":6690},{"capacity":1,"end_us":59219,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":6725},{"end_us":59286,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59279}],"schema":2,"slots":[{"at_us":59286,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913c10dd7048-cca9-4b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":58825,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6151,"slot":0,"token":"18e1913c14973c28-cca9-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913c14973c28-cca9-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6161,"bytes":57,"kind":"head","slot":0},{"at_us":6172,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":88,"abi_return_us":5889,"end_us":5897,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5937,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5929},{"abi_enter_us":5951,"abi_return_us":5956,"end_us":5957,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5951,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5981,"operation":"read","slot":0,"start_us":5978},{"abi_enter_us":5995,"abi_return_us":58704,"end_us":58715,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5995,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":58781,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58776},{"end_us":58810,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58805}],"schema":2,"slots":[{"at_us":58810,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913c14973c28-cca9-4c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":57915,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6448,"slot":0,"token":"18e1913c183f9730-cca9-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913c183f9730-cca9-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6462,"bytes":57,"kind":"head","slot":0},{"at_us":6475,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":79,"abi_return_us":6230,"end_us":6236,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6282,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6276},{"abi_enter_us":6295,"abi_return_us":6299,"end_us":6300,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6295,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6331,"operation":"read","slot":0,"start_us":6329},{"abi_enter_us":6343,"abi_return_us":57781,"end_us":57785,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6343,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":57865,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57860},{"end_us":57898,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57892}],"schema":2,"slots":[{"at_us":57898,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913c183f9730-cca9-4d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":56732,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5191,"slot":0,"token":"18e1913c1bd92cf8-cca9-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913c1bd92cf8-cca9-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5198,"bytes":57,"kind":"head","slot":0},{"at_us":5203,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":88,"abi_return_us":4988,"end_us":4990,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5022,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5017},{"abi_enter_us":5034,"abi_return_us":5038,"end_us":5038,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5034,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5061,"operation":"read","slot":0,"start_us":5060},{"abi_enter_us":5072,"abi_return_us":56604,"end_us":56607,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5072,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":56693,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56688},{"end_us":56717,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56711}],"schema":2,"slots":[{"at_us":56717,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913c1bd92cf8-cca9-4e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":56729,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6467,"slot":0,"token":"18e1913c1f5fac08-cca9-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913c1f5fac08-cca9-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6482,"bytes":57,"kind":"head","slot":0},{"at_us":6490,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":95,"abi_return_us":6263,"end_us":6270,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6325,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6319},{"abi_enter_us":6338,"abi_return_us":6347,"end_us":6347,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6338,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6383,"operation":"read","slot":0,"start_us":6381},{"abi_enter_us":6407,"abi_return_us":56617,"end_us":56621,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6407,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":56693,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56688},{"end_us":56715,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56709}],"schema":2,"slots":[{"at_us":56715,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913c1f5fac08-cca9-4f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501298,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6453,"slot":0,"token":"18e1913c22ecf560-cca9-50"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913c22ecf560-cca9-50","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":99,"operation":"dispatch","outcome":"pending","slot":0,"start_us":31},{"abi_enter_us":142,"abi_return_us":501131,"end_us":501160,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":142,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501274,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501274}],"schema":2,"slots":[{"at_us":501274,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913c22ecf560-cca9-50","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":500594,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6514,"slot":0,"token":"18e1913c40f06268-cca9-51"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913c40f06268-cca9-51","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":92,"abi_return_us":500351,"end_us":500357,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500552,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500552}],"schema":2,"slots":[{"at_us":500552,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913c40f06268-cca9-51","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501089,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6414,"slot":0,"token":"18e1913c5ee41418-cca9-52"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913c5ee41418-cca9-52","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":82,"abi_return_us":500975,"end_us":500986,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501065,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501065}],"schema":2,"slots":[{"at_us":501065,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913c5ee41418-cca9-52","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501525,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5333,"slot":0,"token":"18e1913c7ce110b0-cca9-53"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913c7ce110b0-cca9-53","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":92,"abi_return_us":501297,"end_us":501344,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501495,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501495}],"schema":2,"slots":[{"at_us":501495,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913c7ce110b0-cca9-53","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":500474,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6405,"slot":0,"token":"18e1913c9ae98ab0-cca9-54"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1913c9ae98ab0-cca9-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":6416,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":74,"abi_return_us":6012,"end_us":6013,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6046,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6041},{"abi_enter_us":6059,"abi_return_us":500350,"end_us":500354,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6058,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500428,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500421}],"schema":2,"slots":[{"at_us":500428,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913c9ae98ab0-cca9-54","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501420,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5210,"slot":0,"token":"18e1913cb8e3ef38-cca9-55"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1913cb8e3ef38-cca9-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":5218,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":60,"abi_return_us":5012,"end_us":5016,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5054,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5048},{"abi_enter_us":5067,"abi_return_us":501324,"end_us":501329,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5066,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501401,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501393}],"schema":2,"slots":[{"at_us":501401,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913cb8e3ef38-cca9-55","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501421,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6429,"slot":0,"token":"18e1913cd6e3f910-cca9-56"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913cd6e3f910-cca9-56","watchdog":false,"workers_joined":1,"writes":[{"at_us":6437,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":80,"operation":"handoff","start_us":80},{"abi_enter_us":98,"abi_return_us":6199,"end_us":6203,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6245,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6236},{"abi_enter_us":6261,"abi_return_us":500724,"end_us":500732,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6260,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501035,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500932}],"schema":2,"slots":[{"at_us":501035,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913cd6e3f910-cca9-56","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501620,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5657,"slot":0,"token":"18e1913cf4f6ac40-cca9-57"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1913cf4f6ac40-cca9-57","watchdog":false,"workers_joined":1,"writes":[{"at_us":5667,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"abi_enter_us":104,"abi_return_us":5458,"end_us":5465,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5520,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5513},{"abi_enter_us":5534,"abi_return_us":501497,"end_us":501505,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5534,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501566,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501557}],"schema":2,"slots":[{"at_us":501566,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913cf4f6ac40-cca9-57","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":500872,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6000,"slot":0,"token":"18e1913d12fe14d0-cca9-58"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913d12fe14d0-cca9-58","watchdog":false,"workers_joined":1,"writes":[{"at_us":6031,"bytes":57,"kind":"head","slot":0},{"at_us":6050,"bytes":1,"kind":"body","slot":0},{"at_us":307680,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":72,"abi_return_us":5852,"end_us":5858,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5912,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5906},{"abi_enter_us":5927,"abi_return_us":5934,"end_us":5934,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5927,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5967,"operation":"read","slot":0,"start_us":5964},{"abi_enter_us":5979,"abi_return_us":307524,"end_us":307537,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5979,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307632,"operation":"read","slot":0,"start_us":307628},{"abi_enter_us":307649,"abi_return_us":500747,"end_us":500751,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307648,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500834,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500818}],"schema":2,"slots":[{"at_us":500834,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913d12fe14d0-cca9-58","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":500712,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6433,"slot":0,"token":"18e1913d31021e18-cca9-59"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913d31021e18-cca9-59","watchdog":false,"workers_joined":1,"writes":[{"at_us":6448,"bytes":57,"kind":"head","slot":0},{"at_us":6454,"bytes":1,"kind":"body","slot":0},{"at_us":309307,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":73,"abi_return_us":6227,"end_us":6233,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6285,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6279},{"abi_enter_us":6299,"abi_return_us":6303,"end_us":6304,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6299,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6334,"operation":"read","slot":0,"start_us":6332},{"abi_enter_us":6347,"abi_return_us":309071,"end_us":309078,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6347,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309176,"operation":"read","slot":0,"start_us":309167},{"abi_enter_us":309210,"abi_return_us":500620,"end_us":500623,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309210,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500678,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500671}],"schema":2,"slots":[{"at_us":500678,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913d31021e18-cca9-59","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":500918,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5715,"slot":0,"token":"18e1913d4f065e10-cca9-5a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913d4f065e10-cca9-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5723,"bytes":57,"kind":"head","slot":0},{"at_us":5730,"bytes":1,"kind":"body","slot":0},{"at_us":310316,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":102,"operation":"handoff","start_us":102},{"abi_enter_us":123,"abi_return_us":5435,"end_us":5439,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":123,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5489,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5484},{"abi_enter_us":5503,"abi_return_us":5507,"end_us":5507,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5503,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5534,"operation":"read","slot":0,"start_us":5529},{"abi_enter_us":5558,"abi_return_us":310013,"end_us":310017,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5558,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":310091,"operation":"read","slot":0,"start_us":310087},{"abi_enter_us":310105,"abi_return_us":500826,"end_us":500832,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310105,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":500898,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500889}],"schema":2,"slots":[{"at_us":500898,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913d4f065e10-cca9-5a","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":502894,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5201,"slot":0,"token":"18e1913d6d0b2e90-cca9-5b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913d6d0b2e90-cca9-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5211,"bytes":57,"kind":"head","slot":0},{"at_us":5217,"bytes":1,"kind":"body","slot":0},{"at_us":307018,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":75,"operation":"handoff","start_us":75},{"abi_enter_us":91,"abi_return_us":4991,"end_us":4993,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5026,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5021},{"abi_enter_us":5037,"abi_return_us":5041,"end_us":5042,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5037,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5072,"operation":"read","slot":0,"start_us":5070},{"abi_enter_us":5083,"abi_return_us":306834,"end_us":306839,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5083,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":306914,"operation":"read","slot":0,"start_us":306910},{"abi_enter_us":306927,"abi_return_us":501841,"end_us":501851,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306927,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":502854,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502843}],"schema":2,"slots":[{"at_us":502854,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913d6d0b2e90-cca9-5b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":501430,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6103,"slot":1,"token":"18e1913d8bc51e40-cca9-5c"},{"path":"/readiness/siblings/0","received_us":6929,"slot":0,"token":"18e1913d8bc51e40-cca9-5c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1913d8bc51e40-cca9-5c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6114,"bytes":57,"kind":"head","slot":1},{"at_us":6940,"bytes":57,"kind":"head","slot":0},{"at_us":12305,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":33},{"end_us":409,"operation":"dispatch","outcome":"pending","slot":1,"start_us":369},{"abi_enter_us":629,"abi_return_us":3411,"end_us":3418,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":629,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":3482,"operation":"head-poll","outcome":"ready","slot":1,"start_us":3475},{"abi_enter_us":3498,"abi_return_us":3610,"end_us":3612,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":3498,"status":"FastlyStatus::OK","timeout_ms":496},{"end_us":3642,"operation":"head-poll","outcome":"ready","slot":0,"start_us":3640},{"abi_enter_us":3657,"abi_return_us":9046,"end_us":9050,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":3656,"status":"FastlyStatus::OK","timeout_ms":496},{"bytes":1,"capacity":1,"end_us":9143,"operation":"read","slot":1,"start_us":9139},{"abi_enter_us":9158,"abi_return_us":9163,"end_us":9163,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":9158,"status":"FastlyStatus::OK","timeout_ms":490},{"bytes":1,"capacity":1,"end_us":9320,"operation":"read","slot":1,"start_us":9317},{"abi_enter_us":9342,"abi_return_us":9344,"end_us":9345,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":9341,"status":"FastlyStatus::OK","timeout_ms":490},{"bytes":1,"capacity":1,"end_us":9373,"operation":"read","slot":1,"start_us":9372},{"abi_enter_us":9384,"abi_return_us":9441,"end_us":9443,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":9384,"status":"FastlyStatus::OK","timeout_ms":490},{"bytes":0,"capacity":1,"end_us":9718,"operation":"read","slot":1,"start_us":9716},{"end_us":9859,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":9850},{"abi_enter_us":9952,"abi_return_us":501242,"end_us":501250,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":9951,"status":"FastlyStatus::OK","timeout_ms":490},{"end_us":501392,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501381}],"schema":2,"slots":[{"at_us":501392,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":9859,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913d8bc51e40-cca9-5c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":500822,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6663,"slot":0,"token":"18e1913daa240540-cca9-5d"},{"path":"/readiness/siblings/1","received_us":6739,"slot":1,"token":"18e1913daa240540-cca9-5d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1913daa240540-cca9-5d","watchdog":false,"workers_joined":2,"writes":[{"at_us":6696,"bytes":57,"kind":"head","slot":0},{"at_us":6754,"bytes":57,"kind":"head","slot":1},{"at_us":6764,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":135,"operation":"dispatch","outcome":"pending","slot":1,"start_us":103},{"abi_enter_us":163,"abi_return_us":6548,"end_us":6558,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":163,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6762,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6741},{"abi_enter_us":6832,"abi_return_us":6839,"end_us":6841,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6831,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6897,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6894},{"abi_enter_us":6913,"abi_return_us":6921,"end_us":6922,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6913,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6981,"operation":"read","slot":1,"start_us":6962},{"abi_enter_us":7016,"abi_return_us":7020,"end_us":7021,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":7016,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":16384,"end_us":7078,"operation":"read","slot":1,"start_us":7077},{"end_us":7094,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":7089},{"abi_enter_us":7111,"abi_return_us":500725,"end_us":500737,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":7111,"status":"FastlyStatus::OK","timeout_ms":492},{"end_us":500804,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500793}],"schema":2,"slots":[{"at_us":500804,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":7094,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913daa240540-cca9-5d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":500520,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5523,"slot":1,"token":"18e1913dc83414a8-cca9-5e"},{"path":"/readiness/siblings/0","received_us":5529,"slot":0,"token":"18e1913dc83414a8-cca9-5e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1913dc83414a8-cca9-5e","watchdog":false,"workers_joined":2,"writes":[{"at_us":5537,"bytes":57,"kind":"head","slot":1},{"at_us":5544,"bytes":57,"kind":"head","slot":0},{"at_us":11827,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":133,"operation":"dispatch","outcome":"pending","slot":1,"start_us":97},{"body_reads":0,"deadline_us":500000,"end_us":155,"operation":"handoff","start_us":155},{"abi_enter_us":170,"abi_return_us":5324,"end_us":5331,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":170,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5385,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5379},{"abi_enter_us":5401,"abi_return_us":5404,"end_us":5404,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5401,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5467,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5462},{"abi_enter_us":5485,"abi_return_us":11587,"end_us":11590,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5485,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":11657,"operation":"read","slot":1,"start_us":11653},{"abi_enter_us":11671,"abi_return_us":11675,"end_us":11675,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11671,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11699,"operation":"read","slot":1,"start_us":11697},{"abi_enter_us":11711,"abi_return_us":11713,"end_us":11713,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11711,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11734,"operation":"read","slot":1,"start_us":11733},{"abi_enter_us":11743,"abi_return_us":11746,"end_us":11746,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11743,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11772,"operation":"read","slot":1,"start_us":11772},{"end_us":11786,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11781},{"abi_enter_us":11803,"abi_return_us":500295,"end_us":500316,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11803,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500474,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500455}],"schema":2,"slots":[{"at_us":500474,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11786,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913dc83414a8-cca9-5e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":501913,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6491,"slot":0,"token":"18e1913de6d89fc8-cca9-5f"},{"path":"/readiness/siblings/1","received_us":6558,"slot":1,"token":"18e1913de6d89fc8-cca9-5f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1913de6d89fc8-cca9-5f","watchdog":false,"workers_joined":2,"writes":[{"at_us":6523,"bytes":57,"kind":"head","slot":0},{"at_us":6568,"bytes":57,"kind":"head","slot":1},{"at_us":6572,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":201,"operation":"dispatch","outcome":"pending","slot":0,"start_us":66},{"end_us":539,"operation":"dispatch","outcome":"pending","slot":1,"start_us":420},{"body_reads":0,"deadline_us":500000,"end_us":557,"operation":"handoff","start_us":557},{"abi_enter_us":570,"abi_return_us":5301,"end_us":5311,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":570,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5413,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5403},{"abi_enter_us":5457,"abi_return_us":5461,"end_us":5462,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5456,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5495,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5493},{"abi_enter_us":5519,"abi_return_us":5528,"end_us":5529,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5518,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5596,"operation":"read","slot":1,"start_us":5592},{"abi_enter_us":5623,"abi_return_us":5629,"end_us":5629,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5623,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5693,"operation":"read","slot":1,"start_us":5692},{"end_us":5723,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5716},{"abi_enter_us":5758,"abi_return_us":501646,"end_us":501678,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5758,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501847,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501828}],"schema":2,"slots":[{"at_us":501847,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5723,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913de6d89fc8-cca9-5f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":500963,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5858,"slot":0,"token":"18e1913e050d1780-cca9-60"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913e050d1780-cca9-60","watchdog":false,"workers_joined":1,"writes":[{"at_us":5879,"bytes":57,"kind":"head","slot":0},{"at_us":5885,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":94,"abi_return_us":5520,"end_us":5526,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5587,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5581},{"abi_enter_us":5603,"abi_return_us":5619,"end_us":5620,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5603,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5649,"operation":"read","slot":0,"start_us":5646},{"abi_enter_us":5662,"abi_return_us":500820,"end_us":500831,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5662,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500911,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500903}],"schema":2,"slots":[{"at_us":500911,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e050d1780-cca9-60","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":500927,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5356,"slot":0,"token":"18e1913e2330ca40-cca9-61"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1913e2330ca40-cca9-61","watchdog":false,"workers_joined":1,"writes":[{"at_us":5370,"bytes":57,"kind":"head","slot":0},{"at_us":5377,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":65,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"abi_enter_us":112,"abi_return_us":5087,"end_us":5093,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5141,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5134},{"abi_enter_us":5157,"abi_return_us":5164,"end_us":5164,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5157,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5199,"operation":"read","slot":0,"start_us":5196},{"abi_enter_us":5211,"abi_return_us":500571,"end_us":500617,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5211,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500851,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500832}],"schema":2,"slots":[{"at_us":500851,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e2330ca40-cca9-61","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":501447,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5751,"slot":0,"token":"18e1913e41680cd0-cca9-62"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1913e41680cd0-cca9-62","watchdog":false,"workers_joined":1,"writes":[{"at_us":5777,"bytes":57,"kind":"head","slot":0},{"at_us":5797,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":107,"operation":"handoff","start_us":107},{"abi_enter_us":130,"abi_return_us":5538,"end_us":5548,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":130,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5680,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5664},{"abi_enter_us":5736,"abi_return_us":5767,"end_us":5769,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5735,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5949,"operation":"read","slot":0,"start_us":5934},{"abi_enter_us":5981,"abi_return_us":501307,"end_us":501319,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5981,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501401,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501388}],"schema":2,"slots":[{"at_us":501401,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e41680cd0-cca9-62","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":501743,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5960,"slot":0,"token":"18e1913e5f76dbe8-cca9-63"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1913e5f76dbe8-cca9-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":5980,"bytes":57,"kind":"head","slot":0},{"at_us":5988,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":81,"operation":"handoff","start_us":81},{"abi_enter_us":94,"abi_return_us":5690,"end_us":5696,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5740,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5735},{"abi_enter_us":5755,"abi_return_us":5759,"end_us":5759,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5755,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5807,"operation":"read","slot":0,"start_us":5804},{"abi_enter_us":5822,"abi_return_us":501616,"end_us":501623,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5822,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501702,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501692}],"schema":2,"slots":[{"at_us":501702,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e5f76dbe8-cca9-63","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":5253,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5227,"slot":0,"token":"18e1913e7d85a718-cca9-64"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e7d85a718-cca9-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":5245,"bytes":57,"kind":"head","slot":0},{"at_us":5250,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":90,"abi_return_us":5014,"end_us":5020,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5072,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5062},{"abi_enter_us":5091,"abi_return_us":5097,"end_us":5098,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5091,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5125,"operation":"read","slot":0,"start_us":5122},{"abi_enter_us":5138,"abi_return_us":5139,"end_us":5139,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5138,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5163,"operation":"read","slot":0,"start_us":5162},{"abi_enter_us":5173,"abi_return_us":5174,"end_us":5174,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5173,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5194,"operation":"read","slot":0,"start_us":5193},{"abi_enter_us":5203,"abi_return_us":5206,"end_us":5206,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5203,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5227,"operation":"read","slot":0,"start_us":5226},{"end_us":5239,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5236}],"schema":2,"slots":[{"at_us":5239,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e7d85a718-cca9-64","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":6378,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6428,"slot":0,"token":"18e1913e7e01dbf8-cca9-65"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e7e01dbf8-cca9-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":6436,"bytes":57,"kind":"head","slot":0},{"at_us":6446,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":82,"abi_return_us":6211,"end_us":6217,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6259,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6254},{"abi_enter_us":6273,"abi_return_us":6283,"end_us":6283,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6272,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6311,"operation":"read","slot":0,"start_us":6309},{"abi_enter_us":6323,"abi_return_us":6326,"end_us":6326,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6323,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6352,"operation":"read","slot":0,"start_us":6352},{"end_us":6366,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6363}],"schema":2,"slots":[{"at_us":6366,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e7e01dbf8-cca9-65","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":5387,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5265,"slot":0,"token":"18e1913e7e876390-cca9-66"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e7e876390-cca9-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":5286,"bytes":57,"kind":"head","slot":0},{"at_us":5294,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":93,"abi_return_us":5113,"end_us":5121,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5174,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5167},{"abi_enter_us":5194,"abi_return_us":5200,"end_us":5201,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5194,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5229,"operation":"read","slot":0,"start_us":5226},{"abi_enter_us":5243,"abi_return_us":5244,"end_us":5244,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5243,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5269,"operation":"read","slot":0,"start_us":5267},{"abi_enter_us":5299,"abi_return_us":5300,"end_us":5300,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5299,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5322,"operation":"read","slot":0,"start_us":5321},{"abi_enter_us":5331,"abi_return_us":5334,"end_us":5334,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5331,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5353,"operation":"read","slot":0,"start_us":5352},{"end_us":5365,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5361}],"schema":2,"slots":[{"at_us":5365,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e7e876390-cca9-66","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":12419,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":8153,"slot":0,"token":"18e1913e7f1422d0-cca9-67"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e7f1422d0-cca9-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":8193,"bytes":57,"kind":"head","slot":0},{"at_us":8207,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":61,"operation":"dispatch","outcome":"pending","slot":0,"start_us":29},{"body_reads":0,"deadline_us":500000,"end_us":177,"operation":"handoff","start_us":177},{"abi_enter_us":191,"abi_return_us":9573,"end_us":9581,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":191,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":9682,"operation":"head-poll","outcome":"ready","slot":0,"start_us":9632},{"abi_enter_us":12133,"abi_return_us":12166,"end_us":12170,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":12132,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":3,"capacity":16384,"end_us":12270,"operation":"read","slot":0,"start_us":12265},{"abi_enter_us":12299,"abi_return_us":12304,"end_us":12305,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":12299,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":16384,"end_us":12358,"operation":"read","slot":0,"start_us":12357},{"end_us":12389,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":12382}],"schema":2,"slots":[{"at_us":12389,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e7f1422d0-cca9-67","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6325,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":8523,"slot":0,"token":"18e1913e808f35c8-cca9-68"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e808f35c8-cca9-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":8541,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":115,"operation":"dispatch","outcome":"pending","slot":0,"start_us":66},{"abi_enter_us":193,"abi_return_us":6187,"end_us":6193,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":193,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6249,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6243},{"abi_enter_us":6263,"abi_return_us":6267,"end_us":6268,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6263,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6292,"operation":"read","slot":0,"start_us":6290},{"end_us":6311,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6307}],"schema":2,"slots":[{"at_us":6311,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e808f35c8-cca9-68","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5494,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5657,"slot":0,"token":"18e1913e817910f8-cca9-69"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e817910f8-cca9-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":5673,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":80,"operation":"dispatch","outcome":"pending","slot":0,"start_us":37},{"abi_enter_us":639,"abi_return_us":5157,"end_us":5168,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":638,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5253,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5215},{"abi_enter_us":5337,"abi_return_us":5342,"end_us":5342,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5337,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5462,"operation":"read","slot":0,"start_us":5460},{"end_us":5480,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5475}],"schema":2,"slots":[{"at_us":5480,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e817910f8-cca9-69","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5160,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5602,"slot":0,"token":"18e1913e821e0388-cca9-6a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e821e0388-cca9-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5668,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":123,"operation":"dispatch","outcome":"pending","slot":0,"start_us":63},{"body_reads":0,"deadline_us":500000,"end_us":199,"operation":"handoff","start_us":199},{"abi_enter_us":226,"abi_return_us":5031,"end_us":5037,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":226,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5085,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5079},{"abi_enter_us":5099,"abi_return_us":5102,"end_us":5103,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5099,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5131,"operation":"read","slot":0,"start_us":5129},{"end_us":5147,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5143}],"schema":2,"slots":[{"at_us":5147,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e821e0388-cca9-6a","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5827,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6154,"slot":0,"token":"18e1913e82b85370-cca9-6b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e82b85370-cca9-6b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6173,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":89,"abi_return_us":5702,"end_us":5709,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5750,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5745},{"abi_enter_us":5763,"abi_return_us":5766,"end_us":5766,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5762,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5799,"operation":"read","slot":0,"start_us":5796},{"end_us":5814,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5810}],"schema":2,"slots":[{"at_us":5814,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e82b85370-cca9-6b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6350,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6536,"slot":0,"token":"18e1913e83464f18-cca9-6c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e83464f18-cca9-6c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6564,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":153,"operation":"dispatch","outcome":"pending","slot":0,"start_us":31},{"abi_enter_us":197,"abi_return_us":6274,"end_us":6280,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":197,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6320,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6315},{"end_us":6336,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6336}],"schema":2,"slots":[{"at_us":6336,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913e83464f18-cca9-6c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6381,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6471,"slot":0,"token":"18e1913e83caec50-cca9-6d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e83caec50-cca9-6d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6487,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":69,"abi_return_us":6300,"end_us":6307,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6349,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6344},{"end_us":6364,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6364}],"schema":2,"slots":[{"at_us":6364,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913e83caec50-cca9-6d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6356,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6432,"slot":0,"token":"18e1913e84494fc8-cca9-6e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e84494fc8-cca9-6e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6449,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":89,"abi_return_us":6282,"end_us":6289,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6327,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6321},{"end_us":6341,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6341}],"schema":2,"slots":[{"at_us":6341,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913e84494fc8-cca9-6e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6232,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6326,"slot":0,"token":"18e1913e84c9c680-cca9-6f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e84c9c680-cca9-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6345,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":95,"abi_return_us":6160,"end_us":6166,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6204,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6199},{"end_us":6217,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6217}],"schema":2,"slots":[{"at_us":6217,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913e84c9c680-cca9-6f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57212,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5322,"slot":0,"token":"18e1913e854908a0-cca9-70"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e854908a0-cca9-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":5332,"bytes":66,"kind":"head","slot":0},{"at_us":5340,"bytes":6,"kind":"body","slot":0},{"at_us":57195,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"abi_enter_us":77,"abi_return_us":5065,"end_us":5070,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5110,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5104},{"abi_enter_us":5124,"abi_return_us":5128,"end_us":5129,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5124,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5156,"operation":"read","slot":0,"start_us":5151},{"abi_enter_us":5169,"abi_return_us":56932,"end_us":56936,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5169,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":57168,"error":"Other","operation":"read","slot":0,"start_us":56989},{"end_us":57197,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57190}],"schema":2,"slots":[{"at_us":57197,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e854908a0-cca9-70","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57793,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5253,"slot":0,"token":"18e1913e88eb7420-cca9-71"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e88eb7420-cca9-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":5267,"bytes":66,"kind":"head","slot":0},{"at_us":5273,"bytes":6,"kind":"body","slot":0},{"at_us":57788,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":82,"abi_return_us":5041,"end_us":5048,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5095,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5089},{"abi_enter_us":5114,"abi_return_us":5122,"end_us":5123,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5114,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5173,"operation":"read","slot":0,"start_us":5169},{"abi_enter_us":5195,"abi_return_us":57590,"end_us":57594,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5195,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57748,"error":"Other","operation":"read","slot":0,"start_us":57743},{"end_us":57773,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57766}],"schema":2,"slots":[{"at_us":57773,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e88eb7420-cca9-71","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56791,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6683,"slot":0,"token":"18e1913e8ca0fe28-cca9-72"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e8ca0fe28-cca9-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":6716,"bytes":66,"kind":"head","slot":0},{"at_us":6728,"bytes":6,"kind":"body","slot":0},{"at_us":57035,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":89,"operation":"handoff","start_us":89},{"abi_enter_us":107,"abi_return_us":6394,"end_us":6419,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6504,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6494},{"abi_enter_us":6543,"abi_return_us":6557,"end_us":6557,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6542,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6610,"operation":"read","slot":0,"start_us":6606},{"abi_enter_us":6635,"abi_return_us":56677,"end_us":56681,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6635,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":56748,"error":"Other","operation":"read","slot":0,"start_us":56742},{"end_us":56772,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56765}],"schema":2,"slots":[{"at_us":56772,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e8ca0fe28-cca9-72","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":61691,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6241,"slot":0,"token":"18e1913e902e7278-cca9-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e902e7278-cca9-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":6264,"bytes":66,"kind":"head","slot":0},{"at_us":6271,"bytes":6,"kind":"body","slot":0},{"at_us":60847,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":93,"abi_return_us":6059,"end_us":6065,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6108,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6101},{"abi_enter_us":6125,"abi_return_us":6131,"end_us":6132,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6124,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6161,"operation":"read","slot":0,"start_us":6159},{"abi_enter_us":6172,"abi_return_us":61525,"end_us":61532,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6172,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":61648,"error":"Other","operation":"read","slot":0,"start_us":61643},{"end_us":61676,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61668}],"schema":2,"slots":[{"at_us":61676,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e902e7278-cca9-73","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5251,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5576,"slot":0,"token":"18e1913e94304ba8-cca9-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e94304ba8-cca9-74","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":103,"abi_return_us":5166,"end_us":5174,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5220,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5210},{"end_us":5236,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5236}],"schema":2,"slots":[{"at_us":5236,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913e94304ba8-cca9-74","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":6308,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6452,"slot":0,"token":"18e1913e94a83eb0-cca9-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e94a83eb0-cca9-75","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":73,"abi_return_us":6230,"end_us":6239,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6276,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6271},{"end_us":6290,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6290}],"schema":2,"slots":[{"at_us":6290,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913e94a83eb0-cca9-75","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5529,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5665,"slot":0,"token":"18e1913e95235a50-cca9-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e95235a50-cca9-76","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":89,"operation":"handoff","start_us":89},{"abi_enter_us":116,"abi_return_us":5450,"end_us":5457,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5495,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5490},{"end_us":5514,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5514}],"schema":2,"slots":[{"at_us":5514,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913e95235a50-cca9-76","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5182,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5283,"slot":0,"token":"18e1913e95966770-cca9-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e95966770-cca9-77","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":94,"abi_return_us":5108,"end_us":5116,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5152,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5147},{"end_us":5165,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5165}],"schema":2,"slots":[{"at_us":5165,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913e95966770-cca9-77","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":57761,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5590,"slot":0,"token":"18e1913e960123a8-cca9-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e960123a8-cca9-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":5604,"bytes":57,"kind":"head","slot":0},{"at_us":5610,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":72,"abi_return_us":5402,"end_us":5407,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5456,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5450},{"bytes":1,"capacity":1,"end_us":5483,"operation":"read","slot":0,"start_us":5478},{"capacity":1,"end_us":57695,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5506},{"end_us":57745,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57739}],"schema":2,"slots":[{"at_us":57745,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e960123a8-cca9-78","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":61236,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6453,"slot":0,"token":"18e1913e998fd460-cca9-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e998fd460-cca9-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":6467,"bytes":57,"kind":"head","slot":0},{"at_us":6473,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":74,"abi_return_us":6240,"end_us":6247,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6297,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6291},{"abi_enter_us":6310,"abi_return_us":6314,"end_us":6315,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6310,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6339,"operation":"read","slot":0,"start_us":6336},{"abi_enter_us":6353,"abi_return_us":61143,"end_us":61150,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6353,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":61202,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":61197},{"end_us":61223,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61217}],"schema":2,"slots":[{"at_us":61223,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e998fd460-cca9-79","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":57953,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5239,"slot":0,"token":"18e1913e9d5d95a0-cca9-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913e9d5d95a0-cca9-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5249,"bytes":57,"kind":"head","slot":0},{"at_us":5255,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":88,"abi_return_us":5027,"end_us":5034,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5083,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5077},{"abi_enter_us":5106,"abi_return_us":5114,"end_us":5114,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5105,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5143,"operation":"read","slot":0,"start_us":5140},{"abi_enter_us":5155,"abi_return_us":57831,"end_us":57835,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5155,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57910,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57905},{"end_us":57937,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57929}],"schema":2,"slots":[{"at_us":57937,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913e9d5d95a0-cca9-7a","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":58035,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5261,"slot":0,"token":"18e1913ea0f88710-cca9-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913ea0f88710-cca9-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5277,"bytes":57,"kind":"head","slot":0},{"at_us":5283,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":72,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":130,"operation":"handoff","start_us":130},{"abi_enter_us":145,"abi_return_us":5002,"end_us":5009,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":144,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5058,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5047},{"abi_enter_us":5073,"abi_return_us":5080,"end_us":5081,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5073,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5107,"operation":"read","slot":0,"start_us":5104},{"abi_enter_us":5123,"abi_return_us":57839,"end_us":57848,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5123,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":57975,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57969},{"end_us":58017,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58008}],"schema":2,"slots":[{"at_us":58017,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913ea0f88710-cca9-7b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":58590,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5553,"slot":0,"token":"18e1913ea4996420-cca9-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1913ea4996420-cca9-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5572,"bytes":57,"kind":"head","slot":0},{"at_us":5578,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":105,"operation":"handoff","start_us":105},{"abi_enter_us":159,"abi_return_us":5251,"end_us":5260,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":157,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5318,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5312},{"abi_enter_us":5333,"abi_return_us":5338,"end_us":5339,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5333,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5367,"operation":"read","slot":0,"start_us":5365},{"abi_enter_us":5384,"abi_return_us":58479,"end_us":58483,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5384,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":58552,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58547},{"end_us":58576,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58571}],"schema":2,"slots":[{"at_us":58576,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913ea4996420-cca9-7c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":500909,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6479,"slot":0,"token":"18e1913ea8424010-cca9-7d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913ea8424010-cca9-7d","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":87,"abi_return_us":500834,"end_us":500842,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500885,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500885}],"schema":2,"slots":[{"at_us":500885,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913ea8424010-cca9-7d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501567,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6321,"slot":0,"token":"18e1913ec643ff68-cca9-7e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913ec643ff68-cca9-7e","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":76,"abi_return_us":501333,"end_us":501384,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501522,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501522}],"schema":2,"slots":[{"at_us":501522,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913ec643ff68-cca9-7e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":500367,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6467,"slot":0,"token":"18e1913ee44760b8-cca9-7f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913ee44760b8-cca9-7f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":82,"operation":"handoff","start_us":82},{"abi_enter_us":97,"abi_return_us":500298,"end_us":500307,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500350,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500350}],"schema":2,"slots":[{"at_us":500350,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913ee44760b8-cca9-7f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":500536,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6069,"slot":0,"token":"18e1913f0238e3d0-cca9-80"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913f0238e3d0-cca9-80","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"abi_enter_us":87,"abi_return_us":500341,"end_us":500354,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500510,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500510}],"schema":2,"slots":[{"at_us":500510,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1913f0238e3d0-cca9-80","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501424,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5346,"slot":0,"token":"18e1913f202aa950-cca9-81"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1913f202aa950-cca9-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":5358,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":93,"abi_return_us":5153,"end_us":5160,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5230,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5220},{"abi_enter_us":5248,"abi_return_us":501320,"end_us":501330,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5247,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501401,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501394}],"schema":2,"slots":[{"at_us":501401,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913f202aa950-cca9-81","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501335,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5189,"slot":0,"token":"18e1913f3e2dc450-cca9-82"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1913f3e2dc450-cca9-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":5196,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":77,"abi_return_us":4967,"end_us":4969,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4995,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4990},{"abi_enter_us":5010,"abi_return_us":501230,"end_us":501235,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5010,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501310,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501303}],"schema":2,"slots":[{"at_us":501310,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913f3e2dc450-cca9-82","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":503488,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6419,"slot":0,"token":"18e1913f5c311dd0-cca9-83"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1913f5c311dd0-cca9-83","watchdog":false,"workers_joined":1,"writes":[{"at_us":6425,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":76,"operation":"handoff","start_us":76},{"abi_enter_us":88,"abi_return_us":6207,"end_us":6210,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6246,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6241},{"abi_enter_us":6262,"abi_return_us":502835,"end_us":502851,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6262,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":503132,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":503108}],"schema":2,"slots":[{"at_us":503132,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913f5c311dd0-cca9-83","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501133,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":7595,"slot":0,"token":"18e1913f7ab28ca8-cca9-84"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1913f7ab28ca8-cca9-84","watchdog":false,"workers_joined":1,"writes":[{"at_us":7609,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":146,"operation":"dispatch","outcome":"pending","slot":0,"start_us":37},{"body_reads":0,"deadline_us":500000,"end_us":1367,"operation":"handoff","start_us":1367},{"abi_enter_us":1408,"abi_return_us":3929,"end_us":3936,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1407,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":3990,"operation":"head-poll","outcome":"ready","slot":0,"start_us":3982},{"abi_enter_us":4006,"abi_return_us":501035,"end_us":501041,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4006,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501112,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501105}],"schema":2,"slots":[{"at_us":501112,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913f7ab28ca8-cca9-84","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":501510,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5217,"slot":0,"token":"18e1913f98ebc720-cca9-85"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913f98ebc720-cca9-85","watchdog":false,"workers_joined":1,"writes":[{"at_us":5226,"bytes":57,"kind":"head","slot":0},{"at_us":5232,"bytes":1,"kind":"body","slot":0},{"at_us":306471,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":104,"abi_return_us":4965,"end_us":4970,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5024,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5018},{"abi_enter_us":5040,"abi_return_us":5044,"end_us":5044,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5040,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5077,"operation":"read","slot":0,"start_us":5072},{"abi_enter_us":5090,"abi_return_us":306199,"end_us":306205,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5090,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":306281,"operation":"read","slot":0,"start_us":306277},{"abi_enter_us":306295,"abi_return_us":501414,"end_us":501418,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":306295,"status":"FastlyStatus::OK","timeout_ms":193},{"end_us":501487,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501479}],"schema":2,"slots":[{"at_us":501487,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913f98ebc720-cca9-85","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":505158,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5722,"slot":0,"token":"18e1913fb6fb74e0-cca9-86"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913fb6fb74e0-cca9-86","watchdog":false,"workers_joined":1,"writes":[{"at_us":5736,"bytes":57,"kind":"head","slot":0},{"at_us":5742,"bytes":1,"kind":"body","slot":0},{"at_us":309870,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":93,"abi_return_us":5535,"end_us":5540,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5599,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5594},{"abi_enter_us":5621,"abi_return_us":5627,"end_us":5627,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5621,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5660,"operation":"read","slot":0,"start_us":5658},{"abi_enter_us":5673,"abi_return_us":309652,"end_us":309666,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5673,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":309740,"operation":"read","slot":0,"start_us":309736},{"abi_enter_us":309756,"abi_return_us":502614,"end_us":502621,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309756,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":505117,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502707}],"schema":2,"slots":[{"at_us":505117,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913fb6fb74e0-cca9-86","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":500701,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6801,"slot":0,"token":"18e1913fd5b00d60-cca9-87"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913fd5b00d60-cca9-87","watchdog":false,"workers_joined":1,"writes":[{"at_us":6842,"bytes":57,"kind":"head","slot":0},{"at_us":6900,"bytes":1,"kind":"body","slot":0},{"at_us":310185,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":64,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":168,"operation":"handoff","start_us":168},{"abi_enter_us":252,"abi_return_us":4898,"end_us":4910,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":251,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5025,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5015},{"abi_enter_us":5059,"abi_return_us":5067,"end_us":5068,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5059,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5148,"operation":"read","slot":0,"start_us":5144},{"abi_enter_us":5180,"abi_return_us":308360,"end_us":308378,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5180,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":308705,"operation":"read","slot":0,"start_us":308699},{"abi_enter_us":308802,"abi_return_us":500457,"end_us":500492,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308802,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500623,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500603}],"schema":2,"slots":[{"at_us":500623,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913fd5b00d60-cca9-87","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":502472,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":7036,"slot":0,"token":"18e1913ff40b3b40-cca9-88"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1913ff40b3b40-cca9-88","watchdog":false,"workers_joined":1,"writes":[{"at_us":7102,"bytes":57,"kind":"head","slot":0},{"at_us":7140,"bytes":1,"kind":"body","slot":0},{"at_us":310546,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":101,"operation":"handoff","start_us":101},{"abi_enter_us":117,"abi_return_us":6594,"end_us":6602,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6706,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6674},{"abi_enter_us":6738,"abi_return_us":6765,"end_us":6766,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6738,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6919,"operation":"read","slot":0,"start_us":6913},{"abi_enter_us":6951,"abi_return_us":309849,"end_us":309855,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6951,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309931,"operation":"read","slot":0,"start_us":309925},{"abi_enter_us":309946,"abi_return_us":502391,"end_us":502395,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309946,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":502450,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502443}],"schema":2,"slots":[{"at_us":502450,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1913ff40b3b40-cca9-88","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":500940,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5900,"slot":0,"token":"18e1914012315348-cca9-89"},{"path":"/readiness/siblings/1","received_us":5928,"slot":1,"token":"18e1914012315348-cca9-89"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1914012315348-cca9-89","watchdog":false,"workers_joined":2,"writes":[{"at_us":5911,"bytes":57,"kind":"head","slot":0},{"at_us":5937,"bytes":57,"kind":"head","slot":1},{"at_us":5952,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":195,"operation":"dispatch","outcome":"pending","slot":1,"start_us":93},{"abi_enter_us":238,"abi_return_us":5715,"end_us":5721,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":238,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5770,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5764},{"abi_enter_us":5791,"abi_return_us":5793,"end_us":5793,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5790,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5811,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5810},{"abi_enter_us":5822,"abi_return_us":5825,"end_us":5825,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5822,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5856,"operation":"read","slot":1,"start_us":5853},{"abi_enter_us":5871,"abi_return_us":5873,"end_us":5873,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5871,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5897,"operation":"read","slot":1,"start_us":5896},{"abi_enter_us":5910,"abi_return_us":5911,"end_us":5911,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5910,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5939,"operation":"read","slot":1,"start_us":5938},{"abi_enter_us":5948,"abi_return_us":5951,"end_us":5951,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5948,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5976,"operation":"read","slot":1,"start_us":5976},{"end_us":5995,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5991},{"abi_enter_us":6009,"abi_return_us":500839,"end_us":500846,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6009,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500920,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500911}],"schema":2,"slots":[{"at_us":500920,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5995,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914012315348-cca9-89","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":501181,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5242,"slot":1,"token":"18e191403045fa78-cca9-8a"},{"path":"/readiness/siblings/0","received_us":5249,"slot":0,"token":"18e191403045fa78-cca9-8a"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e191403045fa78-cca9-8a","watchdog":false,"workers_joined":2,"writes":[{"at_us":5255,"bytes":57,"kind":"head","slot":1},{"at_us":5266,"bytes":57,"kind":"head","slot":0},{"at_us":10310,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"end_us":122,"operation":"dispatch","outcome":"pending","slot":1,"start_us":95},{"abi_enter_us":139,"abi_return_us":5032,"end_us":5034,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":138,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5071,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5066},{"abi_enter_us":5087,"abi_return_us":5089,"end_us":5089,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5087,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5106,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5105},{"abi_enter_us":5119,"abi_return_us":10071,"end_us":10073,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5119,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":10123,"operation":"read","slot":1,"start_us":10120},{"abi_enter_us":10140,"abi_return_us":10144,"end_us":10144,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10140,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":16384,"end_us":10170,"operation":"read","slot":1,"start_us":10169},{"end_us":10183,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10180},{"abi_enter_us":10198,"abi_return_us":500993,"end_us":501013,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":10198,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":501156,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501138}],"schema":2,"slots":[{"at_us":501156,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":10183,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191403045fa78-cca9-8a","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":503024,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5853,"slot":0,"token":"18e191404e58dc88-cca9-8b"},{"path":"/readiness/siblings/1","received_us":5862,"slot":1,"token":"18e191404e58dc88-cca9-8b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e191404e58dc88-cca9-8b","watchdog":false,"workers_joined":2,"writes":[{"at_us":5868,"bytes":57,"kind":"head","slot":0},{"at_us":5876,"bytes":57,"kind":"head","slot":1},{"at_us":5885,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":111,"operation":"dispatch","outcome":"pending","slot":1,"start_us":86},{"body_reads":0,"deadline_us":500000,"end_us":138,"operation":"handoff","start_us":138},{"abi_enter_us":150,"abi_return_us":5601,"end_us":5606,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":150,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5646,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5641},{"abi_enter_us":5663,"abi_return_us":5666,"end_us":5666,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5663,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5681,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5680},{"abi_enter_us":5692,"abi_return_us":5696,"end_us":5696,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5692,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5721,"operation":"read","slot":1,"start_us":5718},{"abi_enter_us":5736,"abi_return_us":5738,"end_us":5738,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5736,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5757,"operation":"read","slot":1,"start_us":5756},{"abi_enter_us":5767,"abi_return_us":5768,"end_us":5768,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5766,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5786,"operation":"read","slot":1,"start_us":5785},{"abi_enter_us":5798,"abi_return_us":5801,"end_us":5801,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5798,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5822,"operation":"read","slot":1,"start_us":5822},{"end_us":5833,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5830},{"abi_enter_us":5846,"abi_return_us":501823,"end_us":501847,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5846,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502539,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502518}],"schema":2,"slots":[{"at_us":502539,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5833,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191404e58dc88-cca9-8b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":503061,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6473,"slot":0,"token":"18e191406e2401a0-cca9-8c"},{"path":"/readiness/siblings/1","received_us":6515,"slot":1,"token":"18e191406e2401a0-cca9-8c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e191406e2401a0-cca9-8c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6490,"bytes":57,"kind":"head","slot":0},{"at_us":6522,"bytes":57,"kind":"head","slot":1},{"at_us":6527,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":1226,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"end_us":1495,"operation":"dispatch","outcome":"pending","slot":1,"start_us":1431},{"body_reads":0,"deadline_us":500000,"end_us":2824,"operation":"handoff","start_us":2824},{"abi_enter_us":2878,"abi_return_us":5036,"end_us":5042,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":2878,"status":"FastlyStatus::OK","timeout_ms":497},{"end_us":5093,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5086},{"abi_enter_us":5113,"abi_return_us":5116,"end_us":5117,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5113,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5138,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5136},{"abi_enter_us":5149,"abi_return_us":5154,"end_us":5155,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5149,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5253,"operation":"read","slot":1,"start_us":5250},{"abi_enter_us":5268,"abi_return_us":5271,"end_us":5271,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5267,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5300,"operation":"read","slot":1,"start_us":5299},{"end_us":6232,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6227},{"abi_enter_us":6728,"abi_return_us":502234,"end_us":502346,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6728,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":503038,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502611}],"schema":2,"slots":[{"at_us":503038,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6232,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191406e2401a0-cca9-8c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":502001,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6406,"slot":0,"token":"18e191408cdd6898-cca9-8d"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e191408cdd6898-cca9-8d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6427,"bytes":57,"kind":"head","slot":0},{"at_us":6432,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":246,"operation":"dispatch","outcome":"pending","slot":0,"start_us":109},{"abi_enter_us":337,"abi_return_us":5774,"end_us":5782,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":336,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5985,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5967},{"abi_enter_us":6002,"abi_return_us":6011,"end_us":6012,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6002,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6044,"operation":"read","slot":0,"start_us":6041},{"abi_enter_us":6057,"abi_return_us":501494,"end_us":501554,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6057,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501953,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501899}],"schema":2,"slots":[{"at_us":501953,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191408cdd6898-cca9-8d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":503047,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6855,"slot":0,"token":"18e19140ab0bb630-cca9-8e"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19140ab0bb630-cca9-8e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6999,"bytes":57,"kind":"head","slot":0},{"at_us":7048,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":80,"operation":"dispatch","outcome":"pending","slot":0,"start_us":31},{"abi_enter_us":161,"abi_return_us":6336,"end_us":6343,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":161,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6416,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6408},{"abi_enter_us":6431,"abi_return_us":6437,"end_us":6437,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6431,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6469,"operation":"read","slot":0,"start_us":6466},{"abi_enter_us":6489,"abi_return_us":502909,"end_us":502923,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6489,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":503025,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":503006}],"schema":2,"slots":[{"at_us":503025,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19140ab0bb630-cca9-8e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":501823,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":11954,"slot":0,"token":"18e19140c9a44ad0-cca9-8f"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19140c9a44ad0-cca9-8f","watchdog":false,"workers_joined":1,"writes":[{"at_us":11997,"bytes":57,"kind":"head","slot":0},{"at_us":12012,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":176,"operation":"dispatch","outcome":"pending","slot":0,"start_us":86},{"body_reads":0,"deadline_us":500000,"end_us":262,"operation":"handoff","start_us":262},{"abi_enter_us":288,"abi_return_us":8644,"end_us":8650,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":288,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":8699,"operation":"head-poll","outcome":"ready","slot":0,"start_us":8688},{"abi_enter_us":8714,"abi_return_us":8719,"end_us":8719,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":8714,"status":"FastlyStatus::OK","timeout_ms":491},{"bytes":1,"capacity":1,"end_us":8745,"operation":"read","slot":0,"start_us":8742},{"abi_enter_us":8757,"abi_return_us":501721,"end_us":501728,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":8756,"status":"FastlyStatus::OK","timeout_ms":491},{"end_us":501803,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501795}],"schema":2,"slots":[{"at_us":501803,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19140c9a44ad0-cca9-8f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":502550,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6523,"slot":0,"token":"18e19140e8247d40-cca9-90"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e19140e8247d40-cca9-90","watchdog":false,"workers_joined":1,"writes":[{"at_us":6543,"bytes":57,"kind":"head","slot":0},{"at_us":6551,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"body_reads":0,"deadline_us":500000,"end_us":143,"operation":"handoff","start_us":143},{"abi_enter_us":229,"abi_return_us":6154,"end_us":6161,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":229,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6237,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6230},{"abi_enter_us":6261,"abi_return_us":6270,"end_us":6271,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6260,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6316,"operation":"read","slot":0,"start_us":6312},{"abi_enter_us":6329,"abi_return_us":502314,"end_us":502323,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6328,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":502483,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502473}],"schema":2,"slots":[{"at_us":502483,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19140e8247d40-cca9-90","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":7256,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":7498,"slot":0,"token":"18e1914106ea53d0-cca9-91"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1914106ea53d0-cca9-91","watchdog":false,"workers_joined":1,"writes":[{"at_us":7578,"bytes":57,"kind":"head","slot":0},{"at_us":7590,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":106,"operation":"dispatch","outcome":"pending","slot":0,"start_us":40},{"abi_enter_us":222,"abi_return_us":6753,"end_us":6763,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":221,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6864,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6854},{"abi_enter_us":6891,"abi_return_us":6901,"end_us":6902,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6891,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":7032,"operation":"read","slot":0,"start_us":7026},{"abi_enter_us":7048,"abi_return_us":7053,"end_us":7054,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7047,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7149,"operation":"read","slot":0,"start_us":7147},{"abi_enter_us":7162,"abi_return_us":7170,"end_us":7171,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7162,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7195,"operation":"read","slot":0,"start_us":7194},{"abi_enter_us":7206,"abi_return_us":7209,"end_us":7210,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7205,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":1,"end_us":7229,"operation":"read","slot":0,"start_us":7228},{"end_us":7242,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":7237}],"schema":2,"slots":[{"at_us":7242,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914106ea53d0-cca9-91","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":5301,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5605,"slot":0,"token":"18e191410834f470-cca9-92"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191410834f470-cca9-92","watchdog":false,"workers_joined":1,"writes":[{"at_us":5622,"bytes":57,"kind":"head","slot":0},{"at_us":5628,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":297,"operation":"dispatch","outcome":"pending","slot":0,"start_us":41},{"abi_enter_us":383,"abi_return_us":4729,"end_us":4739,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":382,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4878,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4867},{"abi_enter_us":4913,"abi_return_us":4923,"end_us":4924,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4912,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":3,"capacity":16384,"end_us":5049,"operation":"read","slot":0,"start_us":5044},{"abi_enter_us":5077,"abi_return_us":5084,"end_us":5085,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5077,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5142,"operation":"read","slot":0,"start_us":5141},{"end_us":5170,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5163}],"schema":2,"slots":[{"at_us":5170,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191410834f470-cca9-92","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":5584,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6319,"slot":0,"token":"18e1914109044a90-cca9-93"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1914109044a90-cca9-93","watchdog":false,"workers_joined":1,"writes":[{"at_us":6356,"bytes":57,"kind":"head","slot":0},{"at_us":6369,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":143,"operation":"dispatch","outcome":"pending","slot":0,"start_us":84},{"body_reads":0,"deadline_us":500000,"end_us":213,"operation":"handoff","start_us":213},{"abi_enter_us":239,"abi_return_us":5280,"end_us":5291,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":238,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5344,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5338},{"abi_enter_us":5378,"abi_return_us":5390,"end_us":5392,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5378,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5443,"operation":"read","slot":0,"start_us":5439},{"abi_enter_us":5456,"abi_return_us":5459,"end_us":5460,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5456,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5483,"operation":"read","slot":0,"start_us":5482},{"abi_enter_us":5497,"abi_return_us":5499,"end_us":5499,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5497,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5519,"operation":"read","slot":0,"start_us":5518},{"abi_enter_us":5528,"abi_return_us":5531,"end_us":5531,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5528,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5550,"operation":"read","slot":0,"start_us":5550},{"end_us":5564,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5559}],"schema":2,"slots":[{"at_us":5564,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914109044a90-cca9-93","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":5176,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5379,"slot":0,"token":"18e1914109e64df0-cca9-94"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1914109e64df0-cca9-94","watchdog":false,"workers_joined":1,"writes":[{"at_us":5392,"bytes":57,"kind":"head","slot":0},{"at_us":5398,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":79,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":112,"operation":"handoff","start_us":112},{"abi_enter_us":172,"abi_return_us":4959,"end_us":4967,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":172,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5036,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5022},{"abi_enter_us":5057,"abi_return_us":5066,"end_us":5066,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5057,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5105,"operation":"read","slot":0,"start_us":5102},{"abi_enter_us":5116,"abi_return_us":5119,"end_us":5120,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5116,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5147,"operation":"read","slot":0,"start_us":5146},{"end_us":5162,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5157}],"schema":2,"slots":[{"at_us":5162,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914109e64df0-cca9-94","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5230,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5356,"slot":0,"token":"18e191410a74eda8-cca9-95"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191410a74eda8-cca9-95","watchdog":false,"workers_joined":1,"writes":[{"at_us":5383,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"abi_enter_us":97,"abi_return_us":5094,"end_us":5102,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5151,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5145},{"abi_enter_us":5166,"abi_return_us":5170,"end_us":5170,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5166,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5200,"operation":"read","slot":0,"start_us":5197},{"end_us":5216,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5212}],"schema":2,"slots":[{"at_us":5216,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191410a74eda8-cca9-95","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6303,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6490,"slot":0,"token":"18e191410af49940-cca9-96"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191410af49940-cca9-96","watchdog":false,"workers_joined":1,"writes":[{"at_us":6503,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":149,"abi_return_us":6174,"end_us":6179,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":149,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6226,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6220},{"abi_enter_us":6240,"abi_return_us":6243,"end_us":6244,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6240,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6274,"operation":"read","slot":0,"start_us":6271},{"end_us":6289,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6285}],"schema":2,"slots":[{"at_us":6289,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191410af49940-cca9-96","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5123,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5218,"slot":0,"token":"18e191410b8a9b98-cca9-97"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191410b8a9b98-cca9-97","watchdog":false,"workers_joined":1,"writes":[{"at_us":5230,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":68,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":100,"operation":"handoff","start_us":100},{"abi_enter_us":115,"abi_return_us":5017,"end_us":5021,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":115,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5056,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5051},{"abi_enter_us":5068,"abi_return_us":5074,"end_us":5074,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5068,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5098,"operation":"read","slot":0,"start_us":5096},{"end_us":5111,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5108}],"schema":2,"slots":[{"at_us":5111,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191410b8a9b98-cca9-97","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5463,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5505,"slot":0,"token":"18e191410c0ed728-cca9-98"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191410c0ed728-cca9-98","watchdog":false,"workers_joined":1,"writes":[{"at_us":5516,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"abi_enter_us":88,"abi_return_us":5333,"end_us":5338,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5383,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5377},{"abi_enter_us":5399,"abi_return_us":5402,"end_us":5402,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5398,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5435,"operation":"read","slot":0,"start_us":5433},{"end_us":5450,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5447}],"schema":2,"slots":[{"at_us":5450,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191410c0ed728-cca9-98","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6267,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6424,"slot":0,"token":"18e191410c981bc8-cca9-99"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191410c981bc8-cca9-99","watchdog":false,"workers_joined":1,"writes":[{"at_us":6431,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":90,"abi_return_us":6204,"end_us":6209,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6238,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6234},{"end_us":6252,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6251}],"schema":2,"slots":[{"at_us":6252,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191410c981bc8-cca9-99","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5919,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6017,"slot":0,"token":"18e191410d274c08-cca9-9a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191410d274c08-cca9-9a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6022,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":49,"abi_return_us":5873,"end_us":5875,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":49,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5894,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5892},{"end_us":5906,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5906}],"schema":2,"slots":[{"at_us":5906,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191410d274c08-cca9-9a","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6351,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6429,"slot":0,"token":"18e191410daecf70-cca9-9b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191410daecf70-cca9-9b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6437,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":74,"abi_return_us":6290,"end_us":6293,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6321,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6316},{"end_us":6334,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6334}],"schema":2,"slots":[{"at_us":6334,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191410daecf70-cca9-9b","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5048,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5173,"slot":0,"token":"18e191410e3ef9b0-cca9-9c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191410e3ef9b0-cca9-9c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5180,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":82,"abi_return_us":5000,"end_us":5001,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5022,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5020},{"end_us":5035,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5034}],"schema":2,"slots":[{"at_us":5035,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191410e3ef9b0-cca9-9c","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56974,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6424,"slot":0,"token":"18e191410ebd0b20-cca9-9d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191410ebd0b20-cca9-9d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6440,"bytes":66,"kind":"head","slot":0},{"at_us":6446,"bytes":6,"kind":"body","slot":0},{"at_us":57038,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":69,"abi_return_us":6244,"end_us":6250,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6290,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6284},{"abi_enter_us":6303,"abi_return_us":6307,"end_us":6307,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6303,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6333,"operation":"read","slot":0,"start_us":6330},{"abi_enter_us":6348,"abi_return_us":56824,"end_us":56829,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6348,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":56932,"error":"Other","operation":"read","slot":0,"start_us":56893},{"end_us":56958,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56952}],"schema":2,"slots":[{"at_us":56958,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191410ebd0b20-cca9-9d","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56739,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6442,"slot":0,"token":"18e191411267bfb8-cca9-9e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191411267bfb8-cca9-9e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6454,"bytes":66,"kind":"head","slot":0},{"at_us":6461,"bytes":6,"kind":"body","slot":0},{"at_us":56762,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":89,"abi_return_us":6165,"end_us":6171,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6218,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6212},{"abi_enter_us":6232,"abi_return_us":6236,"end_us":6237,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6232,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6269,"operation":"read","slot":0,"start_us":6266},{"abi_enter_us":6281,"abi_return_us":56540,"end_us":56545,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6281,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":56662,"error":"Other","operation":"read","slot":0,"start_us":56655},{"end_us":56704,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56695}],"schema":2,"slots":[{"at_us":56704,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191411267bfb8-cca9-9e","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57884,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6260,"slot":0,"token":"18e19141161e4f78-cca9-9f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19141161e4f78-cca9-9f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6277,"bytes":66,"kind":"head","slot":0},{"at_us":6283,"bytes":6,"kind":"body","slot":0},{"at_us":58137,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":83,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":152,"operation":"handoff","start_us":152},{"abi_enter_us":168,"abi_return_us":5956,"end_us":5963,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":167,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6021,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6014},{"abi_enter_us":6036,"abi_return_us":6092,"end_us":6094,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6036,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6131,"operation":"read","slot":0,"start_us":6128},{"abi_enter_us":6145,"abi_return_us":57770,"end_us":57776,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6145,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":57840,"error":"Other","operation":"read","slot":0,"start_us":57834},{"end_us":57868,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57861}],"schema":2,"slots":[{"at_us":57868,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19141161e4f78-cca9-9f","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59848,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6137,"slot":0,"token":"18e1914119dc0358-cca9-a0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1914119dc0358-cca9-a0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6149,"bytes":66,"kind":"head","slot":0},{"at_us":6156,"bytes":6,"kind":"body","slot":0},{"at_us":59937,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":80,"operation":"handoff","start_us":80},{"abi_enter_us":95,"abi_return_us":5908,"end_us":5915,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5964,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5957},{"abi_enter_us":5977,"abi_return_us":5989,"end_us":5989,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5977,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":6019,"operation":"read","slot":0,"start_us":6017},{"abi_enter_us":6032,"abi_return_us":59735,"end_us":59738,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6032,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":59806,"error":"Other","operation":"read","slot":0,"start_us":59801},{"end_us":59832,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59826}],"schema":2,"slots":[{"at_us":59832,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914119dc0358-cca9-a0","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5124,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5222,"slot":0,"token":"18e191411db14290-cca9-a1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191411db14290-cca9-a1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":78,"abi_return_us":5057,"end_us":5061,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5091,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5086},{"end_us":5105,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5105}],"schema":2,"slots":[{"at_us":5105,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191411db14290-cca9-a1","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5294,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5354,"slot":0,"token":"18e191411e197e28-cca9-a2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191411e197e28-cca9-a2","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":79,"abi_return_us":5189,"end_us":5193,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5225,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5222},{"end_us":5272,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5272}],"schema":2,"slots":[{"at_us":5272,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191411e197e28-cca9-a2","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5195,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5296,"slot":0,"token":"18e191411e87e7c8-cca9-a3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191411e87e7c8-cca9-a3","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":97,"abi_return_us":5126,"end_us":5131,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":97,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5165,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5160},{"end_us":5180,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5180}],"schema":2,"slots":[{"at_us":5180,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191411e87e7c8-cca9-a3","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5194,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5323,"slot":0,"token":"18e191411ef4b358-cca9-a4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191411ef4b358-cca9-a4","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":72,"abi_return_us":5143,"end_us":5145,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5169,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5166},{"end_us":5181,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5181}],"schema":2,"slots":[{"at_us":5181,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191411ef4b358-cca9-a4","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":57321,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6206,"slot":0,"token":"18e191411f617330-cca9-a5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191411f617330-cca9-a5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6214,"bytes":57,"kind":"head","slot":0},{"at_us":6218,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":70,"abi_return_us":5985,"end_us":5989,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6026,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6020},{"bytes":1,"capacity":1,"end_us":6053,"operation":"read","slot":0,"start_us":6049},{"capacity":1,"end_us":57238,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":6072},{"end_us":57301,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57295}],"schema":2,"slots":[{"at_us":57301,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191411f617330-cca9-a5","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":59427,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5760,"slot":0,"token":"18e1914122f0bc40-cca9-a6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1914122f0bc40-cca9-a6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5773,"bytes":57,"kind":"head","slot":0},{"at_us":5779,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":84,"abi_return_us":5481,"end_us":5484,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5521,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5515},{"abi_enter_us":5535,"abi_return_us":5541,"end_us":5541,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5535,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5565,"operation":"read","slot":0,"start_us":5563},{"abi_enter_us":5582,"abi_return_us":59316,"end_us":59321,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5582,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":59386,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59381},{"end_us":59410,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59403}],"schema":2,"slots":[{"at_us":59410,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914122f0bc40-cca9-a6","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":56613,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5953,"slot":0,"token":"18e1914126a59298-cca9-a7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1914126a59298-cca9-a7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5967,"bytes":57,"kind":"head","slot":0},{"at_us":5980,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":81,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":159,"abi_return_us":5544,"end_us":5550,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":159,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5602,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5594},{"abi_enter_us":5617,"abi_return_us":5624,"end_us":5624,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5616,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5653,"operation":"read","slot":0,"start_us":5650},{"abi_enter_us":5667,"abi_return_us":56489,"end_us":56493,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5667,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56566,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56561},{"end_us":56595,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56586}],"schema":2,"slots":[{"at_us":56595,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914126a59298-cca9-a7","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":55560,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5327,"slot":0,"token":"18e191412a2e2100-cca9-a8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191412a2e2100-cca9-a8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5339,"bytes":57,"kind":"head","slot":0},{"at_us":5350,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":100,"abi_return_us":5118,"end_us":5125,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":100,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5173,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5166},{"abi_enter_us":5187,"abi_return_us":5191,"end_us":5192,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5187,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5231,"operation":"read","slot":0,"start_us":5229},{"abi_enter_us":5243,"abi_return_us":55442,"end_us":55448,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5243,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":55519,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":55514},{"end_us":55543,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55537}],"schema":2,"slots":[{"at_us":55543,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191412a2e2100-cca9-a8","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"truncated","completed_us":56640,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5930,"slot":0,"token":"18e191412da68e80-cca9-a9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191412da68e80-cca9-a9","watchdog":false,"workers_joined":1,"writes":[{"at_us":5938,"bytes":57,"kind":"head","slot":0},{"at_us":5944,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":87,"operation":"handoff","start_us":87},{"abi_enter_us":141,"abi_return_us":5681,"end_us":5685,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":141,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5720,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5715},{"abi_enter_us":5734,"abi_return_us":5740,"end_us":5740,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5734,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5767,"operation":"read","slot":0,"start_us":5764},{"abi_enter_us":5778,"abi_return_us":56534,"end_us":56538,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5777,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56603,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56598},{"end_us":56625,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56619}],"schema":2,"slots":[{"at_us":56625,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191412da68e80-cca9-a9","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501302,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6441,"slot":0,"token":"18e19141312f47e0-cca9-aa"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e19141312f47e0-cca9-aa","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":78,"abi_return_us":501216,"end_us":501226,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501285,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501285}],"schema":2,"slots":[{"at_us":501285,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19141312f47e0-cca9-aa","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501786,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6435,"slot":0,"token":"18e191414f324f58-cca9-ab"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191414f324f58-cca9-ab","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":76,"abi_return_us":499333,"end_us":499346,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"abi_enter_us":499459,"abi_return_us":501739,"end_us":501741,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499459,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501768,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501768}],"schema":2,"slots":[{"at_us":501768,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191414f324f58-cca9-ab","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":501228,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5524,"slot":0,"token":"18e191416d397968-cca9-ac"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191416d397968-cca9-ac","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":89,"abi_return_us":501139,"end_us":501149,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501210,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501210}],"schema":2,"slots":[{"at_us":501210,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191416d397968-cca9-ac","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"silent-head","completed_us":500516,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6453,"slot":0,"token":"18e191418b3bc948-cca9-ad"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191418b3bc948-cca9-ad","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":85,"operation":"handoff","start_us":85},{"abi_enter_us":100,"abi_return_us":500415,"end_us":500427,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":100,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500495,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500495}],"schema":2,"slots":[{"at_us":500495,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191418b3bc948-cca9-ad","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":500675,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5624,"slot":0,"token":"18e19141a92d92b0-cca9-ae"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19141a92d92b0-cca9-ae","watchdog":false,"workers_joined":1,"writes":[{"at_us":5635,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":67,"abi_return_us":5437,"end_us":5442,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5495,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5490},{"abi_enter_us":5511,"abi_return_us":500543,"end_us":500549,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5511,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500652,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500639}],"schema":2,"slots":[{"at_us":500652,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19141a92d92b0-cca9-ae","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":500816,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6535,"slot":0,"token":"18e19141c72b2f70-cca9-af"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19141c72b2f70-cca9-af","watchdog":false,"workers_joined":1,"writes":[{"at_us":6576,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":80,"operation":"dispatch","outcome":"pending","slot":0,"start_us":40},{"abi_enter_us":122,"abi_return_us":6277,"end_us":6284,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":122,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6360,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6353},{"abi_enter_us":6375,"abi_return_us":500733,"end_us":500738,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6374,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500796,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500790}],"schema":2,"slots":[{"at_us":500796,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19141c72b2f70-cca9-af","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":500551,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5582,"slot":0,"token":"18e19141e528bc90-cca9-b0"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19141e528bc90-cca9-b0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5595,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":87,"operation":"handoff","start_us":87},{"abi_enter_us":102,"abi_return_us":5347,"end_us":5354,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5413,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5408},{"abi_enter_us":5428,"abi_return_us":500425,"end_us":500431,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5427,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500517,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500496}],"schema":2,"slots":[{"at_us":500517,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19141e528bc90-cca9-b0","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"pending-body","completed_us":501447,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6441,"slot":0,"token":"18e19142031e1808-cca9-b1"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e19142031e1808-cca9-b1","watchdog":false,"workers_joined":1,"writes":[{"at_us":6453,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":113,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":135,"operation":"handoff","start_us":135},{"abi_enter_us":147,"abi_return_us":6293,"end_us":6299,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":147,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6346,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6339},{"abi_enter_us":6361,"abi_return_us":501344,"end_us":501359,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6361,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501424,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501412}],"schema":2,"slots":[{"at_us":501424,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19142031e1808-cca9-b1","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":500148,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5259,"slot":0,"token":"18e1914221373ba8-cca9-b2"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1914221373ba8-cca9-b2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5276,"bytes":57,"kind":"head","slot":0},{"at_us":5283,"bytes":1,"kind":"body","slot":0},{"at_us":305521,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":82,"abi_return_us":5088,"end_us":5094,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5135,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5129},{"abi_enter_us":5153,"abi_return_us":5160,"end_us":5160,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5153,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5187,"operation":"read","slot":0,"start_us":5184},{"abi_enter_us":5200,"abi_return_us":305334,"end_us":305346,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5200,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":310454,"operation":"read","slot":0,"start_us":310449},{"abi_enter_us":310496,"abi_return_us":500026,"end_us":500031,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310496,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":500122,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500105}],"schema":2,"slots":[{"at_us":500122,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914221373ba8-cca9-b2","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":501374,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5486,"slot":0,"token":"18e191423f56bc30-cca9-b3"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191423f56bc30-cca9-b3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5500,"bytes":57,"kind":"head","slot":0},{"at_us":5506,"bytes":1,"kind":"body","slot":0},{"at_us":308048,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":73,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":117,"abi_return_us":5183,"end_us":5188,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5232,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5226},{"abi_enter_us":5250,"abi_return_us":5257,"end_us":5258,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5250,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5287,"operation":"read","slot":0,"start_us":5284},{"abi_enter_us":5305,"abi_return_us":307691,"end_us":307697,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5304,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":307765,"operation":"read","slot":0,"start_us":307761},{"abi_enter_us":307783,"abi_return_us":501230,"end_us":501240,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307783,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501319,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501309}],"schema":2,"slots":[{"at_us":501319,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191423f56bc30-cca9-b3","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":501060,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5697,"slot":0,"token":"18e191425d825278-cca9-b4"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191425d825278-cca9-b4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5715,"bytes":57,"kind":"head","slot":0},{"at_us":5723,"bytes":1,"kind":"body","slot":0},{"at_us":313646,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":92,"operation":"handoff","start_us":92},{"abi_enter_us":110,"abi_return_us":5407,"end_us":5413,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5462,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5456},{"abi_enter_us":5478,"abi_return_us":5482,"end_us":5483,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5478,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5513,"operation":"read","slot":0,"start_us":5511},{"abi_enter_us":5526,"abi_return_us":313386,"end_us":313395,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5526,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":313511,"operation":"read","slot":0,"start_us":313506},{"abi_enter_us":313545,"abi_return_us":500962,"end_us":500966,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":313544,"status":"FastlyStatus::OK","timeout_ms":186},{"end_us":501039,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501029}],"schema":2,"slots":[{"at_us":501039,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191425d825278-cca9-b4","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"trickle","completed_us":500272,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5201,"slot":0,"token":"18e191427bb4a368-cca9-b5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191427bb4a368-cca9-b5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5209,"bytes":57,"kind":"head","slot":0},{"at_us":5215,"bytes":1,"kind":"body","slot":0},{"at_us":305293,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":80,"operation":"handoff","start_us":80},{"abi_enter_us":92,"abi_return_us":4920,"end_us":4924,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4961,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4956},{"abi_enter_us":4975,"abi_return_us":4980,"end_us":4980,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4975,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5012,"operation":"read","slot":0,"start_us":5010},{"abi_enter_us":5024,"abi_return_us":304996,"end_us":305003,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5024,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":305079,"operation":"read","slot":0,"start_us":305076},{"abi_enter_us":305094,"abi_return_us":500083,"end_us":500087,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":305094,"status":"FastlyStatus::OK","timeout_ms":194},{"end_us":500245,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500236}],"schema":2,"slots":[{"at_us":500245,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191427bb4a368-cca9-b5","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":501354,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6610,"slot":0,"token":"18e1914299d1d230-cca9-b6"},{"path":"/readiness/siblings/1","received_us":6615,"slot":1,"token":"18e1914299d1d230-cca9-b6"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1914299d1d230-cca9-b6","watchdog":false,"workers_joined":2,"writes":[{"at_us":6637,"bytes":57,"kind":"head","slot":1},{"at_us":6722,"bytes":57,"kind":"head","slot":0},{"at_us":12905,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":217,"operation":"dispatch","outcome":"pending","slot":1,"start_us":162},{"abi_enter_us":301,"abi_return_us":6411,"end_us":6417,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":301,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6474,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6467},{"abi_enter_us":6492,"abi_return_us":6495,"end_us":6496,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6492,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6516,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6515},{"abi_enter_us":6530,"abi_return_us":12618,"end_us":12622,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6530,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":12699,"operation":"read","slot":1,"start_us":12694},{"abi_enter_us":12718,"abi_return_us":12723,"end_us":12723,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12718,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12756,"operation":"read","slot":1,"start_us":12755},{"abi_enter_us":12773,"abi_return_us":12774,"end_us":12774,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12773,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12802,"operation":"read","slot":1,"start_us":12802},{"abi_enter_us":12815,"abi_return_us":12818,"end_us":12818,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12815,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":1,"end_us":12855,"operation":"read","slot":1,"start_us":12855},{"end_us":12873,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12868},{"abi_enter_us":12894,"abi_return_us":501258,"end_us":501264,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12894,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":501327,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501321}],"schema":2,"slots":[{"at_us":501327,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12873,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914299d1d230-cca9-b6","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":500712,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6496,"slot":1,"token":"18e19142b81228f8-cca9-b7"},{"path":"/readiness/siblings/0","received_us":6503,"slot":0,"token":"18e19142b81228f8-cca9-b7"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e19142b81228f8-cca9-b7","watchdog":false,"workers_joined":2,"writes":[{"at_us":6514,"bytes":57,"kind":"head","slot":1},{"at_us":6522,"bytes":57,"kind":"head","slot":0},{"at_us":6537,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":123,"operation":"dispatch","outcome":"pending","slot":1,"start_us":105},{"abi_enter_us":138,"abi_return_us":6245,"end_us":6252,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":138,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6316,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6310},{"abi_enter_us":6333,"abi_return_us":6335,"end_us":6336,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6332,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6359,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6358},{"abi_enter_us":6372,"abi_return_us":6375,"end_us":6376,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6372,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6411,"operation":"read","slot":1,"start_us":6408},{"abi_enter_us":6424,"abi_return_us":6427,"end_us":6427,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6424,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6459,"operation":"read","slot":1,"start_us":6458},{"end_us":6474,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6469},{"abi_enter_us":6488,"abi_return_us":500594,"end_us":500601,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6488,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500672,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500664}],"schema":2,"slots":[{"at_us":500672,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6474,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19142b81228f8-cca9-b7","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":502179,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6553,"slot":1,"token":"18e19142d671a850-cca9-b8"},{"path":"/readiness/siblings/0","received_us":6614,"slot":0,"token":"18e19142d671a850-cca9-b8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e19142d671a850-cca9-b8","watchdog":false,"workers_joined":2,"writes":[{"at_us":6576,"bytes":57,"kind":"head","slot":1},{"at_us":6620,"bytes":57,"kind":"head","slot":0},{"at_us":12852,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":136,"operation":"dispatch","outcome":"pending","slot":1,"start_us":105},{"body_reads":0,"deadline_us":500000,"end_us":160,"operation":"handoff","start_us":160},{"abi_enter_us":180,"abi_return_us":6354,"end_us":6363,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":179,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6430,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6423},{"abi_enter_us":6449,"abi_return_us":6455,"end_us":6455,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6449,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6478,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6477},{"abi_enter_us":6490,"abi_return_us":12571,"end_us":12575,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6490,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":12644,"operation":"read","slot":1,"start_us":12640},{"abi_enter_us":12661,"abi_return_us":12665,"end_us":12665,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12661,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12699,"operation":"read","slot":1,"start_us":12697},{"abi_enter_us":12715,"abi_return_us":12716,"end_us":12716,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12714,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12742,"operation":"read","slot":1,"start_us":12741},{"abi_enter_us":12755,"abi_return_us":12757,"end_us":12758,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12755,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":1,"end_us":12791,"operation":"read","slot":1,"start_us":12791},{"end_us":12810,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12806},{"abi_enter_us":12833,"abi_return_us":502001,"end_us":502014,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12833,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":502113,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502100}],"schema":2,"slots":[{"at_us":502113,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12810,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19142d671a850-cca9-b8","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"siblings","completed_us":503812,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5945,"slot":0,"token":"18e19142f514dbb0-cca9-b9"},{"path":"/readiness/siblings/1","received_us":5960,"slot":1,"token":"18e19142f514dbb0-cca9-b9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e19142f514dbb0-cca9-b9","watchdog":false,"workers_joined":2,"writes":[{"at_us":5968,"bytes":57,"kind":"head","slot":0},{"at_us":5978,"bytes":57,"kind":"head","slot":1},{"at_us":5985,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":224,"operation":"dispatch","outcome":"pending","slot":0,"start_us":29},{"end_us":366,"operation":"dispatch","outcome":"pending","slot":1,"start_us":334},{"body_reads":0,"deadline_us":500000,"end_us":411,"operation":"handoff","start_us":411},{"abi_enter_us":471,"abi_return_us":5609,"end_us":5615,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":471,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5671,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5665},{"abi_enter_us":5688,"abi_return_us":5691,"end_us":5691,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5688,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5713,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5712},{"abi_enter_us":5730,"abi_return_us":5736,"end_us":5737,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5730,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5776,"operation":"read","slot":1,"start_us":5773},{"abi_enter_us":5792,"abi_return_us":5794,"end_us":5794,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5791,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5823,"operation":"read","slot":1,"start_us":5822},{"end_us":5838,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5834},{"abi_enter_us":5907,"abi_return_us":500925,"end_us":500931,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5906,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":503774,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":503765}],"schema":2,"slots":[{"at_us":503774,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5838,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19142f514dbb0-cca9-b9","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":500779,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5245,"slot":0,"token":"18e1914314c56c18-cca9-ba"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1914314c56c18-cca9-ba","watchdog":false,"workers_joined":1,"writes":[{"at_us":5259,"bytes":57,"kind":"head","slot":0},{"at_us":5288,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"abi_enter_us":134,"abi_return_us":4866,"end_us":4871,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":133,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4918,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4912},{"abi_enter_us":4935,"abi_return_us":4942,"end_us":4942,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4935,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4981,"operation":"read","slot":0,"start_us":4978},{"abi_enter_us":5000,"abi_return_us":500597,"end_us":500616,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5000,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":500748,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500711}],"schema":2,"slots":[{"at_us":500748,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914314c56c18-cca9-ba","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":501237,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6448,"slot":0,"token":"18e1914332ea1cc0-cca9-bb"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1914332ea1cc0-cca9-bb","watchdog":false,"workers_joined":1,"writes":[{"at_us":6459,"bytes":57,"kind":"head","slot":0},{"at_us":6464,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":68,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"abi_enter_us":122,"abi_return_us":6178,"end_us":6183,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":122,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6228,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6219},{"abi_enter_us":6247,"abi_return_us":6251,"end_us":6252,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6247,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6287,"operation":"read","slot":0,"start_us":6284},{"abi_enter_us":6301,"abi_return_us":501048,"end_us":501061,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6301,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501202,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501191}],"schema":2,"slots":[{"at_us":501202,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914332ea1cc0-cca9-bb","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":501138,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6076,"slot":0,"token":"18e19143511b7b80-cca9-bc"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19143511b7b80-cca9-bc","watchdog":false,"workers_joined":1,"writes":[{"at_us":6085,"bytes":57,"kind":"head","slot":0},{"at_us":6090,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":76,"operation":"handoff","start_us":76},{"abi_enter_us":91,"abi_return_us":5671,"end_us":5676,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5714,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5708},{"abi_enter_us":5728,"abi_return_us":5732,"end_us":5732,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5728,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5760,"operation":"read","slot":0,"start_us":5757},{"abi_enter_us":5773,"abi_return_us":500821,"end_us":500828,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5773,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501103,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501094}],"schema":2,"slots":[{"at_us":501103,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19143511b7b80-cca9-bc","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"exhausted","completed_us":502137,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5562,"slot":0,"token":"18e191436f3f0b18-cca9-bd"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e191436f3f0b18-cca9-bd","watchdog":false,"workers_joined":1,"writes":[{"at_us":5579,"bytes":57,"kind":"head","slot":0},{"at_us":5584,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"abi_enter_us":97,"abi_return_us":5344,"end_us":5349,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":97,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5392,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5386},{"abi_enter_us":5405,"abi_return_us":5409,"end_us":5409,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5405,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5447,"operation":"read","slot":0,"start_us":5444},{"abi_enter_us":5460,"abi_return_us":500832,"end_us":501301,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5460,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501941,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501805}],"schema":2,"slots":[{"at_us":501941,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191436f3f0b18-cca9-bd","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":7177,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6589,"slot":0,"token":"18e191438d8e0010-cca9-be"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191438d8e0010-cca9-be","watchdog":false,"workers_joined":1,"writes":[{"at_us":6612,"bytes":57,"kind":"head","slot":0},{"at_us":6618,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":97,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"abi_enter_us":270,"abi_return_us":6120,"end_us":6162,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":269,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6308,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6252},{"abi_enter_us":6377,"abi_return_us":6398,"end_us":6399,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6376,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6441,"operation":"read","slot":0,"start_us":6437},{"abi_enter_us":6901,"abi_return_us":6907,"end_us":6907,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6901,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6984,"operation":"read","slot":0,"start_us":6981},{"abi_enter_us":7008,"abi_return_us":7010,"end_us":7011,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7008,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":1,"end_us":7045,"operation":"read","slot":0,"start_us":7044},{"abi_enter_us":7060,"abi_return_us":7067,"end_us":7067,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7059,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":0,"capacity":1,"end_us":7094,"operation":"read","slot":0,"start_us":7093},{"end_us":7153,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":7148}],"schema":2,"slots":[{"at_us":7153,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191438d8e0010-cca9-be","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":5467,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5524,"slot":0,"token":"18e191438e6a1bb8-cca9-bf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191438e6a1bb8-cca9-bf","watchdog":false,"workers_joined":1,"writes":[{"at_us":5539,"bytes":57,"kind":"head","slot":0},{"at_us":5598,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":69,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":167,"abi_return_us":5153,"end_us":5158,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":166,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5234,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5226},{"abi_enter_us":5267,"abi_return_us":5283,"end_us":5284,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5267,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5369,"operation":"read","slot":0,"start_us":5365},{"abi_enter_us":5396,"abi_return_us":5401,"end_us":5402,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5396,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5436,"operation":"read","slot":0,"start_us":5436},{"end_us":5452,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5448}],"schema":2,"slots":[{"at_us":5452,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191438e6a1bb8-cca9-bf","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":6223,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5591,"slot":0,"token":"18e191438f1da890-cca9-c0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191438f1da890-cca9-c0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5621,"bytes":57,"kind":"head","slot":0},{"at_us":5634,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":141,"abi_return_us":5759,"end_us":5767,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":141,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5867,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5833},{"abi_enter_us":5961,"abi_return_us":5969,"end_us":5971,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5961,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":6044,"operation":"read","slot":0,"start_us":6041},{"abi_enter_us":6060,"abi_return_us":6061,"end_us":6061,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6060,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6091,"operation":"read","slot":0,"start_us":6090},{"abi_enter_us":6107,"abi_return_us":6108,"end_us":6108,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6107,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6137,"operation":"read","slot":0,"start_us":6137},{"abi_enter_us":6150,"abi_return_us":6152,"end_us":6153,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6150,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6183,"operation":"read","slot":0,"start_us":6182},{"end_us":6201,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6197}],"schema":2,"slots":[{"at_us":6201,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191438f1da890-cca9-c0","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"eof","completed_us":6708,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6948,"slot":0,"token":"18e19143900e7130-cca9-c1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19143900e7130-cca9-c1","watchdog":false,"workers_joined":1,"writes":[{"at_us":6971,"bytes":57,"kind":"head","slot":0},{"at_us":6979,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":221,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":298,"operation":"handoff","start_us":298},{"abi_enter_us":314,"abi_return_us":6466,"end_us":6476,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":314,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6531,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6524},{"abi_enter_us":6553,"abi_return_us":6568,"end_us":6568,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6553,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6609,"operation":"read","slot":0,"start_us":6606},{"abi_enter_us":6624,"abi_return_us":6627,"end_us":6628,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6624,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6657,"operation":"read","slot":0,"start_us":6656},{"end_us":6693,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6689}],"schema":2,"slots":[{"at_us":6693,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19143900e7130-cca9-c1","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6074,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6193,"slot":0,"token":"18e1914390f9ee58-cca9-c2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1914390f9ee58-cca9-c2","watchdog":false,"workers_joined":1,"writes":[{"at_us":6219,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":97,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"abi_enter_us":196,"abi_return_us":5694,"end_us":5700,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":196,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5811,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5781},{"abi_enter_us":5832,"abi_return_us":5837,"end_us":5863,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5832,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5988,"operation":"read","slot":0,"start_us":5984},{"end_us":6030,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6022}],"schema":2,"slots":[{"at_us":6030,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914390f9ee58-cca9-c2","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6276,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6589,"slot":0,"token":"18e1914392068810-cca9-c3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1914392068810-cca9-c3","watchdog":false,"workers_joined":1,"writes":[{"at_us":6609,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":85,"operation":"dispatch","outcome":"pending","slot":0,"start_us":55},{"abi_enter_us":219,"abi_return_us":6070,"end_us":6078,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":218,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6137,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6131},{"abi_enter_us":6157,"abi_return_us":6163,"end_us":6164,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6157,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6237,"operation":"read","slot":0,"start_us":6235},{"end_us":6258,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6254}],"schema":2,"slots":[{"at_us":6258,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914392068810-cca9-c3","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6082,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6177,"slot":0,"token":"18e1914392b13b48-cca9-c4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1914392b13b48-cca9-c4","watchdog":false,"workers_joined":1,"writes":[{"at_us":6189,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":110,"operation":"handoff","start_us":110},{"abi_enter_us":167,"abi_return_us":5935,"end_us":5941,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":167,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5987,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5981},{"abi_enter_us":6007,"abi_return_us":6010,"end_us":6011,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6006,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6044,"operation":"read","slot":0,"start_us":6042},{"end_us":6066,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6060}],"schema":2,"slots":[{"at_us":6066,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1914392b13b48-cca9-c4","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6630,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6634,"slot":0,"token":"18e19143935c82f0-cca9-c5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19143935c82f0-cca9-c5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6702,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":102,"operation":"handoff","start_us":102},{"abi_enter_us":124,"abi_return_us":6463,"end_us":6470,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":124,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6529,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6522},{"abi_enter_us":6551,"abi_return_us":6556,"end_us":6556,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6550,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6588,"operation":"read","slot":0,"start_us":6586},{"end_us":6609,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6605}],"schema":2,"slots":[{"at_us":6609,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19143935c82f0-cca9-c5","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5410,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5540,"slot":0,"token":"18e191439409ca50-cca9-c6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191439409ca50-cca9-c6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5556,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":104,"abi_return_us":5324,"end_us":5332,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5369,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5363},{"end_us":5386,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5386}],"schema":2,"slots":[{"at_us":5386,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191439409ca50-cca9-c6","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5233,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5276,"slot":0,"token":"18e19143949b2158-cca9-c7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19143949b2158-cca9-c7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5298,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":67,"abi_return_us":5090,"end_us":5099,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5181,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5173},{"end_us":5208,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5208}],"schema":2,"slots":[{"at_us":5208,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19143949b2158-cca9-c7","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6229,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6542,"slot":0,"token":"18e19143952b6ec0-cca9-c8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19143952b6ec0-cca9-c8","watchdog":false,"workers_joined":1,"writes":[{"at_us":6557,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":90,"operation":"dispatch","outcome":"pending","slot":0,"start_us":59},{"body_reads":0,"deadline_us":500000,"end_us":155,"operation":"handoff","start_us":155},{"abi_enter_us":179,"abi_return_us":6139,"end_us":6145,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":179,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6191,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6186},{"end_us":6210,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6210}],"schema":2,"slots":[{"at_us":6210,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19143952b6ec0-cca9-c8","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5969,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6215,"slot":0,"token":"18e1914395d46c78-cca9-c9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1914395d46c78-cca9-c9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6229,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":119,"operation":"handoff","start_us":119},{"abi_enter_us":149,"abi_return_us":5892,"end_us":5898,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":149,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5935,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5930},{"end_us":5953,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5953}],"schema":2,"slots":[{"at_us":5953,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1914395d46c78-cca9-c9","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":62025,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6488,"slot":0,"token":"18e191439676ea20-cca9-ca"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191439676ea20-cca9-ca","watchdog":false,"workers_joined":1,"writes":[{"at_us":6930,"bytes":66,"kind":"head","slot":0},{"at_us":6937,"bytes":6,"kind":"body","slot":0},{"at_us":61990,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"abi_enter_us":121,"abi_return_us":6675,"end_us":6683,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":121,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6734,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6728},{"abi_enter_us":6755,"abi_return_us":6763,"end_us":6764,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6755,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6800,"operation":"read","slot":0,"start_us":6797},{"abi_enter_us":6822,"abi_return_us":61792,"end_us":61796,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6821,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":61888,"error":"Other","operation":"read","slot":0,"start_us":61871},{"end_us":61997,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61958}],"schema":2,"slots":[{"at_us":61997,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191439676ea20-cca9-ca","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":64681,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":8039,"slot":0,"token":"18e191439aa60d10-cca9-cb"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191439aa60d10-cca9-cb","watchdog":false,"workers_joined":1,"writes":[{"at_us":8056,"bytes":66,"kind":"head","slot":0},{"at_us":8064,"bytes":6,"kind":"body","slot":0},{"at_us":62762,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":1078,"operation":"dispatch","outcome":"pending","slot":0,"start_us":485},{"abi_enter_us":1494,"abi_return_us":4666,"end_us":4673,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1493,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":4840,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4802},{"abi_enter_us":4864,"abi_return_us":4869,"end_us":4870,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4863,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4903,"operation":"read","slot":0,"start_us":4901},{"abi_enter_us":4917,"abi_return_us":59134,"end_us":59148,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4917,"status":"FastlyStatus::OK","timeout_ms":495},{"capacity":16384,"end_us":64460,"error":"Other","operation":"read","slot":0,"start_us":64452},{"end_us":64574,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":64564}],"schema":2,"slots":[{"at_us":64574,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191439aa60d10-cca9-cb","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":60429,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6523,"slot":0,"token":"18e191439f790950-cca9-cc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191439f790950-cca9-cc","watchdog":false,"workers_joined":1,"writes":[{"at_us":6533,"bytes":66,"kind":"head","slot":0},{"at_us":6537,"bytes":6,"kind":"body","slot":0},{"at_us":58927,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":105,"operation":"handoff","start_us":105},{"abi_enter_us":126,"abi_return_us":6121,"end_us":6127,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":125,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":9355,"operation":"head-poll","outcome":"ready","slot":0,"start_us":9345},{"abi_enter_us":9666,"abi_return_us":9676,"end_us":9677,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":9666,"status":"FastlyStatus::OK","timeout_ms":490},{"bytes":1,"capacity":1,"end_us":9738,"operation":"read","slot":0,"start_us":9735},{"abi_enter_us":9761,"abi_return_us":58507,"end_us":58510,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":9761,"status":"FastlyStatus::OK","timeout_ms":490},{"capacity":1,"end_us":60379,"error":"Other","operation":"read","slot":0,"start_us":60371},{"end_us":60409,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60403}],"schema":2,"slots":[{"at_us":60409,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191439f790950-cca9-cc","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"malformed-body","completed_us":65053,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":12332,"slot":0,"token":"18e19143a3c633c0-cca9-cd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19143a3c633c0-cca9-cd","watchdog":false,"workers_joined":1,"writes":[{"at_us":12367,"bytes":66,"kind":"head","slot":0},{"at_us":12380,"bytes":6,"kind":"body","slot":0},{"at_us":65781,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":88,"operation":"dispatch","outcome":"pending","slot":0,"start_us":56},{"body_reads":0,"deadline_us":500000,"end_us":432,"operation":"handoff","start_us":432},{"abi_enter_us":450,"abi_return_us":17114,"end_us":17123,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":450,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":17184,"operation":"head-poll","outcome":"ready","slot":0,"start_us":17175},{"abi_enter_us":17207,"abi_return_us":17212,"end_us":17213,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":17207,"status":"FastlyStatus::OK","timeout_ms":482},{"bytes":1,"capacity":16384,"end_us":17246,"operation":"read","slot":0,"start_us":17243},{"abi_enter_us":17258,"abi_return_us":64919,"end_us":64924,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":17258,"status":"FastlyStatus::OK","timeout_ms":482},{"capacity":16384,"end_us":65005,"error":"Other","operation":"read","slot":0,"start_us":64999},{"end_us":65037,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":65029}],"schema":2,"slots":[{"at_us":65037,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19143a3c633c0-cca9-cd","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5598,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5784,"slot":0,"token":"18e19143a86619e0-cca9-ce"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19143a86619e0-cca9-ce","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":88,"abi_return_us":5517,"end_us":5524,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5564,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5559},{"end_us":5581,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5581}],"schema":2,"slots":[{"at_us":5581,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19143a86619e0-cca9-ce","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5640,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5806,"slot":0,"token":"18e19143a8fca108-cca9-cf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19143a8fca108-cca9-cf","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":186,"abi_return_us":5560,"end_us":5568,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":186,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5607,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5601},{"end_us":5621,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5621}],"schema":2,"slots":[{"at_us":5621,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19143a8fca108-cca9-cf","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":5485,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5628,"slot":0,"token":"18e19143a999cf50-cca9-d0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19143a999cf50-cca9-d0","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":84,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":160,"operation":"handoff","start_us":160},{"abi_enter_us":185,"abi_return_us":5340,"end_us":5354,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":185,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5448,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5442},{"end_us":5468,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5467}],"schema":2,"slots":[{"at_us":5468,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19143a999cf50-cca9-d0","tolerance_us":5000}} +{"run":"repaired","kind":"body-probe","probe":{"case":"head-error","completed_us":6369,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6636,"slot":0,"token":"18e19143aa36d2a0-cca9-d1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19143aa36d2a0-cca9-d1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":96,"abi_return_us":6289,"end_us":6295,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6335,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6329},{"end_us":6351,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6351}],"schema":2,"slots":[{"at_us":6351,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19143aa36d2a0-cca9-d1","tolerance_us":5000}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1010,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":10,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19150c9d43578-e8d6-3","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2075,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2075,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1075,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19150c9e66d88-e8d6-4","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2263,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1263,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19150ca0d71d0-e8d6-5","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103967,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3967,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19150ca36b450-e8d6-6","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102044,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102044,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2044,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19150d06cf1e0-e8d6-7","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102065,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2065,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19150d68f3a10-e8d6-8","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500039,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":39,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19150dcafbd20-e8d6-9","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501367,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501367,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1367,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19150fa81c1b8-e8d6-a","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501096,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1096,"qualification":false,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e191511871a2d8-e8d6-b","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1011,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":11,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19151365b5dc0-e8d6-c","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2022,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2022,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1022,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19151366e12d0-e8d6-d","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2080,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1080,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e191513696ad58-e8d6-e","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102247,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2247,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1915136bd7708-e8d6-f","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101748,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101748,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1748,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e191513cd979c0-e8d6-10","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101271,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1271,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1915142f6e3d8-e8d6-11","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503397,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3397,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19151490cfed8-e8d6-12","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501036,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501036,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1036,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1915167127b38-e8d6-13","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502056,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2056,"qualification":false,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1915185024100-e8d6-14","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1115,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":115,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19151a2fe4f50-e8d6-15","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2242,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2242,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1242,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19151a312d920-e8d6-16","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2082,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1082,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19151a3422298-e8d6-17","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103894,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3894,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19151a36a76d0-e8d6-18","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":100868,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":100868,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":868,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19151a99faea8-e8d6-19","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101125,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1125,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19151afebe538-e8d6-1a","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502672,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2672,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19151b6056750-e8d6-1b","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501927,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501927,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1927,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19151d4006048-e8d6-1c","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501180,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1180,"qualification":false,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19151f1fc8220-e8d6-1d","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1254,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":254,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e191520fe74e78-e8d6-1e","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2282,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2282,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1282,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e191520ffd7a40-e8d6-1f","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2267,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1267,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19152102bf0c8-e8d6-20","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103907,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3907,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e191521056c988-e8d6-21","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":100212,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":100212,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":212,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19152168e6e78-e8d6-22","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102057,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2057,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e191521c959170-e8d6-23","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500439,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":439,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1915222b99308-e8d6-24","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501176,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501176,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1176,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e191524091e8d0-e8d6-25","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502066,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2066,"qualification":false,"repetition":3,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e191525e84e2e8-e8d6-26","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1262,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":262,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e191527c8229b8-e8d6-27","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":1155,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":1155,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":155,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e191527c993040-e8d6-28","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2127,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1127,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e191527cb84d18-e8d6-29","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101898,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1898,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e191527cec1eb8-e8d6-2a","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101904,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101904,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1904,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e191528344e420-e8d6-2b","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101983,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1983,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19152896812c8-e8d6-2c","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500764,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":764,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e191528f890ef0-e8d6-2d","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501421,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501421,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1421,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19152ad6eda08-e8d6-2e","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501302,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1302,"qualification":false,"repetition":4,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19152cb835a28-e8d6-2f","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1252,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":252,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19152e98ca330-e8d6-30","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2291,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2291,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1291,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19152e9a4bb28-e8d6-31","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2189,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1189,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19152e9de8038-e8d6-32","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":105009,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":5009,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19152ea150540-e8d6-33","within_five_ms":false}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101086,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101086,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1086,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19152f05b8eb0-e8d6-34","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101678,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1678,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19152f66fa228-e8d6-35","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":505018,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":5018,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19152fc8b6e30-e8d6-36","within_five_ms":false}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":506343,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":506343,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":6343,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e191531aaa1bc8-e8d6-37","within_five_ms":false}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501534,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1534,"qualification":false,"repetition":5,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e191533a2e6058-e8d6-38","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1257,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":257,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e1915358303ef0-e8d6-39","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2049,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2049,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1049,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e191535846a168-e8d6-3a","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2035,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1035,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19153587041a8-e8d6-3b","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101221,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1221,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e191535896ffa0-e8d6-3c","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101466,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101466,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1466,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e191535ea4b630-e8d6-3d","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101166,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1166,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1915364c33988-e8d6-3e","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501020,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1020,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e191536ad77be0-e8d6-3f","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":500929,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":500929,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":929,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1915388b8b318-e8d6-40","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501904,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1904,"qualification":false,"repetition":6,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19153a6a40c10-e8d6-41","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1258,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":258,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19153c49c5588-e8d6-42","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2283,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2283,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1283,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19153c4b36bb0-e8d6-43","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2056,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1056,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19153c4e30f00-e8d6-44","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102186,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2186,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19153c50abf28-e8d6-45","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102126,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102126,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2126,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19153cb28bdb0-e8d6-46","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101988,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1988,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e19153d14f2c60-e8d6-47","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500460,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":460,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"native-sleep","token":"18e19153d7702888-e8d6-48","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501175,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501175,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1175,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e19153f548dff8-e8d6-49","within_five_ms":true}} +{"run":"stock","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501320,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1320,"qualification":false,"repetition":7,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1915413374248-e8d6-4a","within_five_ms":true}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":59411,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":11680,"slot":0,"token":"18e19154312f64b0-e8d6-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19154312f64b0-e8d6-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":11707,"bytes":57,"kind":"head","slot":0},{"at_us":11719,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":1104,"operation":"dispatch","outcome":"pending","slot":0,"start_us":399},{"abi_enter_us":1164,"abi_return_us":8997,"end_us":9005,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1163,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":9363,"operation":"head-poll","outcome":"ready","slot":0,"start_us":9161},{"bytes":1,"capacity":1,"end_us":9390,"operation":"read","slot":0,"start_us":9387},{"capacity":1,"end_us":59316,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":9422},{"end_us":59387,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59380}],"schema":2,"slots":[{"at_us":59387,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19154312f64b0-e8d6-4b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":60213,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6452,"slot":0,"token":"18e1915435087c48-e8d6-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915435087c48-e8d6-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6485,"bytes":57,"kind":"head","slot":0},{"at_us":6492,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":82,"abi_return_us":6303,"end_us":6308,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6360,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6356},{"abi_enter_us":6372,"abi_return_us":6379,"end_us":6379,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6372,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6403,"operation":"read","slot":0,"start_us":6401},{"abi_enter_us":6414,"abi_return_us":60095,"end_us":60098,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6414,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":60167,"operation":"read","slot":0,"start_us":60163},{"end_us":60196,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60191}],"schema":2,"slots":[{"at_us":60196,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915435087c48-e8d6-4c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":55914,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5224,"slot":0,"token":"18e1915438c3a3d0-e8d6-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915438c3a3d0-e8d6-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5232,"bytes":57,"kind":"head","slot":0},{"at_us":5237,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":69,"abi_return_us":5016,"end_us":5020,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5062,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5057},{"abi_enter_us":5075,"abi_return_us":5080,"end_us":5080,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5075,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5114,"operation":"read","slot":0,"start_us":5111},{"abi_enter_us":5125,"abi_return_us":55806,"end_us":55809,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5125,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":55869,"operation":"read","slot":0,"start_us":55865},{"end_us":55900,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55895}],"schema":2,"slots":[{"at_us":55900,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915438c3a3d0-e8d6-4d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":56948,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6406,"slot":0,"token":"18e191543c3b7128-e8d6-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191543c3b7128-e8d6-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6413,"bytes":57,"kind":"head","slot":0},{"at_us":6419,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":76,"abi_return_us":6220,"end_us":6223,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6253,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6249},{"abi_enter_us":6264,"abi_return_us":6266,"end_us":6267,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6264,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6295,"operation":"read","slot":0,"start_us":6293},{"abi_enter_us":6305,"abi_return_us":56827,"end_us":56831,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6305,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":56916,"operation":"read","slot":0,"start_us":56912},{"end_us":56933,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56929}],"schema":2,"slots":[{"at_us":56933,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191543c3b7128-e8d6-4e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":58073,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5664,"slot":0,"token":"18e191543fc26d38-e8d6-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191543fc26d38-e8d6-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5672,"bytes":57,"kind":"head","slot":0},{"at_us":5676,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"abi_enter_us":62,"abi_return_us":5315,"end_us":5318,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5347,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5343},{"abi_enter_us":5359,"abi_return_us":5363,"end_us":5363,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5359,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5393,"operation":"read","slot":0,"start_us":5391},{"abi_enter_us":5403,"abi_return_us":57935,"end_us":57939,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5403,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":58035,"operation":"read","slot":0,"start_us":58031},{"end_us":58054,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58050}],"schema":2,"slots":[{"at_us":58054,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191543fc26d38-e8d6-4f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":502287,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6310,"slot":0,"token":"18e19154435f4308-e8d6-50"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e19154435f4308-e8d6-50","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":68,"abi_return_us":499908,"end_us":499916,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"abi_enter_us":499971,"abi_return_us":502239,"end_us":502240,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499971,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502269,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502268}],"schema":2,"slots":[{"at_us":502269,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19154435f4308-e8d6-50","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":500393,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6009,"slot":0,"token":"18e191546169dc00-e8d6-51"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191546169dc00-e8d6-51","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":64,"abi_return_us":500216,"end_us":500224,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500362,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500362}],"schema":2,"slots":[{"at_us":500362,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191546169dc00-e8d6-51","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":502445,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5605,"slot":0,"token":"18e191547f6f92f8-e8d6-52"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191547f6f92f8-e8d6-52","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":73,"operation":"handoff","start_us":73},{"abi_enter_us":90,"abi_return_us":500302,"end_us":500384,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502417,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502417}],"schema":2,"slots":[{"at_us":502417,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191547f6f92f8-e8d6-52","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":500995,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":9007,"slot":0,"token":"18e191549daf2670-e8d6-53"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191549daf2670-e8d6-53","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":1392,"operation":"dispatch","outcome":"pending","slot":0,"start_us":48},{"body_reads":0,"deadline_us":500000,"end_us":1490,"operation":"handoff","start_us":1490},{"abi_enter_us":1530,"abi_return_us":500865,"end_us":500894,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":1530,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":500973,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500973}],"schema":2,"slots":[{"at_us":500973,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191549daf2670-e8d6-53","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":500529,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6413,"slot":0,"token":"18e19154bbcf6e30-e8d6-54"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19154bbcf6e30-e8d6-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":6426,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":70,"abi_return_us":6181,"end_us":6183,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6228,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6218},{"abi_enter_us":6244,"abi_return_us":500348,"end_us":500352,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6244,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500447,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500418}],"schema":2,"slots":[{"at_us":500447,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19154bbcf6e30-e8d6-54","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":501622,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5229,"slot":0,"token":"18e19154d9cd7850-e8d6-55"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19154d9cd7850-e8d6-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":5244,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":92,"abi_return_us":4850,"end_us":4856,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4913,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4906},{"abi_enter_us":4928,"abi_return_us":501528,"end_us":501534,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4928,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501600,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501593}],"schema":2,"slots":[{"at_us":501600,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19154d9cd7850-e8d6-55","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":500317,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5220,"slot":0,"token":"18e19154f7dae3f0-e8d6-56"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19154f7dae3f0-e8d6-56","watchdog":false,"workers_joined":1,"writes":[{"at_us":5228,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"abi_enter_us":79,"abi_return_us":5004,"end_us":5005,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5033,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5028},{"abi_enter_us":5045,"abi_return_us":500170,"end_us":500178,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5045,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500245,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500238}],"schema":2,"slots":[{"at_us":500245,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19154f7dae3f0-e8d6-56","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":501710,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5883,"slot":0,"token":"18e1915515d3f888-e8d6-57"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1915515d3f888-e8d6-57","watchdog":false,"workers_joined":1,"writes":[{"at_us":5904,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":81,"operation":"dispatch","outcome":"pending","slot":0,"start_us":29},{"body_reads":0,"deadline_us":500000,"end_us":115,"operation":"handoff","start_us":115},{"abi_enter_us":130,"abi_return_us":5461,"end_us":5467,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":130,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5524,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5519},{"abi_enter_us":5541,"abi_return_us":501599,"end_us":501616,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5541,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501679,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501672}],"schema":2,"slots":[{"at_us":501679,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915515d3f888-e8d6-57","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":500519,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5514,"slot":0,"token":"18e1915533e23330-e8d6-58"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1915533e23330-e8d6-58","watchdog":false,"workers_joined":1,"writes":[{"at_us":5522,"bytes":57,"kind":"head","slot":0},{"at_us":5529,"bytes":1,"kind":"body","slot":0},{"at_us":307852,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":83,"abi_return_us":5328,"end_us":5334,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5387,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5382},{"abi_enter_us":5402,"abi_return_us":5406,"end_us":5407,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5402,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5434,"operation":"read","slot":0,"start_us":5432},{"abi_enter_us":5445,"abi_return_us":307656,"end_us":307660,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5445,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307734,"operation":"read","slot":0,"start_us":307731},{"abi_enter_us":307748,"abi_return_us":500407,"end_us":500410,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307747,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500485,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500477}],"schema":2,"slots":[{"at_us":500485,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915533e23330-e8d6-58","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":501004,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6458,"slot":0,"token":"18e1915551e2c1d8-e8d6-59"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1915551e2c1d8-e8d6-59","watchdog":false,"workers_joined":1,"writes":[{"at_us":6499,"bytes":57,"kind":"head","slot":0},{"at_us":6561,"bytes":1,"kind":"body","slot":0},{"at_us":308973,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":69,"abi_return_us":6358,"end_us":6368,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6426,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6421},{"abi_enter_us":6444,"abi_return_us":6453,"end_us":6454,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6444,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6484,"operation":"read","slot":0,"start_us":6482},{"abi_enter_us":6501,"abi_return_us":308754,"end_us":308762,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6501,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":308828,"operation":"read","slot":0,"start_us":308826},{"abi_enter_us":308857,"abi_return_us":500920,"end_us":500924,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308857,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500984,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500978}],"schema":2,"slots":[{"at_us":500984,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915551e2c1d8-e8d6-59","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":501415,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5282,"slot":0,"token":"18e191556fea3620-e8d6-5a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191556fea3620-e8d6-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5296,"bytes":57,"kind":"head","slot":0},{"at_us":5302,"bytes":1,"kind":"body","slot":0},{"at_us":307171,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"abi_enter_us":77,"abi_return_us":5124,"end_us":5128,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5176,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5171},{"abi_enter_us":5188,"abi_return_us":5192,"end_us":5192,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5188,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5215,"operation":"read","slot":0,"start_us":5212},{"abi_enter_us":5239,"abi_return_us":306995,"end_us":307011,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5239,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307081,"operation":"read","slot":0,"start_us":307077},{"abi_enter_us":307093,"abi_return_us":501270,"end_us":501274,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307093,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501379,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501363}],"schema":2,"slots":[{"at_us":501379,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191556fea3620-e8d6-5a","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":501352,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5290,"slot":0,"token":"18e191558df290e0-e8d6-5b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191558df290e0-e8d6-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5300,"bytes":57,"kind":"head","slot":0},{"at_us":5315,"bytes":1,"kind":"body","slot":0},{"at_us":308070,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":92,"abi_return_us":5110,"end_us":5112,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5136,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5132},{"abi_enter_us":5153,"abi_return_us":5159,"end_us":5159,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5153,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5188,"operation":"read","slot":0,"start_us":5186},{"abi_enter_us":5198,"abi_return_us":307841,"end_us":307844,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5198,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":307922,"operation":"read","slot":0,"start_us":307919},{"abi_enter_us":307934,"abi_return_us":501149,"end_us":501156,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307934,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501303,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501296}],"schema":2,"slots":[{"at_us":501303,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191558df290e0-e8d6-5b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501531,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5693,"slot":0,"token":"18e19155abff5c58-e8d6-5c"},{"path":"/readiness/siblings/1","received_us":5747,"slot":1,"token":"18e19155abff5c58-e8d6-5c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e19155abff5c58-e8d6-5c","watchdog":false,"workers_joined":2,"writes":[{"at_us":5707,"bytes":57,"kind":"head","slot":0},{"at_us":5754,"bytes":57,"kind":"head","slot":1},{"at_us":5759,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":134,"operation":"dispatch","outcome":"pending","slot":1,"start_us":108},{"abi_enter_us":168,"abi_return_us":5450,"end_us":5455,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":168,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5511,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5507},{"abi_enter_us":5525,"abi_return_us":5526,"end_us":5527,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5525,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5545,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5544},{"abi_enter_us":5555,"abi_return_us":5558,"end_us":5558,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5555,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5582,"operation":"read","slot":1,"start_us":5579},{"abi_enter_us":5593,"abi_return_us":5597,"end_us":5597,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5593,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5622,"operation":"read","slot":1,"start_us":5617},{"abi_enter_us":5632,"abi_return_us":5633,"end_us":5633,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5632,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5653,"operation":"read","slot":1,"start_us":5652},{"abi_enter_us":5665,"abi_return_us":5668,"end_us":5668,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5665,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5692,"operation":"read","slot":1,"start_us":5691},{"end_us":5703,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5701},{"abi_enter_us":5715,"abi_return_us":501377,"end_us":501382,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5715,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501498,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501489}],"schema":2,"slots":[{"at_us":501498,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5703,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19155abff5c58-e8d6-5c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":500694,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6485,"slot":0,"token":"18e19155ca23ca98-e8d6-5d"},{"path":"/readiness/siblings/1","received_us":6495,"slot":1,"token":"18e19155ca23ca98-e8d6-5d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e19155ca23ca98-e8d6-5d","watchdog":false,"workers_joined":2,"writes":[{"at_us":6500,"bytes":57,"kind":"head","slot":0},{"at_us":6509,"bytes":57,"kind":"head","slot":1},{"at_us":6522,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":161,"operation":"dispatch","outcome":"pending","slot":1,"start_us":122},{"abi_enter_us":181,"abi_return_us":6086,"end_us":6091,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":181,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6142,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6138},{"abi_enter_us":6158,"abi_return_us":6160,"end_us":6160,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6158,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6174,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6173},{"abi_enter_us":6184,"abi_return_us":6188,"end_us":6188,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6184,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6218,"operation":"read","slot":1,"start_us":6215},{"abi_enter_us":6231,"abi_return_us":6233,"end_us":6233,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6230,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6254,"operation":"read","slot":1,"start_us":6254},{"end_us":6267,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6265},{"abi_enter_us":6280,"abi_return_us":500579,"end_us":500582,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6280,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500660,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500654}],"schema":2,"slots":[{"at_us":500660,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6267,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19155ca23ca98-e8d6-5d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":502051,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6063,"slot":1,"token":"18e19155e8316900-e8d6-5e"},{"path":"/readiness/siblings/0","received_us":6068,"slot":0,"token":"18e19155e8316900-e8d6-5e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e19155e8316900-e8d6-5e","watchdog":false,"workers_joined":2,"writes":[{"at_us":6072,"bytes":57,"kind":"head","slot":1},{"at_us":6085,"bytes":57,"kind":"head","slot":0},{"at_us":11740,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":100,"operation":"dispatch","outcome":"pending","slot":1,"start_us":79},{"body_reads":0,"deadline_us":500000,"end_us":118,"operation":"handoff","start_us":118},{"abi_enter_us":145,"abi_return_us":5859,"end_us":5863,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":145,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5907,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5902},{"abi_enter_us":5920,"abi_return_us":5921,"end_us":5922,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5920,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5935,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5935},{"abi_enter_us":5945,"abi_return_us":11505,"end_us":11507,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5945,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":11554,"operation":"read","slot":1,"start_us":11551},{"abi_enter_us":11567,"abi_return_us":11569,"end_us":11569,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11567,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11592,"operation":"read","slot":1,"start_us":11591},{"abi_enter_us":11604,"abi_return_us":11605,"end_us":11605,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11604,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11625,"operation":"read","slot":1,"start_us":11625},{"abi_enter_us":11635,"abi_return_us":11637,"end_us":11637,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11634,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11661,"operation":"read","slot":1,"start_us":11660},{"end_us":11675,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11671},{"abi_enter_us":11689,"abi_return_us":501904,"end_us":501916,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11689,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":502027,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502011}],"schema":2,"slots":[{"at_us":502027,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11675,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19155e8316900-e8d6-5e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501311,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5299,"slot":0,"token":"18e1915606589278-e8d6-5f"},{"path":"/readiness/siblings/1","received_us":5305,"slot":1,"token":"18e1915606589278-e8d6-5f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1915606589278-e8d6-5f","watchdog":false,"workers_joined":2,"writes":[{"at_us":5314,"bytes":57,"kind":"head","slot":0},{"at_us":5323,"bytes":57,"kind":"head","slot":1},{"at_us":5328,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":117,"operation":"dispatch","outcome":"pending","slot":1,"start_us":93},{"body_reads":0,"deadline_us":500000,"end_us":148,"operation":"handoff","start_us":148},{"abi_enter_us":164,"abi_return_us":5112,"end_us":5118,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":164,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5165,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5158},{"abi_enter_us":5178,"abi_return_us":5180,"end_us":5181,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5178,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5199,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5198},{"abi_enter_us":5209,"abi_return_us":5212,"end_us":5212,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5209,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5239,"operation":"read","slot":1,"start_us":5237},{"abi_enter_us":5253,"abi_return_us":5255,"end_us":5256,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5253,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5278,"operation":"read","slot":1,"start_us":5277},{"end_us":5290,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5287},{"abi_enter_us":5303,"abi_return_us":501202,"end_us":501209,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5303,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501279,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501271}],"schema":2,"slots":[{"at_us":501279,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5290,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915606589278-e8d6-5f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":500484,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5230,"slot":0,"token":"18e191562470b060-e8d6-60"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e191562470b060-e8d6-60","watchdog":false,"workers_joined":1,"writes":[{"at_us":5238,"bytes":57,"kind":"head","slot":0},{"at_us":5243,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":86,"abi_return_us":5014,"end_us":5018,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5055,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5051},{"abi_enter_us":5068,"abi_return_us":5073,"end_us":5074,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5068,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5099,"operation":"read","slot":0,"start_us":5097},{"abi_enter_us":5111,"abi_return_us":500368,"end_us":500376,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5111,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500463,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500453}],"schema":2,"slots":[{"at_us":500463,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191562470b060-e8d6-60","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":501352,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5231,"slot":0,"token":"18e191564277edf8-e8d6-61"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e191564277edf8-e8d6-61","watchdog":false,"workers_joined":1,"writes":[{"at_us":5271,"bytes":57,"kind":"head","slot":0},{"at_us":5280,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":86,"abi_return_us":5014,"end_us":5016,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5045,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5041},{"abi_enter_us":5058,"abi_return_us":5064,"end_us":5064,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5058,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5096,"operation":"read","slot":0,"start_us":5094},{"abi_enter_us":5108,"abi_return_us":501205,"end_us":501211,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5108,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501290,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501284}],"schema":2,"slots":[{"at_us":501290,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191564277edf8-e8d6-61","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":502163,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5244,"slot":0,"token":"18e191566080bde8-e8d6-62"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e191566080bde8-e8d6-62","watchdog":false,"workers_joined":1,"writes":[{"at_us":5264,"bytes":57,"kind":"head","slot":0},{"at_us":5279,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":95,"operation":"handoff","start_us":95},{"abi_enter_us":115,"abi_return_us":4988,"end_us":4990,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":115,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5026,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5022},{"abi_enter_us":5041,"abi_return_us":5046,"end_us":5046,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5041,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5072,"operation":"read","slot":0,"start_us":5070},{"abi_enter_us":5087,"abi_return_us":501670,"end_us":501731,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5087,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502064,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501900}],"schema":2,"slots":[{"at_us":502064,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191566080bde8-e8d6-62","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":501731,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5613,"slot":0,"token":"18e191567f33b858-e8d6-63"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e191567f33b858-e8d6-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":5697,"bytes":57,"kind":"head","slot":0},{"at_us":5706,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":985,"operation":"dispatch","outcome":"pending","slot":0,"start_us":533},{"body_reads":0,"deadline_us":500000,"end_us":1036,"operation":"handoff","start_us":1036},{"abi_enter_us":1054,"abi_return_us":3968,"end_us":3972,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1051,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":4016,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4010},{"abi_enter_us":4029,"abi_return_us":4035,"end_us":4036,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4029,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4133,"operation":"read","slot":0,"start_us":4130},{"abi_enter_us":4147,"abi_return_us":501637,"end_us":501642,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4147,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501711,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501704}],"schema":2,"slots":[{"at_us":501711,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191567f33b858-e8d6-63","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":6398,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6459,"slot":0,"token":"18e191569d5c5cb8-e8d6-64"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191569d5c5cb8-e8d6-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":6474,"bytes":57,"kind":"head","slot":0},{"at_us":6480,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":94,"abi_return_us":6164,"end_us":6170,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6226,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6219},{"abi_enter_us":6242,"abi_return_us":6246,"end_us":6247,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6242,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6273,"operation":"read","slot":0,"start_us":6270},{"abi_enter_us":6285,"abi_return_us":6286,"end_us":6286,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6285,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6313,"operation":"read","slot":0,"start_us":6312},{"abi_enter_us":6323,"abi_return_us":6323,"end_us":6324,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6323,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6342,"operation":"read","slot":0,"start_us":6342},{"abi_enter_us":6352,"abi_return_us":6355,"end_us":6356,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6352,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6374,"operation":"read","slot":0,"start_us":6374},{"end_us":6386,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6383}],"schema":2,"slots":[{"at_us":6386,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191569d5c5cb8-e8d6-64","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":6390,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6449,"slot":0,"token":"18e191569deb3ed8-e8d6-65"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191569deb3ed8-e8d6-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":6458,"bytes":57,"kind":"head","slot":0},{"at_us":6463,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":97,"abi_return_us":6202,"end_us":6208,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":97,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6268,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6260},{"abi_enter_us":6285,"abi_return_us":6291,"end_us":6292,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6285,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6322,"operation":"read","slot":0,"start_us":6319},{"abi_enter_us":6335,"abi_return_us":6337,"end_us":6337,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6335,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6362,"operation":"read","slot":0,"start_us":6361},{"end_us":6377,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6374}],"schema":2,"slots":[{"at_us":6377,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191569deb3ed8-e8d6-65","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":5328,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5309,"slot":0,"token":"18e191569e71a130-e8d6-66"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191569e71a130-e8d6-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":5324,"bytes":57,"kind":"head","slot":0},{"at_us":5331,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":120,"abi_return_us":5116,"end_us":5121,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":120,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5153,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5148},{"abi_enter_us":5169,"abi_return_us":5175,"end_us":5175,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5169,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5200,"operation":"read","slot":0,"start_us":5198},{"abi_enter_us":5212,"abi_return_us":5212,"end_us":5213,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5212,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5242,"operation":"read","slot":0,"start_us":5241},{"abi_enter_us":5252,"abi_return_us":5252,"end_us":5253,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5252,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5270,"operation":"read","slot":0,"start_us":5270},{"abi_enter_us":5279,"abi_return_us":5281,"end_us":5281,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5279,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5302,"operation":"read","slot":0,"start_us":5301},{"end_us":5313,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5310}],"schema":2,"slots":[{"at_us":5313,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191569e71a130-e8d6-66","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":6005,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6054,"slot":0,"token":"18e191569eec6ac8-e8d6-67"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191569eec6ac8-e8d6-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":6064,"bytes":57,"kind":"head","slot":0},{"at_us":6069,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":83,"abi_return_us":5884,"end_us":5887,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5910,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5906},{"abi_enter_us":5923,"abi_return_us":5926,"end_us":5926,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5923,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5949,"operation":"read","slot":0,"start_us":5948},{"abi_enter_us":5959,"abi_return_us":5960,"end_us":5961,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5959,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5980,"operation":"read","slot":0,"start_us":5980},{"end_us":5995,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5992}],"schema":2,"slots":[{"at_us":5995,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191569eec6ac8-e8d6-67","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5861,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5926,"slot":0,"token":"18e191569f71ded8-e8d6-68"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191569f71ded8-e8d6-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":5938,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":59,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":88,"abi_return_us":5738,"end_us":5743,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5795,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5790},{"abi_enter_us":5808,"abi_return_us":5810,"end_us":5811,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5808,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5836,"operation":"read","slot":0,"start_us":5834},{"end_us":5849,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5846}],"schema":2,"slots":[{"at_us":5849,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191569f71ded8-e8d6-68","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6380,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6427,"slot":0,"token":"18e191569feacfc8-e8d6-69"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191569feacfc8-e8d6-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":6436,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":59,"abi_return_us":6268,"end_us":6272,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6311,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6307},{"abi_enter_us":6323,"abi_return_us":6326,"end_us":6326,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6323,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6355,"operation":"read","slot":0,"start_us":6354},{"end_us":6367,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6365}],"schema":2,"slots":[{"at_us":6367,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191569feacfc8-e8d6-69","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5535,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5579,"slot":0,"token":"18e19156a06a7390-e8d6-6a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156a06a7390-e8d6-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5588,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":81,"abi_return_us":5438,"end_us":5441,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5476,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5471},{"abi_enter_us":5488,"abi_return_us":5490,"end_us":5490,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5488,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5511,"operation":"read","slot":0,"start_us":5509},{"end_us":5524,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5521}],"schema":2,"slots":[{"at_us":5524,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19156a06a7390-e8d6-6a","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5275,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5327,"slot":0,"token":"18e19156a0dd5d88-e8d6-6b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156a0dd5d88-e8d6-6b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5337,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"abi_enter_us":71,"abi_return_us":5172,"end_us":5176,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5209,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5205},{"abi_enter_us":5222,"abi_return_us":5224,"end_us":5224,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5222,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5250,"operation":"read","slot":0,"start_us":5248},{"end_us":5263,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5261}],"schema":2,"slots":[{"at_us":5263,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19156a0dd5d88-e8d6-6b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5122,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5212,"slot":0,"token":"18e19156a14ba018-e8d6-6c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156a14ba018-e8d6-6c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5218,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":69,"abi_return_us":5072,"end_us":5074,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5096,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5092},{"end_us":5108,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5108}],"schema":2,"slots":[{"at_us":5108,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19156a14ba018-e8d6-6c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5191,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5230,"slot":0,"token":"18e19156a1b42200-e8d6-6d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156a1b42200-e8d6-6d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5240,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":57,"abi_return_us":5128,"end_us":5133,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5165,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5161},{"end_us":5177,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5177}],"schema":2,"slots":[{"at_us":5177,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19156a1b42200-e8d6-6d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5789,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5884,"slot":0,"token":"18e19156a21dc110-e8d6-6e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156a21dc110-e8d6-6e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5892,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"abi_enter_us":61,"abi_return_us":5741,"end_us":5743,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5765,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5760},{"end_us":5777,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5777}],"schema":2,"slots":[{"at_us":5777,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19156a21dc110-e8d6-6e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5213,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5221,"slot":0,"token":"18e19156a2904578-e8d6-6f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156a2904578-e8d6-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5233,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"abi_enter_us":63,"abi_return_us":5155,"end_us":5162,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5188,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5184},{"end_us":5200,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5200}],"schema":2,"slots":[{"at_us":5200,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19156a2904578-e8d6-6f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57748,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5293,"slot":0,"token":"18e19156a2fbff98-e8d6-70"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156a2fbff98-e8d6-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":5302,"bytes":66,"kind":"head","slot":0},{"at_us":5306,"bytes":6,"kind":"body","slot":0},{"at_us":57753,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"abi_enter_us":60,"abi_return_us":5141,"end_us":5146,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5179,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5176},{"abi_enter_us":5190,"abi_return_us":5194,"end_us":5194,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5189,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5215,"operation":"read","slot":0,"start_us":5213},{"abi_enter_us":5231,"abi_return_us":57631,"end_us":57635,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5231,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":57717,"operation":"read","slot":0,"start_us":57714},{"end_us":57733,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57729}],"schema":2,"slots":[{"at_us":57733,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19156a2fbff98-e8d6-70","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56672,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6066,"slot":0,"token":"18e19156a691b530-e8d6-71"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156a691b530-e8d6-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":6082,"bytes":66,"kind":"head","slot":0},{"at_us":6089,"bytes":6,"kind":"body","slot":0},{"at_us":56709,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":77,"abi_return_us":5878,"end_us":5880,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5904,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5900},{"abi_enter_us":5915,"abi_return_us":5918,"end_us":5918,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5915,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5938,"operation":"read","slot":0,"start_us":5937},{"abi_enter_us":5951,"abi_return_us":56550,"end_us":56554,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5951,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":56640,"operation":"read","slot":0,"start_us":56637},{"end_us":56657,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56653}],"schema":2,"slots":[{"at_us":56657,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19156a691b530-e8d6-71","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58291,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5855,"slot":0,"token":"18e19156aa20a468-e8d6-72"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156aa20a468-e8d6-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":5871,"bytes":66,"kind":"head","slot":0},{"at_us":5876,"bytes":6,"kind":"body","slot":0},{"at_us":58514,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":67,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":151,"operation":"handoff","start_us":151},{"abi_enter_us":210,"abi_return_us":5614,"end_us":5619,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":210,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5676,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5671},{"abi_enter_us":5688,"abi_return_us":5692,"end_us":5693,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5688,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5736,"operation":"read","slot":0,"start_us":5734},{"abi_enter_us":5762,"abi_return_us":58212,"end_us":58215,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5762,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58265,"operation":"read","slot":0,"start_us":58263},{"end_us":58279,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58275}],"schema":2,"slots":[{"at_us":58279,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19156aa20a468-e8d6-72","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":60524,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5570,"slot":0,"token":"18e19156adc4a628-e8d6-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156adc4a628-e8d6-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":5578,"bytes":66,"kind":"head","slot":0},{"at_us":5583,"bytes":6,"kind":"body","slot":0},{"at_us":60624,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":106,"abi_return_us":5389,"end_us":5390,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5415,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5411},{"abi_enter_us":5426,"abi_return_us":5429,"end_us":5429,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5426,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5678,"operation":"read","slot":0,"start_us":5677},{"abi_enter_us":5700,"abi_return_us":60415,"end_us":60419,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5699,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":60491,"operation":"read","slot":0,"start_us":60487},{"end_us":60507,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60502}],"schema":2,"slots":[{"at_us":60507,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19156adc4a628-e8d6-73","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":6083,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6151,"slot":0,"token":"18e19156b18845a8-e8d6-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156b18845a8-e8d6-74","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":61,"abi_return_us":6015,"end_us":6020,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6054,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6048},{"end_us":6069,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6069}],"schema":2,"slots":[{"at_us":6069,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19156b18845a8-e8d6-74","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":6332,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6416,"slot":0,"token":"18e19156b1fec980-e8d6-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156b1fec980-e8d6-75","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":57,"abi_return_us":6275,"end_us":6278,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6306,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6301},{"end_us":6317,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6317}],"schema":2,"slots":[{"at_us":6317,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19156b1fec980-e8d6-75","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":5925,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6086,"slot":0,"token":"18e19156b277f508-e8d6-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156b277f508-e8d6-76","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":33},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":80,"abi_return_us":5864,"end_us":5867,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5899,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5893},{"end_us":5911,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5911}],"schema":2,"slots":[{"at_us":5911,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19156b277f508-e8d6-76","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":6372,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6426,"slot":0,"token":"18e19156b2edf410-e8d6-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156b2edf410-e8d6-77","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"abi_enter_us":67,"abi_return_us":6306,"end_us":6309,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6332,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6329},{"end_us":6359,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6359}],"schema":2,"slots":[{"at_us":6359,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19156b2edf410-e8d6-77","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":59370,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5456,"slot":0,"token":"18e19156b3689698-e8d6-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156b3689698-e8d6-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":5469,"bytes":57,"kind":"head","slot":0},{"at_us":5474,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":54,"abi_return_us":5328,"end_us":5330,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5363,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5359},{"bytes":1,"capacity":1,"end_us":5387,"operation":"read","slot":0,"start_us":5382},{"capacity":1,"end_us":59298,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5409},{"end_us":59353,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59347}],"schema":2,"slots":[{"at_us":59353,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19156b3689698-e8d6-78","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":61349,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6395,"slot":0,"token":"18e19156b70ff7a0-e8d6-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156b70ff7a0-e8d6-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":6401,"bytes":57,"kind":"head","slot":0},{"at_us":6405,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":57,"abi_return_us":6212,"end_us":6214,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6239,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6235},{"abi_enter_us":6249,"abi_return_us":6252,"end_us":6252,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6249,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6275,"operation":"read","slot":0,"start_us":6274},{"abi_enter_us":6285,"abi_return_us":61262,"end_us":61265,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6285,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":61319,"operation":"read","slot":0,"start_us":61315},{"end_us":61335,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":61331}],"schema":2,"slots":[{"at_us":61335,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19156b70ff7a0-e8d6-79","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":59840,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6394,"slot":0,"token":"18e19156badd3be0-e8d6-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156badd3be0-e8d6-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6400,"bytes":57,"kind":"head","slot":0},{"at_us":6404,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":64,"abi_return_us":6210,"end_us":6211,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6272,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6268},{"abi_enter_us":6284,"abi_return_us":6287,"end_us":6288,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6283,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6313,"operation":"read","slot":0,"start_us":6311},{"abi_enter_us":6322,"abi_return_us":59732,"end_us":59734,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6322,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":59809,"operation":"read","slot":0,"start_us":59806},{"end_us":59825,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59822}],"schema":2,"slots":[{"at_us":59825,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19156badd3be0-e8d6-7a","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":60597,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6422,"slot":0,"token":"18e19156be9517a8-e8d6-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156be9517a8-e8d6-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6428,"bytes":57,"kind":"head","slot":0},{"at_us":6432,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":74,"abi_return_us":6249,"end_us":6252,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6285,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6281},{"abi_enter_us":6297,"abi_return_us":6301,"end_us":6301,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6297,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6323,"operation":"read","slot":0,"start_us":6321},{"abi_enter_us":6334,"abi_return_us":60499,"end_us":60502,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6334,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":60561,"operation":"read","slot":0,"start_us":60557},{"end_us":60582,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60578}],"schema":2,"slots":[{"at_us":60582,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19156be9517a8-e8d6-7b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":60104,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6021,"slot":0,"token":"18e19156c259e7d8-e8d6-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19156c259e7d8-e8d6-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6028,"bytes":57,"kind":"head","slot":0},{"at_us":6033,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"abi_enter_us":62,"abi_return_us":5841,"end_us":5843,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5867,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5862},{"abi_enter_us":5878,"abi_return_us":5881,"end_us":5882,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5878,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5903,"operation":"read","slot":0,"start_us":5902},{"abi_enter_us":5913,"abi_return_us":60003,"end_us":60006,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5913,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":60069,"operation":"read","slot":0,"start_us":60064},{"end_us":60087,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60081}],"schema":2,"slots":[{"at_us":60087,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19156c259e7d8-e8d6-7c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":500871,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6424,"slot":0,"token":"18e19156c6155d80-e8d6-7d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e19156c6155d80-e8d6-7d","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":58,"abi_return_us":500725,"end_us":500732,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500846,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500846}],"schema":2,"slots":[{"at_us":500846,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19156c6155d80-e8d6-7d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":500044,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6422,"slot":0,"token":"18e19156e40d1670-e8d6-7e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e19156e40d1670-e8d6-7e","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":63,"abi_return_us":499841,"end_us":499915,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500015,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500015}],"schema":2,"slots":[{"at_us":500015,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19156e40d1670-e8d6-7e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":501434,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5951,"slot":0,"token":"18e1915702268448-e8d6-7f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1915702268448-e8d6-7f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":97,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":153,"operation":"handoff","start_us":153},{"abi_enter_us":169,"abi_return_us":501311,"end_us":501321,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":169,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501407,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501407}],"schema":2,"slots":[{"at_us":501407,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915702268448-e8d6-7f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":500622,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6501,"slot":0,"token":"18e1915720365d00-e8d6-80"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1915720365d00-e8d6-80","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":62,"operation":"dispatch","outcome":"pending","slot":0,"start_us":34},{"body_reads":0,"deadline_us":500000,"end_us":91,"operation":"handoff","start_us":91},{"abi_enter_us":104,"abi_return_us":500517,"end_us":500524,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500599,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500599}],"schema":2,"slots":[{"at_us":500599,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915720365d00-e8d6-80","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":502125,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6136,"slot":0,"token":"18e191573e2de710-e8d6-81"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e191573e2de710-e8d6-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":6157,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":71,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"abi_enter_us":127,"abi_return_us":5877,"end_us":5881,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":126,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5945,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5934},{"abi_enter_us":5961,"abi_return_us":502017,"end_us":502022,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5961,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502107,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502101}],"schema":2,"slots":[{"at_us":502107,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191573e2de710-e8d6-81","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":501357,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5163,"slot":0,"token":"18e191575c3d2770-e8d6-82"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e191575c3d2770-e8d6-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":5171,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":79,"abi_return_us":4970,"end_us":4972,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5006,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5002},{"abi_enter_us":5020,"abi_return_us":501127,"end_us":501141,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5020,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501267,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501232}],"schema":2,"slots":[{"at_us":501267,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191575c3d2770-e8d6-82","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":501353,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5775,"slot":0,"token":"18e191577a3f3cb8-e8d6-83"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e191577a3f3cb8-e8d6-83","watchdog":false,"workers_joined":1,"writes":[{"at_us":5793,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":98,"operation":"handoff","start_us":98},{"abi_enter_us":134,"abi_return_us":5523,"end_us":5528,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":134,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5580,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5574},{"abi_enter_us":5594,"abi_return_us":501263,"end_us":501269,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5594,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501333,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501326}],"schema":2,"slots":[{"at_us":501333,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191577a3f3cb8-e8d6-83","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":500307,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5245,"slot":0,"token":"18e19157984103e0-e8d6-84"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19157984103e0-e8d6-84","watchdog":false,"workers_joined":1,"writes":[{"at_us":5257,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":135,"operation":"handoff","start_us":135},{"abi_enter_us":191,"abi_return_us":5085,"end_us":5089,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":191,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5133,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5126},{"abi_enter_us":5146,"abi_return_us":500219,"end_us":500222,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5146,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500289,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500283}],"schema":2,"slots":[{"at_us":500289,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19157984103e0-e8d6-84","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":500886,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6404,"slot":0,"token":"18e19157b63190e0-e8d6-85"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e19157b63190e0-e8d6-85","watchdog":false,"workers_joined":1,"writes":[{"at_us":6410,"bytes":57,"kind":"head","slot":0},{"at_us":6418,"bytes":1,"kind":"body","slot":0},{"at_us":308691,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":62,"abi_return_us":6218,"end_us":6220,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6249,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6241},{"abi_enter_us":6261,"abi_return_us":6264,"end_us":6265,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6261,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6291,"operation":"read","slot":0,"start_us":6289},{"abi_enter_us":6302,"abi_return_us":308498,"end_us":308502,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6302,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":308577,"operation":"read","slot":0,"start_us":308574},{"abi_enter_us":308593,"abi_return_us":500766,"end_us":500772,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308593,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500836,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500830}],"schema":2,"slots":[{"at_us":500836,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19157b63190e0-e8d6-85","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":502177,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6019,"slot":0,"token":"18e19157d4370570-e8d6-86"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e19157d4370570-e8d6-86","watchdog":false,"workers_joined":1,"writes":[{"at_us":6026,"bytes":57,"kind":"head","slot":0},{"at_us":6032,"bytes":1,"kind":"body","slot":0},{"at_us":306075,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":69,"abi_return_us":5818,"end_us":5820,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5846,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5842},{"abi_enter_us":5856,"abi_return_us":5859,"end_us":5859,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5856,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5883,"operation":"read","slot":0,"start_us":5880},{"abi_enter_us":5894,"abi_return_us":305867,"end_us":305871,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5893,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":305949,"operation":"read","slot":0,"start_us":305946},{"abi_enter_us":305967,"abi_return_us":502069,"end_us":502079,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":305967,"status":"FastlyStatus::OK","timeout_ms":194},{"end_us":502137,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502131}],"schema":2,"slots":[{"at_us":502137,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19157d4370570-e8d6-86","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":500840,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5243,"slot":0,"token":"18e19157f253ea00-e8d6-87"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e19157f253ea00-e8d6-87","watchdog":false,"workers_joined":1,"writes":[{"at_us":5255,"bytes":57,"kind":"head","slot":0},{"at_us":5262,"bytes":1,"kind":"body","slot":0},{"at_us":305748,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":82,"abi_return_us":5090,"end_us":5100,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5153,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5146},{"abi_enter_us":5173,"abi_return_us":5176,"end_us":5176,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5172,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5206,"operation":"read","slot":0,"start_us":5203},{"abi_enter_us":5218,"abi_return_us":305581,"end_us":305587,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5218,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":305650,"operation":"read","slot":0,"start_us":305647},{"abi_enter_us":305666,"abi_return_us":500742,"end_us":500747,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":305666,"status":"FastlyStatus::OK","timeout_ms":194},{"end_us":500819,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500811}],"schema":2,"slots":[{"at_us":500819,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19157f253ea00-e8d6-87","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":501130,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6441,"slot":0,"token":"18e19158105ec178-e8d6-88"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e19158105ec178-e8d6-88","watchdog":false,"workers_joined":1,"writes":[{"at_us":6452,"bytes":57,"kind":"head","slot":0},{"at_us":6459,"bytes":1,"kind":"body","slot":0},{"at_us":310931,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":82,"abi_return_us":6259,"end_us":6264,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6316,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6311},{"abi_enter_us":6330,"abi_return_us":6336,"end_us":6336,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6330,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6366,"operation":"read","slot":0,"start_us":6364},{"abi_enter_us":6378,"abi_return_us":310684,"end_us":310691,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6378,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":310801,"operation":"read","slot":0,"start_us":310797},{"abi_enter_us":310816,"abi_return_us":501036,"end_us":501040,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310816,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":501106,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501099}],"schema":2,"slots":[{"at_us":501106,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19158105ec178-e8d6-88","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501648,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6456,"slot":0,"token":"18e191582e67b0a8-e8d6-89"},{"path":"/readiness/siblings/1","received_us":6459,"slot":1,"token":"18e191582e67b0a8-e8d6-89"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e191582e67b0a8-e8d6-89","watchdog":false,"workers_joined":2,"writes":[{"at_us":6469,"bytes":57,"kind":"head","slot":1},{"at_us":6476,"bytes":57,"kind":"head","slot":0},{"at_us":6487,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":99,"operation":"dispatch","outcome":"pending","slot":1,"start_us":78},{"abi_enter_us":125,"abi_return_us":6269,"end_us":6271,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":125,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6303,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6299},{"abi_enter_us":6316,"abi_return_us":6317,"end_us":6317,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6316,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6333,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6333},{"abi_enter_us":6344,"abi_return_us":6347,"end_us":6347,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6344,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6370,"operation":"read","slot":1,"start_us":6368},{"abi_enter_us":6381,"abi_return_us":6382,"end_us":6382,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6381,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6404,"operation":"read","slot":1,"start_us":6403},{"abi_enter_us":6414,"abi_return_us":6414,"end_us":6415,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6413,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6433,"operation":"read","slot":1,"start_us":6433},{"abi_enter_us":6443,"abi_return_us":6445,"end_us":6445,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6443,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6477,"operation":"read","slot":1,"start_us":6477},{"end_us":6488,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6486},{"abi_enter_us":6499,"abi_return_us":501550,"end_us":501554,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6499,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501627,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501620}],"schema":2,"slots":[{"at_us":501627,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6488,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191582e67b0a8-e8d6-89","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501050,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5802,"slot":0,"token":"18e191584c866610-e8d6-8a"},{"path":"/readiness/siblings/1","received_us":5815,"slot":1,"token":"18e191584c866610-e8d6-8a"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e191584c866610-e8d6-8a","watchdog":false,"workers_joined":2,"writes":[{"at_us":5810,"bytes":57,"kind":"head","slot":0},{"at_us":5823,"bytes":57,"kind":"head","slot":1},{"at_us":5830,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":97,"operation":"dispatch","outcome":"pending","slot":1,"start_us":82},{"abi_enter_us":124,"abi_return_us":5585,"end_us":5587,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":124,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5637,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5633},{"abi_enter_us":5654,"abi_return_us":5692,"end_us":5693,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5654,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5713,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5712},{"abi_enter_us":5725,"abi_return_us":5728,"end_us":5728,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5725,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5755,"operation":"read","slot":1,"start_us":5753},{"abi_enter_us":5766,"abi_return_us":5769,"end_us":5769,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5766,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5797,"operation":"read","slot":1,"start_us":5797},{"end_us":5809,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5807},{"abi_enter_us":5821,"abi_return_us":500950,"end_us":500955,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5821,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501032,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501025}],"schema":2,"slots":[{"at_us":501032,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5809,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191584c866610-e8d6-8a","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":500567,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5246,"slot":0,"token":"18e191586a982ee0-e8d6-8b"},{"path":"/readiness/siblings/1","received_us":5252,"slot":1,"token":"18e191586a982ee0-e8d6-8b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e191586a982ee0-e8d6-8b","watchdog":false,"workers_joined":2,"writes":[{"at_us":5261,"bytes":57,"kind":"head","slot":0},{"at_us":5270,"bytes":57,"kind":"head","slot":1},{"at_us":5280,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":116,"operation":"dispatch","outcome":"pending","slot":1,"start_us":100},{"body_reads":0,"deadline_us":500000,"end_us":131,"operation":"handoff","start_us":131},{"abi_enter_us":149,"abi_return_us":5002,"end_us":5007,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":149,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5053,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5046},{"abi_enter_us":5066,"abi_return_us":5068,"end_us":5069,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5066,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5083,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5082},{"abi_enter_us":5093,"abi_return_us":5097,"end_us":5097,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5093,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5121,"operation":"read","slot":1,"start_us":5119},{"abi_enter_us":5132,"abi_return_us":5133,"end_us":5133,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5131,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5151,"operation":"read","slot":1,"start_us":5150},{"abi_enter_us":5160,"abi_return_us":5160,"end_us":5161,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5160,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5177,"operation":"read","slot":1,"start_us":5177},{"abi_enter_us":5188,"abi_return_us":5190,"end_us":5190,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5188,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5211,"operation":"read","slot":1,"start_us":5211},{"end_us":5223,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5219},{"abi_enter_us":5234,"abi_return_us":500472,"end_us":500479,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5234,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500547,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500539}],"schema":2,"slots":[{"at_us":500547,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5223,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191586a982ee0-e8d6-8b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":502150,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5464,"slot":0,"token":"18e1915888a77710-e8d6-8c"},{"path":"/readiness/siblings/1","received_us":5478,"slot":1,"token":"18e1915888a77710-e8d6-8c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1915888a77710-e8d6-8c","watchdog":false,"workers_joined":2,"writes":[{"at_us":5481,"bytes":57,"kind":"head","slot":0},{"at_us":5491,"bytes":57,"kind":"head","slot":1},{"at_us":5498,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":88,"operation":"dispatch","outcome":"pending","slot":1,"start_us":64},{"body_reads":0,"deadline_us":500000,"end_us":108,"operation":"handoff","start_us":108},{"abi_enter_us":121,"abi_return_us":5292,"end_us":5294,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":121,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5328,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5323},{"abi_enter_us":5342,"abi_return_us":5344,"end_us":5344,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5342,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5362,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5361},{"abi_enter_us":5371,"abi_return_us":5374,"end_us":5374,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5371,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5426,"operation":"read","slot":1,"start_us":5424},{"abi_enter_us":5442,"abi_return_us":5446,"end_us":5446,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5442,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5472,"operation":"read","slot":1,"start_us":5471},{"end_us":5484,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5481},{"abi_enter_us":5501,"abi_return_us":502059,"end_us":502063,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5501,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502133,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502127}],"schema":2,"slots":[{"at_us":502133,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5484,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915888a77710-e8d6-8c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":501060,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5234,"slot":0,"token":"18e19158a6c61cd8-e8d6-8d"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19158a6c61cd8-e8d6-8d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5247,"bytes":57,"kind":"head","slot":0},{"at_us":5254,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"abi_enter_us":68,"abi_return_us":5082,"end_us":5087,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5137,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5133},{"abi_enter_us":5151,"abi_return_us":5157,"end_us":5157,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5151,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5185,"operation":"read","slot":0,"start_us":5183},{"abi_enter_us":5197,"abi_return_us":500914,"end_us":500926,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5197,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501027,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501012}],"schema":2,"slots":[{"at_us":501027,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19158a6c61cd8-e8d6-8d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":502157,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5224,"slot":0,"token":"18e19158c4ca5118-e8d6-8e"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19158c4ca5118-e8d6-8e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5231,"bytes":57,"kind":"head","slot":0},{"at_us":5238,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":71,"abi_return_us":4983,"end_us":4986,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5022,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5018},{"abi_enter_us":5036,"abi_return_us":5039,"end_us":5040,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5036,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5072,"operation":"read","slot":0,"start_us":5070},{"abi_enter_us":5088,"abi_return_us":499776,"end_us":499782,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5088,"status":"FastlyStatus::OK","timeout_ms":494},{"abi_enter_us":499838,"abi_return_us":502107,"end_us":502108,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":499838,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502140,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502134}],"schema":2,"slots":[{"at_us":502140,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19158c4ca5118-e8d6-8e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":500450,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5248,"slot":0,"token":"18e19158e2e0ffd0-e8d6-8f"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19158e2e0ffd0-e8d6-8f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5254,"bytes":57,"kind":"head","slot":0},{"at_us":5258,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"abi_enter_us":93,"abi_return_us":5045,"end_us":5046,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5068,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5063},{"abi_enter_us":5079,"abi_return_us":5082,"end_us":5082,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5079,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5102,"operation":"read","slot":0,"start_us":5101},{"abi_enter_us":5112,"abi_return_us":500358,"end_us":500367,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5112,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500432,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500426}],"schema":2,"slots":[{"at_us":500432,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19158e2e0ffd0-e8d6-8f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":501661,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5926,"slot":0,"token":"18e1915900d95500-e8d6-90"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1915900d95500-e8d6-90","watchdog":false,"workers_joined":1,"writes":[{"at_us":5935,"bytes":57,"kind":"head","slot":0},{"at_us":5939,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"abi_enter_us":86,"abi_return_us":5741,"end_us":5743,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5777,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5773},{"abi_enter_us":5791,"abi_return_us":5795,"end_us":5795,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5791,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5824,"operation":"read","slot":0,"start_us":5822},{"abi_enter_us":5839,"abi_return_us":501508,"end_us":501514,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5838,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501640,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501632}],"schema":2,"slots":[{"at_us":501640,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915900d95500-e8d6-90","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":6459,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6435,"slot":0,"token":"18e191591ee7b6b8-e8d6-91"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191591ee7b6b8-e8d6-91","watchdog":false,"workers_joined":1,"writes":[{"at_us":6447,"bytes":57,"kind":"head","slot":0},{"at_us":6452,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":60,"abi_return_us":6242,"end_us":6248,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6303,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6299},{"abi_enter_us":6317,"abi_return_us":6325,"end_us":6325,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6317,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6352,"operation":"read","slot":0,"start_us":6350},{"abi_enter_us":6363,"abi_return_us":6364,"end_us":6365,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6363,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6382,"operation":"read","slot":0,"start_us":6381},{"abi_enter_us":6390,"abi_return_us":6391,"end_us":6391,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6390,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6411,"operation":"read","slot":0,"start_us":6411},{"abi_enter_us":6419,"abi_return_us":6421,"end_us":6421,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6419,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6437,"operation":"read","slot":0,"start_us":6437},{"end_us":6448,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6445}],"schema":2,"slots":[{"at_us":6448,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191591ee7b6b8-e8d6-91","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":5554,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5915,"slot":0,"token":"18e191591f882508-e8d6-92"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191591f882508-e8d6-92","watchdog":false,"workers_joined":1,"writes":[{"at_us":5934,"bytes":57,"kind":"head","slot":0},{"at_us":5978,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":170,"operation":"dispatch","outcome":"pending","slot":0,"start_us":31},{"abi_enter_us":219,"abi_return_us":5345,"end_us":5352,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":219,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5407,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5403},{"abi_enter_us":5422,"abi_return_us":5438,"end_us":5439,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5422,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5486,"operation":"read","slot":0,"start_us":5484},{"abi_enter_us":5498,"abi_return_us":5502,"end_us":5502,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5498,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5528,"operation":"read","slot":0,"start_us":5528},{"end_us":5542,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5538}],"schema":2,"slots":[{"at_us":5542,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191591f882508-e8d6-92","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":6498,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6502,"slot":0,"token":"18e1915920159be0-e8d6-93"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915920159be0-e8d6-93","watchdog":false,"workers_joined":1,"writes":[{"at_us":6514,"bytes":57,"kind":"head","slot":0},{"at_us":6520,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"abi_enter_us":104,"abi_return_us":6270,"end_us":6276,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6322,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6317},{"abi_enter_us":6338,"abi_return_us":6343,"end_us":6344,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6338,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6370,"operation":"read","slot":0,"start_us":6367},{"abi_enter_us":6381,"abi_return_us":6382,"end_us":6382,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6381,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6405,"operation":"read","slot":0,"start_us":6405},{"abi_enter_us":6416,"abi_return_us":6416,"end_us":6417,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6416,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6436,"operation":"read","slot":0,"start_us":6436},{"abi_enter_us":6446,"abi_return_us":6448,"end_us":6448,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6446,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6470,"operation":"read","slot":0,"start_us":6469},{"end_us":6482,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6479}],"schema":2,"slots":[{"at_us":6482,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915920159be0-e8d6-93","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":6488,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6508,"slot":0,"token":"18e1915920a50e88-e8d6-94"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915920a50e88-e8d6-94","watchdog":false,"workers_joined":1,"writes":[{"at_us":6521,"bytes":57,"kind":"head","slot":0},{"at_us":6526,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":99,"abi_return_us":6322,"end_us":6326,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":99,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6373,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6368},{"abi_enter_us":6387,"abi_return_us":6392,"end_us":6392,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6387,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6421,"operation":"read","slot":0,"start_us":6419},{"abi_enter_us":6436,"abi_return_us":6438,"end_us":6439,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6436,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6462,"operation":"read","slot":0,"start_us":6462},{"end_us":6475,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6471}],"schema":2,"slots":[{"at_us":6475,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915920a50e88-e8d6-94","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5286,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5328,"slot":0,"token":"18e1915921319ee8-e8d6-95"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915921319ee8-e8d6-95","watchdog":false,"workers_joined":1,"writes":[{"at_us":5341,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":47,"abi_return_us":5168,"end_us":5172,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":47,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5216,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5211},{"abi_enter_us":5229,"abi_return_us":5232,"end_us":5232,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5229,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5258,"operation":"read","slot":0,"start_us":5257},{"end_us":5273,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5270}],"schema":2,"slots":[{"at_us":5273,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915921319ee8-e8d6-95","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5800,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5835,"slot":0,"token":"18e1915921a16fe8-e8d6-96"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915921a16fe8-e8d6-96","watchdog":false,"workers_joined":1,"writes":[{"at_us":5846,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":58,"abi_return_us":5693,"end_us":5697,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5732,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5729},{"abi_enter_us":5746,"abi_return_us":5748,"end_us":5748,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5746,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5775,"operation":"read","slot":0,"start_us":5773},{"end_us":5788,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5785}],"schema":2,"slots":[{"at_us":5788,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915921a16fe8-e8d6-96","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5404,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5372,"slot":0,"token":"18e1915922178278-e8d6-97"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915922178278-e8d6-97","watchdog":false,"workers_joined":1,"writes":[{"at_us":5387,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"abi_enter_us":71,"abi_return_us":5278,"end_us":5284,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5333,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5328},{"abi_enter_us":5347,"abi_return_us":5350,"end_us":5350,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5347,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5378,"operation":"read","slot":0,"start_us":5376},{"end_us":5391,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5388}],"schema":2,"slots":[{"at_us":5391,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915922178278-e8d6-97","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5092,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5180,"slot":0,"token":"18e1915922886cb8-e8d6-98"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915922886cb8-e8d6-98","watchdog":false,"workers_joined":1,"writes":[{"at_us":5187,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":75,"abi_return_us":5004,"end_us":5005,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5031,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5027},{"abi_enter_us":5043,"abi_return_us":5045,"end_us":5045,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5043,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5070,"operation":"read","slot":0,"start_us":5068},{"end_us":5081,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5079}],"schema":2,"slots":[{"at_us":5081,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915922886cb8-e8d6-98","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5670,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5734,"slot":0,"token":"18e1915922f5e428-e8d6-99"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915922f5e428-e8d6-99","watchdog":false,"workers_joined":1,"writes":[{"at_us":5745,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":65,"abi_return_us":5613,"end_us":5616,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5643,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5640},{"end_us":5656,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5656}],"schema":2,"slots":[{"at_us":5656,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915922f5e428-e8d6-99","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6065,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5797,"slot":0,"token":"18e191592368e590-e8d6-9a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191592368e590-e8d6-9a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5829,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":76,"abi_return_us":5868,"end_us":5882,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5998,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5991},{"end_us":6028,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6027}],"schema":2,"slots":[{"at_us":6028,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e191592368e590-e8d6-9a","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5250,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5695,"slot":0,"token":"18e1915923e6bc68-e8d6-9b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915923e6bc68-e8d6-9b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5709,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":239,"operation":"dispatch","outcome":"pending","slot":0,"start_us":126},{"body_reads":0,"deadline_us":500000,"end_us":273,"operation":"handoff","start_us":273},{"abi_enter_us":285,"abi_return_us":5184,"end_us":5189,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":285,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5220,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5216},{"end_us":5233,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5233}],"schema":2,"slots":[{"at_us":5233,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915923e6bc68-e8d6-9b","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6347,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6457,"slot":0,"token":"18e19159245b7738-e8d6-9c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19159245b7738-e8d6-9c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6475,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":74,"abi_return_us":6303,"end_us":6303,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6324,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6320},{"end_us":6335,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6335}],"schema":2,"slots":[{"at_us":6335,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19159245b7738-e8d6-9c","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58664,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5266,"slot":0,"token":"18e1915924ddca80-e8d6-9d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915924ddca80-e8d6-9d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5315,"bytes":66,"kind":"head","slot":0},{"at_us":5356,"bytes":6,"kind":"body","slot":0},{"at_us":59063,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"abi_enter_us":41,"abi_return_us":4570,"end_us":4571,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":41,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4595,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4592},{"abi_enter_us":4607,"abi_return_us":4612,"end_us":4612,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4607,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4645,"operation":"read","slot":0,"start_us":4643},{"abi_enter_us":4656,"abi_return_us":58550,"end_us":58555,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4656,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":1,"end_us":58621,"operation":"read","slot":0,"start_us":58617},{"end_us":58649,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58644}],"schema":2,"slots":[{"at_us":58649,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915924ddca80-e8d6-9d","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59253,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5202,"slot":0,"token":"18e1915928911e20-e8d6-9e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915928911e20-e8d6-9e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5211,"bytes":66,"kind":"head","slot":0},{"at_us":5217,"bytes":6,"kind":"body","slot":0},{"at_us":59329,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":101,"abi_return_us":5015,"end_us":5019,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":101,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5057,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5053},{"abi_enter_us":5073,"abi_return_us":5077,"end_us":5077,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5073,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5106,"operation":"read","slot":0,"start_us":5104},{"abi_enter_us":5118,"abi_return_us":59140,"end_us":59144,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5118,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":59218,"operation":"read","slot":0,"start_us":59212},{"end_us":59236,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59232}],"schema":2,"slots":[{"at_us":59236,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915928911e20-e8d6-9e","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59197,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5198,"slot":0,"token":"18e191592c41c240-e8d6-9f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191592c41c240-e8d6-9f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5204,"bytes":66,"kind":"head","slot":0},{"at_us":5213,"bytes":6,"kind":"body","slot":0},{"at_us":59309,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"abi_enter_us":86,"abi_return_us":4980,"end_us":4982,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5008,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5005},{"abi_enter_us":5021,"abi_return_us":5024,"end_us":5024,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5021,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5045,"operation":"read","slot":0,"start_us":5043},{"abi_enter_us":5055,"abi_return_us":59106,"end_us":59110,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5054,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":59165,"operation":"read","slot":0,"start_us":59159},{"end_us":59181,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59177}],"schema":2,"slots":[{"at_us":59181,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191592c41c240-e8d6-9f","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57917,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6541,"slot":0,"token":"18e191592ff14168-e8d6-a0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191592ff14168-e8d6-a0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6560,"bytes":66,"kind":"head","slot":0},{"at_us":6567,"bytes":6,"kind":"body","slot":0},{"at_us":57970,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":78,"abi_return_us":6383,"end_us":6388,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6444,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6440},{"abi_enter_us":6456,"abi_return_us":6466,"end_us":6467,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6456,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6497,"operation":"read","slot":0,"start_us":6496},{"abi_enter_us":6507,"abi_return_us":57814,"end_us":57818,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6507,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":57880,"operation":"read","slot":0,"start_us":57876},{"end_us":57899,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57894}],"schema":2,"slots":[{"at_us":57899,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191592ff14168-e8d6-a0","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":5998,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6129,"slot":0,"token":"18e19159338b9698-e8d6-a1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19159338b9698-e8d6-a1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"abi_enter_us":73,"abi_return_us":5950,"end_us":5952,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5973,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5969},{"end_us":5985,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5985}],"schema":2,"slots":[{"at_us":5985,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19159338b9698-e8d6-a1","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":6324,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6387,"slot":0,"token":"18e19159340166c0-e8d6-a2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19159340166c0-e8d6-a2","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":49,"abi_return_us":6266,"end_us":6269,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":48,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6295,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6293},{"end_us":6309,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6309}],"schema":2,"slots":[{"at_us":6309,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19159340166c0-e8d6-a2","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":6184,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6271,"slot":0,"token":"18e19159347bc2f8-e8d6-a3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19159347bc2f8-e8d6-a3","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":53,"operation":"handoff","start_us":53},{"abi_enter_us":63,"abi_return_us":6136,"end_us":6138,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6160,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6157},{"end_us":6171,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6171}],"schema":2,"slots":[{"at_us":6171,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19159347bc2f8-e8d6-a3","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":6284,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6396,"slot":0,"token":"18e1915934f47d38-e8d6-a4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915934f47d38-e8d6-a4","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":45,"operation":"handoff","start_us":45},{"abi_enter_us":56,"abi_return_us":6238,"end_us":6240,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6258,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6256},{"end_us":6270,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6270}],"schema":2,"slots":[{"at_us":6270,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915934f47d38-e8d6-a4","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":60254,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6272,"slot":0,"token":"18e19159356ea6a8-e8d6-a5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19159356ea6a8-e8d6-a5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6281,"bytes":57,"kind":"head","slot":0},{"at_us":6286,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":71,"abi_return_us":6079,"end_us":6081,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6141,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6115},{"bytes":1,"capacity":1,"end_us":6169,"operation":"read","slot":0,"start_us":6164},{"capacity":1,"end_us":60183,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":6205},{"end_us":60238,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60232}],"schema":2,"slots":[{"at_us":60238,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19159356ea6a8-e8d6-a5","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":60605,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5949,"slot":0,"token":"18e19159392624b0-e8d6-a6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19159392624b0-e8d6-a6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5962,"bytes":57,"kind":"head","slot":0},{"at_us":5967,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":62,"abi_return_us":5776,"end_us":5780,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5821,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5817},{"abi_enter_us":5833,"abi_return_us":5837,"end_us":5838,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5833,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5860,"operation":"read","slot":0,"start_us":5858},{"abi_enter_us":5872,"abi_return_us":60451,"end_us":60454,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5872,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":60569,"operation":"read","slot":0,"start_us":60565},{"end_us":60586,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60582}],"schema":2,"slots":[{"at_us":60586,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19159392624b0-e8d6-a6","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":59083,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5178,"slot":0,"token":"18e191593cf0fbd8-e8d6-a7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e191593cf0fbd8-e8d6-a7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5195,"bytes":57,"kind":"head","slot":0},{"at_us":5224,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":73,"abi_return_us":4981,"end_us":4983,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5042,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5037},{"abi_enter_us":5056,"abi_return_us":5063,"end_us":5064,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5056,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5110,"operation":"read","slot":0,"start_us":5107},{"abi_enter_us":5122,"abi_return_us":58989,"end_us":58993,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5122,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":59051,"operation":"read","slot":0,"start_us":59048},{"end_us":59067,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59063}],"schema":2,"slots":[{"at_us":59067,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e191593cf0fbd8-e8d6-a7","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":55518,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5200,"slot":0,"token":"18e19159409a25e8-e8d6-a8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19159409a25e8-e8d6-a8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5213,"bytes":57,"kind":"head","slot":0},{"at_us":5218,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":57,"operation":"handoff","start_us":57},{"abi_enter_us":73,"abi_return_us":5053,"end_us":5057,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5095,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5091},{"abi_enter_us":5106,"abi_return_us":5111,"end_us":5112,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5106,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5132,"operation":"read","slot":0,"start_us":5131},{"abi_enter_us":5145,"abi_return_us":55406,"end_us":55410,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5145,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":55482,"operation":"read","slot":0,"start_us":55478},{"end_us":55500,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55494}],"schema":2,"slots":[{"at_us":55500,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19159409a25e8-e8d6-a8","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"truncated","completed_us":58459,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6469,"slot":0,"token":"18e19159443b2620-e8d6-a9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e19159443b2620-e8d6-a9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6483,"bytes":57,"kind":"head","slot":0},{"at_us":6489,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":125,"operation":"handoff","start_us":125},{"abi_enter_us":152,"abi_return_us":6121,"end_us":6126,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":152,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6174,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6168},{"abi_enter_us":6187,"abi_return_us":6217,"end_us":6219,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6187,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6266,"operation":"read","slot":0,"start_us":6263},{"abi_enter_us":6282,"abi_return_us":58350,"end_us":58354,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6282,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":58424,"operation":"read","slot":0,"start_us":58421},{"end_us":58443,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58439}],"schema":2,"slots":[{"at_us":58443,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19159443b2620-e8d6-a9","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":501413,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5191,"slot":0,"token":"18e1915947df27e0-e8d6-aa"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1915947df27e0-e8d6-aa","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":85,"abi_return_us":501317,"end_us":501326,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501392,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501392}],"schema":2,"slots":[{"at_us":501392,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915947df27e0-e8d6-aa","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":501305,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6420,"slot":0,"token":"18e1915965dd2a30-e8d6-ab"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1915965dd2a30-e8d6-ab","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":56,"abi_return_us":501228,"end_us":501234,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501289,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501289}],"schema":2,"slots":[{"at_us":501289,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915965dd2a30-e8d6-ab","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":501462,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6450,"slot":0,"token":"18e1915983e04148-e8d6-ac"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1915983e04148-e8d6-ac","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":76,"operation":"handoff","start_us":76},{"abi_enter_us":90,"abi_return_us":499318,"end_us":499324,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"abi_enter_us":499375,"abi_return_us":501428,"end_us":501429,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499375,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501449,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501449}],"schema":2,"slots":[{"at_us":501449,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915983e04148-e8d6-ac","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"silent-head","completed_us":501410,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6426,"slot":0,"token":"18e19159a1e39ac8-e8d6-ad"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e19159a1e39ac8-e8d6-ad","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":74,"abi_return_us":501329,"end_us":501341,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501391,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501391}],"schema":2,"slots":[{"at_us":501391,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e19159a1e39ac8-e8d6-ad","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":500130,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5245,"slot":0,"token":"18e19159bfe31030-e8d6-ae"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19159bfe31030-e8d6-ae","watchdog":false,"workers_joined":1,"writes":[{"at_us":5254,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":86,"abi_return_us":4998,"end_us":5001,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5050,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5046},{"abi_enter_us":5063,"abi_return_us":500045,"end_us":500050,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5063,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500111,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500105}],"schema":2,"slots":[{"at_us":500111,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19159bfe31030-e8d6-ae","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":501681,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6063,"slot":0,"token":"18e19159ddd352f8-e8d6-af"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19159ddd352f8-e8d6-af","watchdog":false,"workers_joined":1,"writes":[{"at_us":6074,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":54,"abi_return_us":5825,"end_us":5827,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5855,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5851},{"abi_enter_us":5867,"abi_return_us":501493,"end_us":501500,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5867,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501596,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501589}],"schema":2,"slots":[{"at_us":501596,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19159ddd352f8-e8d6-af","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":500134,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5949,"slot":0,"token":"18e19159fbe2cdf0-e8d6-b0"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e19159fbe2cdf0-e8d6-b0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5961,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":94,"operation":"handoff","start_us":94},{"abi_enter_us":108,"abi_return_us":5685,"end_us":5686,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":108,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5720,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5716},{"abi_enter_us":5729,"abi_return_us":500050,"end_us":500056,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5729,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500116,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500110}],"schema":2,"slots":[{"at_us":500116,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e19159fbe2cdf0-e8d6-b0","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"pending-body","completed_us":501776,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6434,"slot":0,"token":"18e1915a19d80640-e8d6-b1"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1915a19d80640-e8d6-b1","watchdog":false,"workers_joined":1,"writes":[{"at_us":6445,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"abi_enter_us":93,"abi_return_us":6241,"end_us":6245,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6283,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6279},{"abi_enter_us":6295,"abi_return_us":501633,"end_us":501639,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6295,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501729,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501720}],"schema":2,"slots":[{"at_us":501729,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915a19d80640-e8d6-b1","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":501937,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5935,"slot":0,"token":"18e1915a37e1b4d8-e8d6-b2"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1915a37e1b4d8-e8d6-b2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5949,"bytes":57,"kind":"head","slot":0},{"at_us":5955,"bytes":1,"kind":"body","slot":0},{"at_us":309700,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"abi_enter_us":93,"abi_return_us":5673,"end_us":5678,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5737,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5732},{"abi_enter_us":5750,"abi_return_us":5759,"end_us":5759,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5750,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5784,"operation":"read","slot":0,"start_us":5781},{"abi_enter_us":5795,"abi_return_us":309565,"end_us":309582,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5795,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309713,"operation":"read","slot":0,"start_us":309708},{"abi_enter_us":309728,"abi_return_us":501812,"end_us":501815,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309728,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501889,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501873}],"schema":2,"slots":[{"at_us":501889,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915a37e1b4d8-e8d6-b2","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":500991,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5878,"slot":0,"token":"18e1915a55f9c320-e8d6-b3"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1915a55f9c320-e8d6-b3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5904,"bytes":57,"kind":"head","slot":0},{"at_us":5916,"bytes":1,"kind":"body","slot":0},{"at_us":308684,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":73,"abi_return_us":5668,"end_us":5675,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5731,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5726},{"abi_enter_us":5747,"abi_return_us":5753,"end_us":5753,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5747,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5783,"operation":"read","slot":0,"start_us":5781},{"abi_enter_us":5795,"abi_return_us":308433,"end_us":308442,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5795,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":308820,"operation":"read","slot":0,"start_us":308816},{"abi_enter_us":308837,"abi_return_us":500902,"end_us":500905,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308837,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500971,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500965}],"schema":2,"slots":[{"at_us":500971,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915a55f9c320-e8d6-b3","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":500814,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5288,"slot":0,"token":"18e1915a7407d2d0-e8d6-b4"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1915a7407d2d0-e8d6-b4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5294,"bytes":57,"kind":"head","slot":0},{"at_us":5301,"bytes":1,"kind":"body","slot":0},{"at_us":309629,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":81,"abi_return_us":5072,"end_us":5074,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5106,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5102},{"abi_enter_us":5124,"abi_return_us":5131,"end_us":5132,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5124,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5158,"operation":"read","slot":0,"start_us":5156},{"abi_enter_us":5170,"abi_return_us":309421,"end_us":309427,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5170,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309505,"operation":"read","slot":0,"start_us":309502},{"abi_enter_us":309519,"abi_return_us":500726,"end_us":500730,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309519,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500793,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500787}],"schema":2,"slots":[{"at_us":500793,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915a7407d2d0-e8d6-b4","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"trickle","completed_us":501095,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5933,"slot":0,"token":"18e1915a9212f868-e8d6-b5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1915a9212f868-e8d6-b5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5941,"bytes":57,"kind":"head","slot":0},{"at_us":5947,"bytes":1,"kind":"body","slot":0},{"at_us":310981,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":57,"operation":"handoff","start_us":57},{"abi_enter_us":83,"abi_return_us":5718,"end_us":5721,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5757,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5753},{"abi_enter_us":5771,"abi_return_us":5775,"end_us":5776,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5771,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5808,"operation":"read","slot":0,"start_us":5806},{"abi_enter_us":5820,"abi_return_us":310762,"end_us":310768,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5820,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":310853,"operation":"read","slot":0,"start_us":310847},{"abi_enter_us":310869,"abi_return_us":500967,"end_us":500972,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310868,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":501058,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501049}],"schema":2,"slots":[{"at_us":501058,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915a9212f868-e8d6-b5","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501474,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6496,"slot":1,"token":"18e1915ab0276cd0-e8d6-b6"},{"path":"/readiness/siblings/0","received_us":6538,"slot":0,"token":"18e1915ab0276cd0-e8d6-b6"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1915ab0276cd0-e8d6-b6","watchdog":false,"workers_joined":2,"writes":[{"at_us":6523,"bytes":57,"kind":"head","slot":1},{"at_us":6545,"bytes":57,"kind":"head","slot":0},{"at_us":11580,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":108,"operation":"dispatch","outcome":"pending","slot":1,"start_us":96},{"abi_enter_us":161,"abi_return_us":6230,"end_us":6232,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":161,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6273,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6268},{"abi_enter_us":6289,"abi_return_us":6291,"end_us":6291,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6289,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6307,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6306},{"abi_enter_us":6322,"abi_return_us":11318,"end_us":11321,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6321,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":11374,"operation":"read","slot":1,"start_us":11371},{"abi_enter_us":11385,"abi_return_us":11388,"end_us":11388,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11385,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11407,"operation":"read","slot":1,"start_us":11407},{"abi_enter_us":11419,"abi_return_us":11420,"end_us":11420,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11419,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11439,"operation":"read","slot":1,"start_us":11438},{"abi_enter_us":11447,"abi_return_us":11449,"end_us":11449,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11447,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11469,"operation":"read","slot":1,"start_us":11469},{"end_us":11482,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11477},{"abi_enter_us":11495,"abi_return_us":501378,"end_us":501384,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11495,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501456,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501451}],"schema":2,"slots":[{"at_us":501456,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11482,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915ab0276cd0-e8d6-b6","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":501111,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5527,"slot":0,"token":"18e1915ace4927a8-e8d6-b7"},{"path":"/readiness/siblings/1","received_us":5533,"slot":1,"token":"18e1915ace4927a8-e8d6-b7"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1915ace4927a8-e8d6-b7","watchdog":false,"workers_joined":2,"writes":[{"at_us":5539,"bytes":57,"kind":"head","slot":0},{"at_us":5547,"bytes":57,"kind":"head","slot":1},{"at_us":5558,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":95,"operation":"dispatch","outcome":"pending","slot":1,"start_us":70},{"abi_enter_us":112,"abi_return_us":5343,"end_us":5347,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5385,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5381},{"abi_enter_us":5398,"abi_return_us":5400,"end_us":5400,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5398,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5414,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5414},{"abi_enter_us":5424,"abi_return_us":5428,"end_us":5428,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5424,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5456,"operation":"read","slot":1,"start_us":5454},{"abi_enter_us":5468,"abi_return_us":5470,"end_us":5470,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5468,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5493,"operation":"read","slot":1,"start_us":5492},{"end_us":5504,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5502},{"abi_enter_us":5518,"abi_return_us":501019,"end_us":501025,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5518,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501093,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501087}],"schema":2,"slots":[{"at_us":501093,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5504,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915ace4927a8-e8d6-b7","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":500581,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6327,"slot":0,"token":"18e1915aec5969d8-e8d6-b8"},{"path":"/readiness/siblings/1","received_us":6336,"slot":1,"token":"18e1915aec5969d8-e8d6-b8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1915aec5969d8-e8d6-b8","watchdog":false,"workers_joined":2,"writes":[{"at_us":6345,"bytes":57,"kind":"head","slot":0},{"at_us":6355,"bytes":57,"kind":"head","slot":1},{"at_us":6363,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":118,"operation":"dispatch","outcome":"pending","slot":1,"start_us":95},{"body_reads":0,"deadline_us":500000,"end_us":142,"operation":"handoff","start_us":142},{"abi_enter_us":159,"abi_return_us":6169,"end_us":6174,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":159,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6226,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6222},{"abi_enter_us":6241,"abi_return_us":6243,"end_us":6243,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6241,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6258,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6257},{"abi_enter_us":6268,"abi_return_us":6271,"end_us":6271,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6268,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6300,"operation":"read","slot":1,"start_us":6297},{"abi_enter_us":6312,"abi_return_us":6313,"end_us":6313,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6312,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6333,"operation":"read","slot":1,"start_us":6332},{"abi_enter_us":6346,"abi_return_us":6347,"end_us":6347,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6346,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6366,"operation":"read","slot":1,"start_us":6365},{"abi_enter_us":6375,"abi_return_us":6376,"end_us":6377,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6375,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6398,"operation":"read","slot":1,"start_us":6398},{"end_us":6412,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6410},{"abi_enter_us":6424,"abi_return_us":500484,"end_us":500490,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6424,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500563,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500555}],"schema":2,"slots":[{"at_us":500563,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6412,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915aec5969d8-e8d6-b8","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"siblings","completed_us":502027,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6182,"slot":0,"token":"18e1915b0a752588-e8d6-b9"},{"path":"/readiness/siblings/1","received_us":6184,"slot":1,"token":"18e1915b0a752588-e8d6-b9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1915b0a752588-e8d6-b9","watchdog":false,"workers_joined":2,"writes":[{"at_us":6195,"bytes":57,"kind":"head","slot":0},{"at_us":6203,"bytes":57,"kind":"head","slot":1},{"at_us":6210,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":101,"operation":"dispatch","outcome":"pending","slot":1,"start_us":77},{"body_reads":0,"deadline_us":500000,"end_us":115,"operation":"handoff","start_us":115},{"abi_enter_us":134,"abi_return_us":5928,"end_us":5931,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":133,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5961,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5957},{"abi_enter_us":5974,"abi_return_us":5976,"end_us":5976,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5974,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5992,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5991},{"abi_enter_us":6002,"abi_return_us":6006,"end_us":6006,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6002,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6033,"operation":"read","slot":1,"start_us":6031},{"abi_enter_us":6043,"abi_return_us":6046,"end_us":6046,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6043,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6071,"operation":"read","slot":1,"start_us":6070},{"end_us":6082,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6080},{"abi_enter_us":6095,"abi_return_us":499861,"end_us":499866,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6095,"status":"FastlyStatus::OK","timeout_ms":493},{"abi_enter_us":499926,"abi_return_us":501984,"end_us":501985,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":499926,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502012,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502007}],"schema":2,"slots":[{"at_us":502012,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6082,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915b0a752588-e8d6-b9","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":500534,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5195,"slot":0,"token":"18e1915b28a56ef0-e8d6-ba"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1915b28a56ef0-e8d6-ba","watchdog":false,"workers_joined":1,"writes":[{"at_us":5205,"bytes":57,"kind":"head","slot":0},{"at_us":5210,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":68,"abi_return_us":4975,"end_us":4977,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5005,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5001},{"abi_enter_us":5023,"abi_return_us":5029,"end_us":5029,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5022,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5055,"operation":"read","slot":0,"start_us":5053},{"abi_enter_us":5066,"abi_return_us":500434,"end_us":500441,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5066,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500512,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500506}],"schema":2,"slots":[{"at_us":500512,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915b28a56ef0-e8d6-ba","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":501176,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5699,"slot":0,"token":"18e1915b469fe318-e8d6-bb"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1915b469fe318-e8d6-bb","watchdog":false,"workers_joined":1,"writes":[{"at_us":5714,"bytes":57,"kind":"head","slot":0},{"at_us":5720,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":71,"abi_return_us":5546,"end_us":5550,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5603,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5598},{"abi_enter_us":5616,"abi_return_us":5622,"end_us":5623,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5616,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5652,"operation":"read","slot":0,"start_us":5649},{"abi_enter_us":5664,"abi_return_us":501081,"end_us":501087,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5664,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501147,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501133}],"schema":2,"slots":[{"at_us":501147,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915b469fe318-e8d6-bb","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":501181,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5398,"slot":0,"token":"18e1915b64a07990-e8d6-bc"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1915b64a07990-e8d6-bc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5407,"bytes":57,"kind":"head","slot":0},{"at_us":5415,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"abi_enter_us":70,"abi_return_us":5230,"end_us":5234,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5261,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5256},{"abi_enter_us":5272,"abi_return_us":5276,"end_us":5276,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5272,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5302,"operation":"read","slot":0,"start_us":5300},{"abi_enter_us":5313,"abi_return_us":501093,"end_us":501100,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5313,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501162,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501156}],"schema":2,"slots":[{"at_us":501162,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915b64a07990-e8d6-bc","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"exhausted","completed_us":501254,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6434,"slot":0,"token":"18e1915b82aa6e78-e8d6-bd"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1915b82aa6e78-e8d6-bd","watchdog":false,"workers_joined":1,"writes":[{"at_us":6444,"bytes":57,"kind":"head","slot":0},{"at_us":6448,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"abi_enter_us":83,"abi_return_us":6261,"end_us":6267,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6316,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6312},{"abi_enter_us":6329,"abi_return_us":6333,"end_us":6333,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6329,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6365,"operation":"read","slot":0,"start_us":6362},{"abi_enter_us":6377,"abi_return_us":500851,"end_us":500868,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6377,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501178,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501160}],"schema":2,"slots":[{"at_us":501178,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915b82aa6e78-e8d6-bd","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":7005,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6475,"slot":0,"token":"18e1915ba0b4d890-e8d6-be"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba0b4d890-e8d6-be","watchdog":false,"workers_joined":1,"writes":[{"at_us":6492,"bytes":57,"kind":"head","slot":0},{"at_us":6498,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":129,"operation":"dispatch","outcome":"pending","slot":0,"start_us":56},{"abi_enter_us":170,"abi_return_us":6071,"end_us":6075,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":170,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6133,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6128},{"abi_enter_us":6148,"abi_return_us":6832,"end_us":6833,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6148,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6876,"operation":"read","slot":0,"start_us":6873},{"abi_enter_us":6889,"abi_return_us":6890,"end_us":6891,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6889,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6916,"operation":"read","slot":0,"start_us":6915},{"abi_enter_us":6926,"abi_return_us":6927,"end_us":6927,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6926,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6946,"operation":"read","slot":0,"start_us":6946},{"abi_enter_us":6956,"abi_return_us":6958,"end_us":6958,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6956,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6980,"operation":"read","slot":0,"start_us":6980},{"end_us":6993,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6989}],"schema":2,"slots":[{"at_us":6993,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915ba0b4d890-e8d6-be","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":5156,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5232,"slot":0,"token":"18e1915ba1595dc0-e8d6-bf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba1595dc0-e8d6-bf","watchdog":false,"workers_joined":1,"writes":[{"at_us":5240,"bytes":57,"kind":"head","slot":0},{"at_us":5248,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":96,"abi_return_us":5003,"end_us":5006,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5042,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5038},{"abi_enter_us":5056,"abi_return_us":5060,"end_us":5061,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5056,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5092,"operation":"read","slot":0,"start_us":5089},{"abi_enter_us":5104,"abi_return_us":5106,"end_us":5106,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5104,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5131,"operation":"read","slot":0,"start_us":5130},{"end_us":5143,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5141}],"schema":2,"slots":[{"at_us":5143,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915ba1595dc0-e8d6-bf","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":5990,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5971,"slot":0,"token":"18e1915ba1d033a0-e8d6-c0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba1d033a0-e8d6-c0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5978,"bytes":57,"kind":"head","slot":0},{"at_us":5984,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":85,"operation":"handoff","start_us":85},{"abi_enter_us":103,"abi_return_us":5768,"end_us":5773,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5820,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5815},{"abi_enter_us":5836,"abi_return_us":5842,"end_us":5842,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5836,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5867,"operation":"read","slot":0,"start_us":5865},{"abi_enter_us":5879,"abi_return_us":5880,"end_us":5880,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5879,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5903,"operation":"read","slot":0,"start_us":5902},{"abi_enter_us":5913,"abi_return_us":5913,"end_us":5913,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5912,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5932,"operation":"read","slot":0,"start_us":5932},{"abi_enter_us":5941,"abi_return_us":5943,"end_us":5943,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5941,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5963,"operation":"read","slot":0,"start_us":5963},{"end_us":5975,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5972}],"schema":2,"slots":[{"at_us":5975,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915ba1d033a0-e8d6-c0","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"eof","completed_us":6478,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6446,"slot":0,"token":"18e1915ba27178c8-e8d6-c1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba27178c8-e8d6-c1","watchdog":false,"workers_joined":1,"writes":[{"at_us":6461,"bytes":57,"kind":"head","slot":0},{"at_us":6467,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"abi_enter_us":92,"abi_return_us":6258,"end_us":6263,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6311,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6306},{"abi_enter_us":6324,"abi_return_us":6329,"end_us":6329,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6324,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6377,"operation":"read","slot":0,"start_us":6354},{"abi_enter_us":6412,"abi_return_us":6417,"end_us":6418,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6412,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6450,"operation":"read","slot":0,"start_us":6449},{"end_us":6464,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6460}],"schema":2,"slots":[{"at_us":6464,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915ba27178c8-e8d6-c1","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5283,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5350,"slot":0,"token":"18e1915ba30350b8-e8d6-c2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba30350b8-e8d6-c2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5361,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":66,"abi_return_us":5176,"end_us":5180,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5221,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5215},{"abi_enter_us":5233,"abi_return_us":5235,"end_us":5235,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5233,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5257,"operation":"read","slot":0,"start_us":5256},{"end_us":5271,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5267}],"schema":2,"slots":[{"at_us":5271,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915ba30350b8-e8d6-c2","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5422,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5429,"slot":0,"token":"18e1915ba372d780-e8d6-c3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba372d780-e8d6-c3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5435,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":56,"abi_return_us":5282,"end_us":5286,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5331,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5327},{"abi_enter_us":5347,"abi_return_us":5349,"end_us":5349,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5347,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5383,"operation":"read","slot":0,"start_us":5381},{"end_us":5407,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5402}],"schema":2,"slots":[{"at_us":5407,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915ba372d780-e8d6-c3","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5271,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5270,"slot":0,"token":"18e1915ba3e2cf90-e8d6-c4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba3e2cf90-e8d6-c4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5284,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":25,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"abi_enter_us":52,"abi_return_us":5175,"end_us":5177,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5210,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5206},{"abi_enter_us":5222,"abi_return_us":5223,"end_us":5224,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5222,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5249,"operation":"read","slot":0,"start_us":5248},{"end_us":5260,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5259}],"schema":2,"slots":[{"at_us":5260,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915ba3e2cf90-e8d6-c4","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5849,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5920,"slot":0,"token":"18e1915ba4510668-e8d6-c5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba4510668-e8d6-c5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5926,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"abi_enter_us":81,"abi_return_us":5754,"end_us":5757,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5785,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5781},{"abi_enter_us":5796,"abi_return_us":5798,"end_us":5798,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5796,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5822,"operation":"read","slot":0,"start_us":5821},{"end_us":5836,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5833}],"schema":2,"slots":[{"at_us":5836,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915ba4510668-e8d6-c5","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6397,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6484,"slot":0,"token":"18e1915ba4ca06f8-e8d6-c6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba4ca06f8-e8d6-c6","watchdog":false,"workers_joined":1,"writes":[{"at_us":6492,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":76,"abi_return_us":6339,"end_us":6342,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6371,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6366},{"end_us":6383,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6383}],"schema":2,"slots":[{"at_us":6383,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915ba4ca06f8-e8d6-c6","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6314,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6446,"slot":0,"token":"18e1915ba548eb58-e8d6-c7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba548eb58-e8d6-c7","watchdog":false,"workers_joined":1,"writes":[{"at_us":6464,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":68,"abi_return_us":6259,"end_us":6262,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6285,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6283},{"end_us":6297,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6297}],"schema":2,"slots":[{"at_us":6297,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915ba548eb58-e8d6-c7","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6295,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6418,"slot":0,"token":"18e1915ba5c4a720-e8d6-c8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba5c4a720-e8d6-c8","watchdog":false,"workers_joined":1,"writes":[{"at_us":6425,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":83,"abi_return_us":6238,"end_us":6240,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6265,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6261},{"end_us":6278,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6278}],"schema":2,"slots":[{"at_us":6278,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915ba5c4a720-e8d6-c8","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6106,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6193,"slot":0,"token":"18e1915ba6425eb8-e8d6-c9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba6425eb8-e8d6-c9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6200,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"abi_enter_us":75,"abi_return_us":6041,"end_us":6047,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6077,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6073},{"end_us":6091,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6091}],"schema":2,"slots":[{"at_us":6091,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915ba6425eb8-e8d6-c9","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59469,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6439,"slot":0,"token":"18e1915ba6bbad68-e8d6-ca"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915ba6bbad68-e8d6-ca","watchdog":false,"workers_joined":1,"writes":[{"at_us":6452,"bytes":66,"kind":"head","slot":0},{"at_us":6457,"bytes":6,"kind":"body","slot":0},{"at_us":59557,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"abi_enter_us":79,"abi_return_us":6263,"end_us":6268,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6310,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6306},{"abi_enter_us":6322,"abi_return_us":6327,"end_us":6328,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6322,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6355,"operation":"read","slot":0,"start_us":6352},{"abi_enter_us":6366,"abi_return_us":59371,"end_us":59373,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6366,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":59436,"operation":"read","slot":0,"start_us":59433},{"end_us":59454,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59448}],"schema":2,"slots":[{"at_us":59454,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915ba6bbad68-e8d6-ca","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58884,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5678,"slot":0,"token":"18e1915baa6c5570-e8d6-cb"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915baa6c5570-e8d6-cb","watchdog":false,"workers_joined":1,"writes":[{"at_us":5686,"bytes":66,"kind":"head","slot":0},{"at_us":5692,"bytes":6,"kind":"body","slot":0},{"at_us":58967,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":68,"abi_return_us":5488,"end_us":5491,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5526,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5520},{"abi_enter_us":5539,"abi_return_us":5542,"end_us":5543,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5539,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5569,"operation":"read","slot":0,"start_us":5567},{"abi_enter_us":5581,"abi_return_us":58783,"end_us":58787,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5581,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":58849,"operation":"read","slot":0,"start_us":58846},{"end_us":58865,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58861}],"schema":2,"slots":[{"at_us":58865,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915baa6c5570-e8d6-cb","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":60516,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6449,"slot":0,"token":"18e1915bae126a70-e8d6-cc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915bae126a70-e8d6-cc","watchdog":false,"workers_joined":1,"writes":[{"at_us":6464,"bytes":66,"kind":"head","slot":0},{"at_us":6472,"bytes":6,"kind":"body","slot":0},{"at_us":60560,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":74,"abi_return_us":6291,"end_us":6294,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6334,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6328},{"abi_enter_us":6347,"abi_return_us":6350,"end_us":6351,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6347,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6374,"operation":"read","slot":0,"start_us":6372},{"abi_enter_us":6385,"abi_return_us":60390,"end_us":60396,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6385,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":60473,"operation":"read","slot":0,"start_us":60469},{"end_us":60494,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60489}],"schema":2,"slots":[{"at_us":60494,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915bae126a70-e8d6-cc","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56224,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5521,"slot":0,"token":"18e1915bb1fd3930-e8d6-cd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915bb1fd3930-e8d6-cd","watchdog":false,"workers_joined":1,"writes":[{"at_us":5548,"bytes":66,"kind":"head","slot":0},{"at_us":5569,"bytes":6,"kind":"body","slot":0},{"at_us":56288,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":73,"operation":"handoff","start_us":73},{"abi_enter_us":84,"abi_return_us":5334,"end_us":5338,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5374,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5370},{"abi_enter_us":5387,"abi_return_us":5393,"end_us":5394,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5387,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5426,"operation":"read","slot":0,"start_us":5424},{"abi_enter_us":5445,"abi_return_us":56116,"end_us":56119,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5445,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":56192,"operation":"read","slot":0,"start_us":56189},{"end_us":56209,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56205}],"schema":2,"slots":[{"at_us":56209,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1915bb1fd3930-e8d6-cd","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":5584,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6734,"slot":0,"token":"18e1915bb5a65b70-e8d6-ce"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915bb5a65b70-e8d6-ce","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":233,"abi_return_us":5512,"end_us":5518,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":233,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5554,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5550},{"end_us":5569,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5569}],"schema":2,"slots":[{"at_us":5569,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915bb5a65b70-e8d6-ce","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":6187,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":7353,"slot":0,"token":"18e1915bb65befd0-e8d6-cf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915bb65befd0-e8d6-cf","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":478,"operation":"dispatch","outcome":"pending","slot":0,"start_us":36},{"abi_enter_us":782,"abi_return_us":6125,"end_us":6129,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":782,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6161,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6156},{"end_us":6173,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6173}],"schema":2,"slots":[{"at_us":6173,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915bb65befd0-e8d6-cf","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":6131,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6300,"slot":0,"token":"18e1915bb6f1b790-e8d6-d0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915bb6f1b790-e8d6-d0","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":79,"abi_return_us":6057,"end_us":6063,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6100,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6096},{"end_us":6115,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6115}],"schema":2,"slots":[{"at_us":6115,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915bb6f1b790-e8d6-d0","tolerance_us":5000}} +{"run":"stock","kind":"body-probe","probe":{"case":"head-error","completed_us":5162,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5367,"slot":0,"token":"18e1915bb7792770-e8d6-d1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1915bb7792770-e8d6-d1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":164,"operation":"handoff","start_us":164},{"abi_enter_us":228,"abi_return_us":5102,"end_us":5106,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":227,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5136,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5132},{"end_us":5149,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5149}],"schema":2,"slots":[{"at_us":5149,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1915bb7792770-e8d6-d1","tolerance_us":5000}} diff --git a/docs/audits/evidence/2026-10-24-fastly-local-recheck.json b/docs/audits/evidence/2026-10-24-fastly-local-recheck.json new file mode 100644 index 000000000..ee41e1cea --- /dev/null +++ b/docs/audits/evidence/2026-10-24-fastly-local-recheck.json @@ -0,0 +1,150 @@ +{ + "schema": 1, + "execution_date": "2026-10-24", + "timezone": "America/Los_Angeles", + "status": "Both local controls failed; cleanup-window overrun retained, G2 unchanged", + "qualification": false, + "edgezero_base_revision": "98a1d0e8c3ac3e0bd9e3f0986ae0390527d7a4a8", + "source_state": "Committed clean source at diagnostic build; local characterization, not frozen qualification. Later checker/replay changes are not part of these guest binaries.", + "command": "CARGO_NET_OFFLINE=true VICEROY_BIN= scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-readiness --output ", + "deployed": false, + "external_requests": false, + "origins": "bounded loopback only", + "unchanged_gate": { + "deadline_us": 500000, + "tolerance_us": 5000, + "maximum_completion_us": 505000, + "body_probes_per_run": 135, + "origin_requests_per_run": 147, + "timer_controls_per_run": 72, + "excluded_warmups_per_run": 3, + "overall_G2": "Failed", + "historical_evidence_rewritten": false, + "runtime_pins_changed": false, + "production_transport_changed": false + }, + "runs": [ + { + "id": "repaired", + "exit_code": 1, + "directory": "/private/tmp/edgezero-pr275-fastly-local-repaired-20261024-01", + "runtime_executable": "/private/tmp/edgezero-viceroy-0.21.0-readiness-20261023/target/debug/viceroy", + "runtime_version": "0.21.0", + "runtime_binary_sha256": "98ea0536478ce2e641f093b2619b7ac7c6c237402f140e5792d854575abc7100", + "guest_sha256": "fb70d997eba46e5f8b67676ae77e37a164ddba2f5b8065bd24d8761dd33d26ad", + "guest_bytes": 787543, + "events_sha256": "d7221d4c5e84047a188734da6cd132d943dbb66a4b0eb3b985aec1e81122783c", + "log_sha256": "57fde22ff6974e6a770703cb00cddd97f0c41ad78bd1b7bde961ba69bb5594fd", + "body_probes": 135, + "origin_requests": 147, + "maximum_completion_us": 505158, + "body_source_error_losses": 0, + "timer_controls": 72, + "late_timer_controls": [ + { + "source": "native-sleep", + "budget_ms": 100, + "elapsed_us": 105010, + "overrun_us": 5010 + }, + { + "source": "native-sleep", + "budget_ms": 100, + "elapsed_us": 105016, + "overrun_us": 5016 + } + ], + "validation_failure": "readiness: 1 readiness leg(s) failed:\ntrickle/selected/buffered/16384/1: reordered or late operation", + "qualification": false + }, + { + "id": "stock", + "exit_code": 1, + "directory": "/private/tmp/edgezero-pr275-fastly-local-stock-20261024-01", + "runtime_executable": "/Users/ag/.asdf/installs/rust/1.98.1/bin/viceroy", + "runtime_version": "0.21.0", + "runtime_binary_sha256": "751ff2c1a83063b109bc212b95be5f52e5ab6284b9027fa03aa229087eba3f30", + "guest_sha256": "fb70d997eba46e5f8b67676ae77e37a164ddba2f5b8065bd24d8761dd33d26ad", + "guest_bytes": 787543, + "events_sha256": "d64bf93164028e12d8b6758601342bebb795db6a8ce1e1852d9f176f6433da80", + "log_sha256": "c09cb8d333f5c2a07253c22cad2eaffbd8141577ec6c74123bfbfd7f9513d4f2", + "body_probes": 135, + "origin_requests": 147, + "maximum_completion_us": 502445, + "body_source_error_losses": 24, + "timer_controls": 72, + "late_timer_controls": [ + { + "source": "native-sleep", + "budget_ms": 100, + "elapsed_us": 105009, + "overrun_us": 5009 + }, + { + "source": "native-sleep", + "budget_ms": 500, + "elapsed_us": 505018, + "overrun_us": 5018 + }, + { + "source": "empty-select", + "budget_ms": 500, + "elapsed_us": 506343, + "overrun_us": 6343 + } + ], + "validation_failure": "readiness: 24 readiness leg(s) failed:\ntruncated/selected/buffered/1/0: wrong case terminal\ntruncated/selected/buffered/16384/0: wrong case terminal\ntruncated/selected/streamed/1/0: wrong case terminal\ntruncated/selected/streamed/16384/0: wrong case terminal\nmalformed-body/selected/buffered/1/0: wrong case terminal\nmalformed-body/selected/buffered/16384/0: wrong case terminal\nmalformed-body/selected/streamed/1/0: wrong case terminal\nmalformed-body/selected/streamed/16384/0: wrong case terminal\ntruncated/selected/buffered/1/1: wrong case terminal\ntruncated/selected/buffered/16384/1: wrong case terminal\ntruncated/selected/streamed/1/1: wrong case terminal\ntruncated/selected/streamed/16384/1: wrong case terminal\nmalformed-body/selected/buffered/1/1: wrong case terminal\nmalformed-body/selected/buffered/16384/1: wrong case terminal\nmalformed-body/selected/streamed/1/1: wrong case terminal\nmalformed-body/selected/streamed/16384/1: wrong case terminal\ntruncated/selected/buffered/1/2: wrong case terminal\ntruncated/selected/buffered/16384/2: wrong case terminal\ntruncated/selected/streamed/1/2: wrong case terminal\ntruncated/selected/streamed/16384/2: wrong case terminal\nmalformed-body/selected/buffered/1/2: wrong case terminal\nmalformed-body/selected/buffered/16384/2: wrong case terminal\nmalformed-body/selected/streamed/1/2: wrong case terminal\nmalformed-body/selected/streamed/16384/2: wrong case terminal", + "qualification": false + } + ], + "cleanup_window": { + "run": "repaired", + "case": "trickle/selected/buffered/16384/1", + "abi_return_us": 502614, + "selection_end_us": 502621, + "cleanup_start_us": 502707, + "cleanup_end_us": 505117, + "completed_us": 505158, + "cleanup_duration_us": 2410, + "completion_over_bound_us": 158, + "anchor": "finish starts before clearing the SDK-owned handle state and ends after release; JSON construction/checksum formatting follow the end sample", + "cause": "SDK handle release, clock sampling and descheduling are not separated by these timestamps; no specific cause proved" + }, + "interpretation": [ + "Both runs use identical guest bytes; runtime executable hashes differ explicitly.", + "The repaired runtime preserves selected source errors but fails the unchanged completion bound once. A within-bound ABI return does not excuse a late terminal cleanup/completion.", + "Stock still loses 24 selected source errors; its empty-select 500 ms control also returns beyond +5 ms. Timer controls preserve lateness rather than changing G2 acceptance.", + "No backdating, timeout reduction, extra tolerance, baseline replacement or blocking-read fallback is introduced.", + "Neither run executes Image Optimizer, proves performance parity or supplies finite B/H(K), a provider horizon approval or hosted evidence.", + "Full events/logs and binaries are identified by hash, not retained release assets. Canonical controls and all body probes are retained separately below." + ], + "selected_source_files": [ + { + "path": "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs", + "sha256": "47d3f8be81c1531ad732d12ac1d645d70deba7328a0d8ded5be12bc0b7e752eb" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/readiness.rs", + "sha256": "6010f19a22282cca84b675b3d3d271e3006d27a1a463c3162910d7baef73e7f8" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence/timers.rs", + "sha256": "07a4c42569e1403da5847d02b3b2a33380253d54f83ffd595f746ba30d7d8748" + }, + { + "path": "tests/fixtures/reusable-app/Cargo.lock", + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + { + "path": "patches/viceroy/0.21.0-body-readiness.patch", + "sha256": "8693ce34f82cc98dc272bf39379007b17a40dfc19138c55b445378fd9a163759" + } + ], + "retained_probes": { + "path": "docs/audits/evidence/2026-10-24-fastly-local-recheck-probes.jsonl", + "sha256": "0f2756c9f2b80e5ac9fa1a4a9f2b688bc833292f81f50092a5d7a36b6ba62e1b", + "records": 414, + "timer_controls": 144, + "body_probes": 270 + } +} diff --git a/docs/audits/evidence/2026-10-25-fastly-mixed-readiness-probes.jsonl b/docs/audits/evidence/2026-10-25-fastly-mixed-readiness-probes.jsonl new file mode 100644 index 000000000..f4083a761 --- /dev/null +++ b/docs/audits/evidence/2026-10-25-fastly-mixed-readiness-probes.jsonl @@ -0,0 +1,675 @@ +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":56177,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6885,"slot":0,"token":"18e1fd2befe396b8-622f-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2befe396b8-622f-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6897,"bytes":57,"kind":"head","slot":0},{"at_us":6903,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":1774,"operation":"dispatch","outcome":"pending","slot":0,"start_us":825},{"abi_enter_us":1841,"abi_return_us":7422,"end_us":7429,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1841,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":8070,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7500},{"bytes":1,"capacity":1,"end_us":8146,"operation":"read","slot":0,"start_us":8136},{"capacity":1,"end_us":56103,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":8202},{"end_us":56160,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56153}],"schema":2,"slots":[{"at_us":56160,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2befe396b8-622f-4b","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":58766,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6478,"slot":0,"token":"18e1fd2bf397b190-622f-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2bf397b190-622f-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6492,"bytes":57,"kind":"head","slot":0},{"at_us":6497,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":94,"abi_return_us":6132,"end_us":6138,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6187,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6181},{"abi_enter_us":6201,"abi_return_us":6208,"end_us":6208,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6201,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6243,"operation":"read","slot":0,"start_us":6240},{"abi_enter_us":6258,"abi_return_us":58645,"end_us":58649,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6258,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":58717,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58712},{"end_us":58751,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58744}],"schema":2,"slots":[{"at_us":58751,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2bf397b190-622f-4c","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":59570,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5280,"slot":0,"token":"18e1fd2bf75fd550-622f-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2bf75fd550-622f-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5296,"bytes":57,"kind":"head","slot":0},{"at_us":5301,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":67,"abi_return_us":5024,"end_us":5031,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5086,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5079},{"abi_enter_us":5104,"abi_return_us":5112,"end_us":5112,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5104,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5142,"operation":"read","slot":0,"start_us":5139},{"abi_enter_us":5157,"abi_return_us":59442,"end_us":59448,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5157,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":59514,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59509},{"end_us":59555,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59549}],"schema":2,"slots":[{"at_us":59555,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2bf75fd550-622f-4d","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":60541,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6101,"slot":0,"token":"18e1fd2bfb371c10-622f-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2bfb371c10-622f-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6110,"bytes":57,"kind":"head","slot":0},{"at_us":6116,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":95,"operation":"handoff","start_us":95},{"abi_enter_us":118,"abi_return_us":5817,"end_us":5822,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":118,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5861,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5856},{"abi_enter_us":5877,"abi_return_us":5884,"end_us":5884,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5877,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5912,"operation":"read","slot":0,"start_us":5910},{"abi_enter_us":5925,"abi_return_us":60416,"end_us":60423,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5925,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":60503,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":60498},{"end_us":60527,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60520}],"schema":2,"slots":[{"at_us":60527,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2bfb371c10-622f-4e","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":56352,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5579,"slot":0,"token":"18e1fd2bff01c458-622f-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2bff01c458-622f-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5595,"bytes":57,"kind":"head","slot":0},{"at_us":5601,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"abi_enter_us":94,"abi_return_us":5373,"end_us":5380,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5441,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5430},{"abi_enter_us":5455,"abi_return_us":5460,"end_us":5461,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5455,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5490,"operation":"read","slot":0,"start_us":5485},{"abi_enter_us":5511,"abi_return_us":56237,"end_us":56241,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5511,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56311,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56306},{"end_us":56336,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56328}],"schema":2,"slots":[{"at_us":56336,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2bff01c458-622f-4f","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":502136,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5466,"slot":0,"token":"18e1fd2c029124d8-622f-50"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2c029124d8-622f-50","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":83,"abi_return_us":499720,"end_us":499728,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"abi_enter_us":499779,"abi_return_us":502066,"end_us":502069,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499779,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502116,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502116}],"schema":2,"slots":[{"at_us":502116,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2c029124d8-622f-50","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":500916,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6040,"slot":0,"token":"18e1fd2c20a49f40-622f-51"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2c20a49f40-622f-51","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":78,"abi_return_us":500761,"end_us":500769,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500890,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500889}],"schema":2,"slots":[{"at_us":500890,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2c20a49f40-622f-51","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":502095,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6442,"slot":0,"token":"18e1fd2c3eb29f50-622f-52"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2c3eb29f50-622f-52","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":84,"abi_return_us":499826,"end_us":499834,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"abi_enter_us":499897,"abi_return_us":502016,"end_us":502018,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499897,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502069,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502069}],"schema":2,"slots":[{"at_us":502069,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2c3eb29f50-622f-52","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":500634,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6470,"slot":0,"token":"18e1fd2c5ccf02f8-622f-53"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2c5ccf02f8-622f-53","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"abi_enter_us":84,"abi_return_us":500517,"end_us":500541,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500616,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500616}],"schema":2,"slots":[{"at_us":500616,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2c5ccf02f8-622f-53","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":500469,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6189,"slot":0,"token":"18e1fd2c7ad79080-622f-54"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2c7ad79080-622f-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":6203,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":67,"abi_return_us":5924,"end_us":5928,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5988,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5978},{"abi_enter_us":6003,"abi_return_us":500323,"end_us":500343,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6003,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500442,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500429}],"schema":2,"slots":[{"at_us":500442,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2c7ad79080-622f-54","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":501389,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6132,"slot":0,"token":"18e1fd2c98de7c10-622f-55"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd2c98de7c10-622f-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":6147,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":67,"abi_return_us":5846,"end_us":5850,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5891,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5885},{"abi_enter_us":5904,"abi_return_us":501260,"end_us":501270,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5904,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501355,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501332}],"schema":2,"slots":[{"at_us":501355,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2c98de7c10-622f-55","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":501676,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5563,"slot":0,"token":"18e1fd2cb6e69080-622f-56"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd2cb6e69080-622f-56","watchdog":false,"workers_joined":1,"writes":[{"at_us":5580,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"body_reads":0,"deadline_us":500000,"end_us":87,"operation":"handoff","start_us":87},{"abi_enter_us":101,"abi_return_us":5310,"end_us":5315,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":100,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5365,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5352},{"abi_enter_us":5379,"abi_return_us":501581,"end_us":501588,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5379,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501656,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501649}],"schema":2,"slots":[{"at_us":501656,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2cb6e69080-622f-56","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":500254,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5456,"slot":0,"token":"18e1fd2cd5025400-622f-57"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd2cd5025400-622f-57","watchdog":false,"workers_joined":1,"writes":[{"at_us":5472,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":92,"operation":"handoff","start_us":92},{"abi_enter_us":116,"abi_return_us":5197,"end_us":5200,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":115,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5244,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5238},{"abi_enter_us":5257,"abi_return_us":500108,"end_us":500117,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5257,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500191,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500182}],"schema":2,"slots":[{"at_us":500191,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2cd5025400-622f-57","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":501863,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6039,"slot":0,"token":"18e1fd2cf2fd1648-622f-58"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2cf2fd1648-622f-58","watchdog":false,"workers_joined":1,"writes":[{"at_us":6053,"bytes":57,"kind":"head","slot":0},{"at_us":6066,"bytes":1,"kind":"body","slot":0},{"at_us":311102,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":89,"abi_return_us":5773,"end_us":5780,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5819,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5814},{"abi_enter_us":5833,"abi_return_us":5837,"end_us":5838,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5833,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5869,"operation":"read","slot":0,"start_us":5867},{"abi_enter_us":5881,"abi_return_us":310857,"end_us":310861,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5881,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":310947,"operation":"read","slot":0,"start_us":310943},{"abi_enter_us":310964,"abi_return_us":501780,"end_us":501783,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310964,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":501842,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501835}],"schema":2,"slots":[{"at_us":501842,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2cf2fd1648-622f-58","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":501500,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5808,"slot":0,"token":"18e1fd2d112024f8-622f-59"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2d112024f8-622f-59","watchdog":false,"workers_joined":1,"writes":[{"at_us":5822,"bytes":57,"kind":"head","slot":0},{"at_us":5829,"bytes":1,"kind":"body","slot":0},{"at_us":308400,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":69,"abi_return_us":5587,"end_us":5594,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5654,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5647},{"abi_enter_us":5668,"abi_return_us":5678,"end_us":5678,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5668,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5725,"operation":"read","slot":0,"start_us":5722},{"abi_enter_us":5741,"abi_return_us":308127,"end_us":308133,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5741,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":308202,"operation":"read","slot":0,"start_us":308198},{"abi_enter_us":308218,"abi_return_us":501410,"end_us":501414,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308218,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501477,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501469}],"schema":2,"slots":[{"at_us":501477,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2d112024f8-622f-59","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":501546,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5250,"slot":0,"token":"18e1fd2d2f417658-622f-5a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2d2f417658-622f-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5266,"bytes":57,"kind":"head","slot":0},{"at_us":5272,"bytes":1,"kind":"body","slot":0},{"at_us":308977,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":59,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":98,"operation":"handoff","start_us":98},{"abi_enter_us":119,"abi_return_us":4948,"end_us":4954,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":119,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5022,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5013},{"abi_enter_us":5040,"abi_return_us":5045,"end_us":5045,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5040,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5072,"operation":"read","slot":0,"start_us":5070},{"abi_enter_us":5086,"abi_return_us":308678,"end_us":308685,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5086,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":308854,"operation":"read","slot":0,"start_us":308847},{"abi_enter_us":308888,"abi_return_us":501450,"end_us":501454,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308888,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501523,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501515}],"schema":2,"slots":[{"at_us":501523,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2d2f417658-622f-5a","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":501073,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5509,"slot":0,"token":"18e1fd2d4d53f6c0-622f-5b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2d4d53f6c0-622f-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5529,"bytes":57,"kind":"head","slot":0},{"at_us":5536,"bytes":1,"kind":"body","slot":0},{"at_us":309302,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":95,"operation":"handoff","start_us":95},{"abi_enter_us":113,"abi_return_us":5256,"end_us":5262,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":113,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5313,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5305},{"abi_enter_us":5325,"abi_return_us":5331,"end_us":5332,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5325,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5368,"operation":"read","slot":0,"start_us":5365},{"abi_enter_us":5379,"abi_return_us":309039,"end_us":309047,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5379,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":309130,"operation":"read","slot":0,"start_us":309126},{"abi_enter_us":309145,"abi_return_us":500968,"end_us":500973,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309145,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501047,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501033}],"schema":2,"slots":[{"at_us":501047,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2d4d53f6c0-622f-5b","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":501513,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6524,"slot":1,"token":"18e1fd2d6b84ccc8-622f-5c"},{"path":"/readiness/siblings/0","received_us":6536,"slot":0,"token":"18e1fd2d6b84ccc8-622f-5c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd2d6b84ccc8-622f-5c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6543,"bytes":57,"kind":"head","slot":1},{"at_us":6551,"bytes":57,"kind":"head","slot":0},{"at_us":11756,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":185,"operation":"dispatch","outcome":"pending","slot":1,"start_us":100},{"abi_enter_us":219,"abi_return_us":6235,"end_us":6240,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":218,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6292,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6286},{"abi_enter_us":6306,"abi_return_us":6308,"end_us":6308,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6305,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6327,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6326},{"abi_enter_us":6337,"abi_return_us":11414,"end_us":11416,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6337,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":11478,"operation":"read","slot":1,"start_us":11474},{"abi_enter_us":11492,"abi_return_us":11495,"end_us":11496,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11491,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11528,"operation":"read","slot":1,"start_us":11526},{"abi_enter_us":11581,"abi_return_us":11584,"end_us":11585,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11581,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11626,"operation":"read","slot":1,"start_us":11625},{"abi_enter_us":11637,"abi_return_us":11640,"end_us":11641,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11637,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11666,"operation":"read","slot":1,"start_us":11666},{"end_us":11684,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11676},{"abi_enter_us":11701,"abi_return_us":500798,"end_us":500808,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11701,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501415,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501395}],"schema":2,"slots":[{"at_us":501415,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11684,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2d6b84ccc8-622f-5c","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":500964,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6754,"slot":0,"token":"18e1fd2d89b12e30-622f-5d"},{"path":"/readiness/siblings/1","received_us":6785,"slot":1,"token":"18e1fd2d89b12e30-622f-5d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd2d89b12e30-622f-5d","watchdog":false,"workers_joined":2,"writes":[{"at_us":6778,"bytes":57,"kind":"head","slot":0},{"at_us":6809,"bytes":57,"kind":"head","slot":1},{"at_us":6822,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":68,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"end_us":158,"operation":"dispatch","outcome":"pending","slot":1,"start_us":144},{"abi_enter_us":184,"abi_return_us":6328,"end_us":6334,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":183,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6399,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6392},{"abi_enter_us":6418,"abi_return_us":6421,"end_us":6422,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6418,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6443,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6442},{"abi_enter_us":6456,"abi_return_us":6463,"end_us":6463,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6456,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6506,"operation":"read","slot":1,"start_us":6503},{"abi_enter_us":6525,"abi_return_us":6528,"end_us":6529,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6525,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6562,"operation":"read","slot":1,"start_us":6562},{"end_us":6593,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6573},{"abi_enter_us":6640,"abi_return_us":500821,"end_us":500826,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6639,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500916,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500889}],"schema":2,"slots":[{"at_us":500916,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6593,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2d89b12e30-622f-5d","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":501108,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6610,"slot":0,"token":"18e1fd2da7e01bf0-622f-5e"},{"path":"/readiness/siblings/1","received_us":6622,"slot":1,"token":"18e1fd2da7e01bf0-622f-5e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd2da7e01bf0-622f-5e","watchdog":false,"workers_joined":2,"writes":[{"at_us":6625,"bytes":57,"kind":"head","slot":0},{"at_us":6632,"bytes":57,"kind":"head","slot":1},{"at_us":6638,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":113,"operation":"dispatch","outcome":"pending","slot":1,"start_us":91},{"body_reads":0,"deadline_us":500000,"end_us":147,"operation":"handoff","start_us":147},{"abi_enter_us":171,"abi_return_us":6297,"end_us":6302,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":171,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6342,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6336},{"abi_enter_us":6359,"abi_return_us":6361,"end_us":6361,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6359,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6378,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6377},{"abi_enter_us":6388,"abi_return_us":6391,"end_us":6391,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6388,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6416,"operation":"read","slot":1,"start_us":6413},{"abi_enter_us":6431,"abi_return_us":6432,"end_us":6432,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6431,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6454,"operation":"read","slot":1,"start_us":6453},{"abi_enter_us":6464,"abi_return_us":6465,"end_us":6466,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6464,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6486,"operation":"read","slot":1,"start_us":6486},{"abi_enter_us":6500,"abi_return_us":6503,"end_us":6503,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6500,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6530,"operation":"read","slot":1,"start_us":6530},{"end_us":6542,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6539},{"abi_enter_us":6555,"abi_return_us":500995,"end_us":501008,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6555,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501089,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501080}],"schema":2,"slots":[{"at_us":501089,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6542,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2da7e01bf0-622f-5e","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":501606,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6597,"slot":1,"token":"18e1fd2dc60e65a0-622f-5f"},{"path":"/readiness/siblings/0","received_us":6614,"slot":0,"token":"18e1fd2dc60e65a0-622f-5f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd2dc60e65a0-622f-5f","watchdog":false,"workers_joined":2,"writes":[{"at_us":6617,"bytes":57,"kind":"head","slot":1},{"at_us":6625,"bytes":57,"kind":"head","slot":0},{"at_us":11802,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":122,"operation":"dispatch","outcome":"pending","slot":1,"start_us":98},{"body_reads":0,"deadline_us":500000,"end_us":238,"operation":"handoff","start_us":238},{"abi_enter_us":255,"abi_return_us":6314,"end_us":6318,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":255,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6363,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6357},{"abi_enter_us":6382,"abi_return_us":6384,"end_us":6384,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6382,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6403,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6402},{"abi_enter_us":6418,"abi_return_us":11485,"end_us":11487,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6418,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":11552,"operation":"read","slot":1,"start_us":11548},{"abi_enter_us":11567,"abi_return_us":11571,"end_us":11571,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11567,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11604,"operation":"read","slot":1,"start_us":11604},{"end_us":11621,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11617},{"abi_enter_us":11637,"abi_return_us":501508,"end_us":501512,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11637,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501586,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501577}],"schema":2,"slots":[{"at_us":501586,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11621,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2dc60e65a0-622f-5f","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":501513,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6209,"slot":0,"token":"18e1fd2de44755e0-622f-60"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd2de44755e0-622f-60","watchdog":false,"workers_joined":1,"writes":[{"at_us":6223,"bytes":57,"kind":"head","slot":0},{"at_us":6229,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":78,"abi_return_us":5951,"end_us":5955,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6002,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5997},{"abi_enter_us":6016,"abi_return_us":6021,"end_us":6021,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6016,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6057,"operation":"read","slot":0,"start_us":6054},{"abi_enter_us":6071,"abi_return_us":499827,"end_us":499831,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6071,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501471,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501462}],"schema":2,"slots":[{"at_us":501471,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2de44755e0-622f-60","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":501012,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5366,"slot":0,"token":"18e1fd2e02584fa8-622f-61"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd2e02584fa8-622f-61","watchdog":false,"workers_joined":1,"writes":[{"at_us":5382,"bytes":57,"kind":"head","slot":0},{"at_us":5387,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":65,"abi_return_us":5115,"end_us":5120,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5170,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5164},{"abi_enter_us":5191,"abi_return_us":5199,"end_us":5199,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5191,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5242,"operation":"read","slot":0,"start_us":5239},{"abi_enter_us":5257,"abi_return_us":500832,"end_us":500838,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5257,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500935,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500922}],"schema":2,"slots":[{"at_us":500935,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e02584fa8-622f-61","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":501356,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5828,"slot":0,"token":"18e1fd2e20729c28-622f-62"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd2e20729c28-622f-62","watchdog":false,"workers_joined":1,"writes":[{"at_us":5846,"bytes":57,"kind":"head","slot":0},{"at_us":5854,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":90,"operation":"handoff","start_us":90},{"abi_enter_us":112,"abi_return_us":5599,"end_us":5606,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5664,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5657},{"abi_enter_us":5681,"abi_return_us":5689,"end_us":5690,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5681,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5727,"operation":"read","slot":0,"start_us":5724},{"abi_enter_us":5741,"abi_return_us":501194,"end_us":501209,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5741,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501308,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501294}],"schema":2,"slots":[{"at_us":501308,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e20729c28-622f-62","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":503259,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6482,"slot":0,"token":"18e1fd2e3e98cba0-622f-63"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2e3e98cba0-622f-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":6501,"bytes":57,"kind":"head","slot":0},{"at_us":6508,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":65,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":147,"operation":"handoff","start_us":147},{"abi_enter_us":187,"abi_return_us":6223,"end_us":6231,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":174,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6320,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6309},{"abi_enter_us":6352,"abi_return_us":6363,"end_us":6364,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6351,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6427,"operation":"read","slot":0,"start_us":6423},{"abi_enter_us":6452,"abi_return_us":502716,"end_us":502723,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6452,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":503188,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":503174}],"schema":2,"slots":[{"at_us":503188,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e3e98cba0-622f-63","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":6650,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6503,"slot":0,"token":"18e1fd2e5cda7e10-622f-64"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e5cda7e10-622f-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":6521,"bytes":57,"kind":"head","slot":0},{"at_us":6530,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"abi_enter_us":105,"abi_return_us":6256,"end_us":6269,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":105,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6334,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6328},{"abi_enter_us":6354,"abi_return_us":6362,"end_us":6363,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6354,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6395,"operation":"read","slot":0,"start_us":6392},{"abi_enter_us":6411,"abi_return_us":6412,"end_us":6412,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6411,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6438,"operation":"read","slot":0,"start_us":6437},{"abi_enter_us":6449,"abi_return_us":6450,"end_us":6450,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6449,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6531,"operation":"read","slot":0,"start_us":6527},{"abi_enter_us":6561,"abi_return_us":6566,"end_us":6567,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6560,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6598,"operation":"read","slot":0,"start_us":6598},{"end_us":6617,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6613}],"schema":2,"slots":[{"at_us":6617,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e5cda7e10-622f-64","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":5299,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5374,"slot":0,"token":"18e1fd2e5d8cbaf8-622f-65"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e5d8cbaf8-622f-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":5404,"bytes":57,"kind":"head","slot":0},{"at_us":5422,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":114,"abi_return_us":5087,"end_us":5094,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":114,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5154,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5147},{"abi_enter_us":5168,"abi_return_us":5178,"end_us":5179,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5168,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5218,"operation":"read","slot":0,"start_us":5215},{"abi_enter_us":5232,"abi_return_us":5234,"end_us":5234,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5232,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5266,"operation":"read","slot":0,"start_us":5265},{"end_us":5282,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5278}],"schema":2,"slots":[{"at_us":5282,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e5d8cbaf8-622f-65","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":6557,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6506,"slot":0,"token":"18e1fd2e5e1839e8-622f-66"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e5e1839e8-622f-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":6524,"bytes":57,"kind":"head","slot":0},{"at_us":6530,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":97,"operation":"handoff","start_us":97},{"abi_enter_us":128,"abi_return_us":6285,"end_us":6292,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":127,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6354,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6347},{"abi_enter_us":6368,"abi_return_us":6377,"end_us":6378,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6368,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6425,"operation":"read","slot":0,"start_us":6423},{"abi_enter_us":6437,"abi_return_us":6439,"end_us":6439,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6437,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6459,"operation":"read","slot":0,"start_us":6458},{"abi_enter_us":6471,"abi_return_us":6472,"end_us":6472,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6471,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6492,"operation":"read","slot":0,"start_us":6492},{"abi_enter_us":6502,"abi_return_us":6505,"end_us":6505,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6502,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6524,"operation":"read","slot":0,"start_us":6524},{"end_us":6541,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6533}],"schema":2,"slots":[{"at_us":6541,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e5e1839e8-622f-66","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":5538,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5556,"slot":0,"token":"18e1fd2e5ec32f88-622f-67"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e5ec32f88-622f-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":5572,"bytes":57,"kind":"head","slot":0},{"at_us":5579,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":164,"operation":"handoff","start_us":164},{"abi_enter_us":236,"abi_return_us":5207,"end_us":5215,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":236,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5264,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5257},{"abi_enter_us":5374,"abi_return_us":5400,"end_us":5402,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5373,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5459,"operation":"read","slot":0,"start_us":5456},{"abi_enter_us":5473,"abi_return_us":5477,"end_us":5477,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5473,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5502,"operation":"read","slot":0,"start_us":5502},{"end_us":5523,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5518}],"schema":2,"slots":[{"at_us":5523,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e5ec32f88-622f-67","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5263,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5508,"slot":0,"token":"18e1fd2e5f68e950-622f-68"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e5f68e950-622f-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":5527,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":156,"operation":"dispatch","outcome":"pending","slot":0,"start_us":34},{"abi_enter_us":199,"abi_return_us":5130,"end_us":5136,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":198,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5184,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5177},{"abi_enter_us":5201,"abi_return_us":5205,"end_us":5205,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5201,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5230,"operation":"read","slot":0,"start_us":5228},{"end_us":5249,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5243}],"schema":2,"slots":[{"at_us":5249,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e5f68e950-622f-68","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5187,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5627,"slot":0,"token":"18e1fd2e5ffcccb0-622f-69"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e5ffcccb0-622f-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":5695,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":190,"abi_return_us":5036,"end_us":5044,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":190,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5104,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5093},{"abi_enter_us":5122,"abi_return_us":5127,"end_us":5127,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5122,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5156,"operation":"read","slot":0,"start_us":5154},{"end_us":5174,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5168}],"schema":2,"slots":[{"at_us":5174,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e5ffcccb0-622f-69","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5426,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5750,"slot":0,"token":"18e1fd2e608c9548-622f-6a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e608c9548-622f-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5770,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"abi_enter_us":98,"abi_return_us":5310,"end_us":5316,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5358,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5352},{"abi_enter_us":5371,"abi_return_us":5374,"end_us":5375,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5370,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5398,"operation":"read","slot":0,"start_us":5396},{"end_us":5413,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5409}],"schema":2,"slots":[{"at_us":5413,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e608c9548-622f-6a","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5308,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5474,"slot":0,"token":"18e1fd2e610e4868-622f-6b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e610e4868-622f-6b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5495,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":31},{"body_reads":0,"deadline_us":500000,"end_us":87,"operation":"handoff","start_us":87},{"abi_enter_us":99,"abi_return_us":5149,"end_us":5156,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":99,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5224,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5216},{"abi_enter_us":5240,"abi_return_us":5243,"end_us":5244,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5240,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5274,"operation":"read","slot":0,"start_us":5272},{"end_us":5294,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5290}],"schema":2,"slots":[{"at_us":5294,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e610e4868-622f-6b","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":7710,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5745,"slot":0,"token":"18e1fd2e618f7e88-622f-6c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e618f7e88-622f-6c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5763,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"abi_enter_us":81,"abi_return_us":7612,"end_us":7621,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7678,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":7671},{"end_us":7694,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":7693}],"schema":2,"slots":[{"at_us":7694,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2e618f7e88-622f-6c","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5907,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6062,"slot":0,"token":"18e1fd2e62340f70-622f-6d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e62340f70-622f-6d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6082,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":151,"operation":"dispatch","outcome":"pending","slot":0,"start_us":58},{"abi_enter_us":189,"abi_return_us":5780,"end_us":5788,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":188,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5860,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5837},{"end_us":5891,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5890}],"schema":2,"slots":[{"at_us":5891,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2e62340f70-622f-6d","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6218,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6499,"slot":0,"token":"18e1fd2e62bf76f0-622f-6e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e62bf76f0-622f-6e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6517,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":156,"operation":"handoff","start_us":156},{"abi_enter_us":215,"abi_return_us":6139,"end_us":6145,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":215,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6187,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6178},{"end_us":6202,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6202}],"schema":2,"slots":[{"at_us":6202,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2e62bf76f0-622f-6e","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5536,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5569,"slot":0,"token":"18e1fd2e635104a8-622f-6f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e635104a8-622f-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5584,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":88,"operation":"handoff","start_us":88},{"abi_enter_us":106,"abi_return_us":5434,"end_us":5460,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5503,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5496},{"end_us":5520,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5520}],"schema":2,"slots":[{"at_us":5520,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2e635104a8-622f-6f","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56018,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5728,"slot":0,"token":"18e1fd2e63e31b18-622f-70"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e63e31b18-622f-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":5743,"bytes":66,"kind":"head","slot":0},{"at_us":5749,"bytes":6,"kind":"body","slot":0},{"at_us":57320,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"abi_enter_us":107,"abi_return_us":4247,"end_us":4252,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4312,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4303},{"abi_enter_us":4325,"abi_return_us":4333,"end_us":4334,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4325,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4366,"operation":"read","slot":0,"start_us":4363},{"abi_enter_us":4379,"abi_return_us":55897,"end_us":55902,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4379,"status":"FastlyStatus::OK","timeout_ms":495},{"capacity":1,"end_us":55969,"error":"Other","operation":"read","slot":0,"start_us":55956},{"end_us":56003,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55993}],"schema":2,"slots":[{"at_us":56003,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e63e31b18-622f-70","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57629,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5648,"slot":0,"token":"18e1fd2e678bf320-622f-71"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e678bf320-622f-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":5674,"bytes":66,"kind":"head","slot":0},{"at_us":5681,"bytes":6,"kind":"body","slot":0},{"at_us":57991,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":119,"abi_return_us":5188,"end_us":5204,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":119,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5276,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5269},{"abi_enter_us":5291,"abi_return_us":5300,"end_us":5300,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5291,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5331,"operation":"read","slot":0,"start_us":5328},{"abi_enter_us":5349,"abi_return_us":57495,"end_us":57504,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5349,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57586,"error":"Other","operation":"read","slot":0,"start_us":57580},{"end_us":57613,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57605}],"schema":2,"slots":[{"at_us":57613,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e678bf320-622f-71","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56762,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6479,"slot":0,"token":"18e1fd2e6b4044a8-622f-72"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e6b4044a8-622f-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":6498,"bytes":66,"kind":"head","slot":0},{"at_us":6504,"bytes":6,"kind":"body","slot":0},{"at_us":57058,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":100,"operation":"handoff","start_us":100},{"abi_enter_us":130,"abi_return_us":6101,"end_us":6109,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":130,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6151,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6144},{"abi_enter_us":6164,"abi_return_us":6172,"end_us":6173,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6164,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6206,"operation":"read","slot":0,"start_us":6204},{"abi_enter_us":6218,"abi_return_us":56649,"end_us":56654,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6218,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":56719,"error":"Other","operation":"read","slot":0,"start_us":56713},{"end_us":56742,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56735}],"schema":2,"slots":[{"at_us":56742,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e6b4044a8-622f-72","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57890,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5344,"slot":0,"token":"18e1fd2e6ee51570-622f-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e6ee51570-622f-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":5362,"bytes":66,"kind":"head","slot":0},{"at_us":5368,"bytes":6,"kind":"body","slot":0},{"at_us":58067,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":91,"operation":"handoff","start_us":91},{"abi_enter_us":109,"abi_return_us":5062,"end_us":5069,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":109,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5123,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5117},{"abi_enter_us":5140,"abi_return_us":5146,"end_us":5147,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5140,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5202,"operation":"read","slot":0,"start_us":5198},{"abi_enter_us":5230,"abi_return_us":57760,"end_us":57768,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5230,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57846,"error":"Other","operation":"read","slot":0,"start_us":57839},{"end_us":57874,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57865}],"schema":2,"slots":[{"at_us":57874,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e6ee51570-622f-73","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":5578,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5773,"slot":0,"token":"18e1fd2e729c9f30-622f-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e729c9f30-622f-74","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"abi_enter_us":104,"abi_return_us":5500,"end_us":5506,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5545,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5540},{"end_us":5562,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5562}],"schema":2,"slots":[{"at_us":5562,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2e729c9f30-622f-74","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":6285,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6467,"slot":0,"token":"18e1fd2e731fadf8-622f-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e731fadf8-622f-75","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":29},{"abi_enter_us":93,"abi_return_us":6196,"end_us":6206,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6248,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6243},{"end_us":6269,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6269}],"schema":2,"slots":[{"at_us":6269,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2e731fadf8-622f-75","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":6303,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6512,"slot":0,"token":"18e1fd2e73b1d7f0-622f-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e73b1d7f0-622f-76","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":85,"operation":"handoff","start_us":85},{"abi_enter_us":98,"abi_return_us":6216,"end_us":6225,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6266,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6260},{"end_us":6282,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6282}],"schema":2,"slots":[{"at_us":6282,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2e73b1d7f0-622f-76","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":6257,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6327,"slot":0,"token":"18e1fd2e74459ff8-622f-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e74459ff8-622f-77","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":91,"operation":"handoff","start_us":91},{"abi_enter_us":108,"abi_return_us":6116,"end_us":6122,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":108,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6164,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6158},{"end_us":6209,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6209}],"schema":2,"slots":[{"at_us":6209,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2e74459ff8-622f-77","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":56333,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6083,"slot":0,"token":"18e1fd2e74d2c8b0-622f-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e74d2c8b0-622f-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":6121,"bytes":57,"kind":"head","slot":0},{"at_us":6148,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"abi_enter_us":89,"abi_return_us":5889,"end_us":5895,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5949,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5938},{"bytes":1,"capacity":1,"end_us":5981,"operation":"read","slot":0,"start_us":5975},{"capacity":1,"end_us":56216,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":6003},{"end_us":56284,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56277}],"schema":2,"slots":[{"at_us":56284,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e74d2c8b0-622f-78","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":58710,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6491,"slot":0,"token":"18e1fd2e78658c60-622f-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e78658c60-622f-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":6511,"bytes":57,"kind":"head","slot":0},{"at_us":6517,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"abi_enter_us":87,"abi_return_us":6186,"end_us":6192,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6247,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6239},{"abi_enter_us":6262,"abi_return_us":6270,"end_us":6271,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6262,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6302,"operation":"read","slot":0,"start_us":6299},{"abi_enter_us":6323,"abi_return_us":58585,"end_us":58592,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6323,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":58664,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58659},{"end_us":58695,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58688}],"schema":2,"slots":[{"at_us":58695,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e78658c60-622f-79","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":57333,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5904,"slot":0,"token":"18e1fd2e7c4bcb28-622f-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e7c4bcb28-622f-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5919,"bytes":57,"kind":"head","slot":0},{"at_us":5927,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":110,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":165,"abi_return_us":2054,"end_us":2063,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":165,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":2145,"operation":"head-poll","outcome":"ready","slot":0,"start_us":2137},{"abi_enter_us":2178,"abi_return_us":2188,"end_us":2189,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":2178,"status":"FastlyStatus::OK","timeout_ms":497},{"bytes":1,"capacity":16384,"end_us":2258,"operation":"read","slot":0,"start_us":2253},{"abi_enter_us":2322,"abi_return_us":57139,"end_us":57144,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":2321,"status":"FastlyStatus::OK","timeout_ms":497},{"capacity":16384,"end_us":57263,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57256},{"end_us":57304,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57294}],"schema":2,"slots":[{"at_us":57304,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e7c4bcb28-622f-7a","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":61225,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6477,"slot":0,"token":"18e1fd2e803d3d20-622f-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e803d3d20-622f-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6501,"bytes":57,"kind":"head","slot":0},{"at_us":6507,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":80,"operation":"handoff","start_us":80},{"abi_enter_us":130,"abi_return_us":6273,"end_us":6281,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":130,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6331,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6324},{"abi_enter_us":6352,"abi_return_us":6360,"end_us":6361,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6351,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6390,"operation":"read","slot":0,"start_us":6387},{"abi_enter_us":6403,"abi_return_us":61092,"end_us":61100,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6403,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":61177,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":61172},{"end_us":61203,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61196}],"schema":2,"slots":[{"at_us":61203,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e803d3d20-622f-7b","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":61358,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6480,"slot":0,"token":"18e1fd2e841585b0-622f-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd2e841585b0-622f-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6499,"bytes":57,"kind":"head","slot":0},{"at_us":6504,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":164,"operation":"handoff","start_us":164},{"abi_enter_us":180,"abi_return_us":6148,"end_us":6155,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":180,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6220,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6214},{"abi_enter_us":6235,"abi_return_us":6242,"end_us":6243,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6234,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6275,"operation":"read","slot":0,"start_us":6272},{"abi_enter_us":6287,"abi_return_us":61223,"end_us":61230,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6286,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":61309,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":61302},{"end_us":61339,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61331}],"schema":2,"slots":[{"at_us":61339,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2e841585b0-622f-7c","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":502088,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6933,"slot":0,"token":"18e1fd2e87ed8fc0-622f-7d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2e87ed8fc0-622f-7d","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":110,"abi_return_us":501632,"end_us":501693,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502062,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502062}],"schema":2,"slots":[{"at_us":502062,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2e87ed8fc0-622f-7d","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":501748,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5865,"slot":0,"token":"18e1fd2ea60bb0b8-622f-7e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2ea60bb0b8-622f-7e","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":163,"operation":"dispatch","outcome":"pending","slot":0,"start_us":61},{"abi_enter_us":231,"abi_return_us":499575,"end_us":499586,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":230,"status":"FastlyStatus::OK","timeout_ms":499},{"abi_enter_us":499644,"abi_return_us":501699,"end_us":501700,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499644,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501726,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501726}],"schema":2,"slots":[{"at_us":501726,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2ea60bb0b8-622f-7e","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":500926,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6257,"slot":0,"token":"18e1fd2ec4236cf8-622f-7f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2ec4236cf8-622f-7f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"abi_enter_us":89,"abi_return_us":500720,"end_us":500734,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500905,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500905}],"schema":2,"slots":[{"at_us":500905,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2ec4236cf8-622f-7f","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":501203,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6422,"slot":0,"token":"18e1fd2ee21b5c98-622f-80"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2ee21b5c98-622f-80","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":76,"abi_return_us":501107,"end_us":501117,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501179,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501179}],"schema":2,"slots":[{"at_us":501179,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd2ee21b5c98-622f-80","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":501169,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5798,"slot":0,"token":"18e1fd2f001e7b80-622f-81"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd2f001e7b80-622f-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":5825,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":75,"abi_return_us":5623,"end_us":5630,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5683,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5675},{"abi_enter_us":5698,"abi_return_us":501046,"end_us":501059,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5698,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501141,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501127}],"schema":2,"slots":[{"at_us":501141,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2f001e7b80-622f-81","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":501206,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5544,"slot":0,"token":"18e1fd2f1e1e79a0-622f-82"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd2f1e1e79a0-622f-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":5560,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":76,"abi_return_us":5295,"end_us":5299,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5350,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5343},{"abi_enter_us":5365,"abi_return_us":501068,"end_us":501081,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5364,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501177,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501160}],"schema":2,"slots":[{"at_us":501177,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2f1e1e79a0-622f-82","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":500681,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5279,"slot":0,"token":"18e1fd2f3c256918-622f-83"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd2f3c256918-622f-83","watchdog":false,"workers_joined":1,"writes":[{"at_us":5300,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":105,"operation":"handoff","start_us":105},{"abi_enter_us":129,"abi_return_us":4987,"end_us":4995,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":128,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5055,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5044},{"abi_enter_us":5095,"abi_return_us":500564,"end_us":500571,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5094,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500641,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500633}],"schema":2,"slots":[{"at_us":500641,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2f3c256918-622f-83","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":501207,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5319,"slot":0,"token":"18e1fd2f5a227550-622f-84"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2f5a227550-622f-84","watchdog":false,"workers_joined":1,"writes":[{"at_us":5337,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":80,"operation":"handoff","start_us":80},{"abi_enter_us":101,"abi_return_us":5130,"end_us":5137,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":101,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5222,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5206},{"abi_enter_us":5255,"abi_return_us":501101,"end_us":501111,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5255,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501185,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501172}],"schema":2,"slots":[{"at_us":501185,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2f5a227550-622f-84","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":502575,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5189,"slot":0,"token":"18e1fd2f782482c8-622f-85"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2f782482c8-622f-85","watchdog":false,"workers_joined":1,"writes":[{"at_us":5207,"bytes":57,"kind":"head","slot":0},{"at_us":5214,"bytes":1,"kind":"body","slot":0},{"at_us":308099,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":81,"abi_return_us":4937,"end_us":4943,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4995,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4988},{"abi_enter_us":5013,"abi_return_us":5019,"end_us":5020,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5013,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5045,"operation":"read","slot":0,"start_us":5042},{"abi_enter_us":5056,"abi_return_us":307814,"end_us":307818,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5056,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307894,"operation":"read","slot":0,"start_us":307891},{"abi_enter_us":307908,"abi_return_us":502480,"end_us":502486,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307908,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":502553,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502545}],"schema":2,"slots":[{"at_us":502553,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2f782482c8-622f-85","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":500493,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5858,"slot":0,"token":"18e1fd2f9645b8d0-622f-86"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2f9645b8d0-622f-86","watchdog":false,"workers_joined":1,"writes":[{"at_us":5868,"bytes":57,"kind":"head","slot":0},{"at_us":5875,"bytes":1,"kind":"body","slot":0},{"at_us":308463,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":99,"abi_return_us":5562,"end_us":5567,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5615,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5608},{"abi_enter_us":5630,"abi_return_us":5639,"end_us":5639,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5630,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5669,"operation":"read","slot":0,"start_us":5666},{"abi_enter_us":5687,"abi_return_us":308159,"end_us":308165,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5687,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":308259,"operation":"read","slot":0,"start_us":308255},{"abi_enter_us":308276,"abi_return_us":500395,"end_us":500400,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308276,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500470,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500462}],"schema":2,"slots":[{"at_us":500470,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2f9645b8d0-622f-86","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":501747,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6085,"slot":0,"token":"18e1fd2fb4477440-622f-87"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2fb4477440-622f-87","watchdog":false,"workers_joined":1,"writes":[{"at_us":6100,"bytes":57,"kind":"head","slot":0},{"at_us":6105,"bytes":1,"kind":"body","slot":0},{"at_us":309771,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":80,"operation":"handoff","start_us":80},{"abi_enter_us":99,"abi_return_us":5797,"end_us":5802,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5855,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5849},{"abi_enter_us":5872,"abi_return_us":5877,"end_us":5878,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5872,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5904,"operation":"read","slot":0,"start_us":5901},{"abi_enter_us":5916,"abi_return_us":309499,"end_us":309512,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5916,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309624,"operation":"read","slot":0,"start_us":309619},{"abi_enter_us":309639,"abi_return_us":501618,"end_us":501621,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309639,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501700,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501682}],"schema":2,"slots":[{"at_us":501700,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2fb4477440-622f-87","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":501087,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5376,"slot":0,"token":"18e1fd2fd26adcc8-622f-88"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd2fd26adcc8-622f-88","watchdog":false,"workers_joined":1,"writes":[{"at_us":5389,"bytes":57,"kind":"head","slot":0},{"at_us":5398,"bytes":1,"kind":"body","slot":0},{"at_us":310437,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":85,"operation":"handoff","start_us":85},{"abi_enter_us":106,"abi_return_us":5081,"end_us":5089,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5145,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5138},{"abi_enter_us":5161,"abi_return_us":5176,"end_us":5176,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5161,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5208,"operation":"read","slot":0,"start_us":5205},{"abi_enter_us":5221,"abi_return_us":310128,"end_us":310136,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5221,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":310226,"operation":"read","slot":0,"start_us":310222},{"abi_enter_us":310242,"abi_return_us":500982,"end_us":500985,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310242,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":501064,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501054}],"schema":2,"slots":[{"at_us":501064,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2fd26adcc8-622f-88","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":501886,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5693,"slot":0,"token":"18e1fd2ff074e538-622f-89"},{"path":"/readiness/siblings/1","received_us":5703,"slot":1,"token":"18e1fd2ff074e538-622f-89"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd2ff074e538-622f-89","watchdog":false,"workers_joined":2,"writes":[{"at_us":5708,"bytes":57,"kind":"head","slot":0},{"at_us":5716,"bytes":57,"kind":"head","slot":1},{"at_us":5722,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":108,"operation":"dispatch","outcome":"pending","slot":1,"start_us":74},{"abi_enter_us":127,"abi_return_us":5437,"end_us":5440,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":127,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5482,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5476},{"abi_enter_us":5497,"abi_return_us":5499,"end_us":5499,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5497,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5514,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5513},{"abi_enter_us":5523,"abi_return_us":5527,"end_us":5527,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5523,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5553,"operation":"read","slot":1,"start_us":5550},{"abi_enter_us":5567,"abi_return_us":5568,"end_us":5568,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5567,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5588,"operation":"read","slot":1,"start_us":5587},{"abi_enter_us":5600,"abi_return_us":5601,"end_us":5601,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5599,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5621,"operation":"read","slot":1,"start_us":5621},{"abi_enter_us":5633,"abi_return_us":5635,"end_us":5635,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5632,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5656,"operation":"read","slot":1,"start_us":5655},{"end_us":5669,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5666},{"abi_enter_us":5681,"abi_return_us":501763,"end_us":501779,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5681,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501865,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501857}],"schema":2,"slots":[{"at_us":501865,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5669,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd2ff074e538-622f-89","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":500555,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5863,"slot":1,"token":"18e1fd300e99b908-622f-8a"},{"path":"/readiness/siblings/0","received_us":5881,"slot":0,"token":"18e1fd300e99b908-622f-8a"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd300e99b908-622f-8a","watchdog":false,"workers_joined":2,"writes":[{"at_us":5905,"bytes":57,"kind":"head","slot":1},{"at_us":5920,"bytes":57,"kind":"head","slot":0},{"at_us":11276,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":103,"operation":"dispatch","outcome":"pending","slot":1,"start_us":83},{"abi_enter_us":144,"abi_return_us":5603,"end_us":5609,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":144,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5714,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5705},{"abi_enter_us":5740,"abi_return_us":5744,"end_us":5745,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5739,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5762,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5761},{"abi_enter_us":5772,"abi_return_us":10983,"end_us":10986,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5772,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11052,"operation":"read","slot":1,"start_us":11049},{"abi_enter_us":11067,"abi_return_us":11071,"end_us":11071,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11067,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11095,"operation":"read","slot":1,"start_us":11094},{"end_us":11116,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11111},{"abi_enter_us":11136,"abi_return_us":500411,"end_us":500429,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11136,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500524,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500504}],"schema":2,"slots":[{"at_us":500524,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11116,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd300e99b908-622f-8a","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":502666,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5655,"slot":1,"token":"18e1fd302ca22368-622f-8b"},{"path":"/readiness/siblings/0","received_us":5665,"slot":0,"token":"18e1fd302ca22368-622f-8b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd302ca22368-622f-8b","watchdog":false,"workers_joined":2,"writes":[{"at_us":5675,"bytes":57,"kind":"head","slot":1},{"at_us":5688,"bytes":57,"kind":"head","slot":0},{"at_us":10823,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":116,"operation":"dispatch","outcome":"pending","slot":1,"start_us":94},{"body_reads":0,"deadline_us":500000,"end_us":135,"operation":"handoff","start_us":135},{"abi_enter_us":154,"abi_return_us":5408,"end_us":5415,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":148,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5479,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5470},{"abi_enter_us":5495,"abi_return_us":5498,"end_us":5498,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5495,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5512,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5511},{"abi_enter_us":5524,"abi_return_us":10536,"end_us":10540,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5524,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":10596,"operation":"read","slot":1,"start_us":10592},{"abi_enter_us":10610,"abi_return_us":10614,"end_us":10615,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10610,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10639,"operation":"read","slot":1,"start_us":10638},{"abi_enter_us":10650,"abi_return_us":10651,"end_us":10652,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10650,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10671,"operation":"read","slot":1,"start_us":10670},{"abi_enter_us":10680,"abi_return_us":10682,"end_us":10682,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10680,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":1,"end_us":10707,"operation":"read","slot":1,"start_us":10707},{"end_us":10720,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10716},{"abi_enter_us":10735,"abi_return_us":502535,"end_us":502551,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":10735,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":502645,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502631}],"schema":2,"slots":[{"at_us":502645,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":10720,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd302ca22368-622f-8b","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":501274,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5315,"slot":1,"token":"18e1fd304ad47840-622f-8c"},{"path":"/readiness/siblings/0","received_us":5338,"slot":0,"token":"18e1fd304ad47840-622f-8c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd304ad47840-622f-8c","watchdog":false,"workers_joined":2,"writes":[{"at_us":5350,"bytes":57,"kind":"head","slot":1},{"at_us":5367,"bytes":57,"kind":"head","slot":0},{"at_us":11630,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":456,"operation":"dispatch","outcome":"pending","slot":1,"start_us":71},{"body_reads":0,"deadline_us":500000,"end_us":477,"operation":"handoff","start_us":477},{"abi_enter_us":495,"abi_return_us":5041,"end_us":5048,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":495,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5123,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5116},{"abi_enter_us":5157,"abi_return_us":5164,"end_us":5166,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5157,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5183,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5182},{"abi_enter_us":5195,"abi_return_us":11283,"end_us":11285,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5195,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11337,"operation":"read","slot":1,"start_us":11334},{"abi_enter_us":11351,"abi_return_us":11355,"end_us":11355,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11351,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11381,"operation":"read","slot":1,"start_us":11380},{"end_us":11394,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11390},{"abi_enter_us":11411,"abi_return_us":501162,"end_us":501169,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11411,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501255,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501244}],"schema":2,"slots":[{"at_us":501255,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11394,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd304ad47840-622f-8c","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":500325,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5459,"slot":0,"token":"18e1fd3068ed2a98-622f-8d"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd3068ed2a98-622f-8d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5476,"bytes":57,"kind":"head","slot":0},{"at_us":5481,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":128,"operation":"dispatch","outcome":"pending","slot":0,"start_us":44},{"abi_enter_us":159,"abi_return_us":5160,"end_us":5166,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":159,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5211,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5205},{"abi_enter_us":5226,"abi_return_us":5230,"end_us":5230,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5225,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5255,"operation":"read","slot":0,"start_us":5252},{"abi_enter_us":5270,"abi_return_us":500236,"end_us":500242,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5270,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500302,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500294}],"schema":2,"slots":[{"at_us":500302,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3068ed2a98-622f-8d","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":500932,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5241,"slot":0,"token":"18e1fd3086e656a0-622f-8e"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd3086e656a0-622f-8e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5255,"bytes":57,"kind":"head","slot":0},{"at_us":5261,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":80,"abi_return_us":4910,"end_us":4914,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4956,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4950},{"abi_enter_us":4971,"abi_return_us":4976,"end_us":4976,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4970,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5011,"operation":"read","slot":0,"start_us":5008},{"abi_enter_us":5023,"abi_return_us":500841,"end_us":500846,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5023,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500910,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500902}],"schema":2,"slots":[{"at_us":500910,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3086e656a0-622f-8e","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":501584,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5681,"slot":0,"token":"18e1fd30a4e9ac38-622f-8f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd30a4e9ac38-622f-8f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5700,"bytes":57,"kind":"head","slot":0},{"at_us":5705,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":65,"operation":"handoff","start_us":65},{"abi_enter_us":77,"abi_return_us":5452,"end_us":5458,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5500,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5495},{"abi_enter_us":5513,"abi_return_us":5521,"end_us":5521,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5513,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5546,"operation":"read","slot":0,"start_us":5544},{"abi_enter_us":5557,"abi_return_us":501489,"end_us":501496,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5557,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501547,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501536}],"schema":2,"slots":[{"at_us":501547,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30a4e9ac38-622f-8f","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":500914,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5389,"slot":0,"token":"18e1fd30c2f742d0-622f-90"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd30c2f742d0-622f-90","watchdog":false,"workers_joined":1,"writes":[{"at_us":5405,"bytes":57,"kind":"head","slot":0},{"at_us":5410,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":80,"abi_return_us":5114,"end_us":5118,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5150,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5145},{"abi_enter_us":5162,"abi_return_us":5167,"end_us":5167,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5162,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5192,"operation":"read","slot":0,"start_us":5190},{"abi_enter_us":5203,"abi_return_us":500803,"end_us":500811,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5203,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500890,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500880}],"schema":2,"slots":[{"at_us":500890,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30c2f742d0-622f-90","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":6427,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6474,"slot":0,"token":"18e1fd30e0fccae8-622f-91"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e0fccae8-622f-91","watchdog":false,"workers_joined":1,"writes":[{"at_us":6491,"bytes":57,"kind":"head","slot":0},{"at_us":6498,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":77,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":135,"abi_return_us":6197,"end_us":6203,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":134,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6256,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6248},{"abi_enter_us":6270,"abi_return_us":6279,"end_us":6280,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6270,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6306,"operation":"read","slot":0,"start_us":6303},{"abi_enter_us":6317,"abi_return_us":6318,"end_us":6318,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6317,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6338,"operation":"read","slot":0,"start_us":6337},{"abi_enter_us":6350,"abi_return_us":6351,"end_us":6351,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6350,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6369,"operation":"read","slot":0,"start_us":6369},{"abi_enter_us":6378,"abi_return_us":6381,"end_us":6381,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6378,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6399,"operation":"read","slot":0,"start_us":6398},{"end_us":6414,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6407}],"schema":2,"slots":[{"at_us":6414,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30e0fccae8-622f-91","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":6099,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6058,"slot":0,"token":"18e1fd30e18b7e28-622f-92"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e18b7e28-622f-92","watchdog":false,"workers_joined":1,"writes":[{"at_us":6072,"bytes":57,"kind":"head","slot":0},{"at_us":6078,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":57,"abi_return_us":5835,"end_us":5841,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5899,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5893},{"abi_enter_us":5928,"abi_return_us":5937,"end_us":5937,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5927,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5973,"operation":"read","slot":0,"start_us":5970},{"abi_enter_us":5990,"abi_return_us":5996,"end_us":5997,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5989,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":6020,"operation":"read","slot":0,"start_us":6019},{"end_us":6033,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6029}],"schema":2,"slots":[{"at_us":6033,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30e18b7e28-622f-92","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":5243,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5238,"slot":0,"token":"18e1fd30e20eb018-622f-93"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e20eb018-622f-93","watchdog":false,"workers_joined":1,"writes":[{"at_us":5253,"bytes":57,"kind":"head","slot":0},{"at_us":5259,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":91,"operation":"handoff","start_us":91},{"abi_enter_us":112,"abi_return_us":5003,"end_us":5009,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5063,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5057},{"abi_enter_us":5078,"abi_return_us":5087,"end_us":5087,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5078,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5113,"operation":"read","slot":0,"start_us":5110},{"abi_enter_us":5124,"abi_return_us":5125,"end_us":5126,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5124,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5149,"operation":"read","slot":0,"start_us":5144},{"abi_enter_us":5159,"abi_return_us":5160,"end_us":5160,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5159,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5183,"operation":"read","slot":0,"start_us":5182},{"abi_enter_us":5192,"abi_return_us":5194,"end_us":5194,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5192,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5216,"operation":"read","slot":0,"start_us":5215},{"end_us":5228,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5224}],"schema":2,"slots":[{"at_us":5228,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30e20eb018-622f-93","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":5152,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5224,"slot":0,"token":"18e1fd30e29020d0-622f-94"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e29020d0-622f-94","watchdog":false,"workers_joined":1,"writes":[{"at_us":5236,"bytes":57,"kind":"head","slot":0},{"at_us":5240,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":107,"abi_return_us":4993,"end_us":4997,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5041,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5037},{"abi_enter_us":5055,"abi_return_us":5060,"end_us":5060,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5055,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5092,"operation":"read","slot":0,"start_us":5089},{"abi_enter_us":5103,"abi_return_us":5105,"end_us":5105,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5103,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5126,"operation":"read","slot":0,"start_us":5126},{"end_us":5138,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5135}],"schema":2,"slots":[{"at_us":5138,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30e29020d0-622f-94","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5272,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6495,"slot":0,"token":"18e1fd30e30f8de8-622f-95"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e30f8de8-622f-95","watchdog":false,"workers_joined":1,"writes":[{"at_us":6513,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":128,"operation":"dispatch","outcome":"pending","slot":0,"start_us":38},{"abi_enter_us":206,"abi_return_us":5128,"end_us":5133,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":205,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5183,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5177},{"abi_enter_us":5199,"abi_return_us":5204,"end_us":5204,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5199,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5235,"operation":"read","slot":0,"start_us":5232},{"end_us":5254,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5250}],"schema":2,"slots":[{"at_us":5254,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30e30f8de8-622f-95","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5727,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5820,"slot":0,"token":"18e1fd30e391f8a0-622f-96"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e391f8a0-622f-96","watchdog":false,"workers_joined":1,"writes":[{"at_us":5832,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":55,"abi_return_us":5592,"end_us":5597,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5642,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5637},{"abi_enter_us":5655,"abi_return_us":5658,"end_us":5662,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5655,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5695,"operation":"read","slot":0,"start_us":5693},{"end_us":5710,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5706}],"schema":2,"slots":[{"at_us":5710,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30e391f8a0-622f-96","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5335,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5380,"slot":0,"token":"18e1fd30e4083240-622f-97"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e4083240-622f-97","watchdog":false,"workers_joined":1,"writes":[{"at_us":5400,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":51,"operation":"handoff","start_us":51},{"abi_enter_us":75,"abi_return_us":5207,"end_us":5212,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5263,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5255},{"abi_enter_us":5276,"abi_return_us":5280,"end_us":5281,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5276,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5305,"operation":"read","slot":0,"start_us":5302},{"end_us":5318,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5315}],"schema":2,"slots":[{"at_us":5318,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30e4083240-622f-97","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6316,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6448,"slot":0,"token":"18e1fd30e47b2408-622f-98"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e47b2408-622f-98","watchdog":false,"workers_joined":1,"writes":[{"at_us":6462,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":80,"operation":"handoff","start_us":80},{"abi_enter_us":96,"abi_return_us":6186,"end_us":6192,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6240,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6233},{"abi_enter_us":6254,"abi_return_us":6257,"end_us":6257,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6253,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6284,"operation":"read","slot":0,"start_us":6281},{"end_us":6298,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6295}],"schema":2,"slots":[{"at_us":6298,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30e47b2408-622f-98","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5981,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6069,"slot":0,"token":"18e1fd30e4fd3cb8-622f-99"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e4fd3cb8-622f-99","watchdog":false,"workers_joined":1,"writes":[{"at_us":6088,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":72,"abi_return_us":5910,"end_us":5916,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5952,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5947},{"end_us":5966,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5965}],"schema":2,"slots":[{"at_us":5966,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd30e4fd3cb8-622f-99","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6265,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6451,"slot":0,"token":"18e1fd30e5767fb0-622f-9a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e5767fb0-622f-9a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6466,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":76,"abi_return_us":6203,"end_us":6208,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6239,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6233},{"end_us":6251,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6251}],"schema":2,"slots":[{"at_us":6251,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd30e5767fb0-622f-9a","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5291,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5399,"slot":0,"token":"18e1fd30e5f3b660-622f-9b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e5f3b660-622f-9b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5414,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":81,"abi_return_us":5224,"end_us":5228,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5259,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5254},{"end_us":5271,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5271}],"schema":2,"slots":[{"at_us":5271,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd30e5f3b660-622f-9b","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6346,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6473,"slot":0,"token":"18e1fd30e6641fb8-622f-9c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e6641fb8-622f-9c","watchdog":false,"workers_joined":1,"writes":[{"at_us":6493,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":93,"abi_return_us":6268,"end_us":6276,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6316,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6310},{"end_us":6330,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6330}],"schema":2,"slots":[{"at_us":6330,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd30e6641fb8-622f-9c","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58900,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5586,"slot":0,"token":"18e1fd30e6e4d108-622f-9d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30e6e4d108-622f-9d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5612,"bytes":66,"kind":"head","slot":0},{"at_us":5619,"bytes":6,"kind":"body","slot":0},{"at_us":59076,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":75,"abi_return_us":5314,"end_us":5319,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5368,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5363},{"abi_enter_us":5388,"abi_return_us":5394,"end_us":5395,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5387,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5457,"operation":"read","slot":0,"start_us":5454},{"abi_enter_us":5470,"abi_return_us":58778,"end_us":58781,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5470,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":58854,"error":"Other","operation":"read","slot":0,"start_us":58841},{"end_us":58880,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58873}],"schema":2,"slots":[{"at_us":58880,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30e6e4d108-622f-9d","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":60134,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5231,"slot":0,"token":"18e1fd30ea8da910-622f-9e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30ea8da910-622f-9e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5245,"bytes":66,"kind":"head","slot":0},{"at_us":5251,"bytes":6,"kind":"body","slot":0},{"at_us":60297,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":88,"abi_return_us":4988,"end_us":4995,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5038,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5032},{"abi_enter_us":5052,"abi_return_us":5065,"end_us":5065,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5052,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5095,"operation":"read","slot":0,"start_us":5092},{"abi_enter_us":5109,"abi_return_us":60017,"end_us":60021,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5109,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":60096,"error":"Other","operation":"read","slot":0,"start_us":60090},{"end_us":60120,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60114}],"schema":2,"slots":[{"at_us":60120,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30ea8da910-622f-9e","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":60508,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6449,"slot":0,"token":"18e1fd30ee4b8400-622f-9f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30ee4b8400-622f-9f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6462,"bytes":66,"kind":"head","slot":0},{"at_us":6467,"bytes":6,"kind":"body","slot":0},{"at_us":60642,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":72,"operation":"handoff","start_us":72},{"abi_enter_us":85,"abi_return_us":6227,"end_us":6233,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6285,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6279},{"abi_enter_us":6299,"abi_return_us":6305,"end_us":6305,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6298,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6339,"operation":"read","slot":0,"start_us":6336},{"abi_enter_us":6354,"abi_return_us":60390,"end_us":60397,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6354,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":60467,"error":"Other","operation":"read","slot":0,"start_us":60462},{"end_us":60491,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60484}],"schema":2,"slots":[{"at_us":60491,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30ee4b8400-622f-9f","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":60720,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5780,"slot":0,"token":"18e1fd30f2101998-622f-a0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30f2101998-622f-a0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5798,"bytes":66,"kind":"head","slot":0},{"at_us":5804,"bytes":6,"kind":"body","slot":0},{"at_us":60847,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":57,"operation":"handoff","start_us":57},{"abi_enter_us":70,"abi_return_us":5554,"end_us":5560,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5615,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5609},{"abi_enter_us":5630,"abi_return_us":5637,"end_us":5637,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5629,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5667,"operation":"read","slot":0,"start_us":5664},{"abi_enter_us":5686,"abi_return_us":60610,"end_us":60615,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5686,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":60683,"error":"Other","operation":"read","slot":0,"start_us":60677},{"end_us":60706,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60699}],"schema":2,"slots":[{"at_us":60706,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30f2101998-622f-a0","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":6050,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6203,"slot":0,"token":"18e1fd30f5db0830-622f-a1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30f5db0830-622f-a1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":83,"abi_return_us":5960,"end_us":5967,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6018,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6013},{"end_us":6034,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6034}],"schema":2,"slots":[{"at_us":6034,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd30f5db0830-622f-a1","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":5928,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6048,"slot":0,"token":"18e1fd30f6571218-622f-a2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30f6571218-622f-a2","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":88,"abi_return_us":5848,"end_us":5855,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5893,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5888},{"end_us":5913,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5913}],"schema":2,"slots":[{"at_us":5913,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd30f6571218-622f-a2","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":5948,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6091,"slot":0,"token":"18e1fd30f6cf3400-622f-a3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30f6cf3400-622f-a3","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":73,"abi_return_us":5877,"end_us":5881,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5920,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5910},{"end_us":5933,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5933}],"schema":2,"slots":[{"at_us":5933,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd30f6cf3400-622f-a3","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":5940,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6409,"slot":0,"token":"18e1fd30f752bfc8-622f-a4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30f752bfc8-622f-a4","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":65,"operation":"dispatch","outcome":"pending","slot":0,"start_us":44},{"body_reads":0,"deadline_us":500000,"end_us":92,"operation":"handoff","start_us":92},{"abi_enter_us":105,"abi_return_us":5860,"end_us":5868,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":105,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5908,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5902},{"end_us":5923,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5923}],"schema":2,"slots":[{"at_us":5923,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd30f752bfc8-622f-a4","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":59198,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5946,"slot":0,"token":"18e1fd30f7d1f630-622f-a5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30f7d1f630-622f-a5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5965,"bytes":57,"kind":"head","slot":0},{"at_us":5971,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":61,"abi_return_us":5744,"end_us":5750,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5801,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5794},{"bytes":1,"capacity":1,"end_us":5838,"operation":"read","slot":0,"start_us":5830},{"capacity":1,"end_us":59103,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5859},{"end_us":59180,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59170}],"schema":2,"slots":[{"at_us":59180,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30f7d1f630-622f-a5","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":56489,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5741,"slot":0,"token":"18e1fd30fb7b5308-622f-a6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30fb7b5308-622f-a6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5760,"bytes":57,"kind":"head","slot":0},{"at_us":5766,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":73,"abi_return_us":5498,"end_us":5504,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5551,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5545},{"abi_enter_us":5563,"abi_return_us":5570,"end_us":5570,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5563,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5599,"operation":"read","slot":0,"start_us":5596},{"abi_enter_us":5611,"abi_return_us":56376,"end_us":56381,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5611,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":56447,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56442},{"end_us":56475,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56469}],"schema":2,"slots":[{"at_us":56475,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30fb7b5308-622f-a6","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":59060,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5213,"slot":0,"token":"18e1fd30fefd9810-622f-a7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd30fefd9810-622f-a7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5228,"bytes":57,"kind":"head","slot":0},{"at_us":5233,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":69,"abi_return_us":4922,"end_us":4926,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4965,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4959},{"abi_enter_us":4978,"abi_return_us":4984,"end_us":4984,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4978,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5017,"operation":"read","slot":0,"start_us":5014},{"abi_enter_us":5028,"abi_return_us":58911,"end_us":58920,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5028,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":59010,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59005},{"end_us":59044,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59033}],"schema":2,"slots":[{"at_us":59044,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd30fefd9810-622f-a7","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":55391,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5246,"slot":0,"token":"18e1fd3102af3e00-622f-a8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3102af3e00-622f-a8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5263,"bytes":57,"kind":"head","slot":0},{"at_us":5271,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"abi_enter_us":104,"abi_return_us":4952,"end_us":4957,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5015,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5003},{"abi_enter_us":5030,"abi_return_us":5039,"end_us":5040,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5029,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5066,"operation":"read","slot":0,"start_us":5063},{"abi_enter_us":5085,"abi_return_us":55223,"end_us":55231,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5085,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":55343,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":55338},{"end_us":55374,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55367}],"schema":2,"slots":[{"at_us":55374,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3102af3e00-622f-a8","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"truncated","completed_us":61282,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6396,"slot":0,"token":"18e1fd31062b7c10-622f-a9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd31062b7c10-622f-a9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6413,"bytes":57,"kind":"head","slot":0},{"at_us":6419,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":86,"operation":"handoff","start_us":86},{"abi_enter_us":100,"abi_return_us":6157,"end_us":6165,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":99,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6229,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6223},{"abi_enter_us":6244,"abi_return_us":6248,"end_us":6249,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6244,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6284,"operation":"read","slot":0,"start_us":6281},{"abi_enter_us":6296,"abi_return_us":61174,"end_us":61179,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6296,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":61241,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":61237},{"end_us":61266,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61261}],"schema":2,"slots":[{"at_us":61266,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd31062b7c10-622f-a9","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":501179,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6479,"slot":0,"token":"18e1fd3109fb3538-622f-aa"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd3109fb3538-622f-aa","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":79,"abi_return_us":501083,"end_us":501091,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501158,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501157}],"schema":2,"slots":[{"at_us":501158,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3109fb3538-622f-aa","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":501291,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5214,"slot":0,"token":"18e1fd3127f762c8-622f-ab"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd3127f762c8-622f-ab","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":84,"abi_return_us":501116,"end_us":501127,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501261,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501261}],"schema":2,"slots":[{"at_us":501261,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3127f762c8-622f-ab","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":501536,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5326,"slot":0,"token":"18e1fd3145f7da00-622f-ac"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd3145f7da00-622f-ac","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":82,"abi_return_us":501399,"end_us":501413,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501503,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501503}],"schema":2,"slots":[{"at_us":501503,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3145f7da00-622f-ac","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"silent-head","completed_us":501090,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5991,"slot":0,"token":"18e1fd3164283ad8-622f-ad"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd3164283ad8-622f-ad","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":145,"operation":"dispatch","outcome":"pending","slot":0,"start_us":54},{"body_reads":0,"deadline_us":500000,"end_us":224,"operation":"handoff","start_us":224},{"abi_enter_us":256,"abi_return_us":500987,"end_us":500995,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":256,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501069,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501069}],"schema":2,"slots":[{"at_us":501069,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3164283ad8-622f-ad","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":500585,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5383,"slot":0,"token":"18e1fd31822c4bf0-622f-ae"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd31822c4bf0-622f-ae","watchdog":false,"workers_joined":1,"writes":[{"at_us":5403,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":77,"abi_return_us":5177,"end_us":5183,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5236,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5230},{"abi_enter_us":5250,"abi_return_us":500498,"end_us":500502,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5250,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500563,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500555}],"schema":2,"slots":[{"at_us":500563,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd31822c4bf0-622f-ae","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":500730,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5694,"slot":0,"token":"18e1fd31a024d7d0-622f-af"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd31a024d7d0-622f-af","watchdog":false,"workers_joined":1,"writes":[{"at_us":5704,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":83,"abi_return_us":5457,"end_us":5461,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":83,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5507,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5501},{"abi_enter_us":5523,"abi_return_us":500641,"end_us":500645,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5523,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500709,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500702}],"schema":2,"slots":[{"at_us":500709,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd31a024d7d0-622f-af","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":501681,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6238,"slot":0,"token":"18e1fd31be260a88-622f-b0"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd31be260a88-622f-b0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6259,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":90,"operation":"handoff","start_us":90},{"abi_enter_us":110,"abi_return_us":5311,"end_us":5318,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":109,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5382,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5376},{"abi_enter_us":5440,"abi_return_us":501516,"end_us":501522,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5440,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501613,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501588}],"schema":2,"slots":[{"at_us":501613,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd31be260a88-622f-b0","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"pending-body","completed_us":501527,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5232,"slot":0,"token":"18e1fd31dc3e2c58-622f-b1"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd31dc3e2c58-622f-b1","watchdog":false,"workers_joined":1,"writes":[{"at_us":5241,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":76,"operation":"handoff","start_us":76},{"abi_enter_us":88,"abi_return_us":4912,"end_us":4915,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4955,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4947},{"abi_enter_us":4971,"abi_return_us":501433,"end_us":501439,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4971,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501505,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501498}],"schema":2,"slots":[{"at_us":501505,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd31dc3e2c58-622f-b1","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":500848,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5590,"slot":0,"token":"18e1fd31fa42b2a0-622f-b2"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd31fa42b2a0-622f-b2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5607,"bytes":57,"kind":"head","slot":0},{"at_us":5613,"bytes":1,"kind":"body","slot":0},{"at_us":309515,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":70,"abi_return_us":5289,"end_us":5293,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5329,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5325},{"abi_enter_us":5346,"abi_return_us":5350,"end_us":5350,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5346,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5378,"operation":"read","slot":0,"start_us":5375},{"abi_enter_us":5391,"abi_return_us":309243,"end_us":309255,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5391,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309343,"operation":"read","slot":0,"start_us":309338},{"abi_enter_us":309360,"abi_return_us":500726,"end_us":500730,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309360,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500812,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500795}],"schema":2,"slots":[{"at_us":500812,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd31fa42b2a0-622f-b2","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":501221,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6133,"slot":0,"token":"18e1fd321849d8c8-622f-b3"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd321849d8c8-622f-b3","watchdog":false,"workers_joined":1,"writes":[{"at_us":6145,"bytes":57,"kind":"head","slot":0},{"at_us":6150,"bytes":1,"kind":"body","slot":0},{"at_us":306202,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":87,"abi_return_us":5826,"end_us":5830,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5873,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5867},{"abi_enter_us":5887,"abi_return_us":5891,"end_us":5891,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5886,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5923,"operation":"read","slot":0,"start_us":5918},{"abi_enter_us":5936,"abi_return_us":305861,"end_us":305865,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5936,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":305939,"operation":"read","slot":0,"start_us":305935},{"abi_enter_us":305954,"abi_return_us":501107,"end_us":501114,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":305954,"status":"FastlyStatus::OK","timeout_ms":194},{"end_us":501197,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501185}],"schema":2,"slots":[{"at_us":501197,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd321849d8c8-622f-b3","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":501871,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6451,"slot":0,"token":"18e1fd32365e4d30-622f-b4"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd32365e4d30-622f-b4","watchdog":false,"workers_joined":1,"writes":[{"at_us":6473,"bytes":57,"kind":"head","slot":0},{"at_us":6481,"bytes":1,"kind":"body","slot":0},{"at_us":311521,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":108,"operation":"handoff","start_us":108},{"abi_enter_us":148,"abi_return_us":6284,"end_us":6291,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":148,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6352,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6343},{"abi_enter_us":6367,"abi_return_us":6375,"end_us":6376,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6367,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6410,"operation":"read","slot":0,"start_us":6408},{"abi_enter_us":6424,"abi_return_us":311169,"end_us":311174,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6424,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":311279,"operation":"read","slot":0,"start_us":311273},{"abi_enter_us":311324,"abi_return_us":501749,"end_us":501756,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":311324,"status":"FastlyStatus::OK","timeout_ms":188},{"end_us":501838,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501817}],"schema":2,"slots":[{"at_us":501838,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd32365e4d30-622f-b4","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"trickle","completed_us":501137,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6485,"slot":0,"token":"18e1fd3254a3b4d8-622f-b5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd3254a3b4d8-622f-b5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6501,"bytes":57,"kind":"head","slot":0},{"at_us":6512,"bytes":1,"kind":"body","slot":0},{"at_us":309655,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":102,"operation":"handoff","start_us":102},{"abi_enter_us":145,"abi_return_us":6064,"end_us":6072,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":145,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6139,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6129},{"abi_enter_us":6153,"abi_return_us":6161,"end_us":6161,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6153,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6190,"operation":"read","slot":0,"start_us":6187},{"abi_enter_us":6207,"abi_return_us":309165,"end_us":309169,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6207,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309241,"operation":"read","slot":0,"start_us":309238},{"abi_enter_us":309255,"abi_return_us":501027,"end_us":501033,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309255,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501103,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501095}],"schema":2,"slots":[{"at_us":501103,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3254a3b4d8-622f-b5","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":501385,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6563,"slot":1,"token":"18e1fd3272b4be40-622f-b6"},{"path":"/readiness/siblings/0","received_us":6574,"slot":0,"token":"18e1fd3272b4be40-622f-b6"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd3272b4be40-622f-b6","watchdog":false,"workers_joined":2,"writes":[{"at_us":6584,"bytes":57,"kind":"head","slot":1},{"at_us":6601,"bytes":57,"kind":"head","slot":0},{"at_us":12883,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":115,"operation":"dispatch","outcome":"pending","slot":1,"start_us":102},{"abi_enter_us":129,"abi_return_us":6269,"end_us":6275,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":128,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6324,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6312},{"abi_enter_us":6340,"abi_return_us":6343,"end_us":6343,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6340,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6358,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6357},{"abi_enter_us":6368,"abi_return_us":12503,"end_us":12507,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6368,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":12573,"operation":"read","slot":1,"start_us":12569},{"abi_enter_us":12586,"abi_return_us":12590,"end_us":12590,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12586,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12614,"operation":"read","slot":1,"start_us":12612},{"abi_enter_us":12623,"abi_return_us":12625,"end_us":12625,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12623,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":1,"capacity":1,"end_us":12651,"operation":"read","slot":1,"start_us":12650},{"abi_enter_us":12662,"abi_return_us":12665,"end_us":12665,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":12662,"status":"FastlyStatus::OK","timeout_ms":487},{"bytes":0,"capacity":1,"end_us":12688,"operation":"read","slot":1,"start_us":12687},{"end_us":12702,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":12696},{"abi_enter_us":12719,"abi_return_us":501077,"end_us":501124,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12719,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":501347,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501330}],"schema":2,"slots":[{"at_us":501347,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":12702,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3272b4be40-622f-b6","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":500805,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6502,"slot":1,"token":"18e1fd3290df27c0-622f-b7"},{"path":"/readiness/siblings/0","received_us":6542,"slot":0,"token":"18e1fd3290df27c0-622f-b7"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd3290df27c0-622f-b7","watchdog":false,"workers_joined":2,"writes":[{"at_us":6524,"bytes":57,"kind":"head","slot":1},{"at_us":6548,"bytes":57,"kind":"head","slot":0},{"at_us":11563,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"end_us":193,"operation":"dispatch","outcome":"pending","slot":1,"start_us":161},{"abi_enter_us":253,"abi_return_us":5916,"end_us":5923,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":253,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5991,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5985},{"abi_enter_us":6010,"abi_return_us":6013,"end_us":6013,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6010,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6029,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6029},{"abi_enter_us":6040,"abi_return_us":10932,"end_us":10934,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6040,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":10989,"operation":"read","slot":1,"start_us":10986},{"abi_enter_us":11004,"abi_return_us":11010,"end_us":11011,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11004,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11036,"operation":"read","slot":1,"start_us":11035},{"end_us":11050,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11045},{"abi_enter_us":11064,"abi_return_us":500669,"end_us":500680,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11064,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":500774,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500751}],"schema":2,"slots":[{"at_us":500774,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11050,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3290df27c0-622f-b7","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":502535,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6336,"slot":1,"token":"18e1fd32aef4ffa0-622f-b8"},{"path":"/readiness/siblings/0","received_us":6348,"slot":0,"token":"18e1fd32aef4ffa0-622f-b8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd32aef4ffa0-622f-b8","watchdog":false,"workers_joined":2,"writes":[{"at_us":6364,"bytes":57,"kind":"head","slot":1},{"at_us":6375,"bytes":57,"kind":"head","slot":0},{"at_us":11580,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":31},{"end_us":178,"operation":"dispatch","outcome":"pending","slot":1,"start_us":139},{"body_reads":0,"deadline_us":500000,"end_us":1073,"operation":"handoff","start_us":1073},{"abi_enter_us":1107,"abi_return_us":5471,"end_us":5478,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":1107,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":5541,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5534},{"abi_enter_us":5558,"abi_return_us":5564,"end_us":5564,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5558,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5580,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5579},{"abi_enter_us":5593,"abi_return_us":10632,"end_us":10637,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5593,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":10704,"operation":"read","slot":1,"start_us":10700},{"abi_enter_us":10718,"abi_return_us":10721,"end_us":10721,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10718,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10750,"operation":"read","slot":1,"start_us":10748},{"abi_enter_us":10761,"abi_return_us":10762,"end_us":10762,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10761,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":1,"capacity":1,"end_us":10783,"operation":"read","slot":1,"start_us":10783},{"abi_enter_us":10793,"abi_return_us":10798,"end_us":10798,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10793,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":1,"end_us":10824,"operation":"read","slot":1,"start_us":10823},{"end_us":10838,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10833},{"abi_enter_us":10854,"abi_return_us":502427,"end_us":502433,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":10854,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":502514,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502505}],"schema":2,"slots":[{"at_us":502514,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":10838,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd32aef4ffa0-622f-b8","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"siblings","completed_us":501768,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5644,"slot":0,"token":"18e1fd32cd6ab678-622f-b9"},{"path":"/readiness/siblings/1","received_us":5702,"slot":1,"token":"18e1fd32cd6ab678-622f-b9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd32cd6ab678-622f-b9","watchdog":false,"workers_joined":2,"writes":[{"at_us":5661,"bytes":57,"kind":"head","slot":0},{"at_us":5710,"bytes":57,"kind":"head","slot":1},{"at_us":5716,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":119,"operation":"dispatch","outcome":"pending","slot":1,"start_us":100},{"body_reads":0,"deadline_us":500000,"end_us":148,"operation":"handoff","start_us":148},{"abi_enter_us":172,"abi_return_us":5337,"end_us":5342,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":172,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5384,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5378},{"abi_enter_us":5400,"abi_return_us":5403,"end_us":5403,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5400,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5420,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5419},{"abi_enter_us":5432,"abi_return_us":5434,"end_us":5435,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5431,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5465,"operation":"read","slot":1,"start_us":5462},{"abi_enter_us":5477,"abi_return_us":5480,"end_us":5480,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5477,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5505,"operation":"read","slot":1,"start_us":5504},{"end_us":5521,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5515},{"abi_enter_us":5537,"abi_return_us":501600,"end_us":501647,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5537,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501746,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501734}],"schema":2,"slots":[{"at_us":501746,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5521,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd32cd6ab678-622f-b9","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":502055,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5860,"slot":0,"token":"18e1fd32eb8f7e90-622f-ba"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd32eb8f7e90-622f-ba","watchdog":false,"workers_joined":1,"writes":[{"at_us":5876,"bytes":57,"kind":"head","slot":0},{"at_us":5882,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":78,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":124,"abi_return_us":5566,"end_us":5574,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":123,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5652,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5642},{"abi_enter_us":5671,"abi_return_us":5676,"end_us":5676,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5671,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5706,"operation":"read","slot":0,"start_us":5703},{"abi_enter_us":5719,"abi_return_us":501760,"end_us":501790,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5719,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501975,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501925}],"schema":2,"slots":[{"at_us":501975,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd32eb8f7e90-622f-ba","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":501293,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6243,"slot":0,"token":"18e1fd3309a77568-622f-bb"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd3309a77568-622f-bb","watchdog":false,"workers_joined":1,"writes":[{"at_us":6261,"bytes":57,"kind":"head","slot":0},{"at_us":6266,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":94,"abi_return_us":5806,"end_us":5810,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5858,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5853},{"abi_enter_us":5873,"abi_return_us":5878,"end_us":5878,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5873,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5926,"operation":"read","slot":0,"start_us":5923},{"abi_enter_us":5939,"abi_return_us":501144,"end_us":501150,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5939,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501265,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501236}],"schema":2,"slots":[{"at_us":501265,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3309a77568-622f-bb","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":501815,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5288,"slot":0,"token":"18e1fd3327afff08-622f-bc"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd3327afff08-622f-bc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5304,"bytes":57,"kind":"head","slot":0},{"at_us":5309,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":99,"abi_return_us":5010,"end_us":5014,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5050,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5044},{"abi_enter_us":5061,"abi_return_us":5065,"end_us":5065,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5061,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5092,"operation":"read","slot":0,"start_us":5089},{"abi_enter_us":5103,"abi_return_us":501665,"end_us":501676,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5103,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501740,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501732}],"schema":2,"slots":[{"at_us":501740,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3327afff08-622f-bc","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"exhausted","completed_us":501124,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5205,"slot":0,"token":"18e1fd3345cc9d48-622f-bd"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd3345cc9d48-622f-bd","watchdog":false,"workers_joined":1,"writes":[{"at_us":5213,"bytes":57,"kind":"head","slot":0},{"at_us":5217,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":95,"abi_return_us":4877,"end_us":4879,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4909,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4904},{"abi_enter_us":4921,"abi_return_us":4924,"end_us":4925,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4921,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4958,"operation":"read","slot":0,"start_us":4956},{"abi_enter_us":4969,"abi_return_us":500983,"end_us":500992,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4969,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501058,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501049}],"schema":2,"slots":[{"at_us":501058,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3345cc9d48-622f-bd","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":6035,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5973,"slot":0,"token":"18e1fd3363d7d280-622f-be"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3363d7d280-622f-be","watchdog":false,"workers_joined":1,"writes":[{"at_us":5996,"bytes":57,"kind":"head","slot":0},{"at_us":6004,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":76,"abi_return_us":5628,"end_us":5635,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5701,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5694},{"abi_enter_us":5718,"abi_return_us":5726,"end_us":5726,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5717,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5758,"operation":"read","slot":0,"start_us":5755},{"abi_enter_us":5772,"abi_return_us":5773,"end_us":5773,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5771,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5795,"operation":"read","slot":0,"start_us":5794},{"abi_enter_us":5805,"abi_return_us":5805,"end_us":5806,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5804,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5958,"operation":"read","slot":0,"start_us":5957},{"abi_enter_us":5977,"abi_return_us":5980,"end_us":5981,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5976,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":6004,"operation":"read","slot":0,"start_us":6003},{"end_us":6018,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6014}],"schema":2,"slots":[{"at_us":6018,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3363d7d280-622f-be","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":5464,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5554,"slot":0,"token":"18e1fd336464dbf8-622f-bf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd336464dbf8-622f-bf","watchdog":false,"workers_joined":1,"writes":[{"at_us":5573,"bytes":57,"kind":"head","slot":0},{"at_us":5581,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":90,"abi_return_us":5307,"end_us":5312,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5352,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5346},{"abi_enter_us":5364,"abi_return_us":5371,"end_us":5371,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5364,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5402,"operation":"read","slot":0,"start_us":5400},{"abi_enter_us":5413,"abi_return_us":5416,"end_us":5417,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5413,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5440,"operation":"read","slot":0,"start_us":5439},{"end_us":5452,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5449}],"schema":2,"slots":[{"at_us":5452,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd336464dbf8-622f-bf","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":5501,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5492,"slot":0,"token":"18e1fd3364ddf010-622f-c0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3364ddf010-622f-c0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5510,"bytes":57,"kind":"head","slot":0},{"at_us":5516,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"abi_enter_us":130,"abi_return_us":5282,"end_us":5289,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":129,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5339,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5334},{"abi_enter_us":5353,"abi_return_us":5358,"end_us":5358,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5352,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5382,"operation":"read","slot":0,"start_us":5380},{"abi_enter_us":5393,"abi_return_us":5394,"end_us":5394,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5393,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5413,"operation":"read","slot":0,"start_us":5412},{"abi_enter_us":5424,"abi_return_us":5425,"end_us":5425,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5424,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5444,"operation":"read","slot":0,"start_us":5443},{"abi_enter_us":5453,"abi_return_us":5455,"end_us":5455,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5452,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5472,"operation":"read","slot":0,"start_us":5472},{"end_us":5485,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5480}],"schema":2,"slots":[{"at_us":5485,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3364ddf010-622f-c0","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"eof","completed_us":5762,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5786,"slot":0,"token":"18e1fd33655d8438-622f-c1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd33655d8438-622f-c1","watchdog":false,"workers_joined":1,"writes":[{"at_us":5799,"bytes":57,"kind":"head","slot":0},{"at_us":5804,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"abi_enter_us":78,"abi_return_us":5576,"end_us":5586,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5636,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5630},{"abi_enter_us":5649,"abi_return_us":5659,"end_us":5660,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5649,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5689,"operation":"read","slot":0,"start_us":5686},{"abi_enter_us":5704,"abi_return_us":5707,"end_us":5707,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5704,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5730,"operation":"read","slot":0,"start_us":5730},{"end_us":5744,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5739}],"schema":2,"slots":[{"at_us":5744,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd33655d8438-622f-c1","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5142,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5357,"slot":0,"token":"18e1fd3365dbc0a0-622f-c2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3365dbc0a0-622f-c2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5378,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"abi_enter_us":94,"abi_return_us":5017,"end_us":5022,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":94,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5069,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5063},{"abi_enter_us":5084,"abi_return_us":5088,"end_us":5088,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5084,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5113,"operation":"read","slot":0,"start_us":5111},{"end_us":5127,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5124}],"schema":2,"slots":[{"at_us":5127,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3365dbc0a0-622f-c2","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5304,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5438,"slot":0,"token":"18e1fd33664f2b80-622f-c3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd33664f2b80-622f-c3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5450,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":69,"abi_return_us":5182,"end_us":5187,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5228,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5222},{"abi_enter_us":5240,"abi_return_us":5243,"end_us":5243,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5240,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5272,"operation":"read","slot":0,"start_us":5270},{"end_us":5285,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5283}],"schema":2,"slots":[{"at_us":5285,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd33664f2b80-622f-c3","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5458,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5556,"slot":0,"token":"18e1fd3366c24458-622f-c4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3366c24458-622f-c4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5574,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"body_reads":0,"deadline_us":500000,"end_us":105,"operation":"handoff","start_us":105},{"abi_enter_us":141,"abi_return_us":5334,"end_us":5339,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":140,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5384,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5377},{"abi_enter_us":5398,"abi_return_us":5402,"end_us":5403,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5398,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5428,"operation":"read","slot":0,"start_us":5425},{"end_us":5442,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5439}],"schema":2,"slots":[{"at_us":5442,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3366c24458-622f-c4","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6402,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6448,"slot":0,"token":"18e1fd3367365b18-622f-c5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3367365b18-622f-c5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6462,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":109,"operation":"handoff","start_us":109},{"abi_enter_us":123,"abi_return_us":6245,"end_us":6255,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":123,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6310,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6303},{"abi_enter_us":6327,"abi_return_us":6331,"end_us":6331,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6327,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6364,"operation":"read","slot":0,"start_us":6361},{"end_us":6379,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6375}],"schema":2,"slots":[{"at_us":6379,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3367365b18-622f-c5","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5333,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5388,"slot":0,"token":"18e1fd3367b92778-622f-c6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3367b92778-622f-c6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5402,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":75,"abi_return_us":5259,"end_us":5264,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5301,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5295},{"end_us":5318,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5318}],"schema":2,"slots":[{"at_us":5318,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3367b92778-622f-c6","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5682,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5737,"slot":0,"token":"18e1fd3368276a08-622f-c7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3368276a08-622f-c7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5765,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":92,"abi_return_us":5596,"end_us":5601,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5638,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5632},{"end_us":5652,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5652}],"schema":2,"slots":[{"at_us":5652,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3368276a08-622f-c7","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5166,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5246,"slot":0,"token":"18e1fd33689d1ed8-622f-c8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd33689d1ed8-622f-c8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5257,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"abi_enter_us":79,"abi_return_us":5093,"end_us":5098,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5134,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5129},{"end_us":5148,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5148}],"schema":2,"slots":[{"at_us":5148,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd33689d1ed8-622f-c8","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5125,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5224,"slot":0,"token":"18e1fd33690c8278-622f-c9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd33690c8278-622f-c9","watchdog":false,"workers_joined":1,"writes":[{"at_us":5242,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":111,"abi_return_us":5054,"end_us":5059,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5094,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5089},{"end_us":5110,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5110}],"schema":2,"slots":[{"at_us":5110,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd33690c8278-622f-c9","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":60368,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5481,"slot":0,"token":"18e1fd3369791370-622f-ca"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3369791370-622f-ca","watchdog":false,"workers_joined":1,"writes":[{"at_us":5502,"bytes":66,"kind":"head","slot":0},{"at_us":5512,"bytes":6,"kind":"body","slot":0},{"at_us":60557,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":65,"abi_return_us":5215,"end_us":5220,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5273,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5267},{"abi_enter_us":5290,"abi_return_us":5294,"end_us":5295,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5290,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5320,"operation":"read","slot":0,"start_us":5318},{"abi_enter_us":5333,"abi_return_us":60267,"end_us":60270,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5333,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":60331,"error":"Other","operation":"read","slot":0,"start_us":60320},{"end_us":60354,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60348}],"schema":2,"slots":[{"at_us":60354,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3369791370-622f-ca","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57813,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6451,"slot":0,"token":"18e1fd336d448ac0-622f-cb"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd336d448ac0-622f-cb","watchdog":false,"workers_joined":1,"writes":[{"at_us":6466,"bytes":66,"kind":"head","slot":0},{"at_us":6472,"bytes":6,"kind":"body","slot":0},{"at_us":57944,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":70,"abi_return_us":6166,"end_us":6170,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6209,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6200},{"abi_enter_us":6224,"abi_return_us":6230,"end_us":6231,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6224,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6258,"operation":"read","slot":0,"start_us":6255},{"abi_enter_us":6269,"abi_return_us":57660,"end_us":57667,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6269,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":57774,"error":"Other","operation":"read","slot":0,"start_us":57765},{"end_us":57798,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57791}],"schema":2,"slots":[{"at_us":57798,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd336d448ac0-622f-cb","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59389,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6421,"slot":0,"token":"18e1fd3370e0b4b0-622f-cc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3370e0b4b0-622f-cc","watchdog":false,"workers_joined":1,"writes":[{"at_us":6433,"bytes":66,"kind":"head","slot":0},{"at_us":6439,"bytes":6,"kind":"body","slot":0},{"at_us":59392,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":92,"abi_return_us":6197,"end_us":6200,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6233,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6227},{"abi_enter_us":6245,"abi_return_us":6249,"end_us":6249,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6245,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6273,"operation":"read","slot":0,"start_us":6271},{"abi_enter_us":6288,"abi_return_us":59169,"end_us":59175,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6288,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":59237,"error":"Other","operation":"read","slot":0,"start_us":59232},{"end_us":59369,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59360}],"schema":2,"slots":[{"at_us":59369,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3370e0b4b0-622f-cc","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57106,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5585,"slot":0,"token":"18e1fd33749417f0-622f-cd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd33749417f0-622f-cd","watchdog":false,"workers_joined":1,"writes":[{"at_us":5624,"bytes":66,"kind":"head","slot":0},{"at_us":5670,"bytes":6,"kind":"body","slot":0},{"at_us":57229,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":93,"abi_return_us":5463,"end_us":5472,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5582,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5569},{"abi_enter_us":5634,"abi_return_us":5679,"end_us":5680,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5634,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5714,"operation":"read","slot":0,"start_us":5712},{"abi_enter_us":5730,"abi_return_us":56935,"end_us":56941,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5730,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57016,"error":"Other","operation":"read","slot":0,"start_us":57010},{"end_us":57084,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57076}],"schema":2,"slots":[{"at_us":57084,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd33749417f0-622f-cd","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":6138,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6116,"slot":0,"token":"18e1fd3378391798-622f-ce"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3378391798-622f-ce","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":83,"abi_return_us":5888,"end_us":5900,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6054,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6044},{"end_us":6111,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6111}],"schema":2,"slots":[{"at_us":6111,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3378391798-622f-ce","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":5479,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5805,"slot":0,"token":"18e1fd3378bd2448-622f-cf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3378bd2448-622f-cf","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":110,"abi_return_us":5405,"end_us":5410,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":109,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5450,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5444},{"end_us":5464,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5464}],"schema":2,"slots":[{"at_us":5464,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3378bd2448-622f-cf","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":5198,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5358,"slot":0,"token":"18e1fd33793b58e0-622f-d0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd33793b58e0-622f-d0","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":90,"operation":"handoff","start_us":90},{"abi_enter_us":112,"abi_return_us":5121,"end_us":5126,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5165,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5159},{"end_us":5182,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5182}],"schema":2,"slots":[{"at_us":5182,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd33793b58e0-622f-d0","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"head-error","completed_us":6417,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6515,"slot":0,"token":"18e1fd3379b3a5c0-622f-d1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3379b3a5c0-622f-d1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":88,"operation":"handoff","start_us":88},{"abi_enter_us":110,"abi_return_us":6328,"end_us":6336,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":109,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6385,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6378},{"end_us":6401,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6401}],"schema":2,"slots":[{"at_us":6401,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3379b3a5c0-622f-d1","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":508002,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":5304,"slot":0,"token":"18e1fd337a3bc568-622f-d2"},{"path":"/readiness/mixed-head-body/1","received_us":5315,"slot":1,"token":"18e1fd337a3bc568-622f-d2"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd337a3bc568-622f-d2","watchdog":false,"workers_joined":2,"writes":[{"at_us":5331,"bytes":61,"kind":"head","slot":1},{"at_us":5362,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":116,"operation":"dispatch","outcome":"pending","slot":1,"start_us":89},{"abi_enter_us":132,"abi_return_us":5111,"end_us":5118,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":132,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5171,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5165},{"abi_enter_us":5191,"abi_return_us":5196,"end_us":5197,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5191,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5230,"operation":"read","slot":1,"start_us":5226},{"abi_enter_us":5274,"abi_return_us":5285,"end_us":5286,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5274,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5346,"operation":"read","slot":1,"start_us":5345},{"abi_enter_us":5385,"abi_return_us":5388,"end_us":5388,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5385,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5415,"operation":"read","slot":1,"start_us":5414},{"abi_enter_us":5435,"abi_return_us":5438,"end_us":5438,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5435,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5461,"operation":"read","slot":1,"start_us":5461},{"end_us":5477,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5473},{"abi_enter_us":5490,"abi_return_us":505376,"end_us":505539,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":5490,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":507957,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":507957}],"schema":2,"slots":[{"at_us":507957,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":5477,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd337a3bc568-622f-d2","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":501380,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":9498,"slot":1,"token":"18e1fd339b5a93a0-622f-d3"},{"path":"/readiness/mixed-head-body/0","received_us":9808,"slot":0,"token":"18e1fd339b5a93a0-622f-d3"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd339b5a93a0-622f-d3","watchdog":false,"workers_joined":2,"writes":[{"at_us":9538,"bytes":61,"kind":"head","slot":1},{"at_us":9552,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":3445,"operation":"dispatch","outcome":"pending","slot":0,"start_us":3397},{"end_us":3559,"operation":"dispatch","outcome":"pending","slot":1,"start_us":3543},{"body_reads":0,"deadline_us":500000,"end_us":3575,"operation":"handoff","start_us":3575},{"abi_enter_us":3587,"abi_return_us":6940,"end_us":7057,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":3587,"status":"FastlyStatus::OK","timeout_ms":496},{"end_us":7750,"operation":"head-poll","outcome":"ready","slot":1,"start_us":7483},{"abi_enter_us":7955,"abi_return_us":7964,"end_us":7965,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":7955,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":8131,"capacity":16384,"end_us":7999,"operation":"read","slot":1,"start_us":7996},{"abi_enter_us":8039,"abi_return_us":8047,"end_us":8048,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":8039,"status":"FastlyStatus::OK","timeout_ms":491},{"bytes":16384,"capacity":16384,"end_us":8096,"operation":"read","slot":1,"start_us":8082},{"abi_enter_us":8140,"abi_return_us":10358,"end_us":10360,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":8140,"status":"FastlyStatus::OK","timeout_ms":491},{"bytes":8254,"capacity":16384,"end_us":10397,"operation":"read","slot":1,"start_us":10395},{"abi_enter_us":10418,"abi_return_us":10421,"end_us":10421,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":10417,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":16384,"end_us":10445,"operation":"read","slot":1,"start_us":10445},{"end_us":10467,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10461},{"abi_enter_us":10485,"abi_return_us":501227,"end_us":501244,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":10484,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":501352,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501352}],"schema":2,"slots":[{"at_us":501352,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":10467,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd339b5a93a0-622f-d3","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":500870,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":5701,"slot":1,"token":"18e1fd33b9babed8-622f-d4"},{"path":"/readiness/mixed-body-head/0","received_us":5715,"slot":0,"token":"18e1fd33b9babed8-622f-d4"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd33b9babed8-622f-d4","watchdog":false,"workers_joined":2,"writes":[{"at_us":5767,"bytes":57,"kind":"head","slot":0},{"at_us":62035,"bytes":61,"kind":"head","slot":1},{"at_us":62102,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":65,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":174,"operation":"dispatch","outcome":"pending","slot":1,"start_us":140},{"abi_enter_us":250,"abi_return_us":5533,"end_us":5555,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":250,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5649,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5631},{"abi_enter_us":5664,"abi_return_us":61823,"end_us":61834,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":5664,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":61947,"operation":"head-poll","outcome":"ready","slot":1,"start_us":61934},{"abi_enter_us":61978,"abi_return_us":61986,"end_us":61987,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":61978,"status":"FastlyStatus::OK","timeout_ms":438},{"bytes":8131,"capacity":16384,"end_us":62063,"operation":"read","slot":1,"start_us":62058},{"abi_enter_us":62123,"abi_return_us":62129,"end_us":62130,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":62123,"status":"FastlyStatus::OK","timeout_ms":437},{"bytes":16384,"capacity":16384,"end_us":62234,"operation":"read","slot":1,"start_us":62227},{"abi_enter_us":62308,"abi_return_us":62316,"end_us":62317,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":62307,"status":"FastlyStatus::OK","timeout_ms":437},{"bytes":8254,"capacity":16384,"end_us":62352,"operation":"read","slot":1,"start_us":62351},{"abi_enter_us":62374,"abi_return_us":62377,"end_us":62378,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":62374,"status":"FastlyStatus::OK","timeout_ms":437},{"bytes":0,"capacity":16384,"end_us":62400,"operation":"read","slot":1,"start_us":62399},{"end_us":62418,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":62412},{"abi_enter_us":62435,"abi_return_us":500349,"end_us":500366,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":62434,"status":"FastlyStatus::OK","timeout_ms":437},{"end_us":500819,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500800}],"schema":2,"slots":[{"at_us":500819,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":62418,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd33b9babed8-622f-d4","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":501505,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":6739,"slot":0,"token":"18e1fd33d807a860-622f-d5"},{"path":"/readiness/mixed-body-head/1","received_us":6751,"slot":1,"token":"18e1fd33d807a860-622f-d5"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd33d807a860-622f-d5","watchdog":false,"workers_joined":2,"writes":[{"at_us":6762,"bytes":57,"kind":"head","slot":0},{"at_us":63711,"bytes":61,"kind":"head","slot":1},{"at_us":63729,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":120,"operation":"dispatch","outcome":"pending","slot":0,"start_us":43},{"end_us":374,"operation":"dispatch","outcome":"pending","slot":1,"start_us":320},{"body_reads":0,"deadline_us":500000,"end_us":432,"operation":"handoff","start_us":432},{"abi_enter_us":464,"abi_return_us":5449,"end_us":5457,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":464,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5522,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5511},{"abi_enter_us":5538,"abi_return_us":63790,"end_us":63795,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":5538,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":63864,"operation":"head-poll","outcome":"ready","slot":1,"start_us":63857},{"abi_enter_us":63881,"abi_return_us":63885,"end_us":63886,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":63881,"status":"FastlyStatus::OK","timeout_ms":436},{"bytes":8131,"capacity":16384,"end_us":63929,"operation":"read","slot":1,"start_us":63926},{"abi_enter_us":63951,"abi_return_us":63954,"end_us":63954,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":63951,"status":"FastlyStatus::OK","timeout_ms":436},{"bytes":16384,"capacity":16384,"end_us":63979,"operation":"read","slot":1,"start_us":63978},{"abi_enter_us":64060,"abi_return_us":67055,"end_us":67058,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64060,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":8254,"capacity":16384,"end_us":67140,"operation":"read","slot":1,"start_us":67134},{"abi_enter_us":67172,"abi_return_us":67196,"end_us":67198,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":67172,"status":"FastlyStatus::OK","timeout_ms":432},{"bytes":0,"capacity":16384,"end_us":67788,"operation":"read","slot":1,"start_us":67786},{"end_us":67809,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":67803},{"abi_enter_us":67834,"abi_return_us":501378,"end_us":501394,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":67834,"status":"FastlyStatus::OK","timeout_ms":432},{"end_us":501458,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501450}],"schema":2,"slots":[{"at_us":501458,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":67809,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd33d807a860-622f-d5","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"full-body-error","completed_us":56335,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5331,"slot":0,"token":"18e1fd33f6435ba8-622f-d6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd33f6435ba8-622f-d6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5350,"bytes":61,"kind":"head","slot":0},{"at_us":5363,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"abi_enter_us":106,"abi_return_us":5048,"end_us":5056,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":105,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5137,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5129},{"abi_enter_us":5152,"abi_return_us":5159,"end_us":5159,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5152,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5190,"operation":"read","slot":0,"start_us":5187},{"abi_enter_us":5218,"abi_return_us":5220,"end_us":5220,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5218,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5245,"operation":"read","slot":0,"start_us":5244},{"abi_enter_us":5291,"abi_return_us":5295,"end_us":5296,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5291,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5340,"operation":"read","slot":0,"start_us":5337},{"abi_enter_us":5375,"abi_return_us":56069,"end_us":56077,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5375,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56276,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56151},{"end_us":56317,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56310}],"schema":2,"slots":[{"at_us":56317,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd33f6435ba8-622f-d6","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"full-body-error","completed_us":56604,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5205,"slot":0,"token":"18e1fd33f9d55ff0-622f-d7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd33f9d55ff0-622f-d7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5222,"bytes":61,"kind":"head","slot":0},{"at_us":5235,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":90,"operation":"handoff","start_us":90},{"abi_enter_us":132,"abi_return_us":4954,"end_us":4960,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":132,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5013,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5007},{"abi_enter_us":5029,"abi_return_us":5034,"end_us":5034,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5029,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5064,"operation":"read","slot":0,"start_us":5062},{"abi_enter_us":5106,"abi_return_us":5109,"end_us":5109,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5106,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5149,"operation":"read","slot":0,"start_us":5147},{"abi_enter_us":5186,"abi_return_us":5188,"end_us":5189,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5185,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5232,"operation":"read","slot":0,"start_us":5231},{"abi_enter_us":5252,"abi_return_us":56467,"end_us":56471,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5252,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56539,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56534},{"end_us":56579,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56571}],"schema":2,"slots":[{"at_us":56579,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd33f9d55ff0-622f-d7","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":502007,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":5344,"slot":0,"token":"18e1fd33fee6b520-622f-d8"},{"path":"/readiness/mixed-head-body/1","received_us":5980,"slot":1,"token":"18e1fd33fee6b520-622f-d8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd33fee6b520-622f-d8","watchdog":false,"workers_joined":2,"writes":[{"at_us":5993,"bytes":61,"kind":"head","slot":1},{"at_us":6006,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":306,"operation":"dispatch","outcome":"pending","slot":0,"start_us":29},{"end_us":436,"operation":"dispatch","outcome":"pending","slot":1,"start_us":390},{"abi_enter_us":486,"abi_return_us":3912,"end_us":3917,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":486,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":3968,"operation":"head-poll","outcome":"ready","slot":1,"start_us":3959},{"abi_enter_us":3983,"abi_return_us":3988,"end_us":3988,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":3983,"status":"FastlyStatus::OK","timeout_ms":496},{"bytes":8131,"capacity":16384,"end_us":4130,"operation":"read","slot":1,"start_us":4126},{"abi_enter_us":4170,"abi_return_us":4173,"end_us":4173,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":4170,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":16384,"capacity":16384,"end_us":4221,"operation":"read","slot":1,"start_us":4208},{"abi_enter_us":4288,"abi_return_us":4290,"end_us":4290,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":4288,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":8254,"capacity":16384,"end_us":4319,"operation":"read","slot":1,"start_us":4317},{"abi_enter_us":4430,"abi_return_us":4435,"end_us":4436,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":4430,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":16384,"end_us":4639,"operation":"read","slot":1,"start_us":4638},{"end_us":4669,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":4655},{"abi_enter_us":4713,"abi_return_us":501851,"end_us":501881,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":4712,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501981,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501981}],"schema":2,"slots":[{"at_us":501981,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":4669,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd33fee6b520-622f-d8","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":518771,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":5641,"slot":1,"token":"18e1fd341d2f8bb0-622f-d9"},{"path":"/readiness/mixed-head-body/0","received_us":5643,"slot":0,"token":"18e1fd341d2f8bb0-622f-d9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd341d2f8bb0-622f-d9","watchdog":false,"workers_joined":2,"writes":[{"at_us":5664,"bytes":61,"kind":"head","slot":1},{"at_us":5679,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":143,"operation":"dispatch","outcome":"pending","slot":1,"start_us":121},{"body_reads":0,"deadline_us":500000,"end_us":157,"operation":"handoff","start_us":157},{"abi_enter_us":169,"abi_return_us":5243,"end_us":5249,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":168,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5301,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5293},{"abi_enter_us":5320,"abi_return_us":5325,"end_us":5326,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5320,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5354,"operation":"read","slot":1,"start_us":5351},{"abi_enter_us":5392,"abi_return_us":5402,"end_us":5403,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5392,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5478,"operation":"read","slot":1,"start_us":5475},{"abi_enter_us":5511,"abi_return_us":5515,"end_us":5516,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5511,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5542,"operation":"read","slot":1,"start_us":5541},{"abi_enter_us":5561,"abi_return_us":5563,"end_us":5563,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5561,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5588,"operation":"read","slot":1,"start_us":5588},{"end_us":5603,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5598},{"abi_enter_us":5618,"abi_return_us":500715,"end_us":500724,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":5618,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500778,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500778}],"schema":2,"slots":[{"at_us":500778,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":5603,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd341d2f8bb0-622f-d9","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":501280,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":6492,"slot":0,"token":"18e1fd343c5c3290-622f-da"},{"path":"/readiness/mixed-body-head/1","received_us":6504,"slot":1,"token":"18e1fd343c5c3290-622f-da"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd343c5c3290-622f-da","watchdog":false,"workers_joined":2,"writes":[{"at_us":6512,"bytes":57,"kind":"head","slot":0},{"at_us":67336,"bytes":61,"kind":"head","slot":1},{"at_us":67352,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":110,"operation":"dispatch","outcome":"pending","slot":1,"start_us":97},{"abi_enter_us":140,"abi_return_us":6188,"end_us":6195,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":140,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6240,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6230},{"abi_enter_us":6253,"abi_return_us":67059,"end_us":67062,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":6253,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":67122,"operation":"head-poll","outcome":"ready","slot":1,"start_us":67116},{"abi_enter_us":67136,"abi_return_us":67140,"end_us":67141,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":67136,"status":"FastlyStatus::OK","timeout_ms":432},{"bytes":8131,"capacity":16384,"end_us":67176,"operation":"read","slot":1,"start_us":67173},{"abi_enter_us":67199,"abi_return_us":67201,"end_us":67201,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":67199,"status":"FastlyStatus::OK","timeout_ms":432},{"bytes":16384,"capacity":16384,"end_us":67224,"operation":"read","slot":1,"start_us":67223},{"abi_enter_us":67304,"abi_return_us":67311,"end_us":67312,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":67304,"status":"FastlyStatus::OK","timeout_ms":432},{"bytes":8254,"capacity":16384,"end_us":67346,"operation":"read","slot":1,"start_us":67343},{"abi_enter_us":67369,"abi_return_us":67372,"end_us":67372,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":67369,"status":"FastlyStatus::OK","timeout_ms":432},{"bytes":0,"capacity":16384,"end_us":67395,"operation":"read","slot":1,"start_us":67395},{"end_us":67409,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":67404},{"abi_enter_us":67423,"abi_return_us":501175,"end_us":501181,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":67423,"status":"FastlyStatus::OK","timeout_ms":432},{"end_us":501262,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501249}],"schema":2,"slots":[{"at_us":501262,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":67409,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd343c5c3290-622f-da","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":501805,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":5716,"slot":0,"token":"18e1fd345a79b360-622f-db"},{"path":"/readiness/mixed-body-head/1","received_us":5726,"slot":1,"token":"18e1fd345a79b360-622f-db"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd345a79b360-622f-db","watchdog":false,"workers_joined":2,"writes":[{"at_us":5742,"bytes":57,"kind":"head","slot":0},{"at_us":64513,"bytes":61,"kind":"head","slot":1},{"at_us":64530,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":115,"operation":"dispatch","outcome":"pending","slot":1,"start_us":102},{"body_reads":0,"deadline_us":500000,"end_us":130,"operation":"handoff","start_us":130},{"abi_enter_us":142,"abi_return_us":5439,"end_us":5444,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":142,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5483,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5477},{"abi_enter_us":5496,"abi_return_us":64286,"end_us":64293,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":5496,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":64353,"operation":"head-poll","outcome":"ready","slot":1,"start_us":64345},{"abi_enter_us":64368,"abi_return_us":64373,"end_us":64373,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64367,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":8131,"capacity":16384,"end_us":64402,"operation":"read","slot":1,"start_us":64399},{"abi_enter_us":64434,"abi_return_us":64438,"end_us":64439,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64434,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":16384,"capacity":16384,"end_us":64477,"operation":"read","slot":1,"start_us":64476},{"abi_enter_us":64522,"abi_return_us":64526,"end_us":64527,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64522,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":8254,"capacity":16384,"end_us":64558,"operation":"read","slot":1,"start_us":64556},{"abi_enter_us":64577,"abi_return_us":64580,"end_us":64580,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64577,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":0,"capacity":16384,"end_us":64601,"operation":"read","slot":1,"start_us":64601},{"end_us":64617,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":64610},{"abi_enter_us":64631,"abi_return_us":501687,"end_us":501697,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":64631,"status":"FastlyStatus::OK","timeout_ms":435},{"end_us":501776,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501755}],"schema":2,"slots":[{"at_us":501776,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":64617,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd345a79b360-622f-db","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"full-body-error","completed_us":59874,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6055,"slot":0,"token":"18e1fd34789c9718-622f-dc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd34789c9718-622f-dc","watchdog":false,"workers_joined":1,"writes":[{"at_us":6072,"bytes":61,"kind":"head","slot":0},{"at_us":6084,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":39},{"abi_enter_us":103,"abi_return_us":5793,"end_us":5797,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5838,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5832},{"abi_enter_us":5857,"abi_return_us":5863,"end_us":5863,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5857,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5892,"operation":"read","slot":0,"start_us":5889},{"abi_enter_us":5927,"abi_return_us":5931,"end_us":5931,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5927,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5967,"operation":"read","slot":0,"start_us":5966},{"abi_enter_us":6017,"abi_return_us":6019,"end_us":6019,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6017,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6052,"operation":"read","slot":0,"start_us":6049},{"abi_enter_us":6081,"abi_return_us":59731,"end_us":59739,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6081,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":59829,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59820},{"end_us":59855,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59848}],"schema":2,"slots":[{"at_us":59855,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd34789c9718-622f-dc","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"full-body-error","completed_us":57812,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5922,"slot":0,"token":"18e1fd347c5ca870-622f-dd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd347c5ca870-622f-dd","watchdog":false,"workers_joined":1,"writes":[{"at_us":5939,"bytes":61,"kind":"head","slot":0},{"at_us":5954,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":96,"operation":"handoff","start_us":96},{"abi_enter_us":124,"abi_return_us":5641,"end_us":5648,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":124,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5705,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5699},{"abi_enter_us":5724,"abi_return_us":5730,"end_us":5730,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5723,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5761,"operation":"read","slot":0,"start_us":5758},{"abi_enter_us":5781,"abi_return_us":5793,"end_us":5793,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5781,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5824,"operation":"read","slot":0,"start_us":5823},{"abi_enter_us":5853,"abi_return_us":5854,"end_us":5855,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5853,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5877,"operation":"read","slot":0,"start_us":5876},{"abi_enter_us":5894,"abi_return_us":57683,"end_us":57692,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5894,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57774,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57769},{"end_us":57797,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57791}],"schema":2,"slots":[{"at_us":57797,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd347c5ca870-622f-dd","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":501668,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":6098,"slot":1,"token":"18e1fd3480099b40-622f-de"},{"path":"/readiness/mixed-head-body/0","received_us":6208,"slot":0,"token":"18e1fd3480099b40-622f-de"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd3480099b40-622f-de","watchdog":false,"workers_joined":2,"writes":[{"at_us":6117,"bytes":61,"kind":"head","slot":1},{"at_us":6135,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":81,"operation":"dispatch","outcome":"pending","slot":0,"start_us":35},{"end_us":273,"operation":"dispatch","outcome":"pending","slot":1,"start_us":210},{"abi_enter_us":312,"abi_return_us":5257,"end_us":5262,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":311,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5319,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5313},{"abi_enter_us":5333,"abi_return_us":5337,"end_us":5338,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5333,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5490,"operation":"read","slot":1,"start_us":5487},{"abi_enter_us":5837,"abi_return_us":5841,"end_us":5842,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5837,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5913,"operation":"read","slot":1,"start_us":5911},{"abi_enter_us":6395,"abi_return_us":6404,"end_us":6405,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6395,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6462,"operation":"read","slot":1,"start_us":6460},{"abi_enter_us":6650,"abi_return_us":6653,"end_us":6654,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6650,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6717,"operation":"read","slot":1,"start_us":6716},{"end_us":6742,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6737},{"abi_enter_us":6758,"abi_return_us":501568,"end_us":501576,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":6758,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501647,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501647}],"schema":2,"slots":[{"at_us":501647,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":6742,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3480099b40-622f-de","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":502013,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":6487,"slot":1,"token":"18e1fd349e676518-622f-df"},{"path":"/readiness/mixed-head-body/0","received_us":6499,"slot":0,"token":"18e1fd349e676518-622f-df"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd349e676518-622f-df","watchdog":false,"workers_joined":2,"writes":[{"at_us":6507,"bytes":61,"kind":"head","slot":1},{"at_us":6521,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":142,"operation":"dispatch","outcome":"pending","slot":1,"start_us":105},{"body_reads":0,"deadline_us":500000,"end_us":161,"operation":"handoff","start_us":161},{"abi_enter_us":174,"abi_return_us":6182,"end_us":6187,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":174,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6228,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6221},{"abi_enter_us":6242,"abi_return_us":6255,"end_us":6255,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6242,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6283,"operation":"read","slot":1,"start_us":6280},{"abi_enter_us":6303,"abi_return_us":6305,"end_us":6306,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6303,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6353,"operation":"read","slot":1,"start_us":6351},{"abi_enter_us":6387,"abi_return_us":6391,"end_us":6392,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6387,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6424,"operation":"read","slot":1,"start_us":6423},{"abi_enter_us":6442,"abi_return_us":6446,"end_us":6446,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6442,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6471,"operation":"read","slot":1,"start_us":6471},{"end_us":6483,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6480},{"abi_enter_us":6496,"abi_return_us":501875,"end_us":501891,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":6496,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501985,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501985}],"schema":2,"slots":[{"at_us":501985,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":6483,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd349e676518-622f-df","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":500682,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":6195,"slot":1,"token":"18e1fd34bc905b80-622f-e0"},{"path":"/readiness/mixed-body-head/0","received_us":6202,"slot":0,"token":"18e1fd34bc905b80-622f-e0"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd34bc905b80-622f-e0","watchdog":false,"workers_joined":2,"writes":[{"at_us":6216,"bytes":57,"kind":"head","slot":0},{"at_us":64970,"bytes":61,"kind":"head","slot":1},{"at_us":64986,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":129,"operation":"dispatch","outcome":"pending","slot":1,"start_us":114},{"abi_enter_us":143,"abi_return_us":5795,"end_us":5801,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":143,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5852,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5845},{"abi_enter_us":5864,"abi_return_us":64595,"end_us":64602,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":5864,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":64701,"operation":"head-poll","outcome":"ready","slot":1,"start_us":64693},{"abi_enter_us":64732,"abi_return_us":64741,"end_us":64742,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64732,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":8131,"capacity":16384,"end_us":64805,"operation":"read","slot":1,"start_us":64802},{"abi_enter_us":64834,"abi_return_us":64837,"end_us":64838,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64834,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":16384,"capacity":16384,"end_us":64868,"operation":"read","slot":1,"start_us":64867},{"abi_enter_us":64930,"abi_return_us":64933,"end_us":64933,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64930,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":8254,"capacity":16384,"end_us":64971,"operation":"read","slot":1,"start_us":64969},{"abi_enter_us":64996,"abi_return_us":64999,"end_us":64999,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64996,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":0,"capacity":16384,"end_us":65025,"operation":"read","slot":1,"start_us":65025},{"end_us":65040,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":65035},{"abi_enter_us":65056,"abi_return_us":500563,"end_us":500574,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":65056,"status":"FastlyStatus::OK","timeout_ms":434},{"end_us":500661,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500651}],"schema":2,"slots":[{"at_us":500661,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":65040,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd34bc905b80-622f-e0","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":502145,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":6236,"slot":0,"token":"18e1fd34daa8e6c8-622f-e1"},{"path":"/readiness/mixed-body-head/1","received_us":6247,"slot":1,"token":"18e1fd34daa8e6c8-622f-e1"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd34daa8e6c8-622f-e1","watchdog":false,"workers_joined":2,"writes":[{"at_us":6256,"bytes":57,"kind":"head","slot":0},{"at_us":64427,"bytes":61,"kind":"head","slot":1},{"at_us":64441,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":134,"operation":"dispatch","outcome":"pending","slot":1,"start_us":109},{"body_reads":0,"deadline_us":500000,"end_us":164,"operation":"handoff","start_us":164},{"abi_enter_us":185,"abi_return_us":5901,"end_us":5909,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":185,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5968,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5961},{"abi_enter_us":5987,"abi_return_us":64094,"end_us":64100,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":5987,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":64164,"operation":"head-poll","outcome":"ready","slot":1,"start_us":64158},{"abi_enter_us":64180,"abi_return_us":64184,"end_us":64185,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64180,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":8131,"capacity":16384,"end_us":64216,"operation":"read","slot":1,"start_us":64213},{"abi_enter_us":64237,"abi_return_us":64255,"end_us":64256,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64236,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":16384,"capacity":16384,"end_us":64301,"operation":"read","slot":1,"start_us":64299},{"abi_enter_us":64336,"abi_return_us":64340,"end_us":64340,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64336,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":8254,"capacity":16384,"end_us":64368,"operation":"read","slot":1,"start_us":64367},{"abi_enter_us":64394,"abi_return_us":64397,"end_us":64397,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64393,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":0,"capacity":16384,"end_us":64421,"operation":"read","slot":1,"start_us":64421},{"end_us":64458,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":64450},{"abi_enter_us":64477,"abi_return_us":500869,"end_us":500879,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":64477,"status":"FastlyStatus::OK","timeout_ms":435},{"end_us":502102,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502092}],"schema":2,"slots":[{"at_us":502102,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":64458,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd34daa8e6c8-622f-e1","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"full-body-error","completed_us":60769,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6494,"slot":0,"token":"18e1fd34f8e40988-622f-e2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd34f8e40988-622f-e2","watchdog":false,"workers_joined":1,"writes":[{"at_us":6513,"bytes":61,"kind":"head","slot":0},{"at_us":6526,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"abi_enter_us":123,"abi_return_us":6182,"end_us":6190,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":122,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6247,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6240},{"abi_enter_us":6265,"abi_return_us":6271,"end_us":6272,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6265,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6302,"operation":"read","slot":0,"start_us":6299},{"abi_enter_us":6342,"abi_return_us":6346,"end_us":6347,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6342,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6479,"operation":"read","slot":0,"start_us":6471},{"abi_enter_us":6523,"abi_return_us":6529,"end_us":6530,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6523,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6575,"operation":"read","slot":0,"start_us":6574},{"abi_enter_us":6601,"abi_return_us":60618,"end_us":60623,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6601,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":60717,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":60708},{"end_us":60747,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60740}],"schema":2,"slots":[{"at_us":60747,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd34f8e40988-622f-e2","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"body-probe","probe":{"case":"full-body-error","completed_us":64726,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5348,"slot":0,"token":"18e1fd34fce23d20-622f-e3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd34fce23d20-622f-e3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5367,"bytes":61,"kind":"head","slot":0},{"at_us":5382,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":94,"operation":"handoff","start_us":94},{"abi_enter_us":120,"abi_return_us":5074,"end_us":5079,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":120,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5135,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5129},{"abi_enter_us":5152,"abi_return_us":5157,"end_us":5158,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5152,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5213,"operation":"read","slot":0,"start_us":5210},{"abi_enter_us":5244,"abi_return_us":5248,"end_us":5249,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5244,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5283,"operation":"read","slot":0,"start_us":5282},{"abi_enter_us":5326,"abi_return_us":5329,"end_us":5329,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5325,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5374,"operation":"read","slot":0,"start_us":5372},{"abi_enter_us":5404,"abi_return_us":58724,"end_us":58729,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5404,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":64667,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":64661},{"end_us":64702,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":64689}],"schema":2,"slots":[{"at_us":64702,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd34fce23d20-622f-e3","tolerance_us":5000}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1019,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":19,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2884349208-622f-3","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2152,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2152,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1152,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd28844971c8-622f-4","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2288,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1288,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2884784610-622f-5","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102829,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2829,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2884a62058-622f-6","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101592,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101592,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1592,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd288ad4ad28-622f-7","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102100,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2100,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2890f60328-622f-8","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":505010,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":5010,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd28971ec780-622f-9","within_five_ms":false}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502252,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502252,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2252,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd28b542e3b8-622f-a","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501772,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1772,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd28d3433bb0-622f-b","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1261,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":261,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd28f14bc168-622f-c","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":3759,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":3759,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2759,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd28f16cc2a0-622f-d","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2509,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1509,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd28f1cbcac0-622f-e","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102308,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2308,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd28f22a9078-622f-f","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102025,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102025,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2025,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd28f85feb78-622f-10","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102513,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2513,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd28feb21578-622f-11","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500207,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":207,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2904edc1a8-622f-12","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501734,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501734,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1734,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2922c688b8-622f-13","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503308,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3308,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2940d849b8-622f-14","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1542,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":542,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd295f044978-622f-15","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":1,"abi_return_us":2905,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2905,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1905,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd295f60ec50-622f-16","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":3875,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2875,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2961b55018-622f-17","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":100653,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":653,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd296394e970-622f-18","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101936,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101936,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1936,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2969c5b090-622f-19","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101283,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1283,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd296ff5b848-622f-1a","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500061,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":61,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd29761299a8-622f-1b","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501984,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501984,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1984,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2993e9a368-622f-1c","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501338,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1338,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd29b1e9cc80-622f-1d","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1259,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":259,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd29cfde5508-622f-1e","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2283,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2283,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1283,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd29cff5bd38-622f-1f","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2284,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1284,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd29d027e128-622f-20","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102426,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2426,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd29d056f7d8-622f-21","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101681,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101681,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1681,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd29d678a3c8-622f-22","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102074,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2074,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd29dc98fbe0-622f-23","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503660,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3660,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd29e2bd0930-622f-24","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501529,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501529,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1529,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2a00ca2a98-622f-25","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500580,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":580,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2a1ec0e5a0-622f-26","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1257,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":257,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2a3ca7c9f8-622f-27","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2268,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2268,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1268,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2a3cbf8c00-622f-28","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2061,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1061,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2a3cef71b8-622f-29","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102677,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2677,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2a3d1cf610-622f-2a","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101689,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101689,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1689,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2a43426ea8-622f-2b","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101891,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1891,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2a4966d5d0-622f-2c","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502116,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2116,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2a4f88b870-622f-2d","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501133,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501133,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1133,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2a6d7d0e30-622f-2e","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501806,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1806,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2a8b6f77c0-622f-2f","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1258,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":258,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2aa96cf540-622f-30","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":7,"abi_return_us":2298,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2298,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1298,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2aa9869f90-622f-31","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2307,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1307,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2aa9be5930-622f-32","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101823,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1823,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2aa9f5c898-622f-33","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101042,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101042,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1042,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2ab00cf120-622f-34","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101341,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1341,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2ab6242178-622f-35","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501264,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1264,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2abc3f5cf8-622f-36","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502087,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502087,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2087,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2ada26cdf0-622f-37","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501762,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1762,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2af82a7d60-622f-38","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1255,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":255,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2b1622d290-622f-39","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2287,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2287,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1287,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2b163ae2b8-622f-3a","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2087,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1087,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2b166c52f8-622f-3b","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":104442,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4442,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2b169700c0-622f-3c","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101659,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101659,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1659,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2b1cd796c0-622f-3d","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101393,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1393,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2b22fa0218-622f-3e","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501954,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1954,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2b291433f8-622f-3f","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502173,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502173,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2173,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2b4707ce38-622f-40","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502101,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2101,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2b65083da0-622f-41","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1262,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":262,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2b83075d18-622f-42","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2318,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2318,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1318,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2b83211af0-622f-43","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2295,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1295,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2b83551b70-622f-44","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":100111,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":111,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2b838639a8-622f-45","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101458,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101458,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1458,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2b8984e0c0-622f-46","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102444,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2444,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2b8fad89c0-622f-47","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504656,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4656,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd2b95dc7450-622f-48","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":500944,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":500944,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":944,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd2bb3f89590-622f-49","within_five_ms":true}} +{"run":"repaired-before-formatting","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501605,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1605,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd2bd1e80950-622f-4a","within_five_ms":true}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":56236,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5409,"slot":0,"token":"18e1fd3f3381b6d0-73d1-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3f3381b6d0-73d1-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5428,"bytes":57,"kind":"head","slot":0},{"at_us":5432,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":1014,"operation":"dispatch","outcome":"pending","slot":0,"start_us":429},{"abi_enter_us":1052,"abi_return_us":4327,"end_us":4330,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":1052,"status":"FastlyStatus::OK","timeout_ms":498},{"end_us":4524,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4370},{"bytes":1,"capacity":1,"end_us":4564,"operation":"read","slot":0,"start_us":4561},{"capacity":1,"end_us":56167,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":4595},{"end_us":56220,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56215}],"schema":2,"slots":[{"at_us":56220,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3f3381b6d0-73d1-4b","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":55166,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5221,"slot":0,"token":"18e1fd3f37131ed8-73d1-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3f37131ed8-73d1-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5240,"bytes":57,"kind":"head","slot":0},{"at_us":5244,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":89,"abi_return_us":4943,"end_us":4948,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4997,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4993},{"abi_enter_us":5010,"abi_return_us":5016,"end_us":5016,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5010,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5049,"operation":"read","slot":0,"start_us":5047},{"abi_enter_us":5062,"abi_return_us":55046,"end_us":55050,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5062,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":55123,"operation":"read","slot":0,"start_us":55120},{"end_us":55151,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55146}],"schema":2,"slots":[{"at_us":55151,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3f37131ed8-73d1-4c","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":56154,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5265,"slot":0,"token":"18e1fd3f3a841630-73d1-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3f3a841630-73d1-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5285,"bytes":57,"kind":"head","slot":0},{"at_us":5290,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":78,"abi_return_us":5021,"end_us":5027,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5087,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5082},{"abi_enter_us":5101,"abi_return_us":5106,"end_us":5107,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5101,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5136,"operation":"read","slot":0,"start_us":5132},{"abi_enter_us":5149,"abi_return_us":56040,"end_us":56047,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5149,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":56113,"operation":"read","slot":0,"start_us":56110},{"end_us":56139,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56135}],"schema":2,"slots":[{"at_us":56139,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3f3a841630-73d1-4d","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":60319,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5594,"slot":0,"token":"18e1fd3f3e02d0f8-73d1-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3f3e02d0f8-73d1-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5606,"bytes":57,"kind":"head","slot":0},{"at_us":5611,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":85,"operation":"handoff","start_us":85},{"abi_enter_us":101,"abi_return_us":5350,"end_us":5354,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":101,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5392,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5387},{"abi_enter_us":5403,"abi_return_us":5408,"end_us":5408,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5403,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5429,"operation":"read","slot":0,"start_us":5427},{"abi_enter_us":5442,"abi_return_us":60199,"end_us":60203,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5442,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":60283,"operation":"read","slot":0,"start_us":60279},{"end_us":60303,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60298}],"schema":2,"slots":[{"at_us":60303,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3f3e02d0f8-73d1-4e","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":55991,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5258,"slot":0,"token":"18e1fd3f41be94c0-73d1-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd3f41be94c0-73d1-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5273,"bytes":57,"kind":"head","slot":0},{"at_us":5278,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"abi_enter_us":104,"abi_return_us":5029,"end_us":5034,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5074,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5070},{"abi_enter_us":5087,"abi_return_us":5091,"end_us":5091,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5087,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5121,"operation":"read","slot":0,"start_us":5119},{"abi_enter_us":5131,"abi_return_us":55864,"end_us":55870,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5131,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":55955,"operation":"read","slot":0,"start_us":55950},{"end_us":55970,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55967}],"schema":2,"slots":[{"at_us":55970,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3f41be94c0-73d1-4f","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501225,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5630,"slot":0,"token":"18e1fd3f45424510-73d1-50"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd3f45424510-73d1-50","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":76,"abi_return_us":501145,"end_us":501153,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501208,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501208}],"schema":2,"slots":[{"at_us":501208,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3f45424510-73d1-50","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501377,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6229,"slot":0,"token":"18e1fd3f633f60e8-73d1-51"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd3f633f60e8-73d1-51","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":79,"abi_return_us":501273,"end_us":501289,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501355,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501355}],"schema":2,"slots":[{"at_us":501355,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3f633f60e8-73d1-51","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":500403,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5627,"slot":0,"token":"18e1fd3f814ac500-73d1-52"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd3f814ac500-73d1-52","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":81,"operation":"handoff","start_us":81},{"abi_enter_us":107,"abi_return_us":500325,"end_us":500339,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500387,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500387}],"schema":2,"slots":[{"at_us":500387,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3f814ac500-73d1-52","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501369,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5218,"slot":0,"token":"18e1fd3f9f554a70-73d1-53"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd3f9f554a70-73d1-53","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":76,"operation":"handoff","start_us":76},{"abi_enter_us":90,"abi_return_us":501292,"end_us":501302,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":90,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501351,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501350}],"schema":2,"slots":[{"at_us":501351,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd3f9f554a70-73d1-53","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":500744,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5325,"slot":0,"token":"18e1fd3fbd5bf780-73d1-54"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd3fbd5bf780-73d1-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":5341,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":48,"abi_return_us":5095,"end_us":5098,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":48,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5145,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5141},{"abi_enter_us":5160,"abi_return_us":500648,"end_us":500654,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5160,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500722,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500715}],"schema":2,"slots":[{"at_us":500722,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3fbd5bf780-73d1-54","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":500903,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6452,"slot":0,"token":"18e1fd3fdb618f38-73d1-55"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd3fdb618f38-73d1-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":6468,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":74,"abi_return_us":6214,"end_us":6220,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6277,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6270},{"abi_enter_us":6291,"abi_return_us":500604,"end_us":500609,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6291,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500868,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500860}],"schema":2,"slots":[{"at_us":500868,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3fdb618f38-73d1-55","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":500052,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5251,"slot":0,"token":"18e1fd3ff95d7290-73d1-56"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd3ff95d7290-73d1-56","watchdog":false,"workers_joined":1,"writes":[{"at_us":5272,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":93,"operation":"handoff","start_us":93},{"abi_enter_us":127,"abi_return_us":4966,"end_us":4971,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":127,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5027,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5022},{"abi_enter_us":5053,"abi_return_us":499962,"end_us":499969,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5053,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500033,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500026}],"schema":2,"slots":[{"at_us":500033,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd3ff95d7290-73d1-56","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":501979,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5239,"slot":0,"token":"18e1fd40174e8078-73d1-57"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd40174e8078-73d1-57","watchdog":false,"workers_joined":1,"writes":[{"at_us":5259,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":85,"operation":"handoff","start_us":85},{"abi_enter_us":96,"abi_return_us":4987,"end_us":4993,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5046,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5041},{"abi_enter_us":5065,"abi_return_us":500939,"end_us":501018,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5065,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501872,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501658}],"schema":2,"slots":[{"at_us":501872,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd40174e8078-73d1-57","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500569,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5202,"slot":0,"token":"18e1fd4035840d88-73d1-58"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd4035840d88-73d1-58","watchdog":false,"workers_joined":1,"writes":[{"at_us":5212,"bytes":57,"kind":"head","slot":0},{"at_us":5218,"bytes":1,"kind":"body","slot":0},{"at_us":308448,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":71,"abi_return_us":4821,"end_us":4823,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4849,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4846},{"abi_enter_us":4861,"abi_return_us":4864,"end_us":4865,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4861,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4889,"operation":"read","slot":0,"start_us":4887},{"abi_enter_us":4900,"abi_return_us":308089,"end_us":308124,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4899,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":308189,"operation":"read","slot":0,"start_us":308186},{"abi_enter_us":308205,"abi_return_us":500485,"end_us":500489,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308205,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500550,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500544}],"schema":2,"slots":[{"at_us":500550,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4035840d88-73d1-58","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":501837,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5258,"slot":0,"token":"18e1fd40538e0e28-73d1-59"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd40538e0e28-73d1-59","watchdog":false,"workers_joined":1,"writes":[{"at_us":5272,"bytes":57,"kind":"head","slot":0},{"at_us":5278,"bytes":1,"kind":"body","slot":0},{"at_us":305804,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":65,"abi_return_us":5061,"end_us":5064,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5104,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5099},{"abi_enter_us":5117,"abi_return_us":5120,"end_us":5121,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5117,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5152,"operation":"read","slot":0,"start_us":5149},{"abi_enter_us":5165,"abi_return_us":305591,"end_us":305597,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5165,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":305676,"operation":"read","slot":0,"start_us":305673},{"abi_enter_us":305689,"abi_return_us":501741,"end_us":501745,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":305689,"status":"FastlyStatus::OK","timeout_ms":194},{"end_us":501807,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501801}],"schema":2,"slots":[{"at_us":501807,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd40538e0e28-73d1-59","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500521,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5609,"slot":0,"token":"18e1fd40719ff638-73d1-5a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd40719ff638-73d1-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5624,"bytes":57,"kind":"head","slot":0},{"at_us":5630,"bytes":1,"kind":"body","slot":0},{"at_us":308178,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":104,"abi_return_us":5363,"end_us":5367,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5407,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5403},{"abi_enter_us":5420,"abi_return_us":5425,"end_us":5425,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5420,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5453,"operation":"read","slot":0,"start_us":5451},{"abi_enter_us":5463,"abi_return_us":307906,"end_us":307912,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5463,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307996,"operation":"read","slot":0,"start_us":307990},{"abi_enter_us":308009,"abi_return_us":500437,"end_us":500441,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308009,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500500,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500494}],"schema":2,"slots":[{"at_us":500500,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd40719ff638-73d1-5a","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":501074,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6454,"slot":0,"token":"18e1fd408f9be160-73d1-5b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd408f9be160-73d1-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6474,"bytes":57,"kind":"head","slot":0},{"at_us":6484,"bytes":1,"kind":"body","slot":0},{"at_us":311487,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":82,"abi_return_us":6243,"end_us":6247,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6285,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6281},{"abi_enter_us":6302,"abi_return_us":6308,"end_us":6308,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6302,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6336,"operation":"read","slot":0,"start_us":6334},{"abi_enter_us":6347,"abi_return_us":311501,"end_us":311508,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6347,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":311586,"operation":"read","slot":0,"start_us":311583},{"abi_enter_us":311600,"abi_return_us":500938,"end_us":500947,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":311600,"status":"FastlyStatus::OK","timeout_ms":188},{"end_us":501035,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501005}],"schema":2,"slots":[{"at_us":501035,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd408f9be160-73d1-5b","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501776,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6473,"slot":0,"token":"18e1fd40adaaeef8-73d1-5c"},{"path":"/readiness/siblings/1","received_us":6481,"slot":1,"token":"18e1fd40adaaeef8-73d1-5c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd40adaaeef8-73d1-5c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6490,"bytes":57,"kind":"head","slot":0},{"at_us":6499,"bytes":57,"kind":"head","slot":1},{"at_us":6506,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":128,"operation":"dispatch","outcome":"pending","slot":1,"start_us":82},{"abi_enter_us":144,"abi_return_us":6195,"end_us":6199,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":144,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6236,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6232},{"abi_enter_us":6252,"abi_return_us":6481,"end_us":6482,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6252,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6505,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6504},{"abi_enter_us":6516,"abi_return_us":6518,"end_us":6519,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6515,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6541,"operation":"read","slot":1,"start_us":6538},{"abi_enter_us":6551,"abi_return_us":6552,"end_us":6553,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6551,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6582,"operation":"read","slot":1,"start_us":6576},{"abi_enter_us":6591,"abi_return_us":6592,"end_us":6593,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6591,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6610,"operation":"read","slot":1,"start_us":6610},{"abi_enter_us":6618,"abi_return_us":6620,"end_us":6622,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6618,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6644,"operation":"read","slot":1,"start_us":6643},{"end_us":6656,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6653},{"abi_enter_us":6667,"abi_return_us":501684,"end_us":501690,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6667,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501758,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501752}],"schema":2,"slots":[{"at_us":501758,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6656,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd40adaaeef8-73d1-5c","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501347,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5770,"slot":1,"token":"18e1fd40cbcd0b78-73d1-5d"},{"path":"/readiness/siblings/0","received_us":5778,"slot":0,"token":"18e1fd40cbcd0b78-73d1-5d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd40cbcd0b78-73d1-5d","watchdog":false,"workers_joined":2,"writes":[{"at_us":5789,"bytes":57,"kind":"head","slot":1},{"at_us":5798,"bytes":57,"kind":"head","slot":0},{"at_us":11449,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":93,"operation":"dispatch","outcome":"pending","slot":1,"start_us":79},{"abi_enter_us":115,"abi_return_us":5545,"end_us":5548,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":115,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5587,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5583},{"abi_enter_us":5601,"abi_return_us":5603,"end_us":5603,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5601,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5617,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5616},{"abi_enter_us":5626,"abi_return_us":11201,"end_us":11203,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5626,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11247,"operation":"read","slot":1,"start_us":11241},{"abi_enter_us":11259,"abi_return_us":11262,"end_us":11263,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11259,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11284,"operation":"read","slot":1,"start_us":11283},{"end_us":11296,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11293},{"abi_enter_us":11309,"abi_return_us":501239,"end_us":501245,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11309,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501313,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501308}],"schema":2,"slots":[{"at_us":501313,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11296,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd40cbcd0b78-73d1-5d","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501231,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6474,"slot":0,"token":"18e1fd40e9e03f90-73d1-5e"},{"path":"/readiness/siblings/1","received_us":6484,"slot":1,"token":"18e1fd40e9e03f90-73d1-5e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd40e9e03f90-73d1-5e","watchdog":false,"workers_joined":2,"writes":[{"at_us":6492,"bytes":57,"kind":"head","slot":0},{"at_us":6501,"bytes":57,"kind":"head","slot":1},{"at_us":6509,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"end_us":90,"operation":"dispatch","outcome":"pending","slot":1,"start_us":78},{"body_reads":0,"deadline_us":500000,"end_us":116,"operation":"handoff","start_us":116},{"abi_enter_us":134,"abi_return_us":6254,"end_us":6258,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":134,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6306,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6302},{"abi_enter_us":6320,"abi_return_us":6322,"end_us":6322,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6320,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6339,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6338},{"abi_enter_us":6348,"abi_return_us":6353,"end_us":6354,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6348,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6375,"operation":"read","slot":1,"start_us":6373},{"abi_enter_us":6385,"abi_return_us":6386,"end_us":6386,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6385,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6404,"operation":"read","slot":1,"start_us":6403},{"abi_enter_us":6416,"abi_return_us":6417,"end_us":6417,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6416,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6435,"operation":"read","slot":1,"start_us":6434},{"abi_enter_us":6443,"abi_return_us":6445,"end_us":6445,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6443,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6464,"operation":"read","slot":1,"start_us":6464},{"end_us":6475,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6472},{"abi_enter_us":6489,"abi_return_us":501106,"end_us":501120,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6489,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501209,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501193}],"schema":2,"slots":[{"at_us":501209,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6475,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd40e9e03f90-73d1-5e","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":500673,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":6521,"slot":1,"token":"18e1fd410816f198-73d1-5f"},{"path":"/readiness/siblings/0","received_us":6535,"slot":0,"token":"18e1fd410816f198-73d1-5f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd410816f198-73d1-5f","watchdog":false,"workers_joined":2,"writes":[{"at_us":6546,"bytes":57,"kind":"head","slot":0},{"at_us":6556,"bytes":57,"kind":"head","slot":1},{"at_us":6563,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":59,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":134,"operation":"dispatch","outcome":"pending","slot":1,"start_us":120},{"body_reads":0,"deadline_us":500000,"end_us":149,"operation":"handoff","start_us":149},{"abi_enter_us":163,"abi_return_us":6220,"end_us":6227,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":163,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6276,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6271},{"abi_enter_us":6289,"abi_return_us":6292,"end_us":6292,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6289,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6312,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6311},{"abi_enter_us":6323,"abi_return_us":6328,"end_us":6328,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6323,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6353,"operation":"read","slot":1,"start_us":6351},{"abi_enter_us":6366,"abi_return_us":6368,"end_us":6369,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6366,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6393,"operation":"read","slot":1,"start_us":6392},{"end_us":6404,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6401},{"abi_enter_us":6416,"abi_return_us":500591,"end_us":500596,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6416,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500656,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500650}],"schema":2,"slots":[{"at_us":500656,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6404,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd410816f198-73d1-5f","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":517183,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6443,"slot":0,"token":"18e1fd4126380090-73d1-60"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd4126380090-73d1-60","watchdog":false,"workers_joined":1,"writes":[{"at_us":6468,"bytes":57,"kind":"head","slot":0},{"at_us":6473,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":81,"abi_return_us":6215,"end_us":6220,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6258,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6254},{"abi_enter_us":6270,"abi_return_us":6275,"end_us":6275,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6269,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6311,"operation":"read","slot":0,"start_us":6309},{"abi_enter_us":6324,"abi_return_us":505909,"end_us":507181,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6324,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":517041,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":516998}],"schema":2,"slots":[{"at_us":517041,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4126380090-73d1-60","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501357,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6425,"slot":0,"token":"18e1fd414563c4e0-73d1-61"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd414563c4e0-73d1-61","watchdog":false,"workers_joined":1,"writes":[{"at_us":6445,"bytes":57,"kind":"head","slot":0},{"at_us":6452,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":666,"abi_return_us":4654,"end_us":4668,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":666,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4768,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4761},{"abi_enter_us":4802,"abi_return_us":4816,"end_us":4817,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4801,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5117,"operation":"read","slot":0,"start_us":5113},{"abi_enter_us":5150,"abi_return_us":501263,"end_us":501267,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5149,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501334,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501328}],"schema":2,"slots":[{"at_us":501334,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd414563c4e0-73d1-61","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501080,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6450,"slot":0,"token":"18e1fd41638e3e00-73d1-62"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd41638e3e00-73d1-62","watchdog":false,"workers_joined":1,"writes":[{"at_us":6468,"bytes":57,"kind":"head","slot":0},{"at_us":6476,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":90,"abi_return_us":6228,"end_us":6233,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6276,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6272},{"abi_enter_us":6291,"abi_return_us":6296,"end_us":6296,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6291,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6322,"operation":"read","slot":0,"start_us":6320},{"abi_enter_us":6333,"abi_return_us":500986,"end_us":500992,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6333,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501056,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501050}],"schema":2,"slots":[{"at_us":501056,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41638e3e00-73d1-62","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":500669,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6483,"slot":0,"token":"18e1fd418195c9b8-73d1-63"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd418195c9b8-73d1-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":6496,"bytes":57,"kind":"head","slot":0},{"at_us":6503,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":86,"abi_return_us":6207,"end_us":6210,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6250,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6245},{"abi_enter_us":6265,"abi_return_us":6271,"end_us":6272,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6265,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6305,"operation":"read","slot":0,"start_us":6303},{"abi_enter_us":6316,"abi_return_us":500578,"end_us":500585,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6316,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500650,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500644}],"schema":2,"slots":[{"at_us":500650,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd418195c9b8-73d1-63","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5811,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5719,"slot":0,"token":"18e1fd419f9ed058-73d1-64"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd419f9ed058-73d1-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":5741,"bytes":57,"kind":"head","slot":0},{"at_us":5748,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":59,"abi_return_us":5550,"end_us":5556,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5619,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5614},{"abi_enter_us":5635,"abi_return_us":5642,"end_us":5643,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5635,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5673,"operation":"read","slot":0,"start_us":5671},{"abi_enter_us":5686,"abi_return_us":5687,"end_us":5687,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5686,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5715,"operation":"read","slot":0,"start_us":5707},{"abi_enter_us":5730,"abi_return_us":5731,"end_us":5731,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5730,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5750,"operation":"read","slot":0,"start_us":5749},{"abi_enter_us":5760,"abi_return_us":5762,"end_us":5762,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5760,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5782,"operation":"read","slot":0,"start_us":5782},{"end_us":5799,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5795}],"schema":2,"slots":[{"at_us":5799,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd419f9ed058-73d1-64","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":8337,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":7771,"slot":0,"token":"18e1fd41a03b23e0-73d1-65"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a03b23e0-73d1-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":7787,"bytes":57,"kind":"head","slot":0},{"at_us":7798,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":69,"abi_return_us":8148,"end_us":8154,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":8213,"operation":"head-poll","outcome":"ready","slot":0,"start_us":8208},{"abi_enter_us":8229,"abi_return_us":8237,"end_us":8238,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":8229,"status":"FastlyStatus::OK","timeout_ms":491},{"bytes":3,"capacity":16384,"end_us":8271,"operation":"read","slot":0,"start_us":8268},{"abi_enter_us":8285,"abi_return_us":8287,"end_us":8288,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":8284,"status":"FastlyStatus::OK","timeout_ms":491},{"bytes":0,"capacity":16384,"end_us":8311,"operation":"read","slot":0,"start_us":8311},{"end_us":8324,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":8321}],"schema":2,"slots":[{"at_us":8324,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41a03b23e0-73d1-65","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":6414,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6416,"slot":0,"token":"18e1fd41a11d5238-73d1-66"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a11d5238-73d1-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":6432,"bytes":57,"kind":"head","slot":0},{"at_us":6436,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":82,"operation":"handoff","start_us":82},{"abi_enter_us":98,"abi_return_us":6171,"end_us":6176,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":97,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6214,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6209},{"abi_enter_us":6228,"abi_return_us":6233,"end_us":6233,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6228,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6264,"operation":"read","slot":0,"start_us":6262},{"abi_enter_us":6277,"abi_return_us":6277,"end_us":6277,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6276,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6306,"operation":"read","slot":0,"start_us":6305},{"abi_enter_us":6320,"abi_return_us":6321,"end_us":6321,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6320,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6352,"operation":"read","slot":0,"start_us":6352},{"abi_enter_us":6362,"abi_return_us":6363,"end_us":6363,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6361,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6387,"operation":"read","slot":0,"start_us":6387},{"end_us":6400,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6397}],"schema":2,"slots":[{"at_us":6400,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41a11d5238-73d1-66","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5813,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5813,"slot":0,"token":"18e1fd41a1b4bbf0-73d1-67"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a1b4bbf0-73d1-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":5829,"bytes":57,"kind":"head","slot":0},{"at_us":5835,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"abi_enter_us":89,"abi_return_us":5657,"end_us":5661,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5704,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5699},{"abi_enter_us":5718,"abi_return_us":5723,"end_us":5724,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5718,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5752,"operation":"read","slot":0,"start_us":5749},{"abi_enter_us":5763,"abi_return_us":5765,"end_us":5765,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5763,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5787,"operation":"read","slot":0,"start_us":5786},{"end_us":5799,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5795}],"schema":2,"slots":[{"at_us":5799,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41a1b4bbf0-73d1-67","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5039,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5535,"slot":0,"token":"18e1fd41a24d0c20-73d1-68"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a24d0c20-73d1-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":5550,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":52,"abi_return_us":4918,"end_us":4923,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":52,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4970,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4965},{"abi_enter_us":4984,"abi_return_us":4986,"end_us":4987,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4984,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":0,"capacity":1,"end_us":5013,"operation":"read","slot":0,"start_us":5010},{"end_us":5027,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5024}],"schema":2,"slots":[{"at_us":5027,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41a24d0c20-73d1-68","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6227,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6323,"slot":0,"token":"18e1fd41a2c0ffb8-73d1-69"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a2c0ffb8-73d1-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":6337,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":56,"abi_return_us":6136,"end_us":6138,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6165,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6160},{"abi_enter_us":6176,"abi_return_us":6178,"end_us":6178,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6176,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6201,"operation":"read","slot":0,"start_us":6200},{"end_us":6214,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6212}],"schema":2,"slots":[{"at_us":6214,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41a2c0ffb8-73d1-69","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6420,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6461,"slot":0,"token":"18e1fd41a33dffb8-73d1-6a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a33dffb8-73d1-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6479,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":45,"operation":"handoff","start_us":45},{"abi_enter_us":54,"abi_return_us":6321,"end_us":6326,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6359,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6355},{"abi_enter_us":6370,"abi_return_us":6373,"end_us":6373,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6370,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6395,"operation":"read","slot":0,"start_us":6392},{"end_us":6407,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6405}],"schema":2,"slots":[{"at_us":6407,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41a33dffb8-73d1-6a","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6409,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6456,"slot":0,"token":"18e1fd41a3bf4d48-73d1-6b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a3bf4d48-73d1-6b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6473,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"abi_enter_us":66,"abi_return_us":6305,"end_us":6309,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6342,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6338},{"abi_enter_us":6356,"abi_return_us":6359,"end_us":6359,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6356,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6383,"operation":"read","slot":0,"start_us":6381},{"end_us":6396,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6394}],"schema":2,"slots":[{"at_us":6396,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41a3bf4d48-73d1-6b","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5146,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5247,"slot":0,"token":"18e1fd41a44198c0-73d1-6c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a44198c0-73d1-6c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5260,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":63,"abi_return_us":5085,"end_us":5089,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5117,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5113},{"end_us":5133,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5133}],"schema":2,"slots":[{"at_us":5133,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd41a44198c0-73d1-6c","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5476,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5203,"slot":0,"token":"18e1fd41a4ad00d8-73d1-6d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a4ad00d8-73d1-6d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5223,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":54,"abi_return_us":5407,"end_us":5413,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5448,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5444},{"end_us":5462,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5462}],"schema":2,"slots":[{"at_us":5462,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd41a4ad00d8-73d1-6d","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5074,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5222,"slot":0,"token":"18e1fd41a523abc0-73d1-6e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a523abc0-73d1-6e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5237,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"abi_enter_us":78,"abi_return_us":5012,"end_us":5015,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5045,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5041},{"end_us":5058,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5058}],"schema":2,"slots":[{"at_us":5058,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd41a523abc0-73d1-6e","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5621,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5653,"slot":0,"token":"18e1fd41a591b7a0-73d1-6f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a591b7a0-73d1-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5670,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":27,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":46,"operation":"handoff","start_us":46},{"abi_enter_us":56,"abi_return_us":5513,"end_us":5518,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5552,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5547},{"end_us":5601,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5601}],"schema":2,"slots":[{"at_us":5601,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd41a591b7a0-73d1-6f","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58070,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5988,"slot":0,"token":"18e1fd41a60706e0-73d1-70"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a60706e0-73d1-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":6003,"bytes":66,"kind":"head","slot":0},{"at_us":6009,"bytes":6,"kind":"body","slot":0},{"at_us":58144,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":57,"abi_return_us":5801,"end_us":5805,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5848,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5844},{"abi_enter_us":5859,"abi_return_us":5865,"end_us":5865,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5859,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5890,"operation":"read","slot":0,"start_us":5888},{"abi_enter_us":5901,"abi_return_us":57972,"end_us":57978,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5901,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58037,"operation":"read","slot":0,"start_us":58033},{"end_us":58053,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58049}],"schema":2,"slots":[{"at_us":58053,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41a60706e0-73d1-70","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58639,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6265,"slot":0,"token":"18e1fd41a9ca0a20-73d1-71"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41a9ca0a20-73d1-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":6275,"bytes":66,"kind":"head","slot":0},{"at_us":6281,"bytes":6,"kind":"body","slot":0},{"at_us":58842,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":70,"abi_return_us":5983,"end_us":5988,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6033,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6029},{"abi_enter_us":6047,"abi_return_us":6053,"end_us":6053,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6047,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6084,"operation":"read","slot":0,"start_us":6082},{"abi_enter_us":6096,"abi_return_us":58543,"end_us":58547,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6096,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":58608,"operation":"read","slot":0,"start_us":58605},{"end_us":58623,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58620}],"schema":2,"slots":[{"at_us":58623,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41a9ca0a20-73d1-71","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57227,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5266,"slot":0,"token":"18e1fd41ad7727e8-73d1-72"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41ad7727e8-73d1-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":5282,"bytes":66,"kind":"head","slot":0},{"at_us":5289,"bytes":6,"kind":"body","slot":0},{"at_us":55871,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"abi_enter_us":81,"abi_return_us":5050,"end_us":5053,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5084,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5081},{"abi_enter_us":5099,"abi_return_us":5103,"end_us":5103,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5099,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5125,"operation":"read","slot":0,"start_us":5123},{"abi_enter_us":5135,"abi_return_us":57104,"end_us":57108,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5135,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":57184,"operation":"read","slot":0,"start_us":57180},{"end_us":57209,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57201}],"schema":2,"slots":[{"at_us":57209,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41ad7727e8-73d1-72","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":61332,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6491,"slot":0,"token":"18e1fd41b1145b78-73d1-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41b1145b78-73d1-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":6507,"bytes":66,"kind":"head","slot":0},{"at_us":6514,"bytes":6,"kind":"body","slot":0},{"at_us":61005,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":82,"operation":"handoff","start_us":82},{"abi_enter_us":95,"abi_return_us":6230,"end_us":6237,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6286,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6281},{"abi_enter_us":6301,"abi_return_us":6307,"end_us":6307,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6301,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6345,"operation":"read","slot":0,"start_us":6339},{"abi_enter_us":6359,"abi_return_us":60802,"end_us":60808,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6359,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":61289,"operation":"read","slot":0,"start_us":61283},{"end_us":61313,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":61308}],"schema":2,"slots":[{"at_us":61313,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41b1145b78-73d1-73","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5637,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6067,"slot":0,"token":"18e1fd41b52d1808-73d1-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41b52d1808-73d1-74","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":102,"abi_return_us":5388,"end_us":5396,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5554,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5547},{"end_us":5583,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5583}],"schema":2,"slots":[{"at_us":5583,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd41b52d1808-73d1-74","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5221,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5521,"slot":0,"token":"18e1fd41b5d7c758-73d1-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41b5d7c758-73d1-75","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":74,"operation":"dispatch","outcome":"pending","slot":0,"start_us":34},{"abi_enter_us":146,"abi_return_us":5139,"end_us":5146,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":145,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5189,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5185},{"end_us":5204,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5204}],"schema":2,"slots":[{"at_us":5204,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd41b5d7c758-73d1-75","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5786,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5943,"slot":0,"token":"18e1fd41b664a9c0-73d1-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41b664a9c0-73d1-76","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":85,"operation":"handoff","start_us":85},{"abi_enter_us":107,"abi_return_us":5717,"end_us":5722,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5755,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5750},{"end_us":5771,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5771}],"schema":2,"slots":[{"at_us":5771,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd41b664a9c0-73d1-76","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5679,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5849,"slot":0,"token":"18e1fd41b71e8f98-73d1-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41b71e8f98-73d1-77","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":86,"operation":"handoff","start_us":86},{"abi_enter_us":105,"abi_return_us":5602,"end_us":5608,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":105,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5643,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5638},{"end_us":5662,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5662}],"schema":2,"slots":[{"at_us":5662,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd41b71e8f98-73d1-77","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":58832,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6494,"slot":0,"token":"18e1fd41b7b05fb8-73d1-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41b7b05fb8-73d1-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":6516,"bytes":57,"kind":"head","slot":0},{"at_us":6524,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":90,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":142,"abi_return_us":6198,"end_us":6204,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":142,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6256,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6251},{"bytes":1,"capacity":1,"end_us":6294,"operation":"read","slot":0,"start_us":6288},{"capacity":1,"end_us":58755,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":6315},{"end_us":58815,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58809}],"schema":2,"slots":[{"at_us":58815,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41b7b05fb8-73d1-78","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":60380,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6436,"slot":0,"token":"18e1fd41bb55a1c8-73d1-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41bb55a1c8-73d1-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":6451,"bytes":57,"kind":"head","slot":0},{"at_us":6456,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":61,"abi_return_us":6248,"end_us":6252,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":60,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6294,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6288},{"abi_enter_us":6308,"abi_return_us":6313,"end_us":6313,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6308,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6337,"operation":"read","slot":0,"start_us":6335},{"abi_enter_us":6348,"abi_return_us":60261,"end_us":60269,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6348,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":60341,"operation":"read","slot":0,"start_us":60337},{"end_us":60363,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60359}],"schema":2,"slots":[{"at_us":60363,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41bb55a1c8-73d1-79","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":57882,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":7351,"slot":0,"token":"18e1fd41bf4a6b38-73d1-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41bf4a6b38-73d1-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":7369,"bytes":57,"kind":"head","slot":0},{"at_us":7376,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":70,"operation":"dispatch","outcome":"pending","slot":0,"start_us":42},{"abi_enter_us":114,"abi_return_us":5802,"end_us":5808,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":113,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5857,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5853},{"abi_enter_us":5871,"abi_return_us":5875,"end_us":5875,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5871,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5924,"operation":"read","slot":0,"start_us":5921},{"abi_enter_us":5938,"abi_return_us":57777,"end_us":57784,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5937,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":57849,"operation":"read","slot":0,"start_us":57846},{"end_us":57867,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57862}],"schema":2,"slots":[{"at_us":57867,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41bf4a6b38-73d1-7a","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":61956,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6526,"slot":0,"token":"18e1fd41c3024ed0-73d1-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41c3024ed0-73d1-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6545,"bytes":57,"kind":"head","slot":0},{"at_us":6552,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":65,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":155,"operation":"handoff","start_us":155},{"abi_enter_us":236,"abi_return_us":6274,"end_us":6293,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":236,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6334,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6329},{"abi_enter_us":6347,"abi_return_us":6355,"end_us":6355,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6346,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6380,"operation":"read","slot":0,"start_us":6378},{"abi_enter_us":6391,"abi_return_us":61415,"end_us":61422,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6391,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":61919,"operation":"read","slot":0,"start_us":61914},{"end_us":61938,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":61934}],"schema":2,"slots":[{"at_us":61938,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41c3024ed0-73d1-7b","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":55819,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5605,"slot":0,"token":"18e1fd41c6f62228-73d1-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd41c6f62228-73d1-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5620,"bytes":57,"kind":"head","slot":0},{"at_us":5627,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":67,"operation":"dispatch","outcome":"pending","slot":0,"start_us":42},{"body_reads":0,"deadline_us":500000,"end_us":156,"operation":"handoff","start_us":156},{"abi_enter_us":171,"abi_return_us":5228,"end_us":5235,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":170,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5283,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5278},{"abi_enter_us":5319,"abi_return_us":5327,"end_us":5329,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5318,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5382,"operation":"read","slot":0,"start_us":5379},{"abi_enter_us":5394,"abi_return_us":55627,"end_us":55654,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5394,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":55781,"operation":"read","slot":0,"start_us":55777},{"end_us":55798,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55794}],"schema":2,"slots":[{"at_us":55798,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd41c6f62228-73d1-7c","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501791,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5407,"slot":0,"token":"18e1fd41ca77ced8-73d1-7d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd41ca77ced8-73d1-7d","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":93,"abi_return_us":499416,"end_us":499467,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"abi_enter_us":499555,"abi_return_us":501738,"end_us":501739,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499555,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501771,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501771}],"schema":2,"slots":[{"at_us":501771,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd41ca77ced8-73d1-7d","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501719,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6464,"slot":0,"token":"18e1fd41e8852ec0-73d1-7e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd41e8852ec0-73d1-7e","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":68,"abi_return_us":499302,"end_us":499310,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"abi_enter_us":499406,"abi_return_us":501672,"end_us":501673,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":499405,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501701,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501701}],"schema":2,"slots":[{"at_us":501701,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd41e8852ec0-73d1-7e","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":500811,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6031,"slot":0,"token":"18e1fd42068d3b60-73d1-7f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd42068d3b60-73d1-7f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"abi_enter_us":71,"abi_return_us":500717,"end_us":500733,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500793,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500792}],"schema":2,"slots":[{"at_us":500793,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd42068d3b60-73d1-7f","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501063,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5753,"slot":0,"token":"18e1fd42248a0918-73d1-80"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd42248a0918-73d1-80","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":82,"abi_return_us":500932,"end_us":500944,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501034,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501034}],"schema":2,"slots":[{"at_us":501034,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd42248a0918-73d1-80","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":501824,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6326,"slot":0,"token":"18e1fd424313c8d8-73d1-81"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd424313c8d8-73d1-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":6345,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":106,"operation":"dispatch","outcome":"pending","slot":0,"start_us":32},{"abi_enter_us":180,"abi_return_us":4854,"end_us":4860,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":179,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4919,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4914},{"abi_enter_us":4931,"abi_return_us":501739,"end_us":501744,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4931,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501806,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501799}],"schema":2,"slots":[{"at_us":501806,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd424313c8d8-73d1-81","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":500344,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6470,"slot":0,"token":"18e1fd4261318440-73d1-82"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd4261318440-73d1-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":6488,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":86,"abi_return_us":6232,"end_us":6237,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6291,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6286},{"abi_enter_us":6303,"abi_return_us":500149,"end_us":500157,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6303,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500310,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500302}],"schema":2,"slots":[{"at_us":500310,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4261318440-73d1-82","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":502121,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5723,"slot":0,"token":"18e1fd427f311500-73d1-83"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd427f311500-73d1-83","watchdog":false,"workers_joined":1,"writes":[{"at_us":5741,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":93,"operation":"handoff","start_us":93},{"abi_enter_us":110,"abi_return_us":5486,"end_us":5492,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5554,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5549},{"abi_enter_us":5570,"abi_return_us":502031,"end_us":502036,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5570,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502102,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502096}],"schema":2,"slots":[{"at_us":502102,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd427f311500-73d1-83","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":501285,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6340,"slot":0,"token":"18e1fd429d572150-73d1-84"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd429d572150-73d1-84","watchdog":false,"workers_joined":1,"writes":[{"at_us":6355,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":86,"abi_return_us":6080,"end_us":6083,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6124,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6120},{"abi_enter_us":6138,"abi_return_us":501204,"end_us":501211,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6138,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501266,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501259}],"schema":2,"slots":[{"at_us":501266,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd429d572150-73d1-84","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":501775,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5320,"slot":0,"token":"18e1fd42bb584850-73d1-85"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd42bb584850-73d1-85","watchdog":false,"workers_joined":1,"writes":[{"at_us":5339,"bytes":57,"kind":"head","slot":0},{"at_us":5351,"bytes":1,"kind":"body","slot":0},{"at_us":309149,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":62,"abi_return_us":5134,"end_us":5138,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5180,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5175},{"abi_enter_us":5193,"abi_return_us":5198,"end_us":5198,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5193,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5222,"operation":"read","slot":0,"start_us":5220},{"abi_enter_us":5239,"abi_return_us":308964,"end_us":308970,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5239,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309043,"operation":"read","slot":0,"start_us":309040},{"abi_enter_us":309060,"abi_return_us":500165,"end_us":500169,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309060,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501744,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501734}],"schema":2,"slots":[{"at_us":501744,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd42bb584850-73d1-85","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500555,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6436,"slot":0,"token":"18e1fd42d971d180-73d1-86"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd42d971d180-73d1-86","watchdog":false,"workers_joined":1,"writes":[{"at_us":6448,"bytes":57,"kind":"head","slot":0},{"at_us":6460,"bytes":1,"kind":"body","slot":0},{"at_us":309612,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":70,"abi_return_us":6154,"end_us":6159,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6207,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6202},{"abi_enter_us":6224,"abi_return_us":6228,"end_us":6229,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6224,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6260,"operation":"read","slot":0,"start_us":6258},{"abi_enter_us":6272,"abi_return_us":309304,"end_us":309316,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6272,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309387,"operation":"read","slot":0,"start_us":309384},{"abi_enter_us":309404,"abi_return_us":500482,"end_us":500485,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309403,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500535,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500530}],"schema":2,"slots":[{"at_us":500535,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd42d971d180-73d1-86","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":502051,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5798,"slot":0,"token":"18e1fd42f7712b90-73d1-87"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd42f7712b90-73d1-87","watchdog":false,"workers_joined":1,"writes":[{"at_us":5832,"bytes":57,"kind":"head","slot":0},{"at_us":5840,"bytes":1,"kind":"body","slot":0},{"at_us":307974,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":96,"abi_return_us":5567,"end_us":5571,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5623,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5619},{"abi_enter_us":5637,"abi_return_us":5643,"end_us":5643,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5637,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5670,"operation":"read","slot":0,"start_us":5669},{"abi_enter_us":5682,"abi_return_us":307742,"end_us":307751,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5682,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307842,"operation":"read","slot":0,"start_us":307839},{"abi_enter_us":307861,"abi_return_us":501934,"end_us":501950,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307861,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":502023,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502013}],"schema":2,"slots":[{"at_us":502023,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd42f7712b90-73d1-87","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":501454,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5393,"slot":0,"token":"18e1fd43159105f0-73d1-88"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd43159105f0-73d1-88","watchdog":false,"workers_joined":1,"writes":[{"at_us":5407,"bytes":57,"kind":"head","slot":0},{"at_us":5412,"bytes":1,"kind":"body","slot":0},{"at_us":307850,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":69,"operation":"handoff","start_us":69},{"abi_enter_us":85,"abi_return_us":5161,"end_us":5163,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5194,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5188},{"abi_enter_us":5208,"abi_return_us":5212,"end_us":5212,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5208,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5249,"operation":"read","slot":0,"start_us":5246},{"abi_enter_us":5264,"abi_return_us":307619,"end_us":307627,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5264,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":307730,"operation":"read","slot":0,"start_us":307727},{"abi_enter_us":307746,"abi_return_us":501330,"end_us":501334,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307745,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":501432,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501425}],"schema":2,"slots":[{"at_us":501432,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd43159105f0-73d1-88","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":500425,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6519,"slot":0,"token":"18e1fd4333a52c38-73d1-89"},{"path":"/readiness/siblings/1","received_us":6609,"slot":1,"token":"18e1fd4333a52c38-73d1-89"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd4333a52c38-73d1-89","watchdog":false,"workers_joined":2,"writes":[{"at_us":6541,"bytes":57,"kind":"head","slot":0},{"at_us":6616,"bytes":57,"kind":"head","slot":1},{"at_us":6631,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":154,"operation":"dispatch","outcome":"pending","slot":1,"start_us":140},{"abi_enter_us":191,"abi_return_us":6127,"end_us":6133,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":191,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6194,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6189},{"abi_enter_us":6212,"abi_return_us":6215,"end_us":6215,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6212,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6232,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6231},{"abi_enter_us":6243,"abi_return_us":6246,"end_us":6247,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6243,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6273,"operation":"read","slot":1,"start_us":6271},{"abi_enter_us":6288,"abi_return_us":6289,"end_us":6290,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6288,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6321,"operation":"read","slot":1,"start_us":6311},{"abi_enter_us":6334,"abi_return_us":6335,"end_us":6335,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6334,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6361,"operation":"read","slot":1,"start_us":6361},{"abi_enter_us":6371,"abi_return_us":6373,"end_us":6374,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6371,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6396,"operation":"read","slot":1,"start_us":6395},{"end_us":6408,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6404},{"abi_enter_us":6461,"abi_return_us":500327,"end_us":500337,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6461,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500405,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500395}],"schema":2,"slots":[{"at_us":500405,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6408,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4333a52c38-73d1-89","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":502692,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5350,"slot":0,"token":"18e1fd4351d0e5a8-73d1-8a"},{"path":"/readiness/siblings/1","received_us":5356,"slot":1,"token":"18e1fd4351d0e5a8-73d1-8a"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd4351d0e5a8-73d1-8a","watchdog":false,"workers_joined":2,"writes":[{"at_us":5364,"bytes":57,"kind":"head","slot":0},{"at_us":5370,"bytes":57,"kind":"head","slot":1},{"at_us":5375,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":147,"operation":"dispatch","outcome":"pending","slot":1,"start_us":115},{"abi_enter_us":179,"abi_return_us":5031,"end_us":5036,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":179,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5084,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5080},{"abi_enter_us":5102,"abi_return_us":5104,"end_us":5104,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5101,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5124,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5123},{"abi_enter_us":5138,"abi_return_us":5141,"end_us":5142,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5138,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5174,"operation":"read","slot":1,"start_us":5172},{"abi_enter_us":5291,"abi_return_us":5294,"end_us":5294,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5291,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5330,"operation":"read","slot":1,"start_us":5329},{"end_us":5344,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5341},{"abi_enter_us":5358,"abi_return_us":501872,"end_us":501925,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5358,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502609,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502047}],"schema":2,"slots":[{"at_us":502609,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5344,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4351d0e5a8-73d1-8a","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":502066,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":7031,"slot":0,"token":"18e1fd43711c7698-73d1-8b"},{"path":"/readiness/siblings/1","received_us":7379,"slot":1,"token":"18e1fd43711c7698-73d1-8b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd43711c7698-73d1-8b","watchdog":false,"workers_joined":2,"writes":[{"at_us":7050,"bytes":57,"kind":"head","slot":0},{"at_us":7389,"bytes":57,"kind":"head","slot":1},{"at_us":7421,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":118,"operation":"dispatch","outcome":"pending","slot":0,"start_us":81},{"end_us":290,"operation":"dispatch","outcome":"pending","slot":1,"start_us":275},{"body_reads":0,"deadline_us":500000,"end_us":308,"operation":"handoff","start_us":308},{"abi_enter_us":321,"abi_return_us":3267,"end_us":3271,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":321,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":3319,"operation":"head-poll","outcome":"ready","slot":0,"start_us":3314},{"abi_enter_us":3333,"abi_return_us":3387,"end_us":3387,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":3333,"status":"FastlyStatus::OK","timeout_ms":496},{"end_us":3404,"operation":"head-poll","outcome":"ready","slot":1,"start_us":3403},{"abi_enter_us":3414,"abi_return_us":3417,"end_us":3417,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":3414,"status":"FastlyStatus::OK","timeout_ms":496},{"bytes":1,"capacity":1,"end_us":3441,"operation":"read","slot":1,"start_us":3438},{"abi_enter_us":3452,"abi_return_us":3453,"end_us":3453,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":3452,"status":"FastlyStatus::OK","timeout_ms":496},{"bytes":1,"capacity":1,"end_us":3471,"operation":"read","slot":1,"start_us":3471},{"abi_enter_us":3485,"abi_return_us":3485,"end_us":3486,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":3484,"status":"FastlyStatus::OK","timeout_ms":496},{"bytes":1,"capacity":1,"end_us":3504,"operation":"read","slot":1,"start_us":3503},{"abi_enter_us":3513,"abi_return_us":3516,"end_us":3516,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":3513,"status":"FastlyStatus::OK","timeout_ms":496},{"bytes":0,"capacity":1,"end_us":3537,"operation":"read","slot":1,"start_us":3536},{"end_us":3549,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":3545},{"abi_enter_us":3565,"abi_return_us":499872,"end_us":499883,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":3565,"status":"FastlyStatus::OK","timeout_ms":496},{"abi_enter_us":499937,"abi_return_us":502002,"end_us":502003,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":499937,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502038,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502024}],"schema":2,"slots":[{"at_us":502038,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":3549,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd43711c7698-73d1-8b","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":500804,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5996,"slot":1,"token":"18e1fd438f8e1a78-73d1-8c"},{"path":"/readiness/siblings/0","received_us":6002,"slot":0,"token":"18e1fd438f8e1a78-73d1-8c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd438f8e1a78-73d1-8c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6012,"bytes":57,"kind":"head","slot":1},{"at_us":6020,"bytes":57,"kind":"head","slot":0},{"at_us":12116,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":106,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":196,"operation":"dispatch","outcome":"pending","slot":1,"start_us":181},{"body_reads":0,"deadline_us":500000,"end_us":211,"operation":"handoff","start_us":211},{"abi_enter_us":223,"abi_return_us":5793,"end_us":5799,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":222,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5850,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5845},{"abi_enter_us":5863,"abi_return_us":5866,"end_us":5866,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5863,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5885,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5884},{"abi_enter_us":5895,"abi_return_us":11870,"end_us":11874,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5895,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":11935,"operation":"read","slot":1,"start_us":11933},{"abi_enter_us":11948,"abi_return_us":11952,"end_us":11953,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11948,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":11980,"operation":"read","slot":1,"start_us":11980},{"end_us":11994,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11990},{"abi_enter_us":12008,"abi_return_us":500685,"end_us":500697,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":12008,"status":"FastlyStatus::OK","timeout_ms":487},{"end_us":500785,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500773}],"schema":2,"slots":[{"at_us":500785,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11994,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd438f8e1a78-73d1-8c","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":503993,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5427,"slot":0,"token":"18e1fd43ad9dd3f0-73d1-8d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd43ad9dd3f0-73d1-8d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5442,"bytes":57,"kind":"head","slot":0},{"at_us":5447,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":71,"abi_return_us":5134,"end_us":5138,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5176,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5172},{"abi_enter_us":5190,"abi_return_us":5195,"end_us":5196,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5190,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5225,"operation":"read","slot":0,"start_us":5222},{"abi_enter_us":5235,"abi_return_us":501035,"end_us":502671,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5235,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":503709,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":503699}],"schema":2,"slots":[{"at_us":503709,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd43ad9dd3f0-73d1-8d","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":502206,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":26447,"slot":0,"token":"18e1fd43ccc3a0e8-73d1-8e"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd43ccc3a0e8-73d1-8e","watchdog":false,"workers_joined":1,"writes":[{"at_us":26512,"bytes":57,"kind":"head","slot":0},{"at_us":26526,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":316,"operation":"dispatch","outcome":"pending","slot":0,"start_us":32},{"abi_enter_us":365,"abi_return_us":22534,"end_us":23029,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":364,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":23879,"operation":"head-poll","outcome":"ready","slot":0,"start_us":23593},{"abi_enter_us":24452,"abi_return_us":24469,"end_us":24471,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":24451,"status":"FastlyStatus::OK","timeout_ms":475},{"bytes":1,"capacity":16384,"end_us":24653,"operation":"read","slot":0,"start_us":24650},{"abi_enter_us":24668,"abi_return_us":502099,"end_us":502105,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":24667,"status":"FastlyStatus::OK","timeout_ms":475},{"end_us":502183,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502172}],"schema":2,"slots":[{"at_us":502183,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd43ccc3a0e8-73d1-8e","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":500521,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6573,"slot":0,"token":"18e1fd43eb328d78-73d1-8f"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd43eb328d78-73d1-8f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6592,"bytes":57,"kind":"head","slot":0},{"at_us":6599,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":59,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":116,"operation":"handoff","start_us":116},{"abi_enter_us":131,"abi_return_us":6050,"end_us":6057,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":131,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6103,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6098},{"abi_enter_us":6116,"abi_return_us":6124,"end_us":6124,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6116,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6150,"operation":"read","slot":0,"start_us":6148},{"abi_enter_us":6166,"abi_return_us":500371,"end_us":500390,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6166,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500483,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500470}],"schema":2,"slots":[{"at_us":500483,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd43eb328d78-73d1-8f","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":500649,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6452,"slot":0,"token":"18e1fd440932e570-73d1-90"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd440932e570-73d1-90","watchdog":false,"workers_joined":1,"writes":[{"at_us":6467,"bytes":57,"kind":"head","slot":0},{"at_us":6472,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":88,"abi_return_us":6219,"end_us":6224,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6260,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6256},{"abi_enter_us":6272,"abi_return_us":6277,"end_us":6277,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6272,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6311,"operation":"read","slot":0,"start_us":6309},{"abi_enter_us":6324,"abi_return_us":500463,"end_us":500493,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6324,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500599,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500588}],"schema":2,"slots":[{"at_us":500599,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd440932e570-73d1-90","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5408,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5493,"slot":0,"token":"18e1fd442757bd28-73d1-91"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd442757bd28-73d1-91","watchdog":false,"workers_joined":1,"writes":[{"at_us":5508,"bytes":57,"kind":"head","slot":0},{"at_us":5514,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":69,"abi_return_us":5199,"end_us":5202,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5238,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5233},{"abi_enter_us":5251,"abi_return_us":5256,"end_us":5256,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5251,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5282,"operation":"read","slot":0,"start_us":5280},{"abi_enter_us":5294,"abi_return_us":5295,"end_us":5295,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5294,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5322,"operation":"read","slot":0,"start_us":5314},{"abi_enter_us":5333,"abi_return_us":5333,"end_us":5334,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5333,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5354,"operation":"read","slot":0,"start_us":5354},{"abi_enter_us":5364,"abi_return_us":5366,"end_us":5366,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5364,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5385,"operation":"read","slot":0,"start_us":5384},{"end_us":5397,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5394}],"schema":2,"slots":[{"at_us":5397,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd442757bd28-73d1-91","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":6482,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6506,"slot":0,"token":"18e1fd4427d58848-73d1-92"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd4427d58848-73d1-92","watchdog":false,"workers_joined":1,"writes":[{"at_us":6524,"bytes":57,"kind":"head","slot":0},{"at_us":6530,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":69,"abi_return_us":6299,"end_us":6305,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6354,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6350},{"abi_enter_us":6369,"abi_return_us":6374,"end_us":6374,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6369,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6409,"operation":"read","slot":0,"start_us":6406},{"abi_enter_us":6424,"abi_return_us":6426,"end_us":6426,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6424,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6449,"operation":"read","slot":0,"start_us":6449},{"end_us":6465,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6462}],"schema":2,"slots":[{"at_us":6465,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4427d58848-73d1-92","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5333,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5265,"slot":0,"token":"18e1fd442864faf0-73d1-93"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd442864faf0-73d1-93","watchdog":false,"workers_joined":1,"writes":[{"at_us":5280,"bytes":57,"kind":"head","slot":0},{"at_us":5286,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"abi_enter_us":72,"abi_return_us":5099,"end_us":5104,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5153,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5148},{"abi_enter_us":5168,"abi_return_us":5174,"end_us":5174,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5168,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5202,"operation":"read","slot":0,"start_us":5200},{"abi_enter_us":5214,"abi_return_us":5215,"end_us":5215,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5214,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5237,"operation":"read","slot":0,"start_us":5236},{"abi_enter_us":5246,"abi_return_us":5247,"end_us":5247,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5246,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5270,"operation":"read","slot":0,"start_us":5269},{"abi_enter_us":5280,"abi_return_us":5282,"end_us":5283,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5280,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5303,"operation":"read","slot":0,"start_us":5302},{"end_us":5315,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5312}],"schema":2,"slots":[{"at_us":5315,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd442864faf0-73d1-93","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":6221,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6449,"slot":0,"token":"18e1fd4429139628-73d1-94"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd4429139628-73d1-94","watchdog":false,"workers_joined":1,"writes":[{"at_us":6462,"bytes":57,"kind":"head","slot":0},{"at_us":6467,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"abi_enter_us":99,"abi_return_us":6072,"end_us":6076,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6115,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6109},{"abi_enter_us":6127,"abi_return_us":6131,"end_us":6132,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6127,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6157,"operation":"read","slot":0,"start_us":6156},{"abi_enter_us":6170,"abi_return_us":6171,"end_us":6171,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6169,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6196,"operation":"read","slot":0,"start_us":6195},{"end_us":6208,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6205}],"schema":2,"slots":[{"at_us":6208,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4429139628-73d1-94","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5443,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5560,"slot":0,"token":"18e1fd4429a39d40-73d1-95"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd4429a39d40-73d1-95","watchdog":false,"workers_joined":1,"writes":[{"at_us":5576,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":64,"abi_return_us":5331,"end_us":5335,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5373,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5369},{"abi_enter_us":5387,"abi_return_us":5389,"end_us":5389,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5387,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5414,"operation":"read","slot":0,"start_us":5412},{"end_us":5429,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5426}],"schema":2,"slots":[{"at_us":5429,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4429a39d40-73d1-95","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5086,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5217,"slot":0,"token":"18e1fd442a1e4b80-73d1-96"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd442a1e4b80-73d1-96","watchdog":false,"workers_joined":1,"writes":[{"at_us":5229,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":59,"abi_return_us":4977,"end_us":4978,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5009,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5006},{"abi_enter_us":5025,"abi_return_us":5026,"end_us":5026,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5025,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5054,"operation":"read","slot":0,"start_us":5053},{"end_us":5071,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5068}],"schema":2,"slots":[{"at_us":5071,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd442a1e4b80-73d1-96","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5157,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5208,"slot":0,"token":"18e1fd442aa42138-73d1-97"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd442aa42138-73d1-97","watchdog":false,"workers_joined":1,"writes":[{"at_us":5217,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":23,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"abi_enter_us":117,"abi_return_us":5047,"end_us":5049,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5078,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5074},{"abi_enter_us":5094,"abi_return_us":5097,"end_us":5097,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5094,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5126,"operation":"read","slot":0,"start_us":5125},{"end_us":5140,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5138}],"schema":2,"slots":[{"at_us":5140,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd442aa42138-73d1-97","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5067,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5176,"slot":0,"token":"18e1fd442b1fc1a8-73d1-98"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd442b1fc1a8-73d1-98","watchdog":false,"workers_joined":1,"writes":[{"at_us":5183,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":28,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":76,"abi_return_us":4962,"end_us":4964,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4995,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4992},{"abi_enter_us":5009,"abi_return_us":5011,"end_us":5012,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5009,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5035,"operation":"read","slot":0,"start_us":5034},{"end_us":5052,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5050}],"schema":2,"slots":[{"at_us":5052,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd442b1fc1a8-73d1-98","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5100,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5209,"slot":0,"token":"18e1fd442b9226d0-73d1-99"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd442b9226d0-73d1-99","watchdog":false,"workers_joined":1,"writes":[{"at_us":5219,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"abi_enter_us":51,"abi_return_us":5038,"end_us":5039,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5056,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5054},{"end_us":5068,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5068}],"schema":2,"slots":[{"at_us":5068,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd442b9226d0-73d1-99","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5245,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5374,"slot":0,"token":"18e1fd442c011d10-73d1-9a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd442c011d10-73d1-9a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5379,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":24,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"abi_enter_us":42,"abi_return_us":5198,"end_us":5199,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":42,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5216,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5214},{"end_us":5231,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5231}],"schema":2,"slots":[{"at_us":5231,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd442c011d10-73d1-9a","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6171,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6188,"slot":0,"token":"18e1fd442c846a58-73d1-9b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd442c846a58-73d1-9b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6206,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":23,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":42,"operation":"handoff","start_us":42},{"abi_enter_us":51,"abi_return_us":6106,"end_us":6109,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":51,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6140,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6136},{"end_us":6154,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6154}],"schema":2,"slots":[{"at_us":6154,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd442c846a58-73d1-9b","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5822,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5889,"slot":0,"token":"18e1fd442d03b060-73d1-9c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd442d03b060-73d1-9c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5899,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":26,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":41,"operation":"handoff","start_us":41},{"abi_enter_us":50,"abi_return_us":5759,"end_us":5763,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":50,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5794,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5791},{"end_us":5807,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5807}],"schema":2,"slots":[{"at_us":5807,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd442d03b060-73d1-9c","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58604,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5773,"slot":0,"token":"18e1fd442d7e04c8-73d1-9d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd442d7e04c8-73d1-9d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5781,"bytes":66,"kind":"head","slot":0},{"at_us":5785,"bytes":6,"kind":"body","slot":0},{"at_us":58680,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"abi_enter_us":102,"abi_return_us":5576,"end_us":5578,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5603,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5600},{"abi_enter_us":5614,"abi_return_us":5618,"end_us":5618,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5614,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5647,"operation":"read","slot":0,"start_us":5644},{"abi_enter_us":5665,"abi_return_us":58499,"end_us":58504,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5665,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58568,"operation":"read","slot":0,"start_us":58565},{"end_us":58587,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58583}],"schema":2,"slots":[{"at_us":58587,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd442d7e04c8-73d1-9d","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58234,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5195,"slot":0,"token":"18e1fd44315278e0-73d1-9e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd44315278e0-73d1-9e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5201,"bytes":66,"kind":"head","slot":0},{"at_us":5206,"bytes":6,"kind":"body","slot":0},{"at_us":58340,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":77,"abi_return_us":4918,"end_us":4919,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4962,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4959},{"abi_enter_us":4974,"abi_return_us":4978,"end_us":4979,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4974,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5009,"operation":"read","slot":0,"start_us":5006},{"abi_enter_us":5022,"abi_return_us":58118,"end_us":58124,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5022,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":58193,"operation":"read","slot":0,"start_us":58190},{"end_us":58214,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58210}],"schema":2,"slots":[{"at_us":58214,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd44315278e0-73d1-9e","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":60048,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5272,"slot":0,"token":"18e1fd4435089f28-73d1-9f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd4435089f28-73d1-9f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5290,"bytes":66,"kind":"head","slot":0},{"at_us":5298,"bytes":6,"kind":"body","slot":0},{"at_us":60183,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":98,"operation":"handoff","start_us":98},{"abi_enter_us":112,"abi_return_us":5019,"end_us":5024,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5060,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5056},{"abi_enter_us":5071,"abi_return_us":5077,"end_us":5077,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5071,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5106,"operation":"read","slot":0,"start_us":5104},{"abi_enter_us":5117,"abi_return_us":59944,"end_us":59948,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5117,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":60017,"operation":"read","slot":0,"start_us":60013},{"end_us":60033,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60029}],"schema":2,"slots":[{"at_us":60033,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4435089f28-73d1-9f","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":55435,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5478,"slot":0,"token":"18e1fd4438d5fad8-73d1-a0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd4438d5fad8-73d1-a0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5491,"bytes":66,"kind":"head","slot":0},{"at_us":5496,"bytes":6,"kind":"body","slot":0},{"at_us":55600,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"abi_enter_us":73,"abi_return_us":5199,"end_us":5203,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5253,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5248},{"abi_enter_us":5267,"abi_return_us":5273,"end_us":5274,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5267,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5308,"operation":"read","slot":0,"start_us":5306},{"abi_enter_us":5321,"abi_return_us":55320,"end_us":55324,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5321,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":55400,"operation":"read","slot":0,"start_us":55395},{"end_us":55419,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55414}],"schema":2,"slots":[{"at_us":55419,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4438d5fad8-73d1-a0","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5969,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6179,"slot":0,"token":"18e1fd443c570f30-73d1-a1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd443c570f30-73d1-a1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"abi_enter_us":102,"abi_return_us":5902,"end_us":5906,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":102,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5940,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5936},{"end_us":5955,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5955}],"schema":2,"slots":[{"at_us":5955,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd443c570f30-73d1-a1","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5058,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5218,"slot":0,"token":"18e1fd443cd82de0-73d1-a2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd443cd82de0-73d1-a2","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":65,"abi_return_us":5001,"end_us":5002,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":65,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5028,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5025},{"end_us":5044,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5043}],"schema":2,"slots":[{"at_us":5044,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd443cd82de0-73d1-a2","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5055,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5232,"slot":0,"token":"18e1fd443d5aeaa0-73d1-a3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd443d5aeaa0-73d1-a3","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":96,"abi_return_us":4993,"end_us":5005,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5030,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5028},{"end_us":5041,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5041}],"schema":2,"slots":[{"at_us":5041,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd443d5aeaa0-73d1-a3","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5342,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5524,"slot":0,"token":"18e1fd443dd9dab8-73d1-a4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd443dd9dab8-73d1-a4","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":106,"operation":"handoff","start_us":106},{"abi_enter_us":136,"abi_return_us":5266,"end_us":5271,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":136,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5307,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5302},{"end_us":5321,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5321}],"schema":2,"slots":[{"at_us":5321,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd443dd9dab8-73d1-a4","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":58315,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6461,"slot":0,"token":"18e1fd443e627f30-73d1-a5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd443e627f30-73d1-a5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6478,"bytes":57,"kind":"head","slot":0},{"at_us":6484,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":71,"abi_return_us":6188,"end_us":6190,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6229,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6225},{"bytes":1,"capacity":1,"end_us":6261,"operation":"read","slot":0,"start_us":6252},{"capacity":1,"end_us":58249,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":6285},{"end_us":58296,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58290}],"schema":2,"slots":[{"at_us":58296,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd443e627f30-73d1-a5","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":60136,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6386,"slot":0,"token":"18e1fd44421f0e18-73d1-a6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd44421f0e18-73d1-a6","watchdog":false,"workers_joined":1,"writes":[{"at_us":6407,"bytes":57,"kind":"head","slot":0},{"at_us":6413,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":74,"abi_return_us":6132,"end_us":6137,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6192,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6186},{"abi_enter_us":6206,"abi_return_us":6212,"end_us":6212,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6205,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6253,"operation":"read","slot":0,"start_us":6250},{"abi_enter_us":6267,"abi_return_us":60034,"end_us":60042,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6267,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":60104,"operation":"read","slot":0,"start_us":60099},{"end_us":60121,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60116}],"schema":2,"slots":[{"at_us":60121,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd44421f0e18-73d1-a6","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":57934,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5633,"slot":0,"token":"18e1fd4445dc8f30-73d1-a7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd4445dc8f30-73d1-a7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5652,"bytes":57,"kind":"head","slot":0},{"at_us":5659,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":62,"abi_return_us":5364,"end_us":5370,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5432,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5426},{"abi_enter_us":5470,"abi_return_us":5480,"end_us":5481,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5470,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5518,"operation":"read","slot":0,"start_us":5510},{"abi_enter_us":5531,"abi_return_us":57823,"end_us":57827,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5530,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":57897,"operation":"read","slot":0,"start_us":57893},{"end_us":57913,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":57909}],"schema":2,"slots":[{"at_us":57913,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4445dc8f30-73d1-a7","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":60765,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5864,"slot":0,"token":"18e1fd44497bbaa8-73d1-a8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd44497bbaa8-73d1-a8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5885,"bytes":57,"kind":"head","slot":0},{"at_us":5893,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":73,"operation":"handoff","start_us":73},{"abi_enter_us":85,"abi_return_us":5602,"end_us":5613,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5653,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5648},{"abi_enter_us":5667,"abi_return_us":5673,"end_us":5673,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5667,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5699,"operation":"read","slot":0,"start_us":5697},{"abi_enter_us":5730,"abi_return_us":60652,"end_us":60659,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5730,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":60729,"operation":"read","slot":0,"start_us":60725},{"end_us":60746,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60742}],"schema":2,"slots":[{"at_us":60746,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd44497bbaa8-73d1-a8","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":59679,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6347,"slot":0,"token":"18e1fd444d4303a8-73d1-a9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd444d4303a8-73d1-a9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6363,"bytes":57,"kind":"head","slot":0},{"at_us":6367,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":122,"abi_return_us":6086,"end_us":6092,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":122,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6142,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6137},{"abi_enter_us":6155,"abi_return_us":6161,"end_us":6161,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6155,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6196,"operation":"read","slot":0,"start_us":6194},{"abi_enter_us":6207,"abi_return_us":59550,"end_us":59559,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6207,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":59642,"operation":"read","slot":0,"start_us":59639},{"end_us":59662,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59655}],"schema":2,"slots":[{"at_us":59662,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd444d4303a8-73d1-a9","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":500777,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5520,"slot":0,"token":"18e1fd445102fd90-73d1-aa"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd445102fd90-73d1-aa","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":111,"abi_return_us":500695,"end_us":500704,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500760,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500760}],"schema":2,"slots":[{"at_us":500760,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd445102fd90-73d1-aa","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":500439,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5243,"slot":0,"token":"18e1fd446ef9b0c8-73d1-ab"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd446ef9b0c8-73d1-ab","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":56,"abi_return_us":500359,"end_us":500371,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500421,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500421}],"schema":2,"slots":[{"at_us":500421,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd446ef9b0c8-73d1-ab","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":502576,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5345,"slot":0,"token":"18e1fd448cf2a620-73d1-ac"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd448cf2a620-73d1-ac","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":89,"abi_return_us":501519,"end_us":501859,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502551,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502551}],"schema":2,"slots":[{"at_us":502551,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd448cf2a620-73d1-ac","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501462,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5382,"slot":0,"token":"18e1fd44ab078400-73d1-ad"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd44ab078400-73d1-ad","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"abi_enter_us":84,"abi_return_us":501400,"end_us":501408,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501446,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501446}],"schema":2,"slots":[{"at_us":501446,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd44ab078400-73d1-ad","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":500407,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6426,"slot":0,"token":"18e1fd44c90ff630-73d1-ae"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd44c90ff630-73d1-ae","watchdog":false,"workers_joined":1,"writes":[{"at_us":6442,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":67,"abi_return_us":6194,"end_us":6197,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6231,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6226},{"abi_enter_us":6243,"abi_return_us":500317,"end_us":500324,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6243,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500388,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500383}],"schema":2,"slots":[{"at_us":500388,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd44c90ff630-73d1-ae","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":501807,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5377,"slot":0,"token":"18e1fd44e70b50d0-73d1-af"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd44e70b50d0-73d1-af","watchdog":false,"workers_joined":1,"writes":[{"at_us":5387,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":66,"abi_return_us":5053,"end_us":5056,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5091,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5087},{"abi_enter_us":5104,"abi_return_us":501654,"end_us":501659,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5103,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501784,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501778}],"schema":2,"slots":[{"at_us":501784,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd44e70b50d0-73d1-af","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":501326,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6445,"slot":0,"token":"18e1fd4505155170-73d1-b0"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd4505155170-73d1-b0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6458,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":97,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":120,"operation":"handoff","start_us":120},{"abi_enter_us":132,"abi_return_us":6152,"end_us":6153,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":132,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6180,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6176},{"abi_enter_us":6192,"abi_return_us":501216,"end_us":501226,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6192,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501297,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501289}],"schema":2,"slots":[{"at_us":501297,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4505155170-73d1-b0","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":501022,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5864,"slot":0,"token":"18e1fd45231806e0-73d1-b1"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd45231806e0-73d1-b1","watchdog":false,"workers_joined":1,"writes":[{"at_us":5888,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"abi_enter_us":99,"abi_return_us":5619,"end_us":5624,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":99,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5673,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5669},{"abi_enter_us":5694,"abi_return_us":500909,"end_us":500918,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5694,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500998,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500984}],"schema":2,"slots":[{"at_us":500998,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd45231806e0-73d1-b1","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500802,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5253,"slot":0,"token":"18e1fd4541341880-73d1-b2"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd4541341880-73d1-b2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5261,"bytes":57,"kind":"head","slot":0},{"at_us":5268,"bytes":1,"kind":"body","slot":0},{"at_us":308257,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":72,"abi_return_us":4965,"end_us":4969,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5014,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5010},{"abi_enter_us":5034,"abi_return_us":5040,"end_us":5041,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5034,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5067,"operation":"read","slot":0,"start_us":5065},{"abi_enter_us":5078,"abi_return_us":307933,"end_us":307946,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5078,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":308029,"operation":"read","slot":0,"start_us":308026},{"abi_enter_us":308043,"abi_return_us":500670,"end_us":500674,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308043,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500774,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500763}],"schema":2,"slots":[{"at_us":500774,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4541341880-73d1-b2","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":501951,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5266,"slot":0,"token":"18e1fd455f3ee828-73d1-b3"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd455f3ee828-73d1-b3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5283,"bytes":57,"kind":"head","slot":0},{"at_us":5290,"bytes":1,"kind":"body","slot":0},{"at_us":311420,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":103,"abi_return_us":4957,"end_us":4962,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5015,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5011},{"abi_enter_us":5031,"abi_return_us":5035,"end_us":5035,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5031,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5062,"operation":"read","slot":0,"start_us":5060},{"abi_enter_us":5076,"abi_return_us":312133,"end_us":312139,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5076,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":312213,"operation":"read","slot":0,"start_us":312210},{"abi_enter_us":312229,"abi_return_us":499523,"end_us":499527,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":312229,"status":"FastlyStatus::OK","timeout_ms":187},{"abi_enter_us":499574,"abi_return_us":501863,"end_us":501865,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":499574,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":501913,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501906}],"schema":2,"slots":[{"at_us":501913,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd455f3ee828-73d1-b3","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500917,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5267,"slot":0,"token":"18e1fd457d623940-73d1-b4"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd457d623940-73d1-b4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5282,"bytes":57,"kind":"head","slot":0},{"at_us":5291,"bytes":1,"kind":"body","slot":0},{"at_us":308862,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":65,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":103,"operation":"handoff","start_us":103},{"abi_enter_us":116,"abi_return_us":5017,"end_us":5024,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5072,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5066},{"abi_enter_us":5087,"abi_return_us":5095,"end_us":5099,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5087,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5125,"operation":"read","slot":0,"start_us":5122},{"abi_enter_us":5137,"abi_return_us":308568,"end_us":308575,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5137,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":308636,"operation":"read","slot":0,"start_us":308633},{"abi_enter_us":308653,"abi_return_us":500790,"end_us":500794,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308653,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":500856,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500848}],"schema":2,"slots":[{"at_us":500856,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd457d623940-73d1-b4","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500568,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5470,"slot":0,"token":"18e1fd459b7bf150-73d1-b5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd459b7bf150-73d1-b5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5490,"bytes":57,"kind":"head","slot":0},{"at_us":5498,"bytes":1,"kind":"body","slot":0},{"at_us":307774,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":97,"abi_return_us":5199,"end_us":5205,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5275,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5270},{"abi_enter_us":5292,"abi_return_us":5300,"end_us":5300,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5292,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5337,"operation":"read","slot":0,"start_us":5334},{"abi_enter_us":5350,"abi_return_us":307442,"end_us":307450,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5350,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":307542,"operation":"read","slot":0,"start_us":307539},{"abi_enter_us":307562,"abi_return_us":500299,"end_us":500303,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307562,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500364,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500356}],"schema":2,"slots":[{"at_us":500364,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd459b7bf150-73d1-b5","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501183,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5960,"slot":1,"token":"18e1fd45b99ec180-73d1-b6"},{"path":"/readiness/siblings/0","received_us":5972,"slot":0,"token":"18e1fd45b99ec180-73d1-b6"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd45b99ec180-73d1-b6","watchdog":false,"workers_joined":2,"writes":[{"at_us":5977,"bytes":57,"kind":"head","slot":1},{"at_us":5981,"bytes":57,"kind":"head","slot":0},{"at_us":5993,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":105,"operation":"dispatch","outcome":"pending","slot":1,"start_us":80},{"abi_enter_us":124,"abi_return_us":5700,"end_us":5706,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":123,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5749,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5744},{"abi_enter_us":5765,"abi_return_us":5767,"end_us":5767,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5765,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5784,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5784},{"abi_enter_us":5800,"abi_return_us":5805,"end_us":5805,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5800,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5836,"operation":"read","slot":1,"start_us":5833},{"abi_enter_us":5851,"abi_return_us":5852,"end_us":5852,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5851,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5892,"operation":"read","slot":1,"start_us":5882},{"abi_enter_us":5904,"abi_return_us":5905,"end_us":5905,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5904,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5932,"operation":"read","slot":1,"start_us":5932},{"abi_enter_us":5942,"abi_return_us":5945,"end_us":5945,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5942,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5967,"operation":"read","slot":1,"start_us":5966},{"end_us":5979,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5976},{"abi_enter_us":5992,"abi_return_us":500895,"end_us":500932,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5992,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501119,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501101}],"schema":2,"slots":[{"at_us":501119,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5979,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd45b99ec180-73d1-b6","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501078,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5830,"slot":1,"token":"18e1fd45d80bf0c0-73d1-b7"},{"path":"/readiness/siblings/0","received_us":5932,"slot":0,"token":"18e1fd45d80bf0c0-73d1-b7"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd45d80bf0c0-73d1-b7","watchdog":false,"workers_joined":2,"writes":[{"at_us":5851,"bytes":57,"kind":"head","slot":1},{"at_us":5942,"bytes":57,"kind":"head","slot":0},{"at_us":11446,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":98,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":158,"operation":"dispatch","outcome":"pending","slot":1,"start_us":144},{"abi_enter_us":172,"abi_return_us":4579,"end_us":4587,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":171,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4632,"operation":"head-poll","outcome":"ready","slot":1,"start_us":4627},{"abi_enter_us":4647,"abi_return_us":4674,"end_us":4674,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":4647,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":4693,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4692},{"abi_enter_us":4704,"abi_return_us":10200,"end_us":10203,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":4704,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":3,"capacity":16384,"end_us":10272,"operation":"read","slot":1,"start_us":10269},{"abi_enter_us":10287,"abi_return_us":10292,"end_us":10292,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":10287,"status":"FastlyStatus::OK","timeout_ms":489},{"bytes":0,"capacity":16384,"end_us":10318,"operation":"read","slot":1,"start_us":10318},{"end_us":10333,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":10328},{"abi_enter_us":10349,"abi_return_us":500505,"end_us":500661,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":10349,"status":"FastlyStatus::OK","timeout_ms":489},{"end_us":500973,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500947}],"schema":2,"slots":[{"at_us":500973,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":10333,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd45d80bf0c0-73d1-b7","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501014,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6653,"slot":0,"token":"18e1fd45f6663828-73d1-b8"},{"path":"/readiness/siblings/1","received_us":6797,"slot":1,"token":"18e1fd45f6663828-73d1-b8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd45f6663828-73d1-b8","watchdog":false,"workers_joined":2,"writes":[{"at_us":6675,"bytes":57,"kind":"head","slot":0},{"at_us":6808,"bytes":57,"kind":"head","slot":1},{"at_us":6815,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":225,"operation":"dispatch","outcome":"pending","slot":0,"start_us":50},{"end_us":353,"operation":"dispatch","outcome":"pending","slot":1,"start_us":306},{"body_reads":0,"deadline_us":500000,"end_us":379,"operation":"handoff","start_us":379},{"abi_enter_us":395,"abi_return_us":4317,"end_us":4323,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":394,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4373,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4368},{"abi_enter_us":4388,"abi_return_us":4435,"end_us":4435,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":4388,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":4454,"operation":"head-poll","outcome":"ready","slot":1,"start_us":4453},{"abi_enter_us":5532,"abi_return_us":5544,"end_us":5545,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5532,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5591,"operation":"read","slot":1,"start_us":5588},{"abi_enter_us":5605,"abi_return_us":5606,"end_us":5607,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5605,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5632,"operation":"read","slot":1,"start_us":5630},{"abi_enter_us":5646,"abi_return_us":5647,"end_us":5647,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5646,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5670,"operation":"read","slot":1,"start_us":5670},{"abi_enter_us":5681,"abi_return_us":5684,"end_us":5684,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5681,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5709,"operation":"read","slot":1,"start_us":5708},{"end_us":5722,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5719},{"abi_enter_us":5741,"abi_return_us":500822,"end_us":500852,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5741,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500993,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500985}],"schema":2,"slots":[{"at_us":500993,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5722,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd45f6663828-73d1-b8","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501639,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6543,"slot":0,"token":"18e1fd4614b4e6d0-73d1-b9"},{"path":"/readiness/siblings/1","received_us":6555,"slot":1,"token":"18e1fd4614b4e6d0-73d1-b9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd4614b4e6d0-73d1-b9","watchdog":false,"workers_joined":2,"writes":[{"at_us":6565,"bytes":57,"kind":"head","slot":0},{"at_us":6580,"bytes":57,"kind":"head","slot":1},{"at_us":6587,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"end_us":258,"operation":"dispatch","outcome":"pending","slot":1,"start_us":159},{"body_reads":0,"deadline_us":500000,"end_us":302,"operation":"handoff","start_us":302},{"abi_enter_us":319,"abi_return_us":6073,"end_us":6079,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":319,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6146,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6141},{"abi_enter_us":6163,"abi_return_us":6166,"end_us":6166,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6163,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6184,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6183},{"abi_enter_us":6196,"abi_return_us":6200,"end_us":6200,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6196,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6232,"operation":"read","slot":1,"start_us":6230},{"abi_enter_us":6245,"abi_return_us":6249,"end_us":6250,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6245,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6277,"operation":"read","slot":1,"start_us":6277},{"end_us":6292,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6289},{"abi_enter_us":6307,"abi_return_us":501223,"end_us":501241,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6307,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501583,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501569}],"schema":2,"slots":[{"at_us":501583,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6292,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4614b4e6d0-73d1-b9","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":500738,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6475,"slot":0,"token":"18e1fd4632dfcd50-73d1-ba"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd4632dfcd50-73d1-ba","watchdog":false,"workers_joined":1,"writes":[{"at_us":6489,"bytes":57,"kind":"head","slot":0},{"at_us":6494,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":91,"abi_return_us":6184,"end_us":6188,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6240,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6235},{"abi_enter_us":6258,"abi_return_us":6267,"end_us":6267,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6258,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6301,"operation":"read","slot":0,"start_us":6299},{"abi_enter_us":6315,"abi_return_us":500609,"end_us":500626,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6315,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500713,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500706}],"schema":2,"slots":[{"at_us":500713,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4632dfcd50-73d1-ba","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501716,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6504,"slot":0,"token":"18e1fd4651081fa8-73d1-bb"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd4651081fa8-73d1-bb","watchdog":false,"workers_joined":1,"writes":[{"at_us":6521,"bytes":57,"kind":"head","slot":0},{"at_us":6527,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":88,"abi_return_us":6236,"end_us":6242,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6455,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6449},{"abi_enter_us":6477,"abi_return_us":6482,"end_us":6483,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6476,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6517,"operation":"read","slot":0,"start_us":6514},{"abi_enter_us":6531,"abi_return_us":501612,"end_us":501626,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6531,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501694,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501689}],"schema":2,"slots":[{"at_us":501694,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4651081fa8-73d1-bb","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501558,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5872,"slot":0,"token":"18e1fd466f1c0770-73d1-bc"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd466f1c0770-73d1-bc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5892,"bytes":57,"kind":"head","slot":0},{"at_us":5902,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":73,"operation":"handoff","start_us":73},{"abi_enter_us":84,"abi_return_us":5627,"end_us":5633,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5678,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5674},{"abi_enter_us":5696,"abi_return_us":5703,"end_us":5703,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5696,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5729,"operation":"read","slot":0,"start_us":5726},{"abi_enter_us":5741,"abi_return_us":501456,"end_us":501463,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5741,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501535,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501526}],"schema":2,"slots":[{"at_us":501535,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd466f1c0770-73d1-bc","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501531,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5318,"slot":0,"token":"18e1fd468d32ae58-73d1-bd"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd468d32ae58-73d1-bd","watchdog":false,"workers_joined":1,"writes":[{"at_us":5345,"bytes":57,"kind":"head","slot":0},{"at_us":5354,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":71,"operation":"handoff","start_us":71},{"abi_enter_us":96,"abi_return_us":5111,"end_us":5117,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5176,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5170},{"abi_enter_us":5191,"abi_return_us":5212,"end_us":5214,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5191,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5270,"operation":"read","slot":0,"start_us":5267},{"abi_enter_us":5282,"abi_return_us":501399,"end_us":501419,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5282,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501507,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501495}],"schema":2,"slots":[{"at_us":501507,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd468d32ae58-73d1-bd","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5228,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5264,"slot":0,"token":"18e1fd46ab4fc998-73d1-be"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46ab4fc998-73d1-be","watchdog":false,"workers_joined":1,"writes":[{"at_us":5279,"bytes":57,"kind":"head","slot":0},{"at_us":5284,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":83,"operation":"dispatch","outcome":"pending","slot":0,"start_us":30},{"abi_enter_us":116,"abi_return_us":5012,"end_us":5016,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":116,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5055,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5051},{"abi_enter_us":5072,"abi_return_us":5076,"end_us":5077,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5072,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5102,"operation":"read","slot":0,"start_us":5100},{"abi_enter_us":5113,"abi_return_us":5114,"end_us":5115,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5113,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5143,"operation":"read","slot":0,"start_us":5135},{"abi_enter_us":5153,"abi_return_us":5154,"end_us":5154,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5153,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5173,"operation":"read","slot":0,"start_us":5172},{"abi_enter_us":5182,"abi_return_us":5184,"end_us":5184,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5182,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5204,"operation":"read","slot":0,"start_us":5204},{"end_us":5216,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5213}],"schema":2,"slots":[{"at_us":5216,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46ab4fc998-73d1-be","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5612,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5665,"slot":0,"token":"18e1fd46abe389d0-73d1-bf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46abe389d0-73d1-bf","watchdog":false,"workers_joined":1,"writes":[{"at_us":5677,"bytes":57,"kind":"head","slot":0},{"at_us":5682,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":63,"abi_return_us":5449,"end_us":5451,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5485,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5479},{"abi_enter_us":5497,"abi_return_us":5502,"end_us":5503,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5497,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5529,"operation":"read","slot":0,"start_us":5526},{"abi_enter_us":5547,"abi_return_us":5550,"end_us":5551,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5547,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5586,"operation":"read","slot":0,"start_us":5585},{"end_us":5599,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5597}],"schema":2,"slots":[{"at_us":5599,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46abe389d0-73d1-bf","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5267,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5305,"slot":0,"token":"18e1fd46ac80a490-73d1-c0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46ac80a490-73d1-c0","watchdog":false,"workers_joined":1,"writes":[{"at_us":5318,"bytes":57,"kind":"head","slot":0},{"at_us":5324,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":75,"operation":"handoff","start_us":75},{"abi_enter_us":106,"abi_return_us":5061,"end_us":5066,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5099,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5094},{"abi_enter_us":5110,"abi_return_us":5118,"end_us":5119,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5110,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5146,"operation":"read","slot":0,"start_us":5143},{"abi_enter_us":5156,"abi_return_us":5157,"end_us":5157,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5156,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5177,"operation":"read","slot":0,"start_us":5176},{"abi_enter_us":5187,"abi_return_us":5188,"end_us":5188,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5187,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5209,"operation":"read","slot":0,"start_us":5208},{"abi_enter_us":5217,"abi_return_us":5220,"end_us":5220,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5217,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5237,"operation":"read","slot":0,"start_us":5237},{"end_us":5249,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5245}],"schema":2,"slots":[{"at_us":5249,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46ac80a490-73d1-c0","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5502,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5567,"slot":0,"token":"18e1fd46ad0b2980-73d1-c1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46ad0b2980-73d1-c1","watchdog":false,"workers_joined":1,"writes":[{"at_us":5583,"bytes":57,"kind":"head","slot":0},{"at_us":5590,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"abi_enter_us":62,"abi_return_us":5372,"end_us":5375,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5403,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5399},{"abi_enter_us":5416,"abi_return_us":5420,"end_us":5420,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5416,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5444,"operation":"read","slot":0,"start_us":5442},{"abi_enter_us":5454,"abi_return_us":5455,"end_us":5456,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5454,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5478,"operation":"read","slot":0,"start_us":5478},{"end_us":5490,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5487}],"schema":2,"slots":[{"at_us":5490,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46ad0b2980-73d1-c1","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5342,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5415,"slot":0,"token":"18e1fd46ad906ac8-73d1-c2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46ad906ac8-73d1-c2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5430,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":54,"abi_return_us":5244,"end_us":5247,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":54,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5279,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5274},{"abi_enter_us":5290,"abi_return_us":5293,"end_us":5293,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5290,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5315,"operation":"read","slot":0,"start_us":5313},{"end_us":5328,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5325}],"schema":2,"slots":[{"at_us":5328,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46ad906ac8-73d1-c2","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5214,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5197,"slot":0,"token":"18e1fd46ae074490-73d1-c3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46ae074490-73d1-c3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5212,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":55,"abi_return_us":5056,"end_us":5062,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":55,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5104,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5100},{"abi_enter_us":5117,"abi_return_us":5120,"end_us":5120,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5117,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5181,"operation":"read","slot":0,"start_us":5179},{"end_us":5199,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5195}],"schema":2,"slots":[{"at_us":5199,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46ae074490-73d1-c3","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6343,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6432,"slot":0,"token":"18e1fd46ae992450-73d1-c4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46ae992450-73d1-c4","watchdog":false,"workers_joined":1,"writes":[{"at_us":6447,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":85,"abi_return_us":6239,"end_us":6243,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":85,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6281,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6275},{"abi_enter_us":6293,"abi_return_us":6295,"end_us":6295,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6293,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6318,"operation":"read","slot":0,"start_us":6316},{"end_us":6331,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6328}],"schema":2,"slots":[{"at_us":6331,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46ae992450-73d1-c4","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5118,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5196,"slot":0,"token":"18e1fd46af228448-73d1-c5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46af228448-73d1-c5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5204,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"abi_enter_us":64,"abi_return_us":5019,"end_us":5022,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":63,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5051,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5047},{"abi_enter_us":5064,"abi_return_us":5066,"end_us":5066,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5064,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5094,"operation":"read","slot":0,"start_us":5092},{"end_us":5106,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5104}],"schema":2,"slots":[{"at_us":5106,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46af228448-73d1-c5","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6319,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6435,"slot":0,"token":"18e1fd46af9961f8-73d1-c6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46af9961f8-73d1-c6","watchdog":false,"workers_joined":1,"writes":[{"at_us":6451,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"abi_enter_us":71,"abi_return_us":6258,"end_us":6263,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6291,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6287},{"end_us":6305,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6305}],"schema":2,"slots":[{"at_us":6305,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd46af9961f8-73d1-c6","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6371,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6450,"slot":0,"token":"18e1fd46b01e8fb8-73d1-c7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46b01e8fb8-73d1-c7","watchdog":false,"workers_joined":1,"writes":[{"at_us":6464,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":9},{"abi_enter_us":59,"abi_return_us":6302,"end_us":6308,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6340,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6336},{"end_us":6356,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6356}],"schema":2,"slots":[{"at_us":6356,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd46b01e8fb8-73d1-c7","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5441,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5475,"slot":0,"token":"18e1fd46b0a65970-73d1-c8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46b0a65970-73d1-c8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5489,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":12},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"abi_enter_us":74,"abi_return_us":5365,"end_us":5372,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5409,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5404},{"end_us":5423,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5423}],"schema":2,"slots":[{"at_us":5423,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd46b0a65970-73d1-c8","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6298,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6441,"slot":0,"token":"18e1fd46b1380668-73d1-c9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46b1380668-73d1-c9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6455,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":59,"operation":"handoff","start_us":59},{"abi_enter_us":93,"abi_return_us":6252,"end_us":6253,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6271,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6269},{"end_us":6283,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6282}],"schema":2,"slots":[{"at_us":6283,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd46b1380668-73d1-c9","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58941,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5224,"slot":0,"token":"18e1fd46b1bd5750-73d1-ca"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46b1bd5750-73d1-ca","watchdog":false,"workers_joined":1,"writes":[{"at_us":5237,"bytes":66,"kind":"head","slot":0},{"at_us":5243,"bytes":6,"kind":"body","slot":0},{"at_us":59056,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":64,"abi_return_us":5038,"end_us":5039,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5062,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5060},{"abi_enter_us":5074,"abi_return_us":5080,"end_us":5081,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5074,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5103,"operation":"read","slot":0,"start_us":5101},{"abi_enter_us":5115,"abi_return_us":58847,"end_us":58851,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5115,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":58908,"operation":"read","slot":0,"start_us":58905},{"end_us":58926,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58922}],"schema":2,"slots":[{"at_us":58926,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46b1bd5750-73d1-ca","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":55891,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5252,"slot":0,"token":"18e1fd46b57100e0-73d1-cb"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46b57100e0-73d1-cb","watchdog":false,"workers_joined":1,"writes":[{"at_us":5265,"bytes":66,"kind":"head","slot":0},{"at_us":5273,"bytes":6,"kind":"body","slot":0},{"at_us":55865,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":70,"abi_return_us":5055,"end_us":5057,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5093,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5089},{"abi_enter_us":5105,"abi_return_us":5110,"end_us":5110,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5105,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5136,"operation":"read","slot":0,"start_us":5134},{"abi_enter_us":5151,"abi_return_us":55686,"end_us":55692,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5151,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":55844,"operation":"read","slot":0,"start_us":55838},{"end_us":55874,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":55869}],"schema":2,"slots":[{"at_us":55874,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46b57100e0-73d1-cb","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58852,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6294,"slot":0,"token":"18e1fd46b90389f8-73d1-cc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46b90389f8-73d1-cc","watchdog":false,"workers_joined":1,"writes":[{"at_us":6309,"bytes":66,"kind":"head","slot":0},{"at_us":6314,"bytes":6,"kind":"body","slot":0},{"at_us":58956,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":106,"abi_return_us":6054,"end_us":6061,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6105,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6100},{"abi_enter_us":6124,"abi_return_us":6127,"end_us":6128,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6124,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6158,"operation":"read","slot":0,"start_us":6155},{"abi_enter_us":6172,"abi_return_us":58708,"end_us":58719,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6172,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":58811,"operation":"read","slot":0,"start_us":58807},{"end_us":58831,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58827}],"schema":2,"slots":[{"at_us":58831,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46b90389f8-73d1-cc","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":74102,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":23683,"slot":0,"token":"18e1fd46bcaf5bb8-73d1-cd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46bcaf5bb8-73d1-cd","watchdog":false,"workers_joined":1,"writes":[{"at_us":23693,"bytes":66,"kind":"head","slot":0},{"at_us":23699,"bytes":6,"kind":"body","slot":0},{"at_us":74362,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":20833,"operation":"handoff","start_us":20833},{"abi_enter_us":20870,"abi_return_us":23331,"end_us":23336,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":20870,"status":"FastlyStatus::OK","timeout_ms":479},{"end_us":23371,"operation":"head-poll","outcome":"ready","slot":0,"start_us":23366},{"abi_enter_us":23385,"abi_return_us":23393,"end_us":23395,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":23385,"status":"FastlyStatus::OK","timeout_ms":476},{"bytes":1,"capacity":16384,"end_us":23433,"operation":"read","slot":0,"start_us":23431},{"abi_enter_us":23456,"abi_return_us":73991,"end_us":73998,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":23456,"status":"FastlyStatus::OK","timeout_ms":476},{"bytes":0,"capacity":16384,"end_us":74066,"operation":"read","slot":0,"start_us":74062},{"end_us":74082,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":74078}],"schema":2,"slots":[{"at_us":74082,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46bcaf5bb8-73d1-cd","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":6328,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6441,"slot":0,"token":"18e1fd46c147f2c0-73d1-ce"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46c147f2c0-73d1-ce","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":64,"abi_return_us":6250,"end_us":6254,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6283,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6279},{"end_us":6296,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6296}],"schema":2,"slots":[{"at_us":6296,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd46c147f2c0-73d1-ce","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5100,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5226,"slot":0,"token":"18e1fd46c1c75bf0-73d1-cf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46c1c75bf0-73d1-cf","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":57,"abi_return_us":5037,"end_us":5040,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5070,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5067},{"end_us":5084,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5084}],"schema":2,"slots":[{"at_us":5084,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd46c1c75bf0-73d1-cf","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":6356,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6463,"slot":0,"token":"18e1fd46c2326e18-73d1-d0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46c2326e18-73d1-d0","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":15},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"abi_enter_us":79,"abi_return_us":6281,"end_us":6284,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6319,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6315},{"end_us":6336,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6335}],"schema":2,"slots":[{"at_us":6336,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd46c2326e18-73d1-d0","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5697,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5911,"slot":0,"token":"18e1fd46c2b64800-73d1-d1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd46c2b64800-73d1-d1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":73,"abi_return_us":5631,"end_us":5635,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5666,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5662},{"end_us":5682,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5682}],"schema":2,"slots":[{"at_us":5682,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd46c2b64800-73d1-d1","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":500812,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":6245,"slot":0,"token":"18e1fd46c32edf18-73d1-d2"},{"path":"/readiness/mixed-head-body/1","received_us":6251,"slot":1,"token":"18e1fd46c32edf18-73d1-d2"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd46c32edf18-73d1-d2","watchdog":false,"workers_joined":2,"writes":[{"at_us":6271,"bytes":61,"kind":"head","slot":1},{"at_us":6301,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":169,"operation":"dispatch","outcome":"pending","slot":1,"start_us":81},{"abi_enter_us":195,"abi_return_us":6019,"end_us":6023,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":195,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6058,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6055},{"abi_enter_us":6070,"abi_return_us":6073,"end_us":6073,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6069,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6099,"operation":"read","slot":1,"start_us":6096},{"abi_enter_us":6127,"abi_return_us":6130,"end_us":6131,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6127,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6161,"operation":"read","slot":1,"start_us":6160},{"abi_enter_us":6206,"abi_return_us":6238,"end_us":6238,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6206,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6264,"operation":"read","slot":1,"start_us":6263},{"abi_enter_us":6284,"abi_return_us":6286,"end_us":6286,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6284,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6305,"operation":"read","slot":1,"start_us":6304},{"end_us":6318,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6313},{"abi_enter_us":6329,"abi_return_us":500736,"end_us":500742,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":6329,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500795,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500795}],"schema":2,"slots":[{"at_us":500795,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":6318,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46c32edf18-73d1-d2","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":501106,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":6479,"slot":1,"token":"18e1fd46e13f2148-73d1-d3"},{"path":"/readiness/mixed-head-body/0","received_us":6485,"slot":0,"token":"18e1fd46e13f2148-73d1-d3"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd46e13f2148-73d1-d3","watchdog":false,"workers_joined":2,"writes":[{"at_us":6496,"bytes":61,"kind":"head","slot":1},{"at_us":6507,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"end_us":104,"operation":"dispatch","outcome":"pending","slot":1,"start_us":84},{"body_reads":0,"deadline_us":500000,"end_us":118,"operation":"handoff","start_us":118},{"abi_enter_us":128,"abi_return_us":6251,"end_us":6255,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":128,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6288,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6283},{"abi_enter_us":6301,"abi_return_us":6304,"end_us":6304,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6300,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6329,"operation":"read","slot":1,"start_us":6327},{"abi_enter_us":6349,"abi_return_us":6356,"end_us":6356,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6348,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6396,"operation":"read","slot":1,"start_us":6394},{"abi_enter_us":6425,"abi_return_us":6427,"end_us":6428,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6425,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6453,"operation":"read","slot":1,"start_us":6452},{"abi_enter_us":6471,"abi_return_us":6473,"end_us":6473,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6471,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6491,"operation":"read","slot":1,"start_us":6491},{"end_us":6504,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6501},{"abi_enter_us":6516,"abi_return_us":501024,"end_us":501032,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":6516,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501086,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501086}],"schema":2,"slots":[{"at_us":501086,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":6504,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46e13f2148-73d1-d3","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":501954,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":9120,"slot":0,"token":"18e1fd46ff579520-73d1-d4"},{"path":"/readiness/mixed-body-head/1","received_us":9842,"slot":1,"token":"18e1fd46ff579520-73d1-d4"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd46ff579520-73d1-d4","watchdog":false,"workers_joined":2,"writes":[{"at_us":9150,"bytes":57,"kind":"head","slot":0},{"at_us":64656,"bytes":61,"kind":"head","slot":1},{"at_us":64671,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":104,"operation":"dispatch","outcome":"pending","slot":1,"start_us":85},{"abi_enter_us":134,"abi_return_us":9370,"end_us":9377,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":134,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":9432,"operation":"head-poll","outcome":"ready","slot":0,"start_us":9423},{"abi_enter_us":9446,"abi_return_us":64487,"end_us":64493,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":9446,"status":"FastlyStatus::OK","timeout_ms":490},{"end_us":64547,"operation":"head-poll","outcome":"ready","slot":1,"start_us":64542},{"abi_enter_us":64565,"abi_return_us":64568,"end_us":64568,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64564,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":8131,"capacity":16384,"end_us":64597,"operation":"read","slot":1,"start_us":64594},{"abi_enter_us":64625,"abi_return_us":64626,"end_us":64627,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64625,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":16384,"capacity":16384,"end_us":64650,"operation":"read","slot":1,"start_us":64649},{"abi_enter_us":64703,"abi_return_us":64711,"end_us":64712,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64703,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":8254,"capacity":16384,"end_us":64764,"operation":"read","slot":1,"start_us":64761},{"abi_enter_us":64789,"abi_return_us":64792,"end_us":64792,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64789,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":0,"capacity":16384,"end_us":64813,"operation":"read","slot":1,"start_us":64813},{"end_us":64827,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":64823},{"abi_enter_us":64841,"abi_return_us":501873,"end_us":501877,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":64841,"status":"FastlyStatus::OK","timeout_ms":435},{"end_us":501935,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501928}],"schema":2,"slots":[{"at_us":501935,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":64827,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd46ff579520-73d1-d4","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":501465,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":6478,"slot":0,"token":"18e1fd471d7eed78-73d1-d5"},{"path":"/readiness/mixed-body-head/1","received_us":6488,"slot":1,"token":"18e1fd471d7eed78-73d1-d5"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd471d7eed78-73d1-d5","watchdog":false,"workers_joined":2,"writes":[{"at_us":6495,"bytes":57,"kind":"head","slot":0},{"at_us":66898,"bytes":61,"kind":"head","slot":1},{"at_us":66913,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":183,"operation":"dispatch","outcome":"pending","slot":1,"start_us":161},{"body_reads":0,"deadline_us":500000,"end_us":222,"operation":"handoff","start_us":222},{"abi_enter_us":242,"abi_return_us":6235,"end_us":6239,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":242,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6269,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6264},{"abi_enter_us":6280,"abi_return_us":66653,"end_us":66657,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":6280,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":66706,"operation":"head-poll","outcome":"ready","slot":1,"start_us":66701},{"abi_enter_us":66719,"abi_return_us":66722,"end_us":66722,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":66719,"status":"FastlyStatus::OK","timeout_ms":433},{"bytes":8131,"capacity":16384,"end_us":66763,"operation":"read","slot":1,"start_us":66760},{"abi_enter_us":66784,"abi_return_us":66787,"end_us":66788,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":66784,"status":"FastlyStatus::OK","timeout_ms":433},{"bytes":16384,"capacity":16384,"end_us":66831,"operation":"read","slot":1,"start_us":66830},{"abi_enter_us":66861,"abi_return_us":66864,"end_us":66864,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":66861,"status":"FastlyStatus::OK","timeout_ms":433},{"bytes":8254,"capacity":16384,"end_us":66897,"operation":"read","slot":1,"start_us":66896},{"abi_enter_us":66914,"abi_return_us":66916,"end_us":66917,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":66914,"status":"FastlyStatus::OK","timeout_ms":433},{"bytes":0,"capacity":16384,"end_us":66935,"operation":"read","slot":1,"start_us":66935},{"end_us":66947,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":66943},{"abi_enter_us":66967,"abi_return_us":501347,"end_us":501359,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":66967,"status":"FastlyStatus::OK","timeout_ms":433},{"end_us":501447,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501434}],"schema":2,"slots":[{"at_us":501447,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":66947,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd471d7eed78-73d1-d5","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":58830,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5990,"slot":0,"token":"18e1fd473bad6dd8-73d1-d6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd473bad6dd8-73d1-d6","watchdog":false,"workers_joined":1,"writes":[{"at_us":6008,"bytes":61,"kind":"head","slot":0},{"at_us":6022,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":106,"abi_return_us":5692,"end_us":5700,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5751,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5745},{"abi_enter_us":5764,"abi_return_us":5774,"end_us":5775,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5764,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5819,"operation":"read","slot":0,"start_us":5816},{"abi_enter_us":5842,"abi_return_us":5844,"end_us":5844,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5842,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5874,"operation":"read","slot":0,"start_us":5872},{"abi_enter_us":5908,"abi_return_us":5909,"end_us":5910,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5908,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5946,"operation":"read","slot":0,"start_us":5945},{"abi_enter_us":5968,"abi_return_us":58694,"end_us":58708,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5968,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":58792,"operation":"read","slot":0,"start_us":58788},{"end_us":58809,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":58805}],"schema":2,"slots":[{"at_us":58809,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd473bad6dd8-73d1-d6","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":60908,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":8213,"slot":0,"token":"18e1fd473f6becd8-73d1-d7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd473f6becd8-73d1-d7","watchdog":false,"workers_joined":1,"writes":[{"at_us":8235,"bytes":61,"kind":"head","slot":0},{"at_us":8248,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":87,"operation":"handoff","start_us":87},{"abi_enter_us":156,"abi_return_us":11476,"end_us":11486,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":156,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":11541,"operation":"head-poll","outcome":"ready","slot":0,"start_us":11536},{"abi_enter_us":11555,"abi_return_us":11561,"end_us":11561,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":11555,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":8131,"capacity":16384,"end_us":11591,"operation":"read","slot":0,"start_us":11588},{"abi_enter_us":11611,"abi_return_us":11620,"end_us":11621,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":11611,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":16384,"capacity":16384,"end_us":11648,"operation":"read","slot":0,"start_us":11648},{"abi_enter_us":11676,"abi_return_us":11677,"end_us":11677,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":11676,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":8254,"capacity":16384,"end_us":11696,"operation":"read","slot":0,"start_us":11696},{"abi_enter_us":11717,"abi_return_us":60777,"end_us":60780,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":11717,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":16384,"end_us":60840,"operation":"read","slot":0,"start_us":60836},{"end_us":60861,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60857}],"schema":2,"slots":[{"at_us":60861,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd473f6becd8-73d1-d7","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":501837,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":5781,"slot":0,"token":"18e1fd4743465078-73d1-d8"},{"path":"/readiness/mixed-head-body/1","received_us":5792,"slot":1,"token":"18e1fd4743465078-73d1-d8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd4743465078-73d1-d8","watchdog":false,"workers_joined":2,"writes":[{"at_us":5808,"bytes":61,"kind":"head","slot":1},{"at_us":5822,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":113,"operation":"dispatch","outcome":"pending","slot":1,"start_us":89},{"abi_enter_us":129,"abi_return_us":5548,"end_us":5552,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":129,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5594,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5588},{"abi_enter_us":5607,"abi_return_us":5611,"end_us":5611,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5606,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5638,"operation":"read","slot":1,"start_us":5636},{"abi_enter_us":5668,"abi_return_us":5671,"end_us":5671,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5668,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5699,"operation":"read","slot":1,"start_us":5698},{"abi_enter_us":5740,"abi_return_us":5743,"end_us":5743,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5740,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5774,"operation":"read","slot":1,"start_us":5773},{"abi_enter_us":5798,"abi_return_us":5800,"end_us":5800,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5798,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5822,"operation":"read","slot":1,"start_us":5822},{"end_us":5834,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5831},{"abi_enter_us":5846,"abi_return_us":501257,"end_us":501381,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":5846,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501803,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501803}],"schema":2,"slots":[{"at_us":501803,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":5834,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4743465078-73d1-d8","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":504310,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":6709,"slot":0,"token":"18e1fd47617b77f8-73d1-d9"},{"path":"/readiness/mixed-head-body/1","received_us":6723,"slot":1,"token":"18e1fd47617b77f8-73d1-d9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd47617b77f8-73d1-d9","watchdog":false,"workers_joined":2,"writes":[{"at_us":6766,"bytes":61,"kind":"head","slot":1},{"at_us":6801,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":183,"operation":"dispatch","outcome":"pending","slot":1,"start_us":173},{"body_reads":0,"deadline_us":500000,"end_us":197,"operation":"handoff","start_us":197},{"abi_enter_us":209,"abi_return_us":6289,"end_us":6296,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":209,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6344,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6339},{"abi_enter_us":6358,"abi_return_us":6370,"end_us":6371,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6358,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6402,"operation":"read","slot":1,"start_us":6399},{"abi_enter_us":6424,"abi_return_us":6426,"end_us":6426,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6424,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6484,"operation":"read","slot":1,"start_us":6483},{"abi_enter_us":6513,"abi_return_us":6515,"end_us":6516,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6513,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6539,"operation":"read","slot":1,"start_us":6538},{"abi_enter_us":6557,"abi_return_us":6559,"end_us":6559,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6557,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6582,"operation":"read","slot":1,"start_us":6582},{"end_us":6595,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6591},{"abi_enter_us":6607,"abi_return_us":501474,"end_us":501490,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":6607,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":504276,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":504275}],"schema":2,"slots":[{"at_us":504276,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":6595,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd47617b77f8-73d1-d9","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":501771,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":6400,"slot":0,"token":"18e1fd4780c68418-73d1-da"},{"path":"/readiness/mixed-body-head/1","received_us":6408,"slot":1,"token":"18e1fd4780c68418-73d1-da"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd4780c68418-73d1-da","watchdog":false,"workers_joined":2,"writes":[{"at_us":6415,"bytes":57,"kind":"head","slot":0},{"at_us":61592,"bytes":61,"kind":"head","slot":1},{"at_us":61626,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":130,"operation":"dispatch","outcome":"pending","slot":1,"start_us":99},{"abi_enter_us":170,"abi_return_us":6052,"end_us":6056,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":170,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6095,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6091},{"abi_enter_us":6110,"abi_return_us":61541,"end_us":61550,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":6110,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":61593,"operation":"head-poll","outcome":"ready","slot":1,"start_us":61588},{"abi_enter_us":61630,"abi_return_us":61638,"end_us":61640,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":61630,"status":"FastlyStatus::OK","timeout_ms":438},{"bytes":8131,"capacity":16384,"end_us":61688,"operation":"read","slot":1,"start_us":61686},{"abi_enter_us":61726,"abi_return_us":61731,"end_us":61731,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":61726,"status":"FastlyStatus::OK","timeout_ms":438},{"bytes":16384,"capacity":16384,"end_us":61763,"operation":"read","slot":1,"start_us":61761},{"abi_enter_us":61837,"abi_return_us":61841,"end_us":61841,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":61837,"status":"FastlyStatus::OK","timeout_ms":438},{"bytes":8254,"capacity":16384,"end_us":61876,"operation":"read","slot":1,"start_us":61874},{"abi_enter_us":61900,"abi_return_us":61903,"end_us":61903,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":61900,"status":"FastlyStatus::OK","timeout_ms":438},{"bytes":0,"capacity":16384,"end_us":61931,"operation":"read","slot":1,"start_us":61930},{"end_us":61945,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":61941},{"abi_enter_us":61961,"abi_return_us":501235,"end_us":501302,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":61960,"status":"FastlyStatus::OK","timeout_ms":438},{"end_us":501582,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501502}],"schema":2,"slots":[{"at_us":501582,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":61945,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4780c68418-73d1-da","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":500989,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":5452,"slot":1,"token":"18e1fd479f089830-73d1-db"},{"path":"/readiness/mixed-body-head/0","received_us":5460,"slot":0,"token":"18e1fd479f089830-73d1-db"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd479f089830-73d1-db","watchdog":false,"workers_joined":2,"writes":[{"at_us":5480,"bytes":57,"kind":"head","slot":0},{"at_us":63372,"bytes":61,"kind":"head","slot":1},{"at_us":63394,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"end_us":126,"operation":"dispatch","outcome":"pending","slot":1,"start_us":104},{"body_reads":0,"deadline_us":500000,"end_us":147,"operation":"handoff","start_us":147},{"abi_enter_us":163,"abi_return_us":5032,"end_us":5037,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":162,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5087,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5083},{"abi_enter_us":5102,"abi_return_us":62947,"end_us":62955,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":5102,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":63013,"operation":"head-poll","outcome":"ready","slot":1,"start_us":63008},{"abi_enter_us":63033,"abi_return_us":63062,"end_us":63062,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":63033,"status":"FastlyStatus::OK","timeout_ms":436},{"bytes":8131,"capacity":16384,"end_us":63106,"operation":"read","slot":1,"start_us":63103},{"abi_enter_us":63137,"abi_return_us":63140,"end_us":63141,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":63137,"status":"FastlyStatus::OK","timeout_ms":436},{"bytes":16384,"capacity":16384,"end_us":63190,"operation":"read","slot":1,"start_us":63189},{"abi_enter_us":63230,"abi_return_us":63427,"end_us":63428,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":63229,"status":"FastlyStatus::OK","timeout_ms":436},{"bytes":8254,"capacity":16384,"end_us":63469,"operation":"read","slot":1,"start_us":63468},{"abi_enter_us":63492,"abi_return_us":63495,"end_us":63496,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":63492,"status":"FastlyStatus::OK","timeout_ms":436},{"bytes":0,"capacity":16384,"end_us":63520,"operation":"read","slot":1,"start_us":63519},{"end_us":63537,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":63533},{"abi_enter_us":63553,"abi_return_us":500757,"end_us":500795,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":63553,"status":"FastlyStatus::OK","timeout_ms":436},{"end_us":500934,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500868}],"schema":2,"slots":[{"at_us":500934,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":63537,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd479f089830-73d1-db","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":56937,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6735,"slot":0,"token":"18e1fd47bdd8d2e8-73d1-dc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd47bdd8d2e8-73d1-dc","watchdog":false,"workers_joined":1,"writes":[{"at_us":6757,"bytes":61,"kind":"head","slot":0},{"at_us":6773,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":106,"abi_return_us":6392,"end_us":6399,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6463,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6457},{"abi_enter_us":6479,"abi_return_us":6483,"end_us":6484,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6479,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6515,"operation":"read","slot":0,"start_us":6512},{"abi_enter_us":6547,"abi_return_us":6549,"end_us":6549,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6547,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6574,"operation":"read","slot":0,"start_us":6573},{"abi_enter_us":6651,"abi_return_us":6653,"end_us":6653,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6651,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6846,"operation":"read","slot":0,"start_us":6845},{"abi_enter_us":6984,"abi_return_us":56782,"end_us":56790,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6984,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":56856,"operation":"read","slot":0,"start_us":56852},{"end_us":56911,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":56905}],"schema":2,"slots":[{"at_us":56911,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd47bdd8d2e8-73d1-dc","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":71804,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":7282,"slot":0,"token":"18e1fd47c247f0c0-73d1-dd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd47c247f0c0-73d1-dd","watchdog":false,"workers_joined":1,"writes":[{"at_us":7480,"bytes":61,"kind":"head","slot":0},{"at_us":7498,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":519,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":898,"operation":"handoff","start_us":898},{"abi_enter_us":912,"abi_return_us":4912,"end_us":4917,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":912,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4953,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4948},{"abi_enter_us":4965,"abi_return_us":4970,"end_us":4970,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4965,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":8131,"capacity":16384,"end_us":5084,"operation":"read","slot":0,"start_us":5081},{"abi_enter_us":5114,"abi_return_us":5117,"end_us":5117,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5114,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5259,"operation":"read","slot":0,"start_us":5254},{"abi_enter_us":5319,"abi_return_us":5342,"end_us":5344,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5318,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5417,"operation":"read","slot":0,"start_us":5415},{"abi_enter_us":5441,"abi_return_us":57305,"end_us":57308,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5441,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":71755,"operation":"read","slot":0,"start_us":71746},{"end_us":71778,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":71771}],"schema":2,"slots":[{"at_us":71778,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd47c247f0c0-73d1-dd","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":502076,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":6814,"slot":1,"token":"18e1fd47c8146830-73d1-de"},{"path":"/readiness/mixed-head-body/0","received_us":6993,"slot":0,"token":"18e1fd47c8146830-73d1-de"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd47c8146830-73d1-de","watchdog":false,"workers_joined":2,"writes":[{"at_us":6841,"bytes":61,"kind":"head","slot":1},{"at_us":6860,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"end_us":123,"operation":"dispatch","outcome":"pending","slot":1,"start_us":104},{"abi_enter_us":232,"abi_return_us":6100,"end_us":6106,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":232,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6156,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6151},{"abi_enter_us":6172,"abi_return_us":6176,"end_us":6177,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6171,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6209,"operation":"read","slot":1,"start_us":6206},{"abi_enter_us":6345,"abi_return_us":6347,"end_us":6347,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6345,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6377,"operation":"read","slot":1,"start_us":6376},{"abi_enter_us":6550,"abi_return_us":6553,"end_us":6553,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6550,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6584,"operation":"read","slot":1,"start_us":6583},{"abi_enter_us":6612,"abi_return_us":6614,"end_us":6614,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6612,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6656,"operation":"read","slot":1,"start_us":6655},{"end_us":6670,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6666},{"abi_enter_us":6685,"abi_return_us":501551,"end_us":501687,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":6685,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":502054,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502054}],"schema":2,"slots":[{"at_us":502054,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":6670,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd47c8146830-73d1-de","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":593907,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":6770,"slot":0,"token":"18e1fd47e6c39210-73d1-df"},{"path":"/readiness/mixed-head-body/1","received_us":6880,"slot":1,"token":"18e1fd47e6c39210-73d1-df"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd47e6c39210-73d1-df","watchdog":false,"workers_joined":2,"writes":[{"at_us":6902,"bytes":61,"kind":"head","slot":1},{"at_us":6921,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":351,"operation":"dispatch","outcome":"pending","slot":0,"start_us":113},{"end_us":625,"operation":"dispatch","outcome":"pending","slot":1,"start_us":560},{"body_reads":0,"deadline_us":500000,"end_us":700,"operation":"handoff","start_us":700},{"abi_enter_us":733,"abi_return_us":5946,"end_us":5955,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":732,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6101,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6062},{"abi_enter_us":6156,"abi_return_us":6163,"end_us":6164,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6156,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6247,"operation":"read","slot":1,"start_us":6243},{"abi_enter_us":6288,"abi_return_us":6292,"end_us":6292,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6288,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6343,"operation":"read","slot":1,"start_us":6341},{"abi_enter_us":6407,"abi_return_us":6555,"end_us":6557,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6407,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6611,"operation":"read","slot":1,"start_us":6608},{"abi_enter_us":6635,"abi_return_us":6639,"end_us":6639,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6634,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6664,"operation":"read","slot":1,"start_us":6663},{"end_us":6684,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6680},{"abi_enter_us":6700,"abi_return_us":501822,"end_us":501836,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":6700,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":593865,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":593865}],"schema":2,"slots":[{"at_us":593865,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":6684,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd47e6c39210-73d1-df","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":501042,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":6626,"slot":1,"token":"18e1fd480a9f6920-73d1-e0"},{"path":"/readiness/mixed-body-head/0","received_us":6634,"slot":0,"token":"18e1fd480a9f6920-73d1-e0"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd480a9f6920-73d1-e0","watchdog":false,"workers_joined":2,"writes":[{"at_us":6651,"bytes":57,"kind":"head","slot":0},{"at_us":65942,"bytes":61,"kind":"head","slot":1},{"at_us":65963,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"end_us":153,"operation":"dispatch","outcome":"pending","slot":1,"start_us":108},{"abi_enter_us":175,"abi_return_us":6306,"end_us":6312,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":175,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6358,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6349},{"abi_enter_us":6373,"abi_return_us":65619,"end_us":65623,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":6373,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":65679,"operation":"head-poll","outcome":"ready","slot":1,"start_us":65673},{"abi_enter_us":65696,"abi_return_us":65702,"end_us":65702,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":65696,"status":"FastlyStatus::OK","timeout_ms":434},{"bytes":8131,"capacity":16384,"end_us":65771,"operation":"read","slot":1,"start_us":65766},{"abi_enter_us":65803,"abi_return_us":65809,"end_us":65811,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":65803,"status":"FastlyStatus::OK","timeout_ms":434},{"bytes":16384,"capacity":16384,"end_us":65882,"operation":"read","slot":1,"start_us":65880},{"abi_enter_us":65933,"abi_return_us":65937,"end_us":65937,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":65933,"status":"FastlyStatus::OK","timeout_ms":434},{"bytes":8254,"capacity":16384,"end_us":65969,"operation":"read","slot":1,"start_us":65968},{"abi_enter_us":65997,"abi_return_us":66002,"end_us":66003,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":65997,"status":"FastlyStatus::OK","timeout_ms":434},{"bytes":0,"capacity":16384,"end_us":66028,"operation":"read","slot":1,"start_us":66028},{"end_us":66074,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":66066},{"abi_enter_us":66096,"abi_return_us":500875,"end_us":500887,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":66096,"status":"FastlyStatus::OK","timeout_ms":433},{"end_us":500980,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500966}],"schema":2,"slots":[{"at_us":500980,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":66074,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd480a9f6920-73d1-e0","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":500953,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":5854,"slot":0,"token":"18e1fd4829883cb8-73d1-e1"},{"path":"/readiness/mixed-body-head/1","received_us":6043,"slot":1,"token":"18e1fd4829883cb8-73d1-e1"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd4829883cb8-73d1-e1","watchdog":false,"workers_joined":2,"writes":[{"at_us":5888,"bytes":57,"kind":"head","slot":0},{"at_us":57638,"bytes":61,"kind":"head","slot":1},{"at_us":57666,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":692,"operation":"dispatch","outcome":"pending","slot":0,"start_us":44},{"end_us":773,"operation":"dispatch","outcome":"pending","slot":1,"start_us":753},{"body_reads":0,"deadline_us":500000,"end_us":792,"operation":"handoff","start_us":792},{"abi_enter_us":808,"abi_return_us":4783,"end_us":4793,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":807,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4870,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4863},{"abi_enter_us":4896,"abi_return_us":56613,"end_us":56623,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":4896,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":56710,"operation":"head-poll","outcome":"ready","slot":1,"start_us":56703},{"abi_enter_us":56736,"abi_return_us":56742,"end_us":56742,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":56736,"status":"FastlyStatus::OK","timeout_ms":443},{"bytes":8131,"capacity":16384,"end_us":56878,"operation":"read","slot":1,"start_us":56873},{"abi_enter_us":56909,"abi_return_us":56916,"end_us":56917,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":56909,"status":"FastlyStatus::OK","timeout_ms":443},{"bytes":16384,"capacity":16384,"end_us":56985,"operation":"read","slot":1,"start_us":56983},{"abi_enter_us":57021,"abi_return_us":57024,"end_us":57024,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":57021,"status":"FastlyStatus::OK","timeout_ms":442},{"bytes":8254,"capacity":16384,"end_us":57050,"operation":"read","slot":1,"start_us":57050},{"abi_enter_us":57074,"abi_return_us":57076,"end_us":57077,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":57074,"status":"FastlyStatus::OK","timeout_ms":442},{"bytes":0,"capacity":16384,"end_us":57101,"operation":"read","slot":1,"start_us":57100},{"end_us":57117,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":57113},{"abi_enter_us":57135,"abi_return_us":500867,"end_us":500871,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":57135,"status":"FastlyStatus::OK","timeout_ms":442},{"end_us":500934,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500928}],"schema":2,"slots":[{"at_us":500934,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":57117,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4829883cb8-73d1-e1","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":59204,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5226,"slot":0,"token":"18e1fd4847cefc20-73d1-e2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd4847cefc20-73d1-e2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5243,"bytes":61,"kind":"head","slot":0},{"at_us":5256,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":76,"abi_return_us":4909,"end_us":4912,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4955,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4951},{"abi_enter_us":4973,"abi_return_us":4978,"end_us":4979,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4972,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":8131,"capacity":16384,"end_us":5012,"operation":"read","slot":0,"start_us":5009},{"abi_enter_us":5046,"abi_return_us":5050,"end_us":5050,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5046,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5091,"operation":"read","slot":0,"start_us":5088},{"abi_enter_us":5140,"abi_return_us":5142,"end_us":5142,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5140,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5177,"operation":"read","slot":0,"start_us":5175},{"abi_enter_us":5203,"abi_return_us":59080,"end_us":59085,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5203,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":59160,"operation":"read","slot":0,"start_us":59156},{"end_us":59184,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":59179}],"schema":2,"slots":[{"at_us":59184,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd4847cefc20-73d1-e2","tolerance_us":5000}} +{"run":"stock-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":60188,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5499,"slot":0,"token":"18e1fd484ba72958-73d1-e3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd484ba72958-73d1-e3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5522,"bytes":61,"kind":"head","slot":0},{"at_us":5540,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":103,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":160,"operation":"handoff","start_us":160},{"abi_enter_us":177,"abi_return_us":5097,"end_us":5111,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":176,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5197,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5189},{"abi_enter_us":5229,"abi_return_us":5244,"end_us":5245,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5229,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5343,"operation":"read","slot":0,"start_us":5336},{"abi_enter_us":5417,"abi_return_us":5424,"end_us":5425,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5416,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5489,"operation":"read","slot":0,"start_us":5463},{"abi_enter_us":5536,"abi_return_us":5539,"end_us":5546,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5536,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5579,"operation":"read","slot":0,"start_us":5577},{"abi_enter_us":5603,"abi_return_us":60069,"end_us":60073,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5603,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":60147,"operation":"read","slot":0,"start_us":60143},{"end_us":60167,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":60162}],"schema":2,"slots":[{"at_us":60167,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd484ba72958-73d1-e3","tolerance_us":5000}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1013,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":13,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3bcbd43948-73d1-3","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2283,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2283,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1283,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3bcbe79650-73d1-4","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2323,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1323,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3bcc1c0c00-73d1-5","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101931,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1931,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3bcc4c3bf0-73d1-6","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101067,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101067,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1067,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3bd2646e18-73d1-7","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101939,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1939,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3bd87c8100-73d1-8","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500212,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":212,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3bdea11ed8-73d1-9","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":500904,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":500904,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":904,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3bfc767318-73d1-a","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501542,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1542,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3c1a6dca60-73d1-b","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1014,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":14,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3c38653d00-73d1-c","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2268,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2268,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1268,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3c38779c20-73d1-d","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2286,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1286,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3c38a65510-73d1-e","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103496,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3496,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3c38d2fea8-73d1-f","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102122,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102122,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2122,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3c3f036420-73d1-10","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":100326,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":326,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3c452c6ec8-73d1-11","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503682,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3682,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3c4b367bd8-73d1-12","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502164,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502164,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2164,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3c6940e5f0-73d1-13","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501926,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1926,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3c8744eb50-73d1-14","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1256,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":256,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3ca5416ae8-73d1-15","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2281,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2281,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1281,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3ca557d918-73d1-16","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2091,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1091,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3ca588eb98-73d1-17","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103086,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3086,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3ca5b2c288-73d1-18","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":103445,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":103445,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3445,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3cabdd1938-73d1-19","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102085,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2085,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3cb218bd98-73d1-1a","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":505017,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":5017,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3cb83fcc70-73d1-1b","within_five_ms":false}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":500912,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":500912,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":912,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3cd65edf98-73d1-1c","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502075,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2075,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3cf45fb0a8-73d1-1d","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1046,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":46,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3d125e3bb0-73d1-1e","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2074,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2074,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1074,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3d12747b00-73d1-1f","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2273,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1273,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3d12a487c8-73d1-20","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103678,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3678,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3d12d5d8c8-73d1-21","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102348,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102348,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2348,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3d190962f0-73d1-22","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101144,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1144,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3d1f369030-73d1-23","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":505011,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":5011,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3d254ce9b0-73d1-24","within_five_ms":false}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502581,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502581,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2581,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3d436b7038-73d1-25","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501940,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1940,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3d61b02fe8-73d1-26","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1011,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":11,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3d7facee00-73d1-27","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2271,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2271,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1271,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3d7fc004b8-73d1-28","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2069,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1069,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3d7ff341e8-73d1-29","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":104474,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4474,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3d801ee1e0-73d1-2a","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102394,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102394,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2394,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3d865ffcb0-73d1-2b","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101248,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1248,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3d8c890370-73d1-2c","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501845,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1845,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3d92a6e6a0-73d1-2d","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501859,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501859,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1859,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3db0951240-73d1-2e","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501452,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1452,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3dce919d90-73d1-2f","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1121,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":121,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3deca3c038-73d1-30","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2282,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2282,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1282,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3decb8ced8-73d1-31","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2062,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1062,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3decfb2b20-73d1-32","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":100892,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":892,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3ded407d38-73d1-33","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101455,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101455,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1455,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3df34b22a0-73d1-34","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101649,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1649,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3df977a7e8-73d1-35","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504990,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4990,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3dff95e8d8-73d1-36","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501740,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501740,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1740,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3e1db39888-73d1-37","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502019,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2019,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3e3bbf5290-73d1-38","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1010,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":10,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3e59b9f1b0-73d1-39","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2281,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2281,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1281,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3e59cc9720-73d1-3a","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2283,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1283,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3e59fe41f8-73d1-3b","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":104449,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4449,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3e5a2be978-73d1-3c","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101196,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101196,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1196,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3e606aa6d0-73d1-3d","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102071,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2071,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3e668efe58-73d1-3e","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503088,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3088,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3e6cb91688-73d1-3f","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502208,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502208,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2208,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3e8abced08-73d1-40","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":500684,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":684,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3ea8bc7210-73d1-41","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1179,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":179,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3ec6a20678-73d1-42","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":1058,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":1058,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":58,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3ec6b841e0-73d1-43","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2274,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1274,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3ec6d4baf0-73d1-44","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102927,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2927,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3ec701d1e8-73d1-45","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101780,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101780,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1780,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3ecd3e83d0-73d1-46","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102181,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2181,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3ed3705878-73d1-47","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502527,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2527,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd3ed9992c70-73d1-48","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501084,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501084,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1084,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd3ef7913f38-73d1-49","within_five_ms":true}} +{"run":"stock-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502097,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2097,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd3f157df3d8-73d1-4a","within_five_ms":true}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":61014,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":11897,"slot":0,"token":"18e1fd593b758fc8-89dc-4b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd593b758fc8-89dc-4b","watchdog":false,"workers_joined":1,"writes":[{"at_us":11905,"bytes":57,"kind":"head","slot":0},{"at_us":11911,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":4561,"operation":"dispatch","outcome":"pending","slot":0,"start_us":1229},{"abi_enter_us":4692,"abi_return_us":9341,"end_us":9347,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":4692,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":9849,"operation":"head-poll","outcome":"ready","slot":0,"start_us":9398},{"bytes":1,"capacity":1,"end_us":9877,"operation":"read","slot":0,"start_us":9873},{"capacity":1,"end_us":60936,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":9913},{"end_us":60997,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60990}],"schema":2,"slots":[{"at_us":60997,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd593b758fc8-89dc-4b","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":55628,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5257,"slot":0,"token":"18e1fd593f9d8ab0-89dc-4c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd593f9d8ab0-89dc-4c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5273,"bytes":57,"kind":"head","slot":0},{"at_us":5279,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":70,"operation":"dispatch","outcome":"pending","slot":0,"start_us":41},{"abi_enter_us":174,"abi_return_us":4973,"end_us":4980,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":174,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5034,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5027},{"abi_enter_us":5048,"abi_return_us":5054,"end_us":5054,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5048,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5086,"operation":"read","slot":0,"start_us":5084},{"abi_enter_us":5098,"abi_return_us":55501,"end_us":55505,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5098,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":55570,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":55565},{"end_us":55611,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55605}],"schema":2,"slots":[{"at_us":55611,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd593f9d8ab0-89dc-4c","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":58056,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5597,"slot":0,"token":"18e1fd59433e3110-89dc-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd59433e3110-89dc-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5610,"bytes":57,"kind":"head","slot":0},{"at_us":5616,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":123,"abi_return_us":4559,"end_us":4565,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":123,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4612,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4606},{"abi_enter_us":4625,"abi_return_us":4632,"end_us":4632,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4625,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4659,"operation":"read","slot":0,"start_us":4657},{"abi_enter_us":4676,"abi_return_us":57938,"end_us":57942,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4676,"status":"FastlyStatus::OK","timeout_ms":495},{"capacity":16384,"end_us":58003,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57998},{"end_us":58038,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58032}],"schema":2,"slots":[{"at_us":58038,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd59433e3110-89dc-4d","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":56073,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6526,"slot":0,"token":"18e1fd59473f3b38-89dc-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd59473f3b38-89dc-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":6621,"bytes":57,"kind":"head","slot":0},{"at_us":6634,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":70,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":118,"operation":"handoff","start_us":118},{"abi_enter_us":132,"abi_return_us":2978,"end_us":2984,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":132,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":3105,"operation":"head-poll","outcome":"ready","slot":0,"start_us":3098},{"abi_enter_us":3135,"abi_return_us":3140,"end_us":3141,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":3135,"status":"FastlyStatus::OK","timeout_ms":496},{"bytes":1,"capacity":1,"end_us":3171,"operation":"read","slot":0,"start_us":3168},{"abi_enter_us":3183,"abi_return_us":55859,"end_us":55862,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":3183,"status":"FastlyStatus::OK","timeout_ms":496},{"capacity":1,"end_us":56029,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56021},{"end_us":56055,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56048}],"schema":2,"slots":[{"at_us":56055,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd59473f3b38-89dc-4e","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":57147,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5996,"slot":0,"token":"18e1fd594b24e978-89dc-4f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd594b24e978-89dc-4f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6008,"bytes":57,"kind":"head","slot":0},{"at_us":6014,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"abi_enter_us":96,"abi_return_us":5778,"end_us":5788,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5830,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5821},{"abi_enter_us":5845,"abi_return_us":5851,"end_us":5855,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5844,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5885,"operation":"read","slot":0,"start_us":5883},{"abi_enter_us":5911,"abi_return_us":57033,"end_us":57037,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5911,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57108,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57103},{"end_us":57132,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57125}],"schema":2,"slots":[{"at_us":57132,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd594b24e978-89dc-4f","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":500748,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5620,"slot":0,"token":"18e1fd594ed59568-89dc-50"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd594ed59568-89dc-50","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":36},{"abi_enter_us":99,"abi_return_us":500661,"end_us":500672,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":99,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500730,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500730}],"schema":2,"slots":[{"at_us":500730,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd594ed59568-89dc-50","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501172,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6441,"slot":0,"token":"18e1fd596cd22c70-89dc-51"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd596cd22c70-89dc-51","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":73,"abi_return_us":501084,"end_us":501095,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501150,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501150}],"schema":2,"slots":[{"at_us":501150,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd596cd22c70-89dc-51","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":500765,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5217,"slot":0,"token":"18e1fd598af7a450-89dc-52"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd598af7a450-89dc-52","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":88,"abi_return_us":500556,"end_us":500567,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500722,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500722}],"schema":2,"slots":[{"at_us":500722,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd598af7a450-89dc-52","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501010,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6361,"slot":0,"token":"18e1fd59a8fede00-89dc-53"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd59a8fede00-89dc-53","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":66,"operation":"handoff","start_us":66},{"abi_enter_us":78,"abi_return_us":500920,"end_us":500930,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500991,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500991}],"schema":2,"slots":[{"at_us":500991,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd59a8fede00-89dc-53","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":501196,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6632,"slot":0,"token":"18e1fd59c72407c0-89dc-54"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd59c72407c0-89dc-54","watchdog":false,"workers_joined":1,"writes":[{"at_us":6660,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":59,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":103,"abi_return_us":6229,"end_us":6246,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":103,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6347,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6327},{"abi_enter_us":6380,"abi_return_us":501057,"end_us":501062,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6380,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501136,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501126}],"schema":2,"slots":[{"at_us":501136,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd59c72407c0-89dc-54","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":501769,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5916,"slot":0,"token":"18e1fd59e5419448-89dc-55"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd59e5419448-89dc-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":5937,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"abi_enter_us":88,"abi_return_us":5677,"end_us":5680,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":88,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5716,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5709},{"abi_enter_us":5730,"abi_return_us":501624,"end_us":501633,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5730,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501725,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501703}],"schema":2,"slots":[{"at_us":501725,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd59e5419448-89dc-55","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":504369,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6494,"slot":0,"token":"18e1fd5a03563f60-89dc-56"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5a03563f60-89dc-56","watchdog":false,"workers_joined":1,"writes":[{"at_us":6506,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":70,"operation":"dispatch","outcome":"pending","slot":0,"start_us":30},{"body_reads":0,"deadline_us":500000,"end_us":126,"operation":"handoff","start_us":126},{"abi_enter_us":163,"abi_return_us":6487,"end_us":6494,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":163,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6549,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6542},{"abi_enter_us":6563,"abi_return_us":504241,"end_us":504246,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6563,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":504337,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":504325}],"schema":2,"slots":[{"at_us":504337,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5a03563f60-89dc-56","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":501422,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":8736,"slot":0,"token":"18e1fd5a21b01968-89dc-57"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5a21b01968-89dc-57","watchdog":false,"workers_joined":1,"writes":[{"at_us":8757,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":838,"operation":"handoff","start_us":838},{"abi_enter_us":855,"abi_return_us":3584,"end_us":3590,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":854,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":3657,"operation":"head-poll","outcome":"ready","slot":0,"start_us":3650},{"abi_enter_us":3682,"abi_return_us":501082,"end_us":501104,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":3682,"status":"FastlyStatus::OK","timeout_ms":496},{"end_us":501344,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501319}],"schema":2,"slots":[{"at_us":501344,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5a21b01968-89dc-57","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":502286,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6495,"slot":0,"token":"18e1fd5a4027cc10-89dc-58"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5a4027cc10-89dc-58","watchdog":false,"workers_joined":1,"writes":[{"at_us":6513,"bytes":57,"kind":"head","slot":0},{"at_us":6522,"bytes":1,"kind":"body","slot":0},{"at_us":307845,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":64,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"abi_enter_us":114,"abi_return_us":6093,"end_us":6100,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":114,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6158,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6151},{"abi_enter_us":6179,"abi_return_us":6187,"end_us":6188,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6179,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6232,"operation":"read","slot":0,"start_us":6229},{"abi_enter_us":6250,"abi_return_us":307443,"end_us":307448,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6250,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":307520,"operation":"read","slot":0,"start_us":307517},{"abi_enter_us":307538,"abi_return_us":502032,"end_us":502038,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307538,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":502227,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502212}],"schema":2,"slots":[{"at_us":502227,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5a4027cc10-89dc-58","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500351,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6428,"slot":0,"token":"18e1fd5a5e8858f0-89dc-59"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5a5e8858f0-89dc-59","watchdog":false,"workers_joined":1,"writes":[{"at_us":6446,"bytes":57,"kind":"head","slot":0},{"at_us":6455,"bytes":1,"kind":"body","slot":0},{"at_us":310428,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":31},{"abi_enter_us":171,"abi_return_us":5976,"end_us":5983,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":170,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6040,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6032},{"abi_enter_us":6062,"abi_return_us":6070,"end_us":6071,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6061,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6108,"operation":"read","slot":0,"start_us":6105},{"abi_enter_us":6129,"abi_return_us":309946,"end_us":309952,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6129,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":310028,"operation":"read","slot":0,"start_us":310024},{"abi_enter_us":310066,"abi_return_us":500194,"end_us":500201,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310065,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":500306,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500283}],"schema":2,"slots":[{"at_us":500306,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5a5e8858f0-89dc-59","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500239,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5543,"slot":0,"token":"18e1fd5a7caf5b58-89dc-5a"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5a7caf5b58-89dc-5a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5553,"bytes":57,"kind":"head","slot":0},{"at_us":5559,"bytes":1,"kind":"body","slot":0},{"at_us":307311,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":87,"operation":"handoff","start_us":87},{"abi_enter_us":107,"abi_return_us":5251,"end_us":5255,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5294,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5287},{"abi_enter_us":5310,"abi_return_us":5315,"end_us":5316,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5309,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5349,"operation":"read","slot":0,"start_us":5346},{"abi_enter_us":5375,"abi_return_us":307007,"end_us":307010,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5375,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":307081,"operation":"read","slot":0,"start_us":307077},{"abi_enter_us":307095,"abi_return_us":500150,"end_us":500154,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307095,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500217,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500209}],"schema":2,"slots":[{"at_us":500217,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5a7caf5b58-89dc-5a","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":501302,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5232,"slot":0,"token":"18e1fd5a9ab64eb8-89dc-5b"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5a9ab64eb8-89dc-5b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5242,"bytes":57,"kind":"head","slot":0},{"at_us":5248,"bytes":1,"kind":"body","slot":0},{"at_us":308494,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":64,"operation":"handoff","start_us":64},{"abi_enter_us":76,"abi_return_us":4940,"end_us":4943,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4975,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4967},{"abi_enter_us":4990,"abi_return_us":4994,"end_us":4994,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4989,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":5023,"operation":"read","slot":0,"start_us":5021},{"abi_enter_us":5037,"abi_return_us":308194,"end_us":308198,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5037,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":308271,"operation":"read","slot":0,"start_us":308267},{"abi_enter_us":308286,"abi_return_us":501210,"end_us":501217,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308286,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501281,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501274}],"schema":2,"slots":[{"at_us":501281,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5a9ab64eb8-89dc-5b","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501429,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6257,"slot":0,"token":"18e1fd5ab8c1e1b0-89dc-5c"},{"path":"/readiness/siblings/1","received_us":6266,"slot":1,"token":"18e1fd5ab8c1e1b0-89dc-5c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd5ab8c1e1b0-89dc-5c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6272,"bytes":57,"kind":"head","slot":0},{"at_us":6283,"bytes":57,"kind":"head","slot":1},{"at_us":6289,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":95,"operation":"dispatch","outcome":"pending","slot":1,"start_us":81},{"abi_enter_us":118,"abi_return_us":6003,"end_us":6007,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":118,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6053,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6045},{"abi_enter_us":6066,"abi_return_us":6068,"end_us":6068,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6066,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6083,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6082},{"abi_enter_us":6092,"abi_return_us":6096,"end_us":6096,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6092,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6120,"operation":"read","slot":1,"start_us":6117},{"abi_enter_us":6132,"abi_return_us":6133,"end_us":6133,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6132,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6152,"operation":"read","slot":1,"start_us":6151},{"abi_enter_us":6161,"abi_return_us":6162,"end_us":6162,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6161,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6181,"operation":"read","slot":1,"start_us":6181},{"abi_enter_us":6190,"abi_return_us":6192,"end_us":6193,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6190,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6215,"operation":"read","slot":1,"start_us":6214},{"end_us":6226,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6223},{"abi_enter_us":6237,"abi_return_us":501241,"end_us":501245,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6236,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501400,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501391}],"schema":2,"slots":[{"at_us":501400,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6226,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5ab8c1e1b0-89dc-5c","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":500098,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5750,"slot":0,"token":"18e1fd5ad6e87e88-89dc-5d"},{"path":"/readiness/siblings/1","received_us":6259,"slot":1,"token":"18e1fd5ad6e87e88-89dc-5d"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd5ad6e87e88-89dc-5d","watchdog":false,"workers_joined":2,"writes":[{"at_us":5769,"bytes":57,"kind":"head","slot":0},{"at_us":6272,"bytes":57,"kind":"head","slot":1},{"at_us":6278,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":135,"operation":"dispatch","outcome":"pending","slot":1,"start_us":108},{"abi_enter_us":166,"abi_return_us":5735,"end_us":5739,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":166,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5793,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5786},{"abi_enter_us":5808,"abi_return_us":5920,"end_us":5922,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5808,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5979,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5971},{"abi_enter_us":5997,"abi_return_us":6004,"end_us":6004,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5997,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":6038,"operation":"read","slot":1,"start_us":6036},{"abi_enter_us":6055,"abi_return_us":6058,"end_us":6058,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6054,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6084,"operation":"read","slot":1,"start_us":6083},{"end_us":6098,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6094},{"abi_enter_us":6118,"abi_return_us":499981,"end_us":499987,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6118,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500064,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500055}],"schema":2,"slots":[{"at_us":500064,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6098,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5ad6e87e88-89dc-5d","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501363,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5390,"slot":1,"token":"18e1fd5af4ee7fb8-89dc-5e"},{"path":"/readiness/siblings/0","received_us":5402,"slot":0,"token":"18e1fd5af4ee7fb8-89dc-5e"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd5af4ee7fb8-89dc-5e","watchdog":false,"workers_joined":2,"writes":[{"at_us":5407,"bytes":57,"kind":"head","slot":1},{"at_us":5415,"bytes":57,"kind":"head","slot":0},{"at_us":11702,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"end_us":148,"operation":"dispatch","outcome":"pending","slot":1,"start_us":116},{"body_reads":0,"deadline_us":500000,"end_us":192,"operation":"handoff","start_us":192},{"abi_enter_us":207,"abi_return_us":5122,"end_us":5127,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":207,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5169,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5163},{"abi_enter_us":5184,"abi_return_us":5187,"end_us":5187,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5184,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5208,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5207},{"abi_enter_us":5220,"abi_return_us":11385,"end_us":11388,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5220,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":11441,"operation":"read","slot":1,"start_us":11438},{"abi_enter_us":11458,"abi_return_us":11462,"end_us":11462,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11458,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11484,"operation":"read","slot":1,"start_us":11483},{"abi_enter_us":11494,"abi_return_us":11495,"end_us":11495,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11494,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11516,"operation":"read","slot":1,"start_us":11516},{"abi_enter_us":11526,"abi_return_us":11528,"end_us":11529,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11526,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11550,"operation":"read","slot":1,"start_us":11550},{"end_us":11564,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11559},{"abi_enter_us":11596,"abi_return_us":501264,"end_us":501270,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11595,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501340,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501333}],"schema":2,"slots":[{"at_us":501340,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11564,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5af4ee7fb8-89dc-5e","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":502217,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6125,"slot":0,"token":"18e1fd5b1316fd08-89dc-5f"},{"path":"/readiness/siblings/1","received_us":6157,"slot":1,"token":"18e1fd5b1316fd08-89dc-5f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd5b1316fd08-89dc-5f","watchdog":false,"workers_joined":2,"writes":[{"at_us":6136,"bytes":57,"kind":"head","slot":0},{"at_us":6162,"bytes":57,"kind":"head","slot":1},{"at_us":6167,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":104,"operation":"dispatch","outcome":"pending","slot":1,"start_us":87},{"body_reads":0,"deadline_us":500000,"end_us":119,"operation":"handoff","start_us":119},{"abi_enter_us":132,"abi_return_us":5840,"end_us":5843,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":132,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5877,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5871},{"abi_enter_us":5972,"abi_return_us":5995,"end_us":5996,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5972,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":6017,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6015},{"abi_enter_us":6029,"abi_return_us":6033,"end_us":6033,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6028,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6067,"operation":"read","slot":1,"start_us":6065},{"abi_enter_us":6078,"abi_return_us":6080,"end_us":6081,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6078,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6103,"operation":"read","slot":1,"start_us":6102},{"end_us":6116,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6112},{"abi_enter_us":6135,"abi_return_us":499752,"end_us":499768,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6135,"status":"FastlyStatus::OK","timeout_ms":493},{"abi_enter_us":499836,"abi_return_us":502124,"end_us":502127,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":499836,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502188,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502173}],"schema":2,"slots":[{"at_us":502188,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6116,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5b1316fd08-89dc-5f","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501863,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6089,"slot":0,"token":"18e1fd5b313dd090-89dc-60"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5b313dd090-89dc-60","watchdog":false,"workers_joined":1,"writes":[{"at_us":6109,"bytes":57,"kind":"head","slot":0},{"at_us":6116,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":89,"abi_return_us":5793,"end_us":5797,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5851,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5845},{"abi_enter_us":5870,"abi_return_us":5876,"end_us":5876,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5870,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5911,"operation":"read","slot":0,"start_us":5909},{"abi_enter_us":5924,"abi_return_us":501735,"end_us":501742,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5924,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501842,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501835}],"schema":2,"slots":[{"at_us":501842,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5b313dd090-89dc-60","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501930,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5832,"slot":0,"token":"18e1fd5b4f501278-89dc-61"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd5b4f501278-89dc-61","watchdog":false,"workers_joined":1,"writes":[{"at_us":5849,"bytes":57,"kind":"head","slot":0},{"at_us":5854,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":91,"abi_return_us":5530,"end_us":5535,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5585,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5580},{"abi_enter_us":5600,"abi_return_us":5605,"end_us":5606,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5600,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5640,"operation":"read","slot":0,"start_us":5637},{"abi_enter_us":5655,"abi_return_us":501761,"end_us":501772,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5655,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501890,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501861}],"schema":2,"slots":[{"at_us":501890,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5b4f501278-89dc-61","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501366,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6436,"slot":0,"token":"18e1fd5b6d81afb8-89dc-62"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5b6d81afb8-89dc-62","watchdog":false,"workers_joined":1,"writes":[{"at_us":6447,"bytes":57,"kind":"head","slot":0},{"at_us":6456,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":99,"abi_return_us":6107,"end_us":6113,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6159,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6153},{"abi_enter_us":6172,"abi_return_us":6180,"end_us":6181,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6172,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6210,"operation":"read","slot":0,"start_us":6208},{"abi_enter_us":6223,"abi_return_us":501264,"end_us":501272,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6222,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501344,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501335}],"schema":2,"slots":[{"at_us":501344,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5b6d81afb8-89dc-62","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501267,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5211,"slot":0,"token":"18e1fd5b8b98a4c0-89dc-63"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd5b8b98a4c0-89dc-63","watchdog":false,"workers_joined":1,"writes":[{"at_us":5224,"bytes":57,"kind":"head","slot":0},{"at_us":5229,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":85,"operation":"handoff","start_us":85},{"abi_enter_us":108,"abi_return_us":4932,"end_us":4935,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":108,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4989,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4978},{"abi_enter_us":5006,"abi_return_us":5010,"end_us":5011,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5005,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5051,"operation":"read","slot":0,"start_us":5048},{"abi_enter_us":5066,"abi_return_us":501161,"end_us":501167,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5065,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501241,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501230}],"schema":2,"slots":[{"at_us":501241,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5b8b98a4c0-89dc-63","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5278,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5355,"slot":0,"token":"18e1fd5ba9a6c028-89dc-64"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5ba9a6c028-89dc-64","watchdog":false,"workers_joined":1,"writes":[{"at_us":5369,"bytes":57,"kind":"head","slot":0},{"at_us":5375,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":96,"abi_return_us":5047,"end_us":5052,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5097,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5091},{"abi_enter_us":5111,"abi_return_us":5115,"end_us":5116,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5110,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5144,"operation":"read","slot":0,"start_us":5141},{"abi_enter_us":5159,"abi_return_us":5160,"end_us":5160,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5159,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5183,"operation":"read","slot":0,"start_us":5182},{"abi_enter_us":5193,"abi_return_us":5193,"end_us":5194,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5193,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5218,"operation":"read","slot":0,"start_us":5218},{"abi_enter_us":5230,"abi_return_us":5231,"end_us":5231,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5229,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5251,"operation":"read","slot":0,"start_us":5250},{"end_us":5263,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5260}],"schema":2,"slots":[{"at_us":5263,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5ba9a6c028-89dc-64","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":6236,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6310,"slot":0,"token":"18e1fd5baa2e66b8-89dc-65"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5baa2e66b8-89dc-65","watchdog":false,"workers_joined":1,"writes":[{"at_us":6321,"bytes":57,"kind":"head","slot":0},{"at_us":6328,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":85,"abi_return_us":6045,"end_us":6049,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":84,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6097,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6091},{"abi_enter_us":6113,"abi_return_us":6120,"end_us":6120,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6113,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6158,"operation":"read","slot":0,"start_us":6156},{"abi_enter_us":6172,"abi_return_us":6175,"end_us":6175,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6172,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6207,"operation":"read","slot":0,"start_us":6207},{"end_us":6223,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6220}],"schema":2,"slots":[{"at_us":6223,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5baa2e66b8-89dc-65","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5047,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5182,"slot":0,"token":"18e1fd5baabbba68-89dc-66"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5baabbba68-89dc-66","watchdog":false,"workers_joined":1,"writes":[{"at_us":5192,"bytes":57,"kind":"head","slot":0},{"at_us":5196,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":110,"operation":"handoff","start_us":110},{"abi_enter_us":133,"abi_return_us":4842,"end_us":4845,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":133,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4876,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4872},{"abi_enter_us":4887,"abi_return_us":4891,"end_us":4891,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4887,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4916,"operation":"read","slot":0,"start_us":4914},{"abi_enter_us":4927,"abi_return_us":4928,"end_us":4928,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4927,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4958,"operation":"read","slot":0,"start_us":4958},{"abi_enter_us":4970,"abi_return_us":4970,"end_us":4971,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4969,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4993,"operation":"read","slot":0,"start_us":4992},{"abi_enter_us":5001,"abi_return_us":5003,"end_us":5003,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5001,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5020,"operation":"read","slot":0,"start_us":5020},{"end_us":5031,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5029}],"schema":2,"slots":[{"at_us":5031,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5baabbba68-89dc-66","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5637,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5680,"slot":0,"token":"18e1fd5bab452a00-89dc-67"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bab452a00-89dc-67","watchdog":false,"workers_joined":1,"writes":[{"at_us":5691,"bytes":57,"kind":"head","slot":0},{"at_us":5696,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"abi_enter_us":78,"abi_return_us":5461,"end_us":5465,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5511,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5505},{"abi_enter_us":5528,"abi_return_us":5533,"end_us":5534,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5528,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5567,"operation":"read","slot":0,"start_us":5565},{"abi_enter_us":5579,"abi_return_us":5581,"end_us":5581,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5579,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5606,"operation":"read","slot":0,"start_us":5605},{"end_us":5623,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5617}],"schema":2,"slots":[{"at_us":5623,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5bab452a00-89dc-67","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5191,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5240,"slot":0,"token":"18e1fd5babca7ae8-89dc-68"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5babca7ae8-89dc-68","watchdog":false,"workers_joined":1,"writes":[{"at_us":5255,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":31,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":54,"abi_return_us":5065,"end_us":5069,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5115,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5110},{"abi_enter_us":5131,"abi_return_us":5134,"end_us":5134,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5131,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5159,"operation":"read","slot":0,"start_us":5157},{"end_us":5173,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5170}],"schema":2,"slots":[{"at_us":5173,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5babca7ae8-89dc-68","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5488,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5604,"slot":0,"token":"18e1fd5bac4490d0-89dc-69"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bac4490d0-89dc-69","watchdog":false,"workers_joined":1,"writes":[{"at_us":5615,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":74,"abi_return_us":5376,"end_us":5379,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5416,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5410},{"abi_enter_us":5430,"abi_return_us":5433,"end_us":5433,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5430,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5459,"operation":"read","slot":0,"start_us":5457},{"end_us":5475,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5472}],"schema":2,"slots":[{"at_us":5475,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5bac4490d0-89dc-69","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5627,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5611,"slot":0,"token":"18e1fd5baccda690-89dc-6a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5baccda690-89dc-6a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5637,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":92,"operation":"handoff","start_us":92},{"abi_enter_us":118,"abi_return_us":5475,"end_us":5478,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":118,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5532,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5505},{"abi_enter_us":5567,"abi_return_us":5570,"end_us":5574,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5567,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5599,"operation":"read","slot":0,"start_us":5597},{"end_us":5613,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5609}],"schema":2,"slots":[{"at_us":5613,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5baccda690-89dc-6a","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5419,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5388,"slot":0,"token":"18e1fd5bad4cec98-89dc-6b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bad4cec98-89dc-6b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5403,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":30,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"abi_enter_us":72,"abi_return_us":5223,"end_us":5226,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5259,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5253},{"abi_enter_us":5270,"abi_return_us":5273,"end_us":5273,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5270,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5324,"operation":"read","slot":0,"start_us":5306},{"end_us":5355,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5339}],"schema":2,"slots":[{"at_us":5355,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5bad4cec98-89dc-6b","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5207,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5408,"slot":0,"token":"18e1fd5baddf4d40-89dc-6c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5baddf4d40-89dc-6c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5420,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":93,"abi_return_us":5144,"end_us":5149,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5180,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5175},{"end_us":5193,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5193}],"schema":2,"slots":[{"at_us":5193,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5baddf4d40-89dc-6c","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5470,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5716,"slot":0,"token":"18e1fd5bae64b980-89dc-6d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bae64b980-89dc-6d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5732,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":96,"operation":"dispatch","outcome":"pending","slot":0,"start_us":57},{"abi_enter_us":131,"abi_return_us":5393,"end_us":5401,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":131,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5438,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5433},{"end_us":5451,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5451}],"schema":2,"slots":[{"at_us":5451,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5bae64b980-89dc-6d","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5319,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5551,"slot":0,"token":"18e1fd5baef0a5d0-89dc-6e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5baef0a5d0-89dc-6e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5565,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":74,"operation":"handoff","start_us":74},{"abi_enter_us":87,"abi_return_us":5252,"end_us":5256,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":87,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5291,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5284},{"end_us":5304,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5304}],"schema":2,"slots":[{"at_us":5304,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5baef0a5d0-89dc-6e","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":6336,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6474,"slot":0,"token":"18e1fd5baf6ac388-89dc-6f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5baf6ac388-89dc-6f","watchdog":false,"workers_joined":1,"writes":[{"at_us":6486,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":96,"abi_return_us":6269,"end_us":6274,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6308,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":6303},{"end_us":6322,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6321}],"schema":2,"slots":[{"at_us":6322,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5baf6ac388-89dc-6f","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":55872,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5677,"slot":0,"token":"18e1fd5baff177e8-89dc-70"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5baff177e8-89dc-70","watchdog":false,"workers_joined":1,"writes":[{"at_us":5691,"bytes":66,"kind":"head","slot":0},{"at_us":5699,"bytes":6,"kind":"body","slot":0},{"at_us":55908,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":56,"abi_return_us":5504,"end_us":5508,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":56,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5552,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5546},{"abi_enter_us":5599,"abi_return_us":5608,"end_us":5609,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5598,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5641,"operation":"read","slot":0,"start_us":5638},{"abi_enter_us":5654,"abi_return_us":55737,"end_us":55742,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5654,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":55827,"error":"Other","operation":"read","slot":0,"start_us":55814},{"end_us":55853,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":55847}],"schema":2,"slots":[{"at_us":55853,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5baff177e8-89dc-70","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56272,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5339,"slot":0,"token":"18e1fd5bb3768f98-89dc-71"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bb3768f98-89dc-71","watchdog":false,"workers_joined":1,"writes":[{"at_us":5354,"bytes":66,"kind":"head","slot":0},{"at_us":5360,"bytes":6,"kind":"body","slot":0},{"at_us":56446,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":94,"operation":"dispatch","outcome":"pending","slot":0,"start_us":54},{"abi_enter_us":130,"abi_return_us":5035,"end_us":5038,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":129,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5080,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5074},{"abi_enter_us":5095,"abi_return_us":5102,"end_us":5103,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5095,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5131,"operation":"read","slot":0,"start_us":5128},{"abi_enter_us":5143,"abi_return_us":56152,"end_us":56156,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5143,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56231,"error":"Other","operation":"read","slot":0,"start_us":56226},{"end_us":56257,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56249}],"schema":2,"slots":[{"at_us":56257,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5bb3768f98-89dc-71","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57456,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5204,"slot":0,"token":"18e1fd5bb70ec5d0-89dc-72"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bb70ec5d0-89dc-72","watchdog":false,"workers_joined":1,"writes":[{"at_us":5215,"bytes":66,"kind":"head","slot":0},{"at_us":5220,"bytes":6,"kind":"body","slot":0},{"at_us":57594,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":75,"operation":"handoff","start_us":75},{"abi_enter_us":98,"abi_return_us":4896,"end_us":4899,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4931,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4925},{"abi_enter_us":4946,"abi_return_us":4951,"end_us":4951,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4946,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4974,"operation":"read","slot":0,"start_us":4972},{"abi_enter_us":4987,"abi_return_us":57318,"end_us":57336,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4986,"status":"FastlyStatus::OK","timeout_ms":495},{"capacity":1,"end_us":57411,"error":"Other","operation":"read","slot":0,"start_us":57405},{"end_us":57441,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57433}],"schema":2,"slots":[{"at_us":57441,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5bb70ec5d0-89dc-72","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":58529,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5258,"slot":0,"token":"18e1fd5bbac77870-89dc-73"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bbac77870-89dc-73","watchdog":false,"workers_joined":1,"writes":[{"at_us":5271,"bytes":66,"kind":"head","slot":0},{"at_us":5275,"bytes":6,"kind":"body","slot":0},{"at_us":58742,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":96,"operation":"handoff","start_us":96},{"abi_enter_us":136,"abi_return_us":4926,"end_us":4930,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":136,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4969,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4963},{"abi_enter_us":5026,"abi_return_us":5031,"end_us":5032,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5026,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5067,"operation":"read","slot":0,"start_us":5065},{"abi_enter_us":5085,"abi_return_us":58417,"end_us":58421,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5085,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":58488,"error":"Other","operation":"read","slot":0,"start_us":58483},{"end_us":58513,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58507}],"schema":2,"slots":[{"at_us":58513,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5bbac77870-89dc-73","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5390,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5560,"slot":0,"token":"18e1fd5bbe81f030-89dc-74"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bbe81f030-89dc-74","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":73,"abi_return_us":5315,"end_us":5320,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5360,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5354},{"end_us":5375,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5375}],"schema":2,"slots":[{"at_us":5375,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5bbe81f030-89dc-74","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5105,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5310,"slot":0,"token":"18e1fd5bbf0dfbc0-89dc-75"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bbf0dfbc0-89dc-75","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":57,"abi_return_us":5036,"end_us":5041,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":57,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5076,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5071},{"end_us":5090,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5090}],"schema":2,"slots":[{"at_us":5090,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5bbf0dfbc0-89dc-75","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5035,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5163,"slot":0,"token":"18e1fd5bbf7cb380-89dc-76"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bbf7cb380-89dc-76","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":32,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"body_reads":0,"deadline_us":500000,"end_us":52,"operation":"handoff","start_us":52},{"abi_enter_us":82,"abi_return_us":4977,"end_us":4980,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5004,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5001},{"end_us":5016,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5016}],"schema":2,"slots":[{"at_us":5016,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5bbf7cb380-89dc-76","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5047,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5218,"slot":0,"token":"18e1fd5bbff99c10-89dc-77"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bbff99c10-89dc-77","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":85,"operation":"handoff","start_us":85},{"abi_enter_us":99,"abi_return_us":4988,"end_us":4990,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":99,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5015,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5011},{"end_us":5033,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5033}],"schema":2,"slots":[{"at_us":5033,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5bbff99c10-89dc-77","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":60199,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5396,"slot":0,"token":"18e1fd5bc07c58d0-89dc-78"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bc07c58d0-89dc-78","watchdog":false,"workers_joined":1,"writes":[{"at_us":5431,"bytes":57,"kind":"head","slot":0},{"at_us":5446,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":28},{"abi_enter_us":75,"abi_return_us":5205,"end_us":5213,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5312,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5303},{"bytes":1,"capacity":1,"end_us":5366,"operation":"read","slot":0,"start_us":5359},{"capacity":1,"end_us":60126,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5523},{"end_us":60181,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60175}],"schema":2,"slots":[{"at_us":60181,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5bc07c58d0-89dc-78","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":56031,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5429,"slot":0,"token":"18e1fd5bc43bb290-89dc-79"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bc43bb290-89dc-79","watchdog":false,"workers_joined":1,"writes":[{"at_us":5437,"bytes":57,"kind":"head","slot":0},{"at_us":5442,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":69,"abi_return_us":5186,"end_us":5188,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5225,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5220},{"abi_enter_us":5236,"abi_return_us":5240,"end_us":5240,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5236,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5262,"operation":"read","slot":0,"start_us":5261},{"abi_enter_us":5273,"abi_return_us":55915,"end_us":55921,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5273,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":55991,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":55985},{"end_us":56017,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56011}],"schema":2,"slots":[{"at_us":56017,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5bc43bb290-89dc-79","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":57406,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5438,"slot":0,"token":"18e1fd5bc7e93200-89dc-7a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bc7e93200-89dc-7a","watchdog":false,"workers_joined":1,"writes":[{"at_us":5451,"bytes":57,"kind":"head","slot":0},{"at_us":5458,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"abi_enter_us":106,"abi_return_us":5119,"end_us":5123,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":106,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5169,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5163},{"abi_enter_us":5185,"abi_return_us":5191,"end_us":5192,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5185,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5220,"operation":"read","slot":0,"start_us":5217},{"abi_enter_us":5232,"abi_return_us":57291,"end_us":57295,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5232,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57364,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57359},{"end_us":57391,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57385}],"schema":2,"slots":[{"at_us":57391,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5bc7e93200-89dc-7a","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":60468,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":6493,"slot":0,"token":"18e1fd5bcba163b8-89dc-7b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bcba163b8-89dc-7b","watchdog":false,"workers_joined":1,"writes":[{"at_us":6512,"bytes":57,"kind":"head","slot":0},{"at_us":6522,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":58,"operation":"handoff","start_us":58},{"abi_enter_us":69,"abi_return_us":6235,"end_us":6243,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":69,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6297,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6291},{"abi_enter_us":6316,"abi_return_us":6322,"end_us":6323,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6316,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6353,"operation":"read","slot":0,"start_us":6351},{"abi_enter_us":6365,"abi_return_us":60360,"end_us":60363,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6365,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":1,"end_us":60422,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":60418},{"end_us":60446,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60440}],"schema":2,"slots":[{"at_us":60446,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5bcba163b8-89dc-7b","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":61651,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":7518,"slot":0,"token":"18e1fd5bcf6e9c40-89dc-7c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5bcf6e9c40-89dc-7c","watchdog":false,"workers_joined":1,"writes":[{"at_us":7536,"bytes":57,"kind":"head","slot":0},{"at_us":7542,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":27},{"body_reads":0,"deadline_us":500000,"end_us":78,"operation":"handoff","start_us":78},{"abi_enter_us":104,"abi_return_us":7257,"end_us":7262,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":104,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7338,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7329},{"abi_enter_us":7355,"abi_return_us":7369,"end_us":7370,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7355,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7402,"operation":"read","slot":0,"start_us":7399},{"abi_enter_us":7414,"abi_return_us":61543,"end_us":61546,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7414,"status":"FastlyStatus::OK","timeout_ms":492},{"capacity":16384,"end_us":61610,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":61605},{"end_us":61635,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":61627}],"schema":2,"slots":[{"at_us":61635,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5bcf6e9c40-89dc-7c","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501012,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6136,"slot":0,"token":"18e1fd5bd3610c20-89dc-7d"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5bd3610c20-89dc-7d","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":77,"abi_return_us":500928,"end_us":500936,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500994,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500993}],"schema":2,"slots":[{"at_us":500994,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5bd3610c20-89dc-7d","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":500135,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5927,"slot":0,"token":"18e1fd5bf17165c0-89dc-7e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5bf17165c0-89dc-7e","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"abi_enter_us":94,"abi_return_us":499981,"end_us":499993,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500099,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500099}],"schema":2,"slots":[{"at_us":500099,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5bf17165c0-89dc-7e","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501550,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6465,"slot":0,"token":"18e1fd5c0f773fe0-89dc-7f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5c0f773fe0-89dc-7f","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"abi_enter_us":95,"abi_return_us":501457,"end_us":501467,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501529,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501529}],"schema":2,"slots":[{"at_us":501529,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5c0f773fe0-89dc-7f","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":502078,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5219,"slot":0,"token":"18e1fd5c2d95a340-89dc-80"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5c2d95a340-89dc-80","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":55,"operation":"dispatch","outcome":"pending","slot":0,"start_us":30},{"body_reads":0,"deadline_us":500000,"end_us":103,"operation":"handoff","start_us":103},{"abi_enter_us":560,"abi_return_us":501923,"end_us":501949,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":560,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":502054,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":502054}],"schema":2,"slots":[{"at_us":502054,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5c2d95a340-89dc-80","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":500233,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5378,"slot":0,"token":"18e1fd5c4bbcd0a0-89dc-81"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5c4bbcd0a0-89dc-81","watchdog":false,"workers_joined":1,"writes":[{"at_us":5401,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":160,"operation":"dispatch","outcome":"pending","slot":0,"start_us":127},{"abi_enter_us":232,"abi_return_us":4786,"end_us":4792,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":232,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4885,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4869},{"abi_enter_us":4925,"abi_return_us":500127,"end_us":500134,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4925,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":500209,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500201}],"schema":2,"slots":[{"at_us":500209,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5c4bbcd0a0-89dc-81","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":501235,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5606,"slot":0,"token":"18e1fd5c69d71168-89dc-82"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd5c69d71168-89dc-82","watchdog":false,"workers_joined":1,"writes":[{"at_us":5614,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"abi_enter_us":99,"abi_return_us":5309,"end_us":5314,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":99,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5355,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5349},{"abi_enter_us":5370,"abi_return_us":501073,"end_us":501080,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5370,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501172,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501164}],"schema":2,"slots":[{"at_us":501172,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5c69d71168-89dc-82","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":500131,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5225,"slot":0,"token":"18e1fd5c87e2c788-89dc-83"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd5c87e2c788-89dc-83","watchdog":false,"workers_joined":1,"writes":[{"at_us":5239,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":98,"abi_return_us":4980,"end_us":4985,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":98,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5047,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5033},{"abi_enter_us":5075,"abi_return_us":500037,"end_us":500041,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5075,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500107,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500096}],"schema":2,"slots":[{"at_us":500107,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5c87e2c788-89dc-83","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":500651,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6424,"slot":0,"token":"18e1fd5ca5e37570-89dc-84"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd5ca5e37570-89dc-84","watchdog":false,"workers_joined":1,"writes":[{"at_us":6439,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":79,"operation":"handoff","start_us":79},{"abi_enter_us":93,"abi_return_us":6099,"end_us":6106,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6160,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6154},{"abi_enter_us":6179,"abi_return_us":500522,"end_us":500539,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6179,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500626,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500612}],"schema":2,"slots":[{"at_us":500626,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5ca5e37570-89dc-84","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":501435,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5950,"slot":0,"token":"18e1fd5cc3ed04c8-89dc-85"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5cc3ed04c8-89dc-85","watchdog":false,"workers_joined":1,"writes":[{"at_us":5969,"bytes":57,"kind":"head","slot":0},{"at_us":5976,"bytes":1,"kind":"body","slot":0},{"at_us":309939,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":100,"abi_return_us":5623,"end_us":5630,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":100,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5689,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5682},{"abi_enter_us":5730,"abi_return_us":5741,"end_us":5743,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5730,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5815,"operation":"read","slot":0,"start_us":5811},{"abi_enter_us":5834,"abi_return_us":309548,"end_us":309555,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5834,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309631,"operation":"read","slot":0,"start_us":309628},{"abi_enter_us":309646,"abi_return_us":501343,"end_us":501347,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309645,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":501412,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501404}],"schema":2,"slots":[{"at_us":501412,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5cc3ed04c8-89dc-85","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500995,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6300,"slot":0,"token":"18e1fd5ce222c4a0-89dc-86"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5ce222c4a0-89dc-86","watchdog":false,"workers_joined":1,"writes":[{"at_us":6313,"bytes":57,"kind":"head","slot":0},{"at_us":6319,"bytes":1,"kind":"body","slot":0},{"at_us":311142,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":72,"abi_return_us":6043,"end_us":6046,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6089,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6083},{"abi_enter_us":6104,"abi_return_us":6109,"end_us":6109,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6104,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6138,"operation":"read","slot":0,"start_us":6136},{"abi_enter_us":6153,"abi_return_us":310867,"end_us":310873,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6153,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":310993,"operation":"read","slot":0,"start_us":310990},{"abi_enter_us":311010,"abi_return_us":500903,"end_us":500907,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":311010,"status":"FastlyStatus::OK","timeout_ms":188},{"end_us":500973,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500966}],"schema":2,"slots":[{"at_us":500973,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5ce222c4a0-89dc-86","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":501472,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6010,"slot":0,"token":"18e1fd5d00506e28-89dc-87"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5d00506e28-89dc-87","watchdog":false,"workers_joined":1,"writes":[{"at_us":6020,"bytes":57,"kind":"head","slot":0},{"at_us":6027,"bytes":1,"kind":"body","slot":0},{"at_us":310536,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":58,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":91,"operation":"handoff","start_us":91},{"abi_enter_us":111,"abi_return_us":5753,"end_us":5757,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5810,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5801},{"abi_enter_us":5830,"abi_return_us":5835,"end_us":5835,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5830,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5861,"operation":"read","slot":0,"start_us":5859},{"abi_enter_us":5874,"abi_return_us":310435,"end_us":310445,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5874,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":310620,"operation":"read","slot":0,"start_us":310612},{"abi_enter_us":310640,"abi_return_us":501322,"end_us":501326,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":310640,"status":"FastlyStatus::OK","timeout_ms":189},{"end_us":501404,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501393}],"schema":2,"slots":[{"at_us":501404,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5d00506e28-89dc-87","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":501710,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5785,"slot":0,"token":"18e1fd5d1e86a718-89dc-88"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5d1e86a718-89dc-88","watchdog":false,"workers_joined":1,"writes":[{"at_us":5796,"bytes":57,"kind":"head","slot":0},{"at_us":5802,"bytes":1,"kind":"body","slot":0},{"at_us":308213,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":94,"operation":"handoff","start_us":94},{"abi_enter_us":121,"abi_return_us":5521,"end_us":5524,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":121,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5568,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5560},{"abi_enter_us":5581,"abi_return_us":5586,"end_us":5586,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5581,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5618,"operation":"read","slot":0,"start_us":5615},{"abi_enter_us":5630,"abi_return_us":307936,"end_us":307942,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5630,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":308024,"operation":"read","slot":0,"start_us":308021},{"abi_enter_us":308040,"abi_return_us":501603,"end_us":501607,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":308040,"status":"FastlyStatus::OK","timeout_ms":191},{"end_us":501684,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501677}],"schema":2,"slots":[{"at_us":501684,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5d1e86a718-89dc-88","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":500508,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6118,"slot":0,"token":"18e1fd5d3ca5d598-89dc-89"},{"path":"/readiness/siblings/1","received_us":6197,"slot":1,"token":"18e1fd5d3ca5d598-89dc-89"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd5d3ca5d598-89dc-89","watchdog":false,"workers_joined":2,"writes":[{"at_us":6135,"bytes":57,"kind":"head","slot":0},{"at_us":6213,"bytes":57,"kind":"head","slot":1},{"at_us":6223,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":105,"operation":"dispatch","outcome":"pending","slot":0,"start_us":79},{"end_us":216,"operation":"dispatch","outcome":"pending","slot":1,"start_us":184},{"abi_enter_us":248,"abi_return_us":5844,"end_us":5851,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":248,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5910,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5904},{"abi_enter_us":5930,"abi_return_us":5933,"end_us":5934,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5930,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5954,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5953},{"abi_enter_us":5965,"abi_return_us":5969,"end_us":5969,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5965,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5996,"operation":"read","slot":1,"start_us":5993},{"abi_enter_us":6014,"abi_return_us":6015,"end_us":6016,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6013,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6041,"operation":"read","slot":1,"start_us":6040},{"abi_enter_us":6053,"abi_return_us":6054,"end_us":6054,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6053,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6082,"operation":"read","slot":1,"start_us":6081},{"abi_enter_us":6093,"abi_return_us":6095,"end_us":6096,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6093,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6123,"operation":"read","slot":1,"start_us":6123},{"end_us":6138,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6133},{"abi_enter_us":6158,"abi_return_us":500292,"end_us":500306,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6158,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500470,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500453}],"schema":2,"slots":[{"at_us":500470,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6138,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5d3ca5d598-89dc-89","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":500813,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5516,"slot":0,"token":"18e1fd5d5aed92e8-89dc-8a"},{"path":"/readiness/siblings/1","received_us":5522,"slot":1,"token":"18e1fd5d5aed92e8-89dc-8a"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd5d5aed92e8-89dc-8a","watchdog":false,"workers_joined":2,"writes":[{"at_us":5525,"bytes":57,"kind":"head","slot":0},{"at_us":5536,"bytes":57,"kind":"head","slot":1},{"at_us":5543,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":111,"operation":"dispatch","outcome":"pending","slot":1,"start_us":98},{"abi_enter_us":140,"abi_return_us":5147,"end_us":5149,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":140,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5185,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5179},{"abi_enter_us":5202,"abi_return_us":5203,"end_us":5203,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5201,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5220,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5219},{"abi_enter_us":5232,"abi_return_us":5236,"end_us":5236,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5232,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5272,"operation":"read","slot":1,"start_us":5270},{"abi_enter_us":5287,"abi_return_us":5290,"end_us":5290,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5287,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5317,"operation":"read","slot":1,"start_us":5316},{"end_us":5332,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5329},{"abi_enter_us":5345,"abi_return_us":500702,"end_us":500717,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5345,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500795,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500781}],"schema":2,"slots":[{"at_us":500795,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5332,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5d5aed92e8-89dc-8a","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501277,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5349,"slot":0,"token":"18e1fd5d7928e0a0-89dc-8b"},{"path":"/readiness/siblings/1","received_us":5360,"slot":1,"token":"18e1fd5d7928e0a0-89dc-8b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd5d7928e0a0-89dc-8b","watchdog":false,"workers_joined":2,"writes":[{"at_us":5367,"bytes":57,"kind":"head","slot":1},{"at_us":5375,"bytes":57,"kind":"head","slot":0},{"at_us":11645,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":121,"operation":"dispatch","outcome":"pending","slot":1,"start_us":97},{"body_reads":0,"deadline_us":500000,"end_us":150,"operation":"handoff","start_us":150},{"abi_enter_us":173,"abi_return_us":5072,"end_us":5074,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":173,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5105,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5099},{"abi_enter_us":5119,"abi_return_us":5121,"end_us":5121,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":5119,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5142,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5141},{"abi_enter_us":5153,"abi_return_us":11369,"end_us":11372,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5153,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":11419,"operation":"read","slot":1,"start_us":11416},{"abi_enter_us":11432,"abi_return_us":11436,"end_us":11437,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11432,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11465,"operation":"read","slot":1,"start_us":11464},{"abi_enter_us":11475,"abi_return_us":11476,"end_us":11477,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11475,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11496,"operation":"read","slot":1,"start_us":11496},{"abi_enter_us":11508,"abi_return_us":11510,"end_us":11510,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11508,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11532,"operation":"read","slot":1,"start_us":11532},{"end_us":11544,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11541},{"abi_enter_us":11558,"abi_return_us":501128,"end_us":501150,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11558,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501217,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501209}],"schema":2,"slots":[{"at_us":501217,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11544,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5d7928e0a0-89dc-8b","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501379,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6516,"slot":0,"token":"18e1fd5d97701d08-89dc-8c"},{"path":"/readiness/siblings/1","received_us":6600,"slot":1,"token":"18e1fd5d97701d08-89dc-8c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd5d97701d08-89dc-8c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6533,"bytes":57,"kind":"head","slot":0},{"at_us":6612,"bytes":57,"kind":"head","slot":1},{"at_us":6619,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":112,"operation":"dispatch","outcome":"pending","slot":1,"start_us":88},{"body_reads":0,"deadline_us":500000,"end_us":132,"operation":"handoff","start_us":132},{"abi_enter_us":176,"abi_return_us":6259,"end_us":6264,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":176,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6309,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6303},{"abi_enter_us":6323,"abi_return_us":6371,"end_us":6382,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":6323,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6445,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6440},{"abi_enter_us":6472,"abi_return_us":6478,"end_us":6478,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6472,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":3,"capacity":16384,"end_us":6512,"operation":"read","slot":1,"start_us":6509},{"abi_enter_us":6525,"abi_return_us":6528,"end_us":6529,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6525,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6559,"operation":"read","slot":1,"start_us":6558},{"end_us":6572,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6568},{"abi_enter_us":6588,"abi_return_us":501169,"end_us":501252,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6588,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501360,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501350}],"schema":2,"slots":[{"at_us":501360,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":6572,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5d97701d08-89dc-8c","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501847,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6540,"slot":0,"token":"18e1fd5db5b891f0-89dc-8d"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd5db5b891f0-89dc-8d","watchdog":false,"workers_joined":1,"writes":[{"at_us":6558,"bytes":57,"kind":"head","slot":0},{"at_us":6564,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":30},{"abi_enter_us":91,"abi_return_us":6042,"end_us":6050,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6099,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6091},{"abi_enter_us":6113,"abi_return_us":6124,"end_us":6124,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6113,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6153,"operation":"read","slot":0,"start_us":6150},{"abi_enter_us":6165,"abi_return_us":501454,"end_us":501498,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6165,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":501767,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501710}],"schema":2,"slots":[{"at_us":501767,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5db5b891f0-89dc-8d","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501375,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5264,"slot":0,"token":"18e1fd5dd3dd2358-89dc-8e"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5dd3dd2358-89dc-8e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5278,"bytes":57,"kind":"head","slot":0},{"at_us":5283,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":60,"operation":"dispatch","outcome":"pending","slot":0,"start_us":31},{"abi_enter_us":108,"abi_return_us":4542,"end_us":4548,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":108,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4595,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4590},{"abi_enter_us":4611,"abi_return_us":4617,"end_us":4617,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4611,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4653,"operation":"read","slot":0,"start_us":4650},{"abi_enter_us":4667,"abi_return_us":501282,"end_us":501288,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4667,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501355,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501346}],"schema":2,"slots":[{"at_us":501355,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5dd3dd2358-89dc-8e","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501461,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5443,"slot":0,"token":"18e1fd5df1f5f4f0-89dc-8f"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5df1f5f4f0-89dc-8f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5460,"bytes":57,"kind":"head","slot":0},{"at_us":5466,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":75,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":106,"operation":"handoff","start_us":106},{"abi_enter_us":124,"abi_return_us":5188,"end_us":5192,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":123,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5234,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5226},{"abi_enter_us":5248,"abi_return_us":5253,"end_us":5253,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5248,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5278,"operation":"read","slot":0,"start_us":5276},{"abi_enter_us":5290,"abi_return_us":501366,"end_us":501370,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5290,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501441,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501428}],"schema":2,"slots":[{"at_us":501441,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5df1f5f4f0-89dc-8f","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":500565,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5767,"slot":0,"token":"18e1fd5e100b3478-89dc-90"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd5e100b3478-89dc-90","watchdog":false,"workers_joined":1,"writes":[{"at_us":5785,"bytes":57,"kind":"head","slot":0},{"at_us":5790,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":23},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":107,"abi_return_us":5517,"end_us":5531,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5558,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5552},{"abi_enter_us":5569,"abi_return_us":5576,"end_us":5576,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5568,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5614,"operation":"read","slot":0,"start_us":5611},{"abi_enter_us":5628,"abi_return_us":500479,"end_us":500483,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5627,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500543,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500535}],"schema":2,"slots":[{"at_us":500543,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e100b3478-89dc-90","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5958,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5957,"slot":0,"token":"18e1fd5e2e1a45f8-89dc-91"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e2e1a45f8-89dc-91","watchdog":false,"workers_joined":1,"writes":[{"at_us":5976,"bytes":57,"kind":"head","slot":0},{"at_us":5983,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":70,"abi_return_us":5708,"end_us":5715,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5759,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5752},{"abi_enter_us":5785,"abi_return_us":5792,"end_us":5792,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5785,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5826,"operation":"read","slot":0,"start_us":5823},{"abi_enter_us":5843,"abi_return_us":5844,"end_us":5845,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5843,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5867,"operation":"read","slot":0,"start_us":5866},{"abi_enter_us":5877,"abi_return_us":5878,"end_us":5878,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5877,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5900,"operation":"read","slot":0,"start_us":5899},{"abi_enter_us":5910,"abi_return_us":5912,"end_us":5912,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5910,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5931,"operation":"read","slot":0,"start_us":5930},{"end_us":5944,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5940}],"schema":2,"slots":[{"at_us":5944,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e2e1a45f8-89dc-91","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5199,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5317,"slot":0,"token":"18e1fd5e2ec0e250-89dc-92"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e2ec0e250-89dc-92","watchdog":false,"workers_joined":1,"writes":[{"at_us":5338,"bytes":57,"kind":"head","slot":0},{"at_us":5352,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":76,"abi_return_us":5005,"end_us":5010,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":76,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5058,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5051},{"abi_enter_us":5076,"abi_return_us":5082,"end_us":5082,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5076,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5116,"operation":"read","slot":0,"start_us":5113},{"abi_enter_us":5132,"abi_return_us":5135,"end_us":5136,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5132,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5168,"operation":"read","slot":0,"start_us":5167},{"end_us":5185,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5181}],"schema":2,"slots":[{"at_us":5185,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e2ec0e250-89dc-92","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":6033,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5871,"slot":0,"token":"18e1fd5e2f517220-89dc-93"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e2f517220-89dc-93","watchdog":false,"workers_joined":1,"writes":[{"at_us":5889,"bytes":57,"kind":"head","slot":0},{"at_us":5897,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":86,"operation":"dispatch","outcome":"pending","slot":0,"start_us":49},{"body_reads":0,"deadline_us":500000,"end_us":138,"operation":"handoff","start_us":138},{"abi_enter_us":154,"abi_return_us":5632,"end_us":5640,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":154,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5700,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5693},{"abi_enter_us":5715,"abi_return_us":5723,"end_us":5723,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5715,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5753,"operation":"read","slot":0,"start_us":5750},{"abi_enter_us":5766,"abi_return_us":5768,"end_us":5768,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5766,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5860,"operation":"read","slot":0,"start_us":5849},{"abi_enter_us":5880,"abi_return_us":5892,"end_us":5893,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5880,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5919,"operation":"read","slot":0,"start_us":5918},{"abi_enter_us":5929,"abi_return_us":5932,"end_us":5933,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5929,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5958,"operation":"read","slot":0,"start_us":5957},{"end_us":5972,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5967}],"schema":2,"slots":[{"at_us":5972,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e2f517220-89dc-93","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5298,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5357,"slot":0,"token":"18e1fd5e2fe60930-89dc-94"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e2fe60930-89dc-94","watchdog":false,"workers_joined":1,"writes":[{"at_us":5373,"bytes":57,"kind":"head","slot":0},{"at_us":5380,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":83,"operation":"handoff","start_us":83},{"abi_enter_us":95,"abi_return_us":5105,"end_us":5112,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":95,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5171,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5164},{"abi_enter_us":5187,"abi_return_us":5194,"end_us":5195,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5187,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5229,"operation":"read","slot":0,"start_us":5226},{"abi_enter_us":5243,"abi_return_us":5245,"end_us":5246,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5243,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5273,"operation":"read","slot":0,"start_us":5272},{"end_us":5286,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5282}],"schema":2,"slots":[{"at_us":5286,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e2fe60930-89dc-94","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6135,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6259,"slot":0,"token":"18e1fd5e3064ed90-89dc-95"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e3064ed90-89dc-95","watchdog":false,"workers_joined":1,"writes":[{"at_us":6275,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":51,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":89,"abi_return_us":6008,"end_us":6015,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6062,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6056},{"abi_enter_us":6078,"abi_return_us":6082,"end_us":6082,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6078,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6107,"operation":"read","slot":0,"start_us":6105},{"end_us":6122,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6118}],"schema":2,"slots":[{"at_us":6122,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e3064ed90-89dc-95","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5818,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5821,"slot":0,"token":"18e1fd5e30e34d20-89dc-96"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e30e34d20-89dc-96","watchdog":false,"workers_joined":1,"writes":[{"at_us":5838,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":53,"abi_return_us":5637,"end_us":5641,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5699,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5694},{"abi_enter_us":5719,"abi_return_us":5722,"end_us":5722,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5718,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5783,"operation":"read","slot":0,"start_us":5781},{"end_us":5804,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5800}],"schema":2,"slots":[{"at_us":5804,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e30e34d20-89dc-96","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6297,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6331,"slot":0,"token":"18e1fd5e315d9da0-89dc-97"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e315d9da0-89dc-97","watchdog":false,"workers_joined":1,"writes":[{"at_us":6345,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":82,"abi_return_us":6158,"end_us":6165,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":81,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6217,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6211},{"abi_enter_us":6232,"abi_return_us":6235,"end_us":6236,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6232,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6267,"operation":"read","slot":0,"start_us":6265},{"end_us":6284,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6278}],"schema":2,"slots":[{"at_us":6284,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e315d9da0-89dc-97","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5498,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5641,"slot":0,"token":"18e1fd5e31e25248-89dc-98"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e31e25248-89dc-98","watchdog":false,"workers_joined":1,"writes":[{"at_us":5658,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":120,"abi_return_us":5355,"end_us":5360,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":119,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5398,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5393},{"abi_enter_us":5412,"abi_return_us":5415,"end_us":5415,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5411,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5465,"operation":"read","slot":0,"start_us":5462},{"end_us":5480,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5477}],"schema":2,"slots":[{"at_us":5480,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e31e25248-89dc-98","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5555,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5624,"slot":0,"token":"18e1fd5e3259dbd8-89dc-99"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e3259dbd8-89dc-99","watchdog":false,"workers_joined":1,"writes":[{"at_us":5641,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":50,"operation":"dispatch","outcome":"pending","slot":0,"start_us":16},{"abi_enter_us":74,"abi_return_us":5445,"end_us":5460,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5512,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5504},{"end_us":5527,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5527}],"schema":2,"slots":[{"at_us":5527,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5e3259dbd8-89dc-99","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5940,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":6095,"slot":0,"token":"18e1fd5e32d103c0-89dc-9a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e32d103c0-89dc-9a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6108,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":72,"abi_return_us":5855,"end_us":5859,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5910,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5906},{"end_us":5926,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5926}],"schema":2,"slots":[{"at_us":5926,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5e32d103c0-89dc-9a","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5146,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5221,"slot":0,"token":"18e1fd5e33496810-89dc-9b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e33496810-89dc-9b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5234,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":29,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"abi_enter_us":72,"abi_return_us":5074,"end_us":5080,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5115,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5109},{"end_us":5129,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5129}],"schema":2,"slots":[{"at_us":5129,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5e33496810-89dc-9b","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5437,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5594,"slot":0,"token":"18e1fd5e33b9ecc0-89dc-9c"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e33b9ecc0-89dc-9c","watchdog":false,"workers_joined":1,"writes":[{"at_us":5602,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":80,"abi_return_us":5371,"end_us":5374,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5410,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5406},{"end_us":5423,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5423}],"schema":2,"slots":[{"at_us":5423,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5e33b9ecc0-89dc-9c","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":60286,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5334,"slot":0,"token":"18e1fd5e342d3478-89dc-9d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e342d3478-89dc-9d","watchdog":false,"workers_joined":1,"writes":[{"at_us":5346,"bytes":66,"kind":"head","slot":0},{"at_us":5352,"bytes":6,"kind":"body","slot":0},{"at_us":60392,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":61,"abi_return_us":5108,"end_us":5113,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":61,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5159,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5153},{"abi_enter_us":5176,"abi_return_us":5181,"end_us":5181,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5176,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5210,"operation":"read","slot":0,"start_us":5207},{"abi_enter_us":5221,"abi_return_us":60174,"end_us":60177,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5221,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":60248,"error":"Other","operation":"read","slot":0,"start_us":60234},{"end_us":60272,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60265}],"schema":2,"slots":[{"at_us":60272,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e342d3478-89dc-9d","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56899,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5479,"slot":0,"token":"18e1fd5e37ec4018-89dc-9e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e37ec4018-89dc-9e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5494,"bytes":66,"kind":"head","slot":0},{"at_us":5500,"bytes":6,"kind":"body","slot":0},{"at_us":57037,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":78,"abi_return_us":5229,"end_us":5236,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5287,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5281},{"abi_enter_us":5305,"abi_return_us":5309,"end_us":5310,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5305,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5338,"operation":"read","slot":0,"start_us":5336},{"abi_enter_us":5352,"abi_return_us":56775,"end_us":56782,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5352,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56856,"error":"Other","operation":"read","slot":0,"start_us":56850},{"end_us":56882,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56874}],"schema":2,"slots":[{"at_us":56882,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e37ec4018-89dc-9e","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57996,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5398,"slot":0,"token":"18e1fd5e3b7a19f8-89dc-9f"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e3b7a19f8-89dc-9f","watchdog":false,"workers_joined":1,"writes":[{"at_us":5410,"bytes":66,"kind":"head","slot":0},{"at_us":5416,"bytes":6,"kind":"body","slot":0},{"at_us":58261,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":63,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":101,"operation":"handoff","start_us":101},{"abi_enter_us":117,"abi_return_us":5010,"end_us":5017,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":117,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5066,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5060},{"abi_enter_us":5079,"abi_return_us":5088,"end_us":5089,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5079,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5128,"operation":"read","slot":0,"start_us":5122},{"abi_enter_us":5159,"abi_return_us":57876,"end_us":57882,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5158,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":57955,"error":"Other","operation":"read","slot":0,"start_us":57949},{"end_us":57981,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57974}],"schema":2,"slots":[{"at_us":57981,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e3b7a19f8-89dc-9f","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":57406,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":6454,"slot":0,"token":"18e1fd5e3f267c40-89dc-a0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e3f267c40-89dc-a0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6479,"bytes":66,"kind":"head","slot":0},{"at_us":6500,"bytes":6,"kind":"body","slot":0},{"at_us":57671,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":53,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"body_reads":0,"deadline_us":500000,"end_us":109,"operation":"handoff","start_us":109},{"abi_enter_us":136,"abi_return_us":6106,"end_us":6113,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":136,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6160,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6153},{"abi_enter_us":6174,"abi_return_us":6179,"end_us":6179,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6174,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6216,"operation":"read","slot":0,"start_us":6206},{"abi_enter_us":6238,"abi_return_us":57290,"end_us":57294,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6238,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":57367,"error":"Other","operation":"read","slot":0,"start_us":57362},{"end_us":57391,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57385}],"schema":2,"slots":[{"at_us":57391,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e3f267c40-89dc-a0","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":6243,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6442,"slot":0,"token":"18e1fd5e42c8ef90-89dc-a1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e42c8ef90-89dc-a1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":82,"abi_return_us":6162,"end_us":6171,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6210,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6205},{"end_us":6228,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6225}],"schema":2,"slots":[{"at_us":6228,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5e42c8ef90-89dc-a1","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":6272,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6434,"slot":0,"token":"18e1fd5e435485f0-89dc-a2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e435485f0-89dc-a2","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":79,"abi_return_us":6208,"end_us":6213,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":79,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6244,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6240},{"end_us":6257,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6257}],"schema":2,"slots":[{"at_us":6257,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5e435485f0-89dc-a2","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5378,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5499,"slot":0,"token":"18e1fd5e43e3d570-89dc-a3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e43e3d570-89dc-a3","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":67,"operation":"handoff","start_us":67},{"abi_enter_us":93,"abi_return_us":5308,"end_us":5313,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":93,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5350,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5344},{"end_us":5364,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5363}],"schema":2,"slots":[{"at_us":5364,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5e43e3d570-89dc-a3","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5990,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6158,"slot":0,"token":"18e1fd5e4462e4c8-89dc-a4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e4462e4c8-89dc-a4","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":13},{"body_reads":0,"deadline_us":500000,"end_us":61,"operation":"handoff","start_us":61},{"abi_enter_us":73,"abi_return_us":5915,"end_us":5920,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":73,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5959,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5955},{"end_us":5975,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5975}],"schema":2,"slots":[{"at_us":5975,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5e4462e4c8-89dc-a4","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":60138,"consumption":"buffered","deadline_us":500000,"mode":"direct","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5238,"slot":0,"token":"18e1fd5e44f11338-89dc-a5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e44f11338-89dc-a5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5249,"bytes":57,"kind":"head","slot":0},{"at_us":5255,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":64,"abi_return_us":5008,"end_us":5013,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5058,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5053},{"bytes":1,"capacity":1,"end_us":5085,"operation":"read","slot":0,"start_us":5079},{"capacity":1,"end_us":60063,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":5109},{"end_us":60121,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60113}],"schema":2,"slots":[{"at_us":60121,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e44f11338-89dc-a5","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":59781,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5272,"slot":0,"token":"18e1fd5e48b37a38-89dc-a6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e48b37a38-89dc-a6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5288,"bytes":57,"kind":"head","slot":0},{"at_us":5293,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":71,"operation":"dispatch","outcome":"pending","slot":0,"start_us":47},{"abi_enter_us":107,"abi_return_us":5000,"end_us":5004,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":107,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5040,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5035},{"abi_enter_us":5054,"abi_return_us":5061,"end_us":5061,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5054,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5086,"operation":"read","slot":0,"start_us":5084},{"abi_enter_us":5105,"abi_return_us":59568,"end_us":59573,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5105,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":59697,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59684},{"end_us":59742,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59732}],"schema":2,"slots":[{"at_us":59742,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e48b37a38-89dc-a6","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":56777,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":7002,"slot":0,"token":"18e1fd5e4c99cc88-89dc-a7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e4c99cc88-89dc-a7","watchdog":false,"workers_joined":1,"writes":[{"at_us":7021,"bytes":57,"kind":"head","slot":0},{"at_us":7028,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":633,"operation":"dispatch","outcome":"pending","slot":0,"start_us":377},{"abi_enter_us":667,"abi_return_us":7474,"end_us":7480,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":666,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":7523,"operation":"head-poll","outcome":"ready","slot":0,"start_us":7517},{"abi_enter_us":7538,"abi_return_us":7545,"end_us":7545,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7538,"status":"FastlyStatus::OK","timeout_ms":492},{"bytes":1,"capacity":16384,"end_us":7575,"operation":"read","slot":0,"start_us":7572},{"abi_enter_us":7589,"abi_return_us":56663,"end_us":56668,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":7589,"status":"FastlyStatus::OK","timeout_ms":492},{"capacity":16384,"end_us":56735,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56727},{"end_us":56761,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56753}],"schema":2,"slots":[{"at_us":56761,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e4c99cc88-89dc-a7","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":60040,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5756,"slot":0,"token":"18e1fd5e5041d1a0-89dc-a8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e5041d1a0-89dc-a8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5778,"bytes":57,"kind":"head","slot":0},{"at_us":5785,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":86,"operation":"handoff","start_us":86},{"abi_enter_us":112,"abi_return_us":5473,"end_us":5478,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":112,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5522,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5515},{"abi_enter_us":5535,"abi_return_us":5541,"end_us":5542,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5535,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5576,"operation":"read","slot":0,"start_us":5574},{"abi_enter_us":5591,"abi_return_us":59923,"end_us":59927,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5591,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":59991,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59986},{"end_us":60022,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":60011}],"schema":2,"slots":[{"at_us":60022,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e5041d1a0-89dc-a8","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"truncated","completed_us":57456,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"truncated","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/truncated/0","received_us":5992,"slot":0,"token":"18e1fd5e54049278-89dc-a9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd5e54049278-89dc-a9","watchdog":false,"workers_joined":1,"writes":[{"at_us":6015,"bytes":57,"kind":"head","slot":0},{"at_us":6021,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":52,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":96,"operation":"handoff","start_us":96},{"abi_enter_us":114,"abi_return_us":5663,"end_us":5672,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":114,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5725,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5716},{"abi_enter_us":5742,"abi_return_us":5747,"end_us":5747,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5742,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5777,"operation":"read","slot":0,"start_us":5775},{"abi_enter_us":5790,"abi_return_us":57276,"end_us":57280,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5790,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":57347,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57342},{"end_us":57430,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57365}],"schema":2,"slots":[{"at_us":57430,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5e54049278-89dc-a9","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":500675,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5588,"slot":0,"token":"18e1fd5e57ac2648-89dc-aa"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5e57ac2648-89dc-aa","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":237,"abi_return_us":500516,"end_us":500541,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":220,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500614,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500614}],"schema":2,"slots":[{"at_us":500614,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5e57ac2648-89dc-aa","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":500928,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":6457,"slot":0,"token":"18e1fd5e75b4bf88-89dc-ab"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5e75b4bf88-89dc-ab","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":75,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"abi_enter_us":117,"abi_return_us":500030,"end_us":500044,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":117,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500883,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500883}],"schema":2,"slots":[{"at_us":500883,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5e75b4bf88-89dc-ab","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":500553,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5695,"slot":0,"token":"18e1fd5e93c0a488-89dc-ac"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5e93c0a488-89dc-ac","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":98,"operation":"handoff","start_us":98},{"abi_enter_us":120,"abi_return_us":500405,"end_us":500445,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":119,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":500529,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500529}],"schema":2,"slots":[{"at_us":500529,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5e93c0a488-89dc-ac","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"silent-head","completed_us":501406,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"silent-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/silent-head/0","received_us":5899,"slot":0,"token":"18e1fd5eb1cf0258-89dc-ad"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5eb1cf0258-89dc-ad","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":95,"operation":"handoff","start_us":95},{"abi_enter_us":115,"abi_return_us":501311,"end_us":501321,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":115,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":501385,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501385}],"schema":2,"slots":[{"at_us":501385,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd5eb1cf0258-89dc-ad","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":502304,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6433,"slot":0,"token":"18e1fd5ecfd5fd88-89dc-ae"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5ecfd5fd88-89dc-ae","watchdog":false,"workers_joined":1,"writes":[{"at_us":6453,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":59,"abi_return_us":6187,"end_us":6190,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6227,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6221},{"abi_enter_us":6240,"abi_return_us":499694,"end_us":499759,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6240,"status":"FastlyStatus::OK","timeout_ms":493},{"abi_enter_us":499877,"abi_return_us":502153,"end_us":502155,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":499877,"status":"FastlyStatus::OK","timeout_ms":1},{"end_us":502262,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502217}],"schema":2,"slots":[{"at_us":502262,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5ecfd5fd88-89dc-ae","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":500881,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":5741,"slot":0,"token":"18e1fd5eee0e16f0-89dc-af"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd5eee0e16f0-89dc-af","watchdog":false,"workers_joined":1,"writes":[{"at_us":5759,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":116,"operation":"dispatch","outcome":"pending","slot":0,"start_us":45},{"abi_enter_us":258,"abi_return_us":5131,"end_us":5136,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":258,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5194,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5182},{"abi_enter_us":5209,"abi_return_us":500779,"end_us":500790,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5209,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500851,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500842}],"schema":2,"slots":[{"at_us":500851,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5eee0e16f0-89dc-af","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":501626,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6070,"slot":0,"token":"18e1fd5f0c12c060-89dc-b0"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5f0c12c060-89dc-b0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6079,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":84,"operation":"handoff","start_us":84},{"abi_enter_us":97,"abi_return_us":5831,"end_us":5835,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":97,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5872,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5867},{"abi_enter_us":5886,"abi_return_us":501534,"end_us":501540,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5885,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501604,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501597}],"schema":2,"slots":[{"at_us":501604,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5f0c12c060-89dc-b0","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"pending-body","completed_us":502151,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"pending-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/pending-body/0","received_us":6056,"slot":0,"token":"18e1fd5f2a2ec648-89dc-b1"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd5f2a2ec648-89dc-b1","watchdog":false,"workers_joined":1,"writes":[{"at_us":6075,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"body_reads":0,"deadline_us":500000,"end_us":63,"operation":"handoff","start_us":63},{"abi_enter_us":86,"abi_return_us":5878,"end_us":5885,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5932,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5925},{"abi_enter_us":5951,"abi_return_us":502055,"end_us":502061,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5950,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":502130,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":502122}],"schema":2,"slots":[{"at_us":502130,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5f2a2ec648-89dc-b1","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500844,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5825,"slot":0,"token":"18e1fd5f48430bd0-89dc-b2"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5f48430bd0-89dc-b2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5835,"bytes":57,"kind":"head","slot":0},{"at_us":5841,"bytes":1,"kind":"body","slot":0},{"at_us":309272,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"abi_enter_us":72,"abi_return_us":5488,"end_us":5490,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":72,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5523,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5516},{"abi_enter_us":5540,"abi_return_us":5546,"end_us":5547,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5540,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5582,"operation":"read","slot":0,"start_us":5579},{"abi_enter_us":5595,"abi_return_us":308924,"end_us":308934,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5595,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":309006,"operation":"read","slot":0,"start_us":309002},{"abi_enter_us":309022,"abi_return_us":500767,"end_us":500771,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309022,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500823,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500816}],"schema":2,"slots":[{"at_us":500823,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5f48430bd0-89dc-b2","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500488,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":6420,"slot":0,"token":"18e1fd5f66539838-89dc-b3"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5f66539838-89dc-b3","watchdog":false,"workers_joined":1,"writes":[{"at_us":6435,"bytes":57,"kind":"head","slot":0},{"at_us":6442,"bytes":1,"kind":"body","slot":0},{"at_us":309272,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":70,"abi_return_us":6198,"end_us":6202,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":70,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6240,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6235},{"abi_enter_us":6252,"abi_return_us":6259,"end_us":6259,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6252,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":6288,"operation":"read","slot":0,"start_us":6285},{"abi_enter_us":6299,"abi_return_us":309025,"end_us":309031,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6299,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":16384,"end_us":309109,"operation":"read","slot":0,"start_us":309106},{"abi_enter_us":309127,"abi_return_us":500400,"end_us":500404,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":309127,"status":"FastlyStatus::OK","timeout_ms":190},{"end_us":500467,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500458}],"schema":2,"slots":[{"at_us":500467,"buffered_bytes":2,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5f66539838-89dc-b3","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":500518,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5203,"slot":0,"token":"18e1fd5f845ad9b8-89dc-b4"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5f845ad9b8-89dc-b4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5209,"bytes":57,"kind":"head","slot":0},{"at_us":5216,"bytes":1,"kind":"body","slot":0},{"at_us":307124,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":62,"operation":"handoff","start_us":62},{"abi_enter_us":74,"abi_return_us":4948,"end_us":4950,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":74,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4981,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4976},{"abi_enter_us":4994,"abi_return_us":4997,"end_us":4998,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4994,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5022,"operation":"read","slot":0,"start_us":5020},{"abi_enter_us":5037,"abi_return_us":306916,"end_us":306924,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5037,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":306990,"operation":"read","slot":0,"start_us":306987},{"abi_enter_us":307005,"abi_return_us":500444,"end_us":500447,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":307005,"status":"FastlyStatus::OK","timeout_ms":192},{"end_us":500498,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500490}],"schema":2,"slots":[{"at_us":500498,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5f845ad9b8-89dc-b4","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"trickle","completed_us":501955,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"trickle","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/trickle/0","received_us":5755,"slot":0,"token":"18e1fd5fa26eb9b0-89dc-b5"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd5fa26eb9b0-89dc-b5","watchdog":false,"workers_joined":1,"writes":[{"at_us":5769,"bytes":57,"kind":"head","slot":0},{"at_us":5775,"bytes":1,"kind":"body","slot":0},{"at_us":305860,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":83,"abi_return_us":5580,"end_us":5617,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":82,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5696,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5690},{"abi_enter_us":5715,"abi_return_us":5721,"end_us":5721,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5714,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5783,"operation":"read","slot":0,"start_us":5772},{"abi_enter_us":5839,"abi_return_us":305601,"end_us":305607,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5839,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":305690,"operation":"read","slot":0,"start_us":305686},{"abi_enter_us":305705,"abi_return_us":501812,"end_us":501824,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":305704,"status":"FastlyStatus::OK","timeout_ms":194},{"end_us":501911,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501895}],"schema":2,"slots":[{"at_us":501911,"buffered_bytes":0,"bytes":2,"checksum":"08f14f07b58deb1a","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5fa26eb9b0-89dc-b5","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501815,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":6507,"slot":0,"token":"18e1fd5fc0950480-89dc-b6"},{"path":"/readiness/siblings/1","received_us":6521,"slot":1,"token":"18e1fd5fc0950480-89dc-b6"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd5fc0950480-89dc-b6","watchdog":false,"workers_joined":2,"writes":[{"at_us":6535,"bytes":57,"kind":"head","slot":1},{"at_us":6546,"bytes":57,"kind":"head","slot":0},{"at_us":12075,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":163,"operation":"dispatch","outcome":"pending","slot":1,"start_us":136},{"abi_enter_us":179,"abi_return_us":6173,"end_us":6180,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":179,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6230,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6221},{"abi_enter_us":6248,"abi_return_us":6251,"end_us":6252,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":6248,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":6269,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6268},{"abi_enter_us":6280,"abi_return_us":11693,"end_us":11700,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":6280,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":11782,"operation":"read","slot":1,"start_us":11777},{"abi_enter_us":11797,"abi_return_us":11804,"end_us":11805,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11797,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11835,"operation":"read","slot":1,"start_us":11834},{"abi_enter_us":11847,"abi_return_us":11848,"end_us":11848,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11847,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":1,"capacity":1,"end_us":11870,"operation":"read","slot":1,"start_us":11869},{"abi_enter_us":11879,"abi_return_us":11882,"end_us":11883,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":11879,"status":"FastlyStatus::OK","timeout_ms":488},{"bytes":0,"capacity":1,"end_us":11910,"operation":"read","slot":1,"start_us":11909},{"end_us":11926,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":11918},{"abi_enter_us":11941,"abi_return_us":501727,"end_us":501733,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":11941,"status":"FastlyStatus::OK","timeout_ms":488},{"end_us":501797,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501790}],"schema":2,"slots":[{"at_us":501797,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":11926,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5fc0950480-89dc-b6","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":500755,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/1","received_us":5381,"slot":1,"token":"18e1fd5fdeef4800-89dc-b7"},{"path":"/readiness/siblings/0","received_us":5385,"slot":0,"token":"18e1fd5fdeef4800-89dc-b7"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd5fdeef4800-89dc-b7","watchdog":false,"workers_joined":2,"writes":[{"at_us":5398,"bytes":57,"kind":"head","slot":0},{"at_us":5411,"bytes":57,"kind":"head","slot":1},{"at_us":5419,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":112,"operation":"dispatch","outcome":"pending","slot":1,"start_us":75},{"abi_enter_us":141,"abi_return_us":5121,"end_us":5126,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":141,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5157,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5152},{"abi_enter_us":5171,"abi_return_us":5174,"end_us":5174,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5171,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5196,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5195},{"abi_enter_us":5206,"abi_return_us":5211,"end_us":5211,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5206,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5238,"operation":"read","slot":1,"start_us":5235},{"abi_enter_us":5253,"abi_return_us":5256,"end_us":5256,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5253,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5280,"operation":"read","slot":1,"start_us":5279},{"end_us":5292,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5289},{"abi_enter_us":5327,"abi_return_us":500607,"end_us":500613,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5327,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500732,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500725}],"schema":2,"slots":[{"at_us":500732,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5292,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5fdeef4800-89dc-b7","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":500697,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5929,"slot":0,"token":"18e1fd5ffd094278-89dc-b8"},{"path":"/readiness/siblings/1","received_us":5967,"slot":1,"token":"18e1fd5ffd094278-89dc-b8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd5ffd094278-89dc-b8","watchdog":false,"workers_joined":2,"writes":[{"at_us":5944,"bytes":57,"kind":"head","slot":0},{"at_us":5974,"bytes":57,"kind":"head","slot":1},{"at_us":5979,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":103,"operation":"dispatch","outcome":"pending","slot":1,"start_us":90},{"body_reads":0,"deadline_us":500000,"end_us":135,"operation":"handoff","start_us":135},{"abi_enter_us":149,"abi_return_us":5695,"end_us":5698,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":148,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5734,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5728},{"abi_enter_us":5748,"abi_return_us":5750,"end_us":5750,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5748,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5768,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5766},{"abi_enter_us":5782,"abi_return_us":5784,"end_us":5785,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5781,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5810,"operation":"read","slot":1,"start_us":5808},{"abi_enter_us":5822,"abi_return_us":5823,"end_us":5823,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5822,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5852,"operation":"read","slot":1,"start_us":5851},{"abi_enter_us":5862,"abi_return_us":5863,"end_us":5863,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5862,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5883,"operation":"read","slot":1,"start_us":5882},{"abi_enter_us":5892,"abi_return_us":5896,"end_us":5897,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5892,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5924,"operation":"read","slot":1,"start_us":5924},{"end_us":5936,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5933},{"abi_enter_us":5948,"abi_return_us":500605,"end_us":500609,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5948,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500678,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500669}],"schema":2,"slots":[{"at_us":500678,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5936,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd5ffd094278-89dc-b8","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"siblings","completed_us":501693,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"siblings","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/siblings/0","received_us":5601,"slot":0,"token":"18e1fd601b2f2f88-89dc-b9"},{"path":"/readiness/siblings/1","received_us":5612,"slot":1,"token":"18e1fd601b2f2f88-89dc-b9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd601b2f2f88-89dc-b9","watchdog":false,"workers_joined":2,"writes":[{"at_us":5618,"bytes":57,"kind":"head","slot":0},{"at_us":5628,"bytes":57,"kind":"head","slot":1},{"at_us":5635,"bytes":3,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":66,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":153,"operation":"dispatch","outcome":"pending","slot":1,"start_us":136},{"body_reads":0,"deadline_us":500000,"end_us":185,"operation":"handoff","start_us":185},{"abi_enter_us":214,"abi_return_us":5333,"end_us":5337,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":214,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5383,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5377},{"abi_enter_us":5399,"abi_return_us":5401,"end_us":5402,"operation":"select","phases":["head"],"selected":0,"slots":[1],"start_us":5399,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":5417,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5416},{"abi_enter_us":5427,"abi_return_us":5431,"end_us":5431,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5427,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5460,"operation":"read","slot":1,"start_us":5458},{"abi_enter_us":5473,"abi_return_us":5475,"end_us":5476,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":5473,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5500,"operation":"read","slot":1,"start_us":5500},{"end_us":5516,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5513},{"abi_enter_us":5531,"abi_return_us":501592,"end_us":501600,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5531,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501659,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501652}],"schema":2,"slots":[{"at_us":501659,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":5516,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd601b2f2f88-89dc-b9","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":500614,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":6467,"slot":0,"token":"18e1fd603955b8d8-89dc-ba"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd603955b8d8-89dc-ba","watchdog":false,"workers_joined":1,"writes":[{"at_us":6484,"bytes":57,"kind":"head","slot":0},{"at_us":6491,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":36,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":66,"abi_return_us":6193,"end_us":6195,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6232,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6226},{"abi_enter_us":6245,"abi_return_us":6248,"end_us":6249,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6245,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6278,"operation":"read","slot":0,"start_us":6276},{"abi_enter_us":6291,"abi_return_us":500523,"end_us":500527,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":6290,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500590,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500578}],"schema":2,"slots":[{"at_us":500590,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd603955b8d8-89dc-ba","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":501711,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5660,"slot":0,"token":"18e1fd605759c220-89dc-bb"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd605759c220-89dc-bb","watchdog":false,"workers_joined":1,"writes":[{"at_us":5670,"bytes":57,"kind":"head","slot":0},{"at_us":5675,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":66,"abi_return_us":5449,"end_us":5453,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5495,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5488},{"abi_enter_us":5514,"abi_return_us":5518,"end_us":5519,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5514,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5549,"operation":"read","slot":0,"start_us":5546},{"abi_enter_us":5564,"abi_return_us":501618,"end_us":501624,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5564,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501691,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501684}],"schema":2,"slots":[{"at_us":501691,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd605759c220-89dc-bb","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":502013,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5182,"slot":0,"token":"18e1fd607582bc70-89dc-bc"}],"terminations":[{"reason":"harness-stop","slot":0}],"token":"18e1fd607582bc70-89dc-bc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5189,"bytes":57,"kind":"head","slot":0},{"at_us":5194,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":77,"operation":"handoff","start_us":77},{"abi_enter_us":90,"abi_return_us":4910,"end_us":4912,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4940,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4935},{"abi_enter_us":4951,"abi_return_us":4954,"end_us":4954,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4951,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4981,"operation":"read","slot":0,"start_us":4979},{"abi_enter_us":4996,"abi_return_us":501920,"end_us":501924,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":4996,"status":"FastlyStatus::OK","timeout_ms":495},{"end_us":501987,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501981}],"schema":2,"slots":[{"at_us":501987,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd607582bc70-89dc-bc","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"exhausted","completed_us":500597,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"exhausted","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/exhausted/0","received_us":5285,"slot":0,"token":"18e1fd6093ae3f30-89dc-bd"}],"terminations":[{"reason":"peer-close","slot":0}],"token":"18e1fd6093ae3f30-89dc-bd","watchdog":false,"workers_joined":1,"writes":[{"at_us":5299,"bytes":57,"kind":"head","slot":0},{"at_us":5306,"bytes":1,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":75,"operation":"handoff","start_us":75},{"abi_enter_us":96,"abi_return_us":5059,"end_us":5063,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":96,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5104,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5099},{"abi_enter_us":5118,"abi_return_us":5124,"end_us":5124,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5118,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5158,"operation":"read","slot":0,"start_us":5155},{"abi_enter_us":5170,"abi_return_us":500507,"end_us":500514,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":5170,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500577,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500570}],"schema":2,"slots":[{"at_us":500577,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","outcome":"timeout","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd6093ae3f30-89dc-bd","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5119,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5192,"slot":0,"token":"18e1fd60b1b0ba08-89dc-be"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b1b0ba08-89dc-be","watchdog":false,"workers_joined":1,"writes":[{"at_us":5206,"bytes":57,"kind":"head","slot":0},{"at_us":5211,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":40,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":64,"abi_return_us":4940,"end_us":4943,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4968,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4964},{"abi_enter_us":4979,"abi_return_us":4982,"end_us":4983,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4979,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":5004,"operation":"read","slot":0,"start_us":5002},{"abi_enter_us":5019,"abi_return_us":5020,"end_us":5020,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5019,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5039,"operation":"read","slot":0,"start_us":5039},{"abi_enter_us":5048,"abi_return_us":5049,"end_us":5049,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5048,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5067,"operation":"read","slot":0,"start_us":5066},{"abi_enter_us":5078,"abi_return_us":5079,"end_us":5080,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5077,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5099,"operation":"read","slot":0,"start_us":5099},{"end_us":5109,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5107}],"schema":2,"slots":[{"at_us":5109,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60b1b0ba08-89dc-be","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":5450,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5498,"slot":0,"token":"18e1fd60b22f5c00-89dc-bf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b22f5c00-89dc-bf","watchdog":false,"workers_joined":1,"writes":[{"at_us":5505,"bytes":57,"kind":"head","slot":0},{"at_us":5509,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":23,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"abi_enter_us":53,"abi_return_us":5333,"end_us":5334,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":53,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5356,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5353},{"abi_enter_us":5366,"abi_return_us":5369,"end_us":5370,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5366,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":5396,"operation":"read","slot":0,"start_us":5395},{"abi_enter_us":5407,"abi_return_us":5409,"end_us":5409,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5407,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5429,"operation":"read","slot":0,"start_us":5429},{"end_us":5440,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5438}],"schema":2,"slots":[{"at_us":5440,"buffered_bytes":3,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60b22f5c00-89dc-bf","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":6437,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":6409,"slot":0,"token":"18e1fd60b2a8f8d0-89dc-c0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b2a8f8d0-89dc-c0","watchdog":false,"workers_joined":1,"writes":[{"at_us":6422,"bytes":57,"kind":"head","slot":0},{"at_us":6427,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":17,"operation":"dispatch","outcome":"pending","slot":0,"start_us":8},{"body_reads":0,"deadline_us":500000,"end_us":39,"operation":"handoff","start_us":39},{"abi_enter_us":63,"abi_return_us":6249,"end_us":6252,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":62,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6281,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6277},{"abi_enter_us":6293,"abi_return_us":6297,"end_us":6297,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6293,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6319,"operation":"read","slot":0,"start_us":6318},{"abi_enter_us":6330,"abi_return_us":6331,"end_us":6331,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6329,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6352,"operation":"read","slot":0,"start_us":6351},{"abi_enter_us":6361,"abi_return_us":6362,"end_us":6363,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6361,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":1,"capacity":1,"end_us":6380,"operation":"read","slot":0,"start_us":6380},{"abi_enter_us":6389,"abi_return_us":6391,"end_us":6391,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6389,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":1,"end_us":6410,"operation":"read","slot":0,"start_us":6410},{"end_us":6422,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6419}],"schema":2,"slots":[{"at_us":6422,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60b2a8f8d0-89dc-c0","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"eof","completed_us":6358,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/eof/0","received_us":5746,"slot":0,"token":"18e1fd60b33b0770-89dc-c1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b33b0770-89dc-c1","watchdog":false,"workers_joined":1,"writes":[{"at_us":5767,"bytes":57,"kind":"head","slot":0},{"at_us":5774,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":21,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":43,"operation":"handoff","start_us":43},{"abi_enter_us":59,"abi_return_us":5683,"end_us":5693,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":59,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5757,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5750},{"abi_enter_us":5776,"abi_return_us":5853,"end_us":5878,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5776,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":3,"capacity":16384,"end_us":6002,"operation":"read","slot":0,"start_us":5998},{"abi_enter_us":6050,"abi_return_us":6063,"end_us":6064,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6050,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6184,"operation":"read","slot":0,"start_us":6181},{"end_us":6262,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6220}],"schema":2,"slots":[{"at_us":6262,"buffered_bytes":0,"bytes":3,"checksum":"bff4aa198026f420","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60b33b0770-89dc-c1","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5075,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5373,"slot":0,"token":"18e1fd60b3d79978-89dc-c2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b3d79978-89dc-c2","watchdog":false,"workers_joined":1,"writes":[{"at_us":5386,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"abi_enter_us":79,"abi_return_us":4958,"end_us":4962,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":78,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5004,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4999},{"abi_enter_us":5016,"abi_return_us":5019,"end_us":5019,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5016,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5047,"operation":"read","slot":0,"start_us":5045},{"end_us":5062,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5058}],"schema":2,"slots":[{"at_us":5062,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60b3d79978-89dc-c2","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5536,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5657,"slot":0,"token":"18e1fd60b4603238-89dc-c3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b4603238-89dc-c3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5669,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":76,"abi_return_us":5416,"end_us":5419,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":75,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5458,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5453},{"abi_enter_us":5472,"abi_return_us":5474,"end_us":5474,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5472,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5510,"operation":"read","slot":0,"start_us":5508},{"end_us":5523,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5521}],"schema":2,"slots":[{"at_us":5523,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60b4603238-89dc-c3","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":5087,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":5249,"slot":0,"token":"18e1fd60b4eaf5a8-89dc-c4"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b4eaf5a8-89dc-c4","watchdog":false,"workers_joined":1,"writes":[{"at_us":5263,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"body_reads":0,"deadline_us":500000,"end_us":75,"operation":"handoff","start_us":75},{"abi_enter_us":86,"abi_return_us":4975,"end_us":4980,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":86,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5020,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5016},{"abi_enter_us":5032,"abi_return_us":5035,"end_us":5035,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5032,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":1,"end_us":5061,"operation":"read","slot":0,"start_us":5059},{"end_us":5074,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":5071}],"schema":2,"slots":[{"at_us":5074,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60b4eaf5a8-89dc-c4","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"empty-eof","completed_us":6301,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"empty-eof","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/empty-eof/0","received_us":6378,"slot":0,"token":"18e1fd60b5637168-89dc-c5"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b5637168-89dc-c5","watchdog":false,"workers_joined":1,"writes":[{"at_us":6392,"bytes":57,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"body_reads":0,"deadline_us":500000,"end_us":56,"operation":"handoff","start_us":56},{"abi_enter_us":67,"abi_return_us":6172,"end_us":6176,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6221,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6213},{"abi_enter_us":6237,"abi_return_us":6241,"end_us":6241,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6237,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6269,"operation":"read","slot":0,"start_us":6267},{"end_us":6286,"operation":"finish","outcome":"eof","phase":"body","slot":0,"start_us":6281}],"schema":2,"slots":[{"at_us":6286,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60b5637168-89dc-c5","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5782,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5445,"slot":0,"token":"18e1fd60b5fa2388-89dc-c6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b5fa2388-89dc-c6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5460,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":33,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"abi_enter_us":71,"abi_return_us":5692,"end_us":5696,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5734,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5728},{"end_us":5756,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5756}],"schema":2,"slots":[{"at_us":5756,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd60b5fa2388-89dc-c6","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5118,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5258,"slot":0,"token":"18e1fd60b688dab0-89dc-c7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b688dab0-89dc-c7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5272,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":58,"abi_return_us":5026,"end_us":5030,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":58,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5084,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":5076},{"end_us":5103,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5103}],"schema":2,"slots":[{"at_us":5103,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd60b688dab0-89dc-c7","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":5013,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5201,"slot":0,"token":"18e1fd60b6ff7dc8-89dc-c8"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b6ff7dc8-89dc-c8","watchdog":false,"workers_joined":1,"writes":[{"at_us":5210,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":48,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"body_reads":0,"deadline_us":500000,"end_us":76,"operation":"handoff","start_us":76},{"abi_enter_us":91,"abi_return_us":4954,"end_us":4956,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":91,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4984,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":4979},{"end_us":4999,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":4999}],"schema":2,"slots":[{"at_us":4999,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd60b6ff7dc8-89dc-c8","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-head","completed_us":4278,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-head","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-head/0","received_us":5225,"slot":0,"token":"18e1fd60b78abe38-89dc-c9"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b78abe38-89dc-c9","watchdog":false,"workers_joined":1,"writes":[{"at_us":5246,"bytes":12,"kind":"head","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":56,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":88,"operation":"handoff","start_us":88},{"abi_enter_us":99,"abi_return_us":4223,"end_us":4225,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":99,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4252,"error":"HttpProtocolError","operation":"head-poll","outcome":"error","slot":0,"start_us":4248},{"end_us":4264,"error":"HttpProtocolError","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":4264}],"schema":2,"slots":[{"at_us":4264,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpProtocolError","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd60b78abe38-89dc-c9","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56919,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5283,"slot":0,"token":"18e1fd60b80a7970-89dc-ca"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60b80a7970-89dc-ca","watchdog":false,"workers_joined":1,"writes":[{"at_us":5297,"bytes":66,"kind":"head","slot":0},{"at_us":5302,"bytes":6,"kind":"body","slot":0},{"at_us":57873,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":41,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"abi_enter_us":64,"abi_return_us":4210,"end_us":4215,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4260,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4254},{"abi_enter_us":4272,"abi_return_us":4277,"end_us":4278,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4272,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":1,"end_us":4326,"operation":"read","slot":0,"start_us":4323},{"abi_enter_us":4349,"abi_return_us":56803,"end_us":56807,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4348,"status":"FastlyStatus::OK","timeout_ms":495},{"capacity":1,"end_us":56880,"error":"Other","operation":"read","slot":0,"start_us":56867},{"end_us":56905,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56899}],"schema":2,"slots":[{"at_us":56905,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60b80a7970-89dc-ca","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56035,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5201,"slot":0,"token":"18e1fd60bbca67a0-89dc-cb"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60bbca67a0-89dc-cb","watchdog":false,"workers_joined":1,"writes":[{"at_us":5209,"bytes":66,"kind":"head","slot":0},{"at_us":5213,"bytes":6,"kind":"body","slot":0},{"at_us":55952,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":47,"operation":"dispatch","outcome":"pending","slot":0,"start_us":14},{"abi_enter_us":68,"abi_return_us":4904,"end_us":4906,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":68,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":4933,"operation":"head-poll","outcome":"ready","slot":0,"start_us":4928},{"abi_enter_us":4945,"abi_return_us":4948,"end_us":4949,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":4945,"status":"FastlyStatus::OK","timeout_ms":495},{"bytes":1,"capacity":16384,"end_us":4993,"operation":"read","slot":0,"start_us":4991},{"abi_enter_us":5010,"abi_return_us":55719,"end_us":55723,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5010,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":55901,"error":"Other","operation":"read","slot":0,"start_us":55895},{"end_us":56019,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56011}],"schema":2,"slots":[{"at_us":56019,"buffered_bytes":1,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60bbca67a0-89dc-cb","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":59470,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5606,"slot":0,"token":"18e1fd60bfb01db0-89dc-cc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60bfb01db0-89dc-cc","watchdog":false,"workers_joined":1,"writes":[{"at_us":5622,"bytes":66,"kind":"head","slot":0},{"at_us":5627,"bytes":6,"kind":"body","slot":0},{"at_us":60319,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":325,"operation":"dispatch","outcome":"pending","slot":0,"start_us":45},{"body_reads":0,"deadline_us":500000,"end_us":393,"operation":"handoff","start_us":393},{"abi_enter_us":417,"abi_return_us":5231,"end_us":5240,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":416,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5396,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5329},{"abi_enter_us":5516,"abi_return_us":5527,"end_us":5528,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5516,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":1,"end_us":5589,"operation":"read","slot":0,"start_us":5586},{"abi_enter_us":5602,"abi_return_us":59351,"end_us":59354,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5602,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":1,"end_us":59428,"error":"Other","operation":"read","slot":0,"start_us":59423},{"end_us":59453,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59447}],"schema":2,"slots":[{"at_us":59453,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60bfb01db0-89dc-cc","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"malformed-body","completed_us":56652,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"malformed-body","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/malformed-body/0","received_us":5336,"slot":0,"token":"18e1fd60c396f8b8-89dc-cd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60c396f8b8-89dc-cd","watchdog":false,"workers_joined":1,"writes":[{"at_us":5346,"bytes":66,"kind":"head","slot":0},{"at_us":5351,"bytes":6,"kind":"body","slot":0},{"at_us":56749,"bytes":3,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":60,"operation":"handoff","start_us":60},{"abi_enter_us":71,"abi_return_us":5112,"end_us":5114,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5160,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5154},{"abi_enter_us":5172,"abi_return_us":5176,"end_us":5176,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5172,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":1,"capacity":16384,"end_us":5208,"operation":"read","slot":0,"start_us":5206},{"abi_enter_us":5225,"abi_return_us":56536,"end_us":56540,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5225,"status":"FastlyStatus::OK","timeout_ms":494},{"capacity":16384,"end_us":56609,"error":"Other","operation":"read","slot":0,"start_us":56604},{"end_us":56633,"error":"Other","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56627}],"schema":2,"slots":[{"at_us":56633,"buffered_bytes":0,"bytes":1,"checksum":"af63f54c86021707","error":"Other","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60c396f8b8-89dc-cd","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5332,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5529,"slot":0,"token":"18e1fd60c72b5e60-89dc-ce"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60c72b5e60-89dc-ce","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":72,"abi_return_us":5285,"end_us":5287,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":71,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5309,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5305},{"end_us":5321,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5321}],"schema":2,"slots":[{"at_us":5321,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd60c72b5e60-89dc-ce","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5349,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5521,"slot":0,"token":"18e1fd60c79cbdd0-89dc-cf"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60c79cbdd0-89dc-cf","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"abi_enter_us":67,"abi_return_us":5297,"end_us":5298,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":66,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5323,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5319},{"end_us":5336,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5336}],"schema":2,"slots":[{"at_us":5336,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd60c79cbdd0-89dc-cf","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":6275,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":6345,"slot":0,"token":"18e1fd60c80d07e8-89dc-d0"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60c80d07e8-89dc-d0","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":1,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":10},{"body_reads":0,"deadline_us":500000,"end_us":47,"operation":"handoff","start_us":47},{"abi_enter_us":64,"abi_return_us":6220,"end_us":6223,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":64,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6248,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":6246},{"end_us":6262,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":6262}],"schema":2,"slots":[{"at_us":6262,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd60c80d07e8-89dc-d0","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"head-error","completed_us":5255,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"head-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/head-error/0","received_us":5349,"slot":0,"token":"18e1fd60c88cde78-89dc-d1"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd60c88cde78-89dc-d1","watchdog":false,"workers_joined":1,"writes":[]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":22,"operation":"dispatch","outcome":"pending","slot":0,"start_us":11},{"body_reads":0,"deadline_us":500000,"end_us":42,"operation":"handoff","start_us":42},{"abi_enter_us":67,"abi_return_us":5200,"end_us":5202,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":67,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5227,"error":"HttpIncompleteResponse","operation":"head-poll","outcome":"error","slot":0,"start_us":5225},{"end_us":5241,"error":"HttpIncompleteResponse","operation":"finish","outcome":"error","phase":"head","slot":0,"start_us":5241}],"schema":2,"slots":[{"at_us":5241,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","error":"HttpIncompleteResponse","outcome":"error","phase":"head"}],"source_state":"working-tree-characterization","token":"18e1fd60c88cde78-89dc-d1","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":501753,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":5540,"slot":0,"token":"18e1fd60c8fbe070-89dc-d2"},{"path":"/readiness/mixed-head-body/1","received_us":5560,"slot":1,"token":"18e1fd60c8fbe070-89dc-d2"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd60c8fbe070-89dc-d2","watchdog":false,"workers_joined":2,"writes":[{"at_us":5569,"bytes":61,"kind":"head","slot":1},{"at_us":5601,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":34,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":100,"operation":"dispatch","outcome":"pending","slot":1,"start_us":77},{"abi_enter_us":122,"abi_return_us":5325,"end_us":5328,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":122,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5359,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5354},{"abi_enter_us":5371,"abi_return_us":5374,"end_us":5375,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5370,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5402,"operation":"read","slot":1,"start_us":5399},{"abi_enter_us":5436,"abi_return_us":5441,"end_us":5441,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5436,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5470,"operation":"read","slot":1,"start_us":5469},{"abi_enter_us":5520,"abi_return_us":5522,"end_us":5522,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5520,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5547,"operation":"read","slot":1,"start_us":5546},{"abi_enter_us":5574,"abi_return_us":5577,"end_us":5577,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5574,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5599,"operation":"read","slot":1,"start_us":5599},{"end_us":5610,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5608},{"abi_enter_us":5625,"abi_return_us":501670,"end_us":501675,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":5625,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501733,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501733}],"schema":2,"slots":[{"at_us":501733,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":5610,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60c8fbe070-89dc-d2","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":501691,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":5378,"slot":0,"token":"18e1fd60e72772d0-89dc-d3"},{"path":"/readiness/mixed-head-body/1","received_us":5386,"slot":1,"token":"18e1fd60e72772d0-89dc-d3"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd60e72772d0-89dc-d3","watchdog":false,"workers_joined":2,"writes":[{"at_us":5400,"bytes":61,"kind":"head","slot":1},{"at_us":5414,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":43,"operation":"dispatch","outcome":"pending","slot":0,"start_us":25},{"end_us":110,"operation":"dispatch","outcome":"pending","slot":1,"start_us":88},{"body_reads":0,"deadline_us":500000,"end_us":129,"operation":"handoff","start_us":129},{"abi_enter_us":155,"abi_return_us":5142,"end_us":5145,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":155,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5177,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5171},{"abi_enter_us":5189,"abi_return_us":5193,"end_us":5193,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5189,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5221,"operation":"read","slot":1,"start_us":5219},{"abi_enter_us":5267,"abi_return_us":5270,"end_us":5271,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5267,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5310,"operation":"read","slot":1,"start_us":5309},{"abi_enter_us":5338,"abi_return_us":5340,"end_us":5340,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5338,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5366,"operation":"read","slot":1,"start_us":5365},{"abi_enter_us":5386,"abi_return_us":5388,"end_us":5388,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5386,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5410,"operation":"read","slot":1,"start_us":5410},{"end_us":5424,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5419},{"abi_enter_us":5436,"abi_return_us":501544,"end_us":501560,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":5436,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":501666,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":501666}],"schema":2,"slots":[{"at_us":501666,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":5424,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd60e72772d0-89dc-d3","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":500337,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":5460,"slot":0,"token":"18e1fd6105503670-89dc-d4"},{"path":"/readiness/mixed-body-head/1","received_us":5490,"slot":1,"token":"18e1fd6105503670-89dc-d4"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd6105503670-89dc-d4","watchdog":false,"workers_joined":2,"writes":[{"at_us":5469,"bytes":57,"kind":"head","slot":0},{"at_us":58998,"bytes":61,"kind":"head","slot":1},{"at_us":59011,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":46,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":97,"operation":"dispatch","outcome":"pending","slot":1,"start_us":84},{"abi_enter_us":123,"abi_return_us":5148,"end_us":5150,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":123,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5181,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5175},{"abi_enter_us":5198,"abi_return_us":58718,"end_us":58722,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":5198,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":58777,"operation":"head-poll","outcome":"ready","slot":1,"start_us":58769},{"abi_enter_us":58792,"abi_return_us":58796,"end_us":58796,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":58792,"status":"FastlyStatus::OK","timeout_ms":441},{"bytes":8131,"capacity":16384,"end_us":58827,"operation":"read","slot":1,"start_us":58824},{"abi_enter_us":58856,"abi_return_us":58860,"end_us":58860,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":58856,"status":"FastlyStatus::OK","timeout_ms":441},{"bytes":16384,"capacity":16384,"end_us":58894,"operation":"read","slot":1,"start_us":58892},{"abi_enter_us":58937,"abi_return_us":58941,"end_us":58941,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":58937,"status":"FastlyStatus::OK","timeout_ms":441},{"bytes":8254,"capacity":16384,"end_us":58976,"operation":"read","slot":1,"start_us":58975},{"abi_enter_us":59000,"abi_return_us":59002,"end_us":59003,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":59000,"status":"FastlyStatus::OK","timeout_ms":441},{"bytes":0,"capacity":16384,"end_us":59028,"operation":"read","slot":1,"start_us":59028},{"end_us":59043,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":59040},{"abi_enter_us":59059,"abi_return_us":500244,"end_us":500255,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":59059,"status":"FastlyStatus::OK","timeout_ms":440},{"end_us":500305,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500298}],"schema":2,"slots":[{"at_us":500305,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":59043,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd6105503670-89dc-d4","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":500673,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":5439,"slot":0,"token":"18e1fd61235e3298-89dc-d5"},{"path":"/readiness/mixed-body-head/1","received_us":5469,"slot":1,"token":"18e1fd61235e3298-89dc-d5"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd61235e3298-89dc-d5","watchdog":false,"workers_joined":2,"writes":[{"at_us":5451,"bytes":57,"kind":"head","slot":0},{"at_us":55935,"bytes":61,"kind":"head","slot":1},{"at_us":55951,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":64,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":165,"operation":"dispatch","outcome":"pending","slot":1,"start_us":140},{"body_reads":0,"deadline_us":500000,"end_us":229,"operation":"handoff","start_us":229},{"abi_enter_us":263,"abi_return_us":5124,"end_us":5128,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":262,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5166,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5161},{"abi_enter_us":5179,"abi_return_us":55653,"end_us":55657,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":5179,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":55708,"operation":"head-poll","outcome":"ready","slot":1,"start_us":55703},{"abi_enter_us":55727,"abi_return_us":55731,"end_us":55731,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":55726,"status":"FastlyStatus::OK","timeout_ms":444},{"bytes":8131,"capacity":16384,"end_us":55760,"operation":"read","slot":1,"start_us":55757},{"abi_enter_us":55796,"abi_return_us":55807,"end_us":55809,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":55781,"status":"FastlyStatus::OK","timeout_ms":444},{"bytes":16384,"capacity":16384,"end_us":55876,"operation":"read","slot":1,"start_us":55865},{"abi_enter_us":55914,"abi_return_us":55921,"end_us":55922,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":55914,"status":"FastlyStatus::OK","timeout_ms":444},{"bytes":8254,"capacity":16384,"end_us":55950,"operation":"read","slot":1,"start_us":55949},{"abi_enter_us":55970,"abi_return_us":55972,"end_us":55973,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":55970,"status":"FastlyStatus::OK","timeout_ms":444},{"bytes":0,"capacity":16384,"end_us":55998,"operation":"read","slot":1,"start_us":55998},{"end_us":56015,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":56009},{"abi_enter_us":56031,"abi_return_us":500123,"end_us":500129,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":56031,"status":"FastlyStatus::OK","timeout_ms":443},{"end_us":500639,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500631}],"schema":2,"slots":[{"at_us":500639,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":56015,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd61235e3298-89dc-d5","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":56551,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6470,"slot":0,"token":"18e1fd61419077d0-89dc-d6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd61419077d0-89dc-d6","watchdog":false,"workers_joined":1,"writes":[{"at_us":6486,"bytes":61,"kind":"head","slot":0},{"at_us":6500,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":54,"operation":"dispatch","outcome":"pending","slot":0,"start_us":26},{"abi_enter_us":111,"abi_return_us":6033,"end_us":6038,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":110,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6089,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6083},{"abi_enter_us":6106,"abi_return_us":6111,"end_us":6111,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6106,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6155,"operation":"read","slot":0,"start_us":6151},{"abi_enter_us":6182,"abi_return_us":6185,"end_us":6185,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6182,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6224,"operation":"read","slot":0,"start_us":6223},{"abi_enter_us":6261,"abi_return_us":6263,"end_us":6263,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6261,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6315,"operation":"read","slot":0,"start_us":6313},{"abi_enter_us":6344,"abi_return_us":56446,"end_us":56451,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6344,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":56506,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":56501},{"end_us":56534,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":56528}],"schema":2,"slots":[{"at_us":56534,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd61419077d0-89dc-d6","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":59871,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6388,"slot":0,"token":"18e1fd614535d150-89dc-d7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd614535d150-89dc-d7","watchdog":false,"workers_joined":1,"writes":[{"at_us":6400,"bytes":61,"kind":"head","slot":0},{"at_us":6412,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":42,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"body_reads":0,"deadline_us":500000,"end_us":70,"operation":"handoff","start_us":70},{"abi_enter_us":90,"abi_return_us":6080,"end_us":6083,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":89,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6117,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6112},{"abi_enter_us":6130,"abi_return_us":6134,"end_us":6134,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6130,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6167,"operation":"read","slot":0,"start_us":6165},{"abi_enter_us":6187,"abi_return_us":6189,"end_us":6189,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6187,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6227,"operation":"read","slot":0,"start_us":6226},{"abi_enter_us":6260,"abi_return_us":6262,"end_us":6262,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6260,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6293,"operation":"read","slot":0,"start_us":6292},{"abi_enter_us":6316,"abi_return_us":59742,"end_us":59749,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6316,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":59829,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59824},{"end_us":59855,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59849}],"schema":2,"slots":[{"at_us":59855,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd614535d150-89dc-d7","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":500677,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":6451,"slot":0,"token":"18e1fd6148fd60a0-89dc-d8"},{"path":"/readiness/mixed-head-body/1","received_us":6468,"slot":1,"token":"18e1fd6148fd60a0-89dc-d8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd6148fd60a0-89dc-d8","watchdog":false,"workers_joined":2,"writes":[{"at_us":6476,"bytes":61,"kind":"head","slot":1},{"at_us":6487,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":45,"operation":"dispatch","outcome":"pending","slot":0,"start_us":17},{"end_us":95,"operation":"dispatch","outcome":"pending","slot":1,"start_us":82},{"abi_enter_us":125,"abi_return_us":6236,"end_us":6238,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":125,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6269,"operation":"head-poll","outcome":"ready","slot":1,"start_us":6263},{"abi_enter_us":6283,"abi_return_us":6285,"end_us":6286,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6283,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6322,"operation":"read","slot":1,"start_us":6311},{"abi_enter_us":6350,"abi_return_us":6354,"end_us":6354,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6350,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6381,"operation":"read","slot":1,"start_us":6380},{"abi_enter_us":6427,"abi_return_us":6431,"end_us":6432,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6427,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6469,"operation":"read","slot":1,"start_us":6467},{"abi_enter_us":6495,"abi_return_us":6497,"end_us":6497,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":6495,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":0,"capacity":16384,"end_us":6537,"operation":"read","slot":1,"start_us":6536},{"end_us":6551,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":6549},{"abi_enter_us":6564,"abi_return_us":500588,"end_us":500592,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":6564,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":500660,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500660}],"schema":2,"slots":[{"at_us":500660,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":6551,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd6148fd60a0-89dc-d8","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":500395,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":5219,"slot":0,"token":"18e1fd61671616e0-89dc-d9"},{"path":"/readiness/mixed-head-body/1","received_us":5223,"slot":1,"token":"18e1fd61671616e0-89dc-d9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd61671616e0-89dc-d9","watchdog":false,"workers_joined":2,"writes":[{"at_us":5230,"bytes":61,"kind":"head","slot":1},{"at_us":5241,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":57,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"end_us":117,"operation":"dispatch","outcome":"pending","slot":1,"start_us":93},{"body_reads":0,"deadline_us":500000,"end_us":139,"operation":"handoff","start_us":139},{"abi_enter_us":153,"abi_return_us":4975,"end_us":4977,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":153,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5003,"operation":"head-poll","outcome":"ready","slot":1,"start_us":4999},{"abi_enter_us":5016,"abi_return_us":5018,"end_us":5019,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5016,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5055,"operation":"read","slot":1,"start_us":5053},{"abi_enter_us":5074,"abi_return_us":5077,"end_us":5077,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5074,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5118,"operation":"read","slot":1,"start_us":5116},{"abi_enter_us":5148,"abi_return_us":5149,"end_us":5150,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5147,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5173,"operation":"read","slot":1,"start_us":5172},{"abi_enter_us":5190,"abi_return_us":5192,"end_us":5192,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5190,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5211,"operation":"read","slot":1,"start_us":5211},{"end_us":5221,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5219},{"abi_enter_us":5234,"abi_return_us":500310,"end_us":500317,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":5234,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500372,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500372}],"schema":2,"slots":[{"at_us":500372,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":5221,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd61671616e0-89dc-d9","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":501598,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":5453,"slot":0,"token":"18e1fd618549ee70-89dc-da"},{"path":"/readiness/mixed-body-head/1","received_us":5462,"slot":1,"token":"18e1fd618549ee70-89dc-da"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd618549ee70-89dc-da","watchdog":false,"workers_joined":2,"writes":[{"at_us":5482,"bytes":57,"kind":"head","slot":0},{"at_us":65520,"bytes":61,"kind":"head","slot":1},{"at_us":65535,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":61,"operation":"dispatch","outcome":"pending","slot":0,"start_us":22},{"end_us":168,"operation":"dispatch","outcome":"pending","slot":1,"start_us":142},{"abi_enter_us":201,"abi_return_us":5038,"end_us":5044,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":200,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5090,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5083},{"abi_enter_us":5103,"abi_return_us":65090,"end_us":65094,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":5103,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":65152,"operation":"head-poll","outcome":"ready","slot":1,"start_us":65146},{"abi_enter_us":65172,"abi_return_us":65177,"end_us":65178,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":65172,"status":"FastlyStatus::OK","timeout_ms":434},{"bytes":8131,"capacity":16384,"end_us":65208,"operation":"read","slot":1,"start_us":65206},{"abi_enter_us":65242,"abi_return_us":65247,"end_us":65248,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":65242,"status":"FastlyStatus::OK","timeout_ms":434},{"bytes":16384,"capacity":16384,"end_us":65286,"operation":"read","slot":1,"start_us":65285},{"abi_enter_us":65336,"abi_return_us":65339,"end_us":65339,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":65336,"status":"FastlyStatus::OK","timeout_ms":434},{"bytes":8254,"capacity":16384,"end_us":65375,"operation":"read","slot":1,"start_us":65373},{"abi_enter_us":65401,"abi_return_us":65403,"end_us":65404,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":65401,"status":"FastlyStatus::OK","timeout_ms":434},{"bytes":0,"capacity":16384,"end_us":65425,"operation":"read","slot":1,"start_us":65425},{"end_us":65441,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":65437},{"abi_enter_us":65456,"abi_return_us":501509,"end_us":501515,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":65456,"status":"FastlyStatus::OK","timeout_ms":434},{"end_us":501579,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501572}],"schema":2,"slots":[{"at_us":501579,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":65441,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd618549ee70-89dc-da","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":501308,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":5536,"slot":0,"token":"18e1fd61a375a3f8-89dc-db"},{"path":"/readiness/mixed-body-head/1","received_us":5539,"slot":1,"token":"18e1fd61a375a3f8-89dc-db"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd61a375a3f8-89dc-db","watchdog":false,"workers_joined":2,"writes":[{"at_us":5553,"bytes":57,"kind":"head","slot":0},{"at_us":64056,"bytes":61,"kind":"head","slot":1},{"at_us":64071,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":20},{"end_us":97,"operation":"dispatch","outcome":"pending","slot":1,"start_us":68},{"body_reads":0,"deadline_us":500000,"end_us":117,"operation":"handoff","start_us":117},{"abi_enter_us":131,"abi_return_us":5261,"end_us":5263,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":130,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5299,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5294},{"abi_enter_us":5314,"abi_return_us":63826,"end_us":63829,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":5314,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":63878,"operation":"head-poll","outcome":"ready","slot":1,"start_us":63873},{"abi_enter_us":63892,"abi_return_us":63895,"end_us":63896,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":63891,"status":"FastlyStatus::OK","timeout_ms":436},{"bytes":8131,"capacity":16384,"end_us":63924,"operation":"read","slot":1,"start_us":63921},{"abi_enter_us":63953,"abi_return_us":63955,"end_us":63955,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":63953,"status":"FastlyStatus::OK","timeout_ms":436},{"bytes":16384,"capacity":16384,"end_us":63979,"operation":"read","slot":1,"start_us":63978},{"abi_enter_us":64015,"abi_return_us":64020,"end_us":64021,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64015,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":8254,"capacity":16384,"end_us":64053,"operation":"read","slot":1,"start_us":64051},{"abi_enter_us":64076,"abi_return_us":64080,"end_us":64080,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":64076,"status":"FastlyStatus::OK","timeout_ms":435},{"bytes":0,"capacity":16384,"end_us":64105,"operation":"read","slot":1,"start_us":64105},{"end_us":64118,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":64114},{"abi_enter_us":64136,"abi_return_us":501208,"end_us":501215,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":64136,"status":"FastlyStatus::OK","timeout_ms":435},{"end_us":501289,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501280}],"schema":2,"slots":[{"at_us":501289,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":64118,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd61a375a3f8-89dc-db","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":57137,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6062,"slot":0,"token":"18e1fd61c1a70a88-89dc-dc"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd61c1a70a88-89dc-dc","watchdog":false,"workers_joined":1,"writes":[{"at_us":6082,"bytes":61,"kind":"head","slot":0},{"at_us":6094,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":38,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":93,"abi_return_us":5718,"end_us":5724,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":92,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5773,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5767},{"abi_enter_us":5788,"abi_return_us":5794,"end_us":5794,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5788,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5824,"operation":"read","slot":0,"start_us":5821},{"abi_enter_us":5863,"abi_return_us":5866,"end_us":5867,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5863,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5903,"operation":"read","slot":0,"start_us":5901},{"abi_enter_us":5945,"abi_return_us":5948,"end_us":5949,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":5945,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5979,"operation":"read","slot":0,"start_us":5978},{"abi_enter_us":6001,"abi_return_us":57032,"end_us":57036,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6001,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":57100,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":57091},{"end_us":57123,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":57118}],"schema":2,"slots":[{"at_us":57123,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd61c1a70a88-89dc-dc","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":58208,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6484,"slot":0,"token":"18e1fd61c557f110-89dc-dd"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd61c557f110-89dc-dd","watchdog":false,"workers_joined":1,"writes":[{"at_us":6503,"bytes":61,"kind":"head","slot":0},{"at_us":6515,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":24},{"body_reads":0,"deadline_us":500000,"end_us":80,"operation":"handoff","start_us":80},{"abi_enter_us":111,"abi_return_us":6165,"end_us":6171,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":111,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6218,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6213},{"abi_enter_us":6233,"abi_return_us":6239,"end_us":6239,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6233,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6283,"operation":"read","slot":0,"start_us":6281},{"abi_enter_us":6305,"abi_return_us":6308,"end_us":6309,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6305,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6347,"operation":"read","slot":0,"start_us":6345},{"abi_enter_us":6379,"abi_return_us":6381,"end_us":6382,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6379,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6411,"operation":"read","slot":0,"start_us":6410},{"abi_enter_us":6430,"abi_return_us":58084,"end_us":58091,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6430,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":58167,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58162},{"end_us":58193,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58186}],"schema":2,"slots":[{"at_us":58193,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd61c557f110-89dc-dd","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":500967,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":5284,"slot":0,"token":"18e1fd61c91c7320-89dc-de"},{"path":"/readiness/mixed-head-body/1","received_us":5292,"slot":1,"token":"18e1fd61c91c7320-89dc-de"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd61c91c7320-89dc-de","watchdog":false,"workers_joined":2,"writes":[{"at_us":5306,"bytes":61,"kind":"head","slot":1},{"at_us":5320,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":39,"operation":"dispatch","outcome":"pending","slot":0,"start_us":21},{"end_us":116,"operation":"dispatch","outcome":"pending","slot":1,"start_us":97},{"abi_enter_us":155,"abi_return_us":5024,"end_us":5027,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":154,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5060,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5054},{"abi_enter_us":5075,"abi_return_us":5078,"end_us":5078,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5074,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5106,"operation":"read","slot":1,"start_us":5104},{"abi_enter_us":5127,"abi_return_us":5145,"end_us":5147,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5127,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5187,"operation":"read","slot":1,"start_us":5185},{"abi_enter_us":5247,"abi_return_us":5250,"end_us":5251,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5247,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5286,"operation":"read","slot":1,"start_us":5285},{"abi_enter_us":5308,"abi_return_us":5310,"end_us":5311,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5308,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5340,"operation":"read","slot":1,"start_us":5340},{"end_us":5356,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5350},{"abi_enter_us":5369,"abi_return_us":500880,"end_us":500885,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":5369,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500949,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500949}],"schema":2,"slots":[{"at_us":500949,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":5356,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd61c91c7320-89dc-de","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-head-body","completed_us":500675,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":5266,"slot":0,"token":"18e1fd61e7490f20-89dc-df"},{"path":"/readiness/mixed-head-body/1","received_us":5271,"slot":1,"token":"18e1fd61e7490f20-89dc-df"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd61e7490f20-89dc-df","watchdog":false,"workers_joined":2,"writes":[{"at_us":5281,"bytes":61,"kind":"head","slot":1},{"at_us":5294,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":35,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":103,"operation":"dispatch","outcome":"pending","slot":1,"start_us":84},{"body_reads":0,"deadline_us":500000,"end_us":135,"operation":"handoff","start_us":135},{"abi_enter_us":156,"abi_return_us":5012,"end_us":5015,"operation":"select","phases":["head","head"],"selected":1,"slots":[0,1],"start_us":156,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5050,"operation":"head-poll","outcome":"ready","slot":1,"start_us":5045},{"abi_enter_us":5063,"abi_return_us":5066,"end_us":5066,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5063,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8131,"capacity":16384,"end_us":5099,"operation":"read","slot":1,"start_us":5091},{"abi_enter_us":5124,"abi_return_us":5126,"end_us":5126,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5124,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":16384,"capacity":16384,"end_us":5149,"operation":"read","slot":1,"start_us":5148},{"abi_enter_us":5182,"abi_return_us":5185,"end_us":5186,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5181,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":8254,"capacity":16384,"end_us":5215,"operation":"read","slot":1,"start_us":5213},{"abi_enter_us":5241,"abi_return_us":5244,"end_us":5244,"operation":"select","phases":["head","body"],"selected":1,"slots":[0,1],"start_us":5241,"status":"FastlyStatus::OK","timeout_ms":494},{"bytes":0,"capacity":16384,"end_us":5265,"operation":"read","slot":1,"start_us":5264},{"end_us":5276,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":5274},{"abi_enter_us":5288,"abi_return_us":500592,"end_us":500599,"operation":"select","phases":["head"],"selected":null,"slots":[0],"start_us":5288,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":500654,"operation":"finish","outcome":"timeout","phase":"head","slot":0,"start_us":500654}],"schema":2,"slots":[{"at_us":500654,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"head"},{"at_us":5276,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd61e7490f20-89dc-df","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":500352,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":5223,"slot":0,"token":"18e1fd620562cf00-89dc-e0"},{"path":"/readiness/mixed-body-head/1","received_us":5244,"slot":1,"token":"18e1fd620562cf00-89dc-e0"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e1fd620562cf00-89dc-e0","watchdog":false,"workers_joined":2,"writes":[{"at_us":5234,"bytes":57,"kind":"head","slot":0},{"at_us":57941,"bytes":61,"kind":"head","slot":1},{"at_us":57954,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":125,"operation":"dispatch","outcome":"pending","slot":1,"start_us":87},{"abi_enter_us":144,"abi_return_us":4984,"end_us":4986,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":144,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":5012,"operation":"head-poll","outcome":"ready","slot":0,"start_us":5007},{"abi_enter_us":5027,"abi_return_us":57749,"end_us":57755,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":5027,"status":"FastlyStatus::OK","timeout_ms":494},{"end_us":57866,"operation":"head-poll","outcome":"ready","slot":1,"start_us":57833},{"abi_enter_us":57891,"abi_return_us":57897,"end_us":57898,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":57891,"status":"FastlyStatus::OK","timeout_ms":442},{"bytes":8131,"capacity":16384,"end_us":57949,"operation":"read","slot":1,"start_us":57947},{"abi_enter_us":57973,"abi_return_us":57987,"end_us":57988,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":57973,"status":"FastlyStatus::OK","timeout_ms":442},{"bytes":16384,"capacity":16384,"end_us":58051,"operation":"read","slot":1,"start_us":58049},{"abi_enter_us":58090,"abi_return_us":58093,"end_us":58093,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":58090,"status":"FastlyStatus::OK","timeout_ms":441},{"bytes":8254,"capacity":16384,"end_us":58118,"operation":"read","slot":1,"start_us":58117},{"abi_enter_us":58144,"abi_return_us":58146,"end_us":58146,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":58144,"status":"FastlyStatus::OK","timeout_ms":441},{"bytes":0,"capacity":16384,"end_us":58166,"operation":"read","slot":1,"start_us":58166},{"end_us":58178,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":58175},{"abi_enter_us":58195,"abi_return_us":500265,"end_us":500269,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":58195,"status":"FastlyStatus::OK","timeout_ms":441},{"end_us":500335,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":500328}],"schema":2,"slots":[{"at_us":500335,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":58178,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd620562cf00-89dc-e0","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"mixed-body-head","completed_us":501328,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":6438,"slot":0,"token":"18e1fd6223729c00-89dc-e1"},{"path":"/readiness/mixed-body-head/1","received_us":6461,"slot":1,"token":"18e1fd6223729c00-89dc-e1"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e1fd6223729c00-89dc-e1","watchdog":false,"workers_joined":2,"writes":[{"at_us":6449,"bytes":57,"kind":"head","slot":0},{"at_us":58787,"bytes":61,"kind":"head","slot":1},{"at_us":58800,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":37,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"end_us":90,"operation":"dispatch","outcome":"pending","slot":1,"start_us":74},{"body_reads":0,"deadline_us":500000,"end_us":102,"operation":"handoff","start_us":102},{"abi_enter_us":126,"abi_return_us":6127,"end_us":6129,"operation":"select","phases":["head","head"],"selected":0,"slots":[0,1],"start_us":126,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6151,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6146},{"abi_enter_us":6162,"abi_return_us":58531,"end_us":58537,"operation":"select","phases":["body","head"],"selected":1,"slots":[0,1],"start_us":6162,"status":"FastlyStatus::OK","timeout_ms":493},{"end_us":58590,"operation":"head-poll","outcome":"ready","slot":1,"start_us":58584},{"abi_enter_us":58604,"abi_return_us":58608,"end_us":58608,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":58604,"status":"FastlyStatus::OK","timeout_ms":441},{"bytes":8131,"capacity":16384,"end_us":58636,"operation":"read","slot":1,"start_us":58633},{"abi_enter_us":58661,"abi_return_us":58663,"end_us":58663,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":58661,"status":"FastlyStatus::OK","timeout_ms":441},{"bytes":16384,"capacity":16384,"end_us":58687,"operation":"read","slot":1,"start_us":58686},{"abi_enter_us":58713,"abi_return_us":58740,"end_us":58740,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":58713,"status":"FastlyStatus::OK","timeout_ms":441},{"bytes":8254,"capacity":16384,"end_us":58761,"operation":"read","slot":1,"start_us":58760},{"abi_enter_us":58778,"abi_return_us":58782,"end_us":58782,"operation":"select","phases":["body","body"],"selected":1,"slots":[0,1],"start_us":58778,"status":"FastlyStatus::OK","timeout_ms":441},{"bytes":0,"capacity":16384,"end_us":58802,"operation":"read","slot":1,"start_us":58801},{"end_us":58814,"operation":"finish","outcome":"eof","phase":"body","slot":1,"start_us":58810},{"abi_enter_us":58828,"abi_return_us":501231,"end_us":501235,"operation":"select","phases":["body"],"selected":null,"slots":[0],"start_us":58828,"status":"FastlyStatus::OK","timeout_ms":441},{"end_us":501300,"operation":"finish","outcome":"timeout","phase":"body","slot":0,"start_us":501293}],"schema":2,"slots":[{"at_us":501300,"buffered_bytes":0,"bytes":0,"checksum":"cbf29ce484222325","outcome":"timeout","phase":"body"},{"at_us":58814,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","outcome":"eof","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd6223729c00-89dc-e1","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":58235,"consumption":"buffered","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6444,"slot":0,"token":"18e1fd6241b396d8-89dc-e2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd6241b396d8-89dc-e2","watchdog":false,"workers_joined":1,"writes":[{"at_us":6456,"bytes":61,"kind":"head","slot":0},{"at_us":6466,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":49,"operation":"dispatch","outcome":"pending","slot":0,"start_us":19},{"abi_enter_us":77,"abi_return_us":6184,"end_us":6187,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":77,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6224,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6219},{"abi_enter_us":6243,"abi_return_us":6248,"end_us":6248,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6242,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6280,"operation":"read","slot":0,"start_us":6278},{"abi_enter_us":6309,"abi_return_us":6314,"end_us":6314,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6309,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6347,"operation":"read","slot":0,"start_us":6346},{"abi_enter_us":6390,"abi_return_us":6393,"end_us":6393,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6390,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6419,"operation":"read","slot":0,"start_us":6419},{"abi_enter_us":6443,"abi_return_us":58138,"end_us":58141,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6443,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":58197,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":58193},{"end_us":58219,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":58213}],"schema":2,"slots":[{"at_us":58219,"buffered_bytes":32769,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd6241b396d8-89dc-e2","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"body-probe","probe":{"case":"full-body-error","completed_us":59837,"consumption":"streamed","deadline_us":500000,"mode":"selected","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6431,"slot":0,"token":"18e1fd62455f3da0-89dc-e3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e1fd62455f3da0-89dc-e3","watchdog":false,"workers_joined":1,"writes":[{"at_us":6445,"bytes":61,"kind":"head","slot":0},{"at_us":6458,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"read_limit":16384,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","samples":[{"end_us":44,"operation":"dispatch","outcome":"pending","slot":0,"start_us":18},{"body_reads":0,"deadline_us":500000,"end_us":68,"operation":"handoff","start_us":68},{"abi_enter_us":80,"abi_return_us":6166,"end_us":6169,"operation":"select","phases":["head"],"selected":0,"slots":[0],"start_us":80,"status":"FastlyStatus::OK","timeout_ms":499},{"end_us":6199,"operation":"head-poll","outcome":"ready","slot":0,"start_us":6194},{"abi_enter_us":6215,"abi_return_us":6219,"end_us":6220,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6215,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8131,"capacity":16384,"end_us":6250,"operation":"read","slot":0,"start_us":6248},{"abi_enter_us":6277,"abi_return_us":6281,"end_us":6282,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6277,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":16384,"capacity":16384,"end_us":6322,"operation":"read","slot":0,"start_us":6321},{"abi_enter_us":6362,"abi_return_us":6364,"end_us":6364,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6362,"status":"FastlyStatus::OK","timeout_ms":493},{"bytes":8254,"capacity":16384,"end_us":6385,"operation":"read","slot":0,"start_us":6384},{"abi_enter_us":6404,"abi_return_us":59729,"end_us":59733,"operation":"select","phases":["body"],"selected":0,"slots":[0],"start_us":6404,"status":"FastlyStatus::OK","timeout_ms":493},{"capacity":16384,"end_us":59790,"error":"UnexpectedEof","operation":"read","slot":0,"start_us":59785},{"end_us":59821,"error":"UnexpectedEof","operation":"finish","outcome":"error","phase":"body","slot":0,"start_us":59816}],"schema":2,"slots":[{"at_us":59821,"buffered_bytes":0,"bytes":32769,"checksum":"6529bfd5f3601707","error":"UnexpectedEof","outcome":"error","phase":"body"}],"source_state":"working-tree-characterization","token":"18e1fd62455f3da0-89dc-e3","tolerance_us":5000}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1257,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":257,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd55d436aac0-89dc-3","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2275,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2275,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1275,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd55d44d6af8-89dc-4","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2506,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1506,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd55d480cf38-89dc-5","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102557,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2557,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd55d4ba80c0-89dc-6","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":102179,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":102179,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2179,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd55dadd45f0-89dc-7","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101265,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1265,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd55e1072f40-89dc-8","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503276,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3276,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd55e71fe638-89dc-9","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502084,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502084,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2084,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd56052a2d28-89dc-a","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502014,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2014,"qualification":false,"repetition":0,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd56232de080-89dc-b","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1011,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":11,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd5641296a00-89dc-c","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2269,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2269,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1269,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd56413cabb0-89dc-d","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2075,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1075,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd56416bd200-89dc-e","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":104220,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4220,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd564194c660-89dc-f","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101653,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101653,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1653,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd5647d07678-89dc-10","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102093,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2093,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd564df0b720-89dc-11","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501627,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1627,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd5654164728-89dc-12","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502140,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502140,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2140,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd567202e070-89dc-13","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502001,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2001,"qualification":false,"repetition":1,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd5690080ac8-89dc-14","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1256,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":256,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd56ae10f9f8-89dc-15","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2281,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2281,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1281,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd56ae2af650-89dc-16","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2297,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1297,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd56ae63b990-89dc-17","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101436,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1436,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd56ae979eb8-89dc-18","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101355,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101355,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1355,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd56b4aac000-89dc-19","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101028,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1028,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd56bac6c6a0-89dc-1a","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":503710,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3710,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd56c0dd0c98-89dc-1b","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501914,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501914,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1914,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd56deea9778-89dc-1c","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502084,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2084,"qualification":false,"repetition":2,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd56fce80170-89dc-1d","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1262,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":262,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd571aeea2c8-89dc-1e","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2122,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2122,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1122,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd571b0e09d8-89dc-1f","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2312,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1312,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd571b46add8-89dc-20","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103436,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3436,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd571b7a8360-89dc-21","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101903,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101903,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1903,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd5721a9b828-89dc-22","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101688,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1688,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd5727ce6d70-89dc-23","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504170,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4170,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd572ded65f8-89dc-24","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502333,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502333,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2333,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd574c02c8a8-89dc-25","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502705,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2705,"qualification":false,"repetition":3,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd576a05acf8-89dc-26","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1259,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":259,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd57880ccb50-89dc-27","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2281,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2281,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1281,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd5788237418-89dc-28","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2278,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1278,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd5788545f88-89dc-29","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":103745,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":3745,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd5788811ca8-89dc-2a","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101069,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101069,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1069,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd578eb82558-89dc-2b","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101707,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1707,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd5794cf3e40-89dc-2c","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":504383,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4383,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd579af003b8-89dc-2d","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502392,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502392,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2392,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd57b905b488-89dc-2e","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501173,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1173,"qualification":false,"repetition":4,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd57d7078b50-89dc-2f","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1163,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":163,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd57f4faa4a8-89dc-30","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":3,"abi_return_us":2288,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2288,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1288,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd57f512d7f8-89dc-31","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2299,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1299,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd57f549c678-89dc-32","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101338,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1338,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd57f57fe208-89dc-33","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101186,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101186,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1186,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd57fb8fee40-89dc-34","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101557,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1557,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd5801ad8f08-89dc-35","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502065,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2065,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd5807cb9d30-89dc-36","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502109,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502109,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2109,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd5825bda130-89dc-37","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":502019,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2019,"qualification":false,"repetition":5,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd5843bb3238-89dc-38","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1212,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":212,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd5861b957b0-89dc-39","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2266,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2266,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1266,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd5861d0d368-89dc-3a","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2276,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1276,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd5862020140-89dc-3b","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":104790,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":4790,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd5862304cd0-89dc-3c","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101239,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101239,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1239,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd586874e240-89dc-3d","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":102061,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2061,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd586e8f27a8-89dc-3e","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501751,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1751,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd5874c70348-89dc-3f","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":501465,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":501465,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1465,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd5892b61948-89dc-40","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501802,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1802,"qualification":false,"repetition":6,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd58b0af87b8-89dc-41","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":1255,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":255,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd58cea96388-89dc-42","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":2284,"abi_status":"FastlyStatus::OK","budget_ms":1,"elapsed_us":2284,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1284,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd58cec2c788-89dc-43","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":1,"elapsed_us":2277,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1277,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd58cefbc178-89dc-44","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101595,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1595,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd58cf2a47a0-89dc-45","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":101083,"abi_status":"FastlyStatus::OK","budget_ms":100,"elapsed_us":101083,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1083,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd58d53e7a58-89dc-46","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":100,"elapsed_us":101686,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1686,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd58db595048-89dc-47","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501736,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1736,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"native-sleep","token":"18e1fd58e17a5058-89dc-48","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"abi_enter_us":0,"abi_return_us":502052,"abi_status":"FastlyStatus::OK","budget_ms":500,"elapsed_us":502052,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":2052,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"selected":null,"source":"empty-select","source_state":"working-tree-characterization","token":"18e1fd58ff697dc8-89dc-49","within_five_ms":true}} +{"run":"repaired-formatted","kind":"timer-control","probe":{"budget_ms":500,"elapsed_us":501390,"event":"readiness-timer-control","measurement_end":"primitive-return","overrun_us":1390,"qualification":false,"repetition":7,"revision":"92daf00ab8bb76b2d4791e727e698e3304cc15ca","schema":1,"source":"wasi-sleep","source_state":"working-tree-characterization","token":"18e1fd591d6ab080-89dc-4a","within_five_ms":true}} diff --git a/docs/audits/evidence/2026-10-25-fastly-mixed-readiness.json b/docs/audits/evidence/2026-10-25-fastly-mixed-readiness.json new file mode 100644 index 000000000..5848eff68 --- /dev/null +++ b/docs/audits/evidence/2026-10-25-fastly-mixed-readiness.json @@ -0,0 +1,170 @@ +{ + "schema": 1, + "execution_date": "2026-10-25", + "qualification": false, + "edgezero_base_revision": "92daf00ab8bb76b2d4791e727e698e3304cc15ca", + "source_state": "working-tree characterization; not a frozen-candidate build", + "execution_environment": "Local macOS, not an isolated-load benchmark; source verification could run concurrently. No scheduling cause or tail bound is inferred.", + "command": "CARGO_NET_OFFLINE=true SSL_CERT_FILE=/etc/ssl/cert.pem VICEROY_BIN= scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-readiness --output ", + "deployed": false, + "external_requests": false, + "runtime_version": "0.21.0", + "runtime_upstream_revision": "a69d982d1d95e13c0fc9f92683b9118639bb4b04", + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)", + "fastly_sdk": "0.13.1", + "deadline_us": 500000, + "tolerance_us": 5000, + "production_transport_changed": false, + "runtime_pins_changed": false, + "historical_evidence_rewritten": false, + "runs": [ + { + "id": "repaired-before-formatting", + "directory": "/private/tmp/edgezero-fastly-mixed-repaired-20261025", + "exit_code": 1, + "runtime_binary_sha256": "98ea0536478ce2e641f093b2619b7ac7c6c237402f140e5792d854575abc7100", + "guest_sha256": "2cfcc3fa6c7813132516cbda49254749f2865164fa2dcaff25b09f8138acea7b", + "guest_bytes": 787972, + "events_sha256": "d9fbdce2850b1f34e645ee26044b6cfbb7dd1a2eb5adb2ac6eeac98ae43c0e78", + "log_sha256": "a64d3bddf3dab849d8ff28d1c29c1d3ce5eb48aca29326c24a8f2645792ec2c5", + "body_probes": 153, + "origin_requests": 177, + "progress_probes": 18, + "progress_origin_requests": 30, + "full_capacity_progress_probes": 18, + "mixed_selection_probes": 12, + "maximum_completion_us": 518771, + "source_error_losses": 0, + "new_truncation_errors_preserved": 6, + "timer_controls": 72, + "late_timer_controls": 1, + "late_probes": [ + { + "case": "mixed-head-body", + "mode": "selected", + "consumption": "buffered", + "read_limit": 16384, + "repetition": 0, + "completed_us": 508002 + }, + { + "case": "mixed-head-body", + "mode": "selected", + "consumption": "streamed", + "read_limit": 16384, + "repetition": 1, + "completed_us": 518771 + } + ], + "validation_failures": [ + "mixed/full-buffer progress: 2 readiness leg(s) failed:\nmixed-head-body/selected/buffered/16384/0: reordered or late operation\nmixed-head-body/selected/streamed/16384/1: missing/late probe completion" + ], + "qualification": false + }, + { + "id": "stock-formatted", + "directory": "/private/tmp/edgezero-fastly-mixed-stock-20261025", + "exit_code": 1, + "runtime_binary_sha256": "751ff2c1a83063b109bc212b95be5f52e5ab6284b9027fa03aa229087eba3f30", + "guest_sha256": "45f4f541a2338bd923ad33afcf4677a0619b5889778f602f8817cc7d2f97f392", + "guest_bytes": 787988, + "events_sha256": "ab2a563c36d2bb6347abcd8ce5a59a63f6fa50524d2548822cb4fe4fcc97666d", + "log_sha256": "98d15fd83e123825f01183f992920d91cb3b602a76a4196cbcfbee0be4ebafe4", + "body_probes": 153, + "origin_requests": 177, + "progress_probes": 18, + "progress_origin_requests": 30, + "full_capacity_progress_probes": 18, + "mixed_selection_probes": 12, + "maximum_completion_us": 593907, + "source_error_losses": 30, + "new_truncation_errors_preserved": 0, + "timer_controls": 72, + "late_timer_controls": 2, + "late_probes": [ + { + "case": "exhausted", + "mode": "selected", + "consumption": "buffered", + "read_limit": 1, + "repetition": 0, + "completed_us": 517183 + }, + { + "case": "mixed-head-body", + "mode": "selected", + "consumption": "streamed", + "read_limit": 16384, + "repetition": 2, + "completed_us": 593907 + } + ], + "validation_failures": [ + "readiness: 25 readiness leg(s) failed:\ntruncated/selected/buffered/1/0: wrong case terminal\ntruncated/selected/buffered/16384/0: wrong case terminal\ntruncated/selected/streamed/1/0: wrong case terminal\ntruncated/selected/streamed/16384/0: wrong case terminal\nexhausted/selected/buffered/1/0: reordered or late operation\nmalformed-body/selected/buffered/1/0: wrong case terminal\nmalformed-body/selected/buffered/16384/0: wrong case terminal\nmalformed-body/selected/streamed/1/0: wrong case terminal\nmalformed-body/selected/streamed/16384/0: wrong case terminal\ntruncated/selected/buffered/1/1: wrong case terminal\ntruncated/selected/buffered/16384/1: wrong case terminal\ntruncated/selected/streamed/1/1: wrong case terminal\ntruncated/selected/streamed/16384/1: wrong case terminal\nmalformed-body/selected/buffered/1/1: wrong case terminal\nmalformed-body/selected/buffered/16384/1: wrong case terminal\nmalformed-body/selected/streamed/1/1: wrong case terminal\nmalformed-body/selected/streamed/16384/1: wrong case terminal\ntruncated/selected/buffered/1/2: wrong case terminal\ntruncated/selected/buffered/16384/2: wrong case terminal\ntruncated/selected/streamed/1/2: wrong case terminal\ntruncated/selected/streamed/16384/2: wrong case terminal\nmalformed-body/selected/buffered/1/2: wrong case terminal\nmalformed-body/selected/buffered/16384/2: wrong case terminal\nmalformed-body/selected/streamed/1/2: wrong case terminal\nmalformed-body/selected/streamed/16384/2: wrong case terminal\nmixed/full-buffer progress: 7 readiness leg(s) failed:\nfull-body-error/selected/buffered/16384/0: wrong case terminal\nfull-body-error/selected/streamed/16384/0: wrong case terminal\nfull-body-error/selected/buffered/16384/1: wrong case terminal\nfull-body-error/selected/streamed/16384/1: wrong case terminal\nmixed-head-body/selected/streamed/16384/2: reordered or late operation\nfull-body-error/selected/buffered/16384/2: wrong case terminal\nfull-body-error/selected/streamed/16384/2: wrong case terminal" + ], + "qualification": false + }, + { + "id": "repaired-formatted", + "directory": "/private/tmp/edgezero-fastly-mixed-repaired-formatted-20261025", + "exit_code": 2, + "runtime_binary_sha256": "98ea0536478ce2e641f093b2619b7ac7c6c237402f140e5792d854575abc7100", + "guest_sha256": "45f4f541a2338bd923ad33afcf4677a0619b5889778f602f8817cc7d2f97f392", + "guest_bytes": 787988, + "events_sha256": "d6c89d5d29f12808c74ce8cb86ef0a147394a65670efba7adb6f6d40246b0cc4", + "log_sha256": "ceb45162882e4bcffc033e5e010df7a23ad42ba1b506fff8978b6bd5a7c60369", + "body_probes": 153, + "origin_requests": 177, + "progress_probes": 18, + "progress_origin_requests": 30, + "full_capacity_progress_probes": 18, + "mixed_selection_probes": 12, + "maximum_completion_us": 504369, + "source_error_losses": 0, + "new_truncation_errors_preserved": 6, + "timer_controls": 72, + "late_timer_controls": 0, + "late_probes": [], + "validation_failures": [], + "qualification": false + } + ], + "formatted_pair_identical_guest": true, + "selected_formatted_source_files": [ + { + "path": "tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs", + "sha256": "1471fef455cf0d89899eb6e604eba3b3c24febf1fdc5066bf89065bc86836e3a" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/net/readiness.rs", + "sha256": "3d819038b2bd9e711addd4105bd8831f45e76ec23cf8d48a4cf6acadd5c3ea15" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/readiness.rs", + "sha256": "fc83764dc9e55ff32e6c7b38f3cd2f55a4f856bb95eca5281d84ee05c7319bd6" + }, + { + "path": "tests/fixtures/reusable-app/Cargo.lock", + "sha256": "7770119d7879e96c50cdf87be7dc9b72f5626a43b63c6b15da1f425179fab2ee" + }, + { + "path": "patches/viceroy/0.21.0-body-readiness.patch", + "sha256": "8693ce34f82cc98dc272bf39379007b17a40dfc19138c55b445378fd9a163759" + } + ], + "retained_probes": { + "path": "docs/audits/evidence/2026-10-25-fastly-mixed-readiness-probes.jsonl", + "sha256": "43fad96badc17b8c7824c2a01d68273e68d7f250dd9a153be5ceac94f20feb33", + "records": 675, + "body_probes": 459, + "timer_controls": 216 + }, + "interpretation": [ + "The formatted pair uses identical guest bytes. The earlier repaired run precedes formatting and has a distinct guest identity; it is retained, not represented as an identical-guest comparison.", + "The repaired runtime preserves all selected source errors and completes the mixed ready sibling before the pending sibling expires. Every supplementary probe records at least one full 16 KiB read.", + "The first repaired run fails two unchanged timing checks. The formatted repaired run passes both local matrices but exits 2, unverified; no tail bound or timing guarantee follows.", + "Stock loses 24 baseline and six new full-body truncation errors and retains its timing failures. No observation is backdated, widened or overwritten.", + "Three cases and two-slot readiness do not prove fairness for arbitrary continuously-ready populations, production read-size approval, resource bounds or hosted transport teardown.", + "Backend registration remains outside this diagnostic window and fixed two-second timers are not a proof of the B31 production policy. No production engine, runtime pin, capability or G2 qualification is promoted." + ] +} diff --git a/docs/audits/evidence/2026-10-26-fastly-cooperative-progress-clients.jsonl b/docs/audits/evidence/2026-10-26-fastly-cooperative-progress-clients.jsonl new file mode 100644 index 000000000..0233251be --- /dev/null +++ b/docs/audits/evidence/2026-10-26-fastly-cooperative-progress-clients.jsonl @@ -0,0 +1,48 @@ +{"run_id":"complete-client-inventory","case":"mixed-head-body","completed_us":511305,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":43948,"slot":0,"token":"18e202b5891f3290-b771-4b"},{"path":"/readiness/mixed-head-body/1","received_us":43955,"slot":1,"token":"18e202b5891f3290-b771-4b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e202b5891f3290-b771-4b","watchdog":false,"workers_joined":2,"writes":[{"at_us":43994,"bytes":61,"kind":"head","slot":1},{"at_us":44107,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":0,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":31320,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b5891f3290-b771-4b"} +{"run_id":"complete-client-inventory","case":"mixed-body-head","completed_us":501262,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":8000,"slot":0,"token":"18e202b5a88511e0-b771-4c"},{"path":"/readiness/mixed-body-head/1","received_us":8016,"slot":1,"token":"18e202b5a88511e0-b771-4c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e202b5a88511e0-b771-4c","watchdog":false,"workers_joined":2,"writes":[{"at_us":8055,"bytes":57,"kind":"head","slot":0},{"at_us":66487,"bytes":61,"kind":"head","slot":1},{"at_us":66505,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":0,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":66358,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b5a88511e0-b771-4c"} +{"run_id":"complete-client-inventory","case":"full-body-error","completed_us":62311,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":7676,"slot":0,"token":"18e202b5c6d86020-b771-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202b5c6d86020-b771-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":7712,"bytes":61,"kind":"head","slot":0},{"at_us":7730,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":0,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b5c6d86020-b771-4d"} +{"run_id":"complete-client-inventory","case":"full-body-error","completed_us":58705,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5363,"slot":0,"token":"18e202b5cae9c1e0-b771-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202b5cae9c1e0-b771-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":5376,"bytes":61,"kind":"head","slot":0},{"at_us":5393,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":0,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b5cae9c1e0-b771-4e"} +{"run_id":"complete-client-inventory","case":"mixed-head-body","completed_us":500562,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":5803,"slot":0,"token":"18e202b5cea85c38-b771-4f"},{"path":"/readiness/mixed-head-body/1","received_us":6003,"slot":1,"token":"18e202b5cea85c38-b771-4f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e202b5cea85c38-b771-4f","watchdog":false,"workers_joined":2,"writes":[{"at_us":6024,"bytes":61,"kind":"head","slot":1},{"at_us":6041,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":1,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":6189,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b5cea85c38-b771-4f"} +{"run_id":"complete-client-inventory","case":"mixed-body-head","completed_us":502331,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":5539,"slot":1,"token":"18e202b5eccaea00-b771-50"},{"path":"/readiness/mixed-body-head/0","received_us":5567,"slot":0,"token":"18e202b5eccaea00-b771-50"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e202b5eccaea00-b771-50","watchdog":false,"workers_joined":2,"writes":[{"at_us":5593,"bytes":57,"kind":"head","slot":0},{"at_us":66074,"bytes":61,"kind":"head","slot":1},{"at_us":66128,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":1,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":65905,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b5eccaea00-b771-50"} +{"run_id":"complete-client-inventory","case":"full-body-error","completed_us":58266,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5962,"slot":0,"token":"18e202b60af985b8-b771-51"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202b60af985b8-b771-51","watchdog":false,"workers_joined":1,"writes":[{"at_us":5980,"bytes":61,"kind":"head","slot":0},{"at_us":5993,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":1,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b60af985b8-b771-51"} +{"run_id":"complete-client-inventory","case":"full-body-error","completed_us":66711,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6711,"slot":0,"token":"18e202b60eccec48-b771-52"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202b60eccec48-b771-52","watchdog":false,"workers_joined":1,"writes":[{"at_us":6729,"bytes":61,"kind":"head","slot":0},{"at_us":6742,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":1,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b60eccec48-b771-52"} +{"run_id":"complete-client-inventory","case":"mixed-head-body","completed_us":501649,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":10642,"slot":0,"token":"18e202b613287e38-b771-53"},{"path":"/readiness/mixed-head-body/1","received_us":10832,"slot":1,"token":"18e202b613287e38-b771-53"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e202b613287e38-b771-53","watchdog":false,"workers_joined":2,"writes":[{"at_us":10874,"bytes":61,"kind":"head","slot":1},{"at_us":10896,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":2,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":8080,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b613287e38-b771-53"} +{"run_id":"complete-client-inventory","case":"mixed-body-head","completed_us":503167,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":6502,"slot":1,"token":"18e202b6582b7080-b771-54"},{"path":"/readiness/mixed-body-head/0","received_us":6823,"slot":0,"token":"18e202b6582b7080-b771-54"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e202b6582b7080-b771-54","watchdog":false,"workers_joined":2,"writes":[{"at_us":6846,"bytes":57,"kind":"head","slot":0},{"at_us":64007,"bytes":61,"kind":"head","slot":1},{"at_us":64122,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":2,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":62891,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b6582b7080-b771-54"} +{"run_id":"complete-client-inventory","case":"full-body-error","completed_us":62621,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":7637,"slot":0,"token":"18e202b6765ac7b8-b771-55"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202b6765ac7b8-b771-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":7653,"bytes":61,"kind":"head","slot":0},{"at_us":7668,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":2,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b6765ac7b8-b771-55"} +{"run_id":"complete-client-inventory","case":"full-body-error","completed_us":61035,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":7360,"slot":0,"token":"18e202b67a3db6d8-b771-56"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202b67a3db6d8-b771-56","watchdog":false,"workers_joined":1,"writes":[{"at_us":7368,"bytes":61,"kind":"head","slot":0},{"at_us":7380,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":2,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202b67a3db6d8-b771-56"} +{"run_id":"unchanged-build-replication","case":"mixed-head-body","completed_us":503219,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":38887,"slot":0,"token":"18e202daa745c7c8-dee3-4b"},{"path":"/readiness/mixed-head-body/1","received_us":38943,"slot":1,"token":"18e202daa745c7c8-dee3-4b"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e202daa745c7c8-dee3-4b","watchdog":false,"workers_joined":2,"writes":[{"at_us":38966,"bytes":61,"kind":"head","slot":1},{"at_us":38995,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":0,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":45037,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202daa745c7c8-dee3-4b"} +{"run_id":"unchanged-build-replication","case":"mixed-body-head","completed_us":501502,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":6193,"slot":1,"token":"18e202dac5984700-dee3-4c"},{"path":"/readiness/mixed-body-head/0","received_us":6408,"slot":0,"token":"18e202dac5984700-dee3-4c"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e202dac5984700-dee3-4c","watchdog":false,"workers_joined":2,"writes":[{"at_us":6481,"bytes":57,"kind":"head","slot":0},{"at_us":69557,"bytes":61,"kind":"head","slot":1},{"at_us":69589,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":0,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":69909,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202dac5984700-dee3-4c"} +{"run_id":"unchanged-build-replication","case":"full-body-error","completed_us":69649,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":11214,"slot":0,"token":"18e202dae3c47988-dee3-4d"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202dae3c47988-dee3-4d","watchdog":false,"workers_joined":1,"writes":[{"at_us":11237,"bytes":61,"kind":"head","slot":0},{"at_us":11255,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":0,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202dae3c47988-dee3-4d"} +{"run_id":"unchanged-build-replication","case":"full-body-error","completed_us":62664,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":8311,"slot":0,"token":"18e202dae80b3f40-dee3-4e"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202dae80b3f40-dee3-4e","watchdog":false,"workers_joined":1,"writes":[{"at_us":8325,"bytes":61,"kind":"head","slot":0},{"at_us":8348,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":0,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202dae80b3f40-dee3-4e"} +{"run_id":"unchanged-build-replication","case":"mixed-head-body","completed_us":502109,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":8052,"slot":1,"token":"18e202daebebb978-dee3-4f"},{"path":"/readiness/mixed-head-body/0","received_us":8071,"slot":0,"token":"18e202daebebb978-dee3-4f"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e202daebebb978-dee3-4f","watchdog":false,"workers_joined":2,"writes":[{"at_us":8075,"bytes":61,"kind":"head","slot":1},{"at_us":8094,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":1,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":7898,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202daebebb978-dee3-4f"} +{"run_id":"unchanged-build-replication","case":"mixed-body-head","completed_us":502794,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":15043,"slot":1,"token":"18e202db1f1e3f00-dee3-50"},{"path":"/readiness/mixed-body-head/0","received_us":15060,"slot":0,"token":"18e202db1f1e3f00-dee3-50"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e202db1f1e3f00-dee3-50","watchdog":false,"workers_joined":2,"writes":[{"at_us":15070,"bytes":57,"kind":"head","slot":0},{"at_us":71732,"bytes":61,"kind":"head","slot":1},{"at_us":71753,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":1,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":61998,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202db1f1e3f00-dee3-50"} +{"run_id":"unchanged-build-replication","case":"full-body-error","completed_us":62876,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6274,"slot":0,"token":"18e202db3e2214a8-dee3-51"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202db3e2214a8-dee3-51","watchdog":false,"workers_joined":1,"writes":[{"at_us":6294,"bytes":61,"kind":"head","slot":0},{"at_us":6309,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":1,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202db3e2214a8-dee3-51"} +{"run_id":"unchanged-build-replication","case":"full-body-error","completed_us":64210,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":7876,"slot":0,"token":"18e202db436727f0-dee3-52"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202db436727f0-dee3-52","watchdog":false,"workers_joined":1,"writes":[{"at_us":7895,"bytes":61,"kind":"head","slot":0},{"at_us":7908,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":1,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202db436727f0-dee3-52"} +{"run_id":"unchanged-build-replication","case":"mixed-head-body","completed_us":501487,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":6042,"slot":1,"token":"18e202db475c1488-dee3-53"},{"path":"/readiness/mixed-head-body/0","received_us":6065,"slot":0,"token":"18e202db475c1488-dee3-53"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e202db475c1488-dee3-53","watchdog":false,"workers_joined":2,"writes":[{"at_us":6076,"bytes":61,"kind":"head","slot":1},{"at_us":6094,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":2,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":6182,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202db475c1488-dee3-53"} +{"run_id":"unchanged-build-replication","case":"mixed-body-head","completed_us":502964,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":6642,"slot":1,"token":"18e202db6563abf8-dee3-54"},{"path":"/readiness/mixed-body-head/0","received_us":6650,"slot":0,"token":"18e202db6563abf8-dee3-54"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e202db6563abf8-dee3-54","watchdog":false,"workers_joined":2,"writes":[{"at_us":6668,"bytes":57,"kind":"head","slot":0},{"at_us":70740,"bytes":61,"kind":"head","slot":1},{"at_us":70757,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":2,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":70668,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202db6563abf8-dee3-54"} +{"run_id":"unchanged-build-replication","case":"full-body-error","completed_us":63681,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5842,"slot":0,"token":"18e202db84335df8-dee3-55"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202db84335df8-dee3-55","watchdog":false,"workers_joined":1,"writes":[{"at_us":5857,"bytes":61,"kind":"head","slot":0},{"at_us":5871,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":2,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202db84335df8-dee3-55"} +{"run_id":"unchanged-build-replication","case":"full-body-error","completed_us":60883,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5294,"slot":0,"token":"18e202db88173b60-dee3-56"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e202db88173b60-dee3-56","watchdog":false,"workers_joined":1,"writes":[{"at_us":5305,"bytes":61,"kind":"head","slot":0},{"at_us":5319,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":2,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e202db88173b60-dee3-56"} +{"run_id":"ci-repaired-runtime","case":"mixed-head-body","completed_us":500685,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":5969,"slot":1,"token":"18e228809cca0180-e007-0"},{"path":"/readiness/mixed-head-body/0","received_us":6162,"slot":0,"token":"18e228809cca0180-e007-0"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e228809cca0180-e007-0","watchdog":false,"workers_joined":2,"writes":[{"at_us":5997,"bytes":61,"kind":"head","slot":1},{"at_us":6041,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":0,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":3951,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e228809cca0180-e007-0"} +{"run_id":"ci-repaired-runtime","case":"mixed-body-head","completed_us":500964,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":5514,"slot":0,"token":"18e22880baf79398-e007-1"},{"path":"/readiness/mixed-body-head/1","received_us":5521,"slot":1,"token":"18e22880baf79398-e007-1"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e22880baf79398-e007-1","watchdog":false,"workers_joined":2,"writes":[{"at_us":5531,"bytes":57,"kind":"head","slot":0},{"at_us":64241,"bytes":61,"kind":"head","slot":1},{"at_us":64256,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":0,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":63868,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e22880baf79398-e007-1"} +{"run_id":"ci-repaired-runtime","case":"full-body-error","completed_us":56499,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6211,"slot":0,"token":"18e22880d933eaf0-e007-2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e22880d933eaf0-e007-2","watchdog":false,"workers_joined":1,"writes":[{"at_us":6223,"bytes":61,"kind":"head","slot":0},{"at_us":6238,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":0,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e22880d933eaf0-e007-2"} +{"run_id":"ci-repaired-runtime","case":"full-body-error","completed_us":58924,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5405,"slot":0,"token":"18e22880dcc2b700-e007-3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e22880dcc2b700-e007-3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5423,"bytes":61,"kind":"head","slot":0},{"at_us":5458,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":0,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e22880dcc2b700-e007-3"} +{"run_id":"ci-repaired-runtime","case":"mixed-head-body","completed_us":500514,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":5338,"slot":0,"token":"18e22880e0766090-e007-4"},{"path":"/readiness/mixed-head-body/1","received_us":5344,"slot":1,"token":"18e22880e0766090-e007-4"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e22880e0766090-e007-4","watchdog":false,"workers_joined":2,"writes":[{"at_us":5358,"bytes":61,"kind":"head","slot":1},{"at_us":5371,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":1,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":5325,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e22880e0766090-e007-4"} +{"run_id":"ci-repaired-runtime","case":"mixed-body-head","completed_us":501853,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":5795,"slot":1,"token":"18e22880fe95b620-e007-5"},{"path":"/readiness/mixed-body-head/0","received_us":5926,"slot":0,"token":"18e22880fe95b620-e007-5"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e22880fe95b620-e007-5","watchdog":false,"workers_joined":2,"writes":[{"at_us":5956,"bytes":57,"kind":"head","slot":0},{"at_us":62589,"bytes":61,"kind":"head","slot":1},{"at_us":62628,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":1,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":62660,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e22880fe95b620-e007-5"} +{"run_id":"ci-repaired-runtime","case":"full-body-error","completed_us":60566,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5700,"slot":0,"token":"18e228811cceaa48-e007-6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e228811cceaa48-e007-6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5720,"bytes":61,"kind":"head","slot":0},{"at_us":5735,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":1,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e228811cceaa48-e007-6"} +{"run_id":"ci-repaired-runtime","case":"full-body-error","completed_us":59581,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5480,"slot":0,"token":"18e2288120a072c8-e007-7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e2288120a072c8-e007-7","watchdog":false,"workers_joined":1,"writes":[{"at_us":5498,"bytes":61,"kind":"head","slot":0},{"at_us":5516,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":1,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288120a072c8-e007-7"} +{"run_id":"ci-repaired-runtime","case":"mixed-head-body","completed_us":501026,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":5795,"slot":1,"token":"18e22881247ca740-e007-8"},{"path":"/readiness/mixed-head-body/0","received_us":5956,"slot":0,"token":"18e22881247ca740-e007-8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e22881247ca740-e007-8","watchdog":false,"workers_joined":2,"writes":[{"at_us":5820,"bytes":61,"kind":"head","slot":1},{"at_us":5835,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":2,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":5777,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e22881247ca740-e007-8"} +{"run_id":"ci-repaired-runtime","case":"mixed-body-head","completed_us":501602,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":7273,"slot":1,"token":"18e2288142bcdec8-e007-9"},{"path":"/readiness/mixed-body-head/0","received_us":7404,"slot":0,"token":"18e2288142bcdec8-e007-9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e2288142bcdec8-e007-9","watchdog":false,"workers_joined":2,"writes":[{"at_us":7424,"bytes":57,"kind":"head","slot":0},{"at_us":67870,"bytes":61,"kind":"head","slot":1},{"at_us":67906,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":2,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":67259,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288142bcdec8-e007-9"} +{"run_id":"ci-repaired-runtime","case":"full-body-error","completed_us":59592,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6600,"slot":0,"token":"18e2288160f244c8-e007-a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e2288160f244c8-e007-a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6620,"bytes":61,"kind":"head","slot":0},{"at_us":6643,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":2,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288160f244c8-e007-a"} +{"run_id":"ci-repaired-runtime","case":"full-body-error","completed_us":58474,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5753,"slot":0,"token":"18e2288164b76ed0-e007-b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e2288164b76ed0-e007-b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5798,"bytes":61,"kind":"head","slot":0},{"at_us":5835,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":2,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":{"category":"bad_gateway","source_unexpected_eof":true},"kind":"error"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288164b76ed0-e007-b"} +{"run_id":"ci-stock-runtime","case":"mixed-head-body","completed_us":500601,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":12239,"slot":1,"token":"18e2288c1b9c1100-f8dd-0"},{"path":"/readiness/mixed-head-body/0","received_us":12251,"slot":0,"token":"18e2288c1b9c1100-f8dd-0"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e2288c1b9c1100-f8dd-0","watchdog":false,"workers_joined":2,"writes":[{"at_us":12259,"bytes":61,"kind":"head","slot":1},{"at_us":12285,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":0,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":4304,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288c1b9c1100-f8dd-0"} +{"run_id":"ci-stock-runtime","case":"mixed-body-head","completed_us":500385,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":5350,"slot":1,"token":"18e2288c3a24daa8-f8dd-1"},{"path":"/readiness/mixed-body-head/0","received_us":5358,"slot":0,"token":"18e2288c3a24daa8-f8dd-1"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e2288c3a24daa8-f8dd-1","watchdog":false,"workers_joined":2,"writes":[{"at_us":5374,"bytes":57,"kind":"head","slot":0},{"at_us":65196,"bytes":61,"kind":"head","slot":1},{"at_us":65214,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":0,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":65057,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288c3a24daa8-f8dd-1"} +{"run_id":"ci-stock-runtime","case":"full-body-error","completed_us":57341,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6066,"slot":0,"token":"18e2288c58265f68-f8dd-2"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e2288c58265f68-f8dd-2","watchdog":false,"workers_joined":1,"writes":[{"at_us":6080,"bytes":61,"kind":"head","slot":0},{"at_us":6094,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":0,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":null,"kind":"eof"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288c58265f68-f8dd-2"} +{"run_id":"ci-stock-runtime","case":"full-body-error","completed_us":60722,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5790,"slot":0,"token":"18e2288c5bb07088-f8dd-3"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e2288c5bb07088-f8dd-3","watchdog":false,"workers_joined":1,"writes":[{"at_us":5813,"bytes":61,"kind":"head","slot":0},{"at_us":5833,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":0,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":null,"kind":"eof"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288c5bb07088-f8dd-3"} +{"run_id":"ci-stock-runtime","case":"mixed-head-body","completed_us":501687,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/1","received_us":5739,"slot":1,"token":"18e2288c5f6edfe8-f8dd-4"},{"path":"/readiness/mixed-head-body/0","received_us":5753,"slot":0,"token":"18e2288c5f6edfe8-f8dd-4"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e2288c5f6edfe8-f8dd-4","watchdog":false,"workers_joined":2,"writes":[{"at_us":5758,"bytes":61,"kind":"head","slot":1},{"at_us":5778,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":1,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":5519,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288c5f6edfe8-f8dd-4"} +{"run_id":"ci-stock-runtime","case":"mixed-body-head","completed_us":501120,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/0","received_us":6488,"slot":0,"token":"18e2288c7d77f628-f8dd-5"},{"path":"/readiness/mixed-body-head/1","received_us":6498,"slot":1,"token":"18e2288c7d77f628-f8dd-5"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e2288c7d77f628-f8dd-5","watchdog":false,"workers_joined":2,"writes":[{"at_us":6510,"bytes":57,"kind":"head","slot":0},{"at_us":63318,"bytes":61,"kind":"head","slot":1},{"at_us":63350,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":1,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":63252,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288c7d77f628-f8dd-5"} +{"run_id":"ci-stock-runtime","case":"full-body-error","completed_us":57060,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5260,"slot":0,"token":"18e2288c9b7ab368-f8dd-6"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e2288c9b7ab368-f8dd-6","watchdog":false,"workers_joined":1,"writes":[{"at_us":5275,"bytes":61,"kind":"head","slot":0},{"at_us":5287,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":1,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":null,"kind":"eof"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288c9b7ab368-f8dd-6"} +{"run_id":"ci-stock-runtime","case":"full-body-error","completed_us":60758,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6140,"slot":0,"token":"18e2288c9f0535d0-f8dd-7"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e2288c9f0535d0-f8dd-7","watchdog":false,"workers_joined":1,"writes":[{"at_us":6157,"bytes":61,"kind":"head","slot":0},{"at_us":6170,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":1,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":null,"kind":"eof"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288c9f0535d0-f8dd-7"} +{"run_id":"ci-stock-runtime","case":"mixed-head-body","completed_us":500800,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-head-body","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-head-body/0","received_us":6027,"slot":0,"token":"18e2288ca2ca0dd0-f8dd-8"},{"path":"/readiness/mixed-head-body/1","received_us":6033,"slot":1,"token":"18e2288ca2ca0dd0-f8dd-8"}],"terminations":[{"reason":"complete","slot":1},{"reason":"harness-stop","slot":0}],"token":"18e2288ca2ca0dd0-f8dd-8","watchdog":false,"workers_joined":2,"writes":[{"at_us":6051,"bytes":61,"kind":"head","slot":1},{"at_us":6063,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":2,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":5603,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288ca2ca0dd0-f8dd-8"} +{"run_id":"ci-stock-runtime","case":"mixed-body-head","completed_us":501369,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":2,"case":"mixed-body-head","errors":[],"harness_shutdown":true,"max_workers":2,"requests":[{"path":"/readiness/mixed-body-head/1","received_us":5309,"slot":1,"token":"18e2288cc0cf5b50-f8dd-9"},{"path":"/readiness/mixed-body-head/0","received_us":5320,"slot":0,"token":"18e2288cc0cf5b50-f8dd-9"}],"terminations":[{"reason":"complete","slot":1},{"reason":"peer-close","slot":0}],"token":"18e2288cc0cf5b50-f8dd-9","watchdog":false,"workers_joined":2,"writes":[{"at_us":5343,"bytes":57,"kind":"head","slot":0},{"at_us":63058,"bytes":61,"kind":"head","slot":1},{"at_us":63073,"bytes":32769,"kind":"body","slot":1}]},"qualification":false,"repetition":2,"result":{"Ok":{"slots":[{"kind":"pending"},{"bytes":32769,"checksum":"6529bfd5f3601707","elapsed_us":62919,"kind":"ok"}],"termination":"Cutoff"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288cc0cf5b50-f8dd-9"} +{"run_id":"ci-stock-runtime","case":"full-body-error","completed_us":58402,"consumption":"buffered","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":6518,"slot":0,"token":"18e2288cdede12f8-f8dd-a"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e2288cdede12f8-f8dd-a","watchdog":false,"workers_joined":1,"writes":[{"at_us":6537,"bytes":61,"kind":"head","slot":0},{"at_us":6550,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":2,"result":{"Ok":{"bytes":0,"checksum":"cbf29ce484222325","failure":null,"kind":"eof"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288cdede12f8-f8dd-a"} +{"run_id":"ci-stock-runtime","case":"full-body-error","completed_us":55503,"consumption":"streamed","event":"fastly-client-progress","origin":{"accepted":1,"case":"full-body-error","errors":[],"harness_shutdown":true,"max_workers":1,"requests":[{"path":"/readiness/full-body-error/0","received_us":5621,"slot":0,"token":"18e2288ce27fc6e0-f8dd-b"}],"terminations":[{"reason":"complete","slot":0}],"token":"18e2288ce27fc6e0-f8dd-b","watchdog":false,"workers_joined":1,"writes":[{"at_us":5637,"bytes":61,"kind":"head","slot":0},{"at_us":5652,"bytes":32769,"kind":"body","slot":0}]},"qualification":false,"repetition":2,"result":{"Ok":{"bytes":32769,"checksum":"6529bfd5f3601707","failure":null,"kind":"eof"}},"revision":"f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a","token":"18e2288ce27fc6e0-f8dd-b"} diff --git a/docs/audits/evidence/2026-10-26-fastly-cooperative-progress.json b/docs/audits/evidence/2026-10-26-fastly-cooperative-progress.json new file mode 100644 index 000000000..53534802b --- /dev/null +++ b/docs/audits/evidence/2026-10-26-fastly-cooperative-progress.json @@ -0,0 +1,305 @@ +{ + "schema": 1, + "execution_date": "2026-10-26", + "qualification": false, + "edgezero_base_revision": "f5d74b4cdb9f08e9986e04c95aeb86a43bad7d0a", + "source_state": "working-tree checkpoint, not a frozen candidate", + "production_sources": [ + { + "path": "crates/edgezero-adapter-fastly/src/outbound.rs", + "sha256": "45ef79f074cf6d37ba9bfc385726997b53c5357d38e6f3e53c79dd08064548e8" + }, + { + "path": "crates/edgezero-adapter-fastly/src/outbound/progress.rs", + "sha256": "52b92e0d5a2e01afd6e4848f64001e9e447de45f6a223cef8eb944fbc59f0538" + }, + { + "path": "crates/edgezero-core/src/outbound.rs", + "sha256": "ab89cc5559d97260189e0594a6d900b3ad74167e66599bc907e12d6d2e2e6541" + } + ], + "execution_environment": "Local macOS; not an isolated-load benchmark. Earlier integration runs overlapped source verification. Later exact-build repetitions retain all timing misses; no scheduling cause or hard latency bound is inferred.", + "command": "CARGO_NET_OFFLINE=true SSL_CERT_FILE=/etc/ssl/cert.pem VICEROY_BIN= scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-readiness --output ", + "runtime_version": "0.21.0", + "runtime_upstream_revision": "a69d982d1d95e13c0fc9f92683b9118639bb4b04", + "runtime_binary_sha256": "98ea0536478ce2e641f093b2619b7ac7c6c237402f140e5792d854575abc7100", + "runtime_patch_sha256": "8693ce34f82cc98dc272bf39379007b17a40dfc19138c55b445378fd9a163759", + "fastly_sdk": "0.13.1", + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)", + "deployed": false, + "external_requests": false, + "runtime_pins_changed": false, + "public_protocol_changed": false, + "retained_client_evidence": "2026-10-26-fastly-cooperative-progress-clients.jsonl", + "retained_client_evidence_sha256": "aad260ee367d3eb08f62e7ddd53bc8d3e58d490bc6e9889ef9dab8fe63baf7eb", + "disposition": "Actual-client functional inventory passes twice (12 probes, 18 origin requests each). Complete SDK timing inventories still fail; failures are not waived or relabeled. No hosted, callback-wide ownership, resource, or total scheduler-bound qualification.", + "runs": [ + { + "id": "serial-drain-red", + "directory": "/private/tmp/edgezero-fastly-multiplexer-red-20261025", + "exit_code": 1, + "guest_sha256": "fd35d77a1f13131e1c567451575b4f262205b67916b8fce4e355f6f54f19f429", + "guest_bytes": 1619478, + "events_sha256": "9682b8d9ff2a4bc86cee6b1ec2f9e8dfb9cdb98b099931f31705a9c14d10c807", + "log_sha256": "02a9c07b56d3886884e8be1a12c7179768c35a93779cbb798d282f1cc31d4b6d", + "actual_client_probes": 4, + "actual_client_origin_requests": 6, + "client_inventory": null, + "sdk_probes": 153, + "sdk_origin_requests": 177, + "failures": [ + { + "event": null, + "case": "readiness-inventory", + "error": "mixed-body-head/buffered: actual outbound client did not preserve sibling/error result" + } + ], + "qualification": false + }, + { + "id": "first-integration", + "directory": "/private/tmp/edgezero-fastly-multiplexer-green-20261025", + "exit_code": 1, + "guest_sha256": "3ee5f2ae8b0e9a22646b6760ce3a8b2e2713893937931688edf2a3a105bd6c3f", + "guest_bytes": 1654263, + "events_sha256": "71aeef1aa188bd22c85f87fb98a9e950cea4de2e228f49935a89e3ef65dee4e0", + "log_sha256": "862988d154664cebb80dc84cb79d2b58705f857d37d704d05b9c6e9a39504adc", + "actual_client_probes": 4, + "actual_client_origin_requests": 3, + "client_inventory": null, + "sdk_probes": 153, + "sdk_origin_requests": 176, + "failures": [ + { + "event": null, + "case": "readiness-inventory", + "error": "readiness: 19 readiness leg(s) failed:\nsilent-head/selected/buffered/16384/0: reordered or late operation\npending-body/selected/buffered/1/0: reordered or late operation\npending-body/selected/streamed/1/1: reordered or late operation\ntrickle/selected/buffered/1/1: reordered or late operation\nsiblings/selected/buffered/1/1: reordered or late operation\nsiblings/selected/streamed/16384/1: reordered or late operation\nexhausted/selected/buffered/1/1: reordered or late operation\nexhausted/selected/streamed/16384/1: reordered or late operation\nsilent-head/selected/streamed/1/2: reordered or late operation\nsilent-head/selected/streamed/16384/2: reordered or late operation\ntrickle/selected/buffered/1/2: reordered or late operation\ntrickle/selected/streamed/1/2: reordered or late operation\ntrickle/selected/streamed/16384/2: reordered or late operation\nsiblings/selected/streamed/1/2: reordered or late operation\nsiblings/selected/streamed/16384/2: reordered or late operation\nexhausted/selected/buffered/1/2: reordered or late operation\nexhausted/selected/buffered/16384/2: reordered or late operation\nexhausted/selected/streamed/16384/2: reordered or late operation\neof/selected/buffered/1/2: origin workers, errors or containment invalid\nmixed/full-buffer progress: 5 readiness leg(s) failed:\nmixed-body-head/selected/streamed/16384/0: reordered or late operation\nmixed-head-body/selected/buffered/16384/1: reordered or late operation\nmixed-body-head/selected/streamed/16384/1: reordered or late operation\nmixed-head-body/selected/streamed/16384/2: reordered or late operation\nmixed-body-head/selected/buffered/16384/2: reordered or late operation\nmixed-head-body/buffered: actual outbound client did not preserve sibling/error result\nmixed-body-head/buffered: actual outbound client did not preserve sibling/error result" + } + ], + "qualification": false + }, + { + "id": "typed-integration", + "directory": "/private/tmp/edgezero-fastly-multiplexer-typed-20261025", + "exit_code": 1, + "guest_sha256": "b275d9e47d911bd23bcb8ba522bf550e86d836d24efd1db1bec4b8b61f2f5d54", + "guest_bytes": 1648760, + "events_sha256": "662484721ee79ebf02bf9913f542f0a5cedca1f6b877970ba51091cb06444498", + "log_sha256": "3edf37c83820ef408e1e8b99d38656a0ee81cce6bb789fe0a1c1c9f65a89f0ec", + "actual_client_probes": 4, + "actual_client_origin_requests": 5, + "client_inventory": null, + "sdk_probes": 153, + "sdk_origin_requests": 177, + "failures": [ + { + "event": null, + "case": "readiness-inventory", + "error": "readiness: 13 readiness leg(s) failed:\nsilent-head/selected/buffered/16384/0: reordered or late operation\nsilent-head/selected/streamed/1/0: reordered or late operation\npending-body/selected/streamed/1/0: reordered or late operation\nsiblings/selected/buffered/16384/0: missing/late probe completion\nsilent-head/selected/streamed/16384/1: reordered or late operation\npending-body/selected/buffered/16384/1: reordered or late operation\ntrickle/selected/buffered/1/1: reordered or late operation\ntrickle/selected/buffered/16384/1: reordered or late operation\nsiblings/selected/streamed/1/1: reordered or late operation\nsiblings/selected/streamed/16384/1: reordered or late operation\nsilent-head/selected/streamed/1/2: reordered or late operation\nsilent-head/selected/streamed/16384/2: reordered or late operation\ntrickle/selected/streamed/1/2: reordered or late operation\nmixed-head-body/buffered: actual outbound client did not preserve sibling/error result" + } + ], + "qualification": false + }, + { + "id": "complete-client-inventory", + "directory": "/private/tmp/edgezero-fastly-multiplexer-final-20261026", + "exit_code": 1, + "guest_sha256": "263db1806a9ad2bf80e768c3b12ddb1ac82392014e7f06fdf7ee39b5690c100b", + "guest_bytes": 1650405, + "events_sha256": "6f731202d3fdbd93153f772e23838eab13d0d97daa7a8d8ef1a7e18a5acc1f34", + "log_sha256": "0aada96fd6c12a18a8976412e63bfa7f8a0ecde1fcbbafc5d63fe7aa497f1ac1", + "actual_client_probes": 12, + "actual_client_origin_requests": 18, + "client_inventory": { + "error": null, + "event": "fastly-client-progress-inventory", + "expected_probes": 12, + "origin_requests": 18, + "probes": 12, + "qualification": false, + "status": "pass" + }, + "sdk_probes": 153, + "sdk_origin_requests": 177, + "failures": [ + { + "event": null, + "case": "readiness-inventory", + "error": "readiness: 2 readiness leg(s) failed:\nsilent-head/selected/streamed/16384/1: reordered or late operation\npending-body/selected/buffered/16384/1: reordered or late operation" + } + ], + "qualification": false + }, + { + "id": "unchanged-build-replication", + "directory": "/private/tmp/edgezero-fastly-multiplexer-replication-20261026", + "exit_code": 1, + "guest_sha256": "263db1806a9ad2bf80e768c3b12ddb1ac82392014e7f06fdf7ee39b5690c100b", + "guest_bytes": 1650405, + "events_sha256": "e0a9e85780616f386cc0eb910c2b9b3dae78a921e8022ac1725e336d34a799db", + "log_sha256": "4b8e24063fd0c8610e646e109d23afcc0f00bfba22648660d74a2d2231ffa4c9", + "actual_client_probes": 12, + "actual_client_origin_requests": 18, + "client_inventory": { + "error": null, + "event": "fastly-client-progress-inventory", + "expected_probes": 12, + "origin_requests": 18, + "probes": 12, + "qualification": false, + "status": "pass" + }, + "sdk_probes": 153, + "sdk_origin_requests": 177, + "failures": [ + { + "event": null, + "case": "readiness-inventory", + "error": "readiness: 10 readiness leg(s) failed:\ntrickle/selected/buffered/16384/0: reordered or late operation\ntrickle/selected/streamed/16384/1: missing/late probe completion\nsiblings/selected/buffered/16384/1: reordered or late operation\nsiblings/selected/streamed/1/1: reordered or late operation\nsilent-head/selected/streamed/16384/2: reordered or late operation\npending-body/selected/buffered/1/2: reordered or late operation\nsiblings/selected/buffered/1/2: reordered or late operation\nsiblings/selected/buffered/16384/2: reordered or late operation\nsiblings/selected/streamed/1/2: reordered or late operation\nexhausted/selected/buffered/1/2: reordered or late operation\nmixed/full-buffer progress: 2 readiness leg(s) failed:\nmixed-head-body/selected/streamed/16384/2: missing/late probe completion\nmixed-body-head/selected/streamed/16384/2: reordered or late operation" + } + ], + "qualification": false + } + ], + "functional_ci": { + "suite": "outbound-client-progress", + "command": "CARGO_NET_OFFLINE=true SSL_CERT_FILE=/etc/ssl/cert.pem VICEROY_BIN= scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-client-progress --output ", + "full_sdk_timing_validator_changed": false, + "qualification": false, + "runtime_rebuild": { + "upstream_revision": "a69d982d1d95e13c0fc9f92683b9118639bb4b04", + "lock_sha256": "d642d177081f42aec75d25c0d1124c013d3abcb081886251420a90e9c53c1a2a", + "patch_sha256": "8693ce34f82cc98dc272bf39379007b17a40dfc19138c55b445378fd9a163759", + "body_source_sha256": "1c56533d85a5c26390f9224e102425cbecc0fe4a280513e00095e7117d6722f7", + "profile": "dev, upstream opt-level=1; debug=0; incremental=0; build jobs=2", + "readiness_tests": 10, + "body_and_collector_tests": 24, + "stock_installation_sha256_before_and_after": "751ff2c1a83063b109bc212b95be5f52e5ab6284b9027fa03aa229087eba3f30" + }, + "harness_sources": [ + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs", + "sha256": "b4bce9ae9d29e58a80228eb96bdf300520db07857541f45a3e1e80623d03c079" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs", + "sha256": "5bbdd819a0c94eec7c9149568b337c3c578478161744d03c0eb1717c731a69dd" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/runners/readiness.rs", + "sha256": "e38161a8076131df0357e6964200e4d413cc44894c48dcbc635ad97ee11649e1" + }, + { + "path": "tests/fixtures/reusable-app/crates/fixture-harness/src/evidence/readiness.rs", + "sha256": "e94f385233f6b16b95470aa21136ce598c843d152e422e12439fb8614a178369" + }, + { + "path": ".github/workflows/test.yml", + "sha256": "e5eecd04ea2dd778d513e9f8eafd3c13ad01196f89cd86c5c552d502d03297dd" + } + ], + "unit_tests": { + "harness": 139, + "workspace": 2257, + "workspace_ignored": 1, + "fastly_wasi": 210, + "fastly_contracts": 35 + }, + "gate_controls": { + "repaired_exit_2_accepted": true, + "unavailable_exit_2_rejected": true, + "stock_actual_exit_1_rejected": true, + "stock_forced_exit_2_rejected": true + }, + "runs": [ + { + "id": "ci-repaired-runtime", + "directory": "/private/tmp/edgezero-fastly-client-ci-repaired-20261026", + "exit_code": 2, + "guard_accepts": true, + "runtime_binary_sha256": "f61b16011483d2c46c812093b14f7d52f932ead27998430a28d79af960b0b634", + "guest_sha256": "263db1806a9ad2bf80e768c3b12ddb1ac82392014e7f06fdf7ee39b5690c100b", + "guest_bytes": 1650405, + "events_sha256": "5e327683022c065243fad63a30a422183dc477a1ea912fc54bc46cf49047b6f9", + "log_sha256": "d1116205715c1fe4476e75652ab8f9490822bd18a86331dc42b258c299646a12", + "inventory": { + "error": null, + "event": "fastly-client-progress-inventory", + "expected_probes": 12, + "origin_requests": 18, + "probes": 12, + "qualification": false, + "status": "pass" + }, + "sdk_probe_records": 0, + "sdk_inventory_records": 0, + "timer_records": 0, + "source_error_failures": [], + "qualification": false + }, + { + "id": "ci-stock-runtime", + "directory": "/private/tmp/edgezero-fastly-client-ci-stock-20261026", + "exit_code": 1, + "guard_accepts": false, + "runtime_binary_sha256": "751ff2c1a83063b109bc212b95be5f52e5ab6284b9027fa03aa229087eba3f30", + "guest_sha256": "263db1806a9ad2bf80e768c3b12ddb1ac82392014e7f06fdf7ee39b5690c100b", + "guest_bytes": 1650405, + "events_sha256": "b877764b5537411eb549e7227f578ace1cd915af863e6edcd37cd7c2682202cc", + "log_sha256": "05a7a56edef1ad3407e6af84c5fa6a4a526692f4e3572b4fbdd3d8b7f1ca581e", + "inventory": { + "error": "full-body-error/client/buffered/16384/0: actual-client typed UnexpectedEof failure was lost\nfull-body-error/client/streamed/16384/0: actual-client typed UnexpectedEof failure was lost\nfull-body-error/client/buffered/16384/1: actual-client typed UnexpectedEof failure was lost\nfull-body-error/client/streamed/16384/1: actual-client typed UnexpectedEof failure was lost\nfull-body-error/client/buffered/16384/2: actual-client typed UnexpectedEof failure was lost\nfull-body-error/client/streamed/16384/2: actual-client typed UnexpectedEof failure was lost\n6 actual-client leg(s) failed:\nfull-body-error/client/buffered/16384/0: actual-client typed UnexpectedEof failure was lost\nfull-body-error/client/streamed/16384/0: actual-client typed UnexpectedEof failure was lost\nfull-body-error/client/buffered/16384/1: actual-client typed UnexpectedEof failure was lost\nfull-body-error/client/streamed/16384/1: actual-client typed UnexpectedEof failure was lost\nfull-body-error/client/buffered/16384/2: actual-client typed UnexpectedEof failure was lost\nfull-body-error/client/streamed/16384/2: actual-client typed UnexpectedEof failure was lost", + "event": "fastly-client-progress-inventory", + "expected_probes": 12, + "origin_requests": 18, + "probes": 12, + "qualification": false, + "status": "fail" + }, + "sdk_probe_records": 0, + "sdk_inventory_records": 0, + "timer_records": 0, + "source_error_failures": [ + { + "case": "full-body-error", + "consumption": "buffered", + "repetition": 0, + "error": "actual-client typed UnexpectedEof failure was lost" + }, + { + "case": "full-body-error", + "consumption": "streamed", + "repetition": 0, + "error": "actual-client typed UnexpectedEof failure was lost" + }, + { + "case": "full-body-error", + "consumption": "buffered", + "repetition": 1, + "error": "actual-client typed UnexpectedEof failure was lost" + }, + { + "case": "full-body-error", + "consumption": "streamed", + "repetition": 1, + "error": "actual-client typed UnexpectedEof failure was lost" + }, + { + "case": "full-body-error", + "consumption": "buffered", + "repetition": 2, + "error": "actual-client typed UnexpectedEof failure was lost" + }, + { + "case": "full-body-error", + "consumption": "streamed", + "repetition": 2, + "error": "actual-client typed UnexpectedEof failure was lost" + } + ], + "qualification": false + } + ], + "disposition": "The functional gate passes only the repaired runtime. Identical guest bytes on stock Viceroy lose all six typed truncation errors and fail. The unavailable-runtime exit 2 has no inventory and is rejected. No SDK timing, hosted, callback-wide or resource qualification is inferred." + } +} diff --git a/docs/guide/adapters/axum.md b/docs/guide/adapters/axum.md index 80b3f3e01..6157cc656 100644 --- a/docs/guide/adapters/axum.md +++ b/docs/guide/adapters/axum.md @@ -41,6 +41,8 @@ variables (see [the migration guide](../manifest-store-migration.md)). The portable store metadata baked into `App` by the `app!` macro drives which logical stores are exposed; no `edgezero.toml` needs to be loaded by the runtime. +If `Hooks::configure` fails, `run_app` returns the source-preserving +`application configuration failed` error before binding the listener. ## Development Server @@ -78,21 +80,172 @@ cargo build -p my-app-adapter-axum --release The binary is placed in `target/release/my-app-adapter-axum`. -## Proxy Client +## Bounded HTTP/1 Ingress -The Axum adapter provides a native HTTP client for proxying: +The standard server entrypoints own a bounded pool of HTTP/1 connections. These limits apply +before application admission, including to sockets that never finish a request head. No custom +listener or router is needed. HTTP/2 is not enabled, and protocol upgrades cannot transfer the +socket out of this pool. + +`run_app` resolves these runtime variables once, before application configuration or binding: + +| Variable (`EDGEZERO__ADAPTER__INGRESS__` prefix) | Default | Supported range | +| ------------------------------------------------ | ------- | --------------- | +| `MAX_CONNECTIONS` | 256 | 1–4096 | +| `MAX_RAW_HEAD_BYTES` | 65536 | 8192–1048576 | +| `MAX_HEADER_COUNT` | 100 | 1–1024 | +| `HEAD_READ_TIMEOUT_MS` | 10000 | 1–120000 | + +Only missing settings use defaults. Explicit values must be unsigned decimal integers within +the supported ranges; empty, signed, malformed, overflowing, and zero values fail startup. +Diagnostics identify the setting, never its supplied value. The effective validated +`AxumIngressConfig` is logged at `Info`. Applications can resolve the same configuration with +`AxumIngressConfig::from_env(&EnvConfig::from_env())` and inspect its getters. Programmatic +servers install the validated value in `AxumDevServerConfig::ingress`: ```rust -use edgezero_adapter_axum::proxy::AxumProxyClient; -use edgezero_core::proxy::ProxyService; +use std::time::Duration; +use edgezero_adapter_axum::{ingress_config::AxumIngressConfig, dev_server::AxumDevServerConfig}; -let client = AxumProxyClient::try_new()?; -let response = ProxyService::new(client).forward(request).await?; +let config = AxumDevServerConfig { + ingress: AxumIngressConfig::new(64, 16384, 64, Duration::from_secs(5))?, + ..AxumDevServerConfig::default() +}; ``` -This uses `reqwest` under the hood for outbound HTTP requests. `try_new` is -fallible because it builds a `reqwest::Client`; it returns a `reqwest::Error` if -the TLS backend cannot be initialised on the host. +Manifest defaults can be supplied through adapter-filtered `[[environment.variables]]` entries. +The Axum CLI forwards the selected manifest's defaults to its Cargo child without mutating the +parent environment. An existing parent variable, including an empty one, takes precedence; +duplicate applicable defaults retain declaration order, with the last taking precedence. +Running the binary directly requires setting the runtime variables directly. + +### Connection Ownership And Deadlines + +Let `C` be `max_connections()`. At most `C` connection futures own parser state, sockets, and +application work, including keep-alive connections. There is no per-connection spawned task or +permit-waiter queue. The accept loop may own **one additional transient accepted socket**; if +capacity is exhausted it closes that socket immediately, without a parser, response, or +background task. Completed connections are reaped before accepting more work. Disconnect, +parser failure, timeout, cancellation, and shutdown drop the single connection owner and release +capacity; unwinding handler failures are isolated to their connection. Shutdown closes the +owned connections rather than waiting indefinitely for a graceful drain. + +The first absolute idle-plus-header deadline starts when the accepted socket enters the pool, +before its future is first polled. Each subsequent deadline starts when Hyper first polls the +next request head after the previous exchange permits it. Idle keep-alive time and incomplete-head +time share this budget; a byte arriving does **not** reset it. Pipelined bytes may already be +buffered, so this is not a deadline starting at the network arrival of each pipelined request. +An independent timer wakes without incoming data, and ready I/O and completed heads also check +expiry before admission. Equality is expired. Slow/trickled or idle connections close when the +deadline expires; an HTTP timeout response is not promised. + +After a complete head is accepted, the header clock is disarmed for application work. Existing +application body-read and response-write deadlines remain in force. Infrastructure timers use +the native Tokio monotonic clock, independently of an application's injectable clock, so a +frozen application clock cannot disable pre-admission expiry. These timers are cooperatively +scheduled: blocking application code can delay all work on the connection executor. + +### Wire Limits And Parser Storage + +Let `H = max_raw_head_bytes()` and `N = max_header_count()`. The pinned Hyper 1.12.0 parser +rejects a complete raw head above `H` bytes or above `N` field lines **before** constructing an +admitted request. The byte budget includes the entire request line, field names, colons, +whitespace, line endings, and the final empty line. Duplicate field lines count separately. +Request-line and header bytes share one combined budget, not independent allowances. Exactly +`H` bytes and `N` fields pass the size/count checks; other protocol, type, or application validation +can still reject an in-budget head. Hyper additionally limits request targets to 65534 bytes +(414). Overflow closes the connection and Hyper attempts a bounded 431 response; other malformed +syntax may receive a bounded 400 response. Expiry or disconnect can prevent response delivery. +Application admission and rendering hooks do not run for raw parser rejections. + +`parser_buffer_bytes()` is Hyper's logical threshold `H`, **not** a hard allocation ceiling. +The receive scratch audit pins Hyper 1.12.0, bytes 1.12.1, http 1.4.2, and Rust 1.95.0: + +- `parser_read_allocation_bytes()` reports the conservative requested-capacity bound `R = 4H` + for each receive backing allocation. Adaptive reads expose spare capacity and shared-buffer + splits can cause a larger replacement allocation than the logical threshold. +- `parser_trailer_allocation_bytes()` reports `T = next_power_of_two(H)` for the separate raw + chunked-trailer buffer. Hyper applies its byte budget separately to trailers, whose completed + raw field section must be **strictly smaller** than `H`; this is not a combined head-plus-trailer + allowance. The trailer field count remains at most `N`. +- Conservative simultaneous receive scratch, including an outstanding channel frame, replacement + allocation, raw trailer growth, and copied field bytes, is `3R + 2T + H`, **plus** parser/header + metadata, URI/method storage, and fixed connection state. These are requested capacities, not + allocator-resident bytes or a complete per-request memory certificate. +- `parser_header_map_allocation_bytes()` reports `M`, a conservative per-map requested-capacity + charge covering both ordinary and adversarial collision-driven growth, duplicate-value storage, + and capacity retained across clear/reuse. For `P = max(100, N) + 1`, raw index capacity is bounded + by the largest power of two at most `10P`, bucket capacity by three quarters of that number, + and duplicate capacity by `next_power_of_two(max(4, P - 1))`. The getter uses public header-type + sizes plus conservative machine-word padding for private map nodes. Charge up to `1.5M` for + one map's reallocating peak, or `3.5M` for overlapping request, response/cache, and trailer maps. + An application-retained request map or admission's temporary header clone is an additional + owner, not included in those three maps. +- Raw parse workspaces additionally retain the default 100-field inline arrays and, above that, + bounded heap arrays. On 64-bit native builds a conservative payload charge for these two + workspaces is `64 * max(100, N)` bytes, plus their fixed container state. Parsed URI/method data + and field-name/value backing bytes are separate charges bounded by the raw input sizes. + +Response header maps become reusable request-parser storage in Hyper. Before handoff, EdgeZero +therefore bounds application response fields to `max_response_header_count() = max(100, N)` +and `max_response_header_bytes() = H`. Field-byte accounting is the sum of name bytes, value +bytes, and four bytes per field (`: ` and CRLF), including duplicates. EdgeZero copies the map +and field data into fresh storage, discarding application-supplied spare capacity and large +shared backing buffers; duplicate values and sensitive flags are preserved. Custom Hyper +reason phrases are removed in favor of canonical status phrases. Hyper's own status line, +framing, connection, and date fields need additional fixed overhead. An oversized response head +closes the connection and reports a transport error to any active egress attempt; it does not +allocate a replacement error response. + +The pool bounds retention of receive scratch, parser-generated error responses, and parser +metadata, not arbitrary application allocations. A budget must additionally charge parsed +request/response metadata and copies, application-held body frames, response buffers, handler +state, stores, outbound exchanges, and allocator overhead. Kernel socket buffers, the OS listen +backlog, and resources outside EdgeZero are excluded. The connection bound is not a published +platform memory ceiling or a whole-process RSS guarantee. + +This combined HTTP/1 resource policy does not implement the stricter independent raw-target / +field-section accounting or raw framing-rejection policy. Admission still receives +`IngressHeadAccounting::HostManaged` and `IngressFraming::HostManaged`; both raw-ingress +capabilities remain `Unsupported`. See [Capabilities](/guide/capabilities). + +## Outbound HTTP + +The Axum adapter uses a `HttpClient` registered in the assembled app's router state when present, +without initializing its default transport. Otherwise it injects `AxumOutboundClient`, backed by +`reqwest` and the application clock. Handlers use `RequestContext::http_client()`; direct wiring +can construct the adapter client with `AxumOutboundClient::try_new()`. + +After admission, request conversion preserves a transport-certified empty body +(`http_body::Body::is_end_stream()`) as buffered empty without polling it. This applies to every +method, not only GET/HEAD; headers and size hints alone never certify emptiness. The request +context still reports Initial, not Cached, and ingress framing stays `HostManaged`. + +Local HTTP/1 TCP checks forward GET/HEAD with absent framing or `Content-Length: 0`, even with an +expired inbound read deadline, without outbound body bytes or length/chunked framing. The demo +and generated proxy handlers benefit through this same converter and their unchanged client API. +This does not change core's first `body_bytes` deadline check: that accessor can still return 408 +for an initial buffered-empty body after expiry. + +Unknown GET/HEAD bodies are settled under the original outbound and admitted inbound deadlines. +Chunked-empty EOF permits dispatch; the first nonempty byte returns 400 before origin contact. +Unread rejected bytes cannot become the next HTTP/1 request. Other methods keep streamed uploads. +Eager begin/batch work requires the existing Tokio local scheduling context; missing context +returns a typed setup error before provider I/O. No capability level is promoted by this migration. + +Axum provides native total deadlines, completion-order batching, pending-future cancellation, +batch slot isolation, cache bypass, wire-authority override, elastic phase budgeting, header +fidelity, and streamed upload cancellation. Downstream responses run on a connection-local Hyper +HTTP/1 executor, so portable non-`Send` streams remain lazy and are polled under Hyper demand. +The connection supervisor enforces the absolute response-write deadline, but frame acceptance is +not proof of socket or client receipt; response-egress capabilities therefore remain BestEffort. +See [Capabilities](/guide/capabilities). + +For streamed responses with `Content-Length`, Axum retains only the final chunk until source EOF +validates, because Hyper stops polling once the declared length is satisfied. A bad tail is not +sent, and a pending tail remains subject to the write deadline. A declared zero-length stream is +validated before handoff. Suppressed HEAD/status bodies are dropped without polling. Clean source +EOF remains `HostHandoff`, not proof of socket flush or client receipt. ## Logging @@ -100,9 +253,38 @@ The Axum adapter's `run_app` helper installs `simple_logger` at the level read f `EDGEZERO__LOGGING__LEVEL`, falling back to `info` when the variable is unset or unparseable. It does not read `edgezero.toml`, and `echo_stdout` has no effect on the runtime. To install a different logger, set `owns_logging = true` on your `app!` -declaration so `run_app` skips its own logger, then install yours in `main`. Wiring -`App::build_app()` and `AxumDevServer` by hand remains the fallback if you also need -to control the bind address or store setup. +declaration so `run_app` skips its own logger, then install yours in `main` **before** `run_app`. +Logger setup now precedes application configuration, and setup failures are returned rather than +ignored. For embedding startup validation, `run_app_with_preflight::(check)` runs `check` +after logging is ready and before configuration or listener binding. The bundled `edgezero demo` +uses this for its capability gate instead of preinstalling a second CLI logger. + +Managed boot warnings use `edgezero::boot` at `Warn`, even when runtime logging is `Off`. +Application-owned loggers can reuse the shared resolver and constants: + +```rust +use edgezero_core::{BOOT_LOG_LEVEL, BOOT_LOG_TARGET, resolve_logging_level}; +use edgezero_core::env_config::EnvConfig; + +let env = EnvConfig::from_env(); +simple_logger::SimpleLogger::new() + .with_level(resolve_logging_level(&env)) + .with_module_level(BOOT_LOG_TARGET, BOOT_LOG_LEVEL) + .init()?; +// Then call run_app::() with owns_logging = true. +``` + +For custom bind addresses or store wiring, build an assembled application and pass it directly +to `AxumDevServer::new(app)` or `AxumDevServer::with_config(app, config)`. These constructors no +longer accept a router or rebuild default policies. All configured policies, state, clock, name +and platform metadata are preserved. Attach store handles/registries with the server's setters. +The standard `run_app` uses the same server construction and serving loop after startup setup. + +Use `server.run()` for a blocking runner, or +`server.serve(listener, shutdown).await` for an already-bound Tokio listener. The listener's actual +address takes precedence over `config.addr`. Resolving the shutdown future cancels active +connections and waits for the local worker and its resources; it does not drain responses. +Dropping the serving future signals cancellation but cannot wait for cleanup. ::: tip Logging status `run_app` wires logging automatically; custom entrypoints should install a logger explicitly. @@ -110,32 +292,48 @@ to control the bind address or store setup. ## Testing -The Axum adapter enables standard Rust testing: +Enable `edgezero-adapter-axum`'s `test-utils` feature for `AxumTestServer`. It binds real TCP and +runs the same bounded HTTP/1 parser, admission, body-read deadlines, response framing, streaming, +write deadlines and egress coordinator as the production server, with its own local executor: ```rust #[cfg(test)] mod tests { use super::*; - use edgezero_core::app::Hooks; - use edgezero_core::http::{Request, Method}; + use edgezero_adapter_axum::dev_server::{AxumDevServer, AxumDevServerConfig}; + use edgezero_adapter_axum::test_utils::AxumTestServer; + use edgezero_core::app::App as EdgeZeroApp; #[tokio::test] async fn test_handler() { - let app = App::build_app(); - let router = app.router(); - - let request = Request::builder() - .method(Method::GET) - .uri("/hello") - .body(Body::empty()) - .unwrap(); - - let response = router.oneshot(request).await.unwrap(); + let app = EdgeZeroApp::build::(edgezero_adapter_axum::AXUM_PLATFORM) + .expect("configured app"); + let server = AxumDevServer::with_config(app, AxumDevServerConfig { + addr: "127.0.0.1:0".parse().unwrap(), + enable_ctrl_c: false, + ..AxumDevServerConfig::default() + }); + let running = AxumTestServer::start(server).await.unwrap(); + let response = reqwest::get(format!("http://{}/hello", running.address())) + .await.unwrap(); assert_eq!(response.status(), 200); + running.shutdown().await.unwrap(); } } ``` +Use an HTTP client such as `reqwest` in the application's dev-dependencies. Select loopback port +zero explicitly to avoid port conflicts. `shutdown().await` cancels live connections, waits for +resource cleanup and returns the serving result; dropping the handle only signals cancellation. +Startup binding failures are returned by `start`, and later serving failures by `shutdown`. + +The helper skips logger initialization, startup preflight and metadata-driven store opening; +attach stores to the supplied server explicitly. It does not install a global test executor. +An app assembled with Cloudflare or Spin platform metadata keeps that metadata and all configured +policies, but receives Axum connection context, not a Cloudflare or Spin provider request context. +`router.oneshot` remains a routing-only unit-test facility: it skips this ingress and egress path. +Begun-outbound-request coverage additionally depends on the separate eager-dispatch implementation. + Run tests: ```bash @@ -175,6 +373,10 @@ EDGEZERO__SECRETS__API_KEY=mysecret edgezero serve --adapter axum For local development, each declared `[stores.config]` id resolves to a local-file config store backed by `.edgezero/local-config-.json`. +The standard runner loads immutable snapshots before binding its listener. +Missing files create empty declared stores. Malformed files retain typed failing +bindings, while allocation-limit violations fail startup. Changes made by `config +push` take effect after restart, not on the next request. The portable manifest carries no inline defaults — the pre-rewrite `[stores.config.defaults]` table is gone (see [the migration guide](../manifest-store-migration.md)). @@ -219,12 +421,59 @@ flow — the bundled `edgezero config push` errors), which writes the same `.edgezero/local-config-.json` files the runtime reads — no shell-out, no server to authenticate against. +### Snapshot Allocation Limits + +Set `EDGEZERO__ADAPTER__CONFIG_STORE__` through the normal environment +configuration/startup path. Absent settings use finite defaults; explicit invalid, +zero, overflowing or inconsistent settings fail before listener binding. + +| Setting | Default | Supported Range / Scope | +| -------------------- | ------- | -------------------------------------------------------------------------- | +| `MAX_FILE_BYTES` | 16 MiB | 1 byte to 256 MiB per file, including JSON syntax and escaping | +| `MAX_ENTRIES` | 1024 | 1 to 65536 entries per file, including duplicate-key attempts | +| `MAX_KEY_BYTES` | 1024 | 1 byte to 64 KiB decoded UTF-8 per key, no larger than the file cap | +| `MAX_VALUE_BYTES` | 8 MiB | 1 byte to 256 MiB decoded UTF-8 per value, no larger than the file cap | +| `MAX_RESIDENT_BYTES` | 32 MiB | 1 byte to 256 MiB across every config snapshot in the startup registry | +| `MAX_STARTUP_BYTES` | 112 MiB | 1 byte to 1 GiB across resident snapshots plus one file's reserved staging | + +These are requested-allocation charges, not an RSS ceiling. Stores load sequentially. +For wire cap `W`, staging reserves `5 * max(W, 32)` bytes: input growth, escaped-string +decoder growth (including old/new allocation overlap), and temporary decoded key/value +strings. A separate 4096-byte read buffer lives on the stack. The startup limit must +cover staging; every index/payload reservation also checks residency plus staging. +This decoder allowance is audited against pinned `serde_json 1.0.150` and Rust 1.95.0; +changing those allocation implementations requires repeating the audit. + +The sorted snapshot index reserves `MAX_ENTRIES * size_of::<(String, ConfigValue)>()` +bytes per store. Each entry additionally charges key UTF-8 bytes, value UTF-8 bytes, +two `Arc` reference counts and up to `size_of::() - 1` alignment bytes. Duplicate +keys are rejected. Keys/values are size-checked before their owned copies; actual reads +detect file growth and reject the first excess byte without appending it. Non-string +values are rejected without constructing a JSON tree. Failed loads drop staging and +partial snapshots. Diagnostics never contain file contents. + +`get` and `get_bounded` return `ConfigValue`, an independently owned, shared UTF-8 +payload. Clones allocate no payload and never retain the whole file. Bounded reads +check both extraction caps and the injected deadline before returning an `Arc` clone; +there is no request-time file read. Borrow with `as_str()` or `as_ref()`; `to_string()` +is an explicit allocating boundary. Already-owned `from_map` inputs are caller allocations +outside these startup-reader charges. + +Budget fixed manifest/registry/handle metadata separately, as well as allocator overhead, +environment-backed secret reads, concurrently parsed config trees, typed outputs and +application retention of returned handles. Canonical hashing streams into SHA-256 rather +than staging a second full JSON string. The extractor drops its raw handle before resolving +secrets. Kernel buffers, filesystem page cache, allocator fragmentation and total RSS are +not guaranteed. Local file reads are synchronous; no finite startup I/O interruption is +claimed. The broad `config-read-allocation-bounds` capability remains `Unsupported`; +this bounded local config-snapshot path does not certify every secret/provider allocation. + ## Container Deployment Build and deploy as a standard container: ```dockerfile -FROM rust:1.75 as builder +FROM rust:1.95.0 as builder WORKDIR /app COPY . . RUN cargo build -p my-app-adapter-axum --release @@ -262,14 +511,14 @@ A typical development workflow: ## Differences from Edge Adapters -| Aspect | Axum | Fastly/Cloudflare | -| ----------- | -------------- | ----------------- | -| Compilation | Native | Wasm | -| Cold start | ~0ms | ~0ms (Wasm) | -| Memory | Unlimited | 128MB typical | -| Filesystem | Full access | Sandboxed | -| Network | Direct | Backend/fetch | -| Concurrency | Multi-threaded | Single-threaded | +| Aspect | Axum | Fastly/Cloudflare | +| ----------- | ------------------------------- | ----------------- | +| Compilation | Native | Wasm | +| Cold start | ~0ms | ~0ms (Wasm) | +| Memory | Operator configured | Provider quota | +| Filesystem | Full access | Sandboxed | +| Network | Direct | Backend/fetch | +| Concurrency | Bounded, connection-local async | Runtime dependent | ::: tip Development Parity While Axum provides a convenient development environment, always test on actual edge platforms before deploying. Provider-specific behaviour such as store backends and request context differs on the real targets. diff --git a/docs/guide/adapters/cloudflare.md b/docs/guide/adapters/cloudflare.md index dad084e8b..290c99820 100644 --- a/docs/guide/adapters/cloudflare.md +++ b/docs/guide/adapters/cloudflare.md @@ -29,11 +29,19 @@ The Wrangler manifest configures your Worker: name = "my-app" main = "build/worker/shim.mjs" compatibility_date = "2023-05-01" +compatibility_flags = ["enable_request_signal", "enable_weak_ref"] [build] command = "worker-build --release" ``` +Keep both compatibility flags when migrating an existing application. `enable_request_signal` +exposes incoming-request cancellation. Bounded config KV reads require `FinalizationRegistry` +for wasm-bindgen cleanup callbacks and fail before dispatch if it is absent; `enable_weak_ref` +enables that support even with an older compatibility date. GC timing is nondeterministic and +does not provide a certified allocation or teardown bound. See +[Cloudflare's compatibility contract](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-finalizationregistry-and-weakref). + ### Entrypoint The Cloudflare Wasm entrypoint in `src/lib.rs` wires the adapter: @@ -55,6 +63,11 @@ derived from the baked store ids and queried individually). Per-id `KV` / `Config` / `Secret` registries are built and injected into request extensions automatically. No `edgezero.toml` is loaded by the runtime — see [the migration guide](../manifest-store-migration.md). +If route/state assembly or `Hooks::configure` fails, `run_app` delivers the typed error's +status and standard JSON body through canonical detached egress, without reading the request +body or opening registries. Internal details stay off the wire and the full failure is logged +once on `edgezero::boot`. No failed app's renderer, headers or callbacks apply. The finite default +write deadline starts before assembly; see [application assembly](/guide/handlers#configuring-the-application-lifecycle). For fully manual wiring, `CloudflareService::new(&app)` builds a dispatcher one store at a time: `.with_config(binding)` (a KV binding name), @@ -64,14 +77,22 @@ matching `.require_kv()` / `.require_secrets()` flags, and finally to open bindings: ```rust -use edgezero_adapter_cloudflare::request::CloudflareService; -use edgezero_core::app::Hooks as _; +use edgezero_adapter_cloudflare::{CLOUDFLARE_PLATFORM, request::CloudflareService}; +use edgezero_core::app::App as EdgeZeroApp; +use edgezero_core::MonotonicClock; use my_app_core::App; use worker::*; #[event(fetch)] pub async fn main(req: Request, env: Env, ctx: Context) -> Result { - let app = App::build_app(); + let clock = MonotonicClock::default(); + let start = clock.now(); + let app = match EdgeZeroApp::build::(CLOUDFLARE_PLATFORM) { + Ok(app) => app, + Err(error) => return edgezero_adapter_cloudflare::request::send_assembly_error( + req, error, ctx, start, clock, + ), + }; CloudflareService::new(&app) .with_config("app_config") .with_kv("sessions") @@ -86,6 +107,23 @@ selectors, so prefer `run_app` unless you are mocking a backend. Use [application retention](#retaining-an-application) with that function when the caller owns a prebuilt app and wants fresh per-request store bindings. +## Ingress Body Conversion + +The converter preserves a genuine native Fetch request's null `body` as buffered empty, without +calling a reader. The standard dispatch path inspects this property only after admission, so +refusal and abort do not acquire a source. A present body always stays on the lazy stream path, +including zero-length ArrayBuffer/Uint8Array bodies and streams that will immediately end. +Neither `Content-Length` nor SDK read-once bookkeeping is an absence signal; consumed present +bodies retain their existing stream-construction error. + +This preserves a transport fact before the shared facade settles unknown GET/HEAD streams. +The facade accepts only EOF, rejects the first nonempty byte before fetch, and races the paired +outbound/inbound limits without restarting either deadline. Request-context state +remains Initial and framing remains `HostManaged`. `into_request()` transfers buffered empty +without draining, while the first `body_bytes()` read still checks the admitted deadline and +returns sticky 408 after expiry. No capability is promoted. Demo and generated handlers inherit +the converter without an API change; local runtime checks do not qualify hosted behavior. + ## Building Build for Cloudflare's Wasm target: @@ -120,17 +158,28 @@ wrangler deploy --cwd crates/my-app-adapter-cloudflare ## Fetch API -Cloudflare Workers use the global `fetch` API for outbound requests: - -```rust -use edgezero_adapter_cloudflare::proxy::CloudflareProxyClient; -use edgezero_core::proxy::ProxyService; - -let client = CloudflareProxyClient; -let response = ProxyService::new(client).forward(request).await?; -``` - -Unlike Fastly, there's no backend configuration needed - Workers can fetch any URL directly. +`CloudflareOutboundClient` uses the Workers global `fetch` API and is injected into core request +extensions. Workers needs no backend registration, but application manifests still declare +outbound hosts so the same portable contract validates on every target. + +The adapter requests raw encoded upstream bytes with manual fetch encoding, disables automatic +redirect following, and owns an abort signal for the absolute request deadline. When an encoded +body is passed through to the downstream response, the response converter separately selects +manual response-body encoding so Workers does not encode it again. These are two distinct +controls. The abort path is implemented, but outbound and streamed-upload deadlines remain +BestEffort until a deployed host-observed cancellation fixture proves a finite bound. Cloudflare +drives batch slots in completion order, maps cache bypass to `NoStore`, and applies a validated +wire-authority override. The platform has not yet proved finite abort teardown or that its final +wire request preserves authority independently from the connection target, so cancellation and +authority override remain BestEffort. Cloudflare and Axum have Native lazy streamed response passthrough; +raw header octets and original non-`set-cookie` field boundaries remain unavailable, so header +fidelity is BestEffort. See [Capabilities](/guide/capabilities). + +Downstream response delivery uses one JavaScript stream writer owned by a coordinator registered +with `Context::wait_until`. It awaits writer backpressure and races the request abort signal and +absolute write deadline. Writer acceptance and close are host-handoff observations; deployed +disconnect and network-completion timing remain unproved, so response-egress capabilities are +BestEffort. ## Logging @@ -138,6 +187,12 @@ EdgeZero does not install a Cloudflare logger by default. Use your preferred log `console_log` or your own `log` implementation), and view output in Wrangler or the Cloudflare dashboard. +Workers console capture does not install a Rust `log` backend. Initialize your backend and +both its filter and facade maximum **before** `run_app`, and set `owns_logging = true`. +`edgezero_core::resolve_logging_level(&env_config)` resolves a supplied platform `EnvConfig`; +`edgezero::boot` is available for separately filtered startup diagnostics. The no-op adapter +initializer cannot guarantee their visibility. + ::: tip Logging status Cloudflare logging is opt-in; install a logger (such as `console_log`) in your entrypoint if you need structured output. @@ -316,18 +371,35 @@ For explicit retention, keep a cache owned by one concrete application and select its `Hooks` type with `dispatch_app::` on every fetch: ```rust -use edgezero_core::app::{App, Hooks}; +use edgezero_adapter_cloudflare::CLOUDFLARE_PLATFORM; +use edgezero_core::{app::App, EdgeError, MonotonicClock}; +use my_app_core::App as MyApp; use std::sync::OnceLock; static APP: OnceLock = OnceLock::new(); +fn retained_app() -> Result<&'static App, EdgeError> { + if let Some(app) = APP.get() { + return Ok(app); + } + let app = App::build::(CLOUDFLARE_PLATFORM)?; + Ok(APP.get_or_init(|| app)) +} + #[worker::event(fetch)] async fn fetch(req: worker::Request, env: worker::Env, ctx: worker::Context) -> worker::Result { - edgezero_adapter_cloudflare::dispatch_app::( - APP.get_or_init(MyApp::build_app), req, env, ctx, - ).await + // Install application-owned logging before attempting assembly. + let clock = MonotonicClock::default(); + let start = clock.now(); + let app = match retained_app() { + Ok(app) => app, + Err(error) => return edgezero_adapter_cloudflare::request::send_assembly_error( + req, error, ctx, start, clock, + ), + }; + edgezero_adapter_cloudflare::dispatch_app::(app, req, env, ctx).await } ``` @@ -336,12 +408,21 @@ the first application value replaces any body-generated default. This correction also applies to existing `run_app` users. `dispatch_app::` reads `MyApp::stores()` and does not initialize logging -or construct an app. Pass an app built from the same `Hooks` type. The caller owns -initialization before construction. It resolves configuration and bindings for -each invocation; never retain `Env`, `Context`, request bodies, or registries in -this cache. The initializer is synchronous and must not recursively access the -cache or block waiting for async initialization. Apps needing fallible or async -initialization own that state machine and publish only a complete snapshot. +or construct an app. Its signature is +`dispatch_app(&App, Request, Env, Context) -> Result`. +Pass an app built from the same `Hooks` type with `CLOUDFLARE_PLATFORM`; the caller +owns logging setup before construction. `App::build` returns `Result` +and invokes fallible `Hooks::configure` after installing platform metadata. +Only a complete successful app enters the cache; an initialization error leaves it +empty for a later attempt. Concurrent construction attempts may build separate +successful candidates, but only one is retained. + +Each invocation resolves fresh configuration, runtime variables, and store +registries, then runs fresh admission and response egress. Never retain `Env`, +`Context`, request bodies, admission resources, or delivery coordinators in the +app cache. Construction is synchronous and must not recursively access the cache +or block waiting for async initialization. Apps needing async initialization or +refresh own that state machine and publish only a complete successful snapshot. Fetch invocations may overlap. Verify isolation while two requests are actually in flight in the same instance. A serialized test or two separate instances does diff --git a/docs/guide/adapters/fastly.md b/docs/guide/adapters/fastly.md index 8e77876f5..77af30f51 100644 --- a/docs/guide/adapters/fastly.md +++ b/docs/guide/adapters/fastly.md @@ -6,7 +6,10 @@ Deploy EdgeZero applications to Fastly's Compute@Edge platform using WebAssembly - [Fastly CLI](https://developer.fastly.com/learning/compute/#install-the-fastly-cli) - Rust `wasm32-wasip1` target: `rustup target add wasm32-wasip1` -- [Viceroy](https://github.com/fastly/Viceroy) for local execution and testing +- [Viceroy](https://github.com/fastly/Viceroy) 0.21.0 for local execution and testing + +The adapter pins the Fastly SDK to 0.13.1. Older Viceroy releases may reject its +host imports before application code executes; use the repository's pinned runtime. ## Project Setup @@ -57,109 +60,75 @@ The Fastly entrypoint wires the adapter: ```rust use my_app_core::App; -#[fastly::main] -fn main(req: fastly::Request) -> Result { - edgezero_adapter_fastly::run_app::(req) +pub fn main() -> Result<(), fastly::Error> { + edgezero_adapter_fastly::run_app::() } ``` +Do not add Fastly's response-returning entrypoint attribute. EdgeZero initializes the ABI, +receives the client request, and owns `stream_to_client` through final body-handle close. + `run_app` builds per-id `KV` / `Config` / `Secret` registries from the portable store metadata baked into `App` by the `app!` macro. Logging settings are baked -in at the same time. A managed deployment resolves `EDGEZERO__*` store selectors -and links each selected Fastly resource under its logical ID; no -`edgezero.toml` or deployment environment is read by the runtime. See +in at the same time. Managed deployments resolve `EDGEZERO__*` store selectors +and link each physical resource under its logical ID; neither `edgezero.toml` +nor the deployment environment is read by the runtime. See [the migration guide](../manifest-store-migration.md). - -For fully manual wiring, `FastlyService::new(&app)` builds a dispatcher one -store at a time: `.with_config(name)`, `.with_config_handle(handle)`, -`.with_kv(name)`, `.with_secrets()`, the matching `.require_kv()` / -`.require_secrets()` flags, and finally `.dispatch(req)`. Prefer `run_app` when -the entrypoint should use the store and logging metadata baked into the app. - -### Migrating a custom entrypoint - -Custom entrypoints previously loaded selectors from the -`edgezero_runtime_env` Config Store on every request and passed an `EnvConfig` -to `dispatch_with_registries`: - -```rust -let stores = MyHooks::stores(); -let env = edgezero_adapter_fastly::runtime_env_config(stores); -edgezero_adapter_fastly::request::dispatch_with_registries( - &app, - req, - stores, - &env, - extend, -) -``` - -Remove the `runtime_env_config` call, any use of `RUNTIME_ENV_STORE_NAME`, and -the `env` argument. Initialize logging from the app's baked Fastly settings: - -```rust -use edgezero_adapter_fastly::{FastlyLogging, init_logger}; -use edgezero_core::app::Hooks as _; - -let stores = MyHooks::stores(); -let logging = FastlyLogging::from(MyHooks::logging_for("fastly")); -if logging.use_fastly_logger && !MyHooks::owns_logging() { - let endpoint = logging.endpoint.as_deref().unwrap_or("stdout"); - init_logger(endpoint, logging.level, logging.echo_stdout)?; -} -edgezero_adapter_fastly::request::dispatch_with_registries( - &app, - req, - stores, - extend, -) -``` - -A hand-written `Hooks` impl inherits the default `stores()`, which is empty, so -`dispatch_with_registries` opens no stores. Such an impl must override -`stores()` or pass explicit `StoresMetadata`. - -`EDGEZERO__STORES______NAME` is now a deployment input. EdgeZero -resolves it before provider mutation and binds that physical resource to the -version under the stable logical `` alias. The runtime opens the alias -directly. Fastly Config Stores always use `` as the entry key, so remove -Fastly `__KEY` selectors as well. - -Fastly logging endpoint, level, and stdout behavior now come from -`[adapters.fastly.logging]` when the immutable application is built. Runtime -`EDGEZERO__LOGGING__*` values cannot vary those settings between production and -staging for the same release. +If route/state assembly or `Hooks::configure` fails, `run_app`, `run_app_with_hooks` and +`run_app_with_config` deliver the error's status and standard JSON body through canonical +detached egress. They release the unread request, skip preparation/finalization and registries, +and log the full failure once on `edgezero::boot`. Internal details stay off the wire. No failed +app's renderer, headers or callbacks apply; the finite default write deadline starts before assembly. + +For fully manual wiring, build `FastlyService`, attach stores as needed, and call its send-owning +`send()` method. Prefer `run_app` for manifest-driven store resolution. + +Fastly store `__NAME` selectors are deployment inputs. The runtime opens logical +aliases directly, and Config Stores use the logical ID as the entry key. Remove +runtime selector-store reads and Fastly `__KEY` overrides when migrating a custom +entrypoint; use the canonical send-owning helper below. Logging endpoint, level +and stdout behavior come from `Hooks::logging_for("fastly")` and cannot vary +between staging and production for the same immutable application release. ### Capturing raw-request signals (JA4, H2 fingerprint) -`run_app` converts the `fastly::Request` into a neutral core request before -dispatch. The client IP is carried across automatically — read it via +`run_app` receives and converts the `fastly::Request` into a neutral core request before +dispatch. The client IP is carried across automatically; read it via `FastlyRequestContext` (see [Context Access](#context-access) below). Other Fastly-only signals that are readable only on the raw request (`get_tls_ja4()`, `get_client_h2_fingerprint()`) aren't reachable from handlers -by default. Use `run_app_with_request_extensions`, which -runs an app closure against a scratch `Extensions` **before** conversion and -merges the values into the core request — so a `State`/extractor or middleware -can read them: +by default. Use `run_app_with_hooks`, which runs a request closure against a +scratch `Extensions` **before** conversion and merges the values into the core +request. Its response closure runs only for routed responses before egress +policy and framing; EdgeZero retains delivery ownership: ```rust #[derive(Clone)] struct Ja4(String); -#[fastly::main] -fn main(req: fastly::Request) -> Result { - edgezero_adapter_fastly::run_app_with_request_extensions::(req, |raw, ext| { - if let Some(ja4) = raw.get_tls_ja4() { - ext.insert(Ja4(ja4.to_owned())); - } - }) +pub fn main() -> Result<(), fastly::Error> { + edgezero_adapter_fastly::run_app_with_hooks::( + |raw, ext| { + if let Some(ja4) = raw.get_tls_ja4() { + ext.insert(Ja4(ja4.to_owned())); + } + }, + |_response| (), + ) + .map(|_post_send_state| ()) } ``` -`run_app` is exactly `run_app_with_request_extensions::(req, |_, _| {})`. -The closure runs once per request; insert whatever typed values your handlers -need, then read them in a handler via a custom extractor or -`ctx.request().extensions().get::()`. +The request closure runs once before admission for each request that reaches +preparation; insert whatever typed values your handlers need, then read them via a custom extractor or +`ctx.request().extensions().get::()`. The optional state returned by +`run_app_with_hooks` is available only after EdgeZero reaches its strongest +terminal delivery boundary. Detached admission responses skip the response +closure and return `None`. + +For fully manual wiring, `FastlyService::send_request_with_hooks` provides the +same closed lifecycle for an already-received request. It does not expose a +response-returning dispatch operation. ### Owning your own logging @@ -173,7 +142,9 @@ edgezero_core::app!("edgezero.toml", owns_logging = true); ``` or on a hand-written `Hooks` impl (`fn owns_logging() -> bool { true }`). Every -adapter's `run_app` honors it, so the app is responsible for logger setup. +adapter's `run_app` honors it. The app must install its backend **before** entering the runner, +apply `Hooks::logging_for("fastly").level`, and set both backend and facade filters. Raising only +`log::set_max_level` does not override `log_fastly`'s internal filter. ## Building @@ -215,24 +186,48 @@ edgezero deploy --adapter fastly \ ``` The release fixes the package, `edgezero.toml`, and its referenced Fastly -manifest before runtime configuration is selected. A bare -`edgezero deploy --adapter fastly` remains only as store-free production -compatibility for an existing manifest command. Staging and any deployment that +manifest before deployment selects physical resource links. A bare +`edgezero deploy --adapter fastly` supports store-free production deployments +using a manifest command. Staging and any deployment that declares a Config, KV, or Secret Store require the verified release-backed managed command above. -## Backends +## Ingress Body Conversion + +Direct request conversion and admitted dispatch preserve actual SDK body absence: +`try_take_body() == None` becomes core `Body::empty()` without constructing an SDK body. +Every `Some(body)` stays on the existing lazy stream/read-deadline path, even when +`known_length()` reports zero. The admitted path transfers the body after the prepare hook, +preserving SDK-prefetched read bytes and pending writes. Conversion does not infer emptiness +from headers or hints, repack the SDK body, read it or flush it. + +Actual WASI SDK tests demonstrate that prefetched read bytes and pending writes can contain +payload despite zero host length. Host length is not EOF even beyond SDK buffering: Viceroy +uses Hyper exact size hints, not a terminal-body signal. A present body is never discarded +on that basis. An absent body remains Initial, not Cached, in the request context; core's first +`body_bytes` read-deadline check is unchanged and can still return 408. Ingress framing remains +`HostManaged`. + +Normal `from_client()` requests carry a body handle even with absent framing or `Content-Length: 0`. +The shared facade now settles those present GET/HEAD sources to EOF or the first nonempty byte +before backend setup. It preserves the original outbound budget and inbound 408 precedence. +The synchronous inbound host read remains non-preemptible, so its read deadline is BestEffort; +post-return checks are not prompt interruption. Demo and generated proxy handlers share this path. +No capability cell or SDK/runtime pin is promoted. -EdgeZero's Fastly proxy client uses **dynamic backends** derived from the target URI (host + scheme). -You do not need to predeclare backends in `fastly.toml` for EdgeZero proxying. +## Backends -```rust -use edgezero_adapter_fastly::proxy::FastlyProxyClient; -use edgezero_core::proxy::ProxyService; +Outbound HTTP uses deterministic **dynamic backends** derived from the canonical target, +TLS identity, and provider timer budget. You do not predeclare those destinations in +`fastly.toml`, but dynamic backends must be enabled on the deployed Fastly service. The local CLI +cannot prove that service entitlement, so `outbound-http` is BestEffort. A disabled service +returns a typed 502 with an enablement diagnostic. -let client = FastlyProxyClient; -let response = ProxyService::new(client).forward(request).await?; -``` +Fastly also has documented deadline, upload, elastic-budget, batch-isolation, and lazy downstream +streaming limitations. EdgeZero owns the low-level `stream_to_client` lifetime and writes portable +response chunks without whole-body collection. Synchronous source polls and hostcalls cannot be +preempted, so downstream streaming and response-egress guarantees remain BestEffort. See +[Capabilities](/guide/capabilities) before marking a capability required. ## Logging @@ -245,14 +240,19 @@ level = "info" echo_stdout = true ``` -To initialize logging manually, call `init_logger` with explicit settings: +Managed Fastly logging keeps the `edgezero::boot` target at `Warn` while ordinary records use +the configured level. Both the SDK and facade allow boot warnings even at ordinary `Off`; +raising the facade maximum alone is insufficient. `edgezero::boot` is a logical target, not a +Fastly endpoint name. Initialize logging before application configuration. + +To initialize logging manually, call `init_logger` with a provisioned endpoint: ```rust use edgezero_adapter_fastly::init_logger; use log::LevelFilter; fn main() { - init_logger("stdout", LevelFilter::Info, true).expect("init logger"); + init_logger("edgezero-logs", LevelFilter::Info, true).expect("init logger"); } ``` @@ -342,8 +342,12 @@ async fn handler(config: Config) -> Result { } ``` -See [the store migration guide](../manifest-store-migration.md) for store selection and [the GitHub Actions guide](../deploy-github-actions.md) for the -immutable-release workflow. +If a configured store link is missing, the adapter logs a one-time warning +and retains a typed failing declared binding, rather than reporting intentional absence. +Raw values are shared `ConfigValue` UTF-8 payloads; use `as_str()` to borrow them. +Migrating from `name`/`adapters.*`? +See [the migration guide](../manifest-store-migration.md). +See [the GitHub Actions guide](../deploy-github-actions.md) for the immutable-release workflow. ## Context Access @@ -366,20 +370,23 @@ async fn handler(ctx: RequestContext) -> Result { ## Streaming -Standard `run_app` and `serve_app` dispatch drain a `Body::Stream` response into -a `fastly::Body` before returning, so the full payload is materialised rather -than delivered chunk by chunk. Custom `lifecycle::serve_custom` callbacks can -instead commit a native streaming response and pump chunks progressively; see -[Custom dispatch and streaming](#custom-dispatch-and-streaming). +Fastly provides `stream_to_client`, but that API is incompatible with the standard +response-returning SDK entrypoint. Generated applications use EdgeZero's undecorated, send-owning +entrypoint instead. EdgeZero commits the response head through the raw Fastly ABI, writes each +portable body chunk to the streaming body handle with short-write accounting, and closes or +abandons that handle exactly once. A blocked synchronous source poll or hostcall cannot be +preempted, and a failed consuming `finish` call leaves no handle to abandon; those limits keep the +response-egress capabilities at BestEffort. -See the [Streaming guide](/guide/streaming) for examples and patterns. +See the [Streaming guide](/guide/streaming) and +[capability matrix](/guide/capabilities#outbound-matrix) for the exact boundary. ## Testing Run contract tests for the Fastly adapter: ```bash -cargo install viceroy --locked +cargo install viceroy --version 0.21.0 --locked export CARGO_TARGET_WASM32_WASIP1_RUNNER="viceroy run" # Run tests @@ -400,7 +407,7 @@ Configure the Fastly adapter in `edgezero.toml`. See [Configuration](/guide/conf ## Reusing a sandbox and retaining an app -Opt in with an ordinary `main` in place of the single-request `#[fastly::main]`: +Opt in with an ordinary `main` in place of the single-request `run_app` call: ```rust use edgezero_adapter_fastly::{Serve, serve_app}; @@ -415,36 +422,50 @@ fn main() -> Result<(), fastly::Error> { } ``` -`serve_app_with_request_extensions` additionally accepts an `FnMut` callback -for fresh extensions on every request. The first request initializes logging -and builds the app. Each request reads runtime configuration and constructs new -store registries. App construction and `configure` execute once per sandbox; -explicit work elsewhere still executes whenever the application calls it. -The extension callback and its captures are retained for the whole serving loop; -only the supplied extensions are fresh. Create request-specific mutable data -inside the callback or store it in those extensions. - -Logging takes the first configuration snapshot, unless the app owns logging. -An unavailable optional runtime configuration store disables logging for that -sandbox, even if subsequent reads recover store selectors. It does not silently -retry or reconfigure logging. The current overlay enables logging when an -endpoint exists and uses `echo_stdout: true`. Applications requiring another -initialization policy should use `lifecycle::serve_custom` and `setup_once`. -The resolved configuration does not distinguish intentionally disabled logging -from an unavailable first snapshot. Warnings through the log facade may be lost -before a logger is installed; absence of a warning does not establish a healthy -configuration read. - -SDK 0.12.1 exposes `with_max_requests`, `with_timeout`, `with_max_lifetime`, and +`serve_app_with_hooks` additionally accepts a preparation hook +`FnMut(&mut fastly::Request, &mut Extensions)` and a finalization hook +`FnMut(&mut edgezero_core::Response) -> State`. Preparation runs before admission; +finalization runs only for routed responses, before egress policy and framing. +Detached admission responses skip finalization. The retained helper discards the +finalizer's state after delivery; use the request-level send helper in a custom +callback when post-send work needs that state. + +The first request initializes logging and attempts to build the app through +`App::build::(FASTLY_PLATFORM) -> Result`. Each request constructs +fresh store registries using deployed logical aliases. Successful app construction +and `configure` execute once per sandbox; explicit work elsewhere still executes +whenever the application calls it. Both hooks and their captures are retained for +the whole serving loop; the supplied native request, extensions, and response are +request-local. Create request-specific mutable data inside the preparation hook +or store it in those extensions. + +Failed route/state or configuration attempts leave the retained slot empty. `serve_app` and +`serve_app_with_hooks` deliver the same detached error response as the single-request helpers; +successful failure-response delivery keeps serving, and the next request retries assembly. +They never retain an error-response router. A custom `serve_custom` callback can use +`request::send_assembly_error(req, error, request_start, clock)` without an App and inspect its +owned `FastlyRequestDelivery::Detached` report. Capture the start with that clock before assembly. +The helper never invokes preparation/finalization, configured rendering, headers, policy or an +app observer. A terminal delivery failure retains the ordinary stop/error behavior; it does +not authorize an automatic native resend. + +Logging uses the immutable baked settings, unless the app owns logging. Setup +completes once per sandbox and does not reconfigure filters on later callbacks. +Applications requiring another initialization policy should use +`lifecycle::serve_custom` and `setup_once`. Store-open diagnostics use the bounded +boot channel after logger setup; an absent warning is not proof of a healthy binding. + +SDK 0.13.1 exposes `with_max_requests`, `with_timeout`, `with_max_lifetime`, and `with_max_memory`. A value of zero disables the request-count or memory limit; it does not request zero callbacks or zero memory use. The lifetime limit is measured from `Serve` construction, including time before the first callback. `with_timeout` bounds only the idle wait for the next request; it does not bound handler or backend time. If your service is billed by memory × wall-clock time, idle waiting is billed too. Limits do not guarantee reuse; any request may start a -fresh sandbox. The standalone Viceroy 0.17.0 pinned for the fixtures admits at most -six requests per guest even with a larger SDK request limit. `edgezero serve` runs -`fastly compute serve`, whose bundled Viceroy can differ. Neither local ceiling +fresh sandbox. The fixtures pin standalone Viceroy 0.21.0; any observed per-guest +request ceiling belongs to that runtime and run, not to the SDK request limit. +`edgezero serve` runs `fastly compute serve`, whose bundled Viceroy can differ. +Record the executable and observed reuse rather than assuming a local ceiling establishes deployed behavior. Lifetime and memory checks occur between callbacks and cannot interrupt blocked application work. Before using a local memory limit, verify that the @@ -453,67 +474,117 @@ ends the SDK loop. CPU clock observations are not reliable cross-run benchmarks. `ServeSummary::requests()` counts attempted callbacks, including a failed one; a crash can prevent a final summary. Handler `EdgeError`s render as responses -and serving continues. Any other returned error (logger setup, a required KV -store that fails to open, request conversion, response-stream collection, or an -`EdgeError` that itself fails to render) makes -the SDK send a 500 with the error text and end the loop. Constructor panics -remain sandbox failures. +and serving continues when adapter delivery succeeds. A returned setup, required-store, +admission-abort, or terminal-delivery error ends the loop and remains in its summary. +The lifecycle wrapper records the callback result without asking the SDK to send a +synthetic 500, including when a response was already committed. Ingress and egress +policies may produce their own error responses; an escaping error is not proof that +any response was sent. Constructor panics remain sandbox failures. ### Custom dispatch and streaming -The standard helper collects core response streams into a native response body. -For progressive client streaming, mutable native requests, response-extension -finalization, or post-send work, use `lifecycle::serve_custom` with a configured -`Serve` builder and a callback receiving `&mut lifecycle::Sandbox`. EdgeZero -owns the retained slot and successful-only initialization; your callback chooses -the application-owned `T`. Initialize lazily so health checks can bypass expensive -construction. Use `request::dispatch_with_registries` when standard translation -suffices. For custom response handling, use -`request::into_core_request_with_registries(req, MyApp::stores(), extend)` -then drive `app.router().oneshot(request)` with `futures::executor::block_on`. -This inserts the same registries for the `Kv`, `Config`, `Secrets` and `AppConfig` -extractors as standard dispatch while leaving the core response available for -streaming and finalization. -The bare `into_core_request` helper inserts no store registries. - -For manual streaming, append every header value, commit once with -`stream_to_client`, pump and flush chunks, then finish the stream. Handle errors -after commitment locally: returning an error to the SDK can trigger another -response attempt. Complete pending backend and post-send operations before the -callback returns. Do not cache native store handles with the app. +The standard helpers preserve core response streams without whole-body collection. +For custom initialization or post-send work, use `lifecycle::serve_custom` with a +configured `Serve` builder and a callback +`FnMut(fastly::Request, &mut lifecycle::Sandbox) -> Result<(), E>`. EdgeZero owns +the retained slot and successful-only initialization; your callback chooses the +application-owned `T`. Initialize lazily so health checks can bypass expensive +construction. + +For an EdgeZero response, call `request::send_request_with_registries_and_hooks` +with the app, store metadata, received request, preparation hook, +and finalization hook. It runs canonical admission, injects fresh `Kv`, `Config`, +`Secrets`, and `AppConfig` registries, then owns framing, streaming, deadlines, and +terminal delivery. Preparation borrows a mutable native request and extension bag; +finalization borrows a mutable routed core response. Neither hook returns a +response for the callback to send separately. The hard-cut result is +`delivery::FastlyRequestDelivery`; inspect its disposition and commitment +before deciding whether to send a native response or perform post-send work. +The same result is returned by `send_with_registries_and_hooks` and +`FastlyService::send_request_with_hooks`. Standard `run_app_with_hooks`, +`serve_app_with_hooks`, and `FastlyService::send` keep their existing result and +stop-on-error policy; successful fallback delivery remains successful there. + +- `NoAttempt(failure)`: no egress attempt, completion, observer or host send occurred. + `failure.reason()` distinguishes typed required-store failures from request-head + conversion (`InvalidUri` or `RequestBuilder`). Public messages and Debug output + contain no request, store or provider text. Standard serving privately retains + its original error and source. +- `Aborted`: application policy intentionally selected no response. Never replace it. +- `Detached(delivery)`: an attempted framework/admission response skipped finalization. +- `Routed { state, delivery }`: retains finalizer state even when framing, fallback, + source or transport delivery fails. Terminal delivery is not necessarily successful. + +`FastlyResponseDelivery::report()` is the exact terminal `ResponseEgressReport` +already supplied to completion and any applicable observer. Its body kind, source +and framing observations, outcome, and fallback disposition describe the attempt. +`error()` separately identifies the last delivery failure: a successful fallback +can retain the original conversion/deadline cause in the report without a delivery +error. `commitment()` describes the **last selected body**, not confirmed client +delivery: `NotCommitted` means its head never reached the host send; `Unknown` +means the host rejected the send; `Committed` means the host accepted the head. + +`may_send_native_response()` permits a replacement only for `NoAttempt` or confirmed +`NotCommitted`, never policy abort, unknown commitment or committed failure. For an +attempted response, its terminal notification has already happened; any native +replacement is outside EdgeZero accounting. Pre-admission rejections and refusals +exclude the global observer; an admitted source-error response also reports +`Detached` but retains the application's observer. These reports do not strengthen +provider capabilities. + +The manual response lifetime remains adapter-owned: append every header value, +commit once through `stream_to_client`, account accepted body bytes, and finish or +abandon the body handle exactly once. A native response constructed directly by +your callback, such as a health reply, must instead be explicitly sent there; +manual native streams must be pumped, flushed, and finished before returning. +The lifecycle wrapper never supplies an extra response. Returning `Err` terminates +serving without a synthetic send, even after commitment. Return `Ok(())` only when +your policy permits continued serving after cleanup. Complete pending backend and +post-send operations before returning. Do not cache native store handles with the app. Use `Request::get_client_request_id()` for request correlation. `FASTLY_TRACE_ID` describes the sandbox and must not be treated as a unique request ID. If a native ID is unavailable, generate a request-local fallback and label its source. Do not retain correlation fields in app state or global logger configuration. -### Custom lifecycle compatibility contract +### Custom lifecycle contract -The custom path uses public APIs: `lifecycle::{Sandbox, serve_custom, run_custom}`, -`request::into_core_request_with_registries`, and `app.router().oneshot`. -EdgeZero owns the successful-only retained slot and delegates request reception, -limits and `HandlerResult` completion to the SDK. Applications own setup, state, -refresh policy, response finalization and sending. Direct SDK serving remains -available when these helpers do not fit. +The custom path uses public APIs: `lifecycle::{Sandbox, serve_custom, run_custom}` +and `request::send_request_with_registries_and_hooks`. EdgeZero owns the +successful-only retained slot and delegates request reception and limits to the +SDK. The callback owns delivery: it either explicitly sends a native response or +calls the adapter's closed send helper. The serving wrapper passes only the +terminal `Result<(), E>` to the SDK without a synthetic response. Applications +own setup, state, refresh policy, hooks, and post-send work. There is no +response-returning conversion or whole-body compatibility path. Follow this order inside a callback: -1. Return lightweight health responses before expensive initialization. -2. Use `sandbox.setup_once` to guard successful logger setup independently from +1. Explicitly send lightweight health responses before expensive initialization. +2. Resolve baked logging with `FastlyLogging::from(MyApp::logging_for("fastly"))`. + Use `sandbox.setup_once` to guard successful logger setup independently from app construction. Failed setup leaves the guard unset; partial side effects - are not rolled back, so retries must be safe. -3. Call `sandbox.initialize` only for application-owned state. Success fills the - slot; failure leaves it empty and returns the error unchanged. Read the value + are not rolled back, so retries must be safe. The closed send helper installs fresh, + request-local logical KV bindings after logger setup, without opening unused stores. + First use opens once per request; failed declarations retain a typed handle rather than + failing KV-free routes. Explicit `FastlyService::require_kv()` remains fail-before-dispatch. +3. Call `sandbox.initialize` with `App::build::(FASTLY_PLATFORM)` for a core + app, mapping its typed error at the adapter boundary. Success fills the slot; + failure leaves it empty and returns the error unchanged. Read the value with `state()`. A handled failure may send a 503 and return `Ok(())` to permit - a later callback to retry. Propagating it with `?` sends a 500 and ends the loop. -4. Convert with `into_core_request_with_registries` and drive router dispatch - with `futures::executor::block_on`. Keep bodies, - extensions, registries and native handles request-local. The bare - `into_core_request` path provides no `Kv`, `Config`, `Secrets` or `AppConfig` - stores. Registered app state still overwrites extensions of the same type. -5. Inspect response extensions and finalize before sending. Append repeated - headers, commit once, flush progressive chunks and finish the writer. After - commitment, handle errors locally and return `()` or `Ok(())`. + a later callback to retry. Propagating it with `?` ends the loop without a synthetic send. +4. Pass the received request to `send_request_with_registries_and_hooks`. Its + `FnOnce` preparation hook may mutate the native request and portable extensions + before admission; its `FnOnce` finalizer may inspect response extensions and + mutate a routed response before egress. Keep bodies, extensions, registries, + and native handles request-local. Registered app state still overwrites + extensions of the same type. +5. Inspect the owned terminal result before using finalizer state or starting post-send + work. Detached responses skip finalization; routed state survives delivery failure. + Schedule application follow-up only for an application-body result selected by your + policy, not a delivered fallback. A committed application failure can still carry + the request-local client needed for follow-up; complete that work before returning. + Never send the core response again. A delivery error alone does not authorize replacement. 6. Complete pending backend and post-send work before returning. Constructors and callbacks that panic remain sandbox failures. @@ -525,20 +596,23 @@ if let Err(error) = sandbox.setup_once(|| install_application_logger()) { fastly::Response::from_status(503).send_to_client(); return Ok(()); } -sandbox.initialize(|| build_application())?; -// Read sandbox.state(), perform registry-aware dispatch, then explicitly send. +sandbox.initialize(|| App::build::(FASTLY_PLATFORM).map_err(fastly::Error::from))?; +// Read sandbox.state() and call the closed send helper; do not send its response again. // The ? above deliberately terminates this sandbox if application construction fails. ``` A complete callback looks like this. `serve_custom` returns a `ServeSummary` -that you inspect or convert with `into_result()`, and the infallible `Hooks::build_app` -needs an explicit error type for `initialize` to infer: +that you inspect or convert with `into_result()`. `App::build` is fallible and +installs `FASTLY_PLATFORM` before application configuration: ```rust use edgezero_adapter_fastly::lifecycle::{self, Sandbox}; -use edgezero_adapter_fastly::{Serve, request, response}; +use edgezero_adapter_fastly::delivery::{FastlyRequestDelivery, FastlyResponseCommitState}; +use edgezero_adapter_fastly::{ + FASTLY_PLATFORM, FastlyLogging, Serve, init_logger, request, +}; use edgezero_core::app::{App, Hooks}; -use futures::executor::block_on; +use my_app_core::App as MyApp; fn main() -> Result<(), fastly::Error> { lifecycle::serve_custom(Serve::new().with_max_requests(10), handle).into_result() @@ -549,20 +623,63 @@ fn handle(req: fastly::Request, sandbox: &mut Sandbox) -> Result<(), fastly fastly::Response::from_status(200).send_to_client(); return Ok(()); } - sandbox.initialize(|| Ok::<_, fastly::Error>(MyApp::build_app()))?; - let core = request::into_core_request_with_registries(req, MyApp::stores(), |_, _| {})?; + let stores = MyApp::stores(); + sandbox.setup_once(|| { + let logging = FastlyLogging::from(MyApp::logging_for("fastly")); + if logging.use_fastly_logger && !MyApp::owns_logging() { + init_logger( + logging.endpoint.as_deref().expect("configured endpoint"), + logging.level, + logging.echo_stdout, + )?; + } + Ok::<(), fastly::Error>(()) + })?; + sandbox.initialize(|| App::build::(FASTLY_PLATFORM).map_err(fastly::Error::from))?; let app = sandbox.state().expect("initialized above"); - let core_response = block_on(app.router().oneshot(core))?; - // Inspect response extensions and finalize here. To stream progressively, - // append headers to a native response and use `stream_to_client` instead. - response::from_core_response(core_response)?.send_to_client(); - Ok(()) + let result = request::send_request_with_registries_and_hooks( + app, + stores, + req, + |_raw, _extensions| {}, + |response| response.status(), + ); + // Custom reuse policy: keep serving after a committed failure, without replacement. + match result { + FastlyRequestDelivery::NoAttempt(failure) => { + // Permitted native replacement: no EdgeZero response was attempted. + fastly::Response::from_status(503).send_to_client(); + Err(fastly::Error::msg(failure.message())) + } + FastlyRequestDelivery::Aborted => Err(fastly::Error::msg("admission aborted")), + FastlyRequestDelivery::Detached(delivery) + | FastlyRequestDelivery::Routed { delivery, .. } => { + if let Some(error) = delivery.error() { + log::error!("{}", error.message()); + if delivery.commitment() == FastlyResponseCommitState::Committed { + return Ok(()); + } + // Never replace Unknown or Committed, including a failed body stream. + if delivery.commitment() == FastlyResponseCommitState::NotCommitted { + fastly::Response::from_status(503).send_to_client(); + } + return Err(fastly::Error::msg(error.message())); + } + Ok(()) + } + _ => Err(fastly::Error::msg("unsupported delivery disposition")), + } } ``` The fixture's [`custom_dispatch`](https://github.com/IABTechLab/edgezero/blob/main/tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs) is a fuller reference covering logger setup, handled initialization failure, -response finalization, progressive streaming and post-send work. +response finalization, adapter-owned progressive streaming and post-send work. +It verifies retained-client follow-up after a committed application-source failure +and suppresses application follow-up when a framing rejection selects fallback. +The `test-utils` module `delivery::testing` can supply synthetic dispatch/delivery +outcomes to the shared hook finisher; it does not simulate SDK head conversion, +host commitment or transport. Runtime fixtures establish those separate boundaries. `Sandbox::requests()` counts attempted callbacks, including early health responses and failed ones. `initialization_attempts()` counts only actual builder calls. @@ -570,14 +687,13 @@ These counters describe this sandbox and are part of the public lifecycle API. They do not establish client completion or provider reuse. Use `lifecycle::run_custom(Request::from_client(), callback)` for the single-request -branch. It creates fresh state without entering `Serve`. Both wrappers complete -`HandlerResult` exactly once. Use an ordinary `fn main`; `#[fastly::main]` may -attempt another error response if an already-completed error propagates. -Registry-aware `dispatch_with_registries` still collects response streams and -cannot replace this explicit streaming/finalization path. - -Keep adapter, core and SDK versions compatible. Run the compatibility fixtures -against the checkout being adopted: +branch, initializing the Fastly ABI before receiving that request. It creates +fresh state without entering `Serve` and returns the callback's `Result<(), E>` +directly, without asking the SDK to render an error. Use an ordinary `fn main`; +the response-returning SDK entrypoint attribute does not own this delivery lifecycle. + +Keep adapter, core and SDK versions aligned with the pinned checkout. Run the +lifecycle fixtures against the checkout being adopted: ```sh ./scripts/smoke_test_reusable_app.sh --adapter fastly --suite smoke diff --git a/docs/guide/adapters/overview.md b/docs/guide/adapters/overview.md index 4bb4ac368..a375e9630 100644 --- a/docs/guide/adapters/overview.md +++ b/docs/guide/adapters/overview.md @@ -9,37 +9,46 @@ Adapters translate provider-specific HTTP primitives into the portable `App` in - Preserve request semantics - Stream responses without buffering where the provider supports it - Expose provider context -- Offer a proxy bridge so handlers can forward traffic without knowing which platform they are on +- Inject the portable outbound HTTP client without exposing provider SDK types to handlers ## Request Conversion -Each adapter exposes an `into_core_request` helper that accepts the provider's request type and returns `edgezero_core::http::Request`. The conversion must: +Canonical adapter dispatch normalizes the provider request head, performs admission, +and only then consumes the body under the admitted limits and deadline. Public raw +conversion helpers are not a common adapter contract or a substitute for this lifecycle. +The admitted conversion must: - **Preserve the HTTP method** exactly (`GET`, `POST`, etc.) - **Parse the full URI** (path and query string) into an `http::Uri`. Reject invalid URIs with `EdgeError::bad_request` - **Copy all headers** into the core request. Provider-specific headers may be filtered only when they clash with platform defaults -- **Consume the request body** into an `edgezero_core::body::Body`. Adapters may buffer inbound bodies today; streaming input should be preserved where available +- **Consume the request body** into an `edgezero_core::body::Body` only after admission, enforcing the selected size limit and absolute read deadline - **Insert a provider context struct** (e.g., `FastlyRequestContext`) into the request extensions. The context should expose metadata such as client IP addresses or environment handles so handlers can reach platform APIs -## Response Conversion +## Response Egress -Adapters also expose `from_core_response` (or equivalent) to transform an `edgezero_core::http::Response` into the provider response type. Implementations must: +Adapters consume a `ResponseEgressEnvelope` and own delivery through the strongest platform +transport boundary available. Implementations must: - **Map HTTP status codes** verbatim - **Copy headers**, respecting casing rules enforced by the provider -- **Preserve streaming bodies** - `Body::Stream` should be written chunk-by-chunk to the provider output without buffering the entire payload. Cloudflare does this through standard dispatch via `Response::from_stream`. Fastly's standard `run_app`/`serve_app` paths drain the stream into a `fastly::Body`; custom [`lifecycle::serve_custom`](/guide/adapters/fastly#custom-dispatch-and-streaming) callbacks can stream progressively. Spin collects into a `Vec` capped at 16 MiB, and Axum buffers as well -- **Handle encoding helpers** (`decode_gzip_stream`, `decode_brotli_stream`) where a provider requires transparent decompression +- **Preserve lazy body production** and platform backpressure without whole-response collection +- **Use the application clock and one absolute write deadline** through terminal observation +- **Account accepted payload bytes** at the adapter's documented host boundary +- **Abort or close owned transport resources** and deliver exactly one terminal report +- **Use the bounded precommit fallback** without replacing a response after commit ## Dispatch Helper -Adapters surface a dispatch entry point, either a free function or a service builder's `dispatch` method, that bridges from the provider event loop into the shared router (`app.router().oneshot(...)`). It should: +Adapters surface a send-owning runner that bridges from the provider event loop into the shared +router. It should: -1. Convert the incoming provider request with `into_core_request` -2. Await the router future -3. Convert the resulting `Response` back into the provider type -4. Map any `EdgeError` into the provider's error type so failures surface as provider errors (often 5xx) instead of panicking +1. Normalize the incoming provider request head and run the app's admission policy +2. Consume the admitted body and await canonical app dispatch, or deliver the detached admission response +3. Carry the returned egress envelope into the adapter coordinator +4. Own body delivery, deadline/abort handling, and terminal observation +5. Map precommit failures to the bounded fallback and expose only category-safe diagnostics -This helper is what demo entrypoints and adapters call when wiring their platform-specific main functions. +Generated and demo entrypoints call only the canonical runner for their adapter. ## Store Registry Resolution @@ -52,30 +61,62 @@ the id-keyed `Kv` / `Secrets` / `Config` extractors or the matching `ctx.kv_store(id)` / `ctx.config_store(id)` / `ctx.secret_store(id)` accessors. The pre-rewrite `Hooks::config_store()` hook is gone. -## Proxy Integration +## Outbound HTTP Integration + +Adapters implement `edgezero_core::outbound::OutboundHttpClient` and inject an `HttpClient` into +each core request. Implementations must: + +- Implement `time`, synchronous `validate_supported_options`, `dispatch_head`, eager `begin`, + and prepared `start_batch_until`. Core owns application-facing `send` and ordered collection. +- Consume `OutboundDispatchRequest` without restarting its captured budget. Return an + `OutboundHead` with original adapter-visible headers, lazy encoded bytes and a teardown owner; + core owns normalization, provenance, decoding, body disposition and bounded finalization. +- Schedule complete begin/batch exchanges before returning their receivers. Driver events report + original indices, actual terminal observations and outcomes, not consumer-created receipts. +- Enforce independent request, encoded response, decoded response, final buffer, header, Brotli, and chunk-shape controls +- Map typed cache-bypass and validated wire-authority policy without changing URI-owned routing, + TLS SNI, or certificate identity +- Preserve typed deadline, transport, protocol, codec, and resource failures without message matching +- Preserve the prepared context's paired clock/timer and exactly-once ownership through response + handoff. Successful streamed collection must not cancel its live payload. +- Attach `x-edgezero-proxy: ` by default; configure relay handles with + `HttpClient::with_proxy_marker(false)` or override an individual request with + `OutboundProxyMarker::Off`. An omitted synthetic marker consumes no header budget and does not + overwrite an upstream marker. +- Publish an exact static capability level for every outbound capability + +Provider limitations are part of the contract rather than hidden implementation details. See +[Capabilities](/guide/capabilities) for the support matrix, timing semantics, and accounting +exclusions. Consumers upgrading platform resource or response construction APIs should use the +[Unreleased migration table](https://github.com/IABTechLab/edgezero/blob/main/CHANGELOG.md#platform-resource-metadata). -Adapters implement `edgezero_core::proxy::ProxyClient` so handlers can forward outbound requests. The client must: +## Logging Initialisation -- Accept a `ProxyRequest` created with `ProxyRequest::from_request` -- Build and send an outbound provider request, reusing headers and streaming the body without buffering -- Convert the provider response into a `ProxyResponse`, again preserving streaming behaviour and normalising encodings -- Attach a diagnostic header (e.g., `x-edgezero-proxy`) identifying which adapter forwarded the call (Fastly, Cloudflare, and Spin do this today; Axum does not) -- Surface provider errors as `EdgeError::internal` so applications can decide how to respond +Fastly wires `log_fastly`, and Axum uses `simple_logger`. Their runners initialize logging +before application configuration. Cloudflare and Spin `init_logger` helpers are no-ops: host +console or stdout/stderr capture does not install a Rust `log` backend. -## Logging Initialisation +`owns_logging = true` skips adapter logger setup completely. Install your backend **before** +entering the adapter runner, apply the configured runtime filter to that backend, and set the +facade maximum high enough for all enabled targets. `edgezero_core::resolve_logging_level(&env)` +resolves the supplied platform environment's runtime level (invalid or absent means `Info`); +it neither installs a logger nor reads manifest defaults implicitly. -Each adapter exports an `init_logger` helper for platform-specific logging backends. Fastly wires -`log_fastly`, Cloudflare currently no-ops, Spin no-ops because Spin manages its own logging -internally, and Axum uses `simple_logger` in its `run_app` helper. -New adapters should provide a comparable helper so apps consistently opt into logging. +Managed Fastly and Axum backends filter `edgezero_core::BOOT_LOG_TARGET` (`edgezero::boot`) at +`BOOT_LOG_LEVEL` (`Warn`) independently of ordinary records. Consequently boot warnings and +errors still appear with ordinary logging set to `Error` or `Off`. Boot `Info`/`Debug` messages +do not appear. The target is not a physical Fastly endpoint. Application-owned loggers choose +their own boot policy; raising `log::set_max_level` alone cannot restore backend-filtered records. ## Contract Tests -To keep the contract enforceable, each adapter includes integration tests that validate request/response conversions and the dispatch helper: +To keep the contract enforceable, each adapter includes tests that validate request conversion, +response framing, and the owned delivery coordinator: -- `into_core_request` for method, URI, header, body, and context propagation -- `from_core_response` for status propagation and streamed body writes -- `dispatch` for routed handlers, body passthrough, and streaming responses +- canonical admission and request conversion for method, URI, header, body, and context propagation +- response-head commit and lazy streamed body writes +- absolute deadlines, accepted-byte accounting, abort/close, and exactly-once terminal reports +- routing, admission refusal, fallback drain, handler, middleware, and fallback response paths ### Fastly Tests @@ -120,10 +161,10 @@ The `wasmtime` version CI uses is pinned in `.tool-versions`. When bringing up another adapter: -1. **Implement request/response conversion functions** that follow the rules above -2. **Provide a context type** exposing the adapter's metadata and insert it in `into_core_request` -3. **Implement a dispatch entry point** (free function or service builder) plus logging helper -4. **Wire up a `ProxyClient`** that streams bodies and normalises encodings +1. **Implement request conversion and an owned response coordinator** that follow the rules above +2. **Provide a context type** exposing the adapter's metadata and insert it during admitted request conversion +3. **Implement a send-owning runner** plus logging helper +4. **Wire up an `OutboundHttpClient`** with limits, deadlines, batching, and typed errors 5. **Copy the contract test suite**, swapping in the new adapter types. Ensure the tests are gated to the target architecture if the adapter SDK does not compile for native hosts 6. **Register the adapter** with `edgezero-adapter::register_adapter` (typically in a `cli` module using the `ctor` crate) so the CLI can discover it dynamically. To take part in `provision` and `config push`, override the relevant `Adapter` trait hooks: `single_store_kinds` and `merged_id_kinds` declare the platform's store shape, `validate_adapter_manifest` checks the adapter's own manifest, `provision` creates platform resources, and `push_config_entries` plus `read_config_entry` move config blobs. `single_store_kinds` and `merged_id_kinds` default to empty, `validate_adapter_manifest` defaults to accepting, `provision` defaults to a no-op, and the config push and read hooks default to reporting the operation as unsupported @@ -152,7 +193,7 @@ logical id under both is a collision that `config validate` rejects. | Axum | `secrets` | none | No | No | Fastly is the only adapter implementing `gc_config_entries` and the staging lifecycle -actions (`DeployStaged`, `EmitVersion`, `Healthcheck`, `Rollback`); the others return +actions (`DeployStaging`, `EmitVersion`, `Healthcheck`, `Rollback`); the others return an unsupported error for those. ## Opt-in application retention @@ -164,12 +205,31 @@ can change and no refresh policy has been defined. Application ownership and request scheduling are separate. An `App` can be retained by its caller; each adapter still controls how requests arrive: -| Adapter | Existing default | Explicit retention | -| ---------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -| Fastly | Build for each single-request invocation | `serve_app` with an SDK `Serve`, or [`lifecycle::serve_custom`](/guide/adapters/fastly#custom-dispatch-and-streaming) | -| Cloudflare | Build on each fetch | Concrete application-owned cache and `dispatch_app` | -| Spin | Build on each invocation | Concrete cache and `dispatch_app` on a compatible host | -| Axum | Build once at server startup | Already retains the router | +| Adapter | Existing default | Explicit retention | +| ---------- | ---------------------------------------- | ------------------------------------------------------------------------------------------ | +| Fastly | Build for each single-request invocation | `serve_app` / `serve_app_with_hooks` with SDK `Serve`, backed by `lifecycle::Sandbox` | +| Cloudflare | Build on each fetch | Concrete application-owned cache and `dispatch_app` | +| Spin | Build on each invocation | Concrete cache and `dispatch_app` on a compatible host | +| Axum | Build once at server startup | Already retains the app and router | + +Hook-based assembly uses `App::build::(PLATFORM) -> Result`. +`Hooks::routes` is fallible: its error skips configuration and is returned unchanged. +Applications that have built their own router use `App::from_router::(router, PLATFORM)` +to apply the same name, configuration and platform check, without rebuilding routes. +Both paths install platform metadata before calling fallible `Hooks::configure` and reject +successful configuration that changes that metadata. `App::state::()` borrows the +router's registered state, also available during configuration; retrieve an `Arc` +registration as that exact type. Retain only a successfully +configured app; never cache an initialization error or publish a partial app. +Macro state expressions can use `state = load_state()?` under the same fallible assembly contract. +Serverless standard entrypoints and custom `request::send_assembly_error` helpers use canonical +detached delivery for failed assembly, preserving typed status without using any partially +configured app callbacks. Fastly retained serving retries after successful error delivery; +Axum instead fails startup before binding. See [assembly failures](/guide/handlers#configuring-the-application-lifecycle). +Cloudflare uses `dispatch_app::(&app, req, env, ctx)`; Spin uses +`dispatch_app::(&app, req)` and returns a raw `SpinResponse`. Neither builds an +app nor initializes logging. The caller must pair the app with its construction +`Hooks` type and selected platform. Retain owned settings, parsed objects, and bounded caches only when their staleness policy is acceptable. Keep native request handles, bodies, store @@ -178,6 +238,11 @@ and cloning do not prove host-resource validity. Shared `Arc` interiors remain shared; the framework does not reset them between requests. Registered app state continues to overwrite request extensions of the same type. +Every dispatch creates fresh admission state and response-egress ownership in +addition to fresh registries. Fastly's preparation/finalization hooks borrow +request-local values; the adapter owns sending and lazy streaming. Retention does +not expose a response-returning Fastly dispatch path or add response buffering. + Applications own refresh, invalidation, key rotation, initialization-failure policy, and workload benchmarks. Publish complete snapshots; overlapping requests keep the snapshot they acquired. Never put an unkeyed static in a generic cache diff --git a/docs/guide/adapters/spin.md b/docs/guide/adapters/spin.md index 38c5129e7..d6728c9c7 100644 --- a/docs/guide/adapters/spin.md +++ b/docs/guide/adapters/spin.md @@ -34,11 +34,11 @@ crates/my-app-adapter-spin/ The Spin entrypoint wires the adapter via `#[http_service]`: ```rust -use spin_sdk::{http::IntoResponse, http::Request, http_service}; +use spin_sdk::{http::Request, http_service}; use my_app_core::App; #[http_service] -async fn handle(req: Request) -> anyhow::Result { +async fn handle(req: Request) -> anyhow::Result { edgezero_adapter_spin::run_app::(req).await } ``` @@ -46,6 +46,11 @@ async fn handle(req: Request) -> anyhow::Result { `run_app` reads the portable store metadata baked into `App` by the `app!` macro plus `EDGEZERO__*` environment variables; it does not require an `edgezero.toml` to be present at runtime. +If route/state assembly or `Hooks::configure` fails, `run_app` and `run_app_with_platform` +deliver the typed error's status and standard JSON body through canonical detached egress. +They release the unread request, use no failed app's policies or headers, and log the full failure +once on `edgezero::boot`. Internal details stay off the wire. The finite default write deadline +starts before assembly; see [application assembly](/guide/handlers#configuring-the-application-lifecycle). ## Building @@ -79,6 +84,31 @@ edgezero deploy --adapter spin spin deploy --from crates/my-app-adapter-spin ``` +## Outbound HTTP + +`SpinOutboundClient` uses the WASI HTTP 0.3 interfaces directly so request-body production, +response completion, limits, and the guest-visible deadline race share one owner. Generated +`spin.toml` files default `allowed_outbound_hosts` to `https://*:*`; cleartext requires an explicit +manifest declaration. + +Spin exposes raw header bytes and drives isolated batch slots in completion order. Cache bypass +adds no synthetic origin header because the adapter owns no intermediary cache. A wire-authority +override is rejected during preflight: WASI HTTP uses the same authority for connection routing, +so Spin cannot preserve URI-owned routing and TLS identity while changing only the HTTP authority. +Unsupported Image Optimizer settings are rejected first, then an unsupported authority override, +before portable method/body validation and budget selection. A pending GET/HEAD source or invalid +response policy cannot mask the authority diagnostic or trigger a body read. The shared facade +prepares contexts and settles unknown GET/HEAD sources before dispatch. Begin and batches schedule +complete exchanges with the WASIp3 executor before returning their completion receivers; later +collection does not start transport work or resample the batch start. +Deadline, batch cancellation, and streamed-upload cancellation remain BestEffort until host +teardown has a documented observed bound, and provider phase-timer defaults may prevent a fully +elastic budget. Downstream response +delivery owns the raw WASI response, body, and result writers in one coordinator and preserves +lazy body production under host backpressure. Successful close is a host handoff, not proof of +client receipt, so response-egress capabilities also remain BestEffort. See +[Capabilities](/guide/capabilities). + ## KV Storage Spin KV is **label-backed and multi-store** — each logical id in @@ -105,7 +135,8 @@ full rationale): - **TTL is unsupported.** Spin's `key_value::Store::set` accepts no expiry. `put_bytes_with_ttl` returns `KvError::Unsupported { operation: "put_bytes_with_ttl" }` (mapped to - HTTP 501); never silently strips the TTL. + HTTP 501) after a successful open; a failed declared binding returns its typed + `OpenFailed` reason first. It never silently strips the TTL. - **Listing is capped.** `Store::get_keys()` is unbounded, so the adapter materialises the key list, filters by prefix, sorts, and pages client-side. A `max_list_keys` cap (default `1000`, override via @@ -204,9 +235,13 @@ Schema-coupling note: the SQLite writer uses the exact `spin_key_value` schema and `INSERT … ON CONFLICT DO UPDATE` statement vendored from spinframework/spin's `crates/key-value-spin/src/store.rs`. A contract test in `edgezero-adapter-spin/src/cli/push_sqlite.rs` asserts -byte-equality against the upstream string, and the workspace's -`spin-sdk = "~7.0"` pin blocks any Spin minor bump that would change -the schema until the operator opts in. +byte-equality against the upstream Spin 4.1.0 strings. The workspace's +exact SDK 7 Git revision, shared with the demo, reusable fixture and generator, +makes every SDK upgrade explicit. The +[dependency audit](../../audits/2026-10-27-spin-sdk-eager-progress.md) records the executor +repair and upstream PR; callers use the SDK's `wasip3::spawn_local` re-export. +The CLI warns when the installed Spin runtime major is outside the +schema-verified 2.x through 4.x range. ```bash # Local dev: writes through to .spin/sqlite_key_value.db. @@ -261,31 +296,18 @@ surfaces before `provision` / `config push` run. ## Logging -`edgezero_adapter_spin::init_logger()` is a no-op today because Spin manages its own -logging internally; `run_app` calls it unless your app sets `owns_logging = true`. An +`edgezero_adapter_spin::init_logger()` is a no-op; `run_app` calls it before application +configuration unless your app sets `owns_logging = true`. An `[adapters.spin.logging]` table in `edgezero.toml` is not consumed by the Spin runtime. View output through `spin up` or your Spin host's log files. ::: tip Logging status -Spin logging is handled by the runtime; install your own `log` implementation in the -entrypoint if you need structured output. +Host stdout/stderr capture does not install a Rust `log` backend. Install yours before the +adapter entrypoint and set `owns_logging = true` if you need facade output. Resolve ordinary +levels with `edgezero_core::resolve_logging_level(&EnvConfig)` and apply both backend and facade +filters; the adapter does not promise boot-target visibility without that backend. ::: -## Proxy Client - -The Spin adapter forwards outbound requests through `spin_sdk::http::send`: - -```rust -use edgezero_adapter_spin::proxy::SpinProxyClient; -use edgezero_core::proxy::ProxyService; - -let response = ProxyService::new(SpinProxyClient).forward(request).await?; -``` - -Proxied responses carry `x-edgezero-proxy: spin`. Every upstream host must be listed -in the component's `allowed_outbound_hosts` in `spin.toml` or the send fails at -runtime. - ## Context Access Access Spin-specific request metadata via the request context extensions: @@ -336,29 +358,60 @@ application logging before the cache's synchronous app constructor. Do not use a generic unkeyed static or hold a lock across an await. ```rust -use edgezero_core::app::{App, Hooks}; -use spin_sdk::{http::{IntoResponse, Request}, http_service}; +use edgezero_adapter_spin::{SPIN_PLATFORM, SpinResponse}; +use edgezero_core::{app::App, EdgeError, MonotonicClock}; +use my_app_core::App as MyApp; +use spin_sdk::{http::Request, http_service}; use std::sync::OnceLock; static APP: OnceLock = OnceLock::new(); +fn retained_app() -> Result<&'static App, EdgeError> { + if let Some(app) = APP.get() { + return Ok(app); + } + let app = App::build::(SPIN_PLATFORM)?; + Ok(APP.get_or_init(|| app)) +} + #[http_service] -async fn handle(req: Request) -> anyhow::Result { - let app = APP.get_or_init(MyApp::build_app); +async fn handle(req: Request) -> anyhow::Result { + // Install application-owned logging before attempting assembly. + let clock = MonotonicClock::default(); + let start = clock.now(); + let app = match retained_app() { + Ok(app) => app, + Err(error) => return Ok(edgezero_adapter_spin::request::send_assembly_error( + req, error, start, clock, + )?), + }; edgezero_adapter_spin::dispatch_app::(app, req).await } ``` -The pinned SDK 6 macro exports a P3 HTTP interface. The `wasm32-wasip2` Rust target +`App::build::(SPIN_PLATFORM)` returns `Result` and invokes +fallible `Hooks::configure` after installing platform metadata. For Akamai Functions, +select `AKAMAI_FUNCTIONS_PLATFORM`; other hosts can supply metadata matching their +configured limits. Only a successful app is cached, so a failed build can be retried. +Concurrent construction attempts may build multiple candidates; only one is retained. +The app must be built from the same `Hooks` type used by dispatch. + +`dispatch_app(&App, SpinRequest) -> anyhow::Result` returns +the raw WASI response, not a collected SDK response. Each invocation resolves fresh +registries and runs admission and response egress; the adapter's coordinator owns +the body writer and transmission-result observation through terminal delivery. + +The pinned SDK 7 macro exports a P3 HTTP interface. The `wasm32-wasip2` Rust target name does not establish the component's HTTP lifecycle. Verify the emitted -interface and host together. spin-sdk 6 requires Spin 3.7 or later. See Spin's +interface and host together. See Spin's [instance-reuse documentation](https://spinframework.dev/v4/http-trigger#controlling-instance-reuse) for the reuse and concurrency controls your host version supports. Test both sequential reuse and overlapping invocations in the same instance. Different guest instances are not evidence of concurrent isolation. Keep -request/response bodies and pending operations invocation-local. Existing -response buffering and stream collection limits still apply. Restore `run_app` +request/response bodies, admission resources, delivery coordinators, and pending +operations invocation-local. Standard response delivery remains lazy under host +backpressure; retention adds no whole-response buffer or collection cap. Restore `run_app` and remove the cache to roll back; default generated entry points are unchanged. ## Next Steps diff --git a/docs/guide/architecture.md b/docs/guide/architecture.md index b01f54dd9..3c0e625d1 100644 --- a/docs/guide/architecture.md +++ b/docs/guide/architecture.md @@ -29,7 +29,8 @@ edgezero/ - **Extractors** - `Json`, `Path`, `Query`, `Form`, `Headers`, `Host`, `ForwardedHost`, and `Validated*` variants - **Middleware** - Composable middleware chain with async support - **Manifest** - `edgezero.toml` parsing and validation -- **Compression** - Shared gzip/brotli stream decoders +- **Compression** - Shared gzip/deflate/brotli stream decoders and typed passthrough classification +- **Outbound HTTP** - Typed requests, responses, limits, deadlines, batching, and capability contracts Handlers in your core crate only depend on `edgezero-core`, keeping them portable. @@ -58,26 +59,27 @@ Adapters translate between provider-specific types and the portable core model: - Converts Fastly `Request` to `edgezero_core::http::Request` - Maps core responses back to Fastly `Response` - Provides `FastlyRequestContext` for accessing Fastly-specific APIs -- Implements `FastlyProxyClient` for upstream requests +- Injects `FastlyOutboundClient` for upstream requests ### edgezero-adapter-cloudflare - Converts Workers `Request` to core request - Maps responses to Workers `Response` - Provides `CloudflareRequestContext` for Workers APIs -- Implements `CloudflareProxyClient` for fetch operations - -### edgezero-adapter-spin - -- Converts `spin_sdk::http::Request` to core request and back -- Provides `SpinRequestContext` for Spin-specific APIs -- Implements `SpinProxyClient` for outbound requests +- Injects `CloudflareOutboundClient` for fetch operations ### edgezero-adapter-axum - Wraps `RouterService` in Axum/Tokio services - Powers the local development server - Supports native container deployments +- Injects `AxumOutboundClient` backed by `reqwest` + +### edgezero-adapter-spin + +- Converts Spin/WASI HTTP requests and responses +- Resolves component-scoped KV, config, and secret bindings +- Injects `SpinOutboundClient` backed by WASI HTTP 0.3 ## CLI Crate @@ -93,16 +95,16 @@ Adapters translate between provider-specific types and the portable core model: ``` ┌─────────────────────────────────────────────────────────────┐ │ Provider Runtime │ -│ (Fastly Compute / Cloudflare Workers / Axum Server) │ +│ (Fastly Compute / Cloudflare Workers / Spin / Axum) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Adapter │ │ - into_core_request(): Provider Request → Core Request │ -│ - from_core_response(): Core Response → Provider Response │ -│ - run_app(): Canonical lifecycle │ -│ - dispatch(): Low-level manual lifecycle │ +│ - response coordinator: Core envelope → owned delivery │ +│ - run_app(): Canonical request and response lifecycle │ +│ - adapter service: Optional low-level send-owning wiring │ └─────────────────────────────────────────────────────────────┘ │ ▼ diff --git a/docs/guide/blob-app-config-migration.md b/docs/guide/blob-app-config-migration.md index 86f6f74c0..0ca558a3c 100644 --- a/docs/guide/blob-app-config-migration.md +++ b/docs/guide/blob-app-config-migration.md @@ -99,7 +99,11 @@ with the same data produce blobs with identical `sha256` and different The push writes to `.edgezero/local-config-.json` next to your `edgezero.toml`. The file is a JSON map of `{ "": "" }`; -the dev server reads the envelope through `AxumConfigStore::get`. There +the dev server reads the envelope through a shared `ConfigValue` snapshot loaded +before listener bind. Push before startup or restart the server afterward. The +raw `ConfigStore` API now returns `ConfigValue`; custom store implementations must +migrate both read methods atomically. Borrow its UTF-8 bytes with `as_str()` and +reserve `to_string()` for deliberate ownership copies. There is no `--local` flag because Axum's push IS always local. ### Cloudflare diff --git a/docs/guide/capabilities.md b/docs/guide/capabilities.md new file mode 100644 index 000000000..3a0b6a504 --- /dev/null +++ b/docs/guide/capabilities.md @@ -0,0 +1,663 @@ +# Capabilities + +Capabilities let an application state which portable runtime behaviors it depends on. +`edgezero build`, `serve`, and `deploy` compare the manifest declaration with the selected +adapter before starting the adapter command. + +## Declaration + +```toml +[capabilities] +required = ["outbound-deadlines"] +optional = ["outbound-http"] + +[capabilities.outbound] +hosts = ["https://api.example.com", "https://*.example.net:*"] +``` + +A capability cannot appear more than once or in both lists. `required` accepts `Native` +and `BoundedCooperative`; it rejects `BestEffort` and `Unsupported`. `optional` never +blocks an operation, but degraded and unavailable support is logged. A missing or malformed +capability contract fails closed when it contains required entries. + +Support levels mean: + +- `Native`: fully supported without a documented deviation. +- `BoundedCooperative`: enforced with a documented deterministic bound. No current outbound + matrix cell uses this level. +- `BestEffort`: implemented, but with a documented behavioral deviation or an unverified + deployment prerequisite. +- `Unsupported`: unavailable on that adapter. + +## Platform Memory Ceilings + +`Adapter::platform_metadata()` reports target-owned resource facts separately from behavioral +capabilities. Runtime adapters install the same `PlatformMetadata` before `Hooks::configure` +runs, so startup validation and CLI tooling use one canonical value. `Primary ceiling` excludes +the separately published Fastly stack allowance. A source is shown only for a known fact; an +unknown fact instead records why it is unknown. + +| Target | Primary ceiling | Separate stack | Scope | Memory source / reason | Live inbound requests | Population source / reason | Host ingress/framing charge | Accounting source / reason | +| ----------------------- | ------------------------- | -------------------- | ------------- | ---------------------------------- | --------------------- | -------------------------------- | --------------------------- | ----------------------------- | +| Axum | Unknown | Unknown | Unknown | Unknown: Operator configured | Unknown | Unknown: Operator configured | Unknown | Unknown: Operator configured | +| Cloudflare Workers | 128000000 bytes (128 MB) | None | Per instance | Platform limit: Cloudflare Workers | Unknown | Unknown: Provider unpublished | Unknown | Unknown: Provider unpublished | +| Fastly Compute | 128000000 bytes (128 MB) | 1000000 bytes (1 MB) | Per execution | Platform limit: Fastly Compute | 1 | Platform limit: Fastly Compute | Unknown | Unknown: Provider unpublished | +| Spin (generic) | Unknown | Unknown | Unknown | Unknown: Runtime configured | Unknown | Unknown: Runtime configured | Unknown | Unknown: Runtime configured | +| Akamai Functions (Spin) | 134217728 bytes (128 MiB) | None | Per execution | Hosted default: Akamai Functions | 1 | Hosted default: Akamai Functions | Unknown | Unknown: Provider unpublished | + +Cloudflare's limit is shared by concurrent requests in one isolate, but the provider does not +publish the maximum live inbound request population charged to that memory domain. Its limit of +six simultaneous outbound connections applies to connections waiting for response headers within +one invocation; it is not an inbound population bound. Fastly publishes per-execution heap and +separate stack allowances with a population of one. Generic Spin leaves all three facts unknown +because deployment configuration controls them. Applications deployed to Akamai Functions opt +into `AKAMAI_FUNCTIONS_PLATFORM`, which publishes the hosted 128 MiB per-execution default and a +population of one rather than changing the meaning of every Spin deployment. + +All current adapters receive host-parsed requests. Where the provider does not publish whether +host parser, HPACK, or framing allocations count toward the memory ceiling, host-ingress +accounting remains `Unknown: Provider unpublished`; guest-visible allocation data cannot fill in +that fact. Unknown memory, population, or host-accounting facts make +`PlatformMetadata::validate_memory_envelope` return `Indeterminate`, so only `Fits` is a complete +validation result. A known minimum can still return `Exceeds` before all other facts are known. + +Sources: [Cloudflare Workers limits](https://developers.cloudflare.com/workers/platform/limits/), +[Fastly Compute resource limits](https://docs.fastly.com/products/compute-resource-limits), and +[Akamai Functions quotas](https://techdocs.akamai.com/akamai-functions/docs/quotas-and-limits). + +## Ingress Matrix + +| Capability | Axum | Cloudflare | Fastly | Spin | +| -------------------------------- | ----------- | ----------- | ----------- | ----------- | +| `ingress-admission` | Native | Native | Native | Native | +| `ingress-admission-abort` | Native | BestEffort | BestEffort | BestEffort | +| `inbound-read-deadlines` | Native | BestEffort | BestEffort | BestEffort | +| `raw-ingress-head-limits` | Unsupported | Unsupported | Unsupported | Unsupported | +| `raw-ingress-framing-validation` | Unsupported | Unsupported | Unsupported | Unsupported | + +All standard adapters resolve the route and run the application admission policy before +middleware, handler dispatch, or body polling. Axum can preempt an asynchronous body read. +Cloudflare and Spin race pending reads against platform timers and enforce absolute checks +around ready results; both require deployed probes before host-side teardown latency can be +bounded. Fastly cannot preempt a synchronous host body read. Those limitations keep the three +deadline cells at `BestEffort`. + +`AdmissionDecision::Abort` is a non-response outcome: it polls no body, invokes no middleware or +handler, starts no response-egress lifecycle, and emits no HTTP response. Axum owns the HTTP/1 +connection and closes or resets it at that boundary, so its abort capability is `Native`. +Cloudflare, Fastly, and Spin convert the outcome to a fixed provider error before response +delivery; their hosts decide the transport effect, so those cells remain `BestEffort` pending +deployed proof of a reset or close. + +The detached-egress decision factory can select the same non-response outcome for normalized +request-head validation or admission-policy failures when an application cannot reserve a bounded +response resource. Axum routes that decision through its proven connection-owned abort path. +Cloudflare, Fastly, and Spin route it through their fixed pre-response provider error boundaries; +this does not promote their `BestEffort` capability cells. + +All current adapters expose `IngressHeadAccounting::HostManaged` and +`IngressFraming::HostManaged`. They perform defensive checks on the normalized request +information they receive, but those checks do not prove parser-boundary accounting or +ambiguous HTTP/1 framing rejection. Neither raw-ingress capability is currently available on +any adapter. + +Axum additionally enforces a finite adapter-owned HTTP/1 connection pool, combined raw-head +byte and field-count limits, and absolute idle-plus-header timeouts before admission. Its +[bounded HTTP/1 ingress configuration](/guide/adapters/axum#bounded-http1-ingress) publishes the +effective limits, accept-loop overshoot, parser storage assumptions, and response-header cache +safety policy. These controls do not supply independent raw-target/field-section accounting or +the strict framing policy, so they do not promote either raw-ingress capability above. + +Separately, Axum preserves transport-certified empty bodies (`is_end_stream()`) as buffered empty +after admission for any method, without reading them. The body remains Initial, not Cached, and +framing remains `HostManaged`. Local HTTP/1 GET/HEAD forwards with absent framing or +`Content-Length: 0` succeed even after the inbound read deadline when passed through +`into_request`; core's first `body_bytes` deadline check is unchanged and can still return 408. +Unknown streams, including chunked-empty and nonempty GET controls that still return 400 without +origin contact, retain their existing behavior. This narrow +conversion checkpoint does not complete general GET/HEAD settlement, add a capability row or +promote any matrix cell. + +Fastly preserves actual SDK body absence (`try_take_body() == None`) as buffered empty after +admission, without constructing an SDK body. Every present SDK body stays lazy: zero host length +does not certify EOF or account for SDK-prefetched bytes and pending writes. Normal Fastly +`from_client()` requests carry a body handle even when bodiless, so general GET/HEAD forwarding +remains unfinished. This changes no matrix cell or framing claim. See +[Fastly ingress conversion](./adapters/fastly.md#ingress-body-conversion) for the SDK and deadline +boundaries. + +Inbound readers and body extractors return 413 for byte-cap overflow, 400 for malformed +payloads, and 408 for an expired admitted read deadline. Demo and scaffold fallback policies +select 413 for overflow and 408 for timeout; applications may select their own bounded responses. + +Admission is body-blind. An application may opt into `ReadBodyBeforeFallback` for pre-resolved +404/405 requests when it needs body-limit precedence: clean EOF at or below the selected cap +retains the canonical 404/405, the first byte over returns the application-selected buffered +`on_exceeded` response first, and expiration of the one absolute read deadline returns the +application-selected buffered `on_timeout` response first. The application-supplied ingress +grant remains live throughout the drain and is released before response conversion, when the +core drain future is dropped, or when the finite read deadline terminates the drain. This path +discards the body and invokes no route middleware, handler, or request context. `Refuse` remains +the zero-read overload path, and ordinary admission retains the immediate, no-poll 404/405 +behavior. Axum dispatches the admission and fallback-drain future directly on its connection-local +executor, so cancellation drops that future and its grant. Cloudflare, Fastly, and Spin retain the +provider limitations described above. + +## Config Matrix + +| Capability | Axum | Cloudflare | Fastly | Spin | +| ------------------------------- | ----------- | ----------- | ----------- | ----------- | +| `config-read-allocation-bounds` | Unsupported | Unsupported | Unsupported | Unsupported | +| `config-read-deadlines` | BestEffort | BestEffort | BestEffort | BestEffort | + +Every adapter enforces per-value and cumulative guest-visible config byte caps before EdgeZero +retains or processes a returned value. That does not bound provider-side materialization, +parser allocation, SDK copies, or other work performed before the value reaches guest code, so +`config-read-allocation-bounds` remains `Unsupported` everywhere. + +Axum's local config-store path now has [bounded startup snapshots](/guide/adapters/axum#snapshot-allocation-limits): +actual file reads, flat-string parsing, per-key/value and entry caps, aggregate snapshot +residency and startup staging are enforced before listener binding. Request reads clone +shared `ConfigValue` handles without copying payloads. This narrower guarantee does not +certify environment-backed secrets, fixed registry metadata, allocator overhead or all +concurrent extraction allocations, so the broad capability is not promoted. + +Cloudflare bounded config reads request KV byte streams and use reusable 16 KiB Rust/JS +BYOB buffers. With `L = min(max_backend_bytes, max_value_bytes)`, the requested payload +storage is bounded conservatively by `2L + 32 KiB`, including accumulator relocation or +the final shared-string copy. Invalid UTF-8 is rejected, not lossily expanded. There is no +text/array-buffer fallback. Overflow, timeout and caller cancellation cancel an acquired +reader; cancellation while waiting for KV acquisition cannot cancel the provider promise. +An acquisition owner cancels any stream that arrives after abandonment. Cancel-promise +rejections are consumed without publishing provider diagnostics. Ready reads check the +application clock before and after each pull and yield to the task queue every 16 chunks. +Bounded KV reads require `FinalizationRegistry` and fail before provider dispatch without it. +Generated/demo Workers explicitly enable `enable_weak_ref`; GC timing remains nondeterministic, +not part of the payload charge. See [Workers compatibility flags](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-finalizationregistry-and-weakref). +Opaque KV buffering, JS wrapper/GC retention and host teardown remain unqualified. + +Fastly bounded config reads use the pinned host ABI with one initialized caller-sized +buffer, never retrying a `BUFLEN` result with the provider's requested length. Physical +reads have an adapter ceiling of 32,000 bytes. Root reads allow at most +`R = min(max_backend_bytes, max(max_value_bytes, 8000), 32000)`; pointer JSON is limited to +8,000 bytes and chunk reads to 7,000 bytes and both remaining allowances. Validated +declared totals must fit before any chunk fetch or reassembly allocation. The bounded +resolver verifies chunk/transport hashes and layout without constructing an envelope +`Value`; the extractor performs inner-envelope verification after its structural scan. +A conservative requested guest-payload charge is `2V + R + 16P + 7000` bytes, with +`V = max_value_bytes`, `P = 8000`, plus fixed objects and allocator overhead. This includes +pointer/schema/index scratch, one chunk, reassembly and the shared-string copy; provider +storage and synchronous-call interruption are excluded. Raw `get` and CLI integrity +operations are not allocation-certified. + +Spin's list-valued host get still materializes a complete value. Stock Spin 4.1.0 has no +configurable read-size cap in its KV interface; Akamai's published 1 MB default value quota +does not establish an exact read-allocation or overridden-quota contract. Neither is used +as a guessed allocation guarantee. A bounded host/backend interface or verified hosted +enforcement is still required. + +The response-egress specification's evidence matrix +(`docs/superpowers/specs/2026-09-08-response-egress-design.md`, section 8) +distinguishes local ownership proofs from deployed lifecycle qualification. Opt-in fixtures and +`scripts/qualify_response_egress.mjs` collect correlated terminal/source/resource evidence; +they cannot certify peer delivery, same-instance recovery or provider allocation. No deployed +artifact has been supplied for the current hosted adapters, so their guarantees are not promoted. + +Config reads use one absolute extraction deadline in the application's monotonic clock domain. +Core passes that same clock through every root, Fastly chunk, and secret read. Adapters sample it +before provider dispatch and after readiness, before propagating either success or provider +failure, so equality expiry wins simultaneous readiness. Cloudflare and Spin also race asynchronous +guest futures against native timers. A timer wake is checked against the injected clock and re-armed +when it arrives early. This bounds EdgeZero's return but does not prove host-side cancellation. Axum +and Fastly provider operations are synchronous and guest-nonpreemptible. No provider operation is +proved cancellable within a finite wall-clock interval, so every adapter reports `BestEffort` for +`config-read-deadlines`. Spin opens declared config labels inside that timed read, after ingress +admission; Cloudflare and Fastly retain failed declared bindings as typed failing handles rather +than treating them as absent. The +[configuration design](https://github.com/IABTechLab/edgezero/blob/main/docs/superpowers/specs/2026-06-16-blob-app-config.md#632-bounded-capability-scoped-extraction-reads) +defines the normative accounting, cancellation, error, and promotion requirements. + +Declared **KV** stores use a separate policy on Fastly, Cloudflare and Spin: registry +construction performs no provider opens, every declared ID remains present, and first use +returns a typed `KvError::OpenFailed` on failure. Missing/invalid bindings map to 500; +provider unavailability maps to 503. Settled open results are request-local; only warning +membership for compiled IDs survives requests. Explicit required-store opening remains +fail-fast, while KV-free routes can serve normally. Axum startup behavior is unchanged. +See [declared KV failures](./kv.md#declared-store-failures). This does not strengthen +config-read or provider-allocation capabilities. + +Application assembly is fallible. `Hooks::configure(&mut App) -> Result<(), EdgeError>` runs after +the runtime adapter installs `PlatformMetadata` and before EdgeZero request conversion, body +polling, or dispatch. The hard-cut, core-owned `App::build::(platform)` constructor requires +every caller to choose target metadata explicitly and propagates the configuration result. After +successful configuration, core rejects changed platform facts, provenance, or unknown reasons +rather than silently restoring them. Axum also completes assembly before binding its listener. + +## Response Egress Matrix + +| Capability | Axum | Cloudflare | Fastly | Spin | +| ---------------------------------- | ---------- | ---------- | ---------- | ---------- | +| `buffered-response-content-length` | Native | BestEffort | BestEffort | BestEffort | +| `response-egress-abort` | BestEffort | BestEffort | BestEffort | BestEffort | +| `response-egress-backpressure` | BestEffort | BestEffort | BestEffort | BestEffort | +| `response-egress-completion` | BestEffort | BestEffort | BestEffort | BestEffort | +| `response-write-deadlines` | BestEffort | BestEffort | BestEffort | BestEffort | + +`buffered-response-content-length` covers untransformed transmitted buffered payloads, including +empty and framework-generated bodies. Core derives their exact length on Axum, Cloudflare and +Spin; Fastly stages the complete native buffer before its SDK-equivalent Automatic handoff. +HEAD/204/205/304 do not gain a core-derived length. Suppressed buffers are dropped and streams +are never polled. Exact Fastly known-buffer HEAD/205 SDK parity remains unqualified: local SDK +controls expose forbidden payload delivery and HTTP/2 protocol failures. The requester approved +preserving strict suppression rather than restoring a HEAD body merely to reproduce SDK framing. +Host transformations may recompute or omit the wire length. Serverless cells remain `BestEffort` +pending frozen qualifying evidence: hosted Fastly HTTP/1.1 and HTTP/2 compression/parity, deployed +Workers header delivery, and the pinned Spin runtime check. Local characterization alone is not +hosted qualification. + +The remaining cells describe transport-observable client-response delivery, not response conversion. +Every adapter now owns a response coordinator through its strongest available host boundary, +uses the application's monotonic clock, applies one absolute write deadline, and emits one +terminal report. The boundaries remain `BestEffort` because none proves end-client receipt: + +- Axum commits when Hyper accepts the response head, accounts payload frames as Hyper polls them, + closes the owned HTTP/1 connection on deadline, and reports clean source EOF as `HostHandoff`. +- Cloudflare awaits JavaScript writer promises, races request abort/deadline signals, and keeps the + coordinator alive with `waitUntil`; deployed disconnect and completion timing remain unproved. +- Fastly hands buffered heads and complete bodies to the host together, accounting transmitted + source bytes once at successful handoff; this is not proof of client receipt. Actual streams + account synchronous body-handle writes through `stream_to_client` close, but Fastly cannot + preempt a blocked source poll or hostcall and loses the body handle if `finish` itself fails. +- Spin owns the WASI response/body/result writers through handoff and races pending operations + against its timer. An early successful host response-result observation is retired once without + ending pending body work; source EOF, writes and body-result publication must still succeed under + the same deadline; host teardown and network completion remain provider-observable only. + +Provider-owned buffering, SDK allocations, and bytes beyond each stated acceptance boundary are +not included. These limitations block whole-response-lifetime certification. + +## Outbound Matrix + +| Capability | Axum | Cloudflare | Fastly | Spin | +| --------------------------------------- | --------------------------------- | --------------------------------- | ----------------------------------- | --------------------------------- | +| `outbound-http` | Native | Native | BestEffort⁹ | Native | +| `outbound-authority-override` | Native | BestEffort⁸ | Native | Unsupported[^spin-authority] | +| `outbound-batch-cancellation` | Native | BestEffort⁸ | BestEffort⁴ | BestEffort⁸ | +| `outbound-batch-completion-order` | Native | Native | BestEffort⁴ | Native | +| `outbound-batch-slot-isolation` | Native | Native | BestEffort⁴ | Native | +| `outbound-cache-bypass` | Native | Native | Native | Native | +| `outbound-complete-resource-accounting` | Unsupported[^resource-accounting] | Unsupported[^resource-accounting] | Unsupported[^resource-accounting] | Unsupported[^resource-accounting] | +| `outbound-header-fidelity` | Native | BestEffort⁸ | Native | BestEffort[^spin-header-fidelity] | +| `outbound-deadlines` | Native | BestEffort⁸ | BestEffort¹ | BestEffort⁸ | +| `outbound-failure-diagnostics` | BestEffort | BestEffort | Native | Native | +| `outbound-image-optimizer` | Unsupported | Unsupported | BestEffort[^native-image-optimizer] | Unsupported | +| `outbound-redirect-following` | Native | Native | Native | Native | +| `outbound-flexible-phase-budget` | Native | Native | BestEffort⁵ | BestEffort⁵ | +| `streamed-upload-deadlines` | Native | BestEffort⁸ | BestEffort² | BestEffort⁸ | +| `lazy-streamed-response-passthrough` | Native³ | Native | BestEffort⁶ | BestEffort⁷ | + +[^native-image-optimizer]: + Fastly single sends with explicit native optimizer options use the SDK's synchronous + optimizer service. Region is required; only bodiless GET/HEAD with default caching are + supported. Viceroy has no optimizer and returns typed `ImageOptimizerUnavailable` (502). + The synchronous call cannot be interrupted and its full blocking horizon is unknown. + The original absolute deadline is checked before dispatch and after return; late results + are rejected, not granted additional budget. Existing response limits and lazy streamed + handling apply afterward. Begun requests and batch slots reject the option. Explicit redirect + following uses the same supported single-send mapping on each hop, without another fetch, + buffering or serialization. No hosted transformation, pooling or performance qualification is claimed. + +[^resource-accounting]: + Complete accounting includes provider parsing and field-section + materialization, informational responses, final headers, trailers, native receive chunks, + guest buffers and spare capacity, decoder state, allocator metadata, and runtime copies. + Current provider APIs do not expose or bound every term, so all adapters report + `Unsupported`. The narrower EdgeZero-owned limits below still apply. + +[^spin-authority]: + Spin's WASI HTTP authority also selects the connection target. An explicit HTTP authority + override is rejected before body polling or provider dispatch because the adapter cannot + preserve the required routing/TLS separation. + +¹ Fastly cannot preempt cold dynamic-backend registration or guest-to-origin writes. Before a +cache-miss registration in `start_batch_until`, EdgeZero checks the original absolute deadline. +Preparation that takes more than 25 ms while that deadline remains live no longer causes an +internal dispatch-slack error. Expiry is rechecked after synchronous preparation and registration; +a late return retains the original timeout cause without restarting the budget. A cold registration +that passes the entry check can still block. Removing the fixed slack refusal does not make SDK +host calls preemptible or provide one end-to-end wall-clock guarantee. Fastly's B31 aggregate policy +anchors provider timers to the configured per-call timeout +(clamped to seven days), or the original selected duration if no timeout is configured. It rounds +up to milliseconds, then to the smallest of `2^0` through `2^29` milliseconds or the seven-day +ceiling. Connect, first-byte and between-bytes use that same value; there is no rigid phase split. +Logical absolute deadlines and late-result rejection are unchanged. Provider timers can outlive a +shorter caller deadline/cutoff substantially, so this is not prompt interruption. + +Versioned backend names depend on origin, authority, TLS and the selected timer configuration, +not client identity or the later remaining budget. A guest-instance-wide verified registry shares +successful registrations across clients and refuses unknown name collisions. There are at most +31 timer configurations per fixed transport tuple, not a bound on arbitrary origins, registrations, +memory or abandoned host work. Hosted pooling, resource lifetime and cancellation remain unqualified. + +² Fastly checks the absolute deadline between streamed upload chunks, but it cannot preempt a +stalled source pull or host write. + +³ Axum preserves the portable non-`Send` stream on a connection-local executor and yields one +payload frame at a time under Hyper demand. Unit and raw-socket tests prove first-frame delivery +before source EOF. Socket/client completion remains separately BestEffort under the response-egress +capabilities above. + +⁴ Fastly dispatches slots sequentially, then cooperatively services pending heads and demanded +bodies within that operation. Each raw body read requires fresh matching timed readiness and +reads at most 16 KiB; rotating slot service prevents a continuously-ready body from monopolizing +the driver. An occupied result slot backpressures only its own exchange. Cached SDK Send heads +retain their opaque cache state and use nonblocking polling with one shared positive 1 ms pulse. +Cold backend registration can still delay dispatch; synchronous provider work and SDK cleanup +remain non-preemptible. This is not callback-wide scheduling or a hosted isolation guarantee. +Local error-preserving readiness requires the explicitly selected +[Viceroy repair](https://github.com/IABTechLab/edgezero/blob/docs/outbound-http-spec/patches/viceroy/README.md); +the stock runtime still loses source errors. +Hosted runtime behavior and resource ownership remain unqualified, so support stays BestEffort. + +⁵ Fastly uses coupled B31 provider timers, but blocking host operations cannot be preempted +and those timers are not one fully elastic end-to-end pool. Spin's host may reject phase +timer settings and retain opaque defaults. Every connect-setting refusal falls back without +an error under the unchanged aggregate deadline race. For first-byte/between-bytes settings, +`NotSupported` is advisory; `Immutable` and `Other` remain typed setup errors. Neither adapter +can promise one fully elastic pool or bounded host teardown. + +⁶ Fastly uses an undecorated, send-owning entrypoint and the low-level `stream_to_client` response +lifetime. Scripted pump tests and the raw ABI Viceroy test prove the pieces, but the production +`run_app` composition has not yet been exercised end to end under Viceroy. The lazy-passthrough +cell therefore remains `BestEffort`; synchronous preemption and finish limitations separately +keep the response-egress cells BestEffort. + +⁷ Spin writes one portable chunk at a time through the WASI body writer. Unit tests and target +compilation prove the coordinator, but a live Spin component test has not yet established the +generated production composition. Host teardown and network completion also lack deployed proof. + +⁸ Spin cancellation is cooperative until host teardown is observable within a documented +bound. Cloudflare wires one owned `AbortController` through fetch and streamed body consumption, +but no deployed host-observed cancellation artifact currently proves a finite cancellation bound; +its outbound and streamed-upload deadline cells therefore remain `BestEffort`. Cloudflare also +exposes normalized header strings rather than original raw octets and field boundaries, although +it preserves the visible header semantics used by the client. + +⁹ Fastly outbound HTTP uses dynamic backends, which must be enabled on the deployed service. +The CLI cannot currently prove that entitlement. If disabled, dispatch returns a typed 502 +with the documented enablement diagnostic. + +## Redirect Policy + +`outbound-redirect-following` reports shared redirect policy, not native deadlines, cancellation +or complete resource bounds. The `Native` cells mean `HttpClient::follow` applies the same explicit +limit, method/replay rules, target callback and head-first body disposition on all four adapters, +using the same client dispatch and response policy as a plain send. Plain sends, begun requests +and batches still return 3xx unfollowed. See [Following Redirects](./proxying.md#following-redirects) +for credential, security, option inheritance and chain-deadline rules. + +Spin requires plain-HTTP origins and targets outside a narrowed host list to be declared. A target +can pass the application policy and still fail at a provider boundary. Fastly dynamic-backend +entitlement, B31 registration and non-preemptible host-call limits remain unchanged. + +Local redirect tests are not hosted qualification. Native Image Optimizer qualification remains +open: its synchronous blocking horizon is unknown, and following retains its existing native call +path without extra fetches, buffering or serialization. No deadline, cancellation, egress or +resource-accounting capability is promoted by this row; the separate matrix cells still apply. + +## Provider Failure Diagnostics + +`outbound-failure-diagnostics` describes retention of available SDK failure evidence, not +stronger transport guarantees. Read it with `EdgeError::provider_failure_detail()`; the closed +catalogs are `AxumFailureKind`, `CloudflareFailureCause`, `FastlyFailureCause`, and +`SpinFailureCause` in `edgezero_core::error_detail`. + +Fastly and Spin report `Native`: their existing SDK failure boundaries project typed causes and +available numeric payloads without provider text. Fastly retains backend/send causes, DNS codes, +TLS alert IDs, HTTP/2 frame/error codes, platform statuses and I/O kinds where supplied. Spin +retains WASI error codes, DNS info codes, TLS alert IDs and size limits, plus preparation errors. +Spin's text-only DNS rcode accepts only `NOERROR`, `FORMERR`, `SERVFAIL`, `NXDOMAIN`, `NOTIMP`, +and `REFUSED`; other values become `Unknown`. A provider-hidden cause is not reconstructed: +pinned Viceroy can report an internal cause where hosted Fastly may expose connection refusal. + +Axum and Cloudflare report `BestEffort` because their providers expose incomplete cause detail. +Axum retains reqwest kind/connect/timeout predicates and an I/O kind when discoverable through +its source chain. Cloudflare retains only allowlisted `AbortError`/`TypeError` exception names; +SDK-erased or unrecognized causes remain `Unknown`. Neither promises complete DNS/TLS attribution. + +`Unknown` describes an observed opaque/unrecognized cause; absent optional codes remain absent. +Timer-only expiry, late success, clean EOF and local validation do not invent provider evidence. +The phase identifies the observed API boundary, not whether request bytes were sent. Detail is +fixed-size and text-free: it carries no URLs, header values, body bytes or provider messages, and +does not change framework wire responses or existing error classification. Framework code does +not log provider detail; any future framework logging must be debug or lower. Application logging +and correlation remain application-owned. Fixed framing-rule error logs are separate. + +This row publishes the implemented SDK projection, not full EZ-16 acceptance, invocation-bound +scope ownership, explicit redirect following, hosted cause provenance, cancellation or client delivery. +Local SDK/Node/WASI fixtures are not hosted qualification; the failed Fastly readiness gate +remains open. Existing deadline, delivery and resource-accounting capability levels are unchanged. +Applications that tolerate reduced detail can declare this capability optional; requiring it +accepts Fastly/Spin and rejects the BestEffort Axum/Cloudflare cells. + +## Outbound Content-Type Provenance + +[^spin-header-fidelity]: + A local Spin 4.1 SDK-only WASI control observes `Connection` removed + before EdgeZero receives response fields, while a nominated extension field remains. EdgeZero + preserves the bytes/field boundaries that the host exposes, but cannot enforce or certify a + hidden nomination. Shared normalization still rejects malformed visible nomination lists and + strips visible nominated fields. This is not complete raw-header fidelity or hosted proof. + +`OutboundResponse::content_type_provenance()` reports original `Content-Type` evidence +captured before EdgeZero drops non-UTF-8 values or hop-by-hop/connection-nominated fields. +It is not MIME validation, an acceptance decision, or a debug-safety guarantee. + +| Provenance | Meaning | +| ---------------------------- | ------------------------------------------------------------------------------------------------------- | +| `SingleTextUnchanged` | Exactly one original UTF-8 value, retained unchanged, with no later mutable header access. | +| `Ineligible(Missing)` | No original value. | +| `Ineligible(MultipleValues)` | Multiple original values, even identical values or a textual value plus one later dropped as non-UTF-8. | +| `Ineligible(NonText)` | Exactly one original value, not valid UTF-8. | +| `Ineligible(Removed)` | An original textual singleton was stripped, such as by `Connection: content-type`. | +| `Ineligible(Modified)` | Mutable header access invalidated an originally eligible response. | +| `Unknown` | The adapter cannot recover original bytes or field boundaries; this says nothing about MIME validity. | + +Axum, Fastly, and Spin retain the original normalization metadata on buffered, streamed, and +bodyless response paths. Cloudflare **always** reports `Unknown`, including for absent or +apparently single values: Fetch string iteration has already lost original octets and multiplicity. +Core does not trim, split commas, or MIME-validate. A known single +`application/json, text/plain` value can be `SingleTextUnchanged`; the application must still +reject it during MIME validation. A quoted comma does not imply multiple original values. + +```rust +use edgezero_core::{ContentTypeProvenance, OutboundResponse}; + +fn content_type_candidate(response: &OutboundResponse) -> Option<&str> { + if response.content_type_provenance() != ContentTypeProvenance::SingleTextUnchanged { + return None; + } + let value = response.headers().get("content-type")?; + let candidate = std::str::from_utf8(value.as_bytes()).ok()?; + // Application must MIME-validate and apply acceptance/redaction policy before use. + Some(candidate) +} +``` + +`headers_mut()` conservatively marks headers dirty even without an edit or when only an +unrelated field changes. The private original evidence stays immutable; the accessor returns +`Ineligible(Modified)` for a previously eligible response. Originally ineligible or unknown +evidence never upgrades, even if the application inserts a valid singleton or restores old bytes. + +For adapter authors, `OutboundHeaderInput::normalize(limiter, method, status, marker)` consumes +the original headers and a `ResponseHeaderLimiter`, returning the normalized header map, +`ResponseHeadNormalization { body_disposition, content_type_provenance }`, and immutable +Location evidence. Pass a limiter that has not already charged this field section. This is the +same operation used by all four adapters and `OutboundHead::new`: it charges original fields +before filtering, captures original evidence, normalizes, then charges the optional synthetic +marker against the same cumulative budget. The separate public `normalize_response_headers` +and `insert_proxy_header` helpers are removed, not retained as compatibility aliases. + +Use the returned `.body_disposition` without a second normalization pass; a 205 must retain +whether its original declared length requires immediate suppression. Pass the original +`.content_type_provenance` (always `Unknown` for `ProviderFieldBoundariesUnknown`, including +Cloudflare) explicitly to the six-argument +`OutboundResponse::new(method, status, headers, body, clock, content_type_provenance)`; +never infer it from the normalized header map. `into_parts()` returns the five-tuple +`(method, status, headers, body, provenance)` without another normalization pass. + +`into_response()` performs final defensive normalization and carries effective +`ContentTypeProvenance` in the resulting response's extensions without upgrading original +ineligible/unknown evidence. That extension describes the **historical outbound boundary only**; +it cannot certify later ordinary `http::Response` edits, which remain application-owned. +Provenance adds only tiny enum metadata, no retained/copied raw headers or body, and no new +capability or change to the matrix above. + +## Host Declarations + +`[capabilities.outbound].hosts` configures platform host plumbing; it is not an application +authorization policy. Enforce user-controlled destination policy in application code. + +- Omitting `hosts` defaults to `https://*:*`. Cleartext is not granted implicitly. +- `"*"` explicitly expands to both `http://*:*` and `https://*:*`. +- A bare host or wildcard subdomain defaults to HTTPS and port 443. +- An explicit scheme may be `http` or `https`; ports are `1..=65535` or `*`. +- Paths, queries, fragments, user information, whitespace, non-ASCII names, and malformed + wildcards are rejected. + +Spin receives the canonicalized entries through `allowed_outbound_hosts`. Cloudflare does not +need a build-time destination list. Fastly creates deterministic dynamic backends from the +canonical target. Axum applies the same manifest validation even though the native client does +not require provider host registration. + +## Limits And Accounting + +Each `OutboundRequest` owns independent limits: + +| Control | Scope | Default | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ------- | +| `max_request_body_bytes` | Buffered or streamed request bytes | 8 MiB | +| `max_encoded_response_bytes` | Upstream transport bytes before decoding | Unset | +| `max_decoded_response_bytes` | Identity or EdgeZero-decoded gzip/deflate/Brotli output | Unset | +| `max_response_bytes` | Final buffered response, including raw passthrough | 1 MiB | +| `max_response_header_bytes` | Adapter-visible upstream header name/value bytes before normalization, plus `x-edgezero-proxy` when enabled | Unset | +| `max_response_header_count` | Adapter-visible upstream header fields before normalization, plus `x-edgezero-proxy` when enabled | Unset | +| `max_brotli_window_bits` | Brotli stream header checked before decoder allocation | 24 | +| `max_decoder_bytes` | Pinned policy charge for Brotli/gzip/deflate decoder state | 32 MiB | +| `max_chunk_bytes` | Maximum emitted item size after decoding or passthrough | Unset | + +Request upload overflow is `EdgeError::PayloadTooLarge` with typed `OutboundUpload` origin; +inbound reader overflow carries `Inbound` instead. Buffered uploads over their cap reject +before provider dispatch on every adapter. Axum and Cloudflare prebuffer streamed uploads +within the cap before contacting the origin. Fastly and Spin may already have sent the head +and a prefix, but overflow aborts the upload rather than completing a truncated request. +An ordinary upstream 413 remains a response and may retain `Retry-After`. EdgeZero-generated +typed cap errors cannot carry that header after custom rendering or final egress preparation. +This is not a claim of hosted qualification; see the follow-up plan's remaining runtime legs. + +The encoded counter applies to every response path. The decoded counter applies to identity +and gzip/deflate/Brotli data decoded by EdgeZero, but not to unknown, stacked, parameterized, or other +raw passthrough encodings, including recognized codings delivered as Original or in a 206 response. +Identity keeps its decoded cap under both choices. The final buffered cap is independent of both. This separation lets +an application permit a larger raw body while keeping decoded expansion small. + +`max_chunk_bytes` is an item-shape guarantee, not a source-allocation limit: splitting a +`Bytes` value may retain its original allocation, and a provider may have already materialized +the source chunk. Header controls cover every adapter-visible upstream field before EdgeZero +normalization, including fields later stripped, and then charge EdgeZero's synthetic proxy marker +only when enabled. `HttpClient::with_proxy_marker(false)` selects a relay default; request +`OutboundProxyMarker::{Inherit, On, Off}` overrides it. The default remains on. Off neither adds +nor removes upstream marker fields and consumes no synthetic byte/count budget. The effective +inspector `client.effective_proxy_marker_enabled(&request)` is available before dispatch. +They do not cover opaque parser allocations, informational blocks that the SDK hides, or +provider-owned trailers. These exclusions are why +`outbound-complete-resource-accounting` is `Unsupported` on every current adapter. + +For a buffered batch, bound both the number of requests and every per-slot cap. Core-retained payload +is approximately the sum of buffered request bodies, configured final response caps, and one +current chunk per actively draining slot. Adapter staging and host/runtime copies are outside +that formula. + +## Encoding Boundaries + +An application-provided request body is sent with its declared `Content-Encoding`; EdgeZero +does not silently recompress it. `ResponseDecoding::Decode` is the default and decodes only a single +bare `gzip`, `deflate`, or `br` coding. `ResponseDecoding::Original` preserves encoded bytes without +allocating a decoder. All 206 responses retain original coding under either choice. Surviving +representation fields stay unchanged; ordinary normalization still removes nominated/non-text fields +first. Neither choice changes request `Accept-Encoding`, and identity retains decoded limits. +Multi-member gzip in Decode mode is drained through the final member and transport EOF. +Unknown codings are typed as `Passthrough(Unsupported(token))`, compound codings as +`Passthrough(Stacked)`, and malformed values as `Passthrough(Malformed)`; all remain encoded +and retain `Content-Encoding`. + +Cloudflare has two separate manual-encoding controls. The outbound fetch requests raw encoded +response bytes so EdgeZero can enforce transport and decode limits. When those bytes are later +passed through to the downstream response, the converter also selects manual response-body +encoding so Workers does not encode them again. The latter does not promise an exact streamed +wire `Content-Length`, because Workers owns final framing. + +Original-coding policy is wired through all four adapters. Local controlled-origin checks cover +gzip/deflate/Brotli on Axum, pinned Viceroy 0.21.0, Spin 4.1 and workerd 2026-09-10, in buffered and +streamed modes, including 206 and Decode controls. These are working-tree characterizations, not +hosted qualification. The [Cloudflare record](../audits/evidence/2026-10-20-cloudflare-outbound.json) +retains exact build tools, served artifacts and compatibility settings. The subsequent +[relay record](../audits/evidence/2026-10-20-outbound-relay.json) goes beyond JSON reports: 24 actual +downstream relay cases per local runtime preserve encoded bytes and surviving representation +fields through the standard converters. Decode controls verify plain output. Host-selected framing +is allowed; any observed Content-Length must match captured bytes. These finite local cases do not +qualify hosted behavior, exact deadlines, cancellation or universal wire framing. No capability is promoted. + +## Batch Timing + +`HttpClient::start_batch_until` returns an adapter-owned `OutboundBatch`. Calling `next()` yields +`OutboundBatchNext::Item` for each terminal slot at most once in observed completion order, paired +with its original input index, followed by `Finished(Completed)` or `Finished(Cutoff)`. Callers can +therefore retain completed results and cancel unresolved work at an application deadline. +`HttpClient::send_all_until` is the fallible ordered collector over that same driver; on success it +returns one `OutboundCollectedSlot` per input plus the typed termination reason. `Resolved` +owns a newly collected outcome, `AlreadyDelivered` records an item already taken by `check` or +`next`, and `Unresolved` records cutoff abandonment. `Completed` forbids `Unresolved`; `Cutoff` +requires at least one. Explicit +adapter failure, premature EOF, duplicate indices, and out-of-range indices return +`OutboundBatchFailure`: its precise batch-level error remains separate from cutoff, and its +index-aligned slots preserve sibling outcomes observed before the failure. +Normal completion requires clean driver EOF after the last item, not merely the expected item +count. Trailing driver failures and malformed events remain visible, including on empty batches. +`elapsed` starts at the single method-entry monotonic snapshot and ends when that slot becomes +terminal, including preflight validation, adapter setup, provider queueing, upload, headers, +buffered body drain, and any delayed guest observation. It is not pure transport RTT. +Preflight failures are timed from the same batch start, and a same-tick result may be zero. +A successful outcome is contractually terminal and on time. `elapsed` is metadata, not a second +lateness check; callers must not sample their clock after the whole batch returns to reinterpret an +earlier `Ok`. Producer contexts expose the captured start and clock; consumer receipts do not +expose an absolute terminal instant and cannot be reconstructed or mutated. +On Axum, Cloudflare, and Spin, terminal instants are sampled synchronously as child futures are +observed ready, so their samples follow poll-observation order in the shared monotonic clock +domain. Once an observation reaches the cutoff, a later observation cannot carry an earlier +on-time sample unless an injected clock violates its monotonic contract. Fastly retains its +documented BestEffort completion-order limitation. + +Standard adapter wiring clones the application's monotonic clock into its outbound client, so +ingress timing, dispatch budgets, slot elapsed values, error precedence, and deferred body +streams remain in one clock domain. The low-level `OutboundResponse` constructor requires that +clock explicitly, so third-party adapters cannot silently fall back to the process clock. A +backwards injected clock cannot enlarge the method-entry budget; backwards elapsed sampling fails +closed as an internal slot outcome with zero elapsed. + +Axum, Cloudflare, and Spin drive complete eligible exchanges concurrently. Fastly dispatches +eligible requests before selecting pending handles, but guest dispatch is sequential and body +drain remains observation-bound; a slot's elapsed value can therefore include sibling delay. +The `outbound-batch-slot-isolation` and `outbound-batch-completion-order` rows expose those +distinctions. + +Batch APIs accept buffered request bodies and buffered response mode only. Use `send` for a +streamed upload or response, and consume a streamed response body inside the same concurrent task +that issued it. + +## Missing Client + +Adapters inject `HttpClient` into request extensions. A handler obtains it with +`RequestContext::http_client()`. The accessor returns `None` when custom/manual wiring omitted +the client; handlers should return an explicit 501 or another application-selected fallback. +The generated project demonstrates the 501 behavior. diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 4aef7303f..07e9ac45f 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -62,6 +62,29 @@ Each item must be: - Either a unit struct or zero-argument constructor - Implementing `edgezero_core::middleware::Middleware` +## Capabilities Section + +Use `[capabilities]` to declare portable runtime behavior that the application requires or +explicitly treats as optional: + +```toml +[capabilities] +required = ["outbound-deadlines"] +optional = ["outbound-http"] + +[capabilities.outbound] +hosts = ["https://api.example.com", "https://*.example.net:*"] +``` + +Required capabilities accept `Native` or `BoundedCooperative` support and fail before adapter +execution for `BestEffort` or `Unsupported`. Optional capabilities log degradation and proceed. +Unknown names, duplicates, and required/optional overlap are validation errors. + +Outbound hosts configure adapter plumbing rather than application authorization. Omitting +`hosts` defaults to `https://*:*`; `hosts = ["*"]` is an explicit grant for both HTTP and HTTPS. +See [Capabilities](/guide/capabilities) for the exact grammar, support matrix, and platform +limitations. + ## HTTP Triggers The `[[triggers.http]]` array defines routes: @@ -75,6 +98,7 @@ handler = "my_app_core::handlers::root" [[triggers.http]] id = "echo" +class = "interactive" path = "/echo/{name}" methods = ["GET", "POST"] handler = "my_app_core::handlers::echo" @@ -91,6 +115,11 @@ body-mode = "buffered" | `adapters` | No | Intended adapter filter (metadata; `app!` currently ignores) | | `description` | No | Human-readable description for docs or tooling | | `body-mode` | No | `buffered` or `stream` | +| `class` | No | Opaque route class exposed to ingress admission policy | + +`class` is application-defined metadata. It is available on matched and +method-not-allowed route metadata, but it is not interpreted by EdgeZero and does not change the +route's stable method-plus-pattern identity. ::: tip Adapter filters The `adapters` field is currently metadata for tooling; `app!` wires all triggers regardless of adapter. @@ -224,6 +253,13 @@ handlers can call `ctx.config_store("app_config")` (or Treat config-store keys like API surface: validate or allowlist any user-controlled lookup before calling `ctx.config_store_default()?.get(...)`. +Raw `ConfigStore::get` and `get_bounded` return shared `ConfigValue` UTF-8 payloads, +not `String`. Cloning shares one value's allocation; borrow with `as_str()` or +`as_ref()`, or explicitly allocate with `to_string()` at an ownership boundary. +`Debug` output is redacted. Typed `AppConfig` fields are unchanged. Axum loads +[bounded immutable snapshots](/guide/adapters/axum#snapshot-allocation-limits) +before listener bind; push updated local config before startup or restart afterward. + ## Application config `edgezero.toml` describes the _shape_ of the app — routes, adapters, @@ -510,7 +546,15 @@ Logging can be configured per adapter under `[adapters..logging]` or via a | `level` | All | Log level: `trace`, `debug`, `info`, `warn`, `error`, `off` | | `echo_stdout` | Fastly, Axum | Mirror logs to stdout | -Note: Cloudflare logging is not wired to a built-in logger yet. +Manifest logging settings and runtime `EDGEZERO__LOGGING__*` overrides are separate; consult +the adapter guide for which source its runner consumes. Cloudflare and Spin do not install a +Rust `log` backend. `owns_logging = true` transfers installation **and filter configuration** +to the application, before entering the runner. The pure +`edgezero_core::resolve_logging_level(&EnvConfig)` helper resolves runtime levels without +implicitly applying manifest defaults or changing global state. + +Managed Fastly/Axum loggers retain `edgezero::boot` errors and warnings at `Warn` even when +ordinary runtime logging is `Off`. See [Logging Initialisation](/guide/adapters/overview#logging-initialisation). ## Full Example @@ -523,6 +567,12 @@ middleware = [ "my_app_core::middleware::Cors" ] +[capabilities] +optional = ["outbound-http"] + +[capabilities.outbound] +hosts = ["https://api.example.com"] + [[triggers.http]] id = "root" path = "/" @@ -640,16 +690,17 @@ The macro: - Generates route registration - Wires middleware from the manifest - Bakes portable store metadata (`Hooks::stores()`) from `[stores.kv]`, `[stores.config]`, and `[stores.secrets]` when present -- Creates the `App` struct that implements `Hooks` (use `App::build_app()`) +- Creates the application hook struct consumed by the fallible, core-owned + `edgezero_core::app::App::build::(platform)` constructor Arguments: -| Argument | Form | Meaning | -| -------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------- | -| manifest path | positional (required) | Path to `edgezero.toml`, resolved against the invoking crate's `CARGO_MANIFEST_DIR` | -| App identifier | positional (optional) | Name for the generated struct instead of `App`; must come immediately after the path, before any keyword argument | -| `owns_logging` | `owns_logging = ` | Sets `Hooks::owns_logging()`. Defaults to `false` | -| `state` | `state = ` | Emits `builder.with_state(expr)` so handlers can extract shared state | +| Argument | Form | Meaning | +| -------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------- | +| manifest path | positional (required) | Path to `edgezero.toml`, resolved against the invoking crate's `CARGO_MANIFEST_DIR` | +| App identifier | positional (optional) | Name for the generated struct instead of `App`; must come immediately after the path, before any keyword argument | +| `owns_logging` | `owns_logging = ` | Sets `Hooks::owns_logging()`. Defaults to `false` | +| `state` | `state = ` | Registers the expression's state value. Use `state = load_state()?` to propagate a typed failure before configuration | ```rust edgezero_core::app!( @@ -663,6 +714,11 @@ edgezero_core::app!( The `state` expression is emitted inside the generated `build_router()`, which runs once per request on the default Fastly, Cloudflare and Spin entry points, so keep it cheap. Build the heavy value once and hand out clones. +Both `build_router()` and the generated `Hooks::routes()` now return +`Result`. Pass a successfully built manual router to +`App::from_router::(router, platform)` to apply the same configuration +as hook-based assembly. `App::state::()` reads its existing registration during +configuration or from entrypoint code; it does not initialize state again. See [Sharing app state](/guide/handlers#sharing-app-state) for how handlers read `state`, and [Owning your own logging](/guide/adapters/fastly#owning-your-own-logging) diff --git a/docs/guide/dependency-majors-migration.md b/docs/guide/dependency-majors-migration.md index d5bcfc15b..ba4d95bbe 100644 --- a/docs/guide/dependency-majors-migration.md +++ b/docs/guide/dependency-majors-migration.md @@ -23,15 +23,20 @@ exists. Update these in your application's workspace `Cargo.toml` at the same time as the EdgeZero dependency: -| Crate | Old | New | Applies to | -| ------------ | ------ | ------ | --------------------------- | -| `fastly` | `0.12` | `0.13` | Fastly adapter | -| `log-fastly` | `0.12` | `0.13` | Fastly adapter | -| `spin-sdk` | `6` | `7` | Spin adapter | -| `validator` | `0.20` | `0.21` | Any typed app config | -| `rusqlite` | `0.32` | `0.40` | Only if you use it directly | - -Then refresh the lockfile: +| Crate | Old | New | Applies to | +| ------------ | ------ | ------------------------------------ | --------------------------- | +| `fastly` | `0.12` | `=0.13.1` | Fastly adapter | +| `log-fastly` | `0.12` | `=0.13.1` | Fastly adapter | +| `spin-sdk` | `6` | `=7.0.0` plus immutable Git revision | Spin adapter | +| `validator` | `0.20` | `0.21` | Any typed app config | +| `rusqlite` | `0.32` | `0.40` | Only if you use it directly | + +Keep the audited SDK requirements exact, including `worker = "=0.8.5"` +for Cloudflare applications. Spin applications that name the SDK directly must also copy +the Git URL and full revision from the matching EdgeZero pin, not mix registry and Git SDKs. +The [Spin dependency audit](../audits/2026-10-27-spin-sdk-eager-progress.md) records that source +and the upstream contribution. Migrate SDK spawn calls to `wasip3::spawn_local`; no compatibility +alias or second executor is provided. Then refresh the lockfile: ```bash cargo update diff --git a/docs/guide/handlers.md b/docs/guide/handlers.md index b2fc3dc75..410654054 100644 --- a/docs/guide/handlers.md +++ b/docs/guide/handlers.md @@ -202,17 +202,17 @@ async fn inspect(ctx: RequestContext) -> Result, EdgeError> { `RequestContext` provides these methods: -| Method | Returns | -| ---------------- | ------------------------------------------ | -| `request()` | `&Request` - full HTTP request | -| `path_params()` | `&PathParams` - raw path parameters | -| `path::()` | Deserialize path params to `T` | -| `query::()` | Deserialize query string to `T` | -| `json::()` | Deserialize JSON body to `T` | -| `form::()` | Deserialize form body to `T` | -| `body()` | `&Body` - raw request body | -| `into_request()` | `Request` - consume context, take request | -| `proxy_handle()` | `Option` - adapter proxy hook | +| Method | Returns | +| ---------------- | --------------------------------------------------- | +| `request()` | `&Request` - full HTTP request | +| `path_params()` | `&PathParams` - raw path parameters | +| `path::()` | Deserialize path params to `T` | +| `query::()` | Deserialize query string to `T` | +| `json::()` | Deserialize JSON body to `T` | +| `form::()` | Deserialize form body to `T` | +| `body()` | `&Body` - raw request body | +| `into_request()` | `Request` - consume context, take request | +| `http_client()` | `Option` - adapter outbound HTTP client | ### Store Extractors @@ -324,16 +324,231 @@ edgezero_core::app!("edgezero.toml", state = crate::app_state()); allowed per `app!` — to share more than one value, wrap them in an aggregate app-state struct and register that. +For fallible state, use `state = crate::load_state()?`, where the call returns +`Result`. The generated `build_router()` returns `Result`; `?` +propagates the original error before configuration and registers `T`, not `Result`. +Never turn a transient initialization error into a successful error-response router or cache it +in `OnceLock>`. Retain only successfully assembled state or applications. + > **Make `app_state()` cheap.** The macro emits the `state` expression inside the > generated `build_router()`, which each adapter's `run_app` calls through -> `A::build_app()` — **once at startup** for long-lived runtimes (Axum), but -> **once per request** on the default Fastly, Cloudflare and Spin entry points. -> Fastly starts a fresh Wasm instance by default; Cloudflare and Spin also -> rebuild the app per request. +> `App::build::(platform)` — **once at startup** for Axum, but **once per +> invocation** for the default Fastly, Cloudflare, and Spin entry points. +> Opt-in retained paths build only until an app is successfully retained; host +> eviction can require fresh initialization on any request. A reused Fastly +> sandbox does not imply that the app is retained unless the entry point chooses it. > So the `state` expression runs on that cadence: build heavy state **once** and > hand out clones (e.g. a `OnceLock>` as above, or a `static`), and > let `T = Arc` so each call is just a refcount bump. Do **not** `Arc::new(HeavyThing::build())` directly in the `state` expression. +### Configuring the application lifecycle + +Macro-driven apps can customize the generated `App` through the existing +`Hooks::configure` seam without replacing manifest-driven routing: + +```rust +use edgezero_core::{ + AdmissionDecision, DetachedResponseEgressDecision, EdgeError, IngressGrant, + ResponseEgressCompletion, DEFAULT_RESPONSE_WRITE_BUDGET, +}; +use std::time::Duration; + +fn configure_app(app: &mut edgezero_core::app::App) -> Result<(), EdgeError> { + app.set_ingress_admission_policy(|head| AdmissionDecision::Admit { + completion: ResponseEgressCompletion::empty(), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(30)), + }); + app.set_detached_response_egress_decision_factory(|head| { + DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::empty(), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), + } + }); + Ok(()) +} + +edgezero_core::app!("edgezero.toml", configure = crate::configure_app); +``` + +`configure = ` must evaluate to a callable that accepts `&mut App` and returns +`Result<(), EdgeError>`. It is +invoked after the manifest router is built and before the app begins serving, so +it can install ingress admission, request-head limits, config extraction limits, +the monotonic clock, an error-response renderer, and response-egress policy or observation hooks. Keep the +callback cheap for the same adapter-lifecycle reasons as `state = ` above. A configuration +error prevents EdgeZero request conversion, body polling, and dispatch; Axum also fails before +binding its listener. + +`App::build::(PLATFORM) -> Result` is the hook-based assembly +path. `Hooks::routes() -> Result` builds routes and their +state first; its original error skips configuration and produces no app. The +macro-generated `build_router()` also returns `Result`, without an infallible +compatibility helper. + +For state loaded from provider bindings, asynchronous reads or explicit settings, +build the router yourself, then call `App::from_router::(router, PLATFORM)`. +It does not call `A::routes`: it applies the same application name, configuration +and complete platform-metadata check as `App::build`, exactly once. Raw constructors +such as `App::new` do not apply hooks and are not substitutes for this assembly path. + +`App::state::() -> Option<&T>` borrows the exact registered type from the +router's existing state registry, without a request, copying the value or rebuilding +it. Configuration may read state before choosing policies. For `with_state(Arc)`, +read `state::>()`; handlers' `State>` shares those same interiors. +Missing types return `None`, and repeated registration of one type remains +last-write-wins. `RouterService::state` exposes the same borrowed view. + +Both assembly paths install the selected platform before calling `Hooks::configure` +and reject a successful configuration that replaces that metadata. Publish or retain +only the completed successful app, never an initialization error. Retaining the app +does not retain request admission grants, registries, bodies, or response-egress +resources; those are fresh for every dispatch. See +[application retention](/guide/adapters/overview#opt-in-application-retention). + +All standard Fastly, Cloudflare and Spin entrypoints deliver route/state or configuration +failures through the same default error renderer and canonical adapter response converter. +The error's status is preserved; internal details remain off the wire. Full failure details +are logged once per failed attempt on `edgezero::boot`, so install the application's logging +backend before assembly when the adapter does not provide one. Axum logs its failed assembly +once on the same boot target and fails startup before binding, without constructing a response. + +No successful app configuration exists on failure, even if `configure` installed some policies +before returning an error. The failure response therefore uses no configured renderer, +application headers, completion factory, egress policy or observer. It uses the default finite +write budget anchored to the request start captured before assembly. This deadline does not +interrupt synchronous initialization itself. Fastly retained serving keeps its app slot empty +and retries on the next request when failure-response delivery succeeds; transport failures +retain their existing terminal behavior. + +Custom entrypoints can construct the same envelope without an app using +`App::assembly_error_egress(error, method, request_start, clock)`. Use the adapter's +`request::send_assembly_error` helper to release the unread native request and enter its +existing converter once. Fastly returns `FastlyRequestDelivery::Detached` with the exact +terminal report; Cloudflare and Spin return their adapter-owned response. The supplied +start and clock must share one monotonic domain. Never send a second native response after +unknown or committed delivery. Fastly's confirmed `NotCommitted` replacement permission +remains an explicit caller decision outside EdgeZero accounting, never an automatic resend. + +Use the synchronous error renderer when framework and handler errors must use an application-owned, +fixed-size wire body: + +```rust +use edgezero_core::{Body, EdgeError, Response}; + +fn configure_app(app: &mut edgezero_core::app::App) -> Result<(), EdgeError> { + app.set_error_response_renderer(|error| Response::new(Body::from(error.kind()))); + Ok(()) +} +``` + +The renderer handles admitted routing/handler errors, post-admission conversion errors, and +detached normalized-ingress errors. EdgeZero reapplies mandatory protocol metadata after the +callback, including the error's status, so the status returned by the callback is ignored; +canonical 405 responses therefore retain their sorted, deduplicated `Allow` field. +Effective `config_out_of_date` responses retain `Retry-After: 60`. +Typed request-body overflow responses always retain status 413 and lose `Retry-After`, even +when a custom renderer adds it. This does not remove an upstream 413 response's retry policy. +Explicit admission refusals and fallback `on_exceeded`/`on_timeout` responses are already complete +application responses and bypass this renderer. Avoid serializing `error.to_string()` when the +body must remain bounded or diagnostics must remain private. + +Every response-producing admission decision owns one explicit `ResponseEgressCompletion`. Use +`ResponseEgressCompletion::empty()` when there is no response-scoped resource. To hold a permit +acquired during admission through terminal egress, move it directly into +`ResponseEgressCompletion::new(move |report| ... )`. The completion is non-clone. +It never belongs in response extensions. It follows admitted, refused, fallback, handler-error, +and middleware-error responses into the adapter's exactly-once egress attempt. +`AdmissionDecision::Abort` is different: it returns no response and therefore owns no completion. + +A handler can place a response-specific write bound in the response extensions without teaching +the global policy callback about application response categories: + +```rust +use edgezero_core::ResponseEgressDeadline; +use std::time::Duration; + +response + .extensions_mut() + .insert(ResponseEgressDeadline::after(Duration::from_secs(2))); +``` + +`after(duration)` starts at the injected-clock instant when response egress begins. EdgeZero +clamps the duration to seven days and fails closed to that start instant on arithmetic overflow. +Use `ResponseEgressDeadline::at(deadline)` when the application already owns an absolute cutoff, +such as a queue-through-egress deadline. EdgeZero removes either extension in `begin()`, exposes +the resolved absolute value to the policy callback, and enforces the earlier application or policy +deadline. The retired `new()` and unresolved `deadline()` APIs are not available. + +Use `left.join(right)` when two independently owned completion actions must follow the same +response. The joined completion invokes the left callback and then the right callback with the +same borrowed terminal report, at most once. Abandoning it before terminal egress releases the +resources captured by both children. On unwind-capable targets, each child has an independent +panic boundary, so a left-callback panic does not suppress the right callback. On panic-abort +targets, termination may occur before the right callback runs. Joining callbacks does not +strengthen the adapter's documented provider-completion boundary. + +When the handler acquires the permit later, use EdgeZero's late-bound resource owner. Put the +non-clone installer in the typed `IngressGrant` and transfer its paired completion with the +admission decision. The handler consumes the grant and installs the permit; terminal completion +takes and drops it: + +```rust +use edgezero_core::{ + IngressGrant, ResponseEgressCompletion, ResponseEgressResource, + ResponseEgressResourceInstallError, +}; + +struct Permit; + +struct AdmissionLease { + response_resource: ResponseEgressResource, +} + +impl AdmissionLease { + fn install(&self, permit: Permit) -> Result<(), ResponseEgressResourceInstallError> { + self.response_resource.install(permit) + } +} + +fn response_lifecycle() -> (IngressGrant, ResponseEgressCompletion) { + let (response_resource, completion) = ResponseEgressCompletion::late_bound(); + ( + IngressGrant::new(AdmissionLease { response_resource }), + completion, + ) +} +``` + +`install` consumes the permit. A second install returns `AlreadyInstalled`; an install after +terminal completion or pre-egress abandonment returns `Closed`. Both failures release the rejected +permit before returning. `ResponseEgressResourceInstallError` implements `std::error::Error`, so a +handler returning `Result<_, EdgeError>` can use +`lease.install(permit).map_err(EdgeError::internal)?`. Do not keep the permit only in a +handler-local guard: that releases it when the handler returns, before response transmission. If +the response envelope is abandoned before egress begins, dropping the completion closes the +surviving installer and releases an installed permit without fabricating a terminal report. + +Normalized request-head rejections happen before admission; admission-policy errors can also fail +before an admission decision transfers its completion. Configure +`App::set_detached_response_egress_decision_factory` to return +`DetachedResponseEgressDecision::Send { completion, deadline }` when those responses need an +application-owned completion resource. Every `Send` deadline is mandatory and absolute in the +request's injected monotonic-clock domain. Construct relative deadlines with +`head.write_deadline_after(duration)`, which anchors to captured `request_start`, clamps to +`DEADLINE_FAR_FUTURE`, and fails closed to `request_start` on arithmetic overflow. The default +factory combines `ResponseEgressCompletion::empty()` with `DEFAULT_RESPONSE_WRITE_BUDGET` measured +from request start. The effective bound is the earlier of the factory deadline and the default +response-egress policy, so a later factory deadline cannot extend the default policy and the policy +cannot extend an earlier factory deadline. Admission-selected detached responses retain their +response-owned deadline path and are outside this factory contract. + +Return `DetachedResponseEgressDecision::Abort` when no bounded response resource is available. +`Abort` creates no response or egress attempt and invokes no completion callback, observer, +handler, middleware, or body poll. Raw parser failures that occur before EdgeZero receives a +request, and platform-to-core head-conversion failures that occur before a normalized head exists, +remain outside this hook. + ## Response Types ### Text Responses @@ -425,12 +640,22 @@ async fn update_user( Extractors return `EdgeError` on failure, which automatically converts to appropriate HTTP responses: -| Error | Status Code | -| --------------------- | ------------------------ | -| JSON parse error | 400 Bad Request | -| Validation error | 422 Unprocessable Entity | -| Missing path param | 400 Bad Request | -| Type conversion error | 400 Bad Request | +| Error | Status Code | +| ---------------------- | ------------------------ | +| JSON parse error | 400 Bad Request | +| Body byte-cap overflow | 413 Payload Too Large | +| Inbound read deadline | 408 Request Timeout | +| Validation error | 422 Unprocessable Entity | +| Missing path param | 400 Bad Request | +| Type conversion error | 400 Bad Request | + +`body_bytes`, `json_within`, `form_within`, and the body extractors return +`EdgeError::PayloadTooLarge` for overflow; malformed JSON or form input remains 400. +`request_body_overflow_origin()` distinguishes `RequestBodyOverflowOrigin::Inbound` from +`OutboundUpload` without parsing messages. The origin is not serialized. An oversized single +normalized `Content-Length` rejects without reading, unless the read deadline has already +expired (408). This early rejection and a stricter cached cap remain reusable at a larger cap; +overflow while consuming a stream is sticky and never retries the source. For custom error handling, return `Result`: @@ -464,6 +689,9 @@ EdgeError::internal("Unexpected failure") // 500 EdgeError::internal(some_error) // 500 (from any error type) ``` +`method_not_allowed` sorts and deduplicates the structured method list. Its response includes the +required `Allow` header; this remains true when an application error renderer supplies the body. + ## Custom Extractors Implement the `FromRequest` trait to create custom extractors: diff --git a/docs/guide/kv.md b/docs/guide/kv.md index e5627b2bb..95608bb1b 100644 --- a/docs/guide/kv.md +++ b/docs/guide/kv.md @@ -109,6 +109,30 @@ For strict correctness, use a transactional data store. Key listing is paginated by design. This avoids buffering an unbounded number of keys in memory and matches the underlying provider APIs. The Spin adapter materialises `Store::get_keys()` and pages client-side; a `max_list_keys` cap (configurable via `EDGEZERO__STORES__KV____MAX_LIST_KEYS`, default `1000`) guards against runaway lists and yields `KvError::LimitExceeded` when exceeded. +## Declared Store Failures + +Fastly, Cloudflare and Spin install every declared logical KV ID without opening its +provider binding. Named/default lookups remain present even if the provider store is +missing; an undeclared ID remains absent. KV-free routes do not open these stores. + +The first operation opens the binding and retains its result only for that request. +Concurrent Spin operations share initialization, but do not hold the initialization lock +during the operation. Cancelling a pending open leaves initialization retryable. A new +request always gets fresh initialization, so provider availability is never cached globally. + +Opening failures return `KvError::OpenFailed { reason: KvStoreOpenFailure }` from +`edgezero_core::key_value_store`. `MissingBinding` and `InvalidConfiguration` become +HTTP 500; `Unavailable` becomes 503. Reasons retain no provider payload or resolved +binding name. Spin maps a disallowed label to invalid configuration, not missing data. +Its TTL operation still returns `Unsupported` after a successful open, without writing. + +Warnings use `edgezero::boot` and contain only the compiled logical ID and fixed reason. +Each ID warns at most once per sandbox; the deduplication set is bounded by compiled +declarations and never evicts IDs or retains handles/errors. Explicit low-level eager +opening remains fallible. Fastly/Cloudflare `require_kv()` still fails before dispatch; +Axum's startup-time store opening is unchanged. This is not a KV allocation or deadline +guarantee; config-store extraction has its own independent bounds. + ## Operation Timing / Observability `KvHandle` emits debug-level timing logs for backend KV operations across all adapters. Logs include safe metadata such as operation name, elapsed milliseconds, success/error status, key or prefix length, hit/miss, byte counts, TTL seconds, and list page counts. diff --git a/docs/guide/middleware.md b/docs/guide/middleware.md index efefe3d3b..8ec84e2dd 100644 --- a/docs/guide/middleware.md +++ b/docs/guide/middleware.md @@ -231,4 +231,4 @@ If you already hold an `Arc` (`BoxMiddleware`), register it with ## Next Steps - Learn about [Streaming](/guide/streaming) for progressive responses -- Explore [Proxying](/guide/proxying) for upstream forwarding +- Explore [Outbound HTTP](/guide/proxying) for upstream requests diff --git a/docs/guide/proxying.md b/docs/guide/proxying.md index 3dd8cafc3..931637854 100644 --- a/docs/guide/proxying.md +++ b/docs/guide/proxying.md @@ -1,63 +1,660 @@ -# Proxying +# Outbound HTTP -EdgeZero provides helpers for forwarding requests to upstream services while staying -provider-agnostic. +EdgeZero exposes one provider-neutral HTTP client to handlers. Adapters inject an `HttpClient` +into request extensions, while application code builds `OutboundRequest` values and receives +typed `OutboundResponse` or `EdgeError` outcomes. -## End-to-End Example +## Hard-Cut Adoption -This example forwards the incoming request upstream, adjusts headers on the way in and out, and -returns a friendly 502 on proxy errors. It uses the adapter-provided proxy handle inserted by each -adapter. +Replace an application's transport layer rather than wrapping it around EdgeZero or keeping +raw-SDK fallback clients. EdgeZero owns transport dispatch, backend registration, response +normalization, transport decoding, bounded buffering and adapter delivery. Applications retain +target authorization, signing, image-profile selection, content rewriting and scheduling policy. +An application-owned rewrite codec is not a second outbound transport. + +The current client already provides indexed batch outcomes, partial-result preservation on +driver failure, observation cutoff and cancellation, per-slot elapsed time, typed errors and +independent response limits. These contracts remain requirements of consolidation, together +with encoding policy, original-header provenance and ingress/egress ownership. Simplifying +internals does not remove them or upgrade the published target capabilities. + +The head-first client contract replaces the old implementer `send` and raw-vector batch APIs; +there are no compatibility wrappers. Core captures invocation time, validates support before body +polling, prepares requests and finalizes provider heads. Adapters retain I/O, scheduling and +transport ownership. Eager work and nonblocking batch observation use that same pipeline. +Explicit redirect following uses that same pipeline with an application-supplied target policy. +Hosted qualification remains separate; applications must gate adoption on the behavior and +target evidence they require, not maintain another SDK client or supply lazy futures as eager work. + +Header-policy consolidation is active: all four adapters pass `OutboundHeaderInput` to the shared +`OutboundHead` constructor. Adapter authors migrate the retired separate header +helpers as described in [response provenance](./capabilities.md#outbound-content-type-provenance). +Ordinary handler, demo and template request APIs are unchanged by that consolidation. + +The facade calls synchronous `validate_supported_options` before common method/body/policy +validation, budget rejection, timer creation, source polling or provider I/O. Implementers receive +opaque prepared contexts; delete local preparation and budget selection. Buffered Batch +restrictions remain. This does not change indexed results, partial failures, observation cutoff, +per-slot timing, independent caps, coding/provenance, application clocks or lifecycle ownership. + +### Paired Clocks And Timers + +Standard entrypoints pair the application's monotonic clock with their runtime's cancellable +timer and inject that client. Demo and generated apps use this path unchanged. Custom client +construction now takes `OutboundTime`: Axum uses `try_with_time`, while Cloudflare, Fastly and +Spin use `with_time`. The old clock-only constructors are removed, not deprecated aliases. + +`OutboundTime::new(clock, factory)` requires a factory that returns an owned local wakeup future +for the supplied absolute deadline and clock domain. A fake clock must wake its pending timers +when advanced. Merely returning a permanently pending future or advancing time without waking +does not implement that contract. Early successful wakeups are rearmed against the original +deadline; operational timer errors propagate unchanged and do not become request timeouts. + +Timers are checked before source polling, retained while pending and cancelled with their +exchange. Native transport total timeouts do not override the supplied pair. Fastly uses its +existing progress multiplexer for timer-only waits as well as transport readiness. Provider +phase timers, synchronous host calls and the published BestEffort limitations remain separate; +this integration is not a hosted capability promotion. + +### Operation Ownership + +All four adapters use one core scope implementation. Canonical entrypoints establish a callback +owner before application preparation and dispatch. Send futures, begun exchanges and batches +capture that owner at invocation, including never-polled futures. Returning, failing or supported +unwinding revokes complete live work even when an application retains its public handle. Later +observation reports typed Internal scope closure, not a timeout, cutoff or clean EOF. Already +recorded batch partials retain their existing failure and accumulation semantics. + +Canonical send helpers reuse an outer callback owner. Without one, they establish an owner +through preparation and delivery and release it when the helper returns. Batches started while +polling a response body have a separate owner released when that transmission ends, including +source/write failure. Earlier callback work remains live, and post-delivery work uses the same +still-active callback. Retained clients resolve ownership on each invocation; they do not +retain the previous request's scope. Polling a callback-created batch inside a body does not +transfer its ownership. + +Fastly's callback progress driver services scheduled Begin and Batch pipelines, including head +processing and bounded collection, while a handler or response source awaits other work. Captured +scope and runtime activation are installed and restored per poll, never held across `.await`. +The old private Fastly lifetime registry is removed, not used alongside core ownership. + +Adapter implementers that spawn local work use the core `OutboundTask::new` runner/owner pair. +Registration is fallible before spawning. Dropping either side releases the producer even if +the other handle is retained; adapters supply only their provider spawn and abort operations. +Completion receivers observe already-scheduled work. `is_ready` and batch `check` do not drive +the SDK, create timers or read transport bodies. + +Successful streamed collection transfers transport ownership into the outbound body's lifetime; +callback exit cannot abort that handed-off body. Downstream response egress has its own body scope +through terminal transmission or Drop. EOF releases its source but does not end that scope. Its +terminal transition releases response-owned SDK work before invoking completion callbacks, without +cancelling work owned by a still-active serving callback. + +To start work during the handler that must continue after its response is returned, choose its +body ownership **at creation**, then attach that exact owner to the returned response: + +```rust +let body_owner = OutboundScopeOwner::response_body(); +let batch = body_owner.with(|| client.start_batch_until(requests, observation_cutoff)); +let origin = client.send(origin_request).await?; +let response = response_with_batch_stream(origin, batch)?; // Application body production. +body_owner.attach(response) +``` + +The owner is provisional until canonical egress accepts the response. Retaining an unattached +owner or an attached-but-unaccepted response cannot bypass callback cleanup. Acceptance happens +before response policy, framing, suppression or body polling. Callback and body lifetimes share +provider progress, but not cancellation roots; a body batch started before the origin remains +serviceable while the origin is awaited and after callback return. A failed application's body +work closes before bounded fallback delivery. No deadlines are restarted during handoff. + +`Body` now has a private payload and optional local ownership metadata. Use `Body::from_bytes` +or `Body::from_stream`, not the removed enum constructors. Transport code can destructure +`BodyPayload` through `Body::into_parts`, retaining its returned owner until terminal egress. +This changes neither buffered representation nor byte copies. Outbound requests cannot reuse a +body with response ownership. Ordinary handlers need no manual owner; the explicit attachment +above is for work deliberately started early on behalf of a later body. + +Manual clients outside request entrypoints remain caller-owned: dropping the operation, pending +handle or batch cancels its work. A manual caller requiring a group lifetime can use +an `OutboundScopeOwner::callback()` binding and keep it alive while awaiting `owner.run(...)`. +Normal handlers and generated apps need no scope +boilerplate. A closed canonical scope never falls back to caller-owned work. Existing deadlines, +caps, source errors and provider-side cancellation guarantees are unchanged; hosted qualification +remains separate. + +## Following Redirects + +`HttpClient::follow` is an explicit per-call choice with a mandatory target policy and hop limit. +`OutboundRedirectLimit::new` accepts only 1..=20 and has no default. At most limit plus one +requests are sent; for example, limit 4 allows five requests. Plain `send`, `begin`, and batches +never follow redirects. This section supersedes the historical outbound spec's instruction to +read `Location` and issue another request when using `follow`; plain sends retain that rule. + +```rust +use edgezero_core::{OutboundRedirectAction, OutboundRedirectLimit, OutboundRequest}; + +enum TargetPolicyError { + TargetDenied, +} + +// The initial target is application-authorized before this call. +let request = OutboundRequest::get("https://assets.example.com/start")?; +let result = client.follow(request, OutboundRedirectLimit::new(4)?, |hop| { + let Some(next) = hop.prepared_request() else { + return Ok(OutboundRedirectAction::Stop); + }; + if next.uri().scheme_str() != Some("https") + || next.uri().host() != Some("assets.example.com") + || next.uri().port_u16().is_some_and(|port| port != 443) + { + return Err(TargetPolicyError::TargetDenied); + } + Ok(OutboundRedirectAction::FollowPrepared) +}).await; +``` + +The initial target is not passed to the redirect policy: authorize it before calling `follow`. +The policy is the only target authorization policy. An open policy also follows private or +link-local addresses. There is no built-in SSRF filter; select schemes, hosts, ports and any +application-specific network restrictions deliberately. A platform host declaration is not a +substitute for application authorization. + +### Methods And Replay + +Exactly 301, 302, 303, 307 and 308 are followable. Every other status is final, including 300, +304, 305 and 306. Rules apply to the request that received the answering response: + +| Status | Method of answering hop | Next method | Next body | +| -------- | ----------------------- | --------------------------------- | --------- | +| 301, 302 | GET, HEAD | same | empty | +| 301, 302 | POST | GET | empty | +| 301, 302 | any other method | not followed: MethodNotRewritable | none | +| 303 | HEAD | HEAD | empty | +| 303 | any other method | GET | empty | +| 307, 308 | any method | same | replayed | + +For example, PUT, PATCH, DELETE and OPTIONS stop on 301/302, become empty GET on 303, and retain +their method and replayable body on 307/308. Buffered bodies replay byte for byte through cheap +`Bytes` sharing. Streamed uploads cannot be replayed: a 307/308 is final with `BodyNotReplayable`, +without consulting policy. An unknown inbound GET/HEAD body is settled once before the initial +hop; EOF makes it replayable as empty, while a nonempty byte is rejected before dispatch. + +Dropping a body removes `Content-Type`, `Content-Length`, `Content-Encoding`, `Content-Language`, +`Content-Location`, and `Expect` from the prepared request. They stay absent on a subsequent +307/308 unless the trusted policy deliberately supplies replacement content and metadata. +An empty buffered body replayed by 307/308 retains its `Content-Type`; replay is not a rewrite. +Ordinary validation still prohibits nonempty GET/HEAD bodies. + +### Targets And Credentials + +The decision order is followable status, Location presence, method, replayability, limit, +Location resolution, then policy. A missing or single empty Location yields `NoLocation`. +Original Location multiplicity and text eligibility are captured before filtering; duplicate or +non-text values fail resolution as typed 502 Protocol errors rather than becoming a valid +singleton after filtering. Provider-hidden original fields remain unavailable. Relative paths, +`../next` and `//other.example/next` resolve against the answering request URI, and fragments +never reach the wire. Invalid URL references, user information and unusable HTTP(S) targets are +Protocol errors. Earlier method/replay/limit stop reasons take precedence over resolution errors. + +A resolved non-HTTP target reaches the policy with no prepared request. The policy may `Stop`, +reject, or `Follow(request)` with its own valid target. Asking for `FollowPrepared` without an +available prepared request, including after `take_prepared_request()`, is a Protocol error. + +When the target changes scheme, host, or effective port, the automatic credential rule removes +only `Authorization`, `Cookie`, and `host_authority_override` before calling the policy. These +carry over on same-origin hops. Other credential-bearing fields, including custom API-key headers +and `x-amz-*` signatures remain: the policy must remove or replace them when necessary. It may +deliberately authorize or re-sign a replacement request; EdgeZero does not strip credentials a +second time afterward. Ordinary request validation and normalization still apply. EdgeZero does +not add `Referer` or `Origin`. + +The prepared request retains every unchanged option: timeout, deadline, cache policy and tags, +upload/response/header/decoder/chunk limits, response decoding, proxy marker, Image Optimizer, +TLS verification, connect timeout and stream idle policy. Use `take_prepared_request()` to modify +it, then return `Follow(request)`. A fresh request is a complete replacement with its own defaults; +caps and options are not implicitly copied into it. Subsequent automatic preparation inherits +the effective preceding request's options, not the initial defaults. + +### Budgets, Bodies And Outcomes + +The per-call timeout starts afresh at each hop, while the initial absolute deadline remains an +independent chain-wide upper bound, including policy execution and final streamed body consumption. +Initial GET/HEAD settlement counts against that bound too. A replacement request cannot remove +or extend that original deadline; an earlier policy deadline may shorten a hop. For a selected +next request, unsupported options are rejected before an expired budget. No expired hop is sent +and no late final success +is returned. A synchronous policy cannot be preempted; its rejection value remains unchanged even +if time passes during the callback. An accepted action remains subject to the original deadline. + +Every hop uses the same supplied client and shared preparation/head-finalization pipeline, +including custom clients. A followed response body is discarded unread: it is not decoded, +buffered, delivered or drained for reuse, and its declared length or source failure cannot fail +the chain. Response-header caps and body-independent validation still apply to every head. +The final response, including stopped or limit-reached 3xx, uses ordinary decoding, caps, body +mode and idle policy. Thus a final body may still fail; a stop reason never bypasses body policy. +With no redirect, `follow` has the same response and limit behavior as `send`. + +`OutboundFollowResult` exposes `response()`, `final_uri()`, `redirects_followed()` and +`stop_reason()`, or transfers them through `into_parts()`. The stop reason is present exactly +for a final followable status: `NoLocation`, `MethodNotRewritable`, `BodyNotReplayable`, +`LimitReached`, or `StoppedByPolicy`. + +`OutboundFollowFailure::Hop` preserves the hop's original `EdgeError` with its zero-based hop +index (initial request = 0). `OutboundFollowFailure::Policy` carries the application's rejection +with its one-based redirect number, unchanged. Neither the policy nor its error needs `Send` or +`Clone`, and no `Sync`, `Debug` or standard error trait is required. The policy is synchronous +`FnMut`; the future owns it. Dropping the future cancels the current hop as a plain send would +and sends no later hop. Framework hop logs include status, scheme, host, port and path, not queries. + +Spin requires plain-HTTP origins and targets outside a narrowed host list to be declared. Fastly +hops wait one at a time and use the same backend identity policy as equivalent single sends. +On Fastly, supported optimizer hops use the same native Image Optimizer call path, with no extra +fetch, buffering or serialization. This does not strengthen its unknown blocking horizon, +deadline/cancellation guarantees or hosted performance qualification. The `Native` redirect +capability describes shared policy only; see the [capability matrix](./capabilities.md#outbound-matrix). + +## Native Image Optimization + +Fastly single sends can request native image optimization from portable code: + +```rust +use edgezero_core::{OutboundImageFormat, OutboundImageOptimizerOptions, + OutboundImageOptimizerRegion, OutboundRequest}; + +let options = OutboundImageOptimizerOptions::new() + .region(OutboundImageOptimizerRegion::UsEast) + .format(OutboundImageFormat::WebP) + .width(100)?; +let request = OutboundRequest::get("https://images.example.com/photo.jpg")? + .image_optimizer(options) + .max_encoded_response_bytes(2 * 1024 * 1024) + .max_decoded_response_bytes(4 * 1024 * 1024) + .max_response_header_bytes(64 * 1024) + .max_response_header_count(100) + .stream_response() + .timeout(std::time::Duration::from_secs(2)); +assert!(request.image_optimizer_options().is_some()); +// Send through the existing request-context HttpClient. +``` + +Only bodiless GET/HEAD and `PlatformDefault` caching are supported. Do not combine this with +cache bypass. Region is required; no default region or transformation is invented. Other +adapters and Fastly begun requests and batch slots reject `image_optimizer` before body polling +or dispatch. Explicit redirect following uses the same supported single-send mapping per hop. +Viceroy and services without the feature return typed `BadGatewayReason::ImageOptimizerUnavailable` +(502), retaining text-free provider detail. There is no software fallback or automatic extra fetch. + +The SDK's optimizer call is synchronous and cannot be cancelled while executing. Its complete +blocking horizon is **unknown**: a timeout does not promise prompt return from that call. When +control returns, the original absolute deadline wins, including over a late successful transform. +Normal header/body limits, content-encoding policy and lazy streamed delivery apply to the result. +Hosted transformation and performance parity remain unqualified. + +EdgeZero retains the built target and signature-bearing headers subject to normal filtering. +The provider separately interprets optimizer query parameters, with explicit options taking +precedence. `preserve_origin_query(true)` preserves only other query parameters at the origin; +it does not stop that transformation. Query stripping before signing, SVG selection, profiles, +authorization and any preflight requests remain application policy. Forwarded inbound GET/HEAD +settlement and explicit redirect following use the same shared dispatch pipeline; neither adds +an optimizer fallback or another provider fetch. + +## Forward An Inbound Request + +This example preserves the inbound method and body, replaces the target URI, removes a private +header, and converts the outbound response back into a core response. + +On Axum, the converter preserves a transport-certified empty body (`is_end_stream()`) as buffered +empty after admission, for any method, without reading it. Actual HTTP/1 GET/HEAD forwards with +absent framing or `Content-Length: 0` succeed through this path even after the inbound read +deadline, with no outbound body bytes, `Transfer-Encoding` or `Content-Length`. + +Unknown bodies remain lazy until `send` or `begin` settles a GET/HEAD body. Empty chunks do not +establish emptiness: only EOF does. The first nonempty byte returns 400 before origin contact. +Settlement races the captured outbound deadline against the admitted inbound read deadline, +retaining 408/504 provenance and original source errors without restarting the outbound budget. +Batch still requires buffered uploads; buffer under an application cap before adding a slot. +Do not discard an inbound body to bypass validation. Other methods retain streamed uploads. + +Fastly separately preserves actual SDK body absence: only `try_take_body() == None` becomes core +`Body::empty()`, without creating an SDK body. Both direct conversion and admitted dispatch keep +every present SDK body lazy, even when `known_length()` reports zero; that query omits SDK buffers +and is not an EOF signal. Normal Fastly `from_client()` requests carry a body handle even when +bodiless, so their emptiness is settled rather than guessed. Fastly's synchronous inbound host +read is not preemptible; its read-deadline behavior remains BestEffort. Do not remove a present SDK +body to force the absence path. See [Fastly ingress conversion](./adapters/fastly.md#ingress-body-conversion). + +Cloudflare preserves a native Fetch request's `body == null` as buffered empty without reading +it. This applies to direct conversion and, after admission, normal dispatch. Every non-null body +remains streamed, even an empty ArrayBuffer, a closed stream or a body with `Content-Length: 0`. +There is no ArrayBuffer-only fallback or header-based inference. See +[Cloudflare ingress conversion](./adapters/cloudflare.md#ingress-body-conversion). + +The certified Axum empty body or absent Fastly/Cloudflare body remains Initial, not Cached. `take_body` +and `into_request` expose buffered empty, but core's first `body_bytes` call still checks the +inbound read deadline and can return 408. Ingress framing remains `HostManaged`. Demo and generated +proxy handlers share their adapter's converter and the common settlement pipeline. Neither +settlement nor normalized requests establish raw parser guarantees or promote target capabilities. ```rust use edgezero_core::action; -use edgezero_core::body::Body; use edgezero_core::context::RequestContext; use edgezero_core::error::EdgeError; -use edgezero_core::http::{Response, StatusCode, Uri}; -use edgezero_core::proxy::ProxyRequest; +use edgezero_core::http::{HeaderValue, Response, Uri}; +use edgezero_core::outbound::{OutboundCachePolicy, OutboundRequest, ResponseDecoding}; +use std::num::NonZeroU64; +use std::time::Duration; #[action] -async fn proxy_with_auth(RequestContext(ctx): RequestContext) -> Result { - let target: Uri = "https://api.example.com".parse().unwrap(); +async fn forward_with_auth( + RequestContext(ctx): RequestContext, +) -> Result { + let client = ctx + .http_client() + .ok_or_else(|| EdgeError::not_implemented("outbound HTTP client not available"))? + .with_proxy_marker(false); + let target: Uri = "https://api.example.com/v1/data" + .parse() + .map_err(|_| EdgeError::bad_request("invalid upstream URI"))?; - let handle = ctx - .proxy_handle() - .ok_or_else(|| EdgeError::internal("proxy client not configured"))?; - - let mut proxy_request = ProxyRequest::from_request(ctx.into_request(), target); - proxy_request.headers_mut().insert( + let mut request = OutboundRequest::from_request(ctx.into_request()?, target)? + .response_decoding(ResponseDecoding::Original) + .cache_policy(OutboundCachePolicy::Bypass) + .timeout(Duration::from_secs(2)) + .connect_timeout(Duration::from_secs(1)) + .max_request_body_bytes(256 * 1024) + .max_encoded_response_bytes(2 * 1024 * 1024) + .max_decoded_response_bytes(4 * 1024 * 1024) + .max_response_bytes(4 * 1024 * 1024) + .max_response_header_bytes(64 * 1024) + .max_response_header_count(100) + .max_chunk_bytes(NonZeroU64::new(64 * 1024).unwrap_or(NonZeroU64::MIN)); + request.headers_mut().insert( "authorization", - "Bearer secret-token".parse().unwrap(), + HeaderValue::from_static("Bearer secret-token"), ); - proxy_request.headers_mut().remove("cookie"); - - match handle.forward(proxy_request).await { - Ok(mut response) => { - response - .headers_mut() - .insert("x-proxy-by", "edgezero".parse().unwrap()); - Ok(response) + request.headers_mut().remove("cookie"); + + let mut response = client.send(request).await?; + response + .headers_mut() + .insert("x-forwarded-by", HeaderValue::from_static("edgezero")); + response.into_response() +} +``` + +Outbound failures remain typed. Deadline expiry maps to `GatewayTimeout { cause }`; transport, +protocol, and codec failures map to inspectable `BadGatewayReason` values; resource limits use +`ResponseLimitReason`. Applications do not need to classify errors by matching messages. + +`EdgeError::provider_failure_detail()` borrows optional, fixed-size provider evidence separately +from that public classification. `ProviderFailureDetail` identifies the adapter and observation +phase and holds only closed causes, numeric codes and I/O kinds, never provider text, URLs, +headers or bodies. Spin DNS text recognizes only `NOERROR`, `FORMERR`, `SERVFAIL`, `NXDOMAIN`, +`NOTIMP` and `REFUSED`; every other spelling is `Unknown`, while absent input remains absent. +The `bad_gateway_with_detail`, `bad_request_with_detail`, `gateway_timeout_with_detail` and `internal_with_detail` +constructors leave Display, messages, statuses, existing reasons and framework wire bytes unchanged. +Detail survives body policies, buffered collection, retained body errors, pending handles and +batch failures. Framework code does not log it; application logging and correlation remain policy. +The existing send and response-body SDK boundaries now populate it on all four adapters. Fastly +also records backend-registration and upload I/O failures; Spin distinguishes send from request +completion. Axum retains reqwest predicates and searches up to 32 source entries for an I/O kind +(reqwest's own predicate walks have their own behavior). Cloudflare recognizes only `AbortError` +and `TypeError` names and never infers a timeout from a name; text-only SDK errors remain Unknown. +When an existing deadline check replaces an observed error, the selected timeout keeps its safe +detail. Timer-only expiry, late successful results and EOF do not invent provider evidence. +Cloudflare request construction and Spin header, timeout-option and target setters record +`RequestPreparation`. This phase describes the observed API boundary, not transport progress; +an error returned by an SDK send operation still has the Send phase, including a reqwest builder +error. Cloudflare's SDK erases constructor exception types, so those causes remain Unknown. +Spin preserves all five header-error categories and fatal first-byte/between-bytes setter +rejections, discarding `Other` messages. A connect-setting refusal is advisory for every +`RequestOptionsError`: it does not fail the request or become the cause of a later timeout. +The unchanged outer race still bounds guest-visible pending work by the aggregate deadline; +the host may retain an opaque phase default. Unit-returning target setters record component +rejection without fabricating an HTTP transport error code. `NotSupported` remains advisory +for all timeout options. +Shared response-header conversion is not labeled as request preparation, and local validation +does not invent provider evidence. + +Response completion callbacks and observers receive the same immutable +`ResponseEgressReport::source_failure` snapshot. It retains only the first converter-observed +body-source error's fixed category, optional provider detail and observation-time body kind. +A first error without provider detail is still an observation; later errors cannot replace it. +This is not the terminal cause: deadline or cancellation may win after a source error becomes +ready, and an application-body observation remains tagged `Application` through fallback delivery. +Recording adds no clock samples, source polls, callbacks or wire diagnostics. Rendered application +errors delivered successfully, clean EOF, and transport-only failures do not invent source errors. +Precommit normalization rejections appear separately in `report.framing_failures.application()` +and `.fallback()`, each an optional `ResponseEgressFramingError`. The first rule wins per body +kind, so a failed fallback cannot hide the application rule or overwrite its own earlier rule. +Recording and fixed-text error-level logging are initial-only; no header values or body bytes are logged. +After writing or termination, normalization still returns its original result without recording a +precommit observation. +`EdgeError::response_framing_error()` also reads this enum when an internal error already owns it. +Policy, deadline, host-head and source failures do not invent framing rules. Postcommit stream +length failures keep their existing source categories, not a new precommit classification. + +Exact report literals must add `source_failure: None` and `framing_failures: Default::default()` +when there is no observation. There is no old-shape compatibility API. Stock fallbacks are +framing-valid; invalid-fallback attribution is verified directly at the shared normalization +boundary, not claimed as an observed adapter-generated failure. +[`outbound-failure-diagnostics`](/guide/capabilities#provider-failure-diagnostics) reports Native +for Fastly/Spin SDK-cause projection and BestEffort for Axum/Cloudflare's reduced visibility. +Hosted qualification and the remaining outbound migration remain open; these diagnostics do not +strengthen transport guarantees. + +`ResponseDecoding::Original` preserves encoded payload bytes and the representation headers +that survive ordinary normalization. It does not restore hop-by-hop or invalid fields. Use it +for relays; the demo and generated proxy handler do so. Other requests default to +`ResponseDecoding::Decode`, which decodes a single bare gzip, deflate or Brotli coding. All 206 +responses retain original coding under either choice, so partial representation offsets stay +meaningful. HEAD/304 metadata and bodyless handling retain their existing behavior. + +Original coding skips decoder allocation and the decoded-byte cap for encoded payloads, not the +encoded or final buffered caps. Identity bodies still obey the decoded cap. `Accept-Encoding` +is independent: a supplied value survives unchanged and an absent value defaults to `identity`. +`response_decoding_setting()` exposes the choice before dispatch; `json()` parses delivered +bytes and never implicitly decodes an original encoded body. + +## Adapter Marker + +`HttpClient::with_proxy_marker(false)` configures a relay handle once. Other handles keep the +default-on policy. `OutboundRequest::proxy_marker(OutboundProxyMarker::On)` or `Off` overrides +that handle; the request default, `Inherit`, uses its client setting. The demo and generated relay +turn the marker off without per-request stripping, while JSON fanout keeps the default on. + +Inspect `client.effective_proxy_marker_enabled(&request)` before sending. This combines both +settings without validation, body polling or I/O; `proxy_marker_setting()` alone may still be +`Inherit`. Supplied client implementations receive a resolved `On` or `Off` setting. + +When off, EdgeZero adds no synthetic `x-edgezero-proxy` field or header-budget charge. It does +not remove or overwrite an upstream field with that name; ordinary normalization still applies, +including adapter-visible Connection nominations. When on, the marker overwrites upstream values +and consumes the same byte/count budget as before. Direct adapter calls with `Inherit` use default on. + +## Construct A Request + +Use `OutboundRequest::get`, `post`, or `new` for a new request. `from_request` is intended for +forwarding an inbound request and performs the same target validation and hop-by-hop header +normalization immediately. + +```rust +use edgezero_core::outbound::{OutboundCachePolicy, OutboundRequest}; +use std::num::NonZeroU64; +use std::time::Duration; + +let request = OutboundRequest::post("https://api.example.com/events")? + .cache_policy(OutboundCachePolicy::Bypass) + .timeout(Duration::from_secs(2)) + .max_request_body_bytes(256 * 1024) + .max_encoded_response_bytes(2 * 1024 * 1024) + .max_decoded_response_bytes(4 * 1024 * 1024) + .max_response_bytes(4 * 1024 * 1024) + .max_response_header_bytes(64 * 1024) + .max_response_header_count(100) + .max_chunk_bytes(NonZeroU64::new(64 * 1024).unwrap_or(NonZeroU64::MIN)) + .max_brotli_window_bits(24) + .max_decoder_bytes(32 * 1024 * 1024) + .header("accept", "application/json")? + .json(&payload)?; +let response = client.send(request).await?; +let decoded: ApiResponse = response.json()?; +``` + +`host_authority_override("virtual.example:8443")` changes only the outgoing HTTP authority. The +request URI still selects the connection destination, TLS SNI, and certificate identity. Axum and +Fastly support that separation, Cloudflare is BestEffort pending a deployed wire probe, and Spin +rejects the override during preflight. + +Buffered response mode is the default. Call `stream_response()` for a streamed response and +consume `response.into_body()` while its absolute request deadline is still active. Use +`into_bytes_bounded`, `into_bytes_bounded_until`, or `json_bounded_until` when application code +performs an additional collection step. + +Each adapter checks its selected absolute budget again before returning a successful response, +including bodyless responses and streamed response heads. Equality is expired: setup and +normalization cannot hand off a late success or restart the budget. This check does not read a +lazy payload or replace an earlier header/source/limit error. It does not upgrade provider +interruption guarantees; blocked host operations retain their documented capability limits. + +`max_chunk_bytes` opt-in rechunks guest-visible response items before they reach the application. +It bounds emitted item shape, not allocation: split `Bytes` values can retain their original +backing allocation, and a provider SDK may allocate the source chunk before EdgeZero receives it. + +### Connect Timeout + +`connect_timeout` is independent of the aggregate timeout/deadline and defaults to unset. Zero is +invalid; a longer connect setting cannot extend the aggregate budget. Keep it a configuration +constant: Fastly includes connect settings in stable backend identity, so different settings can +use different connection pools. First-byte time is not reduced by a fixed connect share. + +Fastly maps the setting to its SDK; hosted expiry and provider DNS/TLS phase coverage still need +qualification. Spin passes it as an advisory WASI option and retains the aggregate race if the +host refuses it. Axum and Cloudflare accept it at BestEffort, retaining aggregate enforcement; +they do not create per-request clients to emulate an independent phase timer. + +## Begin An Exchange + +`begin` settles a permitted unknown GET/HEAD body, then schedules the complete exchange before +returning `OutboundPending`. It does not wait for connection establishment or response headers. +Other streamed uploads are rejected; use `send` for them. Buffered responses are collected under +their caps even while the caller is not collecting the pending result. + +```rust +use edgezero_core::OutboundRequest; +use std::time::Duration; + +let pending = client + .begin( + OutboundRequest::get("https://api.example.com/data")? + .timeout(Duration::from_secs(2)) + .max_response_bytes(64 * 1024), + ) + .await?; +let response = pending.collect().await?; +``` + +`is_ready()` is a nonblocking, non-consuming check; errors are ready too. Dropping or cancelling +a pending handle stops its owned work. Successful collection transfers a streamed body's transport +ownership, so dropping the completed pending handle cannot abort that body. The original deadline +continues to govern it. Axum manual eager clients require an active Tokio `LocalSet`; standard +server entrypoints already supply one. See target guides for scheduling and teardown limitations. + +## Batch Requests + +`start_batch_until` accepts buffered request bodies and buffered response mode only. It yields +terminal slots in observed completion order while retaining each original input index. This lets +an application keep completed results when its own observation cutoff wins: + +```rust +use std::time::Duration; + +use edgezero_core::{EdgeError, OutboundBatchNext, OutboundBatchTermination}; + +let observation_cutoff = ctx + .monotonic_clock() + .deadline_after(Duration::from_secs(2)); +let mut batch = client.start_batch_until(requests, observation_cutoff); +let termination = loop { + match batch.next().await? { + OutboundBatchNext::Item(item) => { + let (index, result) = item.into_parts(); + match result.outcome { + Ok(response) => record_success(index, result.elapsed, response.status()), + Err(error) => record_failure(index, result.elapsed, error), + } } - Err(err) => { - tracing::error!("proxy failed: {}", err); - let response = Response::builder() - .status(StatusCode::BAD_GATEWAY) - .body(Body::from("Bad Gateway")) - .map_err(EdgeError::internal)?; - Ok(response) + OutboundBatchNext::Finished(termination) => break termination, + _ => return Err(EdgeError::internal(anyhow::anyhow!("unsupported batch event"))), + } +}; +assert!(matches!( + termination, + OutboundBatchTermination::Completed | OutboundBatchTermination::Cutoff +)); +``` + +Anchor relative observation cutoffs through the request clock as shown. `Deadline::after` uses the +process default clock and is intended only for code that also uses the default clock. The +observation cutoff stops batch observation regardless of any remaining per-request budget or later +per-request deadline. Passing an earlier observation cutoff intentionally leaves unresolved slots. + +After the last item, continue calling `next()` to await clean driver EOF and receive +`Finished(Completed)`. Item count alone is not completion: a trailing driver failure, duplicate +index, or out-of-range index still fails collection while retaining the previously collected +slots. Custom drivers must finish after their last item; zero-slot drivers are validated too. + +`client.send_all_until(requests, observation_cutoff).await` collects the same driver into an +index-aligned `OutboundCollectedSlot` values. `Resolved` owns a newly collected result; +`AlreadyDelivered` marks an item previously transferred by `check` or `next`; `Unresolved` marks +a cutoff-abandoned slot. `Completed` forbids unresolved slots, while `Cutoff` requires one. +A premature, malformed or failed driver returns `OutboundBatchFailure` with its exact error and +partial position states. Collections and items have read-only accessors and consuming +`into_parts`; their fields and batch identities cannot be reconstructed or mutated: + +```rust +let results = match client + .send_all_until(requests, observation_cutoff) + .await +{ + Ok(results) => results, + Err(failure) => { + let (error, slots) = failure.into_parts(); + for (index, slot) in slots.into_iter().enumerate() { + if let edgezero_core::OutboundCollectedSlot::Resolved(slot) = slot { + retain_terminal_slot(index, slot); + } } + return Err(error); } -} +}; ``` -## Notes +Driver failure never degrades into cutoff-like missing slots. Ordinary terminal results are timed +from the batch method-entry snapshot through their own terminal observation, including preflight +and body buffering. If an earlier per-request deadline has already expired, the result is a timeout +at that logical deadline even when its observation arrives later; its elapsed value ends at expiry. +This exception preserves earlier own-timeout results across a later observation cutoff. Elapsed is +not pure network RTT. Bound the request count and every per-request body limit; EdgeZero +intentionally has no global batch-concurrency or memory cap. + +`check()` observes without waiting. An accumulator obtained from `batch.accumulator()` can retain +intact checked items after inspection, then merge the final success or failure collection. Merging +validates batch identity and fills only `AlreadyDelivered` positions. Insert/merge failures return +the exact original owned inputs; consuming an item's or collection's `into_parts` relinquishes +merge eligibility. `cancel()` returns every never-delivered index, including recorded results +that the caller has not yet taken. -- Fastly and Cloudflare stream outbound request bodies; Axum buffers them before sending, and Spin buffers them too, capping streamed-body collection at 16 MiB (larger streams error) while an already-buffered `Body::Once` bypasses that cap. -- Fastly, Cloudflare, and Spin automatically decode `gzip`/`br` responses for you. -- If you need a direct client (for tests or custom wiring), use the adapter clients - (`FastlyProxyClient`, `CloudflareProxyClient`, `SpinProxyClient`, `AxumProxyClient::try_new()?`). +On Axum, Cloudflare, and Spin, each child samples its terminal instant synchronously when it is +observed ready. Those samples follow poll-observation order in the shared monotonic clock domain, +so after one observation reaches the observation cutoff a later observation cannot be on time +unless an injected clock moves backwards. Fastly's provider-selected completion path remains +BestEffort as documented in the capability matrix. -## Next Steps +## Platform Behavior -- Learn about [Fastly](/guide/adapters/fastly), [Cloudflare](/guide/adapters/cloudflare), and [Spin](/guide/adapters/spin) adapter specifics +The API is portable, but not every runtime can provide identical timing, header, or lazy-stream +semantics. See [Capabilities](/guide/capabilities) for the exact support matrix, Fastly's dynamic +backend prerequisite, each adapter's lazy downstream handoff boundary, Cloudflare's manual +encoding boundaries, and complete resource-accounting exclusions. diff --git a/docs/guide/roadmap.md b/docs/guide/roadmap.md index 683198624..bd219f313 100644 --- a/docs/guide/roadmap.md +++ b/docs/guide/roadmap.md @@ -11,8 +11,6 @@ shift as the roadmap evolves. `edgezero.toml`, respect `RUST_LOG` for dev output, and bake in hot reload for `edgezero serve --adapter axum` (the local dev path; the standalone `dev` subcommand was reserved for a future dev-workflow command, see [CLI reference](./cli-reference#edgezero-demo)). -- Adapter behavior matrix: document which adapters buffer bodies, which preserve streaming, and - where proxy headers/automatic decompression apply so expectations match runtime behavior. - Example coverage: add focused guides for `axum.toml`, manifest `description` fields, logging precedence, and introspection routes + body-mode behavior to reduce ambiguity. - Provider additions: prototype a third adapter (e.g. AWS Lambda@Edge or Vercel Edge Functions) @@ -25,12 +23,12 @@ shift as the roadmap evolves. - Manifest ergonomics: established the `edgezero.toml` schema and CLI scaffolding for route triggers, env/secrets, and build targets. - Documentation baseline: published a single-source-of-truth docs set aligned with current APIs - (App::build_app entrypoints, adapter dispatch signatures, middleware signature, proxy handle - usage). -- Platform focus: Fastly Compute@Edge, Cloudflare Workers, and Fermyon Spin are the edge targets, - with Axum serving local development and native deployment needs. -- Spin support: first-class `edgezero-adapter-spin` (`wasm32-wasip2`) with its own adapter guide, - contract tests, and CLI scaffold target. + (`App::build::` entrypoints, adapter dispatch signatures, middleware signature, and outbound + HTTP capabilities). +- Outbound contract: shipped the portable client, typed limits/errors, per-slot timing, exact + capability matrix, and adapter-specific runtime implementations. +- Platform coverage: Fastly Compute, Cloudflare Workers, Fermyon Spin, and native Axum have + first-class adapters with executable contracts. - Core contracts: request/response mapping rules are now captured in the adapter contract docs. ## Open Design Questions (for later pickup) diff --git a/docs/guide/routing.md b/docs/guide/routing.md index 9c58c81da..556b34c51 100644 --- a/docs/guide/routing.md +++ b/docs/guide/routing.md @@ -15,11 +15,16 @@ handler = "my_app_core::handlers::hello" [[triggers.http]] id = "echo" +class = "interactive" path = "/echo/{name}" methods = ["GET", "POST"] handler = "my_app_core::handlers::echo" ``` +An optional `class` is opaque application metadata for ingress admission. It is available through +`RouteMetadata::class()` for matched and method-not-allowed resolutions and does not change the +route's stable identity. + You can also build routes programmatically using convenience methods: ```rust @@ -40,7 +45,7 @@ use edgezero_core::http::Method; let router = RouterService::builder() .route("/hello", Method::GET, hello_handler) - .route("/echo/{name}", Method::GET, echo_handler) + .route_with_class("/echo/{name}", Method::GET, "interactive", echo_handler) .route("/echo", Method::POST, echo_json_handler) .build(); ``` @@ -111,7 +116,9 @@ RouterService::builder() .build() ``` -EdgeZero automatically returns `405 Method Not Allowed` for requests that match a path but use an unsupported method. +EdgeZero automatically returns `405 Method Not Allowed` for requests that match a path but use an +unsupported method. The response includes an `Allow` header containing the sorted, deduplicated +method set registered for that path. ## Introspection Routes @@ -175,11 +182,12 @@ Axum-style `:name` parameters are **not supported**. Use `{name}` instead. Routes are matched by specificity (static segments first, then parameters, then catch-alls). Registering two routes that conflict for the same method (for example, two routes that both look -like `/users/{id}`) panics while the routes are registered (inside `build_app()`) with +like `/users/{id}`) panics while the routes are registered (inside `App::build::(platform)`) with `duplicate route definition for `. When that happens depends on the adapter: Axum builds the router once at startup, so the conflict -surfaces before any traffic, but the Fastly, Cloudflare, and Spin entry points call `build_app()` -per request, so a conflicting route deploys successfully and then panics on the first request. +surfaces before any traffic, but the Fastly, Cloudflare, and Spin entry points call +`App::build::(platform)` per request, so a conflicting route deploys successfully and then panics on +the first request. Run the app under the Axum dev server before deploying to an edge target. ## Next Steps diff --git a/docs/guide/streaming.md b/docs/guide/streaming.md index c7ba0eaab..f8a5ebe94 100644 --- a/docs/guide/streaming.md +++ b/docs/guide/streaming.md @@ -1,6 +1,8 @@ # Streaming -EdgeZero supports streaming responses for large payloads, real-time data, and server-sent events. +EdgeZero preserves lazy streaming response bodies on every adapter. Each adapter owns body +delivery through its strongest platform boundary; no standard entrypoint collects the complete +downstream response before handoff. ## Streaming Responses @@ -33,30 +35,30 @@ async fn stream_data() -> Response { ## How Streaming Works -The core router passes `Body::Stream` through untouched; whether the client sees -chunks progressively depends on the adapter: +The router keeps `Body::Stream` intact until the adapter response coordinator: 1. Your handler returns `Body::stream(...)` with a `Stream` of chunks -2. Cloudflare wraps the stream in a `ReadableStream` (`Response::from_stream`), so - chunks reach the client as they are produced -3. Spin and Axum collect the stream into a buffer, and Fastly's standard - `run_app`/`serve_app` dispatch writes each chunk - into a host-side body that is only sent once complete (Spin rejects streamed - bodies over 16 MiB), so on all three the client receives the whole body at - once - -On Cloudflare, streaming also keeps memory flat, because each chunk is forwarded as -it is produced. On the buffering adapters a streamed body is collected in full -before the response goes out: Axum into an unbounded buffer, Spin into a buffer -capped at 16 MiB (larger bodies fail), and Fastly into a host-side body that is -only sent once complete. There `Body::stream` is an API-composability convenience, -not a memory saving, and a very large streamed response can exhaust memory or be -rejected. For progressive delivery (SSE or long-lived chunked responses), use Cloudflare's -standard dispatcher or Fastly's custom [`lifecycle::serve_custom` callbacks](/guide/adapters/fastly#custom-dispatch-and-streaming). -The Fastly callback must explicitly commit, pump and flush chunks, and finish the -writer. Convert with `request::into_core_request_with_registries` (the bare -`into_core_request` inserts no store registries) and drive `app.router().oneshot(..)` -yourself, so the core response stays available for streaming. +2. Core validates response framing and wraps declared-length enforcement around the source +3. The adapter polls one chunk at a time under its host's demand or write readiness +4. Accepted bytes, deadline/abort events, and one terminal outcome are recorded by that adapter + +Axum uses a connection-local Hyper body, Cloudflare a JavaScript stream writer, Fastly +`stream_to_client`, and Spin a WASI body writer. See [Capabilities](/guide/capabilities) for the +exact acceptance and completion boundary on each target. + +Application retention does not change this ownership. Every dispatch creates +fresh admission and response-egress state, even when the same successfully built +`App` is reused. Keep body sources, native writers, completion resources, and +pending work request-local rather than storing them in the app cache. + +Fastly's `serve_app` and `serve_app_with_hooks` retain an app through +`lifecycle::Sandbox` while the adapter owns sending and streaming. The hooks +borrow request/response values; they do not return a native response for the SDK +to send again. Cloudflare's `dispatch_app::(&app, req, env, ctx)` returns a +Worker response backed by its request-local coordinator. Spin's +`dispatch_app::(&app, req)` returns a raw `SpinResponse` whose body and +transmission lifetime remain owned by the adapter coordinator. None of these +paths collect the complete response before handoff. ## Server-Sent Events @@ -86,6 +88,12 @@ async fn events() -> Response { } ``` +::: warning Completion semantics +All adapters preserve progressive production, but their response-egress capability cells remain +BestEffort because host acceptance or close does not prove end-client receipt. Fastly source polls +and hostcalls are synchronous and cannot be preempted. +::: + ## Body Modes Routes can specify their body handling mode in the manifest. This is parsed today and reserved @@ -104,43 +112,45 @@ body-mode = "buffered" # or "stream" | `buffered` | Body is fully read into memory before handler runs | | `stream` | Body is passed as a stream for progressive processing | -## Transparent Decompression +## Outbound Response Decompression -On Fastly, Cloudflare, and Spin, EdgeZero automatically decompresses gzip and brotli -responses from upstream services (the Axum proxy client has no decode path): +By default, the outbound client decodes a single bare `gzip`, `deflate`, or `br` content coding +through shared `edgezero-core` decoders. `response_decoding(ResponseDecoding::Original)` opts out; +206 responses always retain original coding. Unknown, parameterized, or stacked codings also +pass through unchanged. Identity bodies keep their decoded-byte cap under either choice. +Encoded transport bytes, decoded output, and final buffered bytes have independent limits; see +[Capabilities](/guide/capabilities#limits-and-accounting). ```rust -// Proxied response with Content-Encoding: gzip is automatically decoded -let response = proxy.forward(request).await?; -// response.body is now decompressed +let request = OutboundRequest::get("https://api.example.com/data")? + .max_encoded_response_bytes(2 * 1024 * 1024) + .max_decoded_response_bytes(8 * 1024 * 1024) + .stream_response(); +let body = client.send(request).await?.into_body(); ``` -This happens transparently in the adapter layer using shared decoders from `edgezero-core`. +Multi-member gzip streams are decoded through every member and drained to transport EOF. +For Brotli, the stream window is checked before decoder allocation and decoder state is charged +against the configured policy limit. ## Memory Considerations -On paths that forward chunks as they are produced (Cloudflare's standard -dispatcher and Fastly `lifecycle::serve_custom` callbacks; see -[How Streaming Works](#how-streaming-works)), streaming is what makes these -workloads fit: +Lazy streaming is useful for: - Large file downloads - Video/audio content - Real-time data feeds - Responses larger than available memory -::: warning Platform Limits -Edge platforms have memory constraints. A Fastly Compute instance has ~128MB by default. On Cloudflare, stream large responses rather than buffering. On Fastly's standard `run_app`/`serve_app` paths, Spin, and Axum a streamed body is still collected in full before it is sent, so keep responses within the instance's memory regardless of how you build the body. Spin additionally caps streamed-body collection at 16 MiB; an already-buffered `Body::Once` bypasses that cap. Fastly's [custom serving callbacks](/guide/adapters/fastly#custom-dispatch-and-streaming) can instead stream progressively with explicit sending. +::: warning Platform limits +Edge platforms have finite memory. Lazy response delivery removes whole-body collection but does +not bound provider queues, SDK allocations, source chunk size, or transport buffers. Bound source +chunks and application work, and use the capability matrix when certifying a deployment. ::: ## Chunked Transfer -When the response size is unknown, `Body::stream` lets the adapter send without a -`Content-Length`. Cloudflare delivers this as chunked transfer; the buffering -adapters compute the length once the body is collected. Fastly custom serving -callbacks may also commit a native streaming response; see -[Custom dispatch and streaming](/guide/adapters/fastly#custom-dispatch-and-streaming). -For example: +Applications may omit `Content-Length` when the response size is unknown: ```rust #[action] @@ -156,7 +166,69 @@ async fn dynamic_content() -> Response { } ``` +Untransformed transmitted buffered bodies carry an exact `Content-Length`, including zero for an +empty body. This includes framework errors and bounded fallbacks. A supplied buffered length must +match the source or conversion fails before commitment. HEAD/204/205/304 do not gain a core-derived +length. Fastly still delegates framing to Automatic, but does not restore suppressed HEAD bytes: +doing so causes HTTP/2 protocol failures in the pinned local runtime. Exact known-buffer HEAD/205 +SDK parity remains an open gate; strict body suppression takes precedence. + +Streams remain progressive: without a declared length the provider chooses transfer framing; +with a declared length EdgeZero enforces the source byte count and aborts a short or long stream. +No stream is buffered or polled merely to guess its length. Fastly preloads actual buffered bodies +before its non-streaming Automatic handoff, so an expired precommit deadline can produce the 504 +fallback instead of truncating an already-committed buffered response. + +Host transformations take precedence over source length. For Fastly host compression requested +with `x-compress-hint`, Automatic framing may recompute or omit client-visible `Content-Length`. +Any such hint also disables manual framing for declared streams; EdgeZero still validates their +untransformed source length. It deliberately does not interpret ambiguous hint values in the guest. +Hosted compression and HTTP/1.1/HTTP/2 SDK parity remain qualification gates, not claims inferred +from Viceroy. See the [response egress capability matrix](/guide/capabilities#response-egress-matrix). + +## Framework Error Headers + +Configure a bounded header set once during application configuration: + +```rust +use edgezero_core::app::App; +use edgezero_core::http::{HeaderMap, HeaderValue}; +use edgezero_core::EdgeError; + +fn configure(app: &mut App) -> Result<(), EdgeError> { + let mut headers = HeaderMap::new(); + headers.insert("x-application-version", HeaderValue::from_static("1.0.0")); + app.set_synthesized_response_headers(headers) + .map_err(EdgeError::internal)?; + Ok(()) +} +``` + +The set applies to normalized ingress errors, admitted source errors, canonical 404/405 responses, +and adapter-built egress fallbacks, including HEAD fallbacks. It does not decorate routed responses +or routed handler errors, application-built admission refusals, or host parser responses. Assembly +failures have no configured application and therefore no configured headers or renderer. + +Limits are **32 field values** and **4096 bytes**, inclusive. Repeated values count separately; +bytes are `2 + sum(name.len() + value.len() + 4)`, including `: ` and CRLF per field and the final +CRLF, but excluding the status line. Values must be ASCII text with optional interior tabs and no +leading/trailing space or tab. Empty values are valid. Framing, hop-by-hop, authentication-proxy, +`Content-Type` and `Content-Encoding` fields are forbidden. Construction errors expose no values. +Accepted maps and backing storage are compacted; these bounds do not cover the caller's input +allocation, existing response/renderer fields, or provider-owned serialization and transport memory. + +Existing response-owned names win; otherwise all configured values of that name are appended in +order. Canonical `Allow`, the effective config-503 retry value and typed body-overflow retry +suppression remain authoritative. The fixed fallback body and independent one-second budget do not +change, and fallback decoration runs neither application code nor validation again. + +On Fastly, construct `SynthesizedResponseHeaders::new(fields)` inside `prepare` and insert the +validated value into the provided extension bag. It replaces the app set for that request before +egress preparation, including early normalized 414/431 errors. An empty validated set clears the +request's decoration; failed construction installs nothing and leaves the app set available. +The other adapters use the app snapshot. This guest-side policy does not qualify hosted delivery. + ## Next Steps -- Learn about [Proxying](/guide/proxying) for forwarding requests upstream -- Explore adapter-specific streaming in the [Fastly](/guide/adapters/fastly), [Cloudflare](/guide/adapters/cloudflare), and [Spin](/guide/adapters/spin) guides +- Learn about [Outbound HTTP](/guide/proxying) for upstream requests +- Explore adapter-specific streaming in the [Fastly](/guide/adapters/fastly), [Cloudflare](/guide/adapters/cloudflare), [Spin](/guide/adapters/spin), and [Axum](/guide/adapters/axum) guides diff --git a/docs/guide/what-is-edgezero.md b/docs/guide/what-is-edgezero.md index 23c4bbda9..9b16aa9eb 100644 --- a/docs/guide/what-is-edgezero.md +++ b/docs/guide/what-is-edgezero.md @@ -1,16 +1,16 @@ # What is EdgeZero? -EdgeZero is a production-ready toolkit for writing an HTTP workload once and deploying it across multiple edge providers. The core stays runtime-agnostic so it compiles cleanly to WebAssembly targets (Fastly Compute@Edge, Cloudflare Workers) and to native hosts (Axum/Tokio) without code changes. +EdgeZero is a production-ready toolkit for writing an HTTP workload once and deploying it across multiple edge providers. The core stays runtime-agnostic so it compiles cleanly to WebAssembly targets (Fastly Compute, Cloudflare Workers, Fermyon Spin) and to native hosts (Axum/Tokio) without code changes. ## Key Features EdgeZero provides developers with: - **Portable HTTP workloads** - Write your business logic once using the shared `edgezero-core` primitives, then compile to any supported target -- **Multiple deployment targets** - Deploy to Fastly Compute@Edge, Cloudflare Workers, or native Axum servers from the same codebase +- **Multiple deployment targets** - Deploy to Fastly Compute, Cloudflare Workers, Fermyon Spin, or native Axum servers from the same codebase - **Type-safe extractors** - Use ergonomic extractors like `Json`, `Path`, and `ValidatedQuery` for clean handler code - **Streaming support** - Stream responses progressively with `Body::stream` for long-lived or chunked responses -- **Proxy helpers** - Forward traffic upstream with built-in `ProxyRequest` and `ProxyService` abstractions +- **Outbound HTTP** - Send typed, bounded requests through one portable client with deadlines and partial-failure batching - **CLI tooling** - Scaffold projects, run dev servers, and deploy with the `edgezero` CLI ## How It Works diff --git a/docs/superpowers/plans/2026-06-01-spin-kv-backed-config.md b/docs/superpowers/plans/2026-06-01-spin-kv-backed-config.md index 25d6f31e2..a719d0f70 100644 --- a/docs/superpowers/plans/2026-06-01-spin-kv-backed-config.md +++ b/docs/superpowers/plans/2026-06-01-spin-kv-backed-config.md @@ -1,5 +1,10 @@ # Plan: Move Spin Config Store onto KV +**Implementation update (2026-10-02):** The eager config-store open described +below was superseded. Declared labels are now bound without host calls during +dispatch; `SpinConfigStore` opens inside the bounded extraction read, after +admission. The config design specifies the current behavior. + **Status:** v12 — REVISION after tenth reviewer pass. Ready for execution. **Reviewer green-lighted start.** diff --git a/docs/superpowers/plans/2026-06-17-blob-app-config.md b/docs/superpowers/plans/2026-06-17-blob-app-config.md index 00c806f82..3f8971d73 100644 --- a/docs/superpowers/plans/2026-06-17-blob-app-config.md +++ b/docs/superpowers/plans/2026-06-17-blob-app-config.md @@ -10,6 +10,25 @@ ## Global Constraints +PR #275 follow-up supersedes this plan's older owned-string store signatures and +per-request Axum file-reading assumptions. The current normative contract is spec +§6.3 and §9.1 plus the Axum guide's snapshot-allocation limits. + +- [x] Hard-cut raw config reads to shared `ConfigValue` across all adapters and fixtures. +- [x] Validate finite Axum startup/snapshot budgets and load before listener binding. +- [x] Bound actual reads, flat decoder staging, entry/key/value allocations and all-store residency. +- [x] Preserve failed declared bindings, reject duplicate keys and invalid registry metadata. +- [x] Stream canonical hashing and drop the extraction-owned raw handle before secrets. +- [x] Align demo, generator, public guides and CHANGELOG with snapshot/restart semantics. +- [x] Stream Cloudflare bounded KV reads through capped BYOB buffers; cancel acquired readers on termination. +- [x] Use caller-sized Fastly ABI buffers without growth/retry, preflight aggregate chunk limits and avoid early envelope DOM allocation. +- [ ] Establish a bounded Spin host/backend read interface or verified hosted allocation ceiling; stock list-valued reads are not sufficient. +- [ ] Qualify hosted provider allocation/cancellation evidence; do not promote capability cells without it. + +These follow-ups supersede older tasks below that describe Cloudflare text materialization, +Fastly SDK auto-growing reads or inner-envelope DOM verification in the bounded resolver. +See spec §9.2/§9.3 and the public capability guide for the current allocation charges. + - **Source of truth:** [`docs/superpowers/specs/2026-06-16-blob-app-config.md`](../specs/2026-06-16-blob-app-config.md). Every task numbered below cites the spec section it implements. - **Hard cutoff:** no `#[serde(default)]` compat fallback for missing legacy state, no runtime fallback to the old per-leaf app-config shape, no platform-side bridge for legacy state. Fastly's direct-or-pointer storage is a new v1 physical representation for the same blob envelope, not legacy compatibility. Per spec §1 / §10.1. - **Atomic cutover:** spec §10.1 + §10.2 require runtime extractor + per-adapter writers + app-demo migration + scaffold templates + CI gate to land in ONE commit (Phase C below). Intermediate trees with new runtime / old writer do NOT compile cleanly because the read-trait-driven inline diff and the gate cross-check would fail. **Bisect-safe annotations** appear in each task heading. @@ -30,7 +49,7 @@ | Phase | Spec § | Commit type | Acceptance | | ----- | ----------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | A | §4 | Pre-cutover infra | Canonical form + envelope + non-finite-float check ship in `edgezero-core`. No in-tree caller exercises them yet. Bisect-safe. | -| B | §3, §5, §6.3, §9.0 | Pre-cutover infra | `ConfigStoreBinding`, `EnvConfig::store_key`, manifest charset, `EdgeError::ConfigOutOfDate`, derive-macro extensions, read trait + per-adapter impls, and Fastly chunk-pointer helpers. None called yet. Bisect-safe. | +| B | §3, §5, §6.3, §9.0 | Pre-cutover infra | `ConfigStoreBinding`, `EnvConfig::store_key`, manifest charset, typed `StoreExtractionReason`, bounded read traits/policies, derive-macro extensions, per-adapter read impls, and Fastly chunk-pointer helpers. None called yet. Bisect-safe. | | C | §3.3, §8.2, §10.2, §10.2.1, §10.2.2 | **CUTOVER (not splittable)** | `AppConfig` extractor + `config push` rewrite + app-demo migration + scaffold templates + all three CI gates land together. Spec §10.1 forbids splitting. | | D | §8.1 | Post-cutover additive | `config diff` command, format renderers, `--exit-code`. Depends on Phase C's read flow + writers. | | E | §10 narrative | Post-cutover docs | Migration guide, smoke scripts, README updates. | @@ -998,7 +1017,7 @@ default = "feature__flags" Run: `cargo test -p edgezero-core --lib manifest` Expected: 3 new tests pass. -## Task B2 — `EdgeError::ConfigOutOfDate` variant + two constructors +## Task B2 — `EdgeError::ConfigOutOfDate` variant + explicit-pair constructor **Files:** @@ -1010,7 +1029,6 @@ Expected: 3 new tests pass. - Produces: - `EdgeError::ConfigOutOfDate { message: String, field_path: String }` - `EdgeError::config_out_of_date(message: impl Into, field_path: impl Into) -> Self` - - `EdgeError::config_out_of_date_from_serde(err: serde_path_to_error::Error) -> Self` - [ ] **Step 1: Add the variant.** @@ -1045,18 +1063,13 @@ Edit `crates/edgezero-core/src/error.rs` near the existing enum. Insert: } } - /// Construct from a `serde_path_to_error` error returned by - /// the deserialise wrapper around the blob's `data` field. - pub fn config_out_of_date_from_serde( - serde_err: serde_path_to_error::Error, - ) -> Self { - Self::ConfigOutOfDate { - message: serde_err.inner().to_string(), - field_path: serde_err.path().to_string(), - } - } ``` +Typed store deserialization is added by Task B2.5 through +`store_deserialization_from_serde`, which carries the inspectable +`StoreExtractionReason::Deserialization`. The earlier compatibility constructor is +intentionally absent so consumers must migrate to the reason-bearing API. + - [ ] **Step 3: Add `serde_path_to_error` to `crates/edgezero-core/Cargo.toml`.** In `[dependencies]`: @@ -1118,186 +1131,123 @@ In the existing `mod tests` block: Run: `cargo test -p edgezero-core --lib error` Expected: pass; no existing test broken (the three exhaustive matches above — `inner()`, `message()`, `status()` — and the `thiserror` `#[error(...)]` attribute on the new variant cover every dispatch surface). -## Task B3 — `EdgeError` response body adds `kind` field + `Retry-After` on `ConfigOutOfDate` +## Task B2.5 — Typed store-extraction reasons (must precede extractor code) **Files:** -- Modify: `crates/edgezero-core/src/error.rs` — extend `IntoResponse` impl. -- Modify: `crates/edgezero-core/src/error.rs` — add per-variant `kind` constants. +- Modify: `crates/edgezero-core/src/error.rs`. +- Modify: every exhaustive `EdgeError` match identified by `rg -n 'EdgeError::' crates`. **Interfaces:** -- Produces: response body shape `{ "error": { "status": , "kind": "", "message": "<…>", "field_path"?: "<…>" } }`. `field_path` ONLY on `ConfigOutOfDate`. `Retry-After: 60` header ONLY on `ConfigOutOfDate`. Per spec §6.3.1. +- Produces: the exact `#[non_exhaustive] StoreExtractionReason` enum and + `EdgeError::StoreExtraction { reason, message, field_path }` contract from spec §6.3. +- Produces: `EdgeError::store_extraction`, + `EdgeError::store_deserialization_from_serde`, and + `EdgeError::store_extraction_reason`. -- [ ] **Step 1: Add a `kind_str` private method per variant.** +- [ ] **Step 1: Write the red error-classification matrix.** Enumerate every reason from + spec §6.3. For each, assert the exact status, kind, `Retry-After`, optional field path, + and absence of a JSON `reason` member. Add constructor/accessor tests and one + `#[non_exhaustive]` external-match compile fixture. -```rust -impl EdgeError { - fn kind_str(&self) -> &'static str { - match self { - EdgeError::BadRequest { .. } => "bad_request", - EdgeError::Internal { .. } => "internal", - EdgeError::MethodNotAllowed { .. } => "method_not_allowed", - EdgeError::NotFound { .. } => "not_found", - EdgeError::NotImplemented { .. } => "not_implemented", - EdgeError::ServiceUnavailable { .. } => "service_unavailable", - EdgeError::Validation { .. } => "validation", - EdgeError::ConfigOutOfDate { .. } => "config_out_of_date", - } - } -} -``` +- [ ] **Step 2: Run the focused tests and record the expected compile-red.** -- [ ] **Step 2: Rewrite the `IntoResponse` impl's body-building block.** +Run: `cargo test -p edgezero-core --lib error::tests::store_extraction` +Expected: compilation fails because `StoreExtractionReason` and the new variant do not +exist. A malformed test is not an acceptable red. -Locate the existing `IntoResponse` impl (around `error.rs:159` per spec). The current block writes `{ "error": { "status": ..., "message": ... } }`. Replace with code that: +- [ ] **Step 3: Add the enum, variant, constructors, accessor, and centralized mapping.** + Keep `ConfigOutOfDate` for non-store callers. `StoreExtraction` delegates status/kind and + response-header policy to its reason. Migrate typed extraction to + `store_deserialization_from_serde`; do not retain a compatibility constructor for typed + store deserialization. Do not leave a typed store path on `config_out_of_date`, + serialize the reason, or include an empty `field_path`. -1. Picks the right status code per variant. -2. Builds a `serde_json::Value::Object` map with `status`, `kind`, `message`, and (for `ConfigOutOfDate`) `field_path`. -3. Sets `Retry-After: 60` ONLY for `ConfigOutOfDate`. +- [ ] **Step 4: Update every exhaustive match.** This includes `StoredError` in the inbound + body design once that work lands; its captured form stores reason/message/path and + reconstructs the same variant. Do not add a wildcard to silence compiler coverage. -Pseudocode shape (adapt to the exact existing `IntoResponse` form): +- [ ] **Step 5: Run the focused tests, then all core tests.** -```rust -impl IntoResponse for EdgeError { - fn into_response(self) -> Response { - let kind = self.kind_str(); - let (status, message, field_path_opt): (StatusCode, String, Option) = match &self { - EdgeError::BadRequest { message } => (StatusCode::BAD_REQUEST, message.clone(), None), - EdgeError::Internal { source } => (StatusCode::INTERNAL_SERVER_ERROR, source.to_string(), None), - EdgeError::MethodNotAllowed { message } => (StatusCode::METHOD_NOT_ALLOWED, message.clone(), None), - EdgeError::NotFound { message } => (StatusCode::NOT_FOUND, message.clone(), None), - EdgeError::NotImplemented { message } => (StatusCode::NOT_IMPLEMENTED, message.clone(), None), - EdgeError::ServiceUnavailable { message } => (StatusCode::SERVICE_UNAVAILABLE, message.clone(), None), - EdgeError::Validation { message } => (StatusCode::UNPROCESSABLE_ENTITY, message.clone(), None), - EdgeError::ConfigOutOfDate { message, field_path } => ( - StatusCode::SERVICE_UNAVAILABLE, - message.clone(), - Some(field_path.clone()), - ), - }; - let mut error_obj = serde_json::Map::new(); - error_obj.insert("status".into(), serde_json::Value::from(status.as_u16())); - error_obj.insert("kind".into(), serde_json::Value::from(kind)); - error_obj.insert("message".into(), serde_json::Value::from(message)); - if let Some(fp) = field_path_opt { - error_obj.insert("field_path".into(), serde_json::Value::from(fp)); - } - let body = serde_json::json!({ "error": serde_json::Value::Object(error_obj) }); - let body_bytes = serde_json::to_vec(&body).unwrap_or_else(|_| b"{}".to_vec()); - // Round-39 L-2: use the EdgeZero `crate::http` re-export, NOT - // direct `http::header::*` paths. Project convention at - // crates/edgezero-core/src/error.rs:8 imports through - // `crate::http::{header::CONTENT_TYPE, ...}`; new code follows - // the same pattern so application code never depends on the - // raw `http` crate. Implementation extends the existing `use` - // line to also bring in `RETRY_AFTER`. - let mut response = Response::builder() - .status(status) - .header(CONTENT_TYPE, "application/json") - .body(Body::from(body_bytes)) - .unwrap(); - if matches!(self, EdgeError::ConfigOutOfDate { .. }) { - response - .headers_mut() - .insert(RETRY_AFTER, HeaderValue::from_static("60")); - } - response - } -} +```bash +cargo test -p edgezero-core --lib error::tests::store_extraction +cargo test -p edgezero-core ``` -Use the actual `Response` / `Body` types as imported elsewhere in `error.rs`. +Expected: all pass. -- [ ] **Step 3: Run existing error tests to confirm nothing regressed (existing tests assert the old shape will fail — that's expected; we update them next).** - -Run: `cargo test -p edgezero-core --lib error` -Note which tests fail (they're asserting the old `{ status, message }` shape). Phase B Task B4 below adds the new tests. - -## Task B4 — Per-variant `kind` + `Retry-After` tests +## Task B3 — `EdgeError` response body adds `kind` and centralized header policy **Files:** -- Modify: `crates/edgezero-core/src/error.rs` (update existing IntoResponse tests + add new ones per spec §12.6.1). +- Modify: `crates/edgezero-core/src/error.rs` — extend `IntoResponse` impl. +- Modify: `crates/edgezero-core/src/error.rs` — add per-variant `kind` constants. **Interfaces:** -- Consumes: `EdgeError::kind_str` + `IntoResponse` impl from Task B3. +- Produces: response body shape `{ "error": { "status": , "kind": "", "message": "<…>", "field_path"?: "<…>" } }`. `field_path` is emitted only for field-anchored variants. `Retry-After: 60` is emitted only for the effective `config_out_of_date` kind, including the mapped store-extraction reasons. Per spec §6.3.1. -- [ ] **Step 1: Update existing tests that assert the old body shape.** Find tests asserting `body.error.status` + `body.error.message` only; extend them to also assert `body.error.kind`. Use the table from spec §12.6.1. +- [ ] **Step 1: Extend the existing `kind_str` match without replacing it.** Keep one + explicit arm for every `EdgeError` variant present after the outbound Phase 1a work and + Task B2.5. `StoreExtraction { reason, .. }` delegates to a total private + `StoreExtractionReason::wire_policy()` table. Do not add a wildcard: later additions such + as inbound `RequestTimeout` must produce a compiler error until their owner adds a kind. -- [ ] **Step 2: Add a new test per variant asserting the `kind` string.** +- [ ] **Step 2: Rewrite the `IntoResponse` impl's body-building block.** -```rust - #[test] - fn kind_strings_per_variant() { - let cases: &[(EdgeError, &str, u16)] = &[ - (EdgeError::BadRequest { message: "x".into() }, "bad_request", 400), - (EdgeError::Internal { source: anyhow::anyhow!("x").into() }, "internal", 500), - (EdgeError::MethodNotAllowed { message: "x".into() }, "method_not_allowed", 405), - (EdgeError::NotFound { message: "x".into() }, "not_found", 404), - (EdgeError::NotImplemented { message: "x".into() }, "not_implemented", 501), - (EdgeError::ServiceUnavailable { message: "x".into() }, "service_unavailable", 503), - (EdgeError::Validation { message: "x".into() }, "validation", 422), - (EdgeError::config_out_of_date("x", "f"), "config_out_of_date", 503), - ]; - for (err, expected_kind, expected_status) in cases { - // Clone the error if it doesn't impl Clone — work around with match. - let response = err.clone().into_response(); // adjust if not Clone - assert_eq!(response.status().as_u16(), *expected_status); - let body_bytes = ...; // collect body - let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap(); - assert_eq!(body["error"]["kind"], serde_json::Value::from(*expected_kind)); - } - } -``` +Locate the existing `IntoResponse` impl (around `error.rs:159` per spec). The current block writes `{ "error": { "status": ..., "message": ... } }`. Replace with code that: -(If `EdgeError` is not `Clone`, restructure to build each case in turn rather than collecting in a slice.) +1. Picks the right status code per variant. +2. Builds a `serde_json::Value::Object` map with `status`, `kind`, `message`, and a + non-empty `field_path` from either `ConfigOutOfDate` or `StoreExtraction`. +3. Sets `Retry-After: 60` only when centralized policy marks the effective kind + `config_out_of_date`; do not use a variant-only `matches!` expression. + +Preserve the actual `IntoResponse` signature, +`fn into_response(self) -> Result`, and the existing +`response_with_body`/JSON fallback path. Compute status, kind, message, optional non-empty +field path, and retry policy before consuming `self`. `ConfigOutOfDate` keeps its current +wire behavior. `StoreExtraction` derives all four decisions from its reason table, but the +reason itself is never serialized. Use `crate::http::header::{CONTENT_TYPE, RETRY_AFTER}`; +do not introduce a direct `http` dependency or invent message fields on structured variants. -- [ ] **Step 3: Add `Retry-After` presence assertion.** +- [ ] **Step 3: Run existing error tests to confirm nothing regressed (existing tests assert the old shape will fail — that's expected; we update them next).** -```rust - #[test] - fn retry_after_only_on_config_out_of_date() { - for (err, expected_retry_after) in [ - (EdgeError::BadRequest { message: "x".into() }, false), - (EdgeError::Internal { source: anyhow::anyhow!("x").into() }, false), - (EdgeError::ServiceUnavailable { message: "x".into() }, false), // round-10 H-3 narrowing - (EdgeError::config_out_of_date("x", "f"), true), - ] { - let response = err.into_response(); - // Round-39 L-2: same `crate::http` re-export rule as the - // production code above — test bodies use the same import. - let header = response.headers().get(RETRY_AFTER); - if expected_retry_after { - assert_eq!(header.unwrap().to_str().unwrap(), "60"); - } else { - assert!(header.is_none(), "unexpected Retry-After header"); - } - } - } -``` +Run: `cargo test -p edgezero-core --lib error` +Expected red before Task B4: only assertions that intentionally pin the old response shape +fail; compilation and unrelated behavior remain intact. -- [ ] **Step 4: Add `field_path` presence assertion.** +## Task B4 — Per-variant/reason `kind` + `Retry-After` tests -```rust - #[test] - fn field_path_only_on_config_out_of_date() { - let err = EdgeError::BadRequest { message: "x".into() }; - let body: serde_json::Value = parse_body(err.into_response()); - assert!(body["error"].get("field_path").is_none()); - - let err = EdgeError::config_out_of_date("x", "feature.new_checkout"); - let body: serde_json::Value = parse_body(err.into_response()); - assert_eq!(body["error"]["field_path"], "feature.new_checkout"); - } +**Files:** - fn parse_body(response: Response) -> serde_json::Value { - // Helper: collect body bytes synchronously. If body is async, - // use futures::executor::block_on; that's the in-tree pattern. - ... - } -``` +- Modify: `crates/edgezero-core/src/error.rs` (update existing IntoResponse tests + add new ones per spec §12.6.1). + +**Interfaces:** + +- Consumes: `EdgeError::kind_str` + `IntoResponse` impl from Task B3. + +- [ ] **Step 1: Update existing tests that assert the old body shape.** Find tests asserting `body.error.status` + `body.error.message` only; extend them to also assert `body.error.kind`. Use the table from spec §12.6.1. + +- [ ] **Step 2: Add one consumed case per variant and per reason.** `EdgeError` is not + `Clone`, so build an owned `Vec<(EdgeError, expected_kind, expected_status)>` and consume + it, or use a case factory. Construct structured variants with their real fields or public + constructors. Include all variants listed by the final §12.6.1 matrix and every known + `StoreExtractionReason`; no fake `message` fields and no wildcard matches. + +- [ ] **Step 3: Add `Retry-After` assertions.** Assert `60` for + `ConfigOutOfDate` and store reasons `Deserialization`, `MissingBlob`, `MissingSecret`, + and `Validation`. + Assert absence for every other reason and ordinary variant, including plain + `ServiceUnavailable` and `DeadlineExceeded`. + +- [ ] **Step 4: Add `field_path` assertions.** Assert omission for every ordinary variant + except a non-empty `ConfigOutOfDate` path. For `StoreExtraction`, table-drive every reason + with both `None` and a supplied path: emit it only where §6.3 permits a field-anchored + path, and never emit an empty string. Parse the existing `Result` + return through the in-tree body collection helper rather than assuming a synchronous + response type. - [ ] **Step 5: Run, confirm all pass.** @@ -2589,6 +2539,77 @@ This branch logic is intentionally large because spec §9.0 says "read-back uses Run: `cargo test --workspace --all-targets` Expected: pass. +## Task B13.5 — Bounded config/secret reads and capability cells + +**Files:** + +- Modify: `crates/edgezero-core/src/config_store.rs`. +- Modify: `crates/edgezero-core/src/secret_store.rs`. +- Modify: `crates/edgezero-core/src/app.rs` and `context.rs`. +- Modify: `crates/edgezero-core/src/manifest.rs` and + `crates/edgezero-adapter/src/registry.rs`. +- Modify: all four adapter config/secret-store implementations and contract tests. + +**Interfaces:** + +- Produces: `ConfigExtractionLimits`, its five defaults, `BoundedStoreRead`, lower-level + deadline/size errors, the bounded trait methods, and matching `ConfigStoreHandle`, + `SecretHandle`, and `BoundSecretStore` forwarding methods from spec §6.3.2. +- Produces: `config-read-allocation-bounds` and `config-read-deadlines` capability cells. +- Preserves: unbounded `ConfigStore::get` and `SecretStore::get_bytes` for hand-managed + callers. `AppConfig` must not call them. + +- [ ] **Step 1: Write red core contract tests.** Test default constants and validation; + exact-cap success; first-byte-over failure; checked cumulative accounting overflow; one + absolute deadline shared across blob, Fastly chunks, and secrets; expiry winning a + simultaneous source result; backend-byte reports below returned value length or above + the supplied allowance; and typed + `ValueTooLarge`/`DeadlineExceeded` mappings. A scripted store records every passed + deadline and decreasing backend allowance and proves the deadline never changes. + +- [ ] **Step 2: Run the focused tests and record the expected compile-red.** + +Run: `cargo test -p edgezero-core --lib config_store::tests::bounded` +Expected: compilation fails on the missing limits and bounded trait methods. + +- [ ] **Step 3: Add the core policy and object-safe bounded methods.** Validate limits when + `App` is finalized, copy them into each `RequestContext`, and keep extractor-private + `ConfigExtractionBudget` state with separate checked backend-byte and retained-value + charging. All helper methods take the same `Deadline` and remaining backend allowance; + none accepts an optional or relative timeout. + +- [ ] **Step 4: Implement Axum's Native paths.** Read local files incrementally with + `max_bytes + 1` bounded allocation and race async reads against the absolute deadline. + Drop/cancel the read on expiry. Add fake-time tests for first-byte/inter-read expiry and a + filesystem test proving an oversized value is rejected without allocating the whole file. + Return exact guest-visible `backend_bytes` for every logical read. + +- [ ] **Step 5: Implement Cloudflare, Fastly, and Spin honestly.** Pass the absolute + deadline and remaining backend allowance through all helper layers, check before host + entry and after return, and discard over-cap materialized values. Fastly counts its root + pointer plus every fetched chunk in `BoundedStoreRead::backend_bytes`; direct reads count + the exposed value bytes. Use native cancellation only where the SDK exposes it. Do not + label a post-return length check as an allocation bound or a synchronous host call as + deadline-preemptible. + +- [ ] **Step 6: Add capability parsing, display, build/deploy enforcement, and matrix tests.** + Pin the initial cells exactly as in spec §6.3.2. Tests fail closed for unknown values and + for a manifest requiring Native on a weaker adapter. + +- [ ] **Step 7: Add deployed timing probes.** Each non-Axum probe uses a delayed backend, + records requested deadline versus observed return/cancellation, and emits a machine-readable + artifact. A passing probe is necessary but not sufficient to upgrade allocation support; + host API documentation must also establish pre-materialization behavior. + +- [ ] **Step 8: Run focused and workspace tests.** + +```bash +cargo test -p edgezero-core --lib config_store::tests::bounded +cargo test --workspace --all-targets +``` + +Expected: all pass. + ## Task B14 — Commit Phase B - [ ] **Step 1: Run the four CI gates.** @@ -2639,10 +2660,12 @@ docs/superpowers/specs/2026-06-16-blob-app-config.md. Config::default() still returns ConfigStoreHandle (unwraps binding.handle); new default_binding() accessor returns the binding for the typed extractor. -- §6.3.1 EdgeError gains the ConfigOutOfDate variant + two - constructors (config_out_of_date / config_out_of_date_from_serde). +- §6.3 EdgeError gains non-exhaustive StoreExtractionReason and + the typed StoreExtraction variant before extractor code lands. Response body adds a stable `kind` field on every variant; - Retry-After: 60 fires only on ConfigOutOfDate. + Retry-After: 60 fires only for the effective config_out_of_date + kind. Typed extraction uses bounded config/secret reads under one + absolute deadline and cumulative byte budget. - §3.3.1 SecretKind::KeyInNamedStore { store_ref_field } added; the #[derive(AppConfig)] macro accepts #[secret(store_ref = \"field\")], rejects #[serde(skip_serializing / skip_serializing_if @@ -2702,40 +2725,15 @@ where { async fn from_request(ctx: &RequestContext) -> Result { let binding = ctx.config_store_default_binding().ok_or_else(|| { - EdgeError::internal(anyhow::anyhow!( - "no default config store registered — check [stores.config] in edgezero.toml" - )) - })?; - let key = &binding.default_key; - let raw = binding - .handle - .get(key) - .await - .map_err(|e| EdgeError::internal(anyhow::anyhow!(e)))? - .ok_or_else(|| EdgeError::config_out_of_date( - format!("missing typed app-config blob at key `{key}` — run ` config push` for this deploy"), - String::new(), - ))?; - let envelope: BlobEnvelope = serde_json::from_str(&raw) - .map_err(|e| EdgeError::internal(anyhow::anyhow!("envelope parse failed: {e}")))?; - envelope - .verify() - .map_err(|e| EdgeError::internal(anyhow::anyhow!("envelope verification failed: {e}")))?; - let mut data = envelope.into_data(); - // Secret walk per §3.3.3. - secret_walk::(ctx, &mut data).await?; - // Deserialise via serde_path_to_error to preserve field_path - // for ConfigOutOfDate per §4.3. - use serde::de::IntoDeserializer as _; - let cfg: C = serde_path_to_error::deserialize(data.into_deserializer()) - .map_err(EdgeError::config_out_of_date_from_serde)?; - cfg.validate().map_err(|err| { - EdgeError::config_out_of_date( - err.to_string(), - first_violating_field(&err).unwrap_or_default(), + EdgeError::store_extraction( + StoreExtractionReason::MissingRegistry, + "no default config store registered — check [stores.config] in edgezero.toml", + None, ) })?; - Ok(AppConfig(cfg)) + extract_from_handle::(ctx, &binding.handle, &binding.default_key) + .await + .map(AppConfig) } } @@ -2745,7 +2743,7 @@ where /// just `"service"`). Round-34 M-1: earlier draft only looked /// at top-level keys, which collapsed /// `service.timeout_ms` → `"service"` and made -/// `EdgeError::ConfigOutOfDate.field_path` useless for any +/// the typed store-extraction `field_path` useless for any /// `#[validate(nested)]` failure (which is the common case in /// app-demo + scaffold templates). fn first_violating_field(errors: &validator::ValidationErrors) -> Option { @@ -2805,32 +2803,39 @@ fn first_violating_field(errors: &validator::ValidationErrors) -> Option async fn secret_walk( ctx: &RequestContext, + budget: &mut ConfigExtractionBudget, data: &mut serde_json::Value, ) -> Result<(), EdgeError> where C: AppConfigMeta, { let data_obj = data.as_object_mut().ok_or_else(|| { - EdgeError::internal(anyhow::anyhow!("blob `data` is not a JSON object")) + EdgeError::store_extraction( + StoreExtractionReason::Deserialization, + "blob `data` is not a JSON object", + None, + ) })?; for field in C::SECRET_FIELDS { let key_name = data_obj .get(field.name) .and_then(|v| v.as_str()) - .ok_or_else(|| EdgeError::config_out_of_date( + .ok_or_else(|| EdgeError::store_extraction( + StoreExtractionReason::Deserialization, format!("missing or non-string value at `{}`", field.name), - field.name.to_owned(), + Some(field.name.to_owned()), ))? .to_owned(); let (bound, resolved_store_id) = match field.kind { SecretKind::KeyInDefault => { let bound = ctx.secret_store_default().ok_or_else(|| { - EdgeError::config_out_of_date( + EdgeError::store_extraction( + StoreExtractionReason::MissingRegistry, format!( "secret field `{}` has kind KeyInDefault but no default secret store is registered", field.name, ), - field.name.to_owned(), + Some(field.name.to_owned()), ) })?; let id = bound.store_name().to_owned(); @@ -2841,22 +2846,52 @@ where let store_id_str = data_obj .get(store_ref_field) .and_then(|v| v.as_str()) - .ok_or_else(|| EdgeError::config_out_of_date( + .ok_or_else(|| EdgeError::store_extraction( + StoreExtractionReason::Deserialization, format!("missing store_ref `{store_ref_field}` for secret field `{}`", field.name), - field.name.to_owned(), + Some(field.name.to_owned()), ))? .to_owned(); let bound = ctx.secret_store(&store_id_str).ok_or_else(|| { - EdgeError::config_out_of_date( - format!("blob declared store_ref `{store_id_str}` but [stores.secrets] has no such id"), - field.name.to_owned(), + EdgeError::store_extraction( + StoreExtractionReason::UnknownStore, + format!( + "secret field `{}` references an unregistered store (identifier redacted)", + field.name, + ), + Some(field.name.to_owned()), ) })?; (bound, store_id_str) } }; - let secret = bound.require_str(&key_name).await.map_err(|err| { - map_secret_error(err, field.name, &resolved_store_id, &key_name) + let read = bound + .get_bytes_bounded( + &key_name, + budget.clock(), + budget.deadline(), + budget.remaining_backend_bytes(), + budget.max_secret_bytes(), + ) + .await + .map_err(|err| map_secret_error(err, field.name, &resolved_store_id, &key_name))?; + budget.charge_backend(read.backend_bytes)?; + let secret_bytes = read.value + .ok_or_else(|| EdgeError::store_extraction( + StoreExtractionReason::MissingSecret, + format!( + "the secret referenced by `{}` was not found in its store (identifier redacted)", + field.name, + ), + Some(field.name.to_owned()), + ))?; + budget.charge_value(secret_bytes.len())?; + let secret = String::from_utf8(secret_bytes.to_vec()).map_err(|_| { + EdgeError::store_extraction( + StoreExtractionReason::InvalidSecretValue, + format!("secret for field `{}` is not UTF-8", field.name), + Some(field.name.to_owned()), + ) })?; data_obj.insert(field.name.to_owned(), serde_json::Value::String(secret)); } @@ -2866,25 +2901,77 @@ where fn map_secret_error( err: crate::secret_store::SecretError, field_name: &str, - store_id: &str, - key_name: &str, + _store_id: &str, + _key_name: &str, ) -> EdgeError { use crate::secret_store::SecretError; match err { - SecretError::NotFound { name } => EdgeError::config_out_of_date( - format!("secret `{name}` in store `{store_id}` not found"), - field_name.to_owned(), + SecretError::DeadlineExceeded => EdgeError::store_extraction( + StoreExtractionReason::DeadlineExceeded, + format!("secret resolution for `{field_name}` exceeded its deadline"), + Some(field_name.to_owned()), ), - SecretError::Validation(msg) => EdgeError::config_out_of_date( - format!("secret `{key_name}` in store `{store_id}` rejected: {msg}"), - field_name.to_owned(), + SecretError::NotFound { .. } => EdgeError::store_extraction( + StoreExtractionReason::MissingSecret, + format!( + "the secret referenced by `{field_name}` was not found in its store (identifier redacted)" + ), + Some(field_name.to_owned()), + ), + SecretError::Validation(_msg) => EdgeError::store_extraction( + StoreExtractionReason::InvalidKey, + format!( + "the secret referenced by `{field_name}` was rejected by its store (details redacted)" + ), + Some(field_name.to_owned()), + ), + SecretError::Unavailable => EdgeError::store_extraction( + StoreExtractionReason::SecretBackendUnavailable, + format!("the secret store for `{field_name}` is unreachable"), + Some(field_name.to_owned()), + ), + SecretError::Internal(_source) => EdgeError::store_extraction( + StoreExtractionReason::BackendFailure, + format!("secret resolution for `{field_name}` failed (details redacted)"), + Some(field_name.to_owned()), + ), + SecretError::ValueTooLarge => EdgeError::store_extraction( + StoreExtractionReason::ValueTooLarge, + format!( + "the secret referenced by `{field_name}` exceeds its configured byte limit" + ), + Some(field_name.to_owned()), + ), + } +} + +fn map_config_store_error(err: ConfigStoreError) -> EdgeError { + match err { + ConfigStoreError::DeadlineExceeded => EdgeError::store_extraction( + StoreExtractionReason::DeadlineExceeded, + "typed app-config store read deadline exceeded", + None, + ), + ConfigStoreError::InvalidKey { .. } => EdgeError::store_extraction( + StoreExtractionReason::InvalidKey, + "typed app-config store rejected the requested key (details redacted)", + None, + ), + ConfigStoreError::Unavailable { .. } => EdgeError::store_extraction( + StoreExtractionReason::BackendUnavailable, + "typed app-config store is unavailable", + None, + ), + ConfigStoreError::Internal { .. } => EdgeError::store_extraction( + StoreExtractionReason::BackendFailure, + "typed app-config store read failed (details redacted)", + None, + ), + ConfigStoreError::ValueTooLarge => EdgeError::store_extraction( + StoreExtractionReason::ValueTooLarge, + "typed app-config store value exceeded its read limit", + None, ), - SecretError::Unavailable => EdgeError::service_unavailable(format!( - "secret store `{store_id}` unreachable" - )), - SecretError::Internal(source) => EdgeError::internal(anyhow::anyhow!( - "secret `{key_name}` in store `{store_id}` produced unexpected store error: {source}" - )), } } ```` @@ -2906,9 +2993,11 @@ where /// store and prefer the bare `C` over wrapping/unwrapping. pub async fn named(ctx: &RequestContext, key: &str) -> Result { let binding = ctx.config_store_default_binding().ok_or_else(|| { - EdgeError::internal(anyhow::anyhow!( - "no default config store registered — check [stores.config] in edgezero.toml" - )) + EdgeError::store_extraction( + StoreExtractionReason::MissingRegistry, + "no default config store registered — check [stores.config] in edgezero.toml", + None, + ) })?; extract_from_handle::(ctx, &binding.handle, key).await } @@ -2922,9 +3011,11 @@ where key: Option<&str>, ) -> Result { let binding = ctx.config_store_binding(store_id).ok_or_else(|| { - EdgeError::internal(anyhow::anyhow!( - "no config store registered for id `{store_id}`" - )) + EdgeError::store_extraction( + StoreExtractionReason::UnknownStore, + format!("no config store registered for id `{store_id}`"), + None, + ) })?; let key = key.unwrap_or(&binding.default_key); extract_from_handle::(ctx, &binding.handle, key).await @@ -2942,36 +3033,49 @@ async fn extract_from_handle( where C: DeserializeOwned + AppConfigMeta + Validate + Send + 'static, { - // ConfigStoreError → EdgeError uses the existing `impl - // From for EdgeError` at - // `crates/edgezero-core/src/error.rs:148`, which maps - // Unavailable → ServiceUnavailable (503), InvalidKey → - // BadRequest (400), Internal → Internal (500). Per spec §6.3's - // mapping table. NEVER `map_err(EdgeError::internal)` here — - // that collapses backpressure / bad-key signals into 500s and - // dashboards lose the distinction. - let raw = handle - .get(key) + let mut budget = ConfigExtractionBudget::start(ctx.config_extraction_limits())?; + let read = handle + .get_bounded( + key, + budget.clock(), + budget.deadline(), + budget.remaining_backend_bytes(), + budget.max_blob_bytes(), + ) .await - .map_err(EdgeError::from)? - .ok_or_else(|| EdgeError::config_out_of_date( + .map_err(map_config_store_error)?; + budget.charge_backend(read.backend_bytes)?; + let raw = read.value + .ok_or_else(|| EdgeError::store_extraction( + StoreExtractionReason::MissingBlob, format!("missing typed app-config blob at key `{key}` — run ` config push` for this deploy"), - String::new(), + None, ))?; + budget.charge_value(raw.len())?; let envelope: BlobEnvelope = serde_json::from_str(&raw) - .map_err(|e| EdgeError::internal(anyhow::anyhow!("envelope parse failed: {e}")))?; + .map_err(|_| EdgeError::store_extraction( + StoreExtractionReason::MalformedEnvelope, + "typed app-config envelope is invalid", + None, + ))?; envelope.verify().map_err(|e| { - EdgeError::internal(anyhow::anyhow!("envelope verification failed: {e}")) + EdgeError::store_extraction( + StoreExtractionReason::IntegrityMismatch, + format!("typed app-config integrity check failed: {e}"), + None, + ) })?; let mut data = envelope.into_data(); - secret_walk::(ctx, &mut data).await?; + secret_walk::(ctx, &mut budget, &mut data).await?; use serde::de::IntoDeserializer as _; let cfg: C = serde_path_to_error::deserialize(data.into_deserializer()) - .map_err(EdgeError::config_out_of_date_from_serde)?; + .map_err(EdgeError::store_deserialization_from_serde)?; cfg.validate().map_err(|err| { - EdgeError::config_out_of_date( - err.to_string(), - first_violating_field(&err).unwrap_or_default(), + let field = first_violating_field(&err).unwrap_or_default(); + EdgeError::store_extraction( + StoreExtractionReason::Validation, + "typed app-config failed validation", + (!field.is_empty()).then_some(field), ) })?; Ok(cfg) @@ -2988,9 +3092,11 @@ where { async fn from_request(ctx: &RequestContext) -> Result { let binding = ctx.config_store_default_binding().ok_or_else(|| { - EdgeError::internal(anyhow::anyhow!( - "no default config store registered — check [stores.config] in edgezero.toml" - )) + EdgeError::store_extraction( + StoreExtractionReason::MissingRegistry, + "no default config store registered — check [stores.config] in edgezero.toml", + None, + ) })?; let key = binding.default_key.clone(); extract_from_handle::(ctx, &binding.handle, &key).await.map(AppConfig) @@ -3002,47 +3108,30 @@ where Per spec §12.1 "AppConfig extractor" bullet list: "`named(key)` reads a different key from the same store" — assert it. Plus add a test for `from_store` reading a non-default `[stores.config]` id. -- [ ] **Step 4: Write four extractor error-path tests covering the full §6.3 mapping table.** - -The `ConfigStoreError → EdgeError` path is delegated to the existing `From` impl, but the extractor is the contract surface — tests need to fail loudly if a future refactor accidentally re-introduces `map_err(EdgeError::internal)`. - -```rust - #[test] - fn app_config_extractor_returns_config_out_of_date_on_missing_blob() { - // Mock a ConfigStore whose get() returns Ok(None). - struct EmptyStore; - #[async_trait(?Send)] - impl ConfigStore for EmptyStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { Ok(None) } - } - // ... build a request context with that store via a binding ... - // Run extract; assert Err(EdgeError::ConfigOutOfDate { .. }). - // Assert message contains "missing typed app-config blob" + "run ` config push`". - } - - #[test] - fn app_config_extractor_maps_config_store_unavailable_to_service_unavailable() { - // Mock store returning Err(ConfigStoreError::Unavailable { ... }). - // Run extract. - // Assert Err(EdgeError::ServiceUnavailable { .. }) — NOT - // EdgeError::Internal. Per §6.3 mapping table. - } - - #[test] - fn app_config_extractor_maps_config_store_invalid_key_to_bad_request() { - // Mock store returning Err(ConfigStoreError::InvalidKey { ... }). - // Run extract. - // Assert Err(EdgeError::BadRequest { .. }). - } - - #[test] - fn app_config_extractor_maps_config_store_internal_to_internal() { - // Mock store returning Err(ConfigStoreError::Internal { source }). - // Run extract. - // Assert Err(EdgeError::Internal { .. }) and that the source - // chain still carries the original anyhow::Error. - } -``` +- [ ] **Step 4: Write extractor error-path tests covering the complete §6.3 reason table.** + +Use scripted config/secret stores and malformed envelope/schema fixtures to produce every +known `StoreExtractionReason`: backend failure/unavailability, deadline, integrity, +envelope/key/secret validity, missing blob/registry/secret, schema mismatch, secret backend +unavailability, unknown store, and byte overflow. Assert the returned variant and reason +directly, then assert its wire status/kind/header/path. Include these boundary cases: + +- `get_bounded` receives the one captured deadline and root cap; the secret walk receives + the same deadline and per-secret cap. +- An exactly-at-cap blob/secret and cumulative total succeed; one byte above each fails as + `ValueTooLarge` before parse or insertion. +- A deadline that expires while the host result becomes ready yields `DeadlineExceeded`. +- `ConfigStoreError::{InvalidKey,Unavailable,Internal}` map to + `InvalidKey`, `BackendUnavailable`, and `BackendFailure`, respectively. +- A missing root returns `MissingBlob`; missing/unknown registries are distinct; a missing + secret returns `MissingSecret`; non-UTF-8 secret bytes return `InvalidSecretValue`. +- Envelope parse, unsupported version/discriminator, and SHA mismatch map to + `MalformedEnvelope`, `UnsupportedVersion`, and `IntegrityMismatch`; deserialize and + validator failures map independently to `Deserialization` and `Validation`, with + redacted paths and messages that never include stored or resolved secret values. + +Do not use the generic `From for EdgeError` in this extractor: that impl is +for hand-managed reads and intentionally lacks extraction context. - [ ] **Step 5: Run.** Expected: pass. @@ -3054,7 +3143,9 @@ Run: `cargo test -p edgezero-core --lib extractor::tests::app_config` - Modify: `crates/edgezero-cli/src/config.rs` — `run_config_push_typed` builds ONE `BlobEnvelope`, serialises it, resolves the target key, and calls the EXISTING `adapter.push_config_entries(...)` writer with exactly one logical `(key, envelope_json)` entry. - Modify: each adapter under `crates/edgezero-adapter-*/src/cli.rs` (and `push_sqlite.rs` / `push_cloud.rs` for Spin) ONLY for: (a) any pre-blob-model "flatten + per-leaf loop" preprocessing the writer did, (b) per-platform cap checks on the single logical entry, and (c) Fastly's adapter-private expansion of an oversized logical entry into physical chunk entries plus a root pointer. Adapters do NOT construct `BlobEnvelope` from `C` — the `(String, String)` writer surface stays exactly as it is at `crates/edgezero-adapter/src/registry.rs:277`. -- Modify: `crates/edgezero-adapter-fastly/src/config_store.rs` — runtime `FastlyConfigStore::get` resolves direct-or-pointer values through the same Fastly chunk helper. +- Modify: `crates/edgezero-adapter-fastly/src/config_store.rs` — runtime + `FastlyConfigStore::{get,get_bounded}` resolve direct-or-pointer values through the same + Fastly chunk helper; the bounded path counts pointer plus chunk bytes. **Ownership rationale (round-26 H-2, updated for Fastly chunking):** the existing `push_config_entries` trait method takes `entries: &[(String, String)]` — a sequence of key/value pairs. Adapters don't see `C` or `cfg`. Earlier draft of this task told each adapter to build the envelope from `cfg`, which would have required widening the trait to take `C` (breaking the adapter abstraction) OR duplicating envelope-construction code across all four adapters. The clean ownership remains: **CLI builds the envelope once, then hands the writer ONE logical entry: `(resolved_key, envelope_json)`.** Fastly may expand that one logical entry into multiple physical config-store entries internally; every other adapter writes the logical entry directly. @@ -3144,7 +3235,10 @@ The existing cap check at line 90 (`if pair.len() >= MAX_ARGV_BYTES_PER_INVOCATI - Fastly local push writes root pointer plus literal dotted chunk keys under `[local_server.config_stores..contents]`. - Fastly local dry-run reports chunking and does not edit `fastly.toml`. - Simulated Fastly failure before the pointer write leaves the previous root active; this is tested at helper/writer level by asserting the root write is last. -- Runtime `FastlyConfigStore::get` resolves a direct value unchanged and reconstructs a pointer value by fetching chunks from the same store. +- Runtime `FastlyConfigStore::{get,get_bounded}` resolve a direct value unchanged and + reconstruct a pointer value by fetching chunks from the same store. The bounded path + threads one deadline/remaining allowance through every fetch and reports the full + guest-visible byte count. - [ ] **Step 5: Run.** @@ -3947,7 +4041,7 @@ app-demo handlers in the same commit). secret walk per Model A (#[secret] / #[secret(store_ref)] / #[secret(store_ref = \"field\")]), serde_path_to_error deserialise, validator::Validate::validate. Missing blob → - EdgeError::ConfigOutOfDate per Q3 (d). + EdgeError::StoreExtraction with reason MissingBlob per Q3 (d). - §8.2 config push rewrite: single logical envelope per [stores.config] root key per adapter. Per-adapter writers (Axum file map / Cloudflare bulk put with namespace-id+remote / @@ -4507,7 +4601,11 @@ trap - EXIT assert_contains "$result" "large-fastly-blob" "fastly chunk-pointer runtime read" ``` -The actual fixture should generate the repeated text programmatically rather than checking in an enormous TOML file. The important assertion is that runtime `FastlyConfigStore::get` follows the pointer and returns the reconstructed normal envelope to the core extractor. +The actual fixture should generate the repeated text programmatically rather than checking +in an enormous TOML file. Assert that runtime `FastlyConfigStore::get_bounded` follows the +pointer, reports pointer plus chunk bytes, and returns the reconstructed normal envelope to +the core extractor; the hand-managed `get` compatibility path returns the same logical +value. **Spin Cloud exception.** Spin Cloud's `Unsupported` read-back per spec §8.3 means the smoke script can't `config diff` against Cloud, but the push + extractor path is the same shape; the smoke loop's `spin:--local` row exercises Spin's SQLite read path (per round-29 H-1's path-resolver fixtures, the SQLite resolution already has unit-test coverage; the smoke just adds the end-to-end composition check). The Spin Cloud branch is exercised by the SEPARATE block below. @@ -4577,7 +4675,7 @@ Mapping spec §s → tasks: | §3.3.2 (writer behaviour + structural checks) | B11 | | §3.3.3 (extractor secret walk) | C1 | | §3.3.4 (blob layout) | C2 (envelope creation) | -| §3.3.6 (SecretError → EdgeError mapping) | C1 (`map_secret_error`) | +| §3.3.6 (SecretError → EdgeError mapping) | B2.5, B13.5, C1 (`map_secret_error`) | | §3.3.7 (sha + secret-key interaction) | A2 + C1 | | §3.3.8 (push vs runtime validation) | B9 + C4 | | §4.1 (envelope shape) | A4 | @@ -4592,10 +4690,11 @@ Mapping spec §s → tasks: | §6.2 (explicit-key `named(key)` form) | C1 (Step 2 inherent impl) | | §6.2.1 (cross-store `from_store(id, key)` form) | C1 (Step 2 inherent impl) | | §6.2.2 (runtime validation) | C1 (`cfg.validate()`) | -| §6.3 (errors — bullet list) | B2, B3, C1 | -| §6.3.1 (`ConfigOutOfDate` body shape) | B2, B3, B4 | +| §6.3 (typed extraction errors) | B2, B2.5, B3, B4, C1 | +| §6.3.1 (`config_out_of_date` wire shape) | B2, B2.5, B3, B4 | +| §6.3.2 (bounded/cancellable reads) | B13.5, C1 | | §6.4 (no caching) | C1 (re-reads every call) | -| §6.5 (existing `ConfigStore` trait stays) | (no code work — kept) | +| §6.5 (existing unbounded store methods stay) | B13.5 (preserved beside bounded methods) | | §7.x (SHA discussion) | A2 (canonical form) | | §8.1 (config diff) | D1, D2 | | §8.2 (config push + consent) | C2, C3, C4 | diff --git a/docs/superpowers/plans/2026-07-04-edgezero-app-macro-state.md b/docs/superpowers/plans/2026-07-04-edgezero-app-macro-state.md index f8a44f632..ea0668778 100644 --- a/docs/superpowers/plans/2026-07-04-edgezero-app-macro-state.md +++ b/docs/superpowers/plans/2026-07-04-edgezero-app-macro-state.md @@ -202,7 +202,7 @@ pub struct DemoState { /// Returns the shared app state, referenced by `app!(..., state = crate::app_state())`. /// /// IMPORTANT: `app!(state = )` emits this call inside the macro-generated -/// `build_router()`, which every adapter's `run_app` invokes via `A::build_app()` +/// `build_router()`, which every adapter's `run_app` invokes via `App::build::(platform)` /// — once at startup for long-lived runtimes (Axum), but **once per request** on /// Fastly Compute (each request is a fresh Wasm instance). So `app_state()` must /// be **cheap**: build the heavy state once and hand out clones. Here a diff --git a/docs/superpowers/plans/2026-07-04-edgezero-fastly-dispatch-fidelity.md b/docs/superpowers/plans/2026-07-04-edgezero-fastly-dispatch-fidelity.md index 03070b3ad..b19962c88 100644 --- a/docs/superpowers/plans/2026-07-04-edgezero-fastly-dispatch-fidelity.md +++ b/docs/superpowers/plans/2026-07-04-edgezero-fastly-dispatch-fidelity.md @@ -1,18 +1,21 @@ # EdgeZero P0-C — Fastly `run_app` Dispatch Fidelity Implementation Plan +> **Status:** Implemented and superseded. The 2026-09-15 consumer-alignment plan replaces the +> request-only Fastly hook below with the closed request/response lifecycle API. + > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Bring Fastly's `run_app` to parity with hand-written custom dispatch: preserve multi-value response headers (`Set-Cookie`), let an app opt out of the adapter's logger init, and add a pre-dispatch hook that reads raw-`fastly::Request` signals (JA4 / H2 / client IP) into the core request's extensions. **Architecture:** Three independent Fastly-adapter changes plus one cross-adapter `Hooks` method. C1 swaps `set_header`→`append_header` on the (fresh) response and fixes the proxy-response conversion to append per value. C2 adds a platform-neutral `Hooks::owns_logging()` that every adapter's entrypoint gates its logger init on, with an `app!(owns_logging = …)` macro argument. C3 adds `run_app_with_request_extensions` that runs an app closure against a scratch `Extensions` **before** request conversion, then `extend`s it into the core request. -**Tech Stack:** Rust 1.95, edition 2021. `edgezero-adapter-fastly` (behind the `fastly` feature), `edgezero-core` (`Hooks` trait, `http::Extensions`), `edgezero-macros` (`app!`). `http` crate `HeaderMap`/`Extensions` via `edgezero_core::http`. +**Tech Stack:** Rust 1.95, edition 2024. `edgezero-adapter-fastly` (behind the `fastly` feature), `edgezero-core` (`Hooks` trait, `http::Extensions`), `edgezero-macros` (`app!`). `http` crate `HeaderMap`/`Extensions` via `edgezero_core::http`. **Source spec:** `docs/superpowers/specs/2026-07-03-edgezero-fastly-dispatch-and-appstate-design.md` (P0-C), verified against `65afbd3`. ## Global Constraints -- **Rust 1.95.0**, edition 2021. +- **Rust 1.95.0**, edition 2024. - **Strict clippy gate** — `[workspace.lints.clippy] restriction = { level = "deny" }`. Every restriction lint is an ERROR. The ones that bite here: - `missing_trait_methods` — an `impl Trait` may not inherit a defaulted method. Adding `Hooks::owns_logging()` forces the **macro-emitted** `impl Hooks` to emit it explicitly (the two in-file `Hooks` test stubs already carry `#[expect(clippy::missing_trait_methods)]`, so they need no change). - `arbitrary_source_item_ordering` — module items / struct fields / enum variants must be alphabetical; place new test fns and struct fields in the correct position, don't append. @@ -27,7 +30,7 @@ ``` (cd crates/edgezero-adapter-fastly && cargo test --features fastly --lib ) ``` - This builds to `wasm32-wasip1` and runs the test binary under Viceroy (0.17.0, pinned in `.tool-versions`), which provides the hostcalls — so `FastlyResponse::from_status`/`append_header`/`get_header_all`, `FastlyRequest::new`/`get_url_str`, and even `get_client_ip_addr` all work at runtime. **Every `cargo test -p edgezero-adapter-fastly --features fastly …` command in the tasks below MUST be run in this `(cd crates/edgezero-adapter-fastly && cargo test --features fastly …)` form** — the commands are written the short way for brevity. + This builds to `wasm32-wasip1` and runs the test binary under Viceroy (0.21.0, pinned in `.tool-versions`), which provides the hostcalls — so `FastlyResponse::from_status`/`append_header`/`get_header_all`, `FastlyRequest::new`/`get_url_str`, and even `get_client_ip_addr` all work at runtime. **Every `cargo test -p edgezero-adapter-fastly --features fastly …` command in the tasks below MUST be run in this `(cd crates/edgezero-adapter-fastly && cargo test --features fastly …)` form** — the commands are written the short way for brevity. - `crates/edgezero-adapter-fastly/tests/contract.rs` is `#![cfg(all(feature = "fastly", target_arch = "wasm32"))]` (the same wasm/Viceroy path). CI runs it via `cargo test -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 --test contract`. - `cargo clippy`/`cargo check --features fastly` type-check on the **host** (no link), so clippy runs from the workspace root as usual. - **CI gates (all must pass):** @@ -818,7 +821,7 @@ where let endpoint = logging.endpoint.as_deref().unwrap_or("stdout"); init_logger(endpoint, logging.level, logging.echo_stdout)?; } - let app = A::build_app(); + let app = App::build::(FASTLY_PLATFORM)?; request::dispatch_with_registries( &app, req, diff --git a/docs/superpowers/plans/2026-07-10-outbound-http-implementation.md b/docs/superpowers/plans/2026-07-10-outbound-http-implementation.md new file mode 100644 index 000000000..74bcab568 --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-outbound-http-implementation.md @@ -0,0 +1,150 @@ +# Outbound HTTP Implementation Index + +> **Status:** Phase index, not an executable implementation plan. +> +> The authoritative contract is +> [`2026-05-21-outbound-http-design.md`](../specs/2026-05-21-outbound-http-design.md). +> This file deliberately does not duplicate adapter algorithms, capability matrices, or +> migration details from that specification. Read the relevant spec sections against the +> current tree before authoring each phase plan. + +## Current Baseline + +- Rust 1.95, edition 2024, resolver 2. +- Four runtime adapters: Axum, Cloudflare, Fastly, and Spin SDK 7 / WASI HTTP 0.3. +- Adapter dispatch has both `execute(..)` and `execute_capture(..)` entry points. +- The action set includes staged deploy, version emission, healthcheck, and rollback in + addition to build/serve/deploy/auth. +- The outbound design declares the **outbound** capability matrix. Its rows and + footnotes in spec §3.5.2 are the only authority for those support levels; other specs may + add non-outbound cells to the shared enum. + +## Locked Scope + +- The public `proxy` API becomes `outbound` without compatibility aliases. Templates, + public docs, generated projects, and `examples/app-demo` migrate in the same implementation + series. +- Core remains runtime-independent and WASM-compatible: no Tokio, reqwest, Fastly, worker, + or Spin SDK dependency enters `edgezero-core`. +- `EdgeError` gains `BadGateway { reason: BadGatewayReason }`, attributed + `GatewayTimeout`, and the distinct + `ResponseTooLarge { reason: ResponseLimitReason }` outcome. Response overflow is not + collapsed into `BadGateway`, and reason/provenance fields remain outside the JSON wire + envelope. +- `DispatchBudget` and `dispatch_budget` land together after `OutboundRequest` exposes its + private budget-input carrier. They are not part of Phase 1a. +- Capability types are owned by `edgezero-core::manifest`. Consequently, + `edgezero-adapter` adds a direct dependency on `edgezero-core` for the registry trait's + public capability signature. +- Outbound capability enforcement applies to construction/deployment of the current + runtime: build, serve, deploy, staged deploy, and demo. New outbound-scoped + `execute_runtime(..)` and `execute_capture_runtime(..)` entry points accept an owned + runtime and no separate adapter/action identity; they derive both values from that + runtime and gate exactly once before shell or registry dispatch. Existing operational + dispatch remains unchanged for auth, version emission, healthcheck, and rollback; + provision and config are also outside this gate. +- Spin request-component setters and request-option setters have different error types. + `RequestOptionsError::NotSupported` retains the outer monotonic deadline race and logs a + BestEffort degradation; `Immutable` and `Other(..)` are internal setup failures. +- The inbound `RequestContext` body-state migration is owned by + [`2026-08-22-inbound-body-design.md`](../specs/2026-08-22-inbound-body-design.md). The + outbound spec depends only on its `into_request()` contract. Ingress admission, + request-start/read deadlines, and raw framing rejection are not prerequisites for Phase + 1a and must not be implemented opportunistically in an outbound phase. +- The downstream response-write lifetime starts after a core `Response` reaches an adapter + converter. Adapter phases may implement the bounded fallback required by this outbound + design, but must not claim a general write deadline, native abort, or exactly-once egress + completion until a dedicated response-egress contract is reviewed. +- GitHub accepts `workflow_dispatch` events only for workflow files already present on the + default branch ([GitHub workflow syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#onworkflow_dispatch)). Before Phase 4 begins, land a standalone infrastructure bootstrap PR that + adds the inert, protected, exact-SHA dispatcher shells + `.github/workflows/outbound-cloudflare-deployed.yml` and + `.github/workflows/outbound-fastly-characterization.yml`. Those default-branch workflows + check out only a maintainer-reviewed `commit_sha` and execute the probe driver from that + tree. This bootstrap is the sole exception to the branch-only no-intermediate-merge rule: + it publishes no API, capability cell, runtime behavior, or secret-bearing automatic trigger. + +## Phase Sequence + +| Phase | Scope | Plan status | +| --- | --- | --- | +| 0 | Bootstrap inert default-branch exact-SHA probe dispatchers and provision protected disposable environments/origin | Not landed; required only before promoting provider capabilities from `BestEffort`: [`2026-09-06-outbound-http-phase0-probe-bootstrap.md`](2026-09-06-outbound-http-phase0-probe-bootstrap.md) | +| 1a | Add `BadGatewayReason`, `BudgetSource`, typed `BadGateway`, attributed `GatewayTimeout`, `Deadline`, and the three deadline constants | Implemented; plan: [`2026-07-10-outbound-http-phase1a-error-time.md`](2026-07-10-outbound-http-phase1a-error-time.md) | +| 1b | Add the pinned direct `url` dependency, outbound request/response types, one-time canonical URI construction, resource-limit builders, `DispatchBudget`, and `dispatch_budget` in a buildable dependency order | Implemented; plan: [`2026-09-06-outbound-http-phase1b-core-types-budget.md`](2026-09-06-outbound-http-phase1b-core-types-budget.md) | +| 2 | Typed body errors, typed response-limit errors, encoded/header/Brotli/chunk controls, bounded/deadline-aware drains, normalization, and shared decoder primitives | Implemented; plan: [`2026-09-06-outbound-http-phase2-body-response-limits.md`](2026-09-06-outbound-http-phase2-body-response-limits.md) | +| 3 | Manifest capability declarations, fail-closed adapter metadata contract, paired target/contract resolver, CLI gates, and selected-component Spin host-drift validation | Implemented; plan: [`2026-09-06-outbound-http-phase3-capabilities-cli.md`](2026-09-06-outbound-http-phase3-capabilities-cli.md) | +| 4 | Axum and Cloudflare outbound implementations, response-converter scheduling, host-event fairness, and contract tests | Implemented; deployed Cloudflare promotion evidence remains external: [`2026-09-06-outbound-http-phase4-axum-cloudflare.md`](2026-09-06-outbound-http-phase4-axum-cloudflare.md) | +| 5 | Spin hand-built WASI HTTP request/response state machines, completion-error mapping, and cooperative raw/decoded-input fairness | Implemented with repository-pinned SDK-resource execution: [`2026-09-06-outbound-http-phase5-spin.md`](2026-09-06-outbound-http-phase5-spin.md) | +| 6 | Fastly dispatch/harvest engine, dynamic backends, timers, and test seams | Implemented; deployed Fastly characterization remains external: [`2026-09-06-outbound-http-phase6-fastly.md`](2026-09-06-outbound-http-phase6-fastly.md) | +| 7 | Templates, `app-demo`, public docs, generated-project checks, hard legacy-API removal, and remaining live-host characterization | Implemented; live-host promotion evidence remains external: [`2026-09-06-outbound-http-phase7-migration-docs.md`](2026-09-06-outbound-http-phase7-migration-docs.md) | + +After the inert workflow bootstrap above, the phase numbers after 1a are organizational +guidance, not permission to split an +invariant across unbuildable commits. Phases 1b and 2 stage the new core module alongside +the old one; Phases 4-6 migrate adapters; Phase 7 migrates external consumers and deletes +the old module. That temporary dual surface is branch-only scaffolding: no intermediate +state is releasable, there are no aliases between old and new names, and the series is not +mergeable until Phase 7's scoped legacy-symbol gate passes. Each task still leaves its +current repository/worktree buildable. + +Phase 3 leaves each in-tree adapter on the registry method's `Unsupported` default. The +exact Axum/Cloudflare, Spin, and Fastly matrix rows are published only in Phases 4, 5, and +6 respectively, in the same commit series as their passing behavior contracts and any +required host evidence. This prevents the CLI from accepting a capability before the +runtime implements it. + +Phases 4-6 include each adapter's direct dependencies, independent `test-utils` feature, +native contract tests, and explicit native/WASM CI activation from spec §5.5. These gates +land with their adapter implementation rather than waiting for phase 7. Cloudflare's raw +subrequest encoding bridge and host-observed cancellation/encoding tests belong to phase 4; +Workers-only response conversion (including `Headers.getAll("set-cookie")`) runs in the +locked workerd fixture, while browser WASM covers only portable bridge behavior. +Phase 2 includes the shared decoder's gzip-member/native-completion contract, transport-side +encoded cap, opt-in rechunker, response-header caps, and pre-allocation Brotli-window check +(§3.4.1/§3.4.5). +Phase 4 includes Axum's connection-owned Hyper HTTP/1 response-delivery boundary and +Cloudflare's host-event yield quotas, frozen-clock regressions, manual response-body encoding, +and null-body 205 branch (§4.1/§4.2). Provider capability promotion still requires the deployed +evidence named by the authoritative design. +Phase 5 Task 0 must verify the Spin SDK-resource runner/harness compatibility gate in spec +§5.5; HTTP/Preview 3 registration flags alone are not execution evidence. Tasks 1-6 remain +blocked until the committed crate-local runner executes the named real-SDK resource tests +with a nonzero count. After that proof, the implementation must route `client::send`, +`request_done`, and response completion through the same typed classifier and make +continuously-ready raw/decoded input yield cooperatively. Phase 6 +includes known-`SendErrorCause` boundary tests and the buffered-batch versus streamed-upload +cleanup distinction (§4.3). These requirements do not change Phase 1a. + +## Plan-Authoring Gate + +Before writing any later phase plan: + +1. Re-read the relevant spec sections and inspect the current implementations and pinned + SDK source. +2. Enumerate every touched file, including Cargo manifests and generated/template surfaces. +3. Keep platform behavior at adapter boundaries and portable value/arithmetic layers in + core. +4. Define executable test seams, their Cargo features, target gates, and CI commands using + spec §5.5. Confirm each command executes tests rather than reporting zero tests behind + a whole-file platform gate, and validate SDK-resource runner/harness compatibility. + Test constructible known SDK enum variants directly; do not fabricate nonexistent variants of + `#[non_exhaustive]` enums with unsafe code or test-only public variants. +5. Separate no-runtime contract tests from live-host characterization. A mock must not be + used to claim host cancellation or wire behavior. +6. Include the resource-limit precedence matrix, every typed reason value, multi-member + gzip drain-to-native-EOF cases, and the platform's exact abort/completion owner. A + rechunker test may claim bounded emitted item size, not bounded transport allocation or + process RSS. +7. Run the repository gates required by `CLAUDE.md`, plus generated-project, + `examples/app-demo`, adapter-target, and documentation checks for phases that touch those + surfaces. Generated-project verification explicitly runs + `cargo test -p scaffold-probe-core --lib`; a build-only command is insufficient. + +## Readiness + +Phases 1a-7 are implemented on PR 275. Phase 0's protected default-branch probe +dispatchers and the resulting deployed-provider observations remain external promotion +evidence; their absence keeps the documented provider limitations at `BestEffort` and does +not turn local or WASM-runner tests into host-certification evidence. The master design +remains the normative behavior contract; these phase plans preserve the implementation +history without replacing it. diff --git a/docs/superpowers/plans/2026-07-10-outbound-http-phase1a-error-time.md b/docs/superpowers/plans/2026-07-10-outbound-http-phase1a-error-time.md new file mode 100644 index 000000000..58269f505 --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-outbound-http-phase1a-error-time.md @@ -0,0 +1,814 @@ +# Outbound HTTP — Phase 1a: typed `EdgeError` 502/504 + `time.rs` primitives + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax. + +**Goal:** Land the **additive, no-new-dependency on the current baseline** core primitives from the outbound-HTTP spec ([`2026-05-21-outbound-http-design.md`](../specs/2026-05-21-outbound-http-design.md)): `EdgeError::BadGateway { reason: BadGatewayReason }`, `GatewayTimeout { cause: BudgetSource }`, and the `edgezero-core::time` module's `Deadline` + budget constants. `BadGatewayDecodeReason`, `BadGatewayReason`, and `BudgetSource` land in `error.rs`, Task 1, because the variants name them; `dispatch_budget` remains Phase 1b. Neither task touches the `proxy → outbound` rename or `Body`, so each task keeps `cargo test --workspace` green. **Scope caveat:** per-task verification is a deliberate local subset (Task 3); generated projects, `app-demo`, and expanded adapter-specific WASM matrices remain CI backstops. + +**Architecture:** `edgezero-core` only. Additive: two new `EdgeError` variants, three non-exhaustive reason/provenance enums, and a new `time` module. The `EdgeError` enum is `#[non_exhaustive]`, but matches inside its defining crate remain exhaustive and must gain both arms. `BadGatewayReason` distinguishes pre-response unreachability from later transport/protocol failures, while `BadGatewayDecodeReason` preserves the EdgeZero-owned codec identity. No reason/cause field is serialized. `message()` preserves Rust-side diagnostics, while HTTP conversion emits fixed `"bad gateway"` / `"gateway timeout"` category strings. No adapter, CLI, or app-demo change. **`DispatchBudget` and `dispatch_budget` are both deferred to Phase 1b**; Phase 1a lands `Deadline` + constants only. + +**Round-59 scope alignment:** the master spec's response resource limits, canonical URL +parser, adapter scheduling/completion rules, ingress ownership, and response-egress gate are +later work. The corrected `Body::Stream` constructors, +Fastly buffered-upload caveat, Axum/Cloudflare upload-pull boundary checks, and Spin +exchange state machine are later outbound/adapter work. They do not alter any Phase 1a +task, file, API, or verification command. In particular, this plan must not opportunistically +change `Body`, `proxy`, or an adapter while landing the error/time primitives. + +**Tech Stack:** Rust 1.95 (edition 2024), `thiserror`, `serde_json`, `web-time` (behind the public `MonotonicInstant` alias), `futures::executor::block_on` for async tests. + +> **Post-Phase 1a note:** later ingress/outbound hardening added the EdgeZero-owned +> `MonotonicClock` handle and made `Deadline::{is_expired_at,remaining_at}` public so one +> application clock can be paired across deferred work. The implementation excerpt below records +> the smaller Phase 1a landing shape; the current API is normative in the outbound design. + +## Global Constraints (from the master design and phase index) + +- **WASM-first:** no `tokio`/runtime deps; public APIs use + `edgezero_core::time::MonotonicInstant`, backed internally by `web_time::Instant`, not + `std::time::Instant`. Core stays `default-features = false`. +- **Colocated tests** (`#[cfg(test)]` same file); async tests use `futures::executor::block_on`. +- **Verbatim constants:** `DEFAULT_NO_DEADLINE_BUDGET = 30 s`, `DEADLINE_FAR_FUTURE = 7 days`, `BATCH_DISPATCH_SLACK_MAX = 25 ms`. +- **CI gates must stay green:** `cargo fmt --all -- --check`; `cargo clippy --workspace --all-targets --all-features -- -D warnings`; `cargo test --workspace --all-targets`; `cargo check --workspace --all-targets --features "fastly cloudflare spin"`; `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin`. +- **Verified against the current worktree's `crates/edgezero-core/src/error.rs`** (re-confirm with the compiler-driven Step 6 rather than trusting line numbers): `EdgeError` today has variants `BadRequest, ConfigOutOfDate, Internal, MethodNotAllowed, NotFound, NotImplemented, ServiceUnavailable, Validation`. The two new arms must be added to **nine** exhaustive matches: five implementation matches (`inner`, `kind_str`, `message`, `status`, and `IntoResponse`'s `field_path_opt`) plus four explicit `ConfigOutOfDate` test matches. The compiler remains the source of truth. Three per-variant tests gain rows for both variants. Reason/provenance tests additionally cover every known enum value and wire-shape isolation. `web-time` presence is confirmed in Task 0. +- **`cargo test` accepts only ONE positional filter** — `cargo test -p X a b` fails with `unexpected argument 'b'` (verified). Use a single common substring or two separate commands. +- **The Clippy gate is STRICT — read this before writing any code.** The root `Cargo.toml` sets `restriction = { level = "deny", priority = -1 }`, and the following are **not** allow-listed, so they are hard errors in **production** code: + - `missing_inline_in_public_items` → **every public fn needs `#[inline]`** (error.rs already carries 14). + - `min_ident_chars` → no single-char idents (`d` → `duration`). + - `arithmetic_side_effects` → **no bare `+` / `-`** on `Instant`/`Duration`; use `checked_add` / `checked_duration_since`. + - `expect_used`, `unwrap_used`, `as_conversions` → forbidden in production; use `?`/`ok_or`/`From`/`TryFrom`. + - **`unseparated_literal_suffix`** → integer suffixes need an underscore: `502_u16`, not `502u16` (verified: `502u16` errors; the opposing `separated_literal_suffix` is allow-listed, so the underscore form is the one that passes). + - **`arbitrary_source_item_ordering`** → **items must be ALPHABETICAL** — consts, enum **variants**, and impl fns alike (verified). This is why `error.rs`'s variants and methods are already alphabetized. Insert new items **in place**; never append. + - **`duration_suboptimal_units`** (pedantic; CI runs `-D warnings`, so it *fails the build*) → use the largest readable unit: `Duration::from_hours(168)` not `from_secs(7*24*60*60)`; `Duration::from_mins(1)` not `from_secs(60)`. + - **Verified end-to-end:** this exact `Deadline` code + these constants compile **clean** under the repo's full lint table (`restriction = deny` + `pedantic` + the real allow-list). `std_instead_of_core`/`std_instead_of_alloc` **are** allow-listed, so `use std::time::Duration;` is fine. + - **In TESTS**, the root `clippy.toml` sets `allow-expect-in-tests = true`, `allow-unwrap-in-tests = true`, `allow-panic-in-tests = true`, `allow-indexing-slicing-in-tests = true` — so `.expect(..)` in tests is fine. **`arithmetic_side_effects` is NOT test-exempt**, which is why the tests below use `checked_add(..).expect("no overflow")` rather than `base + dur`. + +--- + +### Task 0: Confirm the `web-time` dependency + +**Files:** Inspect `crates/edgezero-core/Cargo.toml` + +- [ ] **Step 1: Check whether `web-time` is already a dependency** + +Run: `rg -n 'web-time|web_time' crates/edgezero-core/Cargo.toml` +Expected: a line like `web-time = { workspace = true }`. + +- [ ] **Step 2: If absent, STOP and update/re-review this plan** + +Do not add the dependency as part of this task. The plan's locked premise is that +`web-time` already exists in both the workspace and `edgezero-core`; silently adding it +would contradict the no-new-dependency goal and turn Task 0 from verification into +implementation. + +- [ ] **Step 3: Verify it compiles** — Run: `cargo check -p edgezero-core` — Expected: `Finished`. + +--- + +### Task 1: typed `EdgeError::BadGateway` (502) + `GatewayTimeout` (504) + +**Files:** +- Modify: `crates/edgezero-core/src/error.rs` (enum + constructors + **9 exhaustive matches: 5 impl + 4 test panic-arms**) +- Test: `crates/edgezero-core/src/error.rs` (colocated `#[cfg(test)]`) + +**Interfaces:** +- Produces `bad_gateway(msg)` with `reason: BadGatewayReason::Unspecified`, + `bad_gateway_with_reason(msg, reason)`, `gateway_timeout(msg)` with + `cause: BudgetSource::Unspecified`, and `gateway_timeout_caused(msg, cause)`. + `BadGatewayReason::{Decode(BadGatewayDecodeReason), Protocol, Transport, Unreachable, + Unspecified}`, `BadGatewayDecodeReason::{Brotli, Gzip, Json}`, and + `BudgetSource::{BatchDeadline, Default, PerCallTimeout, Unspecified}` are + `#[non_exhaustive]`, `Clone + Copy + Debug + Eq`. Consumers inspect fields by matching the + variant. The JSON envelope remains `{ "error": { "status", "kind", "message" } }` with + no `field_path`, `reason`, or `cause`. +- `BudgetSource::Unspecified` means no EdgeZero budget source was proven. It covers both + timeout APIs outside a dispatch budget and an independently controlled provider timeout + observed before the selected absolute dispatch deadline expired. A provider phase timer + explicitly configured from the selected budget may retain that budget's source even when + it intentionally fires before the total deadline; an unconfigured DNS/write/host timeout + may not. Adapters must not stamp a configured source onto an early provider event merely + because its SDK error name contains `Timeout`; the outbound adapter phases own the exact + per-variant classification rule. + +- [ ] **Step 1: Write the failing tests (surface, typed fields, and wire isolation)** + +The existing `#[cfg(test)] mod tests` already imports `StatusCode`, `CONTENT_TYPE`, `HeaderValue`, `str` and does `use super::*;`, and has a `parse_body(response) -> serde_json::Value` helper (`tests::parse_body`). Add — **no new imports** (re-importing under `-D warnings` fails): + +This code is **pre-wrapped to rustfmt's canonical form at the final nesting depth** (inside `mod tests` → `fn` → `for`). Written as one-liners, the array-of-tuples rows and the message-bearing `assert!` exceed `max_width = 100` once indented into the test module and rustfmt rewraps them — which would surface as a diff at the Task 3 `cargo fmt --all -- --check` gate. (Step 8's `cargo fmt` would rewrap them for you, but the plan shows the landed form.) + +```rust +#[test] +fn bad_gateway_and_gateway_timeout_surface() { + for (err, code, msg) in [ + ( + EdgeError::bad_gateway("upstream refused"), + StatusCode::BAD_GATEWAY, + "upstream refused", + ), + ( + EdgeError::gateway_timeout("deadline expired"), + StatusCode::GATEWAY_TIMEOUT, + "deadline expired", + ), + ] { + assert_eq!(err.status(), code); + assert_eq!(err.message(), msg); + assert!(err.inner().is_none()); + // Display must render the new variants (not just the pre-existing ones). Assert + // the message is present rather than pinning an exact format, so this survives a + // format tweak while still proving `Display` covers `BadGateway`/`GatewayTimeout`. + assert!(err.to_string().contains(msg)); + } +} + +#[test] +fn bad_gateway_and_gateway_timeout_json_shape() { + for (err, code, kind, msg) in [ + ( + EdgeError::bad_gateway("nope"), + 502_u16, + "bad_gateway", + "bad gateway", + ), + ( + EdgeError::bad_gateway_with_reason("nope", BadGatewayReason::Protocol), + 502_u16, + "bad_gateway", + "bad gateway", + ), + ( + EdgeError::bad_gateway_with_reason("nope", BadGatewayReason::Transport), + 502_u16, + "bad_gateway", + "bad gateway", + ), + ( + EdgeError::bad_gateway_with_reason("nope", BadGatewayReason::Unreachable), + 502_u16, + "bad_gateway", + "bad gateway", + ), + ( + EdgeError::gateway_timeout("late"), + 504_u16, + "gateway_timeout", + "gateway timeout", + ), + // Every OTHER cause too — wire-isolation must hold for ALL FOUR BudgetSource + // values, so a conditional serializer cannot leak `cause` for any of them + // (BatchDeadline / Default / PerCallTimeout / Unspecified are all covered). + ( + EdgeError::gateway_timeout_caused("late", BudgetSource::PerCallTimeout), + 504_u16, + "gateway_timeout", + "gateway timeout", + ), + ( + EdgeError::gateway_timeout_caused("late", BudgetSource::BatchDeadline), + 504_u16, + "gateway_timeout", + "gateway timeout", + ), + ( + EdgeError::gateway_timeout_caused("late", BudgetSource::Default), + 504_u16, + "gateway_timeout", + "gateway timeout", + ), + ] { + let response = err.into_response().expect("response"); + assert_eq!(response.status().as_u16(), code); + let body_json = parse_body(response); // existing helper -> serde_json::Value + assert_eq!(body_json["error"]["status"], code); + assert_eq!(body_json["error"]["kind"], serde_json::Value::from(kind)); + assert_eq!(body_json["error"]["message"], serde_json::Value::from(msg)); + assert!( + body_json["error"].get("field_path").is_none(), + "502/504 carry no field_path" + ); + // Typed classification is Rust-side only; the JSON must not leak either field. + assert!( + body_json["error"].get("reason").is_none(), + "reason is not part of the wire shape" + ); + assert!( + body_json["error"].get("cause").is_none(), + "cause is not part of the wire shape" + ); + } +} + +#[test] +fn bad_gateway_decode_reason_is_not_serialized() { + for reason in [ + BadGatewayDecodeReason::Brotli, + BadGatewayDecodeReason::Gzip, + BadGatewayDecodeReason::Json, + ] { + let err = EdgeError::bad_gateway_with_reason( + "nope", + BadGatewayReason::Decode(reason), + ); + let response = err.into_response().expect("response"); + let body_json = parse_body(response); + assert_eq!(body_json["error"]["status"], 502_u16); + assert_eq!(body_json["error"]["kind"], "bad_gateway"); + assert_eq!(body_json["error"]["message"], "bad gateway"); + assert!(body_json["error"].get("reason").is_none()); + } +} + +#[test] +fn bad_gateway_reason_is_typed() { + let EdgeError::BadGateway { reason, .. } = EdgeError::bad_gateway("x") else { + panic!("expected BadGateway"); + }; + assert_eq!(reason, BadGatewayReason::Unspecified); + + for expected in [ + BadGatewayReason::Decode(BadGatewayDecodeReason::Brotli), + BadGatewayReason::Decode(BadGatewayDecodeReason::Gzip), + BadGatewayReason::Decode(BadGatewayDecodeReason::Json), + BadGatewayReason::Protocol, + BadGatewayReason::Transport, + BadGatewayReason::Unreachable, + BadGatewayReason::Unspecified, + ] { + let EdgeError::BadGateway { reason, .. } = + EdgeError::bad_gateway_with_reason("x", expected) + else { + panic!("expected BadGateway"); + }; + assert_eq!(reason, expected); + } +} + +// The typed timeout-attribution contract (§3.3.2 / §3.4.3). WITHOUT these, a mis-wired +// constructor that dropped the cause, or that always stored the wrong variant, would pass +// the suite. These are the exact assertions compile-verified in the errsurface scaffold. +#[test] +fn bare_gateway_timeout_is_unspecified() { + // `let-else`, NOT `match … { other => panic! }`: a catch-all arm over the + // multi-variant `EdgeError` trips the denied `clippy::wildcard_enum_match_arm` + // (compile-verified against the real enum shape under edition 2024). `let-else` + // has no wildcard arm. + let EdgeError::GatewayTimeout { cause, .. } = EdgeError::gateway_timeout("x") else { + panic!("expected GatewayTimeout"); + }; + assert_eq!(cause, BudgetSource::Unspecified); +} +#[test] +fn gateway_timeout_caused_preserves_cause() { + // Loop var is `expected` (NOT a single char — `min_ident_chars`). **Includes + // `Unspecified`** so an impl that special-cased the bare constructor's cause is + // caught. `let-else` again (no `wildcard_enum_match_arm`). rustfmt-canonical + // (edition 2024) wrapped form, verified. + for expected in [ + BudgetSource::BatchDeadline, + BudgetSource::Default, + BudgetSource::PerCallTimeout, + BudgetSource::Unspecified, + ] { + let EdgeError::GatewayTimeout { cause, .. } = + EdgeError::gateway_timeout_caused("x", expected) + else { + panic!("expected GatewayTimeout"); + }; + assert_eq!(cause, expected); + } +} +``` + +In the **same red test edit**, extend all three existing per-variant matrices. These rows +reference the not-yet-created constructors, just like the focused tests above, so they belong +in the same compile-red state rather than being deferred until after implementation. The +following are three labeled insertion fragments for their named existing test functions, +not one contiguous block to paste at a single location: + +```rust + // `kind_strings_per_variant` + assert_kind!(EdgeError::bad_gateway("x"), "bad_gateway", 502_u16); + assert_kind!(EdgeError::gateway_timeout("x"), "gateway_timeout", 504_u16); + + // `retry_after_only_on_config_out_of_date` + assert_retry_after!(EdgeError::bad_gateway("x"), false); + assert_retry_after!(EdgeError::gateway_timeout("x"), false); + + // `field_path_only_on_config_out_of_date` (there is no helper macro here) + for err in [ + EdgeError::bad_gateway("x"), + EdgeError::gateway_timeout("x"), + ] { + let body = parse_body(err.into_response().expect("response")); + assert!( + body["error"].get("field_path").is_none(), + "field_path should be absent for gateway errors" + ); + } +``` + +Only `kind_strings_per_variant` is exhaustive today; the other two are subset checks. Add +the rows to all three anyway so 502/504 are pinned for kind/status, absence of +`Retry-After`, and absence of `field_path` from the first red run onward. + +- [ ] **Step 2: Run to verify it fails** — Run both focused commands: + `cargo test -p edgezero-core bad_gateway` (matches the two shared surface/wire tests plus + `bad_gateway_reason_is_typed`) and `cargo test -p edgezero-core gateway_timeout` (matches + the two shared tests plus both timeout-attribution tests). Expected: FAIL to compile + (`no variant or associated item named bad_gateway`/`gateway_timeout`). A single + `gateway_timeout` filter does **not** match `bad_gateway_reason_is_typed` and therefore + cannot prove the new 502 classification went red. + +- [ ] **Step 3: Add the two variants** in `pub enum EdgeError` — **ALPHABETICALLY, not appended.** + +`clippy::arbitrary_source_item_ordering` is a denied restriction lint and it **does police enum-variant order** (verified: appending `BadGateway` after `Validation` errors with *"incorrect ordering of items (must be alphabetically ordered)"*). The existing variants are already alphabetical (`BadRequest, ConfigOutOfDate, Internal, MethodNotAllowed, NotFound, NotImplemented, ServiceUnavailable, Validation`), so insert **in place**: +- `BadGateway` goes **before** `BadRequest` (BadG < BadR), +- `GatewayTimeout` goes **between** `ConfigOutOfDate` and `Internal` (C < G < I). + +Resulting order: `BadGateway, BadRequest, ConfigOutOfDate, GatewayTimeout, Internal, MethodNotAllowed, NotFound, NotImplemented, ServiceUnavailable, Validation`. + +```rust + /// Upstream or transport failure (DNS, TLS, connect, unreachable, or a + /// non-timeout send failure). HTTP 502. + #[error("{message}")] + BadGateway { + message: String, + reason: BadGatewayReason, + }, + /// A wall-clock deadline or per-request timeout fired. HTTP 504. + /// Carries typed provenance naming which configured budget input selected the + /// effective deadline. This is not the physical timer phase, proof that the named + /// deadline elapsed, or a retry/batch-abandonment decision. + #[error("{message}")] + GatewayTimeout { message: String, cause: BudgetSource }, +``` + +`BadGatewayDecodeReason`, `BadGatewayReason`, and `BudgetSource` are defined **in `error.rs` +in THIS task (Task 1)**, NOT in `time.rs` (Task 2). That ordering is load-bearing: Task 1 +lands/commits/builds **before** Task 2, and the two variants name these enums, so a +`time`-module home would make the standalone Task-1 commit fail to compile. Define all three +immediately **before `EdgeError`** in alphabetical item order (`BadGatewayDecodeReason`, +`BadGatewayReason`, `BudgetSource`, `EdgeError`); +`time.rs` (Task 2) and `dispatch_budget` (Phase 1b) later +`use crate::error::BudgetSource;`. + +The **derives and variant order are compile-verified** (a throwaway crate under the repo's +`arbitrary_source_item_ordering` deny + `cargo check`): +```rust +// Debug: EdgeError derives Debug and contains both reason enums. +// Clone + Copy: error carriers pass classification/provenance by value. +// PartialEq + Eq: contract tests assert exact values. +// Variants ALPHABETICAL: `arbitrary_source_item_ordering` (denied) rejects any other order. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BadGatewayDecodeReason { + Brotli, + Gzip, + Json, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BadGatewayReason { + Decode(BadGatewayDecodeReason), + Protocol, + Transport, + Unreachable, + Unspecified, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +// non_exhaustive: public enum that must be able to gain a future budget-input source +// without a breaking change. Verified: intra-crate exhaustive matches +// still compile clean under the denied `wildcard_enum_match_arm`. +#[non_exhaustive] +pub enum BudgetSource { + BatchDeadline, + Default, + PerCallTimeout, + Unspecified, +} +``` + +- [ ] **Step 4: Add the constructors** in `impl EdgeError` — **also alphabetically** (the impl's fns are already ordered `bad_request, config_out_of_date, inner, internal, kind_str, message, status, validation`): put `bad_gateway` **before** `bad_request`, and `gateway_timeout` **between** `config_out_of_date` and `inner`. + +```rust + #[inline] + pub fn bad_gateway>(message: S) -> Self { + EdgeError::BadGateway { + message: message.into(), + reason: BadGatewayReason::Unspecified, + } + } + #[inline] + pub fn bad_gateway_with_reason>( + message: S, + reason: BadGatewayReason, + ) -> Self { + EdgeError::BadGateway { + message: message.into(), + reason, + } + } + #[inline] + pub fn gateway_timeout>(message: S) -> Self { + EdgeError::GatewayTimeout { + message: message.into(), + cause: BudgetSource::Unspecified, + } + } + #[inline] + pub fn gateway_timeout_caused>(message: S, cause: BudgetSource) -> Self { + EdgeError::GatewayTimeout { + message: message.into(), + cause, + } + } +``` + +(All four literals are shown in rustfmt's canonical split form, verified with +`rustfmt --edition 2024`.) + +- [ ] **Step 5: Update ALL nine exhaustive matches (crate won't compile until every one is done)** + +`impl` sites: +- `kind_str()` — add `EdgeError::BadGateway { .. } => "bad_gateway",` and `EdgeError::GatewayTimeout { .. } => "gateway_timeout",` +- `status()` — add `EdgeError::BadGateway { .. } => StatusCode::BAD_GATEWAY,` and `EdgeError::GatewayTimeout { .. } => StatusCode::GATEWAY_TIMEOUT,` +- `message()` — add **`EdgeError::BadGateway { message, .. }`** and + **`EdgeError::GatewayTimeout { message, .. }`** to the "clone the `message`" arm. The + `..` is required because both variants carry a second typed field. +- `inner()` — add both variants to the `=> None` arm list. +- `IntoResponse::into_response`'s `field_path_opt` match — add both variants to the `=> None` arm list. + +**Test-module sites (these have explicit panic-arms listing every non-`ConfigOutOfDate` variant, NO `_`):** in each of the **four** `match err { … }` blocks (the fourth is the root-error sentinel test; Step 6's compiler-driven `E0004` sweep is the source of truth for their exact locations), add `| EdgeError::BadGateway { .. } | EdgeError::GatewayTimeout { .. }` to the `=> panic!("expected ConfigOutOfDate")` arm. + +- [ ] **Step 6: Compiler-driven catch — build and fix any remaining non-exhaustive match** + +Run: `cargo build -p edgezero-core --tests` +If it reports `E0004 non-exhaustive patterns` anywhere, add the two arms at that exact site (the compiler prints the file:line). Repeat until it builds. Expected end state: builds clean. + +- [ ] **Step 7: Run the new + matrix tests to verify they pass** + +Run: `cargo test -p edgezero-core bad_gateway`, then +`cargo test -p edgezero-core gateway_timeout` (the **same two filters as red Step 2**), then +`cargo test -p edgezero-core kind_strings_per_variant`, then +`cargo test -p edgezero-core only_on_config_out_of_date` (one filter matches both the +retry_after_* and field_path_* matrices). +Expected: PASS. + +- [ ] **Step 8: Format, lint, full-crate test** + +Run: `cargo fmt -p edgezero-core && cargo clippy -p edgezero-core --all-targets --all-features -- -D warnings && cargo test -p edgezero-core` +Expected: clean, all green. + +- [ ] **Step 9: Commit** + +```bash +git add crates/edgezero-core/src/error.rs +git commit -m "feat(core): add typed gateway errors" +``` + +--- + +### Task 2: `time` module — constants + `Deadline` + +**Files:** +- Create: `crates/edgezero-core/src/time.rs` +- Modify: `crates/edgezero-core/src/lib.rs` (add `pub mod time;`) +- Test: `crates/edgezero-core/src/time.rs` (colocated) + +**Interfaces:** +- Produces (for Phase 1b `dispatch_budget` + all adapters): public + `type MonotonicInstant = web_time::Instant`, injectable `MonotonicClock` with + `deadline_after(Duration)`, and `Deadline` (`Copy`), + `Deadline::after(Duration) -> Self`, `::at_instant(MonotonicInstant) -> Self`, + `::instant(&self) -> MonotonicInstant`, `::remaining(&self) -> Option`, + `::is_expired(&self) -> bool`; consts `DEFAULT_NO_DEADLINE_BUDGET` (30 s), + `DEADLINE_FAR_FUTURE` (7 days), `BATCH_DISPATCH_SLACK_MAX` (25 ms). + **`DispatchBudget` ships in Phase 1b with `dispatch_budget`.** Downstream crates can name, + construct, and compare public timing values without declaring a direct `web-time` + dependency. + +**Deadline semantics (matches spec §3.3.2 `deadline <= now => expired`):** a deadline whose instant is **exactly now** is **expired** — `is_expired()` is `true` and `remaining()` is `None` at equality, not `Some(0)`. A naive `checked_duration_since(now).is_none()` gets this wrong (it returns `Some(ZERO)` at equality), so the impl below uses `checked_duration_since(..).filter(|r| !r.is_zero())` — the zero case is filtered explicitly. + +- [ ] **Step 1: Write the failing tests (deterministic — bounded by explicit instants, no wall-clock tolerance windows)** + +Create `crates/edgezero-core/src/time.rs` with only the test module + `use`: + +```rust +use std::time::Duration; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn monotonic_instant_is_public_clock_type() { + let start = MonotonicInstant::now(); + let deadline = Deadline::at_instant(start); + let _: MonotonicInstant = deadline.instant(); + } + + // The public API promises `Deadline: Copy` (adapters copy it into per-slot budgets + // rather than borrowing). Pin it at COMPILE time — a later `#[derive]` edit that + // drops `Copy` must fail the build, not silently change the API. + #[test] + fn deadline_is_copy() { + fn assert_copy() {} + assert_copy::(); + } + + #[test] + fn constants_have_exact_values() { + assert_eq!(DEFAULT_NO_DEADLINE_BUDGET, Duration::from_secs(30)); + assert_eq!(DEADLINE_FAR_FUTURE, Duration::from_hours(168)); + assert_eq!(BATCH_DISPATCH_SLACK_MAX, Duration::from_millis(25)); + } + + // EXACT + deterministic: every assertion pins BOTH the deadline instant and the + // `now` it is compared against, via the pure `*_at(now)` helpers. No wall-clock + // tolerance windows, no assumption about how fast the test resumes. + + #[test] + fn deadline_before_now_is_expired() { + let base = MonotonicInstant::now(); + let past = Deadline::at_instant(base); + let now = base + .checked_add(Duration::from_secs(1)) + .expect("no overflow"); + assert!(past.is_expired_at(now)); + assert_eq!(past.remaining_at(now), None); + } + + #[test] + fn deadline_exactly_now_is_expired() { + // The equality boundary: deadline instant == now. `deadline <= now` is + // expired, but `checked_duration_since` returns Some(ZERO) here, so a naive + // impl would wrongly report NOT expired. + let base = MonotonicInstant::now(); + let at_now = Deadline::at_instant(base); + assert_eq!( + at_now.remaining_at(base), + None, + "zero remaining is expired, not Some(0)" + ); + assert!( + at_now.is_expired_at(base), + "a deadline exactly at now is expired" + ); + } + + #[test] + fn deadline_in_future_has_exact_remaining() { + let base = MonotonicInstant::now(); + let future = Deadline::at_instant( + base.checked_add(Duration::from_mins(1)) + .expect("no overflow"), + ); + assert!(!future.is_expired_at(base)); + // EXACT equality — both instants are explicit, so there is no elapsed-time slop. + assert_eq!(future.remaining_at(base), Some(Duration::from_mins(1))); + } + + #[test] + fn after_clamps_duration_max_to_far_future() { + // Prove the 7-DAY CLAMP via bounds on the resulting INSTANT (no second + // now()-snapshot to race against). + let before = MonotonicInstant::now(); + let deadline = Deadline::after(Duration::MAX); + let after = MonotonicInstant::now(); + // `after()` computed `t0 + FAR_FUTURE` for some t0 in [before, after], + // so the instant must land within [before+FAR_FUTURE, after+FAR_FUTURE]. + let lower = before + .checked_add(DEADLINE_FAR_FUTURE) + .expect("no overflow"); + let upper = after.checked_add(DEADLINE_FAR_FUTURE).expect("no overflow"); + assert!(deadline.instant() >= lower, "clamped below the 7-day bound"); + assert!( + deadline.instant() <= upper, + "Duration::MAX was NOT clamped to 7 days" + ); + } + + // Public smoke tests: the live-clock wrappers (which call MonotonicInstant::now()) actually + // delegate to the pure *_at helpers. The *_at tests above cover exact arithmetic; + // these guard the public surface AND that `after` honours its duration argument. + #[test] + fn public_remaining_and_is_expired_smoke() { + // Tight instant-bracket around `after`: the resulting deadline MUST land in + // [before + 1h, after + 1h]. Any mutation that perturbs the duration — dropping + // 30s, or clamping to DEADLINE_FAR_FUTURE = 7 days — falls outside the bracket. + // (A loose "remaining is (59 min, 1 h]" check would survive a 30s-off mutant.) + let before = MonotonicInstant::now(); + let far = Deadline::after(Duration::from_hours(1)); + let after = MonotonicInstant::now(); + assert!(!far.is_expired()); + // rustfmt-canonical SPLIT form (verified with `rustfmt --edition 2024`): the + // `.checked_add(..).expect(..)` chain exceeds `chain_width = 60`, so rustfmt + // breaks it across lines — a one-liner here would fail `cargo fmt --check`. + let lo = before + .checked_add(Duration::from_hours(1)) + .expect("no overflow"); + let hi = after + .checked_add(Duration::from_hours(1)) + .expect("no overflow"); + assert!( + far.instant() >= lo && far.instant() <= hi, + "after() must land exactly `now + duration`" + ); + assert!(far.remaining().is_some()); + // `after(ZERO)` yields `now`; by the time is_expired()/remaining() read a later + // MonotonicInstant::now(), it is at-or-past — no `checked_sub` (would underflow a fresh + // WASM Instant near its epoch). + let now_deadline = Deadline::after(Duration::ZERO); + assert!(now_deadline.is_expired()); + assert_eq!(now_deadline.remaining(), None); + } + + #[test] + fn instant_round_trips() { + let base = MonotonicInstant::now() + .checked_add(Duration::from_secs(10)) + .expect("no overflow"); + assert_eq!(Deadline::at_instant(base).instant(), base); + } +} +``` + +> The test snippet above is **pre-wrapped to rustfmt's canonical form** — verified by running `rustfmt --edition 2024` on it, not by eyeballing width. The `.checked_add(..).expect(..)` chains split because they exceed **`chain_width = 60`** (NOT `max_width = 100` — an earlier note wrongly cited the 100 limit; the chain heuristic is the binding one, which is why `lo`/`hi` are multi-line here even though they'd fit on one line width-wise). Copy it verbatim and the Task 3 `cargo fmt --all -- --check` gate stays a no-op. + +- [ ] **Step 2: Wire the module in and run to verify failure** + +Add `pub mod time;` to `crates/edgezero-core/src/lib.rs` (alphabetical position among the `pub mod` lines). +Run: `cargo test -p edgezero-core --lib time::` +Expected: FAIL to compile (`cannot find value DEFAULT_NO_DEADLINE_BUDGET`, `cannot find type Deadline`). + +- [ ] **Step 3: Implement constants + `Deadline`** + +Insert the implementation between the existing `use std::time::Duration;` and +`#[cfg(test)]`. Define the public alias in this module; do not import `web_time::Instant` +under its dependency-owned name: + +> **This code is written to pass the repo's strict Clippy gate.** The workspace sets +> `restriction = { level = "deny", priority = -1 }` (root `Cargo.toml`), and **none** of +> `missing_inline_in_public_items`, `min_ident_chars`, `arithmetic_side_effects`, +> `expect_used`, or `as_conversions` is allow-listed. Therefore: every public method +> carries **`#[inline]`** (matching the 14 existing `#[inline]`s in `error.rs`); no +> single-char idents (`d` → `duration`); and **no bare `-`/`+`** — all arithmetic is +> `checked_*`. The private `*_at(now)` helpers additionally make the logic **pure and +> deterministically testable** (no hidden `MonotonicInstant::now()` inside the assertion). + +```rust + +/// Max adapter overhead tolerated before a fan-out slot fails closed. +pub const BATCH_DISPATCH_SLACK_MAX: Duration = Duration::from_millis(25); +/// Hard clamp on any caller-supplied duration, so construction cannot panic. +pub const DEADLINE_FAR_FUTURE: Duration = Duration::from_hours(168); +/// Budget applied when a request sets neither a timeout nor a deadline. +pub const DEFAULT_NO_DEADLINE_BUDGET: Duration = Duration::from_secs(30); + +/// An absolute, copyable monotonic deadline. A deadline at or before now is expired. +#[derive(Debug, Clone, Copy)] +pub struct Deadline(MonotonicInstant); + +/// Portable monotonic clock instant used by EdgeZero timing APIs. +pub type MonotonicInstant = web_time::Instant; + +impl Deadline { + /// Returns a deadline `now + min(duration, DEADLINE_FAR_FUTURE)`; never panics. + #[inline] + #[must_use] + pub fn after(duration: Duration) -> Self { + let now = MonotonicInstant::now(); + let clamped = duration.min(DEADLINE_FAR_FUTURE); + Deadline(now.checked_add(clamped).unwrap_or(now)) + } + + /// Constructs a deadline from an absolute instant. + #[inline] + #[must_use] + pub fn at_instant(instant: MonotonicInstant) -> Self { + Deadline(instant) + } + + /// Returns the absolute deadline instant. + #[inline] + #[must_use] + pub fn instant(&self) -> MonotonicInstant { + self.0 + } + + /// Returns `true` once the deadline instant is at or before now. + #[inline] + #[must_use] + pub fn is_expired(&self) -> bool { + self.is_expired_at(MonotonicInstant::now()) + } + + fn is_expired_at(&self, now: MonotonicInstant) -> bool { + self.remaining_at(now).is_none() + } + + /// Returns the remaining time, or `None` once the deadline is reached or passed. + #[inline] + #[must_use] + pub fn remaining(&self) -> Option { + self.remaining_at(MonotonicInstant::now()) + } + + fn remaining_at(&self, now: MonotonicInstant) -> Option { + self.0 + .checked_duration_since(now) + .filter(|remaining| !remaining.is_zero()) + } +} +``` + +- [ ] **Step 4: Run the tests to verify they pass** — Run: `cargo test -p edgezero-core --lib time::` — Expected: PASS (all eight). + +- [ ] **Step 5: Format, lint, full-crate test** + +Run: `cargo fmt -p edgezero-core && cargo clippy -p edgezero-core --all-targets --all-features -- -D warnings && cargo test -p edgezero-core` +Expected: clean, all green. + +- [ ] **Step 6: Commit** + +```bash +git add crates/edgezero-core/src/time.rs crates/edgezero-core/src/lib.rs +git commit -m "feat(core): add time module (Deadline + budget constants)" +``` + +--- + +### Task 3: Core CI-gate verification (the five CLAUDE.md gates) + +**Files:** none (verification only). Run from the repo root. + +**Scope:** Phase 1a is **additive, core-only** (new `EdgeError` variants and typed +reason/provenance enums plus a new `time` module; no adapter, CLI, template, or `app-demo` +change), so this task runs the **five CLAUDE.md gates** over the workspace **plus one +core-only wasm32-unknown-unknown check** (Step 5). It deliberately does **not** run the +generated-project build, the `examples/app-demo` build, or expanded per-adapter WASM +test/clippy matrices beyond the required Spin check. **This is a risk-reduced local subset, NOT a proof those are +unaffected:** every one of them compiles `edgezero-core`, so a core change *could* in +principle break them (an unexpected new export collision, a feature-gate interaction). The +judgement is that this purely additive core surface is very unlikely to, and **full CI runs +all of them on the PR regardless** — so Task 3 is a fast local gate, and CI is the actual +backstop. If local certainty is required, run the full workspace + `examples/app-demo` +builds too; otherwise rely on CI. Phases that touch adapters/templates/app-demo add those +locally. The one WASM target that is *not* redundant here is +`wasm32-unknown-unknown`: `web-time::Instant` resolves to its JS +`Date`/`performance.now()` path there, whereas the Spin `wasm32-wasip2` gate uses the WASI +clock — so the new `time` module's `web-time` dependency must be compiled on that target +too. + +- [ ] **Step 1: Format check + workspace test** + +Run: `cargo fmt --all -- --check && cargo test --workspace --all-targets` +Expected: no diff; all green. (Confirms the additive changes broke no crate.) + +- [ ] **Step 2: Clippy (all targets, all features)** + +Run: `cargo clippy --workspace --all-targets --all-features -- -D warnings` +Expected: clean. + +- [ ] **Step 3: Feature-combo check** + +Run: `cargo check --workspace --all-targets --features "fastly cloudflare spin"` +Expected: `Finished`. + +- [ ] **Step 4: Spin wasm target check** + +Run: `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin` +Expected: `Finished`. + +- [ ] **Step 5: Core wasm32-unknown-unknown check (web-time JS path)** + +Run: `cargo check -p edgezero-core --target wasm32-unknown-unknown` +Expected: `Finished`. (Compiles the new `time` module against `web-time`'s browser/JS clock backend — the Cloudflare target — which the Spin `wasip2` gate does not exercise. Core-only, so no adapter is pulled in.) + +(Steps 1–4 collectively execute all five repo CI commands from `CLAUDE.md`; Step 5 adds the `wasm32-unknown-unknown` core check. Do not skip the wasm targets — they are the ones most likely to catch an accidental `std::time` / non-WASM import.) + +--- + +## Self-Review + +- **Spec coverage:** Task 1 = §3.4.3/§7 `error.rs` (both variants, both typed enums, + constructors, full match surface, Rust-side classification, and JSON isolation); Task 2 = + §§3.3.1/3.3.4 (`Deadline` and all three constants). `DispatchBudget` + + `dispatch_budget()` (§3.3.2) are deferred **together** to Phase 1b — a stated sequencing + boundary, not a gap. +- **Compile-safety (the class of bug a prior review caught):** the nine exhaustive matches (5 impl + 4 test panic-arms) are enumerated *and* backed by a compiler-driven catch step; focused tests and all six matrix rows enter the same compile-red edit; the `cargo test` single-filter rule is applied; `is_expired_at` treats a **zero** remaining as expired (`remaining_at` filters out a zero `Duration`), so a deadline exactly at now reads as expired. +- **No placeholders / no flaky tests:** every step has exact code, paths, single-filter commands, expected output; timing tests are bounded by explicit `at_instant` instants (no `now() - 1s` underflow, no wide tolerance windows), and the clamp test proves the 7-day bound. + +## Next (not this plan; each is its own plan, NOT one atomic step) + +Phase 1b must respect the producer's type dependency: `dispatch_budget(&OutboundRequest, ..)` +cannot land before `OutboundRequest` and its private `budget_inputs()` accessor exist. The +next plan must add the pinned direct workspace/`edgezero-core` `url` dependency and either +(1) land `OutboundRequest`/`ResponseMode`, one-time canonical URI construction, +resource-limit builders, `validate_for_dispatch`, `BudgetInputs`, `DispatchBudget`, and +`dispatch_budget` in one buildable slice, or (2) land the request type/accessor in an +earlier buildable slice and the budget carrier/producer immediately after it. +`OutboundResponse`, the typed `ResponseLimitReason` work, the `Body::Stream` error change, +and the `proxy → outbound` rename can then be sequenced around the four-adapter atomic +migration, but no slice may name a type that does not yet exist. diff --git a/docs/superpowers/plans/2026-09-06-outbound-http-phase0-probe-bootstrap.md b/docs/superpowers/plans/2026-09-06-outbound-http-phase0-probe-bootstrap.md new file mode 100644 index 000000000..d8d5c7b3a --- /dev/null +++ b/docs/superpowers/plans/2026-09-06-outbound-http-phase0-probe-bootstrap.md @@ -0,0 +1,159 @@ +# Outbound HTTP Phase 0: Protected Probe Bootstrap Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement +> this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Put inert exact-SHA Cloudflare and Fastly probe dispatchers on the default branch +and provision their disposable protected environments before any implementation phase needs +host evidence. + +**Architecture:** GitHub delivers `workflow_dispatch` only for workflow files already on the +default branch. Two manually dispatched workflows validate a reviewed SHA without secrets, +then run only that SHA's probe driver in a protected disposable environment. They have no +automatic trigger and publish no outbound API or capability behavior. + +**Tech Stack:** GitHub Actions, actionlint 1.7.7, `gh`, disposable Cloudflare/Fastly accounts, +and one authenticated observable probe origin. + +--- + +## Preconditions + +- [ ] The outbound design and implementation index are approved. +- [ ] A repository administrator can create protected environments, environment secrets, + required-reviewer rules, and the fixed `refs/heads/outbound-probe-reviewed` branch. +- [ ] No Phase 4 or Phase 6 capability publication has started. This bootstrap is the sole + intermediate merge allowed by the implementation index. + +## Task Protocol + +For each task, make the stated change, run every exact local command, inspect the workflow +permissions and secret boundaries, and commit only the listed files. A skipped job, absent +secret, missing driver, zero probe count, or mismatched SHA is never success. + +### Task 1: Add inert default-branch dispatchers + +**Files:** +- Modify: `.tool-versions` +- Create: `.github/workflows/outbound-cloudflare-deployed.yml` +- Create: `.github/workflows/outbound-fastly-characterization.yml` + +- [ ] Pin actionlint 1.7.7 in `.tool-versions`. Require + `test "$(actionlint -version | head -n 1)" = "1.7.7"` before validation. +- [ ] Give each workflow only `workflow_dispatch` with one required lowercase 40-hex + `commit_sha` string input and top-level `permissions: contents: read`. Do not add + `pull_request_target`, `pull_request`, `push`, `schedule`, reusable-workflow inputs, or a + user-selectable ref/command/path. Put the exact input SHA in the workflow `run-name` and + protected job name so an environment reviewer can compare it with the reviewed ref before + approving secret access. +- [ ] Pin every third-party action to a reviewed full commit SHA and annotate its release name; + floating major-version tags are forbidden in these secret-bearing workflows. Give both jobs + finite `timeout-minutes`, disable checkout credential persistence, and do not cache any path + that candidate code can write across protected runs. +- [ ] Give each provider workflow a fixed repository-wide `concurrency` group with + `cancel-in-progress: false`. Fastly runs must serialize because they deploy into the same two + disposable services; a queued run may not cancel an active run before its cleanup trap + restores prior service state. +- [ ] Add an unprivileged `validate` job with no environment and no secret references. It + fetches only `refs/heads/outbound-probe-reviewed`, rejects a non-lowercase/full SHA, + requires that commit to be reachable from the fetched ref, checks out the exact object, + and requires `git rev-parse HEAD` to equal the input byte-for-byte. Export only that + validated SHA as a job output. +- [ ] Add a `probe` job that `needs: validate`, binds only its named protected environment, + checks out the validated SHA, and repeats the byte-for-byte HEAD check. Dependency setup, + metadata checks, and fixture builds receive no secrets. Reference disposable secrets only + in the final driver step's `env` block. +- [ ] The Cloudflare driver is exactly + `npm --prefix crates/edgezero-adapter-cloudflare run test:deployed-timing` and receives only + `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_WORKERS_SUBDOMAIN`, + `OUTBOUND_PROBE_ORIGIN_URL`, and `OUTBOUND_PROBE_ORIGIN_TOKEN`. The Fastly driver is exactly + `bash crates/edgezero-adapter-fastly/tests/host/deployed.sh` and receives only + `FASTLY_DYNAMIC_SERVICE_ID`, `FASTLY_DYNAMIC_SERVICE_URL`, `FASTLY_DISABLED_SERVICE_ID`, + `FASTLY_DISABLED_SERVICE_URL`, `FASTLY_API_TOKEN`, `OUTBOUND_PROBE_ORIGIN_URL`, and + `OUTBOUND_PROBE_ORIGIN_TOKEN`. +- [ ] Before the Cloudflare secret-bearing driver step, install the exact Rust and Node + versions from `.tool-versions`, add `wasm32-unknown-unknown`, run + `npm ci --prefix crates/edgezero-adapter-cloudflare`, run the fixture's locked Cargo + metadata and Worker 0.8.5 tree assertions, install + `worker-build 0.8.3 --locked`, assert `worker-build --version` plus locked Wrangler's + version, assert fixture/template compatibility settings, and run + `npm --prefix crates/edgezero-adapter-cloudflare run build:outbound-fixture`. None of these + setup/build steps receives provider or origin secrets. +- [ ] Before the Fastly secret-bearing driver step, install the exact Rust and Fastly CLI + versions from `.tool-versions`, add `wasm32-wasip1`, require + `fastly version` to report 15.1.0, assert both locked dependency graphs select Fastly SDK + 0.12.1, run the standalone fixture's locked metadata/tree checks, and build it without + secrets using + `fastly compute build --non-interactive --dir crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly`. + Require the fixed + `tests/fixtures/outbound-fastly/pkg/edgezero-outbound-probe.tar.gz` artifact. The final + driver may deploy only that prebuilt package; it must not compile, download candidate + dependencies, or select a different artifact after secrets enter its environment. +- [ ] Run `actionlint .github/workflows/outbound-cloudflare-deployed.yml + .github/workflows/outbound-fastly-characterization.yml` and `git diff --check`. Then run + the following negative trigger audit; status 1 is success, a match or scan error is failure: + +```sh +if git grep -nE 'pull_request_target|pull_request:|push:|schedule:' -- \ + .github/workflows/outbound-cloudflare-deployed.yml \ + .github/workflows/outbound-fastly-characterization.yml; then + exit 1 +else + status=$? + test "$status" -eq 1 +fi +``` +- [ ] Commit: `ci: bootstrap protected outbound probes`. + +### Task 2: Provision disposable protected resources + +**Repository/environment state, no source files:** + +- [ ] Create `outbound-cloudflare-probe` and `outbound-fastly-probe` as protected + environments with required maintainers, no self-approval, no production secrets, and no + deployment branches except the default-branch workflow. Populate only the exact secrets + listed in Task 1. +- [ ] Create or verify `refs/heads/outbound-probe-reviewed` as a repository-owned protected + branch with deletion and force pushes disabled. A maintainer reproduces a reviewed tree on + this ref with a fast-forward-only push before requesting environment approval; workflows + reject any SHA not reachable from it. Never dispatch a fork SHA directly. The environment + reviewer compares the SHA shown in the run/job name with the reviewed commit before approval. +- [ ] Provision a disposable Workers account/subdomain and a least-privilege token limited to + Workers Scripts edit in that account. Provision two disposable Fastly services: one with + dynamic backends enabled and one with them disabled, plus a token limited to those services. +- [ ] Freeze the observable origin protocol: authenticated `POST /v1/probes/{run_id}/arm` + creates isolated state; requests use `/v1/probes/{run_id}/{case_id}` for raw bytes, delayed + headers/chunks, stalled upload reads, and cancellation; authenticated + `GET /v1/probes/{run_id}/observations` returns timestamped bytes/EOF/disconnect facts; + authenticated `DELETE /v1/probes/{run_id}` removes the state. Run IDs and tokens are + unguessable, state is isolated per run, and stale state has a bounded retention policy. +- [ ] Verify the origin arm/observe/delete control plane with a unique smoke run and delete + it. Verify both provider tokens can inspect only their disposable resources. Record the + environment settings and resource identifiers in the repository's private operational + record, never in committed logs or docs. + +### Task 3: Merge and prove dispatch availability + +- [ ] Open and review the bootstrap PR independently of the implementation branch. Confirm + its diff contains only `.tool-versions` and the two inert workflow files. +- [ ] Merge it to the default branch. Run + `gh workflow view outbound-cloudflare-deployed.yml --ref main` and + `gh workflow view outbound-fastly-characterization.yml --ref main`; both must identify the + merged workflow and `workflow_dispatch` trigger. +- [ ] Record the bootstrap merge SHA in the outbound implementation PR. Do not dispatch a + probe until its owning phase has added a reviewed nonzero driver; a missing driver must + fail rather than skip. + +## Phase Verification + +- [ ] `test "$(actionlint -version | head -n 1)" = "1.7.7"` +- [ ] `actionlint .github/workflows/outbound-cloudflare-deployed.yml .github/workflows/outbound-fastly-characterization.yml` +- [ ] Repeat Task 1's negative trigger audit and require exit 0 with no matches. +- [ ] `git diff --check` +- [ ] Both workflows are manually visible from the default branch. +- [ ] Both protected environments require reviewer approval and contain only disposable + resources/secrets. +- [ ] The observable origin smoke state was deleted. + +Expected result: later phases can dispatch immutable reviewed SHAs through protected +default-branch workflows without merging any partial outbound implementation. diff --git a/docs/superpowers/plans/2026-09-06-outbound-http-phase1b-core-types-budget.md b/docs/superpowers/plans/2026-09-06-outbound-http-phase1b-core-types-budget.md new file mode 100644 index 000000000..06dcec3b9 --- /dev/null +++ b/docs/superpowers/plans/2026-09-06-outbound-http-phase1b-core-types-budget.md @@ -0,0 +1,269 @@ +# Outbound HTTP Phase 1b: Core Types and Budget Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> +> **Status:** Implemented on PR 275. The unchecked steps below are retained as the original implementation record, not current readiness state. +> **Superseded batch API:** The 2026-09-15 consumer-alignment plan replaces this plan's +> `send_all` surface and budget names. Do not use its batch snippets as current API guidance. +> **Superseded response API:** The Phase 2 Content-Type provenance hardening requires the +> application clock and original provenance in the six-argument response constructor, and +> returns provenance as the fifth `into_parts()` element. The response snippets below are +> historical; use the outbound design's current response contract. + +**Goal:** Add the runtime-independent outbound request, response, client, URL, and dispatch-budget value layer specified in §§3.1-3.3 of the outbound design. + +**Architecture:** Build the new `edgezero_core::outbound` module alongside the legacy `proxy` module so the repository compiles while adapters migrate in Phases 4-6. This coexistence is branch-only scaffolding, not a compatibility promise or a releasable state. Phase 7 deletes `proxy` and proves that no public alias remains. Core canonicalizes a URL once, owns all policy values, and exposes only provider-neutral data to adapters. + +**Tech Stack:** Rust 1.95, `http`, `bytes`, `serde_json`, `url = "=2.5.8"`, `web-time`, `async-trait`, `futures`. + +--- + +## Preconditions and Merge Rule + +- [ ] Confirm Phase 1a tests pass: `cargo test --offline --locked -p edgezero-core --lib`. +- [ ] Read spec §§3.1-3.3, §5.1, §6, and the core entries in §7. +- [ ] Do not publish or merge the series until Phase 7 removes `edgezero_core::proxy`, `ProxyService`, and every legacy public name. Do not add aliases between old and new names. +- [ ] Keep `BudgetInputs` private and limited to `deadline` and `timeout`; response size does not participate in budget selection. + +## Task Protocol + +For every task below: add only the named tests first; run the exact focused command and require a nonzero failure caused by the missing behavior; implement the smallest listed surface; rerun the same command to zero failures; run `cargo test --offline --locked -p edgezero-core --lib` and `git diff --check`; then stage only the task's files and make the stated commit. A compile error for a not-yet-added public item is an acceptable red result. An already-passing new test is not. The contract declarations below are in the repository's required alphabetical item order; keep every actual module item, struct field, enum variant, and impl method in that order, and add `#[inline]` to every public function as required by the denied workspace lints. + +**Required exact test names:** `outbound_request_defaults_and_parts_round_trip`, `outbound_request_canonicalizes_url_table`, `outbound_request_rejects_invalid_target_table`, `outbound_request_rejects_empty_userinfo`, `outbound_request_rejects_backslash_authority_forms`, `outbound_request_preserves_percent_encoded_hash`, `outbound_request_from_request_normalizes_immediately`, `dispatch_validation_precedence_table`, `request_normalization_strips_connection_nominations`, `request_normalization_is_idempotent`, `dispatch_budget_selects_and_attributes_minimum`, `dispatch_budget_rejects_expired_or_zero`, `http_client_delegates_send_and_send_all`, `http_client_preserves_per_slot_elapsed`, and `outbound_response_parts_preserve_method_headers_and_body`. + +**Expected red:** Task 2 initially fails to resolve `edgezero_core::outbound`; Task 3 initially observes nominated/hop-by-hop headers or accepts an invalid request; Task 4 fails to resolve `dispatch_budget`; Task 5 fails to resolve `HttpClient`/`OutboundResponse`. Dependency Task 1 is a lock/dependency precondition and is green when the exact tree assertion passes. + +### Task 1: Pin the canonical URL parser + +**Files:** +- Modify: `Cargo.toml` +- Modify: `crates/edgezero-core/Cargo.toml` +- Modify: `Cargo.lock` +- Modify: `examples/app-demo/Cargo.lock` + +- [ ] Add `url = "=2.5.8"` to workspace dependencies and `url = { workspace = true }` to core. +- [ ] Refresh both independent lock graphs with `cargo check --offline -p edgezero-core` and `cargo check --offline --manifest-path examples/app-demo/Cargo.toml -p app-demo-core`; these commands are intentionally unlocked in this dependency task. +- [ ] Run `cargo tree --offline --locked -p edgezero-core -e normal | rg 'url v2\.5\.8'`; expect exactly the pinned normal dependency. +- [ ] Run `cargo tree --offline --locked --manifest-path examples/app-demo/Cargo.toml -p app-demo-core -e normal | rg 'url v2\.5\.8'`; expect the same exact version in the excluded demo graph. +- [ ] Commit: `build(core): pin outbound URL parser`. + +### Task 2: Add canonical outbound requests + +**Files:** +- Create: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-core/src/lib.rs` + +**Contract:** + +```rust +pub struct OutboundRequest { + body: Body, + deadline: Option, + headers: HeaderMap, + max_decoder_bytes: u64, + max_brotli_window_bits: u8, + max_chunk_bytes: Option, + max_decoded_response_bytes: Option, + max_encoded_response_bytes: Option, + max_request_body_bytes: u64, + max_response_header_bytes: Option, + max_response_header_count: Option, + method: Method, + response_mode: ResponseMode, + timeout: Option, + uri: Uri, +} + +pub struct OutboundRequestParts { + pub body: Body, + pub deadline: Option, + pub headers: HeaderMap, + pub max_decoder_bytes: u64, + pub max_brotli_window_bits: u8, + pub max_chunk_bytes: Option, + pub max_decoded_response_bytes: Option, + pub max_encoded_response_bytes: Option, + pub max_request_body_bytes: u64, + pub max_response_header_bytes: Option, + pub max_response_header_count: Option, + pub method: Method, + pub response_mode: ResponseMode, + pub timeout: Option, + pub uri: Uri, +} + +pub enum ResponseMode { Buffered { max_bytes: u64 }, Streamed } + +impl OutboundRequest { + pub fn body(self, body: impl Into) -> Self; + pub fn deadline(self, deadline: Deadline) -> Self; + pub fn from_parts(parts: OutboundRequestParts) -> Result; + pub fn from_request(request: Request, target: Uri) -> Result; + pub fn get(uri: impl AsRef) -> Result; + pub fn header, V: AsRef<[u8]>>( + self, + name: N, + value: V, + ) -> Result; + pub fn headers(&self) -> &HeaderMap; + pub fn headers_mut(&mut self) -> &mut HeaderMap; + pub fn into_parts(self) -> OutboundRequestParts; + pub fn json(self, value: &T) -> Result; + pub fn max_decoder_bytes(self, bytes: u64) -> Self; + pub fn max_brotli_window_bits(self, bits: u8) -> Self; + pub fn max_chunk_bytes(self, bytes: NonZeroU64) -> Self; + pub fn max_decoded_response_bytes(self, bytes: u64) -> Self; + pub fn max_encoded_response_bytes(self, bytes: u64) -> Self; + pub fn max_request_body_bytes(self, bytes: u64) -> Self; + pub fn max_response_bytes(self, bytes: u64) -> Self; + pub fn max_response_header_bytes(self, bytes: u64) -> Self; + pub fn max_response_header_count(self, count: u64) -> Self; + pub fn method(&self) -> &Method; + pub fn new(method: Method, uri: Uri) -> Result; + pub fn post(uri: impl AsRef) -> Result; + pub fn stream_response(self) -> Self; + pub fn timeout(self, timeout: Duration) -> Self; + pub fn uri(&self) -> &Uri; +} +``` + +- [ ] Add failing `outbound_request_*` tests for defaults, every builder/accessor, `into_parts`/`from_parts`, JSON content type, and header byte validation. `from_request` must preserve the exact method/body while immediately stripping hop-by-hop, nominated, `Host`, `Content-Length`, and `Transfer-Encoding` headers. +- [ ] Add URL table tests for scheme/host case, default ports, DNS, IPv4, bracketed IPv6, path/query WHATWG normalization, and exact canonical serialization. +- [ ] Add rejection rows for non-HTTP schemes, relative/no-authority targets, nonempty and empty userinfo, raw `#` fragments, every raw-backslash special-URL separator form (`//`, `\\`, `/\\`, `\\/` around an empty-userinfo authority), and malformed UTF-8/header bytes. Add positive rows proving percent-encoded `%23` and `%40` remain path/query data rather than being rejected as fragment/userinfo delimiters. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib outbound::tests::outbound_request_`; expect compile/test failure. +- [ ] Implement the types and builders from spec §3.1. Store the canonical `Uri`; adapters must not parse the caller's source string again. +- [ ] Implement raw-string construction in this order: reject a literal `#`; reject every raw `\\` byte before WHATWG parsing; isolate the raw authority between literal `://` and the first `/`, `?`, or `#` and reject any literal `@` (including `https://@example.com/`); `Url::parse`; reject scheme/authority/remaining userinfo violations; clear default ports; serialize once; parse that serialization into `http::Uri`. Rejecting backslashes is the exact pre-parser rule because WHATWG special URLs normalize them as separators; do not add a second ad hoc authority parser. `new(Uri)` runs the same post-parse validation but cannot recover syntax discarded before it received the typed URI. +- [ ] Implement `from_request` by preserving method/body, replacing only the target, and applying the same request normalizer immediately. Adapters still reapply normalization immediately before SDK construction because `headers_mut` can introduce unsafe fields later. +- [ ] Add and test provider accessors `backend_target`, `cert_host`, `host_authority`, `host_name`, and `sni_hostname`. `host_name` is the host-only value required by Fastly backend identity and must not include IPv6 brackets or a port. +- [ ] Set exact defaults: final Buffered response 1 MiB, request body 8 MiB, Brotli window bits 24, and Brotli decoder-requested heap 32 MiB. Encoded and decoded response caps default to unset; all byte caps and counters are `u64`. +- [ ] Rerun the focused test and `cargo test --offline --locked -p edgezero-core --lib`; expect success. +- [ ] Commit: `feat(core): add canonical outbound request types`. + +### Task 3: Add dispatch validation and request normalization + +**Files:** +- Modify: `crates/edgezero-core/src/outbound.rs` + +- [ ] Add failing `dispatch_validation_*` tests for the portable method set, GET/HEAD body rules, streamed-body detection, streamed-response detection, invalid policy combinations, and configured Brotli window values below 10 or above 30. The infallible builder stores any `u8`; only dispatch validation rejects the invalid range. +- [ ] Add failing `request_normalization_*` tests for standard hop-by-hop fields, every valid `Connection` nomination, repeated `Connection` fields, malformed tokens, `Host`, `Content-Length`, `Transfer-Encoding`, and idempotence. +- [ ] Run both filters separately; expect failures: + - `cargo test --offline --locked -p edgezero-core --lib dispatch_validation_` + - `cargo test --offline --locked -p edgezero-core --lib request_normalization_` +- [ ] Implement `validate_for_dispatch`, `normalize_for_dispatch`, `is_stream_body`, `is_stream_response`, and private `budget_inputs`. Validation must happen before body polling; normalization must be repeatable at adapter boundaries. +- [ ] Rerun both filters and the core library suite; expect success. +- [ ] Commit: `feat(core): validate outbound dispatch requests`. + +### Task 4: Add dispatch budget arithmetic + +**Files:** +- Modify: `crates/edgezero-core/src/time.rs` +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-core/src/lib.rs` + +**Contract:** + +```rust +pub struct DispatchBudget { + pub cause: BudgetSource, + pub deadline: Deadline, + pub duration: Duration, +} + +pub fn dispatch_budget( + request: &OutboundRequest, + now: Instant, +) -> Result; +``` + +- [ ] Add failing `dispatch_budget_*` tests for default, timeout only, deadline only, both orderings, equal values favoring `PerCallTimeout`, zero/expired inputs returning attributed 504, seven-day clamping, and one shared `now` across a batch. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib dispatch_budget_`; expect failure. +- [ ] Implement only checked `Instant`/`Duration` arithmetic. Preserve the winning `BudgetSource` through every clamp. +- [ ] Rerun the focused and core suites; expect success. +- [ ] Commit: `feat(core): compute outbound dispatch budgets`. + +### Task 5: Add client and response value surfaces + +**Files:** +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-core/src/context.rs` +- Modify: `crates/edgezero-core/src/lib.rs` + +**Contract:** + +```rust +#[derive(Debug)] +#[non_exhaustive] +pub struct OutboundSlotResult { + pub elapsed: Duration, + pub outcome: Result, +} + +#[async_trait(?Send)] +pub trait OutboundHttpClient: Send + Sync { + async fn send(&self, request: OutboundRequest) -> Result; + async fn send_all( + &self, + requests: Vec, + ) -> Vec; +} + +#[derive(Clone)] +pub struct HttpClient { + inner: Arc, +} + +pub struct OutboundResponse { + body: Body, + headers: HeaderMap, + request_method: Method, + status: StatusCode, +} + +impl HttpClient { + pub fn new(client: Arc) -> Self; + pub async fn send(&self, request: OutboundRequest) + -> Result; + pub async fn send_all(&self, requests: Vec) + -> Vec; + pub fn with_client(client: C) -> Self; +} + +impl OutboundResponse { + pub fn body(&self) -> &Body; + pub fn headers(&self) -> &HeaderMap; + pub fn headers_mut(&mut self) -> &mut HeaderMap; + pub fn into_body(self) -> Body; + pub fn into_parts(self) -> (Method, StatusCode, HeaderMap, Body); + pub fn is_success(&self) -> bool; + pub fn new( + request_method: Method, + status: StatusCode, + headers: HeaderMap, + body: Body, + ) -> Self; + pub fn status(&self) -> StatusCode; +} +``` + +- [ ] Add failing `http_client_*` tests proving `send`/`send_all` delegation, empty-batch forwarding, index alignment, and preservation of each mock slot's distinct `elapsed` plus mixed outcome. Core delegates these values unchanged; complete preflight and real clock ownership belong to each adapter contract in Phases 4-6. +- [ ] Add failing `outbound_response_*` tests for originating method, status/success, immutable and adapter-facing mutable header access, repeated headers, borrowed/consuming body access, and `into_parts`. `into_response` is owned by Phase 2 Task 3 because it depends on response normalization. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib http_client_`; expect a nonzero failure. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib outbound_response_`; expect a nonzero failure. +- [ ] Implement `HttpClient::send` and `send_all` only. Do not expose the inner client and do not recreate `ProxyService::forward`. +- [ ] Implement `OutboundResponse::new`, `headers_mut`, `status`, `is_success`, `headers`, `body`, `into_body`, and `into_parts() -> (Method, StatusCode, HeaderMap, Body)`. Bounded drains, JSON, and `into_response` remain Phase 2. +- [ ] Add `RequestContext::http_client()` using `self.request.extensions()` directly. Keep `proxy_handle()` only as temporary branch scaffolding and add no alias between the two client types. +- [ ] Rerun focused tests, `cargo test --offline --locked -p edgezero-core --lib`, and `cargo check --workspace --all-targets --features "fastly cloudflare spin"`. +- [ ] Commit: `feat(core): add outbound HTTP client surface`. + +## Phase Verification + +- [ ] `cargo fmt --all -- --check` +- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` +- [ ] `cargo test --workspace --all-targets` +- [ ] `cargo check --workspace --all-targets --features "fastly cloudflare spin"` +- [ ] `cargo check -p edgezero-core --target wasm32-unknown-unknown` +- [ ] `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin` +- [ ] `(cd examples/app-demo && cargo test --locked --workspace --all-targets)` +- [ ] `git diff --check` + +Expected result: both core modules compile temporarily, the new outbound API and budget layer are fully tested, and no adapter behavior has changed. diff --git a/docs/superpowers/plans/2026-09-06-outbound-http-phase2-body-response-limits.md b/docs/superpowers/plans/2026-09-06-outbound-http-phase2-body-response-limits.md new file mode 100644 index 000000000..37a7dcc99 --- /dev/null +++ b/docs/superpowers/plans/2026-09-06-outbound-http-phase2-body-response-limits.md @@ -0,0 +1,339 @@ +# Outbound HTTP Phase 2: Typed Bodies, Decoding, and Response Limits Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> +> **Status:** Implemented on PR 275. The unchecked steps below are retained as the original implementation record, not current readiness state. + +**Goal:** Complete the portable response pipeline: exact typed stream errors, response-limit errors, header/body normalization, encoded and decoded caps, Brotli preflight, gzip/deflate/Brotli completion, optional rechunking, and deadline-aware drains. + +**Architecture:** Keep all transport-independent byte accounting and decoding in core. Adapters retain timers, abort handles, trailer/completion protocols, and native body ownership. The pipeline order is fixed: cumulative adapter-visible upstream field accounting before normalization, normalization, synthetic proxy-marker accounting, body disposition and sound `Content-Length` checks, encoded limit, lazy Brotli WBITS/decoder-charge gate, decoder/native EOF, decoded limit for identity or EdgeZero-decoded bodies only, rechunker, outer deadline/cancellation wrapper, then either the independent final Buffered collection cap or a lazy Streamed body. + +**Tech Stack:** Rust 1.95, `bytes`, `futures`, exact manifest-pinned `async-compression = 0.4.43`, `http`, `serde`, `serde_json`. + +--- + +## Preconditions + +### Content-Type Provenance Hardening + +This follow-up preserves original header ambiguity without retaining another raw header copy. + +- [x] Add failing normalization-path tests for missing, single UTF-8, repeated/conflicting, + comma-containing, opaque, mixed valid/opaque, and Connection-nominated Content-Type values. +- [x] Return body disposition and typed Content-Type provenance from the shared normalizer; + inspect all values before filtering and certify only a retained, unchanged textual singleton. +- [x] Require provenance in outbound response construction, preserve it in decomposition and + conversion, and invalidate eligibility after mutable header access without upgrading an + original ineligible/unknown result. MIME parsing and redaction remain application-owned. +- [x] Carry provenance through every Axum/Fastly/Spin conversion path. Cloudflare reports Unknown + because its string-valued, combined headers cannot establish original field/byte identity. +- [x] Exercise actual adapter conversion paths, update demo/template fixtures and public docs, + and run core/workspace tests, strict host/WASM lint, documentation guards and generated builds. + +Verification includes 750 core tests; 200 Axum unit and 13 contract tests, including a raw-TCP +valid-plus-opaque duplicate; 161 Fastly runtime tests; 29 Cloudflare browser runtime tests; +and 35 Spin hosted contracts. Spin executes its provenance matrix through the hosted contract +because the standalone lib harness lacks required host imports; the host rejects Connection +fields before normalization. Demo tests, fresh generated host/three-WASM builds, strict +host/three-WASM Clippy, API docs and documentation format/lint/build checks also pass. + +- [ ] Phase 1b is complete and `cargo test --offline --locked -p edgezero-core --lib` passes. +- [ ] Re-read spec §§3.1.4, 3.3.3-3.4.5, §5.1, and the core file summary in §7. +- [ ] Preserve `Body::into_bytes_bounded(usize)` as the inbound 400 helper. Outbound response drains are separate `u64`/502 APIs. +- [ ] Header-limit ties resolve `HeaderCount` before `HeaderBytes`, matching the specified per-entry algorithm. A malformed or conflicting payload-bearing `Content-Length` is `BadGatewayReason::Protocol`; HEAD/304 representation metadata is preserved without body interpretation. + +## Task Protocol + +For every task: add only the named tests; run the exact focused filter and require failure from the missing behavior; implement the listed API/algorithm; rerun that filter and the full core library suite; run `git diff --check`; then stage only the task's files and make the stated commit. For stream tests, use poll-counting scripted sources so early-stop, order, and EOF claims are executable assertions rather than comments. + +**Required exact test names:** `response_too_large_preserves_every_reason_without_serializing_it`, `response_too_large_constructors_preserve_unspecified_and_specific_reason`, `body_from_stream_preserves_edge_error`, `body_from_external_stream_maps_to_internal`, `body_into_bytes_bounded_preserves_edge_error`, `body_stream_accepts_infallible_bytes`, `body_bounded_checks_before_append`, `response_normalization_precedence_table`, `response_header_limiter_accumulates_field_sections`, `response_resource_header_limit_count_wins_tie`, `content_encoding_classifier_covers_every_visible_shape`, `payload_content_length_rejects_before_body_poll`, `payload_content_length_explicit_identity_rejects_before_body_poll`, `payload_content_length_passthrough_uses_buffered_not_decoded_cap`, `payload_content_length_skips_output_caps_for_compressed_body`, `outbound_response_into_response_reapplies_normalization`, `encoded_limit_stops_before_decoder`, `decoded_limit_bypasses_raw_passthrough`, `buffered_collection_limit_is_independent`, `rechunk_stream_is_lazy_and_ordered`, `outbound_response_until_deadline_wins_ready_result`, `outbound_response_json_error_classification`, `decoder_carrier_restores_exact_edge_error`, `decode_gzip_drains_every_member_to_native_eof`, `decode_brotli_rejects_trailing_data`, `brotli_memory_charge_is_pinned_and_checked`, and `brotli_window_rejects_before_decoder_allocation`. + +**Expected red:** each task either fails to resolve its new type/helper or observes the old behavior: 500/untyped limit, erased stream error, retained unsafe header, over-limit poll/append, cap winning an expired deadline, decoder stopping at its first end marker, or decoder construction before WBITS rejection. Do not accept a panic as the intended red result. + +**Portable pipeline skeleton:** adapters compose the helpers in this exact nesting; an implementation with a different nesting fails the order tests. + +```rust +let encoding = classify_content_encoding(&headers); +let max_buffered_response_bytes = match response_mode { + ResponseMode::Buffered { max_bytes } => Some(max_bytes), + ResponseMode::Streamed => None, +}; +enforce_payload_content_length( + &headers, + &encoding, + max_buffered_response_bytes, + max_decoded_response_bytes, + max_encoded_response_bytes, +)?; +let raw = limit_encoded_stream(native_body, max_encoded_response_bytes); +let decoded = match encoding { + ContentEncoding::Brotli => decode_brotli_stream( + raw, + max_brotli_window_bits, + max_decoder_bytes, + ), + ContentEncoding::Deflate => decode_deflate_stream(raw, max_decoder_bytes), + ContentEncoding::Gzip => decode_gzip_stream(raw, max_decoder_bytes), + ContentEncoding::Identity => raw, + ContentEncoding::Passthrough(_) => raw, +}; +if matches!(encoding, ContentEncoding::Brotli | ContentEncoding::Deflate | ContentEncoding::Gzip) { + headers.remove(CONTENT_ENCODING); + headers.remove(CONTENT_LENGTH); +} +let output_limited = match encoding { + ContentEncoding::Brotli | ContentEncoding::Deflate | ContentEncoding::Gzip | ContentEncoding::Identity => { + limit_decoded_stream(decoded, max_decoded_response_bytes) + } + ContentEncoding::Passthrough(_) => decoded, +}; +let shaped = rechunk_stream(output_limited, max_chunk_bytes); +let timed = adapter_deadline_wrapper(shaped, budget, native_completion); +let body = match response_mode { + ResponseMode::Buffered { max_bytes } => { + Body::from(collect_response_stream(timed, max_bytes).await?) + } + ResponseMode::Streamed => Body::from_stream(timed), +}; +``` + +`adapter_deadline_wrapper` and `native_completion` above are adapter-owned placeholders, not core APIs; they show why rechunking must remain inside the outer timer/cancellation wrapper. +The wrapper rechecks the absolute deadline whenever an inner resource error becomes ready, +so an already-expired adapter budget retains the spec's 504 precedence. The decoded limiter +must remain inside that wrapper, while the final collection helper remains outside it and +preserves the narrower generic-consumer precedence documented in §3.4.5. + +Every transport-independent stream helper accepts and returns the same concrete erased type; +none returns a branch-specific opaque `impl Stream`: + +```rust +pub type BodyStream = LocalBoxStream<'static, Result>; + +pub const BROTLI_DECODER_FIXED_CHARGE_BYTES: u64 = 16_777_216; +pub fn brotli_decoder_memory_charge(window_bits: u8) -> Result; +pub fn decode_brotli_stream( + stream: BodyStream, + max_window_bits: u8, + max_decoder_bytes: u64, +) -> BodyStream; +pub fn decode_deflate_stream(stream: BodyStream, max_decoder_bytes: u64) -> BodyStream; +pub fn decode_gzip_stream(stream: BodyStream, max_decoder_bytes: u64) -> BodyStream; +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ContentEncoding { Brotli, Deflate, Gzip, Identity, Passthrough(PassthroughReason) } +#[non_exhaustive] +pub enum PassthroughReason { Stacked, Unsupported(String), Malformed } +pub fn classify_content_encoding(headers: &HeaderMap) -> ContentEncoding; +pub async fn collect_response_stream(stream: BodyStream, max: u64) -> Result; +pub fn enforce_payload_content_length( + headers: &HeaderMap, + encoding: &ContentEncoding, + max_buffered_bytes: Option, + max_decoded_bytes: Option, + max_encoded_bytes: Option, +) -> Result<(), EdgeError>; +pub fn limit_decoded_stream(stream: BodyStream, max: Option) -> BodyStream; +pub fn limit_encoded_stream(stream: BodyStream, max: Option) -> BodyStream; +pub fn rechunk_stream(stream: BodyStream, max: Option) -> BodyStream; + +pub struct ResponseHeaderLimiter { /* private checked-u64 counters and limits */ } +impl ResponseHeaderLimiter { + pub fn new(max_bytes: Option, max_count: Option) -> Self; + pub fn observe(&mut self, headers: &HeaderMap) -> Result<(), EdgeError>; +} +``` + +Ownership is exact: `BodyStream` lives at `edgezero_core::body::BodyStream`; +`BROTLI_DECODER_FIXED_CHARGE_BYTES`, `ContentEncoding`, +`brotli_decoder_memory_charge`, `classify_content_encoding`, `decode_brotli_stream`, and +`decode_gzip_stream` live in `edgezero_core::compression`; `ResponseHeaderLimiter`, +`collect_response_stream`, `enforce_payload_content_length`, `limit_decoded_stream`, +`limit_encoded_stream`, and `rechunk_stream` live in `edgezero_core::outbound`. Core re-exports +every one at the `edgezero_core` root, and adapter crates use that single stable import +surface. Core owns typed source restoration and codec/accounting errors; adapters own the +outer deadline, cancellation guard, and native-completion protocol. + +### Task 1: Add typed response-limit errors + +**Files:** +- Modify: `crates/edgezero-core/src/error.rs` +- Modify: `crates/edgezero-core/src/lib.rs` + +- [ ] Add failing `response_too_large_*` tests for all reasons: `BrotliWindow`, `BufferedBody`, `DecodedBody`, `DecoderMemory`, `EncodedBody`, `HeaderBytes`, `HeaderCount`, and `Unspecified`. +- [ ] Assert status 502, kind `response_too_large`, no `reason`, `field_path`, or `Retry-After` in the wire response, and preservation of the Rust-side reason. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib response_too_large_`; expect failure. +- [ ] Add `ResponseLimitReason` and `EdgeError::ResponseTooLarge`; update every exhaustive match in production and tests in alphabetical order. +- [ ] Add and test both required constructors: `response_too_large(message)` stores `Unspecified`; `response_too_large_with_reason(message, reason)` stores the supplied reason. +- [ ] Rerun the focused test and core suite; expect success. +- [ ] Commit: `feat(core): add typed outbound response limits`. + +### Task 2: Make body stream errors exact + +**Files:** +- Modify: `crates/edgezero-core/src/body.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-core/src/context.rs` +- Modify: `crates/edgezero-core/src/proxy.rs` +- Modify: `crates/edgezero-adapter-axum/src/request.rs` +- Modify: `crates/edgezero-adapter-axum/src/response.rs` +- Modify: `crates/edgezero-adapter-axum/src/proxy.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/response.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/proxy.rs` +- Modify: `crates/edgezero-adapter-fastly/src/response.rs` +- Modify: `crates/edgezero-adapter-fastly/src/proxy.rs` +- Modify: `crates/edgezero-adapter-spin/src/response.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` + +**Contract:** + +```rust +pub type BodyStream = LocalBoxStream<'static, Result>; + +pub enum Body { + Once(Bytes), + Stream(BodyStream), +} + +pub fn from_external_stream(stream: S) -> Self; // E -> Internal +pub fn from_stream(stream: S) -> Self; // exact EdgeError +pub async fn into_bytes_bounded(self, max_size: usize) -> Result; +pub fn into_stream(self) -> Option; +pub fn stream(stream: S) -> Self; // infallible Bytes +``` + +- [ ] Add failing tests for typed error identity, external error conversion, infallible input, `From`, exact-limit success, first-byte-over-limit failure, checked overflow, and no poll after failure. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib body::tests::`; expect failure. +- [ ] Implement `BodyStream`, all three constructors, `into_stream`, `From`, and pre-append checked accounting. `into_bytes_bounded` must propagate an existing `EdgeError` unchanged. Add one typed-source test for every public error family and a foreign sentinel proving `from_external_stream` sanitizes before collection. Use `from_external_stream` only at actual platform/foreign-error inputs; a platform inbound producer either uses that compatibility boundary or deliberately maps to a public `EdgeError` before `from_stream`. Remove redundant `map_err(EdgeError::internal)` where it would erase an existing `EdgeError`. +- [ ] Run `rg -n 'Body::from_stream|Body::from_external_stream|Body::Stream|Self::Stream|\.into_stream\(' crates examples/app-demo --glob '*.rs' --glob '*.hbs'`. Classify construction/consumption sites, ignoring definitions, comments, and introspection-only matches. Add error-identity regressions at EdgeZero-owned error-producing boundaries; inbound platform streams receive only compile-required `from_external_stream` migration that preserves their existing Internal mapping. +- [ ] Run the core suite and `cargo test --workspace --all-targets`; expect success. +- [ ] Commit: `refactor(core): preserve typed body stream errors`. + +### Task 3: Normalize response metadata and body disposition + +**Files:** +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-core/src/outbound.rs` + +- [ ] Add failing `response_normalization_*` tests for repeated and malformed `Connection`, nominated `content-encoding`/`content-length`, UTF-8 filtering, standard hop-by-hop fields, and idempotence. +- [ ] Add body-disposition cases for HEAD, every 1xx class, 204, positive/zero/absent 205, 304, and ordinary payload-bearing responses. +- [ ] Add explicit malformed, duplicate-conflicting, and comma-list `Content-Length` cases. Payload-bearing/205 ambiguity must fail as protocol; HEAD/304 metadata is retained without parsing it as a body promise. +- [ ] Add `outbound_response_into_response_reapplies_normalization`: mutate headers through `headers_mut`, then prove `into_response` strips hop-by-hop and nominated fields, preserves repeated end-to-end fields, keeps the body lazy, and returns typed protocol/internal errors rather than an infallible or generic conversion. +- [ ] Run both focused commands; each must execute a nonzero matching test count and fail for missing behavior: + - `cargo test --offline --locked -p edgezero-core --lib response_normalization_` + - `cargo test --offline --locked -p edgezero-core --lib outbound_response_into_response_` +- [ ] Implement `normalize_response_headers` returning `ResponseHeadNormalization` (body disposition and pre-filter Content-Type provenance), a private checked `parse_content_length`, plus `OutboundResponse::into_response`. Preserve HEAD/304 representation metadata; strip prohibited 1xx/204 framing; validate payload/205 length syntax; rewrite successful 205 to `Content-Length: 0` only after capturing `declared_body`. `into_response` must reapply the same helper idempotently after any `headers_mut` use, without upgrading original provenance. +- [ ] Rerun focused/core tests; expect success. +- [ ] Commit: `feat(core): normalize outbound response metadata`. + +### Task 4: Implement response header, encoded-byte, and chunk wrappers + +**Files:** +- Modify: `crates/edgezero-core/src/compression.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-core/Cargo.toml` +- Modify: `crates/edgezero-core/src/outbound.rs` + +**Pre-poll length contract:** adapters call this only after response normalization and only +for `ResponseBodyDisposition::Payload`: + +```rust +pub fn enforce_payload_content_length( + headers: &HeaderMap, + encoding: &ContentEncoding, + max_buffered_bytes: Option, + max_decoded_bytes: Option, + max_encoded_bytes: Option, +) -> Result<(), EdgeError>; +``` + +- [ ] Add failing `response_resource_*` tests for exact and over-limit header count/bytes, repeated values, checked overflow, count-before-bytes precedence, and cumulative accounting across successive adapter-visible informational/final/trailer `ResponseHeaderLimiter::observe` calls before normalization. Prove stripped fields still count and a limit cannot reset at a field-section boundary; separately charge EdgeZero's synthetic proxy marker after normalization. Adapters still document whether those sections are exposed before host allocation. +- [ ] Add classifier tables for absent and exactly one bare `identity`, `gzip`, `deflate`, or `br`; ASCII case and surrounding space/tab; repeated fields; comma-stacked, parameterized, empty, malformed/non-UTF-8, and unknown values. The declared results are `Brotli`, `Deflate`, `Gzip`, `Identity`, and typed `Passthrough(PassthroughReason)`; every adapter consumes this helper rather than inspecting the header itself. +- [ ] Add payload-length tables for absent/zero/exact/over values; malformed, comma-list, and conflicting values; effective identity (absent or one bare `identity`) versus gzip/deflate/br/passthrough coding; encoded-only, decoded-only, final-buffer-only, and combined caps. Encoded applies to every coding. Decoded rejects early only for effective identity. The final Buffered cap rejects early for identity and passthrough, where wire bytes are final bytes, but never for gzip/deflate/Brotli that EdgeZero decodes. Include explicit-identity and passthrough final-buffer overages that reject before the scripted body is polled, plus a passthrough body larger than the decoded cap that succeeds when the independent final cap permits it. HEAD/1xx/204/304 never call this helper; 205 uses its disposition protocol. +- [ ] Add encoded-limit tests for exact/over limit, empty chunks counting as zero but remaining observable, cumulative gzip-member input, early stop, and typed source-error preservation. +- [ ] Add decoded-limit tests for exact/over cumulative identity, gzip, and Brotli output; checked overflow; empty chunks; early stop; and typed source-error preservation. A pipeline table proves passthrough never enters this wrapper. +- [ ] Add `rechunk_*` tests for lazy pulls, order preservation, exact maximum item size, empty items, source error ordering, early drop, and no claim about backing allocation/RSS. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib response_resource_`; expect a nonzero failure. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib content_encoding_classifier_`; expect a nonzero failure. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib payload_content_length_`; expect a nonzero failure. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib encoded_limit_`; expect a nonzero failure. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib rechunk_`; expect a nonzero failure. +- [ ] Implement `ContentEncoding` and `classify_content_encoding` in `compression.rs`; + implement `ResponseHeaderLimiter`, `enforce_payload_content_length`, + `limit_decoded_stream`, `limit_encoded_stream`, and `rechunk_stream` in `outbound.rs`, with the exact shared + signatures above. Re-export every new public item from `lib.rs`. The classifier uses + `HeaderMap::get_all`; only one bare visible field can select Identity/Gzip/Brotli, and + every repeated/stacked/parameterized/malformed/non-UTF-8/unknown shape is typed + `Passthrough(PassthroughReason)`. No + helper may poll after a terminal error or cap result. +- [ ] Rerun focused/core tests; expect success. +- [ ] Commit: `feat(core): enforce outbound response resources`. + +### Task 5: Add deadline-aware response drains and JSON + +**Files:** +- Modify: `crates/edgezero-core/src/outbound.rs` + +- [ ] Add failing `outbound_response_drain_*` tests for empty and nonempty `Once` plus `Stream`; exact/over final collection limits; typed source failures; pre-append accounting; and `ResponseLimitReason::BufferedBody`. Exercise the same `collect_response_stream` helper adapters use before constructing a Buffered response. +- [ ] Add `_until` tests for entry, pre-poll, post-ready chunk, source error, cap result, and EOF checks. If expired when a decision is ready, 504 wins with `BudgetSource::Unspecified`, because this API receives only `Deadline`. Separately prove an adapter-style wrapper preserves its supplied `DispatchBudget::cause`. +- [ ] Add `outbound_response_json_*` tests for valid/malformed JSON, buffered/streamed modes, and streamed `json` invalid-state mapping to protocol 502. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib outbound_response_drain_`; expect a nonzero failure. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib outbound_response_until_`; expect a nonzero failure. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib outbound_response_json_`; expect a nonzero failure. +- [ ] Implement `collect_response_stream` plus synchronous `json(&self)` for `Body::Once`, returning protocol 502 for `Body::Stream`; implement consuming `into_bytes_bounded`, `into_bytes_bounded_until`, `json_bounded`, and `json_bounded_until`. These helpers own a final collection cap and report `BufferedBody`; the request's independent decoded-output policy has already wrapped eligible streams. Malformed upstream JSON is 502 with `BadGatewayReason::Decode(BadGatewayDecodeReason::Json)`. Do not delegate to the inbound body helper. +- [ ] Rerun focused/core tests; expect success. +- [ ] Commit: `feat(core): add bounded outbound response drains`. + +### Task 6: Replace decoder error plumbing and completion semantics + +**Files:** +- Modify: `crates/edgezero-core/src/compression.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/proxy.rs` +- Modify: `crates/edgezero-adapter-fastly/src/proxy.rs` +- Modify: `Cargo.toml` +- Modify: `Cargo.lock` +- Modify: `examples/app-demo/Cargo.lock` + +- [ ] Add failing `decoder_carrier_*` tests proving all `EdgeError` variants survive the `io::Error` bridge unchanged and decoder-originated errors become `BadGatewayReason::Decode(BadGatewayDecodeReason::Gzip)` or `Decode(BadGatewayDecodeReason::Brotli)` with diagnostics. +- [ ] Change the workspace requirements to exact `async-compression = "=0.4.43"`, `brotli = "=8.0.4"`, and `brotli-decompressor = "=5.0.1"`; add the decompressor as a core dev dependency so Cargo constrains the same implementation used transitively. Refresh both lockfiles with exact `cargo update --offline --precise` commands for all three packages. +- [ ] Assert both graphs resolve exactly 0.4.43, 8.0.4, and 5.0.1 with no duplicate Brotli decoder version: + - `cargo tree --offline --locked -p edgezero-core -e normal | rg 'async-compression v0\.4\.43'` + - `cargo tree --offline --locked -p edgezero-core | rg 'brotli v8\.0\.4|brotli-decompressor v5\.0\.1'` + - `cargo tree --offline --locked --manifest-path examples/app-demo/Cargo.toml -p app-demo-core -e normal | rg 'async-compression v0\.4\.43'` + - `cargo tree --offline --locked --manifest-path examples/app-demo/Cargo.toml -p app-demo-core | rg 'brotli v8\.0\.4|brotli-decompressor v5\.0\.1'` +- [ ] Add `decode_gzip_*` tests for one member, concatenated/empty/split members, cumulative output, corrupt/truncated later members, trailing garbage, late source errors, empty chunks, and drain to native EOF. +- [ ] Add `decode_brotli_*` tests for one stream, buffered read-ahead recovery, trailing bytes, a second stream, late source errors, empty chunks, and native EOF. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib decoder_carrier_`; expect a nonzero failure. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib decode_gzip_`; expect a nonzero failure. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib decode_brotli_`; expect a nonzero failure. +- [ ] Implement the private exact carrier and the concrete `BodyStream -> BodyStream` decoder signatures; enable `GzipDecoder::multiple_members(true)`; recover and inspect Brotli unread input and continue to native EOF. Migrate the existing Cloudflare/Fastly shared-decoder call sites in this same task so the commit leaves the workspace buildable and does not erase `EdgeError`. Never drain after cap/error/timeout. +- [ ] Rerun focused/core tests; expect success. +- [ ] Commit: `fix(core): preserve decoder completion and errors`. + +### Task 7: Gate Brotli allocation from the stream prefix + +**Files:** +- Modify: `crates/edgezero-core/src/compression.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-core/src/outbound.rs` + +- [ ] Add failing `brotli_window_*` tests for all WBITS values 10 through 30, standard and two-byte large-window forms, every split prefix boundary, exact/over configured window limits, malformed/truncated prefixes, replay fidelity, and proof that both window and decoder-charge rejection occur before decoder construction. Add `brotli_decoder_memory_charge_*` tables asserting `16_777_216 + 2^WBITS` with checked `u64` arithmetic, exact/one-byte-under policy boundaries, default WBITS 24 fitting the 32 MiB default, and no decoder/source poll after a failed preconstruction gate. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib brotli_window_`; expect failure. +- [ ] Audit the exact locked `brotli 8.0.4 -> brotli-decompressor 5.0.1` source graph, including both allocator crates. Record the per-family allocation-payload derivation and explicit unquantified-headroom scope in `docs/audits/2026-09-27-brotli-decoder-memory-accounting.md`, link it from the `BROTLI_DECODER_FIXED_CHARGE_BYTES = 16_777_216` code comment, and separately account for the maximum `2^WBITS` ring window. This source proof owns the accounting constant but does not claim a complete RSS bound. Corpus/allocation measurements are regression evidence only and may not substitute for the audit. Add exact normal dependency, version, registry-source, and checksum assertions so a dependency update cannot retain the old charge silently. +- [ ] Implement one lazy Brotli state machine in `decode_brotli_stream(stream, max_window_bits, max_decoder_bytes)`: read only the fixed-size prefix, compute the source-audited charge, return `BrotliWindow` or `DecoderMemory` before decoder construction, replay the prefix, then construct and drive the decoder. Invalid syntax is `Decode(Brotli)`. Because construction happens on first poll, adapter polling places prefix reads and allocation inside the outer absolute deadline/cancellation wrapper. +- [ ] Compose only the **core-owned segment** in a private core test helper: classify -> payload-length checks -> encoded counter -> decoder (including the lazy Brotli prefix/charge gate) -> decoded counter only for Identity/Deflate/Gzip/Brotli -> optional rechunker -> final collection cap. Add table tests for absent/identity, bare case-insensitive gzip/deflate/br, unknown, parameterized, stacked, and repeated encodings. Give decoded and final caps unequal values in both directions; prove passthrough bypasses only the decoded cap while every Buffered disposition still receives `BufferedBody` at its final cap. The helper mutates test headers to assert the contract that known decoded layers remove visible `content-encoding` and `content-length`, while Identity and Passthrough preserve them. Do not add a production response orchestrator, adapter deadline/cancellation wrapper, native-completion source, or platform response constructor in core. Phases 4-6 compose these public helpers at each native boundary and perform the specified header mutation before `OutboundResponse` construction. +- [ ] Rerun focused/core tests; expect success. +- [ ] Commit: `feat(core): gate brotli decoder allocation`. + +## Phase Verification + +- [ ] `cargo fmt --all -- --check` +- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` +- [ ] `cargo test --workspace --all-targets` +- [ ] `cargo check --workspace --all-targets --features "fastly cloudflare spin"` +- [ ] `cargo check -p edgezero-core --target wasm32-unknown-unknown` +- [ ] `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin` +- [ ] `(cd examples/app-demo && cargo test --locked --workspace --all-targets)` +- [ ] `rg -n 'map_err\(EdgeError::internal\)' crates/edgezero-*` and inspect every remaining foreign-error boundary. +- [ ] `git diff --check` + +Expected result: core exposes one typed, ordered response-processing pipeline. Platform timers and cancellation remain adapter work in Phases 4-6. diff --git a/docs/superpowers/plans/2026-09-06-outbound-http-phase3-capabilities-cli.md b/docs/superpowers/plans/2026-09-06-outbound-http-phase3-capabilities-cli.md new file mode 100644 index 000000000..8c8dc52d4 --- /dev/null +++ b/docs/superpowers/plans/2026-09-06-outbound-http-phase3-capabilities-cli.md @@ -0,0 +1,189 @@ +# Outbound HTTP Phase 3: Capabilities and CLI Enforcement Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> +> **Status:** Implemented on PR 275. The unchecked steps below are retained as the original implementation record, not current readiness state. + +**Goal:** Make outbound requirements declarative and enforce them against one pinned app/runtime pair before build, serve, deploy, staged deploy, or demo execution. + +**Architecture:** Core owns the manifest schema and host grammar; macros bake the validated contract; the adapter registry reports support; the CLI resolves one target and its associated contract once, then gates before shell or registry dispatch. Spin platform-host validation compares canonical atomic host sets for only the selected component. + +**Tech Stack:** Rust 1.95, `serde`, `toml`, `validator`, proc macros/`trybuild`, adapter registry, CLI integration tests. + +--- + +## Preconditions + +- [ ] Phase 2 passes all repository gates. +- [ ] Read spec §3.5 in full, CLI portions of §§5-7, and current adapter discovery paths. +- [ ] Do not edit generated templates, app-demo, or public guide content in this phase; Phase 7 owns consumer declarations and docs. + +## Task Protocol + +For each task: write only the named unit/integration/trybuild cases; run the exact package/filter command and require a nonzero compile/assertion failure; implement the listed surface; rerun focused and package tests; run `git diff --check`; then stage only the task's files and make the stated commit. Resolver tests receive explicit directories/environment maps and never mutate process cwd/env concurrently. + +**Required exact test names:** `capability_manifest_rejects_unknown_duplicate_and_overlap`, `capability_manifest_rejects_reserved_key_at_every_depth_and_case`, `outbound_host_grammar_table`, `outbound_host_default_is_https_only`, `baked_manifest_states_are_distinct`, `baked_manifest_rejects_reserved_key_at_every_depth_and_case`, `app_macro_manifest_caches_are_per_implementation`, `adapter_capability_default_is_unsupported`, `explicit_manifest_is_authoritative`, `runtime_resolver_rejects_cross_app_pair`, `runtime_resolver_rejects_equal_distance_ambiguity`, `spin_selected_component_hosts_match_as_atomic_sets`, `execute_runtime_gates_each_runtime_action_once`, and `operational_actions_keep_existing_dispatch_path`. + +**Expected red:** schema types are initially unresolved; macro fixtures initially compile or share state when they must not; the registry lacks `capability`; resolver tests select/fall back instead of rejecting; Spin drift passes incorrectly; dispatch counters show zero or duplicate gate calls. + +### Task 1: Add strict capability and host schema + +**Files:** +- Modify: `crates/edgezero-core/src/manifest.rs` +- Modify: `crates/edgezero-core/src/lib.rs` + +**Public surface:** `Capability`, `CapabilitySupport`, `ManifestCapabilities`, `ManifestOutboundCapability`, `AtomicHost`, `HostParseError`, `HostPat`, `Port`, `Scheme`, and `canonicalize_outbound_host` exactly as specified in §3.5.1. + +- [ ] Add failing manifest tests for all eight kebab-case outbound capabilities, including `outbound-complete-resource-accounting`, unknown keys, duplicate required/optional entries, required/optional overlap, and serialization round trips. Add a table that rejects any nested key equal to `capabilities` ignoring ASCII case, rejects non-lowercase top-level spellings, and accepts only exact lowercase top-level `capabilities`; recurse through tables and arrays. +- [ ] Add table tests for every accepted/rejected host grammar row from §3.5.1, including IPv4/IPv6, wildcard expansion, ports, LDH limits, punycode, raw Unicode, userinfo, paths, queries, fragments, whitespace, and malformed brackets. +- [ ] Map every rejected host row to the exact non-exhaustive `HostParseError` variant and stable nonempty Display text specified in §3.5.1; messages must not echo caller input. Use `Port::Any`, never a nonexistent wildcard variant. +- [ ] Run `cargo test --offline --locked -p edgezero-core --lib manifest::tests::`; expect failure. +- [ ] Implement strict `deny_unknown_fields` schema plus the runtime TOML reserved-key walker. Keep the shared `is_reserved_capabilities_key` policy and host parsing inside `manifest.rs`, because that file is textually included by the macro crate. +- [ ] Make absent hosts canonicalize to `https://*:*`; explicit `"*"` expands to both HTTP and HTTPS atomic entries. Validation, rendering, and drift checks must call the same parser. +- [ ] Rerun focused/core tests; expect success. +- [ ] Commit: `feat(core): add outbound capability manifest schema`. + +### Task 2: Add baked-manifest state and macro isolation + +**Files:** +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-core/src/manifest.rs` +- Modify: `crates/edgezero-macros/src/app.rs` +- Modify: `crates/edgezero-macros/tests/app_macro.rs` +- Create: `crates/edgezero-macros/tests/ui/app_capabilities_nested.rs` and `.stderr` +- Create: `crates/edgezero-macros/tests/ui/trigger_capabilities_nested.rs` and `.stderr` +- Create: `crates/edgezero-macros/tests/ui/environment_capabilities_nested.rs` and `.stderr` +- Create: `crates/edgezero-macros/tests/ui/adapter_build_capabilities_nested.rs` and `.stderr` +- Create: `crates/edgezero-macros/tests/ui/array_capabilities_nested.rs` and `.stderr` +- Create: `crates/edgezero-macros/tests/ui/app_capabilities_mixed_case.rs` and `.stderr` +- Create matching TOML inputs under `crates/edgezero-macros/tests/fixtures/` with the same stems + +**Contract:** `BakedManifest::{Absent, Malformed, Present}`, `ManifestContract::{Malformed, None, Present}`, and `Hooks::{manifest_json, manifest}`. + +- [ ] Add failing tests for absent, malformed, and present baked manifests; runtime/baked validation parity; reserved keys at every depth/case in both raw TOML and baked JSON; and two app implementations whose caches cannot leak into each other. +- [ ] Add trybuild failures for `capabilities` misplaced under nested tables and arrays at every depth listed by §3.5.3, plus a mixed-case nested spelling. +- [ ] Run `cargo test --offline --locked -p edgezero-macros`; expect failure. +- [ ] In `expand_app`, parse the source manifest to `toml::Value`, run the recursive reserved-key scan before typed deserialization can discard misplaced tables, then deserialize, validate, finalize, and serialize. Generate one `OnceLock` per app implementation, never in a shared default trait method. At runtime, parse baked JSON to `serde_json::Value`, run the equivalent JSON walker using the same case-insensitive key predicate and array recursion, then deserialize, validate, and finalize. Any scan/parse/validation failure is `BakedManifest::Malformed`, never `Absent` or an empty `Present` contract. +- [ ] Rerun macro and core tests; expect success. +- [ ] Commit: `feat(macros): bake outbound capability contracts`. + +### Task 3: Add fail-closed adapter capability metadata + +**Files:** +- Modify: `crates/edgezero-adapter/Cargo.toml` +- Modify: `crates/edgezero-adapter/src/registry.rs` +- Modify: `crates/edgezero-adapter/src/cli_support.rs` +- Modify: `Cargo.lock`, `examples/app-demo/Cargo.lock` + +- [ ] Add failing registry tests for the default `Unsupported` result and a fixture adapter that returns each support level. Do not publish in-tree matrix cells before the owning adapter behavior and evidence land. +- [ ] Run `cargo test --offline --locked -p edgezero-adapter --features cli adapter_capability_`; expect a nonzero failure. +- [ ] Add the direct `edgezero-core` dependency and defaulted `Adapter::capability`. Until Phases 4-6, in-tree adapters inherit `Unsupported`; each owning phase adds its exhaustive override atomically with passing contracts and any required host proof. +- [ ] Refresh both independent lock graphs with `cargo check --offline -p edgezero-adapter --features cli` and `cargo check --offline --manifest-path examples/app-demo/Cargo.toml -p app-demo-cli --tests`; these dependency-refresh commands are intentionally unlocked. The demo command must select a package in the demo workspace; Cargo rejects feature selection for the root workspace's `edgezero-adapter` package through the demo manifest. +- [ ] Assert the new edge with `cargo tree --offline --locked -p edgezero-adapter -e normal | rg 'edgezero-core'` and `cargo tree --offline --locked --manifest-path examples/app-demo/Cargo.toml -p edgezero-adapter -e normal | rg 'edgezero-core'`. +- [ ] Rerun `cargo test --offline --locked -p edgezero-adapter --features cli adapter_capability_`; expect a nonzero test count and success. +- [ ] Commit: `feat(adapter): add fail-closed capability metadata`. + +### Task 4: Build the paired runtime resolver + +**Files:** +- Modify: `crates/edgezero-adapter/src/lib.rs` +- Modify: `crates/edgezero-adapter/src/registry.rs` +- Create: `crates/edgezero-cli/src/manifest_source.rs` +- Modify: `crates/edgezero-cli/src/lib.rs` +- Modify: `crates/edgezero-cli/src/adapter.rs` +- Modify: `crates/edgezero-adapter/src/cli_support.rs` +- Modify: `crates/edgezero-adapter-axum/src/cli.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/cli.rs` +- Modify: `crates/edgezero-adapter-fastly/src/cli.rs` +- Modify: `crates/edgezero-adapter-spin/src/cli.rs` + +**Contract:** declare the exact spec §3.5.3 types in lint order. In +`edgezero-adapter::registry`, add private-field `AdapterExecutionTarget { app_root, +component, platform_manifest }` with public `new` plus read-only accessors, and add +`Adapter::execute_target(action, &target, args)`. Its default returns an explicit pinned-target +unsupported error; it must not call the cwd-discovering `execute`. In `manifest_source`, add +`ResolvedAdapterTarget::{Registered(AdapterExecutionTarget), Shell}`, `ResolvedManifest { +loader, path }`, owned `ResolvedRuntime { action, adapter, contract, target }`, and +`ResolvedShellTarget { bind_host, bind_port, command, environment, root }`. Keep the latter +fields private to `manifest_source`; `ResolvedRuntime` exposes only crate-private read-only +`action()`, `adapter_name()`, `manifest()`, and `target()` accessors, and the contained types +expose read-only accessors for their listed fields. No `ResolvedRuntime` constructor or +mutator is exposed outside the resolver, and module items/struct fields/enum variants/impl +methods must satisfy `arbitrary_source_item_ordering`. + +- [ ] Add pure failing tests for explicit `EDGEZERO_MANIFEST`, captured invocation-directory resolution, ancestor precedence, bounded workspace descendants, equal-distance ambiguity, target/contract mismatch, symlink containment, malformed contracts, and the narrow valid `contract: None` case. Assert app/shell roots are absolute canonical directories, contract/platform manifests are absolute canonical regular files contained by their corresponding root, and file/directory type swaps fail closed. +- [ ] Add tests proving shell-command and registry-backed runtime-producing actions receive + the same pinned target and cannot rediscover a different manifest. Prove the canonical + adapter identity and requested action are stored in each resolved runtime. For every + in-tree adapter, run from an unrelated cwd with conflicting discoverable manifests and + assert `execute_target` uses only `app_root`, `platform_manifest`, and `component`; assert + the default fixture adapter fails instead of entering its cwd-discovering `execute`. + Assert the resolver rejects operational actions so auth/version/healthcheck/rollback + cannot accidentally enter this outbound-only path. +- [ ] Run `cargo test --offline --locked -p edgezero-cli manifest_source`; expect failure. +- [ ] Extract target-argument parsing and discovery into pure helpers. Explicit env selection is authoritative and never falls back. Canonical platform files must remain under the canonical app root. Resolve and pin exactly one adapter identity, manifest contract, target, and runtime-producing action; no later dispatcher step may read environment variables or scan ancestors/workspace descendants. +- [ ] Preserve direct adapter API discovery for callers outside the CLI. Refactor each + in-tree adapter's build/serve/deploy implementation into target-aware internal helpers; + direct `execute` discovers once and delegates, while the paired CLI path calls + `execute_target` with the cross-crate typed target and performs no discovery. Do not use + process-wide cwd changes or synthetic path/component entries in `adapter_args`. +- [ ] Rerun focused CLI tests; expect success. +- [ ] Commit: `refactor(cli): resolve paired runtime contracts`. + +### Task 5: Validate selected Spin component host drift + +**Files:** +- Modify: `crates/edgezero-adapter-spin/src/cli.rs` +- Modify: `crates/edgezero-adapter/src/cli_support.rs` +- Modify: `crates/edgezero-cli/src/manifest_source.rs` + +- [ ] Add failing tests for explicit component, single inferred component, missing/ambiguous components, absent hosts, wildcard expansion, scheme case normalization, malformed platform entries, set-order independence, and drift diagnostics containing manifest path/component/expected list. +- [ ] Run `cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features cli spin_selected_component_`; expect a nonzero failure. +- [ ] Implement one pure component selector shared by adapter validation and CLI enforcement. Compare only selected-component canonical atomic sets. +- [ ] Rerun `cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features cli spin_selected_component_`; expect success. +- [ ] Run `cargo test --offline --locked -p edgezero-cli --no-default-features --features cli spin_selected_component_`; expect success with the Spin registry adapter genuinely unlinked. +- [ ] Separately run `cargo test --offline --locked -p edgezero-cli spin_selected_component_`; expect success with the default linked-adapter set. +- [ ] Commit: `feat(cli): reject Spin outbound host drift`. + +### Task 6: Gate actions exactly once + +**Files:** +- Modify: `crates/edgezero-cli/src/adapter.rs` +- Modify: `crates/edgezero-cli/src/lib.rs` +- Modify: `crates/edgezero-cli/src/demo_server.rs` +- Modify: `crates/edgezero-cli/Cargo.toml` + +**Dispatch signatures:** add two outbound-scoped dispatch functions that accept only the owned `ResolvedRuntime` and adapter arguments. They call private `ensure_action_capabilities(&runtime)` exactly once before branching. That helper derives the canonical adapter name, runtime-producing action, and manifest through read-only accessors and delegates to `ensure_capabilities(runtime.adapter_name(), ManifestContract::from_opt(runtime.manifest()))`. The private post-gate dispatcher also accepts the complete runtime and never gates again. There is no duplicate adapter/action argument that can diverge between admission and the eventual side effect. Keep the existing `execute(..)` / `execute_capture(..)` APIs for exempt operational actions unchanged. + +```rust +pub fn execute_runtime( + runtime: ResolvedRuntime, + adapter_args: &[String], +) -> Result<(), String>; + +pub fn execute_capture_runtime( + runtime: ResolvedRuntime, + adapter_args: &[String], +) -> Result, String>; +``` + +- [ ] Add failing tests with fixture adapters for required versus optional capabilities at all four support levels, missing registry behavior, and malformed/absent contracts. +- [ ] Add a counter seam proving `Build`, `Serve`, `Deploy`, and `DeployStaging` gate once in both `execute_runtime` and `execute_capture_runtime` before any shell/registry side effect. Use two fixture adapters and distinct actions to prove capability lookup, shell diagnostics/registry lookup, and final `AdapterAction` conversion all observe the adapter/action stored by the resolver; the outbound-scoped dispatch API must offer no override arguments. +- [ ] Add `operational_actions_keep_existing_dispatch_path`: all three auth actions, emit-version, healthcheck, and rollback continue to call the existing dispatcher, remain exempt from capability gating, and preserve registered no-project behavior. The test must fail if any of them constructs `ResolvedRuntime`, invokes paired manifest discovery, or acquires a manifest requirement. Provision and config are outside this outbound specification and receive no requirements here. Add a demo test proving Axum checks the baked manifest before startup. +- [ ] Run `cargo test --offline --locked -p edgezero-cli`; expect failure. +- [ ] Implement one private post-gate dispatcher so `execute_capture_runtime` cannot re-enter public `execute_runtime`. Required accepts only Native/BoundedCooperative; optional warns for BestEffort/Unsupported. Derive adapter/action exclusively from `ResolvedRuntime` in every outbound-gated branch. Leave `auth.rs` and the existing emit-version/healthcheck/rollback dispatch path unchanged; after a captured Deploy lacks a version, retain today's registered `EmitVersion` call rather than broadening this resolver. +- [ ] Rerun CLI tests; expect success. Adapter-specific capability values remain Unsupported until their implementation phase. +- [ ] Commit: `feat(cli): enforce outbound capabilities`. + +## Phase Verification + +- [ ] `cargo fmt --all -- --check` +- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` +- [ ] `cargo test --workspace --all-targets` +- [ ] `cargo check --workspace --all-targets --features "fastly cloudflare spin"` +- [ ] `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin` +- [ ] `cargo test -p edgezero-adapter --all-targets` +- [ ] `(cd examples/app-demo && cargo test --locked --workspace --all-targets)` +- [ ] `git diff --check` + +Expected result: every execution path evaluates one manifest/target pair once, before side effects, while platform support claims remain explicit and conservative. diff --git a/docs/superpowers/plans/2026-09-06-outbound-http-phase4-axum-cloudflare.md b/docs/superpowers/plans/2026-09-06-outbound-http-phase4-axum-cloudflare.md new file mode 100644 index 000000000..d849402b3 --- /dev/null +++ b/docs/superpowers/plans/2026-09-06-outbound-http-phase4-axum-cloudflare.md @@ -0,0 +1,307 @@ +# Outbound HTTP Phase 4: Axum and Cloudflare Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> +> **Status:** Deterministic implementation is complete on PR 275. Protected deployed-provider evidence remains an external promotion gate, so the affected capability cells remain `BestEffort`. The unchecked steps below are retained as the original implementation record. +> **Superseded batch/lifecycle API:** The 2026-09-15 consumer-alignment plan replaces this +> plan's `send_all` API and test sentinels. Retain the text only as implementation history. +> **Shared implementation follow-up (R5/R6):** Axum, Cloudflare, and Spin now use +> `OutboundBatch::from_futures`. All four adapters use `ResponseBodyPolicy` / +> `apply_response_body_policy` for the payload-limit/decoding/rechunking composition. +> Transport reads, clocks, timers, cancellation, bodyless handling, and final collection remain +> adapter-owned. These extractions do not change the normative outbound contract. + +**Goal:** Implement the full outbound contract for Axum and Cloudflare, including batch behavior, typed cleanup, adapter scheduling, and executable native/WASM/host contracts. + +**Architecture:** Both clients consume the same core request and response pipeline. Axum uses Reqwest and bounded conversion at one Tokio blocking boundary. Cloudflare uses a target-neutral orchestration driver plus a small Worker WASM bridge that owns abort, timer, native body, and host-event yield resources through terminal completion. + +**Tech Stack:** Reqwest 0.13.4, Axum/Tokio, Worker 0.8.5, Web APIs, `web-time`, `futures`, wasm-bindgen-test, workerd/deployed probes. + +--- + +## Preconditions and Owned Files + +- [ ] Phases 1b-3 pass all gates. +- [ ] The implementation index's inert workflow-bootstrap PR is merged to the default + branch, so `.github/workflows/outbound-cloudflare-deployed.yml` can receive + `workflow_dispatch` for an exact reviewed implementation SHA. Verify the default-branch + workflow has only the protected-environment manual trigger; do not attempt to dispatch a + workflow definition that exists only on this implementation branch. +- [ ] Phase 0 has also provisioned the protected `outbound-cloudflare-probe` environment, + required reviewers, exact disposable secrets/account, fixed reviewed probe ref, and the + authenticated observable-origin arm/case/observe/delete protocol. Task 4 cannot begin on + workflow presence alone. +- [ ] Re-read spec §§3.1-3.5, 4.1, 4.2, 5.2-5.5, and the Axum/Cloudflare rows in §7. +- [ ] Add `web-time` and independent `test-utils = []` to both adapter manifests. `test-utils` must not enable a runtime feature. +- [ ] Modify only Axum/Cloudflare adapters, root `Cargo.toml`/`Cargo.lock`, `examples/app-demo/Cargo.toml`/`Cargo.lock`, their host fixtures, the shared nonzero-test gate, and `.github/workflows/test.yml`. The protected Cloudflare dispatcher remains the inert default-branch bootstrap workflow; no core semantics or Phase 7 templates/docs. + +## Task Protocol + +For each adapter task: add only the named contract cases; run its exact Task 1 command and require nonzero failing tests; implement through the production driver; rerun to zero failures; run that adapter's target check and `git diff --check`; then stage only listed files and make the stated commit. Mock transport proves orchestration only. Any cancellation, raw-wire, or host-yield claim marked host-observed requires the workerd/deployed fixture before its capability cell is published. Task 4 is the exception to the one-commit task protocol: its protected red/green proof requires two immutable commits with the exact messages named below. Keep both commits reachable and do not amend, rebase, or squash either one after recording its workflow evidence. + +**Required exact test names:** `send_all_preflight_precedence_and_indices`, `send_all_reports_per_slot_elapsed`, `one_slot_send_all_matches_send`, `send_all_starts_every_eligible_exchange`, `request_preparation_consumes_entry_budget`, `adapter_final_dispatch_reapplies_request_normalization`, `canonical_uri_wire_serialization_table`, `redirect_response_is_not_followed`, `streamed_tasks_consume_fast_body_before_slow_headers`, `request_deadline_checks_before_and_after_source_ready`, `response_pipeline_preserves_typed_limits`, `response_content_length_rejects_before_body_poll`, `response_content_length_explicit_identity_rejects_before_body_poll`, `repeated_set_cookie_survives_response_conversion`, `decoder_stalls_timeout_at_all_completion_boundaries`, `axum_205_reads_at_most_once`, `axum_response_conversion_keeps_reactor_live`, `cloudflare_fetch_options_are_raw_manual_abortable_and_no_redirect`, `cloudflare_raw_fetch_rejects_failed_option_set_without_dispatch`, `cloudflare_origin_observes_canonical_host`, `cloudflare_stream_delivers_first_chunk_before_source_eof`, `cloudflare_frozen_clock_forces_host_yield`, `cloudflare_205_reads_at_most_once`, `cloudflare_invalid_utf8_request_header_returns_error_not_panic`, `cloudflare_valid_non_ascii_request_header_survives`, `cloudflare_abort_guard_fires_exactly_once`, and `adapter_capability_matrix_matches_outbound_spec`. + +**Expected red:** contract smoke tests first fail their sentinel assertion; behavior tests then expose serial starts, followed redirects, timeout/cap misclassification, reactor starvation, transformed fetch bytes, absent abort, extra 205 reads, or panic. Host-yield proof fails by timer starvation, not by a fabricated clock advance. + +### Task 1: Establish executable contract seams + +**Files:** +- Modify: `Cargo.toml` +- Modify: `Cargo.lock` +- Modify: `examples/app-demo/Cargo.toml` +- Modify: `examples/app-demo/Cargo.lock` +- Modify: `crates/edgezero-adapter-{axum,cloudflare}/Cargo.toml` +- Modify: `crates/edgezero-adapter-cloudflare/.cargo/config.toml` +- Create: `crates/edgezero-adapter-axum/tests/contract.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/contract.rs` +- Modify/Create: both adapters' `src/test_utils.rs` +- Modify: `.github/workflows/test.yml` + +- [ ] Pin Reqwest and Worker before compiling either bridge. Set root `reqwest = "=0.13.4"`. Set root and app-demo Worker requirements to exact `=0.8.5`, both with `default-features = false` and `features = ["http"]`; make the Cloudflare adapter inherit that workspace dependency while retaining `optional = true`. +- [ ] Refresh both independent lock graphs with `cargo update --offline -p reqwest --precise 0.13.4`, `cargo update --offline -p worker --precise 0.8.5`, `cargo update --offline --manifest-path examples/app-demo/Cargo.toml -p reqwest --precise 0.13.4`, and `cargo update --offline --manifest-path examples/app-demo/Cargo.toml -p worker --precise 0.8.5`. +- [ ] Assert both graphs before adding bridge code: + - `cargo tree --offline --locked -p edgezero-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown | rg 'worker v0\.8\.5'` + - `cargo tree --offline --locked --manifest-path examples/app-demo/Cargo.toml -p app-demo-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown | rg 'worker v0\.8\.5'` +- [ ] Assert both Reqwest graphs resolve exactly 0.13.4: + - `cargo tree --offline --locked -p edgezero-adapter-axum --features axum -e normal | rg 'reqwest v0\.13\.4'` + - `cargo tree --offline --locked --manifest-path examples/app-demo/Cargo.toml -p app-demo-adapter-axum -e normal | rg 'reqwest v0\.13\.4'` +- [ ] Add one deliberately failing smoke test to each native contract module. +- [ ] Remove Cloudflare's whole-file `cfg(all(feature = "cloudflare", target_arch = "wasm32"))`; place it only on the browser SDK module and add a native `test-utils` module, so the native command cannot succeed with zero tests. +- [ ] Replace the Cloudflare crate's stale Preview-1 Cargo config with `[build] target = "wasm32-unknown-unknown"` and an exact `wasm-bindgen-test-runner` for that target. CI installs the runner and executes one crate-local contract command without `--target` or `CARGO_TARGET_*_RUNNER`; a nonzero passing sentinel proves local target resolution consumes the committed config. +- [ ] Run: + - `cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --test contract` + - `cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils --test contract` +- [ ] Confirm each command executes a nonzero test count and fails at the assertion, not at linking/imports. +- [ ] Replace smoke failures with feature-gated seams for clocks, timers, transport/body handles, abort events, host yields, and stage delays. Production and fake transports must call the same driver. +- [ ] Commit: `test(adapters): add outbound contract seams`. + +### Task 2: Implement Axum dispatch and batch behavior + +**Files:** +- Create: `crates/edgezero-adapter-axum/src/outbound.rs` beside the temporary legacy `src/proxy.rs` +- Modify: `crates/edgezero-adapter-axum/src/lib.rs` +- Modify: `crates/edgezero-adapter-axum/tests/contract.rs` + +- [ ] Add failing tests for complete batch preflight, empty batch, index alignment, partial failure, one shared `batch_started_at`, distinct per-slot elapsed values, all valid slots started before completion, duplicate request headers, methods, canonical target use, and a 302 response returned without following its `Location`. Advance the clock during normalization/preflight/builder preparation and prove `send`/`send_all` budgets and elapsed values use the method-entry snapshot rather than a post-preflight re-anchor. Preflight failures receive measured elapsed time; an early completed slot retains its own terminal elapsed when a sibling finishes later; backwards injected time yields zero plus Internal, while a legitimate same-tick result may also be zero with its ordinary outcome. A valid one-slot buffered `send_all` must match `send` outcome semantics while its elapsed field is asserted separately. Include GET/HEAD body invalidity beating batch-only stream-mode errors and prove a rejected body source is never polled. +- [ ] In the production Reqwest request captured by the seam, assert exact core serialization for dot segments, percent-encoded delimiters, numeric IPv4 aliases, IDNA input, empty paths, and query preservation. The adapter must never rebuild these values from URL components. +- [ ] Add upload tests for exact/over request cap, source error, stalled pull, timeout before/after readiness, and no further polling after failure. +- [ ] Add `adapter_final_dispatch_reapplies_request_normalization`: mutate `Connection` nominations, `Host`, `Content-Length`, and `Transfer-Encoding` through `headers_mut` after construction, then inspect the production Reqwest request and prove the adapter called `normalize_for_dispatch` immediately before SDK construction. Only adapter-owned framing and the canonical host may remain. +- [ ] Run the Axum contract command; expect failures. +- [ ] Implement `AxumOutboundClient::{send, send_all}`. Capture the monotonic snapshot as the first operation in each public method, before normalization/preflight, and pass it through without re-anchoring. Configure Reqwest with `redirect(Policy::none())`, `no_gzip()`, `no_brotli()`, `no_deflate()`, and `no_zstd()`, and remove the client-wide 30-second timeout. Assert compressed loopback bytes reach only the shared EdgeZero decoder. Insert `Accept-Encoding: identity` only when the normalized request has no caller-supplied `Accept-Encoding`; preserve any caller value for the shared response decoder. +- [ ] Run `validate_for_dispatch` exactly once per request immediately after the method-entry snapshot and before batch-only mode checks, budget selection, body polling, normalization, or Reqwest construction. Batch survivors enter a private already-validated helper so they are not validated twice. +- [ ] Apply `RequestBuilder::timeout(remaining)` immediately before `send`. Race every streamed upload pull against the same absolute budget and buffer only within `max_request_body_bytes`. +- [ ] Use `join_all` after preflight, preserving slot order and independent results. Construct each `OutboundSlotResult` inside that slot's future at its terminal point; never measure after `join_all` returns or copy batch elapsed into all slots. +- [ ] Compile/export the new module for contract tests, but leave production request injection on the legacy client until Task 3 is green. +- [ ] Rerun the contract command; expect success. +- [ ] Commit: `feat(axum): implement outbound dispatch`. + +### Task 3: Implement Axum response pipeline and conversion schedule + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/outbound.rs` +- Delete: `crates/edgezero-adapter-axum/src/proxy.rs` +- Modify: `crates/edgezero-adapter-axum/src/lib.rs` +- Modify: `crates/edgezero-adapter-axum/src/request.rs` +- Modify: `crates/edgezero-adapter-axum/src/response.rs` +- Modify: `crates/edgezero-adapter-axum/src/service.rs` +- Modify: `crates/edgezero-adapter-axum/tests/contract.rs` + +- [ ] Add loopback-origin tests for repeated headers including multiple `Set-Cookie` values, raw malformed nomination/encoding lines, all content-encoding policy rows, cumulative adapter-visible upstream header count/bytes before normalization plus the synthetic proxy marker, encoded bytes, decoded output, Brotli window/decoder-state charge, and independent final Buffered limits, gzip members/native EOF, bodyless/205 cases, timeout races, future/source drop on cancellation/error/cap, and non-2xx success. Use unequal decoded/final caps in both directions and prove raw passthrough bypasses only the decoded cap. Exercise HTTP/1 and HTTP/2 separately: HTTP/1 may close its connection, while HTTP/2 resets only the request stream and may retain the pooled connection. Do not assert a generic connection drop, source allocation bound from `max_chunk_bytes`, or bounded origin observation. +- [ ] Add identity/compressed/passthrough `Content-Length` cases proving the shared pre-poll helper runs after normalization: encoded caps apply to every coding; decoded and final Buffered caps reject early for effective identity; raw passthrough compares encoded plus final Buffered but never decoded; compressed-to-decode compares only encoded. Every rejection leaves the loopback body unread. Include an explicit `Content-Encoding: identity` overage. +- [ ] Classify loopback DNS/connect/TLS establishment failures before a response head as `BadGatewayReason::Unreachable`, failures after connection progress as `Transport`, framing/completion as `Protocol`, and EdgeZero-owned JSON/gzip/Brotli failures with their exact `BadGatewayDecodeReason`. Never infer a reason by parsing diagnostic text; unavailable phase evidence falls back to `Unspecified`. +- [ ] For Axum 205, prove positive visible length aborts without reading and absent/zero length performs at most one native read: EOF succeeds; an empty or nonempty item aborts without a second read. +- [ ] In both Buffered and Streamed modes, stall gzip and Brotli before decoded output, midstream, and after codec EOF but before native EOF. Every case must return the attributed 504, retain typed late source/completion errors, and keep cleanup armed until native EOF. +- [ ] Add the streamed fan-out regression: join tasks that each perform `send` and immediately consume the body; the fast body must finish before a sibling's delayed headers. A control that joins header-only sends then delays consumption must fail. +- [ ] Add exact and one-byte-over `AXUM_RESPONSE_STREAM_BUFFER_BYTES = 16 MiB` conversion cases, preserving the original `EdgeError` status/kind. +- [ ] Add a reactor-progress regression proving no nested runtime/blocking deadlock. +- [ ] Add optional live HTTP/2 characterization through the deployed proxy path and HTTP/3 only when that production client feature is enabled. Record observed stream reset/connection reuse behavior without making it a blocking portable guarantee; the deterministic contract is guest-visible timeout plus request/body/source future drop. +- [ ] Run the Axum contract command; expect failures. +- [ ] Feed Reqwest `Response::chunk()` lazily through the core pipeline; retain method/mode/all policy fields before consuming request parts. +- [ ] Make response conversion async. Await routing and conversion inside exactly one `block_in_place(|| Handle::block_on(async { ... }))` boundary. +- [ ] On conversion failure, emit `EdgeError::into_response()`; never replace typed 502/504 errors with a generic 500. +- [ ] Switch request extensions to `HttpClient`, remove the adapter's legacy module, and do not leave a module/type alias. Do not change inbound buffering except compile-required typed stream conversion. +- [ ] Rerun Axum contract and crate tests; expect success. +- [ ] Commit: `feat(axum): enforce outbound response contracts`. + +### Task 4: Prove a Cloudflare host-event yield primitive + +**Files:** +- Create/Modify: `crates/edgezero-adapter-cloudflare/tests/host/outbound.mjs` +- Create: `crates/edgezero-adapter-cloudflare/tests/host/deployed.mjs` +- Create: `crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/Cargo.toml` +- Create: `crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/Cargo.lock` +- Create: `crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/wrangler.toml` +- Create: `crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/src/lib.rs` +- Create: `crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/README.md` +- Modify: `crates/edgezero-adapter-cloudflare/package.json` +- Create: `crates/edgezero-adapter-cloudflare/package-lock.json` + +- [ ] Put an empty `[workspace]` table in the fixture `Cargo.toml` so Cargo treats it as a standalone nested workspace rather than an undeclared member of the repository workspace. +- [ ] Pin the fixture's Worker dependency to exact `=0.8.5`. Generate and commit its independent Rust lock with `cargo check --offline --manifest-path crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/Cargo.toml`; then require `cargo metadata --offline --locked --format-version 1 --manifest-path crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/Cargo.toml` and `cargo tree --offline --locked --manifest-path crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/Cargo.toml | rg 'worker v0\.8\.5'` before `worker-build`. +- [ ] Pin fixture `compatibility_date = "2023-05-01"` and no additional compatibility flags, exactly matching the generated production `wrangler.toml.hbs`. Add a fixture assertion that parses both files and fails if their date/flags diverge. +- [ ] Pin `worker-build = 0.8.3` in the fixture instructions/CI setup and add package script + `build:outbound-fixture` running + `worker-build --release tests/fixtures/outbound-worker`. The positional path selects the + nested fixture crate; do not pass `--manifest-path`, which is only a forwarded Cargo option + after `worker-build` has already selected a crate. Record `worker-build --version` and + `wrangler --version` in the probe output. +- [ ] Add exact package scripts: `test:workerd` invokes `node tests/host/outbound.mjs --mode workerd`; `test:deployed-timing` invokes `node tests/host/deployed.mjs`. Neither script may turn absent credentials, zero probes, or unsupported host behavior into success. +- [ ] Generate and commit the adapter's first npm lockfile with `npm --prefix crates/edgezero-adapter-cloudflare install --package-lock-only --ignore-scripts`; subsequent host jobs use `npm ci` and may not float Wrangler independently of that lock. +- [ ] Build a deployed timing probe with a frozen guest clock and a continuously-ready stream. Compare candidate host-event yields; microtask/self-wake/immediately-ready futures are invalid. +- [ ] Establish an executable red in commit `test(cloudflare): add failing host-yield candidate`: + use a microtask/self-wake/immediately-ready yield, set `RED_SHA=$(git rev-parse HEAD)`, and + push that exact tree with + `git push origin HEAD:refs/heads/outbound-probe-reviewed`; the protected ref must accept it + only as a fast-forward. Dispatch the + default-branch workflow with + `RED_RUN_URL="$(gh workflow run outbound-cloudflare-deployed.yml --ref main -f commit_sha="$RED_SHA")"`, + derive `RED_RUN_ID="${RED_RUN_URL##*/}"`, reject an empty/non-URL result, and run + `gh run watch "$RED_RUN_ID" --exit-status`. Require that command to return nonzero because + the named frozen-clock timer probe starves while every + setup and positive-control probe executes. Missing credentials, zero probes, build failure, + or an unrelated assertion is not the expected red. Record the workflow URL and `RED_SHA` + before changing the reviewed ref. +- [ ] Select one candidate host-event primitive and freeze one quota in `1..=64` in commit + `test(cloudflare): characterize outbound host yielding`. Set + `GREEN_SHA=$(git rev-parse HEAD)`, push that exact tree to the same reviewed ref with + `git push origin HEAD:refs/heads/outbound-probe-reviewed`, require the update to be a + fast-forward from the red commit, and dispatch with + `GREEN_RUN_URL="$(gh workflow run outbound-cloudflare-deployed.yml --ref main -f commit_sha="$GREEN_SHA")"`, + derive `GREEN_RUN_ID="${GREEN_RUN_URL##*/}"`, reject an empty/non-URL result, and require + `gh run watch "$GREEN_RUN_ID" --exit-status` to succeed. Require every named probe plus a + positive count to pass, including timer and abort delivery under the frozen guest clock. + Record the passing workflow URL, `GREEN_SHA`, primitive, quota, runtime, and observed bound + before starting Task 5. The green commit is Task 4's final commit; do not create a third + summary commit or rewrite either evidence-bearing SHA. +- [ ] If no candidate passes on the pinned Worker runtime, STOP Phase 4 and downgrade/re-review the Cloudflare deadline capability before implementation. + +### Task 5: Implement the Cloudflare target-neutral driver + +**Files:** +- Create: `crates/edgezero-adapter-cloudflare/src/outbound.rs` beside the temporary legacy `src/proxy.rs` +- Create: `crates/edgezero-adapter-cloudflare/src/outbound/worker.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/lib.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/contract.rs` + +- [ ] Add failing native tests for full batch preflight/order/isolation, one-slot `send_all`/`send` outcome equivalence, one method-entry `batch_started_at`, distinct per-slot elapsed values including preflight failures, upload caps/errors/timeouts, send/body timeout races, encoded/decoded/final-cap fairness, frozen-clock terminal decisions, abort ownership, every early-return/drop path, and 3xx visibility without redirect dispatch. Advance the injected clock during request preparation and prove that elapsed time consumes the original `send`/`send_all` budget. Complete one slot early and another late, then prove the first elapsed value was captured at its own terminal point rather than batch return. Include backwards-clock zero-plus-Internal and legitimate same-tick zero cases, GET/HEAD body errors beating batch-only errors, and no rejected-source polls. +- [ ] Through the target-neutral `NativeBody`/header seam, cover every content-encoding row, effective-identity and raw-passthrough pre-poll rejection including explicit `identity`, unequal encoded/decoded/final caps, Brotli decoder-state charge, cumulative adapter-visible upstream header sections before normalization plus the synthetic proxy marker, repeated `Set-Cookie`, 205 settlement, first-chunk-before-EOF, typed completion, and decoder/cap/deadline ownership. These tests own orchestration; Task 7 repeats Workers-only SDK response boundaries under workerd. +- [ ] Prove typed 502 classification at each seam: provider-confirmed DNS/connect/TLS establishment before a response head is `Unreachable`, later body/completion transport is `Transport`, framing is `Protocol`, and shared JSON/gzip/Brotli decoding preserves exact coding identity. A generic Promise rejection without phase evidence is `Unspecified`, not guessed from its message. +- [ ] Capture the exact canonical string supplied to `worker::Request` and assert core + serialization for dot segments, percent-encoded delimiters, numeric IPv4 aliases, IDNA + input, empty paths, and queries; no bridge-side URL reconstruction is allowed. Do not call + this the final wire URL: the Web API may perform a second WHATWG parse/serialization. + Task 7 separately asserts the effective origin-observed target and Host against the + corresponding canonical semantics without requiring byte equality to the input string. +- [ ] Run the Cloudflare native contract command; expect failure. +- [ ] Implement one driver parameterized by `Clock`, `Timer`, `HostYield`, `RawFetch`, `NativeBody`, and `AbortHandle`. Capture the clock as the first operation in each public send method, before normalization/preflight, and never re-anchor. Count ready items including empty chunks; preserve quota state across polls and reset only after the selected host event completes. +- [ ] Run `validate_for_dispatch` exactly once per request immediately after the method-entry snapshot and before batch-only checks, budget selection, body polling, normalization, or Worker request construction. Batch survivors enter a private already-validated helper. +- [ ] Keep the owning abort guard armed through decoder completion and native EOF. Disarm only on full success. +- [ ] Implement concurrent eligible `send_all` exchanges with complete preflight and stable indices. Each slot constructs its timed result at preflight failure or inside its exchange future immediately when terminal; vector assembly performs no timing measurement. +- [ ] Compile/export the new module for contracts, but leave production request injection on the legacy client until Task 6 is green. +- [ ] Rerun native contract tests; expect success. +- [ ] Commit: `feat(cloudflare): add outbound exchange driver`. + +### Task 6: Add Worker raw fetch and response bridges + +**Files:** +- Modify: `crates/edgezero-adapter-cloudflare/src/outbound.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/outbound/worker.rs` +- Delete: `crates/edgezero-adapter-cloudflare/src/proxy.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/lib.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/request.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/response.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/contract.rs` + +- [ ] Add browser WASM tests that inspect `RequestRedirect::Manual` on the constructed `request.inner()` separately from `signal` plus `encodeResponseBody: "manual"` on the final fetch initializer; the latter must not reset redirect. Preserve request header list semantics and compile/exercise only portable Web API bridge construction. +- [ ] Do not call `worker::Headers::get_all` or claim real Worker SDK response conversion in the browser runner: Worker 0.8.5 binds the Workers-only `Headers.getAll()` API without a catch boundary. Compile the production response bridge here, but execute its real header/body conversion, repeated `Set-Cookie`, and checked completion assertions in Task 7's workerd fixture. +- [ ] Keep payload-length/decode/cap/body-disposition behavior in the target-neutral native suite. The workerd suite repeats the SDK boundary cases proving malformed/conflicting lengths and encoded/effective-identity overages reject before a Worker body stream is polled. +- [ ] Ensure the production bridge composes the same native-tested pipeline in both Buffered and Streamed modes. Task 7 runs real Worker cases stalled before decoded output, midstream, and after codec EOF but before native EOF; those cases must retain attributed 504, typed late source/completion errors, and exactly-once abort ownership. +- [ ] Add the same joined send-plus-immediate-body-consumption regression as Axum to the native driver suite. The first response chunk before source EOF is repeated through the real Worker response bridge in Task 7; collecting the whole body is not sufficient evidence. +- [ ] Add `adapter_final_dispatch_reapplies_request_normalization` at the raw-fetch boundary with post-construction `Connection` nominations, stale `Host`, `Content-Length`, and `Transfer-Encoding`. Assert stale Host is removed, no replacement Host header is inserted, and the exact canonical URL string is supplied when constructing the Web request. For headers, exercise request list semantics, preserve valid non-ASCII UTF-8 strings such as `café`, reject a raw invalid-UTF-8 value with an error rather than a panic, and assert only Cloudflare's visible normalized response-string baseline without claiming unavailable raw octets or final wire-URL byte equality. +- [ ] Inject raw-JS option-set failures and prove both a thrown `Reflect::set` and `Ok(false)` return typed Internal without invoking fetch. Also test checked JS-response conversion failure. +- [ ] The browser command must list and execute a nonzero portable bridge sentinel, but it is compile evidence only for the Workers-specific response path. Task 7's workerd suite is the required execution gate for that path. +- [ ] Run the WASM contract command; expect failure before implementation and nonzero success afterward. +- [ ] Implement lazy `Body::Stream` output and the raw JS/Web fetch bridge. Do not add direct Web dependencies unless the pinned Worker re-exports prove insufficient. +- [ ] Switch request extensions to `HttpClient`, delete the legacy module, and leave no module/type alias. +- [ ] Remove production `brotli`/`flate2` only after no production callsite remains. +- [ ] Commit: `feat(cloudflare): bridge raw outbound fetches`. + +### Task 7: Land host evidence and CI gates + +**Files:** +- Modify: `.github/workflows/test.yml` +- Modify: `crates/edgezero-adapter-axum/src/cli.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/cli.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/host/outbound.mjs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/host/deployed.mjs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/src/lib.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/wrangler.toml` +- Modify: `crates/edgezero-adapter-cloudflare/package.json` +- Modify: `crates/edgezero-adapter-cloudflare/package-lock.json` +- Modify: `crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/README.md` +- Create: `scripts/run_test_nonzero.sh` + +- [ ] Run workerd and deployed-origin tests for real Worker SDK response conversion, Workers-only `Headers.getAll("set-cookie")`, repeated `Set-Cookie`, origin-observed effective URL/Host agreement after the Web API's WHATWG normalization, send-future/body cancellation, early consumer drop, cap/decode failure, first chunk before source EOF, gzip/br decoding exactly once in Buffered and Streamed modes, effective-identity `Content-Length` rejection before body polling, the complete passthrough encoding matrix, raw upstream receipt under manual fetch encoding, downstream `EncodeBody::Manual`, 205 settlement, and selected host-yield/frozen-clock timing. Include dot-segment, numeric-IPv4, IDNA, percent-encoding, empty-path, and query rows. No browser-only result substitutes for any of these cases. +- [ ] Make `tests/host/outbound.mjs` require every exact probe identifier, print `PASS ` for each, print a positive final count, and fail on missing/duplicate/zero probes. Include `repeated_set_cookie_survives_response_conversion`, `cloudflare_origin_observes_canonical_host`, `cloudflare_stream_delivers_first_chunk_before_source_eof`, `response_content_length_explicit_identity_rejects_before_body_poll`, `cloudflare_205_reads_at_most_once`, and `cloudflare_frozen_clock_forces_host_yield`. +- [ ] Record runtime/tool versions and observed bounds in fixture output or adjacent README comments. Mocks do not satisfy this gate. +- [ ] Before `worker-build`, CI runs the fixture's locked metadata and Worker tree assertions from Task 4. Install the fixture builder with `cargo install worker-build --version 0.8.3 --locked`; verify `worker-build --version` before invoking `build:outbound-fixture`. Assert the fixture's compatibility date/flags match the production template. The README local command and CI use those same exact versions/settings. +- [ ] Keep deterministic workerd coverage in the normal untrusted `pull_request` workflow. + Use the already-merged default-branch + `.github/workflows/outbound-cloudflare-deployed.yml` bootstrap dispatcher with **only** + `workflow_dispatch`, `contents: read`, and the protected `outbound-cloudflare-probe` + environment; never use `pull_request_target`. It requires `commit_sha`, checks out that + exact full SHA, and fails unless `git rev-parse HEAD` is byte-for-byte equal before any + probe command receives secrets. Do not modify the workflow on this implementation branch + and mistake that branch-only file for executable dispatch infrastructure. +- [ ] The protected job uses only a disposable Workers account and independently observable disposable origin. Define exact secrets `CLOUDFLARE_API_TOKEN` (Workers Scripts edit limited to that account), `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_WORKERS_SUBDOMAIN`, `OUTBOUND_PROBE_ORIGIN_URL`, and `OUTBOUND_PROBE_ORIGIN_TOKEN`; production account tokens, zones, routes, and origins are forbidden. Install Node/Rust from `.tool-versions`, run `npm ci`, assert both lock graphs and Worker 0.8.5, install `worker-build 0.8.3 --locked`, assert tool versions, build the fixture, then run `npm --prefix crates/edgezero-adapter-cloudflare run test:deployed-timing`. +- [ ] Implement the Phase 0-frozen origin protocol in the fixture README and driver: + authenticated `POST /v1/probes/{run_id}/arm` creates isolated state; Worker requests use + `/v1/probes/{run_id}/{case_id}` for raw bytes, delayed headers/chunks, stalled upload reads, + and cancellation; authenticated `GET /v1/probes/{run_id}/observations` returns timestamped + bytes/EOF/disconnect facts; authenticated `DELETE /v1/probes/{run_id}` removes them. Every + request carries an unguessable run ID and token. The driver creates a unique Worker name + from `GITHUB_RUN_ID` + `GITHUB_RUN_ATTEMPT`, polls its `workers.dev` URL, requires every + exact probe ID plus a positive final count, and verifies cancellation/timing from origin + observations rather than client timing alone. Changing that protocol requires updating and + re-reviewing Phase 0 before dispatch. +- [ ] Put Worker deletion and origin-state deletion in `deployed.mjs` `finally` blocks. A failed assertion must still attempt both cleanups; a cleanup failure is reported and fails the job. The script must reject missing variables before deployment and must never reuse a fixed Worker name or observation namespace. +- [ ] Create executable POSIX `scripts/run_test_nonzero.sh `. It first invokes the command with `-- --list`, requires the sentinel as the exact terminal test-name component (an optional Rust module prefix ending in `::` is allowed) and at least one listed test, then invokes the command normally; list or execution failure propagates. Match a listed test identifier, not arbitrary diagnostic text. Use it for every native, browser-WASM, and capability-table Cargo test added to CI so cfg/feature drift cannot turn a gate into zero-test success. +- [ ] Add native Axum/Cloudflare commands and browser-WASM `test-utils` activation to CI through that helper. Add and execute explicit capability-table unit commands so feature-gated `cli.rs` tests run rather than merely compile under clippy: + - `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec` + - `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec` +- [ ] Add the generated Cloudflare adapter target check to CI: + `(cd examples/app-demo && cargo check --offline --locked -p app-demo-adapter-cloudflare --no-default-features --features cloudflare --target wasm32-unknown-unknown)`. +- [ ] Keep the workerd host suite and the trusted-environment deployed cancellation/timing probe as named CI jobs. Capability publication is blocked unless the recorded deployed job passes; native mocks or a credential-skipped zero-test result cannot substitute. +- [ ] For fork PRs, run only non-secret native/browser/workerd jobs automatically. A maintainer reviews the exact commit, reproduces and pushes that tree to `refs/heads/outbound-probe-reviewed`, dispatches the protected workflow for its SHA, and records the successful workflow URL and SHA on the PR. A skipped or mismatched-SHA run cannot promote Cloudflare's BestEffort timing/upload cells or its Native lazy-stream cell. +- [ ] Add table-driven capability tests and publish the overrides only now. Axum: Native for HTTP, header fidelity, deadlines, flexible phase budget, slot isolation, upload deadlines, and lazy response passthrough; Unsupported for complete resource accounting. Cloudflare: Native for HTTP, flexible phase budget, slot isolation, and lazy response passthrough; BestEffort for deadlines, header fidelity, and upload deadlines; Unsupported for complete resource accounting. Both wildcard future capabilities to Unsupported. +- [ ] Commit: `ci: enforce axum and cloudflare outbound contracts`. + +## Phase Verification + +- [ ] `scripts/run_test_nonzero.sh send_all_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --test contract` +- [ ] `scripts/run_test_nonzero.sh send_all_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils --test contract` +- [ ] `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec` +- [ ] `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec` +- [ ] `env CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh cloudflare_fetch_options_are_raw_manual_abortable_and_no_redirect cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --test contract` +- [ ] `(cd crates/edgezero-adapter-cloudflare && ../../scripts/run_test_nonzero.sh cloudflare_fetch_options_are_raw_manual_abortable_and_no_redirect cargo test --offline --locked --no-default-features --features cloudflare,test-utils --test contract)`; no target or runner override is permitted. +- [ ] `cargo check --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare --target wasm32-unknown-unknown` +- [ ] `(cd examples/app-demo && cargo check --offline --locked -p app-demo-adapter-cloudflare --no-default-features --features cloudflare --target wasm32-unknown-unknown)` +- [ ] `cargo metadata --offline --locked --format-version 1 --manifest-path crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/Cargo.toml` +- [ ] `cargo tree --offline --locked --manifest-path crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/Cargo.toml | rg 'worker v0\.8\.5'` +- [ ] `npm ci --prefix crates/edgezero-adapter-cloudflare` +- [ ] `npm --prefix crates/edgezero-adapter-cloudflare run build:outbound-fixture` +- [ ] `npm --prefix crates/edgezero-adapter-cloudflare run test:workerd` +- [ ] Push the reviewed phase HEAD to `refs/heads/outbound-probe-reviewed`, dispatch the + default-branch `.github/workflows/outbound-cloudflare-deployed.yml` with + `commit_sha=$(git rev-parse HEAD)`, and require a successful workflow URL whose checked-out + SHA matches exactly. Directly running the credentialed npm script is not a substitute for + protected-environment, exact-SHA, and cleanup-path evidence. +- [ ] `cargo fmt --all -- --check` +- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` +- [ ] `cargo test --workspace --all-targets` +- [ ] `cargo check --workspace --all-targets --features "fastly cloudflare spin"` +- [ ] `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin` +- [ ] `(cd examples/app-demo && cargo test --locked --workspace --all-targets)` +- [ ] `git diff --check` + +Expected result: Axum and Cloudflare no longer expose adapter `proxy` modules and publish their reviewed capability rows atomically with passing contracts. Cloudflare timing and upload remain `BestEffort` until matching-SHA host-observed evidence justifies promotion. diff --git a/docs/superpowers/plans/2026-09-06-outbound-http-phase5-spin.md b/docs/superpowers/plans/2026-09-06-outbound-http-phase5-spin.md new file mode 100644 index 000000000..6bef558c7 --- /dev/null +++ b/docs/superpowers/plans/2026-09-06-outbound-http-phase5-spin.md @@ -0,0 +1,201 @@ +# Outbound HTTP Phase 5: Spin WASI HTTP Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> +> **Status:** Implemented on PR 275 with the repository-pinned WASI SDK-resource runner. Provider teardown remains `BestEffort`; the unchecked steps below are retained as the original implementation record. +> **Superseded batch/lifecycle API:** The 2026-09-15 consumer-alignment plan replaces this +> plan's `send_all` API and test sentinels. Retain the text only as implementation history. + +**Goal:** Implement Spin outbound HTTP with owned WASI resources, exact request/response completion, typed error classification, cooperative fairness, and a real executable SDK-resource test gate. + +**Architecture:** A target-neutral state-machine driver is exercised natively; a thin Spin SDK layer supplies real WASI HTTP resources. Upload, send, request completion, response body, trailers, and caller-result handles remain owned until their specified terminal branch. One outer monotonic race covers the complete buffered exchange; streamed bodies retain the original deadline. + +**Tech Stack:** exact manifest-pinned Spin SDK 7.0.0, WASI HTTP 0.3, `web-time`, `futures`, wasm32-wasip2, validated Wasmtime runner. + +> **Readiness record:** Task 0's nonzero SDK-resource gate passed and the exact runner is frozen in crate-local Cargo configuration and CI. This note records the original dependency of Tasks 1-6; it is no longer a blocker. + +--- + +## Preconditions + +- [ ] Phases 1b-4 pass all gates. +- [ ] Re-read spec §4.4, Spin rows in §§5.2-5.5, and Spin file summary in §7. +- [ ] Keep capability support BestEffort for deadlines, flexible phase budgets, streamed-upload deadlines, and lazy response passthrough. No fake-resource test may promote these cells. + +## Task Protocol + +For each task: add only the named native or SDK-resource tests; run the exact native or pinned WASM command and require a nonzero failure from missing behavior; implement through the same production state machine; rerun focused and package tests; run `git diff --check`; then stage only listed files and make the stated commit. Resource ownership assertions use counters/defaulted completion writers and simultaneous-readiness scripts, never prose-only reasoning. + +After Task 0, "run native contracts" means: +`scripts/run_test_nonzero.sh send_all_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils --test contract`. +"Run SDK resources" and "run WASM contracts" mean invoking Cargo from +`crates/edgezero-adapter-spin` so its validated `.cargo/config.toml` is loaded: +`../../scripts/run_test_nonzero.sh spin_error_code_table_is_exhaustive cargo test --offline --locked --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources` +and +`../../scripts/run_test_nonzero.sh response_caller_result_succeeds_only_after_native_eof cargo test --offline --locked --no-default-features --features spin,test-utils --target wasm32-wasip2 --test contract`. +Every command must execute a nonzero test count; a target/linker error or zero tests is failure. + +**Required exact test names:** `sdk_fields_preserve_duplicate_values`, `sdk_request_options_execute_all_setters`, `spin_error_code_table_is_exhaustive`, `spin_timeout_provenance_before_and_after_deadline`, `send_all_preflight_precedence_and_indices`, `send_all_reports_per_slot_elapsed`, `one_slot_send_all_matches_send`, `request_preparation_consumes_entry_budget`, `adapter_final_dispatch_reapplies_request_normalization`, `canonical_uri_wire_serialization_table`, `upload_failure_wins_simultaneous_send`, `request_done_error_wins_retained_send`, `reader_gone_never_polls_request_done`, `request_trailers_succeed_only_on_clean_eof_or_reader_gone`, `response_caller_result_succeeds_only_after_native_eof`, `response_content_length_rejects_before_body_poll`, `response_content_length_explicit_identity_rejects_before_body_poll`, `repeated_set_cookie_survives_response_conversion`, `decoder_stalls_timeout_at_all_completion_boundaries`, `raw_and_decoded_quotas_yield_before_item_65`, `streamed_tasks_consume_fast_body_before_slow_headers`, `streamed_body_races_each_pull_against_remaining_budget`, `downstream_fallback_preserves_typed_error_envelope`, and `adapter_capability_matrix_matches_outbound_spec`. + +**Expected red:** Task 0 fails at WASI import/resource execution until the runner is correct; later tests expose wrong poll priority, dropped/early-success completion handles, item 65 without Pending, unbounded streamed pull, or generic response errors. A trap caused by missing imports is a Task 0 failure, never a skipped/pass result. + +**Exchange skeleton:** implement these states directly; do not replace them with an unbiased `select`. + +```rust +enum ExchangeState { + AwaitingRequestDone { request_done: RequestDone, send: StoredSend }, + ReaderGone { request_done: RequestDone, send: SendFuture }, + Uploading { pump: UploadPump, request_done: RequestDone, send: SendFuture }, +} +``` + +`Uploading` polls one pump step first and polls send only after that step returns Pending. `AwaitingRequestDone` polls completion first while retaining ready send. `ReaderGone` never polls completion, waits for send, then drops completion before response conversion. + +### Task 0: Prove the SDK-resource runner or STOP + +**Files:** +- Modify: `Cargo.toml` +- Modify: `Cargo.lock` +- Modify: `examples/app-demo/Cargo.toml` +- Modify: `examples/app-demo/Cargo.lock` +- Create: `crates/edgezero-adapter-spin/tests/sdk_resources.rs` +- Modify: `crates/edgezero-adapter-spin/Cargo.toml` +- Modify after proof: `crates/edgezero-adapter-spin/.cargo/config.toml`, `.tool-versions`, `.github/workflows/test.yml` + +- [x] Pin root and app-demo `spin-sdk` requirements to exact `=7.0.0`; the adapter's exhaustive WASI HTTP classifier makes SDK upgrades explicit review events. +- [x] Refresh both independent locks with the exact manifest requirements using `cargo check --offline -p edgezero-adapter-spin --features spin --target wasm32-wasip2` and `cargo check --offline --manifest-path examples/app-demo/Cargo.toml -p app-demo-adapter-spin --features spin --target wasm32-wasip2`; these dependency-refresh commands are intentionally unlocked. Assert both locked trees report exactly `spin-sdk v7.0.0`. +- [ ] Add the independent `test-utils = []` feature without enabling `spin`; Task 1 adds the driver dependencies. +- [ ] Add nonzero WASM tests that construct real `Fields`, append duplicate fields, construct `RequestOptions`, call all three timeout setters, construct `Request`, and exercise request/response completion resources without invoking an external origin. +- [x] Use the repository-pinned Wasmtime 48.0.2 runner: + +```sh +CARGO_TARGET_WASM32_WASIP2_RUNNER='wasmtime run -W component-model-async=y -S p3=y -S http=y' \ + scripts/run_test_nonzero.sh sdk_fields_preserve_duplicate_values \ + cargo test --offline --locked -p edgezero-adapter-spin --no-default-features \ + --features spin,test-utils --target wasm32-wasip2 --test sdk_resources +``` + +- [ ] Record the exact runtime version, flags, Rust target/component setup, test count, and passing output. Put the verified runner in the crate-local `.cargo/config.toml`; CI must use the same value. `-S http=y -S p3=y` is necessary evidence, not assumed sufficient evidence. +- [ ] From `crates/edgezero-adapter-spin`, rerun without a runner environment override: `../../scripts/run_test_nonzero.sh sdk_fields_preserve_duplicate_values cargo test --offline --locked --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources`; require the same nonzero pass. This proves the committed crate-local Cargo configuration is the configuration later tasks and CI consume. +- [ ] If the pinned runtime cannot execute these resources, STOP. Select and review a compatible exact runtime pin before changing `.tool-versions` and CI together. Compilation, native fakes, and zero-test success do not pass this gate. +- [ ] Commit after proof: `test(spin): execute WASI HTTP SDK resources`. + +### Task 1: Establish the native driver seam and error classifier + +**Files:** +- Modify: `crates/edgezero-adapter-spin/Cargo.toml` +- Create: `crates/edgezero-adapter-spin/src/outbound.rs` beside temporary `src/proxy.rs` +- Modify: `crates/edgezero-adapter-spin/src/lib.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` +- Modify: `crates/edgezero-adapter-spin/tests/sdk_resources.rs` + +- [ ] Add direct `web-time`; keep `test-utils` and `spin` independent. +- [ ] Remove the whole-file WASM gate from `tests/contract.rs`; create native and SDK modules. Confirm the native command executes a nonzero deliberately failing test. +- [ ] In the native contract, test only the target-neutral classifier policy and driver seam; do not claim that a local mirror proves the SDK enum boundary. +- [ ] In `sdk_resources.rs`, add the exhaustive table over all 39 real pinned `spin_sdk::wasip3::http::types::ErrorCode` variants with no wildcard. At or after the absolute deadline, every simultaneous SDK error maps to an attributed 504. Before expiry, all five provider timeout variants map to 504 with `BudgetSource::Unspecified`; caller request policy/size maps to 400; local invariants to 500; pre-response DNS/connect/TLS establishment to `Unreachable`; later connection/read/write failure to `Transport`; response framing/protocol to `Protocol`; and host internal to unspecified 502. Shared decoder tests preserve exact JSON/gzip/Brotli identity. Exercise the entire error table before and after deadline expiry. Invoke all three real request-option setters and record/assert the actual pinned Wasmtime outcome; this suite must not claim it can force host-selected `NotSupported` results. +- [ ] Test the real SDK conversion through `client::send`, `request_done`, and response completion paths under `spin,test-utils`. Non-2xx HTTP statuses remain successful responses. +- [ ] Run native contracts and require the deliberate assertion failure. From `crates/edgezero-adapter-spin`, run `cargo test --offline --locked --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources spin_error_code_table_is_exhaustive`; expect a nonzero compile/assertion failure from the missing classifier. +- [ ] Implement target-neutral traits/resources and `map_spin_send_err(err, deadline, cause)`. It checks absolute expiry before classifying the SDK variant and has no wildcard for the pinned exhaustive enum; any dependency update must revise the mapping and tests in the same change. +- [ ] Run native contracts; expect success, then run SDK resources filtered to `spin_error_code_table_is_exhaustive`; expect a nonzero pass against the real SDK type. +- [ ] Commit: `feat(spin): add outbound driver and error classifier`. + +### Task 2: Implement preflight, request conversion, and batch execution + +**Files:** +- Modify: `crates/edgezero-adapter-spin/src/outbound.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` + +- [ ] Add failing tests for empty batch, complete preflight, streamed request/response slot rejection, stable indices, partial errors, one-slot `send_all`/`send` outcome equivalence, one method-entry `batch_started_at`, distinct per-slot elapsed values including measured preflight time, concurrent eligible exchange progress, and sibling isolation. Advance the clock during normalization/preflight/builder preparation and prove that time consumes the original `send`/`send_all` budget. Complete one slot early and another late and prove the early elapsed value is retained; backwards injected time yields zero plus Internal, while legitimate same-tick completion may also be zero with its ordinary outcome. Include GET/HEAD body errors beating batch-only errors and no polls of rejected source streams. +- [ ] In the target-neutral injected conversion suite, add tests for methods, repeated fields via append, request options order, nonzero nanosecond rounding, and every synthetic setter outcome (`NotSupported`, `Immutable`, `Other`). The real SDK-resource suite only executes the setters and asserts the pinned host's actual result; a deployed-host probe is required before claiming a different host accepts or rejects any setter. Capture the final WASI scheme/authority/path-with-query and assert exact core serialization for dot segments, percent-encoded delimiters, numeric IPv4 aliases, IDNA input, empty paths, and queries. +- [ ] Add `adapter_final_dispatch_reapplies_request_normalization`: after `headers_mut` introduces `Connection` nominations plus stale `Host`, `Content-Length`, and `Transfer-Encoding`, inspect the final WASI `Fields` and prove normalization ran immediately before SDK request construction. Only canonical host and adapter-owned framing may remain. +- [ ] Run native contracts; expect failure. +- [ ] Implement `SpinOutboundClient::{send, send_all}` for contract tests. Capture the monotonic snapshot as the first operation in each public method, then run `validate_for_dispatch` exactly once per request before batch-only mode checks, budget selection, body polling, normalization, or SDK construction; never re-anchor. Retain method, mode, and all policy fields before consuming request parts; production injection switches only after Task 5 is green. +- [ ] Treat `RequestOptionsError::NotSupported` as logged BestEffort degradation while retaining the outer deadline race; `Immutable`/`Other` are internal setup failures. +- [ ] Use `join_all` for eligible complete batch slots after full preflight. Construct each `OutboundSlotResult` when its own preflight/exchange becomes terminal; vector assembly must not sample time. +- [ ] Rerun native and SDK-resource tests; expect success. +- [ ] Commit: `feat(spin): dispatch outbound requests`. + +### Task 3: Implement the biased upload/send state machine + +**Files:** +- Modify: `crates/edgezero-adapter-spin/src/outbound.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` + +- [ ] Add failing transition tests for `Uploading`, `AwaitingRequestDone`, and `ReaderGone`; source/cap/deadline failures, partial writes, cancellation during `write_all`, clean EOF, reader-gone, early response, writer/reader drop, trailer-completion failure, and future/resource drop counts. A successful request-trailers `FutureWriter::write(Ok(None))` is the only path from clean EOF to `AwaitingRequestDone`; `FutureWriteError` means the host dropped its reader, transitions to `ReaderGone`, and never polls `request_done`. WASI HTTP 0.3's component-stream writer has no flush operation, so no test or implementation may invent one. +- [ ] Add simultaneous-readiness tests: upload failure beats send; request completion beats retained send; send is observed only after an upload step returns Pending. +- [ ] Prove request trailers attempt `Ok(None)` only for clean EOF or reader-gone and retain the default failure otherwise. Reader-gone ignores a rejected redundant completion write; clean EOF must inspect the write result and follow the transition above. +- [ ] Implement the state machine exactly as §4.4 specifies. Never poll `request_done` in `ReaderGone`; never lose an already-ready send result while awaiting request completion. +- [ ] Yield once after every accepted upload chunk and recheck the absolute deadline before accepting every terminal or data result. +- [ ] Rerun native contracts; expect success. +- [ ] Commit: `feat(spin): own outbound upload completion`. + +### Task 4: Implement response completion, decoding, and fairness + +**Files:** +- Modify: `crates/edgezero-adapter-spin/src/outbound.rs` +- Delete: `crates/edgezero-adapter-spin/src/decompress.rs` +- Modify: `crates/edgezero-adapter-spin/src/lib.rs` +- Modify: `crates/edgezero-adapter-spin/Cargo.toml` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` + +- [ ] Add failing cases for cumulative adapter-visible upstream header limits before normalization plus the synthetic proxy marker, repeated request fields, repeated response `Set-Cookie`, raw malformed response nomination/encoding, 3xx returned without a follow-up request, HEAD/1xx/204/304, every 205 branch, encoded bytes, decoded output, Brotli window/decoder-state charge, independent final Buffered limits, gzip members, Brotli trailing data, typed source/completion errors, and timeout precedence. Unequal decoded/final caps prove raw passthrough bypasses only decoded accounting; `max_chunk_bytes` is tested only as emitted-item shaping. +- [ ] Add identity/compressed/passthrough `Content-Length` cases proving malformed/conflicting values and encoded/effective-identity decoded overages reject before reading the WASI response body; include exactly one bare `Content-Encoding: identity`. Effective identity compares encoded, decoded, and final Buffered caps; raw passthrough compares encoded plus final Buffered but never decoded; compressed-to-decode compares only encoded. +- [ ] In both Buffered and Streamed modes, stall gzip and Brotli before decoded output, midstream, and after codec EOF but before native EOF. Assert attributed 504, no early caller-result success, and preservation of a late typed source/completion error when it wins before expiry. +- [ ] Add the joined send-plus-immediate-body-consumption regression used by Axum/Cloudflare; a fast body must complete while sibling headers remain pending. +- [ ] Add caller-result tests proving the default is failure and `Ok(())` is written only after native EOF, trailers, decoder completion, caps, and deadlines succeed. Separate the actual shapes: the body is a component `stream`; only the response trailers/completion future carries an `ErrorCode` into `map_spin_send_err`; caller-result `write(Ok(()))` returns `FutureWriteError` when the host dropped its reader. Assert protocol 502 for that failed completion handshake while a consumer waits and cleanup-only behavior during wrapper drop. +- [ ] Add continuously-ready raw and decoded streams. Freeze separate quotas at 64; empty items count; state survives polls/`next()`; item 65 is preceded by a cooperative Pending. +- [ ] Replace adapter-local buffered decompression with core's shared streaming pipeline. Remove production `brotli`/`flate2` only after no production callsite remains. +- [ ] For streamed mode, retain body/trailer/caller-result ownership in `Body::Stream`; late errors must surface as error items, never false EOF. Add pre-pull, timer-ready, item-ready, post-ready, and terminal EOF/error tests for the streamed-only monotonic race. +- [ ] Rerun native and SDK tests; expect success. +- [ ] Commit: `feat(spin): complete outbound response streams`. + +### Task 5: Add the outer deadline race and downstream fallback + +**Files:** +- Modify: `crates/edgezero-adapter-spin/src/outbound.rs` +- Delete: `crates/edgezero-adapter-spin/src/proxy.rs` +- Modify: `crates/edgezero-adapter-spin/src/lib.rs` +- Modify: `crates/edgezero-adapter-spin/src/request.rs` +- Modify: `crates/edgezero-adapter-spin/src/response.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` + +- [ ] Add failing tests for send/upload/buffered-drain timeout, simultaneous exchange/timer readiness, post-ready absolute checks, preservation of all four `BudgetSource` values, and the streamed continuation using only `budget.deadline.remaining()` after headers. +- [ ] Race the whole buffered exchange against `spin_sdk::time::sleep(remaining)`. Poll exchange first but check absolute expiry in both branches so expiry wins simultaneous results. +- [ ] For Streamed mode only, read remaining time after header exchange. If expired, drop native response resources and return attributed 504; otherwise install a fresh per-chunk sleep race bounded by that remaining duration while retaining the original absolute deadline for post-ready checks. Do not add a second race to Buffered mode. +- [ ] Rename/freeze `SPIN_RESPONSE_STREAM_BUFFER_BYTES = 16 MiB`. Test exact/over limit and source failure; convert the original typed error envelope before platform headers are committed. +- [ ] Switch request extensions to `HttpClient`, delete the legacy module, and leave no module/type alias. +- [ ] Rerun native contracts and WASM checks; expect success. +- [ ] Commit: `feat(spin): enforce outbound exchange budgets`. + +### Task 6: Land CI execution gates + +**Files:** +- Modify: `.github/workflows/test.yml` +- Modify: `crates/edgezero-adapter-spin/src/cli.rs` +- Modify if validated in Task 0: `.tool-versions` + +- [ ] Add the native contract command. +- [ ] Add the exact validated SDK-resource and WASM contract runner commands, with `spin,test-utils`, through `scripts/run_test_nonzero.sh` using exact sentinels; a successful zero-test command is failure. +- [ ] Keep ordinary Spin WASM compile checks. +- [ ] Add the generated Spin adapter target check: + `(cd examples/app-demo && cargo check --offline --locked -p app-demo-adapter-spin --no-default-features --features spin --target wasm32-wasip2)`. +- [ ] Add and execute `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec`; clippy compilation alone does not execute the feature-gated `cli.rs` table. +- [ ] Add table-driven capability tests and publish Spin's override only now: Native for outbound HTTP, header fidelity, and slot isolation; BestEffort for deadlines, flexible phase budget, streamed-upload deadlines, and lazy response passthrough; Unsupported for complete resource accounting; future capabilities -> Unsupported. +- [ ] Commit: `ci: execute Spin outbound contracts`. + +## Phase Verification + +- [ ] `scripts/run_test_nonzero.sh send_all_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils --test contract` +- [ ] `(cd crates/edgezero-adapter-spin && ../../scripts/run_test_nonzero.sh response_caller_result_succeeds_only_after_native_eof cargo test --offline --locked --no-default-features --features spin,test-utils --target wasm32-wasip2 --test contract)` using the validated crate-local runner. +- [ ] `(cd crates/edgezero-adapter-spin && ../../scripts/run_test_nonzero.sh spin_error_code_table_is_exhaustive cargo test --offline --locked --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources)` using the validated crate-local runner. +- [ ] `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec` +- [ ] `cargo check --offline --locked -p edgezero-adapter-spin --no-default-features --features spin --target wasm32-wasip2` +- [ ] `(cd examples/app-demo && cargo check --offline --locked -p app-demo-adapter-spin --no-default-features --features spin --target wasm32-wasip2)` +- [ ] `cargo fmt --all -- --check` +- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` +- [ ] `cargo test --workspace --all-targets` +- [ ] `cargo check --workspace --all-targets --features "fastly cloudflare spin"` +- [ ] `(cd examples/app-demo && cargo test --locked --workspace --all-targets)` +- [ ] `git diff --check` + +Expected result: Spin's deterministic contract and real SDK bindings execute in CI, and its conservative capability row lands with them. Host cancellation remains documented BestEffort until a separate live characterization proves a finite bound. diff --git a/docs/superpowers/plans/2026-09-06-outbound-http-phase6-fastly.md b/docs/superpowers/plans/2026-09-06-outbound-http-phase6-fastly.md new file mode 100644 index 000000000..cf94497c4 --- /dev/null +++ b/docs/superpowers/plans/2026-09-06-outbound-http-phase6-fastly.md @@ -0,0 +1,251 @@ +# Outbound HTTP Phase 6: Fastly Dispatch and Harvest Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> +> **Status:** Deterministic implementation is complete on PR 275. Protected deployed-provider characterization remains an external promotion gate, so Fastly limitations remain `BestEffort`. The unchecked steps below are retained as the original implementation record. +> **Superseded batch/lifecycle API:** The 2026-09-15 consumer-alignment plan replaces this +> plan's ordered-harvest API and test sentinels. Retain the text only as implementation history. +> **Backend-name follow-up:** Current names include a per-client namespace in addition to the +> deterministic identity digest, so separately constructed clients cannot collide in one session. + +**Goal:** Implement Fastly outbound HTTP with dispatch-all-before-wait fan-out, deterministic dynamic backends, host timer derivation, typed errors, streamed upload ownership, and bounded downstream conversion. + +**Architecture:** A target-neutral synchronous engine owns slots, backend identity/cache behavior, timing arithmetic, and cleanup. The SDK layer supplies `PendingRequest`, backend registration, and body handles. Batch dispatch is a distinct first phase; ordered harvest may opportunistically poll later slots but never loses a result. + +**Tech Stack:** Fastly SDK 0.13.1, `web-time`, `sha2`, core typed streams/decoders, native `test-utils`, wasm32-wasip1/Viceroy. + +--- + +## Preconditions + +- [ ] Phases 1b-5 pass all gates. +- [ ] The implementation index's inert workflow-bootstrap PR is merged to the default + branch, so `.github/workflows/outbound-fastly-characterization.yml` can receive + `workflow_dispatch` for an exact reviewed implementation SHA. Do not rely on a workflow + definition introduced only on this implementation branch. +- [ ] Phase 0 has also provisioned the protected `outbound-fastly-probe` environment, + required reviewers, exact disposable secrets, enabled/disabled services, fixed reviewed + probe ref, and authenticated observable-origin protocol. Workflow presence alone is not a + runnable host gate. +- [ ] Re-read spec §4.3, Fastly rows in §§5.2-5.5, and the Fastly file summary in §7. +- [ ] Keep outbound HTTP, deadlines, flexible phase budgets, slot isolation, streamed-upload deadlines, and lazy response passthrough at the exact reviewed BestEffort levels. Dynamic-backend service enablement is not statically provable, so `outbound-http` cannot be published as Native. +- [ ] Confirm `OutboundRequest::host_name()` exists from Phase 1b; never reparse the URI to construct backend identity. +- [ ] Require `viceroy --version` to print exactly the repository-pinned `0.21.0`. Run WASM tests from `crates/edgezero-adapter-fastly` so its committed `.cargo/config.toml` supplies both `wasm32-wasip1` and `viceroy run -C ../../examples/app-demo/crates/app-demo-adapter-fastly/fastly.toml --`; no CI environment override may replace that service configuration. + +## Task Protocol + +For each task: add only the named native or SDK-gated cases; run the exact native/WASM command and require a nonzero failure; implement through the shared production engine; rerun focused and package tests; run `git diff --check`; then stage only listed files and make the stated commit. Fake handles establish stage order and cleanup, not real host cancellation or a finite write bound. + +"Run native contracts" means +`scripts/run_test_nonzero.sh send_all_dispatches_every_slot_before_wait cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --test contract`. +"Run Fastly WASM contracts/library tests" means the two crate-local `wasm32-wasip1` +commands in Phase Verification with `fastly,test-utils`. Every command must execute a nonzero test count; a +target/linker error or a zero-test result is failure. + +**Required exact test names:** `backend_identity_uses_every_canonical_property`, `backend_creation_error_table_is_exhaustive`, `backend_builder_keeps_pooling_for_identical_identity_and_settings`, `dispatch_guard_checks_expiry_before_slack`, `send_all_dispatches_every_slot_before_wait`, `send_all_reports_per_slot_elapsed`, `one_slot_send_all_matches_send`, `request_preparation_consumes_entry_budget`, `adapter_final_dispatch_reapplies_request_normalization`, `canonical_uri_wire_serialization_table`, `send_all_reverse_completion_preserves_order`, `done_error_survives_poll_sweep`, `send_error_cause_table_preserves_timeout_source`, `response_content_length_rejects_before_body_poll`, `response_content_length_explicit_identity_rejects_before_body_poll`, `response_read_checks_deadline_after_eof`, `repeated_set_cookie_survives_response_conversion`, `decoder_stalls_timeout_at_all_completion_boundaries`, `streamed_upload_finishes_exactly_once`, `streamed_upload_failure_never_waits`, `downstream_fallback_preserves_typed_error_envelope`, and `adapter_capability_matrix_matches_outbound_spec`. + +**Expected red:** the old client waits during dispatch, backend names/settings collide, error variants map to generic 500, an EOF read escapes deadline checks, or failed uploads finish/wait. SDK-gated tests must fail assertions, not report zero tests. + +**Engine skeleton:** + +```rust +enum Slot { + Done(OutboundSlotResult), + Pending(PendingSlot), + Taken, +} + +struct BackendIdentity { + budget_ms: u32, + host: String, + port: u16, + scheme: Scheme, + tls_mode: TlsMode, +} +``` + +Phase one converts every valid slot to `Pending` or `Done`; no wait occurs. Phase two waits in input order, polls later pending slots opportunistically into `Done`, and replaces an unresolved `Taken` with internal error rather than panicking. + +### Task 1: Establish the engine and executable seams + +**Files:** +- Modify: `Cargo.toml` +- Modify: `examples/app-demo/Cargo.toml` +- Modify: `crates/edgezero-adapter-fastly/Cargo.toml` +- Create: `crates/edgezero-adapter-fastly/src/outbound.rs` beside temporary `src/proxy.rs` +- Create: `crates/edgezero-adapter-fastly/src/outbound/{backend,engine,sdk,test_utils}.rs` +- Modify: `crates/edgezero-adapter-fastly/src/lib.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` +- Modify: `Cargo.lock`, `examples/app-demo/Cargo.lock` + +- [ ] Pin root and app-demo `fastly` requirements to exact `=0.13.1`; refresh both locks with `cargo update --offline -p fastly --precise 0.13.1` and `cargo update --offline --manifest-path examples/app-demo/Cargo.toml -p fastly --precise 0.13.1`. Assert both `cargo tree --offline --locked` graphs resolve exactly `fastly v0.13.1` before writing exhaustive SDK mappings. +- [ ] Add direct `web-time` and independent `test-utils = []`; do not enable Fastly through test-utils. +- [ ] Move whole-file WASM gating to the SDK test module. Add a native deliberately failing smoke test and prove the native command executes it. +- [ ] Run `cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --test contract`; expect a nonzero test count and failure at the smoke assertion, not at linking/imports. +- [ ] Define injectable clock, dispatch-stage callback, backend registrar, synchronous pending/body handles, and hash function. Public retained callbacks must remain `Send + Sync`. +- [ ] Production and tests must call the same generic engine; test code may supply effects, not copy algorithms. +- [ ] Replace the sentinel with a passing seam-construction test and rerun the same native command; require a nonzero test count and success before committing. +- [ ] Commit: `test(fastly): add outbound engine seams`. + +### Task 2: Implement backend identity and host timers + +**Files:** +- Modify: `crates/edgezero-adapter-fastly/src/outbound/backend.rs` +- Modify: `crates/edgezero-adapter-fastly/src/outbound/engine.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` + +- [ ] Add failing native tests for DNS/IP/IPv6/plain/TLS identities, default/explicit ports, exact ceil-to-ms budgets, SHA-256 first-128-bit names, dedup, forced hash collision, externally occupied `NameInUse`, and concurrent lookup. Add a production-builder assertion that no `.enable_pooling(false)` override is applied: identical names/settings may use the SDK default pool, while distinct budgets/settings produce distinct identities. +- [ ] Add timer tests: for total >= 4 ms, connect = total/4, first byte = remainder, between = total; below 4 ms, connect/first each equal total. +- [ ] In the Fastly-featured SDK test module, add an exhaustive no-wildcard table over the real `fastly::backend::BackendCreationError`: `Disallowed` -> actionable unspecified 502; `NameInUse` -> cache/collision protocol; all three `*TimeoutTooLarge`, `NameTooLong`, and `EncodingError` -> internal 500; `HostError` -> unspecified 502. Native tests may exercise an adapter-owned policy model but cannot claim coverage of the SDK enum. +- [ ] Add guard-order tests before cold batch registration and immediately before dispatch: absolute expiry -> attributed 504; live-deadline setup over `BATCH_DISPATCH_SLACK_MAX` -> internal 500; no deadline uses the default budget without the live-deadline slack failure. Prove an over-slack later cache miss invokes no additional registration. +- [ ] Run native contracts; expect a nonzero failure. From `crates/edgezero-adapter-fastly`, run `../../scripts/run_test_nonzero.sh backend_creation_error_table_is_exhaustive cargo test --offline --locked --no-default-features --features fastly,test-utils --lib`; expect a nonzero compile/assertion failure from the missing real-SDK classifier. Do not override the committed target/runner. +- [ ] Implement `BackendIdentity(scheme, host_name, resolved_port, tls_mode, budget_ms)` and `ez_` names. Use true ceil-to-millisecond conversion, including a 1 ms + 1 ns -> 2 ms boundary case. Cache name -> `(identity, backend)` behind `Mutex`; never hold the lock across `finish()`. Leave SDK connection pooling at its default: Fastly reuses only same-name, exact-same-settings backends, and the identity/settings already separate budgets and TLS modes. Probe multi-host heterogeneous budgets because each distinct tuple may require synchronous registration. +- [ ] A cache miss followed by `NameInUse` fails closed. Do not recover with string matching or `Backend::from_name`. +- [ ] Implement the complete creation-error mapping separately from `SendErrorCause`; DNS/TLS/connect failures belong only to the send-stage table. +- [ ] Configure SNI only from `sni_hostname`; certificate checking from `cert_host`, including HTTPS IP literals. +- [ ] Rerun native contracts and the filtered Fastly WASM library test; each must execute a nonzero test count and succeed. +- [ ] Commit: `feat(fastly): configure deterministic outbound backends`. + +### Task 3: Implement two-phase send_all + +**Files:** +- Modify: `crates/edgezero-adapter-fastly/src/outbound/engine.rs` +- Modify: `crates/edgezero-adapter-fastly/src/outbound/sdk.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` + +- [ ] Add failing cases for empty batch, complete preflight and index alignment, every valid dispatch before first wait, one-slot `send_all`/`send` exact error equivalence, reverse completion with stable order, partial dispatch/wait/body errors, preservation of pre-existing `Done` results during poll sweeps, both still-pending and later-ready poll branches, one method-entry `batch_started_at`, distinct per-slot elapsed values, and unresolved-slot internal error. Advance the clock during normalization/preflight/backend preparation and prove the original `send`/`send_all` snapshot owns the budget and elapsed measurement. Preflight/dispatch failures sample immediately; an opportunistically polled early completion keeps its own elapsed value while an earlier index blocks later vector return; backwards injected time yields zero plus Internal, while legitimate same-tick completion may also be zero with its ordinary outcome. Include GET/HEAD body errors beating batch-only errors and no polls of rejected source streams. +- [ ] Capture the exact SDK request target and assert it matches core serialization for dot segments, percent-encoded delimiters, numeric IPv4 aliases, IDNA input, empty paths, and queries; Fastly may derive backend identity from accessors but must not reconstruct the request URI. +- [ ] Add `adapter_final_dispatch_reapplies_request_normalization`: mutate `Connection` nominations plus stale `Host`, `Content-Length`, and `Transfer-Encoding` through `headers_mut`, then inspect the final Fastly request and prove normalization ran immediately before SDK construction. Only canonical host override and adapter-owned framing may remain. +- [ ] Add serial-harvest cases showing a later slot can expire while waiting behind an earlier slot without cancellation or Native slot-isolation claims. +- [ ] Run native contracts; expect failure. +- [ ] Implement `Slot::{Done, Pending, Taken}`. Capture the monotonic snapshot as the first operation in each public send method, then run `validate_for_dispatch` exactly once per request before batch-only checks, budget selection, body polling, normalization, backend construction, or SDK request construction; never re-anchor. Construct `OutboundSlotResult` at each slot's actual terminal point, including preflight/dispatch errors and immediately after wait/poll harvest; final vector assembly performs no clock read. Issue every valid `send_async` sequentially before the first wait; pinned Fastly 0.13.1 returns when transmission begins and continues buffered upload in the background, so a stalled buffered upload may leave its slot unresolved but must not be modelled as synchronously preventing later dispatch calls. Then wait in input order and opportunistically poll later pending slots. +- [ ] Preserve all policy/method/mode fields in `PendingSlot`. Resolve every output index without panic. Do not use SDK `select()`, which cannot preserve stable slot identity. +- [ ] Harvest every successfully dispatched buffered batch request even when siblings fail. +- [ ] Rerun native contracts; expect success. +- [ ] Commit: `feat(fastly): dispatch fanout before ordered harvest`. + +### Task 4: Map SDK errors and process responses + +**Files:** +- Modify: `crates/edgezero-adapter-fastly/src/outbound/engine.rs` +- Modify: `crates/edgezero-adapter-fastly/src/outbound/sdk.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` + +- [ ] Add SDK-gated construction tests for every known public `SendErrorCause` listed in + §4.3, with a mandatory future `_` classifier arm. Split timeout policy into + `BudgetedTimeout` (`ConnectionTimeout`, `HttpResponseTimeout`) and `ProviderTimeout` + (`DnsTimeout`). Pass the absolute deadline, selected cause, and injected observation + instant into the pure classifier; test all four `BudgetSource` values before, exactly at, + and after expiry. Configured phase timers stay attributed; pre-deadline DNS timeout is 504 + with `BudgetSource::Unspecified`; at/after expiry the selected cause wins for every result. +- [ ] Add native response cases for cumulative adapter-visible upstream headers before normalization plus the synthetic proxy marker, repeated request fields, repeated response `Set-Cookie`, raw malformed response nomination/encoding, 3xx returned without a follow-up request, bodyless/205, encoded bytes, decoded output, Brotli window/decoder-state charge, independent final Buffered limits, encoding policy, gzip members/native EOF, errors, deadlines before/after each blocking read including EOF, and drop-on-early-termination. Unequal decoded/final caps prove raw passthrough bypasses only decoded accounting; `max_chunk_bytes` remains an emitted-item guarantee. +- [ ] Add identity/compressed/passthrough `Content-Length` cases proving malformed/conflicting values and encoded/effective-identity decoded overages reject before the first Fastly body read; include exactly one bare `Content-Encoding: identity`. Effective identity compares encoded, decoded, and final Buffered caps; raw passthrough compares encoded plus final Buffered but never decoded; compressed-to-decode compares only encoded. +- [ ] In both Buffered and Streamed modes, stall gzip and Brotli before decoded output, midstream, and after codec EOF but before native EOF. Assert attributed 504 after the blocking read returns, no decoder-end success before native EOF, and late typed source/completion preservation when observed in budget. +- [ ] Run native and Fastly WASM library/contract tests; expect failures. +- [ ] Map configured phase timeout -> attributed 504; early unconfigured DNS timeout -> + unattributed 504; any result at/after absolute expiry -> attributed 504; + DNS/destination/connect/TLS establishment before a response head -> typed 502 + `Unreachable`; later connection/I/O -> `Transport`; response framing -> `Protocol`; + shared JSON/gzip/Brotli failures preserve exact coding identity; local invariant/platform internal -> 500; unspecified + known/future -> unspecified 502 as specified. Define one + `DYNAMIC_BACKENDS_DISABLED_MESSAGE` constant with the exact spec diagnostic and use it for + `BackendCreationError::Disallowed` and the deployed disabled-service assertion. +- [ ] Apply the core response pipeline in order. Drop the owned native body on cap/decode/deadline/consumer termination; do not drain after failure and do not claim finite origin cancellation. +- [ ] Rerun native/WASM tests; expect success. +- [ ] Commit: `feat(fastly): classify and harvest outbound responses`. + +### Task 5: Implement streamed uploads + +**Files:** +- Modify: `crates/edgezero-adapter-fastly/src/outbound/engine.rs` +- Modify: `crates/edgezero-adapter-fastly/src/outbound/sdk.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` + +- [ ] Add failing tests for source/cap/pre-read/post-read/write/flush failures, future drop, clean EOF, `finish` exactly once, finish failure, post-finish expiry, and no wait after failed finish. +- [ ] Retain writer and pending handles together. Every failure drops both without `finish`/`wait`; clean EOF finishes once and only then permits wait. +- [ ] Document that source pulls and host writes can stall beyond the cooperative check and that partial bytes may already be delivered. +- [ ] Rerun native/WASM tests; expect success. +- [ ] Commit: `feat(fastly): own streamed outbound uploads`. + +### Task 6: Migrate request injection and downstream conversion + +**Files:** +- Modify: `crates/edgezero-adapter-fastly/src/request.rs` +- Delete: `crates/edgezero-adapter-fastly/src/proxy.rs` +- Modify: `crates/edgezero-adapter-fastly/src/lib.rs` +- Modify: `crates/edgezero-adapter-fastly/src/response.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` + +- [ ] Inject one `HttpClient::with_client(FastlyOutboundClient::new())` per Fastly session so the backend cache lifetime mirrors the SDK namespace. +- [ ] Delete the legacy module during this switch and leave no module/type alias. +- [ ] Add exact/over `FASTLY_RESPONSE_STREAM_BUFFER_BYTES = 16 MiB` tests and typed stream-error envelope tests. +- [ ] Implement bounded fallback before platform headers are committed. Preserve original status/kind on failure. +- [ ] Rerun adapter and workspace tests; expect success. +- [ ] Commit: `feat(fastly): wire outbound client and response fallback`. + +### Task 7: Land CI gates + +**Files:** +- Modify: `.github/workflows/test.yml` +- Modify: `crates/edgezero-adapter-fastly/src/cli.rs` +- Create: `crates/edgezero-adapter-fastly/tests/host/deployed.sh` +- Create: `crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/Cargo.toml` +- Create: `crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/Cargo.lock` +- Create: `crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/fastly.toml` +- Create: `crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/src/main.rs` +- Create: `crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/README.md` + +- [ ] Add the native contract command and enable `test-utils` in Fastly WASM contract/library runs. Execute every test target through `scripts/run_test_nonzero.sh` with one exact sentinel; retain production target checks. +- [ ] Assert `viceroy --version` is exactly `0.21.0`, then execute both WASM suites from the adapter crate without `CARGO_TARGET_*_RUNNER`; this proves the checked-in runner and demo `fastly.toml` are used locally and in CI. +- [ ] Add the generated Fastly adapter target check: + `(cd examples/app-demo && cargo check --offline --locked -p app-demo-adapter-fastly --no-default-features --features fastly --target wasm32-wasip1)`. +- [ ] Add a standalone deployed-probe crate with an empty `[workspace]`, exact + `fastly = "=0.13.1"`, path dependencies on the production adapter/core, and its own + committed lock. The binary directly exercises `FastlyOutboundClient` for every named + probe case; it must not depend on the not-yet-migrated app-demo handler. Pin the package + name and output to + `tests/fixtures/outbound-fastly/pkg/edgezero-outbound-probe.tar.gz`. Generate the fixture + lock once with + `cargo check --offline --manifest-path crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/Cargo.toml --target wasm32-wasip1`; + subsequent metadata, tree, and build commands are locked. Its `fastly.toml` build script + runs Cargo with `--offline --locked --profile release --target wasm32-wasip1`. +- [ ] Before any protected dispatch, run locked metadata/tree checks for the fixture, require + exactly Fastly SDK 0.13.1, and build without secrets using + `fastly compute build --non-interactive --dir crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly`. + Require the fixed package path to exist. `deployed.sh` accepts only that prebuilt package; + it exits before deployment if the artifact is absent and contains no build, dependency + download, or package-selection fallback while secrets are present. +- [ ] Confirm each command lists and executes nonzero tests. +- [ ] Add and execute `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec`; clippy compilation alone does not execute feature-gated capability tests. +- [ ] Add table-driven capability tests and publish Fastly's override only now: Native for header fidelity; BestEffort for outbound HTTP, deadlines, flexible phase budget, slot isolation, streamed-upload deadlines, and lazy response passthrough; Unsupported for complete resource accounting; future capabilities -> Unsupported. +- [ ] Use the already-merged default-branch trusted characterization dispatcher with the protected `outbound-fastly-probe` environment. It accepts an exact commit SHA, checks out and verifies that SHA, installs Fastly CLI `15.1.0`, and uses only disposable `FASTLY_DYNAMIC_SERVICE_ID`, `FASTLY_DYNAMIC_SERVICE_URL`, `FASTLY_DISABLED_SERVICE_ID`, `FASTLY_DISABLED_SERVICE_URL`, `FASTLY_API_TOKEN`, `OUTBOUND_PROBE_ORIGIN_URL`, and `OUTBOUND_PROBE_ORIGIN_TOKEN` secrets. Never use `pull_request_target`, expose secrets to a fork checkout, or modify a branch-only workflow and assume it is dispatchable. +- [ ] `tests/host/deployed.sh` records each service's prior active version, deploys the fixed prebuilt probe package to both disposable services, sends a unique run ID to an authenticated observable origin, and requires exact positive probe IDs for canonical arbitrary destinations, request method/body, timeout phases, upload cancellation characterization, and response streaming. Characterize which `SendErrorCause` each configured connect/response timer and the provider DNS timeout produces. The enabled service must succeed; the disabled service must return the exact `DYNAMIC_BACKENDS_DISABLED_MESSAGE` typed 502 and must not contact the origin. In a trap, reactivate each recorded prior version, delete only versions created by this run, and delete this run's origin observations; never delete a pre-existing version. Any cleanup failure fails the job. Record the workflow URL, commit SHA, tool versions, and observed bounds in the PR. This evidence characterizes the BestEffort row; it does not promote it to Native. +- [ ] For a fork PR, run only non-secret native/Viceroy checks automatically. A maintainer first reviews, reproduces, and pushes the exact tree to `refs/heads/outbound-probe-reviewed`, then dispatches the protected workflow for that SHA. Missing live evidence does not silently skip a probe or report zero tests. +- [ ] Commit: `ci: execute Fastly outbound contracts`. + +## Phase Verification + +- [ ] `scripts/run_test_nonzero.sh send_all_dispatches_every_slot_before_wait cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --test contract` +- [ ] `test "$(viceroy --version)" = "viceroy 0.21.0"` +- [ ] `(cd crates/edgezero-adapter-fastly && ../../scripts/run_test_nonzero.sh send_all_dispatches_every_slot_before_wait cargo test --offline --locked --no-default-features --features fastly,test-utils --test contract)` +- [ ] `(cd crates/edgezero-adapter-fastly && ../../scripts/run_test_nonzero.sh backend_creation_error_table_is_exhaustive cargo test --offline --locked --no-default-features --features fastly,test-utils --lib)` +- [ ] `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec` +- [ ] `cargo check --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly --target wasm32-wasip1` +- [ ] `(cd examples/app-demo && cargo check --offline --locked -p app-demo-adapter-fastly --no-default-features --features fastly --target wasm32-wasip1)` +- [ ] `cargo metadata --offline --locked --format-version 1 --manifest-path crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/Cargo.toml` +- [ ] `cargo tree --offline --locked --manifest-path crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/Cargo.toml | rg 'fastly v0\.13\.1'` +- [ ] `fastly compute build --non-interactive --dir crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly` +- [ ] `test -f crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/pkg/edgezero-outbound-probe.tar.gz` +- [ ] `cargo fmt --all -- --check` +- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` +- [ ] `cargo test --workspace --all-targets` +- [ ] `cargo check --workspace --all-targets --features "fastly cloudflare spin"` +- [ ] `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin` +- [ ] `(cd examples/app-demo && cargo test --locked --workspace --all-targets)` +- [ ] Push the reviewed phase HEAD to `refs/heads/outbound-probe-reviewed`, dispatch the + default-branch `.github/workflows/outbound-fastly-characterization.yml` with + `commit_sha=$(git rev-parse HEAD)`, and attach a successful workflow URL whose checked-out + SHA matches exactly; this remains characterization of the BestEffort service prerequisite. +- [ ] `git diff --check` + +Expected result: Fastly dispatches all eligible fan-out slots before harvest, preserves typed per-slot outcomes, and documents rather than overclaims its unavoidable host-blocking gaps. diff --git a/docs/superpowers/plans/2026-09-06-outbound-http-phase7-migration-docs.md b/docs/superpowers/plans/2026-09-06-outbound-http-phase7-migration-docs.md new file mode 100644 index 000000000..983a24e17 --- /dev/null +++ b/docs/superpowers/plans/2026-09-06-outbound-http-phase7-migration-docs.md @@ -0,0 +1,242 @@ +# Outbound HTTP Phase 7: Public Migration, Documentation, and Final Gates Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> +> **Status:** Deterministic migration, hard-cut API removal, generated/demo alignment, and documentation gates are implemented on PR 275. Protected provider evidence remains external and does not promote `BestEffort` cells. The unchecked steps below are retained as the original implementation record. +> **Superseded capability/batch surface:** The 2026-09-15 consumer-alignment plan owns the +> current completion-order, cancellation, cache-bypass, authority-override, and Fastly lifecycle +> hard cut. The matrices and commands below are historical. + +**Goal:** Migrate all remaining consumers, delete the legacy proxy API without aliases, publish capability/outbound documentation, and make every generated, example, adapter, WASM, host, and docs gate executable in CI. + +**Architecture:** Migrate non-workspace consumers before deleting core's temporary legacy module. Then enforce the hard public cutoff with a scoped symbol scan. Deterministic merge gates stay separate from the required protected Fastly characterization and the optional Spin teardown characterization; Cloudflare host evidence is already mandatory in Phase 4. + +**Tech Stack:** Rust workspace and generated workspaces, Handlebars templates, VitePress/npm, GitHub Actions, ripgrep structural gates. + +--- + +## Preconditions + +- [ ] Phases 1b-6 pass all their deterministic and required host gates. +- [ ] All four adapters already use `outbound` modules and `HttpClient`; only core scaffolding, generated/template consumers, app-demo, and prose may retain legacy names. +- [ ] Re-read spec §§5-7 and the final capability matrix. Do not promote BestEffort cells during documentation cleanup. + +## Task Protocol + +For each task: first add or run the named structural/build test and capture the expected stale-symbol, missing-capability, or compile failure; perform only that migration slice; rerun it to success plus `git diff --check`; then stage only listed files and make the stated commit. Do not combine the hard core deletion with unrelated prose cleanup. + +**Required exact test/gate names:** `generated_workspace_compiles`, `generated_manifest_declares_outbound_http_optional`, `generated_spin_hosts_default_to_https_only`, `generated_outbound_http_smoke`, `generated_core_tests_execute`, `generated_core_test_gate_rejects_zero_tests`, `app_demo_outbound_client_preserves_method_and_uri`, `app_demo_manifest_declares_outbound_http_optional`, `typed_body_stream_boundaries_preserve_edge_error`, `capabilities_page_lists_exact_matrix`, `docs_sidebar_links_capabilities`, and `outbound_docs_contract`. The source-wide legacy-symbol check is owned by `scripts/check_outbound_legacy_api.sh`, not by a Rust test that would need to embed the forbidden strings. + +**Expected red:** generated/template tests find missing capabilities or stale behavior; app-demo fails against the new API; the hard-cut scan reports exact remaining source/test/template paths; docs checks find missing matrix/sidebar content. Each red result must disappear in its owning task, not be added to an exclusion. + +### Task 1: Migrate scaffolds and strengthen generated-project tests + +**Files:** +- Modify: `crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs` +- Modify: `crates/edgezero-cli/src/templates/root/edgezero.toml.hbs` +- Modify: `crates/edgezero-cli/src/templates/root/README.md.hbs` +- Modify: `crates/edgezero-adapter-spin/src/templates/spin.toml.hbs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `crates/edgezero-cli/tests/generated_project_builds.rs` + +- [ ] Add failing structural tests for `optional = ["outbound-http"]`, outbound API names, Spin absent-host rendering as HTTPS-only, explicit wildcard HTTP+HTTPS expansion, and no implicit cleartext grant. The generated project is portable across all four adapters, so it cannot require a capability Fastly truthfully reports BestEffort. Generated README text states that `optional` explicitly accepts both documented behavioral deviations and runtime failure when an unverified deployment prerequisite is absent; an app whose behavior depends on outbound success must promote the declaration to `required` and select an adapter/deployment whose matrix result passes. Do not add a Rust assertion containing legacy API spellings; Task 3's source-wide negative gate owns their absence after all code/tests/templates migrate. +- [ ] First add only the harness regression + `generated_core_test_gate_rejects_zero_tests`. Run + `cargo test -p edgezero-cli --test generated_project_builds generated_core_test_gate_rejects_zero_tests` + and require a nonzero assertion failure showing the current harness accepts an + otherwise-successful zero-test fixture; dependency/build failure or a filtered zero-test + command is not the expected red. +- [ ] Add a generated core test named `generated_outbound_http_smoke`. Extend the generated-project harness to run `cargo test -p scaffold-probe-core --lib -- --list`, parse listed test identifiers, and require at least one test plus exactly one identifier whose terminal component is `generated_outbound_http_smoke` (the current full name is `handlers::tests::generated_outbound_http_smoke`). Match an optional Rust module prefix ending in `::`, not arbitrary diagnostic text or an impossible bare output line. Then run `cargo test -p scaffold-probe-core --lib`. Rerun the exact `generated_core_test_gate_rejects_zero_tests` command and require a nonzero passing test, then run the `generated_workspace_compiles` sentinel gate below. +- [ ] Run `cargo test -p edgezero-cli generator`; expect failure, then migrate templates/renderers and rerun. +- [ ] Run `scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test -p edgezero-cli --test generated_project_builds`; expect generated compilation and nonzero core tests. +- [ ] Commit: `feat(scaffold): generate outbound HTTP clients`. + +### Task 2: Migrate app-demo and its independent lockfile + +**Files:** +- Modify: `examples/app-demo/edgezero.toml` +- Modify: `examples/app-demo/crates/app-demo-core/src/handlers.rs` +- Modify: `examples/app-demo/crates/app-demo-adapter-spin/spin.toml` +- Modify: `examples/app-demo/crates/app-demo-cli/tests/config_flow.rs` +- Modify: `examples/app-demo/Cargo.lock` + +- [ ] Add/update tests for URI merge/query preservation, injected client success, no-client 501, original method/URI propagation, `OutboundResponse::into_response`, and shipped capability parsing. +- [ ] Before migration, run `(cd examples/app-demo && cargo test --offline --locked -p app-demo-core app_demo_outbound_client_preserves_method_and_uri)` and require a nonzero failure from missing/stale outbound behavior, not dependency resolution or a zero-test filter. +- [ ] Update the mock to implement both required trait methods and preserve partial batch results. +- [ ] Declare outbound HTTP optional so the multi-adapter demo remains deployable to Fastly + with the standard BestEffort warning; make selected Spin hosts match the canonical expected + set. The demo handler must render the typed outbound failure, including the exact + dynamic-backend-disabled 502, rather than assuming optional means success. The test must + still prove the capability is present and not silently omitted. +- [ ] Run these exact independent-workspace gates and require success: `(cd examples/app-demo && cargo fmt --all -- --check)`, `(cd examples/app-demo && cargo clippy --workspace --all-targets --all-features -- -D warnings)`, `(cd examples/app-demo && cargo test --locked --workspace --all-targets)`, `(cd examples/app-demo && cargo check --locked -p app-demo-adapter-cloudflare --target wasm32-unknown-unknown --no-default-features --features cloudflare)`, `(cd examples/app-demo && cargo check --locked -p app-demo-adapter-fastly --target wasm32-wasip1 --no-default-features --features fastly)`, and `(cd examples/app-demo && cargo check --locked -p app-demo-adapter-spin --target wasm32-wasip2 --no-default-features --features spin)`. +- [ ] Commit: `refactor(app-demo): migrate to outbound HTTP API`. + +### Task 3: Perform the hard core cutoff + +**Files:** +- Delete: `crates/edgezero-core/src/proxy.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-core/src/context.rs` +- Create: `scripts/check_outbound_legacy_api.sh` +- Modify: each remaining runtime/template path printed by the first red run of that script; adding an exclusion instead is not permitted + +- [ ] Create `scripts/check_outbound_legacy_api.sh` as the deterministic negative gate for code and generated templates in this task. It initially scans `crates` and `examples/app-demo` for only the legacy API symbols below. Task 4 expands its roots to active documentation after that task owns the prose migration. Implement exit handling explicitly: ripgrep status 0 prints matches and exits 1; status 1 exits 0; status greater than 1 propagates as a scan failure. + +```sh +#!/usr/bin/env bash +set -uo pipefail + +if matches="$(rg -n 'Proxy(Client|Handle|Request|Response|Service)|proxy_handle|edgezero_core::proxy|crate::proxy|pub mod proxy' \ + crates examples/app-demo --glob '*.rs' --glob '*.hbs')"; then + printf '%s\n' "$matches" + exit 1 +else + status=$? + if [ "$status" -eq 1 ]; then exit 0; fi + exit "$status" +fi +``` + +- [ ] Run `bash scripts/check_outbound_legacy_api.sh`; expect failure with every remaining source/test/template match. Migrate each match; do not preserve forbidden spellings in Rust string-literal assertions because this raw-source gate intentionally rejects them too. + +```sh +bash scripts/check_outbound_legacy_api.sh +``` + +- [ ] Delete the legacy module and `proxy_handle`; export only outbound names. Add no deprecated aliases, forwarding traits, or feature flags. +- [ ] Preserve the wire/header constant `PROXY_HEADER`, `x-edgezero-proxy`, route names such as `/proxy/{*rest}`, and ordinary reverse-proxy terminology where semantically correct. +- [ ] Rerun `bash scripts/check_outbound_legacy_api.sh`; expect exit 0 and no output. Run workspace and app-demo tests. +- [ ] Separately inventory every typed-stream boundary; this audit is not satisfied by the legacy-symbol scan: + +```sh +rg -n 'Body::from_stream|Body::from_external_stream|Body::Stream|Self::Stream|\.into_stream\(' \ + crates examples/app-demo --glob '*.rs' --glob '*.hbs' +``` + +- [ ] This reruns the Phase 2 audit rather than creating a new adjacent test for comments, definitions, or unchanged inbound introspection. Classify construction/consumption matches only: EdgeZero-owned outbound/decoder/deadline paths must carry `Result`; genuine platform/foreign inputs alone use `from_external_stream`. Existing Phase 2 and adapter error-identity tests must cover every error-producing owned boundary introduced by the migration. +- [ ] Rerun the inventory after migration and review every remaining construction/consumption line. Then run `cargo test --workspace --all-targets`, `cargo check --workspace --all-targets --features "fastly cloudflare spin"`, the Cloudflare WASM contract command, both Fastly WASM contract/library commands, and both Spin WASM contract/SDK-resource commands from Task 6. Compilation plus the named Phase 2/adapter error-identity tests is the `typed_body_stream_boundaries_preserve_edge_error` gate. +- [ ] Commit: `refactor(core): remove legacy proxy API`. + +### Task 4: Publish outbound and capability documentation + +**Files:** +- Modify: `docs/guide/proxying.md` +- Modify: `docs/guide/handlers.md` +- Modify: `docs/guide/architecture.md` +- Modify: `docs/guide/streaming.md` +- Modify: `docs/guide/what-is-edgezero.md` +- Modify: `docs/guide/configuration.md` +- Modify: `docs/guide/adapters/{overview,axum,cloudflare,fastly,spin}.md` +- Create: `docs/guide/capabilities.md` +- Modify: `docs/.vitepress/config.mts` +- Modify: `scripts/check_outbound_legacy_api.sh` +- Create: `scripts/check_outbound_docs_contract.mjs` +- Modify: `CLAUDE.md`, `.claude/agents/code-architect.md`, `Cargo.toml`, `TODO.md` where they describe the active API + +- [ ] Document all eight outbound capability names, support ladder semantics, exact matrix, every BestEffort footnote including Fastly's dynamic-backend service prerequisite, host grammar/defaults, request/response limits, batch memory model, and no-client behavior. The exact rows are: + +| Capability | Axum | Cloudflare | Fastly | Spin | +| --- | --- | --- | --- | --- | +| `outbound-http` | Native | Native | BestEffort | Native | +| `outbound-complete-resource-accounting` | Unsupported | Unsupported | Unsupported | Unsupported | +| `outbound-header-fidelity` | Native | BestEffort | Native | Native | +| `outbound-deadlines` | Native | BestEffort | BestEffort | BestEffort | +| `outbound-flexible-phase-budget` | Native | Native | BestEffort | BestEffort | +| `send-all-slot-isolation` | Native | Native | BestEffort | Native | +| `streamed-upload-deadlines` | Native | BestEffort | BestEffort | BestEffort | +| `lazy-streamed-response-passthrough` | Native | Native | BestEffort | BestEffort | + +- [ ] Explain that Cloudflare manual upstream fetch encoding and downstream encoded passthrough are distinct controls. +- [ ] Explain the bounded fallback used for precommit conversion failures and Native lazy passthrough on Axum and Cloudflare. +- [ ] Explain that the body/header/window/decoder-state controls bound named guest-visible terms, `max_chunk_bytes` shapes emitted items without limiting source allocation, and complete pre-admission process/isolate accounting is Unsupported on every current adapter because provider parser, field-section, native chunk, allocator, and host-copy terms remain opaque. +- [ ] Before editing prose, expand `scripts/check_outbound_legacy_api.sh` to scan `docs/guide`, `README.md`, `CLAUDE.md`, `TODO.md`, root `Cargo.toml`, and `.claude/agents/code-architect.md` in addition to its existing code/template roots, adding `*.md` and `*.toml` globs. Using explicit active-source roots excludes internal history under `docs/superpowers/specs` and `docs/superpowers/plans`. Run it and require the expected stale-document failure. +- [ ] Add exactly one sidebar item whose link is `/guide/capabilities`; verify all internal links/anchors. +- [ ] Create `scripts/check_outbound_docs_contract.mjs`. It reads `docs/guide/capabilities.md` and `docs/.vitepress/config.mts`, locates exactly one Markdown table with the exact header `Capability | Axum | Cloudflare | Fastly | Spin`, and parses every contiguous data row in that table rather than filtering by a name prefix. It strips backticks around the first cell and only a trailing superscript footnote reference (`¹` through `⁹`) or Markdown footnote reference (`[^…]`) from support cells, then deep-compares the resulting ordered entries to the literal eight-row matrix above. It fails on a missing/duplicate matrix table or any duplicate, missing, extra, reordered, misspelled, or differently supported row, including the two valid names that do not begin `outbound-`. It also requires exactly one `/guide/capabilities` sidebar link. These assertions own `capabilities_page_lists_exact_matrix`, `docs_sidebar_links_capabilities`, and `outbound_docs_contract`. +- [ ] Run `node scripts/check_outbound_docs_contract.mjs`; expect failure before the page/sidebar are complete, then exit 0 after the documentation change. +- [ ] Run `npm --prefix docs run lint`, `npm --prefix docs run format`, and `npm --prefix docs run build`; expect success. +- [ ] Run `bash scripts/check_outbound_legacy_api.sh` over the now-updated active docs; expect no stale API references. Legitimate protocol terminology is outside the script's exact symbol pattern and needs no broad exclusion. +- [ ] Commit: `docs: publish outbound HTTP and capabilities guide`. + +### Task 5: Make the completion matrix a CI contract + +**Files:** +- Modify: `.github/workflows/test.yml` +- Modify: `.github/workflows/format.yml` +- Modify: `scripts/run_tests.sh` +- Modify: `scripts/run_test_nonzero.sh` + +- [ ] Add/confirm native contract commands for all four adapters and `test-utils` in each supported WASM contract run. Route every native, browser-WASM, WASI SDK-resource, Fastly SDK, and CLI capability suite through `scripts/run_test_nonzero.sh [--ignored] `. The helper first runs the command with harness `--list` (and `--ignored` when requested), requires the sentinel as the exact terminal test-name component with only an optional Rust module prefix, and requires at least one listed test, then runs the same selected suite; cfg/feature drift to zero tests fails CI. The generated-project integration target uses this helper, while its nested generated-core command keeps Task 1's equivalent in-harness list/sentinel/count assertion because it runs from a generated directory. +- [ ] Add generated-project core tests, app-demo fmt/clippy/workspace-test and three WASM checks, and PR-time docs lint/format/build. Retain the existing explicit `examples/app-demo` format and clippy steps in `.github/workflows/format.yml`; the root workspace excludes that directory, so root gates cannot replace them. +- [ ] Retain Cloudflare workerd/deployed host-observed cancellation and timing jobs from Phase 4. +- [ ] Keep pinned Spin teardown characterization clearly non-blocking for its existing BestEffort claims. Retain Phase 6's required Fastly enabled/disabled entitlement characterization as evidence that the adapter actually issues requests and maps disabled-service failure; that evidence does not promote the static `outbound-http` cell. Any future Native promotion requires a deployment-aware prerequisite, new evidence, and a spec change. +- [ ] Add `bash scripts/check_outbound_legacy_api.sh` and `node scripts/check_outbound_docs_contract.mjs` as deterministic nonzero-on-violation gates, with the legacy scan roots documented beside its workflow step. +- [ ] Add the four CLI capability suites as explicit CI commands so the declarations and action-specific gate paths cannot disappear behind broad workspace success: + +```sh +scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec +scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec +scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec +scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec +``` + +- [ ] Make the generated-project job run `scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test -p edgezero-cli --test generated_project_builds`. The integration harness then performs Task 1's `generated_outbound_http_smoke` list/count assertion inside the generated workspace before executing its core suite. A successful zero-test command at either level is a CI failure. +- [ ] Commit: `ci: enforce outbound HTTP completion matrix`. + +### Task 6: Final full-system verification + +- [ ] `cargo fmt --all -- --check` +- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` +- [ ] `cargo test --workspace --all-targets` +- [ ] `cargo check --workspace --all-targets --features "fastly cloudflare spin"` +- [ ] `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin` +- [ ] `scripts/run_test_nonzero.sh send_all_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --test contract` +- [ ] `scripts/run_test_nonzero.sh send_all_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils --test contract` +- [ ] `scripts/run_test_nonzero.sh send_all_dispatches_every_slot_before_wait cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --test contract` +- [ ] `scripts/run_test_nonzero.sh send_all_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils --test contract` +- [ ] `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec` +- [ ] `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec` +- [ ] `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec` +- [ ] `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_outbound_spec cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features cli --lib adapter_capability_matrix_matches_outbound_spec` +- [ ] `env CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh cloudflare_fetch_options_are_raw_manual_abortable_and_no_redirect cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --test contract` +- [ ] `(cd crates/edgezero-adapter-cloudflare && ../../scripts/run_test_nonzero.sh cloudflare_fetch_options_are_raw_manual_abortable_and_no_redirect cargo test --offline --locked --no-default-features --features cloudflare,test-utils --test contract)`; this separate run has no target/runner override and proves the committed crate-local Cargo config selects `wasm32-unknown-unknown` plus `wasm-bindgen-test-runner`. +- [ ] `test "$(viceroy --version)" = "viceroy 0.21.0"` +- [ ] `(cd crates/edgezero-adapter-fastly && ../../scripts/run_test_nonzero.sh send_all_dispatches_every_slot_before_wait cargo test --offline --locked --no-default-features --features fastly,test-utils --test contract)` +- [ ] `(cd crates/edgezero-adapter-fastly && ../../scripts/run_test_nonzero.sh backend_creation_error_table_is_exhaustive cargo test --offline --locked --no-default-features --features fastly,test-utils --lib)` +- [ ] `cargo metadata --offline --locked --format-version 1 --manifest-path crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/Cargo.toml` +- [ ] `cargo tree --offline --locked --manifest-path crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/Cargo.toml | rg 'fastly v0\.13\.1'` +- [ ] `fastly compute build --non-interactive --dir crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly` +- [ ] `test -f crates/edgezero-adapter-fastly/tests/fixtures/outbound-fastly/pkg/edgezero-outbound-probe.tar.gz` +- [ ] `(cd crates/edgezero-adapter-spin && ../../scripts/run_test_nonzero.sh response_caller_result_succeeds_only_after_native_eof cargo test --offline --locked --no-default-features --features spin,test-utils --target wasm32-wasip2 --test contract)`; the crate-local runner must contain the exact Phase 5 validated command. +- [ ] `(cd crates/edgezero-adapter-spin && ../../scripts/run_test_nonzero.sh spin_error_code_table_is_exhaustive cargo test --offline --locked --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources)` +- [ ] `cargo metadata --offline --locked --format-version 1 --manifest-path crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/Cargo.toml` +- [ ] `cargo tree --offline --locked --manifest-path crates/edgezero-adapter-cloudflare/tests/fixtures/outbound-worker/Cargo.toml | rg 'worker v0\.8\.5'` +- [ ] `npm ci --prefix crates/edgezero-adapter-cloudflare` +- [ ] `npm --prefix crates/edgezero-adapter-cloudflare run build:outbound-fixture` +- [ ] `npm --prefix crates/edgezero-adapter-cloudflare run test:workerd`; the host driver requires every exact probe ID and a positive count. +- [ ] Push the reviewed final HEAD to `refs/heads/outbound-probe-reviewed`, dispatch the + default-branch `.github/workflows/outbound-cloudflare-deployed.yml` with + `commit_sha=$(git rev-parse HEAD)`, and require a successful workflow URL whose recorded + checkout SHA matches exactly and whose driver reports every exact probe ID plus a positive + count. A direct credentialed npm invocation does not prove protected-environment or + default-branch workflow behavior. +- [ ] Dispatch the default-branch + `.github/workflows/outbound-fastly-characterization.yml` for the same exact reviewed SHA + and require its successful workflow URL, exact checkout SHA, positive probe count, and + cleanup result. +- [ ] `scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test -p edgezero-cli --test generated_project_builds` +- [ ] `(cd examples/app-demo && cargo fmt --all -- --check)` +- [ ] `(cd examples/app-demo && cargo clippy --workspace --all-targets --all-features -- -D warnings)` +- [ ] `(cd examples/app-demo && cargo test --locked --workspace --all-targets)` +- [ ] `(cd examples/app-demo && cargo check --locked -p app-demo-adapter-cloudflare --target wasm32-unknown-unknown --no-default-features --features cloudflare)` +- [ ] `(cd examples/app-demo && cargo check --locked -p app-demo-adapter-fastly --target wasm32-wasip1 --no-default-features --features fastly)` +- [ ] `(cd examples/app-demo && cargo check --locked -p app-demo-adapter-spin --target wasm32-wasip2 --no-default-features --features spin)` +- [ ] `bash scripts/check_outbound_legacy_api.sh` +- [ ] `node scripts/check_outbound_docs_contract.mjs` +- [ ] `npm --prefix docs ci` +- [ ] `npm --prefix docs run lint` +- [ ] `npm --prefix docs run format` +- [ ] `npm --prefix docs run build` +- [ ] Rerun the Task 3 typed-stream inventory. Every construction/consumption match must retain its reviewed owned/external classification; compilation plus the named Phase 2 and adapter tests must cover each error-producing owned boundary introduced by the migration. Then run `git diff --check`. +- [ ] Review `git status --short`; only intended implementation/docs/lock/CI changes may remain. + +Expected result: the repository has one public outbound HTTP API, generated consumers and app-demo exercise it, documentation matches the exact capability contract, and CI covers every deterministic acceptance surface. diff --git a/docs/superpowers/plans/2026-09-08-inbound-ingress-admission.md b/docs/superpowers/plans/2026-09-08-inbound-ingress-admission.md new file mode 100644 index 000000000..bb069360e --- /dev/null +++ b/docs/superpowers/plans/2026-09-08-inbound-ingress-admission.md @@ -0,0 +1,224 @@ +# Inbound Ingress Admission Implementation Plan + +> **Status:** Partially implemented in PR #275. Core route resolution, admission, grants, +> request timing, lazy bounded body state, adapter entry seams, and the opt-in bounded body +> drain with a transferable admission grant and application-selected overflow/timeout responses +> before canonical 404/405 responses are implemented. The Axum raw-parser boundary and +> deployed cancellation probes remain open; raw framing/head-limit capabilities stay +> `Unsupported`, and every current adapter reports host-managed head accounting and framing. +> This plan is owned by the +> [inbound-body design](../specs/2026-08-22-inbound-body-design.md), not the outbound HTTP +> implementation phases. +> +> Checkboxes record implementation status. Mixed adapter/certification tasks remain unchecked +> until every assertion in that task has named evidence. + +**Goal:** Add stable pre-dispatch route resolution, one app-owned ingress admission gate, +request-owned grants, absolute body-read deadlines, parser-level request-target/header +limits and raw HTTP/1 framing rejection on Axum, and lazy bounded inbound-body consumption +across all adapters. + +**Architecture:** Each adapter stamps one monotonic request start and validates raw framing +where possible. Core resolves the route once without dispatching, admission sees that stable +resolution, and admitted requests dispatch through an opaque single-use token. The router +constructs `RequestContext` with the original start, route metadata, one-shot grant, and a +lazy deadline-bound body. Core owns the body cache/poison state machine; adapters own native +read cancellation and honest capability claims. + +**Dependencies:** The outbound Phase 1a `Deadline`, `BudgetSource`, and `BadGatewayReason` +work, plus the Phase 2 `ResponseLimitReason` work, must be present before the total +`StoredError` match lands. No outbound adapter client phase is otherwise required. + +## Global Constraints + +- Do not add Tokio to core or shared adapter crates. +- Route resolution may run before admission; middleware, handlers, extractors, and body + polling may not. +- A resolved token is consumed once and cannot be replayed against another router or a + changed method/path. +- Every admitted body has one finite absolute deadline. Relative per-read timeout resets are + forbidden. +- Raw head limits and framing validation must use a pre-normalization parser boundary. A + normalized target or `HeaderMap` check cannot claim parser-allocation or + request-smuggling protection. +- Preserve `RequestContext::new(Request, PathParams)` for low-level callers; it records no + route or admission grant. +- Follow red-green-refactor for every behavioral task and run the focused test after each + production edit. + +## Task 1: Stable route identity and single-use resolved dispatch + +**Files:** `crates/edgezero-core/src/router.rs`, route tests, public re-exports. + +- [x] Write red tests for structural `RouteId(method, registered_pattern)`, dynamic path + independence, optional manifest route class on matched/405 metadata, deterministic 405 + candidates, and `NotFound`. +- [x] Write red tests proving `resolve` executes no middleware/handler/body poll and captures + exact path params. +- [x] Write red tests proving `dispatch_resolved` does not rematch, consumes its token, and + rejects a foreign-router token or changed method/path as `Internal`. +- [x] Add `RouteId`, class-bearing `RouteMetadata`, `RouteResolution`, opaque + `ResolvedDispatch`, `RouterService::resolve`, `RouterBuilder::route_with_class`, and + `dispatch_resolved` exactly as specified. Route class must not enter `RouteId`. +- [x] Run `cargo test -p edgezero-core --lib router`. + +## Task 2: App admission policy, parser limits, and normalized ingress head + +**Files:** `crates/edgezero-core/src/app.rs`, app tests, macro/configure tests. + +- [x] Write red tests for validated nonzero `IngressHeadLimits`, `IngressHeadAccounting`, + `IngressFraming`, immutable/body-blind `IngressHead`, `AdmissionDecision`, and non-clone + opaque `IngressGrant` downcast behavior. +- [x] Write red ordering tests: framing, resolution, admission, then dispatch; refusal skips + middleware/handler/body polling and retains the chosen response. +- [x] Add the synchronous policy and immutable head-limit setters on `App`; preserve both + through complete app-to-service construction rather than cloning only `app.router()`. +- [x] Add the cloneable app-owned `MonotonicClock`, its setter/snapshot accessors, and tests + proving admission retains that exact handle rather than resnapshotting a global clock. +- [x] Add the default decision: empty grant plus + `request_start + DEFAULT_INBOUND_READ_BUDGET` (30 seconds). Clamp every policy deadline + to `request_start + DEADLINE_FAR_FUTURE` with checked arithmetic; reject overflow or an + invalid policy result before body ownership moves. +- [x] Run `cargo test -p edgezero-core --lib app`. + +## Task 3: RequestContext ingress metadata and grant lifetime + +**Files:** `crates/edgezero-core/src/context.rs`, router/context tests. + +- [x] Write red tests proving `IngressHead` and routed `RequestContext` expose the identical + `MonotonicInstant`, paired `MonotonicClock`, and class-bearing `RouteMetadata` values. +- [x] Write red drop-count tests for taken, untaken, refused, 404, and 405 grants. A second + `take_ingress_grant()` returns `None`; no grant type is clonable or serialized. +- [x] Preserve `RequestContext::new(Request, PathParams)`: snapshot start at construction, + expose no route, return no grant, and make no admission claim. +- [x] Add a crate-private routed constructor consuming `AdmittedIngress`; initialize all + metadata and the admitted clock before middleware starts. +- [x] Run `cargo test -p edgezero-core --lib context`. + +## Task 4: Lazy body cell, bounded extractors, and absolute deadline + +**Files:** `crates/edgezero-core/src/body.rs`, `context.rs`, `extractor.rs`, `error.rs`. + +- [x] Write red state-machine tests for `Initial`, `Draining`, `Cached`, `Poisoned`, and + `Taken`, including reentrancy and dropped-drain poison. +- [x] Write red deadline races for first byte, inter-chunk waits, EOF, source error, and + simultaneous readiness using the admitted clock. Expiry wins and remains sticky as + `RequestTimeout` (408). +- [x] Write red cap tests for exact limit, first byte over, checked accounting overflow, and + stricter re-check of already cached bytes without poisoning the cache. +- [x] Implement private `BodyCell`/`StoredError`, no borrow across await, and fallible + `take_body`/`into_request`. Keep the total error match wildcard-free. +- [x] Migrate JSON/form extractors to the documented defaults and explicit `Within` forms. +- [x] Run `cargo test -p edgezero-core`. + +## Task 5: Future Axum raw request-head and framing certification + +**Files:** pinned Hyper patch/upstream hook, workspace dependency patch, Axum connection +setup/request conversion, raw-socket integration tests. + +Current Axum ingress uses `IngressHeadAccounting::HostManaged` and +`IngressFraming::HostManaged`. Unchecked raw-accounting/framing items in this task are future +promotion evidence, not claims about the current adapter. + +The separate bounded HTTP/1 resource boundary is implemented: validated `AxumIngressConfig`, a +finite connection-owning accept pool, pinned Hyper 1.12.0 combined raw-head/field limits, waking +absolute idle-plus-header timers, and response-head cache compaction. TCP tests cover saturation, +initial/subsequent trickling, idle expiry, exact/over-limit heads, disconnect recovery, and shutdown. +See the [Axum guide](../../guide/adapters/axum.md#bounded-http1-ingress) for its effective configuration +and allocation assumptions. This evidence does not complete the independent raw-accounting or +strict framing items below; HTTP/2 and upgrades remain outside the current server contract. + +- [ ] Add red raw-socket tests for exact/over-limit request-target bytes, raw header bytes, + and field-line count; checked accounting overflow; CL+TE in both orders; duplicate + equal/unequal and comma-list CL; signed/malformed/overflow CL; + repeated/non-final/unsupported TE; HTTP/2 TE; and valid controls. +- [ ] Assert target overflow is 414, header byte/count overflow is 431, diagnostics echo no + request data, and every head rejection happens before resolution/admission/body polling. + Exact limits pass and expose the exact `IngressHeadAccounting::RawValidated` totals. +- [ ] Assert rejection is 400, closes HTTP/1 or resets only the multiplexed stream, invokes + no admission policy, and polls no body. +- [ ] Characterize pinned Hyper 1.12.0 first: preserve source references and tests proving a + `Content-Length` after `Transfer-Encoding` is skipped and equal duplicate lengths are + accepted. These are the red cases the patch must change. +- [x] Replace `axum::serve` with a bounded connection-owning Hyper HTTP/1 accept path while + preserving connect metadata, keep-alive, the existing service/body/egress contract, and + shutdown cleanup. Configure `max_headers`, `max_header_size`, `max_buf_size`, and waking + `header_read_timeout` as resource defense in depth, not strict raw-accounting/framing evidence. +- [ ] Define separate resource models before enabling HTTP/2, socket upgrades, or a graceful + shutdown drain beyond the current bounded close-on-shutdown behavior. +- [ ] Add the smallest audited parser patch (or consume an upstream equivalent) in Hyper's + existing request-line/ordered `httparse` header path. Bound parser reads so the raw head is + rejected rather than accumulated past the installed target/header policy; count duplicate + field lines and exact syntax bytes. In the same boundary reject any CL+TE presence in + either order; any second or comma-list CL; and malformed, repeated/non-final `chunked`, or + unsupported transfer codings before normalization. Keep Hyper as the only HTTP/1 parser; + do not add an independent socket pre-parser that could disagree on pipelined boundaries. +- [ ] Pin the exact patched source/revision and add an upgrade gate that fails when the + patch no longer applies or its source assertions change. Document the upstream issue/PR + and removal condition. +- [ ] Only after raw acceptance, derive the normalized `IngressFraming` summary from the + surviving headers and HTTP version and attach the raw accounting totals. Do not claim the + summaries themselves performed parser enforcement or smuggling validation. +- [ ] Convert accepted bodies lazily, wrap native cancellation under the admitted absolute + deadline, and dispatch with the exact resolved token. +- [ ] Run Axum unit, integration, and raw-socket suites. + +## Task 6: Cloudflare, Fastly, and Spin ingress implementations + +**Files:** each adapter request/service entry point and contract tests. + +Axum shares the current `HostManaged` baseline described in Task 5. This task records the +equivalent baseline and target-specific deadline behavior for the other three adapters. + +- [ ] Write per-adapter ordering tests with an observable first body poll. +- [ ] Stamp `request_start` from `App::monotonic_now()` at earliest guest entry, pass + `HostManaged` framing, attach + `IngressHeadAccounting::HostManaged`, apply only documented post-materialization + defense-in-depth limits, invoke policy once, and preserve the resolved token through + dispatch. +- [x] Implement pre-read/post-ready deadline checks against the admitted app clock and the + strongest available drop/abort primitive. Cloudflare and Spin also race pending reads + against a platform timer; Fastly does not claim preemption around synchronous host calls. +- [x] Add Spin WASI regressions with a permanently pending source for matched dispatch and + both fallback resolutions. Prove timer-first timeout, exact 408/application-selected + response behavior, and exactly-once source/grant release with a deterministic timer seam. +- [ ] Prove buffered provider values enter as `Body::Once` only after admission and within a + documented platform bound; all other paths use lazy `Body::Stream`. +- [ ] Run each adapter's contract suite, including its WASM target where applicable. + +## Task 7: Capabilities, generators, and deployed probes + +**Files:** manifest schema/parser, adapter registry, CLI checks, scaffold templates, docs. + +- [ ] Add separate `ingress-admission`, `inbound-read-deadlines`, + `raw-ingress-head-limits`, and `raw-ingress-framing-validation` cells with the exact + initial matrix in the spec. +- [ ] Test parse/display/round-trip, unknown value rejection, and fail-closed build/serve/ + deploy/demo behavior for Native requirements. +- [ ] Update manifest-generated and hand-written app construction so both retain admission + policy; keep low-level `into_router()` explicit about its lack of adapter admission. +- [x] Keep Cloudflare's pre-select cooperative zero-delay yield and document the frozen-clock + caveat. Spin uses the last positive app-clock snapshot to arm its provider timer and keeps + its post-ready app-clock check. +- [ ] Add Cloudflare and Spin deployed cancellation probes and retain Fastly cooperative + timing evidence. Store machine-readable target/runtime/version/tolerance results. +- [ ] Update adapter capability docs without upgrading a cell from local mocks alone. + +## Task 8: Final verification and integration boundary + +- [ ] Audit for eager request-body collection, post-materialization parser-limit claims, + normalized-header framing claims, rematching after admission, relative timeout resets, + wildcard `EdgeError` matches, and leaked grants. +- [ ] Run: + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace --all-targets +cargo check --workspace --all-targets --features "fastly cloudflare spin" +cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin +``` + +- [ ] Record deployed-probe results separately from deterministic local tests. +- [ ] Commit ingress work independently from outbound adapter phases so each contract can be + reviewed and reverted without changing the other. diff --git a/docs/superpowers/plans/2026-09-08-response-egress-implementation.md b/docs/superpowers/plans/2026-09-08-response-egress-implementation.md new file mode 100644 index 000000000..3e7d71608 --- /dev/null +++ b/docs/superpowers/plans/2026-09-08-response-egress-implementation.md @@ -0,0 +1,484 @@ +# Owned Response Egress Implementation Plan + +PR #275 follow-up expands local Axum TCP qualification: suppressed-body completion +resources, pending producers, non-reading clients, disconnect cleanup, native deadline +wakeup under a frozen injected clock, stopped source polling and single-connection recovery. +Hosted Cloudflare/Fastly/Spin evidence remains a separate unfulfilled qualification gate; +local tests and provider emulators must not be presented as deployed runtime proof. + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to +> implement this plan task by task. Apply `superpowers:test-driven-development` to every +> behavioral change and `superpowers:verification-before-completion` before claiming success. + +**Goal:** Replace response-object handoff and bounded buffering with adapter-owned response +delivery lifetimes across Axum, Cloudflare, Fastly, and Spin, while preserving one application +clock, one absolute deadline, exact accepted-byte accounting, and exactly one terminal report. + +**Architecture:** Core owns the portable lifecycle state machine, policy evaluation, fallback +lifecycle, and shared HTTP framing normalization. Each adapter owns a runtime-specific +coordinator and writer pump through its strongest available transport boundary. There is no +cross-adapter sink abstraction. The migration is a hard cut: obsolete response-returning APIs, +buffering constants, compatibility aliases, tests, docs, and generated entrypoints are removed. + +**Source of truth:** +[`2026-09-08-response-egress-design.md`](../specs/2026-09-08-response-egress-design.md). + +**Tech stack:** Rust 1.95, `http`/`http-body`, Hyper/Axum/Tokio, Cloudflare Workers JS streams, +Fastly Compute body handles, Spin SDK WASIp3, Handlebars generator templates. + +**Implementation status:** The hard-cut API migration and runtime-specific send-owning paths are +complete. Remaining unchecked items are evidence hardening that requires a deployed provider or +additional raw-socket fixtures; they do not justify a `Native` capability claim and are not hidden +as implementation success. + +## Global Constraints + +- Preserve the injected `MonotonicClock` from ingress through every egress sample. +- Evaluate the application response-egress policy exactly once per response attempt. +- Use one absolute write deadline. Never reset it per source poll, write, flush, or finish. +- Account only payload bytes accepted at the adapter's documented boundary, including an + accepted partial prefix before a terminal error. +- One adapter-local coordinator owns `ResponseEgressAttempt`; helpers report events to it and + never terminalize independently. +- Precommit fallback uses the same attempt, records fallback body metadata, preserves the + original terminal cause, and is bounded by the internal fallback safety deadline. +- After commit, never replace or append an application error body. +- Capability cells are `Native` only with direct target-level evidence, `BestEffort` when a + meaningful approximation is tested, and `Unsupported` otherwise. +- One explicitly transitional `#[doc(hidden)]` lifecycle method may coexist with the old method + from Task 1 through Task 8 so the workspace stays buildable while adapters migrate. It is not a + compatibility promise: each migrated adapter must eliminate its old call sites, the legacy scan + must show a monotonically decreasing inventory, and the final tree contains neither method pair + nor any old public API. +- After every production code edit, run the narrowest affected `cargo test -p ...` command. + +## Task 0: Prove the risky target boundaries before the API cut + +**Files:** +- Create: `crates/edgezero-adapter-axum/src/connection.rs` +- Modify: `crates/edgezero-adapter-axum/src/lib.rs` +- Test: colocated tests in `connection.rs` +- Test: `crates/edgezero-adapter-fastly/src/response.rs` +- Test: `crates/edgezero-adapter-spin/src/response.rs` +- Test: `crates/edgezero-adapter-cloudflare/src/response.rs` + +- [x] Add an executable Axum/Hyper HTTP/1 prototype that proves a private non-`Send` response + body can run on `LocalSet`, an independent timer can close a connection while Hyper is blocked + below `poll_frame`, and ordered response IDs can attribute a pipeline-closing deadline without + sharing the attempt between tasks. +- [x] Add a raw-socket proof that distinguishes frame production from the strongest observable + OS-acceptance boundary. If that boundary cannot be associated with one response, plan Axum + success as `HostHandoff`/`BestEffort` rather than `Completed`/`Native`. +- [x] Compile minimal target-gated prototypes for Fastly `StreamingBodyHandle`, Spin raw WASIp3 + response/result writers, and Cloudflare fixed-length/transform writers plus request signal. + These prototypes become the final private adapter primitives; no throwaway public API remains. +- [x] Run the focused Axum raw-socket tests and all three target checks before changing core's + public lifecycle surface. Stop and revise the design if any required primitive is unavailable. + +## Task 1: Harden the core lifecycle and fallback contract + +**Files:** +- Modify: `crates/edgezero-core/src/response_egress.rs` +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Test: colocated tests in the files above + +- [x] Add red tests for `RequestCancelled`, application versus fallback body kind, fallback + `Completed` versus `Aborted`, accepted partial-prefix accounting, terminal races, policy panic, + deadline normalization failure, fallback timeout, fallback write failure, backward/frozen + clocks, observer unwind containment, and category-only EdgeZero diagnostic logging. Treat the + application-owned Rust panic hook as an explicit trust boundary; do not replace it from a library. +- [x] Add the owned begin contract under an explicitly transitional `#[doc(hidden)]` method that + returns a live attempt and structured precommit cause when policy evaluation or deadline + normalization fails. Core must not pre-terminalize. Keep the old method only long enough for + unmigrated adapters to compile, and forbid new call sites. +- [x] Add `ResponseEgressBodyKind::{Application, Fallback}` and + `ResponseEgressFallbackDisposition::{Completed, Aborted}` to bounded terminal reports. +- [x] Add `ResponseEgressOutcome::RequestCancelled`. Defer removal of `ResponseReturned` and the + old begin method until every adapter uses the owned contract in Task 8; run a workspace compile + after each lifecycle signature transition. +- [x] Make fallback lifecycle mutation explicit on `ResponseEgressAttempt`; reject invalid + application-to-fallback transitions and preserve exactly-once observer delivery. +- [x] Ensure routing errors, admission refusals, 404, and 405 responses reach adapters as + `ResponseEgressEnvelope` values. Keep only the explicitly documented low-level core dispatch + escape hatch outside adapter serving paths. +- [x] Run `cargo test -p edgezero-core --lib response_egress` and + `cargo test -p edgezero-core --lib app`, then `cargo check --workspace --all-targets`. + +## Task 2: Centralize HTTP response framing preparation + +**Files:** +- Create: `crates/edgezero-core/src/response_egress_framing.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-core/src/response_egress.rs` +- Test: `crates/edgezero-core/src/response_egress_framing.rs` + +- [x] Add table-driven red tests for all `Content-Length` field lines and comma members, + whitespace, invalid decimal forms, overflow, identical duplicates, conflicting duplicates, + application `Transfer-Encoding`, `Trailer`, final 1xx, 101, successful CONNECT, HEAD, 204, 205, + and 304 precedence. +- [x] Add red tests proving every `Connection` field is parsed as comma-separated tokens and + removes both nominated fields and the standard hop-by-hop set before adapter conversion. +- [x] Add red body-length tests: exact `Body::Once`, incremental streamed equality, early EOF, + one-byte-over, accepted partial native writes, body-suppressed responses, immediate source + release without polling, and no application trailers. +- [x] Implement an adapter-facing prepared-response type containing normalized status, headers, + body transmission mode, request method, and optional declared length. Wrap the body with one + core length-enforcing stream so all adapters share incremental overrun and early-EOF behavior; + the wrapper must not contain runtime writer logic. +- [x] Expose the helper only as hidden adapter plumbing; application-facing response APIs remain + unchanged. +- [x] Run `cargo test -p edgezero-core --lib response_egress_framing` and then + `cargo test -p edgezero-core`. + +## Task 3: Replace Fastly response return with owned transmission + +**Files:** +- Modify: `crates/edgezero-adapter-fastly/src/response.rs` +- Modify: `crates/edgezero-adapter-fastly/src/request.rs` +- Modify: `crates/edgezero-adapter-fastly/src/lib.rs` +- Modify: `crates/edgezero-adapter-fastly/src/cli.rs` +- Modify: `crates/edgezero-adapter-fastly/Cargo.toml` +- Modify: `crates/edgezero-adapter-fastly/src/templates/src/main.rs.hbs` +- Modify: `examples/app-demo/crates/app-demo-adapter-fastly/src/main.rs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Test: `crates/edgezero-adapter-fastly/tests/contract.rs` +- Test: colocated tests in `response.rs` and `lib.rs` + +- [x] Add red adapter-local pump tests for empty, once, multi-chunk, short write, zero-progress + write, source error, write error, finish error, abandon failure, deadline equality at every + observable boundary, and exactly-once finish/abandon/report ordering. +- [x] Add red precommit fallback tests for policy panic, normalization/conversion failure, + already-expired deadline, HEAD suppression, fallback success, partial write, fallback safety + timeout, finish failure, original-cause preservation, and fallback metadata. +- [x] Prototype the low-level path under the real Fastly feature: initialize once, receive the + client request, create an empty `BodyHandle`, commit with + `ResponseHandle::stream_to_client`, and consume through `StreamingBodyHandle`. +- [x] Carry the original request method into framing preparation and prove HEAD and status-based + suppression drops the application source without polling it. +- [x] Implement exact short-write looping and account each accepted prefix before checking the + deadline or processing the next result. On postcommit failure, abandon the body handle and + retain the original cause. +- [x] Hard-cut `run_app` and extension/config variants to no-request, send-owning signatures. + Remove all response-returning service methods used by normal dispatch and remove + `#[fastly::main]` support. +- [x] In the same edit, migrate the Fastly scaffold, demo entrypoint, and generator assertions to + the no-request send-owning runner. Run generated-project and demo Fastly target checks before + considering the old signature removed. +- [x] Delete `FASTLY_RESPONSE_STREAM_BUFFER_BYTES`, buffered response conversion, and imports or + dependencies used only by that path. +- [x] Keep response-write deadline support `BestEffort`: synchronous hostcalls get boundary + checks, not a claim of preemptive cancellation. Define successful completion as Fastly host + acceptance of body-handle close. +- [x] Run host tests with `--features cli`, compile every native and WASM feature combination, + and execute the `fastly,test-utils` contract and library suites under Viceroy. A host + `--all-features` test link is not a valid Fastly gate because provider ABI imports are supplied + only by the WASM runtime. + +## Task 4: Replace Spin full-response conversion with raw WASIp3 delivery + +**Files:** +- Modify: `crates/edgezero-adapter-spin/src/response.rs` +- Modify: `crates/edgezero-adapter-spin/src/request.rs` +- Modify: `crates/edgezero-adapter-spin/src/lib.rs` +- Modify: `crates/edgezero-adapter-spin/src/cli.rs` +- Modify: `crates/edgezero-adapter-spin/Cargo.toml` +- Modify: `crates/edgezero-adapter-spin/src/templates/src/lib.rs.hbs` +- Modify: `examples/app-demo/crates/app-demo-adapter-spin/src/lib.rs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Test: `crates/edgezero-adapter-spin/tests/contract.rs` +- Test: colocated tests in `response.rs` + +- [x] Add red adapter-local coordinator tests for empty, once, streamed, partial write, pending + write versus deadline equality, cancellation acknowledgement, source failure, body-writer + failure, result-writer failure, response-result success/error/pending, destruction during every + state, immediate source release for suppressed bodies, and exactly one terminal report. +- [x] Add the complete precommit fallback matrix: policy/conversion failure, expired deadline, + HEAD suppression, success, partial write, safety timeout, finish/result failure, original-cause + preservation, fallback metadata, and source release. +- [x] Prototype returning a raw `spin_sdk::wasip3` response from the existing + `#[http_service]` entrypoint and owning its `BodyWriter` in a spawned coordinator. +- [x] Carry the original request method into framing preparation and prove HEAD and status-based + suppression drops the application source without polling it. +- [x] Implement cancellable write operations so a losing write remains owned until cancellation + is acknowledged or the response is destroyed. No losing future may retain attempt access. +- [x] Account each accepted prefix, own both the body writer and response-result writer through + terminal observation, close on clean EOF, and use `HostHandoff` for successful completion until + target evidence proves network completion. +- [x] Remove `SpinFullResponse`, `FullBody`, `http_into_wasi_response`, + `SPIN_RESPONSE_STREAM_BUFFER_BYTES`, and all response collection paths. Remove the direct + `wasip3` dependency if the final code uses only the SDK re-export. +- [x] In the same edit, migrate the Spin scaffold, demo adapter, and generator assertions to the + raw-response runner, then compile the generated and demo Spin applications. +- [x] Run `cargo test -p edgezero-adapter-spin --all-features` and + `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin`. + +## Task 5: Give Cloudflare one owned writer and cancellation lifetime + +**Files:** +- Modify: `crates/edgezero-adapter-cloudflare/src/response.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/request.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/context.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/lib.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/cli.rs` +- Modify: `crates/edgezero-adapter-cloudflare/Cargo.toml` +- Modify: `crates/edgezero-adapter-cloudflare/src/templates/src/lib.rs.hbs` +- Modify: `crates/edgezero-adapter-cloudflare/src/templates/wrangler.toml.hbs` +- Modify: `examples/app-demo/crates/app-demo-adapter-cloudflare/src/lib.rs` +- Modify: `examples/app-demo/crates/app-demo-adapter-cloudflare/wrangler.toml` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Create: `scripts/probe_response_egress.mjs` +- Create: `crates/edgezero-adapter-cloudflare/tests/fixtures/response-egress-worker/Cargo.toml` +- Create: `crates/edgezero-adapter-cloudflare/tests/fixtures/response-egress-worker/wrangler.toml` +- Create: `crates/edgezero-adapter-cloudflare/tests/fixtures/response-egress-worker/src/lib.rs` +- Test: `crates/edgezero-adapter-cloudflare/tests/contract.rs` +- Test: colocated WASM/helper tests in `response.rs` + +- [x] Add red state-machine tests for one staged chunk, writer backpressure, declared-length + stream success/mismatch, unknown-length transform stream, enqueue/write rejection, request + abort, independent deadline equality, frozen application clock, cooperative yield, suppressed + body release, and terminal races. +- [x] Add the complete precommit fallback matrix: policy/conversion failure, expired deadline, + fixed-length HEAD suppression, success, partial writer acceptance, safety timeout, close failure, + original-cause preservation, and fallback metadata. +- [x] Capture `Request.signal` before request conversion and retain it through response delivery. + Enable request signals in generated/runtime configuration. +- [x] Carry the original request method into framing preparation and prove HEAD and status-based + suppression drops the application source without polling it. +- [x] For normalized declared lengths, create a fixed-length Workers stream; for unknown length, + create a transform stream. Register the sole writer/coordinator promise with Workers + `Context` so it survives handler return. +- [x] Account bytes only after the writer promise accepts them. On signal abort report + `RequestCancelled`; do not claim `ClientDisconnected` without deployed proof. +- [x] Complete clean close as `HostHandoff`, and keep abort/backpressure/completion/deadline + capabilities `BestEffort` unless the target-level probe proves the stronger boundary. +- [x] Remove the old Rust stream lifecycle wrapper, premature enqueue accounting, and + `ResponseReturned` path. +- [x] In the same edit, migrate the Cloudflare scaffold, demo adapter, and generator assertions; + enable request signals in both Wrangler configurations and compile both applications. +- [ ] **Deferred provider evidence:** add a dedicated Worker fixture with + `/_probe/ready`, `/_probe/slow`, `/_probe/no-demand`, `/_probe/partial`, and + `/_probe/complete` routes plus machine-readable observer log records. In local mode, + `scripts/probe_response_egress.mjs` must spawn + `wrangler dev --config /wrangler.toml --port `, wait for the ready route, + execute assertions, capture logs, and terminate the exact child process in `finally` on success, + failure, or signal. Run it as `node scripts/probe_response_egress.mjs --mode local`. The script + must assert slow-reader backpressure, no-demand expiry, cancel/disconnect behavior, partial-byte + accounting, clean close, and one terminal report. Run the same HTTP assertions without process + ownership as `node scripts/probe_response_egress.mjs --mode deployed --base-url ` and + archive runtime/version/tolerance output. No `Native` promotion may rely on compilation, mocks, + or local Wrangler alone. +- [x] Run deterministic host coordinator tests, + `cargo test -p edgezero-adapter-cloudflare --all-features`, and + `cargo check -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare`. + +## Task 6: Move Axum serving to a connection-owned Hyper boundary + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/response.rs` +- Modify: `crates/edgezero-adapter-axum/src/service.rs` +- Modify: `crates/edgezero-adapter-axum/src/dev_server.rs` +- Modify: `crates/edgezero-adapter-axum/src/lib.rs` +- Modify: `crates/edgezero-adapter-axum/src/cli.rs` +- Modify: `crates/edgezero-adapter-axum/Cargo.toml` +- Modify: root `Cargo.toml` +- Modify: `crates/edgezero-adapter-axum/src/templates/src/main.rs.hbs` +- Modify: `examples/app-demo/crates/app-demo-adapter-axum/src/main.rs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Test: `crates/edgezero-adapter-axum/tests/contract.rs` +- Test: new raw-socket cases colocated with `service.rs` or in adapter integration tests + +- [x] Add a compile-time red prototype for a direct Hyper HTTP/1 connection running on a Tokio + `LocalSet` with a private non-`Send` response body and no unbounded channel. +- [x] Add red body/coordinator tests for source laziness, one staged frame, accepted-byte + accounting, source error, write deadline waking while Hyper is not polling, response drop, + transport error, deadline equality after accepted progress, suppressed-body source release, and + one terminal report. +- [ ] Add raw-socket precommit fallback tests for policy/conversion failure, expired deadline, + HEAD suppression, complete fallback, partial write, safety timeout, connection failure, + original-cause preservation, and fallback metadata. +- [ ] Add raw-socket tests for a normal reader, zero-read client, disconnect before body, + disconnect mid-body, deadline while blocked, HTTP/1 pipelining, collateral connection close, + exact `Content-Length`, and postcommit failure without replacement payload. +- [x] Replace `axum::serve` with an owned Hyper HTTP/1 accept loop. One response coordinator owns + the attempt; the connection supervisor only sends ordered connection events and closes the + HTTP/1 connection when deadline enforcement requires it. +- [x] Carry the original request method into framing preparation and prove HEAD and status-based + suppression drops the application source without polling it. +- [x] Define collateral pipelined outcomes: the response whose deadline wins keeps + `DeadlineExceeded`; other unsettled responses on that connection settle as `TransportError` + unless an earlier cause already won. +- [x] Remove public `EdgeZeroAxumService`, buffered `into_axum_response`, + `AXUM_RESPONSE_STREAM_BUFFER_BYTES`, and any transport-blind normal serving path. +- [x] In the same edit, migrate the Axum scaffold, demo server, and generator assertions to the + connection-owning server API, then compile and smoke-test both generated and demo servers. +- [x] Promote individual Axum capability cells to `Native` only for boundaries established by + the raw-socket tests; leave any unproved cell `BestEffort` or `Unsupported`. +- [x] Run `cargo test -p edgezero-adapter-axum --all-features`. + +## Task 7: Validate all migrated templates and demo apps together + +**Files:** +- Verify: all adapter templates and demo entrypoints migrated in Tasks 3-6 +- Modify: `crates/edgezero-cli/src/generator.rs` +- Test: generator tests in `crates/edgezero-cli/src/generator.rs` + +- [x] Consolidate cross-adapter generator assertions that reject `#[fastly::main]`, + request-in/response-out Fastly runners, `SpinFullResponse`, old Axum service construction, and + missing Cloudflare request-signal configuration. +- [x] Verify every scaffold and demo adapter uses the send-owning entrypoint established in its + adapter task. Do not retain alternate legacy examples. +- [x] Generate one project per adapter into temporary directories and verify the rendered source + contains only current APIs. +- [x] Run `cargo test -p edgezero-cli generator` and + `cargo check --manifest-path examples/app-demo/Cargo.toml --workspace`. +- [x] Run generated target checks for native, `wasm32-wasip1`, `wasm32-unknown-unknown`, and + `wasm32-wasip2` where the installed toolchain supports them. + +## Task 8: Remove old APIs, dependencies, and accidental response bypasses + +**Files:** +- Modify: all adapter `lib.rs`, `request.rs`, `response.rs`, manifests, and contract tests above +- Modify: `scripts/check_outbound_legacy_api.sh` +- Modify: `scripts/check_outbound_docs_contract.mjs` +- Modify: `Cargo.lock` +- Delete: `docs/superpowers/plans/2026-09-12-fastly-manual-response-egress.md` + +- [x] Derive the forbidden inventory from design §6 and extend the legacy checker to scan + production code, templates, demos, generated fixtures, current guides, manifests, and feature + definitions. Fail on `ResponseReturned`, the old `begin` signature, all response buffer + constants, `SpinFullResponse`, `FullBody`, `EdgeZeroAxumService`, public converters such as + `from_core_response`, alternate response-returning dispatch/runners, `#[fastly::main]`, obsolete + feature flags, adapter use of `collect_response_stream*`, and buffered-fallback guide language. +- [x] After all adapters use the owned contract, remove the transitional begin method, rename the + owned method to the final `begin`, remove `ResponseReturned` and its special case, update every + call site, and run `cargo check --workspace --all-targets` immediately. +- [x] Audit every adapter return and `?` path after the request-egress context exists. Route it + through the owned coordinator, including errors from handlers, middleware, routing, fallback + drain, response normalization, and policy evaluation. +- [x] Audit dependencies and feature gates with `cargo tree`/`rg`; `cargo machete` was not + installed. Remove dependencies and gates that existed only for buffered conversion or obsolete + entrypoints. +- [x] Keep outbound buffered-fetch helpers in core: they are live outbound APIs and are not part + of downstream response buffering cleanup. +- [x] Run cross-adapter conformance cases for streamed declared-length early EOF, one-byte overrun, + partial native writes, HEAD/status suppression, deadline equality, fallback accounting, and + original-cause preservation. +- [x] Run the legacy script, docs contract script, adapter tests, and `git diff --check`. + +## Task 9: Align capability evidence and public documentation + +**Files:** +- Modify: `docs/guide/capabilities.md` +- Modify: `docs/guide/architecture.md` +- Modify: `docs/guide/proxying.md` +- Modify: `docs/guide/adapters/overview.md` +- Modify: `docs/guide/adapters/axum.md` +- Modify: `docs/guide/adapters/cloudflare.md` +- Modify: `docs/guide/adapters/fastly.md` +- Modify: `docs/guide/adapters/spin.md` +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: adapter capability declarations in each `src/cli.rs` + +- [x] Add tests that compare adapter capability declarations with the public matrix and reject a + `Native` row lacking a named evidence fixture. +- [x] Document each adapter's commit point, byte-acceptance boundary, terminal success meaning, + cancellation primitive, deadline limitation, provider-owned allocation exclusion, and probe + status. +- [x] Remove language describing the old buffering fallback or response-object handoff as current + behavior. Keep historical implementation plans historical and clearly non-normative. +- [x] Record target/runtime versions and tolerances for any deployed probes that were actually + run. No deployed probe was run in this implementation session, and no capability was promoted + based only on compilation, host mocks, or local runtime behavior. +- [x] Run `node scripts/check_outbound_docs_contract.mjs`, docs lint, and docs build. + +## Task 10: Broad verification and adversarial self-review + +- [x] Run focused adapter and core tests after the final cleanup. +- [x] Run all repository gates: + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace --all-targets +cargo check --workspace --all-targets --features "fastly cloudflare spin" +cargo check -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly +cargo check -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare +cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin +``` + +- [x] Run demo and generated-project checks, legacy/API checks, docs checks, `git diff --check`, + and inspect `git status --short` for unintended files. +- [x] Perform a direct dead-code, duplication, cfg-boundary, and build-matrix review. Separate + review agents were unavailable in this session; do not claim independent-agent evidence. +- [x] Re-read every normative statement in the response-egress spec and record evidence or an + explicit capability limitation for each one. +- [x] Report the final decision log: investigation agents, competing proposals, chosen + runtime-specific-pump design, proof obtained per adapter, remaining provider limitations, and + commands actually run. + +## Decision And Evidence Record + +### Hosted Qualification Follow-up + +- [x] Add a default-off synthetic qualification application through the four standard adapter + entrypoints, with bounded source/terminal/resource counters and compiled build identity. +- [x] Add allowlisted, finite HTTPS client probes and a fail-closed evidence validator; keep + `HostHandoff` distinct from peer delivery and record missing instance identity honestly. +- [x] Exercise validator corruption cases and compile every qualification target in CI. +- [ ] Run against designated nonproduction hosted targets and export complete unsampled telemetry + pinned to the committed source, SDK/runtime, compatibility and deployed binary identity. +- [ ] Prove same-instance recovery and actual transport failure/cancellation boundaries where + observable; do not infer these from a healthy subsequent invocation or promote capabilities. + +No hosted deployment was run for this follow-up. The fixture/validator is qualification +infrastructure, not evidence that the remaining provider lifetime guarantees have been met. + +- **Architecture:** Retained one core lifecycle/framing contract and four private runtime-specific + pumps. Rejected a shared sink abstraction and every response-object or bounded-buffer fallback. +- **API migration:** Removed the old Fastly response-returning entrypoint, Spin full-response + conversion, public Axum transport-blind service, converter-only terminal outcome, and response + buffer constants. Demo and generated applications compile only against the send-owning APIs. +- **Capability policy:** The four response-egress guarantees remain `BestEffort` on every adapter. + Lazy passthrough is `Native` only on Axum and Cloudflare. No local mock or compile check was used + to promote provider behavior. +- **Target evidence:** Fastly contract and library suites passed locally under Viceroy `0.19.0`; + Spin contract and SDK-resource suites passed under Wasmtime `45.0.0`. Those versions are newer + than the repository pins, so CI remains the authoritative pinned-runtime run. Cloudflare WASM + tests compile locally, but no `wasm-bindgen-test-runner` or Wrangler deployment was available. +- **Open evidence:** The deployed Workers probe and the expanded Axum socket matrix remain open. + Their absence is reflected by the unchecked tasks and conservative capability declarations. +- **Review:** Separate review agents were unavailable. Direct review covered framing precedence, + clock/deadline races, accepted-prefix accounting, cancellation ownership, provider diagnostics, + cfg/feature combinations, unsafe boundaries, dependency reachability, retired APIs, demos, and + generated projects. + +## Review Follow-Up: Axum EOF And Assembly Invariants + +- [x] Add Hyper/socket regressions for exact and zero declared-length streams, excess tails, + late source errors, and deadline-bounded EOF validation. Check exactly-once reports and source + release, not only received bytes. +- [x] Stage only the final declared-length chunk until source EOF validates; validate zero-length + streams before Hyper handoff and retain the application clock and response completion owner. +- [x] Add a deterministic expired-fallback-registration regression for GET and HEAD. Determine + EOF from source exhaustion, exhaust failed bodies, and preserve selected finite fallback bytes. +- [x] Add a replacement-hook regression with nondefault platform facts; reject changed metadata + after successful configuration while propagating hook errors unchanged. +- [x] Run scoped Axum/core tests, workspace gates, provider WASM checks, demo tests, and docs + contracts; update the spec/guide evidence without promoting BestEffort guarantees. + +**Decision and evidence (2026-10-06):** Investigation/proposal agents Pasteur and Plato recommended +core-owned driver EOF validation and a complete platform-metadata comparison. Axum reviewer +Noether identified a registration-time deadline race; its deterministic regression failed before +the fix and now selects 504 rather than 500. Core reviewer Beauvoir requested population-only +metadata and cancellation-during-finalization coverage; both are included. Build-validator +Lagrange checked combined native features, strict clippy on all three WASM targets, and the demo. +The chosen fix retains the private Axum pump and adds only final-frame lookahead, rather than +the optional shared-driver/pump refactors. + +Local evidence: workspace tests and strict clippy pass; core has 720 passing unit tests; Axum's +standard-service socket regressions cover clean/empty EOF, excess/error/pending tails, a frozen +application clock, and zero-length registration expiry. Demo tests (54), demo logging tests (2), +formatting, docs lint/build, and docs/dependency contracts pass. These are local proofs, not +deployed-provider certification, and no capability was promoted. Delivery and review-thread +replies are recorded in PR 275's commit and discussion history. diff --git a/docs/superpowers/plans/2026-09-10-demo-generator-lifecycle-alignment.md b/docs/superpowers/plans/2026-09-10-demo-generator-lifecycle-alignment.md new file mode 100644 index 000000000..dbe3f8aa8 --- /dev/null +++ b/docs/superpowers/plans/2026-09-10-demo-generator-lifecycle-alignment.md @@ -0,0 +1,78 @@ +# Demo and Generator Lifecycle Alignment Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the checked-in demo and newly generated projects exercise the current outbound HTTP and ingress-admission contracts instead of relying on compatibility defaults. + +**Architecture:** Extend `app!` with one optional `configure = ` callback that implements the existing `Hooks::configure(&mut App)` seam. The demo uses that callback to demonstrate application-owned leases and composed completion resources. The generated core template installs the same finite route-aware read policy with empty core-owned lifecycle tokens, avoiding a no-op semaphore in every new application. Their outbound examples share explicit encoded, decoded, final-buffer, request-body, header, Brotli, and timeout policy, while a batch endpoint exposes positional per-slot elapsed time. + +**Tech Stack:** Rust, proc macros (`syn`/`quote`), Handlebars templates, TOML manifests, EdgeZero core/adapters, native and WASM contract tests. + +--- + +### Task 1: Add the macro configuration callback + +**Files:** +- Modify: `crates/edgezero-macros/src/app.rs` +- Modify: `crates/edgezero-macros/tests/app_macro.rs` + +- [x] Add parser tests for `configure = crate::configure_app`, mixed keyword ordering, duplicate rejection, and the updated unknown-key diagnostic. +- [x] Run the focused macro tests and confirm they fail because `configure` is not accepted. +- [x] Parse one optional configure expression and emit `Hooks::configure(app)` as a call to it; preserve the empty default for existing macro invocations. +- [x] Add an integration test proving `App::build::(platform)` invokes the emitted callback and installs its policy. +- [x] Run all macro tests and strict macro clippy. + +### Task 2: Align ingress behavior in the demo and scaffold + +**Files:** +- Modify: `examples/app-demo/crates/app-demo-core/src/lib.rs` +- Modify: `examples/app-demo/crates/app-demo-core/src/handlers.rs` +- Modify: `examples/app-demo/edgezero.toml` +- Modify: `crates/edgezero-cli/src/templates/core/src/lib.rs.hbs` +- Modify: `crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs` +- Modify: `crates/edgezero-cli/src/templates/root/edgezero.toml.hbs` + +- [x] Add failing demo tests proving manifest route classes reach resolution, the configured callback supplies finite class-aware deadlines, and the opaque grant can be consumed exactly once by a handler. +- [x] Add route classes for health, diagnostic, and outbound routes in both manifests. +- [x] Define a small application-owned admission lease in the demo; keep the scaffold on empty core lifecycle tokens. In both, outbound routes receive a tighter read budget and every request receives a finite deadline. +- [x] Add an admission diagnostic handler that consumes the grant once and confirms its route class without exposing provider data; the demo additionally proves typed downcast and late-bound ownership. +- [x] Run the demo core tests and generated-source assertions. + +### Task 3: Align outbound examples with current limits and timing + +**Files:** +- Modify: `examples/app-demo/crates/app-demo-core/src/handlers.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs` +- Modify: `examples/app-demo/edgezero.toml` +- Modify: `crates/edgezero-cli/src/templates/root/edgezero.toml.hbs` +- Modify: `crates/edgezero-cli/src/templates/root/README.md.hbs` + +- [x] Extend outbound mock tests first to require explicit per-call timeout, request-body limit, independent encoded/decoded/final limits, header byte/count caps, and Brotli policy. +- [x] Apply one shared outbound policy helper to proxy and batch requests in both demo and template. +- [x] Add a positional batch endpoint that computes one absolute deadline, calls `send_all`, and returns each slot's index, elapsed milliseconds, and typed success/failure category. +- [x] Add tests for positional ordering, empty batches, and distinct per-slot elapsed values. +- [x] Document the generated routes, configured limits, timing semantics, and adapter timing-quality caveat. + +### Task 4: Lock generator/demo parity + +**Files:** +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `.github/workflows/test.yml` +- Test: generated project under a temporary directory + +- [x] Add generator assertions for the configure callback, finite ingress policy, empty core lifecycle tokens, absence of demo-only permits, route classes, independent outbound caps, and per-slot timing endpoint. +- [x] Run focused generator tests and confirm the new assertions fail before template changes, then pass after them. +- [x] Generate a fresh all-adapter project and run its tests and strict clippy. +- [x] Run the checked-in demo tests, strict clippy, and all three adapter WASM checks. + +### Task 5: Full verification and PR update + +**Files:** +- Modify: PR 275 description through `gh` + +- [x] Run `cargo fmt --all -- --check`. +- [x] Run `cargo test --workspace --all-targets`. +- [x] Run `cargo clippy --workspace --all-targets --all-features -- -D warnings`. +- [x] Run `cargo check --workspace --all-targets --features "fastly cloudflare spin"`. +- [x] Run `scripts/run_tests.sh` and documentation checks. +- [x] Update PR metadata, commit, push, wait for all checks, and verify a clean synchronized branch. diff --git a/docs/superpowers/plans/2026-09-10-ingress-fallback-review-hardening.md b/docs/superpowers/plans/2026-09-10-ingress-fallback-review-hardening.md new file mode 100644 index 000000000..d6ffd6e26 --- /dev/null +++ b/docs/superpowers/plans/2026-09-10-ingress-fallback-review-hardening.md @@ -0,0 +1,104 @@ +# Ingress Fallback Review Hardening Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Let applications hold an admission lease while enforcing a finite body cap and absolute read deadline before EdgeZero emits a pre-resolved 404 or 405, with application-controlled overflow and timeout responses. + +**Architecture:** Extend the synchronous, body-blind `ReadBodyBeforeFallback` disposition with a transferable `IngressGrant` and buffered terminal responses. Core retains the exact resolved route token and grant for the full drain lifetime, adapters install the existing deadline-bound lazy body, and resolved dispatch maps typed drain outcomes to either the application response or canonical 404/405 without middleware, handlers, rematching, or string classification. Response-egress capability levels remain unchanged and explicitly document the current certification boundary. + +**Tech Stack:** Rust 2024, `edgezero-core`, Axum/Hyper adapter integration tests, Handlebars project templates, VitePress Markdown, Cargo workspace CI. + +--- + +## Task 1: Add the bounded fallback admission contract + +**Files:** +- Modify: `crates/edgezero-core/src/ingress.rs` +- Modify: `crates/edgezero-core/src/context.rs` +- Modify: `crates/edgezero-core/src/router.rs` +- Modify: `crates/edgezero-core/src/app.rs` + +- [x] Add failing core tests proving `ReadBodyBeforeFallback` is accepted only for `NotFound` and `MethodNotAllowed` and is rejected for matched routes before body polling. +- [x] Add failing dispatch tests proving an exact-cap lengthless body produces the canonical 404/405, the first byte over produces 400 before 404/405, and an expired absolute deadline produces 408. +- [x] Run the focused core tests and record the expected failures caused by the missing variant and dispatch behavior. +- [x] Add the initial bounded fallback disposition. Task 6 hardens its final contract with a transferable grant and exact buffered terminal responses while retaining the no-context/no-handler path. +- [x] Reuse the existing deadline checks in a bounded count-and-discard drain for fallback dispatch. Do not rematch, invoke middleware, construct a routed request context, or poll bodies for ordinary `Admit`/`Refuse` outcomes. +- [x] Run `cargo test -p edgezero-core --lib ingress`, `cargo test -p edgezero-core --lib app`, and `cargo test -p edgezero-core --lib router` until green. + +## Task 2: Prove adapter-level 400-before-404/405 behavior + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/service.rs` + +- [x] Add failing Axum service tests with body streams that omit `Content-Length`: exact-cap unmatched and wrong-method requests retain 404/405, while one-byte-over requests return 400. +- [x] Add a deadline test proving fallback draining uses the admitted absolute read deadline and releases the native body without middleware or handler execution. +- [x] Run the focused tests before production changes to confirm the missing core behavior is the failure source. +- [x] Make only the adapter wiring changes required by the public contract; standard adapters must continue to wrap the unread native body before `dispatch_admitted`. +- [x] Run `cargo test -p edgezero-adapter-axum --all-targets`. + +## Task 3: Keep the demo and generated project current + +**Files:** +- Modify: `examples/app-demo/crates/app-demo-core/src/lib.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/lib.rs.hbs` +- Modify: `crates/edgezero-cli/src/generator.rs` + +- [x] Add failing demo/template assertions for a 4 KiB unmatched/wrong-method fallback cap and finite fallback deadline. +- [x] Update both admission callbacks to return `ReadBodyBeforeFallback` for `NotFound` and `MethodNotAllowed`; matched routes retain route-class grants and class-aware deadlines. +- [x] Add demo lifecycle tests for under-cap 404/405 and overflow 400 precedence. +- [x] Extend generator source assertions so future templates cannot silently drop the fallback policy. +- [x] Run demo core tests, generator tests, and the ignored generated-workspace integration test. + +## Task 4: Correct evidence and certification documentation + +**Files:** +- Modify: `docs/superpowers/specs/2026-08-22-inbound-body-design.md` +- Modify: `docs/superpowers/specs/2026-09-08-response-egress-design.md` +- Modify: `docs/guide/capabilities.md` +- Modify: `docs/superpowers/plans/2026-09-08-inbound-ingress-admission.md` +- Modify: `docs/superpowers/plans/2026-09-08-response-egress-implementation.md` + +- [x] Specify the bounded fallback decision, ordering, cap/deadline errors, route-token preservation, and no-handler/no-middleware behavior. +- [x] Mark parser-level head-limit and Axum raw-socket framing rows as future acceptance criteria. State that every current adapter reports `HostManaged` and both raw ingress capabilities remain `Unsupported`. +- [x] State that Axum currently reports `ResponseReturned` after conversion but before Hyper transmission. Keep abort, backpressure, completion, and write-deadline capabilities `Unsupported` and identify them as certification blockers, not implemented delivery guarantees. +- [x] Update implementation-plan status notes without claiming unfinished raw-boundary or transport-egress work. +- [x] Run documentation format, lint, build, and contract checks. + +## Task 5: Full verification and PR update + +- [x] Run `cargo fmt --all -- --check`. +- [x] Run `cargo test --workspace --all-targets`. +- [x] Run `cargo clippy --workspace --all-targets --all-features -- -D warnings`. +- [x] Run `cargo check --workspace --all-targets --features "fastly cloudflare spin"`. +- [x] Run `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin`. +- [x] Run `scripts/run_tests.sh`, generated-project validation, demo target checks, and `git diff --check`. +- [x] Self-review the complete diff for precedence, cancellation, grant lifetime, error redaction, capability honesty, and demo/template parity. +- [ ] Update PR 275 metadata, commit, push, wait for all checks, and verify a clean synchronized branch. + +## Task 6: Transfer the fallback grant and terminal responses + +**Files:** +- Modify: `crates/edgezero-core/src/ingress.rs` +- Modify: `crates/edgezero-core/src/context.rs` +- Modify: `crates/edgezero-core/src/router.rs` +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-adapter-axum/src/service.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/request.rs` +- Modify: `crates/edgezero-adapter-fastly/src/request.rs` +- Modify: `crates/edgezero-adapter-spin/src/request.rs` +- Modify: adapter contract tests under `crates/edgezero-adapter-*/tests/contract.rs` +- Modify: `examples/app-demo/crates/app-demo-core/src/lib.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/lib.rs.hbs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `docs/superpowers/specs/2026-08-22-inbound-body-design.md` +- Modify: `docs/guide/capabilities.md` + +- [x] Add failing core tests proving a non-empty fallback grant remains live during every body poll and drops exactly once after EOF, overflow, timeout, source failure, or cancellation. +- [x] Add failing core tests proving saturated admission returns an application 503 without polling the body, and fallback terminal responses preserve exact application-selected status, headers, and buffered bytes. +- [x] Add a cloneable buffered ingress-response value and extend `ReadBodyBeforeFallback` with `grant`, `on_exceeded`, and `on_timeout`. Keep `Refuse` as the zero-read overload path. +- [x] Replace fallback drain message classification with private `Complete`, `Exceeded`, and `TimedOut` outcomes. Hold the grant through the drain, release it before response conversion, preserve source errors, and retain canonical 404/405 only after clean EOF. +- [x] Extend Axum, Cloudflare, Fastly, and Spin contract suites so each adapter exercises exact-cap `NotFound` and `MethodNotAllowed` requests, first-byte-over precedence, absolute deadline behavior, zero middleware/handler calls, source release, zero-read refusal, and grant lifecycle at the strongest capability each host exposes. Axum additionally pins deadline-bounded release after an outer abort request because its blocking bridge is not promptly cancellable. +- [x] In every adapter contract suite, assert the exact application-selected overflow and timeout status, headers, and buffered body bytes so adapter conversion cannot silently restore the generic JSON 400/408 renderer. +- [x] Update the demo and generated app policy to carry a fallback lease plus explicit plain-text overflow and timeout responses; extend generator assertions and lifecycle tests so the two remain identical. +- [x] Update the ingress specification, capability guide, and this plan to remove the empty-grant and generic-error claims and state the exact lifecycle and wire-response contract. +- [x] Run focused core, adapter, demo, generator, documentation, full workspace, strict Clippy, feature, and WASM target checks before committing and pushing. diff --git a/docs/superpowers/plans/2026-09-10-outbound-http-review-hardening.md b/docs/superpowers/plans/2026-09-10-outbound-http-review-hardening.md new file mode 100644 index 000000000..1f2ac1f98 --- /dev/null +++ b/docs/superpowers/plans/2026-09-10-outbound-http-review-hardening.md @@ -0,0 +1,171 @@ +# Outbound HTTP Review Hardening Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Close the remaining current-head review gaps by preventing provider diagnostics from reaching HTTP responses, splitting typed config failure reasons, and carrying manifest route class plus one injectable monotonic clock through ingress and outbound HTTP. + +**Architecture:** Keep detailed errors available inside `EdgeError`, but render fixed category-only messages at the wire boundary and stop adapters from embedding provider values where no internal diagnostic is needed. Hard-cut the ambiguous config reasons so exhaustive downstream matches must migrate to the precise taxonomy. Route identity remains method plus pattern, while route class is optional metadata. Use one `App`-owned portable clock handle backed by `web_time` by default; the same handle captures ingress start, evaluates admitted body deadlines, and is cloned into the standard outbound client installed by every adapter. + +**Tech Stack:** Rust 2024, `web-time`, `http`, Axum/Tokio, Cloudflare Workers, Fastly Compute, Fermyon Spin, Cargo workspace and WASM target checks. + +--- + +### Task 1: Wire-Safe Server Error Messages + +**Files:** +- Modify: `crates/edgezero-core/src/error.rs` +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-adapter-axum/src/outbound.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/outbound.rs` +- Modify: `crates/edgezero-adapter-fastly/src/outbound.rs` +- Test: colocated unit tests in the files above + +- [x] Add token-bearing tests proving `BadGateway`, `GatewayTimeout`, `Internal`, and `ResponseTooLarge` preserve their internal diagnostics while `IntoResponse` emits only fixed category messages and never serializes the token, URL, query, or provider text. +- [x] Run `cargo test -p edgezero-core error` and verify the new tests fail on the current `self.message()` wire rendering. +- [x] Add a private exhaustive `wire_message()` policy and use it only in `IntoResponse`; keep `message()`, `Display`, typed reasons, and causes available for internal inspection. +- [x] Replace Axum, Cloudflare, and Fastly outbound provider-error interpolation with fixed diagnostics. Keep Spin's already category-only mappings unchanged. +- [x] Add token-bearing wire regression coverage and adapter-facing secret-error assertions; scan every outbound provider mapping to verify rendered responses cannot contain provider values. +- [x] Run `cargo test -p edgezero-core` and focused adapter tests. + +### Task 2: Split Typed Config Extraction Reasons + +**Files:** +- Modify: `crates/edgezero-core/src/error.rs` +- Modify: `crates/edgezero-core/src/extractor.rs` +- Modify: `crates/edgezero-core/src/context.rs` +- Modify: `docs/superpowers/specs/2026-06-16-blob-app-config.md` +- Modify: `docs/superpowers/plans/2026-06-17-blob-app-config.md` + +- [x] Add failing tests for four independently inspectable reasons: `Deserialization`, `Validation`, `MalformedEnvelope`, and `UnsupportedVersion`. +- [x] Run the focused extractor and error tests and verify the pre-split reason assertions fail. +- [x] Add the four variants to the non-exhaustive enum and its exhaustive wire status/kind policy. Remove the ambiguous predecessor variants; this is an intentional compile-time migration break. +- [x] Map malformed envelope parsing, unsupported envelope version/discriminator, typed `data` deserialization, validator failures, and structural secret-walk failures to their exact new reasons. +- [x] Update stored-error round trips and exhaustive reason tests. +- [x] Run `cargo test -p edgezero-core`. + +### Task 3: Manifest Route Class Metadata + +**Files:** +- Modify: `crates/edgezero-core/src/manifest.rs` +- Modify: `crates/edgezero-core/src/router.rs` +- Modify: `crates/edgezero-macros/src/app.rs` +- Modify: `docs/superpowers/specs/2026-08-22-inbound-body-design.md` +- Modify: `docs/superpowers/plans/2026-09-08-inbound-ingress-admission.md` + +- [x] Add failing manifest, macro-token, router-resolution, admission-head, and request-context tests for optional `class = "auction"` metadata. +- [x] Run focused core and macro tests and verify the field/API are absent. +- [x] Add optional validated `class` to `ManifestHttpTrigger`; do not overload the existing unique trigger `id`. +- [x] Add optional class to `RouteMetadata` without changing `RouteId`; add `RouteMetadata::class()` and a builder path for classed routes while preserving all existing route methods. +- [x] Make `app!` propagate each trigger's class for every generated method. Ensure 404/405 behavior and deterministic allowed-route ordering remain unchanged. +- [x] Run `cargo test -p edgezero-core` and `cargo test -p edgezero-macros`. + +### Task 4: Paired Injectable Ingress Clock + +**Files:** +- Modify: `crates/edgezero-core/src/time.rs` +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-core/src/ingress.rs` +- Modify: `crates/edgezero-core/src/context.rs` +- Modify: `crates/edgezero-adapter-axum/src/service.rs` +- Modify: `crates/edgezero-adapter-axum/src/request.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/request.rs` +- Modify: `crates/edgezero-adapter-fastly/src/request.rs` +- Modify: `crates/edgezero-adapter-spin/src/request.rs` +- Test: colocated core and adapter tests + +- [x] Add failing tests with a manually advanced clock proving the same source captures `request_start`, normalizes the admission deadline, wins simultaneous body readiness/expiry, and poisons repeated body reads exactly once. +- [x] Add a cloneable `MonotonicClock` handle wrapping `Fn() -> MonotonicInstant + Send + Sync`, with a `web_time` default. Make `Deadline::remaining_at` and `is_expired_at` public clock-paired operations. +- [x] Store the clock on `App`, expose a setter and snapshot method, attach it to admitted ingress, and carry it into `RequestContext`. +- [x] Change all standard adapter entry points to capture start from `App`'s clock as their first EdgeZero-owned operation. Pass the admitted clock into each lazy body wrapper and use it for pre-poll/post-ready deadline decisions; retain each platform's strongest available timer/cancellation primitive. +- [x] Keep low-level conversion APIs explicitly non-admitting and preserve their existing default-clock behavior. +- [x] Run focused core and all four adapter request/contract tests. + +### Task 5: Documentation, Metadata, and Full Verification + +**Files:** +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `docs/superpowers/specs/2026-06-16-blob-app-config.md` +- Modify: `docs/superpowers/specs/2026-08-22-inbound-body-design.md` +- Modify: relevant implementation plans under `docs/superpowers/plans/` +- Modify: PR 275 title/body through `gh` + +- [x] Document fixed wire messages, split config reasons, route class semantics, and the paired clock contract. +- [x] Preserve explicit `Unsupported` declarations for raw parser accounting, provider-side config allocation/cancellation, and transport-observed response abort/backpressure/completion/write deadlines. +- [x] Update the PR title from design-only wording and refresh the body without claiming unsupported guarantees. +- [x] Run `cargo fmt --all -- --check`. +- [x] Run `cargo test --workspace --all-targets`. +- [x] Run `cargo clippy --workspace --all-targets --all-features -- -D warnings`. +- [x] Run `cargo check --workspace --all-targets --features "fastly cloudflare spin"`. +- [x] Compile and lint Cloudflare, Fastly, and Spin on their target-specific WASM triples; run the exact Fastly and Spin CI sentinels locally. +- [x] Make the Fastly outbound concurrency sentinel part of the WASM contract binary, as required by the existing CI matrix, and prove it under Viceroy. +- [x] Confirm the Cloudflare browser-runtime contract through the PR's Linux CI job; local Safari WebDriver cannot start in this environment. +- [x] Run documentation contract, formatting, lint, and build checks. +- [x] Commit, push, wait for PR checks, and verify the branch is clean and synchronized. + +### Task 6: Final Self-Review Corrections + +**Files:** +- Modify: `crates/edgezero-core/src/error.rs` +- Modify: `crates/edgezero-core/src/extractor.rs` +- Modify: `crates/edgezero-core/src/ingress.rs` +- Modify: `crates/edgezero-adapter-{axum,cloudflare,fastly,spin}/src/request.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/outbound.rs` +- Modify: `docs/superpowers/specs/2026-08-22-inbound-body-design.md` +- Modify: `.github/workflows/test.yml` + +- [x] Add red regressions for escaped JSON discriminators, config provider-message redaction, + generic Cloudflare fetch classification, paired relative admission deadlines, and native source + release when a terminal body error is emitted. +- [x] Replace textual discriminator detection with structural JSON inspection and keep malformed + envelopes distinct from unsupported versions. +- [x] Redact direct config-store provider failures at conversion and classify an opaque Cloudflare + fetch rejection as `Unspecified` rather than claiming connection-phase evidence. +- [x] Add a request-start-relative deadline helper, stamp ingress before store resolution on every + adapter path, and release body sources at the terminal item rather than retaining them until the + wrapper is dropped. +- [x] Correct BestEffort cancellation wording, stale Spin request-conversion docs, and unsafe + config-plan examples; add explicit Fastly WASM concurrency and public ingress-capability checks. +- [x] Remove the superseded serde constructor and verify no active code depends on it. +- [x] Run focused red/green tests after each correction, then repeat every Task 5 verification gate. + +### Task 7: Pair outbound clients with the application clock + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/outbound.rs` +- Modify: `crates/edgezero-adapter-axum/src/request.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/outbound.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/request.rs` +- Modify: `crates/edgezero-adapter-fastly/src/outbound.rs` +- Modify: `crates/edgezero-adapter-fastly/src/request.rs` +- Modify: `crates/edgezero-adapter-spin/src/outbound.rs` +- Modify: `crates/edgezero-adapter-spin/src/request.rs` +- Modify: adapter contract tests under `crates/edgezero-adapter-*/tests/contract.rs` +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `docs/guide/capabilities.md` + +- [x] Add failing per-adapter tests proving method-entry anchoring, preflight elapsed timing, post-ready expiry, backwards-clock handling, and clock retention by streamed upload/response paths. Cloudflare and Spin deferred-path probes run through their hosted contract binaries rather than unexecuted library-only tests. +- [x] Store a `MonotonicClock` on every native outbound client. Require explicit clocks at every low-level response constructor and use clock-paired client constructors for standard adapter wiring and tests. +- [x] Install each standard request's outbound client with the exact `App::monotonic_clock()` clone used by ingress; keep standalone low-level request converters explicitly default-clocked. +- [x] Replace every production outbound `MonotonicInstant::now`, `Deadline::remaining`, and `Deadline::is_expired` call with the client clock plus explicit `remaining_at` or `is_expired_at`; carry clock clones into all deferred body streams and Fastly pending slots. +- [x] Keep `DispatchBudget` as the copyable result of pure budget selection while ensuring its start snapshot, all later deadline checks, error precedence, dispatch-slack checks, and slot completion observations use one clock domain. Fastly backend identity uses the method-entry `budget.duration`; preparation-time clock samples cannot fragment a homogeneous batch's dynamic-backend identity. +- [x] Update the outbound specification and capability guide with constructor semantics, app-clock propagation, explicit low-level clock ownership, and the full injected-clock acceptance matrix. +- [x] Run focused outbound adapter tests, source scans for global-clock bypasses, full workspace tests, strict Clippy, feature builds, and all three WASM target checks before committing and pushing. + +### Task 8: Final implementation-review corrections + +**Files:** +- Modify: `crates/edgezero-core/src/{app,extractor,lib,outbound,response_egress}.rs` +- Modify: `crates/edgezero-adapter-{axum,cloudflare,fastly,spin}/src/{outbound,response}.rs` +- Modify: `examples/app-demo/crates/app-demo-core/src/handlers.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs` +- Modify: `docs/guide/proxying.md` +- Modify: outbound and response-egress specifications + +- [x] Validate HEAD/304 representation `Content-Length` as one consistent `u64`, reject malformed/conflicting/overflow values before body polling, retain valid metadata, and continue stripping the field on 1xx/204. +- [x] Carry the application clock through `OutboundResponse`, cooperative bounded-until collection, typed config extraction, `ResponseEgressEnvelope`, and every adapter response converter. +- [x] Preserve Cloudflare's intentional abort-on-drop for framing-bodyless responses; handle a null-body 205 without calling `stream()`, disarm only clean absent/zero-length suppression, and keep positive declared length on the abort path. +- [x] Pin Spin's `BodyWriter` default-on-drop host-error contract in source and retain nonblocking deadline exits instead of awaiting after budget expiry. +- [x] Add `max_chunk_bytes` to the demo, generated template, generator sentinel, and both copyable proxy-guide examples; document that rechunking bounds emitted item shape, not provider allocation. +- [x] Add Cloudflare/Spin three-slot preflight-order coverage, executable adapter write-deadline probes, Cloudflare abort lifecycle coverage, and host-duration floor/saturation tests. +- [x] Document intentional zero-cap deny-all behavior and keep loop/self-reference policy explicitly application-owned. +- [x] Run every workspace, strict lint, documentation, generated-project, demo, and target-specific WASM gate. diff --git a/docs/superpowers/plans/2026-09-13-runtime-sdk-hard-cut.md b/docs/superpowers/plans/2026-09-13-runtime-sdk-hard-cut.md new file mode 100644 index 000000000..cb211f347 --- /dev/null +++ b/docs/superpowers/plans/2026-09-13-runtime-sdk-hard-cut.md @@ -0,0 +1,228 @@ +# Runtime SDK Hard-Cut Implementation Plan + +> **Status:** Implemented. Outbound batch capability names and APIs in this historical plan are +> superseded by `2026-09-15-outbound-http-consumer-alignment.md`. + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to +> implement this plan task by task. Apply `superpowers:test-driven-development` to every +> behavioral change and `superpowers:verification-before-completion` before claiming success. + +**Goal:** Upgrade the Fastly, Cloudflare, and Spin adapters, demo applications, and generated +projects to the latest stable provider SDKs with no compatibility branches, while correcting +outbound concurrency wording and downgrading unproved Cloudflare deadline capabilities. + +**Architecture:** Keep the existing EdgeZero outbound and response-egress contracts intact and +adapt only provider-boundary code required by `fastly = 0.13.1`, `worker = 0.8.5`, and +`spin-sdk = 7.0.0`. Dependency pins remain exact in the workspace and demo; generated projects +use the matching release lines. Capability declarations describe verified behavior rather than +SDK potential, so Cloudflare deadline capabilities remain `BestEffort` until a deployed, +host-observed cancellation fixture proves `Native` semantics. + +**Tech Stack:** Rust 1.95, Cargo resolver 2, Fastly Compute SDK 0.13.1, Cloudflare Workers Rust +SDK 0.8.5, Spin Rust SDK 7.0.0/WASIp3, Wasmtime 48.0.2, WASM target checks, Viceroy, Node +documentation guards. + +**Source of truth:** +[`2026-05-21-outbound-http-design.md`](../specs/2026-05-21-outbound-http-design.md) and +[`2026-09-08-response-egress-design.md`](../specs/2026-09-08-response-egress-design.md). + +--- + +## Scope And Constraints + +- This is a hard cut. Do not retain SDK 6/0.12/0.8.3 `cfg` branches, aliases, fallback code, + generated templates, or dependency pins. +- Preserve the existing outbound public API, positional `send_all` behavior, per-slot elapsed + metadata, independent encoded/decoded limits, and response-egress lifecycle. +- Do not use Fastly's direct `PendingRequest::send_to_client` path for EdgeZero outbound calls; + it bypasses EdgeZero response limits, decoding, typed outcomes, and slot timing. +- Keep Fastly's raw status-returning response ABI while the high-level SDK send wrappers can + panic on hostcall errors. +- Do not promote a capability based only on compilation or SDK documentation. A `Native` + deadline/cancellation claim requires target-level and deployed host-observed evidence. +- Do not add absolute terminal instants or a public batch-start instant. Under the current + contract, `Ok` is proof of on-time terminal completion and `elapsed` is metadata, not an + independent lateness detector. +- Historical phase plans and baseline prose may retain old version numbers when explicitly + labeled historical. Normative/current documentation must use the upgraded versions. +- Do not commit or push during this execution unless the user separately requests it. +- After every production Rust edit, run the narrowest affected crate test before proceeding. + +## File Map + +- `Cargo.toml`, `Cargo.lock`: exact workspace provider SDK pins and resolved graph. +- `.tool-versions`: Viceroy release capable of loading Fastly SDK 0.13.1 host imports. +- `examples/app-demo/Cargo.toml`, `examples/app-demo/Cargo.lock`: exact demo pins and graph. +- `crates/edgezero-adapter-{fastly,cloudflare,spin}/src/**`: provider API adaptations only where + the upgraded SDK fails to compile or changes verified behavior. +- `crates/edgezero-adapter-cloudflare/src/cli.rs`: authoritative Cloudflare capability values + and table-driven expectations. +- `crates/edgezero-cli/src/generator.rs`: generated application dependency versions and tests. +- `docs/guide/capabilities.md`: public capability matrix and Cloudflare evidence caveat. +- `docs/superpowers/specs/2026-05-21-outbound-http-design.md`: normative SDK pins, fan-out + wording, Cloudflare support levels, evidence gate, and elapsed contract. +- `scripts/check_outbound_docs_contract.mjs`: executable parity guard for documentation matrix. + +### Task 1: Establish Dependency And Capability Red Tests + +**Files:** +- Modify: `crates/edgezero-adapter-cloudflare/src/cli.rs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `scripts/check_outbound_docs_contract.mjs` + +- [x] Change the Cloudflare capability-table test expectations for + `OutboundDeadlines` and `StreamedUploadDeadlines` from `Native` to `BestEffort` without yet + changing production capability dispatch. +- [x] Run + `cargo test -p edgezero-adapter-cloudflare capability --all-features` and verify it fails only + because production still returns `Native` for those two rows. +- [x] Change generator assertions to require `fastly = "0.13"`, `worker = "0.8.5"`, and + `spin-sdk = "7"` without yet changing generated dependency strings. +- [x] Run the focused generator tests and verify failures report the old generated versions. +- [x] Change the docs contract guard's expected Cloudflare deadline cells to `BestEffort` and run + `node scripts/check_outbound_docs_contract.mjs`; verify it fails against the still-`Native` + public guide/spec matrices. + +### Task 2: Upgrade Exact Workspace And Demo Dependencies + +**Files:** +- Modify: `Cargo.toml` +- Modify: `Cargo.lock` +- Modify: `examples/app-demo/Cargo.toml` +- Modify: `examples/app-demo/Cargo.lock` + +- [x] Replace workspace exact pins with `fastly = "=0.13.1"`, + `fastly-shared = "=0.13.1"`, `fastly-sys = "=0.13.1"`, + `log-fastly = "=0.13.1"`, `worker = "=0.8.5"`, and `spin-sdk = "=7.0.0"` while preserving + current feature sets. The logger pin must move with the SDK so it does not retain a transitive + Fastly 0.12 graph. +- [x] Apply the corresponding exact pins in `examples/app-demo/Cargo.toml`. +- [x] Update the Viceroy tool pin to 0.21.0; older repository/local releases reject host imports + emitted by Fastly SDK 0.13.1 before tests execute. Update Wasmtime to 48.0.2; the former 44/45 + runner line cannot link Spin SDK 7's stable WASI HTTP 0.3 `request-options` resource. +- [x] Refresh both lockfiles with targeted `cargo update -p --precise ` commands; + do not accept unrelated dependency churn. +- [x] Use `cargo tree -i` or lockfile inspection to prove each graph resolves exactly the requested + provider versions and that Spin uses its SDK-provided WASIp3 generation. +- [x] Run `cargo check --workspace --all-targets --features "fastly cloudflare spin"` to expose + provider API changes before editing adapter code. + +### Task 3: Adapt Fastly To SDK 0.13.1 + +**Files:** +- Modify only if required: `crates/edgezero-adapter-fastly/src/outbound.rs` +- Modify only if required: `crates/edgezero-adapter-fastly/src/fastly_response_abi.rs` +- Modify only if required: `crates/edgezero-adapter-fastly/src/response.rs` +- Modify only if required: `crates/edgezero-adapter-fastly/src/request.rs` +- Test: `crates/edgezero-adapter-fastly/tests/contract.rs` + +- [x] Compile the Fastly adapter for `wasm32-wasip1` with its provider feature and record every + concrete 0.13.1 API or exhaustive-enum failure. +- [x] For each behavioral compiler failure, add or tighten the smallest host-testable mapping + test before changing production code. Treat new SDK error variants conservatively and preserve + fixed public diagnostics. +- [x] Adapt the provider boundary without introducing `PendingRequest::send_to_client`, an SDK + compatibility `cfg`, or a response-buffering fallback. +- [x] Run `cargo test -p edgezero-adapter-fastly --features cli` after each Rust edit. +- [x] Run the Fastly target check and Viceroy-backed tests used by the repository. Verify owned + response transmission, outbound limits, positional harvest, and `BestEffort` deadline behavior + remain unchanged. + +### Task 4: Adapt Cloudflare To Worker 0.8.5 And Correct Deadline Claims + +**Files:** +- Modify: `crates/edgezero-adapter-cloudflare/src/cli.rs` +- Modify only if required: `crates/edgezero-adapter-cloudflare/src/outbound.rs` +- Modify only if required: `crates/edgezero-adapter-cloudflare/src/response.rs` +- Modify only if required: `crates/edgezero-adapter-cloudflare/src/request.rs` +- Test: `crates/edgezero-adapter-cloudflare/tests/contract.rs` + +- [x] Change production capability dispatch so `OutboundDeadlines` and + `StreamedUploadDeadlines` return `BestEffort`; keep `OutboundFlexiblePhaseBudget`, + `SendAllSlotIsolation`, `OutboundHttp`, and lazy passthrough at their independently verified + levels. +- [x] Run the previously failing capability test and verify it passes. +- [x] Compile the adapter for `wasm32-unknown-unknown` with Worker 0.8.5 and capture actual binding + failures before editing provider code. +- [x] Add a focused failing regression test before any required behavior adaptation, then make + the minimum provider-boundary change while preserving manual redirect handling, + `encodeResponseBody = "manual"`, the single owned `AbortController`, typed errors, and fixed + public diagnostics. +- [x] Run `cargo test -p edgezero-adapter-cloudflare --all-features` and the Cloudflare WASM + target check. Compilation and local abort tests must not be described as deployed cancellation + proof. + +### Task 5: Adapt Spin To SDK 7.0.0 As A Hard Cut + +**Files:** +- Modify only if required: `crates/edgezero-adapter-spin/src/outbound.rs` +- Modify only if required: `crates/edgezero-adapter-spin/src/request.rs` +- Modify only if required: `crates/edgezero-adapter-spin/src/response.rs` +- Modify only if required: `crates/edgezero-adapter-spin/src/lib.rs` +- Test: `crates/edgezero-adapter-spin/tests/contract.rs` + +- [x] Compile the adapter for `wasm32-wasip2` with Spin SDK 7 and record concrete WASIp3, + `wit-bindgen`, future, stream, body-writer, and result-writer API differences. +- [x] Add a focused failing test for every behavioral adaptation that can be exercised on the + host; use target compilation as the red gate for generated binding signature changes. +- [x] Update imports and owned exchange plumbing directly to the SDK 7 API. Remove obsolete SDK 6 + aliases/imports instead of retaining version branches. +- [x] Preserve the timer race for inbound reads, outbound exchange ownership, response-result + completion, cancellation acknowledgement, and existing `BestEffort` provider limitations. +- [x] Run `cargo test -p edgezero-adapter-spin --all-features` after each Rust edit and finish with + the Spin `wasm32-wasip2` target check. +- [x] Execute the nonzero Spin contract and SDK-resource suites under Wasmtime 48.0.2 with + Preview 3 async and HTTP enabled; compilation alone does not establish runtime compatibility. + +### Task 6: Align Generator, Demo, And Normative Documentation + +**Files:** +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify if generated output changes: provider templates under + `crates/edgezero-adapter-*/src/templates/` +- Modify: `docs/guide/capabilities.md` +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `scripts/check_outbound_docs_contract.mjs` +- Verify: `examples/app-demo/crates/app-demo-adapter-*/` + +- [x] Update generated dependency strings to Fastly 0.13, Worker 0.8.5, and Spin SDK 7; run the + focused generator tests until the red assertions pass. +- [x] Generate a fresh all-adapter application in a temporary directory and compare its + entrypoints and manifests with `examples/app-demo`; remove any obsolete SDK 6/0.12 path rather + than preserving it. +- [x] Change both public and normative matrices so Cloudflare outbound and streamed-upload + deadlines are `BestEffort`, with a concise footnote that `AbortController` wiring is present + but deployed host-observed cancellation evidence is still required for `Native`. +- [x] Tighten the trait summary to “attempt every eligible request before harvest” and preserve + the explicit distinction: Axum/Cloudflare/Spin drive complete exchanges concurrently, while + Fastly dispatches sequentially before ordered harvest and does not guarantee overlap. +- [x] Clarify that a successful slot is contractually terminal and on time; consumers must not + reconstruct lateness from a post-batch clock sample, and no absolute terminal instant is part + of the API. +- [x] Update current SDK references and evidence commands to 0.13.1/0.8.5/7.0.0. Leave explicitly + historical baseline and phase-plan references unchanged. +- [x] Run `node scripts/check_outbound_docs_contract.mjs` and confirm matrix parity. + +### Task 7: Remove Obsolete Paths And Verify The Complete Hard Cut + +**Files:** +- Inspect: all modified files and lockfiles +- Modify only to remove discovered obsolete compatibility code or stale current documentation + +- [x] Run `rg` for old SDK pins, SDK-version `cfg` branches, response-buffering constants, + removed entrypoint signatures, and stale `Native` Cloudflare deadline claims. Classify each + remaining old version reference as explicitly historical or remove/update it. +- [x] Run `cargo fmt --all -- --check`. +- [x] Run `cargo test --workspace --all-targets`. +- [x] Run `cargo clippy --workspace --all-targets --all-features -- -D warnings`. +- [x] Run `cargo check --workspace --all-targets --features "fastly cloudflare spin"`. +- [x] Run provider target checks for Fastly `wasm32-wasip1`, Cloudflare + `wasm32-unknown-unknown`, and Spin `wasm32-wasip2`. +- [x] Run the repository's Viceroy, legacy-API, documentation-contract, and generated-project + integration checks. +- [x] In `examples/app-demo`, run format, clippy, tests, and all three provider target checks with + its refreshed lockfile. +- [x] Run documentation format/lint/build checks. +- [x] Review `git diff --check`, `git diff --stat`, and the full scoped diff. Confirm no unrelated + dirty-worktree changes were overwritten and report any provider-host evidence that could not be + run locally. diff --git a/docs/superpowers/plans/2026-09-14-outbound-http-final-review-hardening.md b/docs/superpowers/plans/2026-09-14-outbound-http-final-review-hardening.md new file mode 100644 index 000000000..0466cf350 --- /dev/null +++ b/docs/superpowers/plans/2026-09-14-outbound-http-final-review-hardening.md @@ -0,0 +1,126 @@ +# Outbound HTTP Final Review Hardening Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Close every independently reproduced defect from the final whole-feature self-review without compatibility shims. + +**Architecture:** Preserve the existing core-owned outbound, ingress, config, and response-egress contracts. Tighten validation and lifecycle ordering at their current ownership boundaries, carry the application monotonic clock through bounded store APIs, and enforce Spin platform-host drift before runtime-producing actions. + +**Tech Stack:** Rust 2024, `http`, `url`, `futures`, Reqwest, Workers/Web APIs, Fastly SDK, Spin SDK/WASI HTTP, TOML manifests, Cargo and Node documentation gates. + +**Execution note:** Unchecked `run_test_nonzero.sh` red-phase steps are retained as the original +TDD recipe. All implementation and final green-phase checks are recorded below. + +**Post-push CI note:** The exact Cloudflare WASM Clippy gate exposed target-only restriction +lints that the earlier host/all-feature runs did not surface. The follow-up scopes test imports, +removes shadowed bindings, keeps test modules after production items, corrects the superseded Axum +blocking-bridge text, and adds a documentation regression guard. The exact target Clippy command +and the 21-test browser contract plus 18-test runtime suites pass after the correction. + +--- + +### Task 1: Core protocol and stream terminal hardening + +**Files:** +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-core/src/compression.rs` +- Modify: `crates/edgezero-core/src/response_egress_framing.rs` +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `docs/superpowers/specs/2026-09-08-response-egress-design.md` + +- [x] Add failing tests named `response_normalization_rejects_non_utf8_content_length`, `typed_outbound_request_rejects_empty_userinfo`, `outbound_normalization_strips_proxy_connection`, and `terminal_stream_error_releases_source_before_yield`. +- [x] Run `scripts/run_test_nonzero.sh response_normalization_rejects_non_utf8_content_length cargo test --offline --locked -p edgezero-core response_normalization_rejects_non_utf8_content_length`, `scripts/run_test_nonzero.sh typed_outbound_request_rejects_empty_userinfo cargo test --offline --locked -p edgezero-core typed_outbound_request_rejects_empty_userinfo`, `scripts/run_test_nonzero.sh outbound_normalization_strips_proxy_connection cargo test --offline --locked -p edgezero-core outbound_normalization_strips_proxy_connection`, and `scripts/run_test_nonzero.sh terminal_stream_error_releases_source_before_yield cargo test --offline --locked -p edgezero-core terminal_stream_error_releases_source_before_yield`; confirm failure for the reviewed reason. +- [x] Preserve framing-sensitive headers until validation, apply typed-URI raw-authority checks, extend request/upstream-response/downstream-egress hop-by-hop stripping to `Proxy-Connection`, and drop terminal stream state before yielding errors. +- [x] Amend the normative field-preservation and terminal-cleanup rules for those exact behaviors. +- [x] Run `cargo test --offline --locked -p edgezero-core` and `node scripts/check_outbound_docs_contract.mjs`. + +### Task 2: Axum and Cloudflare deadline ordering + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/outbound.rs` +- Modify: `crates/edgezero-adapter-axum/src/response.rs` +- Modify: `crates/edgezero-adapter-axum/tests/contract.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/outbound.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/response.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/contract.rs` + +- [x] Add failing tests named `request_timer_arms_from_final_remaining_budget` and `response_egress_post_ready_deadline_precedence` for Axum, plus `raw_fetch_timer_arms_from_final_remaining_budget`, `response_egress_preflight_precedes_writer_operation`, and `bodyless_response_checks_cancel_and_deadline_before_handoff` for Cloudflare. +- [ ] Run `scripts/run_test_nonzero.sh request_timer_arms_from_final_remaining_budget cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils request_timer_arms_from_final_remaining_budget` and `scripts/run_test_nonzero.sh response_egress_post_ready_deadline_precedence cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils response_egress_post_ready_deadline_precedence`; confirm the reviewed failures. +- [ ] Run `scripts/run_test_nonzero.sh raw_fetch_timer_arms_from_final_remaining_budget cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils raw_fetch_timer_arms_from_final_remaining_budget`, `scripts/run_test_nonzero.sh response_egress_preflight_precedes_writer_operation cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils response_egress_preflight_precedes_writer_operation`, and `scripts/run_test_nonzero.sh bodyless_response_checks_cancel_and_deadline_before_handoff cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils bodyless_response_checks_cancel_and_deadline_before_handoff`; confirm the reviewed failures. +- [x] Move outbound remaining-budget samples to the last pre-dispatch boundary, make Cloudflare operation construction lazy until after preflight, and apply post-ready response-egress arbitration consistently. +- [x] Run `cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils`, `cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils`, and `cargo check --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --all-targets`. +- [x] Run `CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh dispatch_runs_router_and_returns_response cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --test contract` and `CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh streamed_upload_eof_yields_before_final_precedence cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --lib`. + +### Task 3: Owned ingress and error egress on every adapter + +**Files:** +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-adapter-axum/src/service.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/request.rs` +- Modify: `crates/edgezero-adapter-fastly/src/request.rs` +- Modify: `crates/edgezero-adapter-spin/src/request.rs` +- Modify: each adapter's `tests/contract.rs` +- Modify: `docs/superpowers/specs/2026-08-22-inbound-body-design.md` +- Modify: `docs/superpowers/specs/2026-09-08-response-egress-design.md` + +- [x] Add `normalized_ingress_error_returns_typed_response_without_application_observation` and `post_admission_failure_uses_owned_error_egress` to every applicable adapter contract suite. +- [ ] Run both named filters through `scripts/run_test_nonzero.sh` for each adapter: Axum with `--no-default-features --features axum,test-utils --test contract`, and Cloudflare, Fastly, and Spin with `--no-default-features --features test-utils --test contract`; confirm all eight named invocations fail for the reviewed boundary mismatch. +- [x] Keep provider parser failures before normalized ingress metadata outside response egress. Route normalized-head failures and admission refusals through a detached envelope carrying the app clock, bounded default policy, and no-op observer. +- [x] Treat successful `PreparedIngress` creation as the owned response-egress boundary. Add one core helper that constructs a post-admission error envelope with the app clock, request method/start, route metadata where available, application policy, and observer. +- [x] Route body-source construction, core-request conversion, and dispatch-rendering failures after that boundary through the owned helper; retain provider errors only when bounded response construction or delivery also fails. +- [x] Amend the ingress/egress specs to pin this exact boundary and its no-application-observation pre-admission behavior. +- [x] Run `cargo test --offline --locked -p edgezero-core`, `cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --test contract`, `cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils --test contract`, `cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --test contract`, and `cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils --test contract`. + +### Task 4: Config deadline clock and precedence hard cut + +**Files:** +- Modify: `crates/edgezero-core/src/config_store.rs` +- Modify: `crates/edgezero-core/src/secret_store.rs` +- Modify: `crates/edgezero-core/src/extractor.rs` +- Modify: all adapter `config_store.rs` and `secret_store.rs` implementations +- Modify: `docs/superpowers/specs/2026-06-16-blob-app-config.md` +- Modify: `docs/guide/capabilities.md` + +- [x] Add core tests `bounded_read_uses_injected_clock` and `bounded_read_deadline_wins_ready_provider_error`; add `bounded_config_read_uses_injected_clock` and `bounded_secret_read_uses_injected_clock` to every adapter. +- [ ] Run `scripts/run_test_nonzero.sh bounded_read_uses_injected_clock cargo test --offline --locked -p edgezero-core bounded_read_uses_injected_clock` and `scripts/run_test_nonzero.sh bounded_read_deadline_wins_ready_provider_error cargo test --offline --locked -p edgezero-core bounded_read_deadline_wins_ready_provider_error`; confirm the reviewed failures. +- [ ] Run the two adapter filters through `scripts/run_test_nonzero.sh` for Axum with `--no-default-features --features axum,test-utils`, and for Cloudflare, Fastly, and Spin with `--no-default-features --features test-utils`; include provider-error-at-expiry coverage and confirm all eight named invocations fail. +- [x] Hard-cut bounded config/secret read signatures to receive the application `MonotonicClock`; remove process-global checks from extraction paths. +- [x] Perform post-ready deadline arbitration before propagating provider results. +- [x] Update every normative API/extractor sketch and capability explanation to require the same application clock. +- [x] Run `cargo test --offline --locked -p edgezero-core`, `cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils`, `cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils`, `cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils`, and `cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils`. + +### Task 5: Spin host plumbing, response settlement, evidence, and documentation + +**Files:** +- Modify: Spin adapter CLI validation and tests +- Modify: `crates/edgezero-cli` runtime/generator tests as needed +- Modify: generated and demo Spin manifests only if their canonical defaults change +- Modify: outbound implementation/specification documents and documentation checker +- Modify: Fastly and Spin outbound/contract/resource tests + +- [x] Add failing selected-component host-drift tests with the shared prefix `spin_selected_component_` for absent/default, explicit hosts, wildcard expansion, normalization, malformed platform entries, ordering, and ambiguous components. +- [ ] Run `scripts/run_test_nonzero.sh spin_selected_component_ cargo test --offline --locked -p edgezero-adapter --features cli spin_selected_component_` and confirm failure. +- [x] Enforce canonical Spin `allowed_outbound_hosts` equality before build/serve/deploy without linking the Spin adapter into no-default-feature CLI builds. +- [x] Add `spin_host_drift_blocks_runtime_actions_before_dispatch` in `edgezero-cli` covering build, serve, deploy, staged deploy, shell and registered targets, and proving zero child/adapter side effects on drift. +- [ ] Run `scripts/run_test_nonzero.sh spin_host_drift_blocks_runtime_actions_before_dispatch cargo test --offline --locked -p edgezero-cli spin_host_drift_blocks_runtime_actions_before_dispatch` and confirm failure; also run `cargo check --offline --locked -p edgezero-cli --no-default-features --features cli` to prove the check does not require the Spin adapter. +- [x] Exercise production Fastly dispatch/harvest under Viceroy, plus Spin coordinator and target-SDK resource construction/drop behavior. Keep deployed provider-observation promotion evidence explicitly unavailable. +- [x] Align Spin framing-bodyless and `205 Reset Content` settlement with the shared contract: framing-bodyless responses release without polling, declared-body 205 responses release immediately, and undeclared-body 205 responses perform at most one deadline-bounded read before release. +- [x] Mark implemented phase status accurately and remove stale Spin SDK/Viceroy references; extend the docs checker. +- [x] Run `CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh send_all_dispatches_every_slot_before_wait cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --test contract`. +- [x] Run `CARGO_TARGET_WASM32_WASIP2_RUNNER='/tmp/wasmtime-v48.0.2-aarch64-macos/wasmtime run -W component-model-async=y -S p3=y -S http=y' scripts/run_test_nonzero.sh sdk_request_and_response_completion_resources_construct cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources`. +- [x] Run `cargo test --offline --locked -p edgezero-adapter --features cli`, `cargo test --offline --locked -p edgezero-cli`, `scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds`, `(cd examples/app-demo && cargo test --locked --workspace --all-targets)`, and `node scripts/check_outbound_docs_contract.mjs`. + +### Task 6: Full verification and delivery + +- [x] Run `cargo fmt --all -- --check`. +- [x] Run `cargo test --workspace --all-targets`. +- [x] Run `cargo clippy --workspace --all-targets --all-features -- -D warnings`. +- [x] Run `cargo check --workspace --all-targets --features "fastly cloudflare spin"`. +- [x] Run `bash scripts/check_adapter_feature_matrix.sh axum native`, `bash scripts/check_adapter_feature_matrix.sh cloudflare native`, `bash scripts/check_adapter_feature_matrix.sh fastly native`, and `bash scripts/check_adapter_feature_matrix.sh spin native`. +- [x] Run `bash scripts/check_adapter_feature_matrix.sh cloudflare wasm32-unknown-unknown`, `bash scripts/check_adapter_feature_matrix.sh fastly wasm32-wasip1`, and `bash scripts/check_adapter_feature_matrix.sh spin wasm32-wasip2`. +- [x] Run `CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh dispatch_runs_router_and_returns_response cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --test contract`, `CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh dispatch_runs_router_and_returns_response cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --test contract`, and `CARGO_TARGET_WASM32_WASIP2_RUNNER='wasmtime run -W component-model-async=y -S p3=y -S http=y' scripts/run_test_nonzero.sh router_dispatches_get_and_returns_response cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test contract`. +- [x] Run `CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh streamed_upload_eof_yields_before_final_precedence cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --lib`, `CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh backend_creation_error_table_is_exhaustive cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --lib`, and `CARGO_TARGET_WASM32_WASIP2_RUNNER='wasmtime run -W component-model-async=y -S p3=y -S http=y' scripts/run_test_nonzero.sh spin_error_code_table_is_exhaustive cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources`. +- [x] Run `scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds`. +- [x] From `examples/app-demo`, run `cargo test --locked --workspace --all-targets`, `cargo check --locked -p app-demo-adapter-cloudflare --target wasm32-unknown-unknown --no-default-features --features cloudflare`, `cargo check --locked -p app-demo-adapter-fastly --target wasm32-wasip1 --no-default-features --features fastly`, and `cargo check --locked -p app-demo-adapter-spin --target wasm32-wasip2 --no-default-features --features spin`. +- [x] Run `bash scripts/check_outbound_legacy_api.sh`, `node scripts/check_outbound_docs_contract.mjs`, and from `docs`, `npm run lint`, `npm run format`, and `npm run build`. +- [x] Run `git diff --check`, review `git diff --stat` and `git diff`, commit the remediation, run `git diff --check origin/main...HEAD`, push PR 275, and report the exact SHA and remaining provider limitations. diff --git a/docs/superpowers/plans/2026-09-15-outbound-http-consumer-alignment.md b/docs/superpowers/plans/2026-09-15-outbound-http-consumer-alignment.md new file mode 100644 index 000000000..0633c490c --- /dev/null +++ b/docs/superpowers/plans/2026-09-15-outbound-http-consumer-alignment.md @@ -0,0 +1,396 @@ +# Outbound HTTP Consumer Alignment Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Complete the portable outbound contract needed by completion-driven, deadline-bound applications while preserving EdgeZero ownership of provider transport and Fastly response egress. + +**Architecture:** Replace the terminal-vector-only batch trait method with an adapter-owned `OutboundBatch` that yields indexed buffered completions until an absolute cutoff. Keep ordered collection as a core convenience over that one driver, add portable cache-bypass and validated wire-authority policy to `OutboundRequest`, and add a closed Fastly lifecycle runner whose hooks cannot take over response transmission. + +**Tech Stack:** Rust 2024, `async-trait`, `futures-util`, `web-time`, Fastly SDK, Cloudflare `worker`, Spin WASI HTTP, Axum/reqwest, Cargo contract tests, Node documentation checks. + +--- + +### Task 1: Lock the revised public contract in the specification + +**Files:** +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `docs/guide/capabilities.md` + +- [x] **Step 1: Replace the terminal-vector-only batching decision** + +Document this hard-cut API and remove normative statements that call `send_all` the only fan-out primitive: + +```rust +pub struct OutboundBatchItem { + pub index: usize, + pub result: OutboundSlotResult, +} + +pub struct OutboundBatchResults { + pub slots: Vec>, +} + +pub trait OutboundHttpClient: Send + Sync { + async fn send(&self, request: OutboundRequest) + -> Result; + + fn start_batch_until( + &self, + requests: Vec, + cutoff: Deadline, + ) -> OutboundBatch; +} +``` + +Specify that `OutboundBatch::next()` is cancellation-safe and yields each terminal slot at most once in observed completion order. The cutoff wins equality. Dropping or cancelling stops observation and applies the adapter's strongest available teardown without claiming that already-issued network side effects are undone. + +- [x] **Step 2: Specify ordered collection as one-driver convenience** + +Document `HttpClient::send_all_until` as `start_batch_until(...).collect().await`, with `slots.len() == requests.len()`, preflight failures as `Some(Err(_))`, and only unresolved-at-cutoff slots as `None`. Remove the old `send_all` API instead of retaining a compatibility alias. + +- [x] **Step 3: Specify request transport policy** + +Add `OutboundCachePolicy::{PlatformDefault, Bypass}` and a validated `host_authority_override`. State that the URI continues to own connection destination, SNI, and certificate identity; the override controls only the outgoing HTTP authority. Document Spin as `Unsupported` for authority separation and require preflight rejection rather than silent semantic drift. + +- [x] **Step 4: Specify the closed Fastly lifecycle runner** + +Define ordering: received request -> raw request mutation/extension injection -> EdgeZero conversion and ingress -> route -> response mutation -> egress policy/framing -> platform send -> return post-send state. Hooks borrow values and cannot send, stream, or consume the response themselves. + +- [x] **Step 5: Run documentation checks** + +Run: `node scripts/check_outbound_docs_contract.mjs` + +Expected: PASS with no stale normative `send_all`-only or canonical-authority contradiction. + +### Task 2: Add the core completion-driven batch API + +**Files:** +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-core/src/context.rs` +- Modify: `crates/edgezero-core/src/time.rs` + +- [x] **Step 1: Write failing core contract tests** + +Add tests proving empty batches, reverse completion order, original indices, exact-once delivery, cancellation-safe `next()`, explicit cancellation, preflight result delivery, cutoff equality, ordered `None` collection, one shared start sample, and backwards-clock handling. + +- [x] **Step 2: Run the focused tests and verify RED** + +Run: `cargo test -p edgezero-core outbound::tests::batch -- --nocapture` + +Expected: FAIL because `OutboundBatch`, `start_batch_until`, and `send_all_until` do not exist. + +- [x] **Step 3: Implement the opaque batch driver** + +Add `OutboundBatch`, `OutboundBatchItem`, and `OutboundBatchResults`. Keep adapter internals opaque through a hidden driver boundary. `next()` must not remove a slot until it returns the item, and `cancel()` must return unresolved indices synchronously while dropping driver-owned work. + +- [x] **Step 4: Hard-cut the trait and handle** + +Remove `OutboundHttpClient::send_all` and `HttpClient::send_all`. Add `start_batch_until` to both, plus `HttpClient::send_all_until` implemented only by collecting the returned batch. + +- [x] **Step 5: Extend dispatch-budget selection** + +Pass the batch cutoff into budget selection as a third candidate without mutating each request. Hard-cut `BudgetSource::BatchDeadline` to `RequestDeadline`, add `BatchCutoff`, and preserve exact attribution to the tightest source. + +- [x] **Step 6: Run core tests and verify GREEN** + +Run: `cargo test -p edgezero-core` + +Expected: PASS. + +### Task 3: Add cache and wire-authority request policy + +**Files:** +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-core/src/error.rs` + +- [x] **Step 1: Write failing policy tests** + +Test default cache policy, bypass round-tripping through parts, valid DNS/IPv4/bracketed-IPv6 authority overrides, malformed/userinfo/path/control rejection, raw `Host` stripping, wire-authority selection, and continued URI-derived backend/SNI/certificate identity. + +- [x] **Step 2: Run focused tests and verify RED** + +Run: `cargo test -p edgezero-core outbound::tests::outbound_request_policy -- --nocapture` + +Expected: FAIL because the policy fields and builders do not exist. + +- [x] **Step 3: Implement the typed fields and builders** + +Add: + +```rust +pub enum OutboundCachePolicy { + PlatformDefault, + Bypass, +} + +pub fn cache_policy(self, policy: OutboundCachePolicy) -> Self; +pub fn host_authority_override(self, authority: &str) -> Result; +pub fn host_authority(&self) -> &str; +``` + +Store a parsed authority, not an unchecked string. Preserve it through `into_parts` and `from_parts`; keep `backend_target`, `sni_hostname`, and `cert_host` URI-derived. + +- [x] **Step 4: Run core tests and verify GREEN** + +Run: `cargo test -p edgezero-core` + +Expected: PASS. + +### Task 4: Implement Axum batch, cache, and authority behavior + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/outbound.rs` +- Modify: `crates/edgezero-adapter-axum/tests/contract.rs` + +- [x] **Step 1: Write failing Axum contract tests** + +Cover completion order, cutoff with recoverable completed slots, drop cancellation, Host override observed by a local server while TLS/connection target stays URI-derived, and cache bypass not synthesizing origin-visible cache headers. + +- [x] **Step 2: Run tests and verify RED** + +Run: `cargo test -p edgezero-adapter-axum outbound -- --nocapture` + +Expected: FAIL against the removed trait method and missing policy conversion. + +- [x] **Step 3: Implement a private `FuturesUnordered` batch driver** + +Prepare every slot from one start sample, enqueue immediate preflight failures, drive complete buffered exchanges, race against the absolute cutoff, and drop unresolved reqwest futures on cancellation. + +- [x] **Step 4: Apply wire authority and cache semantics** + +Set the final `Host` from the validated authority field. Treat cache bypass as satisfied without adding `Cache-Control` because reqwest has no adapter-managed intermediary cache. + +- [x] **Step 5: Run Axum tests and workspace tests** + +Run: `cargo test -p edgezero-adapter-axum && cargo test --workspace --all-targets` + +Expected: PASS. + +### Task 5: Implement Cloudflare batch, cache, and authority behavior + +**Files:** +- Modify: `crates/edgezero-adapter-cloudflare/src/outbound.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/contract.rs` +- Modify: `examples/app-demo/wrangler.toml` +- Modify: `examples/app-demo/wrangler.ci.toml` + +- [x] **Step 1: Write failing native and WASM contract tests** + +Cover completion-order driving, cutoff cancellation through `AbortController`, buffered-only preflight, `CacheMode::NoStore`, duplicate headers, and validated Host override conversion. Keep deployed Host observation as the evidence gate before claiming Native authority separation. + +- [x] **Step 2: Run available tests and verify RED** + +Run: `cargo test -p edgezero-adapter-cloudflare --features test-utils` + +Expected: FAIL until the new batch and request policy are mapped. + +- [x] **Step 3: Implement the Cloudflare driver and mappings** + +Use completion-driven futures with one abort guard per slot. Set `NoStore` only for `Bypass`; append the validated Host authority and continue manual redirect handling. Keep authority separation `BestEffort` until the host-observed probe proves the runtime retained the override. + +- [x] **Step 4: Run Cloudflare checks** + +Run: `cargo test -p edgezero-adapter-cloudflare --features test-utils` + +Run: `cargo check -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare` + +Expected: PASS. + +### Task 6: Implement Spin batch and fail-closed authority behavior + +**Files:** +- Modify: `crates/edgezero-adapter-spin/src/outbound.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` + +- [x] **Step 1: Write failing Spin contract tests** + +Cover completion order, cutoff, owned exchange teardown, full-budget timers, cache bypass without synthetic headers, and explicit Host override rejection before body polling or provider dispatch. + +- [x] **Step 2: Run native tests and verify RED** + +Run: `cargo test -p edgezero-adapter-spin --features test-utils` + +Expected: FAIL until the driver and authority preflight exist. + +- [x] **Step 3: Implement the Spin driver and mappings** + +Drive complete exchange futures under the batch cutoff. Keep cancellation `BestEffort`. Treat cache bypass as satisfied without request-header mutation. Reject authority override because WASI HTTP's authority is also the connection target. + +- [x] **Step 4: Run Spin tests and WASM check** + +Run: `cargo test -p edgezero-adapter-spin --features test-utils` + +Run: `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin` + +Expected: PASS. + +### Task 7: Implement Fastly indexed completion and transport policy + +**Files:** +- Modify: `crates/edgezero-adapter-fastly/src/outbound.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` + +- [x] **Step 1: Write failing target-neutral orchestration tests** + +Cover dispatch-before-select, duplicate targets retaining unique indices, reverse completion order, preflight failures, cutoff equality, explicit cancellation, heterogeneous backend identities, Host override identity separation, and cache bypass on both single and batch paths. + +- [x] **Step 2: Run tests and verify RED** + +Run: `cargo test -p edgezero-adapter-fastly --features test-utils outbound -- --nocapture` + +Expected: FAIL because the existing orchestrator returns only an ordered terminal vector. + +- [x] **Step 3: Replace terminal-vector harvest with an indexed select driver** + +Dispatch every eligible request before blocking. Use Fastly pending-handle selection to associate the selected handle with its original index. Preserve documented sequential dispatch, bounded selection-group, and synchronous selected-body-drain limitations. Stop observation at cutoff and classify cancellation/slot isolation as `BestEffort`. + +- [x] **Step 4: Apply Fastly request policy** + +Use the wire authority for `.override_host`, keep backend target/SNI/certificate URI-derived, include wire authority and relevant budget in backend identity, and call `set_pass(true)` for cache bypass. + +- [x] **Step 5: Run Fastly tests and WASM check** + +Run: `cargo test -p edgezero-adapter-fastly --features test-utils` + +Run: `cargo check -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly` + +Expected: PASS. + +### Task 8: Add the closed Fastly request/response lifecycle runner + +**Files:** +- Modify: `crates/edgezero-adapter-fastly/src/lib.rs` +- Modify: `crates/edgezero-adapter-fastly/src/request.rs` +- Modify: `crates/edgezero-adapter-fastly/src/response.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` +- Modify: `docs/guide/adapters/fastly.md` + +- [x] **Step 1: Write failing lifecycle tests** + +Prove request mutation precedes conversion, injected extensions reach middleware, response mutation precedes egress policy/framing, invalid framing fails closed, streaming stays adapter-owned, transport send happens exactly once, detached ingress failures skip the routed-response hook, and post-send state is returned only after terminal egress observation. + +- [x] **Step 2: Run tests and verify RED** + +Run: `cargo test -p edgezero-adapter-fastly request -- --nocapture` + +Expected: FAIL because only the auto-receiving, request-extension-only runner exists. + +- [x] **Step 3: Implement the lower-level closed runner** + +Add a `FastlyService` method that accepts an already-received request, a borrow-only request preparation hook, and a borrow-only response finalization hook that may return post-send state. Keep conversion, ingress, response-egress begin/finish, stream ownership, and platform delivery private. + +- [x] **Step 4: Build the top-level runner over it and remove the superseded API** + +Keep `run_app` as the simple generated entrypoint. Replace `run_app_with_request_extensions` with one `run_app_with_hooks` helper built over the closed runner; do not keep the request-extension-only compatibility API. + +- [x] **Step 5: Run Fastly tests** + +Run: `cargo test -p edgezero-adapter-fastly` + +Expected: PASS. + +### Task 9: Update capabilities, documentation, demos, and generators + +**Files:** +- Modify: `crates/edgezero-core/src/manifest.rs` +- Modify: `crates/edgezero-adapter/src/registry.rs` +- Modify: `crates/edgezero-adapter-{axum,cloudflare,fastly,spin}/src/lib.rs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `examples/app-demo/crates/app-demo-core/src/handlers.rs` +- Modify: `docs/guide/proxying.md` +- Modify: `docs/guide/adapters/overview.md` +- Modify: `docs/guide/adapters/{axum,cloudflare,fastly,spin}.md` +- Modify: `docs/guide/capabilities.md` +- Modify: `scripts/check_outbound_docs_contract.mjs` +- Modify: `scripts/check_outbound_legacy_api.sh` + +- [x] **Step 1: Write failing capability and generator tests** + +Add capabilities for completion-driven batching, pending cancellation, cache bypass, and wire-authority override. Assert each adapter's exact `Native`/`BestEffort`/`Unsupported` row. Assert generated/demo code uses the hard-cut API and no removed symbol remains. + +Use these rows: + +| Capability | Axum | Cloudflare | Fastly | Spin | +| --- | --- | --- | --- | --- | +| `outbound-batch-completion-order` | Native | Native | BestEffort | Native | +| `outbound-batch-cancellation` | Native | BestEffort | BestEffort | BestEffort | +| `outbound-batch-slot-isolation` | Native | Native | BestEffort | Native | +| `outbound-cache-bypass` | Native | Native | Native | Native | +| `outbound-authority-override` | Native | BestEffort | Native | Unsupported | + +Remove the old `send-all-slot-isolation` capability rather than retaining an alias. + +- [x] **Step 2: Run tests and verify RED** + +Run: `cargo test -p edgezero-core manifest -- --nocapture` + +Run: `cargo test -p edgezero-cli generator -- --nocapture` + +Expected: FAIL until capability metadata and generated source are migrated. + +- [x] **Step 3: Implement metadata and migrate consumers** + +Update adapter capability tables, manifest parsing/rendering, public guides, example handlers, and scaffold assertions. Extend the legacy checker to reject `send_all(` and direct Fastly response transmission in generated entrypoints. + +- [x] **Step 4: Run focused verification** + +Run: `cargo test -p edgezero-core && cargo test -p edgezero-cli` + +Run: `node scripts/check_outbound_docs_contract.mjs` + +Run: `bash scripts/check_outbound_legacy_api.sh` + +Expected: PASS. + +### Task 10: Complete whole-feature verification and cleanup + +**Files:** +- Review: all files changed by Tasks 1-9 + +- [x] **Step 1: Remove superseded code** + +Search for the old trait method, ordered Fastly batch helper, stale cache/Host behavior, and direct response-send bypasses. Remove helpers that no longer have callers rather than leaving compatibility code. + +- [x] **Step 2: Run formatting and full native gates** + +Run: `cargo fmt --all -- --check` + +Run: `cargo clippy --workspace --all-targets --all-features -- -D warnings` + +Run: `cargo test --workspace --all-targets` + +Run: `cargo check --workspace --all-targets --features "fastly cloudflare spin"` + +Expected: PASS with zero warnings. + +- [x] **Step 3: Run target and demo gates** + +Run: `cargo check -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly` + +Run: `cargo check -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare` + +Run: `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin` + +Run: `cargo test --manifest-path examples/app-demo/Cargo.toml --locked` + +Expected: PASS. + +- [x] **Step 4: Run repository contract gates** + +Run: `node scripts/check_outbound_docs_contract.mjs` + +Run: `bash scripts/check_outbound_legacy_api.sh` + +Run: `node scripts/check_brotli_dependency_contract.mjs` + +Run: `bash scripts/check_no_legacy_typed_reads.sh` + +Expected: PASS. + +- [x] **Step 5: Review the final diff** + +Run: `git diff --check && git status --short` + +Expected: no whitespace errors; only intended files changed. diff --git a/docs/superpowers/plans/2026-09-15-reusable-app-lifecycle.md b/docs/superpowers/plans/2026-09-15-reusable-app-lifecycle.md index 05a68c728..064313266 100644 --- a/docs/superpowers/plans/2026-09-15-reusable-app-lifecycle.md +++ b/docs/superpowers/plans/2026-09-15-reusable-app-lifecycle.md @@ -2,16 +2,22 @@ > Historical implementation plan. The private-helper design and custom callback > choices are superseded by [the custom serving lifecycle plan](2026-09-17-custom-serving-lifecycle.md). -> The implemented public `Sandbox` and `Hooks`-typed Cloudflare/Spin dispatch APIs -> are described in the updated [spec](../specs/2026-09-15-reusable-app-lifecycle-design.md). +> PR #275's outbound hard-cut also supersedes the original infallible assembly, +> public raw conversion, response-returning dispatch, and buffering choices. +> Current APIs are reconciled below; the [outbound HTTP design](../specs/2026-05-21-outbound-http-design.md) +> remains authoritative for admission, outbound execution, response egress, and +> capabilities. The updated [lifecycle spec](../specs/2026-09-15-reusable-app-lifecycle-design.md) +> documents successful-only retention. Historical checkmarks and verification +> counts do not verify the reconciled implementation. -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkboxes for tracking; exceptions and evidence limits are recorded in the execution checkpoint. +> This is a historical decision record, not authorization to restore superseded +> APIs. Any new implementation requires a current approved plan. **Goal:** Add explicit application retention for Fastly, Cloudflare, and Spin while preserving existing defaults and Axum's retained-router behavior. -**Architecture:** Keep ownership in the existing core `App`. Add a private, host-testable initialization helper behind Fastly's new `Serve` conveniences and explicit-metadata dispatch wrappers for Cloudflare/Spin. Concrete application caches and provider HTTP fixtures demonstrate retention without introducing a common scheduler or generic singleton. +**Architecture:** Keep ownership in core `App`, built through fallible `App::build::(PLATFORM)`. Fastly uses public, host-testable `Sandbox` state with `serve_app` / `serve_app_with_hooks`; Cloudflare/Spin use `Hooks`-typed prebuilt dispatch. Retain successful apps only and create fresh registries, admission, and response-egress resources per request. No common scheduler or generic singleton is introduced. -**Tech Stack:** Rust, Fastly SDK 0.12.1/Viceroy, worker 0.8/worker-build/Wrangler, Spin SDK 6/P3, Axum, shell, VitePress/Prettier. +**Tech Stack:** Rust, Fastly SDK 0.13.1/Viceroy 0.21.0, worker 0.8.5/worker-build/Wrangler, Spin SDK 7/P3, Axum, shell, VitePress/Prettier. --- @@ -19,8 +25,8 @@ - Source baseline: `593fc9282a1c56e12bae15f91eef2162f4b6a1b7`. - Governing design: [Reusable application lifecycle spec](../specs/2026-09-15-reusable-app-lifecycle-design.md), including the host/WASM test split and attempted-request accounting added during review. -- This document plans implementation. Before executing code changes, obtain plan approval as required by `CLAUDE.md`. Planning does not authorize deployment, publishing, external comments, or committing unrelated work. -- Preserve existing entry points, generated templates, macro grammar, logging ownership, error translation, buffering, stream caps, and defaults. No new Tokio dependency in core/WASM adapters, no provider-independent scheduler, and no external consumer identifiers in documents or fixtures. +- This document records historical implementation. New code work requires approval under `CLAUDE.md`; this record does not authorize deployment, publishing, external comments, or unrelated commits. +- Preserve lifecycle defaults, macro grammar, and logging ownership while following PR #275's canonical fallible assembly and lazy delivery. Do not restore historical response buffering, collection caps, or removed APIs through compatibility aliases. No new Tokio dependency in core/WASM adapters, provider-independent scheduler, or external consumer identifiers. - Re-read current `CLAUDE.md` and compare source with the baseline before execution. Use `superpowers:using-git-worktrees` when starting approved code work. This planning-only document remains in the current workspace. - Run scoped `cargo test` after each code increment. Provider fixture changes also need the real-target check/build. No host stub proves provider behavior. - The commit messages below are implementation checkpoints, not commands to commit during planning. Commit only focused, verified changes within the approved execution scope. @@ -44,7 +50,7 @@ Production and existing tests: - `crates/edgezero-core/src/router.rs`: forced interleaving/isolation test. - `crates/edgezero-core/src/app_config.rs`: audit recursion-guard cleanup and extend colocated tests only where coverage is missing. - `crates/edgezero-adapter-fastly/src/request.rs`: audit bounded warning-cache behavior; preserve production behavior. -- `crates/edgezero-adapter-fastly/src/lib.rs`: private retained state, colocated host tests, two public serving helpers, SDK exports. +- `crates/edgezero-adapter-fastly/src/lib.rs` and `src/lifecycle.rs`: successful-only `Sandbox`, host tests, `serve_app` / `serve_app_with_hooks`, and SDK exports. - `crates/edgezero-adapter-cloudflare/src/lib.rs`: additive `dispatch_app`. - `crates/edgezero-adapter-spin/src/lib.rs`: additive `dispatch_app`. - `.github/workflows/test.yml`: native harness tests and Fastly HTTP smoke using existing Viceroy setup. @@ -79,7 +85,10 @@ tests/fixtures/reusable-app/ Documentation: `docs/guide/adapters/{overview,fastly,cloudflare,spin,axum}.md`, fixture README, and the fresh-Fastly-instance comment in `examples/app-demo/crates/app-demo-core/src/lib.rs`. -No production request/response module changes are expected. Reuse their dispatchers; add no core lifecycle module or manifest setting. +The original retention work minimized request/response changes. PR #275 now owns +canonical admission and response egress in those modules; do not restore the +historical conversion or collection paths. No core lifecycle module or reuse +manifest setting is introduced. ## Task 1: Protect retained ownership and request separation @@ -106,78 +115,32 @@ fn app_can_be_retained_in_a_shared_owner() { ## Task 2: Host-testable Fastly production initialization -**File:** `crates/edgezero-adapter-fastly/src/lib.rs`. +**Files:** `crates/edgezero-adapter-fastly/src/lib.rs`, `src/lifecycle.rs`. + +The original private, infallible initialization sketch is superseded by the +custom lifecycle work and PR #275. Current production uses `Sandbox` with +independent successful setup and fallible construction guards. Native `cargo test --features fastly` has a reproduced pre-existing unresolved-hostcall linker failure. Compile shared initialization logic under `cfg(any(feature = "fastly", test))`, as this file already does for `FastlyLogging`/`EnvConfig`. Do not introduce fake hostcalls. -- [x] **2.1 Write tests referring to private `RetainedApp` first.** Use counted closures and real core `App` values. Run `cargo test -p edgezero-adapter-fastly --lib retained_app`; expected initial failure: missing helper. -- [x] **2.2 Add this private production state** adjacent to the entry points: +- [x] **2.1 Protect successful-only retention.** Use the production `Sandbox` state with counted closures and real core app construction. Initialization failure leaves the slot empty; setup succeeds independently of app construction. +- [x] **2.2 Use the current production lifecycle:** ```rust -#[cfg(any(feature = "fastly", test))] -#[derive(Default)] -struct RetainedApp { - app: Option, -} +use edgezero_adapter_fastly::{FASTLY_PLATFORM, lifecycle::Sandbox}; +use edgezero_core::app::App; -#[cfg(any(feature = "fastly", test))] -impl RetainedApp { - fn get_or_init( - &mut self, - env: &edgezero_core::env_config::EnvConfig, - owns_logging: impl FnOnce() -> bool, - install_logger: impl FnOnce(&str, log::LevelFilter, bool) -> Result<(), E>, - build: impl FnOnce() -> edgezero_core::app::App, - ) -> Result<&edgezero_core::app::App, E> { - if self.app.is_none() { - let logging = FastlyLogging::from(env); - if logging.use_fastly_logger && !owns_logging() { - install_logger( - logging.endpoint.as_deref().unwrap_or("stdout"), - logging.level, - logging.echo_stdout, - )?; - } - } - Ok(self.app.get_or_insert_with(build)) - } -} +let mut retained = Sandbox::::default(); +retained.initialize(|| App::build::(FASTLY_PLATFORM))?; +let app = retained.state().expect("successful initialization"); ``` -Apply existing documentation/lint conventions. The helper stores successful initialization only; it does not cache errors or emulate SDK termination. Task 3 uses this exact state in production. +`Hooks::configure` returns `Result<(), EdgeError>`; `App::build` installs and +validates platform metadata. No partial app or initialization error is retained. +Custom callbacks decide whether a failure is recoverable; standard serving +propagates it and stops without another send. -- [x] **2.3 Add this degraded-snapshot regression**, importing `App`, `RouterService`, and `EnvConfig` from core in the colocated module: - -```rust -#[test] -fn retained_app_keeps_degraded_first_logging_decision() { - let mut retained = RetainedApp::default(); - let installs = std::cell::Cell::new(0); - let builds = std::cell::Cell::new(0); - let first = EnvConfig::from_vars(std::iter::empty::<(String, String)>()); - let later = EnvConfig::from_vars([ - ("EDGEZERO__LOGGING__ENDPOINT", "fixture-logs"), - ("EDGEZERO__LOGGING__LEVEL", "debug"), - ]); - for env in [&first, &later] { - let app = retained.get_or_init( - env, - || false, - |_, _, _| { - installs.set(installs.get() + 1); - Ok::<(), &'static str>(()) - }, - || { - builds.set(builds.get() + 1); - App::with_name(RouterService::builder().build(), "retained") - }, - ).expect("initialization"); - assert_eq!(app.name(), "retained"); - } - assert_eq!(builds.get(), 1); - assert_eq!(installs.get(), 0); -} -``` +- [x] **2.3 Preserve the degraded first logging snapshot.** Exercise production `setup_once` with an empty initial environment and later successful selector reads. Logging is not reconfigured after successful setup, while runtime/store resolution remains per request. The earlier private-helper test sketch is superseded. - [x] **2.4 Complete these cases:** @@ -185,7 +148,7 @@ fn retained_app_keeps_degraded_first_logging_decision() { | ------------------ | ---------------------------------------------------------------------------------------------- | | Configured logging | `logger`, then `build`, once across two calls; endpoint/level from first env; stdout true | | Owned logging | Zero adapter installs, one build | -| Logger error | Exact injected error, zero builds, no stored app; do not present same-owner retry as supported | +| Logger error | Exact injected error, zero builds, no stored app; custom retries require safe setup side effects | | Fresh owner | Two state values initialize independently, no shared static | | Retained app | Two dispatches use the same configured router/state | @@ -203,31 +166,22 @@ pub use fastly::http::serve::{Serve, ServeSummary}; #[cfg(feature = "fastly")] #[inline] -pub fn serve_app(serve: Serve) -> ServeSummary { - serve_app_with_request_extensions::(serve, |_request, _extensions| {}) -} +pub fn serve_app(serve: Serve) -> ServeSummary; #[cfg(feature = "fastly")] #[inline] -pub fn serve_app_with_request_extensions( +pub fn serve_app_with_hooks( serve: Serve, - mut extend: F, + prepare: Prepare, + finalize: Finalize, ) -> ServeSummary where A: Hooks, - F: FnMut(&fastly::Request, &mut Extensions), -{ - let stores = A::stores(); - let logging = FastlyLogging::from(A::logging_for("fastly")); - let mut retained = RetainedApp::default(); - serve.run(move |req| -> Result { - let app = retained.get_or_init(&logging, A::owns_logging, init_logger, A::build_app)?; - request::dispatch_with_registries(app, req, stores, &mut extend) - }) -} + Prepare: FnMut(&mut fastly::Request, &mut Extensions), + Finalize: FnMut(&mut edgezero_core::Response) -> State; ``` -- [x] **3.2 Add API docs** for ordinary `main`, first-callback initialization, baked logging settings, fresh registries/extensions, `owns_logging`, limits, terminal SDK callback errors, attempted counts, and `.into_result()`. Do not promise progressive streaming on this standard path. +- [x] **3.2 Add API docs** for ordinary `main`, successful-only `Sandbox` retention, degraded logging snapshot, fresh registries/admission/egress, `owns_logging`, limits, terminal errors, attempted counts, and `.into_result()`. Preparation borrows the request before admission; finalization borrows only routed responses before policy/framing. The adapter owns lazy delivery; no native response is returned to the SDK for another send. - [x] **3.3 Verify:** ```sh @@ -238,7 +192,7 @@ cargo check -p edgezero-adapter-fastly --features fastly --all-targets Expected: host logic passes and SDK wrappers type-check. HTTP execution remains Task 7. -- [x] **3.4 Confirm all existing helpers/logger/templates are unchanged.** Checkpoint: `feat(fastly): add opt-in retained app serving`. +- [x] **3.4 Preserve lifecycle defaults and logging ownership.** PR #275 supersedes the earlier unchanged-helper guarantee; do not restore removed APIs or buffering. Checkpoint: `feat(fastly): add opt-in retained app serving`. ## Task 4: Cloudflare and Spin prebuilt dispatch @@ -249,24 +203,12 @@ Expected: host logic passes and SDK wrappers type-check. HTTP execution remains ```rust #[cfg(all(feature = "cloudflare", target_arch = "wasm32"))] #[inline] -pub async fn dispatch_app( +pub async fn dispatch_app( app: &edgezero_core::app::App, - stores: StoresMetadata, req: Request, env: Env, ctx: Context, -) -> Result { - let env_config = env_config_from_worker(&env, stores); - request::dispatch_with_registries( - app, req, env, ctx, - request::RegistryInputs { - config_meta: stores.config, - kv_meta: stores.kv, - secret_meta: stores.secrets, - env_config: &env_config, - }, - ).await -} +) -> Result; ``` - [x] **4.2 Run `cargo test -p edgezero-adapter-cloudflare` and `cargo check -p edgezero-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown`.** Binding/runtime assertions come in Task 8. @@ -275,31 +217,25 @@ pub async fn dispatch_app( ```rust #[cfg(all(feature = "spin", target_arch = "wasm32"))] #[inline] -pub async fn dispatch_app( +pub async fn dispatch_app( app: &App, - stores: edgezero_core::app::StoresMetadata, req: SpinRequest, -) -> anyhow::Result { - let env = EnvConfig::from_env(); - request::dispatch_with_registries( - app, req, stores.config, stores.kv, stores.secrets, &env, - ).await -} +) -> anyhow::Result; ``` -- [x] **4.4 Run `cargo test -p edgezero-adapter-spin` and `cargo check -p edgezero-adapter-spin --features spin --target wasm32-wasip2`.** Add public docs for explicit metadata, caller-owned app/logging, fresh request setup, and overlapping invocations. -- [x] **4.5 Leave both `run_app` bodies unchanged.** Delegating them directly would move configuration reads after app construction. Small additive wrappers preserve exact ordering without extra plumbing. Checkpoint: `feat(adapters): dispatch prebuilt Cloudflare and Spin apps`. +- [x] **4.4 Run `cargo test -p edgezero-adapter-spin` and `cargo check -p edgezero-adapter-spin --features spin --target wasm32-wasip2`.** Document `A::stores()` selection, caller-owned successful app/logging, fresh registries/admission/egress, raw Spin response ownership, and overlapping invocations. +- [x] **4.5 Current `run_app` delegates to `dispatch_app::`.** Logging precedes fallible `App::build::(PLATFORM)`; configuration, runtime variables, and registries are resolved for every dispatch. The original nondelegating wrapper choice is superseded by PR #275. Callers pair retained apps with their construction `Hooks` type and selected platform. Checkpoint: `feat(adapters): dispatch prebuilt Cloudflare and Spin apps`. ## Task 5: Build the portable probe fixture **Files:** `tests/fixtures/reusable-app/{Cargo.toml,Cargo.lock,.gitignore}` and `crates/fixture-core/{Cargo.toml,src/lib.rs}` beneath that directory. - [x] **5.1 Create an isolated workspace.** Use resolver 2, edition 2024, `publish = false`, and `default-members = ["crates/fixture-core"]`. Workspace EdgeZero paths are `../../../crates/`. Provider packages enable their own features; core uses `default-features = false`. Match the repository's SDK versions. Keep Tokio confined to the native Axum fixture. Ignore `target`, `.runs`, `.wrangler`, `.spin`, and generated worker output. Generate and commit the fixture lockfile; all subsequent builds use `--locked`. -- [x] **5.2 Add counted `FixtureApp` and a distinct `OtherApp`.** Count `build_app` and `configure` independently. Supply explicit config/KV/secret metadata with fixture-only names. Issue a fresh unique synthetic candidate boot token on every incoming request; each guest latches only its first candidate as its instance marker and increments a guest-local request ordinal. Never share a candidate token across a run or runtime process: fresh guests must report different markers. This test marker must never contain real request data or appear as a production caching example. +- [x] **5.2 Add counted `FixtureApp` and a distinct `OtherApp`.** Count route construction and fallible `configure` independently through `App::build::(PLATFORM)`. Supply explicit config/KV/secret metadata with fixture-only names. Issue a fresh unique synthetic candidate boot token on every incoming request; each guest latches only its first candidate as its instance marker and increments a guest-local request ordinal. Never share a candidate token across a run or runtime process: fresh guests must report different markers. This test marker must never contain real request data or appear as a production caching example. - [x] **5.3 Add a failing probe contract test.** Two requests must return their own path, header, body, and request-extension tokens; an intentionally shared `Arc` counter must increment. Run `cargo test --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-core`; expect the missing probe behavior to fail. - [x] **5.4 Implement the probe with `#[action]` handlers and core HTTP imports.** Keep shared state limited to counters and the controlled fixture identity. Return JSON records containing instance, ordinal, build/configure counts, and echoed synthetic request fields. - [x] **5.5 Add `/cookies` and `/rendered-error`.** Append two distinct `Set-Cookie` values; return an ordinary renderable application error on the latter route. Add assertions for both and rerun the scoped tests. -- [x] **5.6 Add `/stream`, `/stream-error`, and `/overlap/{id}`.** Use a controlled loopback backend through the injected proxy. The overlap route captures request values before awaiting a barrier and returns them afterward. An in-flight guard increments/decrements atomics without keeping a mutex guard across an await. Return the observed maximum in-flight count. +- [x] **5.6 Add `/stream`, `/stream-error`, and `/overlap/{id}`.** Use a controlled loopback backend through the injected outbound HTTP interface. The overlap route captures request values before awaiting a barrier and returns them afterward. An in-flight guard increments/decrements atomics without keeping a mutex guard across an await. Return the observed maximum in-flight count. - [x] **5.7 Add `/bindings` and `/origin/{id}`.** Report only binding presence and nonsecret fixture markers. Origins come from a finite harness-owned allowlist. Test unknown-origin rejection and both concrete apps' different route identities. - [x] **5.8 Run the locked core fixture tests.** Expected: all portable assertions pass without provider calls. Checkpoint: `test(lifecycle): add portable retained-app probes`. @@ -320,28 +256,28 @@ pub async fn dispatch_app( **Files:** `tests/fixtures/reusable-app/crates/fixture-fastly/{Cargo.toml,fastly.toml,src/lib.rs,src/bin/single_request.rs,src/bin/rebuild_per_request.rs,src/bin/retained_app.rs,src/bin/custom_single_request.rs,src/bin/custom_rebuild_per_request.rs,src/bin/custom_retained_app.rs,src/bin/logger_negative.rs}`; extend harness Fastly orchestration. -- [x] **7.1 Declare explicit binary names.** Use `fixture-fastly-a`, `fixture-fastly-b`, `fixture-fastly-c`, `fixture-fastly-custom-a`, `fixture-fastly-custom-b`, `fixture-fastly-custom-c`, and `fixture-fastly-logger-negative`. Pin SDK 0.12.1 and build on `wasm32-wasip1`. -- [x] **7.2 Implement standard A with the existing single-request macro entry point.** Call `run_app_with_request_extensions`, adding only fixture observations. Preserve its current logging and construction behavior. -- [x] **7.3 Implement standard B using raw `Serve::run`.** Use the baked logging decision once, build the app each callback, and call `dispatch_with_registries`. Use the same logging policy as C. Do not call retaining `serve_app` for B. -- [x] **7.4 Implement standard C with `serve_app_with_request_extensions`.** Start with a finite request limit of 10 and a bounded idle wait. Record SDK attempted-request summaries separately from client success. Compare A/B/C with identical payloads, dispatch, and instrumentation. -- [x] **7.5 Implement custom A/B/C around a shared raw dispatch function.** A owns one native request; B builds each callback; C captures an ordinary `Option` and initializes it lazily. Mutate a synthetic native header, record native metadata, convert into core, route, recover response extensions, and apply a fixture finalizer. A health request before C's first application request must leave its build count at zero. +- [x] **7.1 Declare explicit binary names.** Use `fixture-fastly-a`, `fixture-fastly-b`, `fixture-fastly-c`, `fixture-fastly-custom-a`, `fixture-fastly-custom-b`, `fixture-fastly-custom-c`, and `fixture-fastly-logger-negative`. Use the workspace's exact Fastly SDK pin and build on `wasm32-wasip1`. +- [x] **7.2 Implement standard A with the single-request entry point.** Use `run_app_with_hooks`, adding fixture observations through preparation and routed-response finalization. Preserve per-invocation construction and canonical adapter-owned delivery. +- [x] **7.3 Implement standard B using `lifecycle::serve_custom`.** Complete baked logging setup once, call `App::build::(FASTLY_PLATFORM)` each callback, and use `request::send_request_with_registries_and_hooks`. Use the same logging policy and deployed logical store aliases as C. Do not call retaining `serve_app` for B. +- [x] **7.4 Implement standard C with `serve_app_with_hooks`.** Start with a finite request limit of 10 and a bounded idle wait. Record SDK attempted-request summaries separately from completed delivery. Compare A/B/C with identical payloads, canonical dispatch, and instrumentation. +- [x] **7.5 Implement custom A/B/C through the canonical send-owning path.** A uses `run_custom` for one native request; B builds each callback; C initializes a `Sandbox` lazily and retains only success. Use request preparation for synthetic native metadata and routed-response finalization for extension observations. A health callback before C's first application request must leave its initialization count at zero. The original manual-conversion design is superseded by PR #275. - [x] **7.5a Verify request correlation independently of sandbox identity.** Capture `Request::get_client_request_id()` on each callback and record `FASTLY_TRACE_ID` only as sandbox metadata. Never use the latter as a unique request ID. When the native ID is unavailable, use the harness-issued unique per-request token as an explicitly labeled fixture fallback; record availability and do not present fallback evidence as native-ID support. Across callbacks in the same observed guest, verify that correlation is derived afresh from each request and does not persist in the global logger or retained app. Keep correlation in request-local extensions or explicit per-event log fields. Exercise the fallback through a supported runtime case or a labeled injected accessor result. -- [x] **7.6 Implement manual progressive response sending in the custom function.** Append every header value. For a core stream, create the native response, commit using `stream_to_client`, pump chunks, and finish the writable body before callback return. Handle errors after commitment locally with explicit logging/cleanup; do not return an error that causes the SDK to attempt another response. Complete instrumented post-send backend work within the callback and prove the next callback starts afterward. -- [x] **7.7 Configure local backends and stores.** Keep config, KV, and secret fixtures isolated and use the same logical aliases as the baked store metadata. Capture a named logging endpoint distinctly from echoed stdout; if the selected runtime cannot expose endpoint-specific evidence, report that assertion unavailable. -- [x] **7.8 Add terminal and continuing cases.** Rendered handler errors should allow later callbacks in the same instance. Required-KV open errors, inbound conversion errors, response collection errors, and error-rendering failure are separate escaping paths. Test controlled reachable failures; label injected/unavailable cases honestly. Add constructor panic, pre-commit failure, post-commit stream failure, and a later fresh-owner initialization case. -- [x] **7.9 Add the logger negative control.** Naively wrapping enabled-logger `run_app` in `Serve` should expose the second-installation failure. Keep this out of B's performance samples. +- [x] **7.6 Verify canonical progressive response delivery.** Preserve every header value and delayed-chunk production through the adapter's owned stream writer. Terminal failure after commitment cleans up without replacing the response. Lifecycle wrappers record escaping errors without an SDK resend. Complete request-owned post-delivery work before the next callback; do not duplicate transport ownership in a fixture. +- [x] **7.7 Configure local backends and stores.** Use the same logical resource aliases as the baked store metadata, following `scripts/smoke_test_config_key_override.sh`. Keep config, KV, and secret fixtures isolated. Capture a named logging endpoint distinctly from echoed stdout; if the selected runtime cannot expose endpoint-specific evidence, report that assertion unavailable. +- [x] **7.8 Add terminal and continuing cases.** Separate rendered handler errors, admission refusals/aborts, bounded precommit fallbacks, and escaping delivery failures according to the outbound design. Test controlled reachable failures; label injected/unavailable cases honestly. Add configuration failure without caching, constructor panic, post-commit stream failure, and a fresh-owner initialization case. Historical collection-error behavior is superseded. +- [x] **7.9 Add the logger negative control.** Naively wrapping enabled-logger `run_app` in `Serve` should expose the second-installation failure. Keep this out of B's performance samples. The earlier degraded-runtime-selector scenario is superseded by deployment-time logical bindings and baked logging. - [x] **7.10 Build and run the fixtures.** From the fixture workspace run `cargo build --locked --release -p fixture-fastly --bins --target wasm32-wasip1`. Launch the selected executable with `viceroy serve --addr 127.0.0.1: --config `. Account for readiness callbacks or restart before measuring. Run `./scripts/smoke_test_reusable_app.sh --adapter fastly --suite smoke`. -- [x] **7.11 Assert actual reuse before accepting comparisons.** Stable guest marker plus increasing ordinal is required. A builds once per fresh invocation; B builds each reused callback; C builds once per retained owner. Check cookies, buffering versus progressive streaming, request isolation, limit 1/10, idle exit, and restart. Compare standard A/B/C and custom A/B/C separately. +- [x] **7.11 Assert actual reuse before accepting comparisons.** Stable guest marker plus increasing ordinal is required. A builds once per fresh invocation; B builds each reused callback; C retains the first successful app. Check cookies, canonical progressive streaming, request isolation, limit 1/10, idle exit, and restart. Historical buffered comparisons must be rerun against matched lazy delivery. - [x] **7.12 Run existing Fastly tests on their proper runners.** Run default host `cargo test -p edgezero-adapter-fastly --lib`. With the selected Viceroy on PATH, run `CARGO_TARGET_WASM32_WASIP1_RUNNER="viceroy run" cargo test -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 --test contract`, and repeat with `--lib`. Native feature-enabled linking is not a required test route. Checkpoint: `test(fastly): verify reusable sandbox lifecycle over HTTP`. ## Task 8: Exercise Cloudflare and Spin retained ownership **Files:** fixture Cloudflare and Spin package files listed in the file map; provider sections of the harness. -- [x] **8.1 Add the Cloudflare fetch fixture.** Use a `cdylib`/`rlib` and `#[event(fetch)]`. A launch-time fixture mode selects unchanged `run_app` or a concrete `static APP: OnceLock` with `dispatch_app` and full metadata. Add a separate concrete `OTHER_APP`; never place an unkeyed static inside a generic function. Do not retain `Env`, `Context`, bodies, or binding handles. +- [x] **8.1 Add the Cloudflare fetch fixture.** Use a `cdylib`/`rlib` and `#[event(fetch)]`. A launch-time fixture mode selects `run_app` or a concrete `OnceLock` populated only after `App::build::(CLOUDFLARE_PLATFORM)` succeeds, with `dispatch_app::(&app, req, env, ctx)`. Add a separate concrete `OTHER_APP`; never place an unkeyed static inside a generic function. Do not retain initialization errors, `Env`, `Context`, bodies, bindings, admission, or egress resources. - [x] **8.2 Configure local Worker builds and bindings.** Use `main = "build/worker/shim.mjs"`, record a fixed compatibility date, and build with `worker-build --release . -- --locked` from the package. Seed only local KV using the same persistence directory supplied to Wrangler. No remote commands or account credentials are needed. -- [x] **8.3 Add the Spin HTTP fixture.** Use SDK 6's `#[http_service]`, concrete app statics, and explicit store metadata. Configure component permissions, fixture config/secret variables, and a local KV store in `runtime-config.toml`. Keep incoming/outgoing bodies and pending futures request-owned. -- [x] **8.4 Build and boot Spin.** Run `cargo build --locked --release -p fixture-spin --target wasm32-wasip2` from the fixture workspace. The original manifest component source is `../../target/wasm32-wasip2/release/fixture_spin.wasm`; resolve it to an absolute path in copied run manifests. Use the selected `spin up --listen
--runtime-config-file --from `. Verify the emitted interface by successful SDK6/P3 host boot; record independent component inspection as unavailable if no inspection tool is installed. The target name alone is insufficient. +- [x] **8.3 Add the Spin HTTP fixture.** Use the pinned SDK 7's `#[http_service]`, successful-only concrete app statics built with the selected platform, and `dispatch_app::(&app, req) -> anyhow::Result`. This is a raw response, not a collected SDK response. Configure component permissions, fixture variables, and a local KV store in `runtime-config.toml`. Keep bodies, admission/egress, and pending futures request-owned. +- [x] **8.4 Build and boot Spin.** Run `cargo build --locked --release -p fixture-spin --target wasm32-wasip2` from the fixture workspace. Resolve the component source to an absolute path in copied run manifests. Use the selected `spin up --listen
--runtime-config-file --from `. Reverify the current SDK7/P3 interface and host together; historical host boot does not establish current compatibility. Record independent inspection as unavailable if no inspection tool is installed. The target name alone is insufficient. - [x] **8.5 Detect Spin controls from the selected runtime.** The investigated Spin 4.0 help does not expose newer reuse/concurrency controls. Exercise controls only when advertised, record their values, and otherwise observe default behavior while marking those controls unavailable. Never blindly pass flags copied from newer documentation. - [x] **8.6 Run sequential ownership and binding assertions on both providers.** Compare per-request versus retained builds, verify different concrete apps return different routes, and check named/default metadata, cookies, bodies, and rendered errors. Restart to prove cold initialization remains correct. - [ ] **8.7 Force overlapping requests on the same guest.** Hold two distinct requests at the backend barrier, verify both arrivals, release them, and assert stable guest identity, maximum in-flight count at least two, and unchanged per-request fields after awaiting. Different instances or serialized delivery are incomplete evidence, not a pass; retry only within a bounded deadline. Retained Spin and both Cloudflare modes passed; the latest Spin per-request control selected separate guests in all three attempts, so this full matrix criterion remains unverified. @@ -365,7 +301,7 @@ pub async fn dispatch_app( - [x] **10.1 Add measurement validation tests.** Reject missing metrics represented as zero, negative phase deltas, mismatched tool/build identities, and SDK wall time mislabeled as CPU. Run the Rust harness tests before and after implementing the report functions. - [x] **10.2 Preflight guest CPU and memory capabilities.** Before enabling `with_max_memory`, require a successful guest heap snapshot. If unsupported, omit that optional limit in the comparison and report the metric unsupported. Separately test conservative SDK termination only if the selected host can produce the unsupported-snapshot failure naturally or exposes a documented hostcall fault-injection facility. Record that mechanism and label injected evidence. Otherwise mark this runtime case unsupported/unverified; do not patch the SDK or substitute a decision-model test as provider evidence. Record precision and source for every memory/CPU observation. - [x] **10.2a Exercise all four independent SDK limits.** Test `with_max_requests` with 1 and 10, `with_timeout` with a controlled idle gap, `with_max_lifetime` by crossing a positive elapsed deadline during controlled initialization before the next callback, and `with_max_memory` after successful heap preflight with a threshold below the measured fixture baseline. Check summary/next-instance behavior without assuming exact eviction timing or guaranteed reuse. Keep limit-boundary tests separate from performance runs so one limit cannot mask another. -- [x] **10.3 Instrument phases.** Capture before/after initialization and request cleanup memory; build/configure counts; initialization and handler wall time; supported SDK CPU phase deltas; request commitment, guest completion, and client completion. Returned-response sending may occur outside callback timing. Keep SDK attempted counts separate from completions. +- [x] **10.3 Instrument phases.** Capture before/after initialization and request cleanup memory; construction/configure counts; initialization and handler wall time; supported SDK CPU phase deltas; commitment, guest completion, and peer completion. Current Fastly callbacks include adapter-owned terminal delivery, not a returned-response sender outside the callback. Keep SDK attempted counts separate from completions. - [x] **10.4 Run repeated matched A/B/C workloads.** Default to three repetitions of 100 sequential requests per variant, finite limit 10, randomized variant order, release builds, and identical instrumentation/backend/payloads. Record configurable sample counts and seeds. Include cheap and deliberately expensive construction, delayed/large streams, failures, idle gaps, and a longer bounded run with a larger finite limit for memory growth. - [x] **10.5 Exercise finite proxy-origin pressure.** Compare one repeated origin with many distinct loopback origins owned by the harness. Bound count and deadlines, and record waits/failures/completions. Local behavior cannot establish the deployed service-wide dynamic-backend capacity limit, which may block registrations pending capacity. - [x] **10.6 Exercise malformed input and interrupted bodies.** Distinguish host rejection before dispatch from observed adapter conversion errors; label fault injection explicitly. Do not infer a demonstrated exploit from a hypothetical request. @@ -389,7 +325,7 @@ pub async fn dispatch_app( **Files:** no planned new implementation files; fix only findings in the files above. -- [x] **12.1 Review the final diff against the spec.** Confirm unchanged default entry points and generation, core traits and macro grammar, request/response conversion semantics (with the documented Cloudflare duplicate-header correction), header duplication, existing stream caps, and error policy. Production changes include adapter entry points, the public Fastly lifecycle module and registry-aware request conversion, Cloudflare response headers, selected-adapter CLI validation and Spin CLI diagnostic redaction; the updated spec records these scope exceptions. +- [x] **12.1 Review the final diff against current authority.** Preserve lifecycle defaults, macro grammar, and header duplication while using fallible platform-aware assembly, canonical admission, and lazy owned egress. PR #275 supersedes the original unchanged-conversion/stream-cap claims. Production surfaces include the public Fastly lifecycle and borrowing hooks, `Hooks`-typed retained dispatch, and request-local resources; no compatibility alias restores removed paths. - [x] **12.2 Run all repository gates.** Expected: each command exits successfully; record unavailable targets/tools separately rather than claiming success. ```sh @@ -428,7 +364,12 @@ The planning review compared two focused proposals: a private Fastly state helpe A subsequent independent alignment review found two verification omissions from spec §6.3. Tasks 1.6a–1.6b now schedule the persistent-state audit and supported cleanup checks; Tasks 7.5a and 11.2 explicitly cover request correlation and its fallback. These are verification/documentation additions, not evidence of a production defect or a change to lifecycle ownership. -## Execution checkpoint +## Historical execution checkpoint + +The results in this section predate reconciliation with PR #275. They record the +original lifecycle implementation, not current API compatibility or a rerun of +the authoritative outbound contracts. Reverify the current fixtures and gates +before making a new passing claim. Implementation is on `feat/reusable-app-lifecycle` in the original checkout. The implementation is committed on this branch. Existing default entry points, generated templates, @@ -460,10 +401,10 @@ Remaining evidence is environmental or outside this implementation: binding recovery are separate, labeled checks. - No reachable public input triggers an error in the current error renderer. A trap is not evidence of a returned rendering error. -- No component-inspection executable is installed; successful Spin SDK6 component - boot supplies runtime compatibility evidence. -- Standard returned-response helpers expose conversion observations, not a - post-send callback; unavailable CPU/cleanup phases remain unknown. +- No component-inspection executable was installed; the historical Spin component + boot supplies evidence only for that tested SDK/host combination. +- Historical conversion-only phase observations do not measure the current + adapter-owned delivery path. Unavailable CPU/cleanup phases remain unknown. - Deployed reuse frequency, resource accounting, capacity, and application workload benefit require separately authorized deployment and application-owned adoption. - The 300-probe run with limit 100 still observed at most six requests per guest. @@ -490,3 +431,21 @@ now gives concrete mitigations for stale snapshots, cache growth, concurrent mutation, logging ownership, failures, and partial streams. These protect and explain existing behavior; application refresh policies and provider lifecycle limits remain outside the framework's ownership. + +## PR #275 Reconciliation Evidence + +The main merge retains opt-in successful application reuse but hard-cuts historical assembly +and conversion paths to the authoritative outbound contract. Prebuilt Cloudflare/Spin dispatch +and Fastly retained/custom serving use canonical admission and adapter-owned lazy egress. +Every request resolves fresh registries and lifecycle resources. Custom Fastly callbacks return +terminal `Result<(), E>` without allowing the SDK to send a replacement response. + +Verified locally with the current pins: 733 core tests and the full workspace suite; native +and all provider-WASM lint; provider runtime contracts; 5 fixture-core and 26 harness tests; +Fastly fixture finalization and Viceroy 0.21.0 HTTP smoke; demo host tests/lint/WASM builds; +fresh generated-project tests/provider builds; docs format/lint/build and contract guards. +The truncated-upstream proof gates its abort on client observation of the partial body. +The immediate zero-byte source-failure proof requires one correlated terminal completion, +no fallback, and terminal guest eviction; only EOF/reset before a status is accepted, never +a timeout or malformed response. Deployed reuse, teardown, and resource guarantees remain +outside this local evidence. diff --git a/docs/superpowers/plans/2026-09-16-outbound-batch-termination.md b/docs/superpowers/plans/2026-09-16-outbound-batch-termination.md new file mode 100644 index 000000000..f361ee3a4 --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-outbound-batch-termination.md @@ -0,0 +1,236 @@ +# Outbound Batch Termination Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make normal completion, method-cutoff termination, and malformed adapter-driver termination unambiguous in the public outbound batch API. + +**Architecture:** Replace the stream-like `Option` terminal signal with an explicit `OutboundBatchNext::{Item, Finished}` result and add `OutboundBatchTermination::{Completed, Cutoff}` to ordered results. Adapter streams emit public driver events for slots, cutoff, or failure; core validates index range, uniqueness, and premature EOF and returns `OutboundBatchFailure` with the precise `EdgeError::Internal` and previously collected slots instead of silently converting invariant violations into unresolved slots. + +**Tech Stack:** Rust 2024, `futures`, `async-stream`, EdgeZero `EdgeError`, provider adapter contract tests, VitePress documentation checks. + +--- + +### Task 1: Lock the corrected contract in core tests + +**Files:** +- Modify: `crates/edgezero-core/src/outbound.rs` + +- [x] **Step 1: Replace the premature-EOF expectation with failing invariant tests** + +Add focused tests asserting: + +- all slots resolve as `OutboundBatchTermination::Completed`; +- an explicit driver cutoff yields `OutboundBatchTermination::Cutoff` and only then permits `None` slots; +- premature driver EOF returns `EdgeError::Internal`; +- duplicate and out-of-range slot indices return `EdgeError::Internal`; +- an empty batch completes normally; +- dropping a pending `next()` future remains cancellation-safe. + +- [x] **Step 2: Run focused tests and verify RED** + +Run: `cargo test -p edgezero-core outbound::tests::batch_ -- --nocapture` + +Expected: compilation/test failure because the typed termination API and driver events do not exist. + +- [x] **Step 3: Implement the minimal core state machine** + +Add public non-exhaustive enums: + +```rust +pub enum OutboundBatchTermination { + Completed, + Cutoff, +} + +pub enum OutboundBatchNext { + Item(OutboundBatchItem), + Finished(OutboundBatchTermination), +} +``` + +Change `OutboundBatch::next` to return `Result`, `collect` to return `Result`, and `HttpClient::send_all_until` to return `Result`. Add `termination` to `OutboundBatchResults`. + +Use the public adapter driver event with `Item`, `Cutoff`, and `Failed` variants. Mark completion only at driver EOF after every index resolves. Return a fixed category-safe internal error for core-detected premature EOF, duplicate indices, out-of-range indices, or impossible terminal transitions; preserve adapter-supplied failures exactly. Ordered collection retains every slot observed before either class of failure. The event and constructors were promoted from doc-hidden adapter support by the round-6 hardening pass. + +- [x] **Step 4: Run core tests and verify GREEN** + +Run: `cargo test -p edgezero-core` + +Expected: PASS. + +### Task 2: Migrate all adapter drivers + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/outbound.rs` +- Modify: `crates/edgezero-adapter-axum/tests/contract.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/outbound.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/contract.rs` +- Modify: `crates/edgezero-adapter-fastly/src/outbound.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` +- Modify: `crates/edgezero-adapter-spin/src/outbound.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` +- Modify: `crates/edgezero-adapter-spin/tests/sdk_resources.rs` + +- [x] **Step 1: Update adapter contract tests first** + +Assert that ordinary batches finish as `Completed`, deadline/cutoff cases finish as `Cutoff`, and batch iteration handles `Item` then `Finished` through `Result` without treating errors as cutoff. + +- [x] **Step 2: Run adapter contract tests and verify RED** + +Run the four native contract suites through `scripts/run_test_nonzero.sh` using their existing batch sentinels. + +Expected: FAIL until adapters emit explicit driver events. + +- [x] **Step 3: Emit explicit item and cutoff events** + +For Axum, Cloudflare, and Spin, map every completed exchange to an item event and every `finish_batch_item` cutoff decision to a cutoff event. For Fastly, also map initial expiry, dispatch-loop expiry, and selected-body cutoff to a cutoff event; map provider-selection invariant failure to a failure event carrying its precise error. Driver EOF is reserved for full completion only. + +- [x] **Step 4: Run adapter tests and WASM checks** + +Run: + +```sh +cargo test -p edgezero-adapter-axum --features test-utils +cargo test -p edgezero-adapter-cloudflare --features test-utils +cargo test -p edgezero-adapter-fastly --features test-utils +cargo test -p edgezero-adapter-spin --features test-utils +cargo check -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare +cargo check -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly +cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin +``` + +Expected: PASS. + +### Task 3: Migrate consumers and hard-cut guards + +**Files:** +- Modify: `examples/app-demo/crates/app-demo-core/src/handlers.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `crates/edgezero-core/src/context.rs` +- Modify: adapter request/service tests that call `send_all_until` +- Modify: `scripts/check_outbound_legacy_api.sh` + +- [x] **Step 1: Add failing demo and generator assertions** + +Require checked-in and generated handlers to choose explicitly whether to inspect `OutboundBatchFailure.slots` or discard them while propagating `failure.error`, inspect the typed termination reason, and avoid the removed direct-result API shape. + +- [x] **Step 2: Run focused consumer tests and verify RED** + +Run: + +```sh +cargo test -p edgezero-cli generator -- --nocapture +cargo test --manifest-path examples/app-demo/Cargo.toml --locked +``` + +Expected: FAIL until consumers use the new result and termination types. + +- [x] **Step 3: Migrate all callers and test clients** + +Update demo/template handlers, mocks, and adapter tests. Remove the old `OutboundBatch::from_stream` constructor and reject stale `while let Some(item) = batch.next().await` and direct `.send_all_until(...).await.slots` shapes in the legacy checker. + +- [x] **Step 4: Run focused consumer tests and verify GREEN** + +Run the generator and app-demo commands from Step 2 plus `bash scripts/check_outbound_legacy_api.sh`. + +Expected: PASS. + +### Task 4: Update the normative contract and public guides + +**Files:** +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `docs/guide/proxying.md` +- Modify: `docs/guide/capabilities.md` +- Modify: `scripts/check_outbound_docs_contract.mjs` + +- [x] **Step 1: Update specification API and invariants** + +Document explicit `Completed`/`Cutoff` termination, `OutboundBatchFailure`, the successful-result rule that `None` is legal only for `Cutoff`, retained partial slots on failure, and explicit adapter cutoff/failure signaling. + +- [x] **Step 2: Update guides and examples** + +Show `OutboundBatchNext::Item`/`Finished` iteration and explicit handling of `send_all_until(...).await` as `Result`. State that malformed drivers never degrade into timeout-like unresolved slots and that ordered collection preserves slots observed before failure. + +- [x] **Step 3: Extend documentation regression checks** + +Reject stale claims that `next()` returns `Option`, that `send_all_until` directly returns results, or that every missing slot means cutoff without consulting termination. + +- [x] **Step 4: Run documentation checks** + +Run: + +```sh +node scripts/check_outbound_docs_contract.mjs +cd docs && npm run format && npm run lint && npm run build +``` + +Expected: PASS. + +### Task 5: Whole-feature verification and delivery + +**Files:** +- Review: all files changed by Tasks 1-4 + +- [x] **Step 1: Run formatting and strict native gates** + +Run: + +```sh +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace --all-targets +cargo check --workspace --all-targets --features "fastly cloudflare spin" +``` + +Expected: PASS with zero warnings. + +- [x] **Step 2: Run the integrated provider and generated-project suite** + +Run: `scripts/run_tests.sh` + +Expected: PASS, including Fastly Viceroy, provider WASM builds, generated workspace, app-demo, and documentation contracts. + +- [x] **Step 3: Review the final diff** + +Run: `git diff --check`, search for removed API shapes, and confirm only intended files changed. + +- [x] **Step 4: Commit and push PR 275** + +Commit message: `fix(outbound): type batch termination` + +Update the PR description and wait for hosted checks to complete. + +### Review Follow-Up: Driver Finalization + +- [x] Add failing tests for trailing failure, duplicate/out-of-range indices, pending finalization, + zero-slot drivers, and cutoff after every slot resolves. Preserve collected slots on failure. +- [x] Remove completion inference from item count. Accept driver EOF only after every slot + resolves; reject malformed intervening events and retain poisoned-repoll behavior. +- [x] Run `cargo test -p edgezero-core outbound::tests::batch_` and all adapter batch contracts. +- [x] Align the specification, public driver docs, and implementation evidence before delivery. + +**Evidence (2026-10-06):** The trailing-event, zero-slot, and pending-finalization regressions +failed against item-count completion before the fix. Core now validates clean EOF, retains +completed slots on driver failure, and releases a pending finalization driver on cancellation. +The full workspace suite passes, including 720 core unit tests. Explicit nonzero native +contract runs pass for Axum (13), Cloudflare (3), Fastly (1), and Spin (2); strict WASM clippy +passes for all three provider targets. Specs/guides and the documentation checker reflect the +EOF requirement. This follow-up does not claim new deployed-host evidence. + +### Review Follow-Up: Shared Future Driver (R5) + +- [x] Extract `OutboundBatch::from_futures` for Axum, Cloudflare, and Spin, preserving original + indices and preflight/terminal timestamps. Keep Fastly's provider-handle driver separate. +- [x] Drain ready exchange futures before the first yield, retain clean-EOF validation, and + introduce no core clock sampling or timer. Keep transport cancellation inside each adapter. +- [x] Add deterministic timestamp-retention, cutoff-attribution, cancellation, empty/premature + driver, and cutoff-release tests. Release pending futures before delivering `Finished(Cutoff)`. +- [x] Complete merge-wide native/WASM, reusable-fixture, demo/generator, and documentation gates. + +Verified during the main merge: 733 core unit tests and the full workspace suite pass; strict +native and provider-WASM Clippy pass; Cloudflare, Fastly, and Spin runtime contract suites pass. +The reusable fixture suite (5 core and 26 harness tests), Fastly owned-finalization test and +Viceroy 0.21.0 HTTP smoke pass. Demo host tests/lint and all three provider WASM builds pass, +as does a fresh generated workspace. Documentation format/lint/build, dependency contracts, +and the expanded legacy guard pass. This is local evidence, not deployed-provider promotion. diff --git a/docs/superpowers/plans/2026-09-17-custom-serving-lifecycle.md b/docs/superpowers/plans/2026-09-17-custom-serving-lifecycle.md index e78616e78..d5030ff1b 100644 --- a/docs/superpowers/plans/2026-09-17-custom-serving-lifecycle.md +++ b/docs/superpowers/plans/2026-09-17-custom-serving-lifecycle.md @@ -1,5 +1,12 @@ # Custom serving lifecycle +> Historical implementation record, reconciled with PR #275's outbound hard-cut. +> Its former SDK response-sending and manual conversion choices are superseded. +> The [outbound HTTP design](../specs/2026-05-21-outbound-http-design.md) remains +> authoritative for admission, owned response egress, and capabilities; no +> compatibility aliases restore removed APIs. Verification below predates this +> reconciliation and is not evidence of a current rerun. + ## Objective Provide reusable lifecycle mechanics for custom Fastly dispatch without taking @@ -11,10 +18,15 @@ Keep the default entry points unchanged. 1. Add feature-independent `lifecycle::Sandbox` with successful-only lazy initialization, attempted-callback and build counters, and a separate setup guard. Preserve unconstrained initialization errors, including fallback routers. -2. Add Fastly-gated `serve_custom` and `run_custom`. Delegate limits and result - sending to the SDK exactly once. Single mode does not enter the serving loop. +2. Add Fastly-gated `serve_custom` and `run_custom`. Delegate receive limits and + terminal result accounting to the SDK, not response sending. Callbacks return + `Result<(), E>` after owned delivery; escaping errors stop serving without an + SDK-generated response or resend. Single mode does not enter the serving loop. 3. Replace the custom fixture's local state slot and retry bookkeeping with this - API. Keep explicit streaming, duplicate headers, finalization, and post-send work. + API. Use `App::build::(FASTLY_PLATFORM) -> Result` and retain + only successful construction. Route preparation/finalization through + `serve_app_with_hooks` or canonical send-owning request hooks, preserving lazy + streaming, duplicate headers, fresh registries/admission/egress, and post-send work. 4. Document ownership and migration, including request-scoped resources and pre-commit versus post-commit errors. 5. Run host state tests, workspace checks, WASM fixture checks, and the local @@ -28,6 +40,15 @@ with host concurrency and invocation lifetime. Axum constructs its app once for the running server. Fastly's bounded receive loop is not portable to these hosts; this extension does not introduce shared mutable singleton state into them. +Build with `App::build::(PLATFORM)`; `Hooks::configure` is fallible after target +metadata is installed. Cloudflare dispatch takes `(&App, Request, Env, Context)` +and returns `Result`. Spin dispatch takes `(&App, SpinRequest)` +and returns `anyhow::Result`, the raw WASI response backed by owned +egress. Neither dispatcher builds an app or installs logging. Never cache an +initialization failure or retain request registries, admission resources, or +delivery coordinators. Pair the retained app with its construction `Hooks` type +and selected platform. + ## Acceptance Health callbacks count but skip initialization. A failed build is returned and @@ -37,7 +58,7 @@ observe recovery and reuse in the same guest, with progressive streaming, duplicate cookies, finalization metadata and post-commit errors preserved. Local evidence does not establish deployed eviction or long-lived memory bounds. -## Verification +## Historical verification Implemented and independently reviewed against the SDK sending contract and a custom consumer's ownership requirements. Workspace tests: 1,435 passed, one diff --git a/docs/superpowers/plans/2026-09-17-lifecycle-contract-completion.md b/docs/superpowers/plans/2026-09-17-lifecycle-contract-completion.md index 62bc3b373..8d299342f 100644 --- a/docs/superpowers/plans/2026-09-17-lifecycle-contract-completion.md +++ b/docs/superpowers/plans/2026-09-17-lifecycle-contract-completion.md @@ -1,13 +1,26 @@ # Lifecycle contract completion +> Historical implementation record, reconciled with PR #275's outbound hard-cut. +> Raw conversion, response-returning dispatch, and collected response paths are +> superseded by canonical admission and adapter-owned lazy egress. The +> [outbound HTTP design](../specs/2026-05-21-outbound-http-design.md) is authoritative; +> no compatibility aliases restore removed APIs. Recorded checkmarks and test +> counts below predate reconciliation and require current verification. + **Goal:** Make the supported custom lifecycle executable and keep target-specific configuration validation scoped to its destination without disclosing secret references. -**Architecture:** Keep the existing SDK serving loop, raw request conversion, and -retained router interfaces. Applications own initialization failure policy and -response finalization. Extend the existing Rust fixtures instead of adding another -serving abstraction. Standalone config validation remains the portability check; -push and diff validate only their selected adapter. +**Architecture:** Keep provider scheduling separate from successful-only app +retention. Assemble with `App::build::(PLATFORM) -> Result` and +fallible `Hooks::configure`. Fastly uses `Sandbox`, `serve_app`, and +`serve_app_with_hooks` with borrowing preparation/finalization and owned sending; +no raw conversion or response-returning helper bypasses admission or egress. +Cloudflare dispatch takes `(&App, Request, Env, Context)` and returns a Worker +response; Spin takes `(&App, SpinRequest)` and returns raw `SpinResponse`. Both +select `A::stores()` and resolve fresh registries/admission/egress per invocation, +without building an app or installing logging. Applications own retry policy and +never cache initialization errors. Standalone config validation remains the +portability check; push and diff validate only their selected adapter. ## Lifecycle acceptance @@ -30,7 +43,7 @@ push and diff validate only their selected adapter. adapter for push/diff, retaining global schema and handler checks. - [x] Keep field paths and naming rules in errors; omit raw and normalized values. -## Verification +## Historical verification - [x] Run scoped tests after code changes, then workspace tests, format, Clippy, all-adapter feature checks, and Spin WASM compilation. diff --git a/docs/superpowers/plans/2026-09-17-outbound-batch-driver-failure.md b/docs/superpowers/plans/2026-09-17-outbound-batch-driver-failure.md new file mode 100644 index 000000000..f5997bbfe --- /dev/null +++ b/docs/superpowers/plans/2026-09-17-outbound-batch-driver-failure.md @@ -0,0 +1,144 @@ +# Outbound Batch Driver Failure Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Preserve precise adapter-driver failures and all terminal batch slots observed before the failure. + +**Architecture:** Extend the private adapter-to-core driver protocol with an explicit failure event. Incremental callers receive its `EdgeError` directly; ordered collection wraps the error with the index-aligned slots collected so far in a new public `OutboundBatchFailure`. Fastly emits this event when pending-handle selection or metadata reassociation violates its invariant, instead of silently ending its stream or misreporting a deadline cutoff. + +**Tech Stack:** Rust 2024, `futures` streams, Fastly Compute SDK, Cargo native/WASM tests, VitePress documentation checks. + +--- + +### Task 1: Preserve Partial Results on Driver Failure + +**Files:** +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-core/src/lib.rs` + +- [x] **Step 1: Write failing core tests** + +Add tests proving that: + +```rust +let failure = block_on(batch.collect()).expect_err("driver failure"); +assert!(matches!(failure.error, EdgeError::Internal { .. })); +assert!(failure.slots[completed_index].is_some()); +assert!(failure.slots[unresolved_index].is_none()); +``` + +Cover both an explicit driver failure carrying a distinctive diagnostic and premature EOF after one item. + +- [x] **Step 2: Run the focused tests and verify RED** + +Run: `cargo test -p edgezero-core outbound::tests::batch_collection -- --nocapture` + +Expected: compilation fails because `OutboundBatchDriverEvent::Failed` and `OutboundBatchFailure` do not exist. + +- [x] **Step 3: Implement the minimal core contract** + +Add: + +```rust +#[derive(Debug)] +#[non_exhaustive] +pub struct OutboundBatchFailure { + pub error: EdgeError, + pub slots: Vec>, +} +``` + +Add hidden `OutboundBatchDriverEvent::Failed(EdgeError)`. Make `next()` poison the batch and return that exact error. Make `collect()` convert any `next()` error into `OutboundBatchFailure` without discarding its local slot vector. Change `HttpClient::send_all_until` to return `Result`, export the type, and implement `From for EdgeError` so existing handlers can deliberately discard partials with `?`. + +- [x] **Step 4: Run core tests and verify GREEN** + +Run: `cargo test -p edgezero-core` + +Expected: all core tests pass. + +### Task 2: Propagate Fastly Selection Failures + +**Files:** +- Modify: `crates/edgezero-adapter-fastly/src/outbound.rs` + +- [x] **Step 1: Write failing Fastly tests** + +Add target-neutral tests for all three reassociation failures: + +- invalid selected handle index; +- unknown or duplicate pending handle; +- omitted pending handle. + +Assert their exact internal diagnostic and add a driver-level regression proving a previously emitted item remains available in `OutboundBatchFailure` when the Fastly failure event follows it. + +- [x] **Step 2: Run focused tests and verify RED** + +Run: `cargo test -p edgezero-adapter-fastly --features test-utils selection` + +Expected: the regression fails because the harvest path ends with EOF instead of emitting the precise error. + +- [x] **Step 3: Emit the explicit failure event** + +Replace the `let Ok(..) else { return; }` branch with a `match` that yields `OutboundBatchDriverEvent::Failed(error)` and returns. Do not emit `Cutoff`; provider selection failed while the cutoff remained live. + +- [x] **Step 4: Run Fastly tests and verify GREEN** + +Run: `cargo test -p edgezero-adapter-fastly --features test-utils` + +Expected: all Fastly tests pass, including exact diagnostics and partial-result retention. + +### Task 3: Align Consumers and Documentation + +**Files:** +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `docs/superpowers/plans/2026-09-16-outbound-batch-termination.md` +- Modify: `docs/guide/proxying.md` +- Modify: `docs/guide/capabilities.md` +- Modify: `examples/app-demo/crates/app-demo-core/src/handlers.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `scripts/check_outbound_docs_contract.mjs` +- Modify: `scripts/check_outbound_legacy_api.sh` + +- [x] **Step 1: Update executable consumers** + +Keep demo and generated handlers source-compatible through `From for EdgeError`, but make their fail-closed choice explicit by propagating `failure.error`. Demonstrate partial-slot retention in the public guide and update generator assertions to pin the current contract. + +- [x] **Step 2: Update the normative and public contracts** + +Document that explicit adapter failures and malformed EOF are not cutoff, incremental callers receive the precise `EdgeError`, and ordered collection returns `OutboundBatchFailure` containing the same error plus all slots collected before failure. + +- [x] **Step 3: Harden documentation and legacy checkers** + +Require `OutboundBatchFailure` and the new collector signature. Reject stale `Result` declarations and claims that batch-level errors erase prior slot results. + +- [x] **Step 4: Run documentation and generator checks** + +Run: `node scripts/check_outbound_docs_contract.mjs` + +Run: `bash scripts/check_outbound_legacy_api.sh` + +Run: `cargo test -p edgezero-cli generator` + +Expected: all checks pass. + +### Task 4: Full Verification and Delivery + +**Files:** +- Modify: `docs/superpowers/plans/2026-09-17-outbound-batch-driver-failure.md` + +- [x] **Step 1: Run repository verification** + +Run: `scripts/run_tests.sh` + +Run any target-specific strict WASM Clippy commands required by CI if the integrated script does not cover them. + +Expected: formatting, strict Clippy, native tests, all WASM targets, documentation, and contract checks pass. + +- [x] **Step 2: Review the final diff** + +Confirm no selection failure is represented as `Cutoff`, no old collector signature remains, demo/template output matches, and no unrelated lower-priority review item was included. + +- [x] **Step 3: Mark this plan complete, commit, and push** + +Commit the focused implementation and synchronized documentation, push `docs/outbound-http-spec`, and confirm PR #275 checks are green. diff --git a/docs/superpowers/plans/2026-09-17-outbound-http-round6-hardening.md b/docs/superpowers/plans/2026-09-17-outbound-http-round6-hardening.md new file mode 100644 index 000000000..e69c7ed9d --- /dev/null +++ b/docs/superpowers/plans/2026-09-17-outbound-http-round6-hardening.md @@ -0,0 +1,280 @@ +# Outbound HTTP Round 6 Hardening Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Close the actionable round-6 outbound review findings without changing valid monotonic-cutoff semantics. + +**Architecture:** Keep provider orchestration in each adapter, but move the byte-for-byte duplicated terminal-slot classifier into core as documented adapter support. Preserve the concurrent adapters' fail-closed cutoff rule because their shared monotonic completion samples are taken synchronously in poll order; fix Fastly response framing by carrying core's body-transmission disposition through the irreversible commit boundary. Promote the batch-driver construction protocol that generated applications already consume, and align all normative/public documentation with pre-normalization header accounting. + +**Tech Stack:** Rust 2024, `futures`/`futures-util`, Fastly Compute ABI, Cargo workspace tests, Node.js documentation contract checker. + +--- + +### Task 1: Centralize and harden batch-driver invariants + +**Files:** +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-adapter-axum/src/outbound.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/outbound.rs` +- Modify: `crates/edgezero-adapter-fastly/src/outbound.rs` +- Modify: `crates/edgezero-adapter-spin/src/outbound.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `examples/app-demo/crates/app-demo-core/src/handlers.rs` + +- [x] **Step 1: Add red core tests for the shared terminal-slot classifier** + +Add tests covering an on-time outcome, an observation at cutoff, an attributed `BatchCutoff` +timeout, and a terminal sample before the batch start. At this point in the plan, the attributed +timeout characterization records the inherited `None` behavior; Task 5 supersedes that expectation +after confirming timeout provenance is not a batch-abandonment signal. The backward sample pins +zero elapsed plus the existing internal invariant outcome. + +- [x] **Step 2: Run the focused tests and verify RED** + +Run: + +```bash +cargo test --offline --locked -p edgezero-core --lib finish_batch_item_ +``` + +Expected: compilation fails because the shared core helper does not exist. + +- [x] **Step 3: Move the duplicated helper into core and update all adapters** + +Implement one documented `edgezero_core::outbound::finish_batch_item` adapter-support function +using the then-existing behavior. Delete all four adapter-local copies and their now-unused imports. +Do not add completion-group buffering or alter cutoff ordering. Task 5 later narrows cutoff +termination to an actual absolute-cutoff clock observation. + +- [x] **Step 4: Run affected core and adapter tests and verify GREEN** + +Run: + +```bash +cargo test --offline --locked -p edgezero-core --lib finish_batch_item_ +cargo test --offline --locked -p edgezero-adapter-axum --lib +cargo test --offline --locked -p edgezero-adapter-fastly --lib +cargo check --offline --locked -p edgezero-adapter-cloudflare --tests --target wasm32-unknown-unknown --features cloudflare +cargo check --offline --locked -p edgezero-adapter-fastly --tests --target wasm32-wasip1 --features fastly +cargo check --offline --locked -p edgezero-adapter-spin --tests --target wasm32-wasip2 --features spin +``` + +Expected: all pass. + +- [x] **Step 5: Strengthen batch invariant tests before changing API documentation** + +Add exact-message assertions for premature EOF, duplicate index, and out-of-range index. Add a direct `OutboundBatch::cutoff` test and a test proving a second `next()` after an invariant failure returns the poisoned re-poll diagnostic. + +- [x] **Step 6: Run the invariant characterization tests** + +Run: + +```bash +cargo test --offline --locked -p edgezero-core --lib batch_collection_ +cargo test --offline --locked -p edgezero-core --lib batch_cutoff_ +cargo test --offline --locked -p edgezero-core --lib batch_poisoned_ +``` + +Expected: the exact diagnostics and poison/cutoff state behavior already pass. These tests pin +existing invariants before the public documentation/export change; they are characterization +coverage, not a behavior change. + +- [x] **Step 7: Add a red public root-export consumer check** + +Update the generated template and app-demo fixture to import +`edgezero_core::OutboundBatchDriverEvent` from the crate root, then run: + +```bash +cargo test --offline --locked --manifest-path examples/app-demo/Cargo.toml -p app-demo-core --no-run +scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds +``` + +Expected: unresolved import because the root export does not exist yet. + +- [x] **Step 8: Promote the real batch-driver construction API** + +Remove `#[doc(hidden)]` from `OutboundBatchDriverEvent`, `OutboundBatch::from_driver`, and `OutboundBatch::cutoff`; document event ordering, EOF, failure, cutoff, and index invariants. Re-export `OutboundBatchDriverEvent` from the core crate root and update generated/demo test clients to use the root export. Keep one API only; add no compatibility alias. + +- [x] **Step 9: Run core, CLI, and demo checks** + +Run: + +```bash +cargo test --offline --locked -p edgezero-core --lib batch_ +cargo test --offline --locked -p edgezero-cli +cargo test --offline --locked --manifest-path examples/app-demo/Cargo.toml -p app-demo-core +scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds +``` + +Expected: all pass and generator assertions match the public API. + +### Task 2: Prevent manual framing for Fastly body-suppressed responses + +**Files:** +- Modify: `crates/edgezero-adapter-fastly/src/response.rs` + +- [x] **Step 1: Add red HEAD and 304 framing tests** + +Extend the mock committer's prepared value to record `transmits_body`. Assert that HEAD and 304 responses retain valid representation `Content-Length`, transmit no body, and pass `false` through the committer boundary. Add a payload response assertion that passes `true`. + +- [x] **Step 2: Run the focused tests and verify RED** + +Run: + +```bash +cargo test --offline --locked -p edgezero-adapter-fastly --lib body_suppressed_response_disables_manual_framing +cargo test --offline --locked -p edgezero-adapter-fastly --lib payload_response_allows_manual_framing +``` + +Expected: compilation/assertion failure because `EgressCommitter::prepare` does not receive the disposition. + +- [x] **Step 3: Thread the body disposition through Fastly preparation** + +Read `PreparedResponseEgress::transmits_body()` before consuming it, pass the boolean through `EgressCommitter::prepare`, and call `set_manual_framing` only when the response transmits a body and contains `Content-Length`. Update the raw-ABI happy-path test call site. Do not change core's HEAD/304 metadata preservation. + +- [x] **Step 4: Run Fastly native and WASM tests** + +Run: + +```bash +cargo test --offline --locked -p edgezero-adapter-fastly --lib +(cd crates/edgezero-adapter-fastly && cargo test --offline --locked --target wasm32-wasip1 --features fastly --lib) +``` + +Expected: all pass. + +### Task 3: Align the contract and documentation + +**Files:** +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `docs/guide/capabilities.md` +- Modify: `docs/guide/proxying.md` +- Modify: `docs/superpowers/plans/2026-09-06-outbound-http-phase2-body-response-limits.md` +- Modify: `docs/superpowers/plans/2026-09-06-outbound-http-phase4-axum-cloudflare.md` +- Modify: `docs/superpowers/plans/2026-09-06-outbound-http-phase5-spin.md` +- Modify: `docs/superpowers/plans/2026-09-06-outbound-http-phase6-fastly.md` +- Modify: `scripts/check_outbound_docs_contract.mjs` + +- [x] **Step 1: Add red documentation-contract expectations** + +Change the checker to require wording that response-header caps count adapter-visible upstream fields before normalization, including fields later stripped, plus EdgeZero's synthetic proxy field. Require the normative concurrent-driver statement that completion samples are taken synchronously in observation order from one monotonic clock. + +- [x] **Step 2: Run the docs checker and verify RED** + +Run: + +```bash +node scripts/check_outbound_docs_contract.mjs +``` + +Expected: failure because the current guide/spec still say `guest-visible` and omit the monotonic ordering statement. + +- [x] **Step 3: Update normative spec, public guides, and phase-plan terminology** + +Replace misleading `guest-visible header` wording with the exact pre-normalization accounting boundary. State that stripped hop-by-hop fields consume budget and the synthetic `x-edgezero-proxy` field is charged afterward. Document why the concurrent adapters may terminate on their first cutoff observation without discarding an earlier terminal sample, while retaining Fastly's separate BestEffort limitation. Keep proxying's `None` meaning unchanged. + +- [x] **Step 4: Run documentation validation** + +Run: + +```bash +node scripts/check_outbound_docs_contract.mjs +npm --prefix docs run lint +npm --prefix docs run format +``` + +Expected: all pass. + +### Task 4: Verify the complete change and update the PR + +**Files:** +- Modify: `crates/edgezero-adapter-fastly/src/fastly_response_abi.rs` +- Modify: `docs/superpowers/plans/2026-09-17-outbound-http-round6-hardening.md` + +- [x] **Step 1: Audit the Fastly raw-ABI test boundary** + +Confirm the existing mock suite covers post-commit abandon, write/source/deadline failures, and finish consumption. Add only target-valid status-result coverage if it exercises the real shared mapper. Do not add an ABI indirection layer solely to synthesize provider hostcall failures; record Viceroy fault-injection limits in the final review note. + +Audit result: the portable mock suite covers those lifecycle branches, and the real shared status +mapper now has a `wasm32-wasip1`/Viceroy test for success and host failure. Viceroy does not expose +fault injection for each downstream response hostcall, so provider-hostcall failure coverage stops +at that mapper rather than adding a production ABI indirection solely for tests. + +- [x] **Step 2: Mark implementation/documentation tasks complete in this plan** + +Update completed checkboxes before final validation so the checked tree is the tree that will be +committed. Leave only the final gate/review/commit steps unchecked while commands are running. + +- [x] **Step 3: Run every required repository gate** + +Run: + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace --all-targets +cargo check --workspace --all-targets --features "fastly cloudflare spin" +cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin +bash scripts/run_tests.sh +``` + +Expected: all gates pass. + +- [x] **Step 4: Review the final diff** + +Verify there are no adapter-local `finish_batch_item` definitions, no hidden public driver constructors used by scaffolds, no unconditional Fastly manual framing, and no stale `guest-visible` header-cap claims. + +Independent runtime review found no code defect. Independent contract review found and closed four +documentation/API-hardening gaps: the normative spec now includes the public driver protocol and +shared helper, the historical termination plan no longer calls that protocol doc-hidden, Phase 4/5 +mark their removed `send_all` APIs as superseded, and the public driver event is non-exhaustive. + +- [x] **Step 5: Mark verification complete and validate the final documentation state** + +Mark the gate/review checkboxes complete, then run: + +```bash +git diff --check +node scripts/check_outbound_docs_contract.mjs +npm --prefix docs run format +``` + +Expected: all pass after the final plan-file edits. + +- [x] **Step 6: Commit and push** + +Commit the implementation and documentation together with a focused message, push `docs/outbound-http-spec`, and confirm PR 275 checks start against the pushed SHA. + +### Task 5: Preserve early Fastly phase timeouts as slot results + +**Files:** +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-adapter-fastly/src/outbound.rs` +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `scripts/check_outbound_docs_contract.mjs` + +- [x] **Step 1: Add and run the red classifier regression** + +Change the attributed-timeout characterization so a `BatchCutoff` timeout observed before the +absolute cutoff must produce a terminal item. Run the focused test and require one failing test, +not a zero-test filter. + +- [x] **Step 2: Make absolute clock expiry the only cutoff signal** + +Remove provenance inspection from `finish_batch_item`. Keep equality expired and preserve the +existing backward-clock fail-closed behavior. Add a Fastly batch-level regression proving an early +timeout remains terminal while a later sibling is retained. Update the normative spec and +executable docs contract so `BudgetSource::BatchCutoff` cannot be interpreted as batch +abandonment. + +- [x] **Step 3: Run focused and complete verification** + +Run the focused regression, core tests, strict workspace gates, every adapter feature/WASM matrix, +generated-project and app-demo verification, and documentation checks. + +- [x] **Step 4: Commit and push the follow-up** + +Commit the regression and contract correction together, push `docs/outbound-http-spec`, and verify +PR 275 points at the pushed SHA. diff --git a/docs/superpowers/plans/2026-09-19-detached-egress-config-deadlines.md b/docs/superpowers/plans/2026-09-19-detached-egress-config-deadlines.md new file mode 100644 index 000000000..bc9f9dff5 --- /dev/null +++ b/docs/superpowers/plans/2026-09-19-detached-egress-config-deadlines.md @@ -0,0 +1,243 @@ +# Detached Egress and Config Deadline Closure Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Cover normalized pre-admission responses with application-owned completion resources and ensure asynchronous bounded config reads return at their absolute deadline without overstating provider cancellation. + +**Architecture:** Core gains one default-empty detached-egress completion factory keyed by bounded failure metadata. Bounded store methods become required hard-cut trait methods; Cloudflare and Spin race asynchronous provider futures against native timers, while Axum and Fastly retain accurately documented synchronous limitations. Application permits remain application-defined, while a generic core late-bound owner centralizes the response-lifecycle synchronization needed to retain any `Send` resource until terminal egress or abandonment. + +**Tech Stack:** Rust 2024, `async-trait`, `futures-util`, Tokio, Cloudflare Workers `Delay`, Spin SDK `sleep`, existing EdgeZero ingress/egress and capability APIs. + +--- + +### Task 1: Add detached normalized-rejection completion ownership + +**Files:** +- Modify: `crates/edgezero-core/src/response_egress.rs` +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Test: `crates/edgezero-core/src/app.rs` + +- [x] **Step 1: Write failing core lifecycle tests** + + Add tests proving a normalized rejection calls the configured factory once with method, + application-clock request start, status, and stable error kind; invokes the returned completion + exactly once at terminal egress; leaves policy/observer untouched; and drops a captured resource + without a fabricated report when the envelope is abandoned before `begin()`. + +- [x] **Step 2: Run the focused tests and verify RED** + + Run: `cargo test -p edgezero-core --lib app::tests::detached_ingress` + + Expected: compile failure because the detached head and setter do not exist. + +- [x] **Step 3: Implement the factory** + + Add `DetachedResponseEgressHead<'_>` with read-only accessors for request method, request start, + status, and `EdgeError::kind()`. Store an `Arc Fn(&DetachedResponseEgressHead<'head>) -> ResponseEgressCompletion + Send + Sync>` in `App`, default it to an empty completion, expose a generic setter, and call it exactly once from + `detached_ingress_error_egress` before consuming the error into its response. + +- [x] **Step 4: Run core tests** + + Run: `cargo test -p edgezero-core --lib app::tests::detached_ingress` + + Expected: all detached lifecycle cases pass. + +### Task 2: Prove detached completion in every adapter + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/service.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/contract.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` + +- [x] **Step 1: Strengthen existing normalized-ingress rejection tests** + + Configure a detached completion factory with a drop/completion counter, trigger normalized head + rejection, and prove each adapter settles one completion after delivery while admission, + middleware, handlers, policy, observer, and body polling remain untouched. + +- [x] **Step 2: Run or compile each adapter contract under its supported target** + + Run the Axum `normalized_ingress_error` and admission-policy-error filters with a required + nonzero test count. Cloudflare, Fastly, and Spin ingress contracts are target-only, so compile + those tests under strict target Clippy; local runtime execution remains dependent on the pinned + provider runners used by hosted CI. + +### Task 3: Hard-cut bounded store implementations + +**Files:** +- Modify: `crates/edgezero-core/src/config_store.rs` +- Modify: `crates/edgezero-core/src/secret_store.rs` +- Modify: core and adapter test fixtures implementing either trait + +- [x] **Step 1: Remove both cooperative defaults** + + Make `ConfigStore::get_bounded` and `SecretStore::get_bytes_bounded` required. Keep unbounded + convenience reads separate and explicitly outside extraction guarantees. + +- [x] **Step 2: Run workspace check and record expected compile failures** + + Run: `cargo check --workspace --all-targets --features "fastly cloudflare spin"` + + Expected: every fixture relying on the old default fails with a missing required method. + +- [x] **Step 3: Migrate fixtures explicitly** + + Give deterministic in-memory fixtures finite pre/post checks and exact byte accounting. Do not + restore a blanket compatibility helper that can hide a pending provider future. + +- [x] **Step 4: Run core store and extractor tests** + + Run: `cargo test -p edgezero-core --lib config_store` and + `cargo test -p edgezero-core --lib extractor`. + +### Task 4: Race Cloudflare and Spin asynchronous reads + +**Files:** +- Modify: `crates/edgezero-adapter-cloudflare/src/config_store.rs` +- Modify: `crates/edgezero-adapter-spin/src/config_store.rs` +- Modify: `crates/edgezero-adapter-spin/src/secret_store.rs` +- Test: colocated unit tests in those modules + +- [x] **Step 1: Write never-ready and equality-precedence tests** + + Inject a pending provider future and a controlled timer. Prove a clock-confirmed timer completion + returns the typed deadline error, drops the provider future, retains no partial value, and wins + when both sides are ready at equality. Prove an early timer wake re-arms without discarding the + provider result. Repeat for Spin secret reads because typed config extraction shares the same + deadline. + +- [x] **Step 2: Run focused tests and verify they hang/fail under the old await-only helper** + + Use injected immediately-ready timers so the red test itself remains bounded. + +- [x] **Step 3: Implement native timer races** + + Compute remaining time from the injected application clock before provider polling. Cloudflare + uses `worker::Delay`; Spin uses `spin_sdk::time::sleep`. Poll the timer first for equality + precedence, drop the losing provider future, then retain the existing post-ready deadline and + byte-cap checks. Native test-only backends use an injected timer helper rather than wall sleeps. + +- [x] **Step 4: Run native and WASM checks** + + Run the affected native tests plus Cloudflare `wasm32-unknown-unknown` and Spin `wasm32-wasip2` + max-feature Clippy commands. + +### Task 5: Document late binding and align consumer surfaces + +**Files:** +- Modify: `docs/guide/handlers.md` +- Modify: `docs/guide/capabilities.md` +- Modify: `docs/superpowers/specs/2026-08-22-inbound-body-design.md` +- Modify: `docs/superpowers/specs/2026-09-08-response-egress-design.md` +- Modify: `docs/superpowers/specs/2026-06-16-blob-app-config.md` +- Modify: `examples/app-demo/crates/app-demo-core/src/lib.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/lib.rs.hbs` +- Test: demo lifecycle tests and generated-project compile test + +- [x] **Step 1: Add the late-bound shared one-shot example** + + Show one handle travelling through `IngressGrant` and one through + `ResponseEgressCompletion`; the handler installs the acquired permit and terminal completion + takes/releases it. State explicitly that handler-local capture releases too early and that + install-after-terminal must fail closed. + +- [x] **Step 2: Align capability language** + + State that Cloudflare/Spin timer races bound EdgeZero's return, not provider cancellation; + Fastly remains non-preemptible; no adapter satisfies a Native cancellation requirement. + +- [x] **Step 3: Keep demo and generator on the public hard-cut API** + + Configure the detached factory and add lifecycle assertions without pretending the illustrative + lease is a production semaphore. Regenerate/compile the scaffold to prove template parity. + +### Task 6: Replace application-owned permit slots with a reusable core primitive + +**Files:** +- Modify: `crates/edgezero-core/src/response_egress.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `examples/app-demo/crates/app-demo-core/src/lib.rs` +- Modify: `examples/app-demo/crates/app-demo-core/src/handlers.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/lib.rs.hbs` +- Modify: `crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `docs/guide/handlers.md` +- Modify: `docs/superpowers/specs/2026-09-08-response-egress-design.md` + +- [x] **Step 1: Write failing core state-machine tests** + + Specify `ResponseEgressCompletion::late_bound::()` returning one non-clone + `ResponseEgressResource` installer plus its terminal completion. Prove one installation lives + until terminal egress; install-before-terminal and terminal-before-install ordering; duplicate + installation; completion abandonment before and after installation while the installer remains; + and competing terminal attempts. Require duplicate and closed installations to drop the rejected + resource outside the mutex and return distinct typed errors, terminal/abandonment to release an + installed resource exactly once, and envelope abandonment to fabricate no completion or observer + report. Add a compile-fail doctest proving the installer is non-clone and a type assertion proving + it is `Send + Sync` for `T: Send` even when `T` is not `Sync`. Race installation against close, + verify poisoned-lock recovery, and require the typed install failure to implement + `std::error::Error`. + +- [x] **Step 2: Run the focused tests and verify RED** + + Run: `cargo test -p edgezero-core --lib response_egress::tests::late_bound` + + Expected: compile failure because the reusable constructor, resource, and install error do not + exist. + +- [x] **Step 3: Implement the generic ownership primitive** + + Add `ResponseEgressResource` backed by one private shared mutex state and + `ResponseEgressResourceInstallError::{AlreadyInstalled, Closed}`. Its public + `install(&self, resource: T)` method consumes the candidate and never returns it. Require + `T: Send + 'static`. Capture a private completion-side owner in the callback; both callback + execution and owner `Drop` close the slot and take the installed resource while the installer may + remain alive. Always release the mutex before dropping application resources. Keep terminal + callback execution in the existing `ResponseEgressCompletion` exactly-once boundary. + +- [x] **Step 4: Run focused core tests and strict core Clippy** + + Run: `cargo test -p edgezero-core --lib response_egress::tests::late_bound` and + `cargo test -p edgezero-core --doc response_egress` and + `cargo clippy -p edgezero-core --all-targets --all-features -- -D warnings`. + +- [x] **Step 5: Make generator assertions reject the local state machine and verify RED** + + Require `ResponseEgressCompletion::late_bound` and `ResponseEgressResource` in generated core + source, and reject `ResponsePermitSlot`, `ResponsePermitState`, and an application-owned mutex + lifecycle implementation. Run generator tests and verify the existing template fails. + +- [x] **Step 6: Hard-cut demo and generator to the core primitive** + + Delete their local mutex state machine. Keep only the illustrative application permit, + admission lease metadata, and one `install(permit)` call. Retain one integration lifecycle test + in each generated/demo surface; move duplicate/post-terminal/abandonment proofs to core. + +- [x] **Step 7: Replace documentation examples** + + Document the public primitive and its typed failure semantics. Make generated-project assertions + and the normative specification require the core late-bound owner instead of application-owned + synchronization. + +- [x] **Step 8: Run demo, generator, and generated-project gates** + + Run demo tests and strict Clippy, generator unit tests, and the ignored generated workspace gate. + +### Task 7: Full verification + +**Files:** +- Verify all modified files; do not alter unrelated worktree changes. + +- [x] **Step 1: Run repository gates** + + Run workspace tests, strict Clippy, formatting, strict rustdoc, feature compilation, docs/legacy + contract scripts, app-demo tests/Clippy, generated workspace compilation, and all supported WASM + max-feature Clippy checks. + +- [x] **Step 2: Review the final diff** + + Confirm normalized errors own one completion, pending async config reads return typed deadline + errors, capability claims remain conservative, and the pre-existing rustdoc/CI edits are + preserved. diff --git a/docs/superpowers/plans/2026-09-19-detached-ingress-abort-decision.md b/docs/superpowers/plans/2026-09-19-detached-ingress-abort-decision.md new file mode 100644 index 000000000..305a73088 --- /dev/null +++ b/docs/superpowers/plans/2026-09-19-detached-ingress-abort-decision.md @@ -0,0 +1,211 @@ +# Detached Ingress Abort Decision Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Let an application atomically choose either a response-owned completion resource or a transport abort for normalized ingress errors before EdgeZero constructs a detached response. + +**Architecture:** Replace the completion-only detached-response factory with a synchronous typed decision factory returning `DetachedResponseEgressDecision::Send(ResponseEgressCompletion)` or `Abort`. `App::detached_ingress_error_egress` converts that decision into the existing `IngressDispatchOutcome`, and every adapter maps `Aborted` through its existing non-response abort/error boundary. This is a hard cut: no compatibility aliases or legacy callback remain. + +**Tech Stack:** Rust 2024, EdgeZero core ingress/response-egress APIs, Axum/Cloudflare/Fastly/Spin adapters, colocated and adapter contract tests, VitePress documentation. + +--- + +### Task 1: Specify the detached error decision contract + +**Files:** +- Modify: `docs/superpowers/specs/2026-08-22-inbound-body-design.md` +- Modify: `docs/superpowers/specs/2026-09-08-response-egress-design.md` +- Modify: `docs/guide/capabilities.md` +- Modify: `docs/guide/handlers.md` + +- [x] **Step 1: Update the normative contract** + + Define `DetachedResponseEgressDecision::{Send(ResponseEgressCompletion), Abort}`. State that the callback executes exactly once after normalized metadata exists and before response construction. `Abort` creates no response, envelope, completion, observer report, middleware call, handler call, or body poll. The native adapter proves zero response bytes over a raw socket; provider adapters return through their existing fixed pre-response error boundaries without claiming a transport-observed reset. + +- [x] **Step 2: Preserve capability boundaries** + + Keep raw parser failures outside the portable lifecycle and retain the existing `BestEffort` response-egress capability classifications. Update the public capability guide without promoting provider abort support. + +- [x] **Step 3: Run documentation checks** + + Run: `node scripts/check_outbound_docs_contract.mjs` + + Expected: PASS with no stale completion-only factory contract. + +### Task 2: Add failing decision tests across core and adapters + +**Files:** +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-adapter-axum/src/service.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/contract.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` + +- [x] **Step 1: Write failing core tests for `Send`, `Abort`, and the default** + + Add tests proving `Send` creates detached egress and settles its completion exactly once, while `Abort` returns `IngressDispatchOutcome::Aborted` without constructing or settling a completion. Prove an unset factory defaults to `Send(ResponseEgressCompletion::empty())`. + +- [x] **Step 2: Run the focused tests and observe RED** + + Run: `cargo test -p edgezero-core detached_ingress_error` + + Expected: FAIL because the typed decision and outcome-returning API do not exist. + +- [x] **Step 3: Write failing adapter tests before changing production call sites** + + Add two cases per adapter: normalized request-head validation failure and admission-policy failure. Each decision factory returns `Abort`; assert exactly one factory call and zero response delivery, body polling, middleware, handler, completion, and observer activity. Where a failing policy created an owned probe before returning its error, assert its drop count. For the native adapter, add a raw-socket proof of zero response bytes. For provider adapters, assert the existing fixed pre-response error boundary without claiming a transport reset. + +- [x] **Step 4: Run the focused suites and observe RED** + + Run: + + `scripts/run_test_nonzero.sh normalized_ingress_error_decision_abort_closes_without_response cargo test -p edgezero-adapter-axum --no-default-features --features axum,test-utils --lib normalized_ingress_error_decision_abort_closes_without_response` + + `scripts/run_test_nonzero.sh normalized_ingress_error_decision_abort_uses_provider_error cargo test -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --test contract normalized_ingress_error_decision_abort_uses_provider_error` + + `scripts/run_test_nonzero.sh normalized_ingress_error_decision_abort_uses_provider_error cargo test -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --test contract normalized_ingress_error_decision_abort_uses_provider_error` + + `scripts/run_test_nonzero.sh normalized_ingress_error_decision_abort_uses_provider_error cargo test -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test contract normalized_ingress_error_decision_abort_uses_provider_error` + + Expected: FAIL because the decision enum/setter do not exist. The sentinels prove the intended suites are non-empty. + +### Task 3: Implement the core decision and adapter mappings + +**Files:** +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-core/src/response_egress.rs` +- Modify: `crates/edgezero-adapter-axum/src/service.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/request.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/contract.rs` +- Modify: `crates/edgezero-adapter-fastly/src/request.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` +- Modify: `crates/edgezero-adapter-spin/src/request.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` + +- [x] **Step 1: Implement the hard-cut core API** + + Add the public non-exhaustive decision enum and decision-factory alias. Replace `set_detached_response_egress_completion_factory` with `set_detached_response_egress_decision_factory`. Change `detached_ingress_error_egress` to return `IngressDispatchOutcome`, using `Response(Box)` for `Send` and `Aborted` for `Abort`. Default to `Send(ResponseEgressCompletion::empty())`. + +- [x] **Step 2: Migrate existing `Send` fixtures and map every adapter boundary** + + Wrap every existing detached completion in `DetachedResponseEgressDecision::Send`. Match `IngressDispatchOutcome::Response`, `Aborted`, and a wildcard arm at every normalized-validation and admission-policy-error call site. Both `Aborted` and unknown future variants fail closed through the existing non-response abort boundary; do not add another transport error type. + +- [x] **Step 3: Run core and focused adapter tests and observe GREEN** + + Run: `cargo test -p edgezero-core detached_ingress_error` + + Run the four `scripts/run_test_nonzero.sh` commands from Task 2, then run equivalent sentinel commands for each admission-policy-error abort test. + + Expected: PASS. + +- [x] **Step 4: Run the core suite** + + Run: `cargo test -p edgezero-core` + + Expected: PASS. + +### Task 4: Align the reference app and generator + +**Files:** +- Modify: `examples/app-demo/crates/app-demo-core/src/lib.rs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/lib.rs.hbs` + +- [x] **Step 1: Add a failing generator assertion** + + Require generated applications to use the decision factory and `Send` variant. Retain one intentional negative assertion rejecting the removed completion-only setter. + +- [x] **Step 2: Run the generator test and observe RED** + + Run: `cargo test -p edgezero-cli generate_new_scaffolds_workspace_layout` + + Expected: FAIL on the legacy setter or missing decision enum. + +- [x] **Step 3: Update demo and templates** + + Wrap completion resources in `DetachedResponseEgressDecision::Send`. Keep the reference behavior unchanged; the API demonstrates where a bounded application may return `Abort`. + +- [x] **Step 4: Run demo and generator tests** + + Run: `cargo test -p edgezero-cli generate_new_scaffolds_workspace_layout` + + Run from `examples/app-demo`: `cargo test --workspace --all-targets` + + Expected: PASS. + +### Task 5: Complete cross-target verification + +**Files:** +- Modify only files required by verification failures caused by this hard cut. +- Modify: `scripts/check_outbound_legacy_api.sh` +- Modify: `scripts/check_outbound_docs_contract.mjs` + +- [x] **Step 1: Enforce the hard cut in repository checkers** + + Extend the legacy API checker to reject the old factory type/setter in active Rust/template surfaces. Extend the docs contract checker to require the new decision enum, `Send`/`Abort` semantics, and unchanged raw-parser/provider capability caveats. + + Run a scoped stale-API search over `crates`, `examples`, and active guide/spec documents, excluding the intentional generator negative assertion and historical plans. + + Expected: no production, template, test, or documentation matches. + +- [x] **Step 2: Run formatting and native gates** + + Run: `cargo fmt --all -- --check` + + Run: `cargo test --workspace --all-targets` + + Run: `cargo clippy --workspace --all-targets --all-features -- -D warnings` + + Expected: PASS. + +- [x] **Step 3: Run feature and WASM gates** + + Run: `cargo check --workspace --all-targets --features "fastly cloudflare spin"` + + Run the strict WASM Clippy matrix: + + ```bash + cargo clippy -p edgezero-adapter-cloudflare --features "cloudflare" --target wasm32-unknown-unknown --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-cloudflare --features "cloudflare test-utils" --target wasm32-unknown-unknown --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-fastly --features "fastly" --target wasm32-wasip1 --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-fastly --features "fastly cli" --target wasm32-wasip1 --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-fastly --features "fastly test-utils" --target wasm32-wasip1 --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-fastly --features "fastly cli test-utils" --target wasm32-wasip1 --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-spin --features "spin" --target wasm32-wasip2 --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-spin --features "spin test-utils" --target wasm32-wasip2 --all-targets -- -D warnings + ``` + + Run the target-specific adapter contract commands from Task 2 with configured WASM runners, plus: + + ```bash + bash scripts/check_adapter_feature_matrix.sh cloudflare wasm32-unknown-unknown + bash scripts/check_adapter_feature_matrix.sh fastly wasm32-wasip1 + bash scripts/check_adapter_feature_matrix.sh spin wasm32-wasip2 + ``` + + Expected: PASS. + +- [x] **Step 4: Run rustdoc, docs, demo, and generated-project gates** + + Run: + + ```bash + RUSTDOCFLAGS='-D warnings' cargo doc --offline --locked --workspace --all-features --no-deps + scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds + bash scripts/check_outbound_legacy_api.sh + node scripts/check_outbound_docs_contract.mjs + (cd docs && npm run lint && npm run format && npm run build) + (cd examples/app-demo && cargo fmt --all -- --check) + (cd examples/app-demo && cargo test --locked --workspace --all-targets) + (cd examples/app-demo && cargo clippy --workspace --all-targets --all-features -- -D warnings) + (cd examples/app-demo && cargo check --locked -p app-demo-adapter-cloudflare --target wasm32-unknown-unknown --no-default-features --features cloudflare) + (cd examples/app-demo && cargo check --locked -p app-demo-adapter-fastly --target wasm32-wasip1 --no-default-features --features fastly) + (cd examples/app-demo && cargo check --locked -p app-demo-adapter-spin --target wasm32-wasip2 --no-default-features --features spin) + ``` + + Expected: PASS. + +- [x] **Step 5: Review, commit, and push** + + Review the diff for accidental compatibility shims, stale terminology, and unrelated changes. Commit with an application-neutral message and push to the existing PR branch. diff --git a/docs/superpowers/plans/2026-09-19-response-completion-ingress-abort.md b/docs/superpowers/plans/2026-09-19-response-completion-ingress-abort.md new file mode 100644 index 000000000..6e7f5f5bc --- /dev/null +++ b/docs/superpowers/plans/2026-09-19-response-completion-ingress-abort.md @@ -0,0 +1,676 @@ +# Response Completion, Ingress Abort, and Fallible App Assembly Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to +> implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Apply +> `superpowers:test-driven-development` to every behavior change and +> `superpowers:verification-before-completion` before claiming success. + +**Goal:** Give every response-producing admission decision one non-clone completion owner, add a +portable non-response ingress abort outcome, and make application configuration fail explicitly +before an adapter begins serving a request. + +**Architecture:** `AdmissionDecision` owns either a response completion, grant, and deadline or an +explicit abort. Core transfers the completion through `PreparedIngress`, +`ResponseEgressEnvelope`, and the sole `ResponseEgressAttempt`; the attempt stores terminal state, +invokes the response completion once, then invokes the global observer. Adapters map abort to their +strongest pre-response transport boundary and propagate fallible `App::build::(platform)` before request +conversion, admission, or body polling. This is an atomic hard cut with no extension carrier, +infallible compatibility method, or response-extraction bypass. + +**Source of truth:** +- [`2026-09-08-response-egress-design.md`](../specs/2026-09-08-response-egress-design.md) +- [`2026-08-22-inbound-body-design.md`](../specs/2026-08-22-inbound-body-design.md) +- [`2026-06-16-blob-app-config.md`](../specs/2026-06-16-blob-app-config.md) + +**Tech Stack:** Rust 1.95, `http`, futures, Hyper/Axum/Tokio, Cloudflare Workers, Fastly Compute, +Spin SDK WASIp3, proc macros, Handlebars templates, VitePress documentation. + +--- + +## File Structure + +- `crates/edgezero-core/src/response_egress.rs`: owns the non-clone completion callback and invokes + it from the exactly-once attempt state machine. +- `crates/edgezero-core/src/ingress.rs`: owns admission decision/outcome types and transfers the + completion or explicit abort without body access. +- `crates/edgezero-core/src/app.rs`: retains completion across routing/error replacement and exposes + a response-or-abort high-level dispatch result; hard-cuts `Hooks` to fallible construction. +- `crates/edgezero-core/src/manifest.rs` and `crates/edgezero-core/src/lib.rs`: expose the new public + lifecycle types and `ingress-admission-abort` capability. +- `crates/edgezero-macros/src/app.rs`: emits the fallible `configure`/`build_app` contract without + discarding callback results. +- `crates/edgezero-adapter-axum/src/{service,connection,dev_server}.rs`: maps abort to a Hyper + service error/connection close and configuration failure to an error before listener bind. +- `crates/edgezero-adapter-{cloudflare,fastly,spin}/src/{lib,request}.rs`: maps abort to a fixed + platform error and configuration failure to the documented pre-dispatch boundary. +- `crates/edgezero-adapter-{axum,cloudflare,fastly,spin}/src/cli.rs`: declares the new abort + capability conservatively. +- `examples/app-demo/crates/app-demo-core/src/lib.rs` and + `crates/edgezero-cli/src/templates/core/src/lib.rs.hbs`: demonstrate the new completion and + fallible configuration signatures. +- `crates/edgezero-cli/src/generator.rs`, adapter templates, and demo adapter entrypoints: assert + generated and shipped applications use the hard-cut APIs. +- `docs/guide/{handlers,capabilities}.md`, adapter guides, and + `scripts/check_outbound_docs_contract.mjs`: publish and mechanically enforce the contract. + +## Global Constraints + +- `ResponseEgressCompletion` is non-clone, never stored in `http::Extensions`, and always present + on response-producing admission decisions; no-resource callers use `empty()`. +- Terminal state is stored before callbacks. Completion runs once before the global observer; on + unwind-capable targets each panic is contained independently with category-only logging. +- `ResponseEgressEnvelope::into_response()` is removed. A response can be transmitted only after + `begin()` transfers completion ownership into an attempt. +- `Abort` creates no response, envelope, attempt, completion report, or global observer report and + polls no request body. +- No adapter maps `Abort` to EdgeZero's normal JSON error response. +- `Hooks::configure` and core-owned `App::build::` return `Result`; no unchecked or infallible bridge is + retained. +- After every production edit, run the narrowest affected package tests before proceeding. + +### Task 1: Add the response-scoped completion state machine + +**Files:** +- Modify: `crates/edgezero-core/src/response_egress.rs` +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-macros/tests/app_macro.rs` +- Modify: `crates/edgezero-adapter-axum/src/{connection,response}.rs` +- Modify: `crates/edgezero-adapter-{cloudflare,fastly,spin}/src/response.rs` +- Test: colocated tests in `crates/edgezero-core/src/response_egress.rs` + +- [x] **Step 1: Write red completion-order and exactly-once tests** + + Add tests proving normal completion, source/transport failure, fallback completion, guard drop, + and competing terminal signals invoke the response completion exactly once before the global + observer. Add an unwind test proving a completion panic does not suppress the observer and an + observer panic does not re-enter completion. + + Use an ordered recorder whose expected sequence is: + + ```rust + vec!["completion:completed", "observer:completed"] + ``` + +- [x] **Step 2: Run the focused tests and verify the missing API fails** + + Run: + + ```bash + cargo test -p edgezero-core --lib response_egress::tests::response_completion + ``` + + Expected: compile failure because `ResponseEgressCompletion` and the completion constructor + parameter do not exist. + +- [x] **Step 3: Implement direct completion ownership** + + Add the public non-clone owner: + + ```rust + pub struct ResponseEgressCompletion { + callback: Option>, + } + + impl ResponseEgressCompletion { + pub fn empty() -> Self; + pub fn new(complete: Complete) -> Self + where + Complete: FnOnce(&ResponseEgressReport) + Send + 'static; + } + ``` + + Move it into `ResponseEgressEnvelope`, transfer it to `ResponseEgressAttempt` before policy or + framing work, store terminal state first, invoke completion through its own unwind boundary, and + then invoke the observer. In this same step, update every direct attempt/envelope constructor in + core `app.rs` and all four adapter response/connection test helpers to pass an explicit empty + completion. Do not defer those call sites to later tasks: Task 1 must leave the workspace + compilable. Re-export the type from `edgezero_core`. + +- [x] **Step 4: Remove the response extraction bypass** + + Delete `ResponseEgressEnvelope::into_response()`. Replace core test inspection with a test-only + helper that calls `begin()`, begins writing, terminally settles the attempt, and only then obtains + the response from `PreparedResponseEgress`. Migrate the macro integration test at the same time + so the workspace-wide all-targets check remains compilable. Add `compile_fail` rustdoc examples + proving the completion cannot be cloned or inserted into `http::Extensions`. + +- [x] **Step 5: Run the core lifecycle suite** + + Run: + + ```bash + cargo test -p edgezero-core --lib response_egress + cargo test -p edgezero-core --doc + cargo check --workspace --all-targets --features "fastly cloudflare spin" + cargo check -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare + cargo check -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly + cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin + ``` + + Expected: all response-egress unit tests and compile-fail doctests pass. + +- [x] **Step 6: Commit the core completion primitive** + + ```bash + git add crates/edgezero-core/src/response_egress.rs crates/edgezero-core/src/app.rs \ + crates/edgezero-core/src/lib.rs crates/edgezero-macros/tests/app_macro.rs \ + crates/edgezero-adapter-*/src/response.rs \ + crates/edgezero-adapter-axum/src/connection.rs + git commit -m "feat(core): add response-scoped egress completion" + ``` + +### Task 2: Carry completion through ingress and add explicit abort + +**Files:** +- Modify: `crates/edgezero-core/src/ingress.rs` +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-core/src/router.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Test: colocated tests in those files + +- [x] **Step 1: Write red admission ownership tests** + + Add table-driven tests for `Admit`, `ReadBodyBeforeFallback`, `Refuse`, and `Abort`. Prove the + exact completion reaches normal handler success, handler error, canonical 404/405, exact-cap + fallback, overflow, timeout, admitted conversion error, and detached refusal. Prove a dropped + pre-begin envelope releases the captured resource without invoking its callback. + +- [x] **Step 2: Write red abort tests at the core boundary** + + Assert `Abort` yields `IngressAdmissionOutcome::Aborted`, then + `IngressBeginOutcome::Aborted`, and finally `IngressDispatchOutcome::Aborted`; body, middleware, + handler, completion, observer, and response counters must remain zero. + +- [x] **Step 3: Run focused tests and verify the variants fail to compile** + + Run: + + ```bash + cargo test -p edgezero-core --lib ingress::tests + cargo test -p edgezero-core --lib app::tests + ``` + + Expected: compile failures for the new completion fields and abort/result variants. + +- [x] **Step 4: Hard-cut the admission API** + + Implement these shapes without compatibility constructors: + + ```rust + pub enum AdmissionDecision { + Admit { + completion: ResponseEgressCompletion, + grant: IngressGrant, + read_deadline: Deadline, + }, + ReadBodyBeforeFallback { + completion: ResponseEgressCompletion, + grant: IngressGrant, + max_body_bytes: usize, + read_deadline: Deadline, + on_exceeded: BufferedIngressResponse, + on_timeout: BufferedIngressResponse, + }, + Refuse { + completion: ResponseEgressCompletion, + response: Response, + }, + Abort, + } + ``` + + Make `IngressAdmissionOutcome::Admitted` carry `AdmittedIngress` plus completion, make + `PreparedIngress` own completion beside the router token/ingress proof. Define refusal and abort + explicitly at the validated boundary: + + ```rust + pub enum IngressAdmissionOutcome { + Admitted { + completion: ResponseEgressCompletion, + ingress: AdmittedIngress, + }, + Refused { + completion: ResponseEgressCompletion, + response: Response, + }, + Aborted, + } + ``` + + Add `Aborted` to the begin outcome. The default policy supplies + `ResponseEgressCompletion::empty()`. + +- [x] **Step 5: Retain completion across response replacement** + + Add `IngressDispatchOutcome::{Response, Aborted}`. Change `App::dispatch_ingress` to return it. + In `dispatch_admitted`, split the completion from `PreparedIngress` before awaiting router + dispatch, retain it while the router consumes `AdmittedIngress`, and move it into the resulting + envelope regardless of the selected response. Make refusal egress use detached policy/no-op + observer plus the refusal completion. This original phase used an empty completion for + pre-admission errors; the later detached-egress completion factory supersedes that behavior. + +- [x] **Step 6: Migrate all core admission constructors and response-inspection tests** + + Update core/router tests to provide explicit empty completions where no resource is under test. + Replace every envelope `into_response()` use with a begin-and-settle helper. Keep + `PreparedResponseEgress::into_response()` because the attempt already owns completion at that + point. + +- [x] **Step 7: Run all core tests** + + Run: + + ```bash + cargo test -p edgezero-core + ``` + + Expected: all core tests pass, including exact completion and abort counters. + +- [x] **Step 8: Commit the ingress ownership cut** + + ```bash + git add crates/edgezero-core/src/app.rs crates/edgezero-core/src/ingress.rs \ + crates/edgezero-core/src/router.rs crates/edgezero-core/src/lib.rs + git commit -m "feat(core): carry egress completion through ingress" + ``` + +### Task 3: Map abort to the Axum connection boundary + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/service.rs` +- Modify: `crates/edgezero-adapter-axum/src/connection.rs` +- Test: colocated tests in both files +- Test: `crates/edgezero-adapter-axum/tests/contract.rs` + +- [x] **Step 1: Write a red raw-socket abort test** + + Configure admission to return `Abort`, send a request with a body over a real loopback HTTP/1 + socket, and assert EOF/reset with zero response bytes. Counters must prove no body poll, + middleware/handler call, completion, or observer invocation. + +- [x] **Step 2: Run the focused Axum tests and verify the request currently becomes a response** + + Run: + + ```bash + cargo test -p edgezero-adapter-axum admission_abort -- --nocapture + ``` + + Expected: failure because the service has no abort error path. + +- [x] **Step 3: Implement a private Hyper service abort error** + + Change `dispatch_request` and `AxumConnectionService::call` to return + `Result, AxumIngressAbort>`. Map only + `IngressBeginOutcome::Aborted` to this error; all ordinary EdgeZero failures remain bounded HTTP + responses. Extend connection classification with `ConnectionExit::AdmissionAborted` by + downcasting the Hyper error chain, then drop the connection without constructing response head + or body. + +- [x] **Step 4: Migrate Axum tests away from envelope extraction** + + Update service/contract fixtures for mandatory completion fields and begin-and-settle any + response envelope they inspect. + +- [x] **Step 5: Run the Axum suite** + + Run: + + ```bash + cargo test -p edgezero-adapter-axum + ``` + + Expected: all Axum unit, contract, and raw-socket tests pass. + +- [x] **Step 6: Commit the Axum abort boundary** + + ```bash + git add crates/edgezero-adapter-axum/src/service.rs \ + crates/edgezero-adapter-axum/src/connection.rs \ + crates/edgezero-adapter-axum/tests/contract.rs + git commit -m "feat(axum): close connections on ingress abort" + ``` + +### Task 4: Map abort across Cloudflare, Fastly, and Spin + +**Files:** +- Modify: `crates/edgezero-adapter-cloudflare/src/request.rs` +- Modify: `crates/edgezero-adapter-fastly/src/request.rs` +- Modify: `crates/edgezero-adapter-spin/src/request.rs` +- Test: `crates/edgezero-adapter-cloudflare/tests/contract.rs` +- Test: `crates/edgezero-adapter-fastly/tests/contract.rs` +- Test: `crates/edgezero-adapter-spin/tests/contract.rs` + +- [x] **Step 1: Write red adapter abort contract tests** + + Reuse each adapter's injected source/delivery seam. For an aborting app, assert source + construction and delivery closures are never called, no envelope exists, and the adapter returns + a fixed category-only platform error. Keep Cloudflare/Fastly/Spin transport reset claims out of + local tests. + +- [x] **Step 2: Run the three focused suites and verify they fail** + + Run: + + ```bash + CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ + cargo test -p edgezero-adapter-cloudflare --no-default-features \ + --features cloudflare,test-utils --target wasm32-unknown-unknown --test contract admission_abort + CARGO_TARGET_WASM32_WASIP1_RUNNER="viceroy run" \ + cargo test -p edgezero-adapter-fastly --no-default-features \ + --features fastly,test-utils --target wasm32-wasip1 --test contract admission_abort + CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime run -W component-model-async=y -S p3=y -S http=y" \ + cargo test -p edgezero-adapter-spin --no-default-features \ + --features spin,test-utils --target wasm32-wasip2 --test contract admission_abort + ``` + + Expected: failures because unknown begin outcomes are currently rendered as detached internal + responses. + +- [x] **Step 3: Implement non-response abort propagation** + + Cloudflare and Spin return their fixed host error directly before `make_source` or `deliver`. + Fastly adds an abort branch to its private `DispatchOutcome` and returns a fixed + `fastly::Error`/top-level error before finalize or delivery. Remove wildcard branches that convert + the known abort variant into an internal HTTP response; retain a future-unknown fail-closed arm + only where `#[non_exhaustive]` requires it. + +- [x] **Step 4: Migrate contract fixtures to completion ownership** + + Add explicit empty completions to ordinary decisions and replace every + `ResponseEgressEnvelope::into_response()` call with begin-and-settle test helpers. Add one + response-producing test per adapter proving a refusal completion reaches terminal egress once. + +- [x] **Step 5: Run adapter host and target checks** + + Run: + + ```bash + CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ + scripts/run_test_nonzero.sh admission_abort cargo test -p edgezero-adapter-cloudflare \ + --no-default-features --features cloudflare,test-utils \ + --target wasm32-unknown-unknown --test contract + CARGO_TARGET_WASM32_WASIP1_RUNNER="viceroy run" \ + scripts/run_test_nonzero.sh admission_abort cargo test -p edgezero-adapter-fastly \ + --no-default-features --features fastly,test-utils --target wasm32-wasip1 --test contract + CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime run -W component-model-async=y -S p3=y -S http=y" \ + scripts/run_test_nonzero.sh admission_abort cargo test -p edgezero-adapter-spin \ + --no-default-features --features spin,test-utils --target wasm32-wasip2 --test contract + cargo check -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare + cargo check -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly + cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin + ``` + + Expected: all supported host tests and WASM checks pass. + +- [x] **Step 6: Commit provider abort propagation** + + ```bash + git add crates/edgezero-adapter-cloudflare crates/edgezero-adapter-fastly \ + crates/edgezero-adapter-spin + git commit -m "feat(adapters): propagate ingress abort without a response" + ``` + +### Task 5: Hard-cut Hooks and macro expansion to fallible assembly + +**Files:** +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-macros/src/app.rs` +- Test: `crates/edgezero-macros/tests/app_macro.rs` +- Modify: `crates/edgezero-adapter-axum/src/dev_server.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/lib.rs` +- Modify: `crates/edgezero-adapter-fastly/src/lib.rs` +- Modify: `crates/edgezero-adapter-spin/src/lib.rs` +- Test: adapter-local host-testable build seams plus target contract tests + +- [x] **Step 1: Write red core and macro propagation tests** + + Add a failing configure callback returning a typed `EdgeError`. Assert both hand-written Hooks + and macro-generated Hooks return the same error from `App::build::` and never expose a partially + configured `App`. + +- [x] **Step 2: Run core/macro tests and verify callback results are currently discarded** + + Run: + + ```bash + cargo test -p edgezero-core --lib app::tests::build_app + cargo test -p edgezero-macros --test app_macro + ``` + + Expected: compile failure because `configure` and application assembly are infallible. + +- [x] **Step 3: Implement the fallible trait and macro signatures** + + Hard-cut to: + + ```rust + impl App { + pub fn build(platform: PlatformMetadata) -> Result { + let mut app = Self::with_name_and_platform(A::routes(), A::name(), platform); + A::configure(&mut app)?; + Ok(app) + } + } + + pub trait Hooks { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Ok(()) + } + } + ``` + + Make `app!` emit the hook methods and return the configured callback result. Do not add an + overridable builder, callback coercion, or panic fallback. + +- [x] **Step 4: Write red adapter-boundary tests** + + Add one small adapter-local `build_app_for_dispatch()` seam used by the production entrypoint + before any platform request work. Use a `FailingHooks` implementation and a dispatch/bind/receive + closure counter to prove failure prevents the next boundary. Axum's seam is host-tested and + proves no listener bind. Cloudflare, Fastly, and Spin exercise their platform error mapping in + target contract tests. Cloudflare logs only `EdgeError::kind()` and returns exactly + `worker::Error::RustError("application configuration failed")`. + +- [x] **Step 5: Propagate build failure at each entrypoint** + + Axum and Spin retain the `EdgeError` source under fixed `anyhow` context. Cloudflare maps through + a private helper that logs stable kind only and emits the fixed public worker error. Fastly + changes its app runners and generated/demo main return type if necessary so the top-level error + retains the `EdgeError` source under fixed `"application configuration failed"` context. Build + before the boundary specified in the source specs. + +- [x] **Step 6: Run core, macro, and adapter entrypoint tests** + + Run: + + ```bash + cargo test -p edgezero-core + cargo test -p edgezero-macros + cargo test -p edgezero-adapter-axum failing_configuration + CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ + scripts/run_test_nonzero.sh failing_configuration cargo test \ + -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils \ + --target wasm32-unknown-unknown --test contract + CARGO_TARGET_WASM32_WASIP1_RUNNER="viceroy run" \ + scripts/run_test_nonzero.sh failing_configuration cargo test \ + -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils \ + --target wasm32-wasip1 --test contract + CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime run -W component-model-async=y -S p3=y -S http=y" \ + scripts/run_test_nonzero.sh failing_configuration cargo test \ + -p edgezero-adapter-spin --no-default-features --features spin,test-utils \ + --target wasm32-wasip2 --test contract + ``` + + Expected: all package tests pass with no infallible build call sites. + +- [x] **Step 7: Commit fallible application assembly** + + ```bash + git add crates/edgezero-core/src/app.rs crates/edgezero-macros \ + crates/edgezero-adapter-axum/src/dev_server.rs \ + crates/edgezero-adapter-cloudflare/src/lib.rs \ + crates/edgezero-adapter-fastly/src/lib.rs \ + crates/edgezero-adapter-spin/src/lib.rs + git commit -m "feat(app): make application assembly fallible" + ``` + +### Task 6: Align capabilities, demo, generator, templates, and guides + +**Files:** +- Modify: `crates/edgezero-core/src/manifest.rs` +- Modify: `crates/edgezero-adapter-{axum,cloudflare,fastly,spin}/src/cli.rs` +- Modify: `examples/app-demo/crates/app-demo-core/src/lib.rs` +- Modify: `examples/app-demo/crates/app-demo-adapter-fastly/src/main.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/lib.rs.hbs` +- Modify: `crates/edgezero-adapter-fastly/src/templates/src/main.rs.hbs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `docs/guide/handlers.md` +- Modify: `docs/guide/capabilities.md` +- Modify: adapter guides as required by changed entrypoint errors +- Modify: `scripts/check_outbound_docs_contract.mjs` +- Modify: `scripts/check_outbound_legacy_api.sh` +- Test: colocated manifest, CLI capability, generator, demo lifecycle, and docs-contract tests + +- [x] **Step 1: Write red capability and generator assertions** + + Add `Capability::IngressAdmissionAbort` with the string `ingress-admission-abort`. Assert Axum is + `Native`; Cloudflare, Fastly, and Spin are `BestEffort`. Add generator assertions for fallible + `configure_app`, explicit empty completions, fallible `build_app`, and the Fastly return type. + +- [x] **Step 2: Run the focused tests and verify stale artifacts fail** + + Run: + + ```bash + cargo test -p edgezero-core manifest::tests + cargo test -p edgezero-cli generator + node scripts/check_outbound_docs_contract.mjs + ``` + + Expected: failures until capability matrices, generated code, and docs are aligned. + +- [x] **Step 3: Migrate the demo and core template** + + Make `configure_app` return `Result<(), EdgeError>`, add `Ok(())`, and give every decision an + explicit `ResponseEgressCompletion`. Demonstrate a response-owned permit/drop probe without a + global response-id map. Update every demo/template `App::build::` call to handle `Result`. + +- [x] **Step 4: Migrate adapter templates and generated fixtures** + + Update Fastly's template/demo main if its runner now returns `anyhow::Result<()>`; update other + adapter templates only where signatures changed. Regenerate or update snapshots/assertions and + prove a newly generated project contains no removed API. + +- [x] **Step 5: Publish and enforce the capability contract** + + Add the abort capability row to the public guide and checker. Document that Axum's owned HTTP/1 + close is Native while the provider adapters remain BestEffort pending deployed evidence. Update + handler/configuration examples for `Result` and direct completion ownership. Extend the legacy + scan to reject infallible configure/build signatures, `ResponseEgressEnvelope::into_response`, + and any completion-in-response-extension carrier. + +- [x] **Step 6: Run demo, generator, docs, and capability suites** + + Run: + + ```bash + cargo test -p edgezero-cli + scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline \ + --locked -p edgezero-cli --test generated_project_builds + cargo test --manifest-path examples/app-demo/Cargo.toml -p app-demo-core + cargo check --manifest-path examples/app-demo/Cargo.toml --workspace + npm --prefix docs run format + npm --prefix docs run lint + node scripts/check_outbound_docs_contract.mjs + bash scripts/check_outbound_legacy_api.sh + ``` + + Expected: all generated/demo code and documentation checks pass. + +- [x] **Step 7: Commit the consumer migration** + + ```bash + git add crates/edgezero-core/src/manifest.rs crates/edgezero-adapter-*/src/cli.rs \ + crates/edgezero-cli examples/app-demo docs/guide scripts + git commit -m "docs: align consumers with ingress lifecycle hard cut" + ``` + +### Task 7: Comprehensive verification, self-review, and PR update + +**Files:** +- Review: all files changed by Tasks 1-6 +- Modify if needed: PR #275 description + +- [x] **Step 1: Audit for stale and bypassing APIs** + + Run: + + ```bash + rg -n "fn configure\([^)]*&mut App[^)]*\) \{|fn build_app\(\) -> App|ResponseEgressEnvelope.*into_response|AdmissionDecision::(Admit|Refuse|ReadBodyBeforeFallback)" crates examples docs/guide + rg -n "ResponseEgressCompletion" crates examples docs/guide + ``` + + Inspect every result. The only response-extension lifecycle value may be + `ResponseEgressDeadline`; completion must follow direct ownership. + +- [x] **Step 2: Run formatting and full native CI gates** + + Run: + + ```bash + cargo fmt --all -- --check + cargo clippy --workspace --all-targets --all-features -- -D warnings + cargo test --workspace --all-targets + cargo check --workspace --all-targets --features "fastly cloudflare spin" + ``` + + Expected: zero warnings and zero failures. + +- [x] **Step 3: Run all WASM and excluded-workspace gates** + + Run: + + ```bash + cargo check -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly + cargo check -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare + cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin + cargo test --manifest-path examples/app-demo/Cargo.toml --workspace + cargo check --manifest-path examples/app-demo/Cargo.toml --workspace + ``` + + Expected: all target and demo gates pass. + +- [x] **Step 4: Re-run docs and repository contract gates** + + Run: + + ```bash + npm --prefix docs run format + npm --prefix docs run lint + node scripts/check_outbound_docs_contract.mjs + bash scripts/check_outbound_legacy_api.sh + git diff --check + ``` + + Expected: all commands exit zero. + +- [x] **Step 5: Perform a final code-review pass** + + Review ownership/drop paths, callback panic ordering, abort zero-read behavior, fixed public error + messages, adapter capability claims, generated code, and every `App::build::` call. Remove dead + branches and imports introduced by the hard cut; do not retain compatibility aliases. + +- [x] **Step 6: Commit any verification fixes and push PR #275** + + ```bash + git add -A + git commit -m "test: close lifecycle hard-cut coverage" + git push origin docs/outbound-http-spec + ``` + + Skip the final commit only if verification produced no changes. Update PR #275's description to + call out direct response completion ownership, non-response admission abort, fallible app + assembly, capability evidence, and the intentionally breaking migration. diff --git a/docs/superpowers/plans/2026-09-19-response-egress-composition-detached-deadline.md b/docs/superpowers/plans/2026-09-19-response-egress-composition-detached-deadline.md new file mode 100644 index 000000000..ab00c308f --- /dev/null +++ b/docs/superpowers/plans/2026-09-19-response-egress-composition-detached-deadline.md @@ -0,0 +1,327 @@ +# Response Egress Composition and Detached Deadline Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Allow multiple application-owned response resources to share one terminal event and require every detached normalized-error response to carry an absolute application deadline. + +**Architecture:** `ResponseEgressCompletion::join` composes two non-clone completion owners while preserving deterministic callback order and independent panic isolation on unwind-capable targets; panic-abort targets may terminate before the second callback. `DetachedResponseEgressDecision::Send` becomes a named-field variant containing both the completion and a mandatory `Deadline`; `App` inserts that deadline into the existing `ResponseEgressDeadline` extension path before constructing detached egress. The change is a hard cut across core, adapters, the reference app, generated projects, specifications, and contract guards. + +**Tech Stack:** Rust 2024, `edgezero-core`, Axum, Cloudflare Workers, Fastly Compute, Fermyon Spin, Cargo test/Clippy, shell and Node.js contract checks, VitePress documentation. + +--- + +### Task 1: Pin the application-neutral contracts + +**Files:** +- Modify: `docs/superpowers/specs/2026-08-22-inbound-body-design.md` +- Modify: `docs/superpowers/specs/2026-09-08-response-egress-design.md` +- Modify: `docs/guide/handlers.md` +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` + +- [x] **Step 1: Specify completion composition** + + Add `ResponseEgressCompletion::join(self, other) -> Self`. Define left-to-right callback order, the same borrowed terminal report for both callbacks, independent panic isolation on unwind-capable targets, at-most-once execution, and release of both captured resources when the joined completion is abandoned. State explicitly that panic-abort targets may terminate before the second callback runs. + +- [x] **Step 2: Specify the hard-cut detached decision** + + Replace the tuple variant with: + + ```rust + #[non_exhaustive] + pub enum DetachedResponseEgressDecision { + Abort, + Send { + completion: ResponseEgressCompletion, + deadline: Deadline, + }, + } + ``` + + State that the deadline is absolute in the request's injected monotonic-clock domain, is mandatory for every normalized-error `Send` decision, and cannot be extended by the default response-egress policy. Admission-selected detached responses retain their existing response-owned deadline path and are outside this factory contract. `Abort` continues to create no response, attempt, completion callback, observer call, handler call, middleware call, or body poll. + +- [x] **Step 3: Specify relative deadline construction and default behavior** + + Add `DetachedResponseEgressHead::write_deadline_after(Duration) -> Deadline`, clamped to `DEADLINE_FAR_FUTURE` and failing closed to `request_start` on arithmetic overflow. The default decision sends with an empty completion and `DEFAULT_RESPONSE_WRITE_BUDGET` measured from captured request start. + +- [x] **Step 4: Correct public staging terminology** + + Replace the two `deploy-staged` phrases in the outbound specification with `deploy --staging`. Retain “staged version” only where it describes Fastly's provider lifecycle; the public EdgeZero action and flag remain `DeployStaging` and `--staging`. + +- [x] **Step 5: Run documentation formatting** + + Run: `cd docs && npm run format -- --check` + + Expected: PASS. + +### Task 2: Add completion composition test-first + +**Files:** +- Modify: `crates/edgezero-core/src/response_egress.rs` + +- [x] **Step 1: Write failing composition tests** + + Add focused tests proving: + + - joined callbacks both receive the same report and run once in left-to-right order; + - on unwind-capable test targets, a panic in the left callback does not suppress the right callback; + - dropping a joined completion before terminal egress releases both captured resources exactly once; + - calling the enclosing attempt's terminal path more than once cannot rerun either callback. + +- [x] **Step 2: Run the focused tests and observe RED** + + Run: `cargo test -p edgezero-core response_egress_completion_join` + + Expected: FAIL because `ResponseEgressCompletion::join` does not exist. + +- [x] **Step 3: Implement the minimal composition primitive** + + Add complete public rustdoc covering deterministic order, same-report delivery, abandonment, and the unwind-versus-panic-abort qualification. Implement an `#[inline]` consuming method equivalent to: + + ```rust + #[must_use] + #[inline] + pub fn join(mut self, mut other: Self) -> Self { + Self::new(move |report| { + self.complete(report); + other.complete(report); + }) + } + ``` + + Reuse `complete` for each child so each callback retains its own panic boundary on unwind-capable targets. Do not add cloneability, shared mutable callback registries, or a compatibility wrapper. + +- [x] **Step 4: Run focused and core tests** + + Run: `cargo test -p edgezero-core response_egress_completion_join` + + Run: `cargo test -p edgezero-core` + + Expected: PASS. + +### Task 3: Require detached response deadlines test-first + +**Files:** +- Modify: `crates/edgezero-core/src/response_egress.rs` +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-core/src/lib.rs` only if public re-exports require adjustment + +- [x] **Step 1: Write failing head/deadline tests** + + Add tests proving `write_deadline_after` is anchored to `request_start`, clamps durations to `DEADLINE_FAR_FUTURE`, and fails closed to `request_start` on overflow. + +- [x] **Step 2: Write failing detached-send tests** + + Add tests proving: + + - `Send { completion, deadline }` exposes the selected deadline to `ResponseEgressEnvelope::begin`; + - the resulting write policy uses the earlier of the detached application deadline and the default response policy; + - an already-expired detached deadline remains expired; + - `Abort` still creates no response and invokes no completion; + - the default factory selects a finite deadline from captured request start. + +- [x] **Step 3: Run focused tests and observe RED** + + Run: `cargo test -p edgezero-core detached_ingress_error` + + Run: `cargo test -p edgezero-core detached_response_egress_head` + + Expected: FAIL on the missing named fields/helper/deadline propagation. + +- [x] **Step 4: Implement the hard-cut decision and helper** + + Change `Send(ResponseEgressCompletion)` to the named-field variant. Implement `write_deadline_after` with `#[must_use]`, `#[inline]`, and public rustdoc covering request-start anchoring, `DEADLINE_FAR_FUTURE` clamping, and fail-closed overflow, using the same bounded arithmetic pattern as `IngressHead::read_deadline_after`. + +- [x] **Step 5: Thread the deadline through detached egress** + + In `App::detached_ingress_error_egress`, render the bounded response, insert `ResponseEgressDeadline::new(deadline)` into its extensions, and build the existing detached envelope with the supplied completion. Keep the default no-op observer and default policy; `ResponseEgressEnvelope::begin` already clamps the policy to the application deadline. + +- [x] **Step 6: Update the default decision factory** + + Return `Send { completion: ResponseEgressCompletion::empty(), deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET) }`. + +- [x] **Step 7: Run focused and core tests** + + Run: `cargo test -p edgezero-core detached_ingress_error` + + Run: `cargo test -p edgezero-core response_egress` + + Run: `cargo test -p edgezero-core` + + Expected: PASS. + +### Task 4: Migrate adapters and prove detached behavior + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/service.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/contract.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` + +- [x] **Step 1: Migrate all factories to named fields** + + Every `Send`-producing test factory returns `Send { completion, deadline: head.write_deadline_after(...) }`. Retain explicit `Abort` coverage. Keep adapter production dispatch unchanged because it already receives the fully prepared `ResponseEgressEnvelope`. + +- [x] **Step 2: Extend the native deadline proof** + + Add an Axum contract assertion showing an expired detached deadline reaches the response-egress conversion boundary as expired and still preserves exactly-once completion. Keep the existing raw-socket zero-byte abort proof. + +- [x] **Step 3: Run focused adapter tests** + + Run: + + ```bash + scripts/run_test_nonzero.sh normalized_ingress_error_returns_typed_response_without_application_observation cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils + scripts/run_test_nonzero.sh admission_policy_error_decision_send_uses_detached_completion cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils + cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --test contract --no-run + cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --test contract --no-run + cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test contract --no-run + ``` + + Expected: the two native tests execute and pass; each provider contract test target compiles. Provider execution occurs with the pinned runners in Task 6. + +- [x] **Step 4: Run adapter crate tests** + + Run: `cargo test -p edgezero-adapter-axum --all-targets` + + Run: `cargo test -p edgezero-adapter-cloudflare --all-targets --features test-utils` + + Run: `cargo test -p edgezero-adapter-fastly --all-targets --features "cli test-utils"` + + Run: `cargo test -p edgezero-adapter-spin --all-targets --features test-utils` + + Expected: PASS. + +### Task 5: Align demo, templates, generator, and guards + +**Files:** +- Modify: `examples/app-demo/crates/app-demo-core/src/lib.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/lib.rs.hbs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `scripts/check_outbound_legacy_api.sh` +- Modify: `scripts/check_outbound_docs_contract.mjs` + +- [x] **Step 1: Write failing generator, demo, and guard assertions** + + Require generated source to contain the named `completion` and `deadline` fields, the exact `head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET)` expression, and `.join(`. Add a demo lifecycle test proving both composed completion effects occur exactly once. Extend the legacy checker to reject `DetachedResponseEgressDecision::Send(`, `DeployStaged`, `deploy_staged`, `deploy-staged`, and `--staged` in active public/code surfaces while excluding historical plans and legitimate Fastly provider-state prose. + +- [x] **Step 2: Run the focused checks and observe RED** + + Run: `cargo test -p edgezero-cli generate_new_scaffolds_workspace_layout` + + Run: `cd examples/app-demo && cargo test composed_response_completion` + + Run: `bash scripts/check_outbound_legacy_api.sh` + + Expected: FAIL until the template, demo, and stale specification text are migrated. + +- [x] **Step 3: Migrate the reference app and template** + + Return `Send { completion, deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET) }`. Add a concise example composing the static completion with a late-bound completion using `join` without introducing application-specific terminology, and make the new demo lifecycle test pass. + +- [x] **Step 4: Strengthen the documentation contract checker** + + Require the named detached decision fields, mandatory deadline semantics, composition semantics, unchanged abort behavior, and the existing provider capability caveats. + +- [x] **Step 5: Run demo, generator, and contract checks** + + Run: `cargo test -p edgezero-cli generate_new_scaffolds_workspace_layout` + + Run: `cd examples/app-demo && cargo test --workspace --all-targets` + + Run: `bash scripts/check_outbound_legacy_api.sh` + + Run: `node scripts/check_outbound_docs_contract.mjs` + + Expected: PASS. + +### Task 6: Complete cross-target verification and publish + +**Files:** +- Modify only files required by failures caused by this hard cut. + +- [x] **Step 1: Run native workspace gates** + + Run: `cargo fmt --all -- --check` + + Run: `cargo test --workspace --all-targets` + + Run: `cargo clippy --workspace --all-targets --all-features -- -D warnings` + + Run: `cargo check --workspace --all-targets --features "fastly cloudflare spin"` + + Run: + + ```bash + bash scripts/check_adapter_feature_matrix.sh axum native + bash scripts/check_adapter_feature_matrix.sh cloudflare native + bash scripts/check_adapter_feature_matrix.sh fastly native + bash scripts/check_adapter_feature_matrix.sh spin native + ``` + + Expected: PASS. + +- [x] **Step 2: Run strict WASM Clippy and feature-matrix gates** + + Run: + + ```bash + cargo clippy -p edgezero-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-cloudflare --features "cloudflare test-utils" --target wasm32-unknown-unknown --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-fastly --features "fastly cli" --target wasm32-wasip1 --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-fastly --features "fastly test-utils" --target wasm32-wasip1 --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-fastly --features "fastly cli test-utils" --target wasm32-wasip1 --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-spin --features spin --target wasm32-wasip2 --all-targets -- -D warnings + cargo clippy -p edgezero-adapter-spin --features "spin test-utils" --target wasm32-wasip2 --all-targets -- -D warnings + bash scripts/check_adapter_feature_matrix.sh cloudflare wasm32-unknown-unknown + bash scripts/check_adapter_feature_matrix.sh fastly wasm32-wasip1 + bash scripts/check_adapter_feature_matrix.sh spin wasm32-wasip2 + ``` + + Expected: PASS. + +- [x] **Step 3: Run runner-backed WASM tests** + + With the pinned runners installed, run: + + ```bash + CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh dispatch_runs_router_and_returns_response cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --test contract + CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh dispatch_runs_router_and_returns_response cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --test contract + CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh closed_lifecycle_orders_hooks_and_returns_state_after_delivery cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --test contract + CARGO_TARGET_WASM32_WASIP2_RUNNER='wasmtime run -W component-model-async=y -S p3=y -S http=y' scripts/run_test_nonzero.sh router_dispatches_get_and_returns_response cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test contract + CARGO_TARGET_WASM32_WASIP2_RUNNER='wasmtime run -W component-model-async=y -S p3=y -S http=y' scripts/run_test_nonzero.sh spin_error_code_table_is_exhaustive cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources + CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh streamed_upload_eof_yields_before_final_precedence cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --lib + (cd crates/edgezero-adapter-fastly && CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' ../../scripts/run_test_nonzero.sh backend_creation_error_table_is_exhaustive cargo test --offline --locked --no-default-features --features fastly,test-utils --lib) + ``` + + Expected: PASS. If a pinned runner is unavailable locally, the corresponding hosted CI job must pass on the exact pushed head before completion. + +- [x] **Step 4: Run docs, demo, and generated-project gates** + + Run: + + ```bash + RUSTDOCFLAGS='-D warnings' cargo doc --offline --locked --workspace --all-features --no-deps + scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds + bash scripts/check_outbound_legacy_api.sh + node scripts/check_outbound_docs_contract.mjs + (cd docs && npm run lint && npm run format && npm run build) + (cd examples/app-demo && cargo fmt --all -- --check) + (cd examples/app-demo && cargo test --locked --workspace --all-targets) + (cd examples/app-demo && cargo clippy --workspace --all-targets --all-features -- -D warnings) + (cd examples/app-demo && cargo check --locked -p app-demo-adapter-cloudflare --target wasm32-unknown-unknown --no-default-features --features cloudflare) + (cd examples/app-demo && cargo check --locked -p app-demo-adapter-fastly --target wasm32-wasip1 --no-default-features --features fastly) + (cd examples/app-demo && cargo check --locked -p app-demo-adapter-spin --target wasm32-wasip2 --no-default-features --features spin) + ``` + + Expected: PASS. + +- [x] **Step 5: Review the complete diff** + + Verify no tuple `Send` constructor, compatibility shim, legacy public staging identifier, application-specific terminology, unrelated refactor, or unbounded detached response remains. + +- [x] **Step 6: Commit and push** + + Commit with an application-neutral message, push to the existing PR #275 branch, and wait for the hosted check matrix on the exact pushed head. + + Expected: every required check passes; the PR remains mergeable, subject only to repository review policy. diff --git a/docs/superpowers/plans/2026-09-26-platform-memory-ceiling.md b/docs/superpowers/plans/2026-09-26-platform-memory-ceiling.md new file mode 100644 index 000000000..d72a2c384 --- /dev/null +++ b/docs/superpowers/plans/2026-09-26-platform-memory-ceiling.md @@ -0,0 +1,113 @@ +# Platform Memory Ceiling Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +> **Historical status:** The original memory-ceiling work was completed on 2026-09-26. +> +> **Retrospective revision (2026-09-29):** The plan was superseded by the +> [platform resource metadata design](../specs/2026-09-28-platform-resource-metadata-design.md), +> and its API references were updated to the current `PlatformFact` and `PlatformMetadata` +> contract so they cannot be mistaken for migration guidance. The checked items below are a +> retrospective final-state map of the original task areas, not an execution log and not a claim +> that those exact types or tests existed on 2026-09-26. Repository history remains the record of +> the original implementation. See the +> [Unreleased changelog](../../../CHANGELOG.md#platform-resource-metadata) for the hard-cut +> migration table. + +**Retrospective final-state goal:** Expose target memory ceilings, accounting scope, and provenance through both CLI adapter metadata and runtime application configuration. + +**Retrospective final-state architecture:** `edgezero-core` owns an extensible platform descriptor containing explicit known +or unknown memory, inbound population, and host-ingress accounting facts. Runtime adapters pass +that descriptor into application construction before the configuration callback runs, while CLI +adapter implementations delegate to the same target constants. Generic Spin remains unknown unless +an entrypoint explicitly selects a hosted or runtime-configured descriptor. + +**Tech Stack:** Rust, EdgeZero adapter registry, proc macros, Cargo tests, Markdown contract checks. + +--- + +### Task 1: Core platform metadata + +**Files:** +- Create: `crates/edgezero-core/src/platform.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-macros/src/app.rs` + +- [x] Add failing core tests for `MemoryCeiling`, fact-level provenance, `PlatformMetadata`, and visibility inside `Hooks::configure`. +- [x] Run the focused core tests and confirm the missing API failures. +- [x] Add non-exhaustive platform metadata types with const constructors and accessors; provenance is carried by `PlatformFact::Known`; immediately run the focused core tests until green. +- [x] Add `App::platform()` and hard-cut core-owned `App::build::(platform)` as the single construction path; immediately run the focused app tests until green. +- [x] Update macro-generated `Hooks` implementations so platform metadata is installed before `configure`; immediately run macro and generated-code tests until green. + +### Task 2: Canonical adapter metadata + +**Files:** +- Modify: `crates/edgezero-adapter/src/registry.rs` +- Modify: `crates/edgezero-adapter-axum/src/lib.rs` +- Modify: `crates/edgezero-adapter-axum/src/cli.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/lib.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/cli.rs` +- Modify: `crates/edgezero-adapter-fastly/src/lib.rs` +- Modify: `crates/edgezero-adapter-fastly/src/cli.rs` +- Modify: `crates/edgezero-adapter-spin/src/lib.rs` +- Modify: `crates/edgezero-adapter-spin/src/cli.rs` + +- [x] Add a failing registry test proving the inherited CLI default contains three explicit unknown facts; run it and confirm the missing API failure. +- [x] Add failing Axum tests proving CLI and runtime memory, population, and host-accounting facts are unknown with the operator-configured reason; run them and confirm failure before implementation. +- [x] Add failing Cloudflare tests proving `128_000_000` primary bytes, no separate stack, per-instance scope, platform-limit provenance, and provider-unpublished population/accounting facts; run them and confirm failure before implementation. +- [x] Add failing Fastly tests proving `128_000_000` primary bytes, a separate `1_000_000`-byte stack, per-execution scope, a population of one with platform-limit provenance, and provider-unpublished host accounting; run them and confirm failure before implementation. +- [x] Add failing Spin tests proving generic CLI/runtime facts are runtime-configured unknowns and the explicit Akamai profile is `128 * 1024 * 1024` bytes per execution with a population of one and hosted-default provenance; prove the explicit descriptor is visible inside `configure`. +- [x] Add default `Adapter::platform_metadata()` returning fully unknown metadata; run the registry test until green. +- [x] Add Axum's canonical unknown metadata and CLI override; run Axum tests until green. +- [x] Add Cloudflare's canonical platform constant and CLI override; run Cloudflare tests until green. +- [x] Add Fastly's canonical platform constant and CLI override; run Fastly tests until green. +- [x] Add Spin's canonical unknown metadata, CLI override, explicit runtime entrypoint, and Akamai-hosted descriptor; run Spin tests until green. + +### Task 3: Runtime propagation + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/dev_server.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/lib.rs` +- Modify: `crates/edgezero-adapter-fastly/src/lib.rs` +- Modify: `crates/edgezero-adapter-spin/src/lib.rs` +- Test: adapter contract/unit test modules adjacent to these entrypoints + +- [x] Add a failing Axum runtime test, require explicit platform metadata during construction, and run Axum tests until green. +- [x] Add a failing Cloudflare runtime test, require explicit platform metadata during construction, and run Cloudflare tests until green. +- [x] Add a failing Fastly runtime test, require explicit platform metadata during construction, and run Fastly tests until green. +- [x] Add failing generic and explicit-profile Spin runtime tests, require explicit platform metadata during construction, and run Spin tests until green. + +### Task 4: Documentation and drift checks + +**Files:** +- Modify: `docs/guide/capabilities.md` +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `scripts/check_outbound_docs_contract.mjs` +- Modify: adapter/CLI integration tests as needed + +- [x] Add a platform-resource table that distinguishes exact bytes/units, separate stack, scope, population, host-ingress accounting, provenance, and unknown reasons. +- [x] Document that generic Spin is runtime-configured and that the Akamai hosted profile is explicit. +- [x] Add checker assertions for the table and a Rust integration test that derives every documented resource cell from the canonical constants. +- [x] Run the docs contract and focused registry tests. + +### Task 5: Full verification and delivery + +**Files:** +- Verify: workspace and example workspaces + +- [x] Run `cargo fmt --all -- --check`. +- [x] Run `cargo clippy --workspace --all-targets --all-features -- -D warnings`. +- [x] Run `cargo test --workspace --all-targets`. +- [x] Run `cargo check --workspace --all-targets --features "fastly cloudflare spin"`. +- [x] Run `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin`. +- [x] Run `scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds`. +- [x] Run `cargo test --locked --workspace --all-targets` from `examples/app-demo`. +- [x] From `examples/app-demo`, run `cargo check --locked -p app-demo-adapter-cloudflare --target wasm32-unknown-unknown --no-default-features --features cloudflare`. +- [x] From `examples/app-demo`, run `cargo check --locked -p app-demo-adapter-fastly --target wasm32-wasip1 --no-default-features --features fastly`. +- [x] From `examples/app-demo`, run `cargo check --locked -p app-demo-adapter-spin --target wasm32-wasip2 --no-default-features --features spin`. +- [x] Run `bash scripts/check_adapter_feature_matrix.sh native` for Axum, Cloudflare, Fastly, and Spin. +- [x] Run `bash scripts/check_adapter_feature_matrix.sh cloudflare wasm32-unknown-unknown`, `bash scripts/check_adapter_feature_matrix.sh fastly wasm32-wasip1`, and `bash scripts/check_adapter_feature_matrix.sh spin wasm32-wasip2`. +- [x] From `docs`, run `npm run lint`, `npm run format`, and `npm run build`. +- [x] Review the diff for unrelated changes and forbidden downstream-specific wording. +- [x] Commit and push PR 275. diff --git a/docs/superpowers/plans/2026-09-28-platform-resource-metadata.md b/docs/superpowers/plans/2026-09-28-platform-resource-metadata.md new file mode 100644 index 000000000..a02d96870 --- /dev/null +++ b/docs/superpowers/plans/2026-09-28-platform-resource-metadata.md @@ -0,0 +1,137 @@ +# Platform Resource Metadata Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Publish typed platform memory facts and provide fail-closed, scope-aware memory-envelope validation while clarifying outbound batch observation cutoffs. + +**Architecture:** `edgezero-core` owns typed known/unknown resource facts, canonical memory-envelope arithmetic, and validation outcomes. Runtime and CLI adapters delegate to one target constant; application admission remains application-owned. Documentation and executable drift tests keep provider claims, demo code, and generated templates aligned. + +**Tech Stack:** Rust 1.95, Cargo workspace tests, WASM adapter builds, Markdown/VitePress, Node.js contract scripts. + +--- + +### Task 1: Core resource facts + +**Files:** +- Modify: `crates/edgezero-core/src/platform.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Modify: `crates/edgezero-core/src/app.rs` +- Modify: `crates/edgezero-macros/tests/app_macro.rs` + +- [x] Add `platform_fact_distinguishes_known_and_unknown_metadata` and `memory_envelope_accessors_preserve_declared_shape` in `platform.rs`. Use the desired public constructors and accessors so the tests fail to compile while the API is absent. +- [x] Run `cargo test --offline --locked -p edgezero-core platform_fact_distinguishes_known_and_unknown_metadata` and confirm the missing-type failure. +- [x] Add `PlatformFact`, `PlatformResourceSource`, `PlatformUnknownReason`, `InboundRequestPopulationBound`, `HostIngressMemoryAccounting`, and scope-tagged `MemoryEnvelope` with const constructors/accessors. Hard-cut `MemoryCeiling::new` to remove provenance and move it into `PlatformFact::Known`. +- [x] Run the two focused tests and `cargo test --offline --locked -p edgezero-core platform` until green. +- [x] Update app and macro tests to construct three explicit platform facts; run `cargo test --offline --locked -p edgezero-core app::tests` and `cargo test --offline --locked -p edgezero-macros --test app_macro` until green. + +### Task 2: Fail-closed memory validation + +**Files:** +- Modify: `crates/edgezero-core/src/platform.rs` +- Modify: `crates/edgezero-core/src/lib.rs` + +- [x] Add failing tests named `validation_fits_per_execution_with_counted_host_memory`, `validation_fits_per_instance_with_host_memory_outside_ceiling`, `validation_reports_primary_or_stack_excess`, `validation_unknown_ceiling_is_indeterminate`, `validation_unknown_population_is_indeterminate`, `validation_unknown_host_accounting_is_indeterminate`, `validation_missing_published_stack_requirement_is_indeterminate`, `validation_known_minimum_excess_precedes_unknown_facts`, `validation_rejects_scope_mismatch`, `validation_rejects_primary_arithmetic_overflow`, `validation_rejects_unknown_population_lower_bound_overflow`, `validation_rejects_stack_shape_mismatch`, and `validation_rejects_non_unit_per_execution_population`. +- [x] Make zero population unrepresentable by requiring `NonZeroU32` in `InboundRequestPopulationBound::new`; test the constructor signature and round-trip a value of one rather than adding a runtime zero branch. +- [x] Run `cargo test --offline --locked -p edgezero-core platform::tests::validation_` and confirm failure because validation types and methods are absent. +- [x] Implement `MemoryEnvelopeValidation::{Fits, Exceeds, Indeterminate}`, typed indeterminate reasons, `MemoryEnvelopeValidationError`, and `PlatformMetadata::validate_memory_envelope`. +- [x] For unknown per-instance population, compute `fixed + per_request * 1` with checked arithmetic only as a lower-bound overflow proof; otherwise return `Indeterminate`. Check a separate stack allowance independently and require an explicit stack requirement before returning `Fits`. +- [x] Run `cargo test --offline --locked -p edgezero-core platform::tests::validation_` and `cargo test --offline --locked -p edgezero-core` until green. + +### Task 3: Canonical adapter metadata hard cut + +**Files:** +- Modify: `crates/edgezero-adapter/src/registry.rs` +- Modify: `crates/edgezero-adapter-axum/src/lib.rs` +- Modify: `crates/edgezero-adapter-axum/src/cli.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/lib.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/cli.rs` +- Modify: `crates/edgezero-adapter-fastly/src/lib.rs` +- Modify: `crates/edgezero-adapter-fastly/src/cli.rs` +- Modify: `crates/edgezero-adapter-spin/src/lib.rs` +- Modify: `crates/edgezero-adapter-spin/src/cli.rs` + +- [x] Add `adapter_platform_metadata_default_is_unknown` in the registry; add `adapter_platform_metadata_matches_runtime_metadata` in Axum, Cloudflare, and Fastly; add `adapter_platform_metadata_distinguishes_generic_and_hosted_profiles` in Spin. Assert every fact and reason, including one request per execution for Fastly and the hosted Spin profile, and provider-unpublished Cloudflare population/framing facts. +- [x] Run `cargo test --offline --locked -p edgezero-adapter adapter_platform_metadata_default_is_unknown` and confirm it fails against the old trait. Run each adapter test with `scripts/run_test_nonzero.sh cargo test --offline --locked -p edgezero-adapter- --no-default-features --features cli --lib ` and confirm it fails against the retired memory-only adapter metadata method or the old constants. +- [x] Replace the memory-only adapter metadata method with `platform_metadata()` and no compatibility alias. Update each in-tree CLI adapter to return its runtime constant. +- [x] Hard-cut every adapter `PlatformMetadata::new` and `MemoryCeiling::new` caller to the three-fact model. +- [x] Run `cargo test --offline --locked -p edgezero-adapter`, then each adapter's platform metadata tests, until green. + +### Task 4: Batch observation cutoff contract + +**Files:** +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-core/src/context.rs` +- Modify: `crates/edgezero-core/src/time.rs` +- Modify: `crates/edgezero-adapter-axum/src/outbound.rs` +- Modify: `crates/edgezero-adapter-axum/tests/contract.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/outbound.rs` +- Modify: `crates/edgezero-adapter-cloudflare/tests/contract.rs` +- Modify: `crates/edgezero-adapter-fastly/src/outbound.rs` +- Modify: `crates/edgezero-adapter-fastly/tests/contract.rs` +- Modify: `crates/edgezero-adapter-spin/src/outbound.rs` +- Modify: `crates/edgezero-adapter-spin/tests/contract.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs` +- Modify: `examples/app-demo/crates/app-demo-core/src/handlers.rs` +- Modify: `docs/guide/proxying.md` +- Modify: `scripts/check_outbound_docs_contract.mjs` + +- [x] Run the existing early-observation-cutoff adapter contract as a passing characterization test; do not claim it is a TDD red test. +- [x] Add a failing docs/source contract named `observationCutoffContract` that requires the public Rustdoc warning and rejects stale public/example parameter names that use bare `cutoff`. +- [x] Run `node scripts/check_outbound_docs_contract.mjs` and confirm the new contract fails for missing wording. +- [x] Rename parameters and example variables to `observation_cutoff` across the exact files above. Keep `start_batch_until`, `send_all_until`, and `BudgetSource::BatchCutoff`; add no debug assertion. +- [x] Run the docs contract, core outbound tests, and all four focused adapter contract tests until green. + +### Task 5: Documentation, migration, demo, and generated-project drift + +**Files:** +- Create: `CHANGELOG.md` +- Modify: `docs/guide/adapters/overview.md` +- Modify: `docs/guide/capabilities.md` +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `docs/superpowers/plans/2026-09-26-platform-memory-ceiling.md` +- Modify: `scripts/check_outbound_docs_contract.mjs` +- Modify: `scripts/check_outbound_legacy_api.sh` +- Modify: `crates/edgezero-cli/tests/platform_memory_docs.rs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `crates/edgezero-cli/src/templates/core/src/lib.rs.hbs` +- Modify: `examples/app-demo/crates/app-demo-core/src/lib.rs` + +- [x] Rewrite `platform_memory_docs.rs` first to derive every documented memory, population, accounting, scope, and provenance cell from adapter constants. Run `cargo test --offline --locked -p edgezero-cli --test platform_memory_docs` and confirm it fails against the old table. +- [x] Extend the legacy checker to reject the retired memory provenance type, memory-only adapter metadata method, former one-argument platform-metadata constructor, former four-argument memory-ceiling constructor, compatibility aliases, and stale batch parameter/example wording. Explicitly allowlist only `CHANGELOG.md` and the focused platform-resource design, and add checker self-tests proving those forms still fail in every other active surface. Run it and confirm failure before migration. +- [x] Update the capability guide, normative outbound spec, completed platform-memory plan, demo, and generator template. State that unknown provider facts prevent complete validation and that the outbound six-connection limit is not inbound population evidence. +- [x] Add the exact old-to-new migration table from the focused design to `CHANGELOG.md`; make the adapter overview link to that table without repeating legacy signatures. +- [x] Run `cargo test --offline --locked -p edgezero-cli --test platform_memory_docs`, generator unit tests, `bash scripts/check_outbound_legacy_api.sh`, and `node scripts/check_outbound_docs_contract.mjs` until green. +- [x] Run the Task 5 stale-API scan for the retired provenance type, memory-only trait method, optional-return accessor, and hidden clock-pairing constructor; confirm no live stale API remains outside the two allowlisted migration records. + +### Task 6: Review, verification, and delivery + +**Files:** +- Review: all modified files + +- [x] Run `cargo fmt --all -- --check`. +- [x] Run `cargo clippy --offline --locked --workspace --all-targets --all-features -- -D warnings`. +- [x] Run `cargo test --offline --locked --workspace --all-targets`. +- [x] Run `cargo check --offline --locked --workspace --all-targets --features "fastly cloudflare spin"`. +- [x] Run `cargo check --offline --locked -p edgezero-adapter-spin --target wasm32-wasip2 --features spin`. +- [x] Run `cargo test --offline --locked -p edgezero-adapter-fastly --all-targets --features cli`, `cargo clippy --offline --locked -p edgezero-adapter-fastly --features cli --all-targets -- -D warnings`, and `cargo clippy --offline --locked -p edgezero-adapter-fastly --no-default-features --lib -- -D warnings`. +- [x] Run `cargo run --offline --locked -q --bin check_no_nested_app_config --features nested-app-config-check -- examples/app-demo crates/edgezero-cli/src/templates` and `cargo test --offline --locked -p edgezero-cli --features nested-app-config-check --bin check_no_nested_app_config`. +- [x] Run `bash scripts/check_adapter_feature_matrix.sh axum native`, then the same command for `cloudflare`, `fastly`, and `spin` with `native`. +- [x] Run `bash scripts/check_adapter_feature_matrix.sh cloudflare wasm32-unknown-unknown`, `bash scripts/check_adapter_feature_matrix.sh fastly wasm32-wasip1`, and `bash scripts/check_adapter_feature_matrix.sh spin wasm32-wasip2`. +- [x] Run `scripts/run_test_nonzero.sh adapter_capability_matrix_matches_contracts cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features cli --lib adapter_capability_matrix_matches_contracts`, then the same command for `cloudflare`, `fastly`, and `spin`. +- [x] Run `scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --test contract`, the corresponding Cloudflare command with `test-utils`, the Fastly `batch_preflight_rejects_streamed_slots_without_poisoning_siblings` command with `test-utils`, and the Spin `batch_preflight_precedence_and_indices` command with `test-utils`. +- [x] Run `cargo clippy --offline --locked -p edgezero-adapter-axum --no-default-features --features axum --all-targets -- -D warnings` and the same command with `--features axum,test-utils`. +- [x] Run `cargo clippy --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare --target wasm32-unknown-unknown --all-targets -- -D warnings` and the same command with `--features cloudflare,test-utils`. +- [x] Run `cargo clippy --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly --target wasm32-wasip1 --all-targets -- -D warnings`, then repeat with `fastly,cli`, `fastly,test-utils`, and `fastly,cli,test-utils`. +- [x] Run `cargo clippy --offline --locked -p edgezero-adapter-spin --no-default-features --features spin --target wasm32-wasip2 --all-targets -- -D warnings` and the same command with `--features spin,test-utils`. +- [x] Run `CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh dispatch_runs_router_and_returns_response cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --test contract` and `CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh streamed_upload_eof_yields_before_final_precedence cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --lib`. +- [x] Run `CARGO_TARGET_WASM32_WASIP1_RUNNER="viceroy run" scripts/run_test_nonzero.sh dispatch_runs_router_and_returns_response cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --test contract` and the same command with sentinel `closed_lifecycle_orders_hooks_and_returns_state_after_delivery`. +- [x] From `crates/edgezero-adapter-fastly`, run `CARGO_TARGET_WASM32_WASIP1_RUNNER="viceroy run" ../../scripts/run_test_nonzero.sh backend_creation_error_table_is_exhaustive cargo test --offline --locked --no-default-features --features fastly,test-utils --lib`. +- [x] Run `CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime run -W component-model-async=y -S p3=y -S http=y" scripts/run_test_nonzero.sh router_dispatches_get_and_returns_response cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test contract` and `CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime run -W component-model-async=y -S p3=y -S http=y" scripts/run_test_nonzero.sh spin_error_code_table_is_exhaustive cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources`. +- [x] Run `scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds`. +- [x] From `examples/app-demo`, run `cargo fmt --all -- --check`, `cargo clippy --offline --locked --workspace --all-targets --all-features -- -D warnings`, and `cargo test --offline --locked --workspace --all-targets`. +- [x] From `examples/app-demo`, run `cargo check --offline --locked -p app-demo-adapter-cloudflare --target wasm32-unknown-unknown --no-default-features --features cloudflare`, `cargo check --offline --locked -p app-demo-adapter-fastly --target wasm32-wasip1 --no-default-features --features fastly`, and `cargo check --offline --locked -p app-demo-adapter-spin --target wasm32-wasip2 --no-default-features --features spin`. +- [x] Run `RUSTDOCFLAGS="-D warnings" cargo doc --offline --locked --workspace --all-features --no-deps`. +- [x] From `docs`, run `npm ci`, `npm run lint`, `npm run format`, and `npm run build`. +- [x] Run `bash scripts/check_no_placeholder_pins.sh`, `bash scripts/check_no_legacy_typed_reads.sh`, `bash scripts/check_outbound_legacy_api.sh`, `node scripts/check_outbound_docs_contract.mjs`, `node scripts/check_brotli_dependency_contract.mjs`, and `node scripts/check_serde_json_map_contract.mjs`. +- [x] Review `git diff --check`, the full diff, and a case-insensitive prohibited consumer-specific terminology scan across `CHANGELOG.md`, `crates`, `examples`, `docs`, and `scripts` for unrelated changes, stale APIs, and prohibited wording. +- [x] Commit with a consumer-neutral message, push PR 275, and wait for all hosted checks. diff --git a/docs/superpowers/plans/2026-09-29-outbound-content-encoding.md b/docs/superpowers/plans/2026-09-29-outbound-content-encoding.md new file mode 100644 index 000000000..03f6c6e54 --- /dev/null +++ b/docs/superpowers/plans/2026-09-29-outbound-content-encoding.md @@ -0,0 +1,108 @@ +# Outbound Content-Encoding Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Decode standards-compliant HTTP `deflate` responses and expose typed reasons for every raw content-encoding passthrough outcome. + +**Architecture:** Keep classification and codec policy in `edgezero-core`; adapters continue to own native transport, deadline, and cancellation wrappers. Hard-migrate the public classifier to an owned typed result, generalize the existing decoder-memory control across Brotli and flate-family decoders, and preserve raw headers only for typed passthrough outcomes. + +**Tech Stack:** Rust 1.95, `http`, `async-compression`, `compression-codecs`, `flate2`/`miniz_oxide`, futures streams, four EdgeZero runtime adapters. + +--- + +### Task 1: Freeze the classifier and error contracts + +**Files:** +- Modify: `crates/edgezero-core/src/compression.rs` +- Modify: `crates/edgezero-core/src/error.rs` +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-core/src/response_egress_framing.rs` +- Modify: `crates/edgezero-core/src/lib.rs` + +- [x] Add failing classifier tests for bare `deflate`, valid unknown tokens, repeated fields, valid comma lists, invalid list elements, parameters, empty/OWS-only values, non-UTF-8 bytes, and case folding. +- [x] Run `cargo test -p edgezero-core compression::tests::content_encoding_classifier_covers_every_visible_shape`; verify compilation fails because `Deflate` and `PassthroughReason` do not exist. +- [x] Hard-cut `ContentEncoding` to add `Deflate` and `Passthrough(PassthroughReason)`; validate RFC token bytes and preserve lowercase unknown tokens. Borrow the now-non-`Copy` classification in `enforce_payload_content_length` and classify `Deflate` as compressed wire length. +- [x] Add failing `BadGatewayDecodeReason::Deflate` taxonomy/category tests, run them red, then add the variant and stable `bad_gateway_decode_deflate` egress category. +- [x] Re-export `PassthroughReason` and run the focused classifier/taxonomy tests green. +- [x] Run the full core compression suite after Task 2 supplies the new decoder API. + +### Task 2: Add audited zlib-wrapped deflate decoding + +**Files:** +- Modify: `Cargo.toml` +- Modify: `Cargo.lock` +- Modify: `examples/app-demo/Cargo.lock` +- Modify: `crates/edgezero-core/Cargo.toml` +- Modify: `crates/edgezero-core/src/compression.rs` +- Modify: `crates/edgezero-core/src/outbound.rs` +- Modify: `crates/edgezero-core/src/lib.rs` +- Create: `docs/audits/2026-09-29-flate-decoder-memory-accounting.md` +- Create: `scripts/flate_dependency_contract.json` +- Create: `scripts/check_flate_dependency_contract.mjs` +- Modify: `.github/workflows/test.yml` +- Modify: `scripts/run_tests.sh` + +- [x] Add failing stream tests for zlib-wrapped round-trip, raw RFC 1951 rejection, invalid/truncated input, trailing bytes, typed late source-error preservation, source release, and gzip/deflate decoder-memory refusal before body polling. +- [x] Run the focused deflate test and verify compilation fails because the shared decoder, fixed charge, and generalized gzip signature do not exist. +- [x] Enable the `zlib` codecs, exact-pin the audited flate dependency graph, and update both lockfiles. +- [x] Derive and document a conservative fixed flate decoder charge from pinned `async-compression`, `compression-codecs`, `flate2`, and `miniz_oxide`; add a compiled regression covering `InflateState`, codec wrappers, and the EdgeZero output buffer. +- [x] Hard-rename `DEFAULT_MAX_BROTLI_DECODER_BYTES`, the request field/parts field, and builder to `DEFAULT_MAX_DECODER_BYTES` / `max_decoder_bytes`; enforce it before constructing gzip/deflate decoders and against the existing Brotli charge. +- [x] Implement `decode_deflate_stream` with zlib framing, terminal resource release, read-ahead/trailing and concatenated-stream rejection, native-EOF validation, and typed `Decode(Deflate)` failures. Add a pending-after-codec-EOF test proving the decoder does not report terminal success early. +- [x] Add a separate flate dependency contract with exact package/version/source/checksum coverage and direct-normal-dependency assertions for both the root and excluded demo workspaces; wire it after both locked fetches in CI and the local test runner. +- [x] Run `cargo test -p edgezero-core compression`, `cargo test -p edgezero-core outbound::tests`, and `node scripts/check_flate_dependency_contract.mjs`; expect all to pass. + +### Task 3: Integrate every adapter + +**Files:** +- Modify: `crates/edgezero-adapter-axum/src/outbound.rs` +- Modify: `crates/edgezero-adapter-cloudflare/src/outbound.rs` +- Modify: `crates/edgezero-adapter-fastly/src/outbound.rs` +- Modify: `crates/edgezero-adapter-spin/src/outbound.rs` +- Modify: `crates/edgezero-adapter-axum/tests/contract.rs` +- Modify: in-module conversion tests in the Cloudflare, Fastly, and Spin outbound modules + +- [x] Add adapter coverage for zlib-wrapped deflate decoding, `content-encoding`/`content-length` stripping, decoded-cap enforcement, and typed raw-preservation classification. Shared core tests cover codec edge cases; Axum covers the native conversion path, and each adapter compiles against the same branch. +- [x] Update each adapter to consume the borrowed non-`Copy` classification, enforce the generalized decoder cap, and route `Deflate` through the shared decoder. +- [x] Run the workspace adapter test suite and feature compilation gates; all existing adapter tests pass. + +### Task 4: Migrate generated and example applications + +**Files:** +- Modify: `crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs` +- Modify: `crates/edgezero-cli/src/generator.rs` +- Modify: `examples/app-demo/crates/app-demo-core/src/handlers.rs` +- Modify: associated generator and demo tests + +- [x] Add generator/demo assertions for `max_decoder_bytes` and the removal of `max_brotli_decoder_bytes`. +- [x] Hard-migrate templates and demo code to the generalized control. +- [x] Run the workspace CLI/demo tests and generated-project compilation gates. + +### Task 5: Align normative documentation + +**Files:** +- Modify: `docs/superpowers/specs/2026-05-21-outbound-http-design.md` +- Modify: `docs/superpowers/plans/2026-09-06-outbound-http-phase2-body-response-limits.md` +- Modify: `docs/guide/capabilities.md` +- Modify: `docs/guide/architecture.md` +- Modify: `docs/guide/adapters/overview.md` +- Modify: `docs/guide/proxying.md` +- Modify: `docs/guide/streaming.md` +- Modify: `CHANGELOG.md` +- Modify: `scripts/check_outbound_docs_contract.mjs` +- Modify: `scripts/check_outbound_legacy_api.sh` + +- [x] Add docs-contract assertions for the new API/table/error/limit names and removal of stale `deflate`-passthrough wording. +- [x] Update the public API, portable encoding table, pipeline, limits, error taxonomy, test matrix, dependency evidence, public guides, and phase plans to include typed passthrough and zlib-wrapped deflate. +- [x] Record the hard migration and reject the removed bare variant/builder in legacy checks. +- [x] Run the docs and legacy contract scripts. + +### Task 6: Full verification and delivery + +- [x] Run `cargo fmt --all -- --check`. +- [x] Run `cargo test --workspace --all-targets`. +- [x] Run `cargo clippy --workspace --all-targets --all-features -- -D warnings`. +- [x] Run `cargo check --workspace --all-targets --features "fastly cloudflare spin"`. +- [x] Run the exact Cloudflare, Fastly, and Spin WASM clippy/check commands from `.github/workflows/format.yml`. +- [x] Run the excluded demo workspace tests and checks. +- [x] Review `git diff --check`, the complete diff, and generated documentation consistency. +- [ ] Commit, push PR 275, and verify hosted checks. diff --git a/docs/superpowers/plans/2026-10-05-logging-ownership-boot-diagnostics.md b/docs/superpowers/plans/2026-10-05-logging-ownership-boot-diagnostics.md new file mode 100644 index 000000000..2b0dd7d5c --- /dev/null +++ b/docs/superpowers/plans/2026-10-05-logging-ownership-boot-diagnostics.md @@ -0,0 +1,59 @@ +# Logging Ownership and Boot Diagnostics Implementation Plan + +> **For agentic workers:** Use superpowers:executing-plans to implement this approved plan with test-first changes and verification checkpoints. + +**Goal:** Share log-level resolution and preserve boot warnings without raising ordinary logging verbosity. + +**Architecture:** Use the existing `log` facade, one pure core resolver, and a fixed boot target. Reuse backend target filtering, initialize logging before application configuration, and defer configuration-read diagnostics as bounded state. + +**Tech Stack:** Rust, log, fern/log-fastly, simple_logger, Cargo, WASM contract tests. + +--- + +## Task 1: Shared Policy and Ownership + +Files: `crates/edgezero-core/src/{logging,lib,app}.rs`. + +- [x] Add resolver and boot-target tests; run `cargo test -p edgezero-core logging` and observe the missing-feature failure. +- [x] Implement `resolve_logging_level(&EnvConfig) -> LevelFilter`, `BOOT_LOG_TARGET`, and `BOOT_LOG_LEVEL`; document application filter/initialization ownership. +- [x] Rerun core tests and verify the resolver leaves the global maximum unchanged. + +## Task 2: Runtime Filtering and Startup Order + +Files: Fastly `src/{logger,lib}.rs`, Axum `src/dev_server.rs`, Cloudflare/Spin `src/lib.rs`, adapter contract tests. + +- [x] Add failing filter/order tests before changing runtime initialization. +- [x] Share level parsing; preserve boot warnings at every Fastly filtering layer and in Axum target filtering. +- [x] Initialize logging before application configuration and defer early setup warnings with bounded state. Hard-cut Fastly resolution to `FastlyRuntimeConfig` with explicit one-shot emission. +- [x] Preserve application-owned logging state and explicitly retain Cloudflare/Spin no-op initialization. +- [x] Run affected adapter tests and WASM compilation/lint gates. + +## Task 3: Demo, Generator, and Documentation + +Files: CLI `src/{main,demo_server,adapter}.rs`, CLI tests/templates/README, adapter/configuration guides, `CHANGELOG.md`. + +- [x] Add a failing bundled-demo logger-selection regression; avoid preinstalling the CLI backend for `demo`. Retain the capability gate using Axum's reusable logger-first preflight runner. +- [x] Update ownership/filter examples, no-op platform limitations, and boot `Warn` versus ordinary `Off` semantics. +- [x] Keep generated entrypoints free of duplicated logging implementation. +- [x] Run CLI/generator and demo checks. The bundled logging contract, excluded demo workspace tests, and full generated native/WASM workspace check pass. + +## Task 4: Verification and Delivery + +- [x] Review the complete diff with a read-only reviewer and resolve findings. Fastly and Axum/CLI reviewers found no production defect; harden SDK capture, public one-shot emission, conflicting-logger startup, and actual demo boot-warning assertions. Keep provider startup guarantees source-reviewed rather than claiming deployed evidence. +- [x] Run workspace tests, formatting, strict workspace clippy, feature checks, three WASM targets, docs lint/format, and generated/demo checks. + +Delivery record: [PR #275](https://github.com/stackpop/edgezero/pull/275) commit history and hosted check results. + +## Verification Evidence + +- Required native workspace test gate: pass, including 713 core tests and the new startup probes. +- Strict all-feature workspace clippy, Fastly CLI-only/no-feature clippy, and feature compilation: pass. +- Cloudflare, Fastly, and Spin WASM clippy with runtime/test-utils features: pass. +- Excluded app-demo tests, strict clippy, formatting, and all three WASM builds: pass. +- Full generated-workspace native/strict-clippy/core-test/WASM check: pass. +- Bundled demo CI sentinel: pass; runtime `Off` retains a real boot warning and terminal stderr. +- Strict public Rustdoc, docs lint/format/build, outbound docs, codec pins, and JSON-map audits: pass. + +Native `cargo test --all-features` is not the repository's test gate: it cannot link Fastly +runtime host imports on macOS. Runtime features are compile/lint checked and exercised by +provider-hosted CI. This change does not claim new deployed logger evidence. diff --git a/docs/superpowers/plans/2026-10-18-trusted-server-outbound-01-gates.md b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-01-gates.md new file mode 100644 index 000000000..b5630cfed --- /dev/null +++ b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-01-gates.md @@ -0,0 +1,355 @@ +# Outbound Feasibility and Evidence Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Establish the host primitives and acceptance inventory needed to implement the blocking cutover without weakening outbound requirements. + +**Architecture:** Extend the existing reusable-app fixture and harness, not the application transport. Keep characterization results separate from production implementations and from capability promotion. Validate the exact pinned SDK/runtime graph before depending on readiness or executor behavior. + +**Tech Stack:** Existing fixture Cargo workspace, JSON evidence, Fastly SDK 0.13.1, Viceroy 0.21.0, workerd, Spin 4.1, native Axum. + +--- + +Read the [index](2026-10-18-trusted-server-outbound-implementation.md) and its baseline invariants. +This phase does not claim any requirement implemented merely because an SDK declares an API. +Apply the index's per-task test/format/clippy procedure after every eventual code change. + +## Scoped Gate Override (2026-10-24) + +Apply the requester's [scoped timing and native integration approval](../../audits/2026-10-18-outbound-api-approval.md#scoped-timing-and-native-integration-amendment) +before the older G2/G4 implementation prerequisites and phase exits below. The +5 ms tests remain +measured qualification targets, not hard scheduler guarantees. Timing-only G2 failures and missing +finite H(K) approval no longer block the [D1d Send-first checkpoint](2026-10-18-trusted-server-outbound-03-drivers.md#send-first-checkpoint-2026-10-24). +The synchronous optimizer horizon stays explicitly **UNKNOWN** until provider evidence; this +does not mark G2 Passed or supply a finite horizon. Preserve every historical failed observation. + +The checkpoint leaves the current trait, ordinary SDK send branch, backend B/policy, timers, +slack and resources unchanged. B/domain/cardinality approval still gates backend implementation; +typed body-error preservation and fresh-readiness/subsequent-read safety are not waived. +Full EZ-1 settlement, Follow, engine and atomic trait migration remain open. Hosted/performance +qualification and the remaining numerical-policy decisions stay blocked. This supersedes the +blanket implementation stops in G2/G4 only to that extent; the prior execution record remains below. + +## Execution Record + +G1's draft inventory and fail-closed checker are implemented: 255 source acceptance cases, +including two consumer prose checks, plus supplemental template/demo checks produce 511 +reviewed split/environment legs. The subsequent +[local acceptance binding checkpoint](2026-10-18-trusted-server-outbound-implementation.md#g1f9b-local-acceptance-binding-checkpoint) +activates EZ-23 local criteria 01-11 with actual guarded tests and CI commands. The subsequent +consolidation runs one complete Axum suite and derives all required test names, including +criterion 10's factory test, from the same inventory rather than another CI list. The remaining +500 commands are planned, including EZ-23's begun-request and consumer gaps. No applicability, +product or qualification approval is fabricated. Scope lock awaits those owner decisions. + +G2's first measured error-preservation control **failed** on pinned Viceroy 0.21.0. The opt-in +`outbound-readiness` suite and its nonzero measurement guard are implemented. Three direct reads +report `UnexpectedEof`; the corresponding timed-readiness paths report clean EOF. See the +[feasibility report](../../audits/2026-10-18-outbound-feasibility.md). The remaining scenarios below +are not passing or complete. D1's dependent proposal is cancelled pending a reviewed primitive; +G3 probes record native/Viceroy local observations and Cloudflare's tooling block. The initial +Spin failure exposed and now regression-tests an internal egress early-success bug; the fixed +local cases pass. G3's remaining legs and Fastly-policy approvals stay pending. A later scoped +conversation reply authorizes the exact G4 API digest, with maintainer attribution supplied as AG. +P1's core clock/timer foundation has started without changing client traits, adapter transports +or constructors. The response-egress fix itself did not approve the new begin API. + +## Task G1: Acceptance Inventory and Scope Lock + +**Files:** Create `tests/fixtures/reusable-app/outbound-cutover-cases.json` and +`scripts/check_outbound_cutover_evidence.mjs`; test in +`scripts/check_outbound_cutover_evidence.test.mjs`; document the invocation in +`tests/fixtures/reusable-app/README.md`. + +- [x] Write Node tests rejecting a missing blocking leg, wrong candidate SHA, zero samples, + expired-only artifact, wrong environment, failed measurement, and unjustified deferral. + Run `node --test scripts/check_outbound_cutover_evidence.test.mjs`; demonstrate failure. +- [ ] Inventory every acceptance criterion of EZ-1-EZ-25 and EZ-Q1, including unnumbered EZ-9 + cases. Give each an immutable case ID such as `EZ-9/acceptance/09`, source-text digest, and + separate environment legs. JSON fields are `case_id`, `requirement_ids`, `source_digest`, + `environment`, `applicability`, `approval`, `expected`, and `owner`. Digest mismatch requires + review, not automatic acceptance. Build fixtures with `json!` or structured serialization. + Add owning task/substep, named sentinel, exact command, expected assertion, and artifact path + for every leg. Use the explicit follow-up traceability table in plan 04; a grouped title alone + does not establish coverage. Split EZ-2 criterion 14a caching and 14b fresh-execution pooling. +- [ ] Implement fail-closed applicability checks for blocking parts, approved promotions, + confirmed-present follow-ups, and explicit conditions. Split mixed criteria. The default + pending status cannot satisfy a required leg; approved deferred JS/reuse legs do not fail an + unrelated host/WASI promotion. Record unresolved priority/product decisions for maintainers. + Record Image Optimizer staging entitlement/owner availability explicitly: EZ-8 remains + blocking unless maintainers approve a written requirement change. CI's lack of an entitlement + is not a passing result or an automatic demotion. EZ-24 remains Follow-up unless promoted. +- [ ] Record these conditional decisions individually. The task owner prepares the proposal; + the attributed EdgeZero maintainer approves adoption, with TS approval for any change to its + blocking/applicability set. Name the actual approver, date, exact part/target, implementation + owner, sentinel/command, CI owner and evidence in G1/G4. Pending is not adoption, and an + approved sibling never adopts the rest of a range: + + | Conditional Part | Proposal / Implementation Owner | Execution After Approval | + | ----------------------------- | ------------------------------- | -------------------------------------------------------------------------------------- | + | EZ-5.9 and EZ-5.10 separately | P3 | Shared finalizer plus actual adapter metadata controls; Q2 activates adopted sentinels | + | EZ-24.11 | F10a | Published-double abandonment/cancellation on each adopted target | + | EZ-24.12 | F10b | Delete bespoke demo/template doubles and execute generated/demo tests | + | EZ-24.2 JavaScript target | F10c | Explicit JS feature/runtime tests, independent of host/WASI promotion | + | EZ-25.42 | Q1 | Snippet checker, Node tests and compile invocation in Rust-capable CI | + | EZ-25.43 | Q1 | Existing docs-checker drift regressions and CI execution | + +- [x] Run the Node tests and `node scripts/check_outbound_cutover_evidence.mjs --help`. + Add a manifest-validation mode that passes with pending results and a separate release mode + that fails until all applicable results pass. Review the inventory against EZ-Q1.1-Q1.8. + Suggested commit: `test: define outbound cutover evidence inventory`. + +## Task G2: Fastly Timed Header and Body Readiness Proof + +**Files:** Create `tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs` +and register the binary in its `Cargo.toml`; add controlled origin cases in +`tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs`; add execution/evidence in +`runners.rs`, `evidence.rs`, and `main.rs` in the same harness; update fixture `README.md`. +Create the report `docs/audits/2026-10-18-outbound-feasibility.md` as this task's output, not as a +claim that feasibility has already been established. + +- [x] First inspect the exact Fastly `fastly-sys` async I/O declarations, normal-body readiness, + pending-response readiness, read semantics, and Viceroy implementation cited in EZ-2 open + question 6. Record what read size and readiness state make a subsequent read bounded. A + readiness indication followed by an unconstrained blocking read does not establish the gate. +- [ ] Add loopback origin scenarios: silent head; head now/body permanently pending; one byte + every 300 ms under a 500 ms deadline; two pending bodies with only one ready; one ready body + becoming exhausted between reads; EOF, malformed framing, and transport failure. Prove both + buffered and streamed reads return by deadline +5 ms and preserve typed source errors. +- [x] Add an `outbound-readiness` fixture suite and a nonzero case-count assertion. Run + `scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-readiness` after adding + support for that suite. Capture per-read start/end times, readiness outcome, and candidate SHA. + This command is a planned extension, not an existing passing suite at F. +- [x] Name the harness guard `outbound_readiness_rejects_empty_or_unbounded_measurements` and run + `scripts/run_test_nonzero.sh outbound_readiness_rejects_empty_or_unbounded_measurements cargo test --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness outbound_readiness_rejects_empty_or_unbounded_measurements` + before/after adding the suite and evidence assertions. Expected: red for missing case/measurement + validation, then a nonzero pass rejecting empty counts or missing read/timer samples. Execute + the actual `outbound-readiness` command above afterward; its measured host reads, not the + harness unit test, establish feasibility. Apply the fixture task fmt/clippy card. +- [ ] Provide the same fixture build and invocation contract for an authorized hosted Fastly + staging run. TS owns hosted execution under EZ-Q1.4. Store SDK, runtime/service settings, + source SHA, and timings. If proof fails, stop dependent Fastly qualification; report the + precise provider boundary. Never add the rejected blocking-body fallback or widen +5 ms. + Suggested commit after local proof: `test: probe deadline-bounded Fastly body readiness`. +- [ ] Write the feasibility report with exact source/SDK/runtime identities, raw result hashes, + response-head wait operation, body readiness operation, maximum read size after readiness, + timer granularity, EOF/error behavior, cancellation ownership, sibling-progress result, and + proposed finite deadline-only backend-policy bound. Classify each local and hosted leg as + Passed, Failed, or PendingExternal; include the exact callable internal interface and an + example of one timeout/read transition. D1 must read this artifact before selecting its primitive. + B is a proposed measured/cardinality-proved integer here; only G4 can approve it. The report + must not impose a minimum budget or silently omit a budget domain. Include EZ-8.10's proposed + finite numerical horizon rule H(K), maximum and full policy domain separately from B. Account + for quantization, phase composition, retry/provider work and unchanged eligible settings for + equal per-call timeouts with differing deadlines. SDK timer arithmetic alone does not prove a + synchronous host-call bound. Record unknown/failed proof explicitly, never invent a finite horizon. +- [ ] If a local leg fails, cancel D1's proposed Fastly multiplexer/slack-removal implementation + and defer its dependent Fastly D5/D6 qualification legs. P1-P5 portable work and D2-D4 may + proceed independently, but TS production adoption stays blocked. If only hosted execution is + unavailable, mark those legs PendingExternal; locally proved work may proceed at risk without + claiming qualification. If hosted behavior contradicts local proof, cancel provider promotion, + stop release/repinning, and return to a reviewed provider/API or requirement decision. The + report must list runnable, cancelled, and deferred tasks by ID; no legacy client, blocking-read + fallback, cutoff widening, or automatic BestEffort waiver is an alternative branch. + +### G2 Local Runtime Repair Checkpoint (2026-10-23) + +- [x] Carry an exact-source Viceroy 0.21.0 patch and co-located tests in PR #275, with explicit + apply/build/runtime-override instructions. No SDK/runtime version pin or installed binary changes. +- [x] Reproduce nine failing new tests before the repair; pass all ten afterward and all 135 + upstream library tests. Preserve owned typed errors, empty-frame safety, cancellation/append + ownership, first-poll read bounds and cooperative decoder progress. +- [x] Retain separately identified stock/repaired guest runs using identical guest bytes: three + stock comparisons fail, three repaired comparisons preserve UnexpectedEof. Patched probe + exit 2 remains unverified overall; old failed evidence is unchanged. +- [ ] Complete the remaining full G2 ABI scenarios and exact-version review, then obtain the + separate B/H(K) and hosted evidence/approval gates before dependent qualification. + +### G2 Expanded Diagnostic Checkpoint (2026-10-23) + +- [x] Implement eleven owned loopback scenarios with bounded sockets, request heads, workers, + lifetime and joined cleanup; remove the superseded shared-origin truncation shortcut. +- [x] Run a fixed 135-probe / 147-request inventory: buffered and pull-streamed consumption, + one-byte and 16 KiB reads, three repetitions and finite direct truncation controls. +- [x] Reject incomplete/duplicate identities, contradictory reads, missing fresh readiness, + changed clocks/deadlines, invented terminal outcomes, mismatched errors and late operations. + Persist completed samples incrementally so failed guests retain their preceding trace. +- [ ] Establish the fixed +5 ms bound. The first complete repaired run retained a 507.794 ms + pending-body result, so local timing acceptance remains failed. Do not widen or waive it. +- [ ] Complete production-path and exact-version review, B/H(K) decisions and hosted evidence. + +The [expanded diagnostic record](../../audits/evidence/2026-10-23-fastly-readiness-matrix.json) +is working-tree characterization, not frozen-candidate qualification. Earlier failed stock and +repair evidence remains unchanged. Implemented cases do not imply a passing G2 gate. + +See the [repair record](../../audits/evidence/2026-10-23-viceroy-readiness-repair.json) and +[source patch](../../../patches/viceroy/README.md). This fixes the measured local runtime defect, +not the unmodified runtime or hosted service. Strict upstream clippy reports the same stock and +repaired diagnostics under Rust 1.98.1; no unrelated lint changes/suppressions are included. + +### G2 Timer-Control Checkpoint (2026-10-23) + +- [x] Capture raw ABI entry/return before formatting while retaining the outer operation and + whole-exchange +5 ms checks. Reject missing/reversed/outside raw timestamp pairs. +- [x] Run identical guest bytes on stock/repaired runtimes with 72 timer-only controls each: + eight repetitions, 1/100/500 ms, native sleep, guest WASI sleep and empty select. Exclude + three recorded warm-ups and repeat the unchanged 135-probe / 147-request HTTP inventory. +- [x] Retain both measurement phases: native lateness in the first pair and, after review, one + 100 ms stock WASI sleep returning at 106.952 ms without HTTP body handles. Preserve locally + passing repaired matrices, stock source-error failures and earlier failed evidence. +- [x] Exclude status formatting from timer-only comparisons; enforce positive raw wait duration + and the declared primitive-return endpoint. Keep the G2 outer exchange deadline unchanged. +- [ ] Identify or establish a bound for the original late select. Source inspection and these + samples narrow hypotheses, not prove a scheduling cause, fix or deterministic +5 ms bound. + +The [timer-control record](../../audits/evidence/2026-10-23-fastly-timer-controls.json) is unqualified. +G2 remains Failed. Do not shorten budgets, add margins or promote the pending Fastly engine/policy. +Approved portable work can proceed independently of this failed provider gate. + +## Task G3: Eager Scheduling and Empty-Body Host Premises + +**Files:** Modify `tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs`, the +four fixture entrypoints, and harness `net.rs`/`runners.rs`; add adapter-local tests in each +`crates/edgezero-adapter-*/tests/contract.rs` and Spin `tests/sdk_resources.rs` where applicable. + +This is host/runtime characterization before the new public begin API exists. Use direct SDK +operations only inside the opt-in qualification fixture to establish scheduler feasibility; do +not add another production client. D6 repeats the scenarios through the implemented public APIs. + +**Execution update, 2026-10-19:** default-off `progress` fixtures and `outbound-progress` suite +are implemented and included after the ordinary smoke cases. The origin-receipt sentinel passed +after rejecting empty, foreign, duplicate, wrong-protocol, unknown-adapter and unmeasured-wait +traces. Separate regressions cover requested case/token identity, cross-adapter build fingerprints +and a response gate released only by the first client byte. Fixture tests: 33 harness and 10 core +passes, plus the native absent-runtime control. The all-adapter smoke run exited 1: native and +Viceroy each passed five progress and five HTTP/1 framing cases; Spin passed four progress and +five framing cases, but response-owned execution failed. Cloudflare's CLI mismatch prevented +runtime execution without repinning/downloading. See the retained G3 record linked in the +[feasibility report](../../audits/2026-10-18-outbound-feasibility.md). The checkboxes below remain +open because their full protocol, scheduler-context, hosted and callback-return proofs are not +complete. G4 approval is not inferred from partial local evidence. + +**Subsequent repair:** isolated ready/timer-only responses failed through Spin's adapter while +equivalent SDK-native body-writer controls passed. The response-result future could return success +before source/writes completed; the adapter incorrectly aborted on it. It now latches that success +once, retains the same operation and deadline, and closes/publishes the body before terminal success. +The native helper regression was red before this fix; deadline/error precedence and resumption of +the same pinned operation are covered. Spin now passes all five progress cases, five framing cases +and four comparison controls, with 18 origin receipts and two token-correlated SDK terminals. +Native unpolled-worker cleanup and unconsumed-response job drops are also covered. Canonical guest +diagnostics are revalidated after the finite pre-shutdown observation hold. The latest all-adapter +smoke exits 2 for unavailable/unverified legs, not a Spin request-serving failure. Historical failed +evidence remains retained; see the newer repair record in the feasibility report. No SDK pin, +capability, public API or approval was changed. + +**Cloudflare update, 2026-10-20:** an authorized temporary tool install supplied the exact locked +wasm-bindgen CLI without repinning. Local workerd 2026-09-10 now passes 24 coding, 46 marker, +five scheduling and five HTTP/1 framing checks, with 89 asserted origin receipts. The strengthened +harness requires original Workers body signals and keeps independent groups running after policy +failures. See the additive Cloudflare record in the feasibility report for source/artifact/runtime +hashes and exact compatibility settings. This supersedes the old tool gap, not the failed Fastly +gate or the remaining hosted/protocol/context/relay proofs. G3's complete acceptance stays open. + +- [ ] Write a probe that starts a request, then awaits an unrelated timer/KV operation with no + outbound polling for 100 ms. Record when the origin received it. Repeat with a never-polled + batch and with callback return followed by response-owned streaming. Run current probes to + demonstrate the lazy-dispatch gap; count actual requests rather than just future creation. +- [ ] Map supported scheduler installation: the existing native Axum `LocalSet`, workerd local + task scheduling, Spin WASIp3 spawning, and Fastly cooperative timed I/O multiplexing. Prove + missing scheduler context is detected before dispatch without a `spawn_local` panic. Keep + provider dependencies in their adapters; no runtime enters core. +- [ ] Exercise bodiless HTTP/1, explicit zero length, terminating chunk, and supported HTTP/2/3 + cases through actual ingress. Verify workerd's absent-body signal without reproducing the + browser body-loss bug. Establish the Spin zero-length premise before taking its fast path; + header-derived size hints alone are not raw framing evidence. Record any unavailable protocol + leg exactly as the requirement permits, not as a successful test. +- [ ] Run the native fixture harness tests with + `cargo test --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -p fixture-core --features fixture-core/qualification`. + Record local and hosted work separately; no new Native claim follows from a mock. + Suggested commit: `test: characterize outbound progress and empty-body signals`. +- [ ] Add `outbound_progress_probe_requires_origin_receipts` to the fixture harness and run + `scripts/run_test_nonzero.sh outbound_progress_probe_requires_origin_receipts cargo test --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness`. + Expected: red for missing receipt accounting, then zero-origin-receipt and unsupported-protocol + samples cannot masquerade as successful observations. Include the actual SDK probes above in + the smoke qualification routes and run + `scripts/smoke_test_reusable_app.sh --adapter all --suite smoke` with nonzero probe counts per + adapter. Record missing/failed runtime legs rather than inferring Native from the unit test; + apply the fixture task fmt/clippy card. + +## Task G4: Maintainer API and Feasibility Approval + +**Files:** Create `docs/audits/2026-10-18-outbound-api-approval.md` as a future approval record; +input is plan 02's Interfaces block and G2's feasibility report, plus G1's applicability decisions. + +**Preparation update, 2026-10-19:** the [review packet](../../audits/2026-10-18-outbound-api-approval.md) +now pins the complete Interfaces section and its input identities, inventories all eight current +implementations and their callers, and separates API, Fastly policy, optimizer horizon, +applicability and measurement decisions. The requester subsequently replied `continue` to the +question naming the exact API digest, then supplied AG as the approving maintainer. The packet +records this API-only approval, not any Fastly-policy approval. P1's paired-time foundation starts +under that instruction. The remaining G4 decisions stay open. + +**Scoped amendment, 2026-10-24:** the requester approved replacing the private string-backed +region with a public seven-variant enum, retaining `parse`/`as_str` at string boundaries and +removing the wrapper. The [approval record](../../audits/2026-10-18-outbound-api-approval.md#scoped-region-enum-amendment) +preserves the original exact-digest approval and records this amendment separately. No other +G4 decision is approved by that instruction. + +- [x] The plan owner presents the exact Interfaces block, its SHA-256 digest, the old/new + implementer migration table, mixed-collection semantics, marker default, and timeout cause + variants to the EdgeZero maintainer. Include synchronous pre-settlement support rejection and + paired app-clock/adapter-timer provisioning with constructor migration. Digest the complete + Interfaces section, including supporting-type recovery/readback signatures, immutable batch + receipts, Image Optimizer mode matrix and ownership rules, not just the main trait block. + Include batch producer constructors, retained facade constructors, request option/readback + APIs and effective marker inspection, and the complete explicit-follow policy/result/error + surface. Include the Fastly-local observation handle, immutable snapshot/getters, extension + access/current-scope binding and lifetime/counting semantics for blocking EZ-2.13; D1b cannot + invent a different public getter after this freeze. The freeze covers blocking APIs and the + shared producer contract; public follow-up extensions require their own attributed review + before implementation, not an implicit change + to this digest or compatibility defaults. + Include all implementer and caller files; facade-only preparation is intentional and external + integration tests must not require an unchecked prepared-batch constructor. Record Approved, + Rejected, or Pending with reviewer/date + and the approved interface digest. An implementer cannot self-approve by checking this box. +- [ ] The EdgeZero and TS maintainers approve the literal integer B, fixed transport-settings + scope, complete budget domain and rounding/cardinality argument from G2, or record why that + Fastly policy is rejected/pending. Link raw evidence by hash and exact source/SDK identities. + No number has been approved by this plan; lack of an approved number blocks D1. +- [ ] Separately approve the revised EZ-8.10 contract: original t0/D/source, finite numerical H(K) + rule and literal maximum/domain, and proposed 5 ms observation tolerance. Bind approval to the + source/SDK/policy digests before testing; reject a horizon justified only by an observed maximum, + runtime termination or a two-timer sum. No H(K) value is approved or proven by this plan. + Image Optimizer integration stops without this approval; production remains blocked without + hosted proof. This does not widen ordinary outbound deadlines or permit late Ok responses. +- [ ] Record decisions on EZ-24 promotion (Follow-up by default), EZ-8 entitlement availability + (Blocking remains), and M1/M2/M5/M7 sample/threshold definitions. Record zero-baseline and + inconclusive-control rules before replay. M3's count invariant is separate from its report-only + timing distribution; M4 reuse-mode capacity characterization stays deferred. +- [ ] Emit separate API and Fastly-policy decisions. P1 starts only with the approved API digest; + D1 additionally requires passing local G2 proof and an approved literal B; its Image Optimizer + integration also requires the separately approved EZ-8.10 horizon contract. Hosted pending + status is not production approval. A rejected primitive cancels its dependent implementation, + not the requirement; resumption needs a new report and approval. Review any negative result + with maintainers before changing an SDK pin or the blocking set. + +Expected: a signed/attributed decision record with no implicit approvals. No runtime API or +capability changes are made by this task. P1 must stop rather than invent an alternate signature +if its approval is absent or stale. This is the owning task for the index's API approval checkbox. + +## Phase Exit + +- [ ] Run `node --test scripts/check_outbound_cutover_evidence.test.mjs` and the checker's + manifest-validation invocation documented by G1; Expected: nonzero passing tests and a valid + case/owner/command inventory, with pending external results still visibly pending. Use G2/G3's + exact fixture commands above for feasibility. Command success does not supply G4 approval. +- [ ] Inventory covers all requirements without promoting deferred scope. +- [ ] G2 records passing proof or an explicit negative/pending disposition. Only passing local + proof plus G4's Fastly policy approval enables D1; a negative Fastly result does not prohibit + separately approved portable or other-adapter work. Hosted proof passes or visibly blocks + the production pin. +- [ ] G4 records API approval separately from the Fastly feasibility/policy decision. Missing API + approval stops P1; missing literal backend-policy bound stops D1. Missing H(K) approval + stops Image Optimizer integration, not separately approved ordinary multiplexer work. +- [ ] Executor and known-empty integration assumptions are recorded for phase 3. diff --git a/docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md new file mode 100644 index 000000000..344f2bb6b --- /dev/null +++ b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-02-policies.md @@ -0,0 +1,1143 @@ +# Portable Outbound Policies Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add the portable request/response policies and redirect contract needed for adoption while retaining all existing outbound bounds and failure invariants. + +**Architecture:** Refactor only the dispatch/response-policy boundary that must become head-first. Keep normalization, provenance, decoding, and limits shared; put redirect chain policy and scripted test utilities in focused sibling modules under `outbound/`. Adapters continue owning network I/O, clocks, cancellation, and transport resources. + +**Tech Stack:** `edgezero-core`, existing `http` re-exports, pinned `url`, `futures`, existing compression helpers, opt-in `test-utils`. + +--- + +Dependencies: G1 scope decisions, G3 scheduling map, and G4's recorded API approval. G2 gates the dependent Fastly engine. +Paths below are relative to the worktree root. All new public names are proposed, with no aliases +for superseded implementer signatures. Update every in-tree implementation in the same buildable +series; phase 3 supplies adapter integration, not a releasable default-stub period. + +> **Scoped sequencing override, 2026-10-24:** the requester's [timing and native integration amendment](../../audits/2026-10-18-outbound-api-approval.md#scoped-timing-and-native-integration-amendment) +> permits the [D1d Send-first checkpoint](2026-10-18-trusted-server-outbound-03-drivers.md#send-first-checkpoint-2026-10-24) +> on the unchanged current trait before the later atomic head-first/full-trait migration. The +> Interfaces block below remains byte-identical. Its optimizer horizon gate is qualification-only +> for this subset: synchronous blocking horizon **UNKNOWN**, +5 ms a measured target, original +> logical deadlines and late rejection unchanged. B/backend policy, timers, slack, resources and +> typed body-error/readiness safety are not waived. The full migration remains pending; this +> sequencing decision is not approval of alternative signatures or a compatibility helper. + +## Interfaces: Proposed Hard-Cut Contract + +> **Scope redesign authorized, 2026-10-27:** the requester explicitly reopened EdgeZero's +> interface and required an ideal architecture without compatibility paths or duplicated +> implementations. The [approval record](../../audits/2026-10-18-outbound-api-approval.md#reopened-invocation-scope-interface-2026-10-27) +> records this separately from the original exact-digest approval. Core owns one typed scope +> implementation; adapters implement progress, not separate lifetime protocols. The independently +> approved dispatch-slack retirement preserves original deadlines, B31 and all limits. + +G4 owns approval of this single interface contract before P1 changes runtime code. The following +are signature declarations, not claimed existing or completed implementations. Supporting types +are owned by core `outbound`, except the explicitly Fastly-local observation types below; +their fields are private unless shown public. Do not let individual +adapters invent competing head/context/pending/result types. These signatures preserve local +`!Send` futures and `OutboundHttpClient: Send + Sync` without unsafe wrappers. + +```rust,ignore +pub type OutboundFuture = + futures_util::future::LocalBoxFuture<'static, Result>; + +pub enum OutboundSendMode { + Batch, + Begin, + Follow, + Send, +} + +pub trait OutboundHttpClient: Send + Sync { + fn time(&self) -> OutboundTime; + fn progress(&self) -> std::rc::Rc; + fn validate_supported_options(&self, request: &OutboundRequest, + mode: OutboundSendMode) -> Result<(), EdgeError>; + fn dispatch_head(&self, request: OutboundDispatchRequest) + -> OutboundFuture; + fn begin(&self, request: OutboundDispatchRequest) + -> Result; + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch; +} + +impl HttpClient { + pub fn new(client: std::sync::Arc) -> Self; + pub fn with_client(client: Client) -> Self + where Client: OutboundHttpClient + 'static; + pub fn send(&self, request: OutboundRequest) -> OutboundFuture; + pub fn begin(&self, request: OutboundRequest) -> OutboundFuture; + pub fn start_batch_until(&self, requests: Vec, cutoff: Deadline) + -> OutboundBatch; + pub fn send_all_until(&self, requests: Vec, cutoff: Deadline) + -> futures_util::future::LocalBoxFuture<'static, + Result>; + pub fn with_proxy_marker(self, enabled: bool) -> Self; + pub fn proxy_marker_enabled(&self) -> bool; + pub fn effective_proxy_marker_enabled(&self, request: &OutboundRequest) -> bool; +} +``` + +There is one network primitive, `dispatch_head`, plus mandatory eager scheduling and non-I/O +preparation hooks, not three transports. `HttpClient::send` and explicit redirect following use the same primitive and +head finalizer; adapter begin/batch hooks schedule that same pipeline before returning. No trait +default implements eager work as unpolled futures. Remove the old implementer-facing `send` +requirement in the atomic migration; application-facing `HttpClient::send` remains. +`new`/`with_client` retain these signatures and obtain the paired time source through the required +trait hook. They do not create another transport, clock or inert timer. Client marker configuration +belongs to the facade, with default on; effective inspection combines it with the request's override +without polling, validating unrelated options or performing I/O. + +### Request Options and Readback + +```rust,ignore +pub enum ResponseDecoding { Decode, Original } +pub enum OutboundProxyMarker { Inherit, On, Off } +pub enum OutboundImageFormat { Auto, Avif, Gif, Jpeg, JpegXl, Mp4, Png, WebP } +pub enum OutboundResizeFilter { Nearest, Bilinear, Bicubic, Lanczos2, Lanczos3 } +pub enum OutboundCropFocus { Default, Smart, Percent { x: u8, y: u8 } } +pub enum OutboundImageOptimizerRegion { + Asia, Australia, EuCentral, EuWest, UsCentral, UsEast, UsWest, +} + +impl OutboundRequest { + pub fn connect_timeout(self, timeout: Duration) -> Self; + pub fn connect_timeout_setting(&self) -> Option; + pub fn response_decoding(self, choice: ResponseDecoding) -> Self; + pub fn response_decoding_setting(&self) -> ResponseDecoding; + pub fn proxy_marker(self, setting: OutboundProxyMarker) -> Self; + pub fn proxy_marker_setting(&self) -> OutboundProxyMarker; + pub fn image_optimizer(self, options: OutboundImageOptimizerOptions) -> Self; + pub fn image_optimizer_options(&self) -> Option<&OutboundImageOptimizerOptions>; +} + +impl OutboundImageOptimizerRegion { + pub fn parse(token: &str) -> Result; + pub fn as_str(&self) -> &str; +} + +impl OutboundImageOptimizerOptions { + pub fn new() -> Self; + pub fn region(self, region: OutboundImageOptimizerRegion) -> Self; + pub fn region_setting(&self) -> Option<&OutboundImageOptimizerRegion>; + pub fn preserve_origin_query(self, preserve: bool) -> Self; + pub fn preserve_origin_query_setting(&self) -> bool; + pub fn format(self, format: OutboundImageFormat) -> Self; + pub fn format_setting(&self) -> Option; + pub fn quality(self, quality: u8) -> Result; + pub fn quality_setting(&self) -> Option; + pub fn resize_filter(self, filter: OutboundResizeFilter) -> Self; + pub fn resize_filter_setting(&self) -> Option; + pub fn width(self, width: u32) -> Result; + pub fn width_setting(&self) -> Option; + pub fn height(self, height: u32) -> Result; + pub fn height_setting(&self) -> Option; + pub fn crop(self, width: u32, height: u32, focus: OutboundCropFocus) + -> Result; + pub fn crop_setting(&self) -> Option<(u32, u32, OutboundCropFocus)>; +} +``` + +The selector enums, including region, are Copy/Clone/Debug/PartialEq/Eq values. Region supports +direct typed construction and exhaustive matching; `parse` and `as_str` convert exactly the pinned +SDK's seven region tokens without making core depend on that SDK. Only options have private +fields and support Default/Clone/Debug/equality; `new` leaves selectors unset and query +preservation false. Quality above 100, zero dimensions/crop ratio and percent focus above 100 fail +at construction as typed client errors. Paired percentage focus makes incomplete offsets and +smart-plus-offset combinations unrepresentable. Core validates public enum payloads at +the options builder, not after provider dispatch. No implicit region or format is invented. + +Request defaults are no connect timeout, Decode, Inherit and no optimizer. Zero connect timeout +is retained as invalid input and rejected at ordinary preflight under EZ-4.5; unsupported options +still win first. Preserve all settings through parts round-trips and prepared redirect requests. +The client resolves Inherit into the effective on/off value before every dispatch, including +supplied clients. Its effective inspector, not the request's raw Inherit getter alone, satisfies EZ-7.7. + +### Explicit Redirect Following + +```rust,ignore +impl OutboundRedirectLimit { + pub fn new(limit: usize) -> Result; + pub fn get(&self) -> usize; +} + +impl<'head> OutboundRedirectContext<'head> { + pub fn redirect_number(&self) -> usize; + pub fn status(&self) -> StatusCode; + pub fn request_uri(&self) -> &Uri; + pub fn resolved_target(&self) -> &str; + pub fn headers(&self) -> &HeaderMap; + pub fn is_cross_origin(&self) -> bool; + pub fn prepared_request(&self) -> Option<&OutboundRequest>; + pub fn take_prepared_request(&mut self) -> Option; +} + +pub enum OutboundRedirectAction { FollowPrepared, Follow(OutboundRequest), Stop } +pub enum OutboundRedirectStopReason { + NoLocation, MethodNotRewritable, BodyNotReplayable, LimitReached, StoppedByPolicy, +} +pub enum OutboundFollowFailure { + Hop { index: usize, error: EdgeError }, + Policy { redirect_number: usize, error: E }, +} + +impl OutboundFollowResult { + pub fn response(&self) -> &OutboundResponse; + pub fn final_uri(&self) -> &Uri; + pub fn redirects_followed(&self) -> usize; + pub fn stop_reason(&self) -> Option; + pub fn into_parts(self) + -> (OutboundResponse, Uri, usize, Option); +} + +impl HttpClient { + pub fn follow(&self, request: OutboundRequest, + limit: OutboundRedirectLimit, policy: P) + -> futures_util::future::LocalBoxFuture<'static, + Result>> + where + P: for<'head> FnMut(&mut OutboundRedirectContext<'head>) + -> Result + 'static, + E: 'static; +} +``` + +Limit, context and success types have private fields. Limit accepts 1..=20 with no Default; +success/context cannot be publicly reconstructed. The synchronous trusted policy may own Rc and +return an arbitrary non-Clone, non-Send application error unchanged; neither P nor E requires +Send/Sync/Debug/Error. Context borrows the answering head and owns its optional prepared request +for that policy call only. Redirect numbers start at one, hop indices at zero. `FollowPrepared` +uses the remaining prepared request; `Follow(request)` transfers a modified prepared request or +a complete fresh replacement. Taking the prepared request and then selecting FollowPrepared is +a protocol failure, not a second use or a hidden copy. + +The policy runs only at EZ-9.18's prescribed stage and never for the initial request. Invalid +Location evidence/references, user information or unusable HTTP(S) targets fail as typed 502 +Protocol at the answering hop. A valid non-HTTP target is exposed as a resolved string with no +prepared request; Stop/rejection remain possible, while FollowPrepared fails as 502 Protocol. +Errors in a request the policy returns are unchanged hop errors, not reclassified URL failures. +No policy rejection value is converted to EdgeError or exposed on the wire by this API. + +Each hop uses the same client's Follow-mode head-first pipeline and ordinary option validation. +Preparation performs exactly the specified method/body-header/cross-origin credential rules; +policy-authorized replacement credentials are not stripped again. Capture the initial request's +absolute deadline independently, not its per-call timeout deadline. That bound survives every +replacement through final-body consumption. Each hop captures entry before its own shared +preparation and keeps that request's per-call timeout; policy time counts against the original +absolute chain deadline, not an invented chain-wide per-call timeout. A synchronous policy cannot +be preempted: preserve Err(E) unchanged even if it consumed the remaining time; after Ok(action), +recheck the original chain deadline before dispatch or finalization. Unsupported-option validation +still precedes budget rejection for a selected next request. No expired chain returns a late Ok. +Followed heads are discarded without polling; +final heads receive ordinary finalization. At most limit+1 requests are sent. Success reports the +actual final dispatched URI and a stop reason exactly for a followable final status. Dropping the +owned future cancels the current exchange and prevents later hops; no second transport is used. + +### Dispatch and Response Context + +The facade obtains `time()` and samples its clock synchronously at method invocation, before +returning its future. It then calls `validate_supported_options` before ordinary validation, +budget rejection, timer creation or body polling. This required synchronous hook checks the +adapter's support and mode combinations without network I/O, backend registration, body reads, +or runtime task creation. Unsupported Spin authority/Image Optimizer settings therefore reject +even if a body never becomes ready. Supplied clients use the same mode contract. Adapter dispatch +may defensively recheck, but a late-only boundary check is not sufficient. + +The Image Optimizer support matrix is explicit; `Follow` performs the same single-send +primitive on every permitted redirect hop, not the eager begin/batch operation: + +| Adapter | Send | Follow | Begin | Batch | +| ---------------------- | ---------------------------------------------------- | ------------------------------- | ----------- | ----------- | +| Fastly | Supported subject to EZ-8's entitlement/horizon gate | Same mapping and checks as Send | Unsupported | Unsupported | +| Axum, Cloudflare, Spin | Unsupported | Unsupported | Unsupported | Unsupported | + +Validate method/cache combinations and target policy on each prepared hop, preserving the +immutable original chain deadline. Do not reject Fastly `Follow` merely because it is not `Send`. + +After supported-option validation, shared preparation computes the selected budget +once, resolves only permitted GET/HEAD streams, and calls the adapter with `OutboundDispatchRequest`. +Settlement, scheduling and redirect policy time never restart that budget. Transport uses remaining +time to the captured absolute limit, not a newly started `budget.duration`. + +```rust,ignore +impl OutboundTime { + pub fn new( + clock: MonotonicClock, + wait: impl Fn(Deadline, MonotonicClock) -> OutboundFuture<()> + + Send + Sync + 'static, + ) -> Self; + pub fn clock(&self) -> &MonotonicClock; + pub fn wait_until(&self, deadline: Deadline) -> OutboundFuture<()>; +} + +impl OutboundDispatchRequest { + pub fn scope(&self) -> &OutboundScope; + pub fn entered_at(&self) -> MonotonicInstant; + pub fn budget(&self) -> DispatchBudget; + pub fn clock(&self) -> &MonotonicClock; + pub fn time(&self) -> &OutboundTime; + pub fn request(&self) -> &OutboundRequest; + pub fn into_parts(self) -> (OutboundRequestParts, OutboundResponseContext); +} + +impl OutboundResponseContext { + pub fn scope(&self) -> &OutboundScope; + pub fn entered_at(&self) -> MonotonicInstant; + pub fn budget(&self) -> DispatchBudget; + pub fn clock(&self) -> &MonotonicClock; + pub fn time(&self) -> &OutboundTime; + pub fn mode(&self) -> OutboundSendMode; + pub fn request_method(&self) -> &Method; + pub fn response_body_policy(&self) -> &ResponseBodyPolicy; + pub fn original_chain_deadline(&self) -> Option; +} + +impl OutboundPreparedBatch { + pub fn scope(&self) -> &OutboundScope; + pub fn slot_count(&self) -> usize; + pub fn started_at(&self) -> MonotonicInstant; + pub fn observation_cutoff(&self) -> Deadline; + pub fn clock(&self) -> &MonotonicClock; + pub fn time(&self) -> &OutboundTime; + pub fn into_slots(self) + -> (OutboundBatchContext, Vec<(usize, OutboundDispatchRequest)>); +} + +impl OutboundBatchContext { + pub fn scope(&self) -> &OutboundScope; + pub fn slot_count(&self) -> usize; + pub fn started_at(&self) -> MonotonicInstant; + pub fn observation_cutoff(&self) -> Deadline; + pub fn clock(&self) -> &MonotonicClock; + pub fn time(&self) -> &OutboundTime; +} + +impl OutboundHead { + pub fn new( + status: StatusCode, + headers: OutboundHeaderInput, + encoded: BodyStream, + context: OutboundResponseContext, + owner: OutboundExchangeOwner, + ) -> Result; + pub fn status(&self) -> StatusCode; + pub fn headers(&self) -> &HeaderMap; + pub fn location_evidence(&self) -> &OutboundLocationEvidence; + pub fn finish(self) -> OutboundFuture; + pub fn discard(self); +} + +impl OutboundHeaderInput { + pub fn new(adapter: &'static str, headers: HeaderMap, + fidelity: OutboundHeaderFidelity) -> Self; +} + +pub enum OutboundHeaderFidelity { + AdapterVisibleFields, + ProviderFieldBoundariesUnknown, +} + +impl OutboundLocationEvidence { + pub fn observed_count(&self) -> usize; + pub fn has_non_text(&self) -> bool; + pub fn retained_singleton(&self) -> bool; + pub fn fidelity(&self) -> OutboundHeaderFidelity; + pub fn is_missing_or_empty(&self) -> bool; +} + +impl OutboundExchangeOwner { + pub fn new(release: impl FnOnce() + 'static) -> Self; +} + +impl OutboundPending { + pub fn new(already_scheduled: OutboundFuture, + owner: OutboundExchangeOwner) -> Self; + pub fn is_ready(&mut self) -> bool; + pub fn collect(self) -> OutboundFuture; + pub fn cancel(self); +} +``` + +Prepared-batch indices are original input indices. `into_slots` transfers eligible requests +with explicit indices and one opaque batch context. That context privately owns original slot +count, entry instant, paired time source, cutoff, per-slot selected deadline/source metadata and +exact preflight outcomes with their terminal observations. Producers must not enumerate the +filtered eligible vector as if its positions were original indices or re-emit preflight errors. +The facade constructs validated contexts; implementations +read them, not manufacture a new start time. `OutboundResponseContext` owns the selected budget, +paired time source, mode, complete effective response/header policies and immutable original chain deadline. +It also carries the local progress-scope token; it never borrows a request/client stack frame. +It has private fields, is not Clone, and has no public constructor, setters or destructive +decomposition. Adapters may inspect it and pass it unchanged into `OutboundHead::new`; core +retains the complete effective header/marker/decoding policies and scope token privately. The +read-only body-policy view uses P1's effective policy, not defaults reconstructed by the adapter. +`clock()` is the clock inside the captured `time()`. Neither getter permits restarting a budget +or changing the immutable original chain deadline. Prepared batches likewise have no public +constructor; supplied clients receive validated inputs from the facade. +`OutboundBatchContext` likewise has private fields with no public constructor, Clone, Default, +setters or destructive decomposition. It carries terminal metadata, not a duplicate response +context or SDK cleanup owner. Batch constructors consume it once and seed the preflight ledger. + +`OutboundTime` is cloneable and Send + Sync: it owns the existing MonotonicClock and an Arc-backed +timer factory. The factory receives that same clock, creates an owned local future, and uses the +adapter's existing timer facility; core adds no executor or Tokio dependency. The facade races +settlement against `wait_until` for the selected outbound deadline while the inbound stream keeps +its own read-deadline enforcement. Compare both absolute limits in the same application domain; +preserve EZ-1's 408/504 precedence and original typed source errors. A timer or executor setup +failure is typed Internal before body polling or transport I/O, never a panic or silent unbounded read. +Create and initially poll the timer before the first source poll so missing runtime/setup is caught +without reading the body. A later operational timer failure terminates the race with its typed error. +Timer completion is a wakeup to recheck the paired clock, not proof of expiry: early wakeups must +re-arm with a cooperative scheduling opportunity, not spin or extend the captured deadline. +Drop cancels the pending timer registration and releases its resources. +Only the shared factory and captured factory state must be Send + Sync; the returned timer future +is local and may own Rc state. A fake shared waiter registry therefore needs Send + Sync storage +(for example Arc/Mutex), while an exchange owner may capture Rc directly. This intentional split +does not make local futures Send or require weakening the client's Send + Sync contract. + +Each production adapter supplies the existing app clock plus its own deadline-wakeup factory. +Hard-migrate clock-only constructors and update request-context injection, manual clients, +fixtures, demo and generator together: + +| Existing Constructor | Required Replacement | +| ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | +| Axum `try_with_clock(MonotonicClock)` | `try_with_time(OutboundTime) -> Result` | +| Axum internal `with_transport_and_clock(Arc, MonotonicClock)` | `with_transport_and_time(Arc, OutboundTime) -> Self` | +| Fastly/Cloudflare/Spin `with_clock(MonotonicClock)` | `with_time(OutboundTime) -> Self` | + +Native `try_new()` remains fallible; other adapters' `new()` remain infallible. These default +constructors pair the process-default clock with the real adapter timer, never an inert timer. +`HttpClient::new`/`with_client` obtain this pair from the required trait hook, not a hidden second +default clock. Scripted clients install a fake clock AND wakeable timer queue; advancing fake time +wakes waiters without network or wall-clock sleeps. Source and timer pairing is an explicit +implementer obligation with conformance tests, not something MonotonicClock alone can provide. +Fastly's uninterruptible inbound-read exception in EZ-1.5 remains BestEffort; this interface does +not claim to preempt that host read. G2 separately gates bounded outbound reads. + +`OutboundHeaderInput::new(adapter, headers, fidelity)` consumes the adapter-visible HeaderMap, +records the adapter identity for marker policy, and records +whether provider field boundaries are exposed or unknown. `OutboundHead::new` charges header +caps and captures immutable Content-Type and Location evidence **before** EdgeZero filtering. +Location evidence records observed multiplicity, non-text values, retained-singleton status, and +provider fidelity, without copying the raw list. Earlier stop rules use original presence; URL +resolution consults this evidence at EZ-9.18's prescribed stage. Unknown provider information +never becomes certified raw-header fidelity. The same constructor is available to supplied clients. +Evidence has private immutable fields and no public constructor. `observed_count` counts only +adapter-visible original fields. `has_non_text` uses the existing UTF-8 eligibility rule, not a +new ASCII filter. `retained_singleton` means exactly one textual original field survived unchanged +in the visible map; it makes no stronger claim when provider boundaries are unknown. +`is_missing_or_empty` is true only for zero original observed fields or exactly one textual, +originally empty value. Duplicates, non-text fields and normalization removal cannot turn into +absence. Store these scalar facts, not a second header copy. Capture at head construction but +defer duplicate/invalid-location rejection until EZ-9.18's resolution stage; earlier stop rules win. + +The owner is local and may capture `Rc`/SDK handles. It releases exactly once on head drop, +discard, finalization failure, or terminal body drop. `finish` transfers the owner into a streamed +body or releases it after bounded buffered completion; `discard` never polls body bytes. Pending +construction requires work already issued/scheduled; its future is only the completion receiver, +not a lazy exchange. `is_ready` is non-consuming, treats an error as ready, and never blocks. +Successful streamed collection transfers/disarms the pending cancellation owner before returning +the live body; dropping the completed pending handle must not abort that body. The same SDK +resource is never assigned two independent release callbacks. Add a collected-stream-stays-live +regression in addition to cancellation and drop tests. + +### Structured Invocation and Body Ownership + +The lifetime implementation is shared in core, not supplied through separate provider +`check/register/activate` callbacks. The client supplies one typed progress bridge; core captures +the active logical scope synchronously at facade invocation, before returning an owned future or +batch handle. This replaces the earlier unimplemented scope-token gap without another transport. +The required `progress` hook has no compatibility default. Axum, Cloudflare, Spin and supplied +noncooperative test clients explicitly use the same core `IndependentOutboundProgress` bridge. +Fastly implements only its native progress boundary, reusing its existing multiplexer. + +```rust,ignore +pub enum OutboundScopeKind { Callback, ResponseBody } +pub struct OutboundScopeOwner; // unique, local closure authority; not Clone +pub struct OutboundScope; // Clone, local revocable capability; no closure authority +pub struct IndependentOutboundProgress; +pub struct OutboundTask; // local Future, provider-spawned + +impl OutboundTask { + pub fn new(work: futures_util::future::LocalBoxFuture<'static, ()>) + -> Result<(Self, OutboundExchangeOwner), EdgeError>; +} + +pub trait OutboundProgressRuntime { + fn poll( + &self, + scope: &OutboundScope, + time: &OutboundTime, + entered_at: MonotonicInstant, + cx: &mut std::task::Context<'_>, + inner: &mut dyn FnMut(&mut std::task::Context<'_>) -> std::task::Poll<()>, + ) -> std::task::Poll>; +} + +impl OutboundScopeOwner { + pub fn attach(self, response: Response) -> Result; + pub fn callback() -> Self; + pub fn callback_if_absent() -> Option; + pub fn response_body() -> Self; + pub fn scope(&self) -> OutboundScope; + pub fn close(&self); + pub fn with(&self, action: impl FnOnce() -> T) -> T; + pub fn run<'future, F>(&self, future: F) + -> futures_util::future::LocalBoxFuture<'future, F::Output> + where F: std::future::Future + 'future, F::Output: 'future; +} + +pub struct Body; // private payload and optional local egress owner +pub enum BodyPayload { Once(Bytes), Stream(BodyStream) } +impl Body { + pub fn from_bytes>(bytes: B) -> Self; + pub fn from_stream(stream: S) -> Self + where S: futures_util::Stream> + 'static; + pub fn into_parts(self) -> (BodyPayload, Option); +} + +impl OutboundScope { + pub fn capture(progress: std::rc::Rc) -> Self; + pub fn is_managed(&self) -> bool; + pub fn check(&self) -> Result<(), EdgeError>; + pub fn with(&self, action: impl FnOnce() -> T) -> Result; + pub fn run(&self, time: OutboundTime, entered_at: MonotonicInstant, + future: OutboundFuture) -> OutboundFuture; + pub fn provider_state(&self, create: impl FnOnce() -> T) + -> Result, EdgeError>; +} +``` + +Scope state, operation-root cells and cancellation registrations are core-private. They cannot +be forged by constructing replacement prepared contexts. The owner is never stored in `App`, a +shared client, shared service state or HTTP extensions. Retained clients capture the active request +on every invocation, not the request where the client was constructed. + +Standard adapter entrypoints establish callback ownership outside App dispatch, covering assembly, +preparation, admission, routing, error rendering and callback return. Nested canonical send helpers +reuse that owner. A synchronous activation lasts only for a synchronous call; asynchronous work +installs and restores the captured identity on every poll. No thread-local guard spans `.await`. +Later polling in a different callback or response body cannot reparent an operation. Provider +activation must precede settlement timer construction and polling. A progress bridge invokes its +inner step at most once per call, preserves original time/entry inputs and performs only its bounded +provider progress work; it is not another executor or timer source. +Nonblocking `is_ready`/`check` activate ownership only and inspect already-scheduled completion +receivers; they never turn the SDK, create timers or poll transport bodies. Awaited collection +and callback progress use the same registered producer through the progress bridge, not a second +scheduler hidden in the receiver. + +Outside request entrypoints, direct manual operations retain caller-owned lifetimes: dropping the +operation/pending/batch is their cancellation boundary. Callers requiring a request-family boundary +use the same explicit scope owner. This does not make canonical scope ownership optional and does +not turn a closed captured scope into an unscoped operation. Reentrant starts during teardown must +observe the closing scope and fail before body polling, timer registration or provider I/O. + +Core registers the entire owned Send/settling-Begin future immediately, including an unpolled +request body, not merely a cancellation flag. Scope exit first revokes every live root, then +releases futures, SDK cancellation owners, source streams and wakers outside registry borrows. +If revocation occurs inside a poll, the active value and its output are released when that poll +returns. Cleanup is exactly once; user destructors, cancellation callbacks, waker operations and +provider polling never run under a registry borrow. Failure is typed Internal scope closure, not +a fabricated timeout, observation cutoff or clean EOF. +Normal disposal also activates the captured identity: source EOF/error, unpolled Drop and +reentrant cleanup cannot start work under an unmanaged or foreign ambient scope. Supported +unwinding during cleanup cannot suppress exactly-once terminal completion/observer notification. + +Axum, Cloudflare and Spin use the same core `OutboundTask`, backed by that revocation protocol, +instead of separate task cancellation state machines. Dropping either the provider's runner or +its cancellation owner releases producer futures and completion senders even when the other +handle is retained. Registration failure is typed before spawning; adapters keep only provider +spawn/setup and native abort operations. Cooperative Fastly driving uses the same root ownership, +not a second executor. Portable response deadline and terminal classification policy stays in +core; Workers retain host-event yielding and AbortController ownership, not a duplicate timer race. + +Begin transfers live ownership into its pending state before returning it; completing the outer +Begin future does not unregister scheduled work. Successful streamed collection disarms pending +cancellation and unregisters callback ownership before returning the independent transport body. +Batch revocation releases its producer stream and cancellation owner while preserving the existing +ledger, already recorded ready results, receipts and lossless failure/accumulator behavior. Closing +does not poll SDK streams to recover results or invent arrival times. + +Each response-egress attempt owns an independent body scope through its actual terminal transition +or Drop. For work that must start during the callback and continue in its response body, create a +`response_body()` owner **before** starting that work, use its `with`/`run` boundary, then consume +that exact owner with `attach(response)`. Merely polling callback-owned work in a body cannot +transfer it. A body owner created inside a managed callback is provisional: attaching metadata +does not accept the handoff. Callback exit revokes an unaccepted owner, including an attached +response retained outside the callback. The core envelope accepts the exact owner before policy, +framing, suppression, conversion or any body poll; successful streamed outbound collection also +accepts its new body lifetime before returning it. Closed owners cannot be resurrected. + +Response-body metadata is local, not HTTP extensions, and does not change a buffered body into a +stream, copy its bytes or change framing. The `Body` enum constructors/patterns are removed: +construct through `Body::from_bytes`/`from_stream` and inspect the separate `BodyPayload` only at +transport boundaries. Payload transformations preserve the optional owner. An outbound request +cannot carry response-egress metadata; shared validation rejects it before provider work. +Canonical egress extracts the owner before destructive body conversion or suppression. A manual +`into_parts` caller explicitly assumes its lifetime obligation; manually consuming an attached +body without a canonical egress attempt closes it at consumption termination/drop, not at an +unobservable client-delivery event. + +Callback and body scopes share a provider-progress family cache, not closure authority or root +registrations. A body group started before the callback's first origin exchange must retain that +same provider progress engine while the callback awaits the origin and after callback return. +Closing a callback revokes its own operations without clearing a still-live body family's engine. +The last family lease releases the cache. This is one core implementation, not an adapter-specific +handoff registry. + +Core wraps source polling with that body identity and makes +the source itself revocable even if a converted handle is retained. Source EOF releases the source +but does not close the body scope. Terminal egress closes body work before completion resources are +released, without cancelling callback siblings or Fastly post-delivery callback work. The existing +completion-report and provider delivery/cancellation capabilities are unchanged. Selecting a +precommit fallback closes the failed application's body work; the bounded fallback receives its +own body lifetime under the same exactly-once egress attempt. +`ResponseEgressPreparationError` keeps closed ownership separate from the existing six HTTP +framing reasons; ownership rejection must not populate framing-failure diagnostics. +Core and every producer use `DispatchBudget::own_deadline_expired_before` for the earlier-own- +deadline exception. It does not treat a `BatchCutoff`-attributed phase failure as batch abandonment. + +All original budgets, typed timeout provenance, indexed/partial batch outcomes, cutoff equality, +paired clocks, coding/provenance, independent body/header/decoder caps and direct native Image +Optimizer dispatch remain requirements. No SDK/runtime repinning, hosted qualification, lazy eager +default, alternate transport, duplicated provider lifetime registry or legacy API alias is added. + +### Batch Ownership and Mixed Collection + +```rust,ignore +pub enum OutboundBatchCheck { + Finished(OutboundBatchTermination), + Item(OutboundBatchItem), + Pending, +} + +pub enum OutboundCollectedSlot { + AlreadyDelivered, + Resolved(OutboundSlotResult), + Unresolved, +} + +pub enum OutboundBatchDriverEvent { + Cutoff, + Failed(EdgeError), + Item { + index: usize, + completed_at: MonotonicInstant, + outcome: Result, + }, +} + +impl OutboundBatchItem { + pub fn index(&self) -> usize; + pub fn result(&self) -> &OutboundSlotResult; + pub fn into_parts(self) -> (usize, OutboundSlotResult); +} + +impl OutboundBatchResults { + pub fn slots(&self) -> &[OutboundCollectedSlot]; + pub fn termination(&self) -> OutboundBatchTermination; + pub fn into_parts(self) -> (Vec, OutboundBatchTermination); +} + +impl OutboundBatchFailure { + pub fn slots(&self) -> &[OutboundCollectedSlot]; + pub fn error(&self) -> &EdgeError; + pub fn into_parts(self) -> (EdgeError, Vec); +} + +impl OutboundBatchInsertFailure { + pub fn error(&self) -> &EdgeError; + pub fn into_parts(self) -> (EdgeError, OutboundBatchItem); +} + +impl OutboundBatchMergeFailure { + pub fn error(&self) -> &EdgeError; + pub fn into_parts(self) -> ( + EdgeError, + OutboundBatchAccumulator, + Result, + ); +} + +impl OutboundBatch { + pub fn cutoff(context: OutboundBatchContext) -> Self; + pub fn from_driver(context: OutboundBatchContext, events: S, + owner: OutboundExchangeOwner) -> Self + where S: futures::Stream + 'static; + pub fn accumulator(&self) -> OutboundBatchAccumulator; + pub fn check(&mut self) -> Result; + pub async fn next(&mut self) -> Result; + pub async fn collect(self) -> Result; + pub fn cancel(self) -> Vec; +} + +impl OutboundBatchAccumulator { + pub fn insert(&mut self, item: OutboundBatchItem) + -> Result<(), OutboundBatchInsertFailure>; + pub fn merge_success(self, result: OutboundBatchResults) + -> Result; + pub fn merge_failure(self, failure: OutboundBatchFailure) + -> Result; +} +``` + +Both result and failure `slots` become `Vec` in this hard cut; no Option +compatibility accessor remains. `check`/`next` transfer a result once. Collection records consumed +positions as `AlreadyDelivered`, new outcomes as `Resolved`, and pending positions as `Unresolved`. +`Completed` forbids `Unresolved`; `Cutoff` requires at least one. Failure retains the exact error +and those same position states. Clean EOF/trailing-event validation and poisoning are unchanged. +Results/failures have private identity and payload fields, no public constructor, Clone, mutable +access or reconstruction. `slots()`/`termination()`/`error()` permit read-only inspection while +retaining merge eligibility. `into_parts` consumes identity and transfers the original outcomes +and termination/error; it relinquishes merge eligibility, just like consuming an item receipt. +Callers merge first, then decompose when taking response bodies. Hard-migrate old public-field +access rather than preserving mutable field compatibility under the new identity contract. + +Core stamps an opaque batch identity on validated consumer items and collections. Driver events +carry only unvalidated index/observation/outcome payloads; they carry no consumer identity. +`OutboundBatchItem` has private identity/index/result, no Clone, no public `new`, mutable accessor +or reconstruction method. `into_parts` consumes the receipt and relinquishes accumulator +eligibility; a consumer inspecting status/headers may still insert the intact receipt, while a +consumer taking its response body handles AlreadyDelivered itself. Hard-migrate the existing +public constructor/field callers. Remove `from_futures` and `finish_batch_item` rather than keeping +lazy defaults, raw count/clock/cutoff overloads or an alternate receipt constructor. Producers send +raw observations of already scheduled work into `from_driver`; its stream does not schedule I/O. +Core derives elapsed time and cutoff eligibility from the trusted context and earliest actual +adapter terminal observation, not later collection. It preserves the adapter's typed outcome, +including ConnectTimeout, unless the applicable governing deadline had expired at that observation +and precedence requires the selected budget timeout. Later collection never changes a recorded +outcome or cause. A backward sample produces the existing typed internal invariant result with +zero elapsed, never an enlarged budget. No raw successful result is backdated to manufacture +timeliness. IdleTimeout follows EZ-14's streamed-body rules, not this buffered batch ledger. + +The driver still enforces transport and individual deadlines and records progress while consumers +are idle. For slots without an earlier recorded terminal outcome, it emits terminal observations +whose own deadline precedes cutoff before emitting Cutoff, including a timeout first observed +after cutoff. Never replace an earlier on-time success or failure with such a timeout. A late first +poll does not excuse lazy scheduling or waiting behind a sibling. Core applies the existing logical +own-deadline exception using retained metadata; own-deadline equality remains unresolved. A Cutoff +that omits such a required timeout is a typed driver invariant failure, not silent abandonment. +Drain already-recorded on-time outcomes before terminal cutoff/failure handling. Preserve clean +EOF/trailing-event validation and poisoning; premature EOF never becomes cutoff or a timeout. + +`from_driver` owns one task-group/multiplexer cancellation guard; it drops that guard on cancel, +failure or driver completion. Individual exchange owners still release SDK resources exactly once; +the group guard unregisters/cancels work rather than duplicating SDK release callbacks. Scheduling +or setup failure after any work was issued must still return an owned driver (including Failed), +not discard its cancellation handle. Core adds no SDK controls or executor. + +`cutoff(context)` is only a no-scheduled-work constructor. It validates the captured cutoff against +the same clock, preserves deliverable preflight outcomes and leaves cutoff-unresolved slots +unresolved. With unresolved slots, premature use is typed Internal through the batch protocol. +Empty/all-resolved input completes normally without an invalid trailing Cutoff. Once work is scheduled, even if time expires +during scheduling, use `from_driver` with its owner. External supplied clients obtain preparation +through `HttpClient`, not a public fixture constructor; core unit tests may use private test helpers. +The accumulator comes from that batch, validates identity/count/index/duplicate/conflict before +mutation, and substitutes its owned items only for `AlreadyDelivered`. A missing prior item is a +typed merge error, not a timeout. Insert/merge failures own the rejected item or both input values, +so validation never silently discards results. No response clone or full-history replay is needed. +Failure types have private fields and implement Debug/Display/Error with their validation error +as source. `insert` validates completely before mutation, returns the exact rejected receipt on +failure and leaves the accumulator unchanged. `merge_success`/`merge_failure` validate identity, +length, indices, duplicate/conflict and missing-receipt conditions before moving any outcome. On +failure, `into_parts` returns the validation error, unchanged accumulator and exact original +collection; Ok/Err in the nested Result tags success/failure input, not a second attempted merge. +In particular an original driver error remains inside the recovered failure, distinct from the +merge error. No lossy conversion may discard either input or clone non-Clone responses. +`cancel` returns the union of all never-delivered indices, including recorded-but-undelivered +and still-in-flight slots, excluding delivered ones. It deliberately does not distinguish those +two undelivered subcategories; checking first is how a caller retains recorded outcomes. +The TS recipe inserts checked items after inspecting them, then merges final success/failure. +Consumers that already consumed response bodies may handle explicit AlreadyDelivered states +themselves instead of using the optional response accumulator. + +### Phase Causes and Defaults + +Add `BudgetSource::ConnectTimeout` and `BudgetSource::IdleTimeout` to the existing non-exhaustive +enum. `dispatch_budget` still chooses only aggregate budget sources; phase expiry classifiers emit +the new variants. Do not add another cause field or change wire text. An expired governing budget +wins connect ties; an expired application/chain absolute deadline wins idle ties and late observation. +After idle-mode headers, per-call/default expiry does not govern the body. Marker default is on; +client off plus per-request inherit/on/off is one policy, not parallel APIs. Decoding default stays +decode; identity always keeps its decoded cap, including original mode. + +### Fastly Request-Scoped Registration Observation + +These proposed types live in `edgezero_adapter_fastly::context`, not core or the client trait. +G4 approves their signatures and semantics with the rest of this Interfaces section; D1b owns +implementation and external compile/runtime tests. `FastlyRequestContext` stays unchanged. + +```rust,ignore +impl FastlyOutboundObservation { + pub fn current() -> Option; + pub fn snapshot(&self) -> FastlyOutboundStats; +} + +impl FastlyOutboundStats { + pub fn registration_count(&self) -> u64; + pub fn configuration_count(&self) -> u64; + pub fn total_registration_duration(&self) -> Duration; + pub fn max_registration_duration(&self) -> Duration; +} +``` + +The observation handle has private fields and supports Clone/Debug/Send/Sync; a snapshot is an +immutable Copy/Clone/Debug/Eq/PartialEq value. Neither has a public constructor, Default, setter +or reset. Install the handle in the request extension bag before prepare runs; handlers obtain it +with `ctx.extensions().get::().cloned()`. `current()` returns a handle +for the active callback or existing helper-fallback scope, and None outside those scopes. Both +paths observe the same inbound-request accounting, without downcasting the type-erased HttpClient. + +Attribute each operation through D1a's current request scope, never through client construction. +Default, router-state and prepare-supplied Fastly clients, including retained clients, contribute +to that same request's record; a supplied non-Fastly client doing no Fastly registration contributes +nothing. Client precedence is unchanged. Count actual provider registration attempts, including +failures and late returns; do not hide retries. Configuration count is the number of distinct +eligible configuration keys encountered by that request, including cache hits and failed attempts. +Unsupported/already-expired preflight performs no registration/configuration accounting. +Total/max duration measure registration calls through return in the paired application clock, +including failed and late successful registration. Observation must not change wire outcomes, +logical budgets, successful-late-registration caching, or the >=50 ms warning rule. + +The handle reads a coherent numeric snapshot without retaining the callback guard, exchanges, +request data or configuration-key set. Callback exit freezes that record and releases its key +set; a retained handle cannot keep work alive, resume recording or attach to the next request. +After response delivery, the still-active outer callback can continue recording post-delivery +work; helper return and response cleanup do not freeze its record. A standalone helper freezes +only its own fallback record. No observation data is added to response headers or public errors. + +### Compile Contract and Acceptance Sentinels + +G4/P1 compile-contract tests must prove object safety, a Send + Sync client, and owned local heads/ +futures/owners that can capture `Rc` without unsafe Send. The eight implementation sites at F are +the four adapters, core `outbound.rs` and `context.rs` test clients, the demo client, and the CLI +template client. Re-run `rg 'impl OutboundHttpClient'` at implementation time; do not trust a stale count. + +Create `crates/edgezero-core/tests/outbound_public_api.rs` with +`public_follow_and_option_readback_compile_contract`. Compile an external supplied client through +`HttpClient::new` and `with_client`, all four request builders/readbacks, effective marker inspection, +the redirect limit/context/actions/result accessors, and a policy owning Rc with a non-Clone error +that implements neither Debug nor Error. Verify the exact owned local-future return type without +adding Send bounds. Run +`scripts/run_test_nonzero.sh public_follow_and_option_readback_compile_contract cargo test -p edgezero-core --test outbound_public_api`. +Expected: missing or unusable public declarations fail compilation before implementation; after +implementation the named test executes and passes without a network or provider SDK. + +Add `batch_producer_uses_prepared_context` to the same external test file and execute it with +`scripts/run_test_nonzero.sh batch_producer_uses_prepared_context cargo test -p edgezero-core --test outbound_public_api`. +Expected: a supplied producer consumes indexed eligible slots once, returns a context-backed +driver with its cancellation owner, and retains exact preflight outcomes. In colocated core ledger +tests also cover cutoff without scheduled work, setup failure after scheduling, earlier recorded +success across late collection, an early ConnectTimeout collected after both governing deadline +and cutoff with its cause unchanged, own-deadline timeout observed after cutoff, invalid cutoff +omitting that timeout, premature EOF, trailing/duplicate events and one release per owner. External +compile-fail doctests reject raw context construction, cloning and removed producer factories. +These tests establish a usable producer surface without publishing ledger receipts or an executor. + +| Sentinel | Expected Behavior | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `head_disposition_precedes_payload_processing` | Followed pending/error/oversized bodies are not polled; final bodies still enforce policy | +| `identity_original_mode_keeps_decoded_cap` | Declared/observed 11-byte identity fails decoded cap 10 under both choices | +| `location_evidence_survives_header_filtering` | Valid plus non-text Location is still duplicate; earlier redirect stop rules win | +| `mixed_batch_collection_preserves_delivery_state` | Check then next then collect distinguishes AlreadyDelivered/Resolved/Unresolved | +| `batch_accumulator_rejects_foreign_or_missing_items` | Merge rejects before mutation and returns ownership of all inputs | +| `batch_recovery_returns_exact_nonclone_inputs` | Rejected insert/merge returns original bodies and driver error; one release per owner, unchanged accumulator | +| `batch_collections_preserve_identity_until_consumed` | Read-only access permits merge; consuming decomposition transfers bodies/error once without reconstruction | +| `begin_settlement_does_not_restart_budget` | Entry snapshot precedes settlement; expiry polls no body or starts no transport | + +Also add these preparation sentinels: + +| Sentinel | Expected Behavior | +| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| `adapter_preflight_precedes_pending_settlement` | Unsupported Spin authority/Axum Image Optimizer returns 400 with zero body polls, timer registrations or sends | +| `settlement_timer_wakes_without_body_data` | Outbound 200 ms expires as 504 while inbound read is pending under a 10 s deadline; read/timer resources release | +| `settlement_uses_paired_fake_time` | Fake clock advancement wakes the timer; early/frozen-clock wake does not cause premature timeout or a busy loop | +| `configured_connect_timeout_reaches_dispatch` | Facade and redirect rebuild pass 1 s through prepared request readback; the aggregate budget is unchanged | +| `image_optimizer_follow_uses_single_send_mapping` | Fastly Send/Follow use one mapping; Begin/Batch and other adapters reject with zero body/provider activity | + +Adapter-local connect sentinels supplement those core tests: Fastly and Spin implement +`connect_timeout_reaches_provider_settings`, observing the actual setter input. Fastly also +implements `connect_expiry_preserves_governing_budget_precedence`, asserting ConnectTimeout +before the governing expiry, its original budget source on equality/late observation, and a +full response-head budget. Run the native mapping/classifier tests with these exact feature +selections; do not enable WASM SDK features in a native test executable: + +```sh +scripts/run_test_nonzero.sh connect_timeout_reaches_provider_settings cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --lib +scripts/run_test_nonzero.sh connect_expiry_preserves_governing_budget_precedence cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --lib +scripts/run_test_nonzero.sh connect_timeout_reaches_provider_settings cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils --lib +``` + +Place native mapping tests in portable policy code with a recording provider boundary. D1/D4 +also execute actual SDK setter/transport tests on their WASM targets with the platform feature +and runner from Q2; a native mapping test cannot stand in for the SDK-resource leg. +These are mapping/classifier proofs, not substitutes for hosted expiry in EZ-4 criterion 8. + +For each sentinel, run `scripts/run_test_nonzero.sh cargo test -p edgezero-core --lib`: +Expected before implementation: the named test fails for the described behavior or missing API. +Expected after implementation: it executes and passes, then the index's per-task test/fmt/clippy +card passes. These are compile/test targets to implement, not claims of current passing tests. + +## Task P1: Shared Head-First Dispatch and Request Options + +**Files:** Modify `crates/edgezero-core/src/outbound.rs`, `error.rs`, `lib.rs`, `time.rs`; +create `crates/edgezero-core/src/outbound/exchange.rs` and `outbound/image_optimizer.rs`; migrate four adapter `src/outbound.rs` +implementations and all test implementations found by `rg 'impl OutboundHttpClient'`. +Create `crates/edgezero-core/tests/outbound_public_api.rs` for the external compile contracts above. +Migrate callers in all four adapter `tests/contract.rs` and Spin `tests/sdk_resources.rs` in the +same atomic cut, along with core/context tests, demo and template recording clients. Discover +callers as well as implementations with `rg 'OutboundHttpClient|start_batch_until' crates examples tests`. +Also search `rg 'OutboundBatch(Item|Results|Failure)|finish_batch_item' crates examples tests` and +migrate item/collection field access, constructors and helper return handling to the approved API. +Search `rg 'OutboundBatch::(cutoff|from_driver|from_futures)' crates examples tests` as well: migrate +all producers directly to context-taking construction and remove the old future/helper factories. +Explicitly include `crates/edgezero-cli/src/generator.rs` literal source assertions in P1, not only +Q2. Its `failure.error`, `slot.elapsed`, clock-field/import and driver-construction expectations +must change with the template API cut. Search those spellings and `handlers.contains` as well as +type names; string assertions do not necessarily contain the implementing type names. +Behavioral integration tests wrap adapters with `HttpClient::with_client` and use its request-vector +API; generic trait-conformance assertions remain trait assertions. Do not add an unchecked public +`OutboundPreparedBatch` constructor or an old-signature compatibility helper to keep tests compiling. + +| Caller File | Migration Owner | +| ------------------------------------------------------ | -------------------------------------------- | +| `crates/edgezero-adapter-axum/tests/contract.rs` | P1 atomic facade cut; D2/D5 runtime coverage | +| `crates/edgezero-adapter-cloudflare/tests/contract.rs` | P1 atomic facade cut; D3/D5 runtime coverage | +| `crates/edgezero-adapter-fastly/tests/contract.rs` | P1 atomic facade cut; D1/D5 runtime coverage | +| `crates/edgezero-adapter-spin/tests/contract.rs` | P1 atomic facade cut; D4/D5 runtime coverage | +| `crates/edgezero-adapter-spin/tests/sdk_resources.rs` | P1 atomic facade cut; D4 SDK execution | + +- [ ] Add portable tests named `head_disposition_precedes_payload_processing`, + `unsupported_options_precede_expired_budget`, and `request_options_round_trip`. + Run `scripts/run_test_nonzero.sh head_disposition_precedes_payload_processing cargo test -p edgezero-core --lib`; + establish the current inability to discard a permanently pending 302 before body processing. +- [ ] Add `batch_recovery_returns_exact_nonclone_inputs` and external compile-fail doctests proving + consumers cannot construct receipts/collections or mutate identity/index/result/slots/termination. + Add `batch_collections_preserve_identity_until_consumed` for external readback, merge eligibility + and consuming body/error access. Cover recovery after + insert, merge-success and merge-failure rejection, including distinct driver and validation + errors and one release for each original body owner. Run the named core sentinel and + `cargo test -p edgezero-core --doc`; Expected: runtime ownership assertions pass only after + implementation and privacy examples fail to compile for the intended reason. Migrate + old constructor/field consumers and every driver in the same hard cut; remove + `finish_batch_item` and `from_futures` rather than retaining alternate producer paths. +- [ ] Before changing public traits, read G4's approved interface digest and G2/G3 artifacts. Stop + if approval is missing, rejected, or refers to a different interface. Implement the Interfaces + contract above; do not choose new public signatures inside this task. +- [ ] Define one required implementer primitive `dispatch_head`, returning status, normalized + headers/provenance, a lazy encoded body, effective budget, response policy inputs, and the + adapter teardown owner. Apply response-header limits before exposing the head; defer declared + body length, decoding, and payload collection until disposition. An owned finalization method + consumes it once using existing shared helpers. Facade `send` uses final disposition; redirect + following uses the same primitive once per hop. No raw response bypass or duplicate client. + Capture Location evidence before filtering and retain it independently from the visible map. + Preserve all existing normalization precedence: original coding and 206 cannot recover + Connection-nominated or UTF-8-filtered fields. Header caps still charge upstream fields + before filtering, and bodyless/205 semantics still precede payload handling. +- [ ] Add the required synchronous support hook and paired clock/timer hook from Interfaces. + Migrate every constructor and context injector in the same hard cut. Add the three preparation + sentinels above and a missing-runtime/timer setup case; prove unsupported options never + register a timer, poll the body or touch the network. Do not rely on dispatch-time checks. +- [ ] Add request choices for response decoding (default decode/original), marker (default on), + optional connect timeout, and optional Fastly Image Optimizer settings. Preserve every existing + field in `into_parts`/`from_parts`; add getters. Use typed phase causes for connect and later + idle expiry using the explicit BudgetSource variants above without putting detail on the wire. Reject zero connect timeout; + clamp durations; preserve existing effective-budget tie ordering and unsupported-first checks. + Add `configured_connect_timeout_reaches_dispatch` as a facade-to-recording-client proof, not + only a parts round-trip. D1/D4 own the corresponding provider-setter and expiry sentinels. +- [ ] Move shared mode/combination validation into one portable helper used by adapters and tests. + Reject Image Optimizer on begin/batch and with bypass/tags before body resolution, budget, + backend registration, or dispatch. Other adapter-specific Unsupported options are checked + by the synchronous pre-settlement support hook. Validate the exact EZ-8 portable Image Optimizer subset at construction: quality bounds, + nonzero dimensions, offset bounds/pairing, and incompatible focus modes. Preserve typed option + readback. Run `cargo test -p edgezero-core` and the four native `test-utils` contracts; migrate + macro/scaffold/demo test implementations rather than leaving an old-trait compatibility path. + Suggested commit: `feat: establish head-first outbound policy dispatch`. + +## Task P2: Bounded GET/HEAD Body Settlement + +**Files:** Modify core `src/outbound.rs`, `context.rs`, `body.rs`; four adapters' `src/request.rs` +and `src/outbound.rs`; Axum `src/connection.rs` tests; adapter `tests/contract.rs`. + +**Engineering sequencing clarification:** the existing known-empty conversion subtask can +proceed independently at the ingress boundary without changing the public dispatch contract. +The Axum-only checkpoint preserves transport-certified `is_end_stream()` as `Body::empty()` +after admission for any method, without polling or consulting headers/size hints for emptiness. +Initial state is not Cached; ingress framing remains `HostManaged`, and core's first +`body_bytes` deadline precedence is unchanged. Unknown streams retain their existing lazy +conversion and read deadlines. Fastly, Cloudflare and Spin remain unchanged. + +Actual HTTP/1 TCP GET/HEAD forwards with absent framing or `Content-Length: 0` pass through +`into_request` even after the inbound read deadline, with no outbound bytes or length/chunked +framing. Chunked-empty and nonempty GET controls still fail with 400 and no origin request, +preserving connection boundaries. Demo and generated proxy handlers benefit through the same +converter and unchanged API. This is not a new approval, capability row/promotion or completion +of P2/EZ-1. The remaining settlement and support-first/paired-time/head hooks stay with atomic P1; +no backend policy, timer, slack, resource or SDK/runtime pin changes are enabled. The Interfaces +section remains unchanged. + +- [ ] Add core tests for streamed empty chunks then EOF, first nonempty byte, failed/taken/cached + bodies, unsupported option with permanently pending input, expired outbound budget with zero + polls, and competing 408/504 limits. Run `cargo test -p edgezero-core --lib` to show failures. +- [ ] Carry a genuinely known-empty body as empty buffered data after admission. Otherwise settle + only GET/HEAD through the first byte or EOF under the original outbound budget and inbound + read deadline. Preserve source error identity and sticky `BodyCell` behavior. Empty chunks must + neither certify nonempty data nor starve timer progress. Batches still reject streamed uploads. +- [ ] Use the paired adapter timer to wake a permanently pending settlement read at the outbound + limit, not only the inbound stream's longer deadline. Test 200 ms versus 10 s, timer/body + readiness races, missing timer context, early wakeup and cancellation with deterministic fake + time in core; repeat through actual Axum/workerd/Spin timers. Fastly records its EZ-1.5 exception + explicitly rather than inheriting a false universal interruption claim. +- [ ] Integrate actual host emptiness evidence from G3, never raw header guessing. Leave ingress + framing `HostManaged`. Nonempty GET/HEAD fails 400 before connection/backend setup; other + methods keep stream acceptance and existing bounded prebuffering. Ensure outbound framing is + derived from the sent body and empty GET/HEAD never uses chunked encoding. +- [ ] Run `cargo test -p edgezero-core` and each adapter contract; add actual ingress forwarding + cases for GET/HEAD/OPTIONS/DELETE/POST. In Axum TCP tests, reject a body and then pipeline a + healthy request; prove close or correct-offset parsing, not reuse of unread bytes. Complete + EZ-1's environment-specific legs via the G1 inventory. Suggested commit: `fix: forward empty inbound GET and HEAD requests`. + +## Task P3: Original Coding, Partial Responses, and Marker Accounting + +**Files:** Modify core `src/outbound.rs` and `compression.rs`, four adapter `src/outbound.rs` +and Cloudflare `src/response.rs`; tests colocated plus four `tests/contract.rs`. + +- [ ] Add table tests over identity/gzip/deflate/br/stacked/unsupported/malformed coding, both + response modes and decoding choices, 206, HEAD, 304, and declared/observed cap boundaries. + Add `original_coding_skips_decoder_but_keeps_transport_caps` as a nonzero sentinel. +- [ ] Add `original_coding_preserves_normalization_precedence` through the actual normalized + head/finalizer: nominated coding/length/representation fields stay absent, invalid ETag stays + dropped, surviving malformed length still fails, and surviving valid fields are preserved. + Include 204/205, HEAD/304 and 206 controls plus identity decoded caps when coding was removed. + Keep `response_normalization_precedence_table` and the identity-cap regression unchanged. + Run the new sentinel with `scripts/run_test_nonzero.sh original_coding_preserves_normalization_precedence cargo test -p edgezero-core --lib`; + Expected red before implementation and nonzero pass after, followed by scoped fmt/clippy. +- [ ] Preserve original payload bytes and representation fields surviving ordinary normalization + in original mode; never restore stripped coding/length/metadata or bypass security filtering. Do not + allocate a decoder for an encoded passthrough or apply its decoded cap. Identity still receives + declared-length and streamed decoded-cap enforcement under both choices. Test identity length + 11 against decoded cap 10 and encoded/final caps 20, plus an exact-cap success, through the + actual shared response-policy path. Always preserve 206 coding. Enforce encoded and + final limits as applicable, bodyless disposition first, and retain Brotli-configuration validation, + chunk shaping, normalization, immutable Content-Type provenance, and typed cap/decode failures. +- [ ] Add a client-wide marker setting and a per-request tri-state override (inherit/on/off), with + effective-policy inspection before dispatch. Resolve the client setting on all send modes and + redirect requests before handing them to a recording client. Keep the existing default on unless + an explicitly approved requirement revision changes it; EZ-7.5 fixes on for this plan and the + application configures its relay client off. Do not leave the default undecided or keep dual APIs. + Implement opt-out without consuming synthetic header budget when omitted, preserve an upstream + marker field unchanged when off, and retain pre-normalization upstream accounting. Get adapter identity + through existing out-of-band metadata if adopted, not a new required response header. Do not + change user-supplied `Accept-Encoding`; absent still becomes identity. +- [ ] Run core/adapter tests and actual encoded relay cases, especially workerd manual egress and + Spin raw-host characterization. P3 presents separate EZ-5.9/5.10 adoption decisions for + maintainer attribution in G1/G4 before implementation. If adopted, add actual finalizer/adapter + tests for HEAD/304 removal of recognized coding/encoded length and for decoded strong-ETag + weakening plus Accept-Ranges/Content-Digest removal, respectively. Implement only the approved + metadata change; original coding and prior normalization still win. If deferred, retain baseline + decoding behavior and record the deferral rather than silently applying the SHOULD. Include + named sentinels `decoding_bodyless_metadata_follows_adopted_policy` and + `decoded_representation_metadata_follows_adopted_policy` only for their adopted parts, with + weak-ETag, nondecoded, original-mode and 206 controls. Execute the applicable core/native/target + sentinels and activate them in Q2 CI. Run decoder audits. Suggested + commit: `feat: support original response coding and proxy marker policy`. + +## Task P4: Observation API and Arrival Ledger + +**Files:** Create core `src/outbound/progress.rs`; modify `src/outbound.rs`, `time.rs`, and +`lib.rs`; tests colocated. Adapter driver implementation is D1-D4. + +- [ ] Add deterministic tests for recorded arrival versus collection, equality at cutoff, own + deadline before cutoff, equal deadline/cutoff, input-index ties, late `next`, and repeated terminal + observations. Keep every malformed-driver and failure-with-partials regression. + Add the mixed check/next/collect success, cutoff and failure cases and matching/missing/ + foreign/duplicate accumulator cases from the Interfaces block; do not weaken at-most-once ownership. +- [ ] Add an owned begun-request handle with async collection and a nonblocking ready check; + `begin` may await GET/HEAD settlement but not connect/head receipt. Keep `start_batch_until` + synchronous: it validates and hands eligible buffered slots to a verified adapter scheduler or + Fastly pending handle before returning. No lazy-first-poll driver may masquerade as eager. +- [ ] Add a nonblocking batch observation result that distinguishes no-new-result from terminal + completion; route it and `next` through the same validation/poisoning ledger. Store recorded + terminal instants privately with input indices and original budget provenance. Sort observation + by arrival then input index; collection remains input-indexed. Preserve completed outcomes + before cutoff/failure; cancellation reports every not-yet-delivered index. Replace ambiguous + Option slots with the specified collected-slot states in both success and failure results. +- [ ] Run `cargo test -p edgezero-core` and a cancellation-safety test that polls then drops `next` + without consuming an item. A response at cutoff is unresolved; own-deadline timeout before + cutoff remains deliverable. No public terminal metadata is invented before actual observation. + Suggested commit: `feat: define eager exchange and nonblocking batch observation`. +- [ ] Add `arrival_ledger_preserves_late_collection_and_cancel_safety` and run + `scripts/run_test_nonzero.sh arrival_ledger_preserves_late_collection_and_cancel_safety cargo test -p edgezero-core --features test-utils --lib` + before/after implementation. Expected: a real red assertion for missing eager/arrival behavior, + then strict cutoff equality, preserved earlier arrivals, mixed ownership states and cancelled + pending `next` all pass without wall-clock sleeps. Apply the index's task lint card. + +## Task P5: Redirect Following Through the Same Client + +**Files:** Create core `src/outbound/redirect.rs`; modify `src/outbound.rs` and `lib.rs`; +test in that module and each adapter `tests/contract.rs`. + +- [ ] Write the complete EZ-9 status/method/body decision table and precedence tests with a + recording head-first implementation. Include a followed 302 whose body is permanently pending, + malformed compressed bytes, oversized declared length, or a read error; first demonstrate the + missing policy path with `cargo test -p edgezero-core --lib`. +- [ ] Add explicit following with validated hop limit and a synchronous application target policy. + Plain send/begin/batch never follow. Return final URI, hop count, typed stop reason, or unchanged + hop/policy error with its index. Resolve relative Location and fragments with the pinned parser; + enforce the exact EZ-9.18 decision order, duplicate-Location rule, and non-HTTP policy case. +- [ ] Clone only buffered replay bytes. Settle inbound GET/HEAD once. Drop content headers and + Expect on rewrites; strip only the specified credentials/override on cross-origin prepared + requests. Run policy before the next network action and never strip its deliberately replaced + credentials again. Copy options on prepared requests; fresh policy requests use their own + defaults. Retain the initial absolute chain deadline independently through final body consumption. +- [ ] Consume or discard each head once. Followed bodies are dropped without polling by default; + do not add optional drains until their nonblocking work bound is independently proved. Final + 3xx receives ordinary body policy. Run `cargo test -p edgezero-core` and adapter no-redirect/ + followed-response parity cases. Capture logs and prove query tokens never appear. Suggested + commit: `feat: follow redirects with per-hop application policy`. +- [ ] Add `redirect_chain_uses_supplied_head_client_and_original_deadline`; run + `scripts/run_test_nonzero.sh redirect_chain_uses_supplied_head_client_and_original_deadline cargo test -p edgezero-core --features test-utils --lib`. + Expected: red for missing following, then one supplied-client dispatch per hop, no polling of + followed bodies, unchanged typed policy/hop errors and an unextended original chain deadline. + Include each stop reason and normalized Location controls; apply the task lint card. + +## Task P6: Published Portable Scripted Client, Only If Promoted + +**Files:** Create core `src/outbound/testing.rs`; modify core `Cargo.toml`, `src/lib.rs`, and +`src/outbound.rs`; add CI activation in qualification Q2. Otherwise keep this harness crate-private. + +- [ ] Record G1's decision before exporting EZ-24.1-24.10 for host/WASI. Do not implicitly adopt + JavaScript support, script-abandonment reporting, or template replacement. Add deterministic + fixtures using `json!` and an explicitly advanced shared `MonotonicClock`, without wall time. + P6 depends on P1-P5 and their implemented semantics; its promotion never makes production + acceptance depend on the published double. Use the private fixture harness before P6 exists. +- [ ] Record every effective request field/body, send mode, batch/index, dispatch time, and budget. + Use the portable validation/head/finalization helpers, not an adapter capability emulator. Accept + otherwise-valid single Image Optimizer/authority settings; reject universally invalid combinations + and modes before consuming a script. No answer must end under the selected deadline. +- [ ] Script head and chunk times separately, errors, caps, and a failed driver after one item. + Prove `next` indices 1/2/0 at 10/20/30 ms versus `collect` indices 0/1/2, 99 ms kept versus + 100 ms cutoff unresolved, late-check retention, origin/batch overlap, and pending redirect-body + bypass. Unscripted requests fail without panic; recordings remain accessible after injection. +- [ ] Add `scripted_replies_preserve_cutoff_and_arrival_order`; run + `scripts/run_test_nonzero.sh scripted_replies_preserve_cutoff_and_arrival_order cargo test -p edgezero-core --features test-utils --lib` + and `CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh scripted_replies_preserve_cutoff_and_arrival_order cargo test --locked -p edgezero-core --features test-utils --target wasm32-wasip1 --lib scripted_replies_preserve_cutoff_and_arrival_order`. + Expected: red for missing published utility, then nonzero host/WASI passes with 1/2/0 arrival + order, positional collection and strict cutoff boundary using paired fake time. JS legs run + only if adopted. The explicit WASI test filter is required: the nonzero helper verifies + presence but does not filter, and unrelated native thread tests cannot execute under WASIp1. + Apply the task lint card. Document portable + conformance versus real provider behavior. Suggested commit: `test: publish deterministic outbound scripts`. + +## Phase Exit + +- [ ] Run the core commands from the index's per-task card, `cargo test -p edgezero-core --doc`, + and the applicable named sentinels above, then Q2's seven explicit feature-matrix commands. + Expected: nonzero runtime cases, intended privacy compile failures and all caller builds pass; + no unchecked constructor or compatibility implementation was needed to keep tests compiling. +- [ ] Core tests preserve all baseline invariants; all in-tree clients compile with the new protocol. +- [ ] No new API has an unimplemented default that silently drops an option or defers dispatch. +- [ ] Adapter integration and actual ingress/host evidence are completed in phase 3, not inferred + from these portable tests. No intermediate contract-breaking revision is merge-ready. diff --git a/docs/superpowers/plans/2026-10-18-trusted-server-outbound-03-drivers.md b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-03-drivers.md new file mode 100644 index 000000000..d813cdc99 --- /dev/null +++ b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-03-drivers.md @@ -0,0 +1,523 @@ +# Adapter Outbound Progress Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make begun requests and batches progress eagerly, enforce bounded Fastly waits, and preserve arrival-time results and resource ownership on all four adapters. + +**Architecture:** Use each adapter's existing transport and scheduling model with the shared policies/ledger from phase 2. Fastly gets one cooperative timed multiplexer; native Axum uses its established local executor, workerd uses host-local scheduling, and Spin uses WASIp3 spawning. Callback and response-owned exchanges have separate cancellation owners even when they share a client. + +**Tech Stack:** Existing adapter features and target runners, Fastly async I/O handles, reqwest/Axum local runtime, worker fetch/AbortController, Spin SDK/WASI HTTP 0.3. + +--- + +Dependencies: P1/P4 contracts; P2/P3/P5 integration accompanies each driver. G2 must pass before +depending on Fastly timed reads. Do not introduce a background reader that buffers outside caps. +Apply the index's scoped test/fmt/clippy card after each implementation increment, not only at +phase completion. The target checks below supplement, not replace, all five root CI gates. + +**Scoped precedence, 2026-10-24:** the requester's [timing and native integration amendment](../../audits/2026-10-18-outbound-api-approval.md#scoped-timing-and-native-integration-amendment) +permits the Send-first checkpoint below before full D1a-D1d sequencing. Missing finite H(K) and +timing-only G2 failures do not block that subset. Original deadlines and late-result rejection, ++5 ms measured targets/tests, typed body errors and fresh-readiness/subsequent-read safety remain. +The optimizer blocking horizon is **UNKNOWN** until provider evidence. At that checkpoint, +B/backend policy and timers were unapproved. The later scoped B31 approval permits the existing +aggregate identity/timer subset described in D1b below; slack, resources and the readiness engine +retain their separate gates. +The current trait remains unchanged until the later atomic head-first/full-trait migration. + +Each D1-D4 migration implements plan 02's synchronous `validate_supported_options` and paired +`time()` hooks as well as dispatch. Replace clock-only constructors per plan 02's migration table, +preserving Axum's fallible `try_with_time` and transport-sharing setup, and migrate request-context +injection. Reuse the adapter's own timer facility (native runtime, +Workers Delay, Spin sleep, or G2's verified Fastly cooperative wait). Test unsupported options with +a pending inbound body and zero timer/body/transport activity; on Axum/workerd/Spin also test a +200 ms outbound settlement timeout against an otherwise permanently pending 10 s inbound read. +Fastly's EZ-1.5 blocked-inbound-read exception stays explicit; no inert/default timer or core runtime +is an alternative. Fake-clock tests install a paired wakeable timer, not only a timestamp callback. + +## D1: Fastly Driver Task Group + +Execute D1a-D1d serially; D1 closes only when all four close. Each task has its own red/green +cycle and buildable commit. The public trait migration remains atomic across all adapters; these +internal slices do not authorize merging an incomplete public implementation or keeping old APIs. + +### Task D1a: Bounded Multiplexer and Callback Ownership + +#### Current-Operation Implementation Slice (2026-10-25) + +The requester's [scoped multiplexer approval](../../audits/2026-10-18-outbound-api-approval.md#scoped-fastly-current-operation-multiplexer-approval-2026-10-25) +authorizes the existing Send/Batch receive-path replacement, fresh matching readiness, a 16 KiB +raw read maximum and rotating slot service under B31 and the original deadlines/cutoffs/caps. +Local/CI explicitly selects the tracked Viceroy repair; installed pins and hosted promotion stay +unchanged. Keep cached Send heads opaque rather than converting them to raw handles and changing +cache policy. Poll them nonblockingly with one shared positive 1 ms pulse. + +Native deterministic scheduling tests and actual-client fixtures accompany this slice. D1a stays +open: this per-operation engine does not yet supply callback-wide ownership, progress during +unrelated operations or the eager public modes. Full P1/P4 and D1c/D1d integration, slack retirement, +resource evidence and hosted qualification remain separately tracked. No public trait or frozen +Interfaces/requirements change, acceptance-leg promotion or capability upgrade is implied. + +#### Existing-Batch Scope Checkpoint (2026-10-26) + +The [live batch ownership slice](2026-10-18-trusted-server-outbound-implementation.md#existing-batch-scope-checkpoint-2026-10-26) +adds fresh callback guards, helper fallback/reuse and separate transmission scopes. Private weak +registrations revoke whole current batch drivers, not only their progress slots. Retained handles +fail with typed Internal closure and preserve already-collected results; they do not become Cutoff. +Registry cleanup does not poll or drain work. D1a remains open for retained Send roots, transfer +ownership, callback-wide scheduling and the atomic eager/head-first modes. Current-operation +progress, B31, slack, original budgets, pins and capability disclosures are unchanged. No acceptance +leg or hosted qualification is promoted by the new local ownership regressions. + +#### Mixed-Phase Prerequisite Checkpoint (2026-10-25) + +The existing SDK/ABI diagnostic now appends eighteen separately validated probes: a ready +multi-buffer body alongside a permanently pending head, a delayed head alongside a pending body, +and a truncated multi-buffer body. Buffered and pull-streamed consumers run three repetitions at +16 KiB capacity. Require actual mixed domains, full-capacity reads, original deadlines, exact +payload checksums, fresh matching readiness and terminal source errors. Preserve the original +135-probe matrix and its failed historical replays. No production multiplexer, read maximum, +runtime-pin change or capability is approved by this experiment; continuously-ready multi-source +fairness and full D1a integration remain open. + +**Files:** Modify `crates/edgezero-adapter-fastly/src/outbound.rs`; create +`src/outbound/progress.rs` and `src/outbound/backend.rs` only for the progress engine and backend +registry extracted by this change; update `src/lib.rs`, `src/request.rs`, `src/response.rs`, +`src/lifecycle.rs`, `tests/contract.rs`, and colocated lifecycle tests. The existing-batch scope +checkpoint extracts only its private ownership registry into `src/outbound/scope.rs`. +Callback ownership is +blocking EZ-6 work here, not deferred until F1/F6 reuse or delivery-report changes. + +**Dependencies:** P1/P4, passing local G2, G3 and G4's API/Fastly-policy approval. Keep existing +backend identity and slack until D1b replaces them together; no new blocking-read fallback. + +- [ ] Read G2's feasibility report and G4's API/Fastly-policy approval. Check source/SDK/interface + digests and the literal configuration bound B. Adopt exactly the reported timed-read sequence, + readiness predicates and maximum read size. If any dependency is failed/missing/stale, follow + G2's cancelled/deferred map; do not implement a guessed host primitive or remove slack. +- [ ] Add tests for one active slot plus a trickling sibling, two ready bodies, earliest slot + deadline, cutoff with a permanently pending body, handle reassociation failures, and cleanup. + Assert exact indices/reasons and preserved partial results; do not use preflight-only cases as + harvest coverage. Run the Fastly native `test-utils` contract suite to demonstrate failures. + Name the sibling case `fastly_multiplexer_preserves_sibling_progress` and run it with + `scripts/run_test_nonzero.sh fastly_multiplexer_preserves_sibling_progress cargo test -p edgezero-adapter-fastly --features test-utils`. + Expected red before implementation; after implementation, real indexed outcomes and owner + counts pass. Repeat the timed-read fixture via G2, then run the index's task lint card. +- [ ] Use G2's verified timed-read primitives for header readiness, body readiness, and bounded + reads. Wait on all active request-lifetime exchanges and the earliest relevant timer. Bound + bytes/work per turn; yield between ready sources. While an origin read or ordinary send waits, + keep harvesting bidder progress; never enter an uninterruptible response-body read. +- [ ] Keep transport resources in an owned registry with response/callback ownership tokens; + no path/query/header/body survives request lifetime. A response body retains its exchanges + after handoff; callback-owned work survives earlier response cleanup. Drop/cancel removes + only the corresponding exchanges and retains precise SDK-selection failures as driver errors. +- [ ] Install one fresh adapter-private RAII callback guard in `Sandbox::handle`, immediately + around the handler invocation shared by `run_custom` and every `serve_custom` callback. + Bind scope membership per operation, not at client construction; saved clients must resolve + the current scope. Callback exit (Ok, Err, or supported unwinding) releases callback-owned + work exactly once even when clients/handles remain retained elsewhere. Do not retain the + guard in sandbox state or wait for sandbox destruction. Preserve the original return/error + and existing no-synthetic-send behavior. Response-owned work has its separate egress owner. +- [ ] Standalone canonical send helpers and `FastlyService::send_request_with_hooks` establish + a helper-local fallback scope only if no outer callback scope exists. Under custom wrappers + they reuse the outer guard: helper return and response completion cannot cancel post-delivery + callback work. `response.rs` ends only the response-owned scope. New exchanges and completed + streamed-body transfers keep the existing exactly-once teardown owner. +- [ ] Add `callback_scope_exit_releases_retained_exchange` and + `retained_client_resolves_fresh_callback_scope` in lifecycle/contract tests. Cover retained + pending handles, Ok/Err/pre-egress setup failure, supported unwinding, response cleanup while + same-client callback work is live, and post-delivery new work. Unit-simulate two invocations + on the same sandbox/client even when hosted reuse is disabled; previous scope handles cannot + restart or deliver old work. Actual next-request reuse qualification remains conditional. + Run `scripts/run_test_nonzero.sh callback_scope_exit_releases_retained_exchange cargo test -p edgezero-adapter-fastly --lib` + and the actual Viceroy sentinel; Expected: no early cancellation, no extra send, exactly one + release at callback exit, and no reuse of a stale scope. Apply the task fmt/clippy card. + Suggested commit: `feat: multiplex bounded Fastly exchanges with scoped ownership`. + +### Task D1b: Stable Backend Identity and Slack Removal + +**Files:** Fastly `src/outbound.rs`, `src/outbound/backend.rs`, `src/context.rs`, +`src/request.rs`, `src/lifecycle.rs`, `tests/contract.rs`, and fixture Fastly qualification cases; +create `tests/outbound_public_api.rs` for the external observation compile contract. + +**Dependencies:** D1a's verified bounded waits and G4's literal B/domain approval. + +#### Existing-Path B31 Checkpoint (2026-10-25) + +The [scoped B31 approval](../../audits/2026-10-18-outbound-api-approval.md#scoped-b31-implementation-approval) +permits the aggregate-policy backend portion on the current Send/Batch transport before D1a. +Configured timeout, not shortened remaining budget, selects the clamped B31 anchor. All three +provider timers use the same ceiling-rounded value; the original logical budget stays unchanged. +Replace per-client namespaces/caches with one verified guest-instance registry and versioned +configuration names. Compare full identities, serialize lookup/registration/insertion, retain late +successful registrations and fail closed on unknown `NameInUse`. No public API or body clone is +needed; request parts carry the existing timeout through the same preparation path. + +The real WASI regressions cover all 93 boundary samples, equal timeouts under different deadlines +and cutoffs, maximum clamps, native cross-client reuse, collisions, failed retries and late-cache +retention. This implements only the current aggregate identity/timer subset, not all D1b modes or +acceptance criteria. Registration observation, explicit Connect/idle mapping, retirement, the +readiness engine, slack removal and hosted/resource qualification remain open. The existing two +dispatch guards and their tests remain, not an alternate old backend policy. + +- [ ] Add `backend_identity_ignores_remaining_budget` for equal per-call timeouts under different + absolute deadlines, with negative controls for connect/TLS/authority changes. Run + `scripts/run_test_nonzero.sh backend_identity_ignores_remaining_budget cargo test -p edgezero-adapter-fastly --features test-utils`. + Expected red for exact-remaining-budget identity at F; after implementation the equal keys + and distinct controls pass without changing logical deadlines or the approved policy domain. +- [ ] Replace client/exact-budget backend names with deterministic versioned configuration keys. + Include origin, authority and TLS/phase settings; omit request/client/sandbox identifiers. + Equal per-call timeouts remain eligible under differing remaining budgets. Deadline-only + configurations use the G4-approved policy and literal maximum B; its documented timer + boundaries/cardinality proof must cover all valid budgets without shortening a deadline. + Keep explicit connect/idle limits distinct from aggregate deadlines; Image Optimizer is not + an identity discriminator. Detect inconsistent name reuse; never trust `NameInUse` blindly. +- [ ] Add request-local registration count/config-count/total/max duration and >=50 ms warning. + Implement plan 02's G4-approved `FastlyOutboundObservation` and `FastlyOutboundStats` contract. + Install the extension handle before prepare, bind registration accounting per operation to + D1a's scope, and keep the read-only handle independent of cleanup ownership. Freeze numeric + accounting on callback/helper-fallback exit, not response completion or client drop. + Charge deadline from original call entry, retain a successfully registered backend even if + registration returns late, and return attributed 504 without sending an expired slot. Remove + slack guards, constant, hook, and tests only together with verified bounded waits. Preserve + router-state > prepare-hook > default-client precedence and cross-request Axum pooling. +- [ ] Add `registration_observation_public_api` in the new external test file; compile every + getter plus Clone/Send/Sync handle bounds and Copy/equality snapshot bounds without private + constructors or a Fastly SDK dependency. Run + `scripts/run_test_nonzero.sh registration_observation_public_api cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --test outbound_public_api`. + Expected: missing/unusable declarations fail compilation, then a nonzero native pass. +- [ ] Add `request_registration_observation_follows_scope` to native contract tests and repeat + real provider accounting on the Q2 WASI/fixture legs. Run the sentinel using the index's + exact Fastly native feature card. Expected: default, router-state and prepare injection see + the same request's counters; same-key cache hits, failed/late registrations and retries keep + the specified count/durations. Check zero-activity preflight, post-delivery work, helper reuse, + Ok/Err/unwind exit and retained handle/client isolation across two callbacks. Retaining a + handle retains only its frozen numbers, not an old exchange or configuration-key set. +- [ ] Add `registration_warning_obeys_fifty_millisecond_boundary`; run + `scripts/run_test_nonzero.sh registration_warning_obeys_fifty_millisecond_boundary cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --lib`. + Use an injected clock and recording logger for just below 50 ms, exactly 50 ms and above, + including a failed registration. Expected: no warning below the boundary, one warn record + at/above it, correct request-local count/total/max observations, and unchanged wire errors. + This is EZ-2.13's acceptance proof, not a hosted registration-latency threshold. +- [ ] Run native backend/registration contracts and the real G2 readiness suite after removing + slack; deadline, cutoff and sibling-progress sentinels must still execute nonzero. Apply the + task fmt/clippy card. Suggested commit: `feat: stabilize Fastly backend identity and remove proved slack`. + +### Task D1c: Connect Timeout Mapping + +**Files:** Fastly `src/outbound.rs`, `src/outbound/backend.rs`, `tests/contract.rs`, +and fixture qualification/hosted probe cases from G2. + +**Dependencies:** D1b and P1/P5; independent of Image Optimizer horizon/entitlement approval. + +- [ ] Add `connect_timeout_reaches_provider_settings` and + `connect_expiry_preserves_governing_budget_precedence` from plan 02. Exercise Send/Begin/Batch + and permitted Follow hops; observe the actual platform mapping, full response-head budget, + ConnectTimeout before the governing expiry, and its original source at equality/late observation. + Run each via `scripts/run_test_nonzero.sh cargo test -p edgezero-adapter-fastly --features test-utils`. + Expected red for F's split/missing option, then nonzero pass. Hosted EZ-4 criterion 8 separately + proves real expiry; Viceroy setter/classifier tests do not establish that provider guarantee. +- [ ] Implement Fastly connect timeout without a fixed connect/first-byte split. Run native contracts + and `CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh connect_timeout_reaches_provider_settings cargo test --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --lib`. + Expected: actual mapping assertions execute and the 900 ms head under a 1 s budget succeeds. + Keep hosted connect expiry pending until Q4 returns evidence. Apply the task lint card. + Suggested commit: `feat: map explicit Fastly connect timeouts`. + +### Task D1d: Image Optimizer Single-Send Mapping and Horizon + +**Files:** Fastly `src/outbound.rs`, `src/outbound/backend.rs`, `tests/contract.rs`, +and fixture qualification/hosted probe cases from G2. + +#### Send-First Checkpoint (2026-10-24) + +This is an implementation sequencing decision under the [scoped integration authorization](../../audits/2026-10-18-outbound-api-approval.md#scoped-timing-and-native-integration-amendment), +not a claim that the requester explicitly approved different signatures. Its scope is bounded; +the synchronous optimizer wall time is not. Do not wait for full D1c/P3/P5, finite H(K) or a +timing-only G2 pass to implement this subset. Do not mark full D1d or its acceptance legs complete. + +**Timing anchor:** the current trait's production async `send` selects its budget at existing +adapter method entry on first poll, not at facade invocation. Preserve that selected budget before +SDK preparation/registration and across the native call. Facade-invocation capture remains pending +with P1's later atomic head-first/full-trait migration; this checkpoint does not complete P1. + +**Checkpoint files:** core `outbound.rs`, `outbound/image_optimizer.rs` and the external request +API tests; each adapter's existing outbound validation/dispatch and contract tests; Fastly's +direct SDK mapping. Do not change `src/outbound/backend.rs`, backend policy, phase timers or slack. + +- [x] Attach existing typed optimizer values to `OutboundRequest`/parts with builder/readback and + lossless `into_parts`/`from_parts` round trips. Keep `OutboundHttpClient`'s current trait + unchanged. Add no default, alias, compatibility helper or alternative transport. +- [x] Validate support first in all four adapters before budget rejection, body polling, backend + registration or dispatch. Axum/Cloudflare/Spin reject options; Fastly Batch rejects only its + option-bearing slot and allows siblings to proceed. Only Fastly single Send is integrated + here; Begin, Follow and fire-and-forget remain outside this checkpoint, not silently enabled. + Preserve method/body/default-cache restrictions; do not claim full EZ-1 settlement is done. +- [x] Map typed values directly to native Fastly SDK options in the existing `execute` single-Send + branch and use existing `process_response`. Leave the ordinary SDK send branch unchanged. + Preserve built targets/signature-bearing headers, header/body/decoder limits, lazy streamed + bodies and exactly-once cleanup. No extra fetch, option serialization layer or prebuffering. +- [x] Keep the budget selected at existing adapter method entry on first poll, including its + `t0`, `D` and `BudgetSource`, through SDK preparation/registration and host return. Do not + claim facade-invocation capture. Reject + observed pre-send expiry without dispatch. At/after `D`, discard host success and classify + host failure as the original attributed 504. Before `D`, map SDK + `SendErrorCause::ImageOptimizerUnsupported` to a 502 with + `BadGatewayReason::ImageOptimizerUnavailable` on core's non-exhaustive reason enum. Preserve + the existing message `Fastly outbound request failed` exactly; only the typed reason and + provider detail distinguish this cause, without provider-text parsing. State blocking horizon + **UNKNOWN**; infer no finite bound from SDK timers or an unsupported Viceroy response. +- [x] Demonstrate nonzero red/green tests for attachment/parts round trips, support-first rejection + with expired budgets and unread bodies, batch sibling isolation, direct typed SDK mapping, + `BadGatewayReason::ImageOptimizerUnavailable` 502 versus late-504 precedence, retained provider + detail, lazy response limits and exact cleanup. Run the + index's scoped test/fmt/clippy card and actual applicable WASM contracts. Typed body-error + preservation and fresh-readiness/subsequent-read safety remain requirements; no lost-error + EOF conversion or blocking-body fallback is permitted. Retain +5 ms targets and failed traces. + +**Local evidence boundary:** core request/parts option readbacks and exports are stable. Static +inspection confirms that optimizer options select native SDK `.send`, not `.send_async`; this is +not runtime host-entry evidence. `image_optimizer_unavailable_is_typed_on_viceroy` proves the option +is not dropped and unavailable errors are typed, but cannot prove host entry: SDK `.send_async` +can return the same `ImageOptimizerUnsupported` without entering the host. Successful shared-response +cap/laziness checks use synthetic successful SDK responses, not hosted image transformation. +Neither test category proves runtime optimizer host entry or hosted transformation; broader +verification and qualification remain separate. + +Full EZ-1 settlement, Follow, B/backend policy, the new engine and head-first/full-trait migration +remain open. The later trait cut still migrates every implementer/caller atomically, not through +compatibility paths. Hosted/performance qualification, numerical horizon/policy approval and +deployment remain blocked. This checkpoint asserts no optimized hosted result or performance parity. + +#### Remaining Full D1d Work + +The dependencies and earlier prerequisite statements below apply to completing full D1d, not to +the Send-first checkpoint. Read finite-horizon and G2 timing language under the scoped override; +provider evidence/numerical approval remain qualification gates, while B and read-safety gates persist. + +**Dependencies:** D1c, P3/P5 and G4's separately approved EZ-8.10 horizon. Missing approval stops +this task; missing hosted entitlement leaves its production qualification blocking, not passed. + +**Native-parity scope approved by the requester, 2026-10-24:** use generic typed request options +and Fastly's native optimizer, retaining the existing outbound pipeline. The source inspection +used TS `f9281917a7260b723f5f22175911c6d2147ef650`; its optimizer DTO/profile/adapter files match +the `a9037770a` baseline. This inspection is not a new qualification baseline or a passing run. +The approval supplies neither numerical H(K)/B nor performance thresholds, entitlement or +deployment authorization. Runtime integration still waits for D1c/P3/P5, G2 and numerical B/H(K) +approval. Measurement approval and staging authorization separately gate hosted comparison. + +- [ ] Recheck D1c/P3/P5, the current attributed API approval plus scoped amendments, passing G2 + and the separately approved numerical H(K)/B records. Stop before production changes if + any prerequisite is missing. Preserve all baseline outbound fields and behavior; no old-trait + helper, separate image client, ignored options or software-optimization fallback is allowed. +- [ ] At the atomic request/driver cut, add `image_optimizer_options_round_trip` in core + `outbound/image_optimizer.rs` and the external public API test. Exercise all selectors, + query preservation, replacement, `into_parts`/`from_parts` and prepared redirect transfer. + Run `scripts/run_test_nonzero.sh image_optimizer_options_round_trip cargo test --locked -p edgezero-core`. + Expected: missing request attachment is red, then exact option readback passes without + changing signed targets/headers. Leave query/profile parsing and aliases in application + configuration; validate a missing required region before SDK serialization rather than + inventing a default or letting an SDK-built invalid option reach the host. +- [ ] Put the direct SDK value mapping in Fastly `src/outbound/image_optimizer.rs`, called from + the existing Send/Follow dispatch, not a second transport. Add colocated + `image_optimizer_native_mapping_matches_sdk` cases for seven regions, eight formats, + five filters, quality boundaries, pixel dimensions, all crop modes and explicit false/true + query preservation. Run + `scripts/run_test_nonzero.sh image_optimizer_native_mapping_matches_sdk cargo test --locked -p edgezero-adapter-fastly --features fastly,test-utils`. + Expected: missing mapping is red, then exact SDK-field assertions pass. Enum matches and + numeric widening must not allocate a string/JSON/query intermediate; SDK-internal + serialization remains the SDK's work. The non-optimizer dispatch branch does not construct + or clone optimizer options. +- [ ] Add `image_optimizer_stream_is_lazy_and_releases_once` and + `image_optimizer_backend_identity_matches_plain_send` at the scripted provider boundary. + Before first application body polling, assert zero payload reads, one send and unchanged + transport configuration identity; after polling, assert bounded chunks, existing caps, + typed errors and exactly-once release on completion/drop/failure. Cover HEAD/204/205/304 + suppression and a permanently pending/truncated stream. Run each via + `scripts/run_test_nonzero.sh cargo test --locked -p edgezero-adapter-fastly --features fastly,test-utils`. + Scripted identity/count proofs are not hosted pooling or performance evidence. +- [ ] Keep TS-specific policy outside core: profiles, SVG selection, query stripping before + signing, SigV4 and conditional S3 HEAD/raw-error GET stay with the application. Add no + automatic HEAD/retry/fetch. Update the generic attachment/streaming guide and applicable + demo/generated examples only with the working request API, and compile them in the same + hard migration. Do not present an unsupported target as serving optimized images. + +- [ ] Add `image_optimizer_follow_uses_single_send_mapping` at the Fastly boundary and run it + with `scripts/run_test_nonzero.sh image_optimizer_follow_uses_single_send_mapping cargo test -p edgezero-adapter-fastly --features test-utils`. + Expected red for missing mapping, then Send/Follow preserve options and Begin/Batch reject + before provider/body work. Implement Image Optimizer single-send mapping. Use the typed + entitlement reason even if EZ-16 is deferred. Preserve the exact built URI/signature-bearing + headers at the provider boundary; host query transformation is separately characterized. + Image Optimizer body handling remains lazy and uses shared policies after its response head. +- [ ] Before Image Optimizer integration, read G4's separate approved EZ-8.10 horizon contract. + Implement H(K) lookup from the actual stable backend policy, not remaining-budget-derived + identity or an assumed two-timer sum. Add `image_optimizer_horizon_preserves_original_deadline` + with nanosecond/millisecond boundaries, equal timeout/different deadlines, late preparation, + host success/error at D, and post-head pending reads. Report original-entry, preparation and + host-call timings independently; a pre-dispatch timeout is not evidence of host-call boundedness. + Run the named sentinel under native scripted-host tests and actual hosted qualification; + Viceroy's entitlement rejection is not a successful Image Optimizer horizon test. +- [ ] Run native contracts and the actual WASI contracts: + `CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh backend_creation_error_table_is_exhaustive cargo test --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --lib`. + Add equivalent nonzero sentinels for successful dispatch/harvest and `outbound-readiness`. + Complete EZ-2/3/4/6/8 loopback legs; hosted gates remain explicitly pending until run. + Hosted EZ-2 criterion 14b must prove fresh-execution connection reuse using origin connection + IDs and an SDK-only control. Registration counts, same POP and resumed TLS alone do not prove + pooling. Keep existing M1 handshake-rate measurement and M4's separate reuse-mode scope. + Apply the task fmt/clippy card. Suggested commit: `feat: map Fastly optimized single sends with approved horizon`. + +## Task D2: Native Axum Eager Exchanges + +**Files:** Modify `crates/edgezero-adapter-axum/src/outbound.rs`, `request.rs`, `service.rs`, +`response.rs`, `dev_server.rs`; tests in `tests/contract.rs` and colocated modules. + +- [ ] Add `begin_dispatches_without_collection` and `unpolled_batch_dispatches_all_slots` using + loopback origins plus the existing local runtime. Include a missing-runtime-context case that + returns a typed setup error before network activity rather than panicking. +- [ ] Install an adapter-owned local scheduling context through the existing `LocalSet` serving + path and explicit manual-client construction. Spawn local exchange work before returning a + batch/begun handle; do not use `tokio::spawn` with core's `!Send` body/futures or unsafe Send + wrappers. Return response data through bounded local state while preserving shared reqwest + cross-request pooling and caller-supplied client precedence. +- [ ] Record buffered completion/cap/error at the actual observed terminal event and streamed + begin at the head. Race reads against deadlines, retain successful early arrivals for late + collection, enforce strict cutoff equality, and use shared head-first disposition. Dropping a + handle cancels its task and transport chain; detached runtime work must not retain a client cycle. +- [ ] Integrate distinct callback/body cleanup with `AxumEgressBody` and disconnect/write/source + terminal paths. Run `cargo test -p edgezero-adapter-axum --features axum,test-utils` plus TCP + forwarding/cancellation cases. Assert healthy recovery and zero retained exchange owners. + Suggested commit: `feat: drive native outbound exchanges eagerly`. + +## Task D3: Cloudflare Eager Exchanges + +**Files:** Modify `crates/edgezero-adapter-cloudflare/src/outbound.rs`, `request.rs`, `response.rs`; +tests in `tests/contract.rs` and existing WASM-gated module tests; workerd cases belong in +`tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs`, fixture-core qualification, +and fixture-harness `net.rs`/`runners.rs`. + +- [ ] Add workerd cases for begin/batch dispatch during an unrelated awaited timer, arrival during + origin streaming, and a buffered cap failure while no result is being collected. Demonstrate + failures before replacing lazy futures. Keep browser-only body behavior separate from workerd. +- [ ] Spawn exchange work with the provider's local scheduling primitive and keep one abort owner + from setup through streamed body lifetime. Preserve manual redirects, raw response encoding, + multi-value headers, token-safe diagnostics, frozen-clock fairness, and event-loop yield quotas. + Do not treat handing a future to a Rust container as proof it is executing. +- [ ] Hand response-owned exchanges through the existing egress coordinator. Clean transport/body + completion disarms the abort guard and releases its ownership exactly once without aborting + completed work. Disconnect, deadline, source/conversion failure, cancellation, or drop before + clean completion stops guest polling and aborts still-armed unfinished work at most once, + then releases ownership exactly once. A previously aborted/disarmed guard cannot abort again; + retained callback work has a separate owner. Test each terminal path, including setup failure + after controller creation and streamed-body ownership transfer. Track arrival before collection + and keep the shared driver validator. Local tests do not upgrade provider guarantees. +- [ ] Run native `test-utils` contracts and the repository's pinned browser/WASM contract command + from `.github/workflows/test.yml`, with nonzero new sentinels. Run + `scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite smoke` after extending the + qualification fixture cases above. That Wrangler-local/workerd path, not the browser runner, + supplies the required Workers scheduling/lifetime evidence. + Suggested commit: `feat: drive Workers outbound exchanges eagerly`. + +## Task D4: Spin Eager Exchanges + +**Files:** Modify `crates/edgezero-adapter-spin/src/outbound.rs`, `request.rs`, `response.rs`; +tests in `tests/contract.rs` and `tests/sdk_resources.rs`. + +**Existing-path checkpoint, 2026-10-25:** Connect setter refusals now remain advisory under the +unchanged aggregate race. The SDK matrix covers every outcome; the async local Spin progress +fixture proves ready success, actual pending timer wakeup, expired-entry zero polling and owner +release across all existing budget sources. First-byte/between-bytes typed setup failures remain. +This is only the refusal-handling part of the third checkbox below, not configured connect +mapping, eager/head-first integration, hosted expiry proof or completion of D4. See the +[master checkpoint](2026-10-18-trusted-server-outbound-implementation.md#spin-connect-refusal-checkpoint-2026-10-25). + +- [ ] Add real SDK-resource cases for unpolled begin/batch dispatch, pending header/body deadline, + original coding, and response handoff. Run the pinned SDK-resource runner, not a compile-only + `wasm32-wasip2` check or a native test-double substitute. +- [ ] Use `spin_sdk::wasip3::spawn` for eager exchange progress. Preserve the upload/send state + machine, response-completion future, and distinction between clean EOF and truncated transport. + Use the existing timer race for permanently pending operations. Preserve typed provider errors + and source release under every cancellation arm. +- [ ] Pass configured connect timeout independently and accept all documented setter refusals as + BestEffort fallback to the aggregate budget. Do not modify authority to implement the unsupported + override. Integrate body/callback owner cleanup, head-first response policy, shared arrival ledger, + and actual ingress GET/HEAD settlement from P2. + Add `connect_timeout_reaches_provider_settings` with a recording setter and every refusal + variant, and execute it through `run_test_nonzero.sh` in the SDK-resource suite. The recorded + setting uses a configured timeout shorter than the aggregate budget, not a split of that + budget. Add an unset/longer-than-budget control following EZ-4.5; refusal keeps aggregate + enforcement and its typed source. Repeat the controlled-drop runtime case from EZ-4. +- [ ] Run `cargo test -p edgezero-adapter-spin --no-default-features --features test-utils` and + `CARGO_TARGET_WASM32_WASIP2_RUNNER='wasmtime run -W component-model-async=y -S p3=y -S http=y' scripts/run_test_nonzero.sh spin_error_code_table_is_exhaustive cargo test --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources`. + Extend named sentinels for the new paths and run the Spin smoke. Keep teardown BestEffort + unless authorized hosted evidence establishes more. Suggested commit: `feat: drive Spin outbound exchanges eagerly`. + Also run CI gate 5 explicitly: + `cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin`. + Expected: target compilation succeeds with the real Spin dispatch/spawn path enabled. + +## Task D5: Cross-Adapter Precedence and Send-Mode Parity + +**Files:** Four adapter `tests/contract.rs`, core `src/outbound.rs` tests, and the qualification +fixture. Fix only the corresponding adapter implementation when a parity case fails. + +- [ ] Add `send_modes_preserve_options_and_failure_precedence` to each native adapter contract. + Run it with `scripts/run_test_nonzero.sh` using the index's four exact native feature cards, + replacing the existing sentinel name only. Expected: red for missing mode propagation, then + identical effective options/typed outcomes and zero activity on unsupported preflight. + Repeat the same named cases under Q2's SDK target cards and required real-runtime fixtures; + a passing native double cannot close a target leg. Apply the task lint card. + +- [ ] Run each changed option through single send, begun request, batch slot, and redirect hops + where permitted; later include fire-and-forget only if implemented. Exercise client marker defaults + and overrides, all caps, connect limits, coding choices, and Image Optimizer rejections. +- [ ] Assert one unsupported-option category wins before expired budget, GET/HEAD body reads, + and backend/network work. Valid siblings proceed. A timeout cause alone never abandons a batch; + strict observation cutoff and actual clock determine termination. +- [ ] Compare one-slot batch and single send cache/validation/error behavior. Prove no-redirect + following parity for exact/overflow identity, compressed, and original-coded responses. Run the + header-first followed-body versus final-body cases through actual normalization and supplied clients. +- [ ] Run core plus all native adapter contracts, real target contracts, dependency audits, and + docs/legacy gates. Do not merge until all required no-regression sentinels execute nonzero. + Suggested commit: `test: enforce outbound send-mode and cutoff parity`. + +## Task D6: Lifetime and Late-Collection Qualification + +**Files:** Fixture-core qualification cases, four fixture entrypoints, fixture-harness `net.rs`, +`runners.rs`, `evidence.rs`, `main.rs` and suite documentation; adapter-local owner tests. + +- [ ] Add `late_collection_preserves_arrivals_and_response_ownership` to core and applicable + adapter contracts. Run + `scripts/run_test_nonzero.sh late_collection_preserves_arrivals_and_response_ownership cargo test -p edgezero-core --features test-utils --lib` + and the index/Q2's native/SDK cards with that sentinel. Expected: red for late-observation loss, + then on-time outcomes survive late collection, cutoff equality stays unresolved and response/ + callback ownership releases exactly once at its own boundary. Apply the task lint card. +- [ ] Add an `outbound-lifetimes` suite to the harness before invoking + `scripts/smoke_test_reusable_app.sh --adapter all --suite outbound-lifetimes`. + Expected: nonzero G1-mapped applicable EZ-6 cases per adapter, with exact outcomes/timings and + owner counts below. Reuse/hosted conditions stay separately pending until their own runs; + the suite does not skip a required Fastly case merely because G2 failed. + +- [ ] Implement all EZ-6 scenarios 1-23, with reuse scenario 10 and scenario 23's actual next-request + reuse leg separately conditional. Include + origin head 700 ms, bids 180/320/900 ms, batch start 34 ms and cutoff 534 ms; retain the first + two bids and report the third unresolved. Use injected clock for exact unit assertions. +- [ ] Check origin flow while a bidder body is pending; cutoff returns by 539 ms rather than + waiting to 600 ms. Cover own timeout before cutoff, CPU-stretch observation, late collection, + repeated termination, and cancellation reporting recorded-but-undelivered indices. +- [ ] Prove asynchronous response handoff retains body-owned work, egress end releases it once, + and an already-pending callback-owned exchange on the same client survives that cleanup. + Preserve Fastly post-delivery work and every documented host-cancellation limitation. +- [ ] Exercise both Fastly lifecycle wrappers with the callback-scope return/error cases, not + merely a manually dropped pending handle. Observe real cancellation/release with a retained + handle and assert that helper return does not end the outer callback scope. +- [ ] Run applicable local and hosted legs under the G1 inventory. Runtime assertions must count + real progress and terminal notifications; do not use sleeps alone as evidence of ownership. + Suggested commit: `test: qualify outbound arrival and lifetime contracts`. + +## Phase Exit + +- [ ] Execute the index's shared gates plus Q2's explicit native/WASM feature matrices and the + D1-D6 named runtime commands. Record nonzero test counts and the G1 inventory entries; a + passing command cannot substitute for G2/G4 approvals or missing hosted legs. +- [ ] Existing outbound semantics remain green; no lazy adapter path remains behind eager APIs. +- [ ] Fastly timed readiness is proved, not inferred; registration/upload/settlement/Image Optimizer + exceptions stay narrowly documented, never widened to response-body reads. +- [ ] Every required mode has one transport, one response policy, and exactly one teardown owner. diff --git a/docs/superpowers/plans/2026-10-18-trusted-server-outbound-04-followups.md b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-04-followups.md new file mode 100644 index 000000000..cea1ed124 --- /dev/null +++ b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-04-followups.md @@ -0,0 +1,1483 @@ +# Outbound Follow-Up Contracts Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement the remaining adoption requirements in scoped, separately verifiable changes without silently adding them to the production blocking set. + +**Architecture:** Extend the existing request policies, body readers, application assembly, and egress coordinator. Reuse phase 2's head-first boundary and phase 3's owned progress engines. Keep response decoration/failure rendering bounded and keep provider diagnostics out of wire errors. + +**Tech Stack:** Existing core/adapters/macros, portable timers, Fastly lifecycle and response ABI, native Axum local server, existing test utilities and fixture workspace. + +--- + +Dependencies are task-specific. Scheduling a task here does not approve its promotion. When a part +lands, record confirmed-present parts and their environment legs under EZ-Q1.6/Q1.8. Conditional +SHOULD work, JS targets, and reusable-sandbox behavior retain their explicit applicability. + +Task numbers group ownership, not an instruction to ignore dependencies. Use the child-specific +prerequisites below: ordinary cache work needs D1b, while callback/egress ownership integration +needs D1a, not D1d's separately approved Image Optimizer work. Diagnostic types and independent +application policies may land before their adapter integration. For lifecycle integration execute +F8 (KV) and F9a (assembly) before F6b's final custom-send report tests; F9b is not a prerequisite. +F10 requires the published P6 utility and includes +F4/F5 cases only when those parts are adopted. Every task retains a buildable hard-cut boundary. + +## Explicit Requirement Ownership + +Every row's full normative range and acceptance cases belong to the named substep. G1 adds an +individual sentinel/command/assertion/environment/artifact entry for every applicable case; a +single test of the first requirement in a grouped task cannot close the later requirements. + +| Requirement | Owning Substep | Required Acceptance Coverage | +| --------------- | -------------- | --------------------------------------------------------------------------------------------- | +| EZ-10 | F2 | All request readers/extractors, sticky/early errors, every upload mode, renderer and fallback | +| EZ-11 | F3a | Tags, validation/deduplication, freshness/no-store/private, mode and adapter matrix | +| EZ-12 | F3b | TLS policy/readback, trusted-chain wrong-name, transport-key isolation, HTTP no-op | +| EZ-13 | F3c | Spin one-authority evidence, preflight Unsupported and documented config rejection | +| EZ-14 | F4 | All idle/absolute-deadline cases, encoded progress, paused application and classification | +| EZ-15 | F5 | Explicit ownership, 64 KiB response ceiling, capacity recovery and provider lifetime | +| EZ-16 | F6a | Every typed provider detail path, bounded diagnostics, retained errors and wire invariance | +| EZ-17 | F6b | Attempt/commit/body/state report, no-attempt categories, native replacement and Abort | +| EZ-18 | F7a | Buffered/bodyless/streamed framing plus SDK and host-compression parity | +| EZ-19 | F7b | Bounded app/dynamic synthesized headers on detached, error and fallback responses | +| EZ-20 | F8 | Declared failure handles, require_kv, all KV operations and recovery without retention | +| EZ-21 | F6c | All six framing reasons across four adapters and routed/detached/fallback failure paths | +| EZ-22 | F9a | Fallible routes/configure, typed retained state, retry and all selected entrypoint cases | +| EZ-23 | F9b | All 13 policy-aware Axum server/TCP testing acceptance criteria | +| EZ-24 remaining | F10 | Adopted cancellation, JS and generator parts only | + +After each implementation increment run scoped `cargo test`, root `cargo fmt --all -- --check`, +and scoped all-target/all-feature clippy from the index. Native success never replaces the +corresponding adapter-target checks. Expected: real named cases execute, then zero lint warnings. + +## Task F1: Reusable Fastly Backend Retirement + +**Files:** Fastly `src/outbound/backend.rs` from D1, `src/lifecycle.rs`, `src/lib.rs`, +`src/request.rs`, and `tests/contract.rs`; fixture Fastly reusable entrypoints. + +- [ ] Add `backend_retirement_rejects_two_hundred_and_first_configuration`; run + `scripts/run_test_nonzero.sh backend_retirement_rejects_two_hundred_and_first_configuration cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --lib`. + Expected: red for missing capacity/retirement policy, then the 201st configuration sends + nothing, emits typed 503, retires after the current request and retains request count. + Repeat real WASI/reuse cases below and apply the task lint card; single-request adoption + does not depend on a deferred reuse leg. + +- [ ] Add registry-growth tests across clients/callbacks, equal origins under jitter, and the + 200-configurations-plus-one case. Fail before a blocking 201st registration with 503 and no send. +- [ ] Implement threshold/default and application-requested retirement after the current request. + Preserve serve-summary request count, distinguish retirement from error, and do not emit a + synthetic SDK error response. Keep request exchange state outside retained backend metadata. +- [ ] Test cold-registration failure recovery, successful late registration reuse, and abandoned + exchanges across six Viceroy requests and the separate hosted 1,001-request case. Never let an + old request's result enter a new request's batch. +- [ ] Run Fastly native/WASI contracts and reuse fixture legs. These results gate reuse enablement, + not single-request adoption. Suggested commit: `feat: bound reusable backend registration growth`. + +## Task F2: Typed Request Overflow and Reader Precedence + +**Files:** Core `src/error.rs`, `body.rs`, `context.rs`, `extractor.rs`, `app.rs`; four adapter +`src/outbound.rs`; demo `crates/app-demo-core/src/lib.rs`; CLI template `core/src/lib.rs.hbs`. + +- [ ] Add cap-minus-one/exact/plus-one cases for every reader/extractor/upload; distinguish host + framing rejection from host-delivered bytes. Test sticky consumed overflow, reusable early/cached + cap rejection, already-expired inbound deadline, and unchanged malformed JSON/form errors. +- [x] Add a stable 413 category and typed inbound/upload origin, preserving public diagnostics and + sticky body state. Early normalized Content-Length refusal must not poison a later larger-cap + read; 408 still wins if already expired. Update explicit demo/template fallback overflow responses. +- [ ] Reject every buffered over-cap upload before dispatch. Keep Axum/Cloudflare bounded + prebuffering; Fastly/Spin may send a partial upload but must abort it, never complete a truncated + request. Preserve ordinary upstream 413 responses and their Retry-After; generated cap errors + carry none even after custom rendering and synthesized-header composition. Keep an internal + typed overflow-origin marker through those stages rather than classifying by status alone. + The suppression takes precedence over F7's header decoration, never over a config-503 retry policy. +- [ ] Run `cargo test -p edgezero-core`, four adapter contracts, and real ingress/pipelining cases. + Add hosted HTTP/2 overflow evidence where required. Suggested commit: `feat: classify request overflow as payload too large`. + +### F2 Implementation Checkpoint (2026-10-21) + +- [x] Implement `PayloadTooLarge { origin }` and `RequestBodyOverflowOrigin::{Inbound, +OutboundUpload}` with fixed public text, typed access and sticky replay. Keep malformed + JSON/form at 400, admitted expiry at 408, and strict cached-cap rejection reusable. +- [x] Refuse an oversized single normalized Content-Length without source polling or poisoning; + match admission's whitespace normalization and let an already-expired admitted deadline win. + Preserve source-error origins through uploads. +- [x] Reject oversized buffered uploads in shared preflight on all four adapters. Replace the + adapters' own streamed-cap errors without changing their existing prebuffer/abort lifecycles. +- [x] Keep a private overflow-origin marker through default/custom rendering and final egress + preparation. Suppress retry headers only for that typed cause; preserve config-503 and + unmarked upstream-413 retry policies. The original error overrides any marker returned by + a custom renderer, including when restoring config retry policy. Demo/scaffold fallback + defaults now select 413. +- [x] Observe red/green core, Axum, Cloudflare Node/WASM and Fastly Viceroy/WASI tests. Exercise + cap-minus-one/exact/plus-one readers, extractors, preflight and upload seams. Verify Spin's + native exchange arbitration preserves the typed overflow. These are unit/SDK-seam results, + not actual hosted ingress/upload qualification. +- [x] Run actual Axum chunked ingress plus a following pipelined request and custom renderer; + prove 413/no retry and correct reuse or connection close. Prove no origin contact for local + buffered/streamed overflow, and an ordinary upstream 413 retaining `Retry-After: 120`. +- [x] Pass all five root Rust gates (793 core tests), all three real-target WASM clippy checks, + 30 Cloudflare Node/WASM library tests, the targeted Fastly Viceroy upload test, demo and + generator tests, docs format/lint/build, and the legacy/docs/dependency/evidence guards. + The workspace TCP suite requires loopback permission; its approved rerun passes. +- [x] Add the isolated `outbound-overflow` suite through all four actual local entrypoints, + with ten independently attempted cases each, bounded TCP origin receipts and no retries. + Axum, Viceroy and Spin pass all ten. Fastly/Spin streamed overflow closes a TCP connection + before any HTTP head bytes; retain that explicit witness, not a claim of zero contact. +- [x] Reject malformed/truncated metadata, missing head-state evidence, duplicate connections, + clean truncated uploads and collector errors in the evidence validator. Observe regression + failures before the fixes; 51 harness and 17 core fixture tests pass afterward. +- [x] Append the exact 2048-byte chunked POST against a 1024-byte cap after the original ten-case + sequence. Both expanded local runs execute eleven cases per target: Axum, Viceroy and Spin + pass eleven; Cloudflare passes the exact-size case but retains the original next-POST failure. + Preserve the old failed evidence. Extend Axum's TCP pipeline test to cover both its original + 9/8-byte boundary and the exact 2048/1024-byte case. A separate built-in `Json` extractor TCP + test exceeds its existing 8 MiB cap by one byte, reaches custom rendering as typed inbound + overflow, and returns 413 without Retry-After. Ordinary JSON is constructed with `json!`. +- [x] Prove collector Drop signals stop and waits for a gated worker, then releases and joins + cleanly before assertions. Removing the join makes the new regression fail; restoring it + passes. This is SDK-owned harness cleanup, not a claim about provider resources. +- [ ] Close the local Cloudflare recovery failure: three matrix runs return the ingress 413 + followed by a proxy-generated 500 before the next request enters the fixture; the origin + receives no connection. An isolated JavaScript-only cancellation control reproduces the + follow-on failure without EdgeZero/Rust/WASM. This local runtime boundary is not repaired, + waived, silently retried or presented as hosted evidence. The precise native mechanism + remains unproven. Retain the failed runs and control inputs/results in the + [evidence record](../../audits/evidence/2026-10-21-outbound-overflow.json). + Subsequent expanded runs retain that failure. Controlled front-proxy requests fail after + cancellation both with and without `Connection: close`; direct requests to Wrangler's own + user-worker listener recover for both JavaScript-only and actual EdgeZero artifacts. + This narrows the observed boundary, not the native cause. The direct listener is diagnostic + only and cannot replace the standard gate. Retain inputs, failed diagnostic setup attempts + and results in the [boundary record](../../audits/evidence/2026-10-21-overflow-boundaries.json). +- [x] Verify the generated-app and demo TCP proof through their unchanged standard Axum + entrypoints. The generated proof reuses the existing ignored workspace test, discovers + the native executable from Cargo JSON and canonicalizes manifest aliases. A shared + bounded client checks fixed/chunked 4095/4096/4097-byte bodies on both 404/405 paths, + exact 413/no-retry output, 405 Allow, the five-second pending-read 408, and a body tail + sent only after receiving the complete 413, followed by genuine health/recovery requests. + Response completion is sampled separately from socket closure. Parser failures and + child-process exit cannot count as success. Both applications pass all 16 observations; + the generated workspace also passes its 22 core tests, strict host clippy and three WASM + compilation checks. Retain the first path-alias failure, successful raw logs, generated + source archive and native binaries in the [evidence record](../../audits/evidence/2026-10-21-generated-fallback.json). + This closes EZ-10's local generated/demo fallback leg, not F2 overall or hosted qualification. +- [ ] Complete hosted Fastly HTTP/2, TS end-to-end and later F7b composition qualification. + Until those environment legs land, F2 and EZ-10 overall remain incomplete. Its implemented + parts are confirmed present, not silently promoted; no other Follow-up becomes blocking. + +Decision log: independent upload and rendering investigations selected shared buffered preflight +plus localized cap-error changes, rather than rewriting provider transports. Separate SDK/runtime +investigations confirmed Fastly readiness still loses source failures, including through +`is_ready`; the failed G2 gate and cancelled dependent driver migration are unchanged. +Independent harness and platform reviews additionally selected strict framing/correlation +validation and a provider-only diagnostic control over transport changes or recovery workarounds. +The local upload suite observes streamed response heads only and drops their bodies unpolled. +The generator and application-lifecycle investigations selected the existing build gate plus +one shared test-only HTTP parser/client rather than another generated application or custom +server. `httparse = 1.10.1` is a CLI dev dependency only; no normal dependency or audited SDK/ +decoder requirement changes. Existing CI steps execute the new checks without adding job names. +The demo step requires the fallback sentinel and forwards `--include-ignored` to both test +listing and execution, preventing a later ignore annotation from silently skipping the proof. +Seven guard regressions cover all selection modes, missing/zero tests and failure propagation. +Independent acceptance and platform investigations selected the appended exact-size probe and +gated Drop test over changing transports or SDK versions. The topology control reads only the +generated loopback user-worker address from the private local runtime log; no proxy secret is +published. Omitting a connection header, bypassing the proxy, delaying recovery, draining bodies +or replaying POST is not adopted as a repair. Fastly G2 and the approved Interfaces digest remain +unchanged. + +The boundary checkpoint passes all five root Rust gates, 53 harness and 17 qualification-enabled +core fixture tests, fixture clippy, 14 Axum connection tests and docs format/lint/build. The 25 +nonzero/evidence guard tests and dependency/docs/legacy checks pass; the inventory remains +unqualified. Independent acceptance, quality and evidence reviews found no remaining scoped +defect. Both expanded local runtime matrices deliberately exit 1 for Cloudflare recovery. + +Generated/demo TCP commands (loopback permission required; generated dependencies must be cached +for offline execution): + +```sh +CARGO_NET_OFFLINE=true scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds +scripts/run_test_nonzero.sh --include-ignored demo_fallbacks_enforce_body_cap_over_tcp cargo test --offline --locked -p edgezero-cli --features demo-example --test demo_logging +``` + +## F3: Cache, TLS and Authority Task Group + +F3a-F3c are independently closable tasks, not inline labels. None closes another requirement. + +### Task F3a: Cache Tags + +**Files:** Core `src/outbound.rs` and `outbound/image_optimizer.rs`; Fastly `src/outbound.rs`; +four adapter preflight/capability modules and `tests/contract.rs`; cache qualification fixture. + +**Dependencies:** P1/P3/P5 and ordinary Fastly cache integration D1b (including D1a); D2/D3/D4 +for their respective mode-parity legs. No D1c/D1d prerequisite. F5 parity applies only when adopted. + +- [ ] Add typed tag validation/deduplication and cache behavior tests. Empty/no-tags behavior, + bypass combinations, and Image Optimizer conflicts must follow EZ-11/EZ-8. Never send tags as + origin headers. Preserve default freshness and negative private/no-store caching behavior. +- [ ] Implement Fastly tagging with the supported SDK cache interface. Axum/Spin accept tags as + documented no-ops; Cloudflare rejects before dispatch. Run send/begin/batch/redirect and later + fire-and-forget cases, with actual cache probes where the requirement names them. +- [ ] Name the shared rejection case `cache_tags_preserve_mode_and_option_precedence` and run + `scripts/run_test_nonzero.sh cache_tags_preserve_mode_and_option_precedence cargo test -p edgezero-core --features test-utils` + before/after implementation. Expected red for missing policy, then exact effective tags, + rejections and sibling isolation pass. Run each adapter contract and required hosted cache + probes, then the task lint card. Suggested commit: `feat: expose portable outbound cache tags`. + +### Task F3b: Fastly TLS Name-Check Policy + +**Files:** Core `src/outbound.rs`; Fastly `src/outbound.rs`/`outbound/backend.rs`; +four adapter preflight/capability modules and contracts; controlled TLS qualification fixture. + +**Dependencies:** P1/P5 and D1b; independent of F3a. + +- [ ] Add default strict and Fastly-only chain-valid/name-unchecked TLS modes, with typed getter + and parts round-trip. Key backend reuse by TLS settings. Keep HTTP no-op semantics and reject + unsupported adapters without disabling verification globally. Test a trusted-chain/wrong-host + fixture, not an unauthenticated internet endpoint. +- [ ] Add `tls_name_policy_isolates_backend_identity`; run it before/after implementation via + `scripts/run_test_nonzero.sh tls_name_policy_isolates_backend_identity cargo test -p edgezero-adapter-fastly --features test-utils`. + Expected red for missing policy, then strict/no-name keys differ and chain validation remains. + Execute the actual trusted-chain/wrong-name TLS fixture and other adapters' preflight controls; + apply the task lint card. Suggested commit: `feat: add explicit Fastly TLS name-check policy`. + +### Task F3c: Spin Authority Limitation Evidence + +**Files:** Spin `src/outbound.rs`, `tests/contract.rs`, `tests/sdk_resources.rs`; +fixture Spin entrypoint and `docs/guide/adapters/spin.md`/`capabilities.md`. + +**Dependencies:** P1's support hook and D4; independent of F3a/F3b. + +- [ ] Keep Spin authority override Unsupported. Record the one-routing-and-Host authority proof + from pinned WASI 0.3 and a Spin fixture; document the final limitation rather than adding a shim. +- [ ] Add `spin_authority_override_rejects_before_pending_body`; run it with + `scripts/run_test_nonzero.sh spin_authority_override_rejects_before_pending_body cargo test -p edgezero-adapter-spin --no-default-features --features test-utils`. + Expected: zero body polls, timer registrations and provider sends, including under expired + budget. Existing Unsupported cases may already pass; the new ordering assertion is the red/green + target. Repeat actual SDK/Spin fixture legs from D4 and apply the task lint card. + Suggested commit: `docs: qualify Spin authority override limitation`. + +#### Existing-Path Support Precedence Checkpoint (2026-10-25) + +Keep the current Unsupported authority policy and its diagnostic, but reject it before generic +method/body/response-policy validation in the live Send and Batch validators. Image Optimizer +support rejection still precedes authority rejection. The native regression first returned the +generic GET-stream error; moving the existing authority check earlier preserves the correct +unsupported error with zero source polls across GET/HEAD/CONNECT/POST, invalid Brotli policy and +zero timeout. An actual WASI SDK regression exercises both operation paths with the same inputs. + +This is the existing-path ordering portion of P1/F3c, not completion of either task. It adds no +support hook, paired-time constructor, public API, scheduling, timer or transport. The existing +expired batch observation-cutoff short circuit is unchanged. Full support-hook integration, +instrumented timer/provider activity and actual Spin/provider limitation qualification remain +open with D4. No capability is promoted and no acceptance leg is qualified by these checks. + +## Task F4: Stream Idle Policy and Deadline-First Classification + +**Files:** Core `src/outbound.rs`, `outbound/progress.rs`, `time.rs`, `error.rs`; four adapters' +raw response streams and request timeout setup; `outbound/testing.rs` if published. + +- [ ] Add `stream_idle_keeps_absolute_deadline_precedence`; run + `scripts/run_test_nonzero.sh stream_idle_keeps_absolute_deadline_precedence cargo test -p edgezero-core --features test-utils --lib`. + Expected: red for missing idle mode, then absolute-only, idle-only, equality and late samples + retain the required source, and only nonempty encoded progress resets idle waiting. Repeat + the named controls in native/SDK contracts with the index/Q2 cards and actual slow-origin + legs from EZ-14; apply the task lint card. A unit pass does not upgrade provider support. + +- [ ] Add zero/invalid-mode tests, getters and parts round-trip. Add a classifier truth table: + idle only -> idle cause; absolute only -> RequestDeadline; both expired or equal -> RequestDeadline. + In unit clocks, a read at 600 ms with idle 400 ms/deadline 1,000 ms must choose the deadline; + delayed platform failure after both limits must do likewise. No timer extends another budget. +- [ ] Keep the original aggregate budget through headers. In opted-in streamed body mode remove + per-call/default body timeout, retain independent application/chain absolute deadlines, and start + idle waiting only when more nonempty encoded input is needed. Reset on encoded progress, not + decoded output or empty chunks; application pauses do not consume idle time. +- [ ] Integrate per-adapter raw-read races and safe platform timer settings. Remove reqwest's + body-inclusive total timeout only for idle mode, replacing header/absolute enforcement rather + than dropping it. Decoder/rechunk buffering may span idle intervals without false expiry or + invented empty output. Default streamed behavior remains unchanged. +- [ ] Run core/adapter classifier tests and EZ-14 acceptance 1-11, including gzip empty DEFLATE + progress, slow reader, longer-than-call-timeout idle setting, and source release. Hosted Fastly + and CF/Spin cancellation limits stay visible. Update scripts to model header/body budgets + separately. Suggested commit: `feat: bound streamed bodies by idle time and absolute deadline`. + +## Task F5: Explicit Fire-and-Forget Ownership + +**Files:** Core `src/outbound.rs`; four adapter `src/outbound.rs` and lifecycle/entrypoint modules; +fixture-core qualification cases and adapter fixtures. + +- [ ] Add `fire_and_forget_capacity_recovers_after_every_terminal_path`; run + `scripts/run_test_nonzero.sh fire_and_forget_capacity_recovers_after_every_terminal_path cargo test -p edgezero-core --features test-utils --lib`. + Expected: red for missing explicit ownership, then capacity two rejects a third send without + dispatch, every terminal path releases once and the independent 64 KiB discard ceiling holds. + Repeat the named accounting test via native/SDK cards and G1's callback/reuse runtime legs; + deferred provider lifetime evidence remains pending. Apply the task lint card. + +- [ ] Add dispatch-without-observation cases and a per-instance capacity-two test across callbacks. + Saturation rejects before send; capacity recovers after completion/failure/expiry exactly once. + Image Optimizer and redirect following remain forbidden; all allowed request options propagate. +- [ ] Add a dedicated explicit operation and bounded owner/capacity accounting, separate from + dropping an ordinary request. Enforce EZ-15.5's independent 64 KiB response-retention and + discard-read ceiling in addition to tighter request caps and deadlines. Stop reading and + release the response when that ceiling is reached, even if a request permits a larger or + unlimited response. Keep provider-specific post-callback lifetime exactly as EZ-15 permits. +- [ ] Test callback end, reused-instance cross-request work, cancellation, body errors and permanently + pending sources. A completion must never be delivered into another request's batch. Do not + publish a background lifetime guarantee inferred from local handle ownership alone. Test a + response larger than 64 KiB under looser request caps: retention and discard reads remain + bounded, response ownership is released promptly, and capacity recovers exactly once. +- [ ] Run core, adapter, and EZ-15 applicable fixture legs. Document the operation's dispatch + receipt/error semantics and explicit limitations. Suggested commit: `feat: add bounded fire-and-forget outbound ownership`. + +## F6: Diagnostics and Delivery Task Group + +Execute F6a before F6b/F6c. Their acceptance and commits are independent; F6b's final lifecycle +tests wait for F8/F9a, not for unrelated F9b TCP helpers. + +### Task F6a: Log-Only Provider Diagnostics + +**Files:** Core `src/error.rs`, `context.rs`, `response_egress.rs`; +four adapter error/fallback paths and contracts. + +**Dependencies:** P1 and existing typed failure boundaries; D1a/D2/D3/D4 only for integration at +the boundaries they change. Diagnostic types can land earlier. No D1b-D1d or F8/F9a prerequisite. + +#### F6a Foundation Checkpoint (2026-10-21) + +- [x] Add the text-free, fixed-size `ProviderFailureDetail` catalogs and safe numeric payloads + for the pinned SDKs. Adapter identity and cause share one enum; arbitrary provider strings + cannot be stored. Spin's documented six-value DNS allowlist projects other text to Unknown. +- [x] Attach optional detail directly to gateway, timeout and internal errors; retain their + existing Display/message/status/kind/reason/wire contracts and original internal source. + Hard-migrate exact field patterns, without an error wrapper or compatibility variant. +- [x] Demonstrate the retained-body diagnostic-loss regression, then preserve detail during sticky + error reconstruction. Cover codec/limit/rechunk/collection, batch partial failures and owned + pending readiness without changing existing deadline precedence or transport behavior. +- [ ] Populate provider boundaries and observers, add actual SDK/token-bearing adapter regression + fixtures, then run the remaining full F6a acceptance legs and capability/evidence work below. + +The first three items are an independent core foundation, not completion of F6a/EZ-16. No adapter +mapper or diagnostic capability is changed. Context replay retains the existing stringified +internal source behavior; direct body/pending paths retain the original concrete error. Full P1 +hard migration, failed G2, F6c framing detail and hosted qualification remain separate gates. + +Checkpoint validation passed all five required Rust gates (27 workspace binaries, 2,103 tests, +including 800 core tests), four compile-fail doctests, three actual-target WASM clippy checks, +and native outbound contracts on all adapters. Viceroy passed 25 adapter contracts plus four +core diagnostic tests; pinned Wasmtime 48.0.2 passed 35 Spin contracts. Cloudflare passed 32 +Node/WASM unit tests. Its browser contract attempts failed before test execution during local +Safari/Firefox WebDriver initialization; that coverage is unverified, not a passing or hosted leg. +Initial Fastly/Spin invocations lacked explicit runners; the correctly configured reruns above +passed. Demo-core and reusable-fixture-core build checks, docs format/lint/build, dependency and +legacy guards passed. The inventory remains 255 criteria / 511 planned legs, `qualified: false`. + +Independent SDK-catalog and core-flow investigations/proposals selected optional fields on the +original variants, not a wrapper or a reconstructed pipeline error. Spec/quality reviews found no +remaining foundation defect; their explicit getter assertions and outbound-collector coverage +improvements were adopted. No SDK/dependency pins or capability claims changed. + +#### F6a SDK Boundary Checkpoint (2026-10-21) + +- [x] Project all 31 known pinned Fastly send causes and nine backend causes, including optional + numeric statuses/codes and I/O kinds, without changing the existing classification tables. + Dispatch initiation and pending-header completion are the same Send operation: no response + head is available on failure. Upload writes/flush/finish and body reads carry separate phases. +- [x] Project all 39 Spin ErrorCode variants without retaining DNS text, TLS messages, field names, + coding strings or internal descriptions. Distinguish send and request-completion callback + phases without changing exchange arbitration, polling or drop order. +- [x] Extend BadRequest with optional detail and sticky replay for the existing host-rejection + cases. Migrate exact patterns and the Spin test seam without compatibility wrappers. +- [x] Populate Axum reqwest predicate/I/O-source evidence and Cloudflare closed exception-name + evidence at the existing send/body boundaries. Preserve original upload-source errors. +- [x] Demonstrate and repair metadata loss during late backend/upload/pending results, adapter + post-ready checks, Cloudflare host-event/205 arbitration and shared bounded collection. + Keep selected timeout text/cause and every existing clock check; late Ok/EOF and timer-only + outcomes carry no invented detail. Spin uses an explicit private result projector in its + existing race helper, not a second transport or a new public client contract. +- [ ] Finish preparation/header-option enrichment, observer/framing integration and diagnostic + capability publication with the remaining F6a/F6c acceptance and hosted evidence. + +Local SDK-value/Node/WASI fixtures are not hosted cause provenance. Standalone Spin library tests +cannot link the Spin KV interface in plain Wasmtime; the executable SDK resource binary exercises +the actual deadline helper through a test-only seam instead. Browser coverage remains unverified +after the previous WebDriver failures; no new browser/hosted success or G2 waiver is claimed. + +Validation passed the five required Rust gates (801 core tests; 27 workspace binaries and 2,108 +tests), core doctests, all three actual-target WASM clippy checks, and all four native outbound +contract suites. Local Viceroy passed 171 adapter library tests and 25 contracts; pinned Wasmtime +passed 35 Spin contracts and nine SDK resource tests; Cloudflare Node/WASM passed 37 library tests. +The final Axum ordering cleanup was followed by its full unit/contract suite and workspace clippy. +Docs format/lint/build, dependency/legacy guards and the 18 evidence-checker tests passed; inventory +validation still reports 255 criteria / 511 planned legs, `qualified: false`. No dependency pin or +capability changed. + +Independent SDK and Axum/Cloudflare reviews found and closed the timeout-replacement losses, +including the shared collector; classification now precedes additional Axum diagnostic inspection. +Independent demo-core (48 tests) and qualification-enabled fixture-core (17 tests) checks passed. +Remaining nonblocking test gaps include signed Fastly status extrema and explicit Spin timer-won/ +raw-trailer projection cases. The refused-port fixture retains a small port-reuse risk: a bound +non-listening socket timed out on macOS, and port zero returned AddrNotAvailable, so neither +proposed replacement can prove the required ConnectionRefused witness. This is a local harness +limitation, not a transport behavior or hosted-qualification claim. + +#### F6a Preparation Checkpoint (2026-10-21) + +- [x] Record Cloudflare request-header and constructor errors without replacing the retained + worker error. Keep SDK-erased constructor types Unknown; project caught raw Fetch setup + exceptions while retaining their exact fixed internal source. Do not enrich local UTF-8 + validation, nonthrowing runtime refusals or the shared response-header sink. +- [x] Record all five pinned Spin HeaderError variants, timeout-option identity and rejection + category, and component-specific unit setter rejection. Preserve 400/500 classification, + fixed messages, advisory NotSupported success and existing preparation/dispatch order. +- [x] Introduce RequestPreparation for these SDK resource boundaries. Do not infer transport + progress from phase names or relabel errors observed at existing SDK send/body boundaries. + Preserve the outbound contract, dependency pins, approved API digest and failed Fastly G2. +- [x] Finish independent review and current-tree Rust/WASM/docs verification before committing. +- [x] Repair the feature-matrix-discovered Fastly lifecycle helper gate: co-located tests can use + it without enabling test-utils. Keep the unit test active; production builds are unchanged. +- [ ] Complete observer/framing integration, diagnostic capability publication and the remaining + provider/hosted acceptance legs. Preparation fixtures do not close F6a/EZ-16 by themselves. + +Dirac investigated Cloudflare source preservation and its shared request/response header sink; +Banach verified the actual Spin SDK's five header variants and unit-returning setter provenance. +Their proposals favored existing-boundary enrichment over a new request construction path or +transport-error remapping. The selected design uses a distinct preparation phase, retains source +objects where they already existed, and never reconstructs a missing cause from provider text. +Implementation is centralized. No application-specific policy or compatibility alias is added. + +Cloudflare fixtures use actual SDK header/constructor failures and scoped, restored Node globals +for setup exceptions and a nonthrowing refusal control. Spin exercises an actual forbidden field +through the production field-construction helper; normal dispatch strips that field beforehand, +so this is not an end-to-end public-client failure claim. Synthetic enum/unit outcomes cover +otherwise unforced setter branches, including secret-bearing Other payloads. They are projection +tests, not hosted cause or runtime-limit qualification. Tests assert exact existing public error +behavior, typed detail and its text-free representation. Local validation and advisory success +controls remain separate from provider failures. + +Spec review passed. Quality review tightened Cloudflare's setup fixtures so assertions run only +after global restoration and added a direct retained-source identity/payload witness; the revised +review passed. No panic-abort or trap cleanup guarantee is inferred from the test guard. + +Validation passed all five required Rust gates (801 core tests; 27 workspace binaries and 2,108 +tests), core doctests and the nonzero public-error diagnostic sentinel, all three actual-target +WASM clippy checks, and the four native outbound contract suites (17/4/2/3 tests). Cloudflare's +Node/WASM suite passed 40 tests; pinned Wasmtime passed 35 Spin contracts and 12 SDK resource tests; +Viceroy passed 171 Fastly library tests and 25 contracts. The plain-fastly lifecycle sentinel also +passed without test-utils after reproducing its missing-helper compile error. All seven adapter +feature matrices passed eight legs each. Independent demo-core tests passed 48 cases. Docs +format/lint/build, dependency/legacy guards and all 18 evidence-checker tests passed. Inventory +validation remains 255 criteria / 511 planned legs, qualified false. No dependency pin, capability, +frozen API file or approved Interfaces digest changed. Hosted/browser acceptance, full P1 and +the failed Fastly G2 remain open. + +#### F6a Source-Observer Checkpoint (2026-10-21) + +- [x] Retain a fixed-size, text-free first-observed body-source failure in the terminal report. + Copy the existing category and optional provider detail; preserve observation-time body kind + through fallback. Missing detail is meaningful and later observations cannot replace it. +- [x] Wire all four converters before post-ready deadline/cancellation arbitration, including + Axum's zero-length validation and final EOF lookahead and the existing Cloudflare/Spin + source-read races. Preserve polling, clocks, terminal precedence, bytes and resource ownership. +- [x] Deliver the same snapshot to completion and observer; reject post-terminal recording. + Keep rendered application errors distinct from body-source errors and invent no provider + evidence for EOF, timer-only, conversion-only or transport-only failures. +- [x] Add native converter regressions, core state/clock controls, real Cloudflare Node/WASM I/O + checks, and a Spin SDK async HTTP check in the existing local progress fixture. +- [x] Finish independent quality review and broad current-tree Rust/WASM/docs verification before + committing this checkpoint. +- [ ] Complete typed framing reasons, diagnostic capability publication, full F6a acceptance and + hosted qualification. This checkpoint does not complete F6a/EZ-16, P1, or the failed Fastly G2. + +Turing reviewed core state/report ownership and Axum; Heisenberg reviewed converter boundaries +and the real SDK test paths. Their proposals favored one bounded observation on the existing +attempt over wrapping streams, retaining source errors or changing delivery-error types. +Implementation is centralized. The report field is a hard cut, not a compatibility wrapper. +No production application renderer changes: its existing callback already receives the original +error, and delivering that rendered response successfully remains a successful delivery. + +Native regressions first failed on missing source observations, then passed after wiring. Controls +cover detail present/absent, first-None/later-Some, fallback attribution, no recording-time clock or +callback activity, drop classification, terminal rejection and successful rendered-error delivery. +Cloudflare's actual source race covers pre-expired/pre-aborted, EOF, ready error and ready +error followed by deadline equality or cancellation. Its existing scoped Node timer shim is +shared rather than duplicated. Spin's synchronous WASI test export cannot wait on SDK timers: +both generic and SDK executor attempts are retained as failed harness diagnostics, not evidence. +The check instead runs directly under the existing async HTTP export with eight cases on local +Spin 4.1.0, without timer substitutions. Neither local runtime establishes hosted client delivery. + +The actual Cloudflare/Spin races failed when only their inner observation hooks were temporarily +disabled, then passed after restoration; recording only in the outer pump loses those ready +errors. Local Spin progress evidence is retained under `.runs/18e0bf43a59d4170-f275-0`, including +the eight-case receipt, all four passing progress groups and no pre-shutdown worker panic. +The mutation failure remains under `.runs/18e0bf38c867ad68-eb61-0` with its missing-observation +diagnostic; neither failed run is rewritten as passing. These are local characterization runs, +not the frozen acceptance inventory or hosted qualification. + +Independent spec and quality reviews passed after tightening exact provider assertions and adding +terminal-without-observation and successful-rendered-error controls. All five Rust gates passed: +27 workspace binaries, 2,116 tests, zero failures and one existing ignored test, including 804 core +tests. All three actual-target WASM clippy checks passed. Node/WASM passed 41 Cloudflare tests; +Viceroy passed 172 Fastly library tests and 25 contracts; pinned Wasmtime passed 35 Spin contracts +and 12 SDK resource tests. The four native outbound contract suites passed 17/4/2/3 tests. Core +doctests, the nonzero public-error sentinel, all seven eight-leg feature matrices, 48 demo-core +tests, 56 fixture-harness tests, fixture clippy and docs format/lint/build passed. Dependency and +legacy guards and all 18 evidence-checker tests passed. Inventory validation remains 255 criteria / +511 planned legs, qualified false. SDK/decoder pins, frozen policies, the approved Interfaces digest +and capability claims are unchanged. Typed framing reasons, full P1/F6a acceptance, browser/hosted +evidence and the failed Fastly G2 remain open. + +- [ ] Add token-bearing URLs/provider messages to regression fixtures. Assert wire bytes and + existing Display/status/reasons do not change; retained body failures preserve diagnostic detail + internally. Add framing-reason cases independent of provider text and policy-panic/deadline cases. +- [ ] Carry bounded typed provider detail, safe numeric codes, and framing reason to logging/ + observer paths without cloning secret-bearing strings into public responses. Keep configuration + contents and raw request headers out. Missing Image Optimizer entitlement remains typed even + if broader diagnostics are deferred. +- [ ] Add `provider_diagnostics_never_change_public_errors`; run it via + `scripts/run_test_nonzero.sh provider_diagnostics_never_change_public_errors cargo test -p edgezero-core --features test-utils`. + Expected red for unavailable retained detail; after implementation, typed internal detail is + present while token-bearing public wire/Display/status snapshots remain unchanged. Run four + adapter error contracts and the task lint card. Suggested commit: `feat: retain bounded log-only provider diagnostics`. + +#### F6a Capability Publication Checkpoint (2026-10-22) + +- [x] Add `OutboundFailureDiagnostics` / `outbound-failure-diagnostics` to the canonical + manifest enum, serialization and existing four adapter capability tables. Publish Native + for Fastly/Spin SDK-cause projection and BestEffort for Axum/Cloudflare's limited visibility. +- [x] Add manifest round-trip, required/optional gate and actual registered-adapter/document-row + regressions. Reproduce unknown-variant, missing-row and Unsupported-versus-BestEffort + failures before filling their implementation. Keep future capabilities fail-closed. +- [x] Align the public guide, proxying guide, normative design, matrix checker and changelog. + Document closed catalog ownership, Unknown/absent codes, API-boundary phase semantics, + wire privacy and the distinction between diagnostic projection and hosted cause evidence. +- [x] Complete current-tree Rust/WASM/docs verification and independent review before committing. +- [ ] Complete the remaining full EZ-16 acceptance and consumer/hosted qualification separately. + Keep the full P1 migration and failed Fastly G2 open. + +Erdos mapped the canonical enum, existing generic gate and adapter tables; Beauvoir reviewed +catalog visibility and publication scope. Both proposals selected the existing capability +mechanism rather than a new runtime query or diagnostic validator. Implementation is centralized. +Native means retaining available pinned-SDK causes/codes, not recovering Viceroy-hidden causes +or promising client delivery. Local SDK/Node/WASI fixtures remain local evidence. Earlier dated +checkpoints retain their historical capability-publication status; this checkpoint supersedes +that status only. No transport behavior, SDK/decoder pins, frozen policy or approved Interfaces +digest changes, and no compatibility API is added. + +All five required Rust gates passed: 27 workspace binaries, 2,130 passing tests, zero failures +and one existing ignored generated-workspace test, including 812 core tests. All three real-target +WASM clippy checks, Fastly CLI-only/no-default clippy, four core compile-fail doctests and 48 +demo-core tests passed. The actual registered-adapter/doc-row and required/optional gate tests +executed. Docs format/lint/build, dependency/legacy guards and the 18 evidence-checker tests +passed; inventory validation remains 255 criteria / 511 planned legs, qualified false. +The first workspace run could not bind TCP listeners inside the sandbox; the complete rerun +with local socket access passed. A wrong initial Axum test filter selected zero tests and was +not accepted as evidence; the correctly named nonzero regression proved the missing support arm. +Spec review found one stale numeric capability count in the active implementation index; it now +references matrix rows, with a regression guard. Revised spec and quality reviews passed. +No new SDK/runtime/hosted probes or generated-workspace execution are claimed for this metadata +publication; earlier diagnostic projection evidence retains its own revision and scope. + +### Task F6b: Fastly Delivery Reports and Safe Replacement + +**Files:** Core `src/response_egress.rs`; Fastly `src/request.rs`, `response.rs`, `lifecycle.rs`, +`lib.rs`, and contracts; affected macros, demo and CLI templates when signatures change. + +**Dependencies:** F6a, F8 and F9a for final assembly/KV failure-path tests, plus D1a callback ownership. + +- [ ] Hard-migrate Fastly custom-send helpers to an owned delivery report that preserves finalize + state and reports actual disposition/commit/body/outcome. Preserve exactly-once completion. + Policy Abort is intentional and never NoAttempt; it cannot authorize a replacement send. A + committed partial response cannot be replaced by another response. Cover declared KV/assembly + pre-delivery failures and standard lifecycle behavior explicitly. +- [ ] Run core and Fastly contract tests, including actual canonical hook composition and safe + native replacement in both permitted cases: no response was attempted, or an attempted + response is confirmed not committed. In the second case, its original terminal report has + already reached completion and any applicable observer; the native replacement is outside + EdgeZero accounting. Prohibit replacement after policy Abort, unknown commitment, or + committed delivery. Update macro/demo/template callers in the same series if signatures + change. Add `fastly_delivery_report_prevents_unsafe_replacement` and run it with + `scripts/run_test_nonzero.sh fastly_delivery_report_prevents_unsafe_replacement cargo test -p edgezero-adapter-fastly --features test-utils`. + Expected red for missing report, then every disposition/commit state and exactly-once release + passes. Repeat canonical Viceroy composition, then the task lint card. Suggested commit: + `feat: report Fastly delivery disposition without unsafe replacement`. + +#### F6b Delivery Report Checkpoint (2026-10-22) + +- [x] Hard-cut all three custom-send helpers to owned NoAttempt/Aborted/Detached/Routed + results. Preserve routed finalizer state on failure and exact terminal report identity, + last-body commitment and separate delivery errors. Keep standard serving policy unchanged. +- [x] Consume the core attempt for terminal metadata using existing live Drop semantics. + Already-terminal extraction samples no clock and cannot notify twice. Shared transition + publication installs terminal state before completion and observer callbacks. +- [x] Keep safe typed pre-attempt detail separate from privately retained standard errors. + Head conversion names URI/builder failures; required stores retain their typed open reason. + Public Debug/messages exclude original diagnostic text; standard errors retain their source. +- [x] Migrate custom fixtures and test utilities without an old-signature wrapper. Standard + macro/demo/template entrypoints require no source migration. Local Viceroy smoke/progress + covers retained-client follow-up after committed source failure and no follow-up after + framing fallback; attempted reports match one terminal event and NoAttempt matches none. +- [x] Finish final quality review and broad verification before committing this checkpoint. +- [ ] Complete F9a assembly-failure integration, consumer acceptance and separately required + hosted qualification. This checkpoint does not close full F6b/EZ-17 or unblock Fastly G2. + +Schrodinger reviewed the custom-helper ownership contract and found two initial regressions: +head-conversion detail was too coarse and standard pre-attempt errors lost their original source. +Both now have typed-safe report projections plus exact standard-error preservation; the focused +test failed before the fix and passes afterward. Herschel reviewed consuming core extraction and +the final cross-crate changes. Synthetic fixtures share the production finisher but do not claim +to execute SDK conversion or host commitment; separate normalizer/committer and Viceroy tests +cover those boundaries. No submitted Interfaces, frozen policy, SDK/decoder pin, outbound +algorithm or provider capability changes. + +Quality review also corrected two fixture weaknesses: the finalizer now retains the exact +request-injected client handle, and required-store evidence checks the closed NoAttempt category. +The wrong-category control failed before validator enforcement and passes afterward. Documentation +distinguishes unobserved pre-admission/refusal responses from admitted source-error Detached +responses, which retain the application observer. Revised spec and quality reviews passed. + +All five required Rust gates passed: 27 workspace binaries, 2,136 passing tests, zero failures and +one existing ignored test, including 813 core tests. The separately executed generated-workspace +gate passed its native/three-WASM builds and real fallback TCP checks. Demo-core's 48 tests and +its actual Fastly-target build passed. All three actual-target WASM clippy checks, Fastly CLI-only +clippy, both eight-leg Fastly feature matrices, strict public rustdoc, four core compile-fail +doctests, the nonzero replacement and public-wire sentinels, 58 harness tests and 17 qualification +core tests passed. The excluded Fastly fixture and harness clippy checks passed. Dependency, +shared-pin, legacy and docs guards passed, as did docs format/lint/build and 18 evidence-checker +tests. Inventory remains 255 criteria / 511 planned legs, qualified false. + +Viceroy 0.21.0 passed 178 Fastly library tests and 25 contracts. Final local smoke/progress +evidence is retained in `.runs/18e0ea8c3a158010-a4bb-0`; it includes committed partial-source +failure, retained-client post-send completion, framing fallback with no follow-up, and typed +required-store NoAttempt with a native response. The earlier `.runs/18e0ea1e943e5d78-52b2-0` +failure remains retained: its harness expected the retired generic adapter-error event instead +of the committed-source event. A WASM-only test shadow-binding lint also failed before renaming. +These are working-tree local characterization runs, not frozen consumer or hosted acceptance. +F9a integration, consumer tests, hosted qualification, full EZ-17/P1 and failed Fastly G2 remain +open; standard entrypoints, outbound transport algorithms and all pinned inputs are unchanged. + +### Task F6c: Typed Response Framing Diagnostics + +**Files:** Core `src/error.rs`, `response_egress.rs`, `response_egress_framing.rs`; +four adapter normalizer/converter/fallback paths and contracts. + +**Dependencies:** F6a and existing response framing policy; independent of F6b. + +- [x] Add one exact assertion per existing `ResponseEgressFramingError` variant: + `ContentLengthMismatch`, `InvalidConnectionHeader`, `InvalidContentLength`, `UnsupportedStatus`, + `UnsupportedTrailers`, and `UnsupportedTunnel`, rather than string matching. Drive each through the actual + normalizer/converter on Axum, Cloudflare, Fastly and Spin, and routed/detached/fallback paths + wherever the rule is observable. Preserve source-category and late failure/commit distinctions; + observer/log evidence contains the typed rule but response status/body/Display remain unchanged. + Add `framing_observer_preserves_each_typed_reason` to core and adapter contracts; run it via + `scripts/run_test_nonzero.sh framing_observer_preserves_each_typed_reason cargo test -p edgezero-core --features test-utils` + and the corresponding adapter package/target commands, then each response contract suite + and task lint card. Expected: every constructible rule is observed under the + correct category, never flattened into generic transport/conversion or policy-panic failure. + Existing framing rejection stays a green control; newly retained typed observation must fail + before implementation and pass afterward. Suggested commit: `feat: preserve typed egress framing diagnostics`. + +#### F6c Framing Checkpoint (2026-10-21) + +- [x] Reuse the existing six-rule enum in a fixed, two-slot report carrier. Preserve the first + application and fallback reasons independently, without altering source diagnostics. +- [x] Route attempt-owned normalization through one recorder on core begin, all four fallback + preparers and Axum empty-stream revalidation. Log only fixed enum text and body kind at + error level; preserve Display, status, fallback bytes, clocks and source/terminal ownership. +- [x] Publish the existing enum at the crate root and a downcast-only `EdgeError` accessor. + Hard-migrate exact report literals to the new carrier; add no legacy report or error shape. +- [x] Reproduce missing typed observations in core and each native converter, then pass all six + rules through actual routed/refused App dispatch and existing fallback coordinators. + Preserve exactly-once completion, routed observation and detached observer exclusion. +- [x] Verify the local Viceroy routed-trailers case has the fixed error-level log, typed terminal + report and unchanged 500 fallback. Strengthen the probe against additional leaking logs and + response headers, not merely absence of leakage in the matching safe event. +- [x] Finish quality review, negative controls and broad current-tree Rust/WASM/docs verification + before committing this checkpoint. +- [ ] Complete consumer Viceroy acceptance and any separately required hosted qualification. + Do not promote capabilities or treat this checkpoint as full outbound-cutover completion. + +Turing mapped the shared coordinator's enum loss and reviewed the bounded carrier/accessor; +Heisenberg mapped converter test seams and fallback reachability. Their proposals favored one +attempt-owned wrapper over per-adapter duplicated recording or a global first-error slot, which +would hide a fallback's independent rule. Implementation remains centralized. No approved +Interfaces, frozen policies, SDK/decoder pins or capability claims change. + +The six-rule matrix uses the unchanged normalizer through actual App dispatch. Fastly and Axum +exercise their converters directly; Cloudflare and Spin exercise failed `begin()` followed by +their real fallback coordinator with existing mock committers. Detached refusals retain the +completion but deliberately invoke no global observer. Stock 500/504 fallbacks cannot currently +violate these framing rules, so all-six fallback-tagged core normalizer controls establish +attribution only, not an observed adapter-invalid-fallback failure. Postcommit streamed length +errors remain their original source categories and are not relabeled as precommit framing rules. + +Core controls retain simultaneous application/fallback observations, first-per-slot behavior, +terminal rejection, independent source evidence and no recording-time clock/callback activity. +Accessor fixtures compare unchanged Display, status, headers and exact body bytes for all six +rules, and reject postcommit length-error misclassification. Policy, deadline, source and SDK +head-preparation controls must not invent framing observations. The initial expired-policy test +incorrectly assumed normalization rejects expired deadlines; that fixture was corrected to the +existing valid-expired policy contract without changing production precedence. + +The local progress fixture adds one routed `Trailer` rejection on all four targets. Viceroy's +fixture logger exposes the actual core error-level event; its global observer records only the +bounded typed framing snapshot and terminal metadata. Wire and log validators reject the private +sentinel anywhere in the captured response or diagnostics. Mutation fixtures cover an extra +leaking event, a leaking response header, missing/duplicate reports and changed outcomes. This is +local characterization, not hosted delivery or resource-accounting certification. + +Quality review reproduced and corrected an initial-only boundary omission in the new recorder: +normalization after writing had begun could manufacture a precommit observation. The failing +writing-state regression now passes; capture and logging are initial-only, while normalization +still runs and returns the same result. Both independent quality reviews passed after this fix, +the exact wire comparisons, negative controls and strengthened leak validators. + +All five Rust gates passed: 27 workspace binaries, 2,126 tests, zero failures and one existing +ignored test, including 811 core tests. All three actual-target WASM clippy checks passed. +Cloudflare's Node/WASM suite passed 42 tests including the nonzero framing sentinel; Viceroy +passed 173 Fastly library tests and 25 contracts; pinned Wasmtime passed 35 Spin contracts and +12 SDK resource tests. All four native outbound contract suites passed (17/4/2/3 tests). Core +doctests, the public-error nonzero sentinel, all seven eight-leg feature matrices, 48 demo-core +tests, 57 harness tests, 17 qualification-core tests and fixture clippy passed. Docs format, +lint/build, dependency/legacy guards and all 18 evidence-checker tests passed. Inventory remains +255 criteria / 511 planned legs, qualified false. + +The hardened local progress suites passed on all four runtimes. Viceroy evidence is retained in +`.runs/18e0c3d1b2fa2300-154c5-0`; Axum and Spin in `.runs/18e0c3e91b527738-16686-0` and +`.runs/18e0c3d1fe532360-155d1-0`. Cloudflare's first attempt retained an unsupported tool-version +record (`.runs/18e0c3e91a822ee8-166f3-0`); the next retained esbuild 0.21.5's import-syntax failure +(`.runs/18e0c41c99850f20-f5a-0`). The successful `.runs/18e0c58f04ba70a0-6123-0` uses existing +cached wasm-bindgen 0.2.129, esbuild 0.28.2, wasm-opt 130 and Wrangler 4.131.0, with explicit +binary overrides and automatic installation disabled. No dependency repin, generated JavaScript +rewrite, alternate entrypoint or hosted deployment was used. Historical failures remain failures. +These are working-tree characterization runs, not a frozen acceptance claim. Consumer Viceroy +acceptance, hosted qualification, diagnostic capability publication, full P1 and failed Fastly G2 +remain open; the approved Interfaces and frozen policies remain unchanged. + +## F7: Response Framing and Header Task Group + +### Task F7a: Buffered Response Framing + +**Files:** Core `src/response_egress.rs`, `response_egress_framing.rs`; four adapter +`src/response.rs`; Fastly `src/fastly_response_abi.rs`; fixture response cases. + +**Dependencies:** Existing egress coordinator; D1a/D2/D3/D4 for ownership-specific integration. +Independent of F7b and D1b-D1d. + +- [ ] Add exact Content-Length cases for buffered data, explicit values, HEAD/304, mismatched + streamed length, suppressed bodies, and platform compression. Compare Fastly SDK Automatic + framing on HTTP/1.1 and HTTP/2 rather than assuming raw guest length is transformed wire length. +- [ ] Derive a length only for eligible buffered untransformed payloads. Preserve representation + lengths on bodyless responses and actual-body gating of manual framing. Host transforms may + recompute/omit length; never force a pretransform count. Verify declared-length compressed + streams by recording raw/wire sizes and decompressed body hash. +- [ ] Add `buffered_framing_respects_suppression_and_host_transform`; run it using + `scripts/run_test_nonzero.sh buffered_framing_respects_suppression_and_host_transform cargo test -p edgezero-core --features test-utils`. + Expected red for missing derived length, then exact length/bodyless controls pass. Run all + adapter response contracts and required Fastly SDK/host-compression legs, then the task lint + card. Suggested commit: `fix: align buffered response framing across adapters`. + +### F7a Implementation Checkpoint (2026-10-23) + +This is an implementation checkpoint, not full EZ-18 acceptance. The original task checklist stays +open because hosted compression, frozen SDK parity and consumer response-diff evidence are still +required. The approved outbound Interfaces and frozen policies are unchanged. + +- [x] Derive exact transmitted buffered lengths, including empty and framework fallback bodies, + after common header normalization on Axum, Cloudflare and Spin. Continue rejecting explicit + buffered mismatches and incrementally enforcing declared streamed source lengths. +- [x] Hard-cut `ResponseEgressEnvelope::begin` and both preparation APIs to require an explicit + `ResponseEgressFramingMode`; migrate all adapters, demo tests and generated scaffolds without + compatibility wrappers. +- [x] Preload transmitted Fastly buffers before one non-streaming Automatic handoff. Check the + injected deadline before/after each staging write, retain an observed timeout through a later + backwards clock sample, and count source bytes once only after successful bulk handoff. + Never pump, finish or abandon a body consumed by that handoff. +- [x] Preserve strict HEAD/204/205/304 suppression. Original buffers and streams are dropped + without polling. Suppressed Fastly responses use an empty streaming handoff, preventing the + local HTTP/2 host from deriving a false zero-length representation. No unused HEAD restoration + field or accessor remains. Hint-bearing declared streams delegate wire framing to Automatic + while retaining source-length enforcement. +- [x] Add the capability row, deterministic red/green controls, separate `response-framing` + suite, bounded failure captures, actual SDK reference controls and HTTP/2 client observations. + Preserve the existing scheduling/coding suite counts and failed Fastly G2 gate. +- [ ] Freeze and qualify hosted Fastly HTTP/1.1/HTTP/2 compression and SDK parity, including encoded + wire counts and decompressed source hashes. Deployed Workers, frozen Spin evidence and the + consumer response-diff acceptance remain separate required legs. + +Nash investigated ownership, native preload/send and clock ordering. Russell investigated fixture +reuse, wire evidence and false-positive assertions. The selected implementation separates explicit +buffered framing from host-owned Automatic framing and keeps actual streaming progressive. Review +found and fixed avoidable staging writes after deadline, loss of typed timeout provenance, and +fixture predicates that could mistake a timeout or failed transfer for passing evidence. + +The first real Viceroy run invalidated the proposed HEAD-buffer restoration: HTTP/2 SDK controls +with nonempty HEAD/204/304 bodies fail, and the SDK's 205 control transmits forbidden payload. The +requester explicitly chose strict suppression and unqualified parity rather than reproducing that +behavior. Those controls remain failed; this decision does not waive EZ-18.4 or infer hosted behavior. + +Local characterization records 80/80 production cases: 32 on Viceroy 0.21.0 across HTTP/1.1 and +HTTP/2, plus 16 each on local Workers, Spin 4.1.0 and Axum. Nine of 18 Fastly SDK reference controls +fail; `--adapter fastly` and `--adapter all` intentionally return nonzero. HEAD captures report +payload observation as unavailable. Short-source cases require concrete terminal transfer failures, +not client timeouts. The capability remains Native on Axum and BestEffort on all serverless targets. +See [working-tree evidence](../../audits/evidence/2026-10-23-buffered-response-framing.json). + +Verification passes all five required Rust gates, 2,181 tests across 28 workspace binaries +(823 core; one existing ignored test exercised separately by the generated-project gate), all +56 native/WASM feature-compilation combinations, the target clippy variants, native adapter +contracts, the demo and generated application checks, and documentation formatting/lint/build. +The independent build-validator also passes the default native build and all three actual WASM +builds. Its additional native all-features link check fails on Fastly SDK hostcalls. A clean +detached build of pushed baseline `fe0fd4c9` fails with the identical 83 undefined symbols, so this +is recorded as a pre-existing target-link limitation, not converted into a passing check or +"fixed" with native ABI stubs. The temporary baseline worktree was removed without changing it. + +### Task F7b: Bounded Synthesized Response Headers + +**Files:** Core `src/app.rs`, `response_egress.rs`; four adapter `src/response.rs` and contracts; +Fastly request preparation; fixture detached/error/fallback responses. + +**Dependencies:** Existing renderer/egress decisions; D1a/D2/D3/D4 for ownership-specific +integration. F2 adds only the conditional combined overflow test; no F7a or D1b-D1d prerequisite. + +- [x] Add bounded configured header sets for detached/error/fallback paths. Validate app-level + sets at configuration; dynamic sets during normal prepare before installation. Invalid dynamic + headers fail with the typed error while retaining the already-valid app set. Fallback executes + no new application code and preserves response-scoped completion/deadline/abort decisions. +- [x] Where F2 is present, add `generated_overflow_retry_after_precedes_synthesized_headers` + covering renderer plus app-level and Fastly per-request sets. Final generated cap errors omit + Retry-After and retain unrelated headers; ordinary upstream 413 retains its surviving value; + config 503 retains its effective retry value (default 60). Do not globally ban the header from + validated sets. Run `scripts/run_test_nonzero.sh generated_overflow_retry_after_precedes_synthesized_headers cargo test -p edgezero-core --lib` + plus each adapter response contract; Expected: real red/green assertions, not a skipped + cross-feature case. Record the combined leg only when both follow-up parts are adopted. +- [x] Run core and four adapter response contracts; compare normal, fallback, admission refusal, + normalized rejection, assembly failure, and postcommit failure paths. Preserve canonical Allow + and effective config Retry-After policy. Add `synthesized_headers_preserve_detached_ownership` + and run it with `scripts/run_test_nonzero.sh synthesized_headers_preserve_detached_ownership cargo test -p edgezero-core --features test-utils`. + Expected red for missing policy, then bounded validation, preserved valid app headers and + exactly-once completion pass. Apply the task lint card. Suggested commit: + `feat: bound synthesized headers on every response path`. + +### F7b Implementation Checkpoint (2026-10-23) + +This checkpoint implements the guest-side policy, not full EZ-19 acceptance. The task's complete +runtime/consumer checklist remains open, and no frozen Interfaces, policies, SDK pins or capability +claims change. + +- [x] Add immutable typed sets with inclusive 32-value/4096-byte bounds, forbidden-field and portable + ASCII validation. Count repeated names and wire delimiters; compact retained map/backing + storage. Failed installation preserves the previous valid set. +- [x] Snapshot the selected policy on envelopes and attempts. Decorate detached/admitted-source + and canonical 404/405 synthesis, not matched handler results/errors or admission refusals. + Preserve completion/deadline/abort ownership and omit configured policy on assembly failure. +- [x] Apply the snapshot through shared fallback preparation and Axum's secondary detached fallback, + without application callbacks, repeat validation or a different fallback body/budget. +- [x] Consume a validated Fastly `prepare` replacement before conversion/head checks and install it + on the returned envelope before egress begins. An invalid construction installs nothing; + an empty validated set explicitly replaces the app set. +- [x] Add ownership/exclusion, field collision/order, repeated-value byte boundaries and typed + overflow/config-retry tests. Exercise maximum-size sets through shared adapter fallback + fixtures and align demo/template configuration with a framework-error version header. +- [x] Finish all required shared/target checks and independent final review for this checkpoint. +- [x] Record actual local normalized 414/431, GET/HEAD fallback, maximum-size provider delivery and + dynamic replacement runtime evidence through the canonical entrypoints. +- [x] Exercise partial-configuration header exclusion and successful retained-policy recovery + through canonical serverless assembly delivery, plus Axum's pre-listener failure boundary. +- [ ] Complete consumer-owned header integration and hosted/frozen-candidate qualification. + Local wire tests do not close these legs. + +Hilbert mapped the four delivery paths and recommended deferred envelope decoration over ingress +API churn, identifying routed-error exclusions and Axum's secondary builder. Aquinas mapped +acceptance and fixture seams and found weak completion/collision/precedence controls; the affected +assertions were strengthened. The selected path uses one core merge implementation and validated +snapshots, with no compatibility wrappers or duplicate transport. Fastly's prepare override had a +real failing Viceroy control before dispatch integration and passes after integration. Failed G2, +the full P1 hard cut, unresolved Cloudflare F2 recovery and the prior framing parity gates remain +open; this checkpoint does not waive them. + +Final static review found no remaining objection after immediate completion/resource-release, +refusal-witness, complete header-set capture and both Fastly dispatch-arm controls were added. +The maximum-size captures verify all 32 configured values and their 4096-byte field-section +charge, not just one marker. Fastly's two prepare contracts pass under Viceroy. Its admitted +source-error/request-replacement composition is checked through the actual coordinator; this is +not a claim that the canonical SDK `take_body` path produced a source-construction failure. + +All 12 native Spin response controls pass with `spin,test-utils`. The additional WASI library-unit +attempt cannot start under bare pinned Wasmtime 48.0.2 because the binary imports +`spin:key-value/key-value@3.0.0`, which that runner does not implement. That result remains an +unexecuted-host boundary, not a passing target test. Target Clippy passes; no SDK import shim, +feature suppression or hosted-evidence waiver was introduced to force execution. + +Verification passes the five required Rust gates: 2,193 tests across 28 workspace binaries, +including 831 core tests, zero failures and one existing ignored generated-project gate. That +ignored gate was run explicitly and passed: generated formatting, strict lint, target checks, +23 generated-core tests and the actual Axum fallback TCP probe. Demo tests pass with 50 core +tests. All three actual-target SDK Clippy runs and Fastly's native CLI-only variant pass, as do +both required nonzero sentinels and documentation format/lint/build/contract checks. The generator +initially exposed template formatting/import mistakes, which were corrected. Its nested Cargo +commands require inherited `CARGO_NET_OFFLINE=true` in the restricted environment; the final run +also has localhost socket access for the TCP probe. No failed environment run was counted as +passing, and the separate bare-Wasmtime limitation above remains recorded. + +### F7b Local Wire Checkpoint (2026-10-23) + +The separate `synthesized-headers` fixture suite passes 55 actual HTTP/1 cases: Viceroy 0.21.0 +22/22, local Wrangler 4.131.0/workerd 2026-09-10 11/11, Spin 4.1.0 11/11 and native Axum 11/11. +All configured values are checked, including 31 ordered cookies and the exact 4096-byte charge. +Canonical 404/405, matched success/error exclusions and real admission refusals are exercised. +Fastly's normalized 414/431 retain the set, while correlated detached and terminal events prove +zero admission/handler/finalize calls. Its prepare replacement, invalid byte-overflow and empty +replacement paths cover both early rejection and GET/HEAD framing fallback. + +Raw captures observe clean EOF and actual HEAD payload absence; GET fallback stays exactly +`internal server error`. Final Fastly logs are rechecked after teardown for terminal count, +outcome, body kind, fallback disposition and byte count. Mutation and TCP controls reject lost, +reordered/changed/duplicate fields, payload in the header packet or a later write, changed/late +terminal records and capture failures that might otherwise skip later cases. Ramanujan reviewed +fixture design/bounds and target builds; Turing reviewed evidence assertions and identified the +final-log and terminal-content controls. Implementation remains centralized. + +Earlier failed capture runs remain retained. Cloudflare's frontend keeps connections open despite +the close field; a client write-half-close produces observable EOF. Applying that FIN to the +other three frontends cancels parsing before dispatch, so the harness explicitly records the +per-target close mode. A macOS timeout-option failure after FIN was removed by nonblocking reads +under the unchanged absolute five-second budget. These are harness corrections, not production +transport changes, timeout-as-success, alternate listeners or a Cloudflare overflow-recovery fix. + +The [local record](../../audits/evidence/2026-10-23-synthesized-response-headers.json) is working-tree +characterization, not exact-candidate qualification. It does not +close consumer-owned headers, assembly-failure runtime exclusion, hosted delivery, HTTP2/HTTP3, +Fastly's nine failed SDK parity controls or failed G2, nor the full P1 hard cut. + +### F7b Assembly Exclusion Checkpoint (2026-10-23) + +The subsequent `assembly` suite passes 40 HTTP/1 exchanges: 18 Viceroy, 11 local workerd and +11 Spin, plus one native Axum startup control. A validated two-field set is installed before +configuration deliberately fails. Both configured names are absent from canonical failure GET +and HEAD responses; config 503 retains its single retry field, and HEAD transmits no payload. +The existing three-request retained recovery sequence is followed by a canonical 404 carrying +only generation-two headers, proving that exclusion was not an inert configuration fixture. + +One bounded TCP capture implementation now serves both suites. It verifies exact GET framing, +retains actual HEAD bytes through clean EOF and preserves failed captures. The superseded HEAD +reader was removed, with its regression migrated to this path. Fastly's custom delivery reports +match all four distinct request tokens. Failed-generation state release is observed before the +recovery request starts; counts, generation/instance identity, reports and boot logs are rechecked +after teardown. Negative controls cover malformed/competing fields, stale recovered values, +missing/duplicate installation witnesses, count-preserving report duplication and delayed release. + +Axum uses its canonical startup function with the selected port already occupied. The partial +installation witness, exact configuration-error boundary and nonzero exit establish configuration +failure precedence over binding; no HTTP response or native response-delivery guarantee is claimed. +Banach reviewed provider routing/ownership and target builds; Fermat identified the framing, +per-request report correlation and immediate-release controls. Both reviews were addressed without +production API, SDK/pin, frozen-policy or transport changes. + +This closes the local assembly-exclusion leg left open above, not consumer integration, hosted or +frozen-candidate qualification. Fastly G2, the nine failed SDK parity controls, Cloudflare overflow +recovery and the broader outbound migration remain open. The +[local evidence record](../../audits/evidence/2026-10-23-assembly-response-headers.json) preserves +both the earlier successful run and the stronger final controls; it is not a release certificate. + +## Task F8: Declared KV Failure Bindings + +**Files:** Fastly `src/request.rs`/`key_value_store.rs`, Cloudflare and Spin corresponding modules, +core `src/key_value_store.rs` and `store_registry.rs`; adapter tests and fixture routes. + +- [ ] Add `declared_kv_open_failure_retains_binding_and_recovers` to core/adapter tests; run + `scripts/run_test_nonzero.sh declared_kv_open_failure_retains_binding_and_recovers cargo test -p edgezero-core --features test-utils --lib` + and the index/Q2's native/SDK cards using that sentinel. Expected: red when a failed declared + store disappears; after implementation the typed failure binding remains, status dispatch + still works unless require_kv applies, and a later restored binding recovers. Include these + cases in the smoke qualification routes, then execute + `scripts/smoke_test_reusable_app.sh --adapter all --suite smoke` with nonzero case IDs and + actual read/write/delete/list results. Apply the task lint card. + +- [x] Add missing declaration versus declared-open-failure cases, provider StoreNotFound versus + transient error, and unaffected status-route dispatch. Honor explicit `require_kv` where requested. +- [x] Preserve failed declarations as typed handles; defer opening/failure to actual use where + required. Do not retain transient availability errors as permanent absence or indiscriminately + fail every request. Keep bounded extraction/secret/config behavior from the outbound baseline. +- [ ] Test failed read/write/delete/list behavior and recovery on a later request with the binding + restored. Source/provider error payloads remain off the wire and response completion still runs. +- [ ] Run changed adapter unit/contract tests and required real runtime legs. Suggested commit: + `fix: preserve declared KV bindings through provider open failure`. + +### F8 Implementation Checkpoint (2026-10-21) + +The requester explicitly adopted Cloudflare and Spin alongside Fastly. All three canonical +registry paths now install declarations without provider opens. The first operation retains a +settled result only in its request-local binding. `KvStoreOpenFailure` separates missing binding, +invalid configuration and provider unavailability without storing provider text or resolved names; +rendering is 500/500/503. Named/default declarations remain resolvable; unknown IDs remain absent. +Explicit required-store failure still precedes prepare/delivery, and Axum startup is unchanged. + +Lovelace investigated SDK classification and ownership, proposing request-local initialization +rather than per-operation opening because Fastly host handles have no Rust close operation. +Ampere investigated async cancellation and runtime evidence. The selected Spin async mutex +serializes only initialization; operations clone the owned handle and release that lock before I/O. +Cancellation before settlement leaves the cell empty. Cloudflare checks callable namespace methods +before the SDK's unchecked casts. Only warning membership for compiled IDs is global: no eviction, +runtime labels, handles, or availability state survives requests. Implementation was centralized. + +Core fake-binding tests and SDK/source-harness tests distinguish scripted fresh-request recovery +from real provider restoration. The local smoke fixtures retain the same strict +`free/read/read/free` sequence, explicit instance identity, 0/1/1/1 warning receipts and actual +completion reports, followed by separately attributed healthy-store CRUD. Each linked operation +also needs one terminal report, and warning counts are rechecked after runtime shutdown. +The sequential Cloudflare/Spin proof precedes overlap, which can create multiple runtime instances; +it does not disable or relax the overlap checks. Retained failed runs remain evidence, not waivers. + +Verification passed all five root Rust gates (794 core tests), actual Cloudflare JS/WASM acquisition +tests, Fastly WASI contracts, Spin source-harness cancellation/classification tests, fixture tests, +and real-target clippy. The final four-runtime smoke run passes all 24 F8 request cases and all +three post-shutdown warning checks, with no failed case. The overall smoke result is still exit 2: +the separate Spin overlap observations remain unverified (per-request and retained in final C). +Smoke probes after the +new retained-prefix checks are warm, not cold-start benchmark evidence; benchmark ordering is unchanged. + +The checkpoint evidence records exact commands, source/tool/artifact hashes and outcomes in +`docs/audits/evidence/2026-10-21-declared-kv.json`. Consumer acceptance criterion 5, actual provider +outage/restoration and hosted qualification remain open. The untouched G2 failure still blocks +dependent Fastly migration; no outbound API, SDK/decoder pin, capability or approved API digest +changes in this independent follow-up. +All 255 acceptance criteria and the reviewed leg set remain unchanged. The inventory records +the implementation adoption separately; its formal release approvals and planned cutover commands +are not promoted by these local smoke results. + +### F8 Operation Delivery Checkpoint (2026-10-23) + +- [x] Preserve the original same-instance `free/read/read/free` sequence and 0/1/1/1 warning + assertions. Append failed write/delete/list operations through each canonical local serverless + entrypoint, with exact sanitized 500 JSON and actual typed `KvError::OpenFailed` receipts. +- [x] Require one operation-handler entry/result pair per operation and none for KV-free routes. + Match request token, observed instance, store selector and operation; reject a generic internal + error masquerading as the expected missing-binding failure. Enforce start/entry/result/terminal + order, including after shutdown; reject a terminal notification before the operation result. +- [x] Retain healthy linked-store write/read/list/delete checks, then require an explicit null read + result and empty list after delete. This is continuity of a different, linked declaration, + not restoration of the missing declaration. +- [x] Revalidate all 13 request tokens per adapter after runtime shutdown, including when an earlier + smoke check failed. Check exactly-once terminal events and warning counts across the observed + instances; preserve the original error alongside any separately recorded teardown failure. + Cloudflare/Spin warning attribution is per instance; Fastly checks sequential aggregate + endpoint totals because those warning lines do not carry an instance tag. +- [ ] Complete consumer acceptance, actual missing-binding outage/restoration and frozen/hosted + qualification before closing F8. + +The [operation-delivery record](../../audits/evidence/2026-10-23-declared-kv-operations.json) +retains 39 passing request cases and three passing post-shutdown validations. The complete +four-adapter smoke run exits 2, not 0: Spin retained-mode overlap remains unverified. The final run +observed per-request overlap; preceding runs left both modes unverified. Neither closes hosted evidence. +This checkpoint changes fixture instrumentation and validation only. Production store adapters, +outbound behavior, capabilities, SDK/decoder pins and the approved interface contract are unchanged. + +The first expanded run correctly failed its new, overbroad seven-request same-instance assertion. +Pinned Viceroy 0.21.0 has `NEXT_REQ_ACCEPT_MAX = 5`, limiting a guest to its initial request plus +five follow-ups. The final test keeps the original four-request proof unchanged and permits natural +turnover only in the added operations. Cloudflare/Spin require one warning per missing-operation +instance and none for healthy-only instances; Fastly checks its endpoint total after each request +against the number of observed missing-operation instances. The latter cannot independently +attribute late warnings across different instances. The test does not restart the prefix, retry a +failed observation, alter production serving settings or claim a hosted reuse guarantee. The failed +run remains in the evidence record. Original SDK/source-harness recovery tests remain separate. + +## F9: Application Assembly and Axum Task Group + +### Task F9a: Fallible Retained Application Assembly + +**Files:** Core `src/app.rs`/`router.rs`; macros `src/app.rs`; four adapter `src/lib.rs` and +entrypoint/lifecycle modules; fixture apps and affected demo/template callers. + +**Dependencies:** Existing fallible assembly baseline and P1's client contract; D1a/D2/D3/D4 for +affected lifecycle integration. F8 applies to declared-KV assembly-failure integration only. +No F9b or D1b-D1d prerequisite. These scheduling scopes do not change the release blocking set. + +- [x] Add failures-before-configure, registered-state identity/access, configure-from-state, + manually-built-router policy/platform parity, success-only retention, and retry-after-failed-build + tests. Hard-migrate route assembly to Result; no expect-based configuration wrapper. +- [x] Expose typed assembled-app state access and one manual-router assembly path that applies + name/configure/platform checks exactly once. Retain only complete successful apps, safe under + overlapping requests. Standard Fastly serving answers/retries assembly failures through canonical + detached delivery; custom callbacks can do so without an assembled app. Log once on boot target. +- [x] Implement the adopted macro fallible-state and standard serverless failure-response + SHOULDs. Preserve Axum fail-before-bind behavior; do not + retain the temporary failure router or require async route-building inside EdgeZero. +- [x] Add `failed_app_assembly_retries_without_retaining_partial_state`; run it before/after the + hard cut with `scripts/run_test_nonzero.sh failed_app_assembly_retries_without_retaining_partial_state cargo test -p edgezero-adapter-fastly --no-default-features --features test-utils --lib`. + Expected red for missing fallible/retained-state contract, then retry and successful-state + identity pass. Run macros and four adapter assembly contracts plus applicable retained-instance + fixtures and the task lint card. Suggested commit: `feat: assemble and retain only complete applications`. +- [ ] Close the remaining consumer acceptance and exact-candidate qualification record. + The implementation and local runtime checks below do not substitute for those external legs. + +#### F9a Assembly Foundation Checkpoint (2026-10-22) + +- [x] Hard-cut `Hooks::routes` and macro-generated `build_router` to Result. Return route errors + unchanged and skip configuration. Migrate every handwritten implementation and direct + generated-router caller, including the excluded demo and reusable-fixture workspaces. +- [x] Share one configured assembly path through `App::from_router::(router, platform)`. + Apply name/configure/platform checks once, without rebuilding the supplied router, while + preserving configuration-error precedence and the complete metadata comparison. +- [x] Borrow typed state from the router's existing registry through `App::state` and + `RouterService::state`. Preserve exact-type lookup, shared interiors and last-write-wins. + Prove configuration-derived head limits and canonical State extraction use that source. +- [x] Exercise real `Sandbox` route failure, configuration failure, partial-state release, + subsequent success and retained state identity. Keep the existing success-only mechanism. +- [x] Finish scoped spec/quality reviews and all root, actual-target, demo/generator/fixture, + documentation and dependency verification before committing the foundation. +- [x] Implement app-free canonical Fastly assembly-error delivery and standard retained retry + integration in the subsequent unified checkpoint below. The original foundation alone + did not change entrypoint responses; full F9a/EZ-22 consumer acceptance remains separate. +- [x] The requester adopted EZ-22.8 and EZ-22.10 on 2026-10-22: one fallible assembly and + error-delivery contract, including macro state expressions and all single-request Fastly, + Cloudflare and Spin entrypoints. These are part of this hard cut, not compatibility modes. + +#### F9a Unified Failure Delivery Work + +- [x] Construct assembly-error egress without an App or temporary error router. Share the + existing default renderer, preserve typed status and redaction, log the failure once on + the boot target, and carry the caller's clock and captured request start into egress. + No successfully configured policies, completion factory, observer or application headers + exist on this path; use the finite default response-write policy. +- [x] Reuse that envelope through the existing adapter converters on Fastly, Cloudflare and + Spin. Remove superseded host-generic assembly-error mappings and their test-only wrappers. + Keep Axum startup failure before binding; keep transport failures distinct from assembly. +- [x] Let Fastly custom callbacks deliver the error without an App. Standard retained serving + delivers the same detached response and retries assembly on the next request, retaining + only complete success. Preserve commitment-safe delivery and no automatic native resend. +- [x] Exercise fallible macro state, typed/redacted wire failures, HEAD suppression, supplied + clocks, partial-state release, and standard/custom runtime failure-to-success recovery. + Align the demo, generator, active guides and changelog; record local evidence separately + from consumer integration and hosted qualification. Fastly G2 remains a separate gate. + +Rawls mapped macro emission and callers; Herschel mapped retained assembly and provider delivery. +Both proposed one shared configured core path and state access through existing Extensions. +The chosen macro helper itself returns Result rather than keeping an infallible helper wrapped +only at the trait boundary. Existing low-level App constructors remain tools, not compatibility +wrappers. SDK/decoder pins, the approved Interfaces, frozen policies and outbound transports are +unchanged. Fastly G2, P1 cutover and consumer/hosted qualification retain their separate gates. + +Both scoped reviews passed. Quality review identified two additional test gaps, now covered: +the original internal error object is retained across route and both configuration paths, and +all adapter assembly boundaries reject a failed route build before configuration (Axum also +preserves its pre-bind boundary). Real Sandbox tests prove partial-state release and successful +state identity, not HTTP serving-loop recovery. Rawls' verification handoff found incomplete +generated-project output and missing standalone receipts; the generated gate subsequently +completed, fresh demo tests/lints ran, and exact commands/exit results are recorded below. + +Local verification used Rust 1.98.1, with no SDK/decoder repin. Relative to base +`5968cac4e0d18964adebb5e261595431e613cd2a`, the 22 changed Rust/template files have aggregate +SHA-256 `f406cc0fcb7c15b9dd612fb1436ef48890d79875146b092db52a7891e3b4efd0`. +The aggregate hashes the concatenation of sorted `SHA256(file)`, two spaces, repository-relative +path and LF for each changed `.rs`/`.hbs` file. This identifies local implementation inputs, +not a hosted qualification artifact or a release approval. Frozen policy and lockfiles are unchanged. + +All five root gates completed with exit 0: formatting, strict workspace clippy, workspace tests, +feature compilation and Spin target compilation. Workspace tests passed 2,147 cases across 27 +binaries, zero failures and one existing ignored generated-workspace case; core passed 818. +That ignored gate was separately executed and passed native/three-WASM compilation, generated-core +tests and fallback TCP checks. All three actual-target WASM clippy checks passed. Viceroy 0.21.0 +passed 180 library tests and 25 contracts, including the real assembled-app retention case. +Demo-core passed 48 tests and strict clippy; all three demo WASM checks passed. Fixture-core's +17 qualification tests, fixture host/Axum/Fastly clippy and all four fixture target checks passed. +Strict public rustdoc, four core compile-fail doctests, docs format/lint/build, dependency/shared-pin, +legacy/docs guards and 18 evidence-checker tests passed. Inventory remains 255 criteria / 511 +planned legs, qualified false. Early type/import/lint failures remain in the local logs and are +superseded by explicit successful reruns, not treated as passing evidence. + +Exact primary commands below completed with exit 0; `--offline --locked` preserves the selected +workspace inputs. Native TCP and Viceroy commands ran with local socket permission. These are +foundation regression/characterization results only; no new hosted probe or capability is claimed. + +```sh +cargo fmt --all -- --check +cargo clippy --offline --locked --workspace --all-targets --all-features -- -D warnings +cargo test --offline --locked --workspace --all-targets +cargo check --offline --locked --workspace --all-targets --features 'fastly cloudflare spin' +cargo check --offline --locked -p edgezero-adapter-spin --target wasm32-wasip2 --features spin +cargo clippy --offline --locked -p edgezero-adapter-fastly --target wasm32-wasip1 --all-targets --features fastly,cli,test-utils -- -D warnings +cargo clippy --offline --locked -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --all-targets --features cloudflare,cli,test-utils -- -D warnings +cargo clippy --offline --locked -p edgezero-adapter-spin --target wasm32-wasip2 --all-targets --features spin,cli,test-utils -- -D warnings +cargo clippy --offline --locked -p edgezero-adapter-fastly --no-default-features --features cli --all-targets -- -D warnings +SSL_CERT_FILE=/etc/ssl/cert.pem CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' cargo test --offline --locked -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly,test-utils --lib --test contract +scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds +scripts/run_test_nonzero.sh assembly_errors_preserve_the_original_internal_source cargo test --offline --locked -p edgezero-core --lib +scripts/run_test_nonzero.sh failed_app_assembly_retries_without_retaining_partial_state cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --lib +cargo test --offline --locked --manifest-path examples/app-demo/Cargo.toml -p app-demo-core +cargo clippy --offline --locked --manifest-path examples/app-demo/Cargo.toml -p app-demo-core --all-targets --all-features -- -D warnings +cargo fmt --all --manifest-path examples/app-demo/Cargo.toml -- --check +cargo fmt --all --manifest-path tests/fixtures/reusable-app/Cargo.toml -- --check +cargo clippy --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-core -p fixture-harness --all-targets --features fixture-core/qualification -- -D warnings +cargo clippy --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-axum --all-targets -- -D warnings +cargo clippy --offline --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-fastly --target wasm32-wasip1 --all-targets -- -D warnings +RUSTDOCFLAGS='-D warnings' cargo doc --offline --locked --workspace --all-features --no-deps +node scripts/check_outbound_docs_contract.mjs +bash scripts/check_outbound_legacy_api.sh +node scripts/check_serde_json_map_contract.mjs +``` + +#### F9a Unified Failure Delivery Checkpoint (2026-10-22) + +The requester adopted both SHOULDs as one hard-cut contract. One app-free core factory now +shares the default renderer and detached envelope; all three serverless adapters release the +unread native request and use their existing converter once. Failed configuration contributes +no callbacks, headers, observer or retained App. The 30-second default write deadline is +anchored before assembly in the supplied clock domain; synchronous initialization itself is +not preempted. Fastly standard retained serving answers/retries, and custom callbacks receive +the detached owned report without native resend. Axum logs the original failure once but +still returns its source-preserving startup error before binding, without an HTTP response. + +Ramanujan proposed/reviewed Fastly ownership and the focused fixture; Linnaeus mapped the +Cloudflare/Spin converters and reviewed quality. Both approved the shared factory and deletion +of superseded host-generic wrappers; both reviewed the final Axum diagnostic delta. Central +implementation kept transport code, dependency pins, lockfiles, the approved Interfaces and +frozen policies unchanged. Demo state identity and generated manual-router policy tests use +the same assembler; normal generated entrypoints need no extra assembly plumbing. + +Relative to `e07a30d23d37e0f640bdbe6a181a619cc70530bf`, 25 changed Rust/template files have +aggregate SHA-256 `872ddd60aff29ac208dfbbac7570a3326e9cc480b5fad4833124230bee075ef3`, using +the sorted source-file hashing recipe above. This is local working-tree characterization, +not an exact-commit release qualification or approval of unresolved gates. + +Local Rust 1.98.1 verification completed with exit 0: all five root gates; 2,151 workspace +tests across 28 binaries, zero failures and one separately executed ignored generator gate; +820 core tests and the once-only full-log/redacted-wire regression; seven macro integration +tests; all three actual-target WASM clippy checks; 180 Fastly WASI library tests and 25 contracts; +49 demo-core tests, demo clippy/three WASM checks, fresh generated native/three-WASM builds, +22 generated-core tests and fallback TCP checks; 18 fixture-core tests and 61 harness tests; +fixture host/three-WASM clippy and Axum compilation; strict public rustdoc; docs format/lint/build; +decoder/shared-pin/map/legacy/docs guards and 18 evidence-checker tests. The inventory still +contains 255 criteria / 511 planned legs and reports qualified false. + +The new focused `--suite assembly` completed with exit 0 on Viceroy 0.21.0, local workerd +2026-09-10 through Wrangler 4.131.0, and Spin 4.1.0. Seven Fastly cases cover standard/custom +503-to-200-to-200 recovery, successful-state retention, partial-state release, all standard +single-request helpers, exact configuration retry metadata, HEAD suppression and internal-error +redaction. Cloudflare and Spin each pass per-request and retained cases, including actual +same-instance recovery. Boot logs are checked once per failed request, including HEAD. The +existing full Fastly smoke suite also passes after replacing its native initialization response. + +Local events (each with zero failed/unverified/unsupported records): + +| Adapter | Events Path | Events SHA-256 | +| ---------- | ------------------------------------------------------------- | ------------------------------------------------------------------ | +| Fastly | `/private/tmp/edgezero-f9a-unified-fastly-final/events.jsonl` | `f6a9611381ca4269eb4d5d1139bc511b25977d021403c5ebf267e8241216e7aa` | +| Cloudflare | `/private/tmp/edgezero-f9a-unified-cloudflare-b/events.jsonl` | `04fa354bd903b9aa9579df7879a6b8bac9720677eda0d2d81eb67d660af68a99` | +| Spin | `/private/tmp/edgezero-f9a-unified-spin-final/events.jsonl` | `22e8b2f6d195765314fbdbc1fa5e551cde89be7ae807c098d8299bf1942756a2` | + +Earlier fixture/validator failures are retained locally, not counted as passing evidence: +the HEAD capture test incorrectly assumed one TCP read contained the method, the boot validator +counted two separate GET/HEAD failures as one, and fixture-only `std::time::SystemTime` failed +on Workers. The fixes use complete request-line reads, per-failure counts and EdgeZero's portable +clock. Fresh successful reruns supersede those failures. Template formatting and strict target +lint failures were likewise fixed and rerun, without suppressions or weakened assertions. + +Consumer integration (including acceptance criteria 9 and 10), durable exact-candidate/hosted +qualification, full EZ-17/EZ-22 acceptance, P1 cutover and failed Fastly G2 remain open. No +provider capability is promoted, no waiver is inferred, and no temporary failure app is retained. + +### Task F9b: Policy-Aware Axum Servers and Test Helpers + +**Files:** Axum `src/dev_server.rs`, `service.rs`, `test_utils.rs`, `src/lib.rs`, TCP/contract tests; +core/macros callers only where required to consume F9a's assembled app; fixture apps. + +**Dependencies:** F9a and D2's local executor; independent of F6b's delivery report. + +- [x] Make programmatic Axum servers accept the assembled app through the same bounded ingress, + local executor and egress path as `run_app`. Provide policy-aware TCP/test helpers, not a public + alternate router service that bypasses admission. A supplied HttpClient avoids constructing an + unused default transport. Run core/macros, Axum TCP, and required retained-instance fixtures. + Suggested commit: `feat: serve assembled app policies through Axum test helpers`. + +F9b is its own implementation/test slice, not a rename of `router.oneshot`. For each of EZ-23's +13 acceptance criteria, G1 records its own test target, command and assertion before F9b closes. +Implement in this order: assembled-app server construction; every configured policy through real +TCP; default-client avoidance when state supplies a client; policy-aware test helper ownership and +shutdown; method/path extraction plus state/store parity; and retained app/policy identity across +requests. Keep the existing ingress connection/parser/deadline tests unchanged. Add explicit tests +for admission Abort/refusal, head limits, custom renderer, response policy/completion, and the +production local executor. Run: + +```sh +cargo test -p edgezero-adapter-axum --features axum,test-utils +cargo fmt --all -- --check +cargo clippy -p edgezero-adapter-axum --all-targets --all-features -- -D warnings +``` + +Expected: all existing TCP tests and the 13 mapped acceptance cases execute and pass. A thin +router-only response test, compile-only helper, or a new service bypassing production ingress is +not equivalent evidence. + +F9b's sentinel map (current execution/readiness is recorded in the checkpoints below): + +| EZ-23 Criterion | Sentinel / Owner | Expected Assertion | +| --------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| 1 | `configured_target_limit_applies_on_tcp` | Both programmatic and test server reject 100-byte target under configured cap 64 with 414 | +| 2 | `configured_renderer_applies_to_not_found` | Unmatched 404 carries the configured marker | +| 3 | `admission_abort_prevents_response_and_dispatch` | Zero response bytes and zero handler/middleware calls | +| 4 | `test_server_preserves_upload_and_read_deadline` | Full 1 MiB chunked upload, and a stalled upload gets 408 | +| 5 | `test_server_streams_before_source_completion` | First of three timed chunks arrives before third is produced | +| 6 | `test_server_buffers_with_exact_content_length` | Five-byte buffered response has Content-Length 5 | +| 7 | `test_server_suppresses_head_payload` | HEAD has headers and no payload bytes | +| 8 | `raw_head_limit_precedes_admission` | Five fields under raw max four are rejected before admission; server recovers | +| 9 | `test_server_shutdown_releases_live_source` | Mid-stream shutdown drops source and returns successful serving result | +| 10 | `state_client_skips_real_transport_initialization` | Supplied client records send; no real target socket or transport init | +| 11 | `foreign_platform_app_keeps_configured_policies` | Cloudflare-platform app's configured policies still apply under Axum | +| 12 | `plain_tokio_test_runs_local_handler_work` | Local spawned work and begun-request-fed stream complete without executor panic | +| 13 | TS-owned route/parity suites using the new helper | Production settings give 414 for 9000-byte target and empty GET reaches origin on all host-parity apps | + +Run each upstream sentinel with `scripts/run_test_nonzero.sh cargo test -p edgezero-adapter-axum --features axum,test-utils`. +Expected red: at least the configured-app/client-construction cases expose F's gap. Existing +working ingress behaviors are regression controls, not forced failures. TS provides criterion 13's +integration result; EdgeZero does not add a dependency on the consumer's crates to fake it. + +#### F9b Assembled-App TCP Checkpoint (2026-10-22) + +- [x] Hard-cut `AxumDevServer::new` and `with_config` to consume `App`; remove implicit default + reassembly. `run_app` uses the same server constructor/serving path after startup setup. +- [x] Add `serve(listener, shutdown)` and `test-utils::AxumTestServer::{start, address, shutdown}`. + Both use the existing bounded HTTP/1 supervisor, current-thread runtime and LocalSet. + Explicit shutdown signals and awaits the same worker; drop signals without blocking. +- [x] Prefer the registered `HttpClient` lazily, without invoking the default factory. Preserve + its complete policy handle; construct the ordinary fallback once with the application clock. + Remove the obsolete transport-only conversion helper, not retain a compatibility path. +- [x] Exercise the twelve upstream sentinels through TCP or the zero-initialization factory seam, + plus refusal/recovery, detached Send/Abort, egress clock/observer/deadline, explicit store/state + parity, drop cancellation and serving-error controls. Foreign metadata uses the actual + Cloudflare export through a dev-only dependency, not a copied platform constant. +- [x] Update the Axum guide, hard-migration changelog and documentation contract guard. Existing + demo, generated and fixture entrypoints use `run_app` and inherit the same production path. +- [x] Finish final independent spec/quality reviews and broad Rust/docs/demo/generated verification. +- [ ] Close criterion 13 with consumer-owned route/parity results, and criterion 12's conditional + begun-request leg after the separate head-first/eager hard cut. Complete exact-candidate + qualification separately; the whole F9b/EZ-23 task is not closed by this local checkpoint. + +Aristotle investigated constructor callers and worker ownership; Franklin mapped TCP acceptance +and local-executor coverage. Both proposed consuming the complete app and reusing the bounded +serving path, with no public router-only service. Central implementation follows that design. +The initial tests failed on the missing TCP helper and router-only constructor signatures; the +client test also failed on the absent lazy factory seam. An initial sandboxed run denied TCP bind; +real loopback reruns passed. No test assertion or ingress resource bound was weakened. + +The helper deliberately skips logger setup, startup preflight and metadata-driven store opening; +callers attach explicit handles/registries. Foreign-platform apps receive Axum context, not their +provider's request context. Shutdown cancels rather than drains active responses. Local spawning +is covered, but the not-yet-integrated begun-request API is not emulated by ordinary `send`. +Fastly G2, P1 cutover, hosted qualification and all capability assignments remain unchanged. + +Spec review found missing actual Cloudflare metadata and refusal TCP controls; both are now +covered. Quality review strengthened HEAD validation to reject every byte after the first header +terminator, including a transport-generated chunk terminator. The negative control demonstrates +the old suffix assertion accepted that illegal tail. Both reviewers signed off this checkpoint. +The fabricated serving-task failure checks helper error forwarding, not an actual worker startup +failure or JoinError. Cancelled-shutdown cleanup additionally relies on inspection of the existing +drop-guard ownership; explicit shutdown and handle-drop cleanup have real TCP controls. + +Final source verification uses base `f29fd9f2910f3fe192b751b379507fa553209d47` and six changed +Rust files, aggregate SHA-256 `b419afa3b3085ce19af0769042dc70d0725d26fe0aa879a6941be196a816e198` +with F9a's sorted file-digest/path convention. The requirements, approved Interfaces, core +outbound/compression modules and frozen policies remain unchanged; the policies digest remains +`c52b4aea6b8d2793f30784e88016a3676088d344c0a5607774a5d9f1a7ffae80`. +The sole lockfile change records the dev-only Cloudflare metadata dependency; no pins changed. + +All five root gates pass on Rust 1.98.1, including 2,171 workspace tests across 28 binaries with +one existing ignored gate, separately executed successfully. Axum passes 233 unit and 17 contract +tests; all twelve required upstream sentinel invocations list and execute their named tests. +Seven eight-combination adapter matrices, three real WASM-target strict clippy runs, public +rustdoc, dependency/legacy/docs guards, docs format/lint/build, 49 demo tests, the generated +workspace gate (including 22 generated-core tests and TCP fallback checks), 18 fixture-core and +61 harness tests, and all 18 evidence-checker tests pass. The ordinary Axum smoke suite observes +one cold and four reused requests. Local logs and smoke evidence are under +`/private/tmp/edgezero-f9b-*`; they are working-tree characterization, not durable release evidence. +The acceptance inventory remains 255 criteria / 511 planned legs with `qualified: false`. + +The later [G1/F9b binding checkpoint](2026-10-18-trusted-server-outbound-implementation.md#g1f9b-local-acceptance-binding-checkpoint) +activates criteria 01-11 using the actual tests in the map above, strengthens criterion 01's +unconfigured success control, and runs the exact guarded commands in the existing Axum CI job. +The original binding checkpoint selected both criterion 10 tests with a separate factory CI guard. +The subsequent [inventory-driven consolidation](2026-10-18-trusted-server-outbound-implementation.md#g1f9b-inventory-driven-suite-consolidation-2026-10-25) replaces those invocations with one full +Axum suite and requires both primary and additional factory sentinels before execution. +Criteria 12/13 remain planned for their outstanding begun-request/consumer portions. The earlier +511-planned count is historical; current bindings have eleven ready and 500 planned legs without +changing source criteria, applicability or qualification. The local execution record above remains +characterization of its original source, not passing evidence for a later candidate. + +## F10: Conditional Scripted-Client Task Group + +G1/G4 records separate maintainer-attributed decisions for EZ-24.11, EZ-24.12 and the JavaScript +part of EZ-24.2. F10 prepares proposals, not its own approval. Each child closes only for its +selected parts/targets; publishing P6's host/WASI core does not adopt these follow-ups. + +### Task F10a: Abandoned Script and Cancellation Accounting + +**Files:** Core `src/outbound/testing.rs` and colocated tests; CI target test activation in Q2. + +**Dependencies:** Published P6 and explicit EZ-24.11 adoption/confirmed-present disposition. + +- [ ] Add `abandoned_script_releases_once_without_unconsumed_report` covering begun-request and + batch drop/cancel, recorded-but-undelivered outcomes, dispatched versus unscripted requests, + and source release. Run it before/after implementation with + `scripts/run_test_nonzero.sh abandoned_script_releases_once_without_unconsumed_report cargo test -p edgezero-core --features test-utils`. + Expected red for missing accounting, then exactly-once release and no false unconsumed-script + report. Unscripted requests still fail loudly without panic. +- [ ] Run the same nonzero sentinel on the adopted WASI target using P6's pinned runner and on + JS only if F10c is selected. Q2 activates those tests in CI; confirm no wall-clock sleeps, + threads or provider emulation. Apply the task lint card. Suggested commit: + `test: account for abandoned scripted exchanges`. + +### Task F10b: Shared Demo and Generator Test Client + +**Files:** CLI `src/templates/core/src/handlers.rs.hbs`, generator tests and +`tests/generated_project_builds.rs`; demo `crates/app-demo-core/src/handlers.rs`; Q2 CI activation. + +**Dependencies:** Published P6 and explicit EZ-24.12 adoption/confirmed-present disposition; +independent of F10a, with cancellation tests selected only when that part is present. + +- [ ] Replace demo/template bespoke doubles with the published utility, + delete the duplicated implementations, and keep examples thin. Test request option recording, + response policies, arrival/cutoff semantics, redirects, and typed errors rather than empty stubs. +- [ ] Extend the existing `generated_workspace_compiles` integration test rather than adding a + duplicate execution wrapper. It already lists `generated_outbound_http_smoke` exactly once + and runs the generated core tests. Run it before/after migration with + `scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds`. + Expected red for assertions using the unadopted shared double, then actual generated tests + assert its recordings and behavior. Preserve the existing nonzero-listing guard, generated + test execution and demo CI job. Run core test-utils, CLI and excluded demo tests from Q2; + extend their existing CI execution with the adopted cases. Apply the task lint card. + Suggested commit: `test: align generated outbound examples with shared scripts`. + +### Task F10c: Published Scripted Client on JavaScript + +**Files:** Core `Cargo.toml`, test-utils feature gates and `src/outbound/testing.rs`; WASM test runner +setup and CI commands owned by Q2. + +**Dependencies:** Published P6 and explicit JavaScript EZ-24.2 adoption; independent of F10b. + +- [ ] Add a target-specific `wasm-bindgen-test` dev-dependency under core's + `cfg(all(target_arch = "wasm32", target_os = "unknown"))` test configuration, aligned with the pinned runner, + and annotate the JS sentinel with `#[wasm_bindgen_test]`. Keep it out of production + dependencies and the core runtime. Q2 owns installation and CI invocation of the matching + `wasm-bindgen-test-runner`; the native Rust test attribute alone is not a JS execution proof. + +- [ ] Add a WASM-gated `scripted_client_uses_paired_js_fake_time` test for owned local futures, + wakeable fake time, head/chunk timing and strict cutoff without provider emulation. Run the + before/after command: + `CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh scripted_client_uses_paired_js_fake_time cargo test -p edgezero-core --features test-utils --target wasm32-unknown-unknown --lib scripted_client_uses_paired_js_fake_time`. + Keep the explicit library/filter selection for target isolation; P1's external API contracts + run separately with their native commands. + Expected red for missing target support, then a nonzero pass without wall-clock sleeps or + extra runtime/thread requirements. Include F10a only when separately adopted, and activate + the selected tests in Q2 CI. Apply the task lint card. Suggested commit: + `test: qualify portable outbound scripts on JavaScript`. + +## Phase Exit + +- [ ] Execute the index's shared gates and each selected lettered task's named sentinel/target + commands. Use Q2's concrete feature matrices for the changed adapters and G1's acceptance + manifest to check every applicable case; Expected: nonzero passes and no uncovered child. +- [ ] Each implemented part is marked present with matching local/hosted evidence requirements. +- [ ] Deferred parts retain explicit interim behavior and never silently become production gates. +- [ ] New code replaces obsolete code directly; no stale transport, duplicate helper, or compatibility + API remains after the hard migration. diff --git a/docs/superpowers/plans/2026-10-18-trusted-server-outbound-05-qualification.md b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-05-qualification.md new file mode 100644 index 000000000..e9896b541 --- /dev/null +++ b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-05-qualification.md @@ -0,0 +1,447 @@ +# Outbound Qualification and Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Keep active docs, generated projects, and the demo aligned with the hard-cut implementation, then produce durable qualification evidence for the exact consumer pin. + +**Architecture:** Extend existing documentation checks, runtime fixtures, and CI feature/target gates. Compare the approved blocking and confirmed-present follow-up behaviors through real adapters and the consumer's canonical composition; never substitute local mocks for hosted evidence. Store the result as an immutable release asset for a reachable candidate SHA. + +**Tech Stack:** Existing Cargo workspaces and lockfiles, Node test runner, VitePress/Prettier/ESLint, existing WASM runners, fixture harness, protected staging environments. + +--- + +Dependencies: execute documentation/test updates with each behavior change, not only at the end. +Final qualification requires all approved blocking work. All commands below are future execution +steps; none constitutes a claimed passing run at plan creation. +Every implementation increment uses the index's per-task red/green, rustfmt, and strict scoped +clippy card. G4's approved interface digest and G2's recorded feasibility dispositions are inputs; +qualification cannot silently approve an API change or convert pending/failed provider evidence +into a waiver. + +## Scoped Qualification Boundary (2026-10-24) + +The requester's [scoped timing and native integration amendment](../../audits/2026-10-18-outbound-api-approval.md#scoped-timing-and-native-integration-amendment) +allows the D1d Send-first checkpoint, not qualification or deployment. The +5 ms windows and +tests remain measured qualification targets, not hard scheduler guarantees; preserve negative +timing/error evidence. The optimizer blocking horizon is explicitly **UNKNOWN** until provider +evidence. A finite H(K) is not needed before this scoped integration, but unknown is not a passing +finite-horizon result. Q3/Q4's numerical horizon/policy and hosted/performance gates stay open. +No criterion, applicability, comparison requirement or failed result is waived. + +Q1 must publish the current unknown horizon rather than imply a finite rule is already approved. +The checkpoint leaves the current trait, backend B/policy, timers, slack and resources unchanged; +typed body-error/readiness safety remains required. Full EZ-1 settlement, Follow, engine and the +later atomic head-first/full-trait migration remain pending. Earlier checkpoint statements below +are historical preparation records, not a denial of this newer scoped implementation authorization. + +## Task Q1: Active Guides, CHANGELOG, and Anti-Drift Checks + +**Files:** Modify `docs/guide/proxying.md`, `streaming.md`, `capabilities.md`, `handlers.md`, +`middleware.md`, `architecture.md`, `routing.md`, `getting-started.md`, `configuration.md`, +`manifest-store-migration.md`, `blob-app-config-migration.md`, `dependency-majors-migration.md`, +`cli-reference.md`, `deploy-action-adoption.md`, four `docs/guide/adapters/*.md`, `CHANGELOG.md`, +and `tests/fixtures/reusable-app/README.md`; update `scripts/check_outbound_docs_contract.mjs`. +Create `scripts/check_forwarding_example.mjs` and `scripts/check_forwarding_example.test.mjs` +for the mandatory EZ-1.14 doctest. Conditionally add the broader Node regression tests and +guide-snippet checker/tests below; Q2 owns CI activation, with Q1 supplying exact invocations. + +- [ ] Convert EZ-25.1-25.44 into a checked source-to-fix list in G1's inventory. First add failing + documentation-contract cases for incorrect helper signatures, result shape, marker accounting, + current pin text, and false streaming API names. Keep historical old-version columns and audit + provenance outside current-state assertions. +- [ ] Document typed AlreadyDelivered/Resolved/Unresolved collection states on both success and + failure, the batch-bound accumulator recipe after mixed check/next/collect, and one-transfer + ownership. Demonstrate invalid merge inputs retain ownership. Do not publish the old Option + slot shape as current or turn AlreadyDelivered into a timeout. Show read-only item/collection + getters and merge-before-consuming `into_parts`; migrate old public-field access instead of + leaving mutable-field examples or an alternate receipt constructor in the guides. +- [ ] Correct each active snippet against the actual API, without adding root re-exports just to + make an old example compile. The relay example uses `ctx.into_request()?`, forwards actual + GET/HEAD/POST ingress, requests original coding, and suppresses only EdgeZero's marker. Show + explicit follow policy, credential/target restrictions, scheduler requirements, budgets/idle + precedence, and cancellation ownership. Do not show GET/HEAD body dropping as a workaround. +- [x] Unconditionally compile the actual forwarding example for blocking EZ-1.14, independently + of EZ-25.42 adoption. The narrow checker extracts the single Rust fence under + `Forward An Inbound Request` in `docs/guide/proxying.md`, without maintaining a copied example. + Build core with `cargo build --offline --locked -p edgezero-core --lib --message-format=json`; + parse Cargo's artifact records to locate its rlib and dependency directory, then invoke + rustdoc on the extracted Markdown with that extern, the workspace edition and dependencies. + Require exactly one non-ignored doctest in its listing and run it; never invoke the example's + network handler. Missing/duplicate fences, `ignore`, `no_run`, `compile_fail`, zero tests or + compile failure fail the gate. Add negative fixtures for each case, including the missing + `into_request()?`. Run `node --test scripts/check_forwarding_example.test.mjs` and + `node scripts/check_forwarding_example.mjs` before/after the example correction. + The example was corrected by an earlier checkpoint. The checker now rejects a real mutation + removing `into_request()?` and passes the unchanged actual example in a nonzero rustdoc run. + Its empty entrypoint compiles item definitions without invoking the network handler. + This does not substitute for P2's real GET/HEAD/POST forwarding tests. +- [ ] Explain that original coding preserves only representation fields surviving ordinary + normalization, never stripped hop-by-hop/nominated or filtered fields. Document cause-based + generated-overflow Retry-After suppression and its precedence over synthesized headers, + without changing ordinary upstream 413 or the effective config-503 retry policy. +- [ ] Publish the Image Optimizer blocking horizon as **UNKNOWN** until provider evidence and + separate numerical approval, with original entry/preparation/host-call measurement anchors, + the unchanged post-head deadline and +5 ms measured qualification target. Publish a finite + rule/maximum only after that approval and proof, not as a preimplementation condition. Reject current- + state guide claims of a universal twice-effective-budget bound; historical audit text remains + provenance, not a promise. Document callback-scope exit separately from helper/egress completion. +- [ ] Align capabilities in core `src/manifest.rs`, four adapter `src/cli.rs`, guide tables and + tests. Introduce only requirement-owned rows with documented Native/BestEffort/Unsupported + semantics. Existing raw ingress, allocation, cancellation and delivery limitations stay explicit. + Correct guides that still describe F's superseded behavior without rewriting historical specs. +- [ ] List every changed public signature and removed symbol in CHANGELOG, including external + dispatch implementers, eager APIs, new options, assembly/custom-lifecycle changes if adopted, + and generator updates. Keep the hard migration explicit and document client-level marker + configuration and manual native-client scheduler setup. Include clock-only constructor removal, + `with_time(OutboundTime)`, app-clock/timer propagation, and deterministic fake-time wakeups; + do not publish a fake clock alone as sufficient for deadline conformance. + Include the Fastly-local observation/snapshot API, extension and current-callback access, + counting rules and frozen-handle lifetime in the Fastly guide and CHANGELOG; do not describe + its numbers as client-lifetime totals or provider-wide pooling statistics. +- [ ] Record separate maintainer-attributed decisions for EZ-25.42 and EZ-25.43 through G1/G4; + Q1 proposes and implements, Q2 owns CI execution. Neither is implicitly adopted by the other. +- [ ] If EZ-25.42 is adopted, add `scripts/check_guide_rust_snippets.mjs` and + `scripts/check_guide_rust_snippets.test.mjs` for + extracting complete items/wrapped fragments versus reasoned ignored examples. Compile against + workspace dependencies in a temporary crate; do not fetch arbitrary new dependencies or run + network examples. Run `node --test scripts/check_guide_rust_snippets.test.mjs` before/after + implementation, then `node scripts/check_guide_rust_snippets.mjs` in a Rust-capable CI job + after locked dependency preparation. Expected: broken complete items fail, valid items and + wrapped fragments compile, ignored fragments require reasons. Record any approved partial + guide scope explicitly instead of claiming the whole SHOULD. Q2 wires both commands into CI. + Reuse the mandatory forwarding checker when that section is selected; do not duplicate its + extraction/compilation machinery or make its gate conditional on this broader adoption. +- [ ] If EZ-25.43 is adopted, extend the existing docs-contract checker and add + `scripts/check_outbound_docs_contract.test.mjs`. Run + `node --test scripts/check_outbound_docs_contract.test.mjs` before/after implementation, then + the production checker below. Expected: negative current-state drift fixtures fail, while + old-version table columns, audit records and explanatory history pass. Q2 keeps existing + production CI invocations and adds the regression-test command; do not create a duplicate checker. +- [ ] Run `node scripts/check_outbound_docs_contract.mjs`, `bash scripts/check_outbound_legacy_api.sh`, + `npm --prefix docs run format`, `npm --prefix docs run lint`, and `npm --prefix docs run build`. + A compile-only guide test is not runtime forwarding proof. Suggested commit with each behavior: + `docs: align outbound adoption and hard-cut migration guidance`. + +## Task Q2: Demo, Generator, and CI Execution Matrix + +**Files:** Modify CLI `src/templates/core/src/handlers.rs.hbs`, `core/src/lib.rs.hbs`, root +`edgezero.toml.hbs`, root `README.md.hbs`, adapter templates as signatures require, `generator.rs`, +and `tests/generated_project_builds.rs`; corresponding `examples/app-demo/` files; +`.github/workflows/test.yml`, `format.yml`, and `scripts/run_test_nonzero.sh` only if necessary. +Create `tests/fixtures/reusable-app/ts-outbound-qualification-runbook.md` as the source template +for Q4a. Qualification binaries/tooling remain owned by G2/Q3, not duplicated here. + +- [ ] Add behavior tests to both demo and generated core for empty GET/HEAD and POST forwarding, + coding/marker options, begin+batch overlap, strict cutoff, redirect policy and error preservation. + Include identity decoded-cap enforcement in original mode, pre-filter Location evidence, + new connect/idle BudgetSource variants, and mixed observation/collection ownership. + Exercise support rejection before settlement and a pending settlement read woken by its + shorter outbound timer, using paired fake time in unit clients and actual target timers. + Add original-coding normalization controls through the shared finalizer. Where F2/F7 are + present, prove header decoration cannot reintroduce Retry-After on a generated overflow and + does not suppress upstream-413/config-503 retry behavior. Templates call shared APIs; they do + not duplicate filtering, retry-header suppression, callback guards or horizon calculations. + Request assertions must include every effective option; keep tests independent of network or + credentials unless explicitly opt-in runtime integration. Use the published double only if + EZ-24.12 is adopted; otherwise migrate existing recording clients to the new protocol. +- [ ] Keep reusable policy/transport implementation in core/adapters. Templates wire entrypoints, + choose application settings and expose examples; they do not contain another driver, decoder, + redirect loop, body buffer, error taxonomy, or response-delivery implementation. Delete obsolete + helper code and migrate calls directly rather than adding compatibility aliases. +- [ ] Fetch all lockfile workspaces before offline checks: + + ```sh + cargo fetch --locked + cargo fetch --locked --manifest-path examples/app-demo/Cargo.toml + cargo fetch --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml + ``` + +- [ ] Run `cargo test -p edgezero-cli` and + `scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds`. + Preserve this existing integration test's nonzero generated-core listing guard and actual + core-test execution; it is not compile-only despite its name. Extend its assertions for the + new protocol, not a duplicate wrapper. The app-demo CI test job also already exists; run + `cargo test --locked --manifest-path examples/app-demo/Cargo.toml --workspace --all-targets` + and equivalent demo format/clippy checks. +- [ ] Preserve all existing feature combinations in `.github/workflows/format.yml` and + `test.yml`: default/no-default, CLI-only Fastly, native `test-utils`, and each real target with + and without `test-utils`. Check zero-test detection. Execute real SDK paths with the existing + pinned runners, for example from the workspace root: + + After the locked fetches above and installation of the three WASM targets, execute all seven + existing matrix invocations. Each must compile eight feature combinations with exit zero: + + ```sh + bash scripts/check_adapter_feature_matrix.sh axum native + bash scripts/check_adapter_feature_matrix.sh cloudflare native + bash scripts/check_adapter_feature_matrix.sh fastly native + bash scripts/check_adapter_feature_matrix.sh spin native + bash scripts/check_adapter_feature_matrix.sh cloudflare wasm32-unknown-unknown + bash scripts/check_adapter_feature_matrix.sh fastly wasm32-wasip1 + bash scripts/check_adapter_feature_matrix.sh spin wasm32-wasip2 + ``` + + These are offline/locked compilation checks. Preserve their current CI jobs; no Axum WASM + target is supported, and passing matrices replace neither target clippy nor actual SDK execution. + Then execute the target sentinels, for example: + + ```sh + CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh dispatch_runs_router_and_returns_response cargo test --locked -p edgezero-adapter-fastly --no-default-features --features fastly,test-utils --target wasm32-wasip1 --test contract + CARGO_TARGET_WASM32_WASIP2_RUNNER='wasmtime run -W component-model-async=y -S p3=y -S http=y' scripts/run_test_nonzero.sh spin_error_code_table_is_exhaustive cargo test --locked -p edgezero-adapter-spin --no-default-features --features spin,test-utils --target wasm32-wasip2 --test sdk_resources + CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh dispatch_runs_router_and_returns_response cargo test --locked -p edgezero-adapter-cloudflare --no-default-features --features cloudflare,test-utils --target wasm32-unknown-unknown --test contract + ``` + + These runner contracts do not replace actual workerd/Spin HTTP fixtures. Add new nonzero + outbound sentinels for this work instead of relying only on routing sentinels. + +- [x] Run `node --test scripts/check_forwarding_example.test.mjs` and + `node scripts/check_forwarding_example.mjs` unconditionally in the existing Rust-capable + workspace CI job after locked dependency preparation. Preserve the one-doctest assertion; + deferring EZ-25.42 does not skip this blocking EZ-1.14 gate. Do not pass Node through + `run_test_nonzero.sh`, which parses Rust listings. + Implemented in the workspace host-test leg with the repository-pinned Node toolchain; + local execution is separate from a hosted CI result and frozen-candidate qualification. +- [ ] Activate each adopted conditional branch, not just its compilation: P3's individual metadata + sentinels, F10a cancellation on selected targets, F10b's new assertions in the existing generated/ + demo test gates and F10c's + explicit JS runner when adopted. For Q1's adopted EZ-25.42/43 run the exact checker and Node + test commands above in CI with the proper toolchains/dependencies. G1 records job and command + per selected part; deferred parts retain a documented decision, not a green skipped test. +- [ ] Run all five primary gates explicitly, including the independent Spin compilation check: + + ```sh + cargo fmt --all -- --check + cargo clippy --workspace --all-targets --all-features -- -D warnings + cargo test --workspace --all-targets + cargo check --workspace --all-targets --features "fastly cloudflare spin" + cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin + ``` + + Expected: all commands exit zero; gate 5 compiles Spin's real target path after its scheduler + rewrite. Neither a native feature build nor a passing SDK stub replaces that target check. + +- [ ] Compile/check all demo target adapters using their current package names and features in + `test.yml`. Run the root verification list from the index plus fixture workspace tests/format/ + clippy. No green root-workspace result substitutes for the excluded workspaces. Suggested + commit: `test: align demo generator and target gates with outbound adoption`. +- [ ] Prepare and commit the runbook source template for Q4a, with the G1 case map, G4 approval + references, exact build/probe/export/validation commands and expected counts. Use explicit + candidate/build placeholders here; Q4a fills them in its external copy after Q3 freezes. + Finish all documentation/package-template source edits before that freeze. Runtime ownership + acknowledgement, staging authorization and candidate-specific package acceptance remain Q4a + gates, not implied by committing this template. + +### Q2 Runbook Source Preparation Checkpoint (2026-10-23) + +- [x] Add the unaccepted source template with explicit external candidate/build placeholders, + G1/G4 references, current preparation/response-egress commands, expected count domains and + separate freeze, ownership, authorization, entitlement and package-acceptance records. +- [x] Correct G1's stale G4 prerequisite description to acknowledge API-only AG approval without + approving Fastly primitive/policy, B, H(K), applicability or measurements. No leg changes. +- [ ] Complete the full-outbound probe/export implementation and exact runnable command/sentinel/ + expected-count bindings before closing the Q2 runbook task or accepting Q4a's external copy. + +The template explicitly marks planned commands and frozen export as unimplemented. Existing +lifecycle-egress probes are not the full outbound package. It is a source-preparation checkpoint, +not execution readiness, candidate freeze, hosted acceptance or a waiver of failed G2. + +## Task Q3: Evidence Preparation, Candidate Freeze and Local Acceptance + +**Files:** Extend G1's inventory/checker, fixture harness `net.rs`, `runners.rs`, `evidence.rs`, +`main.rs`, fixture-core qualification, and adapter fixtures. No consumer production code is owned +by this plan. + +Finish the source-producing steps below and Q2's runbook/template preparation before freezing. +Pre-freeze runs are development checks, not the authoritative EZ-Q1 record. After the freeze +checkpoint, Q3/Q4 collect evidence externally without editing candidate source. + +- [x] Add `outbound_evidence_assembly_retains_case_counts_and_hashes` to fixture-harness tests and + run `scripts/run_test_nonzero.sh outbound_evidence_assembly_retains_case_counts_and_hashes cargo test --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness`. + Expected: red for incomplete record assembly, then nonzero case counts, exact candidate/build + identities and raw artifact hashes survive export. Run the Node evidence-validator test file + below separately, asserting its cases actually execute; do not pass Node through the Rust + listing helper. Host runs still own measured outcomes, not these synthetic unit cases. + +- [ ] Supply controlled origins for every blocking case and confirmed-present follow-up. Include + simultaneous requests, trickled headers/bodies, pending reads, encoded ranges, invalid body + completion, redirects with unreadable bodies, capacity exhaustion/recovery, and host-cache + behavior where applicable. Each case asserts observable counts, bytes, typed outcomes, ordering, + deadlines, and owner release rather than just HTTP status. +- [ ] Add a record-writer mode and release-validation invocation to the G1 checker. Normalize + results into explicit per-leg pass/fail/approved-deferred status with artifact hashes. Include + confirmed-present follow-up mapping from CHANGELOG plus consumer confirmation. A missing + blocking part is a failure, not an applicability exception. +- [ ] Run `node --test scripts/check_outbound_cutover_evidence.test.mjs` with synthetic records + proving wrong-SHA, missing-leg, wrong-environment, wrong-version, empty measurement, failing + threshold and unapproved-deferral records are rejected. Provide human-readable output with + the exact missing gates. Map new normalization, callback-boundary and combined overflow/ + decoration cases to their actual environment/conditional legs. Reject an Image Optimizer + record lacking the pre-approved H(K) rule/maximum/digest or original-entry timing, or one + substituting pre-dispatch expiry for an executed host-call probe. Do not compute the horizon + retrospectively from the run. Validate both the host-call-entry and original-method-entry + inequalities, so unused preparation budget cannot conceal a host-call horizon violation. + Reject a reuse record based only on registration counts, POP, or TLS + resumption rather than the approved 14b cross-execution origin connection proof. Missing + entitlement/hosted execution is pending, never a passing Image Optimizer result. Suggested + commit: `test: assemble outbound acceptance evidence`. +- [ ] Freeze the candidate only after all implementation, test, checker, documentation and + qualification-package source changes, including Q2's runbook template and G2/Q3 tooling. + Record the clean source SHA and build identities externally. Any later source commit + invalidates exact-commit qualification: freeze the new SHA and rerun all required legs, + not only tests thought affected. Candidate freezing does not authorize deployment or release. +- [ ] At that frozen SHA, run the G1 inventory leg by leg under unit, native Axum TCP, + Viceroy 0.21.0, workerd and Spin 4.1. Record Rust/SDK/runtime versions, exact source SHA, + runner image, case counts, seed, measurements and sample counts. An offline compile, + successful stub, skipped case, or safe refusal of a required bounded operation cannot + satisfy an acceptance leg. These are the authoritative local results Q4a/Q5 consume. + +### Q3 Pre-Freeze Assembly Checkpoint (2026-10-23) + +- [x] Keep assembly and artifact validation in the existing Node checker. Add `--assemble` for + explicit per-leg observations and `--report` for all independently evaluable release gates. + The fixture Rust sentinel exercises that actual CLI; no duplicate Rust validator or JSONL + conversion is added. +- [x] Preserve status/sample/assertion/measurement values, candidate SHA and linked SHA-256 build + identity. Compute raw hashes from bounded regular files, reject escapes and refuse overwrites. + Drafts remain pre-freeze/unqualified and release validation rejects dirty/characterization + evidence. No missing metadata, result, applicability decision or approval is invented. +- [x] Share release checks and artifact hashing with the diagnostic collector. Invalid catalog + entries do not erase unrelated evidence. Accepted totals exclude invalid artifacts/deferrals; + planned-command and comparison gates are evaluated even when results are missing. +- [x] Activate Node regressions and the nonzero Rust sentinel in the existing workspace CI leg. +- [ ] Finish controlled origins, frozen-candidate export/mapping, approved H(K)/pooling validation, + the runbook, candidate freeze and all local/hosted/consumer qualification before closing Q3. + +This is preparatory tooling only. Its synthetic tests do not establish runtime measurements, +source-to-build provenance, maintainer approvals or hosted delivery. The full record-writer/export +step above remains open for frozen-candidate integration. The reviewed 511-leg set, frozen APIs, +transport behavior and failed Fastly G2 are unchanged. + +## Q4: Hosted Qualification Handoff Task Group + +Q4a delivers a runnable package; Q4b is TS-owned execution and result return. Delivery alone +cannot close Q4 or satisfy the hosted acceptance gates. Deployment requires separately recorded +authorization to the consumer-owned staging service, never inferred from this plan. + +### Task Q4a: Qualification Package and Accepted Execution Runbook + +**Files:** Read the committed qualification binaries/tooling and fixture README from G2/Q3, +Q2's `tests/fixtures/reusable-app/ts-outbound-qualification-runbook.md` source template, and +G1's checker/inventory. Produce an external candidate-specific package/runbook and acceptance +record, not another source commit. No accepted runbook exists at plan creation. + +**Dependencies:** Q3's frozen candidate and authoritative local acceptance, the committed Q2 +runbook template and G2/Q3 package sources, and G2/G4 approvals. Bind candidate/build hashes in +the external execution package/evidence record without another source commit. EdgeZero prepares the +package; G1 must record the actual TS maintainer/operator accepting +execution ownership and a reachable runbook/work-item link before Q4b starts. + +- [ ] EdgeZero supplies the exact candidate qualification build for all EZ-Q1.4-listed legs, + including EZ-1 body/deadline cases, EZ-2 criteria 11/12/13b/14a/14b, EZ-4 criterion 8, hosted EZ-6 + scenarios 1/2/6/7/8/11 and hosted Image Optimizer behavior. Include version and candidate SHA + in an authenticated/non-secret fixture identity, never provider diagnostics in public errors. +- [ ] Populate the external runbook copy with candidate/baseline SHAs and build hashes, G1 case + map, G4 API/B/H(K) and threshold approvals, exact SDK/runtime versions, settings, entitlements, + controlled origins, sampling plan and canonical lifecycle composition. List the build, probe, + export and evidence-validation invocations and expected case counts per leg. Require TS to + confirm its deployment/browser/response-diff commands rather than inventing commands for its + repository. Include its known integration command, run from the TS checkout: + + ```sh + cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test integration -- --include-ignored + ``` + +- [ ] Review package identities and runbook invocations against G1; execute local probe/export + paths and the evidence-checker regression tests. Expected: exact-commit package, no zero-case + acceptance, and TS acknowledgement of inputs/ownership. Missing operator, missing command, + entitlement or deployment authorization leaves execution pending; do not fabricate acceptance. + A run exposing an implementation defect requires a new reviewed/frozen source SHA and + rerunning all required local and hosted qualification legs; do not relabel earlier evidence. + +### Task Q4b: TS-Executed Hosted and Composition Evidence + +**Files:** Q4a's accepted runbook/package and G1's evidence artifacts. TS owns its checkout, +deployment and actual hosted runs; EdgeZero owns validating returned records in Q3/Q5. + +**Dependencies:** Q4a acceptance, explicit staging authorization, required entitlements and +pre-approved measurement definitions. None can be replaced with local CI success. + +- [ ] TS runs its own checks and remaining hosted results in canonical `run_custom` plus + `send_request_with_registries_and_hooks`, single-request mode by default. Its baseline is + `a9037770a` plus prerequisite PR 1123 at `4c5ca8ae5`; record immutable identities for both + baseline and candidate, service compatibility settings, entitlements, origin delays, and POP. +- [ ] Run EZ-2 M1, M2, M3, M5 and M6, plus EZ-3 M7, using their approved pass conditions, + randomized build order, + fixed page mix/rate and sample counts. Use a controlled silent-drop connect destination, not + TEST-NET assumptions; keep refused connection separate. Probe heterogeneous hosts/budgets, + rolling versions, origin reuse, and Image Optimizer transformation/bounded-stall/query behavior. + M4 applies only to separately applicable reusable-sandbox qualification before enabling reuse; + record its exclusion from single-request qualification under EZ-Q1.5/Q1.8. Blocking 14b + cross-execution pooling evidence remains required and does not promote M4. +- [ ] For Image Optimizer, use the separately approved EZ-8.10 horizon contract, numerical H(K) + rule/maximum/domain and tolerance. Run 1 s and shortened-absolute-deadline cases with the same + per-call timeout/backend, plus representative policy boundaries. Record preparation, host call + and total time from original entry; require original-source 504 on late success/error. Probe + a timely head with a pending body against the unchanged ordinary deadline. Deterministic + nanosecond/millisecond boundary proofs complement hosted executions; an expired-before-send + sample is not a hosted horizon pass. Violations fail qualification, not trigger threshold inflation. +- [ ] Run EZ-8.12's native-path performance comparison on the same frozen candidate and matched + native TS and SDK-only controls. Before replay, obtain explicit approval of asset/profile + corpus, verified cache-hit/miss classification, GET/HEAD and conditional S3-preflight groups, + sample/warm-up counts, randomized order, client read rate, concurrency and acceptance/noise + rules. Hold region, content negotiation, image bytes, transformation parameters, origin/cache + settings, transport settings and execution mode constant. Do not import M2's bidder threshold. + Record p50/p95 first-byte and completion latency, throughput, allocations/copies/peak buffering, + registrations, send/readiness/read host calls and origin connection/request identities where + measurable. Mark unavailable counters explicitly and obtain a reviewed evidence method; + omitting them silently cannot certify parity. Reject extra automatic preflights/fetches, + image-specific backend identities, pre-drained streamed bodies or altered transformation + semantics. A warm-looking edge header alone is not proof of a cache hit at every relevant + tier; correlate the available provider evidence and origin receipts. Keep failed/inconclusive + controls and results; do not claim no regression until the approved comparison passes. +- [ ] For 14b, run both the SDK-only control and candidate across fresh single-request executions + with approved sample counts and origin connection/execution IDs. Require positive cross- + execution connection reuse in both plus M1's connection/full-TLS/resumed-TLS counts and + thresholds. A negative/inconclusive control blocks qualification; it does not make pooling + vacuously pass. This proof does not promote M4 or reuse-mode follow-ups. Read the approved + literal B and domain from G4; registration cardinality must meet that bound without weakening + live-budget, connect, or Image Optimizer constraints. +- [ ] TS runs the specified Viceroy/workerd/Axum integration scenarios with `--include-ignored`, + browser suite, Spin smoke and Fastly response diff. Approved deferred behavior has explicit + differences, not transport workarounds. Record any failing criterion as a production blocker; + do not quietly demote a blocking deadline requirement to fit provider behavior. +- [ ] Add hosted results to the exact-commit record without committing credentials, raw payloads, + signing material or diagnostic URLs. Reuse-only legs remain gated until reuse is enabled. + Local success cannot satisfy these steps; report absent credentials/authorization as pending. +- [ ] Return to Q3/Q5 the per-leg result, case/sample counts, timings, sanitized raw artifacts and + hashes, candidate/build/deployment identities, failures/pending reasons, and TS acknowledgement. + Validate using G1's release-mode invocation from the accepted runbook. Expected: every required + leg passes its pre-approved threshold, or qualification remains blocked with the exact cause. + +## Task Q5: Release Record and Reachable Pin + +**Files:** Read G1 inventory/checker, `CHANGELOG.md` and fixture README; publish release asset and immutable tag +for the reviewed candidate, created only when release/push is authorized. + +- [ ] Verify the reviewed source/build identities still match Q3's frozen candidate and Q3/Q4's + local/hosted records. Do not freeze a new commit or edit source during publication. Any source + change returns execution to Q3's freeze checkpoint and reruns all required legs for the new SHA. +- [ ] Validate EZ-Q1.1-Q1.8 against the assembled record. Required results all pass; approved + partial promotions/deferrals are explicit. Preserve failures as evidence but never as a passing + record. Review body-read readiness, cancellation, raw-ingress and allocation caveats unchanged. +- [ ] Publish the passing record and supporting artifacts as a durable release asset attached to + a reachable immutable tag for that SHA. A 14-day or 90-day expiring CI artifact is insufficient; + the record remains retrievable while TS pins the commit. Verify URLs and hashes after publishing. +- [ ] Obtain consumer confirmation of implemented follow-up applicability and acceptance of the + candidate. Only then recommend repinning. Do not report production readiness merely because + CI is green or a PR is mergeable. + +## Completion + +- [ ] Execute the index's shared gates, Q1/Q2's concrete checks and selected target commands, and + G1's documented release-mode validation on Q3/Q4's returned record. Expected: nonzero passing + applicable tests and a record for the exact SHA; approval/handoff gates remain separate checks. +- [ ] All baseline outbound guarantees remain enforced and no compatibility implementation remains. +- [ ] Every required actual adapter, generated core, demo, docs and target gate passed nonzero. +- [ ] Hosted evidence and consumer qualification passed for the exact reachable candidate. +- [ ] No pending production gate is described as implementation or certification complete. diff --git a/docs/superpowers/plans/2026-10-18-trusted-server-outbound-implementation.md b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-implementation.md new file mode 100644 index 000000000..245a1066d --- /dev/null +++ b/docs/superpowers/plans/2026-10-18-trusted-server-outbound-implementation.md @@ -0,0 +1,2012 @@ +# Trusted Server Outbound Adoption Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement the Trusted Server adoption requirements without regressing the outbound work already implemented in PR 275. + +**Architecture:** Extend the existing portable outbound policies and adapter-owned exchanges; do not introduce a second transport. Use one head-first dispatch boundary for plain sends, eager exchanges, and explicit redirect following, with independent callback and response-body ownership. Prove the pinned Fastly timed-read boundary before replacing its dispatch and harvest engine. + +**Tech Stack:** Rust 1.98.1, existing Cargo features, Fastly SDK 0.13.1 and Viceroy 0.21.0, worker 0.8.5/workerd, Spin SDK 7.0.0/WASI HTTP 0.3 and Spin 4.1, reqwest 0.13.4, existing native Axum/Hyper runtime. + +--- + +## Status and Authority + +### Current Delivery Dashboard (2026-10-27) + +Reconciled against the current hard-cut working tree above `f4d660b1`, including live prepared +dispatch/head finalization, GET/HEAD settlement, eager drivers and identity-bound batch receipts. +All five primary Rust gates pass locally after the current scope hard cut. The requester reopened the owned +interfaces for a shared typed lifecycle and separately approved dispatch-slack retirement. +Core now owns callback/body scopes and revocable operations; provider hooks supply progress only. +Explicit early body ownership is attached unchanged and accepted at canonical egress, while +unaccepted responses remain callback-bound. The retained 25 ms rejection and injection APIs are +removed; original deadlines, B31 and every portable cap remain enforced. The three requested +implementation slices (shared pipeline, bounded GET/HEAD settlement, eager Begin/Batch ownership) +are locally verified. The subsequent P5 checkpoints below add explicit same-client Follow and +provider-boundary controls; all-target acceptance binding and hosted qualification remain open. +This is the current task-level implementation summary, not qualification of that SHA. The +recorded [local acceptance binding checkpoint](#g1f9b-local-acceptance-binding-checkpoint) +activates existing criteria without changing the production implementation snapshot; the +standalone-runner checkpoint below adds the complete local redirect bindings. +The checkpoint sections below retain their historical commands, results and limitations. The +[Requirement Coverage](#requirement-coverage) table maps every EZ requirement group to its owning +tasks, current implementation/checkpoint and remaining closure work. + +The six approved consolidation commitments are **not collectively complete**: + +| Commitment | Current Implementation | Remaining Work | +| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 1. Shared preparation, validation, normalization, decoding and limits | **Hard-cut implementation locally verified.** The facade captures invocation ownership/time and prepared contexts; all four adapters return raw heads to shared normalization, disposition, decoding and bounded collection. | Bind applicable exact-candidate acceptance legs (P1-P3, D1-D4). | +| 2. One pipeline for send, begin, batches and redirects | **Send/Begin/Batch plus explicit Follow implemented.** The shared pipeline includes eager drivers, ownership, raw observations, observation-only checks, mixed collection, lossless identity-bound recovery and head-first redirect policy. | Complete all-target redirect runtime parity and acceptance/qualification (P5, D1-D6, Q3-Q5). | +| 3. Remove superseded implementations and APIs without compatibility | **Hard cut locally verified.** Old implementer send/raw-vector batches, `from_futures`, `finish_batch_item`, public receipt construction, Fastly's separate lifetime registry, copied task/deadline policies and old Body enum API are removed; no aliases remain. Follow uses those same primitives. | Complete applicable acceptance bindings before closing all P1-P5 work. | +| 4. Preserve direct native Image Optimizer | **Send/Follow share native dispatch.** The native SDK mapping hands its head to the common finalizer without another fetch or transformation pipeline; prepared redirects retain optimizer options. Local actual-client Send/Follow controls verify native unsupported boundaries. | Qualify successful optimizer-first-hop redirects and hosted transformation/performance. The synchronous blocking horizon remains UNKNOWN (D1d/Q4b). | +| 5. Migrate demo, templates, clients and docs together | **Hard-cut callers migrated.** Supplied clients, demo, generator, fixtures, guides and guards use prepared contexts, shared ownership/progress, raw events and explicit collection states; demo/scaffold add explicit allowlisted Follow. | Complete remaining acceptance and qualification (P5/Q1-Q5). | +| 6. Document the TS hard-cut ownership boundary | **Documented** in the [adoption guide](../../guide/proxying.md#hard-cut-adoption). Transport stays upstream; application policies stay in the consumer. | Implement the remaining EdgeZero prerequisites and obtain consumer composition/qualification evidence before cutover. Do not delegate body dropping, buffering or transport workarounds to TS (P1-P5/Q4/Q5). | + +Keep three different status questions separate: + +- **Implementation:** this dashboard and the requirement table describe code present at the + snapshot. A foundation or a local checkpoint does not complete its parent task. No full P1-P6 + or D1-D6 migration task is closed by the checkpoints recorded so far. +- **Executable acceptance coverage:** G1's + [inventory](../../../tests/fixtures/reusable-app/outbound-cutover-cases.json) covers 255 source + criteria and 511 split/environment legs. Its `implementation: planned | ready` field describes + readiness of that leg's declared command/sentinel, not overall feature implementation. + EZ-23 local criteria 01-11 and EZ-9 core criteria 01-15 plus local runtime criteria 16/17 are + `ready`: 32 legs, with 479 still `planned`. G1/Q3 binding work remains unfinished, not evidence + that 479 features are absent. Only the explicitly bound + commands count as executable legs; unrelated checkpoint tests do not activate another leg. +- **Qualification:** Q3 has not frozen a candidate; Q4 has no accepted candidate-specific package + or returned hosted/consumer qualification. `ready` commands and green CI cannot substitute for + passing, exact-candidate evidence. The inventory check remains `qualified: false`. + +Local review repaired expired-cutoff handoff on Fastly and Spin: earlier own-deadline outcomes +remain resolved at logical expiry, with no body, timer or provider work. Demo, template and +supplied fixtures preserve exact-cutoff `Unresolved` outcomes rather than emitting premature EOF. +Fastly's pending response-source read now races its absolute write deadline, reports write expiry +and releases response-owned work without cancelling callback siblings. Registered timer failures +remain typed source failures; these guest fixes do not certify interruption of a blocking hostcall. + +Prior shared-scope checkpoint verification includes 984 core unit tests, six external prepared-client tests, 262 repaired-Viceroy +unit tests and 36 Fastly contracts with no filtered readiness cases, 74 Cloudflare guest unit tests +and 29 contracts, and 21 Spin SDK tests plus 35 WASI contracts. The consolidated native TCP gate +passes 261 library cases plus 25 contracts, including all eleven bound sentinels. Refreshed demo +default-core (55), rendered scaffold (28) and qualification-enabled reusable fixture (29) tests +pass. Strict native/provider Clippy and feature builds pass. The forwarding example compiles, docs lint/format/build +and legacy/dependency/evidence guards pass. These are working-tree observations, not a frozen +candidate or hosted qualification. Subsequent interface and slack authorizations are recorded in the +[approval record](../../audits/2026-10-18-outbound-api-approval.md#original-exact-api-subject). + +CI at `2be1dd87` exposed a local verification gap: Spin's `spin`-only WASM lib-test build +included two helpers called only by `test-utils`. Both red jobs report that same compile +failure, not a guest contract failure. Narrow those helper gates and reproduce both +`spin` and `spin,test-utils` Clippy all-targets commands plus the complete existing +feature matrix; native all-features checks cannot stand in for these combinations. + +Independent core/protocol and caller reviewers identified timer-error precedence, future terminal +observations, cutoff handoff and fixture EOF defects. Adapter reviewers/validators covered the +Axum/Cloudflare pair, Fastly and Spin independently; caller validation covered actual demo, +generated and reusable workspaces. The selected fixes preserve the strict core driver protocol, +shared native pools, original budgets and non-cloneable resource ownership rather than weakening +tests or adding compatibility paths. The parent reran workspace gates and guest contracts after +those earlier fixes. Scope/slack changes require their own fresh verification; no passing check +supplies hosted qualification or additional runtime-policy approval. + +The implemented outbound baseline is protected by [Non-Negotiable Baseline](#non-negotiable-baseline). +The tables track the additional adoption work; they do not authorize replacing or deleting the +existing batch/cutoff/cancellation, timing, cap, encoding, provenance or lifecycle guarantees. +Normative requirements, attributed approval amendments and explicit applicability decisions +remain authoritative over this summary. Pending follow-ups are not silently promoted or waived. + +### P5 Explicit Redirect Policy Checkpoint (2026-10-27) + +The approved `HttpClient::follow` surface is implemented through the same prepared dispatch and +head finalizer as Send, including supplied clients. A synchronous application policy and a +validated 1..=20 limit are required. The implementation preserves the exact decision order, +original Location evidence, byte-sharing replay, method/header/credential rules, unchanged +options and arbitrary policy errors. Followed heads are dropped without polling or decoding; +final heads keep normal body policy. The original absolute deadline survives replacement +requests and final streamed consumption, while per-hop timeouts/defaults remain per-hop. + +Core verification passes 1,044 unit tests and fourteen integration tests, including fifty redirect +tests, the complete status/method table and the mandatory supplied-client/original-deadline +sentinel. Real-TCP Axum controls cover plain Send remaining unfollowed, two-origin following, +pending oversized compressed intermediate bodies and no-redirect coding/limit parity. The +fixture fixes `Date` explicitly so comparing independent responses does not depend on wall time. + +Independent review reproduced reentrant scope closure during policy, unread-head disposal, +GET/HEAD settlement, upload polling and finalizer clock/timer callbacks. Shared preparation, +source binding and finalization now check their captured scope before advancing; there are no +adapter-specific cancellation copies. Finalization retains read-time clock history through +cleanup. Terminal source disposal runs inside the existing revocation check, so a cleanup callback +cannot turn scope closure into clean EOF. The existing private collector accepts a borrowed +payload, avoiding another box without changing public signatures, caps or error precedence. +All reproduced red cases now pass. + +Demo and generated `/follow` handlers use the existing bounded request policy, an app-clock +absolute deadline and initial/per-hop same-origin authorization. Canonical equivalent authorities +are covered; existing `/proxy` and fanout behavior is unchanged. The public capability and guide +describe portable redirect policy separately from provider transport guarantees. The CI pipeline +runs the core sentinel and the existing Spin eager component rather than merely compiling it. + +Final verification after the terminal-cleanup fix passes all five root gates, the consolidated +Axum suite (261 library tests and 27 contracts), sixty demo-core tests, thirty-three freshly +generated core tests and strict clippy for the generated workspace. All eight WASM feature +clippy configurations pass. Runtime suites execute without ignored or filtered cases: +Cloudflare 74 library tests plus 29 contracts and nine Node fixture tests; Spin 35 contracts +plus 21 SDK-resource tests and the eager component; Fastly 262 library tests plus 36 contracts. +Fastly uses the already-approved repaired Viceroy with `SSL_CERT_FILE=/etc/ssl/cert.pem`, +not disabled TLS verification or a promoted runtime pin. The initial sandboxed native run +could not bind sockets; the full workspace and TCP suites pass with local-network permission. +The expensive ignored full generated-workspace gate was not rerun locally; the fresh generated +core tests and workspace clippy above are not a claim that this separate CI gate ran. + +This checkpoint does **not** close P5's complete acceptance matrix. Other provider-runtime Follow +parity, actual Fastly optimizer-hop mapping, consumer composition, criterion-level binding and +hosted qualification remain open. The inventory is unchanged: eleven ready legs, 500 planned, +`qualified: false`. Native redirect policy is not a promotion of deadline, cancellation, framing, +resource-accounting or delivery guarantees. Normative requirements and approved Interfaces +remain unchanged; the SDK/decoder/runtime pins and scoped Viceroy qualification boundary remain +unchanged. + +### Fastly Cooperative-Wake CI Repair (2026-10-27) + +CI at `e02c2360` passes the guest unit/contract suites but fails the reusable HTTP smoke on +variant `b`'s first real outbound `/origin/repeated` request. The recorded socket error is +Linux `WouldBlock` (11), reproduced locally as macOS `WouldBlock` (35); the dependency's +future-incompatibility warning is not the failure. Fixed-phase local tracing shows the +cooperative encoded-body stream waking and yielding after its first chunk, followed by +Fastly entering an empty-handle host selection until the still-live 30-second deadline. +The unchanged smoke's client times out first. No timeout or smoke assertion is relaxed. + +The existing progress engine now forwards source wakes through a per-poll waker and shares +only a notification flag across its family. Same-family nesting never clears that flag; +older retained wakers can notify the current caller. A notified turn still publishes expiry, +but does not enter another host wait. Later non-waking pending polls resume ordinary waits. +The progress state retains no parent task waker, and all temporary tracing is removed. + +The direct/nested-yield and retained-waker regressions first fail deterministic assertions +without sleeping, then pass with the fix. A third control covers deadline expiry during the +yield and restoration of normal waits afterward. All thirty progress tests pass. The exact +`--adapter fastly --suite smoke` command passes locally with exit 0 under the approved repaired +Viceroy, including every existing variant and assertion. This is local runtime evidence, not +a frozen candidate, hosted qualification, a changed runtime pin or capability promotion. +Provider-runtime redirect acceptance work remains open while this CI repair takes priority. + +Final repeat passes all five root gates, all four Fastly WASM clippy configurations, 265 guest +library tests and 36 guest contracts without ignored or filtered cases, plus docs/legacy guards. +The production-client progress suite passes all twelve probes with eighteen origin requests; +the unchanged CI assertions pass. Its expected exit 2 retains `qualification: false`, not a +qualification claim. The requirement inventory and frozen requirements/Interfaces are unchanged. + +### P5 Provider-Boundary Controls (2026-10-27) + +This test-only checkpoint exercises the existing actual provider clients through the shared +Send/Follow pipeline. It adds no transport, implementer API, provider setting or compatibility +path. Production behavior, SDK/decoder/runtime pins and every existing outbound guarantee remain +unchanged. Local validation uses Rust 1.98.1, Node 24.20.0, Spin 4.1.0 and the explicitly selected +approved Viceroy repair; it does not deploy a qualification build. + +Cloudflare's eight new guest tests run `CloudflareOutboundClient` against a manual-fetch fixture, +not a supplied-client substitute. They compare declared and undeclared identity, gzip, deflate +and unsupported-coding responses at the final buffer cap; cover unread pending, erroneous, empty +Brotli and oversized intermediate heads; retain exact stopped/limit-reached failures and hop +indices; and check streamed final parity, original deadlines, replacement requests and drop +cancellation. Fixture teardown restores globals and settles pending fetch/body callbacks. +The complete guest suites pass 82 library tests, 29 contracts and twelve Node fixture tests. +Abort signals and zero source pulls are guest observations, not workerd/origin cancellation proof. +Duplicate or invalid-byte Location evidence unavailable through JS normalization remains covered +only at the portable observable boundary, not certified as original Cloudflare wire evidence. + +Spin's existing local wire component now performs 68 P5 exchanges with 84 correlated real HTTP +requests. Buffered/streamed no-redirect identity, gzip and unsupported-token passthrough controls +compare exact bytes/headers and typed final, decoded and encoded caps. Actual followed pending, +source-error, empty-Brotli and 10 MiB declared heads progress without intermediate body data; +plain Send, policy stop and limit-reached controls retain their ordinary errors. Two origins, +stop metadata and streamed final parity are observed through Spin's SDK. Existing bodyless, +upload, canonical-scope and attached Begin/Batch handoff controls still pass. Failure cleanup +releases pending gates, stops Spin and joins both origin servers and their request handlers. +Queue/failure checks run only after those joins, and an unexpected runtime exit fails the gate +without replacing an earlier test failure. Injected late-worker and unexpected-exit controls +first reproduce false-green outcomes, then are rejected after the fix; the normal suite passes. +The existing Spin CI job now executes this harness with the checksum-verified pinned CLI; +local macOS execution is not evidence of Ubuntu execution or hosted cancellation. + +Fastly's existing production-client progress suite preserves its twelve probes and eighteen +origin receipts, and adds a separate twenty-three-probe P5 inventory with forty-three receipts. +One case inventory feeds execution and validation. Real unsupported-token passthrough uses normal +Decode, not Original mode. Validators require exact typed cap causes and reject incomplete, +duplicate, foreign-token, wrong-mode and substituted evidence. Two-origin Send/Follow and a gated +pending oversized compressed head pass. Native optimizer Send, initial Follow and a policy- +supplied later hop reach Viceroy's typed unavailable boundary; these are compositional dispatch +controls, not a successful optimized-first-hop redirect, transformation or performance proof. +The existing CI predicate requires both inventories, rejects missing/failed/mismatched P5 +evidence and retains the expected unqualified exit 2. No prior assertion is relaxed. + +Independent specification and quality review tightened cap attribution, Follow terminal metadata +and fixture cleanup. A negative regression first accepts an invalid overflow hop and fails; +the fixed validator separately requires correlated completed-Follow metadata or the exact +head/finalization-failure index, without contradictory fields. All eight validator tests, +149 harness tests and 29 qualification-enabled fixture-core tests pass. The parent reran the +actual Spin and Fastly suites, all five root Rust gates, affected WASM checks and the full +Cloudflare suites. Docs lint/format/build, workflow lint and dependency/evidence guards pass. +Remaining P5 work includes workerd/origin parity, +successful native optimizer redirects, conditional idle-policy cases, complete criterion-level +acceptance binding and consumer/hosted qualification. The G1 inventory remains eleven ready legs, +500 planned and `qualified: false`; these controls do not activate incomplete declared legs or +freeze an exact candidate. The normative requirements and approved Interfaces are unchanged. + +### P5 Local Workerd Follow Checkpoint (2026-10-27) + +The existing Rust `outbound-wire` suite now also selects Cloudflare. Its fixture-only +`/progress/redirect` action uses the canonical request-context client, without a replacement Fetch +or another outbound transport. One matrix and the same bounded origins exercise both providers. +Local workerd 2026-09-10 with Wrangler 4.131.0, worker 0.8.5 and the existing compatibility settings +passes 68 exchanges/84 ordered origin requests. Spin retains the same 68/84 inventory and its four +bodyless, upload, scope and SDK-terminal handoff controls. Exact coding/cap parity, unread +intermediate-head progress, two-origin behavior, stopped/limited final bodies and streamed final +responses are checked. Truncated reads retain exact provider-specific reasons; workerd's observed +sanitized cause is pinned by negative validation controls rather than accepted as arbitrary text. + +Startup and teardown use the existing staged build, runtime and process supervisor. Cleanup begins +before local binding setup and handles an absent runtime/origin without discarding the primary +failure. Controlled Rust regressions reject premature exits, including 143; a wrapper's 143 is +accepted only following a confirmed sent TERM. Missing tools cannot publish a passing inventory. +The matching locked wasm-bindgen CLI was rebuilt offline in a temporary directory, not installed +globally or repinned. No deployment, SDK/runtime pin change, capability promotion, candidate freeze +or hosted qualification occurs. Successful native optimizer redirects, conditional idle-policy +coverage, complete criterion-level binding and consumer/hosted acceptance remain open. The G1 +inventory remains eleven ready legs, 500 planned and `qualified: false`. + +### Standalone Test Harness and Local Redirect Bindings (2026-10-27) + +The shared native runner now lives at `crates/edgezero-test-harness`, with its own unpublished +workspace and Cargo lockfile. The old runner package and source directory are removed without an +alias. Unit/contract tests remain in their owning crates, and deployable applications remain in +the fixture workspace. Fixture cwd, fixed build target, staging and `.runs` locations are unchanged; +the launcher resolves the repository before selecting its toolchain. Both fixture and harness +lock identities are recorded separately. The fixture lock only loses packages made unused by +the move; existing dependency versions, decoder/SDK/runtime pins and frozen documents are unchanged. + +All four adapters now execute the same 68-exchange/84-receipt redirect matrix. Spin retains its +bodyless, upload, scope and SDK-terminal controls; Fastly retains its separate progress and native +optimizer-unavailable inventories. Exact provider-specific failures remain enforced. The existing +Rust tests add the literal two-hop query-redaction and signed-date-header controls, plus first-hop +cancellation, without replacing the older controls. EZ-9 unit criteria 01-15 bind all 51 guarded +tests, and local runtime 16/17 bind six environment legs. Together with EZ-23's eleven legs, the +inventory has 32 ready and 479 planned; no applicability approval, candidate freeze, deployment, +provider capability promotion or qualification is inferred. EZ-14 remains an unadopted follow-up. + +CI and active guides use the new manifest and separately fetch, format, lint and test each +workspace. Historical commands and audit paths remain revision-specific records. Axum/Fastly/Spin +runtime jobs use the declared redirect output paths; workerd runtime CI activation remains pending +its explicit tool/runtime setup, separately from the passing local command. Remaining lifetime, +registration, consumer and hosted acceptance work stays open in the owning tasks; +this relocation does not mark the complete adoption plan finished. + +### Begin/Batch Lifetime Probe Checkpoint (2026-10-27) + +The standalone Rust harness now has `--suite outbound-lifetimes`, using the same bounded +origin collector, parser, process supervision and production-client guests as the redirect +suite. It implements the four fixed EZ-6 scenarios 1, 2, 4 and 5, with independently measured +receipts and writes, guest entry/completion times, original cutoffs, typed mediator failures +and exact mixed collection states. CPU-stretch and the other lifetime scenarios remain open. +Clock alignment is explicit, transit uncertainty remains unknown, and schedule timestamps +never substitute for actual observations. Quiet-window proofs use a conservative causal bound, +not an assumed common host/guest clock origin. Cleanup failures prevent passing inventories. + +Local characterization above `b5f94c70` found that all four adapters pass the Begin/unobserved +batch progress cases. Slow/fast timeline runs do **not** pass their strict timing targets; +those misses remain failures. Spin additionally loses the slow-origin batch: the producer's +first poll occurs only after the origin head, after the original cutoff, so no bidder request +is sent. Temporary instrumentation reproduced the cause at the existing SDK boundary: +`wit-bindgen 0.57.1` drains newly spawned tasks after returning Pending without arranging +another poll before waiting on the old host operation. The temporary prints were removed. +Current upstream code drains that work before sleeping; integration of a compatible audited +fix is a separate dependency decision, not a reason to drive producers from observers, +add another executor, extend deadlines or weaken the outbound requirements. + +The existing redirect regression and fixture/harness unit checks remain separate from these +failed acceptance runs. No new leg is marked ready: the inventory remains 32 ready and 479 +planned, with qualification false. Candidate freeze, hosted evidence, provider capability +promotion and optional idle-body adoption remain unchanged. The SDK repair and exact timing +evidence are required before these scenarios can close their owning D6/Q3 work. + +### Spin SDK Eager Progress Dependency Checkpoint (2026-10-27) + +The user requested a Stackpop SDK fork and upstream contribution rather than vendoring. +The coordinated dependency is now SDK 7 from `stackpop/spin-rust-sdk` at signed revision +`e65ff90dadf4173b08b14194e878ad3acd555337`; the proposed upstream repair is +[spin-rust-sdk#169](https://github.com/spinframework/spin-rust-sdk/pull/169). +The [dependency audit](../../audits/2026-10-27-spin-sdk-eager-progress.md) records its exact +scope, regression and verification. WASIp3 0.9.0 retains WASI 0.3.0 interfaces and selects the +corrected wit-bindgen 0.62.0 executor. The task queue makes progress without observer-driven +producers, another executor, compatibility aliases or a transport workaround. + +Root, demo, reusable fixture and scaffold pins use the same Git source/revision; all three +application lockfiles resolve that SDK and macro. Active calls migrate to `spawn_local`. +Generator guards compare source and revision as well as package version. SDK validation +reproduces the exact regression failing on the old dependency chain, then passing with the +upgrade; its workspace tests, doctests and WASM builds pass. Upstream Rust 1.94 CI remains +separate from local Rust 1.98.1 verification, and generated gRPC Clippy warnings reproduce on +unchanged upstream. The private task-context guard does not dereference executor internals. + +Local candidate characterization removes the slow-origin batch starvation and retains the +existing Spin wire and eager ownership controls. Strict timeline target misses remain failures; +this checkpoint does not close D6/Q3 or promote hosted cancellation, scheduler guarantees or +resource accounting. Original deadlines/cutoffs and all outbound requirements remain unchanged. +The frozen requirements/Interfaces and inventory are unchanged: 32 ready, 479 planned, +qualification false. No deployment is authorized by this dependency repair. + +Final adoption checks pass all five root Rust gates, both affected Spin WASM Clippy combinations, +35 contracts, 21 SDK-resource cases and the eager component on exact Wasmtime 48.0.2. Demo core +(60), fixture core (42), generated core (33), generated Spin build/strict Clippy, dependency and +documentation guards pass. The full ignored generated-workspace matrix was not rerun locally. +Spin 4.1 wire checks pass 68 exchanges/84 receipts; the four lifetime cases remain two passes +and two strict timing failures. A pre-existing trailing worker diagnostic was reproduced after +requested shutdown with identical guest artifacts; retain that caveat, not a clean-hosted- +shutdown claim. These checks do not change any acceptance binding or qualification gate. + +### Current Work Queue and Gates + +The Spin host wire suite now uses the existing Rust fixture harness's explicit +`--adapter spin --suite outbound-wire` entrypoint. The former Python runner is removed, without +compatibility wrappers or another process supervisor. +The unchanged guest retains 68 exchanges/84 correlated requests, raw framing and scope/handoff +assertions. Colocated Rust tests cover capture boundaries, duplicate fields, exact bytes, late +worker failures and process-group shutdown. The existing Spin CI job executes the shared harness +and checks its complete inventory. This tooling migration does not change production outbound +behavior, frozen requirements, provider capabilities or qualification status. + +1. **G1/Q3 acceptance binding:** replace planned placeholder commands with actual, nonzero + sentinels and environment-specific assertions/artifacts as each owning task lands. Reconcile + applicability/approval records separately; mark a leg `ready` only when its complete declared + command/assertions are implemented. Historical characterization is not exact-candidate acceptance. +2. **P1-P5 and D2-D4 integration:** bind mode/lifetime acceptance for the locally verified shared + invocation scope and canonical body handoff. Complete all-target runtime acceptance of the + implemented explicit same-client Follow policy. + Do not restore lazy compatibility implementations, duplicate schedulers or unused foundations. +3. **Fastly D1 prerequisites:** the stock readiness/error-preservation failure still gates its + hosted/runtime promotion. The scoped B31 policy and current-operation multiplexer are approved + for implementation; local/CI receive validation explicitly uses the tracked preserving repair. + The isolated runtime repair is not a changed production pin or resource qualification. + Callback-wide scheduling and shared Send/Begin/Batch ownership are integrated; verify the body-first + progress family and the approved dispatch-slack retirement. G2 does not + globally prohibit approved portable or other-adapter work; no lost-error fallback is allowed. +4. **Q1/Q2 then Q3-Q5:** update examples/docs/checks with each implementation change. Finish source + work before freezing and qualifying a candidate. Hosted deployment remains unauthorized; + entitlement, operator acceptance, measurement definitions and provider evidence remain open. + +For every subsequent behavior change, update the affected requirement row and checkpoint in the +same commit, naming its implemented scope, nonzero tests/evidence and next unclosed step. Reconcile +its inventory bindings rather than reporting a percentage from checkbox or test counts. Close a +parent task only after every applicable child and acceptance leg closes. Requirements/digest +changes need explicit review; neither a dashboard edit nor a passing checker supplies approval. + +### Shared Ownership Hard Cut (2026-10-27) + +- [x] Share validation, GET/HEAD settlement, raw-head normalization, decoding and limits across + Send/Begin/Batch; keep direct native Image Optimizer dispatch unchanged. +- [x] Capture complete unpolled operations at invocation. Revoke retained handles through one + core implementation without discarding recorded batch partials, receipts or typed errors. +- [x] Transfer exact provisional Body ownership before egress policy/framing. Keep HEAD/bodyless + work until terminal; cancel a failed application's work before fallback while holding + response completion through fallback's SDK terminal. EOF alone does not complete egress. +- [x] Use one Body source binder and fallible local task runner, including executor exit, + reentrant cancellation and supported cleanup unwind. No copied provider lifetime state. +- [x] Keep readiness probes observation-only and the literal 2/34/120/134/260 ms timeline; + preserve 146/286/366 ms receipts and the original mediator deadline without restart. +- [x] Retire Fastly's slack rejection under the separate approval. Share the earlier-own-deadline + cutoff predicate; retain B31, equality expiry and backwards-clock rejection. +- [x] Migrate and verify supplied clients, all four adapters, demo, generator, fixtures, guides, + changelog and drift guards. No compatibility alias or second transport is retained. + +Read-only reviews covered core/protocol, Axum/Cloudflare, Fastly, Spin and callers separately. +They found body-handoff, cleanup identity/unwind, nonblocking SDK-turn and duplicated task/timer +paths; regression proofs and the shared fixes above close those implementation findings. +Local parser/TCP probes required listener permission; the permission-denied sandbox run is not +passing evidence. The authorized loopback rerun passes. None of these results freezes a candidate, +promotes capabilities, closes the failed historical SDK parity controls, authorizes deployment or +changes the inventory's 500 planned legs / `qualified: false`. Follow and Q3-Q5 remain open. + +### Post-Merge Guest CI Repair (2026-10-26) + +At merge `b7a288d2`, the primary workspace gates passed but two additional CI jobs failed. +The Fastly CLI-only lint configuration exposed declaration ordering hidden by the runtime +feature's ordering exception. Reorder declarations without changing their bodies and apply +the existing conflicting module-file lint exception to the matching native test configuration. +Do not weaken ordering checks or rewrite the earlier source-hashed multiplexer evidence. + +Cloudflare's browser fixture lacked Workers' `FixedLengthStream`; its native request-body +presence check also failed before entering adapter conversion. Run the guest contracts on +the pinned Node host already used by the library tests, retaining the native body-presence +tripwire and sharing the existing scoped numeric-timer guard. A test-only fixed-length stream +model exercises the response writer without a production fallback, bodyless error renderer +or request-body polyfill. Its nine JS tests cover exact/zero/bigint lengths, both length errors, +backpressure, reader cancellation, writer abort, invalid inputs and existing-host preservation. +CI runs that suite alongside the existing nonzero WASM contract sentinel. + +Keep exact response-header map comparisons and explicitly expect canonical buffered lengths +on overflow, timeout and admission-refusal responses. Keep exact payload, status, admission, +source-release and completion assertions. Local Node execution passes all 29 contract cases; +the prior Firefox run and its body-presence failure are diagnostic evidence, not passing +provider qualification. No production response-conversion, SDK pin, capability or frozen +requirements/interface change is introduced. Full head-first/callback ownership integration, +dispatch-slack retirement, candidate freeze and hosted qualification remain open. + +Local repair verification passes all five primary Rust gates (2,257 workspace tests, one +existing ignored test), Fastly CLI-only/default/test-utils Clippy, both Cloudflare WASM Clippy +configurations, 210 Fastly WASI unit tests, 29 Cloudflare contracts, 48 Cloudflare library guest +tests, all nine JS fixture tests, docs format/lint/build and the documentation/dependency guards. +The actual-client Fastly suite again records twelve passing probes and eighteen origin requests, +with the required unqualified exit 2; this does not replace the earlier failed SDK timing results. + +### Fixture Client Selection Checkpoint (2026-10-26) + +Remove the Fastly custom fixture's manual clock-only client construction and prepare-time +HTTP-client override. Its request-local capture retains the actual `RequestContext::http_client()` +before the observation middleware awaits `Next`; routed handler errors therefore keep the client +even though the framework renders their response outside that middleware. The finalizer transfers +the captured handle through the existing owned delivery result. Post-send still requires committed +application delivery and the existing local-origin policy, and a missing capture fails explicitly +instead of constructing another client. Detached, no-attempt, abort and fallback gates are unchanged. + +Use the existing typed request-extension payload `Arc>>`, not another +transport, public factory or compatibility API. Each callback allocates its own capture and releases +the lock before awaiting. App-state client precedence remains unchanged. Tests cover success/error +rendering, effective request/state client selection, one consuming transfer, two-request isolation, +and absent/unrouted clients without a fallback transport. The capture regression failed before +middleware integration and passes afterward. The actual local Fastly smoke suite preserves +progressive/large delivery, source-error reporting, post-send work, fallback exclusion and retained +callback behavior. This is fixture preparation for the paired-time/full-protocol migration, not +completion of P1/D1 or promotion of any acceptance/hosted qualification leg. Production APIs, +default clients, clocks, caps, direct image optimization and frozen digests are unchanged. + +Verification passes all five primary Rust gates (2,272 workspace tests, one existing ignored), +all 23 fixture-core tests with qualification enabled, fixture-core/Fastly guest Clippy, and +all four fixture target builds. The real local Fastly smoke suite exits 0 with its existing +progressive/source-error/post-send/fallback assertions intact. Docs format/lint/build and +the outbound documentation/inventory guards pass; the inventory remains unqualified. Two +independent read-only reviews found no actionable issue in the fixture implementation. + +### Existing-Batch Scope Checkpoint (2026-10-26) + +Implement the private D1a ownership boundary for complete current batch drivers, without +splitting the frozen public protocol. `Sandbox::handle` installs a fresh callback guard for +both custom wrappers. Canonical send helpers install a fallback only outside an existing +scope. Each response transmission has an independent owner for work created during body +production, restored to the active callback at transmission exit. Saved clients resolve +scope at batch creation, not client construction or later driver polling. + +Weak registrations track only live driver roots and unregister on terminal/drop paths. +Scope exit first revokes every root, then drops whole streams outside registry/driver borrows, +including queued results, pending processing and decoder state, and wakes waiting consumers. +The closing scope stays active until cleanup ends, rejecting reentrant Send/Batch starts before +clock/preflight/provider work; nested cleanup scopes cannot reopen it. Waker cloning/destruction +runs outside driver borrows, with a final closure check preventing a stranded Pending result. +A retained +handle emits one typed Internal scope-closure failure instead of Cutoff or clean EOF. Core's +existing poison and partial-failure collection rules remain unchanged. Closure after the last +item but before validated EOF is still a failure, never inferred completion. + +The actual Fastly regression `callback_scope_exit_releases_retained_batch_driver` first failed +because the unpolled driver's captured clock owner survived callback exit, then passed after +integration. It uses queued preflight results to avoid claiming SDK-harvest coverage. The +native response regression `body_created_batches_end_at_egress_without_closing_callback_work` +likewise fails without the transmission guard, then passes for completion, source/write/finish +failure while callback/post-delivery work stays live. The callback batch is polled inside body +production without being reparented. Colocated tests additionally cover +Ok/Err/supported unwind, partial collection, last-item/EOF, wakeups, unregistering, helper-scope +reuse and the real-client `retained_client_resolves_fresh_callback_scope` across two invocations. +The shared production/test helper lifecycle has a WASI fallback/reuse contract. Reentrant +destructor and waker-drop regressions were observed failing before the cleanup-order fixes. + +This does not complete D1a, P1/P4 or an acceptance/qualification leg. Retained Send roots, +response ownership transfers, callback-wide scheduling, unpolled eager modes, arrival recording, +paired-time/trait migration and dispatch-slack retirement remain open. Neither dropping an SDK +handle nor local cleanup proves host cancellation. No public API, SDK/runtime pin, B31 policy, +Image Optimizer path, frozen requirement/interface digest or capability level changes. +Demo/templates need no API migration for this adapter-private checkpoint; their forthcoming +head-first migration remains atomic with all implementers/callers. Hosted deployment is not run. + +Verification passes all five primary Rust gates (2,272 workspace tests, one existing ignored +test), Fastly CLI-only Clippy, WASI Clippy with/without test-utils, 226 Fastly WASI unit tests +and 36 WASI contracts. Docs format/lint/build, legacy/API/docs/decoder guards and unchanged +frozen requirement/Interfaces-plan hashes pass. The actual-client functional suite again records +twelve passing probes and eighteen origin requests with the expected unqualified exit 2. +Independent review reproduced the reentrant failures before correction and cleared the focused +scope implementation afterward. Existing SDK timing failures and hosted gates remain unwaived. + +### Current-Operation Multiplexer Checkpoint (2026-10-25) + +Implement the requester's [scoped approval](../../audits/2026-10-18-outbound-api-approval.md#scoped-fastly-current-operation-multiplexer-approval-2026-10-25) +in the live Fastly Send/Batch paths. A single private per-operation engine services raw batch +heads and demanded response bodies using validated borrowed-handle timed selection. Each raw +body read consumes fresh matching readiness and reads at most 16 KiB. Rotate slots, retain only +one demanded chunk per body and treat occupied result mailboxes as per-slot backpressure. +Cached SDK Send heads remain opaque and use nonblocking polling with one shared positive 1 ms +pulse. Remove the old blocking head waits, selected-body drains and ownership-reassociation code. + +Preserve B31, original logical deadlines and causes, observation cutoffs, partial batch results, +application clocks, all payload/header/codec limits, provenance and direct native Image Optimizer. +Sample the clock after I/O and reject late success while preserving typed source-error details. +Local stream/lease ownership drops guest-side SDK owners on terminal/drop paths without a registry cycle. +Core drops batch drivers before returning terminal/poisoned observations, including retained +batches. The outer Fastly deadline guard retains typed source details and drops the source before +yielding terminal errors. Dropping guest owners does not prove that pending host work is cancelled. +SDK cleanup, cold registration, streamed writes, blocked inbound pulls and the native optimizer +retain their separately disclosed non-preemptible boundaries. + +The real-client mixed-body/head regression failed against the prior serial drain. Deterministic +transport tests exercise fairness, per-read permissions, deadline equality, clock regression, +spurious readiness and cancellation. Review additionally found an occupied-mailbox starvation +bug; a bounded-poll red/green test now covers it. The runtime fixture also checks exact payloads, +typed multi-buffer source errors and request-correlated bounded origin receipts. Local timing +or preflight failures remain failures, not evidence of passing progress. + +CI builds the exact Viceroy source plus tracked repair in isolation, verifies source/lock/patch +identities and upstream readiness tests, then selects its absolute executable explicitly. +The stock installation and dependency/runtime pins remain unchanged. Ordinary CI selects the +explicit functional-only suite and requires its complete passing client inventory, no failure +records and exactly the exit-2 unqualified disposition. The full SDK timing diagnostic stays +separately available with its strict assertions and exit behavior unchanged. + +This does not complete D1a, P1 or any acceptance/qualification leg. Callback-wide scheduling, +independent callback/body cancellation owners, eager Begin, prepared head-first dispatch and the +atomic four-adapter public trait/caller migration remain open. So do dispatch-slack retirement, +total resource accounting and hosted qualification. No compatibility branch, alternative +transport, capability upgrade or frozen Interfaces/requirements change is introduced. + +Verification on 2026-10-26 passes all five primary Rust gates: 2,257 workspace tests with one +existing ignored test, strict Clippy and all required feature/Spin target checks. Fastly passes +210 WASI unit tests and 35 runtime contracts under the repaired runtime. The standalone harness +passes 135 tests and strict Clippy; docs format/lint/build, docs/legacy guards, decoder/map audits +and the 34 cutover-evidence tests pass. The requirements and complete frozen Plan 02 hashes are +unchanged. + +The [new local record](../../audits/evidence/2026-10-26-fastly-cooperative-progress.json) retains +all five runs, including the serial-drain red result and failed integration attempts. Both final +unchanged-build runs pass all twelve actual-client cases with eighteen correlated origin +requests, preserving exact bytes and typed buffered/streamed truncation errors. However, the +full 153-probe SDK diagnostic exits 1 in both runs: the first misses two +5 ms timing targets; +the replication misses twelve. These are not passing full-matrix or qualification results. +The requester directed continuation after the recommended +[functional-versus-timing CI split](../../audits/2026-10-18-outbound-api-approval.md#functional-ci-validation-decision-2026-10-26). +The new Fastly-only `outbound-client-progress` suite reuses the same client probes, validators, +guest and bounded origins, returning before SDK probes or timer controls run. It does not make +the failed full diagnostic pass: no validator is weakened and no failure is relabeled as success. +Four additional parser/aggregation tests cover suite selection, adapter exclusion and retained +failures; all 139 harness tests and strict Clippy pass. The missing temporary repaired runtime +was rebuilt from the exact upstream/lock/patch inputs in an isolated worktree target cache. +Its ten readiness tests, 24 body/collector tests, 210 Fastly WASI unit tests and 35 runtime +contracts pass, including the exact no-config runner form used by CI. + +The functional suite passes on that repaired runtime with twelve correlated probes, eighteen +origin requests, no SDK/timer probes or inventories and exactly exit 2. The identical guest on +the unchanged stock runtime loses all six typed truncation errors and exits 1. The exact CI +guard accepts repaired evidence and rejects both stock evidence and an unavailable-runtime +exit-2 run with no inventory. The [same audit record](../../audits/evidence/2026-10-26-fastly-cooperative-progress.json) +retains these controls and all earlier failed full-matrix runs; its 48 client observations do +not qualify timing, callback-wide ownership or hosted behavior. + +### Mixed-Readiness Diagnostic Checkpoint (2026-10-25) + +Extend the existing diagnostic, not production dispatch: eighteen supplementary probes cover +pending-head/ready-body, pending-body/delayed-head and multi-buffer truncation. Both consumers run +three repetitions at 16 KiB capacity, requiring a real full-sized return. Shared trace validation +checks every live mixed handle, fresh matching readiness, exact payloads/errors and bounded joined +origins with the recorded delayed-head control. Keep the original 135-probe matrix and historical +negative replays unchanged. The [new record](../../audits/evidence/2026-10-25-fastly-mixed-readiness.json) +retains all three runs, including the first repaired timing failure. + +The formatted stock/repaired pair has identical guest bytes. Stock loses thirty source errors +(six new and twenty-four baseline); repaired preserves all and passes 153 probes/177 origin +requests, but exits 2 unverified. Its earlier two timing failures remain failures. Every new probe +records a full 16 KiB read; mixed cases complete the ready sibling before the pending slot expires. +This closes narrow local proof gaps only: continuously-ready multi-source fairness, production read +maximum/primitive approval, runtime promotion, resource accounting and hosted qualification remain +open. No full D1a task, acceptance leg or capability is promoted. The public APIs, B31 production +policy, original logical deadlines, all outbound guarantees and frozen requirements are unchanged. + +Verification also exposed the existing Axum partial-collection test's 25 ms wall-clock race. +Replace sleep-based ordering with an explicitly gated slow origin and coordinated injected clock: +the actual adapter's fast item reaches `collect()` before time advances to the same logical cutoff. +Paused native timers avoid unrelated real-time expiry; an independent five-second real-clock +watchdog bounds the test's self-waking I/O loop. Preserve the successful first slot and unresolved +second slot assertions. This is a test-only repair, not a new timer API or production behavior. + +All five primary Rust gates pass on the stable source, including 2,248 workspace tests and one +existing ignored test. The standalone fixtures pass 128 harness and six core tests; strict harness +and actual Fastly WASI diagnostic Clippy pass. Docs format/lint/build, docs/legacy guards, 34 +cutover-evidence tests and frozen approval identity verification pass. The acceptance inventory +remains 255 criteria/511 legs, eleven ready and 500 planned, with `qualified: false`. Independent +reviews cover the trace/receipt changes and the real-adapter partial-collection test separately. + +### Existing-Path B31 Implementation Checkpoint (2026-10-25) + +The requester explicitly approved scoped B31 production implementation in the +[approval record](../../audits/2026-10-18-outbound-api-approval.md#scoped-b31-implementation-approval). +The existing Fastly Send/Batch path now selects coupled provider timers from the clamped +configured timeout, or its original selected duration when no timeout is configured. True +ceiling-to-millisecond rounding followed by the 31-member ladder never rounds the logical +deadline. Method-entry capture, cutoff eligibility, typed errors and all payload/header limits +remain unchanged. Facade-invocation capture is still part of the later atomic migration. + +Remove client namespaces, exact-budget configuration names, per-client caches and the rigid +quarter/three-quarter split. A single verified guest-instance registry serializes registration, +compares complete identities, rejects unknown name collisions and retains a successful late +registration before returning the original timeout. Versioned names hash canonical transport +fields and the chosen timer. The registry retains no request clock, body, callback or exchange. +Request parts move once through preparation; no new public getter, reconstruction, body copy, +transport or compatibility branch is introduced. Direct native optimizer dispatch is untouched. + +Behavioral regressions first fail against the old policy, then pass under the real pinned WASI +runtime: equal-timeout/different-deadline identity, 93 boundary samples/31 configurations, coupled +phases and cross-client SDK registration reuse. Controls cover observation cutoffs, maximum +timeouts, preserved parts, collision/failure retry and late-success retention with expired cache +access rejection. Actual SDK readbacks preserve target host/port, authority and TLS for DNS, +IPv4 and IPv6; timing getters remain unavailable. All 202 Fastly WASI unit tests and 35 runtime +contracts pass in the current checkpoint. + +Both investigations selected one shared authoritative registry over duplicate client caches. +Separate reviews preserve the scope limits: no readiness, runtime-pin, public protocol, dispatch +slack or capability change. Explicit Connect/idle settings, registration observation, F1 retirement, +total resource accounting, hosted pooling and qualification remain open. No full D1b/EZ-2 task +or acceptance leg is closed by this subset; the original proposal and negative evidence stay +historical, unchanged records. + +Verification passes all five primary Rust gates (2,248 workspace tests; one existing ignored), +strict Fastly WASI Clippy, and the Fastly demo target build. Native count is one lower because +the obsolete namespace-isolation test was replaced by the WASI shared-registry proofs. The docs +checker now rejects the two stale public exact-budget/phase-split descriptions; it failed against +the stale footnote and passes after correction. No acceptance leg is promoted by these checks. +The existing Fastly loopback serving smoke also passes at +`/private/tmp/edgezero-fastly-b31-smoke-20261025`. This is serving/lifecycle regression coverage, +not a hosted backend-pooling, interruption or finite-resource qualification result. Demo code +builds unchanged and generator tests pass through the workspace suite; no public API migration +or caller compatibility wrapper is needed for this private policy change. + +### G2/G4 Policy Review Packet (2026-10-25) + +Historical proposal checkpoint; the scoped implementation status above supersedes only its +implementation-approval gap, not its remaining primitive/resource/qualification decisions. + +The [review packet](../../audits/2026-10-25-fastly-backend-policy-proposal.md) proposes literal +B31, selected by the requester for final review on 2026-10-25, with a B1 comparison, full positive +selected-budget/timeout-anchor domains, upward integer rounding and complete coupled timer-vector +counting. Its executable proposal model passes +93 boundary samples and 16 timeout-anchor controls, including equal timeouts across different +deadline-only buckets. This is not production code, SDK setter evidence or policy approval. + +Two read-only investigations/proposals reviewed timer/resource tradeoffs and the exact repaired +primitive separately. The packet preserves stock error loss, repaired negative timing traces, +untested full-sized-copy and mixed-head/body fairness domains, SDK/provider distinctions and +Viceroy's unavailable timer getters. No production API, backend, timer, slack, resource limit, +dependency or runtime pin changes. G2's gate is not passed, no acceptance leg is promoted, and +no conditional follow-up is adopted. The next step is explicit candidate/resource/primitive +review in G4, followed by its still-required evidence and the approved atomic migration. + +### G2/G4 SDK Registration Checkpoint (2026-10-25) + +Add the WASI-only `proposed_b31_timer_vectors_register` to the existing Fastly outbound unit +module, not a production policy helper or alternate backend path. All 31 candidate vectors +register with coupled Connect and with explicit 200 ms Connect (62 registrations); three +independent oversized-duration controls preserve the exact SDK error and input. Removing each +setter makes the corresponding assertion fail, then the restored guarded test passes. The +existing Fastly WASI CI suite includes it without another filtered command or job. + +The [feasibility checkpoint](../../audits/2026-10-18-outbound-feasibility.md#b31-sdk-registration-checkpoint-2026-10-25) +records the SDK/runtime/source/guest identities and exact command. This adds real pinned-SDK +serialization/local-registration evidence to the arithmetic proposal, not hosted timer acceptance, +Connect enforcement, pooling, resource certification, readiness/error-preservation or production +policy approval. No production implementation, public API, deadline, source pin or capability +changes; G2/G4, the atomic migration and all acceptance-leg statuses remain open as before. + +Verification passes all five primary Rust gates (2,249 workspace tests; one existing ignored), +strict Fastly WASI Clippy, all 194 real Fastly WASI unit tests including the guarded proposal +probe, docs format/lint/build and frozen API identity verification. The selected production-policy +implementation decision remains separate; successful local registration does not authorize it. + +### G1/F9b Inventory-Driven Suite Consolidation (2026-10-25) + +Replace the binding checkpoint's eleven filtered commands and separate factory guard with one +`--all-targets --features axum,test-utils` suite execution in the existing Axum CI job. The existing +nonzero guard's `--inventory axum/native-tcp` mode obtains ready sentinel names from the existing +evidence checker, validates every exact name in the listing, and executes once with +`--include-ignored`. Criterion 10 records its factory test in `additional_sentinels`; there is no +second CI whitelist or command runner. All host CI legs explicitly install the pinned Node +version because their existing guards already use Node. + +Regression tests first failed on the missing inventory mode/schema, then passed after the minimal +extension. Deleting any of the twelve required names rejects execution. Empty ready selections, +malformed additional names and suite failures remain errors. The historical execution and +verification record below is unchanged; the shared command is now the inventory's current binding. +The 255 criteria / 511 legs, eleven ready / 500 planned, applicability, approvals and +`qualified: false` remain unchanged. This consolidation changes tooling and tracking, not outbound +runtime behavior or qualification status. + +Verification passes the consolidated 245-unit/17-contract Axum suite, all 44 guard/evidence +regressions, and all five root Rust gates. Independent inventory review confirmed preserved +bindings; portability review found no code defect. Its documentation cross-reference correction +is applied. These are local checks, not new release or hosted evidence. + +### G1/F9b Local Acceptance Binding Checkpoint + +- [x] Bind EZ-23 criteria 01-11 at `axum/native-tcp` to the existing guarded Cargo tests, + with explicit filters and `--include-ignored` during both discovery and execution. + Preserve all 511 case/environment identities, source text/digests, applicability and approvals. +- [x] Strengthen criterion 01 with a 100-byte target accepted by the same unconfigured router. + Observe the new control fail before adding the test-only route, then pass with configured + 414 through both server paths and unconfigured 200. This is a current positive control, + not a newly executed historical-F comparison. +- [x] Run all eleven inventory commands: twelve distinct tests pass, including both the supplied- + client TCP and default-factory failure-seam tests selected by criterion 10. Guard the factory + sentinel separately in the existing Axum CI job so deleting it cannot silently reduce coverage. +- [x] Observe the inventory-binding regression fail while the legs are still planned, then pass + after wiring the exact commands into the existing CI job. No new checker, execution framework, + capability or job name is introduced. +- [ ] Implement criterion 12's begun-request branch after the eager API migration and obtain + criterion 13's consumer-owned route/parity evidence; both legs remain `planned`. +- [ ] Reconcile the remaining 500 executable bindings and all required applicability decisions; + freeze and qualify a candidate separately through Q3-Q5. + +The inventory now reports 255 criteria, 511 legs, 500 planned and `qualified: false`. +The eleven ready legs point to future raw `.log` artifacts rather than nonexistent JSON exports. +Frozen-candidate execution must capture stdout/stderr there, preserve the guarded exit status +and bind the actual source/build identity through Q3. The local runs are not that record and +neither readiness nor CI supplies promotion/confirmed-present approval for these follow-ups. +Criterion 10 proves avoidance of automatic default transport initialization; its separate factory +fixture may itself construct a client. No network restriction or hosted behavior is inferred. + +Only acceptance bindings, tests, CI and tracking documentation change. The production APIs, +transport paths, SDK/runtime pins, frozen Interfaces, requirements and retained failed evidence +remain unchanged. Full P1-P5/D1-D6 integration and every existing Fastly/provider gate remain open. + +Verification passes all five primary Rust gates, 2,244 workspace tests (one existing ignored), +245 scoped Axum unit tests and 17 contracts, 32 evidence-checker and seven nonzero-guard tests, +docs format/lint/build, docs/legacy guards and the approval-identity verifier. Independent +inventory and criterion-coverage reviews selected the existing tests/CI path plus the missing +positive control, not a duplicate command registry or execution framework. Subsequent spec and +quality reviews found no remaining scoped defect. Generated-project and hosted qualification +were not executed or credited by this checkpoint. + +### Spin Existing-Path Support Precedence Checkpoint (2026-10-25) + +The live Spin Send/Batch validators now reject unsupported authority overrides before portable +method/body/response-policy errors, retaining optimizer-first ordering and unchanged diagnostics. +Native red/green coverage proves a pending GET stream previously masked the authority error. +The regression covers GET/HEAD/CONNECT/POST, invalid Brotli settings and zero timeout with no +body polls; the actual SDK test exercises both Send and Batch. See +[F3c's scoped checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#existing-path-support-precedence-checkpoint-2026-10-25). + +That ordering repair changes no public signature, client constructor, timer, transport, runtime +pin, cutoff rule or capability. The separately approved core helper change is recorded below; +neither slice completes shared preparation, P1/F3c/D4, or qualification. + +### Spin Connect-Refusal Checkpoint (2026-10-25) + +Complete the independent D4/EZ-4.8 refusal-handling subtask on the current live transport: +Connect `NotSupported`, `Immutable` and `Other` are advisory, without provider text or fabricated +timeout evidence. Keep the first-byte/between-bytes typed error policy, all configured timer values, +the absolute budget, outer race and exchange ownership unchanged. This adds no request option, +compatibility path, new transport or capability claim; configured connect mapping and full D4 +remain unfinished. + +The SDK refusal matrix failed red on Connect `Immutable`, then passes all five outcomes while +retaining exact nonconnect diagnostics. A separate test-utils probe executes under the existing +local Spin progress fixture's async service export: five outcomes by four budget sources by three +cases (ready success, pending timer wakeup, expired entry), 60 cases with captured-owner release +and no inherited refusal detail. Pending operations never self-wake; the SDK timer must make +progress even with the injected timestamp frozen. The synchronous SDK command runner cannot block +on an async import, confirmed with an isolated sleep control, so it is not credited with pending +timer evidence and no executor workaround remains in its tests. + +`scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-progress --output /private/tmp/edgezero-spin-connect-fallback-20261025-r3` +passes under Spin 4.1.0 with SDK 7.0.0. The retained events identify this as working-tree +characterization based on `622f996a`, not frozen-candidate qualification. No worker panic occurred +before shutdown; the existing post-shutdown runtime panic remains recorded, not relabeled as +bounded host teardown. All 15 SDK-resource tests and 119 fixture-harness tests pass. All five +primary Rust gates, both eight-feature Spin matrices, strict WASM Clippy with and without +test-utils, the demo check, docs lint/build, docs/legacy guards and approval-identity verification +pass. Independent spec and quality reviews found no remaining scoped blocker. No acceptance leg +is promoted; the full atomic migration and Fastly/hosted gates remain open. + +### Response Handoff Deadline Checkpoint (2026-10-25) + +The current live Axum, Cloudflare and Spin converters checked the budget on entry, but could +return a successful bodyless response or lazy payload head after normalization without a terminal +sample. Fastly already checked those success branches. Direct converter regressions reproduced +late success; the new checks reject equality and later observations with the original selected +BudgetSource. This repairs the protected baseline rather than changing the approved public API. + +All four converters now build the disposition-selected body and use one successful epilogue with +one final budget check and one response constructor. Remove their eight duplicated early-success +constructors, not keep another finalization path. Source/header/cap errors retain their existing +early error exits and diagnostic handling. Preserve each provider's native-body construction and +ownership points, raw read timers, bodyless/205 suppression, lazy payloads and independent caps. +Core header and payload policies remain shared; this is not full head-first finalizer integration. + +Native Axum and Cloudflare Node/WASM tests first failed on late Ok, then passed. Matrices cover +HEAD/200, 204, 304, declared 205 and streamed payload heads, before/equal/after expiry and four +budget causes. Bodyless cases run both buffered and streamed modes. Axum proves zero source polls +and source release; Cloudflare tracks a high-water-mark-zero native stream for its status-200 +cases, plus abort ownership and unchanged known-empty 205 disarming at late handoff. Its 204/304 +controls use actual empty native responses, not tracked-source evidence. Preserve Fastly's existing +late-handoff control unchanged. + +Spin's ordinary library test binary imports its KV service and cannot link under plain Wasmtime; +that attempt is not a passing test. Reuse the existing SDK-resources binary and existing test-only +clock seam to execute the private conversion matrix without another public API. The actual SDK +case fails before the Spin repair and passes afterward. It checks native consumer release through +peer write rejection while producer/completion handles are still held, not by dropping the fixture +first. The same private matrix remains colocated with conversion for a full Spin test host. + +Two read-only investigations compared adding repeated checks or another public finalizer helper +with this single-exit repair. The selected path removes duplicate success handling without adding +an unapproved context constructor or bypassing the pending atomic migration. Demo/template APIs, +batch eligibility/arrival/cutoff rules, SDK/runtime pins, capability levels, frozen Interfaces and +requirements remain unchanged. Full P1/P4/P5/D1-D6 and resource/hosted qualification stay open; +no inventory leg becomes ready or qualified from this checkpoint alone. + +Verification passes all five primary Rust gates, three WASM target lint scopes plus Spin's +non-test-utils scope, the inventory-bound 246-unit/17-contract Axum suite, 193 Fastly SDK unit +tests and 35 contracts, 48 Cloudflare Node/WASM tests, and 14 Spin SDK tests. The demo workspace +all-target/all-feature check, forwarding doctest, docs format/lint/build, docs/legacy guards and +approval-identity verification pass. Final spec and quality reviews found no remaining scoped +blocker after correcting the Spin test-only import. No new browser or hosted qualification is +claimed; the full Spin KV-hosted unit suite was not executed by the SDK-only runner. + +### Shared Live-Validation Checkpoint (2026-10-25) + +The requester's scoped approval hard-cuts the public helper to +`validate_for_dispatch(&request, OutboundSendMode)`. All four live Send/Batch paths now use the +same common validator; the four copied buffered-batch checks are removed. All old in-tree calls +are migrated directly, including the external compile contract and optimizer fixtures. There +is no one-argument wrapper. Target-specific unsupported checks still run first, including +Fastly's existing optimizer Batch diagnostic and Spin's optimizer/authority ordering. + +Core regressions cover Send/Follow/Begin/Batch validation without body polling, method and upload +cap precedence, and unsupported optimizer modes before ordinary validation. Begin does not inherit +Batch's buffered-response restriction, but rejects streamed uploads as EZ-6.3 requires. The +corrected upload assertion failed before adding the Begin guard. The new external contract first failed to compile against +the old signature. The scoped core run passes 869 unit tests and four public API tests. Adapter +and target checks verify the actual live paths separately; these tests do not implement Begin, +Follow, paired invocation-time preparation, an eager scheduler or new arrival semantics. + +The original frozen Interfaces and requirements, Fastly G2/backend-policy gates, runtime pins, +cutoff short circuit, existing transport behavior and capability levels remain unchanged. Demo +and templates do not call this adapter helper; their application request API needs no shim or +cosmetic migration. The full atomic client/driver migration remains P1/P4/P5 and D1-D6. + +Local verification passes all five primary Rust gates, all three scoped WASM Clippy checks, +seven eight-feature adapter/target matrices, the native adapter contracts, 35 Fastly/Viceroy +contracts, 13 Spin WASI SDK tests and 47 Cloudflare Node/WASM unit tests. The excluded demo +workspace passes its all-target/all-feature check. Docs format/lint/build, the forwarding doctest, +docs/legacy guards and approval-identity verifier pass. Independent source-contract and code-quality +reviews found and corrected the Begin upload assertion, then found no remaining scoped blocker. +The Cloudflare browser-contract attempt failed in the local Safari WebDriver (HTTP 500/SIGKILL), +so it is not credited as a passing test or replaced by the Node results. Consumer suites and +hosted qualification were not run. No acceptance leg is newly ready or qualified. + +### Consumer Contract Retention Audit (2026-10-25) + +Reviewed external consumer PR #21 at exact source commit +`1626f72fca8d837dba697977eaa139dd4694ffd3`, using immutable Git objects rather than its working +tree. Its six workspace declarations and eight resolved SDK packages pin +`bc300ace5ca1ffe7060594a7bdb01c1da80de305`, not the older revision still described in its PR body. +This is a source-level integration/requirements audit, not execution of that application's +tests, proof of its resource arithmetic, or candidate qualification. Keep the Non-Negotiable +Baseline above every TS consolidation decision. Do not preserve old APIs for source compatibility +or drop a consumer contract to make the new pipeline simpler. + +| Surface | Current Contract And Hard-Cut Path | +| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Buffered wave dispatch | The consumer submits buffered requests through `HttpClient::start_batch_until`, observes indexed completions and distinguishes Completed/Cutoff/fault. Preserve positional identity, trailing-event validation, partial failures and cancellation. Eager implementation replaces the driver protocol, not these semantics. | +| Independent limits and cache policy | The consumer supplies absolute call deadlines, Bypass, upload/encoded/decoded/final/header/decoder/window/chunk caps. Keep each independent. Original coding for proxy use stays explicit; ordinary decoded responses and typed passthrough reasons remain available. Application wave sizing and protocol classification stay application policy. | +| Timing and clocks | Keep the application clock domain, one terminal per-slot elapsed sample and no later sibling-based reinterpretation. The paired-time cutover must migrate native and scripted clients together without reanchoring absolute call deadlines or inventing Native guarantees. | +| Header provenance | The consumer's source uses the retired public normalization helper in tests. Migrate those fixtures to `OutboundHeaderInput::normalize`, supplying actual header fidelity and policy. Preserve original Content-Type evidence through filtering and body lifting; never reconstruct eligibility from surviving text headers. No compatibility export or extra raw-header copy. | +| Implementer and collection APIs | Scripted/test clients currently implement send/raw-request batches and construct indexed events. P1 migrates them to prepared contexts, raw terminal observations and one exchange owner. Replace mutable receipt/collection field access with the approved consuming accessors and explicit AlreadyDelivered/Resolved/Unresolved states; preserve errors and owned partial results. Do not retain old constructors, lazy defaults or raw-count overloads. | +| Configuration and request/response lifetime | Keep typed declared-store failures, bounded extraction/structural limits, admission/fallback grants, detached Send/Abort, completion join/late binding, and absolute/relative write policy. Startup registry, credentials, catalog/schema residency and application admission sizes remain application-owned. Adapter cancellation/allocation evidence remains capability-scoped. | +| Axum resolved resource facts | **Open implementation gap:** the runner enforces configured connection/parser limits but assembly still receives static unknown `AXUM_PLATFORM`. A scoped follow-up must derive metadata from the same validated runner configuration before assembly, accept only explicit operator ceiling/scope declarations with provenance, and test independent ceiling/population changes, invalid startup settings and unchanged unknown facts. Do not publish an application arithmetic cap as an OS quota. | +| Hosted ingress resource facts | **Open enforcement/evidence gaps:** Cloudflare has no justified per-instance inbound-population bound; all-target host-ingress accounting remains unknown. A guest gate must precede the allocation it claims to bound, and cannot certify earlier provider-created storage. Keep Unknown/Indeterminate until enforced bounds or provider evidence exist; outbound connection limits are not inbound limits. | +| Deployment and qualification | Hosted versus generic Spin selection and application stack requirements need deployment/application evidence. Raw ingress, egress, deadlines and total provider allocations retain their actual capability levels. No full resource certification, hosted approval or consumer repin is implied by green EdgeZero unit tests. | + +Required migration controls, not claimed executions: + +- A buffered successful slot must already own a complete, finalized body under every effective + cap. The consumer's subsequent collection uses an unbounded numeric sentinel in reliance on + that contract; head-first dispatch must not substitute an unfinished stream for buffered success. +- Preserve decoded-by-default bidder responses. Original coding is the explicit proxy policy, + not a setting used by the audited buffered client. Compressed 206 is a separate migration case: + current EdgeZero preserves partial-representation coding, whereas the pinned consumer's cap-row + classifier assumes every supported coding is already decoded. Require status/coding-aware + application handling and a regression; do not undo 206 preservation or silently decode it upstream. +- Bind captured extraction funding before the read, retained configuration through rejection and + terminal egress, completed response storage outliving stream leases, and boot/strict resource + parity to the existing consumer test targets `config_funding`, `auction_config_retention`, + `response_retention` and `boot_strict_parity`. Rerun them with the selected pin and migrated + fixtures; their source presence is not a passing execution record. +- Preserve upstream terminal timing without a later sibling-based recheck, while retaining the + consumer's separate deadline/elapsed accounting for its own settlement work. Driver failures + retain valid upstream partials; application policy may still reject the entire operation. +- The older TS companion draft's retained application transport, universal ingress buffering and + custom lifecycle exceptions are superseded by the approved requirements here. They are not a + fallback plan or permission to retain the plumbing this migration removes. + +The Axum resolved-fact and hosted-resource follow-ups are recorded requirements/gaps, not added +runtime implementation, approved policy changes or newly ready legs in the existing 511-leg +inventory. They need their own scoped design/acceptance before implementation. Keep their eventual +consumer repin atomic with source and fixture migrations; never solve them with a second transport, +unknown-as-zero accounting, or removal of the existing buffered fanout behavior. + +### P2 Cloudflare Native-Body-Absence Checkpoint (2026-10-25) + +Continue P2's existing known-empty ingress subtask without changing the frozen Interfaces, +requirements, current public client protocol, SDK/runtime pins or provider capability claims. +Both direct conversion and admitted dispatch preserve native Fetch `body == null` as +`Body::empty()`. Remove the unsupported ArrayBuffer fallback and unused attachment helper. +Every present source retains the existing lazy reader/deadline/error path, including empty +ArrayBuffer/Uint8Array bodies. Generic source-test seams still represent present streams. + +The two source investigations confirmed that worker 0.8.5 keeps no alternative payload buffer: +its native-null HTTP conversion already selects empty. A prior SDK read can change private +bookkeeping without creating a source; it cannot justify reading a null body. Consumed present +bodies keep their construction error. The smallest implementation preserves absence internally +with `Option`, without another transport, public API, header inference or eager collection. + +Native Node/WASM regressions first demonstrated streamed null bodies on direct and admitted +paths. They cover GET/HEAD/POST absence, present empty/nonempty buffers, expired first reads, +sticky poisoning, unchanged Initial/HostManaged state and refusal/abort before source inspection. +The test delivery seam inspects ingress envelopes; it is not a hosted egress proof. The existing +workerd empty probe separately records the original native snapshot and pre-collection body +storage through production dispatch. Its fixture/validator regressions reject contradictory, +missing or mistyped source evidence. General settlement, support-first paired timing, full +P1-P5/D1-D6 migration and Q3-Q5 qualification remain open. No acceptance leg is promoted here. + +Local Node/WASM verification passes 47 adapter tests; the fixture passes 21 core and 119 harness +tests. Actual local workerd 1.20260910.1 with Wrangler 4.131.0 passes all five source-presence cases +and the existing coding, marker, relay and progress/framing groups. The 223-event log is +`/private/tmp/edgezero-cf-null-body-20261025-c/events.jsonl`, SHA-256 +`5b035c59b03f83c696f378629da8fe2a52c0bee20221c2395becd16142ec46cd`, recorded as working-tree +characterization against base `a0d31519`, not frozen-candidate evidence. The preceding unavailable- +CLI and asdf-shim failures remain in the `20261025` and `20261025-b` run directories. Version-matched +isolated tools and an absolute installed Wrangler path repaired tooling without changing pins. +All five root Rust gates, target/fixture strict clippy, docs formatting/lint/build, forwarding +doctest, legacy/docs guards and approval-identity verification pass. Independent specification +and implementation reviews found no remaining scoped issue. + +### Scoped Timing and Send-First Override (2026-10-24) + +The requester's [scoped approval](../../audits/2026-10-18-outbound-api-approval.md#scoped-timing-and-native-integration-amendment) +supersedes earlier preimplementation finite-H(K) and timing-only G2 blockers below. Original +logical deadlines, sources and late-result rejection remain exact; +5 ms stays a measured +qualification target with unchanged tests and negative evidence, not a hard scheduler guarantee. +The synchronous optimizer blocking horizon is explicitly **UNKNOWN** until provider evidence. +Hosted/performance qualification remains blocked; no historical experiment is relabeled. + +The [D1d Send-first checkpoint](2026-10-18-trusted-server-outbound-03-drivers.md#send-first-checkpoint-2026-10-24) +is an implementation sequencing decision under that authorization, not approval of different +signatures. It attaches typed options to requests/parts, validates support first on all four +adapters, and maps Fastly single Send directly through existing `execute`/`process_response`. +Keep the ordinary SDK send branch and current trait unchanged; introduce no separate transport, +compatibility helper, default or alias. Typed optimizer-unavailable 502 and post-return original +attributed 504 are required, with existing limits, lazy streams, cleanup and target/header fidelity. + +Budget selection still occurs at existing adapter async `send` method entry on first poll, before +SDK preparation/registration; the checkpoint preserves that selected budget across the native call. +It does not introduce facade-invocation capture or complete P1. Invocation-time capture remains +pending with the later atomic head-first/full-trait cut, alongside the existing open engine work. + +Full EZ-1 settlement, Follow, B/backend policy, the new engine and atomic head-first/full-trait +migration remain open. This subset does not depend on completing D1c/P3/P5 or full G2 first, +but does not complete those tasks. B/domain/cardinality/resource approval, backend policy, +timers and slack are unchanged; no backend-policy implementation is enabled. Typed body-error +preservation and fresh-readiness/subsequent-read safety remain requirements. Earlier checkpoint +prerequisite lists, the EZ-8.10 draft summary and Decisions Before Runtime Changes are read with +this override. The historical execution/checkpoint records follow unchanged. + +### Send-First Implementation Checkpoint + +- [x] Attach/read back typed optimizer settings through request parts without changing targets + or signature-bearing headers. Demonstrate missing-API red and passing round trips. +- [x] Reject unsupported adapters/batch modes before ordinary validation, body polling or budget + rejection. Prove a rejected Axum image slot leaves its live plain TCP sibling eligible. +- [x] Map every portable selector directly to pinned SDK fields; use native synchronous Send only + for attached options and retain the ordinary async branch and existing backend policy. +- [x] Preserve selected deadlines through native return and shared response handoff, including + equality, late-error detail and release of late successful resources. Retain exact public + error text while exposing typed optimizer unavailability. +- [x] Retain shared body/header limits and lazy streaming; align the Fastly-only demo/scaffold + route and capability publication. Keep all historical failed qualification evidence unchanged. + +Local verification passes 2,233 workspace tests (one existing ignored build test), all five root +Rust gates, target lints, all 193 Fastly WASI unit cases and two focused optimizer contract cases, +384 native Fastly `cli,test-utils` tests, generated-project builds/tests, demo tests, fixture +replays and documentation guards. These are implementation checks, not hosted transformation, +deadline-tail or performance certification. The source-selection proof is static; the Viceroy +unavailable result alone cannot prove optimizer host entry. The full P1/D1d cut remains open. + +Two read-only investigations compared Send-first integration with another disconnected mapping +foundation. The selected path keeps one existing transport and preserves the pending atomic +trait replacement. An independent final review checked mapping, unsupported-option precedence, +deadlines, ownership, caps and generated routing; its message/provenance corrections were applied. +The extra native `cli,test-utils` lint leg exposed an existing no-attempt constructor conditional; +owning both inputs before selecting the source field removes that redundant conditional drop +without allocating, cloning or changing source ownership. That lint combination now passes too. + +### P2 Axum Host-Known-Empty Checkpoint + +This implements P2's existing known-empty conversion subtask as an engineering sequencing +clarification, not a new user approval or an alternative public contract. Outbound settlement +and the support-first/paired-time/head hooks remain with atomic P1. Preserving an existing +transport fact at ingress does not require those hooks or change the frozen Interfaces. + +- [x] After admission, preserve Axum `is_end_stream()` bodies as `Body::empty()` for any method, + without polling or inferring emptiness from headers or size hints. Keep Initial rather + than Cached state, `IngressFraming::HostManaged`, context metadata and supplied clients. +- [x] Keep unknown bodies on the existing lazy stream/read-deadline path; retain source bytes, + empty chunks, errors and ownership. Core first-`body_bytes` deadline precedence is unchanged: + an initial buffered-empty body can still produce 408 through that accessor after expiry. +- [x] Pass actual HTTP/1 TCP GET/HEAD forwarding checks for absent framing and explicit zero + length through `into_request` despite an expired inbound read deadline, with no outbound + body bytes, Content-Length or Transfer-Encoding. Chunked-empty and nonempty GET controls + still return 400 with no origin request and preserve unread-body connection boundaries. +- [x] Update the active guides and changelog without new capability rows or promotions. Demo + and generated proxy handlers benefit from the same Axum converter with unchanged APIs. +- [ ] Complete general streamed GET/HEAD settlement, support-first preparation, paired outbound + timers, eager/follow integration and the remaining adapter/runtime qualification with the + atomic migration. This checkpoint does not complete P2 or EZ-1. + +Fastly, Cloudflare and Spin conversion is unchanged. No core deadline precedence, public API, +backend policy, B/H(K), phase timer, slack, resource limit or SDK/runtime pin changes. The +checkpoint supplies no new approval or hosted/capability qualification; historical evidence and +the frozen Interfaces remain unchanged. + +Verification passes all five root Rust gates, 2,241 workspace tests (one existing ignored), +245 Axum unit tests plus 17 contracts, 57 demo tests, and the explicit generated-project native/ +three-WASM build and TCP fallback gate. Documentation formatting, lint, build, forwarding doctest +and its 15 checker tests, docs/legacy guards and approval-identity verification pass. Converter +and TCP regressions were red before the fix. Independent protocol and implementation reviews +selected this existing ingress fact over partial outbound hooks, and tightened the pipeline +guard to reject extra/wrong responses and the origin recorder to distinguish connection setup +from HTTP receipt. This remains local implementation evidence, not hosted qualification. + +### P2 Fastly SDK-Body-Absence Checkpoint + +This selects only preservation of actual SDK body absence as an engineering sequencing decision, +not new user approval, a new public contract or full P2 completion. The preceding Axum checkpoint +and its verification remain historical. Outbound settlement and support-first/paired-time/head +hooks remain with atomic P1; the frozen Interfaces, plan 02, requirements and approval record +are unchanged. + +- [x] In direct conversion and admitted dispatch, use `try_take_body()` to preserve `None` as + core `Body::empty()` without creating an SDK body. Acquire the admitted source only after + admission and transfer the final SDK body after the prepare hook. +- [x] Keep every `Some(body)` on the existing lazy stream/read-deadline path, even when host + known length is zero. Preserve prefetched read bytes and pending writes without header/ + hint inference, repacking, reads or flushes during conversion. Generic test-reader seams + remain present sources, not SDK-absence evidence. +- [x] Cover actual WASI SDK bodies with zero host length but payload remaining in prefetched + read bytes and pending writes. Host length is not EOF even beyond SDK buffering: Viceroy + uses Hyper exact size hints, not a terminal-body signal. +- [x] Preserve Initial rather than Cached context state and `IngressFraming::HostManaged`. + Core's first `body_bytes` read-deadline check remains unchanged and can still return 408. +- [ ] Fix ordinary Fastly bodiless forwarding and complete general settlement and the remaining + P2/EZ-1 acceptance through the separately gated atomic migration. + +G3's actual `from_client()` requests always supplied `Some(body)`, including absent framing and +CL0. This checkpoint therefore does not establish ordinary Fastly GET/HEAD forwarding success. +Demo and generated proxy handlers automatically share the converter with unchanged APIs; this +does not mean raw Fastly GET proxying works. No Native claim, new capability row or promotion, +B/backend-policy, slack, timer, resource-limit or SDK/runtime-pin change is enabled. Historical +failures and qualification gates remain; this is not hosted or full Fastly G2 qualification. + +Verification passes all five root Rust gates, 2,241 workspace tests (one existing ignored), +193 Fastly WASI unit tests, 35 WASI contracts including six new SDK-body cases, target clippy, +57 demo tests and the generated-project native/three-WASM/TCP gate. All four existing local +Viceroy progress groups pass, including 24 relay cases, as working-tree characterization only. +Docs formatting/lint/build, forwarding-doctest, docs/legacy and approval-identity guards pass. +The absence test was red before implementation. Independent source and runtime investigations +rejected zero-host-length inference; real SDK tests demonstrate both buffer counterexamples. +Independent review found no remaining scoped defect after replacing a wall-clock-sensitive +prepare test with the injected app clock. No historical evidence or protected API identity changed. + +### Shared Header-Policy Consolidation Checkpoint + +The requester approved consolidation around one EdgeZero implementation and a documented +hard-cut consumer migration, then explicitly required preservation of existing consumer +contracts. Two read-only solution reviews selected a permanent shared header operation rather +than a second transport, a public response-context constructor or premature dispatch replacement. +This checkpoint does not change the frozen Interfaces or claim a new exact-interface approval. + +- [x] Extract `OutboundHeaderInput::normalize` from `OutboundHead::new` and use it immediately + in all four adapter response paths. Preserve pre-filter field accounting, immutable + Content-Type/Location evidence, unknown provider fidelity, marker accounting and original + body disposition. Retain static marker bytes without allocating or copying raw headers. +- [x] Privatize the separate normalization/marker helpers and remove their root exports; + retain private defensive response normalization without a second upstream charge or marker. +- [x] Establish missing-operation compile red, then cover exact count/byte boundaries, filtered + duplicates, unknown fidelity, original positive-205 disposition and marker-off retention. + Add public usage and retired-helper compile-fail doctests. +- [x] Align the capability guide, outbound adoption path and changelog. Handler/demo/template + APIs remain unchanged; their atomic implementer migration remains with P1. +- [x] Complete independent spec/quality reviews, all five root Rust gates, three WASM lint + legs, Fastly and Spin contract execution, core doctests and documentation checks. +- [x] Complete the fresh generated-project native/three-WASM build, 24 generated core cases + and actual TCP fallback gate. An initial sandboxed run denied loopback; the unchanged + loopback-enabled rerun passes. +- [ ] Complete the genuine prepared-context, paired-time, eager scheduling, arrival-ledger and + Follow migration; remove superseded implementer APIs atomically, not through compatibility + adapters or lazy scheduling defaults. Existing positional/partial results, cutoff/cancellation, + timing, typed failures, all caps, encoding/provenance and ingress/egress ownership remain required. + +Payload decoding/collection, SDK resources and abort/EOF acknowledgement, deadline races, +native Image Optimizer, backend identity/timers/slack and all capability levels are unchanged. +Fastly readiness/error-preservation and B/backend-policy constraints still gate their affected +engine work; no numerical policy, provider guarantee or hosted qualification is inferred. +This is a header-only consolidation, not completion of P1/P2 or the full approved scope. + +The scoped workspace run passes 2,244 tests across 29 binaries (one existing ignored), including +866 core cases. Fastly passes 193 WASI unit cases and 35 contracts; Spin passes all 35 contracts +on pinned Wasmtime 48.0.2 after correcting a baseline-proven stale exact-header expectation: +explicit buffered framing adds Content-Length. The corrected fixture still asserts the exact +header set, body, status and lifecycle counts. All 57 demo tests and the approval-identity verifier +pass. The shared-header documentation guard includes nine positive/negative self-checks. + +Cloudflare target compilation/lint passes. Local browser execution is unverified: the installed +0.2.121 runner first mismatched the locked 0.2.128 schema; an isolated exact-version runner then +encountered OS-killed Safari/Firefox WebDrivers. No repository dependency or installed-tool pin +was changed, no browser result is called passing, and no hosted qualification is inferred. + +### Prior Execution Status + +Execution started with G1's inventory/checker and G2's opt-in host diagnostic. No production +runtime migration below is complete. The local G2 control failed: timed body readiness consumes a +source error on pinned Viceroy. See the [feasibility report](../../audits/2026-10-18-outbound-feasibility.md) +for retained evidence and cancelled/deferred Fastly work. G3's local probes are implemented: +native and Viceroy passed the exercised cases. Spin's initial response-owned failure exposed an +existing egress bug: early successful host observation aborted pending body work. That internal +bug is fixed without changing the outbound/public APIs; all local Spin cases and isolated controls +now pass. Cloudflare's subsequent local workerd run passes the exercised coding, marker, +scheduling and framing cases. A subsequent local relay run also passes on all four entrypoints. +Protocol/hosted/context legs and +Fastly-policy approvals remain pending. The requester's scoped reply authorizes the exact G4 API +digest, with approving maintainer name supplied as AG. P1's core paired-time and ownership foundations have started, not +the full client/adapter hard migration. Baseline is PR 275 at +`f6c708ccb5be19ba9fa92575db3c08c0f17e0d72`, called F in the +[requirements](../specs/2026-10-17-edgezero-pr275-requirements.md). +That requirements document supplies observable behavior, priorities, exceptions, acceptance +criteria, and evidence ownership. This series supplies implementation order and file ownership. +Historical outbound plans are implementation history, not instructions to recreate removed APIs. + +The [G4 review packet](../../audits/2026-10-18-outbound-api-approval.md) presents the exact API digest, +hard-migration inventory and independent decisions, including the scoped API-only conversation +approval attributed to AG. Dependent Fastly implementation still needs a +new preserving primitive/report and its separately approved policy. No failed gate was waived. + +### F2 Overflow Checkpoint + +The independent [F2 checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f2-implementation-checkpoint-2026-10-21) +implements typed 413 readers/uploads, zero-read declared-length rejection, retry-header +suppression and aligned demo/scaffold fallback defaults. The isolated local overflow suite now +passes eleven cases each on Axum, Viceroy and Spin, including the exact 2 KiB chunked request +against a 1 KiB cap. That exact case also passes on Cloudflare, which remains failed: after the ingress +413, the next valid POST receives a proxy-generated 500 before fixture entry, also reproduced by +a JavaScript-only control. Later topology diagnostics narrow the failure to Wrangler's front-proxy +path; directly contacting its user-worker listener is not an acceptance replacement or a repair. +Retained failures are not waived by the passing cases. +The generated-app/demo Axum fallback leg also passes real TCP boundary, timeout and deferred +unread-body tests. Hosted and end-to-end legs remain open. This does not waive G2 or change the +exact approved P1 API migration; local upload response heads are observed without polling Fastly +response bodies. + +### F8 Declared KV Checkpoint + +The requester adopted Fastly, Cloudflare and Spin together. The independent +[F8 checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f8-implementation-checkpoint-2026-10-21) +preserves declared KV handles without opening unused stores, provides payload-free typed +500/503 opening failures and retries initialization on later requests. Only warning membership +for compiled IDs survives requests. Explicit required-store and Axum startup policies remain. +Local unit/runtime evidence is separate from consumer integration and hosted restoration. +This follow-up does not unblock G2 or change the frozen P1 API migration. + +The [operation-delivery checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f8-operation-delivery-checkpoint-2026-10-23) +adds real local failed write/delete/list cases, post-delete state checks and post-shutdown +validation of all 39 request lifecycles. The original four-request same-instance warning proof +remains unchanged; Viceroy's six-request guest ceiling permits natural turnover in the added +operations. The final smoke run still exits 2 for the separately unverified Spin retained-mode overlap. +Consumer integration and actual provider outage/restoration remain open. + +### F6a Diagnostic Foundation Checkpoint + +The independent [F6a foundation](2026-10-18-trusted-server-outbound-04-followups.md#f6a-foundation-checkpoint-2026-10-21) +adds fixed-size, text-free provider detail to existing gateway, timeout and internal errors, +preserves public error behavior, and retains that detail through body policies, batches, pending +handles and sticky body replay. Exact field patterns migrate without compatibility wrappers. +The subsequent [SDK boundary checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f6a-sdk-boundary-checkpoint-2026-10-21) +populates the existing send/body boundaries on all four adapters and retains detail through ready +error/deadline arbitration. The [preparation checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f6a-preparation-checkpoint-2026-10-21) +adds request-only Cloudflare and Spin resource-setter diagnostics. The +[source-observer checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f6a-source-observer-checkpoint-2026-10-21) +retains the first observed body-source failure in completion/observer reports without changing +terminal precedence. Capability claims and hosted acceptance remain open. +This does not change transport behavior, unblock Fastly G2 or complete P1/F6a. + +### F6c Framing Checkpoint + +The independent [typed framing checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f6c-framing-checkpoint-2026-10-21) +retains the existing framing rules in separate application/fallback report slots and fixed-text +error-level logs. Shared attempt-owned normalization covers all four adapters without changing +wire bytes or postcommit source categories. This is not consumer or hosted qualification and +does not unblock Fastly G2 or the remaining outbound cutover. + +### F6a Capability Publication Checkpoint + +The independent [capability publication checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f6a-capability-publication-checkpoint-2026-10-22) +adds `outbound-failure-diagnostics` to manifest parsing, existing gates and four adapter tables. +Fastly/Spin are Native for available typed SDK-cause projection; Axum/Cloudflare are BestEffort +because provider visibility is incomplete. Published matrices are checked against the registered +adapters. This does not recover host-hidden causes or close full EZ-16/P1, consumer/hosted +qualification or failed Fastly G2. All transport, deadline and delivery guarantees remain unchanged. + +### F6b Delivery Report Checkpoint + +The [Fastly delivery-report checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f6b-delivery-report-checkpoint-2026-10-22) +hard-migrates custom-send results while preserving standard serving policy. It retains finalizer +state, exact terminal reports, last-body commitment and safe pre-attempt diagnostics. Native +replacement is permitted only before any attempt or after confirmed non-commitment. Local +Viceroy characterization is separate from hosted qualification. F9a assembly-failure integration +is now implemented below; consumer acceptance, full EZ-17 and failed Fastly G2 remain open. + +### F9a Assembly Foundation Checkpoint + +The independent [F9a assembly foundation](2026-10-18-trusted-server-outbound-04-followups.md#f9a-assembly-foundation-checkpoint-2026-10-22) +hard-cuts route assembly to Result, shares configured manual-router assembly, and exposes state +from the existing router registry. That foundation did not change outbound transport or entrypoint +failure responses. The subsequent [unified failure-delivery checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f9a-unified-failure-delivery-checkpoint-2026-10-22) +implements the explicitly adopted macro/serverless SHOULDs, canonical assembly-error delivery +and Fastly retained retry. All three local serverless runtimes pass focused assembly cases; +Axum retains pre-bind startup failure with a once-only boot diagnostic. Consumer acceptance, +durable hosted qualification and full EZ-22 closure remain separate from those local results. + +### F9b Assembled-App TCP Checkpoint + +The independent [F9b assembled-app TCP checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f9b-assembled-app-tcp-checkpoint-2026-10-22) +hard-cuts programmatic Axum servers to consume configured apps and adds an owned production-path +TCP test helper. Supplied clients avoid default transport initialization. Upstream local policy, +streaming and cleanup controls pass the root gates, all seven feature matrices, target lint, +demo/generated checks and local TCP smoke. Independent spec/quality reviews passed. Consumer +parity, the conditional begun-request leg and exact-candidate qualification remain open. + +### F7a Buffered Framing Checkpoint + +The independent [F7a checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f7a-implementation-checkpoint-2026-10-23) +derives eligible buffered lengths and hands transmitted Fastly buffers to the host before any +stream writer can truncate them. Core preparation requires an explicit framing mode; demos and +scaffolds migrate directly. Strict body suppression takes precedence over the pinned local SDK's +failing bodyless reference controls. All 80 local production cases pass, while nine SDK parity +controls remain failed and keep the full framing suite nonzero. Hosted compression, frozen +candidate qualification and consumer comparisons remain open. No serverless capability is +promoted, Fastly G2 remains failed, and neither full F7a nor P1 is complete. + +### F7b Synthesized Header Checkpoint + +The [F7b checkpoint](2026-10-18-trusted-server-outbound-04-followups.md#f7b-implementation-checkpoint-2026-10-23) +adds bounded prevalidated framework-error/fallback header sets and Fastly per-request replacement, +without decorating matched routed responses/errors or application refusals. Envelope/attempt +snapshots retain the existing ownership and fallback budget; demos and scaffolds use the same core +API. Required shared/target checks and independent review pass, including the generated app's +executed tests and TCP probe. The separate local HTTP/1 suite passes 55 cases across all four +runtimes, including complete maximum-size sets, GET/HEAD fallbacks and Fastly's normalized +rejection/request-replacement paths. A further 40 serverless HTTP exchanges and one native startup +control prove partial-configuration exclusion and recovered-policy delivery. Consumer integration +and hosted/frozen-candidate acceptance remain gates. This does not complete P1 or waive failed Fastly G2. + +### P1 Foundation Checkpoint + +- [x] Verify the exact submitted Interfaces digest before implementation and record the scoped + API-only approval with the supplied maintainer attribution, AG. +- [x] Add `OutboundFuture` and `OutboundTime` with the existing clock and an owned local wakeup + factory. Preserve the absolute deadline, retain pending registrations, yield before early + rearming, preserve original timer errors and release terminal/cancelled state exactly once. +- [x] Demonstrate missing-type/export red and the late timer-error regression, then eleven + deterministic passing time tests, including + fake-clock wakeups, frozen time, original error identity, resource release and clock regression. +- [ ] Integrate paired time with support-first preparation and timer-before-body settlement. +- [ ] Hard-migrate the shared dispatch trait, every current implementation, adapter injectors, + behavioral tests, demo, generator and guards together. No old-signature defaults or aliases. + +The initial foundation did not change transport behavior or qualify a runtime. The approved +paired-time integration below now replaces clock-only constructors, while the dispatch-trait +hard cut remains open. G1 applicability, remaining +G3 premises, Fastly B/H(K), and hosted results retain their own open gates. + +### P1 Paired-Time Integration Checkpoint + +The requester explicitly approved moving this integration ahead of the atomic dispatch cut on +2026-10-26; see the scoped approval audit. The frozen Interfaces and Plan 02 are unchanged. + +- [x] Hard-migrate all four live clients, request injectors and affected fixtures to `with_time` + / `try_with_time`, pairing standard entrypoints with the exact application clock. Remove + the clock-only constructors without aliases; leave the public dispatch trait unchanged. +- [x] Use the supplied timer for upload/head/body deadline races, check it before source I/O, + retain pending registrations, preserve typed failures and recheck original deadlines after + ready I/O. Native reqwest total timeouts no longer introduce a second clock domain. +- [x] Register Fastly timers in the existing progress multiplexer, including timer-only waits, + without per-slot sleep/select. Check Workers scheduling and cancellation before fetch; + retain a real host-event yield for frozen-clock fairness. +- [x] Complete cross-target regression checks, deterministic wake/cancellation proofs, strict + lint and independent ownership review; record the result in the approval audit. + +This is live timer integration, not completion of P1. Support-first settlement/context issuance, +the atomic trait and batch-receipt migration, external-client conformance, redirect following and +hosted qualification retain their separate gates. Demo and generator use standard injection, +not removed custom constructors; no alternate client API is added for them. + +### P1 Head Foundation Checkpoint + +- [x] Reverify the frozen Interfaces and policies-file digests; retain AG's scoped API approval. +- [x] Add the approved head/header/fidelity/Location-evidence types, response-context readbacks + and send-mode enum, with no public context constructor, Clone, Default or mutable fields. +- [x] Charge original headers and the effective marker before exposing a head. Capture original + Location facts before filtering, retaining provider-unknown fidelity and Content-Type + uncertainty without a second raw header/value copy. +- [x] Reuse existing normalization unchanged. Body-independent framing validation, including + malformed/conflicting lengths, still runs at construction; payload-length cap checks, + decoding and collection wait for final disposition. Retain the original 205 declared-body flag. +- [x] Implement zero-read discard and finalization with the captured clock/budget, one teardown + owner and live streamed handoff. Release terminal/cancelled reads and timer registrations. + Yield between encoded reads, before decoding, so ready empty/no-output input cannot prevent + the outer timer from waking. Strictly earlier original chain deadlines remain binding; + equal deadlines retain the already-selected budget cause. +- [x] Establish missing-type/export red, empty-input starvation red, and both independent review + regressions red/green. Add 22 deterministic head tests, including pending fake-clock wakeup, + timer setup before source polling, typed error identity, cancellation, bodyless/205, + header/body caps, original/partial coding and retained Location facts. Add privacy doctests. +- [x] Complete broad verification and independent final review for this checkpoint. +- [ ] Integrate the validated facade producer, progress-scope token and all client/adapter + implementations in the atomic hard cut. Complete external supplied-client conformance, + eager progress, settlement, batch-ledger migration and redirect following separately. + +This is a core-internal execution foundation, not live head-first dispatch. Unit tests construct +private contexts; external implementations cannot manufacture them. The existing facade/trait +has not changed. Adapter constructors migrate in the separately approved paired-time checkpoint; +demo, generator and fixtures keep their current valid +implementations until the atomic migration. No compatibility alias, silent option default or second +transport was added. The current inventory is eleven implementation blocks, including the fully +qualified qualification-fixture client; rediscover all callers before that cut. + +Two read-only investigations compared this buildable foundation against an atomic full cut and +selected the former. API approval does not waive Fastly G2, B/H(K), the remaining G3 premises or +hosted/frozen-candidate qualification. Existing evidence records and the frozen Interfaces are unchanged. + +All five workspace Rust gates passed on the final source snapshot, including 853 core unit tests; +core doctests and direct core builds for wasm32-unknown-unknown and wasm32-wasip1 passed too. +Docs format/lint/build, outbound documentation/legacy guards and 29 evidence-checker tests passed. +The inventory remains 255 criteria / 511 planned legs with `qualified: false`. Independent spec +review found and rechecked the encoded-read starvation and equal-deadline attribution fixes; +final quality review found no actionable defect in this subset. Broader terminal-race, delayed +timer-error/clock-regression and streamed pending-cancellation matrices remain integration coverage, +not provider guarantees inferred from these foundation tests. + +### P1 Image Optimizer Values Checkpoint + +Historical checkpoint at `08bfb89d`; the region-enum amendment below supersedes only the region +representation and its privacy doctest. + +- [x] Reverify the frozen Interfaces digest and the seven region tokens against the installed + Fastly SDK 0.13.1 source, without adding that SDK to core. +- [x] Add the approved selector enums, private region/options types and exact consuming + builders/readbacks. Leave every selector unset and query preservation false by default. +- [x] Validate quality 0..=100, nonzero dimensions/ratio components and both percentage coordinates + before values can be retained. The focus enum makes partial/conflicting coordinates + unrepresentable; public Percent payloads are still checked by the crop builder. +- [x] Establish missing-type/export failures in both colocated and external tests, followed by + eight passing unit tests and `public_image_optimizer_value_readbacks_compile_contract`. + Include privacy/invalid-focus compile-fail doctests, fixed invalid-input diagnostics, + selector/default/clone equality, full quality-domain boundaries and replacement semantics. +- [x] Complete broad verification and independent spec/quality review for this checkpoint. +- [ ] Attach values to requests/parts and prepared redirects with the required synchronous support + hook in the atomic migration. Implement permitted Fastly Send/Follow mapping only after + its separate prerequisites and numerical host-call horizon are approved. + +Both read-only investigations selected standalone validation over premature request attachment. +Applications can construct, validate, clone, compare and inspect these values, not send optimized +requests yet. The facade, request/parts, adapters, demo and generator remain on their current +working contracts. No compatibility alias, ignored option, SDK dependency, provider pin or capability +claim was added. This advances the portable construction portion of P1/EZ-8.3, not all of P1 or EZ-8. +Fastly G2, B/H(K), the live hard cut and hosted/frozen-candidate qualification remain open. + +All five workspace Rust gates passed on the unchanged final source snapshot: 2,224 tests passed +and one existing generated-project test remained ignored. Core doctests (including the four new +compile-fail examples), core no-default-features builds for wasm32-unknown-unknown/wasm32-wasip1, +and the Spin wasm32-wasip2 gate passed. Docs format/lint/build, outbound documentation/legacy +guards and 29 evidence-checker tests passed. Independent spec and quality reviews found no issue +in this subset. The inventory remains 255 criteria / 511 planned legs, `qualified: false`. + +### P1 Region Enum Amendment + +- [x] Apply the requester's 2026-10-24 scoped approval: replace the string-backed region with + the public Copy/Clone/Debug/PartialEq/Eq enum Asia/Australia/EuCentral/EuWest/UsCentral/ + UsEast/UsWest. Keep canonical `parse`/`as_str` conversions without a compatibility wrapper. +- [x] Update the current Interfaces and retain the original exact-digest approval as history + in the [approval record](../../audits/2026-10-18-outbound-api-approval.md#scoped-region-enum-amendment). +- [x] Demonstrate missing-variant red in the external API test; cover exhaustive external + matching, direct construction, Copy readback and exact token/variant pairs. Replace the + obsolete privacy doctest with a typed construction example; retain invalid-input tests. +- [x] Complete broad verification and independent review for the amendment. + +Only the standalone region value changes. Request attachment, adapters, demo and generator do +not yet consume these optimizer settings; their existing outbound contracts stay unchanged. +Fastly feasibility/policy/horizon and hosted qualification remain open. + +All five workspace Rust gates passed: 2,225 tests passed across 29 binaries, zero failed and one +existing test remained ignored. The 861 core unit tests, both external API tests and 11 core +doctests passed; core no-default-features checks for wasm32-unknown-unknown/wasm32-wasip1 and +the Spin wasm32-wasip2 gate passed. Docs format/lint/build, outbound documentation/legacy guards, +29 evidence-checker tests and the historical/current API identity verifier passed. Independent +code and documentation reviews found no actionable issue. The inventory remains 255 criteria / +511 planned legs, `qualified: false`; these checks do not qualify the full outbound migration. + +### Native Image Optimizer Parity Preparation + +- [x] Inspect the native TS path and compare the optimizer DTO/profile/adapter sources at + `f9281917a7260b723f5f22175911c6d2147ef650` with baseline `a9037770a`; these files match. + Confirm direct native dispatch and lazy response handling, with application-owned profiles, + SVG exclusion, query stripping and conditional S3 signing/HEAD/raw-error sends. +- [x] Record the requester's 2026-10-24 scope approval separately from numerical Fastly-policy, + measurement and deployment decisions. Keep the current Interfaces unchanged. +- [x] Strengthen EZ-8.12 and its existing hosted criterion with native-path performance parity; + synchronize that one inventory leg and the source digest without changing applicability. +- [x] Expand D1d with exact mapping/request-round-trip/laziness/identity sentinels and document + the paired TS/SDK-only hosted comparison in Q4b and the existing runbook template. +- [x] Complete independent review and preparation checks. +- [ ] Resume runtime integration only after its existing D1c/P3/P5, G2 and numerical B/H(K) + prerequisites. No request attachment, SDK mapping, optimizer dispatch or new examples are + implemented by this preparation checkpoint, and no compatibility path is added. +- [ ] Obtain measurement definitions/acceptance rules, explicit staging authorization and an + entitled service; implement and execute the paired comparison. No performance result is + claimed from local validation or Viceroy's unsupported response. + +Two bounded investigations selected direct native mapping through the existing pipeline over +extra serialization or a separate image client. The approved scope retains every existing +outbound deadline, limit, error, ownership and batch contract. The finite Image Optimizer host-call +horizon remains unproved/unapproved; an instruction to continue is not its numerical approval. +Demo and generator remain on their working request API until the atomic migration; do not add +examples that compile yet cannot optimize. The inventory remains planned, not qualified. + +All five root Rust gates passed on the unchanged runtime: 2,225 tests passed across 29 binaries, +zero failed and one existing test remained ignored. Docs format/lint/build, outbound documentation +and legacy guards, 29 evidence-checker tests and manifest/API identity checks passed. Independent +plan/spec and mechanical evidence reviews found no actionable issue. The inventory still contains +255 criteria / 511 planned legs and `qualified: false`. No hosted request or deployment was made. + +### Local-Only Recheck And Comparison Guard + +The requester instructed continuation without deployment on 2026-10-24. No service, +entitlement or deployment authorization is inferred from that instruction. + +- [x] Recheck the stock/repaired executable hashes and run the existing offline/loopback G2 + suite on identical guest bytes from clean `98a1d0e8`; do not replace either runtime or pin. +- [x] Retain both failed runs and 414 canonical control/probe records separately. Repaired + preserves source errors but completes one probe at 505.158 ms; its terminal release window + crosses the unchanged 505 ms bound after a within-bound ABI return. Stock still loses 24 + selected source errors. Do not backdate, omit cleanup or infer its precise latency cause. +- [x] Add a co-located fixture replay guard for those complete inventories, retained late timer + controls, distinct failure counts and the repaired cleanup/completion boundary. +- [x] Reproduce the evidence checker's acceptance of empty comparison measurements and the + missing hosted optimizer comparison flag, then reject both with the existing schema. Keep + this as a presence check, not a substitute for approved metrics, controls or review. +- [x] Complete independent review and all preparation checks before committing. + +See the [local recheck record](../../audits/evidence/2026-10-24-fastly-local-recheck.json). +No production client, optimizer mapping, deadline, batch behavior, capability or current +Interfaces change. G2 remains Failed and B/H(K)/hosted gates stay open. This is local diagnostic +and evidence-tool progress, not implementation or performance qualification of Image Optimizer. + +All five root Rust gates passed: 2,225 tests passed across 29 binaries, zero failed and one +existing test remained ignored. All 119 fixture-harness tests, fixture fmt/clippy, 31 evidence-tool +tests, docs format/lint/build, script formatting and outbound docs/legacy guards passed. The first +sandboxed fixture run denied existing TCP binds; the unchanged loopback-enabled rerun passed. +Independent reviews verified the evidence identities/replay and comparison-presence fix without +finding an actionable issue. Documentation-only formatting options also fail on the baseline +script files; their existing local formatting passes and is retained. All 511 inventory legs +remain planned and unqualified. The live local G2 diagnostic exits 1 on both runtimes, unchanged +by these passing development checks; no deployment was made. + +### P1/P4 Ownership Checkpoint + +- [x] Add the approved local `OutboundExchangeOwner` and `OutboundPending` types with private + ownership state and root exports. Retain the exact submitted Interfaces digest. +- [x] Use one receiver-observation path for readiness and collection; cache the original outcome, + release a completed receiver immediately and release failed work on observation. Do not poll + a response body, resample deadlines or reconstruct typed errors during handoff. +- [x] Cancel on handle/collection-future drop, including an unpolled future; disarm pending + cancellation only before returning success. Keep streamed-body cleanup separately owned. +- [x] Demonstrate missing-type/export red, then nine deterministic passing ownership tests, + including live-stream handoff, repeated readiness, exact non-Clone error identity, cached + response abandonment, ordered cancellation before receiver/body release and real-waker + registration after a nonblocking check. Debug inspection neither polls nor exposes cached payloads. +- [x] Finish broad verification and independent spec/quality review for this checkpoint. +- [ ] Integrate these primitives with head-first dispatch, verified eager scheduling, scope owners + and the arrival ledger during the full hard cut. No lazy compatibility begin is supplied. + +Both investigation/proposal reviews selected the same minimal design: retain cancellation on cached +success, release it on cached error, and privately disarm it on successful collection. Dropping a +disarmed callback still destroys its captures: pending producers must use passively droppable +cancellation handles, not armed abort-on-drop SDK guards. Buffered publication follows transport +cleanup; streamed publication follows transfer to a distinct body owner. These are producer +obligations, not guarantees inferred by polling a supplied future. No adapter, runtime scheduling +semantics or capability is changed by this standalone foundation. At that checkpoint Fastly G2 +remained failed and Cloudflare tooling was unavailable; portable tests waived neither. The later +Cloudflare checkpoint below supersedes only that local tool gap. + +Checkpoint verification passed: all five workspace Rust gates (782 core tests), core doctests, +all seven eight-combination adapter matrices rerun after the final test additions, three WASM +target clippy runs, demo tests and the generated-workspace gate with 22 executed core tests. +Docs checks, dependency audits, the legacy guard and the 18 evidence-checker tests passed; the +inventory still reports 255 criteria / 511 planned legs with `qualified: false`. Independent spec +and quality reviews found no correctness defect in this subset; the suggested ordered-cleanup +regression was added and reviewed. No new trait implementation or compatibility path was introduced. + +The nine ownership tests also executed successfully under pinned Viceroy on wasm32-wasip1: + +```sh +SSL_CERT_FILE=/etc/ssl/cert.pem CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh pending_cancellation_precedes_receiver_and_cached_body_release cargo test --offline --locked -p edgezero-core --target wasm32-wasip1 --lib outbound::progress::tests +``` + +The initial local runner exited before test discovery because its macOS keychain was unavailable. +Using the existing system CA bundle through the documented native-certificate override fixed +runner startup without changing code, disabling verification or accessing the keychain. These are +portable ownership tests, not Fastly SDK scheduling/readiness or hosted qualification evidence. +The previously retained runtime characterization records remain historical source snapshots. + +### P1/P3 Original-Coding Checkpoint + +- [x] Add the approved Decode/Original request choice, default/readback/parts preservation and + enforced propagation through all four current adapter transports, including Fastly batch metadata. +- [x] Centralize original and mandatory 206 delivery in the existing shared body policy; preserve + normalization precedence, identity decoded caps, transport/final caps and decoder bypass. +- [x] Demonstrate failing decoder/header/buffer-cap regressions, then passing core and actual Axum + transport tests, including mixed batch choices and zero-poll bodyless controls. +- [x] Exercise gzip/deflate/Brotli through pinned local Viceroy and Spin origins: 24 checks per target, + both response modes, Original/200, Original/206, Decode/206 and Decode/200 controls. +- [x] Align relay demo/template requests with Original while keeping JSON fanout on Decode. +- [x] Exercise actual encoded downstream relay through all four local entrypoints, including + workerd manual egress, both response modes and Decode controls; retain wire bytes and metadata. +- [ ] Complete the head-first hard cut, full adapter normalization/limit matrices, Fastly mixed-batch + host proof and hosted qualification before closing P1/P3. + +This original-coding slice preceded the marker checkpoint below and does not implement eager dispatch or redirect following. It +does not adopt EZ-5.9/5.10's optional metadata changes or bypass the failed Fastly readiness gate. +The approved Interfaces text and its digest remain unchanged. + +Checkpoint verification passed: all five workspace Rust gates (768 core tests), all seven +eight-combination adapter matrices, three real-target WASM clippy runs, demo format/clippy/tests, +and the generated-workspace gate including 22 executed generated-core tests. Docs checks, dependency +audits, both nonzero original-coding sentinels and the fixture's tests/clippy passed. Two independent +read-only reviews found no blocking defect; their additional declared/observed cap controls were +added and verified. Local coding results are retained in the feasibility report, not claimed as +release evidence. The forwarding-example checker was delivered in the separate Q1/Q2 checkpoint below. + +### Q1/Q2 Forwarding Documentation Checkpoint + +The mandatory EZ-1.14 checker extracts the actual forwarding fence from `docs/guide/proxying.md`, +builds core offline/locked and discovers its rlib, dependency directory and edition through Cargo +JSON. It requires exactly one listed and passed non-ignored rustdoc test. A checker-owned empty +entrypoint prevents handler invocation; a delimiter longer than every source backtick run keeps +embedded fences from bypassing compilation or the empty entrypoint. Fifteen passing Node cases +cover missing/duplicate fences, weakened annotations, zero tests and actual compilation failures, +including removal of `into_request()?` and both independently reproduced embedded-fence failures. +The existing workspace CI leg runs both Node commands after locked fetches with pinned Node. +This is supplementary compile-only coverage outside the reviewed 511 runtime/environment legs: +it does not adopt optional EZ-25.42, prove GET/HEAD/POST forwarding or close P2 or failed Fastly G2. + +Local checkpoint verification passes all five Rust gates (2,193 workspace tests, zero failures, +one existing ignored test), the 15 checker cases, 18 inventory tests and docs format/lint/build. +Independent review reproduced the embedded-fence bug before its fix and cleared the final checker. +Inventory validation still reports 255 criteria and 511 planned, unqualified environment legs. +These results are local source verification, not hosted CI or frozen-candidate qualification. + +### G2 Timer-Control Checkpoint + +The [timer-control investigation](../../audits/evidence/2026-10-23-fastly-timer-controls.json) +adds raw ABI timestamps and 72 controls per stock/repaired comparison without changing production +code or runtime pins. Native sleep exceeded +5 ms in the initial pair. After excluding formatting +from timer-only measurements, one stock 100 ms WASI sleep returned at 106.952 ms without body +handles. Repaired HTTP matrices passed locally, while stock still lost 24 source errors. +Earlier repaired timing failures remain retained/replayed; these samples do not identify their +cause or establish a bound. G2 stays Failed, with dependent Fastly work gated and approved +portable work independently eligible. B/H(K) and hosted qualification remain separate decisions. + +### G2 Expanded Diagnostic Checkpoint + +The opt-in SDK/ABI fixture now executes eleven controlled origins with buffered and pull-streamed +consumption, capacities 1/16384 and three repetitions, plus finite direct controls: 135 probes and +147 origin requests. Every selected read needs fresh matching readiness and the original absolute +deadline. Origins own bounded workers/sockets and join them on every exit; partial traces survive +ABI failures. Strict mutation tests reject missing inventories, malformed outcomes and error loss. + +The first complete repaired run returned one pending-body probe at 507.794 ms and failed the +unchanged 505 ms boundary. Stock also loses source errors. The separate +[matrix record](../../audits/evidence/2026-10-23-fastly-readiness-matrix.json) retains negative +observations. No capability, SDK/runtime pin, production API, inventory leg or G4 approval changes. +Full G2, B/H(K), the atomic production migration and hosted qualification remain open. + +### G2 Local Runtime Repair Checkpoint + +The [tracked runtime patch](../../../patches/viceroy/README.md) repairs the measured Viceroy +body-readiness error loss and empty-frame hazard without a blocking-read fallback or SDK/runtime +version change. Two independent investigations selected owned queue-based error retention over an +adapter workaround that cannot recover a consumed host error. Ten co-located regressions went red +(nine failures) and then green; all 135 upstream library tests pass. Stock and repaired guest +controls use identical Wasm: three stock comparisons fail, three repaired comparisons preserve +UnexpectedEof. The separate [record](../../audits/evidence/2026-10-23-viceroy-readiness-repair.json) +binds upstream/patch/host/guest identities and retains both event streams. Strict upstream clippy +has identical existing diagnostics on stock/repaired source; no suppression is introduced. +The repaired probe exits 2: full G2 ABI coverage/review, B/H(K), hosted execution, candidate freeze +and production qualification remain open. Historical failure records and all 511 legs are unchanged. + +### Q2 Runbook Source Preparation Checkpoint + +The [runbook source template](../../../tests/fixtures/reusable-app/ts-outbound-qualification-runbook.md) +records implemented preparation/lifecycle-egress commands separately from unimplemented full +outbound probe/export bindings. It includes G1/G4 references, external candidate/build placeholders, +count domains and separate owner acceptance, staging authorization, entitlements and approval fields. +No new checker or second qualification system is introduced. G1's stale G4 reason now acknowledges +AG's API-only approval; all 511 legs and pending policy decisions remain unchanged. Exact runnable +bindings and the Q2/Q4a acceptance gates stay open. No candidate freeze, runtime pin, transport, +capability or failed Fastly G2 disposition changes. + +Local checks pass all five Rust gates (2,193 tests, 28 binaries, one existing ignored), 34 evidence/ +egress Node cases, 15 forwarding-checker cases, the actual one-test doctest and exact Rust assembly +sentinel. Docs format/lint/build, documentation/legacy guards and all six runbook links pass. +The complete TCP-enabled test rerun replaces the sandbox-denied attempt. These checks do not +establish hosted execution or frozen-candidate acceptance. + +Independent command-map and approval reviews favored a static unaccepted template over a second +checker. Spec review corrected product-threshold scope; operator-safety review caught a fail-open +Git status check. Separate assignments now preserve Git failures and explicitly include untracked +files. Negative shell probes cover both errors, wrong SHA, tracked/untracked changes and a clean +control. Final scoped reviews are clear; they do not close full Q2 or Q4 acceptance. + +### Q3 Evidence Preparation Checkpoint + +The [pre-freeze assembly checkpoint](2026-10-18-trusted-server-outbound-05-qualification.md#q3-pre-freeze-assembly-checkpoint-2026-10-23) +adds one Node-owned draft assembler and shared release/report validation. A Rust fixture test +invokes the actual CLI and checks preserved case/sample counts, candidate/build identities, +raw SHA-256 hashes, non-overwrite behavior and release refusal. Draft assembly is always +unqualified, not an automatic conversion of working-tree smoke evidence. Reports retain +independent blockers without accepting ambiguous rows or counting invalid artifacts as passed. +All 511 reviewed legs and applicability decisions remain unchanged. Frozen export, controlled +origins, approvals, source freeze and local/hosted/consumer qualification remain open; this does +not change production APIs, complete P1 or waive failed Fastly G2. + +Independent architecture/coverage proposals selected Node-owned assembly with an actual Rust CLI +test instead of a second constructor/validator. Spec and quality review reproduced and closed +acceptance-count and catalog-isolation errors; final reviews found no remaining scoped issue. +Local verification passes all five Rust gates (2,193 tests, 28 binaries, one existing ignored), +85 harness and 20 qualification-core tests, the exact nonzero CI sentinel, 29 evidence-tool tests, +15 forwarding-checker tests and the one actual doctest. Fixture strict clippy/format, docs +format/lint/build and documentation/legacy guards pass. Complete TCP-enabled reruns replace the +initial sandbox-denied attempts. Inventory remains 255 criteria / 511 planned legs, unqualified. + +### P1/P3 Marker Checkpoint + +- [x] Add the approved client default, effective inspector and per-request Inherit/On/Off APIs; + preserve readback/parts settings and resolve policy before supplied-client send/batch delegation. +- [x] Wire the shared insertion/accounting policy through all four adapters and Fastly pending slots; + retain default-on behavior and ordinary pre-normalization upstream accounting. +- [x] Test exact byte/count budgets, duplicate upstream values, nomination precedence, overwrite + behavior, clone isolation and a non-polling inspector, plus real Axum disposition/batch cases. +- [x] Configure only demo/template relay handles off; keep JSON fanout default on and test both. +- [x] Exercise 46 marker checks each on native Axum, pinned Viceroy and Spin, including mixed-policy + batches; retain working-tree source/artifact identities and the initial Spin failure. +- [x] Isolate Spin's host-hidden Connection nomination with direct SDK controls and demote its + header fidelity to BestEffort without claiming raw-field normalization. +- [ ] Complete hosted marker evidence, plus the larger head-first/eager/follow + migration before closing P1/P3. Future send modes must use the same policy preparation. + +The [marker record](../../audits/evidence/2026-10-20-outbound-marker.json) characterizes visible-field +policy, not a frozen candidate or raw-header fidelity. Fifteen Spin cases explicitly record hidden +nomination rather than count it as successful raw normalization. The same three runs also passed +24 coding controls each. At that checkpoint Cloudflare lacked the exact locked wasm-bindgen CLI; +no download or repin had been attempted. The subsequent Cloudflare checkpoint records its separate +authorized tool install. Fastly's failed readiness gate and existing dispatch-slack guard remain unchanged. + +Checkpoint verification passed: all five workspace Rust gates (773 core tests), all seven +eight-combination adapter matrices, three real-target WASM clippy runs, demo tests/clippy and the +generated-workspace gate including 22 generated-core tests. Independent adapter and core/template +reviews found no remaining marker-policy defect; the template formatting finding and stale Spin +capability prose were corrected and rechecked. The new test-only MarkerClient adds one implementer +to rediscover during the full P1 hard cut; the frozen eight-implementation approval inventory remains +submission-time history and its Interfaces digest is unchanged. + +### Cloudflare Runtime Checkpoint + +- [x] Provision the exact locked wasm-bindgen CLI in an authorized temporary directory without + repinning dependencies or changing global tools. Run only local workerd with local bindings. +- [x] Execute 24 coding, 46 marker, five SDK/public scheduling and five HTTP/1 framing checks; + assert 89 origin receipts and retain the 18 individual scheduling receipts. +- [x] Strengthen framing validation against missing, mistyped or contradictory pre-conversion + Workers signals. Prove all eight independent-group failure combinations retain overall + failure and still execute scheduling/framing; keep Spin terminal diagnostics intact. +- [x] Retain separate initial and strengthened source snapshots, raw events, JS/WASM/runtime/tool + hashes and effective compatibility settings in the + [new record](../../audits/evidence/2026-10-20-cloudflare-outbound.json). +- [x] Complete the independent local downstream encoded relay proof in the checkpoint below. +- [ ] Complete full head-first/eager/follow integration and + hosted/protocol/context/cancellation qualification before closing their parent tasks. + +This dated checkpoint supersedes earlier Cloudflare tool-unavailable status only for the local +cases exercised. No production source, dependency pin, capability assignment, Fastly readiness +gate or approved Interfaces digest changes. Historical evidence is not rewritten as passing. + +Checkpoint verification passed: all five workspace Rust gates, the three real-WASM-target clippy +runs, 37 harness tests, 12 fixture-core tests, fixture format/clippy and docs format/lint/build. +The 18 evidence-checker tests, inventory validation, docs/legacy guards and decoder/map dependency +audits passed. Source, embedded-content, artifact and tool hashes were recomputed successfully. +Two read-only investigations selected private framing/phase helpers over a new runner framework: +the runtime review found no need for URL/header workarounds, while the evidence review identified +the missing host-signal assertion and independent-phase coverage gap. The subsequent scoped spec +review found no defect in the strengthened harness or final local observations. Independent quality +review verified 27 file hashes, 16 embedded payload hashes, 23 final source hashes and the unchanged +approval/requirements digests; it found no remaining defect in this checkpoint. It did not rerun +the broader CI commands, whose results above are from the central verification run. + +### P3 Downstream Relay Checkpoint + +- [x] Reuse the validated coding fixture and current transport; return `into_response()` before + collection. Keep coding JSON reports unchanged and disable markers only on the relay clone. +- [x] Prove zero source polls before streamed handoff, exact buffered/streamed bytes, request + policy parity and isolated target rejection with a private recording client. +- [x] Capture binary TCP payloads without UTF-8 conversion or decoding, enforcing a 256-byte cap + before append and preserving truncation as failure. Run negative wire assertions for changed + bytes, missing metadata, stale coding, invalid lengths and conflicting framing. +- [x] Run 24 actual downstream relay cases on each local runtime, keeping the existing 80 base + cases. Retain 24 ordered relay receipts per target and all four group results. +- [x] Retain the [relay evidence](../../audits/evidence/2026-10-20-outbound-relay.json), including + all 96 relay observations, selected source/artifact/runtime hashes and compatibility settings. +- [ ] Finish head-first/eager/follow integration, full normalization/limit matrices and exact-commit + hosted qualification before closing P3 or its parent phase. + +The final all-runtime command exited zero: 104 base checks and 113 asserted base origin receipts per +adapter. Spin additionally retains its two SDK header and four response-stream controls. This is a +working-tree characterization at base `0250edc9`, not qualification of that commit. No production +adapter or API changed, no compatibility path was added, and Fastly G2 remains failed. Local relay +does not prove forwarding actual inbound GET/HEAD bodies or the new eager APIs. + +Both read-only investigations kept P2's outbound settlement and support-first/time/head hooks +with atomic P1: current clock-only constructors and lazy implementer sends cannot satisfy that +boundary by partial hooks. The independent P3 proof was selected instead of adding unused +foundations or weakening that cut. The later Axum host-known-empty checkpoint independently +preserves an existing transport fact during ingress conversion; it does not implement settlement, +add partial hooks or change a public API. +Future P1 rediscovery must include the new fully qualified test implementation +`impl edgezero_core::OutboundHttpClient for RelayClient`; the frozen approval inventory remains +submission-time history. Scoped spec and reader/validator reviews found no blocking defect. The +remaining nonblocking diagnostic gap is explicit: reader failures are fatal and group-labelled +but can precede per-case relay records. + +Checkpoint verification passed: all five root Rust gates, 14 fixture-core and 41 harness tests, +native fixture clippy/format, and progress-enabled fixture clippy on all three WASM targets. Docs +format/lint/build, the docs/legacy guards, decoder/map dependency audits, 18 evidence-checker tests +and inventory validation passed. The legacy guard first caught a test-double parameter retaining +the old cutoff spelling; it was renamed and the scoped tests, guard and complete four-runtime +suite were rerun. Evidence now points to that final source/run; the earlier passing runtime record +is retained only as diagnostic history. Production sources and lockfiles remain unchanged. +Independent evidence review verified all 96 relay passes, 96 ordered receipts, 42 selected source +hashes and eight embedded texts. Its documentation nit was corrected: truncation tests cover +fixed/chunked bodies; exact-cap and overflow tests cover all three framing modes. + +The requirements' EZ-14.6 correction makes an expired application absolute deadline take +precedence over idle expiry, including a tie and late observation of both expiries. It changes +timeout attribution, not the deadline or the amount of permitted work. + +EZ-8.10's draft revision replaces an impossible universal twice-effective-budget promise for the +synchronous Image Optimizer call with an explicitly approved finite backend-policy horizon H(K). +It preserves the original deadline/source and forbids late success. G4 approval and hosted proof +are still required; ordinary response waits and body reads keep their existing deadline +5 ms bound. + +## Non-Negotiable Baseline + +- Keep `OutboundRequest`, `HttpClient`, positional slot identity, input-ordered collection, + completion-ordered observation, per-slot elapsed time, typed termination, driver poisoning, + failure-with-partial-results, and explicit cancellation. Preserve validation of trailing driver + events and the extra observation required for `Finished(Completed)`. +- Keep independent encoded, decoded, buffered, header, decoder-memory, Brotli-window, and chunk + controls; pre-append enforcement; gzip native completion/multiple members; deflate; typed + passthrough reasons; and typed decode/transport failures. Rechunking is not an allocation bound. +- Keep the app's injected monotonic clock, immutable Content-Type provenance, sanitized public + errors, credential-safe diagnostics, fallible configuration, response completion composition, + detached `Send | Abort`, relative/absolute egress deadlines, and existing bounded ingress/config + behavior. Unknown platform facts stay unknown. +- Hard migration is allowed where the requirements change a contract. Migrate all adapters, + implementations, tests, demo, and generator together. No aliases, old proxy modules, SDK escape + clients, fallback to the old transport, or backward-compatibility branches. +- Keep core free of provider SDKs, Tokio, threads, and production test utilities. Reuse the native + adapter's existing runtime; do not add a runtime dependency to another adapter or core. +- Do not edit the historical normative specs to make a failing implementation appear compliant. + Document explicitly superseded behavior in the active guides and CHANGELOG. + +## Execution Order + +Each linked plan is an independently reviewable work package. Within a package, execute its +checkbox steps in order. D1, F3/F6/F7/F9/F10 and Q4 are task groups: their lettered tasks have separate +files, dependencies and red/green exits, and the parent closes only when every applicable child +closes. Do not parallel-edit shared `outbound.rs`, `error.rs`, or `time.rs`. + +Within qualification, Q1/Q2 and Q3's preparation finish every source change, including package +tooling and the runbook template. Q3 then freezes the candidate before authoritative local runs; +Q4a binds/accepts the external package, Q4b returns hosted results, and Q5 only validates/publishes +that same SHA. A changed source commit returns to the freeze checkpoint and reruns all required +legs; publication must not create a new source revision after qualification. + +| Order | Plan | Scope | Exit Gate | +| ----- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| 1 | [Feasibility and evidence](2026-10-18-trusted-server-outbound-01-gates.md) | Pinned host probes, acceptance inventory, priority decisions | Feasibility artifacts, explicit pass/fail/pending dispositions, applicability and API approval recorded | +| 2 | [Portable policies](2026-10-18-trusted-server-outbound-02-policies.md) | EZ-1, EZ-4/5/7/8/9; shared validation; minimal EZ-24 if promoted | One head-first implementer protocol; portable tests; no default redirects | +| 3 | [Adapter progress](2026-10-18-trusted-server-outbound-03-drivers.md) | EZ-2/3/6 and adapter integration of blocking policies | Eager begin/batches, bounded multiplexing, correct ownership and arrival/cutoff behavior | +| 4 | [Follow-up contracts](2026-10-18-trusted-server-outbound-04-followups.md) | Deferred reuse, EZ-10 through EZ-24 remaining parts | Each selected part passes its own acceptance; no implicit priority promotion | +| 5 | [Qualification and migration](2026-10-18-trusted-server-outbound-05-qualification.md) | EZ-25, demo/templates, CI, EZ-Q1 | Durable exact-commit evidence, consumer comparison, reachable pin | + +Portable changes and each adapter migration form a buildable hard-cut series. Do not merge an +intermediate revision that violates the implemented outbound baseline. Unchanged behavior keeps +its existing tests; replacing a test requires an explicit changed requirement and an equally +strong replacement assertion. + +## Requirement Coverage + +This is the current implementation/next-gap view at the dashboard's snapshot, not a list of +qualified requirements. **All rows retain open acceptance/qualification work.** Local checkpoint +links identify the implemented scope and its test/evidence record; they do not close another +mode, adapter, conditional part or hosted leg. The inventory remains the criterion-level coverage +map; completing its executable bindings is an explicit open task, not hidden by this table. + +| Requirement | Implementation Tasks | Production Applicability | Current Implementation / Checkpoint | Next Unclosed Work | +| ----------- | -------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| EZ-1 | P2; D1-D4 | Blocking, including actual ingress forwarding | **Shared settlement implemented in the worktree.** Certified-empty conversion remains zero-read; unknown GET/HEAD sources settle before dispatch with paired 408/504 deadlines, preserved source errors and hidden deadline provenance across parts. Fastly's approved slack retirement removes the later false rejection. | Complete all-target/runtime acceptance bindings and hosted forwarding evidence. Fastly synchronous inbound reads remain BestEffort. | +| EZ-2 | G2/G4; D1/D5; F1 | Blocking ranges exactly as spec; reuse-only ranges deferred | **Partial implementation:** [B31 existing-path identity/timers](#existing-path-b31-implementation-checkpoint-2026-10-25); [current-operation multiplexer](#current-operation-multiplexer-checkpoint-2026-10-25). Historical mixed/full-buffer readiness failures remain retained. | Callback-wide ownership, remaining modes and registration observations. F1 retirement, hosted pooling and resource qualification remain separate. | +| EZ-3 | D1/D5 | Blocking; bounded waits and original budgets retained | **Approved retirement locally verified.** Remove the 25 ms rejection, exported constant and injection hook. A 26 ms settlement no longer rejects a live budget; dispatch/registration still reject actual expiry and backwards clocks. | Bind bounded-wait acceptance and run the M7 comparison. Preserve original deadlines and B31; hosted/runtime promotion remains separate. | +| EZ-4 | P1; D1-D4 | Blocking; Native/BestEffort distinctions retained | **Configured mapping implemented locally.** Connect setting/readback/round-trip, zero rejection, Fastly stable backend mapping, Spin advisory setter/refusal and configured-vs-unconfigured timeout causes are covered. Axum preserves its shared pool and accepts BestEffort aggregate enforcement; Cloudflare does likewise. Follow retains those settings. | Bind the complete mode/adapter matrices, update capability rows only with evidence, and obtain controlled hosted connect-expiry proof. | +| EZ-5 | P3; D1-D4 | Blocking; Spin conditional host result recorded | **Integrated through the shared head finalizer.** Send/Begin/Batch and final Follow heads retain original-coding policy, body disposition, provenance and independent limits; followed heads are discarded unread. | Complete remaining all-target normalization/limit/runtime acceptance bindings and separately adopted metadata legs. | +| EZ-6 | P4; D1-D6 | Blocking except EZ-6.31 reuse-only | **Shared scope integration locally verified.** Eager drivers feed the arrival ledger, observation-only checks and lossless mixed collection. Core captures unpolled operations including Follow, revokes producer roots without dropping recorded results, and accepts exact provisional body owners before egress policy/framing. Provider family leases preserve early body progress without retaining callback roots. | Bind all applicable mode/lifetime runtime acceptance and qualify providers; local ownership proofs do not close parent qualification. | +| EZ-7 | P3; D1-D4 | Blocking | **Integrated through shared preparation and head finalization.** The facade resolves marker policy before dispatch; original fields and the optional marker are charged once across Send/Begin/Batch/Follow. | Complete remaining all-target marker/normalization acceptance bindings. | +| EZ-8 | P1; D1d/D5 | Blocking; Fastly single-request Send/Follow only | **Send/Follow use the shared pipeline.** Direct native optimizer mapping is preserved without another fetch, buffering or serialization; unsupported modes/adapters reject before settlement. Actual-client Send/initial-Follow/later-hop controls reach Viceroy's native unavailable boundary. | Obtain successful optimized-first-hop redirect and hosted transformation/performance evidence. Blocking horizon remains UNKNOWN. | +| EZ-9 | P1/P5; D1-D5 | Blocking; supplied clients use the same dispatch boundary | **Explicit Follow implemented.** Core owns the decision table, per-hop policy, immutable initial chain deadline, replay and unread disposal. Supplied-client, real-TCP Axum, actual local Spin/Viceroy and Cloudflare guest-boundary controls pass. | Bind full criterion-level acceptance; complete workerd/origin, successful optimizer redirect and consumer/hosted evidence. No compatibility transport. | +| EZ-10 | F2 | Follow-up unless approved as promoted/present | **Local implementation with a known failure:** [typed overflow/uploads/rendering](#f2-overflow-checkpoint); generated/demo TCP proof exists. | Cloudflare front-proxy recovery failure, required hosted HTTP/2 and consumer end-to-end acceptance. Passing sibling cases do not waive the failure. | +| EZ-11 | F3a | Follow-up | **Not implemented:** cache tags. | Tag validation/deduplication, provider mapping, mode/support matrix and actual cache probes. | +| EZ-12 | F3b | Follow-up | **Not implemented:** TLS name-check policy. | Typed setting/readback, backend-key isolation and trusted-chain/wrong-name fixture; no global verification bypass. | +| EZ-13 | F3c | Follow-up; closes with Unsupported evidence | **Unsupported retained with the required synchronous support hook integrated.** Spin authority and unsupported optimizer checks precede ordinary validation, timers and body polling. | Bind the complete activity/SDK limitation and consumer configuration-rejection evidence; do not promote support. | +| EZ-14 | F4 | Follow-up; includes acceptance 10/11 | **Not implemented:** opted-in idle-body policy. | New mode, encoded-progress resets, absolute-deadline precedence and actual adapter read races; existing total deadlines are not idle-mode support. | +| EZ-15 | F5 | Follow-up; reuse legs separately conditional | **Not implemented:** explicit bounded fire-and-forget operation. | Separate capacity/ownership, 64 KiB response ceiling, cancellation/recovery and provider-lifetime proof. | +| EZ-16 | F6a | Follow-up | **Diagnostic checkpoints implemented:** [SDK/source details and capability publication](#f6a-capability-publication-checkpoint). | Full token-safe acceptance matrix, remaining integration cases and consumer/provider qualification. Capability publication itself has landed. | +| EZ-17 | F6b | Follow-up | **Local implementation:** [owned delivery reports/safe replacement](#f6b-delivery-report-checkpoint); subsequent F9a assembly integration has landed. | Consumer canonical-lifecycle acceptance and full required qualification, not another assembly integration implementation. | +| EZ-18 | F7a | Follow-up | **Local implementation:** [buffered framing](#f7a-buffered-framing-checkpoint); 80 production cases passed, nine SDK reference controls failed. | Resolve/qualify parity and hosted compression while preserving strict suppression. The complete local suite remains nonzero. | +| EZ-19 | F7b | Follow-up | **Local implementation:** [bounded synthesized headers](#f7b-synthesized-header-checkpoint), wire and assembly-exclusion checkpoints. | Consumer-owned header integration and hosted/frozen-candidate qualification; local assembly exclusion is not still missing. | +| EZ-20 | F8 | Follow-up | **Local implementation:** [declared KV bindings and operation delivery](#f8-declared-kv-checkpoint) on all three adopted serverless adapters. | Actual provider outage/restoration, consumer acceptance and qualification; the separate Spin retained-overlap uncertainty remains disclosed. | +| EZ-21 | F6c | Follow-up | **Local implementation:** [six typed framing reasons](#f6c-framing-checkpoint), observer/log controls. | Consumer Viceroy acceptance and separately required hosted evidence across applicable failure paths. | +| EZ-22 | F9a | Follow-up; individual SHOULD decisions explicit | **Local implementation:** [unified fallible assembly/failure delivery](#f9a-assembly-foundation-checkpoint), including adopted macro/serverless parts. | Consumer acceptance and exact-candidate qualification; no compatibility assembly path to add. | +| EZ-23 | F9b | Follow-up | **Local implementation:** [assembled-App Axum/TCP helpers](#f9b-assembled-app-tcp-checkpoint), twelve upstream sentinels. | Criterion 12's conditional begun-request portion and criterion 13's consumer route/parity evidence. | +| EZ-24 | P6; F10a/F10b/F10c | Host/WASI publication and each optional extension need separate decisions | **Not implemented as the proposed published utility; adoption pending.** Existing private doubles are not P6. | Attributed applicability decisions, P1-P5 semantics, then selected published-client/cancellation/demo/JS work. Pending is not approved deferral. | +| EZ-25 | Q1/Q2 | Documentation updates accompany each affected behavior | **Hard-cut caller migration locally verified.** Supplied clients, demo, generator and fixtures use prepared contexts, raw events, explicit states, shared progress and the Body payload/owner split. Guides, changelog and guards track the same implementation, including explicit Follow. | Complete applicable acceptance bindings, then qualify the frozen candidate. | +| EZ-Q1 | G1; Q3-Q5 | Required blocking, approved promoted, and confirmed-present parts only | **Preparation only:** inventory/checker, [evidence assembly](#q3-evidence-preparation-checkpoint), [runbook source](#q2-runbook-source-preparation-checkpoint). | Executable leg bindings and scope decisions, remaining sources, candidate freeze, accepted Q4a package, authorized Q4b runs and durable passing Q5 record. | + +This summary inherits the normative-range-to-leaf ownership table in +[plan 04](2026-10-18-trusted-server-outbound-04-followups.md#explicit-requirement-ownership). +In particular EZ-11/12/13 close through F3a/F3b/F3c respectively, EZ-16/17/21 through +F6a/F6b/F6c, EZ-18/19 through F7a/F7b, and EZ-22/23 through F9a/F9b. EZ-24.11/.12/JS +remain separately selected F10a/F10b/F10c tasks. Fastly D1a/D1b/D1c/D1d own progress/backend/ +connect/Image Optimizer work respectively; a dependency names the required children, not all +of D1 unless the entire driver really is required. Hosted qualification explicitly comprises +Q4a's accepted package and Q4b's returned TS-executed results. A sibling's passing test, package +delivery alone or a group heading never closes another leaf's requirement. + +## Decisions Before Runtime Changes + +- [ ] G1 records the decision on EZ-24.1-24.10 host/WASI promotion. The default remains Follow-up; + no promotion is required to start EZ-1-EZ-9. Until an explicit promotion is approved, use a + crate-private recording harness. P6 follows the shared production semantics, never precedes them. +- [ ] Confirm EZ-2 M1-M5/M7 product thresholds and all open questions that change acceptance. + The written normative defaults remain the proposed implementation; do not silently adjust + thresholds or skip required legs. +- [ ] Complete G4's remaining policy decisions. AG approved the exact Interfaces digest for API + implementation, and the requester separately approved the existing-path B31 subset above. + The Fastly readiness primitive and full engine/resource policy remain separately gated. + Numerical H(K) and its hosted evidence still gate qualification, not the permitted native + Send-first integration. Neither approval is inferred from timer rounding. + A failed Fastly feasibility leg does not prohibit approved portable or other-adapter work. + An absent or rejected API approval stops P1; it cannot be inferred from test success. +- [ ] Do not start dependent Fastly engine work until G2 passes locally and the hosted leg is + verified or explicitly retained as a production blocker. A safe 500 before an unbounded read + is not qualification success. + +## Shared Verification and Commit Discipline + +For every runtime Rust task: add a regression test, demonstrate the intended failure, implement +the smallest change, rerun scoped tests, and review the diff. `scripts/run_test_nonzero.sh` must +observe the named new sentinel; an empty test selection is failure, not green evidence. The helper +parses Rust test listings and must not wrap Node tests or host-probe CLIs. Node tasks name their +test file/cases and assert nonzero execution; probe tasks additionally record nonzero real case +counts and measured outcomes. Approval/handoff tasks close with attributed decisions/results, +not an invented Rust test. Commit boundaries +are suggested in each task; commit/push only when authorized and after required checks pass. + +Run these gates before a runtime series is declared ready: + +```sh +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace --all-targets +cargo check --workspace --all-targets --features "fastly cloudflare spin" +cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin +node scripts/check_outbound_docs_contract.mjs +bash scripts/check_outbound_legacy_api.sh +node scripts/check_brotli_dependency_contract.mjs +node scripts/check_flate_dependency_contract.mjs +node scripts/check_serde_json_map_contract.mjs +``` + +Q2 adds the existing target feature matrices, real SDK execution sentinels, fixture workspace, +generated-project tests, and excluded demo workspace. Fetch all three lockfile workspaces before +offline audits. A local contract pass does not promote a hosted capability. +Q2 lists the seven exact `bash scripts/check_adapter_feature_matrix.sh ` +invocations (four native, three WASM), each compiling eight no-default-feature combinations. +They are required compile checks, not substitutes for target clippy or real runtime sentinels. +Every phase exit links back to this shared gate card and its task-specific evidence. The gates +remain centralized rather than diverging into five slightly different copies. + +### Per-Task Red/Green and Lint Procedure + +Each runtime task includes this procedure, not merely the final workspace gate. Run the named +test with `scripts/run_test_nonzero.sh` before implementation; Expected: a real failing assertion +or missing new API, never an empty selection. After implementation, Expected: the named sentinel +executes and passes, then scoped tests pass, rustfmt is clean, and clippy reports zero warnings. +For a core task the exact commands are: + +```sh +cargo test -p edgezero-core --features test-utils +cargo fmt --all -- --check +cargo clippy -p edgezero-core --all-targets --all-features -- -D warnings +``` + +For an adapter task replace the package with the changed adapter package and run its native and +WASM commands in plan 03/Q2. For fixture changes use `--manifest-path tests/fixtures/reusable-app/Cargo.toml`; +format and lint that excluded workspace separately. Demo changes similarly use its own manifest. +Do not commit a public trait cut until every affected implementation and generated fixture builds. + +Use these native test feature selections for every named adapter sentinel; replace only the +sentinel name and test target when the owning task specifies one: + +```sh +scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --test contract +scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils --test contract +scripts/run_test_nonzero.sh batch_preflight_rejects_streamed_slots_without_poisoning_siblings cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --test contract +scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils --test contract +``` + +These existing cases are regression controls, not the new task's red test. Native Fastly tests +must not enable `fastly`: SDK host symbols cannot link on the native target. Real Fastly/Spin/ +Cloudflare SDK tests instead use Q2's explicit WASM target, platform feature and pinned runner. +Native all-features clippy/check is allowed because it does not link/run SDK test binaries. + +### Clippy Briefing + +`Cargo.toml` enables `restriction = deny` and `pedantic`; `clippy.toml` relaxes selected idioms only +in tests. Public functions need `#[inline]`, appropriate `#[must_use]`, and error/panic documentation. +Follow existing alphabetical item/variant ordering; keep types before helper functions where the +lint requires it. Production code must not gain `unwrap`, `expect`, `panic!`, unchecked indexing, +unsafe Send wrappers, blanket lint suppressions, or dead feature-only helpers. Run each adapter's +CLI-only/no-default/target feature matrix: native all-features clippy does not cover WASM-only code. + +## Completion Definition + +- [ ] Every required acceptance/environment leg has an executable case and a passing result. +- [ ] Every deferred leg cites its approved deferral or unmet explicit condition under EZ-Q1.8. +- [ ] No outbound regression, compatibility branch, stale generator example, or unreviewed + capability promotion is present. +- [ ] Q5 identifies a reachable candidate SHA and a durable passing record for that exact SHA. + +Deployment, consumer migration, commits and pushes remain separately authorized actions. A +negative feasibility experiment is not completion of the runtime series or approval to repin. diff --git a/docs/superpowers/specs/2026-05-21-outbound-http-design.md b/docs/superpowers/specs/2026-05-21-outbound-http-design.md new file mode 100644 index 000000000..346b2e939 --- /dev/null +++ b/docs/superpowers/specs/2026-05-21-outbound-http-design.md @@ -0,0 +1,7510 @@ +# EdgeZero Outbound HTTP — Design Spec + +> **Status:** Normative baseline for non-superseded contracts. The head-first API supersession below replaces historical implementer and batch-construction sketches without compatibility wrappers. Earlier Phases 1a-7 and owned response-egress integration are implemented on PR 275; the current adoption tracker distinguishes implementation, remaining scope/redirect work and qualification. Response-egress capabilities remain `BestEffort` pending deployed proof. · **Baseline Date:** 2026-09-15 +> **Branch:** `docs/outbound-http-spec` · **Audience:** EdgeZero maintainers +> **Driving pattern:** fan-out HTTP workloads — N outbound requests under a shared wall-clock deadline, terminal results observed in completion order while preserving input indices. The spec is written against this pattern as a portable substrate; it deliberately does not name a specific consumer. +> **Historical target baseline:** [`stackpop/edgezero` PR #269](https://github.com/stackpop/edgezero/pull/269) (`feature/extensible-cli`, rev `b4c80e9`) — **now merged into `main`** (squash-merged as `e483723`). Relevant baseline changes were the `edgezero_cli::adapter::execute(..)` shell-or-registry dispatcher, expanded runtime `AdapterAction` variants, the then-current Spin SDK 6 / wasip2 integration, the contributor-only `demo` command replacing `dev`, and the app-demo integration crate. The current implementation pins Spin SDK 7.0.0. Non-outbound store/config lifecycle changes remain outside this design. +> **Current checkout (post-#269 and staging-deploy work):** the CLI surface includes `Command::{Build, Serve, Deploy, Auth, Provision, Config, Demo, New}`; `Action` / `AdapterAction` additionally include `DeployStaging`, `EmitVersion`, `Healthcheck`, and `Rollback`; and adapter dispatch has both `execute(..)` and `execute_capture(..)` entry points. `dev` is gone. This outbound spec gates construction/deployment of the current runtime: `build` / `serve` / `deploy` / `deploy --staging` through both dispatch entry points, plus `demo` before Axum starts. Operational auth/version/health/rollback actions and provisioning/config/store lifecycle policy are exempt or belong to their owning specifications (§3.5.3). +> **Where rebase claims live (authoritative surfaces):** §3.5.3 build-enforcement, §3.5.2 `Adapter` trait shape, §5.4 capability tests, and the §7 `edgezero-cli` migration bullet. The §3.5.3 + §7 active text is authoritative. + +## 1. Overview + +### Explicit Redirect Following Supersession (2026-10-27) + +The explicit `HttpClient::follow` API and [redirect guide](../../guide/proxying.md#following-redirects) +supersede this document's historical manual `Location` loop guidance for applications opting in. +Plain Send, Begin and Batch remain unfollowed. Each opted-in hop uses the same supplied client, +shared preparation, provider head acquisition and ordinary finalizer, with a required target policy +and a validated 1..=20 limit. Followed heads are discarded without polling or decoding their bodies; +final heads retain all normal response limits, decoding, provenance and ownership. + +The initial absolute deadline remains an independent upper bound across replacement requests, +policy execution and final streamed consumption. Per-call timeouts remain per-hop. Method/body, +credential and original Location evidence rules are defined by EZ-9 and the approved Interfaces; +this supersession does not authorize an alternate transport, permissive default policy, compatibility +API or a weaker batch contract. Native Image Optimizer requests retain their direct native path. +Local implementation and tests do not close remaining all-target runtime acceptance, candidate +binding, hosted qualification or the optimizer's unknown synchronous blocking horizon. + +### Dispatch-Slack Retirement (2026-10-27) + +The requester authorized retirement of the fixed 25 ms dispatch-slack refusal. The +`BATCH_DISPATCH_SLACK_MAX` constant, `inject_dispatch_slack_for_test` helper and injection machinery +are removed without compatibility aliases. All descriptions below of that guard, its injection +test, slack-derived timing bounds and live-budget Internal failures are historical and superseded, +including the older B31 checkpoint's statement that the guard remains. + +Preparation or backend registration consuming more than 25 ms does not itself reject a request +whose original absolute deadline remains live. Absolute expiry checks, attributed timeout causes, +strict observation-cutoff equality, late-result rejection, B31 backend identities/provider timers +and resource limits remain unchanged. Budgets are neither restarted nor backdated. Synchronous SDK +host calls remain non-preemptible; retirement establishes no finite blocking horizon, prompt +cancellation, Native capability promotion or hosted qualification. Historical probe hashes, +measurements and failures remain unchanged. EZ-3 acceptance legs stay planned until their real +commands and sentinels are bound and verified. + +### Head-First Contract Supersession (2026-10-27) + +The approved [head-first hard-cut contract](../plans/2026-10-18-trusted-server-outbound-02-policies.md#interfaces-proposed-hard-cut-contract) +supersedes older implementer `send`/raw-vector batching, public receipt construction, mutable +collection fields, Option slots and adapter-owned response finalization in this original design. +It also adds the independent connect setting and typed ConnectTimeout/IdleTimeout budget causes; +older enum sketches do not narrow the current error taxonomy. +Those API sketches are historical, not compatibility entrypoints. The shared facade captures +invocation time and prepared contexts; adapters return raw heads and observations of eagerly +scheduled work. Original caps, codecs, provenance, typed errors, cutoff semantics and ownership +remain requirements. See the [implementation tracker](../plans/2026-10-18-trusted-server-outbound-implementation.md) +for incomplete lifecycle, redirect, provider and qualification work rather than inferring +completion from these interfaces or from local test results. + +### Fastly B31 Existing-Path Policy (2026-10-25) + +The [scoped implementation approval](../../audits/2026-10-18-outbound-api-approval.md#scoped-b31-implementation-approval) +supersedes every client-namespaced/exact-budget backend algorithm, quarter/three-quarter phase +split and timing bound derived from that split in this document. Those older Fastly derivations +remain historical design records, not a compatibility policy or current enforcement claim. + +At the existing method-entry snapshot, anchor provider timers to the configured per-call timeout +clamped to seven days; without one, use the originally selected duration. Round upward to +milliseconds, then choose the smallest member of `{2^k ms | k = 0..29} union {604800000 ms}` +at least that large. Connect, first-byte and between-bytes share this value. The selected absolute +deadline, source and observation cutoff stay unchanged. A shorter deadline/cutoff does not change +the configured-timeout backend. Positive sub-millisecond inputs select a 1 ms provider timer, +not a longer logical budget. Explicit Connect/idle options are still separate adoption work. + +Versioned `ez_b31v1_` names hash canonical scheme, host and wire authority (NUL-delimited validated +text), then fixed big-endian port, TLS flag and selected timer milliseconds, using full SHA-256. +A single verified guest-instance registry replaces per-client namespaces/caches. It compares full +identities, serializes synchronous registration/insertion, rechecks expiry after acquiring the +lock, retains successful late registrations before reporting their original timeout, and refuses +unknown `NameInUse`. Only configuration metadata and SDK backend names are retained. + +There are at most 31 deadline-only configurations per fixed transport tuple, not a total memory, +origin or registration bound. Existing 25 ms dispatch guards, blocking head/body operations and +late-result rejection remain. Larger provider timers can substantially outlive a shorter logical +deadline; neither the old phase-sum overshoot bound nor prompt interruption is claimed. Readiness, +retirement, hosted pooling/resource/cancellation proof and the new head-first migration remain open. +All portable caps, cutoff behavior, diagnostics and direct native optimizer dispatch are preserved. + +### Native Image Optimizer Checkpoint (2026-10-24) + +`OutboundRequest::image_optimizer` attaches typed region, format, quality, resize, dimension, +crop and query-preservation settings, retained through request parts. Fastly single sends map +them directly to native SDK options and share ordinary response handling. Other adapters and +Fastly batch slots reject the option before ordinary preflight or body polling. Begun requests, +redirect following and forwarded empty GET/HEAD settlement remain adoption-plan work, not +implemented by this checkpoint. No compatibility client or alternate transport is introduced. + +The [scoped timing amendment](../../audits/2026-10-18-outbound-api-approval.md) +preserves original deadlines and rejection of late results. The synchronous optimizer call cannot +be interrupted; its full blocking horizon is unknown. The +5 ms timing target is qualification +evidence, not a hard scheduler guarantee. Existing failed evidence is retained. Native mapping +and local entitlement rejection do not qualify hosted transforms, pooling or performance. + +### 1.1 Goal + +Make EdgeZero a production-safe substrate for **outbound HTTP fan-out**: an app must be +able to issue many independent target requests, use concurrent complete exchanges where the +platform supports them, enforce per-request and whole-fan-out batch deadlines, keep memory +predictable, and run the *same handler source* unchanged on Axum, Cloudflare Workers, Fastly +Compute, and Spin. + +"Predictable memory" here means: a documented, bounded cost per buffered outbound request +and response, plus an explicit batch-level memory model the app controls (§3.4.4). +It does **not** mean EdgeZero imposes a global allocation ceiling. + +### 1.2 Context + +At the historical implementation baseline, applications proxied a single outbound request +through `ProxyClient` / `ProxyHandle`. The gaps this design set out to close were: + +- A first-class, **independently constructed** outbound request type. +- **Attempt-before-harvest fan-out.** The baseline Fastly client called + `pending_request.wait()` inside a single `send()`, so any `join_all` of `send()` calls ran + strictly serially. The implemented Fastly contract dispatches every eligible request before + harvest without promising simultaneous transport overlap. +- A **portable deadline** primitive. +- **Bounded buffering** helpers with clean error mapping. +- A way for an app to **declare required capabilities** and fail the build early. + +### 1.3 Non-goals + +- No consumer-specific target logic in EdgeZero. +- EdgeZero does not own application target policy or allowlists. It exposes + `OutboundRequest::uri()` so apps enforce their own policy; it never blocks a request itself. +- EdgeZero does not add an application loop counter, `Via` policy, or self-reference rejection. + Those depend on deployment identity and trust boundaries and remain application policy; the + reference proxy example must not imply that `PROXY_HEADER` is a loop-prevention mechanism. +- No new direct dependency on `tokio`, `reqwest`, `fastly`, `worker`, or `spin-sdk` in + application/library crates or in `edgezero-core`. Those stay inside adapter crates. +- No general-purpose "timeout any future" combinator in this spec — see §3.3.5. +- Inbound admission, request-start stamping, request-body read deadlines, and raw request + framing rejection are owned by the + [inbound-body design](2026-08-22-inbound-body-design.md), not by outbound HTTP. Outbound + implementation may start independently; it must not claim those ingress guarantees. +- The lifetime after a core `Response` reaches a platform adapter is a separate + downstream-response concern owned by the + [response-egress design](2026-09-08-response-egress-design.md) and its + [implementation plan](../plans/2026-09-08-response-egress-implementation.md). The adapters now + own that lifecycle through their strongest host boundary, but it remains distinct from the + outbound request deadline and is conservatively classified `BestEffort`. §3.3.3 defines the + precise boundary of the outbound request deadline. + +### 1.4 Decisions locked before / during review + +- **No backward compatibility.** `ProxyClient` is renamed and reshaped in place; + `app-demo`, scaffolding templates, and docs are migrated. No deprecated + aliases. +- **One adapter-owned buffered batch engine.** `start_batch_until` is the only + adapter-facing fan-out primitive for buffered request bodies and buffered responses. + `OutboundBatch::next` exposes indexed terminal results in observed completion order; + `HttpClient::send_all_until` is an ordered collector over that same driver, not a second + orchestration path. The hard cut removes `send_all`. Input validation and result identity + are uniform, while cross-slot timing and teardown quality remain capability-qualified. + Axum, Cloudflare, and Spin drive complete exchanges concurrently. Fastly issues sequential + guest dispatches before selecting pending requests and retains its documented serial body + drain and finite-teardown limitations. Streamed-response fan-out remains non-portable; + applications use single `send` calls for streamed responses. +- **Transport policy is typed.** `OutboundRequest` carries a portable cache policy and an + optional validated HTTP authority override. The URI always owns the connection target, + SNI, and certificate identity. An authority override only changes the outgoing HTTP + authority. An adapter unable to preserve that separation rejects the request before body + polling or provider dispatch. +- **Fastly custom entrypoints remain closed over egress.** The advanced Fastly runner allows + bounded request-preparation and response-finalization hooks while EdgeZero retains ingress, + framing, streaming, deadline, transmission, and completion ownership. The retired + request-extension-only runner is not retained as a compatibility alias. +- **Unified body.** Outbound request and response bodies use the existing core `Body` + type and may be **buffered (default)** or **streamed (opt-in)**. Streaming + proxy-forwarding is preserved — it is not dropped (review finding / residual risk). +- **Deliverables:** this design plus phased implementation plans. Phases 1a-7 and review + hardening were subsequently implemented on PR 275; the status block above names the + remaining response-egress certification boundary. + +## 2. Historical implementation baseline + +This table records the code state from which the outbound implementation began. It is retained +for design provenance and does not describe the current PR head; the status block above is the +current implementation summary. + +| Concern | Baseline | File | +| --- | --- | --- | +| Outbound trait | `ProxyClient::send(ProxyRequest) -> Result` | `crates/edgezero-core/src/proxy.rs:16` | +| Handle | `ProxyHandle` (`Arc`), `RequestContext::proxy_handle()` | `proxy.rs:21`, `context.rs:97` | +| Request type | `ProxyRequest::new(method, uri)`; `ProxyRequest::from_request` (streaming) | `proxy.rs:138`, `proxy.rs:100` | +| Body | `Body { Once(Bytes), Stream(..) }`; `Body::into_bytes_bounded(max)` exists | `body.rs:14`, `body.rs:76` | +| Errors | Phase 1a has typed 502/504 errors; response-limit 502 remains Phase 2. `EdgeError` is `#[non_exhaustive]` | `crates/edgezero-core/src/error.rs` | +| Deadlines | Phase 1a has `Deadline` and constants; `DispatchBudget` / `dispatch_budget` remain Phase 1b | `crates/edgezero-core/src/time.rs` | +| Fastly send | `send_async_streaming()` then `pending_request.wait()` — serializes | `crates/edgezero-adapter-fastly/src/proxy.rs:30` | +| Fastly backend name | host with only `.`/`:` sanitized | `crates/edgezero-adapter-fastly/src/proxy.rs:110` | +| Manifest | no capability declaration or outbound host plumbing | `crates/edgezero-core/src/manifest.rs` | +| Adapter trait | `execute` / `name` plus existing non-outbound lifecycle hooks; no capability metadata | `crates/edgezero-adapter/src/registry.rs` | +| Contract tests | exist for Cloudflare/Fastly/Spin; **Axum has none** | `crates/edgezero-adapter-*/tests/contract.rs` | +| Scaffold templates | emit proxy code | `crates/edgezero-cli/.../handlers.rs.hbs`, `spin.toml.hbs:13` | +| Public docs | document `ProxyService`/`ProxyRequest` | `docs/guide/proxying.md`, `docs/guide/handlers.md`, `docs/guide/architecture.md`, `docs/guide/what-is-edgezero.md`, `docs/guide/adapters/*` | + +## 3. Design + +> **⚠️ Code blocks in this spec are ILLUSTRATIVE, and many predate the strict lint +> gate.** The workspace denies `clippy::restriction` and warns `clippy::pedantic` under +> `-D warnings` (root `Cargo.toml`). Any snippet copied into an implementation **must** +> be brought to that gate before it will compile in CI. The recurring offenders, with +> their required forms (the Phase 1a plan carries the full list and a verified-green +> example): +> - **`missing_inline_in_public_items`** → every public fn needs `#[inline]`. +> - **`min_ident_chars`** → no single-char idents (`d`→`duration`, `e`→`err`, `m`→`manifest`). +> - **`arithmetic_side_effects`** → no bare `+`/`-`/`*`/`/`; use `checked_*` / `saturating_*` / `try_from`. +> - **`as_conversions`** → no `as` casts; use `From`/`TryFrom`/`u64::from`. +> - **`expect_used` / `unwrap_used`** → forbidden in production (allowed in `#[cfg(test)]`); use `?`/`ok_or`. +> - **`arbitrary_source_item_ordering`** → module items, struct fields, enum variants, and impl fns alphabetical (or a documented `#[expect(..)]`). +> - **`duration_suboptimal_units`** → `Duration::from_hours(168)`, `from_mins(1)` — not `from_secs(7*24*60*60)` / `from_secs(60)`. +> +> Where a snippet below still shows a bare-`d` closure param, `x as u64`, or +> `a + b`, read it as shorthand for the lint-clean form — do not copy it verbatim. + +### 3.1 Outbound HTTP client abstraction + +`crates/edgezero-core/src/proxy.rs` is renamed to `crates/edgezero-core/src/outbound.rs`. +Bodies use the **existing core `Body`** type (`Once(Bytes)` | `Stream(..)`), so a request +or response may be buffered or streamed. Buffered is the default; +streaming is an explicit opt-in that preserves proxy-forwarding. + +#### 3.1.1 Adapter-facing trait — two required methods + +```rust +// crates/edgezero-core/src/outbound.rs + +/// One terminal result produced by a buffered batch slot. +#[derive(Debug)] +#[non_exhaustive] +pub struct OutboundSlotResult { + pub elapsed: Duration, + pub outcome: Result, +} + +/// One terminal batch result paired with its original input index. +#[derive(Debug)] +#[non_exhaustive] +pub struct OutboundBatchItem { + pub index: usize, + pub result: OutboundSlotResult, +} + +/// Ordered collection of the batch state at completion or observation cutoff. +#[derive(Debug)] +#[non_exhaustive] +pub struct OutboundBatchResults { + /// Index-aligned with the input. `None` is valid only with `Cutoff` termination. + pub slots: Vec>, + pub termination: OutboundBatchTermination, +} + +/// A batch-level driver failure and the terminal slots collected before it occurred. +#[derive(Debug, Error)] +#[non_exhaustive] +pub struct OutboundBatchFailure { + pub error: EdgeError, + /// Index-aligned with the input; unresolved slots remain `None`. + pub slots: Vec>, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum OutboundBatchTermination { + /// Every input slot produced exactly one terminal result and the driver reached clean EOF. + Completed, + /// The observation cutoff won while one or more slots remained unresolved. + Cutoff, +} + +/// One observable step from an outbound batch. +/// +/// After the final `Item`, callers poll once more to receive `Finished(Completed)`; +/// completion is not inferred from the number of items observed. +/// The final poll waits for driver EOF and still validates trailing events or finalization failures. +#[derive(Debug)] +#[non_exhaustive] +pub enum OutboundBatchNext { + Finished(OutboundBatchTermination), + Item(OutboundBatchItem), +} + +/// Public adapter-to-core batch driver protocol. +#[derive(Debug)] +#[non_exhaustive] +pub enum OutboundBatchDriverEvent { + Cutoff, + Failed(EdgeError), + Item(OutboundBatchItem), +} + +/// Adapter-owned completion driver. The transport and pending handles remain opaque. +pub struct OutboundBatch { /* private */ } + +impl OutboundBatch { + /// Stops observation, applies strongest-available teardown, and returns unresolved indices. + /// The caller still owes every returned slot an outcome because `EdgeZero` observed no + /// terminal result for those indices. + pub fn cancel(self) -> Vec; + + /// Collects terminal items into the original input order until completion or observation cutoff. + pub async fn collect(self) -> Result; + + /// Builds an already-cut-off batch without polling adapter transport work. + pub fn cutoff(slot_count: usize) -> Self; + + /// Builds a batch from an adapter-owned event stream. + pub fn from_driver(slot_count: usize, stream: StreamValue) -> Self + where + StreamValue: Stream + 'static; + + /// Shared future-based driver. Inputs retain original indices and terminal samples. + /// Drain immediately ready futures before delivering the first item; introduce no timer. + pub fn from_futures( + slot_count: usize, + batch_started_at: MonotonicInstant, + observation_cutoff: Deadline, + completed: Vec<(usize, MonotonicInstant, Result)>, + pending_futures: Pending, + ) -> Self + where + F: Future)> + + 'static, + Pending: IntoIterator; + + /// Returns the next observed terminal item or the explicit termination reason. + /// Cancelling this future does not consume or lose a ready item. + pub async fn next(&mut self) -> Result; +} + +#[async_trait(?Send)] +pub trait OutboundHttpClient: Send + Sync { + /// Send a single request. Accepts streamed request bodies — this is the API + /// for streaming proxy-forwarding (one inbound → one outbound). + /// + /// **`Buffered` mode:** `Ok(resp)` means the full exchange completed — + /// headers AND the response body buffered within the deadline and the + /// final collection cap, plus any configured encoded/decoded/resource caps. + /// `Err(_)` is returned for transport failure + /// (DNS/TLS/connect), deadline expiry, or over-cap. + /// + /// **`Streamed` mode:** `Ok(resp)` means headers completed. Body-phase + /// failures surface later, when the caller consumes `resp.into_body()`: + /// - **Read errors / decompression failures / deadline expiry** during + /// chunk reads come from the deadline-aware stream wrapper + /// as `Err(EdgeError::..)` chunks. + /// - A configured **decoded-output cap** wraps effective identity and EdgeZero-decoded + /// gzip/deflate/Brotli output in both response modes, but never raw passthrough. It can yield + /// `ResponseLimitReason::DecodedBody` from the returned stream. A later bounded helper + /// (`OutboundResponse::into_bytes_bounded(max)`, `into_bytes_bounded_until`, or + /// `json_bounded[_until]`) owns a distinct final collection cap and reports + /// `BufferedBody`. Independently configured response-resource controls still apply: + /// header limits can fail `send` before it returns; encoded-byte, Brotli-window, and + /// decoder-heap limits can yield a typed error from the returned stream; and + /// `max_chunk_bytes` shapes emitted items (§3.4.5). + /// - Raw passthrough bypasses `max_decoded_response_bytes` on every adapter because + /// EdgeZero did not produce decoded output. Every standard adapter preserves the resulting + /// `Body::Stream` lazily through its owned response-egress coordinator; there is no implicit + /// downstream whole-body collection cap. The `lazy-streamed-response-passthrough` capability + /// is `Native` on Axum and Cloudflare. It remains `BestEffort` on Fastly and Spin pending + /// end-to-end target evidence, not because either buffers the complete stream. + /// If the caller has *already started writing the downstream response + /// headers* (e.g. a proxy-forward via `into_response` that the platform + /// coordinator has begun sending), HTTP no longer allows a status change. + /// The adapter response coordinator then requests the strongest platform-supported + /// downstream-body abort and logs only a fixed typed category for the originating + /// `EdgeError`; provider/application messages and request data are never interpolated. + /// Clients observe an + /// incomplete response rather than a synthetic 502/504. Exact wire behavior (for example, + /// a connection close or stream reset) is platform/protocol behavior and must be + /// characterized, not universally asserted. The separate response-egress contract in + /// §1.3 owns write deadlines and exactly-once completion. + async fn send(&self, req: OutboundRequest) -> Result; + + /// Attempt every eligible request before waiting for transport completion. Axum, + /// Cloudflare, and Spin drive complete exchanges concurrently. Fastly performs sequential + /// guest dispatch before selecting pending requests, so simultaneous transport overlap is not + /// guaranteed. + /// + /// Each yielded item carries the index of its originating request and that slot's terminal + /// elapsed time. `start_batch_until` + /// captures one `batch_started_at` monotonic instant as its first operation. Every slot, + /// including a slot rejected during preflight, measures from that same instant through + /// the instant its own outcome becomes terminal. Elapsed time therefore includes + /// EdgeZero validation, adapter preparation, provider queueing, upload, response headers, + /// Buffered body drain, and any platform-imposed delayed observation. It is not a pure + /// wire RTT. Each terminal slot samples the clock independently; the batch's eventual + /// final collection time is never copied into every slot. Monotonic subtraction is checked. A + /// backwards injected clock replaces that slot's outcome with an internal invariant error + /// and uses `elapsed = Duration::ZERO` as a fallback. Zero is also a valid elapsed value + /// when start and terminal samples are equal at the clock's resolution; callers distinguish + /// the clock-fault case by its internal outcome, not by elapsed alone. + /// An `Ok` outcome is contractually terminal and on time. `elapsed` is metadata, not an + /// independent lateness detector: callers must not sample their clock after the whole batch + /// returns to reinterpret an earlier success. The API intentionally exposes neither an + /// absolute terminal instant nor the internal `batch_started_at` instant. + /// + /// **Input handling is isolated per slot**: a `bad_request` for + /// one preflight failure never changes another slot's input shape, and one + /// slot's `Ok`/`Err` type never mutates another's. Cross-slot *timing* is + /// **not uniformly isolated** — see the `outbound-batch-slot-isolation` capability + /// (footnote 4): on Axum/CF/Spin it is `Native` (concurrent complete + /// exchanges), but on Fastly it is `BestEffort` because dispatch is + /// sequential, cold backend registration can block later issuance, + /// background request uploads can leave their own slots unresolved, and + /// each selected response body is drained synchronously before the next pending-handle + /// selection. A selected slot can therefore delay later observation, and a later-observed slot + /// whose own budget would have covered it can still return + /// `gateway_timeout`. Apps that require the stricter cross-slot timing + /// guarantee declare the capability required and get a hard build failure + /// on Fastly. `start_batch_until(vec![], observation_cutoff)` produces an already-complete batch. + /// + /// **Memory model — CORE-OWNED retained payload only.** This formula bounds the + /// buffers EdgeZero core holds; it deliberately EXCLUDES (a) adapter-side upload + /// staging copies (e.g. a `chunk.to_vec()` handed to a platform write path) and + /// (b) opaque host/runtime buffering (the Fastly/CF/Spin host may retain its own + /// copy of in-flight bytes). Worst-case core-owned retained buffer for + /// one `send_all_until` is `Σᵢ request_bodyᵢ.len + Σᵢ max_response_bytesᵢ` + /// (per-slot caps). Transient core overhead during a buffered drain adds up to + /// one in-flight chunk per actively-draining slot (the + /// `current_chunk.len()` term from §3.4.4); the full core-owned bound is therefore + /// `Σᵢ request_bodyᵢ.len + Σᵢ max_response_bytesᵢ + Σⱼ + /// current_chunkⱼ.len()` where j ranges over slots currently in a drain + /// step. Actual process RSS can exceed this by the excluded adapter/host terms. EdgeZero does NOT impose a global cap on N — apps are + /// responsible for bounding the number of requests passed in. Fastly attempts + /// to dispatch every slot before harvest, and every slot whose sequential + /// dispatch returns is then in flight at the host. Cold dynamic-backend creation + /// can delay a later dispatch; an unbounded background upload can leave its own slot + /// unresolved, but does not block later `send_async` calls after it has returned or prevent + /// another handle in the active selection group from being selected. A `max_concurrency` knob + /// would not repair those + /// platform gaps, so bound N at the application layer + /// (typically the fan-out batch's target count). + /// + /// **Request bodies MUST be buffered (`Body::Once`).** A `Body::Stream` + /// request body yields an indexed item whose outcome is + /// `Err(EdgeError::bad_request("batch requires buffered request bodies; use send for a + /// streamed upload"))`, + /// identically on every adapter. This rule removes the unbounded + /// **source-pull** problem from portable fan-out. It does NOT bound Fastly's + /// guest-to-origin write of a non-empty `Body::Once`; that separate + /// cross-slot limitation is owned by `outbound-batch-slot-isolation` footnote 4. + /// + /// **Response mode MUST be Buffered.** A request whose `response_mode` + /// is `Streamed` (via `stream_response`) yields an indexed item whose outcome is + /// `Err(EdgeError::bad_request("batch requires buffered responses; + /// use send for a streamed response"))`, identically on every adapter. + /// Reason: the portable batch transfers only terminal, fully buffered slots. A streamed + /// response would transfer a live provider body out of the adapter-owned cancellation and + /// observation-cutoff state machine. There is no portable concurrent + /// body-consumption primitive in the batch driver to fix this (Fastly has no + /// guest reactor). Apps that want streamed responses use + /// single `send` and orchestrate concurrency themselves on the three + /// reactor-bearing adapters: join N complete per-request async tasks, each + /// awaiting `send` and immediately consuming its response through the + /// **app-facing consuming accessor `into_body -> Body`** before that task + /// completes. Do not join header-only sends and then start body consumption: + /// that recreates the same fast-response deadline hazard. Each task drives + /// its body while siblings may still be waiting for headers. + /// `into_parts(..)` exists too but is labelled adapter-facing because it + /// returns the (request method, status, headers, body) tuple that response converters + /// need; pure orchestration paths just want the body. This rule keeps + /// `outbound-batch-slot-isolation`'s `Native` claim on Axum/CF/Spin honest — + /// the cross-slot body-lifetime problem is removed by construction rather + /// than papered over. + /// + /// **"Identical" scope.** The trait contract guarantees identical + /// **input handling**: same preflight, same index alignment, same + /// per-slot Ok/Err shape. The *cross-slot timing behaviour* is **not** + /// uniform — see the `outbound-batch-slot-isolation` capability. + /// On Axum/CF/Spin the private completion driver fans out complete exchanges and a + /// slot's result reflects what it would have produced in isolation. + /// On Fastly, cold backend registration can delay later dispatch. A synchronously drained + /// selected response body can delay later result observation; a background request upload + /// leaves its own slot unresolved but does not block later `send_async` calls once the earlier + /// call has returned. A later-observed slot can therefore return `gateway_timeout` even + /// when its own `budget.deadline` would have covered it in isolation. Apps that require cross-slot + /// isolation declare the capability required and get a hard build + /// failure on Fastly. + /// + /// Per-slot `Ok`/`Err` semantics: since preflight rejects streamed bodies AND + /// streamed responses, every surviving slot is Buffered on both sides, so the + /// per-slot outcome shape matches `send`'s **Buffered-mode** semantics — `Ok(resp)` + /// means the full exchange completed within the deadline and the body fits + /// within `max_response_bytes`; `Err(_)` is transport / deadline / over-cap. + /// Streamed-mode `Ok`-means-headers-only does not apply here because there are + /// no streamed slots. + fn start_batch_until( + &self, + reqs: Vec, + observation_cutoff: Deadline, + ) -> OutboundBatch; +} +``` + +Both `send` and `start_batch_until` are required on the trait. Each adapter implements both and +shares its single-request preparation and buffered response processing with its batch driver. +`next()` yields every observed terminal item at most once and then returns +`Finished(Completed)` or `Finished(Cutoff)`. If its future is dropped before it returns, no item is +consumed. The observation cutoff wins equality; once the adapter clock is at or past that cutoff, +no newly observed success is yielded. Adapter drivers signal cutoff termination explicitly. Stream EOF is +valid only after all input indices have emitted; premature driver EOF, a duplicate or out-of-range +index, or polling after an invariant failure returns `EdgeError::Internal` rather than fabricating +timeout-like unresolved slots. Dropping the batch is equivalent to `cancel()` with the returned +unresolved-index report discarded. Cancellation stops observation and triggers +strongest-available teardown, but does not claim that already-issued network side effects were +undone. + +Core reports normal completion only at driver EOF after every index has resolved. Trailing +`Failed`, duplicate or out-of-range index events remain failures even after the last slot item; +ordered collection retains every result already observed. A driver cutoff with no unresolved +slots is an invariant failure. Zero-slot drivers follow the same validation protocol; +`OutboundBatch::cutoff(0)` supplies an empty driver and completes normally. Custom drivers must +finish after their final item rather than leave finalization pending indefinitely. + +#### 3.1.2 App-facing handle + +```rust +/// Cloneable handle stored in request extensions and handed to handlers. +/// This is the only outbound *client/handle* type application code touches; +/// handlers also build `OutboundRequest` and read `OutboundResponse`. +#[derive(Clone)] +pub struct HttpClient { + inner: Arc, +} + +impl HttpClient { + pub fn new(client: Arc) -> Self; + pub async fn send(&self, req: OutboundRequest) -> Result; + pub fn start_batch_until( + &self, + reqs: Vec, + observation_cutoff: Deadline, + ) -> OutboundBatch; + pub async fn send_all_until( + &self, + reqs: Vec, + observation_cutoff: Deadline, + ) -> Result; + pub fn with_client(client: C) -> Self; +} +``` + +`HttpClient::send_all_until` is exactly +`self.start_batch_until(reqs, observation_cutoff).collect().await`; it is not a second adapter operation. +On success, its `slots` vector always has the input length. A slot rejected during preflight is +`Some(OutboundSlotResult { outcome: Err(..), .. })`. `None` is legal only when `termination` is +`OutboundBatchTermination::Cutoff` and means that the slot had not become terminal before the +observation cutoff. `Completed` guarantees every slot is `Some`. An explicit adapter failure, +premature driver EOF, or invalid index is not a successful collection and is never `Cutoff`. +`OutboundBatchFailure.error` retains the precise `EdgeError`, while `slots` retains every terminal +result collected before the failure. Converting `OutboundBatchFailure` into `EdgeError` explicitly +discards those partial slots. The hard cut retains no deadline-free `send_all` alias. + +Obtained from the context: + +```rust +// crates/edgezero-core/src/context.rs — replaces proxy_handle +// The accessor is valid both before and after the inbound-body restructuring: as an +// inherent method it reads `self.request.extensions()` directly and does not require a +// new public `RequestContext::extensions()` API. +impl RequestContext { + pub fn http_client(&self) -> Option { + self.request.extensions().get::().cloned() + } +} +``` + +#### 3.1.3 Request and response types + +```rust +pub struct OutboundRequest { + body: Body, // buffered or streamed + cache_policy: OutboundCachePolicy, // PlatformDefault (default) | Bypass + deadline: Option, // shared absolute cap; copy one value into every target request, do not recompute per request (see §3.3.2) + headers: HeaderMap, + host_authority_override: Option, // validated wire authority only + max_decoder_bytes: u64, // source-audited decoder-state charge cap; default 32 MiB + max_brotli_window_bits: u8, // advertised-window cap; default 24, valid 10..=30 + max_chunk_bytes: Option, // opt-in Streamed output rechunker + max_decoded_response_bytes: Option, // identity/EdgeZero-decoded output only + max_encoded_response_bytes: Option, // pre-decode guest-visible body cap + max_request_body_bytes: u64, // cap for buffered or streamed body bytes (default 8 MiB) + max_response_header_bytes: Option, // adapter-visible upstream names + values before normalization + max_response_header_count: Option, // adapter-visible upstream entries before normalization + method: Method, + response_mode: ResponseMode, // Buffered { max_bytes } (default) | Streamed + timeout: Option, // per-request budget + uri: Uri, // validated + canonicalized; see below +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum OutboundCachePolicy { + #[default] + PlatformDefault, + Bypass, +} + +// **All OUTBOUND public byte caps and byte-accounting counters are `u64`, NOT `usize`.** +// This invariant is intentionally scoped to the outbound APIs introduced or changed here. +// The +// crate compiles to `wasm32` on three of the four adapters, where `usize` is **32-bit**: +// a cap or a wire `Content-Length` above 4 GiB is not merely wrap-prone, it is +// **unrepresentable** as `usize`, which would silently break the "portable across all +// four adapters" claim on exactly the targets that matter. `u64` gives one ceiling +// (16 EiB) on every target. The buffered `Bytes` a drain produces is still `usize`-length +// (bounded by available guest memory), but the *cap* and the *running total* it is +// compared against are `u64`, so the comparison and the arithmetic cannot wrap or +// truncate. `Content-Length` is parsed as `u64`; for an **effective identity** response +// (no `content-encoding`, or exactly one bare `identity` value) it equals the decompressed +// size, so it can be compared against the +// `u64` decoded cap for an early over-cap reject BEFORE buffering. For a **compressed** +// response the wire `Content-Length` is the compressed size and does NOT bound the decoded +// size, so it cannot early-reject against either the decoded-output or final-buffer cap. When the optional +// `max_encoded_response_bytes` cap is set, that same length can reject against the encoded +// cap before the body read (§3.4.5). Conversions use `u64::from` / `TryFrom`, never `as` +// (denied lint). + +/// How the adapter delivers the response body. Default is `Buffered`. +pub enum ResponseMode { + /// Adapter reads the full delivered body within the deadline, enforcing a final + /// collection/allocation cap regardless of whether the bytes are identity, decoded, or + /// raw passthrough. `OutboundResponse.body` is `Body::Once`. This is independent of + /// `max_decoded_response_bytes`: choose a larger `max_bytes` to permit a large raw + /// passthrough while retaining a smaller decoded-output cap. + Buffered { max_bytes: u64 }, // default max_bytes = DEFAULT_MAX_RESPONSE_BYTES + /// Adapter returns headers; `OutboundResponse.body` is `Body::Stream`. The + /// caller buffers later (e.g. `into_bytes_bounded`) or passes the body through. + Streamed, +} + +impl OutboundRequest { + /// Constructors validate **and canonicalize** the URI once, before app policy or + /// adapter conversion. The canonical serialized URI is the target identity that + /// `uri()` exposes and every adapter sends: + /// + /// - Scheme must be `http` or `https` (plain `http` is permitted — + /// required for loopback contract tests). Other schemes → + /// `Err(EdgeError::bad_request("outbound URI scheme must be http or + /// https"))`. + /// - An authority must be present. Missing authority → + /// `Err(EdgeError::bad_request("outbound URI must be absolute with + /// authority"))`. + /// - **Userinfo is rejected.** `https://user:pass@example.com` → + /// `Err(EdgeError::bad_request("outbound URI must not contain + /// userinfo; pass credentials via the `authorization` header"))`. + /// This keeps the Fastly backend Host override unambiguous and + /// stops accidental credential leakage. Before WHATWG parsing, every constructor inspects + /// the serialized URI authority and rejects a literal `@`, including empty userinfo such as + /// `https://@example.com/`; this applies equally to `new`, `from_parts`, `get`, and `post`. + /// String constructors additionally reject every raw `\\` byte. Backslash is a special-URL separator that WHATWG parsing can + /// normalize into `/`; rejecting it avoids a second, security-sensitive authority parser + /// and prevents forms such as `https:\\@example.com/`, `https:/\\@example.com/`, and + /// `https:\\/\\@example.com/` from hiding empty userinfo during normalization. The + /// constructor then locates the raw authority only after the literal `://` delimiter and + /// before the next `/`, `?`, or `#`, and rejects any literal `@`, including + /// `https://@example.com/`. A percent-encoded `%40` in path/query data is not an authority + /// delimiter and remains allowed. Tests pin all four slash/backslash separator shapes and + /// verify rejection occurs before `url::Url` is invoked. + /// - **Fragments are rejected at the string-input boundary.** + /// `OutboundRequest::get("https://x/p#anchor")` and `::post(..)` parse + /// the input as a string *first* (they take `impl AsRef` — see + /// below) and reject a `#` before `http::Uri` ever sees it, with + /// `Err(EdgeError::bad_request("outbound URI must not contain a + /// fragment"))`. A Uri-typed input has already passed through + /// `http::Uri`; `new` and `from_parts` canonicalize its serialized value but cannot + /// recover syntax the caller's parser discarded. Raw strings should therefore use + /// `get`/`post`. + /// - Canonicalization uses the pinned WHATWG `url::Url` parser, then converts + /// `Url::as_str()` to `http::Uri`. This adds `url` as a direct workspace/core + /// dependency in Phase 1b; adapters do not reparse or rebuild the URI. It normalizes + /// scheme/host case, IDNA and numeric host spellings, default ports, and dot segments, + /// and applies the pinned parser's path/query percent-encoding rules before an app + /// allowlist sees the target. + /// - **Default ports are normalized away.** A URL parsed from + /// `https://example.com:443` is rewritten so `uri.port` returns + /// `None`; `http://example.com:80` likewise. This means + /// `https://example.com` and `https://example.com:443` produce + /// identical `OutboundRequest`s — same `resolved_port` in the + /// Fastly identity, same Host override, one dynamic backend. Explicit + /// non-default ports (`:8443`, `:3000`) are preserved verbatim. + /// - **Scheme and host are lowercased.** `https://EXAMPLE.com`, + /// `HTTPS://example.com`, and `https://example.com` are the same + /// origin. The canonicalization rewrites the stored URI to lowercase + /// so `OutboundRequest::uri` always reports the lowercase form, + /// and downstream consumers (including Fastly backend identity in §4.3, + /// app-level allowlist checks, Spin `allowed_outbound_hosts` + /// matching) compare against one canonical spelling. Userinfo and fragments are + /// rejected above. Path and query are not promised verbatim: WHATWG serialization may + /// remove dot segments or normalize percent-encoding. The canonical serialized result, + /// not the caller's original spelling, is the security and wire identity. + /// + /// Every adapter passes this stored serialization to its SDK without independently + /// joining path/query components. Tier 2 and live-host tests include dot segments, + /// percent-encoded delimiters, numeric IPv4 aliases, IDNA, empty paths, and query + /// characters; an adapter/runtime that changes the effective target after this boundary + /// fails the portable URI contract rather than silently weakening app policy. + pub fn new(method: Method, uri: Uri) -> Result; + /// `get` and `post` take `impl AsRef` (not `TryInto`) so the raw + /// string is available for fragment detection *before* `http::Uri` + /// truncates at `#`. The impl checks for `#` in the input bytes, then + /// parses with `url::Url`, converts the canonical serialization to `Uri`, then runs + /// the remaining + /// canonicalization. `&str`, `String`, and any `AsRef` work; an + /// already-built `Uri` goes through `OutboundRequest::new` (which cannot + /// recover fragments because the `Uri` has already lost them — see + /// "Fragments are rejected at the string-input boundary" above). + pub fn get(uri: impl AsRef) -> Result; + pub fn post(uri: impl AsRef) -> Result; + + /// Forward an inbound request to a new target. Preserves method and body + /// (which may stream). Headers are normalized for proxy forwarding — + /// the rules live in core so adapters cannot diverge: + /// + /// - hop-by-hop headers are stripped: `connection`, `keep-alive`, + /// `proxy-connection`, `proxy-authenticate`, `proxy-authorization`, `te`, `trailer`, + /// `transfer-encoding`, `upgrade` (RFC 7230), plus every header + /// named in the inbound `connection` header value; + /// - `host` is **dropped** from the headers. The only supported way to select a wire + /// authority is `host_authority_override(..)` below; otherwise adapters derive it from + /// the canonical URI. The accessor encodes the rules: explicit port preserved when the URI + /// carries a non-default port (`https://example.com:8443` → + /// `Host: example.com:8443`); port stripped when default + /// (`https://example.com` → `Host: example.com`); IPv6 hosts + /// bracketed. **Adapters MUST use `host_authority()` for the outgoing authority**; no part + /// of the pipeline reads + /// `host` from `req.headers`. `normalize_for_dispatch` re-strips + /// `host` defensively as a safety net for callers that reached past + /// `header(..)` via `headers_mut`; + /// - `content-length` is dropped — the adapter sets it from the new body + /// for `Body::Once`, or omits it (relying on chunked transfer) for + /// `Body::Stream`. + /// + /// All other headers are preserved verbatim. Validates `uri` per `new`. + pub fn from_request(request: Request, uri: Uri) -> Result; + + /// Fallible: header name/value construction from arbitrary inputs can + /// fail. The signature takes `impl AsRef<[u8]>` for both name and value + /// — **not** `TryInto` / `TryInto`. The standard + /// `TryFrom<&str> for HeaderValue` path is built on + /// `HeaderValue::from_str`, which rejects every byte outside visible + /// ASCII and would refuse a valid non-ASCII UTF-8 header + /// (`x-app-display-name: café`) before EdgeZero's own UTF-8 rule could + /// run. By taking bytes directly: + /// + /// 1. `HeaderName::from_bytes(name.as_ref)` — strict name check (HTTP + /// grammar). + /// 2. `std::str::from_utf8(value.as_ref).is_err` → reject with + /// `EdgeError::bad_request("header value is not valid UTF-8: ")` + /// (the EdgeZero rule). + /// 3. `HeaderValue::from_bytes(value.as_ref)` — applies the **HTTP + /// header-value byte rule** (visible ASCII + obs-text; rejects + /// control bytes like `\n`, `\0` that would enable header injection). + /// Combined with step 2, the values that survive are exactly the ones + /// that are **both** valid UTF-8 **and** valid HTTP header bytes — a + /// valid-UTF-8 string containing a forbidden control byte is still + /// rejected, which is intended security behaviour. Two distinct error + /// messages distinguish the cause (forbidden-bytes vs invalid-UTF-8). + /// + /// Works for `&str`, `String`, `&[u8]`, `Vec`, and `HeaderName` / + /// `HeaderValue` (both `AsRef<[u8]>`). + pub fn header(self, name: N, value: V) -> Result + where + N: AsRef<[u8]>, + V: AsRef<[u8]>; + /// Escape hatch for callers holding already-validated + /// `HeaderName`/`HeaderValue` (or building from `from_request`). The + /// returned `HeaderMap` is not validated here — non-UTF-8 values and + /// stray hop-by-hop / framing headers (`host`, `content-length`, + /// `transfer-encoding`) are caught by the adapter's + /// `normalize_for_dispatch` sweep before the request is issued. + pub fn headers_mut(&mut self) -> &mut HeaderMap; + + /// Set the body from `Body` or a buffered conversion. `Bytes`, `Vec`, + /// `&[u8]`, `&str`, and `String` convert to `Body::Once`. A raw `Stream` + /// does not implement `Into`; callers wrap typed streams with + /// `Body::from_stream` and arbitrary external streams with + /// `Body::from_external_stream`. + pub fn body(self, body: impl Into) -> Self; + /// Serialize `value` as JSON and set the request body to the resulting + /// bytes. Sets `content-type: application/json` only if the request has + /// no `content-type` yet — a caller-set value is preserved unchanged. + /// `content-length` is left to the adapter (it is recomputed from the + /// serialized body for `Body::Once` and omitted for `Body::Stream`). + /// Serialization failure yields `Err(EdgeError::internal(..))`. + pub fn json(self, value: &T) -> Result; + + pub fn timeout(self, duration: Duration) -> Self; + pub fn deadline(self, deadline: Deadline) -> Self; + pub fn cache_policy(self, policy: OutboundCachePolicy) -> Self; + /// Sets a validated DNS name, IPv4 address, or bracketed IPv6 literal with an optional + /// port. Schemes, userinfo, paths, queries, fragments, whitespace, controls, malformed + /// brackets, and invalid ports are rejected as `bad_request`. + pub fn host_authority_override(self, authority: &str) -> Result; + pub fn max_decoder_bytes(self, max: u64) -> Self; + pub fn max_brotli_window_bits(self, bits: u8) -> Self; // 10..=30 at dispatch + pub fn max_chunk_bytes(self, max: NonZeroU64) -> Self; // Streamed emitted-item size + pub fn max_decoded_response_bytes(self, max: u64) -> Self; + pub fn max_encoded_response_bytes(self, max: u64) -> Self; + pub fn max_response_header_bytes(self, max: u64) -> Self; + pub fn max_response_header_count(self, max: u64) -> Self; + pub fn max_response_bytes(self, max: u64) -> Self; // selects Buffered { max }; last mode setter wins + pub fn stream_response(self) -> Self; // selects Streamed; last mode setter wins + + // These two methods select mutually exclusive `ResponseMode` variants. They do not compose: + // `max_response_bytes(..).stream_response()` is an uncapped streamed response, while + // `stream_response().max_response_bytes(..)` is a buffered response with the supplied cap. + + // Zero is an intentional deny-all value for every `u64` cap. It is not a malformed + // configuration: an empty body can satisfy a zero body cap, while the first visible byte or + // header entry fails with the cap's typed reason. `max_chunk_bytes` alone requires + // `NonZeroU64` because a zero-sized output item cannot make stream progress. + + /// Cap on the **request** body for both `Body::Once` and `Body::Stream`. + /// EdgeZero's core `Body::Stream` is `LocalBoxStream` + /// (WASM-friendly, not `Send + 'static`), so adapters cannot hand it + /// directly to a SDK that requires `Send` streams (notably reqwest + /// without its `stream` feature). The contract is therefore: every request + /// body is **bounded** by this cap on every adapter; adapters + /// MAY pass the stream through to the platform natively (Fastly's + /// `send_async_streaming`, Spin's WASI outgoing body) or buffer to + /// `Bytes` within the cap before dispatch (Axum, Cloudflare). Over-cap + /// during drain → `PayloadTooLarge { origin: OutboundUpload }` (413). + /// Buffered over-cap bodies fail in shared preflight before any provider dispatch. + /// Default `DEFAULT_OUTBOUND_REQUEST_BODY_BYTES = 8 MiB`. + pub fn max_request_body_bytes(self, max: u64) -> Self; + + pub fn method(&self) -> &Method; + pub fn uri(&self) -> &Uri; // canonical target apps inspect for their allowlist + pub fn headers(&self) -> &HeaderMap; + + // ---- Canonicalized URI accessors (adapter-facing, non-consuming) ---- + // + // These five accessors are the **single canonical source** of the + // host/port/SNI/cert-host split that every adapter needs. They are + // derived from `self.uri` after the canonicalization rules + // have rejected **userinfo and fragments**, validated the port, and applied the pinned + // WHATWG canonicalization. Path and query do not + // appear in these accessors because none of them are host/port/SNI/cert + // values, but they remain accessible via `self.uri` for the wire-level + // request line. **Adapters MUST consume these accessors rather than + // re-deriving from `uri`** for the host/port/SNI/cert split — both to + // share the canonicalization logic and so the Fastly identity hash + // sees a single canonical form. They are also the values + // tested by the Tier 1 half of the five-value row. + // + // **Manifest `[capabilities.outbound].hosts` entries are a separate + // grammar** — those entries are host-authority-only + // declarations, so the manifest-host validator **rejects** path / query + // / fragment / userinfo on the manifest side. That validator and the + // request-URI canonicalization rules above share the userinfo / fragment + // reject and the lowercase-scheme/host pass, but diverge on path/query: + // request URIs canonicalize them; manifest host entries reject them. The + // two rule sets must not be conflated. + + /// Connection target — always `":"`, with the port resolved + /// (default ports filled in: `http` → 80, `https` → 443). IPv6 hosts + /// are bracketed (`[::1]:443`). This is what Fastly's + /// `Backend::builder(name..)` expects and what Spin uses for its + /// `allowed_outbound_hosts` rendering when the source had no explicit + /// port. Stable across canonicalization (same value whether the input + /// was `https://example.com` or `https://example.com:443`). + pub fn backend_target(&self) -> String; + + /// Authority for the outgoing HTTP `Host`/`:authority`. Returns the validated explicit + /// override when present; otherwise derives from the canonical URI and carries the port + /// **only when it is non-default** for the scheme: + /// `https://example.com:8443` → `"example.com:8443"`; + /// `https://example.com` → `"example.com"`. IPv6 hosts are bracketed. + /// This is what Fastly's `.override_host(..)`, Axum's `Host`, and Cloudflare's `Host` + /// conversion consume. Spin rejects an explicit override before body polling because WASI + /// HTTP cannot preserve a distinct URI connection authority and wire authority. + pub fn host_authority(&self) -> &str; + + /// Canonical host only, with no port and no IPv6 brackets. Unlike + /// `sni_hostname`, this returns IP literals too. Fastly uses this value in + /// `BackendIdentity`; adapters MUST NOT recover it by reparsing `uri` or + /// splitting `backend_target`. + pub fn host_name(&self) -> &str; + + /// SNI hostname — what an HTTPS adapter passes to its TLS stack's + /// SNI setter (Fastly's `.sni_hostname(..)`, Spin/CF's underlying + /// TLS config, etc.). Port-stripped, bracket-stripped for IPv6. + /// **Returns `None` for IP-literal hosts** (IPv4 and IPv6) + /// RFC 6066, which forbids SNI for IP literals. Adapters call + /// the TLS-stack SNI setter only when this returns `Some`; for `None` + /// the SNI extension is omitted from the ClientHello. **Adapters + /// MUST NOT fall back to `uri.host` for SNI** — `None` here + /// means "send no SNI," not "derive it yourself." The cert verification + /// host is `cert_host` below, not this accessor. + pub fn sni_hostname(&self) -> Option<&str>; + + /// Certificate-verification host — what an HTTPS adapter passes to + /// its TLS stack's certificate-verification setter (Fastly's + /// `.check_certificate(..)`, Spin/CF's underlying TLS verifier). + /// **Always present for HTTPS, always port-stripped, always + /// bracket-stripped for IPv6.** Unlike SNI, certificate verification + /// is meaningful for IP literals too — verification will check the + /// presented certificate's SAN against the IP literal (e.g. `127.0.0.1`, + /// `::1`). Returns `None` only for non-HTTPS schemes (i.e. `http`), + /// where the accessor is not used by the adapter. **This is the + /// single canonical source for `.check_certificate(..)` arguments + /// across every adapter**; adapters MUST NOT call `uri.host` and + /// post-process — they call `cert_host` and pass it through. + /// + /// Concrete examples: + /// - `https://example.com` / `https://example.com:443` → `Some("example.com")` + /// - `https://example.com:8443` → `Some("example.com")` (port stripped — cert is not port-qualified) + /// - `https://127.0.0.1` → `Some("127.0.0.1")` + /// - `https://[::1]` / `https://[::1]:443` → `Some("::1")` (brackets stripped) + /// - `http://example.com` → `None` + pub fn cert_host(&self) -> Option<&str>; + + // ---- Adapter-facing inspection (non-consuming) ---- + /// Cheap non-consuming check used by batch preflight: if `true`, + /// the slot is rejected with `bad_request` + /// *before* `send_one` is invoked, so the streamed-upload path is never + /// reached from `start_batch_until`. `send` (single-request) handles `Body::Stream` + /// directly per its trait contract. + pub fn is_stream_body(&self) -> bool; + + /// Cheap non-consuming check used by batch preflight: if `true` + /// (i.e. `response_mode == Streamed`), the slot is rejected with + /// `bad_request` before `send_one` is invoked. `send` (single-request) + /// handles streamed responses directly. + pub fn is_stream_response(&self) -> bool; + + // ---- Adapter-facing disassembly / reassembly ---- + /// Consume the request into its constituent parts. Adapters call this + /// inside `send` / `start_batch_until` after `normalize_for_dispatch` has run, + /// to hand the components to the platform SDK. + pub fn into_parts(self) -> OutboundRequestParts; + /// Round-trip constructor for adapters that need to destructure, mutate + /// a single field, and reassemble (rare — most adapter paths consume). + /// All fields are pub on `OutboundRequestParts`, so this is just a + /// disciplined re-wrap and applies the same invariants as + /// `new`/`get`/`post` (URI validation re-runs). + pub fn from_parts(parts: OutboundRequestParts) -> Result; +} + +/// The one shared stream shape used by `Body` and every transport-independent outbound +/// wrapper. Returning this concrete erased type lets raw, gzip, Brotli, limit, and +/// rechunk branches compose without incompatible opaque `impl Stream` return types. +pub type BodyStream = LocalBoxStream<'static, Result>; + +/// The public streamed-body error surface is exact: adapters, decoders, and +/// other EdgeZero-owned producers can carry typed 502/504/over-cap failures +/// without converting them through `anyhow::Error`. +pub enum Body { + Once(Bytes), + Stream(BodyStream), +} + +// Public ownership paths are exact and stable: +// - `edgezero_core::body::BodyStream` and the root re-export +// `edgezero_core::BodyStream` own the shared body stream type. +// - `edgezero_core::compression::{BROTLI_DECODER_FIXED_CHARGE_BYTES, ContentEncoding, +// brotli_decoder_memory_charge, classify_content_encoding, decode_brotli_stream, +// decode_gzip_stream}` own codec policy. +// - `edgezero_core::outbound::{ResponseHeaderLimiter, collect_response_stream, +// enforce_payload_content_length, limit_decoded_stream, limit_encoded_stream, +// normalize_response_headers, rechunk_stream}` own outbound response policy. +// Every listed item is also re-exported from `edgezero_core`; adapter crates import the +// root path and do not depend on private module layout. + +impl Body { + /// Explicit compatibility boundary for arbitrary external stream errors. + /// Every source error is converted to `anyhow::Error` and then wrapped as + /// `EdgeError::internal`; this constructor never attempts to recover a typed + /// `EdgeError` hidden inside the external error. + pub fn from_external_stream(stream: S) -> Self + where + S: Stream> + 'static, + anyhow::Error: From; + + /// Construct a fallible stream whose errors are already classified. This is + /// the required constructor for every in-tree adapter, decoder, and deadline + /// wrapper that can emit an `EdgeError`. + pub fn from_stream(stream: S) -> Self + where + S: Stream> + 'static; + + /// Drain a buffered or streamed body with pre-append checked accounting. A typed + /// `EdgeError` yielded by `Body::Stream` is returned unchanged; it is never wrapped as + /// `internal`. Foreign/ingress errors have already been sanitized by + /// `from_external_stream` before entering this method, so preserving the item here does + /// not expose an unclassified external error. + pub async fn into_bytes_bounded(self, max_size: usize) -> Result; + + /// Returns the exact typed stream for `Body::Stream`, or `None` for + /// `Body::Once`. + pub fn into_stream(self) -> Option; + + /// Construct an infallible byte stream. + pub fn stream(stream: S) -> Self + where + S: Stream + 'static; +} + +impl From for Body { + fn from(value: Bytes) -> Self { Body::Once(value) } +} +// The existing `From>`, `From<&[u8]>`, `From<&str>`, and +// `From` implementations remain buffered conversions. + +// The separate constructors are intentional. Stable Rust cannot provide one +// generic `from_stream` that preserves `E = EdgeError` but maps every +// other `E` to `internal` without overlapping implementations/specialization. +// In-tree code MUST NOT pass an `EdgeError` stream through +// `from_external_stream`, because doing so would erase its status and kind. +// Conversely, platform inbound bodies and every other foreign producer MUST classify or +// sanitize its error before constructing `Body`: use `from_external_stream` when the +// existing behavior is an opaque internal failure, or map deliberately to a typed +// `EdgeError` before `from_stream` when that boundary owns a public classification. Tests +// cover exact propagation through `into_bytes_bounded` and sanitization of a foreign error. + +// Shared transport-independent response pipeline. These helpers are public because +// adapters are separate crates. Every branch accepts and returns the same `BodyStream`, +// preserving exact typed source errors and making the pipeline skeleton buildable. +// All four adapters compose payload policy through `edgezero_core::outbound`: +#[derive(Clone, Copy, Debug)] +pub struct ResponseBodyPolicy { + pub max_brotli_window_bits: u8, + pub max_chunk_bytes: Option, + pub max_decoded_response_bytes: Option, + pub max_decoder_bytes: u64, + pub max_encoded_response_bytes: Option, + pub response_mode: ResponseMode, +} +pub fn apply_response_body_policy( + native: BodyStream, + headers: &mut HeaderMap, + policy: ResponseBodyPolicy, +) -> Result; +// Checks declared lengths without polling, then composes encoded limits, decoding and header +// rewriting, decoded limits (not passthrough), and rechunking, in that order. Adapter-specific +// bodyless handling, transport reads, timers, cancellation, and final collection stay outside. +// `edgezero_core::compression` items, in module order: +pub const BROTLI_DECODER_FIXED_CHARGE_BYTES: u64 = 16_777_216; +pub const FLATE_DECODER_FIXED_CHARGE_BYTES: u64 = 1_048_576; // 1 MiB; see the flate memory audit +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ContentEncoding { + /// One bare, case-insensitive `br` field. + Brotli, + /// One bare, case-insensitive `deflate` field. The body uses RFC 1950 zlib framing. + Deflate, + /// One bare, case-insensitive `gzip` field. + Gzip, + /// No visible `content-encoding`, or one bare `identity` field. + Identity, + /// A coding EdgeZero deliberately preserves as raw bytes. + Passthrough(PassthroughReason), +} +#[non_exhaustive] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PassthroughReason { + /// Invalid token syntax, empty elements, parameters, or non-UTF-8 bytes. + Malformed, + /// More than one field, or one valid comma-separated coding list. + Stacked, + /// One valid but unsupported coding token, normalized to ASCII lowercase. + Unsupported(String), +} +// A direct call with bits outside 10..=30 is invalid caller policy and returns BadRequest; +// checked accounting overflow retains ResponseLimitReason::DecoderMemory. +pub fn brotli_decoder_memory_charge(window_bits: u8) -> Result; +pub fn classify_content_encoding(headers: &HeaderMap) -> ContentEncoding; +/// The returned state machine reads and validates the fixed-size stream prefix before it +/// constructs the decoder, so polling it inside the adapter wrapper keeps prefix reads, +/// decoder allocation, and all output under one absolute deadline/cancellation owner. +pub fn decode_brotli_stream( + stream: BodyStream, + max_window_bits: u8, + max_decoder_bytes: u64, +) -> BodyStream; +pub fn decode_deflate_stream(stream: BodyStream, max_decoder_bytes: u64) -> BodyStream; +pub fn decode_gzip_stream(stream: BodyStream, max_decoder_bytes: u64) -> BodyStream; + +// `edgezero_core::outbound` items, in module order: +pub fn finish_batch_item( + index: usize, + started_at: MonotonicInstant, + completed_at: MonotonicInstant, + observation_cutoff: Deadline, + outcome: Result, +) -> Option; +pub async fn collect_response_stream(stream: BodyStream, max: u64) -> Result; +pub fn limit_decoded_stream(stream: BodyStream, max: Option) -> BodyStream; +pub fn limit_encoded_stream(stream: BodyStream, max: Option) -> BodyStream; +pub fn rechunk_stream(stream: BodyStream, max: Option) -> BodyStream; + +/// Cumulative adapter-visible upstream field-section accounting retained from the first +/// exposed informational block through final headers and exposed trailers, before EdgeZero +/// normalization, plus synthetic fields inserted by EdgeZero after normalization. +pub struct ResponseHeaderLimiter { /* private checked-u64 counters and limits */ } +impl ResponseHeaderLimiter { + pub fn new(max_bytes: Option, max_count: Option) -> Self; + pub fn observe(&mut self, headers: &HeaderMap) -> Result<(), EdgeError>; +} + +/// Disassembled form of an `OutboundRequest`. Adapter-facing only. +#[non_exhaustive] +pub struct OutboundRequestParts { + pub body: Body, + pub cache_policy: OutboundCachePolicy, + pub deadline: Option, + pub headers: HeaderMap, + pub host_authority_override: Option, + pub max_decoder_bytes: u64, + pub max_brotli_window_bits: u8, + pub max_chunk_bytes: Option, + pub max_decoded_response_bytes: Option, + pub max_encoded_response_bytes: Option, + pub max_request_body_bytes: u64, // applies to Body::Once and Body::Stream (u64 — see cap note) + pub max_response_header_bytes: Option, + pub max_response_header_count: Option, + pub method: Method, + pub response_mode: ResponseMode, + pub timeout: Option, + pub uri: Uri, +} + +// `into_parts` and `from_parts` move every field above without defaulting or dropping +// one. In particular, adapter request conversion must retain the response-resource +// settings until response metadata/body processing. Adapters that consume the request at +// dispatch copy the settings they need after the native send into their pending-response +// state; they do not reconstruct them from `ResponseMode`. + +**Cache policy.** `PlatformDefault` leaves the adapter/provider cache behavior unchanged. +`Bypass` means do not satisfy the request from, store its response in, or revalidate through an +adapter-managed intermediary response cache. It does not synthesize `cache-control` or any other +origin-visible request header. Fastly maps it to request pass; Cloudflare maps it to Fetch +`NoStore`; Axum and Spin have no adapter-managed intermediary response cache, so the requested +semantics are already satisfied without transport mutation. Redirect hops are new requests and +must copy the policy deliberately. + +**Authority separation.** The canonical URI remains the only source for DNS/backend target, TLS +SNI, and certificate verification. `host_authority_override` controls only HTTP `Host` or +`:authority` and is included in any adapter transport cache/backend identity that would otherwise +reuse a conflicting wire authority. Axum and Fastly provide Native separation. Cloudflare is +BestEffort until a deployed, host-observed probe proves Fetch retained the requested `Host` under +the pinned compatibility date and SDK. Spin returns `bad_request` before body polling because +WASI HTTP exposes one authority field for both routing and HTTP authority; it must never silently +retarget the connection. + +/// Body policy from the authoritative raw-response metadata pass. Adapters must act on +/// `ResponseHeadNormalization::body_disposition` before inspecting content encoding/length +/// or constructing `OutboundResponse`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResponseBodyDisposition { + FramingBodyless, // HEAD, 1xx, 204, or 304: settle/drop the platform handle; never decode + Payload, // normal payload path: inspect encoding, decode, and enforce the cap + ResetContent { + // Captured before the helper rewrites the downstream `content-length` to zero. + declared_body: bool, + }, // 205: apply the bounded settle/abort protocol in §3.4.1 +} + +/// Why original Content-Type evidence cannot certify a retained textual singleton. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ContentTypeIneligibility { + Missing, + Modified, + MultipleValues, + NonText, + Removed, +} + +/// Original field multiplicity/UTF-8 evidence, not MIME validation or debug safety. +/// No trimming, comma splitting, raw-header retention, or body copying. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ContentTypeProvenance { + Ineligible(ContentTypeIneligibility), + SingleTextUnchanged, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResponseHeadNormalization { + pub body_disposition: ResponseBodyDisposition, + pub content_type_provenance: ContentTypeProvenance, +} + +/// Parse the already-normalized payload-bearing `content-length` and apply every +/// sound pre-poll size decision. Adapters call this only for `Payload`: bodyless and +/// 205 settlement is owned by `ResponseBodyDisposition`. `max_buffered_bytes` is `Some` +/// only for Buffered mode; the independent decoded cap may also be present in Streamed. +/// Malformed, comma-list, or conflicting lengths are protocol 502. The encoded cap applies +/// to every coding. Effective identity compares decoded and Buffered caps too; raw +/// passthrough compares the Buffered cap but never the decoded cap; gzip/deflate/Brotli-to-decode +/// cannot soundly compare wire length with either output cap. +pub fn enforce_payload_content_length( + headers: &HeaderMap, + encoding: &ContentEncoding, + max_buffered_bytes: Option, + max_decoded_bytes: Option, + max_encoded_bytes: Option, +) -> Result<(), EdgeError>; + +pub fn normalize_response_headers( + request_method: &Method, + status: StatusCode, + headers: &mut HeaderMap, +) -> Result; + +pub struct OutboundResponse { + body: Body, // Once in Buffered mode, Stream in Streamed mode + content_type_provenance: ContentTypeProvenance, // immutable original evidence + headers: HeaderMap, + headers_mutated: bool, // separate conservative invalidation flag + monotonic_clock: MonotonicClock, // paired app/client clock for bounded-until helpers + request_method: Method, // retained for HEAD/bodyless response semantics + status: StatusCode, +} + +impl OutboundResponse { + /// Adapter-facing post-conversion constructor. Before calling this, the adapter MUST run + /// `normalize_response_headers` on the raw response metadata, settle any bodyless response, + /// and only then inspect `content-encoding` / `content-length`, decode, and cap. The headers + /// supplied here are therefore already safe for app-facing `headers()` / `into_body()`: + /// hop-by-hop fields and every `connection` nomination are gone; decompression has removed + /// `content-encoding` / `content-length`; and non-UTF-8 header filtering has run. `Body::Once` is + /// used in Buffered mode after the adapter has drained and capped; `Body::Stream` is wrapped + /// with the decoded-output deadline guard in Streamed mode. + /// The caller must provide the outbound client's retained application clock. There is no + /// process-default fallback because deferred body operations must remain in that clock domain. + /// `content_type_provenance` is mandatory pre-filter evidence from the original normalization + /// pass, never inferred from these normalized headers. Use `Unknown` when original bytes or + /// field boundaries are unavailable; Cloudflare MUST always supply `Unknown`. + pub fn new( + request_method: Method, + status: StatusCode, + headers: HeaderMap, + body: Body, + monotonic_clock: MonotonicClock, + content_type_provenance: ContentTypeProvenance, + ) -> Self; + + /// Adapter-facing destructure. Mirrors `OutboundRequest::into_parts`; the retained + /// method is required by response converters for HEAD/bodyless normalization. The + /// fifth element carries the effective provenance, including mutation invalidation. This method + /// does not run the final defensive response-header normalization performed by + /// `into_response`; application boundaries that need that pass call `into_response`. + pub fn into_parts(self) -> (Method, StatusCode, HeaderMap, Body, ContentTypeProvenance); + + /// Adapter-facing mutation point. Merely obtaining this mutable reference marks the headers + /// dirty: an originally eligible accessor result becomes `Ineligible(Modified)`, even without + /// a Content-Type edit. The private original enum stays immutable; ineligible/unknown evidence + /// cannot be upgraded. Apply normal adapter body/header policy before construction instead. + pub fn headers_mut(&mut self) -> &mut HeaderMap; + + // ---- App-facing accessors ---- + pub fn status(&self) -> StatusCode; + pub fn is_success(&self) -> bool; // 2xx + pub fn headers(&self) -> &HeaderMap; + pub fn content_type_provenance(&self) -> ContentTypeProvenance; + pub fn body(&self) -> &Body; + + /// **App-facing consuming accessor** for the response body — the orchestration + /// path for streamed responses recommended by `send`'s rustdoc. + /// Returns the underlying `Body` so app code can iterate `Body::Stream` chunks + /// directly (the wrapper installed at response construction time still + /// enforces `dispatch_budget(req).deadline`) or extract the + /// `Body::Once` `Bytes` if the adapter buffered. This is distinct from the + /// adapter-facing `into_parts(self) -> (Method, StatusCode, HeaderMap, Body, ContentTypeProvenance)` + /// destructure used inside response converters; apps that need just the + /// body for streaming orchestration call `into_body` and drop the rest. + /// On `Streamed` mode with single `send`, this is the canonical orchestration + /// path: on Axum/CF/Spin, join per-request async tasks that each await + /// `send` and immediately iterate that response's `into_body` stream. + /// Body consumption is inside each task, not deferred until all sends + /// have returned headers. `send_all_until` remains buffered-only. + pub fn into_body(self) -> Body; + + /// Buffer the delivered body with a final collection cap. Works for both `Once` + /// and `Stream`. Over-cap yields + /// `Err(EdgeError::response_too_large_with_reason(.., + /// ResponseLimitReason::BufferedBody))` (distinct kind, 502 — NOT + /// `bad_gateway`; §3.4.1). + /// + /// This is NOT a thin wrapper over `Body::into_bytes_bounded` — that + /// helper maps over-limit to `payload_too_large` (413), correct for inbound + /// bodies but wrong for an over-large upstream response. This method + /// performs its own bounded drain (pre-append checked accounting) + /// and maps over-cap to `response_too_large` with reason `BufferedBody` + /// (distinct kind, 502 — §3.4.1; consistent with the top of this doc, + /// NOT `bad_gateway`). + /// For effective identity or EdgeZero-decoded gzip/deflate/Brotli, the request's independent + /// `max_decoded_response_bytes` policy has already wrapped the response stream. For raw + /// passthrough, this helper counts the delivered encoded bytes and does not invent a + /// decoded interpretation. + /// + /// **Effective-budget deadline is already honoured on a streamed body.** + /// Adapters wrap `Streamed` response bodies with a deadline-aware stream bounded by + /// `dispatch_budget(req).deadline` — which is non-`None` even for + /// timeout-only and no-deadline requests (the synthetic 30 s ceiling) — + /// so a detected expiry yields a `gateway_timeout` error chunk and this + /// drain returns 504. Axum/Cloudflare provide timer-backed cancellation; + /// Spin and Fastly retain the BestEffort host gaps documented in §3.5.2. + /// There is no need to thread the deadline through manually — call + /// `into_bytes_bounded_until(max, deadline)` only when you want to + /// **cooperatively narrow** the failure timing on top of the request + /// budget (see the precise bound and caveat below). + pub async fn into_bytes_bounded(self, max: u64) -> Result; + + /// As `into_bytes_bounded`, but additionally bounded by a `Deadline` + /// that the caller passes per drain. **The helper is a *cooperative* + /// post-read / EOF validator, not a timer-backed race.** The bound it + /// provides is *exactly* "the first paired-clock expiry check that observes + /// expiry returns `gateway_timeout`," where the check sites are + /// enumerated below. A read that is already blocked when the deadline + /// passes does **not** get preempted by this helper — it returns when + /// the underlying source returns (chunk, EOF, or wrapper-emitted error + /// chunk past the request budget), and the helper's *next* check (or + /// post-return check for `Body::Once`) is what fires. Real-time + /// preemption is the *wrapper's* job (the adapter installs a + /// deadline-aware stream bounded by `dispatch_budget(req).deadline` at + /// response construction time); the helper only catches the + /// **tighter `until`** case at yield boundaries. + /// + /// Concretely, if the wrapper still has 500 ms and the caller passes + /// `until_deadline = now + 100 ms`, and a body read happens to block + /// for the full 500 ms, the helper does **not** return at 100 ms — it + /// observes the expired `until` at the 500 ms post-read check and + /// returns `gateway_timeout`. The bound the helper provides is "first + /// expiry check at or after `until_deadline`," not wall-clock = `until`. + /// Apps that need wall-clock preemption tighter than the request budget + /// must either lower `dispatch_budget(req).deadline` (set + /// `.deadline(min(req_deadline, app_inner_deadline))` on the builder) + /// or split the work into a smaller request. + /// + /// Works on both `Body::Once` and `Body::Stream`: + /// + /// - **`Body::Once` (already buffered)**: the helper checks + /// `until_deadline.is_expired_at(monotonic_clock.now())` **at entry**, before doing anything + /// else, and returns `gateway_timeout` if expired. Otherwise it + /// checks the buffered length against `max` — under cap → `Ok(bytes)`; + /// over cap → `response_too_large` (distinct kind, 502; NOT `bad_gateway` + /// — see "Oversize is a distinct outcome", §3.4.1). **Precedence: expired deadline beats + /// over-cap** (an over-cap error after the deadline has expired is + /// masked by the deadline check, since the caller's `until` rolled + /// the result regardless of cap behaviour). This entry-time check + /// makes single `send` + `Body::Once` callers see consistent + /// `gateway_timeout` semantics whether their response arrived + /// already-buffered or streamed. + /// - **`Body::Stream`**: the helper checks + /// `until_deadline.is_expired_at(monotonic_clock.now())` **both before issuing each blocking body read and again after it + /// returns** — including the EOF read. Returns + /// `Err(EdgeError::gateway_timeout(..))` (504) on the first expired + /// check. + /// + /// **Enforcement composes layer-wise without sharing state.** The + /// adapter wrapper installed at response construction time enforces + /// the request's `dispatch_budget(req).deadline` by yielding + /// `Err(EdgeError::gateway_timeout_caused(.., budget.cause))` chunks past *that* deadline + ///; this helper enforces `until_deadline` cooperatively at + /// the four check sites enumerated above (entry for `Body::Once`; + /// before and after each underlying read including EOF for + /// `Body::Stream`). **"Whichever fires first" is at yield boundaries + /// only**: the wrapper's error chunk is timer-selected on Axum / CF / Spin + /// (with Spin's host teardown still BestEffort) and cooperatively detected + /// on Fastly; the + /// helper's `until_deadline` fires at the next check site. If the + /// caller's `until_deadline` is tighter and the next underlying read + /// returns promptly, the helper fires first; if the next underlying + /// read blocks past `until` but within the wrapper's budget, the helper + /// still fires (post-read check) and the helper's bound is "read + /// latency + at most one extra check," not zero. There is no shared + /// "effective deadline" stored on `OutboundResponse`; it retains the paired clock, + /// request method, status, headers, body, original Content-Type evidence, and a separate + /// header-dirty flag. There is no `min(..)` computation in the helper. + /// Apps that need a single combined check with **timer-backed + /// preemption** of the tighter deadline pass + /// `min(req_deadline, app_inner_deadline)` to `.deadline(..)` on the + /// `OutboundRequest` builder instead of layering here — that pushes + /// the tighter deadline into the wrapper. Adapter support remains exactly + /// as classified by `outbound-deadlines` in §3.5.2. + /// + /// **Enforcement is layered.** The helper itself is cooperative on every + /// adapter — its before-and-after-read `is_expired` check cannot + /// preempt a read in progress. Real-time enforcement of the request + /// budget comes from the adapter wrapping streamed response bodies at + /// construction time: + /// + /// - **Axum, Cloudflare, Spin** — the adapter wraps the response body + /// with a deadline-aware stream using its platform timer (tokio / + /// `worker::Delay` / wasi monotonic-clock), bounded by + /// `dispatch_budget(req).deadline`. That deadline is non-`None` for + /// every request (synthetic 30 s ceiling when `req.deadline` was + /// absent), so the wrapping is unconditional — *not* "only when + /// `req.deadline.is_some`." Each chunk read is bounded by the + /// request's effective deadline, so a peer that stalls mid-stream + /// produces an error chunk at that deadline rather than blocking. + /// `into_bytes_bounded_until`'s helper-side `is_expired` check on + /// the caller-supplied `until_deadline` is what catches the + /// *tighter* `until` case (e.g. the wrapper has 500 ms left but the + /// caller passed a 100 ms `until`) **at the next yield boundary**, + /// not in real time. If a read happens to block for the full 500 ms, + /// the helper returns at 500 ms with `gateway_timeout` (post-read + /// check observed expiry), not at 100 ms. Use + /// `min(req_deadline, app_inner_deadline)` on the builder for + /// timer-backed preemption. + /// - **Fastly** — no guest async timer, but the adapter still + /// wraps the streamed response body with a **cooperative + /// deadline-aware stream** that checks `budget.deadline.is_expired()` + /// **both before issuing the underlying body read and again after it + /// returns** (including the read that discovers EOF) and + /// emits a `gateway_timeout` error chunk past the deadline instead + /// of `Ok(chunk)` or stream-end. This makes `into_bytes_bounded`, + /// `into_response` passthrough, and any other consumer of the + /// wrapped body honour the deadline uniformly — the deadline does + /// not depend on whether the caller chose this helper specifically. + /// Bounded-cooperative semantics apply: a stream that yields one + /// chunk and then stalls returns control on the host's + /// between-bytes-timeout, so worst-case overshoot per chunk + /// gap is one between-bytes-timeout interval — never unbounded. + /// + /// The real-vs-bounded distinction matches the `outbound-deadlines` + /// capability matrix. Decompression-cap and 502-mapping behavior matches + /// `into_bytes_bounded`. Because this helper receives only a `Deadline`, expiry + /// introduced by its `deadline` argument uses `BudgetSource::Unspecified`. + /// An adapter-installed deadline wrapper instead retains and reports the + /// `DispatchBudget::cause` that selected the request budget. + pub async fn into_bytes_bounded_until( + self, + max: u64, + deadline: Deadline, + ) -> Result; + /// JSON-decode the already-buffered body. Requires `Body::Once`; on a + /// `Body::Stream` returns `Err(EdgeError::bad_gateway_with_reason("response body + /// not buffered; use json_bounded(max) or json_bounded_until(max, + /// deadline)", BadGatewayReason::Protocol))`. Malformed JSON uses reason + /// `Decode(Json)` — an upstream returning unparseable JSON is a 502 outcome, not a 400. + pub fn json(&self) -> Result; + + /// Buffer (with a final collection cap) then JSON-decode in one step. + /// Consuming convenience for the `Streamed` mode: equivalent to + /// `into_bytes_bounded(max).await` + `serde_json::from_slice`, with + /// malformed JSON mapping to `bad_gateway` reason `Decode(Json)` (502). + pub async fn json_bounded(self, max: u64) + -> Result; + + /// As `json_bounded`, additionally bounded by a caller-supplied + /// `Deadline`. **The caller-supplied deadline is enforced + /// cooperatively by `into_bytes_bounded_until`** — that is, at the + /// yield boundaries enumerated in that helper's rustdoc (entry for + /// `Body::Once`; before and after each underlying read including EOF + /// for `Body::Stream`). A read already blocked when `deadline` passes + /// does **not** get preempted by this helper; it returns when the + /// underlying source returns, and the next check fires. **Real-time + /// enforcement is the wrapper's job** — Axum / CF / Spin install a + /// timer-selected deadline-aware stream bounded by + /// `dispatch_budget(req).deadline` at response construction time + /// so expiry becomes a typed 504. Spin's host teardown is still cooperative + /// and BestEffort (footnote 8). Fastly detects expiry cooperatively on the + /// body phase; its capability remains BestEffort because cold dispatch and + /// upload-write gaps are unbounded (footnotes 1–2). + /// The `deadline` argument here only adds the cooperative + /// post-read tighten; it does not get its own wrapper. Apps that need + /// timer-backed preemption of a deadline tighter than the request + /// budget set `.deadline(min(req_deadline, app_inner_deadline))` on + /// the `OutboundRequest` builder so the tighter deadline lands in the + /// wrapper. Malformed JSON maps to `bad_gateway` reason `Decode(Json)` (502). + pub async fn json_bounded_until( + self, + max: u64, + deadline: Deadline, + ) -> Result; + /// Pass the response through as a core `Response` (keeps a streamed body lazy). + /// Infallible in safe use: like the other terminal methods it takes `self` by + /// move, so double-consumption of the body is prevented at compile time. The + /// `Result` carries exactly two error classes on adapters with + /// `outbound-header-fidelity = Native`: `Err(EdgeError::internal(..))` for an + /// adapter-invariant violation, and `Err(EdgeError::bad_gateway_with_reason(.., + /// BadGatewayReason::Protocol))` for a malformed `connection` value introduced after + /// construction through adapter-facing mutation. + /// Malformed visible nomination syntax is rejected on every adapter; a raw non-UTF-8 + /// value is additionally detectable on Native-fidelity adapters. Cloudflare receives + /// only post-workerd strings, so it cannot detect the raw-byte case; its documented + /// `BestEffort` behavior is scoped by `outbound-header-fidelity` (§3.5.2). No other + /// network/status condition produces an error here. + /// + /// **RESPONSE-SIDE hop-by-hop normalization is re-applied here as an idempotent defense + /// (symmetric with the request side, §3.1.4).** The authoritative pass already ran on + /// raw response metadata before bodyless/decode/cap decisions. This final pass protects + /// against a later adapter-side `headers_mut()` mutation. A proxied UPSTREAM response + /// can carry hop-by-hop headers + /// that MUST NOT be forwarded downstream: `into_response` strips `connection`, + /// `keep-alive`, `proxy-connection`, `proxy-authenticate`, `proxy-authorization`, `te`, `trailer`, + /// `transfer-encoding`, `upgrade`, **AND every header NOMINATED by the response's own + /// `connection` value** — so an upstream `Connection: x-private` + `X-Private: secret` + /// cannot leak `X-Private` to the downstream client. Both passes use one core helper, + /// `outbound::normalize_response_headers(..)` (the response twin of + /// `normalize_for_dispatch`), so every adapter and passthrough goes through the same + /// stripping. On Axum/Fastly/Spin, the `connection` header is resolved + /// **fail-closed** exactly as on the request side: a non-UTF-8 value is rejected as + /// `bad_gateway` reason `Protocol`, never silently dropped. On every adapter, a visible + /// nomination that is not an RFC field-name token uses that same reason; implementations + /// do not partially honor a malformed list. Cloudflare applies the same stripping to + /// the strings visible after workerd processing but cannot detect malformed raw bytes or + /// recover original non-`set-cookie` field boundaries. §5.4 pins both the portable visible + /// header behavior and the stronger `outbound-header-fidelity` contract. + /// Uses `ResponseHeadNormalization::body_disposition` for final body suppression and carries + /// effective `ContentTypeProvenance` in the resulting response's extensions. The final pass + /// may downgrade an eligible result, but never upgrades original ineligible/unknown evidence. + /// This extension is historical evidence at the outbound conversion boundary only; later + /// ordinary `http::Response` edits are the application's responsibility. + pub fn into_response(self) -> Result; +} +``` + +The complete builder surface — `new`/`get`/`post`/`from_request`/`header`/`headers_mut`/ +`body`/`json`/`timeout`/`deadline`/`max_decoder_bytes`/ +`max_brotli_window_bits`/`max_chunk_bytes`/`max_decoded_response_bytes`/ +`max_encoded_response_bytes`/`max_request_body_bytes`/`max_response_header_bytes`/ +`max_response_header_count`/`max_response_bytes`/`stream_response`. Every fallible step +returns `EdgeError`, so handler code uses `?` uniformly. + +#### 3.1.4 Adapter behaviour contract — redirects and header encoding + +These rules define two explicit levels. The portable baseline on all four adapters strips +visible hop-by-hop headers and visible `connection` nominations and preserves repeated +`set-cookie`. The fixed portable strip set includes the de facto `proxy-connection` field in +addition to the standard hop-by-hop fields. The stronger **`outbound-header-fidelity`** +capability covers both directions: +outbound request field lines reach the platform transport without EdgeZero coalescing, and +raw response-header octets plus original field-line boundaries reach normalization. This +includes fail-closed malformed `connection`, malformed/repeated `content-encoding` +disposition, and repeated non-`set-cookie` field-line preservation. Axum/Fastly are +`Native`; Cloudflare and Spin are `BestEffort`, so a `required` declaration hard-fails there. +Spin's host hides `Connection` before guest normalization; footnote 8 records the direct-WASI +control. Preserving the remaining raw fields is not sufficient for the stronger contract. +Cloudflare's Fetch `Headers` may semantically combine repeated request values, and response +processing sees post-workerd strings and comma-joined non-`set-cookie` values. It cannot +claim original request field-line preservation, malformed raw `connection` → 502, or +malformed raw `content-encoding` → forced passthrough. + +**Redirects: not followed automatically.** A 3xx upstream response is delivered to the +app as `Ok(OutboundResponse)` with the 3xx status and the `Location` header preserved. +EdgeZero never silently follows a redirect on the app's behalf. This is a security +property: an app that allowlists `https://trusted.example` and checks `req.uri()` before +sending can never be diverted to `https://attacker.example` by an upstream 302, because +following the redirect requires the app to issue a fresh `OutboundRequest` — at which +point its allowlist runs again. Per-adapter mechanics: + +| Adapter | How to disable auto-redirect | +| --- | --- | +| Axum | `reqwest::ClientBuilder::redirect(reqwest::redirect::Policy::none())` | +| Cloudflare | `worker::RequestInit { redirect: worker::RequestRedirect::Manual, .. }` (the enum, **not** the string `"manual"`) | +| Spin (WASI) | the hand-built `wasi:http` request (§4.4) does not auto-follow — no opt-out needed | +| Fastly | `fastly` does not auto-follow — no opt-out needed | + +Apps that want to follow a redirect read `resp.headers().get("location")`, run their +allowlist against the new URI, and issue a new request. + +**Header value encoding: UTF-8.** EdgeZero requires every outbound and inbound-of-outbound +header value to be valid UTF-8. The rationale is **portability, not a WASI limitation**: +WASI `http` `fields` values are `list`, so WASI *can* carry arbitrary bytes — but +Cloudflare Workers models headers as JS strings, and other adapters' header types +(`reqwest`'s `HeaderValue`, etc.) do not uniformly round-trip arbitrary bytes. A single +valid-UTF-8 rule is the portable intersection — uniform behaviour beats per-adapter +lossiness for headers that matter. (The check is additionally an HTTP-validity check: a + UTF-8 string still bearing a forbidden control byte like `\n`/`\0` is rejected — + §3.1.3 `header(..)`.) + +- *Outbound request headers.* `OutboundRequest::header(..)` constructs the + `HeaderValue` via `HeaderValue::from_bytes(value.as_ref())`, **not** + `HeaderValue::from_str` — the latter rejects every byte outside visible ASCII and + would refuse a perfectly valid non-ASCII UTF-8 header like + `x-app-display-name: café` before EdgeZero's UTF-8 rule runs. The builder's + `V: AsRef<[u8]>` bound means `value.as_ref() -> &[u8]` works uniformly for `&str`, + `String`, `&[u8]`, `Vec`, `HeaderName`, and `HeaderValue`. + `HeaderValue::from_bytes` accepts the **HTTP header-value byte set** (visible + ASCII + obs-text, with control bytes like `\n`/`\0` rejected to prevent header + injection); EdgeZero then layers its own UTF-8 check via + `std::str::from_utf8(value.as_ref()).is_ok()`. The accepted set is therefore + **valid UTF-8 *and* valid HTTP header-value bytes**, not "all valid UTF-8" — an + HTTP-invalid byte (`\n`, `\0`) inside a UTF-8-valid string still rejects, and + that's intended security behaviour. Two distinct failure messages: + `Err(EdgeError::bad_request("header value contains forbidden bytes: "))` + for the HTTP-validity reject, `Err(EdgeError::bad_request("header value is not + valid UTF-8: "))` for the UTF-8 reject. Loud and at construction time. + Repeated values in `HeaderMap` are appended separately at every adapter boundary. + Axum/Fastly/Spin preserve those entries as distinct transport fields and satisfy the + Native capability. Cloudflare also calls `Headers::append` for every entry, preserving + list semantics and `set-cookie` behavior where Fetch permits it, but workerd owns final + request serialization and may combine non-`set-cookie` lines. That is the request-side + half of its documented BestEffort result; no code or test may promote semantic equality + to original field-line fidelity. +- *Outbound response headers on `outbound-header-fidelity = Native` adapters.* If an upstream response carries non-UTF-8 header values, + **each individual value** is checked (`std::str::from_utf8` on the raw byte slice from + the platform SDK) — invalid values are dropped, valid sibling values for the same + header name are preserved. Multi-value headers like `set-cookie` therefore keep + every valid entry even if one duplicate is invalid. The adapter emits a `log::warn!` + naming each dropped header. + **TWO headers are exempt from this silent drop, because dropping one field line would + change how the REMAINING lines are interpreted** (the same smuggling shape in both + cases): (a) **`connection`** — resolved fail-closed by the authoritative raw-response + normalizer before body disposition or decoding (a non-UTF-8 value → `bad_gateway`), else + a nominated header could smuggle past hop-by-hop removal; + and (b) **`content-encoding`** — an invalid value **forces the stacked/passthrough + branch** (§3.4.1): the value is not decoded and `content-encoding`/`content-length` are + **preserved**, never stripped. Otherwise `content-encoding: gzip` + a second, invalid + `content-encoding` line would drop to an apparent *single* `gzip`, and the converter + would decode a body that is still one layer compressed while stripping the very headers + that revealed it. §5.4 pins a row **crossing** the two rules (repeated field lines where + one is non-UTF-8). The rest of the response is delivered normally so a + malformed exotic header cannot poison an otherwise valid fan-out batch response. + Cloudflare cannot execute this raw-byte branch. It applies normalization only to the + post-workerd strings it receives; this is the documented `BestEffort` deviation of + `outbound-header-fidelity`, not part of the all-adapter baseline. +- ***Cloudflare degradation — precisely scoped (verified against `worker` 0.8.5 + + workerd source).*** Earlier drafts said CF simply "cannot do multi-value headers." + That is **too pessimistic and wrong for `set-cookie`**. The actual split: + + - **`set-cookie` IS preserved on Cloudflare.** workerd's `getDisplayedHeaders` + special-cases it: with the `httpHeadersGetSetCookie` compatibility flag on, each + `set-cookie` is yielded as its **own** `entries()` tuple. **This repo already + qualifies** — `wrangler.toml.hbs` pins `compatibility_date = "2023-05-01"`, and the + flag enables at `2023-03-01`. The historical `proxy.rs` implementation collapsed + these values with `HeaderMap::insert`; the outbound implementation now appends each + value in both conversion directions. (`worker`'s own `http`-feature conversion does + the same.) The regression contract pins multiple `set-cookie` values so this + EdgeZero-owned behavior cannot silently regress. + - **Repeated *non*-`set-cookie` response headers are irrecoverably comma-joined.** workerd + joins same-name values (`kj::strArray(values, ", ")`) in both `entries()` and + `get()`, and its `getAll()` **throws `TypeError` for any name except + `set-cookie`**. There is **no API** to recover the original separate field lines — + two `x-foo` headers arrive as `x-foo: a, b`. Per RFC 9110 §5.3 that is semantically + equivalent for list-valued fields (exactly why the standard special-cases + `Set-Cookie`), but it is **not byte-faithful**. This is the real, narrow CF + limitation. + - **Raw bytes are unreachable; invalid UTF-8 is lost upstream of EdgeZero.** Header + values cross `v8::String::NewFromUtf8` before the guest sees them, arriving as Rust + `String`. So on CF the adapter **cannot detect** whether an upstream value was + invalid UTF-8, and the "drop the invalid value, keep valid siblings" rule degrades + to "whatever the runtime already decided". Axum, Fastly, and Spin expose raw bytes + and honour the rule as written. + + > **⚠️ `get_all` landmine.** `worker-sys`'s `getAll` binding has **no `catch`**, and + > workerd throws `TypeError` for any name other than `set-cookie`. A + > `get_all("x-foo")` therefore **unwinds across the wasm boundary** rather than + > returning `Err`. **Only ever call `get_all("set-cookie")`.** + + Worker 0.8.5 binds this Workers-only `Headers.getAll()` method without a catch boundary; + ordinary browser WASM runners do not implement it. Therefore real SDK response conversion, + the `get_all("set-cookie")` call, and repeated-Set-Cookie preservation execute only in the + pinned workerd/deployed fixture. Browser WASM tests are limited to portable Web APIs, raw + fetch-option construction, and bridge compilation; a browser pass cannot establish the + Workers response-header contract. + + Narrowing the portable contract to ASCII-only was **rejected** — it would degrade the + three adapters that *can* do this correctly. The §5.4 request/response repeated-field and + non-ASCII rows are asserted on Axum / Fastly / Spin and are **best-effort on + Cloudflare**. + +**Method and request-body portability.** `OutboundRequest` accepts an arbitrary +`Method`, but the platforms do not. Cloudflare's `fetch` restricts the method set and +**forbids a body on `GET`/`HEAD`**. The historical adapter silently coerced unsupported +methods to `GET`, which could issue a `DELETE` as a `GET`; the shared preflight below +replaced that behavior. +The portable contract: + +- **Supported methods** (all four adapters): `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, + `DELETE`, `OPTIONS`. These are guaranteed to reach the upstream with the method + intact. +- **Custom / extension methods** are **not portable**. An `OutboundRequest` carrying a + method outside the list above is rejected at **preflight** with + `Err(EdgeError::bad_request("method is not portable; supported: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS"))` + — on **every** adapter, so the failure is uniform rather than "works on Fastly, + silently becomes GET on Cloudflare". +- **`GET`/`HEAD` with a non-empty body** is rejected at preflight with + `Err(EdgeError::bad_request("GET/HEAD request must not carry a body"))` on every + adapter (CF's `fetch` forbids it; the others would happily send it, so EdgeZero + normalises to the strictest platform). +- **No silent coercion, ever.** An adapter MUST NOT rewrite the method to make a + request sendable. Preflight rejects; it never downgrades. + +**Enforcement point: one core validator, run at DISPATCH — not at construction.** +Validating at construction is **not enforceable**: `OutboundRequest::body(..)` is an +infallible chainable setter (`pub fn body(self, body: impl Into) -> Self`), so a +`GET` that passed a construction-time check can acquire a body immediately afterwards +(`OutboundRequest::get(url)?.body(payload)`) and reach the wire unchecked. The rules +above are therefore enforced by a **single core function**: + +```rust +// edgezero-core/src/outbound.rs — the ONLY place these rules live. +// PUBLIC, not pub(crate): the four adapters are SEPARATE crates and must call it. +// (A pub(crate) fn is unreachable from edgezero-adapter-{axum,cloudflare,fastly,spin}; +// verified — a pub(crate) validator fails to compile at the adapter call site.) +pub fn validate_for_dispatch(req: &OutboundRequest) -> Result<(), EdgeError>; +``` + +It is called **exactly once per request before body polling or platform request +construction**, from **both** paths: every adapter's `send`, immediately after its required +method-entry monotonic snapshot, and `start_batch_until`'s per-slot preflight after the one shared +batch snapshot. The snapshot is the only operation that precedes validation, so request +preparation time consumes the original budget (§3.3.3). Neither path may skip the validator +and no adapter re-implements it — that is what makes the failure identical on all four +adapters, keeps a single `send` and a one-slot collected batch equivalent (§5.4), and preserves +slot-index alignment (§3.1.1). + +**`GET`/`HEAD` + `Body::Stream` is always rejected.** "Non-empty body" is not decidable +for a stream: `Body::Stream` has **no observable emptiness** without polling it, and +polling consumes it. The validator therefore does **not** attempt to peek-and-rechain. +**Preflight precedence (a `GET`/`HEAD` + `Body::Stream` in a batch matches TWO rules — +the method/body rule here AND the batch's generic "no `Body::Stream` in buffered fan-out" +rejection).** The **method/body check runs FIRST**, so the error is the specific +`"GET/HEAD request must not carry a streamed body…"` (below), NOT the generic +streamed-in-batch message — the more informative, method-specific diagnostic wins. +(§5.4 pins this precedence with a `start_batch_until([GET + Body::Stream], observation_cutoff)` row asserting the +method-specific message.) The rule is: + +| Method | Body | Outcome | +| --- | --- | --- | +| `GET` / `HEAD` | `Body::Once(empty)` | `Ok` | +| `GET` / `HEAD` | `Body::Once(non-empty)` | `bad_request("GET/HEAD request must not carry a body")` | +| `GET` / `HEAD` | **`Body::Stream` (any)** | `bad_request("GET/HEAD request must not carry a streamed body; emptiness cannot be determined without consuming the stream")` — rejected **unconditionally**, even if the stream would have yielded nothing | +| other methods | any | `Ok` (subject to the portable-method check above) | + +Rejecting an empty-but-streamed `GET` is a deliberate, documented false-positive: the +alternative (peek the first chunk and re-chain it) adds a buffering seam to every +request for a case with no legitimate use. + +*Implementation guardrail.* The UTF-8 check uses `std::str::from_utf8(value.as_bytes())`, +**not** `HeaderValue::to_str()`. `to_str()` is stricter than UTF-8 — it rejects any +byte outside visible ASCII — and would incorrectly drop valid non-ASCII UTF-8 headers +(e.g. an `x-app-display-name: café` style header). Adapters and the core +`normalize_for_dispatch` helper both use `str::from_utf8(value.as_bytes()).is_ok()`. +Framing-sensitive response `Content-Length` is exempt from this lossy filter until its strict +parser runs; malformed or non-UTF-8 values fail as `bad_gateway` reason `Protocol` rather than +disappearing before framing validation. +§5.4 asserts exact non-ASCII UTF-8 request/response preservation on the three +Native-fidelity adapters. Cloudflare separately proves that its guest-visible string path +does not panic or discard a valid value, without claiming raw-octet or field-line fidelity; +that distinction is its documented BestEffort cell. The same tests assert that a raw header +containing a `\x80` byte is dropped on a Native response or rejected on a request. + +Headers that matter for security, tracing, caching, and content negotiation +(`authorization`, `traceparent`, `cookie`, `cache-control`, `accept`, `content-type`, +…) are ASCII-only by spec and are unaffected by this rule. The trade-off only restricts +exotic non-UTF-8 custom headers; apps requiring fidelity for those must not use +EdgeZero outbound for that case. + +**Response normalization happens before semantic interpretation.** Every adapter calls the +public core helper `outbound::normalize_response_headers(..)` immediately after extracting +the upstream method/status/header fields and **before** bodyless handling, `Content-Encoding` +selection, early `Content-Length` rejection, decompression, or construction of +`OutboundResponse`. Before any lossy filtering, the helper captures original `Content-Type` +multiplicity and UTF-8 validity. It then resolves every visible `connection` line fail-closed, strips +the standard hop-by-hop fields plus all valid nominations, applies the response UTF-8 policy, +and returns `ResponseHeadNormalization { body_disposition, content_type_provenance }`. +Adapters use `.body_disposition` for §3.4.1 and retain `.content_type_provenance` for the +sixth constructor argument, except that Cloudflare always supplies `Unknown`. This order is security- +relevant: `Connection: content-encoding` and `Connection: content-length` must remove those +fields before either can influence decoding or cap logic. As a result, app-facing +`OutboundResponse::headers()` and `into_body()` never expose raw hop-by-hop metadata. +`into_response()` runs the same helper again only as an idempotent defense against later +adapter-facing mutation. + +**Original Content-Type evidence.** `ContentTypeProvenance` is a tiny, non-exhaustive enum, +not a raw-header snapshot or a body copy. Original evidence is determined before dropping +non-UTF-8 values or hop-by-hop/connection-nominated fields: + +- No original value yields `Ineligible(Missing)`. +- More than one original value yields `Ineligible(MultipleValues)`, whether identical, + conflicting, or a mixture of textual and non-textual values. Dropping an invalid value + cannot turn an original duplicate set into an eligible singleton. +- Exactly one value that is not valid UTF-8 yields `Ineligible(NonText)`. +- Exactly one valid UTF-8 value yields `SingleTextUnchanged` only if retained unchanged; + if stripped (for example by `Connection: content-type`), it yields `Ineligible(Removed)`. +- When original bytes or field boundaries are unavailable, the adapter must supply `Unknown`. + Cloudflare MUST always use `Unknown`, including an absent or apparently single textual + value, because Fetch string iteration has already lost original octets and multiplicity. + Axum/Fastly/Spin retain the original pass's metadata on every response path. + +This is provenance only: core does not MIME-validate, trim, redact, or split commas. A known +single value `application/json, text/plain` can be `SingleTextUnchanged` even though the +application's MIME parser must reject it as a media type. A quoted comma likewise does not +create another original field value. `Unknown` says evidence is unavailable, not that MIME +is invalid; `SingleTextUnchanged` does not establish MIME validity or debug safety. +MIME validation, acceptance, and redaction remain application-owned. + +`OutboundResponse::new(method, status, headers, body, clock, content_type_provenance)` +requires explicit metadata from that original pass; it never infers provenance from the +already-normalized headers. The private original enum is immutable. `headers_mut()` sets a +separate dirty flag even if the caller makes no edit or changes an unrelated field. +`content_type_provenance()` then returns `Ineligible(Modified)` for an originally +`SingleTextUnchanged` response; original `Ineligible(..)` reasons and `Unknown` never upgrade. +Restoring the old value does not restore eligibility. + +`into_parts()` returns `(method, status, headers, body, provenance)` with this effective +accessor result and without another normalization pass. `into_response()` runs the defensive +pass and inserts effective `ContentTypeProvenance` into the resulting `Response` extensions; +it can downgrade eligibility but cannot promote original ineligible/unknown evidence. Once +converted, the extension describes the historical outbound boundary only. Ordinary later +`http::Response` header edits are not tracked and must be handled by the application. +No raw header or body is retained or copied for this enum, and no new capability is added. + +For 205, `ResponseBodyDisposition::ResetContent { declared_body }` captures whether a +still-visible, valid `Content-Length` declared a positive length **after** nomination +stripping but **before** the helper rewrites the downstream value to `0`. This lets an +adapter abort immediately without consulting a header the normalizer has already changed. + +For response `connection`, each comma-delimited nomination across every field line must be +a non-empty RFC field-name token after optional whitespace. A non-UTF-8 value (where raw +bytes are available), empty member, forbidden byte, or invalid token rejects the whole +upstream response as `bad_gateway` reason `Protocol` (502); valid prefixes are not +partially honored. +Cloudflare enforces the same visible-token rule on post-workerd strings but cannot detect a +raw malformed value that workerd did not expose, which remains its header-fidelity caveat. + +**Final normalization at dispatch (`outbound::normalize_for_dispatch`).** Two surfaces +bypass the construction-time `header(..)` check — `headers_mut()` exposes raw +`HeaderMap`, and `from_request(..)` carries inbound headers in. Adapters MUST call a +core helper `outbound::normalize_for_dispatch(&mut OutboundRequest)` immediately before +handing the request to the platform SDK. The helper is idempotent and runs the same +rules end-to-end: + +1. **First**, handle every `connection` field value (see step 3's nomination list) **before** + any UTF-8 drop, because it governs the removal of *other* headers. A `connection` + value that is **not valid UTF-8 is rejected** (`EdgeError::bad_request`), NOT silently + dropped: dropping it would discard the removal intent and let a sender **smuggle a + nominated header past hop-by-hop stripping** by appending an invalid byte + (`Connection: x-private,` would otherwise forward `X-Private`). This is the + one header where the lossy drop below is a security hole, so it fails closed instead. + Parse each comma-delimited nomination after optional whitespace as an RFC field-name + token. An empty item, forbidden byte, or otherwise invalid token rejects the request as + `bad_request`; do not strip only a valid prefix and continue. Header-name comparison and + removal are case-insensitive, and nominations are accumulated across repeated + `connection` field lines before any nominated field is removed. +2. Drop any *other* header value that is not valid UTF-8 (drop + `log::warn!` naming the + header) — same lossy semantics as the response side. This applies **only** to + values that arrived via `headers_mut()` or `from_request(..)` (which carries + inbound headers verbatim). `OutboundRequest::header(..)` already rejects invalid + UTF-8 at construction with `bad_request` (§3.1.3), so a non-UTF-8 value can only + reach this stage by bypassing the checked builder. The policy split is + deliberate: construction is loud (caller error → 400); proxy-forward and + pre-validated-map paths are lossy (don't fail an otherwise-good forward over an + exotic header). The `warn!` makes the drop observable in either case. **The + `connection` header is exempt — it was already resolved fail-closed in step 1.** +3. Strip hop-by-hop headers (`connection`, `keep-alive`, `proxy-connection`, `proxy-authenticate`, + `proxy-authorization`, `te`, `trailer`, `transfer-encoding`, `upgrade`, plus every + header named in any `connection` header value — parsed and validated from the + now-guaranteed-UTF-8 values per step 1). Idempotent for `from_request` + output; mandatory for manually built requests. +4. Remove `host` — `normalize_for_dispatch` is the single source of truth for stripping + unchecked header-map input. Adapters set final `Host`/`:authority` from the validated + `req.host_authority()` accessor and never recover it from `req.headers()`. The URI remains + separately available for connection target, SNI, and certificate identity. Spin rejects an + explicit override because WASI HTTP cannot express that separation. Cloudflare applies the + override through Fetch headers but remains BestEffort until the deployed host-observation gate + passes under the pinned SDK and compatibility date. `from_request` (§3.1.3) also drops raw + `host`, so only the typed override can create a URI/wire-authority difference. +5. Remove `content-length` — the adapter sets it from the body (length for + `Body::Once`; omitted for `Body::Stream`). +6. Remove `transfer-encoding` — the adapter sets it per body type and HTTP version. + +Apps can therefore use `headers_mut()` and `from_request` freely; portability and +framing safety are guaranteed by this final sweep, not by individual callers +remembering to sanitize. + +**Multi-value headers preserved.** `HeaderMap` permits repeated names — `set-cookie`, +`warning`, custom tracing headers, etc. EdgeZero adapters MUST preserve every entry for +a repeated header **on requests, and for response `set-cookie`**; repeated +*non-`set-cookie`* **response** field-lines are **outside the portable baseline** +(Cloudflare comma-joins them — the documented §3.1.4 exception), so apps needing that +fidelity declare the capability and target a `Native` adapter. Within that scope: use +`HeaderMap::append` (never `insert`) when building, and read with `get_all` (never `get`) +when serializing to the platform SDK or deserializing platform responses. The implemented +per-adapter mechanics are: + +| Adapter | Request side (build platform request) | Response side (read platform response) | +| --- | --- | --- | +| Axum | `reqwest::RequestBuilder::header` (calls `HeaderMap::append`) | iterate `reqwest::Response::headers()` which is already a `HeaderMap` — preserve as-is | +| Cloudflare | `worker::Headers::append(name, value)` — **not** `set` (collapses) | iterate `worker::Headers` entries; `set-cookie` is enumerated separately by the worker runtime, handled explicitly | +| Fastly | `fastly::Request::append_header(name, value)` — **not** `set_header` | `fastly::Response::get_header_all(name)` per name, **not** `get_header` (returns first only) | +| Spin | append via the WASI HTTP `fields` resource (`wasip3::http::types::Fields::append`, re-exported through `spin_sdk`) — natively multi-value. There is **no** `spin_sdk::http::Headers` type; earlier drafts named one that does not exist | iterate WASI `fields` per name | + +Contract tests in §5.4 exercise repeated `set-cookie` response headers and repeated +outbound request headers, so any regression to collapsing duplicates is caught at CI +time. Cloudflare's response-side case runs in workerd, never the browser harness. If a future SDK update breaks multi-value round-tripping on one adapter, the +spec downgrades the contract for that adapter and documents the limitation rather than +silently dropping headers. + +### 3.2 Buffered fan-out + +`OutboundHttpClient::start_batch_until` is the single adapter-facing concurrency API for +buffered fan-out. It rejects streamed request bodies and streamed response mode during isolated +per-slot preflight. All adapters capture one start instant before preflight and attempt every +eligible request before waiting for transport completion. Each emitted `OutboundBatchItem` +contains the original input index and one terminal `OutboundSlotResult`; completion order never +becomes request identity. + +`OutboundBatch::next()` lets an application parse or otherwise consume a fast terminal response +while slower siblings are still pending. It returns `OutboundBatchNext::Item` for each observed +slot and then `OutboundBatchNext::Finished(Completed | Cutoff)`. This is materially different from +returning only a terminal vector: work performed between completions remains inside the caller's +shared deadline. Dropping a pending `next()` future is safe and does not consume a ready item. Each +index is emitted at most once. `cancel()` ends observation immediately, returns every index that +was not already emitted, and invokes the strongest nonblocking teardown available to the adapter. +The caller retains items it already received, which is the portable +partial-result-on-cancellation contract. + +The driver owns the absolute observation cutoff. Before accepting a provider completion it +samples the same injected clock used for request budgets. Equality is expired. When the observation cutoff +wins, the driver stops yielding new terminal results, tears down or abandons unresolved work, and +emits an explicit cutoff event. A response that was provider-ready but not observable before a +blocking provider primitive returned remains unresolved; capability rows make that limitation +explicit. Core reports normal completion only at driver EOF after every input index emits exactly once. A +premature driver EOF and duplicate or out-of-range indices are invariant failures returned as +`EdgeError::Internal`; they never become `Cutoff`. An adapter that detects its own invariant +failure emits `OutboundBatchDriverEvent::Failed` carrying that exact `EdgeError` rather than ending +its driver or fabricating cutoff termination. Incremental `next()` callers receive the error directly. + +On the concurrent Axum, Cloudflare, and Spin drivers, each child samples its terminal instant +synchronously when that child is observed ready. The terminal samples follow poll-observation order in the shared monotonic clock domain. Once one observation is at or past +the observation cutoff, a later-observed child cannot have an earlier on-time terminal sample unless the +injected clock violates its monotonic contract; backwards samples already fail closed as internal +slot outcomes. The drivers therefore emit `Cutoff` immediately rather than polling or grouping +siblings after expiry. Fastly stores provider-selected completions across blocking host operations +and retains its separately documented BestEffort completion-order limitation. + +`HttpClient::send_all_until` is the simple ordered view. It constructs the batch and calls +`collect()`. A successful `OutboundBatchResults.slots` always has the input length. Preflight and +other terminal failures are `Some`; only work unresolved when the observation cutoff won is `None`, and +only when `termination == Cutoff`. `Completed` guarantees no missing slots. If the driver fails, +`OutboundBatchFailure.slots` has the same input length and preserves every result collected before +the failure; unresolved positions remain `None`, but that absence is distinguished from cutoff by +the `Err` result and precise `error` field. There is no second adapter collector and no +compatibility `send_all` method. + +| Adapter | `start_batch_until` mechanism | Completion/cancellation quality | +| --- | --- | --- | +| Axum | `FuturesUnordered` of complete reqwest exchanges raced with the absolute observation cutoff | completion order and future-drop cancellation are Native | +| Cloudflare | completion set of complete Fetch exchanges, each retaining its `AbortController` | completion order is Native; host cancellation remains BestEffort pending deployed proof | +| Spin | completion set of owned WASI HTTP exchanges raced with the observation-cutoff timer | completion order is Native; host teardown remains BestEffort | +| Fastly | sequential `send_async` dispatch followed by indexed pending-handle selection | completion ordering, observation-cutoff handling, and teardown remain BestEffort | + +**Why the adapter owns the batch.** Fastly's pending requests are not ordinary guest-reactor +futures, and its wait/select calls can block inside the host. Application code therefore cannot +construct one portable `FuturesUnordered` over four providers. The opaque batch driver keeps +pending handles, timer mechanics, abort guards, and provider-specific correlation out of the +application contract. + +**Fastly limitations.** Dynamic backend registration can delay later dispatch. A buffered upload +may continue after `send_async` returns without a finite teardown bound. Pending-handle selection +can identify which request became ready, but draining a selected buffered response body remains a +blocking operation and can delay observation of another ready response. These limitations are +reported by `outbound-batch-completion-order`, `outbound-batch-cancellation`, and +`outbound-batch-slot-isolation`; none is hidden by relabeling serial guest work as parallel. + +**Memory and partial failure.** Retained payload accounting remains the sum of bounded buffered +request and response bodies plus the documented in-flight chunk and provider exclusions. One +slot's validation, transport, timeout, HTTP status, or resource-limit outcome does not mutate any +other slot. `elapsed` is sampled when that slot becomes terminal, including preflight failures; +neither collection time nor cancellation time is copied into completed slots. + +### 3.3 Portable deadline + +#### 3.3.1 `Deadline` — portable value type, in core + +```rust +// crates/edgezero-core/src/time.rs (new module) + +/// Portable monotonic clock instant used by every public EdgeZero timing API. Downstream +/// crates do not need a direct `web-time` dependency to name or construct this value. +pub type MonotonicInstant = web_time::Instant; + +/// Cloneable clock source shared by one application, its admitted requests, and the +/// standard outbound clients installed by adapters. +#[derive(Clone)] +pub struct MonotonicClock { /* Arc MonotonicInstant + Send + Sync> */ } + +impl MonotonicClock { + pub fn new(now: Now) -> Self + where Now: Fn() -> MonotonicInstant + Send + Sync + 'static; + pub fn deadline_after(&self, duration: Duration) -> Deadline; + pub fn now(&self) -> MonotonicInstant; +} + +impl Default for MonotonicClock { /* web_time-backed production source */ } + +/// An absolute monotonic instant after which work should stop. A pure value type +/// — arithmetic over `MonotonicInstant`, identical on every target, with no +/// runtime dependency. `time.rs` contains `Deadline`, `DispatchBudget`, +/// `dispatch_budget`, and the public timing constants; the deliberate +/// constraint is that core carries **no runtime / timer / platform +/// dependency** — none of those types reaches outside the value-level +/// arithmetic and the trait surface adapters implement. +#[derive(Clone, Copy, Debug)] +pub struct Deadline(MonotonicInstant); + +impl Deadline { + /// `now + min(d, DEADLINE_FAR_FUTURE)`, where `DEADLINE_FAR_FUTURE` is a + /// **defined constant** clamp (7 days, see below). Bounded far-future clamping, + /// not "saturate to whatever MonotonicInstant::MAX happens to be" — `std::time::Instant` + /// has no `MAX` and platform overflow behaviour differs. The clamp is + /// finite and well above any realistic fan-out batch/proxy budget, so this never + /// truncates a legitimate caller and never panics. Adapter boundaries must + /// not crash the host. The internal `now + min(d, DEADLINE_FAR_FUTURE)` addition + /// itself uses the same saturating `now.checked_add(clamped).unwrap_or(now)` form + /// as `dispatch_budget`, so even the defensive case where the clamped add + /// would overflow the underlying `Instant` yields an already-expired deadline + /// (fails closed) rather than panicking. + pub fn after(duration: Duration) -> Self; + pub fn at_instant(instant: MonotonicInstant) -> Self; // construct from absolute instant + pub fn instant(&self) -> MonotonicInstant; // accessor for the absolute instant + pub fn is_expired(&self) -> bool; + pub fn is_expired_at(&self, now: MonotonicInstant) -> bool; + pub fn remaining(&self) -> Option; // None once passed + pub fn remaining_at(&self, now: MonotonicInstant) -> Option; +} + +/// Hard upper bound on any caller-supplied duration. The clamp exists so +/// `Deadline::after` and `dispatch_budget` cannot panic on a pathological +/// `Duration::MAX` input. Set to **7 days** rather than something larger so the +/// ceiling fits inside every supported platform's per-request timeout range — in +/// particular Fastly's backend timeouts are `u32` milliseconds (≈ 49.7 days max +/// per Fastly 0.13.1), so the EdgeZero clamp must stay well below that. 7 days +/// is still orders of magnitude above any realistic outbound budget; nobody hits +/// it legitimately. +pub const DEADLINE_FAR_FUTURE: Duration = Duration::from_hours(168); // 7 days; from_hours, not from_secs(7*24*60*60), which trips clippy::duration_suboptimal_units +``` + +Every standard adapter entry clones the exact `App::monotonic_clock()` used to capture +ingress timing into the outbound client inserted in request extensions. Axum exposes +`try_with_time(OutboundTime)`; Cloudflare, Fastly, and Spin expose `with_time(OutboundTime)`. +These replace the clock-only constructors without compatibility aliases. A supplied pair must +provide wakeups in the same monotonic domain; advancing a fake clock alone is not a wakeup. +Default low-level constructors pair `MonotonicClock::default()` with the runtime timer. +They are not app-clock propagation paths. Cloudflare and Spin's former unit-struct literal +construction is intentionally removed by this hard cut; low-level callers migrate to +`CloudflareOutboundClient::new()` / `SpinOutboundClient::new()` or `Default` so every client +has an explicit clock source. + +Once `send` or `start_batch_until` begins, every EdgeZero-owned outbound observation uses that stored +clock: method-entry and terminal slot samples, dispatch budget selection, preparation and +provider error precedence, pre/post-ready checks, and streamed upload/response checks. A +deferred body stream owns a clock clone; it never falls back to process-global time after +`send` returns. Each returned `OutboundResponse` also owns that clock clone, so +`into_bytes_bounded_until` and `json_bounded_until` remain in the same time domain after adapter +return. All EdgeZero-owned asynchronous deadline races use the supplied `OutboundTime` timer, +polled before source I/O. Default timer setup failures are internal errors, not timeouts; +supplied timer errors propagate unchanged. Workers use +checked host scheduling/cancellation, and Fastly registers timers with its existing progress +multiplexer, including timer-only waits; no per-slot blocking sleep is introduced. Native +reqwest total timeouts are disabled so they cannot override the supplied pair. Provider-side +phase timers and non-preemptible host calls retain their documented limitations. Later remaining-budget reads use +`min(budget.deadline.remaining_at(clock.now()), budget.duration)`, so a backwards injected +clock cannot enlarge the budget selected at method entry. Checked elapsed subtraction maps a +backwards terminal sample to the existing zero-elapsed internal invariant outcome. + +#### 3.3.2 Mapping an external batch observation deadline to EdgeZero deadlines + +| External concept | EdgeZero mechanism | +| --- | --- | +| External batch deadline (whole fan-out) | Compute one absolute observation cutoff at handler entry with `RequestContext::monotonic_clock().deadline_after(duration)` and pass it once to `start_batch_until(reqs, observation_cutoff)`. Do not use process-global `Deadline::after` with an injected app clock, and do not copy the observation cutoff into each request. | +| Optional request-specific absolute deadline | `OutboundRequest::deadline(request_deadline)` | +| Per-target request timeout | `OutboundRequest::timeout(per_target)` | +| Effective per-request budget | minimum of request timeout, request deadline, observation cutoff, and the default safety budget where applicable | + +**Effective budget rule (`dispatch_budget(req)`).** Returns a `DispatchBudget` struct +carrying **both** the duration to feed to platform SDK timeouts AND the absolute +`Deadline` to use for cooperative body-phase `is_expired()` checks. The implementation +computes a single set of candidate **absolute** deadlines from one monotonic `now` +snapshot and takes the minimum — so the effective deadline can never extend an +original `req.deadline`, and "no deadline" never gets conflated with "expired +deadline" via an `Option` round-trip. + +```rust +pub struct DispatchBudget { + pub cause: BudgetSource, // WHICH input set the effective deadline (for attribution) + pub deadline: Deadline, // effective absolute deadline + pub duration: Duration, // SDK timeout setting +} + +/// Records which configured input selected the effective deadline. The +/// per-call timeout, request deadline, and observation cutoff are separate inputs (§3.3.2 table); +/// the effective deadline is the tightest candidate, and `cause` +/// remembers which one won. This is provenance, not the physical timer phase and not +/// proof that the named deadline itself expired. +// ONE definition, shared by `DispatchBudget` (here) and `EdgeError::GatewayTimeout` +// (§3.4.3). **Defined in `error.rs` (Phase 1a Task 1)** — NOT the `time` module — because +// `error.rs` (Task 1, committed/built first) NAMES it in `GatewayTimeout`, so it must +// exist in Task 1's deliverable or the Task-1 commit fails to build. `time.rs` (Task 2) +// and `dispatch_budget` (Phase 1b) `use crate::error::BudgetSource;`. +// DERIVES + ORDER are COMPILE-VERIFIED (a throwaway crate under `arbitrary_source_item_ordering`): +// - `Debug` — `EdgeError` derives `Debug` and contains `cause`. +// - `Clone`, `Copy` — budget/error carriers pass provenance by value. +// - `PartialEq, Eq` — the Phase 1a contract tests assert `cause == Unspecified` etc. +// - Variants are **alphabetical** — the denied `clippy::arbitrary_source_item_ordering` +// rejects any other order (verified: the earlier `PerCallTimeout`-first order errored). +/// Which budget INPUT produced the effective deadline (the tightest bound) — the budget +/// SOURCE, NOT the physical phase-timer that fired. On Fastly the per-phase timers +/// (connect/first-byte/between-bytes) are sub-divisions of the budget; when one fires the +/// timeout is still attributed to this source (documented `BestEffort` — §3.5.2 footnote 5), +/// so `RequestDeadline` or `BatchCutoff` may be reported for a connect-phase-slice expiry. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +// `#[non_exhaustive]`: a future phase/reason variant must stay non-breaking (public enum). +#[non_exhaustive] +pub enum BudgetSource { + /// The `start_batch_until` observation cutoff was the tighter bound. + BatchCutoff, + /// Neither timeout nor deadline was set — `DEFAULT_NO_DEADLINE_BUDGET` (30 s) applies. + Default, + /// The per-call `OutboundRequest::timeout` was the tighter bound. + PerCallTimeout, + /// The request's own absolute `OutboundRequest::deadline` was the tighter bound. + RequestDeadline, + /// A timeout with no proven dispatch-budget source: either raised outside a dispatch + /// budget (for example `json_bounded_until`) or reported early by a provider timer + /// that the adapter cannot attribute. The default a bare `gateway_timeout(msg)` carries. + Unspecified, +} + +/// `now` is passed in (not snapshotted internally) so one batch can use +/// **one** `now` snapshot across every slot. Without that, sequential per-slot +/// stored-clock calls produce slightly different `duration` values for the same +/// shared `Deadline`, which on Fastly would produce different `budget_ms` values +/// and therefore different dynamic-backend identities for the same host under one +/// batch deadline. `send` (single request) passes its outbound client's first +/// `clock.now()` sample. +// PRIVACY + NAME-COLLISION CONTRACT (both verified by compiling a skeleton). +// `time.rs` is a sibling module of `outbound.rs` and cannot read `OutboundRequest`'s +// private fields directly — earlier pseudocode did, which does not compile. +// **Crucially, the accessors CANNOT be named `timeout` / `deadline` / +// `max_response_bytes`**: those names are already taken by the PUBLIC BUILDER SETTERS +// ( — `pub fn timeout(self, duration: Duration) -> Self`, etc.). Rust does not overload +// inherent methods, so a same-named getter on the same type is a hard `E0592` +// duplicate-definition error. The inputs are therefore exposed through **one** +// crate-visible accessor returning a struct — no name clash with any setter: +// +// pub(crate) struct BudgetInputs { +// pub timeout: Option, +// pub deadline: Option, +// } +// impl OutboundRequest { +// pub(crate) fn budget_inputs(&self) -> BudgetInputs; +// } +// +// (`pub(crate)` — an internal contract between `outbound.rs` and `time.rs`, not app +// surface. Colocating `dispatch_budget` inside `outbound.rs` is rejected: `time.rs` +// must stay independently unit-testable and free of platform-shaped code.) The +// pseudocode below reads `let inputs = req.budget_inputs;` then `inputs.timeout` / +// `inputs.deadline` — **never** `req.timeout` (field) or `req.timeout` (setter). +// Lint contract (workspace denies `clippy::restriction`): public items need +// `#[inline]` (`missing_inline_in_public_items`); no single-char idents +// (`min_ident_chars`) — hence `duration`/`candidate`, never `d`; no bare arithmetic +// (`arithmetic_side_effects`) — hence `checked_add`; no `expect`/`unwrap` +// (`expect_used`/`unwrap_used`); items alphabetical (`arbitrary_source_item_ordering`). +#[inline] +pub fn dispatch_budget( + req: &OutboundRequest, + now: MonotonicInstant, + observation_cutoff: Option, +) -> Result { + let inputs = req.budget_inputs(); // single crate-visible accessor (see contract above) + + // (1) Candidate absolute deadlines. Build candidates directly from the single `now` + // snapshot; do NOT round-trip through `Deadline::remaining()`. An expired caller + // deadline remains a real (past) candidate, while absence remains no candidate, so + // "no deadline" and "expired deadline" cannot collapse into the same `None`. + // Use checked_add throughout — a caller- + // supplied Duration::MAX must not panic the adapter. The same clamp as + // Deadline::after: cap the duration at DEADLINE_FAR_FUTURE + // *before* the add, so the addition itself never overflows in practice + // (now + 7 days is well within Instant range). checked_add on the + // clamped value is belt-and-suspenders. + let saturating = |duration: Duration| -> Deadline { + let clamped = duration.min(DEADLINE_FAR_FUTURE); + let inst = now.checked_add(clamped).unwrap_or(now); // last-resort: now (immediate) + Deadline::at_instant(inst) + }; + let from_timeout = inputs.timeout.map(&saturating); + // `Deadline::at_instant` is public, so a caller could construct a + // Deadline well past DEADLINE_FAR_FUTURE and bypass Deadline::after's clamp. + // Re-clamp `from_caller` here: the caller's deadline is never honoured beyond + // `now + DEADLINE_FAR_FUTURE`. This only tightens; a caller's deadline closer + // than that is unaffected. + let from_caller = inputs.deadline.map(|deadline| { + let far = now.checked_add(DEADLINE_FAR_FUTURE).unwrap_or(now); + Deadline::at_instant(deadline.instant().min(far)) + }); + let from_observation_cutoff = observation_cutoff.map(|deadline| { + let far = now.checked_add(DEADLINE_FAR_FUTURE).unwrap_or(now); + Deadline::at_instant(deadline.instant().min(far)) + }); + let from_default_only = + (inputs.timeout.is_none() && inputs.deadline.is_none()) + .then(|| saturating(DEFAULT_NO_DEADLINE_BUDGET)); + + // (2) Effective deadline = min of the candidates (always at least one). + // NOTE: no `.expect(..)` — `clippy::expect_used` is DENIED in production code + // (the workspace denies the whole `restriction` group; the test exemption in + // clippy.toml does not apply here). The "unreachable by construction" case + // becomes an explicit invariant error instead of a panic, which is also the + // rule that adapter/core boundaries never crash the host. + // Tag each candidate with the BudgetSource it represents, pick the tightest, and + // CARRY the cause. On an exact-instant tie the iteration order wins. The batch observation + // cutoff is first so equality leaves the slot unresolved. Among request-owned + // constraints, a per-call timeout precedes the absolute request deadline. + let (cause, deadline) = [ + from_observation_cutoff.map(|deadline| (BudgetSource::BatchCutoff, deadline)), + from_timeout.map(|deadline| (BudgetSource::PerCallTimeout, deadline)), + from_caller.map(|deadline| (BudgetSource::RequestDeadline, deadline)), + from_default_only.map(|deadline| (BudgetSource::Default, deadline)), + ] + .into_iter() + .flatten() + .min_by_key(|(_, deadline)| deadline.instant()) + .ok_or_else(|| { + EdgeError::internal(anyhow::anyhow!( + "dispatch_budget: no deadline candidate — invariant violated (adapter bug)" + )) + })?; + + // (3) Duration is derived from the chosen deadline and the same now snapshot + // — never `Deadline::after(duration)`, which would re-anchor to a *later* + // now and could extend the absolute deadline past the caller's intent. + let duration = deadline.instant().saturating_duration_since(now); + if duration.is_zero() { + // `cause` was just computed above — attribute the zero-budget timeout to it. + return Err(EdgeError::gateway_timeout_caused("effective budget is zero", cause)); + } + + Ok(DispatchBudget { duration, deadline, cause }) +} +``` + +**Timeout provenance — the outcome carries the selected budget input, not the timer phase.** +Because `DispatchBudget` records `cause`, every timeout an adapter (or the pre-dispatch +check) raises carries the effective `budget.cause`. `BudgetSource::RequestDeadline` means +the request-carried absolute deadline was tightest; `BatchCutoff` means the observation cutoff +was tightest. Neither alone proves that its absolute instant has elapsed: Fastly may fire a rigid connect/first-byte phase timer +while that absolute deadline is still live (§4.3). It is not a retry classification, +physical timeout phase, or batch-abandonment signal. A timeout says only that EdgeZero +stopped waiting, while the origin may already have committed the effect. The provenance is +carried as a typed field on the error: +`EdgeError::GatewayTimeout { message, cause: BudgetSource }` (§3.4.3) — NOT a message string +to be parsed. A slot that times out is raised via `gateway_timeout_caused(msg, +budget.cause)`; a timeout outside a budget context uses `gateway_timeout(msg)`, whose cause +is `Unspecified`. `dispatch_budget` selects provenance before its zero-duration return, so +an already-expired request deadline is `RequestDeadline` unless a per-call zero timeout has +the same absolute instant; the documented equality rule then selects `PerCallTimeout`. +The zero-effective-budget timeout carries that selected cause. +`BudgetSource::BatchCutoff` attribution alone never emits `OutboundBatchDriverEvent::Cutoff`; +`OutboundBatchDriverEvent::Cutoff` may be emitted only when an observation from the batch's +monotonic clock shows that the absolute observation cutoff has expired; equality is expired. +An earlier Fastly phase timeout remains that slot's terminal `GatewayTimeout` item and does not +discard siblings. + +**This is NORMATIVE for EVERY adapter, not just Fastly.** Every EdgeZero-owned budget timer +that expires — the `reqwest` timeout on **Axum**, the `worker::Delay` expiry on +**Cloudflare**, the raced wasi-timer on **Spin**, Fastly's explicitly configured +connect/response phase timers, and the **streamed-response wrapper's** `gateway_timeout` +chunk on all four — MUST use `gateway_timeout_caused(msg, budget.cause)`, NOT bare +`gateway_timeout`. A provider timeout not configured from the selected budget is different: +the pure classifier receives the absolute deadline and observation instant. At/after that +deadline, deadline precedence attributes the result to `budget.cause`; before it, an +independent provider timeout uses `gateway_timeout(msg)` and `BudgetSource::Unspecified`. +Spin applies that early rule to all five WASI timeout variants because the binding cannot +prove which accepted option caused a result. Fastly applies it to `DnsTimeout`, while its +configured `ConnectionTimeout` and `HttpResponseTimeout` phase timers remain attributed. +The deadline-aware stream wrapper is constructed with `budget.cause` so its past-deadline +chunk carries it. **§5.4 asserts the attributed cause +through ACTUAL adapter results** (not just the core helper): for each of Axum/CF/Spin, a +`.timeout(short).deadline(long)` expiry yields a `PerCallTimeout`-attributed harvested error +and the mirror yields `RequestDeadline`; a no-deadline default yields `Default`. §5.4 pins a Tier 1 test: +`.timeout(short).deadline(long)` that expires yields a `PerCallTimeout`-attributed error, +and `.timeout(long).deadline(short)` yields a `RequestDeadline`-attributed one — the two are +distinguishable from the harvested result alone. + +Behaviour table (the implementation gives these directly; listed here for clarity): + +All `now + t` entries in this table are shorthand for `now + min(t, +DEADLINE_FAR_FUTURE)` (§3.3.1) — the clamp is universal, not a special case for +`Duration::MAX`. + +Below, `clamped(d)` denotes `Deadline::at_instant(d.instant().min(now + +DEADLINE_FAR_FUTURE))` — the re-clamp of a caller's `req.deadline` performed by +`dispatch_budget` so a `Deadline::at_instant` constructed past the 7-day clamp +cannot escape the bound (§3.3.2 step 1). For brevity the table writes +`clamped(d)` rather than the full expression. + +| `req.timeout` | `req.deadline` | `duration` | `deadline` (absolute) | +| --- | --- | --- | --- | +| `None` | `None` | `30 s` | `now + 30 s` | +| `Some(t)` | `None` | `min(t, DEADLINE_FAR_FUTURE)` | `now + min(t, DEADLINE_FAR_FUTURE)` | +| `None` | `Some(d)` | `clamped(d).instant() - now` | `clamped(d)` | +| `Some(t)` | `Some(d)` with `now + min(t, …) ≤ clamped(d).instant()` | `min(t, …)` | `now + min(t, …)` (tighter) — **cause `PerCallTimeout`; EQUALITY goes HERE** | +| `Some(t)` | `Some(d)` with `now + min(t, …) > clamped(d).instant()` | `clamped(d).instant() - now` | `clamped(d)` (strictly tighter) — cause `RequestDeadline` | +| any nonzero/absent timeout | expired (`d.instant() <= now`) | — | `Err(gateway_timeout)` with cause `RequestDeadline` | +| `Some(Duration::ZERO)` | `Some(d)` with `d.instant() == now` | — | `Err(gateway_timeout)` with cause `PerCallTimeout` (the equality tie rule) | +| any | selected duration ends up zero | — | `Err(gateway_timeout)` with the selected cause | +| `Some(Duration::MAX)` | `None` | `DEADLINE_FAR_FUTURE` (7 d) | `now + DEADLINE_FAR_FUTURE` | +| `None` | `Some(d)` 100 years out via `at_instant` | `DEADLINE_FAR_FUTURE` (7 d) | `now + DEADLINE_FAR_FUTURE` | + +`.timeout(50ms)` with no request deadline therefore yields `duration = 50ms` and +`deadline = now + 50ms`, **not** 30 s. The single absolute `deadline` is what Fastly's +between-chunk checks (§3.3.4) and the streamed-body wrappers in §4.1/§4.2/§4.4 use, so +per-request `timeout` is honoured across the entire exchange — including the streamed +body phase — whether or not a request deadline was provided. A batch driver also supplies its +observation cutoff to `dispatch_budget`. When `BatchCutoff` wins and expires, the driver ends with +that slot unresolved rather than manufacturing a per-slot timeout item. A tighter request +timeout or request deadline still produces its ordinary terminal timeout before the observation cutoff. + +"No deadline configured" therefore differs from "deadline configured and expired" — +the former is bounded by the synthetic 30 s ceiling; the latter is a hard fail at +dispatch with `gateway_timeout`. + +The same rule governs the dispatch+headers phase in `Streamed` mode. The body phase is +**also** governed by `dispatch_budget(req).deadline` (see §3.3.3) — the spec +deliberately does +not split the deadline into "before headers" and "after headers" pieces. + +#### 3.3.3 What the deadline covers + +The deadline on `OutboundRequest` covers the **entire upstream exchange** in both modes: +request preparation/upload, dispatch, response headers, and the upstream response-body +read/completion protocol. It ends when the core `OutboundResponse` has been fully buffered +or its Streamed body reaches validated native EOF. It does **not** include the later write of +a resulting core `Response` to the downstream client. That response-egress lifetime needs +its own absolute write deadline, abort semantics, and exactly-once completion contract +(§1.3); an outbound deadline must never be silently reused or restarted for it. The +adapter captures one monotonic `entry_now` as the first operation inside `send`, or one +`batch_now` as the first operation inside `start_batch_until`, **before** request normalization, +batch preflight, platform builder construction, or any other preparation. Validation still +runs before `dispatch_budget` so the portable bad-request precedence is unchanged, but every +valid request computes its budget from that entry snapshot and the optional observation cutoff. No +adapter re-anchors after preflight. Clock-seam tests advance time during preparation and prove it +consumes the same absolute budget; `start_batch_until` additionally proves every valid slot shares +exactly one snapshot. +The upstream mechanism differs: + +- **`Buffered` (default):** the adapter buffers the body *inside* the deadline-bounded + region, so a slow body counts against the budget. `Ok(resp)` from `send`, or a yielded + successful batch item, means the full exchange completed within the applicable deadline. +- **`Streamed`:** `Ok(resp)` is returned once headers arrive — earliest possible + delivery — but the **body stream returned in `resp` is adapter-wrapped to honour + `dispatch_budget(req).deadline`.** That deadline is the *effective* one computed by + the budget rule (§3.3.2), which is non-`None` even for timeout-only and no-deadline + requests — adapters wrap the body stream in every case, not only when + `req.deadline.is_some()`. Axum and Cloudflare provide timer-backed cancellation, + though Cloudflare remains `BestEffort` until deployed host-observed evidence establishes + a finite cancellation bound; + Spin races a monotonic timer but has only cooperative Component Model teardown + (BestEffort, footnote 8); Fastly checks cooperatively between host reads. Every + adapter surfaces deadline expiry as a typed `gateway_timeout`; the capability + matrix states where host work can outlive that result. + +What this means in practice: + +- `OutboundResponse::into_bytes_bounded(max)` on a streamed body already honours the + effective-budget deadline through the wrapped stream — body chunks past the + deadline yield `gateway_timeout`. +- `OutboundResponse::into_bytes_bounded_until(max, deadline)` is for tightening the + bound below the effective-budget deadline (e.g. an inner budget for body-only) — + not for re-applying the same deadline, which is automatic. +- If the caller dropped the `Deadline` value but still wants the same effective + ceiling, passing `Deadline::after(remaining_budget_from_some_source)` works; or + just call `into_bytes_bounded` and trust the wrapped stream. + +This is one upstream contract for everyone: handlers never have to remember "Streamed cuts +the deadline at headers." Adapter notes (§4.1–§4.4) implement it through validated upstream +completion. + +#### 3.3.4 Per-adapter enforcement (`Buffered` mode) + +| Adapter | Mechanism | Strength | +| --- | --- | --- | +| Axum | `reqwest::RequestBuilder::timeout(effective)` — reqwest applies it through response-body read | Real, whole-operation | +| Cloudflare | race the adapter-private `fetch_raw_with_signal` (+ body drain) against `worker::Delay::from(effective)`; final fetch options carry manual response encoding and the abort signal (§4.2); the guard aborts on expiry | Real, whole-operation with cancellation | +| Spin | race the entire `send_one` future (send **and** body collect) against a WASI monotonic-clock timer; drop the guest future on expiry | Guest-visible 504 at the timer; host teardown is cooperative and therefore `BestEffort` (footnote 8) | +| Fastly | the current B31 aggregate policy above configures coupled, ceiling-rounded provider timers from the original timeout/duration anchor; body drain checks the unchanged absolute deadline **after every blocking read, including EOF** | `BestEffort`: timers can outlive a shorter logical deadline, and registration, request writes, head waits and body reads are not guest-preemptible. No phase-sum overshoot bound is claimed | + +**Drop-cancellation guarantee, per adapter (what happens to a LOSING arm).** A fan-out +consumer under deadline pressure needs to know whether a timed-out/deadline-lost send is +actually *aborted* or merely *stopped-being-waited-on* — "harvest returns" ≠ "the pending +request is cancelled." The guarantee: + +| Adapter | On deadline/timeout, the in-flight send is… | Origin observes cancel? | +| --- | --- | --- | +| **Axum** | the request/body future and EdgeZero source are dropped; Reqwest/Hyper performs protocol-specific transport cleanup | HTTP/1 may close the connection; HTTP/2 resets the stream while retaining the pooled connection. No generic connection-drop or bounded origin-observation claim | +| **Cloudflare** | **cancelled** — `controller.abort()` (NOT a bare future-drop, which would leave the subrequest running) | Yes (§5.3 blocking test) | +| **Spin** | guest future is dropped and Component Model cancellation is requested; completion writers default to `Err` | Not guaranteed within a finite bound; host-observed tests are upgrade evidence (footnote 8) | +| **Fastly** | **No bounded origin cancellation guarantee.** Buffered batches retain adapter-private pending handles and select completed headers only after every eligible slot has been issued. Dropping the batch drops unselected handles and stops guest observation, but the SDK does not establish finite host teardown; once a selected response enters a blocking body read, guest cancellation cannot preempt that read. Single-`send` streamed uploads follow §4.3's upload protocol: source/cap failure, deadline expiry, write/flush/finish failure, and future drop can exit without waiting for a response. Dropping unfinished upload handles aborts that guest protocol but likewise does not establish a finite host-teardown bound. Host phase timers can fail a request; a **sibling slot's** deadline never cancels another slot | **No finite bound** — documented BestEffort limitation | + +Axum gives a bounded guest-visible timeout and deterministic future/source drop, while its +wire cleanup remains protocol-dependent. Cloudflare gives explicit bounded abort through +`AbortController`. Spin and Fastly expose different BestEffort gaps: Spin requests +cooperative teardown without a documented host bound, while Fastly exposes no guest +cancellation primitive for a dispatched request. Axum loopback tests distinguish HTTP/1 +connection closure from HTTP/2 stream reset; optional deployed HTTP/2/HTTP/3 +characterization records what intervening proxies and origins observe without promoting +that observation into a portable guarantee. + +**Historical pre-B31 timing derivation (superseded).** The exact-budget phase split and its +derived overshoot bounds below are not the current B31 policy. Use the override in §1 for +backend/timer behavior; unchanged body/cancellation limitations still apply. + +Fastly has no guest wall-clock primitive to +preempt a chunk read in progress. At dispatch the adapter computes `let budget = +dispatch_budget(req, now, observation_cutoff)?` (§3.3.2, where `observation_cutoff` is `None` for a +single `send` and `Some(observation_cutoff)` for a batch; `now` is snapshotted inline for single `send` +and passed in as the shared `batch_now` for `start_batch_until`. `DEFAULT_NO_DEADLINE_BUDGET = 30 s` +and the synthetic absolute deadline both apply when no deadline is set, identical to +every other adapter) and derives the host timeouts via the named helper: + +```rust +// Lint contract: the workspace DENIES `clippy::restriction`, so `as_conversions` +// and `arithmetic_side_effects` are hard errors — no `as` casts, no bare `+`/`-`/`/`. +// Use `Duration::as_millis` (already integer-ms), saturating/checked arithmetic, and +// `u64::try_from` instead of `as u64`. +// Returns the EXACT host-timeout ms (true ceil-to-ms) — the single source used for BOTH +// the phase split (connect/first-byte/between-bytes) AND the backend identity key, so a +// cached backend's timers always match the identity it was registered under. No +// bucketing: the cache is per-session and bounded by the fan-out size, so the raw value +// is fine and keeps the deadline bound exact. +fn fastly_timeout_ms(budget: &DispatchBudget) -> u64 { + // True ceil-to-ms — never floor a sub-ms remainder away. + // `as_millis` floors, so add 1 when there is a sub-ms remainder. + let nanos = budget.duration.subsec_nanos(); + let has_remainder = !nanos.is_multiple_of(1_000_000); + let ceil_ms = budget + .duration + .as_millis() + .saturating_add(u128::from(has_remainder)) + .max(1); + + // The DEADLINE_FAR_FUTURE clamp keeps this below Fastly's 2^32 ms ceiling. Clamp + // defensively, then convert fallibly — a bug elsewhere must not crash the host, + // and a bare `as` cast is forbidden by the lint. + let ceiling = u128::from(u32::MAX).saturating_sub(1); + let clamped = ceil_ms.min(ceiling); + u64::try_from(clamped).unwrap_or(u64::from(u32::MAX).saturating_sub(1)) +} + +// `dispatch_budget` always takes an explicit `now` and optional observation cutoff. Public `send` +// snapshots at method entry before preflight; `start_batch_until` snapshots once into +// `batch_now` at method entry and reuses it +// across slots so the dynamic-backend identity stays consistent for a shared +// caller Deadline. +let now = self.clock.now(); // single `send`; batch passes batch_now +let budget = dispatch_budget(req, now, None)?; + +// Fastly 0.13.1 exposes the timeout setters on BackendBuilder, NOT on Request — see +// https://docs.rs/fastly/0.13.1/fastly/backend/struct.BackendBuilder.html. +// IMPORTANT: connect_timeout and first_byte_timeout are *separate* phase timers +// per Fastly's docs — connect bounds DNS+TCP+TLS setup; first_byte bounds the gap +// from "request sent" until headers are received. Setting both to the same `t` +// would make the dispatch+headers worst case ~2*t, breaking the absolute-deadline +// bound. We therefore SPLIT the budget across the two phases (and the third, +// between-bytes, which only applies once chunks are flowing during body drain), +// keeping the sum exactly equal to total_ms: +// total_ms = ceil-to-ms(budget.duration) +// connect_ms = total_ms / 4 [floor; most connects take <100ms] +// first_byte_ms = total_ms - connect_ms [remainder; sum invariant] +// between_ms = total_ms [body-phase ceiling unchanged] +// Sub-4 ms degenerate case: both = total_ms (sum = 2*total_ms, documented). +// SSL configuration also lives on BackendBuilder: `use_ssl` defaults to false, so +// HTTPS targets MUST opt in explicitly with .enable_ssl() and configure SNI + +// certificate verification (implemented by the dynamic-backend builder in +// crates/edgezero-adapter-fastly/src/outbound.rs). HTTP targets opt out via +// .disable_ssl(). +// +// Five canonicalized values come from the OutboundRequest accessors — +// adapters MUST consume these, never re-derive from `req.uri`): +// - `req.backend_target` — connection target `"host:port"` with the +// resolved port; passed as the +// BackendBuilder's `target` arg. +// (implemented by `OutboundRequest::backend_target`). +// - `req.host_authority` — authority for `.override_host(..)` +// (carries the explicit port only when +// non-default; preserves Host +// semantics). +// - `req.host_name` — canonical host only, with no port or IPv6 brackets; +// used in `BackendIdentity`. +// - `req.sni_hostname` — `Option<&str>`. `Some(host)` for DNS-name HTTPS +// targets; `None` for IP-literal HTTPS (RFC 6066 +// forbids SNI for IP literals). When `None`, the +// adapter omits `.sni_hostname(..)` entirely; it +// does NOT fall back to `req.uri.host`. +// - `req.cert_host` — `Option<&str>`. `Some(host)` for any HTTPS target +// (DNS name OR IP literal — port-stripped, +// bracket-stripped); `None` for non-HTTPS schemes. +// Passed to `.check_certificate(..)` verbatim; the +// adapter does NOT bracket-trim, parse, or +// post-process. +// Phase split. The documented semantics: connect gets a *floor quarter* of the +// already-ceiled total; first_byte gets the remainder; between_bytes gets the full +// budget. Invariant we want: connect_ms + first_byte_ms == total_ms exactly, so +// the worst-case dispatch+headers wall-clock is bounded by `budget.duration` +// (modulo ms rounding). Using `total_ms / 4` (floor) keeps the sum exact; the +// earlier "ceil-to-ms of budget * 1/4" framing was a misnomer — that would have +// made the sum exceed total_ms by up to 1 ms for some inputs. For tiny budgets +// where the 1/4 share would round to 0, we degenerate to "both = total_ms" — +// the absolute-deadline bound becomes 2*total_ms but at sub-4 ms scale this is +// negligible (and the ceil-to-ms rounding already dominates). +let total_ms = fastly_timeout_ms(&budget); // exact ceil-to-ms of budget.duration +let (connect_ms, first_byte_ms) = if total_ms < 4 { + (total_ms, total_ms) // sum = 2*total_ms; documented +} else { + let connect = total_ms / 4; // floor — keeps sum exact + let first_byte = total_ms - connect; // sum = total_ms exactly + (connect, first_byte) +}; +let between_ms = total_ms; +let mut builder = Backend::builder(&backend_name, &req.backend_target()) + .connect_timeout(Duration::from_millis(connect_ms)) + .first_byte_timeout(Duration::from_millis(first_byte_ms)) + .between_bytes_timeout(Duration::from_millis(between_ms)) + .override_host(req.host_authority()); +// TLS handling — the accessors carry the canonicalized split. We do NOT +// inspect `req.uri` directly: `cert_host` returns `Some` iff the scheme is +// HTTPS (the adapter-local "is TLS?" question), and `sni_hostname` carries +// the DNS-vs-IP-literal distinction (`None` for IP literals per RFC 6066). +builder = match req.cert_host() { + Some(cert) => { + // HTTPS: always set .check_certificate(..). Pass req.cert_host + // through unmodified — bracket-stripping for IPv6 is already done in + // the accessor; we never call .trim_start_matches('['). + let mut b = builder.enable_ssl().check_certificate(cert); + // SNI: only when the accessor returns Some (DNS-name host). + // For IP literals (`None`).sni_hostname is omitted entirely. + if let Some(sni) = req.sni_hostname() { + b = b.sni_hostname(sni); + } + b + } + None => builder.disable_ssl(), // HTTP +}; +let backend = builder.finish()?; +// Fastly's Request public API has no `with_backend`. The backend is passed as +// the argument to `send` / `send_async` / `send_async_streaming` at send time +// (each accepts `impl ToBackend`). `Backend` implements `ToBackend`. +// Buffered request body (send_all_until only — preflight rejected streams): +let pending = fastly_req.send_async(&backend)?; +// Streamed request body (single `send` only): +// let (streaming_body, pending) = fastly_req.send_async_streaming(&backend)?; +``` + +The dynamic-backend identity tuple (§4.3) is `scheme + ":" + host + ":" + +resolved_port + ":" + tls_mode + ":" + budget_ms`, where `tls_mode` is derived from +`req.uri().scheme_str()` and `budget_ms = ceil-to-ms(budget.duration)` — the same +`total_ms` that drives the `connect_ms / first_byte_ms / between_ms` deterministic +phase split above. The cached `Backend` and a freshly-requested one therefore always +carry identical timeouts AND identical SSL configuration because both are +deterministic functions of the same tuple. The implementation in +`crates/edgezero-adapter-fastly/src/outbound.rs` applies the SSL setters to every dynamic +backend. The budget is set once +before `send_async` and not mutated afterwards — the Fastly SDK does not expose +dynamic per-chunk timeout updates. During body drain the adapter checks +`budget.deadline.is_expired()` **after every blocking body read returns, including +the EOF read** (per the §3.3.4 rule — the earlier "between chunks" wording was +incomplete because a final EOF read can itself cross the deadline). Because +`dispatch_budget` always returns a concrete `Deadline` (synthetic if the request +had none), this cooperative check works uniformly whether or not the caller +supplied a deadline. +`connect-timeout` and `first-byte-timeout` together bound the dispatch+headers phase +at `budget.duration` (their sum, by the §4.3 split) **when `total_ms ≥ 4`**; for +`total_ms < 4` the code degenerates to `connect = first_byte = total_ms` and the +sum is `2 * total_ms`. The absolute-deadline guarantee in the sub-4 ms branch is +therefore "≤ `total_ms + BATCH_DISPATCH_SLACK_MAX + ms_rounding` past deadline" +(strict upper bound: `BATCH_DISPATCH_SLACK_MAX + total_ms + ms_rounding` +which is `25 + (≤ 3) + (≤ 1) < 29` ms), not the common-case "≤ 26 ms" — see +the two explicit +branches in §4.3 "Net guarantee." Sub-4 ms outbound budgets are degenerate inputs +where ms-rounding already dominates, not a normal operating point. The documented trade-off (§4.3) is that a request +spending more than `budget/4` on the configured connect phase (TCP+TLS) fails at the +connect timer even if the remaining budget would have sufficed for headers; that +is captured by the separate `outbound-flexible-phase-budget` capability (§3.5.1). +DNS is excluded from this causal claim because the pinned SDK exposes no DNS-timeout +setter; live observations may characterize provider behavior but do not turn it into a +portable configured-timer guarantee. +During body drain (post-`wait()`), the adapter checks `budget.deadline.is_expired()` +**after every blocking body read returns, including the EOF read** (not "between +chunks" — the EOF read can itself block past the deadline and would otherwise +slip through with `Ok(resp)`). On the first expired check the slot is aborted +with `gateway_timeout`; each individual chunk-gap (including the gap before EOF) +is bounded by the host `between-bytes-timeout`. So the Buffered `Ok(resp)` +contract — "headers AND body completed within the deadline" — holds **for the response +phase**: either every read (including EOF) observed `!is_expired()`, or the slot returned +`gateway_timeout`. **The REQUEST-transmission phase is not covered by this argument** — no +Fastly timer bounds the guest-to-origin write (footnote 2), which is why Fastly's +`outbound-deadlines` is `BestEffort` rather than `Native`. + +**Slot-level vs. wall-clock-observed completion.** The response-side bound above begins +only after a slot's sequential `send_async` dispatch has returned. A cold backend +registration can block before that call and prevent later dispatches. A buffered request +upload also has no finite host-write completion bound, but pinned Fastly 0.13.1 +`send_async` returns as soon as sending begins and continues transmitting headers/body in +the background; that upload therefore cannot synchronously prevent the guest from issuing +later `send_async` calls. It can leave its own pending handle unresolved. For slots that have +been issued, the Fastly host runs requests independently and applies each slot's configured +response timeouts. + +After dispatch, EdgeZero passes pending handles to `select_handles` in bounded groups. The +selected position and raw handle identity restore the original slot index even when the host +reorders the returned remaining handles. This removes input-order header blocking: a ready slot +in the active group can be observed while another remains pending. It does not make the whole +exchange concurrent in the guest. Once a response head is selected, EdgeZero drains that one +buffered body synchronously before selecting another handle. Therefore: + +- **Per-slot headers after dispatch:** each dispatched slot's connect / first-byte / + between-bytes timers derive from its own `budget.duration`, and the host enforces them + independently. This does not cover a later slot that cold registration prevented from being + dispatched, nor does it prove exact completion order across groups larger than + `fastly_shared::MAX_PENDING_REQS`. +- **Observed completion order:** the adapter yields the slot selected by the host, paired with its + original index. Preflight and synchronous dispatch failures are queued before selected host + completions. Provider selection order, bounded grouping, and synchronous selected-body drain + make `outbound-batch-completion-order` `BestEffort` on Fastly rather than `Native`. +- **Buffered body drain is serialized by selection, not input order.** While one selected body is + draining, other completed headers cannot be observed by the guest. A later-observed slot can + consequently cross its own request deadline or the observation cutoff even if its response would + have completed within that bound when drained in isolation. A request-deadline expiry is a + terminal attributed 504; an observation-cutoff expiry leaves that slot unresolved. Small response + bodies reduce this serial drain term, but the contract assigns no universal threshold because + host-call timing is deployment-specific. + +There is deliberately no finite whole-batch wall-clock bound on Fastly. Dispatch may include cold +registration, request writes have no finite completion bound, `select_handles` is a blocking host +operation, and selected bodies drain one at a time. Once one selected response is actively +draining, however, the worst-case post-deadline overshoot for an individual blocking read is one +between-bytes-timeout interval, which is no greater than that slot's +`effective_at_dispatch`. The absolute check after every read, including EOF, then terminates the +slot. This active-read bound does not include time spent before selection or behind an earlier +selected body. + +Apps reasoning about precise wall-clock behavior must not treat `effective_at_dispatch` as a +whole-batch bound. `outbound-batch-completion-order` exposes the observation-order limitation; +`outbound-batch-slot-isolation` exposes the fact that sequential dispatch and selected-body drain +can change a sibling's observed result; and `outbound-batch-cancellation` exposes the lack of a +finite host teardown proof. Requiring any of those capabilities produces a hard build failure on +Fastly. The warm single-slot body-read mechanism has a documented cooperative bound, while the +static `outbound-deadlines` cell remains `BestEffort` because cold registration and request-write +paths retain unbounded gaps. + +#### 3.3.5 No general-purpose timeout combinator (deliberate) + +An earlier draft put a `timeout(deadline, future)` combinator for *arbitrary* futures in +`edgezero-core`. That is **removed**: a real timer future needs a platform runtime +(`tokio` / `worker` / `spin-sdk`), which core may not depend on (§1.3). Core therefore +ships only the `Deadline` value type; outbound-deadline enforcement lives entirely inside +adapters (§3.3.4). A general arbitrary-future timeout would require an adapter-injected +`Timer` trait and a dedicated capability; it is **out of scope** here because the fan-out pattern's +timing needs are fully met by the outbound path. Noted as possible future work. + +### 3.4 Bounded buffering & error mapping + +#### 3.4.1 Outbound responses + +**Adapter-visible upstream header limits happen first; normalization and no-content handling follow, +before body decode or body-cap logic.** §3.4.5 measures response headers at the earliest +adapter-visible boundary, before normalization can remove fields. After those limits +pass, `normalize_response_headers` captures pre-filter Content-Type evidence and strips +hop-by-hop fields and `connection` nominations (§3.1.4). Its returned +`ResponseHeadNormalization::body_disposition` determines the following handling; its +`content_type_provenance` is retained separately, with Cloudflare always using `Unknown`. +A response that is bodyless by HTTP framing — the response to a +**`HEAD`** request, or any **`1xx`**, **`204`**, or **`304`** status — carries no payload even +though HEAD/304 MAY legitimately carry `Content-Encoding` and a *representation* +`Content-Length` (e.g. a `HEAD` echoing what a `GET` would return; a `304` echoing the cached +representation's metadata). For these: +- **Do NOT attempt to decode.** There are no body bytes; feeding EOF to a compressed decoder + would error and produce a **false `bad_gateway` (502)**. Skip the decoder entirely. +- **Framing headers are status-dependent (RFC 9110 §8.6 / RFC 9112 §6.2).** For a + **`HEAD` response and `304`**, `content-encoding` and a *representation* `content-length` + are legitimate metadata the client needs (a `304` or `HEAD` with them stripped breaks + cache validation). Validate every visible `content-length` as one consistent `u64` first: + malformed values, comma lists, conflicting duplicates, and values above `u64::MAX` are protocol + 502 before any body poll; valid values, including values above 4 GiB, are preserved unchanged. + For **`1xx` and `204`**, + `content-length` is prohibited and is removed. This framing normalization is centralized + in the same response helper as hop-by-hop stripping so every adapter applies it + identically. +- The adapter settles or drops the platform body handle according to that runtime's + protocol and constructs an empty core body. It never feeds a framing-bodyless response + to the content decoder. + +**`205 Reset Content` is a separate semantic-suppression case.** RFC 9110 forbids a server +from generating content in a 205 response, but HTTP/1.1 message framing does not grant 205 +the same automatic no-body precedence as HEAD/1xx/204/304. An illegal framed body therefore +cannot simply be replaced with `Body::Once(empty)` while leaving the native body unread: +that would risk connection reuse on unread bytes and, on Spin, falsely complete the +`consume_body` caller-result protocol. For 205, the adapter skips content decoding and awaits +at most one native body read under the effective deadline. An explicit EOF writes/observes +clean completion; any observed non-empty body bytes cause the adapter to abort/drop the +remaining native body and mark the platform completion protocol as failed. The +downstream/core result is still a 205 with an empty body, and `content-length` is normalized +to `0`. A native read failure before that disposition is `bad_gateway` reason `Transport`; +invalid completion/framing is reason `Protocol`; deadline expiry is the attributed +`gateway_timeout`. `ResetContent { declared_body: true }` permits +immediate abort without a read. + +This contract applies to **guest-visible read results**, not HTTP frame boundaries. +Fastly's `std::io::Read` returns `Ok(0)` for EOF when given a non-empty buffer and cannot +expose an empty wire data frame. Stream APIs can instead yield an empty item before EOF: +that item is not proof of completion, so the adapter conservatively aborts/drops the +remaining body and marks completion failed, still returning an empty 205. It need not +classify the empty item as illegal HTTP content. The one-read limit avoids an unbounded +discard of empty items; tests exercise each SDK's observable EOF/empty-item distinction +without asserting that every adapter can inspect wire frames. On Cloudflare every early +abort disposition calls the subrequest's `AbortController::abort()` (§4.2). + +Cloudflare has one narrower observable boundary: workerd can expose `Response.body == null` +for 205 after suppressing content itself. After adapter-visible upstream header checks, a positive +`Content-Length` still selects `declared_body` and aborts the subrequest. With absent/zero +length and a null body, the adapter treats the host-suppressed body as clean empty 205 and +does not call `stream()` or invent a read. It cannot prove whether illegal bytes existed +before workerd suppressed them, so Tier 3 characterizes this behavior and no test claims raw +framing visibility. A non-null body uses the ordinary one-read protocol above. + +The bodyless determination is **method- and status-aware**. Every adapter passes the +originating request method into `OutboundResponse::new`; `OutboundResponse` retains it until +`into_parts`/`into_response`, so a downstream conversion cannot lose HEAD semantics. The +adapter performs the authoritative disposition before constructing the response; final +conversion only rechecks the already-normalized metadata defensively. §5.4 pins tests for +`HEAD 200` (with `Content-Encoding: +gzip` + representation `Content-Length`, no body → passes through, headers preserved, no +502), `1xx`, `204`, clean 205, illegal-body 205, and `304`. Only AFTER this handling do the +decode/cap rules below apply to payload-bearing responses. + +In `Buffered` mode, `max_response_bytes` (default `DEFAULT_MAX_RESPONSE_BYTES = 1 MiB`) +caps the final collected `Body::Once` for every coding disposition. The independent +`max_decoded_response_bytes` cap is measured only in effective-identity or +EdgeZero-decoded gzip/deflate/Brotli output. Every adapter **must enforce that decoded cap +incrementally** and abort as soon as output exceeds it; this closes the decompression-bomb +gap while allowing a caller to set a larger final buffer for an intentionally retained raw +passthrough body. Decoded over-cap → +`Err(EdgeError::response_too_large_with_reason("response body exceeded N bytes", +ResponseLimitReason::DecodedBody))` (the distinct kind, 502 — §3.4.3; NOT +`bad_gateway`, so a consumer classifies it apart from transport). Final collection +over-cap uses `ResponseLimitReason::BufferedBody` instead. + +**Early `Content-Length` rejection is sound ONLY for effective identity (no +`content-encoding`, or exactly one bare `identity` value) responses.** In that case the wire +`Content-Length` *is* the decompressed +size, so a `u64` `Content-Length` above the `u64` cap can be rejected **before buffering** +(cheap, correct). When the response **is** compressed, the wire `Content-Length` is the +*compressed* size, which bounds the decompressed size in NEITHER direction — gzip +typically expands, but incompressible input can make the compressed representation +*larger* than its decoded output — so an early reject on the wire `Content-Length` could +wrongly reject a body whose decompressed size is under the cap. For compressed responses +the decoded cap is therefore enforced **only** incrementally during decompression (above); +the decoded early-reject shortcut is skipped. It is also skipped for unknown/stacked +`Passthrough`, whose bytes may remain encoded. A configured encoded-byte cap is independent +and may reject the same wire `Content-Length` before reading (§3.4.5). (This corrects an +earlier note that implied every wire `Content-Length` over the decoded cap is rejected up +front.) + +**Pre-append check is mandatory.** Outbound bounded drains +(`OutboundResponse::into_bytes_bounded` / `_until` and adapter buffered-response drains) +MUST check the running total against `max` **before** extending the buffer. The comparison +is done in the outbound cap's `u64` type (§3.1.3): convert `usize` lengths with +`u64::try_from`, use checked addition, compare, then extend. A single oversized chunk on a +small cap would otherwise allocate past the limit before erroring. The persistent collected +buffer therefore never exceeds `max`; inbound bounded-drain semantics are owned by the +[inbound body design](2026-08-22-inbound-body-design.md). + +Worst-case **transient** resident memory during a drain is `max + current_chunk.len()`: +the in-flight chunk briefly co-exists with the collected buffer during the check, then +is dropped (over-cap) or appended (under-cap). **`current_chunk.len()` is +source-controlled, not bounded by this spec.** The `8–64 KiB` figure typical sources +yield (`tokio::io` 8 KiB, `hyper` 16 KiB, WASI body reads 64 KiB) is descriptive of the +adapters' incoming stream chunking, not a contract. Three concrete consequences readers +must internalise: + +- **An upstream that yields one large `Bytes` exceeds the typical figure.** A peer + returning a 4 MiB response in a single chunk produces a single 4 MiB in-flight + `Bytes` while the over-cap check runs; if the cap is 1 MiB, the persistent buffer + never grows past 1 MiB but resident memory transiently includes the full 4 MiB + chunk. The check still aborts before any append, but the host did receive 4 MiB. +- **Rechunking is opt-in.** `OutboundRequest::max_chunk_bytes(NonZeroU64)` installs the + §3.4.5 core wrapper on Streamed app-visible output. Unset requests preserve source chunk + boundaries. Rechunking limits emitted item length; it does not prevent the adapter/host + from allocating or yielding one large source chunk, and shared `Bytes` slices may retain + that large backing allocation until the last slice is dropped. +- **The batch model in §3.4.4 inherits the same property.** `Σⱼ current_chunkⱼ.len()` + for actively-draining slots is bounded by what each source yields, not by EdgeZero. + EdgeZero cannot currently provide a hard per-batch ceiling against adversarial source + allocation. Apps that require one must bound fan-out (N) against a documented + provider/upstream frame or chunk ceiling; `max_chunk_bytes` alone is not that guarantee + (§8 risk 11). + +This is a per-call drain bound, **not** a whole-process memory ceiling; the batch-level +bound is `Σ persistent buffers + Σ in-flight chunks` per §3.4.4, with the same +source-controlled caveat on the in-flight term. + +Decoded-output-cap responsibility per adapter: + +- **Cloudflare** — first disables host response decoding in the final fetch options + using `encodeResponseBody: "manual"` (§4.2). Only then can the shared decoder apply + this policy and its independent decoded-output cap. Existing explicit decoding alone does + not prevent workerd from decoding the subrequest body first. +- **Fastly, Spin** — already decompress gzip/deflate/br explicitly today; the cap obligation + applies in-line in their decode paths. +- **Axum** — the workspace `reqwest` dependency is currently + `default-features = false` and does not enable gzip/deflate/brotli decoding. This migration + does **NOT** enable reqwest's `gzip`/`brotli` auto-decoding. Axum inserts + `accept-encoding: identity` only when the normalized request does not already contain + `accept-encoding`; a caller-supplied value is preserved and its response is processed by + the shared decoder. reqwest's built-in decoder matches + **exact lowercase** `content-encoding` values from a single map entry and would not + honour the portable contract (case-insensitive `GZIP`, `identity`, unknown/**stacked**/ + repeated → passthrough untouched). Instead Axum routes the raw response body through + the **same shared `content-encoding` inspection + decoder** the other adapters use + (§3.4.1 policy table), enforcing the independent decoded-output cap incrementally. This is the + only way all four adapters share one decompression contract. + +**Portable `content-encoding` policy for visible values.** The action table is identical on +all four adapters for the values and field structure visible to the guest. Exact treatment +of malformed raw bytes or field lines that workerd has already joined is guaranteed only by +`outbound-header-fidelity`; Cloudflare applies the table to its post-workerd representation. +Every adapter calls the public core `classify_content_encoding(&HeaderMap)` helper and uses +its typed `ContentEncoding::{Brotli, Deflate, Gzip, Identity, Passthrough(reason)}` result. `Identity` +is distinct from `Passthrough` internally even though neither decodes: only `Identity` lets +`enforce_payload_content_length` compare the visible length with the decoded cap before the +first body poll. Unknown/stacked passthrough bytes may still be encoded, so their encoded +cap and, in Buffered mode, raw final-buffer cap are sound; the decoded cap is not. + +| `content-encoding` value | Action | +| --- | --- | +| absent, or `identity` | **Identity delivery** — no codec runs and bytes are delivered as-is, but the independent decoded-output counter applies because these are already representation bytes. `identity` is treated exactly as absent. | +| a single `gzip` | **Decode** one gzip layer; strip `content-encoding` + `content-length`. | +| a single `br` | **Decode** one brotli layer; strip `content-encoding` + `content-length`. | +| a single `deflate` | **Decode** one RFC 1950 zlib-wrapped DEFLATE layer; strip `content-encoding` + `content-length`. Raw RFC 1951 streams without the zlib wrapper are malformed compressed content and fail as `Decode(Deflate)`. | +| one valid unsupported token (`zstd`, `compress`, …) | **Passthrough `Unsupported(token)`**, untouched; preserve the normalized lowercase token for application policy. | +| more than one field, or one valid coding list (`gzip, deflate`, `br, gzip`, …) | **Passthrough `Stacked`**, untouched. | +| invalid token syntax, an empty list element, a parameterized value, or non-UTF-8 bytes | **Passthrough `Malformed`**, untouched. | + +- **Matching is case-insensitive** on the token (`GZIP` == `gzip`) and tolerant of + optional ASCII space/tab surrounding whitespace. The classifier examines every value + returned by `HeaderMap::get_all`; exactly one field is required for `identity`, `gzip`, + `deflate`, or `br`. Two or more fields are `Passthrough(Stacked)`, even if each field + contains the same token. A sole field containing two or more valid comma-separated tokens + is also `Stacked`; an empty element, non-UTF-8 byte, or otherwise invalid token syntax is + `Malformed`, never a decode attempt. One valid unknown token is lowercased and retained in + `Unsupported(token)`. + **`Content-Encoding` is a bare content-coding token + (RFC 9110 §8.4.1) and carries NO `q=` weight** — quality values belong to + `Accept-Encoding`, not `Content-Encoding`. A value bearing any parameter (`gzip;q=0.5`, + `gzip;x=1`) is therefore **not** the bare `gzip`/`br` form: it falls through to + **`Passthrough(Malformed)`**, never decoded. Only the three known bare single-layer forms + decode; every other valid single token or valid stack passes through with its typed reason. +- **A repeated `content-encoding` field** (two header lines) is treated as the stacked + case → passthrough untouched. +- Passthrough here means `max_encoded_response_bytes` still counts the raw transport body + and Buffered `max_response_bytes` caps the raw final collection. The independent + `max_decoded_response_bytes` counter does not run because EdgeZero produced no decoded + representation. +- Rationale: decoding stacked/unknown encodings is unbounded surface for little value on + edge fan-out; failing them hard (`502`) would break apps that can decode a `zstd` body + themselves. Passthrough is deterministic and never worse than "the app got the bytes." + +Whenever an adapter **does** decompress (the three known single-layer cases above), the +`OutboundResponse.headers` it returns MUST have +both `content-encoding` and `content-length` removed — the original values describe +compressed wire bytes and no longer match the app-visible body. This applies in both +`Buffered` and `Streamed` modes: callers must never see decoded bytes alongside stale +compressed metadata. Existing Cloudflare and Fastly proxy code already does this and +the contract codifies it. + +**Streaming-decompressor design (Streamed mode).** Every adapter wraps the raw compressed byte +stream with a **streaming decoder** that emits decompressed chunks as they arrive, +never buffering the full body. Every standard response-egress path then preserves the wrapped +`Body::Stream` lazily. Axum and Cloudflare are `Native` for lazy passthrough; Fastly and Spin +remain `BestEffort` pending the end-to-end target evidence documented in footnotes 6 / 7. The +decoder's *only* +responsibilities are decoding bytes, stripping the two compressed-only headers, and +surfacing decoder errors. The independent `max_decoded_response_bytes` counter wraps +effective identity and known decoded output in both response modes; it is not part of the +codec implementation and never wraps passthrough. `ResponseMode::Streamed` carries no final +collection cap. The encoded-byte counter and Brotli prefix/window/decoder-state charge are +separate upstream layers (§3.4.5): + +1. Pull a raw compressed chunk from the platform stream. +2. Feed it into the decoder; emit whatever decompressed output is currently available + (zero, one, or many output chunks per input chunk). +3. Yield each decompressed chunk through the decoded-output counter. The decoder itself does + no policy accounting. A later optional rechunker may split that item. +4. End successfully only after codec completion **and** verified native-body EOF; + a codec end marker alone is not EOF. Apply the completion protocol below. +5. `content-encoding` and `content-length` are stripped from + `OutboundResponse.headers` at construction time — the wrapper's output bytes are + the new ground truth. + +Every terminal error item is a resource-ownership boundary. Before a response limiter, gzip or +Brotli decoder, native-EOF validator, or framing wrapper yields `Err`, it drops its source and +decoder/native reader state. The caller need not poll the wrapper again or drop it to release the +owned upstream body; the yielded error item is already terminal and resource-free. + +Cap ownership is then unambiguous: + +- **Buffered mode:** the adapter drains the decompressed stream inside the + buffered-drain helper with `max_response_bytes` (per-append-checked, §3.4.1). + Cap fires inside the adapter. +- **Streamed mode + `into_bytes_bounded(max)` / `into_bytes_bounded_until(max, + deadline)`:** the helper's own pre-append check enforces `max` against the + decompressed chunks it pulls from the wrapped stream. Cap fires in the helper. +- **Streamed mode + `into_response()` passthrough (proxy-forward):** no final collection cap is + implied on any adapter. Configured encoded/header/Brotli limits and any configured decoded cap + still apply before the body reaches response egress. Apps that require a final byte cap do + `into_bytes_bounded` first, then re-emit. The capability matrix records transport-boundary + quality separately from whether EdgeZero preserves lazy production. + +**Oversize is a DISTINCT outcome, not a transport error.** When a decoded/encoded/header +cap fires or a Brotli window is refused, the result is +`EdgeError::response_too_large_with_reason(.., reason)`, a distinct +variant/kind, **NOT** `bad_gateway`. The bare `response_too_large(..)` constructor uses +`ResponseLimitReason::Unspecified` for call sites with no narrower origin. It maps to HTTP +**502** on the wire, but its `kind_str()` is `response_too_large`, so a fan-out consumer can +classify policy/resource failure apart from a genuine transport `bad_gateway`; Rust callers +can additionally distinguish the exact resource without parsing the message. Byte-cap +enforcement stays **incremental**. For an identity response, a wire `Content-Length` above +the decoded cap rejects before buffering; a compressed wire length is not comparable to the +decoded cap, while the independent encoded cap may reject it (§3.4.5). Adding +`ResponseTooLarge` and `ResponseLimitReason` follows the same exhaustive-match discipline as +the Phase 1a variants (every `match` arm and test matrix updated; neither reason nor the +variant adds `Retry-After`/`field_path` to the wire). §5.4 pins resource-reason tests and +keeps transport failure distinguishable from every limit outcome. +- Request-body over-cap is `PayloadTooLarge { origin }` (413), kind `payload_too_large`. + `RequestBodyOverflowOrigin::{Inbound, OutboundUpload}` is inspectable in Rust, not serialized. + Every cap uses the fixed public message `request body too large`. Generated overflow responses + suppress `Retry-After` after custom rendering and final egress preparation; this never strips + a surviving retry header from an ordinary upstream 413 or changes config-503 retry policy. + +**Pipeline order — resource and decoder work stay inside the absolute-deadline output +wrapper.** After adapter-visible upstream header limits and bodyless disposition, payload layers compose +in exactly one order: +`platform raw/completion stream → encoded-byte counter → optional Brotli prefix gate → +EdgeError/io::Error carrier bridge → gzip/deflate/br decoder + native-EOF validation → exact carrier +restoration → decoded-byte cap → optional rechunker → absolute-deadline/cancellation wrapper → +consumer`. Unknown/stacked encodings bypass only the prefix/decoder/carrier stages and decoded +cap; they still pass through raw counting, rechunking, and the deadline. The outer deadline sees +every emitted item and terminal result, while adapter-specific ready-input quotas prevent an +inner decoder poll from monopolizing the runtime. Codec completion and native-EOF validation +remain inside this pipeline, never in an unbounded drain after the deadline wrapper ends. +- Each adapter keeps whatever raw-read timer its transport provides. On Axum, Cloudflare, + and Spin, the outer decoded-output wrapper races every `decoded.next().await` against the + absolute deadline and re-checks the deadline after readiness, including terminal EOF and + error. Fastly performs the same pre/post absolute-deadline checks around each blocking + decoded read but cannot preempt that read guest-side. This covers work performed by the + decoder after the final raw read; a converter-only check cannot cover lazy `Streamed` + consumption. Cloudflare additionally applies §4.2's host-event yield quota to raw + decoder input, decoded output, and terminal checks; clock reads alone cannot advance + its production clock during ready-only processing. +- Its timeout chunk is a typed + `EdgeError::gateway_timeout_caused(.., budget.cause)` (504). On timeout it drops the + decoder/raw stream and invokes the adapter's available transport cancellation. Fastly has + no guest timer, so it retains the host raw-read timeout plus cooperative pre/post checks at + this same outer boundary. +- **Precedence at the decoded-stream boundary:** at or after the deadline, timeout wins over + simultaneous success, EOF, raw-read failure, or decoder failure. A malformed-compression + 502 applies only when the decoder produces that error before the deadline. A caller-owned + final collection cap outside this wrapper may legitimately win after the wrapper yielded a + within-deadline chunk: the generic streamed helper does not retain the request deadline, so it + cannot reclassify a cap decision merely because the clock crossed after that yield. Buffered + adapter drains remain + inside the adapter's whole-exchange race, and `into_bytes_bounded_until` rechecks its own + caller-supplied deadline before returning over-cap; those two surfaces preserve + timeout-over-cap precedence for the deadline they actually own. +- §5.4 tests all four: a compressed **stall before first decoded byte**, **mid-stream stall**, + **stall at EOF** → each `gateway_timeout` (504); **malformed compression with no stall** → 502; + and **malformed-compression-vs-timeout precedence** (deadline fires first → 504, not 502). + Separate cap-precedence tests assert the narrower ownership rule above rather than claiming + a universal race after a within-deadline chunk has already been yielded. + +**Implementation hooks (shared helpers).** The async stream decoders for gzip and brotli live in +`edgezero-core/src/compression.rs`; they are core helpers, not adapter-local code. The removed +Spin slice decoder is not retained as an alternate path. The shared helpers' chunk error type is +**`io::Error`**, and that is **not a free choice**: `TryStreamExt::into_async_read` +(which both helpers use to feed the decoder) is hard-bound to +`Self: TryStreamExt` (futures-util `try_stream/mod.rs`). A +decoder input stream therefore **cannot** simply be re-typed to `EdgeError` — that does +not compile — and naively mapping `EdgeError -> io::Error` on the way in would collapse a +`gateway_timeout` (504) into the decoder's generic 502 outcome, the exact bug §3.4.1 +forbids. Their existing stop-at-first-decoder-EOF loops also need the following +completion correction; reusing those loops unchanged is not conformant. + +**Codec completion is not native-body completion.** A gzip representation can contain +concatenated members within one content-coding layer +([RFC 1952 §2.2](https://www.rfc-editor.org/rfc/rfc1952.html#section-2.2)). The shared helpers +must preserve all decoded payload and observe late source errors before reporting success: + +- **Gzip:** enable `GzipDecoder::multiple_members(true)` in exact manifest-pinned + `async-compression` 0.4.43. Decode every member in order under the same absolute + deadline, including empty members. Any configured decoded-byte cap counts cumulatively + across members in the separate output wrapper; no cap enters the decoder. + Member boundaries reset neither budget. A truncated/corrupt later member or trailing bytes + that are not a valid member are `bad_gateway` reason `Decode(Gzip)`; stacked content-coding + passthrough is unchanged. +- **Brotli:** EdgeZero accepts one complete Brotli stream for a single `br` coding. + After decoder completion, recover its buffered reader and inspect both unread buffered + bytes and subsequent source input. Any trailing non-empty bytes, including a second + Brotli stream, are `bad_gateway` reason `Decode(Brotli)`; never silently discard read-ahead bytes + when recovering the reader. Empty source items are not EOF. Continue completion checks until native EOF + or failure, inside the same deadline wrapper and Cloudflare yield discipline. +- **Both:** a late source `EdgeError` still passes through the exact carrier restoration + below. A source that stalls after the codec end marker remains pending until the + adapter's deadline enforcement yields attributed 504, with Fastly's documented host + gaps unchanged. The outer wrapper checks terminal readiness before exposing EOF/error; + timeout precedence and the existing cap-ownership rule remain unchanged. Do not drain + the remaining input after a cap/error/timeout just to reach EOF; abort/drop it instead. +- Successful decoded-stream EOF requires native EOF and the adapter's required + trailer/completion outcomes. Keep the underlying reader and cancellation/completion + ownership until success or early termination; no terminal result may swallow unread + input or a pending native error. Cloudflare disarms its abort guard only after this + success, and Spin reports caller-result success only after the response protocol and + decoding have succeeded. Already-yielded Streamed bytes remain delivered, but a later + failure must still surface as an error chunk rather than false clean EOF. + +**The bridge (compile-verified).** Carry the typed error *through* the `io::Error` +boundary instead of converting it. On input, wrap each inner `EdgeError` in a private +carrier: `stream.map_err(|error| io::Error::other(Carried(error)))`. On output, first +capture the `io::Error` diagnostic, then inspect `into_inner()` and restore the original +`EdgeError` only when the boxed source downcasts exactly to `Carried`. A missing or failed +downcast is the decoder's own failure and maps to +`EdgeError::bad_gateway_with_reason(.., BadGatewayReason::Decode(coding))`, where the +helper supplies `Gzip` or `Brotli`, while preserving the captured diagnostic. The restored +stream is then wrapped by the +decoded-output deadline guard above, so a lazy stream checks every yield and terminal EOF. + +All adapter outbound clients call these existing core helpers. The wrapper runs incrementally on +every target (§3.4.1), never as a whole-body decode. When an outbound response is forwarded, each +response-egress coordinator continues polling that same portable stream lazily. + +In `Streamed` mode a configured `max_decoded_response_bytes` is enforced incrementally on +effective identity and EdgeZero-decoded gzip/deflate/Brotli output before each item reaches the app. +If that optional policy is unset, there is no default decoded-output cap. A caller may still +apply a distinct final collection cap later with +`OutboundResponse::into_bytes_bounded(max)`. The independent encoded, header, and +Brotli-window/decoder-state limits in §3.4.5 also apply before or during streamed delivery. +The bounded collection method does **not** delegate to +`Body::into_bytes_bounded` directly — `Body::into_bytes_bounded` maps over-limit to +`payload_too_large` (413), correct for the inbound body case but wrong for an over-large +upstream response. `OutboundResponse::into_bytes_bounded` performs its own bounded +drain and maps over-cap to **`response_too_large`** (distinct kind, 502 — §3.4.1; NOT +`bad_gateway`, so a consumer classifies it apart from transport). On adapters that +decompress, the cap is enforced against decompressed output here too. + +#### 3.4.2 Inbound dependency boundary + +Inbound `RequestContext::into_request` semantics are owned by the dedicated +[inbound body design](2026-08-22-inbound-body-design.md). This outbound spec requires only +that `OutboundRequest::from_request` preserve the source method, normalized headers, and +whatever buffered or streamed `Body` the core request supplies; it does not deliver or test +the inbound body state machine, extractor limits, or adapter ingress buffering. +#### 3.4.3 New `EdgeError` variants & mapping + +New variants extend the non-exhaustive enum. Exact field construction/matching of existing +variants is a hard-cut API change, not a compatibility guarantee. + +```rust +// crates/edgezero-core/src/error.rs +// Phase 1a lands the first TWO variants (needed for deadline/transport mapping). +// BadGateway carries a typed reason so consumers never classify retries or diagnostics by +// parsing a provider-specific message. `Unspecified` preserves the ergonomic bare +// constructor and is used when a platform exposes no narrower cause. +EdgeError::BadGateway { message: String, reason: BadGatewayReason, provider_detail: Option } // -> 502 +// GatewayTimeout carries a TYPED `cause`, NOT just a message: consumers can observe +// which configured budget input selected the effective deadline without parsing strings +// (§3.3.2). This does not say which physical timer fired or prove that input elapsed. +// **Phase 1a MUST land this shape** (the `BudgetSource` enum + +// the field), even though its producer `dispatch_budget` is Phase 1b — freezing +// `GatewayTimeout { message }` now would bake in a variant the master contract can't use, +// forcing a breaking change later. `inner()` is still `None` (a `cause` is not a source error). +EdgeError::GatewayTimeout { message: String, cause: BudgetSource, provider_detail: Option } // -> 504 +// The OUTBOUND response-handling phase adds a THIRD, so response-cap over-run is a +// distinct machine-classifiable outcome, NOT collapsed into `bad_gateway` (§3.4.1): +EdgeError::ResponseTooLarge { message: String, reason: ResponseLimitReason } +// -> 502, kind "response_too_large" +// Request byte caps have a separate fixed-message error; the origin is not serialized. +EdgeError::PayloadTooLarge { origin: RequestBodyOverflowOrigin } +// -> 413, kind "payload_too_large" + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BadGatewayDecodeReason { Brotli, Deflate, Gzip, Json } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BadGatewayReason { + Decode(BadGatewayDecodeReason), + ImageOptimizerUnavailable, + Protocol, + Transport, + Unreachable, + Unspecified, +} + +// `BudgetSource` is defined once in §3.3.2 and `error.rs`; use that canonical definition +// here. Its variants are `BatchCutoff`, `Default`, `PerCallTimeout`, `RequestDeadline`, and +// `Unspecified`. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum RequestBodyOverflowOrigin { Inbound, OutboundUpload } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ResponseLimitReason { + BrotliWindow, + BufferedBody, + DecodedBody, + DecoderMemory, + EncodedBody, + HeaderBytes, + HeaderCount, + Unspecified, +} + +pub fn bad_gateway(message: impl Into) -> Self; +pub fn bad_gateway_with_reason( + message: impl Into, + reason: BadGatewayReason, +) -> Self; +pub fn gateway_timeout(message: impl Into) -> Self; // cause = Unspecified +pub fn gateway_timeout_caused(message: impl Into, cause: BudgetSource) -> Self; +pub const fn payload_too_large(origin: RequestBodyOverflowOrigin) -> Self; +pub const fn request_body_overflow_origin(&self) -> Option; +pub fn response_too_large(message: impl Into) -> Self; // reason = Unspecified +pub fn response_too_large_with_reason( + message: impl Into, + reason: ResponseLimitReason, +) -> Self; +``` + +`Unreachable` is reserved for a failure before any upstream response head is available: +DNS resolution, connection establishment/refusal, or TLS establishment. `Transport` covers +an established exchange failing during request upload, response body/trailer reads, native +completion, or a later connection termination. If a provider does not expose enough phase +information, use `Unspecified`; never guess from message text. EdgeZero-owned decoders always +use `Decode(BadGatewayDecodeReason::{Brotli,Deflate,Gzip,Json})` as applicable. Provider-native decoding +remains `BadGatewayReason::Unspecified` unless the adapter can establish the coding without +parsing a diagnostic string. + +`EdgeError::status()` gains `BadGateway => 502`, `GatewayTimeout => 504`, and (outbound +phase) `ResponseTooLarge => 502` with `kind_str() == "response_too_large"`. Like the other +two it carries no `Retry-After` and no `field_path`. `BadGatewayReason` and +`ResponseLimitReason` are Rust-side fields and are not serialized; the JSON envelope stays +`status`/`kind`/`message`. The message at that HTTP boundary is category-only: +`BadGateway` emits `"bad gateway"`, `GatewayTimeout` emits `"gateway timeout"`, +`ResponseTooLarge` emits `"upstream response exceeded configured limits"`, and `Internal` +emits `"internal server error"`. `EdgeError::message()` and `Display` remain internal +diagnostic surfaces, but adapters never interpolate a provider error, URL, query, token, or +response content into an outbound diagnostic. Tests use token-bearing fixtures to prove +`IntoResponse` cannot serialize those values. Each addition follows the same +exhaustive-match discipline (every `match` arm + test matrix updated, alphabetical +insertion); Phase 1a does the first two plus `BadGatewayReason`, and the outbound work does +the third plus `ResponseLimitReason` in the same mechanical style. + +The SDK diagnostic checkpoint adds optional `ProviderFailureDetail` directly to `BadGateway`, +`BadRequest`, `GatewayTimeout` and `Internal`. Semantic constructors default to no detail; the +explicit `*_with_detail` constructors and `provider_failure_detail()` retain/read fixed-size, +text-free adapter/phase/cause evidence. Exact field patterns must migrate; no wrapper variant or +old-shape alias is provided. SDK send/body boundaries on all four adapters, Fastly backend/upload +I/O, and Spin request completion project only closed causes, numeric codes and I/O kinds. Provider +text is never copied into this field. Existing error text, status, kind, typed reason and wire +envelopes remain unchanged, including host-rejected request errors that already mapped to 400. + +`Capability::OutboundFailureDiagnostics` (`outbound-failure-diagnostics`) publishes this existing +SDK-boundary projection: Fastly/Spin are Native; Axum/Cloudflare are BestEffort. Native preserves +typed causes/codes exposed by the pinned SDK, not information hidden by its host. Viceroy can +expose only an internal cause where hosted Fastly may supply a connection-refused cause. Axum's +reqwest predicates/optional I/O kind and Cloudflare's allowlisted exception names do not promise +complete network cause attribution. Catalogs live in `edgezero_core::error_detail`; opaque causes +remain Unknown and absent codes stay absent. Framework provider-detail logging, if added, must +be debug or lower; currently it is application-owned. Publication does not close full EZ-16/P1, +failed Fastly G2, consumer acceptance or hosted qualification, or alter other capability levels. + +An existing deadline check can still replace a ready error with its selected timeout, but preserves +the observed error's diagnostic field. Pure timer wins, successful operations observed late and +clean EOF carry no invented cause. Core bounded collection follows the same rule without changing +its cooperative deadline behavior. Raw Fetch failures use only an allowlisted exception name; +opaque/text-only failures remain Unknown and never become timeouts solely because of that name. +The preparation checkpoint adds `RequestPreparation` for Cloudflare request headers/construction, +raw Fetch setup exceptions, and Spin request-resource setters. The phase names the observed API +boundary, not whether request bytes were sent; SDK send failures retain Send, including reqwest +builder failures. Cloudflare constructor exception types erased by its SDK remain Unknown, while +the original internal source is retained. A false manual-encoding setting result is not a thrown +exception and gains no invented exception detail. Shared response-header conversion is unchanged. +Spin records all five HeaderError categories, the rejected `SpinTimeoutOption`, and explicit +component rejection from unit-returning target setters without fabricating an ErrorCode. Neither +Other strings nor local validation failures become provider evidence, and NotSupported timeout +options still succeed. + +The source-observer checkpoint adds `ResponseEgressReport::source_failure`, an optional, text-free +`ResponseEgressSourceFailure` snapshot shared unchanged by per-response completion and the global +observer. Its getters expose the existing static source-error category, copied optional +`ProviderFailureDetail`, and observation-time body kind. Recording is first-wins even when provider +detail is absent, never allocates or samples the clock, and cannot change a terminal attempt. +All four converters record an observed body-source error before existing post-read deadline or +cancellation arbitration, including zero-length validation and EOF lookahead where those paths +poll the source. Cloudflare and Spin record inside their existing source-read race, before it can +discard a ready error. Terminal precedence, byte accounting, source polling and delivery ownership +are unchanged. An application observation keeps its original tag through fallback delivery; +successful delivery of a rendered application error is not a source failure. Unobserved errors, +clean EOF and transport-only failures carry no invented snapshot. Report field construction is a +hard cut. + +The framing checkpoint exposes the existing six `ResponseEgressFramingError` rules through +`ResponseEgressReport::framing_failures.application()` and `.fallback()`. Two fixed optional slots +retain the first actual precommit normalization rejection per body kind. The shared attempt-owned +normalization wrapper is used by `begin()`, all four fallback preparers and Axum's empty-stream +revalidation. It logs the same enum's fixed text at error level and returns the original error; +header values, body bytes and provider text are excluded. Recording/logging are initial-only; +writing/terminal calls still return the unchanged normalization result. Metadata capture adds no clock read, +source poll, callback or allocation; existing normalization and backend-dependent logging costs +remain. Policy panics, deadline normalization, SDK head rejection and postcommit source failures +gain no invented framing reason. Existing streamed length errors retain their source categories. +`EdgeError::response_framing_error()` downcasts an already-owned internal source without changing +Display or wire rendering. Exact report construction must supply the new carrier, with its empty +default when no rejection was observed; no compatibility shape is provided. Stock fallbacks cannot +currently violate these rules, so direct fallback-tagged normalizer tests are not adapter-invalid- +fallback evidence. Diagnostic capability publication, consumer acceptance and hosted qualification +remain tracked by F6a/F6c; no capability promotion is made. + +| Condition | `EdgeError` | HTTP status | +| --- | --- | --- | +| Invalid outbound URI (relative / no authority / bad scheme) | `bad_request` | 400 | +| EdgeZero-owned inbound/request-upload byte-cap overflow | `payload_too_large`, typed `RequestBodyOverflowOrigin` (not serialized) | 413 | +| Upstream unreachable before a response head (DNS / connect / TLS establishment / connection refusal) | `bad_gateway`, reason `Unreachable` when the adapter can establish it, else `Unspecified` | 502 | +| Outbound transport failure after connection progress (request upload, response read, trailer/completion, connection termination) | `bad_gateway`, reason `Transport` when the adapter can establish it, else `Unspecified` | 502 | +| Outbound protocol/framing/completion failure | `bad_gateway`, reason `Protocol` | 502 | +| Outbound response over a body/header/window resource limit | `response_too_large`, typed `ResponseLimitReason` (§3.4.5) | 502 | +| Outbound response body not valid JSON, gzip, or Brotli / `json::` called on a streamed body | `bad_gateway`, reason `Decode(Json/Gzip/Brotli)` for malformed content and `Protocol` for invalid API state | 502 | +| Outbound per-request timeout or request deadline exceeded | `gateway_timeout` (carries `budget.cause`: per-call vs request deadline — §3.3.2) | 504 | +| Batch observation cutoff reached | collection terminates as `Cutoff`; unresolved slots are `None`; no synthetic terminal slot error is emitted | n/a | +| Malformed or explicitly failed batch driver | `OutboundBatchFailure` with the precise batch-level `Internal` and all previously collected slots; never represented as `Cutoff` | 500 | +| Outbound completed with a non-2xx status | **not an error** — `Ok(OutboundResponse)` | app decides | + +The non-2xx rule is load-bearing: a target returning 204/400/500 is a normal fan-out batch +outcome, not a transport error. + +#### 3.4.4 Batch memory model (explicit) + +`send_all_until` does not impose a global allocation ceiling. Its logical payload accounting has +two parts — +a **persistent collected buffer** term that holds the request payloads and the +buffered response payloads, plus a **transient in-flight chunk** term that +briefly co-exists with the collected buffer per actively-draining slot (per +§3.4.1's pre-append checked accounting, the in-flight chunk is held during the +overflow check before being appended or dropped): + +``` +persistent collected buffer = Σᵢ request_bodyᵢ.len() + + Σᵢ max_response_bytesᵢ (send_all_until is buffered-only) + +transient in-flight chunks = Σⱼ current_chunkⱼ.len() + // j ranges over slots + // currently inside a drain + // step; typically 8-64 KiB + // per active slot + +worst-case LOGICAL PAYLOAD bytes = persistent + transient +// This is NOT a complete core-allocation or process-RSS bound. It EXCLUDES: +// `Vec`/`BytesMut` spare capacity (amortised growth over-allocates), shared `Bytes` backing +// allocations not yet freed, gzip decoder state, Brotli state other than the separately +// bounded source-audited decoder-state charge, and allocator overhead/fragmentation. During active Brotli +// decodes, add up to each slot's `max_decoder_bytes` to this logical payload number; +// even that sum remains narrower than RSS. Actual RSS also includes adapter/host buffering +// and every opaque term listed below (§ send_all_until rustdoc). + +// Equivalently, when all slots share the same response cap, the persistent term is: +// Σᵢ request_bodyᵢ.len() + N × max_response_bytes +// — but the precise sum is over the per-slot caps, not a single N × max. +// Heterogeneous caps (mix of `.max_response_bytes(small)` and unset slots) bound +// the persistent term by Σᵢ instead of N × max(capᵢ). +``` + +`send_all_until` rejects streamed request bodies and streamed responses in preflight +(§3.1.1), so a Streamed-mode batch memory model does not exist. Single `send` +with `Streamed` is the path for lazy bodies, where memory is bounded by the +streaming chunk buffer plus whatever the consumer chooses to buffer via +`into_bytes_bounded`. + +This arithmetic is not an admission-safe process/isolate RSS bound. Informational response +blocks that a host consumes before exposing the final response, parser tables, provider-side +field sections, trailers not exposed to the guest, native receive-chunk allocation, shared +backing storage, `BytesMut` spare capacity, allocator metadata/fragmentation, task stacks, +and host copies can all exist outside it. When an adapter exposes informational fields or +trailers to guest code, it applies the configured header count/byte totals cumulatively +across every exposed field section and final completion; it never claims that this prevents +an earlier host allocation. When those fields are not exposed, their allocation is opaque. +The `outbound-complete-resource-accounting` capability is therefore `Unsupported` for every +current adapter. This is a deliberate fail-closed statement, not a reason to weaken the +narrower limits below. + +EdgeZero's contract — **persistent** (post-append, retained) vs **transient** +(in-flight, dropped after the cap check): + +- **Per-response (Buffered).** *Persistent* memory — the collected buffer — is bounded + by `max_response_bytes`. *Transient* worst-case core-owned payload during a drain is + `max_response_bytes + current_chunk.len()`, where `current_chunk.len()` is + source-controlled (§3.4.1). The post-check buffer never exceeds `max_response_bytes`. +- **Batch (N)** memory is the app's responsibility: the app must bound the number of + requests passed to `send_all_until`. Both terms add up — *persistent* is + `Σᵢ request_bodyᵢ.len() + Σᵢ max_response_bytesᵢ` (`request_bodyᵢ` and + `max_response_bytesᵢ` denote slot `i`'s buffered request body length and its + per-request response cap respectively); *transient* adds + `Σⱼ current_chunkⱼ.len()` over actively-draining slots, source-controlled, plus up to + `Σₖ max_decoder_bytesₖ` over actively decoding Brotli, gzip, or deflate slots. This remains a set of + enforceable guest terms rather than a complete memory bound. + For typical fan-out workloads this is intrinsic — `N` is the fixed, configured target count and + target responses are small JSON. The spec deliberately does **not** add a + `max_concurrency` knob: on Fastly all requests must be in-flight at once for + fan-out to work, so throttling concurrency would defeat the feature. This + requirement is documented in the `send_all_until` rustdoc and in `docs/`. The optional + `max_chunk_bytes` wrapper shapes app-visible items but does not remove the source-chunk + term from this model (§3.4.5). + +#### 3.4.5 Response resource limits + +`ResponseMode::Buffered { max_bytes }` (set by `max_response_bytes`) is the final collection +cap for every Buffered response, including raw passthrough. It is not the decoded-output +policy. Eight independent controls cover transport bytes, identity/decoded output, decoder +heap requests, field metadata, and Streamed item shape. This separation permits, for +example, a 4 MiB decoded limit with a 16 MiB final buffer for an intentionally retained raw +passthrough representation: + +| Builder | Default | Measures | Typed overflow/rejection reason | +| --- | --- | --- | --- | +| `max_decoder_bytes(u64)` | `DEFAULT_MAX_DECODER_BYTES = 32 MiB` | conservative decoder-state charge for Brotli, gzip, or deflate, checked before decoder construction | `ResponseLimitReason::DecoderMemory` | +| `max_encoded_response_bytes(u64)` | unset | cumulative guest-visible body bytes before content decoding | `ResponseLimitReason::EncodedBody` | +| `max_decoded_response_bytes(u64)` | unset | cumulative identity or EdgeZero-decoded gzip/deflate/Brotli output; never raw passthrough | `ResponseLimitReason::DecodedBody` | +| `max_response_header_bytes(u64)` | unset | sum of each adapter-visible upstream field name length plus value length before normalization, plus EdgeZero's synthetic `x-edgezero-proxy` marker | `ResponseLimitReason::HeaderBytes` | +| `max_response_header_count(u64)` | unset | adapter-visible upstream name/value entries before normalization, including repeated values, plus EdgeZero's synthetic `x-edgezero-proxy` marker | `ResponseLimitReason::HeaderCount` | +| `max_brotli_window_bits(u8)` | `DEFAULT_MAX_BROTLI_WINDOW_BITS = 24` | advertised Brotli window bits, valid configured range `10..=30` | `ResponseLimitReason::BrotliWindow` | +| `max_chunk_bytes(NonZeroU64)` | unset | each app-visible `Body::Stream` item after decode or passthrough | no overflow; items are split lazily | +| `max_response_bytes(u64)` | `DEFAULT_MAX_RESPONSE_BYTES = 1 MiB` | final bytes collected into `Body::Once`, regardless of coding disposition | `ResponseLimitReason::BufferedBody` | + +All byte/count fields and running totals use checked `u64` arithmetic. A Brotli limit outside +`10..=30` is caller configuration and fails dispatch as `bad_request`; zero chunk size is +unrepresentable through `NonZeroU64`. Unset optional controls preserve existing behavior. +These are per-response controls, not aggregate batch limits. + +**Enforcement order and precedence:** + +1. At the earliest adapter boundary where an upstream response field section is exposed, count + entries and `name.as_bytes().len() + value.as_bytes().len()` for every visible pair + before normalization, duplicate coalescing by EdgeZero, or additional collected header + state. Fields that normalization later strips still count. Maintain one cumulative count/byte + total across every adapter-visible informational block, the final response headers, and + adapter-visible trailers; a later field section can + therefore fail the returned body/completion stream with the same typed reason. + Increment and check the entry count before adding/checking bytes for that entry, so if + the same field crosses both configured limits, `HeaderCount` wins. Reject on the first + exceeded configured limit; never truncate. Cloudflare can measure + only workerd's already-materialized strings and merged non-`set-cookie` entries. No + adapter claims these limits prevent provider/SDK allocation, parsing of an informational + block or trailer the SDK does not expose, or a host-side rejection that occurs before + metadata reaches the guest. After normalization, account EdgeZero's synthetic + `x-edgezero-proxy: ` field against the same cumulative limiter before inserting it. +2. Apply bodyless/205 disposition. Framing-bodyless responses do not consume a body or + invoke body/window limits. A visible normalized `Content-Length` greater than a configured + encoded cap rejects a payload-bearing response before reading, including compressed + responses. Identity responses independently reject against the decoded cap and, in + Buffered mode, the final-buffer cap. Passthrough responses compare their raw length to + the encoded cap and, in Buffered mode, the final-buffer cap, but never to the decoded cap. + A compressed response that EdgeZero will decode cannot compare wire `Content-Length` to + either output cap. For a + payload-bearing response or 205, malformed, comma-list, or conflicting repeated visible + `Content-Length` is `bad_gateway` reason `Protocol` before body reads. HEAD/304 preserve + representation metadata without interpreting it as a body promise. +3. Count every guest-visible raw body item before handing it to the content decoder or + passthrough stream. The encoded total is cumulative across gzip members and native + completion reads. Check before forwarding/appending. On raw overflow, abort/drop the + owned native response and return/yield `response_too_large_with_reason(.., + ResponseLimitReason::EncodedBody)`. +4. For a single `br` coding, parse only the fixed-size stream prefix needed to determine + WBITS **before constructing the decoder or allocating its state**, then replay those + bytes unchanged. The parser handles both the standard one-byte encoding and the pinned + decoder's two-byte large-window extension (10 through 30 bits); the extension cannot + bypass the configured cap. A valid over-limit value is `BrotliWindow`; malformed or + truncated prefix syntax is `bad_gateway` with reason `Decode(Brotli)`. +5. Before decoder construction, compute + `brotli_decoder_memory_charge(WBITS) = BROTLI_DECODER_FIXED_CHARGE_BYTES + 2^WBITS` + with checked `u64` arithmetic. `BROTLI_DECODER_FIXED_CHARGE_BYTES` is 16 MiB and covers + the pinned Rust decoder's non-ring state; `2^WBITS` charges its maximum ring window. The + normal dependency graph is pinned exactly to `alloc-no-stdlib 2.0.4`, `alloc-stdlib 0.2.4`, + `async-compression 0.4.43`, `brotli 8.0.4`, `brotli-decompressor 5.0.1`, + `compression-codecs 0.4.38`, and `compression-core 0.4.32`. Core constructs + `async_compression`'s reader with the explicitly + pinned `compression_codecs::BrotliDecoder`, and CI runs + `scripts/check_brotli_dependency_contract.mjs` against both root and demo lockfiles. The + checked-in [decoder memory audit](../../audits/2026-09-27-brotli-decoder-memory-accounting.md) + derives a 3,442,530-byte allocation-payload subtotal for that graph, including the ring + allocation's 66-byte write-ahead/dictionary slack beyond `2^WBITS`. The 16 MiB fixed charge + is a conservative accounting reservation with explicit headroom for wrapper and allocator + overhead; it is not represented as an exact total-heap bound. A core regression test checks + compiled type and EdgeZero buffer sizes, while CI pins every audited normal dependency by + version, registry source, and checksum to protect private table, grammar, and layout + assumptions. Allocation measurements may supplement but never replace this source proof. A + dependency upgrade must repeat the audit and may raise the constant; it may not retain the old + charge on test evidence alone. Reject with + `DecoderMemory` before constructing the decoder when the charge exceeds + `max_decoder_bytes`. The default 32 MiB cap therefore admits the default WBITS 24 + and rejects a raised window unless the caller also raises the memory policy. This is a + conservative reservation charge, not a claim that the process reserves or uses exactly + that many bytes. It excludes allocator metadata, fragmentation, output/body buffers, + task stacks, adapter/SDK buffers, and host RSS; `outbound-complete-resource-accounting` + remains Unsupported because those terms are not portable (§3.5.2). +6. Apply `max_decoded_response_bytes` only to effective identity and EdgeZero-decoded + gzip/deflate/Brotli output. Unknown, parameterized, stacked, or otherwise passthrough encodings + bypass this decoded counter. The encoded counter still applies to every path. +7. For Streamed output apply the optional rechunker, then wrap the stream in the adapter's + absolute deadline/cancellation owner. For Buffered output, collect under + `ResponseMode::Buffered { max_bytes }`; that final cap applies to identity, decoded, and + passthrough bytes and reports `BufferedBody`. + The rechunker preserves byte order, error order, cancellation ownership, and absolute + deadline/fairness checks. It pulls lazily and emits items no larger than the configured + value. It does not alter a final Buffered body. Splitting a `Bytes` value may retain its + original backing allocation; copying can temporarily coexist with it. Therefore + `max_chunk_bytes` is an item-shape guarantee, not a source-allocation or RSS guarantee. + +For adapter-owned decisions inside the exchange wrapper, if the request deadline is already +expired when a resource decision becomes ready, the attributed `gateway_timeout` wins. +`into_bytes_bounded_until` applies the same rule to its caller-supplied deadline. The generic +`into_bytes_bounded` helper intentionally retains no deadline; after it receives a chunk that +the stream yielded within budget, its decoded cap can therefore win even if wall clock crosses +the original request deadline before the helper finishes accounting (§3.4.1). Otherwise +pipeline order breaks simultaneous resource ties: header limits precede body limits; encoded +bytes precede decoder/window/decoded bytes. All early termination follows each adapter's +existing native cleanup and completion protocol. + +### 3.5 Capability declaration + +#### 3.5.1 Manifest section + +```toml +# edgezero.toml +[capabilities] +# This strict declaration intentionally rejects Fastly: both of these cells are +# BestEffort there. A portable four-adapter app declares `outbound-http` optional and +# accepts Fastly's documented dynamic-backend service prerequisite. +required = ["outbound-http", "outbound-deadlines"] + +[capabilities.outbound] +# Optional plumbing. OMITTING this field is NOT the same as `["*"]`: +# - field absent → https-only default `["https://*:*"]` (no cleartext) +# - hosts = ["*"] → explicit opt-in to BOTH http and https +# So an existing manifest that never declared hosts keeps its https-only posture. +hosts = ["*"] +``` + +```rust +// crates/edgezero-core/src/manifest.rs — defined INLINE here, NOT in a separate +// capability.rs. `manifest.rs` is textually `include!`d by edgezero-macros +// (manifest_definitions.rs), and edgezero-core depends on edgezero-macros, so the +// macro crate can neither see `edgezero_core::` paths nor add core as a dep (cycle). +// A separate `capability.rs` that `manifest.rs` imports would fail to compile in the +// macro crate. Core re-exports these: `pub use manifest::{Capability, CapabilitySupport};`. +// +// MUST derive Serialize as well as Deserialize: `Manifest` derives `Serialize` and +// `app!` calls `serde_json::to_string(&manifest)` — a Deserialize-only capability type +// breaks the `Manifest` derive. (Both facts verified against the current tree.) + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] // a future capability must not break out-of-tree adapters +// Outbound-only excerpt. The complete shared enum also contains config, ingress, and +// response-egress cells specified by their owning designs. Declaration order follows the +// repository's alphabetical-item lint. +pub enum Capability { + LazyStreamedResponsePassthrough, // downstream response chunks flow without + // collecting the whole body. Cloudflare is + // Native alongside Axum; Fastly/Spin are BestEffort. + OutboundAuthorityOverride, // wire HTTP authority can differ from connection/TLS + // identity without retargeting the request. + OutboundBatchCancellation, // unresolved observation can stop and invokes the + // adapter's strongest documented teardown. + OutboundBatchCompletionOrder, // indexed terminal slots are observable as they finish. + OutboundBatchSlotIsolation, // sibling timing cannot change the result a slot + // would have produced in isolation. + OutboundCacheBypass, // bypasses adapter-managed intermediary response cache + // without synthesizing origin-visible headers. + OutboundCompleteResourceAccounting, // every response allocation from host parsing + // through guest delivery has a portable bound; + // Unsupported on all current targets. + OutboundDeadlines, // one exchange budget: connect, headers, + // buffered body, and streamed body yields. + // Cross-slot selection/drain delay is owned by + // OutboundBatchSlotIsolation. + OutboundFailureDiagnostics, // text-free cause/phase and available numeric SDK detail; + // Fastly/Spin Native, Axum/Cloudflare BestEffort. + OutboundFlexiblePhaseBudget, // the total budget is one elastic pool rather + // than a rigid provider-specific phase split. + OutboundHeaderFidelity, // request field lines survive transport conversion; + // raw response-header octets and original field-line + // boundaries reach security-sensitive normalization. + OutboundHttp, // adapter supplies an outbound HTTP dispatch path; + // support level and documented prerequisites determine + // whether a selected deployment may rely on it + StreamedUploadDeadlines, // can preempt a stalled request-body source/write; + // Fastly and Spin are BestEffort. +} + +impl Capability { + pub fn as_str(&self) -> &'static str; // kebab-case, for messages +} + +// Also inline in manifest.rs (see Capability note above). `Serialize` is needed only if +// it ever appears in a serialized manifest field; it does not today (support is computed +// per-adapter via `Adapter::capability`, not stored), so `Deserialize`/`Serialize` are +// omitted here. If a future manifest field carries a `CapabilitySupport`, add both. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum CapabilitySupport { + /// Implemented, but with a documented limitation or deployment prerequisite that the + /// matrix footnotes describe. A prerequisite that static adapter metadata cannot prove + /// may make the path unavailable in a particular deployment; applications declaring the + /// capability optional explicitly accept that runtime failure mode. The limitation can + /// otherwise be timing-related (unbounded cooperative + /// enforcement, e.g. Fastly source-stream-stall in + /// `streamed-upload-deadlines`) **or observational** (the adapter preserves lazy delivery but + /// cannot prove end-client acceptance, e.g. Axum `lazy-streamed-response-passthrough`). + /// `BestEffort` therefore means + /// "implemented, with a real-world deviation or prerequisite you need to read the + /// footnote to understand" — not specifically "unbounded cooperative timing." + BestEffort, + /// Real enforcement with a precisely documented, deterministic bound on any + /// deviation. No current outbound matrix cell uses this level; it remains in + /// the support ladder for future capabilities with a true end-to-end bound. + BoundedCooperative, + /// Fully supported with no documented caveats. + Native, + /// Not available. + Unsupported, +} +``` + +The capability is named **`outbound-deadlines`**, not `timers`, and describes the +wall-clock budget contract for one outbound HTTP exchange. The matrix support level says +whether that contract is Native or has a documented BestEffort gap. It makes no claim +about timing arbitrary guest computation (which EdgeZero does not offer — §3.3.5). + +```rust +// crates/edgezero-core/src/manifest.rs — new field on Manifest. +// +// MUST derive Serialize as well as Deserialize: `Manifest` derives `Serialize` +// (manifest.rs) and `app!` calls `serde_json::to_string(&manifest)`. A +// Deserialize-only member breaks `Manifest`'s derive and therefore edgezero-core +// AND the macro crate (which textually `include!`s this file). Same for every +// nested type and for `Capability` itself. +// `deny_unknown_fields` is REQUIRED — without it a typo like `require = [..]` or +// `host = [..]` is silently ignored, disabling enforcement or invoking the broad default +// (fail-open). The `#[validate(custom = ..)]` attaches the disjoint/duplicate check. +// `#[non_exhaustive]` matches the existing manifest-struct precedent and keeps future field additions +// non-breaking for out-of-tree code and composes fine with `Default` + `Deserialize` +// (these are built by deserialization / `..Default::default()`, never external literals). +#[derive(Debug, Default, Serialize, Deserialize, Validate)] +#[serde(deny_unknown_fields)] +#[non_exhaustive] +#[validate(schema(function = "validate_capabilities_disjoint"))] +pub struct ManifestCapabilities { + #[serde(default)] + pub required: Vec, + #[serde(default)] + pub optional: Vec, + #[serde(default)] + #[validate(nested)] + pub outbound: ManifestOutboundCapability, +} +// `validate_capabilities_disjoint(&ManifestCapabilities) -> Result<(), ValidationError>` +// rejects a capability DUPLICATED within `required` or `optional`, or listed in BOTH +// (required ∩ optional must be empty — asking for a capability as both is a +// contradiction). Every manifest parse path rejects these failures; baked input becomes +// `Malformed`. §5.4 tests: an unknown field (typo `require`), a duplicate, and a +// required∩optional overlap each fail. + +#[derive(Debug, Default, Serialize, Deserialize, Validate)] +#[serde(deny_unknown_fields)] +#[non_exhaustive] +pub struct ManifestOutboundCapability { + /// Outbound host plumbing. + /// + /// **`Option>`, NOT `Vec` — this is security-relevant.** + /// The renderer defines an explicit `"*"` as **http + https**, while an + /// **absent** field must preserve today's **https-only** default + /// (`["https://*:*"]`). A bare `Vec` with `#[serde(default)] = ["*"]` collapses + /// those two cases, so **every existing manifest that never declared hosts would + /// silently gain cleartext (http) outbound permission** on its next build. The + /// `Option` keeps them distinct: + /// - field absent → `None` → render `["https://*:*"]` (unchanged) + /// - explicit `hosts = ["*"]`→ `Some(["*"])` → render http + https (opt-in) + /// - explicit `hosts = []` → `Some([])` → rejected by `length(min = 1)` + /// + /// Validation applies only when present: `length(min = 1)` enforces at least one + /// entry, and `validate_outbound_hosts` (below) checks each entry against + /// the accepted forms (wildcard, scheme-prefixed, host:port, bare host, + /// wildcard subdomain). `#[validate(nested)]`-style option handling: the custom + /// validator is a no-op for `None`. + #[serde(default)] + #[validate(length(min = 1), custom(function = "validate_outbound_hosts"))] + pub hosts: Option>, +} + +/// Per-entry validation for `[capabilities.outbound].hosts`. This is +/// **host-authority-only plumbing**, not a URI field — the same rationale as +/// `OutboundRequest`'s userinfo rejection: credentials must not leak +/// through the manifest into `allowed_outbound_hosts`. +/// +/// Each entry MUST be one of: +/// - `"*"` (the wildcard). +/// - `scheme://host[:port]` where: +/// - `scheme ∈ {http, https}`, case-**insensitive** at the validator +/// (RFC 3986) — `HTTPS`, `https`, `Https` all accepted. The +/// Spin renderer then canonicalizes to lowercase before emitting +/// `spin.toml`, so the rendered manifest carries one canonical +/// spelling. Other schemes → rejected at the validator. +/// - `host` is a DNS label, IPv4 literal, IPv6 literal in brackets, or +/// `*` / `*.domain.tld` wildcard form. +/// - `port`, if present, is a decimal integer in `1..=65535` **or the literal +/// `*` (wildcard port = any port)**. The wildcard port is what lets a manifest +/// express "**HTTPS to any host on any port**" as `https://*:*` while still +/// scoping HTTP narrowly (`http://api.internal:8080`) — a distinction the bare +/// `"*"` (which grants http+https on any host/port) cannot make. `:*` is +/// therefore VALID; only a bare numeric out of `1..=65535` rejects. +/// - **NO userinfo, NO path, NO query, NO fragment.** `https://user:pass@x`, +/// `https://x/p`, `https://x?q`, `https://x#f` all reject. +/// - `host[:port]` (no scheme) — same host/port rules as above. +/// +/// Empty entries, schemes other than `http`/`https`, ports outside +/// `1..=65535` or non-numeric, any userinfo / path / query / fragment, and +/// malformed authorities the hand-rolled splitter rejects (NOT `http::Uri` — +/// the macro-crate constraint below forbids that dep) all yield a `ValidationError`. `"*"` +/// mixed with specific hosts is allowed; the wildcard renders both schemes +/// and specific hosts render alongside. +/// +/// **Grammar (this is a security-relevant splitter — spell it out, don't hand-wave):** +/// `entry := "*" | [scheme "://"] authority`; `scheme := "http" | "https"` (ASCII, +/// case-insensitive); `authority := hostpat [":" port]`; `hostpat := "*" | +/// "*." label ("." label)* | label ("." label)* | "[" ipv6 "]"`. A **`label`** is a +/// non-empty LDH DNS label: 1..=63 chars of ASCII letter/digit/hyphen, **no leading or +/// trailing hyphen, no underscore, no empty label** (so `-x.com`, `x-.com`, `x..com` +/// (empty middle label), `x_y.com` all reject; full name ≤ 253 chars). **`ipv6`** is a +/// standard RFC 4291 address parsed by `std::net::Ipv6Addr::from_str` on the +/// bracket-stripped inner text — anything it rejects (`[::g]`, `[:::1]`, `[12345::]`, +/// `[1:2:3:4:5:6:7:8:9]`) rejects here. `port := "*" | 1..=65535`. **ASCII/IDNA policy:** hostnames are **ASCII-only** — a non-ASCII byte +/// or a raw Unicode label is REJECTED (callers must pre-encode to punycode `xn--`); +/// this splitter does not perform IDNA itself. +/// +/// has a Tier 1 test row exercising every accept AND reject case. Rejects: +/// empty string, bad scheme (`ftp://x`), missing authority (`https://`), +/// userinfo (`https://u:p@x`), path (`https://x/p`), query (`https://x?q`), +/// fragment (`https://x#f`), out-of-range port (`https://x:0`, `https://x:70000`), +/// non-numeric port (`https://x:abc`), **empty port (`https://x:`)**, +/// **malformed brackets (`https://[::1` unclosed, `https://::1]` no open bracket)**, +/// **unbracketed IPv6 (`https://::1` — colons ambiguous with the port sep)**, +/// **invalid wildcard placement (`ex*ample.com`, `*.*.com`, `a.*.com`, `**.com`)**, +/// **internal/leading/trailing whitespace (`https:// x`, `x .com`, ` x.com`, `x.com `)**, +/// **trailing dot (`x.com.`)**, **non-ASCII / raw-Unicode host (`ex€ample.com`, `café.com`)**. +/// Accepts: wildcard (`*`), wildcard subdomain (`*.example.com`), bare host with port +/// (`x:8443`), bracketed IPv6 (`https://[::1]`), IPv4 (`https://127.0.0.1`), +/// punycode (`xn--caf-dma.com`), and mixed `"*"` + host. +// Takes the INNER Vec — `validator` applies a custom function on `Option` to the +// contained value only, so `None` (field absent → https-only default) is a +// no-op and never fails validation. Signature matches the `Option>` field. +fn validate_outbound_hosts(hosts: &[String]) -> Result<(), ValidationError>; + +// SHARED CANONICALIZER — one parser, three consumers, ATOMIC entries. `validate_outbound_hosts` +// returns `()` (validator contract), but platform-manifest rendering and the +// build/serve/deploy DRIFT check both need the *canonical* form to compare — and they +// MUST NOT re-implement parsing (divergence = a manifest that validates but drifts, or +// drifts spuriously). So both delegate to one function that expands each manifest entry +// into a SET of ATOMIC `(scheme, host, port)` triples — because ONE manifest entry can +// render as MULTIPLE `spin.toml` entries (`"*"` = http AND https → two lines), a +// canonicalizer that returned a single multi-scheme value could never set-equal the two +// rendered lines. Atomic-and-flatten fixes that: +// PUBLIC + cross-crate: the consumers (platform-manifest generation and Spin drift validation +// in the adapter crate) are DIFFERENT crates, so the fn and ALL its types MUST be `pub` and +// exported from `edgezero-core` (a private `fn`/type would not compile at those call sites). +// Concretely: +// - The error is a DEDICATED `pub enum HostParseError` — NOT the validator's +// `ValidationError` (that would leak validator internals into the adapter crate, which +// doesn't depend on `validator`). The MANIFEST validator is a thin wrapper that calls +// `canonicalize_outbound_host` and maps `HostParseError -> ValidationError` at the +// validator boundary only; rendering/drift get the `HostParseError` directly. +// - `AtomicHost` and ALL its component types are public: `pub struct AtomicHost` with +// `pub scheme: Scheme`, `pub host: HostPat`, `pub port: Port`, and `pub enum Scheme`, +// `pub enum HostPat` (`Any` | `Exact(String)` | `WildcardSubdomain(String)`), +// `pub enum Port` (`Any` | `Exact(u16)`). `HostPat` is part of the surface (an earlier +// draft omitted it). Derive `Hash, Eq, PartialEq` for the drift `HashSet`. +// - The parser error surface is exact and safe to expose across crates: +// +// #[non_exhaustive] +// #[derive(Clone, Debug, Eq, PartialEq)] +// pub enum HostParseError { +// Empty, +// FragmentNotAllowed, +// InvalidHost, +// InvalidPort, +// InvalidScheme, +// InvalidWildcard, +// MissingAuthority, +// NonAsciiHost, +// PathNotAllowed, +// QueryNotAllowed, +// UserinfoNotAllowed, +// Whitespace, +// } +// +// It implements `std::error::Error`. Its `Display` messages are respectively +// `host entry is empty`, `fragment is not allowed`, `host is invalid`, +// `port must be * or 1..=65535`, `scheme must be http or https`, +// `wildcard is invalid`, `authority is required`, `host must be ASCII`, +// `path is not allowed`, `query is not allowed`, `userinfo is not allowed`, and +// `whitespace is not allowed`. +// Messages never echo the caller input. Tier 1 maps every reject-table row to one +// variant and asserts nonempty stable Display output; downstream callers include a +// wildcard arm because the enum is non-exhaustive. +// For inputs violating multiple rules, classification precedence is fixed as follows; +// host syntax (including malformed brackets or unbracketed IPv6) is resolved before a +// trailing port is parsed: Empty → Whitespace → InvalidScheme → +// MissingAuthority → UserinfoNotAllowed → PathNotAllowed → QueryNotAllowed → +// FragmentNotAllowed → NonAsciiHost → InvalidWildcard → InvalidHost → InvalidPort. +// - Platform-manifest generation must not hand-inspect internals to build `spin.toml`, so `AtomicHost` +// exposes a canonical rendering method: `pub fn render_spin_host(&self) -> String`. +// **It OMITS a scheme-default exact port** (443 for `Https`, 80 for `Http`) so the +// output is deterministic: `https://x` and `https://x:443` both canonicalize to +// `{Https, Exact("x"), Exact(443)}` and BOTH render as **`"https://x"`** (NOT +// `"https://x:443"` — explicitness is already lost at canonicalization, so the renderer +// must not re-introduce a port that would then mismatch the manifest's `https://x` +// form). A non-default port renders explicitly: `{Https, Exact("x"), Exact(8443)}` -> +// `"https://x:8443"`. `Port::Any` renders `":*"`; `HostPat::Any` renders `"*"` as +// the HOST COMPONENT while retaining the atomic's concrete scheme and port. Therefore +// `{Https, Any, Any}` renders exactly `"https://*:*"`, NEVER bare `"*"` (which Spin +// interprets as both http and https and would widen the https-only default). +// `WildcardSubdomain("example.com")` renders the host component `"*.example.com"`. +// Drift uses `Eq`/`Hash` on the atomics; rendering uses `render_spin_host`. Neither +// re-parses. The input shorthand `"*"` expands to two atomics and consequently renders +// as two explicit lines: `"http://*:*"` and `"https://*:*"`. +// Keep it dependency-free (no `http::Uri`, per the macro-crate constraint below). +// pub fn canonicalize_outbound_host(entry: &str) -> Result, HostParseError>; +// pub struct AtomicHost { pub scheme: Scheme /* Http | Https, CONCRETE — never "both" */, +// pub host: HostPat, pub port: Port /* Any | Exact(u16) */ } +// `"*"` -> [ {Http, *, *}, {Https, *, *} ] (two); `https://x` -> [ {Https, x, Exact(443)} ] +// (one); `https://x:*` -> [ {Https, x, Port::Any} ]. Both the manifest hosts AND the parsed +// `spin.toml` `allowed_outbound_hosts` are flattened through this into a `HashSet`, +// and DRIFT compares the two SETS. So `https://x:443` == `https://x`, `HTTPS`==`https`, +// `:*` == any-port, list order is irrelevant, AND `"*"`/`:*` round-trip +// (manifest one-entry -> two atomics == spin.toml two-lines -> two atomics). §5.4 pins a +// render-then-validate round-trip for BOTH `"*"` and `:*` (the generated values must +// report NO drift). +// +// DEPENDENCY CONSTRAINT: `canonicalize_outbound_host` lives in `manifest.rs`, which is +// **textually `include!`d into `edgezero-macros`** — a crate with **no `http` dependency**. +// So the canonicalizer/`validate_outbound_hosts` MUST be **dependency-free**: parse the +// authority with a small hand-rolled splitter (scheme `://` split, rsplit host:port, +// bracket-strip IPv6), **NOT** `http::Uri`. Using `http::Uri` here would force adding +// `http` to `edgezero-macros`'s `Cargo.toml` (and the include! then drags it into every +// build of the macro crate). Either keep it dependency-free (preferred) or the §7 +// inventory must add `http` to `crates/edgezero-macros/Cargo.toml`. Preferred: no dep. + +// Manifest gains: #[serde(default)] #[validate(nested)] +// pub capabilities: ManifestCapabilities, +// +// AND the TOP-LEVEL `Manifest` struct itself gains `#[serde(deny_unknown_fields)]`. +// Without it the strictness above is defeated one level up: `deny_unknown_fields` on +// `ManifestCapabilities` only catches a bad key INSIDE a correctly-spelled +// `[capabilities]` table. A misspelled SECTION — `[capabilites]`, `[capability]`, +// `[Capabilities]` — is an unknown top-level field, silently dropped, leaving +// `capabilities` at its `Default` (empty required+optional). That is fail-OPEN: the +// app declares a contract, the contract vanishes, and `ensure_capabilities` waves it +// through because `caps.required` is empty. +// #[serde(deny_unknown_fields)] +// pub struct Manifest { .. } +``` + +Every capability field is `#[serde(default)]`, so **schema-conforming** manifests parse +unchanged. This is deliberately narrower than "all existing manifests": the top-level +`#[serde(deny_unknown_fields)]` (above) is an intentional behaviour change — a manifest +carrying an **unknown top-level section** (a custom/misspelled `[...]` table that older +builds silently ignored) now **fails to parse**. That break is the fail-closed direction +and is inventoried here; any repo relying on unknown top-level sections for custom +metadata must move them under a modelled section. + +**Top-level strictness is safe here** — `Manifest` already models every documented +section (`adapters`, `app`, `environment`, `logging`, `stores`, `triggers`), so +`deny_unknown_fields` rejects only genuinely-unknown sections, not valid ones. It is a +deliberate behaviour change: a stray/misspelled top-level section becomes a **hard parse +error** instead of a silent drop, which is the fail-closed direction for a +security-relevant contract. (`#[serde(skip)]` internals are unaffected — they are never +read from input.) §5.4 pins this with regression rows: a manifest whose only capability +declaration is under `[capabilites]` (transposed) must **fail**, not parse to an empty +contract; likewise `[capability]` and a stray unknown section. + +**Top-level `deny_unknown_fields` is necessary but NOT sufficient — a `[capabilities]` table +misplaced at ANY depth would be silently dropped.** Per-struct `deny_unknown_fields` only +catches ONE level (`[app.capabilities]`), and chasing every level is whack-a-mole: +`[triggers.http.capabilities]`, `[environment.variables.capabilities]`, +`[adapters.axum.build.capabilities]` are two levels down, and `ManifestAdapter` can't even +take `deny_unknown_fields` (its `#[serde(flatten)]` legacy map is mutually exclusive with +it). **So the PRIMARY defense is a depth-independent reserved-key scan, not per-struct +strictness.** Before deserializing into `Manifest`, parse the input into a `toml::Value` +or `serde_json::Value` and recursively inspect table/object keys. The only accepted reserved +key is the exact lowercase top-level `capabilities`; any nested key for which +`key.eq_ignore_ascii_case("capabilities")`, and any non-lowercase top-level spelling, is a +hard error. The TOML and baked-JSON walkers share this key policy and array recursion. This +catches +`[app.capabilities]`, `[triggers.http.capabilities]`, `[environment.variables.capabilities]`, +`[adapters.axum.build.capabilities]`, and anything future, in one place, without touching +every struct and without the `flatten` conflict. (Top-level `deny_unknown_fields` still +guards misspelled top-level *sections* like `[capabilites]`; the two are complementary — the +scan owns the depth problem.) §5.4 adds rows: a `capabilities` table nested under `app`, +`triggers.http`, `environment.variables`, and `adapters.axum.build` must EACH **fail to +parse**, not silently drop the block and run with an empty contract. + +#### 3.5.2 Adapter capability metadata + +The `Capability` / `CapabilitySupport` enums are `#[non_exhaustive]` (a future capability +must not force every out-of-tree adapter to recompile-or-break), and `Adapter::capability` +carries a **default returning `CapabilitySupport::Unsupported`** for any capability an +adapter doesn't recognize — so an out-of-tree adapter compiled against an older core still +builds, and an unknown capability fails closed (Unsupported → a `required` mismatch +hard-fails) rather than failing to compile. The registry `Adapter` trait also reports typed target +resource facts through a defaulted `platform_metadata` method. This outbound spec does not change +or depend on the trait's store/config lifecycle methods: + +```rust +// crates/edgezero-adapter/src/registry.rs — current (post-#269) shape +pub trait Adapter: Sync + Send { + fn execute(&self, action: AdapterAction, args: &[String]) -> Result<(), String>; + fn name(&self) -> &'static str; + // Added by this spec. MUST carry a default body: without one, every existing + // (and every out-of-tree) adapter compiled against an older core would fail to + // build, and the intended "unknown capability ⇒ unsupported" contract would not + // hold. The default returns `Unsupported`; in-tree adapters override it. + fn capability(&self, _capability: Capability) -> CapabilitySupport { + CapabilitySupport::Unsupported + } + // Target resource metadata, separate from behavioral capability support. The + // default preserves unknown memory, population, and host-accounting facts. + fn platform_metadata(&self) -> PlatformMetadata { + PlatformMetadata::default() + } + // NOTE for in-tree overrides: an adapter that overrides `capability` REPLACES this + // default — the default does not run for capabilities the override's `match` doesn't + // name. `Capability` is `#[non_exhaustive]`, so every in-tree `match capability { .. }` + // MUST end with `_ => CapabilitySupport::Unsupported`, or a capability added later + // reads as some accidental value instead of the intended fail-closed `Unsupported`. + + // Existing non-outbound methods are elided. `ensure_capabilities` consults only + // `capability(..)` for behavior gates. Resource-policy consumers consult + // `platform_metadata()` independently. +} +``` + +This reference is intentionally partial. Existing non-outbound trait methods retain +their current ownership and behavior. + +`PlatformMetadata` carries `PlatformFact` values for the memory ceiling, the maximum live inbound +request population charged to that memory domain, and whether host-owned ingress memory counts +toward the ceiling. Each known fact carries `PlatformResourceSource`; each unknown fact carries a +`PlatformUnknownReason`. `MemoryCeiling` itself carries exact primary bytes, an optional separately +published stack allowance, and `MemoryCeilingScope::{PerExecution, PerInstance}`. + +Axum records all three facts as `Unknown(OperatorConfigured)`. Cloudflare records a platform-limit +memory fact of 128,000,000 bytes per instance while population and host accounting are +`Unknown(ProviderUnpublished)`. Its six-simultaneous-outbound-connection limit applies to +connections waiting for response headers within one invocation and is not evidence of an inbound +request population bound. Fastly records a platform-limit 128,000,000-byte heap plus a separate +1,000,000-byte stack per execution, a platform-limit population of one, and +`Unknown(ProviderUnpublished)` host accounting. Generic Spin records all three facts as +`Unknown(RuntimeConfigured)`. The explicit Akamai Functions Spin profile records a hosted-default +134,217,728-byte ceiling per execution, a hosted-default population of one, and +`Unknown(ProviderUnpublished)` host accounting. Generic Spin does not inherit that profile because +its deployment memory is runtime-configurable. + +All adapters currently receive host-parsed requests. Provider-unpublished host parser, HPACK, and +framing allocations therefore remain unknown; guest-visible allocation accounting cannot replace +that missing provider fact. `PlatformMetadata::validate_memory_envelope` returns `Fits` only when +the ceiling, population, host accounting, scope, and any separate-stack requirement are complete +and compatible. A known minimum excess returns `Exceeds` even if another fact is unknown. +Otherwise, missing facts return `Indeterminate` with typed reasons and must not be treated as a +complete validation result. + +The registry is CLI-owned, so runtime adapters also pass the same canonical `PlatformMetadata` +to the core-owned `App::build::(platform)` before `Hooks::configure` runs. Application hooks +cannot replace this assembly step. `App::platform()` therefore +supports startup resource validation without linking CLI registration code into runtime artifacts. + +Application assembly preserves the same platform contract for externally built routers through +`App::from_router::(router, platform)`. Hook-based `App::build` first obtains the fallible +`Hooks::routes() -> Result` and delegates to that shared assembly path. +Route failure returns unchanged and skips configuration. Both paths install the application +name and selected platform before configuring exactly once, and reject a changed platform only +after successful configuration. Borrowed `App::state::()` and `RouterService::state::()` +read the existing registered state, including during configuration; they create no second registry. +The macro-generated `build_router` also returns Result, and callers must handle its failure. +This foundation changes no outbound dispatch or entrypoint assembly-error serving policy. + +**Publication order:** adding the trait/default does not immediately advertise the matrix. +Each in-tree adapter inherits `Unsupported` until its outbound implementation, deterministic +contracts, and any required host evidence land. That adapter's exact override is committed +in the same phase. The completed repository has the matrix below; no intermediate branch +state may claim support for behavior that has not landed. + +Capability matrix (all four adapters): + +| Capability | Axum | Cloudflare | Fastly | Spin | +| --- | --- | --- | --- | --- | +| `outbound-http` | Native | Native | BestEffort⁹ | Native | +| `outbound-authority-override` | Native | BestEffort⁸ | Native | Unsupported[^spin-authority] | +| `outbound-batch-cancellation` | Native | BestEffort⁸ | BestEffort⁴ | BestEffort⁸ | +| `outbound-batch-completion-order` | Native | Native | BestEffort⁴ | Native | +| `outbound-batch-slot-isolation` | Native | Native | BestEffort⁴ | Native | +| `outbound-cache-bypass` | Native | Native | Native | Native | +| `outbound-complete-resource-accounting` | Unsupported[^resource-accounting] | Unsupported[^resource-accounting] | Unsupported[^resource-accounting] | Unsupported[^resource-accounting] | +| `outbound-header-fidelity` | Native | BestEffort⁸ | Native | BestEffort⁸ | +| `outbound-deadlines` | Native | BestEffort⁸ | BestEffort¹ | BestEffort⁸ | +| `outbound-failure-diagnostics` | BestEffort | BestEffort | Native | Native | +| `outbound-image-optimizer` | Unsupported | Unsupported | BestEffort[^native-image-optimizer] | Unsupported | +| `outbound-flexible-phase-budget` | Native | Native | BestEffort⁵ | BestEffort⁵ | +| `streamed-upload-deadlines` | Native | BestEffort⁸ | BestEffort² | BestEffort⁸ | +| `lazy-streamed-response-passthrough` | Native³ | Native | BestEffort⁶ | BestEffort⁷ | + +[^native-image-optimizer]: Fastly single sends require optimizer entitlement and an explicit + region, bodiless GET/HEAD and default caching. The synchronous call has no proved total + blocking horizon; late returns become the original attributed 504. Viceroy returns typed + `BadGatewayReason::ImageOptimizerUnavailable` (502). Buffered/streamed response caps, + provenance and normalization are unchanged. Hosted behavior and performance remain unqualified. + +[^spin-authority]: Spin's WASI HTTP authority is both the connection target and outgoing HTTP + authority. EdgeZero rejects an explicit override before body polling or provider dispatch + rather than silently retargeting the connection or ignoring the requested wire contract. + +[^resource-accounting]: This capability means a complete pre-admission bound for process or + isolate memory attributable to one outbound response, including provider parser and + field-section materialization, informational responses, final headers, trailers, + native receive chunks, guest buffers and spare capacity, decoder state, allocator + metadata, and runtime copies. Current platform APIs do not expose or bound every term, + so all four adapters report `Unsupported`. EdgeZero still enforces and documents its + narrower guest-visible body limits and adapter-visible field limits: encoded bytes, + decoded bytes, final Buffered bytes, upstream header fields before normalization plus + EdgeZero's synthetic proxy marker, Brotli decoder-state charge, and emitted item shape. A caller + requiring a complete RSS/isolate bound must declare this capability required and fail + closed on every current target rather than treating the narrower arithmetic in §3.4.4 + as a complete memory guarantee. + +¹ **Fastly `outbound-deadlines` is `BestEffort`, because it cannot be guaranteed on +every request.** Even with an already-registered/cached backend, any non-empty request body +has an unbounded guest-to-origin host-write interval: Fastly exposes no upload-write timer. +For a zero-length request body on the warm path, the dispatch/headers and response-read +portions have the documented deterministic overshoot bounds below (common-case +`total_ms ≥ 4` phase split; the sub-4 ms branch adds `total_ms`, see §4.3 "Net guarantee"). +Separately, the **FIRST request to a new host** calls `Backend::builder(..).finish()`, a synchronous host +call that can block waiting for a service-wide dynamic-backend slot. In `send_all_until`, a cache miss +checks the absolute deadline and `BATCH_DISPATCH_SLACK_MAX` before entering `finish()`, which +prevents an already-delayed later slot from starting another registration. Nothing guest-side +can preempt a registration after it starts, so it may still overshoot before the post-registration +guard executes; on that path the guard checks the **absolute deadline FIRST** and returns an +**attributed `gateway_timeout` (504)** — a `finish()` that returns past the deadline is a +genuine expiry, not an EdgeZero bug (`internal` is reserved for slack exceeded *while time +remains*, §4.3). Either way the wall-clock overshoot happened, so the capability is still +`BestEffort`. Because `capability()` is a **static** value +that cannot distinguish cached from cold, the honest declaration is **`BestEffort`** — a +`required outbound-deadlines` therefore hard-fails on Fastly rather than passing a gate it +cannot actually honour for cold registration or body-bearing writes. Apps that accept +those gaps declare it **`optional`** (logged, never gated) and get the documented partial +bounds below. + +**These dispatch/headers and response-read bounds hold once the backend is registered +(cached); they do not bound request-body transmission.** On the +**first** request to a new host, `Backend::builder(..).finish()` — a +synchronous host call that can block waiting for a service-wide dynamic-backend slot — +may overshoot after the batch pre-registration guard admits it and before the post-registration +guard immediately ahead of `send_async`. On that path the latter guard checks +the deadline first and returns an **attributed `gateway_timeout` (504)** for the actual +expiry (not `internal`); the wall-clock was still overshot, which is exactly why the +capability is `BestEffort` and not `BoundedCooperative` — see §4.3 +*Honesty note — what the guard can and +cannot do*. +- **Single `send`** — `now` is snapshotted inline so there is no batch drift, + and the post-registration **`BATCH_DISPATCH_SLACK_MAX` guard** applies to the gap between + `dispatch_budget(req, now, None)` and `send_async` (backend lookup, possible + `Backend::builder().finish()`, SDK request construction; see §4.3). Worst-case + warm-path dispatch+headers overshoot is `BATCH_DISPATCH_SLACK_MAX + ms_rounding`; the + window is typically narrower than a batch because there is no multi-slot preflight and + dispatch loop. Response-body phase overshoot ≤ one between-bytes-timeout + interval (§3.3.4). For a non-empty buffered or streamed request body, the upload write + remains unbounded and sits outside these finite terms. **Streamed-upload-specific + post-upload overshoot**: when the request + body is `Body::Stream` and the upload drain leaves a tiny positive + `budget.deadline.remaining()`, the post-upload headers wait can additionally + cost up to one dispatch-time `first_byte_ms` interval before the cooperative + check at the `wait()` boundary or the response-wrapper preemption fires + (§4.3 "Response phase"). That overshoot is **one-shot**, not per-chunk — + the response wrapper preempts at the first post-deadline read. +- **`start_batch_until` / `send_all_until`** — `batch_now` is shared across slots so the measured setup and + host-timer portions of dispatch+headers carry + `BATCH_DISPATCH_SLACK_MAX + ms_rounding` (≈ 26 ms when `total_ms ≥ 4`, §4.3 + "Dispatch-overhead slack"). A non-empty buffered body's unbounded host write proceeds + after `send_async` has returned, so it does not prevent later dispatch calls, but the slot + cannot complete until that background transmission and its response progress. Response-body + phase **once a slot is actively draining** is still ≤ one between-bytes-timeout — but the slot's **observed + completion** can additionally be delayed while previously selected response bodies drain. + That selection/drain delay is what the separate + `outbound-batch-slot-isolation` capability owns (footnote 4); the + `outbound-deadlines` bound here is on the active-drain phase only, not on + total observed wall-clock across the batch. + +The finite terms above are hard adapter constants, not "scales with preflight"; cold +registration and request-write completion remain explicitly unbounded. `Native` is reserved +for adapters with no such caveat — this rubric lets future adapters be judged consistently +without quiet downgrading. A new adapter unable to honour a capability declares +`Unsupported` and is caught at build time. The batch *buffered-body* cross-slot +caveat (selected-body-drain false 504s) is **not** within this capability — that one is +`outbound-batch-slot-isolation` (footnote 4), so each label means exactly one thing. + +² Fastly has no guest primitive to preempt a stalled `stream.next().await` while feeding +a streamed REQUEST body via `send_async_streaming` (§4.3). **Both phases are unbounded +on Fastly:** (a) the *source pull* — a source stream that never yields the next chunk +cannot be preempted; and (b) the *host write* — `between_bytes_timeout` is documented as +**receive-side only** (it bounds the gap between bytes *received from origin*) and does +**not** bound guest-to-origin writes, so it gives no inter-chunk guarantee on the upload +path. (An earlier draft of this footnote claimed `between-bytes-timeout` still bounds +upload inter-chunk gaps — that is wrong; §4.3 and §8 risk 7 are correct.) The only +adapter-side bound is the cooperative `budget.deadline.is_expired()` check **between** +chunks. This is `BestEffort` — no documented preemption bound — and is exposed as the +separate +`streamed-upload-deadlines` capability so apps that need real-time enforcement on this +specific path declare it required and get a hard build failure on every BestEffort target, +including Fastly and Spin, per §3.5.3. +Apps that buffer their request bodies before calling `send` are unaffected **on the +source-pull axis only** — buffered uploads use `Body::Once` with no `stream.next().await`. +They are **NOT** exempt on the **host-write** axis: Fastly's `connect_timeout` ends at TLS +setup and `first_byte_timeout` starts at *"request sent"*, so the interval in which the +host pushes the buffered body to the origin is bounded by **no** Fastly timer, and +`between_bytes_timeout` is receive-side only. An origin that completes the handshake then +stops reading stalls that write unbounded, and the guest cannot abort a dispatched +`PendingRequest` (§3.3.3). Buffered uploads therefore fall under `outbound-deadlines`, +which is **`BestEffort` on Fastly** (footnote 1) — a label that already covers this gap; +buffering narrows the exposure to the write phase, it does not remove it. + +⁵ `outbound-flexible-phase-budget` captures whether the adapter treats the request +budget as one elastic pool. Axum and Cloudflare have one authoritative total timer +(reqwest's `.timeout(..)` and `worker::Delay`); a slow connect followed by fast +headers/body inside the total budget succeeds. On Fastly the budget is +**rigidly split** (§4.3 — `connect = budget/4`, `first_byte = 3*budget/4`, +`between_bytes = budget`); a request that takes more than `budget/4` on connect-phase +work fails at the connect timer even though the rest of the budget would have +sufficed. This is a documented `BestEffort` deviation — the platform-level cause is +that Fastly's `BackendBuilder` exposes per-phase timers and no total-budget timer. +Apps that need elastic budget allocation (slow-connect workloads, mixed-latency +upstreams) declare this capability required and get the hard build failure on +Fastly per §3.5.3. Spin also runs an outer monotonic total race, but WASI permits any +`RequestOptions` phase-timer setter to return `NotSupported`. In that case an opaque host +default can fail earlier than the configured total budget, so the static capability is +`BestEffort` even though the guest-visible outer race remains. Promotion requires a target +whose three setters are guaranteed or a host API that disables independent defaults. + +⁴ `outbound-batch-slot-isolation` is `BestEffort` on Fastly for two direct cross-slot reasons. +**(a) Selected response-body drain** (§3.3.4): a slot whose own +`budget.deadline` would have covered its body in isolation can still return +`gateway_timeout`, or remain unresolved at the observation cutoff, because another selected body +monopolised the guest before the next `select_handles` call. Selection is not input-ordered, but +body consumption is still one selected slot at a time. **(b) Cold sequential registration** +(§4.3): a first-time `Backend::builder(..).finish()` can block without a guest-side bound, +preventing later slots from being dispatched. A non-empty buffered upload has no finite +guest-to-origin write bound and can leave its own slot unresolved, but after `send_async` returns +it does not by itself block later dispatch or selection of another ready handle in the active +group. + +Selected-body delay becomes smaller for small response bodies. Small responses do not repair +cold registration, and a small request body still has no documented write-time bound for its own +slot. Consequently the +spec makes no general "typical small-body fan-outs are unaffected" claim. Apps that need +cross-slot result isolation declare this capability required and get a hard build failure on +Fastly per the "required + BestEffort = hard fail" rule (§3.5.3). On Axum/CF/Spin, +the completion driver polls complete per-slot exchanges concurrently, so isolation is `Native`. + +³ `lazy-streamed-response-passthrough` captures whether +`OutboundResponse::into_response()` delivers a streamed upstream body to the platform +response **without whole-body buffering**. Axum and Cloudflare are `Native`; Fastly and Spin are +`BestEffort` under footnotes 6 / 7 pending end-to-end target evidence. Axum runs the non-`Send` +portable stream directly on the connection's `LocalSet` and yields one frame under Hyper demand; +unit and raw-socket tests prove first-frame delivery before source EOF. Socket/client completion +and collateral HTTP/1 deadline closure belong to the separate response-egress capabilities and +remain BestEffort. + +⁶ Fastly uses an undecorated, send-owning entrypoint. EdgeZero receives the request and owns +`stream_to_client`, writes portable chunks with short-write accounting, and closes or abandons the +body handle exactly once. The cell remains `BestEffort` because synchronous source polls and host +writes cannot be preempted, and a consuming `finish` failure leaves no handle to abandon. + +⁷ Spin returns EdgeZero's raw `SpinResponse` and keeps the WASI response, body, and result writers +inside one spawned coordinator. It preserves lazy production, accounts accepted prefixes, races +pending writes against the absolute deadline, and destroys the response on failure. The cell +remains `BestEffort` because guest cancellation does not establish a deployed finite host-teardown +or end-client-completion bound. The hand-built outbound request path remains independently +`BestEffort` per footnote 8. + +⁸ **Spin/Cloudflare deadline and header-fidelity caveats.** Spin has a monotonic timer and can race the +guest-visible exchange, but Component Model cancellation is cooperative. Dropping a +`FutureWriter` that has not written launches a background default write, and dropping a +canonical-ABI subtask does not establish a documented one-tick host teardown bound. The +adapter therefore implements explicit request/response completion protocols (§4.4) and +returns a timeout when its timer wins, while `outbound-deadlines` and +`streamed-upload-deadlines` remain `BestEffort` until host-observed runtime tests prove +bounded teardown for stalled upload and response paths. A local Spin 4.1 SDK-only WASI control +observes `Connection` removed before guest normalization while a nominated extension field remains. +Spin preserves the bytes and boundaries exposed by the host, but cannot establish original raw +header fidelity or filter hidden nominations, so `outbound-header-fidelity` is `BestEffort`. +The stronger raw-header contract in §5.4 is unchanged; adapter-visible injection tests do not +prove host-original fidelity. This local characterization is not hosted qualification. +Cloudflare wires one owned `AbortController` through the final fetch options and body wrapper, +but the PR has no deployed host-observed cancellation artifact proving a finite bound. Its +`outbound-deadlines` and `streamed-upload-deadlines` cells therefore remain `BestEffort` until +that evidence exists. Cloudflare's separate `BestEffort` header-fidelity cell reflects workerd's loss of raw octets and +original non-`set-cookie` field boundaries; its response-out runtime can also recompute or +ignore `Content-Length` for a streamed passthrough. EdgeZero uses `EncodeBody::Manual` to +preserve already encoded bytes and visible `Content-Encoding`, but it does not claim an +exact downstream-wire length where Workers owns framing. + +⁹ **Fastly `outbound-http` requires service enablement.** The adapter implements arbitrary +dynamic destinations, but Fastly disables dynamic backends by default and the local CLI has +no authoritative service-entitlement query. A static capability result therefore cannot +promise that the selected deployment can issue any request. The current cell is +`BestEffort`: optional use is supported with an explicit warning, while a required +declaration fails closed. Trusted Phase 6 live probes characterize enabled and disabled +services; promotion to `Native` requires a separately reviewed deployment preflight that +proves enablement for the selected service. + +#### 3.5.3 Build / startup enforcement + +`ensure_capabilities` is an **outbound runtime gate**, not the owner of every CLI lifecycle. +It runs before shell or registry dispatch for `build` / `serve` / `deploy` / +`deploy --staging`, and before the Axum `demo` runtime starts. The outbound-scoped adapter +dispatch entry points, `execute_runtime(..)` and `execute_capture_runtime(..)`, accept only an owned +`ResolvedRuntime` plus adapter arguments. They derive the canonical adapter identity and +action from that runtime and call the same action-aware gate **before** dispatching to its +already resolved shell or registry target. This is required because +`execute_capture_runtime(..)` owns a separate `run_shell_tee(..)` branch and can otherwise bypass +a gate placed only in `execute_runtime(..)`. `auth *`, `EmitVersion`, `Healthcheck`, and `Rollback` +are operational actions rather than construction/deployment of the current manifest's +runtime and remain exempt; the latter three may be invoked without any manifest contract. +`provision` and every `config` subcommand are outside this outbound specification. Their +owning store/capability specifications decide whether they participate in any broader +cross-capability gate. + +```rust +// crates/edgezero-cli/src/adapter.rs +// Keep the action classification exhaustive over today's Action variants. A newly added +// action must be deliberately classified here; it must not inherit an accidental default. +fn produces_current_runtime(action: Action) -> bool { + match action { + Action::Build | Action::Deploy | Action::DeployStaging | Action::Serve => true, + Action::AuthLogin + | Action::AuthLogout + | Action::AuthStatus + | Action::EmitVersion + | Action::Healthcheck + | Action::Rollback => false, + } +} + +fn ensure_action_capabilities(runtime: &ResolvedRuntime) -> Result<(), String> { + if !produces_current_runtime(runtime.action()) { + return Err("operational action entered outbound runtime dispatcher".to_string()); + } + ensure_capabilities( + runtime.adapter_name(), + ManifestContract::from_opt(runtime.manifest()), + ) +} + +// Outbound-scoped dispatch path 1. Gate BEFORE branching to the resolved target. +pub fn execute_runtime( + runtime: ResolvedRuntime, + adapter_args: &[String], +) -> Result<(), String> { + ensure_action_capabilities(&runtime)?; + // Existing shell-command / registry dispatch follows. The registered-adapter branch + // lives in a private helper so execute_capture can reuse it without gating twice. + execute_after_gate(runtime, adapter_args) +} + +// Outbound-scoped dispatch path 2. This owns a distinct shell/tee branch, so it gates too. +pub fn execute_capture_runtime( + runtime: ResolvedRuntime, + adapter_args: &[String], +) -> Result, String> { + ensure_action_capabilities(&runtime)?; + if let ResolvedAdapterTarget::Shell(shell) = runtime.target() { + // Command/root/environment/bind setup is already stored in `shell`; this branch + // does not re-run `manifest_command`, target discovery, or manifest parsing. + return run_shell_tee(shell, adapter_args).map(Some); + } + // Call the private already-gated registry path, NOT public execute_runtime(..): calling the + // public entry point would duplicate optional-capability warnings. Pass the complete + // resolved pair so the registry branch executes `runtime.target` and preserves + // manifest-present/absent diagnostics without rediscovery. + execute_registered_after_gate(runtime, adapter_args)?; + Ok(None) +} + +// crates/edgezero-cli/src/demo_server.rs — no manifest FILE exists; read the +// manifest baked in by `app!` (Hooks::manifest). +#[cfg(feature = "demo-example")] +pub fn run_demo() -> Result<(), String> { + // baked ('static): BakedManifest -> ManifestContract via as_contract + ensure_capabilities("axum", ::manifest().as_contract())?; + /* …Axum runner… */ +} + +``` + +There is deliberately no separate `adapter_name` or `action` argument on either outbound +dispatcher. The resolver records the canonical adapter identity and requested action in +`ResolvedRuntime`; the gate, registry lookup, shell diagnostics, and +`AdapterAction` conversion all consume those same fields. Resolver-produced values are the +only construction path exposed outside `manifest_source`, so a caller cannot gate adapter A +or action X and then dispatch adapter B or action Y. The private post-gate helpers likewise +accept the complete runtime rather than duplicate identity arguments. The pre-existing +`execute(..)` / `execute_capture(..)` operational path remains available to auth, version, +healthcheck, and rollback and never accepts `ResolvedRuntime`. + +`run_demo` is feature-gated (`demo-example`) and always selects Axum implicitly, so its +gate hardcodes the adapter name and reads the **baked** manifest rather than a file. The +two adapter dispatch entry points plus `run_demo` are exhaustive for the outbound runtime +gate; unrelated CLI lifecycles are deliberately absent. The exact private-helper split may +follow the current module, but the invariant is normative: each public dispatch path gates +exactly once before either shell or registry work. + +`ensure_capabilities` itself reads from the **registry** (not from `Adapter::execute`) +because capability metadata is the trait fact `capability(Capability) -> +CapabilitySupport`, and the registry is where adapter implementations are looked up +by name. That means **shell-overridden adapters still get checked**: even if the +manifest configures `[adapters..commands.build]` so dispatch never reaches +`Adapter::execute`, the gate still consults the registered adapter's `capability(..)` +tuple — the shell override only routes the *action*, it does not opt out of the +*manifest contract*. + +**Missing-from-registry policy.** If `registry::get_adapter(adapter_name)` returns +`None`, the policy depends on whether the manifest declares any required or optional +capabilities: + +| Manifest `[capabilities]` shape | Adapter in registry? | Outcome | +| --- | --- | --- | +| absent or empty (`required = []`, `optional = []`) | no | `log::warn!` "capability check skipped (no capabilities declared)" — proceed | +| **any `required`** entry | no | **hard failure**: `Err("adapter '' is not in the registry; cannot verify REQUIRED capabilities. …")` | +| **only `optional`** entries (no `required`) | no | `log::warn!` "cannot verify its OPTIONAL capabilities — proceeding, since optional capabilities never hard-fail" — proceed | +| absent / empty | yes | proceed (loop bodies trivially pass) | +| has entries | yes | check each per the rubric below | + +This preserves the "required capabilities fail early" contract while honouring +"optional never hard-fails" (§3.5.3) — an unverifiable *optional* capability warns and +proceeds, exactly as a known-degraded optional one does; only an unverifiable **required** +capability is fatal. It also keeps the brand-new-shell-only-adapter ergonomics for the +*no-capabilities* case (e.g. a contributor wiring a new edge platform via shell-out, +before they've written the adapter stub). An app that declares a **required** capability +needs a registered adapter that can answer the `capability(Capability) -> +CapabilitySupport` question; there is no silent bypass of a required contract. + +Commands covered by the outbound gate sites above: + +| Command/action | Entry point | Gate site | +| --- | --- | --- | +| `edgezero build` | `run_build` resolves `Action::Build` → `execute_runtime(runtime, ..)` | `execute_runtime(..)` — **gated** | +| `edgezero serve` | `run_serve` resolves `Action::Serve` → `execute_runtime(runtime, ..)` | `execute_runtime(..)` — **gated** | +| `edgezero deploy` | `run_deploy` resolves `Action::Deploy` → `execute_runtime(runtime, ..)` or, for captured Fastly output, `execute_capture_runtime(runtime, ..)` | whichever outbound-scoped dispatcher is selected — **gated before its shell branch** | +| `edgezero deploy --staging` | `run_deploy` resolves `Action::DeployStaging` → `execute_runtime(runtime, ..)` | `execute_runtime(..)` — **gated** | +| `edgezero auth login` / `logout` / `status` | existing `run_auth` → existing `execute(adapter, action, manifest, ..)` | **EXEMPT** (credential + read-only class); no paired resolver migration | +| version emission / healthcheck / rollback | existing operational call sites → existing dispatcher | **EXEMPT** — no paired resolver or manifest requirement | +| `edgezero demo` (feature `demo-example`) | `run_demo` → Axum runner. `run_demo()` takes **no path or loader** and reads no manifest file, so a file-based gate is impossible. **Locked resolution — gate on baked manifest metadata via a new `Hooks` accessor** (see below) | `run_demo()` calls `ensure_capabilities("axum", ::manifest().as_contract())` before the Axum runner starts | + +**The `demo` gate needs a baked-manifest accessor — `app!` must emit one.** +`run_demo()` (`crates/edgezero-cli/src/demo_server.rs`) hardcodes `run_app::()` for the +concrete `app_demo_core::App` (a **struct**, so it cannot be a trait bound), so a test cannot +inject a crafted `Hooks`. **Add a PURE gate helper**, not a generic `run_demo`: + +```rust +// edgezero-cli — no server start, so a test can call it directly and cheaply. +// NOT feature-gated: this helper and its test are compiled UNCONDITIONALLY (they do not +// live behind `#[cfg(feature = "demo-example")]`), so the row runs under the plain +// `cargo test --workspace --all-targets` CI gate. Only `run_demo()` itself — which pulls +// in the `app-demo` example — stays behind `demo-example`; the pure gate does not depend +// on the example, only on the `Hooks` trait, so keeping it always-compiled is free. +pub(crate) fn demo_capability_gate() -> Result<(), String> { + ensure_capabilities("axum", ::manifest().as_contract()) +} +``` + +`run_demo()` (behind `demo-example`) calls `demo_capability_gate::()?` +**before** `run_app`. The failure test calls `demo_capability_gate::()` directly +with an in-crate `TestApp` — no `demo-example` feature, no blocking server on the success +path — so it is exercised by the default test command. **`TestApp` must override `manifest()`, not just `manifest_json()`:** the default +`manifest()` returns `Absent` (a static in the trait default would be shared across impls), so +overriding only `manifest_json()` would leave the gate seeing `Absent` and proceeding. `TestApp` +overrides `manifest()` to return `Manifest::from_baked_json()`. +`run_demo` has no path, no `ManifestLoader`, and no way to find `edgezero.toml` at runtime. But +the `app!` macro **already parses, validates, and serializes the manifest at compile +time** (`crates/edgezero-macros/src/app.rs`: `manifest.finalize()` → +`serde_json::to_string(&manifest)` → `manifest_json_lit`) — it just embeds that JSON for +the router and never exposes it. Today `Hooks` +(`crates/edgezero-core/src/app.rs`) has `routes()` / `stores()` / `name()` and **no +manifest accessor**, so the gate has nothing to consult. Add one: + +```rust +// edgezero-core/src/app.rs — extend the existing Hooks trait. +pub trait Hooks { + // …existing: build_app / configure / name / routes / stores… + + /// Raw manifest JSON baked in at compile time by `app!`. + /// Default `None` for hand-written `Hooks` impls that never ran the macro. + fn manifest_json() -> Option<&'static str> { None } + + /// Parsed + finalized, cached view of the above. + /// + /// **The default MUST be `Absent` — it must NOT hold a cache.** A `static` declared + /// inside a trait default method body is **one item shared by every implementor**, + /// not one per `Self` (items in generic fns are not monomorphized — Rust Reference). + /// A caching default would therefore let the FIRST app to call `manifest` + /// populate the value every OTHER app reads — capability checks against the wrong + /// manifest. Proven: two impls relying on such a default both returned the first + /// impl's value. The cache lives in each **macro-generated impl** instead (below), + /// where the `static` is a distinct item per impl. + fn manifest() -> BakedManifest { BakedManifest::Absent } +} + +// edgezero-core/src/manifest.rs +// +// THREE states, not `Option`. `Option<&Manifest>` conflates "this app has no baked +// manifest" (legitimate: a hand-written `Hooks`, no macro → no capability contract → +// proceed) with "the baked manifest is CORRUPT" (an adapter/macro contract bug). If +// both collapse to `None` and `ensure_capabilities` treats `None` as permission to +// proceed, a malformed contract **silently disables required-capability enforcement** +// — it fails OPEN. This enum makes that unrepresentable. +// `#[non_exhaustive]`: a future state (e.g. a lazily-parsed variant) must not silently +// pass a match written today. Matches in OTHER crates (the CLI gate) therefore need a +// `_` arm, and that arm MUST fail closed (treat an unknown state as "cannot verify → +// refuse"), never proceed — see `ensure_capabilities`. +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum BakedManifest { + /// No `app!`-baked manifest (hand-written `Hooks`). No capability contract. + Absent, + /// `manifest_json` returned Some, but it did not parse/finalize. An + /// adapter/macro contract bug: the JSON came from `serde_json::to_string` on an + /// already-validated `Manifest`, so this is unreachable unless the macro is broken. + Malformed(&'static str), // static reason, for the diagnostic + /// Successfully parsed + finalized. + Present(&'static Manifest), +} + +impl Manifest { + /// Parse baked JSON and rebuild derived state (`finalize`). Returns + /// `BakedManifest::Malformed` — never `Absent` — on failure, so a corrupt + /// contract can never be mistaken for "no contract". + pub fn from_baked_json(json: &'static str) -> BakedManifest { /* … */ } +} + +// What `app!` GENERATES per app — each impl gets its OWN `manifest` fn, hence its +// OWN `static`, hence a genuinely per-app cache. +// +// TWO requirements on the generated code: +// 1. FULLY-QUALIFIED PATHS. This expands in the DOWNSTREAM crate, which may not have +// `Manifest` or `OnceLock` in scope (or may shadow them). Macro output must name +// `::edgezero_core::manifest::Manifest`, `::std::sync::OnceLock`, etc. — never a +// bare `Manifest`/`OnceLock`. (The snippet below is written qualified for exactly +// this reason; do not "tidy" it into short paths.) +// 2. FAIL CLOSED on a malformed baked contract — see `BakedManifest` below. +impl ::edgezero_core::app::Hooks for MyApp { + fn manifest_json() -> Option<&'static str> { Some(r#"{…baked…}"#) } + + fn manifest() -> ::edgezero_core::manifest::BakedManifest { + // per-IMPL: a distinct static item, because this fn is generated per impl. + static CACHE: ::std::sync::OnceLock<::edgezero_core::manifest::BakedManifest> = + ::std::sync::OnceLock::new(); + *CACHE.get_or_init(|| { + match ::manifest_json() { + None => ::edgezero_core::manifest::BakedManifest::Absent, + Some(json) => ::edgezero_core::manifest::Manifest::from_baked_json(json), + } + }) + } + // …routes/stores/etc… +} + +// edgezero-core/src/manifest.rs — the accessor CANNOT parse+finalize itself, because +// `Manifest::finalize` is `pub(crate)` and the `app!`-generated `manifest` lives in +// the DOWNSTREAM crate, which can't reach it. Core owns the parse+finalize: +impl Manifest { + /// Parse baked JSON and rebuild derived state (`finalize`). + /// Returns `Malformed` — NEVER `Absent` — on failure, so a corrupt contract can + /// never be mistaken for "no contract" (fail closed, see `ensure_capabilities`). + pub fn from_baked_json(json: &'static str) -> BakedManifest { + // SAME pipeline as `try_load_from_str`: parse -> VALIDATE -> finalize. + // Skipping validate() would fail OPEN: `{}` is valid JSON and parses to a + // default Manifest with EMPTY capabilities, so an invalid-but-parseable + // contract would become `Present` and the gate would proceed against no + // required capabilities. finalize() only rebuilds derived logging state — it + // is NOT validation. A parse OR validation failure is a contract bug -> Malformed. + let value: serde_json::Value = match serde_json::from_str(json) { + Ok(value) => value, + Err(_) => return BakedManifest::Malformed("baked manifest did not parse"), + }; + if reject_reserved_capability_keys_json(&value).is_err() { + return BakedManifest::Malformed("baked manifest has misplaced capabilities"); + } + let mut manifest: Manifest = match serde_json::from_value(value) { + Ok(manifest) => manifest, + Err(_) => return BakedManifest::Malformed("baked manifest did not parse"), + }; + if manifest.validate().is_err() { + return BakedManifest::Malformed("baked manifest failed validation"); + } + manifest.finalize(); // pub(crate) — reachable here, inside core + // Leaked into a 'static: parsed once per process behind the generated + // per-impl OnceLock, and the result must outlive the call. + BakedManifest::Present(Box::leak(Box::new(manifest))) + } +} +``` + +> **`from_baked_json` is `#[doc(hidden)]` macro-support API — call it AT MOST ONCE per +> process.** Each successful call `Box::leak`s a `Manifest` into a `'static`; the generated +> `app!` code invokes it exactly once behind a per-impl `OnceLock`, so the leak is bounded +> to one allocation per app. It is `pub` only because the macro expands in the app's crate, +> **not** an invitation for arbitrary callers — a hand-written caller that invokes it per +> request would leak unboundedly. The doc-comment states the once-per-process contract and +> the type is `#[doc(hidden)]`; direct use is out of contract. + +- **`app!` MUST emit BOTH methods explicitly.** The macro's generated `impl Hooks` + cannot rely on the trait defaults: `crates/edgezero-macros/src/app.rs` sets + **`clippy::missing_trait_methods = deny`** (verified — a generated impl that omits a + defaulted method is a hard clippy error: *"missing trait method provided by default"*). + So the macro emits `manifest_json()` (returning `Some(#manifest_json_lit)`) **and** + `manifest()` (the `OnceLock` + `from_baked_json` body above), exactly as it already + emits `configure`/`build_app` for the same reason. **Any hand-written `Hooks` impl in + the codebase must also emit both** (or locally `#[allow]` the lint) — "additive and + defaulted" is **not** free here. +- **Reparsing JSON alone is WRONG — it skips `finalize()`.** `Manifest::finalize()` + rebuilds derived state (e.g. resolved routes) that is **not** in the serialized JSON, + and several fields are `#[serde(skip)]`. A bare `serde_json::from_str` yields a + half-built `Manifest`. Hence `from_baked_json` calls `finalize()` — and hence it must + live in **core** (only core can call the `pub(crate)` `finalize`), not in the generated + downstream impl. +- **Ownership / lifetime — the cache MUST live in the generated impl, never in the trait + default.** The JSON is a `&'static str` in the binary. The `OnceLock` is a + function-local `static` inside **each generated `manifest()`**; because every impl + emits its own `manifest()` fn, each gets a **distinct** `static` — a genuine per-app + cache, parse paid once per process. **A `static` in the trait DEFAULT body is shared + by all implementors** (items inside generic fns are not monomorphized), so a caching + default would serve app A's manifest to app B. This was **empirically proven** with two + impls relying on such a default: both returned the first one's value. Hence the default + is `BakedManifest::Absent`, and a single core-global `OnceLock` is likewise rejected. *(This is a second, + independent reason the macro must emit `manifest()` explicitly — beyond the denied + `clippy::missing_trait_methods`.)* +- **Failure mode — FAIL CLOSED, and the two failures are distinct.** `manifest()` + returns a three-state `BakedManifest`, never an `Option`, because `Option` conflates + two opposite situations: `Absent` (hand-written `Hooks`, no macro → genuinely *no + capability contract* → `ensure_capabilities` short-circuits `Ok(())`, the same "no + manifest" policy as §3.5.3) versus `Malformed` (the JSON was baked but doesn't + parse → an `app!`/core contract bug). If both were `None` and `None` meant "proceed", + **a corrupt baked contract would silently disable required-capability enforcement** — + the gate would report success precisely when it can no longer verify anything. + `Malformed` therefore **hard-fails** with an actionable message. A gate that fails + open on unreadable input is worse than no gate. +- **Test seam:** capability-gate failure tests drive `demo` with a **test-only `Hooks` + impl** overriding `manifest_json()` to return crafted JSON (which then flows through + the real `from_baked_json` + `finalize`) — no file, no macro re-expansion. This is why + `manifest_json()` is a trait method with a default, not a free function or bare `const`. + (The test impl must also emit `manifest()` — or `#[allow(clippy::missing_trait_methods)]` + — per the lint above.) + +Commands **not** covered (and why): +- `edgezero new` — generates source files; no adapter is selected, so capabilities + cannot be checked. The scaffold itself is identical across adapters. +- `edgezero auth *` — **exempt by command class** (credential; §3.5.3 table), so the + gate never runs for it regardless of manifest presence. (Independently, a genuinely + absent manifest is `BakedManifest::Absent` ⇒ no capability contract ⇒ `Ok(())`; a + *malformed* one hard-fails. Documented in the rustdoc.) + +**Support-level enforcement ladder (what `required` means).** `capability()` returns one of `Native` > `BoundedCooperative` > `BestEffort` > `Unsupported`. A capability in `required` is satisfied by **`Native` or `BoundedCooperative`** (both are *real* enforcement — `BoundedCooperative` has a precisely documented, deterministic bound); it **hard-fails** on `BestEffort` (real-world deviation the app must opt into) or `Unsupported`. `optional` never hard-fails — a `BestEffort`/`Unsupported` optional capability is logged, not gated. **Apps that require the documented outbound-deadline guarantee declare `outbound-deadlines` `required`.** No adapter reports `BoundedCooperative` for that capability, so the declaration is currently accepted only on Axum; it hard-fails on Cloudflare, Fastly, and Spin. A separate `outbound-deadlines-exact` capability is unnecessary. + +**Historical (pre-#269) shape — now superseded (PR #269 has merged to main):** +Before #269 landed, `Command::{Build, Serve, Deploy, Dev}` all dispatched through +the registry's `Adapter::execute(AdapterAction::{Build, Serve, Deploy}, ..)` plus +`Command::Dev`'s implicit-Axum runner, and the gate went at the top of each of +those four handlers (or the equivalent helper they called). #269 collapsed the +runtime-producing actions into the single `execute(..)` dispatcher; `demo` remains +the second outbound runtime gate. + +```rust +// NOTE: takes an already-parsed manifest, NOT a `ManifestLoader`. The `demo` gate has +// no manifest *file* to load — it reads the manifest baked in by `app!` via +// `::manifest` (see the demo row above). File-backed callers pass +// `Present`/`Absent` from their loader; `demo` passes the baked `BakedManifest`. +// +// FAIL CLOSED. `Absent` (no contract) proceeds; `Malformed` MUST NOT — an earlier +// draft used `Option` and treated `None` as permission to proceed, so a corrupt baked +// contract silently disabled required-capability enforcement. A capability gate that +// fails open on malformed input is worse than no gate: it reports success. +// INPUT TYPE: a LIFETIME-BEARING contract, NOT `BakedManifest`. File-backed runtime +// sites hold a **local** `&Manifest` borrowed from a loader — those are not +// `'static`, so they cannot be wrapped in `BakedManifest::Present(&'static Manifest)`. +// Only `run_demo` has a `'static` (baked) manifest. So the gate accepts a borrow of any +// lifetime, and `BakedManifest` (which is `'static`) converts INTO it: +// +// #[non_exhaustive] // future states must fail closed in the cross-crate CLI match +// pub enum ManifestContract<'a> { +// Malformed(&'static str), // corrupt baked contract → fail closed +// None, // no contract → proceed (legit: no manifest) +// Present(&'a Manifest), // any lifetime — file-backed OR baked +// } +// impl BakedManifest { +// pub fn as_contract(&self) -> ManifestContract<'_> { /* Absent→None, Malformed→Malformed, Present→Present */ } +// } +// impl<'a> ManifestContract<'a> { +// pub fn from_opt(manifest: Option<&'a Manifest>) -> Self { +// manifest.map_or(Self::None, Self::Present) +// } +// } +// `ManifestContract` + `as_contract` are public because the generated demo app lives +// outside edgezero-core. File-backed callers build `Present(local_ref)` / `None`; +// `run_demo` calls `::manifest.as_contract`. +// `ensure_capabilities` is `pub(crate)` because `run_demo` imports it from another +// edgezero-cli module. +pub(crate) fn ensure_capabilities( + adapter_name: &str, + manifest: ManifestContract<'_>, +) -> Result<(), String> { + let manifest = match manifest { + // No manifest ⇒ no capability contract to enforce. Legitimate. + ManifestContract::None => return Ok(()), + // Corrupt baked contract ⇒ we cannot know what was required. Refuse. + ManifestContract::Malformed(reason) => { + return Err(format!( + "capability check aborted: {reason}. This is an EdgeZero/app! contract \ + bug — the baked manifest is unreadable, so required capabilities \ + cannot be verified. Refusing to proceed rather than silently skipping \ + enforcement." + )); + } + ManifestContract::Present(manifest) => manifest, + // `ManifestContract` is `#[non_exhaustive]`; a future state we don't recognize is + // treated like `Malformed` — we cannot verify the contract, so we FAIL CLOSED. + _ => { + return Err( + "capability check aborted: unrecognized manifest-contract state. \ + Refusing to proceed rather than skipping enforcement." + .to_string(), + ); + } + }; + let caps = &manifest.capabilities; + let Some(adapter) = registry::get_adapter(adapter_name) else { + // Missing-from-registry policy (see table). If the manifest + // declares no capabilities, we can't verify anything anyway — log + // and proceed so brand-new shell-only adapters work before a stub + // is wired. Optional declarations still warn and proceed because optional never + // hard-fails; only a REQUIRED declaration is unverifiable and must fail closed. + if caps.required.is_empty() { + // No REQUIRED capabilities to verify. Optional ones can't be verified either, + // but "optional never hard-fails" (§3.5.3) — so warn and proceed rather than + // erroring. A hard fail here would break an optional-only manifest. + if caps.optional.is_empty() { + log::warn!( + "adapter '{adapter_name}' not in registry; capability check skipped (no capabilities declared)", + ); + } else { + log::warn!( + "adapter '{adapter_name}' not in registry; cannot verify its OPTIONAL \ + capabilities — proceeding, since optional capabilities never hard-fail", + ); + } + return Ok(()); + } + // At least one REQUIRED capability and no metadata to check it against → fail closed. + return Err(format!( + "adapter '{adapter_name}' is not in the registry; cannot verify REQUIRED \ + capabilities. Register an adapter stub that returns capability metadata, or \ + move those entries to `optional`.", + )); + }; + + // POSITIVELY accept only `Native | BoundedCooperative` for a REQUIRED capability. + // `CapabilitySupport` is `#[non_exhaustive]`; matching the two acceptable values and + // routing EVERYTHING else — `BestEffort`, `Unsupported`, and any FUTURE variant via + // `_` — to a rejection fails CLOSED. A `.filter(== Unsupported)` / `.filter(== + // BestEffort)` pair would let an unknown future support level pass a required gate. + let mut unsupported: Vec<&str> = Vec::new(); + let mut best_effort: Vec<&str> = Vec::new(); + for cap in caps.required.iter().copied() { + match adapter.capability(cap) { + CapabilitySupport::Native => {} + CapabilitySupport::BoundedCooperative => log::info!( + "adapter '{adapter_name}': required capability '{}' is bounded-cooperative; \ + see capability docs for the bound", + cap.as_str(), + ), + CapabilitySupport::BestEffort => best_effort.push(cap.as_str()), + // `Unsupported` AND any unknown/future variant → treated as unsupported. + _ => unsupported.push(cap.as_str()), + } + } + if !unsupported.is_empty() { + return Err(format!( + "adapter '{adapter_name}' does not support required capabilities: {}", + unsupported.join(", "), + )); + } + if !best_effort.is_empty() { + return Err(format!( + "adapter '{adapter_name}': required capabilities are only best-effort: {}. \ + best-effort means a documented limitation applies — timing (e.g. \ + unbounded cooperative enforcement) or functional (e.g. lazy streaming \ + becomes buffered). See the capability reference at \ + https://iabtechlab.github.io/edgezero/guide/capabilities. Declare them \ + `optional` if the documented limitation is acceptable.", + best_effort.join(", "), + )); + } + // Optional: warn on BestEffort, Unsupported, AND any unknown/future variant (the + // ladder logs all degradations, per §3.5.3); only `Native | BoundedCooperative` is + // "available" and silent. An unknown future support level is a degradation we can't + // characterize, so it warns rather than passing silently. + for cap in caps.optional.iter().copied() { + match adapter.capability(cap) { + CapabilitySupport::Native | CapabilitySupport::BoundedCooperative => {} + CapabilitySupport::Unsupported => log::warn!( + "adapter '{adapter_name}': optional capability '{}' unavailable", + cap.as_str(), + ), + CapabilitySupport::BestEffort => log::warn!( + "adapter '{adapter_name}': optional capability '{}' is best-effort — a \ + documented deviation applies; see the capability reference at \ + https://iabtechlab.github.io/edgezero/guide/capabilities", + cap.as_str(), + ), + _ => log::warn!( + "adapter '{adapter_name}': optional capability '{}' reports an unrecognized \ + support level; treating as degraded", + cap.as_str(), + ), + } + } + Ok(()) +} +``` + +**Resolve the execution target and its capability contract as one operation.** +`ManifestContract::None → proceed` is safe only when it means no contract belongs to the +target that will actually execute. Resolving `./edgezero.toml` while Spin independently +searches ancestors and workspace descendants for `spin.toml` lets the gate inspect one app +or no app and then build another. The shared resolver therefore returns both values: + +``` +// crates/edgezero-adapter/src/registry.rs — the exact cross-crate handoff. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct AdapterExecutionTarget { + app_root: PathBuf, + component: Option, + platform_manifest: Option, +} + +impl AdapterExecutionTarget { + pub fn app_root(&self) -> &Path; + pub fn component(&self) -> Option<&str>; + pub fn new( + app_root: PathBuf, + component: Option, + platform_manifest: Option, + ) -> Self; + pub fn platform_manifest(&self) -> Option<&Path>; +} + +// The existing `execute` remains the direct-discovery API. The CLI's resolved-runtime +// path calls this method, which may not rediscover from cwd or encode the target in args. +pub trait Adapter { + fn execute_target( + &self, + action: AdapterAction, + target: &AdapterExecutionTarget, + args: &[String], + ) -> Result<(), String>; +} + +// crates/edgezero-cli/src/manifest_source.rs +pub enum ResolvedAdapterTarget { + Registered(AdapterExecutionTarget), + Shell(ResolvedShellTarget), +} + +pub struct ResolvedManifest { + loader: ManifestLoader, + path: PathBuf, +} + +pub struct ResolvedRuntime { + action: Action, + adapter: String, + contract: Option, + target: ResolvedAdapterTarget, +} + +pub struct ResolvedShellTarget { + bind_host: Option, + bind_port: Option, + command: String, + environment: ResolvedEnvironment, + root: PathBuf, +} + +``` + +These declarations and their fields are alphabetized for the repository's denied +`arbitrary_source_item_ordering` lint. `AdapterExecutionTarget` lives in +`edgezero_adapter::registry`; its fields are private and its public constructor/accessors are +the only cross-crate handoff. The remaining fields are private to `manifest_source`. +`ResolvedRuntime` exposes only the alphabetically ordered crate-private read-only getters +`action()`, `adapter_name()`, `manifest()`, and `target()`; the contained +target/manifest types expose read-only getters for their listed fields. No `ResolvedRuntime` +constructor or mutator is exposed outside the resolver. + +`ResolvedRuntime` exists only for the outbound-gated runtime-producing actions `Build`, +`Serve`, `Deploy`, and `DeployStaging`. Its `ResolvedAdapterTarget` is action-specific and +complete: the shell variant owns every input the current shell dispatcher derives from a +manifest; the registered variant pins its app root and optional platform +manifest/component. `AdapterExecutionTarget::app_root` and `ResolvedShellTarget::root` are +absolute canonical directories. `ResolvedManifest::path` and any +`AdapterExecutionTarget::platform_manifest` are absolute canonical regular files contained +by the corresponding root; a platform manifest must also belong to the same selected app +target as the capability contract. Pre-dispatch code asks the target value whether the selected action is +shell- or registry-backed; it does not call `has_manifest_command` or independently resolve +an adapter manifest path after the pair is formed. + +Capture the invocation directory once. An explicit `EDGEZERO_MANIFEST` is authoritative; +resolve a relative value against that captured directory, load exactly that file, and +resolve the adapter target declared relative to its manifest root. Missing, malformed, or +conflicting explicit input fails without fallback. For default discovery, apply the +adapter's existing ancestor/workspace-descendant selection domain to **paired app +candidates**: each candidate `edgezero.toml` is loaded once and its declared target is +resolved with the existing canonical containment/regular-file checks. Select the same +candidate for contract and execution; reject ambiguity or a target/contract mismatch. +`contract: None` is reserved for a selected hand-written target for which discovery proved +there is no associated `edgezero.toml`, not merely no ancestor manifest from the invocation +directory. + +`run_build`, `run_serve`, and `run_deploy` pass one owned `ResolvedRuntime` into new +`execute_runtime(..)` or `execute_capture_runtime(..)` entry points. The scoped dispatcher +derives the adapter and action from that runtime, gates its `contract` exactly once, then executes its pinned `target`; +neither shell nor registry branches rediscover a path or reparse a manifest. A registry +adapter receives `AdapterExecutionTarget` through `Adapter::execute_target`; every in-tree +adapter implements that method by calling target-aware internal build/serve/deploy helpers. +The default trait implementation fails with an explicit "pinned target unsupported" error, +so an out-of-tree adapter cannot silently fall back to `execute` and rediscover cwd. The +existing `Adapter::execute` remains the direct adapter API and preserves its current +discovery behavior for callers outside the paired CLI path. The resolved-runtime dispatcher +must not inject paths/components into stringly passthrough arguments. +Shell overrides retain their declared arguments and run with `Command::current_dir` rooted +at the selected manifest; the CLI never mutates process-wide cwd or environment. A shell +can retarget itself, so the checked target is the declared contract, not a claim that an +arbitrary script's behavior was statically verified. Spin target/component validation must +remain available even when no Spin registry adapter is linked. + +The existing `execute(..)` / `execute_capture(..)` signatures and resolution behavior remain +unchanged for `AuthLogin`, `AuthLogout`, `AuthStatus`, `EmitVersion`, `Healthcheck`, and +`Rollback`. They do not construct `ResolvedRuntime`, enter the outbound capability gate, or +acquire a new manifest requirement. After a captured Fastly Deploy produces no version, +`run_deploy` invokes the existing registered `EmitVersion` path exactly as it does today; +that second operational action is explicitly outside the paired outbound resolver. Provision +and config commands are likewise unchanged. Any attempt to migrate all actions to one owned +runtime belongs in a separate CLI lifecycle specification. + +- **Required + `Unsupported` → hard failure** with an explicit message. +- **Required + `BestEffort` → hard failure.** `BestEffort` means a **documented + deviation from `Native`** — that can be timing (e.g. Fastly's unbounded source-stall + in `streamed-upload-deadlines`) or observational (e.g. Axum cannot prove that a + Hyper-accepted response frame reached the client). Either way the deviation is + real, the matrix footnotes describe it, and "required" should mean the deviation + is unacceptable. If degradation is acceptable, declare the capability `optional` + instead — the principle is "required means the matrix footnote's deviation is not + acceptable for this deployment." +- Required + `BoundedCooperative` → informational log (works, with a documented bound). +- Optional + **`BestEffort` OR `Unsupported`** → `log::warn!` naming the capability and + the degradation (the ladder logs *both*, not just Unsupported); never a hard fail. + +#### 3.5.4 Outbound host plumbing — not policy + +`[capabilities.outbound].hosts` is **plumbing**, not an application security allowlist +(non-goal §1.3). Applications still enforce target policy in handler code. + +- **Spin** requires `allowed_outbound_hosts` in `spin.toml`. The platform manifest + consumes the canonical list below; absent hosts preserve today's + `["https://*:*"]` default. The outbound runtime path for `build` / `serve` / + `deploy` resolves the component with existing CLI semantics: an explicit selector must + exist, one component is implicit, and zero or multiple components without a selector + fail. It compares only that selected component's canonicalized + `allowed_outbound_hosts` set before shell or registry dispatch; a union across components + could let one component's permission mask drift in the component being built. On drift, + fail with the `edgezero.toml` path, selected component, and expected canonical list. + Synchronization is manual: update that component field and rerun. Generation initializes + new projects; this spec does not promise a regeneration command for an existing + user-owned `spin.toml`. + + Every entry is canonicalized by the host-authority subset of `OutboundRequest`'s + URI rules (§3.1.3): lowercase scheme and host; strip `:443` for `https` and + `:80` for `http`; reject userinfo and fragments. Manifest host entries are + declarations, not request targets, so paths and queries are also rejected. Drift + comparison is set-based and order-insensitive. + + The absent default deliberately remains `["https://*:*"]`; it is not widened to + cleartext HTTP. Apps that need cleartext outbound access declare it explicitly. + + | Input form (after canonicalization) | Example | Spin output | + | --- | --- | --- | + | wildcard | `"*"` | `["http://*:*", "https://*:*"]` | + | scheme-prefixed | `"http://localhost:3000"`, `"https://api.example.com:8443"` | rendered as-is | + | `host:port` (no scheme) | `"api.example.com:8443"` | `"https://api.example.com:8443"` | + | bare host | `"api.example.com"` | `"https://api.example.com"` | + | wildcard subdomain | `"*.example.com"` | `"https://*.example.com"` | + + The §3.5.1 validator is authoritative; there is no fallback for other forms. + Mixing `"*"` with specific hosts is allowed. Bare hosts mean HTTPS on the default + port only. Canonicalization occurs once before both rendering and drift comparison. +- **Fastly** uses runtime dynamic backends, so it does not need the list at build time; + `hosts` is informational there. +- **Axum / Cloudflare** ignore the list because they require no host pre-declaration. + +## 4. Adapter-by-adapter implementation notes + +Each adapter renames `src/proxy.rs` → `src/outbound.rs`, replaces its `ProxyClient` +impl with an `OutboundHttpClient` impl, adds `capability()`, and gains a +`tests/contract.rs`. + +### 4.1 Axum — `crates/edgezero-adapter-axum` + +- `AxumProxyClient` → `AxumOutboundClient`; keeps the pooled `reqwest::Client`. +- Build that client with `no_gzip()`, `no_brotli()`, `no_deflate()`, and `no_zstd()` in + addition to the manifest's disabled default features. This makes the raw-byte boundary + explicit even if Reqwest feature selection changes later; only the shared EdgeZero + classifier/decoder may transform a response body. +- `start_batch_until` first snapshots `let batch_now = self.clock.now()` once, then runs a + **preflight** per slot: call `validate_for_dispatch(&request)` first; only a request that + passes that portable validator reaches the batch-only checks. Then any request whose `body` is + `Body::Stream` OR whose `response_mode` is `Streamed` is converted in place to + `Err(EdgeError::bad_request(..))` (§3.1.1) so the trait contract holds identically + on every adapter. Buffered survivors become complete-exchange futures in a + `FuturesUnordered`; the adapter polls every eligible future before yielding the first item and + emits each terminal completion with its original index. It passes that entry snapshot and the + observation cutoff to every per-slot `dispatch_budget(req, batch_now, Some(observation_cutoff))` — see §3.3.2 / + §4.3 for why a per-slot + second clock sample would drift the shared-deadline `duration` and (on Fastly) the + backend identity. +- A public single `send` takes its monotonic snapshot first, calls + `validate_for_dispatch(&req)` exactly once immediately afterward, then invokes the same + private already-validated flow used for batch survivors. +- The private prepared-request flow runs in this order: + 1. **Compute the budget.** `let budget = dispatch_budget(req, now, None)?` (§3.3.2 — + never an adapter-local formula, so `DEFAULT_NO_DEADLINE_BUDGET = 30 s` is + applied uniformly when no deadline is set). On expiry-before-dispatch this + returns `Err(gateway_timeout)` for the slot immediately. For a single `send`, + `now = self.clock.now()` is taken inline. + 2. **If the request body is `Body::Stream`, drain it to `Bytes` first.** Core + `Body::Stream` is `LocalBoxStream` (not the `Send + 'static` stream + `reqwest::Body::wrap_stream` requires), so Axum drains a streamed request body + into `Bytes` up to `req.max_request_body_bytes` (default 8 MiB) **before** + constructing the reqwest request. Pre-append checked accounting per §3.4.1; + over-cap → `payload_too_large` (413, `OutboundUpload`). Every pull uses this absolute-deadline protocol: + (a) call `budget.deadline.remaining()` immediately before the pull and return + attributed `gateway_timeout` if it is `None`; (b) race `source.next()` against + `tokio::time::sleep(remaining)`; (c) when the source arm becomes ready, call + `budget.deadline.is_expired()` **before** inspecting or accepting its chunk, + EOF, or error, and return the same timeout if expired. The post-ready check + makes the deadline win simultaneous source/timer readiness and prevents an + always-ready stream, including one yielding empty chunks, from starving the + timer past the absolute boundary. Only a source result proven ready before the + deadline proceeds to error propagation and pre-append cap accounting. Adding + reqwest's `stream` feature is **not** required. + 3. **Construct the reqwest request.** Build the `reqwest::Request` / + `RequestBuilder` from the buffered (or now-buffered) body, URI, method, + and normalized headers. Do not arm the timeout yet — it gets re-read + at the very last moment in step 4. + 4. **Arm the reqwest timeout and send.** Immediately before + `.send().await`, re-read `budget.deadline.remaining()`. If `None` (drain + + construction consumed the budget) → `gateway_timeout` without + sending. Otherwise `.timeout(remaining)` is set from this + just-re-read value, **not** from the cached value at end-of-drain and + **not** from the original `budget.duration`. Re-reading at arming time + (matching Spin's "at the moment the race starts" — round 21) closes + the construction-time gap that would otherwise let a 100 ms build + phase silently extend the SDK timeout past the absolute deadline. + reqwest's timeout covers the response-body read, so a `Buffered` + drain inherits the deadline. `Buffered` mode drains the response + body with a running decompressed-byte counter against `max_bytes` + (pre-append check per §3.4.1). `Streamed` mode wraps `reqwest`'s + byte stream with a `tokio::time::timeout`-per-chunk wrapper bounded + by `budget.deadline`; the wrapper yields a `gateway_timeout` (attributed via `budget.cause`, §3.3.2) error + chunk past the deadline so the streamed body honours the deadline + end-to-end per §3.3.3. +- **Downstream response delivery is connection-owned and lazy.** `run_app` serves HTTP/1 with a + private Hyper connection on a Tokio `LocalSet`. Its non-`Send` response body owns the sole + `ResponseEgressAttempt`, polls one portable chunk only when Hyper asks for a frame, and accounts + the frame before returning it. An independent connection timer closes the HTTP/1 connection at + the absolute write deadline even when Hyper is not polling the body. The deadline-owning + response reports `DeadlineExceeded`; other unsettled pipelined responses report + `TransportError`. There is no blocking bridge or whole-response collection path. +- Errors: `reqwest` timeout → **`gateway_timeout_caused(msg, budget.cause)`** (carries the + attribution — §3.3.2, NOT bare `gateway_timeout`); connect/DNS/TLS before a response + head → `bad_gateway_with_reason(.., Unreachable)`; transport failure after connection + progress → `Transport`; invalid upstream framing/completion → `Protocol`; shared JSON, + gzip, and Brotli failures → `Decode(Json)`, `Decode(Gzip)`, and `Decode(Brotli)`; + response over-cap → the exact + typed `response_too_large` reason (§3.4.5). Any completed exchange (incl. non-2xx) → + `Ok`. +- `capability()` per §3.5.2: `outbound-http` = `Native`, + `outbound-complete-resource-accounting` = `Unsupported`, + `outbound-header-fidelity` = `Native`, `outbound-deadlines` = `Native`, + `outbound-failure-diagnostics` = `BestEffort` (reqwest predicates and optional I/O evidence), + `outbound-flexible-phase-budget` = `Native` (Axum's reqwest exposes a single total + timeout, not a phase split), `outbound-batch-slot-isolation` = `Native`, + `streamed-upload-deadlines` = `Native`, `lazy-streamed-response-passthrough` = + `Native` (footnote 3 — direct non-`Send` body and raw-socket tests prove no collection). +- Reference adapter for the contract (§5): real loopback HTTP. + +### 4.2 Cloudflare — `crates/edgezero-adapter-cloudflare` + +- `CloudflareProxyClient` → `CloudflareOutboundClient` (stays stateless). +- `start_batch_until` first snapshots `let batch_now = self.clock.now()` once, then runs a + **preflight** per slot: call `validate_for_dispatch(&request)` first; only a request that + passes that portable validator reaches the batch-only checks. Then any request with `Body::Stream` + OR `response_mode = Streamed` is converted to `Err(EdgeError::bad_request(..))` + per §3.1.1 before provider construction. It passes that entry snapshot and the observation cutoff to + every `dispatch_budget` call. Buffered survivors become complete Fetch-exchange futures in a + `FuturesUnordered`; the adapter polls every eligible future before yielding and associates each + terminal completion with its original index. The Workers event loop provides the concurrency. +- A public single `send` takes its monotonic snapshot first, calls + `validate_for_dispatch(&req)` exactly once immediately afterward, then invokes the same + private already-validated flow used for batch survivors. +- The private already-validated flow runs in this order: + 1. **Compute the budget.** `let budget = dispatch_budget(req, now, None)?` (§3.3.2). + Expiry before dispatch returns `Err(gateway_timeout)` for the slot. + 2. **If the request body is `Body::Stream`, drain it to `Bytes` first.** Up to + `req.max_request_body_bytes` (default 8 MiB), pre-append checked accounting; + over-cap → `payload_too_large` (413, `OutboundUpload`). Every pull uses the same absolute-deadline protocol + as Axum: read `remaining()` immediately before the pull and fail with an + attributed `gateway_timeout` when absent; race `source.next()` against + `worker::Delay::from(remaining)`; then, if the source arm becomes ready, + recheck `is_expired()` **before** accepting its chunk, EOF, or error. Timeout + wins simultaneous readiness. Also apply the host-event yield quota below: + post-ready clock checks alone do not prevent starvation on Cloudflare. + Source errors and cap accounting run only after the deadline checks. + 3. **Construct the `worker::Request`.** Build the request from the + buffered (or now-buffered) body, URI, method, and normalized headers. + Set `worker::RequestInit::redirect` to `RequestRedirect::Manual` here and assert + `request.inner().redirect()` is manual. Do not add a `Host` header: pass the exact + canonical URI serialization as the string supplied to `worker::Request` and let Fetch + derive Host from that authority. The Web API may apply its own WHATWG + parse/serialization before network dispatch, so no test claims byte-for-byte equality + between that input string and the final wire URL. Native bridge tests assert the + supplied string; workerd/deployed tests assert the effective origin-observed + scheme/authority/path/query and Host against the corresponding WHATWG canonical + semantics, including dot segments, numeric IPv4 aliases, IDNA, percent encodings, + empty paths, and queries. + Do not start the `worker::Delay` race yet. + 4. **Prepare raw, signal-aware fetch options, then arm the race and send.** + Create a `worker::AbortController` owned by the guard below and take its `signal()`. + The adapter-private `fetch_raw_with_signal` helper follows worker 0.8.5's + `global.rs::fetch_with_request` bridge, but supplies **both** the abort signal and + top-level `encodeResponseBody: "manual"` in the **final fetch options**: + + - Create `worker::web_sys::RequestInit` and call `set_signal(Some(&**signal))` + where `signal: &worker::AbortSignal`. + - Set the `encodeResponseBody` property to the JS string `"manual"` with + `worker::js_sys::Reflect::set`. Handle both an exception and `Ok(false)` as + `internal` setup failures with no dispatch; never ignore a failed property set. + - Use the SDK's `WorkerGlobalScope` bridge to call + `fetch_with_request_and_init(request.inner(), &init)`. Await the Promise through + `worker::wasm_bindgen_futures::JsFuture`, cast the result to + `worker::web_sys::Response` with `JsCast`, then convert it to `worker::Response`. + Fetch rejection before a response head is `bad_gateway` reason `Unreachable` only when + platform evidence establishes DNS/connect/TLS establishment failure. The current Workers + Fetch bridge exposes a generic rejected promise without a stable phase discriminator, so + current generic rejections are `Unspecified`; expiry still wins. An unexpected JS response + type is an `internal` binding invariant failure. + + Redirect mode remains on the already-constructed Request; the final raw + `web_sys::RequestInit` owns only the abort signal and response-encoding override and + must not reset redirect behavior. + + The property setup completes **before** issuing fetch. Immediately before the + fetch call and timer arming, re-read `budget.deadline.remaining()`; `None` → + `gateway_timeout_caused(.., budget.cause)` without sending. Otherwise race the + helper's fetch/response conversion **and**, + in `Buffered` mode, the body drain against `worker::Delay::from(remaining)` + (worker 0.8.5 — `Delay` has private fields, so `worker::Delay(remaining)` tuple + construction does NOT compile; use the `From` impl). **On expiry, + abort through the guard.** Dropping the underlying fetch future alone does not + cancel the in-flight subrequest; dropping the guarded send future cancels it through + the guard's `Drop`. Return `gateway_timeout_caused(.., budget.cause)` on expiry. + The existing gzip/deflate/br + decompression path is kept; the independent decoded-output cap is enforced + incrementally for identity and EdgeZero-decoded output (§3.4.1). The final Buffered + collection cap is enforced separately for every coding disposition. +- **Raw-response option placement is mandatory.** Workerd's + [request initializer](https://github.com/cloudflare/workerd/blob/f2300a54038995b2ceaf0851447aa5b5b36cda13/src/workerd/api/http.h) + defaults to automatic response decoding; `encodeResponseBody: "manual"` returns raw + encoded bytes. Worker 0.8.5's high-level `RequestInit` does not expose this option, + and `Fetch::send_with_signal` constructs final options containing only the signal. + Setting the property on an earlier Request is insufficient: the pinned workerd + [Request-copy path](https://github.com/cloudflare/workerd/blob/f2300a54038995b2ceaf0851447aa5b5b36cda13/src/workerd/api/http.c%2B%2B) + can reset its response-encoding mode. The helper must set it at the final fetch call. + It is a top-level request option, not a `cf` property, and is independent of the + downstream `EncodeBody::Manual` setting. Keep ordinary `worker::Request` construction; + no raw Request re-wrapping or post-construction mutation is needed. Worker re-exports + `web_sys`, `js_sys`, `wasm_bindgen`, and `wasm_bindgen_futures` with the necessary + bindings, so this bridge adds no direct dependency. Phase 4 pins a workerd harness + that implements this option and verifies actual raw bytes; successful `Reflect::set` + alone does not prove the host honors it. +- **Host-event fairness is required, not just repeated clock reads.** In deployed + Cloudflare Workers, `performance.now()` (and therefore `MonotonicInstant`) advances + only after I/O, unlike the normal local workerd clock + ([Cloudflare timing semantics](https://developers.cloudflare.com/workers/runtime-apis/performance/)). + `worker::Delay` depends on a `setTimeout` callback. An always-ready empty source can + otherwise keep the clock frozen, starve that callback, and never hit the byte cap. + Apply a fixed, nonzero adapter-private ready-item quota of at most 64 items between + explicit host-event yields, counting empty items. The Phase 4 authoring gate must select + a primitive that a deployed timing probe proves both yields to another host event and + permits the production clock to advance. The adapter uses a cancellable + `globalThis.setTimeout(0)` future because worker 0.8.5's `Delay` binding does not execute + under the pinned Node WASM runner; the future owns its callback and timer handle and clears + the timer when dropped. After the yield, recheck the original absolute + deadline before processing another item. Count state persists across Rust polls, stream + yields, and `next()` calls; reset it only after the proven host-event yield completes. A + self-wake, `Poll::Pending` round trip, microtask-only yield, or fake clock that advances + independently of the host event is insufficient. + + This discipline applies to request-source draining, raw response input **before** the + stream-to-reader bridge, and decoded-output processing, including the completion + protocol in §3.4.1. Raw-input enforcement must interrupt ready-empty loops even when + the decoder emits no output. Before returning/yielding terminal success, EOF, or error, + and before an adapter-owned cap error, perform a host-event yield and recheck expiry; + a terminal result must not use a clock snapshot frozen throughout its CPU work. + The generic streamed consumer's cap decision after a within-budget yield retains + §3.4.1's narrower deadline ownership; this does not add request-budget state to core + helpers. Existing per-read remaining-budget races may remain: neither progress nor + host yielding reanchors the absolute deadline. All yield futures and native handles + remain owned, so cancellation during a yield follows the same cleanup rules below. + This is cooperative scheduling between bounded processing steps, not preemption of + arbitrary synchronous work inside one source poll. +- **One abort guard owns every in-flight subrequest.** Arm an adapter-private guard + before polling fetch; it owns the `AbortController` and calls `abort()` on early + termination. In Buffered mode it remains owned through metadata normalization, + bodyless/205 settlement, decoding, and bounded draining. Cap overflow (including + identity `Content-Length` rejection), malformed metadata, decoder/read failure, + timeout, and an early 205 abort disposition all abort the subrequest before return. + Dropping the send future while it owns a live subrequest also aborts via the guard's + `Drop`. Preserve each restored `EdgeError` unchanged. Native transport and genuine + codec/trailing-data failures are `bad_gateway`, cap overflow is `response_too_large`, + and the already-expired adapter budget wins with + `gateway_timeout_caused(.., budget.cause)`. An early 205 disposition still returns an + empty 205 after aborting, as specified in §3.4.1. Disarm only after normal native-body + settlement and successful decoding; headers or decoded EOF alone do not prove that + the native body has completed. Abort is idempotent, and the guard records settlement + so explicit abort followed by `Drop` does not repeat it. Native transport failures use + `BadGatewayReason::Transport`; protocol/completion failures use `Protocol`; shared + codec/trailing-data failures use `Decode(Gzip)` or `Decode(Brotli)`. In worker 0.8.5, + `AbortController::abort(self)` consumes the controller: store it as an `Option` and + `take()` it when aborting or disarming; `Drop` aborts only a remaining `Some`. + A successful Streamed return moves the still-armed guard into the decoded-output + wrapper without an ownership gap. +- **205 host suppression.** If workerd exposes a null body for 205, apply the §3.4.1 + branch: positive visible `Content-Length` aborts immediately; absent/zero length plus + null body is accepted as host-suppressed clean EOF without calling `stream()`. A non-null + body receives the ordinary one-read disposition. The adapter does not claim visibility + into bytes workerd may have suppressed before the guest boundary. +- **Streamed responses honour the deadline AND cancel the subrequest.** Wrap the + response body as `Body::Stream`, with a per-chunk race against a `worker::Delay` + bounded by `budget.deadline`. **The abort guard moves into the decoded-output wrapper**, + because the subrequest is still live while the body streams. The guard aborts in + **all three** cases, but what it yields differs by cause, so the status is not + conflated: (a) the deadline `worker::Delay` fires → abort **+ a + `gateway_timeout_caused(.., budget.cause)` (504) error chunk**; (b) a read, decoding, + or completion failure → abort **+ the restored `EdgeError` unchanged**; native + transport and genuine codec/trailing-data failures map to typed-reason `bad_gateway` + (502), while expiry of the adapter deadline wins over either class; (c) the **consumer drops the stream early** (`Drop` on the wrapper) → + **abort only, no error chunk** (the consumer that dropped is not waiting for one). + Merely yielding an error chunk without `abort()` (an earlier draft) leaves the CF + subrequest running after EdgeZero has stopped reading — the same bug the buffered path + fixes. So the deadline is honoured end-to-end and a mid-stream transport failure maps to + 502 (not 504). The guard invokes `abort()` on every early-exit case. Origin-observed + cancellation is required only for probes that deliberately keep the origin active after + that trigger; a transport/completion failure after the origin already closed cannot + produce a second observable disconnect and is not used as cancellation evidence. +- Errors: `worker::Delay` expiry → + **`gateway_timeout_caused(msg, budget.cause)`** (attributed — §3.3.2); + `worker::fetch` DNS/TLS/connection-establishment failures before a response head → + `bad_gateway_with_reason(.., Unreachable)` when provider evidence establishes that + category, otherwise `Unspecified`; later transport failures → `Transport`; invalid + visible upstream framing/completion → `Protocol`; shared JSON/gzip/deflate/Brotli failures → + `Decode(Json)` / `Decode(Gzip)` / `Decode(Brotli)`; **request**-body over-cap → + `payload_too_large` (413, `OutboundUpload`); response-resource failures preserve their exact + `ResponseLimitReason`. A provider error with no defensible narrower category uses + `Unspecified`. Any completed exchange (incl. non-2xx) → `Ok`. +- **Method / body preflight — no silent coercion.** The historical adapter mapped + unsupported methods to `GET`; the outbound implementation removed that behavior. Per §3.1.4, + a non-portable method + or a `GET`/`HEAD` carrying a body is rejected in **core preflight** with + `bad_request`, identically on every adapter. The CF adapter never rewrites the + method to satisfy `fetch`'s restrictions. +- **Multi-value response headers — EdgeZero-owned behavior, not a platform limit.** + `worker::Headers` has both `append(&self, ..)` and `get_all(&self, ..)`, and this + repo's pinned `compatibility_date = "2023-05-01"` already enables workerd's + per-`set-cookie` `entries()` behaviour. `set-cookie` is therefore **fully + preservable** on CF. The outbound and response converters append every value rather than + replacing prior values, and the shared header bridge decodes `HeaderValue::as_bytes()` with + `std::str::from_utf8` instead of calling the stricter `HeaderValue::to_str()`. Valid UTF-8 + such as `café` survives; invalid UTF-8 returns the typed request-validation error before + fetch, never an unwind or lossy conversion. Contract tests pin repeated `set-cookie` values + in both directions and the non-ASCII request path. +- **What stays irrecoverable (§3.1.4):** repeated **non**-`set-cookie` headers are + comma-joined by workerd (`x-foo: a, b`) with no API to recover the separate field + lines, and raw upstream bytes / invalid UTF-8 are lost before the guest sees them. + **Never call `get_all` with any name but `"set-cookie"`** — the binding lacks `catch` + and workerd throws, unwinding across the wasm boundary. +- **Encoded passthrough must stay byte-preserving.** When the portable content-encoding + policy chooses passthrough (unknown, parameterized, or stacked/repeated encoding), the + core response retains the raw bytes and visible `content-encoding`. The Cloudflare + response-out converter MUST then select `worker::EncodeBody::Manual` (via + `Response::with_encode_body`) whenever it forwards an already encoded body; leaving the + default automatic mode lets Workers transform bytes that EdgeZero promised to pass + through unchanged. Known gzip/deflate/br responses decoded by EdgeZero have their encoding + header removed and use the normal automatic/identity path. Workers can recompute or + ignore a user `content-length` for a streamed response, so exact downstream-wire + `Content-Length` retention is part of Cloudflare's documented + `outbound-header-fidelity = BestEffort` deviation; the app-visible header decision and + raw encoded payload remain deterministic. +- `capability()` follows the outbound matrix in §3.5.2. `outbound-http`, + `outbound-flexible-phase-budget` (single `worker::Delay` for the total race, no per-phase + split), `outbound-batch-slot-isolation`, `outbound-batch-completion-order`, + `outbound-cache-bypass`, and `lazy-streamed-response-passthrough` are `Native`. + `outbound-deadlines` and + `streamed-upload-deadlines` are `BestEffort` because the owned abort path lacks deployed + host-observed cancellation evidence. `outbound-header-fidelity` is independently + `BestEffort` because workerd removes raw-octet/field-line information before the guest, + `outbound-failure-diagnostics` is `BestEffort` because Fetch exposes limited exception kinds, + and `outbound-complete-resource-accounting` is `Unsupported`. + Cloudflare's WASM single-threaded guest carries no + `Send` constraint, so `worker::Body::from_stream` consumes the core `Body::Stream` + directly **in the response-out direction** + (`lazy-streamed-response-passthrough` — see §7 `src/response.rs`). The + **outbound-request upload direction** still drains `Body::Stream` to `Bytes` + first (bounded by `max_request_body_bytes`, raced against `budget.deadline`), + because `send_async`-style streamed uploads aren't part of this migration and + the worker SDK's request-body shape differs from `Body::from_stream`. Don't + conflate the two — `send_one`'s flow above is the request side; this bullet is + the response side. + +### 4.3 Fastly — `crates/edgezero-adapter-fastly` + +**Backend/timing override:** the B31 checkpoint in §1 supersedes this section's older +client-namespaced/exact-budget cache, rigid phase split, and overshoot derivations. The old +pseudocode is retained for history only on those subjects. Dispatch-before-harvest, typed errors, +ownership, normalization/caps, and the closed custom lifecycle remain current; this checkpoint +does not replace pending selection/body reads or remove dispatch guards. + +The Fastly outbound implementation uses **dispatch-all-then-select**. This replaced the +historical `proxy.rs` flow, which called `send_async_streaming()` and then +`pending_request.wait()` inside one `send()` and therefore serialized a `join_all` batch. + +Confirmed `fastly` 0.13.1 API: + +```rust +// fastly::http::request +pub fn select>(pending_reqs: I) + -> (Result, Vec); // no index returned +pub fn select_handles>(pending_reqs: I) + -> ( + Result<(ResponseHandle, BodyHandle), SendErrorCause>, + usize, + Vec, + ); // reports the selected position +pub enum PollResult { Pending(PendingRequest), Done(Result) } +// PendingRequest::poll(self) -> PollResult (non-blocking) +// PendingRequest::wait(self) -> Result (blocks on one) +// Request::send_async(self, backend) -> Result +``` + +The high-level `select` discards the selected position, so the adapter uses `select_handles`. +Before each call it records every unique handle's original slot index; the returned selected +position identifies the terminal slot, and returned remaining handles are re-associated by handle +identity because their order is not stable. Provider request/response handles never cross the +core API. The selected response handles are immediately reconstructed as a Fastly `Response` and +drained under that slot's policy: + +```rust +// Dispatch consumes `OutboundRequest`, so each pending slot retains every setting needed to +// finish and bound its buffered response. Provider handles remain adapter-private. +struct PendingSlot { + metadata: PendingSlotMetadata, + pending: PendingRequestHandle, +} + +struct PendingSlotMetadata { + index: usize, + budget: DispatchBudget, + max_decoder_bytes: u64, + max_brotli_window_bits: u8, + max_decoded_response_bytes: Option, + max_encoded_response_bytes: Option, + max_response_header_bytes: Option, + max_response_header_count: Option, + request_method: Method, + response_mode: ResponseMode, +} + +fn start_batch_until( + &self, + requests: Vec, + observation_cutoff: Deadline, +) -> OutboundBatch { + let batch_started_at = self.clock.now(); + let slot_count = requests.len(); + if observation_cutoff.is_expired_at(batch_started_at) { + return OutboundBatch::cutoff(slot_count); + } + + // Validate and synchronously issue every eligible request before selecting any response. + // `validate_for_dispatch` runs before the buffered-batch-only checks, preserving the shared + // GET/HEAD body-error precedence. A preflight/dispatch failure becomes a terminal item only + // when it is observed before the observation cutoff. + let mut completed = Vec::new(); + let mut pending = Vec::new(); + let mut cutoff_reached = false; + for (index, request) in requests.into_iter().enumerate() { + match self.prepare_batch(request, batch_started_at, observation_cutoff) + .and_then(|prepared| self.dispatch_batch_slot(index, prepared)) + { + Ok(slot) => pending.push(slot), + Err(error) => { + let Some(item) = finish_batch_item( + index, + batch_started_at, + self.clock.now(), + observation_cutoff, + Err(error), + ) else { + cutoff_reached = true; + break; + }; + completed.push(item); + } + } + } + + let clock = self.clock.clone(); + OutboundBatch::from_driver(slot_count, stream! { + for item in completed { + yield OutboundBatchDriverEvent::Item(item); + } + if cutoff_reached { + yield OutboundBatchDriverEvent::Cutoff; + return; + } + while !pending.is_empty() && !observation_cutoff.is_expired_at(clock.now()) { + // `select_pending_slot` calls `select_handles`, restores metadata by raw handle + // identity, and supports bounded groups up to Fastly's MAX_PENDING_REQS. + let (selection, metadata) = match select_pending_slot(&mut pending) { + Ok(selected) => selected, + Err(error) => { + yield OutboundBatchDriverEvent::Failed(error); + return; + } + }; + let index = metadata.index; + let outcome = finish_selected(selection, metadata, &clock).await; + let Some(item) = finish_batch_item( + index, + batch_started_at, + clock.now(), + observation_cutoff, + outcome, + ) else { + yield OutboundBatchDriverEvent::Cutoff; + return; + }; + yield OutboundBatchDriverEvent::Item(item); + } + if !pending.is_empty() { + yield OutboundBatchDriverEvent::Cutoff; + } + }) +} +``` + +- **Selection begins only after dispatch.** Every eligible request is handed to `send_async` before + the first `select_handles` call. Fastly dispatch remains sequential and synchronous, so this is + attempt-before-harvest rather than a guarantee of simultaneous overlap. Preflight/dispatch + failures are yielded before selected host completions; completion ordering therefore remains + `BestEffort`. +- **Deadline:** each request's host timeouts are set to the effective budget at dispatch, + so connect+headers cannot block past it. The body phase checks `budget.deadline` + **after every blocking body read returns, including the EOF read** (per §3.3.4 — + the read that discovers EOF can itself cross the deadline and would otherwise + slip through with `Ok(resp)`). Streamed bodies are wrapped to check before and + after each underlying read. Bounded overshoot per §3.3.4. +- **Cancellation / drop semantics.** Dropping `OutboundBatch` drops every retained pending handle + and stops guest observation, but Fastly exposes no finite host-cancellation proof for those + already-issued requests. A selected response body is still drained synchronously, so cancellation + cannot preempt a blocking host body read. A sibling request deadline never actively cancels + another slot. The observation cutoff ends the driver and leaves remaining slots unresolved; a provider-selection + failure that prevents the driver from preserving its slot invariants is a batch-level `Internal` + error and is never represented as unresolved work. +- **Dynamic backends.** Arbitrary HTTPS hosts use Fastly dynamic backends + (`Backend::builder`). Per Fastly's + [`BackendBuilder` docs](https://docs.rs/fastly/latest/fastly/backend/struct.BackendBuilder.html), + the **session-uniqueness rule is unconditional** — a dynamic backend name must + not match the name of any static service backend nor any other dynamic backend + built during this session. `NameInUse` carries no property-comparison + semantics: the SDK signals only "this name is taken in this session," and its + documented recovery (`Backend::from_str(name)`) returns a handle without + exposing the registered properties. EdgeZero therefore owns the entire + uniqueness story **at the guest layer**: a **session-scoped** adapter-local cache + (a `Mutex>` field on the + request-context `FastlyOutboundClient` — `Mutex` not `RefCell`, because the trait is + `Send + Sync`; see *Cache ownership* below) holds the identity → + backend mapping, and a hit + reuses the cached `Backend` while a miss calls `Backend::builder(..).finish()` + exactly once. Because EdgeZero hashes every relevant property into the + backend name (`ez_{sha256_128(identity)}_{client_namespace}`), distinct identities map to + distinct names — so a 50 ms slot and a 3 s slot to the same host get distinct + backends by construction, not by SDK-side property comparison. A + process-local atomic counter assigns a distinct namespace to every client instance; + this prevents two separately constructed clients in one host session from reusing + an uninspectable registration. Clones of one client retain the same cache and namespace. + `NameInUse` on a name **not** in the adapter's collision map can therefore + only mean an externally-registered backend (a static service backend, or another + component in **this** session — NOT a prior session, whose names are gone) is squatting the name — fail-closed `EdgeError::internal` because + the SDK does not let us prove identity match. The precise collision-detection + protocol is in the §4.3 algorithm later in this section. + + Identity tuple: + `scheme + ":" + host + ":" + resolved_port + ":" + tls_mode + ":" + budget_ms`, + where: + - `resolved_port` is the URI port or scheme default (`80`/`443`). + - `tls_mode` is `"tls"` for `https` or `"plain"` for `http`. + - `budget_ms` is the **exact true-ceil-to-ms** of `dispatch_budget(req).duration` — + `((duration.as_nanos() + 999_999) / 1_000_000).max(1)`, lint-clean form in the + `fastly_timeout_ms` helper. **No bucketing.** The cache is **per-session** (below), + so it cannot grow unbounded across requests — earlier drafts bucketed the budget to + bound a *cross-request* thread-local cache, but that cache model was wrong. Dynamic + backend registration names are session-scoped. Connection pooling is a separate SDK + feature. Fastly 0.13.1 reuses a connection only when both the backend name and every + backend setting are identical; the name below incorporates the exact rounded + budget, host, port, scheme, TLS mode, and client namespace. EdgeZero leaves the SDK + default pooling enabled, but a newly constructed client does not intentionally reuse a + previous client's connection by name. Within one client instance, + `send_all_until` + shares a single `batch_now`, so same-budget slots to the same host compute the + **same** `budget_ms` → one backend; distinct budgets → distinct backends. The cache + is therefore bounded by the number of distinct `(host, budget)` pairs in the + session's fan-out (≤ batch size), with **no** millisecond drift to bucket away. + - The **host timers use `budget_ms` exactly** (`connect-timeout` / + `first-byte-timeout` / `between-bytes-timeout` derive from the exact value), so the + headers phase is bounded by `budget.duration` (+ ms-rounding + `BATCH_DISPATCH_SLACK_MAX`), + **not** a looser bucket. The body phase is additionally enforced to the millisecond by + the cooperative `budget.deadline.is_expired()` check against the original `Deadline`. + Removing the bucket removes the ~10 % headers-phase overshoot an earlier draft + introduced; the net guarantee is again exactly what §4.3 "Net guarantee" states. + - `budget_ms` is the **true ceil-to-ms** — `((duration.as_nanos() + 999_999) / + 1_000_000).max(1)` (lint-clean form in `fastly_timeout_ms`), since `as_millis()` + floors and would make the host timeout too tight. (Apps wanting sub-ms wall-clock + should not target Fastly — host timeouts are millisecond-granular.) §3.3.4's "host + timeouts = `budget.duration`" is an abbreviation for "= ceil-to-ms of `budget.duration`". + + A 50 ms slot and a 3 s slot to the same host get **distinct** backends (distinct + `budget_ms` → distinct identity → distinct name) — they must, since their host timeouts + genuinely differ. Within one `send_all_until`, same-budget slots share `batch_now` → identical + `budget_ms` → one backend. + + Name = `format!("ez_{:032x}_{client_namespace}", sha256_128(identity))` — the first + 128 bits of a SHA-256 digest plus a process-local per-client counter. The digest + is collision-resistant in any realistic deployment (the previous 64-bit FNV-1a + draft was not). The name fits inside Fastly's backend-name length limit and is + valid for any host. In a + homogeneous-budget batch all slots targeting the same host + share one backend — **but only because `send_all_until` takes a single `now` snapshot + and passes it to every per-slot `dispatch_budget` call** (§3.3.2). Without that, + sequential `MonotonicInstant::now()` per slot would derive slightly different `duration`s + for the same shared caller `Deadline`, which would produce slightly different + ceiled `budget_ms` values and therefore different identities for the same host + under one batch deadline. The shared-`now` snapshot is a normative requirement + of the `send_all_until` flow, not an implementation hint. In heterogeneous-budget + fan-out each distinct budget gets its own backend, by design. Per-handler + backend count is bounded by `unique(host, port, tls, budget_ms)` tuples; apps + that mix wildly varying budgets should be aware of the dynamic-backend limit on + their Fastly service. Because Fastly dispatch is sequential, a multi-host or + heterogeneous-budget batch may require several synchronous registrations. If an earlier + registration or adapter work consumes more than the 25 ms dispatch slack while a later + slot's deadline remains live, the later cache miss fails as `Internal` at the + pre-registration guard without invoking another `finish()` host call. + + **Dispatch-overhead slack — hard-bounded for a CACHED backend, fail-closed-*detected* + for first-time registration** (see the honesty note after the bullets). Because `batch_now` is captured + *before* preflight, dynamic-backend creation, and `send_async`, the `budget_ms` + baked into the backend identity is a *snapshot* timeout (computed from `batch_now`) — + not the exact remaining wall-clock at the moment the SDK timer is armed. The Fastly host enforces + `budget_ms` from the moment it sees the request, so a request can in principle + complete up to `(now_at_send_async − batch_now) ms` after the absolute fan-out batch + deadline before the host fires its timeout. To keep this slack + **deterministically bounded** for warm/cached adapter setup and host-timer arming (not + request-body transmission, whose write gap remains unbounded). The capability itself is + `BestEffort` because both the cold path below and non-empty request writes have no such + bound: + + - The adapter caps `(now_at_send_async − batch_now)` at + `pub const BATCH_DISPATCH_SLACK_MAX: Duration = Duration::from_millis(25);` + (defined alongside `DEADLINE_FAR_FUTURE` in `src/time.rs`, §7). + - On a batch cache miss, the adapter performs this check before synchronous backend + registration. Every slot performs it again immediately before `send_async`. Each check + evaluates two things **in this order**: + **(1) the absolute deadline FIRST** — `if budget.deadline.is_expired() { return + Err(EdgeError::gateway_timeout_caused("deadline expired during Fastly dispatch", + budget.cause)); }`. A cold `Backend::builder(..).finish()` can block past the deadline + (§4.3 honesty note), and when it returns *after* the deadline, that is a genuine + **504 timeout**, NOT an EdgeZero bug — so it must be an attributed `gateway_timeout`, + never `internal`. **(2) THEN, only if time still remains** (`!is_expired()`) but the + adapter overhead exceeded the slack, `MonotonicInstant::now() - batch_now > + BATCH_DISPATCH_SLACK_MAX` → the remaining slots fail closed with + `Err(EdgeError::internal("Fastly batch adapter overhead between batch_now \ + and SDK arming (preflight + dynamic-backend lookup/creation + SDK setup) \ + exceeded BATCH_DISPATCH_SLACK_MAX; refusing to arm SDK timers with stale \ + duration"))`. So `internal` is reserved for **excess adapter overhead while the + deadline still had time** — a real "our setup is too slow" signal — and never for an + actual expiry. This is an internal diagnostic about **adapter-side** work, + not a handler-side complaint — handler code runs before `start_batch_until` is even + invoked, so it runs before `batch_now` is captured and cannot exhaust this + budget. The interval measured here is adapter overhead: per-slot preflight + validation, dynamic-backend lookup/creation host calls, and SDK setup + before `send_async`. If this fires in production, the operator looks at + backend-creation hostcall latency or a noisy neighbour, not at handler + code. + - The cooperative `budget.deadline.is_expired()` check during body drain still + catches body-phase overshoot per §3.3.4 (one between-bytes-timeout bound). + + **Honesty note — what the guard can and cannot do.** Batch dispatch checks both before a cold + `Backend::builder(..).finish()` and immediately before `send_async`; single `send` retains + the latter check. The pre-registration check prevents later heterogeneous host/budget slots + from beginning more synchronous registrations once earlier adapter work has exhausted the + dispatch slack. It cannot preempt the first admitted `finish()` after that host call starts. + The post-registration check detects any overshoot. That distinction splits the claim in two: + + - **Cache hit (no `finish()` call).** The measured interval is pure adapter compute — + preflight, a map lookup, SDK setup. It is genuinely bounded, and the setup/arming + portion holds the stated `BATCH_DISPATCH_SLACK_MAX + ms_rounding` bound. This does not + turn a body-bearing exchange into a finite end-to-end bound because the subsequent + host write remains untimed. + - **First-time dynamic-backend registration.** `finish()` is a **synchronous host + call that can block** — Fastly may make it wait for a service-wide dynamic-backend + slot. Nothing guest-side can interrupt it. If it blocks past the deadline, the + wall-clock overshoot has *already happened* by the time the post-registration guard runs — so it + **checks the absolute deadline FIRST and returns an attributed `gateway_timeout` (504)** + (the honest outcome for a real expiry). It does **not** call this `internal`: `internal` + is reserved for the *other* case — adapter overhead exceeding the slack **while the + deadline still has time** (a genuine "our setup is too slow" bug), never for a + time-that-actually-ran-out. **On this path the dispatch+headers phase is `BestEffort` for + wall-clock, not bounded** — the guard detects the overshoot and surfaces it as a 504, it + does not prevent it. + + So the honest one-line statement, which the matrix footnote and §5.4 rows must match: + *Fastly declares `outbound-deadlines` = `BestEffort`. The warm/cached path enforces a + documented bound, but a first-time `finish()` registration can overshoot the deadline; the + guard checks the absolute deadline first and returns an **attributed `gateway_timeout` + (504)** for that expiry (NOT `internal` — `internal` is reserved for adapter overhead + exceeding the slack while time still remains), and `capability()` cannot tell warm from + cold — so the static value is `BestEffort`, and a `required outbound-deadlines` hard-fails + on Fastly.* Apps + needing an exact absolute deadline on the dispatch+headers phase — including the first + request to a new host — target Axum. Cloudflare and Spin also avoid Fastly's blocking + registration step, but their cooperative host-teardown semantics keep + `outbound-deadlines` BestEffort (footnote 8). + + Net guarantee, with the explicit **sub-4 ms branch** broken out separately. **Both + branches below hold on the WARM path only — an already-registered (cached) backend, no + `finish()` in the measured interval.** On the first request to a new host, + `Backend::builder(..).finish()` can block unboundedly (per the honesty note above), so + these equations do NOT apply and the phase is `BestEffort`; that is exactly why the + capability is `BestEffort` (footnote 1). The bounds: + + - **`total_ms ≥ 4` (the common case), cached backend**: a Fastly slot can complete at most + **`BATCH_DISPATCH_SLACK_MAX + ms_rounding`** past the absolute fan-out batch + deadline on the dispatch+headers phase. Because connect and first-byte are + *separate* host timers (Fastly docs), the budget is split — `connect_ms = + total_ms / 4`, `first_byte_ms = total_ms - connect_ms` — so their sum equals + `total_ms` exactly and the dispatch+headers host enforcement is bounded by + `budget.duration`. If dispatch happens at `batch_now + Δ` with + `Δ ≤ BATCH_DISPATCH_SLACK_MAX`, the host fires at + `(batch_now + Δ) + (connect_ms + first_byte_ms) = (batch_now + Δ) + total_ms`, + which is `Δ + ms_rounding` past the absolute deadline. Setting *both* timers + to the full budget would have made the worst case ~2× — explicitly *not* what + this design does (see §3.3.4 / §4.3 code block). + - **`total_ms < 4` (the sub-4 ms degenerate case)**: §4.3 sets both + `connect_ms = first_byte_ms = total_ms`, so the dispatch+headers host + enforcement is bounded by `2 × total_ms` (≤ 6 ms total at the edge). The + post-deadline slack is therefore up to `BATCH_DISPATCH_SLACK_MAX + total_ms + + ms_rounding` (strict upper bound `25 + (≤ 3) + (≤ 1) < 29 ms` wall-clock). + At this scale ms-rounding already + dominates a meaningful deadline; sub-4 ms outbound budgets are degenerate + inputs, not a normal operating point. The test row asserts the 2× bound + explicitly rather than the `=` invariant. + + The body-phase cooperative check still adds up to one between-bytes-timeout + overshoot during drain (§3.3.4) in either case, but that's the only other + source. All terms are hard adapter constants, not "scales with preflight." + + Single `send` snapshots `now` at the public method entry before preflight and passes it + into `send_one` — there is no + `batch_now` shared across slots — but time still passes between + `dispatch_budget(req, now, None)` and `send_async` (backend lookup, possible + `Backend::builder().finish()` host call, SDK request construction). The + **same TWO-CHECK guard in the SAME ORDER as batch dispatch** applies (§4.3 — this was + inconsistent before; corrected): immediately before `send_async`, the adapter checks + **(1) the absolute deadline FIRST** — `if budget.deadline.is_expired() { return + Err(gateway_timeout_caused("deadline expired during Fastly dispatch", budget.cause)); }` + (a cold `finish()` returning past the deadline is a genuine **504**, not an EdgeZero bug); + **(2) THEN, only while time remains**, `MonotonicInstant::now() - now > BATCH_DISPATCH_SLACK_MAX` + → `EdgeError::internal(..)` with the same "adapter overhead between dispatch_budget and + SDK arming" diagnostic as batch dispatch. So single `send` returns the attributed **504** for a + real expiry and `internal` only for excess overhead with time to spare — identical to + batch path. The slack window is typically narrower for single `send` (no multi-slot preflight + and dispatch loop), but the bound is the same hard constant; the previous "structurally 0" wording was + incorrect. The phase-budget split and sub-4 ms branch apply identically. + + §5.4 has a row that locks this. The test cannot use a handler-side sleep before + `start_batch_until` — that runs *before* the adapter captures `batch_now`, so it never + exercises the slack guard. The test instead uses an **adapter-internal injection + hook** (a **`#[cfg(feature = "test-utils")]`** callback on the SDK-independent + dispatch driver used by `FastlyOutboundClient`, invoked between `batch_now` capture + and per-slot dispatch) to introduce a + synthetic delay exceeding `BATCH_DISPATCH_SLACK_MAX`. **It must be feature-gated, + not `#[cfg(test)]`** — `tests/contract.rs` is an external integration test and + compiles the adapter *without* `cfg(test)`, so a `#[cfg(test)]` hook would be + invisible to it (§5.5 *Executable test seams*). With the hook set, late slots return + `internal("Fastly batch adapter overhead between batch_now and SDK arming \ + (preflight + dynamic-backend lookup/creation + SDK setup) exceeded \ + BATCH_DISPATCH_SLACK_MAX; refusing to arm SDK timers with stale duration")`; + without it, no slot ever returns that error. Apps that need exact + absolute-deadline enforcement on the dispatch+headers phase target Axum. Cloudflare and + Spin also arm from `budget.deadline.remaining()` (§4.4 step 3), but neither claims a + finite host-teardown bound. **Collision detection** is + belt-and-suspenders. + + **Cache ownership — a request-context/session map (a `Mutex` field on the client).** + This is the single authoritative statement; it governs the protocol below and the + §4.3 *Dynamic backends* discussion. **Fastly dynamic-backend names are + session-scoped, NOT global across requests** (verified against the `BackendBuilder` + docs: dynamic-backend *registration* is per-session — each session registers into its + own namespace, so a `NameInUse` applies to the active session). The SDK separately + enables same-name/exact-same-settings connection pooling across sessions by default; + EdgeZero leaves pooling enabled, but the per-client namespace prevents intentional + same-name reuse across newly constructed clients. `FastlyOutboundClient` is constructed for + each EdgeZero request context (`crates/edgezero-adapter-fastly/src/request.rs`), receives + a fresh cache, and must not be retained beyond that context. The cache is therefore a + field on the client whose intended lifetime is the active request/session: + + ```rust + struct FastlyOutboundClient { + backend_namespace: u64, + // Per-request/per-session dedup map. MUST be Mutex, not RefCell: + // OutboundHttpClient: Send + Sync (stored as Arc in http::Extensions), + // and RefCell is !Sync. The Mutex is uncontended on the single-threaded WASM + // guest — it satisfies the Sync bound, it does not serialize anything. + backends: Mutex>, + // … + } + ``` + + **This reverses two earlier drafts wrong for the same root reason** — the + belief that registration names persist across requests. One made the cache a cross-request + `thread_local!` (which would carry **stale** entries into a reused instance's next + session, where those names are unregistered); another bucketed the budget to bound + that non-existent cross-request growth. Neither is needed: the map exists only to + **dedup within a single session's fan-out** (multiple `send_all_until` slots / multiple + `send`s in one handler to the same host+budget reuse one registration), so its size + is bounded by the fan-out, and it is discarded when the request context ends. Disabling + SDK connection pooling is distinct from discarding this registration cache. The **test seam** + (§5.5) exposes this client field. + + The protocol below takes the map's **uncontended `Mutex`**. To state the one model + plainly, because earlier drafts said three different things: the `Mutex` exists solely + to satisfy `OutboundHttpClient: Send + Sync` (the handle is stored as + `Arc` in `http::Extensions`) — **not** to serialize anything. + The Fastly guest is single-threaded, so no two `send_one` bodies interleave during a + synchronous host call, and **the lock is never held across a host call** (step 3). + (Read any surviving "no lock", "thread-local", or "lock held through `finish()`" + phrasing here as superseded by this paragraph.) + + 1. Lock the map (`self.backends.lock()` — uncontended; handle any poison by treating a poisoned lock as an internal error, never by unwrapping in production). + 2. If the name maps to a stored entry `(stored_identity, cached)`: + - **`stored_identity == identity`**: clone the cached `Backend`, drop the + lock, dispatch. + - **`stored_identity != identity`** (an in-adapter SHA-256-128 collision + between two distinct identities mapping to the same name): fail closed with + `EdgeError::internal("Fastly dynamic backend name collision in this + adapter's map — two distinct identities hashed to the same backend name; + refusing to silently swap settings")`. The previous-round wording reused + the cached backend by name alone, which would have silently bound a new + request to whichever identity got cached first — that bug is fixed by the + explicit identity comparison here. Release the borrow. §5.4 has a row that + exercises this path via an injectable hash collision under the `test-utils` feature + (**not** `#[cfg(test)]` — see §5.5 *Executable test seams*). + 3. Otherwise (name is absent), **release the lock**. For `send_all_until`, run the absolute + deadline-then-dispatch-slack guard before starting more synchronous work; if it fails, + return the typed timeout or `Internal` without registering a backend. Then call + `Backend::builder(..).finish()`. The lock is **not** held across this host call: + `finish()` registers a dynamic backend and can block waiting for a service-wide + backend slot (see *Dispatch-phase deadline honesty* below), and holding a lock + across a potentially-blocking host call is a hazard for any future multi-threaded + host. Releasing it does **not** reintroduce the same-identity race an earlier draft + worried about: the guest is single-threaded and `finish()` is a synchronous + (non-`await`) call, so no other `send_one` can run in the gap — a `NameInUse` from a + genuine miss is therefore still unambiguously external (step 5). + 4. On `Ok(backend)`: **re-acquire the lock**, insert `(identity, backend.clone())` + into the map, drop the lock, and return the `Backend`. + 5. On `Err(NameInUse)`: per Fastly's + [`BackendBuilder` docs](https://docs.rs/fastly/latest/fastly/backend/struct.BackendBuilder.html), + the **session-uniqueness rule is unconditional** — "a dynamic backend name + must not match the name of any static service backend nor match any other + dynamic backend built during this session." `NameInUse` does **not** carry + property-comparison semantics ("same identity → returns Ok" was a false + premise in earlier drafts); the SDK signals only "this name is taken in + this session," period. The SDK's documented recovery pattern is to call + `Backend::from_str(name)` (alias `Backend::from_name`) to obtain a handle + to the already-registered backend — but `from_str` returns a handle only + and **does not expose the registered backend's properties** to the guest + for comparison. + + The lock is released across `finish()` in step 3 and re-acquired in step 4 (not + held continuously — see *Cache ownership*), but the reasoning is unaffected because + the Fastly guest is **single-threaded** and `finish()` is a synchronous + (non-`await`) call: nothing else can run in the gap, so any + name *we* registered in this session necessarily showed up in step 2's borrow. A + `NameInUse` here therefore means the name was registered by an **external + party in this same session**: a static service backend, or another component + of this instance (**not** a prior session — dynamic-backend names are + session-scoped and a fresh session starts with a clean namespace). Since the + SDK does not let us inspect that external + backend's properties, we cannot prove its identity matches ours. Fail + closed with `EdgeError::internal("Fastly Backend::builder returned + NameInUse for a name not in this adapter's collision map; the SDK does + not expose the externally-registered backend's properties, so we cannot + prove identity match — refusing to dispatch to a backend with possibly + mismatched TLS / timeout / SNI configuration")`. Release the borrow. + + The alternative — falling back to `Backend::from_str(name)` and trusting + the external registration — is exactly the "you should be careful to only + use this capability in situations in which you are 100% sure that this + name will always lead to the same place" caveat that Fastly's docs + attach to `from_str`. Since EdgeZero owns the `ez_{sha256_128(identity)}_{client_namespace}` + naming scheme, a `NameInUse` for a name **absent from this session's map** + can only mean one of: (a) a **static service backend** is configured with + that name (the SDK's uniqueness rule spans static + dynamic within the + session), or (b) another component **in this same session** registered it, + or (c) a SHA-256-128 collision (vanishingly unlikely given the 128-bit + identity space). None is safe to silently inherit. + + > Note: it canNOT mean "a prior session registered it." Dynamic-backend + > names are **scoped to the session** and may overlap freely across + > sessions/instances — a fresh session starts with a clean namespace. An + > earlier draft attributed the collision to a prior session / another + > EdgeZero deployment sharing an edge dictionary; that is not how the + > lifetime works. This matters for the session-scoped cache above: the map + > and the host's name registry share **exactly** the same lifetime (the + > session), which is precisely why the map is a reliable mirror of it — and + > why a *cross-request* (process-persisted) map was not: a longer-lived map would + > carry stale names into a new session whose host registry has been cleared, so it + > would stop mirroring the registry. (The map here is per-session/per-request — the + > correct, shorter lifetime; the rejected alternative is the *cross-request* one.) + 6. On any other `Backend::builder(..).finish()` error — i.e. a + **`BackendCreationError`** — **map per the exhaustive stage-1 table below, NOT + with a blanket `bad_gateway`.** This is the one authoritative mapping; earlier + prose here said "map every other creation error to `bad_gateway`", which is wrong: + `ConnectTimeoutTooLarge` / `FirstByteTimeoutTooLarge` / `BetweenBytesTimeoutTooLarge` + / `NameTooLong` / `EncodingError` mean **EdgeZero** violated its own clamp/naming + invariant → **`internal` (500)**, not a 502. Only genuine host rejections + (`Disallowed`, `HostError`) are `bad_gateway`. `Disallowed` gets the dedicated + "enable dynamic backends" diagnostic. (`Backend::builder` is `#[non_exhaustive] = + false`, so the match is exhaustive with **no** `_` arm — a future SDK variant is a + compile error to be classified deliberately.) + + > **DNS / TLS / connect failures do NOT occur at this stage.** The SDK separates + > **`BackendCreationError`** (registering a backend — this step) from + > **`SendErrorCause`** (actually performing the exchange). Registration does not + > resolve DNS or complete a TLS handshake; those happen on **send**, and are + > mapped by the send-stage `SendErrorCause` table below. An earlier draft listed + > "DNS resolution failure / TLS misconfiguration" here — that is wrong, and it + > made the corresponding §5.4 test unwritable (a fake *builder* cannot produce a + > DNS branch). Test the two stages against their own error types. + `EdgeError::internal` is reserved for **adapter contract bugs** — invariant + violations the adapter itself should have prevented (pending-handle selection/correlation + invariants, the `BATCH_DISPATCH_SLACK_MAX` overshoot, this + section's `NameInUse` external-registration case). Release the borrow. + + **Backend *creation* errors are not the transport errors.** The list above covers + `Backend::builder(..).finish()` — i.e. *registering* a dynamic backend. **DNS, TLS, + and connection failures do not surface there**; they arrive later, from the **send** + itself, as a `SendError` whose `SendErrorCause` names the failure. Mapping the two + stages together (as earlier drafts did) would mislabel a connect failure as a + "backend setup" error. The normative **send-stage** mapping — applied at + the selected pending-handle result and the single-`send` wait path, + replacing today's blanket `EdgeError::internal(..)` on `wait()` failure: + + **Per-variant policy.** The variant names below are the real `fastly` 0.13.1 enums + (`backend::builder::BackendCreationError`, `http::request::SendErrorCause`). **A + blanket "anything else → 502" is wrong**: several variants mean *EdgeZero* violated + its own invariant, and reporting those as an upstream gateway failure hides an adapter + bug behind a plausible 502. + + > **⚠️ The two enums have different exhaustiveness properties — verified against the SDK source, and + > this drives both the code and the tests:** + > + > | | `BackendCreationError` | `SendErrorCause` | + > | --- | --- | --- | + > | `#[non_exhaustive]`? | **No** | **Yes** | + > | Exhaustively matchable by us? | **Yes** — omit a `_` arm so a future SDK variant is a **compile error** | **No** — a `_` arm is *mandatory*; new variants silently fall through | + > | Constructible in a test? | **Yes** (`PartialEq` too) | **Yes**, for public known variants; hypothetical future variants cannot be constructed | + > + > So: match `BackendCreationError` exhaustively (no `_`). For `SendErrorCause` an + > exhaustive match is **impossible** — an earlier draft's instruction to "match them + > exhaustively so a future variant is a compile error" is unachievable there, and the + > mandatory `_` arm is exactly why its default must be the *narrow* `Custom`-style + > 502 rather than a blanket. By contrast, the `SendError` wrapper is unconstructible (private fields, + > no public ctor), though `SendError::root_cause() -> &SendErrorCause` lets the + > adapter read the cause. See *Send-stage test seam* below for how this is tested. + + **Stage 1 — `BackendCreationError` (registration):** + + | Variant | EdgeError | Status | Why | + | --- | --- | --- | --- | + | `Disallowed` | `bad_gateway`, reason `Unspecified` | 502 | Dynamic backends off on the service — operator action; carries the dedicated diagnostic but is not a transport/protocol/decode failure. | + | `NameInUse` | *(not an error here)* | — | Handled by the cache protocol above (identity compare → reuse or fail closed). | + | `ConnectTimeoutTooLarge`, `FirstByteTimeoutTooLarge`, `BetweenBytesTimeoutTooLarge` | **`internal`** | **500** | **EdgeZero bug.** The `DEADLINE_FAR_FUTURE` clamp (7 d) plus `fastly_timeout_ms`'s u32-ms clamp exist precisely to make these unreachable. Reaching one means our clamp is broken — not an upstream failure. | + | `NameTooLong` | **`internal`** | **500** | **EdgeZero bug.** We own the naming scheme (`ez_` + 32 hex = fixed length); too-long is impossible unless the scheme changed. | + | `EncodingError` | **`internal`** | **500** | **EdgeZero bug.** We construct the backend strings; invalid UTF-8 is ours. | + | `HostError(FastlyStatus)` | `bad_gateway`, reason `Unspecified` | 502 | Genuine host-side registration rejection with no narrower portable category. | + + **Stage 2 — `SendErrorCause` (the exchange):** + + | Variant(s) | EdgeError | Status | + | --- | --- | --- | + | `DnsTimeout` | `gateway_timeout`; `Unspecified` before absolute expiry, selected cause at/after expiry | **504** | + | `ConnectionTimeout`, `HttpResponseTimeout` | `gateway_timeout` with selected cause because EdgeZero configures those phase timers from the selected budget | **504** | + | `DnsError`, `DestinationNotFound`, `DestinationUnavailable`, `DestinationIpUnroutable`, `ConnectionRefused`, `ConnectionLimitReached` | `bad_gateway`, reason `Unreachable` | 502 — the exchange did not establish a usable upstream response path. | + | `TlsProtocolError`, `TlsCertificateError`, `TlsAlertReceived`, `TlsConfigurationError` | `bad_gateway`, reason `Unreachable` | 502 — TLS establishment failed before a response head. | + | `ConnectionTerminated` | `bad_gateway`, reason `Transport` | 502 — the connection made progress and then terminated. | + | `HttpIncompleteResponse`, `HttpResponseHeaderSectionTooLarge`, `HttpResponseBodyTooLarge`, `HttpResponseStatusInvalid`, `HttpUpgradeFailed`, `Http2StreamError`, `HttpProtocolError` | `bad_gateway`, reason `Protocol` | 502 — malformed/oversized **upstream** response. | + | `IoError` | `bad_gateway`, reason `Transport` | 502 | + | `ImageOptimizerUnsupported` | `bad_gateway`, reason `Unspecified` | 502 — no narrower portable category. | + | `HttpRequestUriInvalid`, `HttpRequestCacheKeyInvalid`, `HttpCacheLimitExceeded`, `HttpCacheApiUnsupported` | **`internal`** | **500** — **EdgeZero bug.** We build the request/URI and don't use the cache API; a *locally-invalid request* is ours, not the upstream's. (§3.1.3/§3.1.4 validate the URI long before dispatch.) | + | `InternalError(..)` | **`internal`** | **500** — the SDK's own unexpected host-internal fault: not the *origin's* failure (so not 502) and not a malformed request WE built (so not kind-(i)), but a platform-internal error. `internal` (500) covers this kind-(ii) fault per the broadened taxonomy (§5.4). | + | `Custom(..)` | `bad_gateway`, reason `Unspecified` | 502 — unknown/extension cause; the only defensible default, and it is *narrow* rather than a blanket. | + + **Send-stage test seam — test known SDK causes and SDK-independent policy.** + Public known `SendErrorCause` variants are constructible outside the SDK; enum-level + `#[non_exhaustive]` prevents exhaustive external matching, not construction of those + variants. Only the `SendError` wrapper requires an SDK-produced value. Retain the + two-stage classification so native policy tests remain SDK-independent, and directly + test the SDK boundary for every known cause: + + ```rust + // 1. A locally-defined, CONSTRUCTIBLE classification of what went wrong. + // Unit tests build these directly — no SDK types involved. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) enum SendFailure { + BudgetedTimeout, // ConnectionTimeout | HttpResponseTimeout + LocalInvariant, // HttpRequestUriInvalid | HttpRequestCacheKeyInvalid | HttpCacheLimitExceeded | HttpCacheApiUnsupported + PlatformInternal, // InternalError: Fastly host/runtime internal fault, not caller input + ProviderTimeout, // DnsTimeout: no Fastly SDK DNS-timeout setter + Transport, // ConnectionTerminated | IoError + Unknown, // Custom, and any future #[non_exhaustive] variant + Unreachable, // DnsError | Destination* | Connection{Refused,LimitReached} | Tls* + UpstreamProtocol, // HttpIncompleteResponse | Http*TooLarge | HttpStatusInvalid | Http2StreamError | ... + } + + // 2. The POLICY: pure, total, unit-testable, zero SDK dependency. Native policy tests + // assert one case per SendFailure, constructed directly. It receives the absolute deadline, + // selected cause, and an injected observation instant. `BudgetedTimeout` is always attributed + // because EdgeZero configured Fastly's connect/response phase timer from that budget. + // `ProviderTimeout` is unattributed before the absolute deadline because Fastly exposes no + // DNS-timeout setter; at/after the deadline, deadline precedence attributes any result. The + // other arms select `BadGatewayReason::{Protocol, Transport, Unreachable, Unspecified}` + // from `failure` when observed in budget. Injecting the instant makes the boundary deterministic and avoids + // a second clock read inside the classifier. + pub(crate) fn classify( + failure: SendFailure, + deadline: Deadline, + cause: BudgetSource, + observed_at: MonotonicInstant, + ) -> EdgeError { /* per the table above */ } + + // 3. The BOUNDARY: known SDK variants are directly tested. A `_` arm is + // MANDATORY (#[non_exhaustive]); future variants map to `Unknown` -> narrow 502. + fn cause_to_failure(cause: &SendErrorCause) -> SendFailure { /* match … , _ => Unknown */ } + ``` + + Tier 2 retains native + **`classify(SendFailure::X, deadline, cause, observed_at)`** tests and adds SDK-gated, + in-crate tests constructing every pinned public `SendErrorCause` variant. Assert both + `cause_to_failure` classification and the composed error's status/kind/reason, including + all budget-source values for both timeout classes before, exactly at, and after the + absolute deadline, plus the exact `BadGatewayReason` for every 502 bucket. These are + Fastly adapter tests, not core + Tier 1 tests; execute the SDK-gated tests through the Fastly WASM `--lib` gate (§5.5). + Only hypothetical future variants use cross-crate compilation and review coverage + for `_ => Unknown => 502`; never fabricate them with unsafe code. Tier 3 retains + real-failure end-to-end coverage of the SDK-produced `SendError` wrapper. + + **Consequence for the "internal is legal on only three paths" assertion (§5.4):** that + claim is now **wrong** and must be widened — `internal` is also correct for the + invariant and platform-internal variants above. The §5.4 row asserts `internal` appears + **only** + for: (a) `BATCH_DISPATCH_SLACK_MAX` overshoot, (b) pending-handle + selection/correlation invariants, (c) the `NameInUse` external-registration case, **(d) the + clamp/name/encoding `BackendCreationError` variants, (e) `SendFailure::LocalInvariant`, + and (f) `SendFailure::PlatformInternal`**. Cases (a)–(e) are EdgeZero-invariant + violations; case (f) is the separately classified Fastly host/runtime internal fault. + Neither class includes an origin transport/protocol failure. + + **`DnsTimeout` is 504, not 502, but early DNS timeout provenance is unspecified.** It names DNS + (transport-shaped, which reads 502) but it **is a fired timer**. Classify by + **"did a timer fire?"**, not by which subsystem reported it. A DNS answer of "no such + host" is 502; a DNS lookup that ran out of time is 504. Fastly exposes no DNS-timeout + setter, so a `DnsTimeout` observed before EdgeZero's absolute deadline uses + `BudgetSource::Unspecified`; at/after the deadline, the deadline wins and carries the + selected cause. Retry policy remains an application decision and is not encoded by this + mapping. Live characterization records which provider cause each configured phase + produces but does not replace the deterministic boundary tests. + + Rationale: a fired host timer is a **deadline** outcome (504) and must be + distinguishable from an upstream that was unreachable (502) — the fan-out caller + retries those differently. `EdgeError::internal` **is** correct for a **narrow** set + of send-stage causes: the `SendFailure::LocalInvariant` group + (`HttpRequestUriInvalid`, `HttpRequestCacheKeyInvalid`, `HttpCache*`) means EdgeZero + built an invalid request, while `SendFailure::PlatformInternal` represents Fastly's + explicit host/runtime `InternalError`. It is **never** correct for a + *transport/upstream* cause + (those are 502/504). (An earlier draft said `internal` is never correct for any + send-stage failure — that contradicts the `LocalInvariant` row of the §4.3 table, + which is authoritative.) A completed exchange, including any non-2xx, is `Ok`. + + **`BackendCreationError::Disallowed`** (dynamic backends not enabled on the + service) is the one creation error that gets its own Rust-side diagnostic: map to + `EdgeError::bad_gateway_with_reason(DYNAMIC_BACKENDS_DISABLED_MESSAGE, BadGatewayReason::Unspecified)`, + so application diagnostics can point at the deployment fix. Rendering that error as an + HTTP response still emits only `"bad gateway"`; the provider-specific text is never a + wire message. + `DYNAMIC_BACKENDS_DISABLED_MESSAGE` is the one shared exact diagnostic: + `"Fastly dynamic backends are not enabled on this service; enable them in the service configuration"`. + + There is no `BackendSlot::Building` / `Failed` variant and no condvar. There **is** an + uncontended `Mutex` (the field is `Mutex>`, required for `Send + Sync`), but + it exists only to satisfy that bound — the Fastly guest is **single-threaded**, so there + is no concurrency for a state machine to guard, and **the lock is never held across the + `finish()` host call** (the *Cache ownership* protocol releases it, then re-acquires to + insert). Nothing can observe an intermediate state because nothing else runs while the + lock is held, and the lock isn't held across the one call that could block. The race the + round-34 review flagged is structurally impossible for that reason. (Earlier drafts + variously said "no lock", "hold the `Mutex` across the host call", and used finer-grained + per-name reservations; all are superseded by the single model in *Cache ownership*.) The protocol applies + to: + + - **`send_all_until`** — each slot looks up its name; if the name already maps to its own + identity, reuse; if it maps to a *different* identity, fail closed with + `EdgeError::internal("dynamic backend name collision — refusing to reuse")`. + - **Single `send`** — same lookup path; same fail-closed behaviour. + - **Across calls within ONE request context — a session-scoped + `Mutex>` field.** Fastly dynamic-backend registration names are + session-scoped. The cache is therefore a field on the request-context + `FastlyOutboundClient`, fresh with that client — + it exists to dedup **within** a single session's fan-out (multiple `send_all_until` slots / + multiple `send`s in one handler to the same host+budget reuse one registration), and + is discarded when the request context ends. **It MUST be a `Mutex>`, NOT a + `RefCell`:** `OutboundHttpClient: Send + Sync` (the handle stores `Arc` in `http::Extensions`, which require `Sync`), and `RefCell` is + `!Sync` — a `RefCell` field would fail to compile. The `Mutex` is **uncontended** on + the single-threaded WASM guest (it exists to satisfy `Sync`, not to serialize), and + the lock is never held across a host call. A SHA-256-128 collision against an earlier + registration in this session is still caught. *(Two earlier drafts got this wrong for + the same root cause — the false belief that registration names persist across requests: one made + the cache a cross-request `thread_local!`, another used a non-`Sync` `RefCell`. Both + are superseded by this request-context `Mutex` field. Connection pooling remains at the + Fastly SDK default because reuse requires the same name and exact same settings; the + deterministic identity/settings prevent cross-budget or cross-TLS reuse.)* + - **`Backend::builder` returns `NameInUse`** — the adapter cannot fully verify + the registered identity. Fastly's `Backend::from_name` returns a handle to the + existing backend but its public getters do not round-trip every builder field + (SNI hostname / certificate hostname are notably opaque per the + `BackendBuilder` / `Backend` docs). So the adapter **fails closed** with + `EdgeError::internal("Fastly Backend::builder returned NameInUse for a name \ + not in this adapter's collision map — refusing to reuse an externally \ + registered backend")`. Names already in the adapter's own map are reused + cheaply with no `Backend::builder` call (the in-memory `Backend` handle is + already present); only an *external* registration of a colliding name + triggers this path, and the safest response is to surface it rather than + guess. This makes the adapter's collision map authoritative. + + Backends are deduplicated by full identity within and across calls. Requires + dynamic backends enabled on the service (surfaced via the `outbound-http` + capability and the service prerequisite below). +- Requests in `start_batch_until` / `send_all_until` are required to have buffered request bodies AND buffered + response mode per the trait contract (§3.1.1). A `Body::Stream` request body + yields `out[i].outcome = Err(EdgeError::bad_request(..))`; a request with + `response_mode = Streamed` also yields + `out[i].outcome = Err(EdgeError::bad_request(..))`. + This removes unbounded application **source pulls** from Fastly's + dispatch-all-then-select model and removes the cross-slot streamed-response + deadline-lifetime problem (§3.1.1), identically on every adapter. It does not + prevent a non-empty buffered body from blocking in Fastly's untimed host-write + interval; that separate limitation is footnotes 1/4. +- **Streamed request bodies in single `send`.** The single-request path accepts + `Body::Stream` and uses `Request::send_async_streaming(&backend) -> + Result<(StreamingBody, PendingRequest), SendError>`. Classify an initial send error + through `SendFailure` (§4.3); on success, feed chunks from the core stream to the + `StreamingBody`, with these rules: + - **Byte count cap.** Pre-append checked accounting against + `req.max_request_body_bytes` (default 8 MiB). Over-cap → `payload_too_large` (413) — + the `StreamingBody` is dropped without `finish()`, the `PendingRequest` is + dropped, and the slot returns the error. + - **Upload ownership and terminal results.** Keep both SDK handles owned until an + explicit terminal branch. Check `budget.deadline.is_expired()` before each source + pull and immediately after it returns, including EOF or a source error. Expiry + drops both handles without `finish()` and returns + `gateway_timeout_caused(.., budget.cause)`. A source `EdgeError` observed before + expiry is preserved unchanged, with the same cleanup. For every `write_all` / + `flush()` result, recheck the deadline before interpreting success or failure; + otherwise an I/O failure maps to `bad_gateway` reason `Transport` and drops both + handles. + On clean source EOF within budget, consume the `StreamingBody` with **exactly one + `finish()` call**. Fastly 0.13.1's `finish(self) -> io::Result<()>` can flush buffered + bytes and fail; dropping the body instead aborts an otherwise successful upload. + Recheck the absolute deadline after `finish()` returns, before interpreting its + result: expiry wins with the attributed 504; an in-budget finish error is 502 reason + `Transport`. + Either error drops `PendingRequest` without calling `wait()`. Only an in-budget + successful finish advances to the response phase. `finish()` consumes the body + even on error, so it is never retried. Dropping the entire send future before + finalization also drops both owned handles without a successful finish. + - **Deadline enforcement has two phases with different bounds:** + - *Source-stream yield* (`stream.next().await`): **unbounded on Fastly** — no + guest async primitive can preempt a stalled `stream.next()` waiting for the + app's source stream to yield. This is the `BestEffort` aspect of + `streamed-upload-deadlines` on Fastly. Apps that need real-time enforcement + against an untrusted upload source must pass a buffered request body + (`Body::Once`) where the bytes are already in hand and no `stream.next().await` + is involved. **This removes only the SOURCE-PULL stall.** It does not bound the + host write: a `Body::Once` is still pushed to the origin in the untimed window + between `connect` and `first_byte`, so a slow-reading origin stalls a buffered + upload too. Buffering is not a remedy for the write path — only a different + adapter is. + - *Host write* (`StreamingBody::write_all` / `flush()` / terminal `finish()`): these + are synchronous host calls. **Fastly's `between_bytes_timeout` applies only to + received bytes (the gap between bytes Fastly receives from the origin), not + to guest-to-origin writes** — see the [Fastly Backend API + docs](https://www.fastly.com/documentation/reference/api/services/backend/), + which describe `between_bytes_timeout` as "maximum duration … that Fastly will + wait while receiving no data on a download from a backend." No published + Fastly backend-timeout field bounds the host-side write of guest-supplied + bytes to origin. **BestEffort** for the write phase: a `StreamingBody::write_all` + whose host TCP buffer is full because origin stopped acking has no + adapter-configurable timeout. The adapter's only recourse is the + `budget.deadline.is_expired()` checks around source pulls, writes, and finalization + described below; these checks cannot interrupt a blocked synchronous host call. + Apps that need real-time enforcement against a slow + origin **read path** rely on `between_bytes_timeout` once the response body + starts flowing. Apps that need real-time enforcement against a slow origin + **write path** must target a different adapter. `max_request_body_bytes` bounds + bytes and memory, not elapsed write time: even one small `write_all` can block + without a documented finite bound, so no cap value creates a wall-clock guarantee. + - *Around each chunk*: the pre-pull and post-ready checks above prevent further + source work after an observed expiry. The post-ready check also prevents a + source that stalled past the deadline from writing the chunk it eventually + yielded. Recheck after each host write/flush, including errors, and after + terminal `finish()`. These cooperative checks detect expiry only once the + synchronous call returns; finalization has the same unbounded host-write gap. + + Net: the capability matrix entry `streamed-upload-deadlines = BestEffort` for + Fastly reflects **both** phases — **source-stream yield AND host write are each + unbounded** on the guest side. There is **no** `BoundedCooperative` write-side + bound: `between_bytes_timeout` is documented as **receive-side only** (it bounds the + gap between bytes *received from origin*) and does **not** bound guest-to-origin + writes. (An earlier draft claimed the write phase was `BoundedCooperative` via + `between-bytes-timeout` and that only source-yield was the "worst phase" — both are + wrong; footnote 2 and §8 risk 7 are correct.) The only adapter-side bound on either + phase is the cooperative `budget.deadline.is_expired()` check around source pulls, + writes, and finalization described above. + - **Response phase: host timeouts are *not* adjustable mid-flight.** The Fastly + SDK sets connect / first-byte / between-bytes timeouts once before `send_async` + (§3.3.4) and does not expose post-dispatch mutation. For + `send_async_streaming`, dispatch happens **before** chunks are fed, so the + response-phase host timeouts are locked to the phase-split values computed at + dispatch (`first_byte_ms` for the headers wait, `between_ms` for inter-chunk + gaps once the response body flows). After the upload `finish()`es the adapter + checks `budget.deadline.remaining()` cooperatively before calling `wait()` — + if `None`, drop the `PendingRequest` and return `gateway_timeout` without + waiting. **If the upload leaves a tiny positive remaining budget**, the + cooperative check at this boundary passes, and the host then waits up to + its dispatch-time `first_byte_ms` for headers even though only the tiny + remainder of batch budget is left. **The headers wait is bounded by at + most one dispatch-time `first_byte_ms` interval past `budget.deadline`** — + a single, one-shot overshoot, not a per-chunk accumulator. + + Once headers arrive, the **response body** flows through the cooperative + deadline-aware wrapper (§4.3 "Streamed-response wrapping"), whose + `is_expired()` check fires before and after **each** underlying read. + Because the wrapper checks after the read that delivered the first body + chunk, and the deadline is already expired by construction in this + scenario, **the very next deadline-check yields `Err(gateway_timeout)`** — + the wrapper does **not** wait another `between_bytes_timeout` per chunk + indefinitely. **After an upload finalizes within budget, response-phase + post-deadline overshoot is bounded by `first_byte_ms` + (the headers wait) plus one `between_bytes_timeout` (the worst-case + interval during which the host is mid-read of the *first* body chunk + when the wrapper fires)** — a closed-form bound, not a per-chunk + accumulator. The previous "plus one between-bytes-timeout per body-chunk + gap" wording in earlier drafts was wrong; the wrapper preempts after the + first post-deadline read returns. This bound excludes the unbounded source pulls, + host writes, and `finish()` call; it is not an end-to-end upload bound. + + This is a deliberate, documented Fastly-specific behaviour of streamed uploads. + Passing `Body::Once` removes only the source-pull stall and avoids this post-upload + timer-staleness shape; it does **not** bound Fastly's host write of those buffered bytes. + Apps that require a strict end-to-end wall-clock bound for any non-empty upload must + target an adapter whose upload path is Native. +- `capability()` per §3.5.2: `outbound-http` = `BestEffort` (footnote 9: adapter + support depends on a service entitlement the static gate cannot verify), + `outbound-complete-resource-accounting` = `Unsupported`, + `outbound-header-fidelity` = `Native`, `outbound-deadlines` = + **`BestEffort`** (footnote 1 — a warm zero-body path has documented partial bounds, but + every non-empty request retains an unbounded host-write interval and the FIRST request + to a new host calls `Backend::builder(..).finish()`, which can also overshoot before the + guard runs; since `capability()` is static and cannot distinguish those paths, + the honest value is `BestEffort`, so a `required outbound-deadlines` correctly hard-fails + on Fastly rather than fooling the gate), + `outbound-failure-diagnostics` = `Native` for available typed SDK causes and numeric fields, + `outbound-flexible-phase-budget` = `BestEffort` (footnote 5 — rigid 1/4 connect + + 3/4 first-byte split per §4.3 can fail a request that would have fit within the + total budget), `outbound-batch-slot-isolation` = `BestEffort` (footnote 4 — sequential + cold registration can delay sibling dispatch and a selected buffered response body can delay + the next selection/result observation), + `streamed-upload-deadlines` = `BestEffort` (footnote 2 — no preemption of a + stalled `stream.next().await`), `lazy-streamed-response-passthrough` = + `BestEffort` (footnote 6 — the send-owning entrypoint preserves lazy streaming, but + synchronous source polls/host writes cannot be preempted and close is only a host-acceptance + observation). + This is the exact outbound tuple `Adapter::capability()` returns on Fastly. + +**Streamed-response wrapping.** Even without a guest async timer, the Fastly adapter +wraps streamed response bodies with a **cooperative deadline-aware stream**. Each +`Stream::next` checks `budget.deadline.is_expired()` **both before issuing the +underlying body read and again after it returns** (including the read that +discovers EOF and would otherwise complete the stream cleanly). On expiry at +either check it yields `Err(EdgeError::gateway_timeout_caused(.., budget.cause))` instead of `Ok(chunk)` +or stream-end. This applies to *every* consumer of the wrapped body — +`into_bytes_bounded`, `into_bytes_bounded_until`, `into_response()` proxy +passthrough — so the deadline cannot be bypassed by choosing a non-helper +consumption path or by riding the final blocking read to EOF. Bounded-cooperative +semantics apply: a chunk gap (including the gap before EOF) is bounded by the +host's `between-bytes-timeout` (set to `budget.duration` at dispatch), so per-gap +overshoot ≤ one between-bytes-timeout interval. + +**Limitation, stated explicitly.** `select_handles` blocks the single-threaded guest until one +handle in the active bounded group completes. Every eligible request is issued before selection, +but sequential dispatch does not prove simultaneous host overlap. Once selected, that response's +buffered body drains synchronously before the next selection. There is no busy-polling, but guest +application work cannot run inside either blocking host boundary. + +**Service prerequisite — dynamic backends.** Fastly outbound HTTP to arbitrary hosts +requires **dynamic backends to be enabled on the Fastly service**. That is a +deployment-time service configuration, not adapter code, and the adapter itself cannot +turn it on. EdgeZero handles the gap as: + +1. **Build / deploy:** the static CLI cannot prove this account/service entitlement. The + Fastly adapter therefore reports `outbound-http = BestEffort`, not `Native`; a required + declaration fails closed, while an optional declaration emits the standard degradation + warning. Here `optional` is an explicit application contract that the handler tolerates + an outbound dispatch failure when the deployment prerequisite is absent; it is not a + promise that every selected service can issue the request. An application whose behavior + requires outbound success cannot use this escape hatch and must select a deployment whose + capability check passes. EdgeZero deliberately does not pretend an informational reminder + is admission control. +2. **Runtime:** if dispatch fails because dynamic backends are disabled, the adapter + surfaces `EdgeError::bad_gateway_with_reason(DYNAMIC_BACKENDS_DISABLED_MESSAGE, + BadGatewayReason::Unspecified)` using the exact shared constant defined in the creation + error mapping above. Apps can inspect/log the Rust-side diagnostic before response + conversion; an HTTP response remains the fixed category-only 502 contract. + +Fastly can be promoted to `Native` only when a separately reviewed deploy-time prerequisite +can prove dynamic-backend enablement for the selected service before publication. Phase 6 +still runs a trusted live-host characterization against one enabled disposable service and +one disabled disposable service: the enabled probe must reach arbitrary canonical +destinations, while the disabled probe must produce the exact typed diagnostic above. That +evidence proves adapter behavior but does not make the static gate deployment-aware, so the +current matrix remains `BestEffort`. + + + +#### 4.3.1 Closed custom-entrypoint lifecycle + +Applications that need Fastly-only request metadata, late response finalization, or work after +client transmission must not call the router and `stream_to_client` directly. That bypasses +EdgeZero ingress admission and response-egress accounting. The adapter instead exposes one closed +service operation for an already-received request: + +```rust +impl FastlyService { + pub fn send_request_with_hooks( + self, + request: fastly::Request, + prepare_request: RequestHook, + finalize_response: ResponseHook, + ) -> FastlyRequestDelivery + where + RequestHook: FnOnce(&mut fastly::Request, &mut Extensions), + ResponseHook: FnOnce(&mut Response) -> PostSend; +} +``` + +The exact order is normative: + +1. Borrow the received Fastly request for sanitization and native metadata extraction; merge the + scratch extensions into the core request. +2. Convert through EdgeZero, resolve the route, perform ingress admission, and dispatch the + admitted request. +3. Run the routed-response hook before response-egress policy and framing are finalized. Detached + conversion, framing, and admission-refusal responses skip this application hook. +4. Revalidate response framing, begin the egress attempt with the application clock/deadline, and + let the adapter alone commit headers, stream bytes, close or abandon the body, and record one + terminal observation. +5. Return an owned `FastlyRequestDelivery` after the adapter's terminal boundary. + `Routed { state, delivery }` retains finalizer state on failure and fallback; `Detached` + skips finalization. `Aborted` means intentional policy abort, not `NoAttempt`. `NoAttempt` + carries payload-free typed required-store or request-head-conversion detail. + +Attempted results carry the exact `ResponseEgressReport` already sent to completion and the +applicable observer, the last selected body's commitment, and its last delivery error separately +from a fallback's original failure cause. `NotCommitted` means its head never reached the host +send; `Unknown` means host rejection leaves client visibility unknown; `Committed` means host +acceptance, not client delivery. `may_send_native_response()` authorizes native replacement only +for NoAttempt or confirmed NotCommitted, never policy abort, Unknown or Committed. An attempted +response is already terminally accounted; a native replacement is outside EdgeZero accounting. +Pre-admission/refusal responses retain observer exclusion; an admitted source-error response +also reports Detached but retains the application observer. Post-send application work must +inspect body kind to avoid running after fallback; routed state remains available after a +committed application-source failure. Complete pending work before returning from the callback. + +Hooks borrow the platform/core values. They cannot take the response, access the egress envelope, +or invoke the platform sender. `run_app` remains the simple generated entrypoint and is built on +this operation. `run_app_with_hooks` is the advanced auto-receiving wrapper; standard entrypoints +retain their existing Result and stop-on-error serving policy, including successful fallback +delivery. Custom callbacks select their own policy, including continuing after committed failure +without a replacement send. The former +`run_app_with_request_extensions` symbol is removed in the hard cut. + +### 4.4 Spin — `crates/edgezero-adapter-spin` + +- `SpinProxyClient` → `SpinOutboundClient` (stays stateless). +- `start_batch_until` first snapshots `let batch_now = self.clock.now()` once, then runs a + **preflight** per slot: call `validate_for_dispatch(&request)` first; only a request that + passes that portable validator reaches the batch-only checks. Then any request with `Body::Stream` + OR `response_mode = Streamed` is converted to `Err(EdgeError::bad_request(..))` + per §3.1.1 before provider construction. It passes that entry snapshot and the observation cutoff to + every `dispatch_budget` call. Buffered survivors become complete-exchange futures in a + `FuturesUnordered`; each drives the hand-built `wasi:http` request plus + `wasip3::http::client::send`. The adapter polls every eligible future before yielding and + preserves original indices. Concurrency materializes only under the real Spin/WASI executor — + see §5.3 for the test consequence. +- A public single `send` calls `validate_for_dispatch(&req)` exactly once immediately after + its method-entry snapshot, then invokes the same private already-validated flow as batch + survivors. The private prepared-request flow computes the single-send budget via the core helper + `dispatch_budget(req, now, None)` (§3.3.2) before consuming the request into parts, then builds + the hand-built `wasi:http` request (§4.4 — all body kinds, buffered and streamed); race + the **whole** operation + (send **and**, in `Buffered` mode, body collect) against a wasi monotonic-clock + timer for **`budget.deadline.remaining()` at the moment the race starts** — + *not* the snapshot-time `budget.duration`. The two differ by however long + preflight + builder construction took since `batch_now`; using `remaining()` + pins the SDK timer to the absolute batch deadline, matching Axum/CF (§4.1 / + §4.2 step 4). If `remaining()` is `None`, return + `gateway_timeout_caused(.., budget.cause)` without + issuing the request. Single `send` snapshots `now = MonotonicInstant::now()` at its + public method entry before preflight. +- **Streamed responses honour the effective-budget deadline — STREAMED MODE ONLY.** This + is the second phase for `Streamed` mode, where `to_core` returns `Body::Stream` **without + draining** (the single exchange race covered upload + headers only). Wrap that + `Body::Stream` with a per-chunk race against a wasi monotonic-clock timer bounded by + `budget.deadline`; the wrapper yields a `gateway_timeout` (attributed via `budget.cause`, §3.3.2) error chunk past the deadline so + the streamed body honours the deadline end-to-end per §3.3.3. **BUFFERED mode does NOT get + this second race:** there, `to_core` `.await`s the full body drain **inside `exchange`**, + which is already bounded by the single deadline race below — the buffered body is consumed + once, under one race, never twice. (So the design is: one exchange race covering + upload+headers+*buffered*-body; for *streamed* bodies the exchange race stops at headers and + this per-chunk wrapper takes over. No path drains the same body under two races.) +- **ALL uploads — including BUFFERED — use the hand-built `wasi:http` request, not + `spin_sdk::http::send`.** `spin_sdk::http::send`'s `IntoRequest` conversion spawns a + **detached, uncancellable** body pump (§4.4) even for a *buffered* `Body::Once`: a + finite buffer still blocks on **host backpressure** if the origin reads slowly, and + dropping the `send` future does not cancel that pump — so a buffered upload could block + past the deadline. Therefore buffered bodies also go through the hand-built request + + owned in-race pump below (a buffered body is just a one-shot stream of a single + chunk), which lets the timer select a guest-visible 504 for both body kinds without an + unowned pump. It does **not** prove bounded host teardown; `streamed-upload-deadlines` + therefore remains `BestEffort` for Spin (footnote 8). + +- **Streamed request bodies — hand-built `wasi:http` request (SDK 7 / WASI 0.3).** + + > **⚠️ Corrected against verified SDK source.** Earlier drafts of this section + > prescribed a WASI-**0.2** loop — `OutputStream::subscribe()` → `Pollable`, + > `check_write()` for a permitted byte count, then `write()`. **That API does not + > exist in Spin SDK 7.** WASI 0.3 deletes `wasi:io` entirely: there is no + > `pollable`, `output-stream`, `check-write`, or `subscribe` anywhere in `wasip3`'s + > WIT or in spin-sdk 7's Rust, and `wasi:http@0.3.0`'s `request.new` takes + > `contents: option>` (a component-model stream), not an `OutputStream` + > resource. The old algorithm was not merely deprecated — it was **unimplementable**. + > + > **Nor can the upload go through `spin_sdk::http::send`.** Its `IntoRequest` impl + > (`http_into_wasi_request`) hands a streaming `http_body::Body` to a **detached, + > uncancellable** pump (`wit_bindgen::spawn`; the runtime's own docs: *"cannot be + > cancelled or monitored"*). Dropping the `send` future cancels the **subtask** but + > **not the pump** — so a *stalled source* (precisely what this capability exists to + > bound) leaves the pump parked in `poll_frame` forever, and the export executor + > will not exit until spawned tasks drain. That is not a weak timing guarantee; it + > **pins the component task alive indefinitely**. Routing streamed uploads through + > the SDK's high-level `send` would make Spin `BestEffort` *and* leak. + + The adapter therefore **builds the `wasi:http` request by hand** and keeps every + request-body and completion handle inside the raced exchange. This avoids the SDK's + detached pump, but it does **not** create a documented synchronous host-teardown bound; + that limitation is why Spin is `BestEffort` for the two deadline capabilities. All of + this uses public API re-exported by `spin_sdk`: `wasip3`, `wit_stream`, and `wit_future`. + + ```rust + use spin_sdk::wasip3::{http::{types, client}, wit_stream, wit_future}; + use futures::future::{poll_fn, select, Either}; + use std::task::Poll; + + let (mut writer, contents_rx) = wit_stream::new::(); + // Dropping an unwritten FutureWriter writes its DEFAULT asynchronously. The default + // must therefore be failure, never a false clean completion. + let (trailers_tx, trailers_rx) = wit_future::new(|| { + Err(types::ErrorCode::InternalError(Some( + "outbound request body producer dropped before completion".into(), + ))) + }); + + // The pinned wasip3 API has TWO error shapes. Request component setters return + // `Result<(), ()>`; RequestOptions timer setters return + // `Result<(), types::RequestOptionsError>`. Do not route both through one closure. + let bad_request_component = |()| { + EdgeError::internal(anyhow::anyhow!("invalid outbound request component")) + }; + let opts = types::RequestOptions::new(); + // Set every available host transport/response timer from a nonzero nanosecond + // snapshot of the remaining effective budget. Ignoring a setter result or leaving + // first-byte/between-bytes at host defaults can weaken those fallback bounds. Even when + // a setter is accepted, an early WASI timeout variant does not prove which timer caused + // it and remains `BudgetSource::Unspecified`; only absolute expiry attributes the selected + // cause. The outer absolute race below remains authoritative and re-reads `remaining()` + // immediately before `select`. + let Some(options_remaining) = budget.deadline.remaining() else { + return Err(EdgeError::gateway_timeout_caused( + "deadline expired before upload setup", budget.cause)); + }; + // `Deadline` is clamped to seven days, so this conversion cannot overflow u64; + // keep the checked conversion so violating that invariant fails locally and loudly. + let transport_ns = u64::try_from(options_remaining.as_nanos()) + .map_err(|_| EdgeError::internal(anyhow::anyhow!( + "outbound deadline exceeds WASI duration range" + )))? + .max(1); + use edgezero_core::error_detail::SpinTimeoutOption; + let mut advisory_timer_refusal = false; + let mut apply_timer_option = |option, option_result| { + match option_result { + Ok(()) => Ok(()), + // WASI explicitly permits a host not to implement an option. The outer + // monotonic race still provides the guest-visible deadline, so this is the + // documented BestEffort fallback rather than a request-fatal 500. + Err(types::RequestOptionsError::NotSupported) => { + advisory_timer_refusal = true; + Ok(()) + } + Err(types::RequestOptionsError::Immutable | types::RequestOptionsError::Other(_)) + if option == SpinTimeoutOption::Connect => { + advisory_timer_refusal = true; + Ok(()) + } + Err(types::RequestOptionsError::Immutable) => Err(EdgeError::internal( + anyhow::anyhow!( + "WASI outbound request options unexpectedly immutable" + ), + )), + Err(types::RequestOptionsError::Other(_)) => Err(EdgeError::internal( + anyhow::anyhow!( + "WASI outbound request option failed" + ), + )), + } + }; + apply_timer_option(SpinTimeoutOption::Connect, opts.set_connect_timeout(Some(transport_ns)))?; + apply_timer_option(SpinTimeoutOption::FirstByte, opts.set_first_byte_timeout(Some(transport_ns)))?; + apply_timer_option(SpinTimeoutOption::BetweenBytes, opts.set_between_bytes_timeout(Some(transport_ns)))?; + if advisory_timer_refusal { + log::debug!( + "Spin host refused one or more advisory WASI HTTP timeout settings; \ + retaining the outer EdgeZero deadline race" + ); + } + + // Bound as `wasi_req`, NOT `req` — the WASI request must not shadow the OUTBOUND + // request `req`, whose `max_request_body_bytes` / `body` we read below. + let (wasi_req, request_done) = + types::Request::new(headers, Some(contents_rx), trailers_rx, Some(opts)); + wasi_req.set_method(&method).map_err(bad_request_component)?; + wasi_req.set_scheme(scheme.as_ref()).map_err(bad_request_component)?; + wasi_req.set_authority(auth).map_err(bad_request_component)?; + wasi_req.set_path_with_query(pq).map_err(bad_request_component)?; + + // The pump lives INSIDE the raced future — no `wit_bindgen::spawn`. + // `max_req` carries the portable body cap for both body kinds. **`parts` is the + // `OutboundRequestParts` from `req.into_parts()`** — the adapter is a SEPARATE crate and + // cannot read `OutboundRequest`'s PRIVATE fields, so it destructures into the pub-field + // `OutboundRequestParts` first (this also gives it `method`/`uri`/`headers`/`body` used + // to build `wasi_req` above). `max_req` is **`u64`** (the cap type, §3.1.3) and is always + // `parts.max_request_body_bytes`. A buffered `Body::Once` rides this same pump for + // cancellability (§4.4), re-expressed as a one-shot single-chunk stream, and must fail before + // its first write when its known length exceeds the cap. + let max_req: u64 = parts.max_request_body_bytes; + // Force one scheduler boundary after each accepted chunk, even when both the + // source and host writer are continuously ready. Without this, an async `while` + // loop over empty/immediately-ready chunks can monopolize one poll and prevent + // both `client::send` and the outer deadline timer from being polled. + async fn cooperative_yield_once() { + let mut yielded = false; + poll_fn(move |cx| { + if yielded { + Poll::Ready(()) + } else { + yielded = true; + cx.waker().wake_by_ref(); + Poll::Pending + } + }).await + } + + let upload_deadline = budget.deadline; + let upload_cause = budget.cause; + enum PumpCompletion { Complete, ReaderGone } + let pump = async move { + let mut sent: u64 = 0; // u64 accounting vs the u64 cap (`max_req`) — usize is u32 + // on wasm32, so a usize counter could wrap below the cap. + // `source` yields `Option>` + // (the error-type change). The item MUST be unwrapped — a source error is a real + // failure (`bad_gateway` from the wrapped stream, or a `gateway_timeout` chunk), not + // a `Bytes`. Dropping it would silently upload a truncated body. + loop { + if upload_deadline.is_expired() { + return Err(EdgeError::gateway_timeout_caused( + "deadline expired during request upload", upload_cause)); + } + let next = source.next().await; // cancellable + // Absolute post-ready check: timeout outranks a chunk, EOF, or source error + // that becomes ready at the deadline. + if upload_deadline.is_expired() { + return Err(EdgeError::gateway_timeout_caused( + "deadline expired during request upload", upload_cause)); + } + let Some(item) = next else { break }; + let chunk: Bytes = item?; // propagate source error + // pre-append cap check against max_request_body_bytes (u64; no `as`, use try_from) + let chunk_len = u64::try_from(chunk.len()).unwrap_or(u64::MAX); + if sent.checked_add(chunk_len).is_none_or(|total| total > max_req) { + return Err(EdgeError::payload_too_large(RequestBodyOverflowOrigin::OutboundUpload)); + } + // checked: bare `+=` trips `clippy::arithmetic_side_effects` (denied). + sent = sent.saturating_add(chunk_len); + let unwritten = writer.write_all(chunk.to_vec()).await; // backpressure; cancellable + if upload_deadline.is_expired() { + return Err(EdgeError::gateway_timeout_caused( + "deadline expired during request upload", upload_cause)); + } + if !unwritten.is_empty() { + // READER GONE is NOT an error. The origin stopped reading — almost always + // because it is about to send (or already sent) an EARLY FINAL response + // (413 Payload Too Large, 401, a redirect, …). Returning an error here + // would DISCARD that valid response and report 502 instead, violating + // "a completed exchange, including non-2xx, is Ok". So end the pump + // cleanly and let `send` surface the response. + drop(writer); + let _ = trailers_tx.write(Ok(None)).await; + return Ok::(PumpCompletion::ReaderGone); + } + cooperative_yield_once().await; + } + drop(writer); // EOF + match trailers_tx.write(Ok(None)).await { // completion signal + Ok(()) => Ok::(PumpCompletion::Complete), + // The host dropped the completion reader. Treat this exactly like an origin + // that stopped consuming the request: preserve the response path and never + // poll `request_done`. + Err(_future_write_error) => Ok(PumpCompletion::ReaderGone), + } + }; + + // `run_exchange` is an ORDERED state machine, NOT `join!`. `join!` would delay an + // already-available response behind a stalled source and could let a moot upload + // override an early final response. `request_done` is the request-transmission result + // returned by Request::new; it is load-bearing and must not be discarded. + // `client::send` resolves to `Result` — the + // LOW-LEVEL WASI response, NOT core `OutboundResponse`. Each success arm must (a) map the + // transport `ErrorCode` via `map_spin_send_err`, THEN (b) convert to core via + // **`to_core`, which is `async`** — `.and_then(to_core)` does NOT compile because WASI + // body collection is asynchronous. Before request fields move into the WASI request and + // upload pump, `req.into_parts()` also produces an owned `SpinResponsePolicy` containing + // `request_method`, `response_mode`, and all five response-resource settings + // (`max_brotli_window_bits`, `max_chunk_bytes`, encoded bytes, header bytes, and header + // count). `to_core` wraps status + `Fields`→multi-value + // `HeaderMap` synchronously, then: **Streamed mode** wraps the body as `Body::Stream` + // and returns immediately (no drain); **Buffered mode** `.await`s the FULL body drain + + // incremental decompress + `max_response_bytes` cap + EOF **inside `exchange`**, so the + // whole collection is bounded by the outer deadline race below (a slow buffered body + // cannot outlive the deadline). This is the hand-rolled equivalent of the SDK's + // `Response::from_response`, done here because we bypass the high-level `send` (§4.4). + // Header/resource enforcement and method-aware body disposition consume the policy; + // no value is reconstructed from the native response. + // `to_core: async fn(wasip3::http::types::Response, SpinResponsePolicy) + // -> Result`. + // Upload completion protocol (the normative transition table follows this snippet): + // - `Uploading` polls one pump step first, then `send`. A ready source/cap/deadline + // failure therefore wins over a send result ready in the same poll. + // - `Complete` transitions to `AwaitingRequestDone`; keep polling `send` for host + // progress, but retain any ready result without accepting it until `request_done` + // succeeds. + // - `ReaderGone` transitions to `ReaderGone`; retain but never poll `request_done`, and + // wait for the early response/error from `send`. + // - if `send` is ready after the pump returned Pending, it is authoritative. Drop the + // pump and `request_done` before response conversion. + // - source/cap/deadline failure does not write success to trailers. Dropping + // `trailers_tx` writes its default Err while the original EdgeError is returned. + // `client::send`, `pump`, and `request_done` are owned by one exchange; no detached task + // exists. Both WASI result sites pass through + // `map_spin_send_err(err, budget.deadline, budget.cause)`. + let exchange = run_exchange( + client::send(wasi_req), + pump, + request_done, + budget.deadline, + budget.cause, + move |native_response| to_core(native_response, response_policy), + ); + + // Response completion is a separate two-way WASI protocol, not just a trailers await. + // Create a caller-result future whose default is Err(InternalError), pass its reader to + // `Response::consume_body`, and retain its writer together with the returned body stream + // and trailers future. The body itself is a component `stream`, not an `ErrorCode` + // result. Its clean EOF/termination is handled by the stream bridge. After body EOF, await + // the response trailers/completion future; an `ErrorCode` from that future is the THIRD + // `map_spin_send_err(err, budget.deadline, budget.cause)` site, alongside `client::send` + // and `request_done`, so response-side timeout codes remain 504 rather than collapsing to + // 502. Only clean trailers permit writing `Ok(())` to the caller-result writer. That write + // returns `Result<(), FutureWriteError<_>>`, not `ErrorCode`: rejection means the host + // dropped its result reader. While a consumer is still waiting, classify that failed + // completion handshake as protocol 502; during wrapper drop, perform cleanup only because + // no consumer remains to receive an error chunk. The absolute deadline still wins and produces the + // attributed gateway_timeout (504). A local deadline/decode/cap failure or early consumer + // drop leaves or explicitly writes Err, never a false clean completion. Buffered conversion + // completes this protocol inside `exchange`; Streamed conversion stores all three handles + // in the `Body::Stream` wrapper until terminal EOF or drop. §5.4 pins clean EOF, + // truncated-response, and early-drop cases. + + // `remaining` is Option, NOT Result — `?` here would not compile in a + // Result-returning fn. An already-expired budget must become gateway_timeout + // explicitly, matching the "expiry before dispatch" contract above. + let Some(race_remaining) = budget.deadline.remaining() else { + // Attribute via the budget's cause — every budget timeout is caused (§3.3.2). + return Err(EdgeError::gateway_timeout_caused( + "deadline expired before upload dispatch", budget.cause)); + }; + + match select(pin!(exchange), pin!(spin_sdk::time::sleep(race_remaining))).await { + // RECHECK the absolute deadline for BOTH arms (deadline-wins, §3.4.1). `select` + // polls `exchange` first, so on *simultaneous* readiness (exchange completes exactly + // as the timer fires) it would return a result produced at/after the deadline — + // whether that result is `Ok` OR a decode/transport `Err`. An expired deadline + // outranks both, so a completed-at-deadline exchange becomes a 504 timeout, never a + // success and never a simultaneously-ready 502. A §5.4 boundary test drives exactly- + // simultaneous readiness (success AND failure) and asserts 504 for each. + Either::Left((resp, _)) => { + if budget.deadline.is_expired() { + Err(EdgeError::gateway_timeout_caused("deadline expired during upload", budget.cause)) + } else { + resp // Ok(within deadline) or a real Err (e.g. bad_gateway) passes through + } + } + Either::Right(_) => Err(EdgeError::gateway_timeout_caused("deadline expired during upload", budget.cause)), + } + ``` + + **`run_exchange` state machine (normative).** The helper owns pinned `send`, `pump`, + and `request_done` futures. “Poll first” is an explicit biased poll order, not merely + source-code order passed to a fairness-rotating selector. + + | State | Polling and transition | Request-side ownership boundary | + | --- | --- | --- | + | `Uploading` | Poll `pump` first for one cooperative step, then poll `send`. `pump = Err(e)` returns `e`; this includes source, cap, and upload-deadline failures. `pump = Complete` transitions to `AwaitingRequestDone` without accepting a simultaneous send result. `pump = ReaderGone` transitions to `ReaderGone`. A send result is authoritative only when that pump poll returned `Pending`. | All three futures remain owned. On authoritative send success/error, drop `pump` and `request_done` before mapping/converting the send result. On pump error, drop `send` and `request_done`; the request trailers writer defaults to failure. | + | `AwaitingRequestDone` | Poll `request_done` first, then poll `send` when its result has not already been retained. A ready send response/error is stored, not returned, so polling can continue to drive host progress. A `request_done` error is mapped with `map_spin_send_err(err, budget.deadline, budget.cause)` and wins over any stored send result. On `request_done` success, consume/drop it, then use the stored send result or continue polling `send`; map its error with the same three arguments or run async `to_core` on success. Only a successful request-trailers completion write enters this state; `FutureWriteError` transitions to `ReaderGone`. | `send` and any ready low-level response remain owned while transmission completion is established. No response conversion or caller-visible send error occurs before `request_done` succeeds. | + | `ReaderGone` | Do not poll `request_done`; poll `send` until its response/error is ready. That result is authoritative because the peer ended request consumption. | Retain `request_done` only until `send` resolves to response headers or error, then drop it **before** `to_core`. It is never stored in the response body wrapper. | + + The cooperative pump boundary after every accepted chunk is part of this state machine: + an immediately-ready or empty-chunk source cannot monopolize one poll, so `send` and the + outer timer are polled. A pump failure already ready in the same poll wins; a later source + failure after an authoritative send result is moot. For `Streamed` responses, the returned + `Body::Stream` owns only response-side body/trailer/caller-result handles described below; + no request-side writer, pump, or `request_done` handle crosses the header-return boundary. + + **Response decoder fairness.** The request pump's one-step boundary does not protect a + decoder that consumes continuously ready raw input without producing output, or decoded + output that remains continuously ready. Wrap both raw decoder input and decoded output + with independent finite, nonzero ready-item quotas no greater than 64; empty items count. + At quota exhaustion, preserve the counters across stream calls, self-wake, and return + `Poll::Pending` so `run_exchange`/the outer monotonic timer and sibling work are polled + before the next item. This self-wake is sufficient on Spin because the WASI monotonic + clock is not frozen by guest CPU work; unlike Cloudflare, no claim depends on a JS host + event advancing `performance.now()`. Recheck the original absolute deadline before + accepting the next item and before terminal EOF/error/cap decisions. Never reanchor it. + The quotas are scheduling guarantees, not byte or allocation caps. + + **Why this remains `BestEffort`.** Before response headers, dropping `client::send` + requests cancellation of its canonical-ABI subtask. After headers, that subtask is + complete; cancellation instead drops/cancels the response stream and trailers handles + and resolves the response-result protocol with `Err`. These are the correct cooperative + Component Model operations, but neither establishes synchronous host teardown or a + one-monotonic-tick bound. In addition, dropping an unwritten `FutureWriter` schedules its + default write rather than completing synchronously. The timer can select a 504 result, + but host work may outlive that selection, so both deadline capabilities stay + `BestEffort` until runtime evidence proves a finite teardown bound. + + **Completion signalling.** Clean request EOF or reader-gone explicitly writes + `Ok(None)` to the trailers future; every other drop defaults to `Err`. The separate + `request_done` result is awaited on full upload and retained on reader-gone as described + above. The `max_request_body_bytes` cap (default 8 MiB) is enforced with pre-append + checked accounting inside the pump loop, `payload_too_large` (413) on EdgeZero cap overflow. + Shared preflight rejects oversized buffered bodies before constructing or sending a WASI request. + + **`RequestOptions` do not bound the upload.** WASI 0.3 keeps `set-connect-timeout` / + `set-first-byte-timeout` / `set-between-bytes-timeout`, but these are transport / + response-side only — the WIT states they are *"separate from any the user may use to + bound an asynchronous call."* They are **not** a substitute for the race above. Attempt + to set all three to the ceiled remaining effective budget and classify every + `RequestOptionsError`: `NotSupported` is advisory for every setting. Every connect-setting + refusal, including `Immutable` and `Other(..)`, proceeds under the authoritative outer race; + it does not attach a provider cause to a later timeout. First-byte/between-bytes `Immutable` + and `Other(..)` remain typed internal setup failures. Advisory logs never include provider text. + When all three are accepted, the host's connect, first-byte, and between-bytes phases use + the configured budget. Other host timeout classes such as DNS and connection-write have + no corresponding WASI setter and may still fire independently. When one is unsupported, + the already-declared Spin BestEffort deadline limitation also covers the unsupported + phase's host default; the guest still observes the outer race if EdgeZero's absolute + deadline wins. + + **This applies to STREAMED response mode ONLY.** In **Buffered** mode `to_core` already + drained the full response body **inside** `exchange` (bounded by the outer deadline race), + so there is **no** second response race — the exchange result IS the finished response. + In **Streamed** mode, `to_core` returned `Body::Stream` without draining, so after the + exchange completes the adapter re-reads `budget.deadline.remaining()`; if `None`, the + streamed response is dropped and the slot returns `gateway_timeout` — no response wait; + otherwise the remaining duration bounds the per-chunk streamed-body wrapper race (the + §3.3.3 wrapper), so upload time is included in the batch budget rather than added on top. + (An earlier draft prescribed this second race unconditionally, contradicting the buffered + single-race flow — corrected to Streamed-only here.) +- Existing gzip/deflate/br decompression is kept; the independent decoded-output cap is enforced + incrementally for identity and EdgeZero-decoded output (§3.4.1), while the final + Buffered cap remains separate. `Streamed` mode wraps the response body as `Body::Stream`. +- **Errors — `map_spin_send_err(err, deadline, cause)` classifies the WASI `ErrorCode`, + mirroring Fastly's deadline-aware `classify` policy (§4.3) but without inventing + provenance.** At the mapping instant it checks `deadline.is_expired()` first. If true, + timeout precedence yields `gateway_timeout_caused(.., cause)` for **any** simultaneous + SDK failure. If false, one of WASI's five provider timeout variants yields bare + `gateway_timeout(..)` and therefore `BudgetSource::Unspecified`: DNS and + connection-write timeouts cannot be configured through `RequestOptions`, and no host + timeout that fires early proves which EdgeZero input selected it. The caller passes all + three values and never stamps `budget.cause` onto a provider event solely because its + variant contains `Timeout`. The `send` future (`wasip3::http::client::send` for all + body kinds — buffered and streamed both go through the hand-built request, §4.4) + fails with a `wasi:http` `ErrorCode`. **The match lists EVERY known variant explicitly and + has NO `_` arm** — so a future SDK/WIT variant **breaks the build** and *forces* the + implementer to classify it (fail-loud, the point of a security/behaviour-relevant + classifier). This resolves the earlier waffle ("keep `_` only if reachable, otherwise + drop"): with `-D warnings`, an `_` arm over an *exhaustive* enum is an + `unreachable_patterns` error anyway, so the no-`_` exhaustive match is the only form that + passes. The pinned `wasip3 = 0.6.0` generated enum is verified exhaustive and is not + `#[non_exhaustive]`; dependency upgrades must update this match and its table in the same + change. Mapping: + - **Timeout variants — ALL FIVE** (verified against wasip3): `DnsTimeout`, + `ConnectionTimeout`, `ConnectionReadTimeout`, `ConnectionWriteTimeout`, + `HttpResponseTimeout` → **`gateway_timeout` (504)**. At/after the absolute deadline the + result carries `budget.cause`; before it, the result carries + `BudgetSource::Unspecified`. **None may fall through to the generic 502.** An exhaustive + classifier unit test against the pinned SDK exercises every timeout before and after + deadline expiry. + - DNS resolution failure, connection refused, TLS/certificate establishment errors, + destination-not-found/unavailable before a response head → + **`bad_gateway_with_reason(.., Unreachable)` (502)**. Connection termination/read/write + failure after connection progress uses **`Transport`**. + - Provider-reported caller-controlled request-policy/size failures → **`bad_request` (400)**: + `HttpRequestDenied`, `HttpRequestBodySize`, `HttpRequestUriTooLong`, + `HttpRequestHeaderSectionSize`, and `HttpRequestHeaderSize`. These can be caused by + a requested target or limits exposed through the outbound API; reporting 500 would + misclassify caller input as an EdgeZero invariant failure. These provider error codes do + not prove an EdgeZero cap fired; its own upload counter returns typed 413 separately. + - Adapter/core invariant failures → **`internal` (500)**: + `HttpRequestLengthRequired` (the adapter owns framing), + `HttpRequestMethodInvalid` / `HttpRequestUriInvalid` (core preflight must reject + these), request-trailer size errors (EdgeZero emits no request trailers), + and `ConfigurationError` (the adapter constructed the request options). + - Upstream response framing/protocol variants → + **`bad_gateway_with_reason(.., Protocol)` (502)**. Shared decoder/JSON failures use + `Decode(Gzip)` / `Decode(Brotli)` / `Decode(Json)`; they do not enter this WASI + classifier. + - Host/runtime catch-all `InternalError` → + **`bad_gateway_with_reason(.., Unspecified)` (502)**. WASI defines this as the fallback + when no specific code fits; it is not evidence that an EdgeZero invariant failed. The + locally generated default `InternalError` used by request/response + completion writers is not reclassified through this arm: source/cap/deadline failures + preserve and return their original typed `EdgeError` while the default only tells the + host that completion was not clean. + - **Future/unknown variant handling.** **Every variant in the pinned + `wasi:http@0.3.0` `ErrorCode` is explicitly named** in one of four outcome buckets (504 + timeout / 502 upstream+transport+protocol+host catch-all / 400 caller-controlled / + 500 locally-invalid), so the match is + exhaustive over the pinned enum with **NO `_`** — a future WIT variant then *breaks the + build* and forces classification (the intended fail-loud behaviour). There is no `_` + arm for the pinned exhaustive enum. If a future binding changes the enum to + `#[non_exhaustive]`, that dependency upgrade must explicitly revise this contract rather + than silently adding a wildcard. The pinned + enum has ~30+ variants — `DNS-error`, `destination-{not-found,unavailable,IP-prohibited, + IP-unroutable}`, `connection-{refused,terminated,timeout,read-timeout,write-timeout, + limit-reached}`, the `TLS-*` errors, the `HTTP-request-*` / `HTTP-response-*` size/format + errors, `HTTP-response-incomplete`, `HTTP-{upgrade-failed,protocol-error}`, + `loop-detected`, `configuration-error`, `internal-error`, … Assignment rule: the five + timeouts → 504; the explicitly named caller-controlled request variants → 400; the + explicitly named adapter/core invariants → 500; **everything else + upstream/transport/protocol** → 502. The **exhaustive classifier + test enumerates every pinned variant** and asserts its bucket, so an added SDK variant + that we forgot to map trips the test (via a `#[deny(unreachable_patterns)]`-style + round-trip or an explicit variant list), not silently defaults. + - The separate wasi-timer we race the exchange against (§4.4) also yields + `gateway_timeout` on expiry. **request**-body over-cap → `payload_too_large` (413); + **response**-body over-cap (decompressed) → `response_too_large` (distinct kind, 502, §3.4.1). Any + completed exchange (incl. non-2xx) → `Ok`. +- Spin requires `allowed_outbound_hosts`; the adapter renders it from + `[capabilities.outbound].hosts` per §3.5.4 when generating `spin.toml`. +- `capability()` per §3.5.2 reports the exact outbound-cell tuple (additional + non-outbound capability variants are owned by their respective specifications): + `outbound-http` = `Native`, `outbound-header-fidelity` = `BestEffort` (footnote 8), + `outbound-complete-resource-accounting` = `Unsupported`, + `outbound-deadlines` = `BestEffort` (footnote 8), + `outbound-failure-diagnostics` = `Native` for available typed WASI causes and numeric fields, + `outbound-flexible-phase-budget` = `BestEffort` (footnote 5: request-option setters may + be unsupported and leave earlier host defaults), `outbound-batch-slot-isolation` = `Native`, + `streamed-upload-deadlines` = `BestEffort` (footnote 8), and + `lazy-streamed-response-passthrough` = `BestEffort` (footnote 7). +- **Response-out passthrough is lazy and coordinator-owned.** Spin returns the raw + `SpinResponse`; one spawned coordinator owns the WASI response, body, and result writers through + host handoff. It races pending source/write/result operations against the absolute response-write + deadline, accounts accepted prefixes, and destroys the response on failure. Host teardown and + end-client completion remain unproved, so the response-egress cells and this lazy-passthrough + cell remain `BestEffort`. The hand-built **outbound streamed-upload** path above remains + independently `BestEffort` per footnote 8. + +## 5. Test plan + +Tests are split by what they can actually prove. A core mock proves portable semantics; an +adapter seam proves conversion/classification; only a live runtime can prove host behavior. + +### 5.1 Tier 1 — core contract, no platform runtime + +Colocated `edgezero-core` tests use `MockOutboundClient`, scripted streams, and injected +monotonic instants. They must not claim platform cancellation or wire behavior. + +Required coverage: + +- Builder defaults and overrides, including buffered vs streamed mode, request and response + caps, timeout, absolute deadline, all HTTP methods, and mutually exclusive body/json + setters. +- Body constructors prove the non-overlapping error contract: `from_stream` preserves + typed `bad_gateway`, `gateway_timeout`, and `response_too_large` chunks exactly; + `from_external_stream` maps arbitrary source errors to `internal`; `stream` remains + infallible; `Bytes` and the existing buffered input types convert to `Body::Once`. +- `OutboundRequest::from_request` preserves the supplied body and exact method, including + DELETE and HEAD. Repeated normalization is idempotent and does not rewrite the method. +- URI preflight rejects unsupported schemes, missing authority, userinfo, fragments, and + invalid method/URI combinations before any adapter work. WHATWG canonicalization covers + dot segments, percent-encoded delimiters, numeric IPv4 aliases, IDNA, empty paths, and + query characters in addition to DNS names, default/non-default ports, bracketed IPv6, + and backend-target/Host/host-only/SNI/certificate values. App-visible and + adapter-visible serializations are equal. Empty userinfo is rejected through both raw-string + and typed-`Uri` constructors before WHATWG normalization can erase it. +- Request normalization strips standard hop-by-hop fields plus every field nominated by + each visible `Connection` value, case-insensitively and across repeated field lines. + Empty or invalid nomination tokens fail the whole request as 400; valid prefixes are not + partially honored. Native-fidelity raw non-UTF-8 `Connection` is also a 400. + Cloudflare's portable baseline is tested over the visible normalized strings. The fixed + request and response strip sets include `Proxy-Connection`. +- Valid non-ASCII UTF-8 custom header values survive on Native-fidelity adapters; + forbidden controls reject at construction, and invalid UTF-8 introduced through + `headers_mut` is dropped except for the fail-closed `Connection` case. +- Response normalization performs the same hop-by-hop and nomination stripping before + `Content-Encoding`/`Content-Length` interpretation. `Connection: content-encoding` and + `Connection: content-length` cannot influence decode/cap logic. Every adapter classifies + malformed visible nomination syntax as 502; Native adapters also classify malformed + raw/non-UTF-8 `Connection` as 502. Cloudflare is tested only to its BestEffort raw-header + fidelity contract. A non-UTF-8 `Content-Length` remains visible until strict framing parsing + and is therefore a protocol 502 rather than a silently absent length. +- `OutboundResponse` construction and `into_parts` round-trip request method, status, + normalized headers, body, and effective Content-Type provenance. The six-argument constructor + requires the paired clock and explicit original evidence; the five-tuple never reconstructs + provenance from normalized headers. Adapters apply bodyless handling for HEAD, every 1xx, 204, + and 304 before content decoding or streaming. For 205, clean EOF completes normally while + a positive `Content-Length` or non-empty body bytes abort the remaining native body; + a visible empty stream item also aborts conservatively without a second read. Byte-read + SDKs use their EOF result and do not attempt to reconstruct invisible frame boundaries. + Spin's caller-result protocol never reports false clean completion. Output bodies are + empty and framing metadata is normalized according to §3.4.1. `into_response` re-runs + normalization idempotently rather than owning the first pass. +- `normalize_response_headers` returns `ResponseHeadNormalization` with independently asserted + body disposition and pre-filter provenance. Cover missing, one textual value, valid non-ASCII + UTF-8, identical/conflicting duplicates, a textual value plus invalid UTF-8, a lone invalid + UTF-8 value, and `Connection: content-type` removal. Assert exact retained bytes, no trimming, + no MIME validation, and no comma splitting: invalid MIME text, a known single comma-containing + value, and a quoted comma can all remain `SingleTextUnchanged`. Dropping bad values never + upgrades original multiplicity. No body polling or copying is needed for this metadata. +- Provenance ownership tests cover the accessor, five-tuple decomposition, and converted + `Response` extension. Merely calling `headers_mut()`, an unrelated-field edit, Content-Type + replacement, and restoring the original value all invalidate a formerly eligible result as + `Ineligible(Modified)` without mutating private original evidence. Originally ineligible + reasons and `Unknown` remain unchanged despite a now-valid normalized map. The final + normalization cannot upgrade them. The converted extension is historical evidence only; + later ordinary `http::Response` edits cannot be certified by it. +- Content decoding covers gzip, br, stacked/repeated encodings, mixed case, unknown + encodings, and a repeated field set with one malformed raw value. Native-fidelity + adapters preserve/pass through a malformed encoding set as specified; Cloudflare asserts + only visible-value behavior. +- Completion tests cover two concatenated gzip members in one chunk and split across + chunks, empty members, cumulative cap overflow in a later member, corrupt/truncated + later members, and trailing garbage. Brotli trailing bytes/concatenated streams reject, + including read-ahead bytes already held by the recovered buffered reader. For both + codecs, a complete encoded payload followed by a typed source error preserves that + error; a payload followed by a pending native EOF cannot report successful completion. + Limiter, decoder, native-EOF, and framing errors release their source/native state before the + terminal `Err` item becomes visible, without requiring a second poll or wrapper drop. +- Response resource tests cover exact-limit and first-byte/entry-over boundaries for raw + encoded body bytes, decoded bytes, header bytes, and header count. They exercise repeated + fields, checked-`u64` overflow/conversion, passthrough encodings, cleanup, deadline + precedence, and exact `ResponseLimitReason` preservation. Before any body poll, a + payload-bearing identity `Content-Length` is compared with both encoded and decoded caps; + a compressed/passthrough-coded length is compared only with the encoded cap. Tests cover + exact/over values, malformed/comma/conflicting lengths, no-poll rejection, and bodyless/205 + exclusions after nomination stripping. Brotli tests cover every + standard WBITS encoding plus the large-window extension, fragmented/empty prefixes, + truncation, replay fidelity, and zero decoder-constructor calls when the window rejects. +- `OutboundRequest::into_parts` / `from_parts` round-trip every budget and response-resource + setting without replacing configured values with defaults. Adapter conversion tests set + all controls together and prove the post-dispatch response path receives each one; the + Fastly batch test inspects the pending response policy after `OutboundRequest` is consumed. +- Rechunk tests cover disabled behavior, an oversized source chunk, bounded emitted items, + lazy source pulls, error order, early drop, and deadline/fairness behavior. They explicitly + retain the source-allocation term rather than claiming the wrapper caps RSS. +- The decoder error carrier restores the exact original `EdgeError` variant/status/kind. + Raw decompressor failures become `bad_gateway` reason `Decode(Gzip)` or + `Decode(Brotli)`; over-cap becomes `response_too_large`; no path stringifies a typed + error. +- The deadline wrapper around **decoded output** covers: compressed input that stalls before + first decoded output, stalls after output, stalls at terminal EOF/trailer validation, and + yields an error concurrently with deadline expiry. It checks every yield and terminal + result; deadline/cancellation wins the simultaneous terminal race. +- Pre-append accounting rejects a chunk that would exceed either request or decompressed + response limits without first allocating/appending it. Exact-limit success and + `u64`/platform-size conversion boundaries are covered. +- `dispatch_budget` covers timeout-only, deadline-only, both orders, equal bounds, + already-expired deadline, synthetic default, overflow clamp, and one shared `now` for + `send_all_until`. `BudgetSource` asserts only selected-input provenance, never timer phase + or retry/abandonment semantics. +- Every timeout path carries `budget.cause`; bare inner/caller deadlines use + `Unspecified`. JSON does not serialize the cause. +- Every `BadGateway` path carries the narrowest available `BadGatewayReason`; the bare + constructor uses `Unspecified` only when no producer-specific origin is known. + `ResponseTooLarge` also maps to 502 but carries `ResponseLimitReason`, not + `BadGatewayReason`. Tests cover every reason and prove the JSON wire shape does not expose + `reason` for either variant. +- `HttpClient::start_batch_until` delegates the complete input and observation cutoff to its injected client. + `HttpClient::send_all_until` collects that same driver into index-aligned `Option` slots, + including empty input, mixed success/error results, and slots unresolved at the observation cutoff. Tests assert + `Completed` for complete and empty batches, `Cutoff` for explicit observation-cutoff termination, and + `OutboundBatchFailure` carrying batch-level `Internal` plus previously collected slots for an + explicit driver failure, premature EOF, or duplicate/out-of-range index. Successful-result + `None` occurs only with `Cutoff`; failure-result `None` marks a slot not collected before the + separate typed failure. These core/mock tests establish handle delegation and collection only; + adapter batch behavior is required separately by the Tier 2 batch row. +- Shared preflight tests pin the GET/HEAD streamed-body diagnostic. Tier 2 verifies each + adapter invokes that validator before its batch-only streamed-body rejection. +- Buffered and streamed response drains enforce caps and deadlines, recheck after EOF, and + preserve typed 502/504/response-too-large outcomes. +- Manifest parsing rejects unknown/duplicate capability names, required/optional overlap, + misplaced nested `capabilities` tables at every array/table depth, and every mixed-case + spelling of the reserved key outside the exact lowercase top-level location. Runtime TOML + and baked JSON run the same reserved-key policy before typed deserialization, then the same + validation/finalization path. Invalid host grammar and non-canonicalizable ports/authorities + also fail. Canonical rendering preserves the scheme of every atomic wildcard: + the absent https-only default renders exactly `https://*:*`, while explicit bare `*` + renders separate http and https entries. Baked JSON runs the same validate/finalize + pipeline and distinguishes Absent from Malformed. + +### 5.2 Tier 2 — adapter contracts, no external network + +Each adapter crate tests its shipped conversion and classification seams. + +| Surface | Required assertions | +| --- | --- | +| Capability metadata | All four adapters return the exact **outbound** matrix cells in §3.5.2, including completion order, cancellation, slot isolation, cache bypass, authority override, complete resource accounting = Unsupported everywhere, failure diagnostics = Native on Fastly/Spin and BestEffort on Axum/Cloudflare, Fastly outbound HTTP = BestEffort, Cloudflare header fidelity = BestEffort, and Spin deadline/upload/flexible-phase-budget = BestEffort. Tests do not assert a fixed global enum count; non-outbound cells belong to their own specs. A fixture adapter that relies on the trait default returns Unsupported. Because each adapter crate matches core's non-exhaustive `Capability` enum across a crate boundary, normal adapter compilation requires a wildcard; review asserts that its result is `_ => Unsupported`. A hypothetical future variant is a structural fail-closed invariant, not a value current Rust code can safely construct at runtime. | +| Request conversion | Method/body/headers/full canonical URI survive conversion; normalized hop-by-hop fields cannot reappear; EdgeZero-owned buffered and streamed request caps map to typed 413 with upload origin. Buffered overflow rejects before dispatch. Dot-segment/percent/numeric-host/IDNA cases use the exact core serialization rather than adapter reconstruction. Typed `EdgeError` request chunks retain their original category and origin through adapter conversion; in-tree paths never route them through `from_external_stream`. | +| Deadline anchoring and clock propagation | Every standard adapter installs its outbound client with the exact `App::monotonic_clock()` clone used by ingress; every low-level response constructor requires its caller to supply that clock explicitly. Every adapter captures one stored-clock snapshot as the first operation in `send`/`start_batch_until`, before normalization, preflight, or builder work. An injected clock advances during preparation and proves that elapsed time consumes the valid request's original budget; no path re-anchors or bypasses the client clock. Preflight slot elapsed, provider-error precedence, post-ready expiry, deferred upload/response streams, returned `OutboundResponse` bounded-until collection, and response-egress coordinator checks use that same handle. A backwards clock cannot enlarge `budget.duration`; a backwards terminal sample produces zero elapsed plus an internal invariant outcome. Invalid-request precedence remains the shared validator's result because validation still runs before budget selection. | +| `start_batch_until` / `send_all_until` on every adapter | Run the production batch orchestration with injected transport/clock seams for Axum, Cloudflare, Fastly, and Spin. Empty input returns empty without dispatch; mixed valid/invalid slots retain exact input indices; preflight failures dispatch no work for that slot and never poll rejected source streams; GET/HEAD body errors precede batch-only errors; transport errors, cap failures, timeouts, and non-2xx responses preserve sibling outcomes without cancelling siblings. Assert every emitted slot carries its own elapsed time from the one method-entry snapshot through that slot's terminal point; advance the injected clock during preflight to prove that time is included. A same-tick terminal result may legitimately report zero. A slow sibling's later completion must not overwrite an earlier slot's elapsed value. Valid one-slot buffered batches match single-send outcome semantics while elapsed is asserted independently. Script reverse completion order on concurrent adapters; assert every eligible exchange is polled before a stalled sibling finishes. Assert cancellation retains already emitted items, reports unresolved indices, and does not consume a ready item when a `next()` future is dropped. Fastly tests dispatch-before-selection, selected-handle identity reassociation, all three reassociation failure diagnostics, exact error-to-driver-event propagation with previously collected slots, bounded groups, and immediate elapsed sampling after each preflight/dispatch/selected-body terminal result while retaining its documented BestEffort timing caveats. Backwards injected time produces zero plus an internal outcome, distinguishable from legitimate zero by outcome. | +| Streamed fan-out usage | On Axum/Cloudflare/Spin, join per-request tasks containing both `send` and body consumption. Script a fast response whose body can finish before its deadline while a sibling's headers remain pending beyond it. Assert the fast body is consumed and succeeds before those sibling headers arrive. Joining only sends and delaying all body consumption must fail this regression. Fastly is excluded from this non-portable usage pattern. | +| Response conversion | Every adapter enforces adapter-visible upstream header limits before normalization, including fields normalization later strips, accounts the synthetic proxy marker after normalization, then applies body/decode caps, calls the shared four-state content-encoding classifier, passes the originating method and retained clock into `OutboundResponse`, and settles native body handles for framing-bodyless and 205 responses; repeated `Set-Cookie` survives. HEAD/304 malformed, conflicting, comma-list, and `u64`-overflow `Content-Length` fail as protocol 502 before body polling while valid representation lengths are retained; 1xx/204 remove the field. Effective identity includes absent or exactly one bare `identity`, and its decoded over-cap `Content-Length` rejects before body polling. Encoded, decoded, header-byte/count, and Brotli-window failures preserve typed reasons and cleanup. | +| Original Content-Type provenance | Drive actual response-conversion paths for buffered and streamed payloads plus HEAD/204/205/304. Axum/Fastly/Spin carry original normalization evidence through all constructor branches. Cover missing, textual, non-ASCII UTF-8, identical/conflicting duplicates, textual plus non-UTF-8, lone non-UTF-8, invalid MIME text, a known single comma-containing value, quoted commas, and connection-nominated removal; assert exact retained bytes and original reasons despite lossy filtering. An Axum local raw-TCP response with one textual and one non-UTF-8 Content-Type must retain `MultipleValues` even when the filtered map has one value. Cloudflare always supplies `Unknown`, even for absent/single/merged values, because its Fetch iterator cannot recover original bytes or field boundaries. Mutable header access never upgrades evidence. These tests establish conversion behavior, not MIME validity, debug safety, or a new capability. | +| 205 settlement | Test declared-body immediate abort, one in-budget clean EOF, observed non-empty bytes, read failure, and deadline precedence. Where the SDK exposes empty items, one empty item aborts without another read; on Fastly a non-empty read buffer returning zero is EOF. Cloudflare additionally tests null-body host suppression with absent/zero and positive visible lengths without claiming hidden-byte visibility. Assert native-handle cleanup and Spin completion signalling using observable results, never synthetic wire-frame visibility. | +| Header fidelity | Axum/Fastly/Spin preserve repeated outbound request field lines and exercise raw malformed response nomination/encoding lines. Cloudflare tests request list semantics and the visible response-string baseline without asserting unavailable octets/line boundaries. Cloudflare encoded passthrough uses `EncodeBody::Manual`; its streamed downstream `Content-Length` is asserted only to the documented BestEffort scope. | +| Decoder integration | Each adapter uses the shared decoder/carrier/deadline pipeline; gzip/deflate/br stalls before output, midstream, and after codec completion but before native EOF produce attributed 504 rather than hanging or degrading to 502/500. Exercise both Buffered and Streamed modes, multi-member gzip/cumulative caps, trailing data, and late typed source/completion errors. No guard disarm or Spin caller-result success occurs solely because a decoder reached its end marker. | +| Response-egress fallback | Every adapter uses the same live attempt for a bounded precommit fallback, preserves the original cause and fallback metadata, and never replaces a response after commit. | +| Axum response-out scheduling | A connection-local `LocalSet` drives the private non-`Send` Hyper body without a blocking bridge. Raw-socket tests prove lazy frames, a zero-read client's independent deadline close, disconnect/drop handling, and deterministic collateral HTTP/1 pipeline attribution. | +| Lazy response-out | Every adapter's local pump emits the first chunk before source EOF. Axum and Cloudflare have target-appropriate proof and report Native. Fastly and Spin remain BestEffort pending the production-runtime evidence in footnotes 6/7. | +| Axum timeout | Fake-time/transport seams prove the remaining budget is armed once and timeout errors preserve provenance. | +| Cloudflare raw fetch | The constructed Request has `RequestRedirect::Manual`; separately, the actual final fetch initializer contains `encodeResponseBody: "manual"` and the guard's abort signal together without resetting redirect. Property-set exception or false returns `internal` without dispatch. The helper uses the checked response conversion and absolute-deadline precedence from §4.2. Native tests assert the exact canonical string supplied to the Web request; they do not call it the final wire URL. WASM compilation verifies the worker 0.8.5 re-exported bindings; SDK tests inspect both boundaries. Effective origin-observed WHATWG target/Host behavior is a future Tier 3 promotion gate. | +| Cloudflare cancellation | The production guard's injected abort operation fires on timeout, buffered cap overflow (early length rejection and incremental decoded overflow), normalization/decode/read failure, early 205 disposition, send-future cancellation, streamed decode/read failure, and early consumer drop. Explicit abort plus drop fires once; normal completed bodies disarm; a streamed response transfers guard ownership. Preserve 502/504/response-too-large/empty-205 outcomes and absolute-deadline precedence. A bare dropped Rust future is insufficient. | +| Streamed request deadline boundary | Axum and Cloudflare check expiry before every pull and after every ready source result. Fake-time streams cover a chunk, EOF, and source error becoming ready exactly at expiry, plus always-ready empty chunks. Cloudflare's clock fixture remains frozen until the injected host-event yield completes: assert the ready-item quota is finite/nonzero and no greater than 64, survives polls/stream yields, counts empty items, and permits no next item before the quota-triggered host yield. Ready-only terminal success/error/cap paths also yield before their expiry decision. Assert attributed 504 without reanchoring the deadline. | +| Cloudflare decoder fairness | With the same frozen clock, test ready-empty raw input that never produces decoded output, continuously ready decoded output, and native-EOF validation after codec completion. Both input and output quotas force host-event progress; terminal EOF/error and adapter-owned cap decisions at expiry yield attributed 504. Generic streamed-consumer caps after a within-budget yield retain §3.4.1's narrower ownership rule. Dropping during a host yield cleans up the owned subrequest guard. An independently advancing fake clock alone cannot establish these invariants. | +| Timeout provenance | Each adapter's actual timed-out result covers timeout-wins, deadline-wins, and synthetic-default input selection in single send, buffered fan-out, and streamed error chunks; no adapter emits a bare un-attributed timeout **for an EdgeZero-owned budget timer**. Spin separately proves that an early WASI provider timeout is 504 with `BudgetSource::Unspecified`, while an error observed at/after absolute expiry is attributed to the selected budget. Fastly proves configured connection/response phase timers retain the selected cause, an early unconfigured DNS timeout is `Unspecified`, and absolute expiry wins every simultaneous result. | +| Fastly stages | Backend identity/canonical host/TLS/SNI inputs, true ceil-to-millisecond phase rounding, cold registration, dispatch-all-before-selection, selected-handle identity reassociation, bounded selection groups, and streamed-upload cooperative checks match §4.3. Heterogeneous-host/budget tests prove distinct backend identities; a scripted cold-registration test proves an over-slack later cache miss is rejected before another registration. Target-neutral tests prove selection metadata follows provider-returned handle identity, and the Fastly contract gate proves streamed-slot preflight failures do not poison eligible siblings. The feature-gated overhead seam proves the final arming guard. `SendFailure` distinguishes budgeted phase timeout from unconfigured provider timeout; both map to 504, with the exact provenance boundary above. Pre-response DNS/connect/TLS establishment -> 502 reason `Unreachable`, later transport -> 502 reason `Transport`, upstream protocol -> 502 reason `Protocol`, unknown/registration rejection -> 502 reason `Unspecified`, local invariants -> 500, and the separately named Fastly platform-internal class -> 500. SDK-gated in-crate tests construct every known `SendErrorCause` and assert both `cause_to_failure` and composed status/kind/reason, including before/at/after deadline provenance; the Fastly WASM `--lib` gate executes them. Only hypothetical future variants remain compile/review coverage. | +| Fastly upload finalization | Empty-source EOF and multi-chunk success call `finish()` exactly once before response wait. Source/cap failure and pre-finish expiry drop both handles without finishing; a typed source error is preserved. Write/flush/finish errors within budget are 502; success or failure returning at expiry is attributed 504. A finish failure or post-finish expiry never waits on `PendingRequest`, and dropping the send future settles its owned handles without successful finalization. Tests use the production upload driver with a scripted writer/pending-handle seam; they do not claim a finite host-write bound. | +| Spin request protocol | Exercise every `run_exchange` transition and ownership boundary: after full upload, `send` continues to be polled but a ready result is retained until `request_done` succeeds; a `request_done` error wins over that stored result and is mapped; reader-gone retains but never polls `request_done` until `send` resolves, then drops it before response conversion; send-first drops all request handles; clean EOF/reader-gone writes `Ok(None)` trailers; source/cap/deadline failure leaves the default `Err`. Biased simultaneous readiness makes an already-ready pump failure beat `send`, while send ready before a later source failure remains authoritative. An always-ready empty-chunk source yields between chunks so send/timer polling cannot starve, and no request-side handle enters a streamed response wrapper. The injected RequestOptions suite covers accepted settings, advisory NotSupported for every setter, advisory Immutable/Other for Connect, and typed Immutable/Other errors for FirstByte/BetweenBytes. The local async Spin service additionally proves pending aggregate timer wakeup, expired-entry zero polling and guest-owner release after each connect outcome; the synchronous SDK command runner does not supply that timer evidence. The SDK-resource suite separately invokes each real setter and records the pinned host's actual result. | +| Spin response protocol | `consume_body` receives the caller-result reader; stream/trailer handles retain the writer; clean EOF/trailers writes `Ok`; body/decode/deadline failure writes or defaults to `Err`; no handle is dropped before its terminal branch. Ready-empty raw input and continuously ready decoded output hit finite input/output quotas, return `Pending`, poll the outer timer/siblings, and retain counters across calls. | +| Spin error classifier | Enumerate every pinned `ErrorCode` with no wildcard. At/after absolute expiry every simultaneous SDK result -> attributed 504. Before expiry, the five provider timeout variants -> 504 with `BudgetSource::Unspecified`; caller-controlled request denied/body/URI/header-size variants -> 400; demonstrated length/method/URI/trailer/config invariants -> 500; pre-response DNS/connect/TLS establishment -> 502 reason `Unreachable`; later connection/read/write failure -> 502 reason `Transport`; upstream response framing/protocol -> 502 reason `Protocol`; host `InternalError`/forced future fallback -> 502 reason `Unspecified`. Shared decode failures separately assert the exact coding reason. Test the three actual `ErrorCode` sites: `client::send`, `request_done`, and the response trailers/completion future. Test component-stream termination and caller-result `FutureWriteError` through their distinct policies; synthetic setter outcomes stay in the injected suite because the real host controls them. | +| Spin timer | Timer selection returns guest-visible 504 and drops owned guest handles. Tests do **not** claim bounded host teardown; that remains Tier 3 characterization and an upgrade criterion. | +| Simultaneous terminal race | For decoder, transport, and Spin exchange seams, a result becoming ready at the absolute deadline yields attributed 504 whether the competing result is success or error. | +| CLI runtime gates | Table-drive every constructible ladder state: required Native/BoundedCooperative succeeds; required BestEffort/Unsupported fails; optional degradation warns and proceeds. Cover missing-registry empty, optional-only, and required manifests; `ManifestContract::None` proceeds while Malformed fails closed. Normal CLI compilation matches core's non-exhaustive `CapabilitySupport` and `ManifestContract` across the crate boundary and therefore requires wildcard arms; review asserts that required-support and contract wildcards return errors, while the optional-support wildcard warns. Hypothetical future variants are not fabricated with unsafe runtime construction. Build/serve/deploy/`deploy --staging` gate before shell dispatch through both `execute_runtime` and `execute_capture_runtime`, and demo gates before startup. Auth/version/healthcheck/rollback remain on the existing exempt dispatcher and never construct `ResolvedRuntime`. Both outbound-scoped dispatchers derive adapter/action from the owned runtime, so no caller-supplied identity can diverge between gate and side effect. Provision/config commands are outside this spec and are not asserted. | +| Runtime resolver | Nested and descendant apps, competing targets, explicit relative/absolute `EDGEZERO_MANIFEST`, missing/malformed input, genuine manifestless targets, and containment/symlink failures resolve one adapter/action/target/contract unit for build/serve/deploy/`deploy --staging` only. Both outbound-scoped dispatchers gate that contract exactly once and execute that target without cwd/env mutation or rediscovery; shell overrides remain rooted at the selected manifest. Registry dispatch receives only typed `AdapterExecutionTarget`, never synthetic path args; conflicting cwd fixtures prove every in-tree `execute_target` honors the pinned app root/platform manifest/component and the default implementation fails closed. Fixture adapters prove the canonical adapter and action captured by the resolver are exactly those observed by capability lookup and final dispatch. Operational actions reject this resolver and retain their existing path. | +| Spin host drift | Build/serve/deploy resolve explicit/single/ambiguous component selection and compare only the selected component's canonicalized set before shell dispatch. Equivalent spelling/order passes; actual drift reports the manifest, component, and expected canonical list without writing files. Rendering `None`/`https://*:*` must not produce bare `*` or any `http` atomic; explicit input `*` produces the two scheme-specific entries. | + +### 5.3 Tier 3 — live host behavior + +- **Axum:** a loopback origin verifies real methods/bodies/headers, gzip/deflate/br decode stacks, + bodyless responses, non-2xx pass-through, response stalls, upload stalls, cap errors, + timeout provenance, and deterministic request/body/source future drop. HTTP/1 closure and + HTTP/2 stream reset are tested separately; pooled-connection teardown and bounded origin + observation are not portable promises. Deployed HTTP/2, and HTTP/3 when enabled, is + optional characterization only. +- **Cloudflare future promotion fixture:** a pinned workerd-compatible harness executes real Worker SDK response + conversion, including the Workers-only `Headers.getAll("set-cookie")` boundary and + repeated `Set-Cookie`, then verifies AbortController cancellation + from the origin's point of view on timer expiry, buffered over-cap/decode failure, + send-future cancellation, and early streamed-body drop, plus lazy streamed-response + delivery. Use origins that keep sending or stall after the triggering result so the + test distinguishes cancellation from normal EOF. In both Buffered and Streamed modes, + gzip/deflate/br decode exactly once and respect decompressed caps; unknown, parameterized, + stacked, and repeated visible encodings preserve the original wire bytes and visible + encoding/length metadata through the portable policy. Pin the harness version and + compatibility settings, including the production template's exact compatibility date/flags + and support for the final-fetch raw-response option; commit the standalone fixture's own + Cargo lock and assert Worker 0.8.5 from that graph before building; + verify encoded downstream passthrough separately from upstream raw-byte receipt. A + runtime that ignores the raw-response option fails this acceptance gate. Passing this + deployed fixture is required evidence before Cloudflare's deadline/cancellation cells can + be promoted to `Native`; the fixture is not present evidence in this PR. Raw + malformed-header fidelity is not asserted because workerd does not expose it. + Exercise the selected production yield primitive with a frozen-clock injection matching + deployed semantics for ready-only upload/raw/decode loops. Observe another host event and + clock advancement before the next quota of items, then verify terminal timeout + precedence. Run one deployed timing probe with the pinned compatibility settings. A + zero-duration `worker::Delay` is accepted only if that probe proves the property; local + workerd's freely advancing clock alone is not evidence. +- **Spin:** a pinned `spin up` harness and external origin record whether stalled upload + and response work is eventually cancelled and whether any component work remains. + Current capability values stay BestEffort regardless of a single passing run. Promotion + to Native requires a documented finite bound, repeatable host-observed tests, and the + corresponding matrix/spec update. +- **Fastly:** a protected manually dispatched job exercises one disposable service with + dynamic backends enabled and one with them disabled. It proves arbitrary canonical + destinations on the enabled service and the exact typed 502/no-origin-contact behavior on + the disabled service, plus records timeout/upload/streaming observations. Phase 6 owns a + standalone, independently locked probe crate because app-demo does not migrate until + Phase 7. The protected workflow builds its fixed package before exposing secrets, and the + driver can deploy only that package. The result + characterizes the `BestEffort` cell; it cannot make the static capability gate aware of + selected-service entitlement. + +### 5.4 Required test-case index + +The Tier 1 bullets, Tier 2 table, and Tier 3 runtime list above are the required test-case +matrix referenced throughout §§3–4. A reference to “the §5.4 row” means the matching +surface in those lists; implementation plans may split one surface into multiple focused +tests, but may not weaken its tier or substitute a mock for a host-observed claim. + +### 5.5 Executable test seams and CI impact + +Integration tests that need deterministic adapter states use a narrowly feature-gated +`test-utils` seam rather than `#[cfg(test)]`: external `tests/*.rs` crates do not see +the library's `cfg(test)` items. Seams inject clocks, transport results, or documented +adapter-stage delays; they do not duplicate the behavior being tested or expose platform +SDK types to core Tier 1 tests. + +**Feature and target contract.** Each of the four adapter `Cargo.toml` files declares +`test-utils = []`. It exposes only the injection accessors and SDK-independent production +drivers needed by these tests; it does not enable `axum`, `cloudflare`, `fastly`, `spin`, +or `cli`, and does not change existing default features. Production adapters call those +same drivers with real clock/transport/handle operations. Tests inject below the behavior +under test; copied algorithms and a core mock client are not adapter contract coverage. +Platform SDK bindings remain behind their existing feature/target gates. Public test-only +callbacks retained by a `Send + Sync` client must preserve those bounds. + +Add Axum's `tests/contract.rs`. In the existing Cloudflare, Fastly, and Spin contract files, +move the whole-file platform/WASM gate onto the SDK-test module and add a native +`test-utils` module. Otherwise the commands below can succeed while running zero tests. +Gate injection-dependent tests on `test-utils`; each native contract target must execute +the shared `send_all_until` cases and its applicable adapter-driver rows in §5.2. SDK-dependent +conversion tests run with a supported host harness, not by calling WASM imports on a native +host. Cloudflare's ordinary browser WASM runner is limited to portable Web APIs and bridge +construction because Worker 0.8.5's uncaught `Headers.getAll()` binding is Workers-only; +its real response conversion runs under workerd. Compile checks alone do not substitute for +test execution. + +**Explicit execution gates**, added to `.github/workflows/test.yml` when each adapter is +implemented (after the existing locked dependency fetch): + +```sh +scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --test contract +scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils --test contract +scripts/run_test_nonzero.sh batch_preflight_rejects_streamed_slots_without_poisoning_siblings cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --test contract +scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils --test contract +``` + +These execute the no-network native seams; Axum's command explicitly enables its native +runtime. `scripts/run_test_nonzero.sh [--ignored] ` lists the selected +suite first, requires the exact sentinel and at least one listed test, then runs the same +selection; every native/WASM/SDK-resource/CLI suite in CI uses it. Workerd/deployed probes +instead require exact probe IDs plus a positive summary count in their own driver. These +gates become runnable as the features and test modules land, not in the design/Phase 1a +change set. Extend the existing WASM contract matrix's feature argument to +`--features "${{ matrix.adapter }},test-utils"`. Retain the Cloudflare and Fastly target +and runner settings. **Spin's bare `wasmtime run` is not an SDK-resource execution gate.** +The validated runner is +[Wasmtime 48.0.2](https://github.com/bytecodealliance/wasmtime/releases/tag/v48.0.2) with +`-W component-model-async=y -S http=y -S p3=y`; CI reads that exact version from +`.tool-versions`. Spin SDK 7's stable WASI HTTP 0.3 imports do not link against the former +Wasmtime 44/45 runner line. The compatibility gate must execute the real contract and +SDK-resource suites, including `Fields` and `RequestOptions` construction/setters. A compile +check, native fake-resource tests, or zero-test success is not a substitute. Any runner-pin or +flag change must rerun both suites; this gate remains separate from Tier 3's +origin-cancellation characterization. +GitHub delivers `workflow_dispatch` only when the workflow file exists on the default +branch ([GitHub workflow syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#onworkflow_dispatch)). Before adapter implementation starts, a standalone infrastructure bootstrap PR must +therefore land inert protected dispatchers for both Cloudflare and Fastly. They publish no +runtime/API/capability change, have no automatic secret-bearing trigger, and only check out a +maintainer-reviewed exact input SHA proven reachable from the fixed repository-owned +`refs/heads/outbound-probe-reviewed` ref before running that tree's probe driver. This bootstrap +is the sole permitted intermediate merge in the otherwise branch-only implementation series. +The reviewed ref is protected against force pushes and deletion; evidence commits advance it +only by fast-forward, and the protected run/job name displays the exact input SHA for the +environment reviewer. +Each provider dispatcher uses a fixed concurrency group with cancellation disabled. Fastly's +driver records and restores both services' prior active versions, removes only versions that +its own run created, and deletes only its own origin-observation namespace. + +Cloudflare's deployed probe uses that default-branch `workflow_dispatch` workflow bound to a protected +`outbound-cloudflare-probe` environment, never `pull_request_target`. It verifies an exact +input commit SHA before exposing disposable-account secrets +`CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_WORKERS_SUBDOMAIN`, +`OUTBOUND_PROBE_ORIGIN_URL`, and `OUTBOUND_PROBE_ORIGIN_TOKEN`. The driver creates a unique +Worker and origin-observation namespace per run, requires named probe IDs and a positive +count, and deletes both in `finally`; assertion and cleanup failures fail the job. The +observable origin exposes authenticated arm, case, observation, and delete endpoints for +raw bytes, stalls, EOF, and disconnect facts. Untrusted forks run workerd only; after review, +a maintainer reproduces the exact tree on `refs/heads/outbound-probe-reviewed` and dispatches the protected +job for that SHA. Phase 4 specifies the concrete commands and origin protocol. + +Fastly WASM gates assert repository-pinned Viceroy 0.21.0 and run from the adapter crate so +its committed runner, including the demo `fastly.toml`, is used without an environment +override. Its default-branch deployed entitlement dispatcher follows the same exact-SHA, +protected-environment, disposable-resource, positive-probe, and cleanup rules in Phase 6. +It validates and builds Phase 6's independently locked fixture without secrets, then the +secret-bearing driver deploys only the fixed prebuilt package path. Keep ordinary production +feature/target checks and the Fastly WASM `--lib` gate. SDK +conversion checks and native fake-transport tests do not establish host cancellation; +the Tier 3 jobs still provide that evidence. + +`#[non_exhaustive]` future variants are a special case: stable Rust cannot safely construct +a variant that does not exist in the pinned enum. Tests therefore exercise every known +state; normal cross-crate compilation forces the production wildcard arms, and review pins +their fail-closed result (`Unsupported` or an error). They must not use `unsafe` discriminant +fabrication or add test-only public enum variants, which would alter serialization and the +macro crate's textually included manifest definitions. The pinned Spin/WASI `ErrorCode` is +an exhaustive enum and is therefore the opposite case: its classifier has no wildcard, so +an SDK variant addition fails compilation and must be assigned explicitly. When a real variant is added, it +becomes a normal table-driven case in the same change. + +Required gates for implementation changes: + +1. Repository `CLAUDE.md` format, test, clippy, feature-combination, and Spin WASM checks. +2. Per-adapter WASM target checks and no-network contract tests. +3. Generated-project build and the excluded `examples/app-demo` build. +4. Published-doc link/navigation verification. +5. Cloudflare host-observed cancellation is a promotion gate for its current `BestEffort` + deadline cells; it is not present evidence for a `Native` claim. +6. Fastly enabled/disabled dynamic-backend characterization for actual adapter behavior; + the static cell remains BestEffort. +7. Spin host-observed characterization job when available; failure preserves BestEffort + and blocks only a proposed promotion to Native. + +Version-specific behavior is a manifest contract, not merely a property of whichever lockfile +was inspected. The implementation phases pin `async-compression = "=0.4.43"`, +`compression-codecs = "=0.4.38"`, `compression-core = "=0.4.32"`, +`flate2 = "=1.1.9"`, `miniz_oxide = "=0.8.9"`, `adler2 = "=2.0.1"`, +`crc32fast = "=1.5.0"`, and `simd-adler32 = "=0.3.9"` for the shared +gzip/deflate decoder graph. The fixed flate decoder charge and checksum/source contract are +recorded in `docs/audits/2026-09-29-flate-decoder-memory-accounting.md` and enforced by the +separate flate dependency audit. +`reqwest = "=0.13.4"`, `worker = "=0.8.5"`, `spin-sdk = "=7.0.0"`, `fastly = "=0.13.1"`, +`fastly-shared = "=0.13.1"`, `fastly-sys = "=0.13.1"`, and +`log-fastly = "=0.13.1"` in the workspace manifests that own those dependencies, refresh +both the root and excluded app-demo lockfiles, and assert both resolved graphs. Spin uses the +SDK's `wasip3` re-export rather than a second direct dependency. SDK 7 is pinned to +`https://github.com/stackpop/spin-rust-sdk.git` at +`e65ff90dadf4173b08b14194e878ad3acd555337`, selecting `wasip3 0.9.0+wasi-0.3.0` and the +corrected `wit-bindgen 0.62.0` executor. See the +[SDK dependency audit](../../audits/2026-10-27-spin-sdk-eager-progress.md) and +[upstream change](https://github.com/spinframework/spin-rust-sdk/pull/169). +Use `spawn_local` without a compatibility alias or another executor. Adapter manifests inherit the corresponding workspace dependency +rather than declaring a broader compatible range. Viceroy is pinned to 0.21.0 because older +repository/local releases reject imports emitted by Fastly SDK 0.13.1 before tests execute. An +SDK enum/table or raw bridge may not be published from a graph that can silently select a later +compatible release. + +The spec and Phase 1a plan themselves are documentation changes, so review-time +verification is Markdown structure, internal-link/anchor consistency, scoped terminology, +and `git diff --check`; Rust builds are required when an implementation plan is executed. + +## 6. Migration impact + +No compatibility shims are introduced for the outbound API rename. + +| Before | After | +| --- | --- | +| `crates/edgezero-core/src/proxy.rs` | `crates/edgezero-core/src/outbound.rs` | +| `ProxyClient` | `OutboundHttpClient` | +| `ProxyHandle` | `HttpClient` | +| `ProxyRequest` | `OutboundRequest` | +| `ProxyResponse` | `OutboundResponse` | +| `ProxyService` | removed; use `HttpClient` | +| `RequestContext::proxy_handle()` | `RequestContext::http_client()` | +| adapter `*ProxyClient` | adapter `*OutboundClient` | + +Current response APIs are a hard cut, including migrations from intermediate PR revisions: + +| Retired outbound API | Required replacement | +| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| four- or five-argument `OutboundResponse::new` | six-argument `new(method, status, headers, body, clock, content_type_provenance)` requiring the application clock and original evidence | +| four-element `OutboundResponse::into_parts()` | five-tuple `(method, status, headers, body, provenance)` with effective `ContentTypeProvenance` | +| `normalize_response_headers(..) -> Result` | `Result`; use `.body_disposition` and retain original `.content_type_provenance` | + +Outbound-facing changes: + +- `OutboundRequest` and `OutboundResponse` continue to use the unified core `Body` + type. Buffered responses remain the default; `stream_response()` opts into a streamed + response. `OutboundRequest::from_request` preserves the source method, normalized + headers, and supplied body without requiring any inbound extractor or body-state + redesign. +- `OutboundResponse` carries the originating request method through construction and + `into_parts` so HEAD and other bodyless-response rules remain enforceable at the final + conversion boundary. Construction now requires six arguments, including the paired clock + and explicit `content_type_provenance`; decomposition returns five elements, ending in + effective provenance. `normalize_response_headers` returns `ResponseHeadNormalization`, + not just `ResponseBodyDisposition`. No compatibility signatures remain. Conversion carries + the enum as a historical outbound-boundary response extension, not a guarantee about later + ordinary `http::Response` edits. +- `ProxyHandle::client()`, outbound request/response `body_mut()`, and outbound + `extensions()` / `extensions_mut()` are removed. `HttpClient` exposes only + `send` / `send_all_until`; the new request and response values are builder-style and do + not carry `Extensions`. +- `PROXY_HEADER` and the observable `x-edgezero-proxy: ` response header are + preserved. The constant moves to `outbound.rs` without changing its value. +- `Body::Stream` changes its chunk error from `anyhow::Error` to `EdgeError` so + deadline, gateway, and over-cap outcomes survive decoder and adapter boundaries. + `Body::from_stream` now accepts only streams already returning `EdgeError`; + callers with arbitrary external errors use `Body::from_external_stream`, which + deliberately maps them to `internal`. `Body::into_stream` returns the exact typed + stream. These signatures are public breaking changes. Every existing + `Body::from_stream`, `Body::into_stream`, and direct `Body::Stream` call site must be + audited: EdgeZero-owned outbound/decoder/deadline producers use the typed constructor; + platform inbound-body sources that previously relied on the generic `anyhow` mapping + use `from_external_stream` to preserve their existing behavior. That mechanical + constructor migration is required for compilation but does not redesign inbound body + extraction, caps, or state. +- `EdgeError` gains `BadGateway { reason: BadGatewayReason }`, + `GatewayTimeout { cause: BudgetSource }`, and + `ResponseTooLarge { reason: ResponseLimitReason }`. Their JSON wire shape remains the + existing error envelope and does not serialize any reason/provenance field. Their wire + messages, plus `Internal`, are fixed category strings; detailed provider diagnostics and + request targets are not serialized. +- `Manifest` gains the outbound matrix capabilities and + `[capabilities.outbound].hosts`. Existing non-outbound capability and store schemas are + unchanged. The depth-independent misplaced-`capabilities` rejection is intentionally + fail-closed. +- `Adapter` gains the defaulted `capability()` method. Each in-tree adapter returns all + outbound matrix values and ends its non-exhaustive match with + `_ => CapabilitySupport::Unsupported`. +- Capability enforcement uses outbound-scoped `execute_runtime(..)` and + `execute_capture_runtime(..)`, which share one helper that gates `build` / `serve` / `deploy` / + `deploy --staging` exactly once before shell or registry dispatch, and `run_demo` gates + the Axum demo from its baked manifest. `auth *`, version emission, healthcheck, + rollback, provision, and config commands are not changed by this outbound specification. +- Build/serve/deploy resolve one `ResolvedRuntime` containing the action, a pinned canonical + adapter identity, a paired adapter target, and an optional `ResolvedManifest`. Dispatch + accepts no separate adapter/action arguments: the selected contract is gated exactly once + and the selected target is executed without reparsing, rediscovery, or process-wide + cwd/env mutation. +- Scaffolding, `examples/app-demo`, and public proxying docs migrate to the renamed + outbound types. Portable multi-adapter examples declare `optional = ["outbound-http"]` + because Fastly's service entitlement is not statically provable; single-target apps may + declare it required on an adapter whose matrix cell satisfies the gate. + Spin's generated platform manifest consumes the canonical outbound host list. +- `docs/guide/capabilities.md` documents all outbound capabilities, the support + matrix, and each BestEffort caveat, and is linked from the VitePress sidebar because + runtime diagnostics point to that published page. + +Repository-wide completion sweeps cover `ProxyClient`, `ProxyHandle`, +`ProxyRequest`, `ProxyResponse`, `ProxyService`, `proxy_handle`, and the four +adapter `*ProxyClient` names across Rust sources, examples, templates, and published +docs. A separate compile-driven sweep covers every `Body::from_stream`, +`Body::into_stream`, and direct `Body::Stream` construction/consumer so no typed +`EdgeError` is accidentally erased at the new explicit external-error boundary. A +generated-project build verifies that scaffold templates compile against the new outbound +API. Inbound body extraction/state and store/config lifecycle behavior remain owned by +their dedicated designs and are not migration prerequisites here. + +## 7. File-by-file change summary + +Exact filenames should follow the current tree at implementation time; function names are +the durable anchors. + +**`crates/edgezero-core`** + +- `src/lib.rs` exports `body`, `compression`, `outbound`, and `time`, plus root re-exports + `BodyStream`, `ContentEncoding`, `ContentTypeIneligibility`, `ContentTypeProvenance`, + `Deadline`, `DispatchBudget`, `HttpClient`, `OutboundHttpClient`, + `OutboundRequest`, `OutboundResponse`, `OutboundSlotResult`, `ResponseBodyDisposition`, + `ResponseHeadNormalization`, `ResponseHeaderLimiter`, `ResponseMode`, `BROTLI_DECODER_FIXED_CHARGE_BYTES`, + `brotli_decoder_memory_charge`, `classify_content_encoding`, `collect_response_stream`, + `decode_brotli_stream`, `decode_gzip_stream`, + `dispatch_budget`, + `enforce_payload_content_length`, `limit_decoded_stream`, `limit_encoded_stream`, + `normalize_response_headers`, + `rechunk_stream`, `validate_for_dispatch`, and the outbound capability types; remove proxy exports. Adapter crates + use these root re-exports as their stable import surface. +- `src/proxy.rs` becomes `src/outbound.rs`. It owns request construction and + validation, canonical URI accessors, the private `BudgetInputs` accessor consumed by + `time.rs`, `HttpClient::send` / `send_all_until`, bounded response drains, hop-by-hop + normalization, response-bodyless rules, and the preserved `PROXY_HEADER`. + `OutboundResponse::new(method, status, headers, body, clock, content_type_provenance)` + requires pre-filter evidence. `content_type_provenance()` accounts for separate header-dirty + tracking without changing private original evidence, and `into_parts` returns + `(method, status, headers, body, provenance)`. The normalization helper returns both body + disposition and original provenance; `into_response` inserts effective provenance as a + historical-boundary extension without upgrading original ineligible/unknown evidence. +- The root workspace and `edgezero-core` manifests add a direct pinned `url` dependency. + URI construction canonicalizes once through `url::Url`, stores the resulting `http::Uri`, + and adapters transmit that exact serialization rather than reparsing it. +- `src/body.rs` changes streamed chunk errors to `EdgeError`, adds + `From`, and implements pre-append checked accounting. + `Body::from_stream` is the typed `EdgeError` constructor used by adapters, + decoders, and deadline wrappers; `Body::from_external_stream` is the explicit + arbitrary-error boundary and maps every source error to `internal`. +- `src/compression.rs` keeps one shared gzip/deflate/br decoder implementation. An exact typed + carrier crosses the `io::Error`-based decoder boundary and restores the original + `EdgeError`. Extend the helpers for concatenated gzip members, strict Brotli/trailing + data handling, and native-EOF validation without losing buffered read-ahead or late + source errors. The absolute deadline/cancellation wrapper sits **outside decoded + output and completion validation** and checks every yield plus terminal EOF/error. +- `src/error.rs` adds `BadGateway { message, reason }`, + `GatewayTimeout { message, cause }`, `ResponseTooLarge { message, reason }`, + `BadGatewayReason`, `BudgetSource`, and `ResponseLimitReason`; update every exhaustive + match and preserve the current JSON envelope. Phase 1a is specified by + `docs/superpowers/plans/2026-07-10-outbound-http-phase1a-error-time.md`. +- `src/time.rs` adds `Deadline` and the three budget constants with `web-time`. + Phase 1b adds `DispatchBudget` and `dispatch_budget` here as the independently testable + value/arithmetic layer; they consume `OutboundRequest::budget_inputs()` without owning + request construction or platform behavior. +- `src/app.rs` adds the three-state baked-manifest accessor to `Hooks`; update both + handwritten in-core `Hooks` implementations and the macro-generated implementation. +- `src/context.rs` performs only the outbound handle rename + `proxy_handle()` -> `http_client()`. Inbound request/body state is unchanged by + this specification; any local test fixture affected by the global `Body::from_stream` + signature change switches to the appropriate explicit constructor without changing + behavior. +- `src/manifest.rs` adds the outbound matrix capabilities, + `ManifestCapabilities`, `ManifestOutboundCapability`, host validation, misplaced + nested-`capabilities` rejection, and the three-state baked-manifest contract. + Runtime and baked parse paths run the same validation/finalization logic. + +**`crates/edgezero-macros`** + +- `src/app.rs` bakes the validated manifest JSON and emits the per-app + `manifest_json()` / `manifest()` accessors. A per-implementation `OnceLock` + prevents cross-app cache sharing. Its raw TOML parse path runs the shared + depth-independent `reject_misplaced_capabilities` scan before typed deserialization; + compile-fail coverage rejects misplaced `capabilities` blocks at multiple depths. + +**`crates/edgezero-adapter`** + +- `Cargo.toml` adds `edgezero-core = { workspace = true }`. The registry trait's + public `capability(Capability) -> CapabilitySupport` signature uses types owned and + re-exported by core, so the adapter crate can no longer remain dependency-free from + `edgezero-core`. This creates no cycle: core depends on macros, while neither core nor + macros depends on `edgezero-adapter`. +- `src/registry.rs` adds the defaulted `Adapter::capability()` method. In-tree + overrides return all outbound matrix cells and use a final + `_ => Unsupported` arm for the non-exhaustive enum. Update the trait rustdoc that + currently promises the crate remains dependency-free from core. No store/provision + API changes belong to this spec. + +**`crates/edgezero-adapter-{axum,cloudflare,fastly,spin}`** + +- Each adapter imports EdgeZero-owned `MonotonicClock`/`MonotonicInstant` timing types from + core and adds `test-utils = []` to `[features]`. Production outbound code does not take + direct process-global snapshots. An adapter client's `Default` implementation may select + the process source, but every response conversion must pass that client's retained clock + explicitly into `OutboundResponse::new`. Keep the adapter test feature independent of + runtime/CLI features and preserve current adapter-client defaults (§5.5). +- `tests/contract.rs` gains the native adapter-driver cases and SDK-specific test modules + described in §5.5. `.github/workflows/test.yml` explicitly executes the native commands + and enables `test-utils` in the existing WASM contract matrix in the same adapter phase. +- Rename each outbound provider module/client from `proxy` / `*ProxyClient` to + `outbound` / `*OutboundClient`; implement both response modes, request preflight, + method-aware `OutboundResponse` construction, request and response normalization, + independent encoded/decoded/final-buffer caps, typed decoder errors, and the exact outbound-cell + capability tuple. Before the native request consumes `OutboundRequestParts`, retain the + originating method, response mode, and every response-resource setting in the owned + response-conversion state; no adapter may reconstruct or default those values after send. +- Adapter outbound-response builders retain request method, status, headers, and body. + They retain pre-filter Content-Type metadata separately (always `Unknown` on Cloudflare), + use `ResponseHeadNormalization::body_disposition`, and pass explicit provenance as the + sixth `OutboundResponse::new` argument on every path. They normalize raw upstream headers + and settle bodyless responses before content + decoding/capping and `OutboundResponse` construction, then reapply normalization + idempotently before final platform conversion. + Every response-egress coordinator preserves a forwarded `Body::Stream` lazily. Axum and + Cloudflare are Native for that outbound capability; Fastly and Spin remain BestEffort pending + their documented target-runtime evidence. +- Do not change inbound platform-request buffering or `RequestContext` construction; + those are owned by the inbound-body design. The one allowed inbound-side edit is the + compile-required, behavior-preserving constructor migration from generic + `Body::from_stream` to `Body::from_external_stream` (or an explicit `EdgeError` map + followed by typed `from_stream`) at existing platform-body conversion sites. + +Adapter-specific work: + +- **Axum:** use the remaining `DispatchBudget` for reqwest's whole-operation timeout; + race every streamed request-source pull and apply the absolute post-ready deadline + check before accepting chunk/EOF/error; disable reqwest auto gzip/deflate/br; use the shared + decoder and normalization. Keep header fidelity Native. +- **Cloudflare:** add the private `fetch_raw_with_signal` bridge using worker's existing + JS/Web API re-exports. Set `encodeResponseBody: "manual"` and the guard's signal on the + final fetch options so the shared decoder receives raw bytes. Use an owning abort + guard plus `worker::Delay` for send/body cancellation on every early exit; use the same + pre-pull/post-ready absolute checks and bounded host-event yield quotas for request + sources, raw decoder input, decoded output, and terminal decisions (§4.2); stream + response output lazily; and classify header fidelity + BestEffort because workerd exposes normalized strings rather than raw malformed field + lines. Set `EncodeBody::Manual` when forwarding already encoded passthrough bytes so + Workers does not transform the payload. Replace the crate-local stale Preview-1 Cargo + config with `wasm32-unknown-unknown` plus `wasm-bindgen-test-runner`, and execute one + crate-local nonzero WASM gate without target/runner overrides in CI. +- **Fastly:** derive stable versioned configuration names using the B31 checkpoint in §1; + configure coupled host timers from the original timeout/duration anchor, not the later remainder; retain the + documented service-entitlement, cold-registration, upload-write, pending-handle selection, and + selected-body-drain + BestEffort gaps. `outbound-http` remains BestEffort until a reviewed deployment + prerequisite can prove dynamic-backend enablement for the selected service. + Native tests cover `SendFailure`; SDK-gated tests construct every known public + `SendErrorCause` and check the boundary mapping as well as composed status/kind and + timeout provenance. Only `SendError`'s private wrapper and hypothetical future enum + variants are not directly constructed. `PlatformInternal` is pinned separately from `LocalInvariant` even though both + currently map to 500, so a future policy change cannot silently conflate their causes. + Add an independently locked standalone deployed-probe crate under the adapter's test + fixtures; build its fixed package before protected-workflow secrets are exposed, and make + the live driver reject an absent or alternate package rather than compiling during deploy. +- **Spin:** use the hand-built WASI HTTP request for buffered and streamed uploads. + Implement the biased `run_exchange` state machine and cooperative per-chunk yield; + retain or consume `request_done` at the exact state boundaries in §4.4; explicitly + resolve request trailers on clean EOF/reader-gone; use the response `consume_body` + caller-result protocol; own every future/reader/writer until its terminal branch; and + classify all three `ErrorCode` sites: `client::send`, `request_done`, and the response + trailers/completion future. Component-stream termination and caller-result + `FutureWriteError` follow their separately specified policies rather than entering that + classifier. + Provider-reported caller-controlled request policy/size variants map to 400 (EdgeZero's + own upload-cap counter instead returns typed 413), core/adapter invariants to + 500, provider timeouts before absolute expiry to unattributed 504, errors observed at or + after absolute expiry to attributed 504, and upstream failures to 502. The monotonic race + returns a guest-visible timeout, while deadline and upload cancellation remain + BestEffort until host-observed tests establish a finite bound. + Replace the adapter-local buffered `src/decompress.rs` path with the shared core + streaming decoder; remove direct production `brotli` / `flate2` dependencies if no + remaining Spin-only use exists (retain only dependencies still required by tests). + +**`crates/edgezero-cli`** + +- The `run_build`, `run_serve`, and `run_deploy` command entry points capture the invocation + directory, resolve one target/contract pair, and pass the resulting `ResolvedRuntime` to + `src/adapter.rs::execute_runtime` or `execute_capture_runtime`. A shared private action gate runs exactly + once at the start of both outbound-scoped dispatch functions and gates `Build` / `Serve` / + `Deploy` / `DeployStaging` before either function's shell-command branch or registry + lookup. It executes the pinned target without rediscovery or reparsing. Auth, + `EmitVersion`, `Healthcheck`, and `Rollback` keep the existing dispatcher and resolution + behavior; they do not construct `ResolvedRuntime`. +- `has_manifest_command` and pre-dispatch adapter-manifest selection are folded into the + action-specific target resolver. Callers inspect the borrowed `ResolvedAdapterTarget` + when deciding which built-in-only arguments to add; they do not repeat shell-command or + platform-manifest discovery before handing the owned pair to the dispatcher. +- `src/manifest_source.rs` (or the existing manifest-loading module) adds the exact shell and + registered `ResolvedAdapterTarget` variants, `ResolvedManifest { loader, path }`, and + `ResolvedRuntime { action, adapter, contract, target }`. The registry crate adds the exact + private-field `AdapterExecutionTarget { app_root, component, platform_manifest }` handoff + plus `Adapter::execute_target`; every in-tree registered adapter uses it without cwd + discovery, and the default fails instead of delegating to `execute`. The runtime pins the canonical adapter + name and resolved action; outbound-scoped dispatcher signatures accept no + duplicate identity arguments. Explicit `EDGEZERO_MANIFEST` is authoritative + relative to the captured invocation directory. Default discovery applies the existing + ancestor/workspace-descendant target domain to paired app candidates; malformed, + ambiguous, mismatched, or containment-invalid candidates fail closed. Genuine + target-associated contract absence alone yields `contract: None`. Deploy's version + fallback remains on the existing operational dispatcher and is not part of this resolver. +- `src/demo_server.rs::run_demo` gates Axum against the baked manifest before startup. +- The Spin build/serve/deploy pre-dispatch path validates only the selected component's + canonical `allowed_outbound_hosts` before shell overrides can bypass it. Drift is + reported with the manifest/component/expected list and repaired manually; generation + initializes new projects but does not rewrite existing user-owned `spin.toml` files. +- Provision and config command implementations are unchanged by this outbound spec. + +**Templates, examples, and docs** + +- Rename generated proxy APIs in Rust and Handlebars sources. The portable root scaffold + and `examples/app-demo` declare `optional = ["outbound-http"]` so their shared manifest + remains usable with Fastly's BestEffort service prerequisite; a single-target app can + promote it to `required` when its selected adapter satisfies the matrix. Spin's generated + platform manifest renders the canonical outbound host list. +- Update public proxying, handler, architecture, streaming, and adapter docs. Add + `docs/guide/capabilities.md` with all outbound capabilities and sidebar + navigation. +- Build one generated project and the excluded `examples/app-demo` workspace so + template/example drift cannot hide behind the root workspace build. The generated + project gate explicitly runs `cargo test -p scaffold-probe-core --lib`; a workspace + build alone does not execute those core fixture tests. + +**Tests and CI** + +- Core colocated tests cover builders, URI validation, normalization, bodyless rules, + typed versus external body-stream constructors, budget selection/provenance, bounded + drains, typed decoder errors, decoded-output deadlines, and `send_all_until` + index/partial-failure semantics. +- Each adapter has no-network contract tests for request conversion, response + conversion, error classification, capability metadata, and platform-specific + timeout mechanics. +- Axum loopback tests prove native wire behavior. Cloudflare's future deployed runtime + cancellation fixture is the promotion gate for its current BestEffort deadline cells. Spin host-observed + tests characterize cancellation and are the criterion for a later BestEffort -> + Native upgrade; they are not a prerequisite for the current BestEffort claim. + Fastly local runtime tests remain conditional on a supported harness, but the protected + enabled/disabled deployed characterization is required before its BestEffort row is + published. +- Required local gates are the repository `CLAUDE.md` commands, all adapter WASM + target checks, the generated-project compile plus explicit nonzero core-test execution, + and the app-demo build. Phase 7 adds deterministic legacy-API and published-capability + document checks; the latter compares all outbound rows/support values and requires exactly + one VitePress sidebar link. Documentation-only edits use Markdown/link/diff verification + rather than rebuilding Rust. + +## 8. Open questions / risks + +1. **`DEFAULT_MAX_RESPONSE_BYTES` = 1 MiB.** Trivially overridable per request via + `max_response_bytes`. Confirm the default suits expected target responses. +2. **Tier 3 CI runtimes.** Viceroy / `workerd` / `spin` jobs add CI cost and + maintenance. The design degrades safely (Tier 1 + Tier 2 always run); the risk is + schedule, not correctness. +3. **Cloudflare cancellation — IMPLEMENTED; DEPLOYED EVIDENCE OPEN.** A timed-out subrequest is cancelled via + the guard's `worker::AbortController`, whose signal is included in the final options + passed by `fetch_raw_with_signal` (§4.2). Dropping the underlying fetch future alone + leaves the subrequest running; dropping the guarded send future aborts through the + guard's `Drop`. The required Tier 3 fixture is not present in this PR, so the deadline + capabilities remain `BestEffort`. Promotion to `Native` requires a deployed probe that keeps + the origin active after timeout/cap/decode/drop triggers and observes cancellation with the + pinned compatibility settings; an origin that already closed before a transport/completion + error is not valid evidence. +4. **Fastly active response-drain overshoot.** Once an individual warm-path slot is + actively draining its response, that read-phase overshoot is bounded by one + between-bytes-timeout interval (§3.3.4). This does not bound cold backend registration, + request-body writes, or time spent behind an earlier selected response-body drain; those + gaps are owned by footnotes 1/2/4 and risks 7/8. If a stricter active-drain guarantee is + ever required, the adapter would need to cap total body-read attempts — out of scope here. +5. **Naming.** `OutboundHttpClient` (trait) vs. `HttpClient` (handle) are close. They + never co-occur in app code — handlers see only `HttpClient` — so the overlap is + low-risk, but a rename of the handle is cheap if preferred. +6. **Axum transport-observed completion follow-up.** The connection-local Hyper body now + preserves a non-`Send` `Body::Stream` lazily without a channel or whole-body collection. + Frame acceptance still does not prove socket or client acceptance, and one expired response + closes its HTTP/1 connection, affecting unsettled pipelined responses. Promotion from + `BestEffort` requires a stronger attributable transport boundary or deployed evidence; the + current implementation reports the limitation explicitly. +7. **Fastly streamed-upload write-phase has no SDK-configurable bound.** + Fastly's `between_bytes_timeout` is documented as receive-side only — it + bounds the gap between bytes received from origin, not the host-side write + of guest-supplied bytes to origin (Fastly Backend API docs; round 50). No + published Fastly backend-timeout field bounds the guest-to-origin write + direction. Streamed-upload write-phase is therefore `BestEffort` on + Fastly (alongside the source-stream-yield `BestEffort`); the cooperative + `budget.deadline.is_expired()` check **between** chunks is the only + adapter-side bound. Apps that need real-time enforcement against a slow + origin on the write path must **target a different adapter**. (Passing a buffered + `Body::Once` does *not* fix this: it removes the source-pull stall but the host still + writes those bytes in the untimed `connect`→`first_byte` window. The write-side gap is + a property of Fastly's timeout model, not of `StreamingBody`.) If a future + Fastly platform release adds a documented guest-write timeout, it would close the + write-side gap only; the capability would remain BestEffort until source-pull + preemption also has a documented bound. Track Fastly host docs. +8. **Fastly buffered-body-drain serialization in batches.** `select_handles` removes input-order + header blocking, but EdgeZero still drains each selected buffered body before selecting the + next completion. Total guest work therefore includes the sum of selected body-drain times, + while Axum/Cloudflare/Spin poll complete exchanges concurrently (§3.3.4). + For small JSON bodies the response-drain term alone is usually negligible; cold backend + registration and unresolved request writes remain separate gaps. For multi-MiB responses + Fastly's serialized drain is suboptimal. **There is no current EdgeZero mitigation** — and + Streamed mode is not the workaround (it is rejected by batch preflight + per §3.1.1, and even via single `send` Fastly has no concurrent + chunk-consumption primitive). Apps that need concurrent large-body fan-out on + Fastly should (a) target a different adapter for that workload, (b) restructure + the topology so parallel large-body drains aren't required, or (c) wait for the + interleaved-drain follow-up. The follow-up — interleaved chunk reads across + selected Fastly `Response` bodies, coordinated from one guest driver — is + non-trivial without an async reactor and is **deferred**. The + `outbound-batch-slot-isolation` capability (§3.5.1 footnote 4) lets apps declare the + requirement explicitly and get a hard build failure on Fastly until this lands. +9. **Fastly independent Connect/idle policy.** The B31 aggregate checkpoint removes the + rigid phase split, but it does not implement independently configured Connect/idle limits + or prove host interruption. Those settings need complete identity/mode integration and + separately qualified provider behavior under the adoption plan; do not restore a ratio knob + or infer a capability promotion from coupled timers. +10. **Spin target documentation drift.** Implementation verification uses + `wasm32-wasip2` for Spin SDK 7. Any remaining comment that associates Spin with + `wasm32-wasip1` should be corrected when the implementation touches that file, but + this documentation-only cleanup is not a prerequisite for the outbound design. +11. **Per-batch transient-memory cap against adversarial chunking — PARTIALLY + RESOLVED.** §3.4.1's + `current_chunk.len()` term is source-controlled — an upstream peer that + yields one large `Bytes` produces a transient resident footprint equal to + that chunk size plus the persistent buffer cap. The design now includes opt-in + `OutboundRequest::max_chunk_bytes(NonZeroU64)` and a lazy consumer-side rechunker + (§3.4.5), with no cost when unset. This bounds app-visible item size and preserves + ordering, errors, deadlines, and cancellation. It does **not** bound transport-side + source allocation or process RSS: splitting `Bytes` can retain the original backing + allocation, and a copied slice may coexist with it. A true transport allocation cap + would require an adapter/provider boundary that refuses an oversized frame before + materialization; that remains unavailable on some targets and must not be inferred + from `max_chunk_bytes`. +12. **Fastly send-owning response entrypoint — RESOLVED.** Generated and demo applications now + use an undecorated no-request entrypoint. EdgeZero initializes the ABI, receives the request, + commits with `stream_to_client`, and owns write/close/abandon. No response-returning scaffold or + whole-body compatibility path remains. Synchronous source polls and hostcalls still prevent a + Native capability claim; footnote 6 records that provider limitation. +13. **Spin streamable response surface — RESOLVED.** The old full-response alias and converter are + removed. `run_app` returns `SpinResponse`, whose spawned coordinator owns the raw WASI response, + body, and result writers and preserves lazy production. The generated project and demo use that + hard-cut signature. The capability remains `BestEffort` only because finite host teardown and + end-client completion are unproved, not because EdgeZero buffers the response. Spin's outbound + request deadline remains independently `BestEffort` per footnote 8. +14. **`outbound-deadlines-exact` capability — not needed.** No adapter reports + `BoundedCooperative` for `outbound-deadlines`; a plain required declaration is currently + accepted only on Axum and hard-fails on Cloudflare, Fastly, and Spin. The support ladder + already expresses the required distinction. +15. **Spin deadline promotion criterion.** The hand-built WASI protocol prevents an + unowned detached pump and the monotonic timer can select a guest-visible 504, but + Component Model cancellation and default `FutureWriter` completion remain + cooperative. A Tier 2 test that observes dropped guest handles is insufficient for + Native. Promotion requires repeatable live-runtime evidence of a documented finite + teardown bound for stalled request upload, response body, and completion-future + paths, followed by an explicit matrix and footnote update. diff --git a/docs/superpowers/specs/2026-06-16-blob-app-config.md b/docs/superpowers/specs/2026-06-16-blob-app-config.md index 0d03c408d..a307e59d9 100644 --- a/docs/superpowers/specs/2026-06-16-blob-app-config.md +++ b/docs/superpowers/specs/2026-06-16-blob-app-config.md @@ -1,12 +1,18 @@ # Blob App Config — Design Spec **Date:** 2026-06-16 -**Status:** v1 — Plan-ready (twenty-three reviewer passes complete; reviewer cleared for plan authoring at round 24) +**Status:** v1 — Plan-ready; typed extraction and bounded-read amendment incorporated 2026-09-08 **Author:** Aram Grigoryan **Related branches:** `feature/extensible-cli` (current baseline) ## v1 changelog +**Cross-contract amendment (2026-09-08):** typed app-config reads now terminate in the +inspectable `StoreExtractionReason` contract from §6.3 and use the one-deadline bounded APIs +from §6.3.2. This amendment supersedes historical changelog entries below that describe +direct `ConfigOutOfDate`/`ServiceUnavailable` variants or unbounded `get`/`require_str` +inside `AppConfig`; those entries remain only as the review history of earlier drafts. + Initial draft after twenty-four review rounds — one author self-review, twenty-three reviewer passes against the current branch's code. Round 24 cleared the spec for plan authoring; @@ -126,12 +132,12 @@ framework-resolved:** distinguish "deploy is incomplete" from "backend is flaky". - **§9.1 Axum reader uses `ConfigStore::get` returning - `Option`.** Previous draft made the extractor parse + `Option`.** Previous draft made the extractor parse the Axum file-map shape directly, bypassing the provider-neutral trait contract. Fixed: the file holds per-key envelope JSON STRINGS, the `AxumConfigStore::get` impl returns the matching string, and the extractor sees - the same `Option` every adapter exposes. + the same `Option` every adapter exposes. - **§3.3.2 secret existence claim dropped.** The earlier draft claimed ` config validate --strict` could verify the named secret exists; the typed validator @@ -392,7 +398,7 @@ ValidationErrorsKind>`. Rewrote the sketch to call example showed envelope OBJECTS as map values; the normative text said each value is an escaped JSON STRING. Picked the string form (matches - `ConfigStore::get -> Option`'s contract across + `ConfigStore::get -> Option`'s contract across all four adapters) and rewrote the §9.1 example to show escaped JSON strings only. The object form was removed. @@ -741,19 +747,18 @@ runtime accuracy + parser fidelity:** to detect any nested reference. §12.17 extended to cover all six generic-wrap shapes plus the multi-line derive case plus the malformed-input case (exit 2). -- **§6.3.1 `EdgeError::config_out_of_date` split into - two constructors.** Round 10 had two contradictory +- **§6.3.1 typed deserialization gained a reason-bearing + constructor.** Round 10 had two contradictory signatures: secret walk called `config_out_of_date(msg, field_path)` (round 10 sketch); §6.3.1 declared the constructor takes a - `serde_path_to_error::Error`. Aligned: two + `serde_path_to_error::Error`. The intermediate draft aligned this with two constructors, `config_out_of_date(message, field_path)` for explicit-pair callers (secret walk + validator - path) and `config_out_of_date_from_serde(err)` for - the deserialise path. Extractor sketch updated to - use the serde constructor; secret-walk + validator - paths use the explicit-pair form. + path) and a serde-specific helper for the deserialise path. The final hard-cut API replaces + the latter with `store_deserialization_from_serde(err)` so typed store extraction always + exposes `StoreExtractionReason::Deserialization`; no compatibility alias remains. - **§3.2.2 + Q10 — `--exit-code` does not mask errors.** Round 10 §12.11 test asserted that without `--exit-code`, a remote-read network @@ -1170,8 +1175,7 @@ validate --help`). populated. Step 5 now wraps `Value::into_deserializer()` with `serde_path_to_error::deserialize` and maps the - error via `config_out_of_date_from_serde` per - the round-11 two-constructor split. Without + error via `store_deserialization_from_serde`. Without this, §12.6's `field_path` assertion would fail. - **Q6 + §12.10 Fastly cap stated as ONE number (64 KiB).** Q6 earlier called it ~8 KiB @@ -2690,30 +2694,58 @@ async fn extract(req: &RequestContext) -> Result where C: DeserializeOwned + AppConfigMeta + Validate + Send + 'static, { - // 1. Fetch the envelope JSON string from the adapter via ConfigStore::get. - // Missing blob maps to ConfigOutOfDate (Q3 (d) per round-18 M-2) — - // re-running ` config push` resolves the case, which is - // exactly what ConfigOutOfDate means. - let raw = config_store.get(&resolved_key).await? - .ok_or_else(|| EdgeError::config_out_of_date( - format!("missing typed app-config blob at key `{resolved_key}` — run ` config push` for this deploy"), - String::new(), - ))?; - let envelope: BlobEnvelope = serde_json::from_str(&raw)?; - envelope.verify_sha()?; + // 1. Fetch under the one extraction-wide byte/time budget (§6.3.2). + let mut budget = ConfigExtractionBudget::start( + req.config_extraction_limits(), + req.monotonic_clock(), + )?; + let read = config_store + .get_bounded( + &resolved_key, + budget.clock(), + budget.deadline(), + budget.remaining_backend_bytes(), + budget.max_blob_bytes(), + ) + .await + .map_err(map_config_store_error)?; + budget.charge_backend(read.backend_bytes)?; + let raw = read.value.ok_or_else(|| EdgeError::store_extraction( + StoreExtractionReason::MissingBlob, + format!("missing typed app-config blob at key `{resolved_key}` — run ` config push` for this deploy"), + None, + ))?; + budget.charge_value(raw.len())?; + let envelope: BlobEnvelope = serde_json::from_str(&raw).map_err(|_| { + EdgeError::store_extraction( + StoreExtractionReason::MalformedEnvelope, + "typed app-config envelope is invalid", + None, + ) + })?; + envelope.verify_sha().map_err(|_| EdgeError::store_extraction( + StoreExtractionReason::IntegrityMismatch, + "typed app-config integrity check failed", + None, + ))?; let mut data: serde_json::Value = envelope.into_data(); // 2. Walk SECRET_FIELDS. For each KeyInDefault / KeyInNamedStore // entry, look up the named secret store, fetch the value, swap // it into `data[field.name]`. StoreRef entries are untouched. let data_obj = data.as_object_mut() - .ok_or_else(|| EdgeError::internal("blob `data` is not a JSON object"))?; + .ok_or_else(|| EdgeError::store_extraction( + StoreExtractionReason::Deserialization, + "blob `data` is not a JSON object", + None, + ))?; for field in C::SECRET_FIELDS { let key_name = data_obj.get(field.name) .and_then(|v| v.as_str()) - .ok_or_else(|| EdgeError::config_out_of_date( + .ok_or_else(|| EdgeError::store_extraction( + StoreExtractionReason::Deserialization, format!("missing or non-string value at `{}`", field.name), - field.name.to_owned(), + Some(field.name.to_owned()), ))? .to_owned(); // For KeyInDefault, resolve to a BOUND default store via @@ -2729,14 +2761,15 @@ where let (bound, resolved_store_id) = match field.kind { SecretKind::KeyInDefault => { let bound = req.secret_store_default().ok_or_else(|| { - EdgeError::config_out_of_date( + EdgeError::store_extraction( + StoreExtractionReason::MissingRegistry, format!( "secret field `{}` has kind KeyInDefault but \ no default secret store is registered (check \ [stores.secrets].default or declare a single id)", field.name, ), - field.name.to_owned(), + Some(field.name.to_owned()), ) })?; let id = bound.store_name().to_owned(); @@ -2746,22 +2779,46 @@ where SecretKind::KeyInNamedStore { store_ref_field } => { let store_id_str = data_obj.get(store_ref_field) .and_then(|v| v.as_str()) - .ok_or_else(|| EdgeError::config_out_of_date( + .ok_or_else(|| EdgeError::store_extraction( + StoreExtractionReason::Deserialization, format!("missing store_ref `{store_ref_field}` for secret field `{}`", field.name), - field.name.to_owned(), + Some(field.name.to_owned()), ))? .to_owned(); let bound = req.secret_store(&store_id_str).ok_or_else(|| { - EdgeError::config_out_of_date( - format!("blob declared store_ref `{store_id_str}` but [stores.secrets] has no such id"), - field.name.to_owned(), + EdgeError::store_extraction( + StoreExtractionReason::UnknownStore, + format!("secret field `{}` references an unregistered store (identifier redacted)", field.name), + Some(field.name.to_owned()), ) })?; (bound, store_id_str) } }; - let secret = bound.require_str(&key_name).await + let read = bound + .get_bytes_bounded( + &key_name, + budget.clock(), + budget.deadline(), + budget.remaining_backend_bytes(), + budget.max_secret_bytes(), + ) + .await .map_err(|err| map_secret_error(err, field.name, &resolved_store_id, &key_name))?; + budget.charge_backend(read.backend_bytes)?; + let secret_bytes = read.value.ok_or_else(|| EdgeError::store_extraction( + StoreExtractionReason::MissingSecret, + format!("the secret referenced by `{}` was not found in its store (identifier redacted)", field.name), + Some(field.name.to_owned()), + ))?; + budget.charge_value(secret_bytes.len())?; + let secret = String::from_utf8(secret_bytes.to_vec()).map_err(|_| { + EdgeError::store_extraction( + StoreExtractionReason::InvalidSecretValue, + format!("the secret referenced by `{}` is not valid UTF-8", field.name), + Some(field.name.to_owned()), + ) + })?; data_obj.insert(field.name.to_owned(), serde_json::Value::String(secret)); } @@ -2769,7 +2826,7 @@ where // the field-path) + validate. use serde::de::IntoDeserializer as _; let cfg: C = serde_path_to_error::deserialize(data.into_deserializer()) - .map_err(EdgeError::config_out_of_date_from_serde)?; + .map_err(EdgeError::store_deserialization_from_serde)?; cfg.validate().map_err(|err| { // The secret walk above replaced `#[secret]` fields with their // RESOLVED values, and `validator`'s params echo the rejected value, @@ -2781,7 +2838,11 @@ where } else { format!("app config failed validation for field `{field}`") }; - EdgeError::config_out_of_date(message, field) + EdgeError::store_extraction( + StoreExtractionReason::Validation, + message, + (!field.is_empty()).then_some(field), + ) })?; Ok(cfg) } @@ -2853,30 +2914,21 @@ RESOLVED value, populated by the extractor's walk. #### 3.3.6 What this means for failure modes -Current `SecretError` -(`crates/edgezero-core/src/secret_store.rs:113`) has four -variants: `NotFound`, `Validation`, `Internal`, `Unavailable`. -`require_str` (line 269) maps invalid-UTF-8 bytes from the -store to `SecretError::Internal`. The extractor wraps each -into an `EdgeError` variant based on what action the operator -can take. The mapping below is comprehensive — every -`SecretError` variant has a documented landing. - -| Extractor failure | `SecretError` | `EdgeError` | Why | -| -------------------------------------------------- | -------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Secret-store ID unknown (no `[stores.secrets].id`) | none — caught before secret call | `ConfigOutOfDate` | Manifest declares the wrong store name OR the manifest wasn't redeployed. Re-push fixes it. | -| Secret key not found in the named store | `SecretError::NotFound` | `ConfigOutOfDate` | Operator forgot to provision the secret value. Re-run secret provisioning. | -| Store rejected the key shape (length, charset) | `SecretError::Validation { .. }` | `ConfigOutOfDate` | The blob's key name is invalid for the named store (e.g. Fastly Secret Store keys are constrained). Operator either renames the secret-store key or fixes the `.toml` value. | -| Secret value is bytes, not UTF-8 | `SecretError::Internal` (from `require_str`) | `Internal` | The store CONTAINS the key but the bytes aren't a `String`. Data quality at rest; not deploy-related. Operator audits the secret-store entry directly. | -| Secret store unreachable (transient network) | `SecretError::Unavailable` | `ServiceUnavailable` | A flaky backend is not an out-of-date config — retry is the right response. Surfaces as HTTP 503; per §6.3.1 the `Retry-After: 60` header is NOT set on `ServiceUnavailable` in v1 (audit of existing producers showed too many non-retryable cases reuse the variant). | -| Any other `SecretError::Internal` | `SecretError::Internal` | `Internal` | Unexpected store-side failure (an adapter bug, a wire-format change). Pages oncall; not actionable by deploy. | - -The boundary between `ConfigOutOfDate` and `Internal` is: -**does re-running ` config push` (or its sibling -secret-provisioning step) fix the situation?** If yes → -`ConfigOutOfDate`. If no → `Internal`. The invalid-UTF-8 case -falls on the wrong side of that boundary (re-push doesn't fix -non-string bytes in the store), so it stays on `Internal`. +Bounded extraction extends `SecretError` with `DeadlineExceeded` and `ValueTooLarge`. +The extractor wraps every lower-level outcome in `EdgeError::StoreExtraction`; the stable +reason remains inspectable even when several reasons share one wire kind. + +| Extractor failure | Lower-level outcome | `StoreExtractionReason` | HTTP / kind | +| --- | --- | --- | --- | +| No default secret registry | caught before read | `MissingRegistry` | 500 / `internal` | +| Referenced named store is unknown | caught before read | `UnknownStore` | 500 / `internal` | +| Secret key not found | bounded read returns `value: None` | `MissingSecret` | 503 / `config_out_of_date` | +| Store rejects key shape | `SecretError::Validation(..)` | `InvalidKey` | 400 / `bad_request` | +| Secret bytes are not UTF-8 | caught after bounded byte read | `InvalidSecretValue` | 500 / `internal` | +| Secret store unavailable | `SecretError::Unavailable` | `SecretBackendUnavailable` | 503 / `service_unavailable` | +| Bounded read deadline expires | `SecretError::DeadlineExceeded` | `DeadlineExceeded` | 503 / `service_unavailable` | +| Per-secret or cumulative byte cap is exceeded | `SecretError::ValueTooLarge` or budget charge | `ValueTooLarge` | 500 / `internal` | +| Any other backend failure | `SecretError::Internal(..)` | `BackendFailure` | 500 / `internal` | The extractor's wrapper around the secret-store call materialises this mapping ONCE near the call site: @@ -2884,10 +2936,12 @@ materialises this mapping ONCE near the call site: ```rust // Matches the actual SecretError shape at // crates/edgezero-core/src/secret_store.rs:113: +// DeadlineExceeded // unit, added by §6.3.2 // Internal(#[from] anyhow::Error) // NotFound { name: String } // struct-like // Unavailable // unit // Validation(String) // tuple +// ValueTooLarge // unit, added by §6.3.2 // SECURITY: the stored key NAME, the store id, and the provider's // message/source are deliberately UNUSED. They are blob- or // provider-controlled strings that can reveal the secret or the @@ -2902,38 +2956,43 @@ fn map_secret_error( _key_name: &str, ) -> EdgeError { match err { - SecretError::NotFound { .. } => EdgeError::config_out_of_date( + SecretError::DeadlineExceeded => EdgeError::store_extraction( + StoreExtractionReason::DeadlineExceeded, + format!("secret resolution for `{field_name}` exceeded its deadline"), + Some(field_name.to_owned()), + ), + SecretError::Internal(_source) => EdgeError::store_extraction( + StoreExtractionReason::BackendFailure, + format!("secret resolution for `{field_name}` failed (details redacted)"), + Some(field_name.to_owned()), + ), + SecretError::NotFound { .. } => EdgeError::store_extraction( + StoreExtractionReason::MissingSecret, format!("the secret referenced by `{field_name}` was not found in its store (identifier redacted)"), - field_name.to_owned(), + Some(field_name.to_owned()), + ), + SecretError::Unavailable => EdgeError::store_extraction( + StoreExtractionReason::SecretBackendUnavailable, + format!("the secret store for `{field_name}` is unreachable"), + Some(field_name.to_owned()), ), - SecretError::Validation(_msg) => EdgeError::config_out_of_date( + SecretError::Validation(_msg) => EdgeError::store_extraction( + StoreExtractionReason::InvalidKey, format!("the secret referenced by `{field_name}` was rejected by its store (details redacted)"), - field_name.to_owned(), + Some(field_name.to_owned()), + ), + SecretError::ValueTooLarge => EdgeError::store_extraction( + StoreExtractionReason::ValueTooLarge, + format!("the secret referenced by `{field_name}` exceeds its configured byte limit"), + Some(field_name.to_owned()), ), - SecretError::Unavailable => EdgeError::service_unavailable(format!( - "the secret store for `{field_name}` is unreachable" - )), - SecretError::Internal(_source) => EdgeError::internal(anyhow::anyhow!( - "secret resolution for `{field_name}` failed (details redacted)" - )), } } ``` -**`Retry-After: 60` is NOT set on `ServiceUnavailable` in -v1.** An earlier draft extended the header from -`ConfigOutOfDate` to `ServiceUnavailable`; round-10 audit -of existing producers (KV size limits at -`crates/edgezero-core/src/key_value_store.rs:708`, missing -named KV store at -`examples/app-demo/.../handlers.rs:185`, missing default -secret store at `.../handlers.rs:285`) found that the -variant is reused for several non-retryable failure modes -where the header would mislead clients into a tight retry -loop. §6.3.1 documents the narrowed rule in detail: -header on `ConfigOutOfDate` ONLY; future v2 work may -split `ServiceUnavailable` so each producer site picks -the right variant. +`Retry-After: 60` is emitted only for the effective `config_out_of_date` kind. In this +table that is `MissingSecret`; `SecretBackendUnavailable` and `DeadlineExceeded` are +`service_unavailable` without a retry header. §6.3.1 centralizes that policy. #### 3.3.7 Sha-canonicalisation interaction @@ -3070,12 +3129,12 @@ extractor doesn't touch TOML on disk at all): | --------------------------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `config push` / `config diff` / generated CLI `config validate` | `.toml` on disk | `deserialize_app_config_with_options(path, app_name, opts)` → `validate_excluding_secrets(&cfg)` → §3.3.2 structural checks | | Bundled raw `edgezero config validate` | `.toml` on disk | `load_app_config_raw(path, app_name)` (TOML round-trip, no `C`, no validators) | -| Runtime extractor (`AppConfig`) | Envelope JSON STRING from `ConfigStore::get(key)` | envelope parse → SHA verify → secret walk (per §3.3.3) → `serde_path_to_error::Deserializer` over the JSON `data` field → `Validate::validate(&cfg)`. The runtime path does NOT load TOML and does NOT apply the env overlay; the env overlay is a CLI-side notion that the operator's push run resolved into the blob. | +| Runtime extractor (`AppConfig`) | Envelope JSON string from `ConfigStore::get_bounded(key, clock, deadline, caps)` | bounded read → envelope parse → SHA verify → bounded secret walk (per §3.3.3) → `serde_path_to_error::Deserializer` over the JSON `data` field → `Validate::validate(&cfg)`. The runtime path does not load TOML and does not apply the env overlay; the operator's push run resolves that CLI-side overlay into the blob. | The CLI paths all share a `build_and_validate` helper in `crates/edgezero-cli/src/config.rs` (sketch in §3.3.2). The runtime extractor's path is sketched in §3.3.3 and -uses the §6.3.1 `EdgeError::config_out_of_date_from_serde` +uses the §6.3.1 `EdgeError::store_deserialization_from_serde` constructor for serde failures (preserving the `field_path` from `serde_path_to_error`). @@ -3345,7 +3404,7 @@ the rule honest, the `#[derive(AppConfig)]` macro enforces: the canonicaliser's "sort keys by UTF-8 byte order of the field identifier" rule (the field identifier isn't the key any more) and complicates `serde_path_to_error` - paths in `EdgeError::ConfigOutOfDate`. Operators who + paths in `EdgeError::StoreExtraction`. Operators who need a flat shape define it explicitly. - **`#[serde(rename_all)]` is already rejected by the existing per-secret-field policy at @@ -3511,15 +3570,10 @@ version, .. }`. Unknown envelope versions error with a pointer at the migration guide. 2. Recompute `canonical_data_sha256(&data)`. 3. If it doesn't match the stored `sha256`, return - `ConfigStoreError::internal("blob sha mismatch: stored {hex} -!= computed {hex}")` (the existing `internal(...)` - constructor at `config_store.rs:180`; see §6.3 for the - reasoning behind keeping this on `Internal` instead of - adding a new variant) and DO NOT proceed. The runtime - gives up on this request rather than silently honouring a - tampered blob. The extractor maps - `ConfigStoreError::Internal` to `EdgeError::Internal` per - existing convention. + `EdgeError::StoreExtraction { reason: IntegrityMismatch, .. }` and DO NOT proceed. The + runtime gives up on this request rather than silently honouring a tampered blob. This is + an extractor-owned integrity decision, not a generic `ConfigStoreError::Internal`, so + callers retain the stable origin without parsing a diagnostic. 4. **Secret walk (per §3.3.3).** Iterate over `C::SECRET_FIELDS`. For each `KeyInDefault` or `KeyInNamedStore` field, look up `data[field.name]` in the @@ -3533,16 +3587,15 @@ version, .. }`. Unknown envelope versions error with a `serde_json::Value::into_deserializer()` (NOT `serde_json::from_value` directly — that path discards the field-path information that - `EdgeError::ConfigOutOfDate.field_path` requires per + `EdgeError::StoreExtraction.field_path` requires per §6.3.1). The wrapper accumulates the JSON path of any deserialise failure (e.g. `"feature.new_checkout"`); the extractor maps the resulting `serde_path_to_error::Error` to - `EdgeError::config_out_of_date_from_serde(err)` per - §6.3.1's two-constructor split, which populates + `EdgeError::store_deserialization_from_serde(err)` per + §6.3.1, which sets reason `Deserialization` and populates `field_path` from `err.path()`. Without this wrapper, a - schema mismatch surfaces as a generic - `EdgeError::ConfigOutOfDate { message, field_path: "" }` + schema mismatch surfaces as a generic untyped error without field context and §12.6's field_path assertion fails. 6. Run `Validate::validate(&cfg)` per §6.2.2 — full validation including secret-bearing fields, now that they hold @@ -3823,6 +3876,16 @@ fallback is centralised in `EnvConfig` so all four adapters share identical behaviour. +A declared config id remains present in the registry when its provider binding +cannot be opened. Cloudflare and Fastly retain a typed failing handle rather +than dropping the id; extraction reports `BackendUnavailable` or +`BackendFailure`, respectively, not `MissingRegistry` or `MissingBlob`. +Provider diagnostics stay in adapter logs, not the public error message. +Spin binds labels without opening them during dispatch. It opens the store +inside `get_bounded`, after ingress admission and within the extraction timer +race, so an unavailable or never-ready config open cannot block unrelated +routes before admission. This bounds guest return, not provider cancellation. + **Extractor consumption.** The extractor reads through the registry: @@ -3831,21 +3894,46 @@ async fn extract(req: &RequestContext, override_key: Option<&str>) -> Result< where C: DeserializeOwned + AppConfigMeta + Validate + Send + 'static, { + let mut budget = ConfigExtractionBudget::start( + req.config_extraction_limits(), + req.monotonic_clock(), + )?; let binding = req .config_store_default_binding() - .ok_or_else(|| EdgeError::internal("no default config store registered"))?; + .ok_or_else(|| EdgeError::store_extraction( + StoreExtractionReason::MissingRegistry, + "no default config store registered", + None, + ))?; let key = override_key.unwrap_or(&binding.default_key); - // Missing blob maps to ConfigOutOfDate per §6.3 / Q3 (d) — a re-run - // of ` config push` is the actionable response, not a 500. - let raw = binding.handle.get(key).await? // ConfigStore::get - .ok_or_else(|| EdgeError::config_out_of_date( + let read = binding + .handle + .get_bounded( + key, + budget.clock(), + budget.deadline(), + budget.remaining_backend_bytes(), + budget.max_blob_bytes(), + ) + .await + .map_err(map_config_store_error)?; + budget.charge_backend(read.backend_bytes)?; + let raw = read.value + .ok_or_else(|| EdgeError::store_extraction( + StoreExtractionReason::MissingBlob, format!("missing typed app-config blob at key `{key}` — run ` config push` for this deploy"), - String::new(), + None, ))?; - // ... envelope parse, sha verify, secret walk, deserialise + validate per §4.3 + budget.charge_value(raw.len())?; + // ... envelope parse, sha verify, secret walk with &mut budget, + // deserialise + validate per §4.3 } ``` +`ConfigExtractionBudget` is extractor-private state implementing §6.3.2. The secret walk +receives `&budget`; it passes the same absolute deadline to every bounded secret read and +charges each returned value before insertion. + `RequestContext` grows two helpers mirroring the existing `config_store_default()` / `config_store(id)`: @@ -4052,8 +4140,8 @@ validate --strict` runs at push time, so env-overlay drift Once the extractor deserialises `data` into `C`, it calls `Validate::validate(&cfg)`. Validation failures map to: -- `EdgeError::ConfigOutOfDate` — naming ONLY the field that - violated its constraint. Same surface as the +- `EdgeError::StoreExtraction` with reason `Validation`, naming ONLY the field that + violated its constraint. It has the same wire surface as the deserialise-failure case (§6.3): operator action is "re-push the typed config; the deployed `.toml` is out of bounds for the deployed code". @@ -4125,67 +4213,122 @@ key)`. ### 6.3 Errors -The extractor surfaces: +The typed classification is fixed before extractor implementation. Callers never parse +`message` to discover an extraction failure's origin: + +```rust +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum StoreExtractionReason { + BackendFailure, + BackendUnavailable, + DeadlineExceeded, + Deserialization, + IntegrityMismatch, + InvalidKey, + InvalidSecretValue, + MalformedEnvelope, + MissingBlob, + MissingRegistry, + MissingSecret, + SecretBackendUnavailable, + StructuralLimit, + UnknownStore, + UnsupportedVersion, + Validation, + ValueTooLarge, +} + +// One EdgeError variant delegates status/kind/header policy to `reason`. +StoreExtraction { + reason: StoreExtractionReason, + message: String, + field_path: Option, +} +``` -- `EdgeError::ServiceUnavailable` — store unreachable, network +`StoreExtractionReason` and the containing `EdgeError` variant are `#[non_exhaustive]`. +The reason survives every `AppConfig`/`from_store`/secret-walk mapping and is available +through `EdgeError::store_extraction_reason() -> Option` without a +downcast. It is Rust-side diagnostic metadata only: the response JSON does not serialize a +`reason` field. `field_path` is `Some` only when the failure is tied to a typed-config path; +an empty path is represented as `None` and omitted from JSON. + +The mapping is total and centralized in `EdgeError::{status_code, kind, response_headers}`: + +| Reason | HTTP / kind | `Retry-After` | `field_path` | +| --- | --- | --- | --- | +| `BackendUnavailable`, `DeadlineExceeded`, `SecretBackendUnavailable` | 503 / `service_unavailable` | absent | optional only for a secret field | +| `Deserialization`, `MissingBlob`, `MissingSecret`, `Validation` | 503 / `config_out_of_date` | `60` | optional for typed-data/validation/secret failures; absent for a missing root blob | +| `InvalidKey` | 400 / `bad_request` | absent | absent | +| `BackendFailure`, `IntegrityMismatch`, `InvalidSecretValue`, `MalformedEnvelope`, `MissingRegistry`, `StructuralLimit`, `UnknownStore`, `UnsupportedVersion`, `ValueTooLarge` | 500 / `internal` | absent | optional only when a typed field selected the store/value | + +`ValueTooLarge` covers any per-blob, per-secret, or cumulative extraction byte cap from +§6.3.2; `DeadlineExceeded` covers the one absolute extraction deadline. A source error that +arrives simultaneously with deadline expiry is classified as `DeadlineExceeded`. +`StructuralLimit` covers a config-envelope JSON node or depth cap reached before either +generic future-format or typed-envelope materialization. Invalid JSON without a breached +structure cap remains `MalformedEnvelope`. Neither category includes stored values in diagnostics. +Tests enumerate every known reason, assert status/kind/header/field-path policy, and prove +the JSON envelope omits the typed reason. + +The extractor surfaces the following wire outcomes through `EdgeError::StoreExtraction`: + +- `service_unavailable` — store unreachable, network errors. Maps to HTTP 503. -- `EdgeError::ConfigOutOfDate { message, field_path }` — +- `config_out_of_date` — typed-struct deserialise failure: the blob is present and the envelope parses, but `data` doesn't fit the runtime's `C` type. Almost always means "the code shipped before the matching ` config push`". See the contract below. -- `EdgeError::Internal` — sha mismatch (drift / corruption), +- `internal` — sha mismatch (drift / corruption), envelope parse failure (envelope `version` unrecognised or shape unexpected). Maps to HTTP 500. These are genuinely unexpected and should page the operator. -- **Key missing from the store** (i.e. - `ConfigStore::get(key)` returned `Ok(None)`) maps to - `EdgeError::ConfigOutOfDate` (Q3 (d) per round-18 - M-2, restated here for §6.3 hard-cutoff). HTTP 503 - with `Retry-After: 60`. Message: `missing typed -app-config blob at key \`\` — run \` - config push\` for this deploy`. Rationale: a missing -typed-app-config blob is operationally -indistinguishable from "the operator didn't run -`config push`yet" — which is exactly the`ConfigOutOfDate`class ("re-run config push fixes -it"). Mapping to`Internal`would page oncall on a -push-fixable condition; mapping to`NotFound`(404) -would imply the URL is wrong, which it isn't. A -future`MaybeAppConfig`→`Option`extractor -(Q3 (c)) could remap this for endpoints that want -explicit defaults; v1 ships with`ConfigOutOfDate` - and no opt-out. +- **Key missing from the store** (`get_bounded` returned `value: None`) becomes + `StoreExtractionReason::MissingBlob`, whose effective outcome is + `config_out_of_date`: HTTP 503 with `Retry-After: 60`. Message: + `missing typed app-config blob at key \`\` — run \` config push\` for + this deploy`. A missing typed-config blob is operationally indistinguishable from a + skipped config push. Mapping it to `internal` would page on-call for a push-fixable + condition; mapping it to `not_found` would imply the request URL is wrong. A future + `MaybeAppConfig -> Option` extractor could provide explicit defaults; v1 has no + opt-out. **Implementation note — `ConfigStoreError` to `EdgeError` -mapping.** `ConfigStoreError` (`config_store.rs:165`) has only -three variants today: `Internal`, `InvalidKey`, `Unavailable`. -The extractor maps: - -| ConfigStoreError | EdgeError | HTTP | Notes | -| ------------------------------------- | -------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `Unavailable` | `ServiceUnavailable` | 503 | Transient backend issue. | -| `Internal` (sha mismatch) | `Internal` | 500 | Drift or corruption — the stored sha doesn't match canonical recompute. | -| `Internal` (envelope parse failure) | `Internal` | 500 | Envelope `version` unrecognised or shape unexpected. | -| `InvalidKey` | `BadRequest` | 400 | Adapter rejected the key shape. | -| _missing key_ (`Ok(None)` from `get`) | `ConfigOutOfDate` | 503 | NOT a `ConfigStoreError` variant — caught at the extractor's `ok_or_else` after `ConfigStore::get` returns `Ok(None)`. Round-18 M-2 reversal: was `Internal` in earlier drafts; the new mapping matches Q3 (d) + §3.3.3's extractor sketch. | - -Plus the new `ConfigOutOfDate` variant `EdgeError` gains as part -of this work; no new variant on `ConfigStoreError` is needed. - -**Why `ConfigOutOfDate` is separate from `Internal`:** the -operator's response is different. `Internal` means "investigate -what's wrong with our store / blob"; `ConfigOutOfDate` means -"re-run ` config push` for the deployed code -revision". A single generic 500 conflates both and trains -operators to ignore the class of error that's most actionable. - -### 6.3.1 `ConfigOutOfDate` concrete contract - -The current `EdgeError` response shape carries `status` + -`message` only (`crates/edgezero-core/src/error.rs:159`). The -blob model needs more structure so dashboards can route -`ConfigOutOfDate` to a different oncall than generic 503s. -Two specific extensions: +mapping.** The current three variants are `Internal`, `InvalidKey`, and `Unavailable`; +§6.3.2 adds `DeadlineExceeded` and `ValueTooLarge` for bounded reads. The extractor maps: + +| ConfigStore outcome | Store extraction reason | HTTP | Notes | +| ------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Unavailable` | `BackendUnavailable` | 503 | Transient backend issue. | +| `DeadlineExceeded` | `DeadlineExceeded` | 503 | The shared absolute extraction deadline expired; expiry wins simultaneous readiness. | +| SHA mismatch | `IntegrityMismatch` | 500 | Drift or corruption — the stored sha doesn't match canonical recompute. | +| Envelope parse/shape failure | `MalformedEnvelope` | 500 | The stored value is not a syntactically valid supported envelope. | +| Envelope version/discriminator failure | `UnsupportedVersion` | 500 | The envelope `version` or `edgezero_kind` is not understood by this build; redeploy an updated build. | +| `InvalidKey` | `InvalidKey` | 400 | Adapter rejected the key shape. | +| `Internal` | `BackendFailure` | 500 | Unexpected backend/adapter failure whose lower layer cannot classify more narrowly. | +| `ValueTooLarge` | `ValueTooLarge` | 500 | Per-value or remaining backend-byte allowance was exceeded. | +| _missing key_ (`value: None` from `get_bounded`) | `MissingBlob` | 503 | NOT a `ConfigStoreError` variant — caught at the extractor's `ok_or_else` after the bounded read returns no value. Round-18 M-2 reversal: was `Internal` in earlier drafts; the new mapping matches Q3 (d) + §3.3.3's extractor sketch. | + +`ConfigStoreError` does not duplicate semantic envelope/schema/registry reasons. It exposes +only failures owned by the backend read boundary. The extractor converts each lower-level +store/secret result once at its ownership boundary and returns the typed +`EdgeError::StoreExtraction` end state. + +**Why the `config_out_of_date` kind is separate from `internal`:** the operator's response +is different. `internal` means "investigate the store/blob/runtime"; +`config_out_of_date` means "re-run ` config push` or provision the referenced +secret for the deployed revision." The typed reason preserves the more precise origin +inside each wire class. + +### 6.3.1 `config_out_of_date` concrete wire contract + +The `config_out_of_date` outcome applies both to the existing +`EdgeError::ConfigOutOfDate` variant and to `EdgeError::StoreExtraction` +when its reason maps to that kind in §6.3. The blob model needs enough +wire structure for dashboards to route this outcome differently from +generic 503s. Three specific extensions: **1. Response-body shape.** Today's `EdgeError::IntoResponse` at `crates/edgezero-core/src/error.rs:159` writes: @@ -4225,7 +4368,7 @@ Concretely: ONLY on `config_out_of_date` (and any future field-anchored variant). Other variants omit it. Clients that don't care can ignore. -- Status code: 503 for `ConfigOutOfDate` (not 500). The +- Status code: 503 for the `config_out_of_date` outcome (not 500). The semantic is "service can't honor this request because the deployed config + code are out of sync; retry after redeploy". @@ -4236,8 +4379,9 @@ to the response body — operators who parse the body shape on the client side need to update. Per §1's hard-cutoff stance, no compat shim. -**2. Send `Retry-After: 60`** on the new `ConfigOutOfDate` -variant ONLY. Earlier drafts extended the header to all +**2. Send `Retry-After: 60`** only when the effective kind is +`config_out_of_date`. This includes `ConfigOutOfDate` and the four +store-extraction reasons mapped to that kind in §6.3. Earlier drafts extended the header to all `ServiceUnavailable` responses; an audit of in-tree `ServiceUnavailable` producers shows that the variant is ALREADY used for several non-config-related conditions @@ -4255,7 +4399,8 @@ Adding `Retry-After: 60` to every `ServiceUnavailable` would lie to clients in three of four cases. The blob model takes the narrower stance: -- **`ConfigOutOfDate`** — header sent. The new variant is +- **`ConfigOutOfDate` or a `StoreExtraction` reason mapped to + `config_out_of_date`** — header sent. This outcome is shaped specifically for "deployed config + code are out of sync; redeploy converges in <60s", which is exactly what the header tells clients. @@ -4268,14 +4413,13 @@ model takes the narrower stance: each producer site picks the right variant; out of scope here. -Added via -`Response::headers_mut().insert("retry-after", -HeaderValue::from_static("60"))` inside the -`IntoResponse` impl branch for `ConfigOutOfDate` only. -Other variants (`ServiceUnavailable`, `Internal`, -`BadRequest`, etc.) do NOT set the header. +Added via the centralized error response-header policy rather than a +variant-only `matches!` check. Other outcomes (`service_unavailable`, +`internal`, `bad_request`, etc.) do NOT set the header. Application custom error rendering may +replace the response body but cannot remove this mandatory header; EdgeZero reapplies the +effective `config_out_of_date` policy after the callback returns. -**3. `field_path` for the variant's payload** comes from +**3. `field_path` for the error payload** comes from `serde_path_to_error::Track::path()` (or equivalent), wrapped around the blob's `data` field's deserialiser. Plain serde errors only report position-in-input, not field-path; the @@ -4284,8 +4428,8 @@ errors only report position-in-input, not field-path; the the deserialise itself. The dep is small (~500 LOC, no transitive deps) and locked-in for the variant. -> **Runtime redaction (security).** In the HTTP `ConfigOutOfDate` -> response the path's STRING segments are redacted to `` +> **Runtime redaction (security).** In an HTTP `config_out_of_date` response produced by +> `StoreExtractionReason::Deserialization`, the path's STRING segments are redacted to `` > while structure (dots and sequence indices) is kept — e.g. > `.`. A `serde_path_to_error` segment for a struct > field is indistinguishable from a MAP KEY, and a map key is stored @@ -4296,11 +4440,10 @@ transitive deps) and locked-in for the variant. > secret-walk/validator constructor still supplies an explicit static > path, since those are code-supplied field names, not stored data. -**Variant declaration (sketch).** Two constructors — -one for the serde-deserialise path (which has rich -field-path data from `serde_path_to_error`), one for -the secret-walk and validator paths (which already -have an explicit `(message, field_path)` pair): +**Variant declaration (sketch).** The fixed `ConfigOutOfDate` variant remains available +for non-store producers. Typed store extraction uses the reason-bearing variant from §6.3 +and two constructors: one for explicit path/reason data and one for the serde-deserialise +path: ```rust pub enum EdgeError { @@ -4309,22 +4452,23 @@ pub enum EdgeError { message: String, // e.g. "missing field `new_checkout`" field_path: String, // e.g. "feature" }, + StoreExtraction { + reason: StoreExtractionReason, + message: String, + field_path: Option, + }, } impl EdgeError { - /// Construct from an explicit (message, field_path) pair. - /// Used by the secret walk (§3.3.3) and the validator path - /// (§6.2.2). `field_path` SHOULD be a dotted path naming - /// the offending field (e.g. `"feature.new_checkout"`); - /// pass `String::new()` when no specific field is anchored - /// (the response simply omits the `field_path` key). - pub fn config_out_of_date( + pub fn store_extraction( + reason: StoreExtractionReason, message: impl Into, - field_path: impl Into, + field_path: Option, ) -> Self { - Self::ConfigOutOfDate { + Self::StoreExtraction { + reason, message: message.into(), - field_path: field_path.into(), + field_path, } } @@ -4342,7 +4486,7 @@ impl EdgeError { /// when the local TOML still matches what was deployed — it /// reads the local source, not the deployed blob, so a drift /// between them is not recoverable this way. - pub fn config_out_of_date_from_serde( + pub fn store_deserialization_from_serde( serde_err: serde_path_to_error::Error, ) -> Self { let category = match serde_err.inner().classify() { @@ -4351,9 +4495,10 @@ impl EdgeError { Category::Eof => "unexpected end of input", Category::Io => "i/o error while reading", }; - Self::ConfigOutOfDate { + Self::StoreExtraction { + reason: StoreExtractionReason::Deserialization, message: format!("typed app-config is out of date ({category}; value redacted)"), - field_path: redact_serde_path(serde_err.path()), + field_path: Some(redact_serde_path(serde_err.path())), } } } @@ -4362,9 +4507,9 @@ impl EdgeError { Caller sites: - Secret walk (§3.3.3) and validator path (§6.2.2): - `EdgeError::config_out_of_date(msg, field_path)`. + `EdgeError::store_extraction(reason, msg, Some(field_path))`. - Blob deserialise (§3.3.3, §6.2.2): - `EdgeError::config_out_of_date_from_serde(err)`. + `EdgeError::store_deserialization_from_serde(err)`. The validator path (§6.2.2) wraps a `validator::ValidationErrors`. **On the RUNTIME path this runs @@ -4382,7 +4527,11 @@ let message = if field.is_empty() { } else { format!("app config failed validation for field `{field}`") }; -EdgeError::config_out_of_date(message, field) // NO validation_err.to_string() +EdgeError::store_extraction( + StoreExtractionReason::Validation, + message, + (!field.is_empty()).then_some(field), +) // NO validation_err.to_string() ``` The `extract_first_field` helper picks the first @@ -4396,10 +4545,214 @@ is dropped entirely rather than logged. mentioning because PR #269 fought several "do we add this dep" rounds. Verdict: yes, this one is small enough, single purpose, and the only realistic way to give operators a -useful field-path hint. Without it, every `ConfigOutOfDate` +useful field-path hint. Without it, every schema-mismatch response says "missing field" with no anchor — operators have to grep the typed struct manually. +### 6.3.2 Bounded, capability-scoped extraction reads + +Typed app-config extraction never calls the unbounded convenience methods +`ConfigStoreHandle::get` or `SecretHandle::get_bytes`. It uses bounded variants under one +budget captured when the extractor starts: + +```rust +pub const DEFAULT_CONFIG_BLOB_BYTES: u64 = 8 * 1024 * 1024; +pub const DEFAULT_CONFIG_BACKEND_BYTES: u64 = 16 * 1024 * 1024; +pub const DEFAULT_CONFIG_EXTRACTION_BYTES: u64 = 16 * 1024 * 1024; +pub const DEFAULT_CONFIG_EXTRACTION_TIMEOUT: Duration = Duration::from_secs(30); +pub const DEFAULT_CONFIG_JSON_DEPTH: u32 = 64; +pub const DEFAULT_CONFIG_JSON_NODES: u32 = 8_192; +pub const DEFAULT_CONFIG_SECRET_BYTES: u64 = 1024 * 1024; + +#[derive(Clone, Copy, Debug)] +pub struct ConfigExtractionLimits { + pub max_backend_bytes: u64, + pub max_blob_bytes: u64, + pub max_json_depth: u32, + pub max_json_nodes: u32, + pub max_secret_bytes: u64, + pub max_total_bytes: u64, + pub timeout: Duration, +} + +pub struct BoundedStoreRead { + pub backend_bytes: u64, + pub value: Option, +} + +#[async_trait(?Send)] +pub trait ConfigStore: Send + Sync { + // Required: there is no cooperative default that can hide a never-ready provider future. + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError>; + + // Preserved for hand-managed reads; it makes no extraction-bound claim. + async fn get(&self, key: &str) -> Result, ConfigStoreError>; +} + +#[async_trait(?Send)] +pub trait SecretStore: Send + Sync { + // Required for the same reason as ConfigStore::get_bounded. + async fn get_bytes_bounded( + &self, + store_name: &str, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError>; + + // Existing unbounded convenience method, not used by AppConfig. + async fn get_bytes( + &self, + store_name: &str, + key: &str, + ) -> Result, SecretError>; +} + +impl ConfigStoreHandle { + pub async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError>; +} + +impl SecretHandle { + pub async fn get_bytes_bounded( + &self, + store_name: &str, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError>; +} + +impl BoundSecretStore { + pub async fn get_bytes_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, SecretError>; +} +``` + +`ConfigValue` is independently owned shared UTF-8 (`Arc`), never a slice retaining +the entire store file. Cloning shares the payload; `as_str()`/`as_ref()` borrow it, +`to_string()` deliberately copies it, and `Debug` is content-redacted. This is a hard +trait migration across all adapters, fixtures and consumers, not a compatibility layer. +Provider `String` to shared-payload conversion overlaps both allocations and must be +accounted separately. Cloudflare/Spin check size before conversion and expiry afterward; +Fastly preserves cumulative root/chunk accounting through the same terminal check. + +The bounded methods add `DeadlineExceeded` and `ValueTooLarge` to both lower-level store +error enums. Those variants are produced at the adapter boundary without formatting away +the cause and map directly to the same-named extraction reasons (`ValueTooLarge` maps to +`StoreExtractionReason::ValueTooLarge`). Existing unbounded methods need not synthesize +these variants. + +`ConfigExtractionLimits::default()` returns the seven constants above. The JSON scanner +counts the root and every nested value, including array/object containers; the root has +depth 1. A streaming `serde_json` visitor enforces both caps before either full JSON parse, +without retaining a `Value` tree. Exact-cap structures are accepted. Depth must be in +`1..=127` (below the parser's recursion ceiling), and node count must be nonzero. +These caps bound the parsed tree's structural amplification; they do not bound transient +string decoding, the provider-owned read buffer, sorted-key scratch, or total process RSS. Canonical hashing streams directly into SHA-256 without a full output-string buffer. + +`App` owns one config-extraction-limits value, set through the fallible +`Hooks::configure(&mut App) -> Result<(), EdgeError>` callback; the core-owned +`App::build::(platform)` prevents request conversion, admission, body polling, or dispatch when validation fails (and prevents +listener bind on Axum), while the adapter copies a successfully validated value into +`RequestContext`. All values must be finite and nonzero, and +`max_total_bytes >= max(max_blob_bytes, max_secret_bytes)`. `timeout` must not exceed +`DEADLINE_FAR_FUTURE`; invalid startup configuration fails before serving requests. + +At the first `AppConfig`/`named`/`from_store` call, core clones the +application-owned `MonotonicClock` from `RequestContext`, snapshots +`extraction_started_at = clock.now()`, and computes exactly one absolute deadline with +checked `extraction_started_at + limits.timeout` and `Deadline::at_instant(..)`. It does +not call `Deadline::after` or sample the process-global clock after the first snapshot. +The deadline is not reset between the root blob read, +Fastly pointer/chunk reads, secret resolution, deserialization, or validation. Every adapter +receives that same `&MonotonicClock` on every bounded call and checks the deadline before +entering a host read and after the host read becomes ready. The post-ready clock check occurs +before applying `?` or otherwise propagating the provider result, so equality expiry wins a +simultaneously ready success or provider error. Fastly uses the same clock for the root read, +each pointer chunk, and every synchronous secret-store open/lookup/plaintext operation. Equality +is expired. Cloudflare KV and Spin KV/variable reads additionally race the guest-visible provider +future against a runtime-native timer for the remaining absolute budget. Because a runtime timer +may wake early or round its duration, EdgeZero resamples the injected clock when the timer resolves +and re-arms it for the new remaining duration unless the absolute deadline is expired. At equality, +timer readiness wins a simultaneously ready provider result, and the losing guest future is +dropped. This bounds when EdgeZero returns `DeadlineExceeded`, but it does not prove that the +provider cancels host-side work. Fastly's synchronous host calls cannot be guest-preempted. The +bounded trait methods are required implementation points: core supplies no default that can +silently await an unbounded provider call forever. + +Each bounded method applies both remaining extraction-wide backend-byte allowance and its +per-value output cap. `backend_bytes` counts every byte exposed to guest code while +satisfying that logical read, including an adapter-private pointer and every chunk used to +reconstruct one Fastly value; it does not count opaque provider work. The adapter uses +checked accounting and never reports a value whose length exceeds `max_value_bytes` or +whose guest-visible read cost exceeds `max_backend_bytes`. A Native implementation rejects +before retaining the first byte beyond either allowance; an Unsupported host-allocation +implementation may first receive a provider-materialized value, then discards it. + +The extractor verifies the returned count, including `backend_bytes >= value.len()` whenever +a value is present, uses checked addition to charge it against the single extraction-wide +`max_backend_bytes`, and separately charges each returned logical blob/secret length against +`max_total_bytes` before retaining or inserting it. It passes the remaining backend +allowance into the next read. Duplicate references to the same secret are charged for every +actual backend read; §6.4 deliberately defines no extraction cache. A backend that reports +more bytes than the allowance, a value larger than the supplied per-value limit, or a byte +count smaller than its returned value fails closed as `BackendFailure`. Any legitimate cap +failure detected by the adapter or extraction budget maps to +`StoreExtractionReason::ValueTooLarge`; expiry maps to +`StoreExtractionReason::DeadlineExceeded`. Partial values are discarded and never parsed, +hashed, deserialized, validated, logged, or returned. + +These are distinct guarantees: + +- **Guest-visible bound:** every target enforces per-value and cumulative byte caps before + EdgeZero retains or processes the returned value. +- **Host-allocation bound:** only a target whose host API supports streaming/size-aware reads + can prevent the provider SDK from first materializing an oversized value. +- **Cancellation bound:** cooperative pre/post checks give a typed eventual result, but only + a target with a cancellable async read and deployed host evidence can claim that provider work + terminates within a finite wall-clock bound. A guest timer race alone proves prompt EdgeZero + return, not provider cancellation. + +The capability ladder therefore gains two config-owned cells: + +| Capability | Axum | Cloudflare | Fastly | Spin | +| --- | --- | --- | --- | --- | +| `config-read-allocation-bounds` | Unsupported overall; bounded local snapshots do not certify secret/environment/allocator overhead | Unsupported overall; capped guest byte streams do not certify opaque KV/JS retention or secret reads | Unsupported overall; caller-sized ABI buffers do not certify provider storage or secret reads | Unsupported until host API documentation and a probe prove it | +| `config-read-deadlines` | BestEffort while request-time reads are synchronous shared-handle lookups with pre/post checks | BestEffort: guest timer race returns promptly, but host cancellation is unproved | BestEffort; synchronous host reads are not guest-preemptible | BestEffort: guest timer race returns promptly, but host cancellation is unproved | + +Apps that require a strict RSS or elapsed-time guarantee declare the corresponding Native +capability and fail before startup/deploy on weaker targets. Capability documentation must +name provider-side materialization, parser allocation, SDK copies, and uninterruptible host +calls as exclusions; it must not imply that a post-return `String::len()` check bounded host +RSS. A downstream requirement for cancellation is satisfied only by a `Native` capability cell; +none of the current adapters makes that claim. One deployed timing probe per non-Axum target +records whether provider cancellation occurs within the documented tolerance before any +capability cell is upgraded. + ### 6.4 Per-request caching **v1 stance: no caching. Every `AppConfig` extractor @@ -4446,8 +4799,9 @@ scope per §3.2. ### 6.5 What happens to the existing `ConfigStore` trait `ConfigStore::get(key)` stays — it's still useful for ad-hoc -lookups against the same backends. But the `AppConfig` extractor -is the ONLY supported way to read the typed app-config. +lookups against the same backends, but it carries none of §6.3.2's extraction guarantees. +The `AppConfig` extractor uses only `get_bounded` and is the ONLY supported way to read +the typed app-config. Hand-written `ctx.config_store_default()?.get("greeting")` against the typed app-config's store will now find a JSON blob under "greeting"-ish keys, not raw strings, and fail to deserialise into @@ -4853,8 +5207,8 @@ pub trait Adapter: Sync + Send { pub enum ReadConfigEntry { /// The store and key both exist; the inner string is the raw - /// blob envelope JSON (the same shape `ConfigStore::get` - /// returns at runtime per `crates/edgezero-core/src/config_store.rs:214`). + /// blob envelope JSON (the same logical value returned by runtime + /// `ConfigStore::{get,get_bounded}`). /// Caller parses + verifies. **Round-27 reviewer flagged an /// earlier `Vec` draft as inconsistent with the rest of /// §9.1-§9.4's per-adapter wording, all of which describes @@ -4970,8 +5324,8 @@ coexist in one file. A flat `{ key: envelope_json_string }` map. Each value is the envelope serialised to JSON and stored as an ESCAPED STRING (not a nested JSON object), so the file deserialises into -`BTreeMap` and Axum's `ConfigStore::get(key)` -returns the string directly without any envelope-aware +a bounded sorted index of independently owned shared `ConfigValue` payloads. +Axum's `ConfigStore::get(key)` returns the shared UTF-8 value without any envelope-aware parsing inside the adapter. ```json @@ -4987,22 +5341,21 @@ parsing inside the adapter. to `.tmp`, fsync, rename). The entry value is the envelope serialised then `serde_json::to_string`-ed and inserted as a string value. -- Runtime: the extractor stays adapter-neutral and only calls - `ConfigStore::get(key) -> Option` per - `crates/edgezero-core/src/config_store.rs:214`. Axum's - `ConfigStore` impl wraps the file map: - - ```rust - impl ConfigStore for AxumConfigStore { - async fn get(&self, key: &str) -> Result, ConfigStoreError> { - let map: BTreeMap = self.map.read().await.clone(); - Ok(map.get(key).cloned()) - } - } - ``` - - The string form keeps the per-adapter `ConfigStore::get -> -Option` contract intact across all four adapters — +- Runtime: the standard runner loads immutable bounded flat-string snapshots before + listener binding, sequentially across the registry. It caps actual file reads, entry + count, decoded key/value bytes, all-store resident allocations and startup staging; + duplicate keys fail. No intermediate `Value` tree is created. Size violations fail + startup; other failed declarations retain typed failing handles. Missing files remain + empty declared stores. Push changes require restart. Request-time `get_bounded` checks + both extraction caps and the injected deadline before cloning one shared payload handle; + raw `get` also shares that payload but makes no extraction-bound claim. + `from_path`, `from_local_file` and fallible `from_map` require `ConfigStoreLimits`. + Caller-owned map inputs are outside startup-reader charges. The complete defaults, + supported ranges, requested-allocation formula, decoder pin and exclusions are normative + in [the Axum snapshot limits guide](../../guide/adapters/axum.md#snapshot-allocation-limits). + + The string form keeps the per-adapter `ConfigStore` logical value contract intact across + all four adapters — storing nested JSON objects would force the Axum store to re-serialise on every `get`, AND would require the extractor to know Axum's file shape. The string form sidesteps both. @@ -5039,8 +5392,25 @@ bulk put --namespace-id= --remote` `wrangler kv key get --binding --remote` (Wrangler 4.x's four-segment subcommand path; the older three-segment `wrangler kv get` is deprecated). -- Runtime: `CloudflareConfigStore::get(key)` returns the JSON - string; the `AppConfig` extractor parses + verifies. +- Runtime: `CloudflareConfigStore::get_bounded` requests a KV byte stream, not `.text()` + or a materialized array buffer. A BYOB reader uses reusable 16 KiB Rust/JS buffers and + rejects the first excess byte before append. Accumulator capacity never intentionally + exceeds `L = min(max_backend_bytes, max_value_bytes)`; requested payload staging is + conservatively `2L + 32 KiB`, covering relocation/shared-string conversion and scratch. + Bounded reads require valid UTF-8, without lossy expansion. Failure to acquire a byte + reader fails closed; there is no unbounded fallback. An acquired reader is cancelled on + overflow, timeout or dropped work. The timer covers acquisition, drain and conversion, + rechecking the injected clock after each wake and before/after every pull, with a task-queue + yield every 16 chunks for frozen-clock hosts. An acquisition owner cancels late-arriving + streams after abandonment; cancellation-promise rejections are consumed without publishing + provider diagnostics. The SDK exposes no acquisition-promise + cancellation. Transferred wasm-bindgen callbacks require `FinalizationRegistry`; absent + runtime support fails before provider dispatch. Demo/generated Workers explicitly set + `enable_weak_ref`, without claiming deterministic GC cleanup. + KV host buffering, JS wrapper/GC retention and finite host teardown remain + unqualified. Deadlines remain BestEffort and broad allocation support Unsupported. + `AppConfig` parses/verifies only after charging the bounded read. Raw `get` remains + explicitly unbounded. - `--local` push: `wrangler kv bulk put --binding --local` — lands in `.wrangler/state`. Local push deliberately addresses by @@ -5077,9 +5447,22 @@ bulk put --namespace-id= --remote` chunk pointer, read/verify/concatenate chunks and return the reconstructed normal envelope string. Missing/corrupt chunks are a read error, not "missing key". -- Runtime: `FastlyConfigStore::get(key)` performs the same - direct-or-pointer resolution and returns the normal envelope JSON - value; the core extractor does not know whether Fastly used chunks. +- Runtime: `FastlyConfigStore::get_bounded` performs the same direct-or-pointer resolution, + passes one absolute deadline/remaining allowance to every read, and returns the normal + envelope JSON value. Its `backend_bytes` includes the pointer and every fetched chunk; + the core extractor does not know which physical form Fastly used. Reads use one initialized + buffer through `fastly-sys 0.13.1`, never SDK auto-growth on `BUFLEN`. A physical entry has + an adapter ceiling of 32,000 bytes; roots are capped by + `R = min(max_backend_bytes, max(max_value_bytes, 8000), 32000)`, pointers by 8,000 bytes, + and chunks by 7,000 bytes and both remaining allowances. Validated declared totals must + fit both aggregate caps before chunk dispatch or exact-size reassembly reservation. + Root discriminators borrow raw tokens instead of materializing a JSON `Value` tree. + The bounded resolver verifies transport hashes/layout, then delegates inner-envelope + materialization and integrity to the extractor's structural preflight, matching direct + values. Raw `get`/CLI paths retain their explicit inner-envelope integrity checks and + are not allocation-certified. A conservative requested guest-payload charge is + `2V + R + 16P + 7000` bytes (`V = max_value_bytes`, `P = 8000`), plus fixed objects and + allocator overhead; host memory and synchronous-call interruption remain excluded. - Local server: `[local_server.config_stores.]` now has ONE active root entry — the blob key. PR #269 F6 already moved local- server seeding to `config push --local`. Local seeding must mirror @@ -5166,8 +5549,12 @@ key-value set --stdin` or `--from-file`. keeps the "no multi-blob merge" stance — splitting is operator-side schema work. -- Runtime: `SpinConfigStore::get(key)` returns the JSON value; - extractor parses. +- Runtime: `SpinConfigStore::get_bounded` returns the JSON value and races the provider future + against a Spin timer under the absolute deadline before applying the guest-visible checks from + §6.3.2. Spin secret reads use the same clock-verified, early-wake-rearming race. The timer bounds + EdgeZero's return but does not prove host-side cancellation or pre-allocation bounds, so deadline + support remains BestEffort and allocation support remains Unsupported. The extractor parses only + after the bounded read is charged. ## 10. Migration @@ -5188,9 +5575,9 @@ maintenance tax. 2. The runtime `AppConfig` extractor replaces direct `ctx.config_store_default()?.get(key)` calls against the typed config's store. Downstream apps with custom `ConfigStore` - consumers see a deprecation note in `CHANGELOG.md`; the - compile-time surface (the `ConfigStore` trait itself) doesn't - change. + consumers must hard-migrate `get`/`get_bounded` and matching handle APIs to + shared `ConfigValue` instead of `String`, as recorded in `CHANGELOG.md`. + No deprecated aliases or dual trait signatures remain. 3. Pre-blob blobs (the flattened per-leaf state) become unreadable. The runtime errors loudly on the first request pointing at the migration guide. We do NOT silently treat @@ -6227,25 +6614,24 @@ What happens when the store has no entry at the requested key? - (b) Return `EdgeError::ServiceUnavailable`. - (c) Add a sibling extractor `MaybeAppConfig` returning `Option` for endpoints that have a sensible default. -- (d) Return `EdgeError::ConfigOutOfDate` — matches the - "re-run ` config push` fixes it" rule - spelled out for `ConfigOutOfDate` at §6.3. +- (d) Return `EdgeError::StoreExtraction` with reason `MissingBlob`; §6.3 maps that reason + to the `config_out_of_date` wire contract because re-running ` config push` + fixes it. - **default:** (d) for v1 (round-18 M-2 reversal of the - earlier (a) pick). The §6.3 rationale defines - `ConfigOutOfDate` as "the situation a re-run of + earlier (a) pick). The §6.3 rationale defines the + `config_out_of_date` wire class as "the situation a re-run of ` config push` resolves" — and a missing blob is squarely in that bucket: the deployed code expects a blob, the store doesn't have one, an operator push fixes it. Mapping this to `Internal` would page oncall (a 500-class signal) when the actionable response is "push the config", which - `ConfigOutOfDate` (503 with `Retry-After: 60`) - already encodes. The §3.3.3 extractor's + `StoreExtractionReason::MissingBlob` (503 / `config_out_of_date` with + `Retry-After: 60`) already encodes. The §3.3.3 extractor's `ok_or_else(|| EdgeError::internal("missing typed app-config blob"))` call site changes to - `EdgeError::config_out_of_date("missing typed -app-config blob at key ``— run` - config push`", String::new())` accordingly. (c) is + `EdgeError::store_extraction(StoreExtractionReason::MissingBlob, "missing typed +app-config blob at key `` — run ` config push`", None)` accordingly. (c) is still tracked as a follow-up for endpoints that want explicit `Option` semantics. @@ -6582,14 +6968,14 @@ length, charset) on the operator-typed KEY NAME. - Returns the deserialised struct for a valid blob. - Errors on sha mismatch (manually-edited blob). - **Missing key (per Q3 (d) / §6.3 — round-19/20).** - Fixture: `ConfigStore::get` returns `Ok(None)`. - Assert the extractor produces - `EdgeError::ConfigOutOfDate { message, field_path }` + Fixture: `ConfigStore::get_bounded` returns `BoundedStoreRead { value: None, .. }`. + Assert the extractor produces `EdgeError::StoreExtraction` with reason `MissingBlob`, + no field path, and a `message` where `message` contains the literal `key -\`\``AND the literal`run +\`\`` and the literal `run \` config push\``. Render the resulting `Response`; assert HTTP status 503 and -the `Retry-After: 60`header is present. NOT`EdgeError::Internal` (the round-18 reversal that +the `Retry-After: 60` header is present. Not `EdgeError::Internal` (the round-18 reversal that round-19 then propagated through §6.3). - `named(key)` reads a different key from the same store. - `BlobEnvelope` deserialise: @@ -6759,35 +7145,23 @@ strip or revert the resolution direction: store-ref field, unchanged). This is the user-facing Model A contract — the framework swaps key NAME for resolved VALUE before the handler sees `cfg`. -- **Missing secret at extract time.** Same fixture, mock - secret store configured to `NotFound` on lookup. Assert - the extractor produces `EdgeError::ConfigOutOfDate` (per - §3.3.6) — NOT `Unavailable` (that's reserved for - store-backend network errors) and NOT `Internal` (which - today's `SecretError::NotFound → EdgeError::Internal` - mapping in `crates/edgezero-core/src/secret_store.rs:131` - produces). The blob model's missing-secret behaviour - requires a DELIBERATE deviation from the current mapping: - the extractor wraps `SecretError::NotFound` into - `ConfigOutOfDate` BEFORE it bubbles, so the dashboards - signal "deploy is incomplete" rather than "we 500'd". - Calls out: the `secret_store.rs` mapping itself doesn't - change for raw handler-side callers (still maps to - `Internal`); the extractor's `?` operator catches it and - re-wraps. Test asserts the resulting `EdgeError` variant - AND the `Retry-After: 60` header per §6.3.1. +- **Missing secret at extract time.** Same fixture, mock secret store returning no value. + Assert the extractor produces `EdgeError::StoreExtraction` with reason `MissingSecret` + (per §3.3.6), not `SecretBackendUnavailable` and not `BackendFailure`. The typed + extractor adds this context at its ownership boundary instead of relying on the generic + raw-store conversion. Assert the reason, `config_out_of_date` wire kind, field path, and + `Retry-After: 60` header per §6.3.1. - **Missing secret-store id at extract time.** Fixture: manifest declares no `[stores.secrets].ids = ["missing"]`, but a `#[secret(store_ref = "vault")]` field's value is - `"missing"`. Assert extractor surfaces - `EdgeError::ConfigOutOfDate` with the actionable message - "blob declared store_ref `missing` but [stores.secrets] - has no such id". + `"missing"`. Assert the extractor produces `EdgeError::StoreExtraction` with reason + `UnknownStore`, HTTP 500 / `internal`, a field path, and a redacted message that does not + repeat the stored identifier. - **Secret-store unreachable at extract time.** Same fixture, mock store configured to error with a network - failure (NOT NotFound). Assert extractor surfaces - `EdgeError::ServiceUnavailable` (per §3.3.6 — a flaky - backend is not an out-of-date config). + failure (not a missing value). Assert the extractor produces + `EdgeError::StoreExtraction` with reason `SecretBackendUnavailable` and the + `service_unavailable` wire kind (per §3.3.6). - **` config validate --strict` — structural checks ONLY.** Three sub-tests, each asserting on a structural property the validator can verify without @@ -6822,14 +7196,12 @@ strip or revert the resolution direction: - **Validate runs on every extract.** Fixture: a blob that serdes cleanly but violates a `#[validate(range(min=1, -max=10000))]` rule. Extract. Assert - `EdgeError::ConfigOutOfDate` with `field_path` naming the - offending field. +max=10000))]` rule. Extract. Assert `EdgeError::StoreExtraction` with reason + `Validation` and `field_path` naming the offending field. - **Validate-OK is the happy path.** Fixture: a blob in bounds. Extract. Assert the typed struct comes back with no error. -- **Validator violation report format.** Assert the - `ConfigOutOfDate` response body matches the nested +- **Validator violation report format.** Assert the `Validation` response body matches the nested envelope documented in §6.3.1 exactly: `{ "error": { "status": 503, "kind": "config_out_of_date", "message": "<…>", "field_path": "" } }`. Assert @@ -6838,74 +7210,36 @@ max=10000))]` rule. Extract. Assert fixture). Assert the response carries the `Retry-After: 60` header. -#### 12.6.1 `kind` strings on every `EdgeError` variant + header policy (round-23 M-1) - -§6.3.1 adds a stable `kind: String` field to the response -body for EVERY `EdgeError` variant (`bad_request`, -`internal`, `method_not_allowed`, `not_found`, -`not_implemented`, `service_unavailable`, `validation`, -and the new `config_out_of_date`). The earlier test plan -only asserted the `config_out_of_date` body; this section -covers the other six and the cross-cutting -header / field-presence rules so a future refactor can't -silently drop or rename a `kind` string. - -- **Stable `kind` strings.** One fixture per variant - that triggers it (e.g. a handler that returns - `Err(EdgeError::not_found("…"))` for `not_found`). - Render the `Response`. Parse the body as JSON. - Assert `body.error.kind` equals the exact string - documented in §6.3.1. Variant → expected string: - - | Variant | Expected `kind` string | - | -------------------- | ----------------------- | - | `BadRequest` | `"bad_request"` | - | `Internal` | `"internal"` | - | `MethodNotAllowed` | `"method_not_allowed"` | - | `NotFound` | `"not_found"` | - | `NotImplemented` | `"not_implemented"` | - | `ServiceUnavailable` | `"service_unavailable"` | - | `Validation` | `"validation"` | - | `ConfigOutOfDate` | `"config_out_of_date"` | - -- **`field_path` is OMITTED outside `ConfigOutOfDate`.** - For each non-`ConfigOutOfDate` variant fixture above, - assert `body.error.get("field_path")` is `None` (the - field is absent from the JSON, not present with an - empty string). Per §6.3.1's "ONLY on - `config_out_of_date`" rule. - -- **`Retry-After: 60` is PRESENT on `ConfigOutOfDate` - ONLY.** Fixture per variant: render the response, - assert `response.headers().get("retry-after")`: - - `ConfigOutOfDate`: `Some("60")`. - - `ServiceUnavailable`: `None`. Round-10 H-3 audit - found the variant is reused for several - non-retryable cases (KV size limit, missing - named KV store, missing default secret store); - sending the header would mislead clients into a - tight retry loop. The §6.3.1 narrowing pins - `ServiceUnavailable` to no `Retry-After`. - - All other variants: `None`. - -- **Status codes match the documented variant → HTTP - mapping.** Fixture per variant: assert - `response.status()`: - - | Variant | HTTP status | - | -------------------- | ----------- | - | `BadRequest` | 400 | - | `Internal` | 500 | - | `MethodNotAllowed` | 405 | - | `NotFound` | 404 | - | `NotImplemented` | 501 | - | `ServiceUnavailable` | 503 | - | `Validation` | 422 | - | `ConfigOutOfDate` | 503 | - - (The two 503-class variants are deliberately - distinguished by the `kind` string + the - `Retry-After` header, not the status code.) +#### 12.6.1 End-state `EdgeError` wire matrix and store-reason policy + +The implementing branch enumerates every then-present `EdgeError` variant without a +wildcard. Final integration includes variants owned by the outbound and inbound specs as +well as this config design: + +| Variant | Expected `kind` | HTTP | +| --- | --- | --- | +| `BadGateway` | `"bad_gateway"` | 502 | +| `BadRequest` | `"bad_request"` | 400 | +| `ConfigOutOfDate` | `"config_out_of_date"` | 503 | +| `GatewayTimeout` | `"gateway_timeout"` | 504 | +| `Internal` | `"internal"` | 500 | +| `MethodNotAllowed` | `"method_not_allowed"` | 405 | +| `NotFound` | `"not_found"` | 404 | +| `NotImplemented` | `"not_implemented"` | 501 | +| `RequestTimeout` | `"request_timeout"` | 408 | +| `ResponseTooLarge` | `"response_too_large"` | 502 | +| `ServiceUnavailable` | `"service_unavailable"` | 503 | +| `StoreExtraction` | reason-dependent per §6.3 | reason-dependent per §6.3 | +| `Validation` | `"validation"` | 422 | + +For every ordinary variant, render a response and assert status, kind, and omission of the +Rust-only typed reason. For every known `StoreExtractionReason`, assert the exact §6.3 +status/kind/`Retry-After`/field-path row. `field_path` is omitted outside +`ConfigOutOfDate` and `StoreExtraction`; it is also omitted when either carrier has no +non-empty path. `Retry-After: 60` appears only on an effective `config_out_of_date` outcome: +the existing `ConfigOutOfDate` variant plus `Deserialization`, `MissingBlob`, +`MissingSecret`, and `Validation`. A plain `ServiceUnavailable` and store reasons mapped to +`service_unavailable` carry no retry header. ### 12.7 Env-var key override (§5.2) @@ -7397,9 +7731,8 @@ default_key }` on `ConfigRegistry`. `Config::default()` the load-bearing commit; everything it touches has to land together per §10's hard-cutoff rule: - `AppConfig` extractor + secret resolution - wired into all four adapters' `ConfigStore::get` - read path. Missing-blob maps to - `EdgeError::ConfigOutOfDate` per Q3 (d). + wired into all four adapters' bounded config-read path. Missing blob maps to + `EdgeError::StoreExtraction` with reason `MissingBlob` per Q3 (d). - `config push` rewrite — single-blob writers per adapter (Axum file map / Cloudflare bulk-put / Fastly `--upsert --stdin` / Spin direct-write). diff --git a/docs/superpowers/specs/2026-06-16-test-coverage-gap.md b/docs/superpowers/specs/2026-06-16-test-coverage-gap.md index 284112015..4daac9c13 100644 --- a/docs/superpowers/specs/2026-06-16-test-coverage-gap.md +++ b/docs/superpowers/specs/2026-06-16-test-coverage-gap.md @@ -435,7 +435,9 @@ git commit -m "test(adapter): cover run_native_cli not-found and non-zero-exit p let garbage = b"this is definitely not a gzip member".to_vec(); let stream = stream::iter(vec![Ok::, io::Error>(garbage)]); let result = block_on(async { - decode_gzip_stream(stream).try_collect::>().await + decode_gzip_stream(stream, FLATE_DECODER_FIXED_CHARGE_BYTES) + .try_collect::>() + .await }); assert!(result.is_err(), "invalid gzip must decode to an error"); } @@ -446,7 +448,9 @@ git commit -m "test(adapter): cover run_native_cli not-found and non-zero-exit p let garbage = vec![0xFF_u8; 64]; let stream = stream::iter(vec![Ok::, io::Error>(garbage)]); let result = block_on(async { - decode_brotli_stream(stream).try_collect::>().await + decode_brotli_stream(stream, 24, 1_u64 << 25) + .try_collect::>() + .await }); assert!(result.is_err(), "invalid brotli must decode to an error"); } diff --git a/docs/superpowers/specs/2026-07-03-edgezero-fastly-dispatch-and-appstate-design.md b/docs/superpowers/specs/2026-07-03-edgezero-fastly-dispatch-and-appstate-design.md index 1dfd8d860..f2edc2c3f 100644 --- a/docs/superpowers/specs/2026-07-03-edgezero-fastly-dispatch-and-appstate-design.md +++ b/docs/superpowers/specs/2026-07-03-edgezero-fastly-dispatch-and-appstate-design.md @@ -1,5 +1,9 @@ # EdgeZero P0-C + P0-D — Fastly `run_app` dispatch fidelity + app-state injection +> **Superseded lifecycle surface (2026-09-15):** The outbound design now specifies +> `run_app_with_hooks` and `FastlyService::send_request_with_hooks`; the request-only hook named +> below was removed in the hard cut. This document remains historical context. + - **Status:** Draft for edgezero maintainer - **Date:** 2026-07-03 - **Target repo:** `github.com/IABTechLab/edgezero` (`edgezero-adapter-fastly`, `edgezero-core`, `edgezero-macros`) @@ -158,7 +162,7 @@ builder = builder.with_state(crate::app_state()); // #state_expr, only when `s // ... routes ... ``` -**`state = ` is a full Rust expression** evaluating to the app-owned value, emitted **verbatim** into `.with_state()`. It must be the call/expression, **not** a bare function path — `with_state` takes the state *value*, so `state = crate::app_state` (a fn item) would pass the function, not its result. Write `state = crate::app_state()` or `state = std::sync::Arc::new(AppState::new())`. (This mirrors nothing magical: the macro does not append `()`.) `run_app` → `A::build_app()` → `routes()` → `build_router()` already runs per request (Fastly) / once at startup (Axum), and #306's dispatch injection clones the value into each request — so `State` reaches `#[action]` handlers on **all four adapters** with no adapter edits. Without the `state` argument, `build_router()` is unchanged (no state), preserving current behavior. +**`state = ` is a full Rust expression** evaluating to the app-owned value, emitted **verbatim** into `.with_state()`. It must be the call/expression, **not** a bare function path — `with_state` takes the state *value*, so `state = crate::app_state` (a fn item) would pass the function, not its result. Write `state = crate::app_state()` or `state = std::sync::Arc::new(AppState::new())`. (This mirrors nothing magical: the macro does not append `()`.) `run_app` → `App::build::(platform)` → `routes()` → `build_router()` already runs per request (Fastly) / once at startup (Axum), and #306's dispatch injection clones the value into each request — so `State` reaches `#[action]` handlers on **all four adapters** with no adapter edits. Without the `state` argument, `build_router()` is unchanged (no state), preserving current behavior. **Single vs. multiple state types.** `with_state` registers one `T` — a single `state = crate::app_state()` covers the `Arc` case (what trusted-server / `app-demo` need). If multiple state types are ever required, allow repeated `state = a(), state = b()` (emit one `.with_state(...)` per occurrence) or add a `RouterBuilder::with_state_extensions(Extensions)` fed by an app-supplied `Extensions` bag. Default to the single-value form unless a concrete multi-type need appears. **The grammar below permits repeated `state`; whether repeats are accepted or rejected is a decision the plan must state** (see §`app!` argument grammar). diff --git a/docs/superpowers/specs/2026-08-22-inbound-body-design.md b/docs/superpowers/specs/2026-08-22-inbound-body-design.md new file mode 100644 index 000000000..8b4d2888b --- /dev/null +++ b/docs/superpowers/specs/2026-08-22-inbound-body-design.md @@ -0,0 +1,1089 @@ +# EdgeZero Inbound Request-Body Design + +> **Status:** Draft. Core admission, deadline-bound body handling, and adapter entry seams are +> partially implemented. The raw-parser requirements in §1.2 are future certification criteria; +> every current raw-ingress capability cell remains `Unsupported`. +> +> Extracted from the +> [outbound-HTTP design](2026-05-21-outbound-http-design.md) so that specification stays +> focused on outbound HTTP. This document owns the **inbound request-body** contract: +> `RequestContext` body reading, adapter ingress, extractor limits, and the `BodyCell` +> state machine. Outbound streaming proxy-forward consumes the resulting +> `BodyCell`/`into_request()` contract but does not own it. + +## 1. Scope + +The goal is to stop adapters from pre-buffering unbounded inbound bodies while preserving +the existing `FromRequest::from_request(&RequestContext, ..)` extractor signature. A body +is consumed from the platform at most once, bounded helpers cache successful bytes, and a +partially consumed failed drain becomes sticky poison. This document does not define +outbound response caps, outbound deadlines, or provider send behavior. + +The total `StoredError` match below targets the end-state `EdgeError` surface and therefore +depends on the outbound error work landing first: `BudgetSource`, `BadGateway`, +`GatewayTimeout`, and `ResponseTooLarge` are defined by the outbound design, not here. If +the body-state migration is split across earlier commits, each commit must still compile +against the variants present at that point. This design additionally owns +`RequestTimeout` (408) for an admitted inbound body whose absolute read deadline expires; +the final `StoredError` match covers every variant then present without a wildcard. + +Rust snippets show interfaces and state shapes; omitted bodies are implementation work. +Implementations must pass the workspace's strict Clippy gate: keep items, enum variants, +and methods alphabetically ordered, add `#[inline]` to public methods, and use checked +arithmetic for drain accounting. State-transition order in prose does not dictate enum +declaration order. + +### 1.1 Adapter ingress seam and admission + +Every adapter entry point has one ingress seam **before middleware, extractor execution, +EdgeZero-owned body buffering, or a `BodyCell::Initial` transition**. Route resolution is +the one deliberate exception: the router resolves once before admission so policy sees the +canonical route identity, but it does not dispatch middleware or a handler. The seam +performs this ordered protocol exactly once per platform request: + +1. Stamp `request_start` from the `App`-owned `MonotonicClock` at the earliest + EdgeZero-owned entry point by calling `App::monotonic_now()`. It means "EdgeZero + received control", not when the client sent its first byte and not when route execution + began. The same cloneable clock handle is attached to the admitted request. +2. Enforce parser-level request-target and header limits, then validate raw request framing, + where the platform exposes a raw boundary (§1.2). Target overflow returns 414, header + byte/count overflow returns 431, and framing rejection returns 400. Every rejection + closes/resets that request without route resolution, app admission, or body polling. +3. Convert only request-head data, retain the unread native body separately, and call + `RouterService::resolve(method, path)` once. Resolution returns an opaque dispatch token + plus `RouteResolution::{Matched, MethodNotAllowed, NotFound}`. It performs no middleware, + handler, body poll, or request-extension injection. The token owns the exact match and + path parameters so admitted dispatch does not rerun matching. +4. Construct a normalized `IngressHead` containing the method, target, version, visible + headers, `IngressHeadAccounting`, `IngressFraming`, adapter request metadata, public + `request_start`, and the stable route resolution. The accounting and framing values say + whether EdgeZero validated the raw boundary or the host owns those decisions. +5. Invoke the one admission policy stored on `App`. It returns `Abort`, + `Refuse { response, completion }`, `Admit { completion, grant, read_deadline }`, or + `ReadBodyBeforeFallback { completion, grant, max_body_bytes, read_deadline, on_exceeded, + on_timeout }`. The callback is synchronous and body-blind: it can inspect the head but cannot + obtain or poll the body. Every response-producing decision carries exactly one non-clone + `ResponseEgressCompletion`; applications without a response-owned resource use + `ResponseEgressCompletion::empty()`. + A refusal bypasses resolved dispatch, terminates the unread platform body with the strongest + target-specific primitive, and converts that response as detached ingress egress. It skips the + application response-egress policy and global observer but retains and terminally invokes the + refusal's response-scoped completion. Normalized head-validation failures obtain their + disposition from the application's detached-egress decision factory. + `Send { completion, deadline }` constructs detached egress with that completion and a mandatory + absolute deadline in the request's injected monotonic-clock domain. The effective bound is the + earlier of that deadline and the default response-egress policy: a later factory deadline cannot + extend the default policy, and the policy cannot extend an earlier factory deadline. The default + selects `Send` with an empty completion and `DEFAULT_RESPONSE_WRITE_BUDGET` measured from + `request_start` through + `DetachedResponseEgressHead::write_deadline_after`. Admission-selected detached responses retain + their response-owned deadline path and are outside this factory contract. `Abort` constructs no + response. Raw parser failures and platform-to-core head-conversion failures before EdgeZero + captures a normalized head remain outside the application lifecycle contract. + `Abort` is a distinct non-response outcome: it polls no body, invokes no middleware or handler, + constructs no response-egress attempt, invokes no completion or observer, and instructs the + adapter to terminate the request at its strongest pre-response reset/error/close boundary. +6. For `Admit`, wrap the still-unread platform body with the absolute deadline and native + cancellation owner, construct the core `Request`, and call + `RouterService::dispatch_resolved` with the exact token from step 3. For `Matched`, the + router constructs `RequestContext` with `request_start`, route metadata, and the + request-owned grant before middleware. `PreparedIngress` retains the non-clone completion + separately from the request and router token. For `MethodNotAllowed`/`NotFound`, ordinary + admission retains immediate 405/404 handling without polling the body; no middleware or + handler runs and the grant is dropped exactly once. The wrapper is installed for + `Body::Once` and `Body::Stream`; a content-type path must not pre-buffer before this point. + After `PreparedIngress` is returned, every adapter setup or conversion failure consumes that + proof through `App::admitted_error_egress`. The resulting envelope retains the admitted app + clock, request start, request method, route metadata, response-egress policy, completion, and + observer. + No adapter may convert such a failure directly to a provider error or detached response. +7. `ReadBodyBeforeFallback` is valid only for the pre-resolved `MethodNotAllowed` and + `NotFound` outcomes. It preserves the exact resolved token, installs the same absolute + deadline wrapper, holds the supplied grant, and drains the body to EOF while discarding its + contents. Clean EOF at or below `max_body_bytes` produces the canonical 405/404; the first + byte over the cap produces the exact buffered `on_exceeded` response, and deadline expiry + produces the exact buffered `on_timeout` response, both before the routing response. A zero + cap accepts only an empty body. The grant remains live through every body poll and is dropped + exactly once after the drain terminates and before response conversion; dropping the core + drain future also releases it. This path invokes no middleware, handler, or `RequestContext` + and never rematches. The decision's completion follows whichever terminal response wins: the + canonical 404/405, exact-cap response, overflow response, timeout response, or a later admitted + conversion error. Returning this decision for `Matched` fails closed before native body + construction and drops its completion/resource without a report because no response attempt + began. Adapter-level cancellation follows the target caveats in §1.3. + +Route identity is stable and structural, never a registration ordinal or randomized hash: + +```rust +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct RouteId { /* private canonical method + registered pattern */ } + +impl RouteId { + pub fn method(&self) -> &Method; + pub fn pattern(&self) -> &str; +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RouteMetadata { /* private RouteId + optional opaque class */ } + +impl RouteMetadata { + pub fn class(&self) -> Option<&str>; + pub fn id(&self) -> &RouteId; + pub fn method(&self) -> &Method; + pub fn pattern(&self) -> &str; +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum RouteResolution { + Matched(RouteMetadata), + MethodNotAllowed { allowed: Arc<[RouteMetadata]> }, + NotFound, +} +``` + +`RouteId` equality/hash is the canonical `(method, registered route pattern)` pair. Dynamic +path parameter values and route class never enter it. `RouteMetadata` owns exactly that +`RouteId` plus optional opaque manifest metadata from `class = "auction"`; its +method/pattern accessors delegate to the id, so independently mutable duplicate fields +cannot disagree. Class is for application admission policy and is neither globally unique +nor interpreted by EdgeZero. `RouterBuilder::route_with_class(path, method, class, handler)` +is the generated-route path; existing route builders produce `class() == None`. +`MethodNotAllowed.allowed` contains every matched +pattern/method candidate sorted by method then pattern; callers derive an Allow set without +depending on `HashMap` order. `RouteInfo` reuses `RouteMetadata` rather than defining a +second identity. Duplicate method/pattern registration remains the existing build-time +error. The resulting `EdgeError::MethodNotAllowed` retains a sorted, deduplicated +`Vec` rather than flattening protocol data into display text. Its canonical response +always emits the corresponding `Allow` field, including after a successful bounded fallback +body drain. An empty method set emits an empty `Allow` value; normal router-produced 405s always +contain at least one method. + +The resolution/dispatch split is an explicit router API, not an adapter-side reimplementation +of matching: + +```rust +pub struct ResolvedDispatch { /* private router identity + path parameters */ } + +impl ResolvedDispatch { + pub fn resolution(&self) -> &RouteResolution; +} + +impl RouterService { + pub fn resolve(&self, method: &Method, path: &str) -> ResolvedDispatch; + + pub async fn dispatch_resolved( + &self, + resolved: ResolvedDispatch, + request: Request, + ingress: AdmittedIngress, + ) -> Result; +} +``` + +`ResolvedDispatch` is opaque, single-use, and bound to the `RouterService` instance that +created it. `dispatch_resolved` rejects a token from another router as `Internal`, consumes +the token, and never matches the method/path again. The `Request` method and path must equal +the values captured by the token; a mismatch is also `Internal`. This prevents middleware or +adapter conversion from admitting one route and dispatching another. `AdmittedIngress` +contains the captured start, finite deadline, paired monotonic clock, and grant; it has no +public constructor so only the ingress protocol can claim admission occurred. + +`IngressGrant` is an opaque request-owned lease carrier. It is intentionally not `Clone`: + +```rust +pub struct IngressGrant { /* Option> */ } + +impl IngressGrant { + pub fn downcast(self) -> Result; + pub fn empty() -> Self; + pub fn new(value: T) -> Self; +} + +pub struct ResponseEgressCompletion { /* non-clone FnOnce callback owner */ } + +impl ResponseEgressCompletion { + pub fn empty() -> Self; + pub fn join(self, other: Self) -> Self; + pub fn new(complete: Complete) -> Self + where + Complete: FnOnce(&ResponseEgressReport) + Send + 'static; +} + +pub struct DetachedResponseEgressHead<'head> { + /* request method, request start, status, and stable error kind */ +} + +impl DetachedResponseEgressHead<'_> { + pub fn write_deadline_after(&self, duration: Duration) -> Deadline; +} + +#[non_exhaustive] +pub enum DetachedResponseEgressDecision { + Abort, + Send { + completion: ResponseEgressCompletion, + deadline: Deadline, + }, +} + +#[derive(Clone, Debug)] +pub struct BufferedIngressResponse { /* exact status + HeaderMap + Bytes */ } + +impl BufferedIngressResponse { + pub fn new>(status: StatusCode, headers: HeaderMap, body: B) -> Self; + pub fn text>(status: StatusCode, text: S) -> Self; + pub fn status(&self) -> StatusCode; + pub fn headers(&self) -> &HeaderMap; + pub fn body(&self) -> &[u8]; +} + +#[non_exhaustive] +pub enum AdmissionDecision { + Abort, + Admit { + completion: ResponseEgressCompletion, + grant: IngressGrant, + read_deadline: Deadline, + }, + ReadBodyBeforeFallback { + completion: ResponseEgressCompletion, + grant: IngressGrant, + max_body_bytes: usize, + read_deadline: Deadline, + on_exceeded: BufferedIngressResponse, + on_timeout: BufferedIngressResponse, + }, + Refuse { + completion: ResponseEgressCompletion, + response: Response, + }, +} + +#[non_exhaustive] +pub enum IngressAdmissionOutcome { + Aborted, + Admitted { + completion: ResponseEgressCompletion, + ingress: AdmittedIngress, + }, + Refused { + completion: ResponseEgressCompletion, + response: Response, + }, +} + +#[non_exhaustive] +pub enum IngressBeginOutcome { + Aborted, + Admitted(PreparedIngress), + Refused(ResponseEgressEnvelope), +} + +#[non_exhaustive] +pub enum IngressDispatchOutcome { + Aborted, + Response(Box), +} +``` + +`ResponseEgressCompletion` is never stored in `Response` extensions and is never cloneable. The +validated `IngressAdmissionOutcome` transfers it into `PreparedIngress`, which owns it beside the +grant-bearing `AdmittedIngress` and resolved router token. Immediately before router dispatch, +`App::dispatch_admitted` separates the completion from the remaining ingress proof and retains it +while the router consumes the request and grant. It then moves that exact completion into the +envelope for whichever response dispatch produces. A response replacement therefore cannot lose +or duplicate the resource. + +`ResponseEgressCompletion::join` consumes two completion owners and produces one owner. Terminal +execution invokes the left callback and then the right callback with the same borrowed +`ResponseEgressReport`, at most once. Abandoning the joined completion without a terminal report +drops both child owners and releases both sets of captured resources. On unwind-capable targets, +each child keeps an independent panic boundary, so a left-callback panic does not suppress the +right callback. A panic-abort target may terminate before the right callback runs. + +`BufferedIngressResponse` is deliberately finite and transport-neutral: it cannot carry a +streaming body or deferred provider work. `new` preserves the application-selected status, +complete header map, and exact bytes. `text` additionally supplies UTF-8 plain-text content +type and exact content length. + +The matched `RequestContext` stores the grant in an unsynchronized one-shot cell because +extractors receive `&RequestContext`. `take_ingress_grant(&self)` removes and returns it at +most once; a second call returns `None`. If application code never takes it, context drop +releases it. A refusal never creates a request-owned grant. `Abort` creates neither a grant nor a +completion. An ordinary admitted 404/405 owns the grant only until the resolved terminal response, +then drops it. The bounded fallback path +holds its grant through the body drain and drops it exactly once before converting the canonical +or application-selected terminal response. It never exposes the grant through a request context. +The carrier exposes no type-id string, serializer, or platform handle API; application code +knows the concrete lease type it inserted. + +`IngressHead::request_start()` and `RequestContext::request_start()` return the captured +`MonotonicInstant`. Both are the same value and clock domain used by `Deadline`; neither +accessor snapshots a new instant. `PreparedIngress::monotonic_clock()` and +`RequestContext::monotonic_clock()` return clones of the exact app clock handle attached at +admission. `IngressHead::route_resolution()` exposes the value above, and a matched +`RequestContext::route_metadata()` returns that exact matched metadata. + +`App::detached_ingress_error_egress(error, method, request_start)` is the adapter boundary for +normalized validation and admission-policy failures. It renders the typed error using a bounded +default response-egress policy and a no-op observer, never the application policy or observer. The +application's detached-egress decision factory sees the request method, captured request start, +response status, and stable error kind, and runs exactly once without middleware, handler, or body +polling. `Send { completion, deadline }` renders the bounded error, transfers that completion into +detached egress, and inserts the supplied deadline into the existing response-owned deadline path. +The deadline is mandatory, absolute in the request's injected monotonic-clock domain, and cannot be +used to extend the default response-egress policy; the effective bound is the earlier of the two. +`Abort` returns +`IngressDispatchOutcome::Aborted` before response construction and creates no response, envelope, +attempt, completion callback, observer call, handler call, middleware call, or body poll. Unknown +future outcome variants fail closed through the same non-response adapter boundary. For normalized +validation failures the factory runs before routing and admission; for an admission-policy error, +route resolution and the failing policy invocation have already occurred, but no decision-owned +completion exists. The default is: + +```rust +DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::empty(), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), +} +``` + +Admission-selected detached responses retain their response-owned deadline path and do not use +this factory. Raw parser failures and platform-to-core head-conversion failures that occur before +EdgeZero can construct normalized `IngressHeadParts` remain explicitly outside the portable +lifecycle, even when an adapter has already sampled request start. A failure after successful +admission but before an attempt begins drops the decision-owned completion/resource without a +report. +`App::admitted_error_egress(prepared, error)` consumes the single-use admission proof, releases +its grant exactly once, and creates an application-owned envelope with the captured request, +route metadata, and exact completion. `App::dispatch_admitted` is infallible at this boundary: +handler and routing errors are rendered into the same owned envelope instead of escaping after +admission. The higher-level `App::dispatch_ingress` returns `IngressDispatchOutcome`, so it cannot +accidentally translate `Abort` into an HTTP response. + +```rust +pub type MonotonicInstant = web_time::Instant; + +#[derive(Clone)] +pub struct MonotonicClock { /* Arc MonotonicInstant + Send + Sync> */ } + +impl MonotonicClock { + pub fn new(now: Now) -> Self + where Now: Fn() -> MonotonicInstant + Send + Sync + 'static; + pub fn deadline_after(&self, duration: Duration) -> Deadline; + pub fn now(&self) -> MonotonicInstant; +} + +impl IngressHead { + pub fn read_deadline_after(&self, duration: Duration) -> Deadline; +} + +impl App { + pub fn monotonic_clock(&self) -> MonotonicClock; + pub fn monotonic_now(&self) -> MonotonicInstant; + pub fn set_monotonic_clock(&mut self, clock: MonotonicClock); +} +``` + +`MonotonicClock::default()` uses `web_time::Instant::now`. Injection is application-wide: +standard adapters take `request_start` from the app before head conversion, admission +stores that same handle, and core plus adapter body checks use +`Deadline::{remaining_at,is_expired_at}(clock.now())`. Admission policies should construct +relative deadlines with `IngressHead::read_deadline_after`, which uses the captured start +and clamps to `DEADLINE_FAR_FUTURE`. Calling the global-clock `Deadline::after` from a policy +while a custom app clock is installed is a caller error; explicit low-level APIs likewise +cannot validate that two bare `MonotonicInstant` values came from the same source. +`DetachedResponseEgressHead::write_deadline_after` follows the same clock-domain rule, anchors the +duration to captured `request_start`, clamps it to `DEADLINE_FAR_FUTURE`, and fails closed to +`request_start` if checked addition overflows. + +`Hooks::configure(&mut App) -> Result<(), EdgeError>` is the app-owned configuration point and +uses a new +`App` admission-policy setter. Manifest-generated and hand-written apps that do not install +one use the portable default: admit with `grant = IngressGrant::empty()`, +`completion = ResponseEgressCompletion::empty()`, and +`read_deadline = request_start + DEFAULT_INBOUND_READ_BUDGET`, where +`DEFAULT_INBOUND_READ_BUDGET = 30 s`. Admission cannot return "no deadline". The adapter +normalizes every admitted deadline to no later than +`request_start + DEADLINE_FAR_FUTURE` using checked addition; overflow fails closed as an +internal policy error. An app that needs a longer upload than the default sets a later +absolute deadline within that bound. The decision is computed per request and is never +cached globally. + +The adapter retains the complete `App` admission policy when constructing its request +service. Paths that currently clone only `app.router()` must also carry that policy; +`App::into_router()` remains usable for low-level callers but does not silently install or +claim ingress admission. Hand-built adapter services expose the same explicit policy setter +and otherwise use the portable default. + +Parser limits are startup policy, not admission output: the parser must know them before it +can safely materialize `IngressHead`. `App` therefore also owns immutable +`IngressHeadLimits`, copied into the adapter service during finalization. The default is +finite on every target: + +```rust +pub const DEFAULT_MAX_REQUEST_HEADER_BYTES: u64 = 65_536; +pub const DEFAULT_MAX_REQUEST_HEADER_COUNT: u64 = 100; +pub const DEFAULT_MAX_REQUEST_TARGET_BYTES: u64 = 8_192; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct IngressHeadLimits { + max_request_header_bytes: u64, + max_request_header_count: u64, + max_request_target_bytes: u64, +} + +impl IngressHeadLimits { + pub fn max_request_header_bytes(&self) -> u64; + pub fn max_request_header_count(&self) -> u64; + pub fn max_request_target_bytes(&self) -> u64; + pub fn with_max_request_header_bytes(self, max: u64) -> Result; + pub fn with_max_request_header_count(self, max: u64) -> Result; + pub fn with_max_request_target_bytes(self, max: u64) -> Result; +} +``` + +All three values must be nonzero. Validation runs when `App` is finalized, before core request +conversion, body polling, admission, or dispatch; Axum additionally validates before listener +bind. Invalid configuration is `Internal`; it never silently means unlimited. Limits are +inclusive and use checked `u64` accounting. + +This seam starts when EdgeZero receives control. It cannot reject or time-bound bytes a +provider accepted or buffered before invoking guest code; adapter capability documentation +must state that host-side exposure rather than attributing it to the guest deadline. + +### 1.2 Raw request-head limits and framing policy + +At a raw parser boundary, request-target bytes are the exact octets between the first and +second spaces of the HTTP/1 request line, before URI normalization. Header bytes are the raw +field-section octets from the first field-name through the terminating empty line, including +colons, optional whitespace, line endings, and the final line ending. Header count is the +number of field lines before duplicate coalescing. The parser rejects on the first byte or +field above the configured value; it does not read an unbounded head and check afterward. +For multiplexed protocols, equivalent accounting is performed on the encoded field section +only when the adapter exposes a bounded pre-materialization hook. + +The raw parser returns `EdgeError::UriTooLong { message }` (414) for target overflow and +`EdgeError::RequestHeaderFieldsTooLarge { message }` (431) for header byte/count overflow. +Neither diagnostic includes target or header contents. These failures happen before route +resolution and therefore never enter `StoredError` through a body drain, but those variants +are still included in its total match. Malformed request syntax remains `BadRequest` (400). + +Admission can inspect whether the configured parser contract actually ran: + +```rust +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum IngressHeadAccounting { + HostManaged, + RawValidated { + request_header_bytes: u64, + request_header_count: u64, + request_target_bytes: u64, + }, +} +``` + +`IngressHead::head_accounting()` returns this value. `RawValidated` is emitted only when all +three values were measured before normalized request construction and found within the exact +`IngressHeadLimits` installed for that service. A normalized `HeaderMap` recount may reject +visible data as defense in depth but must report `HostManaged`; it cannot satisfy the raw +capability or recover field-line/request-target octets discarded by the platform. + +`IngressFraming` distinguishes raw-validated framing from a host-managed request whose raw +boundary EdgeZero cannot inspect: + +```rust +#[non_exhaustive] +pub enum IngressFraming { + Chunked, + ContentLength(u64), + HostManaged, + NoBody, + ProtocolManaged, +} +``` + +`NoBody`, `ContentLength`, `Chunked`, and `ProtocolManaged` are produced only after raw +validation and record an unambiguous result. `HostManaged` is required when the adapter's +`raw-ingress-framing-validation` support is `Unsupported`: it makes no assertion that +ambiguous framing was rejected and must not be treated by admission policy as trusted +length/framing evidence. No variant is reconstructed from a normalized `HeaderMap`. +For HTTP/1.x, EdgeZero rejects at the parser/connection boundary, before a body reader is +created, when any of these hold: + +- both `Content-Length` and `Transfer-Encoding` are present, in either field order; +- `Content-Length` is malformed, signed, overflows `u64`, appears more than once, or uses a + comma-list form, including repeated identical values; +- `Transfer-Encoding` is malformed, repeats `chunked`, does not end in exactly one + `chunked`, or contains a transfer coding the adapter does not implement; or +- framing is otherwise invalid for the request's HTTP version. HTTP/2 and HTTP/3 reject a + `Transfer-Encoding` field rather than treating it as HTTP/1 framing (`TE: trailers` is a + separate field and rule). + +The rejection response is 400; HTTP/1 closes the connection and multiplexed transports +reset only the affected stream. EdgeZero does not drain an ambiguously framed body and does +not invoke the admission callback. This strict duplicate-`Content-Length` policy deliberately +chooses one parser interpretation instead of accepting RFC-permitted identical duplicates. + +Axum is the first required strict raw-boundary implementation. Hyper can discard a +`Content-Length` encountered after `Transfer-Encoding`, so checking the resulting +`Request`/`HeaderMap` is insufficient and must not be described as smuggling +protection. The Axum adapter needs an audited pinned Hyper parser patch or an upstream parser +hook that rejects the strict policy while Hyper still has ordered `httparse` field lines. +The adapter now owns its HTTP/1 accept path and pinned Hyper 1.12.0 connection futures. +`http1::Builder::{max_headers,max_header_size,max_buf_size,header_read_timeout}` provide bounded +pre-admission resource controls, but they do not expose exact request-target/raw-header accounting +and do not preserve every +ambiguous field shape required by this contract; configuring those knobs alone cannot promote +either raw capability. +After that parser has rejected ambiguity, the adapter may derive `IngressFraming` from the +surviving normalized headers plus HTTP version; the derivation is trusted only because the +raw parser decision preceded it. A second independent socket pre-parser is not accepted: it +can disagree with Hyper and cannot safely locate subsequent pipelined request heads without +also owning HTTP body framing. Raw-socket tests in §4 pin the parser behavior. Platform SDKs that expose +only normalized requests cannot claim Native framing validation based on assumed host +behavior; they remain Unsupported until a documented/testable raw rejection seam exists. + +#### Implemented Axum HTTP/1 resource boundary + +`AxumIngressConfig` installs finite validated connection, combined raw-head byte, raw field-count, +and absolute idle-plus-header limits through the standard server startup path. The connection +pool owns at most the configured number of futures plus one transient accepted socket on refusal; +refusal creates no parser, response, task, or capacity waiter. The first deadline is stamped before +scheduling, subsequent deadlines start when Hyper first polls the next eligible keep-alive head, +and byte trickling does not reset them. Independent native timer wakes, I/O checks, and a completed +head check enforce expiry before application admission without replacing application body/egress +deadlines. Shutdown and cancellation drop all connection-owned state. + +The raw-head byte bound includes the request line and all field syntax together. Exact inclusive +head and field-count limits are tested through real TCP; these are not the independent +`IngressHeadLimits` measurements above. Hyper remains the sole HTTP/1 parser. HTTP/2 and socket +upgrade ownership are not enabled by this server path. + +Receive allocation bounds are audited separately from Hyper's logical buffer threshold. Response +heads are capped and copied into compact storage before Hyper caches them for later request parsing; +oversized heads close the connection without another response allocation. Configuration defaults, +ranges, precise wire definitions, trailer behavior, allocation assumptions, and exclusions are in +the [Axum adapter guide](../../guide/adapters/axum.md#bounded-http1-ingress). Real TCP acceptance +tests are colocated in the adapter's `connection.rs` and `dev_server.rs`. This implementation does +not claim strict ambiguous-framing rejection, exact independent target/header totals, kernel-buffer +bounds, or complete RSS certification. Raw capability promotion remains a future parser task. + +### 1.3 Absolute body-read deadline and cancellation + +The admitted `read_deadline` and its paired `MonotonicClock` are carried with the native +body and govern the full inbound +read lifetime: waiting for the first byte, every inter-chunk wait, drain completion, and +late EOF/error. It is not reset by routing, middleware, extractors, repeated cached reads, +or transfer into `OutboundRequest::from_request`. Before each platform read and immediately +after any chunk/EOF/error becomes ready, compare the same absolute deadline. At simultaneous +read completion and expiry, expiry wins. + +When expiry is observable at a guest boundary, the adapter drops or aborts the owned reader +with its strongest available platform primitive and yields +`EdgeError::RequestTimeout { message }` (408). If `BodyCell` is draining, its existing +capture path transitions exactly once to `Poisoned(StoredError::RequestTimeout { .. })`; +the first and every later accessor observe the same status/kind/message and the source is +never polled again. Expiry after a clean cached drain does not retroactively poison cached +bytes. Dropping a caller future without deadline expiry keeps the separate documented +`Internal("inbound body drain cancelled")` poison. + +The public deadline contract is portable; the teardown strength is not. Add four +non-outbound capability cells to the shared capability ladder: + +| Capability | Axum | Cloudflare | Fastly | Spin | +| --- | --- | --- | --- | --- | +| `ingress-admission` | Native | Native | Native | Native | +| `ingress-admission-abort` | Native | BestEffort | BestEffort | BestEffort | +| `inbound-read-deadlines` | Native | BestEffort until a deployed cancellation probe passes | BestEffort (synchronous host reads are not guest-preemptible) | BestEffort until host-observed cancellation is bounded | +| `raw-ingress-head-limits` | Unsupported | Unsupported | Unsupported | Unsupported | +| `raw-ingress-framing-validation` | Unsupported | Unsupported | Unsupported | Unsupported | + +Axum maps `Abort` to a service error from its owned Hyper HTTP/1 connection and drops the +connection before response-head construction; a raw-socket test proves EOF/reset and zero response +bytes. Cloudflare, Fastly, and Spin return the strongest fixed platform error available before core +request-body construction, but remain `BestEffort` until deployed evidence proves that the host +does not synthesize a response or retain the request. No adapter is permitted to translate `Abort` +into EdgeZero's ordinary JSON error response or invoke response-egress callbacks. + +The same adapter rule applies when the detached-egress decision factory selects `Abort` for a +normalized validation or admission-policy failure. Axum proves zero response bytes at its owned +connection boundary. Cloudflare, Fastly, and Spin expose only their fixed pre-response provider +error boundaries and make no transport-observed reset claim. + +BestEffort deadline implementations still use pre-read/post-ready checks and release ownership when +expiry is observed; they do not claim a finite bound around an uninterruptible or opaque host +call. Fastly cannot preempt its synchronous read. Spin races each pending body read against +`spin_sdk::time::sleep(remaining)` and selects the timer first, so guest code can promptly emit +408 and release its source/grant ownership even when the body stream never wakes. Spin remains +BestEffort because dropping that guest reader does not prove that host-side teardown completed +within a bounded interval. A manifest +that requires Native support fails before startup/deploy through the same capability ladder +used elsewhere. Raw head limits, raw framing, and body-read deadline are separate cells: a +platform host may impose undocumented limits or reject malformed framing without exposing +evidence, and may expose a cancellable body while hiding raw target/field-line bytes. +All current standard adapters pass `IngressHeadAccounting::HostManaged` and +`IngressFraming::HostManaged`. Axum is the first planned raw-boundary implementation, but its +two raw capability cells remain `Unsupported` until the parser-boundary implementation and +raw-socket evidence land. + +Axum can preempt its asynchronous native body read at the configured deadline, which supports +the `Native` deadline cell. Its owned Hyper HTTP/1 connection loop runs the portable non-`Send` +dispatch future on a Tokio `LocalSet`; no blocking bridge or nested runtime separates caller +cancellation from the drain. Closing the connection drops the in-flight dispatch future, body +source, and fallback grant. Deadline expiry races the native body read, returns the selected +timeout response when the connection remains writable, and releases the source and grant exactly +once. The adapter suite covers pending reads, cancellation/drop, and deadline precedence. + +Cloudflare performs one `Delay::from(Duration::ZERO).await` cooperative yield before +selecting the body read against its host timer. A deliberately frozen injected clock means +clock-based post-ready checks cannot independently observe elapsed time; the real host timer +still uses the last positive `remaining_at` snapshot. This is deterministic test support, +not a claim that production time can freeze. Native promotion requires one deployed timing +probe that records runtime version, configured deadline, observed cancellation latency, and +tolerance; local mocked timers are insufficient. + +Spin likewise creates its platform timer from the last positive `remaining_at` snapshot and +keeps the post-ready check against the admitted app clock. Its WASI contract suite injects a +deterministic pending-then-ready timer to prove timer-first selection for permanently pending +matched and fallback sources, exact 408/application timeout responses, and exactly-once source +and grant release. The standard WASI test harness cannot poll the provider clock import outside +an exported component task, so the production `spin_sdk::time::sleep` wiring is compile-checked +locally and still requires the deployed host-observed timing probe before Native promotion. + +## 2. Bounded Context Helpers + +Wrap the existing `Body::into_bytes_bounded` with context-level helpers: + +```rust +// crates/edgezero-core/src/context.rs +impl RequestContext { + /// Read the inbound request body into `Bytes`, bounded by `max`. + /// Over-limit yields `Err(EdgeError::PayloadTooLarge { origin: Inbound })` (413). + /// A single normalized Content-Length above the cap rejects without polling and remains + /// reusable at a larger cap. An already-expired read deadline wins with 408. + /// + /// **Takes `&self`** — `RequestContext` carries an internal body cache + /// (an `unsync::OnceCell` style cell; single-threaded + /// request, no `tokio` dep). This is deliberate so that existing + /// `FromRequest` extractors that take `&RequestContext` (e.g. `Json`, + /// `ValidatedJson`) can call it without a trait-signature breaking + /// change. The first call drains the underlying `Body::Stream` into + /// the cell; later calls return a cheap clone. The cached size is + /// re-validated against `max` on every call, so a later, stricter cap + /// is still enforced after buffering. The network body is read at most + /// once. + #[inline] + pub async fn body_bytes(&self, max: usize) -> Result; + + /// Call `body_bytes(max)` then deserialize as `application/x-www-form-urlencoded`. + /// Default cap from extractors: `DEFAULT_INBOUND_FORM_BYTES = 1 MiB` + /// (forms are typically small). Malformed form data → `bad_request` (400). + /// Same `&self` cache semantics as `body_bytes`. + #[inline] + pub async fn form_within(&self, max: usize) + -> Result; + + /// Call `body_bytes(max)` then deserialize as JSON. Malformed inbound + /// JSON yields `Err(EdgeError::bad_request(..))` (a client bug → 400, + /// in contrast to outbound `OutboundResponse::json` which maps to 502). + /// Same `&self` cache semantics as `body_bytes`. + #[inline] + pub async fn json_within(&self, max: usize) + -> Result; +} +``` + +## 3. Context and Adapter Migration + +The memory guarantee in §3.1 only holds if the adapter does not pre-buffer the +inbound request body before core can apply a cap. Today every adapter pre-buffers +(`crates/edgezero-adapter-axum/src/request.rs:24` buffers JSON with `usize::MAX`; +`crates/edgezero-adapter-cloudflare/src/request.rs:60` calls `req.bytes()`; +the Fastly and Spin paths fully materialize the body too). This migration changes that: + +- **Adapter request conversion** stops pre-buffering. Inbound `Request` is exposed to + core with a `Body::Stream` (or `Body::Once` only when the platform genuinely owns + the bytes already — e.g. an in-process Axum body that arrived buffered). Each + adapter's `request.rs` is updated to wrap the platform body as a stream rather than + drain it eagerly. +- **`RequestContext` is restructured** — today it holds a plain `Request`, which cannot + be safely mutated through `&self`. The new shape: + + ```rust + struct BodyCell(/* unsync */ RefCell); + + /// Non-consuming snapshot of cell state for app inspection. + pub enum BodyKind { + Cached { len: usize }, + Draining, + Initial, + Poisoned, + Taken, + } + + enum BodyState { + Cached(Bytes), // body drained successfully + Draining, // body taken out, drain in progress + Initial(Body), // never read; the platform body is still owned + Poisoned(StoredError), // drain failed (over-cap, stream error, drop) + Taken, // body consumed via take_body / into_request + } + + pub struct RequestContext { + body: BodyCell, // interior-mutable + parts: http::request::Parts, // method, uri, version, headers, extensions + path_params: PathParams, + } + ``` + + **`StoredError` — why the cell cannot just store an `EdgeError`.** The poison + contract requires that *every* subsequent access (`body_bytes`, `json_within`, + `form_within`, `into_request`) returns **the same error**. That needs the error to be + reproducible — but **`EdgeError` is not `Clone`**: its `Internal` variant wraps + `anyhow::Error` (`error.rs`: `Internal { #[from] source: AnyError }`), which is + deliberately not clonable. Storing an `EdgeError` and handing out copies therefore + does not compile. `StoredError` is the clonable, reconstructable **essence** of the + error that poisoned the cell: + + It **must be a variant-specific snapshot enum, NOT `{ kind, message }`** — that flat + shape cannot rebuild `EdgeError` faithfully, on two counts a compiler forces: + (a) `EdgeError::Internal`'s `message()` already renders as `"internal error: {source}"`, + so rebuilding via `internal(anyhow!(message))` **doubles the prefix**; (b) + `ConfigOutOfDate` (`field_path`), `MethodNotAllowed` (`method`, `allowed`), and + `NotFound` (`path`) carry structured payloads a single `message` string cannot hold. So + `StoredError` mirrors the variants and captures each payload: + + The [blob-app-config design](2026-06-16-blob-app-config.md) §6.3 classification is + finalized. `StoredError` mirrors its reason-bearing `StoreExtraction` variant and must + continue to mirror every future `EdgeError` payload before this state machine lands. + + ```rust + #[derive(Clone)] + enum StoredError { + BadGateway { message: String, reason: BadGatewayReason }, + BadRequest { message: String }, + PayloadTooLarge { origin: RequestBodyOverflowOrigin }, + ConfigOutOfDate { field_path: String, message: String }, + GatewayTimeout { cause: BudgetSource, message: String }, // preserve typed cause + Internal { rendered: String }, // ALREADY-rendered source; no re-prefixing + MethodNotAllowed { allowed: Vec, method: Method }, + NotFound { path: String }, + NotImplemented { message: String }, + RequestTimeout { message: String }, // admitted inbound read deadline expired + ResponseTooLarge { message: String, reason: ResponseLimitReason }, + ServiceUnavailable { message: String }, + StoreExtraction { + field_path: Option, + message: String, + reason: StoreExtractionReason, + }, + Validation { message: String }, + } + + impl StoredError { + /// Capture an EdgeError's essence at poison time (total match — cannot silently + /// drop a variant). For `Internal`, store `source.to_string` (already rendered), + /// NOT `err.message`, so reconstruction does not re-add the "internal error: " + /// prefix. + fn capture(err: &EdgeError) -> Self { /* one arm per variant */ } + /// Rebuild an equivalent `EdgeError` — same variant, same fields, same status. + /// `Internal { rendered }` → `EdgeError::internal(anyhow!(rendered))`. + fn to_edge_error(&self) -> EdgeError { /* inverse of capture */ } + } + ``` + + Application-owned error rendering is installed once with + `App::set_error_response_renderer(Fn(EdgeError) -> Response)`. The synchronous callback runs + exactly once for handler/routing failures, failures converted with `admitted_error_egress`, and + detached normalized-ingress failures. It permits a deployment to return a fixed-size body + without allocating the framework's canonical JSON payload. EdgeZero applies protocol-required + status and metadata after the callback, so a custom renderer cannot reinterpret the error status + (the callback's returned status is ignored) and a custom 405 renderer cannot remove or replace + the canonical `Allow` field. The same centralized mandatory-header policy preserves + `Retry-After: 60` for every effective `config_out_of_date` outcome after custom rendering. + Typed body-cap overflow has fixed public text and status 413; its inbound/upload origin is + inspectable only in Rust. A private response marker suppresses `Retry-After` after custom + rendering and again during final egress preparation, without stripping upstream 413 retry headers. + Explicit + `AdmissionDecision::Refuse` responses and the `on_exceeded`/`on_timeout` fallback responses + bypass the renderer because the application supplied those complete responses directly. Raw + parser failures before EdgeZero receives a request remain outside the application lifecycle. + + **Decomposition happens once, at poison time.** The drain's `EdgeError` is captured + into `StoredError` and the cell returns `stored.to_edge_error()` — so *even the first* + read gets a reconstructed error, and all later reads are identical. Every accessor's + signature stays `Result<_, EdgeError>` (no `Rc` leaking into the public API). + + **Documented loss:** for the `Internal` variant the **`anyhow` source chain and + backtrace are not preserved** — only the rendered string. A reconstructed `internal` + error's `inner()` yields a fresh `anyhow::Error` carrying that string, not the original + chain. Accepted trade: the alternatives are `EdgeError: Clone` (impossible without + dropping `anyhow`) or `Rc` on every accessor (an API wart for a + diagnostic-only benefit). A platform stream that needs the original chain for + diagnostics must log at the stream-error production boundary, before yielding the typed + error. *(A `BodyCell` drain only ever produces `bad_request` / `bad_gateway` / + `gateway_timeout` / `request_timeout` / `internal`; the other structured variants are + still covered so the enum is total and `capture` never needs a lossy fallback arm.)* + + **Cancelled drain.** A drain future dropped while `Draining` transitions the cell to + `Poisoned(StoredError::Internal { rendered: "inbound body drain cancelled".into() })` + via a drop guard (§4), so a cancelled read is indistinguishable in shape from any + other poison — the next access returns that stored error rather than silently + re-reading a half-consumed body. + + `RefCell` (unsync) is fine because a `RequestContext` is owned per-request and + EdgeZero's async traits already use `?Send`. No `tokio` dependency in core. + + **Construction contract — `RequestContext::new(Request, PathParams)` is PRESERVED.** + `parts` and `body` are **private**, and `BodyCell` / `BodyState` are **not public + types**. Adapters therefore do **not** — and cannot — construct the context from + "parts + a body cell"; earlier drafts said they should, which both leaks an internal + type and misassigns ownership (adapters build a `Request`; the **router** builds the + `RequestContext`). The existing signature is kept verbatim: + + ```rust + impl RequestContext { + #[inline] + pub fn new(request: Request, params: PathParams) -> Self { + Self::new_low_level(request, params, MonotonicInstant::now()) + } + + #[inline] + pub fn request_start(&self) -> MonotonicInstant; + + #[inline] + pub fn monotonic_clock(&self) -> MonotonicClock; + + #[inline] + pub fn route_metadata(&self) -> Option<&RouteMetadata>; + + #[inline] + pub fn take_ingress_grant(&self) -> Option; + } + ``` + + So the migration is **source-compatible for every caller of `new(..)`** — adapters + and the router keep passing a `Request` exactly as they do today, and the + parts/body split becomes an implementation detail. For this low-level constructor, + `request_start` is snapped when `new` is called, `route_metadata()` returns `None`, and + `take_ingress_grant()` returns `None`. That path makes + no `ingress-admission` claim. Resolved router dispatch uses a crate-private constructor + that consumes `AdmittedIngress` and installs its original start, matched metadata, and + grant before middleware. What adapters *do* change is + **what they put in that `Request`**: a lazy `Body::Stream` instead of a + pre-buffered body (first bullet above). `BodyCell` never appears in any public + signature; the only new public surface is the accessor set (`parts()`, + `parts_mut()`, `body_kind()`, `body_bytes`, `json_within`, `form_within`, + `take_body`, `into_request`). + + **Async drain protocol.** A naive "borrow_mut across .await" implementation would + panic on reentrant access or hold the borrow indefinitely if the future is dropped + mid-drain. The implementation is therefore: + + 1. Briefly borrow the cell, `mem::replace` the state with `Draining` while taking + ownership of the `Body`, drop the borrow. (No borrow held across any `.await`.) + 2. Drive the async drain on the owned `Body`. A drop guard wraps the drain such + that, on success, the cell is set to `Cached(bytes)`; on stream error or cap + overflow, the cell is set to `Poisoned(stored_err)`; on **future-cancellation** + (the drain future is dropped), the guard's `Drop` sets the cell to + `Poisoned(StoredError::cancelled())`. The network body is partially consumed and + unrecoverable in every failure case — poison is sticky. + 3. While the cell is in `Draining`, any reentrant `body_bytes` / `json_within` call + observes that state and returns `Err(EdgeError::internal("body read already in + progress"))` rather than panicking; this would only occur in programmer-error + scenarios but must not crash the host. + + Tested in §4: a scripted stream first returns `Pending`, allowing a second accessor to + observe `Draining`; the second call returns `internal` without a panic, then the first + drain is either resumed to success or dropped to exercise cancellation poison. + +- **Public methods become coherent with the cache.** Their post-cache behaviour is + explicit so middleware → handler → proxy-forward chains compose: + + | Method | Behaviour | + | --- | --- | + | `method()` / `uri()` / `headers()` / `extensions()` | from `parts` — unaffected by body state | + | `headers_mut()` / `extensions_mut()` | mutates `parts` — unaffected by body state | + | `parts() -> &http::request::Parts` / `parts_mut() -> &mut http::request::Parts` | direct access to the underlying `Parts` for middleware that needs the full snapshot; same body-state-irrelevance as the granular accessors above. These are the migration target for call sites currently doing `ctx.request()` / `ctx.request_mut()` (§5 sweep). | + | `body_kind() -> BodyKind` | a non-consuming snapshot of the cell state — variants enumerated above (`Initial \| Draining \| Cached { len } \| Poisoned \| Taken`). There is **no** `body() -> &Body` / `body() -> Body` accessor — a `&Body` reference cannot span the cell's interior mutability, and a value-returning getter would either consume the stream (single-shot) or require a tee. Callers either buffer via `body_bytes`/`json_within` or consume via `take_body`/`into_request`. | + | `take_body() -> Result` | consume the body out of the context: `Initial` → `Ok(Body::Stream(..))`, set state to `Taken`; `Cached(bytes)` → `Ok(Body::Once(bytes))`, set state to `Taken`; `Draining` → `Err(EdgeError::internal("body read in progress"))` (programmer error); `Poisoned(err)` → `Err(err.to_edge_error())`; `Taken` → `Ok(Body::empty())`. After a successful `take_body`, the body cannot be re-read or buffered. | + | `body_bytes(max)` / `json_within(max)` / `form_within(max)` | from `Initial`: drains → `Cached`, returns clone (or → `Poisoned(err)` on drain failure, then returns that error). From `Cached`: re-validates `max` and returns a clone. From `Poisoned`: returns a fresh `EdgeError` reproduced from the stored error. From `Draining`: `Err(EdgeError::internal("body read in progress"))` — programmer error. From `Taken`: `Err(EdgeError::internal("body already consumed via take_body"))` — buffered helpers cannot resurrect a body that was handed out. | + | `into_request() -> Result` | reassembles a `Request` from `parts` + the cell's body via the same rules as `take_body`: `Cached` → `Ok(Body::Once(bytes))`, `Initial` → `Ok(Body::Stream(..))`, `Draining` → `Err(EdgeError::internal("body read in progress"))` (programmer error), `Poisoned(err)` → `Err(err.to_edge_error())` — **not** `Body::empty()`, because a poisoned read silently turning into an empty proxy-forward would violate the "poison is sticky" rule below, `Taken` → `Ok(Body::empty())` (the caller consumed via `take_body`, the empty is intentional). This is what `OutboundRequest::from_request(ctx.into_request()?, uri)?` uses, so streaming proxy-forward still works **even after middleware has buffered the body** (the cached `Bytes` flow through), and a permissive proxy-forward cannot mask a stricter middleware's poisoned read. | + + The legacy `request()` / `request_mut()` accessors are removed (they leaked the + whole `Request` and made the body cell incoherent); call sites switch to + `parts()` / `parts_mut()` for headers/method/uri/extensions, `body_kind()` for + state inspection, `body_bytes(max)` / `json_within(max)` for buffered consumption, + `take_body()` for one-shot consumption, and `into_request()` for proxy-forward + reassembly. + +- **Poison semantics on failed body reads.** If `body_bytes` fails mid-drain — the cap + is exceeded, the stream errors, or a future cancellation interrupts the drain — the + network body has already been partially consumed and cannot satisfy any later call. + The body cell transitions to `Poisoned(stored_err)`, where `stored_err` is enough + metadata to reproduce a fresh `EdgeError` on every subsequent call (since `EdgeError` + is not `Clone`). All later `body_bytes`/`json_within` calls return that error; + `body_kind()` reports `Poisoned`; `take_body()` and `into_request()` both return + `Err(stored)` — the latter explicitly fallible so a poisoned read cannot silently + become an empty proxy-forward. The network body is **not** + retried. This is the most defensible contract: silently re-reading is impossible, and + silently succeeding with a larger-cap call would let a permissive extractor mask a + stricter middleware's enforcement. The poisoned error variant matches the first + failure (e.g. an over-cap drain returns `bad_request` on call N+1 too). + +- **Existing extractors.** All extractors that consume the inbound body are migrated to + the bounded helpers: + + | Extractor (today) | After migration | + | --- | --- | + | `Json` (uses `ctx.json()`, assumes buffered body) | delegates to `ctx.json_within(DEFAULT_INBOUND_JSON_BYTES)` — `DEFAULT_INBOUND_JSON_BYTES = 8 MiB` | + | `ValidatedJson` | as above + `validator` pass; sibling `ValidatedJsonWithin` for explicit caps | + | `Form` (uses `ctx.form()`, also rejects streams today — `crates/edgezero-core/src/extractor.rs:375`, `crates/edgezero-core/src/context.rs:31`) | delegates to a new `ctx.form_within(max)` helper, default `DEFAULT_INBOUND_FORM_BYTES = 1 MiB` (forms are typically small) | + | `ValidatedForm` | as above + `validator` pass; sibling `ValidatedFormWithin` for explicit caps | + + The legacy `RequestContext::json()` and `RequestContext::form()` are removed; both + required `Body::Once` and would break once adapters stop pre-buffering. + +- **Extractor trait.** No change required — `FromRequest::from_request(&RequestContext, + ..)` continues to take `&RequestContext`, which works because `body_bytes` is now + `&self`-callable through the cache. + +Net effect: per-inbound-body memory is bounded at the boundary of the bounded helper +that actually reads the body; failed reads are sticky so a permissive caller cannot +silently bypass a stricter one; streaming proxy-forward works whether or not middleware +already buffered the body. The memory cap is independent of §1.3's time bound: both apply +to the same first drain, so an under-cap slow trickle still expires. + +**Sticky poison is scoped to READ/DRAIN failures, NOT cap-rechecks on an already-`Cached` +body — stated to remove an apparent contradiction.** Once the body is `Cached { bytes }`, +a `body_bytes(cap)` call is a **stateless length check** (`bytes.len() <= cap`) that does +**not** mutate the cell: an over-cap result returns an error but leaves the state `Cached`, +so a later `body_bytes(larger_cap)` where the cached length fits **legitimately succeeds**. +This is intended and is **not** a violation of stickiness: stickiness governs a body that +was **consumed/failed while draining** (`Initial → Draining → Poisoned`) — there the cell +is poisoned and every subsequent access (any cap) returns the stored error. The rule +"a permissive caller cannot bypass a stricter one" is about a **poisoning drain**, not +about re-reading an intact cache at different caps. (The security property still holds: +the *first* reader that actually drains sets the cache/poison; a cap check against an +existing cache reveals nothing a caller couldn't compute from the already-materialized +bytes.) §4 pins this: permissive read (caches) → stricter `body_bytes` (over-cap error, +cell stays `Cached`) → permissive retry (succeeds) — asserting the stricter failure does +**not** poison an intact cache. + +### 3.1 Memory Bound + +*(Moved here from the outbound spec's §3.4.4 batch-memory model — this is the inbound-body +half; the outbound spec keeps only the per-response and batch terms.)* + +- **Per-inbound-body.** *Persistent* memory — the cached `Bytes` after a successful drain — + is bounded by the `max` passed to `body_bytes(max)` / `json_within(max)` / + `form_within(max)`. *Transient* worst-case during the drain is the same shape: + `max + current_chunk.len()`, with the in-flight chunk source-controlled. Outbound's + `OutboundResponse::into_bytes_bounded` mirrors this same accounting. + +### 3.2 Extractor Migration + +*(Moved here from the outbound spec's §7 file-by-file migration — this is inbound-only.)* + +- `src/extractor.rs` — extractor migration: `Json` / `ValidatedJson` route through + `ctx.json_within(DEFAULT_INBOUND_JSON_BYTES)`; `Form` / `ValidatedForm` route + through `ctx.form_within(DEFAULT_INBOUND_FORM_BYTES)`; add `ValidatedJsonWithin` + and `ValidatedFormWithin` for explicit caps. Constants exposed: + `pub const DEFAULT_INBOUND_JSON_BYTES: usize = 8 * 1024 * 1024;` and + `pub const DEFAULT_INBOUND_FORM_BYTES: usize = 1024 * 1024;`. + +## 4. Test Plan + +Core tests use scripted local streams and `futures::executor::block_on`; they require no +platform runtime or network. + +| Surface | Required assertions | +| --- | --- | +| Bounded drain | `Body::Once` and multi-chunk streams succeed at and below the cap; the first byte above the cap returns `bad_request` without unchecked accounting overflow. | +| Successful cache | The platform stream is polled only once; repeated reads clone the same bytes. A permissive read followed by a stricter cached read returns an over-cap error while leaving the cell `Cached`; a later permissive read succeeds. | +| Failed drain | Source error and initial-drain cap overflow transition to `Poisoned`; every later buffered accessor, `take_body`, and `into_request` reconstructs the same variant, status, message, and structured fields. The source is never polled again. | +| Parser-level head limits (future acceptance criterion) | Exact-limit request targets, header bytes, and header counts pass at a raw parser boundary. One byte/field over rejects before route resolution, admission, request construction, or body polling with 414/431 and redacted diagnostics. Raw accounting includes duplicate field lines and framing syntax; checked overflow fails closed. Current host-normalized recounts never report `RawValidated` and do not satisfy this row. | +| Admission ordering and route identity | Raw validation runs first where available. Route resolution then runs exactly once without middleware or body polling, and the callback sees the resulting stable `RouteResolution`. Admission runs exactly once before resolved dispatch/body polling. A matched dispatch uses the admitted route and path parameters without rematching; method/path mutation or a foreign token fails closed. Route class reaches matched and 405 metadata but never changes `RouteId`. Refusal polls no body, invokes no middleware/handler, terminates the native reader, preserves the chosen response, and carries its completion through detached egress. `Abort` polls no body, creates no response/attempt, invokes no middleware/handler/completion/observer, and reaches the adapter abort boundary. The default policy supplies empty grant/completion values and one finite deadline derived from `request_start`. | +| Detached normalized-error egress | Every factory-selected `Send { completion, deadline }` carries a mandatory absolute deadline in the request's injected monotonic-clock domain. `write_deadline_after` anchors to `request_start`, clamps to `DEADLINE_FAR_FUTURE`, and fails closed to `request_start` on overflow; the default combines an empty completion with `DEFAULT_RESPONSE_WRITE_BUDGET` from request start. The effective bound is the earlier of the factory deadline and default response-egress policy, so neither can extend the other. Joined completions run left then right with the same borrowed report at most once, release both resources on abandonment, and isolate each child panic only on unwind-capable targets; panic-abort may stop before the right callback. Factory `Abort` creates no response or attempt and invokes no completion, observer, handler, middleware, or body poll. Admission-selected detached responses keep their response-owned deadline path outside this factory contract. | +| Bounded fallback precedence | For pre-resolved 404/405 requests, ordinary `Admit` retains no-poll routing behavior. Opt-in `ReadBodyBeforeFallback` holds the application grant while draining a lengthless or chunked body to EOF under one absolute deadline: exact cap preserves the canonical 404/405, the first byte over returns the exact `on_exceeded` status/header/body response first, and expiry returns the exact `on_timeout` response first. The grant is live during every poll and drops exactly once before response conversion, when the core drain future is dropped, or when the finite deadline terminates it. The completion survives each response choice and settles exactly once at egress; it is not exposed through `RequestContext`. No path rematches or invokes middleware, a handler, or `RequestContext`; `Refuse` and `Abort` remain zero-read; selecting fallback drain for a matched route fails before body construction and drops the never-started completion without a report. Every adapter suite covers both 404 and 405 exact-cap cases plus exact custom terminal responses at its honestly reported deadline capability. | +| Request ingress metadata | `IngressHead` and matched `RequestContext` expose the same captured `request_start`, paired app clock, and route metadata. `take_ingress_grant()` returns the non-clone grant exactly once, then `None`; untaken, refused, 404, and 405 grants each drop exactly once. The preserved low-level context constructor exposes no route and makes no admission claim. | +| Absolute read deadline | A 1-byte-per-step under-cap stream cannot extend its lifetime: first-byte, inter-chunk, EOF, and source-error races use one absolute deadline and the admitted app clock; expiry wins simultaneous readiness, cancels native ownership, and poisons a draining cell as `request_timeout` (408). A manually advanced injected clock proves the tie behavior without wall-clock sleeps. Spin additionally proves that a timer wake interrupts a permanently pending matched or fallback source, preserves the correct terminal response, and releases source/grant ownership exactly once. Cached success is not retroactively poisoned. | +| Cancellation | A stream held at `Pending` leaves the cell `Draining`; dropping the first `body_bytes` future transitions it to the documented cancellation poison, and the next access returns that stored internal error. | +| Reentrancy | While the first scripted drain is pending, a second body accessor returns `internal("body read already in progress")` without a `RefCell` panic. Resuming the first future can still complete and cache bytes. | +| Consumption | `take_body` and `into_request` cover Initial, Cached, Draining, Poisoned, and Taken. Initial preserves a stream; Cached reassembles `Body::Once`; Taken deliberately produces an empty body only where specified. | +| Extraction | JSON/form success, default caps and explicit `Within` caps share one cache. Overflow is typed 413; malformed input remains 400 and validator failures remain 422. Consumed overflow is sticky, while early declared-length and stricter cached-cap rejections remain reusable. | +| Stored errors | `StoredError::capture` exhaustively covers all end-state `EdgeError` variants. Round-trips preserve variant fields, including `BadGatewayReason`, `BudgetSource`, `ResponseLimitReason`, and `StoreExtractionReason` plus its optional field path; `RequestTimeout` remains 408; `Internal` has the one documented source-chain loss without duplicating the `internal error:` prefix. | +| Raw HTTP/1 framing (future acceptance criterion) | Axum raw-socket cases cover CL+TE in both orders, duplicate equal/unequal CL, comma-list CL, overflow/signed/malformed CL, repeated/non-final/unsupported TE, valid single CL, and valid terminal chunked framing. Rejections return 400, close the connection, invoke no admission callback, and poll no body. A `HeaderMap`-only unit test is insufficient, and no current adapter satisfies this row. | + +Each adapter contract test supplies a body stream whose first poll is observable and +asserts that raw validation, when supported, route resolution, and admission complete before +that poll, while middleware and handler dispatch do not begin until after admission. Every +current standard adapter, including Axum, passes `IngressHeadAccounting::HostManaged` and +`IngressFraming::HostManaged`; their tests prove admission cannot accidentally upgrade those +values to a validated claim from normalized headers. A future Axum raw-boundary implementation +may pass validated variants only after the parser-boundary and raw-socket suite land. The same adapter test +then passes the converted core request through `RequestContext`, buffers it as middleware +would, and reassembles it through `into_request`; the outbound-facing request receives the +cached bytes unchanged and retains the original absolute read deadline. Tests must not +substitute an already-buffered body for this lazy ingress assertion. Cloudflare and Spin +need deployed/host-observed cancellation probes before upgrading their BestEffort cells; +Fastly tests cooperative checks without asserting preemption of a synchronous host read. Axum +tests native deadline preemption separately from outer Tower-task cancellation, which cannot +interrupt its current blocking bridge and therefore relies on the finite read deadline for +eventual release. + +## 5. File-by-File Change Summary + +- `crates/edgezero-core/src/context.rs`: split requests internally into private parts plus + `BodyCell`; add the state machine, bounded helpers, body-state accessors, `take_body`, and + fallible `into_request`; store the captured request start, optional route metadata, and + one-shot ingress grant; carry the admitted clock into every body deadline check; remove + whole-request borrow accessors. +- `crates/edgezero-core/src/app.rs`: add the synchronous, body-blind admission policy and + immutable `IngressHeadLimits` plus the injectable `MonotonicClock` to `App`, its default + finite inbound read budget, the normalized ingress-head/decision/accounting types, and the + response-or-abort dispatch result. Carry each decision-owned completion through prepared, + refused, fallback, error, and normal response paths without response extensions. + The macro uses + `Hooks::configure(&mut App) -> Result<(), EdgeError>` as the app-owned setter point, and + the core-owned `App::build::(platform)` prevents request conversion, admission, body polling, + or dispatch when startup policy is invalid. Axum also fails before listener bind; Cloudflare, Fastly, and Spin + enforce this boundary per host invocation. +- `crates/edgezero-core/src/error.rs`: add `RequestHeaderFieldsTooLarge { message }` with + status 431 and kind `request_header_fields_too_large`, `RequestTimeout { message }` with + status 408 and kind `request_timeout`, and `UriTooLong { message }` with status 414 and + kind `uri_too_long`; update every exhaustive match and wire-shape matrix. +- `crates/edgezero-core/src/manifest.rs`, `crates/edgezero-core/src/router.rs`, and + `crates/edgezero-adapter/src/registry.rs`: add optional trigger `class`, carry it as + non-identity route metadata, add the five inbound capability cells, and retain fail-closed + required-capability handling. Keep these counts separate from the outbound design's + eight-cell tuple. +- `crates/edgezero-core/src/extractor.rs`: route JSON/form extractors through bounded + helpers, add explicit-cap variants, and add the two public default constants. +- `crates/edgezero-core/src/body.rs`: retain the core bounded-drain primitive used by the + context and use checked pre-append accounting if it has not already landed through the + outbound body work. +- `crates/edgezero-adapter-{axum,cloudflare,fastly,spin}` request/dispatch services: stamp + `request_start`, apply normalized defense-in-depth checks while reporting the raw boundary + as host-managed, resolve once, and run app admission before middleware/handler dispatch or + body polling; stop eager collection; dispatch with the opaque resolved token; map the explicit + abort outcome to the strongest non-response platform boundary; and wrap each + platform request body with its absolute deadline and strongest available read-ownership + primitive. Keep `Body::Once` only when the platform already owns bounded bytes and admission + has already run. +- Future `crates/edgezero-adapter-axum` server connection work: enforce raw request-target/header + limits and validate raw HTTP/1 field lines before Hyper can discard framing evidence; + replace the current `axum::serve` ownership boundary, preserve HTTP/1 keepalive/pipelining, + HTTP/2, upgrades, connect metadata, and graceful shutdown, and reject/close on overflow or + ambiguity. Hyper builder limits are defense in depth, not the exact enforcement seam. The normalized + `Request` conversion is not the enforcement point. +- `crates/edgezero-core` call sites and tests: migrate `request()` / `request_mut()` users + to parts or body-specific accessors and update the now-fallible `into_request()` calls. +- Adapter contract and host tests: prove admission ordering, one absolute deadline, + target-specific cancellation strength, and the §1.2 raw-framing behavior/capability. + No outbound send or provider error-classification behavior is owned by this + specification. diff --git a/docs/superpowers/specs/2026-09-08-response-egress-design.md b/docs/superpowers/specs/2026-09-08-response-egress-design.md new file mode 100644 index 000000000..6ce398e58 --- /dev/null +++ b/docs/superpowers/specs/2026-09-08-response-egress-design.md @@ -0,0 +1,988 @@ +# EdgeZero Response Egress Design + +> **Status:** Approved implementation contract for provider-owned response lifetimes. +> **Owner:** Core response lifecycle plus adapter-owned transport boundaries. +> **Migration:** Atomic hard cut. No buffered compatibility path is retained. + +## 1. Scope and boundary + +This design owns the lifetime after router dispatch returns a core `Response` and before the +adapter has either reached its strongest observable completion boundary or terminally aborted +the response. It covers one absolute response-write deadline enforced to the adapter's +declared capability boundary, demand-driven body production, client cancellation where +observable, post-commit abort behavior, payload-byte accounting, one terminal report, one +response-scoped completion slot, and one global observer notification. + +It does not extend an outbound fetch deadline, classify upstream failures, or change outbound +response decoding. Those end when the outbound adapter returns an `OutboundResponse`, as +defined by the [outbound HTTP design](2026-05-21-outbound-http-design.md). Inbound request +admission and body-read deadlines are owned by the +[inbound-body design](2026-08-22-inbound-body-design.md). + +Provider-owned buffers, kernel buffers, TCP acknowledgement, peer receipt, and complete +provider/runtime allocation accounting remain outside this contract. Every capability claim +names its exact observable boundary and must not imply one of those excluded guarantees. + +This design replaced the historical converter-owned response lifetime, where Axum, Fastly, +and Spin materialized `Body::Stream` and Cloudflare reported terminal state before its actual +JavaScript writer boundary. The current adapters use the owned coordinators in §5. Returning a +response object is not an egress completion event. + +## 2. Design choice + +Each adapter owns a runtime-specific body pump and transport lifetime. Core supplies the +portable clock, policy, immutable response metadata, and exactly-once attempt guard. There is +no cross-adapter writer or sink trait: Hyper polling, Workers streams, synchronous Fastly +handles, and WASI stream writers have materially different progress and cancellation models. + +Merely replacing collection with each SDK's convenient stream wrapper is insufficient. Such +wrappers hide one or more of deadline wakeup, accepted-byte accounting, consumer cancellation, +finish, or abort. The standard EdgeZero entrypoint must retain ownership through the strongest +boundary the provider exposes. + +One adapter-local coordinator exclusively owns each `ResponseEgressAttempt`. Bodies, timers, +request signals, connection supervisors, and platform writers submit events to that owner; +they do not own independent attempt guards. The coordinator serializes competing terminal +events and releases all source and transport resources after the first terminal transition. + +Every final client response produced by an admission decision uses the owned transport path, +including successful handlers, handler errors, framework 404/405 responses, bounded fallback +responses, and admission refusals. Errors that arise after `PreparedIngress` exists but before +normal dispatch completes consume that proof through `App::admitted_error_egress`; the envelope +retains the app clock, request metadata, policy, observer, response-scoped completion, deadline, +and exactly-once terminal semantics. +An adapter must not turn source construction, core request conversion, or dispatch failure into a +provider error or detached response after this boundary. + +An admission refusal uses detached ingress policy and a no-op global observer, but carries the +response-scoped completion supplied by that refusal. Normalized head-validation failures and +admission-policy errors invoke the application's detached-egress decision factory. The default +factory returns: + +```rust +DetachedResponseEgressDecision::Send { + completion: ResponseEgressCompletion::empty(), + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), +} +``` + +The factory receives only bounded failure metadata and runs exactly once before any envelope is +created; it does not run middleware, the handler, the application egress policy, or the global +observer. +`Send { completion, deadline }` uses the same detached transport path with that completion and a +mandatory absolute deadline in the request's injected monotonic-clock domain. It cannot extend the +default response-egress policy because `begin()` enforces the earlier bound. `Abort` creates no +response, envelope, attempt, completion callback, observer call, handler call, middleware call, or +body poll and reaches the adapter's existing non-response abort/error boundary. Normalized +validation invokes the factory before routing and admission; a policy error invokes it after route +resolution and the failing policy callback. Admission-selected detached responses, including +refusals, retain their existing response-owned deadline path and are outside this factory contract. +`AdmissionDecision::Abort` produces no response, starts no egress attempt, and invokes neither +completion nor observer. Host/parser-generated responses and platform-to-core head-conversion +failures emitted before EdgeZero can construct a normalized head remain outside this lifecycle, +even when request start was already sampled. They are governed by the raw-ingress and adapter +conversion contracts. + +## 3. Core contract + +### 3.1 Policy and timing + +`ResponseEgressEnvelope` retains the exact `App::monotonic_clock()` clone used for ingress and +standard outbound dispatch. `begin()` snapshots `egress_started_at` from that clock immediately +before its first response-conversion operation and returns the same clock with the response, +policy, and attempt. The adapter obtains one finite absolute deadline: + +```rust +pub const DEFAULT_RESPONSE_WRITE_BUDGET: Duration = Duration::from_secs(30); + +#[derive(Clone, Copy, Debug)] +pub struct ResponseEgressDeadline { /* private */ } + +impl ResponseEgressDeadline { + pub const fn after(duration: Duration) -> Self; + pub const fn at(deadline: Deadline) -> Self; +} + +#[derive(Clone, Copy, Debug)] +pub struct ResponseEgressPolicy { + pub write_deadline: Deadline, +} +``` + +An application inserts one `ResponseEgressDeadline` into the returned response extensions. +`ResponseEgressDeadline::at` preserves an already-computed absolute cutoff in the application's +`MonotonicClock` domain, including a queue-through-egress budget. +`ResponseEgressDeadline::after` selects an egress-start-relative deadline without requiring a +response category in the global policy callback. `begin()` samples `egress_started_at` once from +the envelope's injected clock, removes the extension, and resolves `after(duration)` from that +exact sample before invoking the callback. The duration is clamped to `DEADLINE_FAR_FUTURE`, and +checked-add overflow fails closed to `egress_started_at`. `begin()` exposes the resolved absolute +value through `ResponseEgressHead::application_deadline()` and enforces the earlier of it and the +callback-selected `write_deadline`. The retired ambiguous `new()` constructor and unresolved +`deadline()` accessor do not exist. + +Every normalized-error `DetachedResponseEgressDecision::Send` carries such an absolute deadline +as a mandatory named field. `App` inserts it into the existing `ResponseEgressDeadline` extension +path before constructing detached egress. `begin()` enforces the earlier of the factory deadline +and the default response-egress policy, so neither can extend the other. +`DetachedResponseEgressHead::write_deadline_after` +constructs a deadline in the captured request's injected monotonic-clock domain by anchoring the +duration to `request_start`, clamping it to `DEADLINE_FAR_FUTURE`, and failing closed to +`request_start` if checked addition overflows. The default factory supplies an empty completion and +`DEFAULT_RESPONSE_WRITE_BUDGET` measured from request start. Admission-selected detached responses +retain the response-owned deadline path described above and do not use the normalized-error +factory contract. + +`App` owns a synchronous response-egress policy callback configured through +`Hooks::configure`. The callback receives immutable response-head metadata plus the captured +request start, optional route metadata, and optional application deadline; it cannot inspect, +consume, or mutate the body. +The portable default is +`Deadline::at_instant(egress_started_at + DEFAULT_RESPONSE_WRITE_BUDGET)`. + +The adapter normalizes every callback result to no later than +`egress_started_at + DEADLINE_FAR_FUTURE` using checked addition. An already-expired result is +a valid immediate deadline. Arithmetic overflow while deriving the clamp fails before commit +as `ConversionError`; it does not create an unbounded write. It then minimum-clamps that result +against the optional response-carried deadline. Neither path can extend an application deadline. + +The deadline measures total response-egress time. It is never reset by response conversion, +first-byte wait, source readiness, backpressure, chunk boundaries, flushes, close, finish, or +trailers. Equality is expired. Before each operation and immediately after that operation +becomes ready, expiry wins over simultaneous progress. + +If a native operation reports accepted bytes at or after the deadline, the coordinator first +accounts that accepted prefix and then terminalizes as `DeadlineExceeded`. The prefix is never +retried. Deadline precedence controls the terminal cause, not whether completed native progress +is recorded. + +Every application timestamp and terminal report uses the envelope's injected monotonic clock. +An adapter declaring Native deadline support arms a provider timer from the remaining duration +in that same clock domain. Once that timer wakes, expiry is authoritative even if a frozen test +clock has not advanced; the adapter uses the deadline instant as the minimum terminal timestamp. +That timer must not depend on another source poll or write-ready notification to wake. An +adapter unable to preempt a provider operation checks before and after each operation, +documents the non-preemptible interval, and declares `BestEffort`. + +### 3.2 Terminal report + +The app may install one observer. The adapter invokes it synchronously exactly once per +response-egress attempt: + +```rust +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ResponseEgressOutcome { + ClientDisconnected, + Completed, + ConversionError, + DeadlineExceeded, + HostHandoff, + RequestCancelled, + SourceError, + TransportError, + Unspecified, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ResponseEgressBodyKind { + Application, + Fallback, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ResponseEgressFallbackDisposition { + Aborted, + Completed, +} + +#[derive(Clone, Debug)] +pub struct ResponseEgressReport { + pub bytes_written: u64, + pub body_kind: ResponseEgressBodyKind, + pub elapsed: Duration, + pub fallback_disposition: Option, + pub framing_failures: ResponseEgressFramingFailures, + pub outcome: ResponseEgressOutcome, + pub request_start: MonotonicInstant, + pub route: Option, + pub source_failure: Option, +} + +pub struct ResponseEgressSourceFailure { /* fixed-size, text-free, Copy snapshot */ } + +pub struct ResponseEgressFramingFailures { /* two fixed optional existing framing-rule enums */ } + +impl ResponseEgressFramingFailures { + pub fn application(&self) -> Option; + pub fn fallback(&self) -> Option; +} + +impl ResponseEgressSourceFailure { + pub fn body_kind(&self) -> ResponseEgressBodyKind; + pub fn category(&self) -> &'static str; + pub fn provider_failure_detail(&self) -> Option<&ProviderFailureDetail>; +} + +pub struct ResponseEgressCompletion { /* non-clone callback owner */ } + +pub struct DetachedResponseEgressHead<'head> { + /* request method, request start, status, and stable error kind */ +} + +#[non_exhaustive] +pub enum DetachedResponseEgressDecision { + Abort, + Send { + completion: ResponseEgressCompletion, + deadline: Deadline, + }, +} + +impl DetachedResponseEgressHead<'_> { + pub fn write_deadline_after(&self, duration: Duration) -> Deadline; +} + +impl ResponseEgressCompletion { + pub fn empty() -> Self; + + pub fn join(self, other: Self) -> Self; + + pub fn late_bound() -> (ResponseEgressResource, Self) + where + T: Send + 'static; + + pub fn new(complete: Complete) -> Self + where + Complete: FnOnce(&ResponseEgressReport) + Send + 'static; +} + +pub struct ResponseEgressResource { /* private */ } + +impl ResponseEgressResource +where + T: Send + 'static, +{ + pub fn install(&self, resource: T) -> Result<(), ResponseEgressResourceInstallError>; +} + +#[non_exhaustive] +pub enum ResponseEgressResourceInstallError { + AlreadyInstalled, + Closed, +} +``` + +An application that must retain a response-scoped resource until terminal egress constructs one +non-clone `ResponseEgressCompletion` whose `FnOnce` callback owns that resource. The completion is +not attached to a `Response` and never enters `http::Extensions`; it is a separate ownership token +carried by `AdmissionDecision`, `PreparedIngress`, `ResponseEgressEnvelope`, and finally the sole +`ResponseEgressAttempt`. Consequently response cloning, replacement, error rendering, and +framework-generated 404/405 responses cannot duplicate or silently detach the resource. + +Every response-producing admission decision supplies exactly one completion. Applications that +do not need a resource use `ResponseEgressCompletion::empty()`. `Admit` and +`ReadBodyBeforeFallback` transfer it into `PreparedIngress`; `App::dispatch_admitted` removes it +before handing the remaining ingress state to the router and attaches it to whichever response +dispatch produces. That includes handler success, handler error, canonical 404/405, fallback exact +cap, fallback overflow, fallback timeout, and post-admission adapter failure. `Refuse` transfers its +completion directly into detached egress. Normalized pre-admission failures obtain one completion +when the application-configured detached-egress decision factory selects `Send`, or no response +when it selects `Abort`; every `Send` also supplies its mandatory absolute deadline, and the default +uses an empty completion plus `DEFAULT_RESPONSE_WRITE_BUDGET` from request start. Raw parser +failures for which EdgeZero cannot construct +`DetachedResponseEgressHead` remain outside this lifecycle and must not be counted as covered. + +`ResponseEgressCompletion::join` consumes a left and right completion and returns one non-clone +owner. Its terminal callback invokes the left child first and the right child second, passing both +the same borrowed `ResponseEgressReport`; the joined owner executes at most once. If the joined +owner is abandoned before terminal egress, dropping it drops both children and releases both sets +of captured resources without fabricating a report. Each child is invoked through its own panic +boundary on unwind-capable targets, so a panic in the left callback is isolated and does not +suppress the right callback. On panic-abort targets, the process or instance may terminate before +the right callback runs. Composition does not strengthen the adapter's documented completion +boundary or any provider completion guarantee. + +A resource may be acquired after admission. The application calls +`ResponseEgressCompletion::late_bound::()` during admission, puts the returned non-clone +`ResponseEgressResource` installer in `IngressGrant`, and transfers the paired completion into +the admission decision. The handler takes the typed grant and calls `install(&self, resource)`. +Installation consumes the resource. A duplicate returns `AlreadyInstalled`; installation after +terminal completion or pre-`begin()` abandonment returns `Closed`. Both failures drop the rejected +resource before returning and never return ownership to the caller. The typed install error +implements `std::error::Error` so application handlers can propagate it through their normal error +conversion without inspecting text. + +The paired completion owns a private close guard. Terminal completion closes the slot and takes the +installed resource before releasing the internal mutex. Dropping the completion without invoking +its callback performs the same close-and-take operation, even if the installer remains alive. The +slot therefore rejects later installation and releases an installed resource on envelope +abandonment without fabricating a terminal report. The installer is not cloneable; the internal +state is `Send + Sync` for `T: Send` and does not require `T: Sync`. Capturing only a temporary +handler-local guard remains incorrect because it releases the permit before response transmission. +This late-binding pattern does not make the completion cloneable and does not put either owner in +response extensions. + +`ResponseEgressEnvelope::begin()` moves the completion into the sole +`ResponseEgressAttempt` before policy evaluation or framing. The attempt retains it through +application writes, fallback writes, cancellation, adapter errors, and guard drop. The first +terminal transition stores the report in `AttemptState::Terminal`, takes the callback, and invokes +it exactly once. A later signal observes terminal state and cannot complete twice. If an envelope +is dropped before `begin()`, the completion and captured resource are dropped without fabricating +a terminal report. Standard adapters must call `begin()` exactly once for every response envelope. +`ResponseEgressEnvelope` exposes no `into_response()` escape hatch: obtaining a transmittable +response requires `begin()`, which first transfers the completion into the attempt. Dropping an +unbegun envelope is permitted only as cancellation before transmission; it releases the resource +without exposing the response or inventing a report. + +`ResponseEgressAttempt::into_terminal_report(self)` consumes the attempt and returns +the exact stored report without another clock sample or notification when already +terminal. A live attempt follows its existing Drop transition (conversion failure +before writing, transport failure while writing, or the retained fallback cause +with aborted fallback). The first transition installs terminal state before notifying +completion and then the applicable observer; consuming or dropping it cannot notify +again. This is terminal metadata extraction, not a response/transmission escape hatch. + +Fastly custom-send helpers return `FastlyRequestDelivery` with NoAttempt, +policy Aborted, Detached and Routed dispositions. Routed finalizer state survives +delivery failure. Attempted results retain this exact terminal report plus the last +selected body's commitment and last delivery error. Successful fallback transmission +can have no delivery error while the report retains its original failure cause. +NotCommitted means the head never reached host send, Unknown means host rejection +cannot establish client visibility, and Committed means host acceptance only. Native +replacement is allowed only for NoAttempt or confirmed NotCommitted; an attempted +response's terminal callbacks already ran, and native replacement is outside their +accounting. Policy abort never authorizes replacement. Pre-admission/refusal responses +exclude the global observer; admitted source-error responses report Detached but retain it. +Standard entrypoint serving +policy and all BestEffort provider qualifications remain unchanged. + +`ResponseEgressAttempt::record_source_error(&EdgeError)` records only the first converter-observed +body-source error as a fixed category, copied optional provider detail and observation-time body +kind. It performs no clock read, allocation, source poll, state transition or callback. A first +observation without provider detail cannot be replaced by a later one, and a terminal attempt +rejects recording. Completion and observer receive the same snapshot. The snapshot is not the +terminal cause: existing deadline/cancellation precedence still wins, and an application-source +failure keeps its `Application` tag even if fallback is delivered. A successfully delivered +rendered application error, clean EOF or transport-only failure does not create a source failure. +All four converters record before post-ready arbitration, including inside the Cloudflare/Spin +source races; no unobserved error is reconstructed. Exact report literals must add the optional +field. + +`ResponseEgressAttempt::prepare_response(method, response)` delegates to the existing normalizer +once and records the first actual precommit rejection in the application or fallback framing slot. +The fallback tag is selected before normalization. Both slots survive into the same completion +and observer report. Writing/terminal calls still return the original normalization result but +reject recording and logging of precommit observations. Logging includes only the existing enum's +fixed text and body kind at error level. The capture itself samples no clock, polls no source, +allocates no metadata and changes no terminal outcome. The normalizer and logging backend retain +their own allocation behavior. All lifecycle-owned normalization calls, including fallback and +Axum revalidation, use this wrapper. Policy/deadline/host conversion failures without a rejected +framing rule leave both slots empty. Postcommit length errors remain source errors. The public +root export names the existing enum; `EdgeError::response_framing_error()` only downcasts an +already-owned internal source. Display, wire errors and fallback bytes remain unchanged. +Report literals must supply `framing_failures`, empty by default. Current stock fallbacks are +framing-valid; direct invalid-fallback normalization tests establish attribution, not an actual +adapter-generated failure. Capability/evidence claims are not strengthened. + +The response-scoped completion runs before the global observer. Each completion child and the +observer are isolated by independent unwind boundaries: a completion panic is logged with a +category-only message and does not prevent a later joined child or the global observer from +receiving the report; an observer panic does not re-enter the completion callback. This isolation +exists only on unwind-capable targets. Panic-abort targets retain the trusted-callback limitation +below and may terminate before a later child or observer runs. No callback can alter the response +or terminal outcome. + +`ResponseEgressBodyKind::{Application, Fallback}` identifies which bounded wire body the +reported byte count describes. Application bytes and fallback bytes are never mixed because +a fallback is permitted only before application headers commit. `fallback_disposition` is +`None` for the application body and records whether a selected fallback reached its finish +boundary or was aborted. + +`bytes_written` is payload bytes accepted across the adapter's documented guest-visible +write boundary. It excludes headers and protocol framing and does not claim wire delivery. +Checked addition is mandatory; overflow terminalizes as `TransportError`. A partial native +write accounts only its accepted prefix and never retries that prefix. + +`elapsed` is terminal monotonic time minus `egress_started_at`. If the injected clock moves +backward, the report records zero, classifies `Unspecified`, logs the clock fault, and still +completes exactly once. + +Neither completion nor observer callbacks can alter the client response. EdgeZero emits only a +category-level message for a caught callback panic. Rust invokes the application-owned process +panic hook before `catch_unwind` returns, so panic-hook output is outside EdgeZero's +diagnostic-redaction guarantee; applications whose panic payloads may contain secrets must install +an appropriate redacting hook during startup. On panic-abort targets, including the pinned Fastly +WASM build, a policy panic occurs before terminal state and terminates the instance without a +report. A completion or observer panic occurs after terminal state is stored and any fallback has +settled, but terminates the instance before later callbacks can be guaranteed. Application +callbacks are trusted not to panic there; only unwind-capable targets can exercise the contained +panic and callback-ordering contract. + +`Completed` means core EOF and any supported trailers reached the adapter's documented +successful finish boundary. `HostHandoff` means the provider accepted the complete +guest-visible stream but exposes no trustworthy transport-finish callback. +`ClientDisconnected` requires a provider signal proved to mean downstream response-consumer +loss. A request or task cancellation with weaker provenance is `RequestCancelled`. The old +converter-only `ResponseReturned` outcome is removed because no retained entrypoint ends its +lifecycle at response-object construction. + +### 3.3 Exactly-once state machine + +The portable guard remains public to adapters, non-clone, and `#[doc(hidden)]`: + +```text +Initial -> Writing -> Terminal(Completed) +Initial -> Terminal(failure) +Writing -> Terminal(failure or HostHandoff) +``` + +The adapter-local coordinator may have more detailed states such as `HeadReady`, `Committed`, +`SourceEof`, and `Finishing`, but only that coordinator may mutate the core attempt. EOF, +finish, source error, transport error, deadline, explicit cancellation, disconnect, converter +failure, connection loss, and guard drop all compete through one terminal transition. Later +terminal events perform resource cleanup only and cannot replace the original cause. + +Dropping an `Initial` guard reports `ConversionError`. Dropping a `Writing` guard reports +`ClientDisconnected` only when the provider supplied a stable disconnect/cancel signal; +otherwise it reports `TransportError`. Empty and `Body::Once` responses enter `Writing` and +produce one lifecycle report just like streamed responses. + +The existing non-clone core attempt remains the only terminal guard and owns the mandatory +response-scoped completion slot; an empty slot is represented by +`ResponseEgressCompletion::empty()`, not `Option`. `begin()` changes so +policy panic/normalization failure returns the still-live attempt, clock, and failure cause to +the adapter instead of notifying before a fallback is written. The attempt records whether +application or fallback bytes are being written. Adapters may add private event/coordinator +types but must not add a shared runtime writer abstraction, make `BodyStream` require `Send`, +or attach provider capability declarations to individual reports. + +### 3.4 Fallible application assembly + +Application configuration is startup policy and must not require `expect` or panic to reject +invalid limits. Construction is core-owned and installs target metadata before configuration. +After successful configuration, core rejects any changed platform facts, provenance, or unknown +reasons rather than silently restoring them. A hook failure propagates unchanged: + +```rust +impl App { + pub fn build(platform: PlatformMetadata) -> Result { + let mut app = Self::with_name_and_platform(A::routes(), A::name(), platform); + A::configure(&mut app)?; + if app.platform() != platform { + return Err(EdgeError::internal(anyhow::anyhow!( + "application configuration changed adapter-selected platform metadata" + ))); + } + Ok(app) + } +} + +pub trait Hooks { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + Ok(()) + } +} +``` + +The `app!` macro emits the hook signatures only. A supplied `configure = ` callback must return +`Result<(), EdgeError>` and its result is returned directly rather than discarded. No adapter +continues with a partially configured `App`: + +- Axum builds before binding its listener and returns an `anyhow::Error` retaining the + `EdgeError` source. +- Cloudflare builds at the start of each host invocation, before core request conversion, + admission, body polling, or dispatch. It logs only the stable `EdgeError::kind()` and returns a + fixed `worker::Error::RustError("application configuration failed")` so private diagnostics are + not wire-visible. +- Fastly builds after fixed runtime/logger setup but before receiving, converting, admitting, or + dispatching the client request. Its top-level error retains the `EdgeError` source under the + fixed `"application configuration failed"` context. +- Spin builds at the start of each host invocation before core request conversion, admission, + body polling, or dispatch. Its `anyhow::Error` retains the `EdgeError` source under the same + fixed context. + +Cloudflare, Fastly, and Spin are per-invocation build boundaries, not process-start boundaries. +No compatibility `build_app_unchecked`, infallible callback coercion, or panic fallback remains. +The demo, generated template, hand-written `Hooks` implementations, guides, and tests migrate +atomically. + +## 4. Write, commit, and abort semantics + +### 4.1 Commit boundary + +Each adapter documents its response-header commit operation. Before commit, conversion failure +or an expired deadline may be replaced by an adapter-private bounded 500 or 504 response. The +same coordinator and attempt retain transport ownership while writing that fallback. They +account fallback bytes, record `ResponseEgressBodyKind::Fallback`, and report the original +`ConversionError` or `DeadlineExceeded` cause only after the fallback reaches finish or abort. +The fallback does not create another observed attempt or invoke application policy recursively. + +Because the application deadline may already be expired, the fixed fallback body uses a +separate internal one-second safety budget beginning when fallback conversion starts. That +budget is not an extension of the application response deadline. If fallback construction, +write, or finish fails, the adapter aborts and retains the original causal outcome; the report's +byte count, body kind, and fallback disposition still disclose the accepted fallback prefix and +its failed finish. + +The fallback head passes through the same request-method and response-status framing +normalization as an application response. A `HEAD` fallback writes no payload, reports zero +bytes with `ResponseEgressBodyKind::Fallback`, and records completion or abort at the +platform's empty-body finish boundary. + +After commit, status and headers are immutable. Deadline, source failure, cancellation, and +write failure drop the source and staged chunk, invoke the strongest platform abort/reset/error +primitive, and report the first terminal cause. No adapter appends an error payload to a +partially written response. A shared body/framing wrapper drops its source and any native reader +state before making a terminal `Err` item visible; cleanup never depends on the adapter polling +that wrapper again or dropping it after observing the error. + +### 4.1.1 Bounded synthesized headers + +`App::set_synthesized_response_headers(HeaderMap)` validates a complete replacement and retains +an immutable `SynthesizedResponseHeaders` snapshot. A rejected replacement returns +`SynthesizedResponseHeadersError` without changing the installed set. Fields are bounded to 32 +values and 4096 bytes, including repeated names and `: `/CRLF per value plus final CRLF, excluding +the status line. Validation rejects the EZ-19.5 framing/coding field list and values outside visible +ASCII/interior HTAB, including leading/trailing whitespace. Empty values are valid. Accepted maps +and field backing storage are copied into bounded retained storage; caller input allocations and +provider allocations are not certified by this limit. Existing response/renderer fields remain +subject to their separate limits; this bound covers only the configured decoration. + +The envelope captures the snapshot before conversion, with separate private eligibility for +framework-generated initial responses. Detached normalized errors, admitted source errors and +canonical unmatched/wrong-method errors are eligible; matched handler results/errors and admission +refusals are not. Assembly failures have an empty set because no successfully configured app +exists, including when configuration installed policies before returning an error. All +envelopes retain the snapshot for a later adapter-built fallback, including initially ineligible +routed/refusal responses. Fastly `prepare` can insert a validated per-request replacement into its +extension bag. The adapter removes that value before forwarding ordinary extensions and replaces +the returned envelope snapshot before `begin`; invalid construction leaves the app snapshot, while +an explicitly empty set replaces it. + +Decoration precedes the policy-visible initial head. The snapshot transfers to the sole attempt +before policy/deadline/framing preparation can fail. Fallback preparation merges the same validated +set without callbacks, repeated validation, a second completion or an extended deadline. Axum's +emergency detached fallback copies that snapshot before moving or terminalizing the original +attempt. Existing response names win; ordered repeated configured values are preserved when a name +is absent. Typed overflow suppression runs after initial composition and during final normalization; +ordinary upstream 413 retry fields and effective config-503 retry fields remain unchanged. + +Local HTTP/1 fixtures exercise maximum-size delivery and partial-configuration exclusion; they +are not hosted qualification or a promotion of egress guarantees. Consumer parity, deployed +provider delivery and frozen-candidate acceptance retain their own gates. + +### 4.2 Backpressure and memory + +Platform demand drives source polling. EdgeZero retains at most one source chunk plus explicitly +documented platform staging. A source is not polled again until the current chunk has been +fully accepted, partially accepted with its remainder retained, or released during teardown. + +`Body::Once` remains one already-materialized core allocation. This design adds no response +size limit and removes the converter-only 16 MiB collection limits. Outbound `Buffered` mode +and its `collect_response_stream*` helpers remain because they implement a separate caller- +selected outbound fetch contract; response-egress converters no longer call them. + +### 4.3 Cancellation safety + +Dropping a future is not assumed to cancel a provider operation unless the pinned API contract +or a behavioral probe establishes that fact. An adapter must retain ownership of pending +operation state until cancellation is acknowledged or the containing response/connection is +destroyed. Partial progress reported during cancellation is accounted exactly once. A losing +operation never owns or directly mutates the attempt. + +The earliest causal failure remains authoritative when abort/close also fails. Teardown errors +are logged with bounded category-only diagnostics and do not replace that outcome. + +### 4.4 Response framing and body suppression + +Owning a lower-level transport boundary also makes EdgeZero responsible for consistent final +response framing. Each adapter carries the request method into its private egress context, +validates the final response head before commit, and applies this precedence order: + +1. Parse every `Content-Length` field and comma-separated member as an unsigned decimal `u64` + after trimming optional whitespace. Empty, signed, overflowing, or otherwise invalid members + fail as `ConversionError`; repeated members must all have the same value and normalize to one + field. +2. Final 1xx responses and protocol upgrades are unsupported and fail before commit. A + successful response to `CONNECT` is rejected because EdgeZero does not own a tunnel + lifecycle. +3. Status-specific suppression runs before method-specific suppression. A `204` response drops + the body and always removes `Content-Length`. A `205` response drops the body and rejects a + nonzero declared length; explicit zero is permitted. A `304` response drops the body and may + preserve an explicitly supplied representation length. +4. For every other status, a `HEAD` response suppresses the body without polling it and may preserve + an explicitly supplied representation `Content-Length`; that value is not compared with the + unsent body. Suppressed buffers and streams are dropped immediately and are never restored. + Local Fastly SDK controls expose forbidden 205 payload delivery and HTTP/2 failures when a HEAD + body is restored for size inference. The requester approved preserving suppression and leaving + exact HEAD/205 SDK parity unqualified pending hosted evidence, rather than reproducing invalid + protocol behavior. This does not waive that parity gate or claim full EZ-18 acceptance. +5. Only responses whose payload will be transmitted enter body-length validation. + +After suppression is resolved, every adapter applies these common field rules: + +- EdgeZero does not forward an application-supplied `Transfer-Encoding`; the selected platform + transport owns transfer framing. +- For a response whose payload will be transmitted, `Content-Length` on `Body::Once` must equal + the exact body length before commit. A mismatch is `ConversionError` and produces the bounded + pre-commit fallback. +- `begin(framing)` and both preparation APIs require an explicit + `ResponseEgressFramingMode`. Axum, Cloudflare and Spin select `ExplicitBufferedLength`, deriving + the exact length of an eligible transmitted buffer (including empty) after hop-by-hop removal. + Fastly selects `HostAutomatic`: source lengths are still validated, but core adds no length that + could incorrectly constrain host-transformed wire bytes. The same mode applies to fallbacks. +- A streamed body with `Content-Length` is checked incrementally. Exceeding the declared length + aborts immediately; early EOF aborts instead of presenting a clean truncated response. +- A streamed body without `Content-Length` uses the provider's native streaming framing. +- Core `Body` has no trailer variant. An application-supplied `Trailer` declaration is therefore + unsupported and fails before commit. Future trailer support requires an explicit declared + trailer shape in the core response contract. + +Repeated end-to-end fields are appended rather than replaced. Every adapter that receives an +application `Connection` field parses its comma-separated tokens and removes every nominated +field in addition to the standard hop-by-hop fields and the de facto `Proxy-Connection` field +before provider conversion. A provider +that rejects or hides `Connection` earlier documents that equivalent boundary. Invalid +connection tokens fail before commit as `ConversionError`. These rules are shared semantics +implemented separately with each provider's structured header API; adapters do not serialize +raw header blocks. + +## 5. Adapter designs + +### 5.1 Axum: owned Hyper HTTP/1 boundary + +The standard Axum adapter stops returning an erased `axum::body::Body`. It runs an EdgeZero- +owned Hyper HTTP/1 accept/connection loop on a Tokio `LocalSet`, allowing the portable +non-`Send` core stream to remain on one local executor. A private Hyper service converts +`Request` into the existing core request pipeline and returns a private response +body implementing `http_body::Body` directly. + +The adapter owns two coordinated scopes: + +1. A per-response coordinator owns the core stream, current chunk, deadline, byte accounting, + EOF/finish state, and core attempt. +2. A per-connection supervisor owns the Hyper connection future, socket wrapper, and an ordered + registry of response coordinator handles. Each entry records identity, deadline, commit + state, and EOF/flush marker. The supervisor can terminate the connection while Hyper is + blocked below `poll_frame` flushing bytes to the socket. + +The body accounts a payload frame when Hyper accepts it from `poll_frame`. It reports source +EOF to the connection supervisor. Completion is eligible only after a subsequent proved Hyper +framing/flush boundary has accepted all response bytes into the OS socket. It never means peer +receipt. A raw-socket test must prove that the selected boundary occurs once per response; +until then the adapter reports `HostHandoff` and completion remains `BestEffort`. + +Hyper stops polling when its declared-length encoder reaches zero, even without an exact size +hint. For a transmitted stream with `Content-Length`, Axum stages only the final declared-length +chunk and validates source EOF before handing that frame to Hyper. Empty tail chunks yield +cooperatively; excess bytes or a late source error abort without sending the staged frame. The +source is marked exhausted on clean EOF so subsequent body drop settles `HostHandoff` unless a +connection failure has already won. Lookahead remains under the original absolute write deadline +and injected application clock, with the connection supervisor's host timer covering pending +reads. It does not collect the preceding body. + +A transmitted zero-length stream is validated asynchronously before Hyper receives the response, +since Hyper may never poll such a body. The original attempt retains its completion during this +bounded validation. Source failure selects the finite conversion fallback; deadline expiry +selects the timeout fallback. HEAD/status suppression still drops the source without polling. + +The detached finite fallback used after fallback registration fails determines EOF from its +payload source, not from absence of an active attempt. GET retains the selected error bytes; HEAD +sends no payload. Consuming the final frame exhausts the source, and terminal failure explicitly +drops both the source and any staged frame before returning an error. + +HTTP/1 post-commit timeout or source failure closes the connection to preserve framing. This +may cancel pipelined responses on that connection. The supervisor never mutates a core attempt; +it submits an ordered connection event to each registered coordinator. The coordinator whose +deadline caused closure retains `DeadlineExceeded`; other committed or queued coordinators +that have no earlier terminal cause settle as `TransportError`. Already-terminal coordinators +ignore the event. Hyper does not currently enable HTTP/2 in this adapter. Future HTTP/2 support +requires per-stream reset and completion ownership; a connection-wide abort is not acceptable +for multiplexed streams. + +The public transport-blind `EdgeZeroAxumService` embedding path is removed. Public serving APIs +must accept or create a listener and retain EdgeZero connection ownership. There is no +buffered compatibility service. + +### 5.2 Cloudflare: owned Workers stream writer + +Cloudflare retains the standard `#[event(fetch)]` entrypoint but replaces the generic +`Response::from_stream` lifecycle wrapper with an adapter-owned Workers stream bridge. +The request's `AbortSignal` is captured before the request body is moved, and generated plus +demo Wrangler manifests enable `enable_request_signal`. + +For a transmitted response with declared `Content-Length`, including `Body::Stream`, the +adapter owns both sides of a native fixed-length stream so Cloudflare preserves and enforces +that length. A known `Body::Once` without an explicit length may use its exact length. An +unknown-length stream uses a native transform stream. The adapter retains the writable side +and returns the readable side in the response. A task registered with the request `Context` +owns the body source, writer, request signal, clock, deadline timer, and attempt. + +The writer waits for platform demand, writes one chunk, and accounts the chunk only after the +write promise resolves. It does not poll another source item while a write is pending. The +same task always polls an independent deadline timer, so no downstream pull is required for +expiry. On clean EOF it closes the writer before reporting `HostHandoff`; Workers exposes no +socket-flush or response-pump completion callback. + +Request abort and readable-side cancellation are complementary signals. Until deployed +evidence proves that `Request.signal` means downstream response-consumer loss for a specific +topology, it is `RequestCancelled`, not `ClientDisconnected`. A generic write rejection without +that signal is `TransportError`. Deadline or cancellation aborts the writable stream, drops +pending source state, and terminalizes exactly once. A losing write promise must not remain +able to account bytes or report completion after abort. + +High-water-mark behavior, fixed-length semantics, request-signal behavior, and runtime +cancellation require deployed Workers evidence before promotion to `Native`. + +### 5.3 Fastly: manual send-owning entrypoint + +Fastly removes `#[fastly::main]` from generated and demo applications. EdgeZero initializes +the SDK, acquires the client request, dispatches the app, converts the response head, commits +it, pumps the body, and finishes or abandons the native response before returning: + +```rust +fn main() -> Result<(), fastly::Error> { + edgezero_adapter_fastly::run_app::() +} +``` + +All public dispatch variants become send-owning terminal operations. No public +response-returning converter remains. The adapter constructs owned low-level response/body +handles. For a transmitted buffered response it stages the complete native body while the attempt remains +precommit, checking the injected deadline before and after every short write. Preparation failure +releases that body and may select the bounded fallback. One non-streaming Automatic send hands +head and body to the host together, matching the pinned SDK's `send_to_client` ABI. Transmitted +source bytes are counted once at successful handoff; suppressed HEAD bytes count zero. Expiry +during a successful bulk send may produce a late terminal report but cannot truncate or abandon +the already-consumed body. Handoff is not evidence of peer receipt. + +Suppressed HEAD/204/205/304 responses instead use an empty streaming handoff. Their original +buffer or stream is already dropped without polling; no representation bytes are restored or +staged. This prevents the local HTTP/2 host from mistaking a known zero-byte suppressed payload +for a representation `Content-Length: 0`. Explicit representation lengths still reach the host, +whose Automatic framing may omit them. Exact bodyless SDK parity remains unqualified. + +Actual streams use `stream_to_client`, obtaining an unbuffered `StreamingBodyHandle`. Manual +framing is restricted to a transmitted declared-length stream without any `x-compress-hint` field. +Any hint delegates wire framing to Automatic, even `off` or ambiguous values: pinned SDK sources +do not establish the host's hint parser semantics. Source-length enforcement remains unchanged. +Hosted compression and SDK parity over HTTP/1.1 and HTTP/2 remain explicit qualification gates. +The adapter does not use the high-level `StreamingBody`, whose `BufWriter` may flush buffered data +while attempting to finish or abandon. + +Each chunk is fully written before the next source poll. Native short writes account their +accepted prefix and retain only the unwritten suffix. Zero-length progress is a +`TransportError`. Clean EOF invokes `finish`; `finish` returning success reports `Completed` +at the precisely documented boundary "Fastly host accepted body-handle close." It does not +claim that background transmission or peer receipt completed. Source/deadline failure invokes +`abandon` or drops the handle so its native drop guard abandons it. Generic native write errors +are `TransportError` because the SDK exposes no stable client-disconnect reason. + +Source polling and native writes are synchronous and cannot be preempted by an independent +timer. The adapter checks the absolute deadline before and after each source poll, write, and +finish operation. Response-write deadlines therefore have a `BestEffort` ceiling with the +pinned Fastly API. `StreamingBodyHandle::finish` consumes Fastly's only body handle before it +reports success or failure. A finish failure is therefore terminal and cannot be followed by a +second explicit `abandon`; EdgeZero preserves the finish failure as the first cause, reports it +exactly once, and relies on the consumed handle's host-side close semantics. This limitation is +part of Fastly's `BestEffort` abort and completion classifications. + +### 5.4 Spin: raw WASI response and owned writer task + +Spin retains the official `#[http_service]` entrypoint. Its macro accepts a raw WASIp3 +response, so replacing the export mechanism would duplicate version-sensitive SDK machinery +without improving lifecycle ownership. + +`run_app`, `AppExt`, and request dispatch return the concrete raw response type re-exported by +`spin_sdk::wasip3`. The adapter bypasses `http_into_wasi_response`, constructs `BodyWriter` +and `Response::new` itself, and spawns one SDK-owned task. The task owns the core body, +`StreamWriter`, result writer, response-result reader, clock, deadline, and attempt for their +entire lifetime. + +The task races source polling and each individual `StreamWriter::write` against the absolute +deadline. It does not use `write_all` or `send_http_body`, because the lower-level write result +is required to account a transferred prefix during cancellation. Clean EOF closes the stream, +writes the successful body result, and then observes the response-result future. Failure or +deadline drops source state and publishes one bounded WASI error result; it never appends a +post-commit error body. + +On deadline, the task retains the pending write future and writer until cancellation is +acknowledged or destroying the response stream makes further progress impossible. Any prefix +reported during cancellation is accounted before cleanup. The pending operation emits events +to the coordinator and cannot access the attempt directly. + +The current Spin host does not prove that the response-result future corresponds to network +completion. Until host source or deployed evidence proves that boundary, successful guest +termination reports `HostHandoff`, and completion remains `BestEffort`. + +All raw types and task spawning come through the pinned `spin_sdk::wasip3` re-export. A +separate direct `wasip3` dependency is removed if no other live code requires it. + +## 6. Hard-cut migration and deletion + +The implementation is atomic. It removes rather than deprecates: + +- `AXUM_RESPONSE_STREAM_BUFFER_BYTES`, `FASTLY_RESPONSE_STREAM_BUFFER_BYTES`, and + `SPIN_RESPONSE_STREAM_BUFFER_BYTES`; +- Axum's collecting response converter and transport-blind public service path; +- Fastly's response-returning `run_app`, extension/config variants, service dispatch, + registry dispatch, public `from_core_response`, and `#[fastly::main]` scaffolding; +- Spin's `SpinFullResponse`, `FullBody` converter, public `from_core_response`, and matching + `AppExt`/dispatch signatures; +- Cloudflare's generic response-egress stream wrapper and all premature + pre-enqueue accounting branches; +- the converter-only `ResponseEgressOutcome::ResponseReturned` variant and its zero-byte special + case; +- public `ResponseEgressEnvelope::into_response()` and every test/helper that extracts a response + without beginning and terminally settling its attempt; +- any response-extension-based completion carrier or cloneable shared callback slot; completion is + a direct, non-clone admission/envelope/attempt ownership chain; +- infallible `Hooks::configure` and overridable application-assembly signatures, callback-result discards, and + application-side `expect` calls used only to bridge fallible startup policy; +- obsolete tests, fixtures, imports, dependencies, capability footnotes, and adapter-guide + text that describe buffered egress; and +- the superseded Fastly-only response-egress plan. + +The generic `collect_response_stream*` functions remain only where live outbound buffered +fetch APIs use them. Historical design and completed plan documents remain records; current +specifications and guides identify this hard cut as the active contract. + +The demo adapters, adapter-owned Handlebars templates, generator assertions/snapshots, +generated-project compile fixtures, API documentation, capability matrices, and legacy API +contract script change in the same patch series. A repository check rejects production, +template, example, and current-guide references to removed symbols or buffering constants. + +## 7. Capability policy + +`Native` means implementation plus target-appropriate evidence demonstrates the exact stated +boundary without a documented behavioral deviation. Deterministic unit tests prove EdgeZero +mechanics; they do not by themselves prove provider behavior. `BestEffort` means the lifecycle +is implemented but a provider boundary is weaker or not yet proved. An app requiring a Native +capability rejects both `BestEffort` and `Unsupported` before serving traffic. + +The implemented declarations are conservative: + +| Capability | Axum | Cloudflare | Fastly | Spin | +| ------------------------------------ | ---------- | ---------- | ---------- | ---------- | +| `buffered-response-content-length` | Native | BestEffort | BestEffort | BestEffort | +| `response-egress-abort` | BestEffort | BestEffort | BestEffort | BestEffort | +| `response-egress-backpressure` | BestEffort | BestEffort | BestEffort | BestEffort | +| `response-egress-completion` | BestEffort | BestEffort | BestEffort | BestEffort | +| `response-write-deadlines` | BestEffort | BestEffort | BestEffort | BestEffort | +| `lazy-streamed-response-passthrough` | Native | Native | BestEffort | BestEffort | + +Axum raw-socket tests prove deadline wakeup and deterministic collateral pipeline attribution, +but Hyper frame acceptance is not per-response socket or client completion. Cloudflare writer +acceptance and close lack deployed disconnect/completion proof. Fastly cannot preempt synchronous +source polls or hostcalls. Spin cancellation does not prove finite host teardown. These are the +reasons the four egress rows remain `BestEffort`; implementation intent alone would not qualify. + +## 8. Test and evidence matrix + +Every adapter gets deterministic lifecycle tests before provider integration: + +| Surface | Required proof | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Timing | One resolved absolute deadline covers conversion, first byte, source waits, writes, backpressure, and finish. Equality expiry wins; no chunk resets the budget. Frozen/backward clocks still settle exactly once. A response-owned `at` cutoff remains absolute; `after` is resolved exactly once from the injected `egress_started_at` sample, clamps to `DEADLINE_FAR_FUTURE`, and fails closed to that sample on overflow. Every normalized-error `Send` supplies a mandatory absolute deadline in the request's injected clock domain; its relative construction anchors to `request_start`, clamps to `DEADLINE_FAR_FUTURE`, and fails closed to request start on overflow. The default uses `DEFAULT_RESPONSE_WRITE_BUDGET` from request start. The effective bound is the earlier of the application deadline and default egress policy, so neither can extend the other. Admission-selected detached responses retain their response-owned deadline path outside the factory contract. | +| Completion | Empty, buffered, streamed EOF, source failure, transport failure, timeout, disconnect, conversion failure, never-polled body, and guard drop each notify once. Competing terminal events still produce one report. The exact decision-owned completion survives normal dispatch, handler/routing errors, canonical 404/405, bounded fallback outcomes, refusal, policy/framing failure, adapter fallback, and guard drop; it runs before the global observer. Joined completions receive the same borrowed report at most once in left-to-right order and release both resources if abandoned. Child panics remain independently isolated only on unwind-capable targets; panic-abort may terminate before the right callback. A dropped pre-begin envelope releases its resource without fabricating a report. Normalized pre-admission validation invokes the detached-egress decision factory exactly once: `Send { completion, deadline }` supplies exactly one completion and one mandatory deadline, with an empty completion by default, while `Abort` constructs no response or attempt and invokes no completion, observer, handler, middleware, or body poll. Raw parser failures remain outside the lifecycle contract. Explicit ingress abort likewise starts no attempt and invokes no callback. Compile-time tests prove the completion is non-clone and cannot be stored in response extensions. | +| Accounting | Exact payload totals, zero-byte response, checked overflow, short/partial writes, cancellation prefixes, and no headers/framing in the count. | +| Commit | Pre-commit failure may synthesize a bounded fallback; post-commit failure aborts without rewriting status or appending a body. | +| Backpressure | Holding one platform write prevents the next source poll and bounds EdgeZero staging to one chunk. Axum's final declared-length frame has the explicit bounded EOF-lookahead exception above. | +| Teardown | Source drop and native abort/reset/error/close occur on every supported failure path. A terminal stream error releases its source/native state before yielding `Err`, without a repoll or wrapper drop. A losing future cannot mutate terminal state. | +| Headers | Repeated fields, including `Set-Cookie`, survive the new low-level conversion paths. | +| Framing | `HEAD`, body-forbidden statuses, content-length exact/short/long bodies, removed transfer encoding, unsupported upgrades, and early EOF behave identically across adapters. | +| Migration | Removed APIs fail to compile; generated and demo applications compile only against the new entrypoints, response types, and fallible configure signature. Every adapter proves a failing configure result prevents request conversion, body polling, admission, and dispatch. Axum additionally proves no listener bind and a retained `EdgeError` source. Fastly proves no client-request receive and a retained source under the fixed context. Spin proves the retained source and fixed context. Cloudflare proves stable-kind-only logging and the fixed public `worker::Error`. | + +Target evidence adds: + +Current local Axum evidence uses Rust 1.95.0 and the locked Hyper dependency. +`connection::tests::bodyless_tcp_responses_release_the_response_resource_exactly_once` +covers HEAD/204/304 suppression without source polling and one completion/resource release. +`dev_server::bounded_ingress_tests::stalled_egress_termination_releases_capacity_and_recovers` +covers disconnect, a pending producer, and a non-reading client with an always-ready source; +all settle once, stop polling, release resources and recover a one-connection server. +It includes a frozen injected clock and an independently waking native deadline timer. +Existing connection/shutdown tests cover EOF, malformed declared lengths, source/conversion +errors and cancellation. These are native TCP/guest ownership proofs, not confirmed client +delivery or hosted-provider evidence; no capability promotion follows from this test expansion. + +| Target | Tested SDK / Native Revision | Hosted Runtime Revision / Evidence | Egress Qualification | +| --- | --- | --- | --- | +| Axum | Hyper 1.12.0, Rust 1.95.0, local TCP tests | Operator deployment not qualified by local tests | BestEffort; HostHandoff is not peer delivery | +| Cloudflare | worker 0.8.5; guest/WASM contract tests | Unknown; no deployed lifecycle artifact supplied | BestEffort; hosted disconnect/close boundary remains unproved | +| Fastly | fastly 0.13.1; scripted guest sink tests | Unknown; Viceroy is not hosted evidence | BestEffort; synchronous operations cannot be guest-preempted | +| Spin | spin-sdk 7.0.0; scripted guest/WASI tests | Unknown; local Spin is not hosted evidence | BestEffort; finite host teardown remains unproved | + +No normalized guest test qualifies raw-ingress parser guarantees. A future hosted +artifact must identify its source SHA, SDK/runtime revision (or typed unknown), +compatibility settings, scenario, terminal count, resource recovery and ownership boundary. + +### 8.1 Opt-in hosted ownership qualification + +The reusable-app fixture workspace has a default-off `qualification` feature. Its synthetic +application uses the standard entrypoints on all four adapters, including the manual Fastly +send-owning lifecycle. It emits four bounded, correlated records per valid probe: source drop, +terminal report, response-owned resource drop and final counters. Build identity is compiled in; +request headers can select only bounded run/probe tokens and a fixed scenario, never a destination. +No normal demo or generated application installs these probe routes. + +`scripts/qualify_response_egress.mjs --execute-hosted` requires an explicitly allowlisted isolated +nonproduction HTTPS origin, a finite per-probe time budget, source SHA, SDK and runtime identity +(a documented unknown is allowed), compatibility settings and independent deployment/binary +identity. It runs normal, HEAD/204/205/304, slow/non-reading client, disconnect, cancellation, +pending production, expired deadline, source failure and conversion failure probes. Each is +followed by a normal recovery request. Redirects and retries are disabled. Fastly pending probes +additionally require an independently enforced finite execution ceiling and wait for that ceiling +before recovery. A guest deadline cannot preempt an indefinitely pending synchronous source poll. + +The runner caps aggregate observed payload at 64 MiB, plus at most one delivered Node stream chunk +on a cap crossing; input files are read in 16 KiB chunks and rejected before retaining more than +4 MiB. Each evidence-input read can additionally hold one 16 KiB chunk and a final concatenation +copy. These are client-driver budgets, not provider allocation guarantees. + +`--validate` requires complete unsampled telemetry and exactly one terminal/source/resource/final +record for every scheduled probe, with matching build identities and ordered sequence numbers. +It rejects duplicates, missing records, unexpected scenarios, post-terminal source polls, +misclassified failures and unsuccessful normal/bodyless/recovery observations. Its successful +result is only `qualified-ownership` for those scheduled probes, never general delivery or +resource certification. Transport failures qualify only when independently observed; they are +not synthesized by a response object. A run terminated before telemetry export remains unverified. + +The compiled `artifact` identity is a build-input digest (for example the lockfile SHA-256), not +the final binary's self-referential hash. `binary_sha256` and deployment identity are recorded +independently after build. The fixture does not identify provider instances (`unobserved`), so +successful subsequent requests do not prove same-isolate capacity recovery. Hosted teardown, +actual transport interruption, provider allocation and raw parser behavior still require their +own evidence. See the fixture README for build and invocation commands. No hosted run is claimed +by adding or compiling these tools. + +- Axum `tokio::io::duplex` and raw TCP tests for no reader, slow reader, disconnect before + first byte, disconnect mid-body, blocked-socket deadline, normal EOF, keep-alive, + pipelining collateral cancellation, and per-response flush association. +- Cloudflare WASM tests for fixed/unknown-length streams, demand, writer rejection, + readable cancellation, `Request.signal`, no-demand expiry, source release, and one terminal + settlement. Deployed probes cover direct and service-binding topologies. +- Fastly scripted sink tests for short/zero writes, finish/abandon errors, source failure, and + synchronous deadline crossings. Viceroy exercises the undecorated manual entrypoint. +- Spin scripted WASI writer tests for partial progress, cancelled writes, result-writer errors, + response-result timing, source release, and exactly one exported HTTP handler. + +Deployed probes record provider/runtime versions and the source revision. They separately +observe source polls, first-byte timing, slow-reader pressure, disconnect, pre-commit timeout, +post-commit timeout, normal finish, byte accounting, and terminal reports. CI may keep +credentialed probes protected, but capability documentation links the latest evidence used +for any `Native` promotion. + +## 9. Security and observability + +Observer reports contain no body bytes, header values, target addresses, or unbounded error +strings. Route metadata uses the registered pattern, never dynamic path values. Platform logs +may include a bounded static outcome label and byte count; provider/source details stay in +redacted diagnostic logs. + +Fallback responses use fixed public messages and do not serialize provider errors. No adapter +claims that an egress deadline bounds provider buffers, kernel buffers, peer receipt, or total +isolate memory. Those exclusions remain explicit in the public capability guide. diff --git a/docs/superpowers/specs/2026-09-15-reusable-app-lifecycle-design.md b/docs/superpowers/specs/2026-09-15-reusable-app-lifecycle-design.md index 3eb2a2328..6cfae2d0a 100644 --- a/docs/superpowers/specs/2026-09-15-reusable-app-lifecycle-design.md +++ b/docs/superpowers/specs/2026-09-15-reusable-app-lifecycle-design.md @@ -1,6 +1,6 @@ # Opt-in reusable application lifecycle -- **Status:** Implemented; updated for PR #379 review. +- **Status:** Historical lifecycle design; reconciled with PR #275's outbound hard-cut. - **Date:** 2026-09-15 - **Source baseline:** EdgeZero `593fc9282a1c56e12bae15f91eef2162f4b6a1b7`. - **Scope:** Core application ownership and the Fastly, Cloudflare, Spin, and Axum adapter surfaces, including custom dispatch, examples, documentation, and validation. @@ -11,6 +11,13 @@ The original private-helper design in §§8/9.1 is superseded by The implemented `lifecycle::{Sandbox, serve_custom, run_custom}` surface, including its observation counters, is public. No provider-independent scheduler is added. +PR #275 supersedes the original assembly, raw-conversion, response-returning, and +buffered-delivery choices below. The +[outbound HTTP design](2026-05-21-outbound-http-design.md) is authoritative for +admission, outbound execution, response egress, and capability semantics. The API +sections here describe the current production interfaces; historical verification +records do not establish that the reconciled implementation has been reverified. + ### Included compatibility and CLI changes The following focused changes accompany retention and apply to existing users: @@ -39,19 +46,20 @@ Correctness must tolerate fresh initialization on any request. Retention is an o ## 2. Verified baseline -Paths and line numbers below refer to the source baseline, not future implementation locations. +The source baseline above records the historical investigation. These API descriptions +are reconciled with the current production implementations rather than that baseline. | Surface | Current behavior | Reference | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | -| Core construction | `Hooks::build_app` constructs routes and calls `configure`. `App` contains a name and router, not store metadata or logging policy. | `crates/edgezero-core/src/app.rs:14`, `:109` | +| Core construction | `App::build::(platform)` installs platform metadata, constructs routes, and invokes fallible `Hooks::configure`; only success yields an app. | `crates/edgezero-core/src/app.rs` | | Core dispatch | `RouterService` owns `Arc` and supports repeated `oneshot(&self, request)`. | `crates/edgezero-core/src/router.rs:298`, `:349` | | State | Registered state is cloned into each request; shared interiors remain shared. App state overwrites extensions of the same type. | `crates/edgezero-core/src/router.rs:212`, `:251` | | Macro | `app!` generates `Hooks` and router construction. Its state expression executes when the router is built. It does not generate the provider lifecycle. | `crates/edgezero-macros/src/app.rs:214`, `:229` | -| Fastly | Each `run_app_with_request_extensions` call reads runtime configuration, conditionally installs logging, builds the app, and dispatches. | `crates/edgezero-adapter-fastly/src/lib.rs:171` | +| Fastly | `run_app_with_hooks` builds per invocation; `serve_app_with_hooks` retains only successful construction through `Sandbox`. | `crates/edgezero-adapter-fastly/src/lib.rs` | | Fastly logger | Installing the process-global logger a second time returns an error. | `crates/edgezero-adapter-fastly/src/logger.rs:52` | -| Fastly prebuilt dispatch | Public `request::dispatch_with_registries` accepts `&App`, metadata, runtime configuration, and a raw-request extensions callback. | `crates/edgezero-adapter-fastly/src/request.rs:337` | -| Cloudflare | `run_app` rebuilds the app on every fetch. Registry-aware dispatch already takes `&App` but is private. | `crates/edgezero-adapter-cloudflare/src/lib.rs:98`, `src/request.rs:298` | -| Spin | `run_app` rebuilds the app on every handler invocation. Registry-aware dispatch already takes `&App` but is private. | `crates/edgezero-adapter-spin/src/lib.rs:111`, `src/request.rs:183` | +| Fastly prebuilt dispatch | `request::send_request_with_registries_and_hooks` performs canonical admission and owned egress; no native response is returned for another sender. | `crates/edgezero-adapter-fastly/src/request.rs` | +| Cloudflare | `run_app` builds per fetch and delegates to public `dispatch_app::(&app, req, env, ctx)`. | `crates/edgezero-adapter-cloudflare/src/lib.rs` | +| Spin | `run_app` builds per invocation and delegates to public `dispatch_app::(&app, req)`, returning raw `SpinResponse`. | `crates/edgezero-adapter-spin/src/lib.rs` | | Cloudflare/Spin logging | Both adapter logger initializers are currently no-ops. Their existing entry points honor `owns_logging`. | Cloudflare `src/lib.rs:30`; Spin `src/lib.rs:65` | | Axum | `run_app` builds the app once before starting the server, then shares the router through cloned services. | `crates/edgezero-adapter-axum/src/dev_server.rs:352`, `:302`; `src/service.rs:132` | @@ -59,7 +67,7 @@ Paths and line numbers below refer to the source baseline, not future implementa - **Fastly:** reuse is opt-in through an ordinary `main` and SDK `Serve`. A sandbox processes requests sequentially. Lifecycle limits are checked between requests; the platform may end a sandbox earlier. - **Cloudflare:** an isolate can handle overlapping requests on its event loop. No application loop requests the next fetch. Retention must tolerate eviction and must not associate the first request's bindings or context with subsequent requests. -- **Spin:** the pinned SDK 6 HTTP macro exports WASI P3. The cached `spin-macro-6.0.0/src/lib.rs:131` explicitly exports `wasip3::http::service`. The Rust `wasm32-wasip2` build target does not establish a P2 HTTP lifecycle. Current Spin documentation describes P3 instance reuse, including concurrent invocation; P2 components have different behavior. +- **Spin:** the current pinned SDK 7 HTTP macro exports WASI P3. The Rust `wasm32-wasip2` build target does not establish a P2 HTTP lifecycle. Verify the emitted interface and host together; retained state must support overlapping invocations when the host permits them. - **Axum:** the existing long-running service already retains its router and permits concurrent requests. Provider references: @@ -79,7 +87,7 @@ These distinctions prohibit a universal sequential serving loop in core. - Provide the Fastly opt-in lifecycle and retained standard-app convenience. - Document concrete application-owned retention for Cloudflare and Spin. - Preserve fresh request conversion, contexts, extensions, and registry resolution. -- Preserve existing response translation and custom-dispatch escape hatches. +- Preserve canonical admission and adapter-owned lazy response egress for both default and retained dispatch. - Document lifecycle, concurrency, failure, refresh, and resource boundaries. - Provide framework contract tests and local lifecycle fixtures. @@ -97,7 +105,7 @@ These distinctions prohibit a universal sequential serving loop in core. - No new core `Hooks` methods, macro arguments, manifest keys, CLI reuse switch, or provider-independent scheduler. - No framework singleton registry keyed by application type. - No SDK upgrade solely for this feature; Fastly 0.12.1 already provides `Serve`. -- Standard dispatch retains body buffering and stream caps. The header-preservation and CLI diagnostic corrections listed above are explicit scope exceptions; custom callbacks own progressive response streaming. +- Retention introduces no response buffer or stream-collection cap. Standard and custom admitted dispatch use the authoritative outbound design's lazy response-egress contract. The header-preservation and CLI diagnostic corrections listed above remain historical scope exceptions. - No durable cache, automatic configuration refresh, parsed-key cache, or application-specific finalization hook. - No deployment, publishing, or external issue/PR activity as part of implementing this spec. @@ -105,7 +113,12 @@ These distinctions prohibit a universal sequential serving loop in core. ### 4.1 Preserve core ownership -Use the existing `edgezero_core::app::{App, Hooks, StoresMetadata}` and `edgezero_core::http::Extensions`. Do not add metadata or lifecycle state to `App`. +Use `edgezero_core::app::{App, Hooks, StoresMetadata}` and core `Extensions`. +`App::build::(PLATFORM) -> Result` is the canonical assembly +path. It installs target metadata before fallible `Hooks::configure`, propagates +its typed failure, and rejects a successful configuration that changes platform +metadata. Retain only a completed successful app; initialization errors are never +cached. Lifecycle counters and store metadata do not belong in `App`. `App` is structurally `Send + Sync`: handlers and middleware require these bounds, registered state requires them, and the router is reference-counted. Dispatch futures may remain non-`Send`. Add a compile-time assertion protecting the retained object's bounds without imposing `Send` on WASM futures. @@ -119,24 +132,27 @@ pub use fastly::http::serve::{Serve, ServeSummary}; pub fn serve_app(serve: Serve) -> ServeSummary; -pub fn serve_app_with_request_extensions( +pub fn serve_app_with_hooks( serve: Serve, - extend: F, + prepare: Prepare, + finalize: Finalize, ) -> ServeSummary where A: Hooks, - F: FnMut(&fastly::Request, &mut Extensions); + Prepare: FnMut(&mut fastly::Request, &mut Extensions), + Finalize: FnMut(&mut edgezero_core::Response) -> State; ``` -`serve_app` delegates with an empty extensions callback. Both helpers: +`serve_app` delegates with empty preparation and finalization callbacks. Both helpers: 1. Capture static `A::stores()` metadata. 2. Enter the SDK callback before performing host-dependent initialization. -3. Resolve the logging policy baked into `A`. -4. On the first callback only, honor `A::owns_logging()`, initialize adapter logging if enabled, and call `A::build_app()`. +3. Resolve immutable logging settings with `A::logging_for("fastly")`. +4. Complete logging setup through `Sandbox::setup_once`, honoring `A::owns_logging()`, then initialize with `App::build::(FASTLY_PLATFORM)` through `Sandbox::initialize`. 5. Retain the successfully constructed app for subsequent callbacks. -6. On every request, use `request::dispatch_with_registries` with fresh registries opened through the deployed logical resource-link aliases. -7. Return the SDK summary unchanged. Do not hide handler errors or synthesize a guaranteed request count. +6. Open fresh store handles through the deployed logical resource aliases on every dispatch; do not cache them with the application. +7. On every request, use `request::send_request_with_registries_and_hooks` with fresh registries, admission, and response-egress resources. Preparation borrows the native request before admission; finalization borrows routed responses before egress policy and framing. Detached responses skip finalization. +8. Return the SDK summary unchanged. Do not hide handler errors or synthesize a guaranteed request count. The logger's enablement, level, endpoint, and stdout policy come from the application manifest and are immutable for the binary. `owns_logging` retains @@ -144,9 +160,15 @@ its existing meaning: skip adapter logger installation. An application adopting retained construction must arrange its own logger installation before construction if construction needs logging. -Capture the mutable extensions callback in the outer serving closure and pass a fresh `&mut extend` reborrow to each `dispatch_with_registries` invocation. Its `FnOnce` parameter can call that borrowed `FnMut`; do not move the callback out of the serving closure on the first request. +Capture preparation and finalization callbacks in the serving closure and pass +fresh mutable reborrows on every request. Do not move either callback out on the +first invocation or retain request-owned values in its captures. -An initialization `Err` ends the SDK loop. `Hooks::build_app` is infallible by signature: a panic remains a sandbox failure, not a fabricated initialization `Result`. Do not retry after partially completed global initialization. +An initialization `Err` leaves `Sandbox` empty. Standard serving propagates that +error and terminates; custom callbacks may handle a recoverable failure and retry +on a later request. Configuration is fallible, and panics remain sandbox failures. +Partial global setup side effects are not rolled back, so custom retry policy must +make setup safe to repeat. Example entry point after implementation: @@ -162,15 +184,27 @@ The example deliberately uses ordinary `main`, not `#[fastly::main]`. Existing g ### 4.3 Fastly custom lifecycle -Use `lifecycle::serve_custom(Serve, callback)` for EdgeZero-owned custom lifecycle mechanics, or direct SDK serving as an escape hatch. Preserve access to the SDK's `HandlerResult` trait through its existing SDK path; a new framework callback trait is unnecessary. +Use `lifecycle::serve_custom(Serve, callback)` with +`FnMut(fastly::Request, &mut Sandbox) -> Result<(), E>`. The callback owns +terminal delivery. The wrapper records that result through the SDK without asking +the SDK to send a response or synthesize an error response. -A callback receives an owned native request and may mutate it, capture metadata, convert it with `request::into_core_request_with_registries`, dispatch through a retained router, inspect core response extensions, finalize the response, send or stream it explicitly, and finish request-owned post-send work before returning. +A callback receives an owned native request and may use +`request::send_request_with_registries_and_hooks` with a retained app, current +runtime configuration, and request-local hooks. Preparation can mutate the native +request and populate extensions; finalization can inspect or mutate routed core +responses before policy/framing. The adapter retains transmission ownership and +streams lazily through terminal delivery; no public raw-conversion or +response-returning dispatch helper is part of this path. Applications may capture retained state in a closure or pass it through `run_with_context`. Host-dependent or fallible initialization belongs inside the callback. A lightweight health response may bypass expensive initialization; retaining state must not require eager construction before every callback can run. -`request::dispatch_with_registries` is the complete prebuilt-app path when standard response translation is appropriate. `into_core_request_with_registries` exposes the request half for custom streaming; bare `into_core_request` injects no stores. Drive router dispatch with `futures::executor::block_on` in a synchronous callback. The immutable extensions callback is not a substitute for mutable raw-request custom dispatch. +Resolve baked logging once, initialize it before store-open boot diagnostics, +and pass fresh logical store registries through the canonical +send-owning path. Do not manually route a converted request and collect its body; +that would bypass admission or duplicate response-egress ownership. -The custom lifecycle extension adds `lifecycle::Sandbox`: an application-owned payload in a framework-owned successful-only slot, attempted-callback and initialization counters, and an independent successful setup guard. `initialize` returns errors unchanged and leaves the slot empty so a later call can retry; applications decide whether to respond and continue. `run_custom` creates fresh state for a single received request without entering `Serve`. Both wrappers complete `HandlerResult` exactly once. State decision logic is feature-independent for host tests; SDK wrappers require `fastly`. No teardown hook or response-finalization pipeline is introduced. Default generated entry points remain unchanged. +The custom lifecycle uses `lifecycle::Sandbox`: an application-owned payload in a framework-owned successful-only slot, attempted-callback and initialization counters, and an independent successful setup guard. `initialize` returns errors unchanged and leaves the slot empty so a later call can retry; applications decide whether to respond and continue. `run_custom` creates fresh state for a single received request without entering `Serve`. Both wrappers record terminal delivery exactly once without SDK resends. State decision logic is feature-independent for host tests; SDK wrappers require `fastly`. Default generated entry points remain unchanged. ### 4.4 Cloudflare and Spin prebuilt dispatch @@ -189,12 +223,12 @@ pub async fn dispatch_app( pub async fn dispatch_app( app: &App, req: spin_sdk::http::Request, -) -> anyhow::Result; +) -> anyhow::Result; ``` Use the same feature and target gates as each adapter's existing `run_app`. -These functions do not build an app, install logging, or cache anything. They resolve runtime configuration for that invocation, build registries using `A::stores()`, and call the existing internal registry-aware dispatcher. Callers must supply an app constructed from `A`; `App` erases its type, so the function cannot verify provenance. +These functions do not build an app, install logging, or cache anything. They resolve runtime configuration for that invocation, build registries using `A::stores()`, and run fresh admission and response egress through the internal registry-aware dispatcher. Callers must supply an app constructed from `A` with `CLOUDFLARE_PLATFORM` or the selected Spin platform; `App` erases its type, so the function cannot verify provenance. Spin returns the raw WASI response; its coordinator retains the body writer and transmission-result observation. For Cloudflare, derive `EnvConfig` before moving `env` into the dispatcher and keep the configuration alive through the awaited call that borrows it in `RegistryInputs`. For Spin, unpack `stores.config`, `stores.kv`, and `stores.secrets` into the existing dispatcher's separate metadata arguments. These are adapter wrappers, not changes to internal dispatcher signatures. @@ -209,33 +243,44 @@ Do not route these helpers through the manual Cloudflare builder or Spin `AppExt Document a static owned by the concrete application entry-point module: ```rust,ignore -use edgezero_core::app::{App, Hooks}; +use edgezero_adapter_cloudflare::CLOUDFLARE_PLATFORM; +use edgezero_core::{app::App, EdgeError}; use std::sync::OnceLock; static APP: OnceLock = OnceLock::new(); -fn retained_app() -> &'static App { - APP.get_or_init(MyApp::build_app) +fn retained_app() -> Result<&'static App, EdgeError> { + if let Some(app) = APP.get() { + return Ok(app); + } + let app = App::build::(CLOUDFLARE_PLATFORM)?; + Ok(APP.get_or_init(|| app)) } ``` -The fetch/HTTP callback obtains this reference synchronously and passes it and the current request arguments to `dispatch_app::`. +The fetch callback obtains this reference synchronously, maps initialization +failure to a category-safe Worker error, and passes the successful app and current +request arguments to `dispatch_app::`. For Spin, use `SPIN_PLATFORM` or +metadata for the selected host and return `anyhow::Result` from the +HTTP callback. A failed build leaves the cache empty. Concurrent attempts may +construct multiple successful candidates; only one is retained. Requirements for the example and API documentation: - The cache belongs to one concrete application. Never put an unkeyed static inside a generic `cached()`; such statics are shared across type instantiations. -- Perform any application-owned logging initialization before `get_or_init` if construction logs. Adapter logging is currently a no-op on these two targets; do not invent a new logger implementation in this change. +- Perform any application-owned logging initialization before `App::build` if construction logs. Adapter logging is a no-op on these two targets; do not invent a new logger implementation in this change. - The initializer is synchronous and must not recursively access the same cache. Do not block the event loop waiting on an async initializer or hold mutex/borrow guards across dispatch awaits. - Retain only the app, not a future, native request, Worker `Env`/`Context`, or store registry. - Binding/open failures occur during dispatch and are retried on a later request through normal request setup; they must not poison the app cache. -- The simple example does not define fallible/async initialization or refresh. Applications needing these manage an initialized-state owner and publish a complete successful snapshot before sharing it. No generic framework failure cache is added. +- The example propagates fallible synchronous construction without caching errors. Applications needing asynchronous initialization, serialized construction attempts, or refresh manage that owner and publish a complete successful snapshot before sharing it. No generic framework failure cache is added. - App middleware and shared state now survive across requests and can be accessed by overlapping invocations. The application must consent to that changed lifetime by explicitly adopting the example. ### 4.6 Axum Keep the existing construction and service ownership. Include Axum in the shared-state contract tests and documentation matrix, but add no reuse flag or alternate lifecycle helper. -This does not claim that all Axum per-request costs have already been optimized; for example, request conversion still creates proxy wiring. Such work is outside this app-retention change. +This does not claim that all Axum per-request costs have already been optimized; +request-local outbound wiring and admission/egress resources remain necessary. ## 5. Fastly limits and termination @@ -250,9 +295,23 @@ Expose SDK limit configuration without translating or duplicating it: The implementation must follow the pinned SDK's exact comparisons rather than introducing alternative threshold semantics. The platform may enforce additional limits, and request CPU/runtime limits still apply per request. -The pinned SDK stops after a handler error. It attempts a 500 containing the error's display text for `Result` handler returns. This is existing SDK behavior, not a new sanitization policy. - -Distinguish an SDK callback error from an application error rendered as an HTTP response. `RouterService::oneshot` already renders ordinary handler errors, including propagated proxy errors, through `IntoResponse`; an HTTP 4xx/5xx response alone does not stop reuse. If rendering an application error itself returns `Err`, `RouterService::oneshot` propagates that failure too. Errors that escape the adapter boundary, such as error-rendering failure, required-KV opening, inbound conversion/body reads, or response stream collection failures, reach the SDK as `Err` and terminate that sandbox. Recovery then occurs through fresh initialization in another sandbox. The later-request binding retry described in §4.5 applies to Cloudflare/Spin. Do not add a catch-all response conversion to `serve_app`: it would change termination/summary semantics, and the current dispatcher has already erased `EdgeError` into `fastly::Error` text. Test rendered application errors and escaping adapter errors separately. No client-triggerable reuse-denial scenario or performance regression is established without a reproducible workload. +The pinned SDK stops after a handler error. EdgeZero's serving wrappers use a +delivery-result adapter that records `Result<(), E>` without invoking another +send; an escaping error terminates serving without an SDK-generated 500. Direct +SDK result handlers have a different sending contract and are not the canonical +EdgeZero entry path. + +Distinguish a terminal delivery error from an application error rendered as an +HTTP response. Canonical app dispatch renders routing/handler errors through the +configured error renderer, and an HTTP 4xx/5xx alone does not stop reuse. Admission +refusals and bounded precommit fallbacks follow the authoritative outbound design; +they are not escaping SDK errors merely because they report failure status. An +error that escapes owned delivery terminates standard serving without a second +response attempt. Test rendered errors, admission aborts, and terminal delivery +failures separately. Custom callbacks may handle a recoverable initialization +failure before commitment; recovery after an escaping error requires a fresh +sandbox. Cloudflare/Spin resolve bindings again on later requests without caching +initialization failures. `ServeSummary::requests()` counts attempted callback invocations, not successfully completed responses: the SDK increments it before invoking the handler, including the callback that returns an error. A panic may prevent a summary from being returned at all. `ServeSummary` also reports handler wall time, wait wall time, and handler error. It does not expose all reasons for termination: a next-request wait error can end the loop with no summary handler error, and promise registration can panic. `into_result() == Ok(())` alone is not proof of healthy reuse or reaching the requested limit. @@ -268,19 +327,28 @@ and retained entry points; stream translation and response ownership stay unchan | Adapter/path | Existing behavior retained | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| Fastly standard | Duplicate headers are appended. A core stream is consumed into a Fastly body before the response is returned. | -| Fastly custom | Explicit header commitment, chunk pumping, stream finish/drop, response-extension inspection, and application finalization remain possible. | -| Cloudflare | Preserve current stream translation and native response ownership. | -| Spin | Preserve fully buffered output and the current 16 MiB cap on streamed response collection; already buffered bodies do not use that stream cap. | -| Axum | Preserve current conversion, which collects streamed bodies into an in-memory buffer before sending. | +| Fastly standard | Duplicate headers are appended; the adapter owns commitment, lazy chunk writes, terminal observation, and cleanup. | +| Fastly custom | Request preparation and routed-response finalization remain possible through borrowing hooks; canonical delivery stays adapter-owned. | +| Cloudflare | A request-local JavaScript stream coordinator registered with `Context::wait_until` owns backpressure, deadline/abort handling, and close. | +| Spin | A raw `SpinResponse` is backed by an owned WASI body/transmission coordinator; no whole-response collection or historical 16 MiB stream cap. | +| Axum | A connection-local Hyper body preserves lazy production and terminal response-egress observation. | -Fastly and Spin inbound conversions buffer bodies. This feature must not be described as providing end-to-end streaming or removing body costs. +Inbound bodies are consumed only after canonical admission, under the selected +limits and absolute deadline. Lazy downstream delivery does not imply unbounded +uploads, remove provider queues, or eliminate per-request body costs. ### 6.2 Explicit-send error ordering -Fastly callbacks returning `()` or `Ok(())` must have sent a response themselves. They must finish or abandon guest-owned streaming resources before returning. Successful return must not cause a second send. +Custom Fastly callbacks return terminal `Result<(), E>` after owned delivery. +They must finish or abandon guest-owned streaming resources before returning. +The lifecycle wrapper records that result without another send. -Before committing headers, a callback may return an SDK-supported error. After committing a final response, returning `Err` through the built-in `HandlerResult` attempts another send and can panic. Custom code must handle post-commit errors explicitly: finish/drop the stream, log safely, and either return a completed outcome after cleanup or deliberately terminate the sandbox if state cannot safely continue. Do not imply `Result<(), E>` automatically distinguishes pre-commit and post-commit errors. +Canonical response egress decides the bounded fallback before commitment and +never replaces a committed response. After commitment, a body failure follows +adapter-owned cleanup and terminal reporting. An escaping `Err` from an EdgeZero +lifecycle callback terminates serving without an SDK resend. Applications must +not hand a native response to a second SDK sender or bypass this path with raw +conversion/manual body collection. Complete required guest-owned post-send work within the Fastly callback. Finishing a guest response does not mean the peer has received every network byte; do not wait for network delivery acknowledgements. Cloudflare's documented request `wait_until` work remains tied to its own context, not to the retained app. @@ -294,21 +362,29 @@ Complete required guest-owned post-send work within the Fastly callback. Finishi Cloning a wrapper or satisfying `Send + Sync` is not proof of host-resource validity or request isolation. Persistent `Arc` interiors are intentionally shared; the framework does not clear their contents between requests. Existing same-type extension overwrite behavior must be tested and documented, not changed here. -Keep runtime configuration reads and store registry construction per request. Retained app state is a separate snapshot and will not automatically follow selector changes. A refresh must publish a coherent snapshot; in-flight requests keep the snapshot they acquired. Applications may choose not to refresh within an instance, but must document the resulting staleness policy. +Keep store registry construction per request. Cloudflare and Spin resolve their runtime configuration per invocation; Fastly opens immutable deployed logical aliases. Retained app state is a separate snapshot and will not automatically follow configuration changes. A refresh must publish a coherent snapshot; in-flight requests keep the snapshot they acquired. Applications may choose not to refresh within an instance, but must document the resulting staleness policy. Audit all process-global and thread-local state, including `OnceLock` caches, warning suppression, and recursion guards. Static warning suppression can reduce log volume across requests; warning rate must not be interpreted as failure rate. The current bounded recent-name set can evict entries, so suppression is not an absolute once-per-sandbox guarantee. Verify guard cleanup on supported exit/error paths; the presence of a thread-local guard alone does not demonstrate leakage. Audit result: core's recursion depth is thread-local and guarded by `SecretFieldsRecursionGuard`. Tests verify normal scope cleanup and host unwinding followed by fresh entry; host unwinding does not prove cleanup after a terminating WASM trap. Canonical-form instrumentation and environment locks are test-only. Fastly's missing-store warning sets are bounded, and their eviction tests cover repeated names. No request data belongs in these caches. -**Dynamic backends:** `proxy::ensure_backend` creates names from scheme, host, and port, so requests targeting many distinct origins exercise growing registration diversity. Current Fastly documentation describes node-level registration reuse even without sandbox reuse, and a service-wide concurrent dynamic-backend limit that blocks awaiting capacity rather than necessarily returning an immediate registration error. Do not model this as a per-sandbox counter that resets all capacity on every request. Use authorized, bounded origin sets and consider static backends for predictable origins. Test repeated and distinct origins, registration conflicts, and time waiting for capacity. Tune sandbox request limits using measured behavior; `with_max_requests` alone cannot bound service-wide registrations, fan-out within a request, or time blocked inside a request. The between-request lifetime check cannot interrupt a blocked registration. See [registration scope](https://www.fastly.com/documentation/guides/integrations/non-fastly-services/developer-guide-backends/) and [dynamic-backend limit behavior](https://www.fastly.com/documentation/reference/compute/errors). +**Dynamic backends:** Fastly outbound execution registers backends using +URI-derived identity and adapter policy; the outbound design owns naming, +timeout, and dispatch rules. Distinct origins can increase registration +diversity. Do not model service-wide capacity as a counter reset by app or +sandbox reconstruction. Use bounded authorized origin sets and measure repeated +and distinct origins, registration failures/waits, and memory. A between-request +lifecycle limit cannot preempt a blocked hostcall or bound fan-out within one +request. See [registration scope](https://www.fastly.com/documentation/guides/integrations/non-fastly-services/developer-guide-backends/) +and [dynamic-backend limit behavior](https://www.fastly.com/documentation/reference/compute/errors). Fastly environment variables describe the sandbox: `FASTLY_TRACE_ID` must not be treated as a unique request identifier in reusable mode. Capture `Request::get_client_request_id()` separately for request correlation, with a documented fallback when unavailable. Do not retain request correlation fields in the global logger or app state. ## 7. Compatibility and unresolved platform evidence - Existing Fastly, Cloudflare, and Spin `run_app` calls continue building each invocation; generated templates continue calling them. -- Existing Fastly `run_app_with_config`, manual service builders, raw conversion, and custom router dispatch retain their signatures and behavior. -- Existing `Hooks`, `app!` syntax, registered-state precedence, and provider feature gates remain unchanged. +- PR #275 supersedes historical raw conversion and response-returning custom dispatch. Fastly uses `Sandbox`, `serve_app`, `serve_app_with_hooks`, and canonical send-owning request hooks; no compatibility aliases restore removed APIs. +- `Hooks::configure` is fallible and `App::build::(PLATFORM)` is the canonical constructor. Retention does not change macro syntax, registered-state precedence, or provider feature gates. - No Tokio dependency is added to core or WASM adapters. Examples import portable HTTP types from core and use `#[action]` for any new handlers. - Reuse changes app-state lifetime only on explicit adoption. It must never silently become the default through a template or macro change. - Adopting retention changes the frequency of construction-owned logging setup. `owns_logging` still only disables framework installation; applications that refresh logging policy during each construction must move that refresh to explicit request setup or accept an initialization snapshot. The framework does not invoke a new application logging hook. @@ -330,16 +406,16 @@ Expected production changes: - `crates/edgezero-adapter-cloudflare/src/lib.rs`: `Hooks`-typed prebuilt dispatcher shared with `run_app`. - `crates/edgezero-adapter-cloudflare/src/response.rs`: the duplicate-header correction described in §6.1. - `crates/edgezero-adapter-spin/src/lib.rs`: `Hooks`-typed prebuilt dispatcher shared with `run_app`. -- Fastly `src/request.rs`: public registry-aware request conversion shared with buffered dispatch. +- Fastly `src/request.rs`: canonical admitted, send-owning dispatch with request preparation and routed-response finalization hooks. - CLI `src/config.rs` and Spin `src/cli.rs`: selected-adapter validation and diagnostic redaction listed above. -- `crates/edgezero-core/src/app.rs` or existing core tests: compile-time retained-object bounds and shared-state contract coverage; no new core production abstraction. +- `crates/edgezero-core/src/app.rs`: fallible platform-aware assembly and canonical admission/egress; retained-object bounds and shared-state contract coverage. Documentation changes during implementation: - `docs/guide/adapters/overview.md`: lifecycle matrix, retained versus request state, common opt-in contract. - `docs/guide/adapters/fastly.md`: default and reusable `main`, SDK limits, summary/error handling, standard and custom streaming paths, evidence limitations. - `docs/guide/adapters/cloudflare.md`: concrete app cache, fresh `Env`/`Context`, concurrent fetch isolation, cold initialization. -- `docs/guide/adapters/spin.md`: SDK6/P3 distinction, concrete app cache, host reuse controls and concurrency, buffering behavior. +- `docs/guide/adapters/spin.md`: SDK7/P3 distinction, successful-only concrete app cache, host reuse controls and concurrency, raw response-egress ownership. - `docs/guide/adapters/axum.md`: existing retained-router behavior and concurrent shared state. - `examples/app-demo/crates/app-demo-core/src/lib.rs`: qualify fresh-Fastly-instance language as default behavior. @@ -355,13 +431,13 @@ The listed surfaces are implemented; the original design-step authorization chec The current Fastly crate does not link native host tests when its `fastly` feature is enabled. Verification with `cargo test -p edgezero-adapter-fastly --features fastly --lib --offline` failed on arm64 with unresolved Fastly hostcalls, including `_uri_get`. This is pre-existing: the default workspace test gate omits that feature, while the feature-enabled `cargo check` gate type-checks without linking. Do not require native feature-enabled tests or add fake hostcall symbols to make them pass. -The original private-helper plan is superseded by [the custom serving lifecycle plan](../plans/2026-09-17-custom-serving-lifecycle.md). Public `lifecycle::Sandbox` contains feature-independent retention state with no Fastly SDK types or hostcalls; `serve_app` also retains private standard-initialization policy. Its production wrapper supplies runtime configuration, logging/build operations, and SDK dispatch. Host tests must exercise the same state transitions used in production, with counted injected operations or a counted real core app builder; a separate model that merely repeats the expected decisions is insufficient. Keep these mechanics local to the Fastly adapter; the public custom helpers do not introduce a cross-provider lifecycle abstraction. +The original private-helper plan is superseded by [the custom serving lifecycle plan](../plans/2026-09-17-custom-serving-lifecycle.md) and PR #275's outbound hard-cut. Public `lifecycle::Sandbox` contains feature-independent retention state with no Fastly SDK types or hostcalls; standard serving uses its independent setup and successful-app guards. The production wrapper supplies baked logging, fresh logical bindings, fallible platform-aware assembly, and canonical send-owning dispatch. Host tests must exercise those same transitions, not a duplicate model. Keep these mechanics local to Fastly; no cross-provider lifecycle abstraction is introduced. Assign verification explicitly: -- **Default host tests:** retained build/configure counts, first-initialization decisions, logger ownership/order through injected operations, degraded-then-successful configuration inputs preserving the first logging snapshot, terminal initialization errors, and fresh-owner reset. Pure core tests cover extension/state behavior without provider resources. Run `cargo test -p edgezero-adapter-fastly --lib` for the adapter logic. +- **Default host tests:** retained build/configure counts, baked logger ownership/order through injected operations, terminal initialization errors, and fresh-owner reset. Pure core tests cover extension/state behavior without provider resources. Run `cargo test -p edgezero-adapter-fastly --lib` for the adapter logic. - **Feature-enabled checks:** type-check SDK wrapper integration and callback bounds, including target-specific checks. These checks do not prove runtime behavior. -- **WASM/local provider fixtures (§9.2):** actual callback/build counts in reused instances, runtime-store failure/recovery where supported by controlled fixtures, real named-endpoint log delivery, native metadata/handles, stream completion, SDK summary counts/termination, and all other hostcall behavior. Fault injection proves only the injected case; unavailable provider failure modes must be reported separately. +- **WASM/local provider fixtures (§9.2):** actual callback/build counts in reused instances, logical binding failure/recovery where supported by controlled fixtures, real named-endpoint log delivery, native metadata/handles, stream completion, SDK summary counts/termination, and all other hostcall behavior. Fault injection proves only the injected case; unavailable provider failure modes must be reported separately. The requirements below span these layers. They do not require provider calls inside native colocated tests. The warning about feature-disabled no-op stubs excludes claims about provider behavior, not valid tests of shared feature-independent production logic. @@ -369,8 +445,8 @@ The requirements below span these layers. They do not require provider calls ins - Prove a concrete cache cannot return another application's router. No generic unkeyed static may appear in production or examples. - Verify fresh extensions, route parameters, headers, bodies, request IDs, and native metadata on successive requests. - Include same-type app/request extension collisions and intentionally shared `Arc` mutation, distinguishing intended sharing from leakage. -- Verify store metadata, named/default bindings, runtime selectors, missing-store behavior, and recovery after a request-specific binding failure. -- Verify logger ownership, ordering, first-initialization policy, and Fastly named-endpoint output on subsequent requests. Include a degraded first runtime-store read followed by a successful read: selectors recover while the logging snapshot remains disabled, as specified in §4.2. +- Verify store metadata, named/default bindings, deployed logical aliases, missing-store behavior, and recovery after a request-specific binding failure. Verify invocation-scoped selectors separately on targets that support them. +- Verify logger ownership, ordering, baked initialization policy, and Fastly named-endpoint output on subsequent requests. A binding failure must not change the release's logging policy. - Verify standard errors and custom send outcomes; initialization errors, constructor panic, pre-commit errors, post-commit stream errors, and later fresh initialization are separate cases. - Keep existing duplicate `Set-Cookie` and body tests. Add actual delayed-chunk/first-byte tests for paths that support progressive client streaming. - Compile examples against the locked SDKs on their actual targets. Host tests with feature-disabled no-op stubs do not validate provider behavior. @@ -389,7 +465,7 @@ Observed with the pinned standalone Viceroy 0.17.0: a guest admits at most six r - **Fastly:** run A/B/C below, stream completion, duplicate cookies, repeated logging, limit/idle termination, and restart isolation. - **Cloudflare:** use a local Workers runtime to compare per-fetch build versus retained concrete app; force overlapping requests with distinct inputs and a controlled await barrier. Restart to validate cold initialization. Record availability/version rather than assuming the runtime exists. -- **Spin:** run the SDK6/P3 fixture on a compatible local host, with both sequential and concurrent instance reuse. Exercise supported reuse-count/concurrency/idle controls and record chosen values. +- **Spin:** run the SDK7/P3 fixture on a compatible local host, with both sequential and concurrent instance reuse. Exercise supported reuse-count/concurrency/idle controls and record chosen values. - **Axum:** verify one build per server start and independent overlapping requests against the shared router. Use it as a retained-app reference, not as a proxy for WASM performance. A sequential request sequence alone does not prove reuse: observe a stable instance identifier and an increasing request ordinal. A runtime process ID alone is insufficient because one host process can create many guest instances. @@ -402,16 +478,20 @@ A sequential request sequence alone does not prove reuse: observe a stable insta | B | SDK `Serve` | App construction on every callback; global logging installed once | | C | SDK `Serve` | App construction and global logging once per sandbox | -B's logging qualification is mandatory: blindly wrapping the enabled-logger `run_app` would fail on its second installation and confound the comparison. Test that failure separately as a negative control. Keep all other dispatch, configuration, response, and workload choices identical between variants. Test standard dispatch and custom progressive streaming as separate comparable workloads; never compare buffered A against streaming C. +B's logging qualification is mandatory: blindly wrapping the enabled-logger `run_app` would fail on its second installation and confound the comparison. Test that failure separately as a negative control. Keep dispatch, configuration, lazy delivery, and workloads identical between variants. Do not compare historical buffered helpers against current streamed delivery. -Implement B through the custom path in §4.3: `Serve::run`, a once-only logging initialization decision from the baked manifest settings, per-callback `A::build_app()`, then `request::dispatch_with_registries`. Do not use retaining `serve_app` for B. Apply the same baked logging policy in B and C. +Implement B through `lifecycle::serve_custom`: a successful once-only baked logging +decision, per-callback `App::build::(FASTLY_PLATFORM)`, +then `request::send_request_with_registries_and_hooks`. Do not use retaining +`serve_app` for B. Apply the same baked logging policy in B and C so configuration +does not introduce a second experimental variable. Collect: - Sandbox start count, request ordinal, app/configure initialization count, and separately instrumented application initialization phases. - Attempted requests per sandbox, including one-request sandboxes and early exits. Record response commitment, guest-side completion, errors, and client-observed completion separately; do not equate the SDK attempt count with successful responses. Emit per-request records because crashes may prevent a final summary. - Client time to first byte and completion latency, with cold versus reused distributions and p50/p95/p99 plus sample counts. -- Initialization/handler wall time and CPU observations. SDK summary wall time is not CPU time; returned-response sending can occur outside callback timing. +- Initialization/handler wall time and CPU observations. SDK summary wall time is not CPU time; current Fastly callbacks include adapter-owned terminal delivery rather than handing a response to another sender. - SDK CPU phase deltas where available. The pinned SDK warns against cross-run benchmarking with its vCPU clock; corroborate comparisons with controlled profiling or platform telemetry and report precision/unsupported metrics. - Memory before initialization, after initialization, and after each request's cleanup; request-ordinal slope, peak, and plateau. Distinguish guest linear-memory high-water marks, SDK host-inclusive snapshots, and host process RSS. MiB rounding cannot prove absence of small leaks. @@ -445,7 +525,7 @@ The implementation is acceptable when: - Existing generated/default entry points retain their lifecycle defaults, with the separately listed compatibility and CLI corrections. - Fastly standard apps can opt into a bounded SDK serving lifecycle with exactly one successful app initialization per sandbox. -- Custom Fastly callbacks can retain their own state, mutate raw requests, preserve response extensions, explicitly stream, and complete post-send work. +- Custom Fastly callbacks can retain successful app state, prepare raw requests, inspect/finalize routed response extensions through hooks, and use canonical lazy delivery without another sender. - Cloudflare and Spin can use concrete retained apps with full metadata-aware per-request dispatch and tested overlapping-request isolation. - Axum's existing retained-router behavior remains intact. - Fresh initialization, failure recovery, bounded state, duplicate headers, and existing response semantics are validated. diff --git a/docs/superpowers/specs/2026-09-28-platform-resource-metadata-design.md b/docs/superpowers/specs/2026-09-28-platform-resource-metadata-design.md new file mode 100644 index 000000000..10d545b27 --- /dev/null +++ b/docs/superpowers/specs/2026-09-28-platform-resource-metadata-design.md @@ -0,0 +1,152 @@ +# Platform Resource Metadata Design + +**Date:** 2026-09-28 +**Status:** Approved for implementation + +## Purpose + +Applications can currently read a target memory ceiling and whether that ceiling is per execution +or shared by one runtime instance. That is insufficient for fail-closed startup validation: a shared +ceiling also needs the maximum number of live inbound request executions charged to it, and every +ceiling needs an explicit statement about whether provider-owned ingress framing memory is charged +against it. Absence must remain distinguishable from a known value. + +This design publishes those facts without moving application admission policy into EdgeZero. + +## Platform facts + +`edgezero-core` owns a generic fact wrapper: + +```rust +pub enum PlatformFact { + Known { + value: Value, + source: PlatformResourceSource, + }, + Unknown { + reason: PlatformUnknownReason, + }, +} +``` + +`PlatformResourceSource` identifies a published platform limit, a hosted default, or a runtime +configuration. `PlatformUnknownReason` distinguishes operator configuration, runtime configuration, +provider-unpublished facts, and unspecified metadata. Unknown is data, not an absent field and not +permission to substitute an unrelated provider limit. + +`PlatformMetadata` carries three facts: + +- `PlatformFact`; +- `PlatformFact`, whose non-zero value is the maximum number of live + inbound request executions charged to the same memory ceiling; +- `PlatformFact`, whose known values are `CountsTowardCeiling` and + `OutsideCeiling`. + +`MemoryCeiling` retains exact primary bytes, optional separately published stack bytes, and +`MemoryCeilingScope`. Provenance moves to the fact wrapper so every resource fact uses one source +model. + +## Canonical target facts + +| Target | Memory ceiling | Live requests per memory domain | Host ingress/framing charge | +| --- | --- | --- | --- | +| Axum | Unknown: operator configured | Unknown: operator configured | Unknown: operator configured | +| Cloudflare Workers | Known: 128,000,000 bytes per instance | Unknown: provider unpublished | Unknown: provider unpublished | +| Fastly Compute | Known: 128,000,000-byte heap plus separate 1,000,000-byte stack per execution | Known: 1 per execution | Unknown: provider unpublished | +| Spin (generic) | Unknown: runtime configured | Unknown: runtime configured | Unknown: runtime configured | +| Akamai Functions (Spin) | Known hosted default: 134,217,728 bytes per execution | Known hosted default: 1 per execution | Unknown: provider unpublished | + +The Cloudflare six-simultaneous-connection limit is not an inbound population bound. It applies to +outbound connections waiting for headers within one invocation and must not populate this metadata. +All adapters currently receive host-parsed requests, so EdgeZero cannot infer host parser or HPACK +accounting from guest-visible allocations. + +## Validation + +Applications describe the arithmetic they intend to validate with a scope-tagged `MemoryEnvelope`: + +```rust +pub struct MemoryEnvelope { + scope: MemoryCeilingScope, + fixed_bytes: u64, + per_live_request_bytes: u64, + separate_stack_bytes: Option, +} +``` + +`PlatformMetadata::validate_memory_envelope` uses checked arithmetic and returns: + +- `Fits` only when the ceiling, request-population bound, and host-ingress accounting are all known, + the envelope scope matches the ceiling scope, primary required bytes do not exceed the primary + ceiling, and any separately published stack allowance has a supplied requirement within its + limit; +- `Exceeds` when the known minimum primary or separate-stack requirement already exceeds its + ceiling, even if another fact is unknown; +- `Indeterminate` with typed reasons whenever an unknown fact prevents a complete result. + +For `PerExecution`, required bytes are `fixed + per_live_request`; the population bound must be one +when known. For `PerInstance`, required bytes are `fixed + per_live_request * max_live_requests`. +When the per-instance population is unknown, the validator uses one as the checked minimum only to +decide whether `Exceeds` is already certain; otherwise the result remains `Indeterminate`. + +A separately published stack ceiling is checked independently. Omitting the envelope's separate +stack requirement when the platform publishes one makes the result `Indeterminate`; supplying a +separate stack requirement when the platform publishes no separate allowance is an error because +the primary envelope must then include that memory. An envelope whose scope differs from the +published ceiling is an error, as is arithmetic overflow or a non-one population bound attached to +a per-execution ceiling. These failures use a public typed validation error rather than strings. + +`per_live_request_bytes` must include every charge the application claims is covered. If host +ingress memory is known to count toward the ceiling, the caller must include its bounded charge. An +unknown accounting fact prevents `Fits`; callers cannot override it through this API. + +The validation result describes the complete published memory domain. Applications may separately +validate narrower, explicitly named managed budgets, but must not reinterpret `Indeterminate` as a +complete fit. + +## Adapter API + +The CLI adapter trait hard-cuts `memory_ceiling()` to `platform_metadata()`. Runtime entrypoints and +CLI adapters return the same canonical `PlatformMetadata` constant, preventing each new resource +fact from creating another parallel trait method. The default returns fully unknown metadata. + +No compatibility alias remains. + +## Batch observation cutoff + +The batch method names remain unchanged. Public parameter names, examples, and documentation use +`observation_cutoff`. The contract states that this instant stops observation regardless of a +slot's remaining request budget. Passing an earlier instant intentionally produces unresolved slots +at cutoff. + +No debug assertion requires the observation cutoff to cover every request deadline: early cutoffs +are a supported partial-result operation. `BudgetSource::BatchCutoff` continues to identify the +selected effective slot budget and does not, by itself, signal batch termination. + +## Migration record + +This is the historical platform-metadata migration record. Its five-argument response +constructor is superseded by the outbound design's six-argument constructor carrying +original Content-Type provenance; no five-argument compatibility API remains. + +The repository gains an unreleased changelog with an exact migration table: + +| Removed or changed API | Replacement | +| --- | --- | +| `MemoryCeilingSource` | `PlatformResourceSource` inside `PlatformFact::Known` | +| `MemoryCeiling::new(total, scope, stack, source)` | `MemoryCeiling::new(total, scope, stack)` wrapped in `PlatformFact::known(value, source)` | +| `MemoryCeiling::source()` | Match or inspect the enclosing `PlatformFact` | +| `PlatformMetadata::new(Option)` | `PlatformMetadata::new(memory, inbound_population, host_ingress_accounting)` | +| `PlatformMetadata::memory_ceiling() -> Option<_>` | `PlatformMetadata::memory_ceiling() -> PlatformFact<_>` | +| `Adapter::memory_ceiling()` | `Adapter::platform_metadata()` | +| overridable `Hooks::build_app()` and infallible `configure` | `App::build::(platform)` plus fallible `Hooks::configure` | +| four-argument `OutboundResponse::new` | five-argument constructor requiring the application `MonotonicClock` | + +The demo application and generated project use the same current APIs; no stale compatibility path +is retained. + +## Verification + +Tests cover fact construction, each canonical adapter profile, every validation outcome, scope and +arithmetic failures, per-execution population invariants, documentation drift, generated projects, +the demo workspace, native feature matrices, and all three WASM targets. diff --git a/docs/superpowers/specs/2026-10-05-logging-ownership-boot-diagnostics-design.md b/docs/superpowers/specs/2026-10-05-logging-ownership-boot-diagnostics-design.md new file mode 100644 index 000000000..98ab33aba --- /dev/null +++ b/docs/superpowers/specs/2026-10-05-logging-ownership-boot-diagnostics-design.md @@ -0,0 +1,63 @@ +# Logging Ownership and Boot Diagnostics + +## Contract + +`Hooks::owns_logging() == true` means the application owns backend installation, +backend filters, and the `log` facade maximum. Adapters must not replace or +reconfigure that logger. Applications install it before the adapter entrypoint. + +Core exposes a pure `resolve_logging_level(&EnvConfig) -> log::LevelFilter` +helper for `EDGEZERO__LOGGING__LEVEL`. It preserves the existing case-insensitive +parser and `Info` fallback for missing or invalid values; it does not install a +logger, read process environment, or change global filters. Manifest logging +resolution remains separate and is not implicitly applied by this helper. + +`BOOT_LOG_TARGET` names `edgezero::boot`; `BOOT_LOG_LEVEL` is `Warn`. Adapter-owned +Fastly and Axum logging preserves errors and warnings on that target even when +ordinary logging is `Error` or `Off`. Each backend and the facade must admit the +maximum of the ordinary and boot levels, while the outer target-aware filter +continues to suppress ordinary records above their configured level. The boot +target is a category, not a new Fastly log endpoint. + +Fastly `runtime_env_config` returns `FastlyRuntimeConfig { env, ... }`, hard-cutting +the former direct `EnvConfig` return. An unavailable optional runtime store sets +one boolean diagnostic flag. `emit_boot_diagnostics()` consumes its fixed warning +once after backend installation; missing backends do not gain implicit output. +`run_app` and `run_app_with_hooks` emit it even when application assembly returns +an error. The explicit-config runner does not read that runtime store. + +Install adapter-owned logging before application configuration. Diagnostics +generated while resolving logger configuration must be retained as bounded +typed/static state and emitted after installation, not as an unbounded log +buffer. When an application owns logging, the same boot records use its backend +and filters; visibility depends on the application's policy. + +Cloudflare and Spin logger initializers remain explicitly documented no-ops. +Their platform consoles do not automatically install a Rust `log` backend. +This change adds neither a logger dependency nor a new capability there. + +The bundled CLI must not install its CLI logger before running the Axum demo. +Axum's `run_app_with_preflight` runs reusable startup checks after logging setup +and before application configuration or listener binding. The demo uses that +boundary for capability validation. Axum returns logger-setup errors rather than +silently using a different backend. CLI terminal errors use stderr regardless of +the runtime filter. +Generated adapter entrypoints continue delegating to reusable adapter code; +logging policy must not be duplicated in templates. + +## Acceptance + +- Resolver coverage includes every level, absent/invalid values, and no global + logging side effects. +- Ordinary records retain their configured filter; boot `Error`/`Warn` survive + ordinary `Error`/`Off`; boot `Info`/`Debug`/`Trace` remain suppressed. +- Fastly backend filtering and the facade ceiling cannot contradict the boot + target policy. +- Configuration executes after adapter-owned logger setup; application-owned + logger/filter state is not changed. +- Startup warning deferral is bounded and emits each diagnostic once. +- Direct Axum and bundled demo use the same runtime logging configuration. +- Documentation, generator output, and demo describe ownership accurately. + +This is a logging API/documentation improvement, not an outbound correctness or +provider-certification prerequisite. diff --git a/docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md b/docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md new file mode 100644 index 000000000..fbc97fb5e --- /dev/null +++ b/docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md @@ -0,0 +1,2323 @@ +# EdgeZero Requirements for the Trusted Server Cutover + +**Date:** 2026-10-17 + +**Status:** Draft for the EdgeZero implementers; reviewed 2026-10-18, implementation and qualification pending + +**EdgeZero base:** PR 275 head `f6c708ccb5be19ba9fa92575db3c08c0f17e0d72` (branch `docs/outbound-http-spec`), called **F** below + +**Consumer:** Trusted Server `main` at `a9037770a`, which pins EdgeZero `v0.0.9` today + +**Companion:** Trusted Server's `docs/superpowers/specs/2026-10-17-edgezero-pr275-hard-cutover-design.md` (branch `spec/edgezero-pr275-migration`), the Trusted Server side of the cut. It is being revised to the decisions in this document. + +## Scoped Amendment (2026-10-24) + +The requester's [scoped timing and native integration approval](../../audits/2026-10-18-outbound-api-approval.md#scoped-timing-and-native-integration-amendment) +takes precedence over older hard scheduler guarantees and preimplementation H(K)/timing-only G2 +gates in this document. Logical deadlines, sources and late-result rejection remain unchanged. +The +5 ms windows remain measured qualification targets with their existing tests, not guaranteed +scheduler resumption. The synchronous optimizer blocking horizon is explicitly **UNKNOWN** until +provider evidence; native integration may proceed without a fabricated finite horizon. Hosted and +performance qualification remain blocked, and historical negative observations remain unchanged. + +The Send-first checkpoint is an implementation sequencing decision, not approval of different +signatures. It uses the current trait and existing `execute`/`process_response`, leaving the +ordinary SDK send branch unchanged. Full EZ-1 settlement, Follow, B/backend policy, the new engine +and atomic head-first/full-trait migration remain open. Backend B/resource constraints, policy, +timers and slack are not approved or changed. Typed body-error preservation and fresh-readiness/ +subsequent-read safety remain requirements; an uninterruptible body-read fallback is not permitted. +Acceptance scope, IDs, statuses, applicability and comparison gates are unchanged. + +## Purpose + +Trusted Server (TS) is moving onto EdgeZero PR 275 in one hard cut. TS will use EdgeZero's +canonical request lifecycle on all four adapters and EdgeZero's outbound HTTP API for every +outbound request, and it will delete its own transport layer. TS keeps no HTTP or transport +plumbing of its own. Where F cannot do what TS does in production today, the gap is a requirement +in this document, not a TS workaround. + +This document states what EdgeZero must do, not how. Each requirement gives the Trusted Server need, +the gap at F with code citations, normative requirements on observable behavior, acceptance +criteria, what is out of scope, and open questions for the EdgeZero maintainers. API shape, internal +design, and test harnesses are the EdgeZero implementers' choice. + +## Decisions behind these requirements + +The TS maintainer made these decisions. The requirements assume them. + +1. **Hard cut.** TS moves to the PR 275 APIs with no backward compatibility, no shims, and no dual + paths. +2. **No TS plumbing.** TS deletes its own outbound HTTP layer, backend naming, raw-SDK clients, + response decoder, encoding strip, redirect loop, and Fastly response delivery code. Every + outbound call goes through EdgeZero's outbound API. +3. **Canonical Fastly lifecycle.** TS Fastly moves onto EdgeZero's canonical send path inside its + custom entry point, with TS logic in the lifecycle's prepare and finalize hooks. Production runs + single-request sandboxes. Reusable-sandbox mode stays off by default + (`ts:crates/trusted-server-adapter-fastly/Cargo.toml:13-18`), so requirements specific to reuse + mode are follow-ups needed before TS enables it. +4. **Redirects move into EdgeZero.** TS's per-hop redirect loop is deleted in favor of EZ-9. +5. **Blocking set.** The cut cannot ship to production without the blocking parts of EZ-1 through + EZ-9 and a passing EZ-Q1 record. The record covers every acceptance criterion of those blocking + parts, each in the environment it names, together with the criteria of any promoted requirement + and of follow-ups present at the pinned commit (EZ-Q1.6). Criteria of reuse-mode follow-ups gate + only before TS enables reuse mode. Everything else is follow-up unless the maintainers accept a + change from [Proposed priority changes](#proposed-priority-changes). + +## How Trusted Server will use EdgeZero after the cut + +This is context for the requirements, not a requirement on EdgeZero's design. + +- **Lifecycle.** Fastly runs EdgeZero's canonical send path with TS's client-IP sanitization and TLS + signals in the prepare hook, and TS response finalization, EC finalization, privacy guards, and + sandbox counters in the finalize hook. The state finalize returns drives post-send EC pull sync. + Axum, Cloudflare, and Spin run EdgeZero's standard entry points. TS sets the response egress + policy, head limits, and error renderer through `Hooks::configure`. +- **Auction.** Bidders become outbound requests that run as one batch per auction with the auction + deadline as the cutoff. The mediator is one ordinary send after the bidders. +- **Publisher origin.** On the overlapped path (a navigation `GET` with EC preload, + `ts:crates/trusted-server-core/src/publisher.rs:4821-4846`), the origin fetch begins before bid + construction, runs alongside the bidder batch, and streams its response (EZ-6). Bids are collected + at the `` hold while the page streams. Every other origin forward, including `POST`, + `PUT`, `PATCH`, `DELETE`, and `OPTIONS`, is an ordinary send accepting the inbound body stream. + EdgeZero may prebuffer it under the upload cap on Axum and Cloudflare (EZ-10.6). Both are built + from the inbound request (EZ-1). +- **Proxies and integrations.** About twenty call sites send through EdgeZero. They include the + first-party proxy, asset routes (with Fastly Image Optimizer and S3 SigV4 signing, EZ-8), GTM, + GPT, the JS asset proxy, the Prebid bundle, Lockr, Permutive, DataDome, Sourcepoint, Didomi, and + testlight. Redirects follow through EZ-9. Encoded bodies pass through undecoded where TS relays + bytes (EZ-5). +- **Out-of-request work.** EC pull sync runs after delivery, inside the same lifecycle callback, + with the request's outbound client (cross-cutting requirement 4). + Tinybird telemetry is fire-and-forget (EZ-15). The Fastly management API runs inside admin + handlers. +- **Inbound bodies.** Endpoints read bodies with EdgeZero's bounded readers at their own caps + (EZ-10 restores 413). + +## Requirement index + +| ID | Title | Priority | Adapters | +| ----- | --------------------------------------------------------------------------------- | ---------------- | ------------------------------------------ | +| EZ-1 | Bodiless GET/HEAD forwarding | Blocking | all four | +| EZ-2 | Fastly connection reuse across requests | Blocking in part | Fastly | +| EZ-3 | Cold backend registration never fails a request on its own | Blocking | Fastly | +| EZ-4 | Connect timeout independent of the total budget | Blocking | all four | +| EZ-5 | Encoded response passthrough | Blocking | all four | +| EZ-6 | Begin a request without waiting, and observe a batch without blocking | Blocking | all four | +| EZ-7 | Keep the `x-edgezero-proxy` header off relayed responses | Blocking | all four | +| EZ-8 | Fastly Image Optimizer on outbound requests | Blocking | Fastly; others reject | +| EZ-9 | Redirect following with the application's target policy at every hop | Blocking | all four | +| EZ-10 | 413 Payload Too Large for request-body overflow | Follow-up | all four | +| EZ-11 | Cache tagging with surrogate keys | Follow-up | Fastly, Axum, Spin; Cloudflare unsupported | +| EZ-12 | TLS certificate verification mode | Follow-up | Fastly; others unsupported | +| EZ-13 | Spin host authority override | Follow-up | Spin | +| EZ-14 | Idle-time bound for streamed response bodies | Follow-up | all four | +| EZ-15 | Explicit fire-and-forget send | Follow-up | all four | +| EZ-16 | Provider failure detail available to logs | Follow-up | all four | +| EZ-17 | Fastly send outcome tells the application what was committed | Follow-up | Fastly | +| EZ-18 | `Content-Length` on buffered responses | Follow-up | all four | +| EZ-19 | Application headers on detached and egress-fallback responses | Follow-up | all four | +| EZ-20 | A declared store that cannot open fails at use, not on every request | Follow-up | Fastly; Cloudflare and Spin should | +| EZ-21 | Framing failure reason in logs | Follow-up | all four | +| EZ-22 | Fallible application state built and retained with the routes | Follow-up | all four | +| EZ-23 | Axum dev server applies application policies, and tests can run real Axum ingress | Follow-up | Axum | +| EZ-24 | Published outbound test double that behaves like production | Follow-up | core | +| EZ-25 | Documentation drift fixes | Follow-up | docs | +| EZ-Q1 | Recorded passing qualification run of the canonical Fastly composition | Gate | all four | + +## Cross-cutting requirements + +These apply to every requirement in this document. + +1. **Every send mode honors every option.** An outbound option (decoding mode, proxy marker, connect + timeout, idle bound, cache intent, TLS verification, Image Optimizer) behaves the same on a + single send, a begun request (EZ-6), a batch slot, and every redirect hop (EZ-9), and on a + fire-and-forget send once EZ-15 lands. Where a requirement marks a send mode Unsupported for an + option, that send mode rejects it (requirement 2). No send mode silently drops an option. + EZ-9's method rules and trusted policy may change the next request's options; its original + absolute chain deadline cannot be extended. EZ-9.11 exempts followed redirect bodies from + ordinary body processing; final responses retain their effective request's options. +2. **One rejection for unsupported options.** An option whose requirement marks an adapter or send + mode Unsupported is rejected there before anything is sent, with one documented error category + that is the same on every adapter and consistent with F's other outbound preflight rejections. + This rejection comes before every other outbound preflight check. An option marked BestEffort is + accepted, and its weaker guarantee is documented in the capability matrix. +3. **Bounded memory.** Every response EdgeZero holds or drains on the application's behalf, + including responses that arrive while the application is busy elsewhere, stays within that + request's encoded, decoded, and final caps. For EZ-9.11's optional intermediate-body drain, + reaching a cap stops the drain rather than failing the chain. +4. **Request lifetime.** For outbound purposes an inbound request's lifetime ends only after its + per-request serving callback has returned and its adapter-owned response egress has completed or + aborted. Returning a response or its head is not egress completion. On Axum, Cloudflare, and + Spin, response-owned outbound work survives the handoff to the response body or egress + coordinator. On Fastly, work inside the same serving callback after delivery, such as TS's EC + pull sync, can use the request's outbound client. Response-owned work is released when that + egress ends; separate callback-owned work is released when the callback ends. Neither waits for + client-delivery evidence the provider cannot expose. Anything still in flight at request lifetime + end is released, except a fire-and-forget send, whose lifetime EZ-15 defines. +5. **Arrival decides lateness.** A response arrives when its outcome is final. For a buffered batch + slot or buffered begun request that is when its last body byte was received, or when the error or + cap failure that ended it occurred. For a streamed begun request it is when its response head was + received. Lateness is judged at the earliest instant EdgeZero observed the arrival, never at when + the application collected the result. Where an adapter can observe arrivals only during EdgeZero + operations (an outbound wait, a body read, or a non-blocking check), it records each arrival no + later than the next such operation, and the requirement states that adapter's capability level. +6. **Abandon releases; fire-and-forget is explicit.** Dropping or cancelling an in-flight request or + batch releases it on every adapter as far as the host allows. Sending without waiting for the + result is a separate, explicit capability (EZ-15). This document calls that a fire-and-forget + send. "Detached" keeps EdgeZero's meaning of a response produced outside routing. +7. **Diagnostics stay off the wire.** Provider failure detail, framing failure reasons, and payload + limit reasons are available to application logs and never reach a response. Error text that + applications and clients see today does not change. +8. **No silent regressions.** Behavior TS relies on at F (deadline enforcement, admission and + framing checks, reusable-sandbox serving, existing public APIs) keeps working unless a + requirement changes it explicitly. +9. **Capability matrix.** Each requirement's per-adapter capability level (Native, BestEffort, + Unsupported) appears in EdgeZero's capability matrix. Fixing guide text that is wrong at F is part + of the requirement it concerns. Requirements do not ask EdgeZero to edit its immutable + `docs/superpowers/specs/`. +10. **Checkable where it matters.** Each acceptance criterion names the environment where the + behavior must hold, from among unit, Viceroy 0.21.0, hosted Fastly, workerd, Spin 4.1, and Axum. + Viceroy 0.21.0 enforces first-byte and between-bytes timers on dynamic backends and reports their + expiry as a generic host error (`viceroy:src/upstream.rs:351-372`, `viceroy:src/body.rs:235-244`). + It stores the connect timeout without enforcing it, so connect-time behavior is checked on hosted + Fastly. A reused Viceroy guest serves at most a handful of requests, Viceroy reports a refused + connection without detail, and its HTTP layer rejects more than 100 header lines, so criteria + that depend on those facts name hosted Fastly or are written around them. "Every adapter" in a + criterion means Axum, workerd, Spin 4.1, and Viceroy 0.21.0. A criterion has a hosted Fastly leg + only where it names hosted Fastly. + +## Dependencies between requirements + +- EZ-6's overlapped origin fetch is a GET built from the inbound request, so it needs EZ-1. On + Fastly its timelines rely on EZ-2's exact deadline enforcement, including verified + deadline-bounded response-body readiness and reads. An uninterruptible body-read fallback + cannot qualify for the cutoff and non-blocking observation requirements. +- EZ-9's hops rewritten to GET or HEAD are bodiless, so it needs EZ-1. Each new redirect origin is a + new Fastly backend, so it interacts with EZ-2. +- EZ-3 depends on EZ-2. A cold registration can only stop failing requests on its own once exact + deadlines are enforced independently of registration timing. +- EZ-4's connect timeout and EZ-14's idle bound change how Fastly backends are configured, so they + interact with EZ-2's connection reuse. +- EZ-5 does not depend on EZ-18. A preserved upstream `Content-Length` is the application's to + relay, and EZ-18 separately governs lengths on buffered responses EdgeZero frames. +- EZ-8 and EZ-11 conflict on Fastly. The platform disables Image Optimizer under cache bypass and + ignores surrogate keys with Image Optimizer, so those combinations are rejected (cross-cutting + requirement 2). +- EZ-15 and EZ-6 share abandonment semantics (cross-cutting requirement 6). +- EZ-16, EZ-17, and EZ-21 share the log-only diagnostics of cross-cutting requirement 7. +- EZ-20's "nothing was sent" outcome is the one EZ-17 reports, once EZ-17 lands. +- EZ-22 changes what canonical Fastly entry points do when assembly fails, EZ-17 reports it, and + EZ-23's in-process tests build on EZ-22's state. +- EZ-24 follows EZ-6 and EZ-9 semantics, so it changes with them. +- EZ-Q1 runs at the rev that contains the blocking parts of EZ-1 through EZ-9 and any promoted + requirement. + +## Landing and pinning + +Whether the blocking requirements land in PR 275 before it merges or in follow-up EdgeZero pull +requests is the EdgeZero maintainers' call. TS needs three things from the rev it pins: + +- It contains the blocking parts of EZ-1 through EZ-9 and any requirement promoted to blocking. +- It has a passing EZ-Q1 record for that exact commit. EdgeZero produces the results for criteria + in unit, Viceroy 0.21.0, workerd, Spin 4.1, and Axum that do not need TS, and assembles and keeps + the record. TS produces its end-to-end checks and all hosted Fastly results, running an + EdgeZero-provided qualification build for criteria TS configuration cannot express and its own + build for the rest, in single-request mode unless a criterion says otherwise. +- It stays reachable. A tag at that commit is enough, so a later force-push or squash merge cannot + leave TS pinned to an unreachable commit. + +## Proposed priority changes + +The EdgeZero and TS maintainers decide these. + +| ID | Change | Reason | +| ----- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| EZ-2 | Split into blocking and reuse-mode parts | Production runs single-request sandboxes. Connection reuse across those sandboxes (EZ-2.1 to 2.3, 2.6 to 2.10, and 2.13 to 2.17) blocks the cut. Behavior specific to reusable-sandbox mode (EZ-2.4, 2.5, 2.11, 2.12, and 2.18) is needed only before TS enables reuse mode, so it is a follow-up. | +| EZ-15 | Stays Follow-up (recommended) | TS emits Tinybird telemetry inside the `` hold today. Until EZ-15 lands, TS sends the telemetry rows with an ordinary send after delivery, inside the same lifecycle callback (cross-cutting requirement 4), so the page tail is not held. Promote the Fastly part (EZ-15.1 to 15.8, 15.10, and the Fastly clause of 15.11) only if that later emission is not acceptable. | +| EZ-24 | Minimal core Blocking (recommended) | The cut deletes TS's 22 outbound test doubles. Their replacement must match EdgeZero's begin, non-blocking batch checks, arrival-time completion, cutoff, and redirect behavior exactly, and a TS-written copy would drift and be thrown away later. The minimal core is EZ-24.1 to 24.10, with EZ-24.2 limited to the host and wasm32-wasip1 targets. | +| EZ-18 | Stays Follow-up | At F every buffered Fastly response goes out chunked with no `Content-Length`, where TS sends a fixed length today. Header parity is the only reason to promote it, since the tsjs bundles carry no `Content-Length` and host compression is unaffected. TS lists the change as an accepted Fastly difference until EZ-18 lands. | +| EZ-23 | Stays Follow-up | TS's Axum binary moves to `run_app`, which applies `Hooks::configure`. Only programmatic servers and in-process tests lose the policies, and TS keeps the ingress-level checks in its integration suite, which runs the real binary. | +| EZ-13 | Close as documentation and evidence | WASI HTTP 0.3 in spin-sdk 7.0.0 has one authority for routing and `Host`, so the override cannot be built. EdgeZero documents `Unsupported` as final with the evidence, and TS rejects `origin_host_header_override` on Spin at config validation. | + +## Verification + +Each requirement lists acceptance criteria and the TS check that proves it end to end. EdgeZero's +evidence comes from unit and adapter tests, Viceroy 0.21.0 with Fastly SDK 0.13.1, workerd, and +`spin up` on Spin 4.1, at Rust 1.98.1. TS reaches the same toolchain through TS PR #1123, which the +cut depends on. EZ-Q1 states what the qualification record must show before production. The hosted +measurements in EZ-2 to EZ-4 compare against a pre-cut TS build from `main` at `a9037770a` +with TS PR #1123. TS also runs its integration suite (Viceroy, workerd, and the Axum binary), its +browser suite, the Spin smoke, and a Fastly response diff against the same pre-cut build. + +## Related EdgeZero work outside these requirements + +These EdgeZero pull requests are not needed for the cut, but TS work that lands after it depends on +them, and each needs a rebase onto PR 275: + +- **EdgeZero PR #403** (pre-dispatch hooks and request ingress fidelity). TS #1107 and #1251 use it + heavily. F's ingress admission policy is synchronous and read-only, so it does not replace #403. +- **EdgeZero PR #389** (request timing). TS #1261 uses it. +- **EdgeZero PR #409** (trusted proxy metadata) touches the same request path. + +## Citation roots + +- `edgezero:` EdgeZero at F, `f6c708ccb5be19ba9fa92575db3c08c0f17e0d72`. +- `edgezero@v0.0.9:` EdgeZero at tag `v0.0.9`. +- `edgezero-ci:` GitHub Actions run 38934086784 on F. +- `ts:` Trusted Server `main` at `a9037770a`. +- `ts@pr1123:` Trusted Server at the head of PR #1123, commit `4c5ca8ae5`, by file and line. +- `fastly-sdk:` the `fastly` 0.13.1 crate. `fastly-sys:` and `fastly-shared:` the 0.13.1 crates. +- `viceroy:` `viceroy-lib` 0.21.0. `viceroy-0.17.0:` `viceroy-lib` 0.17.0, cited only to show + history. `hyper-0.14.26:` the hyper version Viceroy 0.21.0 uses. +- `spin-sdk:` spin-sdk 7.0.0. `worker:` worker 0.8.5. `reqwest:` reqwest 0.13.4. +- `dep:-/` a registry crate at the version EdgeZero pins. + +Claims marked **Unverified** say what would verify them. + +## Blocking requirements + +### EZ-1. Bodiless GET/HEAD forwarding + +**Priority:** Blocking +**Adapters:** Fastly, Axum, Cloudflare, Spin. The outcome must be the same everywhere. Adapters may differ only in whether the inbound body has to be read to learn that it is empty (EZ-1.2). +**Related:** EZ-6, whose begin call must accept the same bodiless request. EZ-9, which owns resolving the body once per redirect chain (EZ-9.7). EZ-10, which leaves a GET carrying a body at 400 and moves only size overflow to 413. EZ-Q1, which requires a passing result for every criterion here in the environment it names. EZ-25, whose EZ-25.1 owns the forwarding-example change. + +#### Need + +- TS proxies every request that no TS route claims to the publisher origin. Fastly registers that fallback for seven methods (`ts:crates/trusted-server-adapter-fastly/src/app.rs:291-301`), GET, POST, HEAD, OPTIONS, PUT, PATCH and DELETE, on every route path and on `/` and `/{*rest}` (`:1319-1331`). Axum, Cloudflare and Spin call the same core handler (`ts:crates/trusted-server-adapter-axum/src/app.rs:253`, `ts:crates/trusted-server-adapter-cloudflare/src/app.rs:562`, `ts:crates/trusted-server-adapter-spin/src/app.rs:843`). +- The handler rewrites the inbound request in place (`ts:crates/trusted-server-core/src/publisher.rs:3123-3146`, called at `:4810`) and moves it, body included, into the origin request (`:4812-4813`). +- After the cut TS forwards with `OutboundRequest::from_request(req, origin_uri)` and `HttpClient::send`, streaming the inbound body upstream. Only the overlapped origin fetch uses EZ-6's begin call (`publisher.rs:4847-4872`). TS overlaps only when all of these hold. The request is a navigation GET with an EC id and a KV store, and the EC snapshot is needed (`:3109-3116`, `:4821-4833`). The request has no shared-template cache key. The client supports concurrent fan-out and pending streamed responses (`:4840-4846`). +- Without EZ-1 every forwarded GET and HEAD fails with 400. That is every page navigation, every HEAD, and every asset that falls through to the publisher, which is most TS traffic. The TS integration scenarios that load a page through the fallback (`ts:crates/trusted-server-integration-tests/tests/frameworks/scenarios.rs:68-69`) fail on every runtime. +- TS cannot work around it without the body handling that decision 2 forbids. Forcing an empty body on every GET silently drops a body a client really sent, and checking emptiness means TS reads the inbound body itself. +- Today TS on EdgeZero v0.0.9 buffers the client body on Fastly (`edgezero@v0.0.9:crates/edgezero-adapter-fastly/src/request.rs:520-525`) and forwards any non-empty body, GET included (`ts:crates/trusted-server-adapter-fastly/src/platform.rs:330-340`). After EZ-1 a GET that carries body bytes gets 400. Cloudflare's `fetch` forbids a GET body, and EdgeZero already normalizes to that rule (`edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:1588-1591`). TS accepts the change. + +#### Gap at F + +- Every adapter hands the application a streamed body for every admitted request, bodiless or not. See Fastly `edgezero:crates/edgezero-adapter-fastly/src/request.rs:498-507`, Axum `edgezero:crates/edgezero-adapter-axum/src/request.rs:43-48`, Cloudflare `edgezero:crates/edgezero-adapter-cloudflare/src/request.rs:370-385` and `:657-658`, and Spin `edgezero:crates/edgezero-adapter-spin/src/request.rs:574-579`. The stream constructor always yields a stream (`edgezero:crates/edgezero-core/src/body.rs:63-68`). Cloudflare used to keep an empty body when the Worker request reported none. Commit 86f6e834 replaced that with a lazy read, because the browser test runtime can report no body stream for a request that does carry bytes. +- `OutboundRequest::from_request` keeps that stream and drops `Content-Length` and `Transfer-Encoding` at once (`edgezero:crates/edgezero-core/src/outbound.rs:927-934`, `:1858`, `:1863-1866`). +- Preflight rejects every GET/HEAD with a streamed body with 400 (`outbound.rs:1561-1575`). EdgeZero's outbound design calls that a deliberate false positive (`outbound-http-design.md:1619-1639`), and it predates lazy ingress. +- EdgeZero's own forwarding example (`edgezero:docs/guide/proxying.md:21-54`) and the scaffold and demo `proxy_demo` routes, declared for GET (`edgezero:crates/edgezero-cli/src/templates/root/edgezero.toml.hbs:64-67`, `edgezero:examples/app-demo/edgezero.toml:70-76`), fail the same way. This was found by reading the code, not by running it. The guide example also does not compile, because it passes `ctx.into_request()` to `from_request` without unwrapping the `Result` it returns (`proxying.md:32`, `edgezero:crates/edgezero-core/src/context.rs:381`). No test catches the failure, because every `from_request` GET test supplies an empty buffered body (`outbound.rs:4391-4403`, `edgezero:crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs:580-603`, `edgezero:examples/app-demo/crates/app-demo-core/src/handlers.rs:1411-1415`). +- Fastly, Cloudflare and Spin do not carry the client's HTTP version into the core request (Fastly `request.rs:711-719`, Cloudflare `request.rs:327-342`, and Spin, whose WASI request has no version, `dep:wasip3-0.7.1+wasi-0.3.0/wit/deps/http.wit:266-343`). Request headers alone therefore cannot tell an HTTP/1.1 bodiless GET from an HTTP/2 or HTTP/3 GET that carries DATA frames with no `content-length`. +- Hosts differ in whether they say a body is empty before it is read. hyper marks a zero-length HTTP/1 body as ended (`dep:hyper-1.12.0/src/proto/h1/dispatch.rs:313-314`). Fastly reports a known length only in some cases, and its docs list chunked HTTP/1.1 and HTTP/2-or-later without `content-length` as unknown (`fastly-sdk:src/http/body.rs:339-352`). Spin knows only an explicit `Content-Length` (`dep:wasip3-0.7.1+wasi-0.3.0/src/http_compat/mod.rs:148-153`, `:278-296`). F declares every adapter's framing host-managed (`edgezero:docs/guide/capabilities.md:98-102`). +- On the outbound side, Spin hands the host a body stream for every request, even an empty buffered GET (`edgezero:crates/edgezero-adapter-spin/src/outbound.rs:362-364`). Whether the Spin host then frames an empty GET as chunked is **Unverified**. Fastly sends an empty buffered body as an empty set body (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:445-453`) and a still-streamed body through its streaming send (`:1170-1187`). Whether the Fastly host frames a zero-byte streamed upload as chunked is **Unverified**. + +#### Requirements + +- **EZ-1.1** An inbound GET or HEAD is bodiless when the client sent no body bytes, whatever the protocol. For HTTP/1.1 that includes no `Content-Length` and no `Transfer-Encoding`, `Content-Length: 0`, and a chunked body that holds only the terminating zero-length chunk. For HTTP/2 and HTTP/3 it means the stream ended with the request headers, or the DATA frames held zero bytes, with or without a `content-length` field. A bodiless GET or HEAD built with `OutboundRequest::from_request` MUST forward successfully on all four adapters, through `HttpClient::send`, through EZ-6's begin call, and through EZ-9's redirect following. The upstream request MUST carry no body bytes and no `Transfer-Encoding`, and SHOULD NOT carry `Content-Length: 0`. +- **EZ-1.2** Where the host already knows the inbound body is empty, EdgeZero MUST NOT read it. A bodiless forward on that path can never fail with the inbound read deadline, and `RequestContext::take_body` and `into_request` yield an empty buffered body. These levels go in the capability matrix: + - Axum. Native. + - Cloudflare. Native when workerd reports no request body, and BestEffort if acceptance criterion 3 shows it does not. **Unverified** on workerd. Treating a request that reports no body as empty MUST NOT drop a body in a runtime that reports no body stream while still carrying bytes (the browser test runtime case behind commit 86f6e834). + - Fastly. BestEffort. It depends on whether the host reports a zero length for HTTP/1.1, HTTP/2 and, where the service serves it, HTTP/3 bodiless requests. **Unverified** on hosted Fastly (acceptance criterion 2). + - Spin. Unsupported, except for an explicit `Content-Length: 0`. + + On every other path EZ-1.4 and EZ-1.5 apply. + +- **EZ-1.3** EdgeZero MUST NOT decide emptiness from request headers alone. A GET whose headers declare no body, but which carries data (for example HTTP/2 DATA frames with no `content-length`), MUST be treated as carrying a body. The framing EdgeZero reports to the application's admission policy (`IngressHead::framing()`) MUST stay `IngressFraming::HostManaged`. Learning emptiness is not raw framing validation. +- **EZ-1.4** When the adapter does not know whether a forwarded GET/HEAD body is empty, EdgeZero MUST resolve it before sending, reading no more than it takes to see the first body byte or the end of the body. Zero-length chunks do not count as body bytes. An empty body makes the forward bodiless (EZ-1.1). A body with bytes makes the forward fail as in EZ-1.8. An error while reading the inbound body MUST surface unchanged. An expired inbound read deadline stays 408, and a failed host read stays in the existing internal-error category. +- **EZ-1.5** The time spent resolving the body MUST count against the outbound request's own timeout or deadline. If the client neither sends a byte nor ends the body, the forward MUST fail no later than the first of two limits. The outbound budget fails with the gateway-timeout category (504). The inbound read deadline fails with 408. This holds for single send, the begin call (EZ-6), and redirect following (EZ-9). Axum, Cloudflare and Spin MUST enforce the limit while waiting. Fastly MAY observe it only after a blocked host read returns. That is BestEffort, the same limitation as F's `inbound-read-deadlines` cell (`capabilities.md:74`, `:78-83`). +- **EZ-1.6** Only GET and HEAD get this treatment. Other methods, and GET/HEAD requests whose body is already buffered, MUST behave as at F and MUST NOT cause an extra read. The single unsupported-option rejection (cross-cutting requirement 2) MUST come before any resolving, so a GET/HEAD carrying an option the adapter or send mode marks Unsupported is rejected without its body being read. EdgeZero MAY resolve emptiness before its other outbound preflight checks. If it does, a GET/HEAD with a streamed body and some other invalid setting may wait for the inbound body before that error comes back, and the guide MUST say so. +- **EZ-1.7** Batches keep EZ-6's rule against streamed request bodies. A forwarded bodiless GET/HEAD MUST be accepted into a batch after the application has read the inbound body with `RequestContext::body_bytes`. It SHOULD be accepted without that step wherever EZ-1.2 holds. +- **EZ-1.8** A forwarded GET/HEAD that carries body bytes MUST fail with the same 400 F returns today for a GET with a non-empty buffered body (`outbound.rs:1564-1567`). It MUST fail before any backend or connection is set up, and no body byte may reach the upstream. +- **EZ-1.9** The existing `RequestContext` body semantics MUST NOT change. `body_kind()` reports the body as not yet read until it is read or taken. `body_bytes` on a bodiless request returns empty bytes and caches them. A body that was already read forwards its cached bytes and is not read again. A body whose read failed forwards the same error (`context.rs:381-394`). A body that was taken forwards empty. +- **EZ-1.10** Adapters MUST NOT frame an outbound GET/HEAD with an empty body as chunked. That means no `Transfer-Encoding` and no zero-length chunk terminator. For other methods with an empty buffered body, adapters SHOULD send no chunked framing either. +- **EZ-1.11** A forwarded OPTIONS, DELETE, POST, PUT or PATCH built with `OutboundRequest::from_request` MUST succeed through `HttpClient::send` on all four adapters, accepting the inbound stream and forwarding its bytes unchanged. EdgeZero MAY prebuffer that stream under the upload cap before contacting the origin; Axum and Cloudflare do so to satisfy EZ-10.6. This is not a guarantee of progressive network upload, and applications MUST NOT need their own buffering workaround. That includes a bodiless one, such as a CORS preflight OPTIONS with no `Content-Length` on Spin. Where EZ-1.2 holds, a bodiless one SHOULD go out with no body framing. Elsewhere an adapter MAY send it as a zero-length chunked body. EZ-6's begin call rejects these streamed uploads before sending, so TS uses `send` for them. +- **EZ-1.12** Redirect following resolves an inbound GET/HEAD body once per chain (EZ-9.7). +- **EZ-1.13** These invariants MUST hold. This requirement owns the rule on unread bytes, and EZ-10.11 refers to it. + - A GET/HEAD body is never forwarded. + - Outbound framing comes only from the body EdgeZero actually sends, never from inbound framing headers. + - On connections EdgeZero owns (Axum HTTP/1), request bytes left unread after any rejection, including EZ-1.8's 400 and EZ-10's 413, are never parsed as a new request. Closing the connection is acceptable. + - Polling the body of a GET that sent `Expect: 100-continue` may cause a `100 Continue` before the 400 (`dep:hyper-1.12.0/src/proto/h1/conn.rs:411-416`). That is acceptable. +- **EZ-1.14** EZ-25.1 owns the change to the `docs/guide/proxying.md` forwarding example. The example MUST forward a bodiless GET, a HEAD and a POST with a body correctly and MUST compile as a doc test. The capability guide MUST carry the EZ-1.2 levels and note that a late forward on a read path can fail with 408. + +#### Acceptance criteria + +1. Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. Given a route that forwards the inbound request with `from_request` and `send` to an origin that records what it receives, when a client sends a bodiless HTTP/1.1 GET, then the client gets the origin's status and the origin saw a GET with zero body bytes and no `Transfer-Encoding`. Record whether `Content-Length: 0` appeared. The same holds for HEAD, and for a GET whose chunked body holds only the terminator. +2. Environment is hosted Fastly, Trusted Server staging. Repeat criterion 1 with HTTP/1.1 and HTTP/2 clients that send no `content-length`. Repeat it with an HTTP/3 client where the staging service has HTTP/3 enabled. Whether a Fastly Compute service serves HTTP/3 to clients is **Unverified**. Where it does not, the record states that HTTP/3 was not available and marks the HTTP/3 leg not applicable. Record per protocol whether a bodiless forward still succeeds after the inbound read deadline has passed, which shows whether EZ-1.2 held. +3. Environment is workerd. The run first records whether workerd reports no request body for a bodiless GET. Where it does, a handler that takes the body of a bodiless GET observes an empty buffered body. Where it does not, the run records that, and Cloudflare's EZ-1.2 level is BestEffort. +4. Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. When a client sends a GET with `Content-Length: 5` and five bytes, then the client gets the EZ-1.8 400 and the origin records nothing. On workerd, record whether the body reached the guest at all. On Axum, a request pipelined after it is either refused by a connection close or answered from the correct offset. +5. Environment is hosted Fastly, Trusted Server staging. An HTTP/2 GET with a DATA frame and no `content-length` gets the EZ-1.8 400, and the origin records nothing. +6. Environment is Axum. Given an outbound timeout of 200 ms, when a client sends a chunked GET and never sends a chunk, then the forward fails with 504 within 205 ms. With an inbound read deadline of 100 ms, it fails with 408 within 105 ms. On hosted Fastly (Trusted Server staging) the same scenario records the observed outcome and time as BestEffort. +7. Environment is unit. A GET whose streamed body yields only zero-length chunks and then ends is sent bodiless. A GET with a streamed body and an already expired deadline fails with 504, and the body is not read. A POST with a streamed body, and a GET with an empty buffered body, read nothing extra. A taken body forwards empty. A body whose read failed forwards the same error. +8. Environment is Axum and workerd. A bodiless GET forwarded after the inbound read deadline has passed still succeeds (EZ-1.2). On workerd this holds where criterion 3 showed that workerd reports no request body. Otherwise the run records the outcome, and Cloudflare is BestEffort. On Spin 4.1 it succeeds when the request had `Content-Length: 0`. +9. Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A forwarded POST with a 3-byte body arrives at the origin with exactly those 3 bytes. A forwarded bodiless OPTIONS and a bodiless DELETE with no `Content-Length` succeed, and the origin sees zero body bytes. Record whether chunked framing was used. +10. Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1, once EZ-6 is present. A bodiless navigation GET begun with EZ-6's begin call reaches the origin with no body, under EZ-6's rule for when a begun request is on its way. A begun POST with a streamed body is rejected before anything is sent. +11. Environment is Axum and Viceroy 0.21.0, once EZ-9 is present. A forwarded bodiless GET whose origin answers 307 to a second URL makes two requests, both bodiless, and the inbound body is read at most once. +12. Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. After the handler calls `RequestContext::body_bytes` on a bodiless GET, the forwarded request is accepted into a batch. +13. Trusted Server end to end. `cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test integration -- --include-ignored` passes `test_wordpress_fastly`, `test_nextjs_fastly`, `test_wordpress_cloudflare`, `test_nextjs_cloudflare`, `test_wordpress_axum` and `test_nextjs_axum` (`ts:crates/trusted-server-integration-tests/tests/integration.rs:137-167`, `:272-285`) with TS forwarding through `from_request` and no TS-side body handling. Those tests are ignored by default, so the flag is required, as TS CI passes it (`ts:.github/workflows/integration-tests.yml:106`, `:148`). New TS cases pass for `HEAD /` (origin status, no body), a forwarded `OPTIONS /` (origin status), and `GET /` with a body (400). TS's in-process parity suite does not cover adapter ingress, so it is not enough on its own. +14. Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A GET carrying an option the adapter marks Unsupported, with a streamed inbound body whose client never sends a byte, is rejected at once with the unsupported-option error, and the forward does not wait for the body. + +#### Out of scope + +- Forwarding a GET or HEAD body. +- Raw framing validation. F's `raw-ingress-framing-validation` capability stays as it is. +- Resolving emptiness for methods other than GET and HEAD, beyond what EZ-1.11 asks. +- Changing the batch rule on streamed bodies. EZ-6 owns that rule. + +#### Open questions + +1. Should a bodiless OPTIONS or DELETE that the adapter cannot recognize as empty without reading be sent without chunked framing, which means reading before sending? TS prefers yes only if hosted Fastly turns out not to report a zero length for HTTP/2 and HTTP/3 bodiless requests (acceptance criterion 2). TS forwards CORS preflight OPTIONS, and some origins handle a chunked OPTIONS badly. Otherwise TS prefers no. + +### EZ-2. Fastly connection reuse across requests + +**Priority:** Blocking for EZ-2.1 to EZ-2.3, EZ-2.6 to EZ-2.10, and EZ-2.13 to EZ-2.17. Follow-up for EZ-2.4, EZ-2.5, EZ-2.11, EZ-2.12, and EZ-2.18, which apply to reusable-sandbox mode and are required before Trusted Server enables it. Trusted Server production runs single-request sandboxes (`ts:crates/trusted-server-adapter-fastly/Cargo.toml:13-18`). +**Adapters:** Fastly (the behavior that changes). Axum, Cloudflare, Spin (client precedence only; Axum keeps its existing cross-request pooling). +**Related:** EZ-3 (removing the dispatch slack depends on EZ-2.6). EZ-4 (connect timeout is part of reuse eligibility). EZ-6 (begun requests and batch observation inherit EZ-2; EZ-6.16 owns a slot whose deadline is the cutoff). EZ-8 (Image Optimizer exception to EZ-2.6). EZ-9 and EZ-15 (count toward the limit). EZ-11 and EZ-12 (settings that affect eligibility). EZ-17 (owns the helpers' shape). EZ-Q1 (gates these criteria). + +#### Need + +Trusted Server registers Fastly dynamic backends itself today, and their names repeat across requests and sandboxes. The name covers scheme, host, port, certificate check, Host override, a per-provider discriminator, and the first-byte and between-bytes timers, and no counter or request value (`ts:crates/trusted-server-core/src/platform/backend_naming.rs:304-339`, `:352-421`). TS floors remaining budgets onto a ladder so timer values repeat (`:18-22`, `:482-502`), and canonicalizes the mediator's remaining time so "the backend name remains stable across equivalent budget values" (`ts:crates/trusted-server-core/src/auction/orchestrator.rs:958-966`). Registration treats `NameInUse` as reuse (`ts:crates/trusted-server-adapter-fastly/src/backend.rs:256-273`). Config validation budgets provider backends against Fastly's limit of 200, reserving 40 for other traffic (`ts:crates/trusted-server-core/src/platform/backend_naming.rs:23-24`, `:105-111`; `ts:crates/trusted-server-core/src/auction/plan.rs:434-447`). Because a fresh single-request sandbox presents the same backend settings as the last one, Fastly can pool the TLS connections to the publisher origin and to each bidder across sandboxes on a node. + +After the cut TS deletes that layer. It sends origin, integration, and mediator calls with `HttpClient::send`, and bidder calls with `HttpClient::start_batch_until`, each request carrying `OutboundRequest::timeout` and the batch carrying a cutoff at auction start plus the auction timeout (2000 ms in the example config, `ts:trusted-server.example.toml:282`). Provider timeouts default to the auction timeout for the standard profile, 1000 ms for `prebid-server`, and 800 ms for APS (`ts:crates/trusted-server-core/src/auction/profile.rs:170-186`). Batch results carry their input index (`edgezero:crates/edgezero-core/src/outbound.rs:121-128`), so TS no longer needs per-provider backends, and two providers on one host may share connections. + +What regresses at F: + +- In a single-request sandbox, the TS production mode, reuse across sandboxes happens only when the effective budget repeats to the millisecond. The mediator, any provider launched with less than its timeout left (`ts:crates/trusted-server-core/src/auction/orchestrator.rs:453-462`), and any origin fetch bounded by a request deadline lose it. Each of those requests pays a new TCP and TLS handshake and a new backend registration. +- In a reused sandbox (TS feature `reusable-sandbox`, serving loop `ts:crates/trusted-server-adapter-fastly/src/main.rs:123-150`) no request reuses a connection an earlier request opened, and each request adds registrations the sandbox holds until it ends. A sandbox that serves 10 auctions with 15 bidder hosts holds more than 150, close to the platform limit of 200. +- The handshake cost on Fastly for TS's bidders has not been measured. Staging measurements M1 and M2 below measure it. + +#### Gap at F + +- Each `FastlyOutboundClient` takes a new value from a process-global counter and appends it to every backend name (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:35-36`, `:57-65`, `:307-317`). The canonical lifecycle builds a new client for every request (`edgezero:crates/edgezero-adapter-fastly/src/request.rs:446`, `:707-734`). The counter restarts with each wasm instance, so a reused sandbox gives each request a fresh suffix. +- Backend identity includes the effective budget rounded up to the millisecond (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:254-262`, `:664-685`, `:762-765`), and that budget is `deadline - now` at nanosecond precision (`edgezero:crates/edgezero-core/src/time.rs:138-187`). Any deadline- or cutoff-bound budget therefore gives a new backend per request. +- The backend cache lives in the client and is discarded with it (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:290-295`, `:319-377`). +- Platform timers are connect at a quarter of the budget, first byte at the rest, and between bytes at the whole budget (`:718-733`). The wait for response headers, batch observation, and body reads have no guest-side limit (`:443-453`, `:1279-1290`, `:784-825`, `:1203-1238`). Viceroy 0.21.0 enforces the first-byte timer, which there covers connect plus the response head, and the between-bytes timer, and does not enforce connect timeouts (`viceroy:src/wiggle_abi/req_impl.rs:617-644`; `viceroy:src/upstream.rs:351-375`, `:412-414`). It reports a timer expiry as a generic host error (`viceroy:src/error.rs:327-329`), which the SDK reports as `SendErrorCause::InternalError` (`fastly-sdk:src/http/request.rs:3353-3364`) and F maps to `EdgeError::Internal` when the deadline has not passed (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:73-75`, `:95-97`, `:959`). With F's three-quarter first-byte share, a slow response head is cut off before the budget, as a 500. +- The outbound design states that a newly constructed client does not intentionally reuse a previous client's connection and that the client must not outlive its request context (`edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:5139-5146`, `:5341-5352`). It assumes each request starts with an empty backend namespace (`:5413-5466`). +- The platform does not match that model. Viceroy creates its backend table once per sandbox and keeps it across the up to six requests a reused guest serves (`viceroy:src/sandbox.rs:54`, `:106-109`, `:170`), rejects any repeated name (`:698-707`), and builds a new HTTP client for every send, so it never pools connections (`viceroy:src/upstream.rs:356-368`). The SDK pools only for the same backend name with exactly the same settings, across sessions (`fastly-sdk:src/backend/builder.rs:281-294`). Fastly's documentation, fetched 2026-10-17 through a summarizing fetch tool and to be re-checked against the live pages, says "Compute registers dynamic backends at the node level." (Fastly's developer guide to backends, under Integrations and Non-Fastly services). It says a registration at the limit "blocks execution" until "another sandbox of the same service terminates and releases its backends" (Fastly's Compute errors reference). It lists "Maximum number of dynamic backends | 200 | per service" (Fastly's Compute resource limits page). +- Axum already pools across requests through one shared transport (`edgezero:crates/edgezero-adapter-axum/src/service.rs:73`; `edgezero:crates/edgezero-adapter-axum/src/request.rs:68-74`). + +#### Requirements + +Reuse (blocking): + +- **EZ-2.1** On Fastly, two outbound requests MUST be eligible to share a pooled connection when they target the same scheme, host, and port, with the same Host authority, the same TLS settings, and the same connect timeout (EZ-4), subject only to EZ-2.2. This MUST hold for two requests made while serving one inbound request, and for requests made by different single-request sandboxes of one service wherever Fastly pools across sandboxes. It MUST hold regardless of path, query, method, headers, body, cache policy, response mode, and send mode. +- **EZ-2.2** Reuse eligibility MUST NOT depend on the exact remaining budget. Requests with equal per-call timeouts MUST always be eligible with each other, even when a batch cutoff or caller deadline makes their effective budgets differ. For requests bounded only by a deadline or cutoff, the number of distinct backend configurations for one fixed origin/authority/TLS/connect-settings tuple MUST remain at or below a literal positive integer B approved by the EdgeZero and TS maintainers before backend implementation starts. The feasibility report MUST state B, the covered budget domain and timer-rounding policy, and the cardinality argument and boundary tests; "small" or an unbounded list is not a bound. Missing approval blocks that implementation, not a reason to invent a ladder or minimum supported budget. Requests whose budgets differ only by scheduling jitter MUST be eligible with each other except at those documented boundaries. B MUST NOT weaken EZ-2.6/EZ-2.7 or EZ-8.10. +- **EZ-2.3** Across sandboxes, requests with equal origin settings MUST be poolable wherever Fastly pools across sandboxes. Nothing request-, sandbox-, client-, or deployment-specific may prevent that within one EdgeZero version. When two EdgeZero versions run side by side during a rolling deploy, neither MAY fail a request because the other registered the same origin with different settings. + +Reuse in a reused sandbox (follow-up): + +- **EZ-2.4** In a reused sandbox, reuse eligibility MUST NOT depend on which client instance sends. The adapter's per-request default client, a client the application keeps for the sandbox, and a `FastlyOutboundClient::new()` client used after delivery inside the lifecycle callback (cross-cutting requirement 4) MUST all reuse the same connections. +- **EZ-2.5** In a reused sandbox the number of backend registrations MUST grow only with the number of distinct origin configurations used, not with the number of requests served. + +Deadlines (blocking): + +- **EZ-2.6** The per-call deadline MUST still be exact. On Fastly, on a backend used before and on a backend registered for this request, waiting for response headers and each response-body read MUST end no later than the request's deadline plus 5 ms, failing with `EdgeError::GatewayTimeout` (504) and the `BudgetSource` that selected the deadline. This MUST hold under Viceroy 0.21.0 and on hosted Fastly. The only exceptions are operations the host cannot interrupt: first-time backend registration, writing a request body, reading the inbound body to settle a forwarded GET/HEAD (EZ-1.5), and the Image Optimizer send of EZ-8. An overrun in one of those MUST be reported as the same 504 when the operation returns, and the capability footnote MUST name each exception that applies. Response-header and response-body waits and reads are not exceptions; verified deadline-bounded readiness and reads are a qualification prerequisite (Open question 6). +- **EZ-2.7** A platform-side timer MUST NOT fail a request before its own deadline for any budget up to Fastly's 2-minute runtime limit, except for an explicitly requested phase limit such as EZ-4's connect timeout or EZ-14's idle bound. Such an early phase expiry keeps its distinct cause; a phase timer MUST NOT silently shorten another phase or the aggregate budget. A platform-side timer that fires at or after the applicable absolute deadline MUST be reported as the EZ-2.6 504 with the deadline's `BudgetSource`, never as an internal error. This holds under Viceroy 0.21.0 and on hosted Fastly. +- **EZ-2.8** In a batch, a slot whose own deadline passes before the observation cutoff MUST yield its 504 item at that deadline, without waiting for a sibling to finish or for a platform timer. A slot whose deadline is the cutoff follows EZ-6.16. The behavior when the observation cutoff passes is otherwise unchanged from F (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:621-651`). + +Caching and safety (blocking): + +- **EZ-2.9** A single send and a one-slot batch of the same request MUST be indistinguishable in caching, validation, and errors. Cross-cutting requirement 1 owns the rule for the other send modes. Image Optimizer requests are governed by EZ-8. +- **EZ-2.10** EdgeZero MUST NOT send a request over a connection whose settings differ from what the request asks for (TLS settings, Host authority, connect timeout), or whose platform timers could end it before its deadline other than an explicitly requested phase limit (EZ-2.7). If it cannot get such a connection, the request MUST fail before sending with a 500-class internal error, and the reason MUST be logged and kept off the wire. + +Backend limit and retirement (follow-up): + +- **EZ-2.11** One reused sandbox MUST NOT block without end on its own registrations. When the sandbox already holds 200 distinct backend configurations and a request needs another, that request MUST fail promptly with `EdgeError::ServiceUnavailable` (503) and nothing sent, and the sandbox MUST be retired after the current request. A registration that returns after the deadline follows EZ-3.4. +- **EZ-2.12** An application serving through the reusable Fastly lifecycle (`edgezero_adapter_fastly::lifecycle::serve_custom`, `edgezero_adapter_fastly::serve_app`, `edgezero_adapter_fastly::serve_app_with_hooks`) MUST be able to set a threshold of distinct backend configurations at which the sandbox stops being reused, with a documented default, and MUST be able to retire the sandbox from its own callback. Retirement takes effect after the current request completes. It MUST NOT produce an SDK-generated error response. The serving result MUST tell retirement apart from an error, and MUST keep reporting the request count that TS logs today (`ts:crates/trusted-server-adapter-fastly/src/main.rs:144-149`). A client the application keeps for the sandbox MUST NOT carry any request's in-flight exchange into the next request. + +Observation (blocking): + +- **EZ-2.13** An application MUST be able to read, for the inbound request it is serving, the number of backend registrations EdgeZero performed, the number of distinct backend configurations it used, and the total and longest registration time. A registration that takes 50 ms or more MUST be logged at `warn`. None of this reaches the wire. + +Client supply (blocking): + +- **EZ-2.14** EdgeZero MUST document and keep this precedence for the `HttpClient` a handler receives from `RequestContext::http_client` (`edgezero:crates/edgezero-core/src/context.rs:372-374`). A client registered with `RouterBuilder::with_state` wins (`edgezero:crates/edgezero-core/src/router.rs:348-354`, `:407-411`). On Fastly, a client inserted through the `prepare` hook of `send_request_with_registries_and_hooks` comes next (`edgezero:crates/edgezero-adapter-fastly/src/request.rs:446-448`). The adapter default comes last. The router-state rule MUST hold on all four adapters. A client inserted through `prepare` MUST keep reaching the handler. EZ-17 owns the helpers' shape. + +Invariants and documentation (blocking): + +- **EZ-2.15** Axum MUST keep its cross-request pooling. Cloudflare and Spin keep their platform-managed connections. No new capability row is required for pooling, which no adapter can promise. +- **EZ-2.16** EdgeZero MUST NOT retain paths, queries, headers, or bodies across requests. In a reused sandbox it MAY retain origin host names and backend configuration. Its guide MUST tell applications not to let untrusted input choose outbound hosts, because every distinct origin consumes service-wide backend capacity. +- **EZ-2.17** EdgeZero's Fastly guide and capability matrix MUST describe the reuse behavior, the deadline guarantee of EZ-2.6 and its exceptions (Fastly `outbound-deadlines` stays `BestEffort`), the limit and retirement behavior once EZ-2.11 and EZ-2.12 land, and the observation of EZ-2.13. The guide MUST record that the outbound design's dynamic-backend model is superseded, without editing `docs/superpowers/specs/`. The CHANGELOG MUST list each behavior change. + +Per-sandbox accounting (follow-up): + +- **EZ-2.18** In a reused sandbox, an application MUST be able to read the number of distinct backend configurations the sandbox holds and the number of registrations it performed since it started. + +#### Acceptance criteria + +Blocking criteria come first. Viceroy 0.21.0 never pools connections, so reuse is visible there only as registration counts. + +Unit (any host): + +1. Given a client in router state, another inserted through Fastly `prepare`, and the adapter default, when a handler calls `RequestContext::http_client`, then it receives the router-state client. Given only the `prepare` client and the default, it receives the `prepare` client. The router-state case holds on Axum, Cloudflare, and Spin against their defaults. + +Viceroy 0.21.0, one inbound request per case: + +2. Given one inbound request that runs two batches to one loopback origin with per-call timeouts of 800 ms and cutoffs leaving 640 ms and 655 ms, when both complete, then the EZ-2.13 observation reports one registration and one distinct configuration. +3. Given one inbound request whose sends differ only in scheme, port, Host authority, TLS settings, or connect timeout, then the observation reports one distinct configuration per variant. +4. Given a loopback origin that delays its response head by 500 ms and a request with a 100 ms timeout, when sent, then it fails with 504 and `BudgetSource::PerCallTimeout` between 100 and 105 ms after dispatch. (At F, by reading the code, Viceroy's 75 ms first-byte timer fires first and the request fails with an internal error at about 75 ms.) +5. Given an origin that sends its head at once and then one body byte every 300 ms, and a buffered request with a 500 ms timeout, then it fails with 504 between 500 and 505 ms. Repeat with a pending read of a streamed response; that read fails in the same window. The record verifies readiness handling and reads do not enter an uninterruptible host operation; a late-read fallback does not pass. +6. Given a batch with a 1 s cutoff, one slot to an origin that never answers with a 200 ms timeout, and two fast slots, then the silent slot's 504 item arrives between 200 and 205 ms and the batch completes. + +Hosted Fastly, single-request mode. Who produces each result follows EZ-Q1.4. EdgeZero produces criteria 1 to 6. Criteria 11, 12, 13b and 14 need settings Trusted Server's configuration cannot express (a controllable slow origin, a 90 s timeout, a one-slot batch to a test origin, a second EdgeZero commit), so EdgeZero provides a qualification build at the pinned commit and TS deploys it to a TS staging service. TS produces criteria 7 to 10 and 13a with its own builds, and criterion 15. The baseline is a Trusted Server build of `main` at `a9037770a` with TS PR #1123, which registers its own stable backends. The candidate is TS's cut build at the EdgeZero commit TS pins. Criteria 7 to 10 replay a fixed page-view mix at a fixed rate with build order randomized, and report p50, p95, p99, and sample counts per POP. A bound measured inside the guest from dispatch or a deadline keeps its stated tolerance on hosted Fastly. The thresholds in criteria 7 to 10 are Trusted Server's proposed product thresholds (Open question 8). + +7. **M1, handshakes per bidder per auction.** TS-controlled staging endpoints for one provider and the origin log new TCP connections and full versus resumed TLS handshakes. After a 5-minute warm-up, the candidate's new handshakes per request to the origin and to that provider MUST NOT exceed the baseline's by more than 10 percent. +8. **M2, latency.** Per bidder, the candidate's p95 slot latency and p95 response-head latency MUST each be no more than 10 ms above the baseline's. +9. **M3, registration.** In the candidate, the EZ-2.13 observation for each inbound request MUST show no more registrations than the distinct origin configurations that request used. That count is a pass/fail invariant. The total registration time per inbound request is recorded with its distribution and serves as EZ-3's reference; the duration distribution has no threshold because the pre-cut build does not time its own registrations and the platform sets that latency. +10. **M5, pool exhaustion.** At peak replay rate, the candidate's gateway failures, as a share of that origin's requests, MUST be no more than 10 percent above the baseline's share or 0.1 percentage points above it, whichever is larger. Where EZ-16 has landed, the same bound applies to failures whose provider cause is a connection limit. +11. **M6, deadline in production.** Criteria 4 and 5 pass against a staging origin with the same delays, with the windows they state. +12. **Long budgets.** A request with a 90 s per-call timeout to an origin that sends its head at 80 s succeeds (EZ-2.7). +13. **Rolling deploy.** + - 13a. While the baseline and the candidate are active together during a staged rollout to one TS staging service, both serve the same origins, the candidate reports zero EZ-2.10 internal errors, and neither build's requests fail at registration. + - 13b. When the pinned commit changes backend configuration relative to the EdgeZero commit of the previous passing record, the qualification builds of the two commits active together on one staging service both serve the same origins with zero registration failures. The first record has no previous commit, and 13a alone applies. +14. **Caching parity and cross-sandbox pooling, hosted Fastly single-request mode.** + - 14a. A cacheable GET sent twice with `send`, and twice as a one-slot batch, produces the same number of origin receipts in each mode (one per pair), and the second response of each pair is served from cache in both modes. Viceroy's plain send path does not cache (`viceroy:src/wiggle_abi/req_impl.rs:946-977`). + - 14b. A separate SDK-only control and the candidate send sequential cache-bypassed requests from fresh single-request executions to a controlled persistent TLS origin with matched transport settings. Record origin connection IDs, execution IDs, new TCP connections, and full/resumed TLS handshakes. The control MUST demonstrate a connection serving requests from distinct executions, and the candidate MUST demonstrate the same cross-execution reuse while satisfying M1's handshake threshold. Choose and approve the sample count, warm-up, settings and zero-baseline rules before running. Same POP and TLS session resumption are not proof of connection reuse. An inconclusive or negative control is not a passing pooling result: it blocks this gate pending provider evidence or an approved requirement change. This criterion does not promote reusable-sandbox M4 or promise that EdgeZero can force reuse where the provider does not support it. + +Trusted Server end-to-end check (Viceroy 0.21.0, single-request mode): + +15. One auction with three bidder hosts, two providers configured on one of them, and one origin fetch reports four registrations for the inbound request. + +Follow-up criteria (reusable-sandbox mode). Viceroy 0.21.0 serves at most six requests per reused guest. Cases 16 and 17 stay on one guest. Cases 18 and 19 expect a new guest. + +16. Given a reused guest and three sequential requests to the same loopback origin, the EZ-2.18 accounting reports one registration. A fourth request to a second loopback port raises it by one. (At F it reports three after the first three requests.) +17. Given one reused guest where a request on the adapter default client, a request on a client kept for the sandbox, and a `FastlyOutboundClient::new()` send after delivery all target one origin, the accounting reports one registration. +18. Given a reused guest whose first requests send to 200 distinct loopback ports, when a later request needs a 201st, then it fails with 503 within 5 ms and nothing is sent, and the next request is served by a new guest. +19. Given a retirement threshold of 2, when two requests use two loopback ports, then the third request is served by a new guest. Given a callback that asks for retirement on its first request, a new guest serves the second request. In both cases the serving result reports retirement, no error, and the request count served. +20. **M4, limit model, hosted Fastly in reuse mode.** Report distinct backends against sandbox ordinal, retirements, 503s from EZ-2.11, and registrations of 50 ms or more. Then run K concurrent sandboxes with the same N origins, and again with disjoint origins, to learn whether identical configurations in different sandboxes share capacity and whether the 200 applies per node or per service. M1 is repeated in reuse mode, and the candidate's new handshakes per request after each sandbox's first 5 requests MUST NOT exceed the baseline's. +21. Trusted Server end to end, Viceroy 0.21.0 in reuse mode. Two sequential auctions with three bidder hosts and one origin register four configurations in total, and the second auction registers none. + +Additional blocking unit criterion, independent of reusable-sandbox enablement: + +22. **EZ-2.13 warning boundary, any host with injected time and a recording logger.** A backend registration lasting just below 50 ms emits no slow-registration warning; durations of exactly 50 ms and above each emit one record at `warn` when that level is enabled. Cover both successful and failed registration. Assert request-local registration/configuration counts and total/longest durations, and unchanged response status/body/public errors. No query, credential or raw header is logged. This is a deterministic log/observation assertion, not a bound on provider registration latency. EdgeZero supplies this result in the EZ-Q1 record. + +#### Out of scope + +- Guaranteeing connection reuse where Fastly does not pool, or across services. +- Per-request control of pool size, keepalive, or connection lifetime. +- Bounding first-time registration or request-body writes in time (EZ-2.6 covers only how overruns are reported). +- Exact deadlines for Image Optimizer requests (EZ-8). +- A capability row for pooling. + +#### Open questions + +1. **Default retirement threshold (EZ-2.12).** TS prefers 100 distinct backends until M4 reports the capacity model. If identical configurations share capacity across sandboxes, the default can rise toward 160. If not, it should fall, and the guide should say reuse suits only applications with few distinct origins. +2. **Overrun during uninterruptible operations.** Image Optimizer uses EZ-8.10's separately approved backend-policy horizon, not a universal twice-effective-budget promise. First-time registration, request-body writing, and inbound settlement retain only EZ-2.6's existing post-return classification; any additional finite wall-time promise for those operations needs its own evidence and approval. An Image Optimizer horizon MUST NOT silently broaden ordinary response-head or body-read deadline tolerances. +3. **Operator visibility.** Should operators be able to tell from Fastly's own tooling which origin host a registered backend serves? TS prefers yes. It makes M3 and M4 attributable, and origin hosts are not secret. +4. Removed. +5. **Per-backend connection cap.** Fastly's SDK defaults each backend pool to 200 connections (`fastly-sdk:src/backend/builder.rs:132`). Sharing connections across requests makes that cap reachable. TS prefers keeping the default and revisiting it if M5 shows connection-limit failures. +6. **Body-readiness evidence required before qualification.** EdgeZero MUST establish that response-header and response-body readiness can be awaited with a timeout and that subsequent reads preserve the bounded wait, on Fastly SDK 0.13.1 under both Viceroy 0.21.0 and hosted Fastly. The SDK's async I/O interface lists normal bodies as objects whose readiness can be awaited with a timeout (`fastly-sys:src/service0_1_0.rs:317-325`, `:460-471`), and Viceroy 0.21.0 implements timed waits over body items (`viceroy:src/async_io.rs:14-46`; `viceroy:src/sandbox/async_item.rs:154`). Hosted behavior is **Unverified** until M6 and EZ-6's hosted scenarios pass. This evidence is a blocking prerequisite, not permission for an uninterruptible body-read fallback. +7. **Proving EZ-2.10.** How will EdgeZero show that a request is never sent over a connection with the wrong settings, given that the clash is not observable from outside except through criterion 13? TS prefers that EdgeZero's own tests cover it and that TS's hosted runs report zero EZ-2.10 errors. +8. **Staging thresholds for TS to confirm.** The proposed product thresholds are M1 handshakes at most 10 percent above the pre-cut build, M2 p95 slot and response-head latency at most 10 ms above it, M3 registrations per inbound request no more than its distinct origin configurations, and M5 gateway-failure share at most 10 percent or 0.1 percentage points above it, whichever is larger. TS prefers these because they allow measurement noise while still catching a build that loses pooling or adds a registration round trip to every request. + +#### Interim for Trusted Server (follow-ups only) + +Until EZ-2.4, EZ-2.5, EZ-2.11, EZ-2.12, and EZ-2.18 land, Trusted Server keeps `reusable-sandbox` off in production, as it is today (`ts:crates/trusted-server-adapter-fastly/Cargo.toml:13-18`), and serves single-request sandboxes. + +### EZ-3. Cold backend registration never fails a request on its own + +**Priority:** Blocking +**Adapters:** Fastly for the slack. EZ-3.3 holds on all four adapters. Axum, Cloudflare, and Spin have no slack and otherwise do not change. +**Related:** EZ-2 (EZ-2.6 must hold before the slack is removed, so both ship together). EZ-1 (the 408 of EZ-1.5). EZ-6 (begun requests and non-blocking batch observation follow the same rule). EZ-Q1 (gates these criteria). + +#### Need + +Trusted Server has no dispatch slack today. Its auction launches each provider and then collects completions, turning late ones into timeouts where its deadline policy says so (`ts:crates/trusted-server-core/src/auction/orchestrator.rs:440-613`, `:558-570`). After the cut every bidder goes through one `start_batch_until`. In a single-request sandbox no backend is registered yet, so the batch performs one first-time registration per bidder, one after another. F charges all of them, plus each earlier slot's preparation and send, against one 25 ms allowance measured from batch entry. A 12-bidder auction in which each registration costs a few milliseconds (**Unverified**, measured by M3) exhausts it partway through the list. The remaining slots fail with an internal error while more than 1900 ms of a 2000 ms auction remain. In TS's post-cut design an internal error is a transport error, so those bids are lost without a timeout signal. + +Single sends are exposed too. DataDome protection, off by default (`ts:trusted-server.example.toml:599`), runs with a 1500 ms default budget (`ts:crates/trusted-server-core/src/integrations/datadome.rs:284-286`) and fails open on error (`ts:trusted-server.example.toml:598`). If its registration takes more than 25 ms, F returns an internal error and TS skips bot protection for that request. + +#### Gap at F + +- `BATCH_DISPATCH_SLACK_MAX` is 25 ms (`edgezero:crates/edgezero-core/src/time.rs:8-9`), exported publicly (`edgezero:crates/edgezero-core/src/lib.rs:112-115`). +- Before every send, and before each first-time registration in a batch, F fails the request with `EdgeError::Internal` if more than 25 ms passed since the call began, even when the deadline is far away (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:1338-1357`, `:449`, `:521`, `:1179`, `:767-780`). In a batch the 25 ms is shared by all slots (`:576`, `:585-588`). For a single send it includes the registration (`:565-566`). +- A public test hook exists only to exercise this (`:224-252`, `:2531-2532`), and a Viceroy test asserts the internal error (`edgezero:crates/edgezero-adapter-fastly/tests/contract.rs:1295-1321`). +- The outbound design justifies the slack as protection against platform timers armed with a stale budget, and concedes it detects rather than prevents overruns when a backend is new (`edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:5191-5269`). +- The other adapters size their timers from the budget remaining at dispatch and have no slack (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:478-484`; `edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:636-646`; `edgezero:crates/edgezero-adapter-spin/src/outbound.rs:468-488`). + +#### Requirements + +- **EZ-3.1** A Fastly request or batch slot MUST NOT fail because EdgeZero's preparation or backend registration took time while the request's deadline was still in the future. The dispatch-slack internal error MUST no longer occur. +- **EZ-3.2** Before a response arrives, the only time-based failures MUST be `EdgeError::GatewayTimeout` (504) with the `BudgetSource` that selected the deadline or the connect-timeout cause of EZ-4.6, and the 408 of EZ-1.5 when the inbound read deadline expires while a forwarded GET/HEAD body is being settled. +- **EZ-3.3** After the unsupported-option rejection of cross-cutting requirement 2, on every adapter a supported request whose deadline has passed before it is sent MUST fail with 504 and its `BudgetSource` without being sent, and on Fastly without a new backend registration. An unsupported option still takes precedence and is rejected without reading the body. +- **EZ-3.4** A backend registration that returns after the deadline MUST fail its request with 504 and the request's `BudgetSource`, and the registered backend MUST remain usable by later requests. +- **EZ-3.5** EZ-2.6 MUST hold with the slack removed. +- **EZ-3.6** In a batch, a slot whose backend was registered before SHOULD NOT wait behind other slots' first-time registrations. Collected result vectors keep input-index order; `next` and non-blocking checks deliver recorded arrival order under EZ-6.15. Each slot's elapsed time is still measured from batch entry. +- **EZ-3.7** `BATCH_DISPATCH_SLACK_MAX` and the slack test hook MAY be removed from the public API. Nothing in TS uses them. +- **EZ-3.8** EdgeZero's capability matrix footnote on Fastly deadlines (`edgezero:docs/guide/capabilities.md:260-266`) and the CHANGELOG MUST stop describing the 25 ms bound and MUST describe the EZ-2.6 guarantee instead. + +#### Acceptance criteria + +Unit (any host). How a test makes time pass inside EdgeZero is EdgeZero's choice. + +1. Given a 500 ms budget, when more than 25 ms passes inside the send call before dispatch, then the request is sent and no internal error occurs. +2. Given a batch of 20 slots to 20 new origins whose registrations each take 5 ms, and a 1 s cutoff, then all 20 are sent and none fails with an internal error. +3. Given a batch with a 1 s cutoff where the first slot's registration runs first and takes 20 ms, and the second slot has a 10 ms timeout, then the second slot yields 504 with `BudgetSource::PerCallTimeout`, is not sent, causes no registration, and the batch continues. +4. Given a batch ordered new, known, new, known, then both known-origin slots SHOULD be sent before the first registration, the run records the order, and collection restores input-index order. Unit, given slot 1 finishes before slot 0, `next` returns indices 1 then 0 while the collected vector remains indexed 0 then 1. + +Viceroy 0.21.0: + +5. Given a 1 s budget, more than 25 ms passing before sending, and a loopback origin that answers 200, when the request is sent, then it returns 200. (At F it fails with the dispatch-slack internal error.) +6. Trusted Server end to end, single-request mode. A 12-bidder auction against 12 loopback ports completes with no internal-error slots. + +Hosted Fastly, single-request mode, with the same baseline and candidate as EZ-2. EdgeZero produces criteria 1 to 5. Trusted Server produces criteria 6 and 7 with its own builds (EZ-Q1.4). + +7. **M7, slack failures.** On the candidate, the number of slots and single sends that fail with an internal error before sending, including DataDome calls, MUST be zero across the replay, reported per 1000 auctions by bidder count. The candidate's per-slot 504 rate MUST NOT exceed the baseline's by more than 1 percentage point. Both bounds MUST hold for a 12-bidder auction profile. + +8. Unit, every adapter. Given an expired deadline and Image Optimizer options on a send mode where they are Unsupported, the unsupported-option 400 wins without reading the body, registering a backend, or sending. Without the unsupported option, the same request returns the attributed 504. + +#### Out of scope + +- Making first-time registration faster or interruptible. +- Bounding request-body writes. + +#### Open questions + +1. **Cap on first-time registrations per batch.** TS prefers no cap. EZ-3.3 already stops registering for slots whose deadline passed, and EZ-2.11 bounds a reused sandbox's total. A per-batch cap would fail live slots for the same reason the slack did. +2. **M7 threshold for TS to confirm.** The proposed bound on the per-slot 504 rate is 1 percentage point above the pre-cut build. TS prefers it because a larger rise would mean bids lost to timeouts that the pre-cut build received. + +### EZ-4. Connect timeout independent of the total budget + +**Priority:** Blocking +**Adapters:** All four. Fastly Native. Spin BestEffort. Axum and Cloudflare BestEffort, with the setting accepted and the connect phase bounded by the total budget (cross-cutting requirement 2). +**Related:** EZ-2 (requests that differ in connect timeout do not share connections). EZ-6, EZ-9, EZ-15 (the connect timeout travels with begun requests, redirect hops, and, once EZ-15 lands, fire-and-forget sends). EZ-16 (logs tell a connect timeout from a DNS failure). + +#### Need + +Trusted Server sets connect to 1 s on every Fastly backend (`ts:crates/trusted-server-adapter-fastly/src/backend.rs:238-242`) and sets a provider's first-byte and between-bytes timers to its whole transport budget (`ts:crates/trusted-server-core/src/auction/plan.rs:385-398`). DataDome protection passes its 1500 ms as both body timers (`ts:crates/trusted-server-core/src/integrations/datadome/protection.rs:238-239`). Publisher origin fetches use a 15 s first-byte timeout (`ts:crates/trusted-server-core/src/publisher.rs:98`). An unreachable origin therefore fails in about 1 s, and bidders get their full budget to answer. + +F gives the connection a quarter of the budget and the response head the remaining three quarters. A 1000 ms bidder call gets 250 ms to connect and 750 ms to first byte, so a bidder loses a quarter of its think time even on a reused connection, and a slow TLS handshake fails at 250 ms with most of the budget left. DataDome gets 375 ms and 1125 ms. An origin fetch migrated with a 15 s timeout gets 3.75 s to connect, so an unreachable origin holds the page for 3.75 s where TS fails in 1 s today. + +TS needs the response head to get the whole remaining budget, and it needs to keep a 1 s connect bound on origin, integration, and bidder requests. + +#### Gap at F + +- `OutboundRequest` has a total timeout and a deadline, and no connect setting (`edgezero:crates/edgezero-core/src/outbound.rs:49-67`, `:71-90`). +- Fastly reserves a quarter of the budget for connect (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:718-733`). A connect timeout is reported with the budget's `BudgetSource`, as if the whole budget were spent (`:77`, `:931-933`). Viceroy 0.21.0 stores no connect timeout for dynamic backends and enforces F's first-byte share, so at F a 1000 ms request whose head arrives at 900 ms fails at about 750 ms with an internal error (`viceroy:src/wiggle_abi/req_impl.rs:617-644`; `viceroy:src/upstream.rs:370-375`). +- Spin sets connect, first byte, and between bytes to the remaining budget (`edgezero:crates/edgezero-adapter-spin/src/outbound.rs:67-74`, `:468-488`). Only a `NotSupported` refusal is ignored with a `debug` log. `Immutable` and `Other` refusals fail the request as an internal error (`:490-505`). A connect timeout before the deadline is reported with an unspecified source (`:1341-1357`). +- Axum bounds the whole request with one timeout (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:478-484`), and reqwest 0.13.4 offers a connect timeout only per client (reqwest documentation for `RequestBuilder` and `ClientBuilder`, fetched 2026-10-17). Cloudflare races the fetch against the total budget (`edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:636-646`). +- The capability matrix marks `outbound-flexible-phase-budget` `BestEffort` on Fastly and Spin (`edgezero:docs/guide/capabilities.md:244`, `:281-282`). The outbound design defers a configurable split "pending a real use case" (`edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:7213-7221`). TS is that use case. + +#### Requirements + +- **EZ-4.1** An application MUST be able to set a connect timeout on an `OutboundRequest`, independently of `OutboundRequest::timeout` and `OutboundRequest::deadline`. The setting MUST survive `OutboundRequest::into_parts` and `from_parts`, and MUST apply on every send mode (cross-cutting requirement 1). A request built with `OutboundRequest::from_request` has none. +- **EZ-4.2** The connect timeout bounds establishing a new connection. Each adapter's documentation MUST say whether DNS resolution and the TLS handshake fall inside it. It MUST NOT extend the total budget. It MUST NOT reduce the time allowed for the response head. It does not apply when the request uses an existing pooled connection. +- **EZ-4.3** Without a connect timeout, no adapter may bound the connect phase more tightly than the total budget. On Fastly the response head MUST get the whole remaining budget. A request that connects in 10 ms and receives its head at 90 percent of its budget MUST succeed. +- **EZ-4.4** With a 1 s connect timeout on Fastly, a request to an unreachable origin MUST fail within 1 s plus 50 ms whatever its total budget. This restores TS's current fast failure. +- **EZ-4.5** A zero connect timeout MUST be rejected before sending with F's standard outbound preflight rejection (400). This deliberately differs from a zero total timeout, which F reports as 504 with `BudgetSource::PerCallTimeout` (`edgezero:crates/edgezero-core/src/time.rs:176-181`), because a zero connect timeout is invalid input rather than an exhausted budget. A connect timeout longer than the total budget MUST behave as if unset. +- **EZ-4.6** When the connect phase times out before the deadline and a connect timeout was set, the request MUST fail with `EdgeError::GatewayTimeout` (504) and `BudgetSource::ConnectTimeout`. This hard cut uses the existing cause field, not a second timeout-classification axis. When the deadline has passed, the deadline's `BudgetSource` MUST win. When no connect timeout was set and the platform reports a connect timeout before the deadline, the source MUST be `BudgetSource::Unspecified`. +- **EZ-4.7** In a batch, each slot's connect timeout MUST be independent of the cutoff and of other slots. +- **EZ-4.8** On Fastly the connect timeout MUST be passed to the platform in milliseconds, and a connect failure MUST occur within the timeout plus 50 ms on hosted Fastly (Native). Spin MUST pass it to the host and, for any refusal of the setting, fall back to the total budget without an error (BestEffort). This changes F's handling of `Immutable` and `Other` refusals. Axum and Cloudflare MUST accept the setting without an error, with the connect phase bounded by the total budget (BestEffort). Axum MAY honor it exactly and then report Native. +- **EZ-4.9** EdgeZero's capability matrix MUST gain a row for the connect timeout with the levels of EZ-4.8, and Fastly's `outbound-flexible-phase-budget` MUST become Native. The guides MUST say that connect timeouts should be configuration constants, because on Fastly requests with different connect timeouts do not share connections (EZ-2.1). The CHANGELOG MUST list the removal of Fastly's fixed connect share. +- **EZ-4.10** Removed. +- **EZ-4.11** An application MUST be able to read, from a built request, the connect timeout it carries. + +#### Acceptance criteria + +Unit (any host): + +1. Given a request with a 1 s connect timeout, when it goes through `into_parts` and `from_parts` and through a redirect-hop rebuild, then the connect timeout read back under EZ-4.11 is 1 s. A recording head-first client MUST also observe that setting through the prepared dispatch request without shortening its aggregate budget. Fastly and Spin mapping tests MUST observe the value passed to the provider timeout setter; Axum and Cloudflare retain their documented BestEffort acceptance. +2. Given a connect timeout of zero, when the request is sent on any adapter, then it fails with 400 before anything is sent. +3. Given each kind of Spin host refusal of the setting, the request completes within its budget and reports no error for the refusal. +4. Each adapter's capability metadata reports the EZ-4.9 levels. + +Axum and workerd: + +5. Given a request with a 1 ms connect timeout to a loopback origin that accepts at once, the request succeeds (BestEffort). + +Spin 4.1: + +6. Given a 200 ms connect timeout to a controlled destination that silently drops connection attempts without rejecting them, and a 2 s budget, the request fails before the budget with the connect-timeout cause, or, if the host ignores the setting, at the budget with the budget's source. The run records the destination's drop policy and which outcome occurred. A destination that immediately refuses or rejects the connection is a separate unreachable-error case, not this timeout fixture. + +Viceroy 0.21.0: + +7. Given a loopback origin that accepts at once and sends its head after 900 ms, a request with a 1000 ms timeout and no connect timeout succeeds. (At F it fails at about 750 ms with an internal error.) + +Hosted Fastly, single-request mode. Viceroy 0.21.0 does not enforce connect timeouts, so these criteria need hosted Fastly. Criterion 8 uses a controlled network fault fixture, so it runs on EdgeZero's qualification build deployed by TS. TS produces criterion 9 with its own builds, against the EZ-2 baseline (EZ-Q1.4). The bounds in criterion 8 are measured inside the guest and keep their stated tolerance. + +8. **M8, connect.** A request to a controlled destination that silently drops connection attempts, with a 200 ms connect timeout and a 2 s total budget, fails with 504 and the connect-timeout cause within 250 ms of dispatch. Without a connect timeout it fails with the budget's source between 2000 and 2050 ms after dispatch. The record MUST identify the fixture and its enforced drop policy; a reserved documentation address alone is not evidence of a silent drop. A separate request to a reachable host's refused port MUST fail as an unreachable gateway error, without requiring it to wait for a timeout. +9. **Bidder timeouts.** Across the EZ-2 replay, no bidder slot in the candidate fails with a per-call, deadline, or cutoff timeout before its provider timeout has elapsed. The candidate's connect-timeout failures per integration, as a share of that integration's requests, MUST be no more than 0.1 percentage points above the baseline's connection-failure share for the same integration (Open question 3). + +Trusted Server end-to-end check: + +10. A TS unit test over its request builders reads, under EZ-4.11, a 1 s connect timeout on every origin, integration, and bidder `OutboundRequest`. No client double is needed. + +11. Unit, Fastly. A 15 s request with an explicit 1 s connect timeout is eligible for dispatch under EZ-2.10. A connect expiry at 1 s has `BudgetSource::ConnectTimeout`; an expired governing budget wins ties and late observation, retaining its original source. Neither the configured connect timeout nor the error classifier may shorten the response-head budget. + +#### Out of scope + +- A DNS timeout setting. +- Separate first-byte or between-bytes settings (EZ-14 covers idle time). +- Exact connect bounds on Axum, Cloudflare, and Spin. + +#### Open questions + +1. **Default.** TS prefers unset, meaning bounded by the total budget as on Axum and Cloudflare, with TS setting 1 s explicitly. A 1 s default would match Fastly's SDK default (`fastly-sdk:src/backend/builder.rs:153`) and TS today, but would apply on Fastly alone. +2. **Axum exactness.** TS prefers BestEffort. Axum is a development target, and honoring per-request connect timeouts there adds connection pools for little gain. +3. **Criterion 9 threshold for TS to confirm.** The proposed bound is 0.1 percentage points above the pre-cut build's connection-failure share per integration. TS prefers it because the cut keeps TS's 1 s connect bound, so a larger rise would point to a regression rather than to origin behavior. + +### EZ-5. Encoded response passthrough + +**Priority:** Blocking +**Adapters:** Fastly, Axum, Cloudflare, Spin. All four must behave the same. Spin may be marked Unsupported only if its host decodes responses itself. +**Related:** EZ-6 (a begun request keeps the choice), EZ-9 (every hop keeps the choice), EZ-15 (a fire-and-forget send keeps it once EZ-15 lands), EZ-14 (an idle bound applies to the stream as delivered, encoded or decoded), EZ-25.1 (owns the forwarding example in the guide) + +#### Need + +Today TS receives upstream bytes still encoded on Fastly and Axum, which are its production and dev targets. The Fastly adapter copies every upstream header (`ts:crates/trusted-server-adapter-fastly/src/platform.rs:410-411`) and never turns on SDK decompression, which is off by default (`fastly-sdk:src/http/request.rs:131`). The Axum adapter builds `reqwest` without its decompression features (`ts:Cargo.toml:99`, `ts:crates/trusted-server-adapter-axum/src/platform.rs:292-299`). TS's Cloudflare and Spin adapters already decode (`ts:crates/trusted-server-adapter-cloudflare/src/platform.rs:395-415`, `ts:crates/trusted-server-adapter-spin/src/platform.rs:338-349`). So the regression lands on Fastly. + +TS relies on the upstream coding in these places: + +1. **Publisher origin.** TS narrows the client `Accept-Encoding` to gzip, deflate, and br (`ts:crates/trusted-server-core/src/publisher.rs:290-326`, called at `:3128` and `:4740`). It then routes on the response `Content-Encoding` (`:1622-1650`, `:6760`). It relays non-rewritten bodies raw (`:5420-5422`, `:5459`). It decodes rewritten bodies with the upstream coding and re-encodes them with the same coding (`:2602-2605`, `:2923-2928`). The one exception is a template-cache response, which TS emits as identity (`:813-820`). +2. **First-party proxy and creative rewrite.** The redirect loop asks for `gzip, deflate, br` (`ts:crates/trusted-server-core/src/proxy.rs:454`, `:1363-1366`). HTML and CSS are re-encoded with the input coding, and `Content-Encoding` is kept (`:653-699`, `:487-515`, `:459-481`). +3. **Asset proxy.** It forwards the client `Accept-Encoding` together with `If-None-Match`, `If-Modified-Since`, `If-Match`, `If-Unmodified-Since`, `Range`, and `If-Range` (`proxy.rs:90-102`, `:1146-1150`). It streams the response with the upstream representation headers intact. It strips only a short first-party denylist (`:1176-1194`, `:1003-1009`) and may then apply its own `Cache-Control` policy (`:1196-1200`). Origins that store objects with `Content-Encoding` metadata, such as S3, return that coding whatever the request asks for. So a range request returns a 206 whose `Content-Range` counts encoded bytes. +4. **Relays that keep the coding.** + - GPT forwards the client `Accept-Encoding` and rebuilds the response from `Content-Encoding`, `ETag`, and `Last-Modified`. It adds `Vary: Accept-Encoding` only when the response is encoded (`ts:crates/trusted-server-core/src/integrations/gpt.rs:159-166`, `:194-253`). + - The JS asset proxy does the same and also forwards `If-None-Match` and `If-Modified-Since` (`ts:crates/trusted-server-core/src/integrations/js_asset_proxy.rs:290-299`, `:365-410`). + - The Prebid bundle keeps the upstream `Content-Encoding` (`ts:crates/trusted-server-core/src/integrations/prebid.rs:1218-1237`). + - Sourcepoint forwards the client `Accept-Encoding` for paths it does not rewrite, and it skips rewriting encoded bodies (`ts:crates/trusted-server-core/src/integrations/sourcepoint.rs:862-875`, `:945-953`). + - Lockr API, Permutive, and DataDome `/js/*` forward the client `Accept-Encoding` and return the upstream response unchanged (`ts:crates/trusted-server-core/src/integrations/lockr.rs:237`, `:264`; `permutive.rs:255`, `:269`; `datadome.rs:766`, `:789`). + - Didomi forwards the client `Accept-Encoding` and returns the upstream body and representation headers, adding CORS or private cache headers (`didomi.rs:278-290`, `:506-516`). + +On PR 275 every one of these sites receives decoded bytes, with `Content-Encoding` and `Content-Length` removed. The regressions on Fastly are these: + +- Reader HTML ships to browsers uncompressed. TS sees no coding, so its encoder emits identity. +- Creative HTML and CSS, and every relayed JS, JSON, SVG, or font body, also ship uncompressed. Their `Content-Length` is gone, so the downstream response becomes chunked. +- A 206 over an encoded representation cannot be decoded. Asset range requests fail with a gateway error or return a corrupt prefix. +- Strong validators no longer describe the bytes served. GPT, the JS asset proxy, and the asset proxy forward the upstream `ETag` next to a decoded body. + +After the cut, TS asks for the original coding on every site that relays bytes or runs its own rewrite codec. Those are the publisher origin, every first-party proxy hop, the asset proxy, GPT, the JS asset proxy, the Prebid bundle, Sourcepoint, Lockr API, Permutive, DataDome `/js/*`, and Didomi. TS's rewrite pipeline then sees the real upstream coding again. TS keeps decoding on for sites that consume decoded text or JSON. These are the auction providers, EC pull sync, the Fastly management API, DataDome protection, the GTM script rewrite (which asks for identity, `ts:crates/trusted-server-core/src/integrations/google_tag_manager.rs:825-830`), the Lockr and Permutive SDK fetches, and DataDome `tags.js`. + +#### Gap at F + +- EdgeZero decodes a single bare `gzip`, `deflate`, or `br` unconditionally, then removes `Content-Encoding` and `Content-Length` (`edgezero:crates/edgezero-core/src/outbound.rs:1252-1297`, removal at `:1280-1286`; classification at `edgezero:crates/edgezero-core/src/compression.rs:84-126`). No request option turns this off (`outbound.rs:49-67`). +- Unknown and stacked codings, and malformed `Content-Encoding` values, already pass through encoded. Both headers are kept and only the decoded cap is skipped (`outbound.rs:1278`, `:1294`, `:1397-1401`; `compression.rs:94-105`; `edgezero:docs/guide/capabilities.md:395-398`, `:415-420`). Passthrough for recognized codings should behave the same way. +- Every adapter already asks its provider for raw bytes. Axum disables `reqwest` decoding (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:145-154`). Cloudflare asks fetch for manual response encoding (`edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:620-634`). Fastly never enables SDK decompression. **Unverified** whether the Spin host ever decodes. A Spin 4.1 run against a gzip origin would verify it. TS's own Spin adapter decodes in the guest, which is consistent with a raw host. +- A 206 is treated as a normal payload (`outbound.rs:1475-1484`), so a 206 with a recognized coding reaches the decoder. +- HEAD and 304 responses keep `Content-Encoding` and the encoded `Content-Length` (`outbound.rs:1472-1474`). In decoding mode, a HEAD therefore describes the encoded representation while the matching GET describes the decoded one. +- EdgeZero adds `Accept-Encoding: identity` only when the request has none, for example at `edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:752-760`. `OutboundRequest::from_request` keeps the browser's value (`outbound.rs:927-934`), so a forwarding app makes EdgeZero decode what the browser negotiated. The guide's forwarding example does exactly that (`edgezero:docs/guide/proxying.md:12-52`). + +#### Requirements + +- **EZ-5.1** An application MUST be able to choose, per request, whether EdgeZero decodes a recognized response content coding or delivers the response with its original coding. Decoding stays the default. In decoding mode, behavior is unchanged from F except EZ-5.8 and, if adopted, EZ-5.9 and EZ-5.10. The CHANGELOG MUST list those changes. +- **EZ-5.2** When the application asks for the original coding, a payload-bearing gzip, deflate, or br response MUST arrive byte for byte as the upstream sent it, with surviving `Content-Encoding` and `Content-Length` unchanged. Preservation throughout EZ-5 applies only after ordinary response normalization: Connection nominations, UTF-8 filtering, framing validation, and method/status body disposition remain as at F and run before coding interpretation or payload-cap checks. Original coding and 206 passthrough MUST NOT bypass that normalization or restore removed fields. EdgeZero MUST NOT spend decoder memory on an undecoded payload. +- **EZ-5.3** Identity responses, and responses whose coding EdgeZero does not recognize, MUST behave identically under both choices. Identity payloads MUST retain F's declared-length and streamed `max_decoded_response_bytes` enforcement even when original coding is requested; identity requires no decoder but still counts as decoded output. +- **EZ-5.4** Caps MUST still apply when the original coding is kept: + - For payload-bearing responses, the encoded cap (`max_encoded_response_bytes`) MUST apply to the declared `Content-Length` before the body is read and to the bytes as they stream. F's bodyless handling runs first: a HEAD or 304 representation length is not a body allocation and MUST NOT fail a body cap or cause a body read. + - In buffered mode, the final cap (`max_response_bytes`) MUST apply to the encoded bytes. + - The decoded cap MUST NOT apply to a non-identity encoded body EdgeZero passes through without decoding, including recognized codings kept in original mode and F's unknown-coding passthrough. Identity payloads still receive their declared-length and streamed decoded-cap checks under EZ-5.3. + - A request whose `max_brotli_window_bits` is invalid MUST be rejected the same way under either choice. + - Chunk shaping (`max_chunk_bytes`) MUST apply unchanged. +- **EZ-5.5** Keeping the original coding MUST leave `ETag`, `Last-Modified`, `Vary`, `Content-Range`, `Accept-Ranges`, `Cache-Control`, and digest fields surviving EZ-5.2's ordinary normalization exactly as received. A Connection-nominated field or a non-UTF-8 field that F filters out stays absent. +- **EZ-5.6** The choice is an option under cross-cutting requirement 1, in buffered and streamed mode alike. While EdgeZero holds a begun or batched response that kept its coding, cross-cutting requirement 3 bounds it by the caps as EZ-5.4 states them. +- **EZ-5.7** `Accept-Encoding` handling MUST NOT depend on the choice. EdgeZero adds `identity` only when the request carries no `Accept-Encoding`. A value the application set is forwarded unchanged. +- **EZ-5.8** EdgeZero MUST NOT decode a 206 response under either choice. It MUST deliver its original payload bytes and the `Content-Encoding`, `Content-Length`, and `Content-Range` fields surviving EZ-5.2's ordinary normalization. +- **EZ-5.9** In decoding mode, HEAD and 304 responses SHOULD carry the same representation metadata the matching GET would. A recognized coding and its encoded length SHOULD therefore be removed from them. +- **EZ-5.10** In decoding mode, when EdgeZero decodes, it SHOULD weaken a strong `ETag` and remove `Accept-Ranges` and `Content-Digest`, because those describe the encoded representation. RFC 9110 sections 8.8.1 and 8.8.3.3 treat content coding as part of the selected representation. +- **EZ-5.11** An application MUST be able to tell, before sending, which choice a request carries, so its tests can check what each call site asked for. +- **EZ-5.12** Parsing JSON from a response that kept a recognized coding MUST parse the bytes as delivered, without decoding them. A parse failure is F's JSON decode gateway error (`BadGatewayDecodeReason::Json`, `outbound.rs:1688-1693`). Its internal message SHOULD say that the body is content-encoded. +- **EZ-5.13** Where the capability matrix marks an adapter Unsupported for this choice, a request that asks for the original coding MUST be rejected under cross-cutting requirement 2. +- **EZ-5.14** The capability matrix MUST list encoded response passthrough per adapter (cross-cutting requirement 9). The forwarding example that EZ-25.1 owns MUST show a relay that keeps the original coding. + +#### Acceptance criteria + +1. **Unit.** Given gzip, deflate, and br fixtures that declare `Content-Length`, when they are delivered with the original coding and a decoded cap of 1 byte, then the bytes and both headers are unchanged and no cap error occurs. +2. **Unit.** Given the original coding is kept, when the declared length exceeds the encoded cap, then the request fails with the encoded-cap limit before the body is read. When the encoded bytes exceed the final cap in buffered mode, it fails with the final-cap limit (`ResponseLimitReason::BufferedBody` at F). +3. **Unit.** Given a 206 with `Content-Encoding: gzip` and `Content-Range: bytes 10-19/100`, in decoding mode, then the response arrives undecoded with all three headers unchanged. +4. **Axum, Viceroy 0.21.0, workerd, Spin 4.1.** Given an origin that returns a gzip body with `Content-Length`, a strong `ETag`, and `Vary: Accept-Encoding`, when the application asks for the original coding, buffered and streamed, then the bytes and all four headers match the origin. In decoding mode, the bytes are decoded. On Spin 4.1 this holds where the run shows the host passes encoded bodies through. If the run shows the host decodes, the run records that, Spin is marked Unsupported, and the check becomes that a request asking for the original coding is rejected before sending under cross-cutting requirement 2 (EZ-5.13). +5. **Same environments, with the same Spin condition as criterion 4.** Given the request sets no `Accept-Encoding`, the origin sees `identity`. Given the request sets `gzip, br`, the origin sees exactly that, under both choices. Where Spin is Unsupported, the Spin run checks decoding mode only. +6. **Viceroy 0.21.0.** Given a two-slot batch where one slot keeps the original coding, then that slot's body is encoded and the other slot's body is decoded. +7. **workerd.** Given the application relays a passthrough response to its client, then the client receives the exact compressed bytes and `Content-Encoding`, and Workers does not re-encode them. The run records whether the origin `Content-Length` reached the client, and the caveat at `edgezero:docs/guide/capabilities.md:422-426` is updated to match. +8. **Trusted Server, Viceroy 0.21.0.** Run the TS integration suite against origin fixtures. + - A gzip HTML page reaches the client with `Content-Encoding: gzip` and decodes to the rewritten document with the TSJS tag. + - A br SVG relay is byte-identical, with the origin `Content-Length` and `ETag`. + - `Range: bytes=0-99` against a gzip-stored asset returns 206 with the origin `Content-Range` and `Content-Length: 100`. + - A JS asset revalidation with `If-None-Match` returns 304. + - A `/first-party/proxy` HTML creative keeps `Content-Encoding: br`. + +9. **Unit, each adapter's response path.** A HEAD or 304 with `Content-Length: 1073741824` and a 64 KiB encoded cap succeeds without polling a body source under either decoding choice. With original coding requested, its representation length and coding remain unchanged; decoding mode follows EZ-5.9. +10. **Unit and each adapter's response-policy path.** An identity payload with decoded cap 10 and encoded/final caps 20 rejects `Content-Length: 11` before polling the source and rejects 11 observed bytes when no length is declared. Both decoding choices return `ResponseLimitReason::DecodedBody`. A 10-byte control succeeds under both choices. A gzip body kept in original mode remains exempt from the decoded cap, while its encoded and final caps still apply. +11. **Unit and each adapter's actual normalization/response-policy path, both decoding choices and both response modes.** `Connection: content-encoding, content-length, etag` removes those fields before coding/length interpretation; a nominated malformed length does not become a framing error, and removed fields are never restored. A separate non-UTF-8 `ETag` is dropped. Missing or nominated-away coding is classified as identity and retains its decoded cap. Malformed surviving `Content-Length` still fails, and non-UTF-8 `Content-Encoding` retains F's passthrough classification rather than being silently dropped. Valid surviving representation fields remain unchanged in original mode and on a 206. A 204 still removes length and suppresses its body; a 205 still normalizes length to zero and preserves F's declared-body evidence and disposition; HEAD/304 keep bodyless handling without payload-cap rejection or body polling. EZ-5.9's separately adopted metadata change is the only permitted HEAD/304 difference. Keep F's `response_normalization_precedence_table` assertions unchanged. Provider-inexpressible raw fields are injected at the adapter-visible/shared-head unit boundary; provider-visible controls retain the existing header-fidelity limitations. These cases make no new raw-header fidelity claim. + +12. **Conditional, only if EZ-5.10 is adopted; unit and each adapter's actual response-policy path.** A decoded gzip/deflate/br body with a surviving strong ETag, Accept-Ranges and Content-Digest weakens the ETag and removes the other two fields. An already-weak ETag stays weak without double-prefixing. Original coding, 206 and nondecoded controls retain their surviving metadata; fields removed by prior normalization never reappear. Record EZ-5.9 and EZ-5.10 adoption independently, including each applicable environment leg, rather than treating this criterion as implicit promotion. + +#### Out of scope + +- Re-encoding at response egress. EdgeZero compressing a response from the client's `Accept-Encoding` would not replace this requirement. A relay would still lose `ETag`, `Content-Range`, and `Content-Length` fidelity, and it would pay decode and encode cost on bodies it never reads. +- Deciding to decode after seeing the response headers. TS keeps its own rewrite codec for this cut. +- Any change to request-body encoding. F already sends request bodies with their declared coding. + +#### Open questions + +1. Should forwarding an inbound request keep the original coding by default, since forwarding is a proxy role? Trusted Server's preferred answer is no. One default everywhere is easier to reason about, and the forwarding example can set the choice explicitly. +2. Should EZ-5.9 and EZ-5.10 become MUST? Trusted Server's preferred answer is to keep them SHOULD. TS keeps the original coding on every relay, so it does not depend on them, but other applications that relay in decoding mode would benefit. + +### EZ-6. Begin a request without waiting, and observe a batch without blocking + +**Priority:** Blocking, except EZ-6.31, which is Follow-up and required before TS enables reusable sandboxes in production. +**Adapters:** Axum, Cloudflare, Fastly, Spin. The behavior must hold on all four. Fastly may be BestEffort where EZ-6.21 says so. +**Related:** EZ-1 (a begun GET/HEAD built from an inbound request is accepted the same way), EZ-2 (no Fastly platform timer silently shortens the aggregate deadline; explicit phase limits retain their own causes, and EZ-2.8 defers to EZ-6.16 when a slot's deadline is the cutoff), EZ-3 (EZ-3.3 owns the passed-deadline rule), EZ-4, EZ-5, EZ-7, EZ-11, EZ-12, EZ-14 (their per-request options apply to begun requests and batch slots), EZ-8 (Image Optimizer is Unsupported on begun requests and batches), EZ-9 (begun requests and batches never follow redirects), EZ-14 (lifetime of a streamed origin body), EZ-15 (fire-and-forget is separate from abandoning a begun request), EZ-24 (the test double models arrival-time judgment) + +#### Need + +On every auctioned navigation TS runs the origin fetch and the bidder fan-out at the same time, and it keeps every bid that arrived on time even when it looks at the bid late. + +Today on Fastly the publisher does this: + +1. On the overlapped path it starts the origin fetch, with a streamed response, before EC KV preload and bid construction (`ts:crates/trusted-server-core/src/publisher.rs:4834-4873`). The overlapped path is a navigation GET with EC preload and no template-cache key (`ts:crates/trusted-server-core/src/publisher.rs:4821-4846`, `:3109-3116`). Only the Fastly adapter supports it (`ts:crates/trusted-server-adapter-fastly/src/platform.rs:503-505`). +2. It reads the EC snapshot from KV (`ts:crates/trusted-server-core/src/publisher.rs:4874-4888`). +3. It builds, signs, and sends every bidder request without waiting (`ts:crates/trusted-server-core/src/auction/orchestrator.rs:1586-1834`). The in-flight bids are carried across the origin wait (`:27-48`). Auctions run only for GET navigations (`ts:crates/trusted-server-core/src/publisher.rs:3215-3222`). +4. It waits for the origin response head (`ts:crates/trusted-server-core/src/publisher.rs:5153-5154`). When the overlap does not apply, it sends the origin afterwards, and on an auctioned GET the bids are still in flight during that send (`:5155-5176`). +5. It streams the page and holds the tail at `` (`ts:crates/trusted-server-core/src/publisher.rs:3780`, `:3944`), where it collects the auction (`:4017`, `:4058`, `:4120`). The collection loop drains every in-flight bid (`ts:crates/trusted-server-core/src/auction/orchestrator.rs:2127-2297`). Non-HTML responses collect before their body streams (`ts:crates/trusted-server-core/src/publisher.rs:4195-4239`). A template-cache hit streams the cached template and collects at the seam (`:5108-5125`). +6. Collection keeps bids that arrived while the origin was slow, on purpose. The code says dropping them "would discard SSP responses that already arrived" (`ts:crates/trusted-server-core/src/auction/orchestrator.rs:2290-2296`). +7. After the bidders it makes one mediator call with the smaller of the remaining auction time and the mediator timeout, and skips mediation when no time remains (`ts:crates/trusted-server-core/src/auction/orchestrator.rs:2349-2364`). +8. After the response is delivered, inside the same Fastly serving callback, it runs EC pull sync, which sends partner requests concurrently and waits for them (`ts:crates/trusted-server-core/src/ec/pull_sync.rs:225-300`, called from `ts:crates/trusted-server-adapter-fastly/src/main.rs:618-643`). + +The numbers come from the example config. The navigation auction has 500 ms (`ts:trusted-server.example.toml:340`), and the `/auction` endpoint 2000 ms (`:282`). The commented-out mock mediator example uses 1000 ms (`:674`). The origin has 15 s first-byte and between-bytes timers (`ts:crates/trusted-server-core/src/publisher.rs:98`, `:4517-4518`). + +On PR 275's APIs, TS will begin the origin request for the overlapped navigation GET only. Every other origin forward, including POST, PUT, PATCH, DELETE, and OPTIONS, uses `HttpClient::send` accepting the inbound body stream, with bounded EdgeZero-side prebuffering permitted by EZ-1.11, and those methods never run an auction. TS sends the bidders as one `start_batch_until` batch with the auction deadline as the cutoff, keeps working (KV, origin wait, page streaming), checks the batch at body-chunk boundaries, collects it at ``, and then makes the mediator call with one `HttpClient::send`. Pull sync after delivery becomes a batch or plain sends inside the serving callback. + +Without this requirement TS has two bad choices. It can serialize the origin behind the auction, which adds the auction time to TTFB, or it can lose every bid whenever the origin head or the `` chunk arrives after the cutoff. On Axum, Cloudflare, and Spin it would also send no bid at all unless it polled the batch continuously. + +#### Gap at F + +- There is no way to begin one request without waiting. On Fastly `send` waits for the response head (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:450-453`, `:1279-1290`). Batches refuse streamed responses (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:2580-2584`), and the origin must stream. +- On Axum, Cloudflare, and Spin a batch sends nothing until it is polled. Each slot is an unpolled future (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:200-206`; `edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:389-395`; `edgezero:crates/edgezero-adapter-spin/src/outbound.rs:434-440`), although the trait promises that every eligible request is attempted first (`edgezero:crates/edgezero-core/src/outbound.rs:428`). +- Completion is judged when the application looks, not when the response arrived. On Fastly, once the cutoff has passed the driver does not examine pending slots and reports every one as unresolved (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:621`, `:649-651`). A slot examined after its own budget becomes a timeout even if the host received it in time (`:1002`). On the other adapters the completion instant is taken when the slot's future is polled to completion (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:203`; `edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:392`; `edgezero:crates/edgezero-adapter-spin/src/outbound.rs:437`). +- There is no non-blocking way to observe a batch. `OutboundBatch::next`, `collect`, and `HttpClient::send_all_until` all wait (`edgezero:crates/edgezero-core/src/outbound.rs:229-252`, `:343-399`, `:588-596`). Repeated termination exists in code and tests (`:349-351`, `:3035-3040`). +- On Fastly a buffered slot's body is read to the end before any other slot can be observed (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:838-875`, `:1069-1072`; `edgezero:docs/guide/capabilities.md:276-279`). +- The guide says elapsed time includes "any delayed guest observation" (`edgezero:docs/guide/capabilities.md:442-444`), and EdgeZero's design spec records that a provider-ready response can stay unresolved (`edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:1821-1826`). + +#### Requirements + +Beginning one request. + +- **EZ-6.1** An application MUST be able to begin one outbound request, keep working, and collect its response later. A request is on its way once EdgeZero has handed it to the host, the network stack, or the adapter's runtime, so that it proceeds with no further EdgeZero call from the application. When begin returns, the request MUST be on its way. Beginning MUST NOT wait for connection establishment or response headers. This MUST hold on all four adapters. +- **EZ-6.2** A begun request MUST support streamed response mode, where collection returns at the response head with a lazily read body, and buffered response mode, where collection returns the whole body under the request's response caps. +- **EZ-6.3** A begun request MUST accept a buffered request body and a bodiless GET/HEAD built from an inbound request (EZ-1). Any other streamed upload MUST be rejected before anything is sent, as an invalid request (400 class), the category F uses for a streamed upload in a batch. `HttpClient::send` MUST keep accepting streamed uploads. +- **EZ-6.4** For the same request and the same origin behavior, a begun request MUST produce the same status, headers, body, error category, and timeout cause as `HttpClient::send`. Options and client defaults apply to a begun request as cross-cutting requirements 1 and 2 say, including options accepted at BestEffort. +- **EZ-6.5** A begun request's time budget MUST be computed from the moment begin is called, with the same rules as `send`. Time spent settling a bodiless GET/HEAD (EZ-1) counts against it. +- **EZ-6.6** The application MUST be able to ask whether a begun request's response is ready without blocking. + +Batches. + +- **EZ-6.7** When `start_batch_until` returns, every eligible slot MUST be on its way (EZ-6.1), on all four adapters. A batch that is never polled MUST still send every eligible request. +- **EZ-6.8** If an adapter configuration cannot put a request on its way without being polled, beginning the request or the batch slot MUST fail with an internal error (500 class) before anything is sent. It MUST NOT silently defer sending. +- **EZ-6.9** The application MUST be able to check a batch without blocking. A check returns a newly finished slot, the batch termination, or "nothing new". It MUST work inside a handler and inside a streamed response body that EdgeZero is writing to the client, on all four adapters. It MUST follow the same rules as `OutboundBatch::next`, including at most one result per slot, a clean end before `Completed`, and repeated termination. + This is an explicit implementer-facing hard migration: `OutboundHttpClient` gains required eager scheduling operations alongside EZ-9.14's shared head-first network primitive, and every in-tree and supplied implementation migrates together. Lazy compatibility defaults are forbidden. A check or `next` transfers ownership of an item only once. A subsequent collection MUST distinguish an already-delivered index from an unresolved cutoff index, including in partial-failure results; it MUST NOT return an ambiguous `None` under `Completed`. The chosen hard-cut representation is `AlreadyDelivered`, `Resolved(result)`, or `Unresolved`, with input-index alignment and a batch-bound accumulation/merge path. `Completed` permits no `Unresolved`; a failure retains precise error and all remaining observed outcomes. No full-history replay or response cloning is required. + Consumer items have immutable batch identity, index and result, with read-only inspection and consuming decomposition but no public constructor/reconstruction or mutable result/index path. Result/failure collections follow the same rule: read-only slots/termination/error access retains merge eligibility; consuming decomposition transfers the outcomes and relinquishes it. Only core's validated ledger stamps them; adapter helpers produce unvalidated driver events. Failed accumulation or merging MUST return ownership of the rejected item or both unchanged inputs, including the original driver error separately from a merge-validation error. Consuming a receipt relinquishes its accumulator eligibility; it does not permit reconstructing another receipt. + Producer construction consumes the facade's opaque prepared batch context, preserving original indices, entry time, paired clock/timer, cutoff, selected per-slot deadlines/sources and preflight outcomes. Raw count/clock constructors and lazy producer factories are removed in the hard cut. Drivers retain scheduling and transport ownership; core's ledger derives consumer timing and cutoff eligibility from captured metadata and terminal observations, never the later collection time. Typed earlier outcomes, including a connect timeout before the governing deadline, retain their original cause across late collection. +- **EZ-6.10** `OutboundBatch::next` MUST stay safe to cancel. A `next` that is waiting MUST return no later than the cutoff plus 5 ms, even when no slot finishes. + +Arrival-time judgment. + +- **EZ-6.11** Arrival is as cross-cutting requirement 5 defines it. While the application waits on any EdgeZero operation (a begun request's response head, a read from a response body EdgeZero produced, a single send, a batch wait, or a non-blocking check), EdgeZero MUST record the arrival of every other in-flight request in the same request lifetime (cross-cutting requirement 4). Axum, Cloudflare, and Spin MUST also record arrivals while the application yields to their executor awaiting unrelated work. CPU work or awaits that complete synchronously without yielding do not imply a scheduling opportunity; an arrival MUST be recorded at the next EdgeZero observation operation or executor scheduling opportunity, without inventing an earlier observation time. The guide MUST say so. A buffered slot whose body has not ended by the cutoff is unresolved at the cutoff, and EdgeZero MUST NOT wait past the cutoff to finish reading it. EdgeZero MUST NOT enter a response-body read that prevents cutoff enforcement or observation of other in-flight requests. +- **EZ-6.12** A slot is on time if and only if its recorded arrival is strictly before the observation cutoff. An arrival at the cutoff is late. +- **EZ-6.13** A slot's or begun request's own budget MUST be judged at its recorded arrival. A response that finished within its budget MUST be delivered as received, even when the application collects it after the budget and after the cutoff. Collection itself MUST NOT turn such a response into a timeout. +- **EZ-6.14** When EdgeZero first observes a slot at or after the slot's budget deadline, whether or not a response is waiting, it MUST treat the slot as a timeout at the budget deadline, because an earlier arrival cannot be proven. EZ-6.16 decides the outcome when that deadline is the cutoff. +- **EZ-6.15** Finished slots MUST be delivered in order of recorded arrival, with ties in input order. A slot's elapsed time MUST be its arrival minus the batch start. +- **EZ-6.16** After the cutoff, the batch MUST first deliver every slot that arrived on time, then report `Cutoff` if any slot is unresolved and `Completed` otherwise. Nothing first observed at or after the cutoff may be delivered, except a timeout whose budget deadline is before the cutoff. A slot whose budget deadline equals the cutoff has one outcome. It is not delivered, and the batch reports `Cutoff` with that slot unresolved. EZ-2.8's per-slot timeout item applies only to a slot whose deadline is before the cutoff. + +Bounded background work. + +- **EZ-6.17** Responses that arrive while the application is busy elsewhere follow cross-cutting requirement 3. A cap failure MUST become that slot's outcome at the moment the cap was exceeded. +- **EZ-6.18** Receiving one slot's body MUST NOT stall the application. While a bidder's body trickles in, a streamed origin body that the application is reading MUST keep flowing, and other slots MUST still be recorded at arrival. A blocking response-body read is not an allowed fallback. + +Cancellation and request lifetime. + +- **EZ-6.19** Dropping or cancelling a begun request or a batch releases it as cross-cutting requirement 6 says. Cancelling a batch MUST report the slots it never delivered. Results recorded but not yet delivered are discarded by cancellation, so an application that wants them checks the batch first. +- **EZ-6.20** On Fastly, outbound work the application starts inside its per-request serving callback after delivery MUST behave as it does during dispatch, including deadlines, batches, non-blocking checks, and arrival recording. This holds with the per-request client, a client the application keeps for the sandbox, or a newly built client. Cleanup follows cross-cutting requirement 4: callback-owned work ends with that callback, including an error return, even if its client or public pending handle is retained elsewhere; response-owned work ends with its egress. Request-helper return and response completion MUST NOT be mistaken for callback return. On asynchronous egress adapters, returning a response transfers its body-owned work to the adapter coordinator rather than releasing it. Fire-and-forget sends follow EZ-15 instead. +- **EZ-6.21** Per-adapter levels. Axum, Cloudflare, and Spin MUST be Native for beginning a request, checking a batch without blocking, and arrival-time recording, subject to EZ-6.11's cooperative scheduling boundary. On Fastly, a cold backend registration may block begin and arrivals are recorded only during EdgeZero operations, so its levels remain BestEffort. Fastly qualification nevertheless requires verified deadline-bounded response-header and response-body readiness handling and subsequent reads, under Viceroy 0.21.0 and hosted Fastly. If a configuration cannot honor a required bounded wait or non-blocking check, the affected operation MUST fail with an internal error (500 class) before entering the unbounded host operation, and before dispatch if the limitation is already known. That failure is not a passing qualification result or a substitute for the required behavior. A blocking response-body read is not an allowed fallback. Every other requirement in EZ-6 holds on Fastly as written. + +Interaction with response writing. + +- **EZ-6.22** A begun request and a batch MUST be usable from inside a streamed response body for as long as that body lives. On Axum, Cloudflare, and Spin this includes adapter-owned asynchronous egress after the request-dispatch or platform callback returns; that return MUST NOT release work owned by the response. +- **EZ-6.23** When EdgeZero completes or aborts response egress (including client disconnect, write deadline, body error, or request cancellation), every begun request and batch owned by that response body MUST be released per EZ-6.19. This MUST NOT release separate work owned by a still-active Fastly serving callback. Egress completion means the end of the adapter's ownership, not confirmed client delivery. +- **EZ-6.24** On Fastly, a batch wait inside a response body that EdgeZero is writing SHOULD return by the response write deadline, so a cutoff set past that deadline cannot hold the response. The response then ends with the existing write-deadline outcome. +- **EZ-6.25** A streamed response from a begun request MUST keep enforcing its own deadline on each body read. The guide MUST state that a streamed page lives until the earlier of the origin request's deadline (from begin) and the response write deadline (30 s from the start of writing by default, `edgezero:crates/edgezero-core/src/response_egress.rs:14`), and that a hold inside the body counts against both. + +Sequential calls and invariants. + +- **EZ-6.26** `HttpClient::send` and begin MUST work while a batch from the same request lifetime is in flight and after it finishes, including inside a streamed response body. After unsupported-option rejection, a request whose deadline has already passed follows EZ-3.3 on every adapter: a gateway timeout naming the selected budget source and nothing sent, as F already does for otherwise supported requests (`edgezero:crates/edgezero-core/src/time.rs:176-182`). +- **EZ-6.27** Batches MUST keep refusing streamed uploads and streamed response mode. Begun requests and batches MUST NOT follow redirects; a 3xx is returned as the final response. +- **EZ-6.28** Image Optimizer is Unsupported on begun requests and batch slots. Those send modes MUST reject it as cross-cutting requirement 2 says, before any other outbound preflight check. +- **EZ-6.29** Removed. +- **EZ-6.30** EdgeZero's capability matrix MUST gain rows for beginning a request, checking a batch without blocking, and arrival-time recording, at the levels in EZ-6.21. The guide text that says elapsed time includes delayed observation (`edgezero:docs/guide/capabilities.md:442-444`) and that ready handles can stay unobserved (`:276-279`) MUST be corrected. +- **EZ-6.31** Follow-up, required before TS enables reusable sandboxes. On a reused Fastly sandbox, abandoned requests MUST NOT accumulate. An ordinary outbound exchange still in flight when its owning callback and response egress ended MUST NOT be observed by a later request in the same sandbox, the next request MUST still send, and abandoned work MUST be gone once its backend answers or its platform timer fires. Fire-and-forget work has the separate lifetime and accounting of EZ-15 and MUST NOT deliver an outcome into a later request's batch. + +#### Acceptance criteria + +Unless a criterion says otherwise, the scenarios use three bidders A, B, and C, a 500 ms auction, bidder requests with a 1,000 ms per-call timeout, and an origin request with a 15 s timeout. The batch starts at 34 with cutoff 534, so each bidder's budget is 500 ms, set by the cutoff. Times are milliseconds from the inbound request. Elapsed values hold within 5 ms on Axum, workerd, Spin 4.1, and Viceroy 0.21.0, and exactly in unit runs with an injected `MonotonicClock`. On hosted Fastly the origins apply the stated delays after receiving each request. A time measured inside the guest from a deadline or the cutoff (scenario 2's gateway timeout at 534, scenario 7's `Cutoff` by 539) keeps its 5 ms tolerance. Otherwise the pass condition is the set and order of delivered slots and the `Cutoff` or `Completed` outcome. Each elapsed value MUST be no less than the stated value minus 5 ms, and no more than the stated value plus that slot's measured connection setup and round trip plus 5 ms. The run reports both measurements for every slot. Origins are loopback or staging servers that behave as each scenario states. + +1. Slow origin. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin is begun at 2 and its head arrives at 700, the page body arrives in chunks at 700, 900, and 1,300, the batch starts at 34 with cutoff 534, A answers at 180, B at 320, and C at 900, when the application waits for the origin head and checks the batch at 701, then it gets A with elapsed 146, B with elapsed 286, and `Cutoff` with C unresolved. At F, the same flow loses A and B on Fastly and sends no bidder request on the other adapters. +2. Fast origin with mediator. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin head at 120, a body chunk at 260, and A, B, C answering at 180, 320, and 400, when the application waits on the batch at 260, then it gets A, then B, then C and `Completed` at 400. A `send` at 400 built with `OutboundRequest::deadline` set to the cutoff and `OutboundRequest::timeout` set to 1,000 ms gets a 134 ms budget with the request deadline as its cause. If that origin stays silent, the send fails with a gateway timeout at 534. +3. CPU stretch. Axum, workerd, Spin 4.1, Viceroy 0.21.0. Given the application does CPU work from 520 to 560 without awaiting and B answers at 530, then B is either delivered with elapsed 496, where the adapter recorded its arrival during the CPU work, or not delivered, with the batch reporting `Cutoff` and B unresolved. B is never delivered with an elapsed time of 500 or more. The run records which. +4. Begin puts the request on its way. Every adapter. Given begin returns and the application then awaits an unrelated operation (a timer or a KV read) for 100 ms without any other EdgeZero outbound call, then the origin receives the request within that window. +5. Unpolled batch. Every adapter. Given a batch that the application does not check for 300 ms while it awaits an unrelated EdgeZero operation, then every bidder receives its request within that window. +6. Arrival during origin body reads. Viceroy 0.21.0 and hosted Fastly. Given the origin sends body chunks at 100 and 800 and B answers at 320, when the application only reads the origin body, then B is delivered with elapsed 286. +7. Trickling slot. Viceroy 0.21.0 and hosted Fastly. Given a bidder sends its head at 100 and its body at 600 while the origin streams a chunk every 50 ms, then the origin chunks keep reaching the application between 100 and 600 without a stall caused by reading the bidder body. With a 534 cutoff, that bidder is unresolved and the batch reports `Cutoff` by 539. A body-readiness or blocking-read fallback that delays the application until 600 does not pass. +8. Send during a live batch. Viceroy 0.21.0 and hosted Fastly. Same as scenario 1, but the origin is forwarded with `send` after the batch starts. A and B are delivered. +9. Caps while waiting. Every adapter. Given a bidder slot with a 1 MiB decoded cap and a 64 KiB buffered cap, whose gzip body decodes to 128 KiB and trickles in over 300 ms, then the slot fails with the buffered-cap error before the body ends. +10. Abandonment on a reused sandbox (EZ-6.31). Viceroy 0.21.0. Given a reused guest serving 6 requests, each beginning a request to an origin that answers after 2 s and dropping it, then request 6 still sends, no request observes another's arrivals, and no abandoned work remains 2 s after the last request. Hosted Fastly. With reuse limits raised so one sandbox can serve 1,000 requests at about 50 per second, the same holds for request 1,001. The run records how many requests each sandbox served. +11. Write deadline. Axum, workerd, Spin 4.1. Given a response with a 1 s write deadline whose body waits on a batch with a 3 s cutoff, then the response ends with the write-deadline outcome, and the batch's in-flight requests are released. On Viceroy 0.21.0 and hosted Fastly the run records the outcome, because EZ-6.24 is a SHOULD there. +12. Parity. Every adapter. For buffered uploads, a begun request and `send` produce the same outcomes on EdgeZero's existing outbound contract cases, including client defaults. +13. Upload rules. Unit and every adapter. Begin rejects a POST with a streamed body as an invalid request before sending. Begin accepts a bodiless GET built from an inbound request. `send` forwards a POST with a streamed body. +14. Timeout before the cutoff. Every adapter. Given a fourth bidder D in the scenario 1 batch with a 300 ms per-call timeout that never answers, then the batch delivers D as a gateway timeout naming the per-call timeout, with elapsed 300, before it reports `Cutoff`. +15. Arrival at the cutoff. Unit, injected clock. Given a slot whose response is complete exactly at the cutoff, then it is unresolved. +16. Waiting `next` ends at the cutoff. Every adapter. Given a batch with a 200 ms cutoff and no slot answering, then a waiting `next` returns `Cutoff` within 205 ms. Repeat with response headers already received but the body permanently pending; the same bound holds and the slot stays unresolved. +17. Cancellation report. Unit and every adapter. Given a batch with one slot recorded but not delivered and one still in flight, when it is cancelled, then both indices are reported as undelivered. +18. Check inside a streamed body. Every adapter. Given a streamed response body that checks the batch between chunks while two slots are still in flight, then each check returns "nothing new" in under 5 ms. +19. Work after delivery. Viceroy 0.21.0. Given a serving callback that, after delivery, starts a three-slot batch with a client it keeps and collects it, then all three results are delivered with arrival-based elapsed values, and nothing from that batch is still in flight after the callback returns. +20. Work after response handoff. Axum, workerd, and Spin 4.1. A streamed response owns a begun origin request and a bidder batch whose results are still pending when request dispatch or the platform callback returns. The body then collects both and streams their results successfully. Handoff does not cancel either exchange or release its resources early. +21. Response-owned cleanup. Unit for each adapter and Axum, workerd, and Spin 4.1. Repeat the response-owned setup with normal body completion, disconnect, expired write deadline, source failure, and explicit cancellation, wherever observable. Each path releases body-owned exchanges and SDK-owned resources exactly once, stops guest-side polling, and preserves the adapter's documented host-cancellation limitations. Unit and Viceroy 0.21.0 keep a separate callback-owned exchange pending on the same client before response cleanup, then prove cleanup neither cancels it nor stops its observation: that exchange completes in the still-active Fastly callback, which can also start new post-delivery work. A client-wide cancellation that destroys both owners does not pass. +22. Mixed observation and collection. Unit and every adapter. Consume slot 0 with a nonblocking check, slot 1 with `next`, then collect a three-slot batch. The first two positions are `AlreadyDelivered`, never unresolved; the third is `Resolved` on success or `Unresolved` at cutoff. A driver failure after the first two deliveries preserves those states and its exact error. Merging into the matching accumulator restores all owned outcomes without replay or cloning; missing prior results, a foreign batch, duplicate insertion, and a conflicting slot are rejected without losing either input's results. Fresh-batch collection has no `AlreadyDelivered`, and `Completed` never contains `Unresolved`. +23. Callback-scope cleanup. Unit for both Fastly lifecycle wrappers and Viceroy 0.21.0. A custom callback retains a pending callback-owned exchange in state outside the callback, completes response delivery, starts post-delivery work, then returns an error before that exchange finishes. Response cleanup leaves the exchange live until callback return; callback return stops its guest-side work and releases SDK-owned resources exactly once even though the public handle remains retained. Repeat with a normal return and an early error before delivery, without inventing a replacement response. Unit tests cover unwinding where supported, not process-abort teardown. Single-request mode must pass; a next-request isolation leg is conditional on reusable-sandbox adoption, not a prerequisite for this cleanup. + +The Trusted Server end-to-end check runs in TS's integration suite on Viceroy 0.21.0, the Axum binary, and workerd, and in the Spin smoke (`ts:scripts/smoke-spin.sh`) on Spin 4.1. A navigation whose origin head arrives at 700 ms with a 500 ms auction contains the bids from bidders that answered at 180 and 320 ms and not one that answered at 600 ms. A page whose `` arrives at 1,300 ms gives the same result, and TS ranks locally because no mediator time remains. TTFB tracks origin latency plus less than 20 ms. A POST navigation forwards its body through `send` and runs no auction. On Fastly, pull sync after delivery completes inside the serving callback. + +#### Out of scope + +- Streamed responses in batches. One begun request per streamed exchange is enough. +- Streamed uploads on a begun request. TS forwards those with `send`. +- Redirect following on begun requests and batches (EZ-9). +- Fire-and-forget sends (EZ-15). +- An idle policy for long streamed bodies (EZ-14). + +#### Open questions + +1. When EdgeZero first observes a slot late, should it credit the arrival to the previous time it looked? TS prefers no. Crediting the earlier time would admit responses that arrived after the cutoff, and TS treats a delivered result as on time. +2. Should a Fastly batch wait inside a response body be bounded by the write deadline as a MUST rather than the SHOULD in EZ-6.24? TS prefers SHOULD. TS's cutoff (500 ms) is far inside the 30 s write budget, so TS does not depend on it. +3. Which hosted evidence establishes bounded response-body readiness and reads? EdgeZero MUST resolve this before qualification and record it under EZ-2.6, EZ-6.18, and EZ-6.21. If the host cannot provide the required bounded behavior, the blocking acceptance criteria do not pass; a body-transfer-length stall is not an alternative passing result. + +### EZ-7. Keep the `x-edgezero-proxy` header off relayed responses + +**Priority:** Blocking +**Adapters:** Fastly, Axum, Cloudflare, Spin. Identical behavior on all four. +**Related:** EZ-6 and EZ-9 (begun requests and the final response of a followed chain honor the setting), EZ-15 (a fire-and-forget send honors it once EZ-15 lands), EZ-5 (same relay paths), EZ-24 (its test double applies the same default and opt-out), EZ-25.1 (owns the forwarding example) + +#### Need + +TS has no global response-header filter. Its Fastly adapter copies every header of a TS response to the client (`ts:crates/trusted-server-adapter-fastly/src/compat.rs:13-19`, `:49-55`). These paths return upstream headers wholesale, or minus a small denylist: + +- The publisher fallback, on every route (`ts:crates/trusted-server-core/src/publisher.rs:5337-5370`, `:5420-5422`, `:5459`). +- The asset proxy, minus set-cookie, HSTS, and clear-site-data (`ts:crates/trusted-server-core/src/proxy.rs:1194`, `:1003-1009`, `:110-111`). +- `/first-party/proxy`, minus the CORS headers (`proxy.rs:747-761`, `:727-731`). +- GTM beacons and non-2xx GTM script responses, minus the first-party denylist (`ts:crates/trusted-server-core/src/integrations/google_tag_manager.rs:968-987`, strip at `:976`). +- Testlight (`ts:crates/trusted-server-core/src/integrations/testlight.rs:115-130`). +- Sourcepoint, after its own cache and `Location` adjustments (`ts:crates/trusted-server-core/src/integrations/sourcepoint.rs:934`, `:995`, `:1027`, `:1042`, `:1046`). +- Lockr API (`lockr.rs:237`), Permutive (`permutive.rs:255`), DataDome non-200 `tags.js` and `/js/*` (`datadome.rs:668-674`, `:789`), and Didomi (`didomi.rs:506-516`), which return the upstream response with its headers. + +GPT, the JS asset proxy, the Prebid bundle, and the SDK fetches rebuild their responses from allowlists, so they would not leak (`gpt.rs:194-253`, `js_asset_proxy.rs:365-443`, `prebid.rs:1218-1253`). + +On PR 275, every response on those paths gains `x-edgezero-proxy: fastly`. Every proxied page and asset then tells browsers which edge runtime served it, and any downstream cache that stores origin headers keeps the field too. Under the hard-cut decision TS does not strip headers itself, so EdgeZero must not add the field. Two secondary effects follow: + +- The marker consumes response header budget. It costs 20 to 26 bytes depending on the adapter, plus one field (`edgezero:crates/edgezero-core/src/outbound.rs:3361-3403`). So a header-count cap of N admits only N-1 upstream fields. +- Removing it after the fact would mark the response's headers as modified (`outbound.rs:631-639`, `:653-656`). TS does not read that provenance today. + +#### Gap at F + +- Every adapter adds `x-edgezero-proxy: ` to every outbound response after header normalization, bodyless responses included. It overwrites any upstream field of that name and is charged to the header caps (`outbound.rs:1407-1422`; Fastly `edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:1016-1020`; Axum `edgezero:crates/edgezero-adapter-axum/src/outbound.rs:335-339`; Cloudflare `edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:526-530`; Spin `edgezero:crates/edgezero-adapter-spin/src/outbound.rs:660-664`). +- `into_response` carries the field into the application's response (`outbound.rs:723-742`). +- No option on `OutboundRequest` or `HttpClient` turns it off. +- Only tests read the field. Those are the core unit test (`outbound.rs:3375`), an Axum contract test (`edgezero:crates/edgezero-adapter-axum/tests/contract.rs:56-79`), and the docs contract check (`edgezero:scripts/check_outbound_docs_contract.mjs:219`, `:224`). EdgeZero's design says the marker is not a loop-prevention mechanism (`edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:43-45`). +- The marker predates PR 275. TS never received it, because TS did not use EdgeZero's proxy client. + +#### Requirements + +- **EZ-7.1** An application MUST be able to receive outbound responses that carry no EdgeZero-added `x-edgezero-proxy` field. The setting is an option under cross-cutting requirement 1. For a followed redirect chain, it applies to the final response. +- **EZ-7.2** The application MUST be able to set this once for every request it sends through one client handle, including requests it begins, batches, or follows and, once EZ-15 lands, fire-and-forget sends. A per-request setting MUST override the client-wide setting. +- **EZ-7.3** When the marker is off, it MUST NOT be charged to `max_response_header_bytes` or `max_response_header_count`. Those caps then count only upstream fields. +- **EZ-7.4** When the marker is off, EdgeZero MUST neither add nor remove an `x-edgezero-proxy` field. An upstream field of that name is an ordinary upstream header. +- **EZ-7.5** When the marker is on, its value, overwrite behavior, and cap accounting MUST stay as at F. The default remains on for this plan; the client-wide off setting is the supported relay policy. Changing the default requires a separately approved requirement revision, not a compatibility branch or an unresolved implementer choice. +- **EZ-7.6** An application SHOULD still be able to learn which adapter completed an exchange without the header. +- **EZ-7.7** An application MUST be able to tell, before sending, whether a request will carry the marker, after any client-wide setting is applied. Its tests can then check each call site. +- **EZ-7.8** The guide text that describes the marker (`edgezero:docs/guide/adapters/overview.md:78`, `edgezero:docs/guide/capabilities.md:389-390`) MUST describe how to keep it off. The forwarding example that EZ-25.1 owns MUST show it off for responses that reach browsers. + +#### Acceptance criteria + +1. **Unit.** Given an upstream response with N fields and a header-count cap of exactly N, when the marker is off, then the response succeeds. When it is on, it fails with the header-count limit. The same holds for header bytes. +2. **Unit.** Given a client handle set to omit the marker, when the application sends one request with no override, one with an explicit "on", one begun request, and one two-slot batch, then only the explicit "on" request carries the marker. +3. **Unit.** Given the marker is off and the upstream sends `x-edgezero-proxy: upstream`, then the field arrives unchanged. +4. **Axum, Viceroy 0.21.0, workerd, Spin 4.1.** Given 200, 204, HEAD 200, 304, and 302 responses from an origin, when the marker is off, then none carries the field. Under F's behavior, all of them carry it. +5. **Trusted Server, Viceroy 0.21.0.** Browser-facing responses from the publisher fallback, asset proxy, `/first-party/proxy`, GTM collect, Testlight, Lockr API, Permutive, DataDome `/js/*`, Sourcepoint, and Didomi carry no `x-edgezero-proxy`. TS's request client asks for the marker to be off once, and no TS call site sets it per request. + +#### Out of scope + +- Any loop-prevention or `Via` behavior. +- Filtering other upstream headers. TS's own allowlists and denylists stay TS business logic. + +#### Open questions + +1. The previous default question is resolved for the current implementation plan: keep one default, on, and configure browser-facing client handles off under EZ-7.2. A later maintainer-approved change can choose off globally without a compatibility path. + +### EZ-8. Fastly Image Optimizer on outbound requests + +**Priority:** Blocking +**Adapters:** Fastly single sends must honor the options. Axum, Cloudflare and Spin are Unsupported, and so are Fastly begun requests, batches and fire-and-forget sends. +**Related:** EZ-1 (a bodiless GET/HEAD from an inbound request may carry the options, and the rejection comes before body settling), EZ-3 (EZ-3.3 owns the deadline-passed-before-sending rule), EZ-6 and EZ-15 (begun requests and, once EZ-15 lands, fire-and-forget sends reject the options), EZ-11 (EZ-8.6 rejects the options combined with cache tagging), EZ-2 (the options must not reduce connection reuse, and EZ-2's eligibility groups bound the uninterruptible window), EZ-5 (asset routes forward the client's `Accept-Encoding`, so unoptimized assets need encoded passthrough), EZ-9 (the options carry to redirect hops), EZ-16 (provider causes from this path reach logs), EZ-Q1 (owns the hosted Fastly criterion) + +#### Need + +Trusted Server serves publisher images through asset routes and asks Fastly Image Optimizer to transform them. `handle_asset_proxy_request` (`ts:crates/trusted-server-core/src/proxy.rs:1096-1204`) does this in order. + +1. It builds the options from the route's profile table unless the incoming or rewritten path is SVG (`proxy.rs:1105-1117`, `ts:crates/trusted-server-core/src/asset_image_optimizer.rs:43-97`). The closed set covers region, a preserve-origin-query flag, format, quality, resize filter, width, height and crop (`ts:crates/trusted-server-core/src/platform/image_optimizer.rs:12-181`). +2. It strips the origin query by default (`proxy.rs:1118-1120`, `ts:crates/trusted-server-core/src/settings.rs:1738-1758`). Config validation rejects preserving the query on Image Optimizer routes (`settings.rs:1709-1719`) because Image Optimizer reads client query parameters as transformation inputs (`settings.rs:965-967`). +3. It sets `Host` to the URI authority (`proxy.rs:1151`, `:893-917`). S3 SigV4 signs `host`, `x-amz-content-sha256`, `x-amz-date` and an optional `x-amz-security-token` (`ts:crates/trusted-server-core/src/s3_sigv4.rs:62-149`, header set `:128-133`). +4. Only when the route has options, S3 auth, and the request is GET or HEAD (`proxy.rs:1015-1023`), it first sends a signed HEAD without Image Optimizer (`:1025-1078`), because Image Optimizer can mask S3 errors (`:1037-1039`). A failed HEAD answers a HEAD client directly, or triggers a signed streamed GET without Image Optimizer that returns the raw S3 error (`:1056-1077`). +5. It then signs the final request (`proxy.rs:1167-1169`), attaches Image Optimizer (`:1173-1175`), asks for a streamed response (`:1176-1179`) and sends (`:1181-1187`). + +The Trusted Server Fastly adapter translates the options to the SDK (`ts:crates/trusted-server-adapter-fastly/src/platform.rs:190-305`), sends with the blocking `fastly::Request::send` (`:521-523`), and refuses Image Optimizer on its async path (`:527-536`). + +After the cut Trusted Server deletes that adapter layer. The asset route builds an `OutboundRequest` with `stream_response()`, a `timeout`, the SigV4 headers set through `header(..)`, and the Image Optimizer options, then calls `HttpClient::send`. The S3 HEAD preflight and the raw-error GET stay Trusted Server logic and are plain signed sends without options. Without EZ-8 the options cannot be attached, so every optimized route serves the untransformed original at full size and in its original format. A local operator configuration (not named here) enables an Image Optimizer route with S3 SigV4 today, so this blocks the cut. + +#### Gap at F + +`OutboundRequest` carries a fixed portable option set and no way to attach provider options (`edgezero:crates/edgezero-core/src/outbound.rs:48-67`, parts `:69-89`). The Fastly SDK constrains what EdgeZero can offer. + +- Image Optimizer works only on the blocking `Request::send` (`fastly-sdk:src/http/request.rs:606-635`, `send_with_image_opto` `:668-792`). `send_async` refuses it (`:861-865`). The streaming send never looks at it (`:918-947`). Converting a request to handles drops it (`:148-152`, `:2853-2865`). +- Cache pass silently disables it (`:641-660`), and it silently overrides surrogate keys (`:3134-3141`). +- It sends no request body (`:696`, `:761-763`). + +At F the Fastly single send uses `send_async` (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:443-454`), and batch slots use handles (`:519-529`). The error mapper folds `ImageOptimizerUnsupported` into a generic 502 with the message "Fastly outbound request failed" (`:960-965`, `:91-94`). Viceroy cannot run Image Optimizer and answers its host call with `Unsupported` (`viceroy:src/component/image_optimizer.rs:7-15`, `viceroy:src/error.rs:248`), which the SDK reports as `ImageOptimizerUnsupported` (`fastly-sdk:src/http/request.rs:714-719`). The SDK's own `send_async` produces the same cause without reaching the host (`:861-865`). + +#### Requirements + +- **EZ-8.1** An application MUST be able to attach Fastly Image Optimizer options to an individual outbound request from portable code, without depending on the Fastly SDK and without compiling Fastly-only code for other targets. The options MUST stay attached wherever EdgeZero rebuilds or forwards the request, including redirect hops (EZ-9). +- **EZ-8.2** On every adapter and send mode that EZ-8.14 marks Unsupported, a request carrying the options MUST get cross-cutting requirement 2's rejection. The rejection MUST come before anything is sent and before any other outbound preflight check, including EZ-1's settling of an inbound body. It MUST name the option and the adapter. The options MUST NOT be dropped silently. +- **EZ-8.3** The options MUST express at least what Trusted Server uses. That is the seven SDK regions, the preserve-origin-query flag, output format (auto, AVIF, GIF, JPEG, JPEG XL, MP4, PNG, WebP), quality in 0 to 100, resize filter (nearest, bilinear, bicubic, Lanczos 2, Lanczos 3), width and height in pixels, and a crop to an aspect ratio with a default, smart, or explicit x and y percentage focus. Invalid values MUST be rejected when the application builds the options, as a client-class error. Invalid means quality above 100, a zero dimension, an offset above 100, smart focus combined with offsets, or only one offset. Trusted Server enforces all five rules today, at config load (`ts:crates/trusted-server-core/src/settings.rs:1429`, `:1485-1509`) and at runtime (`ts:crates/trusted-server-core/src/asset_image_optimizer.rs:229`, `:275-300`, `:316-343`). +- **EZ-8.4** On Fastly, a single send carrying the options MUST be processed by Image Optimizer, so the response body is the transformed image. Explicit redirect following uses this same supported single-send path on each permitted hop (`OutboundSendMode::Follow`); it is not a begun request or batch. The rebuilt hop retains the options under EZ-8.1 and still passes the method, cache, target-policy and original-chain-deadline checks. +- **EZ-8.5** Only GET and HEAD may carry the options. A request with any other method, or with a streamed upload that would be uploaded, MUST get cross-cutting requirement 2's rejection before backend registration, because Fastly cannot honor the options there. A GET or HEAD whose body EZ-1 settles as bodiless is accepted. One that carries body bytes is rejected as EZ-1 requires. +- **EZ-8.6** A request carrying the options that also asks for cache bypass or cache tagging (EZ-11) MUST get cross-cutting requirement 2's rejection before sending. Fastly silently disables Image Optimizer under bypass and ignores tags under Image Optimizer. Only the default cache policy is valid with the options. This requirement owns that rule. +- **EZ-8.7** In a batch, a slot carrying the options MUST fail as that slot's own result with the EZ-8.2 rejection, and its siblings MUST proceed. F already isolates streamed slots this way (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:2573-2586`, `edgezero:crates/edgezero-adapter-fastly/tests/contract.rs:1262-1279`). +- **EZ-8.8** Begun requests (EZ-6) and, once EZ-15 lands, fire-and-forget sends MUST give a request carrying the options the EZ-8.2 rejection on every adapter. This requirement owns that rule. +- **EZ-8.9** An optimized response MUST get the same response handling as any other Fastly response. That covers response header caps, normalization, the bodyless rules for HEAD, 204 and 304, the proxy marker (EZ-7), the decoding policy (EZ-5), and buffered or streamed mode. A streamed optimized body MUST stay lazy, so the first bytes reach the application before the transform output ends. +- **EZ-8.10** Image Optimizer is a narrowly scoped synchronous host-call exception, not an exact-deadline path. Preserve the original method-entry instant `t0`, selected absolute deadline `D`, and selected `BudgetSource`, with full duration precision. A deadline that has passed before sending follows EZ-3.3. After the host call returns at or after `D`, any success MUST be discarded and any provider failure MUST yield the attributed gateway timeout; the deadline's source wins. Do not round or extend `D`, impose a minimum outbound budget, or restart its budget after preparation. + + The synchronous blocking horizon is explicitly **UNKNOWN** until provider evidence. The 2026-10-24 scoped amendment permits native integration without an approved finite `H(K)`; it does not approve numerical backend B, backend policy, timers, slack or resource changes. Record preparation time, host-call entry/return times, total elapsed from `t0`, the actual backend settings and policy identity, the unknown horizon and selected source. An uninterruptible registration or inbound settlement that itself returns after `D` follows EZ-2.6 and MUST NOT enter Image Optimizer; report that exception's full elapsed time separately, not as a passing host-call-horizon result. + + A future qualification horizon `H(K)` MUST account for the entire synchronous call's applicable phase composition, millisecond timer quantization, retries and provider processing, not assume a sum of two SDK timers is sufficient. It requires provider evidence and separately approved numerical rule, maximum and domain before qualification replay, not a retrospective observed maximum or the sandbox execution limit. The proposed qualification assertions remain `terminal_observation - host_call_entry <= H(K) + 5 ms` and `elapsed_from_t0 <= (D - t0) + H(K) + 5 ms`, with both measured independently. Their 5 ms target is not additional logical budget or a hard scheduler guarantee. Equal per-call timeouts with different effective deadlines MUST retain EZ-2.2's eligible backend/timers; a horizon is not a new backend discriminator. Neither the universal `2 * effective_budget` promise nor another unproved finite bound may be claimed. Missing horizon proof blocks hosted/performance qualification, not the scoped native integration. + + Only the synchronous Image Optimizer call has this unknown blocking horizon. Once its head is available, ordinary buffered completion and streamed body reads retain `D` and EZ-2.6's measured deadline +5 ms qualification target. Typed body errors and fresh-readiness/subsequent-read safety remain required. Neither an unknown horizon nor observation delay permits a late successful response. The capability footnote MUST state **UNKNOWN** until provider evidence supplies an approved horizon, along with measurement anchors, preparation exceptions and pending hosted results. + +- **EZ-8.11** A service without Image Optimizer, or Viceroy, MUST produce a gateway-class 502. Its diagnostic naming the missing entitlement MUST be available to logs and as a typed reason the application can match, so the application can tell this case from other upstream failures without parsing provider text. The error's display text does not change (cross-cutting requirement 7). If Fastly refuses options that EdgeZero itself built, the result MUST be an internal error. +- **EZ-8.12** Attaching the options MUST NOT reduce origin connection reuse. Optimized and unoptimized requests to the same origin with the same transport settings MUST be eligible to share connections under EZ-2.1. The generic integration MUST retain the native optimizer path: map typed values directly to SDK options without another string/JSON/query representation, an extra origin request, a separate image-processing engine, or eager image-body collection in streamed mode. SDK-internal option serialization is not an EdgeZero-added representation. Preserve the selected region, transformation parameters and content-negotiation headers, subject to documented header normalization. Image options MUST NOT become an additional backend-identity discriminator. Performance parity requires the paired hosted comparison below; the implementation shape alone does not establish it. +- **EZ-8.13** On every send mode, and for every request whether or not it carries options, the URI an application reads back from a built `OutboundRequest` (`OutboundRequest::uri`) MUST be the request target EdgeZero hands to the provider. EdgeZero MUST NOT add, remove or reorder query parameters, and MUST NOT change application-set headers beyond its documented normalization (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:752-760`). `Host` stays derived from the URI authority unless the application sets a host authority override. EdgeZero therefore introduces no additional signature-invalidating changes after a request is built. This does not promise that Image Optimizer preserves the origin-facing query: it removes Image Optimizer parameters, and its preserve flag retains only other parameters (`fastly-sdk:src/image_optimizer.rs:678-680`). Signed requests MUST account for that provider transformation; a signature over a query that Image Optimizer removes is not supported by this invariant. Trusted Server strips its optimized origin query before signing. This is the single statement of that invariant in the document. +- **EZ-8.14** Capability levels. Fastly single send is BestEffort, because it needs a service entitlement and Viceroy cannot exercise it. It becomes Native when the EZ-Q1 record shows the hosted criterion below passing. Axum, Cloudflare and Spin are Unsupported on every send mode. Fastly begun requests, batches and fire-and-forget sends are Unsupported. +- **EZ-8.15** When EZ-24's published test double lands, it MUST accept and record attached options on valid single sends, so application tests can assert them. Shared method, body, and incompatible-option checks still apply, and universally Unsupported send modes still reject them under EZ-24.4. +- **EZ-8.16** Documentation MUST add the capability row with its footnote, a guide section on attaching options, the GET/HEAD and default-cache restrictions, EZ-8.10's explicitly **UNKNOWN** synchronous blocking horizon until provider evidence and its original-entry measurement anchors, the unchanged logical deadline and post-head body deadline, the entitlement prerequisite, and a warning that Image Optimizer also reads transformation parameters from the request query, with options taking precedence (`fastly-sdk:src/http/request.rs:3094-3096`). EdgeZero does not filter the query. State explicitly that the synchronous-call window can exceed twice the effective remaining budget, especially when an absolute deadline shortens a reusable backend's per-call timeout. Publish no finite horizon or hosted/performance qualification claim without the separate approval and evidence; retain +5 ms as a measured qualification target, not a hard scheduler guarantee. +- **EZ-8.17** An application MUST be able to read, from a built request, the Image Optimizer options attached to it, so its tests can check each call site. + +#### Acceptance criteria + +- Unit. Given options with quality 101, a zero width, offset 101, smart focus with offsets, or only an x offset, when the application builds them, then it gets a client-class error. Quality 0 and 100 succeed. +- Unit with injected time and a scripted host-call boundary. Budgets of 1 ns, 100 microseconds, 999,999 ns, 1 ms, and 1,000,001 ns keep their original deadlines and attributed timeout sources. Zero/expired budgets dispatch nothing. With the same 1 s per-call timeout and different absolute deadlines (including a 100-microsecond effective budget), plain and optimized requests retain the same eligible backend `K` and timers; the synchronous blocking horizon remains explicitly UNKNOWN until provider evidence. Cover each deadline-only policy boundary and maximum-domain case, preparation consuming almost all budget, and registration returning late. A successful or failed host return at `D` becomes the original attributed 504; no pre-dispatch refusal is counted as proof of a bounded host call. A timely optimized head followed by a permanently pending body retains its original deadline and measured +5 ms qualification target, without a hard scheduler guarantee or an uninterruptible body-read fallback. +- Unit. Given a request with options attached, the application reads back options equal to the ones it attached. The same holds for the next request of a followed redirect (EZ-9). A recording Fastly boundary observes `Follow` on the initial and next hop and invokes the same Image Optimizer mapping as `Send`; `Begin` and `Batch` still reject before body polling or provider work. Other adapters reject the options in all four modes before dispatch. +- Axum. Given a local origin, when a GET with options is sent, then the result is cross-cutting requirement 2's rejection with status 400 naming the option and `axum`, and the origin accepts no connection. Given a GET built from an inbound request whose body is an unread stream, with options, then the result is the same rejection and the inbound body is not read. Given a two-slot batch with options on slot 0, then slot 0 fails with the rejection, slot 1 returns 200, and the batch completes. +- workerd and Spin 4.1. A single send and a batch slot with options each return the rejection, and no fetch or WASI request is made. +- Viceroy 0.21.0. Given a GET with options to a reachable origin, then the result is a 502 with the EZ-8.11 reason and the origin receives no request. This shows the options reached the platform, not that Image Optimizer ran, and the hosted criterion is the proof. Given POST with options, GET with a body, or GET with cache bypass, then each gets the rejection, and EZ-2.13's statistics show no registration. Once EZ-11 lands, the same holds for GET with cache tagging. Given a two-slot batch with options on slot 0, then slot 0 fails and slot 1 reaches the origin. Given an optimized GET and a plain GET to one origin with the same transport settings, then EZ-2.13's statistics report one distinct backend configuration. +- Viceroy 0.21.0. Given a plain send whose URI has percent-encoded path bytes and a query, then the origin records a request target equal to `OutboundRequest::uri`, and application-set headers arrive unchanged except for documented normalization. +- Hosted Fastly, run under EZ-Q1 on Trusted Server's staging service with Image Optimizer enabled. Given a known JPEG and options for width 100 and WebP, when sent as GET, then the response is `image/webp` with a decoded width of 100 and the body streams. The same request as HEAD succeeds, and its status and headers are recorded in the capability footnote. Given an origin that accepts the connection and never answers, and a 1 s timeout, the request fails within the pre-approved EZ-8.10 bound. Repeat with a shortened effective absolute deadline but unchanged per-call timeout/backend and with representative deadline-only policy boundaries; record preparation, host-call and total original-entry timings, settings, `H(K)`, source and outcomes. Exercise the ordinary post-head body deadline separately. A horizon violation fails qualification; do not enlarge the rule to fit the run. An expired pre-dispatch case is a separate result, not a successful horizon probe. With an Image Optimizer parameter and an unrelated query parameter present, record the built request target and the origin target with query preservation on and off. The Image Optimizer parameter is not forwarded; the unrelated parameter is retained only when preservation is on. Documentation MUST distinguish these provider changes from EdgeZero's target invariant. + This hosted criterion also requires a paired native-path performance comparison. Compare the + candidate with both the frozen Trusted Server native path and an SDK-only control using identical + image bytes, region, options, negotiation headers, origin/cache settings and execution mode. + Separate verified cache hits and misses, GET/HEAD, and ordinary versus application-owned S3 + preflight cases. Record sample counts, randomized build order, first-byte/completion latency + distributions, throughput, guest allocations/copied bytes/peak buffering, backend registration + and transport host-call counts, origin receipts and connection identities where observable. + Preserve transformation output semantics and lazy streaming; no additional EdgeZero-origin + fetch or image-specific backend configuration is permitted. Approve sampling, instrumentation, + performance acceptance rules and treatment of noise or unavailable counters before replay, + independently from the horizon rule. Do not borrow unrelated bidder-latency tolerances, weaken + caps/deadlines for speed, or call missing/inconclusive comparisons performance parity. Viceroy's + unsupported result is not an optimization-performance sample. + +- Trusted Server end to end, on a Fastly staging service. An optimized asset path returns a transformed image, the HEAD preflight carries no options, and a missing S3 object returns the raw S3 status from the preflight. Trusted Server unit tests read back the options on the final GET and find them equal to the profile-table expectation (EZ-8.17). + +#### Out of scope + +- Cloudflare Image Resizing (`worker:src/request_init.rs:139-140`). A later Cloudflare-specific requirement can add it. +- The full SDK option set (`fastly-sdk:src/image_optimizer.rs:672-760`). +- Filtering or rewriting the query for Image Optimizer. +- The S3 HEAD preflight, which stays Trusted Server logic. + +#### Open questions + +- **Q8.1** Should the option set grow beyond Trusted Server's subset now? Trusted Server prefers no. Ship the subset it uses and leave room to add fields when a consumer needs them. +- **Q8.2** Removed. +- **Q8.3** What should HEAD with options do if Image Optimizer treats it differently from GET? Trusted Server prefers passing HEAD through unchanged and documenting the hosted result. + +### EZ-9. Redirect following with the application's target policy at every hop + +**Priority:** Blocking +**Adapters:** All four, with identical behavior. On Spin, plain-HTTP origins and origins outside a narrowed host list must be declared. +**Related:** EZ-1 (EZ-9.7 owns settling the body once per chain), EZ-2 (each new origin adds a backend configuration on Fastly), EZ-3 (EZ-3.3 owns failing a hop whose deadline has passed), EZ-6 and EZ-15 (begun requests, batches and, once EZ-15 lands, fire-and-forget sends never follow), EZ-4, EZ-5, EZ-7, EZ-8, EZ-11, EZ-12 and EZ-14 (their options carry to every hop), EZ-10 (a replayed body is re-checked against the upload cap), EZ-24 (each hop reaches a test client as an ordinary send) + +#### Need + +Trusted Server follows redirects in `proxy_with_redirects` (`ts:crates/trusted-server-core/src/proxy.rs:1276-1559`). The maintainer decided to delete that loop and move following into EdgeZero. EdgeZero must reproduce this behavior or improve it deliberately. + +- Limit. `MAX_REDIRECTS = 4` (`:1284`), and the loop runs `0..=MAX_REDIRECTS` (`:1292`), so at most 5 requests go out. At the limit it returns the last 3xx as the result, not an error (`:1470-1481`, test `:4305-4352`). +- Statuses. 301, 302, 303, 307 and 308 are followed (`:1415-1422`). Anything else is final. +- `Location`. A missing or empty value, or one that is not visible ASCII, returns the 3xx (`:1434-1446`). The value is parsed as an absolute URL and otherwise joined against the current URL (`:1484-1490`). A value that fails both is a 502 "invalid redirect". +- Methods. A 301 or 302 answering anything but GET, HEAD or POST is not followed (`:1452-1468`). 301, 302 and 303 become a bodyless GET, and HEAD stays HEAD (`:1534-1553`). 307 and 308 keep the method and replay the body (`:1380`). +- Body headers. Once the body is dropped, `Content-Type`, `Content-Length`, `Content-Encoding`, `Content-Language` and `Content-Location` are removed (`:76-82`, `:1372-1376`) and stay removed after a later 307 (test `:4223-4302`). +- Per-hop policy. The scheme must be http or https (`:1299-1304`). With `require_https` a non-HTTPS target is rejected (`:1305-1310`, redirect `:1492-1498`). A redirect to another scheme returns the 3xx (`:1499-1507`). A host is required (`:1312-1317`, `:1509-1516`). The domain allowlist rejects disallowed hosts (`:1319-1327`, `:1517-1526`). These rejections reach the client as 403 on the first-party proxy and the Prebid bundle (`ts:crates/trusted-server-core/src/error.rs:128-131`). GTM, GPT and testlight wrap them as integration errors, which are 502 (`ts:crates/trusted-server-core/src/integrations/google_tag_manager.rs:968-970`, `ts:crates/trusted-server-core/src/integrations/gpt.rs:282-284`, `ts:crates/trusted-server-core/src/integrations/testlight.rs:208-210`, status `error.rs:127`). +- Headers. The same curated set is rebuilt on every hop (`proxy.rs:1356-1371`). It contains no `Authorization` or `Cookie` today (`:67-73`). +- Budget. Each hop gets a fresh 15 s first-byte timer (`:1329-1344`, `ts:crates/trusted-server-core/src/platform/mod.rs:82`). There is no chain budget. +- Final response. `finalize_response` uses the final URL for its origin log line and pixel heuristics (`proxy.rs:747-761`, `:528-590`, `:633-642`). + +| Caller | Follows | Target policy | Body | Response | +| ----------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------- | --------------------- | -------------------- | +| First-party proxy (`proxy.rs:1577-1603`) | yes | `proxy.allowed_domains` (open when empty), HTTP allowed | none | buffered, rewritten | +| GTM (`ts:crates/trusted-server-core/src/integrations/google_tag_manager.rs:794-833`, send `:968-970`) | yes | HTTPS only, GTM domains | POST beacon, buffered | buffered, rewritten | +| GPT (`ts:crates/trusted-server-core/src/integrations/gpt.rs:142-153`, send `:273-287`) | no | none | none | buffered passthrough | +| Testlight (`ts:crates/trusted-server-core/src/integrations/testlight.rs:199-210`) | yes | open | POST JSON, buffered | passthrough | +| JS asset proxy (`ts:crates/trusted-server-core/src/integrations/js_asset_proxy.rs:279-306`) | no | none | none | streamed passthrough | +| Prebid bundle (`ts:crates/trusted-server-core/src/integrations/prebid.rs:1279-1286`) | yes | HTTPS only, `proxy.allowed_domains` | none | passthrough | + +After the cut, GPT and the JS asset proxy use a plain `HttpClient::send`. The other four ask EdgeZero to follow, with a target policy that applies Trusted Server's scheme, HTTPS and allowlist rules. Without EZ-9, first-party creative proxying, GTM, testlight and the Prebid bundle return raw 3xx responses to browsers and creatives, and GTM and Prebid script loading breaks whenever the upstream redirects. + +#### Gap at F + +EdgeZero never follows redirects, and its outbound spec calls that a security property. Following requires a fresh request, so the application's target policy runs again (`edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:1454-1470`). Target policy is an explicit non-goal (`:40-42`). Axum disables reqwest redirects (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:145-155`), Cloudflare sets manual redirect mode (`edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:426-429`), and Spin and Fastly do not follow (spec `:1466-1467`). An application cannot write a correct loop on F's API alone. `OutboundRequest` cannot be copied for replay, and `OutboundRequestParts` is non-exhaustive (`edgezero:crates/edgezero-core/src/outbound.rs:69-70`). A fragment in a raw-string target fails canonicalization (`:914-916`, `:1880-1884`), while parsing a `Uri` drops it silently (`http:src/uri/path.rs:28-30`). + +#### Requirements + +- **EZ-9.1** Following MUST be an explicit per-call choice. A plain `HttpClient::send`, a begun request (EZ-6), a batch, and, once EZ-15 lands, a fire-and-forget send MUST keep returning 3xx responses unfollowed. +- **EZ-9.2** A call that follows MUST supply a hop limit from 1 to 20, pending Q9.3, and a per-hop target policy. There MUST be no default that follows everything. A limit outside the range MUST be a client-class error when the call is configured. +- **EZ-9.3** Exactly 301, 302, 303, 307 and 308 are followable. Every other status is final. +- **EZ-9.4** `Location` handling. + - A missing or empty `Location` makes the 3xx final, reported as "no location". + - More than one `Location` field (pending Q9.1), a value that is not a URL reference, a resolved URL with user information, or a resolved http or https target EdgeZero cannot send for any other reason MUST fail the chain as a gateway-class 502 protocol failure, not a client error. A request the policy itself returns is not reclassified, so its own errors come back unchanged. + - A relative value resolves against the URI of the hop that returned it. A fragment never reaches the wire. + - A resolved target with a scheme other than http or https is not a resolution failure. It reaches the policy with no prepared next request. The policy can stop, reject, or return a request of its own. If the policy asks to follow a prepared request that does not exist, the chain fails as the 502 protocol failure above. + - EdgeZero MUST preserve evidence of the number and text eligibility of the original `Location` values it receives before its own filtering and normalization. Dropping an invalid value MUST NOT make a duplicate appear to be a singleton or turn an invalid original value into "no location". Validate that evidence at EZ-9.18's location-resolution stage, not ahead of its earlier stop rules. No extra raw header copy is required. Provider-hidden field boundaries remain subject to the existing outbound-header-fidelity limitation; the evidence MUST NOT certify information the provider did not expose. +- **EZ-9.5** The policy MUST run once per redirect, before any network work for that hop. It MUST see the redirect number, the status, the URI that answered, the resolved target, the 3xx headers, and whether the hop is cross-origin. It MUST be able to follow with the next request EdgeZero prepared, when one exists, or with a request it builds or modifies, to stop and return the 3xx as final, or to reject with the application's own error value, after which nothing more is sent. The initial request is not passed to the policy, so the application checks it before the call. The policy MAY re-sign or modify the next request subject to EZ-9.9 and EZ-9.10. It MUST NOT change EZ-9.11's handling of the response body from the hop that already answered. +- **EZ-9.6** Method and body rules, applied to the request of the hop that got the redirect. + + | Status | Method of that hop | Next method | Next body | + | -------- | ------------------------------------- | -------------------------------------------------------- | --------- | + | 301, 302 | GET, HEAD | same | empty | + | 301, 302 | POST | GET | empty | + | 301, 302 | any method other than GET, HEAD, POST | not followed, final, reported as "method not rewritable" | none | + | 303 | HEAD | HEAD | empty | + | 303 | any other | GET | empty | + | 307, 308 | any | same | replayed | + +- **EZ-9.7** Replay. A buffered body MUST be replayed byte for byte. A streamed upload cannot be replayed, so a 307 or 308 after one is final, reported as "body not replayable", and the policy is not consulted. When the method rule drops the body, EdgeZero MUST remove `Content-Type`, `Content-Length`, `Content-Encoding`, `Content-Language`, `Content-Location`, and the `Expect: 100-continue` expectation from its prepared next request. They stay absent from later prepared requests because the dropped body stays dropped, unless the trusted policy explicitly replaces that request or supplies new content and its metadata under EZ-9.9. Ordinary validation still prohibits content on GET/HEAD; EdgeZero MUST NOT generate a `100-continue` expectation without content (RFC 9110 section 10.1.1). A bodiless GET/HEAD whose inbound body is an unread stream MUST be settled once for the whole chain, before the first hop and within the request's budget, the way EZ-1 settles it for a single send. A later 307 or 308 then replays an empty body. This requirement owns the once-per-chain rule. +- **EZ-9.8** Cross-origin hops. When the next target differs from the previous hop in scheme, host or effective port, EdgeZero MUST remove `Authorization`, `Cookie`, and the host authority override (`OutboundRequest::host_authority_override`) from its prepared next request before calling the policy. On a same-origin hop they carry over. EdgeZero MUST NOT add `Referer`, `Origin` or any other header beyond its documented normalization. Other credential-bearing headers, such as `x-amz-*` signatures or custom API-key headers, are not removed, and the documentation MUST say the policy must remove them. A request the trusted policy returns is sent as built, subject to ordinary validation, normalization, and EZ-9.10's original absolute chain deadline. The policy may deliberately authorize or re-sign that request; EdgeZero MUST NOT strip credentials again afterward. Automatic preparation follows reqwest's cross-host rule for the two headers EdgeZero strips (`reqwest:src/redirect.rs:239-252`). +- **EZ-9.9** Every request option not changed by the method or cross-origin rules MUST carry unchanged into EdgeZero's prepared next request. That covers `timeout`, `deadline`, cache policy including tagging, the upload cap, every response cap, decoding settings, the proxy marker, Image Optimizer options, TLS verification mode, connect timeout and stream idle policy. The trusted policy MAY override these options. A freshly built policy request is a complete replacement: its own values, including constructor-default caps, apply without implicitly inheriting the preceding request's caps. To retain them the policy modifies the prepared request or copies them explicitly. Subsequent prepared requests inherit the effective options of the request that received the redirect. EZ-9.10's original absolute chain deadline remains binding in every case. +- **EZ-9.10** Budget. The effective request's per-call `timeout` MUST apply separately to each hop. Any absolute deadline on the initial request MUST remain an independent upper bound for the whole chain, including initial body resolution, policy execution, later dispatch, and consumption of the final response body. A modified or freshly built policy request MUST NOT remove, reset, or extend that original deadline. A policy-supplied deadline may shorten the next hop's budget; the earliest applicable deadline or timeout wins with its existing source attribution. After unsupported-option rejection, a hop whose effective budget has passed is not sent (EZ-3.3). Each hop uses the ordinary single-request dispatch path, so on Fastly a chain waits hop by hop. Its hops MUST be eligible to share connections exactly as single sends with the same origin and per-call timeout are (EZ-2.1, EZ-2.2). +- **EZ-9.11** Response disposition MUST be decided from the response head before declared-length payload-limit checks, decoding, buffering, or body reads. Response-header caps and body-independent header validation still apply to every hop. The body of a followed 3xx MUST NOT be delivered, MUST NOT be decoded, and MUST NOT fail the chain for any reason. That includes body-read errors, malformed or empty compression, and an oversized declared `Content-Length`. EdgeZero MAY discard it without reading. An optional reuse drain MUST have documented byte, memory, and work bounds within the request's caps, MUST NOT await more body bytes or delay the next dispatch waiting for the body, and MUST stop and discard on error or reaching a bound, without failing the chain. Every response returned as final, including a stopped or limit-reached 3xx, MUST receive its effective request's ordinary body handling: mode, decoding settings, caps, and idle policy. For a chain with no redirect, the result MUST be indistinguishable from a plain send of the same request, with the same status, headers, bytes, and limit error. +- **EZ-9.12** At most the limit's number of redirects MUST be followed, so at most limit plus one requests are sent. A followable response that would exceed the limit MUST be treated as final, reported as "limit reached" if its ordinary final-response processing succeeds. Its body can still fail under EZ-9.11; reaching the limit does not bypass decoding or caps. +- **EZ-9.13** A successful result MUST report the final response, the final request URI, the number of redirects followed, and, when the final status is followable, why it was not followed. The reasons are no location, method not rewritable, body not replayable, limit reached, and stopped by policy. The reason is present exactly when the final status is one of the five followable statuses. +- **EZ-9.14** A hop failure MUST return that hop's error unchanged, with which request failed (0 for the initial request). A policy rejection MUST return the application's value unchanged, with the redirect number. No error is turned into a 3xx response. Every hop MUST use exactly one ordinary single-request dispatch through the client on which following was requested, including an application-supplied client. A recording client MUST see each hop's effective method, URI, headers, body, and options; following MUST NOT bypass it through a separate transport. That shared dispatch contract MUST support the head-first response-policy decision of EZ-9.11 before body processing, for supplied implementations as well as adapters. This explicitly permits a hard change to the implementer-facing API, whose concrete shape remains EdgeZero's choice. Implementations migrate directly, without compatibility aliases or a legacy dispatch path. Clients need not implement redirects or receive hop numbering or chain-management responsibilities. Plain-send behavior remains unchanged. +- **EZ-9.15** Abandoning a following call MUST release the in-flight hop as a plain send would, and nothing more is sent. +- **EZ-9.16** EdgeZero SHOULD log each followed hop at debug level with status, scheme, host, port and path, and MUST NOT log query strings, which can carry identifiers. Trusted Server appends `ts-ec` to proxied URLs (`ts:crates/trusted-server-core/src/proxy.rs:1206-1242`). +- **EZ-9.17** Removed. +- **EZ-9.18** When several rules could end the chain at one redirect, the first in this order wins. The order follows Trusted Server's loop (`ts:crates/trusted-server-core/src/proxy.rs:1415-1526`). Followable status (EZ-9.3), then `Location` presence ("no location"), then the method rule ("method not rewritable"), then the replay rule ("body not replayable"), then the limit ("limit reached"), then `Location` resolution (502 protocol failure, except a non-HTTP scheme, which goes on to the policy), then the policy (stop or reject). +- **EZ-9.19** Documentation MUST add a guide section on following redirects with the method table, the credential rules and what they leave in place, the budget rules, and a security note. The note says the policy is the only target policy, and an open policy also follows redirects to private or link-local addresses. The guide MUST record that this supersedes the outbound spec's "read `Location` and issue a new request" guidance for applications that use it, while plain sends keep that rule (`edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:1469-1470`). The capability matrix MUST list redirect following as Native on all four adapters, with a note that Spin needs plain-HTTP origins and origins outside a narrowed host list declared (`edgezero:docs/guide/capabilities.md:362-377`). + +#### Acceptance criteria + +- Unit, with a scripted upstream. For every status from 300 to 308 and each of GET, HEAD, POST, PUT, PATCH, DELETE and OPTIONS with a 5-byte buffered body (empty for GET and HEAD), the number of requests and the second request's method, body and body headers match EZ-9.6. +- Unit. A POST with `Expect: 100-continue` answered by 302 and then 307 sends POST, GET, GET, with the body and expectation only on the first request and no body headers on the later two. A POST with an empty buffered body and `Content-Type` answered by 307 sends `Content-Type` on both requests. +- Unit. A streamed upload answered by 307 sends one request and reports "body not replayable". +- Unit. A GET whose body is an empty unread stream, answered by 307, sends two requests with empty bodies and reports no stop reason. +- Unit. With limit 4 and five 302 responses to GET, five requests are sent and the result is a 302 with "limit reached" and four redirects followed. A PUT answered by 301 at the limit reports "method not rewritable". A 302 with no `Location` answering a PUT at the limit reports "no location". Limits 0 and 21 are rejected. +- Unit. `Authorization` and `Cookie` reach a same-origin hop and do not reach a hop that changes host, port, or only the scheme. A host authority override reaches a same-origin hop only. An `x-amz-date` header reaches both. +- Unit. Relative, `../x`, `//b.example/x` and absolute `Location` values follow. A fragment never reaches the wire. A missing or empty value reports "no location". Two `Location` fields, user information, or an unusable http target give a 502 protocol failure. +- Unit, through actual response normalization and an application-supplied head-first client. A 302 containing `Location: /next` and a second `Location` with invalid UTF-8 still fails as a 502 protocol error and sends no next hop. A single invalid value also fails as 502 rather than reporting "no location". A method-not-rewritable or limit-reached control retains EZ-9.18's earlier stop reason. The public response retains the normal header-filtering behavior; immutable evidence, not an unfiltered header copy, enforces the redirect rule. +- Unit. A policy rejection returns the application's value with redirect number 1 and no second request. A policy stop returns the 3xx with "stopped by policy". A modified next request is sent exactly as returned. A `javascript:` target reaches the policy with no prepared request. A policy that asks to follow a prepared request there gets a 502 protocol failure. +- Unit, through an application-supplied recording client implementing the shared head-first dispatch contract. A followed 302 whose body errors, one with `Content-Encoding: br` and an empty body, one declaring a 10 MiB `Content-Length` under a 1 MiB cap, and one whose body stays permanently pending each lead to the final 200, without waiting for intermediate body data. The client records every hop exactly once. Returning the same 302 as final by policy stop or limit applies ordinary decoding and caps and yields the corresponding error instead of hiding it. +- Unit. A deadline that passes after the first hop fails the second hop as a gateway timeout attributed to the deadline, and the second hop is not sent. Prepared hops retain the preceding request's per-call timeout unless the policy changes it. A fresh policy request with no deadline or a later deadline still cannot send or consume its final streamed body past the original absolute chain deadline; an earlier policy deadline shortens the budget. A synchronous policy cannot be preempted. If it returns an action after the original deadline expired, no next hop is sent and no late final success is returned; unsupported-option rejection still precedes budget rejection for a selected next request. If it rejects instead, its original application error remains unchanged under EZ-9.14. +- Unit. A policy modifying the prepared request retains every unchanged cap and option. A freshly built policy request uses its own caps and options, including constructor defaults, while still observing the original absolute chain deadline. A policy can deliberately re-sign a new origin's request without EdgeZero stripping its credentials afterward. +- Unit. Dropping a following call while hop 1 is in flight sends no hop 2. +- Unit. With logging captured at trace level, a two-hop chain whose URIs carry `?ts-ec=abc` logs neither `ts-ec` nor `abc`. +- Unit. A following call made on an application-supplied recording client records one ordinary send per hop, with each hop's method, URI, headers and body. +- Axum, workerd, Spin 4.1 and Viceroy 0.21.0. For each adapter, with a 200 and no redirect under a 10-byte buffered cap, bodies of 10 and 11 bytes and a declared `Content-Length: 11` give the same status, headers, bytes or limit error from a plain send and from a following call, for identity, gzip and passthrough encodings. Repeat the followed and final 302 body cases from the supplied-client criterion on each adapter, and verify streamed final-response parity; when EZ-14 is present, include its idle-bounded final-body policy. +- Axum and Viceroy 0.21.0, with two local origins where A answers 302 to B and B answers 200. A plain send returns 302. A following call returns 200 with B as the final URI and one redirect followed. +- Viceroy 0.21.0. A two-hop chain to two loopback origins with one per-call timeout leaves EZ-2.13's statistics showing the same two backend configurations as two single sends to those origins with that timeout. +- Spin 4.1. A redirect to a plain-HTTP host that is not declared fails at that hop with Spin's denial, which F maps to 400 (`edgezero:crates/edgezero-adapter-spin/src/outbound.rs:1358-1364`). That Spin 4.1 reports this case as `HttpRequestDenied` is **Unverified**, and this criterion records what it reports. +- Trusted Server end to end. Port every redirect test in `ts:crates/trusted-server-core/src/proxy.rs:3978-4515`, the allowlist tests at `:6114-6258`, and the initial-target policy tests at `:6259-6325` to the following call, with identical recorded methods, URIs, bodies, header names and request counts. Use EZ-24 if it has landed and Trusted Server's own recording client otherwise (EZ-9.14). On the Axum dev server, a signed first-party proxy URL whose origin 302s to an allowed host returns the final 200, and one that 302s to a disallowed host returns 403. + +#### Out of scope + +- Loop detection beyond the hop limit (`edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:43-45`) and blocking private addresses (`:41-42`). Both stay application policy. +- Buffering a streamed upload so it can be replayed. +- An asynchronous policy. +- Following inside begun requests, batches or fire-and-forget sends. + +#### Open questions + +- **Q9.1** Should more than one `Location` field fail or use the first? Trusted Server prefers failing. `Location` is a singleton field, and a well-formed upstream never sends two. Trusted Server uses the first value today (`ts:crates/trusted-server-core/src/proxy.rs:1434-1438`). +- **Q9.2** Should EdgeZero strip credentials again when the policy's modified request changes origin? Trusted Server prefers no. The policy is application code, and stripping again would stop it from re-signing for a new origin. +- **Q9.3** Is 20 the right maximum limit? Trusted Server prefers 20. It bounds backend configurations per chain on Fastly, and Trusted Server uses 4. + +## Follow-up requirements + +### EZ-10. 413 Payload Too Large for request-body overflow + +**Priority:** Follow-up +**Adapters:** Fastly, Axum, Cloudflare, Spin. The status and error category must be the same everywhere. Adapters differ only in what the origin may already have received when a streamed upload overflows (EZ-10.6). +**Related:** EZ-1, whose GET/HEAD body rejection stays 400 and whose EZ-1.13 owns the rule on unread bytes. EZ-16, whose provider detail shares EZ-10's log-only treatment (cross-cutting requirement 7). EZ-17, which must report an upload that overflowed after the request head left as sent. EZ-25, which owns the scaffold and guide fixes. + +#### Need + +TS answers request-body overflow with 413 on every endpoint that reads a body. + +| TS endpoint | Cap | Behavior today | Source | +| ------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `POST /auction` | 256 KiB | Early `Content-Length` check, then a post-read check. 413 `text/plain` | `ts:crates/trusted-server-core/src/auction/endpoints.rs:46`, `:126-159`. Test `:1771-1791` | +| First-party sign and rebuild | 64 KiB each | 413 | `ts:crates/trusted-server-core/src/proxy.rs:40-41`, `:1720-1721`, `:1909-1910`. Tests `:6343`, `:6389` | +| Verify signature, rotate key, deactivate key | 4 KiB | 413 | `ts:crates/trusted-server-core/src/request_signing/endpoints.rs:101-102`, `:116-117`, `:306-307`, `:425-426`. Tests `:995`, `:1034`, `:1073` | +| Cache purge | 4 KiB | 413 | `ts:crates/trusted-server-core/src/cache_purge.rs:30`, `:141-142` | +| Batch sync | 2 MiB | Early `Content-Length` check, then a post-read check. 413 JSON `body_too_large` | `ts:crates/trusted-server-core/src/ec/batch_sync.rs:143-158`, `:188-194` | +| Integration POST proxies (datadome, didomi, permutive, sourcepoint, lockr, testlight) | 256 KiB | 413 | `ts:crates/trusted-server-core/src/integrations/mod.rs:167`, `:174-222` | +| GTM beacons | 64 KiB default | Early `Content-Length` check and a read cap. 413 | `ts:crates/trusted-server-core/src/integrations/google_tag_manager.rs:186-188`, `:290-292`, `:689-728`. Read-cap test `:2763-2817` | + +TS's request-too-large error maps to 413 (`ts:crates/trusted-server-core/src/error.rs:48-50`, `:129`, `ts:crates/trusted-server-core/src/http_util.rs:465-484`). After the cut TS reads every body through EdgeZero's readers, and each returns 400 on overflow. Without EZ-10: + +- Every endpoint above answers an undeclared or understated overflow with 400 instead of 413. Tests that go through TS's early `Content-Length` checks keep passing, but the read-cap tests fail. +- After `RequestContext::json_within`, TS cannot tell overflow from malformed JSON, because both are 400 (`edgezero:crates/edgezero-core/src/context.rs:401-408`). +- A route that uses EdgeZero's extractors cannot fix the status with `App::set_error_response_renderer`, because EdgeZero reapplies the error's own status after the renderer runs (`edgezero:crates/edgezero-core/src/app.rs:334-344`, `edgezero:docs/guide/handlers.md:397-399`). +- A client POST forwarded to the publisher origin over TS's `max_request_body_bytes` cap fails with 400, where 413 is the accurate status. + +#### Gap at F + +- Every inbound reader returns 400 on overflow. That covers `Body::into_bytes_bounded` (`edgezero:crates/edgezero-core/src/body.rs:81-111`), `RequestContext::body_bytes` (`context.rs:202-258`, `:621-626`, `:663-668`), `json_within` and `form_within` (`:345-357`, `:396-408`), and the extractors built on them (`edgezero:crates/edgezero-core/src/extractor.rs:33-44`, `:69-84`, `:86-102`, `:430-439`, `:466`, `:507-516`). EdgeZero's inbound design states 400 for overflow and gives no reason against 413 (`edgezero:docs/superpowers/specs/2026-08-22-inbound-body-design.md:663-664`). +- No declared length is checked early. A 10 MiB declared upload against a 256 KiB cap is read up to the cap before it is refused. +- `max_request_body_bytes` overflow returns 400 on every adapter. See Axum `edgezero:crates/edgezero-adapter-axum/src/outbound.rs:229-235` and `:253-262`, Cloudflare `edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:671-677` and `:871-878`, Fastly `edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:1137-1146` and `:1269-1277`, and Spin `edgezero:crates/edgezero-adapter-spin/src/outbound.rs:582-600`. On Fastly a streamed overflow is found after the request head has been sent (`:1180-1182`), and the upload is abandoned (`outbound-http-design.md:5713-5716`). On Spin even a buffered body over the cap is found only after the request has been issued (`outbound.rs:362-377`, `:565-567`), so Spin does not meet EZ-10.6 at F. +- EdgeZero has no error category with status 413 (`edgezero:crates/edgezero-core/src/error.rs:452-474`). +- The scaffold and demo fallback policies answer an over-cap unmatched request with 400 (`edgezero:crates/edgezero-cli/src/templates/core/src/lib.rs.hbs:104-107`, `edgezero:examples/app-demo/crates/app-demo-core/src/lib.rs:196-199`). EdgeZero's own unit test uses 413 (`edgezero:crates/edgezero-core/src/ingress.rs:980-999`). + +#### Requirements + +- **EZ-10.1** EdgeZero MUST give request-body overflow its own error category with status 413. Applications MUST be able to tell it apart from malformed payloads (400), the inbound read deadline (408) and every other error without matching message text. Its client-visible body MUST be stable, MUST NOT carry provider or internal detail, and MUST NOT depend on which cap fired. +- **EZ-10.2** Every inbound body cap EdgeZero enforces MUST report overflow in that category. That covers `RequestContext::body_bytes`, `json_within`, `form_within`, the `Json`, `ValidatedJson`, `ValidatedJsonWithin`, `Form`, `ValidatedForm` and `ValidatedFormWithin` extractors, and `Body::into_bytes_bounded`. It MUST hold for a declared overflow (`Content-Length` above the cap) and when host-delivered body bytes exceed the cap, including chunked HTTP/1.1, HTTP/2 or HTTP/3 without a length, or a delivered stream larger than its declared length. Host-level framing rejection need not become 413. In HTTP/1, bytes beyond the host's declared body boundary are not additional body bytes EdgeZero can inspect or charge to that request. Malformed JSON or form data under the cap stays 400. +- **EZ-10.3** The existing sticky read semantics of the `RequestContext` readers MUST NOT change. Once a read overflows while reading the body, every later read of that body returns the same 413 and reads nothing more. A stricter cap checked against a body already read returns 413 and leaves the body usable at a larger cap. +- **EZ-10.4** When the admitted request's single normalized `Content-Length` exceeds a reader's cap, the reader SHOULD fail with 413 without reading any body byte. An inbound read deadline that has already passed still wins, with 408. Such an early failure is not a read, so it MUST NOT stop a later read at a larger cap from succeeding, and EZ-10.3's sticky rule does not apply to it. +- **EZ-10.5** Overflow of an outbound request's `max_request_body_bytes` MUST report the same 413 category on all four adapters, for buffered and streamed bodies. An upstream that answers 413 MUST stay an ordinary response with status 413, never this error. +- **EZ-10.6** These rules hold for what the origin may see: + - A buffered body over the cap MUST fail before anything is sent, on every adapter. + - On Axum and Cloudflare a streamed upload over the cap MUST fail before the origin is contacted. + - On Fastly and Spin the origin may already have the request head and part of the body. The upload MUST then be aborted so the origin never receives a complete, well-formed request with a truncated body. EZ-17 reports such a request as sent. +- **EZ-10.7** Applications MUST be able to learn whether an overflow came from an inbound read or an outbound upload without parsing message text. That detail MUST NOT reach the wire, and the text and `Display` of existing errors MUST NOT change (cross-cutting requirement 7). Where EZ-16 has landed, the detail SHOULD be read the same way as EZ-16's provider detail. +- **EZ-10.8** An application's `App::set_error_response_renderer` MUST receive the overflow error, and the response status after rendering MUST be 413. +- **EZ-10.9** The scaffold and demo `AdmissionDecision::ReadBodyBeforeFallback` policies MUST answer an over-cap fallback request with 413. +- **EZ-10.10** An EdgeZero-generated error response for its own typed body-cap overflow MUST NOT carry `Retry-After`, because that configured cap does not represent a temporary failure. This prohibition applies after custom error rendering and synthesized-header composition, includes admitted source errors, and takes precedence over EZ-19.1/EZ-19.4. It is based on the typed overflow cause, not status 413 alone. This restriction MUST NOT strip or reject a surviving `Retry-After` on an ordinary upstream 413 response: RFC 9110 section 15.5.14 permits it for a temporary upstream condition. It MUST NOT alter the effective config-503 retry policy. +- **EZ-10.11** Unread request bytes after a 413 on connections EdgeZero owns follow EZ-1.13. +- **EZ-10.12** The handler and capability guides MUST give the statuses for inbound bodies, which are 413 for overflow, 400 for malformed payloads, and 408 for the read deadline. They MUST also state where Fastly and Spin streamed overflow happens relative to sending. + +#### Acceptance criteria + +1. Environment is unit. Given a 9-byte body with no declared length, when it is read with a cap of 8 through `body_bytes`, `json_within`, `form_within`, an extractor, or `Body::into_bytes_bounded`, then the result is the 413 category and the response status is 413. For the `RequestContext` readers, a second read at any cap returns the same 413 without reading. Malformed JSON under the cap stays 400. +2. Environment is unit. Given a request declaring `Content-Length: 10` with a 10-byte body, when it is read with a cap of 5, then the result is 413. With the inbound read deadline already passed, the result is 408. Record whether any body byte was read before the 413 (EZ-10.4 is SHOULD). Where none was, a later read of that request with a cap of 10 succeeds. +3. Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A chunked POST of 2 KiB to a route capped at 1 KiB gets 413. On Axum, an HTTP/1 request pipelined after it is either refused by a connection close or answered from the correct offset. +4. Environment is hosted Fastly, Trusted Server staging. An HTTP/2 POST with no `content-length` over the cap gets 413. +5. Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A forwarded 9-byte buffered body with `max_request_body_bytes(8)` gets 413, and the origin records no request. +6. Environment is Axum and workerd. A streamed forwarded upload over the cap gets 413, and the origin records no request. On Viceroy 0.21.0 and Spin 4.1, the origin never records a complete request. +7. Environment is Axum. An extractor route with a custom error renderer answers an oversized body with 413, and the response has no `Retry-After`. +8. Environment is Axum, using the generated scaffold and the demo app. An over-cap request to an unmatched route gets 413. +9. Trusted Server end to end. With TS reading bodies through EdgeZero's readers and mapping the overflow category to its request-too-large error, the read-cap 413 tests pass unchanged. They are `ts:crates/trusted-server-core/src/auction/endpoints.rs:1771-1791`, `proxy.rs:6343` and `:6389`, `request_signing/endpoints.rs:995`, `:1034` and `:1073`, and `integrations/google_tag_manager.rs:2763-2817`. A new wire-level case gets 413 for an oversized chunked `POST /auction` with no `Content-Length`. A controlled-stream unit case for batch sync delivers more bytes than both its declared length and the cap and gets 413. Wire-level understated-length cases instead record host framing rejection or the body actually exposed to EdgeZero; they do not require 413 when the host never delivers an over-cap body. + +10. Unit, each adapter's response handling. An upstream 413 with `Retry-After: 120` remains an ordinary response and retains that header; an EdgeZero-generated cap-overflow error has no `Retry-After`. + +#### Out of scope + +- Changing default caps. +- Changing how malformed or duplicate `Content-Length` is handled. EdgeZero rejects those with 400 before admission (`ingress.rs:892-921`). +- Changing how an upstream's own 413 response is surfaced. +- Making a streamed upload overflow invisible to the origin on Fastly and Spin. + +#### Open questions + +1. Should an outbound upload overflow be 413, or a server-error status? TS prefers 413. The documented use of the upload cap is bounding a forwarded client upload (`edgezero:docs/guide/proxying.md:32-35`), where the client is at fault. An application that builds its own oversized body can tell the two apart through EZ-10.7 and remap. +2. Should readers reject a declared oversized length before reading (EZ-10.4 is SHOULD)? TS prefers yes. It saves reading up to the cap, and with it TS can drop its own early checks in the auction, batch-sync and GTM handlers. + +#### Interim for Trusted Server + +Until EZ-10 lands, TS maps the 400 from `RequestContext::body_bytes` and `Body::into_bytes_bounded` to its own 413. Today those two calls return 400 only for overflow, because read failures pass through with their own categories (`body.rs:99`, `context.rs:662`). TS reads JSON with `body_bytes` and then parses it, so malformed input stays a separate 400. TS keeps its early `Content-Length` checks, which are header checks, not transport plumbing. + +### EZ-11. Cache tagging with surrogate keys + +**Priority:** Follow-up +**Adapters:** Fastly must honor tags. Axum and Spin accept them with no effect, since they have no intermediary cache. Cloudflare is Unsupported. +**Related:** EZ-8 (EZ-8.6 rejects tags combined with Image Optimizer), EZ-9 (tags carry to redirect hops), EZ-6 and EZ-15 (EZ-11.4 owns the rule that tags behave the same on begun requests and, once EZ-15 lands, fire-and-forget sends) + +#### Need + +Trusted Server can let publisher origin HTML enter Fastly's cache and tag it for purge. `apply_origin_cache_intent` (`ts:crates/trusted-server-core/src/publisher.rs:4356-4394`) bypasses for non-shareable documents and tags shareable ones with `ts-template ts-template-readerurl-` (`:4384-4391`, keys `ts:crates/trusted-server-core/src/platform/template_cache.rs:46`, `:234-239`). Both origin send sites use it (`publisher.rs:4852-4872`, `:5160-5177`). The cache intent is one enum on purpose, because on Fastly a surrogate key overrides pass, so "bypass and tag" would silently mean "do not bypass" (`ts:crates/trusted-server-core/src/platform/http.rs:10-29`). On an invalid key the Fastly adapter falls back to bypass rather than store an object no purge can reach (`ts:crates/trusted-server-adapter-fastly/src/platform.rs:452-473`). It never sets a TTL (`:448-451`). + +After the cut Trusted Server sets a cache policy of "default plus these tags" on the origin `OutboundRequest`, and falls back to `OutboundCachePolicy::Bypass` if the tags are invalid. Without EZ-11 it can only pick `Bypass` or the default, and the default would cache pages that no purge can remove. + +#### Gap at F + +`OutboundCachePolicy` has only `Bypass` and `PlatformDefault` (`edgezero:crates/edgezero-core/src/outbound.rs:109-115`). Fastly maps `Bypass` to cache pass (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:735-750`) and Cloudflare maps it to no-store (`edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:419-436`). Axum and Spin have no intermediary cache (spec `edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:1045-1051`). The Fastly SDK supports tagging. A surrogate key overrides pass and extends backend `Surrogate-Key` headers (`fastly-sdk:src/http/request.rs:2481-2501`). Keys are printable ASCII without spaces, and invalid keys are silently ignored (`:2486-2490`). A cache override sets TTL and stale-while-revalidate only when they are provided (`fastly-sdk:src/http/request/handle.rs:1834-1845`), so a key alone sets neither. Whether the override's documented effect on cache directive headers (`:939-942`) changes origin freshness for a key-only override is **Unverified**. The tag reaches the host on handle-level sends too (`:943-954`, `:1815-1866`). Workers fetch has no cache-tag field in worker 0.8.5 (`worker:src/request_init.rs:116-166`). + +#### Requirements + +- **EZ-11.1** An application MUST be able to ask, per request, for the default cache behavior plus one or more purge tags. Asking for bypass and tags together MUST be impossible to express or MUST be rejected before sending. +- **EZ-11.2** Tags MUST be validated when the application sets them. There is at least one tag, each tag is non-empty, and each contains only bytes `0x21` to `0x7E`. Invalid tags MUST be a client-class error at that point and never silently ignored. Duplicates collapse while keeping order. +- **EZ-11.3** Tagging MUST behave exactly like the default policy plus tags on any object the intermediary cache stores for the exchange. It MUST NOT force caching, set a TTL, or add any origin-visible request header. +- **EZ-11.4** On Fastly, tags MUST take effect on every send mode, including single send, begun request, batch slot, redirect hop and, once EZ-15 lands, fire-and-forget send, and a purge by any one tag MUST remove the stored object. Capability Native. This requirement owns the same-semantics rule for tagging across send modes. +- **EZ-11.5** On Axum and Spin, tags MUST be accepted and have no effect. Capability Native, because no untagged object can be stored. +- **EZ-11.6** On Cloudflare, tags MUST get cross-cutting requirement 2's rejection, first in preflight and before any fetch, naming the option and the adapter. Capability Unsupported, because degrading to the default would store untagged objects. +- **EZ-11.7** Removed. +- **EZ-11.8** Tags MUST carry to every redirect hop (EZ-9.9). +- **EZ-11.9** Documentation MUST add the capability row, describe the three cache intents, and state that tagging never forces caching. + +#### Acceptance criteria + +- Unit. No tags, an empty tag, `a b`, a non-ASCII tag and `0x7F` are client-class errors. `ts-template` with a reader-URL tag is accepted. `a`, `a` collapses to `a`. +- Viceroy 0.21.0. A tagged single send and a tagged two-slot batch complete without error. +- workerd. A tagged request returns the rejection, and no fetch is made. +- Axum. A tagged request succeeds, and the origin receives no `surrogate-key` or `cache-control` header added by EdgeZero. +- Hosted Fastly. Given a cacheable origin path that counts the requests it receives, two fetches tagged `probe-key` leave the origin count at one. After a purge of `probe-key`, a third fetch raises it to two. The same holds when the fetches are begun requests and when they are batch slots. +- Hosted Fastly. Compare default and tagged single sends, begun requests, and batch slots against separate origin paths returning `Cache-Control: no-store` and `Cache-Control: private`. Neither policy may cache those responses: every request reaches the origin. The record confirms that a key-only override preserves these non-cacheable directives rather than forcing storage. +- Trusted Server end to end. With readthrough enabled on a Fastly staging service, purging `ts-template` turns a cached page into a miss, and the origin sees the request again. Trusted Server unit tests assert the tagged policy at both origin send sites, using EZ-24 if it has landed or Trusted Server's own double otherwise. + +#### Out of scope + +- TTL overrides and forced caching. +- A purge API. +- Cloudflare cache tags. + +#### Open questions + +- **Q11.1** Which length limits apply to a tag and to the whole set? Trusted Server prefers 1024 bytes per tag and 16 KiB in total, rejected when set. These are **Unverified** Fastly limits. A hosted probe with an oversized key, or Fastly's surrogate key documentation, would confirm them. The SDK documents only the byte set (`fastly-sdk:src/http/request.rs:2486-2490`). +- **Q11.2** Should Cloudflare support tags through a raw `cf.cacheTags` property? Trusted Server prefers not now. worker 0.8.5 has no typed field, and whether the runtime honors it on subrequests is **Unverified**. + +#### Interim for Trusted Server + +Until EZ-11 lands, Trusted Server rejects `origin_readthrough_enabled = true` at config validation. Readthrough is off by default (`ts:crates/trusted-server-core/src/creative_opportunities.rs:419-426`), and no local operator configuration enables it. Document fetches keep today's bypass-or-default behavior. + +### EZ-12. TLS certificate verification mode + +**Priority:** Follow-up +**Adapters:** Fastly must honor the mode. It is Unsupported on HTTPS for Axum, Cloudflare and Spin. +**Related:** EZ-2 (EZ-2.1 requires matching TLS settings, and the mode is one of them), EZ-9 (the mode carries to redirect hops) + +#### Need + +`proxy.certificate_check` defaults to `true` and is documented as the switch "for local development with self-signed certificates" (`ts:crates/trusted-server-core/src/settings.rs:1762-1790`). It reaches four sites. Those are asset routes (`ts:crates/trusted-server-core/src/proxy.rs:1136`), every redirect hop (`:1337`), the publisher origin (`ts:crates/trusted-server-core/src/publisher.rs:4516`) and pull sync (`ts:crates/trusted-server-core/src/ec/pull_sync.rs:237`). Integrations and auction providers always verify (`ts:crates/trusted-server-core/src/integrations/mod.rs:53-66`, `ts:crates/trusted-server-core/src/auction/plan.rs:396`). On Fastly, `false` omits the certificate hostname check and keeps TLS, and keeps SNI for hostnames only (`ts:crates/trusted-server-adapter-fastly/src/backend.rs:243-254`, SNI `:245-247`). Fastly logs an INSECURE warning at startup (`ts:crates/trusted-server-adapter-fastly/src/app.rs:240-246`). Axum, Cloudflare and Spin ignore the setting (`ts:crates/trusted-server-adapter-axum/src/platform.rs:290-301`). + +Fastly still validates the chain without the hostname check. That validation "is not optional" (`fastly-sdk:src/backend/builder.rs:176-190`, `:219-229`). So the setting's real effect is that an origin is accepted when its certificate chains to a public root but names a different host. The common case is an origin reached by IP or an internal name. Viceroy is stricter. With no certificate host given it checks the SNI hostname (`viceroy:src/wiggle_abi/req_impl.rs:535-569`), or the backend URI host when there is no SNI either (`viceroy:src/upstream.rs:188-192`), so locally the setting changes nothing. + +After the cut Trusted Server asks for this mode at the four sites when `certificate_check = false`. Without EZ-12, such a Fastly deployment fails every request to an origin whose certificate names another host with a 502 (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:935-944`). No local operator configuration sets `false`. + +#### Gap at F + +Every HTTPS request verifies the certificate against the URI host (`edgezero:crates/edgezero-core/src/outbound.rs:869-873`, Fastly backend `edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:349-357`). For an IP-literal URI F sends no SNI (`edgezero:crates/edgezero-core/src/outbound.rs:1189-1192`). There is no per-request choice. reqwest 0.13.4 with rustls allows skipping only the hostname check when the client also uses an explicit root set instead of platform roots, and fails to build otherwise (`reqwest:src/async_impl/client.rs:734-747`). Workers fetch has no TLS control (`worker:src/request_init.rs:12-27`). WASI HTTP request options have only three timeouts (`spin-sdk:wit/deps/http@0.3.0/types.wit:384-416`). + +#### Requirements + +- **EZ-12.1** An application MUST be able to choose, per request, between full verification (the default) and chain-only verification. Chain-only still validates chain and expiry against the platform's trust roots and skips only the hostname match. A mode MUST NOT disable chain validation. +- **EZ-12.2** On `http` URIs the mode MUST have no effect on any adapter. SNI MUST be unchanged by the mode. +- **EZ-12.3** On Fastly, a chain-only request MUST succeed against a publicly trusted certificate for another name, and MUST fail with a gateway-class 502 against a self-signed, expired or otherwise invalid certificate. Capability Native. +- **EZ-12.4** On Fastly the mode is one of the TLS settings that EZ-2.1 requires to match for connection sharing. EZ-2.1 owns that rule. +- **EZ-12.5** On Fastly, EdgeZero MUST log a warning naming the host the first time a chain-only connection setup is used in a sandbox. +- **EZ-12.6** On Axum, Cloudflare and Spin, chain-only on an `https` URI MUST get cross-cutting requirement 2's rejection, first in preflight and before sending. Capability Unsupported. +- **EZ-12.7** The mode MUST carry to every redirect hop (EZ-9.9). +- **EZ-12.8** Documentation MUST add the capability row, state exactly what chain-only checks, and warn that any publicly trusted certificate can then impersonate the origin. + +#### Acceptance criteria + +- Unit. Chain-only on an `http` URI sends normally on every adapter. +- Axum, workerd and Spin 4.1. Chain-only on `https` returns the rejection and nothing is sent. +- Hosted Fastly. An origin reached by IP whose certificate is valid for a hostname fails with full verification (502) and succeeds with chain-only. An origin with a self-signed certificate fails in both modes. Given an origin that logs the connection each request arrives on, a full and a chain-only request to it in one sandbox arrive on different connections. Viceroy cannot show the mode difference, because it checks the SNI hostname or the URI host whenever no certificate host is given. +- Trusted Server end to end. With `certificate_check = false` on a Fastly staging service whose origin is an IP-literal HTTPS URL, publisher pages load. + +#### Out of scope + +- Disabling chain validation. +- Custom trust roots. +- Chain-only on Axum. + +#### Open questions + +- **Q12.1** Should Axum support chain-only? Trusted Server prefers not now. reqwest needs an explicit root set for it (`reqwest:src/async_impl/client.rs:734-741`), which means a new bundled root dependency or new configuration, and Trusted Server's Axum adapter has always verified in full. +- **Q12.2** Should EdgeZero add custom trust roots for self-signed development origins? Trusted Server prefers a separate follow-up. Fastly supports a CA certificate per backend (`fastly-sdk:src/backend/builder.rs:231-241`), and that is the safe answer to what `certificate_check = false` was meant to do. + +#### Interim for Trusted Server + +Until EZ-12 lands, Trusted Server rejects `certificate_check = false` at config validation on every adapter, because F always verifies the hostname and Axum, Cloudflare and Spin never honored the setting. Trusted Server also corrects the setting's doc comment, which claims self-signed support (`ts:crates/trusted-server-core/src/settings.rs:1765-1767`). + +### EZ-13. Spin host authority override + +**Priority:** Follow-up (Trusted Server recommends closing it as documentation and evidence) +**Adapters:** Spin +**Related:** EZ-9 (a cross-origin redirect hop drops the override), EZ-25 (documentation) + +#### Need + +`publisher.origin_host_header_override` sets the `Host` sent while connecting to `origin_url` (`ts:crates/trusted-server-core/src/settings.rs:44-47`, `:209-216`). The publisher backend passes it as the host override (`ts:crates/trusted-server-core/src/publisher.rs:4510-4520`). After the cut it becomes `OutboundRequest::host_authority_override` on the publisher fetch. Axum and Fastly support that natively, and Cloudflare is BestEffort (`edgezero:docs/guide/capabilities.md:236`). + +On Spin the setting has never worked. Trusted Server's Spin client strips `host` as forbidden by WASI HTTP (`ts:crates/trusted-server-adapter-spin/src/platform.rs:410-428`, `:482`), so the override is dropped and the URI authority is sent. EdgeZero rejects the override on Spin instead, so with the setting present every publisher request on Spin would fail. One deployment's local configuration sets the override, and that deployment runs on Fastly. + +#### Gap at F + +Spin rejects any override with a 400 before body polling (`edgezero:crates/edgezero-adapter-spin/src/outbound.rs:1433-1441`, batches `:1417-1431`). It sets one authority from the URI (`:509-535`). The spec and the matrix record Spin as Unsupported (`edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:1056-1060`, `edgezero:docs/guide/capabilities.md:236`, `:255-258`). WASI HTTP 0.3 in spin-sdk 7.0.0 offers no way to send a `Host` other than the connection authority. + +- A request has one authority, set as "the authority of the Request's target URI" (`spin-sdk:wit/deps/http@0.3.0/types.wit:336-344`). +- Request options carry only three timeouts. There is no connect-target, proxy or address override (`:384-416`). +- Whether a `host` field may be set is implementation-defined. Fields can be forbidden (`:112-114`, `:209-210`), and the host must reject requests with headers "not permitted to be sent" (`:300-304`). Trusted Server's comment says WASI HTTP 0.2 forbids `host` (`ts:crates/trusted-server-adapter-spin/src/platform.rs:412-413`). That is **Unverified** for Spin's host. The source to check is wasmtime's `wasi-http` crate, `crates/wasi-http/src/types.rs`, its forbidden-header check, which is not available locally. +- Even if `host` were accepted, nothing in the interface says the host would send it instead of the authority. That is also **Unverified**. + +#### Requirements + +- **EZ-13.1** Spin MUST keep rejecting a host authority override before anything is sent, on every send mode. It MUST NOT drop the override silently, and MUST NOT send the connection to the override's host. +- **EZ-13.2** The rejection MUST be cross-cutting requirement 2's rejection, first in preflight, naming the option and the adapter. The status stays 400. +- **EZ-13.3** EdgeZero MUST establish and record, on Spin 4.1, whether a component can set a `host` request field and what `Host` an origin receives for a request. +- **EZ-13.4** The capability matrix footnote MUST state that Unsupported is final for spin-sdk 7.0.0 and WASI HTTP 0.3.0, cite the interface evidence and the EZ-13.3 results, and name the change that would reopen it. That change is a WASI request option for the connect target or a proxy, or a Spin runtime setting that maps an authority to an address. The Spin guide (`edgezero:docs/guide/adapters/spin.md:92-94`) and the proxying guide (`edgezero:docs/guide/proxying.md:89-92`) MUST say the same. + +#### Acceptance criteria + +- Spin 4.1. A request with an override, sent alone or in a batch, returns the rejection with status 400, and no WASI request is made. +- Spin 4.1. A component that sets a `host` field records whether the host forbids it. A request to a local origin records the `Host` received, which is expected to equal the authority. Both results appear in the footnote. +- Trusted Server end to end, Spin 4.1. A Spin build whose config sets `publisher.origin_host_header_override` fails config validation with a message pointing to this limitation. A Spin build without it serves publisher pages. + +#### Out of scope + +- Implementing the override on Spin. +- A fallback that silently sends the URI authority instead of the override. + +#### Open questions + +- **Q13.1** Should an override equal to the URI authority be treated as absent on every adapter? Trusted Server prefers yes. It removes a Spin failure with no semantic content. The comparison must ignore a default port, as target canonicalization does (`edgezero:crates/edgezero-core/src/outbound.rs:1622-1627`). If adopted, EZ-13.1 applies only to an override that differs from the URI authority. +- **Q13.2** Should EZ-13 stay on the follow-up list? Trusted Server prefers closing it as documentation and evidence, since spin-sdk 7.0.0 offers no way to implement it. + +#### Interim for Trusted Server + +Trusted Server rejects `publisher.origin_host_header_override` at config validation on Spin builds. Fastly and Axum honor it natively. + +### EZ-14. Idle-time bound for streamed response bodies + +**Priority:** Follow-up +**Adapters:** Fastly, Axum, Cloudflare, Spin. Axum must be Native. The others may be BestEffort where host timers or cancellation cannot be shown to meet the requirement. +**Related:** EZ-2 (exact deadlines on Fastly, which this requirement must not undo for headers), EZ-4 (the connect bound stays separate), EZ-6 (a begun streamed request), EZ-6.27 (batches refuse streamed responses), EZ-9 (only the final response body of a followed chain is read), EZ-5 (the idle bound applies to the stream as delivered, encoded or decoded) + +#### Need + +TS today bounds connection setup and silence, not total stream time. Connect is 1 s (`ts:crates/trusted-server-adapter-fastly/src/backend.rs:240`). First byte and between bytes are 15 s each. The publisher origin sets them at `ts:crates/trusted-server-core/src/publisher.rs:98` and `:4517-4518`. The asset proxy and redirect loop set them at `ts:crates/trusted-server-core/src/proxy.rs:1137-1138` and `:1338-1339`. Integrations take the same 15 s default (`ts:crates/trusted-server-core/src/platform/mod.rs:82`, applied at `ts:crates/trusted-server-core/src/integrations/mod.rs:65` and copied into between bytes at `:157-158`). A streamed page or asset can take as long as it needs while bytes keep arriving. + +On PR 275 a streamed body shares the request's total budget, measured from the start of the send. That budget is the per-call timeout, the deadline, or 30 s by default. Without this requirement: + +- A streamed page, asset, or relayed script that takes longer than the budget is cut mid-body with a gateway timeout, however steadily the origin sends. +- Time the application spends between reads counts too, which includes TS's own rewrite work and a slow client draining the response. So a fast origin feeding a slow client is cut. +- A stalled origin is noticed only when the whole budget runs out. With a budget large enough for long transfers, for example 60 s, a stall ties up the request for 60 s instead of TS's 15 s today. + +TS will ask for a header budget of about 15 s, a connect bound of 1 s (EZ-4), and an idle bound of 15 s on streamed bodies, optionally with an absolute deadline as a hard total. + +#### Gap at F + +- The budget is computed once from the send start (`edgezero:crates/edgezero-core/src/time.rs:138-187`, 30 s default at `:13`). Every body read checks it: + - Fastly checks before and after each read (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:1203-1267`). + - Axum checks per read and through `reqwest`'s per-request timeout, which covers the body (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:270-306`, `:478-484`; `edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:2281`). + - Cloudflare checks per read (`edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:439-496`). + - Spin checks per read (`edgezero:crates/edgezero-adapter-spin/src/outbound.rs:829-870`). +- At F, Fastly sets the platform between-bytes timer to the whole budget (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:718-733`), and Spin sets every phase timer to the remaining budget (`edgezero:crates/edgezero-adapter-spin/src/outbound.rs:67-74`). +- Viceroy 0.21.0 enforces that between-bytes timer. It measures the gap since the last chunk Viceroy delivered to the guest, not since the last byte the origin sent (`viceroy:src/body.rs:235-260`, reset at `:310`). So a guest that pauses reading longer than the timer can fail its next read even when the origin is healthy. When the timer fires during a body read, F reports a 502 transport error, or a 504 if the budget has also passed (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:1219-1228`). Viceroy's first-byte timer covers connect and the response head, and its expiry reaches the application as an internal error (`viceroy:src/upstream.rs:351-375`; `viceroy:src/error.rs:327-329`; `edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:959`). +- No streamed-response option separates the header phase from the body phase (`edgezero:crates/edgezero-core/src/outbound.rs:91-95`, `:1200-1203`). +- EZ-2 makes the total deadline exact on Fastly, and EZ-4 adds a connect bound. Neither releases body reads from the total budget. + +#### Requirements + +- **EZ-14.1** An application MUST be able to ask, per streamed request, that the body be bounded by idle time instead of the request's total budget. The default MUST stay as at F. +- **EZ-14.2** The idle bound MUST apply only to streamed responses. A single send or begun request that asks for it in buffered mode MUST be rejected before sending as an invalid request (400 class). A batch slot that asks for it is refused by the batch's streamed-response rule (EZ-6.27) before the idle bound is considered. A zero idle duration MUST be rejected as an invalid request. A duration beyond EdgeZero's far-future clamp MUST be clamped, as F clamps other durations. +- **EZ-14.3** With an idle bound, the request's budget (per-call timeout, deadline, or default) MUST still bound everything up to the arrival of response headers, exactly as without it. +- **EZ-14.4** After headers arrive, each wait for nonempty upstream body bytes MUST receive bytes, observe end, or fail within the idle duration. The interval starts when EdgeZero needs more upstream bytes and resets when nonempty upstream bytes arrive while that read is pending; empty chunks do not reset it. Time the application spends before its next read MUST NOT count toward the idle bound. A decoded or rechunked application-visible read MAY span several such intervals while upstream bytes keep arriving, without manufacturing empty output or reporting a false idle timeout. +- **EZ-14.5** With an idle bound, an application-set absolute deadline MUST still end the body as a hard total. The per-call timeout and the 30 s default MUST NOT end the body. +- **EZ-14.6** An idle expiry MUST surface as a gateway timeout (504 class) with `BudgetSource::IdleTimeout`. Like EZ-4.6, it uses the existing cause field rather than a separate phase axis. An applicable application-set absolute deadline takes precedence once it has expired, including simultaneous idle and deadline expiry: the timeout MUST carry that deadline's `BudgetSource`, consistent with EZ-2.7 and EZ-4.6. Otherwise, a failure the platform reports after the idle duration has passed MUST be reported as the idle timeout, not as a 502 or an internal error. +- **EZ-14.7** The idle bound MUST track nonempty encoded upstream bytes before decoding (EZ-5) or chunk shaping. Progress that has not yet produced decoded output MUST reset the idle interval. Once buffered input or output is consumed and further upstream data is needed, a stalled source MUST still time out under EZ-14.4; buffered output MUST NOT start a new wait timer by itself. +- **EZ-14.8** On every adapter, a body that keeps producing data at intervals shorter than the idle duration MUST NOT be ended by any EdgeZero or platform timer. That holds even when the idle duration is longer than the per-call timeout, and even when the application pauses between reads. The two limits that still apply are the application's deadline and any platform execution limit the capability matrix documents, such as Fastly's runtime limit (EZ-2.7). On Fastly, asking for an idle bound no longer than the per-call timeout SHOULD NOT reduce connection reuse to that origin compared with the same request without it (EZ-2). +- **EZ-14.9** An application MUST be able to tell, before sending, whether a request carries an idle bound and its duration. +- **EZ-14.10** Requests that do not ask for an idle bound MUST behave exactly as at F on every adapter, apart from EZ-2's exact deadlines. +- **EZ-14.11** On a begun request (EZ-6), the idle bound MUST govern the body once the application reads it. With redirect following (EZ-9), it MUST govern the final response's body. Intermediate redirect bodies are never delivered. +- **EZ-14.12** The capability matrix MUST list idle-bounded streamed bodies per adapter (cross-cutting requirement 9). Axum is Native. Cloudflare and Spin are BestEffort until cancellation is shown to be bounded. Fastly is BestEffort until hosted Fastly shows that a slow reader does not trigger a platform timeout. The streaming guide MUST say that an idle bound without a deadline places no EdgeZero limit on total stream time, MUST name each adapter's platform execution limit, and MUST recommend a deadline for untrusted origins. + +#### Acceptance criteria + +1. **Unit.** Given an idle bound on a buffered single send, or a zero idle duration, then each request is rejected before sending as an invalid request. Given an idle bound on a batch slot, then the slot is refused by the batch's streamed-response rule. +2. **Axum.** Given an origin that sends headers and then 8 chunks 250 ms apart, a per-call timeout of 500 ms, and an idle bound of 400 ms, then the full body arrives in about 2 s. +3. **Axum.** Given the same setup with one 600 ms gap, then the read fails with the idle timeout about 400 ms after the previous chunk, and the cause differs from a per-call timeout. +4. **Axum.** Given a fast origin and an application that waits 1 s between reads, with a 400 ms idle bound, then no idle error occurs. +5. **Axum.** Given an idle bound and a deadline 1 s after the send, then a body still flowing at 1 s ends with a deadline timeout. +6. **Viceroy 0.21.0, workerd, Spin 4.1.** Run scenarios 2, 3, and 4 against an origin that trickles its body. Viceroy enforces the platform between-bytes timer and measures it from delivery to the guest, so scenario 2 shows that EdgeZero sets no platform timer that ends a progressing body early, and scenario 4 shows that a paused reader is not failed (EZ-14.8). +7. **Hosted Fastly.** Run scenarios 2 to 4 against a real origin. Add an idle bound longer than the per-call timeout, with chunks spaced between the two. No platform timer may end the body early. +8. **Trusted Server, Viceroy 0.21.0 and hosted Fastly.** An origin streams an HTML page over 20 s in chunks 2 s apart, and the client receives the full rewritten document. A 16 s stall mid-body ends the client body at about 15 s, and TS logs an idle timeout. + +9. **Unit and Axum.** A valid gzip stream sends nonempty encoded chunks, including empty DEFLATE blocks, every 250 ms under a 400 ms idle bound but produces no decoded bytes for more than 400 ms. The pending decoded read does not fail as idle while encoded bytes arrive. If the encoded source then stays pending for 600 ms, the read fails with the idle-timeout cause after 400 ms of waiting without further upstream bytes. Zero-length source chunks do not postpone that expiry. +10. **Unit with an injected clock and Axum.** A body read starts at 600 ms with a 400 ms idle bound and an application-set absolute deadline at 1,000 ms. With no further upstream bytes, simultaneous expiry at 1,000 ms returns the deadline's gateway-timeout cause, not the idle cause. A control with a later absolute deadline returns the idle cause instead. +11. **Unit, every adapter's timeout classifier.** Given a platform failure first observed after both the pending read's idle expiry and the application's absolute deadline, classification returns the absolute deadline's `BudgetSource`, not the idle cause, a transport error, or an internal error. A control observed after idle expiry but before the absolute deadline returns the idle cause. The delayed observation does not extend either limit. + +#### Out of scope + +- An idle mode for buffered responses. A buffered send returns only after the whole body, so a total budget is the natural contract, and batches rely on it. +- EdgeZero's response write deadline, which the lifecycle requirements own. + +#### Open questions + +None. + +#### Interim for Trusted Server (follow-ups only) + +TS sets an explicit per-call timeout on each streamed site, sized for its longest expected transfer, together with EZ-4's 1 s connect bound. It raises its response write deadline through `App::set_response_egress_policy` (`edgezero:crates/edgezero-core/src/app.rs:466-476`) to match. TS accepts two costs until this lands. A mid-body stall is noticed only when that timeout expires, and a transfer longer than the timeout is cut. + +### EZ-15. Explicit fire-and-forget send + +**Priority:** Follow-up +**Adapters:** Axum, Cloudflare, Fastly, Spin. Lifetime after the callback returns may differ per platform, as EZ-15.11 says. +**Related:** EZ-6 (dropping a begun request cancels it, so fire-and-forget must be explicit; EZ-6.1 defines "on its way"), EZ-1 (a bodiless GET/HEAD built from an inbound request is accepted), EZ-2 (Fastly backend capacity), EZ-3 (EZ-3.3 owns the passed-deadline rule), EZ-8 (Image Optimizer is Unsupported on this send mode), EZ-16 (provider detail in logs) + +#### Need + +TS's Fastly Tinybird telemetry sends one NDJSON POST per auction and never waits for the answer (`ts:crates/trusted-server-adapter-fastly/src/tinybird.rs:133-147`). The backend has 2 s first-byte and between-bytes timers (`:19-20`). The request carries a bearer token (`:109-115`). TS awaits the sink (`ts:crates/trusted-server-core/src/auction/telemetry.rs:526-542`). Calls sit inside the `` hold and the non-HTML collection, before the page tail is released (`ts:crates/trusted-server-core/src/publisher.rs:4283`, `:4227`). Others run on the dispatch-failure, skip, and abandon paths (`:4177`, `:4979`, `:4995`, `:5025`). Only Fastly has a sink today. The no-op sink (`ts:crates/trusted-server-core/src/auction/telemetry.rs:500`) is the default on the other adapters (`ts:crates/trusted-server-core/src/platform/types.rs:473`). + +On PR 275's APIs, TS wants one telemetry sink on every adapter that sends the POST and returns at once, wherever the row is produced. Without this requirement, a normal `send` before delivery holds the page tail for the Tinybird round trip. Dropping a begun request is a cancellation (cross-cutting requirement 6), so it cannot stand in for fire-and-forget. + +#### Gap at F + +- `HttpClient` has no fire-and-forget operation (`edgezero:crates/edgezero-core/src/outbound.rs:567-615`). +- Fastly `send` waits for the response head (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:450-453`). On Axum, Cloudflare, and Spin a dropped, unpolled batch sends nothing (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:200-206`; `edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:389-395`; `edgezero:crates/edgezero-adapter-spin/src/outbound.rs:434-440`). +- EdgeZero's guide warns that request cancellation does not cancel application-spawned background work (`edgezero:docs/guide/adapters/overview.md:252-255`), and EdgeZero offers no bounded alternative. + +#### Requirements + +- **EZ-15.1** An application MUST be able to send a request whose response no one observes. The call MUST return without waiting for the response, and when it returns the request MUST be on its way (EZ-6.1). +- **EZ-15.2** The call MUST reject, before sending and with an immediate error, a streamed request body that would be uploaded, and Image Optimizer options (Unsupported on this send mode, rejected as cross-cutting requirement 2 says, before any other preflight check). A GET or HEAD body that EZ-1 settles as bodiless is accepted. A request whose deadline has already passed fails as EZ-3.3 and EZ-6.26 say. +- **EZ-15.3** The request's budget MUST follow the same rules as `send`, with a 30 s default when the request sets no timeout or deadline. Each adapter MUST cap it at a documented platform lifetime, and the outcome log MUST say when that cap ended the request. +- **EZ-15.4** The request MUST keep running after the application's per-request serving callback returns and after its response egress ends, until the outbound exchange finishes or its budget ends, wherever the platform allows. It is the explicit exception to ordinary request-lifetime cleanup in cross-cutting requirement 4, not just permission to survive an asynchronous response handoff. +- **EZ-15.5** EdgeZero MUST NOT hold more than 64 KiB of the response. It reads and discards at most that much, then releases the response. +- **EZ-15.6** EdgeZero MUST log each outcome, success at `debug` and failure at `warn`, with the target host, the method, the error category, and EdgeZero's own gateway reason or timeout source. EZ-16 provider detail follows EZ-16.7. The log MUST NOT contain the path, the query, any header value, or body bytes. +- **EZ-15.7** In-flight fire-and-forget sends MUST be bounded, by default to 16 per request lifetime and 64 per sandbox or server instance. A call over either bound MUST fail immediately with a service-unavailable error (503 class) and send nothing. The application MUST be able to change both bounds. +- **EZ-15.8** The call MUST work inside a streamed response body that EdgeZero is writing, and after delivery inside the same lifecycle callback. +- **EZ-15.9** Follow-up within EZ-15, required before TS enables reusable sandboxes. On a reused Fastly sandbox, fire-and-forget sends MUST NOT accumulate beyond the per-instance bound, and finished ones MUST stop counting against it. +- **EZ-15.10** EdgeZero's documentation MUST say that dropping a begun request or a batch cancels it and that fire-and-forget uses this capability. +- **EZ-15.11** Per-adapter levels. Axum MUST be Native, and server shutdown MUST stop outstanding sends without waiting for them. Cloudflare, Fastly, and Spin MAY be BestEffort, with the capability matrix stating each platform's lifetime after the callback returns. + +#### Acceptance criteria + +1. Unit. A fire-and-forget POST with a streamed body fails immediately and sends nothing. A fire-and-forget GET built from a bodiless inbound GET is sent. With a per-request bound of 2 and two requests kept pending, the third call fails with the service-unavailable error and only two requests go out. +2. Axum. Given a handler that makes a fire-and-forget call to an origin that answers after 200 ms and then returns 200, the client gets its response in under 50 ms, and the origin receives the request within 1 s. Given a server shutdown with one such send in flight, shutdown returns within 1 s. +3. workerd, Spin 4.1, and Viceroy 0.21.0. Given a handler that makes a fire-and-forget call to an origin that holds the request for 500 ms before answering, and then returns, the client's response completes before the origin answers, and the origin still receives the whole request. The Spin result sets the Spin capability level. +4. Viceroy 0.21.0 and hosted Fastly, reuse-mode follow-up. Configure the instance bound to 2. In one guest, two sequential callbacks each start a send whose origin stays pending, and then return. A third callback's send fails immediately with 503 and produces no origin request. Complete the first outbound exchange; a fourth callback's send is accepted in that same guest. Repeat reclamation after an exchange fails and after its budget expires, within Viceroy's guest request limit by using a separate guest per terminal case. On hosted Fastly, repeat across 500 sequential callbacks in retained guests, recording instance identities and saturation/recovery counts; sends that were accepted reach the origin and the instance count never exceeds 2. +5. Axum, workerd, Spin 4.1, and hosted Fastly. A fire-and-forget call to a refused port logs one `warn` line with the host and an unreachable-category error. On Viceroy 0.21.0 the line carries the category Viceroy produces, and the run records it. No line contains a path, query, or header value. +6. Axum. With an instance bound of 2, two sends owned by different completed request callbacks remain pending. A third call is refused immediately without dispatch. Completing or failing one exchange, or expiring its budget, releases its instance capacity once and admits a subsequent call without restarting the server. +7. Unit, every adapter's accounting. Success, transport failure, budget expiry, and cancellation or shutdown each release accepted-send capacity exactly once. A refused send never acquires capacity or dispatches. A double release must not admit more simultaneous sends than the configured instance bound. + +The Trusted Server end-to-end check runs on the Axum binary, workerd, Spin 4.1, and Fastly staging. It points Tinybird at a mock that answers after 1 s. An auctioned page on each adapter finishes its tail within 50 ms of the auction finishing, and the mock receives one NDJSON POST per auction. + +#### Out of scope + +- A completion callback. TS needs only the log line. +- Running arbitrary application work after the callback returns. Only outbound requests are covered, within the budget and bounds above. + +#### Open questions + +1. Should the Fastly part be Blocking? The Fastly part is EZ-15.1 to EZ-15.8 and EZ-15.10 as they apply on Fastly, plus the Fastly clause of EZ-15.11. EZ-15.9 stays tied to reusable sandboxes. TS prefers that it stay Follow-up. Its interim sends the telemetry row with an ordinary `send` after delivery on Fastly, which holds nothing the client waits on. Promotion matters only if telemetry must leave before delivery. +2. Are 16 per request lifetime and 64 per instance the right defaults? TS prefers them. TS sends a few telemetry rows per request, and the per-instance bound limits Fastly sandboxes and Axum servers. +3. Is 64 KiB the right response read limit? TS prefers it. Tinybird answers with a small JSON document. + +#### Interim for Trusted Server + +Until this lands, TS on Fastly carries the auction telemetry row out of the `` hold and sends it with an ordinary `HttpClient::send` after delivery, inside its serving callback, as it does pull sync (cross-cutting requirement 4). On Axum, Cloudflare, and Spin TS keeps the no-op sink they use today. TS neither holds the page tail nor relies on dropping a begun request. + +### EZ-16. Provider failure detail available to logs + +**Priority:** Follow-up +**Adapters:** Fastly, Axum, Cloudflare, Spin. Fastly and Spin MUST be Native. Axum and Cloudflare may be BestEffort, because their providers expose less. +**Related:** EZ-17, EZ-21, EZ-10 (their not-sent source, framing reason, and payload limit reason should be read the same way once EZ-16 has landed), EZ-14 (an idle timeout can carry the same detail), cross-cutting requirement 7 (diagnostics stay off the wire) + +#### Need + +TS today logs the provider's own cause. Its Fastly client keeps the SDK's send error as the source of its error report on `send` (`ts:crates/trusted-server-adapter-fastly/src/platform.rs:520-523`, `:540-543`). On the select path that auction providers take, it formats the SDK error into the report's text (`:606-614`). TS logs the full report on auction provider failures (`ts:crates/trusted-server-core/src/auction/orchestrator.rs:1422-1446`) and on every request error (`ts:crates/trusted-server-adapter-fastly/src/app.rs:1003-1005`). The SDK reports the specific cause, for example a DNS error with its rcode and info code, a TLS alert with its id, an HTTP/2 stream error with frame type and error code, a refused connection, or an I/O error (`fastly-sdk:src/http/request.rs:3215-3349`). + +On PR 275, TS can log only EdgeZero's category. Operators can no longer tell an NXDOMAIN from a refused connection, an expired certificate, or a TLS alert, because all four become one "destination could not be reached" category. Timeouts and transport failures still classify correctly, which is why this is a follow-up. + +#### Gap at F + +- Fastly reduces every SDK cause to eight failure kinds with six distinct messages, and it discards body-read, upload, and backend-registration error detail (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:40-49`, `:68-99`, `:877-899`, `:929-966`, `:1189-1201`, `:1219`). +- Axum keeps only `reqwest`'s yes/no predicates (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:413-432`, `:457-464`). +- Cloudflare replaces a rejected fetch with one generic failure (`edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:648`, `:1488-1493`). +- Spin drops every error payload (`edgezero:crates/edgezero-adapter-spin/src/outbound.rs:1342-1410`). +- `EdgeError` carries no detail for these failures (`edgezero:crates/edgezero-core/src/error.rs:132-192`). The wire is category-only for gateway, timeout, limit, and internal errors (`error.rs:555-575`, `:599-650`). EdgeZero's design forbids putting provider text into outbound diagnostics (`edgezero:docs/superpowers/specs/2026-05-21-outbound-http-design.md:3036-3040`). +- Provider text really does carry request data. `reqwest`'s error `Display` appends ` for url ()` (`reqwest:src/error.rs:279-281`). Spin's TLS alert payload carries a free-text `alert-message`, and its DNS payload carries the rcode as text (`spin-sdk:wit/deps/http@0.3.0/types.wit:84-94`). +- Viceroy 0.21.0 ignores send error detail on the pending-request wait (`viceroy:src/wiggle_abi/req_impl.rs:1141-1149`). Under Viceroy a refused connection reaches F as an internal error with no cause (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:95-97`, `:959`). + +#### Requirements + +- **EZ-16.1** When EdgeZero reports an outbound failure that came from the provider, the application MUST be able to read structured detail about it from the error, without parsing any text. That covers backend registration, sending, waiting for headers, request-body upload, and response-body reads. This is a MUST on Fastly and Spin. On Axum and Cloudflare it holds as far as the provider exposes the cause. +- **EZ-16.2** The detail MUST name the adapter, the phase in which the failure occurred, and the provider's cause from a closed set that EdgeZero documents per adapter. It MUST include the provider's numeric codes where the provider supplies them. These are DNS rcode and info code, TLS alert id, HTTP/2 frame type and error code, platform status, and I/O error kind. Where the provider supplies a code only as text, such as Spin's DNS rcode, the detail MUST carry it only when it matches a documented closed set of values, and otherwise MUST record it as unknown. +- **EZ-16.3** The detail MUST NOT contain provider-supplied text, URLs, header values, or body bytes. That includes Spin's TLS `alert-message` and `reqwest`'s formatted error. +- **EZ-16.4** Cross-cutting requirement 7 applies to the detail. Error responses that EdgeZero renders MUST be byte-identical with and without it. +- **EZ-16.5** Existing error text MUST NOT change. That covers each error's `Display`, `message()`, `kind()`, `status()`, and its existing reason or cause, for every variant, including `EdgeError::Internal`. +- **EZ-16.6** The detail MUST stay with the error wherever F lets the error travel. That includes body-stream errors after decoding and limits, batch slot results and batch failures, begun requests, redirect hops, and, once EZ-15 lands, fire-and-forget sends. It also covers an error the request context reports again later. +- **EZ-16.7** EdgeZero MUST NOT log the detail itself above debug level. Log policy and request correlation belong to the application. +- **EZ-16.8** Once EZ-16 has landed, the not-sent source (EZ-17), the framing reason (EZ-21), and the payload limit reason (EZ-10) SHOULD be readable from an error in the same way as this detail. +- **EZ-16.9** The capability matrix MUST list failure diagnostics per adapter (cross-cutting requirement 9). Fastly and Spin are Native. Axum and Cloudflare are BestEffort. + +#### Acceptance criteria + +1. **Unit.** Given a gateway, timeout, and internal error, each with detail attached, then rendering each as a response produces the same bytes as without detail. Its `Display` is byte-identical. Its status, kind, and reason are unchanged. +2. **Unit.** Given a body stream that fails with detail, when the error passes through decoding, limits, chunk shaping, and buffered collection, then the final error still carries the detail. +3. **Hosted Fastly.** Given a request to a closed port on a reachable host, then the error is a gateway error with reason `Unreachable`, and its detail names Fastly, the send phase, and the SDK's connection-refused cause. +4. **Viceroy 0.21.0.** Given the same request, then the error is an internal error, because Viceroy ignores send error detail. Its detail names Fastly, the send phase, and an internal cause. +5. **Spin 4.1.** Given a TLS alert with an id and a message, then the detail carries the id and not the message. Given a DNS failure whose rcode text is outside the documented set, then the detail records the rcode as unknown. Given an internal error that carries text, then no text appears in the detail. +6. **Axum.** Given a refused connection to a URL that contains `?token=secret`, then the detail names a connect failure and the refused I/O kind, and `secret` appears in neither the detail nor the rendered error. +7. **workerd.** Given a rejected fetch, then the detail names Cloudflare and the send phase. Its cause is the exception kind only when that kind is in the documented set, and otherwise it is unknown. +8. **Trusted Server, hosted Fastly staging.** Given an auction with one provider pointed at a closed port on a reachable host, then TS logs one line with the provider name and the refused cause, and the client-facing auction error body stays static. + +#### Out of scope + +- Changing any error category, status, or wire message. +- Exposing provider free text in any form. + +#### Open questions + +1. Should Cloudflare report the JavaScript exception kind (for example abort versus type error) when it is in a documented allowlist? Trusted Server's preferred answer is yes. That one distinction lets operators separate EdgeZero's own aborts from network failures. + +#### Interim for Trusted Server (follow-ups only) + +TS logs EdgeZero's category, the gateway reason, and the timeout cause for each failure. It accepts losing the DNS, TLS, and refused distinctions until this lands. + +### EZ-17. Fastly send outcome tells the application what was committed + +**Priority:** Follow-up +**Adapters:** Fastly. Axum, Cloudflare and Spin expose no request-level send helper with preparation and finalization hooks, so the requirement does not apply to them. +**Related:** EZ-18 (decides which body is delivered for buffered responses). EZ-19 (the fallback the report names carries the application's headers). EZ-20 (removes declared-KV open failure as a "no response attempted" cause). EZ-21 (the report carries the framing reason). EZ-16 (cause detail is read the same way once it lands). EZ-2 and EZ-22 (change standard entry-point serving behavior; this requirement does not). EZ-2.14 (refers to EZ-17.7 for the helpers' inputs). + +#### Need + +TS's Fastly entry point sends every response itself and never fails after sending. A post-commit streaming failure is logged and the stream dropped (`ts:crates/trusted-server-adapter-fastly/src/main.rs:668-724`, failure branch `:710-717`). Partner pull sync then runs for real browsers whatever happened to the stream (`ts:crates/trusted-server-adapter-fastly/src/main.rs:512-517`, `:536-541`, `:618-643`). When request conversion fails, TS sends a plain 500 itself (`ts:crates/trusted-server-adapter-fastly/src/main.rs:472-477`). In reuse mode, which is off in production (`ts:crates/trusted-server-adapter-fastly/Cargo.toml:13-18`), the sandbox keeps serving after a post-commit failure, because the callback returns `()` (`ts:crates/trusted-server-adapter-fastly/src/main.rs:131-142`). + +On PR 275 TS calls `send_request_with_registries_and_hooks` from inside `serve_custom` or `run_custom`, and runs pull sync after it returns, inside the same callback, with the state `finalize` returned. Without this requirement: + +1. A client that disconnects mid-page also cancels partner pull sync, although its EC `Set-Cookie` was already committed in the head. +2. TS cannot answer a request EdgeZero never attempted, because the same error also means "delivery failed after commitment". If TS sends nothing, the client gets a host-produced response. In reuse mode the sandbox also stops at the next wait (`fastly-sdk:src/http/serve.rs:357-364`, `:616-619`). +3. In reuse mode, TS cannot keep serving after a post-commit failure while stopping after a pre-send fault, short of matching error strings. +4. When framing fails after `finalize` added the EC cookie, EdgeZero delivers its 500 fallback and reports success, so TS would run pull sync for a cookie the browser never received. + +#### Gap at F + +- The helper returns `Result, fastly::Error>` (`edgezero:crates/edgezero-adapter-fastly/src/request.rs:557-586`). A declared-KV open failure and a head-conversion failure return `Err` before any response attempt (`:570`, `:632-652`, `:445-447`, `:904-906`). An admission abort returns `Err("ingress admission aborted")` (`:383`). +- A routed response runs `finalize`, then delivery, and a delivery error drops the state (`edgezero:crates/edgezero-adapter-fastly/src/request.rs:388-391`). +- Delivery errors collapse to four fixed messages with no commit information (`edgezero:crates/edgezero-adapter-fastly/src/response.rs:115-122`, `:252-257`, `:281-286`). A failure before the head reaches the host is reported as post-commit in one path (`:397-406`), and a host rejection of the head is treated as post-commit with no fallback (`:408-419`; test `:1547-1578`). +- A delivered fallback returns success (`edgezero:crates/edgezero-adapter-fastly/src/response.rs:281-284`, `:335`), although its terminal report says the fallback was delivered (`edgezero:crates/edgezero-core/src/response_egress.rs:846-867`). +- Detached responses run without the application's response-egress observer (`edgezero:crates/edgezero-core/src/app.rs:178-181`; `edgezero:crates/edgezero-core/src/response_egress.rs:661-676`). Only the admitted source-error path uses it (`edgezero:crates/edgezero-core/src/app.rs:86-102`, `:346-361`). An admission-policy error and an unsupported admission outcome also become detached responses (`edgezero:crates/edgezero-adapter-fastly/src/request.rs:486-495`). +- EdgeZero's own fixture labels every error as post-commit, including a declared-KV failure that sent nothing (`edgezero:tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs:232-237`). The Fastly guide warns that an escaping error "is not proof that any response was sent" (`edgezero:docs/guide/adapters/fastly.md:444-446`, `:513-515`). + +#### Requirements + +- **EZ-17.1** After `send_request_with_registries_and_hooks`, `send_with_registries_and_hooks` or `FastlyService::send_request_with_hooks` returns, the application MUST be able to tell which of four things happened. No response was attempted. The application's admission or detached-egress policy chose to abort. A detached response was attempted, for a head limit, a framing violation, an admission refusal, an admission-policy error, or a source error. A routed response was attempted. +- **EZ-17.2** For a routed response, the value `finalize` returned MUST reach the caller in every case, including a delivery failure after commitment and a delivered fallback. +- **EZ-17.3** For every attempted response, the caller MUST learn the commit state of the body EdgeZero last tried to send. That state is one of not committed (the head never reached the host), unknown (the host rejected the head, and whether the client saw anything is unknown), or committed (the host accepted the head). The caller MUST also learn which body that was, the application's or EdgeZero's fallback, and the terminal outcome. The terminal outcome MUST equal the `ResponseEgressReport` that the request's `ResponseEgressCompletion` received for the same attempt and, for routed responses and admitted source errors, the report the application's observer received. Detached responses keep F's rule that the application's observer is not invoked. +- **EZ-17.4** "No response was attempted" MUST be reported if and only if EdgeZero started no response attempt, invoked no observer or completion, sent nothing, and neither the admission policy nor the detached-egress policy chose `Abort`. Only that no-attempt outcome permits the caller to send its own native response without an earlier attempt, and in reuse mode the sandbox MUST be able to keep serving afterwards. A policy `Abort` MUST instead report aborted, even when the same callbacks and sends are absent. After policy `Abort`, neither EdgeZero nor the caller may send a replacement response; EZ-17.5's permission after a noncommitted attempt does not apply. +- **EZ-17.5** After an attempted response that was not committed, the caller MAY send its own native response. EdgeZero MUST document that the attempt's terminal report has already reached the completion, and the observer where EZ-17.3 says it does, and that the native response is not accounted by EdgeZero. After an unknown or committed outcome, EdgeZero MUST document that the caller must not send another response. +- **EZ-17.6** Two "no response attempted" causes MUST be distinguishable as categories. One is an explicitly required store that could not be opened. The other is a request head that could not be converted. The human-readable text MUST be fixed and contain no URI, header value or store name. Cause detail MUST be readable by the application without parsing text and MUST NOT reach the wire (cross-cutting requirement 7). Where EZ-16 has landed, it SHOULD be read the same way as EZ-16's detail. +- **EZ-17.7** `prepare` and `finalize` closures written against F MUST keep working with the helpers. Only what the helpers report changes. +- **EZ-17.8** `run_app`, `run_app_with_hooks`, `serve_app`, `serve_app_with_hooks` and `FastlyService::send` MUST keep F's serving behavior for every scenario in EZ-17.1, except where EZ-2 (sandbox retirement) and EZ-22 (initialization failure) change it. +- **EZ-17.9** Applications MUST be able to exercise every outcome in EZ-17.1 and EZ-17.3 in their own tests without a live host, through EdgeZero's `test-utils` lifecycle support at F (`FastlyService::run_request_with_hooks_for_test`, `edgezero:crates/edgezero-adapter-fastly/src/request.rs:231-249`) or its successor. +- **EZ-17.10** The Fastly guide's custom-lifecycle section and its complete example MUST describe the outcomes, the send permissions in EZ-17.4 and EZ-17.5, and a reuse-mode serving policy that keeps serving after post-commit failures. + +#### Acceptance criteria + +1. Viceroy 0.21.0. Given a routed handler whose body stream fails after its first chunk, and a `finalize` that returns a state value carrying the request's outbound client, when the request is delivered, then the client sees a partial body, and the caller learns routed, application body, committed, source error, and receives the state. An outbound request the callback then makes with that client completes before the callback returns (cross-cutting requirement 4). +2. Viceroy 0.21.0. Given a routed response that `finalize` gives a `Trailer` field, when it is delivered, then the client receives the 500 fallback, and the caller learns routed, fallback body, committed, fallback completed, and receives the `finalize` value. +3. Unit and Viceroy 0.21.0. Given an admission policy that refuses, the caller learns detached, application body, committed, `Completed`, and `finalize` never runs. Given admission `Abort` or detached-egress `Abort`, the caller learns aborted, never no-attempt; no response, fallback, or replacement is sent, no response observer or completion is invoked, and `finalize` never runs. Contrast the genuine no-attempt store failure of criterion 5, which permits the caller's native response. +4. Unit. For each failure point (deadline before the head is prepared, head preparation rejected, deadline before commit, host rejects the head, failure after the host accepted the head, fallback cannot start, fallback rejected by the host), the reported commit state, body and outcome match EZ-17.3, and the completion saw exactly one report equal to the reported one. +5. Viceroy 0.21.0. Given `FastlyService::new(&app).with_kv(name).require_kv()` on a store Viceroy does not link, the caller learns no response attempted, store category, sends a native 500, and the client receives it. +6. Unit and Viceroy 0.21.0. The standard entry points produce F's serving results for the scenarios above, apart from EZ-2 and EZ-22. +7. Trusted Server, Viceroy 0.21.0. Pull sync reaches its partner stub after a mid-body origin abort. No pull-sync call is made when a framing failure delivers the fallback. A request-conversion failure gets TS's own 500, if Viceroy can deliver such a request. + +#### Out of scope + +- Changing the standard entry points' policy of ending the sandbox on post-commit failures. +- Supplying or retaining the outbound client (EZ-2). +- Any response-returning or whole-body compatibility path. + +#### Open questions + +1. Should the standard entry points keep serving after a post-commit failure now that the cause is known? TS's preferred answer is no change in this requirement. TS uses the custom path and sets its own policy. +2. Should "unknown" be folded into "committed"? TS's preferred answer is no. Production behavior after a host rejection of the head is unverified, and folding it in would let a caller double-send or wrongly skip a send. +3. Should only EZ-17.2 be promoted to Blocking? TS's preferred answer is no. It changes the same report, so a partial version would be an API TS migrates twice. If TS needs pull sync after disconnects at the cut, promote all of EZ-17. + +#### Interim for Trusted Server + +TS logs every helper error and returns `Ok(())` from its callback. In reuse mode the sandbox then keeps serving after post-commit failures and stops at the next wait when nothing was sent. TS skips pull sync when the helper returns an error and runs it when the helper reports success. It accepts that pull sync may run after a delivered fallback, and that a head-conversion failure gets the host's response instead of TS's 500. + +### EZ-18. `Content-Length` on buffered responses + +**Priority:** Follow-up. TS's preferred answer on promotion is in open question 1. +**Adapters:** all four. Axum already meets EZ-18.1 at F. Fastly has the added parity requirement EZ-18.4. +**Related:** EZ-5 (F already sends an application- or upstream-supplied length, which EZ-18.2 and EZ-18.3 keep, so EZ-5 does not wait for EZ-18). EZ-17 (reports which body was delivered). EZ-19 (decorated responses get the same framing). EZ-Q1 (its criteria join the record when EZ-18 is present at the pinned commit). + +#### Need + +TS sends every buffered Fastly response with the SDK's `Response::send_to_client` (`ts:crates/trusted-server-adapter-fastly/src/main.rs:720-722`, after `ts:crates/trusted-server-adapter-fastly/src/compat.rs:13-43`). In the SDK's default Automatic framing mode, "a `Content-Length` is used when the size of the body can be determined before it is sent" (`fastly-shared:src/lib.rs:271-272`), and `send_to_client` sends the body as one unit (`fastly-sdk:src/http/response.rs:1735-1738`, `:1816-1821`). Under Viceroy that yields an exact-size body (`viceroy:src/wiggle_abi/resp_impl.rs:164-168`; `viceroy:src/body.rs:54-70`, `:412-427`), which hyper frames with `content-length` (`hyper-0.14.26:src/proto/h1/role.rs:840-883`). So today TS's auction JSON (`ts:crates/trusted-server-core/src/auction/formats.rs:516-527`), its 413 bodies (`ts:crates/trusted-server-core/src/auction/endpoints.rs:136-158`), discovery JSON, error pages and tsjs bundles carry a fixed length. HEAD and 304 responses go out the same way. + +TS's cut spec keeps "same response status and headers" on Fastly (`ts:docs/superpowers/specs/2026-10-17-edgezero-pr275-hard-cutover-design.md:49-52`), and its Fastly response-diff gate allows only the listed Fastly differences (`:712-719`). + +The tsjs bundles set no `Content-Length` (`ts:crates/trusted-server-core/src/http_util.rs:330-336`) and ask the Fastly host to compress them with `x-compress-hint: on` (`ts:crates/trusted-server-core/src/publisher.rs:597`). They are not at risk at F, where they go out chunked in Automatic mode. The hint matters only as a constraint on how buffered framing is restored (EZ-18.5). + +The only reason left to promote the buffered part is header parity. TS knows of no functional breakage on its path from chunked buffered responses. Without EZ-18, TS lists "buffered responses lose `Content-Length` on Fastly" as an intended difference and normalizes it in its response diff. + +#### Gap at F + +- Core never derives a length from a buffered body. It only canonicalizes a length the application set (`edgezero:crates/edgezero-core/src/response_egress_framing.rs:80`, `:115-138`). +- Fastly commits the head (`edgezero:crates/edgezero-adapter-fastly/src/response.rs:408`) before it writes any body bytes (`:434`, `:486`), using the streaming send (`edgezero:crates/edgezero-adapter-fastly/src/fastly_response_abi.rs:64-70`). It selects manual framing only when the body is transmitted and a `Content-Length` header is already present (`edgezero:crates/edgezero-adapter-fastly/src/response.rs:159-161`), so HEAD, 204, 205 and 304 stay in Automatic mode even with a length. In Automatic mode a streamed response "will receive a `Transfer-Encoding: chunked`" (`fastly-shared:src/lib.rs:272-274`). Under Viceroy the body size is unknown, the guest's framing headers are stripped, and hyper sends `transfer-encoding: chunked` (`viceroy:src/body.rs:412-417`; `viceroy:src/http/framing.rs:68-70`; `viceroy:src/http/filtering.rs:3-8`; `hyper-0.14.26:src/proto/h1/role.rs:840-854`). +- EdgeZero's 500 and 504 fallbacks are buffered bodies without a length, so they are chunked too (`edgezero:crates/edgezero-adapter-fastly/src/response.rs:342-351`). +- HEAD and 304 at F use the same commit as other responses, while TS uses `send_to_client`. Under Viceroy hyper writes the same head for both (no `content-length` for an empty HEAD or 304, `hyper-0.14.26:src/proto/h1/role.rs:476-503`, `:840-892`), so only hosted Fastly can show whether the two differ. hyper does write a length for a HEAD whose body has a known non-zero size (`hyper-0.14.26:src/proto/h1/role.rs:490-499`, `:869-881`). +- A write deadline is checked before the head is prepared and before commit (`edgezero:crates/edgezero-adapter-fastly/src/response.rs:364`, `:389`). The only window in which a deadline truncates a buffered response is between commit and the body write (`:423-431`). +- In manual framing mode a `Content-Length` that disagrees with the body truncates it or hangs up the connection (`fastly-shared:src/lib.rs:285-287`). How manual framing interacts with host compression is unverified. +- Cloudflare sends a buffered body without a declared length as an unsized stream (`edgezero:crates/edgezero-adapter-cloudflare/src/response.rs:518-530`; test `:1050-1090`). Spin forwards headers and streams the body (`edgezero:crates/edgezero-adapter-spin/src/response.rs:367-389`). Axum already sends the length (`edgezero:crates/edgezero-adapter-axum/src/response.rs:278-299`). +- The streaming guide says EdgeZero "does not guarantee a particular HTTP/1 transfer coding" (`edgezero:docs/guide/streaming.md:167-169`). + +#### Requirements + +- **EZ-18.1** On every adapter, a transmitted response whose body is fully buffered and is not transformed by the host MUST reach the client with `Content-Length` equal to that buffered body's byte length, including `0`, and without `Transfer-Encoding`, on HTTP/1.1. On HTTP/2 it MUST carry `content-length`. This applies to application responses, rendered error responses, detached responses and EdgeZero's fallbacks. When the host transforms the body, the client-visible length, if present, MUST describe the transmitted representation, not EdgeZero's pre-transform buffer; EZ-18.5 governs, and Fastly's SDK-parity rule in EZ-18.4 takes precedence over adding a buffered length. +- **EZ-18.2** An application `Content-Length` that matches a buffered body MUST be kept when handing it to the host. A mismatch MUST keep F's behavior, a 500 fallback before commit. Host transformation may recompute or omit that length under EZ-18.5; EdgeZero MUST NOT force the pre-transform length onto different transmitted bytes. +- **EZ-18.3** Streamed bodies keep F's behavior. Without a declared length they use the provider's transfer framing. With a declared length, EdgeZero hands that length to the host and enforces it against the source bytes; a stream that runs long or short is aborted. For a host-transformed body, the client-visible framing follows EZ-18.5 instead of forcing that source length onto transformed bytes. +- **EZ-18.4** On Fastly, the framing the client receives for buffered responses and for bodiless responses (HEAD, 204, 205, 304) MUST match what the Fastly SDK's `Response::send_to_client` produces for the same status, headers and body, on HTTP/1.1 and HTTP/2. That is TS's pre-cut behavior, so the HEAD and 304 `Content-Length` behavior is whatever the host does for TS today. +- **EZ-18.5** When a response carries `x-compress-hint` and the Fastly host compresses it, the client MUST receive valid framing and a body that decompresses to the original bytes. EdgeZero MUST NOT cause the host to send a length that disagrees with the compressed body. The host may recompute or omit `Content-Length` as its SDK reference does; host transformation takes precedence over EZ-18.1's untransformed buffered-length rule. This holds for buffered responses and for streamed responses with a declared length, and the record MUST distinguish guest-supplied lengths from client-visible lengths. +- **EZ-18.6** Except where EZ-18.4 requires otherwise on Fastly, EdgeZero MUST NOT add a length to a bodiless response (HEAD, 204, 205, 304) that the application did not supply. On Fastly, a HEAD whose application body has a known size gets the framing EZ-18.4's reference produces for that body. An application-supplied length on HEAD or 304 is passed to the host as at F. +- **EZ-18.7** On Fastly, a write deadline that expires after the head is committed and before a buffered body has been handed to the host SHOULD NOT truncate the response. The head and body SHOULD reach the host together, so an expiry before that point produces the 504 fallback. +- **EZ-18.8** The capability matrix MUST gain a row for buffered-response `Content-Length`. Axum is Native. Fastly is BestEffort until criterion 3 passes on hosted Fastly over HTTP/1.1 and HTTP/2, then Native. Cloudflare is BestEffort until a deployed Workers check shows the header reaching the client. Spin is BestEffort until a Spin 4.1 check shows it. `docs/guide/streaming.md` MUST state that untransformed buffered bodies carry `Content-Length` and describe the host-transformation exception in EZ-18.5. + +#### Acceptance criteria + +1. Viceroy 0.21.0, HTTP/1.1. Given a buffered 200 route, a streamed route without a length, a rendered 404, and a routed response with a `Trailer` field, then the buffered route, the 404 and the 500 fallback (21 bytes, `internal server error`) carry `content-length` and no `transfer-encoding`, and the streamed route carries `transfer-encoding: chunked`. +2. Viceroy 0.21.0, HTTP/2 with prior knowledge, which Viceroy's hyper accepts by default (`hyper-0.14.26:src/server/conn.rs:950-955`). A buffered route carries `content-length`. +3. Hosted Fastly, HTTP/1.1 and HTTP/2, on TS's staging service. For a buffered 200, a buffered 200 with `x-compress-hint: on` requested with `Accept-Encoding: gzip`, HEAD and conditional-GET 304 of a buffered route, and a 204, the framing equals that of TS's pre-cut build. The compressed body decompresses to the expected SHA-256. Also exercise a streamed compressible response with its source `Content-Length` declared and `x-compress-hint: on`, requested with gzip: verify the host-compressed body decompresses to the source SHA-256 and has valid framing. Record the source length, transmitted byte count, coding, and any client-visible length; that length, when present, equals the transmitted count, not the source count. +4. Viceroy 0.21.0. Given a buffered response whose response-owned `ResponseEgressDeadline` is already in the past, the client receives the 504 fallback (32 bytes, `response write deadline exceeded`) with `content-length: 32`. +5. workerd. A buffered route carries `content-length`. Spin 4.1. The same. Axum. Unchanged from F. EdgeZero records a passing run of each for the pinned commit. Its CI runs only the Fastly fixture smoke at F (`edgezero:.github/workflows/test.yml:399-401`). +6. Trusted Server, Viceroy 0.21.0 and hosted Fastly. Auction JSON, discovery JSON, a tsjs bundle and an error page match the pre-cut build's `content-length` and `transfer-encoding`. Publisher HEAD and 304 match the pre-cut build. + +#### Out of scope + +- Manual framing for HEAD and 304 beyond the parity in EZ-18.4. +- Any change to how untransformed streamed bodies are framed; host-transformation framing is governed by EZ-18.5. +- Guessing a length for a streamed body. + +#### Open questions + +1. Promote the buffered part to Blocking? TS's preferred answer is Follow-up with the interim below. The only reason to promote is header parity and the response-diff gate. No functional breakage is known. If the TS maintainer holds the parity goal for the cut, promote EZ-18.1 and EZ-18.4. +2. Should the HEAD and 304 parity in EZ-18.4 be a MUST without hosted evidence? TS's preferred answer is yes. It is TS's current wire behavior, and criterion 3 checks it on hosted Fastly. + +#### Interim for Trusted Server + +TS lists "buffered responses lose `Content-Length` on Fastly, chunked on HTTP/1.1" as an intended Fastly difference in its cut spec, and its response diff normalizes `content-length` and `transfer-encoding` for buffered routes. + +### EZ-19. Application headers on detached and egress-fallback responses + +**Priority:** Follow-up +**Adapters:** all four, Native. The per-request form (EZ-19.2) needs a hook that runs before ingress checks, which only Fastly's canonical path has (`prepare`). +**Related:** EZ-17 (reports when these responses were delivered). EZ-18 (they still carry a length). EZ-21 (a set that breaks framing is diagnosable). EZ-22 (an initialization-failure response has no configured set). + +#### Need + +Every Fastly response TS sends today goes through TS's finalize steps, because TS sends every response itself (`ts:crates/trusted-server-adapter-fastly/src/main.rs:486-506`, `:557`, `:668-689`). Those steps add geo headers or `X-Geo-Info-Available: false`, `X-TS-Version`, `X-TS-Fastly-Version`, `X-TS-ENV: staging`, cookie cache privacy, and the operator's `response_headers` (`ts:crates/trusted-server-adapter-fastly/src/middleware.rs:187-237`), plus sandbox counter headers on `private, no-store` responses (`ts:crates/trusted-server-adapter-fastly/src/main.rs:238-269`). + +On PR 275's canonical path, responses EdgeZero synthesizes skip `finalize`. These are 414 (absolute URL over 8192 bytes), 431 (over 100 header values or 64 KiB of headers), 400 framing rejections (`edgezero:crates/edgezero-core/src/ingress.rs:21-23`), admitted source errors, and the 500 and 504 egress fallbacks. A 500 caused by a bad operator header then carries no `X-TS-Version`, so an operator cannot tell which build produced it, and 414 and 431 lack the operator's headers. TS's spec accepted this on Axum, Cloudflare and Spin (`ts:docs/superpowers/specs/2026-10-17-edgezero-pr275-hard-cutover-design.md:408-411`) before Fastly moved onto the canonical lifecycle. + +#### Gap at F + +- `finalize` runs for routed responses only (`edgezero:crates/edgezero-adapter-fastly/src/request.rs:384-387`, `:475-477`, `:481-483`, `:498-502`). +- `App::set_error_response_renderer` receives only the error, so it cannot add per-request values such as geo, and egress fallbacks bypass it (`edgezero:crates/edgezero-core/src/app.rs:410-423`). Its doc says fallbacks bypass the renderer because "the application supplied those responses directly" (`:415-416`). That is true of admission refusals but not of fallbacks, which the adapter builds. +- Each adapter builds the fallback with no extension point (`edgezero:crates/edgezero-adapter-fastly/src/response.rs:342-351`; `edgezero:crates/edgezero-adapter-cloudflare/src/response.rs:418-427`; `edgezero:crates/edgezero-adapter-spin/src/response.rs:792-801`; `edgezero:crates/edgezero-adapter-axum/src/response.rs:704-732`, which also holds Axum's second fallback builder). +- The fallback runs under a separate 1 s budget and must not run application policy (`edgezero:crates/edgezero-core/src/response_egress.rs:17`; `edgezero:docs/superpowers/specs/2026-09-08-response-egress-design.md:454-461`). + +#### Requirements + +- **EZ-19.1** An application MUST be able to configure, once per app, header fields that EdgeZero adds to every response it synthesizes outside routed dispatch. These are detached ingress errors (head limits, framing, admission-start errors), admitted source errors, and egress fallbacks, including a HEAD fallback, which gets the headers and no body. +- **EZ-19.2** On Fastly's canonical path, the application MUST be able to supply the set for one request from `prepare`, before head limits are checked. A per-request set replaces the app-level set for that request. +- **EZ-19.3** The set MUST NOT be applied to routed responses, admission refusals (the policy builds those), or responses the host produces before the guest runs. +- **EZ-19.4** A set field whose name is already present on the synthesized response MUST be skipped, so a value EdgeZero or the renderer set (for example `Allow` on a 405, `Retry-After`, or `Cache-Control`) stays the only value of that name. Several values of one name within the set are all added, in order. EZ-10.10 overrides this decoration rule for a typed framework-generated body-cap overflow: renderer and configured-set attempts to add `Retry-After` MUST be suppressed in the final response. Do not ban that name from validated sets globally or suppress it merely because a response has status 413; ordinary upstream responses and the effective config-503 retry policy keep their existing treatment. +- **EZ-19.5** The set MUST NOT contain `Content-Length`, `Transfer-Encoding`, `Trailer`, `TE`, `Connection`, `Keep-Alive`, `Proxy-Connection`, `Upgrade`, `Proxy-Authenticate`, `Proxy-Authorization`, `Content-Type` or `Content-Encoding`, and MUST be bounded in field count and bytes. App-level sets MUST be validated at configuration time. Dynamic per-request sets MUST be validated when constructed in `prepare`, before installation; an invalid set returns a typed construction error without replacing the configured set. Only validated sets may be installed, and fallback construction MUST NOT run application code or repeat application validation. EdgeZero documents the bounds. +- **EZ-19.6** Application code MUST NOT run while a fallback is built or sent. The fallback's status, body and 1 s budget MUST NOT change, and adding the set MUST NOT make a deliverable response undeliverable. +- **EZ-19.7** When application configuration itself fails (EZ-22), no configured set exists, and the initialization-failure response carries none. EdgeZero MUST document this next to that response. +- **EZ-19.8** The renderer's doc MUST distinguish admission refusals (application-built) from fallbacks (adapter-built). + +#### Acceptance criteria + +1. Viceroy 0.21.0. Given an app-level set, when a request arrives with a 9 KiB absolute URL, then the 414 carries the set. When a request arrives with about 72 KiB of headers in 60 lines, over EdgeZero's 64 KiB limit and under hyper's 100-line limit and 417,792-byte read buffer (`hyper-0.14.26:src/proto/h1/role.rs:27`; `hyper-0.14.26:src/proto/h1/io.rs:30`; Viceroy does not lower either), then the 431 carries the set. Do not use 101 header lines, because Viceroy's hyper answers those with its own 431 before the guest runs (`hyper-0.14.26:src/error.rs:564`; `hyper-0.14.26:src/proto/h1/role.rs:443-453`). +2. Viceroy 0.21.0. Given a routed response that carries a `Trailer` field, the 500 fallback carries the set and its body is unchanged. A HEAD request to the same route gets the fallback's headers with the set and no body. +3. Viceroy 0.21.0. Given a per-request set supplied in `prepare`, the 414 carries that set and not the app-level one. +4. Unit, each adapter. A fallback carries the app-level set. A rendered 405 keeps its own `Allow` and no second one. A set holding `Cache-Control` leaves a synthesized response's own `Cache-Control` as its only value. +5. Unit. An app-level set containing any name in EZ-19.5, or exceeding the bounds, fails at configuration time. A dynamic per-request set with the same violations fails construction during `prepare`, before installation, without changing the configured set. +6. Trusted Server, Viceroy 0.21.0. The 414 and 431 carry `X-TS-Version`, `X-TS-Fastly-Version`, geo and operator headers. The framing-failure 500 carries the same headers. +7. Unit and Viceroy 0.21.0. Given a valid app-level set and a per-request set constructed in `prepare` whose dynamic values exceed the bound, that construction returns the typed error. A subsequent detached 414 and an egress fallback retain the valid app-level set. Fallback construction does not invoke validation or any application callback. +8. Unit, each adapter, when EZ-10's typed overflow and EZ-19 header composition are present. A generated inbound/upload-cap error remains 413 with no `Retry-After` even when both its custom renderer and app-level set supply one; unrelated configured headers remain. On Fastly, repeat an admitted source error with a per-request replacement set from `prepare`. An ordinary upstream 413 with a non-nominated `Retry-After: 120` retains it. A config-503 response using the default effective retry policy retains its single `Retry-After: 60` despite competing renderer/set values. If either follow-up part is absent, record that combined leg's applicability rather than implicitly promoting it. + +#### Out of scope + +- Running application code for synthesized responses. +- Changing the fallback's status or body. +- Decorating admission refusals. + +#### Open questions + +1. Should a per-request set replace the app-level set or merge with it by name? TS's preferred answer is replace. One rule is easier to reason about, and TS builds the full set per request anyway. +2. Should admission refusals be decorated? TS's preferred answer is no. The policy builds them and can add headers itself. + +#### Interim for Trusted Server + +TS accepts that detached and fallback responses carry no TS headers on Fastly, as it already accepts on the other three adapters. TS may add `X-TS-Version` to detached errors through `App::set_error_response_renderer`. Fallbacks stay bare. + +### EZ-20. A declared store that cannot open fails at use, not on every request + +**Priority:** Follow-up +**Adapters:** Fastly MUST. Cloudflare and Spin SHOULD. Axum keeps its boot-time failure, which happens before it accepts any request. +**Related:** EZ-17 (an explicitly required store keeps the "no response attempted" outcome). + +**Adoption decision (2026-10-21):** The requester approved including Cloudflare and Spin in +the same implementation. EZ-20.7 and acceptance criterion 4 therefore apply to this delivery; +Axum's boot-time policy remains unchanged. Local evidence does not replace criterion 5 or +hosted outage/restoration qualification. + +#### Need + +TS's Fastly `Hooks::stores()` declares the KV store `trusted_server_kv` (`ts:crates/trusted-server-adapter-fastly/src/app.rs:1347-1362`; `ts:edgezero.toml:23-25`). Neither TS's `fastly.toml` (`ts:fastly.toml:25-42`) nor its Viceroy template (`ts:crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml:9-59`) links it, and TS reads no EdgeZero KV store on Fastly (only Cloudflare and Spin call `kv_store_default()`, `ts:crates/trusted-server-adapter-cloudflare/src/platform.rs:613`, `ts:crates/trusted-server-adapter-spin/src/platform.rs:721`). Passing `stores()` to the canonical helper therefore fails every request except TS's health probe, which short-circuits first (`ts:crates/trusted-server-adapter-fastly/src/main.rs:173-176`). One unlinked store in a deployment would take down every route, including routes that never touch KV. + +#### Gap at F + +- Fastly opens every declared KV id per request and returns an error, with no response, on the first failure (`edgezero:crates/edgezero-adapter-fastly/src/request.rs:570`, `:632-652`, `:908-923`). +- Declared config stores that fail to open become failing handles, classified as invalid configuration, and the request proceeds (`edgezero:crates/edgezero-adapter-fastly/src/request.rs:654-677`; `edgezero:crates/edgezero-core/src/config_store.rs:503-510`). The capability guide describes that for config only (`edgezero:docs/guide/capabilities.md:192-194`). +- F's Fastly KV open maps `Ok(None)` to `KvError::Unavailable` (503) and every SDK open error to `KvError::Internal` (500) (`edgezero:crates/edgezero-adapter-fastly/src/key_value_store.rs:30-35`; `edgezero:crates/edgezero-core/src/key_value_store.rs:823-844`). The SDK reports an unlinked store as `KVStoreError::StoreNotFound` on `INVAL`, so that missing-store path is already internal-class (`fastly-sdk:src/kv_store/handle.rs:272-280`). EZ-20.2 distinguishes those absence cases from transient provider failures; it does not blindly reverse all SDK-error mappings. +- Cloudflare and Spin also fail the request on a declared KV store that cannot open (`edgezero:crates/edgezero-adapter-cloudflare/src/request.rs:758-783`; `edgezero:crates/edgezero-adapter-spin/src/request.rs:690-716`). Axum fails at boot (`edgezero:crates/edgezero-adapter-axum/src/dev_server.rs:491-496`). + +#### Requirements + +- **EZ-20.1** On Fastly's canonical path with declared store metadata, a declared KV store that cannot be opened MUST NOT fail the request, skip `prepare`, or prevent dispatch. Handlers MUST still find the store by its declared id, and its operations MUST fail when used. +- **EZ-20.2** An unlinked or missing declared store MUST fail its operations with an internal-class error (500 when rendered), because it is a deployment error. This includes SDK `StoreNotFound` and `Ok(None)`. A transient provider failure to open MUST fail them with an unavailable-class error (503); EdgeZero MUST distinguish it from missing-store and invalid-configuration failures, which remain internal-class. Error text MUST NOT contain the store name or provider text. +- **EZ-20.3** A request that never uses a declared KV store SHOULD incur no extra provider work for it. +- **EZ-20.4** EdgeZero MUST log an open failure at most once per store id per sandbox, as it does today for config. +- **EZ-20.5** Store availability MUST NOT be cached across requests. The once-per-sandbox log state of EZ-20.4 is the only state kept. +- **EZ-20.6** A store the application explicitly requires (`FastlyService::new(&app).with_kv(name).require_kv()`) MUST keep failing the request before anything is sent, reported as EZ-17's "no response attempted". +- **EZ-20.7** Cloudflare and Spin SHOULD follow EZ-20.1 to EZ-20.5. +- **EZ-20.8** The capability guide MUST describe declared-KV failure behavior next to config's. + +#### Acceptance criteria + +1. Viceroy 0.21.0. Given an app declaring a KV id Viceroy does not link, a route that ignores KV returns 200, and a route that reads the store returns 500, in the same request sequence. +2. Unit. SDK `StoreNotFound`, `Ok(None)`, and invalid binding configuration each produce an internal-class failing handle; a transient provider open failure produces an unavailable-class failing handle. No message contains the store name or provider text. +3. Viceroy 0.21.0. Given the setup in criterion 1 in one reused sandbox, two requests to the KV route produce one open-failure log line in total, and requests to the KV-free route produce none. +4. workerd and Spin 4.1, if EZ-20.7 is adopted. Same as criterion 1. +5. Trusted Server, Viceroy 0.21.0. With `TrustedServerApp::stores()` passed to the helper against TS's Viceroy template, every route serves the pre-cut responses. + +#### Out of scope + +- Changing Axum's boot-time failure. +- Changing config or secret store behavior. + +#### Resolved decision + +Cloudflare and Spin change with Fastly in the same release, following the adoption decision above. + +#### Interim for Trusted Server + +TS passes `StoresMetadata::default()` to the helper on Fastly and keeps `Hooks::stores()` only for its manifest-parity test. + +### EZ-21. Framing failure reason in logs + +**Priority:** Follow-up +**Adapters:** all four, Native. +**Related:** EZ-16 and EZ-10 (read the same way once they land). EZ-17 (the report it returns carries the reason). EZ-19 (a set that breaks framing is reported the same way). + +#### Need + +TS's framing failures come from operator `response_headers` or integration headers that name `trailer` or `content-length`. TS's framing-header guard (D8 in TS's cut spec) rejects those at deploy and skips them at runtime (`ts:docs/superpowers/specs/2026-10-17-edgezero-pr275-hard-cutover-design.md:88`, `:529-541`, `:604-605`). TS sets lengths from final bytes on its rewrite paths (`:399-403`), so a wrong length there would be a TS bug that also needs diagnosing. Each of these becomes a 500 fallback. An operator who adds a header and starts serving 500s needs to know which rule fired. + +#### Gap at F + +- The log line says only "response-egress framing validation failed before commit" (`edgezero:crates/edgezero-core/src/response_egress.rs:650-656`), and the report records only a conversion error (`edgezero:crates/edgezero-core/src/response_egress.rs:291-301`; test `edgezero:crates/edgezero-adapter-fastly/src/response.rs:1310-1317`). +- A policy panic, a deadline normalization failure and a Fastly head-preparation failure produce the same outcome (`edgezero:crates/edgezero-core/src/response_egress.rs:620-638`; `edgezero:crates/edgezero-adapter-fastly/src/response.rs:375-385`). +- F defines six framing rules with fixed text, `ResponseEgressFramingError` (`edgezero:crates/edgezero-core/src/response_egress_framing.rs:23-39`), but every adapter discards the rule, including when the fallback itself fails framing (`edgezero:crates/edgezero-adapter-fastly/src/response.rs:316-322`; `edgezero:crates/edgezero-adapter-cloudflare/src/response.rs:392-396`; `edgezero:crates/edgezero-adapter-spin/src/response.rs:766-770`; `edgezero:crates/edgezero-adapter-axum/src/response.rs:635-636`, `:683-686`). + +#### Requirements + +- **EZ-21.1** When a response fails framing validation before commit, the rule that failed MUST be available to the application as one stable name per rule F defines (content-length mismatch, invalid connection header, invalid content-length, unsupported status, unsupported trailers, unsupported tunnel), and per rule added later. It MUST appear in the terminal report that the completion receives, and that the observer receives where EZ-17.3 says it does, and in EdgeZero's log line for that event. +- **EZ-21.2** Applications MUST be able to read the reason from the terminal report without parsing message text. It MUST NOT reach the wire, and existing error `Display` text and the fallback body MUST NOT change (cross-cutting requirement 7). Where EZ-10 or EZ-16 has landed, the reason SHOULD be read the same way as their detail. +- **EZ-21.3** This MUST hold for routed responses, detached responses and the fallback itself, on all four adapters. +- **EZ-21.4** A policy panic, a deadline normalization failure and a host rejection of the head MUST remain distinguishable from framing failures in the report or the log. +- **EZ-21.5** The reason MUST NOT include header values, `Connection` tokens or body bytes. The log line MAY include the route's method and registered pattern, which are bounded (`edgezero:crates/edgezero-core/src/router.rs:40-68`; `edgezero:docs/superpowers/specs/2026-09-08-response-egress-design.md:851-853`). + +#### Acceptance criteria + +1. Unit, each adapter. For each framing rule, a response that breaks it yields a report naming that rule, and a policy panic yields a report naming none. +2. Viceroy 0.21.0. Given a routed response with a `Trailer` field, the runtime log line and the report both name "unsupported trailers", and the client receives the unchanged 500 fallback. +3. Trusted Server, Viceroy 0.21.0. With TS's framing-header guard disabled, an operator `trailer` header produces the 500 fallback and TS's completion log names "unsupported trailers". + +#### Out of scope + +- Naming the offending header in the reason. +- Reasons for post-commit failures, which F already logs by category (`edgezero:crates/edgezero-core/src/response_egress_framing.rs:212-256`). + +#### Open questions + +1. At what level does EdgeZero log the framing reason? TS's preferred answer is `error`, the level F already uses for this event (`edgezero:crates/edgezero-core/src/response_egress.rs:651`). The names are EdgeZero's own rules with no provider data, so a debug-only rule for provider detail need not apply. + +#### Interim for Trusted Server + +TS's framing-header guard prevents the known sources. TS accepts undiagnosed framing 500s from any other source. + +### EZ-22. Fallible application state built and retained with the routes + +**Priority:** Follow-up +**Adapters:** core behavior on all four adapters. The outcome of a failed assembly is specified per entry point. +**Related:** EZ-23 (TS assembles its test apps from explicit settings with the same policies), EZ-17 (reports the failure response as a detached response), EZ-19 (the failure response carries no app-configured headers) + +#### Need + +TS builds one application state per application lifetime. On Fastly it holds `settings`, `orchestrator`, `registry`, `default_kv_store`, and `auction_telemetry_sink` (`ts:crates/trusted-server-adapter-fastly/src/app.rs:186-192`). The auction plan is compiled at `:223` and held inside the orchestrator and registry. Building the state can fail, because it reads the config and secret stores and validates the plan for the target (`ts:crates/trusted-server-adapter-fastly/src/app.rs:200-239`). + +`Hooks::routes()` cannot fail, so every TS adapter turns a state failure into a router that answers every route with the error status (`ts:crates/trusted-server-adapter-fastly/src/app.rs:1042-1068`, `ts:crates/trusted-server-adapter-cloudflare/src/app.rs:410-435`, `ts:crates/trusted-server-adapter-axum/src/app.rs:579-603`). Spin's version always answers 503 and keeps `/health` at 200 (`ts:crates/trusted-server-adapter-spin/src/app.rs:508-538`, `:529-531`). EdgeZero's retention paths treat such a router as a successful app, so retaining it would pin one transient config-store failure for a whole sandbox or isolate. TS avoids this only on Fastly, with its own retained pair of app and state (`ts:crates/trusted-server-adapter-fastly/src/sandbox.rs:57-73`) and a failed build that serves one request and is dropped (`ts:crates/trusted-server-adapter-fastly/src/main.rs:382-412`). + +TS's Fastly entry point reads the state outside the router. It reads `settings` before dispatch to resolve the trusted client IP (`ts:crates/trusted-server-adapter-fastly/src/main.rs:414-427`), after dispatch for finalize headers and EC finalization (`:482-555`), and after the response is sent for pull sync (`:517`). When state is missing it reloads settings from the config store again in the same request (`:489-498`, `:526-553`). + +Cloudflare and Spin rebuild everything on every request, because `run_app` assembles the app per request (`edgezero:crates/edgezero-adapter-cloudflare/src/lib.rs:172-183`, `edgezero:crates/edgezero-adapter-spin/src/lib.rs:187-201`) and TS builds its state inside `routes()` (`ts:crates/trusted-server-adapter-cloudflare/src/app.rs:449-458`, `ts:crates/trusted-server-adapter-spin/src/app.rs:553-562`). Each Cloudflare request re-parses `TRUSTED_SERVER_CONFIG`, re-reads secrets, recompiles the auction plan, and rebuilds the integration registry (`ts:crates/trusted-server-adapter-cloudflare/src/app.rs:129-160`, `:196-217`). Cloudflare state needs the request's `worker::Env`, which TS passes to the input-less `routes()` through a thread-local (`ts:crates/trusted-server-adapter-cloudflare/src/app.rs:52-70`, `ts:crates/trusted-server-adapter-cloudflare/src/lib.rs:27-30`). Spin state comes from asynchronous store reads that TS blocks on inside `routes()` (`ts:crates/trusted-server-adapter-spin/src/app.rs:84-107`). + +On PR 275's APIs TS wants one shape on every adapter. It builds its state, which may fail and may need provider bindings or asynchronous reads, assembles an app with the same name and `configure` policies `App::build` applies, retains it where the adapter allows, and reads the state from the retained app in entry-point code. TS serves Fastly through `serve_custom` with `lifecycle::Sandbox`, so a failed assembly on that path must also answer the current request and must not be retained. + +#### Gap at F + +- `App::build::(platform)` takes the router from `A::routes()`, runs `A::configure`, and rejects changed platform metadata (`edgezero:crates/edgezero-core/src/app.rs:151-164`). Its doc calls it the sole adapter-facing assembly path (`:143-146`). `routes()` returns a bare `RouterService` and cannot fail (`:610-611`). Only `configure` can fail (`:554-556`). +- There is no way to assemble an app from a router the application built itself with `configure` and the platform check applied. The only alternative, `App::with_name_and_platform` (`:488-514`), skips both. +- State registered with `RouterBuilder::with_state` lives in the router (`edgezero:crates/edgezero-core/src/router.rs:337-355`) and reaches handlers through `State` (`edgezero:crates/edgezero-core/src/extractor.rs:619-653`), but neither `RouterService` (`edgezero:crates/edgezero-core/src/router.rs:458-600`) nor `App` (`edgezero:crates/edgezero-core/src/app.rs:38-50`) lets entry-point code read it. +- The `app!` macro registers only an infallible state expression (`edgezero:crates/edgezero-macros/src/app.rs:61-66`, `:358-360`). The demo works around per-request assembly with a process-wide `OnceLock` (`edgezero:examples/app-demo/crates/app-demo-core/src/lib.rs:280-298`). +- When assembly fails, Fastly `serve_app_with_hooks` ends the serving loop without a response for the current request (`edgezero:crates/edgezero-adapter-fastly/src/lib.rs:255`, `edgezero:docs/guide/adapters/fastly.md:439-446`). On the custom path, `Sandbox::initialize` returns the error to the callback and leaves how to respond to the caller (`edgezero:crates/edgezero-adapter-fastly/src/lifecycle.rs:67-85`), and EdgeZero offers no way to answer without an assembled app, so the fixture sends its own native 503 (`edgezero:tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs:203-215`). +- Cloudflare `run_app` returns a generic `worker::Error` on a failed assembly (`edgezero:crates/edgezero-adapter-cloudflare/src/lib.rs:66-74`), and Spin returns an untyped error (`edgezero:crates/edgezero-adapter-spin/src/lib.rs:102-108`, `:199`). Neither sends the error's own status. +- The retention guidance for Cloudflare and Spin keeps only a successfully assembled app (`edgezero:docs/guide/adapters/cloudflare.md:342-378`, `edgezero:docs/guide/adapters/spin.md:338-375`), which is correct and must stay true. + +#### Requirements + +- **EZ-22.1** An application's route building MUST be able to fail with a typed `EdgeError`. When it fails, `App::build` MUST return that error unchanged, MUST NOT apply `configure`, and MUST NOT produce an app. +- **EZ-22.2** App state MUST have one source. The value an application registers with `RouterBuilder::with_state` MUST be the value handlers receive through `State` and the value entry-point code reads from the assembled app, with shared interiors shared. +- **EZ-22.3** Code that holds an assembled app, such as a Fastly custom serving callback or a Cloudflare or Spin retention wrapper, MUST be able to read registered state of a given type without a request, and MUST get nothing when no such state was registered. +- **EZ-22.4** An application's `configure` MUST be able to read the registered state when it chooses policies, for example ingress head limits taken from settings. +- **EZ-22.5** An application MUST be able to assemble an app from a router it built itself. That assembly MUST apply the same three things `App::build` applies to a router from `routes()`, which are the application's name, its `configure` policies, and the platform-metadata check. This covers state that needs provider bindings, asynchronous store reads, or explicit test settings. +- **EZ-22.6** Every EdgeZero entry point or documented recipe that retains an app MUST retain only an app whose route building and `configure` both succeeded. A failure MUST leave nothing retained, and the next request MUST retry assembly. +- **EZ-22.7** Fastly `serve_app` and `serve_app_with_hooks` MUST answer the current request when assembly fails, with the error's HTTP status (for example 503 for `EdgeError::service_unavailable`) and EdgeZero's standard error body, through EdgeZero's response delivery, and MUST keep serving so the next request retries. EZ-17 reports this response as a detached response. Because `configure` did not run, the response carries none of the application's configured error rendering or EZ-19 headers, and the guide MUST say so. +- **EZ-22.8** Fastly `run_app`, `run_app_with_hooks`, and `run_app_with_config`, Cloudflare `run_app`, and Spin `run_app` and `run_app_with_platform` SHOULD answer a failed assembly as EZ-22.7 does instead of failing with a host-generic error. Axum `run_app` MUST keep failing startup before it binds the listener. +- **EZ-22.9** The failure response MUST NOT expose internal error detail. Internal errors MUST render as F renders them today, "internal server error" (`edgezero:crates/edgezero-core/src/error.rs:555-575`). EdgeZero MUST log the full error once per failed attempt on its boot log target. +- **EZ-22.10** Applications declared through the `app!` macro SHOULD be able to declare fallible state with the outcomes of EZ-22.1 and EZ-22.6. +- **EZ-22.11** An assembled app MUST stay safe to retain and share across overlapping requests, as it is at F (`edgezero:crates/edgezero-core/src/app.rs:987-995`). +- **EZ-22.12** A Fastly custom serving callback whose assembly failed MUST be able to answer the current request with that error's HTTP status and EdgeZero's standard error body through EdgeZero's response delivery, without an assembled app, and keep serving so the next request retries. EZ-17 reports this response as a detached response, and EZ-22.9 applies to it. + +#### Acceptance criteria + +1. Environment is unit, host. Given an application whose route building fails with a 503 error and whose `configure` counts calls, when `App::build` runs, then it returns the 503 error and the count is 0. +2. Environment is unit, host. Given state registered as a shared value, when a handler extracts it through `State` during canonical dispatch and entry-point code reads it from the assembled app, then both see the same shared value. +3. Environment is unit, host. Given a `configure` that sets ingress head limits from registered state, when the app is assembled, then the app's head limits are the ones in the state. +4. Environment is unit, host. Given a router the application built itself, when it is assembled through the EZ-22.5 path, then `configure` runs once, the name is the application's, and a `configure` that changes platform metadata fails exactly as it does under `App::build`. The existing cases at `edgezero:crates/edgezero-core/src/app.rs:1022-1127` pass for both paths. +5. Environment is unit, host. Given `lifecycle::Sandbox` with an assembly that fails once and then succeeds, when two requests arrive, then after the first nothing is retained and one build was attempted, and after the second the app is retained and two builds were attempted. +6. Environment is Viceroy 0.21.0. Given TS's composition, `serve_custom` with `lifecycle::Sandbox`, and an application whose assembly fails while a flag is set, when request one arrives with the flag set and request two arrives in the same sandbox with the flag cleared, then request one gets 503 with EdgeZero's JSON error body and nothing is retained, and request two gets 200 with the same instance identity, the next ordinal, and two builds recorded. +7. Environment is workerd and Spin 4.1. Given the documented retention recipe and a build that fails once, when two requests arrive in one instance, then the first gets the failure, nothing is retained, and the second succeeds and is retained (EZ-22.6). Only if EZ-22.8 is adopted, given a failing route build, when `run_app` serves a request, then the client gets 503 with the JSON error body, not a host-generic 500. EdgeZero records a passing run of this criterion for the pinned commit. +8. Environment is unit, host, only if EZ-22.10 is adopted. Given an `app!` application with fallible state that fails, when `App::build` runs, then it fails and nothing is assembled. +9. Environment is Viceroy 0.21.0 with TS's reusable-sandbox feature. Given a config key pointing at a missing blob, when request one arrives, then EdgeZero's 503 is served through EZ-22.12. Given the blob restored, when request two arrives in the same sandbox, then it gets 200 with the same `x-ts-sandbox-instance` and `x-ts-sandbox-builds` one higher (`ts:crates/trusted-server-adapter-fastly/src/sandbox.rs:21-31`). TS no longer keeps its own app-and-state pair or its startup-error router. +10. Environment is workerd. Given TS's Cloudflare entry with the retention recipe, when ten sequential requests report the same isolate identity, then the state is built once. TS no longer keeps the Cloudflare thread-local. +11. Environment is Viceroy 0.21.0. Given `serve_app` and an assembly that fails while a flag is set, when request one arrives with the flag set and request two arrives in the same sandbox with the flag cleared, then request one gets 503 with the JSON error body and request two gets 200 with two builds recorded. + +#### Out of scope + +- Refreshing retained state when configuration changes. Staleness stays an application decision, as the guides already say (`edgezero:docs/guide/adapters/overview.md:229-233`). +- Asynchronous route building inside EdgeZero's entry points. EZ-22.5 covers asynchronous state, because the application can finish its own reads before assembling. +- A framework-owned retention cell for Cloudflare and Spin. The documented recipe stays application-owned. +- Passing platform metadata to route building. + +#### Open questions + +1. Should route building see the platform metadata, as `configure` does? TS prefers no for now. Nothing in TS needs it, and `configure` can choose platform-dependent policy after state exists. +2. Should Fastly `serve_app` stop serving after some number of consecutive assembly failures? TS prefers no. `Serve` request and lifetime limits already bound the loop, and stopping only moves the retry to a cold sandbox. + +#### Interim for Trusted Server + +TS keeps its own retained app-and-state pair on Fastly. It assembles with `App::with_name_and_platform(router, name, FASTLY_PLATFORM)` followed by an explicit `TrustedServerApp::configure(&mut app)?`, because `App::build` cannot return state, and it keeps the startup-error router as the failed-initialization value that serves one request. On Cloudflare and Spin TS keeps assembling per request through `run_app`, as it does at v0.0.9, and does not adopt the retention recipe, because the startup-error router would be retained. No transport code is involved. + +### EZ-23. Axum dev server applies application policies, and tests can run real Axum ingress + +**Priority:** Follow-up +**Adapters:** Axum. The test facility may serve an app assembled for another adapter's platform, without that adapter's provider request context. +**Related:** EZ-22 (TS assembles test apps from explicit settings with production policies), EZ-2 (router state already decides which outbound client a handler gets), EZ-24 (route tests supply the scripted client), EZ-6 (handlers that begin outbound requests must work under the test facility), EZ-1, EZ-10, EZ-18 (TS checks bodiless forwarding, 413, and `Content-Length` framing on host here) + +#### Need + +TS's Axum route tests drive the full router through `EdgeZeroAxumService::new(router)` and `tower::ServiceExt::ready().call(request)` (`ts:crates/trusted-server-adapter-axum/tests/routes.rs:1-11`, `:42-49`, `:86-100`), 28 tests in 972 lines. The cross-adapter parity suite uses the same service for Axum (`ts:crates/trusted-server-integration-tests/tests/parity.rs:15`, helpers at `:74-124`, `:224-243`, `:272-289`) and sends Cloudflare and Spin requests straight to `router.oneshot` (`:126-222`, `:244-271`, `:290-316`). TS's Axum binary hands a router to `AxumDevServer::with_config(router, config)` (`ts:crates/trusted-server-adapter-axum/src/main.rs:29-45`) so it binds the `PORT` its integration runner allocates (`ts:crates/trusted-server-integration-tests/tests/environments/axum.rs:59-67`). + +On PR 275 TS wants its route and parity tests to run through real Axum ingress, meaning admission, head limits, framing, and streamed bodies, with production `configure` policies and a scripted outbound client in place of the network. Without that the tests either stop compiling or move to `router.oneshot`, which never exercises admission, head limits, response framing, or egress policy on any adapter, and TS loses its host-side check for EZ-1, EZ-10, and EZ-18. + +The binary does not need this requirement. EdgeZero's `run_app` assembles through `App::build` and reads its bind address from `EDGEZERO__ADAPTER__HOST` and `EDGEZERO__ADAPTER__PORT` (`edgezero:crates/edgezero-adapter-axum/src/dev_server.rs:498-502`, `:687-689`). Any programmatically configured server, though, silently drops `configure` today. + +#### Gap at F + +- The adapter's request service is private (`edgezero:crates/edgezero-adapter-axum/src/lib.rs:29-30`, `edgezero:crates/edgezero-adapter-axum/src/service.rs:45-77`), and its tower entry point exists only in the adapter's own tests (`:337-355`). At v0.0.9 it was public (`edgezero@v0.0.9:crates/edgezero-adapter-axum/src/service.rs:24-37`, `:126`). +- `AxumDevServer::new` and `with_config` take a `RouterService` (`edgezero:crates/edgezero-adapter-axum/src/dev_server.rs:86-102`, `:160-168`) and serve it as `App::new(router)` (`:133-140`), which installs default policies and the default name (`edgezero:crates/edgezero-core/src/app.rs:323-326`) and never runs `configure`. Only `run_app` assembles through `App::build` (`edgezero:crates/edgezero-adapter-axum/src/dev_server.rs:440-445`, `:491`). +- Normalized head limits come from the app (`edgezero:crates/edgezero-adapter-axum/src/service.rs:225`), so a programmatic server always enforces the defaults of 65536 header bytes, 100 headers, and 8192 target bytes (`edgezero:crates/edgezero-core/src/ingress.rs:21-23`, `:409-418`). The adapter's raw ingress settings still apply, with `AxumIngressConfig` defaults of 64 KiB raw head, 100 fields, a 10 s head timeout, and 256 connections (`edgezero:crates/edgezero-adapter-axum/src/ingress_config.rs:28-38`). +- Serving on an already-bound listener exists but only for the adapter's own tests (`edgezero:crates/edgezero-adapter-axum/src/dev_server.rs:143-158`). The `test-utils` feature is empty, and the test module holds only an environment mutex (`edgezero:crates/edgezero-adapter-axum/src/lib.rs:35-36`, `edgezero:crates/edgezero-adapter-axum/src/test_utils.rs:1-14`). +- A client registered with `RouterBuilder::with_state` already wins over the adapter's per-request outbound client (EZ-2.14, `edgezero:crates/edgezero-core/src/router.rs:407-411`). The adapter still builds a real outbound client during request conversion even then, and fails the request if that build fails (`edgezero:crates/edgezero-adapter-axum/src/request.rs:67-75`). +- Handlers run on a local executor inside the serving thread (`edgezero:crates/edgezero-adapter-axum/src/dev_server.rs:371-422`). In-process dispatch outside that thread has no local executor for work a handler spawns. + +#### Requirements + +- **EZ-23.1** Every way to run an Axum dev server programmatically, including a custom bind address and custom store wiring, MUST serve an assembled application with all of its `configure` policies applied. A construction path MUST NOT drop them silently. +- **EZ-23.2** The policies in EZ-23.1 are at least ingress head limits, the admission policy, the error renderer, the response egress policy and observer, the detached-response decision, config extraction limits, the application clock, and the application name. +- **EZ-23.3** For the same application and the same configuration, `run_app` and the programmatic path MUST behave identically. +- **EZ-23.4** Under the adapter's test utilities, an application MUST be able to run its assembled app in-process, from a standard `#[tokio::test]`, through the same ingress, admission, head limits, inbound read deadlines, response framing, streaming, write deadlines, and egress completion as the production server. Work a handler spawns, including EZ-6's begun requests and the chunks of a streamed response they feed, MUST complete as in production. +- **EZ-23.5** A test MUST be able to learn the server's address, stop it without blocking or leaking connections, and read the serving result. +- **EZ-23.6** Any form of the facility that bypasses part of the production path MUST document what it skips. +- **EZ-23.7** When router state supplies an outbound client (EZ-2.14), the real outbound transport MUST NOT be needed or initialized. +- **EZ-23.8** The dev server and the test facility MUST accept an app assembled with any platform metadata, so host parity tests can run a Cloudflare or Spin app's router and policies through Axum ingress. The docs MUST say that provider request contexts are absent in that mode. +- **EZ-23.9** The dev server SHOULD log one startup warning when the app's normalized head limits exceed the raw ingress budget, because the raw parser rejects first. +- **EZ-23.10** Removed. +- **EZ-23.11** Removed. + +#### Acceptance criteria + +1. Environment is Axum, native. Given an application whose `configure` limits the request target to 64 bytes, when a 100-byte target arrives through the programmatic dev server and through the test facility, then both answer 414. Given the same router served without `configure` at F, the request succeeds, which shows the gap. +2. Environment is Axum, native. Given a `configure` that installs an error renderer adding a marker header, when an unmatched path arrives, then the 404 carries the marker. +3. Environment is Axum, native. Given an admission policy that aborts, when a request arrives, then the connection closes with no response bytes and no handler or middleware runs. +4. Environment is Axum, native. Given a client that uploads 1 MiB with chunked framing, when a handler reads the body, then it sees every byte. Given a client that sends headers and then stops sending the body past the inbound read budget, then it gets 408. +5. Environment is Axum, native. Given a handler that streams three chunks 50 ms apart, when a client reads, then the first chunk arrives before the third is produced and the wire uses chunked transfer coding. +6. Environment is Axum, native. Given a buffered 5-byte response, when it is served, then it carries `content-length: 5`. +7. Environment is Axum, native. Given a HEAD request to a route that returns a body, then the wire carries headers and no body bytes. +8. Environment is Axum, native. Given raw limits of 4 header fields, when a request with 5 fields arrives, then it is rejected before admission and the server keeps serving. +9. Environment is Axum, native. Given a handler mid-stream, when the test stops the server, then the handler's resources are released and the serving result reports success. +10. Environment is Axum, native. Given an outbound client supplied through router state on a host with no outbound network access, when a handler sends a request, then the supplied client sees it, no socket opens to the target, and no request fails because the real transport could not start. +11. Environment is Axum, native. Given an app assembled with Cloudflare's platform metadata, when it runs on the test facility, then its `configure` policies apply. +12. Environment is Axum, native. Given a plain `#[tokio::test]`, when a handler spawns local work (and, once EZ-6 lands, begins an outbound request against a supplied client), then it completes, and a streamed body fed by that work delivers every chunk. +13. Environment is Axum, native. Given TS's route tests and parity suite on the test facility with apps assembled from explicit settings with production policies (EZ-22.5), and Cloudflare and Spin parity moved off `router.oneshot` per EZ-23.8, then a 9000-byte request target, which TS does not override, gets 414 from all three apps, and a bodiless GET whose inbound body is an unread stream reaches a local origin with no request body, which is TS's host check for EZ-1. + +#### Out of scope + +- A general embeddable request service outside test utilities, which would bypass the bounded HTTP/1 ingress the adapter owns (`edgezero:docs/guide/adapters/axum.md:82-88`). +- A host test facility for Cloudflare or Spin. EZ-23.8 covers host parity through Axum ingress. +- Any change to the capability matrix. `raw-ingress-head-limits` and `raw-ingress-framing-validation` stay Unsupported for Axum (`edgezero:docs/guide/capabilities.md:75-76`). + +#### Open questions + +1. Should the test facility be usable outside the adapter's test utilities, for example to run an app under a different supervisor? TS prefers no until someone asks for it outside tests. + +#### Interim for Trusted Server + +TS moves its Axum route tests and all three adapters' parity helpers onto `router.oneshot`, which compiles at F and covers routing, middleware, and handlers but not admission, head limits, framing, or egress. TS accepts that loss in unit tests and keeps the ingress-level checks for EZ-1, EZ-10, and EZ-18 in its integration suite, which already launches the real Axum binary over TCP (`ts:crates/trusted-server-integration-tests/tests/environments/axum.rs:59-67`). The binary moves to `run_app::()` with `owns_logging` set, and the runner sets `EDGEZERO__ADAPTER__PORT`. No transport code is added. + +### EZ-24. Published outbound test double that behaves like production + +**Priority:** Follow-up +**Adapters:** core. The double must behave the same on host, `wasm32-wasip1`, and `wasm32-unknown-unknown`. +**Related:** EZ-6 (begun requests, non-blocking checks, and arrival-time rules must hold through the double), EZ-9 (redirect following must reach the double as one ordinary send per hop), EZ-15 (fire-and-forget sends must be recorded once they exist), EZ-1, EZ-5, EZ-7, EZ-10 (request checks, response handling, and the marker must match adapters as those requirements change them), EZ-23 (route tests supply the double). No acceptance criterion of EZ-1 to EZ-9 depends on this double. + +#### Need + +TS deletes its outbound layer and every test double built on it. Test code holds 23 `impl PlatformHttpClient` or `impl PlatformBackend` blocks across nine files, among them `ts:crates/trusted-server-core/src/platform/test_support.rs:136`, `:155`, `:186`, `:496`, six in the orchestrator's tests (`ts:crates/trusted-server-core/src/auction/orchestrator.rs:3029-3681`), and others in `proxy.rs:2391-2452`, `publisher.rs:16332` and `:20834`, `integrations/prebid.rs:3720`, `integrations/sourcepoint.rs:1175`, and `test_support.rs:252-381` under `ts:crates/trusted-server-core/src/`, plus `app.rs:2717-2894` and `tinybird.rs:281-318` under `ts:crates/trusted-server-adapter-fastly/src/`. All of them move onto one scripted `OutboundHttpClient`. + +TS's tests need that client to: + +- record each request's parts, so one test per call site pins method, URI, timeout, deadline, response mode and cap, cache policy, authority override, request body cap, and the options other EZ requirements add; +- script responses, transport errors, timeouts with the right budget source, and response-limit errors; +- script batch completions at arrival times, including cutoff and a mid-batch driver failure, so the auction's rule that a bid arriving at deadline minus 1 ms is kept and a bid arriving at the deadline is dropped is tested exactly as production applies it; +- overlap an origin request begun under EZ-6 with an auction batch checked without blocking at body-chunk boundaries, and show that a late check never turns an on-time bid into a cutoff; +- script the hops of a redirect-following call (EZ-9) and observe the per-hop target policy through the order of recorded requests; +- reject what adapters reject before sending, such as a GET or HEAD carrying body bytes, a streamed response in a batch, or an option a send mode does not support. + +Without a published double TS writes its own, as EdgeZero's scaffold does (`edgezero:crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs:332-444`), and the copy drifts from adapter behavior. A hand-written double that does not apply the shared request checks and response policy behaves differently from every adapter for a GET with a streamed body or a gzip response. + +#### Gap at F + +- The core test-utilities feature exposes environment guards and in-memory store fixtures (`NoopKvStore`, `InMemorySecretStore`) only (`edgezero:crates/edgezero-core/src/lib.rs:47-50`, `edgezero:crates/edgezero-core/Cargo.toml:57-60`, `edgezero:crates/edgezero-core/src/key_value_store.rs:951`, `edgezero:crates/edgezero-core/src/secret_store.rs:177`). No outbound double and no controllable clock are published, although `MonotonicClock::new` accepts any time source (`edgezero:crates/edgezero-core/src/time.rs:46-53`). +- The public pieces an application can assemble a double from exist, for example `OutboundBatch::from_driver` (`edgezero:crates/edgezero-core/src/outbound.rs:268-287`), `OutboundResponse::new` (`:801-828`), `OutboundRequest::into_parts` (`:1025-1047`), `validate_for_dispatch` (`:1544-1583`), `apply_response_body_policy` (`:1252-1300`), and `finish_batch_item` (`:1307-1330`). Adapter-identical behavior from them requires repeating adapter-private steps, such as the batch eligibility checks copied into all four adapters (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:104-121`, `edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:1511-1516`, `edgezero:crates/edgezero-adapter-spin/src/outbound.rs:1421-1426`, `edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:2577-2582`) and each adapter's response handling order (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:307-397`). +- At F, lazy adapters take a slot's completion time when its exchange future is polled to completion (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:200-203`), and core drains such futures only when the batch is polled (`edgezero:crates/edgezero-core/src/outbound.rs:313-329`). EZ-6 replaces that with arrival time. A double modeled on F would pin the old behavior. +- The scaffold's double emits every batch item at once with fixed elapsed values and empty bodies (`edgezero:crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs:337-380`). It cannot express latency, cutoff, driver failure, or streaming. +- CI runs no core tests on a wasm target. The wasm matrix runs adapter contract tests only (`edgezero:.github/workflows/test.yml:220-241`, `:366`). + +#### Requirements + +- **EZ-24.1** EdgeZero MUST publish, behind an opt-in that production builds do not enable, a scripted outbound client usable wherever an `HttpClient` is accepted, and a controllable clock that the application, its requests, and the scripted client share. +- **EZ-24.2** The scripted client MUST behave identically on host, `wasm32-wasip1`, and `wasm32-unknown-unknown`, and MUST NOT depend on real time, threads, or a particular async runtime. +- **EZ-24.3** It MUST record every dispatched request, in dispatch order, with every field `OutboundRequest::into_parts` exposes (including fields other EZ requirements add), the request body bytes, the dispatch instant, the effective budget and its source, and the send mode, meaning single send, begun request, batch slot with its batch and index, or fire-and-forget send once EZ-15 exists. +- **EZ-24.4** It MUST apply the shared portable preflight checks with the same error category, status, and precedence as adapters. That includes the GET and HEAD body rules of EZ-1, batch eligibility, options universally Unsupported on the selected send mode, invalid combinations such as Image Optimizer with bypass or tags, request body caps, and an already-exhausted budget. It MUST NOT emulate a particular adapter's capability profile: a single-send authority override or otherwise valid Image Optimizer option is accepted and recorded even when some adapters reject it, while Image Optimizer on begin or batch is rejected because that send mode is Unsupported everywhere. Its documentation MUST distinguish portable conformance from provider-specific validation. When another requirement changes shared checks, the scripted client MUST follow without separate work by the application. +- **EZ-24.5** For a scripted upstream response it MUST give the application what an adapter gives for the same upstream bytes. That means the same header limits and hop-by-hop normalization, bodyless and 205 handling, decoding with encoded, decoded, and final caps, and streamed or buffered delivery, with the same statuses, headers, bytes, and `ResponseLimitReason`. It MUST apply EZ-7's marker default and opt-out exactly as adapters do, with the value `test` when the marker is present. +- **EZ-24.6** A test MUST be able to script, per request, a response (status, headers, and a buffered or chunked body with per-chunk timing), a transport error, or no answer. It MUST be told about requests nothing scripted and scripts nothing consumed. An unscripted request MUST fail with an internal error naming its method and URI, without a panic. +- **EZ-24.7** Time in the double MUST be deterministic. A scripted reply arrives at its dispatch instant plus its latency. Lateness, budgets, the cutoff boundary, delivery order, and elapsed time MUST follow cross-cutting requirement 5 and EZ-6.11 to EZ-6.16 as adapters apply them to recorded arrivals. A non-blocking check MUST return only what has already arrived and MUST NOT move the clock. The header phase uses the effective request budget. Without EZ-14's idle mode, a streamed body chunk at or after the request's budget deadline ends the body with the attributed gateway timeout. With idle mode, per-call and default budgets stop governing the body after headers; nonempty upstream-byte gaps and any applicable absolute deadline instead govern it exactly as EZ-14 requires. How the clock advances while the application waits is EdgeZero's choice, provided every wait ends. +- **EZ-24.8** A test MUST be able to script a batch driver failure after a given number of items, delivered to the application as the batch failure F already defines. +- **EZ-24.9** The scripted client MUST NOT follow redirects itself. It MUST support EZ-9.14's shared head-first single-request dispatch contract so the following call applies policy before reading or validating a followed body, and tests observe one dispatch per hop. Final responses still receive the shared response policy of EZ-24.5. Begun requests and batches never follow redirects. +- **EZ-24.10** A test MUST keep access to the recordings and the script after handing the client to the application. +- **EZ-24.11** Dropping or cancelling a begun request or a batch through the double MUST behave as cross-cutting requirement 6 requires of adapters. Scripts for abandoned requests MUST NOT be reported as unconsumed. +- **EZ-24.12** EdgeZero's generated application template MUST use the published double instead of its own. + +#### Acceptance criteria + +All applicable criteria run on host. EdgeZero records passing runs for the pinned commit on `wasm32-wasip1` under a WASI preview 1 runner (the pinned wasmtime or Viceroy 0.21.0). Only when the `wasm32-unknown-unknown` part of EZ-24.2 is adopted, criteria 3 to 8 additionally run under a JavaScript host such as workerd or Node. Criterion 10 runs natively. Partial adoption follows EZ-Q1.8: a host/WASI-only promotion does not require a deferred JavaScript leg, and criteria explicitly conditional on follow-up parts apply only when those parts are promoted or present. + +1. Environment is as above. Given valid GET requests that together exercise every request option, when they are sent, then the recordings show identical parts. Mutually incompatible options, such as Image Optimizer and cache tagging, are exercised on separate valid requests; an invalid combination is rejected without dispatch. Given a POST with a 3-byte body, then the recording shows those bytes. +2. Environment is as above. Given a GET whose inbound body carries bytes, when it is forwarded, then it is rejected with the same error as on an adapter. Given a batch slot that asks for a streamed response, then that slot fails with the adapters' batch-eligibility error and its siblings proceed. +3. Environment is as above. Given a 100 ms timeout and a reply scripted at 150 ms, when the request is sent, then it ends as a gateway timeout with per-call-timeout source, recorded at 100 ms. Given no answer and a 2 s deadline, then it ends as a gateway timeout with request-deadline source at 2 s. +4. Environment is as above. Given three batch slots arriving at 30, 10, and 20 ms, draining through `next` returns indices 1, 2, 0 with elapsed 10, 20, and 30 ms, followed by `Completed`. Collecting an equivalent batch with `collect()` instead returns an input-index-aligned vector: indices 0, 1, 2 have elapsed 30, 10, and 20 ms respectively. +5. Environment is as above. Given a 100 ms cutoff, a slot arriving at 99 ms, and a slot arriving at 100 ms, when the batch is collected, then the first is kept with elapsed 99 ms, the second is unresolved, and the batch ends at the cutoff. Given the 99 ms slot observed only by a check at 101 ms, then it is still kept with elapsed 99 ms. +6. Environment is as above. Given a driver failure scripted after one item, when the batch is collected, then the application gets one item and then the failure, with exactly one slot filled. +7. Environment is as above, once EZ-6 lands. Given a slot arriving at 10 ms, when the application checks without blocking at 0 ms, then nothing is returned. When the clock is advanced to 15 ms and it checks again, then the slot is returned with elapsed 10 ms. +8. Environment is as above, once EZ-6 lands. Given an origin request begun under EZ-6 whose head arrives at 300 ms, and a batch with a 250 ms cutoff whose slots arrive at 50, 100, and 200 ms, when the application checks the batch at 60, 120, and 210 ms, then all three bids are kept. Given a single check at 260 ms instead, then the same three bids are still kept. When the application then waits for the origin, the wait ends with the origin's head. +9. Environment is as above, once EZ-9 lands. Given a 302 then a 200 scripted for a redirect-following call, then two requests are recorded in hop order. A followed 302 whose body is oversized, malformed compressed data, failed, or permanently pending does not fail or delay the final 200. When the same 302 is final, ordinary caps and decoding apply. Given a target policy that rejects the second target, then one request is recorded and the call returns EZ-9's rejection. +10. Environment is Axum, native. Given the same gzip body served by a local origin to the Axum adapter and scripted for the double, then status, decoded bytes, and headers match except the proxy marker value, and an over-limit body fails with the same `ResponseLimitReason` on both. +11. Environment is as above. Given an unscripted request, when it is sent, then it fails with an internal error, the test is told about it, and nothing panics. +12. Environment is as above, only when EZ-24.11 is promoted or present. Given a begun request dropped before its reply arrives, then the test is not told about an unconsumed script for it. +13. Environment is host, Trusted Server. TS has no scripted client of its own. Its per-call-site option tests, batch outcome mapping tests, and cutoff boundary tests run on the published double, and its orchestrator tests that used stub clients migrate to it. +14. Environment is host and `wasm32-wasip1`, once EZ-14 lands; also `wasm32-unknown-unknown` when that part of EZ-24.2 is adopted. Given headers at 10 ms, a 500 ms per-call timeout, a 400 ms idle bound, and nonempty upstream chunks every 250 ms, the full streamed body continues past 500 ms. A later 600 ms upstream gap produces the idle-timeout cause at 400 ms of waiting, while an explicit 1 s absolute deadline ends a still-progressing body at 1 s with the deadline cause. +15. Environment is host and `wasm32-wasip1`; also `wasm32-unknown-unknown` when that part of EZ-24.2 is adopted. Single-send authority override and Image Optimizer options are accepted and recorded under the portable test profile. Image Optimizer on begin or batch is rejected before an expired-deadline check; a valid sibling still proceeds. Documentation says that accepted single-send provider options do not establish support on every real adapter. + +#### Out of scope + +- Emulating a specific adapter's capability rejections, such as Spin refusing an authority override (`edgezero:crates/edgezero-adapter-spin/src/outbound.rs:1433-1441`). EZ-24.4 defines a shared portable profile, not a selectable provider profile. +- Returning hand-built responses that skip the shared response handling. +- Real-time scheduling or any dependency on wall-clock sleeps. + +#### Open questions + +1. Should the double's marker value be `test`? TS prefers yes, so tests can tell double-produced responses from adapter ones. If EZ-7.6 exposes which adapter completed an exchange, the double reports `test` there too. +2. Which part of EZ-24 is the minimal core the head proposes to promote? TS's answer is EZ-24.1 to EZ-24.10, with EZ-24.2 limited to host and `wasm32-wasip1`. EZ-24.11, EZ-24.12, and the `wasm32-unknown-unknown` part of EZ-24.2 stay follow-up. + +#### Interim for Trusted Server + +TS writes its own scripted `OutboundHttpClient` as test code in `ts:crates/trusted-server-core/src/test_support.rs`, following the same arrival-time rules, and deletes it when EZ-24 lands. It is test code, not transport code. + +### EZ-25. Documentation drift fixes + +**Priority:** Follow-up +**Adapters:** documentation for core, Fastly, Axum, Cloudflare, and Spin +**Related:** EZ-1, EZ-5, EZ-7 (EZ-25.1 owns the forwarding example and must show what EZ-1.14, EZ-5.14, and EZ-7.8 require), EZ-22 and EZ-23 (EZ-25.24, EZ-25.25, and EZ-25.37 change again when they land), EZ-2 (EZ-25.7 describes backend identity at F) + +#### Need + +TS ports four adapters by reading these guides, and several of the items below produce a wrong port. The forwarding example in `proxying.md` is the template for TS's publisher origin fetch. As written it does not compile, and once fixed it returns 400 for every publisher GET (EZ-1). The reusable-app README shows a six-argument lifecycle helper that does not exist. The Fastly guide describes delivery through the SDK's `stream_to_client`, which the same guide says the standard path cannot use (`edgezero:docs/guide/adapters/fastly.md:347-349`), so a reader cannot tell which commit behavior applies to TS's finalizer and post-send work. The CHANGELOG says Rust 1.95.0 is still the workspace toolchain while `.tool-versions` pins 1.98.1, and TS pins 1.95.0 (`ts:rust-toolchain.toml:2`). The capability declaration example cannot pass the gate on Fastly, Cloudflare, or Spin, and it is the first thing TS copies into its manifest. + +#### Gap at F + +EdgeZero checks some documentation in CI with `scripts/check_outbound_docs_contract.mjs` (`.github/workflows/test.yml:78`, `.github/workflows/format.yml:228`) and `scripts/check_outbound_legacy_api.sh` (`.github/workflows/test.yml:75`). The docs contract check pins the stale phrase "accounts synchronous body-handle writes through `stream_to_client` close" (`edgezero:scripts/check_outbound_docs_contract.mjs:1610`), pins the Limits table rows exactly (`expectedLimitRows`, `:199-240`, failing on any difference at `:1670-1673`), and requires `axum.md` to contain `EdgeZeroApp::build::(edgezero_adapter_axum::AXUM_PLATFORM)` (`:1069-1078`), which appears only in the Testing sample (`edgezero:docs/guide/adapters/axum.md:280`). Nothing compiles the guides' Rust snippets, none of which is marked `ignore`, which is why EZ-25.2, EZ-25.14, EZ-25.16, and EZ-25.17 are compile errors no check caught. Each item below cites the code that contradicts the guide. + +#### Requirements + +Each item gives the location at F, the current text, and the evidence. The requirement is that the guide states what the evidence shows. The suggested wording is TS's proposal, and EdgeZero may word it differently, provided the result passes this requirement's acceptance criteria and EdgeZero's documentation checks. No item edits `docs/superpowers/specs/`. + +Outbound guides: + +- **EZ-25.1** `docs/guide/proxying.md:9-53`, the "Forward An Inbound Request" example, which this item owns. At F it fails for GET and HEAD, because ingress always hands the router a lazy `Body::Stream` (for example `edgezero:crates/edgezero-adapter-axum/src/request.rs:43-49`), `from_request` keeps that body (`edgezero:crates/edgezero-core/src/outbound.rs:927-934`), and `validate_for_dispatch` rejects any GET or HEAD `Body::Stream` (`:1561-1574`). The example MUST compile, MUST forward a bodiless GET or HEAD once EZ-1 lands, and MUST show what EZ-1.14, EZ-5.14, and EZ-7.8 require. Until EZ-1 lands, the guide MUST say that the example works only for methods that carry a body, and it MUST NOT show a workaround that drops a GET or HEAD body that carries bytes (EZ-1.8). Suggested wording, in both cases, for the sentence at `:9-10` is "This example preserves the inbound method, forwards the inbound body for methods that carry one, replaces the target URI, removes a private header, and converts the outbound response back into a core response. GET and HEAD requests are forwarded without a body." Until EZ-1 lands, add after it "At this release, use it only for methods that carry a body. A GET or HEAD forwarded this way fails with 400." +- **EZ-25.2** `docs/guide/proxying.md:32`. Now `OutboundRequest::from_request(ctx.into_request(), target)?`. `RequestContext::into_request` returns `Result` (`edgezero:crates/edgezero-core/src/context.rs:381`), so the sample does not compile. Suggested replacement is `OutboundRequest::from_request(ctx.into_request()?, target)?`. +- **EZ-25.3** `docs/guide/proxying.md:144-145`. Replace the first sentence, through `results.termination`. Now "`client.send_all_until(requests, observation_cutoff).await` collects the same driver into an index-aligned `Vec>` plus `results.termination`." The return type at F is `OutboundBatchResults { slots, termination }` (`edgezero:crates/edgezero-core/src/outbound.rs:133-140`). Describe the result struct rather than implying the method returns a vector. After EZ-6's hard cut, the guide MUST describe index-aligned typed states for already-delivered, newly resolved, and unresolved slots on both success and failure, including mixed observation/collection and the optional batch-bound accumulator. It MUST NOT retain the old Option shape or describe an already-delivered outcome as an unresolved timeout. +- **EZ-25.4** `docs/guide/streaming.md:116-128` and `docs/guide/capabilities.md:415-420`. The decompression text never says that every adapter sends `accept-encoding: identity` when the request sets none (`edgezero:crates/edgezero-adapter-axum/src/outbound.rs:59-61`, `edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:266-268`, `edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:754-757`, `edgezero:crates/edgezero-adapter-spin/src/outbound.rs:356-358`), so the example at `streaming.md:122-128` asks for identity and rarely exercises decompression. After the sentence ending at `streaming.md:118` and after the paragraph ending at `capabilities.md:420`, suggested addition is "Every adapter sends `accept-encoding: identity` when the request has no `accept-encoding` header, so set one explicitly to receive a compressed response." In the example, after `OutboundRequest::get("https://api.example.com/data")?` at `streaming.md:123`, insert `.header("accept-encoding", "gzip, br")?`. EZ-5.14 governs this text once EZ-5 lands. +- **EZ-25.5** `docs/guide/streaming.md:58-60`. Now "Spin's `dispatch_app::(&app, req)` returns a raw `SpinResponse` whose body and transmission lifetime remain owned by the adapter coordinator." The signature is `-> anyhow::Result` (`edgezero:crates/edgezero-adapter-spin/src/lib.rs:215`). Suggested replacement is "Spin's `dispatch_app::(&app, req)` returns `anyhow::Result`, and the response body and transmission lifetime remain owned by the adapter coordinator." +- **EZ-25.6** The guides and CHANGELOG MUST NOT describe Fastly delivery as `stream_to_client`. They MUST describe what EdgeZero guarantees about commit, framing selection, and body-handle close. Naming the hostcalls is optional. At F the adapter commits through a raw status-returning hostcall, not the SDK's `stream_to_client` (`edgezero:crates/edgezero-adapter-fastly/src/fastly_response_abi.rs:64-70`, called from `edgezero:crates/edgezero-adapter-fastly/src/response.rs:128-137`), appends every header value (`fastly_response_abi.rs:19-34`), and selects manual framing when a transmitted body declares `Content-Length` (`response.rs:159-161`). Locations and suggested wording follow. + - `docs/guide/adapters/fastly.md:468-470`. Now "append every header value, commit once through `stream_to_client`, account accepted body bytes, and finish or abandon the body handle exactly once." Suggested replacement is "append every header value, select manual framing when a transmitted body declares `Content-Length`, commit the head once, account accepted body bytes, and finish or abandon the body handle exactly once." + - `docs/guide/adapters/fastly.md:69`. Replace "owns `stream_to_client` through final body-handle close." with "owns the streaming response through final body-handle close." + - `docs/guide/adapters/fastly.md:201`. Replace "EdgeZero owns the low-level `stream_to_client` lifetime and writes portable" with "EdgeZero owns the streaming response lifetime and writes portable". + - `docs/guide/streaming.md:45-46`. Replace "Fastly `stream_to_client`," with "Fastly an adapter-owned streaming body handle,". + - `docs/guide/capabilities.md:223`. Replace "Fastly accounts synchronous body-handle writes through `stream_to_client` close" with "Fastly accounts synchronous body-handle writes through close". EZ-25.44 covers the docs contract check that pins the old phrase. + - `docs/guide/capabilities.md:284-285`. Replace "the low-level `stream_to_client` response lifetime" with "owns the streaming response lifetime". + - `CHANGELOG.md:253` is EZ-25.32. + - Keep `fastly.md:347-349`, which correctly says the SDK API is incompatible. +- **EZ-25.7** `docs/guide/adapters/fastly.md:194-195`. Now "derived from the canonical target, TLS identity, and provider timer budget". At F the backend identity also includes the wire authority (`edgezero:crates/edgezero-adapter-fastly/src/outbound.rs:254-262`). Suggested replacement is "Outbound HTTP uses deterministic **dynamic backends** derived from the canonical target, TLS identity, wire authority (including any `host_authority_override`), and provider timer budget." EZ-2's documentation replaces this text when EZ-2 lands. +- **EZ-25.8** `docs/guide/capabilities.md:11-12`. Now `required = ["outbound-deadlines"]`. The gate rejects a required capability that is only BestEffort (`edgezero:crates/edgezero-cli/src/adapter.rs:423-428`), and Cloudflare, Fastly, and Spin report `OutboundDeadlines` as BestEffort (`edgezero:crates/edgezero-adapter-cloudflare/src/cli.rs:146`, `edgezero:crates/edgezero-adapter-fastly/src/cli.rs:2052`, `edgezero:crates/edgezero-adapter-spin/src/cli.rs:153`), so the example passes only on Axum. Suggested replacement for the two lines is `required = ["outbound-cache-bypass"]` and `optional = ["outbound-deadlines", "outbound-http"]`. `outbound-cache-bypass` is Native everywhere (`edgezero:docs/guide/capabilities.md:240`). +- **EZ-25.9** `docs/guide/capabilities.md:236` and footnote ⁸ at `:294-299`. The Cloudflare `outbound-authority-override` cell points at ⁸, which never mentions authority override. Suggested addition to ⁸ is "Cloudflare sends the authority override as the `Host` header, but no deployed wire probe yet proves that the final request keeps that authority separate from the connection target, so `outbound-authority-override` also remains `BestEffort`." The code sets `Host` at `edgezero:crates/edgezero-adapter-cloudflare/src/outbound.rs:269-273`, and `proxying.md:89-92` already says the cell awaits a deployed wire probe. +- **EZ-25.10** `docs/guide/capabilities.md:383-393`, "Limits And Accounting". The 30 s default budget is not listed. `DEFAULT_NO_DEADLINE_BUDGET` is 30 s (`edgezero:crates/edgezero-core/src/time.rs:13`) and applies with `BudgetSource::Default` when neither timeout nor deadline is set, and the earliest candidate wins (`:150-166`). Suggested row is "| `timeout` / `deadline` | Per-request budget. The earliest of timeout, deadline, and batch cutoff wins | 30 s when neither is set |". EZ-25.44 covers the docs contract check that pins the table. + +Fastly lifecycle guide: + +- **EZ-25.11** `docs/guide/adapters/fastly.md:400-401`. Now "`FnMut(&mut edgezero_core::Response) -> State`". The crate root does not export `Response` (the root re-exports start at `edgezero:crates/edgezero-core/src/lib.rs:53`), and the type is `edgezero_core::http::Response` (`edgezero:crates/edgezero-core/src/http.rs:36`). Suggested replacement is "`FnMut(&mut edgezero_core::http::Response) -> State`". +- **EZ-25.12** `docs/guide/adapters/fastly.md:460-461`. Now "injects fresh `Kv`, `Config`, `Secrets`, and `AppConfig` registries". Only config, KV, and secret registries are inserted (`edgezero:crates/edgezero-adapter-fastly/src/request.rs:395-415`), and `AppConfig` is an extractor over the config registry. Suggested replacement is "injects fresh KV, config, and secret registries, which back the `Kv`, `Config`, `Secrets`, and `AppConfig` extractors,". +- **EZ-25.13** `docs/guide/adapters/fastly.md:588-590`. Now "Use `lifecycle::run_custom(Request::from_client(), callback)` for the single-request branch, initializing the Fastly ABI before receiving that request." `run_custom` initializes the ABI inside its body, after its argument has already received the request (`edgezero:crates/edgezero-adapter-fastly/src/lifecycle.rs:148-156`), and the fixture calls `fastly::init()` first (`edgezero:tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs:277-282`). Suggested replacement is "For the single-request branch, call `fastly::init()` and then `lifecycle::run_custom(Request::from_client(), callback)`, because the argument receives the request before `run_custom` runs. It creates". Whether `from_client` needs prior initialization is **Unverified**. A Viceroy run that omits `fastly::init()` would settle it. +- **EZ-25.14** `docs/guide/adapters/fastly.md:330-343`. The sample calls `ctx.request()`, which `RequestContext` does not have (its public methods at `edgezero:crates/edgezero-core/src/context.rs:262-616` include `extensions` at `:336` but no `request`), imports neither `Response` nor `EdgeError`, and returns nothing. `FastlyRequestContext` lives in request extensions (`edgezero:crates/edgezero-adapter-fastly/src/context.rs:11-15`). Suggested replacement for the whole sample is + + ```rust + use edgezero_adapter_fastly::context::FastlyRequestContext; + use edgezero_core::context::RequestContext; + use edgezero_core::http::Response; + use edgezero_core::{Body, EdgeError}; + + async fn handler(ctx: RequestContext) -> Result { + let client_ip = ctx + .extensions() + .get::() + .and_then(|fastly_ctx| fastly_ctx.client_ip); + Ok(Response::new(Body::from(format!("{client_ip:?}")))) + } + ``` + + `Body` is a root re-export (`edgezero:crates/edgezero-core/src/lib.rs:56`) and converts from `String` (`edgezero:crates/edgezero-core/src/body.rs:216`). + +- **EZ-25.15** `tests/fixtures/reusable-app/README.md:201`. Now "`send_request_with_registries_and_hooks(app, stores, req, &runtime.env, prepare, finalize)`". The helper takes five arguments (`edgezero:crates/edgezero-adapter-fastly/src/request.rs:557-563`). Suggested replacement is "`send_request_with_registries_and_hooks(app, stores, req, prepare, finalize)`". + +Core and cross-adapter guides: + +- **EZ-25.16** `docs/guide/handlers.md:389`. Now `use edgezero_core::{Body, EdgeError, Response};`. `Response` is not a root export (`edgezero:crates/edgezero-core/src/lib.rs:53-115`). Suggested replacement is `use edgezero_core::http::Response;` and `use edgezero_core::{Body, EdgeError};`. +- **EZ-25.17** `docs/guide/handlers.md:229-242`. The sample imports `use edgezero_core::{EdgeError, Text};`, but `Text` lives at `edgezero_core::response::Text` (`edgezero:crates/edgezero-core/src/response.rs:50`) and is not re-exported. It also uses `#[action]` without importing it and an application type `MyAppConfig` the guide never defines. Suggested replacement for the imports is `use edgezero_core::action;`, `use edgezero_core::extractor::{AppConfig, Kv};`, `use edgezero_core::response::Text;`, and `use edgezero_core::EdgeError;`. Because `MyAppConfig` is the application's own type, mark the fence `rust,ignore` and add the line `// MyAppConfig is the application's own config type, defined with #[derive(AppConfig)].` as its first line. +- **EZ-25.18** `docs/guide/middleware.md:61`. Now "Middleware are applied in order before routes are matched." The route is resolved before admission (`edgezero:crates/edgezero-core/src/app.rs:116-118`), the chain is built only for a matched entry (`edgezero:crates/edgezero-core/src/router.rs:417-418`), and 404 and 405 return without it (`:370-381`). Suggested replacement is "Middleware run in registration order after the route is resolved and the request is admitted, wrapping the matched handler. Unmatched paths (404) and unsupported methods (405) return without running middleware." +- **EZ-25.19** `docs/guide/architecture.md:104` and `:113-116`. The diagram shows "into_core_request(): Provider Request → Core Request" and "RouterService matches routes ... Response built and returned". Canonical runners convert the head first and attach the body after admission (`edgezero:crates/edgezero-adapter-fastly/src/request.rs:446-508`), and the core returns an owned `ResponseEgressEnvelope` (`edgezero:crates/edgezero-core/src/app.rs:111-145`, `:236-256`). Suggested replacement for line 104 is `│ - Head normalization: Provider head → Core request head │`, and for lines 113-116 is + ``` + │ - App resolves the route and runs ingress admission │ + │ - Middleware chain executes (matched routes only) │ + │ - Handler runs with extracted params │ + │ - Response returned in an owned egress envelope │ + ``` + Each replacement line is 63 characters, the box width. +- **EZ-25.20** `docs/guide/routing.md:187-190`. Now "the Fastly, Cloudflare, and Spin entry points call `App::build::(platform)` per request". That holds only for the default `run_app` (`edgezero:crates/edgezero-adapter-fastly/src/lib.rs:205`, `edgezero:crates/edgezero-adapter-cloudflare/src/lib.rs:181`, `edgezero:crates/edgezero-adapter-spin/src/lib.rs:199`). `serve_app` builds once per sandbox (`edgezero:crates/edgezero-adapter-fastly/src/lib.rs:255`), and `dispatch_app` never builds. Suggested replacement for lines 187-190 is "When that happens depends on the adapter. Axum builds the router once at startup, so the conflict surfaces before any traffic. The default Fastly, Cloudflare, and Spin `run_app` entry points call `App::build::(platform)` per request, and Fastly's retained `serve_app` builds on the first request in each sandbox, so a conflicting route deploys successfully and then panics on the first request. With `dispatch_app`, the panic happens wherever the caller builds the app." +- **EZ-25.21** `docs/guide/getting-started.md:94-105`. The tree shows `src/main.rs` for Cloudflare and Spin. Cloudflare scaffolds `src/lib.rs` plus a host stub `src/main.rs` (`edgezero:crates/edgezero-adapter-cloudflare/src/cli.rs:85-95`), and Spin scaffolds `src/lib.rs` and `runtime-config.toml` with no `main.rs` (`edgezero:crates/edgezero-adapter-spin/src/cli.rs:90-106`). Suggested replacement shows the Cloudflare subtree as `Cargo.toml`, `wrangler.toml`, and `src/` holding `lib.rs` (Workers fetch entrypoint) and `main.rs` (host-only stub), and the Spin subtree as `Cargo.toml`, `runtime-config.toml`, `spin.toml`, and `src/lib.rs`. +- **EZ-25.22** `docs/guide/adapters/cloudflare.md:230-231`. Now "Missing bindings log a one-time warning and the id is dropped from the registry." The id keeps a failing handle (`edgezero:crates/edgezero-adapter-cloudflare/src/request.rs:855-862`). Suggested replacement is "Missing bindings log a one-time warning, and the id keeps a failing handle whose reads return `ConfigStoreError::Unavailable`." +- **EZ-25.23** `docs/guide/adapters/overview.md:195`. Now "when `configure` or `app_state` reads values". No `app_state` API exists. The macro argument is `state = ` (`edgezero:crates/edgezero-macros/src/app.rs:61-66`). Suggested replacement is "when `configure` or the `app!` `state` expression reads values". + +Axum guide: + +- **EZ-25.24** `docs/guide/adapters/axum.md:259-261`. The text says wiring `App::build::(edgezero_adapter_axum::AXUM_PLATFORM)` and `AxumDevServer` by hand remains the fallback. At F `AxumDevServer` takes a `RouterService` and wraps it in `App::new`, so application policies are discarded (`edgezero:crates/edgezero-adapter-axum/src/dev_server.rs:93-168`), while `AxumDevServerConfig::ingress` still applies (`edgezero:docs/guide/adapters/axum.md:104-113`). Until EZ-23 lands, suggested replacement is "`AxumDevServer::with_config(router, config)` accepts a router, not an `App`, and serves it through `App::new` with default application policies, so policies installed by `Hooks::configure`, such as `App::set_ingress_head_limits`, the admission policy, and the error renderer, do not apply. `AxumDevServerConfig::ingress` still applies. Use `run_app` when those policies matter." After EZ-23 lands, the text MUST name the delivered way to run a programmatically configured Axum server and state that every `configure` policy, including ingress head limits, applies. +- **EZ-25.25** `docs/guide/adapters/axum.md:266-299`, Testing. The sample calls `app.router()` and `router.oneshot`, which skip admission, head limits, response framing, and egress policy (`edgezero:crates/edgezero-core/src/router.rs:562-569` against `edgezero:crates/edgezero-core/src/app.rs:267-284`), and it relies on `use super::*` for `Body`. Until EZ-23 lands, suggested addition after the block is "`router.oneshot` exercises routing, middleware, and handlers only. It skips ingress admission, head limits, response framing, and egress policy." After EZ-23 lands, the sample MUST run the request through real Axum ingress using the delivered test facility, and MUST still contain `EdgeZeroApp::build::(edgezero_adapter_axum::AXUM_PLATFORM)` (EZ-25.44). +- **EZ-25.26** `docs/guide/adapters/axum.md:488-489`. Now "constructs one app per server startup and serves clones of its router." The service holds one `Arc` (`edgezero:crates/edgezero-adapter-axum/src/service.rs:45-77`). Suggested replacement is "constructs one app per server startup and serves every connection from that shared `Arc`, including its policies." + +Toolchain and CHANGELOG: + +- **EZ-25.27** `CHANGELOG.md:408-409`. Now "Rust `1.95.0`, Node `24.12.0`, and Fastly CLI `15.1.0` remain the workspace toolchain." `.tool-versions` pins `rust 1.98.1`, `nodejs 24.20.0`, and `fastly 16.0.0` (`edgezero:.tool-versions:1-3`), the scaffold pins `rust 1.98.1` (`edgezero:crates/edgezero-cli/src/generator.rs:629`), and the same CHANGELOG says main's 1.98.1 toolchain was merged (`edgezero:CHANGELOG.md:16-17`). Suggested replacement is "The workspace toolchain is Rust `1.98.1`, Node `24.20.0`, and Fastly CLI `16.0.0`, matching `.tool-versions`. See the dependency-majors migration guide for the moved pins." +- **EZ-25.28** `docs/guide/adapters/axum.md:435`. Now `FROM rust:1.95.0 as builder`. Suggested replacement is `FROM rust:1.98.1 as builder`. Whether the `rust:1.98.1` image is built on the same Debian release as the `debian:bookworm-slim` runtime stage is **Unverified**. `docker run --rm rust:1.98.1 cat /etc/os-release` would settle it, and `rust:1.98.1-bookworm` keeps the stages matched either way. +- **EZ-25.29** `docs/guide/adapters/axum.md:162`, `:403`, and `crates/edgezero-adapter-axum/src/ingress_config.rs:150`, `:170`. Each says an allocation audit was done against Rust 1.95.0, while the workspace builds on 1.98.1. These are audit records. EdgeZero SHOULD re-run the receive-scratch, header-map, and config-decoder audits on 1.98.1 and change "1.95.0" to "1.98.1". If it does not, the suggested change in all four places is to replace "and Rust 1.95.0" with "and Rust 1.95.0 (not yet repeated on the workspace's Rust 1.98.1)". + +Migration guides, CHANGELOG, and links: + +- **EZ-25.30** `CHANGELOG.md:338-342`. Now "Fastly `runtime_env_config` now returns `FastlyRuntimeConfig` ... call `emit_boot_diagnostics()`". None of the three names exists in `crates/` (`git grep -n 'runtime_env_config\|FastlyRuntimeConfig\|emit_boot_diagnostics' -- crates` is empty), and `CHANGELOG.md:28-30` already says selector-store setup is retired. Suggested replacement for the paragraph is "Fastly `runtime_env_config`, `FastlyRuntimeConfig`, and `emit_boot_diagnostics()` are removed with no compatibility alias. Custom entrypoints open logical store aliases directly and take logging policy from baked `Hooks::logging_for("fastly")`. Managed runners initialize logging before application configuration. Axum now returns logger-setup errors instead of ignoring them, and preinstalled backends must use `owns_logging = true`." +- **EZ-25.31** `CHANGELOG.md:385`, the migration table row "Fastly `runtime_env_config(...) -> EnvConfig` | `FastlyRuntimeConfig`, using `.env` and `emit_boot_diagnostics()` after logger setup". Suggested right cell is "Removed. Use `run_app`, `run_app_with_hooks`, or `request::send_request_with_registries_and_hooks` with baked `Hooks::stores()` and `Hooks::logging_for("fastly")`". +- **EZ-25.32** `CHANGELOG.md:253`. Now "Fastly's manual `stream_to_client`". Suggested replacement is "Fastly's adapter-owned streaming body handle", for the reason in EZ-25.6. +- **EZ-25.33** `docs/guide/manifest-store-migration.md:173-174`. Now "Fastly additionally exposes a public `request::dispatch_with_registries` for custom entry points." No public `dispatch_with_registries` exists. Cloudflare and Spin keep crate-private ones (`edgezero:crates/edgezero-adapter-cloudflare/src/request.rs:671`, `edgezero:crates/edgezero-adapter-spin/src/request.rs:622`), and Fastly's public helpers are at `edgezero:crates/edgezero-adapter-fastly/src/request.rs:534` and `:557`. Suggested replacement is "Fastly additionally exposes public `request::send_with_registries_and_hooks` and `request::send_request_with_registries_and_hooks` for custom entry points." +- **EZ-25.34** `docs/guide/dependency-majors-migration.md:34-35`. Now "including `worker = "=0.8.5"`". The workspace requirement is `worker = { version = "=0.8.5", default-features = false, features = ["http"] }` (`edgezero:Cargo.toml:120`), and the scaffold and fixture match it. Suggested replacement is "Keep the audited SDK requirements exact, including `worker = { version = "=0.8.5", default-features = false, features = ["http"] }` for Cloudflare applications." +- **EZ-25.35** `docs/guide/dependency-majors-migration.md:65-70`. The scaffold pin table has no `spin` row, but the generator now emits `spin 4.1.0` (`edgezero:crates/edgezero-cli/src/generator.rs:626-628`). Suggested row is "| `spin` | not pinned | **4.1.0** | Spin adapter |". +- **EZ-25.36** `docs/guide/dependency-majors-migration.md:72-75`. Replace the whole paragraph, which says "the deploy action's `install-fastly.sh` fails closed when `.tool-versions` and its own `versions.json` disagree, so an app that moves the Rust pin and leaves the Fastly one behind gets a red deploy". The script compares `versions.json` with the EdgeZero action checkout's `.tool-versions` (`edgezero:.github/actions/deploy-fastly/scripts/install-fastly.sh:42-47`, root set at `edgezero:.github/actions/deploy-fastly/action.yml:182`), not the application's. Suggested replacement is "The deploy action installs the Fastly CLI pinned by its own `versions.json` and fails closed when that pin disagrees with the EdgeZero action repository's `.tool-versions`. It does not read the application's `fastly` line, so keep that line equal to the action's pin for local runs to match deployment." +- **EZ-25.37** `docs/guide/configuration.md:698-703`, the `app!` argument table. It has no `configure` row although the macro accepts `configure = ` (`edgezero:crates/edgezero-macros/src/app.rs:42-49`). Suggested row is "| `configure` | `configure = ` | Emits `Hooks::configure`, calling `(expr)(&mut App) -> Result<(), EdgeError>`. Errors propagate through `App::build`. Defaults to `Ok(())` |". When EZ-22.10 lands, the table MUST also describe fallible state. +- **EZ-25.38** `docs/guide/configuration.md:547`. Now "| `echo_stdout` | Fastly, Axum | Mirror logs to stdout |". The Axum runner builds its logger from the level only (`edgezero:crates/edgezero-adapter-axum/src/dev_server.rs:447-451`), and `axum.md:236` already says `echo_stdout` has no effect there. Suggested replacement is "| `echo_stdout` | Fastly | Mirror logs to stdout. The Axum runner ignores it |". +- **EZ-25.39** `docs/guide/blob-app-config-migration.md:191`, `:377`, and `docs/guide/configuration.md:443`. They name `ConfigOutOfDate`, but extraction raises `EdgeError::StoreExtraction`, whose wire kind for a missing blob, missing secret, deserialization, or validation failure is `config_out_of_date` (`edgezero:crates/edgezero-core/src/error.rs:84-101`, `:184-186`). Suggested replacements are "a `config_out_of_date` error (`EdgeError::StoreExtraction` with reason `MissingSecret`) at runtime" for "`ConfigOutOfDate` at runtime" at `:191`, the heading "### `config_out_of_date` at runtime" at `:377`, and "is a `config_out_of_date` error" for "is a `ConfigOutOfDate` error" at `configuration.md:443`. No link uses the old anchor. +- **EZ-25.40** Broken anchors, checked against a local VitePress build of `docs/`. `docs/guide/capabilities.md:106` links `/guide/adapters/axum#bounded-http1-ingress`, which should be `#bounded-http-1-ingress` for the heading at `axum.md:83`. `docs/guide/cli-reference.md:408` links `#config-gc`, which should be `#edgezero-config-gc`. `docs/guide/deploy-action-adoption.md:138-139` links `./adapters/fastly.md#migrating-a-custom-entrypoint`, a heading this PR removed, which should link `./adapters/fastly.md#entrypoint` (heading at `fastly.md:56`). +- **EZ-25.41** `docs/guide/cli-reference.md:588`. Now "(KV in-memory; config in `.edgezero/local-config-.json`)". Local KV is a redb file (`edgezero:crates/edgezero-adapter-axum/src/dev_server.rs:231-243`). Suggested replacement is "(KV in `.edgezero/kv--.redb`; config in `.edgezero/local-config-.json`)". + +Checks that keep the guides honest: + +- **EZ-25.42** Every complete Rust item in `docs/guide/` SHOULD compile in EdgeZero's CI against the current workspace. A fragment SHOULD either compile inside a documented wrapper or be marked `ignore` with a one-line reason. EZ-25.2, EZ-25.14, EZ-25.16, and EZ-25.17 are compile errors that this would have caught. +- **EZ-25.43** Drift of the kinds in EZ-25.6, EZ-25.27, and EZ-25.30 SHOULD be caught automatically. The check MUST NOT flag the old-version column of a migration table, the audit records EZ-25.29 keeps, or the explanation at `fastly.md:347-349`. +- **EZ-25.44** The fixes MUST leave EdgeZero's existing documentation checks passing. The docs contract check MUST accept EZ-25.6's corrected phrase, MUST add EZ-25.10's row to its expected Limits rows, and MUST keep its `axum.md` build-string assertion satisfied by EZ-25.25's sample. + +#### Acceptance criteria + +1. Environment is EdgeZero CI on the recorded commit. Given the fixes, when `docs/guide` is searched for `stream_to_client`, then only `adapters/fastly.md:347` matches. +2. Environment is EdgeZero CI on the recorded commit. Given the fixes, when `CHANGELOG.md` and `docs/guide/adapters/axum.md` are searched for `1.95.0`, then only the audit statements EZ-25.29 keeps match. +3. Environment is EdgeZero CI on the recorded commit. Given EZ-25.42, when CI runs, then every changed snippet that is a complete item compiles, including the forwarding example, and every changed fragment either compiles inside its documented wrapper or is marked `ignore` with a reason. +4. Environment is EdgeZero CI on the recorded commit. Given a manifest using EZ-25.8's declaration, when the capability gate runs for each of the four adapters, then it passes. +5. Environment is EdgeZero CI on the recorded commit. Given the fixes, when EdgeZero's documentation checks run (`scripts/check_outbound_docs_contract.mjs`, `scripts/check_outbound_legacy_api.sh`, and the docs formatter), then they pass. +6. Environment is Axum, Trusted Server. TS's publisher forwarding, written from the corrected example once EZ-1 lands, forwards a bodiless GET whose inbound body is an unread stream to a local origin with no request body. TS's Fastly custom lifecycle compiles against the five-argument helper the README now shows. + +#### Out of scope + +- Edits to `docs/superpowers/specs/`, which EdgeZero treats as immutable (`edgezero:.github/workflows/test.yml:72-73`). Where a spec is stale, the guides carry the current text. +- New capability cells. EZ-25.9 adds footnote text to an existing BestEffort cell, and EZ-25.8 changes only the example declaration. + +#### Open questions + +1. Should guide snippets compile in CI (EZ-25.42)? TS prefers yes, starting with `proxying.md`, `handlers.md`, and `adapters/fastly.md`, where every compile error in this list lives. +2. Should the axum allocation audits be re-run on Rust 1.98.1 (EZ-25.29)? TS prefers yes, because the audit text promises the numbers hold for the pinned toolchain. + +#### Interim for Trusted Server + +TS ports from the code and from the corrected texts in this section, not from the guides as they stand at F. No transport code is involved. + +## Qualification gate + +### EZ-Q1. Recorded passing qualification run of the canonical Fastly composition + +**Priority:** Gate +**Adapters:** all four, with hosted Fastly for the criteria that name it +**Related:** EZ-1 to EZ-9 (their acceptance criteria are what this record covers). EZ-2, EZ-6, and EZ-15 (their reuse-mode parts gate later). The Trusted Server cutover spec holds the run plan that produces the record. + +#### Need + +Fastly is TS's production target, and the cut moves TS onto `run_custom` with `send_request_with_registries_and_hooks`, a composition TS has never run. EdgeZero's capability guide says "the production `run_app` composition has not yet been exercised end to end under Viceroy" (`edgezero:docs/guide/capabilities.md:284-288`). The fixture README says its cases "describe fixture coverage, not a recorded pass for the current pinned SDK/runtime graph" (`edgezero:tests/fixtures/reusable-app/README.md:285-287`). TS will not ship the cut to production without a record, for the exact commit it pins, showing that the blocking requirements hold in the environments their criteria name. + +#### Gap at F + +CI run 38934086784 passed EdgeZero's Fastly smoke at F with Viceroy 0.21.0, Rust 1.98.1, and Fastly SDK 0.13.1 (`edgezero-ci:run 38934086784`). It does not meet the need. Its artifact expires after 14 days (`edgezero:.github/workflows/test.yml:405-411`), it records no source revision (`edgezero:tests/fixtures/reusable-app/README.md:315-317`), its runner label does not pin an image, its cases do not cover the blocking requirements in this document, and no hosted Fastly run exists. The hosted egress tool contacts no outbound backend (`edgezero:tests/fixtures/reusable-app/README.md:69-71`). + +#### Requirements + +- **EZ-Q1.1** For the exact EdgeZero commit TS pins, there MUST be a recorded result for every acceptance criterion of the blocking parts of EZ-1 to EZ-9 and of any requirement promoted to blocking, each in the environment that criterion names. In particular, EZ-2.6 and EZ-6 require passing Viceroy and hosted Fastly evidence for bounded response-body readiness and reads; SDK interface declarations or a safe failure before an unbounded operation do not qualify as passing behavior. +- **EZ-Q1.2** Every such result MUST pass. Each of those criteria is pass-or-fail, and a measurement it names is judged against the bound it states. A leg or value that a criterion says only to record passes when the record contains it. A blocking criterion without a pass condition is a defect in this document, to be fixed before the record is produced. +- **EZ-Q1.3** The record MUST name the EdgeZero commit, the toolchain (Rust 1.98.1, Viceroy 0.21.0, Fastly SDK 0.13.1, Spin 4.1, and the workerd version, where used), the runner image, and the TS baseline and candidate builds behind any comparison, and for each criterion its environment, result, and any measurements with sample counts. A record covers that one commit only. +- **EZ-Q1.4** EdgeZero produces the results for criteria in unit, Viceroy 0.21.0, workerd, Spin 4.1, and Axum that do not need Trusted Server, and assembles and keeps the record. For hosted Fastly criteria whose settings TS's configuration cannot express, EdgeZero MUST provide a qualification build that TS can deploy to a TS staging service, serving single-request sandboxes (`run_custom`) unless a criterion says otherwise. These are EZ-1 criterion 2 and the hosted leg of criterion 6, EZ-2 criteria 11, 12, 13b, and 14, EZ-4 criterion 8, the hosted legs of EZ-6 scenarios 1, 2, 6, 7, 8, and 11, and EZ-8's hosted criterion. TS produces its own end-to-end checks and the remaining hosted Fastly results with its own build, in single-request mode unless a criterion says otherwise. +- **EZ-Q1.5** Criteria of reuse-mode follow-ups (EZ-2's reuse-mode part and the reuse-mode legs of EZ-6 and EZ-15) are not part of the gate for single-request production. They MUST pass in a record before TS enables reuse mode. +- **EZ-Q1.6** A follow-up present at the pinned commit, as its CHANGELOG states and TS confirms, adds the acceptance criteria for its confirmed-present parts and target legs to those EZ-Q1.1 requires, and EZ-Q1.2 applies to them. Applicability is recorded under EZ-Q1.8; unrelated deferred parts do not join the gate merely because another part is present. The reuse-mode criteria of EZ-Q1.5 stay out of the gate even when present. An absent follow-up's interim applies instead. +- **EZ-Q1.7** The record MUST stay retrievable for as long as TS pins that commit. +- **EZ-Q1.8** The record MUST map acceptance criteria and environment legs to the required blocking parts, approved promotions, confirmed-present follow-up parts, and explicitly conditional parts or targets. For a partial promotion or implementation, only the applicable parts and legs join the gate. A mixed criterion MUST be split into applicable and deferred legs rather than skipped wholesale. A deferred leg MUST name the approved deferral or unmet explicit condition; implementation absence is not a reason to omit a required blocking leg. For example, promoting EZ-24.1 to EZ-24.10 for host and `wasm32-wasip1` does not require its deferred JavaScript legs or the script-abandonment criterion for EZ-24.11. + +#### Acceptance criteria + +1. Trusted Server review. A record that names a commit other than the pin is rejected. +2. Trusted Server review. A record that lacks a result for any criterion EZ-Q1.1 or EZ-Q1.6 requires, records a result in an environment other than the one the criterion names, or records a failing result is rejected. +3. Trusted Server review. A record whose toolchain differs from EZ-Q1.3, that names no runner image, or that omits measurement sample counts or the builds behind a comparison is rejected. +4. Trusted Server review. A record stored only where it expires within 90 days, such as a CI artifact, is rejected. +5. Trusted Server review. A record that omits the applicability map, skips a required blocking leg as unimplemented, or uses partial adoption to skip the applicable portion of a mixed criterion is rejected. Approved deferred targets and conditional follow-up legs do not fail the record when their exclusion is explicitly justified under EZ-Q1.8. + +#### Out of scope + +The run plan. Which party runs which leg, the case list, scripts, and evidence formats belong in the Trusted Server cutover spec, which consumes this record and does not hold them yet. EdgeZero is asked for no runs beyond the acceptance criteria in this document. + +#### Open questions + +1. Where EdgeZero keeps the record. Trusted Server prefers a release asset attached to the tag TS pins, because CI artifacts expire. + +## Review log + +- **Round 1 (Claude Code, five fresh reviewers by section plus one whole-document).** Thirteen + blocking findings, almost all contradictions between sections (synchronous versus async begin, + two owners for in-flight request state, an unbounded background drain, drop and fire-and-forget + semantics, stale references to Fastly internals, tests that depended on a follow-up, and an + unreachable qualification case). All were resolved, then the TS maintainer asked for requirements + rather than implementation, and every section was rewritten to observable behavior and acceptance + criteria. +- **Round 2 (Claude Code, three fresh reviewers on a different model).** Thirteen blocking findings. + Resolved by defining when a response arrives and when a request's lifetime ends, rejecting only + options marked Unsupported (BestEffort options are accepted), correcting the Viceroy 0.21.0 timer + facts, making EZ-Q1 gate every blocking criterion with hosted evidence from TS's own composition + and a pre-cut TS baseline, splitting EZ-2 into a blocking part and a reuse-mode follow-up because + production runs single-request sandboxes, and removing the remaining mechanism and harness text. +- **Round 3 (Claude Code, two fresh reviewers).** Every round 2 blocking finding was confirmed + resolved in the text. New blocking findings were all in how EZ-Q1 assigned runs. Resolved by + separating EdgeZero-side Viceroy evidence from Trusted Server end-to-end checks, running hosted + criteria that TS configuration cannot express on an EdgeZero qualification build that TS deploys, + gating reuse-mode criteria only before TS enables reuse mode, fitting Viceroy cases inside its + six-request guest limit, and defining fire-and-forget lifetime as the one exception to request + lifetime. +- **Round 4 (Claude Code, one fresh reviewer on the round 3 changes).** Every round 3 finding was + resolved. One blocking finding, that two hosted measurements had no pass condition. Resolved by + giving every blocking measurement a pass condition. EZ-Q1 had grown into a full run plan, which is + Trusted Server's rollout work rather than an EdgeZero requirement, so it now states only what the + record must show, and the run plan moves to the Trusted Server cutover spec. +- **Round 5 (Claude Code, one fresh reviewer on the round 4 changes).** Round 4's blocking finding + was resolved. One new blocking finding, that EZ-Q1.5 and EZ-Q1.6 disagreed on a reuse-mode + follow-up present at the pinned commit, and that nothing rejected a record missing a present + follow-up's results. Fixed with the reviewer's replacement text and checked against the head by the + spec author, without a further agent round. Its should-fix items were applied (EZ-6 hosted timing, + the definition of "every adapter", four measurement bounds, conditional workerd and Spin + criteria, and the record's contents). +- **Round 6 (2026-10-18, implementation-grounded review and draft corrections).** Twenty findings + were addressed without changing runtime code. Response-owned outbound work survives asynchronous + response handoff, while Fastly callback-owned post-delivery work keeps its own lifetime. The + uninterruptible response-body fallback was withdrawn: bounded readiness and reads are a blocking + qualification prerequisite, not a weaker passing cutoff outcome. Redirects use a shared + head-first dispatch contract with ordinary final-response processing, explicit trusted-policy + option ownership, and an immutable original absolute chain deadline. Intentional abort cannot + authorize a replacement response. The remaining corrections cover phase-limit precedence, + portable test-client validation and idle semantics, controlled connect-timeout fixtures, + fire-and-forget capacity recovery, upload prebuffering, bodyless lengths, header handling, + provider query transformations, store-open categories, and host-compression framing. New + acceptance cases make these distinctions observable; hosted results are still required by EZ-Q1. +- **Round 7 (2026-10-18, three independent section reviews and targeted rechecks).** Seven + additional P2 findings were fixed: host-visible overflow versus framing rejection, negative + cache-tagging evidence, partial-adoption qualification scope, declared-length streaming + compression evidence, dynamic header-set validation, preservation of already-running + callback-owned exchanges, and an explicit `next` versus `collect()` ordering oracle. The + reviewers re-read the corrections and immediate dependencies and reported all seven findings + closed, with no remaining actionable issue in those replacements. This is requirements review, + not a record of passing runtime or hosted qualification. +- **Implementation-plan boundary corrections (2026-10-18).** The draft replaces Image Optimizer's + impossible universal twice-effective-budget claim with an explicit finite backend-policy horizon + requiring approval and hosted evidence; ordinary outbound deadlines stay unchanged. Fastly + callback exit now has explicit retained-handle acceptance coverage and a lifecycle-owned plan + integration point. Original coding preserves normal header filtering rather than restoring removed + fields. Typed generated-overflow retry suppression wins over synthesized header decoration, + without changing upstream-413 or config-503 treatment. These are document corrections and future + acceptance tasks, not claims of implemented or qualified runtime behavior. +- **Declined findings that explain a requirement's meaning.** + - EZ-9. A followed 3xx body never fails a redirect chain, including its declared length, and is read only within the bound EZ-9.11 sets. + - EZ-6. A begun request does not accept streamed uploads. TS begins only its overlapped navigation + `GET`. The earlier body-transfer-time fallback was withdrawn in round 6 because it cannot meet + the cutoff guarantee; bounded readiness and subsequent reads must be verified instead. + - EZ-14. Forcing every redirect hop to the default body policy would drop a streamed caller's idle + bound on the final hop. + - EZ-17. Promoting only part of EZ-17 would make TS migrate the same report twice. + - EZ-Q1. A record covers exactly one EdgeZero commit. There is no carry-forward to a later pin. diff --git a/examples/app-demo/Cargo.lock b/examples/app-demo/Cargo.lock index 3af3f34f7..5e518a785 100644 --- a/examples/app-demo/Cargo.lock +++ b/examples/app-demo/Cargo.lock @@ -179,9 +179,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.44" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "515a1f282e33d55983c499d7e9e87082e81cbc32974825bf9032f928392d5844" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -219,7 +219,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -321,6 +321,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bitflags" version = "1.3.2" @@ -329,9 +335,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block-buffer" @@ -364,9 +370,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.3" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -396,9 +402,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.4.5" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" dependencies = [ "find-msvc-tools", "jobserver", @@ -408,9 +414,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" @@ -444,9 +450,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -454,9 +460,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -466,21 +472,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cmake" @@ -518,9 +524,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.39" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe67f2944eef52fc7b106b8c9450d243a88701a0c065f7f57235e76abaed7df" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "brotli", "compression-core", @@ -530,9 +536,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.33" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "const-oid" @@ -576,9 +582,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -739,7 +745,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -759,9 +765,10 @@ name = "edgezero-adapter" version = "0.1.0" dependencies = [ "base16ct", + "edgezero-core", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "toml", "walkdir", ] @@ -771,6 +778,7 @@ name = "edgezero-adapter-axum" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "async-trait", "axum", "bytes", @@ -780,12 +788,17 @@ dependencies = [ "futures", "futures-util", "http", + "http-body", + "hyper", + "hyper-util", + "libc", "log", "redb", "reqwest", + "serde", "serde_json", "simple_logger", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "toml", "tower", @@ -798,6 +811,7 @@ name = "edgezero-adapter-cloudflare" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "async-trait", "brotli", "bytes", @@ -812,6 +826,8 @@ dependencies = [ "tempfile", "toml_edit", "walkdir", + "wasm-streams", + "web-sys", "worker", ] @@ -830,6 +846,8 @@ dependencies = [ "edgezero-adapter", "edgezero-core", "fastly", + "fastly-shared", + "fastly-sys", "fern", "flate2", "futures", @@ -838,8 +856,8 @@ dependencies = [ "log-fastly", "serde", "serde_json", - "sha2 0.11.0", - "thiserror 2.0.20", + "sha2 0.11.1", + "thiserror 2.0.21", "toml_edit", "walkdir", ] @@ -849,13 +867,12 @@ name = "edgezero-adapter-spin" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "async-trait", - "brotli", "bytes", "ctor", "edgezero-adapter", "edgezero-core", - "flate2", "futures", "futures-util", "log", @@ -864,7 +881,7 @@ dependencies = [ "serde_json", "spin-sdk", "subtle", - "thiserror 2.0.20", + "thiserror 2.0.21", "toml", "toml_edit", "walkdir", @@ -889,7 +906,7 @@ dependencies = [ "serde_json", "similar", "simple_logger", - "thiserror 2.0.20", + "thiserror 2.0.21", "toml", "validator", ] @@ -898,29 +915,41 @@ dependencies = [ name = "edgezero-core" version = "0.1.0" dependencies = [ + "adler2", + "alloc-no-stdlib", + "alloc-stdlib", "anyhow", "async-compression", "async-stream", "async-trait", "base16ct", + "brotli", + "brotli-decompressor", "bytes", + "compression-codecs", + "compression-core", + "crc32fast", "edgezero-macros", + "flate2", "futures", "futures-util", "http", "http-body", "log", "matchit 0.9.2", + "miniz_oxide", "ryu", "serde", "serde_json", "serde_path_to_error", "serde_urlencoded", - "sha2 0.11.0", - "thiserror 2.0.20", + "sha2 0.11.1", + "simd-adler32", + "thiserror 2.0.21", "toml", "tower-service", "tracing", + "url", "validator", "web-time", ] @@ -934,7 +963,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 3.0.5", + "syn 3.0.6", "toml", "validator", ] @@ -1061,19 +1090,18 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "flate2" -version = "1.1.10" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", - "zlib-rs", ] [[package]] @@ -1159,7 +1187,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -1193,9 +1221,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1241,7 +1269,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -1261,9 +1289,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.5.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1306,22 +1334,21 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" dependencies = [ "atomic-waker", "bytes", - "futures-channel", "futures-core", "http", "http-body", @@ -1336,9 +1363,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.9" +version = "0.27.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" dependencies = [ "http", "hyper", @@ -1351,16 +1378,17 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" dependencies = [ - "base64", + "base64 0.23.1", "bytes", "futures-channel", "futures-util", "http", "http-body", + "httparse", "hyper", "ipnet", "libc", @@ -1526,9 +1554,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.1" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "is_terminal_polyfill" @@ -1563,7 +1591,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.20", + "thiserror 2.0.21", "walkdir", "windows-link", ] @@ -1612,9 +1640,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.105" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ "cfg-if", "futures-util", @@ -1687,9 +1715,9 @@ dependencies = [ [[package]] name = "lru-slab" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "matchit" @@ -1723,9 +1751,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.9.1" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -1813,9 +1841,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad" +checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" dependencies = [ "memchr", "ucd-trie", @@ -1823,9 +1851,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f" +checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf" dependencies = [ "pest", "pest_generator", @@ -1833,9 +1861,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5" +checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109" dependencies = [ "pest", "pest_meta", @@ -1846,9 +1874,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e" +checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb" dependencies = [ "pest", ] @@ -1902,12 +1930,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "prettyplease" -version = "0.2.37" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" dependencies = [ "proc-macro2", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -1929,7 +1957,7 @@ dependencies = [ "proc-macro-error-attr3", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -1943,9 +1971,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" dependencies = [ "bytes", "cfg_aliases", @@ -1955,7 +1983,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tracing", "web-time", @@ -1963,9 +1991,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.17" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" dependencies = [ "aws-lc-rs", "bytes", @@ -1978,7 +2006,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.20", + "thiserror 2.0.21", "tinyvec", "tracing", "web-time", @@ -2015,9 +2043,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -2041,9 +2069,9 @@ dependencies = [ [[package]] name = "redb" -version = "4.2.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de6c3b63e007e90ce536ec2ae4690826136a20ec8dbbbb400daef1bb999d2e36" +checksum = "39178e0a867f9e3eaa6292a4ab1dd3613f2519c5b564341e1e7b29b270736cc4" dependencies = [ "libc", ] @@ -2083,7 +2111,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -2136,7 +2164,7 @@ version = "0.40.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "fallible-iterator", "fallible-streaming-iterator", "libsqlite3-sys", @@ -2160,11 +2188,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -2173,9 +2201,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "aws-lc-rs", "once_cell", @@ -2209,9 +2237,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" dependencies = [ "core-foundation", "core-foundation-sys", @@ -2230,9 +2258,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier-android" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" [[package]] name = "rustls-webpki" @@ -2282,7 +2310,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation", "core-foundation-sys", "libc", @@ -2343,14 +2371,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2378,7 +2406,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -2417,9 +2445,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", "cpufeatures 0.3.1", @@ -2444,9 +2472,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.10" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simd_cesu8" @@ -2493,9 +2521,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.0" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" [[package]] name = "socket2" @@ -2510,8 +2538,7 @@ dependencies = [ [[package]] name = "spin-macro" version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8a641da857d4e7cada31a853c40180eeb10485a0f2f0282d0a7e5a2a4105d4" +source = "git+https://github.com/stackpop/spin-rust-sdk.git?rev=e65ff90dadf4173b08b14194e878ad3acd555337#e65ff90dadf4173b08b14194e878ad3acd555337" dependencies = [ "proc-macro2", "quote", @@ -2521,8 +2548,7 @@ dependencies = [ [[package]] name = "spin-sdk" version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7a3d9a6805ccdb4d404e91ccf7198ac5e486b6199dc9387aeebaa368272112" +source = "git+https://github.com/stackpop/spin-rust-sdk.git?rev=e65ff90dadf4173b08b14194e878ad3acd555337#e65ff90dadf4173b08b14194e878ad3acd555337" dependencies = [ "anyhow", "bytes", @@ -2531,7 +2557,7 @@ dependencies = [ "http-body", "http-body-util", "spin-macro", - "thiserror 2.0.20", + "thiserror 2.0.21", "wasip3", ] @@ -2553,7 +2579,16 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -2568,6 +2603,18 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2598,9 +2645,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -2618,13 +2665,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -2651,11 +2698,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl 2.0.20", + "thiserror-impl 2.0.21", ] [[package]] @@ -2671,13 +2718,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -2724,18 +2771,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" @@ -2761,14 +2799,14 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] name = "tokio-rustls" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" dependencies = [ "rustls", "tokio", @@ -2776,9 +2814,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.5+spec-1.1.0" +version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ "indexmap", "serde_core", @@ -2800,9 +2838,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.13+spec-1.1.0" +version = "0.25.15+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" dependencies = [ "indexmap", "toml_datetime", @@ -2848,7 +2886,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-util", "http", @@ -2924,15 +2962,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "untrusted" @@ -3016,9 +3048,9 @@ dependencies = [ [[package]] name = "want" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" dependencies = [ "try-lock", ] @@ -3040,22 +3072,22 @@ dependencies = [ [[package]] name = "wasip3" -version = "0.7.1+wasi-0.3.0" +version = "0.9.0+wasi-0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e9ed91ee5a995b6fe16c89d4511a8c12eb76967be20b57557618e02e781bcc" +checksum = "f1d5749fdf69bb9400562eba50f8f25f5cd800ef4b74300038b6f99ae7409674" dependencies = [ "bytes", "http", "http-body", - "thiserror 2.0.20", - "wit-bindgen 0.57.1", + "thiserror 2.0.21", + "wit-bindgen 0.62.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -3066,19 +3098,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.78" +version = "0.4.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" dependencies = [ "js-sys", + "tokio", "wasm-bindgen", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3086,31 +3119,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-encoder" -version = "0.247.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" dependencies = [ "leb128fmt", "wasmparser", @@ -3118,9 +3151,9 @@ dependencies = [ [[package]] name = "wasm-metadata" -version = "0.247.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +checksum = "4e7bf119eb01f4a246864c10bd87c8f26c566350abb25aa4d5273c415bbab338" dependencies = [ "anyhow", "indexmap", @@ -3143,11 +3176,11 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.247.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "hashbrown", "indexmap", "semver", @@ -3155,9 +3188,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.105" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" dependencies = [ "js-sys", "wasm-bindgen", @@ -3347,7 +3380,7 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -3356,16 +3389,25 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", +] + +[[package]] +name = "wit-bindgen" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cb4b5556c3a791e86838ea287782bdafa704d55b0e68b5b81a3a16b9ea5f4b" +dependencies = [ + "bitflags 2.13.2", "futures", "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen-core" -version = "0.57.1" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" +checksum = "72ad22a37ecbc0e1fdca34d2b670ba41368cb0920735643e00fdf39a16ee5431" dependencies = [ "anyhow", "heck", @@ -3374,15 +3416,15 @@ dependencies = [ [[package]] name = "wit-bindgen-rust" -version = "0.57.1" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" +checksum = "84ba5213d4332c260a0d4e69e34b0f7e4b85ddae10b6ebfbac00d9c185ba628a" dependencies = [ "anyhow", "heck", "indexmap", "prettyplease", - "syn 2.0.119", + "syn 3.0.6", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3390,27 +3432,27 @@ dependencies = [ [[package]] name = "wit-bindgen-rust-macro" -version = "0.57.1" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9237d678e3513ad24e96fe98beacdc0db6405284ba2a2400418cf0d42caa89" +checksum = "8f19d63da8e478a370ef8192d90ccb517965b7747d6b46cd2d831dd0ef4d2541" dependencies = [ "anyhow", "prettyplease", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", "wit-bindgen-core", "wit-bindgen-rust", ] [[package]] name = "wit-component" -version = "0.247.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" +checksum = "092f783dee3253265fcde131475055f8c275ac0176a9c0396db1dc7b5544bfb4" dependencies = [ "anyhow", - "bitflags 2.13.1", + "bitflags 2.13.2", "indexmap", "log", "serde", @@ -3424,9 +3466,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.247.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" +checksum = "6390f4f02493ce678d509c859cf1698e38929fb75c8012a6ee2f7d6b6cb66cd7" dependencies = [ "anyhow", "hashbrown", @@ -3437,7 +3479,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "unicode-xid", + "unicode-ident", "wasmparser", ] @@ -3461,7 +3503,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "serde_urlencoded", - "strum", + "strum 0.27.2", "tokio", "url", "wasm-bindgen", @@ -3474,15 +3516,15 @@ dependencies = [ [[package]] name = "worker-macros" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d908735d273dd7f9c325a842623f4e5a745e0686187ce465b34dc162ad348df" +checksum = "f3d7389670b55aab89760be149007aeff773cdad7b650d807f7fb556d04bb304" dependencies = [ "async-trait", "proc-macro2", "quote", - "strum", - "syn 2.0.119", + "strum 0.28.0", + "syn 3.0.6", "wasm-bindgen", "wasm-bindgen-macro-support", "worker-sys", @@ -3490,9 +3532,9 @@ dependencies = [ [[package]] name = "worker-sys" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33faa1a8fa6c7eec67b196e008859c44d468a5ad4f991855cdc856f119e0e98f" +checksum = "6a440092e7ae7c5feb5507fbdc6aee0cc241d132757c83e1729766a24cddff82" dependencies = [ "cfg-if", "js-sys", @@ -3519,13 +3561,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", "synstructure", ] @@ -3540,13 +3582,13 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", "synstructure", ] @@ -3586,15 +3628,9 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] -[[package]] -name = "zlib-rs" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" - [[package]] name = "zmij" version = "1.0.23" diff --git a/examples/app-demo/Cargo.toml b/examples/app-demo/Cargo.toml index bc473757e..170c59a6f 100644 --- a/examples/app-demo/Cargo.toml +++ b/examples/app-demo/Cargo.toml @@ -29,8 +29,8 @@ edgezero-adapter-fastly = { path = "../../crates/edgezero-adapter-fastly" } edgezero-adapter-spin = { path = "../../crates/edgezero-adapter-spin" } edgezero-cli = { path = "../../crates/edgezero-cli" } edgezero-core = { path = "../../crates/edgezero-core" } -spin-sdk = { version = "7", default-features = false } -fastly = "0.13" +spin-sdk = { version = "=7.0.0", git = "https://github.com/stackpop/spin-rust-sdk.git", rev = "e65ff90dadf4173b08b14194e878ad3acd555337", default-features = false } +fastly = "=0.13.1" futures = { version = "0.3", default-features = false, features = ["std", "executor"] } log = "0.4" once_cell = "1" @@ -45,7 +45,7 @@ simple_logger = "5" tempfile = "3" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tracing = "0.1" -worker = { version = "0.8", default-features = false, features = ["http"] } +worker = { version = "=0.8.5", default-features = false, features = ["http"] } [profile.release] debug = 1 diff --git a/examples/app-demo/crates/app-demo-adapter-axum/axum.toml b/examples/app-demo/crates/app-demo-adapter-axum/axum.toml index 364f8e071..1b40fab97 100644 --- a/examples/app-demo/crates/app-demo-adapter-axum/axum.toml +++ b/examples/app-demo/crates/app-demo-adapter-axum/axum.toml @@ -2,3 +2,12 @@ crate = "app-demo-adapter-axum" crate_dir = "." port = 8787 + +# Local config snapshots load before listener bind. Push config before startup; +# restart after an update. Override allocation budgets with: +# EDGEZERO__ADAPTER__CONFIG_STORE__MAX_FILE_BYTES=16777216 +# EDGEZERO__ADAPTER__CONFIG_STORE__MAX_ENTRIES=1024 +# EDGEZERO__ADAPTER__CONFIG_STORE__MAX_KEY_BYTES=1024 +# EDGEZERO__ADAPTER__CONFIG_STORE__MAX_VALUE_BYTES=8388608 +# EDGEZERO__ADAPTER__CONFIG_STORE__MAX_RESIDENT_BYTES=33554432 +# EDGEZERO__ADAPTER__CONFIG_STORE__MAX_STARTUP_BYTES=117440512 diff --git a/examples/app-demo/crates/app-demo-adapter-cloudflare/wrangler.toml b/examples/app-demo/crates/app-demo-adapter-cloudflare/wrangler.toml index 6840f654d..9a00b286e 100644 --- a/examples/app-demo/crates/app-demo-adapter-cloudflare/wrangler.toml +++ b/examples/app-demo/crates/app-demo-adapter-cloudflare/wrangler.toml @@ -1,6 +1,7 @@ name = "app-demo-adapter-cloudflare" main = "build/worker/shim.mjs" compatibility_date = "2023-05-01" +compatibility_flags = ["enable_request_signal", "enable_weak_ref"] [build] command = "worker-build --release" diff --git a/examples/app-demo/crates/app-demo-adapter-fastly/src/main.rs b/examples/app-demo/crates/app-demo-adapter-fastly/src/main.rs index b8ba7515a..fac1438ec 100644 --- a/examples/app-demo/crates/app-demo-adapter-fastly/src/main.rs +++ b/examples/app-demo/crates/app-demo-adapter-fastly/src/main.rs @@ -6,11 +6,8 @@ #[cfg(target_arch = "wasm32")] use app_demo_core::App; #[cfg(target_arch = "wasm32")] -use fastly::{Error, Request, Response}; -#[cfg(target_arch = "wasm32")] -#[fastly::main] -pub fn main(req: Request) -> Result { - edgezero_adapter_fastly::run_app::(req) +pub fn main() -> Result<(), fastly::Error> { + edgezero_adapter_fastly::run_app::() } #[cfg(not(target_arch = "wasm32"))] diff --git a/examples/app-demo/crates/app-demo-adapter-spin/src/lib.rs b/examples/app-demo/crates/app-demo-adapter-spin/src/lib.rs index 51adabb4e..8bcb12a06 100644 --- a/examples/app-demo/crates/app-demo-adapter-spin/src/lib.rs +++ b/examples/app-demo/crates/app-demo-adapter-spin/src/lib.rs @@ -9,12 +9,12 @@ #[cfg(target_arch = "wasm32")] use app_demo_core::App; #[cfg(target_arch = "wasm32")] -use spin_sdk::http::{IntoResponse, Request}; +use spin_sdk::http::Request; #[cfg(target_arch = "wasm32")] use spin_sdk::http_service; #[cfg(target_arch = "wasm32")] #[http_service] -async fn handle(req: Request) -> anyhow::Result { +async fn handle(req: Request) -> anyhow::Result { edgezero_adapter_spin::run_app::(req).await } diff --git a/examples/app-demo/crates/app-demo-cli/tests/config_flow.rs b/examples/app-demo/crates/app-demo-cli/tests/config_flow.rs index d99dfa62f..4835c7692 100644 --- a/examples/app-demo/crates/app-demo-cli/tests/config_flow.rs +++ b/examples/app-demo/crates/app-demo-cli/tests/config_flow.rs @@ -11,7 +11,11 @@ #![cfg(test)] use app_demo_core::config::AppDemoConfig; +use edgezero_adapter::cli_support::validate_spin_outbound_host_contract; +use edgezero_adapter_axum::config_store_limits::ConfigStoreLimits; use edgezero_cli::args::{ConfigPushArgs, ConfigValidateArgs}; +use edgezero_core::Capability; +use edgezero_core::manifest::ManifestLoader; use std::fs; use std::path::{Path, PathBuf}; @@ -120,6 +124,43 @@ fn push_args(manifest: &Path, adapter: &str, dry_run: bool) -> ConfigPushArgs { args } +#[test] +fn app_demo_manifest_declares_outbound_http_optional() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let app_manifest_path = root.join("edgezero.toml"); + let spin_manifest_path = root.join("crates/app-demo-adapter-spin/spin.toml"); + let manifest = + ManifestLoader::from_path(&app_manifest_path).expect("parse shipped app-demo manifest"); + assert_eq!( + manifest.manifest().capabilities.optional, + [Capability::OutboundHttp, Capability::OutboundImageOptimizer] + ); + let hosts = manifest + .manifest() + .capabilities + .outbound + .hosts + .as_deref() + .expect("app-demo outbound hosts"); + assert_eq!(hosts, ["https://*:*"]); + + let spin = fs::read_to_string(&spin_manifest_path).expect("read shipped Spin manifest"); + assert_eq!( + spin.matches("allowed_outbound_hosts = [\"https://*:*\"]") + .count(), + 1, + "app-demo Spin manifest must grant exactly the canonical HTTPS wildcard" + ); + assert!(!spin.contains("allowed_outbound_hosts = [\"http://")); + validate_spin_outbound_host_contract( + manifest.manifest(), + &app_manifest_path, + &spin_manifest_path, + None, + ) + .expect("app-demo application and selected Spin component must remain aligned"); +} + #[test] fn config_validate_strict_passes_against_app_demo_config() { // Typed validator runs the raw checks (manifest schema, store @@ -215,7 +256,11 @@ fn config_typed_handler_deserialises_blob_envelope_to_greeting() { }); let envelope = BlobEnvelope::new(data, "2026-01-01T00:00:00Z".to_owned()); let blob_str = serde_json::to_string(&envelope).expect("envelope JSON"); - let store = AxumConfigStore::from_map([("app_config".to_owned(), blob_str)]); + let store = AxumConfigStore::from_map( + [("app_config".to_owned(), blob_str)], + ConfigStoreLimits::default(), + ) + .expect("bounded snapshot"); let config_registry: ConfigRegistry = StoreRegistry::single_id( "app_config".to_owned(), diff --git a/examples/app-demo/crates/app-demo-core/src/handlers.rs b/examples/app-demo/crates/app-demo-core/src/handlers.rs index 0a42da466..c3cccc651 100644 --- a/examples/app-demo/crates/app-demo-core/src/handlers.rs +++ b/examples/app-demo/crates/app-demo-core/src/handlers.rs @@ -1,7 +1,10 @@ -use std::env; +use std::io::Error as IoError; +use std::num::NonZeroU64; use std::sync::Arc; +use std::time::Duration; use bytes::Bytes; +use edgezero_core::ResponseEgressDeadline; use edgezero_core::action; use edgezero_core::body::Body; use edgezero_core::context::RequestContext; @@ -9,27 +12,72 @@ use edgezero_core::error::EdgeError; use edgezero_core::extractor::{ AppConfig, Headers, Json, Kv, Path, Query, Secrets, State, ValidatedPath, }; -use edgezero_core::http::{self, Response, StatusCode, Uri}; -use edgezero_core::proxy::ProxyRequest; +use edgezero_core::http::{self, Method, Response, StatusCode, Uri}; +use edgezero_core::outbound::{ + OutboundBatchTermination, OutboundCachePolicy, OutboundCollectedSlot, OutboundFollowFailure, + OutboundImageFormat, OutboundImageOptimizerOptions, OutboundImageOptimizerRegion, + OutboundRedirectAction, OutboundRedirectLimit, OutboundRequest, ResponseDecoding, +}; use edgezero_core::response::Text; use futures::{StreamExt as _, stream}; -use crate::config::AppDemoConfig; +use crate::{AdmissionLease, config::AppDemoConfig}; const ALLOWED_CONFIG_KEYS: &[&str] = &["greeting", "feature.new_checkout", "service.timeout_ms"]; const DEFAULT_PROXY_BASE: &str = "https://httpbin.org"; +const MAX_DECODER_BYTES: u64 = 0x0200_0000; +const MAX_BROTLI_WINDOW_BITS: u8 = 24; /// Maximum request body size (25 MB, matches KV value limit). const MAX_BODY_SIZE: usize = 25 * 1024 * 1024; +const MAX_DECODED_RESPONSE_BYTES: u64 = 0x0010_0000; +const MAX_ENCODED_RESPONSE_BYTES: u64 = 0x0020_0000; +const MAX_FANOUT_INPUT_BYTES: usize = 0x4000; +const MAX_FANOUT_REQUESTS: usize = 8; +const MAX_FINAL_RESPONSE_BYTES: u64 = 0x0020_0000; +const MAX_OUTBOUND_REQUEST_BODY_BYTES: u64 = 0x0010_0000; +const MAX_RESPONSE_CHUNK_BYTES: u64 = 0x0001_0000; +const MAX_RESPONSE_HEADER_BYTES: u64 = 0x0001_0000; +const MAX_RESPONSE_HEADER_COUNT: u64 = 100; // 512 (KV key limit) - 5 (len of "note:") = 507 const MAX_NOTE_ID_LEN: u64 = 507; +const OUTBOUND_BATCH_BUDGET: Duration = Duration::from_secs(5); +const OUTBOUND_FOLLOW_BUDGET: Duration = Duration::from_secs(5); +const OUTBOUND_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); +const RESPONSE_EGRESS_BUDGET: Duration = Duration::from_secs(2); const SMOKE_SECRET_MISSING_NAME: &str = "SMOKE_SECRET_MISSING"; const SMOKE_SECRET_NAME: &str = "SMOKE_SECRET"; +#[derive(serde::Serialize)] +struct AdmissionView { + grant_consumed_once: bool, + route_class: Option, +} + #[derive(serde::Deserialize)] struct ConfigParams { name: String, } +#[derive(serde::Deserialize)] +struct FanoutRequest { + paths: Vec, +} + +#[derive(serde::Serialize)] +struct FanoutSlot { + elapsed_ms: Option, + index: usize, + outcome: FanoutSlotOutcome, +} + +#[derive(serde::Serialize)] +#[serde(rename_all = "snake_case", tag = "kind")] +enum FanoutSlotOutcome { + Error { category: &'static str, status: u16 }, + Response { status: u16 }, + Unresolved, +} + #[derive(serde::Deserialize)] pub struct EchoBody { pub name: String, @@ -93,22 +141,252 @@ pub async fn echo_json(Json(body): Json) -> Text { Text::new(format!("Hello, {}!", body.name)) } +#[action] +pub async fn admission(RequestContext(ctx): RequestContext) -> Result { + let route_class = ctx + .route_metadata() + .and_then(|metadata| metadata.class()) + .map(str::to_owned); + let grant = ctx + .take_ingress_grant() + .ok_or_else(|| EdgeError::internal(IoError::other("admission grant was not installed")))?; + let lease = grant + .downcast::() + .map_err(|_grant| EdgeError::internal(IoError::other("admission grant type mismatch")))?; + if lease.route_class != route_class { + return Err(EdgeError::internal(IoError::other( + "admission grant route class mismatch", + ))); + } + lease + .install_response_permit() + .map_err(EdgeError::internal)?; + + json_response(&AdmissionView { + grant_consumed_once: ctx.take_ingress_grant().is_none(), + route_class, + }) +} + +#[action] +pub async fn image_demo(RequestContext(ctx): RequestContext) -> Result { + let Some(client) = ctx.http_client() else { + return proxy_not_available_response(); + }; + client + .with_proxy_marker(false) + .send(image_demo_request()?) + .await? + .into_response() +} + +fn image_demo_request() -> Result { + let options = OutboundImageOptimizerOptions::new() + .region(OutboundImageOptimizerRegion::UsEast) + .format(OutboundImageFormat::WebP) + .width(100)?; + Ok( + outbound_policy(OutboundRequest::get("https://httpbin.org/image/jpeg")?) + .cache_policy(OutboundCachePolicy::PlatformDefault) + .image_optimizer(options) + .stream_response(), + ) +} + +#[action] +pub async fn fanout(RequestContext(ctx): RequestContext) -> Result { + let Some(client) = ctx.http_client() else { + return proxy_not_available_response(); + }; + let input: FanoutRequest = ctx.json_within(MAX_FANOUT_INPUT_BYTES).await?; + if input.paths.len() > MAX_FANOUT_REQUESTS { + return Err(EdgeError::validation(format!( + "fanout accepts at most {MAX_FANOUT_REQUESTS} paths" + ))); + } + + let base = ctx + .variable("API_BASE_URL") + .unwrap_or_else(|| DEFAULT_PROXY_BASE.to_owned()); + let deadline = ctx.monotonic_clock().deadline_after(OUTBOUND_BATCH_BUDGET); + let source_uri = Uri::from_static("/fanout"); + let requests = input + .paths + .into_iter() + .map(|path| { + let target = build_proxy_target(&base, &path, &source_uri)?; + Ok(outbound_policy(OutboundRequest::new(Method::GET, target)?).deadline(deadline)) + }) + .collect::, EdgeError>>()?; + let results = client + .send_all_until(requests, deadline) + .await + .map_err(|failure| failure.into_parts().0)?; + let (slots, termination) = results.into_parts(); + let output = slots + .into_iter() + .enumerate() + .map(|(index, slot)| fanout_slot(index, slot, termination)) + .collect::, EdgeError>>()?; + + let mut response = json_response(&output)?; + response + .extensions_mut() + .insert(ResponseEgressDeadline::after(RESPONSE_EGRESS_BUDGET)); + Ok(response) +} + +#[action] +pub async fn follow_demo(RequestContext(ctx): RequestContext) -> Result { + let Some(client) = ctx.http_client() else { + return proxy_not_available_response(); + }; + let base = ctx + .variable("API_BASE_URL") + .unwrap_or_else(|| DEFAULT_PROXY_BASE.to_owned()); + let allowed = base + .parse::() + .map_err(|_error| EdgeError::bad_request("invalid follow base URI"))?; + let target = build_proxy_target(&base, "redirect/1", &Uri::from_static("/follow"))?; + if !follow_target_allowed(&allowed, &target) { + return follow_denied_response(); + } + let deadline = ctx.monotonic_clock().deadline_after(OUTBOUND_FOLLOW_BUDGET); + let request = outbound_policy(OutboundRequest::new(Method::GET, target)?).deadline(deadline); + let allowed_origin = request.uri().clone(); + let followed = client + .follow(request, OutboundRedirectLimit::new(4)?, move |redirect| { + if redirect + .prepared_request() + .is_some_and(|next| follow_target_allowed(&allowed_origin, next.uri())) + { + Ok(OutboundRedirectAction::FollowPrepared) + } else { + Err(EdgeError::bad_request("redirect target is not allowed")) + } + }) + .await; + let result = match followed { + Ok(result) => result, + Err(OutboundFollowFailure::Hop { error, .. }) => return Err(error), + Err(OutboundFollowFailure::Policy { .. }) => return follow_denied_response(), + }; + let (upstream, _, redirects_followed, _) = result.into_parts(); + let mut response = upstream.into_response()?; + response.headers_mut().insert( + "x-follow-redirects", + http::HeaderValue::from(redirects_followed), + ); + Ok(response) +} + +fn follow_denied_response() -> Result { + http::response_builder() + .status(StatusCode::FORBIDDEN) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::text("redirect target is not allowed\n")) + .map_err(EdgeError::internal) +} + +fn follow_target_allowed(allowed: &Uri, target: &Uri) -> bool { + matches!(allowed.scheme_str(), Some("http" | "https")) + && allowed + .authority() + .is_some_and(|authority| !authority.as_str().contains('@')) + && target.scheme() == allowed.scheme() + && target.authority() == allowed.authority() +} + #[action] pub async fn proxy_demo(RequestContext(ctx): RequestContext) -> Result { let params: ProxyPath = ctx.path()?; - let proxy_handle = ctx.proxy_handle(); - let request = ctx.into_request(); - let base = env::var("API_BASE_URL").unwrap_or_else(|_| DEFAULT_PROXY_BASE.to_owned()); + let http_client = ctx + .http_client() + .map(|client| client.with_proxy_marker(false)); + let base = ctx + .variable("API_BASE_URL") + .unwrap_or_else(|| DEFAULT_PROXY_BASE.to_owned()); + let request = ctx.into_request()?; let target = build_proxy_target(&base, ¶ms.rest, request.uri())?; - let proxy_request = ProxyRequest::from_request(request, target); + let outbound_request = outbound_policy(OutboundRequest::from_request(request, target)?) + .response_decoding(ResponseDecoding::Original); - if let Some(handle) = proxy_handle { - handle.forward(proxy_request).await + if let Some(client) = http_client { + client.send(outbound_request).await?.into_response() } else { proxy_not_available_response() } } +fn error_category(status: StatusCode) -> &'static str { + match status { + StatusCode::BAD_REQUEST => "bad_request", + StatusCode::BAD_GATEWAY => "bad_gateway", + StatusCode::GATEWAY_TIMEOUT => "gateway_timeout", + StatusCode::REQUEST_TIMEOUT => "request_timeout", + StatusCode::UNPROCESSABLE_ENTITY => "validation", + _ => "error", + } +} + +fn fanout_slot( + index: usize, + slot_result: OutboundCollectedSlot, + termination: OutboundBatchTermination, +) -> Result { + match slot_result { + OutboundCollectedSlot::Resolved(slot) => { + let outcome = match slot.outcome { + Ok(response) => FanoutSlotOutcome::Response { + status: response.status().as_u16(), + }, + Err(error) => FanoutSlotOutcome::Error { + category: error_category(error.status()), + status: error.status().as_u16(), + }, + }; + Ok(FanoutSlot { + elapsed_ms: Some(slot.elapsed.as_millis()), + index, + outcome, + }) + } + OutboundCollectedSlot::Unresolved if termination == OutboundBatchTermination::Cutoff => { + Ok(FanoutSlot { + elapsed_ms: None, + index, + outcome: FanoutSlotOutcome::Unresolved, + }) + } + OutboundCollectedSlot::Unresolved if termination == OutboundBatchTermination::Completed => { + Err(EdgeError::internal(IoError::other( + "completed outbound batch contained an unresolved slot", + ))) + } + OutboundCollectedSlot::AlreadyDelivered => Err(EdgeError::internal(IoError::other( + "fanout collection unexpectedly contained an already-delivered slot", + ))), + OutboundCollectedSlot::Unresolved => Err(EdgeError::internal(IoError::other( + "outbound batch contained an unresolved slot for an unsupported termination reason", + ))), + } +} + +fn outbound_policy(request: OutboundRequest) -> OutboundRequest { + request + .cache_policy(OutboundCachePolicy::Bypass) + .max_decoder_bytes(MAX_DECODER_BYTES) + .max_brotli_window_bits(MAX_BROTLI_WINDOW_BITS) + .max_decoded_response_bytes(MAX_DECODED_RESPONSE_BYTES) + .max_encoded_response_bytes(MAX_ENCODED_RESPONSE_BYTES) + .max_request_body_bytes(MAX_OUTBOUND_REQUEST_BODY_BYTES) + .max_response_bytes(MAX_FINAL_RESPONSE_BYTES) + .max_chunk_bytes(NonZeroU64::new(MAX_RESPONSE_CHUNK_BYTES).unwrap_or(NonZeroU64::MIN)) + .max_response_header_bytes(MAX_RESPONSE_HEADER_BYTES) + .max_response_header_count(MAX_RESPONSE_HEADER_COUNT) + .timeout(OUTBOUND_REQUEST_TIMEOUT) +} + fn build_proxy_target(base: &str, rest: &str, original_uri: &Uri) -> Result { let mut target = base.trim_end_matches('/').to_owned(); let trimmed_rest = rest.trim_start_matches('/'); @@ -130,9 +408,7 @@ fn build_proxy_target(base: &str, rest: &str, original_uri: &Uri) -> Result Result { - let body = Body::text( - "proxy example is not enabled for this adapter build; enable a proxy-capable adapter", - ); + let body = Body::text("outbound HTTP is not enabled for this adapter build"); http::response_builder() .status(StatusCode::NOT_IMPLEMENTED) .header("content-type", "text/plain; charset=utf-8") @@ -140,6 +416,18 @@ fn proxy_not_available_response() -> Result { .map_err(EdgeError::internal) } +fn json_response(value: &T) -> Result +where + T: serde::Serialize, +{ + let body = Body::json(value).map_err(EdgeError::internal)?; + http::response_builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .body(body) + .map_err(EdgeError::internal) +} + fn text_response(status: StatusCode, message: impl Into) -> Result { http::response_builder() .status(status) @@ -169,7 +457,7 @@ pub async fn config_get(RequestContext(ctx): RequestContext) -> Result text_response(StatusCode::OK, value), + Some(value) => text_response(StatusCode::OK, value.as_str()), None => text_response( StatusCode::NOT_FOUND, format!("config key '{}' not found", params.name), @@ -224,7 +512,7 @@ pub async fn kv_note_put( let store = kv .named("cache") .ok_or_else(|| EdgeError::service_unavailable("KV store `cache` is not registered"))?; - let body = ctx.into_request().into_body(); + let body = ctx.into_request()?.into_body(); let body_bytes = body.into_bytes_bounded(MAX_BODY_SIZE).await?; store .put_bytes(&format!("note:{}", path.id), body_bytes) @@ -321,23 +609,44 @@ pub async fn state_demo( mod tests { use super::*; use async_trait::async_trait; + use edgezero_core::app::App as EdgeZeroApp; use edgezero_core::blob_envelope::BlobEnvelope; use edgezero_core::body::Body; - use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; - use edgezero_core::context::RequestContext; + use edgezero_core::config_store::{ + BoundedStoreRead, ConfigStore, ConfigStoreError, ConfigStoreHandle, + finish_bounded_config_read, + }; + use edgezero_core::context::{RequestContext, RuntimeVariables}; use edgezero_core::http::header::{HeaderName, HeaderValue}; - use edgezero_core::http::{Method, StatusCode, Uri, request_builder}; + use edgezero_core::http::{HeaderMap, Method, StatusCode, Uri, request_builder}; use edgezero_core::key_value_store::{KvError, KvHandle, KvPage, KvStore}; + use edgezero_core::outbound::{ + HttpClient, OutboundBatch, OutboundBatchCheck, OutboundDispatchRequest, + OutboundExchangeOwner, OutboundFuture, OutboundHead, OutboundHeaderFidelity, + OutboundHeaderInput, OutboundHttpClient, OutboundPending, OutboundPreparedBatch, + OutboundProxyMarker, OutboundRequestParts, OutboundResponse, OutboundResponseContext, + OutboundSendMode, OutboundTime, ResponseMode, + }; use edgezero_core::params::PathParams; - use edgezero_core::proxy::{ProxyClient, ProxyHandle, ProxyResponse}; use edgezero_core::response::IntoResponse as _; + use edgezero_core::response_egress_framing::ResponseEgressFramingMode; use edgezero_core::secret_store::{InMemorySecretStore, SecretHandle}; use edgezero_core::store_registry::{ ConfigRegistry, ConfigStoreBinding, KvRegistry, StoreRegistry, }; + use edgezero_core::{ + BodyPayload, BudgetSource, Deadline, IndependentOutboundProgress, MonotonicClock, + MonotonicInstant, OutboundBatchDriverEvent, OutboundProgressRuntime, + }; + use futures::FutureExt as _; use futures::executor::block_on; + use futures::future::ready; use std::collections::{BTreeMap, HashMap}; + use std::future::Future; + use std::pin::Pin; + use std::rc::Rc; use std::sync::{Arc, Mutex}; + use std::task::{Context, Poll, Waker}; use std::time::Duration; struct MapConfigStore(HashMap); @@ -346,14 +655,216 @@ mod tests { data: Mutex>, } - struct TestProxyClient; + #[derive(Clone)] + struct TestTime { + state: Arc>, + } + + struct TestTimeState { + next_timer: usize, + now: MonotonicInstant, + waiters: BTreeMap, + } + + struct TestTimer { + clock: MonotonicClock, + deadline: Deadline, + id: usize, + state: Arc>, + } + + impl Default for TestTime { + fn default() -> Self { + Self { + state: Arc::new(Mutex::new(TestTimeState { + next_timer: 0, + now: MonotonicInstant::now(), + waiters: BTreeMap::new(), + })), + } + } + } + + impl TestTime { + fn advance_to(&self, now: MonotonicInstant) { + let wakers = { + let mut state = self.state.lock().expect("fake time"); + state.now = now; + state.waiters.values().cloned().collect::>() + }; + for waker in wakers { + waker.wake(); + } + } + + fn clock(&self) -> MonotonicClock { + let state = Arc::clone(&self.state); + MonotonicClock::new(move || state.lock().expect("clock").now) + } + + fn pair(&self) -> OutboundTime { + let state = Arc::clone(&self.state); + OutboundTime::new(self.clock(), move |deadline, clock| { + let id = { + let mut registration = state.lock().expect("timer registration"); + let id = registration.next_timer; + registration.next_timer = + registration.next_timer.checked_add(1).expect("timer id"); + id + }; + Box::pin(TestTimer { + clock, + deadline, + id, + state: Arc::clone(&state), + }) + }) + } + } + + impl Future for TestTimer { + type Output = Result<(), EdgeError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.deadline.is_expired_at(self.clock.now()) { + Poll::Ready(Ok(())) + } else { + self.state + .lock() + .expect("timer waiter") + .waiters + .insert(self.id, cx.waker().clone()); + Poll::Pending + } + } + } + + #[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] + impl Drop for TestTimer { + fn drop(&mut self) { + self.state + .lock() + .expect("timer cancellation") + .waiters + .remove(&self.id); + } + } + + #[derive(Clone)] + struct TestOutboundClient { + clock: TestTime, + follow_requests: Arc>>, + redirect_location: Option<&'static str>, + time: OutboundTime, + } + + impl Default for TestOutboundClient { + fn default() -> Self { + let clock = TestTime::default(); + Self { + time: clock.pair(), + clock, + follow_requests: Arc::default(), + redirect_location: None, + } + } + } + + impl TestOutboundClient { + fn finish_ready( + &self, + request: OutboundDispatchRequest, + expected_deadline: Option, + ) -> Result { + let mut future = + Box::pin(async move { self.head(request, expected_deadline)?.finish().await }); + // Fixed replies have at most one chunk; the finalizer yields once before EOF. + for _ in 0_u8..2 { + if let Some(outcome) = future.as_mut().now_or_never() { + return outcome; + } + } + Err(EdgeError::internal(IoError::other( + "scripted response exceeded its fixed poll bound", + ))) + } + + #[expect( + clippy::panic_in_result_fn, + reason = "scripted transport assertions preserve effective request checks" + )] + fn head( + &self, + request: OutboundDispatchRequest, + expected_deadline: Option, + ) -> Result { + let (parts, context) = request.into_parts(); + assert_outbound_policy(&parts); + if context.mode() == OutboundSendMode::Follow { + assert_eq!(parts.method, Method::GET); + assert_eq!(parts.response_decoding, ResponseDecoding::Decode); + assert_eq!(parts.proxy_marker, OutboundProxyMarker::On); + let deadline = parts.deadline.ok_or_else(|| { + EdgeError::internal(IoError::other("missing scripted chain deadline")) + })?; + self.follow_requests + .lock() + .map_err(|_error| { + EdgeError::internal(IoError::other("scripted request log poisoned")) + })? + .push((parts.uri.clone(), deadline)); + } else if context.mode() == OutboundSendMode::Batch { + assert_eq!(parts.response_decoding, ResponseDecoding::Decode); + assert_eq!(parts.proxy_marker, OutboundProxyMarker::On); + let deadline = parts.deadline.expect("batch deadline"); + if let Some(expected) = expected_deadline { + assert_eq!(deadline.instant(), expected.instant()); + } + } else { + assert_eq!(parts.method, Method::POST); + assert!( + parts + .uri + .path_and_query() + .is_some_and(|value| value.as_str() == "/status/201?source=demo") + ); + assert!(parts.deadline.is_none()); + assert_eq!(parts.response_decoding, ResponseDecoding::Original); + assert_eq!(parts.proxy_marker, OutboundProxyMarker::Off); + } + response_for(&parts, context, self.redirect_location) + } + } struct UnavailableConfigStore; #[async_trait::async_trait(?Send)] impl ConfigStore for MapConfigStore { - async fn get(&self, key: &str) -> Result, ConfigStoreError> { - Ok(self.0.get(key).cloned()) + async fn get( + &self, + key: &str, + ) -> Result, ConfigStoreError> { + Ok(self.0.get(key).cloned().map(Into::into)) + } + + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + finish_bounded_config_read( + Ok(self.0.get(key).cloned()), + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) } } @@ -419,34 +930,368 @@ mod tests { } } - #[async_trait(?Send)] - impl ProxyClient for TestProxyClient { - async fn send(&self, request: ProxyRequest) -> Result { - let (_method, uri, _headers, _body, _) = request.into_parts(); - assert!(uri.to_string().contains("status/201")); - Ok(ProxyResponse::new(StatusCode::CREATED, Body::empty())) + impl OutboundHttpClient for TestOutboundClient { + fn begin(&self, request: OutboundDispatchRequest) -> Result { + let outcome = self.finish_ready(request, None); + Ok(OutboundPending::new( + Box::pin(ready(outcome)), + OutboundExchangeOwner::new(|| {}), + )) + } + + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture { + Box::pin(ready(self.head(request, None))) + } + + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let started_at = batch.started_at(); + let observation_cutoff = batch.observation_cutoff(); + let (context, requests) = batch.into_slots(); + if observation_cutoff.is_expired_at(context.clock().now()) { + return OutboundBatch::cutoff(context); + } + let mut results = Vec::with_capacity(requests.len()); + let mut cutoff_unresolved = false; + for (index, request) in requests { + let budget = request.budget(); + let elapsed = Duration::from_millis( + u64::try_from(index).expect("slot index").saturating_add(1), + ); + self.clock + .advance_to(started_at.checked_add(elapsed).expect("terminal instant")); + let outcome = self.finish_ready(request, Some(observation_cutoff)); + let completed_at = self.time.clock().now(); + cutoff_unresolved |= observation_cutoff.is_expired_at(completed_at) + && budget.deadline.instant() >= observation_cutoff.instant(); + results.push(OutboundBatchDriverEvent::Item { + index, + completed_at, + outcome, + }); + } + if cutoff_unresolved { + results.push(OutboundBatchDriverEvent::Cutoff); + } + OutboundBatch::from_driver( + context, + stream::iter(results), + OutboundExchangeOwner::new(|| {}), + ) + } + + fn time(&self) -> OutboundTime { + self.time.clone() + } + + fn validate_supported_options( + &self, + _request: &OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + Ok(()) } } #[async_trait::async_trait(?Send)] impl ConfigStore for UnavailableConfigStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { Err(ConfigStoreError::unavailable("backend offline")) } + + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + finish_bounded_config_read( + self.get(key).await, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) + } } struct FixedStore(String); #[async_trait(?Send)] impl ConfigStore for FixedStore { - async fn get(&self, _key: &str) -> Result, ConfigStoreError> { - Ok(Some(self.0.clone())) + async fn get( + &self, + _key: &str, + ) -> Result, ConfigStoreError> { + Ok(Some(self.0.clone().into())) + } + + async fn get_bounded( + &self, + key: &str, + clock: &MonotonicClock, + deadline: Deadline, + max_backend_bytes: u64, + max_value_bytes: u64, + ) -> Result, ConfigStoreError> { + if deadline.is_expired_at(clock.now()) { + return Err(ConfigStoreError::DeadlineExceeded); + } + finish_bounded_config_read( + self.get(key).await, + clock, + deadline, + max_backend_bytes, + max_value_bytes, + ) } } + fn assert_outbound_policy(parts: &OutboundRequestParts) { + assert_eq!(parts.cache_policy, OutboundCachePolicy::Bypass); + assert_eq!(parts.timeout, Some(OUTBOUND_REQUEST_TIMEOUT)); + assert_eq!( + parts.max_request_body_bytes, + MAX_OUTBOUND_REQUEST_BODY_BYTES + ); + assert_eq!( + parts.max_encoded_response_bytes, + Some(MAX_ENCODED_RESPONSE_BYTES) + ); + assert_eq!( + parts.max_decoded_response_bytes, + Some(MAX_DECODED_RESPONSE_BYTES) + ); + assert_eq!( + parts.response_mode, + ResponseMode::Buffered { + max_bytes: MAX_FINAL_RESPONSE_BYTES, + } + ); + assert_eq!( + parts.max_response_header_bytes, + Some(MAX_RESPONSE_HEADER_BYTES) + ); + assert_eq!( + parts.max_response_header_count, + Some(MAX_RESPONSE_HEADER_COUNT) + ); + assert_eq!( + parts.max_chunk_bytes, + Some(NonZeroU64::new(MAX_RESPONSE_CHUNK_BYTES).unwrap_or(NonZeroU64::MIN)) + ); + assert_eq!(parts.max_brotli_window_bits, MAX_BROTLI_WINDOW_BITS); + assert_eq!(parts.max_decoder_bytes, MAX_DECODER_BYTES); + } + + fn response_for( + parts: &OutboundRequestParts, + context: OutboundResponseContext, + redirect_location: Option<&'static str>, + ) -> Result { + if parts.uri.path() == "/fail" { + return Err(EdgeError::gateway_timeout_caused( + "provider URL https://user:token@example.invalid", + BudgetSource::RequestDeadline, + )); + } + let status = match parts.uri.path() { + "/redirect/1" => StatusCode::FOUND, + "/status/201" => StatusCode::CREATED, + "/status/204" => StatusCode::NO_CONTENT, + _ => StatusCode::OK, + }; + let mut headers = HeaderMap::new(); + headers.insert("x-outbound-test", HeaderValue::from_static("preserved")); + if status == StatusCode::FOUND { + headers.insert( + "location", + HeaderValue::from_static(redirect_location.expect("scripted redirect target")), + ); + } + let encoded = if status == StatusCode::NO_CONTENT { + stream::empty().boxed_local() + } else { + stream::iter([Ok(Bytes::from_static(b"outbound-response"))]).boxed_local() + }; + OutboundHead::new( + status, + OutboundHeaderInput::new( + "scripted", + headers, + OutboundHeaderFidelity::AdapterVisibleFields, + ), + encoded, + context, + OutboundExchangeOwner::new(|| {}), + ) + } + + #[test] + fn follow_demo_uses_supplied_head_client_and_app_deadline() { + let scripted = TestOutboundClient { + redirect_location: Some("/status/201"), + ..TestOutboundClient::default() + }; + let start = scripted.time.clock().now(); + let requests = Arc::clone(&scripted.follow_requests); + let response = run_follow_demo(scripted, "https://allowed.example"); + assert_eq!(response.status(), StatusCode::CREATED); + assert_eq!(response.headers()["x-follow-redirects"], "1"); + assert_eq!(response.headers()["x-outbound-test"], "preserved"); + assert_eq!( + response.into_body().into_bytes().expect("buffered"), + Bytes::from_static(b"outbound-response") + ); + let recorded = requests.lock().expect("recorded requests"); + let expected_deadline = start.checked_add(Duration::from_secs(5)).expect("deadline"); + assert_eq!(recorded.len(), 2); + for (request, path) in recorded.iter().zip(["/redirect/1", "/status/201"]) { + assert_eq!(request.0.scheme_str(), Some("https")); + assert_eq!( + request.0.authority().expect("authority").as_str(), + "allowed.example" + ); + assert_eq!(request.0.path(), path); + assert_eq!(request.1.instant(), expected_deadline); + } + } + + #[test] + fn follow_demo_allows_canonical_equivalent_origins() { + for base in [ + "https://example.com:443", + "https://ExAmPlE.CoM", + "https://ExAmPlE.CoM:443", + ] { + let scripted = TestOutboundClient { + redirect_location: Some("/status/201"), + ..TestOutboundClient::default() + }; + let requests = Arc::clone(&scripted.follow_requests); + let response = run_follow_demo(scripted, base); + assert_eq!(response.status(), StatusCode::CREATED); + assert_eq!(response.headers()["x-follow-redirects"], "1"); + let recorded = requests.lock().expect("recorded requests"); + assert_eq!(recorded.len(), 2); + for (request, path) in recorded.iter().zip(["/redirect/1", "/status/201"]) { + assert_eq!(request.0.scheme_str(), Some("https")); + assert_eq!( + request.0.authority().expect("authority").as_str(), + "example.com" + ); + assert_eq!(request.0.path(), path); + } + } + } + + #[test] + fn follow_demo_checks_initial_target_before_dispatch() { + for base in [ + "ftp://allowed.example", + "https://user:secret@allowed.example", + ] { + let scripted = TestOutboundClient::default(); + let requests = Arc::clone(&scripted.follow_requests); + let response = run_follow_demo(scripted, base); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.into_body().into_bytes().expect("buffered"), + Bytes::from_static(b"redirect target is not allowed\n") + ); + assert!(requests.lock().expect("recorded requests").is_empty()); + } + } + + #[test] + fn follow_demo_stops_after_four_redirects() { + let scripted = TestOutboundClient { + redirect_location: Some("/redirect/1"), + ..TestOutboundClient::default() + }; + let requests = Arc::clone(&scripted.follow_requests); + let response = run_follow_demo(scripted, "https://allowed.example"); + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(response.headers()["location"], "/redirect/1"); + assert_eq!(response.headers()["x-follow-redirects"], "4"); + assert_eq!(requests.lock().expect("recorded requests").len(), 5); + } + + #[test] + fn follow_demo_denies_changed_origin_without_sending_another_hop() { + for base in ["https://allowed.example", "https://AlLoWeD.ExAmPlE:443"] { + for target in [ + "https://other.example/private?token=secret", + "http://allowed.example/private?token=secret", + "https://allowed.example:8443/private?token=secret", + "mailto:user@example.invalid", + ] { + let scripted = TestOutboundClient { + redirect_location: Some(target), + ..TestOutboundClient::default() + }; + let requests = Arc::clone(&scripted.follow_requests); + let response = run_follow_demo(scripted, base); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.into_body().into_bytes().expect("buffered"), + Bytes::from_static(b"redirect target is not allowed\n") + ); + assert_eq!(requests.lock().expect("recorded requests").len(), 1); + } + } + } + + fn run_follow_demo(scripted: TestOutboundClient, base: &str) -> Response { + let mut app = EdgeZeroApp::new(crate::build_router().expect("demo routes")); + app.set_monotonic_clock(scripted.time.clock().clone()); + let mut request = request_builder() + .uri("/follow") + .body(Body::empty()) + .expect("request"); + request + .extensions_mut() + .insert(HttpClient::with_client(scripted)); + request + .extensions_mut() + .insert(RuntimeVariables::from_vars([( + "API_BASE_URL".to_owned(), + base.to_owned(), + )])); + let outcome = block_on(app.dispatch_ingress( + request, + app.monotonic_now(), + edgezero_core::IngressHeadAccounting::HostManaged, + edgezero_core::IngressFraming::HostManaged, + )) + .expect("ingress dispatch"); + let edgezero_core::IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("response outcome"); + }; + let Ok((prepared, _, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("response egress"); + }; + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + prepared.into_response() + } + #[test] fn introspection_routes_are_wired() { - let router = crate::build_router(); + let router = crate::build_router().expect("demo router"); // manifest: 200 + JSON body whose [app].name is "app-demo". let manifest_req = request_builder() @@ -785,7 +1630,7 @@ mod tests { #[test] fn echo_json_formats_payload() { - let ctx = context_with_json("/echo", r#"{"name":"Edge"}"#); + let ctx = context_with_json("/echo", &serde_json::json!({"name": "Edge"}).to_string()); let response = block_on(echo_json(ctx)) .expect("handler ok") .into_response() @@ -906,15 +1751,15 @@ mod tests { } #[test] - fn proxy_demo_uses_injected_handle() { + fn app_demo_outbound_client_preserves_method_and_uri() { let mut request = request_builder() - .method(Method::GET) - .uri("/proxy/status/201") - .body(Body::empty()) + .method(Method::POST) + .uri("/proxy/status/201?source=demo") + .body(Body::text("request-body")) .expect("request"); request .extensions_mut() - .insert(ProxyHandle::with_client(TestProxyClient)); + .insert(HttpClient::with_client(TestOutboundClient::default())); let mut params = HashMap::new(); params.insert("rest".to_owned(), "status/201".to_owned()); @@ -922,15 +1767,299 @@ mod tests { let response = block_on(proxy_demo(ctx)).expect("response"); assert_eq!(response.status(), StatusCode::CREATED); + assert_eq!(response.headers()["x-outbound-test"], "preserved"); + assert_eq!( + response + .into_body() + .into_bytes() + .expect("buffered") + .as_ref(), + b"outbound-response" + ); } #[test] - fn proxy_demo_without_handle_returns_placeholder() { + fn app_demo_outbound_client_retains_its_clock_in_responses() { + let scripted = TestOutboundClient::default(); + let start = scripted.time.clock().now(); + let clock = scripted.clock.clone(); + let client = HttpClient::with_client(scripted).with_proxy_marker(false); + let request = outbound_policy( + OutboundRequest::post("https://example.com/status/201?source=demo").expect("request"), + ) + .response_decoding(ResponseDecoding::Original); + let response = block_on(client.send(request)).expect("response"); + let deadline = start.checked_add(Duration::from_secs(1)).expect("deadline"); + clock.advance_to(deadline); + + let error = block_on(response.into_bytes_bounded_until(64, Deadline::at_instant(deadline))) + .expect_err("retained clock reached deadline"); + + assert!(matches!(error, EdgeError::GatewayTimeout { .. })); + } + + #[test] + fn fanout_rejects_delivered_and_completed_unresolved_slots() { + for slot in [ + OutboundCollectedSlot::AlreadyDelivered, + OutboundCollectedSlot::Unresolved, + ] { + let error = fanout_slot(0, slot, OutboundBatchTermination::Completed) + .err() + .expect("invalid collection state"); + assert_eq!(error.status(), StatusCode::INTERNAL_SERVER_ERROR); + } + let slot = fanout_slot( + 0, + OutboundCollectedSlot::Unresolved, + OutboundBatchTermination::Cutoff, + ) + .expect("cutoff slot"); + assert!(matches!(slot.outcome, FanoutSlotOutcome::Unresolved)); + } + + #[test] + fn scripted_batch_cutoff_retains_only_earlier_completions() { + let scripted = TestOutboundClient::default(); + let time = scripted.time.clone(); + let observation_cutoff = time.clock().deadline_after(Duration::from_millis(2)); + let requests = ["status/200", "status/204"].map(|path| { + outbound_policy( + OutboundRequest::get(format!("https://example.com/{path}")).expect("request"), + ) + .deadline(observation_cutoff) + }); + let batch = HttpClient::with_client(scripted) + .start_batch_until(requests.into(), observation_cutoff); + let results = block_on(batch.collect()).expect("cutoff must not become a driver failure"); + let (collected, termination) = results.into_parts(); + assert_eq!(termination, OutboundBatchTermination::Cutoff); + assert_eq!(collected.len(), 2); + let mut slots = collected.into_iter(); + let Some(OutboundCollectedSlot::Resolved(first)) = slots.next() else { + panic!("first response completed before cutoff"); + }; + assert_eq!(first.elapsed, Duration::from_millis(1)); + assert_eq!( + first.outcome.as_ref().expect("first response").status(), + StatusCode::OK + ); + assert!(matches!( + slots.next(), + Some(OutboundCollectedSlot::Unresolved) + )); + assert!(slots.next().is_none()); + assert_eq!(time.clock().now(), observation_cutoff.instant()); + } + + #[test] + fn scripted_batch_expired_cutoff_keeps_preflight_outcomes() { + for invalid_sibling in [false, true] { + let scripted = TestOutboundClient::default(); + let time = scripted.time.clone(); + let entered_at = time.clock().now(); + let observation_cutoff = Deadline::at_instant(entered_at); + let first = outbound_policy( + OutboundRequest::get("https://example.com/status/200").expect("request"), + ); + let mut second = outbound_policy( + OutboundRequest::get("https://example.com/status/204").expect("request"), + ); + if invalid_sibling { + second = second.stream_response(); + } + let batch = HttpClient::with_client(scripted) + .start_batch_until(vec![first, second], observation_cutoff); + let results = block_on(batch.collect()).expect("expired entry preserves its context"); + let (collected, termination) = results.into_parts(); + assert_eq!(termination, OutboundBatchTermination::Cutoff); + assert_eq!(collected.len(), 2); + assert!(matches!(collected[0], OutboundCollectedSlot::Unresolved)); + let last = collected.into_iter().nth(1).expect("second slot"); + if invalid_sibling { + let OutboundCollectedSlot::Resolved(rejected) = last else { + panic!("original preflight rejection must survive"); + }; + assert_eq!( + rejected + .outcome + .as_ref() + .expect_err("streamed batch response") + .status(), + StatusCode::BAD_REQUEST + ); + } else { + assert!(matches!(last, OutboundCollectedSlot::Unresolved)); + } + assert_eq!(time.clock().now(), entered_at, "no eligible scripted work"); + } + } + + #[test] + fn scripted_batch_preserves_receipts_through_mixed_collection() { + let scripted = TestOutboundClient::default(); + let observation_cutoff = scripted.time.clock().deadline_after(OUTBOUND_BATCH_BUDGET); + let client = HttpClient::with_client(scripted); + let requests = ["status/200", "status/204", "fail"].map(|path| { + outbound_policy( + OutboundRequest::get(format!("https://example.com/{path}")).expect("request"), + ) + .deadline(observation_cutoff) + }); + let mut batch = client.start_batch_until(requests.into(), observation_cutoff); + let mut accumulator = batch.accumulator(); + let OutboundBatchCheck::Item(item) = batch.check().expect("ready scripted item") else { + panic!("scripted work must already be complete"); + }; + assert_eq!(item.index(), 0); + accumulator.insert(item).expect("receipt belongs to batch"); + let result = block_on(batch.collect()).expect("remaining collection"); + assert!(matches!( + result.slots()[0], + OutboundCollectedSlot::AlreadyDelivered + )); + let merged = accumulator + .merge_success(result) + .expect("merge original receipt"); + assert_eq!(merged.termination(), OutboundBatchTermination::Completed); + assert!( + merged + .slots() + .iter() + .all(|slot| matches!(slot, OutboundCollectedSlot::Resolved(_))) + ); + } + + #[test] + fn fanout_reports_positional_elapsed_and_typed_outcomes() { + let ctx = fanout_context( + &serde_json::json!({"paths": ["status/200", "status/204", "fail"]}).to_string(), + ); + let response = block_on(fanout(ctx)).expect("fanout response"); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.headers()["content-type"], "application/json"); + assert!( + response + .extensions() + .get::() + .is_some() + ); + assert!( + !String::from_utf8_lossy(response.body().as_bytes().expect("buffered")) + .contains("token@example.invalid") + ); + + let payload: serde_json::Value = response.body().to_json().expect("json"); + assert_eq!(payload[0]["index"], 0_i64); + assert_eq!(payload[0]["elapsed_ms"], 1_i64); + assert_eq!(payload[0]["outcome"]["kind"], "response"); + assert_eq!(payload[0]["outcome"]["status"], 200_i64); + assert_eq!(payload[1]["index"], 1_i64); + assert_eq!(payload[1]["elapsed_ms"], 2_i64); + assert_eq!(payload[1]["outcome"]["status"], 204_i64); + assert_eq!(payload[2]["index"], 2_i64); + assert_eq!(payload[2]["elapsed_ms"], 3_i64); + assert_eq!(payload[2]["outcome"]["kind"], "error"); + assert_eq!(payload[2]["outcome"]["category"], "gateway_timeout"); + assert_eq!(payload[2]["outcome"]["status"], 504_i64); + } + + #[test] + fn outbound_fixture_settles_no_content_body_at_source() { + let scripted = TestOutboundClient::default(); + let observation_cutoff = scripted.time.clock().deadline_after(OUTBOUND_BATCH_BUDGET); + let request = outbound_policy( + OutboundRequest::get("https://example.com/status/204").expect("request"), + ) + .deadline(observation_cutoff); + let results = block_on( + HttpClient::with_client(scripted).send_all_until(vec![request], observation_cutoff), + ) + .expect("collection"); + let (slots, _) = results.into_parts(); + let Some(OutboundCollectedSlot::Resolved(slot)) = slots.into_iter().next() else { + panic!("resolved no-content response"); + }; + let response = slot.outcome.expect("response"); + + assert_eq!(response.status(), StatusCode::NO_CONTENT); + assert_eq!( + response + .into_response() + .expect("portable response") + .into_body() + .into_bytes() + .expect("buffered"), + Bytes::new() + ); + } + + #[test] + fn fanout_with_empty_input_returns_an_empty_array() { + let ctx = fanout_context(&serde_json::json!({"paths": []}).to_string()); + let response = block_on(fanout(ctx)).expect("fanout response"); + let payload: serde_json::Value = response.body().to_json().expect("json"); + assert_eq!(payload, serde_json::json!([])); + } + + #[test] + fn fanout_rejects_more_than_eight_paths() { + let ctx = fanout_context( + &serde_json::json!({"paths": ["1", "2", "3", "4", "5", "6", "7", "8", "9"]}) + .to_string(), + ); + let error = block_on(fanout(ctx)).expect_err("oversized fanout must fail"); + assert_eq!(error.status(), StatusCode::UNPROCESSABLE_ENTITY); + } + + #[test] + fn proxy_demo_without_client_returns_placeholder() { let ctx = context_with_params("/proxy/status/200", &[("rest", "status/200")]); let response = block_on(proxy_demo(ctx)).expect("response"); assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED); } + #[test] + fn image_demo_uses_default_cache_and_preserves_outbound_limits() { + let request = image_demo_request().expect("native image example"); + let options = request + .image_optimizer_options() + .expect("optimizer options"); + assert_eq!( + options.region_setting(), + Some(&OutboundImageOptimizerRegion::UsEast) + ); + assert_eq!(options.width_setting(), Some(100)); + assert_eq!(options.format_setting(), Some(OutboundImageFormat::WebP)); + let parts = request.into_parts(); + assert_eq!(parts.cache_policy, OutboundCachePolicy::PlatformDefault); + assert_eq!(parts.response_mode, edgezero_core::ResponseMode::Streamed); + assert_eq!( + parts.max_encoded_response_bytes, + Some(MAX_ENCODED_RESPONSE_BYTES) + ); + assert_eq!( + parts.max_decoded_response_bytes, + Some(MAX_DECODED_RESPONSE_BYTES) + ); + assert_eq!( + parts.max_response_header_bytes, + Some(MAX_RESPONSE_HEADER_BYTES) + ); + } + + fn fanout_context(json: &str) -> RequestContext { + let mut request = request_builder() + .method(Method::POST) + .uri("/fanout") + .body(Body::from(json)) + .expect("request"); + request + .extensions_mut() + .insert(HttpClient::with_client(TestOutboundClient::default())); + RequestContext::new(request, PathParams::default()) + } + #[test] fn root_returns_static_body() { let ctx = empty_context("/"); @@ -984,7 +2113,8 @@ mod tests { .to_vec(), ) .expect("utf8"); - assert!(body.contains("required secret is not configured")); + assert!(body.contains("internal server error")); + assert!(!body.contains("required secret is not configured")); assert!(!body.contains("SMOKE_SECRET_MISSING")); } @@ -1009,7 +2139,10 @@ mod tests { let response = block_on(stream(ctx)).expect("handler ok"); assert_eq!(response.status(), StatusCode::OK); - let mut chunks = response.into_body().into_stream().expect("stream body"); + let (payload, body_owner) = response.into_body().into_parts(); + let BodyPayload::Stream(mut chunks) = payload else { + panic!("stream body"); + }; let collected = block_on(async { let mut buf = Vec::new(); while let Some(item) = chunks.next().await { @@ -1022,12 +2155,13 @@ mod tests { String::from_utf8(collected).expect("utf8"), "chunk 0\nchunk 1\nchunk 2\n" ); + drop(body_owner); } #[test] fn state_demo_handler_reads_app_state_through_macro_router() { // build_router() is macro-generated and now calls `.with_state(crate::app_state())`. - let service = crate::build_router(); + let service = crate::build_router().expect("demo router"); let request = request_builder() .method(Method::GET) diff --git a/examples/app-demo/crates/app-demo-core/src/lib.rs b/examples/app-demo/crates/app-demo-core/src/lib.rs index e2d4beba3..45bde1de5 100644 --- a/examples/app-demo/crates/app-demo-core/src/lib.rs +++ b/examples/app-demo/crates/app-demo-core/src/lib.rs @@ -8,7 +8,146 @@ pub mod config; // internally; pub visibility is purely additive. pub mod handlers; +use std::error::Error; +use std::fmt; +#[cfg(test)] +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, OnceLock}; +use std::time::Duration; + +use edgezero_core::app::App as EdgeZeroApp; +use edgezero_core::http::{HeaderMap, HeaderValue, StatusCode}; +use edgezero_core::{ + AdmissionDecision, BufferedIngressResponse, ConfigExtractionLimits, + DEFAULT_RESPONSE_WRITE_BUDGET, DetachedResponseEgressDecision, EdgeError, IngressGrant, + MemoryCeiling, MemoryCeilingScope, MemoryEnvelope, MemoryEnvelopeValidation, + MemoryEnvelopeValidationError, MemoryResource, PlatformMetadata, ResponseEgressCompletion, + ResponseEgressResource, ResponseEgressResourceInstallError, RouteResolution, +}; + +const DEFAULT_INGRESS_READ_BUDGET: Duration = Duration::from_secs(30); +const FALLBACK_INGRESS_BODY_BYTES: usize = 4 * 1024; +const FALLBACK_INGRESS_READ_BUDGET: Duration = Duration::from_secs(5); +const OUTBOUND_INGRESS_READ_BUDGET: Duration = Duration::from_secs(10); +const EXPECTED_FIXED_MEMORY_BYTES: u64 = 4 * 1024 * 1024; +const EXPECTED_PER_LIVE_REQUEST_BYTES: u64 = 1024 * 1024; +const EXPECTED_SEPARATE_STACK_BYTES: u64 = 256 * 1024; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PlatformMemoryStartupError { + Exceeds { + available_bytes: u64, + required_bytes: u64, + resource: MemoryResource, + }, + UnsupportedValidationOutcome, + Validation { + source: MemoryEnvelopeValidationError, + }, +} + +impl fmt::Display for PlatformMemoryStartupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Self::Exceeds { + available_bytes, + required_bytes, + resource, + } => { + let resource_label = match resource { + MemoryResource::Primary => "primary memory", + MemoryResource::SeparateStack => "separate stack", + _ => "unrecognized memory resource", + }; + write!( + f, + "application memory envelope exceeds {resource_label}: required {required_bytes} bytes, available {available_bytes} bytes" + ) + } + Self::UnsupportedValidationOutcome => { + f.write_str("unsupported platform memory validation outcome") + } + Self::Validation { source } => source.fmt(f), + } + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "the typed validation source is the only nested startup error" +)] +impl Error for PlatformMemoryStartupError { + #[expect( + clippy::pattern_type_mismatch, + reason = "match ergonomics preserve the borrowed validation source" + )] + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Validation { source } => Some(source), + Self::Exceeds { .. } | Self::UnsupportedValidationOutcome => None, + } + } +} + +#[derive(Debug)] +struct AdmissionLease { + #[cfg(test)] + release_counter: Option>, + response_resource: ResponseEgressResource, + route_class: Option, +} + +impl AdmissionLease { + fn install_response_permit(&self) -> Result<(), ResponseEgressResourceInstallError> { + let permit = ResponsePermit { + #[cfg(test)] + release_counter: self.release_counter.clone(), + route_class: self.route_class.clone(), + }; + self.response_resource.install(permit) + } + + #[cfg(test)] + fn observe_response_permit_releases(&mut self, counter: Arc) { + self.release_counter = Some(counter); + } +} + +/// Demonstration stand-in for an application semaphore permit. +#[derive(Debug)] +struct ResponsePermit { + #[cfg(test)] + release_counter: Option>, + route_class: Option, +} + +impl ResponsePermit { + fn new(route_class: Option) -> Self { + Self { + #[cfg(test)] + release_counter: None, + route_class, + } + } + + fn release(self) { + drop(self); + } +} + +#[expect( + clippy::missing_trait_methods, + reason = "`Drop::pin_drop` is unstable (`pin_ergonomics`) and conflicts with `drop`" +)] +impl Drop for ResponsePermit { + fn drop(&mut self) { + drop(self.route_class.take()); + #[cfg(test)] + if let Some(counter) = self.release_counter.take() { + counter.fetch_add(1, Ordering::SeqCst); + } + } +} /// App-owned shared state for the `app!(..., state = ...)` demonstration, /// handed to handlers via `State>`. @@ -18,16 +157,143 @@ pub struct DemoState { pub greeting: String, } +/// Installs request-lifecycle policy before any adapter begins polling a body. +fn configure_app(app: &mut EdgeZeroApp) -> Result<(), EdgeError> { + platform_memory_startup(app.platform()).map_err(EdgeError::internal)?; + app.set_config_extraction_limits(ConfigExtractionLimits::default())?; + let mut headers = HeaderMap::new(); + headers.insert( + "x-application-version", + HeaderValue::from_static(env!("CARGO_PKG_VERSION")), + ); + app.set_synthesized_response_headers(headers) + .map_err(EdgeError::internal)?; + app.set_detached_response_egress_decision_factory(|head| { + let permit = ResponsePermit::new(Some(format!("detached:{}", head.error_kind()))); + let completion = ResponseEgressCompletion::new(move |_report| { + permit.release(); + }); + DetachedResponseEgressDecision::Send { + completion, + deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET), + } + }); + app.set_ingress_admission_policy(|head| { + if let RouteResolution::Matched(metadata) = head.route_resolution().clone() { + let route_class = metadata.class().map(str::to_owned); + let read_budget = if route_class.as_deref() == Some("outbound") { + OUTBOUND_INGRESS_READ_BUDGET + } else { + DEFAULT_INGRESS_READ_BUDGET + }; + let permit = ResponsePermit::new(route_class.clone()); + let (lease, completion) = response_lifecycle(route_class, permit); + AdmissionDecision::Admit { + completion, + grant: IngressGrant::new(lease), + read_deadline: head.read_deadline_after(read_budget), + } + } else { + let (lease, completion) = response_lifecycle(None, ResponsePermit::new(None)); + AdmissionDecision::ReadBodyBeforeFallback { + completion, + grant: IngressGrant::new(lease), + max_body_bytes: FALLBACK_INGRESS_BODY_BYTES, + read_deadline: head.read_deadline_after(FALLBACK_INGRESS_READ_BUDGET), + on_exceeded: BufferedIngressResponse::text( + StatusCode::PAYLOAD_TOO_LARGE, + "request body too large\n", + ), + on_timeout: BufferedIngressResponse::text( + StatusCode::REQUEST_TIMEOUT, + "request timeout\n", + ), + } + } + }); + Ok(()) +} + +fn platform_memory_validation( + platform: PlatformMetadata, +) -> Result { + let known_ceiling = platform.memory_ceiling().value(); + let scope = known_ceiling.map_or(MemoryCeilingScope::PerExecution, MemoryCeiling::scope); + let (fixed_bytes, separate_stack_bytes) = + match known_ceiling.and_then(MemoryCeiling::stack_bytes) { + Some(_) => ( + EXPECTED_FIXED_MEMORY_BYTES, + Some(EXPECTED_SEPARATE_STACK_BYTES), + ), + None => ( + EXPECTED_FIXED_MEMORY_BYTES + EXPECTED_SEPARATE_STACK_BYTES, + None, + ), + }; + let envelope = MemoryEnvelope::new( + scope, + fixed_bytes, + EXPECTED_PER_LIVE_REQUEST_BYTES, + separate_stack_bytes, + ); + platform.validate_memory_envelope(envelope) +} + +fn platform_memory_startup(platform: PlatformMetadata) -> Result<(), PlatformMemoryStartupError> { + let validation = platform_memory_validation(platform) + .map_err(|source| PlatformMemoryStartupError::Validation { source })?; + match validation { + MemoryEnvelopeValidation::Fits { .. } => Ok(()), + MemoryEnvelopeValidation::Indeterminate { .. } => { + // Unknown facts remain indeterminate; startup may continue without claiming `Fits` + // or adding an application concurrency gate. + Ok(()) + } + MemoryEnvelopeValidation::Exceeds { + available_bytes, + required_bytes, + resource, + } => Err(PlatformMemoryStartupError::Exceeds { + available_bytes, + required_bytes, + resource, + }), + _ => Err(PlatformMemoryStartupError::UnsupportedValidationOutcome), + } +} + +/// Composes a held permit with a core-owned late-bound response resource. +fn response_lifecycle( + route_class: Option, + permit: ResponsePermit, +) -> (AdmissionLease, ResponseEgressCompletion) { + let static_completion = ResponseEgressCompletion::new(move |_report| { + permit.release(); + }); + let (response_resource, late_bound_completion) = ResponseEgressCompletion::late_bound(); + ( + AdmissionLease { + #[cfg(test)] + release_counter: None, + response_resource, + route_class, + }, + static_completion.join(late_bound_completion), + ) +} + /// Returns the shared app state, referenced by `app!(..., state = crate::app_state())`. /// /// IMPORTANT: `app!(state = )` emits this call inside the macro-generated -/// `build_router()`, which every adapter's `run_app` invokes via `A::build_app()` +/// `build_router()`, which every adapter's `run_app` invokes via +/// `EdgeZeroApp::build::(platform)` /// — once at startup for long-lived runtimes (Axum), but **once per request** on /// default Fastly, Cloudflare and Spin entry points. Fastly starts a fresh Wasm /// instance by default; Cloudflare and Spin also rebuild the app per request. /// So `app_state()` must be **cheap**: build the heavy state once and hand out /// clones. Here a `OnceLock>` builds it lazily and every call just /// bumps the `Arc` refcount — do NOT `Arc::new(..)` a heavy object on each call. +/// Fallible state loaders can instead use `state = load_state()?`; cache only success. #[must_use] #[inline] pub fn app_state() -> Arc { @@ -39,4 +305,533 @@ pub fn app_state() -> Arc { })) } -edgezero_core::app!("../../edgezero.toml", state = crate::app_state()); +edgezero_core::app!( + "../../edgezero.toml", + configure = crate::configure_app, + state = crate::app_state() +); + +#[cfg(test)] +mod lifecycle_tests { + use edgezero_core::response_egress_framing::ResponseEgressFramingMode; + use std::num::NonZeroU32; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::time::Duration; + + use bytes::Bytes; + use edgezero_core::app::App as EdgeZeroApp; + use edgezero_core::body::Body; + use edgezero_core::error::EdgeError; + use edgezero_core::http::{HeaderMap, Method, Response, StatusCode, Version, request_builder}; + use edgezero_core::ingress::{ + AdmissionDecision, IngressBeginOutcome, IngressDispatchOutcome, IngressGrant, + IngressHeadParts, + }; + use edgezero_core::response_egress::ResponseEgressEnvelope; + use edgezero_core::router::RouteResolution; + use edgezero_core::time::MonotonicInstant; + use futures::executor::block_on; + use futures::stream::iter; + + #[test] + fn manual_assembly_and_entrypoint_share_the_registered_demo_state() { + let router = crate::build_router().expect("demo routes"); + let app = EdgeZeroApp::from_router::( + router, + edgezero_core::PlatformMetadata::default(), + ) + .expect("configured demo app"); + let registered = app + .state::>() + .expect("registered demo state"); + assert!(Arc::ptr_eq(registered, &crate::app_state())); + assert_eq!(registered.greeting, "hello from app state"); + } + + #[test] + fn manifest_route_classes_reach_route_resolution() { + let resolved = crate::build_router() + .expect("demo router") + .resolve(&Method::GET, "/proxy/status/200"); + let RouteResolution::Matched(metadata) = resolved.resolution().clone() else { + panic!("proxy route must resolve"); + }; + assert_eq!(metadata.class(), Some("outbound")); + } + + #[test] + fn platform_memory_validation_preserves_unknown_facts() { + let validation = + super::platform_memory_validation(edgezero_core::PlatformMetadata::default()) + .expect("valid demo envelope"); + + let edgezero_core::MemoryEnvelopeValidation::Indeterminate { reasons, .. } = validation + else { + panic!("unknown platform facts must prevent complete validation"); + }; + assert_eq!(reasons.len(), 3); + } + + fn known_platform( + ceiling: edgezero_core::MemoryCeiling, + population: NonZeroU32, + ) -> edgezero_core::PlatformMetadata { + let source = edgezero_core::PlatformResourceSource::PlatformLimit { + provider: "test platform", + }; + edgezero_core::PlatformMetadata::new( + edgezero_core::PlatformFact::known(ceiling, source), + edgezero_core::PlatformFact::known( + edgezero_core::InboundRequestPopulationBound::new(population), + source, + ), + edgezero_core::PlatformFact::known( + edgezero_core::HostIngressMemoryAccounting::OutsideCeiling, + source, + ), + ) + } + + #[test] + fn platform_memory_validation_accounts_for_both_stack_shapes() { + let primary_without_separate_stack = super::EXPECTED_FIXED_MEMORY_BYTES + + super::EXPECTED_SEPARATE_STACK_BYTES + + super::EXPECTED_PER_LIVE_REQUEST_BYTES; + let without_separate_stack = super::platform_memory_validation(known_platform( + edgezero_core::MemoryCeiling::new( + primary_without_separate_stack, + edgezero_core::MemoryCeilingScope::PerExecution, + None, + ), + NonZeroU32::MIN, + )) + .expect("valid envelope without separate stack"); + assert!(matches!( + without_separate_stack, + edgezero_core::MemoryEnvelopeValidation::Fits { + required_primary_bytes, + required_separate_stack_bytes: None, + .. + } if required_primary_bytes == primary_without_separate_stack + )); + + let primary_with_separate_stack = + super::EXPECTED_FIXED_MEMORY_BYTES + super::EXPECTED_PER_LIVE_REQUEST_BYTES; + let with_separate_stack = super::platform_memory_validation(known_platform( + edgezero_core::MemoryCeiling::new( + primary_with_separate_stack, + edgezero_core::MemoryCeilingScope::PerExecution, + Some(super::EXPECTED_SEPARATE_STACK_BYTES), + ), + NonZeroU32::MIN, + )) + .expect("valid envelope with separate stack"); + assert!(matches!( + with_separate_stack, + edgezero_core::MemoryEnvelopeValidation::Fits { + required_primary_bytes, + required_separate_stack_bytes: Some(required_stack_bytes), + .. + } if required_primary_bytes == primary_with_separate_stack + && required_stack_bytes == super::EXPECTED_SEPARATE_STACK_BYTES + )); + } + + #[test] + fn platform_memory_startup_accepts_fit() { + let required_primary = + super::EXPECTED_FIXED_MEMORY_BYTES + super::EXPECTED_PER_LIVE_REQUEST_BYTES; + let platform = known_platform( + edgezero_core::MemoryCeiling::new( + required_primary, + edgezero_core::MemoryCeilingScope::PerExecution, + Some(super::EXPECTED_SEPARATE_STACK_BYTES), + ), + NonZeroU32::MIN, + ); + + assert_eq!(super::platform_memory_startup(platform), Ok(())); + } + + #[test] + fn platform_memory_startup_preserves_primary_excess() { + let platform = known_platform( + edgezero_core::MemoryCeiling::new( + 1, + edgezero_core::MemoryCeilingScope::PerExecution, + None, + ), + NonZeroU32::MIN, + ); + + assert!(matches!( + super::platform_memory_startup(platform), + Err(super::PlatformMemoryStartupError::Exceeds { + available_bytes: 1, + required_bytes, + resource: edgezero_core::MemoryResource::Primary, + }) if required_bytes == super::EXPECTED_FIXED_MEMORY_BYTES + + super::EXPECTED_SEPARATE_STACK_BYTES + + super::EXPECTED_PER_LIVE_REQUEST_BYTES + )); + } + + #[test] + fn platform_memory_startup_preserves_stack_excess() { + let platform = known_platform( + edgezero_core::MemoryCeiling::new( + super::EXPECTED_FIXED_MEMORY_BYTES + super::EXPECTED_PER_LIVE_REQUEST_BYTES, + edgezero_core::MemoryCeilingScope::PerExecution, + Some(1), + ), + NonZeroU32::MIN, + ); + + assert_eq!( + super::platform_memory_startup(platform), + Err(super::PlatformMemoryStartupError::Exceeds { + available_bytes: 1, + required_bytes: super::EXPECTED_SEPARATE_STACK_BYTES, + resource: edgezero_core::MemoryResource::SeparateStack, + }) + ); + } + + #[test] + fn platform_memory_startup_keeps_indeterminate_distinct_from_fit() { + assert_eq!( + super::platform_memory_startup(edgezero_core::PlatformMetadata::default()), + Ok(()) + ); + } + + #[test] + fn platform_memory_startup_preserves_validation_error() { + let platform = known_platform( + edgezero_core::MemoryCeiling::new( + u64::MAX, + edgezero_core::MemoryCeilingScope::PerExecution, + Some(super::EXPECTED_SEPARATE_STACK_BYTES), + ), + NonZeroU32::new(2).expect("non-zero population"), + ); + + assert!(matches!( + super::platform_memory_startup(platform), + Err(super::PlatformMemoryStartupError::Validation { + source: + edgezero_core::MemoryEnvelopeValidationError::NonUnitPerExecutionPopulation { + max_live_requests, + }, + }) if max_live_requests.get() == 2 + )); + } + + #[test] + fn configured_synthesized_headers_cover_detached_errors() { + let app = EdgeZeroApp::build::(edgezero_core::PlatformMetadata::default()) + .expect("configured app"); + let outcome = app.detached_ingress_error_egress( + EdgeError::uri_too_long("private target"), + Method::HEAD, + app.monotonic_now(), + ); + let IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("detached response"); + }; + let response = complete_envelope(*envelope); + assert_eq!(response.status(), StatusCode::URI_TOO_LONG); + assert_eq!( + response.headers()["x-application-version"], + env!("CARGO_PKG_VERSION") + ); + assert_eq!( + response.body().as_bytes().expect("suppressed body").len(), + 0 + ); + } + + #[test] + fn configured_admission_uses_finite_class_aware_deadlines() { + let app = EdgeZeroApp::build::(edgezero_core::PlatformMetadata::default()) + .expect("configured app"); + let start = MonotonicInstant::now(); + + let outbound = begin_ingress(&app, "/proxy/status/200", start); + assert_eq!( + outbound.read_deadline().instant(), + start + .checked_add(Duration::from_secs(10)) + .expect("deadline") + ); + + let health = begin_ingress(&app, "/", start); + assert_eq!( + health.read_deadline().instant(), + start + .checked_add(Duration::from_secs(30)) + .expect("deadline") + ); + + let fallback = begin_ingress(&app, "/missing", start); + assert_eq!( + fallback.read_deadline().instant(), + start.checked_add(Duration::from_secs(5)).expect("deadline") + ); + } + + #[test] + fn admission_handler_consumes_the_typed_grant_once() { + let app = EdgeZeroApp::build::(edgezero_core::PlatformMetadata::default()) + .expect("configured app"); + let start = MonotonicInstant::now(); + let prepared = begin_ingress(&app, "/admission", start); + let request = request_builder() + .method(Method::GET) + .uri("/admission") + .body(Body::empty()) + .expect("request"); + + let response = complete_envelope(block_on(app.dispatch_admitted(prepared, request))); + let payload: serde_json::Value = response.body().to_json().expect("json"); + assert_eq!(payload["route_class"], "diagnostic"); + assert_eq!(payload["grant_consumed_once"], true); + } + + #[test] + fn late_bound_permit_lives_until_terminal_egress() { + let releases = Arc::new(AtomicUsize::new(0)); + let policy_releases = Arc::clone(&releases); + let mut app = EdgeZeroApp::build::(edgezero_core::PlatformMetadata::default()) + .expect("configured app"); + app.set_ingress_admission_policy(move |head| { + let RouteResolution::Matched(metadata) = head.route_resolution().clone() else { + panic!("admission route must resolve"); + }; + let route_class = metadata.class().map(str::to_owned); + let permit = super::ResponsePermit::new(route_class.clone()); + let (mut lease, completion) = super::response_lifecycle(route_class, permit); + lease.observe_response_permit_releases(Arc::clone(&policy_releases)); + AdmissionDecision::Admit { + completion, + grant: IngressGrant::new(lease), + read_deadline: head.read_deadline_after(Duration::from_secs(30)), + } + }); + + let prepared = begin_ingress(&app, "/admission", MonotonicInstant::now()); + let request = request_builder() + .method(Method::GET) + .uri("/admission") + .body(Body::empty()) + .expect("request"); + let envelope = block_on(app.dispatch_admitted(prepared, request)); + + assert_eq!(releases.load(Ordering::SeqCst), 0); + let response = complete_envelope(envelope); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(releases.load(Ordering::SeqCst), 1); + } + + #[test] + fn composed_response_completion() { + let static_effects = Arc::new(AtomicUsize::new(0)); + let late_bound_effects = Arc::new(AtomicUsize::new(0)); + let static_effects_for_policy = Arc::clone(&static_effects); + let late_bound_effects_for_policy = Arc::clone(&late_bound_effects); + let mut app = EdgeZeroApp::build::(edgezero_core::PlatformMetadata::default()) + .expect("configured app"); + app.set_ingress_admission_policy(move |head| { + let static_permit = super::ResponsePermit { + release_counter: Some(Arc::clone(&static_effects_for_policy)), + route_class: None, + }; + let (mut lease, completion) = super::response_lifecycle(None, static_permit); + lease.observe_response_permit_releases(Arc::clone(&late_bound_effects_for_policy)); + lease + .install_response_permit() + .expect("install late-bound response permit"); + AdmissionDecision::Admit { + completion, + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(30)), + } + }); + + let prepared_ingress = begin_ingress(&app, "/", MonotonicInstant::now()); + let request = request_builder() + .method(Method::GET) + .uri("/") + .body(Body::empty()) + .expect("request"); + let envelope = block_on(app.dispatch_admitted(prepared_ingress, request)); + + assert_eq!(static_effects.load(Ordering::SeqCst), 0); + assert_eq!(late_bound_effects.load(Ordering::SeqCst), 0); + let Ok((prepared_response, _, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("response egress begin"); + }; + assert_eq!(static_effects.load(Ordering::SeqCst), 0); + assert_eq!(late_bound_effects.load(Ordering::SeqCst), 0); + + assert!(attempt.begin_writing()); + assert_eq!(static_effects.load(Ordering::SeqCst), 0); + assert_eq!(late_bound_effects.load(Ordering::SeqCst), 0); + + let observed_at = clock.now(); + assert!(attempt.complete(observed_at)); + assert_eq!(static_effects.load(Ordering::SeqCst), 1); + assert_eq!(late_bound_effects.load(Ordering::SeqCst), 1); + + assert!(!attempt.complete(observed_at)); + assert_eq!(static_effects.load(Ordering::SeqCst), 1); + assert_eq!(late_bound_effects.load(Ordering::SeqCst), 1); + drop(attempt); + + assert_eq!(static_effects.load(Ordering::SeqCst), 1); + assert_eq!(late_bound_effects.load(Ordering::SeqCst), 1); + assert_eq!(prepared_response.into_response().status(), StatusCode::OK); + } + + #[test] + fn configured_admission_preserves_fallback_status_at_exact_cap() { + let app = EdgeZeroApp::build::(edgezero_core::PlatformMetadata::default()) + .expect("configured app"); + for (method, path, expected) in [ + (Method::POST, "/missing", StatusCode::NOT_FOUND), + (Method::POST, "/", StatusCode::METHOD_NOT_ALLOWED), + ] { + let request = request_builder() + .method(method) + .uri(path) + .body(Body::stream(iter([Bytes::from(vec![b'a'; 4_096])]))) + .expect("request"); + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + edgezero_core::IngressHeadAccounting::HostManaged, + edgezero_core::IngressFraming::HostManaged, + )) + .expect("dispatch"), + ); + assert_eq!(response.status(), expected); + } + } + + #[test] + fn configured_admission_returns_exact_overflow_response_for_both_fallbacks() { + let app = EdgeZeroApp::build::(edgezero_core::PlatformMetadata::default()) + .expect("configured app"); + for (method, path) in [(Method::POST, "/missing"), (Method::POST, "/")] { + let request = request_builder() + .method(method) + .uri(path) + .body(Body::stream(iter([ + Bytes::from(vec![b'a'; 4_096]), + Bytes::from_static(b"b"), + ]))) + .expect("request"); + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + edgezero_core::IngressHeadAccounting::HostManaged, + edgezero_core::IngressFraming::HostManaged, + )) + .expect("dispatch"), + ); + + assert_plain_text_response( + &response, + StatusCode::PAYLOAD_TOO_LARGE, + b"request body too large\n", + ); + } + } + + #[test] + fn configured_admission_returns_exact_timeout_response() { + let app = EdgeZeroApp::build::(edgezero_core::PlatformMetadata::default()) + .expect("configured app"); + let request = request_builder() + .method(Method::POST) + .uri("/missing") + .body(Body::from_stream(iter([Err::( + EdgeError::request_timeout("adapter read deadline exceeded"), + )]))) + .expect("request"); + let response = complete_dispatch( + block_on(app.dispatch_ingress( + request, + MonotonicInstant::now(), + edgezero_core::IngressHeadAccounting::HostManaged, + edgezero_core::IngressFraming::HostManaged, + )) + .expect("dispatch"), + ); + + assert_plain_text_response(&response, StatusCode::REQUEST_TIMEOUT, b"request timeout\n"); + } + + fn assert_plain_text_response(response: &Response, status: StatusCode, body: &[u8]) { + let expected_length = body.len().to_string(); + assert_eq!(response.status(), status); + assert_eq!( + response + .headers() + .get("content-type") + .expect("content type"), + "text/plain; charset=utf-8" + ); + assert_eq!( + response + .headers() + .get("content-length") + .expect("content length") + .to_str() + .expect("ASCII content length"), + expected_length + ); + assert_eq!(response.body().as_bytes().expect("buffered body"), body); + } + + fn complete_dispatch(outcome: IngressDispatchOutcome) -> Response { + let IngressDispatchOutcome::Response(envelope) = outcome else { + panic!("expected response"); + }; + complete_envelope(*envelope) + } + + fn complete_envelope(envelope: ResponseEgressEnvelope) -> Response { + let Ok((prepared, _, mut attempt, clock)) = + envelope.begin(ResponseEgressFramingMode::ExplicitBufferedLength) + else { + panic!("response egress begin"); + }; + assert!(attempt.begin_writing()); + assert!(attempt.complete(clock.now())); + prepared.into_response() + } + + fn begin_ingress( + app: &EdgeZeroApp, + path: &str, + start: MonotonicInstant, + ) -> edgezero_core::PreparedIngress { + let head = IngressHeadParts::new( + Method::GET, + path.parse().expect("URI"), + Version::HTTP_11, + HeaderMap::new(), + ); + match app.begin_ingress(head, start).expect("begin ingress") { + IngressBeginOutcome::Admitted(prepared) => prepared, + IngressBeginOutcome::Refused(_) => panic!("demo policy must admit request"), + IngressBeginOutcome::Aborted | _ => panic!("unknown admission outcome"), + } + } +} diff --git a/examples/app-demo/edgezero.toml b/examples/app-demo/edgezero.toml index 123e59030..c06df5d03 100644 --- a/examples/app-demo/edgezero.toml +++ b/examples/app-demo/edgezero.toml @@ -7,12 +7,19 @@ version = "0.1.0" kind = "http" entry = "crates/app-demo-core" +[capabilities] +optional = ["outbound-http", "outbound-image-optimizer"] + +[capabilities.outbound] +hosts = ["https://*:*"] + [[triggers.http]] id = "root" path = "/" methods = ["GET"] handler = "app_demo_core::handlers::root" adapters = ["axum", "cloudflare", "fastly", "spin"] +class = "health" description = "Default health-check endpoint" [[triggers.http]] @@ -52,6 +59,14 @@ methods = ["POST"] handler = "app_demo_core::handlers::echo_json" adapters = ["axum", "cloudflare", "fastly", "spin"] +[[triggers.http]] +id = "admission" +path = "/admission" +methods = ["GET"] +handler = "app_demo_core::handlers::admission" +adapters = ["axum", "cloudflare", "fastly", "spin"] +class = "diagnostic" +description = "Confirms route-aware admission and one-time grant consumption" [[triggers.http]] id = "proxy_demo" @@ -59,6 +74,34 @@ path = "/proxy/{*rest}" methods = ["GET", "POST"] handler = "app_demo_core::handlers::proxy_demo" adapters = ["axum", "cloudflare", "fastly", "spin"] +class = "outbound" + +[[triggers.http]] +id = "follow_demo" +path = "/follow" +methods = ["GET"] +handler = "app_demo_core::handlers::follow_demo" +adapters = ["axum", "cloudflare", "fastly", "spin"] +class = "outbound" +description = "Follows at most four redirects within the configured API origin" + +[[triggers.http]] +id = "fanout" +path = "/fanout" +methods = ["POST"] +handler = "app_demo_core::handlers::fanout" +adapters = ["axum", "cloudflare", "fastly", "spin"] +class = "outbound" +description = "Concurrent positional outbound batch with per-slot elapsed time" + +[[triggers.http]] +id = "image-demo" +path = "/image-demo" +methods = ["GET"] +handler = "app_demo_core::handlers::image_demo" +adapters = ["fastly"] +class = "outbound" +description = "Native image optimization; requires Fastly entitlement" [[triggers.http]] id = "config_get" @@ -117,6 +160,7 @@ path = "/_app-demo/manifest" methods = ["GET"] handler = "edgezero_core::introspection::manifest" adapters = ["axum", "cloudflare", "fastly", "spin"] +class = "diagnostic" description = "App manifest as JSON" [[triggers.http]] @@ -125,6 +169,7 @@ path = "/_app-demo/config" methods = ["GET"] handler = "edgezero_core::introspection::config" adapters = ["axum", "cloudflare", "fastly", "spin"] +class = "diagnostic" description = "Effective app config (secret-safe)" [[triggers.http]] @@ -133,6 +178,7 @@ path = "/_app-demo/routes" methods = ["GET"] handler = "edgezero_core::introspection::routes" adapters = ["axum", "cloudflare", "fastly", "spin"] +class = "diagnostic" description = "Registered route table" # -- Secrets demo route -------------------------------------------------------- @@ -163,14 +209,34 @@ default = "app_config" ids = ["default"] default = "default" -# [environment] -# -# [[environment.variables]] -# name = "API_BASE_URL" -# description = "Base URL for upstream API calls" -# env = "API_BASE_URL" -# value = "https://example.com/api" -# +[environment] + +[[environment.variables]] +name = "API_BASE_URL" +description = "Base URL for upstream API calls" +env = "API_BASE_URL" + +# Native pre-admission HTTP/1 resource limits; parent environment values win. +[[environment.variables]] +name = "EDGEZERO__ADAPTER__INGRESS__MAX_CONNECTIONS" +value = "256" +adapters = ["axum"] + +[[environment.variables]] +name = "EDGEZERO__ADAPTER__INGRESS__MAX_RAW_HEAD_BYTES" +value = "65536" +adapters = ["axum"] + +[[environment.variables]] +name = "EDGEZERO__ADAPTER__INGRESS__MAX_HEADER_COUNT" +value = "100" +adapters = ["axum"] + +[[environment.variables]] +name = "EDGEZERO__ADAPTER__INGRESS__HEAD_READ_TIMEOUT_MS" +value = "10000" +adapters = ["axum"] + # [[environment.secrets]] # name = "API_TOKEN" # description = "Secret token used when calling the upstream API" diff --git a/patches/viceroy/0.21.0-body-readiness.patch b/patches/viceroy/0.21.0-body-readiness.patch new file mode 100644 index 000000000..ff16e5227 --- /dev/null +++ b/patches/viceroy/0.21.0-body-readiness.patch @@ -0,0 +1,284 @@ +diff --git a/src/body.rs b/src/body.rs +index 5f86628..465abab 100644 +--- a/src/body.rs ++++ b/src/body.rs +@@ -42,6 +42,8 @@ pub enum Chunk { + Channel(mpsc::Receiver), + /// A version of `HttpBody` that assumes that the interior data is gzip-compressed. + CompressedHttpBody(DecoderState, hyper::Body), ++ /// An owned source error observed by readiness but not yet consumed by a reader. ++ Error(error::Error), + } + + impl Chunk { +@@ -180,13 +182,21 @@ impl Body { + Ok(String::from_utf8(self.read_into_vec().await?).expect("Body was not UTF-8")) + } + +- /// Block until the body has a chunk ready (or is known to be empty). ++ /// Block until a nonempty chunk, an error, or end-of-stream is ready to read. + pub async fn await_ready(&mut self) { +- // Attempt to read a chunk, blocking until one is available (or `None` signals end of stream) +- if let Some(Ok(chunk)) = self.data().await { +- // If we did get a chunk, put it back; subsequent read attempts will find this chunk without +- // additional blocking. +- self.chunks.push_front(chunk.into()) ++ loop { ++ match self.data().await { ++ Some(Ok(chunk)) if chunk.is_empty() => tokio::task::yield_now().await, ++ Some(Ok(chunk)) => { ++ self.chunks.push_front(chunk.into()); ++ return; ++ } ++ Some(Err(error)) => { ++ self.chunks.push_front(Chunk::Error(error)); ++ return; ++ } ++ None => return, ++ } + } + } + +@@ -271,6 +281,7 @@ impl HttpBody for Body { + ) -> Poll>> { + while let Some(mut chunk) = self.chunks.pop_front() { + match chunk { ++ Chunk::Error(error) => return Poll::Ready(Some(Err(error))), + Chunk::HttpBody(mut body) => { + let body_mut = &mut body; + pin_mut!(body_mut); +@@ -374,9 +385,10 @@ impl HttpBody for Body { + // put the body back, so we can poll it again next time + self.chunks.push_front(chunk); + if resulting_bytes.is_empty() { +- // If we got no bytes from this chunk, it might be just the gzip header +- // we'll continue the loop to process more chunks +- continue; ++ // Readiness must yield so selection deadlines and siblings ++ // progress even when the source only produces empty frames. ++ cx.waker().wake_by_ref(); ++ return self.check_timeout_on_pending(cx); + } else { + return Poll::Ready(Some(Ok(resulting_bytes))); + } +@@ -416,6 +428,7 @@ impl HttpBody for Body { + // If this is a streaming body or a compressed chunk, immediately give up on the hint. + Chunk::Channel(_) => return SizeHint::default(), + Chunk::CompressedHttpBody(_, _) => return SizeHint::default(), ++ Chunk::Error(_) => return SizeHint::default(), + Chunk::HttpBody(body) => { + // An `HttpBody` size hint will either be exact, or wide open. If the latter, + // bail out with a wide-open range. +@@ -442,6 +455,212 @@ mod tests { + + use crate::body::Chunk; + ++ fn readiness_source( ++ chunks: Vec>, ++ pending: bool, ++ ) -> ( ++ crate::body::Body, ++ std::sync::Arc, ++ ) { ++ let mut chunks = std::collections::VecDeque::from(chunks); ++ let polls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); ++ let observed = polls.clone(); ++ let stream = futures::stream::poll_fn(move |_| { ++ observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed); ++ match chunks.pop_front() { ++ Some(chunk) => std::task::Poll::Ready(Some(chunk)), ++ None if pending => std::task::Poll::Pending, ++ None => std::task::Poll::Ready(None), ++ } ++ }); ++ (hyper::Body::wrap_stream(stream).into(), polls) ++ } ++ ++ #[tokio::test] ++ async fn readiness_preserves_bytes_without_polling_the_source_again() { ++ use futures::FutureExt; ++ use std::sync::atomic::Ordering; ++ let (mut body, polls) = readiness_source(vec![Ok(Bytes::from_static(b"abc"))], true); ++ body.await_ready().await; ++ let ready_polls = polls.load(Ordering::Relaxed); ++ let mut one = [0; 1]; ++ assert!(matches!(body.read(&mut one).now_or_never(), Some(Ok(1)))); ++ assert_eq!(&one, b"a"); ++ body.await_ready().await; ++ let mut remaining = [0; 16 * 1024]; ++ assert!(matches!( ++ body.read(&mut remaining).now_or_never(), ++ Some(Ok(2)) ++ )); ++ assert_eq!(&remaining[..2], b"bc"); ++ assert_eq!(polls.load(Ordering::Relaxed), ready_polls); ++ assert!(body.await_ready().now_or_never().is_none()); ++ } ++ ++ #[tokio::test] ++ async fn readiness_empty_frame_does_not_authorize_a_pending_read() { ++ use futures::FutureExt; ++ let (mut body, _) = readiness_source(vec![Ok(Bytes::new())], true); ++ assert!(body.await_ready().now_or_never().is_none()); ++ } ++ ++ #[tokio::test] ++ async fn readiness_skips_empty_frames_before_data_and_eof() { ++ use futures::FutureExt; ++ use std::sync::atomic::Ordering; ++ let (mut body, polls) = readiness_source( ++ vec![ ++ Ok(Bytes::new()), ++ Ok(Bytes::from_static(b"x")), ++ Ok(Bytes::new()), ++ ], ++ false, ++ ); ++ body.await_ready().await; ++ let mut byte = [0; 1]; ++ let ready_polls = polls.load(Ordering::Relaxed); ++ assert!(matches!(body.read(&mut byte).now_or_never(), Some(Ok(1)))); ++ assert_eq!(polls.load(Ordering::Relaxed), ready_polls); ++ body.await_ready().await; ++ let eof_polls = polls.load(Ordering::Relaxed); ++ assert!(matches!(body.read(&mut byte).now_or_never(), Some(Ok(0)))); ++ assert_eq!(polls.load(Ordering::Relaxed), eof_polls); ++ } ++ ++ #[tokio::test] ++ async fn readiness_repeated_observation_retains_the_original_error() { ++ use futures::FutureExt; ++ use http_body::Body; ++ use std::sync::atomic::Ordering; ++ let identity = std::sync::Arc::new(std::io::Error::other("readiness sentinel")); ++ let (mut body, polls) = readiness_source( ++ vec![ ++ Ok(Bytes::new()), ++ Err(std::io::Error::other(identity.clone())), ++ ], ++ false, ++ ); ++ body.await_ready().await; ++ let ready_polls = polls.load(Ordering::Relaxed); ++ body.await_ready().await; ++ body.await_ready().await; ++ let Some(Some(Err(crate::Error::HyperError(error)))) = body.data().now_or_never() else { ++ panic!("readiness lost the typed source error"); ++ }; ++ assert!( ++ std::error::Error::source(&error) ++ .and_then(|error| error.downcast_ref::()) ++ .and_then(|error| error.get_ref()) ++ .and_then(|error| error.downcast_ref::>()) ++ .is_some_and(|error| std::sync::Arc::ptr_eq(error, &identity)) ++ ); ++ assert_eq!(polls.load(Ordering::Relaxed), ready_polls); ++ assert!(body.data().await.is_none()); ++ } ++ ++ #[tokio::test] ++ async fn readiness_retained_error_survives_append() { ++ use futures::FutureExt; ++ let (sender, receiver) = tokio::sync::mpsc::channel(1); ++ drop(sender); ++ let mut source: crate::body::Body = receiver.into(); ++ source.await_ready().await; ++ let mut body = crate::body::Body::empty(); ++ body.append(source); ++ assert_eq!(body.len(), None); ++ let mut byte = [0; 1]; ++ assert!(matches!( ++ body.read(&mut byte).now_or_never(), ++ Some(Err(crate::Error::UnfinishedStreamingBody)) ++ )); ++ assert!(matches!(body.read(&mut byte).now_or_never(), Some(Ok(0)))); ++ } ++ ++ #[tokio::test] ++ async fn readiness_cancellation_keeps_a_later_source_error() { ++ use futures::FutureExt; ++ let (sender, receiver) = futures::channel::mpsc::unbounded(); ++ let mut body: crate::body::Body = hyper::Body::wrap_stream(receiver).into(); ++ assert!(body.await_ready().now_or_never().is_none()); ++ sender ++ .unbounded_send(Err::(std::io::Error::other("later error"))) ++ .unwrap(); ++ body.await_ready().await; ++ let mut byte = [0; 1]; ++ assert!(matches!( ++ body.read(&mut byte).now_or_never(), ++ Some(Err(crate::Error::HyperError(_))) ++ )); ++ } ++ ++ #[tokio::test] ++ async fn readiness_empty_source_does_not_hide_a_ready_sibling() { ++ use futures::{ ++ FutureExt, ++ future::{Either, select}, ++ }; ++ let (mut pending, _) = readiness_source(vec![Ok(Bytes::new())], true); ++ let (mut ready, _) = readiness_source(vec![Ok(Bytes::from_static(b"x"))], true); ++ let selected = select(pending.await_ready().boxed(), ready.await_ready().boxed()).await; ++ assert!(matches!(selected, Either::Right(_))); ++ } ++ ++ #[tokio::test] ++ async fn readiness_decoder_no_output_yields_to_the_selection_timer() { ++ use futures::FutureExt; ++ use std::task::Poll; ++ let mut polls = 0; ++ let empty = futures::stream::poll_fn(move |_| { ++ polls += 1; ++ assert!( ++ polls < 1024, ++ "empty compressed frames monopolized a readiness poll" ++ ); ++ Poll::Ready(Some(Ok::<_, std::io::Error>(Bytes::new()))) ++ }); ++ let mut body: crate::body::Body = ++ Chunk::compressed_body(hyper::Body::wrap_stream(empty)).into(); ++ assert!(body.await_ready().now_or_never().is_none()); ++ drop(body); ++ let empty = futures::stream::repeat_with(|| Ok::<_, std::io::Error>(Bytes::new())); ++ let mut body: crate::body::Body = ++ Chunk::compressed_body(hyper::Body::wrap_stream(empty)).into(); ++ assert!( ++ tokio::time::timeout(std::time::Duration::from_millis(5), body.await_ready()) ++ .await ++ .is_err() ++ ); ++ } ++ ++ #[tokio::test] ++ async fn readiness_preserves_decoder_error() { ++ use futures::FutureExt; ++ let mut body: crate::body::Body = ++ Chunk::compressed_body(hyper::Body::from("invalid gzip")).into(); ++ body.await_ready().await; ++ let mut byte = [0; 1]; ++ assert!(matches!( ++ body.read(&mut byte).now_or_never(), ++ Some(Err(crate::Error::IoError(_))) ++ )); ++ } ++ ++ #[tokio::test] ++ async fn readiness_preserves_between_bytes_timeout_without_repolling_source() { ++ use futures::FutureExt; ++ use std::sync::atomic::Ordering; ++ let (mut body, polls) = readiness_source(Vec::new(), true); ++ body.set_between_bytes_timeout(std::time::Duration::from_millis(1)); ++ body.await_ready().await; ++ let ready_polls = polls.load(Ordering::Relaxed); ++ let mut byte = [0; 1]; ++ assert!(matches!( ++ body.read(&mut byte).now_or_never(), ++ Some(Err(crate::Error::BetweenBytesTimeout)) ++ )); ++ assert_eq!(polls.load(Ordering::Relaxed), ready_polls); ++ } ++ + /// Proptest strategy: get a set of Bytes. + fn some_bytes() -> impl Strategy { + proptest::collection::vec(any::(), 0..16).prop_map(|v| v.into()) diff --git a/patches/viceroy/README.md b/patches/viceroy/README.md new file mode 100644 index 000000000..e56bb5512 --- /dev/null +++ b/patches/viceroy/README.md @@ -0,0 +1,107 @@ +# Viceroy 0.21.0 Body Readiness Repair + +This patch is part of PR #275. It repairs the local runtime, not the Fastly SDK or hosted +service. The repository's SDK/runtime version pins are unchanged. Do not replace an installed +Viceroy silently or describe a patched executable as unmodified 0.21.0 evidence. + +Base: official Fastly Viceroy commit `a69d982d1d95e13c0fc9f92683b9118639bb4b04`, tag `v0.21.0`. +The upstream Cargo.lock SHA-256 is +`d642d177081f42aec75d25c0d1124c013d3abcb081886251420a90e9c53c1a2a`. +Upstream source is licensed under Apache-2.0 WITH LLVM-exception; its license files remain +authoritative. The patch changes only `src/body.rs` and includes co-located regression tests. + +## Repair + +Unmodified `Body::await_ready` consumes errors and reports empty frames as ready even though +`Body::read` skips empty frames and may then wait. The repair queues the original owned error, +skips empty frames cooperatively, and yields when gzip produces no output. Queuing rather than +adding an out-of-band error field preserves ownership through append/iteration. Size hints +remain unknown while an error is queued. No error is stringified, replaced with EOF or cloned. + +Tests assert the first read poll is ready without another source poll after readiness. They +cover one-byte/16 KiB buffers and retained remainder, fresh readiness after exhaustion, empty +frames before data/pending/EOF, repeated error observation with original error identity, +append, cancellation, ready siblings, decoder errors/no-output and between-byte timeout. +The bounded-poll assertion rejects an infinite empty compressed source before its timer test +can run; a broken implementation cannot hang that regression indefinitely. + +## Apply and Build + +Run from the EdgeZero checkout with Rust 1.98.1. Choose a new temporary source directory; +the commands never modify the installed executable or the EdgeZero dependency graph. Fetching +the official source/dependencies needs network access; builds/tests afterward are offline/locked. + +```sh +set -eu +EDGEZERO_ROOT=$(git rev-parse --show-toplevel) +: "${VICEROY_SOURCE:?choose a new absolute temporary directory}" +test ! -e "$VICEROY_SOURCE" +git clone --depth 1 --branch v0.21.0 https://github.com/fastly/Viceroy.git "$VICEROY_SOURCE" +base=$(git -C "$VICEROY_SOURCE" rev-parse --verify HEAD) +test "$base" = a69d982d1d95e13c0fc9f92683b9118639bb4b04 +patch="$EDGEZERO_ROOT/patches/viceroy/0.21.0-body-readiness.patch" +git -C "$VICEROY_SOURCE" apply --check "$patch" +git -C "$VICEROY_SOURCE" apply "$patch" +export RUSTUP_TOOLCHAIN=1.98.1 +export ASDF_RUST_VERSION=1.98.1 +export CARGO_TARGET_DIR="$VICEROY_SOURCE/target" +export CARGO_PROFILE_DEV_DEBUG=0 +export CARGO_INCREMENTAL=0 +cargo fetch --locked --manifest-path "$VICEROY_SOURCE/Cargo.toml" +cargo test --offline --locked --manifest-path "$VICEROY_SOURCE/Cargo.toml" -p viceroy-lib --lib body::tests::readiness_ +cargo test --offline --locked --manifest-path "$VICEROY_SOURCE/Cargo.toml" -p viceroy-lib --lib body::tests:: +cargo build --offline --locked --manifest-path "$VICEROY_SOURCE/Cargo.toml" -p viceroy +``` + +Require ten passing new readiness tests and 24 passing body/collector tests, with zero failures. +The second filter also matches `collecting_body::tests`; these counts are not hosted cases. +The development profile has no debug symbols/incremental state; record actual compiler, +profile, source, lockfile, patch and binary hashes rather than inferring identity from version text. + +## Guest-Level Control + +Run the opt-in full SDK readiness/timing diagnostic with an explicit runtime override and a new +empty evidence directory: + +```sh +unset CARGO_TARGET_DIR +VICEROY_BIN="$VICEROY_SOURCE/target/debug/viceroy" scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-readiness --output NEW_EVIDENCE_DIRECTORY +``` + +The fixture must still use its own target directory, as the existing launcher enforces. The +expanded suite requires 135 probes and 147 origin receipts, including three direct/selected +error comparisons. Unmodified 0.21.0 remains a failed control; the repaired executable must +preserve `UnexpectedEof` in both modes. Any HTTP-readiness primitive or exchange-timing failure +exits 1. Timer-only controls are separate observations: lateness remains recorded, not waived. +Even a passing +matrix exits 2: production-path integration, exact-version review, backend policy and hosted +evidence are still required. Do not turn either disposition into an overall gate pass. + +The current-operation multiplexer checkpoint extends this same suite with eighteen raw mixed/ +full-buffer probes and twelve actual Send/Batch client probes. The 135-probe baseline stays +separately validated. Actual-client probes require exact payloads, typed source errors and +request-correlated origin receipts; preflight failures do not count as exercised receive paths. +CI selects the separate `outbound-client-progress` suite and requires exactly exit 2, exactly one +passing functional inventory with twelve probes, twelve expected probes and eighteen origin +requests, `qualification: false`, an explicit unverified/unqualified disposition and no failure +records. This functional gate does not run the full SDK timing matrix. The opt-in +`outbound-readiness` diagnostic above retains that complete matrix, its strict timing validators +and exit-1 failures unchanged; timing misses are not waived or relabeled as passes. + +CI keeps its stock Viceroy installation, clones the exact upstream revision, checks lockfile +and patch hashes, and builds/tests the repair in a separate cached target directory. Fastly +runtime tests and the functional CI suite explicitly select that absolute executable through +`CARGO_TARGET_WASM32_WASIP1_RUNNER` and `VICEROY_BIN`. Neither PATH nor the installed runtime pin +is replaced. Callback-wide scheduling, hosted runtime promotion and resource qualification remain +open; these local checks cannot establish hosted interruption or cleanup bounds. + +Keep the historical failed report and raw evidence unchanged. A separate repair record must +include exact upstream/patch/host-binary/guest-binary identities, commands, counts and outcomes. +Neither unit tests nor this local control approve backend bound B, optimizer horizon H(K), +production dispatch changes, capability promotion, candidate freeze or hosted qualification. + +The [local repair record](../../docs/audits/evidence/2026-10-23-viceroy-readiness-repair.json) +retains both stock and repaired probe events with distinct host executable hashes. All 135 +Viceroy library tests pass. Strict upstream clippy under Rust 1.98.1 reports the same diagnostics +on the untouched base and repaired source (7 library / 25 test errors); this patch does not +suppress or refactor those unrelated upstream warnings. EdgeZero's own strict clippy is separate. diff --git a/scripts/brotli_dependency_contract.json b/scripts/brotli_dependency_contract.json new file mode 100644 index 000000000..bbb4e6b30 --- /dev/null +++ b/scripts/brotli_dependency_contract.json @@ -0,0 +1,37 @@ +[ + [ + "alloc-no-stdlib", + "2.0.4", + "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + ], + [ + "alloc-stdlib", + "0.2.4", + "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" + ], + [ + "async-compression", + "0.4.43", + "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" + ], + [ + "brotli", + "8.0.4", + "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" + ], + [ + "brotli-decompressor", + "5.0.1", + "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" + ], + [ + "compression-codecs", + "0.4.38", + "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" + ], + [ + "compression-core", + "0.4.32", + "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + ] +] diff --git a/scripts/check_adapter_feature_matrix.sh b/scripts/check_adapter_feature_matrix.sh new file mode 100644 index 000000000..87d78a84f --- /dev/null +++ b/scripts/check_adapter_feature_matrix.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +adapter="$1" +target="$2" + +case "${adapter}:${target}" in + axum:native | cloudflare:native | fastly:native | spin:native) ;; + cloudflare:wasm32-unknown-unknown | fastly:wasm32-wasip1 | spin:wasm32-wasip2) ;; + *) + echo "unsupported adapter/target pair: ${adapter}:${target}" >&2 + exit 2 + ;; +esac + +feature_sets=( + "" + "${adapter}" + "cli" + "test-utils" + "${adapter},cli" + "${adapter},test-utils" + "cli,test-utils" + "${adapter},cli,test-utils" +) + +for features in "${feature_sets[@]}"; do + args=( + cargo check + --offline + --locked + --package "edgezero-adapter-${adapter}" + --no-default-features + --all-targets + ) + if [[ "${target}" != "native" ]]; then + args+=(--target "${target}") + fi + label="none" + if [[ -n "${features}" ]]; then + args+=(--features "${features}") + label="${features}" + fi + + echo "==> edgezero-adapter-${adapter} ${target} features=[${label}]" + "${args[@]}" +done diff --git a/scripts/check_brotli_dependency_contract.mjs b/scripts/check_brotli_dependency_contract.mjs new file mode 100644 index 000000000..be23f781e --- /dev/null +++ b/scripts/check_brotli_dependency_contract.mjs @@ -0,0 +1,140 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process' +import { readFileSync } from 'node:fs' + +const expected = JSON.parse( + readFileSync('scripts/brotli_dependency_contract.json', 'utf8'), +) + +const registrySource = 'registry+https://github.com/rust-lang/crates.io-index' + +function fail(workspace, message) { + process.stderr.write(`brotli dependency contract (${workspace}): ${message}\n`) + process.exitCode = 1 +} + +function metadata(workspace) { + const output = execFileSync( + 'cargo', + ['metadata', '--locked', '--offline', '--format-version', '1', '--all-features'], + { cwd: workspace, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }, + ) + return JSON.parse(output) +} + +function lockValue(packageBlock, key) { + return packageBlock.match(new RegExp(`^${key} = "([^"]+)"$`, 'mu'))?.[1] +} + +function auditLockfile(workspace) { + const lockPath = workspace === '.' ? 'Cargo.lock' : `${workspace}/Cargo.lock` + const packageBlocks = readFileSync(lockPath, 'utf8') + .split('[[package]]') + .slice(1) + + for (const [name, version, checksum] of expected) { + const matches = packageBlocks.filter( + (block) => lockValue(block, 'name') === name, + ) + if (matches.length !== 1) { + fail(workspace, `expected one locked ${name} package, found ${matches.length}`) + continue + } + const block = matches[0] + if ( + lockValue(block, 'version') !== version || + lockValue(block, 'source') !== registrySource || + lockValue(block, 'checksum') !== checksum + ) { + fail( + workspace, + `locked ${name} source, version, or checksum differs from the audited contract`, + ) + } + } +} + +function auditWorkflowFetchOrder() { + const workflow = readFileSync('.github/workflows/test.yml', 'utf8') + const lines = workflow.split('\n').map((line) => line.trim()) + const commandIndex = (command) => + lines.findIndex((line) => line === command || line === `run: ${command}`) + const rootFetch = commandIndex('cargo fetch --locked') + const demoFetch = commandIndex( + 'cargo fetch --locked --manifest-path examples/app-demo/Cargo.toml', + ) + const audit = commandIndex('node scripts/check_brotli_dependency_contract.mjs') + + if ( + rootFetch === -1 || + demoFetch === -1 || + audit === -1 || + rootFetch >= audit || + demoFetch >= audit + ) { + fail( + 'workflow', + 'root and app-demo locked fetches must run before the offline dependency audit', + ) + } +} + +function audit(workspace) { + auditLockfile(workspace) + const graph = metadata(workspace) + const packagesById = new Map(graph.packages.map((pkg) => [pkg.id, pkg])) + const nodesById = new Map(graph.resolve.nodes.map((node) => [node.id, node])) + + for (const [name, version] of expected) { + const matches = graph.packages.filter((pkg) => pkg.name === name) + if (matches.length !== 1) { + fail(workspace, `expected one ${name} package, found ${matches.length}`) + continue + } + if (matches[0].version !== version) { + fail( + workspace, + `expected ${name} ${version}, resolved ${matches[0].version}`, + ) + } + } + + const core = graph.packages.find((pkg) => pkg.name === 'edgezero-core') + if (core === undefined) { + fail(workspace, 'edgezero-core package is missing') + return + } + const coreNode = nodesById.get(core.id) + if (coreNode === undefined) { + fail(workspace, 'edgezero-core resolve node is missing') + return + } + + for (const [name, version] of expected) { + const declarations = core.dependencies.filter( + (dependency) => dependency.name === name && dependency.kind === null, + ) + if (declarations.length !== 1 || declarations[0].req !== `=${version}`) { + fail( + workspace, + `edgezero-core must declare exactly one normal ${name} dependency pinned as =${version}`, + ) + } + const direct = coreNode.dependencies + .map((id) => packagesById.get(id)) + .find((pkg) => pkg?.name === name) + if (direct === undefined) { + fail(workspace, `edgezero-core does not pin ${name} as a normal dependency`) + } + } +} + +auditWorkflowFetchOrder() +audit('.') +audit('examples/app-demo') +audit('tests/fixtures/reusable-app') + +if (process.exitCode === undefined) { + process.stdout.write('brotli dependency contract: ok\n') +} diff --git a/scripts/check_flate_dependency_contract.mjs b/scripts/check_flate_dependency_contract.mjs new file mode 100644 index 000000000..a6e188a8d --- /dev/null +++ b/scripts/check_flate_dependency_contract.mjs @@ -0,0 +1,139 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process' +import { readFileSync } from 'node:fs' + +const expected = JSON.parse( + readFileSync('scripts/flate_dependency_contract.json', 'utf8'), +) +const registrySource = 'registry+https://github.com/rust-lang/crates.io-index' + +function fail(workspace, message) { + process.stderr.write(`flate dependency contract (${workspace}): ${message}\n`) + process.exitCode = 1 +} + +function metadata(workspace) { + const output = execFileSync( + 'cargo', + ['metadata', '--locked', '--offline', '--format-version', '1', '--all-features'], + { cwd: workspace, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }, + ) + return JSON.parse(output) +} + +function lockValue(packageBlock, key) { + return packageBlock.match(new RegExp(`^${key} = "([^"]+)"$`, 'mu'))?.[1] +} + +function auditLockfile(workspace) { + const lockPath = workspace === '.' ? 'Cargo.lock' : `${workspace}/Cargo.lock` + const packageBlocks = readFileSync(lockPath, 'utf8') + .split('[[package]]') + .slice(1) + + for (const [name, version, checksum] of expected) { + const matches = packageBlocks.filter( + (block) => lockValue(block, 'name') === name, + ) + if (matches.length !== 1) { + fail(workspace, `expected one locked ${name} package, found ${matches.length}`) + continue + } + const block = matches[0] + if ( + lockValue(block, 'version') !== version || + lockValue(block, 'source') !== registrySource || + lockValue(block, 'checksum') !== checksum + ) { + fail( + workspace, + `locked ${name} source, version, or checksum differs from the audited contract`, + ) + } + } +} + +function auditWorkflowFetchOrder() { + const workflow = readFileSync('.github/workflows/test.yml', 'utf8') + const lines = workflow.split('\n').map((line) => line.trim()) + const commandIndex = (command) => + lines.findIndex((line) => line === command || line === `run: ${command}`) + const rootFetch = commandIndex('cargo fetch --locked') + const demoFetch = commandIndex( + 'cargo fetch --locked --manifest-path examples/app-demo/Cargo.toml', + ) + const audit = commandIndex('node scripts/check_flate_dependency_contract.mjs') + + if ( + rootFetch === -1 || + demoFetch === -1 || + audit === -1 || + rootFetch >= audit || + demoFetch >= audit + ) { + fail( + 'workflow', + 'root and app-demo locked fetches must run before the offline flate dependency audit', + ) + } +} + +function audit(workspace) { + auditLockfile(workspace) + const graph = metadata(workspace) + const packagesById = new Map(graph.packages.map((pkg) => [pkg.id, pkg])) + const nodesById = new Map(graph.resolve.nodes.map((node) => [node.id, node])) + + for (const [name, version] of expected) { + const matches = graph.packages.filter((pkg) => pkg.name === name) + if (matches.length !== 1) { + fail(workspace, `expected one ${name} package, found ${matches.length}`) + continue + } + if (matches[0].version !== version) { + fail( + workspace, + `expected ${name} ${version}, resolved ${matches[0].version}`, + ) + } + } + + const core = graph.packages.find((pkg) => pkg.name === 'edgezero-core') + if (core === undefined) { + fail(workspace, 'edgezero-core package is missing') + return + } + const coreNode = nodesById.get(core.id) + if (coreNode === undefined) { + fail(workspace, 'edgezero-core resolve node is missing') + return + } + + for (const [name, version] of expected) { + const declarations = core.dependencies.filter( + (dependency) => dependency.name === name && dependency.kind === null, + ) + if (declarations.length !== 1 || declarations[0].req !== `=${version}`) { + fail( + workspace, + `edgezero-core must declare exactly one normal ${name} dependency pinned as =${version}`, + ) + } + const direct = coreNode.dependencies + .map((id) => packagesById.get(id)) + .find((pkg) => pkg?.name === name) + if (direct === undefined) { + fail(workspace, `edgezero-core does not pin ${name} as a normal dependency`) + } + } +} + +auditWorkflowFetchOrder() +audit('.') +audit('examples/app-demo') +audit('tests/fixtures/reusable-app') + +if (process.exitCode === undefined) { + process.stdout.write('flate dependency contract: ok\n') +} diff --git a/scripts/check_forwarding_example.mjs b/scripts/check_forwarding_example.mjs new file mode 100644 index 000000000..6e638c3c8 --- /dev/null +++ b/scripts/check_forwarding_example.mjs @@ -0,0 +1,233 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { basename, dirname, isAbsolute, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const repository = fileURLToPath(new URL("../", import.meta.url)); + +export function extractForwardingExample(markdown) { + const lines = markdown.split("\n"); + const examples = []; + let sections = 0; + let selected = false; + let fence; + for (const [index, rawLine] of lines.entries()) { + const line = rawLine.replace(/\r$/u, ""); + if (fence) { + if ( + new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`, "u").test( + line, + ) + ) { + if (fence.selected) { + if (fence.language !== "rust") { + throw new Error("forwarding example must use a plain rust fence"); + } + examples.push(lines.slice(fence.start + 1, index).join("\n") + "\n"); + } + fence = undefined; + } + continue; + } + const opening = /^ {0,3}(`{3,}|~{3,})(.*)$/u.exec(line); + if (opening) { + fence = { + marker: opening[1][0], + length: opening[1].length, + language: opening[2].trim(), + start: index, + selected, + }; + continue; + } + const heading = /^(#{1,6})\s+(.+?)\s*#*\s*$/u.exec(line); + if (heading && heading[1].length <= 2) { + selected = + heading[1] === "##" && heading[2] === "Forward An Inbound Request"; + if (selected) sections += 1; + } + } + if ( + fence?.selected || + sections !== 1 || + examples.length !== 1 || + !examples[0].trim() + ) { + throw new Error( + "forwarding section must contain exactly one closed, nonempty Rust fence", + ); + } + return examples[0]; +} + +export function coreArtifact(output, root = repository) { + let records; + try { + records = output + .split(/\r?\n/u) + .filter((line) => line.trim()) + .map((line) => JSON.parse(line)); + } catch { + throw new Error("Cargo did not emit valid JSON artifact records"); + } + const finished = records.filter( + (record) => record.reason === "build-finished", + ); + if (finished.length !== 1 || finished[0].success !== true) { + throw new Error("Cargo build did not finish successfully"); + } + const artifacts = records.filter( + (record) => + record.reason === "compiler-artifact" && + record.manifest_path === join(root, "crates/edgezero-core/Cargo.toml") && + record.target?.name === "edgezero_core" && + record.target.kind?.includes("lib"), + ); + if (artifacts.length !== 1) { + throw new Error("Cargo must emit exactly one core library artifact"); + } + const artifact = artifacts[0]; + const libraries = artifact.filenames.filter((name) => name.endsWith(".rlib")); + if (libraries.length !== 1 || !isAbsolute(libraries[0])) { + throw new Error( + "Cargo core artifact must identify exactly one absolute rlib path", + ); + } + if (!["2015", "2018", "2021", "2024"].includes(artifact.target.edition)) { + throw new Error("Cargo core artifact must identify its Rust edition"); + } + const directory = dirname(libraries[0]); + return { + rlib: libraries[0], + dependencies: + basename(directory) === "deps" ? directory : join(directory, "deps"), + edition: artifact.target.edition, + }; +} + +function command(program, args, root, label) { + try { + return execFileSync(program, args, { + cwd: root, + encoding: "utf8", + maxBuffer: 32 * 1024 * 1024, + timeout: 10 * 60 * 1000, + stdio: ["ignore", "pipe", "pipe"], + }); + } catch (error) { + throw new Error( + `${label} failed\n${error.stdout ?? ""}\n${error.stderr ?? error.message}`, + ); + } +} + +export function buildCore(root = repository) { + return coreArtifact( + command( + process.env.CARGO || "cargo", + [ + "build", + "--offline", + "--locked", + "-p", + "edgezero-core", + "--lib", + "--message-format=json", + ], + root, + "Cargo core build", + ), + root, + ); +} + +export function requireOneDoctest(listing) { + if ( + listing.split("\n").filter((line) => line.endsWith(": test")).length !== + 1 || + !/^1 test, 0 benchmarks\s*$/mu.test(listing) || + /ignored?/iu.test(listing) + ) { + throw new Error( + "expected exactly one non-ignored listed forwarding doctest", + ); + } +} + +export function requireOnePassedDoctest(output) { + const summaries = output + .split("\n") + .filter((line) => line.startsWith("test result:")); + if ( + summaries.length !== 1 || + !/^test result: ok\. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;/u.test( + summaries[0], + ) + ) { + throw new Error( + "expected exactly one passed, non-ignored forwarding doctest", + ); + } +} + +export function runDoctest(code, artifact, root = repository) { + const temporary = mkdtempSync(join(tmpdir(), "edgezero-forwarding-")); + try { + const markdown = join(temporary, "forwarding.md"); + const delimiter = "`".repeat( + (code.match(/`+/gu) ?? []).reduce( + (length, run) => Math.max(length, run.length + 1), + 3, + ), + ); + // Compile item definitions, but never dispatch or poll the network handler. + writeFileSync( + markdown, + delimiter + "rust\n" + code + "\n# fn main() {}\n" + delimiter + "\n", + ); + const args = [ + "--test", + markdown, + "--edition", + artifact.edition, + "--extern", + `edgezero_core=${artifact.rlib}`, + "-L", + `dependency=${artifact.dependencies}`, + "--test-args=--color=never", + ]; + const rustdoc = process.env.RUSTDOC || "rustdoc"; + requireOneDoctest( + command( + rustdoc, + [...args, "--test-args=--list"], + root, + "rustdoc listing", + ), + ); + requireOnePassedDoctest(command(rustdoc, args, root, "forwarding doctest")); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(resolve(process.argv[1])).href +) { + try { + const code = extractForwardingExample( + readFileSync(join(repository, "docs/guide/proxying.md"), "utf8"), + ); + runDoctest(code, buildCore()); + process.stdout.write( + "forwarding example: one doctest passed; network handler not invoked\n", + ); + } catch (error) { + process.stderr.write(`forwarding example: ${error.message}\n`); + process.exitCode = 1; + } +} diff --git a/scripts/check_forwarding_example.test.mjs b/scripts/check_forwarding_example.test.mjs new file mode 100644 index 000000000..9b92dbe60 --- /dev/null +++ b/scripts/check_forwarding_example.test.mjs @@ -0,0 +1,210 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; +import * as checker from "./check_forwarding_example.mjs"; + +const root = fileURLToPath(new URL("../", import.meta.url)); +const guide = readFileSync(join(root, "docs/guide/proxying.md"), "utf8"); +const section = (code = "fn example() {}", language = "rust") => + `## Forward An Inbound Request\n\n\`\`\`${language}\n${code}\n\`\`\`\n`; + +test("extracts the real forwarding fence without changing its code", () => { + assert.equal(typeof checker.extractForwardingExample, "function"); + const example = checker.extractForwardingExample(guide); + assert.ok(example.includes("ctx.into_request()?")); + assert.ok(example.includes("ResponseDecoding::Original")); + assert.ok(guide.includes(example)); +}); + +test("rejects missing, duplicate, empty and unclosed forwarding fences", () => { + for (const input of [ + "## Other\n" + + section().replace("## Forward An Inbound Request", "### Child"), + section() + section(), + "## Forward An Inbound Request\nNo example.\n", + section() + "\n```rust\nfn second() {}\n```\n", + "## Forward An Inbound Request\n```rust\nfn example() {}\n", + section(""), + ]) { + assert.throws(() => checker.extractForwardingExample(input), /forwarding/i); + } +}); + +test("rejects doctest annotations that can weaken the mandatory gate", () => { + for (const language of [ + "rust,ignore", + "rust,ignore-x86_64", + "rust,no_run", + "rust,compile_fail", + "rust,should_panic", + "rust,edition2018", + "rs", + "", + ]) { + assert.throws( + () => + checker.extractForwardingExample(section("fn example() {}", language)), + /plain rust/i, + ); + } +}); + +test("does not include other sections or mistake fenced headings for sections", () => { + const input = + "## Before\n```text\n## Forward An Inbound Request\n```\n" + + section("fn example() {}\n// ## Other") + + "## After\n```rust\nfn unrelated() {}\n```\n"; + assert.equal( + checker.extractForwardingExample(input), + "fn example() {}\n// ## Other\n", + ); +}); + +function cargoOutput(change = () => {}) { + const artifact = { + reason: "compiler-artifact", + manifest_path: join(root, "crates/edgezero-core/Cargo.toml"), + target: { name: "edgezero_core", kind: ["lib"], edition: "2024" }, + filenames: ["/custom target/debug/libedgezero_core.rlib"], + fresh: true, + }; + const records = [artifact, { reason: "build-finished", success: true }]; + change(records); + return records.map((record) => JSON.stringify(record)).join("\n"); +} + +test("locates fresh core artifacts without assuming the target directory", () => { + assert.deepEqual(checker.coreArtifact(cargoOutput(), root), { + rlib: "/custom target/debug/libedgezero_core.rlib", + dependencies: "/custom target/debug/deps", + edition: "2024", + }); + const output = cargoOutput((records) => { + records[0].filenames = ["/elsewhere/deps/libedgezero_core-123.rlib"]; + }); + assert.equal( + checker.coreArtifact(output, root).dependencies, + "/elsewhere/deps", + ); +}); + +test("rejects failed builds, missing or ambiguous artifacts and malformed JSON", () => { + for (const output of [ + "not JSON", + cargoOutput((records) => records.pop()), + cargoOutput((records) => (records[1].success = false)), + cargoOutput((records) => (records[0].manifest_path = "/other/Cargo.toml")), + cargoOutput((records) => (records[0].filenames = [])), + cargoOutput((records) => (records[0].target.edition = undefined)), + cargoOutput((records) => records.unshift(structuredClone(records[0]))), + ]) { + assert.throws( + () => checker.coreArtifact(output, root), + /Cargo|artifact|edition/, + ); + } +}); + +test("requires one listed and one passed non-ignored doctest", () => { + checker.requireOneDoctest( + "example.md - (line 2): test\n\n1 test, 0 benchmarks\n", + ); + checker.requireOnePassedDoctest( + "running 1 test\ntest example.md - (line 2) ... ok\n" + + "test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s\n", + ); + for (const listing of [ + "0 tests, 0 benchmarks\n", + "a: test\nb: test\n\n2 tests, 0 benchmarks\n", + "a: test\n\n1 test, 0 benchmarks\nignored", + "a: test\n", + ]) { + assert.throws(() => checker.requireOneDoctest(listing), /one.*doctest/i); + } + for (const result of [ + "test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0s", + "test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0s", + "test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0s", + "test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0s\n" + + "test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0s", + ]) { + assert.throws( + () => checker.requireOnePassedDoctest(result), + /one.*doctest/i, + ); + } +}); + +test("real rustdoc compiles the guide and rejects broken forwarding code", async (t) => { + assert.equal(typeof checker.buildCore, "function"); + const artifact = checker.buildCore(root); + const code = checker.extractForwardingExample(guide); + await t.test( + "actual example passes without calling the network handler", + () => { + checker.runDoctest(code, artifact, root); + }, + ); + await t.test( + "missing into_request question mark is a real compiler failure", + () => { + const broken = code.replace("ctx.into_request()?", "ctx.into_request()"); + assert.notEqual(broken, code); + assert.throws(() => checker.runDoctest(broken, artifact, root), /E0308/); + }, + ); + await t.test("invalid Rust fails compilation", () => { + assert.throws( + () => checker.runDoctest(code + "\nfn broken( {}", artifact, root), + /doctest.*failed/s, + ); + }); + await t.test("item bodies are not executed", () => { + checker.runDoctest( + code + '\nfn not_called() { panic!("do not execute"); }', + artifact, + root, + ); + }); + await t.test("embedded fences cannot hide invalid Rust", () => { + const payload = "fn main() {}\n```\n```text\nthis is not valid Rust"; + const extracted = checker.extractForwardingExample( + "## Forward An Inbound Request\n~~~rust\n" + payload + "\n~~~\n", + ); + assert.equal(extracted, payload + "\n"); + assert.throws( + () => checker.runDoctest(extracted, artifact, root), + /doctest.*failed/s, + ); + }); + await t.test("embedded fences cannot enable a snippet-owned main", () => { + const payload = + 'fn handler() { panic!("HANDLER_WAS_CALLED"); }\n' + + "fn main() { handler(); }\n```\n```text\n"; + const extracted = checker.extractForwardingExample( + "## Forward An Inbound Request\n````rust\n" + payload + "\n````\n", + ); + assert.throws( + () => checker.runDoctest(extracted, artifact, root), + (error) => + error.message.includes("Couldn't compile the test") && + !error.message.includes("Test executable failed"), + ); + }); + await t.test( + "top-level executable statements cannot invoke the handler", + () => { + assert.throws( + () => + checker.runDoctest( + code + '\npanic!("do not execute");', + artifact, + root, + ), + /doctest.*failed/s, + ); + }, + ); +}); diff --git a/scripts/check_no_legacy_typed_reads.sh b/scripts/check_no_legacy_typed_reads.sh index 2bf4407e2..d2a5fd06f 100755 --- a/scripts/check_no_legacy_typed_reads.sh +++ b/scripts/check_no_legacy_typed_reads.sh @@ -69,6 +69,21 @@ done < <( 2>/dev/null || true ) +# ------------------------------------------------------------------ +# Pattern 4: superseded serde extraction constructor +# ------------------------------------------------------------------ +while IFS= read -r hit; do + printf '%s: violation: superseded config serde constructor — use store_deserialization_from_serde\n' "${hit}" + VIOLATIONS=$((VIOLATIONS + 1)) +done < <( + grep -rn --include="*.rs" \ + --exclude-dir=target \ + 'config_out_of_date_from_serde' \ + "${REPO_ROOT}/crates" \ + "${REPO_ROOT}/examples" \ + 2>/dev/null || true +) + if [ "${VIOLATIONS}" -gt 0 ]; then printf '\n%d violation(s) found. Migrate to the AppConfig blob-model extractor.\n' "${VIOLATIONS}" >&2 exit 1 diff --git a/scripts/check_outbound_cutover_evidence.mjs b/scripts/check_outbound_cutover_evidence.mjs new file mode 100644 index 000000000..0d5049132 --- /dev/null +++ b/scripts/check_outbound_cutover_evidence.mjs @@ -0,0 +1,917 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto"; +import { + closeSync, + fstatSync, + openSync, + readFileSync, + readSync, + realpathSync, + statSync, + writeFileSync, +} from "node:fs"; +import { dirname, isAbsolute, relative, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const SPEC = "docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md"; +const INVENTORY = "tests/fixtures/reusable-app/outbound-cutover-cases.json"; +// Updating the reviewed environment/split set requires a separate policy review. +const REVIEWED_LEG_SET_SHA256 = + "5ad5ad13cb3a0ea9d96307f1ec150649a21357490b4b9b26ae59027e0e539ec9"; +const sha256 = (value) => createHash("sha256").update(value).digest("hex"); +const hex = (value, length) => + typeof value === "string" && new RegExp(`^[a-f0-9]{${length}}$`).test(value); +const nonempty = (value) => + typeof value === "string" && value.trim().length > 0; +const key = (value) => `${value.case_id}@${value.environment}`; +function requireFact(value, message) { + if (!value) throw new Error(message); +} +function unique(values, describe) { + const map = new Map(); + for (const value of values) { + const id = describe(value); + requireFact(!map.has(id), `duplicate ${id}`); + map.set(id, value); + } + return map; +} + +// IDs follow acceptance order, including the unnumbered EZ-8/EZ-9 lists. +// Hash the whole criterion, not a shortened label or its first line. +export function acceptanceCriteria(source) { + const criteria = []; + for (const section of source.split(/(?=^### EZ-)/m)) { + const name = section.match(/^### (EZ-(?:\d+|Q1))\./)?.[1]; + const acceptance = section + .split(/^#### Acceptance criteria\s*$/m)[1] + ?.split(/^#{2,4} /m)[0]; + if (!name || !acceptance) continue; + const [listed, endToEnd] = acceptance.split( + "\n\nThe Trusted Server end-to-end check ", + ); + const entries = [ + ...listed.matchAll(/^(?:\d+\. |- )(.*(?:\n(?!\d+\. |- ).*)*)/gm), + ]; + for (const [index, match] of entries.entries()) { + const text = match[1].trim(); + criteria.push({ + section: name, + case_id: `${name}/acceptance/${String(index + 1).padStart(2, "0")}`, + text, + source_digest: sha256(text), + }); + } + if (endToEnd) { + const text = `The Trusted Server end-to-end check ${endToEnd.trim()}`; + criteria.push({ + section: name, + case_id: `${name}/acceptance/ts-e2e`, + text, + source_digest: sha256(text), + }); + } + } + requireFact(criteria.length > 0, "source has no acceptance criteria"); + return criteria; +} + +function validateApproval(decision) { + requireFact( + ["pending", "approved", "rejected"].includes(decision.status), + "invalid approval status", + ); + requireFact( + Array.isArray(decision.scope) && + decision.scope.length > 0 && + new Set(decision.scope).size === decision.scope.length, + "approval needs explicit leg scope", + ); + if (decision.status !== "approved") return; + requireFact( + ["promote", "present", "defer", "requirement-change"].includes( + decision.action, + ) && + nonempty(decision.edgezero_approver) && + nonempty(decision.consumer_approver) && + /^\d{4}-\d{2}-\d{2}$/.test(decision.date) && + nonempty(decision.evidence) && + nonempty(decision.owner) && + nonempty(decision.ci_owner), + "approval needs attributed decision, date, owners and evidence", + ); + if (decision.action === "present") + requireFact( + nonempty(decision.changelog), + "present approval needs CHANGELOG confirmation", + ); +} + +function baselineApplicability(c) { + const [section, , number] = c.source_id.split("/"); + if (section === "EZ-Q1") return "gate"; + if (section === "EZ-2" && Number(number) >= 16 && Number(number) <= 21) + return "follow-up"; + if (section === "EZ-6" && number === "10") return "follow-up"; + if ( + [ + "EZ-5/acceptance/12", + "EZ-2/acceptance/10b", + "EZ-2/acceptance/13b", + "EZ-5/acceptance/09b", + "EZ-5/acceptance/11b", + "EZ-6/acceptance/23b", + "EZ-8/acceptance/06b", + "EZ-9/acceptance/16b", + ].includes(c.case_id) || + (c.source_id === "EZ-1/acceptance/02" && c.environment.endsWith("/http3")) + ) + return "conditional"; + return Number(section.slice(3)) <= 9 ? "blocking" : "follow-up"; +} + +export function validateManifest( + manifest, + source, + reviewedLegSetSha256 = REVIEWED_LEG_SET_SHA256, +) { + requireFact( + manifest?.schema === 1 && + Array.isArray(manifest.cases) && + manifest.cases.length > 0 && + Array.isArray(manifest.decisions), + "invalid or empty inventory", + ); + requireFact( + manifest.source_digest === sha256(source), + "source document changed; review acceptance preambles and inventory digests", + ); + const criteria = unique(acceptanceCriteria(source), (c) => c.case_id); + const requirements = new Set( + [...source.matchAll(/\*\*(EZ-(?:\d+|Q1)\.\d+)\*\*/g)].map((m) => m[1]), + ); + const decisions = unique(manifest.decisions, (d) => d.id); + for (const decision of decisions.values()) validateApproval(decision); + const cases = unique(manifest.cases, key); + requireFact( + sha256(JSON.stringify([...cases.keys()].sort())) === reviewedLegSetSha256, + "reviewed leg set changed; split/environment policy review required", + ); + const covered = new Set(); + for (const [id, c] of cases) { + const supplemental = + c.source_kind === "normative" && + c.source_id === "EZ-24.12" && + c.case_id === "EZ-24/requirements/12"; + const normativeText = supplemental + ? source + .split("\n") + .find((line) => line.startsWith("- **EZ-24.12** ")) + ?.slice("- **EZ-24.12** ".length) + : null; + const origin = + supplemental && normativeText + ? { text: normativeText, source_digest: sha256(normativeText) } + : criteria.get(c.source_id); + requireFact( + origin && + (supplemental || + c.case_id === c.source_id || + c.case_id.replace(/[a-z]$/, "") === c.source_id), + `${id}: unknown source criterion`, + ); + requireFact( + c.source_digest === origin.source_digest, + `${id}: source digest changed; review required`, + ); + requireFact( + c.expected === origin.text, + `${id}: expected assertion differs from source`, + ); + requireFact( + Array.isArray(c.requirement_ids) && + c.requirement_ids.length > 0 && + c.requirement_ids.every((r) => requirements.has(r)), + `${id}: unknown requirement IDs`, + ); + requireFact( + ["blocking", "follow-up", "conditional", "gate"].includes( + c.applicability, + ), + `${id}: invalid applicability`, + ); + const baseline = baselineApplicability(c); + requireFact( + !["blocking", "gate"].includes(baseline) || c.applicability === baseline, + `${id}: cannot downgrade blocking applicability`, + ); + requireFact( + ["planned", "ready"].includes(c.implementation), + `${id}: invalid implementation state`, + ); + for (const field of [ + "environment", + "owner", + "task", + "substep", + "sentinel", + "command", + "artifact_path", + ]) + requireFact(nonempty(c[field]), `${id}: missing ${field}`); + if (c.additional_sentinels !== undefined) + requireFact( + Array.isArray(c.additional_sentinels) && + c.additional_sentinels.length > 0 && + c.additional_sentinels.every( + (s) => + typeof s === "string" && + /^[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*$/.test(s), + ) && + new Set([c.sentinel, ...c.additional_sentinels]).size === + c.additional_sentinels.length + 1, + `${id}: invalid additional_sentinels`, + ); + requireFact( + !isAbsolute(c.artifact_path) && + !c.artifact_path.split(/[\\/]/).includes(".."), + `${id}: unsafe artifact path`, + ); + if (c.applicability === "conditional") + requireFact(nonempty(c.condition), `${id}: explicit condition required`); + if (c.approval !== null) + requireFact( + decisions.get(c.approval)?.scope.includes(id), + `${id}: missing or incorrectly scoped approval`, + ); + covered.add(c.source_id); + } + for (const id of criteria.keys()) + requireFact(covered.has(id), `missing criterion ${id}`); + for (const decision of decisions.values()) + for (const scope of decision.scope) + requireFact(cases.has(scope), `approval references unknown leg ${scope}`); + return { + criteria: criteria.size, + legs: cases.size, + planned: manifest.cases.filter((c) => c.implementation === "planned") + .length, + }; +} + +function disposition(c, decisions) { + const decision = decisions.get(c.approval); + if (decision?.status === "approved") { + if (["promote", "present"].includes(decision.action)) return "required"; + // Missing implementation, entitlement or runtime never waives a blocking leg. + requireFact( + !["blocking", "gate"].includes(c.applicability) || + decision.action === "requirement-change", + `${key(c)}: blocking applicability requires an approved requirement change`, + ); + return "deferred"; + } + return ["blocking", "gate"].includes(c.applicability) + ? "required" + : "deferred"; +} + +function validateMeasurement(measurement) { + requireFact( + nonempty(measurement.name) && + Number.isSafeInteger(measurement.samples) && + measurement.samples > 0 && + measurement.passed === true, + "failed measurement or zero measurement samples", + ); + requireFact( + Number.isFinite(measurement.value), + "measurement needs a finite value", + ); + if (measurement.rule === "record") return; + requireFact( + ["le", "lt", "ge", "eq"].includes(measurement.rule), + "unsupported measurement rule", + ); + requireFact( + Number.isFinite(measurement.bound), + "measurement needs a finite bound", + ); + const pass = { + le: () => measurement.value <= measurement.bound, + lt: () => measurement.value < measurement.bound, + ge: () => measurement.value >= measurement.bound, + eq: () => measurement.value === measurement.bound, + }[measurement.rule]; + requireFact(pass && pass(), "failed measurement bound"); +} + +function validateBuildIdentity(build, candidateSha) { + requireFact( + hex(build?.build_id, 64) && build.candidate_sha === candidateSha, + "build identity or candidate mismatch", + ); + requireFact(nonempty(build.artifact_path), "build artifact path required"); +} + +function buildIdentities(record, candidateSha) { + requireFact( + Array.isArray(record?.builds) && record.builds.length > 0, + "build identities required", + ); + const builds = unique(record.builds, (build) => build?.build_id); + for (const build of builds.values()) { + validateBuildIdentity(build, candidateSha); + } + return builds; +} + +function artifactHash(root, name) { + requireFact( + nonempty(name) && !isAbsolute(name) && !name.split(/[\\/]/u).includes(".."), + "unsafe artifact path", + ); + const path = realpathSync(resolve(root, name)); + const within = relative(root, path); + requireFact( + within !== "" && !isAbsolute(within) && within.split(/[\\/]/u)[0] !== "..", + "artifact escapes evidence directory", + ); + const limit = 256 * 1024 * 1024; + const before = statSync(path); + requireFact( + before.isFile() && before.size <= limit, + "artifact must be a regular file at most 256 MiB", + ); + const file = openSync(path, "r"); + try { + const metadata = fstatSync(file); + requireFact( + metadata.isFile() && metadata.size <= limit, + "artifact must be a regular file at most 256 MiB", + ); + const hash = createHash("sha256"); + const buffer = Buffer.alloc(64 * 1024); + let total = 0; + for (;;) { + const bytes = readSync(file, buffer); + if (bytes === 0) break; + total += bytes; + requireFact(total <= limit, "artifact exceeds 256 MiB while reading"); + hash.update(buffer.subarray(0, bytes)); + } + requireFact(total === metadata.size, "artifact changed while reading"); + return hash.digest("hex"); + } finally { + closeSync(file); + } +} + +export function assembleRecord( + manifest, + input, + { candidateSha, source, reviewedLegSetSha256, artifactsRoot }, +) { + validateManifest(manifest, source, reviewedLegSetSha256); + requireFact( + hex(candidateSha, 40) && + input?.schema === 1 && + input.candidate_sha === candidateSha, + "candidate revision mismatch", + ); + requireFact( + ["clean", "working-tree"].includes(input.source_state), + "explicit source provenance required", + ); + const root = realpathSync(resolve(artifactsRoot)); + const builds = buildIdentities(input, candidateSha); + for (const build of builds.values()) { + requireFact( + artifactHash(root, build.artifact_path) === build.build_id, + "build artifact hash mismatch", + ); + } + requireFact( + Array.isArray(input.results) && + input.results.length > 0 && + input.results.length <= manifest.cases.length, + "nonzero bounded observation list required", + ); + const cases = unique(manifest.cases, key); + const results = unique(input.results, key); + const assembly = { + observed_legs: results.size, + passed: 0, + failed: 0, + pending: 0, + samples: 0, + }; + const output = structuredClone(input); + output.results = [...results.values()].map((result) => { + const id = key(result); + const c = cases.get(id); + requireFact(c, `unknown case/environment ${id}`); + requireFact( + result.candidate_sha === candidateSha && builds.has(result.build_id), + `${id}: observation candidate/build mismatch`, + ); + requireFact( + result.source_digest === c.source_digest && + result.command === c.command && + result.sentinel === c.sentinel && + result.artifact_path === c.artifact_path, + `${id}: result contract mismatch`, + ); + requireFact( + ["passed", "failed", "pending"].includes(result.status) && + Number.isSafeInteger(result.samples) && + result.samples >= 0, + `${id}: invalid observation status or sample count`, + ); + requireFact( + Array.isArray(result.assertions) && Array.isArray(result.measurements), + `${id}: assertion and measurement lists required`, + ); + const hash = artifactHash(root, result.artifact_path); + requireFact( + result.artifact_sha256 === undefined || result.artifact_sha256 === hash, + `${id}: raw artifact hash mismatch`, + ); + assembly[result.status] += 1; + assembly.samples += result.samples; + requireFact( + Number.isSafeInteger(assembly.samples), + "aggregate sample count overflow", + ); + return { ...structuredClone(result), artifact_sha256: hash }; + }); + output.stage = "pre-freeze"; + output.qualified = false; + output.assembly = assembly; + return output; +} + +export function inspectRelease( + manifest, + record, + { candidateSha, source, reviewedLegSetSha256, artifactsRoot }, +) { + validateManifest(manifest, source, reviewedLegSetSha256); + const findings = []; + const check = (scope, rule, validate) => { + try { + validate(); + return true; + } catch (error) { + findings.push({ scope, rule, message: error.message }); + return false; + } + }; + check("record", "candidate", () => + requireFact( + hex(candidateSha, 40) && + record?.schema === 1 && + record.candidate_sha === candidateSha, + "candidate revision mismatch", + ), + ); + check("record", "stage", () => + requireFact( + record?.stage === "frozen-candidate", + "pre-freeze or missing-stage evidence cannot qualify a release", + ), + ); + check("record", "source", () => + requireFact( + record?.source_state === "clean" && record?.qualification !== false, + "working-tree characterization cannot qualify a release", + ), + ); + check("record", "runner", () => + requireFact(nonempty(record?.runner_image), "runner image required"), + ); + const versions = { + rust: "1.98.1", + fastly_sdk: "0.13.1", + viceroy: "0.21.0", + spin: "4.1.0", + }; + for (const [tool, version] of Object.entries(versions)) + check("record", `toolchain:${tool}`, () => + requireFact( + record?.toolchain?.[tool] === version, + `wrong toolchain ${tool}`, + ), + ); + check("record", "toolchain:workerd", () => + requireFact( + nonempty(record?.toolchain?.workerd), + "workerd revision required", + ), + ); + const retention = record?.retention; + check("record", "retention", () => + requireFact( + ["release-asset", "repository"].includes(retention?.kind) && + nonempty(retention.uri) && + retention.expires_at === null, + "durable non-expiring retention required while the candidate is pinned", + ), + ); + if (record?.comparisons !== null) + check("record", "comparisons", () => + requireFact( + nonempty(record?.comparisons?.baseline) && + record?.comparisons?.candidate === candidateSha, + "comparison build identities required", + ), + ); + const map = (field, describe) => { + const values = new Map(); + const ambiguous = new Set(); + if ( + !check("record", field, () => + requireFact(Array.isArray(record?.[field]), `${field} map required`), + ) + ) + return values; + for (const [index, value] of record[field].entries()) { + let id; + if ( + !check("record", `${field}:${index}`, () => { + requireFact( + value && typeof value === "object" && !Array.isArray(value), + `invalid ${field} entry`, + ); + id = describe(value); + requireFact(nonempty(id), `invalid ${field} identity`); + }) + ) + continue; + if (values.has(id) || ambiguous.has(id)) { + check(field === "builds" ? "record" : id, `${field}:duplicate`, () => + requireFact(false, `duplicate ${id}`), + ); + values.delete(id); + ambiguous.add(id); + } else values.set(id, value); + } + return values; + }; + const legIdentity = (value) => + nonempty(value.case_id) && nonempty(value.environment) + ? key(value) + : undefined; + const results = map("results", legIdentity); + const applicability = map("applicability", legIdentity); + const cases = unique(manifest.cases, key); + const decisions = unique(manifest.decisions, (d) => d.id); + const builds = map("builds", (build) => build.build_id); + check("record", "builds:nonempty", () => + requireFact(builds.size > 0, "build identities required"), + ); + for (const [id, build] of builds) { + if ( + !check("record", `build:${id}`, () => + validateBuildIdentity(build, candidateSha), + ) + ) + builds.delete(id); + } + let root; + if (artifactsRoot !== undefined) { + check("record", "artifacts", () => { + root = realpathSync(resolve(artifactsRoot)); + }); + for (const build of builds.values()) { + check("record", `build:${build.build_id}`, () => + requireFact( + root && artifactHash(root, build.artifact_path) === build.build_id, + "build artifact hash mismatch", + ), + ); + } + } + for (const id of [...results.keys(), ...applicability.keys()]) + check(id, "known-leg", () => + requireFact(cases.has(id), `unknown case/environment ${id}`), + ); + const passed = []; + const deferred = []; + for (const [id, c] of cases) { + const start = findings.length; + let required; + if ( + !check(id, "disposition", () => { + required = disposition(c, decisions); + }) + ) + continue; + const mapping = applicability.get(id); + check(id, "applicability", () => + requireFact( + mapping?.disposition === required, + `${id}: wrong or missing applicability`, + ), + ); + if (required === "deferred") { + const approved = decisions.get(c.approval)?.status === "approved"; + check(id, "deferral", () => + requireFact( + approved + ? mapping?.approval === c.approval + : mapping?.condition === + (c.condition ?? "follow-up not promoted or confirmed present"), + `${id}: unjustified applicability deferral`, + ), + ); + if (start === findings.length) deferred.push(id); + continue; + } + check(id, "implementation", () => + requireFact( + c.implementation === "ready", + `${id}: planned command is not implemented evidence`, + ), + ); + if (c.comparison === true) + check(id, "comparison", () => + requireFact( + record?.comparisons !== null, + `${id}: baseline comparison required`, + ), + ); + const result = results.get(id); + if ( + !check(id, "result", () => requireFact(result, `${id}: missing result`)) + ) + continue; + if (c.comparison === true) + check(id, "comparison-measurements", () => + requireFact( + Array.isArray(result.measurements) && result.measurements.length > 0, + `${id}: comparison measurements required`, + ), + ); + check(id, "candidate", () => + requireFact( + result.candidate_sha === candidateSha, + `${id}: result revision mismatch`, + ), + ); + check(id, "build", () => + requireFact( + builds.has(result.build_id), + `${id}: missing or unknown result build identity`, + ), + ); + check(id, "contract", () => + requireFact( + result.source_digest === c.source_digest && + result.command === c.command && + result.sentinel === c.sentinel && + result.artifact_path === c.artifact_path, + `${id}: result contract mismatch`, + ), + ); + check(id, "outcome", () => + requireFact( + result.status === "passed" && + Number.isSafeInteger(result.samples) && + result.samples > 0, + `${id}: pending/failed result or zero samples`, + ), + ); + check(id, "artifact-digest", () => + requireFact( + hex(result.artifact_sha256, 64), + `${id}: raw artifact hash required`, + ), + ); + check(id, "assertions", () => + requireFact( + Array.isArray(result.assertions) && + result.assertions.length > 0 && + result.assertions.every((a) => nonempty(a.name) && a.passed === true), + `${id}: missing or failed assertions`, + ), + ); + if ( + check(id, "measurements", () => + requireFact( + Array.isArray(result.measurements), + `${id}: measurement list required`, + ), + ) + ) { + for (const [index, measurement] of result.measurements.entries()) + check(id, `measurement:${index}`, () => + validateMeasurement(measurement), + ); + } + if (start === findings.length) passed.push(id); + } + if (artifactsRoot !== undefined) { + for (const [id, result] of results) { + check(id, "raw-artifact", () => + requireFact( + root && + artifactHash(root, result.artifact_path) === result.artifact_sha256, + "raw artifact hash mismatch", + ), + ); + } + } + const invalid = new Set(findings.map((finding) => finding.scope)); + const accepted = (ids) => + invalid.has("record") ? 0 : ids.filter((id) => !invalid.has(id)).length; + return { + status: findings.length === 0 ? "passed" : "blocked", + candidate_sha: candidateSha, + passed: accepted(passed), + deferred: accepted(deferred), + capabilities_changed: false, + qualified: false, + findings, + }; +} + +export function validateRelease(manifest, record, options) { + const report = inspectRelease(manifest, record, options); + requireFact(report.findings.length === 0, report.findings[0]?.message); + return report; +} + +function readJson(path) { + const metadata = statSync(path); + requireFact( + metadata.isFile() && metadata.size <= 16 * 1024 * 1024, + "JSON input must be a regular file at most 16 MiB", + ); + return JSON.parse(readFileSync(path, "utf8")); +} + +export function readySentinels(manifest, environment) { + const names = manifest.cases + .filter( + (c) => c.environment === environment && c.implementation === "ready", + ) + .flatMap((c) => [c.sentinel, ...(c.additional_sentinels ?? [])]); + requireFact( + names.length > 0, + "no ready test sentinels for selected environment", + ); + requireFact( + names.every((s) => /^[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*$/.test(s)), + "ready test sentinels must be Rust test names", + ); + return [...new Set(names)]; +} + +function run(args) { + if (args.length === 1 && args[0] === "--help") { + process.stdout.write( + "Usage: node scripts/check_outbound_cutover_evidence.mjs --manifest [INVENTORY.json]\n node scripts/check_outbound_cutover_evidence.mjs --sentinels ENVIRONMENT [--inventory INVENTORY.json]\n node scripts/check_outbound_cutover_evidence.mjs --assemble --candidate SHA --input OBSERVATIONS.json --artifacts DIRECTORY --output NEW_DRAFT.json [--inventory INVENTORY.json]\n node scripts/check_outbound_cutover_evidence.mjs --report|--release --candidate SHA --record RECORD.json --artifacts DIRECTORY [--inventory INVENTORY.json]\nAssembly preserves observations in an unqualified pre-freeze draft and exits nonzero for open release gates. It refuses overwrites.\nManifest validation and sentinel listing permit planned/pending legs; neither qualifies a release.\nApprovals and raw measurements require independent review; this tool does not authenticate telemetry.\n", + ); + return; + } + const mode = args.shift(); + requireFact( + [ + "--manifest", + "--sentinels", + "--assemble", + "--report", + "--release", + ].includes(mode), + "choose --manifest, --sentinels, --assemble, --report or --release; see --help", + ); + const environment = mode === "--sentinels" ? args.shift() : undefined; + let inventory = resolve(ROOT, INVENTORY); + const options = {}; + if (mode === "--manifest" && args.length === 1 && !args[0].startsWith("--")) + inventory = resolve(args.shift()); + while (args.length) { + const name = args.shift(); + requireFact( + [ + "--candidate", + "--record", + "--artifacts", + "--inventory", + "--input", + "--output", + ].includes(name) && + nonempty(args[0]) && + !args[0].startsWith("--") && + options[name] === undefined, + "unknown, duplicate or incomplete argument", + ); + options[name] = args.shift(); + } + if (options["--inventory"]) inventory = resolve(options["--inventory"]); + const manifest = readJson(inventory); + const source = readFileSync(resolve(ROOT, SPEC), "utf8"); + const report = validateManifest(manifest, source); + requireFact( + new Set(acceptanceCriteria(source).map((c) => c.section)).size === 26, + "source must contain EZ-1 through EZ-25 and EZ-Q1", + ); + if (mode === "--sentinels") { + requireFact( + nonempty(environment) && + Object.keys(options).every((k) => k === "--inventory"), + "sentinel listing needs an environment and optional inventory only", + ); + process.stdout.write( + readySentinels(manifest, environment).join("\n") + "\n", + ); + return; + } + if (mode === "--manifest") { + requireFact( + Object.keys(options).every((k) => k === "--inventory"), + "release arguments cannot be used in manifest mode", + ); + process.stdout.write( + JSON.stringify({ + status: "valid-inventory", + ...report, + qualified: false, + }) + "\n", + ); + return; + } + const allowed = + mode === "--assemble" + ? ["--candidate", "--input", "--artifacts", "--output", "--inventory"] + : ["--candidate", "--record", "--artifacts", "--inventory"]; + requireFact( + Object.keys(options).every((name) => allowed.includes(name)), + "arguments do not match selected mode", + ); + requireFact( + options["--candidate"] && options["--artifacts"], + "candidate and artifacts directory required", + ); + const context = { + candidateSha: options["--candidate"], + source, + artifactsRoot: resolve(options["--artifacts"]), + }; + if (mode === "--assemble") { + requireFact( + options["--input"] && options["--output"], + "assembly needs --input and --output", + ); + const draft = assembleRecord( + manifest, + readJson(resolve(options["--input"])), + context, + ); + const result = inspectRelease(manifest, draft, context); + const output = JSON.stringify(draft, null, 2) + "\n"; + requireFact( + Buffer.byteLength(output) <= 16 * 1024 * 1024, + "assembled JSON exceeds 16 MiB", + ); + writeFileSync(resolve(options["--output"]), output, { + encoding: "utf8", + flag: "wx", + }); + process.stdout.write( + JSON.stringify( + { ...result, record_output: resolve(options["--output"]) }, + null, + 2, + ) + "\n", + ); + process.exitCode = result.status === "passed" ? 0 : 1; + return; + } + requireFact( + options["--record"] && options["--artifacts"], + "release needs --record and --artifacts", + ); + const record = readJson(resolve(options["--record"])); + const result = + mode === "--report" + ? inspectRelease(manifest, record, context) + : validateRelease(manifest, record, context); + process.stdout.write(JSON.stringify(result, null, 2) + "\n"); + process.exitCode = result.status === "passed" ? 0 : 1; +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(resolve(process.argv[1])).href +) { + try { + run(process.argv.slice(2)); + } catch (error) { + process.stderr.write(`outbound cutover evidence: ${error.message}\n`); + process.exitCode = 1; + } +} diff --git a/scripts/check_outbound_cutover_evidence.test.mjs b/scripts/check_outbound_cutover_evidence.test.mjs new file mode 100644 index 000000000..3036bc62a --- /dev/null +++ b/scripts/check_outbound_cutover_evidence.test.mjs @@ -0,0 +1,1124 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + truncateSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; +import * as checker from "./check_outbound_cutover_evidence.mjs"; +import { + acceptanceCriteria, + validateManifest as validateManifestWithPolicy, + validateRelease, +} from "./check_outbound_cutover_evidence.mjs"; + +const source = `### EZ-1. Forwarding + +#### Requirements + +- **EZ-1.1** Forward without a body. + +#### Acceptance criteria + +1. Axum and workerd. The origin sees no bytes. + +#### Out of scope +`; +const sha = "a".repeat(40); +const digest = (value) => createHash("sha256").update(value).digest("hex"); +const clone = (value) => structuredClone(value); +function fixtureLegPolicy(input) { + if (!input.startsWith("### ")) return undefined; + return digest( + JSON.stringify( + acceptanceCriteria(input) + .flatMap((c) => ["axum", "workerd"].map((env) => `${c.case_id}@${env}`)) + .sort(), + ), + ); +} +function validateManifest(manifest, input) { + return validateManifestWithPolicy(manifest, input, fixtureLegPolicy(input)); +} +function inventory(input = source) { + const criterion = acceptanceCriteria(input)[0]; + return { + schema: 1, + source_digest: digest(input), + decisions: [], + cases: ["axum", "workerd"].map((environment) => ({ + case_id: criterion.case_id, + source_id: criterion.case_id, + source_digest: criterion.source_digest, + requirement_ids: [criterion.section + ".1"], + environment, + applicability: "blocking", + approval: null, + condition: null, + owner: "EdgeZero", + task: "P2", + substep: "actual ingress forwarding", + sentinel: "forwards_bodiless_get", + command: `fixture ${environment} forwards_bodiless_get`, + expected: criterion.text, + artifact_path: `outbound/EZ-1/01/${environment}.json`, + implementation: "ready", + })), + }; +} +function evidence(manifest = inventory()) { + return { + schema: 1, + stage: "frozen-candidate", + source_state: "clean", + candidate_sha: sha, + builds: [ + { + build_id: "d".repeat(64), + candidate_sha: sha, + artifact_path: "build/app.bin", + }, + ], + runner_image: "qualification@sha256:" + "b".repeat(64), + toolchain: { + rust: "1.98.1", + fastly_sdk: "0.13.1", + viceroy: "0.21.0", + spin: "4.1.0", + workerd: "1.20261018.0", + }, + retention: { + kind: "release-asset", + uri: "https://example.com/release/evidence", + expires_at: null, + }, + comparisons: null, + applicability: manifest.cases.map((c) => ({ + case_id: c.case_id, + environment: c.environment, + disposition: "required", + })), + results: manifest.cases.map((c) => ({ + case_id: c.case_id, + environment: c.environment, + candidate_sha: sha, + build_id: "d".repeat(64), + source_digest: c.source_digest, + status: "passed", + command: c.command, + sentinel: c.sentinel, + artifact_path: c.artifact_path, + artifact_sha256: "c".repeat(64), + samples: 1, + assertions: [{ name: "origin body bytes", passed: true }], + measurements: [], + })), + }; +} +function release(manifest, record, input = source) { + return validateRelease(manifest, record, { + candidateSha: sha, + source: input, + reviewedLegSetSha256: fixtureLegPolicy(input), + }); +} + +test("manifest validation accepts pending work, not qualification", () => { + const manifest = inventory(); + manifest.cases.forEach((c) => { + c.implementation = "planned"; + }); + assert.equal(validateManifest(manifest, source).legs, 2); + assert.throws(() => release(manifest, evidence(manifest)), /planned/); +}); +test("ready sentinel selection includes every required test, not planned legs", () => { + assert.equal(typeof checker.readySentinels, "function"); + const manifest = inventory(); + manifest.cases[0].additional_sentinels = ["tests::factory"]; + assert.deepEqual(checker.readySentinels(manifest, "axum"), [ + "forwards_bodiless_get", + "tests::factory", + ]); + manifest.cases[0].implementation = "planned"; + assert.throws( + () => checker.readySentinels(manifest, "axum"), + /no ready test sentinels/, + ); +}); +test("additional test sentinels must be nonempty unique Rust test names", () => { + for (const value of [ + null, + [], + [""], + ["a\nb"], + ["factory", "factory"], + ["forwards_bodiless_get"], + ]) { + const manifest = inventory(); + manifest.cases[0].additional_sentinels = value; + assert.throws( + () => validateManifest(manifest, source), + /additional_sentinels/, + ); + } +}); +test("complete exact-revision evidence passes", () => { + assert.equal(release(inventory(), evidence()).passed, 2); +}); +test("comparison legs require measured observations as well as build identities", () => { + const manifest = inventory(); + manifest.cases[0].comparison = true; + const record = evidence(manifest); + record.comparisons = { baseline: "native-control", candidate: sha }; + assert.throws(() => release(manifest, record), /comparison measurements/); + + record.results[0].measurements = [ + { + name: "fixture comparison", + samples: 1, + value: 1, + rule: "le", + bound: 1, + passed: true, + }, + ]; + assert.equal(release(manifest, record).passed, 2); + record.comparisons = null; + assert.throws(() => release(manifest, record), /baseline comparison/); +}); +test("hosted optimizer acceptance requires a native-path comparison", () => { + const manifest = JSON.parse( + readFileSync( + new URL( + "../tests/fixtures/reusable-app/outbound-cutover-cases.json", + import.meta.url, + ), + "utf8", + ), + ); + const legs = manifest.cases.filter( + (c) => c.source_id === "EZ-8/acceptance/08", + ); + assert.equal(legs.length, 1); + assert.equal(legs[0].comparison, true); +}); +test("rejects a missing blocking environment leg", () => { + const record = evidence(); + record.results.pop(); + assert.throws(() => release(inventory(), record), /missing result/); +}); +test("rejects wrong candidate and per-result revisions", () => { + for (const change of [ + (r) => { + r.candidate_sha = "d".repeat(40); + }, + (r) => { + r.results[0].candidate_sha = "d".repeat(40); + }, + ]) { + const record = evidence(); + change(record); + assert.throws(() => release(inventory(), record), /revision/); + } +}); +test("rejects zero samples and failed measurements or assertions", () => { + for (const change of [ + (r) => { + r.results[0].samples = 0; + }, + (r) => { + r.results[0].assertions[0].passed = false; + }, + (r) => { + r.results[0].measurements = [ + { name: "elapsed", samples: 0, passed: true }, + ]; + }, + (r) => { + r.results[0].measurements = [ + { name: "elapsed", samples: 1, passed: false }, + ]; + }, + (r) => { + r.results[0].status = "pending"; + }, + ]) { + const record = evidence(); + change(record); + assert.throws(() => release(inventory(), record)); + } +}); +test("rejects expired-only artifacts, including long-lived CI artifacts", () => { + for (const retention of [ + { + kind: "ci-artifact", + uri: "https://example.com/artifact", + expires_at: "2099-01-01", + }, + { + kind: "release-asset", + uri: "https://example.com/artifact", + expires_at: "2026-11-01", + }, + ]) { + const record = evidence(); + record.retention = retention; + assert.throws(() => release(inventory(), record), /retention/); + } +}); +test("rejects inherited or unknown numeric measurement rules", () => { + for (const rule of ["toString", "constructor", "unknown"]) { + const record = evidence(); + record.results[0].measurements = [ + { + name: "deadline", + samples: 1, + value: 999, + bound: 5, + passed: true, + rule, + }, + ]; + assert.throws(() => release(inventory(), record), /measurement/); + } +}); +test("rejects wrong environments, duplicates and extra results", () => { + for (const change of [ + (r) => { + r.results[0].environment = "viceroy"; + }, + (r) => { + r.results.push(clone(r.results[0])); + }, + (r) => { + r.results[0].case_id = "EZ-9/acceptance/99"; + }, + ]) { + const record = evidence(); + change(record); + assert.throws(() => release(inventory(), record)); + } +}); +test("rejects unjustified deferral and missing applicability maps", () => { + const record = evidence(); + record.applicability[0] = { + ...record.applicability[0], + disposition: "deferred", + reason: "not implemented", + }; + assert.throws(() => release(inventory(), record), /applicability/); + delete record.applicability; + assert.throws(() => release(inventory(), record), /applicability/); +}); +test("cannot relabel a blocking criterion as an unadopted follow-up", () => { + const manifest = inventory(); + manifest.cases[0].applicability = "follow-up"; + assert.throws(() => validateManifest(manifest, source), /blocking/); +}); +test("requires separately scoped approval, not a sibling promotion", () => { + const input = source.replaceAll("EZ-1", "EZ-24"); + const manifest = inventory(input); + manifest.cases[0].applicability = "conditional"; + manifest.cases[0].condition = "EZ-5.9 adopted"; + manifest.cases[0].approval = "EZ-5.9:axum"; + manifest.decisions = [ + { + id: "EZ-5.9:axum", + status: "pending", + scope: [manifest.cases[0].case_id + "@axum"], + }, + ]; + const record = evidence(manifest); + record.applicability[0] = { + ...record.applicability[0], + disposition: "deferred", + condition: "EZ-5.9 adopted", + }; + record.results.shift(); + assert.equal(release(manifest, record, input).deferred, 1); + manifest.decisions[0] = { + ...manifest.decisions[0], + status: "approved", + action: "promote", + }; + assert.throws(() => release(manifest, record, input), /approval/); +}); +test("a blocking Image Optimizer leg cannot be skipped for missing entitlement", () => { + const record = evidence(); + record.applicability[0].disposition = "deferred"; + record.applicability[0].condition = "no entitlement"; + assert.throws(() => release(inventory(), record), /applicability/); +}); +test("rejects changed criterion text, missing criteria and wrong requirement IDs", () => { + for (const change of [ + (m) => { + m.cases[0].source_digest = "0".repeat(64); + }, + (m) => { + m.cases = []; + }, + (m) => { + m.cases[0].requirement_ids = ["EZ-99.1"]; + }, + (m) => { + m.cases[0].expected = "weaker assertion"; + }, + ]) { + const manifest = inventory(); + change(manifest); + assert.throws(() => validateManifest(manifest, source)); + } +}); +test("parser inventories unnumbered criteria and hashes complete multiline text", () => { + const text = + "### EZ-9. Redirects\n#### Acceptance criteria\n\n- Unit. First.\n A second line.\n- Unit. Second.\n\n#### Out of scope\n"; + const criteria = acceptanceCriteria(text); + assert.deepEqual( + criteria.map((c) => c.case_id), + ["EZ-9/acceptance/01", "EZ-9/acceptance/02"], + ); + assert.equal( + criteria[0].source_digest, + digest("Unit. First.\n A second line."), + ); +}); +test("rejects missing toolchain, runner image and comparison build identities", () => { + for (const change of [ + (r) => { + r.toolchain.rust = "1.95.0"; + }, + (r) => { + delete r.runner_image; + }, + (r) => { + delete r.toolchain.workerd; + }, + (r) => { + r.comparisons = { baseline: "", candidate: sha }; + }, + ]) { + const record = evidence(); + change(record); + assert.throws(() => release(inventory(), record)); + } +}); +test("real inventory covers all 26 acceptance sections with distinct caching/pooling cases", () => { + const path = new URL( + "../tests/fixtures/reusable-app/outbound-cutover-cases.json", + import.meta.url, + ); + const manifest = JSON.parse(readFileSync(path, "utf8")); + const spec = readFileSync( + new URL( + "../docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md", + import.meta.url, + ), + "utf8", + ); + const report = validateManifest(manifest, spec); + assert.equal( + new Set(acceptanceCriteria(spec).map((c) => c.section)).size, + 26, + ); + assert.ok(report.legs > report.criteria); + for (const suffix of ["14a", "14b"]) + assert.ok( + manifest.cases.some((c) => c.case_id === `EZ-2/acceptance/${suffix}`), + ); +}); +test("local assembled-app criteria bind guarded tests without closing pending legs", () => { + const manifest = JSON.parse( + readFileSync( + new URL( + "../tests/fixtures/reusable-app/outbound-cutover-cases.json", + import.meta.url, + ), + "utf8", + ), + ); + const workflow = readFileSync( + new URL("../.github/workflows/test.yml", import.meta.url), + "utf8", + ); + const sentinels = [ + "configured_target_limit_applies_on_tcp", + "configured_renderer_applies_to_not_found", + "admission_abort_prevents_response_and_dispatch", + "test_server_preserves_upload_and_read_deadline", + "test_server_streams_before_source_completion", + "test_server_buffers_with_exact_content_length", + "test_server_suppresses_head_payload", + "raw_head_limit_precedes_admission", + "test_server_shutdown_releases_live_source", + "state_client_skips_real_transport_initialization", + "foreign_platform_app_keeps_configured_policies", + ]; + const cases = manifest.cases.filter((c) => c.task === "F9b"); + assert.equal(cases.length, 13); + for (const [index, name] of sentinels.entries()) { + const criterion = String(index + 1).padStart(2, "0"); + const c = cases.find( + (entry) => entry.case_id === `EZ-23/acceptance/${criterion}`, + ); + assert.ok(c, criterion); + const sentinel = `test_utils::tests::${name}`; + const command = + "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets"; + assert.equal(c.implementation, "ready", criterion); + assert.equal(c.environment, "axum/native-tcp", criterion); + assert.equal(c.applicability, "follow-up", criterion); + assert.equal(c.approval, null, criterion); + assert.equal(c.sentinel, sentinel, criterion); + assert.equal(c.command, command, criterion); + assert.equal( + c.artifact_path, + `outbound/EZ-23/acceptance/${criterion}/axum/native-tcp.log`, + ); + assert.ok(workflow.includes(command), `${criterion} CI command`); + } + for (const criterion of ["12", "13"]) { + const c = cases.find( + (entry) => entry.case_id === `EZ-23/acceptance/${criterion}`, + ); + assert.equal(c.implementation, "planned", criterion); + assert.equal(c.approval, null, criterion); + } + const factory = + "service::tests::state_client_skips_real_transport_initialization_factory"; + assert.deepEqual( + cases.find((c) => c.case_id === "EZ-23/acceptance/10").additional_sentinels, + [factory], + ); + const axumStep = workflow + .split("- name: Run Axum host tests")[1] + .split("- name:")[0]; + assert.equal( + axumStep.match(/cargo test/g)?.length, + 1, + "one Axum suite execution", + ); + assert.ok( + !axumStep.includes("test_utils::tests::"), + "no duplicate CI sentinel list", + ); +}); +test("redirect core criteria bind every required test before one module execution", () => { + const manifest = JSON.parse( + readFileSync( + new URL( + "../tests/fixtures/reusable-app/outbound-cutover-cases.json", + import.meta.url, + ), + "utf8", + ), + ); + const spec = readFileSync( + new URL( + "../docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md", + import.meta.url, + ), + "utf8", + ); + assert.deepEqual(validateManifest(manifest, spec), { + criteria: 255, + legs: 511, + planned: 479, + }); + const command = + "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests"; + const bindings = [ + [ + "01", + [ + "every_status_and_portable_method_obeys_the_redirect_table", + "nonredirect_status_precedes_all_location_rules", + "plain_send_does_not_follow_a_scripted_redirect", + ], + ], + [ + "02", + [ + "rewritten_post_drops_content_metadata_and_expectation_for_later_replay", + "replaying_empty_post_preserves_content_type", + "buffered_redirect_replay_shares_the_original_nonempty_allocation", + ], + ], + [ + "03", + [ + "streamed_upload_cannot_replay_and_never_consults_policy", + "replay_rule_precedes_limit_and_invalid_location", + ], + ], + [ + "04", + [ + "inbound_empty_get_stream_is_settled_once_before_all_replayed_hops", + "send_empty_get_scope_closed_from_eof_never_dispatches", + "send_empty_get_scope_closed_from_source_drop_never_dispatches", + "follow_empty_get_scope_closed_from_eof_never_dispatches", + "follow_empty_get_scope_closed_from_source_drop_never_dispatches", + ], + ], + [ + "05", + [ + "limit_four_sends_five_requests_and_skips_a_fifth_policy_call", + "redirect_limit_accepts_only_one_through_twenty", + "location_presence_and_method_rules_precede_limit_and_location_resolution", + "limit_precedes_invalid_location_and_policy", + ], + ], + ["06", ["origin_changes_strip_only_automatic_credentials_before_policy"]], + [ + "07", + [ + "relative_and_absolute_locations_resolve_without_sending_fragments", + "location_presence_and_method_rules_precede_limit_and_location_resolution", + "unusable_locations_fail_at_the_answering_hop_before_policy", + "original_nontext_and_duplicate_location_evidence_survives_normalization", + ], + ], + [ + "08", + [ + "original_nontext_and_duplicate_location_evidence_survives_normalization", + "earlier_stop_rules_do_not_bypass_normal_header_filtering", + ], + ], + [ + "09", + [ + "policy_owns_local_state_and_returns_nonclone_nonsend_nondebug_error_unchanged", + "policy_stop_returns_the_answering_head_as_final", + "non_http_target_can_be_stopped_rejected_or_replaced_but_not_followed_prepared", + "taking_prepared_request_then_following_prepared_is_a_protocol_failure", + "policy_observes_normalized_head_and_next_request_before_any_next_dispatch", + "policy_returned_invalid_request_remains_a_client_error_at_the_selected_hop", + ], + ], + [ + "10", + [ + "followed_bodies_are_dropped_without_polling_or_finalization", + "stopped_and_limit_reached_heads_keep_ordinary_final_body_errors", + "stopped_pending_final_body_waits_only_until_the_original_deadline", + "transport_errors_keep_their_original_category_and_failed_hop_index", + ], + ], + [ + "11", + [ + "redirect_chain_uses_supplied_head_client_and_original_deadline", + "original_chain_deadline_does_not_replace_a_fresh_requests_default_timeout", + "later_prepared_hops_inherit_policy_overrides_not_the_original_options", + "policy_time_is_charged_to_original_deadline_before_follow_or_finalization", + "expired_policy_rejection_is_not_replaced_with_a_deadline_error", + "unsupported_next_options_win_before_the_expired_chain_budget", + "replacement_deadlines_cannot_extend_the_original_final_stream_bound", + "policy_stop_clock_rewind_after_observed_head_is_internal", + "policy_error_survives_a_clock_rewind_after_observed_head", + ], + ], + [ + "12", + [ + "prepared_requests_retain_all_unchanged_caps_and_options", + "policy_authorized_cross_origin_credentials_are_not_stripped_again", + "image_optimizer_options_survive_every_prepared_follow_hop", + "freshly_built_policy_request_uses_its_own_default_caps_and_options", + "later_prepared_hops_inherit_policy_overrides_not_the_original_options", + ], + ], + [ + "13", + [ + "dropping_inflight_second_hop_releases_it_and_prevents_a_third_dispatch", + "dropping_unpolled_follow_releases_the_owned_request_without_transport_work", + "first_pending_hop_drop_prevents_a_second_dispatch", + "closing_callback_during_policy_prevents_the_next_dispatch", + "closing_callback_from_followed_source_drop_prevents_the_next_dispatch", + ], + ], + ["14", ["followed_hop_logs_omit_query_names_values_and_fragments"]], + [ + "15", + [ + "redirect_chain_uses_supplied_head_client_and_original_deadline", + "every_status_and_portable_method_obeys_the_redirect_table", + "rewritten_post_drops_content_metadata_and_expectation_for_later_replay", + "relative_and_absolute_locations_resolve_without_sending_fragments", + "origin_changes_strip_only_automatic_credentials_before_policy", + ], + ], + ]; + const required = new Set(); + for (const [criterion, names] of bindings) { + const c = manifest.cases.find( + (entry) => + entry.case_id === `EZ-9/acceptance/${criterion}` && + entry.environment === "unit/core", + ); + assert.ok(c, criterion); + assert.equal(c.implementation, "ready", criterion); + assert.equal(c.applicability, "blocking", criterion); + assert.equal(c.approval, null, criterion); + assert.equal(c.condition, null, criterion); + assert.equal(c.command, command, criterion); + assert.equal( + c.artifact_path, + "outbound/redirect/unit/core/raw.log", + criterion, + ); + const expected = names.map((name) => `outbound::redirect_tests::${name}`); + assert.deepEqual( + [c.sentinel, ...(c.additional_sentinels ?? [])], + expected, + criterion, + ); + for (const sentinel of expected) required.add(sentinel); + } + assert.equal(required.size, 51); + assert.deepEqual(checker.readySentinels(manifest, "unit/core"), [ + ...required, + ]); +}); + +test("redirect runtime bindings retain exact output paths and leave unproved legs planned", () => { + const manifest = JSON.parse( + readFileSync( + new URL( + "../tests/fixtures/reusable-app/outbound-cutover-cases.json", + import.meta.url, + ), + "utf8", + ), + ); + const providers = [ + ["axum", "axum"], + ["viceroy", "fastly"], + ["workerd", "cloudflare"], + ["spin", "spin"], + ]; + const bound = []; + for (const [environment, adapter] of providers) { + const criteria = ["axum", "viceroy"].includes(environment) + ? ["16", "17"] + : ["16"]; + for (const criterion of criteria) { + const c = manifest.cases.find( + (entry) => + entry.case_id === `EZ-9/acceptance/${criterion}` && + entry.environment === environment, + ); + assert.ok(c, `${criterion}@${environment}`); + assert.equal(c.implementation, "ready"); + assert.equal(c.applicability, "blocking"); + assert.equal(c.approval, null); + assert.equal(c.condition, null); + assert.equal(c.sentinel, `${adapter}-wire-inventory`); + assert.equal( + c.command, + `scripts/smoke_test_reusable_app.sh --adapter ${adapter} --suite outbound-wire --output tests/fixtures/reusable-app/.runs/outbound/redirect/${environment}`, + ); + assert.equal( + c.artifact_path, + `outbound/redirect/${environment}/events.jsonl`, + ); + bound.push(c); + } + } + assert.equal(bound.length, 6); + const pending = manifest.cases.filter((c) => + ["16b", "18", "19", "20"].some( + (criterion) => c.case_id === `EZ-9/acceptance/${criterion}`, + ), + ); + assert.equal(pending.length, 8); + for (const c of pending) { + assert.equal(c.implementation, "planned", `${c.case_id}@${c.environment}`); + if (c.case_id.endsWith("/16b")) { + assert.equal(c.applicability, "conditional"); + assert.equal(c.condition, "EZ-14 present"); + assert.equal(c.approval, `EZ-14 present@${c.environment}`); + assert.equal( + manifest.decisions.find((d) => d.id === c.approval)?.status, + "pending", + ); + } + } + const ready = manifest.cases.filter((c) => c.implementation === "ready"); + assert.equal(ready.length, 32); + assert.ok( + ready.every( + (c) => + c.case_id.startsWith("EZ-9/acceptance/") || + (c.case_id.startsWith("EZ-23/acceptance/") && + Number(c.case_id.split("/").at(-1)) <= 11), + ), + ); +}); +test("rejects deletion of reviewed split cases or environment legs", () => { + const path = new URL( + "../tests/fixtures/reusable-app/outbound-cutover-cases.json", + import.meta.url, + ); + const original = JSON.parse(readFileSync(path, "utf8")); + const spec = readFileSync( + new URL( + "../docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md", + import.meta.url, + ), + "utf8", + ); + for (const remove of [ + (c) => c.case_id === "EZ-2/acceptance/14b", + (c) => c.case_id === "EZ-1/acceptance/01" && c.environment === "spin/http1", + (c) => c.case_id === "EZ-5/acceptance/09", + ]) { + const manifest = clone(original); + manifest.cases = manifest.cases.filter((c) => !remove(c)); + assert.throws(() => validateManifest(manifest, spec), /leg set/); + } +}); +test("CLI help is usable without evidence and unknown arguments fail", () => { + const script = fileURLToPath( + new URL("./check_outbound_cutover_evidence.mjs", import.meta.url), + ); + const help = spawnSync(process.execPath, [script, "--help"], { + encoding: "utf8", + }); + assert.equal(help.status, 0); + assert.match(help.stdout, /--manifest/); + assert.match(help.stdout, /--release/); + assert.notEqual(spawnSync(process.execPath, [script, "--made-up"]).status, 0); +}); + +function assemblyFixture(t) { + const artifactsRoot = mkdtempSync(join(tmpdir(), "edgezero-record-")); + t.after(() => rmSync(artifactsRoot, { recursive: true, force: true })); + const manifest = inventory(); + const input = evidence(manifest); + const build = Buffer.from("abc"); + input.builds[0].build_id = digest(build); + mkdirSync(join(artifactsRoot, "build")); + writeFileSync(join(artifactsRoot, "build/app.bin"), build); + for (const [index, result] of input.results.entries()) { + result.build_id = digest(build); + delete result.artifact_sha256; + const path = join(artifactsRoot, result.artifact_path); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, JSON.stringify({ candidate_sha: sha, sample: index })); + } + const options = { + candidateSha: sha, + source, + reviewedLegSetSha256: fixtureLegPolicy(source), + artifactsRoot, + }; + return { artifactsRoot, manifest, input, options }; +} + +test("assembly preserves counts, identities and raw hashes without qualification", (t) => { + assert.equal(typeof checker.assembleRecord, "function"); + const { manifest, input, options, artifactsRoot } = assemblyFixture(t); + input.results[0].status = "failed"; + input.results[0].samples = 3; + input.results[0].assertions[0].passed = false; + input.results[1].status = "pending"; + input.results[1].samples = 5; + const original = clone(input); + const assembled = checker.assembleRecord(manifest, input, options); + assert.deepEqual(input, original); + assert.equal(assembled.candidate_sha, sha); + assert.equal(assembled.stage, "pre-freeze"); + assert.equal(assembled.qualified, false); + assert.deepEqual(assembled.assembly, { + observed_legs: 2, + passed: 0, + failed: 1, + pending: 1, + samples: 8, + }); + for (const [index, result] of assembled.results.entries()) { + assert.equal(result.status, input.results[index].status); + assert.equal(result.samples, input.results[index].samples); + assert.deepEqual(result.assertions, input.results[index].assertions); + assert.equal(result.build_id, digest(Buffer.from("abc"))); + assert.equal( + result.artifact_sha256, + digest(readFileSync(join(artifactsRoot, result.artifact_path))), + ); + } + const report = checker.inspectRelease(manifest, assembled, options); + assert.equal(report.status, "blocked"); + assert.equal(report.qualified, false); + assert.ok( + report.findings.some((finding) => /pre-freeze/.test(finding.message)), + ); + assert.throws(() => release(manifest, assembled), /pre-freeze/); +}); + +test("assembly never promotes working-tree observations or fills missing results", (t) => { + const { manifest, input, options } = assemblyFixture(t); + input.source_state = "working-tree"; + input.qualification = false; + input.results.pop(); + const assembled = checker.assembleRecord(manifest, input, options); + assert.equal(assembled.source_state, "working-tree"); + assert.equal(assembled.qualification, false); + assert.equal(assembled.results.length, 1); + assert.ok( + checker + .inspectRelease(manifest, assembled, options) + .findings.some((finding) => /missing result/.test(finding.message)), + ); +}); + +test("assembly rejects incomplete or mismatched candidate, build and case provenance", (t) => { + const { manifest, input, options } = assemblyFixture(t); + for (const change of [ + (record) => delete record.source_state, + (record) => delete record.candidate_sha, + (record) => (record.results[0].candidate_sha = "e".repeat(40)), + (record) => (record.builds[0].candidate_sha = "e".repeat(40)), + (record) => delete record.builds, + (record) => (record.builds[0].build_id = "e".repeat(64)), + (record) => (record.results[0].build_id = "e".repeat(64)), + (record) => (record.results[0].source_digest = "e".repeat(64)), + (record) => (record.results[0].environment = "unknown"), + (record) => record.results.push(clone(record.results[0])), + (record) => (record.results[0].artifact_sha256 = "e".repeat(64)), + (record) => (record.results[0].status = "unverified"), + (record) => (record.results[0].samples = -1), + ]) { + const record = clone(input); + change(record); + assert.throws(() => checker.assembleRecord(manifest, record, options)); + } +}); + +test("assembly rejects artifact traversal, directories and symlink escapes", (t) => { + const { manifest, input, options, artifactsRoot } = assemblyFixture(t); + for (const path of ["../outside.bin", "/outside.bin", "build"]) { + const record = clone(input); + record.builds[0].artifact_path = path; + assert.throws( + () => checker.assembleRecord(manifest, record, options), + /artifact/, + ); + } + const outside = mkdtempSync(join(tmpdir(), "edgezero-outside-")); + t.after(() => rmSync(outside, { recursive: true, force: true })); + writeFileSync(join(outside, "raw.json"), "not evidence"); + const path = join(artifactsRoot, input.results[0].artifact_path); + rmSync(path); + symlinkSync(join(outside, "raw.json"), path); + assert.throws( + () => checker.assembleRecord(manifest, input, options), + /escapes/, + ); +}); + +test("assembly rejects a FIFO before opening it", (t) => { + const { manifest, input, options, artifactsRoot } = assemblyFixture(t); + const fifo = join(artifactsRoot, "build/pending"); + assert.equal(spawnSync("mkfifo", [fifo]).status, 0); + input.builds[0].artifact_path = "build/pending"; + const script = fileURLToPath( + new URL("./check_outbound_cutover_evidence.mjs", import.meta.url), + ); + const child = spawnSync( + process.execPath, + [ + "--input-type=module", + "-e", + `import {assembleRecord} from ${JSON.stringify(script)}; + const {manifest,input,options}=JSON.parse(process.argv[1]); + assembleRecord(manifest,input,options);`, + JSON.stringify({ manifest, input, options }), + ], + { encoding: "utf8", timeout: 2000, killSignal: "SIGKILL" }, + ); + assert.equal( + child.error, + undefined, + "non-regular artifact blocked the exporter", + ); + assert.equal(child.status, 1); + assert.match(child.stderr, /regular file/); +}); + +test("assembly hashes multiple read buffers and rejects oversized artifacts before reading", (t) => { + const { manifest, input, options, artifactsRoot } = assemblyFixture(t); + const path = join(artifactsRoot, input.results[0].artifact_path); + const bytes = Buffer.alloc(128 * 1024 + 7, 0x5a); + writeFileSync(path, bytes); + assert.equal( + checker.assembleRecord(manifest, input, options).results[0].artifact_sha256, + digest(bytes), + ); + truncateSync(path, 256 * 1024 * 1024 + 1); + assert.throws( + () => checker.assembleRecord(manifest, input, options), + /256 MiB/, + ); +}); + +test("report and release both verify build and raw artifact hashes", (t) => { + const { manifest, input, options, artifactsRoot } = assemblyFixture(t); + const record = checker.assembleRecord(manifest, input, options); + record.stage = "frozen-candidate"; + assert.equal( + checker.validateRelease(manifest, record, options).status, + "passed", + ); + writeFileSync( + join(artifactsRoot, record.results[0].artifact_path), + "changed capture", + ); + assert.equal(checker.inspectRelease(manifest, record, options).passed, 1); + writeFileSync(join(artifactsRoot, "build/app.bin"), "changed build"); + const report = checker.inspectRelease(manifest, record, options); + assert.equal(report.status, "blocked"); + assert.equal( + report.passed, + 0, + "corrupt build/artifact evidence cannot count as accepted", + ); + for (const pattern of [ + /build artifact hash mismatch/, + /raw artifact hash mismatch/, + ]) { + assert.ok(report.findings.some((finding) => pattern.test(finding.message))); + } + assert.throws( + () => checker.validateRelease(manifest, record, options), + /hash mismatch/, + ); +}); + +test("report lists independent blockers using the same release rules", () => { + assert.equal(typeof checker.inspectRelease, "function"); + const manifest = inventory(); + const record = evidence(manifest); + record.toolchain.rust = "wrong"; + record.results[0].samples = 0; + record.results.pop(); + const report = checker.inspectRelease(manifest, record, { + candidateSha: sha, + source, + reviewedLegSetSha256: fixtureLegPolicy(source), + }); + assert.equal(report.status, "blocked"); + for (const pattern of [ + /wrong toolchain rust/, + /zero samples/, + /missing result/, + ]) { + assert.ok( + report.findings.some((finding) => pattern.test(finding.message)), + String(pattern), + ); + } + assert.throws(() => release(manifest, record), /wrong toolchain rust/); + record.results = null; + assert.doesNotThrow(() => + checker.inspectRelease(manifest, record, { + candidateSha: sha, + source, + reviewedLegSetSha256: fixtureLegPolicy(source), + }), + ); +}); + +test("report rejects invalid deferral totals and exposes planned commands without results", () => { + const followup = source.replaceAll("EZ-1", "EZ-10"); + const manifest = inventory(followup); + manifest.cases[1].applicability = "follow-up"; + const record = evidence(manifest); + const options = { + candidateSha: sha, + source: followup, + reviewedLegSetSha256: fixtureLegPolicy(followup), + }; + assert.equal(checker.inspectRelease(manifest, record, options).deferred, 0); + manifest.cases[0].implementation = "planned"; + record.results = []; + const report = checker.inspectRelease(manifest, record, options); + for (const rule of ["implementation", "result"]) { + assert.ok( + report.findings.some( + (finding) => + finding.scope === "EZ-10/acceptance/01@axum" && finding.rule === rule, + ), + ); + } +}); + +test("a duplicate catalog entry does not erase unrelated evidence", () => { + const manifest = inventory(); + const options = { + candidateSha: sha, + source, + reviewedLegSetSha256: fixtureLegPolicy(source), + }; + for (const field of ["results", "applicability"]) { + const record = evidence(manifest); + record[field].push(clone(record[field][0])); + record.results[1].candidate_sha = "e".repeat(40); + const report = checker.inspectRelease(manifest, record, options); + assert.ok( + report.findings.some((finding) => /duplicate/.test(finding.message)), + ); + assert.ok( + report.findings.some( + (finding) => + finding.scope === "EZ-1/acceptance/01@workerd" && + /revision mismatch/.test(finding.message), + ), + ); + assert.equal(report.passed, 0); + } + const record = evidence(manifest); + record.builds.push({ + build_id: "e".repeat(64), + candidate_sha: "e".repeat(40), + artifact_path: "bad.bin", + }); + const report = checker.inspectRelease(manifest, record, options); + assert.ok( + report.findings.some((finding) => + /build identity or candidate mismatch/.test(finding.message), + ), + ); + assert.ok( + !report.findings.some( + (finding) => + finding.scope === "EZ-1/acceptance/01@axum" && finding.rule === "build", + ), + ); +}); + +test("release rejects draft, dirty or unbound build identities", () => { + for (const change of [ + (record) => (record.stage = "pre-freeze"), + (record) => (record.source_state = "working-tree"), + (record) => (record.qualification = false), + (record) => delete record.builds, + (record) => (record.results[0].build_id = "e".repeat(64)), + ]) { + const record = evidence(); + change(record); + assert.throws(() => release(inventory(), record)); + } +}); diff --git a/scripts/check_outbound_docs_contract.mjs b/scripts/check_outbound_docs_contract.mjs new file mode 100644 index 000000000..b9f25d068 --- /dev/null +++ b/scripts/check_outbound_docs_contract.mjs @@ -0,0 +1,2633 @@ +#!/usr/bin/env node + +import { readFileSync } from 'node:fs' + +const capabilityPath = 'docs/guide/capabilities.md' +const adapterOverviewPath = 'docs/guide/adapters/overview.md' +const changelogPath = 'CHANGELOG.md' +const handlersGuidePath = 'docs/guide/handlers.md' +const axumGuidePath = 'docs/guide/adapters/axum.md' +const cloudflareGuidePath = 'docs/guide/adapters/cloudflare.md' +const proxyGuidePath = 'docs/guide/proxying.md' +const routingGuidePath = 'docs/guide/routing.md' +const fastlyGuidePath = 'docs/guide/adapters/fastly.md' +const scaffoldReadmePath = + 'crates/edgezero-cli/src/templates/root/README.md.hbs' +const outboundCorePath = 'crates/edgezero-core/src/outbound.rs' +const outboundCoreProductionPaths = [ + outboundCorePath, + ...[ + 'batch', + 'dispatch', + 'exchange', + 'head', + 'image_optimizer', + 'progress', + 'redirect', + 'scope', + ].map((name) => `crates/edgezero-core/src/outbound/${name}.rs`), +] +const outboundCoreTestPath = 'crates/edgezero-core/src/outbound/tests.rs' +const compressionCorePath = 'crates/edgezero-core/src/compression.rs' +const outboundAdapterPaths = [ + 'crates/edgezero-adapter-axum/src/outbound.rs', + 'crates/edgezero-adapter-cloudflare/src/outbound.rs', + 'crates/edgezero-adapter-fastly/src/outbound.rs', + 'crates/edgezero-adapter-spin/src/outbound.rs', +] +const templateHandlersPath = + 'crates/edgezero-cli/src/templates/core/src/handlers.rs.hbs' +const demoHandlersPath = + 'examples/app-demo/crates/app-demo-core/src/handlers.rs' +const brotliAuditPath = + 'docs/audits/2026-09-27-brotli-decoder-memory-accounting.md' +const outboundSpecPath = + 'docs/superpowers/specs/2026-05-21-outbound-http-design.md' +const platformMetadataDesignPath = + 'docs/superpowers/specs/2026-09-28-platform-resource-metadata-design.md' +const inboundSpecPath = + 'docs/superpowers/specs/2026-08-22-inbound-body-design.md' +const responseEgressSpecPath = + 'docs/superpowers/specs/2026-09-08-response-egress-design.md' +const outboundImplementationIndexPath = + 'docs/superpowers/plans/2026-07-10-outbound-http-implementation.md' +const outboundBatchTerminationPlanPath = + 'docs/superpowers/plans/2026-09-16-outbound-batch-termination.md' +const outboundReviewHardeningPlanPath = + 'docs/superpowers/plans/2026-09-10-outbound-http-review-hardening.md' +const spinPhasePath = + 'docs/superpowers/plans/2026-09-06-outbound-http-phase5-spin.md' +const cloudflarePhasePath = + 'docs/superpowers/plans/2026-09-06-outbound-http-phase4-axum-cloudflare.md' +const fastlyPhasePath = + 'docs/superpowers/plans/2026-09-06-outbound-http-phase6-fastly.md' +const migrationPhasePath = + 'docs/superpowers/plans/2026-09-06-outbound-http-phase7-migration-docs.md' +const sidebarPath = 'docs/.vitepress/config.mts' +const expectedHeader = ['Capability', 'Axum', 'Cloudflare', 'Fastly', 'Spin'] +const expectedIngressRows = [ + ['ingress-admission', 'Native', 'Native', 'Native', 'Native'], + [ + 'ingress-admission-abort', + 'Native', + 'BestEffort', + 'BestEffort', + 'BestEffort', + ], + [ + 'inbound-read-deadlines', + 'Native', + 'BestEffort', + 'BestEffort', + 'BestEffort', + ], + [ + 'raw-ingress-head-limits', + 'Unsupported', + 'Unsupported', + 'Unsupported', + 'Unsupported', + ], + [ + 'raw-ingress-framing-validation', + 'Unsupported', + 'Unsupported', + 'Unsupported', + 'Unsupported', + ], +] +const expectedConfigRows = [ + [ + 'config-read-allocation-bounds', + 'Unsupported', + 'Unsupported', + 'Unsupported', + 'Unsupported', + ], + [ + 'config-read-deadlines', + 'BestEffort', + 'BestEffort', + 'BestEffort', + 'BestEffort', + ], +] +const expectedOutboundRows = [ + ['outbound-http', 'Native', 'Native', 'BestEffort', 'Native'], + [ + 'outbound-authority-override', + 'Native', + 'BestEffort', + 'Native', + 'Unsupported', + ], + [ + 'outbound-batch-cancellation', + 'Native', + 'BestEffort', + 'BestEffort', + 'BestEffort', + ], + [ + 'outbound-batch-completion-order', + 'Native', + 'Native', + 'BestEffort', + 'Native', + ], + ['outbound-batch-slot-isolation', 'Native', 'Native', 'BestEffort', 'Native'], + ['outbound-cache-bypass', 'Native', 'Native', 'Native', 'Native'], + [ + 'outbound-complete-resource-accounting', + 'Unsupported', + 'Unsupported', + 'Unsupported', + 'Unsupported', + ], + ['outbound-header-fidelity', 'Native', 'BestEffort', 'Native', 'BestEffort'], + ['outbound-deadlines', 'Native', 'BestEffort', 'BestEffort', 'BestEffort'], + [ + 'outbound-failure-diagnostics', + 'BestEffort', + 'BestEffort', + 'Native', + 'Native', + ], + [ + 'outbound-image-optimizer', + 'Unsupported', + 'Unsupported', + 'BestEffort', + 'Unsupported', + ], + ['outbound-redirect-following', 'Native', 'Native', 'Native', 'Native'], + [ + 'outbound-flexible-phase-budget', + 'Native', + 'Native', + 'BestEffort', + 'BestEffort', + ], + [ + 'streamed-upload-deadlines', + 'Native', + 'BestEffort', + 'BestEffort', + 'BestEffort', + ], + [ + 'lazy-streamed-response-passthrough', + 'Native', + 'Native', + 'BestEffort', + 'BestEffort', + ], +] +const expectedResponseEgressRows = [ + [ + 'buffered-response-content-length', + 'Native', + 'BestEffort', + 'BestEffort', + 'BestEffort', + ], + [ + 'response-egress-abort', + 'BestEffort', + 'BestEffort', + 'BestEffort', + 'BestEffort', + ], + [ + 'response-egress-backpressure', + 'BestEffort', + 'BestEffort', + 'BestEffort', + 'BestEffort', + ], + [ + 'response-egress-completion', + 'BestEffort', + 'BestEffort', + 'BestEffort', + 'BestEffort', + ], + [ + 'response-write-deadlines', + 'BestEffort', + 'BestEffort', + 'BestEffort', + 'BestEffort', + ], +] +const expectedLimitHeader = ['Control', 'Scope', 'Default'] +const expectedLimitRows = [ + ['max_request_body_bytes', 'Buffered or streamed request bytes', '8 MiB'], + [ + 'max_encoded_response_bytes', + 'Upstream transport bytes before decoding', + 'Unset', + ], + [ + 'max_decoded_response_bytes', + 'Identity or EdgeZero-decoded gzip/deflate/Brotli output', + 'Unset', + ], + [ + 'max_response_bytes', + 'Final buffered response, including raw passthrough', + '1 MiB', + ], + [ + 'max_response_header_bytes', + 'Adapter-visible upstream header name/value bytes before normalization, plus `x-edgezero-proxy` when enabled', + 'Unset', + ], + [ + 'max_response_header_count', + 'Adapter-visible upstream header fields before normalization, plus `x-edgezero-proxy` when enabled', + 'Unset', + ], + [ + 'max_brotli_window_bits', + 'Brotli stream header checked before decoder allocation', + '24', + ], + [ + 'max_decoder_bytes', + 'Pinned policy charge for Brotli/gzip/deflate decoder state', + '32 MiB', + ], + [ + 'max_chunk_bytes', + 'Maximum emitted item size after decoding or passthrough', + 'Unset', + ], +] +const expectedMemoryHeader = [ + 'Target', + 'Primary ceiling', + 'Separate stack', + 'Scope', + 'Memory source / reason', + 'Live inbound requests', + 'Population source / reason', + 'Host ingress/framing charge', + 'Accounting source / reason', +] +const expectedMemoryRows = [ + [ + 'Axum', + 'Unknown', + 'Unknown', + 'Unknown', + 'Unknown: Operator configured', + 'Unknown', + 'Unknown: Operator configured', + 'Unknown', + 'Unknown: Operator configured', + ], + [ + 'Cloudflare Workers', + '128000000 bytes (128 MB)', + 'None', + 'Per instance', + 'Platform limit: Cloudflare Workers', + 'Unknown', + 'Unknown: Provider unpublished', + 'Unknown', + 'Unknown: Provider unpublished', + ], + [ + 'Fastly Compute', + '128000000 bytes (128 MB)', + '1000000 bytes (1 MB)', + 'Per execution', + 'Platform limit: Fastly Compute', + '1', + 'Platform limit: Fastly Compute', + 'Unknown', + 'Unknown: Provider unpublished', + ], + [ + 'Spin (generic)', + 'Unknown', + 'Unknown', + 'Unknown', + 'Unknown: Runtime configured', + 'Unknown', + 'Unknown: Runtime configured', + 'Unknown', + 'Unknown: Runtime configured', + ], + [ + 'Akamai Functions (Spin)', + '134217728 bytes (128 MiB)', + 'None', + 'Per execution', + 'Hosted default: Akamai Functions', + '1', + 'Hosted default: Akamai Functions', + 'Unknown', + 'Unknown: Provider unpublished', + ], +] + +function fail(message) { + process.stderr.write(`outbound docs contract: ${message}\n`) + process.exit(1) +} + +function cells(line) { + const trimmed = line.trim() + if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) return null + return trimmed + .slice(1, -1) + .split('|') + .map((cell) => cell.trim()) +} + +function normalizeCapability(value) { + return value.startsWith('`') && value.endsWith('`') + ? value.slice(1, -1) + : value +} + +function normalizeSupport(value) { + return value + .replace(/\[\^[^\]]+\]$/u, '') + .replace(/[¹²³⁴⁵⁶⁷⁸⁹]+$/u, '') + .trim() +} + +const redirectMethodHeader = [ + 'Status', + 'Method of answering hop', + 'Next method', + 'Next body', +] +const redirectMethodRows = [ + ['301, 302', 'GET, HEAD', 'same', 'empty'], + ['301, 302', 'POST', 'GET', 'empty'], + ['301, 302', 'any other method', 'not followed: MethodNotRewritable', 'none'], + ['303', 'HEAD', 'HEAD', 'empty'], + ['303', 'any other method', 'GET', 'empty'], + ['307, 308', 'any method', 'same', 'replayed'], +] +const redirectGuideFragments = [ + '`OutboundRedirectLimit::new` accepts only 1..=20', + '`send`, `begin`, and batches never follow redirects', + 'Every other status is final', + 'The initial target is not passed to the redirect policy', + 'An open policy also follows private or link-local addresses', + 'There is no built-in SSRF filter', + 'scheme, host, or effective port', + 'only `Authorization`, `Cookie`, and `host_authority_override`', + 'custom API-key headers and `x-amz-*` signatures remain', + 'does not strip credentials a second time', + 'does not add `Referer` or `Origin`', + '`Content-Type`, `Content-Length`, `Content-Encoding`, `Content-Language`, `Content-Location`, and `Expect`', + 'settled once before the initial hop', + 'Streamed uploads cannot be replayed', + 'followable status, Location presence, method, replayability, limit, Location resolution, then policy', + 'resolved non-HTTP target reaches the policy with no prepared request', + 'Original Location multiplicity and text eligibility are captured before filtering', + 'fragments never reach the wire', + 'per-call timeout starts afresh at each hop', + 'initial absolute deadline remains an independent chain-wide upper bound', + 'including policy execution and final streamed body consumption', + 'unsupported options are rejected before an expired budget', + 'synchronous policy cannot be preempted', + 'followed response body is discarded unread', + 'Response-header caps and body-independent validation still apply to every head', + 'final response, including stopped or limit-reached 3xx, uses ordinary decoding, caps, body mode and idle policy', + 'Neither the policy nor its error needs `Send` or `Clone`', + 'zero-based hop index (initial request = 0)', + 'one-based redirect number, unchanged', + 'prepared request retains every unchanged option', + 'fresh request is a complete replacement with its own defaults', + 'same supplied client and shared preparation/head-finalization pipeline', + 'supersedes the historical outbound spec', + 'Spin requires plain-HTTP origins and targets outside a narrowed host list to be declared', + 'same native Image Optimizer call path, with no extra fetch, buffering or serialization', +] +const redirectCapabilityFragments = [ + '`outbound-redirect-following` reports shared redirect policy, not native deadlines, cancellation or complete resource bounds', + 'Spin requires plain-HTTP origins and targets outside a narrowed host list to be declared', + 'Local redirect tests are not hosted qualification', + 'Native Image Optimizer qualification remains open', + 'synchronous blocking horizon is unknown', +] + +function redirectDocumentationContractError(guide, capability) { + const section = guide.match( + /^## Following Redirects\r?\n([\s\S]*?)(?=^## |(?![\s\S]))/mu + )?.[1] + if (section === undefined) + return 'outbound guide is missing Following Redirects' + const table = section + .split(/\r?\n/u) + .map(cells) + .filter((row) => row !== null) + const expected = [ + redirectMethodHeader, + redirectMethodHeader.map(() => '---'), + ...redirectMethodRows, + ] + const normalizedTable = table.map((row, index) => + row.map((cell) => + index === 1 && /^:?-{3,}:?$/u.test(cell) + ? '---' + : normalizeCapability(cell) + ) + ) + if (JSON.stringify(normalizedTable) !== JSON.stringify(expected)) { + return 'redirect method/body table disagrees with EZ-9' + } + for (const [source, fragments] of [ + [section, redirectGuideFragments], + [capability, redirectCapabilityFragments], + ]) { + const normalized = source.replaceAll(/\s+/gu, ' ') + for (const fragment of fragments) { + if (!normalized.includes(fragment)) + return `redirect documentation is missing: ${fragment}` + } + } + for (const source of [guide, capability]) { + if ( + /redirect following (?:are |is |remains )?(?:not implemented|pending)|begun\/redirect integration remains/iu.test( + source + ) + ) { + return 'redirect documentation still marks implementation pending' + } + } + return null +} + +function runRedirectDocumentationContractSelfTests() { + const guide = [ + '## Following Redirects', + ...[ + redirectMethodHeader, + redirectMethodHeader.map(() => '---'), + ...redirectMethodRows, + ].map((row) => `| ${row.join(' | ')} |`), + ...redirectGuideFragments, + ].join('\n') + const capability = redirectCapabilityFragments.join('\n') + const cases = [ + [guide, capability, true], + [guide.replace('## Following Redirects', '## Other'), capability, false], + ] + cases.push([guide + '\n## Next Section\n| Other | Table |', capability, true]) + cases.push([ + guide.replace(redirectGuideFragments[0], '') + + '\n## Next Section\n' + + redirectGuideFragments[0], + capability, + false, + ]) + for (const fragment of redirectGuideFragments) + cases.push([guide.replace(fragment, ''), capability, false]) + for (const fragment of redirectCapabilityFragments) + cases.push([guide, capability.replace(fragment, ''), false]) + for (const row of redirectMethodRows) + cases.push([guide.replace(row.join(' | '), 'incorrect'), capability, false]) + cases.push([ + guide + '\nredirect following remains pending', + capability, + false, + ]) + for (const [ + index, + [candidateGuide, candidateCapability, valid], + ] of cases.entries()) { + if ( + (redirectDocumentationContractError( + candidateGuide, + candidateCapability + ) === + null) !== + valid + ) { + fail(`redirect documentation checker self-test ${index} failed`) + } + } +} + +function readRustU64Constant(source, name) { + const match = source.match( + new RegExp(`pub const ${name}: u64 = ([^;]+);`, 'u') + ) + const expression = match?.[1] + if (expression === undefined) { + fail(`cannot find Rust constant ${name}`) + } + const factors = expression.split('*').map((factor) => factor.trim()) + if (!factors.every((factor) => /^[0-9][0-9_]*$/u.test(factor))) { + fail(`Rust constant ${name} has an unsupported expression: ${expression}`) + } + return factors.reduce( + (product, factor) => product * BigInt(factor.replaceAll('_', '')), + 1n + ) +} + +function formatBinaryBytes(bytes) { + const mebibyte = 1024n * 1024n + if (bytes % mebibyte === 0n) { + return `${bytes / mebibyte} MiB` + } + return `${bytes} bytes` +} + +const qualifiedDeadline = String.raw`(?:[A-Za-z_][A-Za-z0-9_]*\s*::\s*)*Deadline` +const optionalDeadline = String.raw`Option\s*<\s*${qualifiedDeadline}\s*>` +const deadlineType = String.raw`(?:${qualifiedDeadline}|${optionalDeadline})` + +function maskObservationCutoffNonCode(source) { + const masked = source.split('') + const blank = (start, end) => { + for (let index = start; index < end; index += 1) { + if (!/[\r\n]/u.test(masked[index])) masked[index] = ' ' + } + } + let index = 0 + while (index < source.length) { + if (source.startsWith('//', index)) { + const newline = source.indexOf('\n', index + 2) + const end = newline === -1 ? source.length : newline + blank(index, end) + index = end + continue + } + if (source.startsWith('/*', index)) { + let depth = 1 + let end = index + 2 + while (end < source.length && depth > 0) { + if (source.startsWith('/*', end)) { + depth += 1 + end += 2 + } else if (source.startsWith('*/', end)) { + depth -= 1 + end += 2 + } else { + end += 1 + } + } + blank(index, end) + index = end + continue + } + const rest = source.slice(index) + const rawStart = rest.match(/^(?:b|c)?r(#{0,255})"/u) + if (rawStart !== null) { + const delimiter = `"${rawStart[1]}` + const closing = source.indexOf(delimiter, index + rawStart[0].length) + const end = closing === -1 ? source.length : closing + delimiter.length + blank(index, end) + index = end + continue + } + const quoted = rest.match( + /^(?:(?:b|c)?"(?:\\[\s\S]|[^"\\])*"|b?'(?:\\(?:u\{[0-9A-Fa-f_]+\}|x[0-9A-Fa-f]{2}|.)|[^'\\\r\n])')/u + ) + if (quoted !== null) { + blank(index, index + quoted[0].length) + index += quoted[0].length + continue + } + index += 1 + } + return masked.join('') +} + +function markdownFenceOpening(line) { + const match = /^ {0,3}(`{3,}|~{3,})[ \t]*([^\r\n]*)$/u.exec(line) + if (match === null) return null + if (match[1][0] === '`' && match[2].includes('`')) return null + const language = match[2] + .trim() + .split(/[:,\s]/u, 1)[0] + .toLowerCase() + return { + marker: match[1][0], + minimumLength: match[1].length, + rust: language === 'rust' || language === 'rs', + } +} + +function markdownFenceClosing(line, fence) { + let index = 0 + while (index < 3 && line[index] === ' ') index += 1 + let markerLength = 0 + while (line[index + markerLength] === fence.marker) markerLength += 1 + return ( + markerLength >= fence.minimumLength && + line.slice(index + markerLength).trim() === '' + ) +} + +function markdownViews(source) { + const prose = source.split('') + const rust = source.split('') + const blank = (view) => { + for (let index = 0; index < source.length; index += 1) { + if (!/[\r\n]/u.test(source[index])) view[index] = ' ' + } + } + const copy = (view, start, end) => { + for (let index = start; index < end; index += 1) view[index] = source[index] + } + blank(prose) + blank(rust) + + let fence = null + let lineStart = 0 + while (lineStart < source.length) { + const newline = source.indexOf('\n', lineStart) + const lineEnd = newline === -1 ? source.length : newline + const nextLine = newline === -1 ? source.length : newline + 1 + const line = source.slice(lineStart, lineEnd).replace(/\r$/u, '') + + if (fence === null) { + const opening = markdownFenceOpening(line) + if (opening === null) copy(prose, lineStart, nextLine) + else { + fence = { + ...opening, + contentStart: newline === -1 ? source.length : newline + 1, + } + } + } else if (markdownFenceClosing(line, fence)) { + if (fence.rust) copy(rust, fence.contentStart, lineStart) + fence = null + } + + if (newline === -1) break + lineStart = nextLine + } + + if (fence?.rust) copy(rust, fence.contentStart, source.length) + return { prose: prose.join(''), rust: rust.join('') } +} + +function closingDelimiter(source, open) { + const pairs = new Map([ + ['(', ')'], + ['[', ']'], + ['{', '}'], + ]) + const stack = [pairs.get(source[open])] + for (let index = open + 1; index < source.length; index += 1) { + if (pairs.has(source[index])) stack.push(pairs.get(source[index])) + else if (source[index] === stack.at(-1)) { + stack.pop() + if (stack.length === 0) return index + } + } + return -1 +} + +function topLevelArguments(source, open, close) { + const argumentsList = [] + let start = open + 1 + for (let index = start; index <= close; index += 1) { + if (index === close || source[index] === ',') { + const argument = source.slice(start, index).trim() + if (argument !== '') argumentsList.push(argument) + start = index + 1 + } else if ('([{'.includes(source[index])) { + index = closingDelimiter(source, index) + if (index === -1) return [] + } + } + return argumentsList +} + +function signatureContractError( + source, + name, + count, + typeShape, + unusedCount = 0 +) { + const code = maskObservationCutoffNonCode(source) + const signatures = + code.match(new RegExp(String.raw`\bfn\s+${name}\s*\([^)]*\)`, 'gu')) ?? [] + const expectedType = + typeShape === 'optional' ? optionalDeadline : qualifiedDeadline + const parameterCount = (parameter) => + signatures.filter((signature) => + new RegExp( + String.raw`(?:^|,)\s*${parameter}\s*:\s*${expectedType}\s*(?=,|\))`, + 'u' + ).test(signature.slice(signature.indexOf('(') + 1)) + ).length + const staleParameter = new RegExp( + String.raw`(?:^|,)\s*(?:cutoff|_cutoff|batch_cutoff)\s*:\s*${deadlineType}\s*(?=,|\))`, + 'u' + ) + return signatures.some((signature) => staleParameter.test(signature)) || + signatures.length !== count || + parameterCount('observation_cutoff') !== count - unusedCount || + parameterCount('_observation_cutoff') !== unusedCount + ? `${name} must use ${typeShape} observation_cutoff: Deadline in ${count} signature(s)` + : null +} + +function exampleCodeError(code) { + const staleName = String.raw`(?:cutoff|_cutoff|batch_cutoff)` + const staleBinding = new RegExp( + String.raw`^\s*let\s+(?:mut\s+)?${staleName}\b`, + 'mu' + ) + if (staleBinding.test(code)) return 'a stale cutoff binding' + const batchCall = /\b(?:start_batch_until|send_all_until)\s*\(/gu + for (const match of code.matchAll(batchCall)) { + const open = match.index + match[0].lastIndexOf('(') + const close = closingDelimiter(code, open) + if (close === -1) continue + const finalArgument = topLevelArguments(code, open, close).at(-1) + if (new RegExp(String.raw`^${staleName}$`, 'u').test(finalArgument ?? '')) { + return 'a stale batch cutoff argument' + } + } + return null +} + +function exampleContractError(path, source) { + if (!path.endsWith('.md') && !path.endsWith('.mdx')) { + return exampleCodeError(maskObservationCutoffNonCode(source)) + } + const views = markdownViews(source) + return ( + exampleCodeError(views.prose) ?? + exampleCodeError(maskObservationCutoffNonCode(views.rust)) + ) +} + +function runObservationCutoffContractSelfTests() { + const bareCutoff = ['cut', 'off'].join('') + const ignoredCutoff = `_${bareCutoff}` + const batchCutoff = `batch_${bareCutoff}` + for (const [parameters, typeShape, valid] of [ + ['observation_cutoff: Deadline', 'plain', true], + ['observation_cutoff: edgezero_core :: Deadline', 'plain', true], + [ + 'observation_cutoff: Option < edgezero_core :: Deadline >', + 'optional', + true, + ], + ['observation_cutoff: Option', 'plain', false], + ['observation_cutoff: Deadline', 'optional', false], + [`${bareCutoff}: Deadline`, 'plain', false], + [`${ignoredCutoff}: edgezero_core::Deadline`, 'plain', false], + [`${batchCutoff}: Option < Deadline >`, 'optional', false], + [`${bareCutoff}: Deadline, observation_cutoff: Deadline`, 'plain', false], + ]) { + const source = `fn dispatch_budget(request: Request, ${parameters}) {}` + const passed = + signatureContractError(source, 'dispatch_budget', 1, typeShape) === null + if (passed !== valid) { + fail(`observation cutoff signature self-test failed for ${parameters}`) + } + } + for (const [name, source, valid] of [ + ['mutable binding', `let mut ${bareCutoff} = deadline();`, false], + ['ignored binding', `let ${ignoredCutoff} = deadline();`, false], + ['batch binding', `let ${batchCutoff} = deadline();`, false], + [ + 'start call', + `client.start_batch_until(vec![request()], ${bareCutoff});`, + false, + ], + ['ignored call', `client.send_all_until(x, ${ignoredCutoff});`, false], + ['send call', `client.send_all_until(x, ${batchCutoff});`, false], + [ + 'multiline trailing call', + `client.start_batch_until(\n make_requests(),\n ${bareCutoff},\n);`, + false, + ], + [ + 'nested cutoff', + `client.start_batch_until(make_requests(x, ${bareCutoff}), observation_cutoff);`, + true, + ], + ['comment', `// let mut ${bareCutoff} = deadline();`, true], + [ + 'string', + `const TEXT: &str = "send_all_until(requests, ${bareCutoff})";`, + true, + ], + ['budget source', 'let source = BudgetSource::BatchCutoff;', true], + ['constructor', 'let batch = OutboundBatch::cutoff(2);', true], + ]) { + if ((exampleContractError('fixture.rs', source) === null) !== valid) { + fail(`observation cutoff example self-test failed for ${name}`) + } + } + if ( + exampleContractError( + 'fixture.md', + `Prose: send_all_until(requests, ${bareCutoff})\n` + + '```rust\nlet observation_cutoff = deadline();\n```' + ) === null + ) { + fail('observation cutoff self-test missed Markdown prose') + } + if ( + exampleContractError( + 'fixture.md', + `\`\`\`rust,no_run\nlet ${bareCutoff} = deadline();\n\`\`\`` + ) === null + ) { + fail('observation cutoff self-test missed a modified Rust fence') + } + if ( + exampleContractError( + 'fixture.md', + `\`\`\`rust\`bad\nlet ${bareCutoff} = deadline();\n\`\`\`` + ) === null + ) { + fail( + 'observation cutoff self-test treated an invalid backtick fence as a fence' + ) + } + for (const [name, fence] of [ + [ + 'tilde colon modifier', + `~~~rust:no_run\nlet ${bareCutoff} = deadline();\n~~~~`, + ], + [ + 'indented space modifier', + ` \`\`\`rust no_run\nlet ${bareCutoff} = deadline();\n \`\`\``, + ], + [ + 'variable-length rs fence', + `\`\`\`\`rs:ignore\nlet ${bareCutoff} = deadline();\n\`\`\`\`\``, + ], + ]) { + if (exampleContractError('fixture.md', fence) === null) { + fail(`observation cutoff self-test missed ${name}`) + } + } + for (const [name, fence] of [ + ['non-Rust fence', `\`\`\`text\nlet ${bareCutoff} = deadline();\n\`\`\``], + [ + 'Rust fence comments and literals', + `\`\`\`rust\n// let ${bareCutoff} = deadline();\n` + + `const TEXT: &str = "let ${bareCutoff} = deadline();";\n` + + `const RAW: &str = r#"let ${bareCutoff} = deadline();"#;\n\`\`\``, + ], + ]) { + if (exampleContractError('fixture.md', fence) !== null) { + fail(`observation cutoff self-test scanned ${name}`) + } + } + for (const [name, source] of [ + [ + 'block comment', + `fn dispatch_budget(x: Request, observation_cutoff: Deadline) {}\n/*\nlet ${bareCutoff} = deadline();\nfn dispatch_budget(x: Request, ${bareCutoff}: Deadline) {}\n*/`, + ], + [ + 'raw string', + `fn dispatch_budget(x: Request, observation_cutoff: Deadline) {}\nconst TEXT: &str = r#"\nlet ${bareCutoff} = deadline();\nfn dispatch_budget(x: Request, ${bareCutoff}: Deadline) {}\n"#;`, + ], + ]) { + if ( + signatureContractError(source, 'dispatch_budget', 1, 'plain') !== null || + exampleContractError('fixture.rs', source) !== null + ) { + fail(`observation cutoff self-test rejected ${name}`) + } + } +} + +function testOnlyCondition(condition) { + const expression = condition.trim() + if (expression === 'test') return true + const operation = /^(all|any)\s*\(/u.exec(expression) + if (operation === null) return false + const open = operation[0].lastIndexOf('(') + const close = closingDelimiter(expression, open) + if (close === -1 || expression.slice(close + 1).trim() !== '') return false + const inputs = topLevelArguments(expression, open, close) + return operation[1] === 'all' + ? inputs.some(testOnlyCondition) + : inputs.length > 0 && inputs.every(testOnlyCondition) +} + +function productionCode(source) { + const code = maskObservationCutoffNonCode(source) + const view = code.split('') + for (const match of code.matchAll(/#\s*\[\s*cfg\s*\(/gu)) { + const open = match.index + match[0].lastIndexOf('(') + const close = closingDelimiter(code, open) + if (close === -1) fail('unclosed cfg condition') + if (!testOnlyCondition(code.slice(open + 1, close))) continue + let cursor = code.indexOf(']', close) + 1 + let end = -1 + for (; cursor < code.length; cursor += 1) { + if (code[cursor] === '(' || code[cursor] === '[') { + cursor = closingDelimiter(code, cursor) + if (cursor === -1) fail('unclosed test item') + } else if (code[cursor] === '{') { + end = closingDelimiter(code, cursor) + break + } else if (code[cursor] === ';') { + end = cursor + break + } + } + if (end === -1) fail('unclosed test item') + for (let index = match.index; index <= end; index += 1) { + if (code[index] !== '\n') view[index] = ' ' + } + } + return view.join('') +} + +function declarationBody(source, declaration) { + const match = declaration.exec(source) + if (match === null) return null + const open = match.index + match[0].lastIndexOf('{') + const close = closingDelimiter(source, open) + return close === -1 ? null : source.slice(open + 1, close) +} + +function liveOutboundContractError(core, batch, runtime, body) { + const trait = declarationBody( + core, + /\bpub\s+trait\s+OutboundHttpClient\s*:\s*Send\s*\+\s*Sync\s*\{/u + ) + if (trait === null) return 'missing object-safe Send + Sync outbound trait' + const hooks = [ + /\bfn\s+progress\s*\(\s*&self\s*\)\s*->\s*(?:std\s*::\s*rc\s*::\s*)?Rc\s*<\s*dyn\s+OutboundProgressRuntime\s*>\s*;/u, + /\bfn\s+time\s*\(\s*&self\s*\)\s*->\s*OutboundTime\s*;/u, + /\bfn\s+validate_supported_options\s*\(\s*&self\s*,\s*request\s*:\s*&OutboundRequest\s*,\s*mode\s*:\s*OutboundSendMode\s*,?\s*\)\s*->\s*Result\s*<\s*\(\s*\)\s*,\s*EdgeError\s*>\s*;/u, + /\bfn\s+dispatch_head\s*\(\s*&self\s*,\s*request\s*:\s*OutboundDispatchRequest\s*,?\s*\)\s*->\s*OutboundFuture\s*<\s*OutboundHead\s*>\s*;/u, + /\bfn\s+begin\s*\(\s*&self\s*,\s*request\s*:\s*OutboundDispatchRequest\s*,?\s*\)\s*->\s*Result\s*<\s*OutboundPending\s*,\s*EdgeError\s*>\s*;/u, + /\bfn\s+start_batch_until\s*\(\s*&self\s*,\s*batch\s*:\s*OutboundPreparedBatch\s*,?\s*\)\s*->\s*OutboundBatch\s*;/u, + ] + if ( + !hooks.every((hook) => hook.test(trait)) || + /\bfn\s+(?:send|capture_scope)\s*\(/u.test(trait) + ) { + return 'outbound trait must require all six hooks including explicit progress runtime, without defaults or retired hooks' + } + if ( + !/\bpub\s+trait\s+OutboundProgressRuntime\b/u.test(runtime) || + !/\bpub\s+struct\s+IndependentOutboundProgress\s*;/u.test(runtime) || + !/\bimpl\s+OutboundProgressRuntime\s+for\s+IndependentOutboundProgress\b/u.test( + runtime + ) + ) { + return 'outbound progress must expose its runtime trait and one concrete independent implementation, not aliases or test fixtures' + } + const scopeOwner = declarationBody( + runtime, + /\bpub\s+struct\s+OutboundScopeOwner\s*\{/u + ) + if ( + scopeOwner === null || + /\bpub\s*(?:\([^)]*\))?\s*\w+\s*:/u.test(scopeOwner) + ) { + return 'core scope ownership must retain opaque fields' + } + const carrier = declarationBody(body, /\bpub\s+struct\s+Body\s*\{/u) + if (carrier === null || /\bpub\s*(?:\([^)]*\))?\s*\w+\s*:/u.test(carrier)) { + return 'Body must be an opaque carrier, not the retired public pattern enum' + } + const payload = declarationBody(body, /\bpub\s+enum\s+BodyPayload\s*\{/u) + if ( + payload === null || + !/\bOnce\s*\(\s*Bytes\s*\)/u.test(payload) || + !/\bStream\s*\(\s*BodyStream\s*\)/u.test(payload) || + !/\bpub\s+fn\s+into_parts\s*\(\s*(?:mut\s+)?self\s*\)\s*->\s*\(\s*BodyPayload\s*,\s*Option\s*<\s*OutboundScopeOwner\s*>\s*\)/u.test( + body + ) + ) { + return 'owned Body inspection must expose canonical BodyPayload variants and transfer its optional owner through into_parts' + } + for (const name of [ + 'OutboundPreparedBatch', + 'OutboundBatchContext', + 'OutboundBatchItem', + 'OutboundBatchResults', + 'OutboundBatchFailure', + ]) { + const fields = declarationBody( + batch, + new RegExp(String.raw`\bpub\s+struct\s+${name}\s*\{`, 'u') + ) + if (fields === null || /\bpub\s*(?:\([^)]*\))?\s*\w+\s*:/u.test(fields)) { + return `${name} must retain private context or identity fields` + } + const implementation = declarationBody( + batch, + new RegExp(String.raw`\bimpl\s+${name}\s*\{`, 'u') + ) + if ( + implementation !== null && + /\bpub\s+fn\s+(?:new|from_parts)\b/u.test(implementation) + ) { + return `${name} must not have a public reconstruction constructor` + } + } + const driver = declarationBody( + batch, + /\bpub\s+enum\s+OutboundBatchDriverEvent\s*\{/u + ) + if ( + driver === null || + !/\bItem\s*\{\s*index\s*:\s*usize\s*,\s*completed_at\s*:\s*MonotonicInstant\s*,\s*outcome\s*:\s*Result\s*<\s*OutboundResponse\s*,\s*EdgeError\s*>\s*,?\s*\}/u.test( + driver + ) + ) { + return 'drivers must emit raw index, terminal timestamp and outcome, not consumer receipts' + } + if ( + !/\bpub\s+fn\s+cutoff\s*\(\s*context\s*:\s*OutboundBatchContext\s*\)\s*->\s*Self/u.test( + batch + ) || + !/\bpub\s+fn\s+from_driver\s*<\s*S\s*>\s*\(\s*context\s*:\s*OutboundBatchContext\s*,\s*events\s*:\s*S\s*,\s*owner\s*:\s*OutboundExchangeOwner\s*,?\s*\)/u.test( + batch + ) + ) { + return 'batch constructors must consume opaque context and the driver cancellation owner' + } + for (const state of ['AlreadyDelivered', 'Resolved', 'Unresolved']) { + if (!new RegExp(String.raw`\b${state}\b`, 'u').test(batch)) + return `missing collected ownership state ${state}` + } + if (/\b(?:from_futures|finish_batch_item)\b/u.test(core + '\n' + batch)) { + return 'removed future factories and receipt helpers must not remain in production' + } + return null +} + +function observationCutoffContract(coreProduction) { + const facade = declarationBody(coreProduction, /\bimpl\s+HttpClient\s*\{/u) + if ( + facade === null || + !/\bpub\s+fn\s+start_batch_until\s*\(\s*&self\s*,\s*requests\s*:\s*Vec\s*<\s*OutboundRequest\s*>\s*,\s*(?:observation_cutoff|cutoff)\s*:\s*Deadline\s*,?\s*\)/u.test( + facade + ) + ) { + fail( + 'facade batch entry must retain request-vector and absolute observation cutoff' + ) + } + const error = signatureContractError( + readFileSync('crates/edgezero-core/src/time.rs', 'utf8'), + 'dispatch_budget', + 1, + 'optional' + ) + if (error !== null) fail(error) + for (const path of [proxyGuidePath]) { + const exampleError = exampleContractError(path, readFileSync(path, 'utf8')) + if (exampleError !== null) fail(`${path} contains ${exampleError}`) + } +} + +function headerNormalizationContractError(source, fidelity) { + const code = productionCode(source) + if ( + (code.match(/OutboundHeaderInput::new\(/gu)?.length ?? 0) !== 1 || + (code.match(/OutboundHead::new\(/gu)?.length ?? 0) !== 1 || + /\.normalize\s*\(/u.test(code) || + /\bOutboundResponse\s*::\s*new\s*\(/u.test(code) || + !code.includes(`OutboundHeaderFidelity::${fidelity}`) || + /\b(?:normalize_response_headers|insert_proxy_header|apply_response_body_policy)\s*\(/u.test( + code + ) + ) { + return 'must pass original fields and provider fidelity to the single shared head finalizer without adapter normalization' + } + return null +} + +function runHeaderNormalizationContractSelfTests() { + const source = ` + let (parts, context) = request.into_parts(); + let headers = OutboundHeaderInput::new( + "cloudflare", response_headers(&response)?, + OutboundHeaderFidelity::ProviderFieldBoundariesUnknown, + ); + OutboundHead::new(status, headers, encoded, context, owner) + ` + const unknown = 'ProviderFieldBoundariesUnknown' + const cases = [ + [source, unknown, true], + [ + source + + '\n#[cfg(all(test, feature = "test-utils"))] mod tests { OutboundResponse::new(); }', + unknown, + true, + ], + [ + source + '\n#[cfg(test)] fn fixture() { OutboundResponse::new(); }', + unknown, + true, + ], + [source, 'AdapterVisibleFields', false], + [ + source.replace('OutboundHead::new(', 'OutboundResponse::new('), + unknown, + false, + ], + [source.replace('OutboundHeaderInput::new(', 'raw_head('), unknown, false], + [`${source}\nheaders.normalize(limiter);`, unknown, false], + [`${source}\ninsert_proxy_header(headers);`, unknown, false], + [ + `${source}\nOutboundResponse::new(method, status, headers, body, clock, provenance);`, + unknown, + false, + ], + [ + source + .split('\n') + .map((line) => `// ${line}`) + .join('\n'), + unknown, + false, + ], + [`/* ${source} */`, unknown, false], + [`const TEXT: &str = r#"${source}"#;`, unknown, false], + ] + for (const [index, [fixture, fidelity, valid]] of cases.entries()) { + if ( + (headerNormalizationContractError(fixture, fidelity) === null) !== + valid + ) { + fail(`head-first header checker self-test ${index} failed`) + } + } +} + +function runLiveOutboundContractSelfTests() { + const core = ` + pub trait OutboundHttpClient: Send + Sync { + fn progress(&self) -> Rc; + fn time(&self) -> OutboundTime; + fn validate_supported_options(&self, request: &OutboundRequest, mode: OutboundSendMode) -> Result<(), EdgeError>; + fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture; + fn begin(&self, request: OutboundDispatchRequest) -> Result; + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch; + } + ` + const batch = ` + pub struct OutboundPreparedBatch { context: OutboundBatchContext } + pub struct OutboundBatchContext { original: usize } + pub struct OutboundBatchItem { identity: Token, index: usize, result: Result } + pub struct OutboundBatchResults { identity: Token, slots: Slots, termination: State } + pub struct OutboundBatchFailure { identity: Token, error: EdgeError, slots: Slots } + pub enum OutboundBatchDriverEvent { + Item { index: usize, completed_at: MonotonicInstant, outcome: Result }, + } + pub enum OutboundCollectedSlot { AlreadyDelivered, Resolved(OutboundSlotResult), Unresolved } + impl OutboundBatch { + pub fn cutoff(context: OutboundBatchContext) -> Self {} + pub fn from_driver(context: OutboundBatchContext, events: S, owner: OutboundExchangeOwner) -> Self {} + } + ` + const runtime = ` + pub trait OutboundProgressRuntime {} + pub struct IndependentOutboundProgress; + impl OutboundProgressRuntime for IndependentOutboundProgress {} + pub struct OutboundScopeOwner { scope: OutboundScope } + ` + const body = ` + pub struct Body { payload: BodyPayload, owner: Option } + pub enum BodyPayload { Once(Bytes), Stream(BodyStream) } + impl Body { + pub fn into_parts(self) -> (BodyPayload, Option) {} + } + ` + const cases = [ + [core, batch, true], + [core.replace('Rc Rc;', + '' + ), + batch, + false, + ], + [core.replace('fn progress', 'fn capture_scope'), batch, false], + [ + core.replace( + '-> Rc;', + '-> Rc { fallback() }' + ), + batch, + false, + ], + [ + core.replace( + '-> Rc;', + '-> Option>;' + ), + batch, + false, + ], + [core.replace('Rc', 'ProgressAlias'), + batch, + false, + ], + [ + core.replace('fn time', 'fn capture_scope(&self) -> Scope; fn time'), + batch, + false, + ], + [core.replace('fn time(&self) -> OutboundTime;', ''), batch, false], + [ + core.replace( + '-> OutboundFuture;', + '-> OutboundFuture { lazy() }' + ), + batch, + false, + ], + [core.replace('pub trait', 'pub unsafe trait'), batch, false], + [ + core.replace('OutboundPreparedBatch', 'Vec'), + batch, + false, + ], + [ + core.replace( + 'fn time', + 'async fn send(&self, request: OutboundRequest); fn time' + ), + batch, + false, + ], + [ + core, + batch.replace('index: usize, result:', 'pub index: usize, result:'), + false, + ], + [ + core, + batch + + '\nimpl OutboundBatchItem { pub fn new(index: usize) -> Self {} }', + false, + ], + [core, batch.replace('completed_at: MonotonicInstant,', ''), false], + [ + core, + batch.replace( + 'Item { index: usize, completed_at: MonotonicInstant, outcome: Result }', + 'Item(OutboundBatchItem)' + ), + false, + ], + [ + core, + batch.replace( + 'context: OutboundBatchContext) -> Self', + 'slot_count: usize) -> Self' + ), + false, + ], + [core, batch.replace('owner: OutboundExchangeOwner', 'owner: ()'), false], + [core, batch.replace('AlreadyDelivered,', ''), false], + [core + '\nfn from_futures() {}', batch, false], + [core + '\nfn finish_batch_item() {}', batch, false], + ] + for (const [ + index, + [candidateCore, candidateBatch, valid], + ] of cases.entries()) { + if ( + (liveOutboundContractError( + productionCode(candidateCore), + productionCode(candidateBatch), + productionCode(runtime), + productionCode(body) + ) === + null) !== + valid + ) { + fail(`live outbound checker self-test ${index} failed`) + } + } + for (const [index, [candidate, valid]] of [ + [runtime, true], + [runtime.replace('pub trait OutboundProgressRuntime {}', ''), false], + [ + runtime.replace( + 'pub struct IndependentOutboundProgress;', + 'pub type IndependentOutboundProgress = OtherRuntime;' + ), + false, + ], + [ + runtime.replace( + 'impl OutboundProgressRuntime for IndependentOutboundProgress {}', + '' + ), + false, + ], + [ + runtime.replace( + 'pub struct OutboundScopeOwner { scope: OutboundScope }', + '' + ), + false, + ], + [ + runtime.replace('scope: OutboundScope', 'pub scope: OutboundScope'), + false, + ], + [`#[cfg(test)] mod tests { ${runtime} }`, false], + ].entries()) { + if ( + (liveOutboundContractError( + productionCode(core), + productionCode(batch), + productionCode(candidate), + productionCode(body) + ) === + null) !== + valid + ) { + fail(`outbound runtime checker self-test ${index} failed`) + } + } + for (const [index, [candidate, valid]] of [ + [body, true], + [body.replace('into_parts(self)', 'into_parts(mut self)'), true], + [body.replace('pub struct Body', 'pub enum Body'), false], + [body.replace('payload: BodyPayload', 'pub payload: BodyPayload'), false], + [body.replace('owner: Option', 'pub owner: Option'), false], + [ + body.replace( + 'pub enum BodyPayload { Once(Bytes), Stream(BodyStream) }', + 'pub type BodyPayload = OtherBody;' + ), + false, + ], + [body.replace('Stream(BodyStream)', 'Stream(OtherStream)'), false], + [body.replace('Once(Bytes),', ''), false], + [ + body.replace( + '-> (BodyPayload, Option)', + '-> BodyPayload' + ), + false, + ], + [`#[cfg(test)] mod tests { ${body} }`, false], + ].entries()) { + if ( + (liveOutboundContractError( + productionCode(core), + productionCode(batch), + productionCode(runtime), + productionCode(candidate) + ) === + null) !== + valid + ) { + fail(`body carrier checker self-test ${index} failed`) + } + } + const onlyTest = '#[cfg(test)] mod tests { OutboundResponse::new(); }' + if (/OutboundResponse::new/u.test(productionCode(onlyTest))) + fail('test fixtures must not satisfy production checks') + const unknown = 'ProviderFieldBoundariesUnknown' + const testConstructor = `#[cfg(test)] mod tests { + OutboundHeaderInput::new("cloudflare", headers, OutboundHeaderFidelity::${unknown}); + OutboundHead::new(status, headers, encoded, context, owner); + }` + if (headerNormalizationContractError(testConstructor, unknown) === null) { + fail('adapter test constructors must not satisfy production head checks') + } +} + +runObservationCutoffContractSelfTests() +runHeaderNormalizationContractSelfTests() +runLiveOutboundContractSelfTests() +runRedirectDocumentationContractSelfTests() +if ( + process.argv.includes('--observation-cutoff-self-test') || + process.argv.includes('--redirect-self-test') +) { + process.exit(0) +} + +let capabilitySource +try { + capabilitySource = readFileSync(capabilityPath, 'utf8') +} catch (error) { + fail(`cannot read ${capabilityPath}: ${error.message}`) +} + +const lines = capabilitySource.split(/\r?\n/u) + +function readExactTable(sectionTitle, header, name) { + const headingIndex = lines.findIndex( + (line) => line.trim() === `## ${sectionTitle}` + ) + if (headingIndex === -1) fail(`${name} section is missing`) + const nextHeadingIndex = lines.findIndex( + (line, index) => index > headingIndex && line.startsWith('## ') + ) + const sectionEnd = nextHeadingIndex === -1 ? lines.length : nextHeadingIndex + const headerIndex = lines.findIndex( + (line, index) => + index > headingIndex && + index < sectionEnd && + JSON.stringify(cells(line)) === JSON.stringify(header) + ) + if (headerIndex === -1) fail(`${name} table is missing`) + const separator = cells(lines[headerIndex + 1] ?? '') + if ( + separator === null || + separator.length !== header.length || + !separator.every((value) => /^:?-{3,}:?$/u.test(value)) + ) { + fail(`${name} table has an invalid separator row`) + } + const rows = [] + for (let index = headerIndex + 2; index < sectionEnd; index += 1) { + const row = cells(lines[index]) + if (row === null) break + if (row.length !== header.length) { + fail( + `${name} row ${index + 1} has ${row.length} cells, expected ${header.length}` + ) + } + rows.push(row) + } + return rows +} + +const actualMemoryRows = readExactTable( + 'Platform Memory Ceilings', + expectedMemoryHeader, + 'platform memory' +) +if (JSON.stringify(actualMemoryRows) !== JSON.stringify(expectedMemoryRows)) { + fail( + `platform memory mismatch\nexpected=${JSON.stringify(expectedMemoryRows)}\nactual=${JSON.stringify(actualMemoryRows)}` + ) +} +const normalizedCapabilitySource = capabilitySource.replaceAll(/\s+/gu, ' ') +for (const fragment of [ + 'SDK failure evidence, not stronger transport guarantees', + 'pinned Viceroy can report an internal cause', + 'Framework code does not log provider detail', + 'Local SDK/Node/WASI fixtures are not hosted qualification', +]) { + if (!normalizedCapabilitySource.includes(fragment)) { + fail(`provider diagnostic capability is missing its scope: ${fragment}`) + } +} +for (const requiredFragment of [ + 'six simultaneous outbound connections', + 'it is not an inbound population bound', + 'host parser, HPACK, or framing allocations', + 'only `Fits` is a complete validation result', +]) { + if (!normalizedCapabilitySource.includes(requiredFragment)) { + fail(`platform memory narrative is missing: ${requiredFragment}`) + } +} + +function tableAfterHeading(source, heading, header) { + const sourceLines = source.split(/\r?\n/u) + const headingIndex = sourceLines.findIndex((line) => line.trim() === heading) + if (headingIndex === -1) fail(`missing heading: ${heading}`) + const nextHeadingOffset = sourceLines + .slice(headingIndex + 1) + .findIndex((line) => /^#{1,6}\s/u.test(line.trim())) + const sectionEnd = + nextHeadingOffset === -1 + ? sourceLines.length + : headingIndex + 1 + nextHeadingOffset + const headerIndex = sourceLines.findIndex( + (line, index) => + index > headingIndex && + index < sectionEnd && + JSON.stringify(cells(line)) === JSON.stringify(header) + ) + if (headerIndex === -1) fail(`missing table under ${heading}`) + const separator = cells(sourceLines[headerIndex + 1] ?? '') + if ( + separator === null || + separator.length !== header.length || + !separator.every((cell) => /^:?-{3,}:?$/u.test(cell)) + ) { + fail(`invalid table separator under ${heading}`) + } + const rows = [] + for (let index = headerIndex + 2; index < sectionEnd; index += 1) { + const row = cells(sourceLines[index]) + if (row === null) break + if (row.length !== header.length) { + fail(`invalid table row width under ${heading}`) + } + rows.push(row) + } + return rows +} + +const migrationHeader = ['Removed or changed API', 'Replacement'] +const designMigrationRows = tableAfterHeading( + readFileSync(platformMetadataDesignPath, 'utf8'), + '## Migration record', + migrationHeader +) +const changelogMigrationRows = tableAfterHeading( + readFileSync(changelogPath, 'utf8'), + '### Platform resource metadata', + migrationHeader +) +if ( + designMigrationRows.length !== 8 || + JSON.stringify(changelogMigrationRows) !== JSON.stringify(designMigrationRows) +) { + fail( + 'Unreleased changelog must contain the focused design migration table exactly' + ) +} +if ( + !readFileSync(adapterOverviewPath, 'utf8').includes( + 'CHANGELOG.md#platform-resource-metadata' + ) +) { + fail('adapter overview must link to the Unreleased migration table') +} + +function findMatrixHeader(sectionTitle, name) { + const headingIndex = lines.findIndex( + (line) => line.trim() === `## ${sectionTitle}` + ) + if (headingIndex === -1) { + fail(`${name} capability section is missing`) + } + const nextHeadingIndex = lines.findIndex( + (line, index) => index > headingIndex && line.startsWith('## ') + ) + const sectionEnd = nextHeadingIndex === -1 ? lines.length : nextHeadingIndex + const headerIndex = lines.findIndex( + (line, index) => + index > headingIndex && + index < sectionEnd && + JSON.stringify(cells(line)) === JSON.stringify(expectedHeader) + ) + if (headerIndex === -1) { + fail(`${name} capability matrix is missing`) + } + return headerIndex +} + +function readMatrix(headerIndex, name) { + const separator = cells(lines[headerIndex + 1] ?? '') + if ( + separator === null || + separator.length !== expectedHeader.length || + !separator.every((value) => /^:?-{3,}:?$/u.test(value)) + ) { + fail(`${name} capability matrix is missing its five-column separator row`) + } + + const actualRows = [] + for (let index = headerIndex + 2; index < lines.length; index += 1) { + const row = cells(lines[index]) + if (row === null) break + if (row.length !== expectedHeader.length) { + fail( + `${name} capability matrix row ${index + 1} has ${row.length} cells, expected 5` + ) + } + actualRows.push([ + normalizeCapability(row[0]), + ...row.slice(1).map(normalizeSupport), + ]) + } + return actualRows +} + +const matrices = [ + [ + 'ingress', + expectedIngressRows, + readMatrix(findMatrixHeader('Ingress Matrix', 'ingress'), 'ingress'), + ], + [ + 'config', + expectedConfigRows, + readMatrix(findMatrixHeader('Config Matrix', 'config'), 'config'), + ], + [ + 'response egress', + expectedResponseEgressRows, + readMatrix( + findMatrixHeader('Response Egress Matrix', 'response egress'), + 'response egress' + ), + ], + [ + 'outbound', + expectedOutboundRows, + readMatrix(findMatrixHeader('Outbound Matrix', 'outbound'), 'outbound'), + ], +] +for (const [name, expectedRows, actualRows] of matrices) { + if (JSON.stringify(actualRows) !== JSON.stringify(expectedRows)) { + fail( + `${name} capability matrix mismatch\nexpected=${JSON.stringify(expectedRows)}\nactual=${JSON.stringify(actualRows)}` + ) + } +} + +const hardCutSurfaces = [ + [capabilityPath, capabilitySource], + [handlersGuidePath, readFileSync(handlersGuidePath, 'utf8')], + [proxyGuidePath, readFileSync(proxyGuidePath, 'utf8')], + [fastlyGuidePath, readFileSync(fastlyGuidePath, 'utf8')], + [scaffoldReadmePath, readFileSync(scaffoldReadmePath, 'utf8')], +] +const redirectDocumentationError = redirectDocumentationContractError( + hardCutSurfaces[2][1], + capabilitySource +) +if (redirectDocumentationError !== null) fail(redirectDocumentationError) +for (const [path, source] of hardCutSurfaces) { + for (const staleFragment of [ + 'HttpClient::send_all`', + '.send_all(', + 'send-all-slot-isolation', + 'run_app_with_request_extensions', + ]) { + if (source.includes(staleFragment)) { + fail(`${path} contains removed outbound API text: ${staleFragment}`) + } + } +} +for (const staleFragment of [ + 'harvests response bodies in input order', + 'harvests responses in input order', + 'Fastly divides a total budget among provider phase timers', + 'Exact ceiled-millisecond budgets are part of backend identity', +]) { + if (capabilitySource.includes(staleFragment)) { + fail( + `${capabilityPath} contains stale Fastly backend or batch behavior: ${staleFragment}` + ) + } +} +const handlersGuideSource = hardCutSurfaces[1][1] +for (const requiredFragment of [ + 'fn configure_app(app: &mut edgezero_core::app::App) -> Result<(), EdgeError>', + 'completion: ResponseEgressCompletion::empty()', + 'DetachedResponseEgressDecision::Send {', + 'deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET),', + 'ResponseEgressCompletion::new(move |report| ... )', + 'Use `left.join(right)`', + '`AdmissionDecision::Abort`', + '`App::set_detached_response_egress_decision_factory`', + 'app.set_error_response_renderer(|error|', + '`DetachedResponseEgressDecision::Abort`', + 'Every `Send` deadline is mandatory and absolute', + '`Abort` creates no response or egress attempt and invokes no completion callback', + 'sorted, deduplicated `Allow` field', + 'Effective `config_out_of_date` responses retain `Retry-After: 60`', + 'status returned by the callback is ignored', + 'never belongs in response extensions', +]) { + if (!handlersGuideSource.includes(requiredFragment)) { + fail( + `${handlersGuidePath} is missing lifecycle contract: ${requiredFragment}` + ) + } +} +const cloudflareGuideSource = readFileSync(cloudflareGuidePath, 'utf8') +for (const path of [ + fastlyGuidePath, + cloudflareGuidePath, + 'docs/guide/adapters/spin.md', +]) { + const guide = readFileSync(path, 'utf8') + if ( + guide.includes('`application configuration failed`') || + !guide.includes('request::send_assembly_error') + ) { + fail(`${path} must use canonical typed assembly-error delivery`) + } +} +if ( + !cloudflareGuideSource.includes( + 'EdgeZeroApp::build::(CLOUDFLARE_PLATFORM)' + ) || + cloudflareGuideSource.includes('build_app') +) { + fail( + `${cloudflareGuidePath} must construct manual applications with Cloudflare platform metadata` + ) +} +const axumGuideSource = readFileSync(axumGuidePath, 'utf8') +for (const fragment of [ + 'AxumDevServer::with_config(app, config)', + 'AxumTestServer::start(server)', + 'running.shutdown().await', + 'without initializing its default transport', + 'not a Cloudflare or Spin provider request context', +]) { + if (!axumGuideSource.includes(fragment)) { + fail( + `${axumGuidePath} is missing assembled-app serving contract: ${fragment}` + ) + } +} +if (axumGuideSource.includes('let router = app.router()')) { + fail( + `${axumGuidePath} must test policies through bounded TCP ingress, not router.oneshot` + ) +} +if ( + !axumGuideSource.includes( + 'EdgeZeroApp::build::(edgezero_adapter_axum::AXUM_PLATFORM)' + ) || + axumGuideSource.includes('build_app') +) { + fail( + `${axumGuidePath} must construct manual applications with Axum platform metadata` + ) +} +const routingGuideSource = readFileSync(routingGuidePath, 'utf8') +for (const requiredFragment of [ + 'Hooks::routes() -> Result', + 'App::from_router::(router, PLATFORM)', + 'App::state::() -> Option<&T>', + "router's existing state registry", + 'state = crate::load_state()?', + 'App::assembly_error_egress(error, method, request_start, clock)', + 'request::send_assembly_error', + 'No successful app configuration exists on failure', +]) { + if (!handlersGuideSource.includes(requiredFragment)) { + fail( + `handlers guide is missing application assembly contract: ${requiredFragment}` + ) + } +} +if ( + !routingGuideSource.includes('App::build::(platform)') || + routingGuideSource.includes('build_app') +) { + fail( + `${routingGuidePath} must document the platform-aware application builder` + ) +} +for (const [path, source] of [ + [capabilityPath, capabilitySource], + [handlersGuidePath, handlersGuideSource], +]) { + if (source.includes('build_app_for_platform')) { + fail(`${path} contains the removed application builder name`) + } +} +if ( + !hardCutSurfaces[2][1].includes('start_batch_until') || + !hardCutSurfaces[2][1].includes('send_all_until') +) { + fail('outbound guide must document completion-order and ordered batch access') +} +for (const requiredFragment of [ + 'OutboundBatchNext::Item', + 'OutboundBatchNext::Finished', + 'OutboundBatchTermination::Completed', + 'OutboundBatchTermination::Cutoff', + 'OutboundBatchFailure', + 'send_all_until(requests, observation_cutoff)', +]) { + if (!hardCutSurfaces[2][1].includes(requiredFragment)) { + fail( + `${proxyGuidePath} is missing typed batch contract: ${requiredFragment}` + ) + } +} +if ( + !hardCutSurfaces[2][1].includes('.deadline_after(Duration::from_secs(2))') || + hardCutSurfaces[2][1].includes( + 'let observation_cutoff = Deadline::after(Duration::from_secs(2))' + ) +) { + fail(`${proxyGuidePath} must anchor batch cutoffs to the request clock`) +} +if ( + !hardCutSurfaces[3][1].includes('run_app_with_hooks') || + !hardCutSurfaces[3][1].includes('send_request_with_hooks') +) { + fail('Fastly guide must document the closed request/response lifecycle APIs') +} +for (const requiredFragment of [ + 'FastlyRequestDelivery', + 'FastlyResponseDelivery::report()', + 'may_send_native_response()', + '`NotCommitted`', + '`Unknown`', + '`Committed`', + 'outside EdgeZero accounting', +]) { + if (!hardCutSurfaces[3][1].includes(requiredFragment)) { + fail( + `Fastly guide is missing delivery-report contract: ${requiredFragment}` + ) + } +} +if (/Result,\s*fastly::Error>/.test(hardCutSurfaces[3][1])) { + fail('Fastly guide must not retain the retired custom-send return signature') +} + +let outboundSpecSource +try { + outboundSpecSource = readFileSync(outboundSpecPath, 'utf8') +} catch (error) { + fail(`cannot read ${outboundSpecPath}: ${error.message}`) +} +if ( + !outboundSpecSource.includes(') -> FastlyRequestDelivery') || + outboundSpecSource.includes(') -> Result, fastly::Error>') +) { + fail( + 'outbound specification must use the owned Fastly custom delivery report' + ) +} +if ( + !outboundSpecSource.includes( + '| `outbound-failure-diagnostics` | BestEffort | BestEffort | Native | Native |' + ) +) { + fail( + 'outbound specification is missing the SDK-projection diagnostic matrix row' + ) +} + +if (!outboundSpecSource.includes('## 2. Historical implementation baseline')) { + fail( + 'outbound specification must label section 2 as a historical implementation baseline' + ) +} +if (outboundSpecSource.includes('## 2. Current state (summary)')) { + fail( + 'outbound specification still labels its historical baseline as current state' + ) +} +if (outboundSpecSource.includes("Today's Fastly client")) { + fail( + 'outbound specification still describes the historical Fastly client as current' + ) +} +for (const staleFragment of [ + 'explicit low-level constructors use a documented default clock', + 'explicit default-clock constructors may select the', + 'Preserve default-clock constructors for low-level use', + 'low-level defaults', +]) { + if (outboundSpecSource.includes(staleFragment)) { + fail( + `outbound specification contains removed clock behavior: ${staleFragment}` + ) + } +} +const budgetSourceDefinitions = outboundSpecSource.match( + /pub enum BudgetSource\s*\{/gu +) +if (budgetSourceDefinitions?.length !== 1) { + fail('outbound specification must define BudgetSource exactly once') +} +for (const staleFragment of [ + 'So the collapse today is', + 'The current adapter maps', + 'two real bugs to fix', + 'The current code (`proxy.rs', + 'crates/edgezero-adapter-spin/src/proxy.rs', + '(`spin/proxy.rs`)', + 'harvests every successfully dispatched `PendingRequest` via blocking `wait()`/`poll()`', + 'cold registration, serial harvest, and streamed-upload cooperative checks', + 'The target-neutral batch probe exercises both still-pending and later-ready harvest branches', + 'send_all_preflight_precedence_and_indices', + 'send_all_dispatches_every_slot_before_wait', + 'ordered harvest', + 'harvest-order', + 'serial harvest', + 'send_one_validated', + 'exact eight outbound', + 'all eight outbound', + 'Fastly send_all_until adapter overhead', + 'pub async fn next(&mut self) -> Option', + 'pub async fn collect(self) -> OutboundBatchResults', + 'OutboundBatch::from_stream', + 'Result', + 'let Ok((selection, metadata)) = select_pending_slot(&mut pending) else', + 'private failure event', + 'four of the eight outbound capabilities', + 'the eight outbound capabilities', + 'all eight rows/support values', + 'allocation-tracking adversarial', +]) { + if (outboundSpecSource.includes(staleFragment)) { + fail( + `outbound specification contains stale implementation text: ${staleFragment}` + ) + } +} +for (const requiredFragment of [ + '`BudgetSource::BatchCutoff` attribution alone never emits `OutboundBatchDriverEvent::Cutoff`', + 'monotonic clock shows that the absolute observation cutoff has expired; equality is expired', + "An earlier Fastly phase timeout remains that slot's terminal `GatewayTimeout` item", + 'pub enum OutboundBatchDriverEvent', + 'pub enum OutboundBatchTermination', + 'pub enum OutboundBatchNext', + 'pub struct OutboundBatchFailure', + // Historical baseline signatures remain checked here, never as live production APIs. + 'pub fn cutoff(slot_count: usize) -> Self', + 'pub fn finish_batch_item(', + 'pub fn from_driver', + 'Result', + 'premature driver EOF', + 'duplicate or out-of-range', + 'Core reports normal completion only at driver EOF', +]) { + if (!outboundSpecSource.includes(requiredFragment)) { + fail( + `outbound specification is missing typed batch contract: ${requiredFragment}` + ) + } +} +if ( + !/terminal samples follow\s+poll-observation order in the shared monotonic clock domain/u.test( + outboundSpecSource + ) +) { + fail( + 'outbound specification is missing the monotonic terminal-observation ordering proof' + ) +} + +const cloudflareExactDeadlineRecommendation = + /\btarget(?:s|ing)?\s+(?:Axum\s+(?:or|and)\s+Cloudflare|Cloudflare\s+(?:or|and)\s+Axum)\b/u +if (cloudflareExactDeadlineRecommendation.test(outboundSpecSource)) { + fail( + 'outbound specification recommends Cloudflare for exact deadlines despite its BestEffort capability' + ) +} + +const inboundSpecSource = readFileSync(inboundSpecPath, 'utf8') +for (const requiredFragment of [ + ' Abort,', + 'pub enum DetachedResponseEgressDecision', + ' Send {', + ' completion: ResponseEgressCompletion,', + ' deadline: Deadline,', + 'DetachedResponseEgressDecision::Send {', + 'deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET),', + 'The deadline is mandatory, absolute', + 'Joined completions run left then right with the same borrowed report at most once', + 'Factory `Abort` creates no response or attempt', + 'make no transport-observed reset claim', + 'ResponseEgressCompletion::empty()', + 'Hooks::configure(&mut App) -> Result<(), EdgeError>', + '`App::set_error_response_renderer(Fn(EdgeError) -> Response)`', + '`Vec` rather than flattening protocol data into display text', + 'custom renderer cannot reinterpret the error status', + 'the canonical `Allow` field', + '`Retry-After: 60` for every effective `config_out_of_date` outcome after custom rendering', +]) { + if (!inboundSpecSource.includes(requiredFragment)) { + fail( + `${inboundSpecPath} is missing lifecycle contract: ${requiredFragment}` + ) + } +} +for (const staleFragment of [ + 'current Tower adapter nevertheless drives', + 'block_in_place` plus a nested runtime `block_on', + 'MethodNotAllowed { allowed: String', + 'Tokio cannot cancel that blocking closure', +]) { + if (inboundSpecSource.includes(staleFragment)) { + fail( + `inbound specification contains removed Axum bridge text: ${staleFragment}` + ) + } +} + +const outboundReviewHardeningPlanSource = readFileSync( + outboundReviewHardeningPlanPath, + 'utf8' +) +for (const staleFragment of [ + 'Preserve default-clock constructors for low-level use', + 'low-level defaults', +]) { + if (outboundReviewHardeningPlanSource.includes(staleFragment)) { + fail(`${outboundReviewHardeningPlanPath} contains removed clock behavior`) + } +} + +const currentDocumentation = [ + [ + outboundImplementationIndexPath, + readFileSync(outboundImplementationIndexPath, 'utf8'), + ], + [ + outboundBatchTerminationPlanPath, + readFileSync(outboundBatchTerminationPlanPath, 'utf8'), + ], + [cloudflarePhasePath, readFileSync(cloudflarePhasePath, 'utf8')], + [spinPhasePath, readFileSync(spinPhasePath, 'utf8')], + [fastlyPhasePath, readFileSync(fastlyPhasePath, 'utf8')], + [migrationPhasePath, readFileSync(migrationPhasePath, 'utf8')], + [outboundSpecPath, outboundSpecSource], +] +for (const [path, source] of currentDocumentation) { + for (const staleVersion of [ + 'Spin SDK 6.0.0', + 'spin-sdk v6.0.0', + 'Viceroy 0.17.0', + 'viceroy 0.17.0', + 'Fastly SDK 0.12.1', + 'Fastly 0.12.1', + 'fastly v0.12.1', + 'fastly = "=0.12.1"', + 'fastly --precise 0.12.1', + 'Worker 0.8.3', + 'worker = "=0.8.3"', + 'worker --precise 0.8.3', + 'worker v0.8.3', + ]) { + if (source.includes(staleVersion)) { + fail(`${path} contains stale runtime tooling: ${staleVersion}`) + } + } +} + +const outboundImplementationIndexSource = currentDocumentation[0][1] +if (!outboundImplementationIndexSource.includes('Spin SDK 7 / WASI HTTP 0.3')) { + fail( + 'outbound implementation index must name the current Spin SDK 7 baseline' + ) +} +if ( + outboundImplementationIndexSource.includes('| Executable:') || + outboundImplementationIndexSource.includes('Tasks 1-6 blocked') || + outboundImplementationIndexSource.includes('all downstream phases stay') || + outboundImplementationIndexSource.includes( + 'exactly seven **outbound** capabilities' + ) || + outboundImplementationIndexSource.includes( + 'exactly twelve **outbound** capabilities' + ) || + outboundImplementationIndexSource.includes( + 'block_in_place` + `Handle::block_on' + ) +) { + fail('outbound implementation index contains stale implementation state') +} + +let brotliAuditSource +try { + brotliAuditSource = readFileSync(brotliAuditPath, 'utf8') +} catch (error) { + fail(`cannot read ${brotliAuditPath}: ${error.message}`) +} +const normalizedBrotliAuditSource = brotliAuditSource.replace(/\s+/gu, ' ') +const expectedBrotliDependencies = JSON.parse( + readFileSync('scripts/brotli_dependency_contract.json', 'utf8') +) +const auditedBrotliDependencies = new Map( + [ + ...brotliAuditSource.matchAll( + /^\| `([^`]+)`\s+\| `([^`]+)`\s+\| `([0-9a-f]{64})` \|$/gmu + ), + ].map((match) => [match[1], [match[2], match[3]]]) +) +if (auditedBrotliDependencies.size !== expectedBrotliDependencies.length) { + fail(`${brotliAuditPath} must contain exactly the pinned dependency graph`) +} +for (const [name, version, checksum] of expectedBrotliDependencies) { + const audited = auditedBrotliDependencies.get(name) + if (audited?.[0] !== version || audited?.[1] !== checksum) { + fail( + `${brotliAuditPath} has stale version or checksum evidence for ${name}` + ) + } +} +for (const requiredFragment of [ + '`brotli-decompressor`', + '`5.0.1`', + '`compression-codecs`', + '`0.4.38`', + '`alloc-stdlib`', + '`0.2.4`', + '3 * 256 * 1080 * 4', + '3,442,530', + '16,777,216', + 'Source-audited payload subtotal', + "The ring buffer's `2^WBITS` bytes are excluded from the fixed term", +]) { + if (!normalizedBrotliAuditSource.includes(requiredFragment)) { + fail(`${brotliAuditPath} is missing audited evidence: ${requiredFragment}`) + } +} + +const outboundBatchTerminationPlanSource = currentDocumentation[1][1] +if (outboundBatchTerminationPlanSource.includes('send_all_until(...).await?')) { + fail( + `${outboundBatchTerminationPlanPath} contains the stale batch collector error conversion` + ) +} +if ( + outboundBatchTerminationPlanSource.includes('doc-hidden adapter driver event') +) { + fail( + `${outboundBatchTerminationPlanPath} still describes the public adapter driver protocol as doc-hidden` + ) +} + +const cloudflarePhaseSource = currentDocumentation[2][1] +for (const staleFragment of [ + "Cloudflare's Native timing claim", + 'Cloudflare: Native for HTTP, deadlines', + '| `outbound-deadlines` | Native | Native |', + '| `streamed-upload-deadlines` | Native | Native |', +]) { + if (cloudflarePhaseSource.includes(staleFragment)) { + fail( + `${cloudflarePhasePath} contains stale Cloudflare capability text: ${staleFragment}` + ) + } +} + +for (const [path, source] of [ + [cloudflarePhasePath, currentDocumentation[2][1]], + [spinPhasePath, currentDocumentation[3][1]], + [fastlyPhasePath, currentDocumentation[4][1]], +]) { + if (!source.includes('**Superseded batch/lifecycle API:**')) { + fail(`${path} does not mark its removed batch API as superseded`) + } +} + +const migrationPhaseSource = currentDocumentation[5][1] +for (const currentRow of [ + '| `outbound-deadlines` | Native | BestEffort | BestEffort | BestEffort |', + '| `streamed-upload-deadlines` | Native | BestEffort | BestEffort | BestEffort |', + '| `lazy-streamed-response-passthrough` | Native | Native | BestEffort | BestEffort |', +]) { + if (!migrationPhaseSource.includes(currentRow)) { + fail( + `${migrationPhasePath} is missing current capability row: ${currentRow}` + ) + } +} + +for (const dependency of [ + 'async-compression 0.4.43', + 'adler2 = "=2.0.1"', + 'brotli 8.0.4', + 'brotli-decompressor 5.0.1', + 'compression-codecs 0.4.38', + 'compression-core 0.4.32', + 'crc32fast = "=1.5.0"', + 'flate2 = "=1.1.9"', + 'miniz_oxide = "=0.8.9"', + 'simd-adler32 = "=0.3.9"', +]) { + if (!outboundSpecSource.includes(`\`${dependency}\``)) { + fail( + `outbound specification is missing audited dependency pin ${dependency}` + ) + } +} +if ( + !outboundSpecSource.includes( + "plus EdgeZero's synthetic `x-edgezero-proxy` marker" + ) +) { + fail( + 'outbound specification does not include the proxy header in response caps' + ) +} + +const responseEgressSpecSource = readFileSync(responseEgressSpecPath, 'utf8') +for (const requiredFragment of [ + 'pub struct ResponseEgressDeadline { /* private */ }', + 'pub const fn at(deadline: Deadline) -> Self;', + 'pub const fn after(duration: Duration) -> Self;', + 'egress-start-relative deadline', + 'resolves `after(duration)` from that', + 'exact sample before invoking the callback', + 'retired ambiguous `new()` constructor and unresolved', + 'pub struct ResponseEgressCompletion', + 'pub enum DetachedResponseEgressDecision', + ' Send {', + ' completion: ResponseEgressCompletion,', + ' deadline: Deadline,', + 'DetachedResponseEgressDecision::Send {', + 'deadline: head.write_deadline_after(DEFAULT_RESPONSE_WRITE_BUDGET),', + 'mandatory absolute deadline', + 'pub fn join(self, other: Self) -> Self;', + '`ResponseEgressCompletion::join` consumes a left and right completion', + 'the same borrowed `ResponseEgressReport`; the joined owner executes at most once', + 'existing non-response abort/error boundary', + 'Normalized pre-admission validation invokes the detached-egress decision factory exactly once', + '`Abort` constructs no response or attempt and invokes no completion, observer, handler, middleware, or body poll', + 'ResponseEgressCompletion::empty()', + 'not attached to a `Response` and never enters `http::Extensions`', + 'ResponseEgressHead::application_deadline()', + 'queue-through-egress', + 'application configuration changed adapter-selected platform metadata', + 'validates source EOF before handing that frame to Hyper', + 'A transmitted zero-length stream is validated asynchronously before Hyper receives the response', +]) { + if (!responseEgressSpecSource.includes(requiredFragment)) { + fail( + `${responseEgressSpecPath} is missing application deadline contract: ${requiredFragment}` + ) + } +} + +if ( + responseEgressSpecSource.includes( + 'Normalized pre-admission validation obtains exactly one completion' + ) +) { + fail( + 'response-egress evidence still claims every normalized rejection has a completion' + ) +} + +for (const requiredFragment of [ + 'detached-egress decision factory', + 'this does not promote their `BestEffort` capability cells', +]) { + if (!capabilitySource.includes(requiredFragment)) { + fail( + `${capabilityPath} is missing detached-ingress abort caveat: ${requiredFragment}` + ) + } +} + +const responseEgressHeadingIndex = lines.findIndex( + (line) => line.trim() === '## Response Egress Matrix' +) +if (responseEgressHeadingIndex === -1) { + fail(`${capabilityPath} is missing the response-egress section`) +} +const nextCapabilityHeadingIndex = lines.findIndex( + (line, index) => index > responseEgressHeadingIndex && line.startsWith('## ') +) +const responseEgressSectionEnd = + nextCapabilityHeadingIndex === -1 ? lines.length : nextCapabilityHeadingIndex +const responseEgressSection = lines + .slice(responseEgressHeadingIndex, responseEgressSectionEnd) + .join('\n') +for (const requiredFragment of [ + 'The boundaries remain `BestEffort` because none proves end-client receipt:', + 'Provider-owned buffering, SDK allocations, and bytes beyond each stated acceptance boundary are', +]) { + if (!responseEgressSection.includes(requiredFragment)) { + fail( + `${capabilityPath} response-egress section is missing caveat: ${requiredFragment}` + ) + } +} + +function responseEgressProviderBullet(provider) { + const bulletIndex = lines.findIndex( + (line, index) => + index > responseEgressHeadingIndex && + index < responseEgressSectionEnd && + line.startsWith(`- ${provider} `) + ) + if (bulletIndex === -1) { + fail(`${capabilityPath} is missing the ${provider} response-egress caveat`) + } + const bullet = [lines[bulletIndex]] + for ( + let index = bulletIndex + 1; + index < responseEgressSectionEnd; + index += 1 + ) { + if (!lines[index].startsWith(' ')) break + bullet.push(lines[index].trim()) + } + return bullet.join(' ') +} + +const responseEgressProviderCaveats = new Map([ + [ + 'Axum', + [ + 'commits when Hyper accepts the response head', + 'closes the owned HTTP/1 connection on deadline', + 'reports clean source EOF as `HostHandoff`', + ], + ], + [ + 'Cloudflare', + [ + 'awaits JavaScript writer promises', + 'races request abort/deadline signals', + 'coordinator alive with `waitUntil`', + 'deployed disconnect and completion timing remain unproved', + ], + ], + [ + 'Fastly', + [ + 'hands buffered heads and complete bodies to the host together', + 'account synchronous body-handle writes through `stream_to_client` close', + 'cannot preempt a blocked source poll or hostcall', + 'loses the body handle if `finish` itself fails', + ], + ], + [ + 'Spin', + [ + 'owns the WASI response/body/result writers through handoff', + 'races pending operations against its timer', + 'host teardown and network completion remain provider-observable only', + ], + ], +]) +for (const [provider, requiredFragments] of responseEgressProviderCaveats) { + const bullet = responseEgressProviderBullet(provider) + for (const requiredFragment of requiredFragments) { + if (!bullet.includes(requiredFragment)) { + fail( + `${capabilityPath} ${provider} response-egress caveat is missing: ${requiredFragment}` + ) + } + } +} + +const limitsHeadingIndex = lines.findIndex( + (line) => line.trim() === '## Limits And Accounting' +) +if (limitsHeadingIndex === -1) { + fail('limits and accounting section is missing') +} +const limitsSectionEnd = lines.findIndex( + (line, index) => index > limitsHeadingIndex && line.startsWith('## ') +) +const limitsHeaderIndex = lines.findIndex( + (line, index) => + index > limitsHeadingIndex && + (limitsSectionEnd === -1 || index < limitsSectionEnd) && + JSON.stringify(cells(line)) === JSON.stringify(expectedLimitHeader) +) +if (limitsHeaderIndex === -1) { + fail('outbound limits table is missing') +} +const limitsSeparator = cells(lines[limitsHeaderIndex + 1] ?? '') +if ( + limitsSeparator === null || + limitsSeparator.length !== expectedLimitHeader.length || + !limitsSeparator.every((value) => /^:?-{3,}:?$/u.test(value)) +) { + fail('outbound limits table is missing its three-column separator row') +} +const actualLimitRows = [] +for (let index = limitsHeaderIndex + 2; index < lines.length; index += 1) { + const row = cells(lines[index]) + if (row === null) break + if (row.length !== expectedLimitHeader.length) { + fail(`outbound limits row ${index + 1} has ${row.length} cells, expected 3`) + } + actualLimitRows.push([normalizeCapability(row[0]), row[1], row[2]]) +} +if (JSON.stringify(actualLimitRows) !== JSON.stringify(expectedLimitRows)) { + fail( + `outbound limits mismatch\nexpected=${JSON.stringify(expectedLimitRows)}\nactual=${JSON.stringify(actualLimitRows)}` + ) +} +for (const requiredFragment of [ + 'including fields later stripped', + "charge EdgeZero's synthetic proxy marker", +]) { + if (!capabilitySource.includes(requiredFragment)) { + fail( + `${capabilityPath} is missing the response-header accounting rule: ${requiredFragment}` + ) + } +} + +let outboundCoreSource +try { + outboundCoreSource = [...outboundCoreProductionPaths, outboundCoreTestPath] + .map((path) => readFileSync(path, 'utf8')) + .join('\n') +} catch (error) { + fail(`cannot read ${outboundCorePath}: ${error.message}`) +} +let compressionCoreSource +try { + compressionCoreSource = readFileSync(compressionCorePath, 'utf8') +} catch (error) { + fail(`cannot read ${compressionCorePath}: ${error.message}`) +} +for (const requiredFragment of [ + 'Deflate,', + 'Passthrough(PassthroughReason)', + 'pub enum PassthroughReason', + 'PassthroughReason::Stacked', + 'PassthroughReason::Malformed', + 'PassthroughReason::Unsupported(token)', + 'pub fn decode_deflate_stream', +]) { + if (!compressionCoreSource.includes(requiredFragment)) { + fail( + `${compressionCorePath} is missing the content-encoding contract: ${requiredFragment}` + ) + } +} +for (const requiredFragment of [ + 'single `deflate`', + 'Passthrough `Unsupported(token)`', + 'Passthrough `Stacked`', + 'Passthrough `Malformed`', + 'decode_deflate_stream', +]) { + if (!outboundSpecSource.includes(requiredFragment)) { + fail( + `${outboundSpecPath} is missing the content-encoding documentation: ${requiredFragment}` + ) + } +} +const coreProduction = outboundCoreProductionPaths + .map((path) => productionCode(readFileSync(path, 'utf8'))) + .join('\n') +const batchProduction = productionCode( + readFileSync('crates/edgezero-core/src/outbound/batch.rs', 'utf8') +) +const runtimeProduction = productionCode( + readFileSync('crates/edgezero-core/src/outbound/scope.rs', 'utf8') +) +const bodyProduction = productionCode( + readFileSync('crates/edgezero-core/src/body.rs', 'utf8') +) +const liveError = liveOutboundContractError( + coreProduction, + batchProduction, + runtimeProduction, + bodyProduction +) +if (liveError !== null) fail(liveError) +observationCutoffContract(coreProduction) +for (const fragment of [ + 'pub enum OutboundProxyMarker', + 'pub fn proxy_marker(', + 'pub fn proxy_marker_setting(', + 'pub fn effective_proxy_marker_enabled(', + 'pub fn proxy_marker_enabled(', + 'pub fn with_proxy_marker(', + 'proxy_marker_off_preserves_upstream_and_consumes_no_header_budget', +]) { + if (!outboundCoreSource.includes(fragment)) { + fail( + `${outboundCorePath} is missing the proxy-marker contract: ${fragment}` + ) + } +} +for (const fragment of [ + 'pub enum ResponseDecoding', + 'pub fn response_decoding(', + 'pub fn response_decoding_setting(', + 'original_coding_skips_decoder_but_keeps_transport_caps', + 'original_coding_preserves_normalization_precedence', +]) { + if (!outboundCoreSource.includes(fragment)) { + fail( + `${outboundCorePath} is missing the original-coding contract: ${fragment}` + ) + } +} +for (const path of [proxyGuidePath, capabilityPath]) { + const source = readFileSync(path, 'utf8') + for (const fragment of [ + 'ResponseDecoding::Original', + 'ResponseDecoding::Decode', + '206', + ]) { + if (!source.includes(fragment)) { + fail(`${path} is missing the original-coding documentation: ${fragment}`) + } + } +} +for (const path of [demoHandlersPath, templateHandlersPath]) { + if (!readFileSync(path, 'utf8').includes('.with_proxy_marker(false)')) { + fail(`${path} must configure its relay handle with the marker off`) + } + if ( + !readFileSync(path, 'utf8').includes( + '.response_decoding(ResponseDecoding::Original)' + ) + ) { + fail(`${path} must select original coding for its relay request`) + } +} +const retiredClockConstructor = ['new_with_', 'monotonic_clock'].join('') +if (outboundCoreSource.includes(retiredClockConstructor)) { + fail('OutboundResponse must not retain the hidden clock-paired constructor') +} +const outboundResponseConstructor = + /impl OutboundResponse \{[\s\S]*?pub fn new\(\s*request_method: Method,\s*status: StatusCode,\s*headers: HeaderMap,\s*body: Body,\s*monotonic_clock: MonotonicClock,\s*content_type_provenance: ContentTypeProvenance,\s*\) -> Self \{/u +if (!outboundResponseConstructor.test(coreProduction)) { + fail( + 'OutboundResponse::new must require the application clock and original Content-Type provenance' + ) +} +const outboundHeadSource = readFileSync( + 'crates/edgezero-core/src/outbound/head.rs', + 'utf8' +) +const headProduction = productionCode(outboundHeadSource) +if ( + (coreProduction.match(/OutboundResponse::new\s*\(/gu)?.length ?? 0) !== 1 || + (headProduction.match(/OutboundResponse::new\s*\(/gu)?.length ?? 0) !== 1 || + !/dispatch_head\s*\([^)]*\)\s*\.await\s*\?\s*\.finish\s*\(/u.test( + coreProduction + ) +) { + fail( + 'production must construct responses through the single shared head finalizer' + ) +} +if ( + !/if fidelity == OutboundHeaderFidelity::ProviderFieldBoundariesUnknown\s*\{\s*normalized\.content_type_provenance = ContentTypeProvenance::Unknown;/u.test( + outboundHeadSource + ) +) { + fail( + 'shared header normalization must preserve unknown provider Content-Type fidelity' + ) +} +for (const adapterPath of outboundAdapterPaths) { + const adapterSource = readFileSync(adapterPath, 'utf8') + const adapterCode = productionCode(adapterSource) + const pairedConstructor = adapterPath.includes('axum') + ? /pub fn try_with_time\(time: OutboundTime\)/u + : /pub fn with_time\(time: OutboundTime\)/u + if ( + !pairedConstructor.test(adapterCode) || + /\bfn\s+(?:try_)?with_clock\s*\(/u.test(adapterCode) || + !adapterCode.includes('time.wait_until(') + ) { + fail( + `${adapterPath} must hard-migrate to paired time and use its deadline wakeup` + ) + } + const fidelity = adapterPath.includes('cloudflare') + ? 'ProviderFieldBoundariesUnknown' + : 'AdapterVisibleFields' + const normalizationError = headerNormalizationContractError( + adapterSource, + fidelity + ) + if (normalizationError !== null) fail(`${adapterPath} ${normalizationError}`) + if (!adapterSource.includes('converted_response_retains_injected_clock')) { + fail(`${adapterPath} must behaviorally test the returned response clock`) + } +} +const documentedDefaults = new Map( + actualLimitRows.map((row) => [row[0], row[2]]) +) +for (const [control, constant] of [ + ['max_request_body_bytes', 'DEFAULT_OUTBOUND_REQUEST_BODY_BYTES'], + ['max_response_bytes', 'DEFAULT_MAX_RESPONSE_BYTES'], + ['max_decoder_bytes', 'DEFAULT_MAX_DECODER_BYTES'], +]) { + const rustDefault = formatBinaryBytes( + readRustU64Constant(outboundCoreSource, constant) + ) + const documentedDefault = documentedDefaults.get(control) + if (documentedDefault !== rustDefault) { + fail( + `${control} default mismatch: Rust ${constant} is ${rustDefault}, documentation says ${documentedDefault}` + ) + } +} + +const sidebarSource = readFileSync(sidebarPath, 'utf8') +const sidebarLinks = + sidebarSource.match(/link:\s*['"]\/guide\/capabilities['"]/gu) ?? [] +if (sidebarLinks.length !== 1) { + fail( + `expected exactly one /guide/capabilities sidebar link, found ${sidebarLinks.length}` + ) +} diff --git a/scripts/check_outbound_legacy_api.sh b/scripts/check_outbound_legacy_api.sh new file mode 100755 index 000000000..2dd7804c5 --- /dev/null +++ b/scripts/check_outbound_legacy_api.sh @@ -0,0 +1,1233 @@ +#!/usr/bin/env bash +set -uo pipefail + +scan() { + local pattern=$1 + shift + + if matches="$(git grep -nE "$pattern" -- "$@")"; then + printf '%s\n' "$matches" + return 1 + fi + + local status=$? + if [ "$status" -eq 1 ]; then + return 0 + fi + return "$status" +} + +scan_detached_send_tuple() { + node --input-type=module - "$@" <<'NODE' +import { execFileSync } from 'node:child_process' +import { readFileSync } from 'node:fs' + +const send = ['DetachedResponseEgressDecision::', 'Send'].join('') +const [decision, sendVariant] = send.split('::') + +function skipRustTrivia(source, start) { + let index = start + + while (index < source.length) { + if (/\s/u.test(source[index])) { + index += 1 + continue + } + if (source.startsWith('//', index)) { + const newline = source.indexOf('\n', index + 2) + index = newline === -1 ? source.length : newline + 1 + continue + } + if (source.startsWith('/*', index)) { + let depth = 1 + index += 2 + while (index < source.length && depth > 0) { + if (source.startsWith('/*', index)) { + depth += 1 + index += 2 + } else if (source.startsWith('*/', index)) { + depth -= 1 + index += 2 + } else { + index += 1 + } + } + if (depth > 0) { + return null + } + continue + } + break + } + + return index +} + +function findLegacyDetachedSendConstructors(source) { + const matches = [] + let searchFrom = 0 + + while (searchFrom < source.length) { + const start = source.indexOf(decision, searchFrom) + if (start === -1) { + break + } + searchFrom = start + decision.length + + if (start > 0 && /[A-Za-z0-9_]/u.test(source[start - 1])) { + continue + } + + let index = skipRustTrivia(source, searchFrom) + if (index === null || !source.startsWith('::', index)) { + continue + } + index = skipRustTrivia(source, index + 2) + if (index === null || !source.startsWith(sendVariant, index)) { + continue + } + index = skipRustTrivia(source, index + sendVariant.length) + if (index === null || source[index] !== '(') { + continue + } + + matches.push({ + index: start, + text: source.slice(start, index + 1), + }) + } + + return matches +} + +let selfTestFailed = false +for (const [name, source] of [ + ['same-line tuple', `${send}(completion)`], + ['newline tuple', `${send}\n(completion)`], + ['line-comment tuple', `${send} // legacy\n(completion)`], + ['block-comment tuple', `${send} /* legacy */ (completion)`], + [ + 'comment before variant', + `${decision}:: /* legacy */ ${sendVariant}(completion)`, + ], + ['newline before path separator', `${decision}\n::${sendVariant}(completion)`], + [ + 'trivia around path separator', + `${decision} /* before */ :: /* after */ ${sendVariant}(completion)`, + ], + [ + 'nested block-comment trivia', + `${decision} /* outer /* nested */ outer */ :: ` + + `/* outer /* nested */ outer */ ${sendVariant} ` + + '/* outer /* nested */ outer */ (completion)', + ], +]) { + if (findLegacyDetachedSendConstructors(source).length !== 1) { + process.stderr.write(`legacy tuple checker self-test failed: ${name}\n`) + selfTestFailed = true + } +} +for (const source of [ + `${send} { completion, deadline }`, + ['"DetachedResponseEgressDecision::",', '"Send("'].join('\n'), +]) { + if (findLegacyDetachedSendConstructors(source).length !== 0) { + process.stderr.write('legacy tuple checker self-test matched a supported form\n') + selfTestFailed = true + } +} +if (selfTestFailed) { + process.exit(2) +} + +const roots = process.argv.slice(2) +const docsIndexFixture = { + path: 'docs/index.md', + source: `${send} /* legacy */ (completion)`, +} +if ( + !roots.includes(docsIndexFixture.path) || + findLegacyDetachedSendConstructors(docsIndexFixture.source).length !== 1 +) { + process.stderr.write('legacy tuple checker did not reject the docs index fixture\n') + process.exit(2) +} +const tracked = execFileSync('git', ['ls-files', '-z', '--', ...roots]) + .toString('utf8') + .split('\0') + .filter(Boolean) +let failed = false +for (const path of tracked) { + const source = readFileSync(path, 'utf8') + for (const match of findLegacyDetachedSendConstructors(source)) { + const line = source.slice(0, match.index).split(/\r?\n/u).length + process.stdout.write(`${path}:${line}:${match.text.replace(/\s+/gu, ' ')}\n`) + failed = true + } +} +process.exit(failed ? 1 : 0) +NODE +} + +scan_platform_resource_legacy() { + node --input-type=module - "$@" <<'NODE' +import { execFileSync } from 'node:child_process' +import { readFileSync } from 'node:fs' + +const memory = ['Memory', 'Ceiling'].join('') +const metadata = ['Platform', 'Metadata'].join('') +const retiredSource = [memory, 'Source'].join('') +const retiredAdapterMethod = ['Adapter::', 'memory_', 'ceiling'].join('') +const retiredClockConstructor = ['new_with_', 'monotonic_clock'].join('') +const retiredByValueAccessor = + `pub const fn memory_${'ceiling'}(self) -> Option<${memory}>` +const allowlist = new Set([ + 'CHANGELOG.md', + 'docs/superpowers/specs/2026-09-28-platform-resource-metadata-design.md', +]) +const authoritativePlan = + 'docs/superpowers/plans/2026-09-28-platform-resource-metadata.md' + +function maskRange(masked, source, start, end) { + for (let index = start; index < end; index += 1) { + if (source[index] !== '\n' && source[index] !== '\r') masked[index] = ' ' + } +} + +function rawStringEnd(source, start) { + let rawPrefix = start + if ( + (source[start] === 'b' || source[start] === 'c') && + source[start + 1] === 'r' + ) { + rawPrefix += 1 + } + if (source[rawPrefix] !== 'r') return null + if (start > 0 && /[A-Za-z0-9_]/u.test(source[start - 1])) return null + + let quote = rawPrefix + 1 + while (source[quote] === '#') quote += 1 + if (source[quote] !== '"') return null + const hashes = source.slice(rawPrefix + 1, quote) + const closing = `"${hashes}` + const closingStart = source.indexOf(closing, quote + 1) + return closingStart === -1 ? source.length : closingStart + closing.length +} + +function characterLiteralEnd(source, start) { + if (source[start] !== "'") return null + let index = start + 1 + if (source[index] === '\\') { + index += 1 + if (source[index] === 'u' && source[index + 1] === '{') { + const brace = source.indexOf('}', index + 2) + if (brace === -1) return null + index = brace + 1 + } else { + index += 1 + } + } else { + const codePoint = source.codePointAt(index) + if (codePoint === undefined || source[index] === '\n' || source[index] === '\r') { + return null + } + index += codePoint > 0xffff ? 2 : 1 + } + return source[index] === "'" ? index + 1 : null +} + +function maskRustCommentsAndLiterals(source) { + const masked = source.split('') + let index = 0 + while (index < source.length) { + if (source.startsWith('//', index)) { + const newline = source.indexOf('\n', index + 2) + const end = newline === -1 ? source.length : newline + maskRange(masked, source, index, end) + index = end + continue + } + if (source.startsWith('/*', index)) { + let depth = 1 + let end = index + 2 + while (end < source.length && depth > 0) { + if (source.startsWith('/*', end)) { + depth += 1 + end += 2 + } else if (source.startsWith('*/', end)) { + depth -= 1 + end += 2 + } else { + end += 1 + } + } + maskRange(masked, source, index, end) + index = end + continue + } + const rawEnd = rawStringEnd(source, index) + if (rawEnd !== null) { + maskRange(masked, source, index, rawEnd) + index = rawEnd + continue + } + if (source[index] === '"') { + let end = index + 1 + while (end < source.length) { + if (source[end] === '\\') { + end += 2 + } else if (source[end] === '"') { + end += 1 + break + } else { + end += 1 + } + } + maskRange(masked, source, index, end) + index = end + continue + } + const characterEnd = characterLiteralEnd(source, index) + if (characterEnd !== null) { + maskRange(masked, source, index, characterEnd) + index = characterEnd + continue + } + index += 1 + } + return masked.join('') +} + +function closingDelimiter(source, open) { + const pairs = new Map([ + ['(', ')'], + ['[', ']'], + ['{', '}'], + ]) + const stack = [pairs.get(source[open])] + for (let index = open + 1; index < source.length; index += 1) { + if (source.startsWith('//', index)) { + const newline = source.indexOf('\n', index + 2) + index = newline === -1 ? source.length : newline + } else if (source.startsWith('/*', index)) { + let depth = 1 + index += 2 + while (index < source.length && depth > 0) { + if (source.startsWith('/*', index)) { + depth += 1 + index += 2 + } else if (source.startsWith('*/', index)) { + depth -= 1 + index += 2 + } else { + index += 1 + } + } + index -= 1 + } else { + const rawEnd = rawStringEnd(source, index) + if (rawEnd !== null) { + index = rawEnd - 1 + continue + } + if (source[index] === '"') { + index += 1 + while (index < source.length && source[index] !== '"') { + if (source[index] === '\\') index += 1 + index += 1 + } + continue + } + const characterEnd = characterLiteralEnd(source, index) + if (characterEnd !== null) { + index = characterEnd - 1 + continue + } + if (pairs.has(source[index])) { + stack.push(pairs.get(source[index])) + } else if (source[index] === stack.at(-1)) { + stack.pop() + if (stack.length === 0) return index + } + } + } + return -1 +} + +function closingGenericList(source, open) { + let angleDepth = 1 + const delimiters = [] + const pairs = new Map([ + ['(', ')'], + ['[', ']'], + ['{', '}'], + ]) + for (let index = open + 1; index < source.length; index += 1) { + if (pairs.has(source[index])) { + delimiters.push(pairs.get(source[index])) + continue + } + if (source[index] === delimiters.at(-1)) { + delimiters.pop() + continue + } + if (delimiters.length !== 0) continue + if (source[index] === '<') angleDepth += 1 + else if (source[index] === '>' && source[index - 1] !== '-') { + angleDepth -= 1 + if (angleDepth === 0) return index + } + } + return -1 +} + +function previousNonWhitespace(source, start, count) { + const characters = [] + for (let index = start - 1; index >= 0 && characters.length < count; index -= 1) { + if (!/\s/u.test(source[index])) characters.unshift(source[index]) + } + return characters.join('') +} + +function closureParametersStart(source, argumentStart, pipe) { + const prefix = source.slice(argumentStart, pipe).trim() + return /^(?:(?:&(?:\s*mut)?|[*!-])\s*)*(?:async\s+)?(?:move\s+)?$/u.test(prefix) +} + +function topLevelArguments(source, open, close) { + const argumentsList = [] + let start = open + 1 + const delimiters = [] + let angleDepth = 0 + let closureParameters = false + + for (let index = start; index <= close; index += 1) { + if (source.startsWith('//', index)) { + const newline = source.indexOf('\n', index + 2) + index = newline === -1 ? close : Math.min(newline, close) + } else if (source.startsWith('/*', index)) { + let depth = 1 + index += 2 + while (index < close && depth > 0) { + if (source.startsWith('/*', index)) { + depth += 1 + index += 2 + } else if (source.startsWith('*/', index)) { + depth -= 1 + index += 2 + } else { + index += 1 + } + } + index -= 1 + continue + } + + const rawEnd = rawStringEnd(source, index) + if (rawEnd !== null) { + index = rawEnd - 1 + continue + } + if (source[index] === '"') { + index += 1 + while (index < close && source[index] !== '"') { + if (source[index] === '\\') index += 1 + index += 1 + } + continue + } + const characterEnd = characterLiteralEnd(source, index) + if (characterEnd !== null) { + index = characterEnd - 1 + continue + } + + if (closureParameters) { + if (source[index] === '|') closureParameters = false + continue + } + if ( + delimiters.length === 0 && + angleDepth === 0 && + source[index] === '|' && + closureParametersStart(source, start, index) + ) { + if (source[index + 1] === '|') index += 1 + else closureParameters = true + continue + } + + if (source[index] === '<' && (angleDepth > 0 || previousNonWhitespace(source, index, 2) === '::')) { + angleDepth += 1 + continue + } + if (source[index] === '>' && angleDepth > 0) { + angleDepth -= 1 + continue + } + if (angleDepth > 0) continue + + if ('([{'.includes(source[index])) { + delimiters.push(new Map([ + ['(', ')'], + ['[', ']'], + ['{', '}'], + ]).get(source[index])) + continue + } + if (source[index] === delimiters.at(-1)) { + delimiters.pop() + continue + } + if (index === close || (source[index] === ',' && delimiters.length === 0)) { + const argument = source.slice(start, index).trim() + if (argument !== '') argumentsList.push(argument) + start = index + 1 + } + } + return argumentsList +} + +function receiverKind(receiver) { + if (/^(?:mut\s+)?self(?:\s*:|$)/u.test(receiver)) return 'owned' + if (/^&\s*(?:'[A-Za-z_][A-Za-z0-9_]*\s+)?(?:mut\s+)?self(?:\s*:|$)/u.test(receiver)) { + return 'borrowed' + } + return null +} + +function retiredMemoryAccessorDeclarations(source, originalSource) { + const matches = [] + const declaration = /\bfn\s+memory_ceiling\b/gu + for (const match of source.matchAll(declaration)) { + let open = match.index + match[0].length + while (/\s/u.test(source[open] ?? '')) open += 1 + if (source[open] === '<') { + const genericClose = closingGenericList(source, open) + if (genericClose === -1) continue + open = genericClose + 1 + while (/\s/u.test(source[open] ?? '')) open += 1 + } + if (source[open] !== '(') continue + const close = closingDelimiter(source, open) + if (close === -1) continue + const argumentsList = topLevelArguments(source, open, close) + if (argumentsList.length !== 1) continue + const receiver = receiverKind(argumentsList[0]) + if (receiver === null) continue + + const returnType = source.slice(close + 1).match(/^\s*->\s*((?:(?:std|core)\s*::\s*option\s*::\s*)?Option)\b/u) + if (returnType === null) continue + const end = close + 1 + returnType.index + returnType[0].length + matches.push({ index: match.index, text: originalSource.slice(match.index, end) }) + } + return matches +} + +function constructorMatches(source, originalSource, typeName, argumentCount) { + const constructor = new RegExp( + `\\b${typeName}\\b\\s*::\\s*new\\s*\\(`, + 'gu', + ) + const matches = [] + for (const match of source.matchAll(constructor)) { + const start = match.index + const open = start + match[0].lastIndexOf('(') + const close = closingDelimiter(source, open) + if (close === -1) continue + if (topLevelArguments(source, open, close).length === argumentCount) { + matches.push({ index: start, text: originalSource.slice(start, close + 1) }) + } + } + return matches +} + +function finalNamedCutoffArguments(source, originalSource) { + const matches = [] + const call = /\b(?:start_batch_until|send_all_until)\s*\(/gu + for (const match of source.matchAll(call)) { + const open = match.index + match[0].lastIndexOf('(') + const close = closingDelimiter(source, open) + if (close === -1) continue + const finalArgument = topLevelArguments(source, open, close).at(-1) + if (/^(?:cutoff|_cutoff|batch_cutoff)$/u.test(finalArgument ?? '')) { + matches.push({ + index: match.index, + text: originalSource.slice(match.index, close + 1), + }) + } + } + return matches +} + +function regexMatches(source, pattern, originalSource) { + return [...source.matchAll(pattern)].map((match) => ({ + index: match.index, + text: originalSource.slice(match.index, match.index + match[0].length), + })) +} + +function outboundLegacyMatches(source, originalSource) { + const matches = regexMatches( + source, + /\b(?:finish_batch_item|from_futures)\b|\bOutboundBatchItem\s*::\s*new\s*\(|\bBody\s*::\s*(?:Once|Stream)\b/gu, + originalSource, + ) + matches.push(...regexMatches( + source, + /\b(?:BATCH_DISPATCH_SLACK_MAX|inject_dispatch_slack_for_test|DISPATCH_SLACK_INJECTION|DispatchSlackInjection|DISPATCH_SLACK_MESSAGE)\b/gu, + originalSource, + )) + const call = /\bOutboundBatch\s*::\s*(from_driver|cutoff)\s*\(/gu + for (const match of source.matchAll(call)) { + const open = match.index + match[0].lastIndexOf('(') + const close = closingDelimiter(source, open) + if (close === -1) continue + const args = topLevelArguments(source, open, close) + if ( + (match[1] === 'from_driver' && args.length !== 3) || + (match[1] === 'cutoff' && (args.length !== 1 || /^(?:[0-9]|slot_count\b|requests\s*\.\s*len\s*\()/u.test(args[0]))) + ) { + matches.push({ index: match.index, text: originalSource.slice(match.index, close + 1) }) + } + } + const protectedTypes = [ + 'OutboundDispatchRequest', 'OutboundResponseContext', 'OutboundPreparedBatch', + 'OutboundBatchContext', 'OutboundBatchItem', 'OutboundBatchResults', 'OutboundBatchFailure', + ] + for (const name of protectedTypes) { + const structure = new RegExp('\\bpub\\s+struct\\s+' + name + '\\s*\\{', 'gu') + for (const match of source.matchAll(structure)) { + const open = match.index + match[0].lastIndexOf('{') + const close = closingDelimiter(source, open) + if (close !== -1 && /\bpub\s*(?:\([^)]*\))?\s*\w+\s*:/u.test(source.slice(open + 1, close))) { + matches.push({ index: match.index, text: originalSource.slice(match.index, close + 1) }) + } + } + const implementation = new RegExp('\\bimpl\\s+' + name + '\\s*\\{', 'gu') + for (const match of source.matchAll(implementation)) { + const open = match.index + match[0].lastIndexOf('{') + const close = closingDelimiter(source, open) + if (close !== -1 && /\bpub\s+fn\s+(?:new|from_parts)\b/u.test(source.slice(open + 1, close))) { + matches.push({ index: match.index, text: originalSource.slice(match.index, close + 1) }) + } + } + } + const implementation = /\bimpl(?:\s*<[^>]*>)?\s+(?:[A-Za-z_]\w*\s*::\s*)*OutboundHttpClient\s+for\b[^{]*\{/gu + for (const match of source.matchAll(implementation)) { + const open = match.index + match[0].lastIndexOf('{') + const close = closingDelimiter(source, open) + const body = source.slice(open + 1, close) + if (close !== -1 && ( + /\bfn\s+send\s*\(/u.test(body) || + /\bfn\s+start_batch_until\s*\([^)]*\bVec\s*<\s*(?:[A-Za-z_]\w*\s*::\s*)*OutboundRequest\s*>/u.test(body) + )) { + matches.push({ index: match.index, text: originalSource.slice(match.index, close + 1) }) + } + } + return matches +} + +function findLegacyForms(source, rustSource = false, originalSource = source, checkOutbound = true) { + const searchable = rustSource ? maskRustCommentsAndLiterals(source) : source + const memoryAccessor = ['memory_', 'ceiling'].join('') + return [ + ...(rustSource && checkOutbound ? outboundLegacyMatches(searchable, originalSource) : []), + ...regexMatches( + searchable, + new RegExp(`\\b${retiredSource}\\b`, 'gu'), + originalSource, + ), + ...regexMatches( + searchable, + new RegExp(`\\b${retiredAdapterMethod.replace('::', '\\s*::\\s*')}\\b`, 'gu'), + originalSource, + ), + ...retiredMemoryAccessorDeclarations(searchable, originalSource), + ...regexMatches( + searchable, + new RegExp(`\\b${retiredClockConstructor}\\b`, 'gu'), + originalSource, + ), + ...regexMatches( + searchable, + new RegExp( + `(?:${metadata}\\s*::\\s*|\\.)${memoryAccessor}\\s*\\(\\s*\\)\\s*->\\s*Option\\b`, + 'gu', + ), + originalSource, + ), + ...regexMatches( + searchable, + /\b(?:cutoff|_cutoff|batch_cutoff)\s*:\s*(?:Option\s*<\s*)?(?:[A-Za-z_][A-Za-z0-9_]*\s*::\s*)*Deadline\s*>?/gu, + originalSource, + ), + ...regexMatches( + searchable, + /\blet\s+(?:mut\s+)?(?:cutoff|_cutoff|batch_cutoff)\b/gu, + originalSource, + ), + ...constructorMatches(searchable, originalSource, metadata, 1), + ...constructorMatches(searchable, originalSource, memory, 4), + ...finalNamedCutoffArguments(searchable, originalSource), + ] +} + +function isRustSource(path) { + return path.endsWith('.rs') || path.endsWith('.rs.hbs') +} + +function isMarkdownSource(path) { + return path.endsWith('.md') || path.endsWith('.mdx') +} + +function fenceOpening(line) { + const match = /^ {0,3}(`{3,}|~{3,})[ \t]*([^\r\n]*)$/u.exec(line) + if (match === null) return null + if (match[1][0] === '`' && match[2].includes('`')) return null + const language = match[2].trim().split(/[:,\s]/u, 1)[0].toLowerCase() + return { + marker: match[1][0], + minimumLength: match[1].length, + rust: language === 'rust' || language === 'rs', + } +} + +function isFenceClosing(line, fence) { + let index = 0 + while (index < 3 && line[index] === ' ') index += 1 + let markerLength = 0 + while (line[index + markerLength] === fence.marker) markerLength += 1 + return ( + markerLength >= fence.minimumLength && + line.slice(index + markerLength).trim() === '' + ) +} + +function copyRange(view, source, start, end) { + for (let index = start; index < end; index += 1) view[index] = source[index] +} + +function markdownViews(source) { + const prose = source.split('') + const rust = source.split('') + maskRange(prose, source, 0, source.length) + maskRange(rust, source, 0, source.length) + let fence = null + let lineStart = 0 + + while (lineStart < source.length) { + const newline = source.indexOf('\n', lineStart) + const lineEnd = newline === -1 ? source.length : newline + const nextLine = newline === -1 ? source.length : newline + 1 + const line = source.slice(lineStart, lineEnd).replace(/\r$/u, '') + + if (fence === null) { + const opening = fenceOpening(line) + if (opening !== null) { + fence = { + ...opening, + contentStart: newline === -1 ? source.length : newline + 1, + } + } else { + copyRange(prose, source, lineStart, nextLine) + } + } else if (isFenceClosing(line, fence)) { + if (fence.rust) copyRange(rust, source, fence.contentStart, lineStart) + fence = null + } + + if (newline === -1) break + lineStart = nextLine + } + + if (fence?.rust) copyRange(rust, source, fence.contentStart, source.length) + return { prose: prose.join(''), rust: rust.join('') } +} + +function deduplicateMatches(matches) { + const unique = new Map() + for (const match of matches) { + unique.set(`${match.index}:${match.text}`, match) + } + return [...unique.values()].sort( + (left, right) => left.index - right.index || left.text.localeCompare(right.text), + ) +} + +function findLegacyFormsForPath(path, source) { + // Opaque producer contexts replace raw cutoff parameters. Facade example naming stays checked. + const preparedProducer = + /^crates\/edgezero-core\/src\/outbound(?:\.rs|\/)/u.test(path) || + /^crates\/edgezero-core\/tests\//u.test(path) || + /^crates\/edgezero-adapter-[^/]+\/src\/(?:outbound(?:\.rs|\/)|scheduling\.rs)/u.test(path) + const checkOutbound = !path.startsWith('docs/superpowers/') + if (isRustSource(path)) { + return findLegacyForms(source, true, source, checkOutbound).filter((match) => + !preparedProducer || + !/^(?:let\s+(?:mut\s+)?(?:cutoff|_cutoff|batch_cutoff)\b|(?:cutoff|_cutoff|batch_cutoff)\s*:|(?:start_batch_until|send_all_until)\s*\()/u.test(match.text), + ) + } + if (!isMarkdownSource(path)) return findLegacyForms(source) + const views = markdownViews(source) + const proseMatches = findLegacyForms(views.prose, false, source, checkOutbound) + const rustFenceMatches = findLegacyForms(views.rust, true, source, checkOutbound) + return deduplicateMatches([...proseMatches, ...rustFenceMatches]) +} + +// Prepared-producer spelling exemptions must never exempt retired APIs or public receipts. +for (const path of [ + 'crates/edgezero-core/src/outbound/batch.rs', + 'crates/edgezero-core/tests/outbound_prepared_client.rs', +]) { + const parameter = ['cut', 'off'].join('') + const current = 'fn issue(' + parameter + ': Deadline) { let ' + parameter + ' = context.observation_cutoff(); }' + if (findLegacyFormsForPath(path, current).length !== 0 || + findLegacyFormsForPath(path, 'OutboundBatch::from_futures(work)').length === 0 || + findLegacyFormsForPath(path, 'pub struct OutboundBatchItem { pub index: usize }').length === 0) { + process.stderr.write('prepared producer hard-cut checker self-test failed\n') + process.exit(2) + } +} +for (const path of [ + 'docs/superpowers/specs/historical-outbound.md', + 'docs/superpowers/plans/historical-outbound.md', +]) { + const history = '~~~rust\nOutboundBatch::from_futures(work)\n~~~' + if (findLegacyFormsForPath(path, history).length !== 0 || + findLegacyFormsForPath('docs/guide/outbound.md', history).length === 0) { + process.stderr.write('historical outbound boundary checker self-test failed\n') + process.exit(2) + } +} + +for (const symbol of [ + 'BATCH_DISPATCH_SLACK_MAX', + 'inject_dispatch_slack_for_test', + 'DISPATCH_SLACK_INJECTION', + 'DispatchSlackInjection', + 'DISPATCH_SLACK_MESSAGE', +]) { + for (const path of [ + 'crates/edgezero-core/src/time.rs', + 'crates/edgezero-core/src/outbound/batch.rs', + 'crates/edgezero-adapter-fastly/src/outbound.rs', + 'examples/app-demo/crates/app-demo-core/src/handlers.rs', + 'tests/fixtures/reusable-app/crates/fixture-core/src/lib.rs', + ]) { + for (const fixture of [ + `pub use retired::${symbol} as replacement;`, + `#[cfg(feature = "test-utils")] fn probe() { ${symbol}(); }`, + `#[cfg(test)] fn probe() { ${symbol}(); }`, + ]) { + if (findLegacyFormsForPath(path, fixture).length === 0) { + process.stderr.write(`dispatch-slack checker missed ${symbol} in ${path}\n`) + process.exit(2) + } + } + const nonCode = `// ${symbol}\nconst TEXT: &str = r#"${symbol}"#;` + if (findLegacyFormsForPath(path, nonCode).length !== 0 || + findLegacyFormsForPath(path, `fn ${symbol}_unrelated() {}`).length !== 0) { + process.stderr.write(`dispatch-slack checker matched non-code or a longer name: ${symbol}\n`) + process.exit(2) + } + } + const fenced = `~~~rust\n${symbol}();\n~~~` + if (findLegacyFormsForPath('docs/guide/outbound.md', fenced).length === 0 || + findLegacyFormsForPath('docs/guide/outbound.md', `Removed \`${symbol}\`.`).length !== 0 || + findLegacyFormsForPath('docs/superpowers/specs/historical-outbound.md', fenced).length !== 0 || + findLegacyFormsForPath('docs/audits/evidence/historical-outbound.json', JSON.stringify({ symbol })).length !== 0) { + process.stderr.write(`dispatch-slack checker lost the live/historical boundary: ${symbol}\n`) + process.exit(2) + } +} + +const bareCutoff = ['cut', 'off'].join('') +const batchCutoff = `batch_${bareCutoff}` +const fixtures = [ + 'Body::Once(bytes)', + 'Body /* carrier */ :: Stream(source)', + 'let edgezero_core::Body::Once(bytes) = body;', + 'match body { edgezero_core::body::Body::Stream(source) => source }', + 'OutboundBatch::from_futures(slots, work)', + 'OutboundBatch /* type */ :: from_futures (slots, work)', + 'finish_batch_item(index, start, end, cutoff, result)', + 'OutboundBatchItem::new(index, result)', + 'OutboundBatch::from_driver(slot_count, events)', + 'OutboundBatch::cutoff(3)', + 'pub struct OutboundBatchItem { pub index: usize, result: Slot }', + 'impl OutboundBatchItem { pub fn new(index: usize) -> Self {} }', + 'impl OutboundHttpClient for Script { async fn send(&self, request: OutboundRequest) {} }', + 'impl OutboundHttpClient for Script { fn start_batch_until(&self, requests: Vec, observation_cutoff: Deadline) {} }', + retiredSource, + `pub type ${retiredSource} = PlatformResourceSource;`, + `pub /* compatibility */ type /* name */ ${retiredSource} = PlatformResourceSource;`, + retiredAdapterMethod, + `Adapter /* path */ :: /* method */ memory_${'ceiling'}`, + `fn /* trait */ memory_${'ceiling'} /* call */ ( /* receiver */ & /* borrow */ self /* end */ ) -> Option<${memory}> {}`, + retiredByValueAccessor, + `pub(crate) unsafe fn memory_${'ceiling'}(mut self) -> Option<${memory}>`, + `pub async fn memory_${'ceiling'}(&mut self) -> Option<${memory}>`, + `pub fn memory_${'ceiling'}<'a>(&'a self) -> Option<${memory}>`, + `pub fn memory_${'ceiling'} Option>(&self) -> Option<${memory}>`, + `${metadata}::new(memory)`, + `${metadata} /* type */ :: /* path */ new /* call */ (memory /*, ignored */)`, + `${metadata}::new(make::<'a, A, B>(memory))`, + `${metadata}::new(make::())`, + `${metadata}::new(|left, right| combine(left, right))`, + `${metadata}::new(&|left, right| combine(left, right))`, + `${metadata}::new(&mut |left, right| combine(left, right))`, + `${memory}::new(total, scope, stack, source)`, + `${memory} /* type */ :: /* path */ new /* call */ (total, scope /*, ignored */, stack, source)`, + `${memory}::new(make::(), scope, stack, source)`, + `${memory}::new(total, |left, right| scope(left, right), stack, source)`, + `${memory}::new(total, &|left, right| scope(left, right), stack, source)`, + retiredClockConstructor, + `${bareCutoff}: Deadline`, + `let ${batchCutoff} = deadline();`, + `client.send_all_until(requests, ${bareCutoff})`, +] +const supported = [ + 'Body::from(bytes)', + 'Body::from_stream(source)', + 'let (payload, owner) = body.into_parts(); match payload { BodyPayload::Once(bytes) => bytes, BodyPayload::Stream(source) => source }', + '// Body::Once(bytes)\nconst TEXT: &str = "Body::Stream(source)";', + 'OutboundBatch::from_driver(context, events, owner)', + 'OutboundBatch::cutoff(context)', + 'OutboundBatchDriverEvent::Item { index, completed_at, outcome }', + 'pub struct OutboundBatchItem { identity: Identity, index: usize, result: Slot }', + 'impl OutboundBatchContext { pub(super) fn new() -> Self {} }', + 'impl OutboundHttpClient for Script { fn dispatch_head(&self, request: OutboundDispatchRequest) -> OutboundFuture {} }', + '// OutboundBatch::from_futures(requests, cutoff)', + 'const TEXT: &str = "OutboundBatchItem::new(index, result)";', + `${metadata}::new(memory, population, accounting)`, + `${metadata}::new(memory, population /*, ignored */, accounting)`, + `${metadata}::new(make::(), population, |left, right| accounting(left, right))`, + `${metadata}::new(make::(), population, &|left, right| accounting(left, right))`, + `${memory}::new(total, scope, stack)`, + `${memory}::new(total, scope /*, ignored */, stack)`, + `${memory}::new(make::<'a, A, B>(), |left, right| scope(left, right), stack)`, + `pub const fn memory_${'ceiling'}(self) -> PlatformFact<${memory}>`, + `pub const fn memory_${'ceiling'}(&self) -> PlatformFact<${memory}>`, + `pub fn memory_${'ceiling'} Option>(&self) -> PlatformFact<${memory}>`, + 'fn platform_metadata(&self) -> PlatformMetadata {}', + 'observation_cutoff: Deadline', + 'let observation_cutoff = deadline();', + 'client.send_all_until(requests, observation_cutoff)', + `// ${retiredSource}\nconst VALUE: usize = 1;`, + `/* ${retiredAdapterMethod} */ const VALUE: usize = 1;`, + `const TEXT: &str = "${metadata}::new(memory)";`, + `const RAW: &str = r#"fn memory_${'ceiling'}(&self)"#;`, + `const RAW_C: &CStr = cr#"quoted " ${retiredSource}"#;`, + `const BYTE: u8 = b'${bareCutoff.charAt(0)}';`, +] + +for (const fixture of fixtures) { + if (findLegacyForms(fixture, true).length === 0) { + process.stderr.write(`platform metadata checker missed fixture: ${fixture}\n`) + process.exit(2) + } +} +for (const fixture of supported) { + if (findLegacyForms(fixture, true).length !== 0) { + process.stderr.write(`platform metadata checker rejected supported fixture: ${fixture}\n`) + process.exit(2) + } +} +for (const fixture of [ + `Prose names ${retiredSource}.`, + `\`${metadata}::new(memory)\``, + ``, +]) { + if (findLegacyFormsForPath('docs/guide/legacy-fixture.md', fixture).length === 0) { + process.stderr.write('platform metadata checker masked a Markdown fixture\n') + process.exit(2) + } +} +for (const [name, fixture, expectedStart] of [ + [ + 'plain Rust fence', + `before\n\`\`\`rust\n${metadata} /* trivia */ :: new(memory)\n\`\`\`\nafter`, + metadata, + ], + [ + 'modified Rust fence', + `\`\`\`rust,no_run\nAdapter /* path */ :: /* method */ memory_${'ceiling'}\n\`\`\``, + 'Adapter', + ], + [ + 'modified rs fence', + `\`\`\`rs,ignore\nfn /* trait */ memory_${'ceiling'} /* call */ ( & /* receiver */ self ) -> Option<${memory}> {}\n\`\`\``, + 'fn', + ], + [ + 'tilde Rust fence with colon modifier', + `~~~rust:no_run\n${metadata} /* trivia */ :: new(memory)\n~~~~`, + metadata, + ], + [ + 'indented Rust fence with space modifier', + ` \`\`\`rust no_run\nAdapter /* path */ :: memory_${'ceiling'}\n \`\`\``, + 'Adapter', + ], + [ + 'variable-length rs fence', + `\`\`\`\`rs:ignore\n${metadata} /* trivia */ :: new(memory)\n\`\`\`\`\``, + metadata, + ], +]) { + const matches = findLegacyFormsForPath('docs/guide/legacy-fixture.md', fixture) + if (matches.length !== 1 || matches[0].index !== fixture.indexOf(expectedStart)) { + process.stderr.write(`platform metadata checker missed ${name}\n`) + process.exit(2) + } +} +const nonRustFence = + `\`\`\`text\n${metadata} /* trivia */ :: new(memory)\n\`\`\`` +if (findLegacyFormsForPath('docs/guide/legacy-fixture.md', nonRustFence).length !== 0) { + process.stderr.write('platform metadata checker parsed a non-Rust fence as Rust\n') + process.exit(2) +} +const nonRustRawHistory = `\`\`\`text\n${retiredSource}\n\`\`\`` +if (findLegacyFormsForPath('docs/guide/legacy-fixture.md', nonRustRawHistory).length !== 0) { + process.stderr.write('platform metadata checker scanned a non-Rust fence\n') + process.exit(2) +} +const invalidBacktickFence = + `\`\`\`rust\`bad\n${metadata}::new(memory)\n\`\`\`` +if (findLegacyFormsForPath('docs/guide/legacy-fixture.md', invalidBacktickFence).length === 0) { + process.stderr.write('platform metadata checker treated an invalid backtick fence as a fence\n') + process.exit(2) +} +const rustNonCodeFixture = + `\`\`\`rust\n// ${retiredSource}\n` + + `const TEXT: &str = "${retiredByValueAccessor}";\n` + + `const RAW: &str = r#"${retiredAdapterMethod}"#;\n\`\`\`` +if ( + findLegacyFormsForPath('docs/guide/legacy-fixture.md', rustNonCodeFixture).length !== 0 +) { + process.stderr.write('platform metadata checker scanned Rust fence non-code\n') + process.exit(2) +} +const duplicateFixture = `\`\`\`rust\n${metadata}::new(memory)\n\`\`\`` +if (findLegacyFormsForPath('docs/guide/legacy-fixture.md', duplicateFixture).length !== 1) { + process.stderr.write('platform metadata checker did not deduplicate Markdown matches\n') + process.exit(2) +} + +const roots = process.argv.slice(2) +const isAllowlisted = (path) => allowlist.has(path) +const rootCovers = (root, path) => path === root || path.startsWith(`${root}/`) +const requiredRootError = (candidateRoots) => + candidateRoots.some((root) => rootCovers(root, authoritativePlan)) + ? null + : `platform metadata checker does not scan ${authoritativePlan}` +const rootsWithPlan = [...roots, authoritativePlan] +const rootsWithoutPlan = rootsWithPlan.filter( + (root) => !rootCovers(root, authoritativePlan), +) +if ( + requiredRootError(rootsWithPlan) !== null || + requiredRootError(rootsWithoutPlan) === null +) { + process.stderr.write('platform metadata checker plan-root self-test failed\n') + process.exit(2) +} +const rootError = requiredRootError(roots) +if (rootError !== null) { + process.stderr.write(`${rootError}\n`) + process.exit(2) +} +for (const root of roots) { + const fixturePath = root.includes('.') ? root : `${root}/legacy-fixture.rs` + if (isAllowlisted(fixturePath)) continue + if (findLegacyFormsForPath(fixturePath, retiredSource).length === 0) { + process.stderr.write( + `platform metadata checker did not reject a fixture in ${fixturePath}\n`, + ) + process.exit(2) + } +} +for (const path of allowlist) { + const nearMiss = `${path}.active` + if ( + !isAllowlisted(path) || + isAllowlisted(nearMiss) || + findLegacyForms(fixtures.join('\n')).length === 0 + ) { + process.stderr.write(`platform metadata checker allowlist self-test failed for ${path}\n`) + process.exit(2) + } +} +if (allowlist.size !== 2) { + process.stderr.write('platform metadata checker allowlist must contain exactly two paths\n') + process.exit(2) +} + +const tracked = execFileSync('git', [ + 'ls-files', + '--cached', + '--others', + '--exclude-standard', + '-z', + '--', + ...roots, +]) + .toString('utf8') + .split('\0') + .filter(Boolean) +let failed = false +for (const path of tracked) { + if (isAllowlisted(path)) continue + const source = readFileSync(path, 'utf8') + for (const match of findLegacyFormsForPath(path, source)) { + const line = source.slice(0, match.index).split(/\r?\n/u).length + process.stdout.write(`${path}:${line}:${match.text.replace(/\s+/gu, ' ')}\n`) + failed = true + } +} +process.exit(failed ? 1 : 0) +NODE +} + +active_surfaces=( + crates + examples/app-demo + tests/fixtures/reusable-app + .github/actions + .github/workflows + docs/.vitepress + docs/guide + docs/index.md + docs/specs + docs/superpowers/specs + docs/superpowers/plans/2026-09-26-platform-memory-ceiling.md + docs/superpowers/plans/2026-09-28-platform-resource-metadata.md + scripts + README.md + CLAUDE.md + TODO.md + Cargo.toml + CHANGELOG.md +) + +assert_active_surface_fixture_rejected() { + local path=$1 + local pattern=$2 + local source=$3 + local active + + for active in "${active_surfaces[@]}"; do + if [ "$active" = "$path" ]; then + if printf '%s\n' "$source" | grep -Eq "$pattern"; then + return 0 + fi + printf 'legacy checker did not reject the %s fixture\n' "$path" >&2 + return 2 + fi + done + + printf 'legacy checker does not scan the %s fixture\n' "$path" >&2 + return 2 +} + +failed=0 + +scan \ + 'Proxy(Client|Handle|Request|Response|Service)|proxy_handle|edgezero_core::proxy|crate::proxy|pub mod proxy' \ + crates examples/app-demo tests/fixtures/reusable-app docs/guide README.md CLAUDE.md TODO.md Cargo.toml \ + .claude/agents/code-architect.md || failed=1 + +# Downstream response delivery is now adapter-owned. Keep the outbound-fetch +# buffering helpers: they implement ResponseMode::Buffered and are unrelated to +# response egress. +scan \ + 'ResponseReturned|begin_owned|[A-Z]+_RESPONSE_STREAM_BUFFER_BYTES|SpinFullResponse|Captured(Cloudflare|Spin)Response|EdgeZeroAxumService|dispatch_for_test|pub (async )?fn (from_core_response|into_axum_response)|buffered (fallback|passthrough)|16 MiB downstream conversion fallback|Response -> Workers Response \(buffered bodies\)' \ + crates examples/app-demo tests/fixtures/reusable-app docs/guide README.md TODO.md Cargo.toml \ + ':(exclude)crates/edgezero-cli/src/generator.rs' || failed=1 + +# The generator keeps a negative assertion containing this spelling. Scan the +# rendered source inputs, demos, and user-facing guides where an attribute would +# preserve the retired Fastly response-returning entrypoint. +scan \ + '#\[fastly::main\]' \ + crates/edgezero-adapter-fastly/src/templates examples/app-demo docs/guide README.md || failed=1 + +# The completion-driven hard cut removes the terminal-vector-only API and the +# request-extension-only Fastly lifecycle. The generator's negative assertion +# intentionally contains `.send_all(` and is excluded here. +scan \ + '\.send_all\(|SendAllSlotIsolation|send-all-slot-isolation|(run|serve)_app_with_request_extensions' \ + crates examples/app-demo tests/fixtures/reusable-app docs/guide README.md CLAUDE.md TODO.md \ + ':(exclude)crates/edgezero-cli/src/generator.rs' || failed=1 + +# Batch termination is explicit. Adapter drivers cannot use stream EOF as a +# cutoff signal, and callers cannot interpret every missing item as timeout. +scan \ + 'OutboundBatch::from_stream|while[[:space:]]+let[[:space:]]+Some\([^)]*\)[[:space:]]*=[[:space:]]*batch\.next\(\)\.await|send_all_until\(.*\)\.await\.slots|Result' \ + crates examples/app-demo tests/fixtures/reusable-app docs/guide README.md CLAUDE.md TODO.md || failed=1 + +# Generated/demo Fastly entrypoints must delegate transmission to EdgeZero. +scan \ + 'stream_to_client|send_to_client|send_with_registries' \ + crates/edgezero-adapter-fastly/src/templates examples/app-demo/crates/app-demo-adapter-fastly || failed=1 + +# Application assembly and ingress response ownership are hard-cut contracts. +# Configuration must be fallible, every response-producing admission variant +# uses named fields, and response completion never travels through HTTP +# extensions or an envelope escape hatch. +scan \ + 'fn configure(_app)?\([^)]*&mut (EdgeZeroApp|edgezero_core::app::App|App)[^)]*\)[[:space:]]*\{' \ + crates examples/app-demo tests/fixtures/reusable-app docs/guide README.md || failed=1 + +scan \ + 'fn build_app\(\)[[:space:]]*->[[:space:]]*App|AdmissionDecision::Refuse[[:space:]]*\(|ResponseEgressEnvelope[^[:space:]]*\.into_response|envelope\.into_response\(\)' \ + crates examples/app-demo tests/fixtures/reusable-app docs/guide README.md || failed=1 + +scan \ + '(extensions(_mut)?\(\)|extensions)\.insert\(ResponseEgressCompletion' \ + crates examples/app-demo tests/fixtures/reusable-app docs/guide README.md \ + ':(exclude)crates/edgezero-core/src/response_egress.rs' || failed=1 + +# Normalized ingress errors use one typed send-or-abort decision. The generator +# keeps the retired setter spelling only in a negative assertion. +scan \ + 'DetachedResponseEgressCompletionFactory|set_detached_response_egress_completion_factory' \ + crates examples/app-demo tests/fixtures/reusable-app docs/guide docs/superpowers/specs README.md \ + ':(exclude)crates/edgezero-cli/src/generator.rs' || failed=1 + +# Detached response egress uses named fields only. This scanner spans comments +# and newlines, and its embedded fixtures keep those cases covered in CI. +scan_detached_send_tuple "${active_surfaces[@]}" || failed=1 + +# Platform resource facts hard-cut the old memory-only trait and constructors. +# Only the migration record and focused design retain historical signatures. +scan_platform_resource_legacy "${active_surfaces[@]}" || failed=1 + +# Response-owned deadlines use explicit absolute or egress-start-relative constructors. Keep the +# removed ambiguous constructor and unresolved accessor out of active code and documentation. +deadline_legacy_pattern='ResponseEgressDeadline::ne' +deadline_legacy_pattern+='w[[:space:]]*\(|ResponseEgressDeadline::dead' +deadline_legacy_pattern+='line|pub[[:space:]]+(const[[:space:]]+)?fn[[:space:]]+dead' +deadline_legacy_pattern+='line\(self\)' +deadline_legacy_fixture='ResponseEgressDeadline::ne' +deadline_legacy_fixture+='w(deadline)' +assert_active_surface_fixture_rejected \ + docs/index.md "$deadline_legacy_pattern" "$deadline_legacy_fixture" || failed=1 +scan "$deadline_legacy_pattern" "${active_surfaces[@]}" || failed=1 + +# Retired staging command identifiers must not return to active code or public +# documentation. Historical plans are outside these paths, and the exact terms +# below intentionally do not match ordinary Fastly "staged" provider-state prose. +staging_pattern='DeploySta' +staging_pattern+='ged|deploy_sta' +staging_pattern+='ged|deploy-sta' +staging_pattern+='ged|--sta' +staging_pattern+='ged' +staging_fixture='deploy-sta' +staging_fixture+='ged' +assert_active_surface_fixture_rejected \ + docs/index.md "$staging_pattern" "$staging_fixture" || failed=1 +scan "$staging_pattern" "${active_surfaces[@]}" || failed=1 + +exit "$failed" diff --git a/scripts/check_serde_json_map_contract.mjs b/scripts/check_serde_json_map_contract.mjs new file mode 100644 index 000000000..ffcd99fac --- /dev/null +++ b/scripts/check_serde_json_map_contract.mjs @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process' + +const tree = execFileSync( + 'cargo', + [ + 'tree', + '--locked', + '--offline', + '--package', + 'edgezero-cli', + '--no-default-features', + '--edges', + 'features,no-dev', + '--invert', + 'serde_json', + ], + { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }, +) + +const forbiddenFeatures = [ + 'serde_json feature "indexmap"', + 'serde_json feature "preserve_order"', +] + +for (const feature of forbiddenFeatures) { + if (tree.includes(feature)) { + process.stderr.write( + `serde_json map contract: no-CLI graph enables ${feature}\n`, + ) + process.exitCode = 1 + } +} + +if (process.exitCode === undefined) { + process.stdout.write('serde_json map contract: ok\n') +} diff --git a/scripts/check_shared_dep_pins.sh b/scripts/check_shared_dep_pins.sh index 1b7a70fc0..637897efd 100755 --- a/scripts/check_shared_dep_pins.sh +++ b/scripts/check_shared_dep_pins.sh @@ -1,113 +1,11 @@ #!/usr/bin/env bash -# check_shared_dep_pins.sh — keep every `validator` pin on one major. -# -# `edgezero-core` re-exports the `Validate` derive, so any crate that -# derives `AppConfig` links against the exact `validator` the core crate -# resolved. A crate that pins a different major gets a second, unrelated -# `Validate` trait and fails with: -# -# error[E0277]: the trait bound `X: validator::traits::Validate` is not satisfied -# -# The pin lives in four places that cargo cannot reconcile for us: this -# workspace, the excluded `examples/app-demo` workspace, the scaffold -# generator's seed list, and the deploy smoke-test fixture. Only the -# first two share a lockfile, so the others drift silently until a -# deploy job fails. -# -# Exit 0 — every pin agrees. -# Exit 1 — prints each disagreeing file and its version. - +# Parse dependency requirements with the generator's TOML-based contract tests. +# Compare complete requirements, including audited exact pins, across the root, +# demo, reusable fixture, scaffold seeds, and deployment smoke fixture. set -euo pipefail - REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" - -# Extract validator's version from a file. Accepts the table form with -# `version` in any key position (`validator = { features = [..], version -# = "X" }`), the bare form (`validator = "X"`), and the escaped spelling -# the scaffold seeds it as. Anchored to an assignment at line start so a -# commented-out or prose mention cannot shadow the real pin, and kept -# deliberately in step with `extract_version` in -# crates/edgezero-cli/src/generator.rs -- the two parsers describe the -# same grammar, and a difference between them shows up as this gate -# reporting a missing pin for one that is merely reformatted. -# Prints nothing when there is no match; callers treat empty as missing. -pin_in() { - grep -hoE '^[[:space:]]*(\\?")?validator[[:space:]]*=[^#]*' "$1" 2>/dev/null | - head -1 | - grep -oE '[0-9]+(\.[0-9]+)*' | - head -1 || true -} - -WANT="$(pin_in Cargo.toml)" -if [[ -z "$WANT" ]]; then - echo "check_shared_dep_pins: no validator pin in root Cargo.toml" >&2 - exit 1 -fi - -VIOLATIONS=0 -for file in \ - examples/app-demo/Cargo.toml \ - .github/actions/deploy-fastly/tests/make-smoke-fixture.sh; do - got="$(pin_in "$file")" - if [[ -z "$got" ]]; then - echo "$file: violation: no validator pin found (expected $WANT)" - VIOLATIONS=$((VIOLATIONS + 1)) - elif [[ "$got" != "$WANT" ]]; then - echo "$file: violation: validator $got, root Cargo.toml has $WANT" - VIOLATIONS=$((VIOLATIONS + 1)) - fi +for sentinel in seeded_dependencies_track_the_edgezero_workspace independent_dependencies_track_the_edgezero_workspace; do + sh scripts/run_test_nonzero.sh "$sentinel" \ + cargo test --offline --locked -p edgezero-cli --features cli --lib "$sentinel" done - -# The scaffold generator seeds the pin as a Rust string literal, and its -# own unit test compares it against the root manifest; check it here too -# so a mismatch is caught by the same gate as the rest. `pin_in` already -# understands the quoted-literal spelling, so it is reused rather than -# re-implemented -- a second parser here drifted from this one once. -gen="crates/edgezero-cli/src/generator.rs" -got="$(pin_in "$gen")" -if [[ -z "$got" ]]; then - echo "$gen: violation: no validator seed found (expected $WANT)" - VIOLATIONS=$((VIOLATIONS + 1)) -elif [[ "$got" != "$WANT" ]]; then - echo "$gen: violation: seeds validator $got, root Cargo.toml has $WANT" - VIOLATIONS=$((VIOLATIONS + 1)) -fi - -# `spin-sdk` is pinned in four places and shares no lockfile across them. -# A mismatch does not fail at the pin: the fixture resolves a second copy -# of the SDK and the build dies on `expected IncomingBody, found -# a different IncomingBody`, naming neither crate nor version. -# Only the major is comparable -- the root spells it `~7.0`, the seeds `7`. -spin_major_in() { - grep -hoE '^[[:space:]]*(\\?")?spin-sdk[[:space:]]*=[^#]*' "$1" 2>/dev/null | - head -1 | - grep -oE '[0-9]+' | - head -1 || true -} - -SPIN_WANT="$(spin_major_in Cargo.toml)" -if [[ -z "$SPIN_WANT" ]]; then - echo "check_shared_dep_pins: no spin-sdk pin in root Cargo.toml" >&2 - exit 1 -fi - -for file in \ - examples/app-demo/Cargo.toml \ - tests/fixtures/reusable-app/crates/fixture-spin/Cargo.toml \ - crates/edgezero-cli/src/generator.rs; do - got="$(spin_major_in "$file")" - if [[ -z "$got" ]]; then - echo "$file: violation: no spin-sdk pin found (expected major $SPIN_WANT)" - VIOLATIONS=$((VIOLATIONS + 1)) - elif [[ "$got" != "$SPIN_WANT" ]]; then - echo "$file: violation: spin-sdk major $got, root Cargo.toml has $SPIN_WANT" - VIOLATIONS=$((VIOLATIONS + 1)) - fi -done - -if ((VIOLATIONS > 0)); then - echo "check_shared_dep_pins: $VIOLATIONS pin(s) out of sync" >&2 - exit 1 -fi -echo "check_shared_dep_pins: validator pins agree ($WANT); spin-sdk majors agree ($SPIN_WANT)" diff --git a/scripts/flate_dependency_contract.json b/scripts/flate_dependency_contract.json new file mode 100644 index 000000000..a6cba8c19 --- /dev/null +++ b/scripts/flate_dependency_contract.json @@ -0,0 +1,42 @@ +[ + [ + "adler2", + "2.0.1", + "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + ], + [ + "async-compression", + "0.4.43", + "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" + ], + [ + "compression-codecs", + "0.4.38", + "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" + ], + [ + "compression-core", + "0.4.32", + "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + ], + [ + "crc32fast", + "1.5.0", + "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" + ], + [ + "flate2", + "1.1.9", + "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" + ], + [ + "miniz_oxide", + "0.8.9", + "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" + ], + [ + "simd-adler32", + "0.3.9", + "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + ] +] diff --git a/scripts/qualify_response_egress.mjs b/scripts/qualify_response_egress.mjs new file mode 100644 index 000000000..db4c2af71 --- /dev/null +++ b/scripts/qualify_response_egress.mjs @@ -0,0 +1,241 @@ +import { writeFile } from "node:fs/promises"; +import { createReadStream } from "node:fs"; +import { request } from "node:https"; +import { pathToFileURL } from "node:url"; + +export const CASES = ["normal", "head", "204", "205", "304", "slow", "nonread", + "disconnect", "cancel", "pending", "expired", "source-error", "conversion-error"]; +const TOKEN = /^[a-z0-9-]{1,64}$/; +const MAX_INPUT = 4 * 1024 * 1024; +const MAX_TRANSFER = 64 * 1024 * 1024; + +function token(value) { + return typeof value === "string" && TOKEN.test(value); +} + +function object(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function hex(value, length) { + return typeof value === "string" && value.length === length && /^[a-f0-9]+$/.test(value); +} + +function clientAction(name) { + return ["nonread", "disconnect", "cancel", "slow"].includes(name) ? name : "read"; +} + +function validIdentity(spec) { + return token(spec.run) && hex(spec.revision, 40) && + hex(spec.artifact, 64) && hex(spec.binary_sha256, 64) && + ["axum", "cloudflare", "fastly", "spin"].includes(spec.adapter) && + typeof spec.sdk === "string" && spec.sdk.length > 0 && spec.sdk.length < 80 && + object(spec.runtime) && + ((typeof spec.runtime.revision === "string" && spec.runtime.revision.length > 0 && spec.runtime.revision.length <= 100) || + ["provider-unpublished", "operator-unavailable"].includes(spec.runtime.unknown)) && + object(spec.compatibility) && Array.isArray(spec.compatibility.flags) && spec.compatibility.flags.length <= 32 && + spec.compatibility.flags.every(flag => typeof flag === "string" && /^[a-zA-Z0-9_-]{1,80}$/.test(flag)) && + (spec.adapter !== "cloudflare" || (typeof spec.compatibility.date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(spec.compatibility.date))) && + typeof spec.deployment === "string" && + spec.deployment.length > 0 && spec.deployment.length <= 200; +} + +function requireFact(condition, reason) { + if (!condition) throw new Error(reason); +} + +export function validateTarget(spec) { + const fields = new Set(["origin", "allowed_origin", "isolated_nonproduction", "run", "adapter", "revision", + "artifact", "binary_sha256", "sdk", "runtime", "compatibility", "deployment", "max_probe_ms", "execution_ceiling_ms"]); + requireFact(Object.keys(spec).every(key => fields.has(key)), "Unexpected target field"); + const url = new URL(spec.origin); + requireFact(url.protocol === "https:" && !url.username && !url.password && + url.pathname === "/" && !url.search && !url.hash && url.origin === spec.allowed_origin, + "An explicitly allowlisted HTTPS origin without credentials or a path is required"); + requireFact(validIdentity(spec), + "SDK, runtime, compatibility and independently recorded deployment identity are required"); + requireFact(Number.isInteger(spec.max_probe_ms) && spec.max_probe_ms >= 1000 && + spec.max_probe_ms <= 10_000, "Probe time budget must be 1000..10000 ms"); + requireFact(spec.isolated_nonproduction === true, "A designated isolated nonproduction target is required"); + requireFact(spec.adapter !== "fastly" || (Number.isInteger(spec.execution_ceiling_ms) && + spec.execution_ceiling_ms > 0 && spec.execution_ceiling_ms <= 60_000), + "Fastly pending-source probes require an independently enforced finite execution ceiling"); + return url; +} + +export function validateEvidence(manifest, clients, events) { + try { + requireFact(manifest.schema === 1 && validIdentity(manifest), + "Revision-pinned deployment metadata is incomplete"); + requireFact(manifest.telemetry_complete === true, "Complete unsampled telemetry export is required"); + requireFact(Array.isArray(manifest.expected) && manifest.expected.length > 0 && + manifest.expected.length <= 2 * CASES.length, "Invalid expected probe set"); + const expected = new Map(); + for (const probe of manifest.expected) { + requireFact(token(probe.probe) && CASES.includes(probe.case) && !expected.has(probe.probe), + "Invalid or duplicate scheduled probe"); + expected.set(probe.probe, probe.case); + } + requireFact(clients.length === expected.size, "Missing or extra client observations"); + const observations = new Map(); + for (const client of clients) { + requireFact(expected.get(client.probe) === client.case && !observations.has(client.probe) && + client.action === clientAction(client.case) && + Number.isSafeInteger(client.bytes) && client.bytes >= 0 && client.bytes <= MAX_TRANSFER, + "Invalid client observation"); + observations.set(client.probe, client); + } + requireFact(events.length <= 4 * expected.size, "Unexpected or duplicate telemetry"); + for (const event of events) { + requireFact(event.schema === 1 && event.run === manifest.run && event.revision === manifest.revision && + event.artifact === manifest.artifact && expected.get(event.probe) === event.case && + token(event.instance), "Telemetry identity mismatch"); + } + for (const [probe, name] of expected) { + const trace = events.filter(event => event.probe === probe).sort((a, b) => a.sequence - b.sequence); + requireFact(trace.length === 4 && trace.every((event, index) => + event.sequence === index && event.instance === trace[0].instance), "Incomplete or conflicting trace sequence"); + const one = kind => { + const selected = trace.filter(event => event.event === kind); + requireFact(selected.length === 1, "Missing or duplicate lifecycle event"); + return selected[0]; + }; + one("source-drop"); + const terminal = one("terminal"); + const resource = one("resource-drop"); + const closed = one("trace-closed"); + requireFact(closed.sequence === 3 && closed.terminals === 1 && closed.source_drops === 1 && + closed.resource_drops === 1 && closed.polls_after_terminal === 0 && + Number.isSafeInteger(closed.polls) && closed.polls >= 0 && terminal.sequence < resource.sequence, + "Ownership counters or release ordering disagree"); + requireFact(Number.isSafeInteger(terminal.bytes_written) && terminal.bytes_written >= 0 && + terminal.bytes_written <= MAX_TRANSFER && ["Application", "Fallback"].includes(terminal.body_kind), + "Invalid terminal byte accounting"); + requireFact(["Completed", "HostHandoff", "ClientDisconnected", "ConversionError", "DeadlineExceeded", + "RequestCancelled", "SourceError", "TransportError"].includes(terminal.outcome), "Unknown terminal outcome"); + const bodyless = ["head", "204", "205", "304"].includes(name); + const client = observations.get(probe); + if (["normal", "head", "204", "205", "304"].includes(name)) { + requireFact(["Completed", "HostHandoff"].includes(terminal.outcome) && + terminal.body_kind === "Application" && terminal.fallback === null && client.ended === true && + client.bytes === (bodyless ? 0 : 3072) && client.status === (bodyless && name !== "head" ? Number(name) : 200), + "Normal/bodyless client observation or terminal outcome disagrees"); + } + if (bodyless) requireFact(closed.polls === 0 && terminal.bytes_written === 0, "Suppressed body was polled or written"); + if (name === "normal") requireFact(closed.polls >= 4 && terminal.bytes_written === 3072, + "Normal response lacks source/accepted-byte evidence"); + if (name === "pending") requireFact(closed.polls >= 1, "Pending producer was never polled"); + if (name === "expired") requireFact(closed.polls === 0 && terminal.body_kind === "Fallback" && + (client.status === null || client.status === 504), "Expired precommit response was not suppressed"); + if (["nonread", "disconnect", "cancel"].includes(name)) requireFact(client.ended === false && + client.result === (name === "disconnect" ? "client-close" : "client-deadline"), + "Client did not exercise the scheduled interruption"); + if (name === "slow") requireFact(Number.isSafeInteger(client.pauses) && client.pauses > 0, + "Slow-client backpressure was not exercised"); + if (["expired", "pending"].includes(name)) requireFact(terminal.outcome === "DeadlineExceeded", "Deadline was not observed"); + if (name === "source-error") requireFact(terminal.outcome === "SourceError", "Source failure was misclassified"); + if (name === "conversion-error") requireFact(terminal.outcome === "ConversionError", "Conversion failure was misclassified"); + if (terminal.body_kind === "Fallback") requireFact(["Completed", "Aborted"].includes(terminal.fallback) && + ["DeadlineExceeded", "ConversionError", "SourceError"].includes(terminal.outcome) && terminal.bytes_written <= 1024, + "Fallback cause or bounded disposition is invalid"); + else requireFact(terminal.fallback === null, "Application report cannot have fallback disposition"); + } + return { status: "qualified-ownership", scope: "scheduled probes only; not confirmed client delivery or provider allocation", + probes: expected.size, capabilities_changed: false }; + } catch (error) { + return { status: "unverified", reason: error.message }; + } +} + +async function readJson(path) { + const chunks = []; + let length = 0; + for await (const chunk of createReadStream(path, { highWaterMark: 16 * 1024 })) { + length += chunk.length; + requireFact(length <= MAX_INPUT, "Evidence input exceeds 4 MiB"); + chunks.push(chunk); + } + const bytes = Buffer.concat(chunks, length); + return JSON.parse(bytes.toString("utf8")); +} + +function runClient(origin, spec, probe, remaining) { + return new Promise(resolve => { + let bytes = 0; + let status = null; + let settled = false; + let pauseTimer; + let pauses = 0; + const action = clientAction(probe.case); + const started = performance.now(); + const finish = (ended, result) => { + if (settled) return; + settled = true; + clearTimeout(timer); + clearTimeout(pauseTimer); + req.destroy(); + resolve({ ...probe, action, ended, result, status, bytes, pauses, elapsed_ms: Math.ceil(performance.now() - started) }); + }; + const req = request(new URL(`/qualification/${probe.case}`, origin), { + method: probe.case === "head" ? "HEAD" : "GET", agent: false, + headers: { "x-qualification-run": spec.run, "x-qualification-probe": probe.probe, "accept-encoding": "identity" }, + }, response => { + status = response.statusCode; + response.on("error", () => finish(false, "response-error")); + response.on("end", () => finish(true, "end")); + if (action === "nonread") { response.pause(); return; } + if (action === "disconnect") { finish(false, "client-close"); return; } + response.on("data", chunk => { + bytes += chunk.length; + if (bytes > remaining) { finish(false, "transfer-cap"); return; } + if (action === "slow") { + pauses++; + response.pause(); + pauseTimer = setTimeout(() => response.resume(), 50); + } + }); + }); + const timer = setTimeout(() => finish(false, "client-deadline"), action === "cancel" ? 100 : spec.max_probe_ms); + req.on("error", () => finish(false, "request-error")); + req.end(); + }); +} + +async function main(args) { + if (args[0] === "--execute-hosted" && args.length === 3) { + const spec = await readJson(args[1]); + const origin = validateTarget(spec); + const expected = CASES.flatMap((name, index) => [ + { probe: `case-${index}`, case: name }, { probe: `recovery-${index}`, case: "normal" }, + ]); + const clients = []; + let remaining = MAX_TRANSFER; + for (const probe of expected) { + requireFact(remaining > 0, "Run transfer budget exhausted"); + const observed = await runClient(origin, spec, probe, remaining); + clients.push(observed); + remaining -= observed.bytes; + if (spec.adapter === "fastly" && ["pending", "cancel", "nonread", "disconnect"].includes(probe.case)) { + const teardown = Math.max(0, spec.execution_ceiling_ms - observed.elapsed_ms) + 100; + await new Promise(resolve => setTimeout(resolve, teardown)); + } + } + await writeFile(args[2], JSON.stringify({ manifest: { ...spec, schema: 1, expected, telemetry_complete: false }, clients }, null, 2), { flag: "wx", mode: 0o600 }); + process.stdout.write("Client observations recorded; hosted qualification requires a complete correlated telemetry export.\n"); + return 2; + } + requireFact(args[0] === "--validate" && args.length === 3, + "Use --execute-hosted TARGET.json CLIENTS.json or --validate CLIENTS.json EVENTS.json"); + const { manifest, clients } = await readJson(args[1]); + const events = await readJson(args[2]); + const result = validateEvidence(manifest, clients, events); + process.stdout.write(`${JSON.stringify(result)}\n`); + return result.status === "qualified-ownership" ? 0 : 2; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main(process.argv.slice(2)).then(code => { process.exitCode = code; }).catch(() => { + process.stderr.write("Hosted lifecycle qualification failed; check the bounded target/evidence inputs.\n"); + process.exitCode = 1; + }); +} diff --git a/scripts/qualify_response_egress.test.mjs b/scripts/qualify_response_egress.test.mjs new file mode 100644 index 000000000..565df13c7 --- /dev/null +++ b/scripts/qualify_response_egress.test.mjs @@ -0,0 +1,115 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { validateEvidence, validateTarget } from "./qualify_response_egress.mjs"; + +function fixture() { + const manifest = { + schema: 1, run: "run-1", revision: "a".repeat(40), artifact: "b".repeat(64), + adapter: "cloudflare", sdk: "worker 0.8.5", runtime: { unknown: "provider-unpublished" }, + compatibility: { date: "2026-10-07", flags: [] }, deployment: "isolated-test-deployment", + binary_sha256: "c".repeat(64), + telemetry_complete: true, expected: [{ probe: "normal-1", case: "normal" }], + }; + const clients = [{ probe: "normal-1", case: "normal", action: "read", ended: true, bytes: 3072, status: 200 }]; + const common = { schema: 1, run: manifest.run, revision: manifest.revision, artifact: manifest.artifact, + probe: "normal-1", case: "normal", instance: "instance-1" }; + const events = [ + { ...common, sequence: 0, event: "source-drop" }, + { ...common, sequence: 1, event: "terminal", outcome: "HostHandoff", body_kind: "Application", + bytes_written: 3072, fallback: null }, + { ...common, sequence: 2, event: "resource-drop" }, + { ...common, sequence: 3, event: "trace-closed", source_drops: 1, resource_drops: 1, + terminals: 1, polls: 4, polls_after_terminal: 0 }, + ]; + return { manifest, clients, events }; +} + +test("complete correlated ownership evidence is accepted without claiming client delivery", () => { + const { manifest, clients, events } = fixture(); + assert.equal(validateEvidence(manifest, clients, events).status, "qualified-ownership"); +}); + +test("missing, duplicated, truncated and mismatched telemetry fails closed", () => { + for (const mutate of [ + ({ events }) => events.splice(0, 1), + ({ events }) => events.push({ ...events[1] }), + ({ events }) => events.pop(), + ({ events }) => { events[1].revision = "c".repeat(40); }, + ({ events }) => { events[3].polls_after_terminal = 1; }, + ({ events }) => { events[3].source_drops = 2; }, + ({ manifest }) => { manifest.telemetry_complete = false; }, + ({ clients }) => clients.pop(), + ({ clients }) => { clients[0].action = "nonread"; }, + ({ manifest }) => { manifest.adapter = "invented-target"; }, + ({ manifest }) => { delete manifest.binary_sha256; }, + ({ manifest }) => manifest.expected.push({ probe: "recovery-1", case: "normal" }), + ({ events }) => { events[1].body_kind = "Fallback"; }, + ({ events }) => { events[1].bytes_written = 0; }, + ({ events }) => { events[3].polls = 0; }, + ({ manifest }) => { manifest.runtime = true; }, + ({ manifest }) => { manifest.compatibility = true; }, + ({ manifest }) => { manifest.binary_sha256 = [manifest.binary_sha256]; }, + ({ manifest, events }) => { delete manifest.run; for (const event of events) delete event.run; }, + ({ manifest, events, clients }) => { + delete manifest.expected[0].probe; delete clients[0].probe; + for (const event of events) delete event.probe; + }, + ]) { + const value = fixture(); + mutate(value); + assert.notEqual(validateEvidence(value.manifest, value.clients, value.events).status, "qualified-ownership"); + } +}); + +test("target requires allowlisting, finite budgets and no unexpected private fields", () => { + const { schema, expected, telemetry_complete, ...identity } = fixture().manifest; + const target = { ...identity, origin: "https://isolated.example.com", allowed_origin: "https://isolated.example.com", + isolated_nonproduction: true, max_probe_ms: 2000 }; + assert.equal(validateTarget(target).origin, target.origin); + for (const changed of [ + { ...target, origin: "http://isolated.example.com" }, + { ...target, origin: "https://token@isolated.example.com" }, + { ...target, allowed_origin: "https://another.example.com" }, + { ...target, isolated_nonproduction: false }, + { ...target, max_probe_ms: 0 }, + { ...target, private_token: "do-not-persist" }, + { ...target, adapter: "fastly" }, + ]) assert.throws(() => validateTarget(changed)); +}); + +test("bodyless and failure cases enforce source activity and terminal classifications", () => { + for (const name of ["head", "204", "205", "304", "pending", "expired", "source-error", "conversion-error"]) { + const value = fixture(); + value.manifest.expected[0].case = name; + value.clients[0].case = name; + for (const event of value.events) event.case = name; + const terminal = value.events[1]; + const closed = value.events[3]; + const client = value.clients[0]; + const bodyless = ["head", "204", "205", "304"].includes(name); + terminal.bytes_written = name === "source-error" ? 1024 : 0; + closed.polls = bodyless || ["expired", "conversion-error"].includes(name) ? 0 : 2; + client.bytes = 0; + client.status = bodyless ? (name === "head" ? 200 : Number(name)) : null; + if (!bodyless) { + terminal.outcome = ({pending: "DeadlineExceeded", expired: "DeadlineExceeded", + "source-error": "SourceError", "conversion-error": "ConversionError"})[name]; + client.ended = false; + } + if (["expired", "conversion-error"].includes(name)) { + terminal.body_kind = "Fallback"; + terminal.fallback = "Completed"; + } + assert.equal(validateEvidence(value.manifest, value.clients, value.events).status, "qualified-ownership", name); + terminal.outcome = "InventedOutcome"; + assert.equal(validateEvidence(value.manifest, value.clients, value.events).status, "unverified", name); + } +}); + +test("reordered complete telemetry is accepted; records from another instance are not", () => { + const value = fixture(); + value.events.reverse(); + assert.equal(validateEvidence(value.manifest, value.clients, value.events).status, "qualified-ownership"); + value.events[0].instance = "another-instance"; + assert.notEqual(validateEvidence(value.manifest, value.clients, value.events).status, "qualified-ownership"); +}); diff --git a/scripts/run_test_nonzero.sh b/scripts/run_test_nonzero.sh new file mode 100755 index 000000000..9dafc8475 --- /dev/null +++ b/scripts/run_test_nonzero.sh @@ -0,0 +1,71 @@ +#!/bin/sh + +set -eu + +selection= +case "${1:-}" in + --ignored|--include-ignored) + selection=$1 + shift + ;; +esac + +if [ "$#" -lt 2 ]; then + echo "usage: $0 [--ignored|--include-ignored] |--inventory " >&2 + exit 2 +fi + +exact=0 +if [ "$1" = --inventory ]; then + if [ "$#" -lt 3 ]; then + echo "inventory mode needs an environment and command" >&2 + exit 2 + fi + sentinels=$(node "$(dirname "$0")/check_outbound_cutover_evidence.mjs" --sentinels "$2") + exact=1 + shift 2 +else + sentinels=$1 + shift +fi +listing=$(mktemp "${TMPDIR:-/tmp}/edgezero-tests.XXXXXX") +trap 'rm -f "$listing"' EXIT HUP INT TERM + +if [ -n "$selection" ]; then + "$@" -- "$selection" --list >"$listing" +else + "$@" -- --list >"$listing" +fi + +REQUIRED_TEST_SENTINELS="$sentinels" awk -v exact="$exact" ' + BEGIN { required = split(ENVIRON["REQUIRED_TEST_SENTINELS"], names, "\n") } + /: test$/ { + count += 1 + name = $0 + sub(/: test$/, "", name) + for (i = 1; i <= required; i++) { + if (name == names[i] || (!exact && name ~ ("::" names[i] "$"))) { + found[i] = 1 + } + } + } + END { + if (count == 0) { + print "selected test suite contains zero tests" > "/dev/stderr" + exit 1 + } + for (i = 1; i <= required; i++) { + if (!found[i]) { + print "required sentinel test not listed: " names[i] > "/dev/stderr" + missing = 1 + } + } + if (missing) exit 1 + } +' "$listing" + +if [ -n "$selection" ]; then + "$@" -- "$selection" +else + "$@" +fi diff --git a/scripts/run_test_nonzero.test.mjs b/scripts/run_test_nonzero.test.mjs new file mode 100644 index 000000000..5ff4d003c --- /dev/null +++ b/scripts/run_test_nonzero.test.mjs @@ -0,0 +1,112 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const script = fileURLToPath(new URL("./run_test_nonzero.sh", import.meta.url)); + +function run( + mode, + listed = ["tests::sentinel"], + exit = 0, + required = ["sentinel"], +) { + const fixture = ` + const mode = ${JSON.stringify(mode)}; + const args = process.argv.slice(1); + const selection = args.filter(arg => arg === "--ignored" || arg === "--include-ignored"); + if (JSON.stringify(selection) !== JSON.stringify(mode ? [mode] : [])) process.exit(87); + if (args.includes("--list")) { + for (const name of ${JSON.stringify(listed)}) console.log(name + ": test"); + } else { + console.log("executed"); + process.exit(${exit}); + } + `; + return spawnSync( + "sh", + [ + script, + ...(mode ? [mode] : []), + ...required, + process.execPath, + "-e", + fixture, + ], + { encoding: "utf8" }, + ); +} + +for (const mode of [null, "--ignored", "--include-ignored"]) { + test(`guard forwards ${mode ?? "default"} selection to listing and execution`, () => { + const result = run(mode); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /executed/); + }); + + test(`guard preserves ${mode ?? "default"} execution failure`, () => { + assert.equal(run(mode, ["tests::sentinel"], 42).status, 42); + }); +} + +test("guard rejects zero or missing sentinel tests before execution", () => { + for (const listed of [[], ["tests::sentinel_suffix"]]) { + const result = run(null, listed); + assert.equal(result.status, 1, result.stderr); + assert.doesNotMatch(result.stdout, /executed/); + } +}); + +const inventory = JSON.parse( + readFileSync( + new URL( + "../tests/fixtures/reusable-app/outbound-cutover-cases.json", + import.meta.url, + ), + "utf8", + ), +); +const required = inventory.cases + .filter( + (c) => c.environment === "axum/native-tcp" && c.implementation === "ready", + ) + .flatMap((c) => [c.sentinel, ...(c.additional_sentinels ?? [])]); + +test("inventory guard executes the whole suite once with all required tests", () => { + const result = run("--include-ignored", [...required, "unrelated::test"], 0, [ + "--inventory", + "axum/native-tcp", + ]); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout.match(/executed/g)?.length, 1); +}); + +test("inventory guard rejects each missing required test before execution", () => { + for (const name of required) { + const result = run( + "--include-ignored", + required.filter((test) => test !== name), + 0, + ["--inventory", "axum/native-tcp"], + ); + assert.equal(result.status, 1, `${name}: ${result.stderr}`); + assert.match(result.stderr, /required sentinel test not listed/); + assert.doesNotMatch(result.stdout, /executed/); + } +}); + +test("inventory guard rejects unknown environments and preserves suite failure", () => { + const unknown = run("--include-ignored", required, 0, [ + "--inventory", + "unknown", + ]); + assert.equal(unknown.status, 1, unknown.stderr); + assert.match(unknown.stderr, /no ready test sentinels/); + assert.doesNotMatch(unknown.stdout, /executed/); + assert.equal( + run("--include-ignored", required, 42, ["--inventory", "axum/native-tcp"]) + .status, + 42, + ); +}); diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 9d957fbc4..4a20d56f6 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -14,7 +14,7 @@ command -v rustup >/dev/null 2>&1 || { exit 1 } -for target in wasm32-wasip1 wasm32-wasip2; do +for target in wasm32-unknown-unknown wasm32-wasip1 wasm32-wasip2; do if ! rustup target list --installed | grep -Fxq "$target"; then echo "$target target is not installed. Run 'rustup target add $target' before re-running this script." >&2 exit 1 @@ -35,9 +35,34 @@ section() { section "Workspace Tests" run cargo test --workspace --all-targets +section "Dependency Contracts" +run node scripts/check_serde_json_map_contract.mjs +run node scripts/check_flate_dependency_contract.mjs + +section "Outbound Contract Tests" +run scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --test contract +run scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils --test contract +run scripts/run_test_nonzero.sh batch_preflight_rejects_streamed_slots_without_poisoning_siblings cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils --test contract +run scripts/run_test_nonzero.sh batch_preflight_precedence_and_indices cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils --test contract + +section "Outbound Capability Tests" +for adapter in axum cloudflare fastly spin; do + run scripts/run_test_nonzero.sh adapter_capability_matrix_matches_contracts cargo test --offline --locked -p "edgezero-adapter-${adapter}" --no-default-features --features cli --lib adapter_capability_matrix_matches_contracts +done + section "Workspace Feature Compilation" run cargo check --workspace --all-targets --features "fastly cloudflare spin" +section "Adapter Native Feature Matrices" +for adapter in axum cloudflare fastly spin; do + run bash scripts/check_adapter_feature_matrix.sh "${adapter}" native +done + +section "Adapter Wasm Feature Matrices" +run bash scripts/check_adapter_feature_matrix.sh cloudflare wasm32-unknown-unknown +run bash scripts/check_adapter_feature_matrix.sh fastly wasm32-wasip1 +run bash scripts/check_adapter_feature_matrix.sh spin wasm32-wasip2 + section "Fastly CLI Tests" run cargo test -p edgezero-adapter-fastly --no-default-features --features cli @@ -47,12 +72,15 @@ section "Fastly Wasm Tests" run cargo test --features fastly --target wasm32-wasip1 -- --nocapture ) -# Spin 6.0 compiles to wasm32-wasip2; CI runs the full contract +# Spin SDK 7 compiles to wasm32-wasip2; CI runs the full contract # test under wasmtime. Locally we just check it compiles — the # contract test needs wasmtime + the wasm runner pinned in CI. section "Spin Wasm Compile Check" run cargo check -p edgezero-adapter-spin --features spin --target wasm32-wasip2 +section "Generated Project" +run scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds + # `examples/app-demo` is excluded from the root workspace # (per `exclude = ["examples/app-demo"]`), so the workspace # test above doesn't cover it. Stage 8.6 wired this gate into @@ -60,7 +88,14 @@ run cargo check -p edgezero-adapter-spin --features spin --target wasm32-wasip2 section "app-demo Workspace Tests" ( cd examples/app-demo - run cargo test --workspace --all-targets + run cargo test --locked --workspace --all-targets + run cargo check --locked -p app-demo-adapter-cloudflare --target wasm32-unknown-unknown --no-default-features --features cloudflare + run cargo check --locked -p app-demo-adapter-fastly --target wasm32-wasip1 --no-default-features --features fastly + run cargo check --locked -p app-demo-adapter-spin --target wasm32-wasip2 --no-default-features --features spin ) +section "Outbound Documentation Contracts" +run bash scripts/check_outbound_legacy_api.sh +run node scripts/check_outbound_docs_contract.mjs + echo "All tests completed successfully." diff --git a/scripts/smoke_test_config_key_override.sh b/scripts/smoke_test_config_key_override.sh index 0c011c8dc..93a1e5d67 100755 --- a/scripts/smoke_test_config_key_override.sh +++ b/scripts/smoke_test_config_key_override.sh @@ -355,7 +355,7 @@ boot_runtime() { fastly) # DEMO_API_TOKEN_SECRET is the env var viceroy reads to # populate the `demo_api_token` secret-store entry seeded - # by seed_secret_for_adapter. viceroy 0.17+ uses `serve` + # by seed_secret_for_adapter. Current Viceroy uses `serve` # for the long-running HTTP path (`run` was renamed). # # The wasm artifact lives in the app-demo workspace's diff --git a/scripts/smoke_test_reusable_app.sh b/scripts/smoke_test_reusable_app.sh index 0dacc8fa1..6fdefd308 100755 --- a/scripts/smoke_test_reusable_app.sh +++ b/scripts/smoke_test_reusable_app.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash set -euo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -exec "${CARGO:-cargo}" run --quiet --locked --manifest-path "$REPO_DIR/tests/fixtures/reusable-app/Cargo.toml" -p fixture-harness -- "$@" +cd "$REPO_DIR" +exec "${CARGO:-cargo}" run --quiet --locked --manifest-path "$REPO_DIR/crates/edgezero-test-harness/Cargo.toml" -- "$@" diff --git a/tests/fixtures/reusable-app/Cargo.lock b/tests/fixtures/reusable-app/Cargo.lock index 2c122ed8e..59f9cb617 100644 --- a/tests/fixtures/reusable-app/Cargo.lock +++ b/tests/fixtures/reusable-app/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -23,50 +23,35 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" -[[package]] -name = "alloc-no-stdlib" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" - [[package]] name = "alloc-stdlib" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ - "alloc-no-stdlib 2.0.4", -] - -[[package]] -name = "alloc-stdlib" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" -dependencies = [ - "alloc-no-stdlib 3.0.0", + "alloc-no-stdlib", ] [[package]] name = "android_system_properties" -version = "0.1.6" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anyhow" -version = "1.0.104" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "async-compression" -version = "0.4.50" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee19bd99b43e3691acbad4e840420a4881cea6c0b66a208125a824f8fd53f5a1" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -98,13 +83,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.92" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] @@ -121,9 +106,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.2" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "zeroize", @@ -131,15 +116,14 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.46.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", "dunce", "fs_extra", - "pkg-config", ] [[package]] @@ -202,9 +186,9 @@ checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base64" -version = "0.23.1" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" @@ -214,9 +198,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -236,55 +220,25 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - [[package]] name = "brotli" version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ - "alloc-no-stdlib 2.0.4", - "alloc-stdlib 0.2.4", - "brotli-decompressor 5.0.3", -] - -[[package]] -name = "brotli" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" -dependencies = [ - "alloc-no-stdlib 3.0.0", - "alloc-stdlib 0.3.0", - "brotli-decompressor 6.0.1", + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", ] [[package]] name = "brotli-decompressor" -version = "5.0.3" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" dependencies = [ - "alloc-no-stdlib 2.0.4", - "alloc-stdlib 0.2.4", -] - -[[package]] -name = "brotli-decompressor" -version = "6.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" -dependencies = [ - "alloc-no-stdlib 3.0.0", - "alloc-stdlib 0.3.0", + "alloc-no-stdlib", + "alloc-stdlib", ] [[package]] @@ -301,9 +255,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.8.0" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", @@ -313,24 +267,24 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.5" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", - "cpufeatures 0.3.1", + "cpufeatures 0.3.0", "rand_core", ] @@ -367,9 +321,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.8" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", @@ -377,11 +331,11 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.45" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98fc98460ba0ad5317075d3632b8dfc45d0be8c4a49347c2a38272019717614a" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ - "brotli 9.0.0", + "brotli", "compression-core", "flate2", "memchr", @@ -389,9 +343,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.33" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "const-oid" @@ -426,18 +380,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.5.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -451,22 +405,11 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "ctrlc" -version = "3.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" -dependencies = [ - "dispatch2", - "nix", - "windows-sys 0.61.2", -] - [[package]] name = "darling" -version = "0.23.0" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -474,10 +417,11 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.23.0" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ + "fnv", "ident_case", "proc-macro2", "quote", @@ -487,9 +431,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", @@ -498,10 +442,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.9" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ + "powerfmt", "serde_core", ] @@ -525,27 +470,15 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.2", - "block2", - "libc", - "objc2", -] - [[package]] name = "displaydoc" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] @@ -565,6 +498,7 @@ name = "edgezero-adapter-axum" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "async-trait", "axum", "bytes", @@ -572,12 +506,17 @@ dependencies = [ "futures", "futures-util", "http", + "http-body", + "hyper", + "hyper-util", + "libc", "log", "redb", "reqwest", + "serde", "serde_json", "simple_logger", - "thiserror 2.0.21", + "thiserror 2.0.20", "tokio", "tower", "tracing", @@ -588,8 +527,9 @@ name = "edgezero-adapter-cloudflare" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "async-trait", - "brotli 8.0.4", + "brotli", "bytes", "edgezero-core", "flate2", @@ -597,6 +537,8 @@ dependencies = [ "futures-util", "log", "serde_json", + "wasm-streams", + "web-sys", "worker", ] @@ -608,11 +550,13 @@ dependencies = [ "async-stream", "async-trait", "base16ct", - "brotli 8.0.4", + "brotli", "bytes", "chrono", "edgezero-core", "fastly", + "fastly-shared", + "fastly-sys", "fern", "flate2", "futures", @@ -622,7 +566,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.1", - "thiserror 2.0.21", + "thiserror 2.0.20", ] [[package]] @@ -630,11 +574,10 @@ name = "edgezero-adapter-spin" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "async-trait", - "brotli 8.0.4", "bytes", "edgezero-core", - "flate2", "futures", "futures-util", "log", @@ -642,36 +585,48 @@ dependencies = [ "serde_json", "spin-sdk", "subtle", - "thiserror 2.0.21", + "thiserror 2.0.20", ] [[package]] name = "edgezero-core" version = "0.1.0" dependencies = [ + "adler2", + "alloc-no-stdlib", + "alloc-stdlib", "anyhow", "async-compression", "async-stream", "async-trait", "base16ct", + "brotli", + "brotli-decompressor", "bytes", + "compression-codecs", + "compression-core", + "crc32fast", "edgezero-macros", + "flate2", "futures", "futures-util", "http", "http-body", "log", "matchit 0.9.2", + "miniz_oxide", "ryu", "serde", "serde_json", "serde_path_to_error", "serde_urlencoded", "sha2 0.11.1", - "thiserror 2.0.21", + "simd-adler32", + "thiserror 2.0.20", "toml", "tower-service", "tracing", + "url", "validator", "web-time", ] @@ -692,9 +647,9 @@ dependencies = [ [[package]] name = "either" -version = "1.19.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elsa" @@ -708,9 +663,9 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -794,9 +749,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.14" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixture-axum" @@ -806,6 +761,9 @@ dependencies = [ "edgezero-adapter-axum", "edgezero-core", "fixture-core", + "reqwest", + "serde_json", + "tokio", ] [[package]] @@ -815,6 +773,7 @@ dependencies = [ "edgezero-adapter-cloudflare", "edgezero-core", "fixture-core", + "futures", "serde_json", "worker", ] @@ -824,8 +783,10 @@ name = "fixture-core" version = "0.1.0" dependencies = [ "async-trait", + "bytes", "edgezero-core", "futures", + "log", "serde_json", ] @@ -836,20 +797,13 @@ dependencies = [ "edgezero-adapter-fastly", "edgezero-core", "fastly", + "fastly-sys", "fixture-core", "futures", "log", "serde_json", ] -[[package]] -name = "fixture-harness" -version = "0.1.0" -dependencies = [ - "ctrlc", - "serde_json", -] - [[package]] name = "fixture-spin" version = "0.1.0" @@ -858,21 +812,34 @@ dependencies = [ "edgezero-adapter-spin", "edgezero-core", "fixture-core", + "futures", + "http-body", "serde_json", "spin-sdk", ] [[package]] name = "flate2" -version = "1.1.10" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", - "zlib-rs", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -896,9 +863,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -911,9 +878,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -921,15 +888,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -938,38 +905,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.34" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1007,25 +974,48 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.3" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core", + "wasip2", + "wasip3 0.4.0+wasi-0.3.0-rc-2026-01-06", "wasm-bindgen", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1036,9 +1026,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.5.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1046,9 +1036,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", @@ -1110,9 +1100,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.10" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", @@ -1125,9 +1115,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.21" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", @@ -1135,7 +1125,6 @@ dependencies = [ "futures-util", "http", "http-body", - "httparse", "hyper", "ipnet", "libc", @@ -1173,9 +1162,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", @@ -1187,9 +1176,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1200,9 +1189,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1214,17 +1203,16 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -1235,15 +1223,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.3.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1289,21 +1277,21 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.2" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "ipnet" -version = "2.12.2" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "itertools" @@ -1332,7 +1320,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.21", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -1371,11 +1359,11 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.35" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.4.3", + "getrandom 0.3.4", "libc", ] @@ -1398,21 +1386,21 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.190" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "litemap" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "log" -version = "0.4.34" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "log-fastly" @@ -1427,9 +1415,9 @@ dependencies = [ [[package]] name = "lru-slab" -version = "0.1.3" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "matchit" @@ -1451,9 +1439,9 @@ checksum = "8863b587001c1b9a8a4e36008cebc6b3612cb1226fe2de94858e06092687b608" [[package]] name = "memchr" -version = "2.8.3" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mime" @@ -1463,9 +1451,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.9.1" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -1473,27 +1461,15 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] -[[package]] -name = "nix" -version = "0.31.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" -dependencies = [ - "bitflags 2.13.2", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "num-conv" version = "0.2.2" @@ -1518,21 +1494,6 @@ dependencies = [ "libc", ] -[[package]] -name = "objc2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - [[package]] name = "once_cell" version = "1.21.4" @@ -1583,26 +1544,20 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkg-config" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" - [[package]] name = "potential_utf" -version = "0.1.6" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] [[package]] name = "powerfmt" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "prettyplease" @@ -1615,41 +1570,51 @@ dependencies = [ ] [[package]] -name = "proc-macro-error-attr3" -version = "3.1.1" +name = "prettyplease" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn 3.0.9", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ "proc-macro2", "quote", ] [[package]] -name = "proc-macro-error3" -version = "3.1.1" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ - "proc-macro-error-attr3", + "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] name = "proc-macro2" -version = "1.0.107" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quinn" -version = "0.11.12" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -1659,7 +1624,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.21", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -1667,13 +1632,13 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.19" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.4.3", + "getrandom 0.4.2", "lru-slab", "rand", "rand_pcg", @@ -1682,7 +1647,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.21", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -1690,27 +1655,33 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.16" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.47" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1719,12 +1690,12 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.3" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.3", + "getrandom 0.4.2", "rand_core", ] @@ -1754,9 +1725,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.13.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1766,9 +1737,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.18" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1777,15 +1748,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.11" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.5" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -1836,9 +1807,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.3" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -1851,9 +1822,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "once_cell", @@ -1865,9 +1836,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.5" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -1877,9 +1848,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.1" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -1887,9 +1858,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.7.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation", "core-foundation-sys", @@ -1908,15 +1879,15 @@ dependencies = [ [[package]] name = "rustls-platform-verifier-android" -version = "0.2.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -1926,9 +1897,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -1960,7 +1931,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.2", + "bitflags 2.11.1", "core-foundation", "core-foundation-sys", "libc", @@ -1985,9 +1956,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.229" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -2006,29 +1977,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.229" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.229" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] name = "serde_json" -version = "1.0.154" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2050,20 +2021,20 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.21" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] name = "serde_spanned" -version = "1.1.2" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -2100,7 +2071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", - "cpufeatures 0.3.1", + "cpufeatures 0.3.0", "digest 0.11.3", ] @@ -2122,15 +2093,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.10" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simd_cesu8" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" dependencies = [ "rustc_version", "simdutf8", @@ -2162,15 +2133,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.3" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.5" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2179,8 +2150,7 @@ dependencies = [ [[package]] name = "spin-macro" version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8a641da857d4e7cada31a853c40180eeb10485a0f2f0282d0a7e5a2a4105d4" +source = "git+https://github.com/stackpop/spin-rust-sdk.git?rev=e65ff90dadf4173b08b14194e878ad3acd555337#e65ff90dadf4173b08b14194e878ad3acd555337" dependencies = [ "proc-macro2", "quote", @@ -2190,8 +2160,7 @@ dependencies = [ [[package]] name = "spin-sdk" version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7a3d9a6805ccdb4d404e91ccf7198ac5e486b6199dc9387aeebaa368272112" +source = "git+https://github.com/stackpop/spin-rust-sdk.git?rev=e65ff90dadf4173b08b14194e878ad3acd555337#e65ff90dadf4173b08b14194e878ad3acd555337" dependencies = [ "anyhow", "bytes", @@ -2200,8 +2169,8 @@ dependencies = [ "http-body", "http-body-util", "spin-macro", - "thiserror 2.0.21", - "wasip3", + "thiserror 2.0.20", + "wasip3 0.9.0+wasi-0.3.0", ] [[package]] @@ -2216,13 +2185,34 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + [[package]] name = "strum" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ - "strum_macros", + "strum_macros 0.28.0", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -2287,13 +2277,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.14.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] @@ -2307,11 +2297,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.21" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.21", + "thiserror-impl 2.0.20", ] [[package]] @@ -2327,9 +2317,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.21" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", @@ -2338,11 +2328,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.55" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", "libc", "num-conv", "num_threads", @@ -2354,15 +2345,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.9" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.32" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -2370,9 +2361,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -2380,15 +2371,24 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.13.3" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.3" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -2402,20 +2402,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] name = "tokio-rustls" -version = "0.26.6" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2423,9 +2423,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.8+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -2438,27 +2438,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.2+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.1.5+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.1.3+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -2482,7 +2482,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.2", + "bitflags 2.11.1", "bytes", "futures-util", "http", @@ -2552,9 +2552,9 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" -version = "1.0.27" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-xid" @@ -2603,12 +2603,13 @@ dependencies = [ [[package]] name = "validator_derive" -version = "0.20.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240e4b81c20a1d6d50d1d7265c658dfbd204e8b9ac4d80f3c931f39462196335" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" dependencies = [ "darling", - "proc-macro-error3", + "once_cell", + "proc-macro-error2", "proc-macro2", "quote", "syn 2.0.119", @@ -2647,24 +2648,33 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.4+wasi-0.2.12" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen 0.57.1", ] [[package]] name = "wasip3" -version = "0.7.1+wasi-0.3.0" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasip3" +version = "0.9.0+wasi-0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e9ed91ee5a995b6fe16c89d4511a8c12eb76967be20b57557618e02e781bcc" +checksum = "f1d5749fdf69bb9400562eba50f8f25f5cd800ef4b74300038b6f99ae7409674" dependencies = [ "bytes", "http", "http-body", - "thiserror 2.0.21", - "wit-bindgen 0.57.1", + "thiserror 2.0.20", + "wit-bindgen 0.62.0", ] [[package]] @@ -2725,31 +2735,53 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.247.0" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.259.0", ] [[package]] name = "wasm-metadata" -version = "0.247.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7bf119eb01f4a246864c10bd87c8f26c566350abb25aa4d5273c415bbab338" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.259.0", + "wasmparser 0.259.0", ] [[package]] name = "wasm-streams" -version = "0.7.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ce9bdac782bbca70d6ac48a3ba18b873c848a706d6dc3bd9a03c1b96f04693" +checksum = "e7d3be8814f5ba5f074491a469eed3d73c273ffad955f25ed1635efac4b0d269" dependencies = [ "futures-util", "js-sys", @@ -2760,12 +2792,24 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.247.0" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" dependencies = [ - "bitflags 2.13.2", - "hashbrown", + "bitflags 2.11.1", + "hashbrown 0.17.1", "indexmap", "semver", ] @@ -2873,7 +2917,16 @@ 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]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -2891,14 +2944,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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -2907,53 +2977,101 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "wit-bindgen" @@ -2961,7 +3079,8 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "bitflags 2.13.2", + "bitflags 2.11.1", + "wit-bindgen-rust-macro 0.51.0", ] [[package]] @@ -2970,80 +3089,149 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" dependencies = [ - "bitflags 2.13.2", + "bitflags 2.11.1", +] + +[[package]] +name = "wit-bindgen" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cb4b5556c3a791e86838ea287782bdafa704d55b0e68b5b81a3a16b9ea5f4b" +dependencies = [ + "bitflags 2.11.1", "futures", - "wit-bindgen-rust-macro", + "wit-bindgen-rust-macro 0.62.0", ] [[package]] name = "wit-bindgen-core" -version = "0.57.1" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" +checksum = "72ad22a37ecbc0e1fdca34d2b670ba41368cb0920735643e00fdf39a16ee5431" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.259.0", ] [[package]] name = "wit-bindgen-rust" -version = "0.57.1" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", "indexmap", - "prettyplease", + "prettyplease 0.2.37", "syn 2.0.119", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "wasm-metadata 0.244.0", + "wit-bindgen-core 0.51.0", + "wit-component 0.244.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ba5213d4332c260a0d4e69e34b0f7e4b85ddae10b6ebfbac00d9c185ba628a" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease 0.3.0", + "syn 3.0.9", + "wasm-metadata 0.259.0", + "wit-bindgen-core 0.62.0", + "wit-component 0.259.0", ] [[package]] name = "wit-bindgen-rust-macro" -version = "0.57.1" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9237d678e3513ad24e96fe98beacdc0db6405284ba2a2400418cf0d42caa89" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ "anyhow", - "prettyplease", + "prettyplease 0.2.37", "proc-macro2", "quote", "syn 2.0.119", - "wit-bindgen-core", - "wit-bindgen-rust", + "wit-bindgen-core 0.51.0", + "wit-bindgen-rust 0.51.0", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f19d63da8e478a370ef8192d90ccb517965b7747d6b46cd2d831dd0ef4d2541" +dependencies = [ + "anyhow", + "prettyplease 0.3.0", + "proc-macro2", + "quote", + "syn 3.0.9", + "wit-bindgen-core 0.62.0", + "wit-bindgen-rust 0.62.0", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata 0.244.0", + "wasmparser 0.244.0", + "wit-parser 0.244.0", ] [[package]] name = "wit-component" -version = "0.247.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" +checksum = "092f783dee3253265fcde131475055f8c275ac0176a9c0396db1dc7b5544bfb4" dependencies = [ "anyhow", - "bitflags 2.13.2", + "bitflags 2.11.1", "indexmap", "log", "serde", "serde_derive", "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "wasm-encoder 0.259.0", + "wasm-metadata 0.259.0", + "wasmparser 0.259.0", + "wit-parser 0.259.0", ] [[package]] name = "wit-parser" -version = "0.247.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", - "hashbrown", "id-arena", "indexmap", "log", @@ -3052,14 +3240,33 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6390f4f02493ce678d509c859cf1698e38929fb75c8012a6ee2f7d6b6cb66cd7" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser 0.259.0", ] [[package]] name = "worker" -version = "0.8.7" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057bce8ec66c80b7e270d884c312e2a4ff60eb997fd02936656106c06a964e5" +checksum = "2f8adbf6c9ae45b665dee995c5e3a342c2bd7d58a2e8ca5c75b50ce8b1b8bfd9" dependencies = [ "async-trait", "bytes", @@ -3075,7 +3282,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "serde_urlencoded", - "strum", + "strum 0.27.2", "tokio", "url", "wasm-bindgen", @@ -3095,7 +3302,7 @@ dependencies = [ "async-trait", "proc-macro2", "quote", - "strum", + "strum 0.28.0", "syn 3.0.9", "wasm-bindgen", "wasm-bindgen-macro-support", @@ -3116,15 +3323,15 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.4" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3133,13 +3340,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", "synstructure", ] @@ -3154,27 +3361,27 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.8" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.9.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3183,9 +3390,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.8" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3194,23 +3401,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.6" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] -[[package]] -name = "zlib-rs" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" - [[package]] name = "zmij" -version = "1.0.23" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/tests/fixtures/reusable-app/Cargo.toml b/tests/fixtures/reusable-app/Cargo.toml index 471f947db..e4e3e4010 100644 --- a/tests/fixtures/reusable-app/Cargo.toml +++ b/tests/fixtures/reusable-app/Cargo.toml @@ -15,7 +15,9 @@ edgezero-adapter-cloudflare = { path = "../../../crates/edgezero-adapter-cloudfl edgezero-adapter-spin = { path = "../../../crates/edgezero-adapter-spin", features = ["spin"] } edgezero-adapter-axum = { path = "../../../crates/edgezero-adapter-axum" } fixture-core = { path = "crates/fixture-core" } -fastly = "0.13" +fastly = "=0.13.1" +worker = { version = "=0.8.5", default-features = false, features = ["http"] } +spin-sdk = { version = "=7.0.0", git = "https://github.com/stackpop/spin-rust-sdk.git", rev = "e65ff90dadf4173b08b14194e878ad3acd555337", default-features = false } futures = "0.3" serde_json = "1" log = "0.4" diff --git a/tests/fixtures/reusable-app/README.md b/tests/fixtures/reusable-app/README.md index 4b03747a3..377d9e7af 100644 --- a/tests/fixtures/reusable-app/README.md +++ b/tests/fixtures/reusable-app/README.md @@ -4,19 +4,645 @@ This standalone Cargo workspace tests provider HTTP behavior without changing normal generated entry points. Run from the repository root: ```sh -cargo test --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-harness -p fixture-core +cargo test --locked --manifest-path crates/edgezero-test-harness/Cargo.toml +cargo test --locked --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-core ./scripts/smoke_test_reusable_app.sh --adapter fastly --suite smoke ./scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite smoke ./scripts/smoke_test_reusable_app.sh --adapter spin --suite smoke ./scripts/smoke_test_reusable_app.sh --adapter axum --suite smoke ``` -The driver is the native Rust `fixture-harness` crate. Its HTTP clients, controlled +The driver is the standalone native Rust `edgezero-test-harness` crate under `crates/`. +It has its own workspace and lockfile, separate from the runtime and fixture applications. +Its HTTP clients, controlled loopback backend, process management, and evidence checks run outside the WASM applications. The shell script only launches it through Cargo. +## Outbound Cutover Gates + +### Dispatch-Slack Retirement (2026-10-27) + +The requester authorized removing the fixed 25 ms dispatch-slack guard and its test injection +helper. This supersedes the older slack-unchanged and slack-pending statements below, not their +historical measurements or failed observations. Original absolute deadlines, timeout attribution, +late-result rejection, B31 backend/timer policy and resource limits remain in force. Synchronous +SDK host calls remain non-preemptible; no capability level, blocking horizon or qualification is +promoted. EZ-3 inventory legs remain planned until real commands and sentinels exercise their full +assertions. Criterion text, source digests and historical evidence are unchanged. + +### Scoped Timing and Integration Override (2026-10-24) + +The requester's [scoped approval](../../../docs/audits/2026-10-18-outbound-api-approval.md#scoped-timing-and-native-integration-amendment) +permits the [D1d Send-first checkpoint](../../../docs/superpowers/plans/2026-10-18-trusted-server-outbound-03-drivers.md#send-first-checkpoint-2026-10-24), +not a qualification pass. The +5 ms diagnostic windows/tests remain measured qualification +targets, not hard scheduler guarantees. Keep every late/error observation and historical exit +unchanged; timing-only G2 failure no longer prohibits this scoped implementation. The optimizer +synchronous blocking horizon is explicitly **UNKNOWN** until provider evidence, not inferred from +timers or a Viceroy rejection. Hosted/performance qualification stays blocked. + +The checkpoint uses the current trait and existing `execute`/`process_response`, leaving the +ordinary SDK send branch unchanged. Full EZ-1 settlement, Follow, engine and atomic trait migration +remain open. The later [scoped B31 approval](../../../docs/audits/2026-10-18-outbound-api-approval.md#scoped-b31-implementation-approval) +implements only the existing aggregate backend identity/timer policy; slack, readiness and resource gates remain open; +typed body-error preservation and fresh-readiness/subsequent-read safety remain requirements. +This override governs older implementation-stop wording below without changing diagnostic +acceptance tests, 255 criteria/511 legs, IDs, statuses, applicability or comparison requirements. + +### Focused Assembly Checks + +```sh +scripts/smoke_test_reusable_app.sh --adapter fastly --suite assembly +scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite assembly +scripts/smoke_test_reusable_app.sh --adapter spin --suite assembly +``` + +This local-only suite exercises the standard entrypoints and the same app-free canonical +error delivery used by custom retention recipes. It checks typed JSON/status/retry headers, +HEAD suppression, internal redaction and one full boot log per failed attempt. Retained +503-to-200-to-200 cases require two builds, shared instance identity and persistent successful +state. Fastly additionally checks configure-failure state release and detached commitment +reports. Instance turnover never counts as a retry pass. Axum's distinct +fail-before-bind behavior and boot diagnostics are covered by its native startup tests. + +Use the explicit Cloudflare tool overrides documented below. Runtime processes, compiled +artifacts and JSONL evidence are scoped to the run directory. Results are working-tree +characterization, not hosted qualification; this suite neither changes capability levels +nor closes the separate Fastly readiness/scheduling gate. + +### Qualification Inventory + +Start with the implementation plan's +[current delivery dashboard](../../../docs/superpowers/plans/2026-10-18-trusted-server-outbound-implementation.md#current-delivery-dashboard-2026-10-27) +for shipped checkpoints, unfinished implementation and blockers. Its requirement table tracks +task-level progress; this inventory tracks criterion/environment execution readiness and the +qualification checker validates separate evidence. These are not interchangeable completion claims. + +The acceptance inventory is `outbound-cutover-cases.json`. It records acceptance criteria, +including the unnumbered lists and consumer end-to-end prose checks, expanded into individual +environment legs. Source and criterion SHA-256 digests require explicit review when the +requirements change. A split criterion retains its source ID and adds a letter to its case ID; +cache parity and fresh-execution pooling are separate cases, not one passing result. +The checker's reviewed leg-set digest also rejects deleted environment/split legs. Supplemental +template/demo checks cite normative EZ-24.12 directly rather than reusing a consumer assertion. + +```sh +node --test scripts/check_outbound_cutover_evidence.test.mjs +node scripts/check_outbound_cutover_evidence.mjs --manifest +node scripts/check_outbound_cutover_evidence.mjs --help +``` + +Run these commands from the repository root. Manifest validation accepts pending decisions and +planned commands; it is not qualification. Per-case sentinels and `outbound-cutover` commands are +implementation targets, not existing passing tests. `consumer-runbook` and `hosted-runbook` denote +external execution recipes whose owners must replace them with exact runnable commands before +setting a leg to `ready`. No staging deployment or approval is inferred from this inventory. + +Specifically, `implementation: planned | ready` means the declared leg's command/sentinel is +planned or implemented and executable with its full assertions. It does not mean the corresponding +feature is absent or qualified. EZ-23 local criteria 01-11 bind guarded Cargo tests and the +existing Axum CI job. EZ-9 core criteria 01-15 bind all 51 redirect tests through one inventory +guard, and runtime criteria 16/17 bind the complete four-adapter wire matrix. There are 32 ready +legs and 479 planned. EZ-23 criteria 12/13 stay planned for their begun-request and consumer-owned +gaps. G1/Q3 must still reconcile the other executable bindings. +A `ready` leg also needs applicable, passing exact-candidate evidence before release credit; +do not promote it solely from a checkpoint test count or green CI. + +The EZ-23 ready legs share one full Axum suite command, including ignored tests: + +```sh +scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets +``` + +The EZ-9 core legs likewise discover every bound primary/additional sentinel before running: + +```sh +scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests +``` + +Runtime commands select `--suite outbound-wire` and write their JSONL to +`.runs/outbound/redirect/{environment}/events.jsonl`. Each adapter must complete 68 exchanges and +84 ordered origin receipts. Idle-body policy remains an unadopted conditional leg; these bindings +do not qualify that policy, native optimizer transformation or consumer/hosted behavior. +Axum, Fastly and Spin execute these bindings in their existing CI jobs. Workerd's command is +locally executable; automatic CI runtime activation remains pending explicit setup of its +matching build tools and selected runtime. Green CI alone does not cover that workerd leg. + +The existing guard asks the evidence checker for every ready sentinel in that environment and +requires exact discovery of all twelve names before executing the suite once. Criterion 10's +`additional_sentinels` binds its factory test alongside its primary TCP test; no separate CI list +or invocation is needed. Missing tests or an empty ready selection fail before execution; test +failures retain their exit status. Their inventory `.log` paths are future raw Cargo output artifacts, +not JSON result exports. At the frozen candidate, capture stdout and stderr, retain the guarded +exit status and bind the source/build identities through Q3's record assembly. Current local/CI +runs are not a qualification record, and executable follow-ups are not automatically promoted or +confirmed present. Other `outbound-cutover` placeholders remain implementation targets. + +The [TS qualification runbook source template](ts-outbound-qualification-runbook.md) separates +implemented preparation/egress commands from missing full-outbound execution/export bindings. +It is unaccepted and unqualified. Q3 must freeze the candidate before Q4a fills an external copy; +actual TS ownership acknowledgement, staging authorization and package acceptance remain open. + +Release validation is a different, fail-closed command: + +```sh +node scripts/check_outbound_cutover_evidence.mjs --release \ + --candidate <40-character-source-SHA> --record RECORD.json --artifacts EVIDENCE_DIRECTORY +``` + +The record must contain the same candidate SHA in every required result, exact command/sentinel/ +source digest/artifact path, positive sample counts, passing assertions and numeric measurement +bounds. Each artifact is checked against its SHA-256 under the evidence directory. An explicit +applicability entry is required for every leg. Deferred follow-ups name the unmet condition or an +attributed, leg-scoped approval; a sibling approval never adopts an entire requirement range. +Blocking work cannot be skipped because it is unimplemented or lacks an entitlement. + +The record also names the pinned toolchain, workerd revision, runner image and baseline/candidate +comparison identities where required. Retention must be a non-expiring repository or release asset +for the lifetime of the pin, not a CI artifact. This validator checks recorded facts and hashes; +it does not authenticate telemetry, approvers, deployment identities or continued remote retention. +Independent owner review remains necessary. + +#### Mandatory Forwarding Example Doctest + +Blocking EZ-1.14 has a supplementary compile-only prerequisite owned by Q1 (checker) and +Q2 (workspace CI). Run from the repository root: + +```sh +node --test scripts/check_forwarding_example.test.mjs +node scripts/check_forwarding_example.mjs +``` + +The checker extracts the actual `Forward An Inbound Request` fence in `docs/guide/proxying.md`, +builds core offline/locked and requires exactly one listed and passed non-ignored rustdoc test. +It compiles item definitions with an empty entrypoint; the network handler is never invoked. +Missing/duplicate fences, weakening annotations, zero tests and compile failures fail the gate. +Regression tests include the missing `into_request()?` mutation and embedded-fence bypasses +through real compilation. + +This CI prerequisite is outside the reviewed 511 environment legs. It grants no runtime +forwarding acceptance and does not adopt optional EZ-25.42's broader guide checker. P2's real +GET/HEAD/POST behavior tests and provider/consumer qualification remain separate requirements. + +### Pre-Freeze Evidence Assembly + +Q3 preparation uses the existing checker rather than a second exporter: + +```sh +node scripts/check_outbound_cutover_evidence.mjs --assemble \ + --candidate SOURCE_SHA --input OBSERVATIONS.json --artifacts EVIDENCE_DIRECTORY \ + --output NEW_DRAFT.json +node scripts/check_outbound_cutover_evidence.mjs --report \ + --candidate SOURCE_SHA --record NEW_DRAFT.json --artifacts EVIDENCE_DIRECTORY +scripts/run_test_nonzero.sh outbound_evidence_assembly_retains_case_counts_and_hashes cargo test --offline --locked --manifest-path crates/edgezero-test-harness/Cargo.toml outbound_evidence_assembly_retains_case_counts_and_hashes +``` + +Both assembly and report exit nonzero while release gates are blocked. Successful draft creation +is identified by `record_output` in the report and a new output file; malformed input or an +existing output path produces an error instead. Assembly never overwrites evidence. + +Inputs use schema 1 with an explicit `candidate_sha`, `source_state` (`clean` or `working-tree`), +nonempty `builds` and nonempty `results`. Each build supplies its candidate SHA, relative +`artifact_path` and `build_id`, the SHA-256 of that file. Each result supplies its exact inventory +case/environment, candidate SHA, linked `build_id`, source digest, command, sentinel, artifact +path, status (`passed`, `failed` or `pending`), sample count, assertions and measurements. +The tool verifies build hashes and computes raw artifact hashes; supplied hashes must agree. +It preserves observation statuses and sample counts, including failed or pending observations, +without filling absent results, changing applicability or approving a deferred part. + +Drafts always have `stage: "pre-freeze"` and `qualified: false`. Runner, toolchain, retention, +comparison and applicability metadata is copied only if supplied; missing fields remain gates. +Working-tree records and explicit `qualification: false` cannot pass release validation. +Release records require `stage: "frozen-candidate"`, `source_state: "clean"` and linked build +identities. These are declared provenance, not proof that a binary was built from the claimed +SHA. Candidate freeze, runtime mapping, approvals and independent evidence review remain required. +Changing a draft's label does not perform those steps. + +For a leg marked `comparison: true`, release/report validation requires comparison build +identities and a nonempty valid measurement list. The hosted Image Optimizer leg is marked this +way. This is a basic evidence-presence check, not proof that the required controls, groups, +sampling or acceptance rules were approved or completely exercised. Those still require the +paired native-path record and independent review; arbitrary measurement names do not certify parity. + +`--report` lists independently evaluable blockers by leg/rule using the same checks as +`--release`, which still fails closed. Invalid or ambiguous entries receive no acceptance credit +without hiding unrelated observations. Assembly does not interpret generic smoke JSONL as +qualification results or substitute FNV fingerprints for SHA-256. All 511 reviewed inventory +legs retain their existing state. No source freeze or hosted qualification is performed here. + +Use stable artifact snapshots: the tool rejects absolute/traversing paths, symlink escapes, +directories and nonregular files. Artifacts are capped at 256 MiB and hashed with a 64 KiB buffer; +JSON inputs/outputs are capped at 16 MiB. It is not a defense against concurrently mutating a +filesystem, and neither verifies telemetry authenticity nor publishes artifacts. + +### Fastly Client Progress + +```sh +VICEROY_BIN=/absolute/path/to/repaired/viceroy scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-client-progress +``` + +This functional CI suite exercises the actual `FastlyOutboundClient` through `HttpClient`, using +the same guest, bounded origins and validators as the full diagnostic below. Four cases run +three times: pending-head/ready-body, pending-body/delayed-head, and buffered/streamed multi-buffer +truncation. Require twelve probes and eighteen correlated origin requests, exact payload bytes +and checksums, preserved typed source errors, and the ready sibling observed before its cutoff. +Preflight failures cannot count as exercised receive paths. + +The same suite also requires a separate P5 inventory: twenty-three redirect probes and +forty-three correlated origin requests. Plain Send stays unfollowed; Follow reaches the second +origin. Buffered and streamed no-redirect controls compare identity, decoded gzip and unsupported- +token passthrough under normal decoding, including exact-cap, overflow and declared-length cases. +A gated pending oversized compressed redirect progresses without origin body data; stopping at +that head applies the ordinary typed cap. Native optimizer Send, initial Follow and a policy- +supplied later hop reach Viceroy's typed unavailable boundary. These are compositional dispatch +controls, not successful optimized-first-hop redirect, transformation or performance evidence. + +Both receive suites require `--adapter fastly` and the pinned runtime version. Select the +[tracked Viceroy repair](../../../patches/viceroy/README.md) explicitly; an installed stock binary +is not silently replaced. CI requires exactly exit 2, both passing complete client/P5 inventories, +an explicit unverified disposition and no failure records. Exit 0 or an unavailable runtime +does not satisfy that gate. The suite deliberately excludes SDK timing probes and timer controls. +It does not establish callback-wide progress, host cancellation, memory bounds or qualification. + +### Fastly Readiness Diagnostic + +```sh +scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-readiness +``` + +The SDK portion of this opt-in suite uses direct pinned SDK/ABI calls in the diagnostic fixture. Eleven cases +cover silent heads, pending/trickled bodies, sibling readiness, exhausted bodies, ordinary/empty +EOF, truncation, malformed heads/chunks and connection closure before a response. Each runs with +buffered and pull-streamed consumption, read capacities 1 and 16384, and three repetitions. +Three additional finite direct-read truncation controls produce 135 probes and 147 origin requests. +Those capacities are diagnostic variations, not an approved production read maximum. + +The full command also runs the twelve actual-client cases above. In total, its HTTP inventory +contains 165 probes and 195 origin requests; the SDK baseline and supplementary probes remain +separately validated. Ordinary CI gates only the explicit functional suite. The full diagnostic +retains its strict timing assertions and exit behavior; timing misses remain failures, not a +passing SDK result. It remains required evidence for applicable qualification work. + +The same command appends 18 supplementary probes and 30 origin requests, separately validated +from the unchanged historical matrix. `mixed-head-body` completes a ready body while a sibling +never returns a head; `mixed-body-head` observes a delayed head while another body is pending. +`full-body-error` closes after 32769 of 32770 declared bytes. Both consumers run three repetitions +at capacity 16384. Checks require actual mixed selection domains, at least one full-capacity read, +exact payload checksums and a fresh matching selection for every read, including terminal errors. +This tests the candidate capacity with a multi-buffer payload, not general fairness when several +sources remain continuously ready or approval of a production maximum. Baseline and supplementary +failures are both collected before the run's result; known stock errors cannot skip the new cases. + +Before the HTTP matrix, timer-only controls run eight repetitions at 1/100/500 ms using +native thread sleep, guest WASI sleep and empty-handle timed ABI select: 72 observations. +Three one-millisecond warm-ups are recorded separately and excluded from those counts. +These controls send no outbound HTTP requests. Lateness flags are derived from measured +elapsed time and retained even when false; they do not change the 135-probe acceptance rules. +Timer-only measurements end at primitive return, before status formatting. Declared ABI +endpoints must agree with that timestamp and cannot report less than the positive timeout. +The two guest controls share Viceroy's asynchronous execution/timer machinery; native sleep +does not exercise its reactor. Differences narrow hypotheses, not prove a scheduling cause. + +Raw ABI entry/return timestamps are captured immediately around select, before status formatting. +The outer operation and whole-exchange intervals still include guest overhead and enforce the +same deadline. Paired raw timestamps must lie within their enclosing operation. Earlier retained +traces do not contain this additional instrumentation and are not rewritten. + +Every head is selected with a positive remaining timeout, then nonblocking-polled. Every selected +body read requires fresh same-slot readiness. One original 500 ms absolute deadline survives the +stream handoff; trickle never resets it. Traces must finish within +5 ms, retain exact EOF/error +outcomes, and cover the complete inventory with unique correlated origin receipts. Streamed +consumption discards each pulled chunk; buffered consumption retains it under a finite byte cap. +The direct control alone bypasses body readiness against a source that closes after 50 ms; +it is never a production fallback. Backend phase timers are fixed at two seconds, outside the +measured exchange window; backend registration is not timed by this proof. + +Each origin owns at most two socket workers, a 4096-byte request-head cap and a three-second +watchdog. Finish/Drop wakes, closes and joins its workers, retaining errors and cleanup reasons. +Harness-forced cleanup is not SDK cancellation evidence. A stuck guest is contained by the +12-second client timeout and runtime process-group cleanup, not counted as successful cancellation. + +Pinned Viceroy 0.21.0 loses source errors on the readiness path and fails validation. The suite +is separate from normal smoke/unit gates. Even a complete primitive matrix exits 2: migrated +production paths, exact-version review, backend policy and hosted qualification remain separate. +Working-tree characterization is labelled as such, never as exact-commit release evidence. +See the [feasibility report](../../../docs/audits/2026-10-18-outbound-feasibility.md). + +PR #275 also carries an [exact-version local Viceroy repair](../../../patches/viceroy/README.md). +Select its built executable explicitly with `VICEROY_BIN`; the default installed runtime and +version pins are not changed. The separate repair record retains three failing stock controls +and three repaired comparisons preserving UnexpectedEof with identical guest bytes. A repaired +run still exits 2 because full G2/hosted proof is pending; do not treat it as a full gate pass. + +### Scheduling and Empty-Body Characterization + +```sh +scripts/smoke_test_reusable_app.sh --adapter all --suite outbound-progress +scripts/smoke_test_reusable_app.sh --adapter all --suite smoke +``` + +### Buffered Response Framing + +```sh +scripts/smoke_test_reusable_app.sh --adapter all --suite response-framing +``` + +This separate suite reuses the progress artifacts and production entrypoints without changing +the outbound scheduling/coding case counts. It requires curl; Fastly additionally requires an +HTTP/2-enabled build for prior-knowledge h2c. Bounded captures record protocol, headers and body +artifacts for literal buffered, empty, explicit-length, stream, suppressed and fallback sources. +Fastly compares framing against a direct pinned-SDK Automatic reference for the same buffered +sources over HTTP/1.1 and HTTP/2. The expired case uses a response-owned absolute deadline. +Each case is attempted independently; a failed case does not hide later observations. +HEAD captures are header-only and report payload observation as unavailable, not a measured empty +body. Failed curl requests retain bounded diagnostics and partial capture artifacts. Short sources +require a terminal HTTP/1 partial-transfer/empty-reply or HTTP/2 stream-error witness; a client +timeout is not a passing abort. Both transfers must succeed before an SDK parity case can pass. + +The pinned local SDK's bodyless controls currently fail: restoring a known HEAD body breaks +HTTP/2, forbidden-status SDK payloads are not consistently suppressed, and representation framing +differs from strict suppression. Fastly and `--adapter all` therefore remain nonzero. These failed +controls are retained as open parity gates, not normalized away or inferred to be hosted behavior. + +These are working-tree local characterization runs. They do not qualify hosted Fastly compression, +deployed Workers header delivery, consumer response parity, or Fastly outbound G2/P1. Hosted +compression still needs encoded wire counts, coding and decompressed source hashes for buffered +and declared-stream sources on both HTTP versions; Viceroy does not supply that transformation. + +### Bounded Synthesized Headers + +```sh +scripts/smoke_test_reusable_app.sh --adapter all --suite synthesized-headers +``` + +This independent suite uses the same production progress entrypoints with suite-only application +configuration. It delivers an entire 32-value/4096-byte configured set: one fixture marker and +31 distinct `Set-Cookie` values, verified in order without comma splitting. GET/HEAD fallbacks +retain the canonical 500 body/suppression behavior. Canonical 404/405 responses are decorated; +matched successes/errors and actual admission refusals are not. The 405 retains its single `Allow`. + +Viceroy additionally receives an absolute target over 9 KiB and about 72 KiB of headers in 60 added +lines, below its parser boundaries but above EdgeZero's normalized limits. A correlated detached +decision and exactly one terminal completion prove guest-side 414/431 generation, with no +admission, handler or finalize invocation. Fastly preparation controls replace/clear the app set +or reject a 4097-byte dynamic set without losing the valid app policy. Final logs are rechecked +after runtime teardown; fallback reports must identify conversion failure, the fallback body, +completed disposition and exact GET/HEAD byte counts. + +Raw HTTP/1 captures require a complete head and clean EOF, not a timeout or reset. HEAD checks +all post-header octets, including bytes sent with the head or in a later write. The client uses +one absolute five-second exchange deadline and a 16 KiB head/4 KiB body capture envelope. +Cloudflare's local frontend requires a client write-half-close to end this controlled exchange; +the other three use `Connection: close` without an early FIN, which their parsers treat as +cancellation. The selected close mode is recorded. No production transport setting is changed. +Capture/artifact failures are recorded without skipping later cases. + +The [local record](../../../docs/audits/evidence/2026-10-23-synthesized-response-headers.json) +contains fingerprints for the passing matrix and retained failed diagnostic runs. +These are local HTTP/1 working-tree observations, not hosted/HTTP2/HTTP3 or frozen-candidate +qualification. The separate `assembly` suite installs policy before deliberate failure and checks +GET/HEAD exclusion, successful recovered-policy 404 delivery and correlated lifecycle evidence. +Axum instead proves its canonical pre-listener startup failure, without constructing a response. +The [assembly record](../../../docs/audits/evidence/2026-10-23-assembly-response-headers.json) +includes the repeated header matrix and final request-correlated assembly evidence. +Consumer-owned header integration remains open. Neither suite reruns nor waives the failed +Fastly SDK parity controls or G2, and does not establish Cloudflare overflow recovery. + +The default-off `progress` feature selects fixture-only SDK probes, independent of the hosted +`qualification` application. Smoke also builds and runs these probes after its existing cases; +their artifacts are staged separately. Eager single/batch SDK starts are compared with the +existing lazy public single/batch APIs during a measured 100 ms unrelated wait. Origin-local +receipt timestamps and consumer poll counts are checked without comparing guest/host clock +epochs. A response-owned stream must reach the client before the gated origin can complete. +Fastly's synchronous SDK dispatch and guest sleep are not a background guest executor proof. + +The Spin progress suite also runs `/progress/sdk-source-diagnostics`: eight SDK source-race +cases check EOF, pre-expiry, ready errors with/without provider detail and deadline equality. +It awaits the production I/O helper inside the existing async HTTP export; the synchronous WASI +test export cannot wait on SDK timers. The probe creates no downstream writer and does not qualify +hosted delivery or change capability claims. Its test utilities are enabled only by `progress`. + +`/progress/sdk-connect-fallback` uses that same async export for five Connect setter outcomes +and four aggregate budget sources. Its 60 checks cover ready success, a non-self-waking operation +released by the real SDK timer, and expired-entry rejection without polling. The captured guest +owner is released on timeout; no refusal detail becomes the later timeout's cause. The injected +timestamp stays frozen during the positive-remaining test, so a clock jump cannot supply progress. +This is guest-race evidence, not independently configured connect enforcement or host teardown. + +Each progress target also runs `/progress/framing-trailers` through its ordinary App/converter. +The client must receive the unchanged 500 fallback with no sentinel leakage in headers or body. +On Viceroy the harness additionally requires one `UnsupportedTrailers` terminal report and the +matching error-level fixed-rule log, rejecting leaked sentinel text anywhere in captured logs. +This is local framing-diagnostic evidence, not hosted response-lifetime qualification. + +Five raw HTTP/1 inputs exercise absent framing, explicit zero length, terminating chunks, and +nonempty fixed/chunked controls. Host snapshots do not consume or replace body data. Native raw +signals are unavailable at the existing entrypoint; Spin hints are explicitly not raw framing +evidence. HTTP/2/3, hosted behavior and provider scheduler-context rejection remain unverified. + +The empty probe transfers the Initial context body with `into_request()` before bounded +collection and reports `body_is_stream` separately from context state. Cloudflare must preserve +the original pre-conversion null-body snapshot as buffered empty for absent/zero-length framing, +and preserve every present source as streamed for chunked-empty and nonempty controls. The +validator rejects missing, mistyped or contradictory storage evidence; byte counts alone cannot +prove this property. First-`body_bytes` deadline precedence has separate adapter regressions. + +The initial local run passed on native/Viceroy but exposed a Spin response-owned egress defect. +Separate ready/timer-only adapter streams and SDK-native controls isolated early successful +host observation being mistaken for transport failure. After the internal fix, Spin passes +all five progress, five framing and four comparison cases. Token-correlated SDK terminal evidence +is required from canonical component stderr; missing, duplicated, failed or foreign completions +and guest panic records fail the check. It is revalidated after the finite pre-shutdown hold. +Host worker-panic text before controlled shutdown fails; post-shutdown observations are recorded +separately and are not proof of panic-free teardown. The original failed evidence remains retained; +these local cases do not qualify hosted Spin or weaken G2. A subsequent Cloudflare run with the +exact matching wasm-bindgen CLI passed 24 coding, 46 marker, five scheduling and five framing +checks on local workerd 2026-09-10. The framing checks require a pre-conversion boolean body +signal, not just matching byte counts. Coding/marker failures do not skip the independent progress +group; any failed group still fails the run. Compilation is not runtime proof. See the +[feasibility report](../../../docs/audits/2026-10-18-outbound-feasibility.md) for exact versions, +source identities, retained evidence and remaining hosted/relay limitations. + +Cloudflare builds require explicit absolute `WASM_BINDGEN_BIN`, `ESBUILD_BIN` and `WASM_OPT_BIN` +executable overrides. The wasm-bindgen version must match the locked crate. Preflight refuses +implicit tool downloads or dependency repins; missing tools produce unverified exit 2, not pass. +Actual failed cases produce exit 1, including when another adapter also has unavailable tooling. +The retained local run uses `WRANGLER_LOG_PATH` in its temporary tool directory and +`WRANGLER_SEND_METRICS=false`; Wrangler runs `dev --local` with loopback-only listening and local +fixture bindings. It neither deploys a Worker nor accesses production bindings. + +### Encoded Downstream Relay + +The same `outbound-progress` suite now adds 24 `relay-*` cases per adapter. They share the coding +probe's validated loopback target and outbound caps, disable only synthetic marker insertion on +the cloned client, and return `OutboundResponse::into_response()` directly. No body collection or +JSON report substitutes for downstream transmission. The TCP reader captures binary payloads +without UTF-8 conversion or content decoding, with a 256-byte capture cap. Tests cover exact-cap +and overflow for fixed, chunked and close-delimited payloads, plus fixed/chunked truncation. + +The matrix crosses gzip/deflate/Brotli, buffered/streamed output, Original/200, Original/206, +Decode/206 and Decode/200. The 18 raw cases retain original bytes and surviving representation +fields, including range metadata; six decode controls verify plain bytes. Wire framing cannot +contain conflicting Content-Length/Transfer-Encoding. Host chunking is allowed, and any observed +Content-Length must match the delivered bytes. + +Each runtime now runs 104 base checks and asserts 113 base origin receipts. Spin additionally runs +two SDK header controls and four response-stream comparison controls, producing 110 client +completions. Relay coverage retains its exact 24 ordered origin receipts. All 16 group-failure +combinations preserve independent execution and overall failure. A reader error remains fatal and +group-labelled, but may precede a per-case relay record. These local cases do not prove inbound +GET/HEAD forwarding, new eager/head-first APIs, hosted qualification or Fastly readiness. + +### Request Overflow Characterization + +```sh +scripts/smoke_test_reusable_app.sh --adapter all --suite outbound-overflow +``` + +This separate opt-in suite reuses the `progress` artifacts and standard entrypoints. Each target +attempts eleven cases: chunked ingress at 1023/1024/1025 bytes under a 1024-byte read cap, +buffered and streamed uploads at 7/8/9 bytes under an 8-byte cap, an ordinary upstream 413, +then the exact 2048-byte chunked ingress case required by EZ-10. The original ten-case order is +unchanged, including the overflow-to-next-upload recovery check; earlier failures do not skip +the appended case. +Uploads use the injected production client, not the scheduling probes' direct SDK requests. +Only response heads are observed; bodies are dropped unpolled. This neither enters Fastly's +failed readiness path nor qualifies downstream relay or hosted behavior. + +Generated overflow must return the fixed 413 JSON category without Retry-After; an ordinary +upstream 413 must retain its status and `Retry-After: 120` in the head report. Buffered overflow +must not contact the origin on any adapter; streamed overflow must not contact it on Axum or +Cloudflare. Fastly/Spin may instead close an incomplete upload. An explicitly observed zero-byte, +pre-head EOF/reset is also recorded as TCP contact, not "no contact"; a missing receipt or partial +head is not equivalent. Complete request framing, malformed fields, timeout, duplicate contact +or over-cap bytes cannot pass as an abort. + +The dedicated origin has one joined worker, two retained connection receipts plus one overflow +witness, a 256-byte body capture plus one witness, 16 KiB combined head/chunk/trailer metadata +and 100 head fields. Each accepted socket has a two-second absolute read/write budget. +Collection observes a further 100 ms before stopping new accepts and joining the worker; up to +two already accepted socket budgets may remain. This is a finite observation, not proof that +provider work can never resume. Client captures are bounded to 4 KiB and reads to six seconds. +First-byte timing is explicitly unmeasured; header/completion times use the client clock. + +The retained local matrix passes all eleven cases on Axum, Viceroy and Spin. The exact 2048-byte +ingress case passes on all four targets. On pinned local +Wrangler/Miniflare/workerd, ingress overflow returns 413, but the following valid POST returns a +proxy-generated 500 before fixture entry. A JavaScript-only body-cancellation control reproduces +the follow-on failure without EdgeZero; the native connection-pool mechanism remains unproven. +The suite retains the failure and exits 1, without retries, recovery sleeps, drains or capability +promotion. See [the original evidence](../../../docs/audits/evidence/2026-10-21-outbound-overflow.json) +and [the boundary/recovery checkpoint](../../../docs/audits/evidence/2026-10-21-overflow-boundaries.json). +Omitting `Connection: close` does not eliminate the failure. Diagnostic requests sent directly +to Wrangler's own user-worker listener recover for both the JavaScript control and the actual +EdgeZero artifact. This narrows the failing boundary to the front-proxy path, not a proven native +connection-pool mechanism. That diagnostic topology is not a replacement acceptance entrypoint. +The collector's Drop test gates worker exit and mutation-checks that Drop waits for its join; +it does not certify provider cancellation after the finite observation window. +Cloudflare recovery, hosted HTTP/2 and end-to-end qualification remain open. The separate +[generated-app/demo TCP proof](../../../docs/audits/evidence/2026-10-21-generated-fallback.json) +passes through their unchanged Axum entrypoints: fixed/chunked 4 KiB boundaries on both fallback +routes, exact 413/no-retry output, 405 Allow, the five-second pending-read 408, and a request-looking +body tail sent only after receiving the 413, followed by genuine health/recovery requests. +Its bounded buffered-response reader uses `httparse`; malformed, ambiguous or incomplete framing +cannot pass as connection-close recovery. These are local HTTP/1 observations, not hosted lifecycle +or full resource certification. The existing generated-workspace and bundled-demo CI steps run them. + +## Opt-in Hosted Response Qualification + +The default-off `qualification` feature selects a synthetic application through the standard +entrypoints. Use only a designated isolated nonproduction service with complete unsampled logging +at `Info` or above, including the `edgezero_qualification` target. No production stores, traffic, +credentials or log exports should be reused. Without this feature, normal fixture applications +remain selected; smoke separately builds the `progress` probes described above. + +Compile from a clean committed checkout with recorded build identity: + +```sh +export EDGEZERO_QUALIFICATION_REVISION="$(git rev-parse HEAD)" +export EDGEZERO_QUALIFICATION_ARTIFACT="$(shasum -a 256 tests/fixtures/reusable-app/Cargo.lock | awk '{print $1}')" +cargo build --locked --release --manifest-path tests/fixtures/reusable-app/Cargo.toml \ + -p fixture-cloudflare --features qualification --target wasm32-unknown-unknown +cargo build --locked --release --manifest-path tests/fixtures/reusable-app/Cargo.toml \ + -p fixture-fastly --features qualification --bin fixture-fastly-qualification --target wasm32-wasip1 +cargo build --locked --release --manifest-path tests/fixtures/reusable-app/Cargo.toml \ + -p fixture-spin --features qualification --target wasm32-wasip2 +cargo build --locked --release --manifest-path tests/fixtures/reusable-app/Cargo.toml \ + -p fixture-axum --features qualification +``` + +Deploy the selected artifact through the platform's normal isolated-service path. Record the +actual binary SHA-256 and deployment identity after build. `artifact` below is the compiled +lockfile/build-input digest, not the binary hash: a binary cannot embed its own hash. Record an +unavailable runtime revision as a reasoned unknown, not an invented version. + +Prepare a target JSON object using actual recorded identities: + +```json +{ + "origin": "https://isolated-qualification.example.com", + "allowed_origin": "https://isolated-qualification.example.com", + "isolated_nonproduction": true, + "run": "qualification-1", + "adapter": "cloudflare", + "revision": "<40-character committed source SHA>", + "artifact": "<64-character compiled build-input SHA-256>", + "binary_sha256": "<64-character deployed artifact SHA-256>", + "sdk": "worker 0.8.5", + "runtime": { "unknown": "provider-unpublished" }, + "compatibility": { + "date": "", + "flags": ["enable_request_signal", "enable_weak_ref"] + }, + "deployment": "", + "max_probe_ms": 2000 +} +``` + +Other adapters use their actual SDK name/version and a compatibility object with a `flags` +array. Fastly additionally requires `execution_ceiling_ms` (1..60000), enforced independently by +the host: guest checks cannot wake a permanently pending synchronous source. The runner waits +for that ceiling before recovery where necessary. The server response budget is 500 ms; the +client budget must be 1000..10000 ms. Thirteen scenarios plus a healthy request after each run +sequentially, without redirects/retries, with a 64 MiB observed-transfer cap (plus one Node stream +chunk on crossing). No outbound backend is contacted. + +```sh +node scripts/qualify_response_egress.mjs --execute-hosted TARGET.json CLIENTS.json +node scripts/qualify_response_egress.mjs --validate CLIENTS.json EVENTS.json +``` + +Client collection intentionally exits 2: it is not qualification by itself. Export the complete +unsampled events as a JSON array in `EVENTS.json`, preserving all records/sequences and excluding +other runs; independently confirm completeness before setting the recorded manifest's +`telemetry_complete` to true. Missing/duplicate/conflicting records remain unverified. Evidence +files are capped at 4 MiB and output uses private file permissions. The validator does not +authenticate fabricated telemetry or prove the supplied deployment identity. + +Each probe emits source-drop, terminal, resource-drop and final counters for one late-bound +completion resource. `HostHandoff` remains distinct from delivery. Success qualifies ownership +only for the scheduled probes, not full Q4: instance identity is `unobserved`, so a healthy next +request does not prove same-isolate capacity recovery. Actual transport failures, host teardown, +raw parser behavior and provider memory need separate evidence. No capability is promoted by +these tools, local tests or successful compilation. The response-egress specification maintains +the per-target evidence matrix and limitations. + ## Understand the change in five minutes +The opt-in `outbound-progress` suite also runs 24 controlled-origin coding checks per target: +gzip, deflate and Brotli; buffered and streamed responses; Original/200, Original/206, Decode/206 +and Decode/200 controls. It compares exact delivered bytes and surviving coding/length metadata +through each actual adapter client. Original and 206 use decoder budget zero and decoded cap one. +These JSON-report probes characterize outbound delivery, not encoded downstream relay framing or +hosted qualification. Cloudflare still requires the exact explicitly supplied build tools. + +It also runs 46 marker-policy checks: inherited-off/explicit-off/explicit-on crossed with absent, +upstream and Connection-nominated markers on GET/200, GET/204, HEAD/200, GET/304 and GET/302, plus +a mixed-policy batch. Returned fields must agree with the client inspector. Spin adds two direct +SDK header snapshots before EdgeZero conversion. If the host hides Connection while retaining the +nominated marker, the record labels the affected cases `provider_hidden_nomination`; those passes +characterize adapter-visible fields, not raw-header fidelity. No provider workaround reconstructs +missing fields, and these cases do not qualify header allocation or hosted cancellation bounds. + Think of the app as the route table plus any state you attach to it. A request still brings its own headers, body, extensions, and provider resources. @@ -43,8 +669,15 @@ Start with these entry points in `crates/fixture-fastly/src/bin/`: | B | `rebuild_per_request.rs` | Sandbox globals; app is rebuilt | | C | `retained_app.rs` | Sandbox globals and the app/router | -The `custom_` versions use the same three lifetimes with manual streaming and -response finalization. A/B/C remain short labels in experiment reports only. +The `custom_` versions use the same three lifetimes with request preparation, +response finalization, adapter-owned streaming, and post-send work. A/B/C remain +short labels in experiment reports only. + +Custom callbacks retain the actual dispatch-selected HTTP client for post-send work, rather +than constructing a second client or overriding request injection in preparation. A request-local +capture is filled before middleware awaits the handler, so routed handler errors still retain +the selected client. State-client precedence is unchanged. Detached/fallback delivery keeps its +existing post-send restrictions; a missing capture never creates a substitute transport. With the pinned Viceroy installed, run a small synthetic comparison: @@ -67,8 +700,10 @@ a prediction of an application's production performance. Token, path, and body checks confirm that each request still sees its own data. Shared state equals the ordinal in retained probe sequences and one in per-request modes. -For production code, read `serve_app_with_request_extensions` in the Fastly -adapter: it owns one app and performs request setup inside the SDK callback. +For production code, read `serve_app` and `serve_app_with_hooks` in the Fastly +adapter: they retain one successfully configured app and perform fresh request +setup inside each SDK callback. Construction uses +`App::build::(FASTLY_PLATFORM) -> Result`, not an infallible hook builder. Cloudflare and Spin instead expose `dispatch_app` and let the caller retain a concrete app. Axum already retains its app. Existing entry points keep their lifecycle defaults until an application explicitly adopts retention. @@ -80,8 +715,13 @@ shared dependencies, and keep its Fastly SDK requirement aligned with the root. Select executables with `VICEROY_BIN`, `WORKER_BUILD_BIN`, `WRANGLER_BIN`, and `SPIN_BIN`. Install tools explicitly; the runner never deploys or installs them. -Use a worker-build version compatible with the locked wasm-bindgen CLI -(worker-build 0.8.3 is compatible with the current lockfile). The runner builds +The PR pins Fastly SDK 0.13.1, Viceroy 0.21.0, `worker` 0.8.5, and `spin-sdk` 7.0.0 +from `stackpop/spin-rust-sdk` at `e65ff90dadf4173b08b14194e878ad3acd555337`, using its +`wasip3 0.9.0` / `wit-bindgen 0.62.0` re-export and `spawn_local`. See the +[dependency audit](../../../docs/audits/2026-10-27-spin-sdk-eager-progress.md). +The repository pins Spin CLI 4.1.0. Use a worker-build +version compatible with the locked worker/wasm-bindgen graph rather than assuming +an older builder remains compatible. The runner builds once with the selected `WORKER_BUILD_BIN`, then starts Wrangler with a snapshot that disables automatic rebuilding. Axum's executable path is read from Cargo's artifact output, so configured build targets cannot select a stale default binary. @@ -102,7 +742,7 @@ crash are not zero attempted requests. ## Fastly comparisons The provider/application boundary is documented in -[`Custom lifecycle compatibility contract`](../../../docs/guide/adapters/fastly.md#custom-lifecycle-compatibility-contract). +[`Custom lifecycle contract`](../../../docs/guide/adapters/fastly.md#custom-lifecycle-contract). This workspace uses path dependencies on the checkout under test. The Fastly smoke suite additionally runs a fresh custom guest through health, a handled initialization failure, another health request, successful initialization, and @@ -116,9 +756,34 @@ preservation and isolation rather than merely setting a constant header. - C: production retained-app helper. - Custom A/B/C: EdgeZero `lifecycle::run_custom` / `serve_custom` and `Sandbox` own the serving boundary and successful-only initialization. Per-request mode uses a fresh - state slot per callback; C uses the retained slot. Raw request mutation/conversion, response-extension finalization, - appended headers, progressive stream pumping with explicit flush/finish, and - completed post-send work. + state slot per callback; C uses the retained slot. Callbacks return `Result<(), E>` + and own delivery by explicitly sending native replies or invoking + `send_request_with_registries_and_hooks(app, stores, req, prepare, finalize)`. + The preparation hook mutates the native request and extensions; the routed-response + finalizer runs before egress policy and framing. The adapter owns appended headers, + progressive body writes, and terminal close/abandon without whole-body collection. + Its owned `FastlyRequestDelivery` distinguishes NoAttempt, policy Aborted, + Detached and Routed. Routed state survives delivery failure; detached responses + skip finalization. Attempted results carry the exact completion report and the + last selected body's NotCommitted/Unknown/Committed state. Only NoAttempt or + confirmed NotCommitted permit native replacement; completed attempt accounting + never includes that replacement. Complete + post-send work before returning. Neither wrapper requests a synthetic SDK send; + `run_custom` returns the callback result directly. + +The custom stream checks retain the finalizer's request-local client and complete +post-send work after a committed application-source failure. A separate framing +rejection delivers the canonical fallback and must not start application follow-up. +Each attempted result must match exactly one correlated egress terminal event; +required-store NoAttempt results must produce none. Host acceptance is not client +delivery, and local Viceroy evidence is not hosted qualification. Synthetic +`delivery::testing` fixtures exercise the shared finisher, not SDK head conversion +or host transport. + +Callbacks that dispatch the app use baked `Hooks::logging_for("fastly")` settings +for logger setup and reopen stores under their stable logical aliases per request. +Deployment selectors choose physical links, not runtime configuration. Native request bodies, extensions, store handles, +and pending work are not retained with the app. ```sh ./scripts/smoke_test_reusable_app.sh --adapter fastly --suite benchmark @@ -146,13 +811,120 @@ ID and distinguish `fixture-logs ::` endpoint records from echoed stdout. The negative control verifies that naively wrapping `run_app` with an enabled global logger fails on its second installation. +The retained serverless smoke fixtures also declare an unlinked KV ID alongside the +healthy default. `/declared-kv/free`, two `/declared-kv/missing/read` requests, then +`/declared-kv/free` must produce 200/500/500/200 in one observed instance, with warning +counts 0/1/1/1. A separate linked-store write/read/list/delete sequence checks continuity. +Failed write/delete/list requests follow that unchanged four-request prefix. The linked-store +sequence also reads and lists after delete, requiring `null` and an empty list. All 13 requests +per serverless adapter require exactly one actual egress terminal. Operation handlers emit one +entry and one typed-result receipt derived from the actual `KvError`, not the rendered status; +the KV-free requests must emit neither. Start, entry, result and terminal events must be ordered; +the post-delete read must contain an explicit `kv_outcome: null`, not merely omit that field. +Missing bindings must retain the exact sanitized 500 JSON. +Every token, instance, typed result and terminal count is checked again after runtime shutdown, +including when an earlier smoke check failed. Fastly endpoint receipts are distinguished from stdout echoes; +Cloudflare/Spin use a fixture-owned logger with instance-tagged structured receipts. +Pinned Viceroy 0.21.0 accepts at most five follow-up requests in one guest, so the additional +operations may naturally cross guests. The original four-request prefix still requires one +instance; the extension records every observed instance without restarting or retrying that prefix. +Cloudflare/Spin require one warning per missing-operation instance and none for healthy-only +instances. Fastly's named-endpoint warnings have no instance tag: after each request and shutdown, +their aggregate count must equal the number of observed missing-operation instances. That checks +sequential totals, not independent attribution of late warnings across different instances. +This is local host characterization, not a hosted reuse guarantee. +Fastly reports `Completed`, Cloudflare/Spin `HostHandoff`; neither qualifies peer delivery. +SDK/source-harness tests separately cover all failed operations, typed reasons, and +fresh-request recovery. Linked-store continuity is not a provider outage/restoration proof. + +The interrupted-upstream case waits until the client observes its partial body before +releasing the backend abort, proving physical progressive delivery. The immediate +zero-byte source-failure case instead requires exactly one response-scoped terminal +report (`SourceError`, application body, zero accepted bytes, no fallback) and terminal +guest eviction. The host may reset before its head reaches the client; only EOF/reset +with no observed status is accepted, not timeouts, malformed responses, or replacement +error responses. Neither proof upgrades the documented egress capability guarantees. + ## Runtime and measurement limits -Raising `--max-requests` cannot extend the pinned Viceroy's per-guest request -ceiling (see the Fastly guide). Verify deployed eviction and long-lived memory -behavior separately. The runner checks the Viceroy version against +Raising `--max-requests` does not guarantee a longer guest lifetime. Any observed +Viceroy per-guest ceiling is local evidence, not a deployed guarantee or a promise +that Viceroy 0.21.0 reproduces an older runtime's ceiling. Verify deployed eviction +and long-lived memory behavior separately. The runner checks the Viceroy version against `.tool-versions` and reports a mismatch as unverified. +Building with `spin-sdk` 7.0.0 emits the SDK's `wasip3` WASI HTTP interface. A +successful build does not prove the selected Spin CLI 4.1.0 host can execute that +component. Retained dispatch also depends on provider-specific runtime support; +an unavailable host, interface mismatch, or missing HTTP/reuse evidence cannot +count as a passing lifecycle assertion. Do not substitute older SDKs or response +converters to make a provider check appear supported. Build/startup failures remain +failures; tools or runtime evidence the runner explicitly marks unsupported or +unverified remain exit 2. + +The adapter's local Spin wire controls run in the existing Rust harness, using the unchanged +guest and pinned Spin. The runner builds and stages that guest; there is no separate host-language +runner or alternate outbound client: + +```sh +cargo test --offline --locked --manifest-path crates/edgezero-test-harness/Cargo.toml +./scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-wire +``` + +These controls preserve raw request framing, scope/handoff checks and 68 redirect exchanges +with 84 correlated origin requests. They establish local SDK behavior, not hosted qualification. + +The same Rust matrix also runs Cloudflare's production client through the standard application +entrypoint and local workerd, without replacing `fetch`: + +```sh +./scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-wire +``` + +Set the explicit absolute Cloudflare build-tool overrides listed above and +`MINIFLARE_WORKERD_PATH` to the local workerd executable. The runner records that executable's +identity and version and the effective compatibility settings; it uses loopback origins, +`dev --local`, synthetic local bindings and disabled metrics. Missing tools are unsupported +exit 2, never a passing inventory. No implicit download, deployment or remote binding is used. +Require all 68 exchanges and 84 ordered origin requests, exact bytes, encoding/cap parity, +unread intermediate-body progress, final stream behavior and stop/hop metadata. Truncated-body +errors must be Cloudflare `Transport` at `ResponseBodyRead`, not Spin's `Protocol` classification. +Both runners share cleanup, including pre-startup failures, and publish success only afterward. +The local workerd run is distinct from Node guest fixtures and from hosted cancellation evidence. + +## Begin And Batch Lifetime Probes + +```sh +./scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-lifetimes +``` + +The same suite accepts `fastly`, `cloudflare` or `spin` with the explicit local tooling above. +It exercises EZ-6 scenarios 1, 2, 4 and 5 through the injected production client: slow origin, +fast origin with a silent mediator, Begin followed by an unrelated 100 ms timer, and an +unobserved batch during a separate 300 ms send. It does not implement CPU-stretch scenario 3 +or the remaining response-owned, cleanup, cap and reusable-sandbox acceptance scenarios. + +Each case records actual guest operation entry/duration, per-slot arrival elapsed, exact +collection states, typed errors, and independently observed host receipts/head/chunk writes. +Bodies are capped at 64 bytes. The existing bounded origin collector/parser and joined worker +ownership are reused; registrations, roles and event counts are finite, and unexpected or late +worker failures prevent a passing inventory. The suite runs all four cases even when an earlier +case fails, but publishes success only after runtime and origin cleanup succeed. + +Host request-send and guest request-start have different origins, particularly on a cold guest. +The two timeline cases explicitly record a separate scheduling alignment derived from the first +origin receipt and its guest send timestamp. Transit uncertainty remains unknown and +`clock_sync_proven` remains false. Scheduled timestamps never replace actual writes or guest +observations. The frozen +/-5 ms timeline targets remain checked; misses return exit 1, not a +qualified pass. For quiet-window proofs, a head-write-start/guest-head-observation causal bound +conservatively compares receipts without pretending the clocks are synchronized. The 5 ms +measurement guard makes that proof stricter, and is not an extra deadline allowance. + +These are local characterization/acceptance probes, not a hosted evidence package. Until the +complete declared criteria and timing evidence pass, the inventory bindings stay planned. +No candidate freeze, provider capability promotion, deployment, or optional idle-body adoption +is implied. Run artifacts preserve failures as well as successes. + Cloudflare and Spin may select different guests during overlap or recovery tests; those observations are unverified. A success requires the same instance and the specific injected failure. Overlap checks use each request's `before.inflight` @@ -172,9 +944,16 @@ snapshots over short guest lifetimes cannot establish absence of small leaks. The smoke suite exercises actual adapter failures with controlled inputs: -- Panic inside `MeasuredApp::build_app`, followed by a request in another guest. -- A truncated upstream body used as the inbound body, failing `into_core_request`. -- An erroring core body stream, failing buffered response conversion. +These cases describe fixture coverage, not a recorded pass for the current pinned +SDK/runtime graph. Only correlated evidence from a run of this checkout establishes +an assertion; results from older runtimes do not certify the migrated entry points. + +- Panic inside `MeasuredApp::configure` during `App::build`, followed by a request + in another guest. +- A truncated upstream body used as the inbound body, exercising the canonical + ingress read/error-response path rather than a public conversion helper. +- An erroring core body stream, exercising terminal response-egress failure and + close/abandon without collecting the whole response first. - A missing required KV selector terminating the SDK loop, with an attempted request summary. A separate custom policy catches that error and proves successful bindings and retained app state on the following request. @@ -188,9 +967,11 @@ The smoke suite exercises actual adapter failures with controlled inputs: `summary.json` groups observations by variant, repetition, and guest. It reports initialization phases, SDK attempts, recorded client completion, CPU deltas, and memory change/peak/slope/last-three-sample plateau over actual ordinals. -Missing phase events remain unknown. Standard helpers expose conversion-lifetime -samples; custom callbacks expose response commitment and guest completion. These -are different observation points. A standard request-start sample occurs after +Missing phase events remain unknown. Historical event labels such as +`conversion_completed` identify only the instrumented resource lifetime; they +are not evidence of whole-body collection, exact response commitment, or complete +cleanup. Custom callbacks additionally record terminal delivery and guest completion +where instrumented. These are different observation points. A standard request-start sample occurs after initialization, while the custom callback sample occurs before it. Each run saves its lockfile, configuration, compiler/runtime identity, and binary @@ -211,12 +992,13 @@ absence of small leaks or bounded growth for an arbitrary application. These are explicitly unverified, not assertions counted as passed: -| Case | Available evidence / limitation | -| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Optional runtime-store open fails, then recovers in the same guest | Host tests inject degraded then successful configuration snapshots; required-registry recovery is exercised above. Local runtimes cannot make the fixed optional store fail transiently. | -| `EdgeError` itself fails to render | The renderer builds fixed valid statuses/headers from JSON values; no public input or injection hook triggers a returned rendering error. | -| Unavailable heap hostcall | Only the preflight is implemented. Under a memory limit, a failed heap snapshot makes the SDK stop after the first request. Local runtimes supply the hostcall and cannot disable it. | -| Full standard callback/send/cleanup timing | The public retained helper has no post-send callback. Conversion observations and SDK summary timing are labeled separately; unavailable phases stay unknown. | -| Deployed eviction, endpoint handles, resource accounting, capacity, and workload gains | Require separately authorized deployment and application telemetry. Local CPU samples are not cross-run CPU benchmarks; finite origin tests do not establish service-wide capacity. | +| Case | Available evidence / limitation | +| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Optional runtime-store open fails, then recovers in the same guest | Host tests inject degraded then successful configuration snapshots; required-registry recovery is exercised above. Local runtimes cannot make the fixed optional store fail transiently. | +| Spin WASI HTTP interface or provider runtime unavailable | SDK compilation alone does not establish host support or reuse. Preserve build/startup failures and explicitly unsupported/unverified records; no older-component compatibility path is used. | +| `EdgeError` itself fails to render | The renderer builds fixed valid statuses/headers from JSON values; no public input or injection hook triggers a returned rendering error. | +| Unavailable heap hostcall | Only the preflight is implemented. Under a memory limit, a failed heap snapshot makes the SDK stop after the first request. Local runtimes supply the hostcall and cannot disable it. | +| Full standard callback/send/cleanup timing | The public retained helper has no post-send callback; its finalizer runs before delivery and its state is discarded. Instrumented resource observations and SDK summary timing are labeled separately; unavailable phases stay unknown. | +| Deployed eviction, endpoint handles, resource accounting, capacity, and workload gains | Require separately authorized deployment and application telemetry. Local CPU samples are not cross-run CPU benchmarks; finite origin tests do not establish service-wide capacity. | No deployment is performed by these fixtures. diff --git a/tests/fixtures/reusable-app/crates/fixture-axum/Cargo.toml b/tests/fixtures/reusable-app/crates/fixture-axum/Cargo.toml index b5d212b6a..b236c2678 100644 --- a/tests/fixtures/reusable-app/crates/fixture-axum/Cargo.toml +++ b/tests/fixtures/reusable-app/crates/fixture-axum/Cargo.toml @@ -4,11 +4,27 @@ version.workspace = true edition.workspace = true publish.workspace = true +[features] +qualification = ["fixture-core/qualification"] +progress = ["fixture-core/qualification", "dep:reqwest", "dep:tokio"] + [dependencies] fixture-core.workspace = true edgezero-core.workspace = true edgezero-adapter-axum = { workspace = true, features = ["axum"] } anyhow = "1" +serde_json.workspace = true +reqwest = { version = "=0.13.4", default-features = false, optional = true } +tokio = { version = "1", features = ["rt", "time"], optional = true } + +[[bin]] +name = "fixture-axum-outbound-progress" +path = "src/bin/outbound_progress.rs" +required-features = ["progress"] + +[[bin]] +name = "fixture-axum-assembly" +path = "src/bin/assembly.rs" [lints] workspace = true diff --git a/tests/fixtures/reusable-app/crates/fixture-axum/src/bin/assembly.rs b/tests/fixtures/reusable-app/crates/fixture-axum/src/bin/assembly.rs new file mode 100644 index 000000000..9b5e9b8dd --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-axum/src/bin/assembly.rs @@ -0,0 +1,4 @@ +fn main() -> anyhow::Result<()> { + fixture_core::init_fixture_logging(|line| eprintln!("{line}"))?; + edgezero_adapter_axum::dev_server::run_app::() +} diff --git a/tests/fixtures/reusable-app/crates/fixture-axum/src/bin/outbound_progress.rs b/tests/fixtures/reusable-app/crates/fixture-axum/src/bin/outbound_progress.rs new file mode 100644 index 000000000..49d273816 --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-axum/src/bin/outbound_progress.rs @@ -0,0 +1,6 @@ +#[path = "../progress.rs"] +mod progress; + +fn main() -> anyhow::Result<()> { + edgezero_adapter_axum::dev_server::run_app::() +} diff --git a/tests/fixtures/reusable-app/crates/fixture-axum/src/main.rs b/tests/fixtures/reusable-app/crates/fixture-axum/src/main.rs index a1da0b308..bbf1deb22 100644 --- a/tests/fixtures/reusable-app/crates/fixture-axum/src/main.rs +++ b/tests/fixtures/reusable-app/crates/fixture-axum/src/main.rs @@ -1,3 +1,8 @@ fn main() -> anyhow::Result<()> { - edgezero_adapter_axum::dev_server::run_app::() + edgezero_adapter_axum::dev_server::run_app::() } + +#[cfg(not(feature = "qualification"))] +use fixture_core::FixtureApp as ActiveApp; +#[cfg(feature = "qualification")] +use fixture_core::qualification::QualificationApp as ActiveApp; diff --git a/tests/fixtures/reusable-app/crates/fixture-axum/src/progress.rs b/tests/fixtures/reusable-app/crates/fixture-axum/src/progress.rs new file mode 100644 index 000000000..b166b7196 --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-axum/src/progress.rs @@ -0,0 +1,146 @@ +use edgezero_core::{EdgeError, app::Hooks, router::RouterService}; +use fixture_core::qualification::progress::{self, Job, ProbeRuntime}; +use std::{future::Future, pin::Pin, sync::Arc, time::Duration}; + +pub struct NativeProgressApp; +pub struct NativeRuntime; + +struct AbortOnDrop(tokio::task::AbortHandle); +impl Drop for AbortOnDrop { + fn drop(&mut self) { + self.0.abort(); + } +} + +fn spawn_owned( + runtime: tokio::runtime::Handle, + worker: impl Future> + Send + 'static, +) -> Job { + let task = runtime.spawn(worker); + let guard = AbortOnDrop(task.abort_handle()); + Box::pin(async move { + let _guard = guard; + task.await.map_err(|_| progress::probe_failure())? + }) +} + +impl Hooks for NativeProgressApp { + fn configure(app: &mut edgezero_core::app::App) -> Result<(), EdgeError> { + fixture_core::qualification::response_headers::configure(app) + } + + fn routes() -> Result { + Ok(progress::router( + Arc::new(NativeRuntime), + serde_json::json!({"source":"not-exposed-by-native-entrypoint","known_empty":null}), + None, + )) + } +} + +async fn fetch(target: String) -> Result { + let response = reqwest::Client::builder() + .no_proxy() + .redirect(reqwest::redirect::Policy::none()) + .timeout(Duration::from_secs(2)) + .build() + .map_err(|_| progress::probe_failure())? + .get(target) + .send() + .await + .map_err(|_| progress::probe_failure())?; + if !response.status().is_success() { + return Err(progress::probe_failure()); + } + let bytes = response + .bytes() + .await + .map_err(|_| progress::probe_failure())?; + if bytes.len() > 64 { + return Err(progress::probe_failure()); + } + Ok(bytes.len() as u64) +} + +impl ProbeRuntime for NativeRuntime { + fn adapter(&self) -> &'static str { + "axum" + } + fn perform(&self, target: String) -> Job { + Box::pin(fetch(target)) + } + fn start(&self, target: String) -> Result { + let runtime = + tokio::runtime::Handle::try_current().map_err(|_| progress::probe_failure())?; + Ok(spawn_owned(runtime, fetch(target))) + } + fn pause(&self, duration: Duration) -> Pin>> { + Box::pin(async move { + tokio::time::sleep(duration).await; + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use fixture_core::qualification::progress::ProbeRuntime; + + #[test] + fn unpolled_consumer_drop_aborts_the_worker_before_runtime_shutdown() { + use std::sync::atomic::{AtomicUsize, Ordering}; + struct Released(Arc); + impl Drop for Released { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap(); + runtime.block_on(async { + for before_first_poll in [true, false] { + let releases = Arc::new(AtomicUsize::new(0)); + let polls = Arc::new(AtomicUsize::new(0)); + let (released, started) = (Released(Arc::clone(&releases)), Arc::clone(&polls)); + let job = spawn_owned(tokio::runtime::Handle::current(), async move { + let _released = released; + started.fetch_add(1, Ordering::SeqCst); + std::future::pending::>().await + }); + if before_first_poll { + assert_eq!(polls.load(Ordering::SeqCst), 0); + } else { + tokio::time::timeout(Duration::from_secs(1), async { + while polls.load(Ordering::SeqCst) == 0 { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + } + drop(job); + tokio::time::timeout(Duration::from_secs(1), async { + while releases.load(Ordering::SeqCst) == 0 { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + assert_eq!(releases.load(Ordering::SeqCst), 1); + tokio::task::yield_now().await; + assert_eq!(releases.load(Ordering::SeqCst), 1); + } + }); + } + + #[test] + fn progress_start_rejects_absent_runtime_without_spawning() { + assert!( + NativeRuntime + .start("http://127.0.0.1:1/g3/test/slot-0".into()) + .is_err() + ); + } +} diff --git a/tests/fixtures/reusable-app/crates/fixture-cloudflare/Cargo.toml b/tests/fixtures/reusable-app/crates/fixture-cloudflare/Cargo.toml index 2e9f7f485..9d69376e0 100644 --- a/tests/fixtures/reusable-app/crates/fixture-cloudflare/Cargo.toml +++ b/tests/fixtures/reusable-app/crates/fixture-cloudflare/Cargo.toml @@ -4,12 +4,17 @@ version.workspace = true edition.workspace = true publish.workspace = true +[features] +qualification = ["fixture-core/qualification"] +progress = ["fixture-core/qualification", "dep:futures"] + [dependencies] serde_json.workspace = true fixture-core.workspace = true edgezero-core.workspace = true edgezero-adapter-cloudflare.workspace = true -worker = "0.8" +worker.workspace = true +futures = { workspace = true, optional = true } [lib] crate-type = ["cdylib", "rlib"] diff --git a/tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs b/tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs index 9b722fd85..1bfdeb068 100644 --- a/tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs +++ b/tests/fixtures/reusable-app/crates/fixture-cloudflare/src/lib.rs @@ -1,55 +1,103 @@ +#![cfg(target_arch = "wasm32")] + +#[cfg(feature = "progress")] +mod progress; + +use edgezero_core::MonotonicClock; use edgezero_core::app::{App, Hooks}; -use fixture_core::{FixtureApp, MissingBindingApp, OtherApp}; +#[cfg(not(feature = "qualification"))] +use fixture_core::DeclaredKvApp as FixtureApp; +use fixture_core::assembly::{ConfigurationFailureApp, InternalFailureApp, RetryApp}; +#[cfg(feature = "qualification")] +use fixture_core::qualification::QualificationApp as FixtureApp; +use fixture_core::{OtherApp, retained_app}; use std::sync::OnceLock; use worker::{Context, Env, Request, Response, event}; static APP: OnceLock = OnceLock::new(); static OTHER_APP: OnceLock = OnceLock::new(); +static ASSEMBLY_APP: OnceLock = OnceLock::new(); + +fn telemetry(line: &str) { + worker::console_log!("{line}"); +} + +async fn dispatch_selected( + req: Request, + env: Env, + ctx: Context, + retained: bool, + cell: &OnceLock, +) -> worker::Result { + if !retained { + return edgezero_adapter_cloudflare::run_app::(req, env, ctx).await; + } + let clock = MonotonicClock::default(); + let start = clock.now(); + match retained_app::(cell, edgezero_adapter_cloudflare::CLOUDFLARE_PLATFORM) { + Ok(app) => edgezero_adapter_cloudflare::dispatch_app::(app, req, env, ctx).await, + Err(error) => { + drop(env); + edgezero_adapter_cloudflare::request::send_assembly_error(req, error, ctx, start, clock) + } + } +} #[event(fetch)] async fn fetch(req: Request, env: Env, ctx: Context) -> worker::Result { + #[cfg(feature = "progress")] + if req.path().starts_with("/progress/") { + return progress::dispatch(req, env, ctx).await; + } if !FixtureApp::owns_logging() { drop(edgezero_adapter_cloudflare::init_logger()); + } else { + fixture_core::init_fixture_logging(telemetry) + .map_err(|_error| worker::Error::RustError("fixture logging failed".to_owned()))?; } let retained = env .var("FIXTURE_MODE") .map(|v| v.to_string() == "retained") .unwrap_or(false); + match req.path().as_str() { + "/assembly/configuration" => { + return edgezero_adapter_cloudflare::run_app::(req, env, ctx) + .await; + } + "/assembly/internal" => { + return edgezero_adapter_cloudflare::run_app::(req, env, ctx).await; + } + "/assembly/retry" | "/assembly/retry/missing" => { + return dispatch_selected::>(req, env, ctx, retained, &ASSEMBLY_APP) + .await; + } + _ => {} + } if retained && req.path() == "/binding-failure" { - // Pairs MissingBindingApp with FixtureApp's router; valid only while - // dispatch_app reads nothing but A::stores() (see MissingBindingApp). - let result = edgezero_adapter_cloudflare::dispatch_app::( - APP.get_or_init(FixtureApp::build_app), - req, - env, - ctx, - ) - .await; + let clock = MonotonicClock::default(); + let start = clock.now(); + let app = match retained_app::( + &APP, + edgezero_adapter_cloudflare::CLOUDFLARE_PLATFORM, + ) { + Ok(app) => app, + Err(error) => { + return edgezero_adapter_cloudflare::request::send_assembly_error( + req, error, ctx, start, clock, + ); + } + }; + let result = edgezero_adapter_cloudflare::request::CloudflareService::new(app) + .with_kv("missing_fixture_kv") + .require_kv() + .dispatch(req, env, ctx) + .await; return match result { Err(error) => Response::from_json(&serde_json::json!({"instance":fixture_core::instance_id(),"source":"injected_required_binding","error":error.to_string()})).map(|r|r.with_status(503)), Ok(response) => Ok(response), }; } if req.path() == "/other" { - if !retained { - return edgezero_adapter_cloudflare::run_app::(req, env, ctx).await; - } - return edgezero_adapter_cloudflare::dispatch_app::( - OTHER_APP.get_or_init(OtherApp::build_app), - req, - env, - ctx, - ) - .await; - } - if retained { - edgezero_adapter_cloudflare::dispatch_app::( - APP.get_or_init(FixtureApp::build_app), - req, - env, - ctx, - ) - .await - } else { - edgezero_adapter_cloudflare::run_app::(req, env, ctx).await + return dispatch_selected::(req, env, ctx, retained, &OTHER_APP).await; } + dispatch_selected::(req, env, ctx, retained, &APP).await } diff --git a/tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs b/tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs new file mode 100644 index 000000000..988d78b32 --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-cloudflare/src/progress.rs @@ -0,0 +1,118 @@ +use edgezero_core::EdgeError; +use fixture_core::qualification::progress::{self, Job, ProbeRuntime, ProgressHooks}; +use futures::{channel::oneshot, future::LocalBoxFuture}; +use serde_json::json; +use std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, +}; + +struct Runtime; + +impl ProbeRuntime for Runtime { + fn adapter(&self) -> &'static str { + "cloudflare" + } + fn perform(&self, target: String) -> Job { + Box::pin(async move { + let request = worker::Request::new(&target, worker::Method::Get) + .map_err(|_| progress::probe_failure())?; + let mut response = worker::Fetch::Request(request) + .send() + .await + .map_err(|_| progress::probe_failure())?; + if response.status_code() != 200 { + return Err(progress::probe_failure()); + } + let body = response + .bytes() + .await + .map_err(|_| progress::probe_failure())?; + if body.len() > 64 { + return Err(progress::probe_failure()); + } + Ok(body.len() as u64) + }) + } + fn start(&self, target: String) -> Result { + let job = self.perform(target); + let (send, receive) = oneshot::channel(); + worker::wasm_bindgen_futures::spawn_local(async move { + let _ = send.send(job.await); + }); + Ok(Box::pin(async move { + receive.await.map_err(|_| progress::probe_failure())? + })) + } + fn pause(&self, duration: Duration) -> LocalBoxFuture<'static, ()> { + Box::pin(worker::Delay::from(duration)) + } +} + +pub async fn dispatch( + request: worker::Request, + env: worker::Env, + ctx: worker::Context, +) -> worker::Result { + let probe = request + .path() + .strip_prefix("/progress/overflow/") + .filter(|case| { + matches!( + *case, + "inbound" + | "buffered-7" + | "buffered-8" + | "buffered-9" + | "streamed-7" + | "streamed-8" + | "streamed-9" + | "upstream-413" + ) + }) + .map(str::to_owned) + .zip( + request + .headers() + .get("x-request-token") + .ok() + .flatten() + .filter(|token| { + !token.is_empty() + && token.len() <= 64 + && token + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + }), + ); + if let Some((case, token)) = &probe { + worker::console_log!( + "{}", + json!({"event":"overflow-dispatch","phase":"entry","case":case,"token":token,"qualification":false}) + ); + } + // Inspect the original object without cloning or consuming its body. + let signal = json!({"source":"workerd-before-conversion", "body_absent":request.inner().body().is_none()}); + let returned = Arc::new(AtomicBool::new(false)); + let app = progress::application( + Arc::new(Runtime), + signal, + Some(Arc::clone(&returned)), + edgezero_adapter_cloudflare::CLOUDFLARE_PLATFORM, + ) + .map_err(|_| worker::Error::RustError("progress probe configuration failed".into()))?; + let response = + edgezero_adapter_cloudflare::dispatch_app::(&app, request, env, ctx).await; + returned.store(true, Ordering::SeqCst); + if let Some((case, token)) = &probe { + worker::console_log!( + "{}", + json!({"event":"overflow-dispatch","phase":"return","case":case,"token":token, + "status":response.as_ref().ok().map(worker::Response::status_code),"outcome":if response.is_ok(){"response"}else{"error"},"qualification":false}) + ); + } + response +} diff --git a/tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml b/tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml index 12957af18..a67631ef5 100644 --- a/tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml +++ b/tests/fixtures/reusable-app/crates/fixture-cloudflare/wrangler.toml @@ -1,6 +1,7 @@ name = "reusable-app-fixture" main = "build/worker/shim.mjs" compatibility_date = "2026-04-22" +compatibility_flags = ["enable_request_signal", "enable_weak_ref"] [build] command = "worker-build --release . -- --locked" [vars] diff --git a/tests/fixtures/reusable-app/crates/fixture-core/Cargo.toml b/tests/fixtures/reusable-app/crates/fixture-core/Cargo.toml index 42582466d..af18ca455 100644 --- a/tests/fixtures/reusable-app/crates/fixture-core/Cargo.toml +++ b/tests/fixtures/reusable-app/crates/fixture-core/Cargo.toml @@ -4,11 +4,16 @@ version.workspace = true edition.workspace = true publish.workspace = true +[features] +qualification = [] + [dependencies] edgezero-core.workspace = true futures.workspace = true serde_json.workspace = true async-trait = "0.1" +bytes = "1" +log.workspace = true [lints] workspace = true diff --git a/tests/fixtures/reusable-app/crates/fixture-core/src/assembly.rs b/tests/fixtures/reusable-app/crates/fixture-core/src/assembly.rs new file mode 100644 index 000000000..65ea1703f --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-core/src/assembly.rs @@ -0,0 +1,175 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, OnceLock}; + +use edgezero_core::app::{App, Hooks}; +use edgezero_core::body::Body; +use edgezero_core::extractor::State; +use edgezero_core::http::{HeaderMap, Response, response_builder}; +use edgezero_core::router::RouterService; +use edgezero_core::{EdgeError, MonotonicClock, action}; + +static ATTEMPTS: AtomicUsize = AtomicUsize::new(0); +static CONFIGURATIONS: AtomicUsize = AtomicUsize::new(0); +static INSTANCE: OnceLock = OnceLock::new(); + +pub struct ConfigurationFailureApp; +pub struct InternalFailureApp; +pub struct RetryApp; + +struct ProbeState { + generation: usize, + visits: AtomicUsize, +} + +impl Drop for ProbeState { + fn drop(&mut self) { + crate::emit(serde_json::json!({"event":"assembly-state-drop", + "instance":instance(),"generation":self.generation})); + } +} + +impl Hooks for ConfigurationFailureApp { + fn configure(app: &mut App) -> Result<(), EdgeError> { + install_synthesized_headers(app, "configuration", 1)?; + app.set_error_response_renderer(|_| { + Response::new(Body::from("private-configured-renderer")) + }); + app.set_response_egress_policy(|_, _| panic!("failed app policy must not run")); + Err(EdgeError::config_out_of_date( + "configuration unavailable", + "", + )) + } + + fn owns_logging() -> bool { + true + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } +} + +impl Hooks for InternalFailureApp { + fn owns_logging() -> bool { + true + } + + fn routes() -> Result { + Err(EdgeError::internal(std::io::Error::other( + "assembly-private-token=secret", + ))) + } +} + +impl Hooks for RetryApp { + fn configure(app: &mut App) -> Result<(), EdgeError> { + let generation = app + .state::>() + .ok_or_else(|| EdgeError::service_unavailable("registered state missing"))? + .generation; + CONFIGURATIONS.fetch_add(1, Ordering::SeqCst); + install_synthesized_headers(app, "retry", generation)?; + if CONFIGURE_FAILURE && generation == 1 { + return Err(EdgeError::service_unavailable("configuration unavailable")); + } + Ok(()) + } + + fn owns_logging() -> bool { + true + } + + fn routes() -> Result { + let attempt = ATTEMPTS.fetch_add(1, Ordering::SeqCst) + 1; + crate::emit(serde_json::json!({"event":"assembly-attempt", + "instance":instance(),"attempt":attempt})); + if !CONFIGURE_FAILURE && attempt == 1 { + return Err(EdgeError::service_unavailable("state unavailable")); + } + Ok(RouterService::builder() + .with_state(Arc::new(ProbeState { + generation: attempt, + visits: AtomicUsize::new(0), + })) + .get("/assembly/retry", probe) + .build()) + } +} + +fn install_synthesized_headers( + app: &mut App, + owner: &str, + generation: usize, +) -> Result<(), EdgeError> { + let value = format!("{owner}-{generation}"); + let mut headers = HeaderMap::new(); + headers.insert( + "x-assembly-synthesized", + value.parse().map_err(EdgeError::internal)?, + ); + headers.insert( + "set-cookie", + format!("assembly={value}; Path=/") + .parse() + .map_err(EdgeError::internal)?, + ); + app.set_synthesized_response_headers(headers) + .map_err(EdgeError::internal)?; + crate::emit( + serde_json::json!({"event":"assembly-synthesized-installed","instance":instance(), + "owner":owner,"generation":generation,"fields":2}), + ); + Ok(()) +} + +fn instance() -> &'static str { + INSTANCE.get_or_init(|| format!("assembly-{:?}", MonotonicClock::default().now())) +} + +#[action] +async fn probe(State(state): State>) -> Result { + let visits = state.visits.fetch_add(1, Ordering::SeqCst) + 1; + response_builder() + .header("content-type", "application/json") + .header("x-assembly-configured", "true") + .body(Body::from( + serde_json::json!({"instance":instance(), + "attempts":ATTEMPTS.load(Ordering::SeqCst), "generation":state.generation, + "configurations":CONFIGURATIONS.load(Ordering::SeqCst),"visits":visits}) + .to_string(), + )) + .map_err(EdgeError::internal) +} + +#[cfg(test)] +mod tests { + use std::sync::OnceLock; + + use edgezero_core::PlatformMetadata; + use edgezero_core::app::App; + use edgezero_core::http::StatusCode; + + #[test] + fn assembly_fixture_retries_failure_and_retains_only_configured_success() { + let cell = OnceLock::::new(); + let Err(error) = + crate::retained_app::>(&cell, PlatformMetadata::default()) + else { + panic!("first assembly must fail"); + }; + assert_eq!(error.status(), StatusCode::SERVICE_UNAVAILABLE); + assert!(cell.get().is_none()); + let app = crate::retained_app::>(&cell, PlatformMetadata::default()) + .unwrap(); + let retained = + crate::retained_app::>(&cell, PlatformMetadata::default()) + .unwrap(); + assert!(std::ptr::eq(app, retained)); + assert_eq!(super::ATTEMPTS.load(std::sync::atomic::Ordering::SeqCst), 2); + assert_eq!( + super::CONFIGURATIONS.load(std::sync::atomic::Ordering::SeqCst), + 1 + ); + } +} diff --git a/tests/fixtures/reusable-app/crates/fixture-core/src/lib.rs b/tests/fixtures/reusable-app/crates/fixture-core/src/lib.rs index 0108b3893..8f59d3790 100644 --- a/tests/fixtures/reusable-app/crates/fixture-core/src/lib.rs +++ b/tests/fixtures/reusable-app/crates/fixture-core/src/lib.rs @@ -1,20 +1,79 @@ +pub mod assembly; +#[cfg(test)] +mod outbound_test_support; +#[cfg(feature = "qualification")] +pub mod qualification; + use edgezero_core::app::{App, Hooks, StoreMetadata, StoresMetadata}; use edgezero_core::http::{Method, Response, response_builder}; -use edgezero_core::proxy::ProxyRequest; +use edgezero_core::key_value_store::KvError; +use edgezero_core::outbound::{HttpClient, OutboundRequest}; use edgezero_core::router::RouterService; use edgezero_core::{action, body::Body, context::RequestContext, error::EdgeError}; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, OnceLock}; +use std::sync::{Arc, Mutex, OnceLock}; static INSTANCE: OnceLock = OnceLock::new(); static BUILDS: AtomicUsize = AtomicUsize::new(0); static ORDINAL: AtomicUsize = AtomicUsize::new(0); static INFLIGHT: AtomicUsize = AtomicUsize::new(0); +const MAX_BODY_BYTES: usize = 1024 * 1024; +static TELEMETRY: OnceLock = OnceLock::new(); + +struct FixtureLogger; +impl log::Log for FixtureLogger { + fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { + metadata.level() <= log::Level::Warn + } + fn log(&self, record: &log::Record<'_>) { + if self.enabled(record.metadata()) { + emit( + serde_json::json!({"event":"fixture_log","instance":instance_id(), + "level":record.level().to_string(),"target":record.target(),"message":record.args().to_string()}), + ); + } + } + fn flush(&self) {} +} + +pub fn init_fixture_logging(sink: fn(&str)) -> Result<(), EdgeError> { + static LOGGER: FixtureLogger = FixtureLogger; + static INSTALLED: OnceLock> = OnceLock::new(); + TELEMETRY.get_or_init(|| sink); + INSTALLED + .get_or_init(|| log::set_logger(&LOGGER)) + .as_ref() + .map_err(|_error| { + EdgeError::internal(std::io::Error::other("fixture logger installation failed")) + })?; + log::set_max_level(log::LevelFilter::Warn); + Ok(()) +} + +fn emit(event: serde_json::Value) { + if let Some(sink) = TELEMETRY.get() { + sink(&event.to_string()); + } +} pub fn instance_id() -> Option<&'static str> { INSTANCE.get().map(String::as_str) } +pub fn retained_app( + cell: &OnceLock, + platform: edgezero_core::PlatformMetadata, +) -> Result<&App, EdgeError> { + if let Some(app) = cell.get() { + return Ok(app); + } + let app = App::build::(platform)?; + // Keep an already-installed winner; configuration errors never enter the cell. + let _ = cell.set(app); + cell.get() + .ok_or_else(|| EdgeError::internal(std::io::Error::other("application retention failed"))) +} + /// Carry a fixture-owned lifetime observer through request and response conversion. #[derive(Clone)] pub struct ResponseLifetime( @@ -63,56 +122,66 @@ impl Hooks for FixtureApp { }), } } - fn build_app() -> App { + fn configure(app: &mut App) -> Result<(), EdgeError> { BUILDS.fetch_add(1, Ordering::SeqCst); - let mut app = App::new(Self::routes()); - Self::configure(&mut app); - app - } - fn configure(app: &mut App) { app.set_name("fixture"); + Ok(()) } - fn routes() -> RouterService { - RouterService::builder() + fn routes() -> Result { + Ok(RouterService::builder() .middleware(ObserveRequest) .with_state(Arc::new(AtomicUsize::new(0))) .get("/probe/{id}", probe) .post("/probe/{id}", probe) .get("/bindings", bindings) + .get("/declared-kv/free", declared_kv_free) + .get("/declared-kv/{store}/{operation}", declared_kv) .get("/cookies", cookies) .get("/rendered-error", rendered_error) .get("/stream", backend) .get("/stream-error", backend) .get("/overlap/{id}", overlap) .get("/origin/{id}", backend) - .build() + .build()) } } -/// Swaps only the KV binding metadata to inject a required-binding failure. -/// -/// Fixtures pair this type with the retained `App` built from `FixtureApp`, so a -/// second construction does not invalidate the recovery fixture's one-build -/// assertion. That deliberately breaks the `dispatch_app` precondition that the -/// app is built from `A`; it is correct only because `dispatch_app` currently -/// reads nothing but `A::stores()`. If an adapter starts consulting another hook -/// (for example `A::owns_logging()`), this pairing must be revisited. -pub struct MissingBindingApp; -impl Hooks for MissingBindingApp { - fn routes() -> RouterService { +/// Exercises an unused missing declaration beside a healthy default binding. +pub struct DeclaredKvApp; +impl Hooks for DeclaredKvApp { + fn configure(app: &mut App) -> Result<(), EdgeError> { + FixtureApp::configure(app)?; + app.set_ingress_admission_policy(|head| { + let token = head.headers().get("x-request-token").and_then(|value|value.to_str().ok()).unwrap_or("missing").to_owned(); + edgezero_core::AdmissionDecision::Admit { + completion: edgezero_core::ResponseEgressCompletion::new(move |report| { + emit(serde_json::json!({"event":"egress_terminal","instance":instance_id(),"token":token, + "outcome":format!("{:?}",report.outcome),"body_kind":format!("{:?}",report.body_kind), + "bytes_written":report.bytes_written,"fallback":report.fallback_disposition.map(|value|format!("{value:?}"))})); + }), + grant: edgezero_core::IngressGrant::empty(), + read_deadline: head.read_deadline_after(edgezero_core::DEFAULT_INBOUND_READ_BUDGET), + } + }); + Ok(()) + } + fn owns_logging() -> bool { + true + } + fn routes() -> Result { FixtureApp::routes() } fn stores() -> StoresMetadata { let mut stores = FixtureApp::stores(); stores.kv = Some(StoreMetadata { - default: "missing_fixture_kv", - ids: &["missing_fixture_kv"], + default: "fixture_kv", + ids: &["fixture_kv", "missing_fixture_kv"], }); stores } } impl Hooks for OtherApp { - fn routes() -> RouterService { - RouterService::builder().get("/other", other).build() + fn routes() -> Result { + Ok(RouterService::builder().get("/other", other).build()) } } @@ -121,21 +190,21 @@ async fn other(_ctx: RequestContext) -> Result { Ok("other-app".into()) } -fn record(ctx: &RequestContext) -> Result { - let req = ctx.request(); - let token = req +async fn record(ctx: &RequestContext) -> Result { + let body = ctx.body_bytes(MAX_BODY_BYTES).await?; + let token = ctx .headers() .get("x-request-token") .and_then(|v| v.to_str().ok()) .unwrap_or("missing"); - let observation = req + let observation = ctx .extensions() .get::() .cloned() .ok_or_else(|| { EdgeError::internal(std::io::Error::other("observation middleware absent")) })?; - let shared = req + let shared = ctx .extensions() .get::>() .unwrap() @@ -146,28 +215,15 @@ fn record(ctx: &RequestContext) -> Result { "correlation": observation.correlation, "native_id": observation.native_id, "builds": BUILDS.load(Ordering::SeqCst), "path": ctx.path_params().get("id"), "token": token, - "mutated": req.headers().get("x-fixture-mutated").is_some(), - "body": String::from_utf8_lossy(ctx.body().as_bytes().unwrap_or_default()), + "mutated": ctx.headers().get("x-fixture-mutated").is_some(), + "body": String::from_utf8_lossy(&body), "shared": shared, "inflight": INFLIGHT.load(Ordering::SeqCst), })) } #[action] -async fn probe(mut ctx: RequestContext) -> Result { - use futures::StreamExt; - let body = std::mem::replace(ctx.request_mut().body_mut(), Body::empty()); - let bytes = match body { - Body::Once(bytes) => bytes.to_vec(), - Body::Stream(mut stream) => { - let mut bytes = Vec::new(); - while let Some(chunk) = stream.next().await { - bytes.extend_from_slice(&chunk.map_err(EdgeError::internal)?); - } - bytes - } - }; - *ctx.request_mut().body_mut() = Body::from(bytes); - Ok(record(&ctx)?.to_string()) +async fn probe(ctx: RequestContext) -> Result { + Ok(record(&ctx).await?.to_string()) } #[action] @@ -194,7 +250,6 @@ pub fn is_loopback_http(uri: &edgezero_core::http::Uri) -> bool { async fn fetch_backend(ctx: &RequestContext) -> Result { let uri = ctx - .request() .headers() .get("x-fixture-backend") .and_then(|v| v.to_str().ok()) @@ -205,10 +260,15 @@ async fn fetch_backend(ctx: &RequestContext) -> Result { if !is_loopback_http(&uri) { return Err(EdgeError::bad_request("backend must be loopback HTTP")); } - ctx.proxy_handle() - .ok_or_else(|| EdgeError::internal(std::io::Error::other("missing proxy")))? - .forward(ProxyRequest::new(Method::GET, uri)) - .await + let mut request = OutboundRequest::new(Method::GET, uri)?; + if matches!(ctx.uri().path(), "/stream" | "/stream-error") { + request = request.stream_response(); + } + ctx.http_client() + .ok_or_else(|| EdgeError::internal(std::io::Error::other("missing HTTP client")))? + .send(request) + .await? + .into_response() } #[action] @@ -226,9 +286,9 @@ impl Drop for InFlight { async fn overlap(ctx: RequestContext) -> Result { INFLIGHT.fetch_add(1, Ordering::SeqCst); let _guard = InFlight; - let before = record(&ctx)?; + let before = record(&ctx).await?; let _response = fetch_backend(&ctx).await?; - let mut after = record(&ctx)?; + let mut after = record(&ctx).await?; after["before"] = before; Ok(after.to_string()) } @@ -284,6 +344,70 @@ async fn bindings(ctx: RequestContext) -> Result { .to_string()) } +#[action] +async fn declared_kv_free(ctx: RequestContext) -> Result { + let mut report = record(&ctx).await?; + report["declared_kv"] = serde_json::json!(ctx.kv_store("missing_fixture_kv").is_some()); + report["default_kv"] = serde_json::json!(ctx.kv_store_default().is_some()); + report["unknown_kv"] = serde_json::json!(ctx.kv_store("undeclared").is_some()); + Ok(report.to_string()) +} + +#[action] +async fn declared_kv(ctx: RequestContext) -> Result { + let mut report = record(&ctx).await?; + let (store_label, name) = match ctx.path_params().get("store") { + Some("missing") => ("missing", "missing_fixture_kv"), + Some("linked") => ("linked", "fixture_kv"), + _ => return Err(EdgeError::bad_request("unknown controlled KV store")), + }; + let operation = match ctx.path_params().get("operation") { + Some(operation @ ("read" | "write" | "delete" | "list")) => operation, + _ => return Err(EdgeError::bad_request("unknown controlled KV operation")), + }; + let receipt = |event: &str, outcome: Option<&str>, reason: Option| { + let event = serde_json::json!({"event":event,"token":report["token"],"instance":report["instance"], + "store":store_label,"operation":operation,"outcome":outcome,"reason":reason}); + if let Some(sink) = TELEMETRY.get() { + sink(&event.to_string()); + } else { + // The ordinary Fastly fixture keeps its named logger; receipts use stdout. + println!("{event}"); + } + }; + receipt("declared_kv_enter", None, None); + let store = ctx + .kv_store(name) + .ok_or_else(|| missing_binding("declared KV handle absent"))?; + let outcome = match operation { + "read" => store + .get::("f8-key") + .await + .map(|value| serde_json::json!(value)), + "write" => store + .put("f8-key", &"f8-value") + .await + .map(|()| serde_json::json!("written")), + "delete" => store + .delete("f8-key") + .await + .map(|()| serde_json::json!("deleted")), + "list" => store + .list_keys_page("f8-", None, 100) + .await + .map(|page| serde_json::json!(page.keys)), + _ => return Err(EdgeError::bad_request("unknown controlled KV operation")), + }; + let (status, reason) = match &outcome { + Ok(_) => ("success", None), + Err(KvError::OpenFailed { reason }) => ("open-failed", Some(format!("{reason:?}"))), + Err(_) => ("operation-failed", None), + }; + receipt("declared_kv_result", Some(status), reason); + report["kv_outcome"] = outcome?; + Ok(report.to_string()) +} + struct ObserveRequest; #[async_trait::async_trait(?Send)] impl edgezero_core::middleware::Middleware for ObserveRequest { @@ -292,24 +416,25 @@ impl edgezero_core::middleware::Middleware for ObserveRequest { mut ctx: RequestContext, next: edgezero_core::middleware::Next<'_>, ) -> Result { - if ctx.request().extensions().get::().is_none() { + if let Some(captured) = ctx.extensions().get::>>>() { + *captured.lock().expect("request client capture lock") = ctx.http_client(); + } + if ctx.extensions().get::().is_none() { let candidate = ctx - .request() .headers() .get("x-request-token") .and_then(|value| value.to_str().ok()) .unwrap_or("missing"); let observation = observe(candidate, None); - ctx.request_mut().extensions_mut().insert(observation); + emit( + serde_json::json!({"event":"request_start","instance":observation.instance, + "ordinal":observation.ordinal,"token":candidate,"correlation":observation.correlation,"native_id":observation.native_id}), + ); + ctx.extensions_mut().insert(observation); } - let lifetime = ctx - .request() - .extensions() - .get::() - .cloned(); + let lifetime = ctx.extensions().get::().cloned(); let finalization = Finalize( - ctx.request() - .headers() + ctx.headers() .get("x-request-token") .and_then(|value| value.to_str().ok()) .unwrap_or("missing") @@ -331,11 +456,434 @@ mod tests { use edgezero_core::app::Hooks; use edgezero_core::body::Body; use edgezero_core::http::request_builder; + use edgezero_core::outbound::{ + HttpClient, OutboundBatch, OutboundBatchDriverEvent, OutboundBatchTermination, + OutboundCollectedSlot, OutboundDispatchRequest, OutboundExchangeOwner, OutboundFuture, + OutboundHead, OutboundHttpClient, OutboundPending, OutboundPreparedBatch, OutboundSendMode, + OutboundTime, + }; + use edgezero_core::{ + BodyPayload, BudgetSource, Deadline, IndependentOutboundProgress, OutboundProgressRuntime, + OutboundScopeOwner, + }; use futures::executor::block_on; + use std::rc::Rc; + use std::sync::Mutex; + use std::task::Poll; + use std::time::Duration; + + struct NamedClient { + name: &'static str, + time: OutboundTime, + calls: Arc, + } + + impl NamedClient { + fn new(name: &'static str) -> Self { + Self { + name, + time: crate::outbound_test_support::TestTime::default().pair(), + calls: Arc::new(AtomicUsize::new(0)), + } + } + } + + impl OutboundHttpClient for NamedClient { + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn time(&self) -> OutboundTime { + self.time.clone() + } + + fn validate_supported_options( + &self, + _request: &OutboundRequest, + _mode: OutboundSendMode, + ) -> Result<(), EdgeError> { + Ok(()) + } + + fn dispatch_head(&self, _request: OutboundDispatchRequest) -> OutboundFuture { + self.calls.fetch_add(1, Ordering::SeqCst); + Box::pin(futures::future::ready(Err(EdgeError::bad_request( + self.name, + )))) + } + + fn begin(&self, _request: OutboundDispatchRequest) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); + Err(EdgeError::bad_request(self.name)) + } + + fn start_batch_until(&self, batch: OutboundPreparedBatch) -> OutboundBatch { + let (context, requests) = batch.into_slots(); + if requests.is_empty() + && context + .observation_cutoff() + .is_expired_at(context.clock().now()) + { + return OutboundBatch::cutoff(context); + } + let events = requests + .into_iter() + .map(|(index, _request)| { + self.calls.fetch_add(1, Ordering::SeqCst); + OutboundBatchDriverEvent::Item { + index, + completed_at: self.time.clock().now(), + outcome: Err(EdgeError::bad_request(self.name)), + } + }) + .collect::>(); + OutboundBatch::from_driver( + context, + futures::stream::iter(events), + OutboundExchangeOwner::new(|| {}), + ) + } + } + + #[test] + fn early_body_batch_attachment_survives_eof_and_closes_on_response_drop() { + for abandon_before_handoff in [false, true] { + let scripted = NamedClient::new("scripted terminal failure"); + let calls = Arc::clone(&scripted.calls); + let observation_cutoff = scripted.time.clock().deadline_after(Duration::from_secs(1)); + let client = HttpClient::with_client(scripted); + let polls = Arc::new(AtomicUsize::new(0)); + let (response, scope) = { + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let batch = owner.with(|| { + client.start_batch_until( + vec![OutboundRequest::get("https://example.com/early").unwrap()], + observation_cutoff, + ) + }); + assert_eq!( + calls.load(Ordering::SeqCst), + 1, + "start before handler return" + ); + let observed = Arc::clone(&polls); + let source = futures::stream::once(async move { + observed.fetch_add(1, Ordering::SeqCst); + let results = batch + .collect() + .await + .map_err(|failure| failure.into_parts().0)?; + assert_eq!(results.termination(), OutboundBatchTermination::Completed); + let report = serde_json::json!({"slots":results.slots().len()}); + Ok(bytes::Bytes::from(report.to_string())) + }); + let response = owner + .attach(Response::new(Body::from_stream(source))) + .expect("attach the exact early body owner"); + (response, scope) + }; + assert_eq!( + polls.load(Ordering::SeqCst), + 0, + "handler has returned without polling" + ); + scope + .check() + .expect("response owns the early batch lifetime"); + if abandon_before_handoff { + drop(response); + assert!(matches!(scope.check(), Err(EdgeError::Internal { .. }))); + assert_eq!(polls.load(Ordering::SeqCst), 0); + continue; + } + let (payload, body_owner) = response.into_body().into_parts(); + assert!(body_owner.is_some(), "handoff carries the early owner"); + let BodyPayload::Stream(source) = payload else { + panic!("batch report is streamed"); + }; + let bytes = block_on(Body::from_stream(source).into_bytes_bounded(4096)).unwrap(); + assert_eq!( + serde_json::from_slice::(&bytes).unwrap(), + serde_json::json!({"slots":1}) + ); + assert_eq!(polls.load(Ordering::SeqCst), 1); + scope + .check() + .expect("EOF alone is not transmission completion"); + drop(body_owner); + assert!(matches!(scope.check(), Err(EdgeError::Internal { .. }))); + } + } + + #[test] + fn attached_body_metadata_is_rejected_before_outbound_dispatch() { + let scripted = NamedClient::new("must not dispatch attached request metadata"); + let calls = Arc::clone(&scripted.calls); + let client = HttpClient::with_client(scripted); + let owner = OutboundScopeOwner::response_body(); + let scope = owner.scope(); + let response = owner + .attach(Response::new(Body::empty())) + .expect("response attachment"); + let request = OutboundRequest::post("https://example.com/invalid-attachment") + .unwrap() + .body(response.into_body()); + let error = + block_on(client.send(request)).expect_err("response metadata is not a request payload"); + assert_eq!(error.status(), 400); + assert_eq!(calls.load(Ordering::SeqCst), 0); + assert!(matches!(scope.check(), Err(EdgeError::Internal { .. }))); + } + + #[test] + fn supplied_client_scope_reactivates_after_yield_and_releases_unpolled_send() { + let scripted = NamedClient::new("manual caller owns this exchange"); + let calls = Arc::clone(&scripted.calls); + let client = HttpClient::with_client(scripted); + let error = + block_on(client.send(OutboundRequest::get("https://example.com/manual").unwrap())) + .expect_err("manual caller must still reach the supplied client"); + assert_eq!(error.message(), "manual caller owns this exchange"); + assert_eq!(calls.load(Ordering::SeqCst), 1); + + let lifetime = Arc::new(()); + let released = Arc::downgrade(&lifetime); + let polls = Arc::new(AtomicUsize::new(0)); + let observed = Arc::clone(&polls); + let source = futures::stream::poll_fn(move |_cx| { + let _held = &lifetime; + observed.fetch_add(1, Ordering::SeqCst); + Poll::Pending + }); + let request = OutboundRequest::post("https://example.com/unpolled") + .unwrap() + .body(Body::from_stream(source)); + let callback = OutboundScopeOwner::callback(); + let mut first_poll = true; + let mut retained = None; + block_on(callback.run(async { + futures::future::poll_fn(|cx| { + if first_poll { + first_poll = false; + cx.waker().wake_by_ref(); + Poll::Pending + } else { + Poll::Ready(()) + } + }) + .await; + retained = Some(client.send(request)); + })); + let retained = retained.expect("send created after the second outer poll"); + assert!(!first_poll); + assert!(released.upgrade().is_some()); + assert_eq!(polls.load(Ordering::SeqCst), 0); + drop(callback); + assert!( + released.upgrade().is_none(), + "scope exit releases an unpolled root" + ); + let error = block_on(retained).expect_err("closed invocation cannot dispatch later"); + assert!(matches!(error, EdgeError::Internal { .. })); + assert_eq!(polls.load(Ordering::SeqCst), 0); + assert_eq!(calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn named_batch_exact_cutoff_preserves_preflight_without_io() { + for sibling in ["cutoff", "invalid", "own-timeout"] { + let time = crate::outbound_test_support::TestTime::default(); + let own_deadline = Deadline::at_instant(time.clock().now()); + time.advance_to( + own_deadline + .instant() + .checked_add(Duration::from_millis(1)) + .expect("entry instant"), + ); + let entered_at = time.clock().now(); + let calls = Arc::new(AtomicUsize::new(0)); + let client = HttpClient::with_client(NamedClient { + name: "must not dispatch", + time: time.pair(), + calls: Arc::clone(&calls), + }); + let first = OutboundRequest::get("https://example.com/first").unwrap(); + let second = OutboundRequest::get("https://example.com/second").unwrap(); + let second = match sibling { + "invalid" => second.stream_response(), + "own-timeout" => second.deadline(own_deadline), + _ => second, + }; + let batch = + client.start_batch_until(vec![first, second], Deadline::at_instant(entered_at)); + let results = block_on(batch.collect()).expect("exact cutoff is not driver EOF"); + let (collected, termination) = results.into_parts(); + assert_eq!(termination, OutboundBatchTermination::Cutoff); + assert_eq!(collected.len(), 2); + assert!(matches!(collected[0], OutboundCollectedSlot::Unresolved)); + let last = collected.into_iter().nth(1).expect("second slot"); + if sibling == "cutoff" { + assert!(matches!(last, OutboundCollectedSlot::Unresolved)); + } else { + let OutboundCollectedSlot::Resolved(rejected) = last else { + panic!("preflight outcome must be retained"); + }; + assert_eq!(rejected.elapsed, Duration::ZERO); + let error = rejected.outcome.expect_err("preflight failure"); + if sibling == "invalid" { + assert_eq!(error.status(), 400); + } else { + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + } + )); + } + } + assert_eq!(calls.load(Ordering::SeqCst), 0); + assert_eq!(time.clock().now(), entered_at); + } + } + + #[test] + fn capture_keeps_effective_client_for_success_and_rendered_handler_errors() { + for state_client in [false, true] { + let mut router = RouterService::builder() + .middleware(ObserveRequest) + .get("/cookies", cookies) + .get("/error", rendered_error); + if state_client { + router = + router.with_state(HttpClient::with_client(NamedClient::new("state client"))); + } + let app = App::new(router.build()); + let mut captures = Vec::new(); + for (path, expected_status) in [("/cookies", 200), ("/error", 400)] { + let captured: Arc>> = Arc::new(Mutex::new(None)); + let mut request = request_builder().uri(path).body(Body::empty()).unwrap(); + request + .extensions_mut() + .insert(HttpClient::with_client(NamedClient::new("request client"))); + request.extensions_mut().insert(Arc::clone(&captured)); + let response = block_on(app.router().oneshot(request)).unwrap(); + assert_eq!(response.status().as_u16(), expected_status); + captures.push(captured); + } + for captured in captures { + let client = captured + .lock() + .unwrap() + .take() + .expect("capture before handler"); + let error = + block_on(client.send(OutboundRequest::get("https://example.com").unwrap())) + .unwrap_err(); + assert_eq!( + error.message(), + if state_client { + "state client" + } else { + "request client" + } + ); + assert!( + captured.lock().unwrap().take().is_none(), + "one request-owned transfer" + ); + } + } + } + + #[test] + fn capture_does_not_create_a_client_for_absent_or_unrouted_requests() { + let app = App::build::(edgezero_core::PlatformMetadata::default()).unwrap(); + for (path, status) in [("/cookies", 200), ("/unknown", 404)] { + let captured: Arc>> = Arc::new(Mutex::new(None)); + let mut request = request_builder().uri(path).body(Body::empty()).unwrap(); + request.extensions_mut().insert(Arc::clone(&captured)); + if status == 404 { + request + .extensions_mut() + .insert(HttpClient::with_client(NamedClient::new("not routed"))); + } + let response = block_on(app.router().oneshot(request)).unwrap(); + assert_eq!(response.status().as_u16(), status); + assert!(captured.lock().unwrap().take().is_none()); + } + } + + #[test] + fn retained_app_retries_failed_configuration_and_reuses_success() { + static ATTEMPTS: AtomicUsize = AtomicUsize::new(0); + struct RetryApp; + impl Hooks for RetryApp { + fn configure(_app: &mut App) -> Result<(), EdgeError> { + if ATTEMPTS.fetch_add(1, Ordering::SeqCst) == 0 { + Err(EdgeError::service_unavailable( + "injected configuration failure", + )) + } else { + Ok(()) + } + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + let cell = OnceLock::new(); + let platform = edgezero_core::PlatformMetadata::default(); + assert!(retained_app::(&cell, platform).is_err()); + assert!(cell.get().is_none()); + let first = retained_app::(&cell, platform).unwrap(); + let second = retained_app::(&cell, platform).unwrap(); + assert!(std::ptr::eq(first, second)); + assert_eq!(ATTEMPTS.load(Ordering::SeqCst), 2); + } + + #[test] + fn retained_app_returns_an_already_installed_winner() { + static CELL: OnceLock = OnceLock::new(); + struct RacingApp; + impl Hooks for RacingApp { + fn configure(app: &mut App) -> Result<(), EdgeError> { + let winner = App::with_name(RouterService::builder().build(), "winner"); + assert!(CELL.set(winner).is_ok()); + app.set_name("discarded"); + Ok(()) + } + + fn routes() -> Result { + Ok(RouterService::builder().build()) + } + } + + let app = + retained_app::(&CELL, edgezero_core::PlatformMetadata::default()).unwrap(); + assert_eq!(app.name(), "winner"); + } + + #[test] + fn probe_accepts_exact_body_bound_and_rejects_overflow() { + let app = App::build::(edgezero_core::PlatformMetadata::default()).unwrap(); + for (size, status) in [(1024 * 1024, 200), (1024 * 1024 + 1, 413)] { + let req = request_builder() + .uri("/probe/bounded") + .header("x-request-token", "bounded") + .body(Body::from(vec![b'x'; size])) + .unwrap(); + let response = block_on(app.router().oneshot(req)).unwrap(); + assert_eq!(response.status().as_u16(), status); + } + } #[test] fn retained_probe_echoes_each_request_and_preserves_cookies() { - let app = FixtureApp::build_app(); + let app = App::build::(edgezero_core::PlatformMetadata::default()).unwrap(); for (index, token) in ["one", "two"].into_iter().enumerate() { let req = request_builder() .uri(format!("/probe/{token}")) @@ -360,8 +908,8 @@ mod tests { #[test] fn rejects_remote_origins_and_keeps_concrete_apps_separate() { - let app = FixtureApp::build_app(); - let other = OtherApp::build_app(); + let app = App::build::(edgezero_core::PlatformMetadata::default()).unwrap(); + let other = App::build::(edgezero_core::PlatformMetadata::default()).unwrap(); let req = request_builder() .uri("/origin/x") .header("x-fixture-backend", "https://example.com/") diff --git a/tests/fixtures/reusable-app/crates/fixture-core/src/outbound_test_support.rs b/tests/fixtures/reusable-app/crates/fixture-core/src/outbound_test_support.rs new file mode 100644 index 000000000..4e235d55e --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-core/src/outbound_test_support.rs @@ -0,0 +1,140 @@ +use edgezero_core::{Deadline, EdgeError, MonotonicClock, MonotonicInstant, OutboundTime}; +use std::collections::BTreeMap; +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub(crate) struct TestTime { + state: Arc>, +} + +struct TestTimeState { + next_timer: usize, + now: MonotonicInstant, + waiters: BTreeMap, +} + +struct TestTimer { + clock: MonotonicClock, + deadline: Deadline, + id: usize, + state: Arc>, +} + +impl Default for TestTime { + fn default() -> Self { + Self { + state: Arc::new(Mutex::new(TestTimeState { + next_timer: 0, + now: MonotonicInstant::now(), + waiters: BTreeMap::new(), + })), + } + } +} + +impl TestTime { + pub(crate) fn advance_to(&self, now: MonotonicInstant) { + let wakers = { + let mut state = self.state.lock().expect("fake time"); + state.now = now; + state.waiters.values().cloned().collect::>() + }; + for waker in wakers { + waker.wake(); + } + } + + pub(crate) fn clock(&self) -> MonotonicClock { + let state = Arc::clone(&self.state); + MonotonicClock::new(move || state.lock().expect("clock").now) + } + + pub(crate) fn pair(&self) -> OutboundTime { + let state = Arc::clone(&self.state); + OutboundTime::new(self.clock(), move |deadline, clock| { + let id = { + let mut state = state.lock().expect("timer registration"); + let id = state.next_timer; + state.next_timer = state.next_timer.checked_add(1).expect("timer id"); + id + }; + Box::pin(TestTimer { + clock, + deadline, + id, + state: Arc::clone(&state), + }) + }) + } +} + +impl std::future::Future for TestTimer { + type Output = Result<(), EdgeError>; + + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + if self.deadline.is_expired_at(self.clock.now()) { + std::task::Poll::Ready(Ok(())) + } else { + self.state + .lock() + .expect("timer waiter") + .waiters + .insert(self.id, cx.waker().clone()); + std::task::Poll::Pending + } + } +} + +#[expect(clippy::missing_trait_methods, reason = "Drop::pin_drop is unstable")] +impl Drop for TestTimer { + fn drop(&mut self) { + self.state + .lock() + .expect("timer cancellation") + .waiters + .remove(&self.id); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::task::{Context, Wake, Waker}; + use std::time::Duration; + + struct WakeCount(AtomicUsize); + + impl Wake for WakeCount { + fn wake(self: Arc) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + #[test] + fn paired_time_wakes_waiters_and_cancels_registrations() { + let time = TestTime::default(); + let pair = time.pair(); + let deadline = pair.clock().deadline_after(Duration::from_secs(1)); + let count = Arc::new(WakeCount(AtomicUsize::new(0))); + let waker = Waker::from(Arc::clone(&count)); + let mut context = Context::from_waker(&waker); + let mut completed = pair.wait_until(deadline); + let mut cancelled = pair.wait_until(deadline); + assert!(completed.as_mut().poll(&mut context).is_pending()); + assert!(cancelled.as_mut().poll(&mut context).is_pending()); + assert_eq!(time.state.lock().unwrap().waiters.len(), 2); + drop(cancelled); + assert_eq!(time.state.lock().unwrap().waiters.len(), 1); + time.advance_to(deadline.instant()); + assert_eq!(count.0.load(Ordering::SeqCst), 1); + assert!(matches!( + completed.as_mut().poll(&mut context), + std::task::Poll::Ready(Ok(())) + )); + assert!(time.state.lock().unwrap().waiters.is_empty()); + } +} diff --git a/tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs new file mode 100644 index 000000000..2a8224da4 --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification.rs @@ -0,0 +1,336 @@ +//! Opt-in, synthetic hosted probes through standard application/adaptor lifecycles. + +pub mod framing; +mod lifetimes; +mod overflow; +pub mod progress; +mod redirect; +pub mod response_headers; + +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll}; +use std::time::Duration; + +use bytes::Bytes; +use edgezero_core::app::{App, Hooks}; +use edgezero_core::context::RequestContext; +use edgezero_core::http::{Method, Response, StatusCode, response_builder}; +use edgezero_core::ingress::{AdmissionDecision, IngressGrant}; +use edgezero_core::response_egress::{ + ResponseEgressCompletion, ResponseEgressDeadline, ResponseEgressResource, +}; +use edgezero_core::router::RouterService; +use edgezero_core::{Body, EdgeError, action}; +use futures::Stream; +use serde_json::{Value, json}; + +const CASES: &[&str] = &[ + "normal", + "head", + "204", + "205", + "304", + "slow", + "nonread", + "disconnect", + "cancel", + "pending", + "expired", + "source-error", + "conversion-error", +]; + +pub struct QualificationApp; + +#[derive(Default)] +struct Counters { + polls: u64, + polls_after_terminal: u64, + resource_drops: u64, + sequence: u32, + source_drops: u64, + terminals: u64, +} + +struct ProbeTrace { + artifact: String, + case: String, + counters: Mutex, + probe: String, + revision: String, + run: String, +} + +struct ResourceDropProbe(Arc); + +struct ProbeGrant { + resource: ResponseEgressResource, + trace: Arc, +} + +struct ProbeSource { + chunk: Bytes, + remaining: u32, + trace: Arc, +} + +impl Hooks for QualificationApp { + fn configure(app: &mut App) -> Result<(), EdgeError> { + let revision = option_env!("EDGEZERO_QUALIFICATION_REVISION").unwrap_or(""); + let artifact = option_env!("EDGEZERO_QUALIFICATION_ARTIFACT").unwrap_or(""); + if !hex_identity(revision, 40) || !hex_identity(artifact, 64) { + return Err(EdgeError::internal(std::io::Error::other( + "qualification build identity is absent", + ))); + } + app.set_name("lifecycle-qualification"); + app.set_ingress_admission_policy(move |head| { + let token = |name| head.headers().get(name).and_then(|value| value.to_str().ok()) + .filter(|value| valid_token(value)).map(str::to_owned); + let case = head.target().path().strip_prefix("/qualification/") + .filter(|name| CASES.contains(name)); + let (Some(run), Some(probe), Some(case)) = (token("x-qualification-run"), token("x-qualification-probe"), case) else { + let mut response = Response::new(Body::from("invalid qualification probe\n")); + *response.status_mut() = StatusCode::BAD_REQUEST; + return AdmissionDecision::Refuse { completion: ResponseEgressCompletion::empty(), + response }; + }; + let trace = Arc::new(ProbeTrace { artifact: artifact.to_owned(), case: case.to_owned(), + counters: Mutex::new(Counters::default()), probe, revision: revision.to_owned(), run }); + let (resource, ownership) = ResponseEgressCompletion::late_bound(); + let terminal_trace = Arc::clone(&trace); + let terminal = ResponseEgressCompletion::new(move |report| { + terminal_trace.record("terminal", |state| state.terminals += 1, json!({ + "outcome": format!("{:?}", report.outcome), "body_kind": format!("{:?}", report.body_kind), + "bytes_written": report.bytes_written, + "fallback": report.fallback_disposition.map(|value| format!("{value:?}")), + })); + }); + AdmissionDecision::Admit { + completion: terminal.join(ownership), grant: IngressGrant::new(ProbeGrant { resource, trace }), + read_deadline: head.read_deadline_after(Duration::from_secs(1)), + } + }); + Ok(()) + } + + fn routes() -> Result { + Ok(RouterService::builder() + .get("/qualification/{case}", response_probe) + .route("/qualification/{case}", Method::HEAD, response_probe) + .build()) + } +} + +impl ProbeTrace { + fn event(&self, kind: &str, sequence: u32, fields: Value) -> Value { + let mut event = json!({ "schema": 1, "run": self.run, "probe": self.probe, "case": self.case, + "revision": self.revision, "artifact": self.artifact, "instance": "unobserved", + "event": kind, "sequence": sequence }); + if let (Some(object), Some(fields)) = (event.as_object_mut(), fields.as_object()) { + object.extend( + fields + .iter() + .map(|(name, value)| (name.clone(), value.clone())), + ); + } + event + } + + fn record(&self, kind: &str, update: impl FnOnce(&mut Counters), fields: Value) { + let mut state = self + .counters + .lock() + .unwrap_or_else(|error| error.into_inner()); + update(&mut state); + let event = self.event(kind, state.sequence, fields); + state.sequence += 1; + log::info!(target: "edgezero_qualification", "{event}"); + } +} + +impl Drop for ProbeTrace { + fn drop(&mut self) { + let state = self + .counters + .lock() + .unwrap_or_else(|error| error.into_inner()); + let event = self.event( + "trace-closed", + state.sequence, + json!({ "terminals": state.terminals, + "source_drops": state.source_drops, "resource_drops": state.resource_drops, + "polls": state.polls, "polls_after_terminal": state.polls_after_terminal }), + ); + log::info!(target: "edgezero_qualification", "{event}"); + } +} + +impl Drop for ResourceDropProbe { + fn drop(&mut self) { + self.0.record( + "resource-drop", + |state| state.resource_drops += 1, + json!({}), + ); + } +} + +impl ProbeSource { + fn new(trace: Arc) -> Self { + let large = matches!( + trace.case.as_str(), + "slow" | "nonread" | "disconnect" | "cancel" + ); + Self { + chunk: Bytes::from(vec![b'x'; if large { 16 * 1024 } else { 1024 }]), + remaining: if large { 512 } else { 3 }, + trace, + } + } +} + +impl Stream for ProbeSource { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + { + let mut state = self + .trace + .counters + .lock() + .unwrap_or_else(|error| error.into_inner()); + state.polls += 1; + if state.terminals != 0 { + state.polls_after_terminal += 1; + } + } + if self.trace.case == "pending" { + return Poll::Pending; + } + if self.remaining == 0 { + return Poll::Ready(None); + } + if self.trace.case == "source-error" && self.remaining == 2 { + self.remaining = 0; + return Poll::Ready(Some(Err(EdgeError::bad_gateway( + "synthetic qualification source error", + )))); + } + self.remaining -= 1; + Poll::Ready(Some(Ok(self.chunk.clone()))) + } +} + +impl Drop for ProbeSource { + fn drop(&mut self) { + self.trace + .record("source-drop", |state| state.source_drops += 1, json!({})); + } +} + +fn hex_identity(value: &str, length: usize) -> bool { + value.len() == length + && value + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) +} + +fn valid_token(value: &str) -> bool { + !value.is_empty() + && value.len() <= 64 + && value + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') +} + +#[action] +async fn response_probe(ctx: RequestContext) -> Result { + let grant = ctx + .take_ingress_grant() + .and_then(|grant| grant.downcast::().ok()) + .ok_or_else(|| { + EdgeError::internal(std::io::Error::other("qualification grant is absent")) + })?; + grant + .resource + .install(ResourceDropProbe(Arc::clone(&grant.trace))) + .map_err(|_error| { + EdgeError::internal(std::io::Error::other("qualification resource is closed")) + })?; + let case = grant.trace.case.clone(); + let status = match case.as_str() { + "204" => StatusCode::NO_CONTENT, + "205" => StatusCode::RESET_CONTENT, + "304" => StatusCode::NOT_MODIFIED, + _ => StatusCode::OK, + }; + let mut response = response_builder() + .status(status) + .header("content-type", "application/octet-stream") + .body(Body::from_stream(ProbeSource::new(grant.trace))) + .map_err(EdgeError::internal)?; + if case == "conversion-error" { + response.headers_mut().insert( + "content-length", + "not-a-length".parse().map_err(EdgeError::internal)?, + ); + } + response + .extensions_mut() + .insert(ResponseEgressDeadline::after(if case == "expired" { + Duration::ZERO + } else { + Duration::from_millis(500) + })); + Ok(response) +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::StreamExt as _; + + #[test] + fn finite_source_releases_once_and_never_polls_after_terminal() { + let trace = test_trace("normal"); + let mut source = ProbeSource::new(Arc::clone(&trace)); + let mut bytes = 0; + futures::executor::block_on(async { + while let Some(chunk) = source.next().await { + bytes += chunk.expect("source chunk").len(); + } + }); + assert_eq!(bytes, 3072); + drop(source); + let counters = trace.counters.lock().expect("counters"); + assert_eq!(counters.source_drops, 1); + assert_eq!(counters.polls, 4); + assert_eq!(counters.polls_after_terminal, 0); + } + + #[test] + fn pending_source_is_pending_and_source_failure_is_real() { + let trace = test_trace("pending"); + let mut source = Box::pin(ProbeSource::new(trace)); + let mut cx = Context::from_waker(futures::task::noop_waker_ref()); + assert!(source.as_mut().poll_next(&mut cx).is_pending()); + let trace = test_trace("source-error"); + let source = ProbeSource::new(trace); + let chunks = futures::executor::block_on(source.collect::>()); + assert_eq!(chunks.len(), 2); + assert!(chunks.last().expect("source error").is_err()); + } + + fn test_trace(case: &str) -> Arc { + Arc::new(ProbeTrace { + artifact: "b".repeat(64), + case: case.to_owned(), + counters: Mutex::new(Counters::default()), + probe: "test-1".to_owned(), + revision: "a".repeat(40), + run: "test-run".to_owned(), + }) + } +} diff --git a/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/framing.rs b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/framing.rs new file mode 100644 index 000000000..83bfba27d --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/framing.rs @@ -0,0 +1,125 @@ +//! Literal response sources for adapter framing and SDK-reference probes. + +use bytes::Bytes; +use edgezero_core::context::RequestContext; +use edgezero_core::http::{Response, StatusCode, response_builder}; +use edgezero_core::response_egress::ResponseEgressDeadline; +use edgezero_core::time::Deadline; +use edgezero_core::{Body, EdgeError, action}; +use futures::stream; + +pub fn source(case: &str) -> Result { + let mut builder = response_builder(); + let body = match case { + "buffered" | "head" | "expired" => Body::from("abcdef"), + "empty" => Body::empty(), + "explicit" => { + builder = builder.header("content-length", "6"); + Body::from("abcdef") + } + "mismatch" => { + builder = builder.header("content-length", "7"); + Body::from("abcdef") + } + "trailers" => { + builder = builder.header("trailer", "private-probe-field"); + Body::from("must-not-escape") + } + "head-explicit" | "not-modified-explicit" => { + builder = builder.header("content-length", "6"); + if case == "not-modified-explicit" { + builder = builder.status(StatusCode::NOT_MODIFIED); + } + Body::from("abcdef") + } + "not-modified" | "no-content" | "reset-content" => { + builder = builder.status(match case { + "not-modified" => StatusCode::NOT_MODIFIED, + "no-content" => StatusCode::NO_CONTENT, + _ => StatusCode::RESET_CONTENT, + }); + Body::from("abcdef") + } + "streamed" | "declared-stream" | "short-stream" => { + if case != "streamed" { + builder = builder.header( + "content-length", + if case == "short-stream" { "7" } else { "6" }, + ); + } + Body::stream(stream::iter([ + Bytes::from_static(b"abc"), + Bytes::from_static(b"def"), + ])) + } + "compress-buffered" | "compress-declared-stream" => { + builder = builder + .header("content-type", "text/plain") + .header("x-compress-hint", "on"); + let bytes = Bytes::from("compressible-source-".repeat(512)); + if case == "compress-declared-stream" { + builder = builder.header("content-length", bytes.len()); + Body::stream(stream::iter([bytes])) + } else { + Body::from_bytes(bytes) + } + } + _ => return Err(EdgeError::bad_request("unknown controlled framing case")), + }; + builder.body(body).map_err(EdgeError::internal) +} + +#[action] +pub(super) async fn probe(ctx: RequestContext) -> Result { + let case = ctx + .path_params() + .get("case") + .ok_or_else(|| EdgeError::bad_request("missing controlled framing case"))?; + if case == "rendered-not-found" { + return Err(EdgeError::not_found("/controlled-framing-missing")); + } + let mut response = source(case)?; + if case == "expired" { + response + .extensions_mut() + .insert(ResponseEgressDeadline::at(Deadline::at_instant( + ctx.monotonic_clock().now(), + ))); + } + Ok(response) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sources_do_not_prepopulate_implicit_framing() { + for case in [ + "buffered", + "empty", + "head", + "not-modified", + "no-content", + "reset-content", + "streamed", + "compress-buffered", + ] { + let response = source(case).unwrap(); + assert!(!response.headers().contains_key("content-length"), "{case}"); + assert!( + !response.headers().contains_key("transfer-encoding"), + "{case}" + ); + } + for case in [ + "explicit", + "head-explicit", + "not-modified-explicit", + "declared-stream", + ] { + assert_eq!(source(case).unwrap().headers()["content-length"], "6"); + } + assert!(source("uncontrolled").is_err()); + } +} diff --git a/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/lifetimes.rs b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/lifetimes.rs new file mode 100644 index 000000000..1aa6da1dc --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/lifetimes.rs @@ -0,0 +1,638 @@ +use edgezero_core::context::RequestContext; +use edgezero_core::http::{HeaderMap, Method, Response, Uri, response_builder}; +use edgezero_core::{ + Body, Deadline, EdgeError, HttpClient, MonotonicClock, MonotonicInstant, OutboundBatch, + OutboundBatchCheck, OutboundBatchNext, OutboundCollectedSlot, OutboundRequest, + OutboundResponse, OutboundSlotResult, action, +}; +use serde_json::{Value, json}; +use std::time::Duration; + +struct Probe { + case: String, + token: String, + base: Uri, +} + +struct Timeline { + clock: MonotonicClock, + start: MonotonicInstant, + events: Vec, +} + +#[derive(Default)] +struct Report { + items: Vec, + slots: Vec<&'static str>, + termination: Option, + error: Option, + origin: Option, + mediator: Option, +} + +impl Probe { + fn parse(case: &str, headers: &HeaderMap) -> Result { + if !matches!( + case, + "slow-origin" | "fast-origin" | "begin-progress" | "batch-progress" + ) { + return Err(invalid()); + } + let token = field(headers, "x-request-token")?; + if token.is_empty() + || token.len() > 64 + || !token + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + { + return Err(invalid()); + } + let target = field(headers, "x-fixture-backend")?; + if target.len() > 2048 || target.contains('#') { + return Err(invalid()); + } + let base: Uri = target.parse().map_err(|_| invalid())?; + if !crate::is_loopback_http(&base) + || !base.port_u16().is_some_and(|port| port != 0) + || base + .authority() + .is_some_and(|authority| authority.as_str().contains('@')) + || base.query().is_some() + || base.path() != format!("/lifetimes/{token}/{case}") + { + return Err(invalid()); + } + Ok(Self { + case: case.to_owned(), + token: token.to_owned(), + base, + }) + } + + fn request(&self, role: &str, timeline: &Timeline) -> Result { + let uri = format!("{}/{role}", self.base) + .parse() + .map_err(|_| invalid())?; + let mut request = OutboundRequest::new(Method::GET, uri)? + .timeout(Duration::from_secs(if role == "origin" { 15 } else { 1 })) + .max_response_bytes(64) + .header( + "x-fixture-at-us", + timeline.elapsed_us(timeline.clock.now()).to_string(), + )?; + if role == "origin" { + request = request.stream_response().max_decoded_response_bytes(64); + } + Ok(request) + } +} + +fn invalid() -> EdgeError { + EdgeError::bad_request("invalid lifetime probe") +} + +fn field<'a>(headers: &'a HeaderMap, name: &str) -> Result<&'a str, EdgeError> { + let mut values = headers.get_all(name).iter(); + let value = values.next().ok_or_else(invalid)?; + if values.next().is_some() { + return Err(invalid()); + } + value.to_str().map_err(|_| invalid()) +} + +impl Timeline { + fn new(clock: MonotonicClock, start: MonotonicInstant) -> Self { + Self { + clock, + start, + events: Vec::new(), + } + } + + fn elapsed_us(&self, at: MonotonicInstant) -> u64 { + micros(at.saturating_duration_since(self.start)) + } + + fn remaining(&self, millis: u64) -> Duration { + (self.start + Duration::from_millis(millis)).saturating_duration_since(self.clock.now()) + } + + fn observation_cutoff(&self) -> Deadline { + Deadline::at_instant(self.start + Duration::from_millis(534)) + } + + fn event(&mut self, op: &str, at: MonotonicInstant, result: Value) { + self.events.push(json!({"op":op,"at_us":self.elapsed_us(at), + "duration_us":micros(self.clock.now().saturating_duration_since(at)),"result":result})); + } + + async fn wait_until(&self, ctx: &RequestContext, millis: u64) -> Result<(), EdgeError> { + let remaining = self.remaining(millis); + if !remaining.is_zero() { + super::progress::pause(ctx, remaining)?.await; + } + if !self.remaining(millis).is_zero() { + return Err(EdgeError::internal(std::io::Error::other( + "lifetime timer returned before its target", + ))); + } + Ok(()) + } +} + +fn micros(duration: Duration) -> u64 { + u64::try_from(duration.as_micros()).unwrap_or(u64::MAX) +} + +impl Report { + fn json(&self, probe: &Probe, timeline: &Timeline) -> Value { + json!({"schema":1,"case":probe.case,"token":probe.token,"qualification":false, + "events":timeline.events,"items":self.items,"slots":self.slots, + "termination":self.termination,"error":self.error,"origin":self.origin,"mediator":self.mediator}) + } +} + +fn error_report(error: &EdgeError) -> Value { + let reason = match error { + EdgeError::GatewayTimeout { cause, .. } => Some(format!("{cause:?}")), + EdgeError::BadGateway { reason, .. } => Some(format!("{reason:?}")), + EdgeError::ResponseTooLarge { reason, .. } => Some(format!("{reason:?}")), + _ => None, + }; + json!({"kind":error.kind(),"status":error.status().as_u16(),"reason":reason}) +} + +async fn response_report(response: Result) -> Value { + let outcome = match response { + Ok(response) => { + let status = response.status().as_u16(); + response + .into_bytes_bounded(64) + .await + .map(|bytes| json!({"status":status,"bytes":bytes.to_vec()})) + } + Err(error) => Err(error), + }; + outcome.unwrap_or_else(|error| json!({"error":error_report(&error)})) +} + +async fn record_item(report: &mut Report, index: usize, slot: OutboundSlotResult, via: &str) { + report.items.push( + json!({"index":index,"via":via,"elapsed_us":micros(slot.elapsed), + "outcome":response_report(slot.outcome).await}), + ); +} + +async fn record_slots(report: &mut Report, slots: Vec) { + for (index, slot) in slots.into_iter().enumerate() { + let state = match slot { + OutboundCollectedSlot::AlreadyDelivered => "AlreadyDelivered", + OutboundCollectedSlot::Unresolved => "Unresolved", + OutboundCollectedSlot::Resolved(slot) => { + record_item(report, index, slot, "collect").await; + "Resolved" + } + }; + report.slots.push(state); + } +} + +async fn collect(batch: OutboundBatch, report: &mut Report, timeline: &mut Timeline) { + let at = timeline.clock.now(); + let result = batch.collect().await; + timeline.event( + "collect", + at, + json!(if result.is_ok() { "ok" } else { "error" }), + ); + let slots = match result { + Ok(results) => { + let (slots, termination) = results.into_parts(); + report.termination = Some(format!("{termination:?}")); + slots + } + Err(failure) => { + let (error, slots) = failure.into_parts(); + report.error = Some(error_report(&error)); + slots + } + }; + record_slots(report, slots).await; +} + +async fn check( + batch: &mut OutboundBatch, + report: &mut Report, + timeline: &mut Timeline, +) -> Result { + let at = timeline.clock.now(); + let result = batch.check(); + timeline.event( + "check", + at, + json!(match &result { + Ok(OutboundBatchCheck::Item(_)) => "Item", + Ok(OutboundBatchCheck::Finished(_)) => "Finished", + Ok(OutboundBatchCheck::Pending) => "Pending", + Err(_) => "error", + }), + ); + match result? { + OutboundBatchCheck::Item(item) => { + let (index, slot) = item.into_parts(); + record_item(report, index, slot, "check").await; + Ok(true) + } + OutboundBatchCheck::Finished(_) | OutboundBatchCheck::Pending => Ok(false), + } +} + +async fn run( + ctx: &RequestContext, + probe: &Probe, + client: &HttpClient, + report: &mut Report, + timeline: &mut Timeline, +) -> Result<(), EdgeError> { + let origin = if probe.case == "batch-progress" { + None + } else { + timeline.wait_until(ctx, 2).await?; + let request = probe.request("origin", timeline)?; + let at = timeline.clock.now(); + let result = client.begin(request).await; + timeline.event( + "begin", + at, + json!(if result.is_ok() { "scheduled" } else { "error" }), + ); + Some(result?) + }; + if probe.case == "begin-progress" { + let at = timeline.clock.now(); + timeline.event("quiet-start", at, Value::Null); + super::progress::pause(ctx, Duration::from_millis(100))?.await; + let at = timeline.clock.now(); + timeline.event("quiet-end", at, Value::Null); + let at = timeline.clock.now(); + let result = origin.ok_or_else(invalid)?.collect().await; + timeline.event( + "origin-head", + at, + json!(if result.is_ok() { "ok" } else { "error" }), + ); + report.origin = Some(response_report(result).await); + return Ok(()); + } + + timeline.wait_until(ctx, 34).await?; + let requests = ["a", "b", "c"] + .into_iter() + .map(|role| probe.request(role, timeline)) + .collect::, _>>()?; + let observation_cutoff = timeline.observation_cutoff(); + let at = timeline.clock.now(); + let mut batch = client.start_batch_until(requests, observation_cutoff); + timeline.event("batch-start", at, Value::Null); + + if probe.case == "batch-progress" { + let request = probe.request("quiet", timeline)?; + let at = timeline.clock.now(); + timeline.event("quiet-start", at, Value::Null); + let result = client.send(request).await; + let at = timeline.clock.now(); + timeline.event("quiet-end", at, Value::Null); + let quiet = response_report(result).await; + if let Some(error) = quiet.get("error") { + report.error = Some(error.clone()); + } + collect(batch, report, timeline).await; + return Ok(()); + } + + let at = timeline.clock.now(); + let result = origin.ok_or_else(invalid)?.collect().await; + timeline.event( + "origin-head", + at, + json!(if result.is_ok() { "ok" } else { "error" }), + ); + let response = result?; + if probe.case == "slow-origin" { + timeline.wait_until(ctx, 701).await?; + // Three slots plus one terminal observation; never spin on Pending. + for _ in 0..4 { + match check(&mut batch, report, timeline).await { + Ok(true) => {} + Ok(false) => break, + Err(error) => { + report.error = Some(error_report(&error)); + break; + } + } + } + collect(batch, report, timeline).await; + let at = timeline.clock.now(); + report.origin = Some(response_report(Ok(response)).await); + timeline.event("origin-body", at, Value::Null); + return Ok(()); + } + + let at = timeline.clock.now(); + report.origin = Some(response_report(Ok(response)).await); + timeline.event("origin-body", at, Value::Null); + if let Err(error) = check(&mut batch, report, timeline).await { + report.error = Some(error_report(&error)); + } + let at = timeline.clock.now(); + let result = batch.next().await; + timeline.event( + "next", + at, + json!(match &result { + Ok(OutboundBatchNext::Item(_)) => "Item", + Ok(OutboundBatchNext::Finished(_)) => "Finished", + Err(_) => "error", + }), + ); + match result { + Ok(OutboundBatchNext::Item(item)) => { + let (index, slot) = item.into_parts(); + record_item(report, index, slot, "next").await; + } + Ok(OutboundBatchNext::Finished(_)) => {} + Err(error) => report.error = Some(error_report(&error)), + } + collect(batch, report, timeline).await; + + let request = probe + .request("mediator", timeline)? + .deadline(observation_cutoff); + let at = timeline.clock.now(); + let remaining = observation_cutoff.instant().saturating_duration_since(at); + let result = client.send(request).await; + timeline.event( + "mediator-send", + at, + json!(if result.is_ok() { "ok" } else { "error" }), + ); + let mut mediator = + json!({"sent_at_us":timeline.elapsed_us(at),"remaining_us":micros(remaining),"error":null}); + match result { + Ok(response) => mediator["outcome"] = response_report(Ok(response)).await, + Err(error) => mediator["error"] = error_report(&error), + } + report.mediator = Some(mediator); + Ok(()) +} + +#[action] +pub(super) async fn probe(ctx: RequestContext) -> Result { + let case = ctx.path_params().get("case").ok_or_else(invalid)?; + let probe = Probe::parse(case, ctx.headers())?; + let client = ctx.http_client().ok_or_else(|| { + EdgeError::internal(std::io::Error::other( + "lifetime probe has no injected client", + )) + })?; + let mut timeline = Timeline::new(ctx.monotonic_clock(), ctx.request_start()); + let mut report = Report::default(); + if let Err(error) = run(&ctx, &probe, &client, &mut report, &mut timeline).await + && report.error.is_none() + { + report.error = Some(error_report(&error)); + } + response_builder() + .header("content-type", "application/json") + .body(Body::json(&report.json(&probe, &timeline)).map_err(EdgeError::internal)?) + .map_err(EdgeError::internal) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::outbound_test_support::TestTime; + use bytes::Bytes; + use edgezero_core::http::{Method, StatusCode, request_builder}; + use edgezero_core::{Body, BudgetSource, ContentTypeProvenance, ResponseMode}; + use futures::{StreamExt, executor::block_on}; + use std::{cell::Cell, rc::Rc}; + + fn headers(case: &str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("x-request-token", "Run_1-a".parse().unwrap()); + headers.insert( + "x-fixture-backend", + format!("http://127.0.0.1:1234/lifetimes/Run_1-a/{case}") + .parse() + .unwrap(), + ); + headers + } + + fn response(body: Body) -> OutboundResponse { + OutboundResponse::new( + Method::GET, + StatusCode::OK, + HeaderMap::new(), + body, + TestTime::default().clock(), + ContentTypeProvenance::Unknown, + ) + } + + #[test] + fn requests_use_actual_guest_elapsed_and_original_limits() { + let time = TestTime::default(); + let start = time.clock().now(); + let timeline = Timeline::new(time.clock(), start); + for case in [ + "slow-origin", + "fast-origin", + "begin-progress", + "batch-progress", + ] { + let probe = Probe::parse(case, &headers(case)).unwrap(); + assert_eq!(probe.case, case); + assert_eq!(probe.token, "Run_1-a"); + time.advance_to(start + Duration::from_micros(37_123)); + for role in ["origin", "a", "b", "c", "quiet", "mediator"] { + let parts = probe.request(role, &timeline).unwrap().into_parts(); + assert_eq!(parts.method, Method::GET); + assert_eq!(parts.uri.to_string(), format!("{}/{role}", probe.base)); + assert_eq!(parts.headers["x-fixture-at-us"], "37123"); + assert_eq!( + parts.timeout, + Some(Duration::from_secs(if role == "origin" { 15 } else { 1 })) + ); + if role == "origin" { + assert!(matches!(parts.response_mode, ResponseMode::Streamed)); + assert_eq!(parts.max_decoded_response_bytes, Some(64)); + } else { + assert!(matches!( + parts.response_mode, + ResponseMode::Buffered { max_bytes: 64 } + )); + } + } + } + } + + #[test] + fn input_rejects_uncontrolled_ambiguous_or_unimplemented_cases() { + for case in ["cpu-stretch", "", "other"] { + assert!(Probe::parse(case, &headers(case)).is_err()); + } + for target in [ + "https://127.0.0.1:1234/lifetimes/Run_1-a/slow-origin", + "http://example.com:1234/lifetimes/Run_1-a/slow-origin", + "http://user@127.0.0.1:1234/lifetimes/Run_1-a/slow-origin", + "http://127.0.0.1/lifetimes/Run_1-a/slow-origin", + "http://127.0.0.1:0/lifetimes/Run_1-a/slow-origin", + "http://127.0.0.1:1234/lifetimes/other/slow-origin", + "http://127.0.0.1:1234/lifetimes/Run_1-a/fast-origin", + "http://127.0.0.1:1234/lifetimes/Run_1-a/slow-origin?x=1", + "http://127.0.0.1:1234/lifetimes/Run_1-a/slow-origin#x", + ] { + let mut headers = headers("slow-origin"); + headers.insert("x-fixture-backend", target.parse().unwrap()); + assert!(Probe::parse("slow-origin", &headers).is_err(), "{target}"); + } + for name in ["x-request-token", "x-fixture-backend"] { + let mut duplicate = headers("slow-origin"); + duplicate.append(name, duplicate[name].clone()); + assert!(Probe::parse("slow-origin", &duplicate).is_err()); + duplicate.remove(name); + assert!(Probe::parse("slow-origin", &duplicate).is_err()); + } + for token in [ + "".to_owned(), + "a".repeat(65), + "a/b".to_owned(), + "a%2fb".to_owned(), + ] { + let mut headers = headers("slow-origin"); + headers.insert("x-request-token", token.parse().unwrap()); + assert!(Probe::parse("slow-origin", &headers).is_err()); + } + } + + #[test] + fn timeline_keeps_two_thirty_four_and_fixed_five_thirty_four_without_clock_loops() { + let time = TestTime::default(); + let start = time.clock().now(); + let mut timeline = Timeline::new(time.clock(), start); + assert_eq!(timeline.remaining(2), Duration::from_millis(2)); + time.advance_to(start + Duration::from_millis(2)); + assert_eq!(timeline.remaining(34), Duration::from_millis(32)); + time.advance_to(start + Duration::from_millis(34)); + let at = time.clock().now(); + time.advance_to(start + Duration::from_micros(34_017)); + timeline.event("batch-start", at, Value::Null); + assert_eq!( + timeline.events, + [json!({"op":"batch-start","at_us":34000,"duration_us":17,"result":null})] + ); + time.advance_to(start + Duration::from_millis(400)); + assert_eq!( + timeline.observation_cutoff().instant(), + start + Duration::from_millis(534) + ); + assert_eq!(timeline.remaining(534), Duration::from_millis(134)); + time.advance_to(start + Duration::from_millis(710)); + assert_eq!(timeline.remaining(701), Duration::ZERO); + assert_eq!(timeline.elapsed_us(time.clock().now()), 710_000); + } + + #[test] + fn collection_report_preserves_receipts_and_delivered_versus_unresolved_slots() { + let time = TestTime::default(); + let timeline = Timeline::new(time.clock(), time.clock().now()); + let probe = Probe::parse("slow-origin", &headers("slow-origin")).unwrap(); + let mut report = Report::default(); + block_on(record_item( + &mut report, + 0, + OutboundSlotResult::new( + Duration::from_micros(146_123), + Ok(response(Body::from("A"))), + ), + "check", + )); + block_on(record_slots( + &mut report, + vec![ + OutboundCollectedSlot::AlreadyDelivered, + OutboundCollectedSlot::Resolved(OutboundSlotResult::new( + Duration::from_micros(286_456), + Err(EdgeError::gateway_timeout_caused( + "private message", + BudgetSource::PerCallTimeout, + )), + )), + OutboundCollectedSlot::Unresolved, + ], + )); + let json = report.json(&probe, &timeline); + assert_eq!( + json["items"], + json!([ + {"index":0,"via":"check","elapsed_us":146123,"outcome":{"status":200,"bytes":[65]}}, + {"index":1,"via":"collect","elapsed_us":286456,"outcome":{"error":{"kind":"gateway_timeout","status":504,"reason":"PerCallTimeout"}}} + ]) + ); + assert_eq!( + json["slots"], + json!(["AlreadyDelivered", "Resolved", "Unresolved"]) + ); + for field in ["termination", "error", "origin", "mediator"] { + assert_eq!(json.get(field), Some(&Value::Null), "{field}"); + } + assert_eq!(json["qualification"], false); + assert_eq!(json["schema"], 1); + assert!(!json.to_string().contains("private message")); + } + + #[test] + fn report_body_consumption_is_bounded_and_errors_keep_actual_reasons() { + let polls = Rc::new(Cell::new(0)); + let count = Rc::clone(&polls); + let source = futures::stream::repeat(Bytes::from_static(&[b'x'; 64])).map(move |bytes| { + count.set(count.get() + 1); + Ok(bytes) + }); + let report = block_on(response_report(Ok(response(Body::from_stream(source))))); + assert_eq!( + report, + json!({"error":{"kind":"response_too_large","status":502,"reason":"BufferedBody"}}) + ); + assert!(polls.get() <= 2, "unbounded report body polling"); + assert_eq!( + error_report(&EdgeError::gateway_timeout_caused( + "private", + BudgetSource::RequestDeadline + )), + json!({"kind":"gateway_timeout","status":504,"reason":"RequestDeadline"}) + ); + assert_eq!( + error_report(&EdgeError::bad_request("private")), + json!({"kind":"bad_request","status":400,"reason":null}) + ); + } + + #[test] + fn route_requires_an_injected_context_client() { + let router = edgezero_core::router::RouterService::builder() + .get("/progress/lifetimes/{case}", probe) + .build(); + let mut request = request_builder() + .uri("/progress/lifetimes/slow-origin") + .body(Body::empty()) + .unwrap(); + *request.headers_mut() = headers("slow-origin"); + assert_eq!( + block_on(router.oneshot(request)).unwrap().status(), + StatusCode::INTERNAL_SERVER_ERROR + ); + } +} diff --git a/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/overflow.rs b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/overflow.rs new file mode 100644 index 000000000..1b66a1656 --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/overflow.rs @@ -0,0 +1,136 @@ +//! Synthetic byte-cap probes through the injected production client, not direct SDK requests. + +use bytes::Bytes; +use edgezero_core::context::RequestContext; +use edgezero_core::http::{Response, Uri, response_builder}; +use edgezero_core::outbound::{OutboundCachePolicy, OutboundRequest}; +use edgezero_core::{Body, EdgeError, action}; +use serde_json::json; +use std::time::Duration; + +fn invalid() -> EdgeError { + EdgeError::bad_request("invalid controlled overflow probe") +} + +fn upload_request(case: &str, uri: Uri) -> Result { + let (streamed, len) = match case { + "buffered-7" => (false, 7), + "buffered-8" | "upstream-413" => (false, 8), + "buffered-9" => (false, 9), + "streamed-7" => (true, 7), + "streamed-8" => (true, 8), + "streamed-9" => (true, 9), + _ => return Err(invalid()), + }; + let body = if streamed { + Body::stream(futures::stream::iter([ + Bytes::from_static(b"xxxx"), + Bytes::from(vec![b'x'; len - 4]), + ])) + } else { + Body::from(vec![b'x'; len]) + }; + Ok( + OutboundRequest::new(edgezero_core::http::Method::POST, uri)? + .body(body) + .max_request_body_bytes(8) + .max_response_header_bytes(4096) + .max_response_header_count(32) + .max_encoded_response_bytes(64) + .max_decoded_response_bytes(64) + .cache_policy(OutboundCachePolicy::Bypass) + .timeout(Duration::from_secs(2)) + .stream_response(), + ) +} + +#[action] +pub(super) async fn probe(ctx: RequestContext) -> Result { + let case = ctx.path_params().get("case").ok_or_else(invalid)?; + if case == "inbound" { + let received = ctx.body_bytes(1024).await?; + return response_builder() + .header("content-type", "application/json") + .body( + Body::json(&json!({"case":case,"received":received.len(),"qualification":false})) + .map_err(EdgeError::internal)?, + ) + .map_err(EdgeError::internal); + } + let token = ctx + .headers() + .get("x-request-token") + .and_then(|value| value.to_str().ok()) + .filter(|value| { + !value.is_empty() + && value.len() <= 64 + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + }) + .ok_or_else(invalid)?; + let uri: Uri = ctx + .headers() + .get("x-fixture-backend") + .and_then(|value| value.to_str().ok()) + .ok_or_else(invalid)? + .parse() + .map_err(|_| invalid())?; + if !crate::is_loopback_http(&uri) + || uri.port_u16().is_none() + || uri.query().is_some() + || uri.path() != format!("/overflow/{token}/{case}") + { + return Err(invalid()); + } + let request = upload_request(case, uri)?; + let client = ctx.http_client().ok_or_else(|| { + EdgeError::internal(std::io::Error::other( + "overflow probe has no injected client", + )) + })?; + let response = client.send(request).await?; + let report = json!({"case":case,"outcome":"response","upstream_status":response.status().as_u16(), + "retry_after":response.headers().get("retry-after").and_then(|value|value.to_str().ok()),"qualification":false}); + // This proves upload/head handling only. Never enter Fastly's failed G2 body-read path. + drop(response); + response_builder() + .header("content-type", "application/json") + .body(Body::json(&report).map_err(EdgeError::internal)?) + .map_err(EdgeError::internal) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn upload_shapes_are_bounded_and_use_streamed_response_heads() { + for mode in ["buffered", "streamed"] { + for len in [7_usize, 8, 9] { + let request = upload_request( + &format!("{mode}-{len}"), + Uri::from_static("http://127.0.0.1:1234/"), + ) + .unwrap() + .into_parts(); + assert_eq!(request.max_request_body_bytes, 8); + assert!(matches!( + request.response_mode, + edgezero_core::ResponseMode::Streamed + )); + assert_eq!(request.body.is_stream(), mode == "streamed"); + let bytes = + futures::executor::block_on(request.body.into_bytes_bounded(16)).unwrap(); + assert_eq!(bytes.as_ref(), vec![b'x'; len]); + } + } + assert!( + upload_request( + "streamed-1000000", + Uri::from_static("http://127.0.0.1:1234/") + ) + .is_err() + ); + } +} diff --git a/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs new file mode 100644 index 000000000..9d0d1e976 --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/progress.rs @@ -0,0 +1,1076 @@ +//! Fixture-only orchestration; SDK operations stay in the provider fixture crates. + +use bytes::Bytes; +use edgezero_core::app::{App, Hooks}; +use edgezero_core::context::RequestContext; +use edgezero_core::http::{Method, Response, Uri, response_builder}; +use edgezero_core::outbound::{ + OutboundCachePolicy, OutboundProxyMarker, OutboundRequest, ResponseDecoding, +}; +use edgezero_core::response_egress::{ResponseEgressObserver, ResponseEgressReport}; +use edgezero_core::router::RouterService; +use edgezero_core::time::{Deadline, MonotonicInstant}; +use edgezero_core::{Body, EdgeError, action}; +use futures::{ + StreamExt, + future::{LocalBoxFuture, join_all, poll_fn}, +}; +use serde_json::{Value, json}; +use std::sync::{ + Arc, OnceLock, + atomic::{AtomicBool, AtomicUsize, Ordering}, +}; +use std::time::Duration; + +pub type Job = LocalBoxFuture<'static, Result>; + +pub trait ProbeRuntime: Send + Sync { + fn adapter(&self) -> &'static str; + fn perform(&self, target: String) -> Job; + fn start(&self, target: String) -> Result; + fn pause(&self, duration: Duration) -> LocalBoxFuture<'static, ()>; +} + +pub struct ProgressHooks; +impl Hooks for ProgressHooks { + fn routes() -> Result { + Ok(RouterService::builder().build()) + } +} + +struct State { + runtime: Arc, + host_signal: Value, + dispatch_returned: Option>, +} + +pub(super) fn pause( + ctx: &RequestContext, + duration: Duration, +) -> Result, EdgeError> { + let state = ctx + .extensions() + .get::>() + .ok_or_else(probe_failure)?; + Ok(state.runtime.pause(duration)) +} + +struct HandlerReturned(Arc); + +struct FramingObserver; + +impl ResponseEgressObserver for FramingObserver { + fn complete(&self, report: &ResponseEgressReport) { + if report.framing_failures.application().is_some() + || report.framing_failures.fallback().is_some() + { + eprintln!( + "{}", + json!({"event":"framing-terminal", "application":report.framing_failures.application().map(|reason|format!("{reason:?}")), + "fallback":report.framing_failures.fallback().map(|reason|format!("{reason:?}")), + "outcome":format!("{:?}",report.outcome),"body_kind":format!("{:?}",report.body_kind), + "bytes_written":report.bytes_written,"disposition":report.fallback_disposition.map(|value|format!("{value:?}")),"qualification":false}) + ); + } + } +} +impl Drop for HandlerReturned { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } +} + +pub fn router( + runtime: Arc, + host_signal: Value, + dispatch_returned: Option>, +) -> RouterService { + RouterService::builder() + .with_state(Arc::new(State { + runtime, + host_signal, + dispatch_returned, + })) + .get("/progress/framing-trailers", framing_trailers) + .get("/progress/framing/{case}", super::framing::probe) + .route( + "/progress/framing/{case}", + Method::HEAD, + super::framing::probe, + ) + .get( + "/progress/synthesized/{case}", + super::response_headers::probe, + ) + .route( + "/progress/synthesized/{case}", + Method::HEAD, + super::response_headers::probe, + ) + .get("/progress/redirect", super::redirect::probe) + .get("/progress/lifetimes/{case}", super::lifetimes::probe) + .get("/progress/{case}", progress) + .post("/progress/{case}", progress) + .post("/progress/overflow/{case}", super::overflow::probe) + .build() +} + +pub fn application( + runtime: Arc, + host_signal: Value, + dispatch_returned: Option>, + platform: edgezero_core::PlatformMetadata, +) -> Result { + let mut app = App::with_name_and_platform( + router(runtime, host_signal, dispatch_returned), + "outbound-progress-probe", + platform, + ); + app.set_response_egress_observer(FramingObserver); + super::response_headers::configure(&mut app)?; + Ok(app) +} + +#[action] +async fn framing_trailers() -> Result { + response_builder() + .header("trailer", "private-token-secret") + .body(Body::from("private-token-secret")) + .map_err(EdgeError::internal) +} + +pub fn probe_failure() -> EdgeError { + EdgeError::internal(std::io::Error::other("outbound progress SDK probe failed")) +} + +fn observed(mut job: Job, closed: Arc, polls: Arc) -> Job { + Box::pin(poll_fn(move |cx| { + if !closed.load(Ordering::SeqCst) { + polls.fetch_add(1, Ordering::SeqCst); + } + job.as_mut().poll(cx) + })) +} + +#[action] +async fn progress(ctx: RequestContext) -> Result { + let state = ctx + .extensions() + .get::>() + .cloned() + .ok_or_else(probe_failure)?; + let case = ctx + .path_params() + .get("case") + .ok_or_else(probe_failure)? + .to_owned(); + if case == "empty" { + let before = format!("{:?}", ctx.body_kind()); + let request = ctx.into_request()?; + let streamed = request.body().is_stream(); + let method = request.method().as_str().to_owned(); + let body = request.into_body().into_bytes_bounded(64).await?; + return json_response(json!({"case":case, "adapter":state.runtime.adapter(), + "host_signal":state.host_signal, "body_kind_before":before, "body_is_stream":streamed, + "bytes_read":body.len(), "method":method, "qualification":false})); + } + if case.starts_with("coding-") || case.starts_with("relay-") { + return coding_probe(ctx, &case).await; + } + if case.starts_with("marker-") { + return marker_probe(ctx, &case).await; + } + let token = ctx + .headers() + .get("x-request-token") + .and_then(|v| v.to_str().ok()) + .filter(|s| { + !s.is_empty() + && s.len() <= 64 + && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') + }) + .ok_or_else(|| EdgeError::bad_request("invalid probe token"))? + .to_owned(); + if matches!( + case.as_str(), + "response-owned-ready" | "response-owned-timer-only" + ) { + return response_control(state, case, token); + } + let slots = match case.as_str() { + "eager-single" | "lazy-single" => 1, + "eager-batch" | "baseline-batch" | "response-owned" => 2, + _ => return Err(EdgeError::bad_request("unknown progress probe")), + }; + let base: Uri = ctx + .headers() + .get("x-fixture-backend") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| EdgeError::bad_request("missing probe origin"))? + .parse() + .map_err(|_| EdgeError::bad_request("invalid probe origin"))?; + if !crate::is_loopback_http(&base) + || base.port_u16().is_none() + || base.query().is_some() + || base.path() != format!("/g3/{token}") + { + return Err(EdgeError::bad_request( + "probe origin must be the controlled loopback path", + )); + } + state.runtime.perform(format!("{base}/start")).await?; + let closed = Arc::new(AtomicBool::new(false)); + let polls = Arc::new(AtomicUsize::new(0)); + let returned = Arc::new(AtomicBool::new(false)); + let _returned = HandlerReturned(Arc::clone(&returned)); + let mut jobs = Vec::new(); + if case == "lazy-single" || case == "baseline-batch" { + let client = ctx.http_client().ok_or_else(probe_failure)?; + let requests = (0..slots) + .map(|index| { + OutboundRequest::new( + Method::GET, + format!("{base}/slot-{index}") + .parse() + .map_err(|_| probe_failure())?, + ) + .map(|r| { + r.timeout(Duration::from_secs(2)) + .max_response_bytes(64) + .cache_policy(OutboundCachePolicy::Bypass) + }) + }) + .collect::, _>>()?; + let job: Job = if case == "lazy-single" { + Box::pin(async move { + client + .send(requests.into_iter().next().ok_or_else(probe_failure)?) + .await?; + Ok(0) + }) + } else { + let batch = client.start_batch_until(requests, Deadline::after(Duration::from_secs(3))); + Box::pin(async move { + let results = batch.collect().await.map_err(|_| probe_failure())?; + if results.slots().len() != slots + || results + .slots() + .iter() + .any(|s| !matches!(s, edgezero_core::OutboundCollectedSlot::Resolved(r) if r.outcome.is_ok())) + { + return Err(probe_failure()); + } + Ok(0) + }) + }; + jobs.push(observed(job, Arc::clone(&closed), Arc::clone(&polls))); + } else { + for index in 0..slots { + let job = state.runtime.start(format!("{base}/slot-{index}"))?; + jobs.push(observed(job, Arc::clone(&closed), Arc::clone(&polls))); + } + } + let response_owned = case == "response-owned"; + let finish = async move { + let pause_start = MonotonicInstant::now(); + state.runtime.pause(Duration::from_millis(100)).await; + let pause_ns = pause_start.elapsed().as_nanos() as u64; + state.runtime.perform(format!("{base}/end")).await?; + closed.store(true, Ordering::SeqCst); + let sizes = join_all(jobs) + .await + .into_iter() + .collect::, _>>()?; + Ok::<_, EdgeError>( + json!({"adapter":state.runtime.adapter(), "case":case, "token":token, "slots":slots, + "consumer_polls_before_window_end":polls.load(Ordering::SeqCst), "sizes":sizes,"pause_ns":pause_ns, + "handler_returned":returned.load(Ordering::SeqCst), + "dispatch_returned":state.dispatch_returned.as_ref().map(|r|r.load(Ordering::SeqCst)), + "revision":option_env!("EDGEZERO_QUALIFICATION_REVISION").unwrap_or("unrecorded"), "qualification":false}), + ) + }; + if response_owned { + let stream = futures::stream::once(async { Ok(Bytes::from_static(b" ")) }).chain( + futures::stream::once(async move { finish.await.map(|v| Bytes::from(v.to_string())) }), + ); + return response_builder() + .header("content-type", "application/json") + .body(Body::from_stream(stream)) + .map_err(EdgeError::internal); + } + json_response(finish.await?) +} + +async fn marker_probe(ctx: RequestContext, case: &str) -> Result { + let target: Uri = ctx + .headers() + .get("x-fixture-backend") + .and_then(|v| v.to_str().ok()) + .ok_or_else(probe_failure)? + .parse() + .map_err(|_| probe_failure())?; + let client = ctx + .http_client() + .ok_or_else(probe_failure)? + .with_proxy_marker(false); + let parts: Vec<_> = case.split('-').collect(); + let (method, path, setting) = if case == "marker-batch" { + ( + Method::GET, + "/marker/get/200/absent".to_owned(), + OutboundProxyMarker::Inherit, + ) + } else { + let [ + "marker", + method @ ("get" | "head"), + status @ ("200" | "204" | "304" | "302"), + shape @ ("absent" | "upstream" | "nominated"), + setting @ ("inherit" | "on" | "off"), + ] = parts.as_slice() + else { + return Err(EdgeError::bad_request("invalid marker probe")); + }; + let setting = match *setting { + "on" => OutboundProxyMarker::On, + "off" => OutboundProxyMarker::Off, + _ => OutboundProxyMarker::Inherit, + }; + ( + if *method == "head" { + Method::HEAD + } else { + Method::GET + }, + format!("/marker/{method}/{status}/{shape}"), + setting, + ) + }; + if !crate::is_loopback_http(&target) + || target.port_u16().is_none() + || target.query().is_some() + || target.path() != path + { + return Err(EdgeError::bad_request( + "marker probe requires controlled loopback origin", + )); + } + if case == "marker-batch" { + let requests = [OutboundProxyMarker::Inherit, OutboundProxyMarker::On] + .into_iter() + .map(|setting| { + OutboundRequest::new(Method::GET, target.clone()) + .map(|request| request.proxy_marker(setting).max_response_bytes(0)) + }) + .collect::, _>>()?; + let results = client + .send_all_until( + requests, + ctx.monotonic_clock().deadline_after(Duration::from_secs(2)), + ) + .await + .map_err(|failure| failure.into_parts().0)?; + let mut markers = Vec::new(); + for slot in results.into_parts().0 { + let edgezero_core::OutboundCollectedSlot::Resolved(slot) = slot else { + return Err(probe_failure()); + }; + let response = slot.outcome?; + markers.push( + response + .headers() + .get("x-edgezero-proxy") + .and_then(|v| v.to_str().ok()) + .map(str::to_owned), + ); + response.into_bytes_bounded(0).await?; + } + return json_response(json!({"case":case,"markers":markers,"qualification":false})); + } + let request = OutboundRequest::new(method, target)? + .proxy_marker(setting) + .max_response_bytes(0); + let enabled = client.effective_proxy_marker_enabled(&request); + let response = client.send(request).await?; + let status = response.status().as_u16(); + let markers: Vec<_> = response + .headers() + .get_all("x-edgezero-proxy") + .iter() + .map(|value| String::from_utf8_lossy(value.as_bytes()).into_owned()) + .collect(); + response.into_bytes_bounded(0).await?; + json_response( + json!({"case":case,"status":status,"markers":markers,"enabled":enabled,"qualification":false}), + ) +} + +async fn coding_probe(ctx: RequestContext, case: &str) -> Result { + let fields: Vec<_> = case.split('-').collect(); + let [ + kind @ ("coding" | "relay"), + coding @ ("gzip" | "deflate" | "br"), + mode @ ("buffered" | "streamed"), + status @ ("200" | "206"), + choice @ ("original" | "decode"), + ] = fields.as_slice() + else { + return Err(EdgeError::bad_request("invalid coding probe")); + }; + let target: Uri = ctx + .headers() + .get("x-fixture-backend") + .and_then(|v| v.to_str().ok()) + .ok_or_else(probe_failure)? + .parse() + .map_err(|_| probe_failure())?; + if !crate::is_loopback_http(&target) + || target.port_u16().is_none() + || target.query().is_some() + || target.path() != format!("/coding/{coding}/{status}") + { + return Err(EdgeError::bad_request( + "coding probe requires controlled loopback origin", + )); + } + let original = *choice == "original" || *status == "206"; + let request = OutboundRequest::new(Method::GET, target)? + .header("accept-encoding", "gzip, deflate, br")? + .cache_policy(OutboundCachePolicy::Bypass) + .response_decoding(if *choice == "original" { + ResponseDecoding::Original + } else { + ResponseDecoding::Decode + }) + .max_decoded_response_bytes(if original { 1 } else { 64 }) + .max_decoder_bytes(if original { + 0 + } else { + edgezero_core::DEFAULT_MAX_DECODER_BYTES + }) + .max_encoded_response_bytes(256) + .max_response_bytes(256) + .timeout(Duration::from_secs(2)); + let request = if *mode == "streamed" { + request.stream_response() + } else { + request + }; + let client = ctx.http_client().ok_or_else(probe_failure)?; + let client = if *kind == "relay" { + client.with_proxy_marker(false) + } else { + client + }; + let response = client.send(request).await?; + if *kind == "relay" { + return response.into_response(); + } + let status = response.status().as_u16(); + let encoding = response + .headers() + .get("content-encoding") + .and_then(|v| v.to_str().ok()) + .map(str::to_owned); + let length = response + .headers() + .get("content-length") + .and_then(|v| v.to_str().ok()) + .map(str::to_owned); + let bytes = response.into_bytes_bounded(256).await?; + json_response( + json!({"case":case,"status":status,"content_encoding":encoding,"content_length":length,"bytes":bytes.to_vec(),"qualification":false}), + ) +} + +fn json_response(value: Value) -> Result { + response_builder() + .header("content-type", "application/json") + .body(Body::from(value.to_string())) + .map_err(EdgeError::internal) +} + +fn response_control(state: Arc, case: String, token: String) -> Result { + let returned = Arc::new(AtomicBool::new(false)); + let _returned = HandlerReturned(Arc::clone(&returned)); + let first_poll = Arc::new(OnceLock::new()); + let (first_state, first_returned, first_snapshot) = ( + Arc::clone(&state), + Arc::clone(&returned), + Arc::clone(&first_poll), + ); + let prefix = futures::stream::once(async move { + let _ = first_snapshot.set(( + first_returned.load(Ordering::SeqCst), + first_state + .dispatch_returned + .as_ref() + .map(|r| r.load(Ordering::SeqCst)), + )); + Ok(Bytes::from_static(b" ")) + }); + let tail = futures::stream::once(async move { + let started = MonotonicInstant::now(); + if case == "response-owned-timer-only" { + state.runtime.pause(Duration::from_millis(100)).await; + } + let pause_ns = started.elapsed().as_nanos() as u64; + let (first_handler, first_dispatch) = + first_poll.get().copied().ok_or_else(probe_failure)?; + let report = json!({"adapter":state.runtime.adapter(),"case":case,"token":token,"pause_ns":pause_ns, + "handler_returned_at_first_poll":first_handler,"dispatch_returned_at_first_poll":first_dispatch, + "handler_returned":returned.load(Ordering::SeqCst),"dispatch_returned":state.dispatch_returned.as_ref().map(|r|r.load(Ordering::SeqCst)), + "revision":option_env!("EDGEZERO_QUALIFICATION_REVISION").unwrap_or("unrecorded"),"qualification":false}); + Ok(Bytes::from(report.to_string())) + }); + response_builder() + .header("content-type", "application/json") + .body(Body::from_stream(prefix.chain(tail))) + .map_err(EdgeError::internal) +} + +#[cfg(test)] +mod tests { + use super::*; + use edgezero_core::{ + BodyPayload, BudgetSource, HttpClient, IndependentOutboundProgress, + OutboundBatchTermination, OutboundCollectedSlot, OutboundProgressRuntime, + }; + use futures::executor::block_on; + use std::rc::Rc; + use std::sync::Mutex; + + #[derive(Clone, Default)] + struct Fake(Arc>>); + + impl ProbeRuntime for Fake { + fn adapter(&self) -> &'static str { + "test" + } + fn perform(&self, target: String) -> Job { + let events = Arc::clone(&self.0); + Box::pin(async move { + events + .lock() + .unwrap() + .push(target.rsplit('/').next().unwrap().into()); + Ok(0) + }) + } + fn start(&self, target: String) -> Result { + self.0 + .lock() + .unwrap() + .push(target.rsplit('/').next().unwrap().into()); + Ok(Box::pin(async { Ok(0) })) + } + fn pause(&self, _duration: Duration) -> LocalBoxFuture<'static, ()> { + let events = Arc::clone(&self.0); + Box::pin(async move { + events.lock().unwrap().push("wait".into()); + }) + } + } + + fn request(case: &str, target: &str) -> edgezero_core::http::Request { + edgezero_core::http::request_builder() + .uri(format!("/progress/{case}")) + .header("x-request-token", "one") + .header("x-fixture-backend", target) + .body(Body::empty()) + .unwrap() + } + + #[test] + fn lifetime_pause_reuses_only_the_configured_runtime_timer() { + let runtime = Fake::default(); + let mut ctx = + RequestContext::new(request("empty", "http://127.0.0.1:1"), Default::default()); + assert!(pause(&ctx, Duration::from_millis(100)).is_err()); + ctx.extensions_mut().insert(Arc::new(State { + runtime: Arc::new(runtime.clone()), + host_signal: Value::Null, + dispatch_returned: None, + })); + let timer = pause(&ctx, Duration::from_millis(100)).unwrap(); + assert!(runtime.0.lock().unwrap().is_empty()); + block_on(timer); + assert_eq!(*runtime.0.lock().unwrap(), ["wait"]); + } + + #[test] + fn empty_probe_observes_storage_before_transferring_initial_body() { + for (body, streamed, expected) in [ + (Body::empty(), false, 0), + (Body::from_stream(futures::stream::empty()), true, 0), + ( + Body::from_stream(futures::stream::iter([Ok(Bytes::from_static(b"data"))])), + true, + 4, + ), + ] { + let router = router(Arc::new(Fake::default()), Value::Null, None); + let mut input = request("empty", "http://127.0.0.1:1"); + *input.body_mut() = body; + let response = block_on(router.oneshot(input)).unwrap(); + let probe: Value = serde_json::from_slice( + &block_on(response.into_body().into_bytes_bounded(4096)).unwrap(), + ) + .unwrap(); + assert_eq!(probe["body_kind_before"], "Initial"); + assert_eq!(probe["body_is_stream"], streamed); + assert_eq!(probe["bytes_read"], expected); + } + } + + struct RelayClient { + time: edgezero_core::OutboundTime, + polls: Arc, + calls: Arc>>, + } + + impl edgezero_core::OutboundHttpClient for RelayClient { + fn progress(&self) -> Rc { + Rc::new(IndependentOutboundProgress) + } + + fn time(&self) -> edgezero_core::OutboundTime { + self.time.clone() + } + + fn validate_supported_options( + &self, + _request: &OutboundRequest, + _mode: edgezero_core::OutboundSendMode, + ) -> Result<(), EdgeError> { + Ok(()) + } + + fn dispatch_head( + &self, + request: edgezero_core::OutboundDispatchRequest, + ) -> edgezero_core::OutboundFuture { + let (parts, context) = request.into_parts(); + self.calls.lock().unwrap().push(json!({ + "marker":format!("{:?}",parts.proxy_marker),"mode":format!("{:?}",parts.response_mode), + "decoding":format!("{:?}",parts.response_decoding),"encoded":parts.max_encoded_response_bytes, + "decoded":parts.max_decoded_response_bytes,"decoder":parts.max_decoder_bytes, + "timeout":parts.timeout.map(|t|t.as_millis()),"cache":format!("{:?}",parts.cache_policy), + "accept_encoding":parts.headers["accept-encoding"].to_str().unwrap(),"uri":parts.uri.to_string(), + })); + let raw = parts.response_decoding == ResponseDecoding::Original + || parts.uri.path().ends_with("206"); + let bytes = if raw { + Bytes::from_static(&[255, 0, 128]) + } else { + Bytes::from_static(b"decoded") + }; + let body = if matches!(parts.response_mode, edgezero_core::ResponseMode::Streamed) { + let polls = Arc::clone(&self.polls); + futures::stream::once(async move { + polls.fetch_add(1, Ordering::SeqCst); + Ok(bytes) + }) + .boxed_local() + } else { + futures::stream::iter([Ok(bytes)]).boxed_local() + }; + let mut headers = edgezero_core::http::HeaderMap::new(); + headers.insert("content-type", "application/octet-stream".parse().unwrap()); + headers.insert("etag", "\"fixture\"".parse().unwrap()); + if raw { + headers.insert("content-encoding", "gzip".parse().unwrap()); + headers.insert("content-length", "3".parse().unwrap()); + } + let head = edgezero_core::OutboundHead::new( + if parts.uri.path().ends_with("206") { + edgezero_core::http::StatusCode::PARTIAL_CONTENT + } else { + edgezero_core::http::StatusCode::OK + }, + edgezero_core::OutboundHeaderInput::new( + "scripted", + headers, + edgezero_core::OutboundHeaderFidelity::AdapterVisibleFields, + ), + body, + context, + edgezero_core::OutboundExchangeOwner::new(|| {}), + ); + Box::pin(futures::future::ready(head)) + } + + fn begin( + &self, + request: edgezero_core::OutboundDispatchRequest, + ) -> Result { + let outcome = self.finish_ready(request); + Ok(edgezero_core::OutboundPending::new( + Box::pin(futures::future::ready(outcome)), + edgezero_core::OutboundExchangeOwner::new(|| {}), + )) + } + + fn start_batch_until( + &self, + batch: edgezero_core::OutboundPreparedBatch, + ) -> edgezero_core::OutboundBatch { + let (context, requests) = batch.into_slots(); + if requests.is_empty() + && context + .observation_cutoff() + .is_expired_at(context.clock().now()) + { + return edgezero_core::OutboundBatch::cutoff(context); + } + let events = requests + .into_iter() + .map(|(index, request)| { + let outcome = self.finish_ready(request); + edgezero_core::OutboundBatchDriverEvent::Item { + index, + completed_at: self.time.clock().now(), + outcome, + } + }) + .collect::>(); + edgezero_core::OutboundBatch::from_driver( + context, + futures::stream::iter(events), + edgezero_core::OutboundExchangeOwner::new(|| {}), + ) + } + } + + impl RelayClient { + fn finish_ready( + &self, + request: edgezero_core::OutboundDispatchRequest, + ) -> Result { + use edgezero_core::OutboundHttpClient as _; + use futures::{FutureExt as _, TryFutureExt as _}; + let mut future = Box::pin( + self.dispatch_head(request) + .and_then(edgezero_core::OutboundHead::finish), + ); + // Fixed one-chunk replies need one cooperative turn before EOF. + for _ in 0..2 { + if let Some(outcome) = future.as_mut().now_or_never() { + return outcome; + } + } + panic!("scripted response did not complete within its fixed poll bound"); + } + } + + #[test] + fn relay_batch_exact_cutoff_preserves_preflight_without_io() { + for sibling in ["cutoff", "invalid", "own-timeout"] { + let time = crate::outbound_test_support::TestTime::default(); + let own_deadline = Deadline::at_instant(time.clock().now()); + time.advance_to( + own_deadline + .instant() + .checked_add(Duration::from_millis(1)) + .expect("entry instant"), + ); + let entered_at = time.clock().now(); + let polls = Arc::new(AtomicUsize::new(0)); + let calls = Arc::new(Mutex::new(Vec::new())); + let client = HttpClient::with_client(RelayClient { + time: time.pair(), + polls: Arc::clone(&polls), + calls: Arc::clone(&calls), + }); + let first = OutboundRequest::get("http://127.0.0.1:1234/coding/gzip/200").unwrap(); + let second = OutboundRequest::get("http://127.0.0.1:1234/coding/gzip/200").unwrap(); + let second = match sibling { + "invalid" => second.stream_response(), + "own-timeout" => second.deadline(own_deadline), + _ => second, + }; + let batch = + client.start_batch_until(vec![first, second], Deadline::at_instant(entered_at)); + let results = block_on(batch.collect()).expect("exact cutoff is not driver EOF"); + let (collected, termination) = results.into_parts(); + assert_eq!(termination, OutboundBatchTermination::Cutoff); + assert_eq!(collected.len(), 2); + assert!(matches!(collected[0], OutboundCollectedSlot::Unresolved)); + let last = collected.into_iter().nth(1).expect("second slot"); + if sibling == "cutoff" { + assert!(matches!(last, OutboundCollectedSlot::Unresolved)); + } else { + let OutboundCollectedSlot::Resolved(rejected) = last else { + panic!("preflight outcome must be retained"); + }; + assert_eq!(rejected.elapsed, Duration::ZERO); + let error = rejected.outcome.expect_err("preflight failure"); + if sibling == "invalid" { + assert_eq!(error.status(), 400); + } else { + assert!(matches!( + error, + EdgeError::GatewayTimeout { + cause: BudgetSource::RequestDeadline, + .. + } + )); + } + } + assert!(calls.lock().unwrap().is_empty()); + assert_eq!(polls.load(Ordering::SeqCst), 0); + assert_eq!(time.clock().now(), entered_at); + } + } + + #[test] + fn relay_handoff_preserves_bytes_and_stream_ownership_with_matching_policy() { + for mode in ["streamed", "buffered"] { + for (choice, status) in [ + ("original", "200"), + ("original", "206"), + ("decode", "206"), + ("decode", "200"), + ] { + let polls = Arc::new(AtomicUsize::new(0)); + let calls = Arc::new(Mutex::new(Vec::new())); + let client = edgezero_core::HttpClient::with_client(RelayClient { + time: crate::outbound_test_support::TestTime::default().pair(), + polls: Arc::clone(&polls), + calls: Arc::clone(&calls), + }); + let router = router(Arc::new(Fake::default()), json!({}), None); + let target = format!("http://127.0.0.1:1234/coding/gzip/{status}"); + for prefix in ["relay", "coding"] { + let case = format!("{prefix}-gzip-{mode}-{status}-{choice}"); + let mut request = request(&case, &target); + request.extensions_mut().insert(client.clone()); + let before = polls.load(Ordering::SeqCst); + let response = block_on(router.oneshot(request)).unwrap(); + if prefix == "relay" { + assert_eq!(response.status().as_str(), status); + assert_eq!(response.headers()["etag"], "\"fixture\""); + assert_eq!(polls.load(Ordering::SeqCst), before); + assert_eq!(response.body().is_stream(), mode == "streamed"); + } + let raw = choice == "original" || status == "206"; + let expected = if raw { + Bytes::from_static(&[255, 0, 128]) + } else { + Bytes::from_static(b"decoded") + }; + let bytes = block_on(response.into_body().into_bytes_bounded(4096)).unwrap(); + if prefix == "relay" { + assert_eq!(bytes, expected); + } else { + assert_eq!( + serde_json::from_slice::(&bytes).unwrap()["bytes"], + json!(expected.as_ref()) + ); + } + } + let recorded = calls.lock().unwrap(); + assert_eq!(recorded.len(), 2); + assert_eq!(recorded[0]["marker"], "Off"); + assert_eq!(recorded[1]["marker"], "On"); + let mut relay = recorded[0].clone(); + relay["marker"] = json!("On"); + assert_eq!(relay, recorded[1]); + } + } + } + + #[test] + fn overflow_fixture_routes_exercise_the_real_inbound_reader() { + let router = router(Arc::new(Fake::default()), json!({}), None); + for len in [1023_usize, 1024, 1025] { + let request = edgezero_core::http::request_builder() + .method(Method::POST) + .uri("/progress/overflow/inbound") + .body(Body::stream(futures::stream::iter([Bytes::from(vec![ + b'x'; + len + ])]))) + .unwrap(); + let response = block_on(router.oneshot(request)).unwrap(); + assert_eq!(response.status(), if len > 1024 { 413 } else { 200 }); + let bytes = block_on(response.into_body().into_bytes_bounded(256)).unwrap(); + let value: Value = serde_json::from_slice(&bytes).unwrap(); + if len > 1024 { + assert_eq!(value["error"]["kind"], "payload_too_large"); + } else { + assert_eq!(value["received"], len); + } + } + } + + #[test] + fn overflow_fixture_rejects_uncontrolled_targets_before_client_lookup() { + let router = router(Arc::new(Fake::default()), json!({}), None); + for target in [ + "https://example.com/overflow/test/buffered-9", + "http://127.0.0.1/overflow/test/buffered-9", + "http://127.0.0.1:1234/other", + "http://127.0.0.1:1234/overflow/test/buffered-9?q=1", + ] { + let request = edgezero_core::http::request_builder() + .method(Method::POST) + .uri("/progress/overflow/buffered-9") + .header("x-request-token", "test") + .header("x-fixture-backend", target) + .body(Body::empty()) + .unwrap(); + let response = block_on(router.oneshot(request)).unwrap(); + assert_eq!(response.status(), 400); + } + } + + #[test] + fn relay_rejects_invalid_cases_and_targets_before_client_lookup() { + let router = router(Arc::new(Fake::default()), json!({}), None); + for target in [ + "https://example.com/coding/gzip/200", + "http://example.com:1234/coding/gzip/200", + "http://127.0.0.1/coding/gzip/200", + "http://127.0.0.1:1234/coding/gzip/200?q=1", + "http://127.0.0.1:1234/other", + ] { + let response = + block_on(router.oneshot(request("relay-gzip-streamed-200-original", target))) + .unwrap(); + assert_eq!(response.status(), 400); + } + for case in [ + "relay-gzip-other-200-original", + "relay-gzip-streamed-201-original", + "relay-unknown-streamed-200-original", + "relay-gzip-streamed-200-other", + ] { + let response = + block_on(router.oneshot(request(case, "http://127.0.0.1:1234/coding/gzip/200"))) + .unwrap(); + assert_eq!(response.status(), 400); + } + } + + #[test] + fn eager_probe_does_not_consume_jobs_until_after_unrelated_wait() { + let runtime = Fake::default(); + let router = router(Arc::new(runtime.clone()), json!({"source":"test"}), None); + let response = + block_on(router.oneshot(request("eager-batch", "http://127.0.0.1:1234/g3/one"))) + .unwrap(); + assert_eq!(response.status(), 200); + assert_eq!( + *runtime.0.lock().unwrap(), + ["start", "slot-0", "slot-1", "wait", "end"] + ); + } + + #[test] + fn response_probe_retains_jobs_until_stream_consumption() { + let runtime = Fake::default(); + let router = router(Arc::new(runtime.clone()), json!({}), None); + let response = + block_on(router.oneshot(request("response-owned", "http://127.0.0.1:1234/g3/one"))) + .unwrap(); + assert_eq!(*runtime.0.lock().unwrap(), ["start", "slot-0", "slot-1"]); + let bytes = block_on(response.into_body().into_bytes_bounded(4096)).unwrap(); + let report: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(report["handler_returned"], true); + assert_eq!( + *runtime.0.lock().unwrap(), + ["start", "slot-0", "slot-1", "wait", "end"] + ); + } + + #[test] + fn dropping_unconsumed_response_releases_its_unpolled_jobs_once() { + struct Count(Arc); + impl Drop for Count { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + struct Runtime(Arc); + impl ProbeRuntime for Runtime { + fn adapter(&self) -> &'static str { + "test" + } + fn perform(&self, _target: String) -> Job { + Box::pin(async { Ok(0) }) + } + fn start(&self, _target: String) -> Result { + let count = Count(Arc::clone(&self.0)); + Ok(Box::pin(async move { + let _count = count; + std::future::pending().await + })) + } + fn pause(&self, _duration: Duration) -> LocalBoxFuture<'static, ()> { + Box::pin(async {}) + } + } + let drops = Arc::new(AtomicUsize::new(0)); + let router = router(Arc::new(Runtime(Arc::clone(&drops))), json!({}), None); + let response = + block_on(router.oneshot(request("response-owned", "http://127.0.0.1:1234/g3/one"))) + .unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(drops.load(Ordering::SeqCst), 0); + drop(response); + assert_eq!(drops.load(Ordering::SeqCst), 2); + drop(router); + assert_eq!(drops.load(Ordering::SeqCst), 2); + } + + #[test] + fn response_controls_do_not_dispatch_or_poll_the_tail_inside_the_handler() { + for case in ["response-owned-ready", "response-owned-timer-only"] { + let runtime = Fake::default(); + let dispatched = Arc::new(AtomicBool::new(false)); + let router = router( + Arc::new(runtime.clone()), + json!({}), + Some(Arc::clone(&dispatched)), + ); + let response = + block_on(router.oneshot(request(case, "https://example.com/unused"))).unwrap(); + assert_eq!(response.status(), 200); + assert!(runtime.0.lock().unwrap().is_empty()); + dispatched.store(true, Ordering::SeqCst); + let (payload, body_owner) = response.into_body().into_parts(); + let BodyPayload::Stream(mut stream) = payload else { + panic!("control must stream") + }; + let mut bytes = block_on(stream.next()).unwrap().unwrap().to_vec(); + assert_eq!(bytes, b" "); + assert!(runtime.0.lock().unwrap().is_empty()); + bytes.extend(block_on(Body::from_stream(stream).into_bytes_bounded(4096)).unwrap()); + let report: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(report["case"], case); + assert_eq!(report["handler_returned_at_first_poll"], true); + assert_eq!(report["dispatch_returned_at_first_poll"], true); + assert_eq!(report["handler_returned"], true); + assert_eq!(report["dispatch_returned"], true); + let expected: Vec<&str> = if case.ends_with("timer-only") { + vec!["wait"] + } else { + vec![] + }; + assert_eq!(*runtime.0.lock().unwrap(), expected); + drop(body_owner); + } + } + + #[test] + fn probe_rejects_remote_target_before_any_sdk_work() { + let runtime = Fake::default(); + let router = router(Arc::new(runtime.clone()), json!({}), None); + let response = + block_on(router.oneshot(request("eager-single", "https://example.com/g3/one"))) + .unwrap(); + assert_eq!(response.status(), 400); + assert!(runtime.0.lock().unwrap().is_empty()); + } +} diff --git a/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/redirect.rs b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/redirect.rs new file mode 100644 index 000000000..1a4560240 --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/redirect.rs @@ -0,0 +1,346 @@ +use edgezero_core::context::RequestContext; +use edgezero_core::http::{HeaderMap, Method, Response, Uri, response_builder}; +use edgezero_core::{ + Body, EdgeError, OutboundFollowFailure, OutboundRedirectAction, OutboundRedirectLimit, + OutboundRedirectStopReason, OutboundRequest, OutboundResponse, action, +}; +use serde_json::{Value, json}; +use std::{cell::Cell, convert::Infallible, rc::Rc, time::Duration}; + +struct Probe { + uri: Uri, + mode: String, + streamed: bool, + encoded: bool, +} + +fn invalid() -> EdgeError { + EdgeError::bad_request("invalid redirect probe") +} + +fn field<'a>(headers: &'a HeaderMap, name: &str) -> Result, EdgeError> { + let mut values = headers.get_all(name).iter(); + let value = values.next(); + if values.next().is_some() { + return Err(invalid()); + } + value + .map(|value| value.to_str().map_err(|_| invalid())) + .transpose() +} + +fn flag(headers: &HeaderMap, name: &str) -> Result { + match field(headers, name)? { + None | Some("false") => Ok(false), + Some("true") => Ok(true), + _ => Err(invalid()), + } +} + +impl Probe { + fn parse(headers: &HeaderMap) -> Result { + let target = field(headers, "x-origin")?.ok_or_else(invalid)?; + if target.len() > 2048 || target.contains('#') { + return Err(invalid()); + } + let uri: Uri = target.parse().map_err(|_| invalid())?; + if !crate::is_loopback_http(&uri) + || !uri.port_u16().is_some_and(|port| port != 0) + || uri + .authority() + .is_some_and(|authority| authority.as_str().contains('@')) + || !uri.path().starts_with("/p5/") + || uri.query().is_some() + { + return Err(invalid()); + } + let mode = field(headers, "x-p5-mode")?.ok_or_else(invalid)?; + if !matches!(mode, "send" | "follow" | "stop" | "limit") { + return Err(invalid()); + } + Ok(Self { + uri, + mode: mode.to_owned(), + streamed: flag(headers, "x-p5-stream")?, + encoded: flag(headers, "x-p5-encoded-cap")?, + }) + } + + fn request(&self) -> Result { + let mut request = OutboundRequest::new(Method::GET, self.uri.clone())? + .timeout(Duration::from_secs(3)) + .max_response_bytes(10); + if self.streamed { + request = request.stream_response().max_decoded_response_bytes(10); + } + if self.encoded { + request = request.max_encoded_response_bytes(10); + } + Ok(request) + } +} + +fn error_report(error: &EdgeError) -> Value { + let reason = match error { + EdgeError::BadGateway { reason, .. } => format!("{reason:?}"), + EdgeError::ResponseTooLarge { reason, .. } => format!("{reason:?}"), + EdgeError::GatewayTimeout { cause, .. } => format!("{cause:?}"), + _ => String::new(), + }; + json!({"error":{ + "kind":error.kind(),"status":error.status().as_u16(),"reason":reason, + "provider":error.provider_failure_detail().map(|detail|format!("{detail:?}")), + }}) +} + +async fn response_report( + response: OutboundResponse, + final_uri: Uri, + redirects: usize, + stop: Option, + policies: usize, +) -> Value { + let status = response.status().as_u16(); + let streamed = response.body().is_stream(); + let mut headers = response + .headers() + .iter() + .map(|(name, value)| (name.as_str().to_owned(), value.as_bytes().to_vec())) + .collect::>(); + headers.sort_unstable(); + // This report bound exceeds the request's tested buffered and streamed limits. + let outcome = match response.into_bytes_bounded(1024).await { + Ok(bytes) => json!({"status":status,"headers":headers,"bytes":bytes.to_vec()}), + Err(error) => error_report(&error), + }; + json!({"outcome":outcome,"hop":redirects,"final_uri":final_uri.to_string(), + "redirects":redirects,"stop":stop.map(|reason|format!("{reason:?}")), + "streamed":streamed,"policies":policies}) +} + +#[action] +pub(super) async fn probe(ctx: RequestContext) -> Result { + let probe = Probe::parse(ctx.headers())?; + let client = ctx.http_client().ok_or_else(|| { + EdgeError::internal(std::io::Error::other( + "redirect probe has no injected client", + )) + })?; + let request = probe.request()?; + let policies = Rc::new(Cell::new(0_usize)); + let exchange = if probe.mode == "send" { + client + .send(request) + .await + .map(|response| (response, probe.uri, 0, None)) + .map_err(|error| (0, error)) + } else { + let observed = Rc::clone(&policies); + let stop = probe.mode == "stop"; + let limit = OutboundRedirectLimit::new(if probe.mode == "limit" { 1 } else { 4 })?; + client + .follow(request, limit, move |_| { + observed.set(observed.get().saturating_add(1)); + Ok::<_, Infallible>(if stop { + OutboundRedirectAction::Stop + } else { + OutboundRedirectAction::FollowPrepared + }) + }) + .await + .map(edgezero_core::OutboundFollowResult::into_parts) + .map_err(|failure| match failure { + OutboundFollowFailure::Hop { index, error } => (index, error), + OutboundFollowFailure::Policy { error, .. } => match error {}, + }) + }; + let report = match exchange { + Ok((response, uri, redirects, stop)) => { + response_report(response, uri, redirects, stop, policies.get()).await + } + Err((hop, error)) => { + json!({"outcome":error_report(&error),"hop":hop,"policies":policies.get()}) + } + }; + response_builder() + .header("content-type", "application/json") + .body(Body::json(&report).map_err(EdgeError::internal)?) + .map_err(EdgeError::internal) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::outbound_test_support::TestTime; + use bytes::Bytes; + use edgezero_core::http::{HeaderValue, StatusCode, request_builder}; + use edgezero_core::{ContentTypeProvenance, ResponseMode}; + use futures::{StreamExt, executor::block_on}; + use std::{cell::Cell, rc::Rc, time::Duration}; + + fn headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + for (name, value) in [ + ("x-origin", "http://127.0.0.1:1234/p5/final"), + ("x-p5-mode", "follow"), + ("x-p5-stream", "false"), + ("x-p5-encoded-cap", "false"), + ] { + headers.insert(name, value.parse().unwrap()); + } + headers + } + + fn response(headers: HeaderMap, body: Body) -> OutboundResponse { + OutboundResponse::new( + Method::GET, + StatusCode::OK, + headers, + body, + TestTime::default().clock(), + ContentTypeProvenance::Unknown, + ) + } + + #[test] + fn redirect_probe_settings_keep_three_second_and_ten_byte_limits() { + for mode in ["send", "follow", "stop", "limit"] { + for streamed in [false, true] { + for encoded in [false, true] { + let mut headers = headers(); + headers.insert("x-p5-mode", mode.parse().unwrap()); + headers.insert("x-p5-stream", streamed.to_string().parse().unwrap()); + headers.insert("x-p5-encoded-cap", encoded.to_string().parse().unwrap()); + let probe = Probe::parse(&headers).unwrap(); + assert_eq!(probe.mode, mode); + assert_eq!(probe.streamed, streamed); + assert_eq!(probe.encoded, encoded); + let parts = probe.request().unwrap().into_parts(); + assert_eq!(parts.method, Method::GET); + assert_eq!(parts.timeout, Some(Duration::from_secs(3))); + assert_eq!(parts.max_encoded_response_bytes, encoded.then_some(10)); + if streamed { + assert!(matches!(parts.response_mode, ResponseMode::Streamed)); + assert_eq!(parts.max_decoded_response_bytes, Some(10)); + } else { + assert!(matches!( + parts.response_mode, + ResponseMode::Buffered { max_bytes: 10 } + )); + } + } + } + } + } + + #[test] + fn redirect_probe_rejects_uncontrolled_or_ambiguous_input() { + for target in [ + "https://127.0.0.1:1234/p5/final", + "http://example.com:1234/p5/final", + "http://user@127.0.0.1:1234/p5/final", + "http://127.0.0.1/p5/final", + "http://127.0.0.1:0/p5/final", + "http://127.0.0.1:1234/other", + "http://127.0.0.1:1234/p5/final?extra=true", + "http://127.0.0.1:1234/p5/final#fragment", + ] { + let mut headers = headers(); + headers.insert("x-origin", target.parse().unwrap()); + assert!(Probe::parse(&headers).is_err(), "{target}"); + } + for name in ["x-origin", "x-p5-mode", "x-p5-stream", "x-p5-encoded-cap"] { + let mut headers = headers(); + headers.append(name, headers[name].clone()); + assert!(Probe::parse(&headers).is_err(), "{name}"); + } + for (name, value) in [ + ("x-p5-mode", "other"), + ("x-p5-stream", "TRUE"), + ("x-p5-encoded-cap", "1"), + ] { + let mut headers = headers(); + headers.insert(name, value.parse().unwrap()); + assert!(Probe::parse(&headers).is_err()); + } + let mut oversized = headers(); + oversized.insert( + "x-origin", + format!("http://127.0.0.1:1/p5/{}", "a".repeat(2048)) + .parse() + .unwrap(), + ); + assert!(Probe::parse(&oversized).is_err()); + } + + #[test] + fn redirect_report_preserves_raw_bytes_metadata_and_explicit_null_stop() { + let mut headers = HeaderMap::new(); + headers.append("x-value", HeaderValue::from_bytes(&[255, 128]).unwrap()); + headers.append("x-value", HeaderValue::from_static("second")); + let report = block_on(response_report( + response(headers, Body::from(vec![0, 255, 128])), + Uri::from_static("http://127.0.0.1:1234/p5/final"), + 2, + None, + 2, + )); + assert_eq!(report["outcome"]["bytes"], json!([0, 255, 128])); + assert_eq!( + report["outcome"]["headers"], + json!([["x-value", b"second"], ["x-value", [255, 128]]]) + ); + assert_eq!(report["hop"], 2); + assert_eq!(report["policies"], 2); + assert_eq!(report["redirects"], 2); + assert_eq!(report["final_uri"], "http://127.0.0.1:1234/p5/final"); + assert_eq!(report.get("stop"), Some(&Value::Null)); + assert_eq!(report["streamed"], false); + } + + #[test] + fn redirect_report_bounds_stream_consumption_and_keeps_final_metadata() { + let polls = Rc::new(Cell::new(0)); + let counted = Rc::clone(&polls); + let source = futures::stream::repeat(Bytes::from_static(&[b'x'; 512])).map(move |chunk| { + counted.set(counted.get() + 1); + Ok(chunk) + }); + let report = block_on(response_report( + response(HeaderMap::new(), Body::from_stream(source)), + Uri::from_static("http://127.0.0.1:1234/p5/final"), + 1, + Some(OutboundRedirectStopReason::LimitReached), + 1, + )); + assert_eq!(report["outcome"]["error"]["kind"], "response_too_large"); + assert!(report["outcome"].get("bytes").is_none()); + assert_eq!(report["stop"], "LimitReached"); + assert_eq!(report["hop"], 1); + assert_eq!(report["streamed"], true); + assert!(polls.get() <= 3, "unbounded report consumption"); + } + + #[test] + fn redirect_report_does_not_echo_error_messages() { + let report = error_report(&EdgeError::bad_request("private input")); + assert_eq!( + report, + json!({"error":{"kind":"bad_request","status":400,"reason":"","provider":null}}) + ); + } + + #[test] + fn redirect_probe_requires_the_context_client() { + let router = edgezero_core::router::RouterService::builder() + .get("/progress/redirect", probe) + .build(); + let mut request = request_builder() + .uri("/progress/redirect") + .body(Body::empty()) + .unwrap(); + *request.headers_mut() = headers(); + let response = block_on(router.oneshot(request)).unwrap(); + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + } +} diff --git a/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/response_headers.rs b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/response_headers.rs new file mode 100644 index 000000000..88db821eb --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-core/src/qualification/response_headers.rs @@ -0,0 +1,140 @@ +//! Suite-only inputs for the canonical synthesized-header delivery paths. + +use edgezero_core::app::App; +use edgezero_core::context::RequestContext; +use edgezero_core::http::{HeaderMap, HeaderValue, Response}; +use edgezero_core::ingress::{AdmissionDecision, IngressGrant}; +use edgezero_core::response_egress::{DetachedResponseEgressDecision, ResponseEgressCompletion}; +use edgezero_core::{Body, EdgeError, action}; +use serde_json::json; +use std::time::Duration; + +pub fn enabled() -> bool { + option_env!("EDGEZERO_QUALIFICATION_SUITE") == Some("synthesized-headers") +} + +pub fn fields(source: &'static str) -> HeaderMap { + let mut fields = HeaderMap::new(); + fields.insert("x-synthesized-fixture", HeaderValue::from_static(source)); + for index in 0..30 { + fields.append( + "set-cookie", + format!("slot-{index:02}=value; Path=/").parse().unwrap(), + ); + } + let used = 2 + + fields + .iter() + .map(|(name, value)| name.as_str().len() + value.len() + 4) + .sum::() + + "set-cookie".len() + + 4; + let padding = 4096_usize + .checked_sub(used + "padding=".len()) + .expect("fixture header budget"); + fields.append( + "set-cookie", + format!("padding={}", "a".repeat(padding)).parse().unwrap(), + ); + fields +} + +pub fn token(headers: &HeaderMap) -> Option { + headers + .get("x-request-token") + .and_then(|value| value.to_str().ok()) + .filter(|value| { + !value.is_empty() + && value.len() <= 64 + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + }) + .map(str::to_owned) +} + +fn completion(token: Option) -> ResponseEgressCompletion { + ResponseEgressCompletion::new(move |report| { + eprintln!( + "{}", + json!({"event":"synthesized-terminal","token":token, + "body_kind":format!("{:?}", report.body_kind),"outcome":format!("{:?}",report.outcome),"bytes_written":report.bytes_written, + "fallback":report.fallback_disposition.map(|value|format!("{value:?}")),"qualification":false}) + ); + }) +} + +pub fn detached_witness(app: &mut App, token: Option) { + app.set_detached_response_egress_decision_factory(move |head| { + eprintln!("{}", json!({"event":"synthesized-detached","token":token,"status":head.status().as_u16(),"qualification":false})); + DetachedResponseEgressDecision::Send { completion: completion(token.clone()), deadline: head.write_deadline_after(Duration::from_secs(2)) } + }); +} + +pub fn configure(app: &mut App) -> Result<(), EdgeError> { + if !enabled() { + return Ok(()); + } + app.set_synthesized_response_headers(fields("app")) + .map_err(EdgeError::internal)?; + app.set_ingress_admission_policy(|head| { + let token = token(head.headers()); + eprintln!( + "{}", + json!({"event":"synthesized-admission","token":token,"qualification":false}) + ); + let completion = completion(token); + if head.target().path() == "/progress/synthesized/refused" { + let mut response = Response::new(Body::from("refused by probe")); + *response.status_mut() = edgezero_core::http::StatusCode::SERVICE_UNAVAILABLE; + return AdmissionDecision::Refuse { + completion, + response, + }; + } + AdmissionDecision::Admit { + completion, + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(Duration::from_secs(2)), + } + }); + Ok(()) +} + +#[action] +pub(super) async fn probe(ctx: RequestContext) -> Result { + eprintln!( + "{}", + json!({"event":"synthesized-handler","token":token(ctx.headers()),"qualification":false}) + ); + match ctx.path_params().get("case") { + Some("success") => Ok(Response::new(Body::from("routed success"))), + Some("handler-error") => Err(EdgeError::bad_request("controlled handler error")), + Some("fallback") => super::framing::source("trailers"), + _ => Err(EdgeError::bad_request("unknown synthesized header case")), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use edgezero_core::SynthesizedResponseHeaders; + + #[test] + fn synthesized_fixture_fills_the_inclusive_policy_bounds() { + for source in ["app", "request"] { + let fields = fields(source); + assert_eq!(fields.len(), 32); + assert_eq!( + 2 + fields + .iter() + .map(|(name, value)| name.as_str().len() + value.len() + 4) + .sum::(), + 4096 + ); + assert_eq!(fields["x-synthesized-fixture"], source); + assert_eq!(fields.get_all("set-cookie").iter().count(), 31); + SynthesizedResponseHeaders::new(fields).unwrap(); + } + } +} diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/Cargo.toml b/tests/fixtures/reusable-app/crates/fixture-fastly/Cargo.toml index db5870bf7..7be6c72aa 100644 --- a/tests/fixtures/reusable-app/crates/fixture-fastly/Cargo.toml +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/Cargo.toml @@ -4,11 +4,17 @@ version.workspace = true edition.workspace = true publish.workspace = true +[features] +qualification = ["fixture-core/qualification"] +progress = ["fixture-core/qualification"] +readiness = ["dep:fastly-sys"] + [dependencies] edgezero-core.workspace = true edgezero-adapter-fastly.workspace = true fixture-core.workspace = true fastly.workspace = true +fastly-sys = { version = "=0.13.1", optional = true } futures.workspace = true serde_json.workspace = true log.workspace = true @@ -17,6 +23,25 @@ log.workspace = true name = "fixture-fastly-single-request" path = "src/bin/single_request.rs" +[[bin]] +name = "fixture-fastly-assembly" +path = "src/bin/assembly.rs" + +[[bin]] +name = "fixture-fastly-outbound-progress" +path = "src/bin/outbound_progress.rs" +required-features = ["progress"] + +[[bin]] +name = "fixture-fastly-outbound-readiness" +path = "src/bin/outbound_readiness.rs" +required-features = ["readiness"] + +[[bin]] +name = "fixture-fastly-qualification" +path = "src/bin/qualification.rs" +required-features = ["qualification"] + [[bin]] name = "fixture-fastly-rebuild-per-request" path = "src/bin/rebuild_per_request.rs" diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/assembly.rs b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/assembly.rs new file mode 100644 index 000000000..bde07c2db --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/assembly.rs @@ -0,0 +1,86 @@ +use std::time::Duration; + +use edgezero_adapter_fastly::lifecycle::{Sandbox, serve_custom}; +use edgezero_adapter_fastly::request::{ + send_assembly_error, send_request_with_registries_and_hooks, +}; +use edgezero_adapter_fastly::{FASTLY_PLATFORM, FastlyLogging, Serve}; +use edgezero_core::MonotonicClock; +use edgezero_core::app::{App, Hooks}; +use fixture_core::assembly::{ConfigurationFailureApp, InternalFailureApp, RetryApp}; + +fn main() -> Result<(), fastly::Error> { + fastly::init(); + fixture_core::init_fixture_logging(|line| println!("{line}"))?; + let case = fastly::ConfigStore::open("assembly") + .get("case") + .ok_or_else(|| fastly::Error::msg("missing assembly case"))?; + let serve = Serve::new() + .with_max_requests(4) + .with_timeout(Duration::from_secs(5)); + match case.as_str() { + "standard-retained" => { + fixture_fastly::finish(edgezero_adapter_fastly::serve_app::>(serve)) + } + "custom-retained" => { + fixture_fastly::finish(serve_custom(serve, |req, retained: &mut Sandbox| { + let token = req + .get_header_str("x-request-token") + .filter(|value| { + !value.is_empty() + && value.len() <= 64 + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + }) + .map(str::to_owned); + let clock = MonotonicClock::default(); + let start = clock.now(); + let delivered = + match retained.initialize(|| App::build::>(FASTLY_PLATFORM)) { + Ok(()) => send_request_with_registries_and_hooks( + retained + .state() + .ok_or_else(|| fastly::Error::msg("app was not retained"))?, + RetryApp::::stores(), + req, + |_, _| {}, + |_| (), + ), + Err(error) => send_assembly_error(req, error, start, clock), + }; + if let Some(delivery) = delivered.delivery() { + println!( + "{}", + serde_json::json!({"event":"assembly-delivery","token":token, + "attempts":retained.initialization_attempts(), + "retained":retained.state().is_some(), + "disposition":if matches!(delivered, edgezero_adapter_fastly::delivery::FastlyRequestDelivery::Detached(_)) { "detached" } else { "routed" }, + "commitment":format!("{:?}",delivery.commitment()), + "outcome":format!("{:?}",delivery.report().outcome), + "fallback":delivery.report().fallback_disposition.is_some(), + "may_send_native":delivered.may_send_native_response()}) + ); + } + fixture_fastly::strict_delivery(delivered) + })) + } + "single-routes" => edgezero_adapter_fastly::run_app::>(), + "single-config" => edgezero_adapter_fastly::run_app::(), + "single-internal" => edgezero_adapter_fastly::run_app::(), + "single-hooks" => { + edgezero_adapter_fastly::run_app_with_hooks::( + |_, _| panic!("prepare must not run after failed assembly"), + |_| panic!("finalize must not run after failed assembly"), + ) + .map(|_| ()) + } + "single-config-explicit" => { + edgezero_adapter_fastly::run_app_with_config::( + &FastlyLogging::from(ConfigurationFailureApp::logging_for("fastly")), + None, + ) + } + _ => Err(fastly::Error::msg("unknown assembly case")), + } +} diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_lifetime.rs b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_lifetime.rs index a4fbdbaa2..44868c6a0 100644 --- a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_lifetime.rs +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_lifetime.rs @@ -1,25 +1,27 @@ use edgezero_core::app::{App, Hooks, StoresMetadata}; +use edgezero_core::error::EdgeError; use edgezero_core::router::RouterService; use std::time::Duration; struct SlowInitialization; impl Hooks for SlowInitialization { - fn routes() -> RouterService { + fn routes() -> Result { fixture_fastly::MeasuredApp::routes() } fn stores() -> StoresMetadata { fixture_fastly::MeasuredApp::stores() } - fn build_app() -> App { + fn configure(app: &mut App) -> Result<(), EdgeError> { // A real elapsed deadline, not only the SDK's zero-limit boundary. std::thread::sleep(Duration::from_millis(100)); - fixture_fastly::MeasuredApp::build_app() + fixture_fastly::MeasuredApp::configure(app) } } fn main() -> Result<(), fastly::Error> { - let summary = edgezero_adapter_fastly::serve_app_with_request_extensions::( + let summary = edgezero_adapter_fastly::serve_app_with_hooks::( fixture_fastly::serving().with_max_lifetime(Duration::from_millis(50)), fixture_fastly::extend, + |_response| (), ); let elapsed = summary.time_handler() >= Duration::from_millis(50); fixture_fastly::finish(summary)?; diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_memory.rs b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_memory.rs index 612ffa6bf..4eaedba68 100644 --- a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_memory.rs +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_memory.rs @@ -1,14 +1,19 @@ fn main() -> Result<(), fastly::Error> { + fastly::init(); println!( "{}", serde_json::json!({"event":"heap_preflight", "heap_mib":fastly::compute_runtime::heap_memory_snapshot_mib().ok()}) ); // A one-MiB threshold is below this fixture's observed baseline. If the heap // snapshot fails, the SDK treats usage as u32::MAX and also stops after one request. - fixture_fastly::finish( - edgezero_adapter_fastly::serve_app_with_request_extensions::( - fixture_fastly::serving().with_max_memory(1), - fixture_fastly::extend, - ), - ) + fixture_fastly::finish(edgezero_adapter_fastly::serve_app_with_hooks::< + fixture_fastly::MeasuredApp, + _, + _, + _, + >( + fixture_fastly::serving().with_max_memory(1), + fixture_fastly::extend, + |_response| (), + )) } diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_requests.rs b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_requests.rs index 8ce7b5b8d..c15c294f2 100644 --- a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_requests.rs +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/limit_requests.rs @@ -1,9 +1,9 @@ fn main() -> Result<(), fastly::Error> { let serve = fixture_fastly::serving().with_max_requests(1); - fixture_fastly::finish( - edgezero_adapter_fastly::serve_app_with_request_extensions::( - serve, - fixture_fastly::extend, - ), - ) + fixture_fastly::finish(edgezero_adapter_fastly::serve_app_with_hooks::< + fixture_fastly::MeasuredApp, + _, + _, + _, + >(serve, fixture_fastly::extend, |_response| ())) } diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/logger_negative.rs b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/logger_negative.rs index a0cee0de3..a648d299f 100644 --- a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/logger_negative.rs +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/logger_negative.rs @@ -1,11 +1,3 @@ fn main() -> Result<(), fastly::Error> { - fixture_fastly::finish(fixture_fastly::serving().run(|req| { - let observation = fixture_fastly::observation(&req); - edgezero_adapter_fastly::run_app_with_request_extensions::( - req, - |_, extensions| { - extensions.insert(observation); - }, - ) - })) + fixture_fastly::logger_negative() } diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs new file mode 100644 index 000000000..1e25e2faf --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_progress.rs @@ -0,0 +1,154 @@ +use edgezero_core::EdgeError; +use fastly::http::request::{PendingRequest, PollResult}; +use fixture_core::qualification::progress::{self, Job, ProbeRuntime}; +use futures::future::LocalBoxFuture; +use serde_json::json; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; + +struct Runtime; + +impl ProbeRuntime for Runtime { + fn adapter(&self) -> &'static str { + "fastly" + } + fn perform(&self, target: String) -> Job { + Box::pin(async move { Runtime.start(target)?.await }) + } + fn start(&self, target: String) -> Result { + let pending = fastly::Request::get(target) + .with_pass(true) + .send_async("progress-origin") + .map_err(|_| progress::probe_failure())?; + Ok(complete(pending)) + } + fn pause(&self, duration: Duration) -> LocalBoxFuture<'static, ()> { + // This is a synchronous guest suspension, not a background guest executor. + Box::pin(async move { + std::thread::sleep(duration); + }) + } +} + +fn complete(mut pending: PendingRequest) -> Job { + Box::pin(async move { + let started = Instant::now(); + loop { + match pending.poll() { + PollResult::Done(result) => { + let response = result.map_err(|_| progress::probe_failure())?; + if !response.get_status().is_success() { + return Err(progress::probe_failure()); + } + // The controlled origin sends CL:0. Do not enter the failed G2 body-read path. + return Ok(0); + } + PollResult::Pending(next) => pending = next, + } + if started.elapsed() >= Duration::from_secs(2) { + return Err(progress::probe_failure()); + } + std::thread::sleep(Duration::from_millis(1)); + } + }) +} + +fn telemetry(line: &str) { + eprintln!("{line}"); +} + +fn main() -> Result<(), fastly::Error> { + fastly::init(); + fixture_core::init_fixture_logging(telemetry)?; + let mut request = fastly::Request::from_client(); + if let Some(case) = request.get_path().strip_prefix("/progress/reference/") { + let source = fixture_core::qualification::framing::source(case)?; + let (parts, body) = source.into_parts(); + let (payload, body_owner) = body.into_parts(); + let edgezero_core::BodyPayload::Once(bytes) = payload else { + return Err(progress::probe_failure().into()); + }; + let mut response = fastly::Response::from_status(parts.status.as_u16()) + .with_framing_headers_mode(fastly::http::FramingHeadersMode::Automatic); + for (name, value) in &parts.headers { + response.append_header(name.as_str(), value.as_bytes()); + } + response.set_body(bytes.as_ref()); + response.send_to_client(); + drop(body_owner); + return Ok(()); + } + let signal = json!({"source":"fastly-sdk-before-conversion", "has_body":request.has_body(), + "known_length":request.try_get_body_mut().and_then(fastly::Body::known_length)}); + if let Some(target) = request.get_header_str("x-fixture-backend") { + let uri: edgezero_core::http::Uri = target.parse()?; + if !fixture_core::is_loopback_http(&uri) || uri.port_u16().is_none() { + return Err(progress::probe_failure().into()); + } + fastly::Backend::builder( + "progress-origin", + uri.authority() + .ok_or_else(progress::probe_failure)? + .as_str(), + ) + .connect_timeout(Duration::from_secs(2)) + .first_byte_timeout(Duration::from_secs(2)) + .between_bytes_timeout(Duration::from_secs(2)) + .finish()?; + } + let mut app = progress::application( + Arc::new(Runtime), + signal, + None, + edgezero_adapter_fastly::FASTLY_PLATFORM, + )?; + let headers_probe = fixture_core::qualification::response_headers::enabled(); + let probe_token = request + .get_header_str("x-request-token") + .filter(|value| { + !value.is_empty() + && value.len() <= 64 + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + }) + .map(str::to_owned); + if headers_probe { + fixture_core::qualification::response_headers::detached_witness( + &mut app, + probe_token.clone(), + ); + } + fixture_fastly::strict_delivery( + edgezero_adapter_fastly::request::FastlyService::new(&app).send_request_with_hooks( + request, + |request, scratch| { + if !headers_probe { return; } + use edgezero_core::{SynthesizedResponseHeaders, SynthesizedResponseHeadersError}; + let mode = request.get_header_str("x-fixture-synthesized-mode").unwrap_or("app"); + let result = match mode { + "replace" => SynthesizedResponseHeaders::new(fixture_core::qualification::response_headers::fields("request")), + "empty" => SynthesizedResponseHeaders::new(edgezero_core::http::HeaderMap::new()), + "invalid" => { + let mut fields = fixture_core::qualification::response_headers::fields("request"); + fields.insert("x-synthesized-fixture", "request!".parse().unwrap()); + SynthesizedResponseHeaders::new(fields) + } + _ => { return; } + }; + let outcome = match result { + Ok(headers) => { scratch.insert(headers); "installed" }, + Err(SynthesizedResponseHeadersError::TooManyBytes) => "TooManyBytes", + Err(SynthesizedResponseHeadersError::TooManyFields) => "TooManyFields", + Err(_) => "unexpected-error", + }; + eprintln!("{}",json!({"event":"synthesized-prepare","token":probe_token,"mode":mode,"outcome":outcome,"qualification":false})); + }, + |_| { + if headers_probe { eprintln!("{}",json!({"event":"synthesized-finalize","token":probe_token,"qualification":false})); } + }, + ), + ) +} diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs new file mode 100644 index 000000000..324561d37 --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/outbound_readiness.rs @@ -0,0 +1,849 @@ +use fastly::handle::{BodyHandle, PendingRequestHandle, PollHandleResult, ResponseHandle}; +use fastly::{Backend, Request, Response}; +use serde_json::{Value, json}; +use std::io::Read; +use std::time::{Duration, Instant}; + +const DEADLINE_US: u64 = 500_000; + +fn p5_failure(error: &edgezero_core::EdgeError) -> Value { + use edgezero_core::{EdgeError, FastlyFailureCause, ProviderFailureDetail}; + let reason = match error { + EdgeError::BadGateway { reason, .. } => format!("{reason:?}"), + EdgeError::ResponseTooLarge { reason, .. } => format!("{reason:?}"), + _ => String::new(), + }; + json!({"kind":"error","category":error.kind(),"status":error.status().as_u16(), + "reason":reason,"hop":null,"provider_optimizer_unsupported":matches!(error.provider_failure_detail(), + Some(ProviderFailureDetail::Fastly { cause:FastlyFailureCause::ImageOptimizerUnsupported,.. }))}) +} + +fn p5_options() -> Result { + use edgezero_core::{ + OutboundImageFormat, OutboundImageOptimizerOptions, OutboundImageOptimizerRegion, + }; + OutboundImageOptimizerOptions::new() + .region(OutboundImageOptimizerRegion::UsEast) + .format(OutboundImageFormat::WebP) + .preserve_origin_query(true) + .width(320)? + .quality(80) +} + +async fn p5_response( + result: Result, +) -> Value { + let response = match result { + Ok(response) => response, + Err(error) => return p5_failure(&error), + }; + let status = response.status().as_u16(); + let streamed = response.body().is_stream(); + let mut headers: Vec<_> = response + .headers() + .iter() + .map(|(name, value)| { + ( + name.as_str().to_owned(), + value.to_str().unwrap_or("").to_owned(), + ) + }) + .collect(); + headers.sort(); + match response.into_bytes_bounded(10).await { + Ok(bytes) => { + json!({"kind":"ok","status":status,"headers":headers,"bytes":bytes.to_vec(),"streamed":streamed}) + } + Err(error) => p5_failure(&error), + } +} + +async fn p5_follow( + client: &edgezero_core::HttpClient, + request: edgezero_core::OutboundRequest, + final_uri: &str, + stop: bool, + attach_options: bool, +) -> Result { + use edgezero_core::{ + EdgeError, OutboundFollowFailure, OutboundRedirectAction, OutboundRedirectLimit, + }; + use std::{cell::Cell, rc::Rc}; + let calls = Rc::new(Cell::new(0usize)); + let observed_calls = Rc::clone(&calls); + let checked = Rc::new(Cell::new(false)); + let observed_options = Rc::clone(&checked); + let initial_uri = request.uri().to_string(); + let target = final_uri.to_owned(); + let outcome = client + .follow(request, OutboundRedirectLimit::new(1)?, move |context| { + observed_calls.set(observed_calls.get() + 1); + if context.redirect_number() != 1 + || context.status().as_u16() != 302 + || context.request_uri().to_string() != initial_uri + || context.resolved_target() != target + || !context.is_cross_origin() + { + return Err(EdgeError::bad_request("unexpected P5 policy target")); + } + if stop { + return Ok(OutboundRedirectAction::Stop); + } + if attach_options { + let options = p5_options()?; + let next = context + .take_prepared_request() + .ok_or_else(|| EdgeError::bad_request("missing P5 prepared request"))? + .image_optimizer(options.clone()); + observed_options.set(next.image_optimizer_options() == Some(&options)); + return Ok(OutboundRedirectAction::Follow(next)); + } + Ok(OutboundRedirectAction::FollowPrepared) + }) + .await; + let mut result = match outcome { + Ok(followed) => { + let (response, uri, redirects, reason) = followed.into_parts(); + json!({"response":p5_response(Ok(response)).await,"final_uri":uri.to_string(), + "redirects":redirects,"stop_reason":reason.map(|reason|format!("{reason:?}"))}) + } + Err(OutboundFollowFailure::Hop { index, error }) => { + json!({"response":p5_failure(&error),"hop":index}) + } + Err(OutboundFollowFailure::Policy { error, .. }) => return Err(error), + }; + result["policy_calls"] = json!(calls.get()); + result["options_checked"] = json!(checked.get()); + Ok(result) +} + +fn p5_probe(request: &Request, case: &str) -> Result<(), fastly::Error> { + use edgezero_adapter_fastly::outbound::FastlyOutboundClient; + use edgezero_core::http::Uri; + use edgezero_core::{Deadline, EdgeError, HttpClient, OutboundRequest, ResponseDecoding}; + let token = request_token(request)?; + let target: Uri = request + .get_header_str("x-fixture-backend") + .ok_or_else(|| std::io::Error::other("missing P5 origin"))? + .parse()?; + let suffix = target + .path() + .strip_prefix(&format!("/p5/{token}/")) + .ok_or_else(|| std::io::Error::other("foreign P5 path"))?; + let (port, shape) = suffix + .split_once('/') + .ok_or_else(|| std::io::Error::other("invalid P5 path"))?; + let port: u16 = port.parse()?; + let expected = match case { + "two-origin" | "optimizer-next" => "redirect", + "pending-head" => "pending", + "optimizer-send" | "optimizer-initial" => "image", + _ => { + let fields: Vec<_> = case.split('/').collect(); + if !matches!( + fields.as_slice(), + [ + "parity", + "identity" | "gzip" | "passthrough", + "buffered" | "streamed", + "ten" | "eleven" | "declared" + ] + ) { + return Err(std::io::Error::other("unknown P5 case").into()); + } + case + } + }; + if target.scheme_str() != Some("http") + || target.host() != Some("127.0.0.1") + || target.port_u16().is_none_or(|port| port == 0) + || target.port_u16() == Some(port) + || target + .authority() + .is_none_or(|authority| authority.as_str().contains('@')) + || target.query().is_some() + || port == 0 + || shape != expected + { + return Err(std::io::Error::other("P5 requires two controlled loopback origins").into()); + } + let a_uri = target.to_string(); + let b_uri = format!("http://127.0.0.1:{port}/p5/{token}/{port}/final"); + let client = HttpClient::with_client(FastlyOutboundClient::new()); + let decoding = ResponseDecoding::Decode; + let make = || -> Result { + let request = OutboundRequest::get(&a_uri)? + .header("x-request-token", token)? + .timeout(Duration::from_secs(2)) + .deadline(Deadline::after(Duration::from_secs(2))) + .max_encoded_response_bytes(64) + .max_decoded_response_bytes(10) + .max_response_bytes(10) + .response_decoding(decoding); + Ok( + if case.contains("/streamed/") + || case == "pending-head" + || case.starts_with("optimizer-") + { + request.stream_response() + } else { + request + }, + ) + }; + let result = futures::executor::block_on(async { + if case.starts_with("optimizer-") { + // Viceroy proves native dispatch/unavailability, not transformed output or performance. + let options = p5_options()?; + let next = case == "optimizer-next"; + let outbound = if next { + make()? + } else { + make()?.image_optimizer(options.clone()) + }; + let readback = outbound.image_optimizer_options() == Some(&options); + if case == "optimizer-send" { + return Ok::<_, EdgeError>( + json!({"failure":p5_response(client.send(outbound).await).await, + "policy_calls":0,"options_checked":readback}), + ); + } + let followed = p5_follow(&client, outbound, &b_uri, false, next).await?; + let mut failure = followed["response"].clone(); + failure["hop"] = followed["hop"].clone(); + return Ok( + json!({"failure":failure,"policy_calls":followed["policy_calls"], + "options_checked":if next {followed["options_checked"] == true} else {readback}}), + ); + } + if case == "pending-head" { + let followed = p5_follow(&client, make()?, &b_uri, false, false).await?; + let stopped = p5_follow(&client, make()?, &b_uri, true, false).await?; + return Ok(json!({"follow":followed,"stop":stopped})); + } + let sent = p5_response(client.send(make()?).await).await; + let followed = p5_follow(&client, make()?, &b_uri, false, false).await?; + Ok(json!({"send":sent,"follow":followed})) + }); + let probe = json!({"event":"fastly-p5-probe","case":case,"token":token, + "revision":env!("EDGEZERO_QUALIFICATION_REVISION"),"qualification":false, + "a_uri":a_uri,"b_uri":b_uri,"response_decoding":format!("{decoding:?}"), + "result":result.map_err(|error|p5_failure(&error))}); + Response::from_body(serde_json::to_string(&probe)?).send_to_client(); + Ok(()) +} + +fn client_failure(error: &edgezero_core::EdgeError) -> Value { + use edgezero_core::{FastlyFailureCause, ProviderFailureDetail, ProviderFailurePhase}; + let source_eof = matches!( + error.provider_failure_detail(), + Some(ProviderFailureDetail::Fastly { + phase: ProviderFailurePhase::ResponseBodyRead, + cause: FastlyFailureCause::IoError { + kind: std::io::ErrorKind::UnexpectedEof + }, + }) + ); + json!({"category":error.kind(),"source_unexpected_eof":source_eof}) +} + +fn client_probe( + request: &Request, + case: &str, + consumption: &str, + authority: &str, +) -> Result<(), fastly::Error> { + use edgezero_adapter_fastly::outbound::FastlyOutboundClient; + use edgezero_core::{BodyPayload, Deadline, HttpClient, OutboundRequest}; + let token = request_token(request)?; + let client = HttpClient::with_client(FastlyOutboundClient::new()); + let make = |slot| { + OutboundRequest::get(format!("http://{authority}/readiness/{case}/{slot}")) + .and_then(|request| request.header("x-readiness-token", token)) + .map(|request| { + request + .timeout(Duration::from_secs(2)) + .max_response_bytes(65_536) + }) + }; + let start = Instant::now(); + let mut body_owner = None; + let result = futures::executor::block_on(async { + if case == "full-body-error" { + let mut outbound = make(0)?; + if consumption == "streamed" { + outbound = outbound.stream_response(); + } + let mut bytes = 0usize; + let mut checksum = 0xcbf29ce484222325u64; + let result = match client.send(outbound).await { + Ok(response) if consumption == "streamed" => { + use futures::StreamExt; + let (payload, attached_owner) = response.into_body().into_parts(); + body_owner = attached_owner; + let BodyPayload::Stream(mut source) = payload else { + return Err(edgezero_core::EdgeError::internal(std::io::Error::other( + "expected streamed response", + ))); + }; + let mut result = Ok(()); + while let Some(chunk) = source.next().await { + match chunk { + Ok(chunk) => { + bytes += chunk.len(); + if bytes > 65_536 { + return Err(edgezero_core::EdgeError::internal( + std::io::Error::other("probe response limit exceeded"), + )); + } + for byte in chunk { + checksum = + (checksum ^ u64::from(byte)).wrapping_mul(0x100000001b3); + } + } + Err(error) => { + result = Err(error); + break; + } + } + } + result + } + Ok(response) => response.into_bytes_bounded(65_536).await.map(|_| ()), + Err(error) => Err(error), + }; + return Ok::<_, edgezero_core::EdgeError>( + json!({"kind":if result.is_err(){"error"}else{"eof"}, + "bytes":bytes,"checksum":format!("{checksum:016x}"),"failure":result.err().map(|e|client_failure(&e))}), + ); + } + let results = client + .send_all_until( + vec![make(0)?, make(1)?], + Deadline::after(Duration::from_micros(DEADLINE_US)), + ) + .await + .map_err(|failure| failure.into_parts().0)?; + let (collected, termination) = results.into_parts(); + let mut slots = Vec::new(); + for slot in collected { + slots.push(match slot { + edgezero_core::OutboundCollectedSlot::Unresolved => json!({"kind":"pending"}), + edgezero_core::OutboundCollectedSlot::Resolved(slot) => match slot.outcome { + Ok(response) => { + let bytes = response.into_bytes_bounded(65_536).await?; + let checksum = bytes.iter().fold(0xcbf29ce484222325u64, |sum, byte| (sum ^ u64::from(*byte)).wrapping_mul(0x100000001b3)); + json!({"kind":"ok","bytes":bytes.len(),"checksum":format!("{checksum:016x}"),"elapsed_us":slot.elapsed.as_micros()}) + } + Err(error) => json!({"kind":"error","failure":client_failure(&error)}), + }, + edgezero_core::OutboundCollectedSlot::AlreadyDelivered => return Err(edgezero_core::EdgeError::internal(std::io::Error::other("unexpected delivered readiness slot"))), + }); + } + Ok(json!({"termination":format!("{termination:?}"),"slots":slots})) + }); + let probe = json!({"event":"fastly-client-progress","case":case,"consumption":consumption, + "token":token,"revision":env!("EDGEZERO_QUALIFICATION_REVISION"),"result":result.map_err(|e|client_failure(&e)),"completed_us":start.elapsed().as_micros(),"qualification":false}); + println!("{probe}"); + Response::from_body(serde_json::to_string(&probe)?).send_to_client(); + drop(body_owner); + Ok(()) +} + +struct Selection { + status: String, + index: Option, + valid: bool, + entered: Instant, + returned: Instant, +} + +// Only this opt-in host-primitive experiment calls the raw readiness ABI. +#[allow( + unsafe_code, + reason = "probe the pinned ABI with live borrowed handles" +)] +fn select(handles: &[u32], timeout_ms: u32) -> Selection { + let mut selected = u32::MAX; + let entered = Instant::now(); + // SAFETY: slices remain live, and the caller retains ownership of every handle. + let status = unsafe { + fastly_sys::fastly_async_io::select( + handles.as_ptr(), + handles.len(), + timeout_ms, + &mut selected, + ) + }; + let returned = Instant::now(); + let index = (selected != u32::MAX).then_some(selected as usize); + let valid = status.is_ok() && index.is_none_or(|index| index < handles.len()); + Selection { + status: format!("{status:?}"), + index, + valid, + entered, + returned, + } +} + +#[allow( + unsafe_code, + reason = "borrow identity without transferring body ownership" +)] +fn body_id(body: &BodyHandle) -> u32 { + // SAFETY: readiness observes identity only; the body remains owned by its slot. + unsafe { body.as_u32() } +} + +enum Handle { + Head(PendingRequestHandle), + Body { + _head: ResponseHandle, + body: BodyHandle, + }, +} + +struct Slot { + handle: Option, + phase: &'static str, + result: Option, + bytes: usize, + checksum: u64, +} + +struct Exchange { + identity: Value, + start: Instant, + selected: bool, + read_limit: usize, + heads_first: bool, + samples: Vec, + slots: Vec, +} + +impl Exchange { + fn now(&self) -> u64 { + self.start.elapsed().as_micros() as u64 + } + + fn record(&mut self, sample: Value) -> Result<(), fastly::Error> { + if self.samples.len() >= 512 { + return Err(std::io::Error::other("readiness trace capacity exhausted").into()); + } + println!( + "{}", + json!({"event":"readiness-sample", "identity":self.identity, "sample":sample}) + ); + self.samples.push(sample); + Ok(()) + } + + fn finish( + &mut self, + slot: usize, + outcome: &str, + error: Option, + ) -> Result<(), fastly::Error> { + let start = self.now(); + let state = &mut self.slots[slot]; + // Close guest-owned resources before recording the terminal boundary. + state.handle = None; + let phase = state.phase; + let end = self.now(); + let mut sample = json!({"operation":"finish", "slot":slot, "start_us":start, + "end_us":end, "outcome":outcome, "phase":phase}); + let mut result = json!({"outcome":outcome, "phase":phase, "at_us":end, + "bytes":self.slots[slot].bytes, + "checksum":format!("{:016x}", self.slots[slot].checksum)}); + if let Some(error) = error { + sample["error"] = json!(error); + result["error"] = json!(error); + } + self.slots[slot].result = Some(result); + self.record(sample) + } + + // A pull performs at most one raw body read per fresh matching readiness result. + fn next_chunk(&mut self) -> Result)>, fastly::Error> { + loop { + if self.slots.iter().all(|slot| slot.result.is_some()) { + return Ok(None); + } + if self.now() >= DEADLINE_US { + for slot in 0..self.slots.len() { + if self.slots[slot].result.is_none() { + self.finish(slot, "timeout", None)?; + } + } + continue; + } + let heads_only = self.heads_first + && self + .slots + .iter() + .any(|slot| matches!(slot.handle, Some(Handle::Head(_)))); + let live: Vec<_> = self + .slots + .iter() + .enumerate() + .filter_map(|(index, slot)| match &slot.handle { + Some(Handle::Head(handle)) => Some((index, "head", handle.as_u32())), + Some(Handle::Body { body, .. }) if !heads_only => { + Some((index, "body", body_id(body))) + } + _ => None, + }) + .collect(); + let slot = if self.selected || live.iter().any(|(_, phase, _)| *phase == "head") { + let handles: Vec<_> = live.iter().map(|(_, _, id)| *id).collect(); + let start = self.now(); + if start >= DEADLINE_US { + continue; + } + let timeout_ms = ((DEADLINE_US - start) / 1000).max(1) as u32; + let selection = select(&handles, timeout_ms); + let end = self.now(); + self.record(json!({"operation":"select", "start_us":start, "end_us":end, + "slots":live.iter().map(|(slot, _, _)| *slot).collect::>(), + "phases":live.iter().map(|(_, phase, _)| *phase).collect::>(), + "timeout_ms":timeout_ms, "status":selection.status, "selected":selection.index, + "abi_enter_us":selection.entered.duration_since(self.start).as_micros(), + "abi_return_us":selection.returned.duration_since(self.start).as_micros()}))?; + if !selection.valid { + return Err(std::io::Error::other("invalid readiness status or index").into()); + } + if end >= DEADLINE_US { + continue; + } + let Some(index) = selection.index else { + continue; + }; + live[index].0 + } else { + live[0].0 + }; + let before = self.now(); + if before >= DEADLINE_US { + continue; + } + if matches!(self.slots[slot].handle, Some(Handle::Head(_))) { + let Some(Handle::Head(pending)) = self.slots[slot].handle.take() else { + return Err(std::io::Error::other("missing selected head").into()); + }; + let outcome = pending.poll(); + let end = self.now(); + match outcome { + PollHandleResult::Pending(pending) => { + self.slots[slot].handle = Some(Handle::Head(pending)); + self.record( + json!({"operation":"head-poll", "slot":slot, "start_us":before, + "end_us":end, "outcome":"pending"}), + )?; + return Err(std::io::Error::other("selected head remained pending").into()); + } + PollHandleResult::Done(Ok((head, body))) => { + self.record( + json!({"operation":"head-poll", "slot":slot, "start_us":before, + "end_us":end, "outcome":"ready"}), + )?; + if end < DEADLINE_US { + self.slots[slot].phase = "body"; + self.slots[slot].handle = Some(Handle::Body { _head: head, body }); + } else { + drop((head, body)); + } + } + PollHandleResult::Done(Err(error)) => { + let error = format!("{error:?}"); + self.record( + json!({"operation":"head-poll", "slot":slot, "start_us":before, + "end_us":end, "outcome":"error", "error":error}), + )?; + if end < DEADLINE_US { + self.finish(slot, "error", Some(error))?; + } + } + } + continue; + } + let mut chunk = vec![0; self.read_limit]; + println!( + "{}", + json!({"event":"read-enter", "identity":self.identity, "slot":slot, "start_us":before}) + ); + let before = self.now(); + if before >= DEADLINE_US { + continue; + } + let Some(Handle::Body { body, .. }) = &mut self.slots[slot].handle else { + return Err(std::io::Error::other("missing selected body").into()); + }; + let outcome = body.read(&mut chunk); + let end = self.now(); + let mut sample = json!({"operation":"read", "slot":slot, "start_us":before, + "end_us":end, "capacity":self.read_limit}); + let (bytes, error) = match outcome { + Ok(bytes) => { + sample["bytes"] = json!(bytes); + (bytes, None) + } + Err(error) => { + let error = format!("{:?}", error.kind()); + sample["error"] = json!(error); + (0, Some(error)) + } + }; + self.record(sample)?; + if end >= DEADLINE_US { + continue; + } + if let Some(error) = error { + self.finish(slot, "error", Some(error))?; + } else if bytes == 0 { + self.finish(slot, "eof", None)?; + } else { + chunk.truncate(bytes); + let state = &mut self.slots[slot]; + state.bytes += bytes; + if state.bytes > 65_536 { + return Err(std::io::Error::other("diagnostic byte cap exceeded").into()); + } + for byte in &chunk { + state.checksum = + (state.checksum ^ u64::from(*byte)).wrapping_mul(0x100000001b3); + } + return Ok(Some((slot, chunk))); + } + } + } +} + +// Ownership crosses the pull boundary before any body read, without resetting time. +struct PullBody(Exchange); +impl PullBody { + fn new(mut exchange: Exchange) -> Result { + let now = exchange.now(); + exchange.record(json!({"operation":"handoff", "start_us":now, "end_us":now, + "body_reads":0, "deadline_us":DEADLINE_US}))?; + Ok(Self(exchange)) + } + fn next(&mut self) -> Result)>, fastly::Error> { + self.0.next_chunk() + } +} + +fn request_token(request: &Request) -> Result<&str, fastly::Error> { + let token = request + .get_header_str("x-request-token") + .ok_or_else(|| std::io::Error::other("missing token"))?; + if token.is_empty() + || token.len() > 128 + || !token + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-') + { + return Err(std::io::Error::other("invalid token").into()); + } + Ok(token) +} + +fn timer_control(request: &Request, source: &str, budget: &str) -> Result<(), fastly::Error> { + let budget_ms: u32 = budget.parse()?; + if ![1, 100, 500].contains(&budget_ms) || !["empty-select", "wasi-sleep"].contains(&source) { + return Err(std::io::Error::other("invalid timer control").into()); + } + let token = request_token(request)?; + let start = Instant::now(); + let selection = if source == "empty-select" { + Some(select(&[], budget_ms)) + } else { + std::thread::sleep(Duration::from_millis(u64::from(budget_ms))); + None + }; + let elapsed_us = match &selection { + Some(selection) => selection.returned.duration_since(start), + None => start.elapsed(), + } + .as_micros() as u64; + let budget_us = u64::from(budget_ms) * 1000; + let mut result = json!({"schema":1, "event":"readiness-timer-control", "source":source, + "budget_ms":budget_ms, "token":token, "elapsed_us":elapsed_us, + "overrun_us":elapsed_us.saturating_sub(budget_us), + "within_five_ms":elapsed_us <= budget_us + 5000, "qualification":false, + "revision":option_env!("EDGEZERO_QUALIFICATION_REVISION").unwrap_or("unrecorded"), + "source_state":"working-tree-characterization", "measurement_end":"primitive-return"}); + if let Some(selection) = selection { + result["abi_status"] = json!(selection.status); + result["selected"] = json!(selection.index); + result["abi_enter_us"] = json!(selection.entered.duration_since(start).as_micros()); + result["abi_return_us"] = json!(selection.returned.duration_since(start).as_micros()); + } + println!("{result}"); + Response::from_body(serde_json::to_string(&result)?).send_to_client(); + Ok(()) +} + +fn main() -> Result<(), fastly::Error> { + fastly::init(); + let request = Request::from_client(); + let segments: Vec<_> = request + .get_path() + .trim_start_matches('/') + .split('/') + .collect(); + if let ["timer-control", source, budget] = segments.as_slice() { + return timer_control(&request, source, budget); + } + if let Some(case) = request.get_path().strip_prefix("/p5/") { + return p5_probe(&request, case); + } + let [case, mode, consumption, limit] = segments.as_slice() else { + return Err(std::io::Error::other("invalid readiness route").into()); + }; + if ![ + "truncated", + "silent-head", + "pending-body", + "trickle", + "siblings", + "exhausted", + "eof", + "empty-eof", + "malformed-head", + "malformed-body", + "head-error", + "mixed-head-body", + "mixed-body-head", + "full-body-error", + ] + .contains(case) + || !["buffered", "streamed"].contains(consumption) + || !["selected", "direct", "client"].contains(mode) + || (*mode == "direct" + && (*case != "truncated" || *consumption != "buffered" || *limit != "1")) + { + return Err(std::io::Error::other("unsupported readiness leg").into()); + } + let read_limit: usize = limit.parse()?; + if ![1, 16_384].contains(&read_limit) { + return Err(std::io::Error::other("unsupported diagnostic read capacity").into()); + } + if matches!( + *case, + "mixed-head-body" | "mixed-body-head" | "full-body-error" + ) && read_limit != 16_384 + { + return Err( + std::io::Error::other("progress probe requires full diagnostic capacity").into(), + ); + } + let target: edgezero_core::http::Uri = request + .get_header_str("x-fixture-backend") + .ok_or_else(|| std::io::Error::other("missing controlled origin"))? + .parse()?; + let authority = target + .authority() + .ok_or_else(|| std::io::Error::other("missing authority"))?; + if target.scheme_str() != Some("http") + || target.host() != Some("127.0.0.1") + || target.port_u16().is_none_or(|port| port == 0) + || authority.as_str().contains('@') + || target.query().is_some() + || target.path() != format!("/readiness/{case}/0") + { + return Err(std::io::Error::other("origin must be controlled loopback endpoint").into()); + } + let token = request_token(&request)?; + if *mode == "client" { + if !matches!( + *case, + "mixed-head-body" | "mixed-body-head" | "full-body-error" + ) { + return Err(std::io::Error::other("unsupported client probe").into()); + } + return client_probe(&request, case, consumption, authority.as_str()); + } + let backend = Backend::builder("readiness-origin", authority.as_str()) + .connect_timeout(Duration::from_secs(2)) + .first_byte_timeout(Duration::from_secs(2)) + .between_bytes_timeout(Duration::from_secs(2)) + .finish()?; + let mut exchange = Exchange { + identity: json!({"case":case,"mode":mode,"consumption":consumption, + "read_limit":read_limit,"token":token}), + start: Instant::now(), + selected: *mode == "selected", + read_limit, + heads_first: *case == "siblings", + samples: Vec::new(), + slots: Vec::new(), + }; + let count = if matches!(*case, "siblings" | "mixed-head-body" | "mixed-body-head") { + 2 + } else { + 1 + }; + for slot in 0..count { + let (head, body) = Request::get(format!("http://{authority}/readiness/{case}/{slot}")) + .with_header("x-readiness-token", token) + .with_pass(true) + .into_handles(); + let before = exchange.now(); + let pending = head.send_async(body.unwrap_or_else(BodyHandle::new), backend.name()); + let end = exchange.now(); + let pending = match pending { + Ok(pending) => pending, + Err(error) => { + exchange.record( + json!({"operation":"dispatch", "slot":slot, "start_us":before, + "end_us":end, "outcome":"error", "error":format!("{error:?}")}), + )?; + return Err(std::io::Error::other("diagnostic dispatch failed").into()); + } + }; + exchange.slots.push(Slot { + handle: Some(Handle::Head(pending)), + phase: "head", + result: None, + bytes: 0, + checksum: 0xcbf29ce484222325, + }); + exchange.record( + json!({"operation":"dispatch", "slot":slot, "start_us":before, + "end_us":end, "outcome":"pending"}), + )?; + } + let mut buffered = vec![Vec::new(); exchange.slots.len()]; + if *consumption == "buffered" { + while let Some((slot, chunk)) = exchange.next_chunk()? { + buffered[slot].extend_from_slice(&chunk); + } + } else { + let mut stream = PullBody::new(exchange)?; + while let Some((_slot, chunk)) = stream.next()? { + drop(chunk); + } + exchange = stream.0; + } + let completed = exchange.now(); + let slots: Vec<_> = exchange + .slots + .into_iter() + .enumerate() + .map(|(slot, state)| { + let mut result = state.result.unwrap_or(Value::Null); + result["buffered_bytes"] = json!(buffered[slot].len()); + result + }) + .collect(); + let probe = json!({"schema":2, "case":case, "mode":mode, "consumption":consumption, + "read_limit":read_limit, "token":token, + "revision":option_env!("EDGEZERO_QUALIFICATION_REVISION").unwrap_or("unrecorded"), + "source_state":"working-tree-characterization", "qualification":false, + "deadline_us":DEADLINE_US, "tolerance_us":5000, "samples":exchange.samples, + "slots":slots, "completed_us":completed}); + println!("{probe}"); + Response::from_body(serde_json::to_string(&probe)?).send_to_client(); + Ok(()) +} diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/qualification.rs b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/qualification.rs new file mode 100644 index 000000000..dbbef3ecb --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/qualification.rs @@ -0,0 +1,6 @@ +fn main() -> Result<(), fastly::Error> { + edgezero_adapter_fastly::serve_app::( + edgezero_adapter_fastly::Serve::new(), + ) + .into_result() +} diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/retained_app.rs b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/retained_app.rs index 16381ab5d..3bbee3abc 100644 --- a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/retained_app.rs +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/retained_app.rs @@ -1,8 +1,12 @@ fn main() -> Result<(), fastly::Error> { - fixture_fastly::finish( - edgezero_adapter_fastly::serve_app_with_request_extensions::( - fixture_fastly::serving(), - fixture_fastly::extend, - ), - ) + fixture_fastly::finish(edgezero_adapter_fastly::serve_app_with_hooks::< + fixture_fastly::MeasuredApp, + _, + _, + _, + >( + fixture_fastly::serving(), + fixture_fastly::extend, + |_response| (), + )) } diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/single_request.rs b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/single_request.rs index 1136a122a..b8748ddaf 100644 --- a/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/single_request.rs +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/src/bin/single_request.rs @@ -1,7 +1,7 @@ -#[fastly::main] -fn main(req: fastly::Request) -> Result { - edgezero_adapter_fastly::run_app_with_request_extensions::( - req, +fn main() -> Result<(), fastly::Error> { + edgezero_adapter_fastly::run_app_with_hooks::( fixture_fastly::extend, + |_response| (), ) + .map(|_state| ()) } diff --git a/tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs b/tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs index cde8f6a46..4b53e0daf 100644 --- a/tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs +++ b/tests/fixtures/reusable-app/crates/fixture-fastly/src/lib.rs @@ -1,17 +1,68 @@ -use edgezero_adapter_fastly::{FastlyLogging, Serve, ServeSummary, init_logger}; -use edgezero_core::app::{App, Hooks, StoreMetadata}; +use edgezero_adapter_fastly::delivery::{ + FastlyNoAttemptReason, FastlyRequestDelivery, FastlyResponseCommitState, +}; +use edgezero_adapter_fastly::lifecycle::{Sandbox, serve_custom}; +use edgezero_adapter_fastly::request::{ + send_assembly_error, send_request_with_registries_and_hooks, +}; +use edgezero_adapter_fastly::{FASTLY_PLATFORM, FastlyLogging, Serve, ServeSummary, init_logger}; +use edgezero_core::app::{App, Hooks}; use edgezero_core::body::Body; -use edgezero_core::http::Extensions; +use edgezero_core::error::EdgeError; +use edgezero_core::http::{Extensions, Response as CoreResponse, StatusCode, Uri}; +use edgezero_core::outbound::{HttpClient, OutboundRequest}; +use edgezero_core::{ + AdmissionDecision, DEFAULT_INBOUND_READ_BUDGET, IngressGrant, MonotonicClock, + ResponseEgressCompletion, +}; use fastly::{Error, Request, Response}; use fixture_core::{FixtureApp, Observation, observe}; -use futures::{StreamExt, executor::block_on}; -use std::io::Write; +use futures::executor::block_on; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; static INITIALIZATIONS: Mutex> = Mutex::new(Vec::new()); static PANIC_IN_CONSTRUCTOR: AtomicBool = AtomicBool::new(false); +pub fn strict_delivery(result: FastlyRequestDelivery<()>) -> Result<(), Error> { + match result { + FastlyRequestDelivery::Routed { delivery, .. } + | FastlyRequestDelivery::Detached(delivery) => delivery + .error() + .map_or(Ok(()), |error| Err(Error::msg(error.message()))), + FastlyRequestDelivery::NoAttempt(reason) => Err(Error::msg(reason.message())), + FastlyRequestDelivery::Aborted => Err(Error::msg("ingress admission aborted")), + _ => Err(Error::msg("unsupported request delivery disposition")), + } +} + +fn delivery_observed(result: &FastlyRequestDelivery, obs: &Observation, token: &str) { + let disposition = match result { + FastlyRequestDelivery::Routed { .. } => "routed", + FastlyRequestDelivery::Detached(_) => "detached", + FastlyRequestDelivery::NoAttempt(_) => "no-attempt", + FastlyRequestDelivery::Aborted => "aborted", + _ => "unknown", + }; + let delivery = result.delivery(); + println!( + "{}", + serde_json::json!({"event":"delivery_report", "instance":obs.instance, + "ordinal":obs.ordinal,"token":token,"disposition":disposition, + "commitment":delivery.map(|value|format!("{:?}",value.commitment())), + "body_kind":delivery.map(|value|format!("{:?}",value.report().body_kind)), + "outcome":delivery.map(|value|format!("{:?}",value.report().outcome)), + "bytes_written":delivery.map(|value|value.report().bytes_written), + "fallback":delivery.and_then(|value|value.report().fallback_disposition).map(|value|format!("{value:?}")), + "may_send_native":result.may_send_native_response(), + "no_attempt_reason":result.no_attempt_reason().map(|value|match value { + FastlyNoAttemptReason::RequiredStore(_) => "required-store", + FastlyNoAttemptReason::RequestHeadConversion(_) => "request-head-conversion", + _ => "unknown", + })}) + ); +} + fn lifecycle(event: &str, observation: &Observation, token: &str) { println!( "{}", @@ -61,7 +112,7 @@ pub fn observation(req: &Request) -> Observation { ); value } -pub fn extend(req: &Request, extensions: &mut Extensions) { +pub fn extend(req: &mut Request, extensions: &mut Extensions) { let value = observation(req); let token = req.get_header_str("x-request-token").unwrap_or("missing"); initialized(&value, token); @@ -102,24 +153,63 @@ fn post_commit_error(obs: &Observation, token: &str, error: &dyn std::fmt::Displ ); } pub fn rebuilt() -> Result<(), Error> { - let mut logger_installed = false; - let logging = FastlyLogging::from(MeasuredApp::logging_for("fastly")); - finish(serving().run(move |req| -> Result { + finish(serve_custom(serving(), |req, setup: &mut Sandbox<()>| { + let stores = MeasuredApp::stores(); + let logging = FastlyLogging::from(MeasuredApp::logging_for("fastly")); + setup.setup_once(|| install_logger(&logging))?; + let app = App::build::(FASTLY_PLATFORM)?; + strict_delivery(send_request_with_registries_and_hooks( + &app, + stores, + req, + extend, + |_response| (), + )) + })) +} + +pub fn logger_negative() -> Result<(), Error> { + finish(serve_custom(serving(), |req, _state: &mut Sandbox<()>| { + let obs = observation(&req); let stores = MeasuredApp::stores(); - if !logger_installed { - install_logger(&logging)?; - logger_installed = true; + let logging = FastlyLogging::from(MeasuredApp::logging_for("fastly")); + if let Err(error) = install_logger(&logging) { + Response::from_status(500) + .with_body(error.to_string()) + .send_to_client(); + return Err(error); } - let app = MeasuredApp::build_app(); - edgezero_adapter_fastly::request::dispatch_with_registries(&app, req, stores, extend) + let app = App::build::(FASTLY_PLATFORM)?; + initialized( + &obs, + req.get_header_str("x-request-token").unwrap_or("missing"), + ); + strict_delivery(send_request_with_registries_and_hooks( + &app, + stores, + req, + |_, extensions| { + extensions.insert(obs); + }, + |_response| (), + )) })) } -fn custom_dispatch( - mut req: Request, - sandbox: &mut edgezero_adapter_fastly::lifecycle::Sandbox, - reuse_app: bool, -) -> Result<(), Error> { +fn finalize_response(response: &mut CoreResponse, attempts: u64) { + if let Some(fixture_core::Finalize(value)) = + response.extensions_mut().remove::() + { + response + .headers_mut() + .append("x-fixture-finalized", value.parse().unwrap()); + } + response + .headers_mut() + .insert("x-fixture-attempts", attempts.to_string().parse().unwrap()); +} + +fn custom_dispatch(req: Request, sandbox: &mut Sandbox, reuse_app: bool) -> Result<(), Error> { let token = req .get_header_str("x-request-token") .unwrap_or("missing") @@ -148,117 +238,113 @@ fn custom_dispatch( return Ok(()); } log::info!("fixture request {}", obs.correlation); - // Only the streaming route schedules post-send work. - let post_send = req - .get_header_str("x-fixture-backend") - .filter(|uri| uri.contains("/chunks")) - .map(|uri| uri.replace("/chunks", "/post-send")); - req.set_header("x-fixture-mutated", "true"); + let reject_framing = req.get_path() == "/probe/framing-fallback"; + let post_send = req.get_header_str("x-fixture-backend").and_then(|value| { + let uri: Uri = value.parse().ok()?; + let authority = uri.authority()?; + matches!(uri.path(), "/chunks" | "/abort") + .then(|| format!("http://{authority}/post-send?token={token}")) + }); // Per-request mode scopes application state to the current callback. - let mut per_request = edgezero_adapter_fastly::lifecycle::Sandbox::default(); + let mut per_request = Sandbox::default(); let state = if reuse_app { sandbox } else { &mut per_request }; + let clock = MonotonicClock::default(); + let start = clock.now(); let result = state.initialize(|| { if req.get_path() == "/initialization-error" { - Err(Error::msg("injected initialization failure")) + Err(EdgeError::service_unavailable("state unavailable")) } else { - Ok(MeasuredApp::build_app()) + App::build::(FASTLY_PLATFORM) } }); - if result.is_err() { - Response::from_status(503) - .with_header( - "x-fixture-attempts", - state.initialization_attempts().to_string(), - ) - .with_body_json(&serde_json::json!({"instance":obs.instance, - "ordinal":obs.ordinal,"retained":state.state().is_some()}))? - .send_to_client(); - return Ok(()); + if let Err(error) = result { + println!( + "{}", + serde_json::json!({"event":"assembly-rejected", "token":token, + "instance":obs.instance,"ordinal":obs.ordinal,"retained":state.state().is_some(), + "attempts":state.initialization_attempts()}) + ); + let delivered = send_assembly_error(req, error, start, clock); + delivery_observed(&delivered, &obs, &token); + lifecycle("guest_completed", &obs, &token); + return strict_delivery(delivered); } initialized(&obs, &token); - let core = edgezero_adapter_fastly::request::into_core_request_with_registries( - req, + let app = state.state().unwrap(); + let attempts = state.initialization_attempts(); + let client = Arc::new(Mutex::new(None::)); + let request_client = Arc::clone(&client); + let delivered = send_request_with_registries_and_hooks( + app, stores, - |_, extensions| { + req, + |raw, extensions| { + raw.set_header("x-fixture-mutated", "true"); extensions.insert(obs.clone()); + extensions.insert(request_client); + }, + |response| { + finalize_response(response, attempts); + if reject_framing { + response + .headers_mut() + .insert("trailer", "fixture-trailer".parse().unwrap()); + } + client.lock().expect("request client capture lock").take() }, - )?; - let proxy = core - .extensions() - .get::() - .cloned(); - let mut response = block_on(state.state().unwrap().router().oneshot(core))?; - if let Some(fixture_core::Finalize(value)) = - response.extensions_mut().remove::() - { - response - .headers_mut() - .append("x-fixture-finalized", value.parse().unwrap()); - } - let (parts, body) = response.into_parts(); - let mut native = Response::from_status(parts.status.as_u16()); - native.set_header( - "x-fixture-attempts", - state.initialization_attempts().to_string(), ); - for (name, value) in &parts.headers { - native.append_header(name.as_str(), value.as_bytes()); - } - match body { - Body::Once(bytes) => { - native.set_body(bytes.to_vec()); - native.send_to_client(); - lifecycle("response_committed", &obs, &token); + delivery_observed(&delivered, &obs, &token); + let client = match delivered { + FastlyRequestDelivery::NoAttempt(reason) => { + Response::from_status(500) + .with_body(reason.message()) + .send_to_client(); + lifecycle("guest_completed", &obs, &token); + return Ok(()); } - Body::Stream(mut stream) => { - let mut writer = native.stream_to_client(); - lifecycle("response_committed", &obs, &token); - while let Some(chunk) = block_on(stream.next()) { - match chunk { - Ok(bytes) => { - if let Err(error) = writer.write_all(&bytes).and_then(|()| writer.flush()) { - post_commit_error(&obs, &token, &error); - drop(writer); - lifecycle("guest_completed", &obs, &token); - return Ok(()); - } - } - Err(error) => { - post_commit_error(&obs, &token, &error); - drop(writer); - lifecycle("guest_completed", &obs, &token); - return Ok(()); - } + FastlyRequestDelivery::Aborted | FastlyRequestDelivery::Detached(_) => { + lifecycle("guest_completed", &obs, &token); + return Ok(()); + } + FastlyRequestDelivery::Routed { + state: client, + delivery, + } => { + if let Some(error) = delivery.error() { + if delivery.commitment() == FastlyResponseCommitState::Committed { + post_commit_error(&obs, &token, &Error::msg(error.message())); + } else { + lifecycle("uncommitted_delivery_error", &obs, &token); } } - if let Err(error) = writer.finish() { - post_commit_error(&obs, &token, &error); + if delivery.commitment() != FastlyResponseCommitState::Committed + || delivery.report().body_kind != edgezero_core::ResponseEgressBodyKind::Application + { + lifecycle("guest_completed", &obs, &token); + return Ok(()); } + lifecycle("delivery_completed", &obs, &token); + client } - } - if let (Some(uri), Some(proxy)) = (post_send, proxy) { - let Ok(uri) = uri.parse::() else { - return Ok(()); - }; - if fixture_core::is_loopback_http(&uri) { - let outcome = block_on(proxy.forward(edgezero_core::proxy::ProxyRequest::new( - edgezero_core::http::Method::GET, - uri, - ))); - // All failures here are after commitment: record them without a second send. - match outcome { - Ok(response) => { - if let Body::Stream(mut stream) = response.into_body() { - while let Some(chunk) = block_on(stream.next()) { - if let Err(error) = chunk { - println!("post-send body failure: {error}"); - break; - } - } - } - } - Err(error) => println!("post-send failure: {error}"), + _ => return Err(Error::msg("unsupported request delivery disposition")), + }; + if let Some(uri) = post_send { + let client = + client.ok_or_else(|| Error::msg("routed fixture did not capture HTTP client"))?; + let outcome = block_on(async { + let uri: Uri = uri.parse().map_err(EdgeError::internal)?; + if !fixture_core::is_loopback_http(&uri) { + return Err(EdgeError::bad_request("backend must be loopback HTTP")); } + client + .send(OutboundRequest::get(uri.to_string())?.stream_response()) + .await? + .into_bytes_bounded(1024 * 1024) + .await?; + Ok::<(), EdgeError>(()) + }); + if let Err(error) = outcome { + post_commit_error(&obs, &token, &error); } } println!( @@ -276,6 +362,7 @@ pub fn custom(reuse_sandbox: bool, reuse_app: bool) -> Result<(), Error> { move |req, state| custom_dispatch(req, state, reuse_app), )) } else { + fastly::init(); edgezero_adapter_fastly::lifecycle::run_custom(Request::from_client(), |req, state| { custom_dispatch(req, state, false) }) @@ -295,19 +382,24 @@ impl Hooks for MeasuredApp { } } - fn routes() -> edgezero_core::router::RouterService { + fn routes() -> Result { FixtureApp::routes() } fn stores() -> edgezero_core::app::StoresMetadata { - FixtureApp::stores() + let mut stores = FixtureApp::stores(); + stores.kv = Some(edgezero_core::app::StoreMetadata { + default: "fixture_kv", + ids: &["fixture_kv", "missing_fixture_kv"], + }); + stores } - fn build_app() -> App { + fn configure(app: &mut App) -> Result<(), EdgeError> { if PANIC_IN_CONSTRUCTOR.swap(false, Ordering::SeqCst) { println!( "{}", - serde_json::json!({"event":"constructor_panic","instance":fixture_core::instance_id(),"source":"injected_inside_build_app"}) + serde_json::json!({"event":"constructor_panic","instance":fixture_core::instance_id(),"source":"injected_inside_configure"}) ); - panic!("injected failure inside MeasuredApp::build_app"); + panic!("injected failure inside MeasuredApp::configure"); } let cpu_before = fastly::compute_runtime::elapsed_vcpu_ms().ok(); let heap_before = fastly::compute_runtime::heap_memory_snapshot_mib().ok(); @@ -316,85 +408,171 @@ impl Hooks for MeasuredApp { .unwrap_or("0") .parse() .unwrap(); + let source = + serde_json::json!({"routes": ["one", "two"], "cache": {"capacity": 128}}).to_string(); for _ in 0..rounds { - let parsed: serde_json::Value = - serde_json::from_str(r#"{"routes":["one","two"],"cache":{"capacity":128}}"#) - .unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&source).unwrap(); std::hint::black_box(parsed); } - let app = FixtureApp::build_app(); + FixtureApp::configure(app)?; + app.set_ingress_admission_policy(|head| { + let token = head + .headers() + .get("x-request-token") + .and_then(|value| value.to_str().ok()) + .unwrap_or("missing") + .to_owned(); + AdmissionDecision::Admit { + completion: ResponseEgressCompletion::new(move |report| { + println!( + "{}", + serde_json::json!({"event":"egress_terminal","instance":fixture_core::instance_id(), + "token":token,"outcome":format!("{:?}",report.outcome), + "body_kind":format!("{:?}",report.body_kind),"bytes_written":report.bytes_written, + "fallback":report.fallback_disposition.map(|value|format!("{value:?}"))}) + ); + }), + grant: IngressGrant::empty(), + read_deadline: head.read_deadline_after(DEFAULT_INBOUND_READ_BUDGET), + } + }); INITIALIZATIONS.lock().unwrap().push(serde_json::json!({"event":"initialization", "wall_ns":start.elapsed().as_nanos(), "cpu_before_ms":cpu_before,"cpu_after_ms":fastly::compute_runtime::elapsed_vcpu_ms().ok(), "heap_before_mib":heap_before,"heap_after_mib":fastly::compute_runtime::heap_memory_snapshot_mib().ok(), "construction_rounds":rounds})); - app + Ok(()) } } /// Controlled fixture inputs exercise real adapter boundaries, not provider outages. pub fn faults() -> Result<(), Error> { - use edgezero_core::http::response_builder; - let mut retained = None; - finish(serving().run(move |mut req: Request| -> Result { - let obs = observation(&req); - let token = req.get_header_str("x-request-token").unwrap_or("missing").to_owned(); - let path = req.get_path().to_owned(); - if path == "/constructor-panic" { - PANIC_IN_CONSTRUCTOR.store(true, Ordering::SeqCst); - retained = None; - } - let app = retained.get_or_insert_with(MeasuredApp::build_app); - initialized(&obs, &token); - let result = match path.as_str() { - "/collect-error" => { - let body = Body::from_stream(futures::stream::iter([ - Err(std::io::Error::other("injected core stream failure")) - ])); - edgezero_adapter_fastly::response::from_core_response(response_builder().body(body).unwrap()).map_err(Error::from) + use edgezero_adapter_fastly::request::FastlyService; + finish(serve_custom( + serving(), + |mut req: Request, retained: &mut Sandbox| { + let obs = observation(&req); + let token = req + .get_header_str("x-request-token") + .unwrap_or("missing") + .to_owned(); + let path = req.get_path().to_owned(); + if path == "/constructor-panic" { + PANIC_IN_CONSTRUCTOR.store(true, Ordering::SeqCst); } - "/inbound-error" => { - let mut upstream = Request::get("http://fault-origin/abort").send("fault-origin")?; + retained.initialize(|| App::build::(FASTLY_PLATFORM))?; + let app = retained.state().unwrap(); + initialized(&obs, &token); + if path == "/inbound-error" { + let mut upstream = + Request::get("http://fault-origin/abort").send("fault-origin")?; req.set_body(upstream.take_body()); - edgezero_adapter_fastly::request::into_core_request(req) - .map(|_| Response::from_status(200)).map_err(Error::from) } - _ => { - let stores = if path == "/recoverable-store-error" - || path == "/terminal-store-error" - { - edgezero_core::app::StoresMetadata { - kv: Some(StoreMetadata { - default: "missing_fixture_store", - ids: &["missing_fixture_store"], - }), - ..MeasuredApp::stores() - } - } else { - MeasuredApp::stores() - }; - edgezero_adapter_fastly::request::dispatch_with_registries( + let prepare = |raw: &mut Request, extensions: &mut Extensions| { + if path == "/collect-error" || path == "/inbound-error" { + raw.set_url(format!("http://example.com/probe{}", path)); + } + extensions.insert(obs.clone()); + }; + let finalize = |response: &mut CoreResponse| { + if path == "/collect-error" { + *response.status_mut() = StatusCode::OK; + *response.body_mut() = Body::from_stream(futures::stream::once(async { + Err(EdgeError::internal(std::io::Error::other( + "injected core stream failure", + ))) + })); + } + response.status() + }; + let result = if path == "/recoverable-store-error" || path == "/terminal-store-error" { + FastlyService::new(app) + .with_kv("missing_fixture_store") + .require_kv() + .send_request_with_hooks(req, prepare, finalize) + } else { + send_request_with_registries_and_hooks( app, + MeasuredApp::stores(), req, - stores, - |_, extensions| { - extensions.insert(obs.clone()); - }, + prepare, + finalize, ) - } - }; - match result { - Ok(response) => { lifecycle("conversion_completed", &obs, &token); Ok(response) } - Err(error) => { - println!("{}", serde_json::json!({"event":"adapter_error","instance":obs.instance,"ordinal":obs.ordinal,"token":token,"path":path,"error":error.to_string(),"source":"controlled_fixture_input"})); - if path == "/recoverable-store-error" { - // Explicit custom policy permits another callback; standard helpers propagate. - Ok(Response::from_status(503).with_body("injected selector failed")) - } else { - lifecycle("terminal_error", &obs, &token); - Err(error) + }; + delivery_observed(&result, &obs, &token); + let (status, delivery) = match result { + FastlyRequestDelivery::Routed { state, delivery } => (Some(state), delivery), + FastlyRequestDelivery::Detached(delivery) => (None, delivery), + FastlyRequestDelivery::Aborted => return Ok(()), + FastlyRequestDelivery::NoAttempt(reason) => { + let error = Error::msg(reason.message()); + println!( + "{}", + serde_json::json!({"event":"adapter_error","instance":obs.instance,"ordinal":obs.ordinal,"token":token,"path":path,"error":error.to_string(),"source":"controlled_fixture_input"}) + ); + if path == "/recoverable-store-error" { + // Explicit custom policy permits another callback; standard helpers propagate. + Response::from_status(503) + .with_body("injected selector failed") + .send_to_client(); + return Ok(()); + } else { + Response::from_status(500) + .with_body("injected selector failed") + .send_to_client(); + lifecycle("terminal_error", &obs, &token); + return Err(error); + } + } + _ => return Err(Error::msg("unsupported request delivery disposition")), + }; + if let Some(error) = delivery.error() { + let error = Error::msg(error.message()); + if delivery.commitment() == FastlyResponseCommitState::Committed { + post_commit_error(&obs, &token, &error); } + lifecycle("terminal_error", &obs, &token); + return Err(error); } + if path == "/inbound-error" { + assert_eq!(status, Some(StatusCode::INTERNAL_SERVER_ERROR)); + lifecycle("ingress_error_response", &obs, &token); + } + lifecycle("delivery_completed", &obs, &token); + Ok(()) + }, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use edgezero_core::http::Response as CoreResponse; + + #[test] + fn finalization_preserves_streaming_body_and_request_local_headers() { + for (attempts, token) in [(1, "first"), (2, "second")] { + let mut response = CoreResponse::new(Body::stream(futures::stream::once(async { + Body::from("chunk").into_bytes().unwrap() + }))); + response + .extensions_mut() + .insert(fixture_core::Finalize(token.to_owned())); + + finalize_response(&mut response, attempts); + + assert_eq!(response.headers()["x-fixture-finalized"], token); + assert_eq!( + response.headers()["x-fixture-attempts"], + attempts.to_string() + ); + assert!( + response + .extensions() + .get::() + .is_none() + ); + assert!(response.body().is_stream()); } - })) + } } diff --git a/tests/fixtures/reusable-app/crates/fixture-harness/Cargo.toml b/tests/fixtures/reusable-app/crates/fixture-harness/Cargo.toml deleted file mode 100644 index 3438d4cf4..000000000 --- a/tests/fixtures/reusable-app/crates/fixture-harness/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "fixture-harness" -version.workspace = true -edition.workspace = true -publish.workspace = true - -[dependencies] -ctrlc = { version = "3.5.2", features = ["termination"] } -serde_json.workspace = true - -[lints] -workspace = true diff --git a/tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs b/tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs deleted file mode 100644 index acc293e71..000000000 --- a/tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs +++ /dev/null @@ -1,382 +0,0 @@ -mod evidence; -mod net; -mod runners; -use evidence::{Result, require}; -use serde_json::{Value, json}; -use std::{ - fs::{self, OpenOptions}, - io::Write, - path::{Path, PathBuf}, - process::{Child, Command, Stdio}, - sync::{ - Mutex, PoisonError, - atomic::{AtomicU64, Ordering}, - }, - thread, - time::{Duration, Instant, SystemTime, UNIX_EPOCH}, -}; - -pub struct Args { - adapter: String, - suite: String, - output: Option, - requests: usize, - repetitions: usize, - seed: u64, - construction_rounds: u64, - max_requests: usize, -} -impl Args { - fn parse() -> Result { - let mut a = Self { - adapter: "fastly".into(), - suite: "smoke".into(), - output: None, - requests: 100, - repetitions: 3, - seed: 856, - construction_rounds: 0, - max_requests: 10, - }; - let mut args = std::env::args().skip(1); - while let Some(key) = args.next() { - if key == "--help" || key == "-h" { - println!( - "fixture-harness [--adapter fastly|cloudflare|spin|axum|all] [--suite smoke|benchmark] [--output EMPTY_DIR] [--requests 100] [--repetitions 3] [--seed 856] [--construction-rounds 0] [--max-requests 10]" - ); - std::process::exit(0); - } - let value = args.next().ok_or(format!("missing value for {key}"))?; - match key.as_str() { - "--adapter" => a.adapter = value, - "--suite" => a.suite = value, - "--output" => a.output = Some(value.into()), - "--requests" => a.requests = value.parse()?, - "--repetitions" => a.repetitions = value.parse()?, - "--seed" => a.seed = value.parse()?, - "--construction-rounds" => a.construction_rounds = value.parse()?, - "--max-requests" => a.max_requests = value.parse()?, - _ => return Err(format!("unknown option {key}").into()), - } - } - require( - ["fastly", "cloudflare", "spin", "axum", "all"].contains(&a.adapter.as_str()), - "unknown adapter", - )?; - require( - ["smoke", "benchmark"].contains(&a.suite.as_str()), - "unknown suite", - )?; - require( - a.requests > 0 && a.repetitions > 0, - "requests and repetitions must be positive", - )?; - require( - (1..=1000).contains(&a.max_requests), - "max-requests must be 1..1000", - )?; - Ok(a) - } -} -pub fn root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .canonicalize() - .expect("fixture workspace root") -} -pub fn token() -> String { - static NEXT: AtomicU64 = AtomicU64::new(0); - format!( - "{:x}-{:x}-{:x}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(), - std::process::id(), - NEXT.fetch_add(1, Ordering::Relaxed) - ) -} -pub struct Records { - path: PathBuf, - values: Vec, -} -impl Records { - fn collect_clients(&mut self, fields: Value) -> Result<()> { - for event in net::take_client_events() { - if !self.values.iter().any(|existing| { - existing["event"] == "client_completed" && existing["token"] == event["token"] - }) { - self.push(annotate(event, fields.clone()))?; - } - } - Ok(()) - } - fn push(&mut self, v: Value) -> Result<()> { - if matches!( - v["status"].as_str(), - Some("fail" | "unverified" | "unsupported") - ) { - eprintln!("fixture-harness: {v}"); - } - writeln!( - OpenOptions::new() - .create(true) - .append(true) - .open(&self.path)?, - "{v}" - )?; - self.values.push(v); - Ok(()) - } -} -pub fn logs(path: &Path) -> Vec { - fs::read_to_string(path) - .unwrap_or_default() - .lines() - .filter(|l| l.starts_with('{')) - .filter_map(|l| serde_json::from_str(l).ok()) - .collect() -} -pub fn await_evidence(mut check: impl FnMut() -> Result<()>) -> Result<()> { - let start = Instant::now(); - loop { - match check() { - Ok(()) => return Ok(()), - Err(e) if start.elapsed() >= Duration::from_secs(5) => return Err(e), - Err(_) => thread::sleep(Duration::from_millis(50)), - } - } -} -pub fn executable(variable: &str, default: &str) -> Option { - let value = std::env::var(variable).unwrap_or_else(|_| default.into()); - let path = PathBuf::from(&value); - if path.components().count() > 1 { - return path.canonicalize().ok(); - } - std::env::split_paths(&std::env::var_os("PATH")?) - .map(|p| p.join(&value)) - .find(|p| p.is_file()) - .and_then(|p| p.canonicalize().ok()) -} -pub fn checked(command: &mut Command) -> Result<()> { - let status = command.status()?; - require( - status.success(), - format!("command {command:?} failed: {status}"), - ) -} -/// Process groups of live runtimes, killed by the Ctrl-C handler because a -/// terminal SIGINT reaches only the harness's own foreground group. -static LIVE_GROUPS: Mutex> = Mutex::new(Vec::new()); -fn signal_group(id: u32, signal: &str) { - #[cfg(unix)] - let target = format!("-{id}"); - #[cfg(not(unix))] - let target = id.to_string(); - let _ = Command::new("kill") - .args([signal, "--", &target]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); -} -fn kill_live_groups_and_exit() { - for id in LIVE_GROUPS - .lock() - .unwrap_or_else(PoisonError::into_inner) - .iter() - { - signal_group(*id, "-KILL"); - } - std::process::exit(130); -} -pub struct Process(Child); -impl Process { - pub fn start(command: &mut Command, log: &Path, port: u16) -> Result { - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - command.process_group(0); - } - let file = fs::File::create(log)?; - let mut p = Self( - command - .stdout(Stdio::from(file.try_clone()?)) - .stderr(Stdio::from(file)) - .spawn()?, - ); - LIVE_GROUPS - .lock() - .unwrap_or_else(PoisonError::into_inner) - .push(p.0.id()); - let start = Instant::now(); - loop { - if std::net::TcpStream::connect(("127.0.0.1", port)).is_ok() { - return Ok(p); - } - require( - p.0.try_wait()?.is_none(), - format!("runtime exited; see {}", log.display()), - )?; - require( - start.elapsed() < Duration::from_secs(45), - "runtime did not listen", - )?; - thread::sleep(Duration::from_millis(50)); - } - } -} -impl Drop for Process { - fn drop(&mut self) { - let id = self.0.id(); - signal_group(id, "-TERM"); - let start = Instant::now(); - while start.elapsed() < Duration::from_secs(5) { - if matches!(self.0.try_wait(), Ok(Some(_))) { - break; - } - thread::sleep(Duration::from_millis(50)); - } - // The parent may have exited while a descendant still holds sockets. - #[cfg(unix)] - signal_group(id, "-KILL"); - #[cfg(not(unix))] - let _ = self.0.kill(); - let _ = self.0.wait(); - LIVE_GROUPS - .lock() - .unwrap_or_else(PoisonError::into_inner) - .retain(|live| *live != id); - } -} -pub fn req(port: u16, path: &str) -> Result { - net::request(port, path, &token(), None, None) -} -pub fn guest(reply: &Value) -> Result { - require( - reply["status"] == 200, - format!("unexpected response: {reply}"), - )?; - Ok(serde_json::from_str( - reply["body"].as_str().ok_or("missing body")?, - )?) -} -pub fn annotate(mut value: Value, fields: Value) -> Value { - value - .as_object_mut() - .unwrap() - .extend(fields.as_object().unwrap().clone()); - value -} -pub fn artifact(path: &Path) -> Result { - let bytes = fs::read(path)?; - let fingerprint = bytes.iter().fold(0xcbf29ce484222325u64, |hash, byte| { - (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3) - }); - Ok( - json!({"path":path,"bytes":bytes.len(),"fnv1a64":format!("{fingerprint:016x}"),"purpose":"non-security build identity"}), - ) -} -/// The compiler that `$CARGO` builds with: `$RUSTC` when set, else the -/// `rustc` beside `$CARGO`, else whichever `rustc` is on `PATH`. -fn rustc() -> PathBuf { - if let Some(rustc) = std::env::var_os("RUSTC") { - return rustc.into(); - } - std::env::var_os("CARGO") - .map(|cargo| { - Path::new(&cargo).with_file_name(format!("rustc{}", std::env::consts::EXE_SUFFIX)) - }) - .filter(|rustc| rustc.is_file()) - .unwrap_or_else(|| "rustc".into()) -} -fn run() -> Result { - let args = Args::parse()?; - ctrlc::set_handler(kill_live_groups_and_exit)?; - let path = args - .output - .clone() - .unwrap_or_else(|| root().join(".runs").join(token())); - if path.exists() { - require( - fs::read_dir(&path)?.next().is_none(), - "output directory must be empty; refusing to overwrite evidence", - )?; - } - fs::create_dir_all(&path)?; - let path = path.canonicalize()?; - fs::copy(root().join("Cargo.lock"), path.join("Cargo.lock"))?; - let mut records = Records { - path: path.join("events.jsonl"), - values: Vec::new(), - }; - records.push(json!({"event":"run","seed":args.seed,"suite":args.suite,"construction_rounds":args.construction_rounds,"max_requests":args.max_requests}))?; - records.push(json!({"event":"build_environment","rustc":String::from_utf8_lossy(&Command::new(rustc()).arg("--version").output()?.stdout).trim(),"host_os":std::env::consts::OS,"host_arch":std::env::consts::ARCH,"lockfile":artifact(&path.join("Cargo.lock"))?}))?; - let adapters = if args.adapter == "all" { - vec!["fastly", "cloudflare", "spin", "axum"] - } else { - vec![args.adapter.as_str()] - }; - for adapter in adapters { - let recorded = records.values.len(); - let result = if adapter == "fastly" { - runners::fastly(&args, &path, &mut records) - } else { - runners::provider(adapter, &args, &path, &mut records) - }; - if let Err(error) = result { - // Runners record their own contextual failure for this error; add one only - // when they did not (an earlier non-fatal fail record must not hide it). - let error = error.to_string(); - if !records.values[recorded..] - .iter() - .any(|r| r["status"] == "fail" && r["error"] == error.as_str()) - { - records.push(json!({"status":"fail","adapter":adapter,"error":error}))?; - } - } - } - let summary = evidence::summarize(&records.values); - if summary["build_identity"]["status"] == "invalid" { - let builds = records - .values - .iter() - .filter(|r| r["event"] == "build") - .map(|r| json!({"variant":r["variant"],"adapter":r["adapter"],"repetition":r["repetition"],"fnv1a64":r["artifact"]["fnv1a64"]})) - .collect::>(); - records - .push(json!({"status":"fail","reason":"mixed build fingerprints","builds":builds}))?; - } - let code = if records.values.iter().any(|r| r["status"] == "fail") { - 1 - } else if records - .values - .iter() - .any(|r| r["status"] == "unverified" || r["status"] == "unsupported") - { - 2 - } else { - 0 - }; - fs::write( - path.join("summary.json"), - serde_json::to_string_pretty(&summary)?, - )?; - for (key, value) in summary["variants"].as_object().unwrap() { - println!( - "{key}: {} samples, completion p50 {:.3} ms", - value["samples"], - value["completion_ns"]["p50"].as_f64().unwrap_or(0.) / 1_000_000. - ); - } - println!("{}", json!({"exit_code":code,"evidence":path})); - Ok(code) -} -fn main() { - match run() { - Ok(code) => std::process::exit(code), - Err(error) => { - eprintln!("fixture-harness: {error}"); - std::process::exit(1); - } - } -} diff --git a/tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs b/tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs deleted file mode 100644 index 8c5653245..000000000 --- a/tests/fixtures/reusable-app/crates/fixture-harness/src/net.rs +++ /dev/null @@ -1,355 +0,0 @@ -use crate::evidence::{Result, require}; -use serde_json::{Value, json}; -use std::{ - io::{BufRead, BufReader, Read, Write}, - net::{TcpListener, TcpStream}, - sync::{ - Arc, Condvar, Mutex, - atomic::{AtomicBool, Ordering}, - }, - thread, - time::{Duration, Instant}, -}; - -pub type Signal = Arc<(Mutex, Condvar)>; -static CLIENT_EVENTS: Mutex> = Mutex::new(Vec::new()); -pub fn take_client_events() -> Vec { - std::mem::take(&mut *CLIENT_EVENTS.lock().unwrap()) -} - -#[derive(Debug)] -pub struct InterruptedResponse { - pub status: u16, - pub body: Vec, - pub reason: String, -} -impl std::fmt::Display for InterruptedResponse { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "response {} interrupted after {} bytes: {}", - self.status, - self.body.len(), - self.reason - ) - } -} -impl std::error::Error for InterruptedResponse {} -pub fn release(signal: &Signal) { - *signal.0.lock().unwrap() = true; - signal.1.notify_all(); -} -pub struct Backend { - pub port: u16, - pub receipts: Arc>>, - pub release: Signal, - stop: Arc, - worker: Option>, -} -impl Backend { - pub fn start() -> Result { - let listener = TcpListener::bind(("127.0.0.1", 0))?; - let port = listener.local_addr()?.port(); - listener.set_nonblocking(true)?; - let receipts = Arc::new(Mutex::new(Vec::new())); - let release = Arc::new((Mutex::new(false), Condvar::new())); - let barrier = Arc::new((Mutex::new(0usize), Condvar::new())); - let stop = Arc::new(AtomicBool::new(false)); - let (r, s, b, done) = ( - receipts.clone(), - release.clone(), - barrier.clone(), - stop.clone(), - ); - let worker = thread::spawn(move || { - while !done.load(Ordering::Relaxed) { - match listener.accept() { - Ok((stream, _)) => { - let (r, s, b) = (r.clone(), s.clone(), b.clone()); - thread::spawn(move || { - let _ = serve(stream, r, s, b); - }); - } - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - thread::sleep(Duration::from_millis(10)) - } - Err(_) => break, - } - } - }); - Ok(Self { - port, - receipts, - release, - stop, - worker: Some(worker), - }) - } - pub fn url(&self, path: &str) -> String { - format!("http://127.0.0.1:{}{path}", self.port) - } -} -impl Drop for Backend { - fn drop(&mut self) { - release(&self.release); - self.stop.store(true, Ordering::Relaxed); - if let Some(worker) = self.worker.take() { - let _ = worker.join(); - } - } -} -fn serve( - mut stream: TcpStream, - receipts: Arc>>, - signal: Signal, - barrier: Arc<(Mutex, Condvar)>, -) -> Result<()> { - stream.set_nonblocking(false)?; - stream.set_read_timeout(Some(Duration::from_secs(10)))?; - let mut reader = BufReader::new(stream.try_clone()?); - let mut line = String::new(); - reader.read_line(&mut line)?; - let path = line - .split_whitespace() - .nth(1) - .ok_or("missing request path")? - .to_owned(); - loop { - line.clear(); - reader.read_line(&mut line)?; - if line == "\r\n" || line.is_empty() { - break; - } - } - receipts.lock().unwrap().push(path.clone()); - if path.starts_with("/barrier") { - let mut count = barrier.0.lock().unwrap(); - *count += 1; - barrier.1.notify_all(); - let (count, _) = barrier - .1 - .wait_timeout_while(count, Duration::from_secs(8), |n| *n < 2) - .map_err(|_| "barrier poisoned")?; - if *count < 2 { - stream.write_all(b"HTTP/1.1 504 Gateway Timeout\r\nContent-Length: 0\r\n\r\n")?; - return Ok(()); - } - } - if path.starts_with("/chunks") { - stream.write_all(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n5\r\nfirst\r\n")?; - stream.flush()?; - let (ready, _) = signal - .1 - .wait_timeout_while(signal.0.lock().unwrap(), Duration::from_secs(8), |v| !*v) - .map_err(|_| "release poisoned")?; - if *ready { - if path.contains("large=1") { - let body = vec![b'z'; 256 * 1024]; - write!(stream, "{:x}\r\n", body.len())?; - stream.write_all(&body)?; - stream.write_all(b"\r\n0\r\n\r\n")?; - } else { - stream.write_all(b"6\r\nsecond\r\n0\r\n\r\n")?; - } - } - } else if path.starts_with("/abort") { - stream.write_all( - b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\nConnection: close\r\n\r\npartial", - )?; - } else { - stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")?; - } - Ok(()) -} -pub fn port() -> Result { - Ok(TcpListener::bind(("127.0.0.1", 0))?.local_addr()?.port()) -} - -pub fn malformed_request(port: u16, token: &str) -> Result { - let mut stream = TcpStream::connect(("127.0.0.1", port))?; - stream.set_read_timeout(Some(Duration::from_secs(5)))?; - write!( - stream, - "POST /probe/malformed HTTP/1.1\r\nHost: localhost\r\nx-request-token: {token}\r\nContent-Length: invalid\r\nConnection: close\r\n\r\n" - )?; - let mut line = String::new(); - BufReader::new(stream).read_line(&mut line)?; - Ok(line.trim().to_owned()) -} -pub fn request( - port: u16, - path: &str, - token: &str, - backend: Option<&str>, - signal: Option<&Signal>, -) -> Result { - let start = Instant::now(); - let mut stream = TcpStream::connect(("127.0.0.1", port))?; - stream.set_read_timeout(Some(Duration::from_secs(12)))?; - stream.set_write_timeout(Some(Duration::from_secs(12)))?; - let probe = path.starts_with("/probe/"); - let body = if probe { token } else { "" }; - write!( - stream, - "{} {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nx-request-token: {token}\r\nContent-Length: {}\r\nConnection: close\r\n", - if probe { "POST" } else { "GET" }, - body.len() - )?; - if let Some(url) = backend { - write!(stream, "x-fixture-backend: {url}\r\n")?; - } - write!(stream, "\r\n{body}")?; - stream.flush()?; - let mut reader = BufReader::new(stream); - let mut line = String::new(); - reader.read_line(&mut line)?; - let status: u16 = line - .split_whitespace() - .nth(1) - .ok_or("missing HTTP response status")? - .parse()?; - let mut headers = Vec::new(); - let mut length = None; - let mut chunked = false; - loop { - line.clear(); - require(reader.read_line(&mut line)? > 0, "truncated HTTP headers")?; - if line == "\r\n" { - break; - } - let (name, value) = line - .trim_end() - .split_once(':') - .ok_or("malformed HTTP header")?; - let value = value.trim(); - if name.eq_ignore_ascii_case("content-length") { - length = Some(value.parse::()?); - } - if name.eq_ignore_ascii_case("transfer-encoding") { - chunked = value.to_ascii_lowercase().contains("chunked"); - } - headers.push(json!([name, value])); - } - let header_ns = start.elapsed().as_nanos() as u64; - let mut first_byte_ns = None; - let mut bytes = Vec::new(); - let outcome = (|| -> Result<()> { - let mut read_part = |reader: &mut BufReader, count: usize| -> Result<()> { - if count == 0 { - return Ok(()); - } - let mut first = [0]; - reader.read_exact(&mut first)?; - bytes.push(first[0]); - if first_byte_ns.is_none() { - first_byte_ns = Some(start.elapsed().as_nanos() as u64); - if let Some(signal) = signal { - release(signal); - } - } - let mut remaining = count - 1; - let mut buffer = [0u8; 8192]; - while remaining > 0 { - let size = remaining.min(buffer.len()); - let read = reader.read(&mut buffer[..size])?; - require(read > 0, "truncated HTTP body")?; - bytes.extend_from_slice(&buffer[..read]); - remaining -= read; - } - Ok(()) - }; - if chunked { - loop { - line.clear(); - require(reader.read_line(&mut line)? > 0, "truncated chunk header")?; - let count = usize::from_str_radix(line.trim().split(';').next().unwrap_or(""), 16)?; - if count == 0 { - loop { - line.clear(); - require(reader.read_line(&mut line)? > 0, "truncated chunk trailer")?; - if line == "\r\n" { - break; - } - } - break; - } - read_part(&mut reader, count)?; - let mut crlf = [0; 2]; - reader.read_exact(&mut crlf)?; - require(crlf == *b"\r\n", "invalid chunk terminator")?; - } - } else if let Some(length) = length { - read_part(&mut reader, length)?; - } else { - let mut first = [0]; - if reader.read(&mut first)? != 0 { - bytes.push(first[0]); - first_byte_ns = Some(start.elapsed().as_nanos() as u64); - if let Some(signal) = signal { - release(signal); - } - reader.read_to_end(&mut bytes)?; - } - } - Ok(()) - })(); - if let Err(error) = outcome { - return Err(Box::new(InterruptedResponse { - status, - body: bytes, - reason: error.to_string(), - })); - } - let event = json!({"event":"client_completed","token":token,"status":status,"headers":headers,"body":String::from_utf8(bytes)?,"header_ns":header_ns,"first_byte_ns":first_byte_ns.unwrap_or(header_ns),"complete_ns":start.elapsed().as_nanos() as u64}); - CLIENT_EVENTS.lock().unwrap().push(event.clone()); - Ok(event) -} -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn nonblocking_connection_waits_for_request_bytes() { - let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); - let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap(); - client - .set_read_timeout(Some(Duration::from_secs(2))) - .unwrap(); - let (server, _) = listener.accept().unwrap(); - server.set_nonblocking(true).unwrap(); - let worker = thread::spawn(move || { - serve( - server, - Arc::new(Mutex::new(Vec::new())), - Arc::new((Mutex::new(false), Condvar::new())), - Arc::new((Mutex::new(0), Condvar::new())), - ) - }); - thread::sleep(Duration::from_millis(100)); - client - .write_all(b"GET /ok HTTP/1.1\r\nHost: localhost\r\n\r\n") - .unwrap(); - let mut reply = String::new(); - client.read_to_string(&mut reply).unwrap(); - worker.join().unwrap().unwrap(); - assert!(reply.starts_with("HTTP/1.1 200")); - } - #[test] - fn progressive_body_and_abort() { - let backend = Backend::start().unwrap(); - let result = request(backend.port, "/chunks", "x", None, Some(&backend.release)).unwrap(); - assert_eq!(result["body"], "firstsecond"); - let failure = request(backend.port, "/abort", "x", None, None).unwrap_err(); - let partial = failure - .downcast_ref::() - .expect("response started"); - assert_eq!(partial.status, 200); - assert_eq!(partial.body, b"partial"); - } - - #[test] - fn connection_failure_is_not_an_interrupted_response() { - let unused = port().unwrap(); - let failure = request(unused, "/abort", "x", None, None).unwrap_err(); - assert!(failure.downcast_ref::().is_none()); - } -} diff --git a/tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs b/tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs deleted file mode 100644 index 53d1792e2..000000000 --- a/tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs +++ /dev/null @@ -1,1136 +0,0 @@ -use crate::{evidence::*, net::Backend, *}; -use std::collections::HashMap; -fn cargo_build() -> Command { - let mut command = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())); - command - .current_dir(root()) - .env("CARGO_TARGET_DIR", root().join("target")); - command -} - -fn built_executable(output: &[u8], name: &str) -> Result { - String::from_utf8_lossy(output) - .lines() - .filter_map(|line| serde_json::from_str::(line).ok()) - .find_map(|record| { - (record["reason"] == "compiler-artifact" && record["target"]["name"] == name) - .then(|| record["executable"].as_str().map(PathBuf::from)) - .flatten() - }) - .ok_or_else(|| format!("Cargo did not report an executable for {name}").into()) -} -/// Copies a built artifact (file or directory) into the run directory, so a -/// concurrent build into the shared target cannot swap it under a running -/// server; the copy is what gets served and fingerprinted. -fn stage(from: &Path, run: &Path) -> Result { - let to = run - .join("artifacts") - .join(from.file_name().ok_or("artifact has no file name")?); - copy_tree(from, &to)?; - Ok(to) -} -fn copy_tree(from: &Path, to: &Path) -> Result<()> { - if from.is_dir() { - fs::create_dir_all(to)?; - for entry in fs::read_dir(from)? { - let entry = entry?; - copy_tree(&entry.path(), &to.join(entry.file_name()))?; - } - } else { - fs::create_dir_all(to.parent().ok_or("artifact has no parent")?)?; - fs::copy(from, to)?; - } - Ok(()) -} -/// Teardown shared by every runtime run. The runtime lives inside `outcome`'s -/// computation, so it has stopped by now: record client completions and guest -/// logs, then a failure carrying `fields` and `failure` context. -fn settle( - records: &mut Records, - log: &Path, - fields: Value, - failure: Value, - outcome: Result<()>, -) -> Result<()> { - records.collect_clients(fields.clone())?; - for event in logs(log) { - records.push(annotate(event, fields.clone()))?; - } - if let Err(error) = &outcome { - records.push(annotate( - annotate(json!({"status":"fail","error":error.to_string()}), fields), - failure, - ))?; - } - outcome -} -fn wrangler_runtime_config(package: &Path, main: &Path) -> Result { - let config = fs::read_to_string(package.join("wrangler.toml"))?; - let mut skip = false; - let mut lines = Vec::new(); - for line in config.lines() { - if line.trim().starts_with('[') { - skip = line.trim() == "[build]"; - } - if skip { - continue; - } - lines.push(if line.starts_with("main =") { - format!("main = {main:?}") - } else { - line.to_owned() - }); - } - Ok(lines.join("\n") + "\n") -} - -fn wasm(run: &Path, variant: &str) -> PathBuf { - run.join("artifacts").join(wasm_name(variant)) -} -fn wasm_name(variant: &str) -> String { - let name = match variant { - "a" => "single-request", - "b" => "rebuild-per-request", - "c" => "retained-app", - "custom-a" => "custom-single-request", - "custom-b" => "custom-rebuild-per-request", - "custom-c" => "custom-retained-app", - other => other, - }; - format!("fixture-fastly-{name}.wasm") -} -fn stage_fastly_artifacts(run: &Path) -> Result<()> { - let release = root().join("target/wasm32-wasip1/release"); - for variant in [ - "a", - "b", - "c", - "custom-a", - "custom-b", - "custom-c", - "logger-negative", - "limit-requests", - "limit-lifetime", - "limit-memory", - "faults", - ] { - stage(&release.join(wasm_name(variant)), run)?; - } - Ok(()) -} -fn fastly_command(exe: &Path, config: &Path, run: &Path, variant: &str, port: u16) -> Command { - let mut cmd = Command::new(exe); - cmd.args(["serve", "--addr", &format!("127.0.0.1:{port}"), "--config"]) - .arg(config) - .arg(wasm(run, variant)); - cmd -} - -fn custom_initialization_contract( - exe: &Path, - config: &Path, - run: &Path, - records: &mut Records, -) -> Result<()> { - let log = run.join("custom-initialization.log"); - let outcome = (|| -> Result<()> { - let port = net::port()?; - let _process = Process::start( - &mut fastly_command(exe, config, run, "custom-c", port), - &log, - port, - )?; - let mut instances = Vec::new(); - for (path, status, attempts, initialized) in [ - ("/health", 200, 0, false), - ("/initialization-error", 503, 1, false), - ("/health", 200, 1, false), - ("/probe/recovered", 200, 2, true), - ("/probe/retained", 200, 2, true), - ] { - let reply = req(port, path)?; - require( - reply["status"] == status, - "custom initialization status mismatch", - )?; - let value: Value = serde_json::from_str(reply["body"].as_str().ok_or("missing body")?)?; - instances.push(value["instance"].clone()); - let same_instance = instances.iter().all(|id| id == &instances[0]); - if same_instance { - require( - value["ordinal"] == instances.len(), - "recovery ordinal mismatch", - )?; - if initialized { - require(value["builds"] == 1, "recovered application was rebuilt")?; - } else { - require( - value["retained"] == false, - "health or failure retained an app", - )?; - } - require( - reply["headers"].as_array().unwrap().iter().any(|h| { - h[0].as_str() - .is_some_and(|name| name.eq_ignore_ascii_case("x-fixture-attempts")) - && h[1].as_str().and_then(|v| v.parse::().ok()) == Some(attempts) - }), - "lazy build attempt count mismatch", - )?; - } - records.push(annotate( - reply, - json!({"variant":"custom-initialization", "guest":value}), - ))?; - } - records.push(json!({"assertion":"custom_lazy_initialization_recovery", "status":if instances.iter().all(|id| id == &instances[0]) {"pass"} else {"unverified"}, "reason":"recovery requires all five callbacks in the same guest"})) - })(); - settle( - records, - &log, - json!({"variant":"custom-initialization","repetition":0}), - json!({"adapter":"fastly"}), - outcome, - ) -} - -pub fn fastly(args: &Args, run: &Path, records: &mut Records) -> Result<()> { - let Some(exe) = executable("VICEROY_BIN", "viceroy") else { - return records.push(json!({"status":"unsupported","reason":"Viceroy unavailable"})); - }; - let version = Command::new(&exe).arg("--version").output()?; - require(version.status.success(), "Viceroy version command failed")?; - let version = String::from_utf8(version.stdout)?.trim().to_owned(); - let pins = fs::read_to_string(root().join("../../../.tool-versions"))?; - let pin = pins - .lines() - .find_map(|line| line.strip_prefix("viceroy ")) - .map(str::trim) - .ok_or("missing Viceroy pin")?; - records.push(json!({"event":"runtime","adapter":"fastly","executable":exe,"version":version,"expected":pin}))?; - if version.split_whitespace().nth(1) != Some(pin) { - // Keep going: an unpinned Viceroy still gets signal, and a failure still exits 1. - records.push(json!({"status":"unverified","adapter":"fastly","reason":format!("Viceroy version {version} does not match pinned {pin}")}))?; - } - checked( - cargo_build() - .args([ - "build", - "--locked", - "--release", - "-p", - "fixture-fastly", - "--bins", - "--target", - "wasm32-wasip1", - ]) - .env("FIXTURE_BUILD_ROUNDS", args.construction_rounds.to_string()) - .env("FIXTURE_MAX_REQUESTS", args.max_requests.to_string()), - )?; - stage_fastly_artifacts(run)?; - let config_snapshot = run.join("fastly.toml"); - fs::copy( - root().join("crates/fixture-fastly/fastly.toml"), - &config_snapshot, - )?; - if args.suite == "smoke" { - custom_initialization_contract(&exe, &config_snapshot, run, records)?; - } - let mut variants = vec!["a", "b", "c", "custom-a", "custom-b", "custom-c"]; - if args.suite == "smoke" { - variants.extend(["limit-requests", "limit-lifetime", "limit-memory"]); - } - let mut rng = args.seed; - for repetition in 0..if args.suite == "benchmark" { - args.repetitions - } else { - 1 - } { - // A recorded deterministic Fisher-Yates order; independent of language RNG versions. - for i in (1..variants.len()).rev() { - rng = rng - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - variants.swap(i, (rng as usize) % (i + 1)); - } - records - .push(json!({"event":"variant_order","repetition":repetition,"variants":variants}))?; - for variant in &variants { - fastly_variant( - args, - &exe, - &config_snapshot, - run, - variant, - repetition, - records, - )?; - } - } - if args.suite == "smoke" { - logging(&exe, run, records)?; - faults(&exe, run, records)?; - } - if records - .values - .iter() - .any(|e| e["event"] == "heap_preflight" && e["heap_mib"].is_null()) - { - records.push(json!({"status":"unsupported","reason":"heap snapshot unavailable; memory limit unverified"}))?; - } - Ok(()) -} -fn fastly_variant( - args: &Args, - exe: &Path, - config: &Path, - run: &Path, - variant: &str, - repetition: usize, - records: &mut Records, -) -> Result<()> { - records.push(json!({"event":"build","variant":variant,"repetition":repetition,"target":"wasm32-wasip1","artifact":artifact(&wasm(run, variant))?,"config":artifact(config)?}))?; - let log = run.join(format!("{variant}-{repetition}.log")); - let outcome = (|| -> Result<()> { - let backend = Backend::start()?; - let port = net::port()?; - let _process = Process::start( - &mut fastly_command(exe, config, run, variant, port), - &log, - port, - )?; - let seen = fastly_probes(args, port, variant, repetition, records)?; - if ["c", "custom-c"].contains(&variant) { - thread::sleep(Duration::from_secs(2)); - let reply = req(port, "/probe/after-idle")?; - require( - !seen.contains_key( - guest(&reply)?["instance"] - .as_str() - .ok_or("missing instance")?, - ), - "idle guest was reused", - )?; - records.push(annotate( - reply, - json!({"variant":variant,"repetition":repetition,"assertion":"idle_fresh_initialization"}), - ))?; - } - let bindings = req(port, "/bindings")?; - require( - guest(&bindings)? == json!({"config":true,"kv":true,"secrets":true,"unknown":false}), - "binding mismatch", - )?; - records.push(annotate( - bindings, - json!({"variant":variant,"repetition":repetition,"assertion":"binding_reads"}), - ))?; - require( - cookie_values(&req(port, "/cookies")?["headers"]) - == vec!["first=1; Path=/", "second=2; Path=/"], - "duplicate cookies lost", - )?; - require( - req(port, "/rendered-error")?["status"] == 400, - "rendered error mismatch", - )?; - require( - req(port, "/probe/after")?["status"] == 200, - "request after error failed", - )?; - if variant.starts_with("custom-") { - custom_stream_checks(port, &backend, &log, variant, repetition, records)?; - } - if args.suite == "smoke" { - smoke_origin_checks(port, &backend, &log, variant, repetition, records)?; - } - if variant.starts_with("limit-") { - await_evidence(|| validate_limit_summaries(&logs(&log)))?; - } - records.push(json!({"status":"pass","variant":variant,"repetition":repetition,"assertions":["initialization","isolation","cookies","rendered_error"]})) - })(); - settle( - records, - &log, - json!({"variant":variant,"repetition":repetition}), - json!({"adapter":"fastly"}), - outcome, - ) -} -fn fastly_probes( - args: &Args, - port: u16, - variant: &str, - repetition: usize, - records: &mut Records, -) -> Result> { - let mut seen: HashMap = HashMap::new(); - let mut reused = false; - for _ in 0..if args.suite == "benchmark" { - args.requests - } else { - 5 - } { - let token = token(); - let reply = net::request(port, &format!("/probe/{token}"), &token, None, None)?; - let value = guest(&reply)?; - require( - value["token"] == token && value["path"] == token && value["body"] == token, - format!("request isolation: {value}"), - )?; - if let Some(previous) = seen.get(value["instance"].as_str().ok_or("missing instance")?) { - reused = true; - require( - value["ordinal"].as_u64() > previous["ordinal"].as_u64() - && value["correlation"] != previous["correlation"], - "reuse ordinal/correlation mismatch", - )?; - } - require( - value["mutated"] == variant.starts_with("custom-"), - "raw request mutation mismatch", - )?; - if variant.starts_with("custom-") { - require( - reply["headers"].as_array().unwrap().iter().any(|h| { - h[0].as_str() - .is_some_and(|name| name.eq_ignore_ascii_case("x-fixture-finalized")) - && h[1] == token - }), - "request-specific response extension was lost or leaked", - )?; - } - match variant { - "c" | "custom-c" => require( - value["ordinal"] - .as_u64() - .is_some_and(|n| n <= args.max_requests as u64) - && value["builds"] == 1, - "retained app initialization mismatch", - )?, - "b" | "custom-b" => require( - value["builds"] == value["ordinal"], - "per-request initialization mismatch", - )?, - _ => require( - value["ordinal"] == 1 && value["builds"] == 1, - "single-request initialization mismatch", - )?, - } - validate_shared_state(&value, ["c", "custom-c"].contains(&variant))?; - seen.insert(value["instance"].as_str().unwrap().into(), value.clone()); - records.push(annotate( - reply, - json!({"variant":variant,"repetition":repetition,"guest":value,"workload":"probe"}), - ))?; - } - if ["b", "c", "custom-b", "custom-c"].contains(&variant) && !reused { - records.push(json!({"status":"unverified","variant":variant,"repetition":repetition,"reason":"no actual guest reuse observed"}))?; - } - Ok(seen) -} -fn custom_stream_checks( - port: u16, - backend: &Backend, - log: &Path, - variant: &str, - repetition: usize, - records: &mut Records, -) -> Result<()> { - let token = token(); - let stream = net::request( - port, - "/stream", - &token, - Some(&backend.url(&format!("/chunks?token={token}"))), - Some(&backend.release), - )?; - require( - stream["body"] == "firstsecond", - "progressive stream mismatch", - )?; - require( - stream["headers"].as_array().unwrap().iter().any(|h| { - h[0].as_str() - .is_some_and(|s| s.eq_ignore_ascii_case("x-fixture-finalized")) - && h[1] == token - }), - "response not finalized", - )?; - await_evidence(|| validate_post_send(&token, &backend.receipts.lock().unwrap(), &logs(log)))?; - records.push(annotate( - stream, - json!({"variant":variant,"repetition":repetition,"assertion":"progressive_stream_and_post_send"}), - ))?; - *backend.release.0.lock().unwrap() = false; - let large_token = crate::token(); - let large = net::request( - port, - "/stream", - &large_token, - Some(&backend.url("/chunks?large=1")), - Some(&backend.release), - )?; - let large_body = large["body"].as_str().ok_or("missing large body")?; - require( - large_body.starts_with("first") && large_body.len() == 5 + 256 * 1024, - "large stream truncated", - )?; - records.push(annotate( - large, - json!({"variant":variant,"repetition":repetition,"assertion":"large_progressive_stream"}), - ))?; - let failure_token = crate::token(); - let failure = net::request( - port, - "/stream-error", - &failure_token, - Some(&backend.url(&format!("/abort?token={failure_token}"))), - None, - ); - let failure = match failure { - Ok(reply) => { - return Err(format!("interrupted stream unexpectedly completed: {reply}").into()); - } - Err(error) => error, - }; - let partial = failure - .downcast_ref::() - .ok_or("stream failure occurred before response body: not post-commit evidence")?; - require( - partial.status == 200 && partial.body == b"partial", - "missing partial committed response", - )?; - await_evidence(|| { - require( - logs(log) - .iter() - .any(|e| e["event"] == "post_commit_error" && e["token"] == failure_token), - "missing correlated post-commit error", - ) - })?; - records.push(json!({"event":"expected_stream_error","variant":variant,"repetition":repetition,"token":failure_token,"status_code":partial.status,"partial_body":String::from_utf8_lossy(&partial.body),"source":"controlled_backend_abort"})) -} -fn smoke_origin_checks( - port: u16, - backend: &Backend, - log: &Path, - variant: &str, - repetition: usize, - records: &mut Records, -) -> Result<()> { - for repeat in 0..3 { - let reply = net::request( - port, - "/origin/repeated", - &token(), - Some(&backend.url("/ok")), - None, - )?; - require( - reply["status"] == 200 && reply["body"] == "ok", - "repeated origin failed", - )?; - records.push(annotate(reply, json!({"variant":variant,"repetition":repetition,"origin_repeat":repeat,"assertion":"bounded_repeated_origin"})))?; - } - for origin in 0..3 { - let distinct = Backend::start()?; - let reply = net::request( - port, - &format!("/origin/{origin}"), - &token(), - Some(&distinct.url("/ok")), - None, - )?; - require( - reply["status"] == 200 && reply["body"] == "ok", - "distinct origin failed", - )?; - records.push(annotate( - reply, - json!({"variant":variant,"repetition":repetition,"assertion":"bounded_distinct_origin"}), - ))?; - } - if ["custom-b", "custom-c"].contains(&variant) { - let token = token(); - if let Ok(failed) = net::request(port, "/panic", &token, None, None) { - require( - failed["status"].as_u64().is_some_and(|s| s >= 500), - "panic did not fail", - )?; - } - let fresh = req(port, "/probe/fresh")?; - let value = guest(&fresh)?; - let mut failed_guest = Value::Null; - await_evidence(|| { - failed_guest = logs(log) - .iter() - .find(|e| e["event"] == "request_start" && e["token"] == token) - .map(|e| e["instance"].clone()) - .ok_or("missing panic attempt")?; - Ok(()) - })?; - require(value["instance"] != failed_guest, "terminated guest reused")?; - records.push(annotate(fresh,json!({"variant":variant,"repetition":repetition,"assertion":"terminated_guest_not_reused","failed_guest":failed_guest})))?; - } - Ok(()) -} -fn logging(exe: &Path, run: &Path, records: &mut Records) -> Result<()> { - let base = fs::read_to_string(root().join("crates/fixture-fastly/fastly.toml"))?; - let config = run.join("logging.toml"); - fs::write(&config, base)?; - for variant in ["b", "c", "custom-c", "logger-negative"] { - let log = run.join(format!("logging-{variant}.log")); - let outcome = (|| -> Result<()> { - let port = net::port()?; - let process = Process::start( - &mut fastly_command(exe, &config, run, variant, port), - &log, - port, - )?; - let mut replies = Vec::new(); - for _ in 0..if variant == "logger-negative" { 2 } else { 3 } { - replies.push(req(port, "/probe/log")?); - } - drop(process); - records.push(json!({"event":"logging_check","variant":variant,"statuses":replies.iter().map(|r|r["status"].clone()).collect::>(),"log_file":log.file_name().map(|n| n.to_string_lossy())}))?; - if variant == "logger-negative" { - let starts = logs(&log) - .into_iter() - .filter(|e| e["event"] == "request_start") - .collect::>(); - let status = validate_negative_logging(&replies, &starts)?; - records.push(json!({"status":status,"assertion":"repeated_logger_installation","variant":variant}))?; - } else { - let guests = replies.iter().map(guest).collect::>>()?; - let text = fs::read_to_string(&log)?; - let delivered = text - .lines() - .filter(|l| l.starts_with("fixture-logs :: ")) - .filter_map(|l| { - l.rsplit_once("fixture request ") - .map(|(_, v)| v.trim().to_owned()) - }) - .collect::>(); - let status = validate_logging(&guests, &delivered)?; - records.push(json!({"status":status,"assertion":"named_endpoint_receipt","variant":variant,"received":delivered.len()}))?; - } - Ok(()) - })(); - settle( - records, - &log, - json!({"variant":format!("logging-{variant}"),"repetition":0}), - json!({"adapter":"fastly"}), - outcome, - )?; - } - Ok(()) -} -pub fn provider(name: &str, args: &Args, run: &Path, records: &mut Records) -> Result<()> { - let package = root().join("crates").join(format!("fixture-{name}")); - let Some((exe, staged)) = provider_build(name, &package, run, records)? else { - return Ok(()); - }; - let modes = if name == "axum" { - vec!["retained"] - } else { - vec!["per-request", "retained"] - }; - for mode in modes { - let port = net::port()?; - let mut command = provider_command(name, mode, &exe, &staged, &package, run, port)?; - let log = run.join(format!("{name}-{mode}.log")); - let mut failure = json!({"phase":"startup"}); - let outcome = Process::start(&mut command, &log, port) - .and_then(|_process| provider_checks(name, mode, port, args, records, &mut failure)); - settle( - records, - &log, - json!({"adapter":name,"mode":mode}), - failure, - outcome, - )?; - } - Ok(()) -} -/// Builds the provider fixture and stages its artifact in the run directory. -/// Returns the runtime executable and the staged artifact, or `None` when a -/// required tool is unavailable. -fn provider_build( - name: &str, - package: &Path, - run: &Path, - records: &mut Records, -) -> Result> { - if name == "axum" { - let output = cargo_build() - .args([ - "build", - "--locked", - "-p", - "fixture-axum", - "--message-format=json-render-diagnostics", - ]) - .stderr(Stdio::inherit()) - .output()?; - require(output.status.success(), "Axum fixture build failed")?; - let staged = stage(&built_executable(&output.stdout, "fixture-axum")?, run)?; - records.push(json!({"event":"build","adapter":name,"artifact":artifact(&staged)?}))?; - return Ok(Some((staged.clone(), staged))); - } - let (variable, tool) = if name == "cloudflare" { - ("WRANGLER_BIN", "wrangler") - } else { - ("SPIN_BIN", "spin") - }; - let Some(exe) = executable(variable, tool) else { - records.push( - json!({"status":"unsupported","adapter":name,"reason":format!("{tool} unavailable")}), - )?; - return Ok(None); - }; - records.push(json!({"event":"runtime","adapter":name,"executable":exe,"version":String::from_utf8_lossy(&Command::new(&exe).arg("--version").output()?.stdout).trim()}))?; - let (staged, wasm) = if name == "spin" { - let help = Command::new(&exe).args(["up", "--help"]).output()?; - fs::write(run.join("spin-up-help.txt"), &help.stdout)?; - records.push(json!({"event":"runtime_controls","adapter":"spin","selected":"host defaults","source":"spin-up-help.txt"}))?; - checked(cargo_build().args([ - "build", - "--locked", - "--release", - "-p", - "fixture-spin", - "--target", - "wasm32-wasip2", - ]))?; - let staged = stage( - &root().join("target/wasm32-wasip2/release/fixture_spin.wasm"), - run, - )?; - (staged.clone(), staged) - } else { - let Some(builder) = executable("WORKER_BUILD_BIN", "worker-build") else { - records.push(json!({"status":"unsupported","reason":"worker-build unavailable"}))?; - return Ok(None); - }; - checked( - Command::new(builder) - .args(["--release", ".", "--", "--locked"]) - .env("CARGO_TARGET_DIR", root().join("target")) - .current_dir(package), - )?; - let staged = stage(&package.join("build"), run)?; - // Only the explicit builder runs before artifact fingerprinting. - fs::write( - run.join("wrangler.toml"), - wrangler_runtime_config(package, &staged.join("worker/shim.mjs"))?, - )?; - let wasm = staged.join("index_bg.wasm"); - (staged, wasm) - }; - records.push(json!({"event":"build","adapter":name,"artifact":artifact(&wasm)?}))?; - Ok(Some((exe, staged))) -} -fn provider_command( - name: &str, - mode: &str, - exe: &Path, - staged: &Path, - package: &Path, - run: &Path, - port: u16, -) -> Result { - let mut command = Command::new(exe); - command - .env("CARGO_TARGET_DIR", root().join("target")) - .current_dir(package) - .env("EDGEZERO__ADAPTER__HOST", "127.0.0.1") - .env("EDGEZERO__ADAPTER__PORT", port.to_string()); - if name == "axum" { - let state = run.join("axum-state"); - fs::create_dir_all(state.join(".edgezero"))?; - fs::write( - state.join(".edgezero/local-config-fixture_config.json"), - "{\"marker\":\"local-fixture\"}", - )?; - command - .current_dir(state) - .env("fixture_marker", "fixture-only-value"); - } else if name == "cloudflare" { - let state = run.join(mode).join("worker-state"); - checked( - Command::new(exe) - .args([ - "kv", - "key", - "put", - "marker", - "local-fixture", - "--binding", - "fixture_config", - "--local", - "--persist-to", - ]) - .arg(&state) - .current_dir(package), - )?; - command - .arg("--config") - .arg(run.join("wrangler.toml")) - .args([ - "dev", - "--local", - "--ip", - "127.0.0.1", - "--port", - &port.to_string(), - "--var", - &format!("FIXTURE_MODE:{mode}"), - "--persist-to", - ]) - .arg(state); - } else { - let manifest = fs::read_to_string(package.join("spin.toml"))? - .replace( - "../../target/wasm32-wasip2/release/fixture_spin.wasm", - &staged.display().to_string(), - ) - .replace( - "FIXTURE_MODE = \"retained\"", - &format!("FIXTURE_MODE = \"{mode}\""), - ); - let config = run.join(format!("spin-{mode}.toml")); - fs::write(&config, manifest)?; - command - .args(["up", "--listen", &format!("127.0.0.1:{port}"), "--from"]) - .arg(config) - .arg("--runtime-config-file") - .arg(package.join("runtime-config.toml")); - } - Ok(command) -} -/// Runs the provider assertions against a listening runtime, keeping -/// `failure` pointed at the current phase so a failure record names its step. -fn provider_checks( - name: &str, - mode: &str, - port: u16, - args: &Args, - records: &mut Records, - failure: &mut Value, -) -> Result<()> { - let mut seen: HashMap = HashMap::new(); - let mut reused = false; - let repetitions = if args.suite == "benchmark" { - args.repetitions - } else { - 1 - }; - let requests = if args.suite == "benchmark" { - args.requests - } else { - 5 - }; - for repetition in 0..repetitions { - *failure = json!({"phase":"probe","repetition":repetition}); - for _ in 0..requests { - let token = token(); - let reply = net::request(port, &format!("/probe/{token}"), &token, None, None)?; - let value = guest(&reply)?; - require( - value["token"] == token && value["path"] == token && value["body"] == token, - "probe request isolation mismatch", - )?; - if mode == "retained" { - require(value["builds"] == 1, "retained initialization mismatch")?; - } - validate_shared_state(&value, mode == "retained")?; - let instance = value["instance"].as_str().ok_or("missing instance")?; - if let Some(previous) = seen.get(instance) { - reused = true; - require( - value["ordinal"].as_u64() > previous["ordinal"].as_u64(), - "ordinal did not increase", - )?; - if mode == "per-request" { - require( - value["builds"].as_u64() > previous["builds"].as_u64(), - "per-request app was not rebuilt", - )?; - } - } - seen.insert(instance.into(), value.clone()); - records.push(annotate( - reply, - json!({"guest":value,"adapter":name,"mode":mode,"repetition":repetition,"workload":"probe"}), - ))?; - } - } - if !reused { - records.push(json!({"status":"unverified","adapter":name,"mode":mode,"reason":"no same-guest sequential reuse"}))?; - } - if mode == "retained" && name != "axum" { - *failure = json!({"phase":"binding_failure"}); - let failed = req(port, "/binding-failure")?; - require( - failed["status"] == 503, - "injected required binding did not fail", - )?; - let failed_guest: Value = serde_json::from_str(failed["body"].as_str().unwrap())?; - require( - failed_guest["error"] - .as_str() - .is_some_and(|error| error.contains("missing_fixture_kv")), - "binding failure did not identify the injected missing KV binding", - )?; - let recovered = guest(&req(port, "/probe/after-binding-failure")?)?; - require( - recovered["builds"] == 1, - "app rebuilt after binding failure", - )?; - records.push(json!({"adapter":name,"mode":mode,"status":if failed_guest["instance"] == recovered["instance"] {"pass"} else {"unverified"},"assertion":"same_guest_binding_recovery","source":"injected_required_binding"}))?; - } - *failure = json!({"phase":"bindings"}); - let bindings = req(port, "/bindings")?; - require( - guest(&bindings)? == json!({"config":true,"kv":true,"secrets":true,"unknown":false}), - "binding mismatch", - )?; - records.push(annotate( - bindings, - json!({"adapter":name,"mode":mode,"assertion":"binding_reads"}), - ))?; - *failure = json!({"phase":"cookies"}); - let cookies = req(port, "/cookies")?; - if cookie_values(&cookies["headers"]) != vec!["first=1; Path=/", "second=2; Path=/"] { - records.push(annotate(cookies,json!({"status":"fail","adapter":name,"mode":mode,"phase":"cookies","reason":"duplicate Set-Cookie values were not preserved"})))?; - } - *failure = json!({"phase":"rendered_error"}); - require( - req(port, "/rendered-error")?["status"] == 400, - "rendered error mismatch", - )?; - if name != "axum" { - *failure = json!({"phase":"alternate_app"}); - require( - req(port, "/other")?["body"] == "other-app", - "alternate app mismatch", - )?; - } - *failure = json!({"phase":"overlap"}); - overlap_check(name, mode, port, records) -} -fn overlap_check(name: &str, mode: &str, port: u16, records: &mut Records) -> Result<()> { - let mut observed_overlap = false; - for attempt in 0..3 { - let backend = Backend::start()?; - let tokens = [token(), token()]; - let replies = thread::scope(|scope| { - let jobs = tokens - .iter() - .map(|token| { - let url = backend.url("/barrier"); - scope.spawn(move || { - net::request(port, &format!("/overlap/{token}"), token, Some(&url), None) - }) - }) - .collect::>(); - jobs.into_iter() - .map(|job| { - job.join() - .map_err(|_| "overlap request thread panicked".into()) - .and_then(|v| v) - }) - .collect::>>() - })?; - let values = replies.iter().map(guest).collect::>>()?; - for (value, token) in values.iter().zip(&tokens) { - require( - value["token"] == *token - && value["before"]["token"] == *token - && value["path"] == *token - && value["before"]["path"] == *token, - "overlap request-local values changed", - )?; - } - if let Err(error) = validate_overlap(&values[0], &values[1]) { - records.push(json!({"event":"overlap_observation","adapter":name,"mode":mode,"attempt":attempt,"reason":error.to_string(),"observations":values}))?; - continue; - } - observed_overlap = true; - break; - } - records.push(json!({"status":if observed_overlap {"pass"} else {"unverified"},"adapter":name,"mode":mode,"assertion":"same_guest_overlap","attempt_limit":3})) -} - -fn faults(exe: &Path, run: &Path, records: &mut Records) -> Result<()> { - let backend = Backend::start()?; - let config = run.join("faults.toml"); - fs::write( - &config, - format!( - "{}\n[local_server.backends.fault-origin]\nurl = {:?}\n", - fs::read_to_string(root().join("crates/fixture-fastly/fastly.toml"))?, - backend.url("/") - ), - )?; - for (index, path) in [ - "/constructor-panic", - "/collect-error", - "/inbound-error", - "/terminal-store-error", - "/recoverable-store-error", - ] - .iter() - .enumerate() - { - let log = run.join(format!("fault-{index}.log")); - let outcome = (|| -> Result<()> { - let port = net::port()?; - let _process = Process::start( - &mut fastly_command(exe, &config, run, "faults", port), - &log, - port, - )?; - let malformed_token = token(); - let rejected = net::malformed_request(port, &malformed_token)?; - require( - rejected.contains(" 400 "), - format!("malformed request was not rejected: {rejected}"), - )?; - require( - !logs(&log).iter().any(|e| e["token"] == malformed_token), - "malformed request reached callback unexpectedly", - )?; - records.push(json!({"status":"pass","assertion":"malformed_rejected_before_dispatch","token":malformed_token,"status_line":rejected}))?; - let failed_token = token(); - let reply = net::request(port, path, &failed_token, None, None)?; - let recoverable = *path == "/recoverable-store-error"; - require( - reply["status"] == if recoverable { 503 } else { 500 }, - format!("fault not observed: {path}: {reply}"), - )?; - let following = req(port, "/probe/recovered")?; - let value = guest(&following)?; - let mut failed_guest = Value::Null; - await_evidence(|| { - let events = logs(&log); - failed_guest = events - .iter() - .find(|e| e["event"] == "request_start" && e["token"] == failed_token) - .map(|e| e["instance"].clone()) - .ok_or("missing fault attempt")?; - let event = if *path == "/constructor-panic" { - "constructor_panic" - } else { - "adapter_error" - }; - require( - events - .iter() - .any(|e| e["event"] == event && e["instance"] == failed_guest), - "missing actual failure boundary", - ) - })?; - if recoverable { - let status = if value["instance"] == failed_guest { - "pass" - } else { - "unverified" - }; - require( - value["builds"] == 1, - "retained app rebuilt after handled store failure", - )?; - require( - guest(&req(port, "/bindings")?)? - == json!({"config":true,"kv":true,"secrets":true,"unknown":false}), - "bindings failed to recover", - )?; - records.push(json!({"status":status,"assertion":"injected_selector_failure_recovery","instance":failed_guest,"source":"custom_callback_handles_real_registry_error"}))?; - } else { - require( - value["instance"] != failed_guest, - "terminal failure guest reused", - )?; - if *path != "/constructor-panic" { - await_evidence(|| { - require( - logs(&log).iter().any(|e| { - e["event"] == "sdk_summary" - && e["instance"] == failed_guest - && e["attempted"] == 1 - }), - "missing terminal attempt summary", - ) - })?; - } - records.push(json!({"status":"pass","assertion":"terminal_fault","path":path,"failed_guest":failed_guest,"source":"controlled_fixture_input"}))?; - } - records.push(annotate( - reply, - json!({"variant":"faults","repetition":index,"assertion":path}), - ))?; - Ok(()) - })(); - settle( - records, - &log, - json!({"variant":"faults","repetition":index}), - json!({"adapter":"fastly","path":path}), - outcome, - )?; - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn cargo_artifacts_select_the_fresh_executable_in_a_configured_target() { - let output = concat!( - "{\"reason\":\"compiler-artifact\",\"target\":{\"name\":\"dependency\"},\"executable\":null}\n", - "{\"reason\":\"compiler-artifact\",\"target\":{\"name\":\"fixture-axum\"},\"executable\":\"/tmp/target/host-triple/debug/fixture-axum\"}\n", - "{\"reason\":\"build-finished\",\"success\":true}\n", - ); - assert_eq!( - built_executable(output.as_bytes(), "fixture-axum").unwrap(), - PathBuf::from("/tmp/target/host-triple/debug/fixture-axum") - ); - assert!(built_executable(b"", "fixture-axum").is_err()); - } - #[test] - fn wrangler_runtime_configuration_does_not_rebuild_fingerprinted_artifacts() { - let package = root().join("crates/fixture-cloudflare"); - let staged = Path::new("/run/artifacts/build/worker/shim.mjs"); - let config = wrangler_runtime_config(&package, staged).unwrap(); - assert!(!config.contains("[build]") && !config.contains("worker-build")); - assert!(config.contains(&format!("main = {staged:?}"))); - assert!(config.contains("binding = \"fixture_kv\"")); - } - #[test] - fn builds_share_a_fixed_target_directory() { - let command = cargo_build(); - let target = root().join("target"); - assert!( - command - .get_envs() - .any(|(key, value)| key == "CARGO_TARGET_DIR" && value == Some(target.as_os_str())) - ); - assert_eq!(command.get_current_dir(), Some(root().as_path())); - } - #[test] - fn served_artifacts_are_copies_inside_the_run_directory() { - let run = std::env::temp_dir().join(format!("fixture-harness-stage-{}", token())); - let source = run.join("source/fixture-fastly-retained-app.wasm"); - fs::create_dir_all(source.parent().unwrap()).unwrap(); - fs::write(&source, b"first").unwrap(); - let staged = stage(&source, &run).unwrap(); - assert_eq!(staged, wasm(&run, "c")); - fs::write(&source, b"second").unwrap(); - assert_eq!(fs::read(&staged).unwrap(), b"first"); - fs::remove_dir_all(&run).unwrap(); - } -} diff --git a/tests/fixtures/reusable-app/crates/fixture-spin/Cargo.toml b/tests/fixtures/reusable-app/crates/fixture-spin/Cargo.toml index 0f1976bd9..06924a1b1 100644 --- a/tests/fixtures/reusable-app/crates/fixture-spin/Cargo.toml +++ b/tests/fixtures/reusable-app/crates/fixture-spin/Cargo.toml @@ -4,13 +4,19 @@ version.workspace = true edition.workspace = true publish.workspace = true +[features] +qualification = ["fixture-core/qualification"] +progress = ["fixture-core/qualification", "dep:futures", "dep:http-body", "edgezero-adapter-spin/test-utils"] + [dependencies] serde_json.workspace = true fixture-core.workspace = true edgezero-core.workspace = true edgezero-adapter-spin.workspace = true -spin-sdk = { version = "~7.0", default-features = false, features = ["http", "key-value"] } +spin-sdk = { workspace = true, features = ["http", "key-value"] } anyhow = "1" +futures = { workspace = true, optional = true } +http-body = { version = "1", optional = true } [lib] crate-type = ["cdylib", "rlib"] diff --git a/tests/fixtures/reusable-app/crates/fixture-spin/src/lib.rs b/tests/fixtures/reusable-app/crates/fixture-spin/src/lib.rs index b3f0949b1..e005bb0c9 100644 --- a/tests/fixtures/reusable-app/crates/fixture-spin/src/lib.rs +++ b/tests/fixtures/reusable-app/crates/fixture-spin/src/lib.rs @@ -1,5 +1,19 @@ +#![cfg(target_arch = "wasm32")] +#![allow( + unsafe_code, + reason = "spin's #[http_service] macro generates the unsafe wasm export" +)] + +#[cfg(feature = "progress")] +mod progress; +use edgezero_core::MonotonicClock; use edgezero_core::app::{App, Hooks}; -use fixture_core::{FixtureApp, MissingBindingApp, OtherApp}; +#[cfg(not(feature = "qualification"))] +use fixture_core::DeclaredKvApp as FixtureApp; +use fixture_core::assembly::{ConfigurationFailureApp, InternalFailureApp, RetryApp}; +#[cfg(feature = "qualification")] +use fixture_core::qualification::QualificationApp as FixtureApp; +use fixture_core::{OtherApp, retained_app}; use spin_sdk::{ http::{IntoResponse, Request}, http_service, @@ -7,33 +21,72 @@ use spin_sdk::{ use std::sync::OnceLock; static APP: OnceLock = OnceLock::new(); static OTHER_APP: OnceLock = OnceLock::new(); +static ASSEMBLY_APP: OnceLock = OnceLock::new(); + +fn telemetry(line: &str) { + eprintln!("{line}"); +} + +async fn dispatch_selected( + req: Request, + retained: bool, + cell: &OnceLock, +) -> anyhow::Result { + if !retained { + return edgezero_adapter_spin::run_app::(req).await; + } + let clock = MonotonicClock::default(); + let start = clock.now(); + match retained_app::(cell, edgezero_adapter_spin::SPIN_PLATFORM) { + Ok(app) => edgezero_adapter_spin::dispatch_app::(app, req).await, + Err(error) => Ok(edgezero_adapter_spin::request::send_assembly_error( + req, error, start, clock, + )?), + } +} #[http_service] -async fn handle(req: Request) -> anyhow::Result { +async fn handle(req: Request) -> anyhow::Result { + #[cfg(feature = "progress")] + if req.uri().path().starts_with("/progress/") { + return progress::dispatch(req).await; + } if !FixtureApp::owns_logging() { drop(edgezero_adapter_spin::init_logger()); + } else { + fixture_core::init_fixture_logging(telemetry)?; } let retained = std::env::var("FIXTURE_MODE").as_deref() == Ok("retained"); + match req.uri().path() { + "/assembly/configuration" => { + return edgezero_adapter_spin::run_app::(req).await; + } + "/assembly/internal" => { + return edgezero_adapter_spin::run_app::(req).await; + } + "/assembly/retry" | "/assembly/retry/missing" => { + return dispatch_selected::>(req, retained, &ASSEMBLY_APP).await; + } + _ => {} + } if retained && req.uri().path() == "/binding-failure" { - // Pairs MissingBindingApp with FixtureApp's router; valid only while - // dispatch_app reads nothing but A::stores() (see MissingBindingApp). - let result = edgezero_adapter_spin::dispatch_app::( - APP.get_or_init(FixtureApp::build_app), - req, - ) - .await; + let result = + edgezero_adapter_spin::key_value_store::SpinKvStore::open("missing_fixture_kv").await; return match result { Err(error) => { let body = serde_json::json!({"instance":fixture_core::instance_id(),"source":"injected_required_binding","error":format!("{error:#}")}).to_string(); - Ok(edgezero_adapter_spin::response::from_core_response( - edgezero_core::http::response_builder() - .status(503) - .body(edgezero_core::body::Body::from(body)) - .unwrap(), - ) - .await?) + edgezero_core::http::response_builder() + .status(503) + .header("content-type", "application/json") + .body(body)? + .into_response() + .map_err(|error| { + anyhow::anyhow!("fixture response conversion failed: {error:?}") + }) } - Ok(response) => Ok(response), + Ok(_) => Err(anyhow::anyhow!( + "injected missing binding unexpectedly opened" + )), }; } if req.uri().path() == "/bindings" { @@ -41,22 +94,7 @@ async fn handle(req: Request) -> anyhow::Result { store.set("marker", b"local-fixture").await?; } if req.uri().path() == "/other" { - if !retained { - return edgezero_adapter_spin::run_app::(req).await; - } - return edgezero_adapter_spin::dispatch_app::( - OTHER_APP.get_or_init(OtherApp::build_app), - req, - ) - .await; - } - if retained { - edgezero_adapter_spin::dispatch_app::( - APP.get_or_init(FixtureApp::build_app), - req, - ) - .await - } else { - edgezero_adapter_spin::run_app::(req).await + return dispatch_selected::(req, retained, &OTHER_APP).await; } + dispatch_selected::(req, retained, &APP).await } diff --git a/tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs b/tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs new file mode 100644 index 000000000..f9c061bd6 --- /dev/null +++ b/tests/fixtures/reusable-app/crates/fixture-spin/src/progress.rs @@ -0,0 +1,237 @@ +use edgezero_adapter_spin::{ + outbound::connect_timeout_fallback_holds_for_test, + response::response_source_diagnostics_hold_for_test, +}; +use edgezero_core::EdgeError; +use fixture_core::qualification::progress::{self, Job, ProbeRuntime, ProgressHooks}; +use futures::{channel::oneshot, future::LocalBoxFuture}; +use http_body::Body as _; +use serde_json::json; +use spin_sdk::http::body::IncomingBodyExt as _; +use std::{ + sync::{ + Arc, Once, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, +}; + +struct Runtime; + +impl ProbeRuntime for Runtime { + fn adapter(&self) -> &'static str { + "spin" + } + fn perform(&self, target: String) -> Job { + Box::pin(async move { + let response = spin_sdk::http::get(&target) + .await + .map_err(|_| progress::probe_failure())?; + if response.status() != 200 { + return Err(progress::probe_failure()); + } + let body = response + .into_body() + .bytes() + .await + .map_err(|_| progress::probe_failure())?; + if body.len() > 64 { + return Err(progress::probe_failure()); + } + Ok(body.len() as u64) + }) + } + fn start(&self, target: String) -> Result { + let job = self.perform(target); + let (send, receive) = oneshot::channel(); + spin_sdk::wasip3::spawn_local(async move { + let _ = send.send(job.await); + }); + Ok(Box::pin(async move { + receive.await.map_err(|_| progress::probe_failure())? + })) + } + fn pause(&self, duration: Duration) -> LocalBoxFuture<'static, ()> { + Box::pin(spin_sdk::time::sleep(duration)) + } +} + +pub async fn dispatch( + request: spin_sdk::http::Request, +) -> anyhow::Result { + static PANIC_DIAGNOSTIC: Once = Once::new(); + PANIC_DIAGNOSTIC.call_once(|| { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic| { + // Local diagnostic only: preserve failure and the original hook, never render it on wire. + let message: String = panic.to_string().chars().take(512).collect(); + eprintln!( + "{}", + json!({"event":"progress-guest-panic","message":message,"qualification":false}) + ); + previous(panic); + })); + }); + eprintln!( + "{}", + json!({"event":"progress-dispatch-start","qualification":false}) + ); + let case = request + .uri() + .path() + .strip_prefix("/progress/") + .unwrap_or(""); + if case == "sdk-source-diagnostics" { + response_source_diagnostics_hold_for_test().await?; + return sdk_probe_response( + json!({"source_diagnostics":"pass", "cases":8, "qualification":false}), + ); + } + if case == "sdk-connect-fallback" { + if !connect_timeout_fallback_holds_for_test().await { + anyhow::bail!("connect refusal lost aggregate deadline enforcement"); + } + return sdk_probe_response( + json!({"connect_fallback":"pass", "outcomes":5, "budget_sources":4, "checks":60, "qualification":false}), + ); + } + if case == "sdk-marker-headers" { + return sdk_marker_headers(&request).await; + } + if matches!(case, "sdk-stream-ready" | "sdk-stream-timer-only") { + return sdk_response_control(&request, case.to_owned()); + } + let hint = request.body().size_hint(); + let signal = json!({"source":"spin-sdk-after-conversion", "size_hint_lower":hint.lower(), + "size_hint_upper":hint.upper(), "end_stream":request.body().is_end_stream(), "raw_framing_evidence":false}); + let returned = Arc::new(AtomicBool::new(false)); + let app = progress::application( + Arc::new(Runtime), + signal, + Some(Arc::clone(&returned)), + edgezero_adapter_spin::SPIN_PLATFORM, + )?; + let response = edgezero_adapter_spin::dispatch_app::(&app, request).await; + returned.store(true, Ordering::SeqCst); + eprintln!( + "{}", + json!({"event":"progress-dispatch-returned","qualification":false}) + ); + response +} + +fn sdk_probe_response( + probe: serde_json::Value, +) -> anyhow::Result { + use spin_sdk::http::IntoResponse as _; + edgezero_core::http::response_builder() + .header("content-type", "application/json") + .body(probe.to_string())? + .into_response() + .map_err(|_| anyhow::anyhow!("SDK probe fixture conversion failed")) +} + +async fn sdk_marker_headers( + request: &spin_sdk::http::Request, +) -> anyhow::Result { + use spin_sdk::http::{FromResponse as _, IntoRequest as _, IntoResponse as _}; + let target: edgezero_core::http::Uri = request + .headers() + .get("x-fixture-backend") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| anyhow::anyhow!("missing SDK probe origin"))? + .parse()?; + if target.scheme_str() != Some("http") + || target.host() != Some("127.0.0.1") + || target.port_u16().is_none() + || target.query().is_some() + || !["/marker/get/200/upstream", "/marker/get/200/nominated"].contains(&target.path()) + { + return Err(anyhow::anyhow!("SDK probe requires controlled origin")); + } + let outgoing = edgezero_core::http::request_builder() + .uri(target) + .body(spin_sdk::http::EmptyBody::new())? + .into_request() + .map_err(|_| anyhow::anyhow!("SDK probe request conversion failed"))?; + let native = spin_sdk::wasip3::http::client::send(outgoing) + .await + .map_err(|_| anyhow::anyhow!("SDK probe send failed"))?; + let fields = native.get_headers().copy_all(); + let connection: Vec<_> = fields + .iter() + .filter(|(name, _)| name.eq_ignore_ascii_case("connection")) + .map(|(_, value)| String::from_utf8_lossy(value).into_owned()) + .collect(); + let markers: Vec<_> = fields + .iter() + .filter(|(name, _)| name.eq_ignore_ascii_case("x-edgezero-proxy")) + .map(|(_, value)| String::from_utf8_lossy(value).into_owned()) + .collect(); + let converted = spin_sdk::http::Response::from_response(native) + .map_err(|_| anyhow::anyhow!("SDK probe response conversion failed"))?; + let bytes = converted + .into_body() + .bytes() + .await + .map_err(|_| anyhow::anyhow!("SDK probe body failed"))?; + if !bytes.is_empty() { + return Err(anyhow::anyhow!("SDK probe expected empty body")); + } + let report = json!({"case":"sdk-marker-headers","sdk_native":true,"connection":connection,"markers":markers,"qualification":false}); + edgezero_core::http::response_builder() + .header("content-type", "application/json") + .body(report.to_string())? + .into_response() + .map_err(|_| anyhow::anyhow!("SDK probe output conversion failed")) +} + +fn sdk_response_control( + request: &spin_sdk::http::Request, + case: String, +) -> anyhow::Result { + use spin_sdk::wasip3::{http::types::Fields, http_compat::BodyWriter}; + use std::future::IntoFuture as _; + + let token = request + .headers() + .get("x-request-token") + .and_then(|v| v.to_str().ok()) + .filter(|s| { + !s.is_empty() + && s.len() <= 64 + && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') + }) + .ok_or_else(|| anyhow::anyhow!("invalid SDK control token"))? + .to_owned(); + let fields = Fields::from_list(&[("content-type".to_owned(), b"application/json".to_vec())]) + .map_err(|_| anyhow::anyhow!("SDK control field construction failed"))?; + let (writer, contents, body_result) = BodyWriter::new(); + let (response, transmission) = + edgezero_adapter_spin::SpinResponse::new(fields, Some(contents), body_result); + spin_sdk::wasip3::spawn_local(async move { + let BodyWriter { + mut stream_writer, + result_writer, + .. + } = writer; + let result = async { + if !stream_writer.write_all(b" ".to_vec()).await.is_empty() {return Err(());} + let started = edgezero_core::time::MonotonicInstant::now(); + if case.ends_with("timer-only") {spin_sdk::time::sleep(Duration::from_millis(100)).await;} + let pause_ns = started.elapsed().as_nanos() as u64; + let report = json!({"adapter":"spin","case":case,"token":token,"sdk_native":true,"pause_ns":pause_ns, + "revision":option_env!("EDGEZERO_QUALIFICATION_REVISION").unwrap_or("unrecorded"),"qualification":false}); + if !stream_writer.write_all(report.to_string().into_bytes()).await.is_empty() {return Err(());} + drop(stream_writer); + result_writer.write(Ok(None)).await.map_err(|_|())?; + transmission.into_future().await.map_err(|_|())?; + Ok(()) + }.await; + eprintln!( + "{}", + json!({"event":"sdk-stream-coordinator-terminal","case":case,"token":token,"coordinator_ok":result.is_ok(),"qualification":false}) + ); + }); + Ok(response) +} diff --git a/tests/fixtures/reusable-app/outbound-cutover-cases.json b/tests/fixtures/reusable-app/outbound-cutover-cases.json new file mode 100644 index 000000000..428af3d01 --- /dev/null +++ b/tests/fixtures/reusable-app/outbound-cutover-cases.json @@ -0,0 +1,18581 @@ +{ + "schema": 1, + "source": "docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md", + "source_digest": "a3d58bcabe7e9e556d21227de973c660794d25606b7ee3365bd7808d3d101c70", + "status": "partially activated inventory; EZ-23 local acceptance 01-11 and EZ-9 unit acceptance 01-15 plus local runtime 16/17 bind executable guarded commands; 479 other legs remain planned; readiness is not qualification or applicability approval; mandatory EZ-1.14 compile-only prerequisite: README.md#mandatory-forwarding-example-doctest (Q1/Q2); no forwarding runtime-leg credit or EZ-25.42 adoption", + "external_prerequisites": [ + { + "requirement": "EZ-8", + "status": "PendingExternal", + "reason": "No staging entitlement or execution owner confirmed; remains Blocking." + }, + { + "requirement": "G4", + "status": "Pending", + "reason": "Original API approval and the requester's scoped 2026-10-24 timing/native-integration amendment are recorded in docs/audits/2026-10-18-outbound-api-approval.md#scoped-timing-and-native-integration-amendment. Send-first integration may proceed on the unchanged current trait with UNKNOWN synchronous blocking horizon; timing-only G2/finite-H(K) proof is not a preimplementation gate. Literal B/backend policy/resources remain unapproved; timers/slack and typed body-error/readiness safety are unchanged. Full settlement/Follow/engine/atomic migration and hosted/performance qualification remain open." + }, + { + "requirement": "EZ-2/M1-M7", + "status": "Pending", + "reason": "Product sample counts and thresholds require confirmation before comparison." + } + ], + "decisions": [ + { + "id": "hosted HTTP3 enabled@hosted-fastly/http3", + "status": "pending", + "scope": ["EZ-1/acceptance/02@hosted-fastly/http3"], + "owner": "P2", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-16 present@hosted-fastly", + "status": "pending", + "scope": ["EZ-2/acceptance/10b@hosted-fastly"], + "owner": "F6a", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "previous-record backend configuration changed@hosted-fastly", + "status": "pending", + "scope": ["EZ-2/acceptance/13b@hosted-fastly"], + "owner": "D1b", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-5.9 adopted@unit/axum", + "status": "pending", + "scope": [ + "EZ-5/acceptance/09b@unit/axum", + "EZ-5/acceptance/11b@unit/axum" + ], + "owner": "P3", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-5.9 adopted@unit/fastly", + "status": "pending", + "scope": [ + "EZ-5/acceptance/09b@unit/fastly", + "EZ-5/acceptance/11b@unit/fastly" + ], + "owner": "P3", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-5.9 adopted@unit/cloudflare", + "status": "pending", + "scope": [ + "EZ-5/acceptance/09b@unit/cloudflare", + "EZ-5/acceptance/11b@unit/cloudflare" + ], + "owner": "P3", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-5.9 adopted@unit/spin", + "status": "pending", + "scope": [ + "EZ-5/acceptance/09b@unit/spin", + "EZ-5/acceptance/11b@unit/spin" + ], + "owner": "P3", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-5.9 adopted@unit/core", + "status": "pending", + "scope": ["EZ-5/acceptance/11b@unit/core"], + "owner": "P3", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-5.10 adopted@unit/core", + "status": "pending", + "scope": ["EZ-5/acceptance/12@unit/core"], + "owner": "P3", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-5.10 adopted@unit/axum", + "status": "pending", + "scope": ["EZ-5/acceptance/12@unit/axum"], + "owner": "P3", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-5.10 adopted@unit/fastly", + "status": "pending", + "scope": ["EZ-5/acceptance/12@unit/fastly"], + "owner": "P3", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-5.10 adopted@unit/cloudflare", + "status": "pending", + "scope": ["EZ-5/acceptance/12@unit/cloudflare"], + "owner": "P3", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-5.10 adopted@unit/spin", + "status": "pending", + "scope": ["EZ-5/acceptance/12@unit/spin"], + "owner": "P3", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "reusable-sandbox adoption@unit/fastly/both-wrappers", + "status": "pending", + "scope": ["EZ-6/acceptance/23b@unit/fastly/both-wrappers"], + "owner": "F1", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "reusable-sandbox adoption@viceroy", + "status": "pending", + "scope": ["EZ-6/acceptance/23b@viceroy"], + "owner": "F1", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-11 present@viceroy", + "status": "pending", + "scope": ["EZ-8/acceptance/06b@viceroy"], + "owner": "F3a", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-14 present@axum", + "status": "pending", + "scope": ["EZ-9/acceptance/16b@axum"], + "owner": "F4", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-14 present@viceroy", + "status": "pending", + "scope": ["EZ-9/acceptance/16b@viceroy"], + "owner": "F4", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-14 present@workerd", + "status": "pending", + "scope": ["EZ-9/acceptance/16b@workerd"], + "owner": "F4", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-14 present@spin", + "status": "pending", + "scope": ["EZ-9/acceptance/16b@spin"], + "owner": "F4", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-10 overflow and EZ-19 header composition present@unit/axum", + "status": "pending", + "scope": ["EZ-19/acceptance/08@unit/axum"], + "owner": "F7b", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-10 overflow and EZ-19 header composition present@unit/fastly", + "status": "pending", + "scope": ["EZ-19/acceptance/08@unit/fastly"], + "owner": "F7b", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-10 overflow and EZ-19 header composition present@unit/cloudflare", + "status": "pending", + "scope": ["EZ-19/acceptance/08@unit/cloudflare"], + "owner": "F7b", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-10 overflow and EZ-19 header composition present@unit/spin", + "status": "pending", + "scope": ["EZ-19/acceptance/08@unit/spin"], + "owner": "F7b", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-20.7 adopted@workerd", + "status": "pending", + "implementation_adoption": "Requester approved same-release implementation on 2026-10-21; see docs/audits/evidence/2026-10-21-declared-kv.json. Local implementation evidence does not supply the separate release approval fields below.", + "scope": ["EZ-20/acceptance/04@workerd"], + "owner": "F8", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-20.7 adopted@spin", + "status": "pending", + "implementation_adoption": "Requester approved same-release implementation on 2026-10-21; see docs/audits/evidence/2026-10-21-declared-kv.json. Local implementation evidence does not supply the separate release approval fields below.", + "scope": ["EZ-20/acceptance/04@spin"], + "owner": "F8", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-22.8 adopted@workerd/retained-instance", + "status": "pending", + "scope": ["EZ-22/acceptance/07b@workerd/retained-instance"], + "owner": "F9a", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-22.8 adopted@spin/retained-instance", + "status": "pending", + "scope": ["EZ-22/acceptance/07b@spin/retained-instance"], + "owner": "F9a", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-22.10 adopted@unit/core", + "status": "pending", + "scope": ["EZ-22/acceptance/08@unit/core"], + "owner": "F9a", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-24.2 JS target adopted@unit/core-js", + "status": "pending", + "scope": [ + "EZ-24/acceptance/03@unit/core-js", + "EZ-24/acceptance/04@unit/core-js", + "EZ-24/acceptance/05@unit/core-js", + "EZ-24/acceptance/06@unit/core-js", + "EZ-24/acceptance/07@unit/core-js", + "EZ-24/acceptance/08@unit/core-js", + "EZ-24/acceptance/15@unit/core-js" + ], + "owner": "P6", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-24.11 adopted@unit/core", + "status": "pending", + "scope": ["EZ-24/acceptance/12@unit/core"], + "owner": "P6", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-24.11 adopted@unit/core-wasip1", + "status": "pending", + "scope": ["EZ-24/acceptance/12@unit/core-wasip1"], + "owner": "P6", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-24.11 adopted and JS target adopted@unit/core-js", + "status": "pending", + "scope": ["EZ-24/acceptance/12@unit/core-js"], + "owner": "P6", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-14 present@unit/core", + "status": "pending", + "scope": ["EZ-24/acceptance/14@unit/core"], + "owner": "P6", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-14 present@unit/core-wasip1", + "status": "pending", + "scope": ["EZ-24/acceptance/14@unit/core-wasip1"], + "owner": "P6", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-14 present and EZ-24.2 JS target adopted@unit/core-js", + "status": "pending", + "scope": ["EZ-24/acceptance/14@unit/core-js"], + "owner": "P6", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-25.42 adopted@edgezero-ci", + "status": "pending", + "scope": ["EZ-25/acceptance/03@edgezero-ci"], + "owner": "Q1", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-24.12 adopted@axum/demo", + "status": "pending", + "scope": ["EZ-24/requirements/12@axum/demo"], + "owner": "F10b", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-24.12 adopted@axum/generated", + "status": "pending", + "scope": ["EZ-24/requirements/12@axum/generated"], + "owner": "F10b", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + }, + { + "id": "EZ-25.43 adopted@edgezero-ci", + "status": "pending", + "scope": ["EZ-25/acceptance/05b@edgezero-ci"], + "owner": "Q1", + "ci_owner": null, + "edgezero_approver": null, + "consumer_approver": null, + "date": null, + "evidence": null + } + ], + "cases": [ + { + "case_id": "EZ-1/acceptance/01", + "source_id": "EZ-1/acceptance/01", + "source_digest": "fd3183791ed90916c4da5c18f2067b8d430e0a65e5c5ab2729c987c2fefabc0b", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "axum/http1", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-01-axum-http1", + "sentinel": "ez_1_acceptance_01_axum_http1", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-1/acceptance/01", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. Given a route that forwards the inbound request with `from_request` and `send` to an origin that records what it receives, when a client sends a bodiless HTTP/1.1 GET, then the client gets the origin's status and the origin saw a GET with zero body bytes and no `Transfer-Encoding`. Record whether `Content-Length: 0` appeared. The same holds for HEAD, and for a GET whose chunked body holds only the terminator.", + "artifact_path": "outbound/EZ-1/acceptance/01/axum/http1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/01", + "source_id": "EZ-1/acceptance/01", + "source_digest": "fd3183791ed90916c4da5c18f2067b8d430e0a65e5c5ab2729c987c2fefabc0b", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "viceroy/http1", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-01-viceroy-http1", + "sentinel": "ez_1_acceptance_01_viceroy_http1", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-1/acceptance/01", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. Given a route that forwards the inbound request with `from_request` and `send` to an origin that records what it receives, when a client sends a bodiless HTTP/1.1 GET, then the client gets the origin's status and the origin saw a GET with zero body bytes and no `Transfer-Encoding`. Record whether `Content-Length: 0` appeared. The same holds for HEAD, and for a GET whose chunked body holds only the terminator.", + "artifact_path": "outbound/EZ-1/acceptance/01/viceroy/http1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/01", + "source_id": "EZ-1/acceptance/01", + "source_digest": "fd3183791ed90916c4da5c18f2067b8d430e0a65e5c5ab2729c987c2fefabc0b", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "workerd/http1", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-01-workerd-http1", + "sentinel": "ez_1_acceptance_01_workerd_http1", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-1/acceptance/01", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. Given a route that forwards the inbound request with `from_request` and `send` to an origin that records what it receives, when a client sends a bodiless HTTP/1.1 GET, then the client gets the origin's status and the origin saw a GET with zero body bytes and no `Transfer-Encoding`. Record whether `Content-Length: 0` appeared. The same holds for HEAD, and for a GET whose chunked body holds only the terminator.", + "artifact_path": "outbound/EZ-1/acceptance/01/workerd/http1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/01", + "source_id": "EZ-1/acceptance/01", + "source_digest": "fd3183791ed90916c4da5c18f2067b8d430e0a65e5c5ab2729c987c2fefabc0b", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "spin/http1", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-01-spin-http1", + "sentinel": "ez_1_acceptance_01_spin_http1", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-1/acceptance/01", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. Given a route that forwards the inbound request with `from_request` and `send` to an origin that records what it receives, when a client sends a bodiless HTTP/1.1 GET, then the client gets the origin's status and the origin saw a GET with zero body bytes and no `Transfer-Encoding`. Record whether `Content-Length: 0` appeared. The same holds for HEAD, and for a GET whose chunked body holds only the terminator.", + "artifact_path": "outbound/EZ-1/acceptance/01/spin/http1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/02", + "source_id": "EZ-1/acceptance/02", + "source_digest": "34cf488bc053f6cb7567e6c21e40760153b8216628e482adab65b510c971aad3", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "hosted-fastly/http1", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-02-hosted-fastly-http1", + "sentinel": "ez_1_acceptance_02_hosted_fastly_http1", + "command": "hosted-runbook EZ-1/acceptance/02 --environment hosted-fastly/http1 --sentinel ez_1_acceptance_02_hosted_fastly_http1", + "expected": "Environment is hosted Fastly, Trusted Server staging. Repeat criterion 1 with HTTP/1.1 and HTTP/2 clients that send no `content-length`. Repeat it with an HTTP/3 client where the staging service has HTTP/3 enabled. Whether a Fastly Compute service serves HTTP/3 to clients is **Unverified**. Where it does not, the record states that HTTP/3 was not available and marks the HTTP/3 leg not applicable. Record per protocol whether a bodiless forward still succeeds after the inbound read deadline has passed, which shows whether EZ-1.2 held.", + "artifact_path": "outbound/EZ-1/acceptance/02/hosted-fastly/http1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/02", + "source_id": "EZ-1/acceptance/02", + "source_digest": "34cf488bc053f6cb7567e6c21e40760153b8216628e482adab65b510c971aad3", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "hosted-fastly/http2", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-02-hosted-fastly-http2", + "sentinel": "ez_1_acceptance_02_hosted_fastly_http2", + "command": "hosted-runbook EZ-1/acceptance/02 --environment hosted-fastly/http2 --sentinel ez_1_acceptance_02_hosted_fastly_http2", + "expected": "Environment is hosted Fastly, Trusted Server staging. Repeat criterion 1 with HTTP/1.1 and HTTP/2 clients that send no `content-length`. Repeat it with an HTTP/3 client where the staging service has HTTP/3 enabled. Whether a Fastly Compute service serves HTTP/3 to clients is **Unverified**. Where it does not, the record states that HTTP/3 was not available and marks the HTTP/3 leg not applicable. Record per protocol whether a bodiless forward still succeeds after the inbound read deadline has passed, which shows whether EZ-1.2 held.", + "artifact_path": "outbound/EZ-1/acceptance/02/hosted-fastly/http2.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/02", + "source_id": "EZ-1/acceptance/02", + "source_digest": "34cf488bc053f6cb7567e6c21e40760153b8216628e482adab65b510c971aad3", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "hosted-fastly/http3", + "applicability": "conditional", + "approval": "hosted HTTP3 enabled@hosted-fastly/http3", + "condition": "hosted HTTP3 enabled", + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-02-hosted-fastly-http3", + "sentinel": "ez_1_acceptance_02_hosted_fastly_http3", + "command": "hosted-runbook EZ-1/acceptance/02 --environment hosted-fastly/http3 --sentinel ez_1_acceptance_02_hosted_fastly_http3", + "expected": "Environment is hosted Fastly, Trusted Server staging. Repeat criterion 1 with HTTP/1.1 and HTTP/2 clients that send no `content-length`. Repeat it with an HTTP/3 client where the staging service has HTTP/3 enabled. Whether a Fastly Compute service serves HTTP/3 to clients is **Unverified**. Where it does not, the record states that HTTP/3 was not available and marks the HTTP/3 leg not applicable. Record per protocol whether a bodiless forward still succeeds after the inbound read deadline has passed, which shows whether EZ-1.2 held.", + "artifact_path": "outbound/EZ-1/acceptance/02/hosted-fastly/http3.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/03", + "source_id": "EZ-1/acceptance/03", + "source_digest": "3ab117b7961d23728fe3ca24722c1060e872e5813b4a4def5dde14c4229aa80b", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-03-workerd", + "sentinel": "ez_1_acceptance_03_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-1/acceptance/03", + "expected": "Environment is workerd. The run first records whether workerd reports no request body for a bodiless GET. Where it does, a handler that takes the body of a bodiless GET observes an empty buffered body. Where it does not, the run records that, and Cloudflare's EZ-1.2 level is BestEffort.", + "artifact_path": "outbound/EZ-1/acceptance/03/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/04", + "source_id": "EZ-1/acceptance/04", + "source_digest": "bbe02d8f28ec513f8f2b6c20d0d3385a3871313be6af32ea579be5fe2597ca37", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-04-axum", + "sentinel": "ez_1_acceptance_04_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-1/acceptance/04", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. When a client sends a GET with `Content-Length: 5` and five bytes, then the client gets the EZ-1.8 400 and the origin records nothing. On workerd, record whether the body reached the guest at all. On Axum, a request pipelined after it is either refused by a connection close or answered from the correct offset.", + "artifact_path": "outbound/EZ-1/acceptance/04/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/04", + "source_id": "EZ-1/acceptance/04", + "source_digest": "bbe02d8f28ec513f8f2b6c20d0d3385a3871313be6af32ea579be5fe2597ca37", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-04-viceroy", + "sentinel": "ez_1_acceptance_04_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-1/acceptance/04", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. When a client sends a GET with `Content-Length: 5` and five bytes, then the client gets the EZ-1.8 400 and the origin records nothing. On workerd, record whether the body reached the guest at all. On Axum, a request pipelined after it is either refused by a connection close or answered from the correct offset.", + "artifact_path": "outbound/EZ-1/acceptance/04/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/04", + "source_id": "EZ-1/acceptance/04", + "source_digest": "bbe02d8f28ec513f8f2b6c20d0d3385a3871313be6af32ea579be5fe2597ca37", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-04-workerd", + "sentinel": "ez_1_acceptance_04_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-1/acceptance/04", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. When a client sends a GET with `Content-Length: 5` and five bytes, then the client gets the EZ-1.8 400 and the origin records nothing. On workerd, record whether the body reached the guest at all. On Axum, a request pipelined after it is either refused by a connection close or answered from the correct offset.", + "artifact_path": "outbound/EZ-1/acceptance/04/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/04", + "source_id": "EZ-1/acceptance/04", + "source_digest": "bbe02d8f28ec513f8f2b6c20d0d3385a3871313be6af32ea579be5fe2597ca37", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-04-spin", + "sentinel": "ez_1_acceptance_04_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-1/acceptance/04", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. When a client sends a GET with `Content-Length: 5` and five bytes, then the client gets the EZ-1.8 400 and the origin records nothing. On workerd, record whether the body reached the guest at all. On Axum, a request pipelined after it is either refused by a connection close or answered from the correct offset.", + "artifact_path": "outbound/EZ-1/acceptance/04/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/05", + "source_id": "EZ-1/acceptance/05", + "source_digest": "ff301364809a2811f9d2656f6facd43c2f06b24a3f28dc243d55c8ea2731c74c", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "consumer-hosted-fastly/http2", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "P2", + "substep": "EZ-1-acceptance-05-consumer-hosted-fastly-http2", + "sentinel": "ez_1_acceptance_05_consumer_hosted_fastly_http2", + "command": "consumer-runbook EZ-1/acceptance/05 --environment consumer-hosted-fastly/http2 --sentinel ez_1_acceptance_05_consumer_hosted_fastly_http2", + "expected": "Environment is hosted Fastly, Trusted Server staging. An HTTP/2 GET with a DATA frame and no `content-length` gets the EZ-1.8 400, and the origin records nothing.", + "artifact_path": "outbound/EZ-1/acceptance/05/consumer-hosted-fastly/http2.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/06", + "source_id": "EZ-1/acceptance/06", + "source_digest": "c5cb36d9fd126f56051dad7a2ab8a87c64d728db1782bb9ba8ad31b6b8bddac8", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "axum/http1-chunked", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-06-axum-http1-chunked", + "sentinel": "ez_1_acceptance_06_axum_http1_chunked", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-1/acceptance/06", + "expected": "Environment is Axum. Given an outbound timeout of 200 ms, when a client sends a chunked GET and never sends a chunk, then the forward fails with 504 within 205 ms. With an inbound read deadline of 100 ms, it fails with 408 within 105 ms. On hosted Fastly (Trusted Server staging) the same scenario records the observed outcome and time as BestEffort.", + "artifact_path": "outbound/EZ-1/acceptance/06/axum/http1-chunked.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/06", + "source_id": "EZ-1/acceptance/06", + "source_digest": "c5cb36d9fd126f56051dad7a2ab8a87c64d728db1782bb9ba8ad31b6b8bddac8", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "hosted-fastly/http1-chunked", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-06-hosted-fastly-http1-chunked", + "sentinel": "ez_1_acceptance_06_hosted_fastly_http1_chunked", + "command": "hosted-runbook EZ-1/acceptance/06 --environment hosted-fastly/http1-chunked --sentinel ez_1_acceptance_06_hosted_fastly_http1_chunked", + "expected": "Environment is Axum. Given an outbound timeout of 200 ms, when a client sends a chunked GET and never sends a chunk, then the forward fails with 504 within 205 ms. With an inbound read deadline of 100 ms, it fails with 408 within 105 ms. On hosted Fastly (Trusted Server staging) the same scenario records the observed outcome and time as BestEffort.", + "artifact_path": "outbound/EZ-1/acceptance/06/hosted-fastly/http1-chunked.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/07", + "source_id": "EZ-1/acceptance/07", + "source_digest": "1e69318f91a6b86c5dcf464a0e13a0bbfd75c470d5fbb2b31ea0bd2e5536a561", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-07-unit-core", + "sentinel": "ez_1_acceptance_07_unit_core", + "command": "scripts/run_test_nonzero.sh ez_1_acceptance_07_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is unit. A GET whose streamed body yields only zero-length chunks and then ends is sent bodiless. A GET with a streamed body and an already expired deadline fails with 504, and the body is not read. A POST with a streamed body, and a GET with an empty buffered body, read nothing extra. A taken body forwards empty. A body whose read failed forwards the same error.", + "artifact_path": "outbound/EZ-1/acceptance/07/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/08", + "source_id": "EZ-1/acceptance/08", + "source_digest": "ec108915dbb078725aa757ff7d68b3e9d7b2d095d3a9a409dc788ef7946e19e7", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-08-axum", + "sentinel": "ez_1_acceptance_08_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-1/acceptance/08", + "expected": "Environment is Axum and workerd. A bodiless GET forwarded after the inbound read deadline has passed still succeeds (EZ-1.2). On workerd this holds where criterion 3 showed that workerd reports no request body. Otherwise the run records the outcome, and Cloudflare is BestEffort. On Spin 4.1 it succeeds when the request had `Content-Length: 0`.", + "artifact_path": "outbound/EZ-1/acceptance/08/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/08", + "source_id": "EZ-1/acceptance/08", + "source_digest": "ec108915dbb078725aa757ff7d68b3e9d7b2d095d3a9a409dc788ef7946e19e7", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-08-workerd", + "sentinel": "ez_1_acceptance_08_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-1/acceptance/08", + "expected": "Environment is Axum and workerd. A bodiless GET forwarded after the inbound read deadline has passed still succeeds (EZ-1.2). On workerd this holds where criterion 3 showed that workerd reports no request body. Otherwise the run records the outcome, and Cloudflare is BestEffort. On Spin 4.1 it succeeds when the request had `Content-Length: 0`.", + "artifact_path": "outbound/EZ-1/acceptance/08/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/08", + "source_id": "EZ-1/acceptance/08", + "source_digest": "ec108915dbb078725aa757ff7d68b3e9d7b2d095d3a9a409dc788ef7946e19e7", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "spin/content-length-0", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-08-spin-content-length-0", + "sentinel": "ez_1_acceptance_08_spin_content_length_0", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-1/acceptance/08", + "expected": "Environment is Axum and workerd. A bodiless GET forwarded after the inbound read deadline has passed still succeeds (EZ-1.2). On workerd this holds where criterion 3 showed that workerd reports no request body. Otherwise the run records the outcome, and Cloudflare is BestEffort. On Spin 4.1 it succeeds when the request had `Content-Length: 0`.", + "artifact_path": "outbound/EZ-1/acceptance/08/spin/content-length-0.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/09", + "source_id": "EZ-1/acceptance/09", + "source_digest": "7fc84608108781c4cd717ebea7b3d1dd4c06228d1f42248151441ff2f34cfc2a", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-09-axum", + "sentinel": "ez_1_acceptance_09_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-1/acceptance/09", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A forwarded POST with a 3-byte body arrives at the origin with exactly those 3 bytes. A forwarded bodiless OPTIONS and a bodiless DELETE with no `Content-Length` succeed, and the origin sees zero body bytes. Record whether chunked framing was used.", + "artifact_path": "outbound/EZ-1/acceptance/09/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/09", + "source_id": "EZ-1/acceptance/09", + "source_digest": "7fc84608108781c4cd717ebea7b3d1dd4c06228d1f42248151441ff2f34cfc2a", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-09-viceroy", + "sentinel": "ez_1_acceptance_09_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-1/acceptance/09", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A forwarded POST with a 3-byte body arrives at the origin with exactly those 3 bytes. A forwarded bodiless OPTIONS and a bodiless DELETE with no `Content-Length` succeed, and the origin sees zero body bytes. Record whether chunked framing was used.", + "artifact_path": "outbound/EZ-1/acceptance/09/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/09", + "source_id": "EZ-1/acceptance/09", + "source_digest": "7fc84608108781c4cd717ebea7b3d1dd4c06228d1f42248151441ff2f34cfc2a", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-09-workerd", + "sentinel": "ez_1_acceptance_09_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-1/acceptance/09", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A forwarded POST with a 3-byte body arrives at the origin with exactly those 3 bytes. A forwarded bodiless OPTIONS and a bodiless DELETE with no `Content-Length` succeed, and the origin sees zero body bytes. Record whether chunked framing was used.", + "artifact_path": "outbound/EZ-1/acceptance/09/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/09", + "source_id": "EZ-1/acceptance/09", + "source_digest": "7fc84608108781c4cd717ebea7b3d1dd4c06228d1f42248151441ff2f34cfc2a", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-09-spin", + "sentinel": "ez_1_acceptance_09_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-1/acceptance/09", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A forwarded POST with a 3-byte body arrives at the origin with exactly those 3 bytes. A forwarded bodiless OPTIONS and a bodiless DELETE with no `Content-Length` succeed, and the origin sees zero body bytes. Record whether chunked framing was used.", + "artifact_path": "outbound/EZ-1/acceptance/09/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/10", + "source_id": "EZ-1/acceptance/10", + "source_digest": "89a07ed1116a08469fab9415fe51d8fe7f8cb5d12732f0e69a19c217d7a67830", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-10-axum", + "sentinel": "ez_1_acceptance_10_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-1/acceptance/10", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1, once EZ-6 is present. A bodiless navigation GET begun with EZ-6's begin call reaches the origin with no body, under EZ-6's rule for when a begun request is on its way. A begun POST with a streamed body is rejected before anything is sent.", + "artifact_path": "outbound/EZ-1/acceptance/10/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/10", + "source_id": "EZ-1/acceptance/10", + "source_digest": "89a07ed1116a08469fab9415fe51d8fe7f8cb5d12732f0e69a19c217d7a67830", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-10-viceroy", + "sentinel": "ez_1_acceptance_10_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-1/acceptance/10", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1, once EZ-6 is present. A bodiless navigation GET begun with EZ-6's begin call reaches the origin with no body, under EZ-6's rule for when a begun request is on its way. A begun POST with a streamed body is rejected before anything is sent.", + "artifact_path": "outbound/EZ-1/acceptance/10/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/10", + "source_id": "EZ-1/acceptance/10", + "source_digest": "89a07ed1116a08469fab9415fe51d8fe7f8cb5d12732f0e69a19c217d7a67830", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-10-workerd", + "sentinel": "ez_1_acceptance_10_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-1/acceptance/10", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1, once EZ-6 is present. A bodiless navigation GET begun with EZ-6's begin call reaches the origin with no body, under EZ-6's rule for when a begun request is on its way. A begun POST with a streamed body is rejected before anything is sent.", + "artifact_path": "outbound/EZ-1/acceptance/10/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/10", + "source_id": "EZ-1/acceptance/10", + "source_digest": "89a07ed1116a08469fab9415fe51d8fe7f8cb5d12732f0e69a19c217d7a67830", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-10-spin", + "sentinel": "ez_1_acceptance_10_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-1/acceptance/10", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1, once EZ-6 is present. A bodiless navigation GET begun with EZ-6's begin call reaches the origin with no body, under EZ-6's rule for when a begun request is on its way. A begun POST with a streamed body is rejected before anything is sent.", + "artifact_path": "outbound/EZ-1/acceptance/10/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/11", + "source_id": "EZ-1/acceptance/11", + "source_digest": "c5238b222a7cb79f07ee66e8e16803ee27627a9934c2559b8be745e857461cc0", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-11-axum", + "sentinel": "ez_1_acceptance_11_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-1/acceptance/11", + "expected": "Environment is Axum and Viceroy 0.21.0, once EZ-9 is present. A forwarded bodiless GET whose origin answers 307 to a second URL makes two requests, both bodiless, and the inbound body is read at most once.", + "artifact_path": "outbound/EZ-1/acceptance/11/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/11", + "source_id": "EZ-1/acceptance/11", + "source_digest": "c5238b222a7cb79f07ee66e8e16803ee27627a9934c2559b8be745e857461cc0", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-11-viceroy", + "sentinel": "ez_1_acceptance_11_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-1/acceptance/11", + "expected": "Environment is Axum and Viceroy 0.21.0, once EZ-9 is present. A forwarded bodiless GET whose origin answers 307 to a second URL makes two requests, both bodiless, and the inbound body is read at most once.", + "artifact_path": "outbound/EZ-1/acceptance/11/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/12", + "source_id": "EZ-1/acceptance/12", + "source_digest": "f641992210c8d8f63fe14f2b5d118dfc732acacb9eac32c7bab06b7017802a6e", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-12-axum", + "sentinel": "ez_1_acceptance_12_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-1/acceptance/12", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. After the handler calls `RequestContext::body_bytes` on a bodiless GET, the forwarded request is accepted into a batch.", + "artifact_path": "outbound/EZ-1/acceptance/12/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/12", + "source_id": "EZ-1/acceptance/12", + "source_digest": "f641992210c8d8f63fe14f2b5d118dfc732acacb9eac32c7bab06b7017802a6e", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-12-viceroy", + "sentinel": "ez_1_acceptance_12_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-1/acceptance/12", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. After the handler calls `RequestContext::body_bytes` on a bodiless GET, the forwarded request is accepted into a batch.", + "artifact_path": "outbound/EZ-1/acceptance/12/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/12", + "source_id": "EZ-1/acceptance/12", + "source_digest": "f641992210c8d8f63fe14f2b5d118dfc732acacb9eac32c7bab06b7017802a6e", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-12-workerd", + "sentinel": "ez_1_acceptance_12_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-1/acceptance/12", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. After the handler calls `RequestContext::body_bytes` on a bodiless GET, the forwarded request is accepted into a batch.", + "artifact_path": "outbound/EZ-1/acceptance/12/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/12", + "source_id": "EZ-1/acceptance/12", + "source_digest": "f641992210c8d8f63fe14f2b5d118dfc732acacb9eac32c7bab06b7017802a6e", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-12-spin", + "sentinel": "ez_1_acceptance_12_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-1/acceptance/12", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. After the handler calls `RequestContext::body_bytes` on a bodiless GET, the forwarded request is accepted into a batch.", + "artifact_path": "outbound/EZ-1/acceptance/12/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/13", + "source_id": "EZ-1/acceptance/13", + "source_digest": "0d616bc23926030005fb19d311fef9931d251b2b89c9e1d6e706ebf80962f07c", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "consumer-viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "P2", + "substep": "EZ-1-acceptance-13-consumer-viceroy", + "sentinel": "ez_1_acceptance_13_consumer_viceroy", + "command": "consumer-runbook EZ-1/acceptance/13 --environment consumer-viceroy --sentinel ez_1_acceptance_13_consumer_viceroy", + "expected": "Trusted Server end to end. `cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test integration -- --include-ignored` passes `test_wordpress_fastly`, `test_nextjs_fastly`, `test_wordpress_cloudflare`, `test_nextjs_cloudflare`, `test_wordpress_axum` and `test_nextjs_axum` (`ts:crates/trusted-server-integration-tests/tests/integration.rs:137-167`, `:272-285`) with TS forwarding through `from_request` and no TS-side body handling. Those tests are ignored by default, so the flag is required, as TS CI passes it (`ts:.github/workflows/integration-tests.yml:106`, `:148`). New TS cases pass for `HEAD /` (origin status, no body), a forwarded `OPTIONS /` (origin status), and `GET /` with a body (400). TS's in-process parity suite does not cover adapter ingress, so it is not enough on its own.", + "artifact_path": "outbound/EZ-1/acceptance/13/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/13", + "source_id": "EZ-1/acceptance/13", + "source_digest": "0d616bc23926030005fb19d311fef9931d251b2b89c9e1d6e706ebf80962f07c", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "consumer-workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "P2", + "substep": "EZ-1-acceptance-13-consumer-workerd", + "sentinel": "ez_1_acceptance_13_consumer_workerd", + "command": "consumer-runbook EZ-1/acceptance/13 --environment consumer-workerd --sentinel ez_1_acceptance_13_consumer_workerd", + "expected": "Trusted Server end to end. `cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test integration -- --include-ignored` passes `test_wordpress_fastly`, `test_nextjs_fastly`, `test_wordpress_cloudflare`, `test_nextjs_cloudflare`, `test_wordpress_axum` and `test_nextjs_axum` (`ts:crates/trusted-server-integration-tests/tests/integration.rs:137-167`, `:272-285`) with TS forwarding through `from_request` and no TS-side body handling. Those tests are ignored by default, so the flag is required, as TS CI passes it (`ts:.github/workflows/integration-tests.yml:106`, `:148`). New TS cases pass for `HEAD /` (origin status, no body), a forwarded `OPTIONS /` (origin status), and `GET /` with a body (400). TS's in-process parity suite does not cover adapter ingress, so it is not enough on its own.", + "artifact_path": "outbound/EZ-1/acceptance/13/consumer-workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/13", + "source_id": "EZ-1/acceptance/13", + "source_digest": "0d616bc23926030005fb19d311fef9931d251b2b89c9e1d6e706ebf80962f07c", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "consumer-axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "P2", + "substep": "EZ-1-acceptance-13-consumer-axum", + "sentinel": "ez_1_acceptance_13_consumer_axum", + "command": "consumer-runbook EZ-1/acceptance/13 --environment consumer-axum --sentinel ez_1_acceptance_13_consumer_axum", + "expected": "Trusted Server end to end. `cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test integration -- --include-ignored` passes `test_wordpress_fastly`, `test_nextjs_fastly`, `test_wordpress_cloudflare`, `test_nextjs_cloudflare`, `test_wordpress_axum` and `test_nextjs_axum` (`ts:crates/trusted-server-integration-tests/tests/integration.rs:137-167`, `:272-285`) with TS forwarding through `from_request` and no TS-side body handling. Those tests are ignored by default, so the flag is required, as TS CI passes it (`ts:.github/workflows/integration-tests.yml:106`, `:148`). New TS cases pass for `HEAD /` (origin status, no body), a forwarded `OPTIONS /` (origin status), and `GET /` with a body (400). TS's in-process parity suite does not cover adapter ingress, so it is not enough on its own.", + "artifact_path": "outbound/EZ-1/acceptance/13/consumer-axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/14", + "source_id": "EZ-1/acceptance/14", + "source_digest": "9771ee3b0baae5829927c596764177501fac471c247b8e896c6c021fb9a375b5", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-14-axum", + "sentinel": "ez_1_acceptance_14_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-1/acceptance/14", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A GET carrying an option the adapter marks Unsupported, with a streamed inbound body whose client never sends a byte, is rejected at once with the unsupported-option error, and the forward does not wait for the body.", + "artifact_path": "outbound/EZ-1/acceptance/14/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/14", + "source_id": "EZ-1/acceptance/14", + "source_digest": "9771ee3b0baae5829927c596764177501fac471c247b8e896c6c021fb9a375b5", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-14-viceroy", + "sentinel": "ez_1_acceptance_14_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-1/acceptance/14", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A GET carrying an option the adapter marks Unsupported, with a streamed inbound body whose client never sends a byte, is rejected at once with the unsupported-option error, and the forward does not wait for the body.", + "artifact_path": "outbound/EZ-1/acceptance/14/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/14", + "source_id": "EZ-1/acceptance/14", + "source_digest": "9771ee3b0baae5829927c596764177501fac471c247b8e896c6c021fb9a375b5", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-14-workerd", + "sentinel": "ez_1_acceptance_14_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-1/acceptance/14", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A GET carrying an option the adapter marks Unsupported, with a streamed inbound body whose client never sends a byte, is rejected at once with the unsupported-option error, and the forward does not wait for the body.", + "artifact_path": "outbound/EZ-1/acceptance/14/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-1/acceptance/14", + "source_id": "EZ-1/acceptance/14", + "source_digest": "9771ee3b0baae5829927c596764177501fac471c247b8e896c6c021fb9a375b5", + "requirement_ids": [ + "EZ-1.1", + "EZ-1.2", + "EZ-1.3", + "EZ-1.4", + "EZ-1.5", + "EZ-1.6", + "EZ-1.7", + "EZ-1.8", + "EZ-1.9", + "EZ-1.10", + "EZ-1.11", + "EZ-1.12", + "EZ-1.13", + "EZ-1.14" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P2", + "substep": "EZ-1-acceptance-14-spin", + "sentinel": "ez_1_acceptance_14_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-1/acceptance/14", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A GET carrying an option the adapter marks Unsupported, with a streamed inbound body whose client never sends a byte, is rejected at once with the unsupported-option error, and the forward does not wait for the body.", + "artifact_path": "outbound/EZ-1/acceptance/14/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/01", + "source_id": "EZ-2/acceptance/01", + "source_digest": "887229030d93b6f617636c21091dfbb4a5f2f02b2a9878d8910535cc33a5b701", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "unit/axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-01-unit-axum", + "sentinel": "ez_2_acceptance_01_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_2_acceptance_01_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Given a client in router state, another inserted through Fastly `prepare`, and the adapter default, when a handler calls `RequestContext::http_client`, then it receives the router-state client. Given only the `prepare` client and the default, it receives the `prepare` client. The router-state case holds on Axum, Cloudflare, and Spin against their defaults.\n\nViceroy 0.21.0, one inbound request per case:", + "artifact_path": "outbound/EZ-2/acceptance/01/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/01", + "source_id": "EZ-2/acceptance/01", + "source_digest": "887229030d93b6f617636c21091dfbb4a5f2f02b2a9878d8910535cc33a5b701", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-01-unit-fastly", + "sentinel": "ez_2_acceptance_01_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_2_acceptance_01_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Given a client in router state, another inserted through Fastly `prepare`, and the adapter default, when a handler calls `RequestContext::http_client`, then it receives the router-state client. Given only the `prepare` client and the default, it receives the `prepare` client. The router-state case holds on Axum, Cloudflare, and Spin against their defaults.\n\nViceroy 0.21.0, one inbound request per case:", + "artifact_path": "outbound/EZ-2/acceptance/01/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/01", + "source_id": "EZ-2/acceptance/01", + "source_digest": "887229030d93b6f617636c21091dfbb4a5f2f02b2a9878d8910535cc33a5b701", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "unit/cloudflare", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-01-unit-cloudflare", + "sentinel": "ez_2_acceptance_01_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_2_acceptance_01_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Given a client in router state, another inserted through Fastly `prepare`, and the adapter default, when a handler calls `RequestContext::http_client`, then it receives the router-state client. Given only the `prepare` client and the default, it receives the `prepare` client. The router-state case holds on Axum, Cloudflare, and Spin against their defaults.\n\nViceroy 0.21.0, one inbound request per case:", + "artifact_path": "outbound/EZ-2/acceptance/01/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/01", + "source_id": "EZ-2/acceptance/01", + "source_digest": "887229030d93b6f617636c21091dfbb4a5f2f02b2a9878d8910535cc33a5b701", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "unit/spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-01-unit-spin", + "sentinel": "ez_2_acceptance_01_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_2_acceptance_01_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Given a client in router state, another inserted through Fastly `prepare`, and the adapter default, when a handler calls `RequestContext::http_client`, then it receives the router-state client. Given only the `prepare` client and the default, it receives the `prepare` client. The router-state case holds on Axum, Cloudflare, and Spin against their defaults.\n\nViceroy 0.21.0, one inbound request per case:", + "artifact_path": "outbound/EZ-2/acceptance/01/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/02", + "source_id": "EZ-2/acceptance/02", + "source_digest": "3512ee0bf692318201d419bcf22d51c9e2fe82b52627cb424beffab52145e2e4", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-02-viceroy", + "sentinel": "ez_2_acceptance_02_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-2/acceptance/02", + "expected": "Given one inbound request that runs two batches to one loopback origin with per-call timeouts of 800 ms and cutoffs leaving 640 ms and 655 ms, when both complete, then the EZ-2.13 observation reports one registration and one distinct configuration.", + "artifact_path": "outbound/EZ-2/acceptance/02/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/03", + "source_id": "EZ-2/acceptance/03", + "source_digest": "d3c56eda047e53cf3dc437e8d777d2b288e075f6d4bef0ad3fae1f5c0527379c", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-03-viceroy", + "sentinel": "ez_2_acceptance_03_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-2/acceptance/03", + "expected": "Given one inbound request whose sends differ only in scheme, port, Host authority, TLS settings, or connect timeout, then the observation reports one distinct configuration per variant.", + "artifact_path": "outbound/EZ-2/acceptance/03/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/04", + "source_id": "EZ-2/acceptance/04", + "source_digest": "ff2d56fdb3cfb8b7621ff9ac487de130042da0bcb0befb9147cd83e5f1baf7df", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-04-viceroy", + "sentinel": "ez_2_acceptance_04_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-2/acceptance/04", + "expected": "Given a loopback origin that delays its response head by 500 ms and a request with a 100 ms timeout, when sent, then it fails with 504 and `BudgetSource::PerCallTimeout` between 100 and 105 ms after dispatch. (At F, by reading the code, Viceroy's 75 ms first-byte timer fires first and the request fails with an internal error at about 75 ms.)", + "artifact_path": "outbound/EZ-2/acceptance/04/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/05", + "source_id": "EZ-2/acceptance/05", + "source_digest": "45b3f130d14a102b5342ac7507047c65db1af3580a791bdf0c1f0ced9cf04649", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-05-viceroy", + "sentinel": "ez_2_acceptance_05_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-2/acceptance/05", + "expected": "Given an origin that sends its head at once and then one body byte every 300 ms, and a buffered request with a 500 ms timeout, then it fails with 504 between 500 and 505 ms. Repeat with a pending read of a streamed response; that read fails in the same window. The record verifies readiness handling and reads do not enter an uninterruptible host operation; a late-read fallback does not pass.", + "artifact_path": "outbound/EZ-2/acceptance/05/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/06", + "source_id": "EZ-2/acceptance/06", + "source_digest": "1fcc5198df0f32b84b8ec5990596c57c6ba02015260cd90be6ed7421d2da0e1f", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-06-viceroy", + "sentinel": "ez_2_acceptance_06_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-2/acceptance/06", + "expected": "Given a batch with a 1 s cutoff, one slot to an origin that never answers with a 200 ms timeout, and two fast slots, then the silent slot's 504 item arrives between 200 and 205 ms and the batch completes.\n\nHosted Fastly, single-request mode. Who produces each result follows EZ-Q1.4. EdgeZero produces criteria 1 to 6. Criteria 11, 12, 13b and 14 need settings Trusted Server's configuration cannot express (a controllable slow origin, a 90 s timeout, a one-slot batch to a test origin, a second EdgeZero commit), so EdgeZero provides a qualification build at the pinned commit and TS deploys it to a TS staging service. TS produces criteria 7 to 10 and 13a with its own builds, and criterion 15. The baseline is a Trusted Server build of `main` at `a9037770a` with TS PR #1123, which registers its own stable backends. The candidate is TS's cut build at the EdgeZero commit TS pins. Criteria 7 to 10 replay a fixed page-view mix at a fixed rate with build order randomized, and report p50, p95, p99, and sample counts per POP. A bound measured inside the guest from dispatch or a deadline keeps its stated tolerance on hosted Fastly. The thresholds in criteria 7 to 10 are Trusted Server's proposed product thresholds (Open question 8).", + "artifact_path": "outbound/EZ-2/acceptance/06/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/07", + "source_id": "EZ-2/acceptance/07", + "source_digest": "461a8663bbe5221f89cad36c677551eb7c3f67cf149ce63dd9f40a74630ce366", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-07-hosted-fastly", + "sentinel": "ez_2_acceptance_07_hosted_fastly", + "command": "hosted-runbook EZ-2/acceptance/07 --environment hosted-fastly --sentinel ez_2_acceptance_07_hosted_fastly", + "expected": "**M1, handshakes per bidder per auction.** TS-controlled staging endpoints for one provider and the origin log new TCP connections and full versus resumed TLS handshakes. After a 5-minute warm-up, the candidate's new handshakes per request to the origin and to that provider MUST NOT exceed the baseline's by more than 10 percent.", + "artifact_path": "outbound/EZ-2/acceptance/07/hosted-fastly.json", + "implementation": "planned", + "comparison": true + }, + { + "case_id": "EZ-2/acceptance/08", + "source_id": "EZ-2/acceptance/08", + "source_digest": "b8ac48469b90d46bf20fd01867827606b4ba18cf531c635a598954f6834a9518", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-08-hosted-fastly", + "sentinel": "ez_2_acceptance_08_hosted_fastly", + "command": "hosted-runbook EZ-2/acceptance/08 --environment hosted-fastly --sentinel ez_2_acceptance_08_hosted_fastly", + "expected": "**M2, latency.** Per bidder, the candidate's p95 slot latency and p95 response-head latency MUST each be no more than 10 ms above the baseline's.", + "artifact_path": "outbound/EZ-2/acceptance/08/hosted-fastly.json", + "implementation": "planned", + "comparison": true + }, + { + "case_id": "EZ-2/acceptance/09", + "source_id": "EZ-2/acceptance/09", + "source_digest": "cfd0da378e45cc71e5a2ee8554f384d15afd28d2002262c14a477be049b58ee7", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-09-hosted-fastly", + "sentinel": "ez_2_acceptance_09_hosted_fastly", + "command": "hosted-runbook EZ-2/acceptance/09 --environment hosted-fastly --sentinel ez_2_acceptance_09_hosted_fastly", + "expected": "**M3, registration.** In the candidate, the EZ-2.13 observation for each inbound request MUST show no more registrations than the distinct origin configurations that request used. That count is a pass/fail invariant. The total registration time per inbound request is recorded with its distribution and serves as EZ-3's reference; the duration distribution has no threshold because the pre-cut build does not time its own registrations and the platform sets that latency.", + "artifact_path": "outbound/EZ-2/acceptance/09/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/10", + "source_id": "EZ-2/acceptance/10", + "source_digest": "94a115b099a40a89e22c18dd0b54ab51bada5b9644d35d0f0480c3060557132f", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-10-hosted-fastly", + "sentinel": "ez_2_acceptance_10_hosted_fastly", + "command": "hosted-runbook EZ-2/acceptance/10 --environment hosted-fastly --sentinel ez_2_acceptance_10_hosted_fastly", + "expected": "**M5, pool exhaustion.** At peak replay rate, the candidate's gateway failures, as a share of that origin's requests, MUST be no more than 10 percent above the baseline's share or 0.1 percentage points above it, whichever is larger. Where EZ-16 has landed, the same bound applies to failures whose provider cause is a connection limit.", + "artifact_path": "outbound/EZ-2/acceptance/10/hosted-fastly.json", + "implementation": "planned", + "comparison": true + }, + { + "case_id": "EZ-2/acceptance/10b", + "source_id": "EZ-2/acceptance/10", + "source_digest": "94a115b099a40a89e22c18dd0b54ab51bada5b9644d35d0f0480c3060557132f", + "requirement_ids": ["EZ-2.13", "EZ-16.1"], + "environment": "hosted-fastly", + "applicability": "conditional", + "approval": "EZ-16 present@hosted-fastly", + "condition": "EZ-16 present", + "owner": "EdgeZero", + "task": "F6a", + "substep": "EZ-2-acceptance-10b-hosted-fastly", + "sentinel": "ez_2_acceptance_10b_hosted_fastly", + "command": "hosted-runbook EZ-2/acceptance/10b --environment hosted-fastly --sentinel ez_2_acceptance_10b_hosted_fastly", + "expected": "**M5, pool exhaustion.** At peak replay rate, the candidate's gateway failures, as a share of that origin's requests, MUST be no more than 10 percent above the baseline's share or 0.1 percentage points above it, whichever is larger. Where EZ-16 has landed, the same bound applies to failures whose provider cause is a connection limit.", + "artifact_path": "outbound/EZ-2/acceptance/10b/hosted-fastly.json", + "implementation": "planned", + "comparison": true + }, + { + "case_id": "EZ-2/acceptance/11", + "source_id": "EZ-2/acceptance/11", + "source_digest": "f0b2465164db2d742746a3af293b84481d90ae1547aa2c875e43b433e58b88b1", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-11-hosted-fastly", + "sentinel": "ez_2_acceptance_11_hosted_fastly", + "command": "hosted-runbook EZ-2/acceptance/11 --environment hosted-fastly --sentinel ez_2_acceptance_11_hosted_fastly", + "expected": "**M6, deadline in production.** Criteria 4 and 5 pass against a staging origin with the same delays, with the windows they state.", + "artifact_path": "outbound/EZ-2/acceptance/11/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/12", + "source_id": "EZ-2/acceptance/12", + "source_digest": "3135a3f119c84deeacab2d4da34b6337fa8d1e4ea6d8cd7ea3a6888549562246", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-12-hosted-fastly", + "sentinel": "ez_2_acceptance_12_hosted_fastly", + "command": "hosted-runbook EZ-2/acceptance/12 --environment hosted-fastly --sentinel ez_2_acceptance_12_hosted_fastly", + "expected": "**Long budgets.** A request with a 90 s per-call timeout to an origin that sends its head at 80 s succeeds (EZ-2.7).", + "artifact_path": "outbound/EZ-2/acceptance/12/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/13a", + "source_id": "EZ-2/acceptance/13", + "source_digest": "ae6f8c224e466eb4ec370564d6a5d59b863e17b9c665ec877aa3be94817a3500", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-13a-hosted-fastly", + "sentinel": "ez_2_acceptance_13a_hosted_fastly", + "command": "hosted-runbook EZ-2/acceptance/13a --environment hosted-fastly --sentinel ez_2_acceptance_13a_hosted_fastly", + "expected": "**Rolling deploy.**\n - 13a. While the baseline and the candidate are active together during a staged rollout to one TS staging service, both serve the same origins, the candidate reports zero EZ-2.10 internal errors, and neither build's requests fail at registration.\n - 13b. When the pinned commit changes backend configuration relative to the EdgeZero commit of the previous passing record, the qualification builds of the two commits active together on one staging service both serve the same origins with zero registration failures. The first record has no previous commit, and 13a alone applies.", + "artifact_path": "outbound/EZ-2/acceptance/13a/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/13b", + "source_id": "EZ-2/acceptance/13", + "source_digest": "ae6f8c224e466eb4ec370564d6a5d59b863e17b9c665ec877aa3be94817a3500", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "hosted-fastly", + "applicability": "conditional", + "approval": "previous-record backend configuration changed@hosted-fastly", + "condition": "previous-record backend configuration changed", + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-13b-hosted-fastly", + "sentinel": "ez_2_acceptance_13b_hosted_fastly", + "command": "hosted-runbook EZ-2/acceptance/13b --environment hosted-fastly --sentinel ez_2_acceptance_13b_hosted_fastly", + "expected": "**Rolling deploy.**\n - 13a. While the baseline and the candidate are active together during a staged rollout to one TS staging service, both serve the same origins, the candidate reports zero EZ-2.10 internal errors, and neither build's requests fail at registration.\n - 13b. When the pinned commit changes backend configuration relative to the EdgeZero commit of the previous passing record, the qualification builds of the two commits active together on one staging service both serve the same origins with zero registration failures. The first record has no previous commit, and 13a alone applies.", + "artifact_path": "outbound/EZ-2/acceptance/13b/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/14a", + "source_id": "EZ-2/acceptance/14", + "source_digest": "f608415a86bf3b47cdfe554f11f5ea4d1cee4d1fdf113e93401d135a0750b2d2", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-14a-hosted-fastly", + "sentinel": "ez_2_acceptance_14a_hosted_fastly", + "command": "hosted-runbook EZ-2/acceptance/14a --environment hosted-fastly --sentinel ez_2_acceptance_14a_hosted_fastly", + "expected": "**Caching parity and cross-sandbox pooling, hosted Fastly single-request mode.**\n - 14a. A cacheable GET sent twice with `send`, and twice as a one-slot batch, produces the same number of origin receipts in each mode (one per pair), and the second response of each pair is served from cache in both modes. Viceroy's plain send path does not cache (`viceroy:src/wiggle_abi/req_impl.rs:946-977`).\n - 14b. A separate SDK-only control and the candidate send sequential cache-bypassed requests from fresh single-request executions to a controlled persistent TLS origin with matched transport settings. Record origin connection IDs, execution IDs, new TCP connections, and full/resumed TLS handshakes. The control MUST demonstrate a connection serving requests from distinct executions, and the candidate MUST demonstrate the same cross-execution reuse while satisfying M1's handshake threshold. Choose and approve the sample count, warm-up, settings and zero-baseline rules before running. Same POP and TLS session resumption are not proof of connection reuse. An inconclusive or negative control is not a passing pooling result: it blocks this gate pending provider evidence or an approved requirement change. This criterion does not promote reusable-sandbox M4 or promise that EdgeZero can force reuse where the provider does not support it.\n\nTrusted Server end-to-end check (Viceroy 0.21.0, single-request mode):", + "artifact_path": "outbound/EZ-2/acceptance/14a/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/14b", + "source_id": "EZ-2/acceptance/14", + "source_digest": "f608415a86bf3b47cdfe554f11f5ea4d1cee4d1fdf113e93401d135a0750b2d2", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "hosted-fastly/fresh-single-executions", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-14b-hosted-fastly-fresh-single-executions", + "sentinel": "ez_2_acceptance_14b_hosted_fastly_fresh_single_executions", + "command": "hosted-runbook EZ-2/acceptance/14b --environment hosted-fastly/fresh-single-executions --sentinel ez_2_acceptance_14b_hosted_fastly_fresh_single_executions", + "expected": "**Caching parity and cross-sandbox pooling, hosted Fastly single-request mode.**\n - 14a. A cacheable GET sent twice with `send`, and twice as a one-slot batch, produces the same number of origin receipts in each mode (one per pair), and the second response of each pair is served from cache in both modes. Viceroy's plain send path does not cache (`viceroy:src/wiggle_abi/req_impl.rs:946-977`).\n - 14b. A separate SDK-only control and the candidate send sequential cache-bypassed requests from fresh single-request executions to a controlled persistent TLS origin with matched transport settings. Record origin connection IDs, execution IDs, new TCP connections, and full/resumed TLS handshakes. The control MUST demonstrate a connection serving requests from distinct executions, and the candidate MUST demonstrate the same cross-execution reuse while satisfying M1's handshake threshold. Choose and approve the sample count, warm-up, settings and zero-baseline rules before running. Same POP and TLS session resumption are not proof of connection reuse. An inconclusive or negative control is not a passing pooling result: it blocks this gate pending provider evidence or an approved requirement change. This criterion does not promote reusable-sandbox M4 or promise that EdgeZero can force reuse where the provider does not support it.\n\nTrusted Server end-to-end check (Viceroy 0.21.0, single-request mode):", + "artifact_path": "outbound/EZ-2/acceptance/14b/hosted-fastly/fresh-single-executions.json", + "implementation": "planned", + "comparison": true + }, + { + "case_id": "EZ-2/acceptance/15", + "source_id": "EZ-2/acceptance/15", + "source_digest": "91f60148142ddc5157228a4920ae85e2960df3829f11bde543506333b15238ea", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "consumer-viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "D1b", + "substep": "EZ-2-acceptance-15-consumer-viceroy", + "sentinel": "ez_2_acceptance_15_consumer_viceroy", + "command": "consumer-runbook EZ-2/acceptance/15 --environment consumer-viceroy --sentinel ez_2_acceptance_15_consumer_viceroy", + "expected": "One auction with three bidder hosts, two providers configured on one of them, and one origin fetch reports four registrations for the inbound request.\n\nFollow-up criteria (reusable-sandbox mode). Viceroy 0.21.0 serves at most six requests per reused guest. Cases 16 and 17 stay on one guest. Cases 18 and 19 expect a new guest.", + "artifact_path": "outbound/EZ-2/acceptance/15/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/16", + "source_id": "EZ-2/acceptance/16", + "source_digest": "4a9a6a1d47b1102a7826e285ad38cc6d661fc4edafef592695197f7f7bdc79b5", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "viceroy/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F1", + "substep": "EZ-2-acceptance-16-viceroy-reuse", + "sentinel": "ez_2_acceptance_16_viceroy_reuse", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-2/acceptance/16", + "expected": "Given a reused guest and three sequential requests to the same loopback origin, the EZ-2.18 accounting reports one registration. A fourth request to a second loopback port raises it by one. (At F it reports three after the first three requests.)", + "artifact_path": "outbound/EZ-2/acceptance/16/viceroy/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/17", + "source_id": "EZ-2/acceptance/17", + "source_digest": "79bc6b85426ed1935b21242c88db0f1bdb011e4d2f5953217c2bc1dea0397abc", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "viceroy/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F1", + "substep": "EZ-2-acceptance-17-viceroy-reuse", + "sentinel": "ez_2_acceptance_17_viceroy_reuse", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-2/acceptance/17", + "expected": "Given one reused guest where a request on the adapter default client, a request on a client kept for the sandbox, and a `FastlyOutboundClient::new()` send after delivery all target one origin, the accounting reports one registration.", + "artifact_path": "outbound/EZ-2/acceptance/17/viceroy/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/18", + "source_id": "EZ-2/acceptance/18", + "source_digest": "4a04991431c7ddacfb52e2c237ad1617710864334bf8febecd6a22867536dd0d", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "viceroy/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F1", + "substep": "EZ-2-acceptance-18-viceroy-reuse", + "sentinel": "ez_2_acceptance_18_viceroy_reuse", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-2/acceptance/18", + "expected": "Given a reused guest whose first requests send to 200 distinct loopback ports, when a later request needs a 201st, then it fails with 503 within 5 ms and nothing is sent, and the next request is served by a new guest.", + "artifact_path": "outbound/EZ-2/acceptance/18/viceroy/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/19", + "source_id": "EZ-2/acceptance/19", + "source_digest": "d6fbe5d077899fe90b420a27639ec331c93dcbd7b4532aa6cba9ed667162d9ff", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "viceroy/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F1", + "substep": "EZ-2-acceptance-19-viceroy-reuse", + "sentinel": "ez_2_acceptance_19_viceroy_reuse", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-2/acceptance/19", + "expected": "Given a retirement threshold of 2, when two requests use two loopback ports, then the third request is served by a new guest. Given a callback that asks for retirement on its first request, a new guest serves the second request. In both cases the serving result reports retirement, no error, and the request count served.", + "artifact_path": "outbound/EZ-2/acceptance/19/viceroy/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/20", + "source_id": "EZ-2/acceptance/20", + "source_digest": "27992d17e37ac6a3ddf0eed6c42f74dd2dade227baf9e140c2ab641a111b9e59", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "hosted-fastly/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F1", + "substep": "EZ-2-acceptance-20-hosted-fastly-reuse", + "sentinel": "ez_2_acceptance_20_hosted_fastly_reuse", + "command": "hosted-runbook EZ-2/acceptance/20 --environment hosted-fastly/reuse --sentinel ez_2_acceptance_20_hosted_fastly_reuse", + "expected": "**M4, limit model, hosted Fastly in reuse mode.** Report distinct backends against sandbox ordinal, retirements, 503s from EZ-2.11, and registrations of 50 ms or more. Then run K concurrent sandboxes with the same N origins, and again with disjoint origins, to learn whether identical configurations in different sandboxes share capacity and whether the 200 applies per node or per service. M1 is repeated in reuse mode, and the candidate's new handshakes per request after each sandbox's first 5 requests MUST NOT exceed the baseline's.", + "artifact_path": "outbound/EZ-2/acceptance/20/hosted-fastly/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/21", + "source_id": "EZ-2/acceptance/21", + "source_digest": "ff8c27438e54e6d9983d22ee55afa7dfa9877f1a34eff7faa2fdadc000f90cd4", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "consumer-viceroy/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F1", + "substep": "EZ-2-acceptance-21-consumer-viceroy-reuse", + "sentinel": "ez_2_acceptance_21_consumer_viceroy_reuse", + "command": "consumer-runbook EZ-2/acceptance/21 --environment consumer-viceroy/reuse --sentinel ez_2_acceptance_21_consumer_viceroy_reuse", + "expected": "Trusted Server end to end, Viceroy 0.21.0 in reuse mode. Two sequential auctions with three bidder hosts and one origin register four configurations in total, and the second auction registers none.\n\nAdditional blocking unit criterion, independent of reusable-sandbox enablement:", + "artifact_path": "outbound/EZ-2/acceptance/21/consumer-viceroy/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-2/acceptance/22", + "source_id": "EZ-2/acceptance/22", + "source_digest": "7eea1baac51433ecba2301d152ab884aa9ef32205067de851738f3f5fadb919d", + "requirement_ids": [ + "EZ-2.1", + "EZ-2.2", + "EZ-2.3", + "EZ-2.4", + "EZ-2.5", + "EZ-2.6", + "EZ-2.7", + "EZ-2.8", + "EZ-2.9", + "EZ-2.10", + "EZ-2.11", + "EZ-2.12", + "EZ-2.13", + "EZ-2.14", + "EZ-2.15", + "EZ-2.16", + "EZ-2.17", + "EZ-2.18" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-2-acceptance-22-unit-fastly", + "sentinel": "ez_2_acceptance_22_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_2_acceptance_22_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "**EZ-2.13 warning boundary, any host with injected time and a recording logger.** A backend registration lasting just below 50 ms emits no slow-registration warning; durations of exactly 50 ms and above each emit one record at `warn` when that level is enabled. Cover both successful and failed registration. Assert request-local registration/configuration counts and total/longest durations, and unchanged response status/body/public errors. No query, credential or raw header is logged. This is a deterministic log/observation assertion, not a bound on provider registration latency. EdgeZero supplies this result in the EZ-Q1 record.", + "artifact_path": "outbound/EZ-2/acceptance/22/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-3/acceptance/01", + "source_id": "EZ-3/acceptance/01", + "source_digest": "bde7c23286b51db4131cb9b3e005d3c2d1ffb1ab306b24397084fe03511f7a38", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-3-acceptance-01-unit-fastly", + "sentinel": "ez_3_acceptance_01_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_3_acceptance_01_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Given a 500 ms budget, when more than 25 ms passes inside the send call before dispatch, then the request is sent and no internal error occurs.", + "artifact_path": "outbound/EZ-3/acceptance/01/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-3/acceptance/02", + "source_id": "EZ-3/acceptance/02", + "source_digest": "1463622572eb1df670a3a6c3f66334d49de5e75406472d4475341c47c85fe3a5", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-3-acceptance-02-unit-fastly", + "sentinel": "ez_3_acceptance_02_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_3_acceptance_02_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Given a batch of 20 slots to 20 new origins whose registrations each take 5 ms, and a 1 s cutoff, then all 20 are sent and none fails with an internal error.", + "artifact_path": "outbound/EZ-3/acceptance/02/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-3/acceptance/03", + "source_id": "EZ-3/acceptance/03", + "source_digest": "864c7e401193fbf9340abc45cd65ebf79305be64c3e5a2675888ff2b74d7c5b1", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-3-acceptance-03-unit-fastly", + "sentinel": "ez_3_acceptance_03_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_3_acceptance_03_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Given a batch with a 1 s cutoff where the first slot's registration runs first and takes 20 ms, and the second slot has a 10 ms timeout, then the second slot yields 504 with `BudgetSource::PerCallTimeout`, is not sent, causes no registration, and the batch continues.", + "artifact_path": "outbound/EZ-3/acceptance/03/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-3/acceptance/04", + "source_id": "EZ-3/acceptance/04", + "source_digest": "66d683688e22de748295c9aadf1c91ec5c181e5b00d7ed682c78a581f0f6744b", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-3-acceptance-04-unit-fastly", + "sentinel": "ez_3_acceptance_04_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_3_acceptance_04_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Given a batch ordered new, known, new, known, then both known-origin slots SHOULD be sent before the first registration, the run records the order, and collection restores input-index order. Unit, given slot 1 finishes before slot 0, `next` returns indices 1 then 0 while the collected vector remains indexed 0 then 1.\n\nViceroy 0.21.0:", + "artifact_path": "outbound/EZ-3/acceptance/04/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-3/acceptance/04", + "source_id": "EZ-3/acceptance/04", + "source_digest": "66d683688e22de748295c9aadf1c91ec5c181e5b00d7ed682c78a581f0f6744b", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-3-acceptance-04-unit-core", + "sentinel": "ez_3_acceptance_04_unit_core", + "command": "scripts/run_test_nonzero.sh ez_3_acceptance_04_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Given a batch ordered new, known, new, known, then both known-origin slots SHOULD be sent before the first registration, the run records the order, and collection restores input-index order. Unit, given slot 1 finishes before slot 0, `next` returns indices 1 then 0 while the collected vector remains indexed 0 then 1.\n\nViceroy 0.21.0:", + "artifact_path": "outbound/EZ-3/acceptance/04/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-3/acceptance/05", + "source_id": "EZ-3/acceptance/05", + "source_digest": "e76d30c21f946bd373c8ccf6eadd517bfb856c570a9443d818a88cab58063fc0", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-3-acceptance-05-viceroy", + "sentinel": "ez_3_acceptance_05_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-3/acceptance/05", + "expected": "Given a 1 s budget, more than 25 ms passing before sending, and a loopback origin that answers 200, when the request is sent, then it returns 200. (At F it fails with the dispatch-slack internal error.)", + "artifact_path": "outbound/EZ-3/acceptance/05/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-3/acceptance/06", + "source_id": "EZ-3/acceptance/06", + "source_digest": "1dbf74c0e4f003fd313c63b7997c86680c9acccfcd1de87cf6c66825d5a7002b", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "consumer-viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "D1b", + "substep": "EZ-3-acceptance-06-consumer-viceroy", + "sentinel": "ez_3_acceptance_06_consumer_viceroy", + "command": "consumer-runbook EZ-3/acceptance/06 --environment consumer-viceroy --sentinel ez_3_acceptance_06_consumer_viceroy", + "expected": "Trusted Server end to end, single-request mode. A 12-bidder auction against 12 loopback ports completes with no internal-error slots.\n\nHosted Fastly, single-request mode, with the same baseline and candidate as EZ-2. EdgeZero produces criteria 1 to 5. Trusted Server produces criteria 6 and 7 with its own builds (EZ-Q1.4).", + "artifact_path": "outbound/EZ-3/acceptance/06/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-3/acceptance/07", + "source_id": "EZ-3/acceptance/07", + "source_digest": "dc5854d4c070304ccda96aa723a62650b74b9d5f0cce61de263c57faed9cfd4c", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "consumer-hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "D1b", + "substep": "EZ-3-acceptance-07-consumer-hosted-fastly", + "sentinel": "ez_3_acceptance_07_consumer_hosted_fastly", + "command": "consumer-runbook EZ-3/acceptance/07 --environment consumer-hosted-fastly --sentinel ez_3_acceptance_07_consumer_hosted_fastly", + "expected": "**M7, slack failures.** On the candidate, the number of slots and single sends that fail with an internal error before sending, including DataDome calls, MUST be zero across the replay, reported per 1000 auctions by bidder count. The candidate's per-slot 504 rate MUST NOT exceed the baseline's by more than 1 percentage point. Both bounds MUST hold for a 12-bidder auction profile.", + "artifact_path": "outbound/EZ-3/acceptance/07/consumer-hosted-fastly.json", + "implementation": "planned", + "comparison": true + }, + { + "case_id": "EZ-3/acceptance/08", + "source_id": "EZ-3/acceptance/08", + "source_digest": "d9f604d10e5e05df82845210a2857a04da9823eafbc01499c5bfa11b138941ac", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "unit/axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-3-acceptance-08-unit-axum", + "sentinel": "ez_3_acceptance_08_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_3_acceptance_08_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Unit, every adapter. Given an expired deadline and Image Optimizer options on a send mode where they are Unsupported, the unsupported-option 400 wins without reading the body, registering a backend, or sending. Without the unsupported option, the same request returns the attributed 504.", + "artifact_path": "outbound/EZ-3/acceptance/08/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-3/acceptance/08", + "source_id": "EZ-3/acceptance/08", + "source_digest": "d9f604d10e5e05df82845210a2857a04da9823eafbc01499c5bfa11b138941ac", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-3-acceptance-08-unit-fastly", + "sentinel": "ez_3_acceptance_08_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_3_acceptance_08_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit, every adapter. Given an expired deadline and Image Optimizer options on a send mode where they are Unsupported, the unsupported-option 400 wins without reading the body, registering a backend, or sending. Without the unsupported option, the same request returns the attributed 504.", + "artifact_path": "outbound/EZ-3/acceptance/08/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-3/acceptance/08", + "source_id": "EZ-3/acceptance/08", + "source_digest": "d9f604d10e5e05df82845210a2857a04da9823eafbc01499c5bfa11b138941ac", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "unit/cloudflare", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-3-acceptance-08-unit-cloudflare", + "sentinel": "ez_3_acceptance_08_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_3_acceptance_08_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Unit, every adapter. Given an expired deadline and Image Optimizer options on a send mode where they are Unsupported, the unsupported-option 400 wins without reading the body, registering a backend, or sending. Without the unsupported option, the same request returns the attributed 504.", + "artifact_path": "outbound/EZ-3/acceptance/08/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-3/acceptance/08", + "source_id": "EZ-3/acceptance/08", + "source_digest": "d9f604d10e5e05df82845210a2857a04da9823eafbc01499c5bfa11b138941ac", + "requirement_ids": [ + "EZ-3.1", + "EZ-3.2", + "EZ-3.3", + "EZ-3.4", + "EZ-3.5", + "EZ-3.6", + "EZ-3.7", + "EZ-3.8" + ], + "environment": "unit/spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1b", + "substep": "EZ-3-acceptance-08-unit-spin", + "sentinel": "ez_3_acceptance_08_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_3_acceptance_08_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Unit, every adapter. Given an expired deadline and Image Optimizer options on a send mode where they are Unsupported, the unsupported-option 400 wins without reading the body, registering a backend, or sending. Without the unsupported option, the same request returns the attributed 504.", + "artifact_path": "outbound/EZ-3/acceptance/08/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/01", + "source_id": "EZ-4/acceptance/01", + "source_digest": "066868866bd43fdab1d188702a7d15f84e210f6e030a40964dc9b3fe8f82ee1f", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-01-unit-core", + "sentinel": "ez_4_acceptance_01_unit_core", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_01_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Given a request with a 1 s connect timeout, when it goes through `into_parts` and `from_parts` and through a redirect-hop rebuild, then the connect timeout read back under EZ-4.11 is 1 s. A recording head-first client MUST also observe that setting through the prepared dispatch request without shortening its aggregate budget. Fastly and Spin mapping tests MUST observe the value passed to the provider timeout setter; Axum and Cloudflare retain their documented BestEffort acceptance.", + "artifact_path": "outbound/EZ-4/acceptance/01/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/01", + "source_id": "EZ-4/acceptance/01", + "source_digest": "066868866bd43fdab1d188702a7d15f84e210f6e030a40964dc9b3fe8f82ee1f", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-01-unit-axum", + "sentinel": "ez_4_acceptance_01_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_01_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Given a request with a 1 s connect timeout, when it goes through `into_parts` and `from_parts` and through a redirect-hop rebuild, then the connect timeout read back under EZ-4.11 is 1 s. A recording head-first client MUST also observe that setting through the prepared dispatch request without shortening its aggregate budget. Fastly and Spin mapping tests MUST observe the value passed to the provider timeout setter; Axum and Cloudflare retain their documented BestEffort acceptance.", + "artifact_path": "outbound/EZ-4/acceptance/01/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/01", + "source_id": "EZ-4/acceptance/01", + "source_digest": "066868866bd43fdab1d188702a7d15f84e210f6e030a40964dc9b3fe8f82ee1f", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-01-unit-fastly", + "sentinel": "ez_4_acceptance_01_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_01_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Given a request with a 1 s connect timeout, when it goes through `into_parts` and `from_parts` and through a redirect-hop rebuild, then the connect timeout read back under EZ-4.11 is 1 s. A recording head-first client MUST also observe that setting through the prepared dispatch request without shortening its aggregate budget. Fastly and Spin mapping tests MUST observe the value passed to the provider timeout setter; Axum and Cloudflare retain their documented BestEffort acceptance.", + "artifact_path": "outbound/EZ-4/acceptance/01/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/01", + "source_id": "EZ-4/acceptance/01", + "source_digest": "066868866bd43fdab1d188702a7d15f84e210f6e030a40964dc9b3fe8f82ee1f", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/cloudflare", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-01-unit-cloudflare", + "sentinel": "ez_4_acceptance_01_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_01_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Given a request with a 1 s connect timeout, when it goes through `into_parts` and `from_parts` and through a redirect-hop rebuild, then the connect timeout read back under EZ-4.11 is 1 s. A recording head-first client MUST also observe that setting through the prepared dispatch request without shortening its aggregate budget. Fastly and Spin mapping tests MUST observe the value passed to the provider timeout setter; Axum and Cloudflare retain their documented BestEffort acceptance.", + "artifact_path": "outbound/EZ-4/acceptance/01/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/01", + "source_id": "EZ-4/acceptance/01", + "source_digest": "066868866bd43fdab1d188702a7d15f84e210f6e030a40964dc9b3fe8f82ee1f", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-01-unit-spin", + "sentinel": "ez_4_acceptance_01_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_01_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Given a request with a 1 s connect timeout, when it goes through `into_parts` and `from_parts` and through a redirect-hop rebuild, then the connect timeout read back under EZ-4.11 is 1 s. A recording head-first client MUST also observe that setting through the prepared dispatch request without shortening its aggregate budget. Fastly and Spin mapping tests MUST observe the value passed to the provider timeout setter; Axum and Cloudflare retain their documented BestEffort acceptance.", + "artifact_path": "outbound/EZ-4/acceptance/01/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/02", + "source_id": "EZ-4/acceptance/02", + "source_digest": "c187dcb2f6ad472c8e333e71caf44cbb8b6f443516a93fe4b504e564498e29bc", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-02-unit-axum", + "sentinel": "ez_4_acceptance_02_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_02_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Given a connect timeout of zero, when the request is sent on any adapter, then it fails with 400 before anything is sent.", + "artifact_path": "outbound/EZ-4/acceptance/02/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/02", + "source_id": "EZ-4/acceptance/02", + "source_digest": "c187dcb2f6ad472c8e333e71caf44cbb8b6f443516a93fe4b504e564498e29bc", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-02-unit-fastly", + "sentinel": "ez_4_acceptance_02_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_02_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Given a connect timeout of zero, when the request is sent on any adapter, then it fails with 400 before anything is sent.", + "artifact_path": "outbound/EZ-4/acceptance/02/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/02", + "source_id": "EZ-4/acceptance/02", + "source_digest": "c187dcb2f6ad472c8e333e71caf44cbb8b6f443516a93fe4b504e564498e29bc", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/cloudflare", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-02-unit-cloudflare", + "sentinel": "ez_4_acceptance_02_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_02_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Given a connect timeout of zero, when the request is sent on any adapter, then it fails with 400 before anything is sent.", + "artifact_path": "outbound/EZ-4/acceptance/02/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/02", + "source_id": "EZ-4/acceptance/02", + "source_digest": "c187dcb2f6ad472c8e333e71caf44cbb8b6f443516a93fe4b504e564498e29bc", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-02-unit-spin", + "sentinel": "ez_4_acceptance_02_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_02_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Given a connect timeout of zero, when the request is sent on any adapter, then it fails with 400 before anything is sent.", + "artifact_path": "outbound/EZ-4/acceptance/02/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/03", + "source_id": "EZ-4/acceptance/03", + "source_digest": "a850d0c93c50ca585aa956e693886f807207beabb64a86f4352e7fd188d1ac7a", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-03-unit-spin", + "sentinel": "ez_4_acceptance_03_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_03_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Given each kind of Spin host refusal of the setting, the request completes within its budget and reports no error for the refusal.", + "artifact_path": "outbound/EZ-4/acceptance/03/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/04", + "source_id": "EZ-4/acceptance/04", + "source_digest": "551bee0bcba22ce8549e37fe5dd898d18accb2f72a49a3639833f0ec646732e7", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-04-unit-axum", + "sentinel": "ez_4_acceptance_04_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_04_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Each adapter's capability metadata reports the EZ-4.9 levels.\n\nAxum and workerd:", + "artifact_path": "outbound/EZ-4/acceptance/04/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/04", + "source_id": "EZ-4/acceptance/04", + "source_digest": "551bee0bcba22ce8549e37fe5dd898d18accb2f72a49a3639833f0ec646732e7", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-04-unit-fastly", + "sentinel": "ez_4_acceptance_04_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_04_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Each adapter's capability metadata reports the EZ-4.9 levels.\n\nAxum and workerd:", + "artifact_path": "outbound/EZ-4/acceptance/04/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/04", + "source_id": "EZ-4/acceptance/04", + "source_digest": "551bee0bcba22ce8549e37fe5dd898d18accb2f72a49a3639833f0ec646732e7", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/cloudflare", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-04-unit-cloudflare", + "sentinel": "ez_4_acceptance_04_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_04_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Each adapter's capability metadata reports the EZ-4.9 levels.\n\nAxum and workerd:", + "artifact_path": "outbound/EZ-4/acceptance/04/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/04", + "source_id": "EZ-4/acceptance/04", + "source_digest": "551bee0bcba22ce8549e37fe5dd898d18accb2f72a49a3639833f0ec646732e7", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-04-unit-spin", + "sentinel": "ez_4_acceptance_04_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_04_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Each adapter's capability metadata reports the EZ-4.9 levels.\n\nAxum and workerd:", + "artifact_path": "outbound/EZ-4/acceptance/04/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/05", + "source_id": "EZ-4/acceptance/05", + "source_digest": "0636eb47e6ac5253df6e2c76ab653a81dd927c713d773633a7fe841019ba0f98", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-05-axum", + "sentinel": "ez_4_acceptance_05_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-4/acceptance/05", + "expected": "Given a request with a 1 ms connect timeout to a loopback origin that accepts at once, the request succeeds (BestEffort).\n\nSpin 4.1:", + "artifact_path": "outbound/EZ-4/acceptance/05/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/05", + "source_id": "EZ-4/acceptance/05", + "source_digest": "0636eb47e6ac5253df6e2c76ab653a81dd927c713d773633a7fe841019ba0f98", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-05-workerd", + "sentinel": "ez_4_acceptance_05_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-4/acceptance/05", + "expected": "Given a request with a 1 ms connect timeout to a loopback origin that accepts at once, the request succeeds (BestEffort).\n\nSpin 4.1:", + "artifact_path": "outbound/EZ-4/acceptance/05/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/06", + "source_id": "EZ-4/acceptance/06", + "source_digest": "44d800b520af3959ded322cf0220e146d894ca625fb58d7c95de1a16af69b9ce", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-06-spin", + "sentinel": "ez_4_acceptance_06_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-4/acceptance/06", + "expected": "Given a 200 ms connect timeout to a controlled destination that silently drops connection attempts without rejecting them, and a 2 s budget, the request fails before the budget with the connect-timeout cause, or, if the host ignores the setting, at the budget with the budget's source. The run records the destination's drop policy and which outcome occurred. A destination that immediately refuses or rejects the connection is a separate unreachable-error case, not this timeout fixture.\n\nViceroy 0.21.0:", + "artifact_path": "outbound/EZ-4/acceptance/06/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/07", + "source_id": "EZ-4/acceptance/07", + "source_digest": "1b95d404628e0e78d00bba88c708a6d28684bb43b72a8e236032a5a8e832192c", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-07-viceroy", + "sentinel": "ez_4_acceptance_07_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-4/acceptance/07", + "expected": "Given a loopback origin that accepts at once and sends its head after 900 ms, a request with a 1000 ms timeout and no connect timeout succeeds. (At F it fails at about 750 ms with an internal error.)\n\nHosted Fastly, single-request mode. Viceroy 0.21.0 does not enforce connect timeouts, so these criteria need hosted Fastly. Criterion 8 uses a controlled network fault fixture, so it runs on EdgeZero's qualification build deployed by TS. TS produces criterion 9 with its own builds, against the EZ-2 baseline (EZ-Q1.4). The bounds in criterion 8 are measured inside the guest and keep their stated tolerance.", + "artifact_path": "outbound/EZ-4/acceptance/07/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/08", + "source_id": "EZ-4/acceptance/08", + "source_digest": "34332af3509a84da03cc681f1cb5ba5f7a30a699fff513da29af8c3b9847ca0a", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-08-hosted-fastly", + "sentinel": "ez_4_acceptance_08_hosted_fastly", + "command": "hosted-runbook EZ-4/acceptance/08 --environment hosted-fastly --sentinel ez_4_acceptance_08_hosted_fastly", + "expected": "**M8, connect.** A request to a controlled destination that silently drops connection attempts, with a 200 ms connect timeout and a 2 s total budget, fails with 504 and the connect-timeout cause within 250 ms of dispatch. Without a connect timeout it fails with the budget's source between 2000 and 2050 ms after dispatch. The record MUST identify the fixture and its enforced drop policy; a reserved documentation address alone is not evidence of a silent drop. A separate request to a reachable host's refused port MUST fail as an unreachable gateway error, without requiring it to wait for a timeout.", + "artifact_path": "outbound/EZ-4/acceptance/08/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/09", + "source_id": "EZ-4/acceptance/09", + "source_digest": "90f11d9e4ab068cd40f340efa6e353771740f088299e4c46bbaf718d69835dcf", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "consumer-hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "D1c", + "substep": "EZ-4-acceptance-09-consumer-hosted-fastly", + "sentinel": "ez_4_acceptance_09_consumer_hosted_fastly", + "command": "consumer-runbook EZ-4/acceptance/09 --environment consumer-hosted-fastly --sentinel ez_4_acceptance_09_consumer_hosted_fastly", + "expected": "**Bidder timeouts.** Across the EZ-2 replay, no bidder slot in the candidate fails with a per-call, deadline, or cutoff timeout before its provider timeout has elapsed. The candidate's connect-timeout failures per integration, as a share of that integration's requests, MUST be no more than 0.1 percentage points above the baseline's connection-failure share for the same integration (Open question 3).\n\nTrusted Server end-to-end check:", + "artifact_path": "outbound/EZ-4/acceptance/09/consumer-hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/10", + "source_id": "EZ-4/acceptance/10", + "source_digest": "951041a06bbc35f9f604da543116154f1504fcc831e98fe2de7c985691a454f7", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "consumer-unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "D1c", + "substep": "EZ-4-acceptance-10-consumer-unit-core", + "sentinel": "ez_4_acceptance_10_consumer_unit_core", + "command": "consumer-runbook EZ-4/acceptance/10 --environment consumer-unit/core --sentinel ez_4_acceptance_10_consumer_unit_core", + "expected": "A TS unit test over its request builders reads, under EZ-4.11, a 1 s connect timeout on every origin, integration, and bidder `OutboundRequest`. No client double is needed.", + "artifact_path": "outbound/EZ-4/acceptance/10/consumer-unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-4/acceptance/11", + "source_id": "EZ-4/acceptance/11", + "source_digest": "f03ab43246f52309c44c46c70d8d8ca65215472e52f254a8c2153df86e7d9eda", + "requirement_ids": [ + "EZ-4.1", + "EZ-4.2", + "EZ-4.3", + "EZ-4.4", + "EZ-4.5", + "EZ-4.6", + "EZ-4.7", + "EZ-4.8", + "EZ-4.9", + "EZ-4.10", + "EZ-4.11" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1c", + "substep": "EZ-4-acceptance-11-unit-fastly", + "sentinel": "ez_4_acceptance_11_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_4_acceptance_11_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit, Fastly. A 15 s request with an explicit 1 s connect timeout is eligible for dispatch under EZ-2.10. A connect expiry at 1 s has `BudgetSource::ConnectTimeout`; an expired governing budget wins ties and late observation, retaining its original source. Neither the configured connect timeout nor the error classifier may shorten the response-head budget.", + "artifact_path": "outbound/EZ-4/acceptance/11/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/01", + "source_id": "EZ-5/acceptance/01", + "source_digest": "c306f2e268a02c92092ada94e73660b92fe756f6ab468857a3d49026639a00bf", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-01-unit-core", + "sentinel": "ez_5_acceptance_01_unit_core", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_01_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit.** Given gzip, deflate, and br fixtures that declare `Content-Length`, when they are delivered with the original coding and a decoded cap of 1 byte, then the bytes and both headers are unchanged and no cap error occurs.", + "artifact_path": "outbound/EZ-5/acceptance/01/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/02", + "source_id": "EZ-5/acceptance/02", + "source_digest": "94ee4d08a751a1dc70cdf2d8acc7c33dc69ff428260110e47f2a4194cec6f018", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-02-unit-core", + "sentinel": "ez_5_acceptance_02_unit_core", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_02_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit.** Given the original coding is kept, when the declared length exceeds the encoded cap, then the request fails with the encoded-cap limit before the body is read. When the encoded bytes exceed the final cap in buffered mode, it fails with the final-cap limit (`ResponseLimitReason::BufferedBody` at F).", + "artifact_path": "outbound/EZ-5/acceptance/02/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/03", + "source_id": "EZ-5/acceptance/03", + "source_digest": "488907376c58dd2f67bc2fef17b7d628bfe55332e7bc07e1dc89c521befaa7a5", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-03-unit-core", + "sentinel": "ez_5_acceptance_03_unit_core", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_03_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit.** Given a 206 with `Content-Encoding: gzip` and `Content-Range: bytes 10-19/100`, in decoding mode, then the response arrives undecoded with all three headers unchanged.", + "artifact_path": "outbound/EZ-5/acceptance/03/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/04", + "source_id": "EZ-5/acceptance/04", + "source_digest": "c0de58e1abb89a222ccf7119c77e15b30bc17d55d8f4429c2865fbd783fd93f8", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-04-axum", + "sentinel": "ez_5_acceptance_04_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-5/acceptance/04", + "expected": "**Axum, Viceroy 0.21.0, workerd, Spin 4.1.** Given an origin that returns a gzip body with `Content-Length`, a strong `ETag`, and `Vary: Accept-Encoding`, when the application asks for the original coding, buffered and streamed, then the bytes and all four headers match the origin. In decoding mode, the bytes are decoded. On Spin 4.1 this holds where the run shows the host passes encoded bodies through. If the run shows the host decodes, the run records that, Spin is marked Unsupported, and the check becomes that a request asking for the original coding is rejected before sending under cross-cutting requirement 2 (EZ-5.13).", + "artifact_path": "outbound/EZ-5/acceptance/04/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/04", + "source_id": "EZ-5/acceptance/04", + "source_digest": "c0de58e1abb89a222ccf7119c77e15b30bc17d55d8f4429c2865fbd783fd93f8", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-04-viceroy", + "sentinel": "ez_5_acceptance_04_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-5/acceptance/04", + "expected": "**Axum, Viceroy 0.21.0, workerd, Spin 4.1.** Given an origin that returns a gzip body with `Content-Length`, a strong `ETag`, and `Vary: Accept-Encoding`, when the application asks for the original coding, buffered and streamed, then the bytes and all four headers match the origin. In decoding mode, the bytes are decoded. On Spin 4.1 this holds where the run shows the host passes encoded bodies through. If the run shows the host decodes, the run records that, Spin is marked Unsupported, and the check becomes that a request asking for the original coding is rejected before sending under cross-cutting requirement 2 (EZ-5.13).", + "artifact_path": "outbound/EZ-5/acceptance/04/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/04", + "source_id": "EZ-5/acceptance/04", + "source_digest": "c0de58e1abb89a222ccf7119c77e15b30bc17d55d8f4429c2865fbd783fd93f8", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-04-workerd", + "sentinel": "ez_5_acceptance_04_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-5/acceptance/04", + "expected": "**Axum, Viceroy 0.21.0, workerd, Spin 4.1.** Given an origin that returns a gzip body with `Content-Length`, a strong `ETag`, and `Vary: Accept-Encoding`, when the application asks for the original coding, buffered and streamed, then the bytes and all four headers match the origin. In decoding mode, the bytes are decoded. On Spin 4.1 this holds where the run shows the host passes encoded bodies through. If the run shows the host decodes, the run records that, Spin is marked Unsupported, and the check becomes that a request asking for the original coding is rejected before sending under cross-cutting requirement 2 (EZ-5.13).", + "artifact_path": "outbound/EZ-5/acceptance/04/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/04", + "source_id": "EZ-5/acceptance/04", + "source_digest": "c0de58e1abb89a222ccf7119c77e15b30bc17d55d8f4429c2865fbd783fd93f8", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-04-spin", + "sentinel": "ez_5_acceptance_04_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-5/acceptance/04", + "expected": "**Axum, Viceroy 0.21.0, workerd, Spin 4.1.** Given an origin that returns a gzip body with `Content-Length`, a strong `ETag`, and `Vary: Accept-Encoding`, when the application asks for the original coding, buffered and streamed, then the bytes and all four headers match the origin. In decoding mode, the bytes are decoded. On Spin 4.1 this holds where the run shows the host passes encoded bodies through. If the run shows the host decodes, the run records that, Spin is marked Unsupported, and the check becomes that a request asking for the original coding is rejected before sending under cross-cutting requirement 2 (EZ-5.13).", + "artifact_path": "outbound/EZ-5/acceptance/04/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/05", + "source_id": "EZ-5/acceptance/05", + "source_digest": "fd29f719c2fcf7b06cd28170b84daf1efe3440318f0fbecfb425f0ead404b170", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-05-axum", + "sentinel": "ez_5_acceptance_05_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-5/acceptance/05", + "expected": "**Same environments, with the same Spin condition as criterion 4.** Given the request sets no `Accept-Encoding`, the origin sees `identity`. Given the request sets `gzip, br`, the origin sees exactly that, under both choices. Where Spin is Unsupported, the Spin run checks decoding mode only.", + "artifact_path": "outbound/EZ-5/acceptance/05/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/05", + "source_id": "EZ-5/acceptance/05", + "source_digest": "fd29f719c2fcf7b06cd28170b84daf1efe3440318f0fbecfb425f0ead404b170", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-05-viceroy", + "sentinel": "ez_5_acceptance_05_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-5/acceptance/05", + "expected": "**Same environments, with the same Spin condition as criterion 4.** Given the request sets no `Accept-Encoding`, the origin sees `identity`. Given the request sets `gzip, br`, the origin sees exactly that, under both choices. Where Spin is Unsupported, the Spin run checks decoding mode only.", + "artifact_path": "outbound/EZ-5/acceptance/05/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/05", + "source_id": "EZ-5/acceptance/05", + "source_digest": "fd29f719c2fcf7b06cd28170b84daf1efe3440318f0fbecfb425f0ead404b170", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-05-workerd", + "sentinel": "ez_5_acceptance_05_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-5/acceptance/05", + "expected": "**Same environments, with the same Spin condition as criterion 4.** Given the request sets no `Accept-Encoding`, the origin sees `identity`. Given the request sets `gzip, br`, the origin sees exactly that, under both choices. Where Spin is Unsupported, the Spin run checks decoding mode only.", + "artifact_path": "outbound/EZ-5/acceptance/05/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/05", + "source_id": "EZ-5/acceptance/05", + "source_digest": "fd29f719c2fcf7b06cd28170b84daf1efe3440318f0fbecfb425f0ead404b170", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-05-spin", + "sentinel": "ez_5_acceptance_05_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-5/acceptance/05", + "expected": "**Same environments, with the same Spin condition as criterion 4.** Given the request sets no `Accept-Encoding`, the origin sees `identity`. Given the request sets `gzip, br`, the origin sees exactly that, under both choices. Where Spin is Unsupported, the Spin run checks decoding mode only.", + "artifact_path": "outbound/EZ-5/acceptance/05/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/06", + "source_id": "EZ-5/acceptance/06", + "source_digest": "9c60aa9f37682db3ea495472ec5cdde25c87bfb574f089ba5d7781f1b2c877f5", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-06-viceroy", + "sentinel": "ez_5_acceptance_06_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-5/acceptance/06", + "expected": "**Viceroy 0.21.0.** Given a two-slot batch where one slot keeps the original coding, then that slot's body is encoded and the other slot's body is decoded.", + "artifact_path": "outbound/EZ-5/acceptance/06/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/07", + "source_id": "EZ-5/acceptance/07", + "source_digest": "cbf4d4da84bc30e9f1cb192470d3ca3dc811e86b116db7952c0024bec1932682", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-07-workerd", + "sentinel": "ez_5_acceptance_07_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-5/acceptance/07", + "expected": "**workerd.** Given the application relays a passthrough response to its client, then the client receives the exact compressed bytes and `Content-Encoding`, and Workers does not re-encode them. The run records whether the origin `Content-Length` reached the client, and the caveat at `edgezero:docs/guide/capabilities.md:422-426` is updated to match.", + "artifact_path": "outbound/EZ-5/acceptance/07/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/08", + "source_id": "EZ-5/acceptance/08", + "source_digest": "ef68cf0d7258b386994f6abd9aaa37f2a3b56d3db8bfb9c68f74e79d17a01ece", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "consumer-viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "P3", + "substep": "EZ-5-acceptance-08-consumer-viceroy", + "sentinel": "ez_5_acceptance_08_consumer_viceroy", + "command": "consumer-runbook EZ-5/acceptance/08 --environment consumer-viceroy --sentinel ez_5_acceptance_08_consumer_viceroy", + "expected": "**Trusted Server, Viceroy 0.21.0.** Run the TS integration suite against origin fixtures.\n - A gzip HTML page reaches the client with `Content-Encoding: gzip` and decodes to the rewritten document with the TSJS tag.\n - A br SVG relay is byte-identical, with the origin `Content-Length` and `ETag`.\n - `Range: bytes=0-99` against a gzip-stored asset returns 206 with the origin `Content-Range` and `Content-Length: 100`.\n - A JS asset revalidation with `If-None-Match` returns 304.\n - A `/first-party/proxy` HTML creative keeps `Content-Encoding: br`.", + "artifact_path": "outbound/EZ-5/acceptance/08/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/09", + "source_id": "EZ-5/acceptance/09", + "source_digest": "dc59f01847a83284dc02103e7a81568b8d7bf68439bc659252d8ec58d0617b2f", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-09-unit-axum", + "sentinel": "ez_5_acceptance_09_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_09_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "**Unit, each adapter's response path.** A HEAD or 304 with `Content-Length: 1073741824` and a 64 KiB encoded cap succeeds without polling a body source under either decoding choice. With original coding requested, its representation length and coding remain unchanged; decoding mode follows EZ-5.9.", + "artifact_path": "outbound/EZ-5/acceptance/09/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/09b", + "source_id": "EZ-5/acceptance/09", + "source_digest": "dc59f01847a83284dc02103e7a81568b8d7bf68439bc659252d8ec58d0617b2f", + "requirement_ids": ["EZ-5.9"], + "environment": "unit/axum", + "applicability": "conditional", + "approval": "EZ-5.9 adopted@unit/axum", + "condition": "EZ-5.9 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-09b-unit-axum", + "sentinel": "ez_5_acceptance_09b_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_09b_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "**Unit, each adapter's response path.** A HEAD or 304 with `Content-Length: 1073741824` and a 64 KiB encoded cap succeeds without polling a body source under either decoding choice. With original coding requested, its representation length and coding remain unchanged; decoding mode follows EZ-5.9.", + "artifact_path": "outbound/EZ-5/acceptance/09b/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/09", + "source_id": "EZ-5/acceptance/09", + "source_digest": "dc59f01847a83284dc02103e7a81568b8d7bf68439bc659252d8ec58d0617b2f", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-09-unit-fastly", + "sentinel": "ez_5_acceptance_09_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_09_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "**Unit, each adapter's response path.** A HEAD or 304 with `Content-Length: 1073741824` and a 64 KiB encoded cap succeeds without polling a body source under either decoding choice. With original coding requested, its representation length and coding remain unchanged; decoding mode follows EZ-5.9.", + "artifact_path": "outbound/EZ-5/acceptance/09/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/09b", + "source_id": "EZ-5/acceptance/09", + "source_digest": "dc59f01847a83284dc02103e7a81568b8d7bf68439bc659252d8ec58d0617b2f", + "requirement_ids": ["EZ-5.9"], + "environment": "unit/fastly", + "applicability": "conditional", + "approval": "EZ-5.9 adopted@unit/fastly", + "condition": "EZ-5.9 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-09b-unit-fastly", + "sentinel": "ez_5_acceptance_09b_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_09b_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "**Unit, each adapter's response path.** A HEAD or 304 with `Content-Length: 1073741824` and a 64 KiB encoded cap succeeds without polling a body source under either decoding choice. With original coding requested, its representation length and coding remain unchanged; decoding mode follows EZ-5.9.", + "artifact_path": "outbound/EZ-5/acceptance/09b/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/09", + "source_id": "EZ-5/acceptance/09", + "source_digest": "dc59f01847a83284dc02103e7a81568b8d7bf68439bc659252d8ec58d0617b2f", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/cloudflare", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-09-unit-cloudflare", + "sentinel": "ez_5_acceptance_09_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_09_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "**Unit, each adapter's response path.** A HEAD or 304 with `Content-Length: 1073741824` and a 64 KiB encoded cap succeeds without polling a body source under either decoding choice. With original coding requested, its representation length and coding remain unchanged; decoding mode follows EZ-5.9.", + "artifact_path": "outbound/EZ-5/acceptance/09/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/09b", + "source_id": "EZ-5/acceptance/09", + "source_digest": "dc59f01847a83284dc02103e7a81568b8d7bf68439bc659252d8ec58d0617b2f", + "requirement_ids": ["EZ-5.9"], + "environment": "unit/cloudflare", + "applicability": "conditional", + "approval": "EZ-5.9 adopted@unit/cloudflare", + "condition": "EZ-5.9 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-09b-unit-cloudflare", + "sentinel": "ez_5_acceptance_09b_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_09b_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "**Unit, each adapter's response path.** A HEAD or 304 with `Content-Length: 1073741824` and a 64 KiB encoded cap succeeds without polling a body source under either decoding choice. With original coding requested, its representation length and coding remain unchanged; decoding mode follows EZ-5.9.", + "artifact_path": "outbound/EZ-5/acceptance/09b/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/09", + "source_id": "EZ-5/acceptance/09", + "source_digest": "dc59f01847a83284dc02103e7a81568b8d7bf68439bc659252d8ec58d0617b2f", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-09-unit-spin", + "sentinel": "ez_5_acceptance_09_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_09_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "**Unit, each adapter's response path.** A HEAD or 304 with `Content-Length: 1073741824` and a 64 KiB encoded cap succeeds without polling a body source under either decoding choice. With original coding requested, its representation length and coding remain unchanged; decoding mode follows EZ-5.9.", + "artifact_path": "outbound/EZ-5/acceptance/09/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/09b", + "source_id": "EZ-5/acceptance/09", + "source_digest": "dc59f01847a83284dc02103e7a81568b8d7bf68439bc659252d8ec58d0617b2f", + "requirement_ids": ["EZ-5.9"], + "environment": "unit/spin", + "applicability": "conditional", + "approval": "EZ-5.9 adopted@unit/spin", + "condition": "EZ-5.9 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-09b-unit-spin", + "sentinel": "ez_5_acceptance_09b_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_09b_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "**Unit, each adapter's response path.** A HEAD or 304 with `Content-Length: 1073741824` and a 64 KiB encoded cap succeeds without polling a body source under either decoding choice. With original coding requested, its representation length and coding remain unchanged; decoding mode follows EZ-5.9.", + "artifact_path": "outbound/EZ-5/acceptance/09b/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/10", + "source_id": "EZ-5/acceptance/10", + "source_digest": "81779bccd5ea005659ad44dd5de6baad8d6d438e25397ecc5522bb6873ba02af", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-10-unit-core", + "sentinel": "ez_5_acceptance_10_unit_core", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_10_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit and each adapter's response-policy path.** An identity payload with decoded cap 10 and encoded/final caps 20 rejects `Content-Length: 11` before polling the source and rejects 11 observed bytes when no length is declared. Both decoding choices return `ResponseLimitReason::DecodedBody`. A 10-byte control succeeds under both choices. A gzip body kept in original mode remains exempt from the decoded cap, while its encoded and final caps still apply.", + "artifact_path": "outbound/EZ-5/acceptance/10/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/10", + "source_id": "EZ-5/acceptance/10", + "source_digest": "81779bccd5ea005659ad44dd5de6baad8d6d438e25397ecc5522bb6873ba02af", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-10-unit-axum", + "sentinel": "ez_5_acceptance_10_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_10_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "**Unit and each adapter's response-policy path.** An identity payload with decoded cap 10 and encoded/final caps 20 rejects `Content-Length: 11` before polling the source and rejects 11 observed bytes when no length is declared. Both decoding choices return `ResponseLimitReason::DecodedBody`. A 10-byte control succeeds under both choices. A gzip body kept in original mode remains exempt from the decoded cap, while its encoded and final caps still apply.", + "artifact_path": "outbound/EZ-5/acceptance/10/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/10", + "source_id": "EZ-5/acceptance/10", + "source_digest": "81779bccd5ea005659ad44dd5de6baad8d6d438e25397ecc5522bb6873ba02af", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-10-unit-fastly", + "sentinel": "ez_5_acceptance_10_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_10_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "**Unit and each adapter's response-policy path.** An identity payload with decoded cap 10 and encoded/final caps 20 rejects `Content-Length: 11` before polling the source and rejects 11 observed bytes when no length is declared. Both decoding choices return `ResponseLimitReason::DecodedBody`. A 10-byte control succeeds under both choices. A gzip body kept in original mode remains exempt from the decoded cap, while its encoded and final caps still apply.", + "artifact_path": "outbound/EZ-5/acceptance/10/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/10", + "source_id": "EZ-5/acceptance/10", + "source_digest": "81779bccd5ea005659ad44dd5de6baad8d6d438e25397ecc5522bb6873ba02af", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/cloudflare", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-10-unit-cloudflare", + "sentinel": "ez_5_acceptance_10_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_10_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "**Unit and each adapter's response-policy path.** An identity payload with decoded cap 10 and encoded/final caps 20 rejects `Content-Length: 11` before polling the source and rejects 11 observed bytes when no length is declared. Both decoding choices return `ResponseLimitReason::DecodedBody`. A 10-byte control succeeds under both choices. A gzip body kept in original mode remains exempt from the decoded cap, while its encoded and final caps still apply.", + "artifact_path": "outbound/EZ-5/acceptance/10/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/10", + "source_id": "EZ-5/acceptance/10", + "source_digest": "81779bccd5ea005659ad44dd5de6baad8d6d438e25397ecc5522bb6873ba02af", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-10-unit-spin", + "sentinel": "ez_5_acceptance_10_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_10_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "**Unit and each adapter's response-policy path.** An identity payload with decoded cap 10 and encoded/final caps 20 rejects `Content-Length: 11` before polling the source and rejects 11 observed bytes when no length is declared. Both decoding choices return `ResponseLimitReason::DecodedBody`. A 10-byte control succeeds under both choices. A gzip body kept in original mode remains exempt from the decoded cap, while its encoded and final caps still apply.", + "artifact_path": "outbound/EZ-5/acceptance/10/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/11", + "source_id": "EZ-5/acceptance/11", + "source_digest": "1d3648f27ff90f9c9635d63b43d0730fe62082725e04c1817fa42991cf5b0794", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-11-unit-core", + "sentinel": "ez_5_acceptance_11_unit_core", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_11_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit and each adapter's actual normalization/response-policy path, both decoding choices and both response modes.** `Connection: content-encoding, content-length, etag` removes those fields before coding/length interpretation; a nominated malformed length does not become a framing error, and removed fields are never restored. A separate non-UTF-8 `ETag` is dropped. Missing or nominated-away coding is classified as identity and retains its decoded cap. Malformed surviving `Content-Length` still fails, and non-UTF-8 `Content-Encoding` retains F's passthrough classification rather than being silently dropped. Valid surviving representation fields remain unchanged in original mode and on a 206. A 204 still removes length and suppresses its body; a 205 still normalizes length to zero and preserves F's declared-body evidence and disposition; HEAD/304 keep bodyless handling without payload-cap rejection or body polling. EZ-5.9's separately adopted metadata change is the only permitted HEAD/304 difference. Keep F's `response_normalization_precedence_table` assertions unchanged. Provider-inexpressible raw fields are injected at the adapter-visible/shared-head unit boundary; provider-visible controls retain the existing header-fidelity limitations. These cases make no new raw-header fidelity claim.", + "artifact_path": "outbound/EZ-5/acceptance/11/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/11b", + "source_id": "EZ-5/acceptance/11", + "source_digest": "1d3648f27ff90f9c9635d63b43d0730fe62082725e04c1817fa42991cf5b0794", + "requirement_ids": ["EZ-5.9"], + "environment": "unit/core", + "applicability": "conditional", + "approval": "EZ-5.9 adopted@unit/core", + "condition": "EZ-5.9 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-11b-unit-core", + "sentinel": "ez_5_acceptance_11b_unit_core", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_11b_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit and each adapter's actual normalization/response-policy path, both decoding choices and both response modes.** `Connection: content-encoding, content-length, etag` removes those fields before coding/length interpretation; a nominated malformed length does not become a framing error, and removed fields are never restored. A separate non-UTF-8 `ETag` is dropped. Missing or nominated-away coding is classified as identity and retains its decoded cap. Malformed surviving `Content-Length` still fails, and non-UTF-8 `Content-Encoding` retains F's passthrough classification rather than being silently dropped. Valid surviving representation fields remain unchanged in original mode and on a 206. A 204 still removes length and suppresses its body; a 205 still normalizes length to zero and preserves F's declared-body evidence and disposition; HEAD/304 keep bodyless handling without payload-cap rejection or body polling. EZ-5.9's separately adopted metadata change is the only permitted HEAD/304 difference. Keep F's `response_normalization_precedence_table` assertions unchanged. Provider-inexpressible raw fields are injected at the adapter-visible/shared-head unit boundary; provider-visible controls retain the existing header-fidelity limitations. These cases make no new raw-header fidelity claim.", + "artifact_path": "outbound/EZ-5/acceptance/11b/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/11", + "source_id": "EZ-5/acceptance/11", + "source_digest": "1d3648f27ff90f9c9635d63b43d0730fe62082725e04c1817fa42991cf5b0794", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-11-unit-axum", + "sentinel": "ez_5_acceptance_11_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_11_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "**Unit and each adapter's actual normalization/response-policy path, both decoding choices and both response modes.** `Connection: content-encoding, content-length, etag` removes those fields before coding/length interpretation; a nominated malformed length does not become a framing error, and removed fields are never restored. A separate non-UTF-8 `ETag` is dropped. Missing or nominated-away coding is classified as identity and retains its decoded cap. Malformed surviving `Content-Length` still fails, and non-UTF-8 `Content-Encoding` retains F's passthrough classification rather than being silently dropped. Valid surviving representation fields remain unchanged in original mode and on a 206. A 204 still removes length and suppresses its body; a 205 still normalizes length to zero and preserves F's declared-body evidence and disposition; HEAD/304 keep bodyless handling without payload-cap rejection or body polling. EZ-5.9's separately adopted metadata change is the only permitted HEAD/304 difference. Keep F's `response_normalization_precedence_table` assertions unchanged. Provider-inexpressible raw fields are injected at the adapter-visible/shared-head unit boundary; provider-visible controls retain the existing header-fidelity limitations. These cases make no new raw-header fidelity claim.", + "artifact_path": "outbound/EZ-5/acceptance/11/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/11b", + "source_id": "EZ-5/acceptance/11", + "source_digest": "1d3648f27ff90f9c9635d63b43d0730fe62082725e04c1817fa42991cf5b0794", + "requirement_ids": ["EZ-5.9"], + "environment": "unit/axum", + "applicability": "conditional", + "approval": "EZ-5.9 adopted@unit/axum", + "condition": "EZ-5.9 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-11b-unit-axum", + "sentinel": "ez_5_acceptance_11b_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_11b_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "**Unit and each adapter's actual normalization/response-policy path, both decoding choices and both response modes.** `Connection: content-encoding, content-length, etag` removes those fields before coding/length interpretation; a nominated malformed length does not become a framing error, and removed fields are never restored. A separate non-UTF-8 `ETag` is dropped. Missing or nominated-away coding is classified as identity and retains its decoded cap. Malformed surviving `Content-Length` still fails, and non-UTF-8 `Content-Encoding` retains F's passthrough classification rather than being silently dropped. Valid surviving representation fields remain unchanged in original mode and on a 206. A 204 still removes length and suppresses its body; a 205 still normalizes length to zero and preserves F's declared-body evidence and disposition; HEAD/304 keep bodyless handling without payload-cap rejection or body polling. EZ-5.9's separately adopted metadata change is the only permitted HEAD/304 difference. Keep F's `response_normalization_precedence_table` assertions unchanged. Provider-inexpressible raw fields are injected at the adapter-visible/shared-head unit boundary; provider-visible controls retain the existing header-fidelity limitations. These cases make no new raw-header fidelity claim.", + "artifact_path": "outbound/EZ-5/acceptance/11b/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/11", + "source_id": "EZ-5/acceptance/11", + "source_digest": "1d3648f27ff90f9c9635d63b43d0730fe62082725e04c1817fa42991cf5b0794", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-11-unit-fastly", + "sentinel": "ez_5_acceptance_11_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_11_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "**Unit and each adapter's actual normalization/response-policy path, both decoding choices and both response modes.** `Connection: content-encoding, content-length, etag` removes those fields before coding/length interpretation; a nominated malformed length does not become a framing error, and removed fields are never restored. A separate non-UTF-8 `ETag` is dropped. Missing or nominated-away coding is classified as identity and retains its decoded cap. Malformed surviving `Content-Length` still fails, and non-UTF-8 `Content-Encoding` retains F's passthrough classification rather than being silently dropped. Valid surviving representation fields remain unchanged in original mode and on a 206. A 204 still removes length and suppresses its body; a 205 still normalizes length to zero and preserves F's declared-body evidence and disposition; HEAD/304 keep bodyless handling without payload-cap rejection or body polling. EZ-5.9's separately adopted metadata change is the only permitted HEAD/304 difference. Keep F's `response_normalization_precedence_table` assertions unchanged. Provider-inexpressible raw fields are injected at the adapter-visible/shared-head unit boundary; provider-visible controls retain the existing header-fidelity limitations. These cases make no new raw-header fidelity claim.", + "artifact_path": "outbound/EZ-5/acceptance/11/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/11b", + "source_id": "EZ-5/acceptance/11", + "source_digest": "1d3648f27ff90f9c9635d63b43d0730fe62082725e04c1817fa42991cf5b0794", + "requirement_ids": ["EZ-5.9"], + "environment": "unit/fastly", + "applicability": "conditional", + "approval": "EZ-5.9 adopted@unit/fastly", + "condition": "EZ-5.9 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-11b-unit-fastly", + "sentinel": "ez_5_acceptance_11b_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_11b_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "**Unit and each adapter's actual normalization/response-policy path, both decoding choices and both response modes.** `Connection: content-encoding, content-length, etag` removes those fields before coding/length interpretation; a nominated malformed length does not become a framing error, and removed fields are never restored. A separate non-UTF-8 `ETag` is dropped. Missing or nominated-away coding is classified as identity and retains its decoded cap. Malformed surviving `Content-Length` still fails, and non-UTF-8 `Content-Encoding` retains F's passthrough classification rather than being silently dropped. Valid surviving representation fields remain unchanged in original mode and on a 206. A 204 still removes length and suppresses its body; a 205 still normalizes length to zero and preserves F's declared-body evidence and disposition; HEAD/304 keep bodyless handling without payload-cap rejection or body polling. EZ-5.9's separately adopted metadata change is the only permitted HEAD/304 difference. Keep F's `response_normalization_precedence_table` assertions unchanged. Provider-inexpressible raw fields are injected at the adapter-visible/shared-head unit boundary; provider-visible controls retain the existing header-fidelity limitations. These cases make no new raw-header fidelity claim.", + "artifact_path": "outbound/EZ-5/acceptance/11b/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/11", + "source_id": "EZ-5/acceptance/11", + "source_digest": "1d3648f27ff90f9c9635d63b43d0730fe62082725e04c1817fa42991cf5b0794", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/cloudflare", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-11-unit-cloudflare", + "sentinel": "ez_5_acceptance_11_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_11_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "**Unit and each adapter's actual normalization/response-policy path, both decoding choices and both response modes.** `Connection: content-encoding, content-length, etag` removes those fields before coding/length interpretation; a nominated malformed length does not become a framing error, and removed fields are never restored. A separate non-UTF-8 `ETag` is dropped. Missing or nominated-away coding is classified as identity and retains its decoded cap. Malformed surviving `Content-Length` still fails, and non-UTF-8 `Content-Encoding` retains F's passthrough classification rather than being silently dropped. Valid surviving representation fields remain unchanged in original mode and on a 206. A 204 still removes length and suppresses its body; a 205 still normalizes length to zero and preserves F's declared-body evidence and disposition; HEAD/304 keep bodyless handling without payload-cap rejection or body polling. EZ-5.9's separately adopted metadata change is the only permitted HEAD/304 difference. Keep F's `response_normalization_precedence_table` assertions unchanged. Provider-inexpressible raw fields are injected at the adapter-visible/shared-head unit boundary; provider-visible controls retain the existing header-fidelity limitations. These cases make no new raw-header fidelity claim.", + "artifact_path": "outbound/EZ-5/acceptance/11/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/11b", + "source_id": "EZ-5/acceptance/11", + "source_digest": "1d3648f27ff90f9c9635d63b43d0730fe62082725e04c1817fa42991cf5b0794", + "requirement_ids": ["EZ-5.9"], + "environment": "unit/cloudflare", + "applicability": "conditional", + "approval": "EZ-5.9 adopted@unit/cloudflare", + "condition": "EZ-5.9 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-11b-unit-cloudflare", + "sentinel": "ez_5_acceptance_11b_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_11b_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "**Unit and each adapter's actual normalization/response-policy path, both decoding choices and both response modes.** `Connection: content-encoding, content-length, etag` removes those fields before coding/length interpretation; a nominated malformed length does not become a framing error, and removed fields are never restored. A separate non-UTF-8 `ETag` is dropped. Missing or nominated-away coding is classified as identity and retains its decoded cap. Malformed surviving `Content-Length` still fails, and non-UTF-8 `Content-Encoding` retains F's passthrough classification rather than being silently dropped. Valid surviving representation fields remain unchanged in original mode and on a 206. A 204 still removes length and suppresses its body; a 205 still normalizes length to zero and preserves F's declared-body evidence and disposition; HEAD/304 keep bodyless handling without payload-cap rejection or body polling. EZ-5.9's separately adopted metadata change is the only permitted HEAD/304 difference. Keep F's `response_normalization_precedence_table` assertions unchanged. Provider-inexpressible raw fields are injected at the adapter-visible/shared-head unit boundary; provider-visible controls retain the existing header-fidelity limitations. These cases make no new raw-header fidelity claim.", + "artifact_path": "outbound/EZ-5/acceptance/11b/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/11", + "source_id": "EZ-5/acceptance/11", + "source_digest": "1d3648f27ff90f9c9635d63b43d0730fe62082725e04c1817fa42991cf5b0794", + "requirement_ids": [ + "EZ-5.1", + "EZ-5.2", + "EZ-5.3", + "EZ-5.4", + "EZ-5.5", + "EZ-5.6", + "EZ-5.7", + "EZ-5.8", + "EZ-5.11", + "EZ-5.12", + "EZ-5.13", + "EZ-5.14" + ], + "environment": "unit/spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-11-unit-spin", + "sentinel": "ez_5_acceptance_11_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_11_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "**Unit and each adapter's actual normalization/response-policy path, both decoding choices and both response modes.** `Connection: content-encoding, content-length, etag` removes those fields before coding/length interpretation; a nominated malformed length does not become a framing error, and removed fields are never restored. A separate non-UTF-8 `ETag` is dropped. Missing or nominated-away coding is classified as identity and retains its decoded cap. Malformed surviving `Content-Length` still fails, and non-UTF-8 `Content-Encoding` retains F's passthrough classification rather than being silently dropped. Valid surviving representation fields remain unchanged in original mode and on a 206. A 204 still removes length and suppresses its body; a 205 still normalizes length to zero and preserves F's declared-body evidence and disposition; HEAD/304 keep bodyless handling without payload-cap rejection or body polling. EZ-5.9's separately adopted metadata change is the only permitted HEAD/304 difference. Keep F's `response_normalization_precedence_table` assertions unchanged. Provider-inexpressible raw fields are injected at the adapter-visible/shared-head unit boundary; provider-visible controls retain the existing header-fidelity limitations. These cases make no new raw-header fidelity claim.", + "artifact_path": "outbound/EZ-5/acceptance/11/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/11b", + "source_id": "EZ-5/acceptance/11", + "source_digest": "1d3648f27ff90f9c9635d63b43d0730fe62082725e04c1817fa42991cf5b0794", + "requirement_ids": ["EZ-5.9"], + "environment": "unit/spin", + "applicability": "conditional", + "approval": "EZ-5.9 adopted@unit/spin", + "condition": "EZ-5.9 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-11b-unit-spin", + "sentinel": "ez_5_acceptance_11b_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_11b_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "**Unit and each adapter's actual normalization/response-policy path, both decoding choices and both response modes.** `Connection: content-encoding, content-length, etag` removes those fields before coding/length interpretation; a nominated malformed length does not become a framing error, and removed fields are never restored. A separate non-UTF-8 `ETag` is dropped. Missing or nominated-away coding is classified as identity and retains its decoded cap. Malformed surviving `Content-Length` still fails, and non-UTF-8 `Content-Encoding` retains F's passthrough classification rather than being silently dropped. Valid surviving representation fields remain unchanged in original mode and on a 206. A 204 still removes length and suppresses its body; a 205 still normalizes length to zero and preserves F's declared-body evidence and disposition; HEAD/304 keep bodyless handling without payload-cap rejection or body polling. EZ-5.9's separately adopted metadata change is the only permitted HEAD/304 difference. Keep F's `response_normalization_precedence_table` assertions unchanged. Provider-inexpressible raw fields are injected at the adapter-visible/shared-head unit boundary; provider-visible controls retain the existing header-fidelity limitations. These cases make no new raw-header fidelity claim.", + "artifact_path": "outbound/EZ-5/acceptance/11b/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/12", + "source_id": "EZ-5/acceptance/12", + "source_digest": "955c0ef5b1050edb9d6d6825205e5924daecaad2213736f77e15721cd50098b5", + "requirement_ids": ["EZ-5.10"], + "environment": "unit/core", + "applicability": "conditional", + "approval": "EZ-5.10 adopted@unit/core", + "condition": "EZ-5.10 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-12-unit-core", + "sentinel": "ez_5_acceptance_12_unit_core", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_12_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Conditional, only if EZ-5.10 is adopted; unit and each adapter's actual response-policy path.** A decoded gzip/deflate/br body with a surviving strong ETag, Accept-Ranges and Content-Digest weakens the ETag and removes the other two fields. An already-weak ETag stays weak without double-prefixing. Original coding, 206 and nondecoded controls retain their surviving metadata; fields removed by prior normalization never reappear. Record EZ-5.9 and EZ-5.10 adoption independently, including each applicable environment leg, rather than treating this criterion as implicit promotion.", + "artifact_path": "outbound/EZ-5/acceptance/12/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/12", + "source_id": "EZ-5/acceptance/12", + "source_digest": "955c0ef5b1050edb9d6d6825205e5924daecaad2213736f77e15721cd50098b5", + "requirement_ids": ["EZ-5.10"], + "environment": "unit/axum", + "applicability": "conditional", + "approval": "EZ-5.10 adopted@unit/axum", + "condition": "EZ-5.10 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-12-unit-axum", + "sentinel": "ez_5_acceptance_12_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_12_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "**Conditional, only if EZ-5.10 is adopted; unit and each adapter's actual response-policy path.** A decoded gzip/deflate/br body with a surviving strong ETag, Accept-Ranges and Content-Digest weakens the ETag and removes the other two fields. An already-weak ETag stays weak without double-prefixing. Original coding, 206 and nondecoded controls retain their surviving metadata; fields removed by prior normalization never reappear. Record EZ-5.9 and EZ-5.10 adoption independently, including each applicable environment leg, rather than treating this criterion as implicit promotion.", + "artifact_path": "outbound/EZ-5/acceptance/12/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/12", + "source_id": "EZ-5/acceptance/12", + "source_digest": "955c0ef5b1050edb9d6d6825205e5924daecaad2213736f77e15721cd50098b5", + "requirement_ids": ["EZ-5.10"], + "environment": "unit/fastly", + "applicability": "conditional", + "approval": "EZ-5.10 adopted@unit/fastly", + "condition": "EZ-5.10 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-12-unit-fastly", + "sentinel": "ez_5_acceptance_12_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_12_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "**Conditional, only if EZ-5.10 is adopted; unit and each adapter's actual response-policy path.** A decoded gzip/deflate/br body with a surviving strong ETag, Accept-Ranges and Content-Digest weakens the ETag and removes the other two fields. An already-weak ETag stays weak without double-prefixing. Original coding, 206 and nondecoded controls retain their surviving metadata; fields removed by prior normalization never reappear. Record EZ-5.9 and EZ-5.10 adoption independently, including each applicable environment leg, rather than treating this criterion as implicit promotion.", + "artifact_path": "outbound/EZ-5/acceptance/12/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/12", + "source_id": "EZ-5/acceptance/12", + "source_digest": "955c0ef5b1050edb9d6d6825205e5924daecaad2213736f77e15721cd50098b5", + "requirement_ids": ["EZ-5.10"], + "environment": "unit/cloudflare", + "applicability": "conditional", + "approval": "EZ-5.10 adopted@unit/cloudflare", + "condition": "EZ-5.10 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-12-unit-cloudflare", + "sentinel": "ez_5_acceptance_12_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_12_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "**Conditional, only if EZ-5.10 is adopted; unit and each adapter's actual response-policy path.** A decoded gzip/deflate/br body with a surviving strong ETag, Accept-Ranges and Content-Digest weakens the ETag and removes the other two fields. An already-weak ETag stays weak without double-prefixing. Original coding, 206 and nondecoded controls retain their surviving metadata; fields removed by prior normalization never reappear. Record EZ-5.9 and EZ-5.10 adoption independently, including each applicable environment leg, rather than treating this criterion as implicit promotion.", + "artifact_path": "outbound/EZ-5/acceptance/12/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-5/acceptance/12", + "source_id": "EZ-5/acceptance/12", + "source_digest": "955c0ef5b1050edb9d6d6825205e5924daecaad2213736f77e15721cd50098b5", + "requirement_ids": ["EZ-5.10"], + "environment": "unit/spin", + "applicability": "conditional", + "approval": "EZ-5.10 adopted@unit/spin", + "condition": "EZ-5.10 adopted", + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-5-acceptance-12-unit-spin", + "sentinel": "ez_5_acceptance_12_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_5_acceptance_12_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "**Conditional, only if EZ-5.10 is adopted; unit and each adapter's actual response-policy path.** A decoded gzip/deflate/br body with a surviving strong ETag, Accept-Ranges and Content-Digest weakens the ETag and removes the other two fields. An already-weak ETag stays weak without double-prefixing. Original coding, 206 and nondecoded controls retain their surviving metadata; fields removed by prior normalization never reappear. Record EZ-5.9 and EZ-5.10 adoption independently, including each applicable environment leg, rather than treating this criterion as implicit promotion.", + "artifact_path": "outbound/EZ-5/acceptance/12/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/01", + "source_id": "EZ-6/acceptance/01", + "source_digest": "0e6522a87a5f5631a86916fb97123604590206f4afcdc20311dfd33947f00084", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-01-axum", + "sentinel": "ez_6_acceptance_01_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/01", + "expected": "Slow origin. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin is begun at 2 and its head arrives at 700, the page body arrives in chunks at 700, 900, and 1,300, the batch starts at 34 with cutoff 534, A answers at 180, B at 320, and C at 900, when the application waits for the origin head and checks the batch at 701, then it gets A with elapsed 146, B with elapsed 286, and `Cutoff` with C unresolved. At F, the same flow loses A and B on Fastly and sends no bidder request on the other adapters.", + "artifact_path": "outbound/EZ-6/acceptance/01/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/01", + "source_id": "EZ-6/acceptance/01", + "source_digest": "0e6522a87a5f5631a86916fb97123604590206f4afcdc20311dfd33947f00084", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-01-viceroy", + "sentinel": "ez_6_acceptance_01_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/01", + "expected": "Slow origin. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin is begun at 2 and its head arrives at 700, the page body arrives in chunks at 700, 900, and 1,300, the batch starts at 34 with cutoff 534, A answers at 180, B at 320, and C at 900, when the application waits for the origin head and checks the batch at 701, then it gets A with elapsed 146, B with elapsed 286, and `Cutoff` with C unresolved. At F, the same flow loses A and B on Fastly and sends no bidder request on the other adapters.", + "artifact_path": "outbound/EZ-6/acceptance/01/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/01", + "source_id": "EZ-6/acceptance/01", + "source_digest": "0e6522a87a5f5631a86916fb97123604590206f4afcdc20311dfd33947f00084", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-01-workerd", + "sentinel": "ez_6_acceptance_01_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/01", + "expected": "Slow origin. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin is begun at 2 and its head arrives at 700, the page body arrives in chunks at 700, 900, and 1,300, the batch starts at 34 with cutoff 534, A answers at 180, B at 320, and C at 900, when the application waits for the origin head and checks the batch at 701, then it gets A with elapsed 146, B with elapsed 286, and `Cutoff` with C unresolved. At F, the same flow loses A and B on Fastly and sends no bidder request on the other adapters.", + "artifact_path": "outbound/EZ-6/acceptance/01/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/01", + "source_id": "EZ-6/acceptance/01", + "source_digest": "0e6522a87a5f5631a86916fb97123604590206f4afcdc20311dfd33947f00084", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-01-spin", + "sentinel": "ez_6_acceptance_01_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/01", + "expected": "Slow origin. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin is begun at 2 and its head arrives at 700, the page body arrives in chunks at 700, 900, and 1,300, the batch starts at 34 with cutoff 534, A answers at 180, B at 320, and C at 900, when the application waits for the origin head and checks the batch at 701, then it gets A with elapsed 146, B with elapsed 286, and `Cutoff` with C unresolved. At F, the same flow loses A and B on Fastly and sends no bidder request on the other adapters.", + "artifact_path": "outbound/EZ-6/acceptance/01/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/01", + "source_id": "EZ-6/acceptance/01", + "source_digest": "0e6522a87a5f5631a86916fb97123604590206f4afcdc20311dfd33947f00084", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-01-hosted-fastly", + "sentinel": "ez_6_acceptance_01_hosted_fastly", + "command": "hosted-runbook EZ-6/acceptance/01 --environment hosted-fastly --sentinel ez_6_acceptance_01_hosted_fastly", + "expected": "Slow origin. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin is begun at 2 and its head arrives at 700, the page body arrives in chunks at 700, 900, and 1,300, the batch starts at 34 with cutoff 534, A answers at 180, B at 320, and C at 900, when the application waits for the origin head and checks the batch at 701, then it gets A with elapsed 146, B with elapsed 286, and `Cutoff` with C unresolved. At F, the same flow loses A and B on Fastly and sends no bidder request on the other adapters.", + "artifact_path": "outbound/EZ-6/acceptance/01/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/02", + "source_id": "EZ-6/acceptance/02", + "source_digest": "3361ed096e8de852e90f511cd999f73d83e09d5e8ae058b5c6ab87738bab06d2", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-02-axum", + "sentinel": "ez_6_acceptance_02_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/02", + "expected": "Fast origin with mediator. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin head at 120, a body chunk at 260, and A, B, C answering at 180, 320, and 400, when the application waits on the batch at 260, then it gets A, then B, then C and `Completed` at 400. A `send` at 400 built with `OutboundRequest::deadline` set to the cutoff and `OutboundRequest::timeout` set to 1,000 ms gets a 134 ms budget with the request deadline as its cause. If that origin stays silent, the send fails with a gateway timeout at 534.", + "artifact_path": "outbound/EZ-6/acceptance/02/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/02", + "source_id": "EZ-6/acceptance/02", + "source_digest": "3361ed096e8de852e90f511cd999f73d83e09d5e8ae058b5c6ab87738bab06d2", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-02-viceroy", + "sentinel": "ez_6_acceptance_02_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/02", + "expected": "Fast origin with mediator. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin head at 120, a body chunk at 260, and A, B, C answering at 180, 320, and 400, when the application waits on the batch at 260, then it gets A, then B, then C and `Completed` at 400. A `send` at 400 built with `OutboundRequest::deadline` set to the cutoff and `OutboundRequest::timeout` set to 1,000 ms gets a 134 ms budget with the request deadline as its cause. If that origin stays silent, the send fails with a gateway timeout at 534.", + "artifact_path": "outbound/EZ-6/acceptance/02/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/02", + "source_id": "EZ-6/acceptance/02", + "source_digest": "3361ed096e8de852e90f511cd999f73d83e09d5e8ae058b5c6ab87738bab06d2", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-02-workerd", + "sentinel": "ez_6_acceptance_02_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/02", + "expected": "Fast origin with mediator. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin head at 120, a body chunk at 260, and A, B, C answering at 180, 320, and 400, when the application waits on the batch at 260, then it gets A, then B, then C and `Completed` at 400. A `send` at 400 built with `OutboundRequest::deadline` set to the cutoff and `OutboundRequest::timeout` set to 1,000 ms gets a 134 ms budget with the request deadline as its cause. If that origin stays silent, the send fails with a gateway timeout at 534.", + "artifact_path": "outbound/EZ-6/acceptance/02/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/02", + "source_id": "EZ-6/acceptance/02", + "source_digest": "3361ed096e8de852e90f511cd999f73d83e09d5e8ae058b5c6ab87738bab06d2", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-02-spin", + "sentinel": "ez_6_acceptance_02_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/02", + "expected": "Fast origin with mediator. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin head at 120, a body chunk at 260, and A, B, C answering at 180, 320, and 400, when the application waits on the batch at 260, then it gets A, then B, then C and `Completed` at 400. A `send` at 400 built with `OutboundRequest::deadline` set to the cutoff and `OutboundRequest::timeout` set to 1,000 ms gets a 134 ms budget with the request deadline as its cause. If that origin stays silent, the send fails with a gateway timeout at 534.", + "artifact_path": "outbound/EZ-6/acceptance/02/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/02", + "source_id": "EZ-6/acceptance/02", + "source_digest": "3361ed096e8de852e90f511cd999f73d83e09d5e8ae058b5c6ab87738bab06d2", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-02-hosted-fastly", + "sentinel": "ez_6_acceptance_02_hosted_fastly", + "command": "hosted-runbook EZ-6/acceptance/02 --environment hosted-fastly --sentinel ez_6_acceptance_02_hosted_fastly", + "expected": "Fast origin with mediator. Axum, workerd, Spin 4.1, Viceroy 0.21.0, hosted Fastly. Given the origin head at 120, a body chunk at 260, and A, B, C answering at 180, 320, and 400, when the application waits on the batch at 260, then it gets A, then B, then C and `Completed` at 400. A `send` at 400 built with `OutboundRequest::deadline` set to the cutoff and `OutboundRequest::timeout` set to 1,000 ms gets a 134 ms budget with the request deadline as its cause. If that origin stays silent, the send fails with a gateway timeout at 534.", + "artifact_path": "outbound/EZ-6/acceptance/02/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/03", + "source_id": "EZ-6/acceptance/03", + "source_digest": "c51182de2f9b675050f17b357662722206feaf8a0db6f0dad873e33826c9acca", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-03-axum", + "sentinel": "ez_6_acceptance_03_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/03", + "expected": "CPU stretch. Axum, workerd, Spin 4.1, Viceroy 0.21.0. Given the application does CPU work from 520 to 560 without awaiting and B answers at 530, then B is either delivered with elapsed 496, where the adapter recorded its arrival during the CPU work, or not delivered, with the batch reporting `Cutoff` and B unresolved. B is never delivered with an elapsed time of 500 or more. The run records which.", + "artifact_path": "outbound/EZ-6/acceptance/03/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/03", + "source_id": "EZ-6/acceptance/03", + "source_digest": "c51182de2f9b675050f17b357662722206feaf8a0db6f0dad873e33826c9acca", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-03-viceroy", + "sentinel": "ez_6_acceptance_03_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/03", + "expected": "CPU stretch. Axum, workerd, Spin 4.1, Viceroy 0.21.0. Given the application does CPU work from 520 to 560 without awaiting and B answers at 530, then B is either delivered with elapsed 496, where the adapter recorded its arrival during the CPU work, or not delivered, with the batch reporting `Cutoff` and B unresolved. B is never delivered with an elapsed time of 500 or more. The run records which.", + "artifact_path": "outbound/EZ-6/acceptance/03/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/03", + "source_id": "EZ-6/acceptance/03", + "source_digest": "c51182de2f9b675050f17b357662722206feaf8a0db6f0dad873e33826c9acca", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-03-workerd", + "sentinel": "ez_6_acceptance_03_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/03", + "expected": "CPU stretch. Axum, workerd, Spin 4.1, Viceroy 0.21.0. Given the application does CPU work from 520 to 560 without awaiting and B answers at 530, then B is either delivered with elapsed 496, where the adapter recorded its arrival during the CPU work, or not delivered, with the batch reporting `Cutoff` and B unresolved. B is never delivered with an elapsed time of 500 or more. The run records which.", + "artifact_path": "outbound/EZ-6/acceptance/03/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/03", + "source_id": "EZ-6/acceptance/03", + "source_digest": "c51182de2f9b675050f17b357662722206feaf8a0db6f0dad873e33826c9acca", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-03-spin", + "sentinel": "ez_6_acceptance_03_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/03", + "expected": "CPU stretch. Axum, workerd, Spin 4.1, Viceroy 0.21.0. Given the application does CPU work from 520 to 560 without awaiting and B answers at 530, then B is either delivered with elapsed 496, where the adapter recorded its arrival during the CPU work, or not delivered, with the batch reporting `Cutoff` and B unresolved. B is never delivered with an elapsed time of 500 or more. The run records which.", + "artifact_path": "outbound/EZ-6/acceptance/03/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/04", + "source_id": "EZ-6/acceptance/04", + "source_digest": "9cedb4ec3090c069db0db04ee95c602d43bbc657ae16fa5ec8a845307e7cd308", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-04-axum", + "sentinel": "ez_6_acceptance_04_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/04", + "expected": "Begin puts the request on its way. Every adapter. Given begin returns and the application then awaits an unrelated operation (a timer or a KV read) for 100 ms without any other EdgeZero outbound call, then the origin receives the request within that window.", + "artifact_path": "outbound/EZ-6/acceptance/04/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/04", + "source_id": "EZ-6/acceptance/04", + "source_digest": "9cedb4ec3090c069db0db04ee95c602d43bbc657ae16fa5ec8a845307e7cd308", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-04-viceroy", + "sentinel": "ez_6_acceptance_04_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/04", + "expected": "Begin puts the request on its way. Every adapter. Given begin returns and the application then awaits an unrelated operation (a timer or a KV read) for 100 ms without any other EdgeZero outbound call, then the origin receives the request within that window.", + "artifact_path": "outbound/EZ-6/acceptance/04/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/04", + "source_id": "EZ-6/acceptance/04", + "source_digest": "9cedb4ec3090c069db0db04ee95c602d43bbc657ae16fa5ec8a845307e7cd308", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-04-workerd", + "sentinel": "ez_6_acceptance_04_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/04", + "expected": "Begin puts the request on its way. Every adapter. Given begin returns and the application then awaits an unrelated operation (a timer or a KV read) for 100 ms without any other EdgeZero outbound call, then the origin receives the request within that window.", + "artifact_path": "outbound/EZ-6/acceptance/04/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/04", + "source_id": "EZ-6/acceptance/04", + "source_digest": "9cedb4ec3090c069db0db04ee95c602d43bbc657ae16fa5ec8a845307e7cd308", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-04-spin", + "sentinel": "ez_6_acceptance_04_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/04", + "expected": "Begin puts the request on its way. Every adapter. Given begin returns and the application then awaits an unrelated operation (a timer or a KV read) for 100 ms without any other EdgeZero outbound call, then the origin receives the request within that window.", + "artifact_path": "outbound/EZ-6/acceptance/04/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/05", + "source_id": "EZ-6/acceptance/05", + "source_digest": "517c2d88651bd1abcbfae0ab2310a5c53d44ab7fd87d2d9e8b8f4e0fdda99eeb", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-05-axum", + "sentinel": "ez_6_acceptance_05_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/05", + "expected": "Unpolled batch. Every adapter. Given a batch that the application does not check for 300 ms while it awaits an unrelated EdgeZero operation, then every bidder receives its request within that window.", + "artifact_path": "outbound/EZ-6/acceptance/05/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/05", + "source_id": "EZ-6/acceptance/05", + "source_digest": "517c2d88651bd1abcbfae0ab2310a5c53d44ab7fd87d2d9e8b8f4e0fdda99eeb", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-05-viceroy", + "sentinel": "ez_6_acceptance_05_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/05", + "expected": "Unpolled batch. Every adapter. Given a batch that the application does not check for 300 ms while it awaits an unrelated EdgeZero operation, then every bidder receives its request within that window.", + "artifact_path": "outbound/EZ-6/acceptance/05/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/05", + "source_id": "EZ-6/acceptance/05", + "source_digest": "517c2d88651bd1abcbfae0ab2310a5c53d44ab7fd87d2d9e8b8f4e0fdda99eeb", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-05-workerd", + "sentinel": "ez_6_acceptance_05_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/05", + "expected": "Unpolled batch. Every adapter. Given a batch that the application does not check for 300 ms while it awaits an unrelated EdgeZero operation, then every bidder receives its request within that window.", + "artifact_path": "outbound/EZ-6/acceptance/05/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/05", + "source_id": "EZ-6/acceptance/05", + "source_digest": "517c2d88651bd1abcbfae0ab2310a5c53d44ab7fd87d2d9e8b8f4e0fdda99eeb", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-05-spin", + "sentinel": "ez_6_acceptance_05_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/05", + "expected": "Unpolled batch. Every adapter. Given a batch that the application does not check for 300 ms while it awaits an unrelated EdgeZero operation, then every bidder receives its request within that window.", + "artifact_path": "outbound/EZ-6/acceptance/05/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/06", + "source_id": "EZ-6/acceptance/06", + "source_digest": "3c0f3d08ab80ce98b4468d1a528febba6c57a0fdb4eb5ad60affb34ffa6a211e", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-06-viceroy", + "sentinel": "ez_6_acceptance_06_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/06", + "expected": "Arrival during origin body reads. Viceroy 0.21.0 and hosted Fastly. Given the origin sends body chunks at 100 and 800 and B answers at 320, when the application only reads the origin body, then B is delivered with elapsed 286.", + "artifact_path": "outbound/EZ-6/acceptance/06/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/06", + "source_id": "EZ-6/acceptance/06", + "source_digest": "3c0f3d08ab80ce98b4468d1a528febba6c57a0fdb4eb5ad60affb34ffa6a211e", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-06-hosted-fastly", + "sentinel": "ez_6_acceptance_06_hosted_fastly", + "command": "hosted-runbook EZ-6/acceptance/06 --environment hosted-fastly --sentinel ez_6_acceptance_06_hosted_fastly", + "expected": "Arrival during origin body reads. Viceroy 0.21.0 and hosted Fastly. Given the origin sends body chunks at 100 and 800 and B answers at 320, when the application only reads the origin body, then B is delivered with elapsed 286.", + "artifact_path": "outbound/EZ-6/acceptance/06/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/07", + "source_id": "EZ-6/acceptance/07", + "source_digest": "aeddb383cb4b0907939c832d9bfa73ea557bd087fc3e49e6475b917ceb28e98a", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-07-viceroy", + "sentinel": "ez_6_acceptance_07_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/07", + "expected": "Trickling slot. Viceroy 0.21.0 and hosted Fastly. Given a bidder sends its head at 100 and its body at 600 while the origin streams a chunk every 50 ms, then the origin chunks keep reaching the application between 100 and 600 without a stall caused by reading the bidder body. With a 534 cutoff, that bidder is unresolved and the batch reports `Cutoff` by 539. A body-readiness or blocking-read fallback that delays the application until 600 does not pass.", + "artifact_path": "outbound/EZ-6/acceptance/07/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/07", + "source_id": "EZ-6/acceptance/07", + "source_digest": "aeddb383cb4b0907939c832d9bfa73ea557bd087fc3e49e6475b917ceb28e98a", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-07-hosted-fastly", + "sentinel": "ez_6_acceptance_07_hosted_fastly", + "command": "hosted-runbook EZ-6/acceptance/07 --environment hosted-fastly --sentinel ez_6_acceptance_07_hosted_fastly", + "expected": "Trickling slot. Viceroy 0.21.0 and hosted Fastly. Given a bidder sends its head at 100 and its body at 600 while the origin streams a chunk every 50 ms, then the origin chunks keep reaching the application between 100 and 600 without a stall caused by reading the bidder body. With a 534 cutoff, that bidder is unresolved and the batch reports `Cutoff` by 539. A body-readiness or blocking-read fallback that delays the application until 600 does not pass.", + "artifact_path": "outbound/EZ-6/acceptance/07/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/08", + "source_id": "EZ-6/acceptance/08", + "source_digest": "9e5ad56ff0e9d42743be2b225ac60d230a65a1d0ad9c0dbf8234adb3808ffd30", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-08-viceroy", + "sentinel": "ez_6_acceptance_08_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/08", + "expected": "Send during a live batch. Viceroy 0.21.0 and hosted Fastly. Same as scenario 1, but the origin is forwarded with `send` after the batch starts. A and B are delivered.", + "artifact_path": "outbound/EZ-6/acceptance/08/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/08", + "source_id": "EZ-6/acceptance/08", + "source_digest": "9e5ad56ff0e9d42743be2b225ac60d230a65a1d0ad9c0dbf8234adb3808ffd30", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-08-hosted-fastly", + "sentinel": "ez_6_acceptance_08_hosted_fastly", + "command": "hosted-runbook EZ-6/acceptance/08 --environment hosted-fastly --sentinel ez_6_acceptance_08_hosted_fastly", + "expected": "Send during a live batch. Viceroy 0.21.0 and hosted Fastly. Same as scenario 1, but the origin is forwarded with `send` after the batch starts. A and B are delivered.", + "artifact_path": "outbound/EZ-6/acceptance/08/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/09", + "source_id": "EZ-6/acceptance/09", + "source_digest": "03a5d30a47f710ee0c0ba26dd63cd7edc25a9f2d4dd8799f98ce54b3ded5fb10", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-09-axum", + "sentinel": "ez_6_acceptance_09_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/09", + "expected": "Caps while waiting. Every adapter. Given a bidder slot with a 1 MiB decoded cap and a 64 KiB buffered cap, whose gzip body decodes to 128 KiB and trickles in over 300 ms, then the slot fails with the buffered-cap error before the body ends.", + "artifact_path": "outbound/EZ-6/acceptance/09/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/09", + "source_id": "EZ-6/acceptance/09", + "source_digest": "03a5d30a47f710ee0c0ba26dd63cd7edc25a9f2d4dd8799f98ce54b3ded5fb10", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-09-viceroy", + "sentinel": "ez_6_acceptance_09_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/09", + "expected": "Caps while waiting. Every adapter. Given a bidder slot with a 1 MiB decoded cap and a 64 KiB buffered cap, whose gzip body decodes to 128 KiB and trickles in over 300 ms, then the slot fails with the buffered-cap error before the body ends.", + "artifact_path": "outbound/EZ-6/acceptance/09/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/09", + "source_id": "EZ-6/acceptance/09", + "source_digest": "03a5d30a47f710ee0c0ba26dd63cd7edc25a9f2d4dd8799f98ce54b3ded5fb10", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-09-workerd", + "sentinel": "ez_6_acceptance_09_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/09", + "expected": "Caps while waiting. Every adapter. Given a bidder slot with a 1 MiB decoded cap and a 64 KiB buffered cap, whose gzip body decodes to 128 KiB and trickles in over 300 ms, then the slot fails with the buffered-cap error before the body ends.", + "artifact_path": "outbound/EZ-6/acceptance/09/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/09", + "source_id": "EZ-6/acceptance/09", + "source_digest": "03a5d30a47f710ee0c0ba26dd63cd7edc25a9f2d4dd8799f98ce54b3ded5fb10", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-09-spin", + "sentinel": "ez_6_acceptance_09_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/09", + "expected": "Caps while waiting. Every adapter. Given a bidder slot with a 1 MiB decoded cap and a 64 KiB buffered cap, whose gzip body decodes to 128 KiB and trickles in over 300 ms, then the slot fails with the buffered-cap error before the body ends.", + "artifact_path": "outbound/EZ-6/acceptance/09/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/10", + "source_id": "EZ-6/acceptance/10", + "source_digest": "9c9209ec5391b2145227bb725d582ab7312a6c0ba5e108d53ab4b59062ec96b2", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F1", + "substep": "EZ-6-acceptance-10-viceroy-reuse", + "sentinel": "ez_6_acceptance_10_viceroy_reuse", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/10", + "expected": "Abandonment on a reused sandbox (EZ-6.31). Viceroy 0.21.0. Given a reused guest serving 6 requests, each beginning a request to an origin that answers after 2 s and dropping it, then request 6 still sends, no request observes another's arrivals, and no abandoned work remains 2 s after the last request. Hosted Fastly. With reuse limits raised so one sandbox can serve 1,000 requests at about 50 per second, the same holds for request 1,001. The run records how many requests each sandbox served.", + "artifact_path": "outbound/EZ-6/acceptance/10/viceroy/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/10", + "source_id": "EZ-6/acceptance/10", + "source_digest": "9c9209ec5391b2145227bb725d582ab7312a6c0ba5e108d53ab4b59062ec96b2", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "hosted-fastly/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F1", + "substep": "EZ-6-acceptance-10-hosted-fastly-reuse", + "sentinel": "ez_6_acceptance_10_hosted_fastly_reuse", + "command": "hosted-runbook EZ-6/acceptance/10 --environment hosted-fastly/reuse --sentinel ez_6_acceptance_10_hosted_fastly_reuse", + "expected": "Abandonment on a reused sandbox (EZ-6.31). Viceroy 0.21.0. Given a reused guest serving 6 requests, each beginning a request to an origin that answers after 2 s and dropping it, then request 6 still sends, no request observes another's arrivals, and no abandoned work remains 2 s after the last request. Hosted Fastly. With reuse limits raised so one sandbox can serve 1,000 requests at about 50 per second, the same holds for request 1,001. The run records how many requests each sandbox served.", + "artifact_path": "outbound/EZ-6/acceptance/10/hosted-fastly/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/11", + "source_id": "EZ-6/acceptance/11", + "source_digest": "9ee3fa4b69b28c63285db44e1f64f5e86d7cf804ae38e8e452075304f9da517b", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-11-axum", + "sentinel": "ez_6_acceptance_11_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/11", + "expected": "Write deadline. Axum, workerd, Spin 4.1. Given a response with a 1 s write deadline whose body waits on a batch with a 3 s cutoff, then the response ends with the write-deadline outcome, and the batch's in-flight requests are released. On Viceroy 0.21.0 and hosted Fastly the run records the outcome, because EZ-6.24 is a SHOULD there.", + "artifact_path": "outbound/EZ-6/acceptance/11/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/11", + "source_id": "EZ-6/acceptance/11", + "source_digest": "9ee3fa4b69b28c63285db44e1f64f5e86d7cf804ae38e8e452075304f9da517b", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-11-viceroy", + "sentinel": "ez_6_acceptance_11_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/11", + "expected": "Write deadline. Axum, workerd, Spin 4.1. Given a response with a 1 s write deadline whose body waits on a batch with a 3 s cutoff, then the response ends with the write-deadline outcome, and the batch's in-flight requests are released. On Viceroy 0.21.0 and hosted Fastly the run records the outcome, because EZ-6.24 is a SHOULD there.", + "artifact_path": "outbound/EZ-6/acceptance/11/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/11", + "source_id": "EZ-6/acceptance/11", + "source_digest": "9ee3fa4b69b28c63285db44e1f64f5e86d7cf804ae38e8e452075304f9da517b", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-11-workerd", + "sentinel": "ez_6_acceptance_11_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/11", + "expected": "Write deadline. Axum, workerd, Spin 4.1. Given a response with a 1 s write deadline whose body waits on a batch with a 3 s cutoff, then the response ends with the write-deadline outcome, and the batch's in-flight requests are released. On Viceroy 0.21.0 and hosted Fastly the run records the outcome, because EZ-6.24 is a SHOULD there.", + "artifact_path": "outbound/EZ-6/acceptance/11/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/11", + "source_id": "EZ-6/acceptance/11", + "source_digest": "9ee3fa4b69b28c63285db44e1f64f5e86d7cf804ae38e8e452075304f9da517b", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-11-spin", + "sentinel": "ez_6_acceptance_11_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/11", + "expected": "Write deadline. Axum, workerd, Spin 4.1. Given a response with a 1 s write deadline whose body waits on a batch with a 3 s cutoff, then the response ends with the write-deadline outcome, and the batch's in-flight requests are released. On Viceroy 0.21.0 and hosted Fastly the run records the outcome, because EZ-6.24 is a SHOULD there.", + "artifact_path": "outbound/EZ-6/acceptance/11/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/11", + "source_id": "EZ-6/acceptance/11", + "source_digest": "9ee3fa4b69b28c63285db44e1f64f5e86d7cf804ae38e8e452075304f9da517b", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "hosted-fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-11-hosted-fastly", + "sentinel": "ez_6_acceptance_11_hosted_fastly", + "command": "hosted-runbook EZ-6/acceptance/11 --environment hosted-fastly --sentinel ez_6_acceptance_11_hosted_fastly", + "expected": "Write deadline. Axum, workerd, Spin 4.1. Given a response with a 1 s write deadline whose body waits on a batch with a 3 s cutoff, then the response ends with the write-deadline outcome, and the batch's in-flight requests are released. On Viceroy 0.21.0 and hosted Fastly the run records the outcome, because EZ-6.24 is a SHOULD there.", + "artifact_path": "outbound/EZ-6/acceptance/11/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/12", + "source_id": "EZ-6/acceptance/12", + "source_digest": "0dfa783ee3519f4e021d1b770f7200922fd3c3a2a51693be8ce1d2ad0b11d3c4", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-12-axum", + "sentinel": "ez_6_acceptance_12_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/12", + "expected": "Parity. Every adapter. For buffered uploads, a begun request and `send` produce the same outcomes on EdgeZero's existing outbound contract cases, including client defaults.", + "artifact_path": "outbound/EZ-6/acceptance/12/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/12", + "source_id": "EZ-6/acceptance/12", + "source_digest": "0dfa783ee3519f4e021d1b770f7200922fd3c3a2a51693be8ce1d2ad0b11d3c4", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-12-viceroy", + "sentinel": "ez_6_acceptance_12_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/12", + "expected": "Parity. Every adapter. For buffered uploads, a begun request and `send` produce the same outcomes on EdgeZero's existing outbound contract cases, including client defaults.", + "artifact_path": "outbound/EZ-6/acceptance/12/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/12", + "source_id": "EZ-6/acceptance/12", + "source_digest": "0dfa783ee3519f4e021d1b770f7200922fd3c3a2a51693be8ce1d2ad0b11d3c4", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-12-workerd", + "sentinel": "ez_6_acceptance_12_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/12", + "expected": "Parity. Every adapter. For buffered uploads, a begun request and `send` produce the same outcomes on EdgeZero's existing outbound contract cases, including client defaults.", + "artifact_path": "outbound/EZ-6/acceptance/12/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/12", + "source_id": "EZ-6/acceptance/12", + "source_digest": "0dfa783ee3519f4e021d1b770f7200922fd3c3a2a51693be8ce1d2ad0b11d3c4", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-12-spin", + "sentinel": "ez_6_acceptance_12_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/12", + "expected": "Parity. Every adapter. For buffered uploads, a begun request and `send` produce the same outcomes on EdgeZero's existing outbound contract cases, including client defaults.", + "artifact_path": "outbound/EZ-6/acceptance/12/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/13", + "source_id": "EZ-6/acceptance/13", + "source_digest": "f3570fbc9b1809845bc96a5656c9329d05d8498bd8c87ffdedc534be9d20d602", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-13-unit-core", + "sentinel": "ez_6_acceptance_13_unit_core", + "command": "scripts/run_test_nonzero.sh ez_6_acceptance_13_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Upload rules. Unit and every adapter. Begin rejects a POST with a streamed body as an invalid request before sending. Begin accepts a bodiless GET built from an inbound request. `send` forwards a POST with a streamed body.", + "artifact_path": "outbound/EZ-6/acceptance/13/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/13", + "source_id": "EZ-6/acceptance/13", + "source_digest": "f3570fbc9b1809845bc96a5656c9329d05d8498bd8c87ffdedc534be9d20d602", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-13-axum", + "sentinel": "ez_6_acceptance_13_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/13", + "expected": "Upload rules. Unit and every adapter. Begin rejects a POST with a streamed body as an invalid request before sending. Begin accepts a bodiless GET built from an inbound request. `send` forwards a POST with a streamed body.", + "artifact_path": "outbound/EZ-6/acceptance/13/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/13", + "source_id": "EZ-6/acceptance/13", + "source_digest": "f3570fbc9b1809845bc96a5656c9329d05d8498bd8c87ffdedc534be9d20d602", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-13-viceroy", + "sentinel": "ez_6_acceptance_13_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/13", + "expected": "Upload rules. Unit and every adapter. Begin rejects a POST with a streamed body as an invalid request before sending. Begin accepts a bodiless GET built from an inbound request. `send` forwards a POST with a streamed body.", + "artifact_path": "outbound/EZ-6/acceptance/13/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/13", + "source_id": "EZ-6/acceptance/13", + "source_digest": "f3570fbc9b1809845bc96a5656c9329d05d8498bd8c87ffdedc534be9d20d602", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-13-workerd", + "sentinel": "ez_6_acceptance_13_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/13", + "expected": "Upload rules. Unit and every adapter. Begin rejects a POST with a streamed body as an invalid request before sending. Begin accepts a bodiless GET built from an inbound request. `send` forwards a POST with a streamed body.", + "artifact_path": "outbound/EZ-6/acceptance/13/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/13", + "source_id": "EZ-6/acceptance/13", + "source_digest": "f3570fbc9b1809845bc96a5656c9329d05d8498bd8c87ffdedc534be9d20d602", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-13-spin", + "sentinel": "ez_6_acceptance_13_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/13", + "expected": "Upload rules. Unit and every adapter. Begin rejects a POST with a streamed body as an invalid request before sending. Begin accepts a bodiless GET built from an inbound request. `send` forwards a POST with a streamed body.", + "artifact_path": "outbound/EZ-6/acceptance/13/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/14", + "source_id": "EZ-6/acceptance/14", + "source_digest": "c532493e7b829fb4d002f9966b68551dc65e2ca69c9f7ec6f87309cb96f59bc5", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-14-axum", + "sentinel": "ez_6_acceptance_14_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/14", + "expected": "Timeout before the cutoff. Every adapter. Given a fourth bidder D in the scenario 1 batch with a 300 ms per-call timeout that never answers, then the batch delivers D as a gateway timeout naming the per-call timeout, with elapsed 300, before it reports `Cutoff`.", + "artifact_path": "outbound/EZ-6/acceptance/14/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/14", + "source_id": "EZ-6/acceptance/14", + "source_digest": "c532493e7b829fb4d002f9966b68551dc65e2ca69c9f7ec6f87309cb96f59bc5", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-14-viceroy", + "sentinel": "ez_6_acceptance_14_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/14", + "expected": "Timeout before the cutoff. Every adapter. Given a fourth bidder D in the scenario 1 batch with a 300 ms per-call timeout that never answers, then the batch delivers D as a gateway timeout naming the per-call timeout, with elapsed 300, before it reports `Cutoff`.", + "artifact_path": "outbound/EZ-6/acceptance/14/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/14", + "source_id": "EZ-6/acceptance/14", + "source_digest": "c532493e7b829fb4d002f9966b68551dc65e2ca69c9f7ec6f87309cb96f59bc5", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-14-workerd", + "sentinel": "ez_6_acceptance_14_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/14", + "expected": "Timeout before the cutoff. Every adapter. Given a fourth bidder D in the scenario 1 batch with a 300 ms per-call timeout that never answers, then the batch delivers D as a gateway timeout naming the per-call timeout, with elapsed 300, before it reports `Cutoff`.", + "artifact_path": "outbound/EZ-6/acceptance/14/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/14", + "source_id": "EZ-6/acceptance/14", + "source_digest": "c532493e7b829fb4d002f9966b68551dc65e2ca69c9f7ec6f87309cb96f59bc5", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-14-spin", + "sentinel": "ez_6_acceptance_14_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/14", + "expected": "Timeout before the cutoff. Every adapter. Given a fourth bidder D in the scenario 1 batch with a 300 ms per-call timeout that never answers, then the batch delivers D as a gateway timeout naming the per-call timeout, with elapsed 300, before it reports `Cutoff`.", + "artifact_path": "outbound/EZ-6/acceptance/14/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/15", + "source_id": "EZ-6/acceptance/15", + "source_digest": "caebea46c3a5d4d3bfba59fa0aaec5b2279015f67cc3d8646c463b2cdd5da013", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-15-unit-core", + "sentinel": "ez_6_acceptance_15_unit_core", + "command": "scripts/run_test_nonzero.sh ez_6_acceptance_15_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Arrival at the cutoff. Unit, injected clock. Given a slot whose response is complete exactly at the cutoff, then it is unresolved.", + "artifact_path": "outbound/EZ-6/acceptance/15/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/16", + "source_id": "EZ-6/acceptance/16", + "source_digest": "af756ecdadf6742f689bd22809a8b38369e6ad78377b203eb6d0b496e5e78724", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-16-axum", + "sentinel": "ez_6_acceptance_16_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/16", + "expected": "Waiting `next` ends at the cutoff. Every adapter. Given a batch with a 200 ms cutoff and no slot answering, then a waiting `next` returns `Cutoff` within 205 ms. Repeat with response headers already received but the body permanently pending; the same bound holds and the slot stays unresolved.", + "artifact_path": "outbound/EZ-6/acceptance/16/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/16", + "source_id": "EZ-6/acceptance/16", + "source_digest": "af756ecdadf6742f689bd22809a8b38369e6ad78377b203eb6d0b496e5e78724", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-16-viceroy", + "sentinel": "ez_6_acceptance_16_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/16", + "expected": "Waiting `next` ends at the cutoff. Every adapter. Given a batch with a 200 ms cutoff and no slot answering, then a waiting `next` returns `Cutoff` within 205 ms. Repeat with response headers already received but the body permanently pending; the same bound holds and the slot stays unresolved.", + "artifact_path": "outbound/EZ-6/acceptance/16/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/16", + "source_id": "EZ-6/acceptance/16", + "source_digest": "af756ecdadf6742f689bd22809a8b38369e6ad78377b203eb6d0b496e5e78724", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-16-workerd", + "sentinel": "ez_6_acceptance_16_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/16", + "expected": "Waiting `next` ends at the cutoff. Every adapter. Given a batch with a 200 ms cutoff and no slot answering, then a waiting `next` returns `Cutoff` within 205 ms. Repeat with response headers already received but the body permanently pending; the same bound holds and the slot stays unresolved.", + "artifact_path": "outbound/EZ-6/acceptance/16/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/16", + "source_id": "EZ-6/acceptance/16", + "source_digest": "af756ecdadf6742f689bd22809a8b38369e6ad78377b203eb6d0b496e5e78724", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-16-spin", + "sentinel": "ez_6_acceptance_16_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/16", + "expected": "Waiting `next` ends at the cutoff. Every adapter. Given a batch with a 200 ms cutoff and no slot answering, then a waiting `next` returns `Cutoff` within 205 ms. Repeat with response headers already received but the body permanently pending; the same bound holds and the slot stays unresolved.", + "artifact_path": "outbound/EZ-6/acceptance/16/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/17", + "source_id": "EZ-6/acceptance/17", + "source_digest": "03ed75534b1d14da2c5b3692ae8675460859e220969535c60e55ac289c306515", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-17-unit-core", + "sentinel": "ez_6_acceptance_17_unit_core", + "command": "scripts/run_test_nonzero.sh ez_6_acceptance_17_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Cancellation report. Unit and every adapter. Given a batch with one slot recorded but not delivered and one still in flight, when it is cancelled, then both indices are reported as undelivered.", + "artifact_path": "outbound/EZ-6/acceptance/17/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/17", + "source_id": "EZ-6/acceptance/17", + "source_digest": "03ed75534b1d14da2c5b3692ae8675460859e220969535c60e55ac289c306515", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-17-axum", + "sentinel": "ez_6_acceptance_17_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/17", + "expected": "Cancellation report. Unit and every adapter. Given a batch with one slot recorded but not delivered and one still in flight, when it is cancelled, then both indices are reported as undelivered.", + "artifact_path": "outbound/EZ-6/acceptance/17/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/17", + "source_id": "EZ-6/acceptance/17", + "source_digest": "03ed75534b1d14da2c5b3692ae8675460859e220969535c60e55ac289c306515", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-17-viceroy", + "sentinel": "ez_6_acceptance_17_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/17", + "expected": "Cancellation report. Unit and every adapter. Given a batch with one slot recorded but not delivered and one still in flight, when it is cancelled, then both indices are reported as undelivered.", + "artifact_path": "outbound/EZ-6/acceptance/17/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/17", + "source_id": "EZ-6/acceptance/17", + "source_digest": "03ed75534b1d14da2c5b3692ae8675460859e220969535c60e55ac289c306515", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-17-workerd", + "sentinel": "ez_6_acceptance_17_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/17", + "expected": "Cancellation report. Unit and every adapter. Given a batch with one slot recorded but not delivered and one still in flight, when it is cancelled, then both indices are reported as undelivered.", + "artifact_path": "outbound/EZ-6/acceptance/17/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/17", + "source_id": "EZ-6/acceptance/17", + "source_digest": "03ed75534b1d14da2c5b3692ae8675460859e220969535c60e55ac289c306515", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-17-spin", + "sentinel": "ez_6_acceptance_17_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/17", + "expected": "Cancellation report. Unit and every adapter. Given a batch with one slot recorded but not delivered and one still in flight, when it is cancelled, then both indices are reported as undelivered.", + "artifact_path": "outbound/EZ-6/acceptance/17/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/18", + "source_id": "EZ-6/acceptance/18", + "source_digest": "115ca09e3f8c629955f10d81f399428d0fd787dca9d108cc70358dab572e781a", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-18-axum", + "sentinel": "ez_6_acceptance_18_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/18", + "expected": "Check inside a streamed body. Every adapter. Given a streamed response body that checks the batch between chunks while two slots are still in flight, then each check returns \"nothing new\" in under 5 ms.", + "artifact_path": "outbound/EZ-6/acceptance/18/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/18", + "source_id": "EZ-6/acceptance/18", + "source_digest": "115ca09e3f8c629955f10d81f399428d0fd787dca9d108cc70358dab572e781a", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-18-viceroy", + "sentinel": "ez_6_acceptance_18_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/18", + "expected": "Check inside a streamed body. Every adapter. Given a streamed response body that checks the batch between chunks while two slots are still in flight, then each check returns \"nothing new\" in under 5 ms.", + "artifact_path": "outbound/EZ-6/acceptance/18/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/18", + "source_id": "EZ-6/acceptance/18", + "source_digest": "115ca09e3f8c629955f10d81f399428d0fd787dca9d108cc70358dab572e781a", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-18-workerd", + "sentinel": "ez_6_acceptance_18_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/18", + "expected": "Check inside a streamed body. Every adapter. Given a streamed response body that checks the batch between chunks while two slots are still in flight, then each check returns \"nothing new\" in under 5 ms.", + "artifact_path": "outbound/EZ-6/acceptance/18/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/18", + "source_id": "EZ-6/acceptance/18", + "source_digest": "115ca09e3f8c629955f10d81f399428d0fd787dca9d108cc70358dab572e781a", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-18-spin", + "sentinel": "ez_6_acceptance_18_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/18", + "expected": "Check inside a streamed body. Every adapter. Given a streamed response body that checks the batch between chunks while two slots are still in flight, then each check returns \"nothing new\" in under 5 ms.", + "artifact_path": "outbound/EZ-6/acceptance/18/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/19", + "source_id": "EZ-6/acceptance/19", + "source_digest": "4fe859c183e470d0a02773f0563b57d550dcea6d27275b2dfe1d1ac77e76ba0e", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-19-viceroy", + "sentinel": "ez_6_acceptance_19_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/19", + "expected": "Work after delivery. Viceroy 0.21.0. Given a serving callback that, after delivery, starts a three-slot batch with a client it keeps and collects it, then all three results are delivered with arrival-based elapsed values, and nothing from that batch is still in flight after the callback returns.", + "artifact_path": "outbound/EZ-6/acceptance/19/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/20", + "source_id": "EZ-6/acceptance/20", + "source_digest": "08c72d4df9222b01a1f373a2b6f178f97fecd31f8ad35ce64ebe46f98e44c5be", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-20-axum", + "sentinel": "ez_6_acceptance_20_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/20", + "expected": "Work after response handoff. Axum, workerd, and Spin 4.1. A streamed response owns a begun origin request and a bidder batch whose results are still pending when request dispatch or the platform callback returns. The body then collects both and streams their results successfully. Handoff does not cancel either exchange or release its resources early.", + "artifact_path": "outbound/EZ-6/acceptance/20/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/20", + "source_id": "EZ-6/acceptance/20", + "source_digest": "08c72d4df9222b01a1f373a2b6f178f97fecd31f8ad35ce64ebe46f98e44c5be", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-20-workerd", + "sentinel": "ez_6_acceptance_20_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/20", + "expected": "Work after response handoff. Axum, workerd, and Spin 4.1. A streamed response owns a begun origin request and a bidder batch whose results are still pending when request dispatch or the platform callback returns. The body then collects both and streams their results successfully. Handoff does not cancel either exchange or release its resources early.", + "artifact_path": "outbound/EZ-6/acceptance/20/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/20", + "source_id": "EZ-6/acceptance/20", + "source_digest": "08c72d4df9222b01a1f373a2b6f178f97fecd31f8ad35ce64ebe46f98e44c5be", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-20-spin", + "sentinel": "ez_6_acceptance_20_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/20", + "expected": "Work after response handoff. Axum, workerd, and Spin 4.1. A streamed response owns a begun origin request and a bidder batch whose results are still pending when request dispatch or the platform callback returns. The body then collects both and streams their results successfully. Handoff does not cancel either exchange or release its resources early.", + "artifact_path": "outbound/EZ-6/acceptance/20/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/21", + "source_id": "EZ-6/acceptance/21", + "source_digest": "b4cd675497b04cb2fee5cc7e557dce1eeab3603cefcf0a08a4a0c52f052172a1", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "unit/axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-21-unit-axum", + "sentinel": "ez_6_acceptance_21_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_6_acceptance_21_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Response-owned cleanup. Unit for each adapter and Axum, workerd, and Spin 4.1. Repeat the response-owned setup with normal body completion, disconnect, expired write deadline, source failure, and explicit cancellation, wherever observable. Each path releases body-owned exchanges and SDK-owned resources exactly once, stops guest-side polling, and preserves the adapter's documented host-cancellation limitations. Unit and Viceroy 0.21.0 keep a separate callback-owned exchange pending on the same client before response cleanup, then prove cleanup neither cancels it nor stops its observation: that exchange completes in the still-active Fastly callback, which can also start new post-delivery work. A client-wide cancellation that destroys both owners does not pass.", + "artifact_path": "outbound/EZ-6/acceptance/21/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/21", + "source_id": "EZ-6/acceptance/21", + "source_digest": "b4cd675497b04cb2fee5cc7e557dce1eeab3603cefcf0a08a4a0c52f052172a1", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-21-unit-fastly", + "sentinel": "ez_6_acceptance_21_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_6_acceptance_21_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Response-owned cleanup. Unit for each adapter and Axum, workerd, and Spin 4.1. Repeat the response-owned setup with normal body completion, disconnect, expired write deadline, source failure, and explicit cancellation, wherever observable. Each path releases body-owned exchanges and SDK-owned resources exactly once, stops guest-side polling, and preserves the adapter's documented host-cancellation limitations. Unit and Viceroy 0.21.0 keep a separate callback-owned exchange pending on the same client before response cleanup, then prove cleanup neither cancels it nor stops its observation: that exchange completes in the still-active Fastly callback, which can also start new post-delivery work. A client-wide cancellation that destroys both owners does not pass.", + "artifact_path": "outbound/EZ-6/acceptance/21/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/21", + "source_id": "EZ-6/acceptance/21", + "source_digest": "b4cd675497b04cb2fee5cc7e557dce1eeab3603cefcf0a08a4a0c52f052172a1", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "unit/cloudflare", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-21-unit-cloudflare", + "sentinel": "ez_6_acceptance_21_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_6_acceptance_21_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Response-owned cleanup. Unit for each adapter and Axum, workerd, and Spin 4.1. Repeat the response-owned setup with normal body completion, disconnect, expired write deadline, source failure, and explicit cancellation, wherever observable. Each path releases body-owned exchanges and SDK-owned resources exactly once, stops guest-side polling, and preserves the adapter's documented host-cancellation limitations. Unit and Viceroy 0.21.0 keep a separate callback-owned exchange pending on the same client before response cleanup, then prove cleanup neither cancels it nor stops its observation: that exchange completes in the still-active Fastly callback, which can also start new post-delivery work. A client-wide cancellation that destroys both owners does not pass.", + "artifact_path": "outbound/EZ-6/acceptance/21/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/21", + "source_id": "EZ-6/acceptance/21", + "source_digest": "b4cd675497b04cb2fee5cc7e557dce1eeab3603cefcf0a08a4a0c52f052172a1", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "unit/spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-21-unit-spin", + "sentinel": "ez_6_acceptance_21_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_6_acceptance_21_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Response-owned cleanup. Unit for each adapter and Axum, workerd, and Spin 4.1. Repeat the response-owned setup with normal body completion, disconnect, expired write deadline, source failure, and explicit cancellation, wherever observable. Each path releases body-owned exchanges and SDK-owned resources exactly once, stops guest-side polling, and preserves the adapter's documented host-cancellation limitations. Unit and Viceroy 0.21.0 keep a separate callback-owned exchange pending on the same client before response cleanup, then prove cleanup neither cancels it nor stops its observation: that exchange completes in the still-active Fastly callback, which can also start new post-delivery work. A client-wide cancellation that destroys both owners does not pass.", + "artifact_path": "outbound/EZ-6/acceptance/21/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/21", + "source_id": "EZ-6/acceptance/21", + "source_digest": "b4cd675497b04cb2fee5cc7e557dce1eeab3603cefcf0a08a4a0c52f052172a1", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-21-axum", + "sentinel": "ez_6_acceptance_21_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/21", + "expected": "Response-owned cleanup. Unit for each adapter and Axum, workerd, and Spin 4.1. Repeat the response-owned setup with normal body completion, disconnect, expired write deadline, source failure, and explicit cancellation, wherever observable. Each path releases body-owned exchanges and SDK-owned resources exactly once, stops guest-side polling, and preserves the adapter's documented host-cancellation limitations. Unit and Viceroy 0.21.0 keep a separate callback-owned exchange pending on the same client before response cleanup, then prove cleanup neither cancels it nor stops its observation: that exchange completes in the still-active Fastly callback, which can also start new post-delivery work. A client-wide cancellation that destroys both owners does not pass.", + "artifact_path": "outbound/EZ-6/acceptance/21/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/21", + "source_id": "EZ-6/acceptance/21", + "source_digest": "b4cd675497b04cb2fee5cc7e557dce1eeab3603cefcf0a08a4a0c52f052172a1", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-21-workerd", + "sentinel": "ez_6_acceptance_21_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/21", + "expected": "Response-owned cleanup. Unit for each adapter and Axum, workerd, and Spin 4.1. Repeat the response-owned setup with normal body completion, disconnect, expired write deadline, source failure, and explicit cancellation, wherever observable. Each path releases body-owned exchanges and SDK-owned resources exactly once, stops guest-side polling, and preserves the adapter's documented host-cancellation limitations. Unit and Viceroy 0.21.0 keep a separate callback-owned exchange pending on the same client before response cleanup, then prove cleanup neither cancels it nor stops its observation: that exchange completes in the still-active Fastly callback, which can also start new post-delivery work. A client-wide cancellation that destroys both owners does not pass.", + "artifact_path": "outbound/EZ-6/acceptance/21/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/21", + "source_id": "EZ-6/acceptance/21", + "source_digest": "b4cd675497b04cb2fee5cc7e557dce1eeab3603cefcf0a08a4a0c52f052172a1", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-21-spin", + "sentinel": "ez_6_acceptance_21_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/21", + "expected": "Response-owned cleanup. Unit for each adapter and Axum, workerd, and Spin 4.1. Repeat the response-owned setup with normal body completion, disconnect, expired write deadline, source failure, and explicit cancellation, wherever observable. Each path releases body-owned exchanges and SDK-owned resources exactly once, stops guest-side polling, and preserves the adapter's documented host-cancellation limitations. Unit and Viceroy 0.21.0 keep a separate callback-owned exchange pending on the same client before response cleanup, then prove cleanup neither cancels it nor stops its observation: that exchange completes in the still-active Fastly callback, which can also start new post-delivery work. A client-wide cancellation that destroys both owners does not pass.", + "artifact_path": "outbound/EZ-6/acceptance/21/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/21", + "source_id": "EZ-6/acceptance/21", + "source_digest": "b4cd675497b04cb2fee5cc7e557dce1eeab3603cefcf0a08a4a0c52f052172a1", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy/callback-owner", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-21-viceroy-callback-owner", + "sentinel": "ez_6_acceptance_21_viceroy_callback_owner", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/21", + "expected": "Response-owned cleanup. Unit for each adapter and Axum, workerd, and Spin 4.1. Repeat the response-owned setup with normal body completion, disconnect, expired write deadline, source failure, and explicit cancellation, wherever observable. Each path releases body-owned exchanges and SDK-owned resources exactly once, stops guest-side polling, and preserves the adapter's documented host-cancellation limitations. Unit and Viceroy 0.21.0 keep a separate callback-owned exchange pending on the same client before response cleanup, then prove cleanup neither cancels it nor stops its observation: that exchange completes in the still-active Fastly callback, which can also start new post-delivery work. A client-wide cancellation that destroys both owners does not pass.", + "artifact_path": "outbound/EZ-6/acceptance/21/viceroy/callback-owner.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/22", + "source_id": "EZ-6/acceptance/22", + "source_digest": "f4ab1fcb9ae4968773276b4309d9ab1135cc91b4bde57f5a35409a8bf4189a4f", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-22-unit-core", + "sentinel": "ez_6_acceptance_22_unit_core", + "command": "scripts/run_test_nonzero.sh ez_6_acceptance_22_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Mixed observation and collection. Unit and every adapter. Consume slot 0 with a nonblocking check, slot 1 with `next`, then collect a three-slot batch. The first two positions are `AlreadyDelivered`, never unresolved; the third is `Resolved` on success or `Unresolved` at cutoff. A driver failure after the first two deliveries preserves those states and its exact error. Merging into the matching accumulator restores all owned outcomes without replay or cloning; missing prior results, a foreign batch, duplicate insertion, and a conflicting slot are rejected without losing either input's results. Fresh-batch collection has no `AlreadyDelivered`, and `Completed` never contains `Unresolved`.", + "artifact_path": "outbound/EZ-6/acceptance/22/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/22", + "source_id": "EZ-6/acceptance/22", + "source_digest": "f4ab1fcb9ae4968773276b4309d9ab1135cc91b4bde57f5a35409a8bf4189a4f", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-22-axum", + "sentinel": "ez_6_acceptance_22_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-6/acceptance/22", + "expected": "Mixed observation and collection. Unit and every adapter. Consume slot 0 with a nonblocking check, slot 1 with `next`, then collect a three-slot batch. The first two positions are `AlreadyDelivered`, never unresolved; the third is `Resolved` on success or `Unresolved` at cutoff. A driver failure after the first two deliveries preserves those states and its exact error. Merging into the matching accumulator restores all owned outcomes without replay or cloning; missing prior results, a foreign batch, duplicate insertion, and a conflicting slot are rejected without losing either input's results. Fresh-batch collection has no `AlreadyDelivered`, and `Completed` never contains `Unresolved`.", + "artifact_path": "outbound/EZ-6/acceptance/22/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/22", + "source_id": "EZ-6/acceptance/22", + "source_digest": "f4ab1fcb9ae4968773276b4309d9ab1135cc91b4bde57f5a35409a8bf4189a4f", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-22-viceroy", + "sentinel": "ez_6_acceptance_22_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/22", + "expected": "Mixed observation and collection. Unit and every adapter. Consume slot 0 with a nonblocking check, slot 1 with `next`, then collect a three-slot batch. The first two positions are `AlreadyDelivered`, never unresolved; the third is `Resolved` on success or `Unresolved` at cutoff. A driver failure after the first two deliveries preserves those states and its exact error. Merging into the matching accumulator restores all owned outcomes without replay or cloning; missing prior results, a foreign batch, duplicate insertion, and a conflicting slot are rejected without losing either input's results. Fresh-batch collection has no `AlreadyDelivered`, and `Completed` never contains `Unresolved`.", + "artifact_path": "outbound/EZ-6/acceptance/22/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/22", + "source_id": "EZ-6/acceptance/22", + "source_digest": "f4ab1fcb9ae4968773276b4309d9ab1135cc91b4bde57f5a35409a8bf4189a4f", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-22-workerd", + "sentinel": "ez_6_acceptance_22_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-6/acceptance/22", + "expected": "Mixed observation and collection. Unit and every adapter. Consume slot 0 with a nonblocking check, slot 1 with `next`, then collect a three-slot batch. The first two positions are `AlreadyDelivered`, never unresolved; the third is `Resolved` on success or `Unresolved` at cutoff. A driver failure after the first two deliveries preserves those states and its exact error. Merging into the matching accumulator restores all owned outcomes without replay or cloning; missing prior results, a foreign batch, duplicate insertion, and a conflicting slot are rejected without losing either input's results. Fresh-batch collection has no `AlreadyDelivered`, and `Completed` never contains `Unresolved`.", + "artifact_path": "outbound/EZ-6/acceptance/22/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/22", + "source_id": "EZ-6/acceptance/22", + "source_digest": "f4ab1fcb9ae4968773276b4309d9ab1135cc91b4bde57f5a35409a8bf4189a4f", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-22-spin", + "sentinel": "ez_6_acceptance_22_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-6/acceptance/22", + "expected": "Mixed observation and collection. Unit and every adapter. Consume slot 0 with a nonblocking check, slot 1 with `next`, then collect a three-slot batch. The first two positions are `AlreadyDelivered`, never unresolved; the third is `Resolved` on success or `Unresolved` at cutoff. A driver failure after the first two deliveries preserves those states and its exact error. Merging into the matching accumulator restores all owned outcomes without replay or cloning; missing prior results, a foreign batch, duplicate insertion, and a conflicting slot are rejected without losing either input's results. Fresh-batch collection has no `AlreadyDelivered`, and `Completed` never contains `Unresolved`.", + "artifact_path": "outbound/EZ-6/acceptance/22/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/23", + "source_id": "EZ-6/acceptance/23", + "source_digest": "9367265200604b97708ec941ce019be407122026eb1c79e4720091db9fc5323d", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "unit/fastly/both-wrappers", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-23-unit-fastly-both-wrappers", + "sentinel": "ez_6_acceptance_23_unit_fastly_both_wrappers", + "command": "scripts/run_test_nonzero.sh ez_6_acceptance_23_unit_fastly_both_wrappers cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Callback-scope cleanup. Unit for both Fastly lifecycle wrappers and Viceroy 0.21.0. A custom callback retains a pending callback-owned exchange in state outside the callback, completes response delivery, starts post-delivery work, then returns an error before that exchange finishes. Response cleanup leaves the exchange live until callback return; callback return stops its guest-side work and releases SDK-owned resources exactly once even though the public handle remains retained. Repeat with a normal return and an early error before delivery, without inventing a replacement response. Unit tests cover unwinding where supported, not process-abort teardown. Single-request mode must pass; a next-request isolation leg is conditional on reusable-sandbox adoption, not a prerequisite for this cleanup.", + "artifact_path": "outbound/EZ-6/acceptance/23/unit/fastly/both-wrappers.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/23b", + "source_id": "EZ-6/acceptance/23", + "source_digest": "9367265200604b97708ec941ce019be407122026eb1c79e4720091db9fc5323d", + "requirement_ids": ["EZ-6.31"], + "environment": "unit/fastly/both-wrappers", + "applicability": "conditional", + "approval": "reusable-sandbox adoption@unit/fastly/both-wrappers", + "condition": "reusable-sandbox adoption", + "owner": "EdgeZero", + "task": "F1", + "substep": "EZ-6-acceptance-23b-unit-fastly-both-wrappers", + "sentinel": "ez_6_acceptance_23b_unit_fastly_both_wrappers", + "command": "scripts/run_test_nonzero.sh ez_6_acceptance_23b_unit_fastly_both_wrappers cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Callback-scope cleanup. Unit for both Fastly lifecycle wrappers and Viceroy 0.21.0. A custom callback retains a pending callback-owned exchange in state outside the callback, completes response delivery, starts post-delivery work, then returns an error before that exchange finishes. Response cleanup leaves the exchange live until callback return; callback return stops its guest-side work and releases SDK-owned resources exactly once even though the public handle remains retained. Repeat with a normal return and an early error before delivery, without inventing a replacement response. Unit tests cover unwinding where supported, not process-abort teardown. Single-request mode must pass; a next-request isolation leg is conditional on reusable-sandbox adoption, not a prerequisite for this cleanup.", + "artifact_path": "outbound/EZ-6/acceptance/23b/unit/fastly/both-wrappers.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/23", + "source_id": "EZ-6/acceptance/23", + "source_digest": "9367265200604b97708ec941ce019be407122026eb1c79e4720091db9fc5323d", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D6", + "substep": "EZ-6-acceptance-23-viceroy", + "sentinel": "ez_6_acceptance_23_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/23", + "expected": "Callback-scope cleanup. Unit for both Fastly lifecycle wrappers and Viceroy 0.21.0. A custom callback retains a pending callback-owned exchange in state outside the callback, completes response delivery, starts post-delivery work, then returns an error before that exchange finishes. Response cleanup leaves the exchange live until callback return; callback return stops its guest-side work and releases SDK-owned resources exactly once even though the public handle remains retained. Repeat with a normal return and an early error before delivery, without inventing a replacement response. Unit tests cover unwinding where supported, not process-abort teardown. Single-request mode must pass; a next-request isolation leg is conditional on reusable-sandbox adoption, not a prerequisite for this cleanup.", + "artifact_path": "outbound/EZ-6/acceptance/23/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/23b", + "source_id": "EZ-6/acceptance/23", + "source_digest": "9367265200604b97708ec941ce019be407122026eb1c79e4720091db9fc5323d", + "requirement_ids": ["EZ-6.31"], + "environment": "viceroy", + "applicability": "conditional", + "approval": "reusable-sandbox adoption@viceroy", + "condition": "reusable-sandbox adoption", + "owner": "EdgeZero", + "task": "F1", + "substep": "EZ-6-acceptance-23b-viceroy", + "sentinel": "ez_6_acceptance_23b_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-6/acceptance/23b", + "expected": "Callback-scope cleanup. Unit for both Fastly lifecycle wrappers and Viceroy 0.21.0. A custom callback retains a pending callback-owned exchange in state outside the callback, completes response delivery, starts post-delivery work, then returns an error before that exchange finishes. Response cleanup leaves the exchange live until callback return; callback return stops its guest-side work and releases SDK-owned resources exactly once even though the public handle remains retained. Repeat with a normal return and an early error before delivery, without inventing a replacement response. Unit tests cover unwinding where supported, not process-abort teardown. Single-request mode must pass; a next-request isolation leg is conditional on reusable-sandbox adoption, not a prerequisite for this cleanup.", + "artifact_path": "outbound/EZ-6/acceptance/23b/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/ts-e2e", + "source_id": "EZ-6/acceptance/ts-e2e", + "source_digest": "340e19f8f12495ea6ff62586cf336a48e1f90386285b18c0ac17ef36782d0d9c", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "consumer-viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "D6", + "substep": "EZ-6-acceptance-ts-e2e-consumer-viceroy", + "sentinel": "ez_6_acceptance_ts_e2e_consumer_viceroy", + "command": "consumer-runbook EZ-6/acceptance/ts-e2e --environment consumer-viceroy --sentinel ez_6_acceptance_ts_e2e_consumer_viceroy", + "expected": "The Trusted Server end-to-end check runs in TS's integration suite on Viceroy 0.21.0, the Axum binary, and workerd, and in the Spin smoke (`ts:scripts/smoke-spin.sh`) on Spin 4.1. A navigation whose origin head arrives at 700 ms with a 500 ms auction contains the bids from bidders that answered at 180 and 320 ms and not one that answered at 600 ms. A page whose `` arrives at 1,300 ms gives the same result, and TS ranks locally because no mediator time remains. TTFB tracks origin latency plus less than 20 ms. A POST navigation forwards its body through `send` and runs no auction. On Fastly, pull sync after delivery completes inside the serving callback.", + "artifact_path": "outbound/EZ-6/acceptance/ts-e2e/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/ts-e2e", + "source_id": "EZ-6/acceptance/ts-e2e", + "source_digest": "340e19f8f12495ea6ff62586cf336a48e1f90386285b18c0ac17ef36782d0d9c", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "consumer-axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "D6", + "substep": "EZ-6-acceptance-ts-e2e-consumer-axum", + "sentinel": "ez_6_acceptance_ts_e2e_consumer_axum", + "command": "consumer-runbook EZ-6/acceptance/ts-e2e --environment consumer-axum --sentinel ez_6_acceptance_ts_e2e_consumer_axum", + "expected": "The Trusted Server end-to-end check runs in TS's integration suite on Viceroy 0.21.0, the Axum binary, and workerd, and in the Spin smoke (`ts:scripts/smoke-spin.sh`) on Spin 4.1. A navigation whose origin head arrives at 700 ms with a 500 ms auction contains the bids from bidders that answered at 180 and 320 ms and not one that answered at 600 ms. A page whose `` arrives at 1,300 ms gives the same result, and TS ranks locally because no mediator time remains. TTFB tracks origin latency plus less than 20 ms. A POST navigation forwards its body through `send` and runs no auction. On Fastly, pull sync after delivery completes inside the serving callback.", + "artifact_path": "outbound/EZ-6/acceptance/ts-e2e/consumer-axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/ts-e2e", + "source_id": "EZ-6/acceptance/ts-e2e", + "source_digest": "340e19f8f12495ea6ff62586cf336a48e1f90386285b18c0ac17ef36782d0d9c", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "consumer-workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "D6", + "substep": "EZ-6-acceptance-ts-e2e-consumer-workerd", + "sentinel": "ez_6_acceptance_ts_e2e_consumer_workerd", + "command": "consumer-runbook EZ-6/acceptance/ts-e2e --environment consumer-workerd --sentinel ez_6_acceptance_ts_e2e_consumer_workerd", + "expected": "The Trusted Server end-to-end check runs in TS's integration suite on Viceroy 0.21.0, the Axum binary, and workerd, and in the Spin smoke (`ts:scripts/smoke-spin.sh`) on Spin 4.1. A navigation whose origin head arrives at 700 ms with a 500 ms auction contains the bids from bidders that answered at 180 and 320 ms and not one that answered at 600 ms. A page whose `` arrives at 1,300 ms gives the same result, and TS ranks locally because no mediator time remains. TTFB tracks origin latency plus less than 20 ms. A POST navigation forwards its body through `send` and runs no auction. On Fastly, pull sync after delivery completes inside the serving callback.", + "artifact_path": "outbound/EZ-6/acceptance/ts-e2e/consumer-workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-6/acceptance/ts-e2e", + "source_id": "EZ-6/acceptance/ts-e2e", + "source_digest": "340e19f8f12495ea6ff62586cf336a48e1f90386285b18c0ac17ef36782d0d9c", + "requirement_ids": [ + "EZ-6.1", + "EZ-6.2", + "EZ-6.3", + "EZ-6.4", + "EZ-6.5", + "EZ-6.6", + "EZ-6.7", + "EZ-6.8", + "EZ-6.9", + "EZ-6.10", + "EZ-6.11", + "EZ-6.12", + "EZ-6.13", + "EZ-6.14", + "EZ-6.15", + "EZ-6.16", + "EZ-6.17", + "EZ-6.18", + "EZ-6.19", + "EZ-6.20", + "EZ-6.21", + "EZ-6.22", + "EZ-6.23", + "EZ-6.24", + "EZ-6.25", + "EZ-6.26", + "EZ-6.27", + "EZ-6.28", + "EZ-6.29", + "EZ-6.30", + "EZ-6.31" + ], + "environment": "consumer-spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "D6", + "substep": "EZ-6-acceptance-ts-e2e-consumer-spin", + "sentinel": "ez_6_acceptance_ts_e2e_consumer_spin", + "command": "consumer-runbook EZ-6/acceptance/ts-e2e --environment consumer-spin --sentinel ez_6_acceptance_ts_e2e_consumer_spin", + "expected": "The Trusted Server end-to-end check runs in TS's integration suite on Viceroy 0.21.0, the Axum binary, and workerd, and in the Spin smoke (`ts:scripts/smoke-spin.sh`) on Spin 4.1. A navigation whose origin head arrives at 700 ms with a 500 ms auction contains the bids from bidders that answered at 180 and 320 ms and not one that answered at 600 ms. A page whose `` arrives at 1,300 ms gives the same result, and TS ranks locally because no mediator time remains. TTFB tracks origin latency plus less than 20 ms. A POST navigation forwards its body through `send` and runs no auction. On Fastly, pull sync after delivery completes inside the serving callback.", + "artifact_path": "outbound/EZ-6/acceptance/ts-e2e/consumer-spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-7/acceptance/01", + "source_id": "EZ-7/acceptance/01", + "source_digest": "e2c198abd0ad0159156279fc1eb61fefcebb2be789598bf4077d10ea8df298f3", + "requirement_ids": [ + "EZ-7.1", + "EZ-7.2", + "EZ-7.3", + "EZ-7.4", + "EZ-7.5", + "EZ-7.6", + "EZ-7.7", + "EZ-7.8" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-7-acceptance-01-unit-core", + "sentinel": "ez_7_acceptance_01_unit_core", + "command": "scripts/run_test_nonzero.sh ez_7_acceptance_01_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit.** Given an upstream response with N fields and a header-count cap of exactly N, when the marker is off, then the response succeeds. When it is on, it fails with the header-count limit. The same holds for header bytes.", + "artifact_path": "outbound/EZ-7/acceptance/01/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-7/acceptance/02", + "source_id": "EZ-7/acceptance/02", + "source_digest": "8c99cde6c522bd520275b15cf777dae78822cd01641b1704e5b5cc796663cb45", + "requirement_ids": [ + "EZ-7.1", + "EZ-7.2", + "EZ-7.3", + "EZ-7.4", + "EZ-7.5", + "EZ-7.6", + "EZ-7.7", + "EZ-7.8" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-7-acceptance-02-unit-core", + "sentinel": "ez_7_acceptance_02_unit_core", + "command": "scripts/run_test_nonzero.sh ez_7_acceptance_02_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit.** Given a client handle set to omit the marker, when the application sends one request with no override, one with an explicit \"on\", one begun request, and one two-slot batch, then only the explicit \"on\" request carries the marker.", + "artifact_path": "outbound/EZ-7/acceptance/02/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-7/acceptance/03", + "source_id": "EZ-7/acceptance/03", + "source_digest": "978864da3009fa75d7e62e0607a9a3b23dae63991bfc5a607c36445456d9bcac", + "requirement_ids": [ + "EZ-7.1", + "EZ-7.2", + "EZ-7.3", + "EZ-7.4", + "EZ-7.5", + "EZ-7.6", + "EZ-7.7", + "EZ-7.8" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-7-acceptance-03-unit-core", + "sentinel": "ez_7_acceptance_03_unit_core", + "command": "scripts/run_test_nonzero.sh ez_7_acceptance_03_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit.** Given the marker is off and the upstream sends `x-edgezero-proxy: upstream`, then the field arrives unchanged.", + "artifact_path": "outbound/EZ-7/acceptance/03/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-7/acceptance/04", + "source_id": "EZ-7/acceptance/04", + "source_digest": "0c8dc39d51ee2d273a21726163929444186e89a3bbe55628b1d0db3f67d3a081", + "requirement_ids": [ + "EZ-7.1", + "EZ-7.2", + "EZ-7.3", + "EZ-7.4", + "EZ-7.5", + "EZ-7.6", + "EZ-7.7", + "EZ-7.8" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-7-acceptance-04-axum", + "sentinel": "ez_7_acceptance_04_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-7/acceptance/04", + "expected": "**Axum, Viceroy 0.21.0, workerd, Spin 4.1.** Given 200, 204, HEAD 200, 304, and 302 responses from an origin, when the marker is off, then none carries the field. Under F's behavior, all of them carry it.", + "artifact_path": "outbound/EZ-7/acceptance/04/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-7/acceptance/04", + "source_id": "EZ-7/acceptance/04", + "source_digest": "0c8dc39d51ee2d273a21726163929444186e89a3bbe55628b1d0db3f67d3a081", + "requirement_ids": [ + "EZ-7.1", + "EZ-7.2", + "EZ-7.3", + "EZ-7.4", + "EZ-7.5", + "EZ-7.6", + "EZ-7.7", + "EZ-7.8" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-7-acceptance-04-viceroy", + "sentinel": "ez_7_acceptance_04_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-7/acceptance/04", + "expected": "**Axum, Viceroy 0.21.0, workerd, Spin 4.1.** Given 200, 204, HEAD 200, 304, and 302 responses from an origin, when the marker is off, then none carries the field. Under F's behavior, all of them carry it.", + "artifact_path": "outbound/EZ-7/acceptance/04/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-7/acceptance/04", + "source_id": "EZ-7/acceptance/04", + "source_digest": "0c8dc39d51ee2d273a21726163929444186e89a3bbe55628b1d0db3f67d3a081", + "requirement_ids": [ + "EZ-7.1", + "EZ-7.2", + "EZ-7.3", + "EZ-7.4", + "EZ-7.5", + "EZ-7.6", + "EZ-7.7", + "EZ-7.8" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-7-acceptance-04-workerd", + "sentinel": "ez_7_acceptance_04_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-7/acceptance/04", + "expected": "**Axum, Viceroy 0.21.0, workerd, Spin 4.1.** Given 200, 204, HEAD 200, 304, and 302 responses from an origin, when the marker is off, then none carries the field. Under F's behavior, all of them carry it.", + "artifact_path": "outbound/EZ-7/acceptance/04/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-7/acceptance/04", + "source_id": "EZ-7/acceptance/04", + "source_digest": "0c8dc39d51ee2d273a21726163929444186e89a3bbe55628b1d0db3f67d3a081", + "requirement_ids": [ + "EZ-7.1", + "EZ-7.2", + "EZ-7.3", + "EZ-7.4", + "EZ-7.5", + "EZ-7.6", + "EZ-7.7", + "EZ-7.8" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P3", + "substep": "EZ-7-acceptance-04-spin", + "sentinel": "ez_7_acceptance_04_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-7/acceptance/04", + "expected": "**Axum, Viceroy 0.21.0, workerd, Spin 4.1.** Given 200, 204, HEAD 200, 304, and 302 responses from an origin, when the marker is off, then none carries the field. Under F's behavior, all of them carry it.", + "artifact_path": "outbound/EZ-7/acceptance/04/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-7/acceptance/05", + "source_id": "EZ-7/acceptance/05", + "source_digest": "0dcdbcc4b3d14322464c9a23014f75ff556120d90760a0c240c33fb201845bb2", + "requirement_ids": [ + "EZ-7.1", + "EZ-7.2", + "EZ-7.3", + "EZ-7.4", + "EZ-7.5", + "EZ-7.6", + "EZ-7.7", + "EZ-7.8" + ], + "environment": "consumer-viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "P3", + "substep": "EZ-7-acceptance-05-consumer-viceroy", + "sentinel": "ez_7_acceptance_05_consumer_viceroy", + "command": "consumer-runbook EZ-7/acceptance/05 --environment consumer-viceroy --sentinel ez_7_acceptance_05_consumer_viceroy", + "expected": "**Trusted Server, Viceroy 0.21.0.** Browser-facing responses from the publisher fallback, asset proxy, `/first-party/proxy`, GTM collect, Testlight, Lockr API, Permutive, DataDome `/js/*`, Sourcepoint, and Didomi carry no `x-edgezero-proxy`. TS's request client asks for the marker to be off once, and no TS call site sets it per request.", + "artifact_path": "outbound/EZ-7/acceptance/05/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/01", + "source_id": "EZ-8/acceptance/01", + "source_digest": "8d92f8a05dd84765891e0e6609aa34a0ce9c1210fbbc1a75424d014b9d882de0", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-01-unit-core", + "sentinel": "ez_8_acceptance_01_unit_core", + "command": "scripts/run_test_nonzero.sh ez_8_acceptance_01_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Unit. Given options with quality 101, a zero width, offset 101, smart focus with offsets, or only an x offset, when the application builds them, then it gets a client-class error. Quality 0 and 100 succeed.", + "artifact_path": "outbound/EZ-8/acceptance/01/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/02", + "source_id": "EZ-8/acceptance/02", + "source_digest": "66faae98ff4c480aa7c8747a41e291d7259ec51f9638bf5339d4fa3c6e56e10c", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-02-unit-fastly", + "sentinel": "ez_8_acceptance_02_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_8_acceptance_02_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit with injected time and a scripted host-call boundary. Budgets of 1 ns, 100 microseconds, 999,999 ns, 1 ms, and 1,000,001 ns keep their original deadlines and attributed timeout sources. Zero/expired budgets dispatch nothing. With the same 1 s per-call timeout and different absolute deadlines (including a 100-microsecond effective budget), plain and optimized requests retain the same eligible backend `K` and timers; the synchronous blocking horizon remains explicitly UNKNOWN until provider evidence. Cover each deadline-only policy boundary and maximum-domain case, preparation consuming almost all budget, and registration returning late. A successful or failed host return at `D` becomes the original attributed 504; no pre-dispatch refusal is counted as proof of a bounded host call. A timely optimized head followed by a permanently pending body retains its original deadline and measured +5 ms qualification target, without a hard scheduler guarantee or an uninterruptible body-read fallback.", + "artifact_path": "outbound/EZ-8/acceptance/02/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/03", + "source_id": "EZ-8/acceptance/03", + "source_digest": "a10ae41e74c0a8ec88e7030699d6e565283b97569f80d8a8d4afd97096ccf59f", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-03-unit-core", + "sentinel": "ez_8_acceptance_03_unit_core", + "command": "scripts/run_test_nonzero.sh ez_8_acceptance_03_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Unit. Given a request with options attached, the application reads back options equal to the ones it attached. The same holds for the next request of a followed redirect (EZ-9). A recording Fastly boundary observes `Follow` on the initial and next hop and invokes the same Image Optimizer mapping as `Send`; `Begin` and `Batch` still reject before body polling or provider work. Other adapters reject the options in all four modes before dispatch.", + "artifact_path": "outbound/EZ-8/acceptance/03/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/03", + "source_id": "EZ-8/acceptance/03", + "source_digest": "a10ae41e74c0a8ec88e7030699d6e565283b97569f80d8a8d4afd97096ccf59f", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "unit/axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-03-unit-axum", + "sentinel": "ez_8_acceptance_03_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_8_acceptance_03_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Unit. Given a request with options attached, the application reads back options equal to the ones it attached. The same holds for the next request of a followed redirect (EZ-9). A recording Fastly boundary observes `Follow` on the initial and next hop and invokes the same Image Optimizer mapping as `Send`; `Begin` and `Batch` still reject before body polling or provider work. Other adapters reject the options in all four modes before dispatch.", + "artifact_path": "outbound/EZ-8/acceptance/03/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/03", + "source_id": "EZ-8/acceptance/03", + "source_digest": "a10ae41e74c0a8ec88e7030699d6e565283b97569f80d8a8d4afd97096ccf59f", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "unit/fastly", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-03-unit-fastly", + "sentinel": "ez_8_acceptance_03_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_8_acceptance_03_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit. Given a request with options attached, the application reads back options equal to the ones it attached. The same holds for the next request of a followed redirect (EZ-9). A recording Fastly boundary observes `Follow` on the initial and next hop and invokes the same Image Optimizer mapping as `Send`; `Begin` and `Batch` still reject before body polling or provider work. Other adapters reject the options in all four modes before dispatch.", + "artifact_path": "outbound/EZ-8/acceptance/03/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/03", + "source_id": "EZ-8/acceptance/03", + "source_digest": "a10ae41e74c0a8ec88e7030699d6e565283b97569f80d8a8d4afd97096ccf59f", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "unit/cloudflare", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-03-unit-cloudflare", + "sentinel": "ez_8_acceptance_03_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_8_acceptance_03_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Unit. Given a request with options attached, the application reads back options equal to the ones it attached. The same holds for the next request of a followed redirect (EZ-9). A recording Fastly boundary observes `Follow` on the initial and next hop and invokes the same Image Optimizer mapping as `Send`; `Begin` and `Batch` still reject before body polling or provider work. Other adapters reject the options in all four modes before dispatch.", + "artifact_path": "outbound/EZ-8/acceptance/03/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/03", + "source_id": "EZ-8/acceptance/03", + "source_digest": "a10ae41e74c0a8ec88e7030699d6e565283b97569f80d8a8d4afd97096ccf59f", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "unit/spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-03-unit-spin", + "sentinel": "ez_8_acceptance_03_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_8_acceptance_03_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Unit. Given a request with options attached, the application reads back options equal to the ones it attached. The same holds for the next request of a followed redirect (EZ-9). A recording Fastly boundary observes `Follow` on the initial and next hop and invokes the same Image Optimizer mapping as `Send`; `Begin` and `Batch` still reject before body polling or provider work. Other adapters reject the options in all four modes before dispatch.", + "artifact_path": "outbound/EZ-8/acceptance/03/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/04", + "source_id": "EZ-8/acceptance/04", + "source_digest": "b217fb5a5f8249cc9d247283f9d0e589073c609d3e86bacc8c2f9a05b73a0db0", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-04-axum", + "sentinel": "ez_8_acceptance_04_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-8/acceptance/04", + "expected": "Axum. Given a local origin, when a GET with options is sent, then the result is cross-cutting requirement 2's rejection with status 400 naming the option and `axum`, and the origin accepts no connection. Given a GET built from an inbound request whose body is an unread stream, with options, then the result is the same rejection and the inbound body is not read. Given a two-slot batch with options on slot 0, then slot 0 fails with the rejection, slot 1 returns 200, and the batch completes.", + "artifact_path": "outbound/EZ-8/acceptance/04/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/05", + "source_id": "EZ-8/acceptance/05", + "source_digest": "c8f034d57be5fd67956b7829ada804397736a5b0cea3acd4f1b779e6ccb5b4d2", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-05-workerd", + "sentinel": "ez_8_acceptance_05_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-8/acceptance/05", + "expected": "workerd and Spin 4.1. A single send and a batch slot with options each return the rejection, and no fetch or WASI request is made.", + "artifact_path": "outbound/EZ-8/acceptance/05/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/05", + "source_id": "EZ-8/acceptance/05", + "source_digest": "c8f034d57be5fd67956b7829ada804397736a5b0cea3acd4f1b779e6ccb5b4d2", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-05-spin", + "sentinel": "ez_8_acceptance_05_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-8/acceptance/05", + "expected": "workerd and Spin 4.1. A single send and a batch slot with options each return the rejection, and no fetch or WASI request is made.", + "artifact_path": "outbound/EZ-8/acceptance/05/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/06", + "source_id": "EZ-8/acceptance/06", + "source_digest": "e1375e35a5b0d640f6648371d1d1406f34a1b968077ad3a3059e0973dff88fe1", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-06-viceroy", + "sentinel": "ez_8_acceptance_06_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-8/acceptance/06", + "expected": "Viceroy 0.21.0. Given a GET with options to a reachable origin, then the result is a 502 with the EZ-8.11 reason and the origin receives no request. This shows the options reached the platform, not that Image Optimizer ran, and the hosted criterion is the proof. Given POST with options, GET with a body, or GET with cache bypass, then each gets the rejection, and EZ-2.13's statistics show no registration. Once EZ-11 lands, the same holds for GET with cache tagging. Given a two-slot batch with options on slot 0, then slot 0 fails and slot 1 reaches the origin. Given an optimized GET and a plain GET to one origin with the same transport settings, then EZ-2.13's statistics report one distinct backend configuration.", + "artifact_path": "outbound/EZ-8/acceptance/06/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/06b", + "source_id": "EZ-8/acceptance/06", + "source_digest": "e1375e35a5b0d640f6648371d1d1406f34a1b968077ad3a3059e0973dff88fe1", + "requirement_ids": ["EZ-8.6", "EZ-11.1"], + "environment": "viceroy", + "applicability": "conditional", + "approval": "EZ-11 present@viceroy", + "condition": "EZ-11 present", + "owner": "EdgeZero", + "task": "F3a", + "substep": "EZ-8-acceptance-06b-viceroy", + "sentinel": "ez_8_acceptance_06b_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-8/acceptance/06b", + "expected": "Viceroy 0.21.0. Given a GET with options to a reachable origin, then the result is a 502 with the EZ-8.11 reason and the origin receives no request. This shows the options reached the platform, not that Image Optimizer ran, and the hosted criterion is the proof. Given POST with options, GET with a body, or GET with cache bypass, then each gets the rejection, and EZ-2.13's statistics show no registration. Once EZ-11 lands, the same holds for GET with cache tagging. Given a two-slot batch with options on slot 0, then slot 0 fails and slot 1 reaches the origin. Given an optimized GET and a plain GET to one origin with the same transport settings, then EZ-2.13's statistics report one distinct backend configuration.", + "artifact_path": "outbound/EZ-8/acceptance/06b/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/07", + "source_id": "EZ-8/acceptance/07", + "source_digest": "f6ae8599ce965826c18f8dd8f346ac28ca5e970be79d6233ccfc6d64e211882b", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-07-viceroy", + "sentinel": "ez_8_acceptance_07_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-8/acceptance/07", + "expected": "Viceroy 0.21.0. Given a plain send whose URI has percent-encoded path bytes and a query, then the origin records a request target equal to `OutboundRequest::uri`, and application-set headers arrive unchanged except for documented normalization.", + "artifact_path": "outbound/EZ-8/acceptance/07/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/08", + "source_id": "EZ-8/acceptance/08", + "source_digest": "b01192f46c0ab7fa7f45027a1a2b7d788193be9cfff870c506e1b19a09ce9073", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "hosted-fastly/io-entitled", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "D1d", + "substep": "EZ-8-acceptance-08-hosted-fastly-io-entitled", + "sentinel": "ez_8_acceptance_08_hosted_fastly_io_entitled", + "command": "hosted-runbook EZ-8/acceptance/08 --environment hosted-fastly/io-entitled --sentinel ez_8_acceptance_08_hosted_fastly_io_entitled", + "expected": "Hosted Fastly, run under EZ-Q1 on Trusted Server's staging service with Image Optimizer enabled. Given a known JPEG and options for width 100 and WebP, when sent as GET, then the response is `image/webp` with a decoded width of 100 and the body streams. The same request as HEAD succeeds, and its status and headers are recorded in the capability footnote. Given an origin that accepts the connection and never answers, and a 1 s timeout, the request fails within the pre-approved EZ-8.10 bound. Repeat with a shortened effective absolute deadline but unchanged per-call timeout/backend and with representative deadline-only policy boundaries; record preparation, host-call and total original-entry timings, settings, `H(K)`, source and outcomes. Exercise the ordinary post-head body deadline separately. A horizon violation fails qualification; do not enlarge the rule to fit the run. An expired pre-dispatch case is a separate result, not a successful horizon probe. With an Image Optimizer parameter and an unrelated query parameter present, record the built request target and the origin target with query preservation on and off. The Image Optimizer parameter is not forwarded; the unrelated parameter is retained only when preservation is on. Documentation MUST distinguish these provider changes from EdgeZero's target invariant.\n This hosted criterion also requires a paired native-path performance comparison. Compare the\n candidate with both the frozen Trusted Server native path and an SDK-only control using identical\n image bytes, region, options, negotiation headers, origin/cache settings and execution mode.\n Separate verified cache hits and misses, GET/HEAD, and ordinary versus application-owned S3\n preflight cases. Record sample counts, randomized build order, first-byte/completion latency\n distributions, throughput, guest allocations/copied bytes/peak buffering, backend registration\n and transport host-call counts, origin receipts and connection identities where observable.\n Preserve transformation output semantics and lazy streaming; no additional EdgeZero-origin\n fetch or image-specific backend configuration is permitted. Approve sampling, instrumentation,\n performance acceptance rules and treatment of noise or unavailable counters before replay,\n independently from the horizon rule. Do not borrow unrelated bidder-latency tolerances, weaken\n caps/deadlines for speed, or call missing/inconclusive comparisons performance parity. Viceroy's\n unsupported result is not an optimization-performance sample.", + "artifact_path": "outbound/EZ-8/acceptance/08/hosted-fastly/io-entitled.json", + "implementation": "planned", + "comparison": true + }, + { + "case_id": "EZ-8/acceptance/09", + "source_id": "EZ-8/acceptance/09", + "source_digest": "f2cf27d8a2b8cfb3a6879879b1ee629c19962cd704711fc60e62c96e07b17ade", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "consumer-hosted-fastly/io-entitled", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "D1d", + "substep": "EZ-8-acceptance-09-consumer-hosted-fastly-io-entitled", + "sentinel": "ez_8_acceptance_09_consumer_hosted_fastly_io_entitled", + "command": "consumer-runbook EZ-8/acceptance/09 --environment consumer-hosted-fastly/io-entitled --sentinel ez_8_acceptance_09_consumer_hosted_fastly_io_entitled", + "expected": "Trusted Server end to end, on a Fastly staging service. An optimized asset path returns a transformed image, the HEAD preflight carries no options, and a missing S3 object returns the raw S3 status from the preflight. Trusted Server unit tests read back the options on the final GET and find them equal to the profile-table expectation (EZ-8.17).", + "artifact_path": "outbound/EZ-8/acceptance/09/consumer-hosted-fastly/io-entitled.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-8/acceptance/09", + "source_id": "EZ-8/acceptance/09", + "source_digest": "f2cf27d8a2b8cfb3a6879879b1ee629c19962cd704711fc60e62c96e07b17ade", + "requirement_ids": [ + "EZ-8.1", + "EZ-8.2", + "EZ-8.3", + "EZ-8.4", + "EZ-8.5", + "EZ-8.6", + "EZ-8.7", + "EZ-8.8", + "EZ-8.9", + "EZ-8.10", + "EZ-8.11", + "EZ-8.12", + "EZ-8.13", + "EZ-8.14", + "EZ-8.15", + "EZ-8.16", + "EZ-8.17" + ], + "environment": "consumer-unit/core/profile-readback", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "D1d", + "substep": "EZ-8-acceptance-09-consumer-unit-core-profile-readback", + "sentinel": "ez_8_acceptance_09_consumer_unit_core_profile_readback", + "command": "consumer-runbook EZ-8/acceptance/09 --environment consumer-unit/core/profile-readback --sentinel ez_8_acceptance_09_consumer_unit_core_profile_readback", + "expected": "Trusted Server end to end, on a Fastly staging service. An optimized asset path returns a transformed image, the HEAD preflight carries no options, and a missing S3 object returns the raw S3 status from the preflight. Trusted Server unit tests read back the options on the final GET and find them equal to the profile-table expectation (EZ-8.17).", + "artifact_path": "outbound/EZ-8/acceptance/09/consumer-unit/core/profile-readback.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/01", + "source_id": "EZ-9/acceptance/01", + "source_digest": "7b3d318090bc133a413cc6ece588d88709b62089154c75b15706583e73630e02", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-01-unit-core", + "sentinel": "outbound::redirect_tests::every_status_and_portable_method_obeys_the_redirect_table", + "additional_sentinels": [ + "outbound::redirect_tests::nonredirect_status_precedes_all_location_rules", + "outbound::redirect_tests::plain_send_does_not_follow_a_scripted_redirect" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit, with a scripted upstream. For every status from 300 to 308 and each of GET, HEAD, POST, PUT, PATCH, DELETE and OPTIONS with a 5-byte buffered body (empty for GET and HEAD), the number of requests and the second request's method, body and body headers match EZ-9.6.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/02", + "source_id": "EZ-9/acceptance/02", + "source_digest": "cccc1afd8e0790a662f02ba67030e55fc3a07d8a70d05b8ce3e7e530be802be0", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-02-unit-core", + "sentinel": "outbound::redirect_tests::rewritten_post_drops_content_metadata_and_expectation_for_later_replay", + "additional_sentinels": [ + "outbound::redirect_tests::replaying_empty_post_preserves_content_type", + "outbound::redirect_tests::buffered_redirect_replay_shares_the_original_nonempty_allocation" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. A POST with `Expect: 100-continue` answered by 302 and then 307 sends POST, GET, GET, with the body and expectation only on the first request and no body headers on the later two. A POST with an empty buffered body and `Content-Type` answered by 307 sends `Content-Type` on both requests.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/03", + "source_id": "EZ-9/acceptance/03", + "source_digest": "66131339af1b25235babaf862317b562fa3f706c76b13de67d5930245a23d5d6", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-03-unit-core", + "sentinel": "outbound::redirect_tests::streamed_upload_cannot_replay_and_never_consults_policy", + "additional_sentinels": [ + "outbound::redirect_tests::replay_rule_precedes_limit_and_invalid_location" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. A streamed upload answered by 307 sends one request and reports \"body not replayable\".", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/04", + "source_id": "EZ-9/acceptance/04", + "source_digest": "adc18428c5c0e57739a8587dd3907a77f6a0dfd7274404225035244d292bedea", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-04-unit-core", + "sentinel": "outbound::redirect_tests::inbound_empty_get_stream_is_settled_once_before_all_replayed_hops", + "additional_sentinels": [ + "outbound::redirect_tests::send_empty_get_scope_closed_from_eof_never_dispatches", + "outbound::redirect_tests::send_empty_get_scope_closed_from_source_drop_never_dispatches", + "outbound::redirect_tests::follow_empty_get_scope_closed_from_eof_never_dispatches", + "outbound::redirect_tests::follow_empty_get_scope_closed_from_source_drop_never_dispatches" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. A GET whose body is an empty unread stream, answered by 307, sends two requests with empty bodies and reports no stop reason.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/05", + "source_id": "EZ-9/acceptance/05", + "source_digest": "554c9e1b0641c0e9dc344a409a82135b6b91215d3a0445fe8feaa208f33ff6ec", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-05-unit-core", + "sentinel": "outbound::redirect_tests::limit_four_sends_five_requests_and_skips_a_fifth_policy_call", + "additional_sentinels": [ + "outbound::redirect_tests::redirect_limit_accepts_only_one_through_twenty", + "outbound::redirect_tests::location_presence_and_method_rules_precede_limit_and_location_resolution", + "outbound::redirect_tests::limit_precedes_invalid_location_and_policy" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. With limit 4 and five 302 responses to GET, five requests are sent and the result is a 302 with \"limit reached\" and four redirects followed. A PUT answered by 301 at the limit reports \"method not rewritable\". A 302 with no `Location` answering a PUT at the limit reports \"no location\". Limits 0 and 21 are rejected.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/06", + "source_id": "EZ-9/acceptance/06", + "source_digest": "5a43766f5f2fca300a837c2307a1c68c6e758bdb8cd6d72f84e40aff989d3ef5", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-06-unit-core", + "sentinel": "outbound::redirect_tests::origin_changes_strip_only_automatic_credentials_before_policy", + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. `Authorization` and `Cookie` reach a same-origin hop and do not reach a hop that changes host, port, or only the scheme. A host authority override reaches a same-origin hop only. An `x-amz-date` header reaches both.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/07", + "source_id": "EZ-9/acceptance/07", + "source_digest": "d1cb2a23d26bdc6c25d642b53540384d81ac0c2545f43e38f8a60a75d8795f68", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-07-unit-core", + "sentinel": "outbound::redirect_tests::relative_and_absolute_locations_resolve_without_sending_fragments", + "additional_sentinels": [ + "outbound::redirect_tests::location_presence_and_method_rules_precede_limit_and_location_resolution", + "outbound::redirect_tests::unusable_locations_fail_at_the_answering_hop_before_policy", + "outbound::redirect_tests::original_nontext_and_duplicate_location_evidence_survives_normalization" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. Relative, `../x`, `//b.example/x` and absolute `Location` values follow. A fragment never reaches the wire. A missing or empty value reports \"no location\". Two `Location` fields, user information, or an unusable http target give a 502 protocol failure.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/08", + "source_id": "EZ-9/acceptance/08", + "source_digest": "0cabb5626c9738752f6aaba52b56795d91664be06636408b3c3ddabf0eae2372", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-08-unit-core", + "sentinel": "outbound::redirect_tests::original_nontext_and_duplicate_location_evidence_survives_normalization", + "additional_sentinels": [ + "outbound::redirect_tests::earlier_stop_rules_do_not_bypass_normal_header_filtering" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit, through actual response normalization and an application-supplied head-first client. A 302 containing `Location: /next` and a second `Location` with invalid UTF-8 still fails as a 502 protocol error and sends no next hop. A single invalid value also fails as 502 rather than reporting \"no location\". A method-not-rewritable or limit-reached control retains EZ-9.18's earlier stop reason. The public response retains the normal header-filtering behavior; immutable evidence, not an unfiltered header copy, enforces the redirect rule.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/09", + "source_id": "EZ-9/acceptance/09", + "source_digest": "90416f5bef898c7a6c4cce9a53dab10e864e5288fe8aeb361563bd2945d7825d", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-09-unit-core", + "sentinel": "outbound::redirect_tests::policy_owns_local_state_and_returns_nonclone_nonsend_nondebug_error_unchanged", + "additional_sentinels": [ + "outbound::redirect_tests::policy_stop_returns_the_answering_head_as_final", + "outbound::redirect_tests::non_http_target_can_be_stopped_rejected_or_replaced_but_not_followed_prepared", + "outbound::redirect_tests::taking_prepared_request_then_following_prepared_is_a_protocol_failure", + "outbound::redirect_tests::policy_observes_normalized_head_and_next_request_before_any_next_dispatch", + "outbound::redirect_tests::policy_returned_invalid_request_remains_a_client_error_at_the_selected_hop" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. A policy rejection returns the application's value with redirect number 1 and no second request. A policy stop returns the 3xx with \"stopped by policy\". A modified next request is sent exactly as returned. A `javascript:` target reaches the policy with no prepared request. A policy that asks to follow a prepared request there gets a 502 protocol failure.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/10", + "source_id": "EZ-9/acceptance/10", + "source_digest": "2e0acf55e6f8698d174c7d86398c4317461838a6b44d4a1873a414f4bfbe8ab4", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-10-unit-core", + "sentinel": "outbound::redirect_tests::followed_bodies_are_dropped_without_polling_or_finalization", + "additional_sentinels": [ + "outbound::redirect_tests::stopped_and_limit_reached_heads_keep_ordinary_final_body_errors", + "outbound::redirect_tests::stopped_pending_final_body_waits_only_until_the_original_deadline", + "outbound::redirect_tests::transport_errors_keep_their_original_category_and_failed_hop_index" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit, through an application-supplied recording client implementing the shared head-first dispatch contract. A followed 302 whose body errors, one with `Content-Encoding: br` and an empty body, one declaring a 10 MiB `Content-Length` under a 1 MiB cap, and one whose body stays permanently pending each lead to the final 200, without waiting for intermediate body data. The client records every hop exactly once. Returning the same 302 as final by policy stop or limit applies ordinary decoding and caps and yields the corresponding error instead of hiding it.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/11", + "source_id": "EZ-9/acceptance/11", + "source_digest": "81ae4828d4a4b81beeac677cda3f70097a11af685a63b6e15814dc2174e3fed4", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-11-unit-core", + "sentinel": "outbound::redirect_tests::redirect_chain_uses_supplied_head_client_and_original_deadline", + "additional_sentinels": [ + "outbound::redirect_tests::original_chain_deadline_does_not_replace_a_fresh_requests_default_timeout", + "outbound::redirect_tests::later_prepared_hops_inherit_policy_overrides_not_the_original_options", + "outbound::redirect_tests::policy_time_is_charged_to_original_deadline_before_follow_or_finalization", + "outbound::redirect_tests::expired_policy_rejection_is_not_replaced_with_a_deadline_error", + "outbound::redirect_tests::unsupported_next_options_win_before_the_expired_chain_budget", + "outbound::redirect_tests::replacement_deadlines_cannot_extend_the_original_final_stream_bound", + "outbound::redirect_tests::policy_stop_clock_rewind_after_observed_head_is_internal", + "outbound::redirect_tests::policy_error_survives_a_clock_rewind_after_observed_head" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. A deadline that passes after the first hop fails the second hop as a gateway timeout attributed to the deadline, and the second hop is not sent. Prepared hops retain the preceding request's per-call timeout unless the policy changes it. A fresh policy request with no deadline or a later deadline still cannot send or consume its final streamed body past the original absolute chain deadline; an earlier policy deadline shortens the budget. A synchronous policy cannot be preempted. If it returns an action after the original deadline expired, no next hop is sent and no late final success is returned; unsupported-option rejection still precedes budget rejection for a selected next request. If it rejects instead, its original application error remains unchanged under EZ-9.14.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/12", + "source_id": "EZ-9/acceptance/12", + "source_digest": "54537c868ee065272c17d281011298b896b1e4ff3b194fcaf30a60534e38a8e5", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-12-unit-core", + "sentinel": "outbound::redirect_tests::prepared_requests_retain_all_unchanged_caps_and_options", + "additional_sentinels": [ + "outbound::redirect_tests::policy_authorized_cross_origin_credentials_are_not_stripped_again", + "outbound::redirect_tests::image_optimizer_options_survive_every_prepared_follow_hop", + "outbound::redirect_tests::freshly_built_policy_request_uses_its_own_default_caps_and_options", + "outbound::redirect_tests::later_prepared_hops_inherit_policy_overrides_not_the_original_options" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. A policy modifying the prepared request retains every unchanged cap and option. A freshly built policy request uses its own caps and options, including constructor defaults, while still observing the original absolute chain deadline. A policy can deliberately re-sign a new origin's request without EdgeZero stripping its credentials afterward.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/13", + "source_id": "EZ-9/acceptance/13", + "source_digest": "1516cbd55c0ae450794977ea6e0174848eda8c5a8d3d122813ab250762cce05e", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-13-unit-core", + "sentinel": "outbound::redirect_tests::dropping_inflight_second_hop_releases_it_and_prevents_a_third_dispatch", + "additional_sentinels": [ + "outbound::redirect_tests::dropping_unpolled_follow_releases_the_owned_request_without_transport_work", + "outbound::redirect_tests::first_pending_hop_drop_prevents_a_second_dispatch", + "outbound::redirect_tests::closing_callback_during_policy_prevents_the_next_dispatch", + "outbound::redirect_tests::closing_callback_from_followed_source_drop_prevents_the_next_dispatch" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. Dropping a following call while hop 1 is in flight sends no hop 2.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/14", + "source_id": "EZ-9/acceptance/14", + "source_digest": "7a176d9b4afbc4d979551580b7e640224da2495a656bfd5aea75d29874ee04f0", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-14-unit-core", + "sentinel": "outbound::redirect_tests::followed_hop_logs_omit_query_names_values_and_fragments", + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. With logging captured at trace level, a two-hop chain whose URIs carry `?ts-ec=abc` logs neither `ts-ec` nor `abc`.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/15", + "source_id": "EZ-9/acceptance/15", + "source_digest": "fd9ee9e0d22ac49e7bf666f07c6ce91da4b36eff33620d2e585b6a5c2a8ecfa7", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-15-unit-core", + "sentinel": "outbound::redirect_tests::redirect_chain_uses_supplied_head_client_and_original_deadline", + "additional_sentinels": [ + "outbound::redirect_tests::every_status_and_portable_method_obeys_the_redirect_table", + "outbound::redirect_tests::rewritten_post_drops_content_metadata_and_expectation_for_later_replay", + "outbound::redirect_tests::relative_and_absolute_locations_resolve_without_sending_fragments", + "outbound::redirect_tests::origin_changes_strip_only_automatic_credentials_before_policy" + ], + "command": "scripts/run_test_nonzero.sh --inventory unit/core cargo test --offline --locked -p edgezero-core --features test-utils --lib outbound::redirect_tests", + "expected": "Unit. A following call made on an application-supplied recording client records one ordinary send per hop, with each hop's method, URI, headers and body.", + "artifact_path": "outbound/redirect/unit/core/raw.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/16", + "source_id": "EZ-9/acceptance/16", + "source_digest": "076cf85dd7d3c073f9b40ad3ea6b2ab3fae4563dc9950c587b18c2f79a2ff661", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-16-axum", + "sentinel": "axum-wire-inventory", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-wire --output tests/fixtures/reusable-app/.runs/outbound/redirect/axum", + "expected": "Axum, workerd, Spin 4.1 and Viceroy 0.21.0. For each adapter, with a 200 and no redirect under a 10-byte buffered cap, bodies of 10 and 11 bytes and a declared `Content-Length: 11` give the same status, headers, bytes or limit error from a plain send and from a following call, for identity, gzip and passthrough encodings. Repeat the followed and final 302 body cases from the supplied-client criterion on each adapter, and verify streamed final-response parity; when EZ-14 is present, include its idle-bounded final-body policy.", + "artifact_path": "outbound/redirect/axum/events.jsonl", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/16b", + "source_id": "EZ-9/acceptance/16", + "source_digest": "076cf85dd7d3c073f9b40ad3ea6b2ab3fae4563dc9950c587b18c2f79a2ff661", + "requirement_ids": ["EZ-9.9", "EZ-14.11"], + "environment": "axum", + "applicability": "conditional", + "approval": "EZ-14 present@axum", + "condition": "EZ-14 present", + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-9-acceptance-16b-axum", + "sentinel": "ez_9_acceptance_16b_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-9/acceptance/16b", + "expected": "Axum, workerd, Spin 4.1 and Viceroy 0.21.0. For each adapter, with a 200 and no redirect under a 10-byte buffered cap, bodies of 10 and 11 bytes and a declared `Content-Length: 11` give the same status, headers, bytes or limit error from a plain send and from a following call, for identity, gzip and passthrough encodings. Repeat the followed and final 302 body cases from the supplied-client criterion on each adapter, and verify streamed final-response parity; when EZ-14 is present, include its idle-bounded final-body policy.", + "artifact_path": "outbound/EZ-9/acceptance/16b/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/16", + "source_id": "EZ-9/acceptance/16", + "source_digest": "076cf85dd7d3c073f9b40ad3ea6b2ab3fae4563dc9950c587b18c2f79a2ff661", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-16-viceroy", + "sentinel": "fastly-wire-inventory", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-wire --output tests/fixtures/reusable-app/.runs/outbound/redirect/viceroy", + "expected": "Axum, workerd, Spin 4.1 and Viceroy 0.21.0. For each adapter, with a 200 and no redirect under a 10-byte buffered cap, bodies of 10 and 11 bytes and a declared `Content-Length: 11` give the same status, headers, bytes or limit error from a plain send and from a following call, for identity, gzip and passthrough encodings. Repeat the followed and final 302 body cases from the supplied-client criterion on each adapter, and verify streamed final-response parity; when EZ-14 is present, include its idle-bounded final-body policy.", + "artifact_path": "outbound/redirect/viceroy/events.jsonl", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/16b", + "source_id": "EZ-9/acceptance/16", + "source_digest": "076cf85dd7d3c073f9b40ad3ea6b2ab3fae4563dc9950c587b18c2f79a2ff661", + "requirement_ids": ["EZ-9.9", "EZ-14.11"], + "environment": "viceroy", + "applicability": "conditional", + "approval": "EZ-14 present@viceroy", + "condition": "EZ-14 present", + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-9-acceptance-16b-viceroy", + "sentinel": "ez_9_acceptance_16b_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-9/acceptance/16b", + "expected": "Axum, workerd, Spin 4.1 and Viceroy 0.21.0. For each adapter, with a 200 and no redirect under a 10-byte buffered cap, bodies of 10 and 11 bytes and a declared `Content-Length: 11` give the same status, headers, bytes or limit error from a plain send and from a following call, for identity, gzip and passthrough encodings. Repeat the followed and final 302 body cases from the supplied-client criterion on each adapter, and verify streamed final-response parity; when EZ-14 is present, include its idle-bounded final-body policy.", + "artifact_path": "outbound/EZ-9/acceptance/16b/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/16", + "source_id": "EZ-9/acceptance/16", + "source_digest": "076cf85dd7d3c073f9b40ad3ea6b2ab3fae4563dc9950c587b18c2f79a2ff661", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "workerd", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-16-workerd", + "sentinel": "cloudflare-wire-inventory", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-wire --output tests/fixtures/reusable-app/.runs/outbound/redirect/workerd", + "expected": "Axum, workerd, Spin 4.1 and Viceroy 0.21.0. For each adapter, with a 200 and no redirect under a 10-byte buffered cap, bodies of 10 and 11 bytes and a declared `Content-Length: 11` give the same status, headers, bytes or limit error from a plain send and from a following call, for identity, gzip and passthrough encodings. Repeat the followed and final 302 body cases from the supplied-client criterion on each adapter, and verify streamed final-response parity; when EZ-14 is present, include its idle-bounded final-body policy.", + "artifact_path": "outbound/redirect/workerd/events.jsonl", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/16b", + "source_id": "EZ-9/acceptance/16", + "source_digest": "076cf85dd7d3c073f9b40ad3ea6b2ab3fae4563dc9950c587b18c2f79a2ff661", + "requirement_ids": ["EZ-9.9", "EZ-14.11"], + "environment": "workerd", + "applicability": "conditional", + "approval": "EZ-14 present@workerd", + "condition": "EZ-14 present", + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-9-acceptance-16b-workerd", + "sentinel": "ez_9_acceptance_16b_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-9/acceptance/16b", + "expected": "Axum, workerd, Spin 4.1 and Viceroy 0.21.0. For each adapter, with a 200 and no redirect under a 10-byte buffered cap, bodies of 10 and 11 bytes and a declared `Content-Length: 11` give the same status, headers, bytes or limit error from a plain send and from a following call, for identity, gzip and passthrough encodings. Repeat the followed and final 302 body cases from the supplied-client criterion on each adapter, and verify streamed final-response parity; when EZ-14 is present, include its idle-bounded final-body policy.", + "artifact_path": "outbound/EZ-9/acceptance/16b/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/16", + "source_id": "EZ-9/acceptance/16", + "source_digest": "076cf85dd7d3c073f9b40ad3ea6b2ab3fae4563dc9950c587b18c2f79a2ff661", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-16-spin", + "sentinel": "spin-wire-inventory", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-wire --output tests/fixtures/reusable-app/.runs/outbound/redirect/spin", + "expected": "Axum, workerd, Spin 4.1 and Viceroy 0.21.0. For each adapter, with a 200 and no redirect under a 10-byte buffered cap, bodies of 10 and 11 bytes and a declared `Content-Length: 11` give the same status, headers, bytes or limit error from a plain send and from a following call, for identity, gzip and passthrough encodings. Repeat the followed and final 302 body cases from the supplied-client criterion on each adapter, and verify streamed final-response parity; when EZ-14 is present, include its idle-bounded final-body policy.", + "artifact_path": "outbound/redirect/spin/events.jsonl", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/16b", + "source_id": "EZ-9/acceptance/16", + "source_digest": "076cf85dd7d3c073f9b40ad3ea6b2ab3fae4563dc9950c587b18c2f79a2ff661", + "requirement_ids": ["EZ-9.9", "EZ-14.11"], + "environment": "spin", + "applicability": "conditional", + "approval": "EZ-14 present@spin", + "condition": "EZ-14 present", + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-9-acceptance-16b-spin", + "sentinel": "ez_9_acceptance_16b_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-9/acceptance/16b", + "expected": "Axum, workerd, Spin 4.1 and Viceroy 0.21.0. For each adapter, with a 200 and no redirect under a 10-byte buffered cap, bodies of 10 and 11 bytes and a declared `Content-Length: 11` give the same status, headers, bytes or limit error from a plain send and from a following call, for identity, gzip and passthrough encodings. Repeat the followed and final 302 body cases from the supplied-client criterion on each adapter, and verify streamed final-response parity; when EZ-14 is present, include its idle-bounded final-body policy.", + "artifact_path": "outbound/EZ-9/acceptance/16b/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/17", + "source_id": "EZ-9/acceptance/17", + "source_digest": "9c979270788e935349759b1739b75e7acc0349735d2ec01134db72e42358e598", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-17-axum", + "sentinel": "axum-wire-inventory", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-wire --output tests/fixtures/reusable-app/.runs/outbound/redirect/axum", + "expected": "Axum and Viceroy 0.21.0, with two local origins where A answers 302 to B and B answers 200. A plain send returns 302. A following call returns 200 with B as the final URI and one redirect followed.", + "artifact_path": "outbound/redirect/axum/events.jsonl", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/17", + "source_id": "EZ-9/acceptance/17", + "source_digest": "9c979270788e935349759b1739b75e7acc0349735d2ec01134db72e42358e598", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-17-viceroy", + "sentinel": "fastly-wire-inventory", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-wire --output tests/fixtures/reusable-app/.runs/outbound/redirect/viceroy", + "expected": "Axum and Viceroy 0.21.0, with two local origins where A answers 302 to B and B answers 200. A plain send returns 302. A following call returns 200 with B as the final URI and one redirect followed.", + "artifact_path": "outbound/redirect/viceroy/events.jsonl", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/18", + "source_id": "EZ-9/acceptance/18", + "source_digest": "cdf8129b3e5227ae8978527d1ede763b7613fae5ace002fc145318cea1ed9171", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "viceroy", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-18-viceroy", + "sentinel": "ez_9_acceptance_18_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-9/acceptance/18", + "expected": "Viceroy 0.21.0. A two-hop chain to two loopback origins with one per-call timeout leaves EZ-2.13's statistics showing the same two backend configurations as two single sends to those origins with that timeout.", + "artifact_path": "outbound/EZ-9/acceptance/18/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/19", + "source_id": "EZ-9/acceptance/19", + "source_digest": "bec1d13db1bee0aa8deed2abeaaac480b1a2f8c41bd221494176c3a1980ce9c8", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "spin", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P5", + "substep": "EZ-9-acceptance-19-spin", + "sentinel": "ez_9_acceptance_19_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-9/acceptance/19", + "expected": "Spin 4.1. A redirect to a plain-HTTP host that is not declared fails at that hop with Spin's denial, which F maps to 400 (`edgezero:crates/edgezero-adapter-spin/src/outbound.rs:1358-1364`). That Spin 4.1 reports this case as `HttpRequestDenied` is **Unverified**, and this criterion records what it reports.", + "artifact_path": "outbound/EZ-9/acceptance/19/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/20", + "source_id": "EZ-9/acceptance/20", + "source_digest": "8dbb57d70e1ff347b260665677919b84344e1363a0b2e1e0a19a5735e7c84ecd", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "consumer-unit/core", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "P5", + "substep": "EZ-9-acceptance-20-consumer-unit-core", + "sentinel": "ez_9_acceptance_20_consumer_unit_core", + "command": "consumer-runbook EZ-9/acceptance/20 --environment consumer-unit/core --sentinel ez_9_acceptance_20_consumer_unit_core", + "expected": "Trusted Server end to end. Port every redirect test in `ts:crates/trusted-server-core/src/proxy.rs:3978-4515`, the allowlist tests at `:6114-6258`, and the initial-target policy tests at `:6259-6325` to the following call, with identical recorded methods, URIs, bodies, header names and request counts. Use EZ-24 if it has landed and Trusted Server's own recording client otherwise (EZ-9.14). On the Axum dev server, a signed first-party proxy URL whose origin 302s to an allowed host returns the final 200, and one that 302s to a disallowed host returns 403.", + "artifact_path": "outbound/EZ-9/acceptance/20/consumer-unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-9/acceptance/20", + "source_id": "EZ-9/acceptance/20", + "source_digest": "8dbb57d70e1ff347b260665677919b84344e1363a0b2e1e0a19a5735e7c84ecd", + "requirement_ids": [ + "EZ-9.1", + "EZ-9.2", + "EZ-9.3", + "EZ-9.4", + "EZ-9.5", + "EZ-9.6", + "EZ-9.7", + "EZ-9.8", + "EZ-9.9", + "EZ-9.10", + "EZ-9.11", + "EZ-9.12", + "EZ-9.13", + "EZ-9.14", + "EZ-9.15", + "EZ-9.16", + "EZ-9.17", + "EZ-9.18", + "EZ-9.19" + ], + "environment": "consumer-axum", + "applicability": "blocking", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "P5", + "substep": "EZ-9-acceptance-20-consumer-axum", + "sentinel": "ez_9_acceptance_20_consumer_axum", + "command": "consumer-runbook EZ-9/acceptance/20 --environment consumer-axum --sentinel ez_9_acceptance_20_consumer_axum", + "expected": "Trusted Server end to end. Port every redirect test in `ts:crates/trusted-server-core/src/proxy.rs:3978-4515`, the allowlist tests at `:6114-6258`, and the initial-target policy tests at `:6259-6325` to the following call, with identical recorded methods, URIs, bodies, header names and request counts. Use EZ-24 if it has landed and Trusted Server's own recording client otherwise (EZ-9.14). On the Axum dev server, a signed first-party proxy URL whose origin 302s to an allowed host returns the final 200, and one that 302s to a disallowed host returns 403.", + "artifact_path": "outbound/EZ-9/acceptance/20/consumer-axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/01", + "source_id": "EZ-10/acceptance/01", + "source_digest": "1718e4144875e5c4f1601cc307c4dcdf80ff30f01182dbc2183ec07801b5118c", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-01-unit-core", + "sentinel": "ez_10_acceptance_01_unit_core", + "command": "scripts/run_test_nonzero.sh ez_10_acceptance_01_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is unit. Given a 9-byte body with no declared length, when it is read with a cap of 8 through `body_bytes`, `json_within`, `form_within`, an extractor, or `Body::into_bytes_bounded`, then the result is the 413 category and the response status is 413. For the `RequestContext` readers, a second read at any cap returns the same 413 without reading. Malformed JSON under the cap stays 400.", + "artifact_path": "outbound/EZ-10/acceptance/01/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/02", + "source_id": "EZ-10/acceptance/02", + "source_digest": "63c88c92ed776e68751a2a516c1bf951127ebd6b71618ec0d0af55c74d71edd2", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-02-unit-core", + "sentinel": "ez_10_acceptance_02_unit_core", + "command": "scripts/run_test_nonzero.sh ez_10_acceptance_02_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is unit. Given a request declaring `Content-Length: 10` with a 10-byte body, when it is read with a cap of 5, then the result is 413. With the inbound read deadline already passed, the result is 408. Record whether any body byte was read before the 413 (EZ-10.4 is SHOULD). Where none was, a later read of that request with a cap of 10 succeeds.", + "artifact_path": "outbound/EZ-10/acceptance/02/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/03", + "source_id": "EZ-10/acceptance/03", + "source_digest": "b6d2a39b3349a3e0a63d05c85c38c36838299bdca336846522996aa8e7200988", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "axum/http1-chunked", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-03-axum-http1-chunked", + "sentinel": "ez_10_acceptance_03_axum_http1_chunked", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-10/acceptance/03", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A chunked POST of 2 KiB to a route capped at 1 KiB gets 413. On Axum, an HTTP/1 request pipelined after it is either refused by a connection close or answered from the correct offset.", + "artifact_path": "outbound/EZ-10/acceptance/03/axum/http1-chunked.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/03", + "source_id": "EZ-10/acceptance/03", + "source_digest": "b6d2a39b3349a3e0a63d05c85c38c36838299bdca336846522996aa8e7200988", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "viceroy/http1-chunked", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-03-viceroy-http1-chunked", + "sentinel": "ez_10_acceptance_03_viceroy_http1_chunked", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-10/acceptance/03", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A chunked POST of 2 KiB to a route capped at 1 KiB gets 413. On Axum, an HTTP/1 request pipelined after it is either refused by a connection close or answered from the correct offset.", + "artifact_path": "outbound/EZ-10/acceptance/03/viceroy/http1-chunked.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/03", + "source_id": "EZ-10/acceptance/03", + "source_digest": "b6d2a39b3349a3e0a63d05c85c38c36838299bdca336846522996aa8e7200988", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "workerd/http1-chunked", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-03-workerd-http1-chunked", + "sentinel": "ez_10_acceptance_03_workerd_http1_chunked", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-10/acceptance/03", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A chunked POST of 2 KiB to a route capped at 1 KiB gets 413. On Axum, an HTTP/1 request pipelined after it is either refused by a connection close or answered from the correct offset.", + "artifact_path": "outbound/EZ-10/acceptance/03/workerd/http1-chunked.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/03", + "source_id": "EZ-10/acceptance/03", + "source_digest": "b6d2a39b3349a3e0a63d05c85c38c36838299bdca336846522996aa8e7200988", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "spin/http1-chunked", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-03-spin-http1-chunked", + "sentinel": "ez_10_acceptance_03_spin_http1_chunked", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-10/acceptance/03", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A chunked POST of 2 KiB to a route capped at 1 KiB gets 413. On Axum, an HTTP/1 request pipelined after it is either refused by a connection close or answered from the correct offset.", + "artifact_path": "outbound/EZ-10/acceptance/03/spin/http1-chunked.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/04", + "source_id": "EZ-10/acceptance/04", + "source_digest": "3ba85ab44855e4396c8176561b5e7242ea0442a3f76208ee5f53fe947e265164", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "consumer-hosted-fastly/http2", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F2", + "substep": "EZ-10-acceptance-04-consumer-hosted-fastly-http2", + "sentinel": "ez_10_acceptance_04_consumer_hosted_fastly_http2", + "command": "consumer-runbook EZ-10/acceptance/04 --environment consumer-hosted-fastly/http2 --sentinel ez_10_acceptance_04_consumer_hosted_fastly_http2", + "expected": "Environment is hosted Fastly, Trusted Server staging. An HTTP/2 POST with no `content-length` over the cap gets 413.", + "artifact_path": "outbound/EZ-10/acceptance/04/consumer-hosted-fastly/http2.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/05", + "source_id": "EZ-10/acceptance/05", + "source_digest": "de74487fc393ec2526a4f2100cbad816b114699fcaf19a265838f8f8e019b884", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-05-axum", + "sentinel": "ez_10_acceptance_05_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-10/acceptance/05", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A forwarded 9-byte buffered body with `max_request_body_bytes(8)` gets 413, and the origin records no request.", + "artifact_path": "outbound/EZ-10/acceptance/05/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/05", + "source_id": "EZ-10/acceptance/05", + "source_digest": "de74487fc393ec2526a4f2100cbad816b114699fcaf19a265838f8f8e019b884", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-05-viceroy", + "sentinel": "ez_10_acceptance_05_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-10/acceptance/05", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A forwarded 9-byte buffered body with `max_request_body_bytes(8)` gets 413, and the origin records no request.", + "artifact_path": "outbound/EZ-10/acceptance/05/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/05", + "source_id": "EZ-10/acceptance/05", + "source_digest": "de74487fc393ec2526a4f2100cbad816b114699fcaf19a265838f8f8e019b884", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "workerd", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-05-workerd", + "sentinel": "ez_10_acceptance_05_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-10/acceptance/05", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A forwarded 9-byte buffered body with `max_request_body_bytes(8)` gets 413, and the origin records no request.", + "artifact_path": "outbound/EZ-10/acceptance/05/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/05", + "source_id": "EZ-10/acceptance/05", + "source_digest": "de74487fc393ec2526a4f2100cbad816b114699fcaf19a265838f8f8e019b884", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-05-spin", + "sentinel": "ez_10_acceptance_05_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-10/acceptance/05", + "expected": "Environment is Axum, Viceroy 0.21.0, workerd and Spin 4.1. A forwarded 9-byte buffered body with `max_request_body_bytes(8)` gets 413, and the origin records no request.", + "artifact_path": "outbound/EZ-10/acceptance/05/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/06", + "source_id": "EZ-10/acceptance/06", + "source_digest": "f8478cf79c3c0cbae35082f8621498dc268c44c8ea23586b927ef139b14d6dbe", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-06-axum", + "sentinel": "ez_10_acceptance_06_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-10/acceptance/06", + "expected": "Environment is Axum and workerd. A streamed forwarded upload over the cap gets 413, and the origin records no request. On Viceroy 0.21.0 and Spin 4.1, the origin never records a complete request.", + "artifact_path": "outbound/EZ-10/acceptance/06/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/06", + "source_id": "EZ-10/acceptance/06", + "source_digest": "f8478cf79c3c0cbae35082f8621498dc268c44c8ea23586b927ef139b14d6dbe", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-06-viceroy", + "sentinel": "ez_10_acceptance_06_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-10/acceptance/06", + "expected": "Environment is Axum and workerd. A streamed forwarded upload over the cap gets 413, and the origin records no request. On Viceroy 0.21.0 and Spin 4.1, the origin never records a complete request.", + "artifact_path": "outbound/EZ-10/acceptance/06/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/06", + "source_id": "EZ-10/acceptance/06", + "source_digest": "f8478cf79c3c0cbae35082f8621498dc268c44c8ea23586b927ef139b14d6dbe", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "workerd", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-06-workerd", + "sentinel": "ez_10_acceptance_06_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-10/acceptance/06", + "expected": "Environment is Axum and workerd. A streamed forwarded upload over the cap gets 413, and the origin records no request. On Viceroy 0.21.0 and Spin 4.1, the origin never records a complete request.", + "artifact_path": "outbound/EZ-10/acceptance/06/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/06", + "source_id": "EZ-10/acceptance/06", + "source_digest": "f8478cf79c3c0cbae35082f8621498dc268c44c8ea23586b927ef139b14d6dbe", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-06-spin", + "sentinel": "ez_10_acceptance_06_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-10/acceptance/06", + "expected": "Environment is Axum and workerd. A streamed forwarded upload over the cap gets 413, and the origin records no request. On Viceroy 0.21.0 and Spin 4.1, the origin never records a complete request.", + "artifact_path": "outbound/EZ-10/acceptance/06/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/07", + "source_id": "EZ-10/acceptance/07", + "source_digest": "054aa19e144b18afecda7b0f291ab17cac1e5285c7647cec295284883ee2d445", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-07-axum", + "sentinel": "ez_10_acceptance_07_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-10/acceptance/07", + "expected": "Environment is Axum. An extractor route with a custom error renderer answers an oversized body with 413, and the response has no `Retry-After`.", + "artifact_path": "outbound/EZ-10/acceptance/07/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/08", + "source_id": "EZ-10/acceptance/08", + "source_digest": "938e291c1b594fda0f1d716556fb74b657384c9e677481862736d53489bbeeca", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "axum/generated", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-08-axum-generated", + "sentinel": "ez_10_acceptance_08_axum_generated", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-10/acceptance/08", + "expected": "Environment is Axum, using the generated scaffold and the demo app. An over-cap request to an unmatched route gets 413.", + "artifact_path": "outbound/EZ-10/acceptance/08/axum/generated.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/08", + "source_id": "EZ-10/acceptance/08", + "source_digest": "938e291c1b594fda0f1d716556fb74b657384c9e677481862736d53489bbeeca", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "axum/demo", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-08-axum-demo", + "sentinel": "ez_10_acceptance_08_axum_demo", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-10/acceptance/08", + "expected": "Environment is Axum, using the generated scaffold and the demo app. An over-cap request to an unmatched route gets 413.", + "artifact_path": "outbound/EZ-10/acceptance/08/axum/demo.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/09", + "source_id": "EZ-10/acceptance/09", + "source_digest": "6567b07324ce76fadf87806d733f6fe8972cb31a96ee392285694ea2d9d27684", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "consumer-unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F2", + "substep": "EZ-10-acceptance-09-consumer-unit-core", + "sentinel": "ez_10_acceptance_09_consumer_unit_core", + "command": "consumer-runbook EZ-10/acceptance/09 --environment consumer-unit/core --sentinel ez_10_acceptance_09_consumer_unit_core", + "expected": "Trusted Server end to end. With TS reading bodies through EdgeZero's readers and mapping the overflow category to its request-too-large error, the read-cap 413 tests pass unchanged. They are `ts:crates/trusted-server-core/src/auction/endpoints.rs:1771-1791`, `proxy.rs:6343` and `:6389`, `request_signing/endpoints.rs:995`, `:1034` and `:1073`, and `integrations/google_tag_manager.rs:2763-2817`. A new wire-level case gets 413 for an oversized chunked `POST /auction` with no `Content-Length`. A controlled-stream unit case for batch sync delivers more bytes than both its declared length and the cap and gets 413. Wire-level understated-length cases instead record host framing rejection or the body actually exposed to EdgeZero; they do not require 413 when the host never delivers an over-cap body.", + "artifact_path": "outbound/EZ-10/acceptance/09/consumer-unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/09", + "source_id": "EZ-10/acceptance/09", + "source_digest": "6567b07324ce76fadf87806d733f6fe8972cb31a96ee392285694ea2d9d27684", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "consumer-wire", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F2", + "substep": "EZ-10-acceptance-09-consumer-wire", + "sentinel": "ez_10_acceptance_09_consumer_wire", + "command": "consumer-runbook EZ-10/acceptance/09 --environment consumer-wire --sentinel ez_10_acceptance_09_consumer_wire", + "expected": "Trusted Server end to end. With TS reading bodies through EdgeZero's readers and mapping the overflow category to its request-too-large error, the read-cap 413 tests pass unchanged. They are `ts:crates/trusted-server-core/src/auction/endpoints.rs:1771-1791`, `proxy.rs:6343` and `:6389`, `request_signing/endpoints.rs:995`, `:1034` and `:1073`, and `integrations/google_tag_manager.rs:2763-2817`. A new wire-level case gets 413 for an oversized chunked `POST /auction` with no `Content-Length`. A controlled-stream unit case for batch sync delivers more bytes than both its declared length and the cap and gets 413. Wire-level understated-length cases instead record host framing rejection or the body actually exposed to EdgeZero; they do not require 413 when the host never delivers an over-cap body.", + "artifact_path": "outbound/EZ-10/acceptance/09/consumer-wire.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/10", + "source_id": "EZ-10/acceptance/10", + "source_digest": "3bc2808dabc4212a7f00cd083c9733a14aad55fe2eca7ad5fc14ed6e9f891af7", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "unit/axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-10-unit-axum", + "sentinel": "ez_10_acceptance_10_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_10_acceptance_10_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Unit, each adapter's response handling. An upstream 413 with `Retry-After: 120` remains an ordinary response and retains that header; an EdgeZero-generated cap-overflow error has no `Retry-After`.", + "artifact_path": "outbound/EZ-10/acceptance/10/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/10", + "source_id": "EZ-10/acceptance/10", + "source_digest": "3bc2808dabc4212a7f00cd083c9733a14aad55fe2eca7ad5fc14ed6e9f891af7", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "unit/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-10-unit-fastly", + "sentinel": "ez_10_acceptance_10_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_10_acceptance_10_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit, each adapter's response handling. An upstream 413 with `Retry-After: 120` remains an ordinary response and retains that header; an EdgeZero-generated cap-overflow error has no `Retry-After`.", + "artifact_path": "outbound/EZ-10/acceptance/10/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/10", + "source_id": "EZ-10/acceptance/10", + "source_digest": "3bc2808dabc4212a7f00cd083c9733a14aad55fe2eca7ad5fc14ed6e9f891af7", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "unit/cloudflare", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-10-unit-cloudflare", + "sentinel": "ez_10_acceptance_10_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_10_acceptance_10_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Unit, each adapter's response handling. An upstream 413 with `Retry-After: 120` remains an ordinary response and retains that header; an EdgeZero-generated cap-overflow error has no `Retry-After`.", + "artifact_path": "outbound/EZ-10/acceptance/10/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-10/acceptance/10", + "source_id": "EZ-10/acceptance/10", + "source_digest": "3bc2808dabc4212a7f00cd083c9733a14aad55fe2eca7ad5fc14ed6e9f891af7", + "requirement_ids": [ + "EZ-10.1", + "EZ-10.2", + "EZ-10.3", + "EZ-10.4", + "EZ-10.5", + "EZ-10.6", + "EZ-10.7", + "EZ-10.8", + "EZ-10.9", + "EZ-10.10", + "EZ-10.11", + "EZ-10.12" + ], + "environment": "unit/spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F2", + "substep": "EZ-10-acceptance-10-unit-spin", + "sentinel": "ez_10_acceptance_10_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_10_acceptance_10_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Unit, each adapter's response handling. An upstream 413 with `Retry-After: 120` remains an ordinary response and retains that header; an EdgeZero-generated cap-overflow error has no `Retry-After`.", + "artifact_path": "outbound/EZ-10/acceptance/10/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-11/acceptance/01", + "source_id": "EZ-11/acceptance/01", + "source_digest": "2fffc9721bf0e5d75bc70eca38fe2b0e388dde7f93629bf64c7508e76d7d33ea", + "requirement_ids": [ + "EZ-11.1", + "EZ-11.2", + "EZ-11.3", + "EZ-11.4", + "EZ-11.5", + "EZ-11.6", + "EZ-11.7", + "EZ-11.8", + "EZ-11.9" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3a", + "substep": "EZ-11-acceptance-01-unit-core", + "sentinel": "ez_11_acceptance_01_unit_core", + "command": "scripts/run_test_nonzero.sh ez_11_acceptance_01_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Unit. No tags, an empty tag, `a b`, a non-ASCII tag and `0x7F` are client-class errors. `ts-template` with a reader-URL tag is accepted. `a`, `a` collapses to `a`.", + "artifact_path": "outbound/EZ-11/acceptance/01/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-11/acceptance/02", + "source_id": "EZ-11/acceptance/02", + "source_digest": "bf445c42708e3c1fa7fb29953e16ef9f52e1ca03eb0e55b3430a343269cdd322", + "requirement_ids": [ + "EZ-11.1", + "EZ-11.2", + "EZ-11.3", + "EZ-11.4", + "EZ-11.5", + "EZ-11.6", + "EZ-11.7", + "EZ-11.8", + "EZ-11.9" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3a", + "substep": "EZ-11-acceptance-02-viceroy", + "sentinel": "ez_11_acceptance_02_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-11/acceptance/02", + "expected": "Viceroy 0.21.0. A tagged single send and a tagged two-slot batch complete without error.", + "artifact_path": "outbound/EZ-11/acceptance/02/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-11/acceptance/03", + "source_id": "EZ-11/acceptance/03", + "source_digest": "a95fcc9b8ccfadc79ba6b8296728653bad47c1986d26b074689633b12fdac98c", + "requirement_ids": [ + "EZ-11.1", + "EZ-11.2", + "EZ-11.3", + "EZ-11.4", + "EZ-11.5", + "EZ-11.6", + "EZ-11.7", + "EZ-11.8", + "EZ-11.9" + ], + "environment": "workerd", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3a", + "substep": "EZ-11-acceptance-03-workerd", + "sentinel": "ez_11_acceptance_03_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-11/acceptance/03", + "expected": "workerd. A tagged request returns the rejection, and no fetch is made.", + "artifact_path": "outbound/EZ-11/acceptance/03/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-11/acceptance/04", + "source_id": "EZ-11/acceptance/04", + "source_digest": "fc595d2cfab1f64ca92fa8d71155b529a054398082f978bd2bc21e51ee8685ad", + "requirement_ids": [ + "EZ-11.1", + "EZ-11.2", + "EZ-11.3", + "EZ-11.4", + "EZ-11.5", + "EZ-11.6", + "EZ-11.7", + "EZ-11.8", + "EZ-11.9" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3a", + "substep": "EZ-11-acceptance-04-axum", + "sentinel": "ez_11_acceptance_04_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-11/acceptance/04", + "expected": "Axum. A tagged request succeeds, and the origin receives no `surrogate-key` or `cache-control` header added by EdgeZero.", + "artifact_path": "outbound/EZ-11/acceptance/04/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-11/acceptance/05", + "source_id": "EZ-11/acceptance/05", + "source_digest": "329c8ffb14359193160f2e1f35eb8e2dd36103f1a50eb53988742a0474530b43", + "requirement_ids": [ + "EZ-11.1", + "EZ-11.2", + "EZ-11.3", + "EZ-11.4", + "EZ-11.5", + "EZ-11.6", + "EZ-11.7", + "EZ-11.8", + "EZ-11.9" + ], + "environment": "hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3a", + "substep": "EZ-11-acceptance-05-hosted-fastly", + "sentinel": "ez_11_acceptance_05_hosted_fastly", + "command": "hosted-runbook EZ-11/acceptance/05 --environment hosted-fastly --sentinel ez_11_acceptance_05_hosted_fastly", + "expected": "Hosted Fastly. Given a cacheable origin path that counts the requests it receives, two fetches tagged `probe-key` leave the origin count at one. After a purge of `probe-key`, a third fetch raises it to two. The same holds when the fetches are begun requests and when they are batch slots.", + "artifact_path": "outbound/EZ-11/acceptance/05/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-11/acceptance/06", + "source_id": "EZ-11/acceptance/06", + "source_digest": "51480b87574ea6875040fa6ced70fa0bd2a38a8a345b39cf858d38f6e1a42f85", + "requirement_ids": [ + "EZ-11.1", + "EZ-11.2", + "EZ-11.3", + "EZ-11.4", + "EZ-11.5", + "EZ-11.6", + "EZ-11.7", + "EZ-11.8", + "EZ-11.9" + ], + "environment": "hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3a", + "substep": "EZ-11-acceptance-06-hosted-fastly", + "sentinel": "ez_11_acceptance_06_hosted_fastly", + "command": "hosted-runbook EZ-11/acceptance/06 --environment hosted-fastly --sentinel ez_11_acceptance_06_hosted_fastly", + "expected": "Hosted Fastly. Compare default and tagged single sends, begun requests, and batch slots against separate origin paths returning `Cache-Control: no-store` and `Cache-Control: private`. Neither policy may cache those responses: every request reaches the origin. The record confirms that a key-only override preserves these non-cacheable directives rather than forcing storage.", + "artifact_path": "outbound/EZ-11/acceptance/06/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-11/acceptance/07", + "source_id": "EZ-11/acceptance/07", + "source_digest": "8ace2a8914188b2ea9874f97ab01d3a12b5548dcc8832231e6af5616a572d3f4", + "requirement_ids": [ + "EZ-11.1", + "EZ-11.2", + "EZ-11.3", + "EZ-11.4", + "EZ-11.5", + "EZ-11.6", + "EZ-11.7", + "EZ-11.8", + "EZ-11.9" + ], + "environment": "consumer-hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F3a", + "substep": "EZ-11-acceptance-07-consumer-hosted-fastly", + "sentinel": "ez_11_acceptance_07_consumer_hosted_fastly", + "command": "consumer-runbook EZ-11/acceptance/07 --environment consumer-hosted-fastly --sentinel ez_11_acceptance_07_consumer_hosted_fastly", + "expected": "Trusted Server end to end. With readthrough enabled on a Fastly staging service, purging `ts-template` turns a cached page into a miss, and the origin sees the request again. Trusted Server unit tests assert the tagged policy at both origin send sites, using EZ-24 if it has landed or Trusted Server's own double otherwise.", + "artifact_path": "outbound/EZ-11/acceptance/07/consumer-hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-11/acceptance/07", + "source_id": "EZ-11/acceptance/07", + "source_digest": "8ace2a8914188b2ea9874f97ab01d3a12b5548dcc8832231e6af5616a572d3f4", + "requirement_ids": [ + "EZ-11.1", + "EZ-11.2", + "EZ-11.3", + "EZ-11.4", + "EZ-11.5", + "EZ-11.6", + "EZ-11.7", + "EZ-11.8", + "EZ-11.9" + ], + "environment": "consumer-unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F3a", + "substep": "EZ-11-acceptance-07-consumer-unit-core", + "sentinel": "ez_11_acceptance_07_consumer_unit_core", + "command": "consumer-runbook EZ-11/acceptance/07 --environment consumer-unit/core --sentinel ez_11_acceptance_07_consumer_unit_core", + "expected": "Trusted Server end to end. With readthrough enabled on a Fastly staging service, purging `ts-template` turns a cached page into a miss, and the origin sees the request again. Trusted Server unit tests assert the tagged policy at both origin send sites, using EZ-24 if it has landed or Trusted Server's own double otherwise.", + "artifact_path": "outbound/EZ-11/acceptance/07/consumer-unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-12/acceptance/01", + "source_id": "EZ-12/acceptance/01", + "source_digest": "b70367b84ac31be6d9ff163aa7b48fa1087782652f464cb1e54e5db17c03052e", + "requirement_ids": [ + "EZ-12.1", + "EZ-12.2", + "EZ-12.3", + "EZ-12.4", + "EZ-12.5", + "EZ-12.6", + "EZ-12.7", + "EZ-12.8" + ], + "environment": "unit/axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3b", + "substep": "EZ-12-acceptance-01-unit-axum", + "sentinel": "ez_12_acceptance_01_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_12_acceptance_01_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Unit. Chain-only on an `http` URI sends normally on every adapter.", + "artifact_path": "outbound/EZ-12/acceptance/01/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-12/acceptance/01", + "source_id": "EZ-12/acceptance/01", + "source_digest": "b70367b84ac31be6d9ff163aa7b48fa1087782652f464cb1e54e5db17c03052e", + "requirement_ids": [ + "EZ-12.1", + "EZ-12.2", + "EZ-12.3", + "EZ-12.4", + "EZ-12.5", + "EZ-12.6", + "EZ-12.7", + "EZ-12.8" + ], + "environment": "unit/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3b", + "substep": "EZ-12-acceptance-01-unit-fastly", + "sentinel": "ez_12_acceptance_01_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_12_acceptance_01_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit. Chain-only on an `http` URI sends normally on every adapter.", + "artifact_path": "outbound/EZ-12/acceptance/01/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-12/acceptance/01", + "source_id": "EZ-12/acceptance/01", + "source_digest": "b70367b84ac31be6d9ff163aa7b48fa1087782652f464cb1e54e5db17c03052e", + "requirement_ids": [ + "EZ-12.1", + "EZ-12.2", + "EZ-12.3", + "EZ-12.4", + "EZ-12.5", + "EZ-12.6", + "EZ-12.7", + "EZ-12.8" + ], + "environment": "unit/cloudflare", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3b", + "substep": "EZ-12-acceptance-01-unit-cloudflare", + "sentinel": "ez_12_acceptance_01_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_12_acceptance_01_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Unit. Chain-only on an `http` URI sends normally on every adapter.", + "artifact_path": "outbound/EZ-12/acceptance/01/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-12/acceptance/01", + "source_id": "EZ-12/acceptance/01", + "source_digest": "b70367b84ac31be6d9ff163aa7b48fa1087782652f464cb1e54e5db17c03052e", + "requirement_ids": [ + "EZ-12.1", + "EZ-12.2", + "EZ-12.3", + "EZ-12.4", + "EZ-12.5", + "EZ-12.6", + "EZ-12.7", + "EZ-12.8" + ], + "environment": "unit/spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3b", + "substep": "EZ-12-acceptance-01-unit-spin", + "sentinel": "ez_12_acceptance_01_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_12_acceptance_01_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Unit. Chain-only on an `http` URI sends normally on every adapter.", + "artifact_path": "outbound/EZ-12/acceptance/01/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-12/acceptance/02", + "source_id": "EZ-12/acceptance/02", + "source_digest": "94f6f2e363d096916f1e7960ef32f58d19e1c31fbb90f6cfd815ed6776cc2f74", + "requirement_ids": [ + "EZ-12.1", + "EZ-12.2", + "EZ-12.3", + "EZ-12.4", + "EZ-12.5", + "EZ-12.6", + "EZ-12.7", + "EZ-12.8" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3b", + "substep": "EZ-12-acceptance-02-axum", + "sentinel": "ez_12_acceptance_02_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-12/acceptance/02", + "expected": "Axum, workerd and Spin 4.1. Chain-only on `https` returns the rejection and nothing is sent.", + "artifact_path": "outbound/EZ-12/acceptance/02/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-12/acceptance/02", + "source_id": "EZ-12/acceptance/02", + "source_digest": "94f6f2e363d096916f1e7960ef32f58d19e1c31fbb90f6cfd815ed6776cc2f74", + "requirement_ids": [ + "EZ-12.1", + "EZ-12.2", + "EZ-12.3", + "EZ-12.4", + "EZ-12.5", + "EZ-12.6", + "EZ-12.7", + "EZ-12.8" + ], + "environment": "workerd", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3b", + "substep": "EZ-12-acceptance-02-workerd", + "sentinel": "ez_12_acceptance_02_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-12/acceptance/02", + "expected": "Axum, workerd and Spin 4.1. Chain-only on `https` returns the rejection and nothing is sent.", + "artifact_path": "outbound/EZ-12/acceptance/02/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-12/acceptance/02", + "source_id": "EZ-12/acceptance/02", + "source_digest": "94f6f2e363d096916f1e7960ef32f58d19e1c31fbb90f6cfd815ed6776cc2f74", + "requirement_ids": [ + "EZ-12.1", + "EZ-12.2", + "EZ-12.3", + "EZ-12.4", + "EZ-12.5", + "EZ-12.6", + "EZ-12.7", + "EZ-12.8" + ], + "environment": "spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3b", + "substep": "EZ-12-acceptance-02-spin", + "sentinel": "ez_12_acceptance_02_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-12/acceptance/02", + "expected": "Axum, workerd and Spin 4.1. Chain-only on `https` returns the rejection and nothing is sent.", + "artifact_path": "outbound/EZ-12/acceptance/02/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-12/acceptance/03", + "source_id": "EZ-12/acceptance/03", + "source_digest": "53e815ee0dcf61c120f1dcd59cf924e8c8885c028acfc7de46997cd455e3ec4e", + "requirement_ids": [ + "EZ-12.1", + "EZ-12.2", + "EZ-12.3", + "EZ-12.4", + "EZ-12.5", + "EZ-12.6", + "EZ-12.7", + "EZ-12.8" + ], + "environment": "hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3b", + "substep": "EZ-12-acceptance-03-hosted-fastly", + "sentinel": "ez_12_acceptance_03_hosted_fastly", + "command": "hosted-runbook EZ-12/acceptance/03 --environment hosted-fastly --sentinel ez_12_acceptance_03_hosted_fastly", + "expected": "Hosted Fastly. An origin reached by IP whose certificate is valid for a hostname fails with full verification (502) and succeeds with chain-only. An origin with a self-signed certificate fails in both modes. Given an origin that logs the connection each request arrives on, a full and a chain-only request to it in one sandbox arrive on different connections. Viceroy cannot show the mode difference, because it checks the SNI hostname or the URI host whenever no certificate host is given.", + "artifact_path": "outbound/EZ-12/acceptance/03/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-12/acceptance/04", + "source_id": "EZ-12/acceptance/04", + "source_digest": "0deef705805283b8e15adaaaddab8f20f2c9b49ad846c1e2e395f9eed0e71d88", + "requirement_ids": [ + "EZ-12.1", + "EZ-12.2", + "EZ-12.3", + "EZ-12.4", + "EZ-12.5", + "EZ-12.6", + "EZ-12.7", + "EZ-12.8" + ], + "environment": "consumer-hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F3b", + "substep": "EZ-12-acceptance-04-consumer-hosted-fastly", + "sentinel": "ez_12_acceptance_04_consumer_hosted_fastly", + "command": "consumer-runbook EZ-12/acceptance/04 --environment consumer-hosted-fastly --sentinel ez_12_acceptance_04_consumer_hosted_fastly", + "expected": "Trusted Server end to end. With `certificate_check = false` on a Fastly staging service whose origin is an IP-literal HTTPS URL, publisher pages load.", + "artifact_path": "outbound/EZ-12/acceptance/04/consumer-hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-13/acceptance/01", + "source_id": "EZ-13/acceptance/01", + "source_digest": "4d652ab0a99f09ef7e062522b05f795ca06e7135ba94a23c87aaedd1e6b77f64", + "requirement_ids": ["EZ-13.1", "EZ-13.2", "EZ-13.3", "EZ-13.4"], + "environment": "spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3c", + "substep": "EZ-13-acceptance-01-spin", + "sentinel": "ez_13_acceptance_01_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-13/acceptance/01", + "expected": "Spin 4.1. A request with an override, sent alone or in a batch, returns the rejection with status 400, and no WASI request is made.", + "artifact_path": "outbound/EZ-13/acceptance/01/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-13/acceptance/02", + "source_id": "EZ-13/acceptance/02", + "source_digest": "c23920bd0d5cc25e83b131caa5c8ae5556fe43f53536df46cd71b734ee8327e0", + "requirement_ids": ["EZ-13.1", "EZ-13.2", "EZ-13.3", "EZ-13.4"], + "environment": "spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F3c", + "substep": "EZ-13-acceptance-02-spin", + "sentinel": "ez_13_acceptance_02_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-13/acceptance/02", + "expected": "Spin 4.1. A component that sets a `host` field records whether the host forbids it. A request to a local origin records the `Host` received, which is expected to equal the authority. Both results appear in the footnote.", + "artifact_path": "outbound/EZ-13/acceptance/02/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-13/acceptance/03", + "source_id": "EZ-13/acceptance/03", + "source_digest": "2cab3d473eeead7a8a4d066b532c75537b7d0d8dfc6dc651071aea89c55aa51c", + "requirement_ids": ["EZ-13.1", "EZ-13.2", "EZ-13.3", "EZ-13.4"], + "environment": "consumer-spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F3c", + "substep": "EZ-13-acceptance-03-consumer-spin", + "sentinel": "ez_13_acceptance_03_consumer_spin", + "command": "consumer-runbook EZ-13/acceptance/03 --environment consumer-spin --sentinel ez_13_acceptance_03_consumer_spin", + "expected": "Trusted Server end to end, Spin 4.1. A Spin build whose config sets `publisher.origin_host_header_override` fails config validation with a message pointing to this limitation. A Spin build without it serves publisher pages.", + "artifact_path": "outbound/EZ-13/acceptance/03/consumer-spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/01", + "source_id": "EZ-14/acceptance/01", + "source_digest": "912d08eaa3add46c570ca1e50aa38e94972fe0d84ac4636c314c898c94c22e2d", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-01-unit-core", + "sentinel": "ez_14_acceptance_01_unit_core", + "command": "scripts/run_test_nonzero.sh ez_14_acceptance_01_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit.** Given an idle bound on a buffered single send, or a zero idle duration, then each request is rejected before sending as an invalid request. Given an idle bound on a batch slot, then the slot is refused by the batch's streamed-response rule.", + "artifact_path": "outbound/EZ-14/acceptance/01/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/02", + "source_id": "EZ-14/acceptance/02", + "source_digest": "45b8a013a7fdc37b7763bfe8660662a5aca01f6c6af1abce97a92ab4ee5686ce", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-02-axum", + "sentinel": "ez_14_acceptance_02_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-14/acceptance/02", + "expected": "**Axum.** Given an origin that sends headers and then 8 chunks 250 ms apart, a per-call timeout of 500 ms, and an idle bound of 400 ms, then the full body arrives in about 2 s.", + "artifact_path": "outbound/EZ-14/acceptance/02/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/03", + "source_id": "EZ-14/acceptance/03", + "source_digest": "2f95154765f51b502ca94a26af6e6feb3a17ab3b3054c2fac63ed192152b0609", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-03-axum", + "sentinel": "ez_14_acceptance_03_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-14/acceptance/03", + "expected": "**Axum.** Given the same setup with one 600 ms gap, then the read fails with the idle timeout about 400 ms after the previous chunk, and the cause differs from a per-call timeout.", + "artifact_path": "outbound/EZ-14/acceptance/03/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/04", + "source_id": "EZ-14/acceptance/04", + "source_digest": "55e5b56f4cb9188dfe62c1d82e0f3d08c415901a1193a2897268d3a7a50c5a61", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-04-axum", + "sentinel": "ez_14_acceptance_04_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-14/acceptance/04", + "expected": "**Axum.** Given a fast origin and an application that waits 1 s between reads, with a 400 ms idle bound, then no idle error occurs.", + "artifact_path": "outbound/EZ-14/acceptance/04/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/05", + "source_id": "EZ-14/acceptance/05", + "source_digest": "693340385a7cbb528773e8b53f21ae070f522b4aad3b69e5718ed8bf8afd5971", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-05-axum", + "sentinel": "ez_14_acceptance_05_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-14/acceptance/05", + "expected": "**Axum.** Given an idle bound and a deadline 1 s after the send, then a body still flowing at 1 s ends with a deadline timeout.", + "artifact_path": "outbound/EZ-14/acceptance/05/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/06", + "source_id": "EZ-14/acceptance/06", + "source_digest": "ac5e7bb08a5ea8de12deed8aa04a71b9bc303236b95dd18a9cb10970a892a807", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-06-viceroy", + "sentinel": "ez_14_acceptance_06_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-14/acceptance/06", + "expected": "**Viceroy 0.21.0, workerd, Spin 4.1.** Run scenarios 2, 3, and 4 against an origin that trickles its body. Viceroy enforces the platform between-bytes timer and measures it from delivery to the guest, so scenario 2 shows that EdgeZero sets no platform timer that ends a progressing body early, and scenario 4 shows that a paused reader is not failed (EZ-14.8).", + "artifact_path": "outbound/EZ-14/acceptance/06/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/06", + "source_id": "EZ-14/acceptance/06", + "source_digest": "ac5e7bb08a5ea8de12deed8aa04a71b9bc303236b95dd18a9cb10970a892a807", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "workerd", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-06-workerd", + "sentinel": "ez_14_acceptance_06_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-14/acceptance/06", + "expected": "**Viceroy 0.21.0, workerd, Spin 4.1.** Run scenarios 2, 3, and 4 against an origin that trickles its body. Viceroy enforces the platform between-bytes timer and measures it from delivery to the guest, so scenario 2 shows that EdgeZero sets no platform timer that ends a progressing body early, and scenario 4 shows that a paused reader is not failed (EZ-14.8).", + "artifact_path": "outbound/EZ-14/acceptance/06/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/06", + "source_id": "EZ-14/acceptance/06", + "source_digest": "ac5e7bb08a5ea8de12deed8aa04a71b9bc303236b95dd18a9cb10970a892a807", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-06-spin", + "sentinel": "ez_14_acceptance_06_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-14/acceptance/06", + "expected": "**Viceroy 0.21.0, workerd, Spin 4.1.** Run scenarios 2, 3, and 4 against an origin that trickles its body. Viceroy enforces the platform between-bytes timer and measures it from delivery to the guest, so scenario 2 shows that EdgeZero sets no platform timer that ends a progressing body early, and scenario 4 shows that a paused reader is not failed (EZ-14.8).", + "artifact_path": "outbound/EZ-14/acceptance/06/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/07", + "source_id": "EZ-14/acceptance/07", + "source_digest": "b4ecea1c27acd38b37fdaa72e8fcb44e8f265c865d167dd3d678e49e30c40604", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-07-hosted-fastly", + "sentinel": "ez_14_acceptance_07_hosted_fastly", + "command": "hosted-runbook EZ-14/acceptance/07 --environment hosted-fastly --sentinel ez_14_acceptance_07_hosted_fastly", + "expected": "**Hosted Fastly.** Run scenarios 2 to 4 against a real origin. Add an idle bound longer than the per-call timeout, with chunks spaced between the two. No platform timer may end the body early.", + "artifact_path": "outbound/EZ-14/acceptance/07/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/08", + "source_id": "EZ-14/acceptance/08", + "source_digest": "1c18de7409a6a08fae58e435ccee6608e902df66add60c5b777eb3c65a5b6939", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "consumer-viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F4", + "substep": "EZ-14-acceptance-08-consumer-viceroy", + "sentinel": "ez_14_acceptance_08_consumer_viceroy", + "command": "consumer-runbook EZ-14/acceptance/08 --environment consumer-viceroy --sentinel ez_14_acceptance_08_consumer_viceroy", + "expected": "**Trusted Server, Viceroy 0.21.0 and hosted Fastly.** An origin streams an HTML page over 20 s in chunks 2 s apart, and the client receives the full rewritten document. A 16 s stall mid-body ends the client body at about 15 s, and TS logs an idle timeout.", + "artifact_path": "outbound/EZ-14/acceptance/08/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/08", + "source_id": "EZ-14/acceptance/08", + "source_digest": "1c18de7409a6a08fae58e435ccee6608e902df66add60c5b777eb3c65a5b6939", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "consumer-hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F4", + "substep": "EZ-14-acceptance-08-consumer-hosted-fastly", + "sentinel": "ez_14_acceptance_08_consumer_hosted_fastly", + "command": "consumer-runbook EZ-14/acceptance/08 --environment consumer-hosted-fastly --sentinel ez_14_acceptance_08_consumer_hosted_fastly", + "expected": "**Trusted Server, Viceroy 0.21.0 and hosted Fastly.** An origin streams an HTML page over 20 s in chunks 2 s apart, and the client receives the full rewritten document. A 16 s stall mid-body ends the client body at about 15 s, and TS logs an idle timeout.", + "artifact_path": "outbound/EZ-14/acceptance/08/consumer-hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/09", + "source_id": "EZ-14/acceptance/09", + "source_digest": "78060be9940b82af4b536cbcba0aee53a7de88629c612c39441354df22db2903", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-09-unit-core", + "sentinel": "ez_14_acceptance_09_unit_core", + "command": "scripts/run_test_nonzero.sh ez_14_acceptance_09_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit and Axum.** A valid gzip stream sends nonempty encoded chunks, including empty DEFLATE blocks, every 250 ms under a 400 ms idle bound but produces no decoded bytes for more than 400 ms. The pending decoded read does not fail as idle while encoded bytes arrive. If the encoded source then stays pending for 600 ms, the read fails with the idle-timeout cause after 400 ms of waiting without further upstream bytes. Zero-length source chunks do not postpone that expiry.", + "artifact_path": "outbound/EZ-14/acceptance/09/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/09", + "source_id": "EZ-14/acceptance/09", + "source_digest": "78060be9940b82af4b536cbcba0aee53a7de88629c612c39441354df22db2903", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-09-axum", + "sentinel": "ez_14_acceptance_09_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-14/acceptance/09", + "expected": "**Unit and Axum.** A valid gzip stream sends nonempty encoded chunks, including empty DEFLATE blocks, every 250 ms under a 400 ms idle bound but produces no decoded bytes for more than 400 ms. The pending decoded read does not fail as idle while encoded bytes arrive. If the encoded source then stays pending for 600 ms, the read fails with the idle-timeout cause after 400 ms of waiting without further upstream bytes. Zero-length source chunks do not postpone that expiry.", + "artifact_path": "outbound/EZ-14/acceptance/09/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/10", + "source_id": "EZ-14/acceptance/10", + "source_digest": "04767dacf1d92a6004d6aab8d2a11637b39ff4a3b02d6f02419303b0b77b99fe", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-10-unit-core", + "sentinel": "ez_14_acceptance_10_unit_core", + "command": "scripts/run_test_nonzero.sh ez_14_acceptance_10_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit with an injected clock and Axum.** A body read starts at 600 ms with a 400 ms idle bound and an application-set absolute deadline at 1,000 ms. With no further upstream bytes, simultaneous expiry at 1,000 ms returns the deadline's gateway-timeout cause, not the idle cause. A control with a later absolute deadline returns the idle cause instead.", + "artifact_path": "outbound/EZ-14/acceptance/10/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/10", + "source_id": "EZ-14/acceptance/10", + "source_digest": "04767dacf1d92a6004d6aab8d2a11637b39ff4a3b02d6f02419303b0b77b99fe", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-10-axum", + "sentinel": "ez_14_acceptance_10_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-14/acceptance/10", + "expected": "**Unit with an injected clock and Axum.** A body read starts at 600 ms with a 400 ms idle bound and an application-set absolute deadline at 1,000 ms. With no further upstream bytes, simultaneous expiry at 1,000 ms returns the deadline's gateway-timeout cause, not the idle cause. A control with a later absolute deadline returns the idle cause instead.", + "artifact_path": "outbound/EZ-14/acceptance/10/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/11", + "source_id": "EZ-14/acceptance/11", + "source_digest": "3c0ed413fd09306536c7c47d81191b51dd16ae773cca7636c1b6c5da58560b87", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "unit/axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-11-unit-axum", + "sentinel": "ez_14_acceptance_11_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_14_acceptance_11_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "**Unit, every adapter's timeout classifier.** Given a platform failure first observed after both the pending read's idle expiry and the application's absolute deadline, classification returns the absolute deadline's `BudgetSource`, not the idle cause, a transport error, or an internal error. A control observed after idle expiry but before the absolute deadline returns the idle cause. The delayed observation does not extend either limit.", + "artifact_path": "outbound/EZ-14/acceptance/11/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/11", + "source_id": "EZ-14/acceptance/11", + "source_digest": "3c0ed413fd09306536c7c47d81191b51dd16ae773cca7636c1b6c5da58560b87", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "unit/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-11-unit-fastly", + "sentinel": "ez_14_acceptance_11_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_14_acceptance_11_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "**Unit, every adapter's timeout classifier.** Given a platform failure first observed after both the pending read's idle expiry and the application's absolute deadline, classification returns the absolute deadline's `BudgetSource`, not the idle cause, a transport error, or an internal error. A control observed after idle expiry but before the absolute deadline returns the idle cause. The delayed observation does not extend either limit.", + "artifact_path": "outbound/EZ-14/acceptance/11/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/11", + "source_id": "EZ-14/acceptance/11", + "source_digest": "3c0ed413fd09306536c7c47d81191b51dd16ae773cca7636c1b6c5da58560b87", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "unit/cloudflare", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-11-unit-cloudflare", + "sentinel": "ez_14_acceptance_11_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_14_acceptance_11_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "**Unit, every adapter's timeout classifier.** Given a platform failure first observed after both the pending read's idle expiry and the application's absolute deadline, classification returns the absolute deadline's `BudgetSource`, not the idle cause, a transport error, or an internal error. A control observed after idle expiry but before the absolute deadline returns the idle cause. The delayed observation does not extend either limit.", + "artifact_path": "outbound/EZ-14/acceptance/11/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-14/acceptance/11", + "source_id": "EZ-14/acceptance/11", + "source_digest": "3c0ed413fd09306536c7c47d81191b51dd16ae773cca7636c1b6c5da58560b87", + "requirement_ids": [ + "EZ-14.1", + "EZ-14.2", + "EZ-14.3", + "EZ-14.4", + "EZ-14.5", + "EZ-14.6", + "EZ-14.7", + "EZ-14.8", + "EZ-14.9", + "EZ-14.10", + "EZ-14.11", + "EZ-14.12" + ], + "environment": "unit/spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F4", + "substep": "EZ-14-acceptance-11-unit-spin", + "sentinel": "ez_14_acceptance_11_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_14_acceptance_11_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "**Unit, every adapter's timeout classifier.** Given a platform failure first observed after both the pending read's idle expiry and the application's absolute deadline, classification returns the absolute deadline's `BudgetSource`, not the idle cause, a transport error, or an internal error. A control observed after idle expiry but before the absolute deadline returns the idle cause. The delayed observation does not extend either limit.", + "artifact_path": "outbound/EZ-14/acceptance/11/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/01", + "source_id": "EZ-15/acceptance/01", + "source_digest": "38ea92f3003865aff39139ec7a1a9b34f2c5cfb50f4feb3ab1e22b8323a8ddff", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-01-unit-core", + "sentinel": "ez_15_acceptance_01_unit_core", + "command": "scripts/run_test_nonzero.sh ez_15_acceptance_01_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Unit. A fire-and-forget POST with a streamed body fails immediately and sends nothing. A fire-and-forget GET built from a bodiless inbound GET is sent. With a per-request bound of 2 and two requests kept pending, the third call fails with the service-unavailable error and only two requests go out.", + "artifact_path": "outbound/EZ-15/acceptance/01/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/02", + "source_id": "EZ-15/acceptance/02", + "source_digest": "02f869a7931c488529bf973c36da1f9ac3da007340415e73b681e2d1f7aa8327", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-02-axum", + "sentinel": "ez_15_acceptance_02_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-15/acceptance/02", + "expected": "Axum. Given a handler that makes a fire-and-forget call to an origin that answers after 200 ms and then returns 200, the client gets its response in under 50 ms, and the origin receives the request within 1 s. Given a server shutdown with one such send in flight, shutdown returns within 1 s.", + "artifact_path": "outbound/EZ-15/acceptance/02/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/03", + "source_id": "EZ-15/acceptance/03", + "source_digest": "5cd7329d5f36ee8aafd7ee88531302e137cb99679ab63fa9edf3e3e1d16b8abc", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "workerd", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-03-workerd", + "sentinel": "ez_15_acceptance_03_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-15/acceptance/03", + "expected": "workerd, Spin 4.1, and Viceroy 0.21.0. Given a handler that makes a fire-and-forget call to an origin that holds the request for 500 ms before answering, and then returns, the client's response completes before the origin answers, and the origin still receives the whole request. The Spin result sets the Spin capability level.", + "artifact_path": "outbound/EZ-15/acceptance/03/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/03", + "source_id": "EZ-15/acceptance/03", + "source_digest": "5cd7329d5f36ee8aafd7ee88531302e137cb99679ab63fa9edf3e3e1d16b8abc", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-03-spin", + "sentinel": "ez_15_acceptance_03_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-15/acceptance/03", + "expected": "workerd, Spin 4.1, and Viceroy 0.21.0. Given a handler that makes a fire-and-forget call to an origin that holds the request for 500 ms before answering, and then returns, the client's response completes before the origin answers, and the origin still receives the whole request. The Spin result sets the Spin capability level.", + "artifact_path": "outbound/EZ-15/acceptance/03/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/03", + "source_id": "EZ-15/acceptance/03", + "source_digest": "5cd7329d5f36ee8aafd7ee88531302e137cb99679ab63fa9edf3e3e1d16b8abc", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-03-viceroy", + "sentinel": "ez_15_acceptance_03_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-15/acceptance/03", + "expected": "workerd, Spin 4.1, and Viceroy 0.21.0. Given a handler that makes a fire-and-forget call to an origin that holds the request for 500 ms before answering, and then returns, the client's response completes before the origin answers, and the origin still receives the whole request. The Spin result sets the Spin capability level.", + "artifact_path": "outbound/EZ-15/acceptance/03/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/04", + "source_id": "EZ-15/acceptance/04", + "source_digest": "7b363f19b0b3e0fd12841941ffab57193223cf438a780429d46602e900f8201e", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "viceroy/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-04-viceroy-reuse", + "sentinel": "ez_15_acceptance_04_viceroy_reuse", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-15/acceptance/04", + "expected": "Viceroy 0.21.0 and hosted Fastly, reuse-mode follow-up. Configure the instance bound to 2. In one guest, two sequential callbacks each start a send whose origin stays pending, and then return. A third callback's send fails immediately with 503 and produces no origin request. Complete the first outbound exchange; a fourth callback's send is accepted in that same guest. Repeat reclamation after an exchange fails and after its budget expires, within Viceroy's guest request limit by using a separate guest per terminal case. On hosted Fastly, repeat across 500 sequential callbacks in retained guests, recording instance identities and saturation/recovery counts; sends that were accepted reach the origin and the instance count never exceeds 2.", + "artifact_path": "outbound/EZ-15/acceptance/04/viceroy/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/04", + "source_id": "EZ-15/acceptance/04", + "source_digest": "7b363f19b0b3e0fd12841941ffab57193223cf438a780429d46602e900f8201e", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "hosted-fastly/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-04-hosted-fastly-reuse", + "sentinel": "ez_15_acceptance_04_hosted_fastly_reuse", + "command": "hosted-runbook EZ-15/acceptance/04 --environment hosted-fastly/reuse --sentinel ez_15_acceptance_04_hosted_fastly_reuse", + "expected": "Viceroy 0.21.0 and hosted Fastly, reuse-mode follow-up. Configure the instance bound to 2. In one guest, two sequential callbacks each start a send whose origin stays pending, and then return. A third callback's send fails immediately with 503 and produces no origin request. Complete the first outbound exchange; a fourth callback's send is accepted in that same guest. Repeat reclamation after an exchange fails and after its budget expires, within Viceroy's guest request limit by using a separate guest per terminal case. On hosted Fastly, repeat across 500 sequential callbacks in retained guests, recording instance identities and saturation/recovery counts; sends that were accepted reach the origin and the instance count never exceeds 2.", + "artifact_path": "outbound/EZ-15/acceptance/04/hosted-fastly/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/05", + "source_id": "EZ-15/acceptance/05", + "source_digest": "54361cb61217bdf66288c090b0a5af42a9d698982ff58c433ba28c0502cbbcbe", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-05-axum", + "sentinel": "ez_15_acceptance_05_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-15/acceptance/05", + "expected": "Axum, workerd, Spin 4.1, and hosted Fastly. A fire-and-forget call to a refused port logs one `warn` line with the host and an unreachable-category error. On Viceroy 0.21.0 the line carries the category Viceroy produces, and the run records it. No line contains a path, query, or header value.", + "artifact_path": "outbound/EZ-15/acceptance/05/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/05", + "source_id": "EZ-15/acceptance/05", + "source_digest": "54361cb61217bdf66288c090b0a5af42a9d698982ff58c433ba28c0502cbbcbe", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-05-viceroy", + "sentinel": "ez_15_acceptance_05_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-15/acceptance/05", + "expected": "Axum, workerd, Spin 4.1, and hosted Fastly. A fire-and-forget call to a refused port logs one `warn` line with the host and an unreachable-category error. On Viceroy 0.21.0 the line carries the category Viceroy produces, and the run records it. No line contains a path, query, or header value.", + "artifact_path": "outbound/EZ-15/acceptance/05/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/05", + "source_id": "EZ-15/acceptance/05", + "source_digest": "54361cb61217bdf66288c090b0a5af42a9d698982ff58c433ba28c0502cbbcbe", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "workerd", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-05-workerd", + "sentinel": "ez_15_acceptance_05_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-15/acceptance/05", + "expected": "Axum, workerd, Spin 4.1, and hosted Fastly. A fire-and-forget call to a refused port logs one `warn` line with the host and an unreachable-category error. On Viceroy 0.21.0 the line carries the category Viceroy produces, and the run records it. No line contains a path, query, or header value.", + "artifact_path": "outbound/EZ-15/acceptance/05/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/05", + "source_id": "EZ-15/acceptance/05", + "source_digest": "54361cb61217bdf66288c090b0a5af42a9d698982ff58c433ba28c0502cbbcbe", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-05-spin", + "sentinel": "ez_15_acceptance_05_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-15/acceptance/05", + "expected": "Axum, workerd, Spin 4.1, and hosted Fastly. A fire-and-forget call to a refused port logs one `warn` line with the host and an unreachable-category error. On Viceroy 0.21.0 the line carries the category Viceroy produces, and the run records it. No line contains a path, query, or header value.", + "artifact_path": "outbound/EZ-15/acceptance/05/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/05", + "source_id": "EZ-15/acceptance/05", + "source_digest": "54361cb61217bdf66288c090b0a5af42a9d698982ff58c433ba28c0502cbbcbe", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-05-hosted-fastly", + "sentinel": "ez_15_acceptance_05_hosted_fastly", + "command": "hosted-runbook EZ-15/acceptance/05 --environment hosted-fastly --sentinel ez_15_acceptance_05_hosted_fastly", + "expected": "Axum, workerd, Spin 4.1, and hosted Fastly. A fire-and-forget call to a refused port logs one `warn` line with the host and an unreachable-category error. On Viceroy 0.21.0 the line carries the category Viceroy produces, and the run records it. No line contains a path, query, or header value.", + "artifact_path": "outbound/EZ-15/acceptance/05/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/06", + "source_id": "EZ-15/acceptance/06", + "source_digest": "9f9f4c90d52e3b955bfb1b12164a24193aed3f72738b25a666da336c96681d21", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-06-axum", + "sentinel": "ez_15_acceptance_06_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-15/acceptance/06", + "expected": "Axum. With an instance bound of 2, two sends owned by different completed request callbacks remain pending. A third call is refused immediately without dispatch. Completing or failing one exchange, or expiring its budget, releases its instance capacity once and admits a subsequent call without restarting the server.", + "artifact_path": "outbound/EZ-15/acceptance/06/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/07", + "source_id": "EZ-15/acceptance/07", + "source_digest": "b5cd1939a97f9e8a5218a16e0041f91400e64ccb7ecbac90442e89c7e34045e1", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "unit/axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-07-unit-axum", + "sentinel": "ez_15_acceptance_07_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_15_acceptance_07_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Unit, every adapter's accounting. Success, transport failure, budget expiry, and cancellation or shutdown each release accepted-send capacity exactly once. A refused send never acquires capacity or dispatches. A double release must not admit more simultaneous sends than the configured instance bound.", + "artifact_path": "outbound/EZ-15/acceptance/07/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/07", + "source_id": "EZ-15/acceptance/07", + "source_digest": "b5cd1939a97f9e8a5218a16e0041f91400e64ccb7ecbac90442e89c7e34045e1", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "unit/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-07-unit-fastly", + "sentinel": "ez_15_acceptance_07_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_15_acceptance_07_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit, every adapter's accounting. Success, transport failure, budget expiry, and cancellation or shutdown each release accepted-send capacity exactly once. A refused send never acquires capacity or dispatches. A double release must not admit more simultaneous sends than the configured instance bound.", + "artifact_path": "outbound/EZ-15/acceptance/07/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/07", + "source_id": "EZ-15/acceptance/07", + "source_digest": "b5cd1939a97f9e8a5218a16e0041f91400e64ccb7ecbac90442e89c7e34045e1", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "unit/cloudflare", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-07-unit-cloudflare", + "sentinel": "ez_15_acceptance_07_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_15_acceptance_07_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Unit, every adapter's accounting. Success, transport failure, budget expiry, and cancellation or shutdown each release accepted-send capacity exactly once. A refused send never acquires capacity or dispatches. A double release must not admit more simultaneous sends than the configured instance bound.", + "artifact_path": "outbound/EZ-15/acceptance/07/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/07", + "source_id": "EZ-15/acceptance/07", + "source_digest": "b5cd1939a97f9e8a5218a16e0041f91400e64ccb7ecbac90442e89c7e34045e1", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "unit/spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F5", + "substep": "EZ-15-acceptance-07-unit-spin", + "sentinel": "ez_15_acceptance_07_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_15_acceptance_07_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Unit, every adapter's accounting. Success, transport failure, budget expiry, and cancellation or shutdown each release accepted-send capacity exactly once. A refused send never acquires capacity or dispatches. A double release must not admit more simultaneous sends than the configured instance bound.", + "artifact_path": "outbound/EZ-15/acceptance/07/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/ts-e2e", + "source_id": "EZ-15/acceptance/ts-e2e", + "source_digest": "64a9e329ed6766fb81b8f3f976130e4d434482493b1622de94b4d9ad5b0f327f", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "consumer-axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F5", + "substep": "EZ-15-acceptance-ts-e2e-consumer-axum", + "sentinel": "ez_15_acceptance_ts_e2e_consumer_axum", + "command": "consumer-runbook EZ-15/acceptance/ts-e2e --environment consumer-axum --sentinel ez_15_acceptance_ts_e2e_consumer_axum", + "expected": "The Trusted Server end-to-end check runs on the Axum binary, workerd, Spin 4.1, and Fastly staging. It points Tinybird at a mock that answers after 1 s. An auctioned page on each adapter finishes its tail within 50 ms of the auction finishing, and the mock receives one NDJSON POST per auction.", + "artifact_path": "outbound/EZ-15/acceptance/ts-e2e/consumer-axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/ts-e2e", + "source_id": "EZ-15/acceptance/ts-e2e", + "source_digest": "64a9e329ed6766fb81b8f3f976130e4d434482493b1622de94b4d9ad5b0f327f", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "consumer-workerd", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F5", + "substep": "EZ-15-acceptance-ts-e2e-consumer-workerd", + "sentinel": "ez_15_acceptance_ts_e2e_consumer_workerd", + "command": "consumer-runbook EZ-15/acceptance/ts-e2e --environment consumer-workerd --sentinel ez_15_acceptance_ts_e2e_consumer_workerd", + "expected": "The Trusted Server end-to-end check runs on the Axum binary, workerd, Spin 4.1, and Fastly staging. It points Tinybird at a mock that answers after 1 s. An auctioned page on each adapter finishes its tail within 50 ms of the auction finishing, and the mock receives one NDJSON POST per auction.", + "artifact_path": "outbound/EZ-15/acceptance/ts-e2e/consumer-workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/ts-e2e", + "source_id": "EZ-15/acceptance/ts-e2e", + "source_digest": "64a9e329ed6766fb81b8f3f976130e4d434482493b1622de94b4d9ad5b0f327f", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "consumer-spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F5", + "substep": "EZ-15-acceptance-ts-e2e-consumer-spin", + "sentinel": "ez_15_acceptance_ts_e2e_consumer_spin", + "command": "consumer-runbook EZ-15/acceptance/ts-e2e --environment consumer-spin --sentinel ez_15_acceptance_ts_e2e_consumer_spin", + "expected": "The Trusted Server end-to-end check runs on the Axum binary, workerd, Spin 4.1, and Fastly staging. It points Tinybird at a mock that answers after 1 s. An auctioned page on each adapter finishes its tail within 50 ms of the auction finishing, and the mock receives one NDJSON POST per auction.", + "artifact_path": "outbound/EZ-15/acceptance/ts-e2e/consumer-spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-15/acceptance/ts-e2e", + "source_id": "EZ-15/acceptance/ts-e2e", + "source_digest": "64a9e329ed6766fb81b8f3f976130e4d434482493b1622de94b4d9ad5b0f327f", + "requirement_ids": [ + "EZ-15.1", + "EZ-15.2", + "EZ-15.3", + "EZ-15.4", + "EZ-15.5", + "EZ-15.6", + "EZ-15.7", + "EZ-15.8", + "EZ-15.9", + "EZ-15.10", + "EZ-15.11" + ], + "environment": "consumer-hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F5", + "substep": "EZ-15-acceptance-ts-e2e-consumer-hosted-fastly", + "sentinel": "ez_15_acceptance_ts_e2e_consumer_hosted_fastly", + "command": "consumer-runbook EZ-15/acceptance/ts-e2e --environment consumer-hosted-fastly --sentinel ez_15_acceptance_ts_e2e_consumer_hosted_fastly", + "expected": "The Trusted Server end-to-end check runs on the Axum binary, workerd, Spin 4.1, and Fastly staging. It points Tinybird at a mock that answers after 1 s. An auctioned page on each adapter finishes its tail within 50 ms of the auction finishing, and the mock receives one NDJSON POST per auction.", + "artifact_path": "outbound/EZ-15/acceptance/ts-e2e/consumer-hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-16/acceptance/01", + "source_id": "EZ-16/acceptance/01", + "source_digest": "fcc219da49b6f72b61691e310cbab018285ccd89a2d255f7745ee7b9960c7ea9", + "requirement_ids": [ + "EZ-16.1", + "EZ-16.2", + "EZ-16.3", + "EZ-16.4", + "EZ-16.5", + "EZ-16.6", + "EZ-16.7", + "EZ-16.8", + "EZ-16.9" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6a", + "substep": "EZ-16-acceptance-01-unit-core", + "sentinel": "ez_16_acceptance_01_unit_core", + "command": "scripts/run_test_nonzero.sh ez_16_acceptance_01_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit.** Given a gateway, timeout, and internal error, each with detail attached, then rendering each as a response produces the same bytes as without detail. Its `Display` is byte-identical. Its status, kind, and reason are unchanged.", + "artifact_path": "outbound/EZ-16/acceptance/01/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-16/acceptance/02", + "source_id": "EZ-16/acceptance/02", + "source_digest": "352f5c6312849779bfcd8dead36f6804979f39aca61424f9c0d6ca6de81c546f", + "requirement_ids": [ + "EZ-16.1", + "EZ-16.2", + "EZ-16.3", + "EZ-16.4", + "EZ-16.5", + "EZ-16.6", + "EZ-16.7", + "EZ-16.8", + "EZ-16.9" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6a", + "substep": "EZ-16-acceptance-02-unit-core", + "sentinel": "ez_16_acceptance_02_unit_core", + "command": "scripts/run_test_nonzero.sh ez_16_acceptance_02_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "**Unit.** Given a body stream that fails with detail, when the error passes through decoding, limits, chunk shaping, and buffered collection, then the final error still carries the detail.", + "artifact_path": "outbound/EZ-16/acceptance/02/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-16/acceptance/03", + "source_id": "EZ-16/acceptance/03", + "source_digest": "4a55c6c4a8b527500cbea29808eea2f608effae5322104c3fcd87e76966f0f7b", + "requirement_ids": [ + "EZ-16.1", + "EZ-16.2", + "EZ-16.3", + "EZ-16.4", + "EZ-16.5", + "EZ-16.6", + "EZ-16.7", + "EZ-16.8", + "EZ-16.9" + ], + "environment": "hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6a", + "substep": "EZ-16-acceptance-03-hosted-fastly", + "sentinel": "ez_16_acceptance_03_hosted_fastly", + "command": "hosted-runbook EZ-16/acceptance/03 --environment hosted-fastly --sentinel ez_16_acceptance_03_hosted_fastly", + "expected": "**Hosted Fastly.** Given a request to a closed port on a reachable host, then the error is a gateway error with reason `Unreachable`, and its detail names Fastly, the send phase, and the SDK's connection-refused cause.", + "artifact_path": "outbound/EZ-16/acceptance/03/hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-16/acceptance/04", + "source_id": "EZ-16/acceptance/04", + "source_digest": "53e7244d4965f948681332bb1352a7eef177df5a345968d7b817145dc4210349", + "requirement_ids": [ + "EZ-16.1", + "EZ-16.2", + "EZ-16.3", + "EZ-16.4", + "EZ-16.5", + "EZ-16.6", + "EZ-16.7", + "EZ-16.8", + "EZ-16.9" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6a", + "substep": "EZ-16-acceptance-04-viceroy", + "sentinel": "ez_16_acceptance_04_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-16/acceptance/04", + "expected": "**Viceroy 0.21.0.** Given the same request, then the error is an internal error, because Viceroy ignores send error detail. Its detail names Fastly, the send phase, and an internal cause.", + "artifact_path": "outbound/EZ-16/acceptance/04/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-16/acceptance/05", + "source_id": "EZ-16/acceptance/05", + "source_digest": "ed44d60f41728268017a3c79daa8f0f77bb73caf73d71a3d2f915e9d565eb195", + "requirement_ids": [ + "EZ-16.1", + "EZ-16.2", + "EZ-16.3", + "EZ-16.4", + "EZ-16.5", + "EZ-16.6", + "EZ-16.7", + "EZ-16.8", + "EZ-16.9" + ], + "environment": "spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6a", + "substep": "EZ-16-acceptance-05-spin", + "sentinel": "ez_16_acceptance_05_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-16/acceptance/05", + "expected": "**Spin 4.1.** Given a TLS alert with an id and a message, then the detail carries the id and not the message. Given a DNS failure whose rcode text is outside the documented set, then the detail records the rcode as unknown. Given an internal error that carries text, then no text appears in the detail.", + "artifact_path": "outbound/EZ-16/acceptance/05/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-16/acceptance/06", + "source_id": "EZ-16/acceptance/06", + "source_digest": "36e18f73a527fe4c174392b978d17c6754233cb5554c8605bf3f61090b2ffc7f", + "requirement_ids": [ + "EZ-16.1", + "EZ-16.2", + "EZ-16.3", + "EZ-16.4", + "EZ-16.5", + "EZ-16.6", + "EZ-16.7", + "EZ-16.8", + "EZ-16.9" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6a", + "substep": "EZ-16-acceptance-06-axum", + "sentinel": "ez_16_acceptance_06_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-16/acceptance/06", + "expected": "**Axum.** Given a refused connection to a URL that contains `?token=secret`, then the detail names a connect failure and the refused I/O kind, and `secret` appears in neither the detail nor the rendered error.", + "artifact_path": "outbound/EZ-16/acceptance/06/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-16/acceptance/07", + "source_id": "EZ-16/acceptance/07", + "source_digest": "48d984faf1cc701b4f6932ba54d1b0d20dc163f0f4234ddbc8e828daed501724", + "requirement_ids": [ + "EZ-16.1", + "EZ-16.2", + "EZ-16.3", + "EZ-16.4", + "EZ-16.5", + "EZ-16.6", + "EZ-16.7", + "EZ-16.8", + "EZ-16.9" + ], + "environment": "workerd", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6a", + "substep": "EZ-16-acceptance-07-workerd", + "sentinel": "ez_16_acceptance_07_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-16/acceptance/07", + "expected": "**workerd.** Given a rejected fetch, then the detail names Cloudflare and the send phase. Its cause is the exception kind only when that kind is in the documented set, and otherwise it is unknown.", + "artifact_path": "outbound/EZ-16/acceptance/07/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-16/acceptance/08", + "source_id": "EZ-16/acceptance/08", + "source_digest": "94182848070c739e28fd9908411f669b173b7c4686413d5f952c952a60306f55", + "requirement_ids": [ + "EZ-16.1", + "EZ-16.2", + "EZ-16.3", + "EZ-16.4", + "EZ-16.5", + "EZ-16.6", + "EZ-16.7", + "EZ-16.8", + "EZ-16.9" + ], + "environment": "consumer-hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F6a", + "substep": "EZ-16-acceptance-08-consumer-hosted-fastly", + "sentinel": "ez_16_acceptance_08_consumer_hosted_fastly", + "command": "consumer-runbook EZ-16/acceptance/08 --environment consumer-hosted-fastly --sentinel ez_16_acceptance_08_consumer_hosted_fastly", + "expected": "**Trusted Server, hosted Fastly staging.** Given an auction with one provider pointed at a closed port on a reachable host, then TS logs one line with the provider name and the refused cause, and the client-facing auction error body stays static.", + "artifact_path": "outbound/EZ-16/acceptance/08/consumer-hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-17/acceptance/01", + "source_id": "EZ-17/acceptance/01", + "source_digest": "d3022458ed3199d298a86b6023655aefcdb2c55630e2335d6d4b0685d2cca756", + "requirement_ids": [ + "EZ-17.1", + "EZ-17.2", + "EZ-17.3", + "EZ-17.4", + "EZ-17.5", + "EZ-17.6", + "EZ-17.7", + "EZ-17.8", + "EZ-17.9", + "EZ-17.10" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6b", + "substep": "EZ-17-acceptance-01-viceroy", + "sentinel": "ez_17_acceptance_01_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-17/acceptance/01", + "expected": "Viceroy 0.21.0. Given a routed handler whose body stream fails after its first chunk, and a `finalize` that returns a state value carrying the request's outbound client, when the request is delivered, then the client sees a partial body, and the caller learns routed, application body, committed, source error, and receives the state. An outbound request the callback then makes with that client completes before the callback returns (cross-cutting requirement 4).", + "artifact_path": "outbound/EZ-17/acceptance/01/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-17/acceptance/02", + "source_id": "EZ-17/acceptance/02", + "source_digest": "6dec7bb9d6d6eae42b142af202439d254967f0758ec62c5da0487ff195dae9e9", + "requirement_ids": [ + "EZ-17.1", + "EZ-17.2", + "EZ-17.3", + "EZ-17.4", + "EZ-17.5", + "EZ-17.6", + "EZ-17.7", + "EZ-17.8", + "EZ-17.9", + "EZ-17.10" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6b", + "substep": "EZ-17-acceptance-02-viceroy", + "sentinel": "ez_17_acceptance_02_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-17/acceptance/02", + "expected": "Viceroy 0.21.0. Given a routed response that `finalize` gives a `Trailer` field, when it is delivered, then the client receives the 500 fallback, and the caller learns routed, fallback body, committed, fallback completed, and receives the `finalize` value.", + "artifact_path": "outbound/EZ-17/acceptance/02/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-17/acceptance/03", + "source_id": "EZ-17/acceptance/03", + "source_digest": "d7e64a685431f272ea81ffccc61253007e157a9fa281389f844219e05f031be9", + "requirement_ids": [ + "EZ-17.1", + "EZ-17.2", + "EZ-17.3", + "EZ-17.4", + "EZ-17.5", + "EZ-17.6", + "EZ-17.7", + "EZ-17.8", + "EZ-17.9", + "EZ-17.10" + ], + "environment": "unit/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6b", + "substep": "EZ-17-acceptance-03-unit-fastly", + "sentinel": "ez_17_acceptance_03_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_17_acceptance_03_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit and Viceroy 0.21.0. Given an admission policy that refuses, the caller learns detached, application body, committed, `Completed`, and `finalize` never runs. Given admission `Abort` or detached-egress `Abort`, the caller learns aborted, never no-attempt; no response, fallback, or replacement is sent, no response observer or completion is invoked, and `finalize` never runs. Contrast the genuine no-attempt store failure of criterion 5, which permits the caller's native response.", + "artifact_path": "outbound/EZ-17/acceptance/03/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-17/acceptance/03", + "source_id": "EZ-17/acceptance/03", + "source_digest": "d7e64a685431f272ea81ffccc61253007e157a9fa281389f844219e05f031be9", + "requirement_ids": [ + "EZ-17.1", + "EZ-17.2", + "EZ-17.3", + "EZ-17.4", + "EZ-17.5", + "EZ-17.6", + "EZ-17.7", + "EZ-17.8", + "EZ-17.9", + "EZ-17.10" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6b", + "substep": "EZ-17-acceptance-03-viceroy", + "sentinel": "ez_17_acceptance_03_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-17/acceptance/03", + "expected": "Unit and Viceroy 0.21.0. Given an admission policy that refuses, the caller learns detached, application body, committed, `Completed`, and `finalize` never runs. Given admission `Abort` or detached-egress `Abort`, the caller learns aborted, never no-attempt; no response, fallback, or replacement is sent, no response observer or completion is invoked, and `finalize` never runs. Contrast the genuine no-attempt store failure of criterion 5, which permits the caller's native response.", + "artifact_path": "outbound/EZ-17/acceptance/03/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-17/acceptance/04", + "source_id": "EZ-17/acceptance/04", + "source_digest": "9a629793180cdaca5018441db729b751eaaf844714c637fb5914ea0d570f7f78", + "requirement_ids": [ + "EZ-17.1", + "EZ-17.2", + "EZ-17.3", + "EZ-17.4", + "EZ-17.5", + "EZ-17.6", + "EZ-17.7", + "EZ-17.8", + "EZ-17.9", + "EZ-17.10" + ], + "environment": "unit/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6b", + "substep": "EZ-17-acceptance-04-unit-fastly", + "sentinel": "ez_17_acceptance_04_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_17_acceptance_04_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit. For each failure point (deadline before the head is prepared, head preparation rejected, deadline before commit, host rejects the head, failure after the host accepted the head, fallback cannot start, fallback rejected by the host), the reported commit state, body and outcome match EZ-17.3, and the completion saw exactly one report equal to the reported one.", + "artifact_path": "outbound/EZ-17/acceptance/04/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-17/acceptance/05", + "source_id": "EZ-17/acceptance/05", + "source_digest": "90c4b4106053e6062566995ac7c91946d0261357bd1b0e7ebc19ad3df6271eba", + "requirement_ids": [ + "EZ-17.1", + "EZ-17.2", + "EZ-17.3", + "EZ-17.4", + "EZ-17.5", + "EZ-17.6", + "EZ-17.7", + "EZ-17.8", + "EZ-17.9", + "EZ-17.10" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6b", + "substep": "EZ-17-acceptance-05-viceroy", + "sentinel": "ez_17_acceptance_05_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-17/acceptance/05", + "expected": "Viceroy 0.21.0. Given `FastlyService::new(&app).with_kv(name).require_kv()` on a store Viceroy does not link, the caller learns no response attempted, store category, sends a native 500, and the client receives it.", + "artifact_path": "outbound/EZ-17/acceptance/05/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-17/acceptance/06", + "source_id": "EZ-17/acceptance/06", + "source_digest": "6889db31bbc877484b45c6b5d2e42dc91ec3161cb885480f291035aec01a9b84", + "requirement_ids": [ + "EZ-17.1", + "EZ-17.2", + "EZ-17.3", + "EZ-17.4", + "EZ-17.5", + "EZ-17.6", + "EZ-17.7", + "EZ-17.8", + "EZ-17.9", + "EZ-17.10" + ], + "environment": "unit/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6b", + "substep": "EZ-17-acceptance-06-unit-fastly", + "sentinel": "ez_17_acceptance_06_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_17_acceptance_06_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit and Viceroy 0.21.0. The standard entry points produce F's serving results for the scenarios above, apart from EZ-2 and EZ-22.", + "artifact_path": "outbound/EZ-17/acceptance/06/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-17/acceptance/06", + "source_id": "EZ-17/acceptance/06", + "source_digest": "6889db31bbc877484b45c6b5d2e42dc91ec3161cb885480f291035aec01a9b84", + "requirement_ids": [ + "EZ-17.1", + "EZ-17.2", + "EZ-17.3", + "EZ-17.4", + "EZ-17.5", + "EZ-17.6", + "EZ-17.7", + "EZ-17.8", + "EZ-17.9", + "EZ-17.10" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6b", + "substep": "EZ-17-acceptance-06-viceroy", + "sentinel": "ez_17_acceptance_06_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-17/acceptance/06", + "expected": "Unit and Viceroy 0.21.0. The standard entry points produce F's serving results for the scenarios above, apart from EZ-2 and EZ-22.", + "artifact_path": "outbound/EZ-17/acceptance/06/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-17/acceptance/07", + "source_id": "EZ-17/acceptance/07", + "source_digest": "dac456270cd725f677c4dae5ab49c1158c867127e60bf6f65014217daead4e27", + "requirement_ids": [ + "EZ-17.1", + "EZ-17.2", + "EZ-17.3", + "EZ-17.4", + "EZ-17.5", + "EZ-17.6", + "EZ-17.7", + "EZ-17.8", + "EZ-17.9", + "EZ-17.10" + ], + "environment": "consumer-viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F6b", + "substep": "EZ-17-acceptance-07-consumer-viceroy", + "sentinel": "ez_17_acceptance_07_consumer_viceroy", + "command": "consumer-runbook EZ-17/acceptance/07 --environment consumer-viceroy --sentinel ez_17_acceptance_07_consumer_viceroy", + "expected": "Trusted Server, Viceroy 0.21.0. Pull sync reaches its partner stub after a mid-body origin abort. No pull-sync call is made when a framing failure delivers the fallback. A request-conversion failure gets TS's own 500, if Viceroy can deliver such a request.", + "artifact_path": "outbound/EZ-17/acceptance/07/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-18/acceptance/01", + "source_id": "EZ-18/acceptance/01", + "source_digest": "8561a00a124a7782fd152dd214e86efb81a29f46bdd4539bcfadf532ef8dbe76", + "requirement_ids": [ + "EZ-18.1", + "EZ-18.2", + "EZ-18.3", + "EZ-18.4", + "EZ-18.5", + "EZ-18.6", + "EZ-18.7", + "EZ-18.8" + ], + "environment": "viceroy/http1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7a", + "substep": "EZ-18-acceptance-01-viceroy-http1", + "sentinel": "ez_18_acceptance_01_viceroy_http1", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-18/acceptance/01", + "expected": "Viceroy 0.21.0, HTTP/1.1. Given a buffered 200 route, a streamed route without a length, a rendered 404, and a routed response with a `Trailer` field, then the buffered route, the 404 and the 500 fallback (21 bytes, `internal server error`) carry `content-length` and no `transfer-encoding`, and the streamed route carries `transfer-encoding: chunked`.", + "artifact_path": "outbound/EZ-18/acceptance/01/viceroy/http1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-18/acceptance/02", + "source_id": "EZ-18/acceptance/02", + "source_digest": "4a29d968dab066cb231abb41008b9da11c36bd1bab223cfa4cab41d353f11822", + "requirement_ids": [ + "EZ-18.1", + "EZ-18.2", + "EZ-18.3", + "EZ-18.4", + "EZ-18.5", + "EZ-18.6", + "EZ-18.7", + "EZ-18.8" + ], + "environment": "viceroy/http2-prior-knowledge", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7a", + "substep": "EZ-18-acceptance-02-viceroy-http2-prior-knowledge", + "sentinel": "ez_18_acceptance_02_viceroy_http2_prior_knowledge", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-18/acceptance/02", + "expected": "Viceroy 0.21.0, HTTP/2 with prior knowledge, which Viceroy's hyper accepts by default (`hyper-0.14.26:src/server/conn.rs:950-955`). A buffered route carries `content-length`.", + "artifact_path": "outbound/EZ-18/acceptance/02/viceroy/http2-prior-knowledge.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-18/acceptance/03", + "source_id": "EZ-18/acceptance/03", + "source_digest": "b0f560193d9b89adddeda14327d46ad84a504177aece21342e9b7396533406d8", + "requirement_ids": [ + "EZ-18.1", + "EZ-18.2", + "EZ-18.3", + "EZ-18.4", + "EZ-18.5", + "EZ-18.6", + "EZ-18.7", + "EZ-18.8" + ], + "environment": "hosted-fastly/http1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7a", + "substep": "EZ-18-acceptance-03-hosted-fastly-http1", + "sentinel": "ez_18_acceptance_03_hosted_fastly_http1", + "command": "hosted-runbook EZ-18/acceptance/03 --environment hosted-fastly/http1 --sentinel ez_18_acceptance_03_hosted_fastly_http1", + "expected": "Hosted Fastly, HTTP/1.1 and HTTP/2, on TS's staging service. For a buffered 200, a buffered 200 with `x-compress-hint: on` requested with `Accept-Encoding: gzip`, HEAD and conditional-GET 304 of a buffered route, and a 204, the framing equals that of TS's pre-cut build. The compressed body decompresses to the expected SHA-256. Also exercise a streamed compressible response with its source `Content-Length` declared and `x-compress-hint: on`, requested with gzip: verify the host-compressed body decompresses to the source SHA-256 and has valid framing. Record the source length, transmitted byte count, coding, and any client-visible length; that length, when present, equals the transmitted count, not the source count.", + "artifact_path": "outbound/EZ-18/acceptance/03/hosted-fastly/http1.json", + "implementation": "planned", + "comparison": false, + "assertion_scope": "Buffered and bodyless parity only" + }, + { + "case_id": "EZ-18/acceptance/03", + "source_id": "EZ-18/acceptance/03", + "source_digest": "b0f560193d9b89adddeda14327d46ad84a504177aece21342e9b7396533406d8", + "requirement_ids": [ + "EZ-18.1", + "EZ-18.2", + "EZ-18.3", + "EZ-18.4", + "EZ-18.5", + "EZ-18.6", + "EZ-18.7", + "EZ-18.8" + ], + "environment": "hosted-fastly/http2", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7a", + "substep": "EZ-18-acceptance-03-hosted-fastly-http2", + "sentinel": "ez_18_acceptance_03_hosted_fastly_http2", + "command": "hosted-runbook EZ-18/acceptance/03 --environment hosted-fastly/http2 --sentinel ez_18_acceptance_03_hosted_fastly_http2", + "expected": "Hosted Fastly, HTTP/1.1 and HTTP/2, on TS's staging service. For a buffered 200, a buffered 200 with `x-compress-hint: on` requested with `Accept-Encoding: gzip`, HEAD and conditional-GET 304 of a buffered route, and a 204, the framing equals that of TS's pre-cut build. The compressed body decompresses to the expected SHA-256. Also exercise a streamed compressible response with its source `Content-Length` declared and `x-compress-hint: on`, requested with gzip: verify the host-compressed body decompresses to the source SHA-256 and has valid framing. Record the source length, transmitted byte count, coding, and any client-visible length; that length, when present, equals the transmitted count, not the source count.", + "artifact_path": "outbound/EZ-18/acceptance/03/hosted-fastly/http2.json", + "implementation": "planned", + "comparison": false, + "assertion_scope": "Buffered and bodyless parity only" + }, + { + "case_id": "EZ-18/acceptance/04", + "source_id": "EZ-18/acceptance/04", + "source_digest": "4c6b3085caec1ffe5149b02f7bc5038709ea074f32a79979dbb470d226c83624", + "requirement_ids": [ + "EZ-18.1", + "EZ-18.2", + "EZ-18.3", + "EZ-18.4", + "EZ-18.5", + "EZ-18.6", + "EZ-18.7", + "EZ-18.8" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7a", + "substep": "EZ-18-acceptance-04-viceroy", + "sentinel": "ez_18_acceptance_04_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-18/acceptance/04", + "expected": "Viceroy 0.21.0. Given a buffered response whose response-owned `ResponseEgressDeadline` is already in the past, the client receives the 504 fallback (32 bytes, `response write deadline exceeded`) with `content-length: 32`.", + "artifact_path": "outbound/EZ-18/acceptance/04/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-18/acceptance/05", + "source_id": "EZ-18/acceptance/05", + "source_digest": "52760ab4fde9120a3f868f69d45787f4fec90249a7ff65e994a31345e1c653f5", + "requirement_ids": [ + "EZ-18.1", + "EZ-18.2", + "EZ-18.3", + "EZ-18.4", + "EZ-18.5", + "EZ-18.6", + "EZ-18.7", + "EZ-18.8" + ], + "environment": "workerd", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7a", + "substep": "EZ-18-acceptance-05-workerd", + "sentinel": "ez_18_acceptance_05_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-18/acceptance/05", + "expected": "workerd. A buffered route carries `content-length`. Spin 4.1. The same. Axum. Unchanged from F. EdgeZero records a passing run of each for the pinned commit. Its CI runs only the Fastly fixture smoke at F (`edgezero:.github/workflows/test.yml:399-401`).", + "artifact_path": "outbound/EZ-18/acceptance/05/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-18/acceptance/05", + "source_id": "EZ-18/acceptance/05", + "source_digest": "52760ab4fde9120a3f868f69d45787f4fec90249a7ff65e994a31345e1c653f5", + "requirement_ids": [ + "EZ-18.1", + "EZ-18.2", + "EZ-18.3", + "EZ-18.4", + "EZ-18.5", + "EZ-18.6", + "EZ-18.7", + "EZ-18.8" + ], + "environment": "spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7a", + "substep": "EZ-18-acceptance-05-spin", + "sentinel": "ez_18_acceptance_05_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-18/acceptance/05", + "expected": "workerd. A buffered route carries `content-length`. Spin 4.1. The same. Axum. Unchanged from F. EdgeZero records a passing run of each for the pinned commit. Its CI runs only the Fastly fixture smoke at F (`edgezero:.github/workflows/test.yml:399-401`).", + "artifact_path": "outbound/EZ-18/acceptance/05/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-18/acceptance/05", + "source_id": "EZ-18/acceptance/05", + "source_digest": "52760ab4fde9120a3f868f69d45787f4fec90249a7ff65e994a31345e1c653f5", + "requirement_ids": [ + "EZ-18.1", + "EZ-18.2", + "EZ-18.3", + "EZ-18.4", + "EZ-18.5", + "EZ-18.6", + "EZ-18.7", + "EZ-18.8" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7a", + "substep": "EZ-18-acceptance-05-axum", + "sentinel": "ez_18_acceptance_05_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-18/acceptance/05", + "expected": "workerd. A buffered route carries `content-length`. Spin 4.1. The same. Axum. Unchanged from F. EdgeZero records a passing run of each for the pinned commit. Its CI runs only the Fastly fixture smoke at F (`edgezero:.github/workflows/test.yml:399-401`).", + "artifact_path": "outbound/EZ-18/acceptance/05/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-18/acceptance/06", + "source_id": "EZ-18/acceptance/06", + "source_digest": "e9ec6d77ffb42e8cffba348fc410814737d5d9981198fec67a969e12860661f8", + "requirement_ids": [ + "EZ-18.1", + "EZ-18.2", + "EZ-18.3", + "EZ-18.4", + "EZ-18.5", + "EZ-18.6", + "EZ-18.7", + "EZ-18.8" + ], + "environment": "consumer-viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F7a", + "substep": "EZ-18-acceptance-06-consumer-viceroy", + "sentinel": "ez_18_acceptance_06_consumer_viceroy", + "command": "consumer-runbook EZ-18/acceptance/06 --environment consumer-viceroy --sentinel ez_18_acceptance_06_consumer_viceroy", + "expected": "Trusted Server, Viceroy 0.21.0 and hosted Fastly. Auction JSON, discovery JSON, a tsjs bundle and an error page match the pre-cut build's `content-length` and `transfer-encoding`. Publisher HEAD and 304 match the pre-cut build.", + "artifact_path": "outbound/EZ-18/acceptance/06/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-18/acceptance/06", + "source_id": "EZ-18/acceptance/06", + "source_digest": "e9ec6d77ffb42e8cffba348fc410814737d5d9981198fec67a969e12860661f8", + "requirement_ids": [ + "EZ-18.1", + "EZ-18.2", + "EZ-18.3", + "EZ-18.4", + "EZ-18.5", + "EZ-18.6", + "EZ-18.7", + "EZ-18.8" + ], + "environment": "consumer-hosted-fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F7a", + "substep": "EZ-18-acceptance-06-consumer-hosted-fastly", + "sentinel": "ez_18_acceptance_06_consumer_hosted_fastly", + "command": "consumer-runbook EZ-18/acceptance/06 --environment consumer-hosted-fastly --sentinel ez_18_acceptance_06_consumer_hosted_fastly", + "expected": "Trusted Server, Viceroy 0.21.0 and hosted Fastly. Auction JSON, discovery JSON, a tsjs bundle and an error page match the pre-cut build's `content-length` and `transfer-encoding`. Publisher HEAD and 304 match the pre-cut build.", + "artifact_path": "outbound/EZ-18/acceptance/06/consumer-hosted-fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/01", + "source_id": "EZ-19/acceptance/01", + "source_digest": "8bfbfd5a0dc8c9172f2aed2f7213db4fb3480dbe4afaa78beeab2ac85939fceb", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-01-viceroy", + "sentinel": "ez_19_acceptance_01_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-19/acceptance/01", + "expected": "Viceroy 0.21.0. Given an app-level set, when a request arrives with a 9 KiB absolute URL, then the 414 carries the set. When a request arrives with about 72 KiB of headers in 60 lines, over EdgeZero's 64 KiB limit and under hyper's 100-line limit and 417,792-byte read buffer (`hyper-0.14.26:src/proto/h1/role.rs:27`; `hyper-0.14.26:src/proto/h1/io.rs:30`; Viceroy does not lower either), then the 431 carries the set. Do not use 101 header lines, because Viceroy's hyper answers those with its own 431 before the guest runs (`hyper-0.14.26:src/error.rs:564`; `hyper-0.14.26:src/proto/h1/role.rs:443-453`).", + "artifact_path": "outbound/EZ-19/acceptance/01/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/02", + "source_id": "EZ-19/acceptance/02", + "source_digest": "6310f89f8367f703e94082e9a060828afd7f99b5bd5a94e0a4d07e00e90809b3", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-02-viceroy", + "sentinel": "ez_19_acceptance_02_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-19/acceptance/02", + "expected": "Viceroy 0.21.0. Given a routed response that carries a `Trailer` field, the 500 fallback carries the set and its body is unchanged. A HEAD request to the same route gets the fallback's headers with the set and no body.", + "artifact_path": "outbound/EZ-19/acceptance/02/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/03", + "source_id": "EZ-19/acceptance/03", + "source_digest": "f9545df9731c71a73e6dd3d5cdb74da484138674b89de76cb0b2c7c904260b10", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-03-viceroy", + "sentinel": "ez_19_acceptance_03_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-19/acceptance/03", + "expected": "Viceroy 0.21.0. Given a per-request set supplied in `prepare`, the 414 carries that set and not the app-level one.", + "artifact_path": "outbound/EZ-19/acceptance/03/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/04", + "source_id": "EZ-19/acceptance/04", + "source_digest": "94e023751b97a62c27f1a4fbd3a42a3d3b37c6906c51c9707fc2bc3ec92994f3", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "unit/axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-04-unit-axum", + "sentinel": "ez_19_acceptance_04_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_19_acceptance_04_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Unit, each adapter. A fallback carries the app-level set. A rendered 405 keeps its own `Allow` and no second one. A set holding `Cache-Control` leaves a synthesized response's own `Cache-Control` as its only value.", + "artifact_path": "outbound/EZ-19/acceptance/04/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/04", + "source_id": "EZ-19/acceptance/04", + "source_digest": "94e023751b97a62c27f1a4fbd3a42a3d3b37c6906c51c9707fc2bc3ec92994f3", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "unit/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-04-unit-fastly", + "sentinel": "ez_19_acceptance_04_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_19_acceptance_04_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit, each adapter. A fallback carries the app-level set. A rendered 405 keeps its own `Allow` and no second one. A set holding `Cache-Control` leaves a synthesized response's own `Cache-Control` as its only value.", + "artifact_path": "outbound/EZ-19/acceptance/04/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/04", + "source_id": "EZ-19/acceptance/04", + "source_digest": "94e023751b97a62c27f1a4fbd3a42a3d3b37c6906c51c9707fc2bc3ec92994f3", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "unit/cloudflare", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-04-unit-cloudflare", + "sentinel": "ez_19_acceptance_04_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_19_acceptance_04_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Unit, each adapter. A fallback carries the app-level set. A rendered 405 keeps its own `Allow` and no second one. A set holding `Cache-Control` leaves a synthesized response's own `Cache-Control` as its only value.", + "artifact_path": "outbound/EZ-19/acceptance/04/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/04", + "source_id": "EZ-19/acceptance/04", + "source_digest": "94e023751b97a62c27f1a4fbd3a42a3d3b37c6906c51c9707fc2bc3ec92994f3", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "unit/spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-04-unit-spin", + "sentinel": "ez_19_acceptance_04_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_19_acceptance_04_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Unit, each adapter. A fallback carries the app-level set. A rendered 405 keeps its own `Allow` and no second one. A set holding `Cache-Control` leaves a synthesized response's own `Cache-Control` as its only value.", + "artifact_path": "outbound/EZ-19/acceptance/04/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/05", + "source_id": "EZ-19/acceptance/05", + "source_digest": "4b72f394412db8b02ab4ff42c0717a38f2a8340d1614235c84049b3dcb4017f8", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-05-unit-core", + "sentinel": "ez_19_acceptance_05_unit_core", + "command": "scripts/run_test_nonzero.sh ez_19_acceptance_05_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Unit. An app-level set containing any name in EZ-19.5, or exceeding the bounds, fails at configuration time. A dynamic per-request set with the same violations fails construction during `prepare`, before installation, without changing the configured set.", + "artifact_path": "outbound/EZ-19/acceptance/05/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/06", + "source_id": "EZ-19/acceptance/06", + "source_digest": "00c8cb265bfd03d83fe1cc63796d4ddfb68b3fd53d1960656fa1477b35cc971e", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "consumer-viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F7b", + "substep": "EZ-19-acceptance-06-consumer-viceroy", + "sentinel": "ez_19_acceptance_06_consumer_viceroy", + "command": "consumer-runbook EZ-19/acceptance/06 --environment consumer-viceroy --sentinel ez_19_acceptance_06_consumer_viceroy", + "expected": "Trusted Server, Viceroy 0.21.0. The 414 and 431 carry `X-TS-Version`, `X-TS-Fastly-Version`, geo and operator headers. The framing-failure 500 carries the same headers.", + "artifact_path": "outbound/EZ-19/acceptance/06/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/07", + "source_id": "EZ-19/acceptance/07", + "source_digest": "628b56858b83690585a8d616b9e3f82ed43cc350f0fdc54a40bb77aec09228b4", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-07-unit-core", + "sentinel": "ez_19_acceptance_07_unit_core", + "command": "scripts/run_test_nonzero.sh ez_19_acceptance_07_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Unit and Viceroy 0.21.0. Given a valid app-level set and a per-request set constructed in `prepare` whose dynamic values exceed the bound, that construction returns the typed error. A subsequent detached 414 and an egress fallback retain the valid app-level set. Fallback construction does not invoke validation or any application callback.", + "artifact_path": "outbound/EZ-19/acceptance/07/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/07", + "source_id": "EZ-19/acceptance/07", + "source_digest": "628b56858b83690585a8d616b9e3f82ed43cc350f0fdc54a40bb77aec09228b4", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-07-viceroy", + "sentinel": "ez_19_acceptance_07_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-19/acceptance/07", + "expected": "Unit and Viceroy 0.21.0. Given a valid app-level set and a per-request set constructed in `prepare` whose dynamic values exceed the bound, that construction returns the typed error. A subsequent detached 414 and an egress fallback retain the valid app-level set. Fallback construction does not invoke validation or any application callback.", + "artifact_path": "outbound/EZ-19/acceptance/07/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/08", + "source_id": "EZ-19/acceptance/08", + "source_digest": "a23ba66cd0b869ece1f49d0c89a033b09477f070e0ff23ce9f96bd4709651fd5", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "unit/axum", + "applicability": "conditional", + "approval": "EZ-10 overflow and EZ-19 header composition present@unit/axum", + "condition": "EZ-10 overflow and EZ-19 header composition present", + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-08-unit-axum", + "sentinel": "ez_19_acceptance_08_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_19_acceptance_08_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Unit, each adapter, when EZ-10's typed overflow and EZ-19 header composition are present. A generated inbound/upload-cap error remains 413 with no `Retry-After` even when both its custom renderer and app-level set supply one; unrelated configured headers remain. On Fastly, repeat an admitted source error with a per-request replacement set from `prepare`. An ordinary upstream 413 with a non-nominated `Retry-After: 120` retains it. A config-503 response using the default effective retry policy retains its single `Retry-After: 60` despite competing renderer/set values. If either follow-up part is absent, record that combined leg's applicability rather than implicitly promoting it.", + "artifact_path": "outbound/EZ-19/acceptance/08/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/08", + "source_id": "EZ-19/acceptance/08", + "source_digest": "a23ba66cd0b869ece1f49d0c89a033b09477f070e0ff23ce9f96bd4709651fd5", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "unit/fastly", + "applicability": "conditional", + "approval": "EZ-10 overflow and EZ-19 header composition present@unit/fastly", + "condition": "EZ-10 overflow and EZ-19 header composition present", + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-08-unit-fastly", + "sentinel": "ez_19_acceptance_08_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_19_acceptance_08_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit, each adapter, when EZ-10's typed overflow and EZ-19 header composition are present. A generated inbound/upload-cap error remains 413 with no `Retry-After` even when both its custom renderer and app-level set supply one; unrelated configured headers remain. On Fastly, repeat an admitted source error with a per-request replacement set from `prepare`. An ordinary upstream 413 with a non-nominated `Retry-After: 120` retains it. A config-503 response using the default effective retry policy retains its single `Retry-After: 60` despite competing renderer/set values. If either follow-up part is absent, record that combined leg's applicability rather than implicitly promoting it.", + "artifact_path": "outbound/EZ-19/acceptance/08/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/08", + "source_id": "EZ-19/acceptance/08", + "source_digest": "a23ba66cd0b869ece1f49d0c89a033b09477f070e0ff23ce9f96bd4709651fd5", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "unit/cloudflare", + "applicability": "conditional", + "approval": "EZ-10 overflow and EZ-19 header composition present@unit/cloudflare", + "condition": "EZ-10 overflow and EZ-19 header composition present", + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-08-unit-cloudflare", + "sentinel": "ez_19_acceptance_08_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_19_acceptance_08_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Unit, each adapter, when EZ-10's typed overflow and EZ-19 header composition are present. A generated inbound/upload-cap error remains 413 with no `Retry-After` even when both its custom renderer and app-level set supply one; unrelated configured headers remain. On Fastly, repeat an admitted source error with a per-request replacement set from `prepare`. An ordinary upstream 413 with a non-nominated `Retry-After: 120` retains it. A config-503 response using the default effective retry policy retains its single `Retry-After: 60` despite competing renderer/set values. If either follow-up part is absent, record that combined leg's applicability rather than implicitly promoting it.", + "artifact_path": "outbound/EZ-19/acceptance/08/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-19/acceptance/08", + "source_id": "EZ-19/acceptance/08", + "source_digest": "a23ba66cd0b869ece1f49d0c89a033b09477f070e0ff23ce9f96bd4709651fd5", + "requirement_ids": [ + "EZ-19.1", + "EZ-19.2", + "EZ-19.3", + "EZ-19.4", + "EZ-19.5", + "EZ-19.6", + "EZ-19.7", + "EZ-19.8" + ], + "environment": "unit/spin", + "applicability": "conditional", + "approval": "EZ-10 overflow and EZ-19 header composition present@unit/spin", + "condition": "EZ-10 overflow and EZ-19 header composition present", + "owner": "EdgeZero", + "task": "F7b", + "substep": "EZ-19-acceptance-08-unit-spin", + "sentinel": "ez_19_acceptance_08_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_19_acceptance_08_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Unit, each adapter, when EZ-10's typed overflow and EZ-19 header composition are present. A generated inbound/upload-cap error remains 413 with no `Retry-After` even when both its custom renderer and app-level set supply one; unrelated configured headers remain. On Fastly, repeat an admitted source error with a per-request replacement set from `prepare`. An ordinary upstream 413 with a non-nominated `Retry-After: 120` retains it. A config-503 response using the default effective retry policy retains its single `Retry-After: 60` despite competing renderer/set values. If either follow-up part is absent, record that combined leg's applicability rather than implicitly promoting it.", + "artifact_path": "outbound/EZ-19/acceptance/08/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-20/acceptance/01", + "source_id": "EZ-20/acceptance/01", + "source_digest": "898f8eae1630bcbea7870b10e5c9ab974eea9080c811419f46dd69163e2bb9c1", + "requirement_ids": [ + "EZ-20.1", + "EZ-20.2", + "EZ-20.3", + "EZ-20.4", + "EZ-20.5", + "EZ-20.6", + "EZ-20.7", + "EZ-20.8" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F8", + "substep": "EZ-20-acceptance-01-viceroy", + "sentinel": "ez_20_acceptance_01_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-20/acceptance/01", + "expected": "Viceroy 0.21.0. Given an app declaring a KV id Viceroy does not link, a route that ignores KV returns 200, and a route that reads the store returns 500, in the same request sequence.", + "artifact_path": "outbound/EZ-20/acceptance/01/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-20/acceptance/02", + "source_id": "EZ-20/acceptance/02", + "source_digest": "7c751f9262fdb7270459663414bd6abdb81661c18107f0a678f9d8967b1c7715", + "requirement_ids": [ + "EZ-20.1", + "EZ-20.2", + "EZ-20.3", + "EZ-20.4", + "EZ-20.5", + "EZ-20.6", + "EZ-20.7", + "EZ-20.8" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F8", + "substep": "EZ-20-acceptance-02-unit-core", + "sentinel": "ez_20_acceptance_02_unit_core", + "command": "scripts/run_test_nonzero.sh ez_20_acceptance_02_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Unit. SDK `StoreNotFound`, `Ok(None)`, and invalid binding configuration each produce an internal-class failing handle; a transient provider open failure produces an unavailable-class failing handle. No message contains the store name or provider text.", + "artifact_path": "outbound/EZ-20/acceptance/02/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-20/acceptance/02", + "source_id": "EZ-20/acceptance/02", + "source_digest": "7c751f9262fdb7270459663414bd6abdb81661c18107f0a678f9d8967b1c7715", + "requirement_ids": [ + "EZ-20.1", + "EZ-20.2", + "EZ-20.3", + "EZ-20.4", + "EZ-20.5", + "EZ-20.6", + "EZ-20.7", + "EZ-20.8" + ], + "environment": "unit/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F8", + "substep": "EZ-20-acceptance-02-unit-fastly", + "sentinel": "ez_20_acceptance_02_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_20_acceptance_02_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit. SDK `StoreNotFound`, `Ok(None)`, and invalid binding configuration each produce an internal-class failing handle; a transient provider open failure produces an unavailable-class failing handle. No message contains the store name or provider text.", + "artifact_path": "outbound/EZ-20/acceptance/02/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-20/acceptance/03", + "source_id": "EZ-20/acceptance/03", + "source_digest": "6a34ab92d98eb8f58ac4ef33aa50cd9cfbb61215fbb7f7ebb90c5d130514f187", + "requirement_ids": [ + "EZ-20.1", + "EZ-20.2", + "EZ-20.3", + "EZ-20.4", + "EZ-20.5", + "EZ-20.6", + "EZ-20.7", + "EZ-20.8" + ], + "environment": "viceroy/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F8", + "substep": "EZ-20-acceptance-03-viceroy-reuse", + "sentinel": "ez_20_acceptance_03_viceroy_reuse", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-20/acceptance/03", + "expected": "Viceroy 0.21.0. Given the setup in criterion 1 in one reused sandbox, two requests to the KV route produce one open-failure log line in total, and requests to the KV-free route produce none.", + "artifact_path": "outbound/EZ-20/acceptance/03/viceroy/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-20/acceptance/04", + "source_id": "EZ-20/acceptance/04", + "source_digest": "293373f34e7c2d4562d3ee04ed71db3dc79d9fc5907b3d41728b23777b6e4f64", + "requirement_ids": [ + "EZ-20.1", + "EZ-20.2", + "EZ-20.3", + "EZ-20.4", + "EZ-20.5", + "EZ-20.6", + "EZ-20.7", + "EZ-20.8" + ], + "environment": "workerd", + "applicability": "conditional", + "approval": "EZ-20.7 adopted@workerd", + "condition": "EZ-20.7 adopted", + "owner": "EdgeZero", + "task": "F8", + "substep": "EZ-20-acceptance-04-workerd", + "sentinel": "ez_20_acceptance_04_workerd", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-20/acceptance/04", + "expected": "workerd and Spin 4.1, if EZ-20.7 is adopted. Same as criterion 1.", + "artifact_path": "outbound/EZ-20/acceptance/04/workerd.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-20/acceptance/04", + "source_id": "EZ-20/acceptance/04", + "source_digest": "293373f34e7c2d4562d3ee04ed71db3dc79d9fc5907b3d41728b23777b6e4f64", + "requirement_ids": [ + "EZ-20.1", + "EZ-20.2", + "EZ-20.3", + "EZ-20.4", + "EZ-20.5", + "EZ-20.6", + "EZ-20.7", + "EZ-20.8" + ], + "environment": "spin", + "applicability": "conditional", + "approval": "EZ-20.7 adopted@spin", + "condition": "EZ-20.7 adopted", + "owner": "EdgeZero", + "task": "F8", + "substep": "EZ-20-acceptance-04-spin", + "sentinel": "ez_20_acceptance_04_spin", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-20/acceptance/04", + "expected": "workerd and Spin 4.1, if EZ-20.7 is adopted. Same as criterion 1.", + "artifact_path": "outbound/EZ-20/acceptance/04/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-20/acceptance/05", + "source_id": "EZ-20/acceptance/05", + "source_digest": "d712f2c2d633bdc32583e1105d54ee4ec0a2f2bdd2e6361152a562cec519843b", + "requirement_ids": [ + "EZ-20.1", + "EZ-20.2", + "EZ-20.3", + "EZ-20.4", + "EZ-20.5", + "EZ-20.6", + "EZ-20.7", + "EZ-20.8" + ], + "environment": "consumer-viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F8", + "substep": "EZ-20-acceptance-05-consumer-viceroy", + "sentinel": "ez_20_acceptance_05_consumer_viceroy", + "command": "consumer-runbook EZ-20/acceptance/05 --environment consumer-viceroy --sentinel ez_20_acceptance_05_consumer_viceroy", + "expected": "Trusted Server, Viceroy 0.21.0. With `TrustedServerApp::stores()` passed to the helper against TS's Viceroy template, every route serves the pre-cut responses.", + "artifact_path": "outbound/EZ-20/acceptance/05/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-21/acceptance/01", + "source_id": "EZ-21/acceptance/01", + "source_digest": "411dffd40561389e3ba02ddab8340b5118da41077a274657b00d22d162140416", + "requirement_ids": [ + "EZ-21.1", + "EZ-21.2", + "EZ-21.3", + "EZ-21.4", + "EZ-21.5" + ], + "environment": "unit/axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6c", + "substep": "EZ-21-acceptance-01-unit-axum", + "sentinel": "ez_21_acceptance_01_unit_axum", + "command": "scripts/run_test_nonzero.sh ez_21_acceptance_01_unit_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Unit, each adapter. For each framing rule, a response that breaks it yields a report naming that rule, and a policy panic yields a report naming none.", + "artifact_path": "outbound/EZ-21/acceptance/01/unit/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-21/acceptance/01", + "source_id": "EZ-21/acceptance/01", + "source_digest": "411dffd40561389e3ba02ddab8340b5118da41077a274657b00d22d162140416", + "requirement_ids": [ + "EZ-21.1", + "EZ-21.2", + "EZ-21.3", + "EZ-21.4", + "EZ-21.5" + ], + "environment": "unit/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6c", + "substep": "EZ-21-acceptance-01-unit-fastly", + "sentinel": "ez_21_acceptance_01_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_21_acceptance_01_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Unit, each adapter. For each framing rule, a response that breaks it yields a report naming that rule, and a policy panic yields a report naming none.", + "artifact_path": "outbound/EZ-21/acceptance/01/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-21/acceptance/01", + "source_id": "EZ-21/acceptance/01", + "source_digest": "411dffd40561389e3ba02ddab8340b5118da41077a274657b00d22d162140416", + "requirement_ids": [ + "EZ-21.1", + "EZ-21.2", + "EZ-21.3", + "EZ-21.4", + "EZ-21.5" + ], + "environment": "unit/cloudflare", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6c", + "substep": "EZ-21-acceptance-01-unit-cloudflare", + "sentinel": "ez_21_acceptance_01_unit_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_21_acceptance_01_unit_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Unit, each adapter. For each framing rule, a response that breaks it yields a report naming that rule, and a policy panic yields a report naming none.", + "artifact_path": "outbound/EZ-21/acceptance/01/unit/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-21/acceptance/01", + "source_id": "EZ-21/acceptance/01", + "source_digest": "411dffd40561389e3ba02ddab8340b5118da41077a274657b00d22d162140416", + "requirement_ids": [ + "EZ-21.1", + "EZ-21.2", + "EZ-21.3", + "EZ-21.4", + "EZ-21.5" + ], + "environment": "unit/spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6c", + "substep": "EZ-21-acceptance-01-unit-spin", + "sentinel": "ez_21_acceptance_01_unit_spin", + "command": "scripts/run_test_nonzero.sh ez_21_acceptance_01_unit_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Unit, each adapter. For each framing rule, a response that breaks it yields a report naming that rule, and a policy panic yields a report naming none.", + "artifact_path": "outbound/EZ-21/acceptance/01/unit/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-21/acceptance/02", + "source_id": "EZ-21/acceptance/02", + "source_digest": "2534db4fb0a77ca5230249a4b69cc2512ef94e80f04d6daf164bff1ba981b57d", + "requirement_ids": [ + "EZ-21.1", + "EZ-21.2", + "EZ-21.3", + "EZ-21.4", + "EZ-21.5" + ], + "environment": "viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F6c", + "substep": "EZ-21-acceptance-02-viceroy", + "sentinel": "ez_21_acceptance_02_viceroy", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-21/acceptance/02", + "expected": "Viceroy 0.21.0. Given a routed response with a `Trailer` field, the runtime log line and the report both name \"unsupported trailers\", and the client receives the unchanged 500 fallback.", + "artifact_path": "outbound/EZ-21/acceptance/02/viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-21/acceptance/03", + "source_id": "EZ-21/acceptance/03", + "source_digest": "42e338cbf6f016a2371374ec69bc527840f4632da353be179eb04115651427fc", + "requirement_ids": [ + "EZ-21.1", + "EZ-21.2", + "EZ-21.3", + "EZ-21.4", + "EZ-21.5" + ], + "environment": "consumer-viceroy", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F6c", + "substep": "EZ-21-acceptance-03-consumer-viceroy", + "sentinel": "ez_21_acceptance_03_consumer_viceroy", + "command": "consumer-runbook EZ-21/acceptance/03 --environment consumer-viceroy --sentinel ez_21_acceptance_03_consumer_viceroy", + "expected": "Trusted Server, Viceroy 0.21.0. With TS's framing-header guard disabled, an operator `trailer` header produces the 500 fallback and TS's completion log names \"unsupported trailers\".", + "artifact_path": "outbound/EZ-21/acceptance/03/consumer-viceroy.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/01", + "source_id": "EZ-22/acceptance/01", + "source_digest": "d9d36eb82eab80edc54dd3dcf215d94a3e41eb4d9d01ca4377b145323ff37e3b", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-01-unit-core", + "sentinel": "ez_22_acceptance_01_unit_core", + "command": "scripts/run_test_nonzero.sh ez_22_acceptance_01_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is unit, host. Given an application whose route building fails with a 503 error and whose `configure` counts calls, when `App::build` runs, then it returns the 503 error and the count is 0.", + "artifact_path": "outbound/EZ-22/acceptance/01/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/02", + "source_id": "EZ-22/acceptance/02", + "source_digest": "a8b685920a351f5a5ab5b0d279a8ac951abe601e2abd530105c31dd261e12e53", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-02-unit-core", + "sentinel": "ez_22_acceptance_02_unit_core", + "command": "scripts/run_test_nonzero.sh ez_22_acceptance_02_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is unit, host. Given state registered as a shared value, when a handler extracts it through `State` during canonical dispatch and entry-point code reads it from the assembled app, then both see the same shared value.", + "artifact_path": "outbound/EZ-22/acceptance/02/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/03", + "source_id": "EZ-22/acceptance/03", + "source_digest": "1e3a383bd75a1858d71028cdfb6c049069a06916881aff2119ad95b43500d355", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-03-unit-core", + "sentinel": "ez_22_acceptance_03_unit_core", + "command": "scripts/run_test_nonzero.sh ez_22_acceptance_03_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is unit, host. Given a `configure` that sets ingress head limits from registered state, when the app is assembled, then the app's head limits are the ones in the state.", + "artifact_path": "outbound/EZ-22/acceptance/03/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/04", + "source_id": "EZ-22/acceptance/04", + "source_digest": "4fbd322c266b2fb8ddfda14bd0fa65d734d64b426bcc92845b63cccb34213b1b", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-04-unit-core", + "sentinel": "ez_22_acceptance_04_unit_core", + "command": "scripts/run_test_nonzero.sh ez_22_acceptance_04_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is unit, host. Given a router the application built itself, when it is assembled through the EZ-22.5 path, then `configure` runs once, the name is the application's, and a `configure` that changes platform metadata fails exactly as it does under `App::build`. The existing cases at `edgezero:crates/edgezero-core/src/app.rs:1022-1127` pass for both paths.", + "artifact_path": "outbound/EZ-22/acceptance/04/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/05", + "source_id": "EZ-22/acceptance/05", + "source_digest": "3dc122cf85118b502a1c200a67bf5f1b2a8eb512598158ebffd0cc9c3addea9d", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "unit/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-05-unit-fastly", + "sentinel": "ez_22_acceptance_05_unit_fastly", + "command": "scripts/run_test_nonzero.sh ez_22_acceptance_05_unit_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Environment is unit, host. Given `lifecycle::Sandbox` with an assembly that fails once and then succeeds, when two requests arrive, then after the first nothing is retained and one build was attempted, and after the second the app is retained and two builds were attempted.", + "artifact_path": "outbound/EZ-22/acceptance/05/unit/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/06", + "source_id": "EZ-22/acceptance/06", + "source_digest": "76525036ce858e66fc38518ad6412b04e7d664a18956838456c841009d7ea99b", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "viceroy/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-06-viceroy-reuse", + "sentinel": "ez_22_acceptance_06_viceroy_reuse", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-22/acceptance/06", + "expected": "Environment is Viceroy 0.21.0. Given TS's composition, `serve_custom` with `lifecycle::Sandbox`, and an application whose assembly fails while a flag is set, when request one arrives with the flag set and request two arrives in the same sandbox with the flag cleared, then request one gets 503 with EdgeZero's JSON error body and nothing is retained, and request two gets 200 with the same instance identity, the next ordinal, and two builds recorded.", + "artifact_path": "outbound/EZ-22/acceptance/06/viceroy/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/07", + "source_id": "EZ-22/acceptance/07", + "source_digest": "ae01686760141a5d6e7d9ceef0fffdcae177fdabe578caedfd1864473575523f", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "workerd/retained-instance", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-07-workerd-retained-instance", + "sentinel": "ez_22_acceptance_07_workerd_retained_instance", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-22/acceptance/07", + "expected": "Environment is workerd and Spin 4.1. Given the documented retention recipe and a build that fails once, when two requests arrive in one instance, then the first gets the failure, nothing is retained, and the second succeeds and is retained (EZ-22.6). Only if EZ-22.8 is adopted, given a failing route build, when `run_app` serves a request, then the client gets 503 with the JSON error body, not a host-generic 500. EdgeZero records a passing run of this criterion for the pinned commit.", + "artifact_path": "outbound/EZ-22/acceptance/07/workerd/retained-instance.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/07b", + "source_id": "EZ-22/acceptance/07", + "source_digest": "ae01686760141a5d6e7d9ceef0fffdcae177fdabe578caedfd1864473575523f", + "requirement_ids": ["EZ-22.8"], + "environment": "workerd/retained-instance", + "applicability": "conditional", + "approval": "EZ-22.8 adopted@workerd/retained-instance", + "condition": "EZ-22.8 adopted", + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-07b-workerd-retained-instance", + "sentinel": "ez_22_acceptance_07b_workerd_retained_instance", + "command": "scripts/smoke_test_reusable_app.sh --adapter cloudflare --suite outbound-cutover --case EZ-22/acceptance/07b", + "expected": "Environment is workerd and Spin 4.1. Given the documented retention recipe and a build that fails once, when two requests arrive in one instance, then the first gets the failure, nothing is retained, and the second succeeds and is retained (EZ-22.6). Only if EZ-22.8 is adopted, given a failing route build, when `run_app` serves a request, then the client gets 503 with the JSON error body, not a host-generic 500. EdgeZero records a passing run of this criterion for the pinned commit.", + "artifact_path": "outbound/EZ-22/acceptance/07b/workerd/retained-instance.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/07", + "source_id": "EZ-22/acceptance/07", + "source_digest": "ae01686760141a5d6e7d9ceef0fffdcae177fdabe578caedfd1864473575523f", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "spin/retained-instance", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-07-spin-retained-instance", + "sentinel": "ez_22_acceptance_07_spin_retained_instance", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-22/acceptance/07", + "expected": "Environment is workerd and Spin 4.1. Given the documented retention recipe and a build that fails once, when two requests arrive in one instance, then the first gets the failure, nothing is retained, and the second succeeds and is retained (EZ-22.6). Only if EZ-22.8 is adopted, given a failing route build, when `run_app` serves a request, then the client gets 503 with the JSON error body, not a host-generic 500. EdgeZero records a passing run of this criterion for the pinned commit.", + "artifact_path": "outbound/EZ-22/acceptance/07/spin/retained-instance.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/07b", + "source_id": "EZ-22/acceptance/07", + "source_digest": "ae01686760141a5d6e7d9ceef0fffdcae177fdabe578caedfd1864473575523f", + "requirement_ids": ["EZ-22.8"], + "environment": "spin/retained-instance", + "applicability": "conditional", + "approval": "EZ-22.8 adopted@spin/retained-instance", + "condition": "EZ-22.8 adopted", + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-07b-spin-retained-instance", + "sentinel": "ez_22_acceptance_07b_spin_retained_instance", + "command": "scripts/smoke_test_reusable_app.sh --adapter spin --suite outbound-cutover --case EZ-22/acceptance/07b", + "expected": "Environment is workerd and Spin 4.1. Given the documented retention recipe and a build that fails once, when two requests arrive in one instance, then the first gets the failure, nothing is retained, and the second succeeds and is retained (EZ-22.6). Only if EZ-22.8 is adopted, given a failing route build, when `run_app` serves a request, then the client gets 503 with the JSON error body, not a host-generic 500. EdgeZero records a passing run of this criterion for the pinned commit.", + "artifact_path": "outbound/EZ-22/acceptance/07b/spin/retained-instance.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/08", + "source_id": "EZ-22/acceptance/08", + "source_digest": "f45c99859f39904c755d3edfc9f0d23f1fb3d884d498def87563ccd0f6fa68a6", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "unit/core", + "applicability": "conditional", + "approval": "EZ-22.10 adopted@unit/core", + "condition": "EZ-22.10 adopted", + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-08-unit-core", + "sentinel": "ez_22_acceptance_08_unit_core", + "command": "scripts/run_test_nonzero.sh ez_22_acceptance_08_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is unit, host, only if EZ-22.10 is adopted. Given an `app!` application with fallible state that fails, when `App::build` runs, then it fails and nothing is assembled.", + "artifact_path": "outbound/EZ-22/acceptance/08/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/09", + "source_id": "EZ-22/acceptance/09", + "source_digest": "4397e2eb2038deea5292bb8bf03fdd14949f9c89c7675244efe32b8cf0cf0ab4", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "consumer-viceroy/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F9a", + "substep": "EZ-22-acceptance-09-consumer-viceroy-reuse", + "sentinel": "ez_22_acceptance_09_consumer_viceroy_reuse", + "command": "consumer-runbook EZ-22/acceptance/09 --environment consumer-viceroy/reuse --sentinel ez_22_acceptance_09_consumer_viceroy_reuse", + "expected": "Environment is Viceroy 0.21.0 with TS's reusable-sandbox feature. Given a config key pointing at a missing blob, when request one arrives, then EdgeZero's 503 is served through EZ-22.12. Given the blob restored, when request two arrives in the same sandbox, then it gets 200 with the same `x-ts-sandbox-instance` and `x-ts-sandbox-builds` one higher (`ts:crates/trusted-server-adapter-fastly/src/sandbox.rs:21-31`). TS no longer keeps its own app-and-state pair or its startup-error router.", + "artifact_path": "outbound/EZ-22/acceptance/09/consumer-viceroy/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/10", + "source_id": "EZ-22/acceptance/10", + "source_digest": "2b42fad82e9231c8f2b0d6f06823aea88452e69c01429079f3de1ed85c12e16f", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "consumer-workerd/same-isolate", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F9a", + "substep": "EZ-22-acceptance-10-consumer-workerd-same-isolate", + "sentinel": "ez_22_acceptance_10_consumer_workerd_same_isolate", + "command": "consumer-runbook EZ-22/acceptance/10 --environment consumer-workerd/same-isolate --sentinel ez_22_acceptance_10_consumer_workerd_same_isolate", + "expected": "Environment is workerd. Given TS's Cloudflare entry with the retention recipe, when ten sequential requests report the same isolate identity, then the state is built once. TS no longer keeps the Cloudflare thread-local.", + "artifact_path": "outbound/EZ-22/acceptance/10/consumer-workerd/same-isolate.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-22/acceptance/11", + "source_id": "EZ-22/acceptance/11", + "source_digest": "f533d3381683044ea935e43d1197df9f9f03fed8edd699c96efb48c82aff04a3", + "requirement_ids": [ + "EZ-22.1", + "EZ-22.2", + "EZ-22.3", + "EZ-22.4", + "EZ-22.5", + "EZ-22.6", + "EZ-22.7", + "EZ-22.8", + "EZ-22.9", + "EZ-22.10", + "EZ-22.11", + "EZ-22.12" + ], + "environment": "viceroy/reuse", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9a", + "substep": "EZ-22-acceptance-11-viceroy-reuse", + "sentinel": "ez_22_acceptance_11_viceroy_reuse", + "command": "scripts/smoke_test_reusable_app.sh --adapter fastly --suite outbound-cutover --case EZ-22/acceptance/11", + "expected": "Environment is Viceroy 0.21.0. Given `serve_app` and an assembly that fails while a flag is set, when request one arrives with the flag set and request two arrives in the same sandbox with the flag cleared, then request one gets 503 with the JSON error body and request two gets 200 with two builds recorded.", + "artifact_path": "outbound/EZ-22/acceptance/11/viceroy/reuse.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/01", + "source_id": "EZ-23/acceptance/01", + "source_digest": "1f28b36119155b7dfc8f1d4b24cd2415af90846f61b29757139ee6f0d49d0900", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-01-axum-native-tcp", + "sentinel": "test_utils::tests::configured_target_limit_applies_on_tcp", + "command": "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets", + "expected": "Environment is Axum, native. Given an application whose `configure` limits the request target to 64 bytes, when a 100-byte target arrives through the programmatic dev server and through the test facility, then both answer 414. Given the same router served without `configure` at F, the request succeeds, which shows the gap.", + "artifact_path": "outbound/EZ-23/acceptance/01/axum/native-tcp.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/02", + "source_id": "EZ-23/acceptance/02", + "source_digest": "50d1aac7cf1828ad9fb5273eecc730ff99964f0398bbdb2fb27f059398c8bf02", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-02-axum-native-tcp", + "sentinel": "test_utils::tests::configured_renderer_applies_to_not_found", + "command": "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets", + "expected": "Environment is Axum, native. Given a `configure` that installs an error renderer adding a marker header, when an unmatched path arrives, then the 404 carries the marker.", + "artifact_path": "outbound/EZ-23/acceptance/02/axum/native-tcp.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/03", + "source_id": "EZ-23/acceptance/03", + "source_digest": "3236b945f49937be610f663e56b1fe87c2ec4c7548014561deed9efaacfa4458", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-03-axum-native-tcp", + "sentinel": "test_utils::tests::admission_abort_prevents_response_and_dispatch", + "command": "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets", + "expected": "Environment is Axum, native. Given an admission policy that aborts, when a request arrives, then the connection closes with no response bytes and no handler or middleware runs.", + "artifact_path": "outbound/EZ-23/acceptance/03/axum/native-tcp.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/04", + "source_id": "EZ-23/acceptance/04", + "source_digest": "c1a14b636e1b94a3345174e47745a958af5bcdc7694f191697c047c46d7485cb", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-04-axum-native-tcp", + "sentinel": "test_utils::tests::test_server_preserves_upload_and_read_deadline", + "command": "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets", + "expected": "Environment is Axum, native. Given a client that uploads 1 MiB with chunked framing, when a handler reads the body, then it sees every byte. Given a client that sends headers and then stops sending the body past the inbound read budget, then it gets 408.", + "artifact_path": "outbound/EZ-23/acceptance/04/axum/native-tcp.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/05", + "source_id": "EZ-23/acceptance/05", + "source_digest": "d7171d7e4813e0bc3da5d72dc2e563a10036a826ba0d11118cbcd7a8ade99cf8", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-05-axum-native-tcp", + "sentinel": "test_utils::tests::test_server_streams_before_source_completion", + "command": "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets", + "expected": "Environment is Axum, native. Given a handler that streams three chunks 50 ms apart, when a client reads, then the first chunk arrives before the third is produced and the wire uses chunked transfer coding.", + "artifact_path": "outbound/EZ-23/acceptance/05/axum/native-tcp.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/06", + "source_id": "EZ-23/acceptance/06", + "source_digest": "ab7c05d7b801af4ff646716bc5273b17361a3505656e20d9ac4fb302c33f5de9", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-06-axum-native-tcp", + "sentinel": "test_utils::tests::test_server_buffers_with_exact_content_length", + "command": "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets", + "expected": "Environment is Axum, native. Given a buffered 5-byte response, when it is served, then it carries `content-length: 5`.", + "artifact_path": "outbound/EZ-23/acceptance/06/axum/native-tcp.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/07", + "source_id": "EZ-23/acceptance/07", + "source_digest": "91610ad4c0d8b2433180ae68f0163ddf769290fdadc9f8af40410259bbd50432", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-07-axum-native-tcp", + "sentinel": "test_utils::tests::test_server_suppresses_head_payload", + "command": "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets", + "expected": "Environment is Axum, native. Given a HEAD request to a route that returns a body, then the wire carries headers and no body bytes.", + "artifact_path": "outbound/EZ-23/acceptance/07/axum/native-tcp.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/08", + "source_id": "EZ-23/acceptance/08", + "source_digest": "06baaec3976e3b056ad4d09f37b8b27ffe7c2a53b11634237668f4c5ddfa165c", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-08-axum-native-tcp", + "sentinel": "test_utils::tests::raw_head_limit_precedes_admission", + "command": "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets", + "expected": "Environment is Axum, native. Given raw limits of 4 header fields, when a request with 5 fields arrives, then it is rejected before admission and the server keeps serving.", + "artifact_path": "outbound/EZ-23/acceptance/08/axum/native-tcp.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/09", + "source_id": "EZ-23/acceptance/09", + "source_digest": "25ed5c8a7b2f666637f11a16ff6dfecca2e6a67f1c402d17f2fd1d0a82d8a665", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-09-axum-native-tcp", + "sentinel": "test_utils::tests::test_server_shutdown_releases_live_source", + "command": "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets", + "expected": "Environment is Axum, native. Given a handler mid-stream, when the test stops the server, then the handler's resources are released and the serving result reports success.", + "artifact_path": "outbound/EZ-23/acceptance/09/axum/native-tcp.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/10", + "source_id": "EZ-23/acceptance/10", + "source_digest": "190da887701a396a0e2b853b02a31ae516daddb8473664fb5d0a8c0b25461019", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-10-axum-native-tcp", + "sentinel": "test_utils::tests::state_client_skips_real_transport_initialization", + "additional_sentinels": [ + "service::tests::state_client_skips_real_transport_initialization_factory" + ], + "command": "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets", + "expected": "Environment is Axum, native. Given an outbound client supplied through router state on a host with no outbound network access, when a handler sends a request, then the supplied client sees it, no socket opens to the target, and no request fails because the real transport could not start.", + "artifact_path": "outbound/EZ-23/acceptance/10/axum/native-tcp.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/11", + "source_id": "EZ-23/acceptance/11", + "source_digest": "99c6b437ba664833fdeaa06166631cbf6589fd9052f81577b203fd449ec81c8b", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-11-axum-native-tcp", + "sentinel": "test_utils::tests::foreign_platform_app_keeps_configured_policies", + "command": "scripts/run_test_nonzero.sh --include-ignored --inventory axum/native-tcp cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils --all-targets", + "expected": "Environment is Axum, native. Given an app assembled with Cloudflare's platform metadata, when it runs on the test facility, then its `configure` policies apply.", + "artifact_path": "outbound/EZ-23/acceptance/11/axum/native-tcp.log", + "implementation": "ready", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/12", + "source_id": "EZ-23/acceptance/12", + "source_digest": "46e6174c79e3d30dbd08a50889274cf6830ecb3caafa6b0694d44398bd99c193", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F9b", + "substep": "EZ-23-acceptance-12-axum-native-tcp", + "sentinel": "ez_23_acceptance_12_axum_native_tcp", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-23/acceptance/12", + "expected": "Environment is Axum, native. Given a plain `#[tokio::test]`, when a handler spawns local work (and, once EZ-6 lands, begins an outbound request against a supplied client), then it completes, and a streamed body fed by that work delivers every chunk.", + "artifact_path": "outbound/EZ-23/acceptance/12/axum/native-tcp.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-23/acceptance/13", + "source_id": "EZ-23/acceptance/13", + "source_digest": "c3d1a0cbc2aa37209715617dee9f5a68cad99dee541d08bf761345a3372a6d96", + "requirement_ids": [ + "EZ-23.1", + "EZ-23.2", + "EZ-23.3", + "EZ-23.4", + "EZ-23.5", + "EZ-23.6", + "EZ-23.7", + "EZ-23.8", + "EZ-23.9", + "EZ-23.10", + "EZ-23.11" + ], + "environment": "consumer-axum/native-tcp", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "F9b", + "substep": "EZ-23-acceptance-13-consumer-axum-native-tcp", + "sentinel": "ez_23_acceptance_13_consumer_axum_native_tcp", + "command": "consumer-runbook EZ-23/acceptance/13 --environment consumer-axum/native-tcp --sentinel ez_23_acceptance_13_consumer_axum_native_tcp", + "expected": "Environment is Axum, native. Given TS's route tests and parity suite on the test facility with apps assembled from explicit settings with production policies (EZ-22.5), and Cloudflare and Spin parity moved off `router.oneshot` per EZ-23.8, then a 9000-byte request target, which TS does not override, gets 414 from all three apps, and a bodiless GET whose inbound body is an unread stream reaches a local origin with no request body, which is TS's host check for EZ-1.", + "artifact_path": "outbound/EZ-23/acceptance/13/consumer-axum/native-tcp.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/01", + "source_id": "EZ-24/acceptance/01", + "source_digest": "55badeb7a85bea84772fb3c4074f74de094c9295167e1ceee498e869e9d8db94", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-01-unit-core", + "sentinel": "ez_24_acceptance_01_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_01_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is as above. Given valid GET requests that together exercise every request option, when they are sent, then the recordings show identical parts. Mutually incompatible options, such as Image Optimizer and cache tagging, are exercised on separate valid requests; an invalid combination is rejected without dispatch. Given a POST with a 3-byte body, then the recording shows those bytes.", + "artifact_path": "outbound/EZ-24/acceptance/01/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/01", + "source_id": "EZ-24/acceptance/01", + "source_digest": "55badeb7a85bea84772fb3c4074f74de094c9295167e1ceee498e869e9d8db94", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-01-unit-core-wasip1", + "sentinel": "ez_24_acceptance_01_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_01_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is as above. Given valid GET requests that together exercise every request option, when they are sent, then the recordings show identical parts. Mutually incompatible options, such as Image Optimizer and cache tagging, are exercised on separate valid requests; an invalid combination is rejected without dispatch. Given a POST with a 3-byte body, then the recording shows those bytes.", + "artifact_path": "outbound/EZ-24/acceptance/01/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/02", + "source_id": "EZ-24/acceptance/02", + "source_digest": "3abaa7f4eac4eb3624eebf829a9207dbed1c5eed07f54b9d4f2655f371b1d7b7", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-02-unit-core", + "sentinel": "ez_24_acceptance_02_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_02_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is as above. Given a GET whose inbound body carries bytes, when it is forwarded, then it is rejected with the same error as on an adapter. Given a batch slot that asks for a streamed response, then that slot fails with the adapters' batch-eligibility error and its siblings proceed.", + "artifact_path": "outbound/EZ-24/acceptance/02/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/02", + "source_id": "EZ-24/acceptance/02", + "source_digest": "3abaa7f4eac4eb3624eebf829a9207dbed1c5eed07f54b9d4f2655f371b1d7b7", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-02-unit-core-wasip1", + "sentinel": "ez_24_acceptance_02_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_02_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is as above. Given a GET whose inbound body carries bytes, when it is forwarded, then it is rejected with the same error as on an adapter. Given a batch slot that asks for a streamed response, then that slot fails with the adapters' batch-eligibility error and its siblings proceed.", + "artifact_path": "outbound/EZ-24/acceptance/02/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/03", + "source_id": "EZ-24/acceptance/03", + "source_digest": "08da734bb351f035349a119c283470c408f83827dbba7b10212918af12ed25c7", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-03-unit-core", + "sentinel": "ez_24_acceptance_03_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_03_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is as above. Given a 100 ms timeout and a reply scripted at 150 ms, when the request is sent, then it ends as a gateway timeout with per-call-timeout source, recorded at 100 ms. Given no answer and a 2 s deadline, then it ends as a gateway timeout with request-deadline source at 2 s.", + "artifact_path": "outbound/EZ-24/acceptance/03/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/03", + "source_id": "EZ-24/acceptance/03", + "source_digest": "08da734bb351f035349a119c283470c408f83827dbba7b10212918af12ed25c7", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-03-unit-core-wasip1", + "sentinel": "ez_24_acceptance_03_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_03_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is as above. Given a 100 ms timeout and a reply scripted at 150 ms, when the request is sent, then it ends as a gateway timeout with per-call-timeout source, recorded at 100 ms. Given no answer and a 2 s deadline, then it ends as a gateway timeout with request-deadline source at 2 s.", + "artifact_path": "outbound/EZ-24/acceptance/03/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/03", + "source_id": "EZ-24/acceptance/03", + "source_digest": "08da734bb351f035349a119c283470c408f83827dbba7b10212918af12ed25c7", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-js", + "applicability": "conditional", + "approval": "EZ-24.2 JS target adopted@unit/core-js", + "condition": "EZ-24.2 JS target adopted", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-03-unit-core-js", + "sentinel": "ez_24_acceptance_03_unit_core_js", + "command": "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh ez_24_acceptance_03_unit_core_js cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-unknown-unknown --lib", + "expected": "Environment is as above. Given a 100 ms timeout and a reply scripted at 150 ms, when the request is sent, then it ends as a gateway timeout with per-call-timeout source, recorded at 100 ms. Given no answer and a 2 s deadline, then it ends as a gateway timeout with request-deadline source at 2 s.", + "artifact_path": "outbound/EZ-24/acceptance/03/unit/core-js.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/04", + "source_id": "EZ-24/acceptance/04", + "source_digest": "3f52dfb741a991cd93537ed202e18754f2174c9e6686d957ab7315298ed24bbd", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-04-unit-core", + "sentinel": "ez_24_acceptance_04_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_04_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is as above. Given three batch slots arriving at 30, 10, and 20 ms, draining through `next` returns indices 1, 2, 0 with elapsed 10, 20, and 30 ms, followed by `Completed`. Collecting an equivalent batch with `collect()` instead returns an input-index-aligned vector: indices 0, 1, 2 have elapsed 30, 10, and 20 ms respectively.", + "artifact_path": "outbound/EZ-24/acceptance/04/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/04", + "source_id": "EZ-24/acceptance/04", + "source_digest": "3f52dfb741a991cd93537ed202e18754f2174c9e6686d957ab7315298ed24bbd", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-04-unit-core-wasip1", + "sentinel": "ez_24_acceptance_04_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_04_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is as above. Given three batch slots arriving at 30, 10, and 20 ms, draining through `next` returns indices 1, 2, 0 with elapsed 10, 20, and 30 ms, followed by `Completed`. Collecting an equivalent batch with `collect()` instead returns an input-index-aligned vector: indices 0, 1, 2 have elapsed 30, 10, and 20 ms respectively.", + "artifact_path": "outbound/EZ-24/acceptance/04/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/04", + "source_id": "EZ-24/acceptance/04", + "source_digest": "3f52dfb741a991cd93537ed202e18754f2174c9e6686d957ab7315298ed24bbd", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-js", + "applicability": "conditional", + "approval": "EZ-24.2 JS target adopted@unit/core-js", + "condition": "EZ-24.2 JS target adopted", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-04-unit-core-js", + "sentinel": "ez_24_acceptance_04_unit_core_js", + "command": "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh ez_24_acceptance_04_unit_core_js cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-unknown-unknown --lib", + "expected": "Environment is as above. Given three batch slots arriving at 30, 10, and 20 ms, draining through `next` returns indices 1, 2, 0 with elapsed 10, 20, and 30 ms, followed by `Completed`. Collecting an equivalent batch with `collect()` instead returns an input-index-aligned vector: indices 0, 1, 2 have elapsed 30, 10, and 20 ms respectively.", + "artifact_path": "outbound/EZ-24/acceptance/04/unit/core-js.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/05", + "source_id": "EZ-24/acceptance/05", + "source_digest": "160815a4ab9ea269f3682cc5ed479b375dfb856225c9b95a535625e6f785feac", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-05-unit-core", + "sentinel": "ez_24_acceptance_05_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_05_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is as above. Given a 100 ms cutoff, a slot arriving at 99 ms, and a slot arriving at 100 ms, when the batch is collected, then the first is kept with elapsed 99 ms, the second is unresolved, and the batch ends at the cutoff. Given the 99 ms slot observed only by a check at 101 ms, then it is still kept with elapsed 99 ms.", + "artifact_path": "outbound/EZ-24/acceptance/05/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/05", + "source_id": "EZ-24/acceptance/05", + "source_digest": "160815a4ab9ea269f3682cc5ed479b375dfb856225c9b95a535625e6f785feac", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-05-unit-core-wasip1", + "sentinel": "ez_24_acceptance_05_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_05_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is as above. Given a 100 ms cutoff, a slot arriving at 99 ms, and a slot arriving at 100 ms, when the batch is collected, then the first is kept with elapsed 99 ms, the second is unresolved, and the batch ends at the cutoff. Given the 99 ms slot observed only by a check at 101 ms, then it is still kept with elapsed 99 ms.", + "artifact_path": "outbound/EZ-24/acceptance/05/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/05", + "source_id": "EZ-24/acceptance/05", + "source_digest": "160815a4ab9ea269f3682cc5ed479b375dfb856225c9b95a535625e6f785feac", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-js", + "applicability": "conditional", + "approval": "EZ-24.2 JS target adopted@unit/core-js", + "condition": "EZ-24.2 JS target adopted", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-05-unit-core-js", + "sentinel": "ez_24_acceptance_05_unit_core_js", + "command": "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh ez_24_acceptance_05_unit_core_js cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-unknown-unknown --lib", + "expected": "Environment is as above. Given a 100 ms cutoff, a slot arriving at 99 ms, and a slot arriving at 100 ms, when the batch is collected, then the first is kept with elapsed 99 ms, the second is unresolved, and the batch ends at the cutoff. Given the 99 ms slot observed only by a check at 101 ms, then it is still kept with elapsed 99 ms.", + "artifact_path": "outbound/EZ-24/acceptance/05/unit/core-js.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/06", + "source_id": "EZ-24/acceptance/06", + "source_digest": "674512f173dfb168268d1bd36ad262bb8d57a7a82a224bcc96031fd7d0303b1b", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-06-unit-core", + "sentinel": "ez_24_acceptance_06_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_06_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is as above. Given a driver failure scripted after one item, when the batch is collected, then the application gets one item and then the failure, with exactly one slot filled.", + "artifact_path": "outbound/EZ-24/acceptance/06/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/06", + "source_id": "EZ-24/acceptance/06", + "source_digest": "674512f173dfb168268d1bd36ad262bb8d57a7a82a224bcc96031fd7d0303b1b", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-06-unit-core-wasip1", + "sentinel": "ez_24_acceptance_06_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_06_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is as above. Given a driver failure scripted after one item, when the batch is collected, then the application gets one item and then the failure, with exactly one slot filled.", + "artifact_path": "outbound/EZ-24/acceptance/06/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/06", + "source_id": "EZ-24/acceptance/06", + "source_digest": "674512f173dfb168268d1bd36ad262bb8d57a7a82a224bcc96031fd7d0303b1b", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-js", + "applicability": "conditional", + "approval": "EZ-24.2 JS target adopted@unit/core-js", + "condition": "EZ-24.2 JS target adopted", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-06-unit-core-js", + "sentinel": "ez_24_acceptance_06_unit_core_js", + "command": "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh ez_24_acceptance_06_unit_core_js cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-unknown-unknown --lib", + "expected": "Environment is as above. Given a driver failure scripted after one item, when the batch is collected, then the application gets one item and then the failure, with exactly one slot filled.", + "artifact_path": "outbound/EZ-24/acceptance/06/unit/core-js.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/07", + "source_id": "EZ-24/acceptance/07", + "source_digest": "3812d69464abda56e6d3f0a17f83851bf71acc8aac856d07245960208298b2d4", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-07-unit-core", + "sentinel": "ez_24_acceptance_07_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_07_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is as above, once EZ-6 lands. Given a slot arriving at 10 ms, when the application checks without blocking at 0 ms, then nothing is returned. When the clock is advanced to 15 ms and it checks again, then the slot is returned with elapsed 10 ms.", + "artifact_path": "outbound/EZ-24/acceptance/07/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/07", + "source_id": "EZ-24/acceptance/07", + "source_digest": "3812d69464abda56e6d3f0a17f83851bf71acc8aac856d07245960208298b2d4", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-07-unit-core-wasip1", + "sentinel": "ez_24_acceptance_07_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_07_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is as above, once EZ-6 lands. Given a slot arriving at 10 ms, when the application checks without blocking at 0 ms, then nothing is returned. When the clock is advanced to 15 ms and it checks again, then the slot is returned with elapsed 10 ms.", + "artifact_path": "outbound/EZ-24/acceptance/07/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/07", + "source_id": "EZ-24/acceptance/07", + "source_digest": "3812d69464abda56e6d3f0a17f83851bf71acc8aac856d07245960208298b2d4", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-js", + "applicability": "conditional", + "approval": "EZ-24.2 JS target adopted@unit/core-js", + "condition": "EZ-24.2 JS target adopted", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-07-unit-core-js", + "sentinel": "ez_24_acceptance_07_unit_core_js", + "command": "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh ez_24_acceptance_07_unit_core_js cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-unknown-unknown --lib", + "expected": "Environment is as above, once EZ-6 lands. Given a slot arriving at 10 ms, when the application checks without blocking at 0 ms, then nothing is returned. When the clock is advanced to 15 ms and it checks again, then the slot is returned with elapsed 10 ms.", + "artifact_path": "outbound/EZ-24/acceptance/07/unit/core-js.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/08", + "source_id": "EZ-24/acceptance/08", + "source_digest": "20ca0faecb1e256478c4b67341396da5231d3bcd1fb77b09bf89013e74a8dd2e", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-08-unit-core", + "sentinel": "ez_24_acceptance_08_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_08_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is as above, once EZ-6 lands. Given an origin request begun under EZ-6 whose head arrives at 300 ms, and a batch with a 250 ms cutoff whose slots arrive at 50, 100, and 200 ms, when the application checks the batch at 60, 120, and 210 ms, then all three bids are kept. Given a single check at 260 ms instead, then the same three bids are still kept. When the application then waits for the origin, the wait ends with the origin's head.", + "artifact_path": "outbound/EZ-24/acceptance/08/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/08", + "source_id": "EZ-24/acceptance/08", + "source_digest": "20ca0faecb1e256478c4b67341396da5231d3bcd1fb77b09bf89013e74a8dd2e", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-08-unit-core-wasip1", + "sentinel": "ez_24_acceptance_08_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_08_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is as above, once EZ-6 lands. Given an origin request begun under EZ-6 whose head arrives at 300 ms, and a batch with a 250 ms cutoff whose slots arrive at 50, 100, and 200 ms, when the application checks the batch at 60, 120, and 210 ms, then all three bids are kept. Given a single check at 260 ms instead, then the same three bids are still kept. When the application then waits for the origin, the wait ends with the origin's head.", + "artifact_path": "outbound/EZ-24/acceptance/08/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/08", + "source_id": "EZ-24/acceptance/08", + "source_digest": "20ca0faecb1e256478c4b67341396da5231d3bcd1fb77b09bf89013e74a8dd2e", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-js", + "applicability": "conditional", + "approval": "EZ-24.2 JS target adopted@unit/core-js", + "condition": "EZ-24.2 JS target adopted", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-08-unit-core-js", + "sentinel": "ez_24_acceptance_08_unit_core_js", + "command": "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh ez_24_acceptance_08_unit_core_js cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-unknown-unknown --lib", + "expected": "Environment is as above, once EZ-6 lands. Given an origin request begun under EZ-6 whose head arrives at 300 ms, and a batch with a 250 ms cutoff whose slots arrive at 50, 100, and 200 ms, when the application checks the batch at 60, 120, and 210 ms, then all three bids are kept. Given a single check at 260 ms instead, then the same three bids are still kept. When the application then waits for the origin, the wait ends with the origin's head.", + "artifact_path": "outbound/EZ-24/acceptance/08/unit/core-js.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/09", + "source_id": "EZ-24/acceptance/09", + "source_digest": "f36419ff52d1b4fb045d68b1ff1e96b28d20ca54d6d5442e7b32b1423e599759", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-09-unit-core", + "sentinel": "ez_24_acceptance_09_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_09_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is as above, once EZ-9 lands. Given a 302 then a 200 scripted for a redirect-following call, then two requests are recorded in hop order. A followed 302 whose body is oversized, malformed compressed data, failed, or permanently pending does not fail or delay the final 200. When the same 302 is final, ordinary caps and decoding apply. Given a target policy that rejects the second target, then one request is recorded and the call returns EZ-9's rejection.", + "artifact_path": "outbound/EZ-24/acceptance/09/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/09", + "source_id": "EZ-24/acceptance/09", + "source_digest": "f36419ff52d1b4fb045d68b1ff1e96b28d20ca54d6d5442e7b32b1423e599759", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-09-unit-core-wasip1", + "sentinel": "ez_24_acceptance_09_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_09_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is as above, once EZ-9 lands. Given a 302 then a 200 scripted for a redirect-following call, then two requests are recorded in hop order. A followed 302 whose body is oversized, malformed compressed data, failed, or permanently pending does not fail or delay the final 200. When the same 302 is final, ordinary caps and decoding apply. Given a target policy that rejects the second target, then one request is recorded and the call returns EZ-9's rejection.", + "artifact_path": "outbound/EZ-24/acceptance/09/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/10", + "source_id": "EZ-24/acceptance/10", + "source_digest": "aa1e3cb809d4a598270fcec6d9115dd5d25f8356fcac333eb61cae15a2b81456", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-10-axum", + "sentinel": "ez_24_acceptance_10_axum", + "command": "scripts/smoke_test_reusable_app.sh --adapter axum --suite outbound-cutover --case EZ-24/acceptance/10", + "expected": "Environment is Axum, native. Given the same gzip body served by a local origin to the Axum adapter and scripted for the double, then status, decoded bytes, and headers match except the proxy marker value, and an over-limit body fails with the same `ResponseLimitReason` on both.", + "artifact_path": "outbound/EZ-24/acceptance/10/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/11", + "source_id": "EZ-24/acceptance/11", + "source_digest": "67e3bee2c3a92072e431f2e63758ccb58c052f8b4420008837a831b3c5f35d62", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-11-unit-core", + "sentinel": "ez_24_acceptance_11_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_11_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is as above. Given an unscripted request, when it is sent, then it fails with an internal error, the test is told about it, and nothing panics.", + "artifact_path": "outbound/EZ-24/acceptance/11/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/11", + "source_id": "EZ-24/acceptance/11", + "source_digest": "67e3bee2c3a92072e431f2e63758ccb58c052f8b4420008837a831b3c5f35d62", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-11-unit-core-wasip1", + "sentinel": "ez_24_acceptance_11_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_11_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is as above. Given an unscripted request, when it is sent, then it fails with an internal error, the test is told about it, and nothing panics.", + "artifact_path": "outbound/EZ-24/acceptance/11/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/12", + "source_id": "EZ-24/acceptance/12", + "source_digest": "ffcadb8ecd7b096b04e604f82e3361502b6c0dccb71a24177ad6d1aa4b601da2", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "conditional", + "approval": "EZ-24.11 adopted@unit/core", + "condition": "EZ-24.11 adopted", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-12-unit-core", + "sentinel": "ez_24_acceptance_12_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_12_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is as above, only when EZ-24.11 is promoted or present. Given a begun request dropped before its reply arrives, then the test is not told about an unconsumed script for it.", + "artifact_path": "outbound/EZ-24/acceptance/12/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/12", + "source_id": "EZ-24/acceptance/12", + "source_digest": "ffcadb8ecd7b096b04e604f82e3361502b6c0dccb71a24177ad6d1aa4b601da2", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "conditional", + "approval": "EZ-24.11 adopted@unit/core-wasip1", + "condition": "EZ-24.11 adopted", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-12-unit-core-wasip1", + "sentinel": "ez_24_acceptance_12_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_12_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is as above, only when EZ-24.11 is promoted or present. Given a begun request dropped before its reply arrives, then the test is not told about an unconsumed script for it.", + "artifact_path": "outbound/EZ-24/acceptance/12/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/12", + "source_id": "EZ-24/acceptance/12", + "source_digest": "ffcadb8ecd7b096b04e604f82e3361502b6c0dccb71a24177ad6d1aa4b601da2", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-js", + "applicability": "conditional", + "approval": "EZ-24.11 adopted and JS target adopted@unit/core-js", + "condition": "EZ-24.11 adopted and JS target adopted", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-12-unit-core-js", + "sentinel": "ez_24_acceptance_12_unit_core_js", + "command": "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh ez_24_acceptance_12_unit_core_js cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-unknown-unknown --lib", + "expected": "Environment is as above, only when EZ-24.11 is promoted or present. Given a begun request dropped before its reply arrives, then the test is not told about an unconsumed script for it.", + "artifact_path": "outbound/EZ-24/acceptance/12/unit/core-js.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/13", + "source_id": "EZ-24/acceptance/13", + "source_digest": "8cd172689493e820b3453bd908ce333e76d35293f7a2ad1e5bf2e246b246fdbb", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "consumer-unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "P6", + "substep": "EZ-24-acceptance-13-consumer-unit-core", + "sentinel": "ez_24_acceptance_13_consumer_unit_core", + "command": "consumer-runbook EZ-24/acceptance/13 --environment consumer-unit/core --sentinel ez_24_acceptance_13_consumer_unit_core", + "expected": "Environment is host, Trusted Server. TS has no scripted client of its own. Its per-call-site option tests, batch outcome mapping tests, and cutoff boundary tests run on the published double, and its orchestrator tests that used stub clients migrate to it.", + "artifact_path": "outbound/EZ-24/acceptance/13/consumer-unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/14", + "source_id": "EZ-24/acceptance/14", + "source_digest": "83c47d482d525ad6e9548b098615b2324da08d21db04f05296882de45fda96f1", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "conditional", + "approval": "EZ-14 present@unit/core", + "condition": "EZ-14 present", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-14-unit-core", + "sentinel": "ez_24_acceptance_14_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_14_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is host and `wasm32-wasip1`, once EZ-14 lands; also `wasm32-unknown-unknown` when that part of EZ-24.2 is adopted. Given headers at 10 ms, a 500 ms per-call timeout, a 400 ms idle bound, and nonempty upstream chunks every 250 ms, the full streamed body continues past 500 ms. A later 600 ms upstream gap produces the idle-timeout cause at 400 ms of waiting, while an explicit 1 s absolute deadline ends a still-progressing body at 1 s with the deadline cause.", + "artifact_path": "outbound/EZ-24/acceptance/14/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/14", + "source_id": "EZ-24/acceptance/14", + "source_digest": "83c47d482d525ad6e9548b098615b2324da08d21db04f05296882de45fda96f1", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "conditional", + "approval": "EZ-14 present@unit/core-wasip1", + "condition": "EZ-14 present", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-14-unit-core-wasip1", + "sentinel": "ez_24_acceptance_14_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_14_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is host and `wasm32-wasip1`, once EZ-14 lands; also `wasm32-unknown-unknown` when that part of EZ-24.2 is adopted. Given headers at 10 ms, a 500 ms per-call timeout, a 400 ms idle bound, and nonempty upstream chunks every 250 ms, the full streamed body continues past 500 ms. A later 600 ms upstream gap produces the idle-timeout cause at 400 ms of waiting, while an explicit 1 s absolute deadline ends a still-progressing body at 1 s with the deadline cause.", + "artifact_path": "outbound/EZ-24/acceptance/14/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/14", + "source_id": "EZ-24/acceptance/14", + "source_digest": "83c47d482d525ad6e9548b098615b2324da08d21db04f05296882de45fda96f1", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-js", + "applicability": "conditional", + "approval": "EZ-14 present and EZ-24.2 JS target adopted@unit/core-js", + "condition": "EZ-14 present and EZ-24.2 JS target adopted", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-14-unit-core-js", + "sentinel": "ez_24_acceptance_14_unit_core_js", + "command": "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh ez_24_acceptance_14_unit_core_js cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-unknown-unknown --lib", + "expected": "Environment is host and `wasm32-wasip1`, once EZ-14 lands; also `wasm32-unknown-unknown` when that part of EZ-24.2 is adopted. Given headers at 10 ms, a 500 ms per-call timeout, a 400 ms idle bound, and nonempty upstream chunks every 250 ms, the full streamed body continues past 500 ms. A later 600 ms upstream gap produces the idle-timeout cause at 400 ms of waiting, while an explicit 1 s absolute deadline ends a still-progressing body at 1 s with the deadline cause.", + "artifact_path": "outbound/EZ-24/acceptance/14/unit/core-js.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/15", + "source_id": "EZ-24/acceptance/15", + "source_digest": "71ae117e499ce9925c3f92b0a685392ee48958d3d2e957cba1696d6019646529", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-15-unit-core", + "sentinel": "ez_24_acceptance_15_unit_core", + "command": "scripts/run_test_nonzero.sh ez_24_acceptance_15_unit_core cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils", + "expected": "Environment is host and `wasm32-wasip1`; also `wasm32-unknown-unknown` when that part of EZ-24.2 is adopted. Single-send authority override and Image Optimizer options are accepted and recorded under the portable test profile. Image Optimizer on begin or batch is rejected before an expired-deadline check; a valid sibling still proceeds. Documentation says that accepted single-send provider options do not establish support on every real adapter.", + "artifact_path": "outbound/EZ-24/acceptance/15/unit/core.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/15", + "source_id": "EZ-24/acceptance/15", + "source_digest": "71ae117e499ce9925c3f92b0a685392ee48958d3d2e957cba1696d6019646529", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-wasip1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-15-unit-core-wasip1", + "sentinel": "ez_24_acceptance_15_unit_core_wasip1", + "command": "CARGO_TARGET_WASM32_WASIP1_RUNNER='viceroy run' scripts/run_test_nonzero.sh ez_24_acceptance_15_unit_core_wasip1 cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-wasip1 --lib", + "expected": "Environment is host and `wasm32-wasip1`; also `wasm32-unknown-unknown` when that part of EZ-24.2 is adopted. Single-send authority override and Image Optimizer options are accepted and recorded under the portable test profile. Image Optimizer on begin or batch is rejected before an expired-deadline check; a valid sibling still proceeds. Documentation says that accepted single-send provider options do not establish support on every real adapter.", + "artifact_path": "outbound/EZ-24/acceptance/15/unit/core-wasip1.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-24/acceptance/15", + "source_id": "EZ-24/acceptance/15", + "source_digest": "71ae117e499ce9925c3f92b0a685392ee48958d3d2e957cba1696d6019646529", + "requirement_ids": [ + "EZ-24.1", + "EZ-24.2", + "EZ-24.3", + "EZ-24.4", + "EZ-24.5", + "EZ-24.6", + "EZ-24.7", + "EZ-24.8", + "EZ-24.9", + "EZ-24.10", + "EZ-24.11", + "EZ-24.12" + ], + "environment": "unit/core-js", + "applicability": "conditional", + "approval": "EZ-24.2 JS target adopted@unit/core-js", + "condition": "EZ-24.2 JS target adopted", + "owner": "EdgeZero", + "task": "P6", + "substep": "EZ-24-acceptance-15-unit-core-js", + "sentinel": "ez_24_acceptance_15_unit_core_js", + "command": "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner scripts/run_test_nonzero.sh ez_24_acceptance_15_unit_core_js cargo test --offline --locked -p edgezero-core --no-default-features --features test-utils --target wasm32-unknown-unknown --lib", + "expected": "Environment is host and `wasm32-wasip1`; also `wasm32-unknown-unknown` when that part of EZ-24.2 is adopted. Single-send authority override and Image Optimizer options are accepted and recorded under the portable test profile. Image Optimizer on begin or batch is rejected before an expired-deadline check; a valid sibling still proceeds. Documentation says that accepted single-send provider options do not establish support on every real adapter.", + "artifact_path": "outbound/EZ-24/acceptance/15/unit/core-js.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-25/acceptance/01", + "source_id": "EZ-25/acceptance/01", + "source_digest": "2aea7fba95c5e829cbacaee3490f2670be18f9448dcf96e4cc2b28b8bd58e55e", + "requirement_ids": [ + "EZ-25.1", + "EZ-25.2", + "EZ-25.3", + "EZ-25.4", + "EZ-25.5", + "EZ-25.6", + "EZ-25.7", + "EZ-25.8", + "EZ-25.9", + "EZ-25.10", + "EZ-25.11", + "EZ-25.12", + "EZ-25.13", + "EZ-25.14", + "EZ-25.15", + "EZ-25.16", + "EZ-25.17", + "EZ-25.18", + "EZ-25.19", + "EZ-25.20", + "EZ-25.21", + "EZ-25.22", + "EZ-25.23", + "EZ-25.24", + "EZ-25.25", + "EZ-25.26", + "EZ-25.27", + "EZ-25.28", + "EZ-25.29", + "EZ-25.30", + "EZ-25.31", + "EZ-25.32", + "EZ-25.33", + "EZ-25.34", + "EZ-25.35", + "EZ-25.36", + "EZ-25.37", + "EZ-25.38", + "EZ-25.39", + "EZ-25.40", + "EZ-25.41", + "EZ-25.42", + "EZ-25.43", + "EZ-25.44" + ], + "environment": "edgezero-ci", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "Q1", + "substep": "EZ-25-acceptance-01-edgezero-ci", + "sentinel": "ez_25_acceptance_01_edgezero_ci", + "command": "node scripts/check_outbound_docs_contract.mjs", + "expected": "Environment is EdgeZero CI on the recorded commit. Given the fixes, when `docs/guide` is searched for `stream_to_client`, then only `adapters/fastly.md:347` matches.", + "artifact_path": "outbound/EZ-25/acceptance/01/edgezero-ci.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-25/acceptance/02", + "source_id": "EZ-25/acceptance/02", + "source_digest": "eceafb2e730f5f23e25c04487b7df30799321cec0635973bd42a4cbc43c52716", + "requirement_ids": [ + "EZ-25.1", + "EZ-25.2", + "EZ-25.3", + "EZ-25.4", + "EZ-25.5", + "EZ-25.6", + "EZ-25.7", + "EZ-25.8", + "EZ-25.9", + "EZ-25.10", + "EZ-25.11", + "EZ-25.12", + "EZ-25.13", + "EZ-25.14", + "EZ-25.15", + "EZ-25.16", + "EZ-25.17", + "EZ-25.18", + "EZ-25.19", + "EZ-25.20", + "EZ-25.21", + "EZ-25.22", + "EZ-25.23", + "EZ-25.24", + "EZ-25.25", + "EZ-25.26", + "EZ-25.27", + "EZ-25.28", + "EZ-25.29", + "EZ-25.30", + "EZ-25.31", + "EZ-25.32", + "EZ-25.33", + "EZ-25.34", + "EZ-25.35", + "EZ-25.36", + "EZ-25.37", + "EZ-25.38", + "EZ-25.39", + "EZ-25.40", + "EZ-25.41", + "EZ-25.42", + "EZ-25.43", + "EZ-25.44" + ], + "environment": "edgezero-ci", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "Q1", + "substep": "EZ-25-acceptance-02-edgezero-ci", + "sentinel": "ez_25_acceptance_02_edgezero_ci", + "command": "node scripts/check_outbound_docs_contract.mjs", + "expected": "Environment is EdgeZero CI on the recorded commit. Given the fixes, when `CHANGELOG.md` and `docs/guide/adapters/axum.md` are searched for `1.95.0`, then only the audit statements EZ-25.29 keeps match.", + "artifact_path": "outbound/EZ-25/acceptance/02/edgezero-ci.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-25/acceptance/03", + "source_id": "EZ-25/acceptance/03", + "source_digest": "b74f51c63a57bd2ccb5143993185011627d251b5d96643c1bd6b47f7cfa62a11", + "requirement_ids": [ + "EZ-25.1", + "EZ-25.2", + "EZ-25.3", + "EZ-25.4", + "EZ-25.5", + "EZ-25.6", + "EZ-25.7", + "EZ-25.8", + "EZ-25.9", + "EZ-25.10", + "EZ-25.11", + "EZ-25.12", + "EZ-25.13", + "EZ-25.14", + "EZ-25.15", + "EZ-25.16", + "EZ-25.17", + "EZ-25.18", + "EZ-25.19", + "EZ-25.20", + "EZ-25.21", + "EZ-25.22", + "EZ-25.23", + "EZ-25.24", + "EZ-25.25", + "EZ-25.26", + "EZ-25.27", + "EZ-25.28", + "EZ-25.29", + "EZ-25.30", + "EZ-25.31", + "EZ-25.32", + "EZ-25.33", + "EZ-25.34", + "EZ-25.35", + "EZ-25.36", + "EZ-25.37", + "EZ-25.38", + "EZ-25.39", + "EZ-25.40", + "EZ-25.41", + "EZ-25.42", + "EZ-25.43", + "EZ-25.44" + ], + "environment": "edgezero-ci", + "applicability": "conditional", + "approval": "EZ-25.42 adopted@edgezero-ci", + "condition": "EZ-25.42 adopted", + "owner": "EdgeZero", + "task": "Q1", + "substep": "EZ-25-acceptance-03-edgezero-ci", + "sentinel": "ez_25_acceptance_03_edgezero_ci", + "command": "node scripts/check_outbound_docs_contract.mjs", + "expected": "Environment is EdgeZero CI on the recorded commit. Given EZ-25.42, when CI runs, then every changed snippet that is a complete item compiles, including the forwarding example, and every changed fragment either compiles inside its documented wrapper or is marked `ignore` with a reason.", + "artifact_path": "outbound/EZ-25/acceptance/03/edgezero-ci.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-25/acceptance/04", + "source_id": "EZ-25/acceptance/04", + "source_digest": "8ff11b72f85c2523c03fcb867213d279deb3fb1cfdfba7fc3958c6d7b6d113d2", + "requirement_ids": [ + "EZ-25.1", + "EZ-25.2", + "EZ-25.3", + "EZ-25.4", + "EZ-25.5", + "EZ-25.6", + "EZ-25.7", + "EZ-25.8", + "EZ-25.9", + "EZ-25.10", + "EZ-25.11", + "EZ-25.12", + "EZ-25.13", + "EZ-25.14", + "EZ-25.15", + "EZ-25.16", + "EZ-25.17", + "EZ-25.18", + "EZ-25.19", + "EZ-25.20", + "EZ-25.21", + "EZ-25.22", + "EZ-25.23", + "EZ-25.24", + "EZ-25.25", + "EZ-25.26", + "EZ-25.27", + "EZ-25.28", + "EZ-25.29", + "EZ-25.30", + "EZ-25.31", + "EZ-25.32", + "EZ-25.33", + "EZ-25.34", + "EZ-25.35", + "EZ-25.36", + "EZ-25.37", + "EZ-25.38", + "EZ-25.39", + "EZ-25.40", + "EZ-25.41", + "EZ-25.42", + "EZ-25.43", + "EZ-25.44" + ], + "environment": "edgezero-ci/axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "Q1", + "substep": "EZ-25-acceptance-04-edgezero-ci-axum", + "sentinel": "ez_25_acceptance_04_edgezero_ci_axum", + "command": "scripts/run_test_nonzero.sh ez_25_acceptance_04_edgezero_ci_axum cargo test --offline --locked -p edgezero-adapter-axum --no-default-features --features axum,test-utils", + "expected": "Environment is EdgeZero CI on the recorded commit. Given a manifest using EZ-25.8's declaration, when the capability gate runs for each of the four adapters, then it passes.", + "artifact_path": "outbound/EZ-25/acceptance/04/edgezero-ci/axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-25/acceptance/04", + "source_id": "EZ-25/acceptance/04", + "source_digest": "8ff11b72f85c2523c03fcb867213d279deb3fb1cfdfba7fc3958c6d7b6d113d2", + "requirement_ids": [ + "EZ-25.1", + "EZ-25.2", + "EZ-25.3", + "EZ-25.4", + "EZ-25.5", + "EZ-25.6", + "EZ-25.7", + "EZ-25.8", + "EZ-25.9", + "EZ-25.10", + "EZ-25.11", + "EZ-25.12", + "EZ-25.13", + "EZ-25.14", + "EZ-25.15", + "EZ-25.16", + "EZ-25.17", + "EZ-25.18", + "EZ-25.19", + "EZ-25.20", + "EZ-25.21", + "EZ-25.22", + "EZ-25.23", + "EZ-25.24", + "EZ-25.25", + "EZ-25.26", + "EZ-25.27", + "EZ-25.28", + "EZ-25.29", + "EZ-25.30", + "EZ-25.31", + "EZ-25.32", + "EZ-25.33", + "EZ-25.34", + "EZ-25.35", + "EZ-25.36", + "EZ-25.37", + "EZ-25.38", + "EZ-25.39", + "EZ-25.40", + "EZ-25.41", + "EZ-25.42", + "EZ-25.43", + "EZ-25.44" + ], + "environment": "edgezero-ci/fastly", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "Q1", + "substep": "EZ-25-acceptance-04-edgezero-ci-fastly", + "sentinel": "ez_25_acceptance_04_edgezero_ci_fastly", + "command": "scripts/run_test_nonzero.sh ez_25_acceptance_04_edgezero_ci_fastly cargo test --offline --locked -p edgezero-adapter-fastly --no-default-features --features test-utils", + "expected": "Environment is EdgeZero CI on the recorded commit. Given a manifest using EZ-25.8's declaration, when the capability gate runs for each of the four adapters, then it passes.", + "artifact_path": "outbound/EZ-25/acceptance/04/edgezero-ci/fastly.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-25/acceptance/04", + "source_id": "EZ-25/acceptance/04", + "source_digest": "8ff11b72f85c2523c03fcb867213d279deb3fb1cfdfba7fc3958c6d7b6d113d2", + "requirement_ids": [ + "EZ-25.1", + "EZ-25.2", + "EZ-25.3", + "EZ-25.4", + "EZ-25.5", + "EZ-25.6", + "EZ-25.7", + "EZ-25.8", + "EZ-25.9", + "EZ-25.10", + "EZ-25.11", + "EZ-25.12", + "EZ-25.13", + "EZ-25.14", + "EZ-25.15", + "EZ-25.16", + "EZ-25.17", + "EZ-25.18", + "EZ-25.19", + "EZ-25.20", + "EZ-25.21", + "EZ-25.22", + "EZ-25.23", + "EZ-25.24", + "EZ-25.25", + "EZ-25.26", + "EZ-25.27", + "EZ-25.28", + "EZ-25.29", + "EZ-25.30", + "EZ-25.31", + "EZ-25.32", + "EZ-25.33", + "EZ-25.34", + "EZ-25.35", + "EZ-25.36", + "EZ-25.37", + "EZ-25.38", + "EZ-25.39", + "EZ-25.40", + "EZ-25.41", + "EZ-25.42", + "EZ-25.43", + "EZ-25.44" + ], + "environment": "edgezero-ci/cloudflare", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "Q1", + "substep": "EZ-25-acceptance-04-edgezero-ci-cloudflare", + "sentinel": "ez_25_acceptance_04_edgezero_ci_cloudflare", + "command": "scripts/run_test_nonzero.sh ez_25_acceptance_04_edgezero_ci_cloudflare cargo test --offline --locked -p edgezero-adapter-cloudflare --no-default-features --features test-utils", + "expected": "Environment is EdgeZero CI on the recorded commit. Given a manifest using EZ-25.8's declaration, when the capability gate runs for each of the four adapters, then it passes.", + "artifact_path": "outbound/EZ-25/acceptance/04/edgezero-ci/cloudflare.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-25/acceptance/04", + "source_id": "EZ-25/acceptance/04", + "source_digest": "8ff11b72f85c2523c03fcb867213d279deb3fb1cfdfba7fc3958c6d7b6d113d2", + "requirement_ids": [ + "EZ-25.1", + "EZ-25.2", + "EZ-25.3", + "EZ-25.4", + "EZ-25.5", + "EZ-25.6", + "EZ-25.7", + "EZ-25.8", + "EZ-25.9", + "EZ-25.10", + "EZ-25.11", + "EZ-25.12", + "EZ-25.13", + "EZ-25.14", + "EZ-25.15", + "EZ-25.16", + "EZ-25.17", + "EZ-25.18", + "EZ-25.19", + "EZ-25.20", + "EZ-25.21", + "EZ-25.22", + "EZ-25.23", + "EZ-25.24", + "EZ-25.25", + "EZ-25.26", + "EZ-25.27", + "EZ-25.28", + "EZ-25.29", + "EZ-25.30", + "EZ-25.31", + "EZ-25.32", + "EZ-25.33", + "EZ-25.34", + "EZ-25.35", + "EZ-25.36", + "EZ-25.37", + "EZ-25.38", + "EZ-25.39", + "EZ-25.40", + "EZ-25.41", + "EZ-25.42", + "EZ-25.43", + "EZ-25.44" + ], + "environment": "edgezero-ci/spin", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "Q1", + "substep": "EZ-25-acceptance-04-edgezero-ci-spin", + "sentinel": "ez_25_acceptance_04_edgezero_ci_spin", + "command": "scripts/run_test_nonzero.sh ez_25_acceptance_04_edgezero_ci_spin cargo test --offline --locked -p edgezero-adapter-spin --no-default-features --features test-utils", + "expected": "Environment is EdgeZero CI on the recorded commit. Given a manifest using EZ-25.8's declaration, when the capability gate runs for each of the four adapters, then it passes.", + "artifact_path": "outbound/EZ-25/acceptance/04/edgezero-ci/spin.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-25/acceptance/05", + "source_id": "EZ-25/acceptance/05", + "source_digest": "1f720daf578c15ddb539ea9eee47fbc90d9a061b54b6218d9699b16638dbc7aa", + "requirement_ids": [ + "EZ-25.1", + "EZ-25.2", + "EZ-25.3", + "EZ-25.4", + "EZ-25.5", + "EZ-25.6", + "EZ-25.7", + "EZ-25.8", + "EZ-25.9", + "EZ-25.10", + "EZ-25.11", + "EZ-25.12", + "EZ-25.13", + "EZ-25.14", + "EZ-25.15", + "EZ-25.16", + "EZ-25.17", + "EZ-25.18", + "EZ-25.19", + "EZ-25.20", + "EZ-25.21", + "EZ-25.22", + "EZ-25.23", + "EZ-25.24", + "EZ-25.25", + "EZ-25.26", + "EZ-25.27", + "EZ-25.28", + "EZ-25.29", + "EZ-25.30", + "EZ-25.31", + "EZ-25.32", + "EZ-25.33", + "EZ-25.34", + "EZ-25.35", + "EZ-25.36", + "EZ-25.37", + "EZ-25.38", + "EZ-25.39", + "EZ-25.40", + "EZ-25.41", + "EZ-25.42", + "EZ-25.43", + "EZ-25.44" + ], + "environment": "edgezero-ci", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "Q1", + "substep": "EZ-25-acceptance-05-edgezero-ci", + "sentinel": "ez_25_acceptance_05_edgezero_ci", + "command": "node scripts/check_outbound_docs_contract.mjs", + "expected": "Environment is EdgeZero CI on the recorded commit. Given the fixes, when EdgeZero's documentation checks run (`scripts/check_outbound_docs_contract.mjs`, `scripts/check_outbound_legacy_api.sh`, and the docs formatter), then they pass.", + "artifact_path": "outbound/EZ-25/acceptance/05/edgezero-ci.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-25/acceptance/06", + "source_id": "EZ-25/acceptance/06", + "source_digest": "ddbac3600072fbebcf32dd35ae61810821ce5795232c5a23907893c14ef05af5", + "requirement_ids": [ + "EZ-25.1", + "EZ-25.2", + "EZ-25.3", + "EZ-25.4", + "EZ-25.5", + "EZ-25.6", + "EZ-25.7", + "EZ-25.8", + "EZ-25.9", + "EZ-25.10", + "EZ-25.11", + "EZ-25.12", + "EZ-25.13", + "EZ-25.14", + "EZ-25.15", + "EZ-25.16", + "EZ-25.17", + "EZ-25.18", + "EZ-25.19", + "EZ-25.20", + "EZ-25.21", + "EZ-25.22", + "EZ-25.23", + "EZ-25.24", + "EZ-25.25", + "EZ-25.26", + "EZ-25.27", + "EZ-25.28", + "EZ-25.29", + "EZ-25.30", + "EZ-25.31", + "EZ-25.32", + "EZ-25.33", + "EZ-25.34", + "EZ-25.35", + "EZ-25.36", + "EZ-25.37", + "EZ-25.38", + "EZ-25.39", + "EZ-25.40", + "EZ-25.41", + "EZ-25.42", + "EZ-25.43", + "EZ-25.44" + ], + "environment": "consumer-axum", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "Q1", + "substep": "EZ-25-acceptance-06-consumer-axum", + "sentinel": "ez_25_acceptance_06_consumer_axum", + "command": "consumer-runbook EZ-25/acceptance/06 --environment consumer-axum --sentinel ez_25_acceptance_06_consumer_axum", + "expected": "Environment is Axum, Trusted Server. TS's publisher forwarding, written from the corrected example once EZ-1 lands, forwards a bodiless GET whose inbound body is an unread stream to a local origin with no request body. TS's Fastly custom lifecycle compiles against the five-argument helper the README now shows.", + "artifact_path": "outbound/EZ-25/acceptance/06/consumer-axum.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-25/acceptance/06", + "source_id": "EZ-25/acceptance/06", + "source_digest": "ddbac3600072fbebcf32dd35ae61810821ce5795232c5a23907893c14ef05af5", + "requirement_ids": [ + "EZ-25.1", + "EZ-25.2", + "EZ-25.3", + "EZ-25.4", + "EZ-25.5", + "EZ-25.6", + "EZ-25.7", + "EZ-25.8", + "EZ-25.9", + "EZ-25.10", + "EZ-25.11", + "EZ-25.12", + "EZ-25.13", + "EZ-25.14", + "EZ-25.15", + "EZ-25.16", + "EZ-25.17", + "EZ-25.18", + "EZ-25.19", + "EZ-25.20", + "EZ-25.21", + "EZ-25.22", + "EZ-25.23", + "EZ-25.24", + "EZ-25.25", + "EZ-25.26", + "EZ-25.27", + "EZ-25.28", + "EZ-25.29", + "EZ-25.30", + "EZ-25.31", + "EZ-25.32", + "EZ-25.33", + "EZ-25.34", + "EZ-25.35", + "EZ-25.36", + "EZ-25.37", + "EZ-25.38", + "EZ-25.39", + "EZ-25.40", + "EZ-25.41", + "EZ-25.42", + "EZ-25.43", + "EZ-25.44" + ], + "environment": "consumer-fastly-build", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "Q1", + "substep": "EZ-25-acceptance-06-consumer-fastly-build", + "sentinel": "ez_25_acceptance_06_consumer_fastly_build", + "command": "consumer-runbook EZ-25/acceptance/06 --environment consumer-fastly-build --sentinel ez_25_acceptance_06_consumer_fastly_build", + "expected": "Environment is Axum, Trusted Server. TS's publisher forwarding, written from the corrected example once EZ-1 lands, forwards a bodiless GET whose inbound body is an unread stream to a local origin with no request body. TS's Fastly custom lifecycle compiles against the five-argument helper the README now shows.", + "artifact_path": "outbound/EZ-25/acceptance/06/consumer-fastly-build.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-Q1/acceptance/01", + "source_id": "EZ-Q1/acceptance/01", + "source_digest": "7e000db034659cac3f4ae177d61a68ade120730ad1287dafbe1409d88c99de2c", + "requirement_ids": [ + "EZ-Q1.1", + "EZ-Q1.2", + "EZ-Q1.3", + "EZ-Q1.4", + "EZ-Q1.5", + "EZ-Q1.6", + "EZ-Q1.7", + "EZ-Q1.8" + ], + "environment": "consumer-review", + "applicability": "gate", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "Q5", + "substep": "EZ-Q1-acceptance-01-consumer-review", + "sentinel": "ez_q1_acceptance_01_consumer_review", + "command": "consumer-runbook EZ-Q1/acceptance/01 --environment consumer-review --sentinel ez_q1_acceptance_01_consumer_review", + "expected": "Trusted Server review. A record that names a commit other than the pin is rejected.", + "artifact_path": "outbound/EZ-Q1/acceptance/01/consumer-review.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-Q1/acceptance/02", + "source_id": "EZ-Q1/acceptance/02", + "source_digest": "f5f37160a57805c76d39d701288303d01059e4269186a165267361c71e72ad03", + "requirement_ids": [ + "EZ-Q1.1", + "EZ-Q1.2", + "EZ-Q1.3", + "EZ-Q1.4", + "EZ-Q1.5", + "EZ-Q1.6", + "EZ-Q1.7", + "EZ-Q1.8" + ], + "environment": "consumer-review", + "applicability": "gate", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "Q5", + "substep": "EZ-Q1-acceptance-02-consumer-review", + "sentinel": "ez_q1_acceptance_02_consumer_review", + "command": "consumer-runbook EZ-Q1/acceptance/02 --environment consumer-review --sentinel ez_q1_acceptance_02_consumer_review", + "expected": "Trusted Server review. A record that lacks a result for any criterion EZ-Q1.1 or EZ-Q1.6 requires, records a result in an environment other than the one the criterion names, or records a failing result is rejected.", + "artifact_path": "outbound/EZ-Q1/acceptance/02/consumer-review.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-Q1/acceptance/03", + "source_id": "EZ-Q1/acceptance/03", + "source_digest": "81d3628a20acbda2c707897232390ef446b9ac70ecd4439ed2f78f1cdb31c3ab", + "requirement_ids": [ + "EZ-Q1.1", + "EZ-Q1.2", + "EZ-Q1.3", + "EZ-Q1.4", + "EZ-Q1.5", + "EZ-Q1.6", + "EZ-Q1.7", + "EZ-Q1.8" + ], + "environment": "consumer-review", + "applicability": "gate", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "Q5", + "substep": "EZ-Q1-acceptance-03-consumer-review", + "sentinel": "ez_q1_acceptance_03_consumer_review", + "command": "consumer-runbook EZ-Q1/acceptance/03 --environment consumer-review --sentinel ez_q1_acceptance_03_consumer_review", + "expected": "Trusted Server review. A record whose toolchain differs from EZ-Q1.3, that names no runner image, or that omits measurement sample counts or the builds behind a comparison is rejected.", + "artifact_path": "outbound/EZ-Q1/acceptance/03/consumer-review.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-Q1/acceptance/04", + "source_id": "EZ-Q1/acceptance/04", + "source_digest": "1fd1ab3e832336d38386149f377a3b5e0c86f520a7d14e0d4b052fb374b70d83", + "requirement_ids": [ + "EZ-Q1.1", + "EZ-Q1.2", + "EZ-Q1.3", + "EZ-Q1.4", + "EZ-Q1.5", + "EZ-Q1.6", + "EZ-Q1.7", + "EZ-Q1.8" + ], + "environment": "consumer-review", + "applicability": "gate", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "Q5", + "substep": "EZ-Q1-acceptance-04-consumer-review", + "sentinel": "ez_q1_acceptance_04_consumer_review", + "command": "consumer-runbook EZ-Q1/acceptance/04 --environment consumer-review --sentinel ez_q1_acceptance_04_consumer_review", + "expected": "Trusted Server review. A record stored only where it expires within 90 days, such as a CI artifact, is rejected.", + "artifact_path": "outbound/EZ-Q1/acceptance/04/consumer-review.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-Q1/acceptance/05", + "source_id": "EZ-Q1/acceptance/05", + "source_digest": "89872e35e980b8cbd01f0bae84c225bb6f6da0e3a387186ec34d43cb4ad2f896", + "requirement_ids": [ + "EZ-Q1.1", + "EZ-Q1.2", + "EZ-Q1.3", + "EZ-Q1.4", + "EZ-Q1.5", + "EZ-Q1.6", + "EZ-Q1.7", + "EZ-Q1.8" + ], + "environment": "consumer-review", + "applicability": "gate", + "approval": null, + "condition": null, + "owner": "Trusted Server", + "task": "Q5", + "substep": "EZ-Q1-acceptance-05-consumer-review", + "sentinel": "ez_q1_acceptance_05_consumer_review", + "command": "consumer-runbook EZ-Q1/acceptance/05 --environment consumer-review --sentinel ez_q1_acceptance_05_consumer_review", + "expected": "Trusted Server review. A record that omits the applicability map, skips a required blocking leg as unimplemented, or uses partial adoption to skip the applicable portion of a mixed criterion is rejected. Approved deferred targets and conditional follow-up legs do not fail the record when their exclusion is explicitly justified under EZ-Q1.8.", + "artifact_path": "outbound/EZ-Q1/acceptance/05/consumer-review.json", + "implementation": "planned", + "comparison": false + }, + { + "case_id": "EZ-18/acceptance/03b", + "source_id": "EZ-18/acceptance/03", + "source_digest": "b0f560193d9b89adddeda14327d46ad84a504177aece21342e9b7396533406d8", + "requirement_ids": ["EZ-18.5"], + "environment": "hosted-fastly/http1", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7a", + "substep": "EZ-18-acceptance-03-hosted-fastly-http1", + "sentinel": "ez_18_acceptance_03_hosted_fastly_http1_streamed_transform", + "command": "hosted-runbook EZ-18/acceptance/03b --environment hosted-fastly/http1 --sentinel ez_18_acceptance_03_hosted_fastly_http1_streamed_transform", + "expected": "Hosted Fastly, HTTP/1.1 and HTTP/2, on TS's staging service. For a buffered 200, a buffered 200 with `x-compress-hint: on` requested with `Accept-Encoding: gzip`, HEAD and conditional-GET 304 of a buffered route, and a 204, the framing equals that of TS's pre-cut build. The compressed body decompresses to the expected SHA-256. Also exercise a streamed compressible response with its source `Content-Length` declared and `x-compress-hint: on`, requested with gzip: verify the host-compressed body decompresses to the source SHA-256 and has valid framing. Record the source length, transmitted byte count, coding, and any client-visible length; that length, when present, equals the transmitted count, not the source count.", + "artifact_path": "outbound/EZ-18/acceptance/03b/hosted-fastly/http1.json", + "implementation": "planned", + "comparison": false, + "assertion_scope": "Streamed host-transformation framing only" + }, + { + "case_id": "EZ-18/acceptance/03b", + "source_id": "EZ-18/acceptance/03", + "source_digest": "b0f560193d9b89adddeda14327d46ad84a504177aece21342e9b7396533406d8", + "requirement_ids": ["EZ-18.5"], + "environment": "hosted-fastly/http2", + "applicability": "follow-up", + "approval": null, + "condition": null, + "owner": "EdgeZero", + "task": "F7a", + "substep": "EZ-18-acceptance-03-hosted-fastly-http2", + "sentinel": "ez_18_acceptance_03_hosted_fastly_http2_streamed_transform", + "command": "hosted-runbook EZ-18/acceptance/03b --environment hosted-fastly/http2 --sentinel ez_18_acceptance_03_hosted_fastly_http2_streamed_transform", + "expected": "Hosted Fastly, HTTP/1.1 and HTTP/2, on TS's staging service. For a buffered 200, a buffered 200 with `x-compress-hint: on` requested with `Accept-Encoding: gzip`, HEAD and conditional-GET 304 of a buffered route, and a 204, the framing equals that of TS's pre-cut build. The compressed body decompresses to the expected SHA-256. Also exercise a streamed compressible response with its source `Content-Length` declared and `x-compress-hint: on`, requested with gzip: verify the host-compressed body decompresses to the source SHA-256 and has valid framing. Record the source length, transmitted byte count, coding, and any client-visible length; that length, when present, equals the transmitted count, not the source count.", + "artifact_path": "outbound/EZ-18/acceptance/03b/hosted-fastly/http2.json", + "implementation": "planned", + "comparison": false, + "assertion_scope": "Streamed host-transformation framing only" + }, + { + "case_id": "EZ-24/requirements/12", + "source_id": "EZ-24.12", + "source_digest": "99bbb8bf53e20c00884675bc178f50cc62ac2deefdbfd56834f34cd3c3bb6879", + "requirement_ids": ["EZ-24.12"], + "environment": "axum/demo", + "applicability": "conditional", + "approval": "EZ-24.12 adopted@axum/demo", + "condition": "EZ-24.12 adopted", + "owner": "EdgeZero", + "task": "F10b", + "substep": "Replace demo/template bespoke doubles with the published client", + "sentinel": "demo_uses_published_outbound_client", + "command": "cargo test --offline --locked --manifest-path examples/app-demo/Cargo.toml", + "expected": "EdgeZero's generated application template MUST use the published double instead of its own.", + "artifact_path": "outbound/EZ-24/requirements/12/axum/demo.json", + "implementation": "planned", + "comparison": false, + "source_kind": "normative", + "assertion_scope": "Template migration plus aligned demo coverage under F10b" + }, + { + "case_id": "EZ-24/requirements/12", + "source_id": "EZ-24.12", + "source_digest": "99bbb8bf53e20c00884675bc178f50cc62ac2deefdbfd56834f34cd3c3bb6879", + "requirement_ids": ["EZ-24.12"], + "environment": "axum/generated", + "applicability": "conditional", + "approval": "EZ-24.12 adopted@axum/generated", + "condition": "EZ-24.12 adopted", + "owner": "EdgeZero", + "task": "F10b", + "substep": "Replace demo/template bespoke doubles with the published client", + "sentinel": "generated_workspace_compiles", + "command": "scripts/run_test_nonzero.sh --ignored generated_workspace_compiles cargo test --offline --locked -p edgezero-cli --test generated_project_builds", + "expected": "EdgeZero's generated application template MUST use the published double instead of its own.", + "artifact_path": "outbound/EZ-24/requirements/12/axum/generated.json", + "implementation": "planned", + "comparison": false, + "source_kind": "normative", + "assertion_scope": "Template migration plus aligned demo coverage under F10b" + }, + { + "case_id": "EZ-25/acceptance/05b", + "source_id": "EZ-25/acceptance/05", + "source_digest": "1f720daf578c15ddb539ea9eee47fbc90d9a061b54b6218d9699b16638dbc7aa", + "requirement_ids": ["EZ-25.43"], + "environment": "edgezero-ci", + "applicability": "conditional", + "approval": "EZ-25.43 adopted@edgezero-ci", + "condition": "EZ-25.43 adopted", + "owner": "EdgeZero", + "task": "Q1", + "substep": "Adopt independent docs drift-checker regressions", + "sentinel": "outbound_docs_checker_rejects_drift", + "command": "node --test scripts/check_outbound_docs_contract.test.mjs", + "expected": "Environment is EdgeZero CI on the recorded commit. Given the fixes, when EdgeZero's documentation checks run (`scripts/check_outbound_docs_contract.mjs`, `scripts/check_outbound_legacy_api.sh`, and the docs formatter), then they pass.", + "artifact_path": "outbound/EZ-25/acceptance/05b/edgezero-ci.json", + "implementation": "planned", + "comparison": false + } + ] +} diff --git a/tests/fixtures/reusable-app/ts-outbound-qualification-runbook.md b/tests/fixtures/reusable-app/ts-outbound-qualification-runbook.md new file mode 100644 index 000000000..9800e1d13 --- /dev/null +++ b/tests/fixtures/reusable-app/ts-outbound-qualification-runbook.md @@ -0,0 +1,254 @@ +# Trusted Server Outbound Qualification Runbook Template + +**Source template only: unaccepted, unqualified and not execution authorization.** +Fill an external copy after Q3 freezes the candidate. Do not fill candidate identities by +editing this source after freeze. This document changes no outbound contract or capability. + +## Authorities and Entry Gates + +**Dispatch-slack retirement, 2026-10-27:** the requester authorized removing the fixed 25 ms +dispatch-slack guard and its test injection helper. This supersedes older statements below that +slack remains unchanged. Original absolute deadlines, timeout causes, late-result rejection, B31 +backend/timer policy and resource limits remain unchanged. Synchronous SDK host calls are still +non-preemptible; no finite blocking horizon, Native capability or hosted qualification follows. +Keep historical hashes, measurements and failed probes intact. EZ-3 legs remain planned until +real executable commands and sentinels are bound; this authorization is not acceptance evidence. + +**Current scoped override, 2026-10-24:** the requester's [timing and native integration approval](../../../docs/audits/2026-10-18-outbound-api-approval.md#scoped-timing-and-native-integration-amendment) +permits a Send-first implementation checkpoint, not qualification/deployment. It supersedes the +implementation-blocking interpretation of the earlier G2/H(K) statements below: +5 ms stays a +measured qualification target with unchanged tests; the synchronous optimizer blocking horizon +is explicitly **UNKNOWN** until provider evidence. Original deadlines/sources and late-result +rejection remain. No old failure becomes a pass, and no numerical horizon is approved here. + +The existing trait, ordinary SDK send branch, B/backend policy, timers, slack and resource +constraints remain unchanged. Typed body errors and fresh-readiness/read safety remain required. +Full EZ-1 settlement, Follow, engine and atomic head-first/full-trait migration remain open. +The external numerical-policy/H(K), hosted evidence and performance-comparison gates below +still block qualification; an unknown horizon cannot satisfy their finite-bound assertions. + +- [Requirements](../../../docs/superpowers/specs/2026-10-17-edgezero-pr275-requirements.md) + define acceptance; [G1 inventory](outbound-cutover-cases.json) maps criteria to environments, + sentinels, owners and applicability decisions. Its 255 criteria / 511 planned legs are a + coverage domain, **not 511 passing tests**. +- [G2 feasibility](../../../docs/audits/2026-10-18-outbound-feasibility.md) remains failed. + Fastly timed readiness loses body errors in pinned Viceroy. The atomic outbound migration + remains blocked; no blocking-read fallback, compatibility path or pin change is authorized. + A separately identified [local runtime repair](../../../patches/viceroy/README.md) fixes the + measured error loss in a patched executable. It does not qualify stock 0.21.0, the full G2 + scenario set or hosted Fastly; remaining exact-version/policy review stays open. + The [expanded local matrix](../../../docs/audits/evidence/2026-10-23-fastly-readiness-matrix.json) + executes all eleven diagnostic scenarios but retains a repaired-runtime result beyond +5 ms. + Error retention is repaired locally; timing acceptance and qualification remain failed/open. +- [G4 approval record](../../../docs/audits/2026-10-18-outbound-api-approval.md) records AG's + API-only approval of Interfaces SHA-256 + `c623e7fc5b189e2d9122756f29297a92031a9936b13c6cc475484d4e028a4a7a`. + Fastly primitive/policy including literal B, numerical H(K), applicability and measurement + approvals remain separate pending decisions. Historical requirements/inventory hashes in + that record are not refreshed approvals of later source changes. +- [Qualification plan](../../../docs/superpowers/plans/2026-10-18-trusted-server-outbound-05-qualification.md) + owns Q2 source preparation, Q3 freeze/local acceptance, Q4a package acceptance, Q4b execution + and Q5 publication. No candidate has been frozen or package accepted by this template. +- Full outbound case execution and frozen-candidate export are not implemented. Existing + response-egress probes do not replace them. Recorded Fastly SDK framing/bodyless parity + failures and Cloudflare front-proxy overflow-recovery failures are not waived. + +Do not start qualification until all prerequisite implementation, feasibility, policy and +source-producing work is complete. Deployment additionally requires the actual execution +owner's acceptance, explicit staging authorization and required entitlements below. + +## External Candidate and Acceptance Sheet + +Every `UNSET` blocks the dependent step; an operator must supply and review facts, not infer +them from a successful command. Preserve separate source, build-input, binary and deployment +identities. Record unavailable provider revisions as typed, reasoned unknowns. + +| Field | Required External Record | +| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Q3 freeze and local acceptance | UNSET: freeze reference, clean 40-character candidate SHA, authoritative per-leg local evidence | +| Baseline and prerequisite | UNSET: full immutable identities resolving TS baseline `a9037770a` and prerequisite PR 1123 `4c5ca8ae5` | +| Reviewed source subjects | UNSET: requirements/inventory digests, approved Interfaces digest, reviewed changes since historical approval packet | +| G4 decisions | UNSET: separate API, primitive/B, H(K), applicability and measurement decision IDs, scope, exact subject digest, approver, date and evidence reference | +| Build identities | UNSET: lockfiles/build-input SHA-256, commands/features/target, runner image, binary and packaged/deployed artifact SHA-256 | +| Runtime identities | UNSET: actual SDK/CLI/runtime revisions, compatibility date/flags, deployment/service identity and POP where observable | +| Execution ownership | UNSET: named EdgeZero preparer and actual TS maintainer/operator, dated ownership acknowledgement, reachable runbook/work-item link | +| Package acceptance | UNSET: TS acceptance date, accepted external runbook/package digest and candidate/build identities | +| Staging authorization | UNSET: issuer, date, evidence reference, isolated service and permitted actions; no production traffic/stores | +| Entitlements and origins | UNSET: required feature entitlements, controlled origins, approved delays/failure modes and non-secret settings references | +| Execution mapping | UNSET: each G1 leg, actual command/cwd, unique sentinel, expected assertions/case counts, samples/thresholds where required, artifact and owner | +| Measurement approvals | UNSET: M1/M2/M5/M7 comparison definitions, thresholds, sampling/randomization, 14b control and H(K) numerical rule/domain/maximum/tolerance | +| Image performance comparison | UNSET: matched TS/SDK-only controls, asset/profile corpus, cache classification, GET/HEAD/S3 groups, instrumentation, samples and acceptance/noise rules | +| Handoff and retention | UNSET: sanitized artifact manifest, independent review, durable non-expiring retention and consumer confirmation | + +Use the frozen values from the external record; never substitute the current `HEAD`: + +```sh +set -eu +: "${CANDIDATE_SHA:?Q3 external freeze record required}" +: "${BUILD_INPUT_SHA256:?reviewed build-input identity required}" +actual_sha=$(git rev-parse --verify HEAD) +test "$actual_sha" = "$CANDIDATE_SHA" +source_status=$(git status --porcelain --untracked-files=all) +test -z "$source_status" +``` + +Run from the candidate checkout root. Record the full command/environment and verify exact +SHA/digest syntax and source-to-build provenance independently. A source change after freeze +requires a new freeze and **all required local and hosted legs rerun**, not affected-only tests. +Freeze, package acceptance and release/push authorization are different decisions. + +## Implemented Preparation Commands + +These commands are runnable now from the repository root; their results are development checks, +not exact-candidate acceptance. Install the worktree's `.tool-versions` and fetch all three +locked Cargo workspaces as described in Q2 before offline checks. + +```sh +node --test scripts/check_outbound_cutover_evidence.test.mjs +node scripts/check_outbound_cutover_evidence.mjs --manifest +node --test scripts/check_forwarding_example.test.mjs +node scripts/check_forwarding_example.mjs +scripts/run_test_nonzero.sh outbound_evidence_assembly_retains_case_counts_and_hashes cargo test --offline --locked --manifest-path crates/edgezero-test-harness/Cargo.toml outbound_evidence_assembly_retains_case_counts_and_hashes +``` + +Expected: nonzero Node test listings with zero failures, exactly one passing non-ignored +forwarding doctest, exactly one passing named Rust sentinel, and inventory totals of +255 criteria / 511 planned legs / `qualified:false`. Record actual test counts for the frozen +candidate. A manifest pass deliberately permits pending decisions and planned commands. + +Also run Q2's five primary Rust gates, excluded demo/generated/fixture checks, seven adapter +feature matrices and actual target sentinels. Those exact commands remain in the qualification +plan; compilation, routing sentinels and local smoke are not hosted outbound acceptance. + +Worktree tool pins are Rust 1.98.1, Node 24.20.0, Fastly CLI 16.0.0, Spin 4.1.0, +standalone Viceroy 0.21.0 and Wasmtime 48.0.2. SDK requirements are Fastly 0.13.1, +worker 0.8.5 and spin-sdk 7.0.0 from `stackpop/spin-rust-sdk` at +`e65ff90dadf4173b08b14194e878ad3acd555337` (`wasip3 0.9.0` / `wit-bindgen 0.62.0`). +Record actual executable identities rather than borrowing +historical paths. Local fixture runners accept `VICEROY_BIN`, `SPIN_BIN`, `WRANGLER_BIN` and +`WORKER_BUILD_BIN`; Cloudflare also requires explicit absolute `WASM_BINDGEN_BIN`, +`ESBUILD_BIN` and `WASM_OPT_BIN`. Preserve `WRANGLER_SEND_METRICS=false` and a scoped +`WRANGLER_LOG_PATH`. See the [fixture README](README.md) for runtime preflight and limitations. + +## Existing Response-Egress Fixture Only + +The following builds select the implemented lifecycle fixture, **not the planned full +outbound qualification package**. These commands may be checked locally before freeze using +explicit characterization identities; those builds cannot be relabelled frozen evidence. +For accepted execution use only the Q3 sheet's clean candidate and reviewed build inputs. + +```sh +export EDGEZERO_QUALIFICATION_REVISION="$CANDIDATE_SHA" +export EDGEZERO_QUALIFICATION_ARTIFACT="$BUILD_INPUT_SHA256" +cargo build --offline --locked --release --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-cloudflare --features qualification --target wasm32-unknown-unknown +cargo build --offline --locked --release --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-fastly --features qualification --bin fixture-fastly-qualification --target wasm32-wasip1 +cargo build --offline --locked --release --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-spin --features qualification --target wasm32-wasip2 +cargo build --offline --locked --release --manifest-path tests/fixtures/reusable-app/Cargo.toml -p fixture-axum --features qualification +``` + +`EDGEZERO_QUALIFICATION_ARTIFACT` is a lockfile/build-input digest, **not a binary hash**. +After building, record actual binary/package SHA-256 separately. Cloudflare's Cargo output +alone is not its deployable worker package; record the generated shim, WASM and configuration +identities. TS must confirm the actual isolated-service packaging/deployment command and +settings. This template supplies no deployment authorization or invented deployment recipe. + +Only after accepted target configuration and explicit staging authorization: + +```sh +node scripts/qualify_response_egress.mjs --execute-hosted TARGET.json CLIENTS.json +node scripts/qualify_response_egress.mjs --validate CLIENTS.json EVENTS.json +``` + +Use the README's strict target JSON shape with actual allowlisted HTTPS origin, SDK/runtime, +compatibility, candidate, artifact and deployment facts. Thirteen scheduled scenarios plus +thirteen healthy recovery requests produce **26 client observations**; a complete fixture run +requires four lifecycle events per request, **104 events**. These are not the 511 G1 legs or +measurement sample counts. No outbound backend is contacted by this fixture. + +Collection intentionally exits **2**, not a qualification pass. Export complete unsampled +events independently; only an independently confirmed complete export permits setting +`telemetry_complete`. Do not discard failures or duplicate/conflicting events. Validation can +qualify ownership for the scheduled probes only: `HostHandoff` is not client delivery, instance +identity is unobserved, and recovery does not prove same-isolate capacity recovery. Missing +transport/parser/allocation evidence remains separate. Fastly requires an independently enforced +host execution ceiling of 1..60000 ms; client budget is 1000..10000 ms and the fixture server +response budget is 500 ms. Before/after checks cannot interrupt a blocking host call. + +## Full Outbound Execution Bindings: Not Implemented + +G1 strings containing `outbound-cutover`, `--case`, `hosted-runbook` or `consumer-runbook` +are planned recipes, not commands to copy into a terminal. Q3/Q4a must replace them with +owner-confirmed runnable invocations before a leg is ready. No full outbound probe or +frozen-record export invocation is available at this checkpoint. + +For every applicable leg, the accepted external package must bind: case/environment/source +digest, unique sentinel, command/cwd, owner, candidate/build identity, expected assertions and +case count, pre-approved sample count/threshold where required, raw artifact and export path. Include explicit +approved-deferred mappings without counting them as passed executions. Do not substitute a +positive sample number for a complete expected-case count or invent absent TS commands. + +Required hosted coverage includes EZ-Q1.4's EZ-1 body/deadline cases, EZ-2 criteria +11/12/13b/14a/14b, EZ-4 criterion 8, EZ-6 scenarios 1/2/6/7/8/11 and Image Optimizer. +TS confirms canonical `run_custom` plus `send_request_with_registries_and_hooks` composition, +single-request mode by default, and its deployment/browser/response-diff/export commands. +Its known integration command, **from the TS checkout**, is: + +```sh +cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test integration -- --include-ignored +``` + +Preserve these distinct measurement obligations from the requirements and Q4b: + +- Confirm M1/M2/M5/M7 product comparisons, samples, settings and thresholds before replay. + M3's registration-count bound is a fixed pass/fail invariant; its timing distribution is + report-only, not subject to a product threshold. M6 keeps criteria 4/5's fixed deadline + windows. Preserve ordinary deadline plus 5 ms acceptance windows and original on-time-success + rules; these do not become negotiable product thresholds. +- Confirm EZ-8.12's native-path comparison separately: identical region/options/image bytes, + negotiation and transport/cache settings; verified cache-hit/miss groups; GET/HEAD and + application-owned S3 preflight groups; matched client rate/concurrency and randomized build + order. Record latency distributions, throughput, allocation/copy/buffering and host-call counts, + plus origin receipts/connection identities where observable. Disclose unavailable counters and + agree how they will be evidenced before replay. Neither SDK-shaped code, Viceroy rejection nor + an unmeasured/missing control proves performance parity. Do not reuse bidder-latency tolerance. +- For 14b, both SDK-only control and candidate need positive cross-execution origin connection + IDs across fresh single-request executions. Registration counts, POP or TLS resumption alone + do not prove pooling. A negative/inconclusive control blocks qualification. Separately deferred + M4/reused-sandbox work does not waive 14b or approve reuse. +- Pre-approve literal B/domain and numerical H(K) rule/domain/maximum/tolerance with exact + subject digests. Record original entry t0, original D/BudgetSource, preparation, host-call and + total timings. Exercise the actual optimizer host call and timely-head/pending-body case; + pre-dispatch expiry is not a host-call proof. Validate both original-entry and host-entry + inequalities; never select a horizon or inflate thresholds after seeing results. + +## Draft Assembly, Validation and Release + +The existing tool consumes explicit observations, not generic smoke JSONL/FNV summaries: + +```sh +node scripts/check_outbound_cutover_evidence.mjs --assemble --candidate "$CANDIDATE_SHA" --input OBSERVATIONS.json --artifacts EVIDENCE_DIRECTORY --output NEW_DRAFT.json +node scripts/check_outbound_cutover_evidence.mjs --report --candidate "$CANDIDATE_SHA" --record NEW_DRAFT.json --artifacts EVIDENCE_DIRECTORY +node scripts/check_outbound_cutover_evidence.mjs --release --candidate "$CANDIDATE_SHA" --record FROZEN_RECORD.json --artifacts EVIDENCE_DIRECTORY +``` + +`--assemble` preserves supplied counts/outcomes and verifies linked build/raw-artifact SHA-256; +it writes a new `stage:"pre-freeze", qualified:false` draft and exits nonzero for open gates. +`--report` and `--release` share release rules. A successful check does **not** authenticate +telemetry/approvers, prove source-to-binary provenance, authorize execution/publication, establish +criterion-specific H(K)/pooling thresholds, or independently grant qualification. Their output's +`qualified:false` is not changed by success. Frozen-candidate export/mapping remains unimplemented; +changing a draft label does not perform freeze, execution, approval or export. + +Use stable sanitized file snapshots: checker JSON is capped at 16 MiB; raw artifacts at 256 MiB, +hashed with 64 KiB scratch. The checker rejects artifact symlink/path escapes and refuses output +overwrites. The separate hosted-egress tool caps evidence inputs at 4 MiB and also refuses existing +output files; use operator-controlled paths. Exclude credentials, signing material, raw application +payloads and diagnostic URLs. The tools do not protect against fabricated telemetry or authenticate +provider identity; the checker also requires files not be mutated concurrently. Review independently. + +Q5 additionally requires every required leg and pre-approved measurement to pass, applicability +and consumer confirmation, exact unchanged candidate/build identities, and separately authorized +publication to a reachable immutable tag with durable non-expiring artifacts. Verify remote +hashes and retention after publication. Missing command, owner, authorization, entitlement, +approval or evidence leaves qualification blocked; neither a green CI run nor delivery of this +template closes Q4/Q5 or promotes BestEffort/Unsupported guarantees.